diff --git a/controllers/casbin_adapter.go b/controllers/casbin_adapter.go new file mode 100644 index 00000000..5f3e303f --- /dev/null +++ b/controllers/casbin_adapter.go @@ -0,0 +1,94 @@ +// Copyright 2022 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/astaxie/beego/utils/pagination" + "github.com/casdoor/casdoor/object" + "github.com/casdoor/casdoor/util" +) + +func (c *ApiController) GetCasbinAdapters() { + 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.GetCasbinAdapters(owner) + c.ServeJSON() + } else { + limit := util.ParseInt(limit) + paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetCasbinAdapterCount(owner, field, value))) + adapters := object.GetPaginationCasbinAdapters(owner, paginator.Offset(), limit, field, value, sortField, sortOrder) + c.ResponseOk(adapters, paginator.Nums()) + } +} + +func (c *ApiController) GetCasbinAdapter() { + id := c.Input().Get("id") + c.Data["json"] = object.GetCasbinAdapter(id) + c.ServeJSON() +} + +func (c *ApiController) UpdateCasbinAdapter() { + id := c.Input().Get("id") + + var casbinAdapter object.CasbinAdapter + err := json.Unmarshal(c.Ctx.Input.RequestBody, &casbinAdapter) + if err != nil { + c.ResponseError(err.Error()) + return + } + + c.Data["json"] = wrapActionResponse(object.UpdateCasbinAdapter(id, &casbinAdapter)) + c.ServeJSON() +} + +func (c *ApiController) AddCasbinAdapter() { + var casbinAdapter object.CasbinAdapter + err := json.Unmarshal(c.Ctx.Input.RequestBody, &casbinAdapter) + if err != nil { + c.ResponseError(err.Error()) + return + } + + c.Data["json"] = wrapActionResponse(object.AddCasbinAdapter(&casbinAdapter)) + c.ServeJSON() +} + +func (c *ApiController) DeleteCasbinAdapter() { + var casbinAdapter object.CasbinAdapter + err := json.Unmarshal(c.Ctx.Input.RequestBody, &casbinAdapter) + if err != nil { + c.ResponseError(err.Error()) + return + } + + c.Data["json"] = wrapActionResponse(object.DeleteCasbinAdapter(&casbinAdapter)) + c.ServeJSON() +} + +func (c *ApiController) SyncPolicies() { + id := c.Input().Get("id") + adapter := object.GetCasbinAdapter(id) + + c.Data["json"] = object.SyncPolicies(adapter) + c.ServeJSON() +} diff --git a/object/adapter.go b/object/adapter.go index 749ddc40..21a9c1ca 100644 --- a/object/adapter.go +++ b/object/adapter.go @@ -145,6 +145,11 @@ func (a *Adapter) createTable() { panic(err) } + err = a.Engine.Sync2(new(CasbinAdapter)) + if err != nil { + panic(err) + } + err = a.Engine.Sync2(new(Provider)) if err != nil { panic(err) diff --git a/object/casbin_adapter.go b/object/casbin_adapter.go new file mode 100644 index 00000000..f052baab --- /dev/null +++ b/object/casbin_adapter.go @@ -0,0 +1,217 @@ +// Copyright 2022 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" + "strings" + + "github.com/casbin/casbin/v2" + "github.com/casbin/casbin/v2/model" + xormadapter "github.com/casbin/xorm-adapter/v2" + "github.com/casdoor/casdoor/util" + + "xorm.io/core" +) + +type CasbinAdapter 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"` + + Organization string `xorm:"varchar(100)" json:"organization"` + Type string `xorm:"varchar(100)" json:"type"` + Model string `xorm:"varchar(100)" json:"model"` + + Host string `xorm:"varchar(100)" json:"host"` + Port int `json:"port"` + User string `xorm:"varchar(100)" json:"user"` + Password string `xorm:"varchar(100)" json:"password"` + DatabaseType string `xorm:"varchar(100)" json:"databaseType"` + Database string `xorm:"varchar(100)" json:"database"` + Table string `xorm:"varchar(100)" json:"table"` + IsEnabled bool `json:"isEnabled"` + + Adapter *xormadapter.Adapter `xorm:"-" json:"-"` +} + +func GetCasbinAdapterCount(owner, field, value string) int { + session := GetSession(owner, -1, -1, field, value, "", "") + count, err := session.Count(&CasbinAdapter{}) + if err != nil { + panic(err) + } + + return int(count) +} + +func GetCasbinAdapters(owner string) []*CasbinAdapter { + adapters := []*CasbinAdapter{} + err := adapter.Engine.Where("owner = ?", owner).Find(&adapters) + if err != nil { + panic(err) + } + + return adapters +} + +func GetPaginationCasbinAdapters(owner string, page, limit int, field, value, sort, order string) []*CasbinAdapter { + session := GetSession(owner, page, limit, field, value, sort, order) + adapters := []*CasbinAdapter{} + err := session.Find(&adapters) + if err != nil { + panic(err) + } + + return adapters +} + +func getCasbinAdapter(owner, name string) *CasbinAdapter { + if owner == "" || name == "" { + return nil + } + + casbinAdapter := CasbinAdapter{Owner: owner, Name: name} + existed, err := adapter.Engine.Get(&casbinAdapter) + if err != nil { + panic(err) + } + + if existed { + return &casbinAdapter + } else { + return nil + } +} + +func GetCasbinAdapter(id string) *CasbinAdapter { + owner, name := util.GetOwnerAndNameFromId(id) + return getCasbinAdapter(owner, name) +} + +func UpdateCasbinAdapter(id string, casbinAdapter *CasbinAdapter) bool { + owner, name := util.GetOwnerAndNameFromId(id) + if getCasbinAdapter(owner, name) == nil { + return false + } + + session := adapter.Engine.ID(core.PK{owner, name}).AllCols() + if casbinAdapter.Password == "***" { + session.Omit("password") + } + affected, err := session.Update(casbinAdapter) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func AddCasbinAdapter(casbinAdapter *CasbinAdapter) bool { + affected, err := adapter.Engine.Insert(casbinAdapter) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func DeleteCasbinAdapter(casbinAdapter *CasbinAdapter) bool { + affected, err := adapter.Engine.ID(core.PK{casbinAdapter.Owner, casbinAdapter.Name}).Delete(&CasbinAdapter{}) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func (casbinAdapter *CasbinAdapter) GetId() string { + return fmt.Sprintf("%s/%s", casbinAdapter.Owner, casbinAdapter.Name) +} + +func (casbinAdapter *CasbinAdapter) getTable() string { + if casbinAdapter.DatabaseType == "mssql" { + return fmt.Sprintf("[%s]", casbinAdapter.Table) + } else { + return casbinAdapter.Table + } +} + +func safeReturn(policy []string, i int) string { + if len(policy) > i { + return policy[i] + } else { + return "" + } +} + +func matrixToCasbinRules(pType string, policies [][]string) []*xormadapter.CasbinRule { + res := []*xormadapter.CasbinRule{} + + for _, policy := range policies { + line := xormadapter.CasbinRule{ + PType: pType, + V0: safeReturn(policy, 0), + V1: safeReturn(policy, 1), + V2: safeReturn(policy, 2), + V3: safeReturn(policy, 3), + V4: safeReturn(policy, 4), + V5: safeReturn(policy, 5), + } + res = append(res, &line) + } + + return res +} + +func SyncPolicies(casbinAdapter *CasbinAdapter) []*xormadapter.CasbinRule { + // init Adapter + if casbinAdapter.Adapter == nil { + var dataSourceName string + if casbinAdapter.DatabaseType == "mssql" { + dataSourceName = fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s", casbinAdapter.User, casbinAdapter.Password, casbinAdapter.Host, casbinAdapter.Port, casbinAdapter.Database) + } else if casbinAdapter.DatabaseType == "postgres" { + dataSourceName = fmt.Sprintf("user=%s password=%s host=%s port=%d sslmode=disable dbname=%s", casbinAdapter.User, casbinAdapter.Password, casbinAdapter.Host, casbinAdapter.Port, casbinAdapter.Database) + } else { + dataSourceName = fmt.Sprintf("%s:%s@tcp(%s:%d)/", casbinAdapter.User, casbinAdapter.Password, casbinAdapter.Host, casbinAdapter.Port) + } + + if !isCloudIntranet { + dataSourceName = strings.ReplaceAll(dataSourceName, "dbi.", "db.") + } + + casbinAdapter.Adapter, _ = xormadapter.NewAdapterByEngineWithTableName(NewAdapter(casbinAdapter.DatabaseType, dataSourceName, casbinAdapter.Database).Engine, casbinAdapter.getTable(), "") + } + + // init Model + modelObj := getModel(casbinAdapter.Owner, casbinAdapter.Model) + m, err := model.NewModelFromString(modelObj.ModelText) + if err != nil { + panic(err) + } + + // init Enforcer + enforcer, err := casbin.NewEnforcer(m, casbinAdapter.Adapter) + if err != nil { + panic(err) + } + + policies := matrixToCasbinRules("p", enforcer.GetPolicy()) + if strings.Contains(modelObj.ModelText, "[role_definition]") { + policies = append(policies, matrixToCasbinRules("g", enforcer.GetGroupingPolicy())...) + } + + return policies +} diff --git a/routers/router.go b/routers/router.go index 84bd76c2..4bfe6ed9 100644 --- a/routers/router.go +++ b/routers/router.go @@ -97,6 +97,13 @@ func initAPI() { beego.Router("/api/add-model", &controllers.ApiController{}, "POST:AddModel") beego.Router("/api/delete-model", &controllers.ApiController{}, "POST:DeleteModel") + beego.Router("/api/get-adapters", &controllers.ApiController{}, "GET:GetCasbinAdapters") + beego.Router("/api/get-adapter", &controllers.ApiController{}, "GET:GetCasbinAdapter") + beego.Router("/api/update-adapter", &controllers.ApiController{}, "POST:UpdateCasbinAdapter") + beego.Router("/api/add-adapter", &controllers.ApiController{}, "POST:AddCasbinAdapter") + beego.Router("/api/delete-adapter", &controllers.ApiController{}, "POST:DeleteCasbinAdapter") + beego.Router("/api/sync-policies", &controllers.ApiController{}, "GET:SyncPolicies") + beego.Router("/api/set-password", &controllers.ApiController{}, "POST:SetPassword") beego.Router("/api/check-user-password", &controllers.ApiController{}, "POST:CheckUserPassword") beego.Router("/api/get-email-and-phone", &controllers.ApiController{}, "POST:GetEmailAndPhone") diff --git a/web/src/AdapterEditPage.js b/web/src/AdapterEditPage.js new file mode 100644 index 00000000..6eb97a51 --- /dev/null +++ b/web/src/AdapterEditPage.js @@ -0,0 +1,412 @@ +// Copyright 2022 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, Input, InputNumber, Row, Select, Switch, Table, Tooltip} from "antd"; +import * as AdapterBackend from "./backend/AdapterBackend"; +import * as OrganizationBackend from "./backend/OrganizationBackend"; +import * as Setting from "./Setting"; +import i18next from "i18next"; + +import "codemirror/lib/codemirror.css"; +import * as ModelBackend from "./backend/ModelBackend"; +import {EditOutlined, MinusOutlined} from "@ant-design/icons"; +require("codemirror/theme/material-darker.css"); +require("codemirror/mode/javascript/javascript"); + +const {Option} = Select; + +class AdapterEditPage extends React.Component { + constructor(props) { + super(props); + this.state = { + classes: props, + organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName, + adapterName: props.match.params.adapterName, + adapter: null, + organizations: [], + models: [], + policyLists: [], + mode: props.location.mode !== undefined ? props.location.mode : "edit", + }; + } + + UNSAFE_componentWillMount() { + this.getAdapter(); + this.getOrganizations(); + } + + getAdapter() { + AdapterBackend.getAdapter(this.state.organizationName, this.state.adapterName) + .then((adapter) => { + this.setState({ + adapter: adapter, + }); + + this.getModels(adapter.owner); + }); + } + + getOrganizations() { + OrganizationBackend.getOrganizations(this.state.organizationName) + .then((res) => { + this.setState({ + organizations: (res.msg === undefined) ? res : [], + }); + }); + } + + getModels(organizationName) { + ModelBackend.getModels(organizationName) + .then((res) => { + this.setState({ + models: res, + }); + }); + } + + parseAdapterField(key, value) { + if (["port"].includes(key)) { + value = Setting.myParseInt(value); + } + return value; + } + + updateAdapterField(key, value) { + value = this.parseAdapterField(key, value); + + const adapter = this.state.adapter; + adapter[key] = value; + this.setState({ + adapter: adapter, + }); + } + + synPolicies() { + this.setState({loading: true}); + AdapterBackend.syncPolicies(this.state.adapter.owner, this.state.adapter.name) + .then((res) => { + this.setState({loading: false, policyLists: res}); + }) + .catch(error => { + this.setState({loading: false}); + Setting.showMessage("error", `Adapter failed to get policies: ${error}`); + }); + } + + renderTable(table) { + const columns = [ + { + title: "Rule Type", + dataIndex: "PType", + key: "PType", + width: "100px", + }, + { + title: "V0", + dataIndex: "V0", + key: "V0", + width: "100px", + }, + { + title: "V1", + dataIndex: "V1", + key: "V1", + width: "100px", + }, + { + title: "V2", + dataIndex: "V2", + key: "V2", + width: "100px", + }, + { + title: "V3", + dataIndex: "V3", + key: "V3", + width: "100px", + }, + { + title: "V4", + dataIndex: "V4", + key: "V4", + width: "100px", + }, + { + title: "V5", + dataIndex: "V5", + key: "V5", + width: "100px", + }, + { + title: "Option", + key: "option", + width: "100px", + render: (text, record, index) => { + return ( +
+ +
+ ); + }, + }]; + + return ( +
+ + + ); + } + + renderAdapter() { + return ( + + {this.state.mode === "add" ? i18next.t("adapter:New Adapter") : i18next.t("adapter:Edit Adapter")}     + + + {this.state.mode === "add" ? : null} + + } style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner"> + + + {Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} : + + + + + + + + {Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} : + + + { + this.updateAdapterField("name", e.target.value); + }} /> + + + + + {Setting.getLabel(i18next.t("provider:Type"), i18next.t("provider:Type - Tooltip"))} : + + + + + + + + {Setting.getLabel(i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} : + + + { + this.updateAdapterField("host", e.target.value); + }} /> + + + + + {Setting.getLabel(i18next.t("provider:Port"), i18next.t("provider:Port - Tooltip"))} : + + + { + this.updateAdapterField("port", value); + }} /> + + + + + {Setting.getLabel(i18next.t("general:User"), i18next.t("general:User - Tooltip"))} : + + + { + this.updateAdapterField("user", e.target.value); + }} /> + + + + + {Setting.getLabel(i18next.t("general:Password"), i18next.t("general:Password - Tooltip"))} : + + + { + this.updateAdapterField("password", e.target.value); + }} /> + + + + + {Setting.getLabel(i18next.t("syncer:Database type"), i18next.t("syncer:Database type - Tooltip"))} : + + + + + + + + {Setting.getLabel(i18next.t("syncer:Database"), i18next.t("syncer:Database - Tooltip"))} : + + + { + this.updateAdapterField("database", e.target.value); + }} /> + + + + + {Setting.getLabel(i18next.t("syncer:Table"), i18next.t("syncer:Table - Tooltip"))} : + + + { + this.updateAdapterField("table", e.target.value); + }} /> + + + + + {Setting.getLabel(i18next.t("general:Model"), i18next.t("general:Model - Tooltip"))} : + + + + + + + + {Setting.getLabel(i18next.t("adapter:Policies"), i18next.t("adapter:Policies - Tooltip"))} : + + + + + + + + + + { + this.renderTable(this.state.policyLists) + } + + + + + {Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} : + + + { + this.updateAdapterField("isEnabled", checked); + }} /> + + + + ); + } + + submitAdapterEdit(willExist) { + const adapter = Setting.deepCopy(this.state.adapter); + AdapterBackend.updateAdapter(this.state.adapter.owner, this.state.adapterName, adapter) + .then((res) => { + if (res.msg === "") { + Setting.showMessage("success", "Successfully saved"); + this.setState({ + adapterName: this.state.adapter.name, + }); + + if (willExist) { + this.props.history.push("/adapters"); + } else { + this.props.history.push(`/adapters/${this.state.adapter.name}`); + } + } else { + Setting.showMessage("error", res.msg); + this.updateAdapterField("name", this.state.adapterName); + } + }) + .catch(error => { + Setting.showMessage("error", `Failed to connect to server: ${error}`); + }); + } + + deleteAdapter() { + AdapterBackend.deleteAdapter(this.state.adapter) + .then(() => { + this.props.history.push("/adapters"); + }) + .catch(error => { + Setting.showMessage("error", `adapter failed to delete: ${error}`); + }); + } + + render() { + return ( +
+ { + this.state.adapter !== null ? this.renderAdapter() : null + } +
+ + + {this.state.mode === "add" ? : null} +
+
+ ); + } +} + +export default AdapterEditPage; diff --git a/web/src/AdapterListPage.js b/web/src/AdapterListPage.js new file mode 100644 index 00000000..0c45e064 --- /dev/null +++ b/web/src/AdapterListPage.js @@ -0,0 +1,261 @@ +// Copyright 2022 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, Popconfirm, Switch, Table} from "antd"; +import moment from "moment"; +import * as Setting from "./Setting"; +import * as AdapterBackend from "./backend/AdapterBackend"; +import i18next from "i18next"; +import BaseListPage from "./BaseListPage"; + +class AdapterListPage extends BaseListPage { + newAdapter() { + const randomName = Setting.getRandomName(); + return { + owner: "built-in", + name: `adapter_${randomName}`, + createdTime: moment().format(), + organization: "built-in", + type: "Database", + host: "localhost", + port: 3306, + user: "root", + password: "123456", + databaseType: "mysql", + database: "dbName", + table: "tableName", + isEnabled: false, + }; + } + + addAdapter() { + const newAdapter = this.newAdapter(); + AdapterBackend.addAdapter(newAdapter) + .then((res) => { + this.props.history.push({pathname: `/adapters/${newAdapter.owner}/${newAdapter.name}`, mode: "add"}); + } + ) + .catch(error => { + Setting.showMessage("error", `Adapter failed to add: ${error}`); + }); + } + + deleteAdapter(i) { + AdapterBackend.deleteAdapter(this.state.data[i]) + .then((res) => { + Setting.showMessage("success", "Adapter deleted successfully"); + this.setState({ + data: Setting.deleteRow(this.state.data, i), + pagination: {total: this.state.pagination.total - 1}, + }); + } + ) + .catch(error => { + Setting.showMessage("error", `Adapter failed to delete: ${error}`); + }); + } + + renderTable(adapters) { + const columns = [ + { + title: i18next.t("general:Organization"), + dataIndex: "organization", + key: "organization", + width: "120px", + sorter: true, + ...this.getColumnSearchProps("organization"), + render: (text, record, index) => { + return ( + + {text} + + ); + }, + }, + { + title: i18next.t("general:Name"), + dataIndex: "name", + key: "name", + width: "150px", + fixed: "left", + sorter: true, + ...this.getColumnSearchProps("name"), + render: (text, record, index) => { + return ( + + {text} + + ); + }, + }, + { + 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("provider:Type"), + dataIndex: "type", + key: "type", + width: "100px", + sorter: true, + filterMultiple: false, + filters: [ + {text: "Database", value: "Database"}, + ], + }, + { + title: i18next.t("provider:Host"), + dataIndex: "host", + key: "host", + width: "120px", + sorter: true, + ...this.getColumnSearchProps("host"), + }, + { + title: i18next.t("provider:Port"), + dataIndex: "port", + key: "port", + width: "100px", + sorter: true, + ...this.getColumnSearchProps("port"), + }, + { + title: i18next.t("general:User"), + dataIndex: "user", + key: "user", + width: "120px", + sorter: true, + ...this.getColumnSearchProps("user"), + }, + { + title: i18next.t("general:Password"), + dataIndex: "password", + key: "password", + width: "120px", + sorter: true, + ...this.getColumnSearchProps("password"), + }, + { + title: i18next.t("syncer:Database type"), + dataIndex: "databaseType", + key: "databaseType", + width: "120px", + sorter: (a, b) => a.databaseType.localeCompare(b.databaseType), + }, + { + title: i18next.t("syncer:Database"), + dataIndex: "database", + key: "database", + width: "120px", + sorter: true, + }, + { + title: i18next.t("syncer:Table"), + dataIndex: "table", + key: "table", + width: "120px", + sorter: true, + }, + { + title: i18next.t("general:Is enabled"), + dataIndex: "isEnabled", + key: "isEnabled", + width: "120px", + sorter: true, + render: (text, record, index) => { + return ( + + ); + }, + }, + { + title: i18next.t("general:Action"), + dataIndex: "", + key: "op", + width: "170px", + fixed: (Setting.isMobile()) ? "false" : "right", + render: (text, record, index) => { + return ( +
+ + this.deleteAdapter(index)} + > + + +
+ ); + }, + }, + ]; + + 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 ( +
+
( +
+ {i18next.t("general:Adapters")}     + +
+ )} + loading={this.state.loading} + onChange={this.handleTableChange} + /> + + ); + } + + 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}); + AdapterBackend.getAdapters("", 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 AdapterListPage; diff --git a/web/src/App.js b/web/src/App.js index f3e79d06..f3c76143 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -72,6 +72,8 @@ import CasLogout from "./auth/CasLogout"; import ModelListPage from "./ModelListPage"; import ModelEditPage from "./ModelEditPage"; import SystemInfo from "./SystemInfo"; +import AdapterListPage from "./AdapterListPage"; +import AdapterEditPage from "./AdapterEditPage"; const {Header, Footer} = Layout; @@ -123,6 +125,8 @@ class App extends Component { this.setState({selectedMenuKey: "/permissions"}); } else if (uri.includes("/models")) { this.setState({selectedMenuKey: "/models"}); + } else if (uri.includes("/adapters")) { + this.setState({selectedMenuKey: "/adapters"}); } else if (uri.includes("/providers")) { this.setState({selectedMenuKey: "/providers"}); } else if (uri.includes("/applications")) { @@ -405,6 +409,13 @@ class App extends Component { ); + res.push( + + + {i18next.t("general:Adapters")} + + + ); res.push( @@ -544,6 +555,8 @@ class App extends Component { this.renderLoginIfNotLoggedIn()} /> this.renderLoginIfNotLoggedIn()} /> this.renderLoginIfNotLoggedIn()} /> + this.renderLoginIfNotLoggedIn()} /> + this.renderLoginIfNotLoggedIn()} /> this.renderLoginIfNotLoggedIn()} /> this.renderLoginIfNotLoggedIn()} /> this.renderLoginIfNotLoggedIn()} /> diff --git a/web/src/backend/AdapterBackend.js b/web/src/backend/AdapterBackend.js new file mode 100644 index 00000000..b9794abb --- /dev/null +++ b/web/src/backend/AdapterBackend.js @@ -0,0 +1,63 @@ +// Copyright 2022 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 getAdapters(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") { + return fetch(`${Setting.ServerUrl}/api/get-adapters?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, { + method: "GET", + credentials: "include", + }).then(res => res.json()); +} + +export function getAdapter(owner, name) { + return fetch(`${Setting.ServerUrl}/api/get-adapter?id=${owner}/${encodeURIComponent(name)}`, { + method: "GET", + credentials: "include", + }).then(res => res.json()); +} + +export function updateAdapter(owner, name, Adapter) { + const newAdapter = Setting.deepCopy(Adapter); + return fetch(`${Setting.ServerUrl}/api/update-adapter?id=${owner}/${encodeURIComponent(name)}`, { + method: "POST", + credentials: "include", + body: JSON.stringify(newAdapter), + }).then(res => res.json()); +} + +export function addAdapter(Adapter) { + const newAdapter = Setting.deepCopy(Adapter); + return fetch(`${Setting.ServerUrl}/api/add-adapter`, { + method: "POST", + credentials: "include", + body: JSON.stringify(newAdapter), + }).then(res => res.json()); +} + +export function deleteAdapter(Adapter) { + const newAdapter = Setting.deepCopy(Adapter); + return fetch(`${Setting.ServerUrl}/api/delete-adapter`, { + method: "POST", + credentials: "include", + body: JSON.stringify(newAdapter), + }).then(res => res.json()); +} + +export function syncPolicies(owner, name) { + return fetch(`${Setting.ServerUrl}/api/sync-policies?id=${owner}/${encodeURIComponent(name)}`, { + method: "GET", + credentials: "include", + }).then(res => res.json()); +} diff --git a/web/src/locales/de/data.json b/web/src/locales/de/data.json index 58c54821..2ecca2e7 100644 --- a/web/src/locales/de/data.json +++ b/web/src/locales/de/data.json @@ -5,6 +5,13 @@ "My Account": "Mein Konto", "Sign Up": "Registrieren" }, + "adapter": { + "Edit Adapter": "Edit Adapter", + "New Adapter": "New Adapter", + "Policies": "Policies", + "Policies - Tooltip": "Policies - Tooltip", + "Sync": "Sync" + }, "application": { "Background URL": "Background URL", "Background URL - Tooltip": "Background URL - Tooltip", @@ -107,6 +114,7 @@ "Action": "Aktion", "Adapter": "Adapter", "Adapter - Tooltip": "Adapter - Tooltip", + "Adapters": "Adapters", "Add": "Neu", "Affiliation URL": "Affiliation-URL", "Affiliation URL - Tooltip": "Unique string-style identifier", diff --git a/web/src/locales/en/data.json b/web/src/locales/en/data.json index e2fd4c1f..07886cae 100644 --- a/web/src/locales/en/data.json +++ b/web/src/locales/en/data.json @@ -5,6 +5,13 @@ "My Account": "My Account", "Sign Up": "Sign Up" }, + "adapter": { + "Edit Adapter": "Edit Adapter", + "New Adapter": "New Adapter", + "Policies": "Policies", + "Policies - Tooltip": "Policies - Tooltip", + "Sync": "Sync" + }, "application": { "Background URL": "Background URL", "Background URL - Tooltip": "Background URL - Tooltip", @@ -107,6 +114,7 @@ "Action": "Action", "Adapter": "Adapter", "Adapter - Tooltip": "Adapter - Tooltip", + "Adapters": "Adapters", "Add": "Add", "Affiliation URL": "Affiliation URL", "Affiliation URL - Tooltip": "Affiliation URL - Tooltip", diff --git a/web/src/locales/fr/data.json b/web/src/locales/fr/data.json index 2dec0853..2d8fbfc1 100644 --- a/web/src/locales/fr/data.json +++ b/web/src/locales/fr/data.json @@ -5,6 +5,13 @@ "My Account": "Mon Compte", "Sign Up": "S'inscrire" }, + "adapter": { + "Edit Adapter": "Edit Adapter", + "New Adapter": "New Adapter", + "Policies": "Policies", + "Policies - Tooltip": "Policies - Tooltip", + "Sync": "Sync" + }, "application": { "Background URL": "Background URL", "Background URL - Tooltip": "Background URL - Tooltip", @@ -107,6 +114,7 @@ "Action": "Action", "Adapter": "Adapter", "Adapter - Tooltip": "Adapter - Tooltip", + "Adapters": "Adapters", "Add": "Ajouter", "Affiliation URL": "URL d'affiliation", "Affiliation URL - Tooltip": "Unique string-style identifier", diff --git a/web/src/locales/ja/data.json b/web/src/locales/ja/data.json index ce14099d..cecebdef 100644 --- a/web/src/locales/ja/data.json +++ b/web/src/locales/ja/data.json @@ -5,6 +5,13 @@ "My Account": "マイアカウント", "Sign Up": "新規登録" }, + "adapter": { + "Edit Adapter": "Edit Adapter", + "New Adapter": "New Adapter", + "Policies": "Policies", + "Policies - Tooltip": "Policies - Tooltip", + "Sync": "Sync" + }, "application": { "Background URL": "Background URL", "Background URL - Tooltip": "Background URL - Tooltip", @@ -107,6 +114,7 @@ "Action": "アクション", "Adapter": "Adapter", "Adapter - Tooltip": "Adapter - Tooltip", + "Adapters": "Adapters", "Add": "追加", "Affiliation URL": "アフィリエイトURL", "Affiliation URL - Tooltip": "Unique string-style identifier", diff --git a/web/src/locales/ko/data.json b/web/src/locales/ko/data.json index b1ca65e8..e7a6d6c3 100644 --- a/web/src/locales/ko/data.json +++ b/web/src/locales/ko/data.json @@ -5,6 +5,13 @@ "My Account": "My Account", "Sign Up": "Sign Up" }, + "adapter": { + "Edit Adapter": "Edit Adapter", + "New Adapter": "New Adapter", + "Policies": "Policies", + "Policies - Tooltip": "Policies - Tooltip", + "Sync": "Sync" + }, "application": { "Background URL": "Background URL", "Background URL - Tooltip": "Background URL - Tooltip", @@ -107,6 +114,7 @@ "Action": "Action", "Adapter": "Adapter", "Adapter - Tooltip": "Adapter - Tooltip", + "Adapters": "Adapters", "Add": "Add", "Affiliation URL": "Affiliation URL", "Affiliation URL - Tooltip": "Unique string-style identifier", diff --git a/web/src/locales/ru/data.json b/web/src/locales/ru/data.json index 3448e685..cf28f25d 100644 --- a/web/src/locales/ru/data.json +++ b/web/src/locales/ru/data.json @@ -5,6 +5,13 @@ "My Account": "Мой аккаунт", "Sign Up": "Регистрация" }, + "adapter": { + "Edit Adapter": "Edit Adapter", + "New Adapter": "New Adapter", + "Policies": "Policies", + "Policies - Tooltip": "Policies - Tooltip", + "Sync": "Sync" + }, "application": { "Background URL": "Background URL", "Background URL - Tooltip": "Background URL - Tooltip", @@ -107,6 +114,7 @@ "Action": "Действие", "Adapter": "Adapter", "Adapter - Tooltip": "Adapter - Tooltip", + "Adapters": "Adapters", "Add": "Добавить", "Affiliation URL": "URL-адрес партнёра", "Affiliation URL - Tooltip": "Unique string-style identifier", diff --git a/web/src/locales/zh/data.json b/web/src/locales/zh/data.json index 43127f1d..0451af3f 100644 --- a/web/src/locales/zh/data.json +++ b/web/src/locales/zh/data.json @@ -5,6 +5,13 @@ "My Account": "我的账户", "Sign Up": "注册" }, + "adapter": { + "Edit Adapter": "编辑适配器", + "New Adapter": "添加适配器", + "Policies": "策略", + "Policies - Tooltip": "策略", + "Sync": "同步" + }, "application": { "Background URL": "背景图URL", "Background URL - Tooltip": "登录页背景图的链接", @@ -107,6 +114,7 @@ "Action": "操作", "Adapter": "适配器", "Adapter - Tooltip": "策略存储的表名", + "Adapters": "适配器", "Add": "添加", "Affiliation URL": "工作单位URL", "Affiliation URL - Tooltip": "工作单位URL",