mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-09 01:13:41 +08:00
Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
5707e38912 | |||
ed959bd8c7 | |||
b6cdc46023 | |||
c661a57cb2 | |||
8456b7f7c4 | |||
e8d2906e3c | |||
1edb91b3a3 | |||
94b6eb803d | |||
cfce5289ed | |||
10f1c37730 |
@ -15,6 +15,8 @@
|
||||
package authz
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/casbin/casbin/v2"
|
||||
"github.com/casbin/casbin/v2/model"
|
||||
xormadapter "github.com/casbin/xorm-adapter/v2"
|
||||
@ -28,7 +30,7 @@ func InitAuthz() {
|
||||
var err error
|
||||
|
||||
tableNamePrefix := conf.GetConfigString("tableNamePrefix")
|
||||
a, err := xormadapter.NewAdapterWithTableName(conf.GetConfigString("driverName"), conf.GetBeegoConfDataSourceName()+conf.GetConfigString("dbName"), "casbin_rule", tableNamePrefix, true)
|
||||
a, err := xormadapter.NewAdapterWithTableName(conf.GetConfigString("driverName"), conf.GetConfigDataSourceName()+conf.GetConfigString("dbName"), "casbin_rule", tableNamePrefix, true)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@ -108,6 +110,7 @@ p, *, *, GET, /api/saml/metadata, *, *
|
||||
p, *, *, *, /cas, *, *
|
||||
p, *, *, *, /api/webauthn, *, *
|
||||
p, *, *, GET, /api/get-release, *, *
|
||||
p, *, *, GET, /api/get-default-application, *, *
|
||||
`
|
||||
|
||||
sa := stringadapter.NewAdapter(ruleText)
|
||||
@ -144,7 +147,7 @@ func IsAllowed(subOwner string, subName string, method string, urlPath string, o
|
||||
|
||||
func isAllowedInDemoMode(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool {
|
||||
if method == "POST" {
|
||||
if urlPath == "/api/login" || urlPath == "/api/logout" || urlPath == "/api/signup" || urlPath == "/api/send-verification-code" {
|
||||
if strings.HasPrefix(urlPath, "/api/login") || urlPath == "/api/logout" || urlPath == "/api/signup" || urlPath == "/api/send-verification-code" {
|
||||
return true
|
||||
} else if urlPath == "/api/update-user" {
|
||||
// Allow ordinary users to update their own information
|
||||
|
@ -18,3 +18,4 @@ logPostOnly = true
|
||||
origin =
|
||||
staticBaseUrl = "https://cdn.casbin.org"
|
||||
isDemoMode = false
|
||||
batchSize = 100
|
||||
|
33
conf/conf.go
33
conf/conf.go
@ -24,6 +24,19 @@ import (
|
||||
"github.com/astaxie/beego"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// this array contains the beego configuration items that may be modified via env
|
||||
presetConfigItems := []string{"httpport", "appname"}
|
||||
for _, key := range presetConfigItems {
|
||||
if value, ok := os.LookupEnv(key); ok {
|
||||
err := beego.AppConfig.Set(key, value)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetConfigString(key string) string {
|
||||
if value, ok := os.LookupEnv(key); ok {
|
||||
return value
|
||||
@ -55,17 +68,7 @@ func GetConfigInt64(key string) (int64, error) {
|
||||
return num, err
|
||||
}
|
||||
|
||||
func init() {
|
||||
// this array contains the beego configuration items that may be modified via env
|
||||
presetConfigItems := []string{"httpport", "appname"}
|
||||
for _, key := range presetConfigItems {
|
||||
if value, ok := os.LookupEnv(key); ok {
|
||||
beego.AppConfig.Set(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetBeegoConfDataSourceName() string {
|
||||
func GetConfigDataSourceName() string {
|
||||
dataSourceName := GetConfigString("dataSourceName")
|
||||
|
||||
runningInDocker := os.Getenv("RUNNING_IN_DOCKER")
|
||||
@ -84,3 +87,11 @@ func GetBeegoConfDataSourceName() string {
|
||||
func IsDemoMode() bool {
|
||||
return strings.ToLower(GetConfigString("isDemoMode")) == "true"
|
||||
}
|
||||
|
||||
func GetConfigBatchSize() int {
|
||||
res, err := strconv.Atoi(GetConfigString("batchSize"))
|
||||
if err != nil {
|
||||
res = 100
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
@ -121,3 +121,23 @@ func (c *ApiController) DeleteOrganization() {
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteOrganization(&organization))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetDefaultApplication ...
|
||||
// @Title GetDefaultApplication
|
||||
// @Tag Organization API
|
||||
// @Description get default application
|
||||
// @Param id query string true "organization id"
|
||||
// @Success 200 {object} Response The Response object
|
||||
// @router /get-default-application [get]
|
||||
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")
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(application)
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ func InitConfig() {
|
||||
}
|
||||
|
||||
func InitAdapter(createDatabase bool) {
|
||||
adapter = NewAdapter(conf.GetConfigString("driverName"), conf.GetBeegoConfDataSourceName(), conf.GetConfigString("dbName"))
|
||||
adapter = NewAdapter(conf.GetConfigString("driverName"), conf.GetConfigDataSourceName(), conf.GetConfigString("dbName"))
|
||||
if createDatabase {
|
||||
adapter.CreateDatabase()
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ package object
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
@ -66,6 +67,9 @@ type Application struct {
|
||||
TermsOfUse string `xorm:"varchar(100)" json:"termsOfUse"`
|
||||
SignupHtml string `xorm:"mediumtext" json:"signupHtml"`
|
||||
SigninHtml string `xorm:"mediumtext" json:"signinHtml"`
|
||||
FormCss string `xorm:"text" json:"formCss"`
|
||||
FormOffset int `json:"formOffset"`
|
||||
FormBackgroundUrl string `xorm:"varchar(200)" json:"formBackgroundUrl"`
|
||||
}
|
||||
|
||||
func GetApplicationCount(owner, field, value string) int {
|
||||
@ -319,7 +323,8 @@ func (application *Application) GetId() string {
|
||||
func CheckRedirectUriValid(application *Application, redirectUri string) bool {
|
||||
validUri := false
|
||||
for _, tmpUri := range application.RedirectUris {
|
||||
if strings.Contains(redirectUri, tmpUri) {
|
||||
tmpUriRegex := regexp.MustCompile(tmpUri)
|
||||
if tmpUriRegex.MatchString(redirectUri) || strings.Contains(redirectUri, tmpUri) {
|
||||
validUri = true
|
||||
break
|
||||
}
|
||||
|
@ -157,6 +157,7 @@ func initBuiltInApplication() {
|
||||
},
|
||||
RedirectUris: []string{},
|
||||
ExpireInHours: 168,
|
||||
FormOffset: 8,
|
||||
}
|
||||
AddApplication(application)
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ type Organization struct {
|
||||
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
|
||||
PhonePrefix string `xorm:"varchar(10)" json:"phonePrefix"`
|
||||
DefaultAvatar string `xorm:"varchar(100)" json:"defaultAvatar"`
|
||||
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
|
||||
Tags []string `xorm:"mediumtext" json:"tags"`
|
||||
MasterPassword string `xorm:"varchar(100)" json:"masterPassword"`
|
||||
EnableSoftDeletion bool `json:"enableSoftDeletion"`
|
||||
@ -216,3 +217,32 @@ func CheckAccountItemModifyRule(accountItem *AccountItem, user *User) (bool, str
|
||||
}
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func GetDefaultApplication(id string) *Application {
|
||||
organization := GetOrganization(id)
|
||||
if organization == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if organization.DefaultApplication != "" {
|
||||
return getApplication("admin", organization.DefaultApplication)
|
||||
}
|
||||
|
||||
applications := []*Application{}
|
||||
err := adapter.Engine.Asc("created_time").Find(&applications, &Application{Organization: organization.Name})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if len(applications) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, application := range applications {
|
||||
if application.EnableSignUp {
|
||||
return application
|
||||
}
|
||||
}
|
||||
|
||||
return applications[0]
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ func getEnforcer(permission *Permission) *casbin.Enforcer {
|
||||
tableName = permission.Adapter
|
||||
}
|
||||
tableNamePrefix := conf.GetConfigString("tableNamePrefix")
|
||||
adapter, err := xormadapter.NewAdapterWithTableName(conf.GetConfigString("driverName"), conf.GetBeegoConfDataSourceName()+conf.GetConfigString("dbName"), tableName, tableNamePrefix, true)
|
||||
adapter, err := xormadapter.NewAdapterWithTableName(conf.GetConfigString("driverName"), conf.GetConfigDataSourceName()+conf.GetConfigString("dbName"), tableName, tableNamePrefix, true)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/duo-labs/webauthn/webauthn"
|
||||
"xorm.io/core"
|
||||
@ -487,7 +488,7 @@ func AddUsers(users []*User) bool {
|
||||
}
|
||||
|
||||
func AddUsersInBatch(users []*User) bool {
|
||||
batchSize := 1000
|
||||
batchSize := conf.GetConfigBatchSize()
|
||||
|
||||
if len(users) == 0 {
|
||||
return false
|
||||
|
@ -80,7 +80,7 @@ func SetUserField(user *User, field string, value string) bool {
|
||||
value = user.Password
|
||||
}
|
||||
|
||||
affected, err := adapter.Engine.Table(user).ID(core.PK{user.Owner, user.Name}).Update(map[string]interface{}{field: value})
|
||||
affected, err := adapter.Engine.Table(user).ID(core.PK{user.Owner, user.Name}).Update(map[string]interface{}{strings.ToLower(field): value})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@ -141,9 +141,6 @@ func SetUserOAuthProperties(organization *Organization, user *User, providerType
|
||||
if userInfo.UnionId != "" {
|
||||
propertyName := fmt.Sprintf("oauth_%s_unionId", providerType)
|
||||
setUserProperty(user, propertyName, userInfo.UnionId)
|
||||
if providerType == "DingTalk" && user.DingTalk == "" {
|
||||
user.DingTalk = userInfo.UnionId
|
||||
}
|
||||
}
|
||||
|
||||
if userInfo.AvatarUrl != "" {
|
||||
|
@ -60,6 +60,7 @@ func initAPI() {
|
||||
beego.Router("/api/update-organization", &controllers.ApiController{}, "POST:UpdateOrganization")
|
||||
beego.Router("/api/add-organization", &controllers.ApiController{}, "POST:AddOrganization")
|
||||
beego.Router("/api/delete-organization", &controllers.ApiController{}, "POST:DeleteOrganization")
|
||||
beego.Router("/api/get-default-application", &controllers.ApiController{}, "GET:GetDefaultApplication")
|
||||
|
||||
beego.Router("/api/get-global-users", &controllers.ApiController{}, "GET:GetGlobalUsers")
|
||||
beego.Router("/api/get-users", &controllers.ApiController{}, "GET:GetUsers")
|
||||
|
@ -388,16 +388,15 @@ class App extends Component {
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
);
|
||||
res.push(
|
||||
<Menu.Item key="/permissions">
|
||||
<Link to="/permissions">
|
||||
{i18next.t("general:Permissions")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
);
|
||||
}
|
||||
|
||||
res.push(
|
||||
<Menu.Item key="/permissions">
|
||||
<Link to="/permissions">
|
||||
{i18next.t("general:Permissions")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
);
|
||||
|
||||
if (Setting.isAdminUser(this.state.account)) {
|
||||
res.push(
|
||||
<Menu.Item key="/models">
|
||||
|
@ -76,3 +76,8 @@
|
||||
flex: 1;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.loginBackground {
|
||||
background: #ffffff no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Card, Col, Input, Popover, Row, Select, Switch, Upload} from "antd";
|
||||
import {Button, Card, Col, Input, Popover, Radio, 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";
|
||||
@ -35,9 +35,18 @@ import "codemirror/lib/codemirror.css";
|
||||
require("codemirror/theme/material-darker.css");
|
||||
require("codemirror/mode/htmlmixed/htmlmixed");
|
||||
require("codemirror/mode/xml/xml");
|
||||
require("codemirror/mode/css/css");
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
const template = {
|
||||
padding: "30px",
|
||||
border: "2px solid #ffffff",
|
||||
borderRadius: "7px",
|
||||
backgroundColor: "#ffffff",
|
||||
boxShadow: " 0px 0px 20px rgba(0, 0, 0, 0.20)",
|
||||
};
|
||||
|
||||
class ApplicationEditPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -111,7 +120,7 @@ class ApplicationEditPage extends React.Component {
|
||||
}
|
||||
|
||||
parseApplicationField(key, value) {
|
||||
if (["expireInHours", "refreshExpireInHours"].includes(key)) {
|
||||
if (["expireInHours", "refreshExpireInHours", "offset"].includes(key)) {
|
||||
value = Setting.myParseInt(value);
|
||||
}
|
||||
return value;
|
||||
@ -148,6 +157,7 @@ class ApplicationEditPage extends React.Component {
|
||||
}
|
||||
|
||||
renderApplication() {
|
||||
const preview = JSON.stringify(template, null, 2);
|
||||
return (
|
||||
<Card size="small" title={
|
||||
<div>
|
||||
@ -536,6 +546,66 @@ class ApplicationEditPage extends React.Component {
|
||||
this.renderSignupSigninPreview()
|
||||
}
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Background URL"), i18next.t("application:Background URL - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} style={(Setting.isMobile()) ? {maxWidth: "100%"} : {}}>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 1}>
|
||||
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={23} >
|
||||
<Input prefix={<LinkOutlined />} value={this.state.application.formBackgroundUrl} onChange={e => {
|
||||
this.updateApplicationField("formBackgroundUrl", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:Preview")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<a target="_blank" rel="noreferrer" href={this.state.application.formBackgroundUrl}>
|
||||
<img src={this.state.application.formBackgroundUrl} alt={this.state.application.formBackgroundUrl} height={90} style={{marginBottom: "20px"}} />
|
||||
</a>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Form CSS"), i18next.t("application:Form CSS - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<Popover placement="right" content={
|
||||
<div style={{width: "900px", height: "300px"}} >
|
||||
<CodeMirror value={this.state.application.formCss === "" ? preview : this.state.application.formCss}
|
||||
options={{mode: "css", theme: "material-darker"}}
|
||||
onBeforeChange={(editor, data, value) => {
|
||||
this.updateApplicationField("formCss", value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
} title={i18next.t("application:Form CSS - Edit")} trigger="click">
|
||||
<Input value={this.state.application.formCss} style={{marginBottom: "10px"}} onChange={e => {
|
||||
this.updateApplicationField("formCss", e.target.value);
|
||||
}} />
|
||||
</Popover>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:From position"), i18next.t("application:From position - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Radio.Group onChange={e => {this.updateApplicationField("formOffset", e.target.value);}} value={this.state.application.formOffset !== 0 ? this.state.application.formOffset : 8}>
|
||||
<Radio.Button value={2}>left</Radio.Button>
|
||||
<Radio.Button value={8}>center</Radio.Button>
|
||||
<Radio.Button value={14}>right</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
!this.state.application.enableSignUp ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
@ -591,7 +661,7 @@ class ApplicationEditPage extends React.Component {
|
||||
<LoginPage type={"login"} mode={"signup"} application={this.state.application} />
|
||||
)
|
||||
}
|
||||
<div style={maskStyle}></div>
|
||||
<div style={maskStyle} />
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={11}>
|
||||
@ -605,7 +675,7 @@ class ApplicationEditPage extends React.Component {
|
||||
<br />
|
||||
<div style={{position: "relative", width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems: "center", overflow: "auto", flexDirection: "column", flex: "auto"}}>
|
||||
<LoginPage type={"login"} mode={"signin"} application={this.state.application} />
|
||||
<div style={maskStyle}></div>
|
||||
<div style={maskStyle} />
|
||||
</div>
|
||||
</Col>
|
||||
</React.Fragment>
|
||||
|
@ -53,6 +53,7 @@ class ApplicationListPage extends BaseListPage {
|
||||
redirectUris: ["http://localhost:9000/callback"],
|
||||
tokenFormat: "JWT",
|
||||
expireInHours: 24 * 7,
|
||||
formOffset: 8,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,7 @@
|
||||
import React from "react";
|
||||
import {Button, Card, Col, Input, Row, Select, Switch} from "antd";
|
||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||
import * as ApplicationBackend from "./backend/ApplicationBackend";
|
||||
import * as LdapBackend from "./backend/LdapBackend";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
@ -31,6 +32,7 @@ class OrganizationEditPage extends React.Component {
|
||||
classes: props,
|
||||
organizationName: props.match.params.organizationName,
|
||||
organization: null,
|
||||
applications: [],
|
||||
ldaps: null,
|
||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||
};
|
||||
@ -38,6 +40,7 @@ class OrganizationEditPage extends React.Component {
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getOrganization();
|
||||
this.getApplications();
|
||||
this.getLdaps();
|
||||
}
|
||||
|
||||
@ -50,6 +53,15 @@ class OrganizationEditPage extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
getApplications() {
|
||||
ApplicationBackend.getApplicationsByOrganization("admin", this.state.organizationName)
|
||||
.then((applications) => {
|
||||
this.setState({
|
||||
applications: applications,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getLdaps() {
|
||||
LdapBackend.getLdaps(this.state.organizationName)
|
||||
.then(res => {
|
||||
@ -209,6 +221,18 @@ class OrganizationEditPage extends React.Component {
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Default application"), i18next.t("general:Default application - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.organization.defaultApplication} onChange={(value => {this.updateOrganizationField("defaultApplication", value);})}>
|
||||
{
|
||||
this.state.applications?.map((item, index) => <Option key={index} value={item.name}>{item.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("organization:Tags"), i18next.t("organization:Tags - Tooltip"))} :
|
||||
|
@ -35,6 +35,7 @@ class OrganizationListPage extends BaseListPage {
|
||||
PasswordSalt: "",
|
||||
phonePrefix: "86",
|
||||
defaultAvatar: `${Setting.StaticBaseUrl}/img/casbin.svg`,
|
||||
defaultApplication: "",
|
||||
tags: [],
|
||||
masterPassword: "",
|
||||
enableSoftDeletion: false,
|
||||
|
@ -82,7 +82,7 @@ export const OtherProviderInfo = {
|
||||
url: "https://cloud.tencent.com/product/cos",
|
||||
},
|
||||
"Azure Blob": {
|
||||
logo: `${StaticBaseUrl}/img/social_azure.jpg`,
|
||||
logo: `${StaticBaseUrl}/img/social_azure.png`,
|
||||
url: "https://azure.microsoft.com/en-us/services/storage/blobs/",
|
||||
},
|
||||
},
|
||||
@ -354,6 +354,14 @@ export function isPromptAnswered(user, application) {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function parseObject(s) {
|
||||
try {
|
||||
return eval("(" + s + ")");
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseJson(s) {
|
||||
if (s === "") {
|
||||
return null;
|
||||
@ -500,7 +508,7 @@ export function getFriendlyFileSize(size) {
|
||||
return `${num} ${"KMGTPEZY"[i - 1]}B`;
|
||||
}
|
||||
|
||||
function getRandomInt(s) {
|
||||
function getHashInt(s) {
|
||||
let hash = 0;
|
||||
if (s.length !== 0) {
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
@ -510,16 +518,16 @@ function getRandomInt(s) {
|
||||
}
|
||||
}
|
||||
|
||||
if (hash < 0) {
|
||||
hash = -hash;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
export function getAvatarColor(s) {
|
||||
const colorList = ["#f56a00", "#7265e6", "#ffbf00", "#00a2ae"];
|
||||
let random = getRandomInt(s);
|
||||
if (random < 0) {
|
||||
random = -random;
|
||||
}
|
||||
return colorList[random % 4];
|
||||
const hash = getHashInt(s);
|
||||
return colorList[hash % 4];
|
||||
}
|
||||
|
||||
export function getLanguage() {
|
||||
@ -707,12 +715,12 @@ export function renderLogo(application) {
|
||||
if (application.homepageUrl !== "") {
|
||||
return (
|
||||
<a target="_blank" rel="noreferrer" href={application.homepageUrl}>
|
||||
<img width={250} src={application.logo} alt={application.displayName} style={{marginBottom: "30px"}} />
|
||||
<img width={250} src={application.logo} alt={application.displayName} style={{marginBottom: "10px"}} />
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<img width={250} src={application.logo} alt={application.displayName} style={{marginBottom: "30px"}} />
|
||||
<img width={250} src={application.logo} alt={application.displayName} style={{marginBottom: "10px"}} />
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -913,6 +921,14 @@ export function scrollToDiv(divId) {
|
||||
}
|
||||
}
|
||||
|
||||
export function inIframe() {
|
||||
try {
|
||||
return window !== window.parent;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export function getSyncerTableColumns(syncer) {
|
||||
switch (syncer.type) {
|
||||
case "Keycloak":
|
||||
|
@ -35,7 +35,7 @@ class AuthCallback extends React.Component {
|
||||
// realRedirectUrl = "http://localhost:9000"
|
||||
const params = new URLSearchParams(this.props.location.search);
|
||||
const state = params.get("state");
|
||||
const queryString = Util.stateToGetQueryParams(state);
|
||||
const queryString = Util.getQueryParamsFromState(state);
|
||||
return new URLSearchParams(queryString);
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,7 @@ import {Button, Checkbox, Col, Form, Input, Result, Row, Spin, Tabs} from "antd"
|
||||
import {LockOutlined, UserOutlined} from "@ant-design/icons";
|
||||
import * as UserWebauthnBackend from "../backend/UserWebauthnBackend";
|
||||
import * as AuthBackend from "./AuthBackend";
|
||||
import * as OrganizationBackend from "../backend/OrganizationBackend";
|
||||
import * as ApplicationBackend from "../backend/ApplicationBackend";
|
||||
import * as Provider from "./Provider";
|
||||
import * as ProviderButton from "./ProviderButton";
|
||||
@ -90,12 +91,26 @@ class LoginPage extends React.Component {
|
||||
return;
|
||||
}
|
||||
|
||||
ApplicationBackend.getApplication("admin", this.state.applicationName)
|
||||
.then((application) => {
|
||||
this.setState({
|
||||
application: application,
|
||||
if (this.state.owner === null || this.state.owner === undefined || this.state.owner === "") {
|
||||
ApplicationBackend.getApplication("admin", this.state.applicationName)
|
||||
.then((application) => {
|
||||
this.setState({
|
||||
application: application,
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
OrganizationBackend.getDefaultApplication("admin", this.state.owner)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
application: res.data,
|
||||
applicationName: res.data.name,
|
||||
});
|
||||
} else {
|
||||
Util.showMessage("error", res.msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getSamlApplication() {
|
||||
@ -334,49 +349,54 @@ class LoginPage extends React.Component {
|
||||
>
|
||||
</Form.Item>
|
||||
{this.renderMethodChoiceBox()}
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: i18next.t("login:Please input your username, Email or phone!"),
|
||||
},
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (this.state.isCodeSignin) {
|
||||
if (this.state.email !== "" && !Setting.isValidEmail(this.state.username) && !Setting.isValidPhone(this.state.username)) {
|
||||
this.setState({validEmailOrPhone: false});
|
||||
return Promise.reject(i18next.t("login:The input is not valid Email or Phone!"));
|
||||
}
|
||||
<Row style={{minHeight: 130, alignItems: "center"}}>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: i18next.t("login:Please input your username, Email or phone!"),
|
||||
},
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (this.state.isCodeSignin) {
|
||||
if (this.state.email !== "" && !Setting.isValidEmail(this.state.username) && !Setting.isValidPhone(this.state.username)) {
|
||||
this.setState({validEmailOrPhone: false});
|
||||
return Promise.reject(i18next.t("login:The input is not valid Email or Phone!"));
|
||||
}
|
||||
|
||||
if (Setting.isValidPhone(this.state.username)) {
|
||||
this.setState({validPhone: true});
|
||||
}
|
||||
if (Setting.isValidEmail(this.state.username)) {
|
||||
this.setState({validEmail: true});
|
||||
}
|
||||
}
|
||||
if (Setting.isValidPhone(this.state.username)) {
|
||||
this.setState({validPhone: true});
|
||||
}
|
||||
if (Setting.isValidEmail(this.state.username)) {
|
||||
this.setState({validEmail: true});
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({validEmailOrPhone: true});
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
prefix={<UserOutlined className="site-form-item-icon" />}
|
||||
placeholder={this.state.isCodeSignin ? i18next.t("login:Email or phone") : i18next.t("login:username, Email or phone")}
|
||||
disabled={!application.enablePassword}
|
||||
onChange={e => {
|
||||
this.setState({
|
||||
username: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
{
|
||||
this.renderPasswordOrCodeInput()
|
||||
}
|
||||
this.setState({validEmailOrPhone: true});
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
id = "input"
|
||||
prefix={<UserOutlined className="site-form-item-icon" />}
|
||||
placeholder={this.state.isCodeSignin ? i18next.t("login:Email or phone") : i18next.t("login:username, Email or phone")}
|
||||
disabled={!application.enablePassword}
|
||||
onChange={e => {
|
||||
this.setState({
|
||||
username: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
{
|
||||
this.renderPasswordOrCodeInput()
|
||||
}
|
||||
</Row>
|
||||
<Form.Item>
|
||||
<Form.Item name="autoSignin" valuePropName="checked" noStyle>
|
||||
<Checkbox style={{float: "left"}} disabled={!application.enablePassword}>
|
||||
@ -618,28 +638,32 @@ class LoginPage extends React.Component {
|
||||
const application = this.getApplicationObj();
|
||||
if (this.state.loginMethod === "password") {
|
||||
return this.state.isCodeSignin ? (
|
||||
<Form.Item
|
||||
name="code"
|
||||
rules={[{required: true, message: i18next.t("login:Please input your code!")}]}
|
||||
>
|
||||
<CountDownInput
|
||||
disabled={this.state.username?.length === 0 || !this.state.validEmailOrPhone}
|
||||
onButtonClickArgs={[this.state.username, this.state.validEmail ? "email" : "phone", Setting.getApplicationName(application)]}
|
||||
application={application}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="code"
|
||||
rules={[{required: true, message: i18next.t("login:Please input your code!")}]}
|
||||
>
|
||||
<CountDownInput
|
||||
disabled={this.state.username?.length === 0 || !this.state.validEmailOrPhone}
|
||||
onButtonClickArgs={[this.state.username, this.state.validEmail ? "email" : "phone", Setting.getApplicationName(application)]}
|
||||
application={application}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[{required: true, message: i18next.t("login:Please input your password!")}]}
|
||||
>
|
||||
<Input
|
||||
prefix={<LockOutlined className="site-form-item-icon" />}
|
||||
type="password"
|
||||
placeholder={i18next.t("login:Password")}
|
||||
disabled={!application.enablePassword}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[{required: true, message: i18next.t("login:Please input your password!")}]}
|
||||
>
|
||||
<Input
|
||||
prefix={<LockOutlined className="site-form-item-icon" />}
|
||||
type="password"
|
||||
placeholder={i18next.t("login:Password")}
|
||||
disabled={!application.enablePassword}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -680,29 +704,36 @@ class LoginPage extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
const formStyle = Setting.inIframe() ? null : Setting.parseObject(application.formCss);
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Col span={24} style={{display: "flex", justifyContent: "center"}}>
|
||||
<div style={{marginTop: "80px", marginBottom: "50px", textAlign: "center"}}>
|
||||
{
|
||||
Setting.renderHelmet(application)
|
||||
}
|
||||
<CustomGithubCorner />
|
||||
{
|
||||
Setting.renderLogo(application)
|
||||
}
|
||||
{/* {*/}
|
||||
{/* this.state.clientId !== null ? "Redirect" : null*/}
|
||||
{/* }*/}
|
||||
{
|
||||
this.renderSignedInBox()
|
||||
}
|
||||
{
|
||||
this.renderForm(application)
|
||||
}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<div className="loginBackground" style={{backgroundImage: Setting.inIframe() ? null : `url(${application.formBackgroundUrl})`}}>
|
||||
<CustomGithubCorner />
|
||||
<Row>
|
||||
<Col span={8} offset={application.formOffset === 0 || Setting.inIframe() ? 8 : application.formOffset} style={{display: "flex", justifyContent: "center"}}>
|
||||
<div style={{marginTop: "80px", marginBottom: "50px", textAlign: "center", ...formStyle}}>
|
||||
<div>
|
||||
{
|
||||
Setting.renderHelmet(application)
|
||||
}
|
||||
{
|
||||
Setting.renderLogo(application)
|
||||
}
|
||||
{/* {*/}
|
||||
{/* this.state.clientId !== null ? "Redirect" : null*/}
|
||||
{/* }*/}
|
||||
{
|
||||
this.renderSignedInBox()
|
||||
}
|
||||
{
|
||||
this.renderForm(application)
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -177,7 +177,9 @@ export function getAuthUrl(application, provider, method) {
|
||||
let endpoint = authInfo[provider.type].endpoint;
|
||||
const redirectUri = `${window.location.origin}/callback`;
|
||||
const scope = authInfo[provider.type].scope;
|
||||
const state = Util.getQueryParamsToState(application.name, provider.name, method);
|
||||
|
||||
const isShortState = provider.type === "WeChat" && navigator.userAgent.includes("MicroMessenger");
|
||||
const state = Util.getStateFromQueryParams(application.name, provider.name, method, isShortState);
|
||||
|
||||
if (provider.type === "Google") {
|
||||
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=code&state=${state}`;
|
||||
|
@ -614,13 +614,15 @@ class SignupPage extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
const formStyle = Setting.inIframe() ? null : Setting.parseObject(application.formCss);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="loginBackground" style={{backgroundImage: Setting.inIframe() ? null : `url(${application.formBackgroundUrl})`}}>
|
||||
<CustomGithubCorner />
|
||||
|
||||
<Row>
|
||||
<Col span={24} style={{display: "flex", justifyContent: "center"}} >
|
||||
<div style={{marginTop: "10px", textAlign: "center"}}>
|
||||
<Col span={8} offset={application.formOffset === 0 || Setting.inIframe() ? 8 : application.formOffset} style={{display: "flex", justifyContent: "center"}} >
|
||||
<div style={{marginBottom: "10px", textAlign: "center", ...formStyle}}>
|
||||
{
|
||||
Setting.renderHelmet(application)
|
||||
}
|
||||
|
@ -126,15 +126,27 @@ export function getOAuthGetParameters(params) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getQueryParamsToState(applicationName, providerName, method) {
|
||||
export function getStateFromQueryParams(applicationName, providerName, method, isShortState) {
|
||||
let query = window.location.search;
|
||||
query = `${query}&application=${applicationName}&provider=${providerName}&method=${method}`;
|
||||
if (method === "link") {
|
||||
query = `${query}&from=${window.location.pathname}`;
|
||||
}
|
||||
return btoa(query);
|
||||
|
||||
if (!isShortState) {
|
||||
return btoa(query);
|
||||
} else {
|
||||
const state = providerName;
|
||||
sessionStorage.setItem(state, query);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export function stateToGetQueryParams(state) {
|
||||
return atob(state);
|
||||
export function getQueryParamsFromState(state) {
|
||||
const query = sessionStorage.getItem(state);
|
||||
if (query === null) {
|
||||
return atob(state);
|
||||
} else {
|
||||
return query;
|
||||
}
|
||||
}
|
||||
|
@ -54,3 +54,10 @@ export function deleteOrganization(organization) {
|
||||
body: JSON.stringify(newOrganization),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getDefaultApplication(owner, name) {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-default-application?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
@ -6,6 +6,8 @@
|
||||
"Sign Up": "Registrieren"
|
||||
},
|
||||
"application": {
|
||||
"Background URL": "Background URL",
|
||||
"Background URL - Tooltip": "Background URL - Tooltip",
|
||||
"Copy SAML metadata URL": "Copy SAML metadata URL",
|
||||
"Copy prompt page URL": "Copy prompt page URL",
|
||||
"Copy signin page URL": "Copy signin page URL",
|
||||
@ -21,6 +23,11 @@
|
||||
"Enable signup": "Anmeldung aktivieren",
|
||||
"Enable signup - Tooltip": "Whether to allow users to sign up",
|
||||
"File uploaded successfully": "Datei erfolgreich hochgeladen",
|
||||
"Form CSS": "Form CSS",
|
||||
"Form CSS - Edit": "Form CSS - Edit",
|
||||
"Form CSS - Tooltip": "Form CSS - Tooltip",
|
||||
"From position": "From position",
|
||||
"From position - Tooltip": "From position - Tooltip",
|
||||
"Grant types": "Grant types",
|
||||
"Grant types - Tooltip": "Grant types - Tooltip",
|
||||
"New Application": "New Application",
|
||||
@ -117,6 +124,8 @@
|
||||
"Click to Upload": "Click to Upload",
|
||||
"Client IP": "Client-IP",
|
||||
"Created time": "Erstellte Zeit",
|
||||
"Default application": "Default application",
|
||||
"Default application - Tooltip": "Default application - Tooltip",
|
||||
"Default avatar": "Standard Avatar",
|
||||
"Default avatar - Tooltip": "default avatar",
|
||||
"Delete": "Löschen",
|
||||
|
@ -6,6 +6,8 @@
|
||||
"Sign Up": "Sign Up"
|
||||
},
|
||||
"application": {
|
||||
"Background URL": "Background URL",
|
||||
"Background URL - Tooltip": "Background URL - Tooltip",
|
||||
"Copy SAML metadata URL": "Copy SAML metadata URL",
|
||||
"Copy prompt page URL": "Copy prompt page URL",
|
||||
"Copy signin page URL": "Copy signin page URL",
|
||||
@ -21,6 +23,11 @@
|
||||
"Enable signup": "Enable signup",
|
||||
"Enable signup - Tooltip": "Enable signup - Tooltip",
|
||||
"File uploaded successfully": "File uploaded successfully",
|
||||
"Form CSS": "Form CSS",
|
||||
"Form CSS - Edit": "Form CSS - Edit",
|
||||
"Form CSS - Tooltip": "Form CSS - Tooltip",
|
||||
"From position": "From position",
|
||||
"From position - Tooltip": "From position - Tooltip",
|
||||
"Grant types": "Grant types",
|
||||
"Grant types - Tooltip": "Grant types - Tooltip",
|
||||
"New Application": "New Application",
|
||||
@ -117,6 +124,8 @@
|
||||
"Click to Upload": "Click to Upload",
|
||||
"Client IP": "Client IP",
|
||||
"Created time": "Created time",
|
||||
"Default application": "Default application",
|
||||
"Default application - Tooltip": "Default application - Tooltip",
|
||||
"Default avatar": "Default avatar",
|
||||
"Default avatar - Tooltip": "Default avatar - Tooltip",
|
||||
"Delete": "Delete",
|
||||
|
@ -6,6 +6,8 @@
|
||||
"Sign Up": "S'inscrire"
|
||||
},
|
||||
"application": {
|
||||
"Background URL": "Background URL",
|
||||
"Background URL - Tooltip": "Background URL - Tooltip",
|
||||
"Copy SAML metadata URL": "Copy SAML metadata URL",
|
||||
"Copy prompt page URL": "Copy prompt page URL",
|
||||
"Copy signin page URL": "Copy signin page URL",
|
||||
@ -21,6 +23,11 @@
|
||||
"Enable signup": "Activer l'inscription",
|
||||
"Enable signup - Tooltip": "Whether to allow users to sign up",
|
||||
"File uploaded successfully": "Fichier téléchargé avec succès",
|
||||
"Form CSS": "Form CSS",
|
||||
"Form CSS - Edit": "Form CSS - Edit",
|
||||
"Form CSS - Tooltip": "Form CSS - Tooltip",
|
||||
"From position": "From position",
|
||||
"From position - Tooltip": "From position - Tooltip",
|
||||
"Grant types": "Grant types",
|
||||
"Grant types - Tooltip": "Grant types - Tooltip",
|
||||
"New Application": "New Application",
|
||||
@ -117,6 +124,8 @@
|
||||
"Click to Upload": "Click to Upload",
|
||||
"Client IP": "IP du client",
|
||||
"Created time": "Date de création",
|
||||
"Default application": "Default application",
|
||||
"Default application - Tooltip": "Default application - Tooltip",
|
||||
"Default avatar": "Avatar par défaut",
|
||||
"Default avatar - Tooltip": "default avatar",
|
||||
"Delete": "Supprimez",
|
||||
|
@ -6,6 +6,8 @@
|
||||
"Sign Up": "新規登録"
|
||||
},
|
||||
"application": {
|
||||
"Background URL": "Background URL",
|
||||
"Background URL - Tooltip": "Background URL - Tooltip",
|
||||
"Copy SAML metadata URL": "Copy SAML metadata URL",
|
||||
"Copy prompt page URL": "Copy prompt page URL",
|
||||
"Copy signin page URL": "Copy signin page URL",
|
||||
@ -21,6 +23,11 @@
|
||||
"Enable signup": "サインアップを有効にする",
|
||||
"Enable signup - Tooltip": "Whether to allow users to sign up",
|
||||
"File uploaded successfully": "ファイルが正常にアップロードされました",
|
||||
"Form CSS": "Form CSS",
|
||||
"Form CSS - Edit": "Form CSS - Edit",
|
||||
"Form CSS - Tooltip": "Form CSS - Tooltip",
|
||||
"From position": "From position",
|
||||
"From position - Tooltip": "From position - Tooltip",
|
||||
"Grant types": "Grant types",
|
||||
"Grant types - Tooltip": "Grant types - Tooltip",
|
||||
"New Application": "New Application",
|
||||
@ -117,6 +124,8 @@
|
||||
"Click to Upload": "Click to Upload",
|
||||
"Client IP": "クライアント IP",
|
||||
"Created time": "作成日時",
|
||||
"Default application": "Default application",
|
||||
"Default application - Tooltip": "Default application - Tooltip",
|
||||
"Default avatar": "デフォルトのアバター",
|
||||
"Default avatar - Tooltip": "default avatar",
|
||||
"Delete": "削除",
|
||||
|
@ -6,6 +6,8 @@
|
||||
"Sign Up": "Sign Up"
|
||||
},
|
||||
"application": {
|
||||
"Background URL": "Background URL",
|
||||
"Background URL - Tooltip": "Background URL - Tooltip",
|
||||
"Copy SAML metadata URL": "Copy SAML metadata URL",
|
||||
"Copy prompt page URL": "Copy prompt page URL",
|
||||
"Copy signin page URL": "Copy signin page URL",
|
||||
@ -21,6 +23,11 @@
|
||||
"Enable signup": "Enable signup",
|
||||
"Enable signup - Tooltip": "Whether to allow users to sign up",
|
||||
"File uploaded successfully": "File uploaded successfully",
|
||||
"Form CSS": "Form CSS",
|
||||
"Form CSS - Edit": "Form CSS - Edit",
|
||||
"Form CSS - Tooltip": "Form CSS - Tooltip",
|
||||
"From position": "From position",
|
||||
"From position - Tooltip": "From position - Tooltip",
|
||||
"Grant types": "Grant types",
|
||||
"Grant types - Tooltip": "Grant types - Tooltip",
|
||||
"New Application": "New Application",
|
||||
@ -117,6 +124,8 @@
|
||||
"Click to Upload": "Click to Upload",
|
||||
"Client IP": "Client IP",
|
||||
"Created time": "Created time",
|
||||
"Default application": "Default application",
|
||||
"Default application - Tooltip": "Default application - Tooltip",
|
||||
"Default avatar": "Default avatar",
|
||||
"Default avatar - Tooltip": "default avatar",
|
||||
"Delete": "Delete",
|
||||
|
@ -6,6 +6,8 @@
|
||||
"Sign Up": "Регистрация"
|
||||
},
|
||||
"application": {
|
||||
"Background URL": "Background URL",
|
||||
"Background URL - Tooltip": "Background URL - Tooltip",
|
||||
"Copy SAML metadata URL": "Копировать адрес метаданных SAML",
|
||||
"Copy prompt page URL": "Скопировать URL-адрес страницы запроса",
|
||||
"Copy signin page URL": "Скопировать URL-адрес страницы входа",
|
||||
@ -21,6 +23,11 @@
|
||||
"Enable signup": "Включить регистрацию",
|
||||
"Enable signup - Tooltip": "Whether to allow users to sign up",
|
||||
"File uploaded successfully": "Файл успешно загружен",
|
||||
"Form CSS": "Form CSS",
|
||||
"Form CSS - Edit": "Form CSS - Edit",
|
||||
"Form CSS - Tooltip": "Form CSS - Tooltip",
|
||||
"From position": "From position",
|
||||
"From position - Tooltip": "From position - Tooltip",
|
||||
"Grant types": "Виды грантов",
|
||||
"Grant types - Tooltip": "Виды грантов - Подсказка",
|
||||
"New Application": "Новое приложение",
|
||||
@ -117,6 +124,8 @@
|
||||
"Click to Upload": "Нажмите здесь, чтобы загрузить",
|
||||
"Client IP": "IP клиента",
|
||||
"Created time": "Время создания",
|
||||
"Default application": "Default application",
|
||||
"Default application - Tooltip": "Default application - Tooltip",
|
||||
"Default avatar": "Аватар по умолчанию",
|
||||
"Default avatar - Tooltip": "default avatar",
|
||||
"Delete": "Удалить",
|
||||
|
@ -6,6 +6,8 @@
|
||||
"Sign Up": "注册"
|
||||
},
|
||||
"application": {
|
||||
"Background URL": "背景图URL",
|
||||
"Background URL - Tooltip": "登录页背景图的链接",
|
||||
"Copy SAML metadata URL": "复制SAML元数据URL",
|
||||
"Copy prompt page URL": "复制提醒页面URL",
|
||||
"Copy signin page URL": "复制登录页面URL",
|
||||
@ -21,6 +23,11 @@
|
||||
"Enable signup": "启用注册",
|
||||
"Enable signup - Tooltip": "是否允许用户注册",
|
||||
"File uploaded successfully": "文件上传成功",
|
||||
"Form CSS": "表单CSS",
|
||||
"Form CSS - Edit": "编辑表单CSS",
|
||||
"Form CSS - Tooltip": "表单的CSS样式(增加边框和阴影)",
|
||||
"From position": "面板的位置",
|
||||
"From position - Tooltip": "登录和注册面板的位置",
|
||||
"Grant types": "OAuth授权类型",
|
||||
"Grant types - Tooltip": "选择允许哪些OAuth协议中的Grant types",
|
||||
"New Application": "添加应用",
|
||||
@ -117,6 +124,8 @@
|
||||
"Click to Upload": "点击上传",
|
||||
"Client IP": "客户端IP",
|
||||
"Created time": "创建时间",
|
||||
"Default application": "默认应用",
|
||||
"Default application - Tooltip": "默认应用",
|
||||
"Default avatar": "默认头像",
|
||||
"Default avatar - Tooltip": "默认头像",
|
||||
"Delete": "删除",
|
||||
|
Reference in New Issue
Block a user