From 85523fa9d45aeeda22af5357498810a7138f9674 Mon Sep 17 00:00:00 2001 From: Yang Luo Date: Sat, 13 Mar 2021 23:47:35 +0800 Subject: [PATCH] Add token pages. --- controllers/token.go | 70 +++++++++++ object/adapter.go | 5 + object/token.go | 95 +++++++++++++++ routers/router.go | 6 + web/src/App.js | 13 ++ web/src/TokenEditPage.js | 181 +++++++++++++++++++++++++++ web/src/TokenListPage.js | 210 ++++++++++++++++++++++++++++++++ web/src/backend/TokenBackend.js | 56 +++++++++ 8 files changed, 636 insertions(+) create mode 100644 controllers/token.go create mode 100644 object/token.go create mode 100644 web/src/TokenEditPage.js create mode 100644 web/src/TokenListPage.js create mode 100644 web/src/backend/TokenBackend.js diff --git a/controllers/token.go b/controllers/token.go new file mode 100644 index 00000000..0761bc9b --- /dev/null +++ b/controllers/token.go @@ -0,0 +1,70 @@ +// 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/casdoor/casdoor/object" +) + +func (c *ApiController) GetTokens() { + owner := c.Input().Get("owner") + + c.Data["json"] = object.GetTokens(owner) + c.ServeJSON() +} + +func (c *ApiController) GetToken() { + id := c.Input().Get("id") + + c.Data["json"] = object.GetToken(id) + c.ServeJSON() +} + +func (c *ApiController) UpdateToken() { + id := c.Input().Get("id") + + var token object.Token + err := json.Unmarshal(c.Ctx.Input.RequestBody, &token) + if err != nil { + panic(err) + } + + c.Data["json"] = object.UpdateToken(id, &token) + c.ServeJSON() +} + +func (c *ApiController) AddToken() { + var token object.Token + err := json.Unmarshal(c.Ctx.Input.RequestBody, &token) + if err != nil { + panic(err) + } + + c.Data["json"] = object.AddToken(&token) + c.ServeJSON() +} + +func (c *ApiController) DeleteToken() { + var token object.Token + err := json.Unmarshal(c.Ctx.Input.RequestBody, &token) + if err != nil { + panic(err) + } + + c.Data["json"] = object.DeleteToken(&token) + c.ServeJSON() +} diff --git a/object/adapter.go b/object/adapter.go index aab9110b..3e457356 100644 --- a/object/adapter.go +++ b/object/adapter.go @@ -109,4 +109,9 @@ func (a *Adapter) createTable() { if err != nil { panic(err) } + + err = a.engine.Sync2(new(Token)) + if err != nil { + panic(err) + } } diff --git a/object/token.go b/object/token.go new file mode 100644 index 00000000..05b4bad6 --- /dev/null +++ b/object/token.go @@ -0,0 +1,95 @@ +// 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 ( + "github.com/casdoor/casdoor/util" + "xorm.io/core" +) + +type Token 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"` + + Application string `xorm:"varchar(100)" json:"application"` + + AccessToken string `xorm:"varchar(100)" json:"accessToken"` + ExpiresIn int `json:"expiresIn"` + Scope string `xorm:"varchar(100)" json:"scope"` + TokenType string `xorm:"varchar(100)" json:"tokenType"` +} + +func GetTokens(owner string) []*Token { + tokens := []*Token{} + err := adapter.engine.Desc("created_time").Find(&tokens, &Token{Owner: owner}) + if err != nil { + panic(err) + } + + return tokens +} + +func getToken(owner string, name string) *Token { + token := Token{Owner: owner, Name: name} + existed, err := adapter.engine.Get(&token) + if err != nil { + panic(err) + } + + if existed { + return &token + } else { + return nil + } +} + +func GetToken(id string) *Token { + owner, name := util.GetOwnerAndNameFromId(id) + return getToken(owner, name) +} + +func UpdateToken(id string, token *Token) bool { + owner, name := util.GetOwnerAndNameFromId(id) + if getToken(owner, name) == nil { + return false + } + + _, err := adapter.engine.ID(core.PK{owner, name}).AllCols().Update(token) + if err != nil { + panic(err) + } + + //return affected != 0 + return true +} + +func AddToken(token *Token) bool { + affected, err := adapter.engine.Insert(token) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func DeleteToken(token *Token) bool { + affected, err := adapter.engine.ID(core.PK{token.Owner, token.Name}).Delete(&Token{}) + if err != nil { + panic(err) + } + + return affected != 0 +} diff --git a/routers/router.go b/routers/router.go index 62247d76..d2d21c2f 100644 --- a/routers/router.go +++ b/routers/router.go @@ -63,4 +63,10 @@ func initAPI() { beego.Router("/api/update-application", &controllers.ApiController{}, "POST:UpdateApplication") beego.Router("/api/add-application", &controllers.ApiController{}, "POST:AddApplication") beego.Router("/api/delete-application", &controllers.ApiController{}, "POST:DeleteApplication") + + beego.Router("/api/get-tokens", &controllers.ApiController{}, "GET:GetTokens") + beego.Router("/api/get-token", &controllers.ApiController{}, "GET:GetToken") + beego.Router("/api/update-token", &controllers.ApiController{}, "POST:UpdateToken") + beego.Router("/api/add-token", &controllers.ApiController{}, "POST:AddToken") + beego.Router("/api/delete-token", &controllers.ApiController{}, "POST:DeleteToken") } diff --git a/web/src/App.js b/web/src/App.js index 78e29452..5488ad1a 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -26,6 +26,8 @@ import ProviderListPage from "./ProviderListPage"; import ProviderEditPage from "./ProviderEditPage"; import ApplicationListPage from "./ApplicationListPage"; import ApplicationEditPage from "./ApplicationEditPage"; +import TokenListPage from "./TokenListPage"; +import TokenEditPage from "./TokenEditPage"; import AccountPage from "./account/AccountPage"; import HomePage from "./basic/HomePage"; import CustomGithubCorner from "./CustomGithubCorner"; @@ -76,6 +78,8 @@ class App extends Component { this.setState({ selectedMenuKey: 3 }); } else if (uri.includes('applications')) { this.setState({ selectedMenuKey: 4 }); + } else if (uri.includes('tokens')) { + this.setState({ selectedMenuKey: 5 }); } else { this.setState({ selectedMenuKey: -1 }); } @@ -227,6 +231,13 @@ class App extends Component { ); + res.push( + + + {i18next.t("general:Tokens")} + + + ); } return res; } @@ -290,6 +301,8 @@ class App extends Component { this.renderLoginIfNotLoggedIn()}/> this.renderLoginIfNotLoggedIn()}/> this.renderLoginIfNotLoggedIn()}/> + this.renderLoginIfNotLoggedIn()}/> + this.renderLoginIfNotLoggedIn()}/> ) diff --git a/web/src/TokenEditPage.js b/web/src/TokenEditPage.js new file mode 100644 index 00000000..375b4b13 --- /dev/null +++ b/web/src/TokenEditPage.js @@ -0,0 +1,181 @@ +// 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, Row, Select} from 'antd'; +import * as TokenBackend from "./backend/TokenBackend"; +import * as Setting from "./Setting"; +import i18next from "i18next"; + +const { Option } = Select; + +class TokenEditPage extends React.Component { + constructor(props) { + super(props); + this.state = { + classes: props, + tokenName: props.match.params.tokenName, + token: null, + }; + } + + componentWillMount() { + this.getToken(); + } + + getToken() { + TokenBackend.getToken("admin", this.state.tokenName) + .then((token) => { + this.setState({ + token: token, + }); + }); + } + + parseTokenField(key, value) { + // if ([].includes(key)) { + // value = Setting.myParseInt(value); + // } + return value; + } + + updateTokenField(key, value) { + value = this.parseTokenField(key, value); + + let token = this.state.token; + token[key] = value; + this.setState({ + token: token, + }); + } + + renderToken() { + return ( + + {i18next.t("token:Edit Token")}     + + + } style={{marginLeft: '5px'}} type="inner"> + + + {i18next.t("general:Name")}: + + + { + this.updateTokenField('name', e.target.value); + }} /> + + + + + {i18next.t("general:Application")}: + + + { + this.updateTokenField('application', e.target.value); + }} /> + + + + + {i18next.t("general:Access Token")}: + + + { + this.updateTokenField('accessToken', e.target.value); + }} /> + + + + + {i18next.t("general:Expires In")}: + + + { + this.updateTokenField('expiresIn', e.target.value); + }} /> + + + + + {i18next.t("general:Scope")}: + + + { + this.updateTokenField('scope', e.target.value); + }} /> + + + + + {i18next.t("general:Token Type")}: + + + { + this.updateTokenField('tokenType', e.target.value); + }} /> + + + + ) + } + + submitTokenEdit() { + let token = Setting.deepCopy(this.state.token); + TokenBackend.updateToken(this.state.token.owner, this.state.tokenName, token) + .then((res) => { + if (res) { + Setting.showMessage("success", `Successfully saved`); + this.setState({ + tokenName: this.state.token.name, + }); + this.props.history.push(`/tokens/${this.state.token.name}`); + } else { + Setting.showMessage("error", `failed to save: server side failure`); + this.updateTokenField('name', this.state.tokenName); + } + }) + .catch(error => { + Setting.showMessage("error", `failed to save: ${error}`); + }); + } + + render() { + return ( +
+ + + + + { + this.state.token !== null ? this.renderToken() : null + } + + + + + + + + + + + +
+ ); + } +} + +export default TokenEditPage; diff --git a/web/src/TokenListPage.js b/web/src/TokenListPage.js new file mode 100644 index 00000000..9a3847a3 --- /dev/null +++ b/web/src/TokenListPage.js @@ -0,0 +1,210 @@ +// 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, Col, Popconfirm, Row, Table} from 'antd'; +import moment from "moment"; +import * as Setting from "./Setting"; +import * as TokenBackend from "./backend/TokenBackend"; +import i18next from "i18next"; + +class TokenListPage extends React.Component { + constructor(props) { + super(props); + this.state = { + classes: props, + tokens: null, + }; + } + + componentWillMount() { + this.getTokens(); + } + + getTokens() { + TokenBackend.getTokens("admin") + .then((res) => { + this.setState({ + tokens: res, + }); + }); + } + + newToken() { + return { + owner: "admin", // this.props.account.tokenname, + name: `token_${this.state.tokens.length}`, + createdTime: moment().format(), + application: "app-built-in", + accessToken: "", + expiresIn: 7200, + scope: "read", + tokenType: "Bearer", + } + } + + addToken() { + const newToken = this.newToken(); + TokenBackend.addToken(newToken) + .then((res) => { + Setting.showMessage("success", `Token added successfully`); + this.setState({ + tokens: Setting.prependRow(this.state.tokens, newToken), + }); + } + ) + .catch(error => { + Setting.showMessage("error", `Token failed to add: ${error}`); + }); + } + + deleteToken(i) { + TokenBackend.deleteToken(this.state.tokens[i]) + .then((res) => { + Setting.showMessage("success", `Token deleted successfully`); + this.setState({ + tokens: Setting.deleteRow(this.state.tokens, i), + }); + } + ) + .catch(error => { + Setting.showMessage("error", `Token failed to delete: ${error}`); + }); + } + + renderTable(tokens) { + const columns = [ + { + title: i18next.t("general:Name"), + dataIndex: 'name', + key: 'name', + width: '120px', + sorter: (a, b) => a.name.localeCompare(b.name), + render: (text, record, index) => { + return ( + + {text} + + ) + } + }, + { + title: i18next.t("general:Created Time"), + dataIndex: 'createdTime', + key: 'createdTime', + width: '160px', + sorter: (a, b) => a.createdTime.localeCompare(b.createdTime), + render: (text, record, index) => { + return Setting.getFormattedDate(text); + } + }, + { + title: i18next.t("token:Application"), + dataIndex: 'application', + key: 'application', + width: '150px', + sorter: (a, b) => a.application.localeCompare(b.application), + render: (text, record, index) => { + return ( + + {text} + + ) + } + }, + { + title: i18next.t("token:Access Token"), + dataIndex: 'accessToken', + key: 'accessToken', + width: '150px', + sorter: (a, b) => a.accessToken.localeCompare(b.accessToken), + }, + { + title: i18next.t("token:Expires In"), + dataIndex: 'expiresIn', + key: 'expiresIn', + width: '150px', + sorter: (a, b) => a.expiresIn - b.expiresIn, + }, + { + title: i18next.t("token:Scope"), + dataIndex: 'scope', + key: 'scope', + width: '150px', + sorter: (a, b) => a.scope.localeCompare(b.scope), + }, + { + title: i18next.t("token:Token Type"), + dataIndex: 'tokenType', + key: 'tokenType', + width: '150px', + sorter: (a, b) => a.tokenType.localeCompare(b.tokenType), + }, + { + title: i18next.t("general:Action"), + dataIndex: '', + key: 'op', + width: '170px', + render: (text, record, index) => { + return ( +
+ + this.deleteToken(index)} + > + + +
+ ) + } + }, + ]; + + return ( +
+ ( +
+ {i18next.t("general:Tokens")}     + +
+ )} + loading={tokens === null} + /> + + ); + } + + render() { + return ( +
+ +
+ + + { + this.renderTable(this.state.tokens) + } + + + + + + ); + } +} + +export default TokenListPage; diff --git a/web/src/backend/TokenBackend.js b/web/src/backend/TokenBackend.js new file mode 100644 index 00000000..1adb93a5 --- /dev/null +++ b/web/src/backend/TokenBackend.js @@ -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 getTokens(owner) { + return fetch(`${Setting.ServerUrl}/api/get-tokens?owner=${owner}`, { + method: "GET", + credentials: "include" + }).then(res => res.json()); +} + +export function getToken(owner, name) { + return fetch(`${Setting.ServerUrl}/api/get-token?id=${owner}/${encodeURIComponent(name)}`, { + method: "GET", + credentials: "include" + }).then(res => res.json()); +} + +export function updateToken(owner, name, token) { + let newToken = Setting.deepCopy(token); + return fetch(`${Setting.ServerUrl}/api/update-token?id=${owner}/${encodeURIComponent(name)}`, { + method: 'POST', + credentials: 'include', + body: JSON.stringify(newToken), + }).then(res => res.json()); +} + +export function addToken(token) { + let newToken = Setting.deepCopy(token); + return fetch(`${Setting.ServerUrl}/api/add-token`, { + method: 'POST', + credentials: 'include', + body: JSON.stringify(newToken), + }).then(res => res.json()); +} + +export function deleteToken(token) { + let newToken = Setting.deepCopy(token); + return fetch(`${Setting.ServerUrl}/api/delete-token`, { + method: 'POST', + credentials: 'include', + body: JSON.stringify(newToken), + }).then(res => res.json()); +}