Compare commits

...

6 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
caf9e26acd Fix copyright year to 2026 and revert data.json changes
Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com>
Agent-Logs-Url: https://github.com/casdoor/casdoor/sessions/700523a4-b522-4380-ba0d-a073a8cf8186
2026-03-21 11:52:51 +00:00
Yang Luo
d42a9b95ce Update ormer.go 2026-03-21 19:47:14 +08:00
Yang Luo
4caa3008b0 Merge branch 'master' into copilot/add-apikeys-management 2026-03-21 19:45:32 +08:00
copilot-swe-agent[bot]
82136edabd Fix inconsistent key state label (Inactive vs Suspended)
Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com>
2026-03-18 14:13:16 +00:00
copilot-swe-agent[bot]
ff7b335acb Add Key management feature: backend Go code and frontend JS pages
Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com>
2026-03-18 14:08:35 +00:00
copilot-swe-agent[bot]
5c62be5aac Initial plan 2026-03-18 13:48:22 +00:00
9 changed files with 1083 additions and 2 deletions

206
controllers/key.go Normal file
View File

@@ -0,0 +1,206 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/json"
"github.com/beego/beego/v2/core/utils/pagination"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// GetKeys
// @Title GetKeys
// @Tag Key API
// @Description get keys
// @Param owner query string true "The owner of keys"
// @Success 200 {array} object.Key The Response object
// @router /get-keys [get]
func (c *ApiController) GetKeys() {
owner := c.Ctx.Input.Query("owner")
limit := c.Ctx.Input.Query("pageSize")
page := c.Ctx.Input.Query("p")
field := c.Ctx.Input.Query("field")
value := c.Ctx.Input.Query("value")
sortField := c.Ctx.Input.Query("sortField")
sortOrder := c.Ctx.Input.Query("sortOrder")
if limit == "" || page == "" {
keys, err := object.GetKeys(owner)
if err != nil {
c.ResponseError(err.Error())
return
}
maskedKeys, err := object.GetMaskedKeys(keys, true, nil)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(maskedKeys)
} else {
limit := util.ParseInt(limit)
count, err := object.GetKeyCount(owner, field, value)
if err != nil {
c.ResponseError(err.Error())
return
}
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
keys, err := object.GetPaginationKeys(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
if err != nil {
c.ResponseError(err.Error())
return
}
maskedKeys, err := object.GetMaskedKeys(keys, true, nil)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(maskedKeys, paginator.Nums())
}
}
// GetGlobalKeys
// @Title GetGlobalKeys
// @Tag Key API
// @Description get global keys
// @Success 200 {array} object.Key The Response object
// @router /get-global-keys [get]
func (c *ApiController) GetGlobalKeys() {
limit := c.Ctx.Input.Query("pageSize")
page := c.Ctx.Input.Query("p")
field := c.Ctx.Input.Query("field")
value := c.Ctx.Input.Query("value")
sortField := c.Ctx.Input.Query("sortField")
sortOrder := c.Ctx.Input.Query("sortOrder")
if limit == "" || page == "" {
keys, err := object.GetGlobalKeys()
if err != nil {
c.ResponseError(err.Error())
return
}
maskedKeys, err := object.GetMaskedKeys(keys, true, nil)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(maskedKeys)
} else {
limit := util.ParseInt(limit)
count, err := object.GetGlobalKeyCount(field, value)
if err != nil {
c.ResponseError(err.Error())
return
}
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
keys, err := object.GetPaginationGlobalKeys(paginator.Offset(), limit, field, value, sortField, sortOrder)
if err != nil {
c.ResponseError(err.Error())
return
}
maskedKeys, err := object.GetMaskedKeys(keys, true, nil)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(maskedKeys, paginator.Nums())
}
}
// GetKey
// @Title GetKey
// @Tag Key API
// @Description get key
// @Param id query string true "The id ( owner/name ) of the key"
// @Success 200 {object} object.Key The Response object
// @router /get-key [get]
func (c *ApiController) GetKey() {
id := c.Ctx.Input.Query("id")
key, err := object.GetKey(id)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(key)
}
// UpdateKey
// @Title UpdateKey
// @Tag Key API
// @Description update key
// @Param id query string true "The id ( owner/name ) of the key"
// @Param body body object.Key true "The details of the key"
// @Success 200 {object} controllers.Response The Response object
// @router /update-key [post]
func (c *ApiController) UpdateKey() {
id := c.Ctx.Input.Query("id")
var key object.Key
err := json.Unmarshal(c.Ctx.Input.RequestBody, &key)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.UpdateKey(id, &key))
c.ServeJSON()
}
// AddKey
// @Title AddKey
// @Tag Key API
// @Description add key
// @Param body body object.Key true "The details of the key"
// @Success 200 {object} controllers.Response The Response object
// @router /add-key [post]
func (c *ApiController) AddKey() {
var key object.Key
err := json.Unmarshal(c.Ctx.Input.RequestBody, &key)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddKey(&key))
c.ServeJSON()
}
// DeleteKey
// @Title DeleteKey
// @Tag Key API
// @Description delete key
// @Param body body object.Key true "The details of the key"
// @Success 200 {object} controllers.Response The Response object
// @router /delete-key [post]
func (c *ApiController) DeleteKey() {
var key object.Key
err := json.Unmarshal(c.Ctx.Input.RequestBody, &key)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.DeleteKey(&key))
c.ServeJSON()
}

