Compare commits

..

93 Commits

Author SHA1 Message Date
DacongDA
558b168477 feat: can verify OTP during OAuth login (#3531)
* feat: support verify OTP during OAuth login

* fix: fail to login if mfa not enable

* fix: fail to login if mfa not enable

* fix: fix mfaRequired not valid in saml/auth
2025-01-27 19:37:26 +08:00
DacongDA
802b6812a9 feat: fix strange "Email is invalid" error in forget password page (#3527) 2025-01-23 14:35:11 +08:00
DacongDA
a5a627f92e feat: optimize get-groups API and GroupListPage (#3518)
* fix: optimize get-groups api and GroupListPage

* fix: fix linter issue
2025-01-23 09:47:39 +08:00
DacongDA
9701818a6e feat: delete groups for user while deleting user (#3525) 2025-01-23 09:46:33 +08:00
DacongDA
06986fbd41 feat: fix theme filter for other URLs like SAML (#3523)
* fix: fix error cause by theme filter

* fix: add saml url to theme filter and use getGetOwnerAndNameFromIdWithError instead of using GetOwnerAndNameFromId

* fix: fix code error

* fix: add support for cas and pack judgement into a function

* fix: fix linter err
2025-01-22 19:12:12 +08:00
hsluoyz
3d12ac8dc2 feat: improve HandleScim() 2025-01-22 16:15:19 +08:00
DacongDA
f01839123f feat: fix missing param recoveryCodes in /mfa/setup/enable API (#3520) 2025-01-21 22:56:02 +08:00
DacongDA
e1b3b0ac6a feat: allow user use other mfaType in mfa step and skip redundant MFA verification (#3499)
* feat: allow user use other mfaType in mfa step and skip redundant MFA verification

* feat: improve format
2025-01-21 20:16:18 +08:00
DacongDA
4b0a2fdbfc feat: append HTML document title and favicon to cookie (#3519)
* feat: append HTML document title and favicon to cookie

* feat: remove useless cookie
2025-01-21 19:42:21 +08:00
DacongDA
db551eb24a feat: LDAP user can reset password with old password and new password (#3516)
* feat: support user reset password with old password and new password

* feat: merge similar code
2025-01-20 21:42:05 +08:00
DacongDA
18b49bb731 feat: can reset LDAP password with different password encryption methods (#3513) 2025-01-20 20:00:23 +08:00
hsluoyz
17653888a3 feat: refactor the TestSmtpServer code 2025-01-20 03:17:09 +08:00
hsluoyz
ee16616df4 feat: support socks5Proxy for AWS Email provider 2025-01-20 02:39:23 +08:00
hsluoyz
ea450005e0 feat: fix "logo" bug in footer 2025-01-20 00:01:46 +08:00
DacongDA
4c5ad14f6b fix: spin will squeeze login panel (#3509) 2025-01-19 23:35:04 +08:00
DacongDA
49dda2aea5 feat: append footerHtml to cookie (#3508) 2025-01-19 23:34:43 +08:00
DacongDA
a74a004540 feat: append logo url to cookie (#3507) 2025-01-19 08:02:44 +08:00
DacongDA
2b89f6b37b feat: fix issue that application theme is ignored in appendThemeCookie() (#3506) 2025-01-18 21:28:39 +08:00
DacongDA
c699e35e6b feat: load theme from first HTML render cookie (#3505) 2025-01-18 19:04:16 +08:00
DacongDA
e28d90d0aa feat: support CUCloud SMN notification provider (#3502) 2025-01-17 08:35:31 +08:00
DacongDA
4fc7600865 feat: skip update user ranking if ranking not in accountItem (#3500) 2025-01-14 22:43:49 +08:00
Wind Li
19f62a461b feat: fix SAML's redirectUrl and POST ProtocolBinding (#3498) 2025-01-13 20:55:37 +08:00
DacongDA
7ddc2778c0 feat: show error message when organization doesn't have default application in invitation edit page (#3495)
* fix: inform user when organization haven't default application in signup page

* fix: include org name in the error message
2025-01-12 22:48:21 +08:00
DacongDA
b96fa2a995 feat: skip GetUserCount() if there is no quota limit (#3491) 2025-01-10 22:28:25 +08:00
hsluoyz
fcfb73af6e feat: increase org password field length to 200 2025-01-09 20:07:49 +08:00
hsluoyz
43bebc03b9 feat: fix crash in roleChangeTrigger() 2025-01-09 16:41:56 +08:00
WindSpiritSR
c5f25cbc7d feat: getPidByPort() supports alpine now (#3483)
Signed-off-by: WindSpiritSR <simon343riley@gmail.com>
2025-01-08 12:18:46 +08:00
Cutsin
3feb6ce84d feat: add Kwai OAuth provider (#3480)
* feat: add Kwai OAuth provider

* fix: incorrect parameter in getAuthUrl
2025-01-08 00:09:16 +08:00
hsluoyz
08d6b45fc5 feat: keeps "build" folder during yarn build 2025-01-07 23:38:50 +08:00
hsluoyz
56d0de64dc feat: support StopOldInstance() 2025-01-07 21:39:21 +08:00
DacongDA
1813e8e8c7 feat: return goroutine error in get-dashboard API (#3479) 2025-01-07 10:35:45 +08:00
DacongDA
e27c764a55 feat: fix bug that GitHub oauth provider shows error if failed to fetch user's email (#3474)
* fix: fix github idp will stop login if it cannot fetch user's email through al restful api

* Update github.go

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2025-01-05 20:25:42 +08:00
DacongDA
e5a2057382 feat: fix empty scope bug in RefreshToken API (#3467)
* fix: fix scope will be empty when user not passing scope in refresh api

* fix: promote code format
2025-01-02 12:53:17 +08:00
Yang Luo
8457ff7433 feat: support radiusDefaultOrganization in app.conf 2025-01-02 00:10:58 +08:00
DacongDA
888a6f2feb feat: add regex to restrict Email addresses in OAuth provider (#3465)
* feat: support use regex expression to limit email receiver address

* feat: limit in correct pos

* feat: promote code format

* feat: promote code format

* fix: fix linter issue
2025-01-02 00:00:57 +08:00
IZUMI-Zu
b57b64fc36 feat: add origin field for mfaAccountTable (#3463) 2024-12-29 22:51:21 +08:00
DacongDA
0d239ba1cf feat: improve the error message of GitHub OAuth provider (#3462) 2024-12-29 21:54:54 +08:00
DacongDA
8927e08217 feat: speed up GetDashboard() by only fetching last 30 days data (#3458)
* feat: only check 30 days data

* refactor: refactor GetDashboard to reduce code line

* refactor: refactor GetDashboard to reduce code line

* refactor: remove unused where

* fix: fix error code
2024-12-29 16:15:52 +08:00
DacongDA
0636069584 feat: only fetch created_time field to reduce data size in get-dashboard API (#3457) 2024-12-28 23:52:19 +08:00
Yang Luo
4d0f73c84e feat: fix Casdoor OAuth provider doesn't use domain field bug 2024-12-28 10:01:56 +08:00
reserved
74a2478e10 feat: Make MinIO storage provider region setting configurable (#3433)
* fix: Make MinIO provider region setting configurable

* Fix: Correct the issue where modifications to MinIO's default logic caused behavioral discrepancies
2024-12-23 16:07:14 +08:00
nano
acc6f3e887 feat: escape the avatal URL in CAS response (#3434) 2024-12-20 17:11:58 +08:00
Xin-Fax
185ab9750a feat: fix VerificationRecord.IsUsed JSON Field Mapping 2024-12-18 13:56:54 +08:00
Cliff
48adc050d6 feat: can pass empty user id on user update (#3443) 2024-12-18 07:56:44 +08:00
Coki
b0e318c9db feat: add localized tab titles for Basic and Advanced Editors (#3431)
* feat: add localized tab titles for Basic and Advanced Editors

* docs: update translations for model editor labels in multiple locales
2024-12-16 08:34:13 +08:00
Coki
f9a6efc00f feat: advanced model editor should support changing UI language (#3430) 2024-12-15 15:53:29 +08:00
DacongDA
bd4a6775dd feat: get github user email with user/emails api (#3428)
* feat: get user email use `user/emails` api

* feat: improve code format

* feat: improve code format
2024-12-15 10:28:18 +08:00
Coki
e3a43d0062 feat: improve the advanced editor of model edit page (#3427) 2024-12-15 02:07:02 +08:00
DacongDA
0cf281cac0 feat: fix record's password regex bug (#3421) 2024-12-11 08:43:03 +08:00
XIAOZHUOWU
7322f67ae0 feat: add model, adapter and enforcer to the dashboard page chart (#3413)
* [feature] Add more data (Model, Adapter, Enforcer) to the dashboard page chart #3379

* feat: add model, adapter, enforcer to dashboard
2024-12-09 16:07:39 +08:00
Xin-Fax
b927c6d7b4 feat: support LDAP's SetPassword (#3395)
* fix: Resolve the issue mentioned in #3392

* fix: Change checkLdapUserPassword to CheckLdapUserPassword.

* fix: the issue mentioned by hsluoyz.

* fix: Check if the user parameter is nil

* fix: use existing i18n message
2024-12-09 16:06:24 +08:00
nohup
01212cd1f3 feat: add AiAssistantUrl to frontend config (#3385) 2024-12-08 20:44:28 +08:00
Xinyu Ge
bf55f94d41 feat: support CUCloud OSS storage provider (#3400) 2024-12-08 20:24:38 +08:00
Yang Luo
f14711d315 feat: fix frontend bug 2024-12-07 21:53:01 +08:00
DacongDA
58e1c28f7c feat: support LDAPS protocol (#3390)
* feat: support ldaps

* fix: unencrypted port 389 not work after enable SSL
fix: remove useless conf and set ldapsCertId to empty
fix: return and log getTLSconfig error

* fix: remove unused setting

* fix: check nil condition

* fix: not log fail when certId is empty
2024-12-07 21:26:07 +08:00
Yang Luo
922b19c64b feat: reduce i18n items 2024-12-07 21:22:57 +08:00
DacongDA
1d21c3fa90 feat: fix issue that introspectionResponse uses Bearer instead of raw tokenType (#3399) 2024-12-05 20:59:30 +08:00
DacongDA
6175fd6764 feat: make token_type_hint optional (#3397) 2024-12-04 20:10:15 +08:00
Luckery
2ceb54f058 feat: support most popular currencies (#3388) 2024-12-01 21:46:44 +08:00
DacongDA
aaeaa7fefa feat: update go sms sender (#3386) 2024-11-29 23:00:34 +08:00
DacongDA
d522247552 feat: fix countryCode param bug in MFA login (#3384) 2024-11-29 21:46:06 +08:00
DacongDA
79dbdab6c9 feat: fix "dest is missing" bug in MFA login (#3383)
* feat: support stateless mfa setup

* Revert "feat: support stateless mfa setup"

This reverts commit bd843b2ff3.

* feat: use new implement

* fix: missing set field on login
2024-11-29 19:59:30 +08:00
DacongDA
fe40910e3b feat: support stateless MFA setup (#3382) 2024-11-29 19:50:10 +08:00
Xinyu Ge
2d1736f13a feat: Add more data to the dashboard page chart #3365 (#3375)
* test

* feat: #3365 add more dada to the dashboard page chart

* feat: #3365 Add more data to the dashboard page chart
2024-11-26 09:16:35 +08:00
ming.zhang
12b4d1c7cd feat: change LDAP attribute from cn to title for correct username mapping (#3378) 2024-11-26 09:13:05 +08:00
hamidreza abedi
a45d2b87c1 feat: Add translations for Persian (#3372) 2024-11-23 16:24:07 +08:00
DacongDA
8484465d09 feat: fix SAML failed to redirect issue when login api returns RequiredMfa (#3364) 2024-11-21 20:31:56 +08:00
Luckery
dff65eee20 feat: Force users to change their passwords after 3/6/12 months (#3352)
* feat: Force users to change their passwords after 3/6/12 months

* feat: Check if the password has expired by using the last_change_password_time field added to the user table

* feat: Use the created_time field of the user table to aid password expiration checking

* feat: Rename variable
2024-11-19 21:06:52 +08:00
Eng Zer Jun
596016456c feat: update CI's upload-artifact and download-artifact actions to v4 (#3361)
v3 of `actions/upload-artifact` and `actions/download-artifact` will be
fully deprecated by 5 December 2024. Jobs that are scheduled to run
during the brownout periods will also fail. See [1][2].

[1]: https://github.blog/changelog/2024-04-16-deprecation-notice-v3-of-the-artifact-actions/
[2]: https://github.blog/changelog/2024-11-05-notice-of-breaking-changes-for-github-actions/

Signed-off-by: Eng Zer Jun <engzerjun@gmail.com>
2024-11-19 00:07:59 +08:00
DacongDA
673261c258 feat: fix placeholder bug in signin page (#3359) 2024-11-17 00:14:26 +08:00
DacongDA
3c5985a3c0 fix: fix several bugs in samlRequest (#3358) 2024-11-17 00:14:04 +08:00
DacongDA
4f3d62520a feat: fix the dashboard page shows zero data in mobile phone (#3356) 2024-11-16 22:02:49 +08:00
DacongDA
96f8b3d937 feat: fix SAML metadata URL and XML generation issue when enablePostBinding is enabled (#3354) 2024-11-16 15:35:30 +08:00
Yang Luo
7ab5a5ade1 feat: add processArgsToTempFiles() to RunCasbinCommand() 2024-11-15 20:25:48 +08:00
Yang Luo
5cbd0a96ca Use json format for argString in RunCasbinCommand() 2024-11-15 18:27:25 +08:00
Yang Luo
7ccd8c4d4f feat: add RunCasbinCommand() API 2024-11-15 17:44:57 +08:00
ZhaoYP 2001
b0fa3fc484 feat: add Casbin CLI API to Casdoor (#3351) 2024-11-15 16:10:22 +08:00
Yang Luo
af01c4226a feat: add Organization.PasswordExpireDays field 2024-11-15 11:33:28 +08:00
DacongDA
7a3d85a29a feat: update github token to fix CI cannot release issue (#3348) 2024-11-14 18:05:56 +08:00
IZUMI-Zu
fd5ccd8d41 feat: support copying token to clipboard for casdoor-app (#3345)
* feat: support copy token to clipboard for casdoor-app auth

* feat: abstract casdoor-app related code
2024-11-13 17:06:09 +08:00
Yang Luo
a439c5195d feat: get token only by hash now, remove get-by-value backward-compatible code 2024-11-13 17:04:27 +08:00
Yang Luo
ba2e997d54 feat: fix CheckUpdateUser() logic to fix add-user error 2024-11-06 08:34:13 +08:00
Luckery
0818de85d1 feat: fix username checks when organization.UseEmailAsUsername is enabled (#3329)
* feat: Username support email format

* feat: Only fulfill the first requirement

* fix: Improve code robustness
2024-11-05 20:38:47 +08:00
Yang Luo
457c6098a4 feat: fix MFA empty CountryCode bug and show MFA error better in frontend 2024-11-04 16:17:24 +08:00
Yang Luo
60f979fbb5 feat: fix MfaSetupPage empty bug when user's signup application is empty 2024-11-04 00:04:47 +08:00
Luckery
ff53e44fa6 feat: use virtual select UI in role edit page (#3322) 2024-11-03 20:05:34 +08:00
Yang Luo
1832de47db feat: fix bug in CheckEntryIp() 2024-11-03 20:00:52 +08:00
Yang Luo
535eb0c465 fix: fix IP Whitelist field bug in application edit page 2024-11-03 19:55:59 +08:00
ithilelda
c190634cf3 feat: show Domain field for Qiniu storage provider (#3318)
allow Qiniu Provider to edit the Domain property in the edit page.
2024-10-27 14:10:58 +08:00
Cliff
f7559aa040 feat: set created time if not presented in AddUser() API (#3315) 2024-10-24 23:06:05 +08:00
DacongDA
1e0b709c73 feat: pass signin method to CAS login to fix bug (#3313) 2024-10-24 14:56:12 +08:00
DacongDA
c0800b7fb3 feat: add util.IsValidOrigin() to improve CORS filter (#3301)
* fix: CORS check issue

* fix: promote format

* fix: promote format

* fix: promote format

* fix: promote format

* Update application.go

* Update cors_filter.go

* Update validation.go

---------

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2024-10-20 20:09:21 +08:00
eya46
6fcdad2100 feat: fix bug that fails to login when PasswordObfuscator is enabled (#3299) 2024-10-19 23:09:59 +08:00
123 changed files with 4183 additions and 2076 deletions

View File

@@ -114,12 +114,12 @@ jobs:
wait-on-timeout: 210 wait-on-timeout: 210
working-directory: ./web working-directory: ./web
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v4
if: failure() if: failure()
with: with:
name: cypress-screenshots name: cypress-screenshots
path: ./web/cypress/screenshots path: ./web/cypress/screenshots
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v4
if: always() if: always()
with: with:
name: cypress-videos name: cypress-videos
@@ -147,7 +147,7 @@ jobs:
- name: Release - name: Release
run: yarn global add semantic-release@17.4.4 && semantic-release run: yarn global add semantic-release@17.4.4 && semantic-release
env: env:
GH_TOKEN: ${{ secrets.GH_BOT_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Fetch Current version - name: Fetch Current version
id: get-current-tag id: get-current-tag

View File

@@ -98,6 +98,7 @@ p, *, *, GET, /api/get-organization-names, *, *
p, *, *, GET, /api/get-all-objects, *, * p, *, *, GET, /api/get-all-objects, *, *
p, *, *, GET, /api/get-all-actions, *, * p, *, *, GET, /api/get-all-actions, *, *
p, *, *, GET, /api/get-all-roles, *, * p, *, *, GET, /api/get-all-roles, *, *
p, *, *, GET, /api/run-casbin-command, *, *
p, *, *, GET, /api/get-invitation-info, *, * p, *, *, GET, /api/get-invitation-info, *, *
p, *, *, GET, /api/faceid-signin-begin, *, * p, *, *, GET, /api/faceid-signin-begin, *, *
` `

View File

@@ -25,7 +25,10 @@ enableErrorMask = false
enableGzip = true enableGzip = true
inactiveTimeoutMinutes = inactiveTimeoutMinutes =
ldapServerPort = 389 ldapServerPort = 389
ldapsCertId = ""
ldapsServerPort = 636
radiusServerPort = 1812 radiusServerPort = 1812
radiusDefaultOrganization = "built-in"
radiusSecret = "secret" radiusSecret = "secret"
quota = {"organization": -1, "user": -1, "application": -1, "provider": -1} quota = {"organization": -1, "user": -1, "application": -1, "provider": -1}
logConfig = {"filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"} logConfig = {"filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}

View File

@@ -22,6 +22,7 @@ import (
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"regexp"
"strconv" "strconv"
"strings" "strings"
@@ -305,6 +306,35 @@ func isProxyProviderType(providerType string) bool {
return false return false
} }
func checkMfaEnable(c *ApiController, user *object.User, organization *object.Organization, verificationType string) bool {
if object.IsNeedPromptMfa(organization, user) {
// The prompt page needs the user to be srigned in
c.SetSessionUsername(user.GetId())
c.ResponseOk(object.RequiredMfa)
return true
}
if user.IsMfaEnabled() {
c.setMfaUserSession(user.GetId())
mfaList := object.GetAllMfaProps(user, true)
mfaAllowList := []*object.MfaProps{}
for _, prop := range mfaList {
if prop.MfaType == verificationType || !prop.Enabled {
continue
}
mfaAllowList = append(mfaAllowList, prop)
}
if len(mfaAllowList) >= 1 {
c.SetSession("verificationCodeType", verificationType)
c.Ctx.Input.CruSession.SessionRelease(c.Ctx.ResponseWriter)
c.ResponseOk(object.NextMfa, mfaAllowList)
return true
}
}
return false
}
// Login ... // Login ...
// @Title Login // @Title Login
// @Tag Login API // @Tag Login API
@@ -330,6 +360,8 @@ func (c *ApiController) Login() {
return return
} }
verificationType := ""
if authForm.Username != "" { if authForm.Username != "" {
if authForm.Type == ResponseTypeLogin { if authForm.Type == ResponseTypeLogin {
if c.GetSessionUsername() != "" { if c.GetSessionUsername() != "" {
@@ -424,6 +456,12 @@ func (c *ApiController) Login() {
c.ResponseError(err.Error(), nil) c.ResponseError(err.Error(), nil)
return return
} }
if verificationCodeType == object.VerifyTypePhone {
verificationType = "sms"
} else {
verificationType = "email"
}
} else { } else {
var application *object.Application var application *object.Application
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application)) application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
@@ -514,16 +552,7 @@ func (c *ApiController) Login() {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
} }
if object.IsNeedPromptMfa(organization, user) { if checkMfaEnable(c, user, organization, verificationType) {
// The prompt page needs the user to be signed in
c.SetSessionUsername(user.GetId())
c.ResponseOk(object.RequiredMfa)
return
}
if user.IsMfaEnabled() {
c.setMfaUserSession(user.GetId())
c.ResponseOk(object.NextMfa, user.GetPreferredMfaProps(true))
return return
} }
@@ -617,6 +646,17 @@ func (c *ApiController) Login() {
c.ResponseError(fmt.Sprintf(c.T("auth:Failed to login in: %s"), err.Error())) c.ResponseError(fmt.Sprintf(c.T("auth:Failed to login in: %s"), err.Error()))
return return
} }
if provider.EmailRegex != "" {
reg, err := regexp.Compile(provider.EmailRegex)
if err != nil {
c.ResponseError(fmt.Sprintf(c.T("auth:Failed to login in: %s"), err.Error()))
return
}
if !reg.MatchString(userInfo.Email) {
c.ResponseError(fmt.Sprintf(c.T("check:Email is invalid")))
}
}
} }
if authForm.Method == "signup" { if authForm.Method == "signup" {
@@ -648,6 +688,11 @@ func (c *ApiController) Login() {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
if checkMfaEnable(c, user, organization, verificationType) {
return
}
resp = c.HandleLoggedIn(application, user, &authForm) resp = c.HandleLoggedIn(application, user, &authForm)
c.Ctx.Input.SetParam("recordUserId", user.GetId()) c.Ctx.Input.SetParam("recordUserId", user.GetId())
@@ -854,7 +899,12 @@ func (c *ApiController) Login() {
} }
if authForm.Passcode != "" { if authForm.Passcode != "" {
mfaUtil := object.GetMfaUtil(authForm.MfaType, user.GetPreferredMfaProps(false)) if authForm.MfaType == c.GetSession("verificationCodeType") {
c.ResponseError("Invalid multi-factor authentication type")
return
}
user.CountryCode = user.GetCountryCode(user.CountryCode)
mfaUtil := object.GetMfaUtil(authForm.MfaType, user.GetMfaProps(authForm.MfaType, false))
if mfaUtil == nil { if mfaUtil == nil {
c.ResponseError("Invalid multi-factor authentication type") c.ResponseError("Invalid multi-factor authentication type")
return return
@@ -865,6 +915,7 @@ func (c *ApiController) Login() {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
c.SetSession("verificationCodeType", "")
} else if authForm.RecoveryCode != "" { } else if authForm.RecoveryCode != "" {
err = object.MfaRecover(user, authForm.RecoveryCode) err = object.MfaRecover(user, authForm.RecoveryCode)
if err != nil { if err != nil {
@@ -877,7 +928,11 @@ func (c *ApiController) Login() {
} }
var application *object.Application var application *object.Application
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application)) if authForm.ClientId == "" {
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
} else {
application, err = object.GetApplicationByClientId(authForm.ClientId)
}
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
@@ -907,6 +962,10 @@ func (c *ApiController) Login() {
return return
} }
if authForm.Provider == "" {
authForm.Provider = authForm.ProviderBack
}
user := c.getCurrentUser() user := c.getCurrentUser()
resp = c.HandleLoggedIn(application, user, &authForm) resp = c.HandleLoggedIn(application, user, &authForm)

View File

@@ -0,0 +1,114 @@
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
)
func processArgsToTempFiles(args []string) ([]string, []string, error) {
tempFiles := []string{}
newArgs := []string{}
for i := 0; i < len(args); i++ {
if (args[i] == "-m" || args[i] == "-p") && i+1 < len(args) {
pattern := fmt.Sprintf("casbin_temp_%s_*.conf", args[i])
tempFile, err := os.CreateTemp("", pattern)
if err != nil {
return nil, nil, fmt.Errorf("failed to create temp file: %v", err)
}
_, err = tempFile.WriteString(args[i+1])
if err != nil {
tempFile.Close()
return nil, nil, fmt.Errorf("failed to write to temp file: %v", err)
}
tempFile.Close()
tempFiles = append(tempFiles, tempFile.Name())
newArgs = append(newArgs, args[i], tempFile.Name())
i++
} else {
newArgs = append(newArgs, args[i])
}
}
return tempFiles, newArgs, nil
}
// RunCasbinCommand
// @Title RunCasbinCommand
// @Tag Enforcer API
// @Description Call Casbin CLI commands
// @Success 200 {object} controllers.Response The Response object
// @router /run-casbin-command [get]
func (c *ApiController) RunCasbinCommand() {
language := c.Input().Get("language")
argString := c.Input().Get("args")
if language == "" {
language = "go"
}
// use "casbin-go-cli" by default, can be also "casbin-java-cli", "casbin-node-cli", etc.
// the pre-built binary of "casbin-go-cli" can be found at: https://github.com/casbin/casbin-go-cli/releases
binaryName := fmt.Sprintf("casbin-%s-cli", language)
_, err := exec.LookPath(binaryName)
if err != nil {
c.ResponseError(fmt.Sprintf("executable file: %s not found in PATH", binaryName))
return
}
// RBAC model & policy example:
// https://door.casdoor.com/api/run-casbin-command?language=go&args=["enforce", "-m", "[request_definition]\nr = sub, obj, act\n\n[policy_definition]\np = sub, obj, act\n\n[role_definition]\ng = _, _\n\n[policy_effect]\ne = some(where (p.eft == allow))\n\n[matchers]\nm = g(r.sub, p.sub) %26%26 r.obj == p.obj %26%26 r.act == p.act", "-p", "p, alice, data1, read\np, bob, data2, write\np, data2_admin, data2, read\np, data2_admin, data2, write\ng, alice, data2_admin", "alice", "data1", "read"]
// Casbin CLI usage:
// https://github.com/jcasbin/casbin-java-cli?tab=readme-ov-file#get-started
var args []string
err = json.Unmarshal([]byte(argString), &args)
if err != nil {
c.ResponseError(err.Error())
return
}
tempFiles, processedArgs, err := processArgsToTempFiles(args)
defer func() {
for _, file := range tempFiles {
os.Remove(file)
}
}()
if err != nil {
c.ResponseError(err.Error())
return
}
command := exec.Command(binaryName, processedArgs...)
outputBytes, err := command.CombinedOutput()
if err != nil {
errorString := err.Error()
if outputBytes != nil {
output := string(outputBytes)
errorString = fmt.Sprintf("%s, error: %s", output, err.Error())
}
c.ResponseError(errorString)
return
}
output := string(outputBytes)
output = strings.TrimSuffix(output, "\n")
c.ResponseOk(output)
}

View File

@@ -70,15 +70,33 @@ func (c *ApiController) GetGroups() {
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} else { }
err = object.ExtendGroupsWithUsers(groups) groupsHaveChildrenMap, err := object.GetGroupsHaveChildrenMap(groups)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
}
for _, group := range groups {
_, ok := groupsHaveChildrenMap[group.Name]
if ok {
group.HaveChildren = true
} }
c.ResponseOk(groups, paginator.Nums()) parent, ok := groupsHaveChildrenMap[group.ParentId]
if ok {
group.ParentName = parent.DisplayName
}
} }
err = object.ExtendGroupsWithUsers(groups)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(groups, paginator.Nums())
} }
} }

View File

@@ -22,13 +22,6 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
) )
const (
MfaRecoveryCodesSession = "mfa_recovery_codes"
MfaCountryCodeSession = "mfa_country_code"
MfaDestSession = "mfa_dest"
MfaTotpSecretSession = "mfa_totp_secret"
)
// MfaSetupInitiate // MfaSetupInitiate
// @Title MfaSetupInitiate // @Title MfaSetupInitiate
// @Tag MFA API // @Tag MFA API
@@ -72,11 +65,6 @@ func (c *ApiController) MfaSetupInitiate() {
} }
recoveryCode := uuid.NewString() recoveryCode := uuid.NewString()
c.SetSession(MfaRecoveryCodesSession, recoveryCode)
if mfaType == object.TotpType {
c.SetSession(MfaTotpSecretSession, mfaProps.Secret)
}
mfaProps.RecoveryCodes = []string{recoveryCode} mfaProps.RecoveryCodes = []string{recoveryCode}
resp := mfaProps resp := mfaProps
@@ -94,6 +82,9 @@ func (c *ApiController) MfaSetupInitiate() {
func (c *ApiController) MfaSetupVerify() { func (c *ApiController) MfaSetupVerify() {
mfaType := c.Ctx.Request.Form.Get("mfaType") mfaType := c.Ctx.Request.Form.Get("mfaType")
passcode := c.Ctx.Request.Form.Get("passcode") passcode := c.Ctx.Request.Form.Get("passcode")
secret := c.Ctx.Request.Form.Get("secret")
dest := c.Ctx.Request.Form.Get("dest")
countryCode := c.Ctx.Request.Form.Get("countryCode")
if mfaType == "" || passcode == "" { if mfaType == "" || passcode == "" {
c.ResponseError("missing auth type or passcode") c.ResponseError("missing auth type or passcode")
@@ -104,32 +95,28 @@ func (c *ApiController) MfaSetupVerify() {
MfaType: mfaType, MfaType: mfaType,
} }
if mfaType == object.TotpType { if mfaType == object.TotpType {
secret := c.GetSession(MfaTotpSecretSession) if secret == "" {
if secret == nil {
c.ResponseError("totp secret is missing") c.ResponseError("totp secret is missing")
return return
} }
config.Secret = secret.(string) config.Secret = secret
} else if mfaType == object.SmsType { } else if mfaType == object.SmsType {
dest := c.GetSession(MfaDestSession) if dest == "" {
if dest == nil {
c.ResponseError("destination is missing") c.ResponseError("destination is missing")
return return
} }
config.Secret = dest.(string) config.Secret = dest
countryCode := c.GetSession(MfaCountryCodeSession) if countryCode == "" {
if countryCode == nil {
c.ResponseError("country code is missing") c.ResponseError("country code is missing")
return return
} }
config.CountryCode = countryCode.(string) config.CountryCode = countryCode
} else if mfaType == object.EmailType { } else if mfaType == object.EmailType {
dest := c.GetSession(MfaDestSession) if dest == "" {
if dest == nil {
c.ResponseError("destination is missing") c.ResponseError("destination is missing")
return return
} }
config.Secret = dest.(string) config.Secret = dest
} }
mfaUtil := object.GetMfaUtil(mfaType, config) mfaUtil := object.GetMfaUtil(mfaType, config)
@@ -159,6 +146,10 @@ func (c *ApiController) MfaSetupEnable() {
owner := c.Ctx.Request.Form.Get("owner") owner := c.Ctx.Request.Form.Get("owner")
name := c.Ctx.Request.Form.Get("name") name := c.Ctx.Request.Form.Get("name")
mfaType := c.Ctx.Request.Form.Get("mfaType") mfaType := c.Ctx.Request.Form.Get("mfaType")
secret := c.Ctx.Request.Form.Get("secret")
dest := c.Ctx.Request.Form.Get("dest")
countryCode := c.Ctx.Request.Form.Get("secret")
recoveryCodes := c.Ctx.Request.Form.Get("recoveryCodes")
user, err := object.GetUser(util.GetId(owner, name)) user, err := object.GetUser(util.GetId(owner, name))
if err != nil { if err != nil {
@@ -176,43 +167,39 @@ func (c *ApiController) MfaSetupEnable() {
} }
if mfaType == object.TotpType { if mfaType == object.TotpType {
secret := c.GetSession(MfaTotpSecretSession) if secret == "" {
if secret == nil {
c.ResponseError("totp secret is missing") c.ResponseError("totp secret is missing")
return return
} }
config.Secret = secret.(string) config.Secret = secret
} else if mfaType == object.EmailType { } else if mfaType == object.EmailType {
if user.Email == "" { if user.Email == "" {
dest := c.GetSession(MfaDestSession) if dest == "" {
if dest == nil {
c.ResponseError("destination is missing") c.ResponseError("destination is missing")
return return
} }
user.Email = dest.(string) user.Email = dest
} }
} else if mfaType == object.SmsType { } else if mfaType == object.SmsType {
if user.Phone == "" { if user.Phone == "" {
dest := c.GetSession(MfaDestSession) if dest == "" {
if dest == nil {
c.ResponseError("destination is missing") c.ResponseError("destination is missing")
return return
} }
user.Phone = dest.(string) user.Phone = dest
countryCode := c.GetSession(MfaCountryCodeSession) if countryCode == "" {
if countryCode == nil {
c.ResponseError("country code is missing") c.ResponseError("country code is missing")
return return
} }
user.CountryCode = countryCode.(string) user.CountryCode = countryCode
} }
} }
recoveryCodes := c.GetSession(MfaRecoveryCodesSession)
if recoveryCodes == nil { if recoveryCodes == "" {
c.ResponseError("recovery codes is missing") c.ResponseError("recovery codes is missing")
return return
} }
config.RecoveryCodes = []string{recoveryCodes.(string)} config.RecoveryCodes = []string{recoveryCodes}
mfaUtil := object.GetMfaUtil(mfaType, config) mfaUtil := object.GetMfaUtil(mfaType, config)
if mfaUtil == nil { if mfaUtil == nil {
@@ -226,14 +213,6 @@ func (c *ApiController) MfaSetupEnable() {
return return
} }
c.DelSession(MfaRecoveryCodesSession)
if mfaType == object.TotpType {
c.DelSession(MfaTotpSecretSession)
} else {
c.DelSession(MfaCountryCodeSession)
c.DelSession(MfaDestSession)
}
c.ResponseOk(http.StatusText(http.StatusOK)) c.ResponseOk(http.StatusText(http.StatusOK))
} }

View File

@@ -21,6 +21,11 @@ import (
) )
func (c *RootController) HandleScim() { func (c *RootController) HandleScim() {
_, ok := c.RequireAdmin()
if !ok {
return
}
path := c.Ctx.Request.URL.Path path := c.Ctx.Request.URL.Path
c.Ctx.Request.URL.Path = strings.TrimPrefix(path, "/scim") c.Ctx.Request.URL.Path = strings.TrimPrefix(path, "/scim")
scim.Server.ServeHTTP(c.Ctx.ResponseWriter, c.Ctx.Request) scim.Server.ServeHTTP(c.Ctx.ResponseWriter, c.Ctx.Request)

View File

@@ -93,7 +93,7 @@ func (c *ApiController) SendEmail() {
// when receiver is the reserved keyword: "TestSmtpServer", it means to test the SMTP server instead of sending a real Email // when receiver is the reserved keyword: "TestSmtpServer", it means to test the SMTP server instead of sending a real Email
if len(emailForm.Receivers) == 1 && emailForm.Receivers[0] == "TestSmtpServer" { if len(emailForm.Receivers) == 1 && emailForm.Receivers[0] == "TestSmtpServer" {
err = object.DailSmtpServer(provider) err = object.TestSmtpServer(provider)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return

View File

@@ -322,17 +322,22 @@ func (c *ApiController) IntrospectToken() {
} }
tokenTypeHint := c.Input().Get("token_type_hint") tokenTypeHint := c.Input().Get("token_type_hint")
token, err := object.GetTokenByTokenValue(tokenValue, tokenTypeHint) var token *object.Token
if err != nil { if tokenTypeHint != "" {
c.ResponseTokenError(err.Error()) token, err = object.GetTokenByTokenValue(tokenValue, tokenTypeHint)
return if err != nil {
} c.ResponseTokenError(err.Error())
if token == nil { return
c.Data["json"] = &object.IntrospectionResponse{Active: false} }
c.ServeJSON() if token == nil {
return c.Data["json"] = &object.IntrospectionResponse{Active: false}
c.ServeJSON()
return
}
} }
var introspectionResponse object.IntrospectionResponse
if application.TokenFormat == "JWT-Standard" { if application.TokenFormat == "JWT-Standard" {
jwtToken, err := object.ParseStandardJwtTokenByApplication(tokenValue, application) jwtToken, err := object.ParseStandardJwtTokenByApplication(tokenValue, application)
if err != nil || jwtToken.Valid() != nil { if err != nil || jwtToken.Valid() != nil {
@@ -344,12 +349,37 @@ func (c *ApiController) IntrospectToken() {
return return
} }
c.Data["json"] = &object.IntrospectionResponse{ introspectionResponse = object.IntrospectionResponse{
Active: true, Active: true,
Scope: jwtToken.Scope, Scope: jwtToken.Scope,
ClientId: clientId, ClientId: clientId,
Username: token.User, Username: jwtToken.Name,
TokenType: token.TokenType, TokenType: jwtToken.TokenType,
Exp: jwtToken.ExpiresAt.Unix(),
Iat: jwtToken.IssuedAt.Unix(),
Nbf: jwtToken.NotBefore.Unix(),
Sub: jwtToken.Subject,
Aud: jwtToken.Audience,
Iss: jwtToken.Issuer,
Jti: jwtToken.ID,
}
} else {
jwtToken, err := object.ParseJwtTokenByApplication(tokenValue, application)
if err != nil || jwtToken.Valid() != nil {
// and token revoked case. but we not implement
// TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs.
// refs: https://tools.ietf.org/html/rfc7009
c.Data["json"] = &object.IntrospectionResponse{Active: false}
c.ServeJSON()
return
}
introspectionResponse = object.IntrospectionResponse{
Active: true,
Scope: jwtToken.Scope,
ClientId: clientId,
Username: jwtToken.Name,
TokenType: jwtToken.TokenType,
Exp: jwtToken.ExpiresAt.Unix(), Exp: jwtToken.ExpiresAt.Unix(),
Iat: jwtToken.IssuedAt.Unix(), Iat: jwtToken.IssuedAt.Unix(),
Nbf: jwtToken.NotBefore.Unix(), Nbf: jwtToken.NotBefore.Unix(),
@@ -358,33 +388,22 @@ func (c *ApiController) IntrospectToken() {
Iss: jwtToken.Issuer, Iss: jwtToken.Issuer,
Jti: jwtToken.ID, Jti: jwtToken.ID,
} }
c.ServeJSON()
return
} }
jwtToken, err := object.ParseJwtTokenByApplication(tokenValue, application) if tokenTypeHint == "" {
if err != nil || jwtToken.Valid() != nil { token, err = object.GetTokenByTokenValue(tokenValue, introspectionResponse.TokenType)
// and token revoked case. but we not implement if err != nil {
// TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs. c.ResponseTokenError(err.Error())
// refs: https://tools.ietf.org/html/rfc7009 return
c.Data["json"] = &object.IntrospectionResponse{Active: false} }
c.ServeJSON() if token == nil {
return c.Data["json"] = &object.IntrospectionResponse{Active: false}
c.ServeJSON()
return
}
} }
introspectionResponse.TokenType = token.TokenType
c.Data["json"] = &object.IntrospectionResponse{ c.Data["json"] = introspectionResponse
Active: true,
Scope: jwtToken.Scope,
ClientId: clientId,
Username: token.User,
TokenType: token.TokenType,
Exp: jwtToken.ExpiresAt.Unix(),
Iat: jwtToken.IssuedAt.Unix(),
Nbf: jwtToken.NotBefore.Unix(),
Sub: jwtToken.Subject,
Aud: jwtToken.Audience,
Iss: jwtToken.Issuer,
Jti: jwtToken.ID,
}
c.ServeJSON() c.ServeJSON()
} }

View File

@@ -353,13 +353,7 @@ func (c *ApiController) AddUser() {
return return
} }
count, err := object.GetUserCount("", "", "", "") if err := checkQuotaForUser(); err != nil {
if err != nil {
c.ResponseError(err.Error())
return
}
if err := checkQuotaForUser(int(count)); err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
@@ -475,6 +469,16 @@ func (c *ApiController) SetPassword() {
userId := util.GetId(userOwner, userName) userId := util.GetId(userOwner, userName)
user, err := object.GetUser(userId)
if err != nil {
c.ResponseError(err.Error())
return
}
if user == nil {
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), userId))
return
}
requestUserId := c.GetSessionUsername() requestUserId := c.GetSessionUsername()
if requestUserId == "" && code == "" { if requestUserId == "" && code == "" {
c.ResponseError(c.T("general:Please login first"), "Please login first") c.ResponseError(c.T("general:Please login first"), "Please login first")
@@ -518,7 +522,11 @@ func (c *ApiController) SetPassword() {
} }
} }
} else if code == "" { } else if code == "" {
err = object.CheckPassword(targetUser, oldPassword, c.GetAcceptLanguage()) if user.Ldap == "" {
err = object.CheckPassword(targetUser, oldPassword, c.GetAcceptLanguage())
} else {
err = object.CheckLdapUserPassword(targetUser, oldPassword, c.GetAcceptLanguage())
}
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
@@ -561,8 +569,18 @@ func (c *ApiController) SetPassword() {
targetUser.Password = newPassword targetUser.Password = newPassword
targetUser.UpdateUserPassword(organization) targetUser.UpdateUserPassword(organization)
targetUser.NeedUpdatePassword = false targetUser.NeedUpdatePassword = false
targetUser.LastChangePasswordTime = util.GetCurrentTime()
if user.Ldap == "" {
_, err = object.UpdateUser(userId, targetUser, []string{"password", "need_update_password", "password_type", "last_change_password_time"}, false)
} else {
if isAdmin {
err = object.ResetLdapPassword(targetUser, "", newPassword, c.GetAcceptLanguage())
} else {
err = object.ResetLdapPassword(targetUser, oldPassword, newPassword, c.GetAcceptLanguage())
}
}
_, err = object.UpdateUser(userId, targetUser, []string{"password", "need_update_password", "password_type"}, false)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return

View File

@@ -294,12 +294,18 @@ func checkQuotaForProvider(count int) error {
return nil return nil
} }
func checkQuotaForUser(count int) error { func checkQuotaForUser() error {
quota := conf.GetConfigQuota().User quota := conf.GetConfigQuota().User
if quota == -1 { if quota == -1 {
return nil return nil
} }
if count >= quota {
count, err := object.GetUserCount("", "", "", "")
if err != nil {
return err
}
if int(count) >= quota {
return fmt.Errorf("user quota is exceeded") return fmt.Errorf("user quota is exceeded")
} }
return nil return nil

View File

@@ -246,8 +246,6 @@ func (c *ApiController) SendVerificationCode() {
if user != nil && util.GetMaskedEmail(mfaProps.Secret) == vform.Dest { if user != nil && util.GetMaskedEmail(mfaProps.Secret) == vform.Dest {
vform.Dest = mfaProps.Secret vform.Dest = mfaProps.Secret
} }
} else if vform.Method == MfaSetupVerification {
c.SetSession(MfaDestSession, vform.Dest)
} }
provider, err = application.GetEmailProvider(vform.Method) provider, err = application.GetEmailProvider(vform.Method)
@@ -282,11 +280,6 @@ func (c *ApiController) SendVerificationCode() {
vform.CountryCode = user.GetCountryCode(vform.CountryCode) vform.CountryCode = user.GetCountryCode(vform.CountryCode)
} }
} }
if vform.Method == MfaSetupVerification {
c.SetSession(MfaCountryCodeSession, vform.CountryCode)
c.SetSession(MfaDestSession, vform.Dest)
}
} else if vform.Method == MfaAuthVerification { } else if vform.Method == MfaAuthVerification {
mfaProps := user.GetPreferredMfaProps(false) mfaProps := user.GetPreferredMfaProps(false)
if user != nil && util.GetMaskedPhone(mfaProps.Secret) == vform.Dest { if user != nil && util.GetMaskedPhone(mfaProps.Secret) == vform.Dest {
@@ -294,6 +287,7 @@ func (c *ApiController) SendVerificationCode() {
} }
vform.CountryCode = mfaProps.CountryCode vform.CountryCode = mfaProps.CountryCode
vform.CountryCode = user.GetCountryCode(vform.CountryCode)
} }
provider, err = application.GetSmsProvider(vform.Method, vform.CountryCode) provider, err = application.GetSmsProvider(vform.Method, vform.CountryCode)

View File

@@ -16,7 +16,9 @@ package email
import ( import (
"crypto/tls" "crypto/tls"
"strings"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/gomail/v2" "github.com/casdoor/gomail/v2"
) )
@@ -33,6 +35,13 @@ func NewSmtpEmailProvider(userName string, password string, host string, port in
dialer.SSL = !disableSsl dialer.SSL = !disableSsl
if strings.HasSuffix(host, ".amazonaws.com") {
socks5Proxy := conf.GetConfigString("socks5Proxy")
if socks5Proxy != "" {
dialer.SetSocks5Proxy(socks5Proxy)
}
}
return &SmtpEmailProvider{Dialer: dialer} return &SmtpEmailProvider{Dialer: dialer}
} }

View File

@@ -37,13 +37,14 @@ type AuthForm struct {
Region string `json:"region"` Region string `json:"region"`
InvitationCode string `json:"invitationCode"` InvitationCode string `json:"invitationCode"`
Application string `json:"application"` Application string `json:"application"`
ClientId string `json:"clientId"` ClientId string `json:"clientId"`
Provider string `json:"provider"` Provider string `json:"provider"`
Code string `json:"code"` ProviderBack string `json:"providerBack"`
State string `json:"state"` Code string `json:"code"`
RedirectUri string `json:"redirectUri"` State string `json:"state"`
Method string `json:"method"` RedirectUri string `json:"redirectUri"`
Method string `json:"method"`
EmailCode string `json:"emailCode"` EmailCode string `json:"emailCode"`
PhoneCode string `json:"phoneCode"` PhoneCode string `json:"phoneCode"`

11
go.mod
View File

@@ -9,10 +9,10 @@ require (
github.com/beego/beego v1.12.12 github.com/beego/beego v1.12.12
github.com/beevik/etree v1.1.0 github.com/beevik/etree v1.1.0
github.com/casbin/casbin/v2 v2.77.2 github.com/casbin/casbin/v2 v2.77.2
github.com/casdoor/go-sms-sender v0.24.0 github.com/casdoor/go-sms-sender v0.25.0
github.com/casdoor/gomail/v2 v2.0.1 github.com/casdoor/gomail/v2 v2.1.0
github.com/casdoor/ldapserver v1.2.0 github.com/casdoor/ldapserver v1.2.0
github.com/casdoor/notify v0.45.0 github.com/casdoor/notify v1.0.0
github.com/casdoor/oss v1.8.0 github.com/casdoor/oss v1.8.0
github.com/casdoor/xorm-adapter/v3 v3.1.0 github.com/casdoor/xorm-adapter/v3 v3.1.0
github.com/casvisor/casvisor-go-sdk v1.4.0 github.com/casvisor/casvisor-go-sdk v1.4.0
@@ -60,9 +60,10 @@ require (
github.com/xorm-io/core v0.7.4 github.com/xorm-io/core v0.7.4
github.com/xorm-io/xorm v1.1.6 github.com/xorm-io/xorm v1.1.6
github.com/yusufpapurcu/wmi v1.2.2 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/crypto v0.21.0 golang.org/x/crypto v0.32.0
golang.org/x/net v0.21.0 golang.org/x/net v0.34.0
golang.org/x/oauth2 v0.17.0 golang.org/x/oauth2 v0.17.0
golang.org/x/text v0.21.0
google.golang.org/api v0.150.0 google.golang.org/api v0.150.0
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 gopkg.in/square/go-jose.v2 v2.6.0

45
go.sum
View File

@@ -1087,14 +1087,14 @@ github.com/casdoor/casdoor-go-sdk v0.50.0 h1:bUYbz/MzJuWfLKJbJM0+U0YpYewAur+THp5
github.com/casdoor/casdoor-go-sdk v0.50.0/go.mod h1:cMnkCQJgMYpgAlgEx8reSt1AVaDIQLcJ1zk5pzBaz+4= github.com/casdoor/casdoor-go-sdk v0.50.0/go.mod h1:cMnkCQJgMYpgAlgEx8reSt1AVaDIQLcJ1zk5pzBaz+4=
github.com/casdoor/go-reddit/v2 v2.1.0 h1:kIbfdJ7AA7H0uTQ8s0q4GGZqSS5V9wVE74RrXyD9XPs= github.com/casdoor/go-reddit/v2 v2.1.0 h1:kIbfdJ7AA7H0uTQ8s0q4GGZqSS5V9wVE74RrXyD9XPs=
github.com/casdoor/go-reddit/v2 v2.1.0/go.mod h1:eagkvwlZ4Hcsuc/uQsLHYEulz5jN65SVSwV/AIE7zsc= github.com/casdoor/go-reddit/v2 v2.1.0/go.mod h1:eagkvwlZ4Hcsuc/uQsLHYEulz5jN65SVSwV/AIE7zsc=
github.com/casdoor/go-sms-sender v0.24.0 h1:LNLsce3EG/87I3JS6UiajF3LlQmdIiCgebEu0IE4wSM= github.com/casdoor/go-sms-sender v0.25.0 h1:eF4cOCSbjVg7+0uLlJQnna/FQ0BWW+Fp/x4cXhzQu1Y=
github.com/casdoor/go-sms-sender v0.24.0/go.mod h1:bOm4H8/YfJmEHjBatEVQFOnAf0OOn1B0Wi5B7zDhws0= github.com/casdoor/go-sms-sender v0.25.0/go.mod h1:bOm4H8/YfJmEHjBatEVQFOnAf0OOn1B0Wi5B7zDhws0=
github.com/casdoor/gomail/v2 v2.0.1 h1:J+FG6x80s9e5lBHUn8Sv0Y56mud34KiWih5YdmudR/w= github.com/casdoor/gomail/v2 v2.1.0 h1:ua97E3CARnF1Ik8ga/Drz9uGZfaElXJumFexiErWUxM=
github.com/casdoor/gomail/v2 v2.0.1/go.mod h1:VnGPslEAtpix5FjHisR/WKB1qvZDBaujbikxDe9d+2Q= github.com/casdoor/gomail/v2 v2.1.0/go.mod h1:GFzOD9RhY0nODiiPaQiOa6DfoKtmO9aTesu5qrp26OI=
github.com/casdoor/ldapserver v1.2.0 h1:HdSYe+ULU6z9K+2BqgTrJKQRR4//ERAXB64ttOun6Ow= github.com/casdoor/ldapserver v1.2.0 h1:HdSYe+ULU6z9K+2BqgTrJKQRR4//ERAXB64ttOun6Ow=
github.com/casdoor/ldapserver v1.2.0/go.mod h1:VwYU2vqQ2pA8sa00PRekH71R2XmgfzMKhmp1XrrDu2s= github.com/casdoor/ldapserver v1.2.0/go.mod h1:VwYU2vqQ2pA8sa00PRekH71R2XmgfzMKhmp1XrrDu2s=
github.com/casdoor/notify v0.45.0 h1:OlaFvcQFjGOgA4mRx07M8AH1gvb5xNo21mcqrVGlLgk= github.com/casdoor/notify v1.0.0 h1:oldsaaQFPrlufm/OA314z8DwFVE1Tc9Gt1z4ptRHhXw=
github.com/casdoor/notify v0.45.0/go.mod h1:wNHQu0tiDROMBIvz0j3Om3Lhd5yZ+AIfnFb8MYb8OLQ= github.com/casdoor/notify v1.0.0/go.mod h1:wNHQu0tiDROMBIvz0j3Om3Lhd5yZ+AIfnFb8MYb8OLQ=
github.com/casdoor/oss v1.8.0 h1:uuyKhDIp7ydOtV4lpqhAY23Ban2Ln8La8+QT36CwylM= github.com/casdoor/oss v1.8.0 h1:uuyKhDIp7ydOtV4lpqhAY23Ban2Ln8La8+QT36CwylM=
github.com/casdoor/oss v1.8.0/go.mod h1:uaqO7KBI2lnZcnB8rF7O6C2bN7llIbfC5Ql8ex1yR1U= github.com/casdoor/oss v1.8.0/go.mod h1:uaqO7KBI2lnZcnB8rF7O6C2bN7llIbfC5Ql8ex1yR1U=
github.com/casdoor/xorm-adapter/v3 v3.1.0 h1:NodWayRtSLVSeCvL9H3Hc61k0G17KhV9IymTCNfh3kk= github.com/casdoor/xorm-adapter/v3 v3.1.0 h1:NodWayRtSLVSeCvL9H3Hc61k0G17KhV9IymTCNfh3kk=
@@ -2163,8 +2163,10 @@ golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20181106170214-d68db9428509/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20181106170214-d68db9428509/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -2230,8 +2232,10 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20171115151908-9dfe39835686/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20171115151908-9dfe39835686/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -2319,8 +2323,10 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -2375,8 +2381,11 @@ golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -2503,8 +2512,11 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -2524,8 +2536,10 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -2546,8 +2560,10 @@ golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -2634,8 +2650,9 @@ golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM= golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -1,167 +1,167 @@
{ {
"account": { "account": {
"Failed to add user": "Failed to add user", "Failed to add user": "عدم موفقیت در افزودن کاربر",
"Get init score failed, error: %w": "Get init score failed, error: %w", "Get init score failed, error: %w": "عدم موفقیت در دریافت امتیاز اولیه، خطا: %w",
"Please sign out first": "Please sign out first", "Please sign out first": "لطفاً ابتدا خارج شوید",
"The application does not allow to sign up new account": "The application does not allow to sign up new account" "The application does not allow to sign up new account": "برنامه اجازه ثبت‌نام حساب جدید را نمی‌دهد"
}, },
"auth": { "auth": {
"Challenge method should be S256": "Challenge method should be S256", "Challenge method should be S256": "روش چالش باید S256 باشد",
"Failed to create user, user information is invalid: %s": "Failed to create user, user information is invalid: %s", "Failed to create user, user information is invalid: %s": "عدم موفقیت در ایجاد کاربر، اطلاعات کاربر نامعتبر است: %s",
"Failed to login in: %s": "Failed to login in: %s", "Failed to login in: %s": "عدم موفقیت در ورود: %s",
"Invalid token": "Invalid token", "Invalid token": "توکن نامعتبر",
"State expected: %s, but got: %s": "State expected: %s, but got: %s", "State expected: %s, but got: %s": "وضعیت مورد انتظار: %s، اما دریافت شد: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up", "The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "حساب برای ارائه‌دهنده: %s و نام کاربری: %s (%s) وجود ندارد و مجاز به ثبت‌نام به‌عنوان حساب جدید از طریق %%s نیست، لطفاً از روش دیگری برای ثبت‌نام استفاده کنید",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support", "The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "حساب برای ارائه‌دهنده: %s و نام کاربری: %s (%s) وجود ندارد و مجاز به ثبت‌نام به‌عنوان حساب جدید نیست، لطفاً با پشتیبانی IT خود تماس بگیرید",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)", "The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "حساب برای ارائه‌دهنده: %s و نام کاربری: %s (%s) در حال حاضر به حساب دیگری مرتبط است: %s (%s)",
"The application: %s does not exist": "The application: %s does not exist", "The application: %s does not exist": "برنامه: %s وجود ندارد",
"The login method: login with LDAP is not enabled for the application": "The login method: login with LDAP is not enabled for the application", "The login method: login with LDAP is not enabled for the application": "روش ورود: ورود با LDAP برای برنامه فعال نیست",
"The login method: login with SMS is not enabled for the application": "The login method: login with SMS is not enabled for the application", "The login method: login with SMS is not enabled for the application": "روش ورود: ورود با پیامک برای برنامه فعال نیست",
"The login method: login with email is not enabled for the application": "The login method: login with email is not enabled for the application", "The login method: login with email is not enabled for the application": "روش ورود: ورود با ایمیل برای برنامه فعال نیست",
"The login method: login with face is not enabled for the application": "The login method: login with face is not enabled for the application", "The login method: login with face is not enabled for the application": "روش ورود: ورود با چهره برای برنامه فعال نیست",
"The login method: login with password is not enabled for the application": "The login method: login with password is not enabled for the application", "The login method: login with password is not enabled for the application": "روش ورود: ورود با رمز عبور برای برنامه فعال نیست",
"The organization: %s does not exist": "The organization: %s does not exist", "The organization: %s does not exist": "سازمان: %s وجود ندارد",
"The provider: %s is not enabled for the application": "The provider: %s is not enabled for the application", "The provider: %s is not enabled for the application": "ارائه‌دهنده: %s برای برنامه فعال نیست",
"Unauthorized operation": "Unauthorized operation", "Unauthorized operation": "عملیات غیرمجاز",
"Unknown authentication type (not password or provider), form = %s": "Unknown authentication type (not password or provider), form = %s", "Unknown authentication type (not password or provider), form = %s": "نوع احراز هویت ناشناخته (نه رمز عبور و نه ارائه‌دهنده)، فرم = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags", "User's tag: %s is not listed in the application's tags": "برچسب کاربر: %s در برچسب‌های برنامه فهرست نشده است",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing" "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "کاربر پرداختی %s اشتراک فعال یا در انتظار ندارد و برنامه: %s قیمت‌گذاری پیش‌فرض ندارد"
}, },
"cas": { "cas": {
"Service %s and %s do not match": "Service %s and %s do not match" "Service %s and %s do not match": "سرویس %s و %s مطابقت ندارند"
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation cannot be blank", "Affiliation cannot be blank": "وابستگی نمی‌تواند خالی باشد",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules", "Default code does not match the code's matching rules": "کد پیش‌فرض با قوانین تطبیق کد مطابقت ندارد",
"DisplayName cannot be blank": "DisplayName cannot be blank", "DisplayName cannot be blank": "نام نمایشی نمی‌تواند خالی باشد",
"DisplayName is not valid real name": "DisplayName is not valid real name", "DisplayName is not valid real name": "نام نمایشی یک نام واقعی معتبر نیست",
"Email already exists": "Email already exists", "Email already exists": "ایمیل قبلاً وجود دارد",
"Email cannot be empty": "Email cannot be empty", "Email cannot be empty": "ایمیل نمی‌تواند خالی باشد",
"Email is invalid": "Email is invalid", "Email is invalid": "ایمیل نامعتبر است",
"Empty username.": "Empty username.", "Empty username.": "نام کاربری خالی است.",
"Face data does not exist, cannot log in": "Face data does not exist, cannot log in", "Face data does not exist, cannot log in": "داده‌های چهره وجود ندارد، نمی‌توان وارد شد",
"Face data mismatch": "Face data mismatch", "Face data mismatch": "عدم تطابق داده‌های چهره",
"FirstName cannot be blank": "FirstName cannot be blank", "FirstName cannot be blank": "نام نمی‌تواند خالی باشد",
"Invitation code cannot be blank": "Invitation code cannot be blank", "Invitation code cannot be blank": "کد دعوت نمی‌تواند خالی باشد",
"Invitation code exhausted": "Invitation code exhausted", "Invitation code exhausted": "کد دعوت استفاده شده است",
"Invitation code is invalid": "Invitation code is invalid", "Invitation code is invalid": "کد دعوت نامعتبر است",
"Invitation code suspended": "Invitation code suspended", "Invitation code suspended": "کد دعوت معلق است",
"LDAP user name or password incorrect": "LDAP user name or password incorrect", "LDAP user name or password incorrect": "نام کاربری یا رمز عبور LDAP نادرست است",
"LastName cannot be blank": "LastName cannot be blank", "LastName cannot be blank": "نام خانوادگی نمی‌تواند خالی باشد",
"Multiple accounts with same uid, please check your ldap server": "Multiple accounts with same uid, please check your ldap server", "Multiple accounts with same uid, please check your ldap server": "چندین حساب با uid یکسان، لطفاً سرور LDAP خود را بررسی کنید",
"Organization does not exist": "Organization does not exist", "Organization does not exist": "سازمان وجود ندارد",
"Phone already exists": "Phone already exists", "Phone already exists": "تلفن قبلاً وجود دارد",
"Phone cannot be empty": "Phone cannot be empty", "Phone cannot be empty": "تلفن نمی‌تواند خالی باشد",
"Phone number is invalid": "Phone number is invalid", "Phone number is invalid": "شماره تلفن نامعتبر است",
"Please register using the email corresponding to the invitation code": "Please register using the email corresponding to the invitation code", "Please register using the email corresponding to the invitation code": "لطفاً با استفاده از ایمیل مربوط به کد دعوت ثبت‌نام کنید",
"Please register using the phone corresponding to the invitation code": "Please register using the phone corresponding to the invitation code", "Please register using the phone corresponding to the invitation code": "لطفاً با استفاده از تلفن مربوط به کد دعوت ثبت‌نام کنید",
"Please register using the username corresponding to the invitation code": "Please register using the username corresponding to the invitation code", "Please register using the username corresponding to the invitation code": "لطفاً با استفاده از نام کاربری مربوط به کد دعوت ثبت‌نام کنید",
"Session outdated, please login again": "Session outdated, please login again", "Session outdated, please login again": "جلسه منقضی شده است، لطفاً دوباره وارد شوید",
"The invitation code has already been used": "The invitation code has already been used", "The invitation code has already been used": "کد دعوت قبلاً استفاده شده است",
"The user is forbidden to sign in, please contact the administrator": "The user is forbidden to sign in, please contact the administrator", "The user is forbidden to sign in, please contact the administrator": "ورود کاربر ممنوع است، لطفاً با مدیر تماس بگیرید",
"The user: %s doesn't exist in LDAP server": "The user: %s doesn't exist in LDAP server", "The user: %s doesn't exist in LDAP server": "کاربر: %s در سرور LDAP وجود ندارد",
"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.": "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.", "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.": "نام کاربری فقط می‌تواند حاوی کاراکترهای الفبایی عددی، زیرخط یا خط تیره باشد، نمی‌تواند خط تیره یا زیرخط متوالی داشته باشد، و نمی‌تواند با خط تیره یا زیرخط شروع یا پایان یابد.",
"The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex": "The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex", "The value \"%s\" for account field \"%s\" doesn't match the account item regex": "مقدار \"%s\" برای فیلد حساب \"%s\" با عبارت منظم مورد حساب مطابقت ندارد",
"The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"": "The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"", "The value \"%s\" for signup field \"%s\" doesn't match the signup item regex of the application \"%s\"": "مقدار \"%s\" برای فیلد ثبت‌نام \"%s\" با عبارت منظم مورد ثبت‌نام برنامه \"%s\" مطابقت ندارد",
"Username already exists": "Username already exists", "Username already exists": "نام کاربری قبلاً وجود دارد",
"Username cannot be an email address": "Username cannot be an email address", "Username cannot be an email address": "نام کاربری نمی‌تواند یک آدرس ایمیل باشد",
"Username cannot contain white spaces": "Username cannot contain white spaces", "Username cannot contain white spaces": "نام کاربری نمی‌تواند حاوی فاصله باشد",
"Username cannot start with a digit": "Username cannot start with a digit", "Username cannot start with a digit": "نام کاربری نمی‌تواند با یک رقم شروع شود",
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).", "Username is too long (maximum is 39 characters).": "نام کاربری بیش از حد طولانی است (حداکثر ۳۹ کاراکتر).",
"Username must have at least 2 characters": "Username must have at least 2 characters", "Username must have at least 2 characters": "نام کاربری باید حداقل ۲ کاراکتر داشته باشد",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again", "You have entered the wrong password or code too many times, please wait for %d minutes and try again": "شما رمز عبور یا کد اشتباه را بیش از حد وارد کرده‌اید، لطفاً %d دقیقه صبر کنید و دوباره تلاش کنید",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone", "Your region is not allow to signup by phone": "منطقه شما اجازه ثبت‌نام با تلفن را ندارد",
"password or code is incorrect": "password or code is incorrect", "password or code is incorrect": "رمز عبور یا کد نادرست است",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances", "password or code is incorrect, you have %d remaining chances": "رمز عبور یا کد نادرست است، شما %d فرصت باقی‌مانده دارید",
"unsupported password type: %s": "unsupported password type: %s" "unsupported password type: %s": "نوع رمز عبور پشتیبانی نشده: %s"
}, },
"general": { "general": {
"Missing parameter": "Missing parameter", "Missing parameter": "پارامتر گمشده",
"Please login first": "Please login first", "Please login first": "لطفاً ابتدا وارد شوید",
"The organization: %s should have one application at least": "The organization: %s should have one application at least", "The organization: %s should have one application at least": "سازمان: %s باید حداقل یک برنامه داشته باشد",
"The user: %s doesn't exist": "The user: %s doesn't exist", "The user: %s doesn't exist": "کاربر: %s وجود ندارد",
"don't support captchaProvider: ": "don't support captchaProvider: ", "don't support captchaProvider: ": "از captchaProvider پشتیبانی نمی‌شود: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode", "this operation is not allowed in demo mode": "این عملیات در حالت دمو مجاز نیست",
"this operation requires administrator to perform": "this operation requires administrator to perform" "this operation requires administrator to perform": "این عملیات نیاز به مدیر برای انجام دارد"
}, },
"ldap": { "ldap": {
"Ldap server exist": "Ldap server exist" "Ldap server exist": "سرور LDAP وجود دارد"
}, },
"link": { "link": {
"Please link first": "Please link first", "Please link first": "لطفاً ابتدا پیوند دهید",
"This application has no providers": "This application has no providers", "This application has no providers": "این برنامه ارائه‌دهنده‌ای ندارد",
"This application has no providers of type": "This application has no providers of type", "This application has no providers of type": "این برنامه ارائه‌دهنده‌ای از نوع ندارد",
"This provider can't be unlinked": "This provider can't be unlinked", "This provider can't be unlinked": "این ارائه‌دهنده نمی‌تواند لغو پیوند شود",
"You are not the global admin, you can't unlink other users": "You are not the global admin, you can't unlink other users", "You are not the global admin, you can't unlink other users": "شما مدیر جهانی نیستید، نمی‌توانید کاربران دیگر را لغو پیوند کنید",
"You can't unlink yourself, you are not a member of any application": "You can't unlink yourself, you are not a member of any application" "You can't unlink yourself, you are not a member of any application": "شما نمی‌توانید خودتان را لغو پیوند کنید، شما عضو هیچ برنامه‌ای نیستید"
}, },
"organization": { "organization": {
"Only admin can modify the %s.": "Only admin can modify the %s.", "Only admin can modify the %s.": "فقط مدیر می‌تواند %s را تغییر دهد.",
"The %s is immutable.": "The %s is immutable.", "The %s is immutable.": "%s غیرقابل تغییر است.",
"Unknown modify rule %s.": "Unknown modify rule %s." "Unknown modify rule %s.": "قانون تغییر ناشناخته %s."
}, },
"permission": { "permission": {
"The permission: \\\"%s\\\" doesn't exist": "The permission: \\\"%s\\\" doesn't exist" "The permission: \"%s\" doesn't exist": "مجوز: \"%s\" وجود ندارد"
}, },
"provider": { "provider": {
"Invalid application id": "Invalid application id", "Invalid application id": "شناسه برنامه نامعتبر",
"the provider: %s does not exist": "the provider: %s does not exist" "the provider: %s does not exist": "ارائه‌دهنده: %s وجود ندارد"
}, },
"resource": { "resource": {
"User is nil for tag: avatar": "User is nil for tag: avatar", "User is nil for tag: avatar": "کاربر برای برچسب: آواتار تهی است",
"Username or fullFilePath is empty: username = %s, fullFilePath = %s": "Username or fullFilePath is empty: username = %s, fullFilePath = %s" "Username or fullFilePath is empty: username = %s, fullFilePath = %s": "نام کاربری یا مسیر کامل فایل خالی است: نام کاربری = %s، مسیر کامل فایل = %s"
}, },
"saml": { "saml": {
"Application %s not found": "Application %s not found" "Application %s not found": "برنامه %s یافت نشد"
}, },
"saml_sp": { "saml_sp": {
"provider %s's category is not SAML": "provider %s's category is not SAML" "provider %s's category is not SAML": "دسته‌بندی ارائه‌دهنده %s SAML نیست"
}, },
"service": { "service": {
"Empty parameters for emailForm: %v": "Empty parameters for emailForm: %v", "Empty parameters for emailForm: %v": "پارامترهای خالی برای emailForm: %v",
"Invalid Email receivers: %s": "Invalid Email receivers: %s", "Invalid Email receivers: %s": "گیرندگان ایمیل نامعتبر: %s",
"Invalid phone receivers: %s": "Invalid phone receivers: %s" "Invalid phone receivers: %s": "گیرندگان تلفن نامعتبر: %s"
}, },
"storage": { "storage": {
"The objectKey: %s is not allowed": "The objectKey: %s is not allowed", "The objectKey: %s is not allowed": "objectKey: %s مجاز نیست",
"The provider type: %s is not supported": "The provider type: %s is not supported" "The provider type: %s is not supported": "نوع ارائه‌دهنده: %s پشتیبانی نمی‌شود"
}, },
"token": { "token": {
"Grant_type: %s is not supported in this application": "Grant_type: %s is not supported in this application", "Grant_type: %s is not supported in this application": "grant_type: %s در این برنامه پشتیبانی نمی‌شود",
"Invalid application or wrong clientSecret": "Invalid application or wrong clientSecret", "Invalid application or wrong clientSecret": "برنامه نامعتبر یا clientSecret نادرست",
"Invalid client_id": "Invalid client_id", "Invalid client_id": "client_id نامعتبر",
"Redirect URI: %s doesn't exist in the allowed Redirect URI list": "Redirect URI: %s doesn't exist in the allowed Redirect URI list", "Redirect URI: %s doesn't exist in the allowed Redirect URI list": "آدرس بازگشت: %s در لیست آدرس‌های بازگشت مجاز وجود ندارد",
"Token not found, invalid accessToken": "Token not found, invalid accessToken" "Token not found, invalid accessToken": "توکن یافت نشد، accessToken نامعتبر"
}, },
"user": { "user": {
"Display name cannot be empty": "Display name cannot be empty", "Display name cannot be empty": "نام نمایشی نمی‌تواند خالی باشد",
"New password cannot contain blank space.": "New password cannot contain blank space." "New password cannot contain blank space.": "رمز عبور جدید نمی‌تواند حاوی فاصله خالی باشد."
}, },
"user_upload": { "user_upload": {
"Failed to import users": "Failed to import users" "Failed to import users": "عدم موفقیت در وارد کردن کاربران"
}, },
"util": { "util": {
"No application is found for userId: %s": "No application is found for userId: %s", "No application is found for userId: %s": "هیچ برنامه‌ای برای userId: %s یافت نشد",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s", "No provider for category: %s is found for application: %s": "هیچ ارائه‌دهنده‌ای برای دسته‌بندی: %s برای برنامه: %s یافت نشد",
"The provider: %s is not found": "The provider: %s is not found" "The provider: %s is not found": "ارائه‌دهنده: %s یافت نشد"
}, },
"verification": { "verification": {
"Invalid captcha provider.": "Invalid captcha provider.", "Invalid captcha provider.": "ارائه‌دهنده کپچا نامعتبر.",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s", "Phone number is invalid in your region %s": "شماره تلفن در منطقه شما نامعتبر است %s",
"The verification code has not been sent yet!": "The verification code has not been sent yet!", "The verification code has not been sent yet!": "کد تأیید هنوز ارسال نشده است!",
"The verification code has not been sent yet, or has already been used!": "The verification code has not been sent yet, or has already been used!", "The verification code has not been sent yet, or has already been used!": "کد تأیید هنوز ارسال نشده است، یا قبلاً استفاده شده است!",
"Turing test failed.": "Turing test failed.", "Turing test failed.": "تست تورینگ ناموفق بود.",
"Unable to get the email modify rule.": "Unable to get the email modify rule.", "Unable to get the email modify rule.": "عدم توانایی در دریافت قانون تغییر ایمیل.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.", "Unable to get the phone modify rule.": "عدم توانایی در دریافت قانون تغییر تلفن.",
"Unknown type": "Unknown type", "Unknown type": "نوع ناشناخته",
"Wrong verification code!": "Wrong verification code!", "Wrong verification code!": "کد تأیید اشتباه!",
"You should verify your code in %d min!": "You should verify your code in %d min!", "You should verify your code in %d min!": "شما باید کد خود را در %d دقیقه تأیید کنید!",
"please add a SMS provider to the \\\"Providers\\\" list for the application: %s": "please add a SMS provider to the \\\"Providers\\\" list for the application: %s", "please add a SMS provider to the \"Providers\" list for the application: %s": "لطفاً یک ارائه‌دهنده پیامک به لیست \"ارائه‌دهندگان\" برای برنامه: %s اضافه کنید",
"please add an Email provider to the \\\"Providers\\\" list for the application: %s": "please add an Email provider to the \\\"Providers\\\" list for the application: %s", "please add an Email provider to the \"Providers\" list for the application: %s": "لطفاً یک ارائه‌دهنده ایمیل به لیست \"ارائه‌دهندگان\" برای برنامه: %s اضافه کنید",
"the user does not exist, please sign up first": "the user does not exist, please sign up first" "the user does not exist, please sign up first": "کاربر وجود ندارد، لطفاً ابتدا ثبت‌نام کنید"
}, },
"webauthn": { "webauthn": {
"Found no credentials for this user": "Found no credentials for this user", "Found no credentials for this user": "هیچ اعتباری برای این کاربر یافت نشد",
"Please call WebAuthnSigninBegin first": "Please call WebAuthnSigninBegin first" "Please call WebAuthnSigninBegin first": "لطفاً ابتدا WebAuthnSigninBegin را فراخوانی کنید"
} }
} }

View File

@@ -188,10 +188,23 @@ type GitHubUserInfo struct {
} `json:"plan"` } `json:"plan"`
} }
type GitHubUserEmailInfo struct {
Email string `json:"email"`
Primary bool `json:"primary"`
Verified bool `json:"verified"`
Visibility string `json:"visibility"`
}
type GitHubErrorInfo struct {
Message string `json:"message"`
DocumentationUrl string `json:"documentation_url"`
Status string `json:"status"`
}
func (idp *GithubIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) { func (idp *GithubIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
req, err := http.NewRequest("GET", "https://api.github.com/user", nil) req, err := http.NewRequest("GET", "https://api.github.com/user", nil)
if err != nil { if err != nil {
panic(err) return nil, err
} }
req.Header.Add("Authorization", "token "+token.AccessToken) req.Header.Add("Authorization", "token "+token.AccessToken)
resp, err := idp.Client.Do(req) resp, err := idp.Client.Do(req)
@@ -212,6 +225,42 @@ func (idp *GithubIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error)
return nil, err return nil, err
} }
if githubUserInfo.Email == "" {
reqEmail, err := http.NewRequest("GET", "https://api.github.com/user/emails", nil)
if err != nil {
return nil, err
}
reqEmail.Header.Add("Authorization", "token "+token.AccessToken)
respEmail, err := idp.Client.Do(reqEmail)
if err != nil {
return nil, err
}
defer respEmail.Body.Close()
emailBody, err := io.ReadAll(respEmail.Body)
if err != nil {
return nil, err
}
if respEmail.StatusCode != 200 {
var errMessage GitHubErrorInfo
err = json.Unmarshal(emailBody, &errMessage)
if err != nil {
return nil, err
}
fmt.Printf("GithubIdProvider:GetUserInfo() error, status code = %d, error message = %v\n", respEmail.StatusCode, errMessage)
} else {
var userEmails []GitHubUserEmailInfo
err = json.Unmarshal(emailBody, &userEmails)
if err != nil {
return nil, err
}
githubUserInfo.Email = idp.getEmailFromEmailsResult(userEmails)
}
}
userInfo := UserInfo{ userInfo := UserInfo{
Id: strconv.Itoa(githubUserInfo.Id), Id: strconv.Itoa(githubUserInfo.Id),
Username: githubUserInfo.Login, Username: githubUserInfo.Login,
@@ -248,3 +297,27 @@ func (idp *GithubIdProvider) postWithBody(body interface{}, url string) ([]byte,
return data, nil return data, nil
} }
func (idp *GithubIdProvider) getEmailFromEmailsResult(emailInfo []GitHubUserEmailInfo) string {
primaryEmail := ""
verifiedEmail := ""
for _, addr := range emailInfo {
if !addr.Verified || strings.Contains(addr.Email, "users.noreply.github.com") {
continue
}
if addr.Primary {
primaryEmail = addr.Email
break
} else if verifiedEmail == "" {
verifiedEmail = addr.Email
}
}
if primaryEmail != "" {
return primaryEmail
}
return verifiedEmail
}

161
idp/kwai.go Normal file
View File

@@ -0,0 +1,161 @@
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package idp
import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"golang.org/x/oauth2"
)
type KwaiIdProvider struct {
Client *http.Client
Config *oauth2.Config
}
func NewKwaiIdProvider(clientId string, clientSecret string, redirectUrl string) *KwaiIdProvider {
idp := &KwaiIdProvider{}
idp.Config = idp.getConfig(clientId, clientSecret, redirectUrl)
return idp
}
func (idp *KwaiIdProvider) SetHttpClient(client *http.Client) {
idp.Client = client
}
func (idp *KwaiIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config {
endpoint := oauth2.Endpoint{
TokenURL: "https://open.kuaishou.com/oauth2/access_token",
AuthURL: "https://open.kuaishou.com/oauth2/authorize", // qr code: /oauth2/connect
}
config := &oauth2.Config{
Scopes: []string{"user_info"},
Endpoint: endpoint,
ClientID: clientId,
ClientSecret: clientSecret,
RedirectURL: redirectUrl,
}
return config
}
type KwaiTokenResp struct {
Result int `json:"result"`
ErrorMsg string `json:"error_msg"`
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
RefreshTokenExpiresIn int `json:"refresh_token_expires_in"`
OpenId string `json:"open_id"`
Scopes []string `json:"scopes"`
}
// GetToken use code to get access_token
func (idp *KwaiIdProvider) GetToken(code string) (*oauth2.Token, error) {
params := map[string]string{
"app_id": idp.Config.ClientID,
"app_secret": idp.Config.ClientSecret,
"code": code,
"grant_type": "authorization_code",
}
tokenUrl := fmt.Sprintf("%s?app_id=%s&app_secret=%s&code=%s&grant_type=authorization_code",
idp.Config.Endpoint.TokenURL, params["app_id"], params["app_secret"], params["code"])
resp, err := idp.Client.Get(tokenUrl)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var tokenResp KwaiTokenResp
err = json.Unmarshal(body, &tokenResp)
if err != nil {
return nil, err
}
if tokenResp.Result != 1 {
return nil, fmt.Errorf("get token error: %s", tokenResp.ErrorMsg)
}
token := &oauth2.Token{
AccessToken: tokenResp.AccessToken,
RefreshToken: tokenResp.RefreshToken,
Expiry: time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second),
}
raw := make(map[string]interface{})
raw["open_id"] = tokenResp.OpenId
token = token.WithExtra(raw)
return token, nil
}
// More details: https://open.kuaishou.com/openapi/user_info
type KwaiUserInfo struct {
Result int `json:"result"`
ErrorMsg string `json:"error_msg"`
UserInfo struct {
Head string `json:"head"`
Name string `json:"name"`
Sex string `json:"sex"`
City string `json:"city"`
} `json:"user_info"`
}
// GetUserInfo use token to get user profile
func (idp *KwaiIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
userInfoUrl := fmt.Sprintf("https://open.kuaishou.com/openapi/user_info?app_id=%s&access_token=%s",
idp.Config.ClientID, token.AccessToken)
resp, err := idp.Client.Get(userInfoUrl)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var kwaiUserInfo KwaiUserInfo
err = json.Unmarshal(body, &kwaiUserInfo)
if err != nil {
return nil, err
}
if kwaiUserInfo.Result != 1 {
return nil, fmt.Errorf("get user info error: %s", kwaiUserInfo.ErrorMsg)
}
userInfo := &UserInfo{
Id: token.Extra("open_id").(string),
Username: kwaiUserInfo.UserInfo.Name,
DisplayName: kwaiUserInfo.UserInfo.Name,
AvatarUrl: kwaiUserInfo.UserInfo.Head,
Extra: map[string]string{
"gender": kwaiUserInfo.UserInfo.Sex,
"city": kwaiUserInfo.UserInfo.City,
},
}
return userInfo, nil
}

View File

@@ -113,6 +113,8 @@ func GetIdProvider(idpInfo *ProviderInfo, redirectUrl string) (IdProvider, error
return NewOktaIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl), nil return NewOktaIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl), nil
case "Douyin": case "Douyin":
return NewDouyinIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil return NewDouyinIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "Kwai":
return NewKwaiIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "Bilibili": case "Bilibili":
return NewBilibiliIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil return NewBilibiliIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "MetaMask": case "MetaMask":

View File

@@ -15,6 +15,7 @@
package ldap package ldap
import ( import (
"crypto/tls"
"fmt" "fmt"
"hash/fnv" "hash/fnv"
"log" "log"
@@ -27,21 +28,68 @@ import (
func StartLdapServer() { func StartLdapServer() {
ldapServerPort := conf.GetConfigString("ldapServerPort") ldapServerPort := conf.GetConfigString("ldapServerPort")
if ldapServerPort == "" || ldapServerPort == "0" { ldapsServerPort := conf.GetConfigString("ldapsServerPort")
return
}
server := ldap.NewServer() server := ldap.NewServer()
serverSsl := ldap.NewServer()
routes := ldap.NewRouteMux() routes := ldap.NewRouteMux()
routes.Bind(handleBind) routes.Bind(handleBind)
routes.Search(handleSearch).Label(" SEARCH****") routes.Search(handleSearch).Label(" SEARCH****")
server.Handle(routes) server.Handle(routes)
err := server.ListenAndServe("0.0.0.0:" + ldapServerPort) serverSsl.Handle(routes)
go func() {
if ldapServerPort == "" || ldapServerPort == "0" {
return
}
err := server.ListenAndServe("0.0.0.0:" + ldapServerPort)
if err != nil {
log.Printf("StartLdapServer() failed, err = %s", err.Error())
}
}()
go func() {
if ldapsServerPort == "" || ldapsServerPort == "0" {
return
}
ldapsCertId := conf.GetConfigString("ldapsCertId")
if ldapsCertId == "" {
return
}
config, err := getTLSconfig(ldapsCertId)
if err != nil {
log.Printf("StartLdapsServer() failed, err = %s", err.Error())
return
}
secureConn := func(s *ldap.Server) {
s.Listener = tls.NewListener(s.Listener, config)
}
err = serverSsl.ListenAndServe("0.0.0.0:"+ldapsServerPort, secureConn)
if err != nil {
log.Printf("StartLdapsServer() failed, err = %s", err.Error())
}
}()
}
func getTLSconfig(ldapsCertId string) (*tls.Config, error) {
rawCert, err := object.GetCert(ldapsCertId)
if err != nil { if err != nil {
log.Printf("StartLdapServer() failed, err = %s", err.Error()) return nil, err
} }
if rawCert == nil {
return nil, fmt.Errorf("cert is empty")
}
cert, err := tls.X509KeyPair([]byte(rawCert.Certificate), []byte(rawCert.PrivateKey))
if err != nil {
return &tls.Config{}, err
}
return &tls.Config{
MinVersion: tls.VersionTLS10,
MaxVersion: tls.VersionTLS13,
Certificates: []tls.Certificate{cert},
}, nil
} }
func handleBind(w ldap.ResponseWriter, m *ldap.Message) { func handleBind(w ldap.ResponseWriter, m *ldap.Message) {
@@ -142,7 +190,7 @@ func handleSearch(w ldap.ResponseWriter, m *ldap.Message) {
} }
for _, attr := range attrs { for _, attr := range attrs {
e.AddAttribute(message.AttributeDescription(attr), getAttribute(string(attr), user)) e.AddAttribute(message.AttributeDescription(attr), getAttribute(string(attr), user))
if string(attr) == "cn" { if string(attr) == "title" {
e.AddAttribute(message.AttributeDescription(attr), getAttribute("title", user)) e.AddAttribute(message.AttributeDescription(attr), getAttribute("title", user))
} }
} }

View File

@@ -83,6 +83,11 @@ func main() {
// logs.SetLevel(logs.LevelInformational) // logs.SetLevel(logs.LevelInformational)
logs.SetLogFuncCall(false) logs.SetLogFuncCall(false)
err = util.StopOldInstance(port)
if err != nil {
panic(err)
}
go ldap.StartLdapServer() go ldap.StartLdapServer()
go radius.StartRadiusServer() go radius.StartRadiusServer()
go object.ClearThroughputPerSecond() go object.ClearThroughputPerSecond()

29
notification/cucloud.go Normal file
View File

@@ -0,0 +1,29 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notification
import (
"github.com/casdoor/notify"
"github.com/casdoor/notify/service/cucloud"
)
func NewCucloudProvider(accessKey, secretKey, topicName, messageTitle, cloudRegionCode, accountId, notifyType string) (notify.Notifier, error) {
cucloud := cucloud.New(accessKey, secretKey, topicName, messageTitle, cloudRegionCode, accountId, notifyType)
notifier := notify.New()
notifier.UseServices(cucloud)
return notifier, nil
}

View File

@@ -16,7 +16,7 @@ package notification
import "github.com/casdoor/notify" import "github.com/casdoor/notify"
func GetNotificationProvider(typ string, clientId string, clientSecret string, clientId2 string, clientSecret2 string, appId string, receiver string, method string, title string, metaData string) (notify.Notifier, error) { func GetNotificationProvider(typ string, clientId string, clientSecret string, clientId2 string, clientSecret2 string, appId string, receiver string, method string, title string, metaData string, regionId string) (notify.Notifier, error) {
if typ == "Telegram" { if typ == "Telegram" {
return NewTelegramProvider(clientSecret, receiver) return NewTelegramProvider(clientSecret, receiver)
} else if typ == "Custom HTTP" { } else if typ == "Custom HTTP" {
@@ -53,6 +53,8 @@ func GetNotificationProvider(typ string, clientId string, clientSecret string, c
return NewRocketChatProvider(clientId, clientSecret, appId, receiver) return NewRocketChatProvider(clientId, clientSecret, appId, receiver)
} else if typ == "Viber" { } else if typ == "Viber" {
return NewViberProvider(clientId, clientSecret, appId, receiver) return NewViberProvider(clientId, clientSecret, appId, receiver)
} else if typ == "CUCloud" {
return NewCucloudProvider(clientId, clientSecret, appId, title, regionId, clientId2, metaData)
} }
return nil, nil return nil, nil

View File

@@ -481,7 +481,10 @@ func GetApplicationByClientId(clientId string) (*Application, error) {
} }
func GetApplication(id string) (*Application, error) { func GetApplication(id string) (*Application, error) {
owner, name := util.GetOwnerAndNameFromId(id) owner, name, err := util.GetOwnerAndNameFromIdWithError(id)
if err != nil {
return nil, err
}
return getApplication(owner, name) return getApplication(owner, name)
} }
@@ -723,8 +726,15 @@ func (application *Application) GetId() string {
} }
func (application *Application) IsRedirectUriValid(redirectUri string) bool { func (application *Application) IsRedirectUriValid(redirectUri string) bool {
redirectUris := append([]string{"http://localhost:", "https://localhost:", "http://127.0.0.1:", "http://casdoor-app", ".chromiumapp.org"}, application.RedirectUris...) isValid, err := util.IsValidOrigin(redirectUri)
for _, targetUri := range redirectUris { if err != nil {
panic(err)
}
if isValid {
return true
}
for _, targetUri := range application.RedirectUris {
targetUriRegex := regexp.MustCompile(targetUri) targetUriRegex := regexp.MustCompile(targetUri)
if targetUriRegex.MatchString(redirectUri) || strings.Contains(redirectUri, targetUri) { if targetUriRegex.MatchString(redirectUri) || strings.Contains(redirectUri, targetUri) {
return true return true

View File

@@ -273,7 +273,7 @@ func CheckPasswordComplexity(user *User, password string) string {
return CheckPasswordComplexityByOrg(organization, password) return CheckPasswordComplexityByOrg(organization, password)
} }
func checkLdapUserPassword(user *User, password string, lang string) error { func CheckLdapUserPassword(user *User, password string, lang string) error {
ldaps, err := GetLdaps(user.Owner) ldaps, err := GetLdaps(user.Owner)
if err != nil { if err != nil {
return err return err
@@ -368,7 +368,7 @@ func CheckUserPassword(organization string, username string, password string, la
} }
// only for LDAP users // only for LDAP users
err = checkLdapUserPassword(user, password, lang) err = CheckLdapUserPassword(user, password, lang)
if err != nil { if err != nil {
if err.Error() == "user not exist" { if err.Error() == "user not exist" {
return nil, fmt.Errorf(i18n.Translate(lang, "check:The user: %s doesn't exist in LDAP server"), username) return nil, fmt.Errorf(i18n.Translate(lang, "check:The user: %s doesn't exist in LDAP server"), username)
@@ -381,7 +381,13 @@ func CheckUserPassword(organization string, username string, password string, la
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = checkPasswordExpired(user, lang)
if err != nil {
return nil, err
}
} }
return user, nil return user, nil
} }
@@ -520,11 +526,46 @@ func CheckUsername(username string, lang string) string {
return "" return ""
} }
func CheckUsernameWithEmail(username string, lang string) string {
if username == "" {
return i18n.Translate(lang, "check:Empty username.")
} else if len(username) > 39 {
return i18n.Translate(lang, "check:Username is too long (maximum is 39 characters).")
}
// https://stackoverflow.com/questions/58726546/github-username-convention-using-regex
if !util.ReUserNameWithEmail.MatchString(username) {
return i18n.Translate(lang, "check:Username supports email format. Also 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. Also pay attention to the email format.")
}
return ""
}
func CheckUpdateUser(oldUser, user *User, lang string) string { func CheckUpdateUser(oldUser, user *User, lang string) string {
if oldUser.Name != user.Name { if oldUser.Name != user.Name {
if msg := CheckUsername(user.Name, lang); msg != "" { organizationName := oldUser.Owner
return msg if organizationName == "" {
organizationName = user.Owner
} }
organization, err := getOrganization("admin", organizationName)
if err != nil {
return err.Error()
}
if organization == nil {
return fmt.Sprintf(i18n.Translate(lang, "auth:The organization: %s does not exist"), organizationName)
}
if organization.UseEmailAsUsername {
if msg := CheckUsernameWithEmail(user.Name, lang); msg != "" {
return msg
}
} else {
if msg := CheckUsername(user.Name, lang); msg != "" {
return msg
}
}
if HasUserByField(user.Owner, "name", user.Name) { if HasUserByField(user.Owner, "name", user.Name) {
return i18n.Translate(lang, "check:Username already exists") return i18n.Translate(lang, "check:Username already exists")
} }

View File

@@ -43,6 +43,8 @@ func CheckEntryIp(clientIp string, user *User, application *Application, organiz
if err != nil { if err != nil {
application.IpRestriction = err.Error() + application.Name application.IpRestriction = err.Error() + application.Name
return fmt.Errorf(err.Error() + application.Name) return fmt.Errorf(err.Error() + application.Name)
} else {
application.IpRestriction = ""
} }
if organization == nil && application.OrganizationObj != nil { if organization == nil && application.OrganizationObj != nil {
@@ -55,6 +57,8 @@ func CheckEntryIp(clientIp string, user *User, application *Application, organiz
if err != nil { if err != nil {
organization.IpRestriction = err.Error() + organization.Name organization.IpRestriction = err.Error() + organization.Name
return fmt.Errorf(err.Error() + organization.Name) return fmt.Errorf(err.Error() + organization.Name)
} else {
organization.IpRestriction = ""
} }
} }

View File

@@ -0,0 +1,53 @@
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"time"
"github.com/casdoor/casdoor/i18n"
"github.com/casdoor/casdoor/util"
)
func checkPasswordExpired(user *User, lang string) error {
organization, err := GetOrganizationByUser(user)
if err != nil {
return err
}
if organization == nil {
return fmt.Errorf(i18n.Translate(lang, "check:Organization does not exist"))
}
passwordExpireDays := organization.PasswordExpireDays
if passwordExpireDays <= 0 {
return nil
}
lastChangePasswordTime := user.LastChangePasswordTime
if lastChangePasswordTime == "" {
if user.CreatedTime == "" {
return fmt.Errorf(i18n.Translate(lang, "check:Your password has expired. Please reset your password by clicking \"Forgot password\""))
}
lastChangePasswordTime = user.CreatedTime
}
lastTime := util.String2Time(lastChangePasswordTime)
expireTime := lastTime.AddDate(0, 0, passwordExpireDays)
if time.Now().After(expireTime) {
return fmt.Errorf(i18n.Translate(lang, "check:Your password has expired. Please reset your password by clicking \"Forgot password\""))
}
return nil
}

View File

@@ -16,23 +16,18 @@
package object package object
import ( import "github.com/casdoor/casdoor/email"
"crypto/tls"
"github.com/casdoor/casdoor/email" // TestSmtpServer Test the SMTP server
"github.com/casdoor/gomail/v2" func TestSmtpServer(provider *Provider) error {
) smtpEmailProvider := email.NewSmtpEmailProvider(provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, provider.Type, provider.DisableSsl)
sender, err := smtpEmailProvider.Dialer.Dial()
func getDialer(provider *Provider) *gomail.Dialer { if err != nil {
dialer := &gomail.Dialer{} return err
dialer = gomail.NewDialer(provider.Host, provider.Port, provider.ClientId, provider.ClientSecret)
if provider.Type == "SUBMAIL" {
dialer.TLSConfig = &tls.Config{InsecureSkipVerify: true}
} }
defer sender.Close()
dialer.SSL = !provider.DisableSsl return nil
return dialer
} }
func SendEmail(provider *Provider, title string, content string, dest string, sender string) error { func SendEmail(provider *Provider, title string, content string, dest string, sender string) error {
@@ -50,16 +45,3 @@ func SendEmail(provider *Provider, title string, content string, dest string, se
return emailProvider.Send(fromAddress, fromName, dest, title, content) return emailProvider.Send(fromAddress, fromName, dest, title, content)
} }
// DailSmtpServer Dail Smtp server
func DailSmtpServer(provider *Provider) error {
dialer := getDialer(provider)
sender, err := dialer.Dial()
if err != nil {
return err
}
defer sender.Close()
return nil
}

View File

@@ -19,124 +19,90 @@ import (
"time" "time"
) )
type Dashboard struct { type DashboardDateItem struct {
OrganizationCounts []int `json:"organizationCounts"` CreatedTime string `json:"createTime"`
UserCounts []int `json:"userCounts"`
ProviderCounts []int `json:"providerCounts"`
ApplicationCounts []int `json:"applicationCounts"`
SubscriptionCounts []int `json:"subscriptionCounts"`
} }
func GetDashboard(owner string) (*Dashboard, error) { type DashboardMapItem struct {
dashboardDateItems []DashboardDateItem
itemCount int64
}
func GetDashboard(owner string) (*map[string][]int64, error) {
if owner == "All" { if owner == "All" {
owner = "" owner = ""
} }
dashboard := &Dashboard{ dashboard := make(map[string][]int64)
OrganizationCounts: make([]int, 31), dashboardMap := sync.Map{}
UserCounts: make([]int, 31), tableNames := []string{"organization", "user", "provider", "application", "subscription", "role", "group", "resource", "cert", "permission", "transaction", "model", "adapter", "enforcer"}
ProviderCounts: make([]int, 31),
ApplicationCounts: make([]int, 31), time30day := time.Now().AddDate(0, 0, -30)
SubscriptionCounts: make([]int, 31), var wg sync.WaitGroup
var err error
wg.Add(len(tableNames))
ch := make(chan error, len(tableNames))
for _, tableName := range tableNames {
dashboard[tableName+"Counts"] = make([]int64, 31)
tableName := tableName
go func(ch chan error) {
defer wg.Done()
dashboardDateItems := []DashboardDateItem{}
var countResult int64
dbQueryBefore := ormer.Engine.Cols("created_time")
dbQueryAfter := ormer.Engine.Cols("created_time")
if owner != "" {
dbQueryAfter = dbQueryAfter.And("owner = ?", owner)
dbQueryBefore = dbQueryBefore.And("owner = ?", owner)
}
if countResult, err = dbQueryBefore.And("created_time < ?", time30day).Table(tableName).Count(); err != nil {
ch <- err
return
}
if err = dbQueryAfter.And("created_time >= ?", time30day).Table(tableName).Find(&dashboardDateItems); err != nil {
ch <- err
return
}
dashboardMap.Store(tableName, DashboardMapItem{
dashboardDateItems: dashboardDateItems,
itemCount: countResult,
})
}(ch)
} }
organizations := []Organization{}
users := []User{}
providers := []Provider{}
applications := []Application{}
subscriptions := []Subscription{}
var wg sync.WaitGroup
wg.Add(5)
go func() {
defer wg.Done()
if err := ormer.Engine.Find(&organizations, &Organization{Owner: owner}); err != nil {
panic(err)
}
}()
go func() {
defer wg.Done()
if err := ormer.Engine.Find(&users, &User{Owner: owner}); err != nil {
panic(err)
}
}()
go func() {
defer wg.Done()
if err := ormer.Engine.Find(&providers, &Provider{Owner: owner}); err != nil {
panic(err)
}
}()
go func() {
defer wg.Done()
if err := ormer.Engine.Find(&applications, &Application{Owner: owner}); err != nil {
panic(err)
}
}()
go func() {
defer wg.Done()
if err := ormer.Engine.Find(&subscriptions, &Subscription{Owner: owner}); err != nil {
panic(err)
}
}()
wg.Wait() wg.Wait()
close(ch)
for err = range ch {
if err != nil {
return nil, err
}
}
nowTime := time.Now() nowTime := time.Now()
for i := 30; i >= 0; i-- { for i := 30; i >= 0; i-- {
cutTime := nowTime.AddDate(0, 0, -i) cutTime := nowTime.AddDate(0, 0, -i)
dashboard.OrganizationCounts[30-i] = countCreatedBefore(organizations, cutTime) for _, tableName := range tableNames {
dashboard.UserCounts[30-i] = countCreatedBefore(users, cutTime) item, exist := dashboardMap.Load(tableName)
dashboard.ProviderCounts[30-i] = countCreatedBefore(providers, cutTime) if !exist {
dashboard.ApplicationCounts[30-i] = countCreatedBefore(applications, cutTime) continue
dashboard.SubscriptionCounts[30-i] = countCreatedBefore(subscriptions, cutTime) }
dashboard[tableName+"Counts"][30-i] = countCreatedBefore(item.(DashboardMapItem), cutTime)
}
} }
return dashboard, nil return &dashboard, nil
} }
func countCreatedBefore(objects interface{}, before time.Time) int { func countCreatedBefore(dashboardMapItem DashboardMapItem, before time.Time) int64 {
count := 0 count := dashboardMapItem.itemCount
switch obj := objects.(type) { for _, e := range dashboardMapItem.dashboardDateItems {
case []Organization: createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", e.CreatedTime)
for _, o := range obj { if createdTime.Before(before) {
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", o.CreatedTime) count++
if createdTime.Before(before) {
count++
}
}
case []User:
for _, u := range obj {
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", u.CreatedTime)
if createdTime.Before(before) {
count++
}
}
case []Provider:
for _, p := range obj {
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", p.CreatedTime)
if createdTime.Before(before) {
count++
}
}
case []Application:
for _, a := range obj {
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", a.CreatedTime)
if createdTime.Before(before) {
count++
}
}
case []Subscription:
for _, s := range obj {
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", s.CreatedTime)
if createdTime.Before(before) {
count++
}
} }
} }
return count return count

View File

@@ -17,7 +17,6 @@ package object
import ( import (
"errors" "errors"
"fmt" "fmt"
"sync"
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
@@ -36,12 +35,14 @@ type Group struct {
ContactEmail string `xorm:"varchar(100)" json:"contactEmail"` ContactEmail string `xorm:"varchar(100)" json:"contactEmail"`
Type string `xorm:"varchar(100)" json:"type"` Type string `xorm:"varchar(100)" json:"type"`
ParentId string `xorm:"varchar(100)" json:"parentId"` ParentId string `xorm:"varchar(100)" json:"parentId"`
ParentName string `xorm:"-" json:"parentName"`
IsTopGroup bool `xorm:"bool" json:"isTopGroup"` IsTopGroup bool `xorm:"bool" json:"isTopGroup"`
Users []string `xorm:"-" json:"users"` Users []string `xorm:"-" json:"users"`
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
Key string `json:"key,omitempty"` Key string `json:"key,omitempty"`
Children []*Group `json:"children,omitempty"` HaveChildren bool `xorm:"-" json:"haveChildren"`
Children []*Group `json:"children,omitempty"`
IsEnabled bool `json:"isEnabled"` IsEnabled bool `json:"isEnabled"`
} }
@@ -79,6 +80,26 @@ func GetPaginationGroups(owner string, offset, limit int, field, value, sortFiel
return groups, nil return groups, nil
} }
func GetGroupsHaveChildrenMap(groups []*Group) (map[string]*Group, error) {
groupsHaveChildren := []*Group{}
resultMap := make(map[string]*Group)
groupIds := []string{}
for _, group := range groups {
groupIds = append(groupIds, group.Name)
groupIds = append(groupIds, group.ParentId)
}
err := ormer.Engine.Cols("owner", "name", "parent_id", "display_name").Distinct("parent_id").In("parent_id", groupIds).Find(&groupsHaveChildren)
if err != nil {
return nil, err
}
for _, group := range groups {
resultMap[group.Name] = group
}
return resultMap, nil
}
func getGroup(owner string, name string) (*Group, error) { func getGroup(owner string, name string) (*Group, error) {
if owner == "" || name == "" { if owner == "" || name == "" {
return nil, nil return nil, nil
@@ -298,17 +319,11 @@ func ExtendGroupWithUsers(group *Group) error {
return nil return nil
} }
users, err := GetUsers(group.Owner)
if err != nil {
return err
}
groupId := group.GetId() groupId := group.GetId()
userIds := []string{} userIds := []string{}
for _, user := range users { userIds, err := userEnforcer.GetAllUsersByGroup(groupId)
if util.InSlice(user.Groups, groupId) { if err != nil {
userIds = append(userIds, user.GetId()) return err
}
} }
group.Users = userIds group.Users = userIds
@@ -316,29 +331,14 @@ func ExtendGroupWithUsers(group *Group) error {
} }
func ExtendGroupsWithUsers(groups []*Group) error { func ExtendGroupsWithUsers(groups []*Group) error {
var wg sync.WaitGroup
errChan := make(chan error, len(groups))
for _, group := range groups { for _, group := range groups {
wg.Add(1) users, err := userEnforcer.GetAllUsersByGroup(group.GetId())
go func(group *Group) {
defer wg.Done()
err := ExtendGroupWithUsers(group)
if err != nil {
errChan <- err
}
}(group)
}
wg.Wait()
close(errChan)
for err := range errChan {
if err != nil { if err != nil {
return err return err
} }
}
group.Users = users
}
return nil return nil
} }

View File

@@ -33,6 +33,7 @@ type Ldap struct {
Filter string `xorm:"varchar(200)" json:"filter"` Filter string `xorm:"varchar(200)" json:"filter"`
FilterFields []string `xorm:"varchar(100)" json:"filterFields"` FilterFields []string `xorm:"varchar(100)" json:"filterFields"`
DefaultGroup string `xorm:"varchar(100)" json:"defaultGroup"` DefaultGroup string `xorm:"varchar(100)" json:"defaultGroup"`
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
AutoSync int `json:"autoSync"` AutoSync int `json:"autoSync"`
LastSync string `xorm:"varchar(100)" json:"lastSync"` LastSync string `xorm:"varchar(100)" json:"lastSync"`
@@ -149,7 +150,7 @@ func UpdateLdap(ldap *Ldap) (bool, error) {
} }
affected, err := ormer.Engine.ID(ldap.Id).Cols("owner", "server_name", "host", affected, err := ormer.Engine.ID(ldap.Id).Cols("owner", "server_name", "host",
"port", "enable_ssl", "username", "password", "base_dn", "filter", "filter_fields", "auto_sync", "default_group").Update(ldap) "port", "enable_ssl", "username", "password", "base_dn", "filter", "filter_fields", "auto_sync", "default_group", "password_type").Update(ldap)
if err != nil { if err != nil {
return false, nil return false, nil
} }

View File

@@ -15,14 +15,18 @@
package object package object
import ( import (
"crypto/md5"
"encoding/base64"
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/i18n"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
goldap "github.com/go-ldap/ldap/v3" goldap "github.com/go-ldap/ldap/v3"
"github.com/thanhpk/randstr" "github.com/thanhpk/randstr"
"golang.org/x/text/encoding/unicode"
) )
type LdapConn struct { type LdapConn struct {
@@ -371,6 +375,88 @@ func GetExistUuids(owner string, uuids []string) ([]string, error) {
return existUuids, nil return existUuids, nil
} }
func ResetLdapPassword(user *User, oldPassword string, newPassword string, lang string) error {
ldaps, err := GetLdaps(user.Owner)
if err != nil {
return err
}
for _, ldapServer := range ldaps {
conn, err := ldapServer.GetLdapConn()
if err != nil {
continue
}
searchReq := goldap.NewSearchRequest(ldapServer.BaseDn, goldap.ScopeWholeSubtree, goldap.NeverDerefAliases,
0, 0, false, ldapServer.buildAuthFilterString(user), []string{}, nil)
searchResult, err := conn.Conn.Search(searchReq)
if err != nil {
conn.Close()
return err
}
if len(searchResult.Entries) == 0 {
conn.Close()
continue
}
if len(searchResult.Entries) > 1 {
conn.Close()
return fmt.Errorf(i18n.Translate(lang, "check:Multiple accounts with same uid, please check your ldap server"))
}
userDn := searchResult.Entries[0].DN
var pwdEncoded string
modifyPasswordRequest := goldap.NewModifyRequest(userDn, nil)
if conn.IsAD {
utf16 := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM)
pwdEncoded, err := utf16.NewEncoder().String("\"" + newPassword + "\"")
if err != nil {
conn.Close()
return err
}
modifyPasswordRequest.Replace("unicodePwd", []string{pwdEncoded})
modifyPasswordRequest.Replace("userAccountControl", []string{"512"})
} else if oldPassword != "" {
modifyPasswordRequestWithOldPassword := goldap.NewPasswordModifyRequest(userDn, oldPassword, newPassword)
_, err = conn.Conn.PasswordModify(modifyPasswordRequestWithOldPassword)
if err != nil {
conn.Close()
return err
}
conn.Close()
return nil
} else {
switch ldapServer.PasswordType {
case "SSHA":
pwdEncoded, err = generateSSHA(newPassword)
break
case "MD5":
md5Byte := md5.Sum([]byte(newPassword))
md5Password := base64.StdEncoding.EncodeToString(md5Byte[:])
pwdEncoded = "{MD5}" + md5Password
break
case "Plain":
pwdEncoded = newPassword
break
default:
pwdEncoded = newPassword
break
}
modifyPasswordRequest.Replace("userPassword", []string{pwdEncoded})
}
err = conn.Conn.Modify(modifyPasswordRequest)
if err != nil {
conn.Close()
return err
}
conn.Close()
}
return nil
}
func (ldapUser *LdapUser) buildLdapUserName(owner string) (string, error) { func (ldapUser *LdapUser) buildLdapUserName(owner string) (string, error) {
user := User{} user := User{}
uidWithNumber := fmt.Sprintf("%s_%s", ldapUser.Uid, ldapUser.UidNumber) uidWithNumber := fmt.Sprintf("%s_%s", ldapUser.Uid, ldapUser.UidNumber)

View File

@@ -0,0 +1,36 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"crypto/rand"
"crypto/sha1"
"encoding/base64"
)
func generateSSHA(password string) (string, error) {
salt := make([]byte, 4)
_, err := rand.Read(salt)
if err != nil {
return "", err
}
combined := append([]byte(password), salt...)
hash := sha1.Sum(combined)
hashWithSalt := append(hash[:], salt...)
encoded := base64.StdEncoding.EncodeToString(hashWithSalt)
return "{SSHA}" + encoded, nil
}

View File

@@ -23,7 +23,7 @@ import (
func getNotificationClient(provider *Provider) (notify.Notifier, error) { func getNotificationClient(provider *Provider) (notify.Notifier, error) {
var client notify.Notifier var client notify.Notifier
client, err := notification.GetNotificationProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.ClientId2, provider.ClientSecret2, provider.AppId, provider.Receiver, provider.Method, provider.Title, provider.Metadata) client, err := notification.GetNotificationProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.ClientId2, provider.ClientSecret2, provider.AppId, provider.Receiver, provider.Method, provider.Title, provider.Metadata, provider.RegionId)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -62,14 +62,15 @@ type Organization struct {
PasswordOptions []string `xorm:"varchar(100)" json:"passwordOptions"` PasswordOptions []string `xorm:"varchar(100)" json:"passwordOptions"`
PasswordObfuscatorType string `xorm:"varchar(100)" json:"passwordObfuscatorType"` PasswordObfuscatorType string `xorm:"varchar(100)" json:"passwordObfuscatorType"`
PasswordObfuscatorKey string `xorm:"varchar(100)" json:"passwordObfuscatorKey"` PasswordObfuscatorKey string `xorm:"varchar(100)" json:"passwordObfuscatorKey"`
PasswordExpireDays int `json:"passwordExpireDays"`
CountryCodes []string `xorm:"varchar(200)" json:"countryCodes"` CountryCodes []string `xorm:"varchar(200)" json:"countryCodes"`
DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"` DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"`
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"` DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
Tags []string `xorm:"mediumtext" json:"tags"` Tags []string `xorm:"mediumtext" json:"tags"`
Languages []string `xorm:"varchar(255)" json:"languages"` Languages []string `xorm:"varchar(255)" json:"languages"`
ThemeData *ThemeData `xorm:"json" json:"themeData"` ThemeData *ThemeData `xorm:"json" json:"themeData"`
MasterPassword string `xorm:"varchar(100)" json:"masterPassword"` MasterPassword string `xorm:"varchar(200)" json:"masterPassword"`
DefaultPassword string `xorm:"varchar(100)" json:"defaultPassword"` DefaultPassword string `xorm:"varchar(200)" json:"defaultPassword"`
MasterVerificationCode string `xorm:"varchar(100)" json:"masterVerificationCode"` MasterVerificationCode string `xorm:"varchar(100)" json:"masterVerificationCode"`
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"` IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
InitScore int `json:"initScore"` InitScore int `json:"initScore"`
@@ -150,7 +151,10 @@ func getOrganization(owner string, name string) (*Organization, error) {
} }
func GetOrganization(id string) (*Organization, error) { func GetOrganization(id string) (*Organization, error) {
owner, name := util.GetOwnerAndNameFromId(id) owner, name, err := util.GetOwnerAndNameFromIdWithError(id)
if err != nil {
return nil, err
}
return getOrganization(owner, name) return getOrganization(owner, name)
} }

View File

@@ -16,6 +16,7 @@ package object
import ( import (
"fmt" "fmt"
"regexp"
"strings" "strings"
"github.com/beego/beego/context" "github.com/beego/beego/context"
@@ -70,6 +71,7 @@ type Provider struct {
IdP string `xorm:"mediumtext" json:"idP"` IdP string `xorm:"mediumtext" json:"idP"`
IssuerUrl string `xorm:"varchar(100)" json:"issuerUrl"` IssuerUrl string `xorm:"varchar(100)" json:"issuerUrl"`
EnableSignAuthnRequest bool `json:"enableSignAuthnRequest"` EnableSignAuthnRequest bool `json:"enableSignAuthnRequest"`
EmailRegex string `xorm:"varchar(200)" json:"emailRegex"`
ProviderUrl string `xorm:"varchar(200)" json:"providerUrl"` ProviderUrl string `xorm:"varchar(200)" json:"providerUrl"`
} }
@@ -200,6 +202,13 @@ func UpdateProvider(id string, provider *Provider) (bool, error) {
return false, nil return false, nil
} }
if provider.EmailRegex != "" {
_, err := regexp.Compile(provider.EmailRegex)
if err != nil {
return false, err
}
}
if name != provider.Name { if name != provider.Name {
err := providerChangeTrigger(name, provider.Name) err := providerChangeTrigger(name, provider.Name)
if err != nil { if err != nil {
@@ -234,6 +243,13 @@ func AddProvider(provider *Provider) (bool, error) {
provider.IntranetEndpoint = util.GetEndPoint(provider.IntranetEndpoint) provider.IntranetEndpoint = util.GetEndPoint(provider.IntranetEndpoint)
} }
if provider.EmailRegex != "" {
_, err := regexp.Compile(provider.EmailRegex)
if err != nil {
return false, err
}
}
affected, err := ormer.Engine.Insert(provider) affected, err := ormer.Engine.Insert(provider)
if err != nil { if err != nil {
return false, err return false, err
@@ -421,7 +437,7 @@ func FromProviderToIdpInfo(ctx *context.Context, provider *Provider) *idp.Provid
providerInfo.ClientId = provider.ClientId2 providerInfo.ClientId = provider.ClientId2
providerInfo.ClientSecret = provider.ClientSecret2 providerInfo.ClientSecret = provider.ClientSecret2
} }
} else if provider.Type == "AzureAD" || provider.Type == "AzureADB2C" || provider.Type == "ADFS" || provider.Type == "Okta" { } else if provider.Type == "ADFS" || provider.Type == "AzureAD" || provider.Type == "AzureADB2C" || provider.Type == "Casdoor" || provider.Type == "Okta" {
providerInfo.HostUrl = provider.Domain providerInfo.HostUrl = provider.Domain
} }

View File

@@ -33,7 +33,7 @@ var (
func init() { func init() {
logPostOnly = conf.GetConfigBool("logPostOnly") logPostOnly = conf.GetConfigBool("logPostOnly")
passwordRegex = regexp.MustCompile("\"password\":\".+\"") passwordRegex = regexp.MustCompile("\"password\":\"([^\"]*?)\"")
} }
type Record struct { type Record struct {

View File

@@ -338,6 +338,10 @@ func roleChangeTrigger(oldName string, newName string) error {
for _, role := range roles { for _, role := range roles {
for j, u := range role.Roles { for j, u := range role.Roles {
if u == "*" {
continue
}
owner, name := util.GetOwnerAndNameFromId(u) owner, name := util.GetOwnerAndNameFromId(u)
if name == oldName { if name == oldName {
role.Roles[j] = util.GetId(owner, newName) role.Roles[j] = util.GetId(owner, newName)
@@ -358,6 +362,10 @@ func roleChangeTrigger(oldName string, newName string) error {
for _, permission := range permissions { for _, permission := range permissions {
for j, u := range permission.Roles { for j, u := range permission.Roles {
// u = organization/username // u = organization/username
if u == "*" {
continue
}
owner, name := util.GetOwnerAndNameFromId(u) owner, name := util.GetOwnerAndNameFromId(u)
if name == oldName { if name == oldName {
permission.Roles[j] = util.GetId(owner, newName) permission.Roles[j] = util.GetId(owner, newName)

View File

@@ -26,6 +26,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"strings"
"time" "time"
"github.com/beevik/etree" "github.com/beevik/etree"
@@ -222,10 +223,13 @@ func GetSamlMeta(application *Application, host string, enablePostBinding bool)
originFrontend, originBackend := getOriginFromHost(host) originFrontend, originBackend := getOriginFromHost(host)
idpLocation := "" idpLocation := ""
idpBinding := ""
if enablePostBinding { if enablePostBinding {
idpLocation = fmt.Sprintf("%s/api/saml/redirect/%s/%s", originBackend, application.Owner, application.Name) idpLocation = fmt.Sprintf("%s/api/saml/redirect/%s/%s", originBackend, application.Owner, application.Name)
idpBinding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
} else { } else {
idpLocation = fmt.Sprintf("%s/login/saml/authorize/%s/%s", originFrontend, application.Owner, application.Name) idpLocation = fmt.Sprintf("%s/login/saml/authorize/%s/%s", originFrontend, application.Owner, application.Name)
idpBinding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
} }
d := IdpEntityDescriptor{ d := IdpEntityDescriptor{
@@ -258,7 +262,7 @@ func GetSamlMeta(application *Application, host string, enablePostBinding bool)
{Xmlns: "urn:oasis:names:tc:SAML:2.0:assertion", Name: "Name", NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic", FriendlyName: "Name"}, {Xmlns: "urn:oasis:names:tc:SAML:2.0:assertion", Name: "Name", NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic", FriendlyName: "Name"},
}, },
SingleSignOnService: SingleSignOnService{ SingleSignOnService: SingleSignOnService{
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", Binding: idpBinding,
Location: idpLocation, Location: idpLocation,
}, },
ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol", ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol",
@@ -273,29 +277,38 @@ func GetSamlMeta(application *Application, host string, enablePostBinding bool)
func GetSamlResponse(application *Application, user *User, samlRequest string, host string) (string, string, string, error) { func GetSamlResponse(application *Application, user *User, samlRequest string, host string) (string, string, string, error) {
// request type // request type
method := "GET" method := "GET"
samlRequest = strings.ReplaceAll(samlRequest, " ", "+")
// base64 decode // base64 decode
defated, err := base64.StdEncoding.DecodeString(samlRequest) defated, err := base64.StdEncoding.DecodeString(samlRequest)
if err != nil { if err != nil {
return "", "", "", fmt.Errorf("err: Failed to decode SAML request, %s", err.Error()) return "", "", "", fmt.Errorf("err: Failed to decode SAML request, %s", err.Error())
} }
// decompress var requestByte []byte
var buffer bytes.Buffer
rdr := flate.NewReader(bytes.NewReader(defated))
for { if strings.Contains(string(defated), "xmlns:") {
_, err = io.CopyN(&buffer, rdr, 1024) requestByte = defated
if err != nil { } else {
if err == io.EOF { // decompress
break var buffer bytes.Buffer
rdr := flate.NewReader(bytes.NewReader(defated))
for {
_, err = io.CopyN(&buffer, rdr, 1024)
if err != nil {
if err == io.EOF {
break
}
return "", "", "", err
} }
return "", "", "", err
} }
requestByte = buffer.Bytes()
} }
var authnRequest saml.AuthNRequest var authnRequest saml.AuthNRequest
err = xml.Unmarshal(buffer.Bytes(), &authnRequest) err = xml.Unmarshal(requestByte, &authnRequest)
if err != nil { if err != nil {
return "", "", "", fmt.Errorf("err: Failed to unmarshal AuthnRequest, please check the SAML request, %s", err.Error()) return "", "", "", fmt.Errorf("err: Failed to unmarshal AuthnRequest, please check the SAML request, %s", err.Error())
} }
@@ -325,6 +338,9 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
} else if authnRequest.AssertionConsumerServiceURL == "" { } else if authnRequest.AssertionConsumerServiceURL == "" {
return "", "", "", fmt.Errorf("err: SAML request don't has attribute 'AssertionConsumerServiceURL' in <samlp:AuthnRequest>") return "", "", "", fmt.Errorf("err: SAML request don't has attribute 'AssertionConsumerServiceURL' in <samlp:AuthnRequest>")
} }
if authnRequest.ProtocolBinding == "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" {
method = "POST"
}
_, originBackend := getOriginFromHost(host) _, originBackend := getOriginFromHost(host)

View File

@@ -102,14 +102,6 @@ func GetTokenByAccessToken(accessToken string) (*Token, error) {
return nil, err return nil, err
} }
if !existed {
token = Token{AccessToken: accessToken}
existed, err = ormer.Engine.Get(&token)
if err != nil {
return nil, err
}
}
if !existed { if !existed {
return nil, nil return nil, nil
} }
@@ -123,14 +115,6 @@ func GetTokenByRefreshToken(refreshToken string) (*Token, error) {
return nil, err return nil, err
} }
if !existed {
token = Token{RefreshToken: refreshToken}
existed, err = ormer.Engine.Get(&token)
if err != nil {
return nil, err
}
}
if !existed { if !existed {
return nil, nil return nil, nil
} }
@@ -140,6 +124,7 @@ func GetTokenByRefreshToken(refreshToken string) (*Token, error) {
func GetTokenByTokenValue(tokenValue, tokenTypeHint string) (*Token, error) { func GetTokenByTokenValue(tokenValue, tokenTypeHint string) (*Token, error) {
switch tokenTypeHint { switch tokenTypeHint {
case "access_token": case "access_token":
case "access-token":
token, err := GetTokenByAccessToken(tokenValue) token, err := GetTokenByAccessToken(tokenValue)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -148,6 +133,7 @@ func GetTokenByTokenValue(tokenValue, tokenTypeHint string) (*Token, error) {
return token, nil return token, nil
} }
case "refresh_token": case "refresh_token":
case "refresh-token":
token, err := GetTokenByRefreshToken(tokenValue) token, err := GetTokenByRefreshToken(tokenValue)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -22,6 +22,7 @@ import (
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"math/rand" "math/rand"
"strings"
"sync" "sync"
"time" "time"
@@ -184,6 +185,15 @@ func StoreCasTokenForProxyTicket(token *CasAuthenticationSuccess, targetService,
return proxyTicket return proxyTicket
} }
func escapeXMLText(input string) (string, error) {
var sb strings.Builder
err := xml.EscapeText(&sb, []byte(input))
if err != nil {
return "", err
}
return sb.String(), nil
}
func GenerateCasToken(userId string, service string) (string, error) { func GenerateCasToken(userId string, service string) (string, error) {
user, err := GetUser(userId) user, err := GetUser(userId)
if err != nil { if err != nil {
@@ -225,6 +235,11 @@ func GenerateCasToken(userId string, service string) (string, error) {
} }
if value != "" { if value != "" {
if escapedValue, err := escapeXMLText(value); err != nil {
return "", err
} else {
value = escapedValue
}
authenticationSuccess.Attributes.UserAttributes.Attributes = append(authenticationSuccess.Attributes.UserAttributes.Attributes, &CasNamedAttribute{ authenticationSuccess.Attributes.UserAttributes.Attributes = append(authenticationSuccess.Attributes.UserAttributes.Attributes, &CasNamedAttribute{
Name: k, Name: k,
Value: value, Value: value,

View File

@@ -309,22 +309,29 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
}, nil }, nil
} }
var oldTokenScope string
if application.TokenFormat == "JWT-Standard" { if application.TokenFormat == "JWT-Standard" {
_, err = ParseStandardJwtToken(refreshToken, cert) oldToken, err := ParseStandardJwtToken(refreshToken, cert)
if err != nil { if err != nil {
return &TokenError{ return &TokenError{
Error: InvalidGrant, Error: InvalidGrant,
ErrorDescription: fmt.Sprintf("parse refresh token error: %s", err.Error()), ErrorDescription: fmt.Sprintf("parse refresh token error: %s", err.Error()),
}, nil }, nil
} }
oldTokenScope = oldToken.Scope
} else { } else {
_, err = ParseJwtToken(refreshToken, cert) oldToken, err := ParseJwtToken(refreshToken, cert)
if err != nil { if err != nil {
return &TokenError{ return &TokenError{
Error: InvalidGrant, Error: InvalidGrant,
ErrorDescription: fmt.Sprintf("parse refresh token error: %s", err.Error()), ErrorDescription: fmt.Sprintf("parse refresh token error: %s", err.Error()),
}, nil }, nil
} }
oldTokenScope = oldToken.Scope
}
if scope == "" {
scope = oldTokenScope
} }
// generate a new token // generate a new token
@@ -504,7 +511,7 @@ func GetPasswordToken(application *Application, username string, password string
} }
if user.Ldap != "" { if user.Ldap != "" {
err = checkLdapUserPassword(user, password, "en") err = CheckLdapUserPassword(user, password, "en")
} else { } else {
err = CheckPassword(user, password, "en") err = CheckPassword(user, password, "en")
} }

View File

@@ -129,6 +129,7 @@ type User struct {
Bilibili string `xorm:"bilibili varchar(100)" json:"bilibili"` Bilibili string `xorm:"bilibili varchar(100)" json:"bilibili"`
Okta string `xorm:"okta varchar(100)" json:"okta"` Okta string `xorm:"okta varchar(100)" json:"okta"`
Douyin string `xorm:"douyin varchar(100)" json:"douyin"` Douyin string `xorm:"douyin varchar(100)" json:"douyin"`
Kwai string `xorm:"kwai varchar(100)" json:"kwai"`
Line string `xorm:"line varchar(100)" json:"line"` Line string `xorm:"line varchar(100)" json:"line"`
Amazon string `xorm:"amazon varchar(100)" json:"amazon"` Amazon string `xorm:"amazon varchar(100)" json:"amazon"`
Auth0 string `xorm:"auth0 varchar(100)" json:"auth0"` Auth0 string `xorm:"auth0 varchar(100)" json:"auth0"`
@@ -200,8 +201,9 @@ type User struct {
Permissions []*Permission `json:"permissions"` Permissions []*Permission `json:"permissions"`
Groups []string `xorm:"groups varchar(1000)" json:"groups"` Groups []string `xorm:"groups varchar(1000)" json:"groups"`
LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"` LastChangePasswordTime string `xorm:"varchar(100)" json:"lastChangePasswordTime"`
SigninWrongTimes int `json:"signinWrongTimes"` LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"`
SigninWrongTimes int `json:"signinWrongTimes"`
ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"` ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"`
MfaAccounts []MfaAccount `xorm:"mfaAccounts blob" json:"mfaAccounts"` MfaAccounts []MfaAccount `xorm:"mfaAccounts blob" json:"mfaAccounts"`
@@ -236,6 +238,7 @@ type MfaAccount struct {
AccountName string `xorm:"varchar(100)" json:"accountName"` AccountName string `xorm:"varchar(100)" json:"accountName"`
Issuer string `xorm:"varchar(100)" json:"issuer"` Issuer string `xorm:"varchar(100)" json:"issuer"`
SecretKey string `xorm:"varchar(100)" json:"secretKey"` SecretKey string `xorm:"varchar(100)" json:"secretKey"`
Origin string `xorm:"varchar(100)" json:"origin"`
} }
type FaceId struct { type FaceId struct {
@@ -678,6 +681,10 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
user.Password = oldUser.Password user.Password = oldUser.Password
} }
if user.Id != oldUser.Id && user.Id == "" {
user.Id = oldUser.Id
}
if user.Avatar != oldUser.Avatar && user.Avatar != "" && user.PermanentAvatar != "*" { if user.Avatar != oldUser.Avatar && user.Avatar != "" && user.PermanentAvatar != "*" {
user.PermanentAvatar, err = getPermanentAvatarUrl(user.Owner, user.Name, user.Avatar, false) user.PermanentAvatar, err = getPermanentAvatarUrl(user.Owner, user.Name, user.Avatar, false)
if err != nil { if err != nil {
@@ -690,9 +697,9 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
"owner", "display_name", "avatar", "first_name", "last_name", "owner", "display_name", "avatar", "first_name", "last_name",
"location", "address", "country_code", "region", "language", "affiliation", "title", "id_card_type", "id_card", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application", "location", "address", "country_code", "region", "language", "affiliation", "title", "id_card_type", "id_card", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application",
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts", "face_ids", "mfaAccounts", "is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts", "face_ids", "mfaAccounts",
"signin_wrong_times", "last_signin_wrong_time", "groups", "access_key", "access_secret", "mfa_phone_enabled", "mfa_email_enabled", "signin_wrong_times", "last_change_password_time", "last_signin_wrong_time", "groups", "access_key", "access_secret", "mfa_phone_enabled", "mfa_email_enabled",
"github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs", "github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs",
"baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "line", "amazon", "baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "kwai", "line", "amazon",
"auth0", "battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox", "auth0", "battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox",
"eveonline", "fitbit", "gitea", "heroku", "influxcloud", "instagram", "intercom", "kakao", "lastfm", "mailru", "meetup", "eveonline", "fitbit", "gitea", "heroku", "influxcloud", "instagram", "intercom", "kakao", "lastfm", "mailru", "meetup",
"microsoftonline", "naver", "nextcloud", "onedrive", "oura", "patreon", "paypal", "salesforce", "shopify", "soundcloud", "microsoftonline", "naver", "nextcloud", "onedrive", "oura", "patreon", "paypal", "salesforce", "shopify", "soundcloud",
@@ -816,6 +823,10 @@ func AddUser(user *User) (bool, error) {
user.UpdateUserPassword(organization) user.UpdateUserPassword(organization)
} }
if user.CreatedTime == "" {
user.CreatedTime = util.GetCurrentTime()
}
err = user.UpdateUserHash() err = user.UpdateUserHash()
if err != nil { if err != nil {
return false, err return false, err
@@ -835,11 +846,14 @@ func AddUser(user *User) (bool, error) {
} }
} }
count, err := GetUserCount(user.Owner, "", "", "") rankingItem := GetAccountItemByName("Ranking", organization)
if err != nil { if rankingItem != nil {
return false, err count, err := GetUserCount(user.Owner, "", "", "")
if err != nil {
return false, err
}
user.Ranking = int(count + 1)
} }
user.Ranking = int(count + 1)
if user.Groups != nil && len(user.Groups) > 0 { if user.Groups != nil && len(user.Groups) > 0 {
_, err = userEnforcer.UpdateGroupsForUser(user.GetId(), user.Groups) _, err = userEnforcer.UpdateGroupsForUser(user.GetId(), user.Groups)
@@ -951,6 +965,14 @@ func DeleteUser(user *User) (bool, error) {
return false, err return false, err
} }
ok, err := userEnforcer.DeleteGroupsForUser(user.GetId())
if err != nil {
return false, err
}
if !ok {
return false, nil
}
organization, err := GetOrganizationByUser(user) organization, err := GetOrganizationByUser(user)
if err != nil { if err != nil {
return false, err return false, err

View File

@@ -57,7 +57,7 @@ type VerificationRecord struct {
Receiver string `xorm:"varchar(100) index notnull" json:"receiver"` Receiver string `xorm:"varchar(100) index notnull" json:"receiver"`
Code string `xorm:"varchar(10) notnull" json:"code"` Code string `xorm:"varchar(10) notnull" json:"code"`
Time int64 `xorm:"notnull" json:"time"` Time int64 `xorm:"notnull" json:"time"`
IsUsed bool IsUsed bool `xorm:"notnull" json:"isUsed"`
} }
func IsAllowSend(user *User, remoteAddr, recordType string) error { func IsAllowSend(user *User, remoteAddr, recordType string) error {

View File

@@ -68,8 +68,10 @@ func handleAccessRequest(w radius.ResponseWriter, r *radius.Request) {
log.Printf("handleAccessRequest() username=%v, org=%v, password=%v", username, organization, password) log.Printf("handleAccessRequest() username=%v, org=%v, password=%v", username, organization, password)
if organization == "" { if organization == "" {
w.Write(r.Response(radius.CodeAccessReject)) organization = conf.GetConfigString("radiusDefaultOrganization")
return if organization == "" {
organization = "built-in"
}
} }
var user *object.User var user *object.User

View File

@@ -16,11 +16,11 @@ package routers
import ( import (
"net/http" "net/http"
"strings"
"github.com/beego/beego/context" "github.com/beego/beego/context"
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
) )
const ( const (
@@ -52,7 +52,13 @@ func CorsFilter(ctx *context.Context) {
origin = "" origin = ""
} }
if strings.HasPrefix(origin, "http://localhost") || strings.HasPrefix(origin, "https://localhost") || strings.HasPrefix(origin, "http://127.0.0.1") || strings.HasPrefix(origin, "http://casdoor-app") || strings.Contains(origin, ".chromiumapp.org") { isValid, err := util.IsValidOrigin(origin)
if err != nil {
ctx.ResponseWriter.WriteHeader(http.StatusForbidden)
responseError(ctx, err.Error())
return
}
if isValid {
setCorsHeaders(ctx, origin) setCorsHeaders(ctx, origin)
return return
} }

View File

@@ -174,6 +174,8 @@ func initAPI() {
beego.Router("/api/get-all-actions", &controllers.ApiController{}, "GET:GetAllActions") beego.Router("/api/get-all-actions", &controllers.ApiController{}, "GET:GetAllActions")
beego.Router("/api/get-all-roles", &controllers.ApiController{}, "GET:GetAllRoles") beego.Router("/api/get-all-roles", &controllers.ApiController{}, "GET:GetAllRoles")
beego.Router("/api/run-casbin-command", &controllers.ApiController{}, "GET:RunCasbinCommand")
beego.Router("/api/get-sessions", &controllers.ApiController{}, "GET:GetSessions") beego.Router("/api/get-sessions", &controllers.ApiController{}, "GET:GetSessions")
beego.Router("/api/get-session", &controllers.ApiController{}, "GET:GetSingleSession") beego.Router("/api/get-session", &controllers.ApiController{}, "GET:GetSingleSession")
beego.Router("/api/update-session", &controllers.ApiController{}, "POST:UpdateSession") beego.Router("/api/update-session", &controllers.ApiController{}, "POST:UpdateSession")

View File

@@ -133,6 +133,14 @@ func StaticFilter(ctx *context.Context) {
path += urlPath path += urlPath
} }
// Preventing synchronization problems from concurrency
ctx.Input.CruSession = nil
organizationThemeCookie, err := appendThemeCookie(ctx, urlPath)
if err != nil {
fmt.Println(err)
}
if strings.Contains(path, "/../") || !util.FileExist(path) { if strings.Contains(path, "/../") || !util.FileExist(path) {
path = webBuildFolder + "/index.html" path = webBuildFolder + "/index.html"
} }
@@ -149,13 +157,13 @@ func StaticFilter(ctx *context.Context) {
} }
if oldStaticBaseUrl == newStaticBaseUrl { if oldStaticBaseUrl == newStaticBaseUrl {
makeGzipResponse(ctx.ResponseWriter, ctx.Request, path) makeGzipResponse(ctx.ResponseWriter, ctx.Request, path, organizationThemeCookie)
} else { } else {
serveFileWithReplace(ctx.ResponseWriter, ctx.Request, path) serveFileWithReplace(ctx.ResponseWriter, ctx.Request, path, organizationThemeCookie)
} }
} }
func serveFileWithReplace(w http.ResponseWriter, r *http.Request, name string) { func serveFileWithReplace(w http.ResponseWriter, r *http.Request, name string, organizationThemeCookie *OrganizationThemeCookie) {
f, err := os.Open(filepath.Clean(name)) f, err := os.Open(filepath.Clean(name))
if err != nil { if err != nil {
panic(err) panic(err)
@@ -168,7 +176,13 @@ func serveFileWithReplace(w http.ResponseWriter, r *http.Request, name string) {
} }
oldContent := util.ReadStringFromPath(name) oldContent := util.ReadStringFromPath(name)
newContent := strings.ReplaceAll(oldContent, oldStaticBaseUrl, newStaticBaseUrl) newContent := oldContent
if organizationThemeCookie != nil {
newContent = strings.ReplaceAll(newContent, "https://cdn.casbin.org/img/favicon.png", organizationThemeCookie.Favicon)
newContent = strings.ReplaceAll(newContent, "<title>Casdoor</title>", fmt.Sprintf("<title>%s</title>", organizationThemeCookie.DisplayName))
}
newContent = strings.ReplaceAll(newContent, oldStaticBaseUrl, newStaticBaseUrl)
http.ServeContent(w, r, d.Name(), d.ModTime(), strings.NewReader(newContent)) http.ServeContent(w, r, d.Name(), d.ModTime(), strings.NewReader(newContent))
} }
@@ -182,14 +196,14 @@ func (w gzipResponseWriter) Write(b []byte) (int, error) {
return w.Writer.Write(b) return w.Writer.Write(b)
} }
func makeGzipResponse(w http.ResponseWriter, r *http.Request, path string) { func makeGzipResponse(w http.ResponseWriter, r *http.Request, path string, organizationThemeCookie *OrganizationThemeCookie) {
if !enableGzip || !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { if !enableGzip || !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
serveFileWithReplace(w, r, path) serveFileWithReplace(w, r, path, organizationThemeCookie)
return return
} }
w.Header().Set("Content-Encoding", "gzip") w.Header().Set("Content-Encoding", "gzip")
gz := gzip.NewWriter(w) gz := gzip.NewWriter(w)
defer gz.Close() defer gz.Close()
gzw := gzipResponseWriter{Writer: gz, ResponseWriter: w} gzw := gzipResponseWriter{Writer: gz, ResponseWriter: w}
serveFileWithReplace(gzw, r, path) serveFileWithReplace(gzw, r, path, organizationThemeCookie)
} }

119
routers/theme_filter.go Normal file
View File

@@ -0,0 +1,119 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package routers
import (
"encoding/json"
"fmt"
"strings"
"github.com/beego/beego/context"
"github.com/casdoor/casdoor/object"
)
type OrganizationThemeCookie struct {
ThemeData *object.ThemeData
LogoUrl string
FooterHtml string
Favicon string
DisplayName string
}
func appendThemeCookie(ctx *context.Context, urlPath string) (*OrganizationThemeCookie, error) {
organizationThemeCookie, err := getOrganizationThemeCookieFromUrlPath(ctx, urlPath)
if err != nil {
return nil, err
}
if organizationThemeCookie != nil {
return organizationThemeCookie, setThemeDataCookie(ctx, organizationThemeCookie)
}
return nil, nil
}
func getOrganizationThemeCookieFromUrlPath(ctx *context.Context, urlPath string) (*OrganizationThemeCookie, error) {
var application *object.Application
var organization *object.Organization
var err error
if urlPath == "/login" {
application, err = object.GetDefaultApplication(fmt.Sprintf("admin/built-in"))
if err != nil {
return nil, err
}
} else if strings.HasPrefix(urlPath, "/login/oauth/authorize") {
clientId := ctx.Input.Query("client_id")
if clientId == "" {
return nil, nil
}
application, err = object.GetApplicationByClientId(clientId)
if err != nil {
return nil, err
}
} else if strings.HasPrefix(urlPath, "/login/saml") {
owner, _ := strings.CutPrefix(urlPath, "/login/saml/authorize/")
application, err = object.GetApplication(owner)
if err != nil {
return nil, err
}
} else if strings.HasPrefix(urlPath, "/login/") {
owner, _ := strings.CutPrefix(urlPath, "/login/")
if owner == "undefined" || strings.Count(owner, "/") > 0 {
return nil, nil
}
application, err = object.GetDefaultApplication(fmt.Sprintf("admin/%s", owner))
if err != nil {
return nil, err
}
} else if strings.HasPrefix(urlPath, "/cas/") && strings.HasSuffix(urlPath, "/login") {
owner, _ := strings.CutPrefix(urlPath, "/cas/")
owner, _ = strings.CutSuffix(owner, "/login")
application, err = object.GetApplication(owner)
if err != nil {
return nil, err
}
}
if application == nil {
return nil, nil
}
organization = application.OrganizationObj
if organization == nil {
organization, err = object.GetOrganization(fmt.Sprintf("admin/%s", application.Organization))
if err != nil {
return nil, err
}
}
organizationThemeCookie := &OrganizationThemeCookie{
application.ThemeData,
application.Logo,
application.FooterHtml,
organization.Favicon,
organization.DisplayName,
}
return organizationThemeCookie, nil
}
func setThemeDataCookie(ctx *context.Context, organizationThemeCookie *OrganizationThemeCookie) error {
themeDataString, err := json.Marshal(organizationThemeCookie.ThemeData)
if err != nil {
return err
}
ctx.SetCookie("organizationTheme", string(themeDataString))
ctx.SetCookie("organizationLogo", organizationThemeCookie.LogoUrl)
ctx.SetCookie("organizationFootHtml", organizationThemeCookie.FooterHtml)
return nil
}

21
storage/cucloud_oss.go Normal file
View File

@@ -0,0 +1,21 @@
package storage
import (
awss3 "github.com/aws/aws-sdk-go/service/s3"
"github.com/casdoor/oss"
"github.com/casdoor/oss/s3"
)
func NewCUCloudOssStorageProvider(clientId string, clientSecret string, region string, bucket string, endpoint string) oss.StorageInterface {
sp := s3.New(&s3.Config{
AccessID: clientId,
AccessKey: clientSecret,
Region: region,
Bucket: bucket,
Endpoint: endpoint,
S3Endpoint: endpoint,
ACL: awss3.BucketCannedACLPublicRead,
})
return sp
}

View File

@@ -23,7 +23,10 @@ func GetStorageProvider(providerType string, clientId string, clientSecret strin
case "AWS S3": case "AWS S3":
return NewAwsS3StorageProvider(clientId, clientSecret, region, bucket, endpoint), nil return NewAwsS3StorageProvider(clientId, clientSecret, region, bucket, endpoint), nil
case "MinIO": case "MinIO":
return NewMinIOS3StorageProvider(clientId, clientSecret, "_", bucket, endpoint), nil if region == "" {
region = "_"
}
return NewMinIOS3StorageProvider(clientId, clientSecret, region, bucket, endpoint), nil
case "Aliyun OSS": case "Aliyun OSS":
return NewAliyunOssStorageProvider(clientId, clientSecret, region, bucket, endpoint), nil return NewAliyunOssStorageProvider(clientId, clientSecret, region, bucket, endpoint), nil
case "Tencent Cloud COS": case "Tencent Cloud COS":
@@ -38,6 +41,8 @@ func GetStorageProvider(providerType string, clientId string, clientSecret strin
return NewSynologyNasStorageProvider(clientId, clientSecret, endpoint), nil return NewSynologyNasStorageProvider(clientId, clientSecret, endpoint), nil
case "Casdoor": case "Casdoor":
return NewCasdoorStorageProvider(providerType, clientId, clientSecret, region, bucket, endpoint, cert, content), nil return NewCasdoorStorageProvider(providerType, clientId, clientSecret, region, bucket, endpoint, cert, content), nil
case "CUCloud OSS":
return NewCUCloudOssStorageProvider(clientId, clientSecret, region, bucket, endpoint), nil
} }
return nil, nil return nil, nil

View File

@@ -7558,6 +7558,9 @@
"type": "integer", "type": "integer",
"format": "int64" "format": "int64"
}, },
"kwai": {
"type": "string"
},
"language": { "language": {
"type": "string" "type": "string"
}, },

View File

@@ -4981,6 +4981,8 @@ definitions:
karma: karma:
type: integer type: integer
format: int64 format: int64
kwai:
type: string
language: language:
type: string type: string
lark: lark:

97
util/process.go Normal file
View File

@@ -0,0 +1,97 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package util
import (
"fmt"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
)
func getPidByPort(port int) (int, error) {
var cmd *exec.Cmd
switch runtime.GOOS {
case "windows":
cmd = exec.Command("cmd", "/c", "netstat -ano | findstr :"+strconv.Itoa(port))
case "darwin", "linux":
cmd = exec.Command("lsof", "-t", "-i", ":"+strconv.Itoa(port))
default:
return 0, fmt.Errorf("unsupported OS: %s", runtime.GOOS)
}
output, err := cmd.Output()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
if exitErr.ExitCode() == 1 {
return 0, nil
}
} else {
return 0, err
}
}
lines := strings.Split(string(output), "\n")
for _, line := range lines {
fields := strings.Fields(line)
if len(fields) > 0 {
if runtime.GOOS == "windows" {
if fields[1] == "0.0.0.0:"+strconv.Itoa(port) {
pid, err := strconv.Atoi(fields[len(fields)-1])
if err != nil {
return 0, err
}
return pid, nil
}
} else {
pid, err := strconv.Atoi(fields[0])
if err != nil {
return 0, err
}
return pid, nil
}
}
}
return 0, nil
}
func StopOldInstance(port int) error {
pid, err := getPidByPort(port)
if err != nil {
return err
}
if pid == 0 {
return nil
}
process, err := os.FindProcess(pid)
if err != nil {
return err
}
err = process.Kill()
if err != nil {
return err
} else {
fmt.Printf("The old instance with pid: %d has been stopped\n", pid)
}
return nil
}

View File

@@ -17,6 +17,7 @@ package util
import ( import (
"fmt" "fmt"
"net/mail" "net/mail"
"net/url"
"regexp" "regexp"
"strings" "strings"
@@ -24,10 +25,11 @@ import (
) )
var ( var (
rePhone *regexp.Regexp rePhone *regexp.Regexp
ReWhiteSpace *regexp.Regexp ReWhiteSpace *regexp.Regexp
ReFieldWhiteList *regexp.Regexp ReFieldWhiteList *regexp.Regexp
ReUserName *regexp.Regexp ReUserName *regexp.Regexp
ReUserNameWithEmail *regexp.Regexp
) )
func init() { func init() {
@@ -35,6 +37,7 @@ func init() {
ReWhiteSpace, _ = regexp.Compile(`\s`) ReWhiteSpace, _ = regexp.Compile(`\s`)
ReFieldWhiteList, _ = regexp.Compile(`^[A-Za-z0-9]+$`) ReFieldWhiteList, _ = regexp.Compile(`^[A-Za-z0-9]+$`)
ReUserName, _ = regexp.Compile("^[a-zA-Z0-9]+([-._][a-zA-Z0-9]+)*$") ReUserName, _ = regexp.Compile("^[a-zA-Z0-9]+([-._][a-zA-Z0-9]+)*$")
ReUserNameWithEmail, _ = regexp.Compile(`^([a-zA-Z0-9]+([-._][a-zA-Z0-9]+)*)|([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$`) // Add support for email formats
} }
func IsEmailValid(email string) bool { func IsEmailValid(email string) bool {
@@ -100,3 +103,21 @@ func GetCountryCode(prefix string, phone string) (string, error) {
func FilterField(field string) bool { func FilterField(field string) bool {
return ReFieldWhiteList.MatchString(field) return ReFieldWhiteList.MatchString(field)
} }
func IsValidOrigin(origin string) (bool, error) {
urlObj, err := url.Parse(origin)
if err != nil {
return false, err
}
if urlObj == nil {
return false, nil
}
originHostOnly := ""
if urlObj.Host != "" {
originHostOnly = fmt.Sprintf("%s://%s", urlObj.Scheme, urlObj.Hostname())
}
res := originHostOnly == "http://localhost" || originHostOnly == "https://localhost" || originHostOnly == "http://127.0.0.1" || originHostOnly == "http://casdoor-app" || strings.HasSuffix(originHostOnly, ".chromiumapp.org")
return res, nil
}

View File

@@ -1,4 +1,5 @@
const CracoLessPlugin = require("craco-less"); const CracoLessPlugin = require("craco-less");
const path = require("path");
module.exports = { module.exports = {
devServer: { devServer: {
@@ -55,47 +56,42 @@ module.exports = {
}, },
], ],
webpack: { webpack: {
configure: { configure: (webpackConfig, { env, paths }) => {
// ignore webpack warnings by source-map-loader paths.appBuild = path.resolve(__dirname, "build-temp");
webpackConfig.output.path = path.resolve(__dirname, "build-temp");
// ignore webpack warnings by source-map-loader
// https://github.com/facebook/create-react-app/pull/11752#issuecomment-1345231546 // https://github.com/facebook/create-react-app/pull/11752#issuecomment-1345231546
ignoreWarnings: [ webpackConfig.ignoreWarnings = [
function ignoreSourcemapsloaderWarnings(warning) { function ignoreSourcemapsloaderWarnings(warning) {
return ( return (
warning.module && warning.module &&
warning.module.resource.includes('node_modules') && warning.module.resource.includes("node_modules") &&
warning.details && warning.details &&
warning.details.includes('source-map-loader') warning.details.includes("source-map-loader")
) );
}, },
], ];
// use polyfill Buffer with Webpack 5 // use polyfill Buffer with Webpack 5
// https://viglucci.io/articles/how-to-polyfill-buffer-with-webpack-5 // https://viglucci.io/articles/how-to-polyfill-buffer-with-webpack-5
// https://craco.js.org/docs/configuration/webpack/ // https://craco.js.org/docs/configuration/webpack/
resolve: { webpackConfig.resolve.fallback = {
fallback: { buffer: require.resolve("buffer/"),
// "process": require.resolve('process/browser'), process: false,
// "util": require.resolve("util/"), util: false,
// "url": require.resolve("url/"), url: false,
// "zlib": require.resolve("browserify-zlib"), zlib: false,
// "stream": require.resolve("stream-browserify"), stream: false,
// "http": require.resolve("stream-http"), http: false,
// "https": require.resolve("https-browserify"), https: false,
// "assert": require.resolve("assert/"), assert: false,
"buffer": require.resolve('buffer/'), crypto: false,
"process": false, os: false,
"util": false, fs: false,
"url": false, };
"zlib": false,
"stream": false, return webpackConfig;
"http": false,
"https": false,
"assert": false,
"buffer": false,
"crypto": false,
"os": false,
"fs": false,
},
}
}, },
} },
}; };

21
web/mv.js Normal file
View File

@@ -0,0 +1,21 @@
const fs = require("fs");
const path = require("path");
const sourceDir = path.join(__dirname, "build-temp");
const targetDir = path.join(__dirname, "build");
if (!fs.existsSync(sourceDir)) {
// eslint-disable-next-line no-console
console.error(`Source directory "${sourceDir}" does not exist.`);
process.exit(1);
}
if (fs.existsSync(targetDir)) {
fs.rmSync(targetDir, {recursive: true, force: true});
// eslint-disable-next-line no-console
console.log(`Target directory "${targetDir}" has been deleted successfully.`);
}
fs.renameSync(sourceDir, targetDir);
// eslint-disable-next-line no-console
console.log(`Renamed "${sourceDir}" to "${targetDir}" successfully.`);

View File

@@ -57,6 +57,7 @@
"scripts": { "scripts": {
"start": "cross-env PORT=7001 craco start", "start": "cross-env PORT=7001 craco start",
"build": "craco build", "build": "craco build",
"postbuild": "node mv.js",
"test": "craco test", "test": "craco test",
"eject": "craco eject", "eject": "craco eject",
"crowdin:sync": "crowdin upload && crowdin download", "crowdin:sync": "crowdin upload && crowdin download",

View File

@@ -36,6 +36,7 @@ const {Footer, Content} = Layout;
import {setTwoToneColor} from "@ant-design/icons"; import {setTwoToneColor} from "@ant-design/icons";
import * as ApplicationBackend from "./backend/ApplicationBackend"; import * as ApplicationBackend from "./backend/ApplicationBackend";
import * as Cookie from "cookie";
setTwoToneColor("rgb(87,52,211)"); setTwoToneColor("rgb(87,52,211)");
@@ -269,7 +270,9 @@ class App extends Component {
}); });
} }
renderFooter() { renderFooter(logo, footerHtml) {
logo = logo ?? this.state.logo;
footerHtml = footerHtml ?? this.state.application?.footerHtml;
return ( return (
<React.Fragment> <React.Fragment>
{!this.state.account ? null : <div style={{display: "none"}} id="CasdoorApplicationName" value={this.state.account.signupApplication} />} {!this.state.account ? null : <div style={{display: "none"}} id="CasdoorApplicationName" value={this.state.account.signupApplication} />}
@@ -280,14 +283,14 @@ class App extends Component {
} }
}> }>
{ {
this.state.application?.footerHtml && this.state.application.footerHtml !== "" ? footerHtml && footerHtml !== "" ?
<React.Fragment> <React.Fragment>
<div dangerouslySetInnerHTML={{__html: this.state.application.footerHtml}} /> <div dangerouslySetInnerHTML={{__html: footerHtml}} />
</React.Fragment> </React.Fragment>
: ( : (
Conf.CustomFooter !== null ? Conf.CustomFooter : ( Conf.CustomFooter !== null ? Conf.CustomFooter : (
<React.Fragment> <React.Fragment>
Powered by <a target="_blank" href="https://casdoor.org" rel="noreferrer"><img style={{paddingBottom: "3px"}} height={"20px"} alt={"Casdoor"} src={this.state.logo} /></a> Powered by <a target="_blank" href="https://casdoor.org" rel="noreferrer"><img style={{paddingBottom: "3px"}} height={"20px"} alt={"Casdoor"} src={logo} /></a>
</React.Fragment> </React.Fragment>
) )
) )
@@ -308,7 +311,7 @@ class App extends Component {
AI Assistant AI Assistant
</a> </a>
</Tooltip> </Tooltip>
<a className="custom-link" style={{float: "right", marginTop: "2px"}} target="_blank" rel="noreferrer" href={"https://ai.casbin.com"}> <a className="custom-link" style={{float: "right", marginTop: "2px"}} target="_blank" rel="noreferrer" href={`${Conf.AiAssistantUrl}`}>
<ShareAltOutlined className="custom-link" style={{fontSize: "20px", color: "rgb(140,140,140)"}} /> <ShareAltOutlined className="custom-link" style={{fontSize: "20px", color: "rgb(140,140,140)"}} />
</a> </a>
<a className="custom-link" style={{float: "right", marginRight: "30px", marginTop: "2px"}} target="_blank" rel="noreferrer" href={"https://github.com/casibase/casibase"}> <a className="custom-link" style={{float: "right", marginRight: "30px", marginTop: "2px"}} target="_blank" rel="noreferrer" href={"https://github.com/casibase/casibase"}>
@@ -326,7 +329,7 @@ class App extends Component {
}} }}
visible={this.state.isAiAssistantOpen} visible={this.state.isAiAssistantOpen}
> >
<iframe id="iframeHelper" title={"iframeHelper"} src={"https://ai.casbin.com/?isRaw=1"} width="100%" height="100%" scrolling="no" frameBorder="no" /> <iframe id="iframeHelper" title={"iframeHelper"} src={`${Conf.AiAssistantUrl}/?isRaw=1`} width="100%" height="100%" scrolling="no" frameBorder="no" />
</Drawer> </Drawer>
); );
} }
@@ -358,13 +361,37 @@ class App extends Component {
} }
}; };
onLoginSuccess(redirectUrl) {
window.google?.accounts?.id?.cancel();
if (redirectUrl) {
localStorage.setItem("mfaRedirectUrl", redirectUrl);
}
this.getAccount();
}
renderPage() { renderPage() {
if (this.isDoorPages()) { if (this.isDoorPages()) {
let themeData = this.state.themeData;
let logo = this.state.logo;
let footerHtml = null;
if (this.state.organization === undefined) {
const curCookie = Cookie.parse(document.cookie);
if (curCookie["organizationTheme"] && curCookie["organizationTheme"] !== "null") {
themeData = JSON.parse(curCookie["organizationTheme"]);
}
if (curCookie["organizationLogo"] && curCookie["organizationLogo"] !== "") {
logo = curCookie["organizationLogo"];
}
if (curCookie["organizationFootHtml"] && curCookie["organizationFootHtml"] !== "") {
footerHtml = curCookie["organizationFootHtml"];
}
}
return ( return (
<ConfigProvider theme={{ <ConfigProvider theme={{
token: { token: {
colorPrimary: this.state.themeData.colorPrimary, colorPrimary: themeData.colorPrimary,
borderRadius: this.state.themeData.borderRadius, borderRadius: themeData.borderRadius,
}, },
algorithm: Setting.getAlgorithm(this.state.themeAlgorithm), algorithm: Setting.getAlgorithm(this.state.themeAlgorithm),
}}> }}>
@@ -382,26 +409,20 @@ class App extends Component {
application: application, application: application,
}); });
}} }}
onLoginSuccess={(redirectUrl) => { onLoginSuccess={(redirectUrl) => {this.onLoginSuccess(redirectUrl);}}
window.google?.accounts?.id?.cancel();
if (redirectUrl) {
localStorage.setItem("mfaRedirectUrl", redirectUrl);
}
this.getAccount();
}}
onUpdateAccount={(account) => this.onUpdateAccount(account)} onUpdateAccount={(account) => this.onUpdateAccount(account)}
updataThemeData={this.setTheme} updataThemeData={this.setTheme}
/> : /> :
<Switch> <Switch>
<Route exact path="/callback" component={AuthCallback} /> <Route exact path="/callback" render={(props) => <AuthCallback {...props} {...this.props} application={this.state.application} onLoginSuccess={(redirectUrl) => {this.onLoginSuccess(redirectUrl);}} />} />
<Route exact path="/callback/saml" component={SamlCallback} /> <Route exact path="/callback/saml" render={(props) => <SamlCallback {...props} {...this.props} application={this.state.application} onLoginSuccess={(redirectUrl) => {this.onLoginSuccess(redirectUrl);}} />} />
<Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")} <Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")}
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>} />} /> extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>} />} />
</Switch> </Switch>
} }
</Content> </Content>
{ {
this.renderFooter() this.renderFooter(logo, footerHtml)
} }
{ {
this.renderAiAssistant() this.renderAiAssistant()

View File

@@ -603,7 +603,7 @@ class ApplicationEditPage extends React.Component {
{Setting.getLabel(i18next.t("general:IP whitelist"), i18next.t("general:IP whitelist - Tooltip"))} : {Setting.getLabel(i18next.t("general:IP whitelist"), i18next.t("general:IP whitelist - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input placeholder = {this.state.application.organizationObj?.ipWhitelist} value={this.state.application.ipWhiteList} onChange={e => { <Input placeholder = {this.state.application.organizationObj?.ipWhitelist} value={this.state.application.ipWhitelist} onChange={e => {
this.updateApplicationField("ipWhitelist", e.target.value); this.updateApplicationField("ipWhitelist", e.target.value);
}} /> }} />
</Col> </Col>
@@ -765,7 +765,7 @@ class ApplicationEditPage extends React.Component {
/> />
<br /> <br />
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => { <Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
copy(`${window.location.origin}/api/saml/metadata?application=admin/${encodeURIComponent(this.state.applicationName)}&post=${this.state.application.enableSamlPostBinding}`); copy(`${window.location.origin}/api/saml/metadata?application=admin/${encodeURIComponent(this.state.applicationName)}&enablePostBinding=${this.state.application.enableSamlPostBinding}`);
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully")); Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}} }}
> >

View File

@@ -19,6 +19,7 @@ import "codemirror/mode/properties/properties";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import IframeEditor from "./IframeEditor"; import IframeEditor from "./IframeEditor";
import {Tabs} from "antd"; import {Tabs} from "antd";
import i18next from "i18next";
const {TabPane} = Tabs; const {TabPane} = Tabs;
@@ -68,8 +69,8 @@ const CasbinEditor = ({model, onModelTextChange}) => {
return ( return (
<div style={{height: "100%", width: "100%", display: "flex", flexDirection: "column"}}> <div style={{height: "100%", width: "100%", display: "flex", flexDirection: "column"}}>
<Tabs activeKey={activeKey} onChange={handleTabChange} style={{flex: "0 0 auto", marginTop: "-10px"}}> <Tabs activeKey={activeKey} onChange={handleTabChange} style={{flex: "0 0 auto", marginTop: "-10px"}}>
<TabPane tab="Basic Editor" key="basic" /> <TabPane tab={i18next.t("model:Basic Editor")} key="basic" />
<TabPane tab="Advanced Editor" key="advanced" /> <TabPane tab={i18next.t("model:Advanced Editor")} key="advanced" />
</Tabs> </Tabs>
<div style={{flex: "1 1 auto", overflow: "hidden"}}> <div style={{flex: "1 1 auto", overflow: "hidden"}}>
{activeKey === "advanced" ? ( {activeKey === "advanced" ? (

View File

@@ -31,3 +31,6 @@ export const ThemeDefault = {
}; };
export const CustomFooter = null; export const CustomFooter = null;
// Blank or null to hide Ai Assistant button
export const AiAssistantUrl = "https://ai.casbin.com";

View File

@@ -111,7 +111,7 @@ class EntryPage extends React.Component {
<div className={`${isDarkMode ? "loginBackgroundDark" : "loginBackground"}`} <div className={`${isDarkMode ? "loginBackgroundDark" : "loginBackground"}`}
style={{backgroundImage: Setting.inIframe() || Setting.isMobile() ? null : `url(${this.state.application?.formBackgroundUrl})`}}> style={{backgroundImage: Setting.inIframe() || Setting.isMobile() ? null : `url(${this.state.application?.formBackgroundUrl})`}}>
<Spin size="large" spinning={this.state.application === undefined && this.state.pricing === undefined} tip={i18next.t("login:Loading")} <Spin size="large" spinning={this.state.application === undefined && this.state.pricing === undefined} tip={i18next.t("login:Loading")}
style={{margin: "0 auto"}} /> style={{width: "100%", margin: "0 auto", position: "absolute"}} />
<Switch> <Switch>
<Route exact path="/signup" render={(props) => this.renderHomeIfLoggedIn(<SignupPage {...this.props} application={this.state.application} applicationName={authConfig.appName} onUpdateApplication={onUpdateApplication} {...props} />)} /> <Route exact path="/signup" render={(props) => this.renderHomeIfLoggedIn(<SignupPage {...this.props} application={this.state.application} applicationName={authConfig.appName} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/signup/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<SignupPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} /> <Route exact path="/signup/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<SignupPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />

View File

@@ -33,18 +33,6 @@ class GroupListPage extends BaseListPage {
} }
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
super.UNSAFE_componentWillMount(); super.UNSAFE_componentWillMount();
this.getGroups(this.state.owner);
}
getGroups(organizationName) {
GroupBackend.getGroups(organizationName)
.then((res) => {
if (res.status === "ok") {
this.setState({
groups: res.data,
});
}
});
} }
newGroup() { newGroup() {
@@ -188,12 +176,8 @@ class GroupListPage extends BaseListPage {
{record.parentId} {record.parentId}
</Link>; </Link>;
} }
const parentGroup = this.state.groups.find((group) => group.name === text); return <Link to={`/groups/${record.owner}/${record.parentId}`}>
if (parentGroup === undefined) { {record?.parentName}
return "";
}
return <Link to={`/groups/${parentGroup.owner}/${parentGroup.name}`}>
{parentGroup?.displayName}
</Link>; </Link>;
}, },
}, },
@@ -215,12 +199,11 @@ class GroupListPage extends BaseListPage {
width: "180px", width: "180px",
fixed: (Setting.isMobile()) ? "false" : "right", fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => { render: (text, record, index) => {
const haveChildren = this.state.groups.find((group) => group.parentId === record.id) !== undefined;
return ( return (
<div> <div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/groups/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button> <Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/groups/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<PopconfirmModal <PopconfirmModal
disabled={haveChildren} disabled={record.haveChildren}
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`} title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
onConfirm={() => this.deleteGroup(index)} onConfirm={() => this.deleteGroup(index)}
> >

View File

@@ -17,6 +17,7 @@ import React, {forwardRef, useEffect, useImperativeHandle, useRef, useState} fro
const IframeEditor = forwardRef(({initialModelText, onModelTextChange}, ref) => { const IframeEditor = forwardRef(({initialModelText, onModelTextChange}, ref) => {
const iframeRef = useRef(null); const iframeRef = useRef(null);
const [iframeReady, setIframeReady] = useState(false); const [iframeReady, setIframeReady] = useState(false);
const currentLang = localStorage.getItem("language") || "en";
useEffect(() => { useEffect(() => {
const handleMessage = (event) => { const handleMessage = (event) => {
@@ -26,24 +27,31 @@ const IframeEditor = forwardRef(({initialModelText, onModelTextChange}, ref) =>
onModelTextChange(event.data.modelText); onModelTextChange(event.data.modelText);
} else if (event.data.type === "iframeReady") { } else if (event.data.type === "iframeReady") {
setIframeReady(true); setIframeReady(true);
iframeRef.current?.contentWindow.postMessage({ if (initialModelText && iframeRef.current?.contentWindow) {
type: "initializeModel", iframeRef.current.contentWindow.postMessage({
modelText: initialModelText, type: "initializeModel",
}, "*"); modelText: initialModelText,
lang: currentLang,
}, "*");
}
} }
}; };
window.addEventListener("message", handleMessage); window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage); return () => window.removeEventListener("message", handleMessage);
}, [onModelTextChange, initialModelText]); }, [onModelTextChange, initialModelText, currentLang]);
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
getModelText: () => { getModelText: () => {
iframeRef.current?.contentWindow.postMessage({type: "getModelText"}, "*"); if (iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.postMessage({
type: "getModelText",
}, "*");
}
}, },
updateModelText: (newModelText) => { updateModelText: (newModelText) => {
if (iframeReady) { if (iframeReady && iframeRef.current?.contentWindow) {
iframeRef.current?.contentWindow.postMessage({ iframeRef.current.contentWindow.postMessage({
type: "updateModelText", type: "updateModelText",
modelText: newModelText, modelText: newModelText,
}, "*"); }, "*");
@@ -54,7 +62,7 @@ const IframeEditor = forwardRef(({initialModelText, onModelTextChange}, ref) =>
return ( return (
<iframe <iframe
ref={iframeRef} ref={iframeRef}
src="https://editor.casbin.org/model-editor" src={`https://editor.casbin.org/model-editor?lang=${currentLang}`}
frameBorder="0" frameBorder="0"
width="100%" width="100%"
height="500px" height="500px"

View File

@@ -106,6 +106,22 @@ class InvitationEditPage extends React.Component {
}); });
} }
copySignupLink() {
let defaultApplication;
if (this.state.invitation.owner === "built-in") {
defaultApplication = "app-built-in";
} else {
const selectedOrganization = Setting.getArrayItem(this.state.organizations, "name", this.state.invitation.owner);
defaultApplication = selectedOrganization.defaultApplication;
if (!defaultApplication) {
Setting.showMessage("error", i18next.t("invitation:You need to specify a default application for ") + selectedOrganization.name);
return;
}
}
copy(`${window.location.origin}/signup/${defaultApplication}?invitationCode=${this.state.invitation?.defaultCode}`);
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}
renderInvitation() { renderInvitation() {
const isCreatedByPlan = this.state.invitation.tag === "auto_created_invitation_for_plan"; const isCreatedByPlan = this.state.invitation.tag === "auto_created_invitation_for_plan";
return ( return (
@@ -114,16 +130,7 @@ class InvitationEditPage extends React.Component {
{this.state.mode === "add" ? i18next.t("invitation:New Invitation") : i18next.t("invitation:Edit Invitation")}&nbsp;&nbsp;&nbsp;&nbsp; {this.state.mode === "add" ? i18next.t("invitation:New Invitation") : i18next.t("invitation:Edit Invitation")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitInvitationEdit(false)}>{i18next.t("general:Save")}</Button> <Button onClick={() => this.submitInvitationEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitInvitationEdit(true)}>{i18next.t("general:Save & Exit")}</Button> <Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitInvitationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
<Button style={{marginLeft: "20px"}} onClick={() => { <Button style={{marginLeft: "20px"}} onClick={_ => this.copySignupLink()}>
let defaultApplication;
if (this.state.invitation.owner === "built-in") {
defaultApplication = "app-built-in";
} else {
defaultApplication = Setting.getArrayItem(this.state.organizations, "name", this.state.invitation.owner).defaultApplication;
}
copy(`${window.location.origin}/signup/${defaultApplication}?invitationCode=${this.state.invitation?.defaultCode}`);
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}}>
{i18next.t("application:Copy signup page URL")} {i18next.t("application:Copy signup page URL")}
</Button> </Button>
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteInvitation()}>{i18next.t("general:Cancel")}</Button> : null} {this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteInvitation()}>{i18next.t("general:Cancel")}</Button> : null}
@@ -330,16 +337,7 @@ class InvitationEditPage extends React.Component {
<div style={{marginTop: "20px", marginLeft: "40px"}}> <div style={{marginTop: "20px", marginLeft: "40px"}}>
<Button size="large" onClick={() => this.submitInvitationEdit(false)}>{i18next.t("general:Save")}</Button> <Button size="large" onClick={() => this.submitInvitationEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitInvitationEdit(true)}>{i18next.t("general:Save & Exit")}</Button> <Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitInvitationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
<Button style={{marginLeft: "20px"}} size="large" onClick={() => { <Button style={{marginLeft: "20px"}} size="large" onClick={_ => this.copySignupLink()}>
let defaultApplication;
if (this.state.invitation.owner === "built-in") {
defaultApplication = "app-built-in";
} else {
defaultApplication = Setting.getArrayItem(this.state.organizations, "name", this.state.invitation.owner).defaultApplication;
}
copy(`${window.location.origin}/signup/${defaultApplication}?invitationCode=${this.state.invitation?.defaultCode}`);
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}}>
{i18next.t("application:Copy signup page URL")} {i18next.t("application:Copy signup page URL")}
</Button> </Button>
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteInvitation()}>{i18next.t("general:Cancel")}</Button> : null} {this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteInvitation()}>{i18next.t("general:Cancel")}</Button> : null}

View File

@@ -228,6 +228,21 @@ class LdapEditPage extends React.Component {
/> />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("general:Password type"), i18next.t("general:Password type - Tooltip"))} :
</Col>
<Col span={21}>
<Select virtual={false} style={{width: "100%"}} value={this.state.ldap.passwordType ?? []} onChange={(value => {
this.updateLdapField("passwordType", value);
})}
>
<Option key={"Plain"} value={"Plain"}>{i18next.t("general:Plain")}</Option>
<Option key={"SSHA"} value={"SSHA"} >SSHA</Option>
<Option key={"MD5"} value={"MD5"} >MD5</Option>
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}> <Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("ldap:Default group"), i18next.t("ldap:Default group - Tooltip"))} : {Setting.getLabel(i18next.t("ldap:Default group"), i18next.t("ldap:Default group - Tooltip"))} :

View File

@@ -192,17 +192,21 @@ function ManagementPage(props) {
themeAlgorithm={props.themeAlgorithm} themeAlgorithm={props.themeAlgorithm}
onChange={props.setLogoAndThemeAlgorithm} /> onChange={props.setLogoAndThemeAlgorithm} />
<LanguageSelect languages={props.account.organization.languages} /> <LanguageSelect languages={props.account.organization.languages} />
<Tooltip title="Click to open AI assitant"> {
<div className="select-box" onClick={props.openAiAssistant}> Conf.AiAssistantUrl?.trim() && (
<DeploymentUnitOutlined style={{fontSize: "24px"}} /> <Tooltip title="Click to open AI assistant">
</div> <div className="select-box" onClick={props.openAiAssistant}>
</Tooltip> <DeploymentUnitOutlined style={{fontSize: "24px"}} />
</div>
</Tooltip>
)
}
<OpenTour /> <OpenTour />
{Setting.isAdminUser(props.account) && !Setting.isMobile() && (props.uri.indexOf("/trees") === -1) && {Setting.isAdminUser(props.account) && (props.uri.indexOf("/trees") === -1) &&
<OrganizationSelect <OrganizationSelect
initValue={Setting.getOrganization()} initValue={Setting.getOrganization()}
withAll={true} withAll={true}
style={{marginRight: "20px", width: "180px", display: "flex"}} style={{marginRight: "20px", width: "180px", display: !Setting.isMobile() ? "flex" : "none"}}
onChange={(value) => { onChange={(value) => {
Setting.setOrganization(value); Setting.setOrganization(value);
}} }}

View File

@@ -339,6 +339,16 @@ class OrganizationEditPage extends React.Component {
</Col> </Col>
</Row>) </Row>)
} }
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("organization:Password expire days"), i18next.t("organization:Password expire days - Tooltip"))} :
</Col>
<Col span={4} >
<InputNumber value={this.state.organization.passwordExpireDays} onChange={value => {
this.updateOrganizationField("passwordExpireDays", value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Supported country codes"), i18next.t("general:Supported country codes - Tooltip"))} : {Setting.getLabel(i18next.t("general:Supported country codes"), i18next.t("general:Supported country codes - Tooltip"))} :

View File

@@ -37,6 +37,7 @@ class OrganizationListPage extends BaseListPage {
passwordOptions: [], passwordOptions: [],
passwordObfuscatorType: "Plain", passwordObfuscatorType: "Plain",
passwordObfuscatorKey: "", passwordObfuscatorKey: "",
passwordExpireDays: 0,
countryCodes: ["US"], countryCodes: ["US"],
defaultAvatar: `${Setting.StaticBaseUrl}/img/casbin.svg`, defaultAvatar: `${Setting.StaticBaseUrl}/img/casbin.svg`,
defaultApplication: "", defaultApplication: "",

View File

@@ -123,6 +123,22 @@ class ProductBuyPage extends React.Component {
return "$"; return "$";
} else if (product?.currency === "CNY") { } else if (product?.currency === "CNY") {
return "¥"; return "¥";
} else if (product?.currency === "EUR") {
return "€";
} else if (product?.currency === "JPY") {
return "¥";
} else if (product?.currency === "GBP") {
return "£";
} else if (product?.currency === "AUD") {
return "A$";
} else if (product?.currency === "CAD") {
return "C$";
} else if (product?.currency === "CHF") {
return "CHF";
} else if (product?.currency === "HKD") {
return "HK$";
} else if (product?.currency === "SGD") {
return "S$";
} else { } else {
return "(Unknown currency)"; return "(Unknown currency)";
} }

View File

@@ -209,6 +209,14 @@ class ProductEditPage extends React.Component {
[ [
{id: "USD", name: "USD"}, {id: "USD", name: "USD"},
{id: "CNY", name: "CNY"}, {id: "CNY", name: "CNY"},
{id: "EUR", name: "EUR"},
{id: "JPY", name: "JPY"},
{id: "GBP", name: "GBP"},
{id: "AUD", name: "AUD"},
{id: "CAD", name: "CAD"},
{id: "CHF", name: "CHF"},
{id: "HKD", name: "HKD"},
{id: "SGD", name: "SGD"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>) ].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
} }
</Select> </Select>

View File

@@ -297,6 +297,8 @@ class ProviderEditPage extends React.Component {
return Setting.getLabel(i18next.t("provider:Scene"), i18next.t("provider:Scene - Tooltip")); return Setting.getLabel(i18next.t("provider:Scene"), i18next.t("provider:Scene - Tooltip"));
} else if (provider.type === "WeChat Pay") { } else if (provider.type === "WeChat Pay") {
return Setting.getLabel(i18next.t("provider:App ID"), i18next.t("provider:App ID - Tooltip")); return Setting.getLabel(i18next.t("provider:App ID"), i18next.t("provider:App ID - Tooltip"));
} else if (provider.type === "CUCloud") {
return Setting.getLabel(i18next.t("provider:Account ID"), i18next.t("provider:Account ID - Tooltip"));
} else { } else {
return Setting.getLabel(i18next.t("provider:Client ID 2"), i18next.t("provider:Client ID 2 - Tooltip")); return Setting.getLabel(i18next.t("provider:Client ID 2"), i18next.t("provider:Client ID 2 - Tooltip"));
} }
@@ -393,6 +395,9 @@ class ProviderEditPage extends React.Component {
} else if (provider.type === "Line" || provider.type === "Matrix" || provider.type === "Rocket Chat") { } else if (provider.type === "Line" || provider.type === "Matrix" || provider.type === "Rocket Chat") {
text = i18next.t("provider:App Key"); text = i18next.t("provider:App Key");
tooltip = i18next.t("provider:App Key - Tooltip"); tooltip = i18next.t("provider:App Key - Tooltip");
} else if (provider.type === "CUCloud") {
text = i18next.t("provider:Topic name");
tooltip = i18next.t("provider:Topic name - Tooltip");
} }
} }
@@ -633,6 +638,20 @@ class ProviderEditPage extends React.Component {
</React.Fragment> </React.Fragment>
) )
} }
{
this.state.provider.category === "OAuth" ? (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Email regex"), i18next.t("provider:Email regex - Tooltip"))} :
</Col>
<Col span={22}>
<TextArea rows={4} value={this.state.provider.emailRegex} onChange={e => {
this.updateProviderField("emailRegex", e.target.value);
}} />
</Col>
</Row>
) : null
}
{ {
this.state.provider.type === "Custom" ? ( this.state.provider.type === "Custom" ? (
<React.Fragment> <React.Fragment>
@@ -757,7 +776,7 @@ class ProviderEditPage extends React.Component {
) )
} }
{ {
this.state.provider.category !== "Email" && this.state.provider.type !== "WeChat" && this.state.provider.type !== "Apple" && this.state.provider.type !== "Aliyun Captcha" && this.state.provider.type !== "WeChat Pay" && this.state.provider.type !== "Twitter" && this.state.provider.type !== "Reddit" ? null : ( this.state.provider.category !== "Email" && this.state.provider.type !== "WeChat" && this.state.provider.type !== "Apple" && this.state.provider.type !== "Aliyun Captcha" && this.state.provider.type !== "WeChat Pay" && this.state.provider.type !== "Twitter" && this.state.provider.type !== "Reddit" && this.state.provider.type !== "CUCloud" ? null : (
<React.Fragment> <React.Fragment>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
@@ -770,7 +789,7 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
{ {
(this.state.provider.type === "WeChat Pay") || (this.state.provider.category === "Email" && (this.state.provider.type === "Azure ACS" || this.state.provider.type === "SendGrid")) ? null : ( (this.state.provider.type === "WeChat Pay" || this.state.provider.type === "CUCloud") || (this.state.provider.category === "Email" && (this.state.provider.type === "Azure ACS" || this.state.provider.type === "SendGrid")) ? null : (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{this.getClientSecret2Label(this.state.provider)} : {this.getClientSecret2Label(this.state.provider)} :
@@ -856,9 +875,9 @@ class ProviderEditPage extends React.Component {
</Row> </Row>
) )
} }
{this.state.provider.category === "Storage" || ["Custom HTTP SMS", "Custom HTTP Email"].includes(this.state.provider.type) ? ( {this.state.provider.category === "Storage" || ["Custom HTTP SMS", "Custom HTTP Email", "CUCloud"].includes(this.state.provider.type) ? (
<div> <div>
{["Local File System"].includes(this.state.provider.type) ? null : ( {["Local File System", "CUCloud"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}> <Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Endpoint"), i18next.t("provider:Region endpoint for Internet"))} : {Setting.getLabel(i18next.t("provider:Endpoint"), i18next.t("provider:Region endpoint for Internet"))} :
@@ -870,7 +889,7 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
)} )}
{["Custom HTTP SMS", "Local File System", "MinIO", "Tencent Cloud COS", "Google Cloud Storage", "Qiniu Cloud Kodo", "Synology", "Casdoor"].includes(this.state.provider.type) ? null : ( {["Custom HTTP SMS", "Local File System", "MinIO", "Tencent Cloud COS", "Google Cloud Storage", "Qiniu Cloud Kodo", "Synology", "Casdoor", "CUCloud"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}> <Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Endpoint (Intranet)"), i18next.t("provider:Region endpoint for Intranet"))} : {Setting.getLabel(i18next.t("provider:Endpoint (Intranet)"), i18next.t("provider:Region endpoint for Intranet"))} :
@@ -882,7 +901,7 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
)} )}
{["Custom HTTP SMS", "Local File System"].includes(this.state.provider.type) ? null : ( {["Custom HTTP SMS", "Local File System", "CUCloud"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}> <Col style={{marginTop: "5px"}} span={2}>
{["Casdoor"].includes(this.state.provider.type) ? {["Casdoor"].includes(this.state.provider.type) ?
@@ -896,7 +915,7 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
)} )}
{["Custom HTTP SMS"].includes(this.state.provider.type) ? null : ( {["Custom HTTP SMS", "CUCloud"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}> <Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Path prefix"), i18next.t("provider:Path prefix - Tooltip"))} : {Setting.getLabel(i18next.t("provider:Path prefix"), i18next.t("provider:Path prefix - Tooltip"))} :
@@ -908,7 +927,7 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
)} )}
{["Custom HTTP SMS", "Qiniu Cloud Kodo", "Synology", "Casdoor"].includes(this.state.provider.type) ? null : ( {["Custom HTTP SMS", "Synology", "Casdoor", "CUCloud"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}> <Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} : {Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
@@ -932,7 +951,7 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
) : null} ) : null}
{["AWS S3", "Tencent Cloud COS", "Qiniu Cloud Kodo", "Casdoor"].includes(this.state.provider.type) ? ( {["AWS S3", "Tencent Cloud COS", "Qiniu Cloud Kodo", "Casdoor", "CUCloud OSS", "MinIO", "CUCloud"].includes(this.state.provider.type) ? (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}> <Col style={{marginTop: "5px"}} span={2}>
{["Casdoor"].includes(this.state.provider.type) ? {["Casdoor"].includes(this.state.provider.type) ?
@@ -971,7 +990,7 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
) : null} ) : null}
{["Custom HTTP"].includes(this.state.provider.type) ? ( {["Custom HTTP", "CUCloud"].includes(this.state.provider.type) ? (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Parameter"), i18next.t("provider:Parameter - Tooltip"))} : {Setting.getLabel(i18next.t("provider:Parameter"), i18next.t("provider:Parameter - Tooltip"))} :
@@ -983,7 +1002,7 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
) : null} ) : null}
{["Google Chat"].includes(this.state.provider.type) ? ( {["Google Chat", "CUCloud"].includes(this.state.provider.type) ? (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Metadata"), i18next.t("provider:Metadata - Tooltip"))} : {Setting.getLabel(i18next.t("provider:Metadata"), i18next.t("provider:Metadata - Tooltip"))} :

View File

@@ -187,7 +187,7 @@ class RoleEditPage extends React.Component {
{Setting.getLabel(i18next.t("role:Sub users"), i18next.t("role:Sub users - Tooltip"))} : {Setting.getLabel(i18next.t("role:Sub users"), i18next.t("role:Sub users - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} mode="multiple" style={{width: "100%"}} value={this.state.role.users} <Select virtual={true} mode="multiple" style={{width: "100%"}} value={this.state.role.users}
onChange={(value => {this.updateRoleField("users", value);})} onChange={(value => {this.updateRoleField("users", value);})}
options={this.state.users.map((user) => Setting.getOption(`${user.owner}/${user.name}`, `${user.owner}/${user.name}`))} options={this.state.users.map((user) => Setting.getOption(`${user.owner}/${user.name}`, `${user.owner}/${user.name}`))}
/> />

View File

@@ -14,7 +14,7 @@
import React from "react"; import React from "react";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import {Select, Tag, Tooltip, message, theme} from "antd"; import {Button, Select, Tag, Tooltip, message, theme} from "antd";
import {QuestionCircleTwoTone} from "@ant-design/icons"; import {QuestionCircleTwoTone} from "@ant-design/icons";
import {isMobile as isMobileDevice} from "react-device-detect"; import {isMobile as isMobileDevice} from "react-device-detect";
import "./i18n"; import "./i18n";
@@ -25,6 +25,8 @@ import {Helmet} from "react-helmet";
import * as Conf from "./Conf"; import * as Conf from "./Conf";
import * as phoneNumber from "libphonenumber-js"; import * as phoneNumber from "libphonenumber-js";
import moment from "moment"; import moment from "moment";
import {MfaAuthVerifyForm, NextMfa, RequiredMfa} from "./auth/mfa/MfaAuthVerifyForm";
import {EmailMfaType, SmsMfaType, TotpMfaType} from "./auth/MfaSetupPage";
const {Option} = Select; const {Option} = Select;
@@ -233,6 +235,10 @@ export const OtherProviderInfo = {
logo: `${StaticBaseUrl}/img/casdoor.png`, logo: `${StaticBaseUrl}/img/casdoor.png`,
url: "https://casdoor.org/docs/provider/storage/overview", url: "https://casdoor.org/docs/provider/storage/overview",
}, },
"CUCloud OSS": {
logo: `${StaticBaseUrl}/img/social_cucloud.png`,
url: "https://www.cucloud.cn/product/oss.html",
},
}, },
SAML: { SAML: {
"Aliyun IDaaS": { "Aliyun IDaaS": {
@@ -401,6 +407,10 @@ export const OtherProviderInfo = {
logo: `${StaticBaseUrl}/img/social_viber.png`, logo: `${StaticBaseUrl}/img/social_viber.png`,
url: "https://www.viber.com/", url: "https://www.viber.com/",
}, },
"CUCloud": {
logo: `${StaticBaseUrl}/img/cucloud.png`,
url: "https://www.cucloud.cn/",
},
}, },
}; };
@@ -920,7 +930,7 @@ export function getClickable(text) {
return ( return (
<a onClick={() => { <a onClick={() => {
copy(text); copy(text);
showMessage("success", "Copied to clipboard"); showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}}> }}>
{text} {text}
</a> </a>
@@ -981,6 +991,7 @@ export function getProviderTypeOptions(category) {
{id: "Bilibili", name: "Bilibili"}, {id: "Bilibili", name: "Bilibili"},
{id: "Okta", name: "Okta"}, {id: "Okta", name: "Okta"},
{id: "Douyin", name: "Douyin"}, {id: "Douyin", name: "Douyin"},
{id: "Kwai", name: "Kwai"},
{id: "Line", name: "Line"}, {id: "Line", name: "Line"},
{id: "Amazon", name: "Amazon"}, {id: "Amazon", name: "Amazon"},
{id: "Auth0", name: "Auth0"}, {id: "Auth0", name: "Auth0"},
@@ -1078,6 +1089,7 @@ export function getProviderTypeOptions(category) {
{id: "Google Cloud Storage", name: "Google Cloud Storage"}, {id: "Google Cloud Storage", name: "Google Cloud Storage"},
{id: "Synology", name: "Synology"}, {id: "Synology", name: "Synology"},
{id: "Casdoor", name: "Casdoor"}, {id: "Casdoor", name: "Casdoor"},
{id: "CUCloud OSS", name: "CUCloud OSS"},
] ]
); );
} else if (category === "SAML") { } else if (category === "SAML") {
@@ -1131,6 +1143,7 @@ export function getProviderTypeOptions(category) {
{id: "Reddit", name: "Reddit"}, {id: "Reddit", name: "Reddit"},
{id: "Rocket Chat", name: "Rocket Chat"}, {id: "Rocket Chat", name: "Rocket Chat"},
{id: "Viber", name: "Viber"}, {id: "Viber", name: "Viber"},
{id: "CUCloud", name: "CUCloud"},
]); ]);
} else { } else {
return []; return [];
@@ -1171,7 +1184,7 @@ export function renderLogo(application) {
function isSigninMethodEnabled(application, signinMethod) { function isSigninMethodEnabled(application, signinMethod) {
if (application && application.signinMethods) { if (application && application.signinMethods) {
return application.signinMethods.filter(item => item.name === signinMethod && item.rule !== "Hide-Password").length > 0; return application.signinMethods.filter(item => item.name === signinMethod && item.rule !== "Hide password").length > 0;
} else { } else {
return false; return false;
} }
@@ -1550,9 +1563,25 @@ export function getDefaultHtmlEmailContent() {
export function getCurrencyText(product) { export function getCurrencyText(product) {
if (product?.currency === "USD") { if (product?.currency === "USD") {
return i18next.t("product:USD"); return i18next.t("currency:USD");
} else if (product?.currency === "CNY") { } else if (product?.currency === "CNY") {
return i18next.t("product:CNY"); return i18next.t("currency:CNY");
} else if (product?.currency === "EUR") {
return i18next.t("currency:EUR");
} else if (product?.currency === "JPY") {
return i18next.t("currency:JPY");
} else if (product?.currency === "GBP") {
return i18next.t("currency:GBP");
} else if (product?.currency === "AUD") {
return i18next.t("currency:AUD");
} else if (product?.currency === "CAD") {
return i18next.t("currency:CAD");
} else if (product?.currency === "CHF") {
return i18next.t("currency:CHF");
} else if (product?.currency === "HKD") {
return i18next.t("currency:HKD");
} else if (product?.currency === "SGD") {
return i18next.t("currency:SGD");
} else { } else {
return "(Unknown currency)"; return "(Unknown currency)";
} }
@@ -1561,3 +1590,114 @@ export function getCurrencyText(product) {
export function isDarkTheme(themeAlgorithm) { export function isDarkTheme(themeAlgorithm) {
return themeAlgorithm && themeAlgorithm.includes("dark"); return themeAlgorithm && themeAlgorithm.includes("dark");
} }
function getPreferredMfaProp(mfaProps) {
for (const i in mfaProps) {
if (mfaProps[i].isPreffered) {
return mfaProps[i];
}
}
return mfaProps[0];
}
export function checkLoginMfa(res, body, params, handleLogin, componentThis, requireRedirect = null) {
if (res.data === RequiredMfa) {
if (!requireRedirect) {
componentThis.props.onLoginSuccess(window.location.href);
} else {
componentThis.props.onLoginSuccess(requireRedirect);
}
} else if (res.data === NextMfa) {
componentThis.setState({
mfaProps: res.data2,
selectedMfaProp: getPreferredMfaProp(res.data2),
}, () => {
body["providerBack"] = body["provider"];
body["provider"] = "";
componentThis.setState({
getVerifyTotp: () => renderMfaAuthVerifyForm(body, params, handleLogin, componentThis),
});
});
} else if (res.data === "SelectPlan") {
// paid-user does not have active or pending subscription, go to application default pricing page to select-plan
const pricing = res.data2;
goToLink(`/select-plan/${pricing.owner}/${pricing.name}?user=${body.username}`);
} else if (res.data === "BuyPlanResult") {
// paid-user has pending subscription, go to buy-plan/result apge to notify payment result
const sub = res.data2;
goToLink(`/buy-plan/${sub.owner}/${sub.pricing}/result?subscription=${sub.name}`);
} else {
handleLogin(res);
}
}
export function getApplicationObj(componentThis) {
return componentThis.props.application;
}
export function parseOffset(offset) {
if (offset === 2 || offset === 4 || inIframe() || isMobile()) {
return "0 auto";
}
if (offset === 1) {
return "0 10%";
}
if (offset === 3) {
return "0 60%";
}
}
function renderMfaAuthVerifyForm(values, authParams, onSuccess, componentThis) {
return (
<div>
<MfaAuthVerifyForm
mfaProps={componentThis.state.selectedMfaProp}
formValues={values}
authParams={authParams}
application={getApplicationObj(componentThis)}
onFail={(errorMessage) => {
showMessage("error", errorMessage);
}}
onSuccess={(res) => onSuccess(res)}
/>
<div>
{
componentThis.state.mfaProps.map((mfa) => {
if (componentThis.state.selectedMfaProp.mfaType === mfa.mfaType) {return null;}
let mfaI18n = "";
switch (mfa.mfaType) {
case SmsMfaType: mfaI18n = i18next.t("mfa:Use SMS"); break;
case TotpMfaType: mfaI18n = i18next.t("mfa:Use Authenticator App"); break ;
case EmailMfaType: mfaI18n = i18next.t("mfa:Use Email") ;break;
}
return <div key={mfa.mfaType}><Button type={"link"} onClick={() => {
componentThis.setState({
selectedMfaProp: mfa,
});
}}>{mfaI18n}</Button></div>;
})
}
</div>
</div>);
}
export function renderLoginPanel(application, getInnerComponent, componentThis) {
return (
<div className="login-content" style={{margin: componentThis.props.preview ?? parseOffset(application.formOffset)}}>
{inIframe() || isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCss}} />}
{inIframe() || !isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCssMobile}} />}
<div className={isDarkTheme(componentThis.props.themeAlgorithm) ? "login-panel-dark" : "login-panel"}>
<div className="side-image" style={{display: application.formOffset !== 4 ? "none" : null}}>
<div dangerouslySetInnerHTML={{__html: application.formSideHtml}} />
</div>
<div className="login-form">
<div>
{
getInnerComponent()
}
</div>
</div>
</div>
</div>
);
}

View File

@@ -1009,6 +1009,19 @@ class UserEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
); );
} else if (accountItem.name === "Last change password time") {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Last change password time"), i18next.t("user:Last change password time"))} :
</Col>
<Col span={22}>
<Input value={this.state.user.lastChangePasswordTime} onChange={e => {
this.updateUserField("lastChangePasswordTime", e.target.value);
}} />
</Col>
</Row>
);
} else if (accountItem.name === "Managed accounts") { } else if (accountItem.name === "Managed accounts") {
return ( return (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >

View File

@@ -21,6 +21,7 @@ import {authConfig} from "./Auth";
import * as Setting from "../Setting"; import * as Setting from "../Setting";
import i18next from "i18next"; import i18next from "i18next";
import RedirectForm from "../common/RedirectForm"; import RedirectForm from "../common/RedirectForm";
import {renderLoginPanel} from "../Setting";
class AuthCallback extends React.Component { class AuthCallback extends React.Component {
constructor(props) { constructor(props) {
@@ -131,19 +132,23 @@ class AuthCallback extends React.Component {
// user is using casdoor as cas sso server, and wants the ticket to be acquired // user is using casdoor as cas sso server, and wants the ticket to be acquired
AuthBackend.loginCas(body, {"service": casService}).then((res) => { AuthBackend.loginCas(body, {"service": casService}).then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
let msg = "Logged in successfully."; const handleCasLogin = (res) => {
if (casService === "") { let msg = "Logged in successfully.";
// If service was not specified, Casdoor must display a message notifying the client that it has successfully initiated a single sign-on session. if (casService === "") {
msg += "Now you can visit apps protected by Casdoor."; // If service was not specified, Casdoor must display a message notifying the client that it has successfully initiated a single sign-on session.
} msg += "Now you can visit apps protected by Casdoor.";
Setting.showMessage("success", msg); }
Setting.showMessage("success", msg);
if (casService !== "") { if (casService !== "") {
const st = res.data; const st = res.data;
const newUrl = new URL(casService); const newUrl = new URL(casService);
newUrl.searchParams.append("ticket", st); newUrl.searchParams.append("ticket", st);
window.location.href = newUrl.toString(); window.location.href = newUrl.toString();
} }
};
Setting.checkLoginMfa(res, body, {"service": casService}, handleCasLogin, this);
} else { } else {
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
} }
@@ -159,54 +164,58 @@ class AuthCallback extends React.Component {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
const responseType = this.getResponseType(); const responseType = this.getResponseType();
if (responseType === "login") { const handleLogin = (res) => {
if (res.data2) { if (responseType === "login") {
sessionStorage.setItem("signinUrl", signinUrl); if (res.data2) {
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
return;
}
Setting.showMessage("success", "Logged in successfully");
// Setting.goToLinkSoft(this, "/");
const link = Setting.getFromLink();
Setting.goToLink(link);
} else if (responseType === "code") {
if (res.data2) {
sessionStorage.setItem("signinUrl", signinUrl);
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
return;
}
const code = res.data;
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
// Setting.showMessage("success", `Authorization code: ${res.data}`);
} else if (responseType === "token" || responseType === "id_token") {
if (res.data2) {
sessionStorage.setItem("signinUrl", signinUrl);
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
return;
}
const token = res.data;
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}${responseType}=${token}&state=${oAuthParams.state}&token_type=bearer`);
} else if (responseType === "link") {
const from = innerParams.get("from");
Setting.goToLinkSoftOrJumpSelf(this, from);
} else if (responseType === "saml") {
if (res.data2.method === "POST") {
this.setState({
samlResponse: res.data,
redirectUrl: res.data2.redirectUrl,
relayState: oAuthParams.relayState,
});
} else {
if (res.data2.needUpdatePassword) {
sessionStorage.setItem("signinUrl", signinUrl); sessionStorage.setItem("signinUrl", signinUrl);
Setting.goToLinkSoft(this, `/forget/${applicationName}`); Setting.goToLinkSoft(this, `/forget/${applicationName}`);
return; return;
} }
const SAMLResponse = res.data; Setting.showMessage("success", "Logged in successfully");
const redirectUri = res.data2.redirectUrl; // Setting.goToLinkSoft(this, "/");
Setting.goToLink(`${redirectUri}?SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`); const link = Setting.getFromLink();
Setting.goToLink(link);
} else if (responseType === "code") {
if (res.data2) {
sessionStorage.setItem("signinUrl", signinUrl);
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
return;
}
const code = res.data;
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
// Setting.showMessage("success", `Authorization code: ${res.data}`);
} else if (responseType === "token" || responseType === "id_token") {
if (res.data2) {
sessionStorage.setItem("signinUrl", signinUrl);
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
return;
}
const token = res.data;
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}${responseType}=${token}&state=${oAuthParams.state}&token_type=bearer`);
} else if (responseType === "link") {
const from = innerParams.get("from");
Setting.goToLinkSoftOrJumpSelf(this, from);
} else if (responseType === "saml") {
if (res.data2.method === "POST") {
this.setState({
samlResponse: res.data,
redirectUrl: res.data2.redirectUrl,
relayState: oAuthParams.relayState,
});
} else {
if (res.data2.needUpdatePassword) {
sessionStorage.setItem("signinUrl", signinUrl);
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
return;
}
const SAMLResponse = res.data;
const redirectUri = res.data2.redirectUrl;
Setting.goToLink(`${redirectUri}${redirectUri.includes("?") ? "&" : "?"}SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);
}
} }
} };
Setting.checkLoginMfa(res, body, oAuthParams, handleLogin, this, window.location.origin);
} else { } else {
this.setState({ this.setState({
msg: res.msg, msg: res.msg,
@@ -220,6 +229,11 @@ class AuthCallback extends React.Component {
return <RedirectForm samlResponse={this.state.samlResponse} redirectUrl={this.state.redirectUrl} relayState={this.state.relayState} />; return <RedirectForm samlResponse={this.state.samlResponse} redirectUrl={this.state.redirectUrl} relayState={this.state.relayState} />;
} }
if (this.state.getVerifyTotp !== undefined) {
const application = Setting.getApplicationObj(this);
return renderLoginPanel(application, this.state.getVerifyTotp, this);
}
return ( return (
<div style={{display: "flex", justifyContent: "center", alignItems: "center"}}> <div style={{display: "flex", justifyContent: "center", alignItems: "center"}}>
{ {

View File

@@ -264,6 +264,9 @@ class ForgetPage extends React.Component {
) )
} }
onValuesChange={(changedValues, allValues) => { onValuesChange={(changedValues, allValues) => {
if (!changedValues.dest) {
return;
}
const verifyType = changedValues.dest?.indexOf("@") === -1 ? "phone" : "email"; const verifyType = changedValues.dest?.indexOf("@") === -1 ? "phone" : "email";
this.setState({ this.setState({
dest: changedValues.dest, dest: changedValues.dest,

View File

@@ -0,0 +1,31 @@
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {createButton} from "react-social-login-buttons";
import {StaticBaseUrl} from "../Setting";
function Icon({width = 24, height = 24}) {
return <img src={`${StaticBaseUrl}/buttons/kwai.svg`} alt="Sign in with Kwai" style={{width: width, height: height}} />;
}
const config = {
text: "Sign in with Kwai",
icon: Icon,
style: {background: "#ffffff", color: "#000000"},
activeStyle: {background: "#ededee"},
};
const KwaiLoginButton = createButton(config);
export default KwaiLoginButton;

View File

@@ -34,7 +34,7 @@ import {SendCodeInput} from "../common/SendCodeInput";
import LanguageSelect from "../common/select/LanguageSelect"; import LanguageSelect from "../common/select/LanguageSelect";
import {CaptchaModal, CaptchaRule} from "../common/modal/CaptchaModal"; import {CaptchaModal, CaptchaRule} from "../common/modal/CaptchaModal";
import RedirectForm from "../common/RedirectForm"; import RedirectForm from "../common/RedirectForm";
import {MfaAuthVerifyForm, NextMfa, RequiredMfa} from "./mfa/MfaAuthVerifyForm"; import {RequiredMfa} from "./mfa/MfaAuthVerifyForm";
import {GoogleOneTapLoginVirtualButton} from "./GoogleLoginButton"; import {GoogleOneTapLoginVirtualButton} from "./GoogleLoginButton";
import * as ProviderButton from "./ProviderButton"; import * as ProviderButton from "./ProviderButton";
const FaceRecognitionModal = lazy(() => import("../common/modal/FaceRecognitionModal")); const FaceRecognitionModal = lazy(() => import("../common/modal/FaceRecognitionModal"));
@@ -227,7 +227,26 @@ class LoginPage extends React.Component {
return "password"; return "password";
} }
getPlaceholder() { getCurrentLoginMethod() {
if (this.state.loginMethod === "password") {
return "Password";
} else if (this.state.loginMethod?.includes("verificationCode")) {
return "Verification code";
} else if (this.state.loginMethod === "webAuthn") {
return "WebAuthn";
} else if (this.state.loginMethod === "ldap") {
return "LDAP";
} else if (this.state.loginMethod === "faceId") {
return "Face ID";
} else {
return "Password";
}
}
getPlaceholder(defaultPlaceholder = null) {
if (defaultPlaceholder) {
return defaultPlaceholder;
}
switch (this.state.loginMethod) { switch (this.state.loginMethod) {
case "verificationCode": return i18next.t("login:Email or phone"); case "verificationCode": return i18next.t("login:Email or phone");
case "verificationCodeEmail": return i18next.t("login:Email"); case "verificationCodeEmail": return i18next.t("login:Email");
@@ -262,17 +281,7 @@ class LoginPage extends React.Component {
values["organization"] = this.getApplicationObj().organization; values["organization"] = this.getApplicationObj().organization;
} }
if (this.state.loginMethod === "password") { values["signinMethod"] = this.getCurrentLoginMethod();
values["signinMethod"] = "Password";
} else if (this.state.loginMethod?.includes("verificationCode")) {
values["signinMethod"] = "Verification code";
} else if (this.state.loginMethod === "webAuthn") {
values["signinMethod"] = "WebAuthn";
} else if (this.state.loginMethod === "ldap") {
values["signinMethod"] = "LDAP";
} else if (this.state.loginMethod === "faceId") {
values["signinMethod"] = "Face ID";
}
const oAuthParams = Util.getOAuthGetParameters(); const oAuthParams = Util.getOAuthGetParameters();
values["type"] = oAuthParams?.responseType ?? this.state.type; values["type"] = oAuthParams?.responseType ?? this.state.type;
@@ -409,6 +418,7 @@ class LoginPage extends React.Component {
if (this.state.type === "cas") { if (this.state.type === "cas") {
// CAS // CAS
const casParams = Util.getCasParameters(); const casParams = Util.getCasParameters();
values["signinMethod"] = this.getCurrentLoginMethod();
values["type"] = this.state.type; values["type"] = this.state.type;
AuthBackend.loginCas(values, casParams).then((res) => { AuthBackend.loginCas(values, casParams).then((res) => {
const loginHandler = (res) => { const loginHandler = (res) => {
@@ -428,25 +438,7 @@ class LoginPage extends React.Component {
}; };
if (res.status === "ok") { if (res.status === "ok") {
if (res.data === NextMfa) { Setting.checkLoginMfa(res, values, casParams, loginHandler, this);
this.setState({
getVerifyTotp: () => {
return (
<MfaAuthVerifyForm
mfaProps={res.data2}
formValues={values}
authParams={casParams}
application={this.getApplicationObj()}
onFail={() => {
Setting.showMessage("error", i18next.t("mfa:Verification failed"));
}}
onSuccess={(res) => loginHandler(res)}
/>);
},
});
} else {
loginHandler(res);
}
} else { } else {
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
} }
@@ -478,6 +470,10 @@ class LoginPage extends React.Component {
const accessToken = res.data; const accessToken = res.data;
Setting.goToLink(`${oAuthParams.redirectUri}#${amendatoryResponseType}=${accessToken}&state=${oAuthParams.state}&token_type=bearer`); Setting.goToLink(`${oAuthParams.redirectUri}#${amendatoryResponseType}=${accessToken}&state=${oAuthParams.state}&token_type=bearer`);
} else if (responseType === "saml") { } else if (responseType === "saml") {
if (res.data === RequiredMfa) {
this.props.onLoginSuccess(window.location.href);
return;
}
if (res.data2.needUpdatePassword) { if (res.data2.needUpdatePassword) {
sessionStorage.setItem("signinUrl", window.location.href); sessionStorage.setItem("signinUrl", window.location.href);
Setting.goToLink(this, `/forget/${this.state.applicationName}`); Setting.goToLink(this, `/forget/${this.state.applicationName}`);
@@ -491,39 +487,13 @@ class LoginPage extends React.Component {
} else { } else {
const SAMLResponse = res.data; const SAMLResponse = res.data;
const redirectUri = res.data2.redirectUrl; const redirectUri = res.data2.redirectUrl;
Setting.goToLink(`${redirectUri}?SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`); Setting.goToLink(`${redirectUri}${redirectUri.includes("?") ? "&" : "?"}SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);
} }
} }
}; };
if (res.status === "ok") { if (res.status === "ok") {
if (res.data === NextMfa) { Setting.checkLoginMfa(res, values, oAuthParams, loginHandler, this);
this.setState({
getVerifyTotp: () => {
return (
<MfaAuthVerifyForm
mfaProps={res.data2}
formValues={values}
authParams={oAuthParams}
application={this.getApplicationObj()}
onFail={() => {
Setting.showMessage("error", i18next.t("mfa:Verification failed"));
}}
onSuccess={(res) => loginHandler(res)}
/>);
},
});
} else if (res.data === "SelectPlan") {
// paid-user does not have active or pending subscription, go to application default pricing page to select-plan
const pricing = res.data2;
Setting.goToLink(`/select-plan/${pricing.owner}/${pricing.name}?user=${values.username}`);
} else if (res.data === "BuyPlanResult") {
// paid-user has pending subscription, go to buy-plan/result apge to notify payment result
const sub = res.data2;
Setting.goToLink(`/buy-plan/${sub.owner}/${sub.pricing}/result?subscription=${sub.name}`);
} else {
loginHandler(res);
}
} else { } else {
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
} }
@@ -672,7 +642,7 @@ class LoginPage extends React.Component {
id="input" id="input"
className="login-username-input" className="login-username-input"
prefix={<UserOutlined className="site-form-item-icon" />} prefix={<UserOutlined className="site-form-item-icon" />}
placeholder={this.getPlaceholder()} placeholder={this.getPlaceholder(signinItem.placeholder)}
onChange={e => { onChange={e => {
this.setState({ this.setState({
username: e.target.value, username: e.target.value,
@@ -1086,7 +1056,7 @@ class LoginPage extends React.Component {
className="login-password-input" className="login-password-input"
prefix={<LockOutlined className="site-form-item-icon" />} prefix={<LockOutlined className="site-form-item-icon" />}
type="password" type="password"
placeholder={i18next.t("general:Password")} placeholder={signinItem.placeholder ? signinItem.placeholder : i18next.t("general:Password")}
disabled={this.state.loginMethod === "password" ? !Setting.isPasswordEnabled(application) : !Setting.isLdapEnabled(application)} disabled={this.state.loginMethod === "password" ? !Setting.isPasswordEnabled(application) : !Setting.isLdapEnabled(application)}
/> />
</Form.Item> </Form.Item>
@@ -1136,7 +1106,7 @@ class LoginPage extends React.Component {
]); ]);
application?.signinMethods?.forEach((signinMethod) => { application?.signinMethods?.forEach((signinMethod) => {
if (signinMethod.rule === "Hide-Password") { if (signinMethod.rule === "Hide password") {
return; return;
} }
const item = itemsMap.get(generateItemKey(signinMethod.name, signinMethod.rule)); const item = itemsMap.get(generateItemKey(signinMethod.name, signinMethod.rule));

View File

@@ -37,7 +37,7 @@ class MfaSetupPage extends React.Component {
this.state = { this.state = {
account: props.account, account: props.account,
application: null, application: null,
applicationName: props.account.signupApplication ?? "", applicationName: props.account.signupApplication ?? localStorage.getItem("applicationName") ?? "",
current: location.state?.from !== undefined ? 1 : 0, current: location.state?.from !== undefined ? 1 : 0,
mfaProps: null, mfaProps: null,
mfaType: params.get("mfaType") ?? SmsMfaType, mfaType: params.get("mfaType") ?? SmsMfaType,
@@ -179,8 +179,10 @@ class MfaSetupPage extends React.Component {
mfaProps={this.state.mfaProps} mfaProps={this.state.mfaProps}
application={this.state.application} application={this.state.application}
user={this.props.account} user={this.props.account}
onSuccess={() => { onSuccess={(res) => {
this.setState({ this.setState({
dest: res.dest,
countryCode: res.countryCode,
current: this.state.current + 1, current: this.state.current + 1,
}); });
}} }}
@@ -195,7 +197,7 @@ class MfaSetupPage extends React.Component {
); );
case 2: case 2:
return ( return (
<MfaEnableForm user={this.getUser()} mfaType={this.state.mfaType} recoveryCodes={this.state.mfaProps.recoveryCodes} <MfaEnableForm user={this.getUser()} mfaType={this.state.mfaType} secret={this.state.mfaProps.secret} recoveryCodes={this.state.mfaProps.recoveryCodes} dest={this.state.dest} countryCode={this.state.countryCode}
onSuccess={() => { onSuccess={() => {
Setting.showMessage("success", i18next.t("general:Enabled successfully")); Setting.showMessage("success", i18next.t("general:Enabled successfully"));
this.props.onfinish(); this.props.onfinish();

View File

@@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import CryptoJS from "crypto-js"; import CryptoJS from "crypto-js";
import i18next from "i18next"; import {Buffer} from "buffer";
export function getRandomKeyForObfuscator(obfuscatorType) { export function getRandomKeyForObfuscator(obfuscatorType) {
if (obfuscatorType === "DES") { if (obfuscatorType === "DES") {
@@ -45,17 +45,17 @@ function encrypt(cipher, key, iv, password) {
export function checkPasswordObfuscator(passwordObfuscatorType, passwordObfuscatorKey) { export function checkPasswordObfuscator(passwordObfuscatorType, passwordObfuscatorKey) {
if (passwordObfuscatorType === undefined) { if (passwordObfuscatorType === undefined) {
return i18next.t("organization:failed to get password obfuscator"); return "passwordObfuscatorType should not be undefined";
} else if (passwordObfuscatorType === "Plain" || passwordObfuscatorType === "") { } else if (passwordObfuscatorType === "Plain" || passwordObfuscatorType === "") {
return ""; return "";
} else if (passwordObfuscatorType === "AES" || passwordObfuscatorType === "DES") { } else if (passwordObfuscatorType === "AES" || passwordObfuscatorType === "DES") {
if (passwordObfuscatorKeyRegexes[passwordObfuscatorType].test(passwordObfuscatorKey)) { if (passwordObfuscatorKeyRegexes[passwordObfuscatorType].test(passwordObfuscatorKey)) {
return ""; return "";
} else { } else {
return `${i18next.t("organization:The password obfuscator key doesn't match the regex")}: ${passwordObfuscatorKeyRegexes[passwordObfuscatorType].source}`; return `The password obfuscator key doesn't match the regex: ${passwordObfuscatorKeyRegexes[passwordObfuscatorType].source}`;
} }
} else { } else {
return `${i18next.t("organization:unsupported password obfuscator type")}: ${passwordObfuscatorType}`; return `unsupported password obfuscator type: ${passwordObfuscatorType}`;
} }
} }

View File

@@ -119,6 +119,10 @@ const authInfo = {
scope: "user_info", scope: "user_info",
endpoint: "https://open.douyin.com/platform/oauth/connect", endpoint: "https://open.douyin.com/platform/oauth/connect",
}, },
Kwai: {
scope: "user_info",
endpoint: "https://open.kuaishou.com/oauth2/connect",
},
Custom: { Custom: {
endpoint: "https://example.com/", endpoint: "https://example.com/",
}, },
@@ -470,6 +474,8 @@ export function getAuthUrl(application, provider, method, code) {
return `${provider.domain}/v1/authorize?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`; return `${provider.domain}/v1/authorize?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
} else if (provider.type === "Douyin" || provider.type === "TikTok") { } else if (provider.type === "Douyin" || provider.type === "TikTok") {
return `${endpoint}?client_key=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`; return `${endpoint}?client_key=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
} else if (provider.type === "Kwai") {
return `${endpoint}?app_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
} else if (provider.type === "Custom") { } else if (provider.type === "Custom") {
return `${provider.customAuthUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${provider.scopes}&response_type=code&state=${state}`; return `${provider.customAuthUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${provider.scopes}&response_type=code&state=${state}`;
} else if (provider.type === "Bilibili") { } else if (provider.type === "Bilibili") {

View File

@@ -40,6 +40,7 @@ import SteamLoginButton from "./SteamLoginButton";
import BilibiliLoginButton from "./BilibiliLoginButton"; import BilibiliLoginButton from "./BilibiliLoginButton";
import OktaLoginButton from "./OktaLoginButton"; import OktaLoginButton from "./OktaLoginButton";
import DouyinLoginButton from "./DouyinLoginButton"; import DouyinLoginButton from "./DouyinLoginButton";
import KwaiLoginButton from "./KwaiLoginButton";
import LoginButton from "./LoginButton"; import LoginButton from "./LoginButton";
import * as AuthBackend from "./AuthBackend"; import * as AuthBackend from "./AuthBackend";
import {WechatOfficialAccountModal} from "./Util"; import {WechatOfficialAccountModal} from "./Util";
@@ -96,6 +97,8 @@ function getSigninButton(provider) {
return <OktaLoginButton text={text} align={"center"} />; return <OktaLoginButton text={text} align={"center"} />;
} else if (provider.type === "Douyin") { } else if (provider.type === "Douyin") {
return <DouyinLoginButton text={text} align={"center"} />; return <DouyinLoginButton text={text} align={"center"} />;
} else if (provider.type === "Kwai") {
return <KwaiLoginButton text={text} align={"center"} />;
} else { } else {
return <LoginButton key={provider.type} type={provider.type} logoUrl={getProviderLogoURL(provider)} />; return <LoginButton key={provider.type} type={provider.type} logoUrl={getProviderLogoURL(provider)} />;
} }

View File

@@ -20,6 +20,7 @@ import * as Util from "./Util";
import * as Setting from "../Setting"; import * as Setting from "../Setting";
import i18next from "i18next"; import i18next from "i18next";
import {authConfig} from "./Auth"; import {authConfig} from "./Auth";
import {renderLoginPanel} from "../Setting";
class SamlCallback extends React.Component { class SamlCallback extends React.Component {
constructor(props) { constructor(props) {
@@ -81,13 +82,26 @@ class SamlCallback extends React.Component {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
const responseType = this.getResponseType(redirectUri); const responseType = this.getResponseType(redirectUri);
if (responseType === "login") { const handleLogin = (res2) => {
Setting.showMessage("success", "Logged in successfully"); if (responseType === "login") {
Setting.goToLink("/"); Setting.showMessage("success", "Logged in successfully");
} else if (responseType === "code") { Setting.goToLink("/");
const code = res.data; } else if (responseType === "code") {
Setting.goToLink(`${redirectUri}?code=${code}&state=${state}`); const code = res2.data;
} Setting.goToLink(`${redirectUri}?code=${code}&state=${state}`);
}
};
Setting.checkLoginMfa(res, body, {
clientId: clientId,
responseType: responseType,
redirectUri: messages[3],
state: state,
nonce: "",
scope: "read",
challengeMethod: "",
codeChallenge: "",
type: "code",
}, handleLogin, this);
} else { } else {
this.setState({ this.setState({
msg: res.msg, msg: res.msg,
@@ -97,6 +111,11 @@ class SamlCallback extends React.Component {
} }
render() { render() {
if (this.state.getVerifyTotp !== undefined) {
const application = Setting.getApplicationObj(this);
return renderLoginPanel(application, this.state.getVerifyTotp, this, window.location.origin);
}
return ( return (
<div style={{display: "flex", justifyContent: "center", alignItems: "center"}}> <div style={{display: "flex", justifyContent: "center", alignItems: "center"}}>
{ {

View File

@@ -113,6 +113,9 @@ export function getCasLoginParameters(owner, name) {
export function getOAuthGetParameters(params) { export function getOAuthGetParameters(params) {
const queries = (params !== undefined) ? params : new URLSearchParams(window.location.search); const queries = (params !== undefined) ? params : new URLSearchParams(window.location.search);
const lowercaseQueries = {};
queries.forEach((val, key) => {lowercaseQueries[key.toLowerCase()] = val;});
const clientId = getRefinedValue(queries.get("client_id")); const clientId = getRefinedValue(queries.get("client_id"));
const responseType = getRefinedValue(queries.get("response_type")); const responseType = getRefinedValue(queries.get("response_type"));
@@ -138,9 +141,9 @@ export function getOAuthGetParameters(params) {
const nonce = getRefinedValue(queries.get("nonce")); const nonce = getRefinedValue(queries.get("nonce"));
const challengeMethod = getRefinedValue(queries.get("code_challenge_method")); const challengeMethod = getRefinedValue(queries.get("code_challenge_method"));
const codeChallenge = getRefinedValue(queries.get("code_challenge")); const codeChallenge = getRefinedValue(queries.get("code_challenge"));
const samlRequest = getRefinedValue(queries.get("SAMLRequest")); const samlRequest = getRefinedValue(lowercaseQueries["samlRequest".toLowerCase()]);
const relayState = getRefinedValue(queries.get("RelayState")); const relayState = getRefinedValue(lowercaseQueries["RelayState".toLowerCase()]);
const noRedirect = getRefinedValue(queries.get("noRedirect")); const noRedirect = getRefinedValue(lowercaseQueries["noRedirect".toLowerCase()]);
if (clientId === "" && samlRequest === "") { if (clientId === "" && samlRequest === "") {
// login // login

View File

@@ -33,7 +33,8 @@ export function MfaAuthVerifyForm({formValues, authParams, mfaProps, application
const verify = ({passcode}) => { const verify = ({passcode}) => {
setLoading(true); setLoading(true);
const values = {...formValues, passcode, mfaType}; const values = {...formValues, passcode};
values["mfaType"] = mfaProps.mfaType;
const loginFunction = formValues.type === "cas" ? AuthBackend.loginCas : AuthBackend.login; const loginFunction = formValues.type === "cas" ? AuthBackend.loginCas : AuthBackend.login;
loginFunction(values, authParams).then((res) => { loginFunction(values, authParams).then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
@@ -71,7 +72,7 @@ export function MfaAuthVerifyForm({formValues, authParams, mfaProps, application
<div style={{marginBottom: 24, textAlign: "center", fontSize: "24px"}}> <div style={{marginBottom: 24, textAlign: "center", fontSize: "24px"}}>
{i18next.t("mfa:Multi-factor authentication")} {i18next.t("mfa:Multi-factor authentication")}
</div> </div>
{mfaType === SmsMfaType || mfaType === EmailMfaType ? ( {mfaProps.mfaType === SmsMfaType || mfaProps.mfaType === EmailMfaType ? (
<Fragment> <Fragment>
<div style={{marginBottom: 24}}> <div style={{marginBottom: 24}}>
{i18next.t("mfa:You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue")} {i18next.t("mfa:You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue")}

View File

@@ -3,13 +3,17 @@ import i18next from "i18next";
import React, {useState} from "react"; import React, {useState} from "react";
import * as MfaBackend from "../../backend/MfaBackend"; import * as MfaBackend from "../../backend/MfaBackend";
export function MfaEnableForm({user, mfaType, recoveryCodes, onSuccess, onFail}) { export function MfaEnableForm({user, mfaType, secret, recoveryCodes, dest, countryCode, onSuccess, onFail}) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const requestEnableMfa = () => { const requestEnableMfa = () => {
const data = { const data = {
mfaType, mfaType,
secret,
dest,
countryCode,
...user, ...user,
}; };
data["recoveryCodes"] = recoveryCodes[0];
setLoading(true); setLoading(true);
MfaBackend.MfaSetupEnable(data).then(res => { MfaBackend.MfaSetupEnable(data).then(res => {
if (res.status === "ok") { if (res.status === "ok") {

View File

@@ -26,11 +26,13 @@ export const mfaSetup = "mfaSetup";
export function MfaVerifyForm({mfaProps, application, user, onSuccess, onFail}) { export function MfaVerifyForm({mfaProps, application, user, onSuccess, onFail}) {
const [form] = Form.useForm(); const [form] = Form.useForm();
const onFinish = ({passcode}) => { const onFinish = ({passcode, countryCode, dest}) => {
const data = {passcode, mfaType: mfaProps.mfaType, ...user}; const data = {passcode, mfaType: mfaProps.mfaType, secret: mfaProps.secret, dest: dest, countryCode: countryCode, ...user};
MfaBackend.MfaSetupVerify(data) MfaBackend.MfaSetupVerify(data)
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
res.dest = dest;
res.countryCode = countryCode;
onSuccess(res); onSuccess(res);
} else { } else {
onFail(res); onFail(res);

View File

@@ -1,5 +1,5 @@
import {UserOutlined} from "@ant-design/icons"; import {UserOutlined} from "@ant-design/icons";
import {Button, Form, Input} from "antd"; import {Button, Form, Input, Space} from "antd";
import i18next from "i18next"; import i18next from "i18next";
import React, {useEffect} from "react"; import React, {useEffect} from "react";
import {CountryCodeSelect} from "../../common/select/CountryCodeSelect"; import {CountryCodeSelect} from "../../common/select/CountryCodeSelect";
@@ -15,15 +15,18 @@ export const MfaVerifySmsForm = ({mfaProps, application, onFinish, method, user}
useEffect(() => { useEffect(() => {
if (method === mfaAuth) { if (method === mfaAuth) {
setDest(mfaProps.secret); setDest(mfaProps.secret);
form.setFieldValue("dest", mfaProps.secret);
return; return;
} }
if (mfaProps.mfaType === SmsMfaType) { if (mfaProps.mfaType === SmsMfaType) {
setDest(user.phone); setDest(user.phone);
form.setFieldValue("dest", user.phone);
return; return;
} }
if (mfaProps.mfaType === EmailMfaType) { if (mfaProps.mfaType === EmailMfaType) {
setDest(user.email); setDest(user.email);
form.setFieldValue("dest", user.email);
} }
}, [mfaProps.mfaType]); }, [mfaProps.mfaType]);
@@ -57,45 +60,44 @@ export const MfaVerifySmsForm = ({mfaProps, application, onFinish, method, user}
<div style={{marginBottom: 20, textAlign: "left", gap: 8}}> <div style={{marginBottom: 20, textAlign: "left", gap: 8}}>
{isEmail() ? i18next.t("mfa:Your email is") : i18next.t("mfa:Your phone is")} {dest} {isEmail() ? i18next.t("mfa:Your email is") : i18next.t("mfa:Your phone is")} {dest}
</div> : </div> :
(<React.Fragment> (
<p>{isEmail() ? i18next.t("mfa:Please bind your email first, the system will automatically uses the mail for multi-factor authentication") : <p>{isEmail() ? i18next.t("mfa:Please bind your email first, the system will automatically uses the mail for multi-factor authentication") :
i18next.t("mfa:Please bind your phone first, the system automatically uses the phone for multi-factor authentication")} i18next.t("mfa:Please bind your phone first, the system automatically uses the phone for multi-factor authentication")}
</p> </p>
<Input.Group compact style={{width: "300Px", marginBottom: "30px"}}>
{isEmail() ? null :
<Form.Item
name="countryCode"
noStyle
rules={[
{
required: false,
message: i18next.t("signup:Please select your country code!"),
},
]}
>
<CountryCodeSelect
initValue={mfaProps.countryCode}
style={{width: "30%"}}
countryCodes={application.organizationObj.countryCodes}
/>
</Form.Item>
}
<Form.Item
name="dest"
noStyle
rules={[{required: true, message: i18next.t("login:Please input your Email or Phone!")}]}
>
<Input
style={{width: isEmail() ? "100% " : "70%"}}
onChange={(e) => {setDest(e.target.value);}}
prefix={<UserOutlined />}
placeholder={isEmail() ? i18next.t("general:Email") : i18next.t("general:Phone")}
/>
</Form.Item>
</Input.Group>
</React.Fragment>
) )
} }
<Space.Compact style={{width: "300Px", marginBottom: "30px", display: isShowText() ? "none" : ""}}>
{isEmail() || isShowText() ? null :
<Form.Item
name="countryCode"
noStyle
rules={[
{
required: false,
message: i18next.t("signup:Please select your country code!"),
},
]}
>
<CountryCodeSelect
initValue={mfaProps.countryCode}
style={{width: "30%"}}
countryCodes={application.organizationObj.countryCodes}
/>
</Form.Item>
}
<Form.Item
name="dest"
noStyle
rules={[{required: true, message: i18next.t("login:Please input your Email or Phone!")}]}
>
<Input
style={{width: isEmail() ? "100% " : "70%"}}
onChange={(e) => {setDest(e.target.value);}}
prefix={<UserOutlined />}
placeholder={isEmail() ? i18next.t("general:Email") : i18next.t("general:Phone")}
/>
</Form.Item>
</Space.Compact>
<Form.Item <Form.Item
name="passcode" name="passcode"
rules={[{required: true, message: i18next.t("login:Please input your code!")}]} rules={[{required: true, message: i18next.t("login:Please input your code!")}]}

View File

@@ -32,6 +32,9 @@ export function MfaSetupVerify(values) {
formData.append("name", values.name); formData.append("name", values.name);
formData.append("mfaType", values.mfaType); formData.append("mfaType", values.mfaType);
formData.append("passcode", values.passcode); formData.append("passcode", values.passcode);
formData.append("secret", values.secret);
formData.append("dest", values.dest);
formData.append("countryCode", values.countryCode);
return fetch(`${Setting.ServerUrl}/api/mfa/setup/verify`, { return fetch(`${Setting.ServerUrl}/api/mfa/setup/verify`, {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
@@ -44,6 +47,10 @@ export function MfaSetupEnable(values) {
formData.append("mfaType", values.mfaType); formData.append("mfaType", values.mfaType);
formData.append("owner", values.owner); formData.append("owner", values.owner);
formData.append("name", values.name); formData.append("name", values.name);
formData.append("secret", values.secret);
formData.append("recoveryCodes", values.recoveryCodes);
formData.append("dest", values.dest);
formData.append("countryCode", values.countryCode);
return fetch(`${Setting.ServerUrl}/api/mfa/setup/enable`, { return fetch(`${Setting.ServerUrl}/api/mfa/setup/enable`, {
method: "POST", method: "POST",
credentials: "include", credentials: "include",

View File

@@ -135,6 +135,15 @@ const Dashboard = (props) => {
i18next.t("general:Applications"), i18next.t("general:Applications"),
i18next.t("general:Organizations"), i18next.t("general:Organizations"),
i18next.t("general:Subscriptions"), i18next.t("general:Subscriptions"),
i18next.t("general:Roles"),
i18next.t("general:Groups"),
i18next.t("general:Resources"),
i18next.t("general:Certs"),
i18next.t("general:Permissions"),
i18next.t("general:Transactions"),
i18next.t("general:Models"),
i18next.t("general:Adapters"),
i18next.t("general:Enforcers"),
], top: "10%"}, ], top: "10%"},
grid: {left: "3%", right: "4%", bottom: "0", top: "25%", containLabel: true}, grid: {left: "3%", right: "4%", bottom: "0", top: "25%", containLabel: true},
xAxis: {type: "category", boundaryGap: false, data: dateArray}, xAxis: {type: "category", boundaryGap: false, data: dateArray},
@@ -145,6 +154,15 @@ const Dashboard = (props) => {
{name: i18next.t("general:Providers"), type: "line", data: dashboardData.providerCounts}, {name: i18next.t("general:Providers"), type: "line", data: dashboardData.providerCounts},
{name: i18next.t("general:Applications"), type: "line", data: dashboardData.applicationCounts}, {name: i18next.t("general:Applications"), type: "line", data: dashboardData.applicationCounts},
{name: i18next.t("general:Subscriptions"), type: "line", data: dashboardData.subscriptionCounts}, {name: i18next.t("general:Subscriptions"), type: "line", data: dashboardData.subscriptionCounts},
{name: i18next.t("general:Roles"), type: "line", data: dashboardData.roleCounts},
{name: i18next.t("general:Groups"), type: "line", data: dashboardData.groupCounts},
{name: i18next.t("general:Resources"), type: "line", data: dashboardData.resourceCounts},
{name: i18next.t("general:Certs"), type: "line", data: dashboardData.certCounts},
{name: i18next.t("general:Permissions"), type: "line", data: dashboardData.permissionCounts},
{name: i18next.t("general:Transactions"), type: "line", data: dashboardData.transactionCounts},
{name: i18next.t("general:Models"), type: "line", data: dashboardData.modelCounts},
{name: i18next.t("general:Adapters"), type: "line", data: dashboardData.adapterCounts},
{name: i18next.t("general:Enforcers"), type: "line", data: dashboardData.enforcerCounts},
], ],
}; };
myChart.setOption(option); myChart.setOption(option);

View File

@@ -0,0 +1,113 @@
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Alert, Button, QRCode} from "antd";
import copy from "copy-to-clipboard";
import * as Setting from "../Setting";
import i18next from "i18next";
export const generateCasdoorAppUrl = (accessToken, forQrCode = true) => {
let qrUrl = "";
let error = null;
if (!accessToken) {
error = i18next.t("general:Access token is empty");
return {qrUrl, error};
}
qrUrl = `casdoor-app://login?serverUrl=${window.location.origin}&accessToken=${accessToken}`;
if (forQrCode && qrUrl.length >= 2000) {
qrUrl = "";
error = i18next.t("general:QR code is too large");
}
return {qrUrl, error};
};
export const CasdoorAppQrCode = ({accessToken, icon}) => {
const {qrUrl, error} = generateCasdoorAppUrl(accessToken, true);
if (error) {
return <Alert message={error} type="error" showIcon />;
}
return (
<QRCode
value={qrUrl}
icon={icon}
errorLevel="M"
size={230}
bordered={false}
/>
);
};
export const CasdoorAppUrl = ({accessToken}) => {
const {qrUrl, error} = generateCasdoorAppUrl(accessToken, false);
const handleCopyUrl = async() => {
if (!window.isSecureContext) {
return;
}
copy(qrUrl);
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
};
if (error) {
return <Alert message={error} type="error" showIcon />;
}
return (
<div>
<div style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "10px",
}}>
{window.isSecureContext && (
<Button size="small" type="primary" onClick={handleCopyUrl} style={{marginLeft: "10px"}}>
{i18next.t("resource:Copy Link")}
</Button>
)}
</div>
<div
style={{
padding: "10px",
maxWidth: "400px",
maxHeight: "100px",
overflow: "auto",
wordBreak: "break-all",
whiteSpace: "pre-wrap",
cursor: "pointer",
userSelect: "all",
backgroundColor: "#f5f5f5",
borderRadius: "4px",
}}
onClick={(e) => {
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(e.target);
selection.removeAllRanges();
selection.addRange(range);
}}
>
{qrUrl}
</div>
</div>
);
};

View File

@@ -105,7 +105,7 @@ export const PasswordModal = (props) => {
}); });
}; };
const hasOldPassword = user.password !== ""; const hasOldPassword = (user.password !== "" || user.ldap !== "");
return ( return (
<Row> <Row>

View File

@@ -161,6 +161,18 @@
"Sending": "Sending", "Sending": "Sending",
"Submit and complete": "Submit and complete" "Submit and complete": "Submit and complete"
}, },
"currency": {
"AUD": "AUD",
"CAD": "CAD",
"CHF": "CHF",
"CNY": "CNY",
"EUR": "EUR",
"GBP": "GBP",
"HKD": "HKD",
"JPY": "JPY",
"SGD": "SGD",
"USD": "USD"
},
"enforcer": { "enforcer": {
"Edit Enforcer": "Edit Enforcer", "Edit Enforcer": "Edit Enforcer",
"New Enforcer": "New Enforcer" "New Enforcer": "New Enforcer"
@@ -266,10 +278,13 @@
"Go to writable demo site?": "Go to writable demo site?", "Go to writable demo site?": "Go to writable demo site?",
"Groups": "Groups", "Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip", "Groups - Tooltip": "Groups - Tooltip",
"Hide password": "Hide password",
"Home": "Home", "Home": "Home",
"Home - Tooltip": "Home page of the application", "Home - Tooltip": "Home page of the application",
"ID": "ID", "ID": "ID",
"ID - Tooltip": "Unique random string", "ID - Tooltip": "Unique random string",
"IP whitelist": "IP whitelist",
"IP whitelist - Tooltip": "IP whitelist - Tooltip",
"Identity": "Identity", "Identity": "Identity",
"Invitations": "Invitations", "Invitations": "Invitations",
"Is enabled": "Is enabled", "Is enabled": "Is enabled",
@@ -312,6 +327,10 @@
"Password - Tooltip": "Make sure the password is correct", "Password - Tooltip": "Make sure the password is correct",
"Password complexity options": "Password complexity options", "Password complexity options": "Password complexity options",
"Password complexity options - Tooltip": "Different combinations of password complexity options", "Password complexity options - Tooltip": "Different combinations of password complexity options",
"Password obf key": "Password obf key",
"Password obf key - Tooltip": "Password obf key - Tooltip",
"Password obfuscator": "Password obfuscator",
"Password obfuscator - Tooltip": "Password obfuscator - Tooltip",
"Password salt": "Password salt", "Password salt": "Password salt",
"Password salt - Tooltip": "Random parameter used for password encryption", "Password salt - Tooltip": "Random parameter used for password encryption",
"Password type": "Password type", "Password type": "Password type",
@@ -559,7 +578,6 @@
"Use SMS": "Use SMS", "Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code", "Use SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code", "Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed",
"Verify Code": "Verify Code", "Verify Code": "Verify Code",
"Verify Password": "Verify Password", "Verify Password": "Verify Password",
"You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue", "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
@@ -574,6 +592,8 @@
"Secret Key": "Secret Key" "Secret Key": "Secret Key"
}, },
"model": { "model": {
"Advanced Editor": "Advanced Editor",
"Basic Editor": "Basic Editor",
"Edit Model": "Edit Model", "Edit Model": "Edit Model",
"Model text": "Model text", "Model text": "Model text",
"Model text - Tooltip": "Casbin access control model, including built-in models like ACL, RBAC, ABAC, RESTful, etc. You can also create custom models. For more information, please visit the Casbin website", "Model text - Tooltip": "Casbin access control model, including built-in models like ACL, RBAC, ABAC, RESTful, etc. You can also create custom models. For more information, please visit the Casbin website",
@@ -592,6 +612,8 @@
"Modify rule": "Modify rule", "Modify rule": "Modify rule",
"New Organization": "New Organization", "New Organization": "New Organization",
"Optional": "Optional", "Optional": "Optional",
"Password expire days": "Password expire days",
"Password expire days - Tooltip": "Password expire days - Tooltip",
"Prompt": "Prompt", "Prompt": "Prompt",
"Required": "Required", "Required": "Required",
"Soft deletion": "Soft deletion", "Soft deletion": "Soft deletion",
@@ -709,7 +731,6 @@
"Alipay": "Alipay", "Alipay": "Alipay",
"Buy": "Buy", "Buy": "Buy",
"Buy Product": "Buy Product", "Buy Product": "Buy Product",
"CNY": "CNY",
"Detail": "Detail", "Detail": "Detail",
"Detail - Tooltip": "Detail of product", "Detail - Tooltip": "Detail of product",
"Dummy": "Dummy", "Dummy": "Dummy",
@@ -740,7 +761,6 @@
"Test buy page..": "Test buy page..", "Test buy page..": "Test buy page..",
"There is no payment channel for this product.": "There is no payment channel for this product.", "There is no payment channel for this product.": "There is no payment channel for this product.",
"This product is currently not in sale.": "This product is currently not in sale.", "This product is currently not in sale.": "This product is currently not in sale.",
"USD": "USD",
"WeChat Pay": "WeChat Pay" "WeChat Pay": "WeChat Pay"
}, },
"provider": { "provider": {
@@ -893,6 +913,7 @@
"Signup HTML - Edit": "Signup HTML - Edit", "Signup HTML - Edit": "Signup HTML - Edit",
"Signup HTML - Tooltip": "Custom HTML for replacing the default signup page style", "Signup HTML - Tooltip": "Custom HTML for replacing the default signup page style",
"Signup group": "Signup group", "Signup group": "Signup group",
"Signup group - Tooltip": "Signup group - Tooltip",
"Silent": "Silent", "Silent": "Silent",
"Site key": "Site key", "Site key": "Site key",
"Site key - Tooltip": "Site key", "Site key - Tooltip": "Site key",
@@ -1157,6 +1178,7 @@
"Keys": "Keys", "Keys": "Keys",
"Language": "Language", "Language": "Language",
"Language - Tooltip": "Language - Tooltip", "Language - Tooltip": "Language - Tooltip",
"Last change password time": "Last change password time",
"Link": "Link", "Link": "Link",
"Location": "Location", "Location": "Location",
"Location - Tooltip": "City of residence", "Location - Tooltip": "City of residence",

View File

@@ -161,6 +161,18 @@
"Sending": "Odesílání", "Sending": "Odesílání",
"Submit and complete": "Odeslat a dokončit" "Submit and complete": "Odeslat a dokončit"
}, },
"currency": {
"AUD": "AUD",
"CAD": "CAD",
"CHF": "CHF",
"CNY": "CNY",
"EUR": "EUR",
"GBP": "GBP",
"HKD": "HKD",
"JPY": "JPY",
"SGD": "SGD",
"USD": "USD"
},
"enforcer": { "enforcer": {
"Edit Enforcer": "Upravit Enforcer", "Edit Enforcer": "Upravit Enforcer",
"New Enforcer": "Nový Enforcer" "New Enforcer": "Nový Enforcer"
@@ -266,10 +278,13 @@
"Go to writable demo site?": "Přejít na zapisovatelnou demo stránku?", "Go to writable demo site?": "Přejít na zapisovatelnou demo stránku?",
"Groups": "Skupiny", "Groups": "Skupiny",
"Groups - Tooltip": "Skupiny - Tooltip", "Groups - Tooltip": "Skupiny - Tooltip",
"Hide password": "Hide password",
"Home": "Domů", "Home": "Domů",
"Home - Tooltip": "Domovská stránka aplikace", "Home - Tooltip": "Domovská stránka aplikace",
"ID": "ID", "ID": "ID",
"ID - Tooltip": "Unikátní náhodný řetězec", "ID - Tooltip": "Unikátní náhodný řetězec",
"IP whitelist": "IP whitelist",
"IP whitelist - Tooltip": "IP whitelist - Tooltip",
"Identity": "Identita", "Identity": "Identita",
"Invitations": "Pozvánky", "Invitations": "Pozvánky",
"Is enabled": "Je povoleno", "Is enabled": "Je povoleno",
@@ -312,6 +327,10 @@
"Password - Tooltip": "Ujistěte se, že heslo je správné", "Password - Tooltip": "Ujistěte se, že heslo je správné",
"Password complexity options": "Možnosti složitosti hesla", "Password complexity options": "Možnosti složitosti hesla",
"Password complexity options - Tooltip": "Různé kombinace možností složitosti hesla", "Password complexity options - Tooltip": "Různé kombinace možností složitosti hesla",
"Password obf key": "Password obf key",
"Password obf key - Tooltip": "Password obf key - Tooltip",
"Password obfuscator": "Password obfuscator",
"Password obfuscator - Tooltip": "Password obfuscator - Tooltip",
"Password salt": "Heslová sůl", "Password salt": "Heslová sůl",
"Password salt - Tooltip": "Náhodný parametr použitý pro šifrování hesla", "Password salt - Tooltip": "Náhodný parametr použitý pro šifrování hesla",
"Password type": "Typ hesla", "Password type": "Typ hesla",
@@ -559,7 +578,6 @@
"Use SMS": "Použít SMS", "Use SMS": "Použít SMS",
"Use SMS verification code": "Použít ověřovací kód SMS", "Use SMS verification code": "Použít ověřovací kód SMS",
"Use a recovery code": "Použít obnovovací kód", "Use a recovery code": "Použít obnovovací kód",
"Verification failed": "Ověření selhalo",
"Verify Code": "Ověřit kód", "Verify Code": "Ověřit kód",
"Verify Password": "Ověřit heslo", "Verify Password": "Ověřit heslo",
"You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue", "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
@@ -574,6 +592,8 @@
"Secret Key": "Secret Key" "Secret Key": "Secret Key"
}, },
"model": { "model": {
"Advanced Editor": "Pokročilý editor",
"Basic Editor": "Základní editor",
"Edit Model": "Upravit model", "Edit Model": "Upravit model",
"Model text": "Text modelu", "Model text": "Text modelu",
"Model text - Tooltip": "Casbin model řízení přístupu, včetně vestavěných modelů jako ACL, RBAC, ABAC, RESTful, atd. Můžete také vytvářet vlastní modely. Pro více informací navštivte webové stránky Casbin", "Model text - Tooltip": "Casbin model řízení přístupu, včetně vestavěných modelů jako ACL, RBAC, ABAC, RESTful, atd. Můžete také vytvářet vlastní modely. Pro více informací navštivte webové stránky Casbin",
@@ -592,6 +612,8 @@
"Modify rule": "Upravit pravidlo", "Modify rule": "Upravit pravidlo",
"New Organization": "Nová organizace", "New Organization": "Nová organizace",
"Optional": "Volitelný", "Optional": "Volitelný",
"Password expire days": "Password expire days",
"Password expire days - Tooltip": "Password expire days - Tooltip",
"Prompt": "Výzva", "Prompt": "Výzva",
"Required": "Povinné", "Required": "Povinné",
"Soft deletion": "Měkké smazání", "Soft deletion": "Měkké smazání",
@@ -709,7 +731,6 @@
"Alipay": "Alipay", "Alipay": "Alipay",
"Buy": "Koupit", "Buy": "Koupit",
"Buy Product": "Koupit produkt", "Buy Product": "Koupit produkt",
"CNY": "CNY",
"Detail": "Detail", "Detail": "Detail",
"Detail - Tooltip": "Detail produktu", "Detail - Tooltip": "Detail produktu",
"Dummy": "Dummy", "Dummy": "Dummy",
@@ -740,7 +761,6 @@
"Test buy page..": "Testovací stránka nákupu..", "Test buy page..": "Testovací stránka nákupu..",
"There is no payment channel for this product.": "Pro tento produkt neexistuje žádný platební kanál.", "There is no payment channel for this product.": "Pro tento produkt neexistuje žádný platební kanál.",
"This product is currently not in sale.": "Tento produkt není momentálně v prodeji.", "This product is currently not in sale.": "Tento produkt není momentálně v prodeji.",
"USD": "USD",
"WeChat Pay": "WeChat Pay" "WeChat Pay": "WeChat Pay"
}, },
"provider": { "provider": {
@@ -893,6 +913,7 @@
"Signup HTML - Edit": "Upravit HTML pro registraci", "Signup HTML - Edit": "Upravit HTML pro registraci",
"Signup HTML - Tooltip": "Vlastní HTML pro nahrazení výchozího stylu registrační stránky", "Signup HTML - Tooltip": "Vlastní HTML pro nahrazení výchozího stylu registrační stránky",
"Signup group": "Skupina pro registraci", "Signup group": "Skupina pro registraci",
"Signup group - Tooltip": "Signup group - Tooltip",
"Silent": "Tiché", "Silent": "Tiché",
"Site key": "Klíč stránky", "Site key": "Klíč stránky",
"Site key - Tooltip": "Nápověda ke klíči stránky", "Site key - Tooltip": "Nápověda ke klíči stránky",
@@ -1157,6 +1178,7 @@
"Keys": "Klíče", "Keys": "Klíče",
"Language": "Jazyk", "Language": "Jazyk",
"Language - Tooltip": "Jazyk - Nápověda", "Language - Tooltip": "Jazyk - Nápověda",
"Last change password time": "Last change password time",
"Link": "Odkaz", "Link": "Odkaz",
"Location": "Místo", "Location": "Místo",
"Location - Tooltip": "Město bydliště", "Location - Tooltip": "Město bydliště",

View File

@@ -161,6 +161,18 @@
"Sending": "Sendet", "Sending": "Sendet",
"Submit and complete": "Einreichen und abschließen" "Submit and complete": "Einreichen und abschließen"
}, },
"currency": {
"AUD": "AUD",
"CAD": "CAD",
"CHF": "CHF",
"CNY": "CNY",
"EUR": "EUR",
"GBP": "GBP",
"HKD": "HKD",
"JPY": "JPY",
"SGD": "SGD",
"USD": "USD"
},
"enforcer": { "enforcer": {
"Edit Enforcer": "Edit Enforcer", "Edit Enforcer": "Edit Enforcer",
"New Enforcer": "New Enforcer" "New Enforcer": "New Enforcer"
@@ -266,10 +278,13 @@
"Go to writable demo site?": "Gehe zur beschreibbaren Demo-Website?", "Go to writable demo site?": "Gehe zur beschreibbaren Demo-Website?",
"Groups": "Groups", "Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip", "Groups - Tooltip": "Groups - Tooltip",
"Hide password": "Hide password",
"Home": "Zuhause", "Home": "Zuhause",
"Home - Tooltip": "Homepage der Anwendung", "Home - Tooltip": "Homepage der Anwendung",
"ID": "ID", "ID": "ID",
"ID - Tooltip": "Einzigartiger Zufallsstring", "ID - Tooltip": "Einzigartiger Zufallsstring",
"IP whitelist": "IP whitelist",
"IP whitelist - Tooltip": "IP whitelist - Tooltip",
"Identity": "Identity", "Identity": "Identity",
"Invitations": "Invitations", "Invitations": "Invitations",
"Is enabled": "Ist aktiviert", "Is enabled": "Ist aktiviert",
@@ -312,6 +327,10 @@
"Password - Tooltip": "Stellen Sie sicher, dass das Passwort korrekt ist", "Password - Tooltip": "Stellen Sie sicher, dass das Passwort korrekt ist",
"Password complexity options": "Password complexity options", "Password complexity options": "Password complexity options",
"Password complexity options - Tooltip": "Password complexity options - Tooltip", "Password complexity options - Tooltip": "Password complexity options - Tooltip",
"Password obf key": "Password obf key",
"Password obf key - Tooltip": "Password obf key - Tooltip",
"Password obfuscator": "Password obfuscator",
"Password obfuscator - Tooltip": "Password obfuscator - Tooltip",
"Password salt": "Passwort-Salt", "Password salt": "Passwort-Salt",
"Password salt - Tooltip": "Zufälliger Parameter, der für die Verschlüsselung von Passwörtern verwendet wird", "Password salt - Tooltip": "Zufälliger Parameter, der für die Verschlüsselung von Passwörtern verwendet wird",
"Password type": "Passworttyp", "Password type": "Passworttyp",
@@ -559,7 +578,6 @@
"Use SMS": "Use SMS", "Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code", "Use SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code", "Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed",
"Verify Code": "Verify Code", "Verify Code": "Verify Code",
"Verify Password": "Verify Password", "Verify Password": "Verify Password",
"You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue", "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
@@ -574,6 +592,8 @@
"Secret Key": "Secret Key" "Secret Key": "Secret Key"
}, },
"model": { "model": {
"Advanced Editor": "Erweiterter Editor",
"Basic Editor": "Basis-Editor",
"Edit Model": "Modell bearbeiten", "Edit Model": "Modell bearbeiten",
"Model text": "Modelltext", "Model text": "Modelltext",
"Model text - Tooltip": "Casbin Zugriffskontrollmodell inklusive integrierter Modelle wie ACL, RBAC, ABAC, RESTful, usw. Sie können auch benutzerdefinierte Modelle erstellen. Weitere Informationen finden Sie auf der Casbin-Website", "Model text - Tooltip": "Casbin Zugriffskontrollmodell inklusive integrierter Modelle wie ACL, RBAC, ABAC, RESTful, usw. Sie können auch benutzerdefinierte Modelle erstellen. Weitere Informationen finden Sie auf der Casbin-Website",
@@ -592,6 +612,8 @@
"Modify rule": "Regel ändern", "Modify rule": "Regel ändern",
"New Organization": "Neue Organisation", "New Organization": "Neue Organisation",
"Optional": "Optional", "Optional": "Optional",
"Password expire days": "Password expire days",
"Password expire days - Tooltip": "Password expire days - Tooltip",
"Prompt": "Prompt", "Prompt": "Prompt",
"Required": "Required", "Required": "Required",
"Soft deletion": "Softe Löschung", "Soft deletion": "Softe Löschung",
@@ -709,7 +731,6 @@
"Alipay": "Alipay", "Alipay": "Alipay",
"Buy": "Kaufen", "Buy": "Kaufen",
"Buy Product": "Produkt kaufen", "Buy Product": "Produkt kaufen",
"CNY": "CNY",
"Detail": "Detail", "Detail": "Detail",
"Detail - Tooltip": "Detail des Produkts", "Detail - Tooltip": "Detail des Produkts",
"Dummy": "Dummy", "Dummy": "Dummy",
@@ -740,7 +761,6 @@
"Test buy page..": "Testkaufseite.", "Test buy page..": "Testkaufseite.",
"There is no payment channel for this product.": "Es gibt keinen Zahlungskanal für dieses Produkt.", "There is no payment channel for this product.": "Es gibt keinen Zahlungskanal für dieses Produkt.",
"This product is currently not in sale.": "Dieses Produkt steht derzeit nicht zum Verkauf.", "This product is currently not in sale.": "Dieses Produkt steht derzeit nicht zum Verkauf.",
"USD": "USD",
"WeChat Pay": "WeChat Pay" "WeChat Pay": "WeChat Pay"
}, },
"provider": { "provider": {
@@ -893,6 +913,7 @@
"Signup HTML - Edit": "Registrierung HTML - Bearbeiten", "Signup HTML - Edit": "Registrierung HTML - Bearbeiten",
"Signup HTML - Tooltip": "Benutzerdefiniertes HTML zur Ersetzung des Standard-Registrierungs-Seitenstils", "Signup HTML - Tooltip": "Benutzerdefiniertes HTML zur Ersetzung des Standard-Registrierungs-Seitenstils",
"Signup group": "Signup group", "Signup group": "Signup group",
"Signup group - Tooltip": "Signup group - Tooltip",
"Silent": "Silent", "Silent": "Silent",
"Site key": "Site-Key", "Site key": "Site-Key",
"Site key - Tooltip": "Seitenschlüssel", "Site key - Tooltip": "Seitenschlüssel",
@@ -1157,6 +1178,7 @@
"Keys": "Schlüssel", "Keys": "Schlüssel",
"Language": "Language", "Language": "Language",
"Language - Tooltip": "Language - Tooltip", "Language - Tooltip": "Language - Tooltip",
"Last change password time": "Last change password time",
"Link": "Link", "Link": "Link",
"Location": "Ort", "Location": "Ort",
"Location - Tooltip": "Stadt des Wohnsitzes", "Location - Tooltip": "Stadt des Wohnsitzes",

View File

@@ -161,6 +161,18 @@
"Sending": "Sending", "Sending": "Sending",
"Submit and complete": "Submit and complete" "Submit and complete": "Submit and complete"
}, },
"currency": {
"AUD": "AUD",
"CAD": "CAD",
"CHF": "CHF",
"CNY": "CNY",
"EUR": "EUR",
"GBP": "GBP",
"HKD": "HKD",
"JPY": "JPY",
"SGD": "SGD",
"USD": "USD"
},
"enforcer": { "enforcer": {
"Edit Enforcer": "Edit Enforcer", "Edit Enforcer": "Edit Enforcer",
"New Enforcer": "New Enforcer" "New Enforcer": "New Enforcer"
@@ -266,10 +278,13 @@
"Go to writable demo site?": "Go to writable demo site?", "Go to writable demo site?": "Go to writable demo site?",
"Groups": "Groups", "Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip", "Groups - Tooltip": "Groups - Tooltip",
"Hide password": "Hide password",
"Home": "Home", "Home": "Home",
"Home - Tooltip": "Home page of the application", "Home - Tooltip": "Home page of the application",
"ID": "ID", "ID": "ID",
"ID - Tooltip": "Unique random string", "ID - Tooltip": "Unique random string",
"IP whitelist": "IP whitelist",
"IP whitelist - Tooltip": "IP whitelist - Tooltip",
"Identity": "Identity", "Identity": "Identity",
"Invitations": "Invitations", "Invitations": "Invitations",
"Is enabled": "Is enabled", "Is enabled": "Is enabled",
@@ -312,6 +327,10 @@
"Password - Tooltip": "Make sure the password is correct", "Password - Tooltip": "Make sure the password is correct",
"Password complexity options": "Password complexity options", "Password complexity options": "Password complexity options",
"Password complexity options - Tooltip": "Different combinations of password complexity options", "Password complexity options - Tooltip": "Different combinations of password complexity options",
"Password obf key": "Password obf key",
"Password obf key - Tooltip": "Password obf key - Tooltip",
"Password obfuscator": "Password obfuscator",
"Password obfuscator - Tooltip": "Password obfuscator - Tooltip",
"Password salt": "Password salt", "Password salt": "Password salt",
"Password salt - Tooltip": "Random parameter used for password encryption", "Password salt - Tooltip": "Random parameter used for password encryption",
"Password type": "Password type", "Password type": "Password type",
@@ -559,7 +578,6 @@
"Use SMS": "Use SMS", "Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code", "Use SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code", "Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed",
"Verify Code": "Verify Code", "Verify Code": "Verify Code",
"Verify Password": "Verify Password", "Verify Password": "Verify Password",
"You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue", "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
@@ -574,6 +592,8 @@
"Secret Key": "Secret Key" "Secret Key": "Secret Key"
}, },
"model": { "model": {
"Advanced Editor": "Advanced Editor",
"Basic Editor": "Basic Editor",
"Edit Model": "Edit Model", "Edit Model": "Edit Model",
"Model text": "Model text", "Model text": "Model text",
"Model text - Tooltip": "Casbin access control model, including built-in models like ACL, RBAC, ABAC, RESTful, etc. You can also create custom models. For more information, please visit the Casbin website", "Model text - Tooltip": "Casbin access control model, including built-in models like ACL, RBAC, ABAC, RESTful, etc. You can also create custom models. For more information, please visit the Casbin website",
@@ -592,6 +612,8 @@
"Modify rule": "Modify rule", "Modify rule": "Modify rule",
"New Organization": "New Organization", "New Organization": "New Organization",
"Optional": "Optional", "Optional": "Optional",
"Password expire days": "Password expire days",
"Password expire days - Tooltip": "Password expire days - Tooltip",
"Prompt": "Prompt", "Prompt": "Prompt",
"Required": "Required", "Required": "Required",
"Soft deletion": "Soft deletion", "Soft deletion": "Soft deletion",
@@ -709,7 +731,6 @@
"Alipay": "Alipay", "Alipay": "Alipay",
"Buy": "Buy", "Buy": "Buy",
"Buy Product": "Buy Product", "Buy Product": "Buy Product",
"CNY": "CNY",
"Detail": "Detail", "Detail": "Detail",
"Detail - Tooltip": "Detail of product", "Detail - Tooltip": "Detail of product",
"Dummy": "Dummy", "Dummy": "Dummy",
@@ -740,7 +761,6 @@
"Test buy page..": "Test buy page..", "Test buy page..": "Test buy page..",
"There is no payment channel for this product.": "There is no payment channel for this product.", "There is no payment channel for this product.": "There is no payment channel for this product.",
"This product is currently not in sale.": "This product is currently not in sale.", "This product is currently not in sale.": "This product is currently not in sale.",
"USD": "USD",
"WeChat Pay": "WeChat Pay" "WeChat Pay": "WeChat Pay"
}, },
"provider": { "provider": {
@@ -893,6 +913,7 @@
"Signup HTML - Edit": "Signup HTML - Edit", "Signup HTML - Edit": "Signup HTML - Edit",
"Signup HTML - Tooltip": "Custom HTML for replacing the default signup page style", "Signup HTML - Tooltip": "Custom HTML for replacing the default signup page style",
"Signup group": "Signup group", "Signup group": "Signup group",
"Signup group - Tooltip": "Signup group - Tooltip",
"Silent": "Silent", "Silent": "Silent",
"Site key": "Site key", "Site key": "Site key",
"Site key - Tooltip": "Site key", "Site key - Tooltip": "Site key",
@@ -1157,6 +1178,7 @@
"Keys": "Keys", "Keys": "Keys",
"Language": "Language", "Language": "Language",
"Language - Tooltip": "Language - Tooltip", "Language - Tooltip": "Language - Tooltip",
"Last change password time": "Last change password time",
"Link": "Link", "Link": "Link",
"Location": "Location", "Location": "Location",
"Location - Tooltip": "City of residence", "Location - Tooltip": "City of residence",

Some files were not shown because too many files have changed in this diff Show More