diff --git a/controllers/record.go b/controllers/record.go new file mode 100644 index 0000000..0fa201f --- /dev/null +++ b/controllers/record.go @@ -0,0 +1,108 @@ +// Copyright 2024 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/casbin/caswaf/object" +) + +func (c *ApiController) GetRecords() { + if c.RequireSignedIn() { + return + } + + owner := c.Input().Get("owner") + if owner == "admin" { + owner = "" + } + + sites, err := object.GetRecords(owner) + if err != nil { + c.ResponseError(err.Error()) + return + } + + // object.GetMaskedSites(sites, util.GetHostname()) + c.ResponseOk(sites) +} + +func (c *ApiController) DeleteRecord() { + if c.RequireSignedIn() { + return + } + + var record object.Record + err := json.Unmarshal(c.Ctx.Input.RequestBody, &record) + if err != nil { + c.ResponseError(err.Error()) + return + } + + c.Data["json"] = wrapActionResponse(object.DeleteRecord(&record)) + c.ServeJSON() +} + +func (c *ApiController) UpdateRecord() { + if c.RequireSignedIn() { + return + } + + owner := c.Input().Get("owner") + id := c.Input().Get("id") + + var record object.Record + err := json.Unmarshal(c.Ctx.Input.RequestBody, &record) + if err != nil { + c.ResponseError(err.Error()) + return + } + + c.Data["json"] = wrapActionResponse(object.UpdateRecord(owner, id, &record)) + c.ServeJSON() +} + +func (c *ApiController) GetRecord() { + if c.RequireSignedIn() { + return + } + + owner := c.Input().Get("owner") + id := c.Input().Get("id") + record, err := object.GetRecord(owner, id) + if err != nil { + c.ResponseError(err.Error()) + return + } + + c.ResponseOk(record) +} + +func (c *ApiController) AddRecord() { + if c.RequireSignedIn() { + return + } + + var record object.Record + err := json.Unmarshal(c.Ctx.Input.RequestBody, &record) + if err != nil { + c.ResponseError(err.Error()) + return + } + + c.Data["json"] = wrapActionResponse(object.AddRecord(&record)) + c.ServeJSON() +} diff --git a/object/ormer.go b/object/ormer.go index 3b12a19..40ac973 100644 --- a/object/ormer.go +++ b/object/ormer.go @@ -188,4 +188,9 @@ func (a *Ormer) createTable() { if err != nil { panic(err) } + + err = a.Engine.Sync2(new(Record)) + if err != nil { + panic(err) + } } diff --git a/object/record.go b/object/record.go new file mode 100644 index 0000000..a833ab4 --- /dev/null +++ b/object/record.go @@ -0,0 +1,97 @@ +// Copyright 2024 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 ( + "strconv" + + "github.com/xorm-io/core" +) + +type Record struct { + Id int64 `xorm:"int notnull pk autoincr" json:"id"` + Owner string `xorm:"varchar(100) notnull" json:"owner"` + CreatedTime string `xorm:"varchar(100) notnull" json:"createdTime"` + + Method string `xorm:"varchar(100)" json:"method"` + Host string `xorm:"varchar(100)" json:"host"` + Path string `xorm:"varchar(100)" json:"path"` + ClientIp string `xorm:"varchar(100)" json:"clientIp"` + UserAgent string `xorm:"varchar(512)" json:"userAgent"` +} + +func GetRecords(owner string) ([]*Record, error) { + records := []*Record{} + err := ormer.Engine.Asc("id").Asc("host").Find(&records, &Record{Owner: owner}) + if err != nil { + return nil, err + } + + return records, nil +} + +func AddRecord(record *Record) (bool, error) { + affected, err := ormer.Engine.Insert(record) + if err != nil { + return false, err + } + + return affected != 0, nil +} + +func DeleteRecord(record *Record) (bool, error) { + affected, err := ormer.Engine.ID(core.PK{record.Id}).Delete(&Record{}) + if err != nil { + return false, err + } + + return affected != 0, nil +} + +func UpdateRecord(owner string, id string, record *Record) (bool, error) { + affected, err := ormer.Engine.ID(core.PK{record.Id}).AllCols().Update(record) + if err != nil { + return false, err + } + + return affected != 0, nil +} + +func GetRecord(owner string, id string) (*Record, error) { + idNum, err := strconv.Atoi(id) + if err != nil { + return nil, err + } + + record, err := getRecord(owner, int64(idNum)) + if err != nil { + return nil, err + } + + return record, nil +} + +func getRecord(owner string, id int64) (*Record, error) { + record := Record{Owner: owner, Id: id} + existed, err := ormer.Engine.Get(&record) + if err != nil { + return nil, err + } + + if existed { + return &record, nil + } + return nil, nil +} diff --git a/routers/router.go b/routers/router.go index 8720161..89de865 100644 --- a/routers/router.go +++ b/routers/router.go @@ -52,4 +52,11 @@ func initAPI() { beego.Router("/api/delete-cert", &controllers.ApiController{}, "POST:DeleteCert") beego.Router("/api/get-applications", &controllers.ApiController{}, "GET:GetApplications") + + beego.Router("/api/get-records", &controllers.ApiController{}, "GET:GetRecords") + beego.Router("/api/get-record", &controllers.ApiController{}, "GET:GetRecord") + beego.Router("/api/delete-record", &controllers.ApiController{}, "POST:DeleteRecord") + beego.Router("/api/update-record", &controllers.ApiController{}, "POST:UpdateRecord") + beego.Router("/api/add-record", &controllers.ApiController{}, "POST:AddRecord") + } diff --git a/service/proxy.go b/service/proxy.go index 24b92f8..7277d66 100644 --- a/service/proxy.go +++ b/service/proxy.go @@ -17,7 +17,6 @@ package service import ( "crypto/tls" "fmt" - httptx "github.com/corazawaf/coraza/v3/http" "net" "net/http" "net/http/httputil" @@ -28,6 +27,7 @@ import ( "github.com/beego/beego" "github.com/casbin/caswaf/object" "github.com/casbin/caswaf/util" + httptx "github.com/corazawaf/coraza/v3/http" ) func forwardHandler(targetUrl string, writer http.ResponseWriter, request *http.Request) { @@ -66,6 +66,41 @@ func getHostNonWww(host string) string { return res } +func getClientIp(r *http.Request) string { + forwarded := r.Header.Get("X-Forwarded-For") + if forwarded != "" { + clientIP := strings.Split(forwarded, ",")[0] + return strings.TrimSpace(clientIP) + } + + realIP := r.Header.Get("X-Real-IP") + if realIP != "" { + return realIP + } + + ip, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + return r.RemoteAddr + } + return ip +} + +func logRequest(clientIp string, r *http.Request) { + if !strings.Contains(r.UserAgent(), "Uptime-Kuma") { + fmt.Printf("handleRequest: %s\t%s\t%s\t%s\t%s\n", r.RemoteAddr, r.Method, r.Host, r.RequestURI, r.UserAgent()) + record := object.Record{ + Owner: "admin", + CreatedTime: util.GetCurrentTime(), + Method: r.Method, + Host: r.Host, + Path: r.RequestURI, + ClientIp: clientIp, + UserAgent: r.UserAgent(), + } + object.AddRecord(&record) + } +} + func redirectToHttps(w http.ResponseWriter, r *http.Request) { targetUrl := fmt.Sprintf("https://%s", joinPath(r.Host, r.RequestURI)) http.Redirect(w, r, targetUrl, http.StatusMovedPermanently) @@ -82,9 +117,8 @@ func redirectToHost(w http.ResponseWriter, r *http.Request, host string) { } func handleRequest(w http.ResponseWriter, r *http.Request) { - if !strings.Contains(r.UserAgent(), "Uptime-Kuma") { - fmt.Printf("handleRequest: %s\t%s\t%s\t%s\t%s\n", r.RemoteAddr, r.Method, r.Host, r.RequestURI, r.UserAgent()) - } + clientIp := getClientIp(r) + logRequest(clientIp, r) site := getSiteByDomainWithWww(r.Host) if site == nil { diff --git a/web/src/App.js b/web/src/App.js index b23b7ff..40d0b05 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -27,8 +27,9 @@ import SiteEditPage from "./SiteEditPage"; import CertListPage from "./CertListPage"; import CertEditPage from "./CertEditPage"; import SigninPage from "./SigninPage"; +import RecordListPage from "./RecordListPage"; +import RecordEditPage from "./RecordEditPage"; import i18next from "i18next"; -// import SelectLanguageBox from "./SelectLanguageBox"; const {Header, Footer} = Layout; @@ -71,6 +72,8 @@ class App extends Component { this.setState({selectedMenuKey: "/sites"}); } else if (uri.includes("/certs")) { this.setState({selectedMenuKey: "/certs"}); + } else if (uri.includes("/records")) { + this.setState({selectedMenuKey: "/records"}); } else { this.setState({selectedMenuKey: "null"}); } @@ -253,6 +256,13 @@ class App extends Component { ); + res.push( + + + {i18next.t("general:Records")} + + + ); return res; } @@ -310,6 +320,9 @@ class App extends Component { this.renderSigninIfNotSignedIn()} /> this.renderSigninIfNotSignedIn()} /> this.renderSigninIfNotSignedIn()} /> + + this.renderSigninIfNotSignedIn()} /> + this.renderSigninIfNotSignedIn()} /> ); diff --git a/web/src/BaseListPage.js b/web/src/BaseListPage.js new file mode 100644 index 0000000..d6bb90b --- /dev/null +++ b/web/src/BaseListPage.js @@ -0,0 +1,64 @@ +// Copyright 2024 The CasWAF 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, Result} from "antd"; +import i18next from "i18next"; + +class BaseListPage extends React.Component { + constructor(props) { + super(props); + this.state = { + classes: props, + data: [], + pagination: { + current: 1, + pageSize: 10, + }, + loading: false, + isAuthorized: true, + }; + } + + UNSAFE_componentWillMount() { + this.fetch(); + } + + handleTableChange = () => { + this.fetch(); + }; + + render() { + if (!this.state.isAuthorized) { + return ( + } + /> + ); + } + + return ( +
+ { + this.renderTable(this.state.data) + } +
+ ); + } +} + +export default BaseListPage; diff --git a/web/src/CertListPage.js b/web/src/CertListPage.js index 68aa54b..0896542 100644 --- a/web/src/CertListPage.js +++ b/web/src/CertListPage.js @@ -14,38 +14,36 @@ import React from "react"; import {Link} from "react-router-dom"; -import {Button, Col, Popconfirm, Row, Table} from "antd"; +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 copy from "copy-to-clipboard"; +import BaseListPage from "./BaseListPage"; -class CertListPage extends React.Component { - constructor(props) { - super(props); - this.state = { - classes: props, - certs: null, - }; - } +class CertListPage extends BaseListPage { UNSAFE_componentWillMount() { - this.getCerts(); + this.fetch(); } - getCerts() { + fetch = (params = {}) => { + this.setState({loading: true}); CertBackend.getCerts(this.props.account.name) .then((res) => { + this.setState({ + loading: false, + }); if (res.status === "ok") { this.setState({ - certs: res.data, + data: res.data, }); } else { Setting.showMessage("error", `Failed to get certs: ${res.msg}`); } }); - } + }; newCert() { const randomName = Setting.getRandomName(); @@ -71,7 +69,7 @@ class CertListPage extends React.Component { } else { Setting.showMessage("success", "Cert added successfully"); this.setState({ - certs: Setting.prependRow(this.state.certs, newCert), + data: Setting.prependRow(this.state.data, newCert), }); } } @@ -82,14 +80,14 @@ class CertListPage extends React.Component { } deleteCert(i) { - CertBackend.deleteCert(this.state.certs[i]) + CertBackend.deleteCert(this.state.data[i]) .then((res) => { if (res.status === "error") { Setting.showMessage("error", `Failed to delete: ${res.msg}`); } else { Setting.showMessage("success", "Cert deleted successfully"); this.setState({ - certs: Setting.deleteRow(this.state.certs, i), + data: Setting.deleteRow(this.state.data, i), }); } } @@ -99,7 +97,7 @@ class CertListPage extends React.Component { }); } - renderTable(certs) { + renderTable(data) { const columns = [ { title: i18next.t("general:Owner"), @@ -262,32 +260,19 @@ class CertListPage extends React.Component { return (
- (
{i18next.t("general:Certs")}    
)} - loading={certs === null} + loading={this.state.loading} + onChange={this.handleTableChange} /> ); } - - render() { - return ( -
- -
- { - this.renderTable(this.state.certs) - } - - - - ); - } } export default CertListPage; diff --git a/web/src/RecordEditPage.js b/web/src/RecordEditPage.js new file mode 100644 index 0000000..e56b1bb --- /dev/null +++ b/web/src/RecordEditPage.js @@ -0,0 +1,189 @@ +// Copyright 2023 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} from "antd"; +import * as RecordBackend from "./backend/RecordBackend"; +import * as Setting from "./Setting"; +import i18next from "i18next"; + +// const {Option} = Select; + +class RecordEditPage extends React.Component { + constructor(props) { + super(props); + this.state = { + classes: props, + owner: props.match.params.owner, + id: props.match.params.id, + record: null, + }; + } + + UNSAFE_componentWillMount() { + this.getRecord(); + } + + getRecord() { + RecordBackend.getRecord(this.state.owner, this.state.id) + .then((res) => { + if (res.status === "ok") { + this.setState({ + record: res.data, + }); + } else { + Setting.showMessage("error", `Failed to get record: ${res.msg}`); + } + }); + } + + updateRecordField(key, value) { + const record = this.state.record; + record[key] = value; + this.setState({ + record: record, + }); + } + + submitRecordEdit() { + const record = Setting.deepCopy(this.state.record); + RecordBackend.updateRecord(this.state.record.owner, this.state.id, record) + .then((res) => { + if (res.status === "error") { + Setting.showMessage("error", `Failed to save: ${res.msg}`); + this.updateRecordField("id", this.state.id); + } else { + Setting.showMessage("success", "Successfully saved"); + this.setState({ + id: this.state.record.id, + }); + this.props.history.push(`/records/${this.state.record.owner}/${this.state.record.id}`); + this.getRecord(); + } + }) + .catch(error => { + Setting.showMessage("error", `failed to save: ${error}`); + }); + } + + renderRecord() { + return ( + + {i18next.t("general:Edit Record")}     + + + } style={{marginLeft: "5px"}} type="inner"> + + + {i18next.t("general:Owner")}: + + + { + this.updateRecordField("owner", e.target.value); + }} /> + + + + + {i18next.t("general:CreatedTime")}: + + + { + this.updateRecordField("createdTime", e.target.value); + }} /> + + + + + {i18next.t("general:Method")}: + + + { + this.updateRecordField("method", e.target.value); + }} /> + + + + + {i18next.t("general:Host")}: + + + { + this.updateRecordField("host", e.target.value); + }} /> + + + + + {i18next.t("general:Path")}: + + + { + this.updateRecordField("path", e.target.value); + }} /> + + + + + {i18next.t("general:Client ip")}: + + + { + this.updateRecordField("clientIp", e.target.value); + }} /> + + + + + {i18next.t("general:UserAgent")}: + + + { + this.updateRecordField("userAgent", e.target.value); + }} /> + + + + ); + } + + render() { + return ( +
+ +
+ + + { + this.state.record !== null ? this.renderRecord() : null + } + + + + + + + + + + + + + ); + } + +} + +export default RecordEditPage; diff --git a/web/src/RecordListPage.js b/web/src/RecordListPage.js new file mode 100644 index 0000000..c7fef48 --- /dev/null +++ b/web/src/RecordListPage.js @@ -0,0 +1,199 @@ +// Copyright 2021 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, Popconfirm, Table} from "antd"; +import moment from "moment"; +import * as Setting from "./Setting"; +import * as RecordBackend from "./backend/RecordBackend"; +import i18next from "i18next"; +import BaseListPage from "./BaseListPage"; + +class RecordListPage extends BaseListPage { + + UNSAFE_componentWillMount() { + this.fetch(); + } + + fetch = (params = {}) => { + this.setState({loading: true}); + RecordBackend.getRecords(this.props.account.name) + .then((res) => { + this.setState({ + loading: false, + }); + if (res.status === "ok") { + this.setState({ + data: res.data, + }); + } else { + Setting.showMessage("error", `Failed to get records: ${res.msg}`); + } + }); + }; + + newRecord() { + const randomName = Setting.getRandomName(); + return { + owner: this.props.account.name, + name: `record_${randomName}`, + createdTime: moment().format(), + method: "GET", + host: "door.casdoor.com", + path: "/", + userAgent: "", + }; + } + + addRecord() { + const newRecord = this.newRecord(); + RecordBackend.addRecord(newRecord) + .then((res) => { + if (res.status === "error") { + Setting.showMessage("error", `Failed to add: ${res.msg}`); + } else { + Setting.showMessage("success", "Record added successfully"); + this.setState({ + data: Setting.addRow(this.state.data, res.data), + }); + this.fetch(); + } + } + ) + .catch(error => { + Setting.showMessage("error", `Record failed to add: ${error}`); + }); + } + + deleteRecord(i) { + RecordBackend.deleteRecord(this.state.data[i]) + .then((res) => { + if (res.status === "error") { + Setting.showMessage("error", `Failed to delete: ${res.msg}`); + } else { + Setting.showMessage("success", "Record deleted successfully"); + this.setState({ + data: Setting.deleteRow(this.state.data, i), + }); + } + } + ) + .catch(error => { + Setting.showMessage("error", `Record failed to delete: ${error}`); + }); + } + + renderTable(data) { + const columns = [ + { + title: i18next.t("general:ID"), + dataIndex: "id", + key: "id", + width: "30px", + sorter: (a, b) => a.id - b.id, + }, + { + title: i18next.t("general:Owner"), + dataIndex: "owner", + key: "owner", + width: "30px", + sorter: (a, b) => a.owner.localeCompare(b.owner), + }, + { + title: i18next.t("general:Created time"), + dataIndex: "createdTime", + key: "createdTime", + width: "70px", + sorter: (a, b) => a.createdTime.localeCompare(b.createdTime), + render: (text, record, index) => { + return Setting.getFormattedDate(text); + }, + }, + { + title: i18next.t("general:Method"), + dataIndex: "method", + key: "method", + width: "30px", + sorter: (a, b) => a.method.localeCompare(b.method), + }, + { + title: i18next.t("general:Host"), + dataIndex: "host", + key: "host", + width: "50px", + sorter: (a, b) => a.host.localeCompare(b.host), + }, + { + title: i18next.t("general:Path"), + dataIndex: "path", + key: "path", + width: "100px", + sorter: (a, b) => a.path.localeCompare(b.path), + }, + { + title: i18next.t("general:Client ip"), + dataIndex: "clientIp", + key: "clientIp", + width: "100px", + sorter: (a, b) => a.clientIp.localeCompare(b.clientIp), + }, + { + title: i18next.t("general:User agent"), + dataIndex: "userAgent", + key: "userAgent", + width: "240px", + sorter: (a, b) => a.userAgent.localeCompare(b.userAgent), + }, + { + title: i18next.t("general:Action"), + dataIndex: "action", + key: "action", + width: "180px", + render: (text, record, index) => { + return ( +
+ + this.deleteRecord(index)} + okText="OK" + cancelText="Cancel" + > + + +
+ ); + }, + }, + ]; + + return ( +
+
( +
+ {i18next.t("general:Records")}     + +
+ )} + loading={this.state.loading} + onChange={this.handleTableChange} + /> + + ); + } + +} + +export default RecordListPage; diff --git a/web/src/SiteListPage.js b/web/src/SiteListPage.js index b08088d..84a7ce9 100644 --- a/web/src/SiteListPage.js +++ b/web/src/SiteListPage.js @@ -14,37 +14,35 @@ import React from "react"; import {Link} from "react-router-dom"; -import {Button, Col, Popconfirm, Row, Table, Tag, Tooltip} from "antd"; +import {Button, Popconfirm, Table, Tag, Tooltip} from "antd"; import moment from "moment"; import * as Setting from "./Setting"; import * as SiteBackend from "./backend/SiteBackend"; import i18next from "i18next"; +import BaseListPage from "./BaseListPage"; -class SiteListPage extends React.Component { - constructor(props) { - super(props); - this.state = { - classes: props, - sites: null, - }; - } +class SiteListPage extends BaseListPage { UNSAFE_componentWillMount() { - this.getSites(); + this.fetch(); } - getSites() { + fetch = (params = {}) => { + this.setState({loading: true}); SiteBackend.getSites(this.props.account.name) .then((res) => { + this.setState({ + loading: false, + }); if (res.status === "ok") { this.setState({ - sites: res.data, + data: res.data, }); } else { Setting.showMessage("error", `Failed to get sites: ${res.msg}`); } }); - } + }; newSite() { const randomName = Setting.getRandomName(); @@ -62,7 +60,7 @@ class SiteListPage extends React.Component { port: 8000, sslMode: "HTTPS Only", sslCert: "", - publicIp: "", + publicIp: "8.131.81.162", node: "", isSelf: false, nodes: [], @@ -79,7 +77,7 @@ class SiteListPage extends React.Component { } else { Setting.showMessage("success", "Site added successfully"); this.setState({ - sites: Setting.prependRow(this.state.sites, newSite), + data: Setting.prependRow(this.state.data, newSite), }); } } @@ -90,14 +88,14 @@ class SiteListPage extends React.Component { } deleteSite(i) { - SiteBackend.deleteSite(this.state.sites[i]) + SiteBackend.deleteSite(this.state.data[i]) .then((res) => { if (res.status === "error") { Setting.showMessage("error", `Failed to delete: ${res.msg}`); } else { Setting.showMessage("success", "Site deleted successfully"); this.setState({ - sites: Setting.deleteRow(this.state.sites, i), + data: Setting.deleteRow(this.state.data, i), }); } } @@ -107,7 +105,7 @@ class SiteListPage extends React.Component { }); } - renderTable(sites) { + renderTable(data) { // const renderExternalLink = () => { // return ( //
-
(
{i18next.t("general:Sites")}    
)} - loading={sites === null} + loading={data === null} + onChange={this.handleTableChange} /> ); } - render() { - return ( -
- -
- { - this.renderTable(this.state.sites) - } - - - - ); - } } export default SiteListPage; diff --git a/web/src/backend/RecordBackend.js b/web/src/backend/RecordBackend.js new file mode 100644 index 0000000..a05f87d --- /dev/null +++ b/web/src/backend/RecordBackend.js @@ -0,0 +1,56 @@ +// Copyright 2023 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 getRecords(owner) { + return fetch(`${Setting.ServerUrl}/api/get-records?owner=${owner}`, { + method: "GET", + credentials: "include", + }).then(res => res.json()); +} + +export function deleteRecord(record) { + const newRecord = Setting.deepCopy(record); + return fetch(`${Setting.ServerUrl}/api/delete-record`, { + method: "POST", + credentials: "include", + body: JSON.stringify(newRecord), + }).then(res => res.json()); +} + +export function getRecord(owner, id) { + return fetch(`${Setting.ServerUrl}/api/get-record?owner=${owner}&id=${id}`, { + method: "GET", + credentials: "include", + }).then(res => res.json()); +} + +export function updateRecord(owner, id, record) { + const newRecord = Setting.deepCopy(record); + return fetch(`${Setting.ServerUrl}/api/update-record?owner=${owner}&id=${id}`, { + method: "POST", + credentials: "include", + body: JSON.stringify(newRecord), + }).then(res => res.json()); +} + +export function addRecord(record) { + const newRecord = Setting.deepCopy(record); + return fetch(`${Setting.ServerUrl}/api/add-record`, { + method: "POST", + credentials: "include", + body: JSON.stringify(newRecord), + }).then(res => res.json()); +}