208
object/key.go Normal file
View File

@@ -0,0 +1,208 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"github.com/casdoor/casdoor/util"
"github.com/xorm-io/core"
)
type Key struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
// Type indicates the scope this key belongs to: "Organization", "Application", or "User"
Type string `xorm:"varchar(100)" json:"type"`
Organization string `xorm:"varchar(100)" json:"organization"`
Application string `xorm:"varchar(100)" json:"application"`
User string `xorm:"varchar(100)" json:"user"`
AccessKey string `xorm:"varchar(100) index" json:"accessKey"`
AccessSecret string `xorm:"varchar(100)" json:"accessSecret"`
ExpireTime string `xorm:"varchar(100)" json:"expireTime"`
State string `xorm:"varchar(100)" json:"state"`
}
func GetKeyCount(owner, field, value string) (int64, error) {
session := GetSession(owner, -1, -1, field, value, "", "")
return session.Count(&Key{})
}
func GetGlobalKeyCount(field, value string) (int64, error) {
session := GetSession("", -1, -1, field, value, "", "")
return session.Count(&Key{})
}
func GetKeys(owner string) ([]*Key, error) {
keys := []*Key{}
err := ormer.Engine.Desc("created_time").Find(&keys, &Key{Owner: owner})
if err != nil {
return keys, err
}
return keys, nil
}
func GetPaginationKeys(owner string, offset, limit int, field, value, sortField, sortOrder string) ([]*Key, error) {
keys := []*Key{}
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&keys)
if err != nil {
return keys, err
}
return keys, nil
}
func GetGlobalKeys() ([]*Key, error) {
keys := []*Key{}
err := ormer.Engine.Desc("created_time").Find(&keys)
if err != nil {
return keys, err
}
return keys, nil
}
func GetPaginationGlobalKeys(offset, limit int, field, value, sortField, sortOrder string) ([]*Key, error) {
keys := []*Key{}
session := GetSession("", offset, limit, field, value, sortField, sortOrder)
err := session.Find(&keys)
if err != nil {
return keys, err
}
return keys, nil
}
func getKey(owner, name string) (*Key, error) {
if owner == "" || name == "" {
return nil, nil
}
key := Key{Owner: owner, Name: name}
existed, err := ormer.Engine.Get(&key)
if err != nil {
return &key, err
}
if existed {
return &key, nil
}
return nil, nil
}
func GetKey(id string) (*Key, error) {
owner, name, err := util.GetOwnerAndNameFromIdWithError(id)
if err != nil {
return nil, err
}
return getKey(owner, name)
}
func GetMaskedKey(key *Key, isMaskEnabled bool) *Key {
if !isMaskEnabled {
return key
}
if key == nil {
return nil
}
if key.AccessSecret != "" {
key.AccessSecret = "***"
}
return key
}
func GetMaskedKeys(keys []*Key, isMaskEnabled bool, err error) ([]*Key, error) {
if err != nil {
return nil, err
}
for _, key := range keys {
GetMaskedKey(key, isMaskEnabled)
}
return keys, nil
}
func UpdateKey(id string, key *Key) (bool, error) {
owner, name, err := util.GetOwnerAndNameFromIdWithError(id)
if err != nil {
return false, err
}
if k, err := getKey(owner, name); err != nil {
return false, err
} else if k == nil {
return false, nil
}
key.UpdatedTime = util.GetCurrentTime()
affected, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(key)
if err != nil {
return false, err
}
return affected != 0, nil
}
func AddKey(key *Key) (bool, error) {
if key.AccessKey == "" {
key.AccessKey = util.GenerateId()
}
if key.AccessSecret == "" {
key.AccessSecret = util.GenerateId()
}
affected, err := ormer.Engine.Insert(key)
if err != nil {
return false, err
}
return affected != 0, nil
}
func DeleteKey(key *Key) (bool, error) {
affected, err := ormer.Engine.ID(core.PK{key.Owner, key.Name}).Delete(&Key{})
if err != nil {
return false, err
}
return affected != 0, nil
}
func (key *Key) GetId() string {
return fmt.Sprintf("%s/%s", key.Owner, key.Name)
}
func GetKeyByAccessKey(accessKey string) (*Key, error) {
if accessKey == "" {
return nil, nil
}
key := Key{AccessKey: accessKey}
existed, err := ormer.Engine.Get(&key)
if err != nil {
return nil, err
}
if existed {
return &key, nil
}
return nil, nil
}

