Compare commits

...

9 Commits

Author SHA1 Message Date
jakiuncle
29f1ec08a2 fix: fix CI error by auto waiting for localhost:7001 to start up (#1548) 2023-02-14 14:50:58 +08:00
June
389744a27d feat: change claims to claimsWithoutThirdIdp when gen token (#1552) 2023-02-14 09:33:46 +08:00
June
dc7b66822d feat: change token ExpiresIn to second (#1550) 2023-02-14 09:18:30 +08:00
Yaodong Yu
efacf8226c fix: session Id error (#1554) 2023-02-13 22:58:26 +08:00
Zayn Xie
6beb68dcce fix: some bugs in session module when testing single-log-in (#1547)
Co-authored-by: Zayn Xie <84443886+xiaoniuren99@users.noreply.github.com>
2023-02-13 18:16:31 +08:00
Yang Luo
c9b990a319 Add removeExtraSessionIds() 2023-02-12 21:11:16 +08:00
Yang Luo
eedcde3aa5 Refactor session.go 2023-02-12 21:06:08 +08:00
Yaodong Yu
950a274b23 fix: region don't display in userEditPage (#1544) 2023-02-12 18:56:56 +08:00
Yang Luo
478bd05db4 Improve error handling in migrator 2023-02-12 10:39:20 +08:00
25 changed files with 160 additions and 151 deletions

View File

@@ -97,21 +97,20 @@ jobs:
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: 16 node-version: 16
- name: back start
run: nohup go run ./main.go &
working-directory: ./
- name: front install - name: front install
run: yarn install run: yarn install
working-directory: ./web working-directory: ./web
- name: front start - name: front start
run: nohup yarn start & run: nohup yarn start &
working-directory: ./web working-directory: ./web
- name: back start - uses: cypress-io/github-action@v4
run: nohup go run ./main.go & with:
working-directory: ./ working-directory: ./web
- name: Sleep for starting wait-on: 'http://localhost:7001'
run: sleep 90s wait-on-timeout: 180
shell: bash
- name: e2e
run: npx cypress run --spec "**/e2e/**.cy.js"
working-directory: ./web
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
if: failure() if: failure()

View File

@@ -255,7 +255,9 @@ func (c *ApiController) Logout() {
if accessToken == "" && redirectUri == "" { if accessToken == "" && redirectUri == "" {
c.ClearUserSession() c.ClearUserSession()
// TODO https://github.com/casdoor/casdoor/pull/1494#discussion_r1095675265 // TODO https://github.com/casdoor/casdoor/pull/1494#discussion_r1095675265
object.DeleteSessionId(util.GetSessionId(object.CasdoorOrganization, object.CasdoorApplication, user), c.Ctx.Input.CruSession.SessionID()) owner, username := util.GetOwnerAndNameFromId(user)
object.DeleteSessionId(util.GetSessionId(owner, username, object.CasdoorApplication), c.Ctx.Input.CruSession.SessionID())
util.LogInfo(c.Ctx, "API: [%s] logged out", user) util.LogInfo(c.Ctx, "API: [%s] logged out", user)
application := c.GetSessionApplication() application := c.GetSessionApplication()

View File

@@ -127,7 +127,7 @@ func (c *ApiController) DeleteSession() {
// @Param id query string true "The id(organization/application/user) of session" // @Param id query string true "The id(organization/application/user) of session"
// @Param sessionId query string true "sessionId to be checked" // @Param sessionId query string true "sessionId to be checked"
// @Success 200 {array} string The Response object // @Success 200 {array} string The Response object
// @router /is-user-session-duplicated [get] // @router /is-session-duplicated [get]
func (c *ApiController) IsSessionDuplicated() { func (c *ApiController) IsSessionDuplicated() {
id := c.Input().Get("sessionPkId") id := c.Input().Get("sessionPkId")
sessionId := c.Input().Get("sessionId") sessionId := c.Input().Get("sessionId")

View File

@@ -49,19 +49,27 @@ func (*Migrator_1_240_0_PR_1539) DoMigration() *migrate.Migration {
SessionId []string `json:"sessionId"` SessionId []string `json:"sessionId"`
} }
var err error
tx := engine.NewSession() tx := engine.NewSession()
defer tx.Close() defer tx.Close()
tx.Begin() err := tx.Begin()
if err != nil {
return err
}
tx.Table("session_tmp").CreateTable(&Session{}) err = tx.Table("session_tmp").CreateTable(&Session{})
if err != nil {
return err
}
oldSessions := []*oldSession{} oldSessions := []*oldSession{}
newSessions := []*Session{} newSessions := []*Session{}
tx.Table("session").Find(&oldSessions) err = tx.Table("session").Find(&oldSessions)
if err != nil {
return err
}
for _, oldSession := range oldSessions { for _, oldSession := range oldSessions {
newApplication := "null" newApplication := "null"

View File

@@ -15,6 +15,8 @@
package object package object
import ( import (
"fmt"
"github.com/beego/beego" "github.com/beego/beego"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/xorm-io/core" "github.com/xorm-io/core"
@@ -72,20 +74,23 @@ func GetSessionCount(owner, field, value string) int {
func GetSingleSession(id string) *Session { func GetSingleSession(id string) *Session {
owner, name, application := util.GetOwnerAndNameAndOtherFromId(id) owner, name, application := util.GetOwnerAndNameAndOtherFromId(id)
session := &Session{Owner: owner, Name: name, Application: application} session := Session{Owner: owner, Name: name, Application: application}
_, err := adapter.Engine.ID(core.PK{owner, name, application}).Get(session) get, err := adapter.Engine.Get(&session)
if err != nil { if err != nil {
panic(err) panic(err)
} }
return session if !get {
return nil
}
return &session
} }
func UpdateSession(id string, session *Session) bool { func UpdateSession(id string, session *Session) bool {
owner, name, application := util.GetOwnerAndNameAndOtherFromId(id) owner, name, application := util.GetOwnerAndNameAndOtherFromId(id)
_, err := adapter.Engine.ID(core.PK{owner, name, application}).Get(session) if GetSingleSession(id) == nil {
if err != nil {
return false return false
} }
@@ -97,20 +102,23 @@ func UpdateSession(id string, session *Session) bool {
return affected != 0 return affected != 0
} }
func AddSession(session *Session) bool { func removeExtraSessionIds(session *Session) {
owner, name, application := session.Owner, session.Name, session.Application if len(session.SessionId) > 100 {
session.SessionId = session.SessionId[(len(session.SessionId) - 100):]
dbSession := &Session{Owner: owner, Name: name, Application: application}
get, err := adapter.Engine.ID(core.PK{owner, name, application}).Get(dbSession)
if err != nil {
return false
} }
}
var affected int64 func AddSession(session *Session) bool {
var dbErr error dbSession := GetSingleSession(session.GetId())
if !get { if dbSession == nil {
session.CreatedTime = util.GetCurrentTime() session.CreatedTime = util.GetCurrentTime()
affected, dbErr = adapter.Engine.Insert(session)
affected, err := adapter.Engine.Insert(session)
if err != nil {
panic(err)
}
return affected != 0
} else { } else {
m := make(map[string]struct{}) m := make(map[string]struct{})
for _, v := range dbSession.SessionId { for _, v := range dbSession.SessionId {
@@ -121,51 +129,46 @@ func AddSession(session *Session) bool {
dbSession.SessionId = append(dbSession.SessionId, v) dbSession.SessionId = append(dbSession.SessionId, v)
} }
} }
affected, dbErr = adapter.Engine.ID(core.PK{owner, name, application}).Update(dbSession)
removeExtraSessionIds(dbSession)
return UpdateSession(dbSession.GetId(), dbSession)
} }
if dbErr != nil {
panic(dbErr)
}
return affected != 0
} }
func DeleteSession(id string) bool { func DeleteSession(id string) bool {
owner, name, application := util.GetOwnerAndNameAndOtherFromId(id) owner, name, application := util.GetOwnerAndNameAndOtherFromId(id)
session := &Session{Owner: owner, Name: name, Application: application}
_, err := adapter.Engine.ID(core.PK{owner, name, application}).Get(session)
if err != nil {
return false
}
if owner == CasdoorOrganization && application == CasdoorApplication { if owner == CasdoorOrganization && application == CasdoorApplication {
DeleteBeegoSession(session.SessionId) session := GetSingleSession(id)
if session != nil {
DeleteBeegoSession(session.SessionId)
}
}
affected, err := adapter.Engine.ID(core.PK{owner, name, application}).Delete(&Session{})
if err != nil {
panic(err)
} }
affected, err := adapter.Engine.ID(core.PK{owner, name, application}).Delete(session)
return affected != 0 return affected != 0
} }
func DeleteSessionId(id string, sessionId string) bool { func DeleteSessionId(id string, sessionId string) bool {
owner, name, application := util.GetOwnerAndNameAndOtherFromId(id) session := GetSingleSession(id)
if session == nil {
session := &Session{Owner: owner, Name: name, Application: application}
_, err := adapter.Engine.ID(core.PK{owner, name, application}).Get(session)
if err != nil {
return false return false
} }
owner, _, application := util.GetOwnerAndNameAndOtherFromId(id)
if owner == CasdoorOrganization && application == CasdoorApplication { if owner == CasdoorOrganization && application == CasdoorApplication {
DeleteBeegoSession([]string{sessionId}) DeleteBeegoSession([]string{sessionId})
} }
session.SessionId = util.DeleteVal(session.SessionId, sessionId)
if len(session.SessionId) < 1 { session.SessionId = util.DeleteVal(session.SessionId, sessionId)
affected, _ := adapter.Engine.ID(core.PK{owner, name, application}).Delete(session) if len(session.SessionId) == 0 {
return affected != 0 return DeleteSession(id)
} else { } else {
affected, _ := adapter.Engine.ID(core.PK{owner, name, application}).Update(session) return UpdateSession(id, session)
return affected != 0
} }
} }
@@ -178,11 +181,13 @@ func DeleteBeegoSession(sessionIds []string) {
} }
} }
func (session *Session) GetId() string {
return fmt.Sprintf("%s/%s/%s", session.Owner, session.Name, session.Application)
}
func IsSessionDuplicated(id string, sessionId string) bool { func IsSessionDuplicated(id string, sessionId string) bool {
owner, name, application := util.GetOwnerAndNameAndOtherFromId(id) session := GetSingleSession(id)
session := &Session{Owner: owner, Name: name, Application: application} if session == nil {
get, _ := adapter.Engine.ID(core.PK{owner, name, application}).Get(session)
if !get {
return false return false
} else { } else {
if len(session.SessionId) > 1 { if len(session.SessionId) > 1 {

View File

@@ -27,7 +27,7 @@ import (
) )
const ( const (
hourMinutes = 60 hourSeconds = int(time.Hour / time.Second)
InvalidRequest = "invalid_request" InvalidRequest = "invalid_request"
InvalidClient = "invalid_client" InvalidClient = "invalid_client"
InvalidGrant = "invalid_grant" InvalidGrant = "invalid_grant"
@@ -306,7 +306,7 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU
Code: util.GenerateClientId(), Code: util.GenerateClientId(),
AccessToken: accessToken, AccessToken: accessToken,
RefreshToken: refreshToken, RefreshToken: refreshToken,
ExpiresIn: application.ExpireInHours * hourMinutes, ExpiresIn: application.ExpireInHours * hourSeconds,
Scope: scope, Scope: scope,
TokenType: "Bearer", TokenType: "Bearer",
CodeChallenge: challenge, CodeChallenge: challenge,
@@ -442,7 +442,7 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
Code: util.GenerateClientId(), Code: util.GenerateClientId(),
AccessToken: newAccessToken, AccessToken: newAccessToken,
RefreshToken: newRefreshToken, RefreshToken: newRefreshToken,
ExpiresIn: application.ExpireInHours * hourMinutes, ExpiresIn: application.ExpireInHours * hourSeconds,
Scope: scope, Scope: scope,
TokenType: "Bearer", TokenType: "Bearer",
} }
@@ -592,7 +592,7 @@ func GetPasswordToken(application *Application, username string, password string
Code: util.GenerateClientId(), Code: util.GenerateClientId(),
AccessToken: accessToken, AccessToken: accessToken,
RefreshToken: refreshToken, RefreshToken: refreshToken,
ExpiresIn: application.ExpireInHours * hourMinutes, ExpiresIn: application.ExpireInHours * hourSeconds,
Scope: scope, Scope: scope,
TokenType: "Bearer", TokenType: "Bearer",
CodeIsUsed: true, CodeIsUsed: true,
@@ -632,7 +632,7 @@ func GetClientCredentialsToken(application *Application, clientSecret string, sc
User: nullUser.Name, User: nullUser.Name,
Code: util.GenerateClientId(), Code: util.GenerateClientId(),
AccessToken: accessToken, AccessToken: accessToken,
ExpiresIn: application.ExpireInHours * hourMinutes, ExpiresIn: application.ExpireInHours * hourSeconds,
Scope: scope, Scope: scope,
TokenType: "Bearer", TokenType: "Bearer",
CodeIsUsed: true, CodeIsUsed: true,
@@ -659,7 +659,7 @@ func GetTokenByUser(application *Application, user *User, scope string, host str
Code: util.GenerateClientId(), Code: util.GenerateClientId(),
AccessToken: accessToken, AccessToken: accessToken,
RefreshToken: refreshToken, RefreshToken: refreshToken,
ExpiresIn: application.ExpireInHours * hourMinutes, ExpiresIn: application.ExpireInHours * hourSeconds,
Scope: scope, Scope: scope,
TokenType: "Bearer", TokenType: "Bearer",
CodeIsUsed: true, CodeIsUsed: true,

View File

@@ -265,8 +265,8 @@ func generateJwtToken(application *Application, user *User, nonce string, scope
claimsWithoutThirdIdp := getClaimsWithoutThirdIdp(claims) claimsWithoutThirdIdp := getClaimsWithoutThirdIdp(claims)
token = jwt.NewWithClaims(jwt.SigningMethodRS256, claimsWithoutThirdIdp) token = jwt.NewWithClaims(jwt.SigningMethodRS256, claimsWithoutThirdIdp)
claims.ExpiresAt = jwt.NewNumericDate(refreshExpireTime) claimsWithoutThirdIdp.ExpiresAt = jwt.NewNumericDate(refreshExpireTime)
claims.TokenType = "refresh-token" claimsWithoutThirdIdp.TokenType = "refresh-token"
refreshToken = jwt.NewWithClaims(jwt.SigningMethodRS256, claimsWithoutThirdIdp) refreshToken = jwt.NewWithClaims(jwt.SigningMethodRS256, claimsWithoutThirdIdp)
} }

View File

@@ -31,6 +31,6 @@ func GetCurrentUnixTime() string {
func IsTokenExpired(createdTime string, expiresIn int) bool { func IsTokenExpired(createdTime string, expiresIn int) bool {
createdTimeObj, _ := time.Parse(time.RFC3339, createdTime) createdTimeObj, _ := time.Parse(time.RFC3339, createdTime)
expiresAtObj := createdTimeObj.Add(time.Duration(expiresIn) * time.Minute) expiresAtObj := createdTimeObj.Add(time.Duration(expiresIn) * time.Second)
return time.Now().After(expiresAtObj) return time.Now().After(expiresAtObj)
} }

View File

@@ -56,15 +56,15 @@ func Test_IsTokenExpired(t *testing.T) {
description: "Token emitted now is valid for 60 minutes", description: "Token emitted now is valid for 60 minutes",
input: input{ input: input{
createdTime: time.Now().Format(time.RFC3339), createdTime: time.Now().Format(time.RFC3339),
expiresIn: 60, expiresIn: 3600,
}, },
expected: false, expected: false,
}, },
{ {
description: "Token emitted 60 minutes before now is valid for 60 minutes", description: "Token emitted 60 minutes before now is valid for 61 minutes",
input: input{ input: input{
createdTime: time.Now().Add(-time.Minute * 60).Format(time.RFC3339), createdTime: time.Now().Add(-time.Minute * 60).Format(time.RFC3339),
expiresIn: 61, expiresIn: 3660,
}, },
expected: false, expected: false,
}, },
@@ -72,7 +72,7 @@ func Test_IsTokenExpired(t *testing.T) {
description: "Token emitted 2 hours before now is Expired after 60 minutes", description: "Token emitted 2 hours before now is Expired after 60 minutes",
input: input{ input: input{
createdTime: time.Now().Add(-time.Hour * 2).Format(time.RFC3339), createdTime: time.Now().Add(-time.Hour * 2).Format(time.RFC3339),
expiresIn: 60, expiresIn: 3600,
}, },
expected: true, expected: true,
}, },
@@ -80,23 +80,23 @@ func Test_IsTokenExpired(t *testing.T) {
description: "Token emitted 61 minutes before now is Expired after 60 minutes", description: "Token emitted 61 minutes before now is Expired after 60 minutes",
input: input{ input: input{
createdTime: time.Now().Add(-time.Minute * 61).Format(time.RFC3339), createdTime: time.Now().Add(-time.Minute * 61).Format(time.RFC3339),
expiresIn: 60, expiresIn: 3600,
}, },
expected: true, expected: true,
}, },
{ {
description: "Token emitted 2 hours before now is valid for 120 minutes", description: "Token emitted 2 hours before now is valid for 121 minutes",
input: input{ input: input{
createdTime: time.Now().Add(-time.Hour * 2).Format(time.RFC3339), createdTime: time.Now().Add(-time.Hour * 2).Format(time.RFC3339),
expiresIn: 121, expiresIn: 7260,
}, },
expected: false, expected: false,
}, },
{ {
description: "Token emitted 159 minutes before now is Expired after 60 minutes", description: "Token emitted 159 minutes before now is Expired after 120 minutes",
input: input{ input: input{
createdTime: time.Now().Add(-time.Minute * 159).Format(time.RFC3339), createdTime: time.Now().Add(-time.Minute * 159).Format(time.RFC3339),
expiresIn: 120, expiresIn: 7200,
}, },
expected: true, expected: true,
}, },

View File

@@ -84,8 +84,8 @@ class App extends Component {
uri: null, uri: null,
menuVisible: false, menuVisible: false,
themeAlgorithm: ["default"], themeAlgorithm: ["default"],
themeData: Setting.ThemeDefault, themeData: Conf.ThemeDefault,
logo: this.getLogo(Setting.getAlgorithmNames(Setting.ThemeDefault)), logo: this.getLogo(Setting.getAlgorithmNames(Conf.ThemeDefault)),
}; };
Setting.initServerUrl(); Setting.initServerUrl();

View File

@@ -18,6 +18,7 @@ import {CopyOutlined, LinkOutlined, UploadOutlined} from "@ant-design/icons";
import * as ApplicationBackend from "./backend/ApplicationBackend"; import * as ApplicationBackend from "./backend/ApplicationBackend";
import * as CertBackend from "./backend/CertBackend"; import * as CertBackend from "./backend/CertBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import * as Conf from "./Conf";
import * as ProviderBackend from "./backend/ProviderBackend"; import * as ProviderBackend from "./backend/ProviderBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend"; import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as ResourceBackend from "./backend/ResourceBackend"; import * as ResourceBackend from "./backend/ResourceBackend";
@@ -717,7 +718,7 @@ class ApplicationEditPage extends React.Component {
<Col span={22} style={{marginTop: "5px"}}> <Col span={22} style={{marginTop: "5px"}}>
<Row> <Row>
<Radio.Group value={this.state.application.themeData?.isEnabled ?? false} onChange={e => { <Radio.Group value={this.state.application.themeData?.isEnabled ?? false} onChange={e => {
const {_, ...theme} = this.state.application.themeData ?? {...Setting.ThemeDefault, isEnabled: false}; const {_, ...theme} = this.state.application.themeData ?? {...Conf.ThemeDefault, isEnabled: false};
this.updateApplicationField("themeData", {...theme, isEnabled: e.target.value}); this.updateApplicationField("themeData", {...theme, isEnabled: e.target.value});
}} > }} >
<Radio.Button value={false}>{i18next.t("application:Follow organization theme")}</Radio.Button> <Radio.Button value={false}>{i18next.t("application:Follow organization theme")}</Radio.Button>
@@ -728,7 +729,7 @@ class ApplicationEditPage extends React.Component {
this.state.application.themeData?.isEnabled ? this.state.application.themeData?.isEnabled ?
<Row style={{marginTop: "20px"}}> <Row style={{marginTop: "20px"}}>
<ThemeEditor themeData={this.state.application.themeData} onThemeChange={(_, nextThemeData) => { <ThemeEditor themeData={this.state.application.themeData} onThemeChange={(_, nextThemeData) => {
const {isEnabled} = this.state.application.themeData ?? {...Setting.ThemeDefault, isEnabled: false}; const {isEnabled} = this.state.application.themeData ?? {...Conf.ThemeDefault, isEnabled: false};
this.updateApplicationField("themeData", {...nextThemeData, isEnabled}); this.updateApplicationField("themeData", {...nextThemeData, isEnabled});
}} /> }} />
</Row> : null </Row> : null
@@ -764,7 +765,7 @@ class ApplicationEditPage extends React.Component {
} }
renderSignupSigninPreview() { renderSignupSigninPreview() {
const themeData = this.state.application.themeData ?? Setting.ThemeDefault; const themeData = this.state.application.themeData ?? Conf.ThemeDefault;
let signUpUrl = `/signup/${this.state.application.name}`; let signUpUrl = `/signup/${this.state.application.name}`;
const signInUrl = `/login/oauth/authorize?client_id=${this.state.application.clientId}&response_type=code&redirect_uri=${this.state.application.redirectUris[0]}&scope=read&state=casdoor`; const signInUrl = `/login/oauth/authorize?client_id=${this.state.application.clientId}&response_type=code&redirect_uri=${this.state.application.redirectUris[0]}&scope=read&state=casdoor`;
const maskStyle = {position: "absolute", top: "0px", left: "0px", zIndex: 10, height: "97%", width: "100%", background: "rgba(0,0,0,0.4)"}; const maskStyle = {position: "absolute", top: "0px", left: "0px", zIndex: 10, height: "97%", width: "100%", background: "rgba(0,0,0,0.4)"};
@@ -835,7 +836,7 @@ class ApplicationEditPage extends React.Component {
} }
renderPromptPreview() { renderPromptPreview() {
const themeData = this.state.application.themeData ?? Setting.ThemeDefault; const themeData = this.state.application.themeData ?? Conf.ThemeDefault;
const promptUrl = `/prompt/${this.state.application.name}`; const promptUrl = `/prompt/${this.state.application.name}`;
const maskStyle = {position: "absolute", top: "0px", left: "0px", zIndex: 10, height: "100%", width: "100%", background: "rgba(0,0,0,0.4)"}; const maskStyle = {position: "absolute", top: "0px", left: "0px", zIndex: 10, height: "100%", width: "100%", background: "rgba(0,0,0,0.4)"};
return ( return (

View File

@@ -17,6 +17,13 @@ export const GithubRepo = "https://github.com/casdoor/casdoor";
export const ForceLanguage = ""; export const ForceLanguage = "";
export const DefaultLanguage = "en"; export const DefaultLanguage = "en";
export const InitThemeAlgorithm = true;
export const EnableExtraPages = true; export const EnableExtraPages = true;
export const InitThemeAlgorithm = true;
export const ThemeDefault = {
themeType: "default",
colorPrimary: "#5734d3",
borderRadius: 6,
isCompact: false,
};

View File

@@ -17,6 +17,7 @@ import {Redirect, Route, Switch} from "react-router-dom";
import {Spin} from "antd"; import {Spin} from "antd";
import i18next from "i18next"; import i18next from "i18next";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import * as Conf from "./Conf";
import SignupPage from "./auth/SignupPage"; import SignupPage from "./auth/SignupPage";
import SelfLoginPage from "./auth/SelfLoginPage"; import SelfLoginPage from "./auth/SelfLoginPage";
import LoginPage from "./auth/LoginPage"; import LoginPage from "./auth/LoginPage";
@@ -62,7 +63,7 @@ class EntryPage extends React.Component {
application: application, application: application,
}); });
const themeData = application !== null ? Setting.getThemeData(application.organizationObj, application) : Setting.ThemeDefault; const themeData = application !== null ? Setting.getThemeData(application.organizationObj, application) : Conf.ThemeDefault;
this.props.updataThemeData(themeData); this.props.updataThemeData(themeData);
}; };

View File

@@ -18,6 +18,7 @@ import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as ApplicationBackend from "./backend/ApplicationBackend"; import * as ApplicationBackend from "./backend/ApplicationBackend";
import * as LdapBackend from "./backend/LdapBackend"; import * as LdapBackend from "./backend/LdapBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import * as Conf from "./Conf";
import i18next from "i18next"; import i18next from "i18next";
import {LinkOutlined} from "@ant-design/icons"; import {LinkOutlined} from "@ant-design/icons";
import LdapTable from "./LdapTable"; import LdapTable from "./LdapTable";
@@ -324,7 +325,7 @@ class OrganizationEditPage extends React.Component {
<Col span={22} style={{marginTop: "5px"}}> <Col span={22} style={{marginTop: "5px"}}>
<Row> <Row>
<Radio.Group value={this.state.organization.themeData?.isEnabled ?? false} onChange={e => { <Radio.Group value={this.state.organization.themeData?.isEnabled ?? false} onChange={e => {
const {_, ...theme} = this.state.organization.themeData ?? {...Setting.ThemeDefault, isEnabled: false}; const {_, ...theme} = this.state.organization.themeData ?? {...Conf.ThemeDefault, isEnabled: false};
this.updateOrganizationField("themeData", {...theme, isEnabled: e.target.value}); this.updateOrganizationField("themeData", {...theme, isEnabled: e.target.value});
}} > }} >
<Radio.Button value={false}>{i18next.t("organization:Follow global theme")}</Radio.Button> <Radio.Button value={false}>{i18next.t("organization:Follow global theme")}</Radio.Button>
@@ -335,7 +336,7 @@ class OrganizationEditPage extends React.Component {
this.state.organization.themeData?.isEnabled ? this.state.organization.themeData?.isEnabled ?
<Row style={{marginTop: "20px"}}> <Row style={{marginTop: "20px"}}>
<ThemeEditor themeData={this.state.organization.themeData} onThemeChange={(_, nextThemeData) => { <ThemeEditor themeData={this.state.organization.themeData} onThemeChange={(_, nextThemeData) => {
const {isEnabled} = this.state.organization.themeData ?? {...Setting.ThemeDefault, isEnabled: false}; const {isEnabled} = this.state.organization.themeData ?? {...Conf.ThemeDefault, isEnabled: false};
this.updateOrganizationField("themeData", {...nextThemeData, isEnabled}); this.updateOrganizationField("themeData", {...nextThemeData, isEnabled});
}} /> }} />
</Row> : null </Row> : null

View File

@@ -17,7 +17,7 @@ import i18next from "i18next";
import React from "react"; import React from "react";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import * as UserBackend from "./backend/UserBackend"; import * as UserBackend from "./backend/UserBackend";
import {CountDownInput} from "./common/CountDownInput"; import {SendCodeInput} from "./common/SendCodeInput";
import {MailOutlined, PhoneOutlined} from "@ant-design/icons"; import {MailOutlined, PhoneOutlined} from "@ant-design/icons";
export const ResetModal = (props) => { export const ResetModal = (props) => {
@@ -93,7 +93,7 @@ export const ResetModal = (props) => {
/> />
</Row> </Row>
<Row style={{width: "100%", marginBottom: "20px"}}> <Row style={{width: "100%", marginBottom: "20px"}}>
<CountDownInput <SendCodeInput
textBefore={i18next.t("code:Code You Received")} textBefore={i18next.t("code:Code You Received")}
onChange={setCode} onChange={setCode}
method={"reset"} method={"reset"}

View File

@@ -18,6 +18,16 @@ import {Dropdown} from "antd";
import "./App.less"; import "./App.less";
import {GlobalOutlined} from "@ant-design/icons"; import {GlobalOutlined} from "@ant-design/icons";
export const Countries = [{label: "English", key: "en", country: "US", alt: "English"},
{label: "简体中文", key: "zh", country: "CN", alt: "简体中文"},
{label: "Español", key: "es", country: "ES", alt: "Español"},
{label: "Français", key: "fr", country: "FR", alt: "Français"},
{label: "Deutsch", key: "de", country: "DE", alt: "Deutsch"},
{label: "日本語", key: "ja", country: "JP", alt: "日本語"},
{label: "한국어", key: "ko", country: "KR", alt: "한국어"},
{label: "Русский", key: "ru", country: "RU", alt: "Русский"},
];
function flagIcon(country, alt) { function flagIcon(country, alt) {
return ( return (
<img width={24} alt={alt} src={`${Setting.StaticBaseUrl}/flag-icons/${country}.svg`} /> <img width={24} alt={alt} src={`${Setting.StaticBaseUrl}/flag-icons/${country}.svg`} />
@@ -32,12 +42,12 @@ class SelectLanguageBox extends React.Component {
languages: props.languages ?? ["en", "zh", "es", "fr", "de", "ja", "ko", "ru"], languages: props.languages ?? ["en", "zh", "es", "fr", "de", "ja", "ko", "ru"],
}; };
Setting.Countries.forEach((country) => { Countries.forEach((country) => {
new Image().src = `${Setting.StaticBaseUrl}/flag-icons/${country.country}.svg`; new Image().src = `${Setting.StaticBaseUrl}/flag-icons/${country.country}.svg`;
}); });
} }
items = Setting.Countries.map((country) => Setting.getItem(country.label, country.key, flagIcon(country.country, country.alt))); items = Countries.map((country) => Setting.getItem(country.label, country.key, flagIcon(country.country, country.alt)));
getOrganizationLanguages(languages) { getOrganizationLanguages(languages) {
const select = []; const select = [];

View File

@@ -49,8 +49,8 @@ class SelectRegionBox extends React.Component {
} }
> >
{ {
Setting.CountryRegionData.map((item, index) => ( Setting.getCountryNames().map((item) => (
<Option key={index} value={item.code} label={`${item.name} (${item.code})`} > <Option key={item.code} value={item.code} label={`${item.name} (${item.code})`} >
<img src={`${Setting.StaticBaseUrl}/flag-icons/${item.code}.svg`} alt={item.name} height={20} style={{marginRight: 10}} /> <img src={`${Setting.StaticBaseUrl}/flag-icons/${item.code}.svg`} alt={item.name} height={20} style={{marginRight: 10}} />
{`${item.name} (${item.code})`} {`${item.name} (${item.code})`}
</Option> </Option>

View File

@@ -30,33 +30,13 @@ export const ServerUrl = "";
// export const StaticBaseUrl = "https://cdn.jsdelivr.net/gh/casbin/static"; // export const StaticBaseUrl = "https://cdn.jsdelivr.net/gh/casbin/static";
export const StaticBaseUrl = "https://cdn.casbin.org"; export const StaticBaseUrl = "https://cdn.casbin.org";
// https://catamphetamine.gitlab.io/country-flag-icons/3x2/index.html
export const CountryRegionData = getCountryRegionData();
export const Countries = [{label: "English", key: "en", country: "US", alt: "English"},
{label: "简体中文", key: "zh", country: "CN", alt: "简体中文"},
{label: "Español", key: "es", country: "ES", alt: "Español"},
{label: "Français", key: "fr", country: "FR", alt: "Français"},
{label: "Deutsch", key: "de", country: "DE", alt: "Deutsch"},
{label: "日本語", key: "ja", country: "JP", alt: "日本語"},
{label: "한국어", key: "ko", country: "KR", alt: "한국어"},
{label: "Русский", key: "ru", country: "RU", alt: "Русский"},
];
export const ThemeDefault = {
themeType: "default",
colorPrimary: "#5734d3",
borderRadius: 6,
isCompact: false,
};
export function getThemeData(organization, application) { export function getThemeData(organization, application) {
if (application?.themeData?.isEnabled) { if (application?.themeData?.isEnabled) {
return application.themeData; return application.themeData;
} else if (organization?.themeData?.isEnabled) { } else if (organization?.themeData?.isEnabled) {
return organization.themeData; return organization.themeData;
} else { } else {
return ThemeDefault; return Conf.ThemeDefault;
} }
} }
@@ -208,18 +188,18 @@ export const OtherProviderInfo = {
}, },
}; };
export function getCountryRegionData() { export function getCountriesData() {
let language = i18next.language;
if (language === null || language === "null") {
language = Conf.DefaultLanguage;
}
const countries = require("i18n-iso-countries"); const countries = require("i18n-iso-countries");
countries.registerLocale(require("i18n-iso-countries/langs/" + language + ".json")); countries.registerLocale(require("i18n-iso-countries/langs/" + getLanguage() + ".json"));
const data = countries.getNames(language, {select: "official"}); return countries;
const result = []; }
for (const i in data) {result.push({code: i, name: data[i]});}
return result; export function getCountryNames() {
const data = getCountriesData().getNames(getLanguage(), {select: "official"});
return Object.entries(data).map(items => {
return {code: items[0], name: items[1]};
});
} }
export function initServerUrl() { export function initServerUrl() {
@@ -702,7 +682,7 @@ export function getLanguageText(text) {
} }
export function getLanguage() { export function getLanguage() {
return i18next.language; return i18next.language ?? Conf.DefaultLanguage;
} }
export function setLanguage(language) { export function setLanguage(language) {

View File

@@ -135,13 +135,6 @@ class UserListPage extends BaseListPage {
} }
renderTable(users) { renderTable(users) {
// transfer country code to name based on selected language
const countries = require("i18n-iso-countries");
countries.registerLocale(require("i18n-iso-countries/langs/" + i18next.language + ".json"));
for (const index in users) {
users[index].region = countries.getName(users[index].region, i18next.language, {select: "official"});
}
const columns = [ const columns = [
{ {
title: i18next.t("general:Organization"), title: i18next.t("general:Organization"),
@@ -267,6 +260,9 @@ class UserListPage extends BaseListPage {
width: "140px", width: "140px",
sorter: true, sorter: true,
...this.getColumnSearchProps("region"), ...this.getColumnSearchProps("region"),
render: (text, record, index) => {
return Setting.getCountriesData().getName(record.region, Setting.getLanguage(), {select: "official"});
},
}, },
{ {
title: i18next.t("user:Tag"), title: i18next.t("user:Tag"),

View File

@@ -19,7 +19,7 @@ import * as ApplicationBackend from "../backend/ApplicationBackend";
import * as Util from "./Util"; import * as Util from "./Util";
import * as Setting from "../Setting"; import * as Setting from "../Setting";
import i18next from "i18next"; import i18next from "i18next";
import {CountDownInput} from "../common/CountDownInput"; import {SendCodeInput} from "../common/SendCodeInput";
import * as UserBackend from "../backend/UserBackend"; import * as UserBackend from "../backend/UserBackend";
import {CheckCircleOutlined, KeyOutlined, LockOutlined, SolutionOutlined, UserOutlined} from "@ant-design/icons"; import {CheckCircleOutlined, KeyOutlined, LockOutlined, SolutionOutlined, UserOutlined} from "@ant-design/icons";
import CustomGithubCorner from "../CustomGithubCorner"; import CustomGithubCorner from "../CustomGithubCorner";
@@ -350,14 +350,14 @@ class ForgetPage extends React.Component {
]} ]}
> >
{this.state.verifyType === "email" ? ( {this.state.verifyType === "email" ? (
<CountDownInput <SendCodeInput
disabled={this.state.username === "" || this.state.verifyType === ""} disabled={this.state.username === "" || this.state.verifyType === ""}
method={"forget"} method={"forget"}
onButtonClickArgs={[this.state.email, "email", Setting.getApplicationName(this.getApplicationObj()), this.state.name]} onButtonClickArgs={[this.state.email, "email", Setting.getApplicationName(this.getApplicationObj()), this.state.name]}
application={application} application={application}
/> />
) : ( ) : (
<CountDownInput <SendCodeInput
disabled={this.state.username === "" || this.state.verifyType === ""} disabled={this.state.username === "" || this.state.verifyType === ""}
method={"forget"} method={"forget"}
onButtonClickArgs={[this.state.phone, "phone", Setting.getApplicationName(this.getApplicationObj()), this.state.name]} onButtonClickArgs={[this.state.phone, "phone", Setting.getApplicationName(this.getApplicationObj()), this.state.name]}

View File

@@ -26,7 +26,7 @@ import * as Setting from "../Setting";
import SelfLoginButton from "./SelfLoginButton"; import SelfLoginButton from "./SelfLoginButton";
import i18next from "i18next"; import i18next from "i18next";
import CustomGithubCorner from "../CustomGithubCorner"; import CustomGithubCorner from "../CustomGithubCorner";
import {CountDownInput} from "../common/CountDownInput"; import {SendCodeInput} from "../common/SendCodeInput";
import SelectLanguageBox from "../SelectLanguageBox"; import SelectLanguageBox from "../SelectLanguageBox";
import {CaptchaModal} from "../common/CaptchaModal"; import {CaptchaModal} from "../common/CaptchaModal";
import RedirectForm from "../common/RedirectForm"; import RedirectForm from "../common/RedirectForm";
@@ -419,7 +419,7 @@ class LoginPage extends React.Component {
rules={[ rules={[
{ {
required: true, required: true,
message: i18next.t("login:Please input your username, Email or phone!"), message: i18next.t("login:Please input your Email or Phone!"),
}, },
{ {
validator: (_, value) => { validator: (_, value) => {
@@ -755,7 +755,7 @@ class LoginPage extends React.Component {
name="code" name="code"
rules={[{required: true, message: i18next.t("login:Please input your code!")}]} rules={[{required: true, message: i18next.t("login:Please input your code!")}]}
> >
<CountDownInput <SendCodeInput
disabled={this.state.username?.length === 0 || !this.state.validEmailOrPhone} disabled={this.state.username?.length === 0 || !this.state.validEmailOrPhone}
method={"login"} method={"login"}
onButtonClickArgs={[this.state.username, this.state.validEmail ? "email" : "phone", Setting.getApplicationName(application)]} onButtonClickArgs={[this.state.username, this.state.validEmail ? "email" : "phone", Setting.getApplicationName(application)]}

View File

@@ -21,7 +21,7 @@ import i18next from "i18next";
import * as Util from "./Util"; import * as Util from "./Util";
import {authConfig} from "./Auth"; import {authConfig} from "./Auth";
import * as ApplicationBackend from "../backend/ApplicationBackend"; import * as ApplicationBackend from "../backend/ApplicationBackend";
import {CountDownInput} from "../common/CountDownInput"; import {SendCodeInput} from "../common/SendCodeInput";
import SelectRegionBox from "../SelectRegionBox"; import SelectRegionBox from "../SelectRegionBox";
import CustomGithubCorner from "../CustomGithubCorner"; import CustomGithubCorner from "../CustomGithubCorner";
import SelectLanguageBox from "../SelectLanguageBox"; import SelectLanguageBox from "../SelectLanguageBox";
@@ -365,7 +365,7 @@ class SignupPage extends React.Component {
message: i18next.t("code:Please input your verification code!"), message: i18next.t("code:Please input your verification code!"),
}]} }]}
> >
<CountDownInput <SendCodeInput
disabled={!this.state.validEmail} disabled={!this.state.validEmail}
method={"signup"} method={"signup"}
onButtonClickArgs={[this.state.email, "email", Setting.getApplicationName(application)]} onButtonClickArgs={[this.state.email, "email", Setting.getApplicationName(application)]}
@@ -419,7 +419,7 @@ class SignupPage extends React.Component {
}, },
]} ]}
> >
<CountDownInput <SendCodeInput
disabled={!this.state.validPhone} disabled={!this.state.validPhone}
method={"signup"} method={"signup"}
onButtonClickArgs={[this.state.phone, "phone", Setting.getApplicationName(application)]} onButtonClickArgs={[this.state.phone, "phone", Setting.getApplicationName(application)]}

View File

@@ -109,7 +109,7 @@ export function setPassword(userOwner, userName, oldPassword, newPassword) {
}).then(res => res.json()); }).then(res => res.json());
} }
export function sendCode(checkType, checkId, checkKey, method, dest, type, applicationId, checkUser) { export function sendCode(checkType, checkId, checkKey, method, dest, type, applicationId, checkUser = "") {
const formData = new FormData(); const formData = new FormData();
formData.append("checkType", checkType); formData.append("checkType", checkType);
formData.append("checkId", checkId); formData.append("checkId", checkId);

View File

@@ -21,7 +21,7 @@ import {CaptchaWidget} from "./CaptchaWidget";
const {Search} = Input; const {Search} = Input;
export const CountDownInput = (props) => { export const SendCodeInput = (props) => {
const {disabled, textBefore, onChange, onButtonClickArgs, application, method} = props; const {disabled, textBefore, onChange, onButtonClickArgs, application, method} = props;
const [visible, setVisible] = React.useState(false); const [visible, setVisible] = React.useState(false);
const [key, setKey] = React.useState(""); const [key, setKey] = React.useState("");

View File

@@ -14,14 +14,13 @@
import {Card, ConfigProvider, Form, Layout, Switch, theme} from "antd"; import {Card, ConfigProvider, Form, Layout, Switch, theme} from "antd";
import ThemePicker from "./ThemePicker"; import ThemePicker from "./ThemePicker";
import ColorPicker from "./ColorPicker"; import ColorPicker, {GREEN_COLOR, PINK_COLOR} from "./ColorPicker";
import RadiusPicker from "./RadiusPicker"; import RadiusPicker from "./RadiusPicker";
import * as React from "react"; import * as React from "react";
import {GREEN_COLOR, PINK_COLOR} from "./ColorPicker"; import {useEffect} from "react";
import {Content} from "antd/es/layout/layout"; import {Content} from "antd/es/layout/layout";
import i18next from "i18next"; import i18next from "i18next";
import {useEffect} from "react"; import * as Conf from "../../Conf";
import * as Setting from "../../Setting";
const ThemesInfo = { const ThemesInfo = {
default: {}, default: {},
@@ -41,7 +40,7 @@ const ThemesInfo = {
const onChange = () => {}; const onChange = () => {};
export default function ThemeEditor(props) { export default function ThemeEditor(props) {
const themeData = props.themeData ?? Setting.ThemeDefault; const themeData = props.themeData ?? Conf.ThemeDefault;
const onThemeChange = props.onThemeChange ?? onChange; const onThemeChange = props.onThemeChange ?? onChange;
const {isCompact, themeType, ...themeToken} = themeData; const {isCompact, themeType, ...themeToken} = themeData;
@@ -59,7 +58,7 @@ export default function ThemeEditor(props) {
}, [isLight, isCompact]); }, [isLight, isCompact]);
useEffect(() => { useEffect(() => {
const mergedData = Object.assign(Object.assign(Object.assign({}, Setting.ThemeDefault), {themeType}), ThemesInfo[themeType]); const mergedData = Object.assign(Object.assign(Object.assign({}, Conf.ThemeDefault), {themeType}), ThemesInfo[themeType]);
onThemeChange(null, mergedData); onThemeChange(null, mergedData);
form.setFieldsValue(mergedData); form.setFieldsValue(mergedData);
}, [themeType]); }, [themeType]);