mirror of
https://github.com/casdoor/casdoor.git
synced 2025-05-23 02:35:49 +08:00
feat: destroy session after delete user (#1441)
* fix: destroy session after delete user * feat: visual session * fix: go lint * feat: add translation * feat: auto flush after offline * fix: delete one session * fix: move 403 page to baseListPage
This commit is contained in:
parent
4ab2ca7a25
commit
b525210835
@ -242,6 +242,7 @@ func (c *ApiController) Signup() {
|
||||
// @router /logout [get,post]
|
||||
func (c *ApiController) Logout() {
|
||||
user := c.GetSessionUsername()
|
||||
object.DeleteSessionId(user, c.Ctx.Input.CruSession.SessionID())
|
||||
util.LogInfo(c.Ctx, "API: [%s] logged out", user)
|
||||
|
||||
application := c.GetSessionApplication()
|
||||
|
@ -139,6 +139,10 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
})
|
||||
}
|
||||
|
||||
if resp.Status == "ok" {
|
||||
object.SetSession(user.GetId(), c.Ctx.Input.CruSession.SessionID())
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
|
68
controllers/session.go
Normal file
68
controllers/session.go
Normal file
@ -0,0 +1,68 @@
|
||||
// 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/beego/beego/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
// DeleteSession
|
||||
// @Title DeleteSession
|
||||
// @Tag Session API
|
||||
// @Description Delete session by userId
|
||||
// @Param ID query string true "The ID(owner/name) of user."
|
||||
// @Success 200 {array} string The Response object
|
||||
// @router /delete-session [post]
|
||||
func (c *ApiController) DeleteSession() {
|
||||
var session object.Session
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &session)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteSession(util.GetId(session.Owner, session.Name)))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetSessions
|
||||
// @Title GetSessions
|
||||
// @Tag Session API
|
||||
// @Description Get organization user sessions
|
||||
// @Param owner query string true "The organization name"
|
||||
// @Success 200 {array} string The Response object
|
||||
// @router /get-sessions [get]
|
||||
func (c *ApiController) GetSessions() {
|
||||
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")
|
||||
owner := c.Input().Get("owner")
|
||||
if limit == "" || page == "" {
|
||||
c.Data["json"] = object.GetSessions(owner)
|
||||
c.ServeJSON()
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetSessionCount(owner, field, value)))
|
||||
sessions := object.GetPaginationSessions(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
c.ResponseOk(sessions, paginator.Nums())
|
||||
}
|
||||
}
|
@ -222,6 +222,11 @@ func (a *Adapter) createTable() {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = a.Engine.Sync2(new(Session))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func GetSession(owner string, offset, limit int, field, value, sortField, sortOrder string) *xorm.Session {
|
||||
|
132
object/session.go
Normal file
132
object/session.go
Normal file
@ -0,0 +1,132 @@
|
||||
// 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 (
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"xorm.io/core"
|
||||
)
|
||||
|
||||
type Session 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"`
|
||||
|
||||
SessionId []string `json:"sessionId"`
|
||||
}
|
||||
|
||||
func SetSession(id string, sessionId string) {
|
||||
owner, name := util.GetOwnerAndNameFromIdNoCheck(id)
|
||||
session := &Session{Owner: owner, Name: name}
|
||||
get, err := adapter.Engine.Get(session)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
session.SessionId = append(session.SessionId, sessionId)
|
||||
if get {
|
||||
_, err = adapter.Engine.ID(core.PK{owner, name}).Update(session)
|
||||
} else {
|
||||
session.CreatedTime = time.Now().Format(time.RFC3339)
|
||||
_, err = adapter.Engine.Insert(session)
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteSession(id string) bool {
|
||||
owner, name := util.GetOwnerAndNameFromIdNoCheck(id)
|
||||
|
||||
session := &Session{Owner: owner, Name: name}
|
||||
_, err := adapter.Engine.ID(core.PK{owner, name}).Get(session)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
DeleteBeegoSession(session.SessionId)
|
||||
|
||||
affected, err := adapter.Engine.ID(core.PK{owner, name}).Delete(session)
|
||||
return affected != 0
|
||||
}
|
||||
|
||||
func DeleteSessionId(id string, sessionId string) bool {
|
||||
owner, name := util.GetOwnerAndNameFromIdNoCheck(id)
|
||||
|
||||
session := &Session{Owner: owner, Name: name}
|
||||
_, err := adapter.Engine.ID(core.PK{owner, name}).Get(session)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
DeleteBeegoSession([]string{sessionId})
|
||||
session.SessionId = util.DeleteVal(session.SessionId, sessionId)
|
||||
|
||||
if len(session.SessionId) < 1 {
|
||||
affected, _ := adapter.Engine.ID(core.PK{owner, name}).Delete(session)
|
||||
return affected != 0
|
||||
} else {
|
||||
affected, _ := adapter.Engine.ID(core.PK{owner, name}).Update(session)
|
||||
return affected != 0
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteBeegoSession(sessionIds []string) {
|
||||
for _, sessionId := range sessionIds {
|
||||
err := beego.GlobalSessions.GetProvider().SessionDestroy(sessionId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetSessions(owner string) []*Session {
|
||||
sessions := []*Session{}
|
||||
var err error
|
||||
if owner != "" {
|
||||
err = adapter.Engine.Desc("created_time").Where("owner = ?", owner).Find(&sessions)
|
||||
} else {
|
||||
err = adapter.Engine.Desc("created_time").Find(&sessions)
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return sessions
|
||||
}
|
||||
|
||||
func GetPaginationSessions(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Session {
|
||||
sessions := []*Session{}
|
||||
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
|
||||
err := session.Find(&sessions)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return sessions
|
||||
}
|
||||
|
||||
func GetSessionCount(owner, field, value string) int {
|
||||
session := GetSession(owner, -1, -1, field, value, "", "")
|
||||
count, err := session.Count(&Session{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return int(count)
|
||||
}
|
@ -529,6 +529,9 @@ func AddUsersInBatch(users []*User) bool {
|
||||
}
|
||||
|
||||
func DeleteUser(user *User) bool {
|
||||
// Forced offline the user first
|
||||
DeleteSession(user.GetId())
|
||||
|
||||
affected, err := adapter.Engine.ID(core.PK{user.Owner, user.Name}).Delete(&User{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
@ -164,6 +164,9 @@ func initAPI() {
|
||||
beego.Router("/api/get-records-filter", &controllers.ApiController{}, "POST:GetRecordsByFilter")
|
||||
beego.Router("/api/add-record", &controllers.ApiController{}, "POST:AddRecord")
|
||||
|
||||
beego.Router("/api/get-sessions", &controllers.ApiController{}, "GET:GetSessions")
|
||||
beego.Router("/api/delete-session", &controllers.ApiController{}, "POST:DeleteSession")
|
||||
|
||||
beego.Router("/api/get-webhooks", &controllers.ApiController{}, "GET:GetWebhooks")
|
||||
beego.Router("/api/get-webhook", &controllers.ApiController{}, "GET:GetWebhook")
|
||||
beego.Router("/api/update-webhook", &controllers.ApiController{}, "POST:UpdateWebhook")
|
||||
|
25
util/slice.go
Normal file
25
util/slice.go
Normal file
@ -0,0 +1,25 @@
|
||||
// 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 util
|
||||
|
||||
func DeleteVal(values []string, val string) []string {
|
||||
newValues := []string{}
|
||||
for _, v := range values {
|
||||
if v != val {
|
||||
newValues = append(newValues, v)
|
||||
}
|
||||
}
|
||||
return newValues
|
||||
}
|
@ -9,7 +9,7 @@
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"antd": "5.0.5",
|
||||
"antd": "5.1.2",
|
||||
"codemirror": "^5.61.1",
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
"core-js": "^3.25.0",
|
||||
|
@ -260,6 +260,13 @@ class AdapterListPage extends BaseListPage {
|
||||
searchText: params.searchText,
|
||||
searchedColumn: params.searchedColumn,
|
||||
});
|
||||
} else {
|
||||
if (res.msg.includes("Unauthorized")) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
isAuthorized: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -70,6 +70,7 @@ import AdapterListPage from "./AdapterListPage";
|
||||
import AdapterEditPage from "./AdapterEditPage";
|
||||
import {withTranslation} from "react-i18next";
|
||||
import SelectThemeBox from "./SelectThemeBox";
|
||||
import SessionListPage from "./SessionListPage";
|
||||
|
||||
const {Header, Footer, Content} = Layout;
|
||||
|
||||
@ -421,6 +422,10 @@ class App extends Component {
|
||||
"/tokens"
|
||||
));
|
||||
|
||||
res.push(Setting.getItem(<Link to="/sessions">{i18next.t("general:Sessions")}</Link>,
|
||||
"/sessions"
|
||||
));
|
||||
|
||||
res.push(Setting.getItem(<Link to="/webhooks">{i18next.t("general:Webhooks")}</Link>,
|
||||
"/webhooks"
|
||||
));
|
||||
@ -517,6 +522,7 @@ class App extends Component {
|
||||
<Route exact path="/ldap/:ldapId" render={(props) => this.renderLoginIfNotLoggedIn(<LdapEditPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/ldap/sync/:ldapId" render={(props) => this.renderLoginIfNotLoggedIn(<LdapSyncPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/tokens" render={(props) => this.renderLoginIfNotLoggedIn(<TokenListPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/sessions" render={(props) => this.renderLoginIfNotLoggedIn(<SessionListPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/tokens/:tokenName" render={(props) => this.renderLoginIfNotLoggedIn(<TokenEditPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/webhooks" render={(props) => this.renderLoginIfNotLoggedIn(<WebhookListPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/webhooks/:webhookName" render={(props) => this.renderLoginIfNotLoggedIn(<WebhookEditPage account={this.state.account} {...props} />)} />
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Card, Col, Input, Popover, Radio, Row, Select, Switch, Upload} from "antd";
|
||||
import {Button, Card, Col, Input, Popover, Radio, Result, Row, Select, Switch, Upload} from "antd";
|
||||
import {CopyOutlined, LinkOutlined, UploadOutlined} from "@ant-design/icons";
|
||||
import * as ApplicationBackend from "./backend/ApplicationBackend";
|
||||
import * as CertBackend from "./backend/CertBackend";
|
||||
@ -103,6 +103,7 @@ class ApplicationEditPage extends React.Component {
|
||||
uploading: false,
|
||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||
samlMetadata: null,
|
||||
isAuthorized: true,
|
||||
};
|
||||
}
|
||||
|
||||
@ -129,9 +130,15 @@ class ApplicationEditPage extends React.Component {
|
||||
getOrganizations() {
|
||||
OrganizationBackend.getOrganizations("admin")
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
organizations: (res.msg === undefined) ? res : [],
|
||||
});
|
||||
if (res?.status === "error") {
|
||||
this.setState({
|
||||
isAuthorized: false,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
organizations: (res.msg === undefined) ? res : [],
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -838,6 +845,17 @@ class ApplicationEditPage extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.isAuthorized) {
|
||||
return (
|
||||
<Result
|
||||
status="403"
|
||||
title="403 Unauthorized"
|
||||
subTitle={i18next.t("general:Sorry, you do not have permission to access this page or logged in status invalid.")}
|
||||
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
|
@ -14,7 +14,7 @@
|
||||
|
||||
import React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {Button, Col, List, Popconfirm, Row, Table, Tooltip} from "antd";
|
||||
import {Button, Col, List, Popconfirm, Result, Row, Table, Tooltip} from "antd";
|
||||
import {EditOutlined} from "@ant-design/icons";
|
||||
import moment from "moment";
|
||||
import * as Setting from "./Setting";
|
||||
@ -36,6 +36,7 @@ class ApplicationListPage extends BaseListPage {
|
||||
loading: false,
|
||||
searchText: "",
|
||||
searchedColumn: "",
|
||||
isAuthorized: true,
|
||||
};
|
||||
}
|
||||
|
||||
@ -258,6 +259,17 @@ class ApplicationListPage extends BaseListPage {
|
||||
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
|
||||
};
|
||||
|
||||
if (!this.state.isAuthorized) {
|
||||
return (
|
||||
<Result
|
||||
status="403"
|
||||
title="403 Unauthorized"
|
||||
subTitle={i18next.t("general:Sorry, you do not have permission to access this page or logged in status invalid.")}
|
||||
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table scroll={{x: "max-content"}} columns={columns} dataSource={applications} rowKey="name" size="middle" bordered pagination={paginationProps}
|
||||
@ -292,6 +304,13 @@ class ApplicationListPage extends BaseListPage {
|
||||
searchText: params.searchText,
|
||||
searchedColumn: params.searchedColumn,
|
||||
});
|
||||
} else {
|
||||
if (res.msg.includes("Unauthorized")) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
isAuthorized: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -13,9 +13,10 @@
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Input, Space} from "antd";
|
||||
import {Button, Input, Result, Space} from "antd";
|
||||
import {SearchOutlined} from "@ant-design/icons";
|
||||
import Highlighter from "react-highlight-words";
|
||||
import i18next from "i18next";
|
||||
|
||||
class BaseListPage extends React.Component {
|
||||
constructor(props) {
|
||||
@ -127,6 +128,17 @@ class BaseListPage extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.state.isAuthorized) {
|
||||
return (
|
||||
<Result
|
||||
status="403"
|
||||
title="403 Unauthorized"
|
||||
subTitle={i18next.t("general:Sorry, you do not have permission to access this page or logged in status invalid.")}
|
||||
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
|
@ -227,6 +227,13 @@ class CertListPage extends BaseListPage {
|
||||
searchText: params.searchText,
|
||||
searchedColumn: params.searchedColumn,
|
||||
});
|
||||
} else {
|
||||
if (res.msg.includes("Unauthorized")) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
isAuthorized: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -200,6 +200,13 @@ class ModelListPage extends BaseListPage {
|
||||
searchText: params.searchText,
|
||||
searchedColumn: params.searchedColumn,
|
||||
});
|
||||
} else {
|
||||
if (res.msg.includes("Unauthorized")) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
isAuthorized: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -251,7 +251,7 @@ class OrganizationListPage extends BaseListPage {
|
||||
<Result
|
||||
status="403"
|
||||
title="403 Unauthorized"
|
||||
subTitle={i18next.t("general:Sorry, you do not have permission to access this page.")}
|
||||
subTitle={i18next.t("general:Sorry, you do not have permission to access this page or logged in status invalid.")}
|
||||
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>}
|
||||
/>
|
||||
);
|
||||
|
@ -278,6 +278,13 @@ class PaymentListPage extends BaseListPage {
|
||||
searchText: params.searchText,
|
||||
searchedColumn: params.searchedColumn,
|
||||
});
|
||||
} else {
|
||||
if (res.msg.includes("Unauthorized")) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
isAuthorized: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -358,6 +358,13 @@ class PermissionListPage extends BaseListPage {
|
||||
searchText: params.searchText,
|
||||
searchedColumn: params.searchedColumn,
|
||||
});
|
||||
} else {
|
||||
if (res.msg.includes("Unauthorized")) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
isAuthorized: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -295,6 +295,13 @@ class ProductListPage extends BaseListPage {
|
||||
searchText: params.searchText,
|
||||
searchedColumn: params.searchedColumn,
|
||||
});
|
||||
} else {
|
||||
if (res.msg.includes("Unauthorized")) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
isAuthorized: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -14,7 +14,7 @@
|
||||
|
||||
import React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {Button, Popconfirm, Table} from "antd";
|
||||
import {Button, Popconfirm, Result, Table} from "antd";
|
||||
import moment from "moment";
|
||||
import * as Setting from "./Setting";
|
||||
import * as ProviderBackend from "./backend/ProviderBackend";
|
||||
@ -36,6 +36,7 @@ class ProviderListPage extends BaseListPage {
|
||||
loading: false,
|
||||
searchText: "",
|
||||
searchedColumn: "",
|
||||
isAuthorized: true,
|
||||
};
|
||||
}
|
||||
newProvider() {
|
||||
@ -227,6 +228,17 @@ class ProviderListPage extends BaseListPage {
|
||||
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
|
||||
};
|
||||
|
||||
if (!this.state.isAuthorized) {
|
||||
return (
|
||||
<Result
|
||||
status="403"
|
||||
title="403 Unauthorized"
|
||||
subTitle={i18next.t("general:Sorry, you do not have permission to access this page or logged in status invalid.")}
|
||||
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table scroll={{x: "max-content"}} columns={columns} dataSource={providers} rowKey="name" size="middle" bordered pagination={paginationProps}
|
||||
@ -268,6 +280,13 @@ class ProviderListPage extends BaseListPage {
|
||||
searchText: params.searchText,
|
||||
searchedColumn: params.searchedColumn,
|
||||
});
|
||||
} else {
|
||||
if (res.msg.includes("Unauthorized")) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
isAuthorized: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -222,6 +222,13 @@ class RecordListPage extends BaseListPage {
|
||||
searchText: params.searchText,
|
||||
searchedColumn: params.searchedColumn,
|
||||
});
|
||||
} else {
|
||||
if (res.data.includes("Please login first")) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
isAuthorized: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Popconfirm, Table, Upload} from "antd";
|
||||
import {Button, Popconfirm, Result, Table, Upload} from "antd";
|
||||
import {UploadOutlined} from "@ant-design/icons";
|
||||
import copy from "copy-to-clipboard";
|
||||
import * as Setting from "./Setting";
|
||||
@ -37,6 +37,7 @@ class ResourceListPage extends BaseListPage {
|
||||
searchedColumn: "",
|
||||
fileList: [],
|
||||
uploading: false,
|
||||
isAuthorized: true,
|
||||
};
|
||||
}
|
||||
|
||||
@ -272,6 +273,17 @@ class ResourceListPage extends BaseListPage {
|
||||
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
|
||||
};
|
||||
|
||||
if (!this.state.isAuthorized) {
|
||||
return (
|
||||
<Result
|
||||
status="403"
|
||||
title="403 Unauthorized"
|
||||
subTitle={i18next.t("general:Sorry, you do not have permission to access this page or logged in status invalid.")}
|
||||
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table scroll={{x: "max-content"}} columns={columns} dataSource={resources} rowKey="name" size="middle" bordered pagination={paginationProps}
|
||||
@ -308,6 +320,13 @@ class ResourceListPage extends BaseListPage {
|
||||
searchText: params.searchText,
|
||||
searchedColumn: params.searchedColumn,
|
||||
});
|
||||
} else {
|
||||
if (res.data.includes("Please login first")) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
isAuthorized: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -231,6 +231,13 @@ class RoleListPage extends BaseListPage {
|
||||
searchText: params.searchText,
|
||||
searchedColumn: params.searchedColumn,
|
||||
});
|
||||
} else {
|
||||
if (res.msg.includes("Unauthorized")) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
isAuthorized: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
162
web/src/SessionListPage.js
Normal file
162
web/src/SessionListPage.js
Normal file
@ -0,0 +1,162 @@
|
||||
// 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 BaseListPage from "./BaseListPage";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
import {Link} from "react-router-dom";
|
||||
import {Button, Popconfirm, Table, Tag} from "antd";
|
||||
import React from "react";
|
||||
import * as SessionBackend from "./backend/SessionBackend";
|
||||
|
||||
class SessionListPage extends BaseListPage {
|
||||
|
||||
deleteSession(i) {
|
||||
SessionBackend.deleteSession(this.state.data[i])
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
|
||||
this.setState({
|
||||
data: Setting.deleteRow(this.state.data, i),
|
||||
pagination: {total: this.state.pagination.total - 1},
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
renderTable(sessions) {
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("general:Name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: "150px",
|
||||
fixed: "left",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("name"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Organization"),
|
||||
dataIndex: "owner",
|
||||
key: "organization",
|
||||
width: "110px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("organization"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/organizations/${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:Session ID"),
|
||||
dataIndex: "sessionId",
|
||||
key: "sessionId",
|
||||
width: "180px",
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return text.map((item, index) =>
|
||||
<Tag key={index}>{item}</Tag>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
dataIndex: "",
|
||||
key: "op",
|
||||
width: "70px",
|
||||
fixed: (Setting.isMobile()) ? "false" : "right",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
<Popconfirm
|
||||
title={`Sure to delete session: ${record.name} ?`}
|
||||
onConfirm={() => this.deleteSession(index)}
|
||||
>
|
||||
<Button style={{marginBottom: "10px"}} type="primary" 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={sessions} rowKey="name" size="middle" bordered pagination={paginationProps}
|
||||
loading={this.state.loading}
|
||||
onChange={this.handleTableChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
fetch = (params = {}) => {
|
||||
let field = params.searchedColumn, value = params.searchText;
|
||||
const sortField = params.sortField, sortOrder = params.sortOrder;
|
||||
if (params.contentType !== undefined && params.contentType !== null) {
|
||||
field = "contentType";
|
||||
value = params.contentType;
|
||||
}
|
||||
this.setState({loading: true});
|
||||
SessionBackend.getSessions("", 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,
|
||||
});
|
||||
} else {
|
||||
if (res.msg.includes("Unauthorized")) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
isAuthorized: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default SessionListPage;
|
@ -45,9 +45,9 @@ export const Countries = [{label: "English", key: "en", country: "US", alt: "Eng
|
||||
|
||||
const {defaultAlgorithm, darkAlgorithm, compactAlgorithm} = theme;
|
||||
|
||||
export const Themes = [{label: "Dark", key: "Dark", style: darkAlgorithm, selectThemeLogo: `${StaticBaseUrl}/img/dark.svg`},
|
||||
{label: "Compact", key: "Compact", style: compactAlgorithm, selectThemeLogo: `${StaticBaseUrl}/img/compact.svg`},
|
||||
{label: "Default", key: "Default", style: defaultAlgorithm, selectThemeLogo: `${StaticBaseUrl}/img/light.svg`},
|
||||
export const Themes = [{label: i18next.t("general:Dark"), key: "Dark", style: darkAlgorithm, selectThemeLogo: `${StaticBaseUrl}/img/dark.svg`},
|
||||
{label: i18next.t("general:Compact"), key: "Compact", style: compactAlgorithm, selectThemeLogo: `${StaticBaseUrl}/img/compact.svg`},
|
||||
{label: i18next.t("general:Default"), key: "Default", style: defaultAlgorithm, selectThemeLogo: `${StaticBaseUrl}/img/light.svg`},
|
||||
];
|
||||
|
||||
export const OtherProviderInfo = {
|
||||
|
@ -288,6 +288,13 @@ class SyncerListPage extends BaseListPage {
|
||||
searchText: params.searchText,
|
||||
searchedColumn: params.searchedColumn,
|
||||
});
|
||||
} else {
|
||||
if (res.msg.includes("Unauthorized")) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
isAuthorized: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -253,6 +253,13 @@ class TokenListPage extends BaseListPage {
|
||||
searchText: params.searchText,
|
||||
searchedColumn: params.searchedColumn,
|
||||
});
|
||||
} else {
|
||||
if (res.msg.includes("Unauthorized")) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
isAuthorized: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -14,7 +14,7 @@
|
||||
|
||||
import React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {Button, Popconfirm, Switch, Table, Upload} from "antd";
|
||||
import {Button, Popconfirm, Result, Switch, Table, Upload} from "antd";
|
||||
import {UploadOutlined} from "@ant-design/icons";
|
||||
import moment from "moment";
|
||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||
@ -38,6 +38,7 @@ class UserListPage extends BaseListPage {
|
||||
loading: false,
|
||||
searchText: "",
|
||||
searchedColumn: "",
|
||||
isAuthorized: true,
|
||||
};
|
||||
}
|
||||
|
||||
@ -369,6 +370,17 @@ class UserListPage extends BaseListPage {
|
||||
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
|
||||
};
|
||||
|
||||
if (!this.state.isAuthorized) {
|
||||
return (
|
||||
<Result
|
||||
status="403"
|
||||
title="403 Unauthorized"
|
||||
subTitle={i18next.t("general:Sorry, you do not have permission to access this page or logged in status invalid.")}
|
||||
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table scroll={{x: "max-content"}} columns={columns} dataSource={users} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
|
||||
@ -411,6 +423,13 @@ class UserListPage extends BaseListPage {
|
||||
if (users.length > 0) {
|
||||
this.getOrganization(users[0].owner);
|
||||
}
|
||||
} else {
|
||||
if (res.msg.includes("Unauthorized")) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
isAuthorized: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@ -432,6 +451,13 @@ class UserListPage extends BaseListPage {
|
||||
if (users.length > 0) {
|
||||
this.getOrganization(users[0].owner);
|
||||
}
|
||||
} else {
|
||||
if (res.msg.includes("Unauthorized")) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
isAuthorized: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -253,6 +253,13 @@ class WebhookListPage extends BaseListPage {
|
||||
searchText: params.searchText,
|
||||
searchedColumn: params.searchedColumn,
|
||||
});
|
||||
} else {
|
||||
if (res.msg.includes("Unauthorized")) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
isAuthorized: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
36
web/src/backend/SessionBackend.js
Normal file
36
web/src/backend/SessionBackend.js
Normal file
@ -0,0 +1,36 @@
|
||||
// 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 getSessions(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-sessions?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function deleteSession(session) {
|
||||
return fetch(`${Setting.ServerUrl}/api/delete-session`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(session),
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
@ -236,6 +236,8 @@
|
||||
"Roles - Tooltip": "Roles - Tooltip",
|
||||
"Save": "Speichern",
|
||||
"Save & Exit": "Speichern & Beenden",
|
||||
"Session ID": "Session ID",
|
||||
"Sessions": "Sessions",
|
||||
"Signin URL": "Anmelde-URL",
|
||||
"Signin URL - Tooltip": "sign in url",
|
||||
"Signup URL": "Registrierungs-URL",
|
||||
@ -244,7 +246,7 @@
|
||||
"Signup application - Tooltip": "Signup application - Tooltip",
|
||||
"Sorry, the page you visited does not exist.": "Die von Ihnen besuchte Seite existiert leider nicht.",
|
||||
"Sorry, the user you visited does not exist or you are not authorized to access this user.": "Sorry, the user you visited does not exist or you are not authorized to access this user.",
|
||||
"Sorry, you do not have permission to access this page.": "Sorry, you do not have permission to access this page.",
|
||||
"Sorry, you do not have permission to access this page or logged in status invalid.": "Sorry, you do not have permission to access this page or logged in status invalid.",
|
||||
"State": "State",
|
||||
"State - Tooltip": "State - Tooltip",
|
||||
"Successfully added": "Successfully added",
|
||||
|
@ -236,6 +236,8 @@
|
||||
"Roles - Tooltip": "Roles - Tooltip",
|
||||
"Save": "Save",
|
||||
"Save & Exit": "Save & Exit",
|
||||
"Session ID": "Session ID",
|
||||
"Sessions": "Sessions",
|
||||
"Signin URL": "Signin URL",
|
||||
"Signin URL - Tooltip": "Signin URL - Tooltip",
|
||||
"Signup URL": "Signup URL",
|
||||
@ -244,7 +246,7 @@
|
||||
"Signup application - Tooltip": "Signup application - Tooltip",
|
||||
"Sorry, the page you visited does not exist.": "Sorry, the page you visited does not exist.",
|
||||
"Sorry, the user you visited does not exist or you are not authorized to access this user.": "Sorry, the user you visited does not exist or you are not authorized to access this user.",
|
||||
"Sorry, you do not have permission to access this page.": "Sorry, you do not have permission to access this page.",
|
||||
"Sorry, you do not have permission to access this page or logged in status invalid.": "Sorry, you do not have permission to access this page or logged in status invalid.",
|
||||
"State": "State",
|
||||
"State - Tooltip": "State - Tooltip",
|
||||
"Successfully added": "Successfully added",
|
||||
|
@ -236,6 +236,8 @@
|
||||
"Roles - Tooltip": "Roles - Tooltip",
|
||||
"Save": "Enregistrer",
|
||||
"Save & Exit": "Enregistrer & Quitter",
|
||||
"Session ID": "Session ID",
|
||||
"Sessions": "Sessions",
|
||||
"Signin URL": "URL de connexion",
|
||||
"Signin URL - Tooltip": "sign in url",
|
||||
"Signup URL": "URL d'inscription",
|
||||
@ -244,7 +246,7 @@
|
||||
"Signup application - Tooltip": "Signup application - Tooltip",
|
||||
"Sorry, the page you visited does not exist.": "Désolé, la page que vous avez visitée n'existe pas.",
|
||||
"Sorry, the user you visited does not exist or you are not authorized to access this user.": "Sorry, the user you visited does not exist or you are not authorized to access this user.",
|
||||
"Sorry, you do not have permission to access this page.": "Désolé, vous n'avez pas la permission d'accéder à cette page.",
|
||||
"Sorry, you do not have permission to access this page or logged in status invalid.": "Sorry, you do not have permission to access this page or logged in status invalid.",
|
||||
"State": "State",
|
||||
"State - Tooltip": "State - Tooltip",
|
||||
"Successfully added": "Successfully added",
|
||||
|
@ -236,6 +236,8 @@
|
||||
"Roles - Tooltip": "Roles - Tooltip",
|
||||
"Save": "保存",
|
||||
"Save & Exit": "保存して終了",
|
||||
"Session ID": "Session ID",
|
||||
"Sessions": "Sessions",
|
||||
"Signin URL": "サインインURL",
|
||||
"Signin URL - Tooltip": "sign in url",
|
||||
"Signup URL": "サインアップURL",
|
||||
@ -244,7 +246,7 @@
|
||||
"Signup application - Tooltip": "Signup application - Tooltip",
|
||||
"Sorry, the page you visited does not exist.": "申し訳ありませんが、訪問したページは存在しません。",
|
||||
"Sorry, the user you visited does not exist or you are not authorized to access this user.": "Sorry, the user you visited does not exist or you are not authorized to access this user.",
|
||||
"Sorry, you do not have permission to access this page.": "申し訳ありませんが、このページにアクセスする権限がありません。",
|
||||
"Sorry, you do not have permission to access this page or logged in status invalid.": "Sorry, you do not have permission to access this page or logged in status invalid.",
|
||||
"State": "State",
|
||||
"State - Tooltip": "State - Tooltip",
|
||||
"Successfully added": "Successfully added",
|
||||
|
@ -236,6 +236,8 @@
|
||||
"Roles - Tooltip": "Roles - Tooltip",
|
||||
"Save": "Save",
|
||||
"Save & Exit": "Save & Exit",
|
||||
"Session ID": "Session ID",
|
||||
"Sessions": "Sessions",
|
||||
"Signin URL": "Signin URL",
|
||||
"Signin URL - Tooltip": "sign in url",
|
||||
"Signup URL": "Signup URL",
|
||||
@ -244,7 +246,7 @@
|
||||
"Signup application - Tooltip": "Signup application - Tooltip",
|
||||
"Sorry, the page you visited does not exist.": "Sorry, the page you visited does not exist.",
|
||||
"Sorry, the user you visited does not exist or you are not authorized to access this user.": "Sorry, the user you visited does not exist or you are not authorized to access this user.",
|
||||
"Sorry, you do not have permission to access this page.": "Sorry, you do not have permission to access this page.",
|
||||
"Sorry, you do not have permission to access this page or logged in status invalid.": "Sorry, you do not have permission to access this page or logged in status invalid.",
|
||||
"State": "State",
|
||||
"State - Tooltip": "State - Tooltip",
|
||||
"Successfully added": "Successfully added",
|
||||
|
@ -236,6 +236,8 @@
|
||||
"Roles - Tooltip": "Roles - Tooltip",
|
||||
"Save": "Сохранить",
|
||||
"Save & Exit": "Сохранить и выйти",
|
||||
"Session ID": "Session ID",
|
||||
"Sessions": "Sessions",
|
||||
"Signin URL": "URL входа",
|
||||
"Signin URL - Tooltip": "sign in url",
|
||||
"Signup URL": "URL регистрации",
|
||||
@ -244,7 +246,7 @@
|
||||
"Signup application - Tooltip": "Signup application - Tooltip",
|
||||
"Sorry, the page you visited does not exist.": "Извините, посещенная вами страница не существует.",
|
||||
"Sorry, the user you visited does not exist or you are not authorized to access this user.": "Sorry, the user you visited does not exist or you are not authorized to access this user.",
|
||||
"Sorry, you do not have permission to access this page.": "Извините, вы не имеете права доступа к этой странице.",
|
||||
"Sorry, you do not have permission to access this page or logged in status invalid.": "Sorry, you do not have permission to access this page or logged in status invalid.",
|
||||
"State": "State",
|
||||
"State - Tooltip": "State - Tooltip",
|
||||
"Successfully added": "Successfully added",
|
||||
|
@ -236,6 +236,8 @@
|
||||
"Roles - Tooltip": "角色",
|
||||
"Save": "保存",
|
||||
"Save & Exit": "保存 & 退出",
|
||||
"Session ID": "会话 ID",
|
||||
"Sessions": "会话",
|
||||
"Signin URL": "登录URL",
|
||||
"Signin URL - Tooltip": "用户的登录地址",
|
||||
"Signup URL": "注册URL",
|
||||
@ -244,7 +246,7 @@
|
||||
"Signup application - Tooltip": "表示用户注册时通过哪个应用注册的",
|
||||
"Sorry, the page you visited does not exist.": "抱歉,您访问的页面不存在",
|
||||
"Sorry, the user you visited does not exist or you are not authorized to access this user.": "抱歉,您访问的用户不存在或您无权访问该用户",
|
||||
"Sorry, you do not have permission to access this page.": "抱歉,您无权访问该页面",
|
||||
"Sorry, you do not have permission to access this page or logged in status invalid.": "抱歉,您无权访问该页面或登录状态失效",
|
||||
"State": "状态",
|
||||
"State - Tooltip": "状态",
|
||||
"Successfully added": "添加成功",
|
||||
|
23344
web/yarn.lock
23344
web/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user