View File

@@ -343,6 +343,11 @@ func (a *Ormer) createTable() {
panic(err)
}
err = a.Engine.Sync2(new(Key))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Role))
if err != nil {
panic(err)

View File

@@ -155,6 +155,13 @@ func InitAPI() {
web.Router("/api/delete-cert", &controllers.ApiController{}, "POST:DeleteCert")
web.Router("/api/update-cert-domain-expire", &controllers.ApiController{}, "POST:UpdateCertDomainExpire")
web.Router("/api/get-keys", &controllers.ApiController{}, "GET:GetKeys")
web.Router("/api/get-global-keys", &controllers.ApiController{}, "GET:GetGlobalKeys")
web.Router("/api/get-key", &controllers.ApiController{}, "GET:GetKey")
web.Router("/api/update-key", &controllers.ApiController{}, "POST:UpdateKey")
web.Router("/api/add-key", &controllers.ApiController{}, "POST:AddKey")
web.Router("/api/delete-key", &controllers.ApiController{}, "POST:DeleteKey")
web.Router("/api/get-roles", &controllers.ApiController{}, "GET:GetRoles")
web.Router("/api/get-role", &controllers.ApiController{}, "GET:GetRole")
web.Router("/api/update-role", &controllers.ApiController{}, "POST:UpdateRole")

View File

@@ -174,7 +174,7 @@ class App extends Component {
const validMenuItems = [
"/", "/shortcuts", "/apps", // Home group
"/organizations", "/groups", "/users", "/invitations", // User Management
"/applications", "/providers", "/resources", "/certs", // Identity
"/applications", "/providers", "/resources", "/certs", "/keys", // Identity
"/roles", "/permissions", "/models", "/adapters", "/enforcers", // Authorization
"/sessions", "/records", "/tokens", "/verifications", // Logging & Auditing
"/products", "/orders", "/payments", "/plans", "/pricings", "/subscriptions", "/transactions", // Business
@@ -215,6 +215,8 @@ class App extends Component {
} else if (uri.includes("/certs")) {
return "/certs";
}
} else if (uri.includes("/keys")) {
return "/keys";
} else if (uri.includes("/roles") || uri.includes("/permissions") || uri.includes("/models") || uri.includes("/adapters") || uri.includes("/enforcers")) {
if (uri.includes("/roles")) {
return "/roles";
@@ -293,7 +295,7 @@ class App extends Component {
this.setState({selectedMenuKey: "/home"});
} else if (uri.includes("/organizations") || uri.includes("/trees") || uri.includes("/groups") || uri.includes("/users") || uri.includes("/invitations")) {
this.setState({selectedMenuKey: "/orgs"});
} else if (uri.includes("/applications") || uri.includes("/providers") || uri.includes("/resources") || uri.includes("/certs")) {
} else if (uri.includes("/applications") || uri.includes("/providers") || uri.includes("/resources") || uri.includes("/certs") || uri.includes("/keys")) {
this.setState({selectedMenuKey: "/identity"});
} else if (uri.includes("/roles") || uri.includes("/permissions") || uri.includes("/models") || uri.includes("/adapters") || uri.includes("/enforcers")) {
this.setState({selectedMenuKey: "/auth"});

312
web/src/KeyEditPage.js Normal file
View File

@@ -0,0 +1,312 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Card, Col, DatePicker, Input, Row, Select} from "antd";
import * as KeyBackend from "./backend/KeyBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as ApplicationBackend from "./backend/ApplicationBackend";
import * as UserBackend from "./backend/UserBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
import moment from "moment";
const {Option} = Select;
class KeyEditPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
organizationName: props.match.params.organizationName,
keyName: props.match.params.keyName,
key: null,
organizations: [],
applications: [],
users: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
UNSAFE_componentWillMount() {
this.getKey();
this.getOrganizations();
}
getKey() {
KeyBackend.getKey(this.state.organizationName, this.state.keyName)
.then((res) => {
if (res.data === null) {
this.props.history.push("/404");
return;
}
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
key: res.data,
});
this.getApplicationsByOrganization(res.data.organization || this.state.organizationName);
this.getUsersByOrganization(res.data.organization || this.state.organizationName);
});
}
getOrganizations() {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: res.data || [],
});
});
}
getApplicationsByOrganization(organizationName) {
ApplicationBackend.getApplicationsByOrganization("admin", organizationName)
.then((res) => {
this.setState({
applications: res.data || [],
});
});
}
getUsersByOrganization(organizationName) {
UserBackend.getUsers(organizationName)
.then((res) => {
if (res.status === "ok") {
this.setState({
users: res.data || [],
});
}
});
}
parseKeyField(key, value) {
return value;
}
updateKeyField(key, value) {
value = this.parseKeyField(key, value);
const keyObj = this.state.key;
keyObj[key] = value;
this.setState({
key: keyObj,
});
}
renderKey() {
return (
<Card size="small" title={
<div>
{this.state.mode === "add" ? i18next.t("key:New Key") : i18next.t("key:Edit Key")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitKeyEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitKeyEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteKey()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
<Row style={{marginTop: "10px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} disabled={!Setting.isAdminUser(this.props.account)} value={this.state.key.owner} onChange={(value => {
this.updateKeyField("owner", value);
this.updateKeyField("organization", value);
this.getApplicationsByOrganization(value);
this.getUsersByOrganization(value);
})}>
{
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.key.name} onChange={e => {
this.updateKeyField("name", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.key.displayName} onChange={e => {
this.updateKeyField("displayName", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Type"), i18next.t("general:Type - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.key.type} onChange={(value => {
this.updateKeyField("type", value);
})}>
<Option value="Organization">{i18next.t("general:Organization")}</Option>
<Option value="Application">{i18next.t("general:Application")}</Option>
<Option value="User">{i18next.t("general:User")}</Option>
</Select>
</Col>
</Row>
{
this.state.key.type === "Application" ? (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Application"), i18next.t("general:Application - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.key.application} onChange={(value => {
this.updateKeyField("application", value);
})}>
{
this.state.applications.map((application, index) => <Option key={index} value={application.name}>{application.name}</Option>)
}
</Select>
</Col>
</Row>
) : null
}
{
this.state.key.type === "User" ? (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:User"), i18next.t("general:User - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.key.user} onChange={(value => {
this.updateKeyField("user", value);
})}>
{
this.state.users.map((user, index) => <Option key={index} value={user.name}>{user.name}</Option>)
}
</Select>
</Col>
</Row>
) : null
}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("key:Access key"), i18next.t("key:Access key - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.key.accessKey} readOnly={true} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("key:Access secret"), i18next.t("key:Access secret - Tooltip"))} :
</Col>
<Col span={22} >
<Input.Password value={this.state.key.accessSecret} readOnly={true} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Expire time"), i18next.t("general:Expire time - Tooltip"))} :
</Col>
<Col span={22} >
<DatePicker
showTime
value={this.state.key.expireTime ? moment(this.state.key.expireTime) : null}
onChange={(value, dateString) => {
this.updateKeyField("expireTime", dateString);
}}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:State"), i18next.t("general:State - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.key.state} onChange={(value => {
this.updateKeyField("state", value);
})}>
<Option value="Active">{i18next.t("subscription:Active")}</Option>
<Option value="Inactive">{i18next.t("key:Inactive")}</Option>
</Select>
</Col>
</Row>
</Card>
);
}
submitKeyEdit(exitAfterSave) {
const key = Setting.deepCopy(this.state.key);
KeyBackend.updateKey(this.state.organizationName, this.state.keyName, key)
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully saved"));
this.setState({
organizationName: this.state.key.owner,
keyName: this.state.key.name,
});
if (exitAfterSave) {
this.props.history.push("/keys");
} else {
this.props.history.push(`/keys/${this.state.key.owner}/${this.state.key.name}`);
}
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
this.updateKeyField("owner", this.state.organizationName);
this.updateKeyField("name", this.state.keyName);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteKey() {
KeyBackend.deleteKey(this.state.key)
.then((res) => {
if (res.status === "ok") {
this.props.history.push("/keys");
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
render() {
return (
<div>
{
this.state.key !== null ? this.renderKey() : null
}
<div style={{marginTop: "20px", marginLeft: "40px"}}>
<Button size="large" onClick={() => this.submitKeyEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitKeyEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
</div>
</div>
);
}
}
export default KeyEditPage;

253
web/src/KeyListPage.js Normal file
View File

@@ -0,0 +1,253 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Link} from "react-router-dom";
import {Button, Table} from "antd";
import moment from "moment";
import * as Setting from "./Setting";
import * as KeyBackend from "./backend/KeyBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class KeyListPage extends BaseListPage {
newKey() {
const randomName = Setting.getRandomName();
const owner = Setting.getRequestOrganization(this.props.account);
return {
owner: owner,
name: `key_${randomName}`,
createdTime: moment().format(),
updatedTime: moment().format(),
displayName: `New Key - ${randomName}`,
type: "Organization",
organization: owner,
application: "",
user: "",
accessKey: "",
accessSecret: "",
expireTime: "",
state: "Active",
};
}
addKey() {
const newKey = this.newKey();
KeyBackend.addKey(newKey)
.then((res) => {
if (res.status === "ok") {
this.props.history.push({pathname: `/keys/${newKey.owner}/${newKey.name}`, mode: "add"});
Setting.showMessage("success", i18next.t("general:Successfully added"));
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteKey(i) {
KeyBackend.deleteKey(this.state.data[i])
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.fetch({
pagination: {
...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
});
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
renderTable(keys) {
const columns = [
{
title: i18next.t("general:Name"),
dataIndex: "name",
key: "name",
width: "140px",
fixed: "left",
sorter: true,
...this.getColumnSearchProps("name"),
render: (text, record, index) => {
return (
<Link to={`/keys/${record.owner}/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Organization"),
dataIndex: "owner",
key: "owner",
width: "150px",
sorter: true,
...this.getColumnSearchProps("owner"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Created time"),
dataIndex: "createdTime",
key: "createdTime",
width: "160px",
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
},
},
{
title: i18next.t("general:Display name"),
dataIndex: "displayName",
key: "displayName",
width: "170px",
sorter: true,
...this.getColumnSearchProps("displayName"),
},
{
title: i18next.t("general:Type"),
dataIndex: "type",
key: "type",
width: "140px",
sorter: true,
filterMultiple: false,
filters: [
{text: i18next.t("general:Organization"), value: "Organization"},
{text: i18next.t("general:Application"), value: "Application"},
{text: i18next.t("general:User"), value: "User"},
],
},
{
title: i18next.t("key:Access key"),
dataIndex: "accessKey",
key: "accessKey",
width: "300px",
sorter: true,
...this.getColumnSearchProps("accessKey"),
},
{
title: i18next.t("general:Expire time"),
dataIndex: "expireTime",
key: "expireTime",
width: "160px",
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
},
},
{
title: i18next.t("general:State"),
dataIndex: "state",
key: "state",
width: "120px",
sorter: true,
...this.getColumnSearchProps("state"),
},
{
title: i18next.t("general:Action"),
dataIndex: "",
key: "op",
width: "180px",
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/keys/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<PopconfirmModal
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
onConfirm={() => this.deleteKey(index)}
>
</PopconfirmModal>
</div>
);
},
},
];
const paginationProps = {
total: this.state.pagination.total,
showQuickJumper: true,
showSizeChanger: true,
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
};
return (
<div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={keys} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Keys")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small" onClick={this.addKey.bind(this)}>{i18next.t("general:Add")}</Button>
</div>
)}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
</div>
);
}
fetch = (params = {}) => {
let field = params.searchedColumn, value = params.searchText;
const sortField = params.sortField, sortOrder = params.sortOrder;
if (params.type !== undefined && params.type !== null) {
field = "type";
value = params.type;
}
this.setState({loading: true});
(Setting.isDefaultOrganizationSelected(this.props.account) ? KeyBackend.getGlobalKeys(params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
: KeyBackend.getKeys(Setting.getRequestOrganization(this.props.account), params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder))
.then((res) => {
this.setState({
loading: false,
});
if (res.status === "ok") {
this.setState({
data: res.data,
pagination: {
...params.pagination,
total: res.data2,
},
searchText: params.searchText,
searchedColumn: params.searchedColumn,
});
} else {
if (Setting.isResponseDenied(res)) {
this.setState({
isAuthorized: false,
});
} else {
Setting.showMessage("error", res.msg);
}
}
});
};
}
export default KeyListPage;

View File

@@ -47,6 +47,8 @@ import RecordListPage from "./RecordListPage";
import ResourceListPage from "./ResourceListPage";
import CertListPage from "./CertListPage";
import CertEditPage from "./CertEditPage";
import KeyListPage from "./KeyListPage";
import KeyEditPage from "./KeyEditPage";
import RoleListPage from "./RoleListPage";
import RoleEditPage from "./RoleEditPage";
import PermissionListPage from "./PermissionListPage";
@@ -329,6 +331,9 @@ function ManagementPage(props) {
Setting.getItem(<Link to="/providers">{i18next.t("application:Providers")}</Link>, "/providers"),
Setting.getItem(<Link to="/resources">{i18next.t("general:Resources")}</Link>, "/resources"),
Setting.getItem(<Link to="/certs">{i18next.t("general:Certs")}</Link>, "/certs"),
Setting.getItem(<Link to="/keys">{i18next.t("general:Keys")}</Link>, "/keys"),
Setting.getItem(<Link to="/sites">{i18next.t("general:Sites")}</Link>, "/sites"),
Setting.getItem(<Link to="/rules">{i18next.t("general:Rules")}</Link>, "/rules"),
]));
res.push(Setting.getItem(<Link style={{color: textColor}} to="/roles">{i18next.t("general:Authorization")}</Link>, "/auth", <SafetyCertificateTwoTone twoToneColor={twoToneColor} />, [
@@ -485,6 +490,8 @@ function ManagementPage(props) {
<Route exact path="/resources" render={(props) => renderLoginIfNotLoggedIn(<ResourceListPage account={account} {...props} />)} />
<Route exact path="/certs" render={(props) => renderLoginIfNotLoggedIn(<CertListPage account={account} {...props} />)} />
<Route exact path="/certs/:organizationName/:certName" render={(props) => renderLoginIfNotLoggedIn(<CertEditPage account={account} {...props} />)} />
<Route exact path="/keys" render={(props) => renderLoginIfNotLoggedIn(<KeyListPage account={account} {...props} />)} />
<Route exact path="/keys/:organizationName/:keyName" render={(props) => renderLoginIfNotLoggedIn(<KeyEditPage account={account} {...props} />)} />
<Route exact path="/servers" render={(props) => renderLoginIfNotLoggedIn(<ServerListPage account={account} {...props} />)} />
<Route exact path="/servers/:organizationName/:serverName" render={(props) => renderLoginIfNotLoggedIn(<ServerEditPage account={account} {...props} />)} />
<Route exact path="/sites" render={(props) => renderLoginIfNotLoggedIn(<SiteListPage account={account} {...props} />)} />

View File

@@ -0,0 +1,81 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as Setting from "../Setting";
export function getKeys(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
return fetch(`${Setting.ServerUrl}/api/get-keys?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
method: "GET",
credentials: "include",
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function getGlobalKeys(page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
return fetch(`${Setting.ServerUrl}/api/get-global-keys?p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
method: "GET",
credentials: "include",
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function getKey(owner, name) {
return fetch(`${Setting.ServerUrl}/api/get-key?id=${owner}/${encodeURIComponent(name)}`, {
method: "GET",
credentials: "include",
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function updateKey(owner, name, key) {
const newKey = Setting.deepCopy(key);
return fetch(`${Setting.ServerUrl}/api/update-key?id=${owner}/${encodeURIComponent(name)}`, {
method: "POST",
credentials: "include",
body: JSON.stringify(newKey),
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function addKey(key) {
const newKey = Setting.deepCopy(key);
return fetch(`${Setting.ServerUrl}/api/add-key`, {
method: "POST",
credentials: "include",
body: JSON.stringify(newKey),
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function deleteKey(key) {
const newKey = Setting.deepCopy(key);
return fetch(`${Setting.ServerUrl}/api/delete-key`, {
method: "POST",
credentials: "include",
body: JSON.stringify(newKey),
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}