Add cert pages.

This commit is contained in:
Yang Luo 2021-12-31 00:36:36 +08:00
parent c1cd187558
commit ba1646a0c3
10 changed files with 832 additions and 1 deletions

116
controllers/cert.go Normal file
View File

@ -0,0 +1,116 @@
// Copyright 2021 The casbin 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/astaxie/beego/utils/pagination"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/util"
)
// GetCerts
// @Title GetCerts
// @Tag Cert API
// @Description get certs
// @Param owner query string true "The owner of certs"
// @Success 200 {array} object.Cert The Response object
// @router /get-certs [get]
func (c *ApiController) GetCerts() {
owner := c.Input().Get("owner")
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
value := c.Input().Get("value")
sortField := c.Input().Get("sortField")
sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" {
c.Data["json"] = object.GetMaskedCerts(object.GetCerts(owner))
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetCertCount(owner, field, value)))
certs := object.GetMaskedCerts(object.GetPaginationCerts(owner, paginator.Offset(), limit, field, value, sortField, sortOrder))
c.ResponseOk(certs, paginator.Nums())
}
}
// @Title GetCert
// @Tag Cert API
// @Description get cert
// @Param id query string true "The id of the cert"
// @Success 200 {object} object.Cert The Response object
// @router /get-cert [get]
func (c *ApiController) GetCert() {
id := c.Input().Get("id")
c.Data["json"] = object.GetMaskedCert(object.GetCert(id))
c.ServeJSON()
}
// @Title UpdateCert
// @Tag Cert API
// @Description update cert
// @Param id query string true "The id of the cert"
// @Param body body object.Cert true "The details of the cert"
// @Success 200 {object} controllers.Response The Response object
// @router /update-cert [post]
func (c *ApiController) UpdateCert() {
id := c.Input().Get("id")
var cert object.Cert
err := json.Unmarshal(c.Ctx.Input.RequestBody, &cert)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.UpdateCert(id, &cert))
c.ServeJSON()
}
// @Title AddCert
// @Tag Cert API
// @Description add cert
// @Param body body object.Cert true "The details of the cert"
// @Success 200 {object} controllers.Response The Response object
// @router /add-cert [post]
func (c *ApiController) AddCert() {
var cert object.Cert
err := json.Unmarshal(c.Ctx.Input.RequestBody, &cert)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.AddCert(&cert))
c.ServeJSON()
}
// @Title DeleteCert
// @Tag Cert API
// @Description delete cert
// @Param body body object.Cert true "The details of the cert"
// @Success 200 {object} controllers.Response The Response object
// @router /delete-cert [post]
func (c *ApiController) DeleteCert() {
var cert object.Cert
err := json.Unmarshal(c.Ctx.Input.RequestBody, &cert)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.DeleteCert(&cert))
c.ServeJSON()
}

View File

