mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-08 09:01:00 +08:00
Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
6a1ec51978 | |||
dffa68cbce | |||
fad209a7a3 | |||
8b222ce2e3 | |||
c5293f428d | |||
146aec9ee8 | |||
50a52de856 | |||
8f7a8d7d4f | |||
23f3fe1e3c | |||
59ff5e02ab | |||
8d41508d6b | |||
04f70cf012 | |||
83724c73f9 | |||
33e419e133 | |||
b832c304ae | |||
4c7f6fda37 | |||
e4a54fe375 | |||
87da3dad76 | |||
44ad88353f | |||
a955fb57d6 | |||
d2960ad66b | |||
5243aabf43 | |||
d3a2c2a66e | |||
0a9058a585 | |||
225719810b |
3
.gitignore
vendored
3
.gitignore
vendored
@ -27,3 +27,6 @@ logs/
|
||||
files/
|
||||
lastupdate.tmp
|
||||
commentsRouter*.go
|
||||
|
||||
# ignore build result
|
||||
casdoor
|
@ -51,7 +51,7 @@
|
||||
## Documentation
|
||||
|
||||
- International: https://casdoor.org
|
||||
- Asian mirror: https://docs.casdoor.cn
|
||||
- Asian mirror: https://casdoor.cn
|
||||
|
||||
## Install
|
||||
|
||||
@ -69,7 +69,7 @@ https://casdoor.org/docs/how-to-connect/overview
|
||||
|
||||
## Integrations
|
||||
|
||||
https://casdoor.org/docs/integration/apisix
|
||||
https://casdoor.org/docs/category/integrations
|
||||
|
||||
## How to contact?
|
||||
|
||||
|
9
SECURITY.md
Normal file
9
SECURITY.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
We are grateful for security researchers and users reporting a vulnerability to us first. To ensure that your request is handled in a timely manner and we can keep users safe, please follow the below guidelines.
|
||||
|
||||
- **Please do not report security vulnerabilities directly on GitHub.**
|
||||
|
||||
- To report a vulnerability, please email [admin@casdoor.org](admin@casdoor.org).
|
@ -15,12 +15,14 @@
|
||||
package authz
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/casbin/casbin/v2"
|
||||
"github.com/casbin/casbin/v2/model"
|
||||
xormadapter "github.com/casbin/xorm-adapter/v3"
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
stringadapter "github.com/qiangmzsx/string-adapter/v2"
|
||||
)
|
||||
|
||||
@ -138,6 +140,12 @@ func IsAllowed(subOwner string, subName string, method string, urlPath string, o
|
||||
}
|
||||
}
|
||||
|
||||
userId := fmt.Sprintf("%s/%s", subOwner, subName)
|
||||
user := object.GetUser(userId)
|
||||
if user != nil && user.IsAdmin && subOwner == objOwner {
|
||||
return true
|
||||
}
|
||||
|
||||
res, err := Enforcer.Enforce(subOwner, subName, method, urlPath, objOwner, objName)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
@ -133,11 +133,12 @@ func (c *ApiController) GetDefaultApplication() {
|
||||
userId := c.GetSessionUsername()
|
||||
id := c.Input().Get("id")
|
||||
|
||||
application := object.GetMaskedApplication(object.GetDefaultApplication(id), userId)
|
||||
if application == nil {
|
||||
c.ResponseError("Please set a default application for this organization")
|
||||
application, err := object.GetDefaultApplication(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(application)
|
||||
maskedApplication := object.GetMaskedApplication(application, userId)
|
||||
c.ResponseOk(maskedApplication)
|
||||
}
|
||||
|
@ -183,6 +183,12 @@ func (c *ApiController) AddUser() {
|
||||
return
|
||||
}
|
||||
|
||||
msg := object.CheckUsername(user.Name)
|
||||
if msg != "" {
|
||||
c.ResponseError(msg)
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddUser(&user))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
@ -282,7 +282,7 @@ func getUser(gothUser goth.User, provider string) *UserInfo {
|
||||
}
|
||||
}
|
||||
if provider == "steam" {
|
||||
user.Username = user.DisplayName
|
||||
user.Username = user.Id
|
||||
user.Email = ""
|
||||
}
|
||||
return &user
|
||||
|
@ -19,11 +19,13 @@ import (
|
||||
"runtime"
|
||||
|
||||
"github.com/beego/beego"
|
||||
xormadapter "github.com/casbin/xorm-adapter/v3"
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
_ "github.com/denisenkom/go-mssqldb" // db = mssql
|
||||
_ "github.com/go-sql-driver/mysql" // db = mysql
|
||||
_ "github.com/lib/pq" // db = postgres
|
||||
"xorm.io/xorm/migrate"
|
||||
//_ "github.com/mattn/go-sqlite3" // db = sqlite3
|
||||
"xorm.io/core"
|
||||
"xorm.io/xorm"
|
||||
@ -40,6 +42,7 @@ func InitConfig() {
|
||||
beego.BConfig.WebConfig.Session.SessionOn = true
|
||||
|
||||
InitAdapter(true)
|
||||
initMigrations()
|
||||
}
|
||||
|
||||
func InitAdapter(createDatabase bool) {
|
||||
@ -214,6 +217,11 @@ func (a *Adapter) createTable() {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = a.Engine.Sync2(new(xormadapter.CasbinRule))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func GetSession(owner string, offset, limit int, field, value, sortField, sortOrder string) *xorm.Session {
|
||||
@ -239,3 +247,22 @@ func GetSession(owner string, offset, limit int, field, value, sortField, sortOr
|
||||
}
|
||||
return session
|
||||
}
|
||||
|
||||
func initMigrations() {
|
||||
migrations := []*migrate.Migration{
|
||||
{
|
||||
ID: "20221015CasbinRule--fill ptype field with p",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
_, err := tx.Cols("ptype").Update(&xormadapter.CasbinRule{
|
||||
Ptype: "p",
|
||||
})
|
||||
return err
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return tx.DropTables(&xormadapter.CasbinRule{})
|
||||
},
|
||||
},
|
||||
}
|
||||
m := migrate.New(adapter.Engine, migrate.DefaultOptions, migrations)
|
||||
m.Migrate()
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ func downloadFile(url string) (*bytes.Buffer, error) {
|
||||
return fileBuffer, nil
|
||||
}
|
||||
|
||||
func getPermanentAvatarUrl(organization string, username string, url string) string {
|
||||
func getPermanentAvatarUrl(organization string, username string, url string, upload bool) string {
|
||||
if url == "" {
|
||||
return ""
|
||||
}
|
||||
@ -62,6 +62,14 @@ func getPermanentAvatarUrl(organization string, username string, url string) str
|
||||
fullFilePath := fmt.Sprintf("/avatar/%s/%s.png", organization, username)
|
||||
uploadedFileUrl, _ := getUploadFileUrl(defaultStorageProvider, fullFilePath, false)
|
||||
|
||||
if upload {
|
||||
DownloadAndUpload(url, fullFilePath)
|
||||
}
|
||||
|
||||
return uploadedFileUrl
|
||||
}
|
||||
|
||||
func DownloadAndUpload(url string, fullFilePath string) {
|
||||
fileBuffer, err := downloadFile(url)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -71,6 +79,4 @@ func getPermanentAvatarUrl(organization string, username string, url string) str
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return uploadedFileUrl
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ func TestSyncPermanentAvatars(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
|
||||
user.PermanentAvatar = getPermanentAvatarUrl(user.Owner, user.Name, user.Avatar)
|
||||
user.PermanentAvatar = getPermanentAvatarUrl(user.Owner, user.Name, user.Avatar, true)
|
||||
updateUserColumn("permanent_avatar", user)
|
||||
fmt.Printf("[%d/%d]: Update user: [%s]'s permanent avatar: %s\n", i, len(users), user.GetId(), user.PermanentAvatar)
|
||||
}
|
||||
|
@ -59,6 +59,11 @@ func CheckUserSignup(application *Application, organization *Organization, usern
|
||||
if reWhiteSpace.MatchString(username) {
|
||||
return "username cannot contain white spaces"
|
||||
}
|
||||
msg := CheckUsername(username)
|
||||
if msg != "" {
|
||||
return msg
|
||||
}
|
||||
|
||||
if HasUserByField(organization.Name, "name", username) {
|
||||
return "username already exists"
|
||||
}
|
||||
@ -313,3 +318,19 @@ func CheckAccessPermission(userId string, application *Application) (bool, error
|
||||
}
|
||||
return allowed, err
|
||||
}
|
||||
|
||||
func CheckUsername(username string) string {
|
||||
if username == "" {
|
||||
return "Empty username."
|
||||
} else if len(username) > 39 {
|
||||
return "Username is too long (maximum is 39 characters)."
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/58726546/github-username-convention-using-regex
|
||||
re, _ := regexp.Compile("^[a-zA-Z0-9]+((?:-[a-zA-Z0-9]+)|(?:_[a-zA-Z0-9]+))*$")
|
||||
if !re.MatchString(username) {
|
||||
return "The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline."
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
@ -409,6 +409,7 @@ func SyncLdapUsers(owner string, users []LdapRespUser, ldapId string) (*[]LdapRe
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found && !AddUser(&User{
|
||||
Owner: owner,
|
||||
Name: buildLdapUserName(user.Uid, user.UidNumber),
|
||||
|
@ -218,14 +218,14 @@ func CheckAccountItemModifyRule(accountItem *AccountItem, user *User) (bool, str
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func GetDefaultApplication(id string) *Application {
|
||||
func GetDefaultApplication(id string) (*Application, error) {
|
||||
organization := GetOrganization(id)
|
||||
if organization == nil {
|
||||
return nil
|
||||
return nil, fmt.Errorf("The organization: %s does not exist", id)
|
||||
}
|
||||
|
||||
if organization.DefaultApplication != "" {
|
||||
return getApplication("admin", organization.DefaultApplication)
|
||||
return getApplication("admin", organization.DefaultApplication), fmt.Errorf("The default application: %s does not exist", organization.DefaultApplication)
|
||||
}
|
||||
|
||||
applications := []*Application{}
|
||||
@ -235,7 +235,7 @@ func GetDefaultApplication(id string) *Application {
|
||||
}
|
||||
|
||||
if len(applications) == 0 {
|
||||
return nil
|
||||
return nil, fmt.Errorf("The application does not exist")
|
||||
}
|
||||
|
||||
defaultApplication := applications[0]
|
||||
@ -249,5 +249,5 @@ func GetDefaultApplication(id string) *Application {
|
||||
extendApplicationWithProviders(defaultApplication)
|
||||
extendApplicationWithOrg(defaultApplication)
|
||||
|
||||
return defaultApplication
|
||||
return defaultApplication, nil
|
||||
}
|
||||
|
@ -120,7 +120,7 @@ func (syncer *Syncer) updateUserForOriginalFields(user *User) (bool, error) {
|
||||
}
|
||||
|
||||
if user.Avatar != oldUser.Avatar && user.Avatar != "" {
|
||||
user.PermanentAvatar = getPermanentAvatarUrl(user.Owner, user.Name, user.Avatar)
|
||||
user.PermanentAvatar = getPermanentAvatarUrl(user.Owner, user.Name, user.Avatar, true)
|
||||
}
|
||||
|
||||
columns := syncer.getCasdoorColumns()
|
||||
|
@ -703,7 +703,7 @@ func GetWechatMiniProgramToken(application *Application, code string, host strin
|
||||
}
|
||||
// Add new user
|
||||
var name string
|
||||
if username != "" {
|
||||
if CheckUsername(username) == "" {
|
||||
name = username
|
||||
} else {
|
||||
name = fmt.Sprintf("wechat-%s", openId)
|
||||
|
@ -386,7 +386,7 @@ func UpdateUser(id string, user *User, columns []string, isGlobalAdmin bool) boo
|
||||
user.UpdateUserHash()
|
||||
|
||||
if user.Avatar != oldUser.Avatar && user.Avatar != "" && user.PermanentAvatar != "*" {
|
||||
user.PermanentAvatar = getPermanentAvatarUrl(user.Owner, user.Name, user.Avatar)
|
||||
user.PermanentAvatar = getPermanentAvatarUrl(user.Owner, user.Name, user.Avatar, false)
|
||||
}
|
||||
|
||||
if len(columns) == 0 {
|
||||
@ -419,7 +419,7 @@ func UpdateUserForAllFields(id string, user *User) bool {
|
||||
user.UpdateUserHash()
|
||||
|
||||
if user.Avatar != oldUser.Avatar && user.Avatar != "" {
|
||||
user.PermanentAvatar = getPermanentAvatarUrl(user.Owner, user.Name, user.Avatar)
|
||||
user.PermanentAvatar = getPermanentAvatarUrl(user.Owner, user.Name, user.Avatar, false)
|
||||
}
|
||||
|
||||
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(user)
|
||||
@ -449,7 +449,7 @@ func AddUser(user *User) bool {
|
||||
user.UpdateUserHash()
|
||||
user.PreHash = user.Hash
|
||||
|
||||
user.PermanentAvatar = getPermanentAvatarUrl(user.Owner, user.Name, user.Avatar)
|
||||
user.PermanentAvatar = getPermanentAvatarUrl(user.Owner, user.Name, user.Avatar, false)
|
||||
|
||||
user.Ranking = GetUserCount(user.Owner, "", "") + 1
|
||||
|
||||
@ -474,7 +474,7 @@ func AddUsers(users []*User) bool {
|
||||
user.UpdateUserHash()
|
||||
user.PreHash = user.Hash
|
||||
|
||||
user.PermanentAvatar = getPermanentAvatarUrl(user.Owner, user.Name, user.Avatar)
|
||||
user.PermanentAvatar = getPermanentAvatarUrl(user.Owner, user.Name, user.Avatar, true)
|
||||
}
|
||||
|
||||
affected, err := adapter.Engine.Insert(users)
|
||||
|
@ -159,7 +159,7 @@ func DisableVerificationCode(dest string) {
|
||||
}
|
||||
}
|
||||
|
||||
// from Casnode/object/validateCode.go line 116
|
||||
// From Casnode/object/validateCode.go line 116
|
||||
var stdNums = []byte("0123456789")
|
||||
|
||||
func getRandomCode(length int) string {
|
||||
|
@ -63,11 +63,16 @@ func getObject(ctx *context.Context) (string, string) {
|
||||
if method == http.MethodGet {
|
||||
// query == "?id=built-in/admin"
|
||||
id := ctx.Input.Query("id")
|
||||
if id == "" {
|
||||
return "", ""
|
||||
if id != "" {
|
||||
return util.GetOwnerAndNameFromId(id)
|
||||
}
|
||||
|
||||
return util.GetOwnerAndNameFromId(id)
|
||||
owner := ctx.Input.Query("owner")
|
||||
if owner != "" {
|
||||
return owner, ""
|
||||
}
|
||||
|
||||
return "", ""
|
||||
} else {
|
||||
body := ctx.Input.RequestBody
|
||||
|
||||
|
@ -16,8 +16,8 @@ import React, {Component} from "react";
|
||||
import "./App.less";
|
||||
import {Helmet} from "react-helmet";
|
||||
import * as Setting from "./Setting";
|
||||
import {DownOutlined, LogoutOutlined, SettingOutlined} from "@ant-design/icons";
|
||||
import {Avatar, BackTop, Button, Card, Dropdown, Layout, Menu, Result} from "antd";
|
||||
import {BarsOutlined, DownOutlined, LogoutOutlined, SettingOutlined} from "@ant-design/icons";
|
||||
import {Avatar, BackTop, Button, Card, Drawer, Dropdown, Layout, Menu, Result} from "antd";
|
||||
import {Link, Redirect, Route, Switch, withRouter} from "react-router-dom";
|
||||
import OrganizationListPage from "./OrganizationListPage";
|
||||
import OrganizationEditPage from "./OrganizationEditPage";
|
||||
@ -74,6 +74,7 @@ import ModelEditPage from "./ModelEditPage";
|
||||
import SystemInfo from "./SystemInfo";
|
||||
import AdapterListPage from "./AdapterListPage";
|
||||
import AdapterEditPage from "./AdapterEditPage";
|
||||
import {withTranslation} from "react-i18next";
|
||||
|
||||
const {Header, Footer} = Layout;
|
||||
|
||||
@ -85,6 +86,7 @@ class App extends Component {
|
||||
selectedMenuKey: 0,
|
||||
account: undefined,
|
||||
uri: null,
|
||||
menuVisible: false,
|
||||
};
|
||||
|
||||
Setting.initServerUrl();
|
||||
@ -298,12 +300,12 @@ class App extends Component {
|
||||
<Menu onClick={this.handleRightDropdownClick.bind(this)}>
|
||||
<Menu.Item key="/account">
|
||||
<SettingOutlined />
|
||||
|
||||
|
||||
{i18next.t("account:My Account")}
|
||||
</Menu.Item>
|
||||
<Menu.Item key="/logout">
|
||||
<LogoutOutlined />
|
||||
|
||||
|
||||
{i18next.t("account:Logout")}
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
@ -378,6 +380,9 @@ class App extends Component {
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
);
|
||||
}
|
||||
|
||||
if (Setting.isLocalAdminUser(this.state.account)) {
|
||||
res.push(
|
||||
<Menu.Item key="/users">
|
||||
<Link to="/users">
|
||||
@ -592,6 +597,18 @@ class App extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
onClose = () => {
|
||||
this.setState({
|
||||
menuVisible: false,
|
||||
});
|
||||
};
|
||||
|
||||
showMenu = () => {
|
||||
this.setState({
|
||||
menuVisible: true,
|
||||
});
|
||||
};
|
||||
|
||||
renderContent() {
|
||||
if (!Setting.isMobile()) {
|
||||
return (
|
||||
@ -610,7 +627,7 @@ class App extends Component {
|
||||
// theme="dark"
|
||||
mode={(Setting.isMobile() && this.isStartPages()) ? "inline" : "horizontal"}
|
||||
selectedKeys={[`${this.state.selectedMenuKey}`]}
|
||||
style={{lineHeight: "64px", width: "78%", position: "absolute", left: "145px"}}
|
||||
style={{lineHeight: "64px", position: "absolute", left: "145px", right: "200px"}}
|
||||
>
|
||||
{
|
||||
this.renderMenu()
|
||||
@ -643,22 +660,28 @@ class App extends Component {
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
<Menu
|
||||
// theme="dark"
|
||||
mode={(Setting.isMobile() && this.isStartPages()) ? "inline" : "horizontal"}
|
||||
selectedKeys={[`${this.state.selectedMenuKey}`]}
|
||||
style={{lineHeight: "64px"}}
|
||||
>
|
||||
{
|
||||
this.renderMenu()
|
||||
}
|
||||
<div style = {{float: "right"}}>
|
||||
<Drawer title={i18next.t("general:Close")} placement="left" visible={this.state.menuVisible} onClose={this.onClose}>
|
||||
<Menu
|
||||
// theme="dark"
|
||||
mode={(Setting.isMobile()) ? "inline" : "horizontal"}
|
||||
selectedKeys={[`${this.state.selectedMenuKey}`]}
|
||||
style={{lineHeight: "64px"}}
|
||||
onClick={this.onClose}
|
||||
>
|
||||
{
|
||||
this.renderAccount()
|
||||
this.renderMenu()
|
||||
}
|
||||
<SelectLanguageBox />
|
||||
</div>
|
||||
</Menu>
|
||||
</Menu>
|
||||
</Drawer>
|
||||
<Button icon={<BarsOutlined />} onClick={this.showMenu} type="text">
|
||||
{i18next.t("general:Menu")}
|
||||
</Button>
|
||||
<div style = {{float: "right"}}>
|
||||
{
|
||||
this.renderAccount()
|
||||
}
|
||||
<SelectLanguageBox />
|
||||
</div>
|
||||
</Header>
|
||||
{
|
||||
this.renderRouter()
|
||||
@ -776,4 +799,4 @@ class App extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(App);
|
||||
export default withRouter(withTranslation()(App));
|
||||
|
@ -12,19 +12,21 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React, {useState} from "react";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import Cropper from "react-cropper";
|
||||
import "cropperjs/dist/cropper.css";
|
||||
import * as Setting from "./Setting";
|
||||
import {Button, Col, Modal, Row} from "antd";
|
||||
import {Button, Col, Modal, Row, Select} from "antd";
|
||||
import i18next from "i18next";
|
||||
import * as ResourceBackend from "./backend/ResourceBackend";
|
||||
|
||||
export const CropperDiv = (props) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [options, setOptions] = useState([]);
|
||||
const [image, setImage] = useState("");
|
||||
const [cropper, setCropper] = useState();
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
const [confirmLoading, setConfirmLoading] = React.useState(false);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||
const {title} = props;
|
||||
const {user} = props;
|
||||
const {buttonText} = props;
|
||||
@ -60,7 +62,7 @@ export const CropperDiv = (props) => {
|
||||
ResourceBackend.uploadResource(user.owner, user.name, "avatar", "CropperDiv", fullFilePath, blob)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
window.location.href = "/account";
|
||||
window.location.href = window.location.pathname;
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
@ -88,6 +90,48 @@ export const CropperDiv = (props) => {
|
||||
uploadButton.click();
|
||||
};
|
||||
|
||||
const getOptions = (data) => {
|
||||
const options = [];
|
||||
if (props.account.organization.defaultAvatar !== null) {
|
||||
options.push({value: props.account.organization.defaultAvatar});
|
||||
}
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if (data[i].fileType === "image") {
|
||||
const url = `${data[i].url}`;
|
||||
options.push({
|
||||
value: url,
|
||||
});
|
||||
}
|
||||
}
|
||||
return options;
|
||||
};
|
||||
|
||||
const getBase64Image = (src) => {
|
||||
return new Promise((resolve) => {
|
||||
const image = new Image();
|
||||
image.src = src;
|
||||
image.setAttribute("crossOrigin", "anonymous");
|
||||
image.onload = () => {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(image, 0, 0, image.width, image.height);
|
||||
const dataURL = canvas.toDataURL("image/png");
|
||||
resolve(dataURL);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
ResourceBackend.getResources(props.account.owner, props.account.name, "", "", "", "", "", "")
|
||||
.then((res) => {
|
||||
setLoading(false);
|
||||
setOptions(getOptions(res));
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button type="default" onClick={showModal}>
|
||||
@ -105,10 +149,20 @@ export const CropperDiv = (props) => {
|
||||
[<Button block key="submit" type="primary" onClick={handleOk}>{i18next.t("user:Set new profile picture")}</Button>]
|
||||
}
|
||||
>
|
||||
<Col style={{margin: "0px auto 40px auto", width: 1000, height: 300}}>
|
||||
<Col style={{margin: "0px auto 60px auto", width: 1000, height: 350}}>
|
||||
<Row style={{width: "100%", marginBottom: "20px"}}>
|
||||
<input style={{display: "none"}} ref={input => uploadButton = input} type="file" accept="image/*" onChange={onChange} />
|
||||
<Button block onClick={selectFile}>{i18next.t("user:Select a photo...")}</Button>
|
||||
<Select
|
||||
style={{width: "100%"}}
|
||||
loading={loading}
|
||||
placeholder={i18next.t("user:Please select avatar from resources")}
|
||||
onChange={(async value => {
|
||||
setImage(await getBase64Image(value));
|
||||
})}
|
||||
options={options}
|
||||
allowClear={true}
|
||||
/>
|
||||
</Row>
|
||||
<Cropper
|
||||
style={{height: "100%"}}
|
||||
|
@ -35,6 +35,7 @@ class PermissionEditPage extends React.Component {
|
||||
permissionName: props.match.params.permissionName,
|
||||
permission: null,
|
||||
organizations: [],
|
||||
model: null,
|
||||
users: [],
|
||||
roles: [],
|
||||
models: [],
|
||||
@ -59,6 +60,7 @@ class PermissionEditPage extends React.Component {
|
||||
this.getRoles(permission.owner);
|
||||
this.getModels(permission.owner);
|
||||
this.getResources(permission.owner);
|
||||
this.getModel(permission.owner, permission.model);
|
||||
});
|
||||
}
|
||||
|
||||
@ -98,6 +100,15 @@ class PermissionEditPage extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
getModel(organizationName, modelName) {
|
||||
ModelBackend.getModel(organizationName, modelName)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
model: res,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getResources(organizationName) {
|
||||
ApplicationBackend.getApplicationsByOrganization("admin", organizationName)
|
||||
.then((res) => {
|
||||
@ -115,6 +126,10 @@ class PermissionEditPage extends React.Component {
|
||||
}
|
||||
|
||||
updatePermissionField(key, value) {
|
||||
if (key === "model") {
|
||||
this.getModel(this.state.permission.owner, value);
|
||||
}
|
||||
|
||||
value = this.parsePermissionField(key, value);
|
||||
|
||||
const permission = this.state.permission;
|
||||
@ -124,6 +139,13 @@ class PermissionEditPage extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
hasRoleDefinition(model) {
|
||||
if (model !== null) {
|
||||
return model.modelText.includes("role_definition");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
renderPermission() {
|
||||
return (
|
||||
<Card size="small" title={
|
||||
@ -214,7 +236,7 @@ class PermissionEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("role:Sub roles"), i18next.t("role:Sub roles - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} mode="tags" style={{width: "100%"}} value={this.state.permission.roles} onChange={(value => {this.updatePermissionField("roles", value);})}>
|
||||
<Select virtual={false} disabled={!this.hasRoleDefinition(this.state.model)} mode="tags" style={{width: "100%"}} value={this.state.permission.roles} onChange={(value => {this.updatePermissionField("roles", value);})}>
|
||||
{
|
||||
this.state.roles.filter(roles => (roles.owner !== this.state.roles.owner || roles.name !== this.state.roles.name)).map((permission, index) => <Option key={index} value={`${permission.owner}/${permission.name}`}>{`${permission.owner}/${permission.name}`}</Option>)
|
||||
}
|
||||
|
@ -341,7 +341,7 @@ class PermissionListPage extends BaseListPage {
|
||||
this.setState({loading: true});
|
||||
|
||||
const getPermissions = Setting.isLocalAdminUser(this.props.account) ? PermissionBackend.getPermissions : PermissionBackend.getPermissionsBySubmitter;
|
||||
getPermissions("", params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
|
||||
getPermissions(Setting.isAdminUser(this.props.account) ? "" : this.props.account.owner, params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
|
@ -25,7 +25,7 @@ class RoleListPage extends BaseListPage {
|
||||
newRole() {
|
||||
const randomName = Setting.getRandomName();
|
||||
return {
|
||||
owner: "built-in",
|
||||
owner: this.props.account.owner,
|
||||
name: `role_${randomName}`,
|
||||
createdTime: moment().format(),
|
||||
displayName: `New Role - ${randomName}`,
|
||||
@ -211,7 +211,7 @@ class RoleListPage extends BaseListPage {
|
||||
value = params.type;
|
||||
}
|
||||
this.setState({loading: true});
|
||||
RoleBackend.getRoles("", params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
|
||||
RoleBackend.getRoles(Setting.isAdminUser(this.props.account) ? "" : this.props.account.owner, params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
|
@ -554,7 +554,7 @@ export function changeLanguage(language) {
|
||||
localStorage.setItem("language", language);
|
||||
changeMomentLanguage(language);
|
||||
i18next.changeLanguage(language);
|
||||
window.location.reload(true);
|
||||
// window.location.reload(true);
|
||||
}
|
||||
|
||||
export function changeMomentLanguage(language) {
|
||||
|
@ -17,7 +17,6 @@ import {Button, Card, Col, Input, Result, Row, Select, Spin, Switch} from "antd"
|
||||
import * as UserBackend from "./backend/UserBackend";
|
||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||
import * as Setting from "./Setting";
|
||||
import {LinkOutlined} from "@ant-design/icons";
|
||||
import i18next from "i18next";
|
||||
import CropperDiv from "./CropperDiv.js";
|
||||
import * as ApplicationBackend from "./backend/ApplicationBackend";
|
||||
@ -232,16 +231,6 @@ class UserEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("general:Avatar"), i18next.t("general:Avatar - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:URL")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input prefix={<LinkOutlined />} value={this.state.user.avatar} onChange={e => {
|
||||
this.updateUserField("avatar", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:Preview")}:
|
||||
@ -661,7 +650,7 @@ class UserEditPage extends React.Component {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
this.state.loading ? <Spin size="large" /> : (
|
||||
this.state.loading ? <Spin size="large" style={{marginLeft: "50%", marginTop: "10%"}} /> : (
|
||||
this.state.user !== null ? this.renderUser() :
|
||||
<Result
|
||||
status="404"
|
||||
|
@ -374,7 +374,7 @@ class UserListPage extends BaseListPage {
|
||||
const sortField = params.sortField, sortOrder = params.sortOrder;
|
||||
this.setState({loading: true});
|
||||
if (this.state.organizationName === undefined) {
|
||||
UserBackend.getGlobalUsers(params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
|
||||
(Setting.isAdminUser(this.props.account) ? UserBackend.getGlobalUsers(params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder) : UserBackend.getUsers(this.props.account.owner, params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder))
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
|
@ -29,6 +29,7 @@ import i18next from "i18next";
|
||||
import CustomGithubCorner from "../CustomGithubCorner";
|
||||
import {CountDownInput} from "../common/CountDownInput";
|
||||
import SelectLanguageBox from "../SelectLanguageBox";
|
||||
import {withTranslation} from "react-i18next";
|
||||
|
||||
const {TabPane} = Tabs;
|
||||
|
||||
@ -155,8 +156,8 @@ class LoginPage extends React.Component {
|
||||
values["type"] = "saml";
|
||||
}
|
||||
|
||||
if (this.state.owner !== null && this.state.owner !== undefined) {
|
||||
values["organization"] = this.state.owner;
|
||||
if (this.state.application.organization !== null && this.state.application.organization !== undefined) {
|
||||
values["organization"] = this.state.application.organization;
|
||||
}
|
||||
}
|
||||
postCodeLoginAction(res) {
|
||||
@ -730,4 +731,4 @@ class LoginPage extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default LoginPage;
|
||||
export default withTranslation()(LoginPage);
|
||||
|
@ -44,10 +44,11 @@ class SingleCard extends React.Component {
|
||||
|
||||
return (
|
||||
<Card.Grid style={gridStyle} onClick={() => Setting.goToLinkSoft(this, silentSigninLink)}>
|
||||
<img src={logo} alt="logo" height={60} style={{marginBottom: "20px"}} />
|
||||
<img src={logo} alt="logo" width={"100%"} style={{marginBottom: "20px"}} />
|
||||
<Meta
|
||||
title={title}
|
||||
description={desc}
|
||||
style={{justifyContent: "center"}}
|
||||
/>
|
||||
</Card.Grid>
|
||||
);
|
||||
@ -61,7 +62,7 @@ class SingleCard extends React.Component {
|
||||
<Card
|
||||
hoverable
|
||||
cover={
|
||||
<img alt="logo" src={logo} style={{width: "100%", height: "210px", objectFit: "scale-down"}} />
|
||||
<img alt="logo" src={logo} style={{width: "100%", objectFit: "scale-down"}} />
|
||||
}
|
||||
onClick={() => Setting.goToLinkSoft(this, silentSigninLink)}
|
||||
style={isSingle ? {width: "320px"} : {width: "100%"}}
|
||||
|
@ -23,6 +23,7 @@ import ja from "./locales/ja/data.json";
|
||||
import es from "./locales/es/data.json";
|
||||
import * as Conf from "./Conf";
|
||||
import * as Setting from "./Setting";
|
||||
import {initReactI18next} from "react-i18next";
|
||||
|
||||
const resources = {
|
||||
en: en,
|
||||
@ -80,7 +81,7 @@ function initLanguage() {
|
||||
return language;
|
||||
}
|
||||
|
||||
i18n.init({
|
||||
i18n.use(initReactI18next).init({
|
||||
lng: initLanguage(),
|
||||
|
||||
resources: resources,
|
||||
|
@ -133,6 +133,7 @@
|
||||
"Certs": "Certs",
|
||||
"Click to Upload": "Click to Upload",
|
||||
"Client IP": "Client-IP",
|
||||
"Close": "Close",
|
||||
"Created time": "Erstellte Zeit",
|
||||
"Default application": "Default application",
|
||||
"Default application - Tooltip": "Default application - Tooltip",
|
||||
@ -165,6 +166,7 @@
|
||||
"Logo - Tooltip": "App's image tag",
|
||||
"Master password": "Master-Passwort",
|
||||
"Master password - Tooltip": "Masterpasswort - Tooltip",
|
||||
"Menu": "Menu",
|
||||
"Method": "Methode",
|
||||
"Model": "Model",
|
||||
"Model - Tooltip": "Model - Tooltip",
|
||||
|
@ -133,6 +133,7 @@
|
||||
"Certs": "Certs",
|
||||
"Click to Upload": "Click to Upload",
|
||||
"Client IP": "Client IP",
|
||||
"Close": "Close",
|
||||
"Created time": "Created time",
|
||||
"Default application": "Default application",
|
||||
"Default application - Tooltip": "Default application - Tooltip",
|
||||
@ -165,6 +166,7 @@
|
||||
"Logo - Tooltip": "Logo - Tooltip",
|
||||
"Master password": "Master password",
|
||||
"Master password - Tooltip": "Master password - Tooltip",
|
||||
"Menu": "Menu",
|
||||
"Method": "Method",
|
||||
"Model": "Model",
|
||||
"Model - Tooltip": "Model - Tooltip",
|
||||
|
@ -133,6 +133,7 @@
|
||||
"Certs": "Certes",
|
||||
"Click to Upload": "Click to Upload",
|
||||
"Client IP": "IP du client",
|
||||
"Close": "Close",
|
||||
"Created time": "Date de création",
|
||||
"Default application": "Default application",
|
||||
"Default application - Tooltip": "Default application - Tooltip",
|
||||
@ -165,6 +166,7 @@
|
||||
"Logo - Tooltip": "App's image tag",
|
||||
"Master password": "Mot de passe maître",
|
||||
"Master password - Tooltip": "Mot de passe maître - Infobulle",
|
||||
"Menu": "Menu",
|
||||
"Method": "Méthode",
|
||||
"Model": "Model",
|
||||
"Model - Tooltip": "Model - Tooltip",
|
||||
|
@ -133,6 +133,7 @@
|
||||
"Certs": "Certs",
|
||||
"Click to Upload": "Click to Upload",
|
||||
"Client IP": "クライアント IP",
|
||||
"Close": "Close",
|
||||
"Created time": "作成日時",
|
||||
"Default application": "Default application",
|
||||
"Default application - Tooltip": "Default application - Tooltip",
|
||||
@ -165,6 +166,7 @@
|
||||
"Logo - Tooltip": "App's image tag",
|
||||
"Master password": "マスターパスワード",
|
||||
"Master password - Tooltip": "マスターパスワード - ツールチップ",
|
||||
"Menu": "Menu",
|
||||
"Method": "方法",
|
||||
"Model": "Model",
|
||||
"Model - Tooltip": "Model - Tooltip",
|
||||
|
@ -133,6 +133,7 @@
|
||||
"Certs": "Certs",
|
||||
"Click to Upload": "Click to Upload",
|
||||
"Client IP": "Client IP",
|
||||
"Close": "Close",
|
||||
"Created time": "Created time",
|
||||
"Default application": "Default application",
|
||||
"Default application - Tooltip": "Default application - Tooltip",
|
||||
@ -165,6 +166,7 @@
|
||||
"Logo - Tooltip": "App's image tag",
|
||||
"Master password": "Master password",
|
||||
"Master password - Tooltip": "Master password - Tooltip",
|
||||
"Menu": "Menu",
|
||||
"Method": "Method",
|
||||
"Model": "Model",
|
||||
"Model - Tooltip": "Model - Tooltip",
|
||||
|
@ -133,6 +133,7 @@
|
||||
"Certs": "Сертификаты",
|
||||
"Click to Upload": "Нажмите здесь, чтобы загрузить",
|
||||
"Client IP": "IP клиента",
|
||||
"Close": "Close",
|
||||
"Created time": "Время создания",
|
||||
"Default application": "Default application",
|
||||
"Default application - Tooltip": "Default application - Tooltip",
|
||||
@ -165,6 +166,7 @@
|
||||
"Logo - Tooltip": "App's image tag",
|
||||
"Master password": "Мастер-пароль",
|
||||
"Master password - Tooltip": "Мастер-пароль - Tooltip",
|
||||
"Menu": "Menu",
|
||||
"Method": "Метод",
|
||||
"Model": "Модель",
|
||||
"Model - Tooltip": "Модель - Подсказка",
|
||||
|
@ -133,6 +133,7 @@
|
||||
"Certs": "证书",
|
||||
"Click to Upload": "点击上传",
|
||||
"Client IP": "客户端IP",
|
||||
"Close": "关闭",
|
||||
"Created time": "创建时间",
|
||||
"Default application": "默认应用",
|
||||
"Default application - Tooltip": "默认应用",
|
||||
@ -165,6 +166,7 @@
|
||||
"Logo - Tooltip": "应用程序向外展示的图标",
|
||||
"Master password": "万能密码",
|
||||
"Master password - Tooltip": "可用来登录该组织下的所有用户,方便管理员以该用户身份登录,以解决技术问题",
|
||||
"Menu": "目录",
|
||||
"Method": "方法",
|
||||
"Model": "模型",
|
||||
"Model - Tooltip": "Casbin模型",
|
||||
|
Reference in New Issue
Block a user