Compare commits

...

84 Commits

Author SHA1 Message Date
a0931e4597 feat: add userTypes to Organization 2025-02-09 17:12:13 +08:00
c181006661 feat: cache theme in signup page (#3568) 2025-02-09 15:12:35 +08:00
2e83e49492 feat: fix bug due to null characters in descriptor when creating a payment intent (#3567) 2025-02-08 19:35:51 +08:00
5661942175 feat: add CLI version cache and proxy support (#3565)
* feat: add CLI version cache mechanism

* feat: add /api/refresh-engines to allowed endpoints in demo mode

* feat: add proxy support for cli downloader

* feat: add SafeGoroutine for CLIDownloader initialization

* refactor: optimize code structure
2025-02-08 19:34:19 +08:00
7f9f7c6468 feat: add CLI tools auto-downloader and updater (#3559)
* feat: add CLI downloader feature

* feat: add CLI refresh endpoint and scheduler

* feat: improve binary names mapping for different platforms and architectures

* fix: format binary names in getBinaryNames function

* fix: change file permission notation to octal in cli_downloader.go

* feat: add isDemoMode check for CLI downloader features
2025-02-07 19:22:56 +08:00
b7a818e2d3 feat: support AirWallex payment provider (#3558)
* feat: support AirWallex payment provider

* chore: add some information due to AirWallex's risk control policy
2025-02-07 19:19:30 +08:00
1a8cfe4ee6 feat: can fetch SAML metadata from URL (#3560) 2025-02-06 23:50:39 +08:00
b3526de675 feat: add checkOrgMasterVerificationCode() 2025-02-06 23:46:22 +08:00
3b9e08b70d feat: Fix reset password flow for shared application (#3556) 2025-02-06 18:03:23 +08:00
cfc6015aca feat: rename Casdoor app URL to authenticator (#3553) 2025-02-05 23:08:06 +08:00
1600a6799a feat: return error for updateUsedByCode() 2025-02-05 13:40:41 +08:00
ca60cc3a33 feat: show SAML cert parse error better in frontend (#3551) 2025-02-05 10:06:02 +08:00
df295717f0 feat: can define what Casdoor pages an org admin can see via Organization.NavItems (#3539)
* feat: support define what Casdoor pages an org admin can see

* feat: remove useless code

* fix: fix NavItemNodes i18next invalid

* fix: only global admin can edit navItems

* fix: move navItem tree to extra file
2025-02-03 00:40:21 +08:00
e3001671a2 feat: fix bug that can not delete user if user doesn't belong to any group (#3544) 2025-02-02 17:54:05 +08:00
bbe2162e27 feat: fix bug in GetTokenByTokenValue() (#3541) 2025-01-30 00:48:20 +08:00
92b5ce3722 feat: add identifier validation for security in RunCasbinCommand (#3535)
* feat: add identifier validation for security in RunCasbinCommand

* refactor: update identifier validation to use SHA-256 hash and timestamp
2025-01-29 18:30:06 +08:00
bad21fb6bb feat: check empty password in CheckPassword() 2025-01-28 21:13:59 +08:00
5a78dcf06d feat: fix Casbin Permissions Not Working When Auto-login is Enabled (#3537)
* fix: fix Casbin Permissions Not Working When Auto-login is Enabled

* fix: fix oauth fastLogin not support permission
2025-01-28 19:15:53 +08:00
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
802b6812a9 feat: fix strange "Email is invalid" error in forget password page (#3527) 2025-01-23 14:35:11 +08:00
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
9701818a6e feat: delete groups for user while deleting user (#3525) 2025-01-23 09:46:33 +08:00
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
3d12ac8dc2 feat: improve HandleScim() 2025-01-22 16:15:19 +08:00
f01839123f feat: fix missing param recoveryCodes in /mfa/setup/enable API (#3520) 2025-01-21 22:56:02 +08:00
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
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
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
18b49bb731 feat: can reset LDAP password with different password encryption methods (#3513) 2025-01-20 20:00:23 +08:00
17653888a3 feat: refactor the TestSmtpServer code 2025-01-20 03:17:09 +08:00
ee16616df4 feat: support socks5Proxy for AWS Email provider 2025-01-20 02:39:23 +08:00
ea450005e0 feat: fix "logo" bug in footer 2025-01-20 00:01:46 +08:00
4c5ad14f6b fix: spin will squeeze login panel (#3509) 2025-01-19 23:35:04 +08:00
49dda2aea5 feat: append footerHtml to cookie (#3508) 2025-01-19 23:34:43 +08:00
a74a004540 feat: append logo url to cookie (#3507) 2025-01-19 08:02:44 +08:00
2b89f6b37b feat: fix issue that application theme is ignored in appendThemeCookie() (#3506) 2025-01-18 21:28:39 +08:00
c699e35e6b feat: load theme from first HTML render cookie (#3505) 2025-01-18 19:04:16 +08:00
e28d90d0aa feat: support CUCloud SMN notification provider (#3502) 2025-01-17 08:35:31 +08:00
4fc7600865 feat: skip update user ranking if ranking not in accountItem (#3500) 2025-01-14 22:43:49 +08:00
19f62a461b feat: fix SAML's redirectUrl and POST ProtocolBinding (#3498) 2025-01-13 20:55:37 +08:00
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
b96fa2a995 feat: skip GetUserCount() if there is no quota limit (#3491) 2025-01-10 22:28:25 +08:00
fcfb73af6e feat: increase org password field length to 200 2025-01-09 20:07:49 +08:00
43bebc03b9 feat: fix crash in roleChangeTrigger() 2025-01-09 16:41:56 +08:00
c5f25cbc7d feat: getPidByPort() supports alpine now (#3483)
Signed-off-by: WindSpiritSR <simon343riley@gmail.com>
2025-01-08 12:18:46 +08:00
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
08d6b45fc5 feat: keeps "build" folder during yarn build 2025-01-07 23:38:50 +08:00
56d0de64dc feat: support StopOldInstance() 2025-01-07 21:39:21 +08:00
1813e8e8c7 feat: return goroutine error in get-dashboard API (#3479) 2025-01-07 10:35:45 +08:00
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
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
8457ff7433 feat: support radiusDefaultOrganization in app.conf 2025-01-02 00:10:58 +08:00
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
b57b64fc36 feat: add origin field for mfaAccountTable (#3463) 2024-12-29 22:51:21 +08:00
0d239ba1cf feat: improve the error message of GitHub OAuth provider (#3462) 2024-12-29 21:54:54 +08:00
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
0636069584 feat: only fetch created_time field to reduce data size in get-dashboard API (#3457) 2024-12-28 23:52:19 +08:00
4d0f73c84e feat: fix Casdoor OAuth provider doesn't use domain field bug 2024-12-28 10:01:56 +08:00
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
acc6f3e887 feat: escape the avatal URL in CAS response (#3434) 2024-12-20 17:11:58 +08:00
185ab9750a feat: fix VerificationRecord.IsUsed JSON Field Mapping 2024-12-18 13:56:54 +08:00
48adc050d6 feat: can pass empty user id on user update (#3443) 2024-12-18 07:56:44 +08:00
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
f9a6efc00f feat: advanced model editor should support changing UI language (#3430) 2024-12-15 15:53:29 +08:00
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
e3a43d0062 feat: improve the advanced editor of model edit page (#3427) 2024-12-15 02:07:02 +08:00
0cf281cac0 feat: fix record's password regex bug (#3421) 2024-12-11 08:43:03 +08:00
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
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
01212cd1f3 feat: add AiAssistantUrl to frontend config (#3385) 2024-12-08 20:44:28 +08:00
bf55f94d41 feat: support CUCloud OSS storage provider (#3400) 2024-12-08 20:24:38 +08:00
f14711d315 feat: fix frontend bug 2024-12-07 21:53:01 +08:00
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
922b19c64b feat: reduce i18n items 2024-12-07 21:22:57 +08:00
1d21c3fa90 feat: fix issue that introspectionResponse uses Bearer instead of raw tokenType (#3399) 2024-12-05 20:59:30 +08:00
6175fd6764 feat: make token_type_hint optional (#3397) 2024-12-04 20:10:15 +08:00
2ceb54f058 feat: support most popular currencies (#3388) 2024-12-01 21:46:44 +08:00
aaeaa7fefa feat: update go sms sender (#3386) 2024-11-29 23:00:34 +08:00
d522247552 feat: fix countryCode param bug in MFA login (#3384) 2024-11-29 21:46:06 +08:00
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
fe40910e3b feat: support stateless MFA setup (#3382) 2024-11-29 19:50:10 +08:00
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
12b4d1c7cd feat: change LDAP attribute from cn to title for correct username mapping (#3378) 2024-11-26 09:13:05 +08:00
a45d2b87c1 feat: Add translations for Persian (#3372) 2024-11-23 16:24:07 +08:00
124 changed files with 5012 additions and 2020 deletions

View File

@ -99,6 +99,7 @@ p, *, *, GET, /api/get-all-objects, *, *
p, *, *, GET, /api/get-all-actions, *, *
p, *, *, GET, /api/get-all-roles, *, *
p, *, *, GET, /api/run-casbin-command, *, *
p, *, *, POST, /api/refresh-engines, *, *
p, *, *, GET, /api/get-invitation-info, *, *
p, *, *, GET, /api/faceid-signin-begin, *, *
`
@ -156,7 +157,7 @@ func IsAllowed(subOwner string, subName string, method string, urlPath string, o
func isAllowedInDemoMode(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool {
if method == "POST" {
if strings.HasPrefix(urlPath, "/api/login") || urlPath == "/api/logout" || urlPath == "/api/signup" || urlPath == "/api/callback" || urlPath == "/api/send-verification-code" || urlPath == "/api/send-email" || urlPath == "/api/verify-captcha" || urlPath == "/api/verify-code" || urlPath == "/api/check-user-password" || strings.HasPrefix(urlPath, "/api/mfa/") || urlPath == "/api/webhook" || urlPath == "/api/get-qrcode" {
if strings.HasPrefix(urlPath, "/api/login") || urlPath == "/api/logout" || urlPath == "/api/signup" || urlPath == "/api/callback" || urlPath == "/api/send-verification-code" || urlPath == "/api/send-email" || urlPath == "/api/verify-captcha" || urlPath == "/api/verify-code" || urlPath == "/api/check-user-password" || strings.HasPrefix(urlPath, "/api/mfa/") || urlPath == "/api/webhook" || urlPath == "/api/get-qrcode" || urlPath == "/api/refresh-engines" {
return true
} else if urlPath == "/api/update-user" {
// Allow ordinary users to update their own information

View File

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

View File

@ -22,6 +22,7 @@ import (
"io"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
@ -305,6 +306,35 @@ func isProxyProviderType(providerType string) bool {
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 ...
// @Title Login
// @Tag Login API
@ -330,6 +360,8 @@ func (c *ApiController) Login() {
return
}
verificationType := ""
if authForm.Username != "" {
if authForm.Type == ResponseTypeLogin {
if c.GetSessionUsername() != "" {
@ -424,6 +456,12 @@ func (c *ApiController) Login() {
c.ResponseError(err.Error(), nil)
return
}
if verificationCodeType == object.VerifyTypePhone {
verificationType = "sms"
} else {
verificationType = "email"
}
} else {
var application *object.Application
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
@ -514,16 +552,7 @@ func (c *ApiController) Login() {
c.ResponseError(err.Error())
}
if object.IsNeedPromptMfa(organization, user) {
// 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))
if checkMfaEnable(c, user, organization, verificationType) {
return
}
@ -617,6 +646,17 @@ func (c *ApiController) Login() {
c.ResponseError(fmt.Sprintf(c.T("auth:Failed to login in: %s"), err.Error()))
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" {
@ -648,6 +688,11 @@ func (c *ApiController) Login() {
c.ResponseError(err.Error())
return
}
if checkMfaEnable(c, user, organization, verificationType) {
return
}
resp = c.HandleLoggedIn(application, user, &authForm)
c.Ctx.Input.SetParam("recordUserId", user.GetId())
@ -854,18 +899,32 @@ func (c *ApiController) Login() {
}
if authForm.Passcode != "" {
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.GetPreferredMfaProps(false))
mfaUtil := object.GetMfaUtil(authForm.MfaType, user.GetMfaProps(authForm.MfaType, false))
if mfaUtil == nil {
c.ResponseError("Invalid multi-factor authentication type")
return
}
err = mfaUtil.Verify(authForm.Passcode)
passed, err := c.checkOrgMasterVerificationCode(user, authForm.Passcode)
if err != nil {
c.ResponseError(err.Error())
return
}
if !passed {
err = mfaUtil.Verify(authForm.Passcode)
if err != nil {
c.ResponseError(err.Error())
return
}
}
c.SetSession("verificationCodeType", "")
} else if authForm.RecoveryCode != "" {
err = object.MfaRecover(user, authForm.RecoveryCode)
if err != nil {
@ -878,7 +937,11 @@ func (c *ApiController) Login() {
}
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 {
c.ResponseError(err.Error())
return
@ -908,6 +971,10 @@ func (c *ApiController) Login() {
return
}
if authForm.Provider == "" {
authForm.Provider = authForm.ProviderBack
}
user := c.getCurrentUser()
resp = c.HandleLoggedIn(application, user, &authForm)

View File

@ -15,13 +15,76 @@
package controllers
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"os/exec"
"sort"
"strings"
"sync"
"time"
)
type CLIVersionInfo struct {
Version string
BinaryPath string
BinaryTime time.Time
}
var (
cliVersionCache = make(map[string]*CLIVersionInfo)
cliVersionMutex sync.RWMutex
)
// getCLIVersion
// @Title getCLIVersion
// @Description Get CLI version with cache mechanism
// @Param language string The language of CLI (go/java/rust etc.)
// @Return string The version string of CLI
// @Return error Error if CLI execution fails
func getCLIVersion(language string) (string, error) {
binaryName := fmt.Sprintf("casbin-%s-cli", language)
binaryPath, err := exec.LookPath(binaryName)
if err != nil {
return "", fmt.Errorf("executable file not found: %v", err)
}
fileInfo, err := os.Stat(binaryPath)
if err != nil {
return "", fmt.Errorf("failed to get binary info: %v", err)
}
cliVersionMutex.RLock()
if info, exists := cliVersionCache[language]; exists {
if info.BinaryPath == binaryPath && info.BinaryTime == fileInfo.ModTime() {
cliVersionMutex.RUnlock()
return info.Version, nil
}
}
cliVersionMutex.RUnlock()
cmd := exec.Command(binaryName, "--version")
output, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("failed to get CLI version: %v", err)
}
version := strings.TrimSpace(string(output))
cliVersionMutex.Lock()
cliVersionCache[language] = &CLIVersionInfo{
Version: version,
BinaryPath: binaryPath,
BinaryTime: fileInfo.ModTime(),
}
cliVersionMutex.Unlock()
return version, nil
}
func processArgsToTempFiles(args []string) ([]string, []string, error) {
tempFiles := []string{}
newArgs := []string{}
@ -57,6 +120,11 @@ func processArgsToTempFiles(args []string) ([]string, []string, error) {
// @Success 200 {object} controllers.Response The Response object
// @router /run-casbin-command [get]
func (c *ApiController) RunCasbinCommand() {
if err := validateIdentifier(c); err != nil {
c.ResponseError(err.Error())
return
}
language := c.Input().Get("language")
argString := c.Input().Get("args")
@ -84,6 +152,16 @@ func (c *ApiController) RunCasbinCommand() {
return
}
if len(args) > 0 && args[0] == "--version" {
version, err := getCLIVersion(language)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(version)
return
}
tempFiles, processedArgs, err := processArgsToTempFiles(args)
defer func() {
for _, file := range tempFiles {
@ -112,3 +190,58 @@ func (c *ApiController) RunCasbinCommand() {
output = strings.TrimSuffix(output, "\n")
c.ResponseOk(output)
}
// validateIdentifier
// @Title validateIdentifier
// @Description Validate the request hash and timestamp
// @Param hash string The SHA-256 hash string
// @Return error Returns error if validation fails, nil if successful
func validateIdentifier(c *ApiController) error {
language := c.Input().Get("language")
args := c.Input().Get("args")
hash := c.Input().Get("m")
timestamp := c.Input().Get("t")
if hash == "" || timestamp == "" || language == "" || args == "" {
return fmt.Errorf("invalid identifier")
}
requestTime, err := time.Parse(time.RFC3339, timestamp)
if err != nil {
return fmt.Errorf("invalid identifier")
}
timeDiff := time.Since(requestTime)
if timeDiff > 5*time.Minute || timeDiff < -5*time.Minute {
return fmt.Errorf("invalid identifier")
}
params := map[string]string{
"language": language,
"args": args,
}
keys := make([]string, 0, len(params))
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
var paramParts []string
for _, k := range keys {
paramParts = append(paramParts, fmt.Sprintf("%s=%s", k, params[k]))
}
paramString := strings.Join(paramParts, "&")
version := "casbin-editor-v1"
rawString := fmt.Sprintf("%s|%s|%s", version, timestamp, paramString)
hasher := sha256.New()
hasher.Write([]byte(rawString))
calculatedHash := strings.ToLower(hex.EncodeToString(hasher.Sum(nil)))
if calculatedHash != strings.ToLower(hash) {
return fmt.Errorf("invalid identifier")
}
return nil
}

View File

@ -0,0 +1,519 @@
package controllers
import (
"archive/tar"
"archive/zip"
"compress/gzip"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/beego/beego"
"github.com/casdoor/casdoor/proxy"
"github.com/casdoor/casdoor/util"
)
const (
javaCliRepo = "https://api.github.com/repos/jcasbin/casbin-java-cli/releases/latest"
goCliRepo = "https://api.github.com/repos/casbin/casbin-go-cli/releases/latest"
rustCliRepo = "https://api.github.com/repos/casbin-rs/casbin-rust-cli/releases/latest"
downloadFolder = "bin"
)
type ReleaseInfo struct {
TagName string `json:"tag_name"`
Assets []struct {
Name string `json:"name"`
URL string `json:"browser_download_url"`
} `json:"assets"`
}
// @Title getBinaryNames
// @Description Get binary names for different platforms and architectures
// @Success 200 {map[string]string} map[string]string "Binary names map"
func getBinaryNames() map[string]string {
const (
golang = "go"
java = "java"
rust = "rust"
)
arch := runtime.GOARCH
archMap := map[string]struct{ goArch, rustArch string }{
"amd64": {"x86_64", "x86_64"},
"arm64": {"arm64", "aarch64"},
}
archNames, ok := archMap[arch]
if !ok {
archNames = struct{ goArch, rustArch string }{arch, arch}
}
switch runtime.GOOS {
case "windows":
return map[string]string{
golang: fmt.Sprintf("casbin-go-cli_Windows_%s.zip", archNames.goArch),
java: "casbin-java-cli.jar",
rust: fmt.Sprintf("casbin-rust-cli-%s-pc-windows-gnu", archNames.rustArch),
}
case "darwin":
return map[string]string{
golang: fmt.Sprintf("casbin-go-cli_Darwin_%s.tar.gz", archNames.goArch),
java: "casbin-java-cli.jar",
rust: fmt.Sprintf("casbin-rust-cli-%s-apple-darwin", archNames.rustArch),
}
case "linux":
return map[string]string{
golang: fmt.Sprintf("casbin-go-cli_Linux_%s.tar.gz", archNames.goArch),
java: "casbin-java-cli.jar",
rust: fmt.Sprintf("casbin-rust-cli-%s-unknown-linux-gnu", archNames.rustArch),
}
default:
return nil
}
}
// @Title getFinalBinaryName
// @Description Get final binary name for specific language
// @Param lang string true "Language type (go/java/rust)"
// @Success 200 {string} string "Final binary name"
func getFinalBinaryName(lang string) string {
switch lang {
case "go":
if runtime.GOOS == "windows" {
return "casbin-go-cli.exe"
}
return "casbin-go-cli"
case "java":
return "casbin-java-cli.jar"
case "rust":
if runtime.GOOS == "windows" {
return "casbin-rust-cli.exe"
}
return "casbin-rust-cli"
default:
return ""
}
}
// @Title getLatestCLIURL
// @Description Get latest CLI download URL from GitHub
// @Param repoURL string true "GitHub repository URL"
// @Param language string true "Language type"
// @Success 200 {string} string "Download URL and version"
func getLatestCLIURL(repoURL string, language string) (string, string, error) {
client := proxy.GetHttpClient(repoURL)
resp, err := client.Get(repoURL)
if err != nil {
return "", "", fmt.Errorf("failed to fetch release info: %v", err)
}
defer resp.Body.Close()
var release ReleaseInfo
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
return "", "", err
}
binaryNames := getBinaryNames()
if binaryNames == nil {
return "", "", fmt.Errorf("unsupported OS: %s", runtime.GOOS)
}
binaryName := binaryNames[language]
for _, asset := range release.Assets {
if asset.Name == binaryName {
return asset.URL, release.TagName, nil
}
}
return "", "", fmt.Errorf("no suitable binary found for OS: %s, language: %s", runtime.GOOS, language)
}
// @Title extractGoCliFile
// @Description Extract the Go CLI file
// @Param filePath string true "The file path"
// @Success 200 {string} string "The extracted file path"
// @router /extractGoCliFile [post]
func extractGoCliFile(filePath string) error {
tempDir := filepath.Join(downloadFolder, "temp")
if err := os.MkdirAll(tempDir, 0o755); err != nil {
return err
}
defer os.RemoveAll(tempDir)
if runtime.GOOS == "windows" {
if err := unzipFile(filePath, tempDir); err != nil {
return err
}
} else {
if err := untarFile(filePath, tempDir); err != nil {
return err
}
}
execName := "casbin-go-cli"
if runtime.GOOS == "windows" {
execName += ".exe"
}
var execPath string
err := filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error {
if info.Name() == execName {
execPath = path
return nil
}
return nil
})
if err != nil {
return err
}
finalPath := filepath.Join(downloadFolder, execName)
if err := os.Rename(execPath, finalPath); err != nil {
return err
}
return os.Remove(filePath)
}
// @Title unzipFile
// @Description Unzip the file
// @Param zipPath string true "The zip file path"
// @Param destDir string true "The destination directory"
// @Success 200 {string} string "The extracted file path"
// @router /unzipFile [post]
func unzipFile(zipPath, destDir string) error {
r, err := zip.OpenReader(zipPath)
if err != nil {
return err
}
defer r.Close()
for _, f := range r.File {
fpath := filepath.Join(destDir, f.Name)
if f.FileInfo().IsDir() {
os.MkdirAll(fpath, os.ModePerm)
continue
}
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
return err
}
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
rc, err := f.Open()
if err != nil {
outFile.Close()
return err
}
_, err = io.Copy(outFile, rc)
outFile.Close()
rc.Close()
if err != nil {
return err
}
}
return nil
}
// @Title untarFile
// @Description Untar the file
// @Param tarPath string true "The tar file path"
// @Param destDir string true "The destination directory"
// @Success 200 {string} string "The extracted file path"
// @router /untarFile [post]
func untarFile(tarPath, destDir string) error {
file, err := os.Open(tarPath)
if err != nil {
return err
}
defer file.Close()
gzr, err := gzip.NewReader(file)
if err != nil {
return err
}
defer gzr.Close()
tr := tar.NewReader(gzr)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
path := filepath.Join(destDir, header.Name)
switch header.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(path, 0o755); err != nil {
return err
}
case tar.TypeReg:
outFile, err := os.Create(path)
if err != nil {
return err
}
if _, err := io.Copy(outFile, tr); err != nil {
outFile.Close()
return err
}
outFile.Close()
}
}
return nil
}
// @Title createJavaCliWrapper
// @Description Create the Java CLI wrapper
// @Param binPath string true "The binary path"
// @Success 200 {string} string "The created file path"
// @router /createJavaCliWrapper [post]
func createJavaCliWrapper(binPath string) error {
if runtime.GOOS == "windows" {
// Create a Windows CMD file
cmdPath := filepath.Join(binPath, "casbin-java-cli.cmd")
cmdContent := fmt.Sprintf(`@echo off
java -jar "%s\casbin-java-cli.jar" %%*`, binPath)
err := os.WriteFile(cmdPath, []byte(cmdContent), 0o755)
if err != nil {
return fmt.Errorf("failed to create Java CLI wrapper: %v", err)
}
} else {
// Create Unix shell script
shPath := filepath.Join(binPath, "casbin-java-cli")
shContent := fmt.Sprintf(`#!/bin/sh
java -jar "%s/casbin-java-cli.jar" "$@"`, binPath)
err := os.WriteFile(shPath, []byte(shContent), 0o755)
if err != nil {
return fmt.Errorf("failed to create Java CLI wrapper: %v", err)
}
}
return nil
}
// @Title downloadCLI
// @Description Download and setup CLI tools
// @Success 200 {error} error "Error if any"
func downloadCLI() error {
pathEnv := os.Getenv("PATH")
binPath, err := filepath.Abs(downloadFolder)
if err != nil {
return fmt.Errorf("failed to get absolute path to download directory: %v", err)
}
if !strings.Contains(pathEnv, binPath) {
newPath := fmt.Sprintf("%s%s%s", binPath, string(os.PathListSeparator), pathEnv)
if err := os.Setenv("PATH", newPath); err != nil {
return fmt.Errorf("failed to update PATH environment variable: %v", err)
}
}
if err := os.MkdirAll(downloadFolder, 0o755); err != nil {
return fmt.Errorf("failed to create download directory: %v", err)
}
repos := map[string]string{
"java": javaCliRepo,
"go": goCliRepo,
"rust": rustCliRepo,
}
for lang, repo := range repos {
cliURL, version, err := getLatestCLIURL(repo, lang)
if err != nil {
fmt.Printf("failed to get %s CLI URL: %v\n", lang, err)
continue
}
originalPath := filepath.Join(downloadFolder, getBinaryNames()[lang])
fmt.Printf("downloading %s CLI: %s\n", lang, cliURL)
client := proxy.GetHttpClient(cliURL)
resp, err := client.Get(cliURL)
if err != nil {
fmt.Printf("failed to download %s CLI: %v\n", lang, err)
continue
}
func() {
defer resp.Body.Close()
if err := os.MkdirAll(filepath.Dir(originalPath), 0o755); err != nil {
fmt.Printf("failed to create directory for %s CLI: %v\n", lang, err)
return
}
tmpFile := originalPath + ".tmp"
out, err := os.Create(tmpFile)
if err != nil {
fmt.Printf("failed to create or write %s CLI: %v\n", lang, err)
return
}
defer func() {
out.Close()
os.Remove(tmpFile)
}()
if _, err = io.Copy(out, resp.Body); err != nil ||
out.Close() != nil ||
os.Rename(tmpFile, originalPath) != nil {
fmt.Printf("failed to download %s CLI: %v\n", lang, err)
return
}
}()
if lang == "go" {
if err := extractGoCliFile(originalPath); err != nil {
fmt.Printf("failed to extract Go CLI: %v\n", err)
continue
}
} else {
finalPath := filepath.Join(downloadFolder, getFinalBinaryName(lang))
if err := os.Rename(originalPath, finalPath); err != nil {
fmt.Printf("failed to rename %s CLI: %v\n", lang, err)
continue
}
}
if runtime.GOOS != "windows" {
execPath := filepath.Join(downloadFolder, getFinalBinaryName(lang))
if err := os.Chmod(execPath, 0o755); err != nil {
fmt.Printf("failed to set %s CLI execution permission: %v\n", lang, err)
continue
}
}
fmt.Printf("downloaded %s CLI version: %s\n", lang, version)
if lang == "java" {
if err := createJavaCliWrapper(binPath); err != nil {
fmt.Printf("failed to create Java CLI wrapper: %v\n", err)
continue
}
}
}
return nil
}
// @Title RefreshEngines
// @Tag CLI API
// @Description Refresh all CLI engines
// @Param m query string true "Hash for request validation"
// @Param t query string true "Timestamp for request validation"
// @Success 200 {object} controllers.Response The Response object
// @router /refresh-engines [post]
func (c *ApiController) RefreshEngines() {
if !beego.AppConfig.DefaultBool("isDemoMode", false) {
c.ResponseError("refresh engines is only available in demo mode")
return
}
hash := c.Input().Get("m")
timestamp := c.Input().Get("t")
if hash == "" || timestamp == "" {
c.ResponseError("invalid identifier")
return
}
requestTime, err := time.Parse(time.RFC3339, timestamp)
if err != nil {
c.ResponseError("invalid identifier")
return
}
timeDiff := time.Since(requestTime)
if timeDiff > 5*time.Minute || timeDiff < -5*time.Minute {
c.ResponseError("invalid identifier")
return
}
version := "casbin-editor-v1"
rawString := fmt.Sprintf("%s|%s", version, timestamp)
hasher := sha256.New()
hasher.Write([]byte(rawString))
calculatedHash := strings.ToLower(hex.EncodeToString(hasher.Sum(nil)))
if calculatedHash != strings.ToLower(hash) {
c.ResponseError("invalid identifier")
return
}
err = downloadCLI()
if err != nil {
c.ResponseError(fmt.Sprintf("failed to refresh engines: %v", err))
return
}
c.ResponseOk(map[string]string{
"status": "success",
"message": "CLI engines updated successfully",
})
}
// @Title ScheduleCLIUpdater
// @Description Start periodic CLI update scheduler
func ScheduleCLIUpdater() {
if !beego.AppConfig.DefaultBool("isDemoMode", false) {
return
}
ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()
for range ticker.C {
err := downloadCLI()
if err != nil {
fmt.Printf("failed to update CLI: %v\n", err)
} else {
fmt.Println("CLI updated successfully")
}
}
}
// @Title DownloadCLI
// @Description Download the CLI
// @Success 200 {string} string "The downloaded file path"
// @router /downloadCLI [post]
func DownloadCLI() error {
return downloadCLI()
}
// @Title InitCLIDownloader
// @Description Initialize CLI downloader and start update scheduler
func InitCLIDownloader() {
if !beego.AppConfig.DefaultBool("isDemoMode", false) {
return
}
util.SafeGoroutine(func() {
err := DownloadCLI()
if err != nil {
fmt.Printf("failed to initialize CLI downloader: %v\n", err)
}
ScheduleCLIUpdater()
})
}

View File

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

View File

@ -124,7 +124,9 @@ func (c *ApiController) UpdateOrganization() {
return
}
c.Data["json"] = wrapActionResponse(object.UpdateOrganization(id, &organization))
isGlobalAdmin, _ := c.isGlobalAdmin()
c.Data["json"] = wrapActionResponse(object.UpdateOrganization(id, &organization, isGlobalAdmin))
c.ServeJSON()
}

View File

@ -21,6 +21,11 @@ import (
)
func (c *RootController) HandleScim() {
_, ok := c.RequireAdmin()
if !ok {
return
}
path := c.Ctx.Request.URL.Path
c.Ctx.Request.URL.Path = strings.TrimPrefix(path, "/scim")
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
if len(emailForm.Receivers) == 1 && emailForm.Receivers[0] == "TestSmtpServer" {
err = object.DailSmtpServer(provider)
err = object.TestSmtpServer(provider)
if err != nil {
c.ResponseError(err.Error())
return

View File

@ -322,17 +322,22 @@ func (c *ApiController) IntrospectToken() {
}
tokenTypeHint := c.Input().Get("token_type_hint")
token, err := object.GetTokenByTokenValue(tokenValue, tokenTypeHint)
if err != nil {
c.ResponseTokenError(err.Error())
return
}
if token == nil {
c.Data["json"] = &object.IntrospectionResponse{Active: false}
c.ServeJSON()
return
var token *object.Token
if tokenTypeHint != "" {
token, err = object.GetTokenByTokenValue(tokenValue, tokenTypeHint)
if err != nil {
c.ResponseTokenError(err.Error())
return
}
if token == nil {
c.Data["json"] = &object.IntrospectionResponse{Active: false}
c.ServeJSON()
return
}
}
var introspectionResponse object.IntrospectionResponse
if application.TokenFormat == "JWT-Standard" {
jwtToken, err := object.ParseStandardJwtTokenByApplication(tokenValue, application)
if err != nil || jwtToken.Valid() != nil {
@ -344,12 +349,37 @@ func (c *ApiController) IntrospectToken() {
return
}
c.Data["json"] = &object.IntrospectionResponse{
introspectionResponse = object.IntrospectionResponse{
Active: true,
Scope: jwtToken.Scope,
ClientId: clientId,
Username: token.User,
TokenType: token.TokenType,
Username: jwtToken.Name,
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(),
Iat: jwtToken.IssuedAt.Unix(),
Nbf: jwtToken.NotBefore.Unix(),
@ -358,33 +388,22 @@ func (c *ApiController) IntrospectToken() {
Iss: jwtToken.Issuer,
Jti: jwtToken.ID,
}
c.ServeJSON()
return
}
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
if tokenTypeHint == "" {
token, err = object.GetTokenByTokenValue(tokenValue, introspectionResponse.TokenType)
if err != nil {
c.ResponseTokenError(err.Error())
return
}
if token == nil {
c.Data["json"] = &object.IntrospectionResponse{Active: false}
c.ServeJSON()
return
}
}
introspectionResponse.TokenType = token.TokenType
c.Data["json"] = &object.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.Data["json"] = introspectionResponse
c.ServeJSON()
}

View File

@ -353,13 +353,7 @@ func (c *ApiController) AddUser() {
return
}
count, err := object.GetUserCount("", "", "", "")
if err != nil {
c.ResponseError(err.Error())
return
}
if err := checkQuotaForUser(int(count)); err != nil {
if err := checkQuotaForUser(); err != nil {
c.ResponseError(err.Error())
return
}
@ -475,6 +469,16 @@ func (c *ApiController) SetPassword() {
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()
if requestUserId == "" && code == "" {
c.ResponseError(c.T("general:Please login first"), "Please login first")
@ -518,7 +522,11 @@ func (c *ApiController) SetPassword() {
}
}
} 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 {
c.ResponseError(err.Error())
return
@ -563,7 +571,16 @@ func (c *ApiController) SetPassword() {
targetUser.NeedUpdatePassword = false
targetUser.LastChangePasswordTime = util.GetCurrentTime()
_, err = object.UpdateUser(userId, targetUser, []string{"password", "need_update_password", "password_type", "last_change_password_time"}, false)
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())
}
}
if err != nil {
c.ResponseError(err.Error())
return

View File

@ -294,12 +294,18 @@ func checkQuotaForProvider(count int) error {
return nil
}
func checkQuotaForUser(count int) error {
func checkQuotaForUser() error {
quota := conf.GetConfigQuota().User
if quota == -1 {
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 nil

View File

@ -246,8 +246,6 @@ func (c *ApiController) SendVerificationCode() {
if user != nil && util.GetMaskedEmail(mfaProps.Secret) == vform.Dest {
vform.Dest = mfaProps.Secret
}
} else if vform.Method == MfaSetupVerification {
c.SetSession(MfaDestSession, vform.Dest)
}
provider, err = application.GetEmailProvider(vform.Method)
@ -282,11 +280,6 @@ func (c *ApiController) SendVerificationCode() {
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 {
mfaProps := user.GetPreferredMfaProps(false)
if user != nil && util.GetMaskedPhone(mfaProps.Secret) == vform.Dest {
@ -517,20 +510,28 @@ func (c *ApiController) VerifyCode() {
}
}
result, err := object.CheckVerificationCode(checkDest, authForm.Code, c.GetAcceptLanguage())
passed, err := c.checkOrgMasterVerificationCode(user, authForm.Code)
if err != nil {
c.ResponseError(c.T(err.Error()))
return
}
if result.Code != object.VerificationSuccess {
c.ResponseError(result.Msg)
return
}
err = object.DisableVerificationCode(checkDest)
if err != nil {
c.ResponseError(err.Error())
return
if !passed {
result, err := object.CheckVerificationCode(checkDest, authForm.Code, c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error())
return
}
if result.Code != object.VerificationSuccess {
c.ResponseError(result.Msg)
return
}
err = object.DisableVerificationCode(checkDest)
if err != nil {
c.ResponseError(err.Error())
return
}
}
c.SetSession("verifiedCode", authForm.Code)

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 controllers
import (
"fmt"
"github.com/casdoor/casdoor/object"
)
func (c *ApiController) checkOrgMasterVerificationCode(user *object.User, code string) (bool, error) {
organization, err := object.GetOrganizationByUser(user)
if err != nil {
return false, err
}
if organization == nil {
return false, fmt.Errorf("The organization: %s does not exist", user.Owner)
}
if organization.MasterVerificationCode != "" && organization.MasterVerificationCode == code {
return true, nil
}
return false, nil
}

View File

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

View File

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

11
go.mod
View File

@ -9,10 +9,10 @@ require (
github.com/beego/beego v1.12.12
github.com/beevik/etree v1.1.0
github.com/casbin/casbin/v2 v2.77.2
github.com/casdoor/go-sms-sender v0.24.0
github.com/casdoor/gomail/v2 v2.0.1
github.com/casdoor/go-sms-sender v0.25.0
github.com/casdoor/gomail/v2 v2.1.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/xorm-adapter/v3 v3.1.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/xorm v1.1.6
github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/crypto v0.21.0
golang.org/x/net v0.21.0
golang.org/x/crypto v0.32.0
golang.org/x/net v0.34.0
golang.org/x/oauth2 v0.17.0
golang.org/x/text v0.21.0
google.golang.org/api v0.150.0
gopkg.in/ini.v1 v1.67.0 // indirect
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/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-sms-sender v0.24.0 h1:LNLsce3EG/87I3JS6UiajF3LlQmdIiCgebEu0IE4wSM=
github.com/casdoor/go-sms-sender v0.24.0/go.mod h1:bOm4H8/YfJmEHjBatEVQFOnAf0OOn1B0Wi5B7zDhws0=
github.com/casdoor/gomail/v2 v2.0.1 h1:J+FG6x80s9e5lBHUn8Sv0Y56mud34KiWih5YdmudR/w=
github.com/casdoor/gomail/v2 v2.0.1/go.mod h1:VnGPslEAtpix5FjHisR/WKB1qvZDBaujbikxDe9d+2Q=
github.com/casdoor/go-sms-sender v0.25.0 h1:eF4cOCSbjVg7+0uLlJQnna/FQ0BWW+Fp/x4cXhzQu1Y=
github.com/casdoor/go-sms-sender v0.25.0/go.mod h1:bOm4H8/YfJmEHjBatEVQFOnAf0OOn1B0Wi5B7zDhws0=
github.com/casdoor/gomail/v2 v2.1.0 h1:ua97E3CARnF1Ik8ga/Drz9uGZfaElXJumFexiErWUxM=
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/go.mod h1:VwYU2vqQ2pA8sa00PRekH71R2XmgfzMKhmp1XrrDu2s=
github.com/casdoor/notify v0.45.0 h1:OlaFvcQFjGOgA4mRx07M8AH1gvb5xNo21mcqrVGlLgk=
github.com/casdoor/notify v0.45.0/go.mod h1:wNHQu0tiDROMBIvz0j3Om3Lhd5yZ+AIfnFb8MYb8OLQ=
github.com/casdoor/notify v1.0.0 h1:oldsaaQFPrlufm/OA314z8DwFVE1Tc9Gt1z4ptRHhXw=
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/go.mod h1:uaqO7KBI2lnZcnB8rF7O6C2bN7llIbfC5Ql8ex1yR1U=
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.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
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.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-20180807140117-3d87b88a115f/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.10.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.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-20180724234803-3673e40ba225/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.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
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.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-20190226205417-e64efc72b421/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.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.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
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-20180830151530-49385e6e1522/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.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.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
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.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
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.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.3.0/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.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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
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-20190308202827-9d24e82272b4/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.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
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.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-20191011141410-1b5146add898/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": {
"Failed to add user": "Failed to add user",
"Get init score failed, error: %w": "Get init score failed, error: %w",
"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"
"Failed to add user": "عدم موفقیت در افزودن کاربر",
"Get init score failed, error: %w": "عدم موفقیت در دریافت امتیاز اولیه، خطا: %w",
"Please sign out first": "لطفاً ابتدا خارج شوید",
"The application does not allow to sign up new account": "برنامه اجازه ثبت‌نام حساب جدید را نمی‌دهد"
},
"auth": {
"Challenge method should be S256": "Challenge method should be S256",
"Failed to create user, user information is invalid: %s": "Failed to create user, user information is invalid: %s",
"Failed to login in: %s": "Failed to login in: %s",
"Invalid token": "Invalid token",
"State expected: %s, but got: %s": "State expected: %s, but got: %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, 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) 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 application: %s does not exist": "The application: %s does not exist",
"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 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 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 organization: %s does not exist": "The organization: %s does not exist",
"The provider: %s is not enabled for the application": "The provider: %s is not enabled for the application",
"Unauthorized operation": "Unauthorized operation",
"Unknown authentication type (not password or provider), form = %s": "Unknown authentication type (not password or provider), form = %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",
"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"
"Challenge method should be S256": "روش چالش باید S256 باشد",
"Failed to create user, user information is invalid: %s": "عدم موفقیت در ایجاد کاربر، اطلاعات کاربر نامعتبر است: %s",
"Failed to login in: %s": "عدم موفقیت در ورود: %s",
"Invalid token": "توکن نامعتبر",
"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": "حساب برای ارائه‌دهنده: %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": "حساب برای ارائه‌دهنده: %s و نام کاربری: %s (%s) وجود ندارد و مجاز به ثبت‌نام به‌عنوان حساب جدید نیست، لطفاً با پشتیبانی IT خود تماس بگیرید",
"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": "برنامه: %s وجود ندارد",
"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 email 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 organization: %s does not exist": "سازمان: %s وجود ندارد",
"The provider: %s is not enabled for the application": "ارائه‌دهنده: %s برای برنامه فعال نیست",
"Unauthorized operation": "عملیات غیرمجاز",
"Unknown authentication type (not password or provider), form = %s": "نوع احراز هویت ناشناخته (نه رمز عبور و نه ارائه‌دهنده)، فرم = %s",
"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": "کاربر پرداختی %s اشتراک فعال یا در انتظار ندارد و برنامه: %s قیمت‌گذاری پیش‌فرض ندارد"
},
"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": {
"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",
"DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists",
"Email cannot be empty": "Email cannot be empty",
"Email is invalid": "Email is invalid",
"Empty username.": "Empty username.",
"Face data does not exist, cannot log in": "Face data does not exist, cannot log in",
"Face data mismatch": "Face data mismatch",
"FirstName cannot be blank": "FirstName cannot be blank",
"Invitation code cannot be blank": "Invitation code cannot be blank",
"Invitation code exhausted": "Invitation code exhausted",
"Invitation code is invalid": "Invitation code is invalid",
"Invitation code suspended": "Invitation code suspended",
"LDAP user name or password incorrect": "LDAP user name or password incorrect",
"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",
"Organization does not exist": "Organization does not exist",
"Phone already exists": "Phone already exists",
"Phone cannot be empty": "Phone cannot be empty",
"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 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",
"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 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 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 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\\\"",
"Username already exists": "Username already exists",
"Username cannot be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces",
"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 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",
"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, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
"Affiliation cannot be blank": "وابستگی نمی‌تواند خالی باشد",
"Default code does not match the code's matching rules": "کد پیش‌فرض با قوانین تطبیق کد مطابقت ندارد",
"DisplayName cannot be blank": "نام نمایشی نمی‌تواند خالی باشد",
"DisplayName is not valid real name": "نام نمایشی یک نام واقعی معتبر نیست",
"Email already exists": "ایمیل قبلاً وجود دارد",
"Email cannot be empty": "ایمیل نمی‌تواند خالی باشد",
"Email is invalid": "ایمیل نامعتبر است",
"Empty username.": "نام کاربری خالی است.",
"Face data does not exist, cannot log in": "داده‌های چهره وجود ندارد، نمی‌توان وارد شد",
"Face data mismatch": "عدم تطابق داده‌های چهره",
"FirstName cannot be blank": "نام نمی‌تواند خالی باشد",
"Invitation code cannot be blank": "کد دعوت نمی‌تواند خالی باشد",
"Invitation code exhausted": "کد دعوت استفاده شده است",
"Invitation code is invalid": "کد دعوت نامعتبر است",
"Invitation code suspended": "کد دعوت معلق است",
"LDAP user name or password incorrect": "نام کاربری یا رمز عبور LDAP نادرست است",
"LastName cannot be blank": "نام خانوادگی نمی‌تواند خالی باشد",
"Multiple accounts with same uid, please check your ldap server": "چندین حساب با uid یکسان، لطفاً سرور LDAP خود را بررسی کنید",
"Organization does not exist": "سازمان وجود ندارد",
"Phone already exists": "تلفن قبلاً وجود دارد",
"Phone cannot be empty": "تلفن نمی‌تواند خالی باشد",
"Phone number is invalid": "شماره تلفن نامعتبر است",
"Please register using the email 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": "لطفاً با استفاده از نام کاربری مربوط به کد دعوت ثبت‌نام کنید",
"Session outdated, please login again": "جلسه منقضی شده است، لطفاً دوباره وارد شوید",
"The invitation code has already been used": "کد دعوت قبلاً استفاده شده است",
"The user is forbidden to sign in, please contact the administrator": "ورود کاربر ممنوع است، لطفاً با مدیر تماس بگیرید",
"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 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\"": "مقدار \"%s\" برای فیلد ثبت‌نام \"%s\" با عبارت منظم مورد ثبت‌نام برنامه \"%s\" مطابقت ندارد",
"Username already exists": "نام کاربری قبلاً وجود دارد",
"Username cannot be an email address": "نام کاربری نمی‌تواند یک آدرس ایمیل باشد",
"Username cannot contain white spaces": "نام کاربری نمی‌تواند حاوی فاصله باشد",
"Username cannot start with a digit": "نام کاربری نمی‌تواند با یک رقم شروع شود",
"Username is too long (maximum is 39 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": "شما رمز عبور یا کد اشتباه را بیش از حد وارد کرده‌اید، لطفاً %d دقیقه صبر کنید و دوباره تلاش کنید",
"Your region is not allow to signup by phone": "منطقه شما اجازه ثبت‌نام با تلفن را ندارد",
"password or code is incorrect": "رمز عبور یا کد نادرست است",
"password or code is incorrect, you have %d remaining chances": "رمز عبور یا کد نادرست است، شما %d فرصت باقی‌مانده دارید",
"unsupported password type: %s": "نوع رمز عبور پشتیبانی نشده: %s"
},
"general": {
"Missing parameter": "Missing parameter",
"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 user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: ",
"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"
"Missing parameter": "پارامتر گمشده",
"Please login first": "لطفاً ابتدا وارد شوید",
"The organization: %s should have one application at least": "سازمان: %s باید حداقل یک برنامه داشته باشد",
"The user: %s doesn't exist": "کاربر: %s وجود ندارد",
"don't support captchaProvider: ": "از captchaProvider پشتیبانی نمی‌شود: ",
"this operation is not allowed in demo mode": "این عملیات در حالت دمو مجاز نیست",
"this operation requires administrator to perform": "این عملیات نیاز به مدیر برای انجام دارد"
},
"ldap": {
"Ldap server exist": "Ldap server exist"
"Ldap server exist": "سرور LDAP وجود دارد"
},
"link": {
"Please link first": "Please link first",
"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 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 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"
"Please link first": "لطفاً ابتدا پیوند دهید",
"This application has no providers": "این برنامه ارائه‌دهنده‌ای ندارد",
"This application has no providers of type": "این برنامه ارائه‌دهنده‌ای از نوع ندارد",
"This provider can't be unlinked": "این ارائه‌دهنده نمی‌تواند لغو پیوند شود",
"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": "شما نمی‌توانید خودتان را لغو پیوند کنید، شما عضو هیچ برنامه‌ای نیستید"
},
"organization": {
"Only admin can modify the %s.": "Only admin can modify the %s.",
"The %s is immutable.": "The %s is immutable.",
"Unknown modify rule %s.": "Unknown modify rule %s."
"Only admin can modify the %s.": "فقط مدیر می‌تواند %s را تغییر دهد.",
"The %s is immutable.": "%s غیرقابل تغییر است.",
"Unknown modify rule %s.": "قانون تغییر ناشناخته %s."
},
"permission": {
"The permission: \\\"%s\\\" doesn't exist": "The permission: \\\"%s\\\" doesn't exist"
"The permission: \"%s\" doesn't exist": "مجوز: \"%s\" وجود ندارد"
},
"provider": {
"Invalid application id": "Invalid application id",
"the provider: %s does not exist": "the provider: %s does not exist"
"Invalid application id": "شناسه برنامه نامعتبر",
"the provider: %s does not exist": "ارائه‌دهنده: %s وجود ندارد"
},
"resource": {
"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"
"User is nil for tag: avatar": "کاربر برای برچسب: آواتار تهی است",
"Username or fullFilePath is empty: username = %s, fullFilePath = %s": "نام کاربری یا مسیر کامل فایل خالی است: نام کاربری = %s، مسیر کامل فایل = %s"
},
"saml": {
"Application %s not found": "Application %s not found"
"Application %s not found": "برنامه %s یافت نشد"
},
"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": {
"Empty parameters for emailForm: %v": "Empty parameters for emailForm: %v",
"Invalid Email receivers: %s": "Invalid Email receivers: %s",
"Invalid phone receivers: %s": "Invalid phone receivers: %s"
"Empty parameters for emailForm: %v": "پارامترهای خالی برای emailForm: %v",
"Invalid Email receivers: %s": "گیرندگان ایمیل نامعتبر: %s",
"Invalid phone receivers: %s": "گیرندگان تلفن نامعتبر: %s"
},
"storage": {
"The objectKey: %s is not allowed": "The objectKey: %s is not allowed",
"The provider type: %s is not supported": "The provider type: %s is not supported"
"The objectKey: %s is not allowed": "objectKey: %s مجاز نیست",
"The provider type: %s is not supported": "نوع ارائه‌دهنده: %s پشتیبانی نمی‌شود"
},
"token": {
"Grant_type: %s is not supported in this application": "Grant_type: %s is not supported in this application",
"Invalid application or wrong clientSecret": "Invalid application or wrong clientSecret",
"Invalid client_id": "Invalid 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",
"Token not found, invalid accessToken": "Token not found, invalid accessToken"
"Grant_type: %s is not supported in this application": "grant_type: %s در این برنامه پشتیبانی نمی‌شود",
"Invalid application or wrong clientSecret": "برنامه نامعتبر یا clientSecret نادرست",
"Invalid client_id": "client_id نامعتبر",
"Redirect URI: %s doesn't exist in the allowed Redirect URI list": "آدرس بازگشت: %s در لیست آدرس‌های بازگشت مجاز وجود ندارد",
"Token not found, invalid accessToken": "توکن یافت نشد، accessToken نامعتبر"
},
"user": {
"Display name cannot be empty": "Display name cannot be empty",
"New password cannot contain blank space.": "New password cannot contain blank space."
"Display name cannot be empty": "نام نمایشی نمی‌تواند خالی باشد",
"New password cannot contain blank space.": "رمز عبور جدید نمی‌تواند حاوی فاصله خالی باشد."
},
"user_upload": {
"Failed to import users": "Failed to import users"
"Failed to import users": "عدم موفقیت در وارد کردن کاربران"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",
"The provider: %s is not found": "The provider: %s is not found"
"No application is found for userId: %s": "هیچ برنامه‌ای برای userId: %s یافت نشد",
"No provider for category: %s is found for application: %s": "هیچ ارائه‌دهنده‌ای برای دسته‌بندی: %s برای برنامه: %s یافت نشد",
"The provider: %s is not found": "ارائه‌دهنده: %s یافت نشد"
},
"verification": {
"Invalid captcha provider.": "Invalid captcha provider.",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %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, or has already been used!": "The verification code has not been sent yet, or has already been used!",
"Turing test failed.": "Turing test failed.",
"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.",
"Unknown type": "Unknown type",
"Wrong verification code!": "Wrong verification code!",
"You should verify your code in %d min!": "You should verify your code in %d min!",
"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 an Email provider to the \\\"Providers\\\" list for the application: %s": "please add an Email provider to the \\\"Providers\\\" list for the application: %s",
"the user does not exist, please sign up first": "the user does not exist, please sign up first"
"Invalid captcha provider.": "ارائه‌دهنده کپچا نامعتبر.",
"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, or has already been used!": "کد تأیید هنوز ارسال نشده است، یا قبلاً استفاده شده است!",
"Turing test failed.": "تست تورینگ ناموفق بود.",
"Unable to get the email modify rule.": "عدم توانایی در دریافت قانون تغییر ایمیل.",
"Unable to get the phone modify rule.": "عدم توانایی در دریافت قانون تغییر تلفن.",
"Unknown type": "نوع ناشناخته",
"Wrong verification code!": "کد تأیید اشتباه!",
"You should verify your code in %d min!": "شما باید کد خود را در %d دقیقه تأیید کنید!",
"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": "لطفاً یک ارائه‌دهنده ایمیل به لیست \"ارائه‌دهندگان\" برای برنامه: %s اضافه کنید",
"the user does not exist, please sign up first": "کاربر وجود ندارد، لطفاً ابتدا ثبت‌نام کنید"
},
"webauthn": {
"Found no credentials for this user": "Found no credentials for this user",
"Please call WebAuthnSigninBegin first": "Please call WebAuthnSigninBegin first"
"Found no credentials for this user": "هیچ اعتباری برای این کاربر یافت نشد",
"Please call WebAuthnSigninBegin first": "لطفاً ابتدا WebAuthnSigninBegin را فراخوانی کنید"
}
}

View File

@ -188,10 +188,23 @@ type GitHubUserInfo struct {
} `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) {
req, err := http.NewRequest("GET", "https://api.github.com/user", nil)
if err != nil {
panic(err)
return nil, err
}
req.Header.Add("Authorization", "token "+token.AccessToken)
resp, err := idp.Client.Do(req)
@ -212,6 +225,42 @@ func (idp *GithubIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error)
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{
Id: strconv.Itoa(githubUserInfo.Id),
Username: githubUserInfo.Login,
@ -248,3 +297,27 @@ func (idp *GithubIdProvider) postWithBody(body interface{}, url string) ([]byte,
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
case "Douyin":
return NewDouyinIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "Kwai":
return NewKwaiIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "Bilibili":
return NewBilibiliIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "MetaMask":

View File

@ -15,6 +15,7 @@
package ldap
import (
"crypto/tls"
"fmt"
"hash/fnv"
"log"
@ -27,21 +28,68 @@ import (
func StartLdapServer() {
ldapServerPort := conf.GetConfigString("ldapServerPort")
if ldapServerPort == "" || ldapServerPort == "0" {
return
}
ldapsServerPort := conf.GetConfigString("ldapsServerPort")
server := ldap.NewServer()
serverSsl := ldap.NewServer()
routes := ldap.NewRouteMux()
routes.Bind(handleBind)
routes.Search(handleSearch).Label(" SEARCH****")
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 {
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) {
@ -142,7 +190,7 @@ func handleSearch(w ldap.ResponseWriter, m *ldap.Message) {
}
for _, attr := range attrs {
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))
}
}

View File

@ -22,6 +22,7 @@ import (
_ "github.com/beego/beego/session/redis"
"github.com/casdoor/casdoor/authz"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/controllers"
"github.com/casdoor/casdoor/ldap"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/proxy"
@ -45,6 +46,7 @@ func main() {
object.InitCasvisorConfig()
util.SafeGoroutine(func() { object.RunSyncUsersJob() })
util.SafeGoroutine(func() { controllers.InitCLIDownloader() })
// beego.DelStaticPath("/static")
// beego.SetStaticPath("/static", "web/build/static")
@ -83,6 +85,11 @@ func main() {
// logs.SetLevel(logs.LevelInformational)
logs.SetLogFuncCall(false)
err = util.StopOldInstance(port)
if err != nil {
panic(err)
}
go ldap.StartLdapServer()
go radius.StartRadiusServer()
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"
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" {
return NewTelegramProvider(clientSecret, receiver)
} else if typ == "Custom HTTP" {
@ -53,6 +53,8 @@ func GetNotificationProvider(typ string, clientId string, clientSecret string, c
return NewRocketChatProvider(clientId, clientSecret, appId, receiver)
} else if typ == "Viber" {
return NewViberProvider(clientId, clientSecret, appId, receiver)
} else if typ == "CUCloud" {
return NewCucloudProvider(clientId, clientSecret, appId, title, regionId, clientId2, metaData)
}
return nil, nil

View File

@ -481,7 +481,10 @@ func GetApplicationByClientId(clientId 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)
}

View File

@ -241,6 +241,10 @@ func CheckPassword(user *User, password string, lang string, options ...bool) er
return fmt.Errorf(i18n.Translate(lang, "check:Organization does not exist"))
}
if password == "" {
return fmt.Errorf(i18n.Translate(lang, "check:Password cannot be empty"))
}
passwordType := user.PasswordType
if passwordType == "" {
passwordType = organization.PasswordType
@ -273,7 +277,7 @@ func CheckPasswordComplexity(user *User, password string) string {
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)
if err != nil {
return err
@ -368,7 +372,7 @@ func CheckUserPassword(organization string, username string, password string, la
}
// only for LDAP users
err = checkLdapUserPassword(user, password, lang)
err = CheckLdapUserPassword(user, password, lang)
if err != nil {
if err.Error() == "user not exist" {
return nil, fmt.Errorf(i18n.Translate(lang, "check:The user: %s doesn't exist in LDAP server"), username)

View File

@ -16,23 +16,18 @@
package object
import (
"crypto/tls"
import "github.com/casdoor/casdoor/email"
"github.com/casdoor/casdoor/email"
"github.com/casdoor/gomail/v2"
)
func getDialer(provider *Provider) *gomail.Dialer {
dialer := &gomail.Dialer{}
dialer = gomail.NewDialer(provider.Host, provider.Port, provider.ClientId, provider.ClientSecret)
if provider.Type == "SUBMAIL" {
dialer.TLSConfig = &tls.Config{InsecureSkipVerify: true}
// TestSmtpServer Test the SMTP server
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()
if err != nil {
return err
}
defer sender.Close()
dialer.SSL = !provider.DisableSsl
return dialer
return nil
}
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)
}
// 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"
)
type Dashboard struct {
OrganizationCounts []int `json:"organizationCounts"`
UserCounts []int `json:"userCounts"`
ProviderCounts []int `json:"providerCounts"`
ApplicationCounts []int `json:"applicationCounts"`
SubscriptionCounts []int `json:"subscriptionCounts"`
type DashboardDateItem struct {
CreatedTime string `json:"createTime"`
}
func GetDashboard(owner string) (*Dashboard, error) {
type DashboardMapItem struct {
dashboardDateItems []DashboardDateItem
itemCount int64
}
func GetDashboard(owner string) (*map[string][]int64, error) {
if owner == "All" {
owner = ""
}
dashboard := &Dashboard{
OrganizationCounts: make([]int, 31),
UserCounts: make([]int, 31),
ProviderCounts: make([]int, 31),
ApplicationCounts: make([]int, 31),
SubscriptionCounts: make([]int, 31),
dashboard := make(map[string][]int64)
dashboardMap := sync.Map{}
tableNames := []string{"organization", "user", "provider", "application", "subscription", "role", "group", "resource", "cert", "permission", "transaction", "model", "adapter", "enforcer"}
time30day := time.Now().AddDate(0, 0, -30)
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()
close(ch)
for err = range ch {
if err != nil {
return nil, err
}
}
nowTime := time.Now()
for i := 30; i >= 0; i-- {
cutTime := nowTime.AddDate(0, 0, -i)
dashboard.OrganizationCounts[30-i] = countCreatedBefore(organizations, cutTime)
dashboard.UserCounts[30-i] = countCreatedBefore(users, cutTime)
dashboard.ProviderCounts[30-i] = countCreatedBefore(providers, cutTime)
dashboard.ApplicationCounts[30-i] = countCreatedBefore(applications, cutTime)
dashboard.SubscriptionCounts[30-i] = countCreatedBefore(subscriptions, cutTime)
for _, tableName := range tableNames {
item, exist := dashboardMap.Load(tableName)
if !exist {
continue
}
dashboard[tableName+"Counts"][30-i] = countCreatedBefore(item.(DashboardMapItem), cutTime)
}
}
return dashboard, nil
return &dashboard, nil
}
func countCreatedBefore(objects interface{}, before time.Time) int {
count := 0
switch obj := objects.(type) {
case []Organization:
for _, o := range obj {
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", o.CreatedTime)
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++
}
func countCreatedBefore(dashboardMapItem DashboardMapItem, before time.Time) int64 {
count := dashboardMapItem.itemCount
for _, e := range dashboardMapItem.dashboardDateItems {
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", e.CreatedTime)
if createdTime.Before(before) {
count++
}
}
return count

View File

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

View File

@ -103,6 +103,7 @@ func initBuiltInOrganization() bool {
PasswordOptions: []string{"AtLeast6"},
CountryCodes: []string{"US", "ES", "FR", "DE", "GB", "CN", "JP", "KR", "VN", "ID", "SG", "IN"},
DefaultAvatar: fmt.Sprintf("%s/img/casbin.svg", conf.GetConfigString("staticBaseUrl")),
UserTypes: []string{},
Tags: []string{},
Languages: []string{"en", "zh", "es", "fr", "de", "id", "ja", "ko", "ru", "vi", "pt"},
InitScore: 2000,

View File

@ -33,6 +33,7 @@ type Ldap struct {
Filter string `xorm:"varchar(200)" json:"filter"`
FilterFields []string `xorm:"varchar(100)" json:"filterFields"`
DefaultGroup string `xorm:"varchar(100)" json:"defaultGroup"`
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
AutoSync int `json:"autoSync"`
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",
"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 {
return false, nil
}

View File

@ -15,14 +15,18 @@
package object
import (
"crypto/md5"
"encoding/base64"
"errors"
"fmt"
"strings"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/i18n"
"github.com/casdoor/casdoor/util"
goldap "github.com/go-ldap/ldap/v3"
"github.com/thanhpk/randstr"
"golang.org/x/text/encoding/unicode"
)
type LdapConn struct {
@ -371,6 +375,88 @@ func GetExistUuids(owner string, uuids []string) ([]string, error) {
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) {
user := User{}
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) {
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 {
return nil, err
}

View File

@ -66,11 +66,12 @@ type Organization struct {
CountryCodes []string `xorm:"varchar(200)" json:"countryCodes"`
DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"`
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
UserTypes []string `xorm:"mediumtext" json:"userTypes"`
Tags []string `xorm:"mediumtext" json:"tags"`
Languages []string `xorm:"varchar(255)" json:"languages"`
ThemeData *ThemeData `xorm:"json" json:"themeData"`
MasterPassword string `xorm:"varchar(100)" json:"masterPassword"`
DefaultPassword string `xorm:"varchar(100)" json:"defaultPassword"`
MasterPassword string `xorm:"varchar(200)" json:"masterPassword"`
DefaultPassword string `xorm:"varchar(200)" json:"defaultPassword"`
MasterVerificationCode string `xorm:"varchar(100)" json:"masterVerificationCode"`
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
InitScore int `json:"initScore"`
@ -79,6 +80,7 @@ type Organization struct {
UseEmailAsUsername bool `json:"useEmailAsUsername"`
EnableTour bool `json:"enableTour"`
IpRestriction string `json:"ipRestriction"`
NavItems []string `xorm:"varchar(500)" json:"navItems"`
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"`
@ -151,7 +153,10 @@ func getOrganization(owner string, name 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)
}
@ -192,9 +197,10 @@ func GetMaskedOrganizations(organizations []*Organization, errs ...error) ([]*Or
return organizations, nil
}
func UpdateOrganization(id string, organization *Organization) (bool, error) {
func UpdateOrganization(id string, organization *Organization, isGlobalAdmin bool) (bool, error) {
owner, name := util.GetOwnerAndNameFromId(id)
if org, err := getOrganization(owner, name); err != nil {
org, err := getOrganization(owner, name)
if err != nil {
return false, err
} else if org == nil {
return false, nil
@ -219,6 +225,10 @@ func UpdateOrganization(id string, organization *Organization) (bool, error) {
}
}
if !isGlobalAdmin {
organization.NavItems = org.NavItems
}
session := ormer.Engine.ID(core.PK{owner, name}).AllCols()
if organization.MasterPassword == "***" {

View File

@ -219,8 +219,11 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
ProductName: product.Name,
PayerName: payerName,
PayerId: user.Id,
PayerEmail: user.Email,
PaymentName: paymentName,
ProductDisplayName: product.DisplayName,
ProductDescription: product.Description,
ProductImage: product.Image,
Price: product.Price,
Currency: product.Currency,
ReturnUrl: returnUrl,

View File

@ -16,6 +16,7 @@ package object
import (
"fmt"
"regexp"
"strings"
"github.com/beego/beego/context"
@ -70,6 +71,7 @@ type Provider struct {
IdP string `xorm:"mediumtext" json:"idP"`
IssuerUrl string `xorm:"varchar(100)" json:"issuerUrl"`
EnableSignAuthnRequest bool `json:"enableSignAuthnRequest"`
EmailRegex string `xorm:"varchar(200)" json:"emailRegex"`
ProviderUrl string `xorm:"varchar(200)" json:"providerUrl"`
}
@ -200,6 +202,13 @@ func UpdateProvider(id string, provider *Provider) (bool, error) {
return false, nil
}
if provider.EmailRegex != "" {
_, err := regexp.Compile(provider.EmailRegex)
if err != nil {
return false, err
}
}
if name != provider.Name {
err := providerChangeTrigger(name, provider.Name)
if err != nil {
@ -234,6 +243,13 @@ func AddProvider(provider *Provider) (bool, error) {
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)
if err != nil {
return false, err
@ -309,6 +325,12 @@ func GetPaymentProvider(p *Provider) (pp.PaymentProvider, error) {
return nil, err
}
return pp, nil
} else if typ == "AirWallex" {
pp, err := pp.NewAirwallexPaymentProvider(p.ClientId, p.ClientSecret)
if err != nil {
return nil, err
}
return pp, nil
} else if typ == "Balance" {
pp, err := pp.NewBalancePaymentProvider()
if err != nil {
@ -421,7 +443,7 @@ func FromProviderToIdpInfo(ctx *context.Context, provider *Provider) *idp.Provid
providerInfo.ClientId = provider.ClientId2
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
}

View File

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

View File

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

View File

@ -338,6 +338,9 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
} else if authnRequest.AssertionConsumerServiceURL == "" {
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)

View File

@ -123,7 +123,7 @@ func GetTokenByRefreshToken(refreshToken string) (*Token, error) {
func GetTokenByTokenValue(tokenValue, tokenTypeHint string) (*Token, error) {
switch tokenTypeHint {
case "access_token":
case "access_token", "access-token":
token, err := GetTokenByAccessToken(tokenValue)
if err != nil {
return nil, err
@ -131,7 +131,7 @@ func GetTokenByTokenValue(tokenValue, tokenTypeHint string) (*Token, error) {
if token != nil {
return token, nil
}
case "refresh_token":
case "refresh_token", "refresh-token":
token, err := GetTokenByRefreshToken(tokenValue)
if err != nil {
return nil, err
@ -144,13 +144,13 @@ func GetTokenByTokenValue(tokenValue, tokenTypeHint string) (*Token, error) {
return nil, nil
}
func updateUsedByCode(token *Token) bool {
func updateUsedByCode(token *Token) (bool, error) {
affected, err := ormer.Engine.Where("code=?", token.Code).Cols("code_is_used").Update(token)
if err != nil {
panic(err)
return false, err
}
return affected != 0
return affected != 0, nil
}
func GetToken(id string) (*Token, error) {

View File

@ -22,6 +22,7 @@ import (
"encoding/xml"
"fmt"
"math/rand"
"strings"
"sync"
"time"
@ -184,6 +185,15 @@ func StoreCasTokenForProxyTicket(token *CasAuthenticationSuccess, targetService,
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) {
user, err := GetUser(userId)
if err != nil {
@ -225,6 +235,11 @@ func GenerateCasToken(userId string, service string) (string, error) {
}
if value != "" {
if escapedValue, err := escapeXMLText(value); err != nil {
return "", err
} else {
value = escapedValue
}
authenticationSuccess.Attributes.UserAttributes.Attributes = append(authenticationSuccess.Attributes.UserAttributes.Attributes, &CasNamedAttribute{
Name: k,
Value: value,

View File

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

View File

@ -129,6 +129,7 @@ type User struct {
Bilibili string `xorm:"bilibili varchar(100)" json:"bilibili"`
Okta string `xorm:"okta varchar(100)" json:"okta"`
Douyin string `xorm:"douyin varchar(100)" json:"douyin"`
Kwai string `xorm:"kwai varchar(100)" json:"kwai"`
Line string `xorm:"line varchar(100)" json:"line"`
Amazon string `xorm:"amazon varchar(100)" json:"amazon"`
Auth0 string `xorm:"auth0 varchar(100)" json:"auth0"`
@ -237,6 +238,7 @@ type MfaAccount struct {
AccountName string `xorm:"varchar(100)" json:"accountName"`
Issuer string `xorm:"varchar(100)" json:"issuer"`
SecretKey string `xorm:"varchar(100)" json:"secretKey"`
Origin string `xorm:"varchar(100)" json:"origin"`
}
type FaceId struct {
@ -679,6 +681,10 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
user.Password = oldUser.Password
}
if user.Id != oldUser.Id && user.Id == "" {
user.Id = oldUser.Id
}
if user.Avatar != oldUser.Avatar && user.Avatar != "" && user.PermanentAvatar != "*" {
user.PermanentAvatar, err = getPermanentAvatarUrl(user.Owner, user.Name, user.Avatar, false)
if err != nil {
@ -693,7 +699,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts", "face_ids", "mfaAccounts",
"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",
"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",
"eveonline", "fitbit", "gitea", "heroku", "influxcloud", "instagram", "intercom", "kakao", "lastfm", "mailru", "meetup",
"microsoftonline", "naver", "nextcloud", "onedrive", "oura", "patreon", "paypal", "salesforce", "shopify", "soundcloud",
@ -840,11 +846,14 @@ func AddUser(user *User) (bool, error) {
}
}
count, err := GetUserCount(user.Owner, "", "", "")
if err != nil {
return false, err
rankingItem := GetAccountItemByName("Ranking", organization)
if rankingItem != nil {
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 {
_, err = userEnforcer.UpdateGroupsForUser(user.GetId(), user.Groups)
@ -956,6 +965,11 @@ func DeleteUser(user *User) (bool, error) {
return false, err
}
_, err = userEnforcer.DeleteGroupsForUser(user.GetId())
if err != nil {
return false, err
}
organization, err := GetOrganizationByUser(user)
if err != nil {
return false, err

View File

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

289
pp/airwallex.go Normal file
View File

@ -0,0 +1,289 @@
package pp
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/casdoor/casdoor/conf"
)
type AirwallexPaymentProvider struct {
Client *AirwallexClient
}
func NewAirwallexPaymentProvider(clientId string, apiKey string) (*AirwallexPaymentProvider, error) {
isProd := conf.GetConfigString("runmode") == "prod"
apiEndpoint := "https://api-demo.airwallex.com/api/v1"
apiCheckout := "https://checkout-demo.airwallex.com/#/standalone/checkout?"
if isProd {
apiEndpoint = "https://api.airwallex.com/api/v1"
apiCheckout = "https://checkout.airwallex.com/#/standalone/checkout?"
}
client := &AirwallexClient{
ClientId: clientId,
APIKey: apiKey,
APIEndpoint: apiEndpoint,
APICheckout: apiCheckout,
client: &http.Client{Timeout: 15 * time.Second},
}
pp := &AirwallexPaymentProvider{
Client: client,
}
return pp, nil
}
func (pp *AirwallexPaymentProvider) Pay(r *PayReq) (*PayResp, error) {
// Create a payment intent
intent, err := pp.Client.CreateIntent(r)
if err != nil {
return nil, err
}
payUrl, err := pp.Client.GetCheckoutUrl(intent, r)
if err != nil {
return nil, err
}
return &PayResp{
PayUrl: payUrl,
OrderId: intent.MerchantOrderId,
}, nil
}
func (pp *AirwallexPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {
notifyResult := &NotifyResult{}
intent, err := pp.Client.GetIntentByOrderId(orderId)
if err != nil {
return nil, err
}
// Check intent status
switch intent.Status {
case "PENDING", "REQUIRES_PAYMENT_METHOD", "REQUIRES_CUSTOMER_ACTION", "REQUIRES_CAPTURE":
notifyResult.PaymentStatus = PaymentStateCreated
return notifyResult, nil
case "CANCELLED":
notifyResult.PaymentStatus = PaymentStateCanceled
return notifyResult, nil
case "EXPIRED":
notifyResult.PaymentStatus = PaymentStateTimeout
return notifyResult, nil
case "SUCCEEDED":
// Skip
default:
notifyResult.PaymentStatus = PaymentStateError
notifyResult.NotifyMessage = fmt.Sprintf("unexpected airwallex checkout status: %v", intent.Status)
return notifyResult, nil
}
// Check attempt status
if intent.PaymentStatus != "" {
switch intent.PaymentStatus {
case "CANCELLED", "EXPIRED", "RECEIVED", "AUTHENTICATION_REDIRECTED", "AUTHORIZED", "CAPTURE_REQUESTED":
notifyResult.PaymentStatus = PaymentStateCreated
return notifyResult, nil
case "PAID", "SETTLED":
// Skip
default:
notifyResult.PaymentStatus = PaymentStateError
notifyResult.NotifyMessage = fmt.Sprintf("unexpected airwallex checkout payment status: %v", intent.PaymentStatus)
return notifyResult, nil
}
}
// The Payment has succeeded.
var productDisplayName, productName, providerName string
if description, ok := intent.Metadata["description"]; ok {
productName, productDisplayName, providerName, _ = parseAttachString(description.(string))
}
orderId = intent.MerchantOrderId
return &NotifyResult{
PaymentName: orderId,
PaymentStatus: PaymentStatePaid,
ProductName: productName,
ProductDisplayName: productDisplayName,
ProviderName: providerName,
Price: priceStringToFloat64(intent.Amount.String()),
Currency: intent.Currency,
OrderId: orderId,
}, nil
}
func (pp *AirwallexPaymentProvider) GetInvoice(paymentName, personName, personIdCard, personEmail, personPhone, invoiceType, invoiceTitle, invoiceTaxId string) (string, error) {
return "", nil
}
func (pp *AirwallexPaymentProvider) GetResponseError(err error) string {
if err == nil {
return "success"
}
return "fail"
}
/*
* Airwallex Client implementation (to be removed upon official SDK release)
*/
type AirwallexClient struct {
ClientId string
APIKey string
APIEndpoint string
APICheckout string
client *http.Client
tokenCache *AirWallexTokenInfo
tokenMutex sync.RWMutex
}
type AirWallexTokenInfo struct {
Token string `json:"token"`
ExpiresAt string `json:"expires_at"`
parsedExpiresAt time.Time
}
type AirWallexIntentResp struct {
Id string `json:"id"`
ClientSecret string `json:"client_secret"`
MerchantOrderId string `json:"merchant_order_id"`
}
func (c *AirwallexClient) GetToken() (string, error) {
c.tokenMutex.Lock()
defer c.tokenMutex.Unlock()
if c.tokenCache != nil && time.Now().Before(c.tokenCache.parsedExpiresAt) {
return c.tokenCache.Token, nil
}
req, _ := http.NewRequest("POST", c.APIEndpoint+"/authentication/login", bytes.NewBuffer([]byte("{}")))
req.Header.Set("x-client-id", c.ClientId)
req.Header.Set("x-api-key", c.APIKey)
resp, err := c.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
var result AirWallexTokenInfo
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", err
}
if result.Token == "" {
return "", fmt.Errorf("invalid token response")
}
expiresAt := strings.Replace(result.ExpiresAt, "+0000", "+00:00", 1)
result.parsedExpiresAt, _ = time.Parse(time.RFC3339, expiresAt)
c.tokenCache = &result
return result.Token, nil
}
func (c *AirwallexClient) authRequest(method, url string, body interface{}) (map[string]interface{}, error) {
token, err := c.GetToken()
if err != nil {
return nil, err
}
b, _ := json.Marshal(body)
req, _ := http.NewRequest(method, url, bytes.NewBuffer(b))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return result, nil
}
func (c *AirwallexClient) CreateIntent(r *PayReq) (*AirWallexIntentResp, error) {
description := joinAttachString([]string{r.ProductName, r.ProductDisplayName, r.ProviderName})
orderId := r.PaymentName
intentReq := map[string]interface{}{
"currency": r.Currency,
"amount": r.Price,
"merchant_order_id": orderId,
"request_id": orderId,
"descriptor": strings.ReplaceAll(string([]rune(description)[:32]), "\x00", ""),
"metadata": map[string]interface{}{"description": description},
"order": map[string]interface{}{"products": []map[string]interface{}{{"name": r.ProductDisplayName, "quantity": 1, "desc": r.ProductDescription, "image_url": r.ProductImage}}},
"customer": map[string]interface{}{"merchant_customer_id": r.PayerId, "email": r.PayerEmail, "first_name": r.PayerName, "last_name": r.PayerName},
}
intentUrl := fmt.Sprintf("%s/pa/payment_intents/create", c.APIEndpoint)
intentRes, err := c.authRequest("POST", intentUrl, intentReq)
if err != nil {
return nil, fmt.Errorf("failed to create payment intent: %v", err)
}
return &AirWallexIntentResp{
Id: intentRes["id"].(string),
ClientSecret: intentRes["client_secret"].(string),
MerchantOrderId: intentRes["merchant_order_id"].(string),
}, nil
}
type AirwallexIntent struct {
Amount json.Number `json:"amount"`
Currency string `json:"currency"`
Id string `json:"id"`
Status string `json:"status"`
Descriptor string `json:"descriptor"`
MerchantOrderId string `json:"merchant_order_id"`
LatestPaymentAttempt struct {
Status string `json:"status"`
} `json:"latest_payment_attempt"`
Metadata map[string]interface{} `json:"metadata"`
}
type AirwallexIntents struct {
Items []AirwallexIntent `json:"items"`
}
type AirWallexIntentInfo struct {
Amount json.Number
Currency string
Id string
Status string
Descriptor string
MerchantOrderId string
PaymentStatus string
Metadata map[string]interface{}
}
func (c *AirwallexClient) GetIntentByOrderId(orderId string) (*AirWallexIntentInfo, error) {
intentUrl := fmt.Sprintf("%s/pa/payment_intents/?merchant_order_id=%s", c.APIEndpoint, orderId)
intentRes, err := c.authRequest("GET", intentUrl, nil)
if err != nil {
return nil, fmt.Errorf("failed to get payment intent: %v", err)
}
items := intentRes["items"].([]interface{})
if len(items) == 0 {
return nil, fmt.Errorf("no payment intent found for order id: %s", orderId)
}
var intent AirwallexIntent
if b, err := json.Marshal(items[0]); err == nil {
json.Unmarshal(b, &intent)
}
return &AirWallexIntentInfo{
Id: intent.Id,
Amount: intent.Amount,
Currency: intent.Currency,
Status: intent.Status,
Descriptor: intent.Descriptor,
MerchantOrderId: intent.MerchantOrderId,
PaymentStatus: intent.LatestPaymentAttempt.Status,
Metadata: intent.Metadata,
}, nil
}
func (c *AirwallexClient) GetCheckoutUrl(intent *AirWallexIntentResp, r *PayReq) (string, error) {
return fmt.Sprintf("%sintent_id=%s&client_secret=%s&mode=payment&currency=%s&amount=%v&requiredBillingContactFields=%s&successUrl=%s&failUrl=%s&logoUrl=%s",
c.APICheckout,
intent.Id,
intent.ClientSecret,
r.Currency,
r.Price,
url.QueryEscape(`["address"]`),
r.ReturnUrl,
r.ReturnUrl,
"data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=", // replace default logo
), nil
}

View File

@ -33,8 +33,11 @@ type PayReq struct {
ProductName string
PayerName string
PayerId string
PayerEmail string
PaymentName string
ProductDisplayName string
ProductDescription string
ProductImage string
Price float64
Currency string

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)
if organization == "" {
w.Write(r.Response(radius.CodeAccessReject))
return
organization = conf.GetConfigString("radiusDefaultOrganization")
if organization == "" {
organization = "built-in"
}
}
var user *object.User

View File

@ -175,6 +175,7 @@ func initAPI() {
beego.Router("/api/get-all-roles", &controllers.ApiController{}, "GET:GetAllRoles")
beego.Router("/api/run-casbin-command", &controllers.ApiController{}, "GET:RunCasbinCommand")
beego.Router("/api/refresh-engines", &controllers.ApiController{}, "POST:RefreshEngines")
beego.Router("/api/get-sessions", &controllers.ApiController{}, "GET:GetSessions")
beego.Router("/api/get-session", &controllers.ApiController{}, "GET:GetSingleSession")

View File

@ -80,6 +80,15 @@ func fastAutoSignin(ctx *context.Context) (string, error) {
return "", nil
}
isAllowed, err := object.CheckLoginPermission(userId, application)
if err != nil {
return "", err
}
if !isAllowed {
return "", nil
}
code, err := object.GetOAuthCode(userId, clientId, responseType, redirectUri, scope, state, nonce, codeChallenge, ctx.Request.Host, getAcceptLanguage(ctx))
if err != nil {
return "", err
@ -133,6 +142,14 @@ func StaticFilter(ctx *context.Context) {
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) {
path = webBuildFolder + "/index.html"
}
@ -149,13 +166,13 @@ func StaticFilter(ctx *context.Context) {
}
if oldStaticBaseUrl == newStaticBaseUrl {
makeGzipResponse(ctx.ResponseWriter, ctx.Request, path)
makeGzipResponse(ctx.ResponseWriter, ctx.Request, path, organizationThemeCookie)
} 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))
if err != nil {
panic(err)
@ -168,7 +185,13 @@ func serveFileWithReplace(w http.ResponseWriter, r *http.Request, name string) {
}
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))
}
@ -182,14 +205,14 @@ func (w gzipResponseWriter) Write(b []byte) (int, error) {
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") {
serveFileWithReplace(w, r, path)
serveFileWithReplace(w, r, path, organizationThemeCookie)
return
}
w.Header().Set("Content-Encoding", "gzip")
gz := gzip.NewWriter(w)
defer gz.Close()
gzw := gzipResponseWriter{Writer: gz, ResponseWriter: w}
serveFileWithReplace(gzw, r, path)
serveFileWithReplace(gzw, r, path, organizationThemeCookie)
}

128
routers/theme_filter.go Normal file
View File

@ -0,0 +1,128 @@
// 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" || urlPath == "/signup" {
application, err = object.GetDefaultApplication(fmt.Sprintf("admin/built-in"))
if err != nil {
return nil, err
}
} else if strings.HasSuffix(urlPath, "/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, "/signup/") {
owner, _ := strings.CutPrefix(urlPath, "/signup/")
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":
return NewAwsS3StorageProvider(clientId, clientSecret, region, bucket, endpoint), nil
case "MinIO":
return NewMinIOS3StorageProvider(clientId, clientSecret, "_", bucket, endpoint), nil
if region == "" {
region = "_"
}
return NewMinIOS3StorageProvider(clientId, clientSecret, region, bucket, endpoint), nil
case "Aliyun OSS":
return NewAliyunOssStorageProvider(clientId, clientSecret, region, bucket, endpoint), nil
case "Tencent Cloud COS":
@ -38,6 +41,8 @@ func GetStorageProvider(providerType string, clientId string, clientSecret strin
return NewSynologyNasStorageProvider(clientId, clientSecret, endpoint), nil
case "Casdoor":
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

View File

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

View File

@ -4981,6 +4981,8 @@ definitions:
karma:
type: integer
format: int64
kwai:
type: string
language:
type: string
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

@ -118,6 +118,6 @@ func IsValidOrigin(origin string) (bool, error) {
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")
res := originHostOnly == "http://localhost" || originHostOnly == "https://localhost" || originHostOnly == "http://127.0.0.1" || originHostOnly == "http://casdoor-authenticator" || strings.HasSuffix(originHostOnly, ".chromiumapp.org")
return res, nil
}

View File

@ -1,4 +1,5 @@
const CracoLessPlugin = require("craco-less");
const path = require("path");
module.exports = {
devServer: {
@ -55,47 +56,42 @@ module.exports = {
},
],
webpack: {
configure: {
// ignore webpack warnings by source-map-loader
configure: (webpackConfig, { env, paths }) => {
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
ignoreWarnings: [
webpackConfig.ignoreWarnings = [
function ignoreSourcemapsloaderWarnings(warning) {
return (
warning.module &&
warning.module.resource.includes('node_modules') &&
warning.module.resource.includes("node_modules") &&
warning.details &&
warning.details.includes('source-map-loader')
)
warning.details.includes("source-map-loader")
);
},
],
];
// use polyfill Buffer with Webpack 5
// https://viglucci.io/articles/how-to-polyfill-buffer-with-webpack-5
// https://craco.js.org/docs/configuration/webpack/
resolve: {
fallback: {
// "process": require.resolve('process/browser'),
// "util": require.resolve("util/"),
// "url": require.resolve("url/"),
// "zlib": require.resolve("browserify-zlib"),
// "stream": require.resolve("stream-browserify"),
// "http": require.resolve("stream-http"),
// "https": require.resolve("https-browserify"),
// "assert": require.resolve("assert/"),
"buffer": require.resolve('buffer/'),
"process": false,
"util": false,
"url": false,
"zlib": false,
"stream": false,
"http": false,
"https": false,
"assert": false,
"buffer": false,
"crypto": false,
"os": false,
"fs": false,
},
}
webpackConfig.resolve.fallback = {
buffer: require.resolve("buffer/"),
process: false,
util: false,
url: false,
zlib: false,
stream: false,
http: false,
https: false,
assert: false,
crypto: false,
os: false,
fs: false,
};
return webpackConfig;
},
}
},
};

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": {
"start": "cross-env PORT=7001 craco start",
"build": "craco build",
"postbuild": "node mv.js",
"test": "craco test",
"eject": "craco eject",
"crowdin:sync": "crowdin upload && crowdin download",

View File

@ -36,6 +36,7 @@ const {Footer, Content} = Layout;
import {setTwoToneColor} from "@ant-design/icons";
import * as ApplicationBackend from "./backend/ApplicationBackend";
import * as Cookie from "cookie";
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 (
<React.Fragment>
{!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>
<div dangerouslySetInnerHTML={{__html: this.state.application.footerHtml}} />
<div dangerouslySetInnerHTML={{__html: footerHtml}} />
</React.Fragment>
: (
Conf.CustomFooter !== null ? Conf.CustomFooter : (
<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>
)
)
@ -308,7 +311,7 @@ class App extends Component {
AI Assistant
</a>
</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)"}} />
</a>
<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}
>
<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>
);
}
@ -358,13 +361,37 @@ class App extends Component {
}
};
onLoginSuccess(redirectUrl) {
window.google?.accounts?.id?.cancel();
if (redirectUrl) {
localStorage.setItem("mfaRedirectUrl", redirectUrl);
}
this.getAccount();
}
renderPage() {
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 (
<ConfigProvider theme={{
token: {
colorPrimary: this.state.themeData.colorPrimary,
borderRadius: this.state.themeData.borderRadius,
colorPrimary: themeData.colorPrimary,
borderRadius: themeData.borderRadius,
},
algorithm: Setting.getAlgorithm(this.state.themeAlgorithm),
}}>
@ -382,26 +409,20 @@ class App extends Component {
application: application,
});
}}
onLoginSuccess={(redirectUrl) => {
window.google?.accounts?.id?.cancel();
if (redirectUrl) {
localStorage.setItem("mfaRedirectUrl", redirectUrl);
}
this.getAccount();
}}
onLoginSuccess={(redirectUrl) => {this.onLoginSuccess(redirectUrl);}}
onUpdateAccount={(account) => this.onUpdateAccount(account)}
updataThemeData={this.setTheme}
/> :
<Switch>
<Route exact path="/callback" component={AuthCallback} />
<Route exact path="/callback/saml" component={SamlCallback} />
<Route exact path="/callback" render={(props) => <AuthCallback {...props} {...this.props} application={this.state.application} onLoginSuccess={(redirectUrl) => {this.onLoginSuccess(redirectUrl);}} />} />
<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.")}
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>} />} />
</Switch>
}
</Content>
{
this.renderFooter()
this.renderFooter(logo, footerHtml)
}
{
this.renderAiAssistant()

View File

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

View File

@ -31,3 +31,6 @@ export const ThemeDefault = {
};
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"}`}
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")}
style={{margin: "0 auto"}} />
style={{width: "100%", margin: "0 auto", position: "absolute"}} />
<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/: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() {
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() {
@ -188,12 +176,8 @@ class GroupListPage extends BaseListPage {
{record.parentId}
</Link>;
}
const parentGroup = this.state.groups.find((group) => group.name === text);
if (parentGroup === undefined) {
return "";
}
return <Link to={`/groups/${parentGroup.owner}/${parentGroup.name}`}>
{parentGroup?.displayName}
return <Link to={`/groups/${record.owner}/${record.parentId}`}>
{record?.parentName}
</Link>;
},
},
@ -215,12 +199,11 @@ class GroupListPage extends BaseListPage {
width: "180px",
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
const haveChildren = this.state.groups.find((group) => group.parentId === record.id) !== undefined;
return (
<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>
<PopconfirmModal
disabled={haveChildren}
disabled={record.haveChildren}
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
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 iframeRef = useRef(null);
const [iframeReady, setIframeReady] = useState(false);
const currentLang = localStorage.getItem("language") || "en";
useEffect(() => {
const handleMessage = (event) => {
@ -26,24 +27,31 @@ const IframeEditor = forwardRef(({initialModelText, onModelTextChange}, ref) =>
onModelTextChange(event.data.modelText);
} else if (event.data.type === "iframeReady") {
setIframeReady(true);
iframeRef.current?.contentWindow.postMessage({
type: "initializeModel",
modelText: initialModelText,
}, "*");
if (initialModelText && iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.postMessage({
type: "initializeModel",
modelText: initialModelText,
lang: currentLang,
}, "*");
}
}
};
window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage);
}, [onModelTextChange, initialModelText]);
}, [onModelTextChange, initialModelText, currentLang]);
useImperativeHandle(ref, () => ({
getModelText: () => {
iframeRef.current?.contentWindow.postMessage({type: "getModelText"}, "*");
if (iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.postMessage({
type: "getModelText",
}, "*");
}
},
updateModelText: (newModelText) => {
if (iframeReady) {
iframeRef.current?.contentWindow.postMessage({
if (iframeReady && iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.postMessage({
type: "updateModelText",
modelText: newModelText,
}, "*");
@ -54,7 +62,7 @@ const IframeEditor = forwardRef(({initialModelText, onModelTextChange}, ref) =>
return (
<iframe
ref={iframeRef}
src="https://editor.casbin.org/model-editor"
src={`https://editor.casbin.org/model-editor?lang=${currentLang}`}
frameBorder="0"
width="100%"
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() {
const isCreatedByPlan = this.state.invitation.tag === "auto_created_invitation_for_plan";
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;
<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"}} onClick={() => {
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"));
}}>
<Button style={{marginLeft: "20px"}} onClick={_ => this.copySignupLink()}>
{i18next.t("application:Copy signup page URL")}
</Button>
{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"}}>
<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"}} size="large" onClick={() => {
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"));
}}>
<Button style={{marginLeft: "20px"}} size="large" onClick={_ => this.copySignupLink()}>
{i18next.t("application:Copy signup page URL")}
</Button>
{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>
</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"}} >
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("ldap:Default group"), i18next.t("ldap:Default group - Tooltip"))} :

View File

@ -192,11 +192,15 @@ function ManagementPage(props) {
themeAlgorithm={props.themeAlgorithm}
onChange={props.setLogoAndThemeAlgorithm} />
<LanguageSelect languages={props.account.organization.languages} />
<Tooltip title="Click to open AI assitant">
<div className="select-box" onClick={props.openAiAssistant}>
<DeploymentUnitOutlined style={{fontSize: "24px"}} />
</div>
</Tooltip>
{
Conf.AiAssistantUrl?.trim() && (
<Tooltip title="Click to open AI assistant">
<div className="select-box" onClick={props.openAiAssistant}>
<DeploymentUnitOutlined style={{fontSize: "24px"}} />
</div>
</Tooltip>
)
}
<OpenTour />
{Setting.isAdminUser(props.account) && (props.uri.indexOf("/trees") === -1) &&
<OrganizationSelect
@ -237,7 +241,7 @@ function ManagementPage(props) {
<Link to="/">
<img className="logo" src={logo ?? props.logo} alt="logo" />
</Link>,
disabled: true,
disabled: true, key: "logo",
style: {
padding: 0,
height: "auto",
@ -319,7 +323,35 @@ function ManagementPage(props) {
}
}
return res;
const navItems = props.account.organization.navItems;
if (!Array.isArray(navItems)) {
return res;
}
if (navItems.includes("all")) {
return res;
}
const resFiltered = res.map(item => {
if (!Array.isArray(item.children)) {
return item;
}
const filteredChildren = [];
item.children.forEach(itemChild => {
if (navItems.includes(itemChild.key)) {
filteredChildren.push(itemChild);
}
});
item.children = filteredChildren;
return item;
});
return resFiltered.filter(item => {
if (item.key === "#" || item.key === "logo") {return true;}
return Array.isArray(item.children) && item.children.length > 0;
});
}
function renderLoginIfNotLoggedIn(component) {

View File

@ -26,6 +26,7 @@ import LdapTable from "./table/LdapTable";
import AccountTable from "./table/AccountTable";
import ThemeEditor from "./common/theme/ThemeEditor";
import MfaTable from "./table/MfaTable";
import {NavItemTree} from "./common/NavItemTree";
const {Option} = Select;
@ -420,6 +421,18 @@ class OrganizationEditPage extends React.Component {
} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("organization:User types"), i18next.t("organization:User types - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} mode="tags" style={{width: "100%"}} value={this.state.organization.userTypes} onChange={(value => {this.updateOrganizationField("userTypes", value);})}>
{
this.state.organization.userTypes?.map((item, index) => <Option key={index} value={item}>{item}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("organization:Tags"), i18next.t("organization:Tags - Tooltip"))} :
@ -522,6 +535,21 @@ class OrganizationEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Navbar items"), i18next.t("general:Navbar items - Tooltip"))} :
</Col>
<Col span={22} >
<NavItemTree
disabled={!Setting.isAdminUser(this.props.account)}
checkedKeys={this.state.organization.navItems ?? ["all"]}
defaultExpandedKeys={["all"]}
onCheck={(checked, _) => {
this.updateOrganizationField("navItems", checked);
}}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("organization:Account items"), i18next.t("organization:Account items - Tooltip"))} :

View File

@ -122,7 +122,7 @@ class PaymentResultPage extends React.Component {
payment: payment,
});
if (payment.state === "Created") {
if (["PayPal", "Stripe", "Alipay", "WeChat Pay", "Balance"].includes(payment.type)) {
if (["PayPal", "Stripe", "AirWallex", "Alipay", "WeChat Pay", "Balance"].includes(payment.type)) {
this.setState({
timeout: setTimeout(async() => {
await PaymentBackend.notifyPayment(this.state.owner, this.state.paymentName);

View File

@ -123,6 +123,22 @@ class ProductBuyPage extends React.Component {
return "$";
} else if (product?.currency === "CNY") {
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 {
return "(Unknown currency)";
}
@ -222,6 +238,8 @@ class ProductBuyPage extends React.Component {
text = i18next.t("product:PayPal");
} else if (provider.type === "Stripe") {
text = i18next.t("product:Stripe");
} else if (provider.type === "AirWallex") {
text = i18next.t("product:AirWallex");
}
return (

View File

@ -209,6 +209,14 @@ class ProductEditPage extends React.Component {
[
{id: "USD", name: "USD"},
{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>)
}
</Select>

View File

@ -297,6 +297,8 @@ class ProviderEditPage extends React.Component {
return Setting.getLabel(i18next.t("provider:Scene"), i18next.t("provider:Scene - Tooltip"));
} else if (provider.type === "WeChat Pay") {
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 {
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") {
text = i18next.t("provider:App Key");
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");
}
}
@ -462,6 +467,39 @@ class ProviderEditPage extends React.Component {
this.updateProviderField("issuerUrl", issuerUrl);
}
fetchSamlMetadata() {
this.setState({
metadataLoading: true,
});
fetch(this.state.requestUrl, {
method: "GET",
}).then(res => {
if (!res.ok) {
return Promise.reject("error");
}
return res.text();
}).then(text => {
this.updateProviderField("metadata", text);
this.parseSamlMetadata();
Setting.showMessage("success", i18next.t("general:Successfully added"));
}).catch(err => {
Setting.showMessage("error", err.message);
}).finally(() => {
this.setState({
metadataLoading: false,
});
});
}
parseSamlMetadata() {
try {
this.loadSamlConfiguration();
Setting.showMessage("success", i18next.t("provider:Parse metadata successfully"));
} catch (err) {
Setting.showMessage("error", i18next.t("provider:Can not parse metadata"));
}
}
renderProvider() {
return (
<Card size="small" title={
@ -633,6 +671,20 @@ class ProviderEditPage extends React.Component {
</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" ? (
<React.Fragment>
@ -757,7 +809,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>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
@ -770,7 +822,7 @@ class ProviderEditPage extends React.Component {
</Col>
</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"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{this.getClientSecret2Label(this.state.provider)} :
@ -856,9 +908,9 @@ class ProviderEditPage extends React.Component {
</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>
{["Local File System"].includes(this.state.provider.type) ? null : (
{["Local File System", "CUCloud"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Endpoint"), i18next.t("provider:Region endpoint for Internet"))} :
@ -870,7 +922,7 @@ class ProviderEditPage extends React.Component {
</Col>
</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"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Endpoint (Intranet)"), i18next.t("provider:Region endpoint for Intranet"))} :
@ -882,7 +934,7 @@ class ProviderEditPage extends React.Component {
</Col>
</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"}} >
<Col style={{marginTop: "5px"}} span={2}>
{["Casdoor"].includes(this.state.provider.type) ?
@ -896,7 +948,7 @@ class ProviderEditPage extends React.Component {
</Col>
</Row>
)}
{["Custom HTTP SMS"].includes(this.state.provider.type) ? null : (
{["Custom HTTP SMS", "CUCloud"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Path prefix"), i18next.t("provider:Path prefix - Tooltip"))} :
@ -908,7 +960,7 @@ class ProviderEditPage extends React.Component {
</Col>
</Row>
)}
{["Custom HTTP SMS", "Synology", "Casdoor"].includes(this.state.provider.type) ? null : (
{["Custom HTTP SMS", "Synology", "Casdoor", "CUCloud"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
@ -932,7 +984,7 @@ class ProviderEditPage extends React.Component {
</Col>
</Row>
) : 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"}} >
<Col style={{marginTop: "5px"}} span={2}>
{["Casdoor"].includes(this.state.provider.type) ?
@ -971,7 +1023,7 @@ class ProviderEditPage extends React.Component {
</Col>
</Row>
) : null}
{["Custom HTTP"].includes(this.state.provider.type) ? (
{["Custom HTTP", "CUCloud"].includes(this.state.provider.type) ? (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Parameter"), i18next.t("provider:Parameter - Tooltip"))} :
@ -983,7 +1035,7 @@ class ProviderEditPage extends React.Component {
</Col>
</Row>
) : null}
{["Google Chat"].includes(this.state.provider.type) ? (
{["Google Chat", "CUCloud"].includes(this.state.provider.type) ? (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Metadata"), i18next.t("provider:Metadata - Tooltip"))} :
@ -1223,6 +1275,21 @@ class ProviderEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Metadata url"), i18next.t("provider:Metadata url - Tooltip"))} :
</Col>
<Col span={6} >
<Input value={this.state.requestUrl} onChange={e => {
this.setState({
requestUrl: e.target.value,
});
}} />
</Col>
<Col span={16} >
<Button type="primary" loading={this.state.metadataLoading} onClick={() => {this.fetchSamlMetadata();}}>{i18next.t("general:Request")}</Button>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Metadata"), i18next.t("provider:Metadata - Tooltip"))} :
@ -1236,14 +1303,7 @@ class ProviderEditPage extends React.Component {
<Row style={{marginTop: "20px"}}>
<Col style={{marginTop: "5px"}} span={2} />
<Col span={2}>
<Button type="primary" onClick={() => {
try {
this.loadSamlConfiguration();
Setting.showMessage("success", i18next.t("provider:Parse metadata successfully"));
} catch (err) {
Setting.showMessage("error", i18next.t("provider:Can not parse metadata"));
}
}}>
<Button type="primary" onClick={() => {this.parseSamlMetadata();}}>
{i18next.t("provider:Parse")}
</Button>
</Col>

View File

@ -14,7 +14,7 @@
import React from "react";
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 {isMobile as isMobileDevice} from "react-device-detect";
import "./i18n";
@ -25,6 +25,8 @@ import {Helmet} from "react-helmet";
import * as Conf from "./Conf";
import * as phoneNumber from "libphonenumber-js";
import moment from "moment";
import {MfaAuthVerifyForm, NextMfa, RequiredMfa} from "./auth/mfa/MfaAuthVerifyForm";
import {EmailMfaType, SmsMfaType, TotpMfaType} from "./auth/MfaSetupPage";
const {Option} = Select;
@ -233,6 +235,10 @@ export const OtherProviderInfo = {
logo: `${StaticBaseUrl}/img/casdoor.png`,
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: {
"Aliyun IDaaS": {
@ -273,6 +279,10 @@ export const OtherProviderInfo = {
logo: `${StaticBaseUrl}/img/social_stripe.png`,
url: "https://stripe.com/",
},
"AirWallex": {
logo: `${StaticBaseUrl}/img/payment_airwallex.svg`,
url: "https://airwallex.com/",
},
"GC": {
logo: `${StaticBaseUrl}/img/payment_gc.png`,
url: "https://gc.org",
@ -401,6 +411,10 @@ export const OtherProviderInfo = {
logo: `${StaticBaseUrl}/img/social_viber.png`,
url: "https://www.viber.com/",
},
"CUCloud": {
logo: `${StaticBaseUrl}/img/cucloud.png`,
url: "https://www.cucloud.cn/",
},
},
};
@ -920,7 +934,7 @@ export function getClickable(text) {
return (
<a onClick={() => {
copy(text);
showMessage("success", "Copied to clipboard");
showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}}>
{text}
</a>
@ -981,6 +995,7 @@ export function getProviderTypeOptions(category) {
{id: "Bilibili", name: "Bilibili"},
{id: "Okta", name: "Okta"},
{id: "Douyin", name: "Douyin"},
{id: "Kwai", name: "Kwai"},
{id: "Line", name: "Line"},
{id: "Amazon", name: "Amazon"},
{id: "Auth0", name: "Auth0"},
@ -1078,6 +1093,7 @@ export function getProviderTypeOptions(category) {
{id: "Google Cloud Storage", name: "Google Cloud Storage"},
{id: "Synology", name: "Synology"},
{id: "Casdoor", name: "Casdoor"},
{id: "CUCloud OSS", name: "CUCloud OSS"},
]
);
} else if (category === "SAML") {
@ -1094,6 +1110,7 @@ export function getProviderTypeOptions(category) {
{id: "WeChat Pay", name: "WeChat Pay"},
{id: "PayPal", name: "PayPal"},
{id: "Stripe", name: "Stripe"},
{id: "AirWallex", name: "AirWallex"},
{id: "GC", name: "GC"},
]);
} else if (category === "Captcha") {
@ -1131,6 +1148,7 @@ export function getProviderTypeOptions(category) {
{id: "Reddit", name: "Reddit"},
{id: "Rocket Chat", name: "Rocket Chat"},
{id: "Viber", name: "Viber"},
{id: "CUCloud", name: "CUCloud"},
]);
} else {
return [];
@ -1171,7 +1189,7 @@ export function renderLogo(application) {
function isSigninMethodEnabled(application, signinMethod) {
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 {
return false;
}
@ -1385,7 +1403,13 @@ export function getTag(color, text, icon) {
}
export function getApplicationName(application) {
return `${application?.owner}/${application?.name}`;
let name = `${application?.owner}/${application?.name}`;
if (application?.isShared && application?.organization) {
name += `-org-${application.organization}`;
}
return name;
}
export function getApplicationDisplayName(application) {
@ -1550,9 +1574,25 @@ export function getDefaultHtmlEmailContent() {
export function getCurrencyText(product) {
if (product?.currency === "USD") {
return i18next.t("product:USD");
return i18next.t("currency:USD");
} 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 {
return "(Unknown currency)";
}
@ -1561,3 +1601,114 @@ export function getCurrencyText(product) {
export function isDarkTheme(themeAlgorithm) {
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

@ -397,6 +397,12 @@ class UserEditPage extends React.Component {
</Row>
);
} else if (accountItem.name === "User type") {
let userTypes = ["normal-user", "paid-user"];
const organization = this.getUserOrganization();
if (organization && organization.userTypes && organization.userTypes.length > 0) {
userTypes = organization.userTypes;
}
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
@ -404,7 +410,7 @@ class UserEditPage extends React.Component {
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.user.type} onChange={(value => {this.updateUserField("type", value);})}
options={["normal-user", "paid-user"].map(item => Setting.getOption(item, item))}
options={userTypes.map(item => Setting.getOption(item, item))}
/>
</Col>
</Row>

View File

@ -21,6 +21,7 @@ import {authConfig} from "./Auth";
import * as Setting from "../Setting";
import i18next from "i18next";
import RedirectForm from "../common/RedirectForm";
import {renderLoginPanel} from "../Setting";
class AuthCallback extends React.Component {
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
AuthBackend.loginCas(body, {"service": casService}).then((res) => {
if (res.status === "ok") {
let msg = "Logged in successfully.";
if (casService === "") {
// 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);
const handleCasLogin = (res) => {
let msg = "Logged in successfully.";
if (casService === "") {
// 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);
if (casService !== "") {
const st = res.data;
const newUrl = new URL(casService);
newUrl.searchParams.append("ticket", st);
window.location.href = newUrl.toString();
}
if (casService !== "") {
const st = res.data;
const newUrl = new URL(casService);
newUrl.searchParams.append("ticket", st);
window.location.href = newUrl.toString();
}
};
Setting.checkLoginMfa(res, body, {"service": casService}, handleCasLogin, this);
} else {
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
}
@ -159,54 +164,58 @@ class AuthCallback extends React.Component {
.then((res) => {
if (res.status === "ok") {
const responseType = this.getResponseType();
if (responseType === "login") {
if (res.data2) {
sessionStorage.setItem("signinUrl", signinUrl);
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) {
const handleLogin = (res) => {
if (responseType === "login") {
if (res.data2) {
sessionStorage.setItem("signinUrl", signinUrl);
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
return;
}
const SAMLResponse = res.data;
const redirectUri = res.data2.redirectUrl;
Setting.goToLink(`${redirectUri}?SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);
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);
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 {
this.setState({
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} />;
}
if (this.state.getVerifyTotp !== undefined) {
const application = Setting.getApplicationObj(this);
return renderLoginPanel(application, this.state.getVerifyTotp, this);
}
return (
<div style={{display: "flex", justifyContent: "center", alignItems: "center"}}>
{

View File

@ -264,6 +264,9 @@ class ForgetPage extends React.Component {
)
}
onValuesChange={(changedValues, allValues) => {
if (!changedValues.dest) {
return;
}
const verifyType = changedValues.dest?.indexOf("@") === -1 ? "phone" : "email";
this.setState({
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 {CaptchaModal, CaptchaRule} from "../common/modal/CaptchaModal";
import RedirectForm from "../common/RedirectForm";
import {MfaAuthVerifyForm, NextMfa, RequiredMfa} from "./mfa/MfaAuthVerifyForm";
import {RequiredMfa} from "./mfa/MfaAuthVerifyForm";
import {GoogleOneTapLoginVirtualButton} from "./GoogleLoginButton";
import * as ProviderButton from "./ProviderButton";
const FaceRecognitionModal = lazy(() => import("../common/modal/FaceRecognitionModal"));
@ -438,25 +438,7 @@ class LoginPage extends React.Component {
};
if (res.status === "ok") {
if (res.data === NextMfa) {
this.setState({
getVerifyTotp: () => {
return (
<MfaAuthVerifyForm
mfaProps={res.data2}
formValues={values}
authParams={casParams}
application={this.getApplicationObj()}
onFail={(errorMessage) => {
Setting.showMessage("error", errorMessage);
}}
onSuccess={(res) => loginHandler(res)}
/>);
},
});
} else {
loginHandler(res);
}
Setting.checkLoginMfa(res, values, casParams, loginHandler, this);
} else {
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
}
@ -505,39 +487,13 @@ class LoginPage extends React.Component {
} else {
const SAMLResponse = res.data;
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.data === NextMfa) {
this.setState({
getVerifyTotp: () => {
return (
<MfaAuthVerifyForm
mfaProps={res.data2}
formValues={values}
authParams={oAuthParams}
application={this.getApplicationObj()}
onFail={(errorMessage) => {
Setting.showMessage("error", errorMessage);
}}
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);
}
Setting.checkLoginMfa(res, values, oAuthParams, loginHandler, this);
} else {
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
}
@ -1150,7 +1106,7 @@ class LoginPage extends React.Component {
]);
application?.signinMethods?.forEach((signinMethod) => {
if (signinMethod.rule === "Hide-Password") {
if (signinMethod.rule === "Hide password") {
return;
}
const item = itemsMap.get(generateItemKey(signinMethod.name, signinMethod.rule));

View File

@ -179,8 +179,10 @@ class MfaSetupPage extends React.Component {
mfaProps={this.state.mfaProps}
application={this.state.application}
user={this.props.account}
onSuccess={() => {
onSuccess={(res) => {
this.setState({
dest: res.dest,
countryCode: res.countryCode,
current: this.state.current + 1,
});
}}
@ -195,7 +197,7 @@ class MfaSetupPage extends React.Component {
);
case 2:
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={() => {
Setting.showMessage("success", i18next.t("general:Enabled successfully"));
this.props.onfinish();

View File

@ -13,7 +13,6 @@
// limitations under the License.
import CryptoJS from "crypto-js";
import i18next from "i18next";
import {Buffer} from "buffer";
export function getRandomKeyForObfuscator(obfuscatorType) {
@ -46,17 +45,17 @@ function encrypt(cipher, key, iv, password) {
export function checkPasswordObfuscator(passwordObfuscatorType, passwordObfuscatorKey) {
if (passwordObfuscatorType === undefined) {
return i18next.t("organization:failed to get password obfuscator");
return "passwordObfuscatorType should not be undefined";
} else if (passwordObfuscatorType === "Plain" || passwordObfuscatorType === "") {
return "";
} else if (passwordObfuscatorType === "AES" || passwordObfuscatorType === "DES") {
if (passwordObfuscatorKeyRegexes[passwordObfuscatorType].test(passwordObfuscatorKey)) {
return "";
} 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 {
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",
endpoint: "https://open.douyin.com/platform/oauth/connect",
},
Kwai: {
scope: "user_info",
endpoint: "https://open.kuaishou.com/oauth2/connect",
},
Custom: {
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}`;
} else if (provider.type === "Douyin" || provider.type === "TikTok") {
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") {
return `${provider.customAuthUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${provider.scopes}&response_type=code&state=${state}`;
} else if (provider.type === "Bilibili") {

View File

@ -40,9 +40,11 @@ import SteamLoginButton from "./SteamLoginButton";
import BilibiliLoginButton from "./BilibiliLoginButton";
import OktaLoginButton from "./OktaLoginButton";
import DouyinLoginButton from "./DouyinLoginButton";
import KwaiLoginButton from "./KwaiLoginButton";
import LoginButton from "./LoginButton";
import * as AuthBackend from "./AuthBackend";
import {WechatOfficialAccountModal} from "./Util";
import * as Setting from "../Setting";
function getSigninButton(provider) {
const text = i18next.t("login:Sign in with {type}").replace("{type}", provider.displayName !== "" ? provider.displayName : provider.type);
@ -96,6 +98,8 @@ function getSigninButton(provider) {
return <OktaLoginButton text={text} align={"center"} />;
} else if (provider.type === "Douyin") {
return <DouyinLoginButton text={text} align={"center"} />;
} else if (provider.type === "Kwai") {
return <KwaiLoginButton text={text} align={"center"} />;
} else {
return <LoginButton key={provider.type} type={provider.type} logoUrl={getProviderLogoURL(provider)} />;
}
@ -111,10 +115,14 @@ function goToSamlUrl(provider, location) {
const relayState = `${clientId}&${state}&${providerName}&${realRedirectUri}&${redirectUri}`;
AuthBackend.getSamlLogin(`${provider.owner}/${providerName}`, btoa(relayState)).then((res) => {
if (res.data2 === "POST") {
document.write(res.data);
if (res.status === "ok") {
if (res.data2 === "POST") {
document.write(res.data);
} else {
window.location.href = res.data;
}
} else {
window.location.href = res.data;
Setting.showMessage("error", res.msg);
}
});
}

View File

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

View File

@ -33,7 +33,8 @@ export function MfaAuthVerifyForm({formValues, authParams, mfaProps, application
const verify = ({passcode}) => {
setLoading(true);
const values = {...formValues, passcode, mfaType};
const values = {...formValues, passcode};
values["mfaType"] = mfaProps.mfaType;
const loginFunction = formValues.type === "cas" ? AuthBackend.loginCas : AuthBackend.login;
loginFunction(values, authParams).then((res) => {
if (res.status === "ok") {
@ -71,7 +72,7 @@ export function MfaAuthVerifyForm({formValues, authParams, mfaProps, application
<div style={{marginBottom: 24, textAlign: "center", fontSize: "24px"}}>
{i18next.t("mfa:Multi-factor authentication")}
</div>
{mfaType === SmsMfaType || mfaType === EmailMfaType ? (
{mfaProps.mfaType === SmsMfaType || mfaProps.mfaType === EmailMfaType ? (
<Fragment>
<div style={{marginBottom: 24}}>
{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 * 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 requestEnableMfa = () => {
const data = {
mfaType,
secret,
dest,
countryCode,
...user,
};
data["recoveryCodes"] = recoveryCodes[0];
setLoading(true);
MfaBackend.MfaSetupEnable(data).then(res => {
if (res.status === "ok") {

View File

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

View File

@ -1,5 +1,5 @@
import {UserOutlined} from "@ant-design/icons";
import {Button, Form, Input} from "antd";
import {Button, Form, Input, Space} from "antd";
import i18next from "i18next";
import React, {useEffect} from "react";
import {CountryCodeSelect} from "../../common/select/CountryCodeSelect";
@ -15,15 +15,18 @@ export const MfaVerifySmsForm = ({mfaProps, application, onFinish, method, user}
useEffect(() => {
if (method === mfaAuth) {
setDest(mfaProps.secret);
form.setFieldValue("dest", mfaProps.secret);
return;
}
if (mfaProps.mfaType === SmsMfaType) {
setDest(user.phone);
form.setFieldValue("dest", user.phone);
return;
}
if (mfaProps.mfaType === EmailMfaType) {
setDest(user.email);
form.setFieldValue("dest", user.email);
}
}, [mfaProps.mfaType]);
@ -57,45 +60,44 @@ export const MfaVerifySmsForm = ({mfaProps, application, onFinish, method, user}
<div style={{marginBottom: 20, textAlign: "left", gap: 8}}>
{isEmail() ? i18next.t("mfa:Your email is") : i18next.t("mfa:Your phone is")} {dest}
</div> :
(<React.Fragment>
(
<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")}
</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
name="passcode"
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("mfaType", values.mfaType);
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`, {
method: "POST",
credentials: "include",
@ -44,6 +47,10 @@ export function MfaSetupEnable(values) {
formData.append("mfaType", values.mfaType);
formData.append("owner", values.owner);
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`, {
method: "POST",
credentials: "include",

View File

@ -135,6 +135,15 @@ const Dashboard = (props) => {
i18next.t("general:Applications"),
i18next.t("general:Organizations"),
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%"},
grid: {left: "3%", right: "4%", bottom: "0", top: "25%", containLabel: true},
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:Applications"), type: "line", data: dashboardData.applicationCounts},
{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);

View File

@ -14,6 +14,7 @@
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";
@ -26,7 +27,7 @@ export const generateCasdoorAppUrl = (accessToken, forQrCode = true) => {
return {qrUrl, error};
}
qrUrl = `casdoor-app://login?serverUrl=${window.location.origin}&accessToken=${accessToken}`;
qrUrl = `casdoor-authenticator://login?serverUrl=${window.location.origin}&accessToken=${accessToken}`;
if (forQrCode && qrUrl.length >= 2000) {
qrUrl = "";
@ -62,12 +63,8 @@ export const CasdoorAppUrl = ({accessToken}) => {
return;
}
try {
await navigator.clipboard.writeText(qrUrl);
Setting.showMessage("success", i18next.t("general:Copied to clipboard"));
} catch (err) {
Setting.showMessage("error", i18next.t("general:Failed to copy"));
}
copy(qrUrl);
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
};
if (error) {
@ -82,14 +79,9 @@ export const CasdoorAppUrl = ({accessToken}) => {
alignItems: "center",
marginBottom: "10px",
}}>
<span>{i18next.t("general:URL String")}</span>
{window.isSecureContext && (
<Button
size="small"
onClick={handleCopyUrl}
style={{marginLeft: "10px"}}
>
{i18next.t("general:Copy URL")}
<Button size="small" type="primary" onClick={handleCopyUrl} style={{marginLeft: "10px"}}>
{i18next.t("resource:Copy Link")}
</Button>
)}
</div>

View File

@ -0,0 +1,97 @@
import i18next from "i18next";
import {Tree} from "antd";
import React from "react";
export const NavItemTree = ({disable, checkedKeys, defaultExpandedKeys, onCheck}) => {
const NavItemNodes = [
{
title: i18next.t("organization:All"),
key: "all",
children: [
{
title: i18next.t("general:Home"),
key: "/home-top",
children: [
{title: i18next.t("general:Dashboard"), key: "/"},
{title: i18next.t("general:Shortcuts"), key: "/shortcuts"},
{title: i18next.t("general:Apps"), key: "/apps"},
],
},
{
title: i18next.t("general:User Management"),
key: "/orgs-top",
children: [
{title: i18next.t("general:Organizations"), key: "/organizations"},
{title: i18next.t("general:Groups"), key: "/groups"},
{title: i18next.t("general:Users"), key: "/users"},
{title: i18next.t("general:Invitations"), key: "/invitations"},
],
},
{
title: i18next.t("general:Identity"),
key: "/applications-top",
children: [
{title: i18next.t("general:Applications"), key: "/applications"},
{title: i18next.t("general:Providers"), key: "/providers"},
{title: i18next.t("general:Resources"), key: "/resources"},
{title: i18next.t("general:Certs"), key: "/certs"},
],
},
{
title: i18next.t("general:Authorization"),
key: "/roles-top",
children: [
{title: i18next.t("general:Applications"), key: "/roles"},
{title: i18next.t("general:Permissions"), key: "/permissions"},
{title: i18next.t("general:Models"), key: "/models"},
{title: i18next.t("general:Adapters"), key: "/adapters"},
{title: i18next.t("general:Enforcers"), key: "/enforcers"},
],
},
{
title: i18next.t("general:Logging & Auditing"),
key: "/sessions-top",
children: [
{title: i18next.t("general:Sessions"), key: "/sessions"},
{title: i18next.t("general:Records"), key: "/records"},
{title: i18next.t("general:Tokens"), key: "/tokens"},
{title: i18next.t("general:Verifications"), key: "/verifications"},
],
},
{
title: i18next.t("general:Business & Payments"),
key: "/business-top",
children: [
{title: i18next.t("general:Products"), key: "/products"},
{title: i18next.t("general:Payments"), key: "/payments"},
{title: i18next.t("general:Plans"), key: "/plans"},
{title: i18next.t("general:Pricings"), key: "/pricings"},
{title: i18next.t("general:Subscriptions"), key: "/subscriptions"},
{title: i18next.t("general:Transactions"), key: "/transactions"},
],
},
{
title: i18next.t("general:Admin"),
key: "/admin-top",
children: [
{title: i18next.t("general:System Info"), key: "/sysinfo"},
{title: i18next.t("general:Syncers"), key: "/syncers"},
{title: i18next.t("general:Webhooks"), key: "/webhooks"},
{title: i18next.t("general:Swagger"), key: "/swagger"},
],
},
],
},
];
return (
<Tree
disabled={disable}
checkable
checkedKeys={checkedKeys}
defaultExpandedKeys={defaultExpandedKeys}
onCheck={onCheck}
treeData={NavItemNodes}
/>
);
};

View File

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

View File

@ -161,6 +161,18 @@
"Sending": "Sending",
"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": {
"Edit Enforcer": "Edit Enforcer",
"New Enforcer": "New Enforcer"
@ -266,10 +278,13 @@
"Go to writable demo site?": "Go to writable demo site?",
"Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip",
"Hide password": "Hide password",
"Home": "Home",
"Home - Tooltip": "Home page of the application",
"ID": "ID",
"ID - Tooltip": "Unique random string",
"IP whitelist": "IP whitelist",
"IP whitelist - Tooltip": "IP whitelist - Tooltip",
"Identity": "Identity",
"Invitations": "Invitations",
"Is enabled": "Is enabled",
@ -312,6 +327,10 @@
"Password - Tooltip": "Make sure the password is correct",
"Password complexity options": "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 - Tooltip": "Random parameter used for password encryption",
"Password type": "Password type",
@ -559,7 +578,6 @@
"Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed",
"Verify Code": "Verify Code",
"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",
@ -574,6 +592,8 @@
"Secret Key": "Secret Key"
},
"model": {
"Advanced Editor": "Advanced Editor",
"Basic Editor": "Basic Editor",
"Edit Model": "Edit Model",
"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",
@ -592,6 +612,8 @@
"Modify rule": "Modify rule",
"New Organization": "New Organization",
"Optional": "Optional",
"Password expire days": "Password expire days",
"Password expire days - Tooltip": "Password expire days - Tooltip",
"Prompt": "Prompt",
"Required": "Required",
"Soft deletion": "Soft deletion",
@ -709,7 +731,6 @@
"Alipay": "Alipay",
"Buy": "Buy",
"Buy Product": "Buy Product",
"CNY": "CNY",
"Detail": "Detail",
"Detail - Tooltip": "Detail of product",
"Dummy": "Dummy",
@ -736,11 +757,11 @@
"Sold": "Sold",
"Sold - Tooltip": "Quantity sold",
"Stripe": "Stripe",
"AirWallex": "AirWallex",
"Tag - Tooltip": "Tag of product",
"Test buy page..": "Test buy page..",
"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.",
"USD": "USD",
"WeChat Pay": "WeChat Pay"
},
"provider": {
@ -893,6 +914,7 @@
"Signup HTML - Edit": "Signup HTML - Edit",
"Signup HTML - Tooltip": "Custom HTML for replacing the default signup page style",
"Signup group": "Signup group",
"Signup group - Tooltip": "Signup group - Tooltip",
"Silent": "Silent",
"Site key": "Site key",
"Site key - Tooltip": "Site key",
@ -1157,6 +1179,7 @@
"Keys": "Keys",
"Language": "Language",
"Language - Tooltip": "Language - Tooltip",
"Last change password time": "Last change password time",
"Link": "Link",
"Location": "Location",
"Location - Tooltip": "City of residence",

View File

@ -161,6 +161,18 @@
"Sending": "Odesílání",
"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": {
"Edit Enforcer": "Upravit Enforcer",
"New Enforcer": "Nový Enforcer"
@ -266,10 +278,13 @@
"Go to writable demo site?": "Přejít na zapisovatelnou demo stránku?",
"Groups": "Skupiny",
"Groups - Tooltip": "Skupiny - Tooltip",
"Hide password": "Hide password",
"Home": "Domů",
"Home - Tooltip": "Domovská stránka aplikace",
"ID": "ID",
"ID - Tooltip": "Unikátní náhodný řetězec",
"IP whitelist": "IP whitelist",
"IP whitelist - Tooltip": "IP whitelist - Tooltip",
"Identity": "Identita",
"Invitations": "Pozvánky",
"Is enabled": "Je povoleno",
@ -312,6 +327,10 @@
"Password - Tooltip": "Ujistěte se, že heslo je správné",
"Password complexity options": "Možnosti 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 - Tooltip": "Náhodný parametr použitý pro šifrování hesla",
"Password type": "Typ hesla",
@ -559,7 +578,6 @@
"Use SMS": "Použít SMS",
"Use SMS verification code": "Použít ověřovací kód SMS",
"Use a recovery code": "Použít obnovovací kód",
"Verification failed": "Ověření selhalo",
"Verify Code": "Ověřit kód",
"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",
@ -574,6 +592,8 @@
"Secret Key": "Secret Key"
},
"model": {
"Advanced Editor": "Pokročilý editor",
"Basic Editor": "Základní editor",
"Edit Model": "Upravit model",
"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",
@ -592,6 +612,8 @@
"Modify rule": "Upravit pravidlo",
"New Organization": "Nová organizace",
"Optional": "Volitelný",
"Password expire days": "Password expire days",
"Password expire days - Tooltip": "Password expire days - Tooltip",
"Prompt": "Výzva",
"Required": "Povinné",
"Soft deletion": "Měkké smazání",
@ -709,7 +731,6 @@
"Alipay": "Alipay",
"Buy": "Koupit",
"Buy Product": "Koupit produkt",
"CNY": "CNY",
"Detail": "Detail",
"Detail - Tooltip": "Detail produktu",
"Dummy": "Dummy",
@ -736,11 +757,11 @@
"Sold": "Prodáno",
"Sold - Tooltip": "Prodávané množství",
"Stripe": "Stripe",
"AirWallex": "AirWallex",
"Tag - Tooltip": "Štítek produktu",
"Test buy page..": "Testovací stránka nákupu..",
"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.",
"USD": "USD",
"WeChat Pay": "WeChat Pay"
},
"provider": {
@ -893,6 +914,7 @@
"Signup HTML - Edit": "Upravit HTML pro registraci",
"Signup HTML - Tooltip": "Vlastní HTML pro nahrazení výchozího stylu registrační stránky",
"Signup group": "Skupina pro registraci",
"Signup group - Tooltip": "Signup group - Tooltip",
"Silent": "Tiché",
"Site key": "Klíč stránky",
"Site key - Tooltip": "Nápověda ke klíči stránky",
@ -1157,6 +1179,7 @@
"Keys": "Klíče",
"Language": "Jazyk",
"Language - Tooltip": "Jazyk - Nápověda",
"Last change password time": "Last change password time",
"Link": "Odkaz",
"Location": "Místo",
"Location - Tooltip": "Město bydliště",

View File

@ -161,6 +161,18 @@
"Sending": "Sendet",
"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": {
"Edit Enforcer": "Edit Enforcer",
"New Enforcer": "New Enforcer"
@ -266,10 +278,13 @@
"Go to writable demo site?": "Gehe zur beschreibbaren Demo-Website?",
"Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip",
"Hide password": "Hide password",
"Home": "Zuhause",
"Home - Tooltip": "Homepage der Anwendung",
"ID": "ID",
"ID - Tooltip": "Einzigartiger Zufallsstring",
"IP whitelist": "IP whitelist",
"IP whitelist - Tooltip": "IP whitelist - Tooltip",
"Identity": "Identity",
"Invitations": "Invitations",
"Is enabled": "Ist aktiviert",
@ -312,6 +327,10 @@
"Password - Tooltip": "Stellen Sie sicher, dass das Passwort korrekt ist",
"Password complexity options": "Password complexity options",
"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 - Tooltip": "Zufälliger Parameter, der für die Verschlüsselung von Passwörtern verwendet wird",
"Password type": "Passworttyp",
@ -559,7 +578,6 @@
"Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed",
"Verify Code": "Verify Code",
"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",
@ -574,6 +592,8 @@
"Secret Key": "Secret Key"
},
"model": {
"Advanced Editor": "Erweiterter Editor",
"Basic Editor": "Basis-Editor",
"Edit Model": "Modell bearbeiten",
"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",
@ -592,6 +612,8 @@
"Modify rule": "Regel ändern",
"New Organization": "Neue Organisation",
"Optional": "Optional",
"Password expire days": "Password expire days",
"Password expire days - Tooltip": "Password expire days - Tooltip",
"Prompt": "Prompt",
"Required": "Required",
"Soft deletion": "Softe Löschung",
@ -709,7 +731,6 @@
"Alipay": "Alipay",
"Buy": "Kaufen",
"Buy Product": "Produkt kaufen",
"CNY": "CNY",
"Detail": "Detail",
"Detail - Tooltip": "Detail des Produkts",
"Dummy": "Dummy",
@ -736,11 +757,11 @@
"Sold": "Verkauft",
"Sold - Tooltip": "Menge verkauft",
"Stripe": "Stripe",
"AirWallex": "AirWallex",
"Tag - Tooltip": "Tag des Produkts",
"Test buy page..": "Testkaufseite.",
"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.",
"USD": "USD",
"WeChat Pay": "WeChat Pay"
},
"provider": {
@ -893,6 +914,7 @@
"Signup HTML - Edit": "Registrierung HTML - Bearbeiten",
"Signup HTML - Tooltip": "Benutzerdefiniertes HTML zur Ersetzung des Standard-Registrierungs-Seitenstils",
"Signup group": "Signup group",
"Signup group - Tooltip": "Signup group - Tooltip",
"Silent": "Silent",
"Site key": "Site-Key",
"Site key - Tooltip": "Seitenschlüssel",
@ -1157,6 +1179,7 @@
"Keys": "Schlüssel",
"Language": "Language",
"Language - Tooltip": "Language - Tooltip",
"Last change password time": "Last change password time",
"Link": "Link",
"Location": "Ort",
"Location - Tooltip": "Stadt des Wohnsitzes",

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