@ -145,6 +145,11 @@ func (a *Adapter) createTable() {
panic(err)
}
err = a.Engine.Sync2(new(Cert))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Ldap))
if err != nil {
panic(err)

146
object/cert.go Normal file
View File

@ -0,0 +1,146 @@
// Copyright 2021 The casbin 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/casbin/casdoor/util"
"xorm.io/core"
)
type Cert 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"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Scope string `xorm:"varchar(100)" json:"scope"`
Type string `xorm:"varchar(100)" json:"type"`
CryptoAlgorithm string `xorm:"varchar(100)" json:"cryptoAlgorithm"`
BitSize int `json:"bitSize"`
ExpireInYears int `json:"expireInYears"`
PublicKey string `xorm:"mediumtext" json:"publicKey"`
PrivateKey string `xorm:"mediumtext" json:"privateKey"`
}
func GetMaskedCert(cert *Cert) *Cert {
if cert == nil {
return nil
}
return cert
}
func GetMaskedCerts(certs []*Cert) []*Cert {
for _, cert := range certs {
cert = GetMaskedCert(cert)
}
return certs
}
func GetCertCount(owner, field, value string) int {
session := adapter.Engine.Where("owner=?", owner)
if field != "" && value != "" {
session = session.And(fmt.Sprintf("%s like ?", util.SnakeString(field)), fmt.Sprintf("%%%s%%", value))
}
count, err := session.Count(&Cert{})
if err != nil {
panic(err)
}
return int(count)
}
func GetCerts(owner string) []*Cert {
certs := []*Cert{}
err := adapter.Engine.Desc("created_time").Find(&certs, &Cert{Owner: owner})
if err != nil {
panic(err)
}
return certs
}
func GetPaginationCerts(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Cert {
certs := []*Cert{}
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&certs)
if err != nil {
panic(err)
}
return certs
}
func getCert(owner string, name string) *Cert {
if owner == "" || name == "" {
return nil
}
cert := Cert{Owner: owner, Name: name}
existed, err := adapter.Engine.Get(&cert)
if err != nil {
panic(err)
}
if existed {
return &cert
} else {
return nil
}
}
func GetCert(id string) *Cert {
owner, name := util.GetOwnerAndNameFromId(id)
return getCert(owner, name)
}
func UpdateCert(id string, cert *Cert) bool {
owner, name := util.GetOwnerAndNameFromId(id)
if getCert(owner, name) == nil {
return false
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(cert)
if err != nil {
panic(err)
}
return affected != 0
}
func AddCert(cert *Cert) bool {
affected, err := adapter.Engine.Insert(cert)
if err != nil {
panic(err)
}
return affected != 0
}
func DeleteCert(cert *Cert) bool {
affected, err := adapter.Engine.ID(core.PK{cert.Owner, cert.Name}).Delete(&Cert{})
if err != nil {
panic(err)
}
return affected != 0
}
func (p *Cert) GetId() string {
return fmt.Sprintf("%s/%s", p.Owner, p.Name)
}

View File

@ -128,6 +128,12 @@ func initAPI() {
beego.Router("/api/add-syncer", &controllers.ApiController{}, "POST:AddSyncer")
beego.Router("/api/delete-syncer", &controllers.ApiController{}, "POST:DeleteSyncer")
beego.Router("/api/get-certs", &controllers.ApiController{}, "GET:GetCerts")
beego.Router("/api/get-cert", &controllers.ApiController{}, "GET:GetCert")
beego.Router("/api/update-cert", &controllers.ApiController{}, "POST:UpdateCert")
beego.Router("/api/add-cert", &controllers.ApiController{}, "POST:AddCert")
beego.Router("/api/delete-cert", &controllers.ApiController{}, "POST:DeleteCert")
beego.Router("/api/send-email", &controllers.ApiController{}, "POST:SendEmail")
beego.Router("/api/send-sms", &controllers.ApiController{}, "POST:SendSms")

View File

@ -13,6 +13,7 @@
"codemirror": "^5.61.1",
"copy-to-clipboard": "^3.3.1",
"craco-less": "^1.17.1",
"file-saver": "^2.0.5",
"i18n-iso-countries": "^7.0.0",
"i18next": "^19.8.9",
"moment": "^2.29.1",

View File

@ -28,7 +28,6 @@ import ProviderEditPage from "./ProviderEditPage";
import ApplicationListPage from "./ApplicationListPage";
import ApplicationEditPage from "./ApplicationEditPage";
import ResourceListPage from "./ResourceListPage";
// import ResourceEditPage from "./ResourceEditPage";
import LdapEditPage from "./LdapEditPage";
import LdapSyncPage from "./LdapSyncPage";
import TokenListPage from "./TokenListPage";
@ -38,6 +37,8 @@ import WebhookListPage from "./WebhookListPage";
import WebhookEditPage from "./WebhookEditPage";
import SyncerListPage from "./SyncerListPage";
import SyncerEditPage from "./SyncerEditPage";
import CertListPage from "./CertListPage";
import CertEditPage from "./CertEditPage";
import AccountPage from "./account/AccountPage";
import HomePage from "./basic/HomePage";
import CustomGithubCorner from "./CustomGithubCorner";
@ -115,6 +116,8 @@ class App extends Component {
this.setState({ selectedMenuKey: '/webhooks' });
} else if (uri.includes('/syncers')) {
this.setState({ selectedMenuKey: '/syncers' });
} else if (uri.includes('/certs')) {
this.setState({ selectedMenuKey: '/certs' });
} else if (uri.includes('/signup')) {
this.setState({ selectedMenuKey: '/signup' });
} else if (uri.includes('/login')) {
@ -376,6 +379,13 @@ class App extends Component {
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/certs">
<Link to="/certs">
{i18next.t("general:Certs")}
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/swagger">
<a target="_blank" rel="noreferrer" href={Setting.isLocalhost() ? `${Setting.ServerUrl}/swagger` : "/swagger"}>
@ -440,6 +450,8 @@ class App extends Component {
<Route exact path="/webhooks/:webhookName" render={(props) => this.renderLoginIfNotLoggedIn(<WebhookEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/syncers" render={(props) => this.renderLoginIfNotLoggedIn(<SyncerListPage account={this.state.account} {...props} />)}/>
<Route exact path="/syncers/:syncerName" render={(props) => this.renderLoginIfNotLoggedIn(<SyncerEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/certs" render={(props) => this.renderLoginIfNotLoggedIn(<CertListPage account={this.state.account} {...props} />)}/>
<Route exact path="/certs/:certName" render={(props) => this.renderLoginIfNotLoggedIn(<CertEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/records" render={(props) => this.renderLoginIfNotLoggedIn(<RecordListPage account={this.state.account} {...props} />)}/>
<Route exact path="/.well-known/openid-configuration" render={(props) => <OdicDiscoveryPage />}/>
<Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")}

254
web/src/CertEditPage.js Normal file
View File

@ -0,0 +1,254 @@
// Copyright 2021 The casbin 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, Input, InputNumber, Row, Select} from 'antd';
import * as CertBackend from "./backend/CertBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
import copy from "copy-to-clipboard";
import FileSaver from "file-saver";
const { Option } = Select;
const { TextArea } = Input;
class CertEditPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
certName: props.match.params.certName,
cert: null,
};
}
UNSAFE_componentWillMount() {
this.getCert();
}
getCert() {
CertBackend.getCert("admin", this.state.certName)
.then((cert) => {
this.setState({
cert: cert,
});
});
}
parseCertField(key, value) {
if (["port"].includes(key)) {
value = Setting.myParseInt(value);
}
return value;
}
updateCertField(key, value) {
value = this.parseCertField(key, value);
let cert = this.state.cert;
cert[key] = value;
this.setState({
cert: cert,
});
}
renderCert() {
return (
<Card size="small" title={
<div>
{i18next.t("cert:Edit Cert")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitCertEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitCertEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
</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:Name"), i18next.t("general:Name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.cert.name} onChange={e => {
this.updateCertField('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.cert.displayName} onChange={e => {
this.updateCertField('displayName', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Scope"), i18next.t("cert:Scope - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.cert.scope} onChange={(value => {
this.updateCertField('scope', value);
})}>
{
[
{id: 'JWT', name: 'JWT'},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Type"), i18next.t("cert:Type - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.cert.type} onChange={(value => {
this.updateCertField('type', value);
})}>
{
[
{id: 'x509', name: 'x509'},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Crypto algorithm"), i18next.t("cert:Crypto algorithm - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.cert.cryptoAlgorithm} onChange={(value => {
this.updateCertField('cryptoAlgorithm', value);
})}>
{
[
{id: 'RSA', name: 'RSA'},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Bit size"), i18next.t("cert:Bit size - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.cert.bitSize} onChange={value => {
this.updateCertField('bitSize', value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Expire in years"), i18next.t("cert:Expire in years - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.cert.expireInYears} onChange={value => {
this.updateCertField('expireInYears', value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Public key"), i18next.t("cert:Public key - Tooltip"))} :
</Col>
<Col span={9} >
<Button style={{marginRight: '10px', marginBottom: '10px'}} onClick={() => {
copy(this.state.cert.publicKey);
Setting.showMessage("success", i18next.t("cert:Public key copied to clipboard successfully"));
}}
>
{i18next.t("cert:Copy public key")}
</Button>
<Button type="primary" onClick={() => {
const blob = new Blob([this.state.cert.publicKey], {type: "text/plain;charset=utf-8"});
FileSaver.saveAs(blob, "token_jwt_key.pem");
}}
>
{i18next.t("cert:Download public key")}
</Button>
<TextArea autoSize={{minRows: 30, maxRows: 30}} value={this.state.cert.publicKey} onChange={e => {
this.updateCertField('publicKey', e.target.value);
}} />
</Col>
<Col span={1} />
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Private key"), i18next.t("cert:Private key - Tooltip"))} :
</Col>
<Col span={9} >
<Button style={{marginRight: '10px', marginBottom: '10px'}} onClick={() => {
copy(this.state.cert.privateKey);
Setting.showMessage("success", i18next.t("cert:Private key copied to clipboard successfully"));
}}
>
{i18next.t("cert:Copy private key")}
</Button>
<Button type="primary" onClick={() => {
const blob = new Blob([this.state.cert.privateKey], {type: "text/plain;charset=utf-8"});
FileSaver.saveAs(blob, "token_jwt_key.key");
}}
>
{i18next.t("cert:Download private key")}
</Button>
<TextArea autoSize={{minRows: 30, maxRows: 30}} value={this.state.cert.privateKey} onChange={e => {
this.updateCertField('privateKey', e.target.value);
}} />
</Col>
</Row>
</Card>
)
}
submitCertEdit(willExist) {
let cert = Setting.deepCopy(this.state.cert);
CertBackend.updateCert(this.state.cert.owner, this.state.certName, cert)
.then((res) => {
if (res.msg === "") {
Setting.showMessage("success", `Successfully saved`);
this.setState({
certName: this.state.cert.name,
});
if (willExist) {
this.props.history.push(`/certs`);
} else {
this.props.history.push(`/certs/${this.state.cert.name}`);
}
} else {
Setting.showMessage("error", res.msg);
this.updateCertField('name', this.state.certName);
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
});
}
render() {
return (
<div>
{
this.state.cert !== null ? this.renderCert() : null
}
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitCertEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitCertEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
</div>
</div>
);
}
}
export default CertEditPage;

230
web/src/CertListPage.js Normal file
View File

@ -0,0 +1,230 @@
// Copyright 2021 The casbin 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, Popconfirm, Table} from 'antd';
import moment from "moment";
import * as Setting from "./Setting";
import * as CertBackend from "./backend/CertBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
class CertListPage extends BaseListPage {
newCert() {
const randomName = Setting.getRandomName();
return {
owner: "admin", // this.props.account.certname,
name: `cert_${randomName}`,
createdTime: moment().format(),
displayName: `New Cert - ${randomName}`,
scope: "JWT",
type: "x509",
cryptoAlgorithm: "RSA",
bitSize: 4096,
expireInYears: 20,
publicKey: "",
privateKey: "",
}
}
addCert() {
const newCert = this.newCert();
CertBackend.addCert(newCert)
.then((res) => {
Setting.showMessage("success", `Cert added successfully`);
this.props.history.push(`/certs/${newCert.name}`);
}
)
.catch(error => {
Setting.showMessage("error", `Cert failed to add: ${error}`);
});
}
deleteCert(i) {
CertBackend.deleteCert(this.state.data[i])
.then((res) => {
Setting.showMessage("success", `Cert deleted successfully`);
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
}
)
.catch(error => {
Setting.showMessage("error", `Cert failed to delete: ${error}`);
});
}
renderTable(certs) {
const columns = [
{
title: i18next.t("general:Name"),
dataIndex: 'name',
key: 'name',
width: '120px',
fixed: 'left',
sorter: true,
...this.getColumnSearchProps('name'),
render: (text, record, index) => {
return (
<Link to={`/certs/${text}`}>
{text}
</Link>
)
}
},
{
title: i18next.t("general:Created time"),
dataIndex: 'createdTime',
key: 'createdTime',
width: '180px',
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
}
},
{
title: i18next.t("general:Display name"),
dataIndex: 'displayName',
key: 'displayName',
// width: '100px',
sorter: true,
...this.getColumnSearchProps('displayName'),
},
{
title: i18next.t("cert:Scope"),
dataIndex: 'scope',
key: 'scope',
filterMultiple: false,
filters: [
{text: 'JWT', value: 'JWT'},
],
width: '110px',
sorter: true,
},
{
title: i18next.t("cert:Type"),
dataIndex: 'type',
key: 'type',
filterMultiple: false,
filters: [
{text: 'x509', value: 'x509'},
],
width: '110px',
sorter: true,
},
{
title: i18next.t("cert:Crypto algorithm"),
dataIndex: 'cryptoAlgorithm',
key: 'cryptoAlgorithm',
filterMultiple: false,
filters: [
{text: 'RSA', value: 'RSA'},
],
width: '190px',
sorter: true,
},
{
title: i18next.t("cert:Bit size"),
dataIndex: 'bitSize',
key: 'bitSize',
width: '130px',
sorter: true,
...this.getColumnSearchProps('bitSize'),
},
{
title: i18next.t("cert:Expire in years"),
dataIndex: 'expireInYears',
key: 'expireInYears',
width: '170px',
sorter: true,
...this.getColumnSearchProps('expireInYears'),
},
{
title: i18next.t("general:Action"),
dataIndex: '',
key: 'op',
width: '170px',
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(`/certs/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm
title={`Sure to delete cert: ${record.name} ?`}
onConfirm={() => this.deleteCert(index)}
>
<Button style={{marginBottom: '10px'}} type="danger">{i18next.t("general:Delete")}</Button>
</Popconfirm>
</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={certs} rowKey="name" size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Certs")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small" onClick={this.addCert.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;
let sortField = params.sortField, sortOrder = params.sortOrder;
if (params.category !== undefined && params.category !== null) {
field = "category";
value = params.category;
} else if (params.type !== undefined && params.type !== null) {
field = "type";
value = params.type;
}
this.setState({ loading: true });
CertBackend.getCerts("admin", params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
.then((res) => {
if (res.status === "ok") {
this.setState({
loading: false,
data: res.data,
pagination: {
...params.pagination,
total: res.data2,
},
searchText: params.searchText,
searchedColumn: params.searchedColumn,
});
}
});
};
}
export default CertListPage;

View File

@ -0,0 +1,56 @@
// Copyright 2021 The casbin 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 getCerts(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
return fetch(`${Setting.ServerUrl}/api/get-certs?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
method: "GET",
credentials: "include"
}).then(res => res.json());
}
export function getCert(owner, name) {
return fetch(`${Setting.ServerUrl}/api/get-cert?id=${owner}/${encodeURIComponent(name)}`, {
method: "GET",
credentials: "include"
}).then(res => res.json());
}
export function updateCert(owner, name, cert) {
let newCert = Setting.deepCopy(cert);
return fetch(`${Setting.ServerUrl}/api/update-cert?id=${owner}/${encodeURIComponent(name)}`, {
method: 'POST',
credentials: 'include',
body: JSON.stringify(newCert),
}).then(res => res.json());
}
export function addCert(cert) {
let newCert = Setting.deepCopy(cert);
return fetch(`${Setting.ServerUrl}/api/add-cert`, {
method: 'POST',
credentials: 'include',
body: JSON.stringify(newCert),
}).then(res => res.json());
}
export function deleteCert(cert) {
let newCert = Setting.deepCopy(cert);
return fetch(`${Setting.ServerUrl}/api/delete-cert`, {
method: 'POST',
credentials: 'include',
body: JSON.stringify(newCert),
}).then(res => res.json());
}

View File

@ -5190,6 +5190,11 @@ file-loader@6.1.1:
loader-utils "^2.0.0"
schema-utils "^3.0.0"
file-saver@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38"
integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==
file-uri-to-path@1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"