Compare commits

...

39 Commits

Author SHA1 Message Date
a45d2b87c1 feat: Add translations for Persian (#3372) 2024-11-23 16:24:07 +08:00
8484465d09 feat: fix SAML failed to redirect issue when login api returns RequiredMfa (#3364) 2024-11-21 20:31:56 +08:00
dff65eee20 feat: Force users to change their passwords after 3/6/12 months (#3352)
* feat: Force users to change their passwords after 3/6/12 months

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

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

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

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

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

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

* feat: Only fulfill the first requirement

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

* fix: promote format

* fix: promote format

* fix: promote format

* fix: promote format

* Update application.go

* Update cors_filter.go

* Update validation.go

---------

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2024-10-20 20:09:21 +08:00
6fcdad2100 feat: fix bug that fails to login when PasswordObfuscator is enabled (#3299) 2024-10-19 23:09:59 +08:00
69d26d5c21 feat: add-user/update-user API should check if username/id/email/phone has duplicated with existing user (#3295) 2024-10-18 22:18:37 +08:00
94e6b5ecb8 feat: fix bug in SetPassword() API (#3296) 2024-10-18 20:50:43 +08:00
95e8bdcd36 feat: add initDataNewOnly to app.conf to skip overriding existing data in initDataFromFile() (#3294)
* feat: support control whether overwrite existing data during initDataFromFile

* feat: change conf var name

* feat: change conf var name
2024-10-18 00:08:08 +08:00
6f1f93725e feat: fix GetAllActions()'s bug (#3289) 2024-10-16 21:55:06 +08:00
7ae067e369 feat: only admin can specify user in BuyProduct() (#3287)
* fix: balance can be used without login

* fix: balance can be used without login

* fix: fix bug

* fix: fix bug
2024-10-16 00:02:04 +08:00
dde936e935 feat: fix null application crash in CheckEntryIp() 2024-10-15 22:11:15 +08:00
fb561a98c8 feat: fix null user crash in RefreshToken() 2024-10-15 21:38:33 +08:00
7cd8f030ee feat: support IP limitation for user entry pages (#3267)
* feat: support IP limitation for user entry pages

* fix: error message, ip whiteList, check_entry_ip

* fix: perform checks on the backend

* fix: change the implementation of checking IpWhitelist

* fix: add entryIpCheck in SetPassword and remove it from VerifyCode

* fix: remove additional error message pop-ups

* fix: add isRestricted and show ip error in EntryPage.js

* fix: error message

* Update auth.go

* Update check_ip.go

* Update check_ip.go

* fix: update return value of the check function from string to error

* fix: remoteAddress position

* fix: IP whitelist

* fix: clientIp

* fix:add util.GetClientIpFromRequest

* fix: remove duplicate IP and port separation codes and remove extra special characters after clientIp

* fix: gofumpt

* fix: getIpInfo and localhost

---------

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2024-10-15 20:40:14 +08:00
a3f8ded10c feat: refactor util.GetClientIpFromRequest() 2024-10-15 12:22:38 +08:00
e3d135bc6e feat: improve MFA desc text (#3284)
* fix: fix i18n error for mfa

* fix: fix i18n error for mfa

* fix: promote translate
2024-10-14 18:31:48 +08:00
fc864b0de4 feat: support ".login-panel-dark" CSS for signup/login pages (#3269)
* feat: add custom dark mode CSS for login and registration forms.

* refactor: extract dark theme check to Setting.js
2024-10-13 22:31:54 +08:00
73 changed files with 2239 additions and 1491 deletions

View File

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

View File

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

View File

@ -29,5 +29,6 @@ radiusServerPort = 1812
radiusSecret = "secret" radiusSecret = "secret"
quota = {"organization": -1, "user": -1, "application": -1, "provider": -1} quota = {"organization": -1, "user": -1, "application": -1, "provider": -1}
logConfig = {"filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"} logConfig = {"filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}
initDataNewOnly = false
initDataFile = "./init_data.json" initDataFile = "./init_data.json"
frontendBaseDir = "../casdoor" frontendBaseDir = "../cc_0"

View File

@ -116,6 +116,13 @@ func (c *ApiController) Signup() {
return return
} }
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
err = object.CheckEntryIp(clientIp, nil, application, organization, c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error())
return
}
msg := object.CheckUserSignup(application, organization, &authForm, c.GetAcceptLanguage()) msg := object.CheckUserSignup(application, organization, &authForm, c.GetAcceptLanguage())
if msg != "" { if msg != "" {
c.ResponseError(msg) c.ResponseError(msg)

View File

@ -110,6 +110,9 @@ func (c *ApiController) GetApplication() {
} }
} }
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
object.CheckEntryIp(clientIp, nil, application, nil, c.GetAcceptLanguage())
c.ResponseOk(object.GetMaskedApplication(application, userId)) c.ResponseOk(object.GetMaskedApplication(application, userId))
} }
@ -229,6 +232,11 @@ func (c *ApiController) UpdateApplication() {
return return
} }
if err = object.CheckIpWhitelist(application.IpWhitelist, c.GetAcceptLanguage()); err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.UpdateApplication(id, &application)) c.Data["json"] = wrapActionResponse(object.UpdateApplication(id, &application))
c.ServeJSON() c.ServeJSON()
} }
@ -259,6 +267,11 @@ func (c *ApiController) AddApplication() {
return return
} }
if err = object.CheckIpWhitelist(application.IpWhitelist, c.GetAcceptLanguage()); err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddApplication(&application)) c.Data["json"] = wrapActionResponse(object.AddApplication(&application))
c.ServeJSON() c.ServeJSON()
} }

View File

@ -55,6 +55,13 @@ func tokenToResponse(token *object.Token) *Response {
func (c *ApiController) HandleLoggedIn(application *object.Application, user *object.User, form *form.AuthForm) (resp *Response) { func (c *ApiController) HandleLoggedIn(application *object.Application, user *object.User, form *form.AuthForm) (resp *Response) {
userId := user.GetId() userId := user.GetId()
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
err := object.CheckEntryIp(clientIp, user, application, application.OrganizationObj, c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error())
return
}
allowed, err := object.CheckLoginPermission(userId, application) allowed, err := object.CheckLoginPermission(userId, application)
if err != nil { if err != nil {
c.ResponseError(err.Error(), nil) c.ResponseError(err.Error(), nil)
@ -256,6 +263,9 @@ func (c *ApiController) GetApplicationLogin() {
} }
} }
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
object.CheckEntryIp(clientIp, nil, application, nil, c.GetAcceptLanguage())
application = object.GetMaskedApplication(application, "") application = object.GetMaskedApplication(application, "")
if msg != "" { if msg != "" {
c.ResponseError(msg, application) c.ResponseError(msg, application)
@ -844,6 +854,7 @@ func (c *ApiController) Login() {
} }
if authForm.Passcode != "" { if authForm.Passcode != "" {
user.CountryCode = user.GetCountryCode(user.CountryCode)
mfaUtil := object.GetMfaUtil(authForm.MfaType, user.GetPreferredMfaProps(false)) mfaUtil := object.GetMfaUtil(authForm.MfaType, user.GetPreferredMfaProps(false))
if mfaUtil == nil { if mfaUtil == nil {
c.ResponseError("Invalid multi-factor authentication type") c.ResponseError("Invalid multi-factor authentication type")

View File

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

View File

@ -119,6 +119,11 @@ func (c *ApiController) UpdateOrganization() {
return return
} }
if err = object.CheckIpWhitelist(organization.IpWhitelist, c.GetAcceptLanguage()); err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.UpdateOrganization(id, &organization)) c.Data["json"] = wrapActionResponse(object.UpdateOrganization(id, &organization))
c.ServeJSON() c.ServeJSON()
} }
@ -149,6 +154,11 @@ func (c *ApiController) AddOrganization() {
return return
} }
if err = object.CheckIpWhitelist(organization.IpWhitelist, c.GetAcceptLanguage()); err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddOrganization(&organization)) c.Data["json"] = wrapActionResponse(object.AddOrganization(&organization))
c.ServeJSON() c.ServeJSON()
} }

View File

@ -182,6 +182,10 @@ func (c *ApiController) BuyProduct() {
paidUserName := c.Input().Get("userName") paidUserName := c.Input().Get("userName")
owner, _ := util.GetOwnerAndNameFromId(id) owner, _ := util.GetOwnerAndNameFromId(id)
userId := util.GetId(owner, paidUserName) userId := util.GetId(owner, paidUserName)
if paidUserName != "" && !c.IsAdmin() {
c.ResponseError(c.T("general:Only admin user can specify user"))
return
}
if paidUserName == "" { if paidUserName == "" {
userId = c.GetSessionUsername() userId = c.GetSessionUsername()
} }

View File

@ -364,7 +364,8 @@ func (c *ApiController) AddUser() {
return return
} }
msg := object.CheckUsername(user.Name, c.GetAcceptLanguage()) emptyUser := object.User{}
msg := object.CheckUpdateUser(&emptyUser, &user, c.GetAcceptLanguage())
if msg != "" { if msg != "" {
c.ResponseError(msg) c.ResponseError(msg)
return return
@ -489,7 +490,12 @@ func (c *ApiController) SetPassword() {
c.ResponseError(c.T("general:Missing parameter")) c.ResponseError(c.T("general:Missing parameter"))
return return
} }
if userId != c.GetSession("verifiedUserId") {
c.ResponseError(c.T("general:Wrong userId"))
return
}
c.SetSession("verifiedCode", "") c.SetSession("verifiedCode", "")
c.SetSession("verifiedUserId", "")
} }
targetUser, err := object.GetUser(userId) targetUser, err := object.GetUser(userId)
@ -535,11 +541,29 @@ func (c *ApiController) SetPassword() {
return return
} }
application, err := object.GetApplicationByUser(targetUser)
if err != nil {
c.ResponseError(err.Error())
return
}
if application == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:the application for user %s is not found"), userId))
return
}
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
err = object.CheckEntryIp(clientIp, targetUser, application, organization, c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error())
return
}
targetUser.Password = newPassword targetUser.Password = newPassword
targetUser.UpdateUserPassword(organization) targetUser.UpdateUserPassword(organization)
targetUser.NeedUpdatePassword = false targetUser.NeedUpdatePassword = false
targetUser.LastChangePasswordTime = util.GetCurrentTime()
_, err = object.UpdateUser(userId, targetUser, []string{"password", "need_update_password", "password_type"}, false) _, err = object.UpdateUser(userId, targetUser, []string{"password", "need_update_password", "password_type", "last_change_password_time"}, false)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return

View File

@ -132,7 +132,8 @@ func (c *ApiController) SendVerificationCode() {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
remoteAddr := util.GetIPFromRequest(c.Ctx.Request)
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
if msg := vform.CheckParameter(form.SendVerifyCode, c.GetAcceptLanguage()); msg != "" { if msg := vform.CheckParameter(form.SendVerifyCode, c.GetAcceptLanguage()); msg != "" {
c.ResponseError(msg) c.ResponseError(msg)
@ -259,7 +260,7 @@ func (c *ApiController) SendVerificationCode() {
return return
} }
sendResp = object.SendVerificationCodeToEmail(organization, user, provider, remoteAddr, vform.Dest) sendResp = object.SendVerificationCodeToEmail(organization, user, provider, clientIp, vform.Dest)
case object.VerifyTypePhone: case object.VerifyTypePhone:
if vform.Method == LoginVerification || vform.Method == ForgetVerification { if vform.Method == LoginVerification || vform.Method == ForgetVerification {
if user != nil && util.GetMaskedPhone(user.Phone) == vform.Dest { if user != nil && util.GetMaskedPhone(user.Phone) == vform.Dest {
@ -293,6 +294,7 @@ func (c *ApiController) SendVerificationCode() {
} }
vform.CountryCode = mfaProps.CountryCode vform.CountryCode = mfaProps.CountryCode
vform.CountryCode = user.GetCountryCode(vform.CountryCode)
} }
provider, err = application.GetSmsProvider(vform.Method, vform.CountryCode) provider, err = application.GetSmsProvider(vform.Method, vform.CountryCode)
@ -309,7 +311,7 @@ func (c *ApiController) SendVerificationCode() {
c.ResponseError(fmt.Sprintf(c.T("verification:Phone number is invalid in your region %s"), vform.CountryCode)) c.ResponseError(fmt.Sprintf(c.T("verification:Phone number is invalid in your region %s"), vform.CountryCode))
return return
} else { } else {
sendResp = object.SendVerificationCodeToPhone(organization, user, provider, remoteAddr, phone) sendResp = object.SendVerificationCodeToPhone(organization, user, provider, clientIp, phone)
} }
} }
@ -532,5 +534,6 @@ func (c *ApiController) VerifyCode() {
} }
c.SetSession("verifiedCode", authForm.Code) c.SetSession("verifiedCode", authForm.Code)
c.SetSession("verifiedUserId", user.GetId())
c.ResponseOk() c.ResponseOk()
} }

View File

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

View File

@ -15,10 +15,10 @@
"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) 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 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 application: %s does not exist": "Приложение: %s не существует",
"The login method: login with LDAP is not enabled for the application": "The login method: login with LDAP is not enabled for the application", "The login method: login with LDAP is not enabled for the application": "Метод входа в систему: вход с помощью LDAP не включен для приложения",
"The login method: login with SMS is not enabled for the application": "The login method: login with SMS is not enabled for the application", "The login method: login with SMS is not enabled for the application": "Метод входа: вход с помощью SMS не включен для приложения",
"The login method: login with email is not enabled for the application": "The login method: login with email is not enabled for the application", "The login method: login with email is not enabled for the application": "Метод входа: вход с помощью электронной почты не включен для приложения",
"The login method: login with face is not enabled for the application": "The login method: login with face is not enabled for the application", "The login method: login with face is not enabled for the application": "Метод входа: вход с помощью лица не включен для приложения",
"The login method: login with password is not enabled for the application": "Метод входа: вход с паролем не включен для приложения", "The login method: login with password is not enabled for the application": "Метод входа: вход с паролем не включен для приложения",
"The organization: %s does not exist": "The organization: %s does not exist", "The organization: %s does not exist": "The organization: %s does not exist",
"The provider: %s is not enabled for the application": "Провайдер: %s не включен для приложения", "The provider: %s is not enabled for the application": "Провайдер: %s не включен для приложения",
@ -53,16 +53,16 @@
"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 email corresponding to the invitation code": "Пожалуйста, зарегистрируйтесь, используя электронную почту, соответствующую коду приглашения",
"Please register using the phone corresponding to the invitation code": "Please register using the phone corresponding to the invitation code", "Please register using the phone corresponding to the invitation code": "Пожалуйста, зарегистрируйтесь по телефону, соответствующему коду приглашения",
"Please register using the username corresponding to the invitation code": "Please register using the username corresponding to the invitation code", "Please register using the username corresponding to the invitation code": "Пожалуйста, зарегистрируйтесь, используя имя пользователя, соответствующее коду приглашения",
"Session outdated, please login again": "Сессия устарела, пожалуйста, войдите снова", "Session outdated, please login again": "Сессия устарела, пожалуйста, войдите снова",
"The invitation code has already been used": "The invitation code has already been used", "The invitation code has already been used": "The invitation code has already been used",
"The user is forbidden to sign in, please contact the administrator": "Пользователю запрещен вход, пожалуйста, обратитесь к администратору", "The user is forbidden to sign in, please contact the administrator": "Пользователю запрещен вход, пожалуйста, обратитесь к администратору",
"The user: %s doesn't exist in LDAP server": "Пользователь %s не существует на LDAP сервере", "The user: %s doesn't exist in LDAP server": "Пользователь %s не существует на LDAP сервере",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "Имя пользователя может состоять только из буквенно-цифровых символов, нижних подчеркиваний или дефисов, не может содержать последовательные дефисы или подчеркивания, а также не может начинаться или заканчиваться на дефис или подчеркивание.", "The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "Имя пользователя может состоять только из буквенно-цифровых символов, нижних подчеркиваний или дефисов, не может содержать последовательные дефисы или подчеркивания, а также не может начинаться или заканчиваться на дефис или подчеркивание.",
"The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex": "The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex", "The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex": "Значение \\\"%s\\\" для поля аккаунта \\\"%s\\\" не соответствует регулярному значению",
"The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"": "The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"", "The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"": "Значение \\\"%s\\\" поля регистрации \\\"%s\\\" не соответствует регулярному выражению приложения \\\"%s\\\"",
"Username already exists": "Имя пользователя уже существует", "Username already exists": "Имя пользователя уже существует",
"Username cannot be an email address": "Имя пользователя не может быть адресом электронной почты", "Username cannot be an email address": "Имя пользователя не может быть адресом электронной почты",
"Username cannot contain white spaces": "Имя пользователя не может содержать пробелы", "Username cannot contain white spaces": "Имя пользователя не может содержать пробелы",
@ -78,11 +78,11 @@
"general": { "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 organization: %s should have one application at least": "Организация: %s должна иметь хотя бы одно приложение",
"The user: %s doesn't exist": "Пользователь %s не существует", "The user: %s doesn't exist": "Пользователь %s не существует",
"don't support captchaProvider: ": "неподдерживаемый captchaProvider: ", "don't support captchaProvider: ": "неподдерживаемый 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" "this operation requires administrator to perform": "для выполнения этой операции требуется администратор"
}, },
"ldap": { "ldap": {
"Ldap server exist": "LDAP-сервер существует" "Ldap server exist": "LDAP-сервер существует"
@ -101,11 +101,11 @@
"Unknown modify rule %s.": "Неизвестное изменение правила %s." "Unknown modify rule %s.": "Неизвестное изменение правила %s."
}, },
"permission": { "permission": {
"The permission: \\\"%s\\\" doesn't exist": "The permission: \\\"%s\\\" doesn't exist" "The permission: \\\"%s\\\" doesn't exist": "Разрешение: \\\"%s\\\" не существует"
}, },
"provider": { "provider": {
"Invalid application id": "Неверный идентификатор приложения", "Invalid application id": "Неверный идентификатор приложения",
"the provider: %s does not exist": "провайдер: %s не существует" "the provider: %s does not exist": "Провайдер: %s не существует"
}, },
"resource": { "resource": {
"User is nil for tag: avatar": "Пользователь равен нулю для тега: аватар", "User is nil for tag: avatar": "Пользователь равен нулю для тега: аватар",
@ -115,7 +115,7 @@
"Application %s not found": "Приложение %s не найдено" "Application %s not found": "Приложение %s не найдено"
}, },
"saml_sp": { "saml_sp": {
"provider %s's category is not SAML": "категория провайдера %s не является SAML" "provider %s's category is not SAML": "Категория провайдера %s не является SAML"
}, },
"service": { "service": {
"Empty parameters for emailForm: %v": "Пустые параметры для emailForm: %v", "Empty parameters for emailForm: %v": "Пустые параметры для emailForm: %v",
@ -148,7 +148,7 @@
"verification": { "verification": {
"Invalid captcha provider.": "Недействительный поставщик CAPTCHA.", "Invalid captcha provider.": "Недействительный поставщик CAPTCHA.",
"Phone number is invalid in your region %s": "Номер телефона недействителен в вашем регионе %s", "Phone number is invalid in your region %s": "Номер телефона недействителен в вашем регионе %s",
"The verification code has not been sent yet!": "The verification code has not been sent yet!", "The verification code has not been sent yet!": "Код проверки еще не отправлен!",
"The verification code has not been sent yet, or has already been used!": "The verification code has not been sent yet, or has already been used!", "The verification code has not been sent yet, or has already been used!": "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.": "Невозможно получить правило изменения электронной почты.",
@ -156,8 +156,8 @@
"Unknown type": "Неизвестный тип", "Unknown type": "Неизвестный тип",
"Wrong verification code!": "Неправильный код подтверждения!", "Wrong verification code!": "Неправильный код подтверждения!",
"You should verify your code in %d min!": "Вы должны проверить свой код через %d минут!", "You should verify your code in %d min!": "Вы должны проверить свой код через %d минут!",
"please add a SMS provider to the \\\"Providers\\\" list for the application: %s": "please add a SMS provider to the \\\"Providers\\\" list for the application: %s", "please add a SMS provider to the \\\"Providers\\\" list for the application: %s": "Пожалуйста, добавьте поставщика SMS в список \\\"Провайдеры\\\" для приложения: %s",
"please add an Email provider to the \\\"Providers\\\" list for the application: %s": "please add an Email provider to the \\\"Providers\\\" list for the application: %s", "please add an Email provider to the \\\"Providers\\\" list for the application: %s": "Пожалуйста, добавьте поставщика электронной почты в список \\\"Провайдеры\\\" для приложения: %s",
"the user does not exist, please sign up first": "Пользователь не существует, пожалуйста, сначала зарегистрируйтесь" "the user does not exist, please sign up first": "Пользователь не существует, пожалуйста, сначала зарегистрируйтесь"
}, },
"webauthn": { "webauthn": {

View File

@ -95,6 +95,7 @@ type Application struct {
Tags []string `xorm:"mediumtext" json:"tags"` Tags []string `xorm:"mediumtext" json:"tags"`
SamlAttributes []*SamlItem `xorm:"varchar(1000)" json:"samlAttributes"` SamlAttributes []*SamlItem `xorm:"varchar(1000)" json:"samlAttributes"`
IsShared bool `json:"isShared"` IsShared bool `json:"isShared"`
IpRestriction string `json:"ipRestriction"`
ClientId string `xorm:"varchar(100)" json:"clientId"` ClientId string `xorm:"varchar(100)" json:"clientId"`
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"` ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
@ -108,6 +109,7 @@ type Application struct {
SigninUrl string `xorm:"varchar(200)" json:"signinUrl"` SigninUrl string `xorm:"varchar(200)" json:"signinUrl"`
ForgetUrl string `xorm:"varchar(200)" json:"forgetUrl"` ForgetUrl string `xorm:"varchar(200)" json:"forgetUrl"`
AffiliationUrl string `xorm:"varchar(100)" json:"affiliationUrl"` AffiliationUrl string `xorm:"varchar(100)" json:"affiliationUrl"`
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
TermsOfUse string `xorm:"varchar(100)" json:"termsOfUse"` TermsOfUse string `xorm:"varchar(100)" json:"termsOfUse"`
SignupHtml string `xorm:"mediumtext" json:"signupHtml"` SignupHtml string `xorm:"mediumtext" json:"signupHtml"`
SigninHtml string `xorm:"mediumtext" json:"signinHtml"` SigninHtml string `xorm:"mediumtext" json:"signinHtml"`
@ -721,8 +723,15 @@ func (application *Application) GetId() string {
} }
func (application *Application) IsRedirectUriValid(redirectUri string) bool { func (application *Application) IsRedirectUriValid(redirectUri string) bool {
redirectUris := append([]string{"http://localhost:", "https://localhost:", "http://127.0.0.1:", "http://casdoor-app", ".chromiumapp.org"}, application.RedirectUris...) isValid, err := util.IsValidOrigin(redirectUri)
for _, targetUri := range redirectUris { if err != nil {
panic(err)
}
if isValid {
return true
}
for _, targetUri := range application.RedirectUris {
targetUriRegex := regexp.MustCompile(targetUri) targetUriRegex := regexp.MustCompile(targetUri)
if targetUriRegex.MatchString(redirectUri) || strings.Contains(redirectUri, targetUri) { if targetUriRegex.MatchString(redirectUri) || strings.Contains(redirectUri, targetUri) {
return true return true

View File

@ -381,7 +381,13 @@ func CheckUserPassword(organization string, username string, password string, la
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = checkPasswordExpired(user, lang)
if err != nil {
return nil, err
}
} }
return user, nil return user, nil
} }
@ -520,11 +526,46 @@ func CheckUsername(username string, lang string) string {
return "" return ""
} }
func CheckUsernameWithEmail(username string, lang string) string {
if username == "" {
return i18n.Translate(lang, "check:Empty username.")
} else if len(username) > 39 {
return i18n.Translate(lang, "check:Username is too long (maximum is 39 characters).")
}
// https://stackoverflow.com/questions/58726546/github-username-convention-using-regex
if !util.ReUserNameWithEmail.MatchString(username) {
return i18n.Translate(lang, "check:Username supports email format. Also The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline. Also pay attention to the email format.")
}
return ""
}
func CheckUpdateUser(oldUser, user *User, lang string) string { func CheckUpdateUser(oldUser, user *User, lang string) string {
if oldUser.Name != user.Name { if oldUser.Name != user.Name {
if msg := CheckUsername(user.Name, lang); msg != "" { organizationName := oldUser.Owner
return msg if organizationName == "" {
organizationName = user.Owner
} }
organization, err := getOrganization("admin", organizationName)
if err != nil {
return err.Error()
}
if organization == nil {
return fmt.Sprintf(i18n.Translate(lang, "auth:The organization: %s does not exist"), organizationName)
}
if organization.UseEmailAsUsername {
if msg := CheckUsernameWithEmail(user.Name, lang); msg != "" {
return msg
}
} else {
if msg := CheckUsername(user.Name, lang); msg != "" {
return msg
}
}
if HasUserByField(user.Owner, "name", user.Name) { if HasUserByField(user.Owner, "name", user.Name) {
return i18n.Translate(lang, "check:Username already exists") return i18n.Translate(lang, "check:Username already exists")
} }
@ -539,6 +580,11 @@ func CheckUpdateUser(oldUser, user *User, lang string) string {
return i18n.Translate(lang, "check:Phone already exists") return i18n.Translate(lang, "check:Phone already exists")
} }
} }
if oldUser.IpWhitelist != user.IpWhitelist {
if err := CheckIpWhitelist(user.IpWhitelist, lang); err != nil {
return err.Error()
}
}
return "" return ""
} }

104
object/check_ip.go Normal file
View File

@ -0,0 +1,104 @@
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"net"
"strings"
"github.com/casdoor/casdoor/i18n"
)
func CheckEntryIp(clientIp string, user *User, application *Application, organization *Organization, lang string) error {
entryIp := net.ParseIP(clientIp)
if entryIp == nil {
return fmt.Errorf(i18n.Translate(lang, "check:Failed to parse client IP: %s"), clientIp)
} else if entryIp.IsLoopback() {
return nil
}
var err error
if user != nil {
err = isEntryIpAllowd(user.IpWhitelist, entryIp, lang)
if err != nil {
return fmt.Errorf(err.Error() + user.Name)
}
}
if application != nil {
err = isEntryIpAllowd(application.IpWhitelist, entryIp, lang)
if err != nil {
application.IpRestriction = err.Error() + application.Name
return fmt.Errorf(err.Error() + application.Name)
} else {
application.IpRestriction = ""
}
if organization == nil && application.OrganizationObj != nil {
organization = application.OrganizationObj
}
}
if organization != nil {
err = isEntryIpAllowd(organization.IpWhitelist, entryIp, lang)
if err != nil {
organization.IpRestriction = err.Error() + organization.Name
return fmt.Errorf(err.Error() + organization.Name)
} else {
organization.IpRestriction = ""
}
}
return nil
}
func isEntryIpAllowd(ipWhitelistStr string, entryIp net.IP, lang string) error {
if ipWhitelistStr == "" {
return nil
}
ipWhitelist := strings.Split(ipWhitelistStr, ",")
for _, ip := range ipWhitelist {
_, ipNet, err := net.ParseCIDR(ip)
if err != nil {
return err
}
if ipNet == nil {
return fmt.Errorf(i18n.Translate(lang, "check:CIDR for IP: %s should not be empty"), entryIp.String())
}
if ipNet.Contains(entryIp) {
return nil
}
}
return fmt.Errorf(i18n.Translate(lang, "check:Your IP address: %s has been banned according to the configuration of: "), entryIp.String())
}
func CheckIpWhitelist(ipWhitelistStr string, lang string) error {
if ipWhitelistStr == "" {
return nil
}
ipWhiteList := strings.Split(ipWhitelistStr, ",")
for _, ip := range ipWhiteList {
if _, _, err := net.ParseCIDR(ip); err != nil {
return fmt.Errorf(i18n.Translate(lang, "check:%s does not meet the CIDR format requirements: %s"), ip, err.Error())
}
}
return nil
}

View File

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

View File

@ -48,12 +48,16 @@ type InitData struct {
Transactions []*Transaction `json:"transactions"` Transactions []*Transaction `json:"transactions"`
} }
var initDataNewOnly bool
func InitFromFile() { func InitFromFile() {
initDataFile := conf.GetConfigString("initDataFile") initDataFile := conf.GetConfigString("initDataFile")
if initDataFile == "" { if initDataFile == "" {
return return
} }
initDataNewOnly = conf.GetConfigBool("initDataNewOnly")
initData, err := readInitDataFromFile(initDataFile) initData, err := readInitDataFromFile(initDataFile)
if err != nil { if err != nil {
panic(err) panic(err)
@ -269,6 +273,9 @@ func initDefinedOrganization(organization *Organization) {
} }
if existed != nil { if existed != nil {
if initDataNewOnly {
return
}
affected, err := deleteOrganization(organization) affected, err := deleteOrganization(organization)
if err != nil { if err != nil {
panic(err) panic(err)
@ -295,6 +302,9 @@ func initDefinedApplication(application *Application) {
} }
if existed != nil { if existed != nil {
if initDataNewOnly {
return
}
affected, err := deleteApplication(application) affected, err := deleteApplication(application)
if err != nil { if err != nil {
panic(err) panic(err)
@ -316,6 +326,9 @@ func initDefinedUser(user *User) {
panic(err) panic(err)
} }
if existed != nil { if existed != nil {
if initDataNewOnly {
return
}
affected, err := deleteUser(user) affected, err := deleteUser(user)
if err != nil { if err != nil {
panic(err) panic(err)
@ -342,6 +355,9 @@ func initDefinedCert(cert *Cert) {
} }
if existed != nil { if existed != nil {
if initDataNewOnly {
return
}
affected, err := DeleteCert(cert) affected, err := DeleteCert(cert)
if err != nil { if err != nil {
panic(err) panic(err)
@ -364,6 +380,9 @@ func initDefinedLdap(ldap *Ldap) {
} }
if existed != nil { if existed != nil {
if initDataNewOnly {
return
}
affected, err := DeleteLdap(ldap) affected, err := DeleteLdap(ldap)
if err != nil { if err != nil {
panic(err) panic(err)
@ -385,6 +404,9 @@ func initDefinedProvider(provider *Provider) {
} }
if existed != nil { if existed != nil {
if initDataNewOnly {
return
}
affected, err := DeleteProvider(provider) affected, err := DeleteProvider(provider)
if err != nil { if err != nil {
panic(err) panic(err)
@ -406,6 +428,9 @@ func initDefinedModel(model *Model) {
} }
if existed != nil { if existed != nil {
if initDataNewOnly {
return
}
affected, err := DeleteModel(model) affected, err := DeleteModel(model)
if err != nil { if err != nil {
panic(err) panic(err)
@ -428,6 +453,9 @@ func initDefinedPermission(permission *Permission) {
} }
if existed != nil { if existed != nil {
if initDataNewOnly {
return
}
affected, err := deletePermission(permission) affected, err := deletePermission(permission)
if err != nil { if err != nil {
panic(err) panic(err)
@ -450,6 +478,9 @@ func initDefinedPayment(payment *Payment) {
} }
if existed != nil { if existed != nil {
if initDataNewOnly {
return
}
affected, err := DeletePayment(payment) affected, err := DeletePayment(payment)
if err != nil { if err != nil {
panic(err) panic(err)
@ -472,6 +503,9 @@ func initDefinedProduct(product *Product) {
} }
if existed != nil { if existed != nil {
if initDataNewOnly {
return
}
affected, err := DeleteProduct(product) affected, err := DeleteProduct(product)
if err != nil { if err != nil {
panic(err) panic(err)
@ -494,6 +528,9 @@ func initDefinedResource(resource *Resource) {
} }
if existed != nil { if existed != nil {
if initDataNewOnly {
return
}
affected, err := DeleteResource(resource) affected, err := DeleteResource(resource)
if err != nil { if err != nil {
panic(err) panic(err)
@ -516,6 +553,9 @@ func initDefinedRole(role *Role) {
} }
if existed != nil { if existed != nil {
if initDataNewOnly {
return
}
affected, err := deleteRole(role) affected, err := deleteRole(role)
if err != nil { if err != nil {
panic(err) panic(err)
@ -538,6 +578,9 @@ func initDefinedSyncer(syncer *Syncer) {
} }
if existed != nil { if existed != nil {
if initDataNewOnly {
return
}
affected, err := DeleteSyncer(syncer) affected, err := DeleteSyncer(syncer)
if err != nil { if err != nil {
panic(err) panic(err)
@ -560,6 +603,9 @@ func initDefinedToken(token *Token) {
} }
if existed != nil { if existed != nil {
if initDataNewOnly {
return
}
affected, err := DeleteToken(token) affected, err := DeleteToken(token)
if err != nil { if err != nil {
panic(err) panic(err)
@ -582,6 +628,9 @@ func initDefinedWebhook(webhook *Webhook) {
} }
if existed != nil { if existed != nil {
if initDataNewOnly {
return
}
affected, err := DeleteWebhook(webhook) affected, err := DeleteWebhook(webhook)
if err != nil { if err != nil {
panic(err) panic(err)
@ -603,6 +652,9 @@ func initDefinedGroup(group *Group) {
panic(err) panic(err)
} }
if existed != nil { if existed != nil {
if initDataNewOnly {
return
}
affected, err := deleteGroup(group) affected, err := deleteGroup(group)
if err != nil { if err != nil {
panic(err) panic(err)
@ -624,6 +676,9 @@ func initDefinedAdapter(adapter *Adapter) {
panic(err) panic(err)
} }
if existed != nil { if existed != nil {
if initDataNewOnly {
return
}
affected, err := DeleteAdapter(adapter) affected, err := DeleteAdapter(adapter)
if err != nil { if err != nil {
panic(err) panic(err)
@ -645,6 +700,9 @@ func initDefinedEnforcer(enforcer *Enforcer) {
panic(err) panic(err)
} }
if existed != nil { if existed != nil {
if initDataNewOnly {
return
}
affected, err := DeleteEnforcer(enforcer) affected, err := DeleteEnforcer(enforcer)
if err != nil { if err != nil {
panic(err) panic(err)
@ -666,6 +724,9 @@ func initDefinedPlan(plan *Plan) {
panic(err) panic(err)
} }
if existed != nil { if existed != nil {
if initDataNewOnly {
return
}
affected, err := DeletePlan(plan) affected, err := DeletePlan(plan)
if err != nil { if err != nil {
panic(err) panic(err)
@ -687,6 +748,9 @@ func initDefinedPricing(pricing *Pricing) {
panic(err) panic(err)
} }
if existed != nil { if existed != nil {
if initDataNewOnly {
return
}
affected, err := DeletePricing(pricing) affected, err := DeletePricing(pricing)
if err != nil { if err != nil {
panic(err) panic(err)
@ -708,6 +772,9 @@ func initDefinedInvitation(invitation *Invitation) {
panic(err) panic(err)
} }
if existed != nil { if existed != nil {
if initDataNewOnly {
return
}
affected, err := DeleteInvitation(invitation) affected, err := DeleteInvitation(invitation)
if err != nil { if err != nil {
panic(err) panic(err)
@ -743,6 +810,9 @@ func initDefinedSubscription(subscription *Subscription) {
panic(err) panic(err)
} }
if existed != nil { if existed != nil {
if initDataNewOnly {
return
}
affected, err := DeleteSubscription(subscription) affected, err := DeleteSubscription(subscription)
if err != nil { if err != nil {
panic(err) panic(err)
@ -764,6 +834,9 @@ func initDefinedTransaction(transaction *Transaction) {
panic(err) panic(err)
} }
if existed != nil { if existed != nil {
if initDataNewOnly {
return
}
affected, err := DeleteTransaction(transaction) affected, err := DeleteTransaction(transaction)
if err != nil { if err != nil {
panic(err) panic(err)

View File

@ -62,6 +62,7 @@ type Organization struct {
PasswordOptions []string `xorm:"varchar(100)" json:"passwordOptions"` PasswordOptions []string `xorm:"varchar(100)" json:"passwordOptions"`
PasswordObfuscatorType string `xorm:"varchar(100)" json:"passwordObfuscatorType"` PasswordObfuscatorType string `xorm:"varchar(100)" json:"passwordObfuscatorType"`
PasswordObfuscatorKey string `xorm:"varchar(100)" json:"passwordObfuscatorKey"` PasswordObfuscatorKey string `xorm:"varchar(100)" json:"passwordObfuscatorKey"`
PasswordExpireDays int `json:"passwordExpireDays"`
CountryCodes []string `xorm:"varchar(200)" json:"countryCodes"` CountryCodes []string `xorm:"varchar(200)" json:"countryCodes"`
DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"` DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"`
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"` DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
@ -71,11 +72,13 @@ type Organization struct {
MasterPassword string `xorm:"varchar(100)" json:"masterPassword"` MasterPassword string `xorm:"varchar(100)" json:"masterPassword"`
DefaultPassword string `xorm:"varchar(100)" json:"defaultPassword"` DefaultPassword string `xorm:"varchar(100)" json:"defaultPassword"`
MasterVerificationCode string `xorm:"varchar(100)" json:"masterVerificationCode"` MasterVerificationCode string `xorm:"varchar(100)" json:"masterVerificationCode"`
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
InitScore int `json:"initScore"` InitScore int `json:"initScore"`
EnableSoftDeletion bool `json:"enableSoftDeletion"` EnableSoftDeletion bool `json:"enableSoftDeletion"`
IsProfilePublic bool `json:"isProfilePublic"` IsProfilePublic bool `json:"isProfilePublic"`
UseEmailAsUsername bool `json:"useEmailAsUsername"` UseEmailAsUsername bool `json:"useEmailAsUsername"`
EnableTour bool `json:"enableTour"` EnableTour bool `json:"enableTour"`
IpRestriction string `json:"ipRestriction"`
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"` MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"` AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"`

View File

@ -364,7 +364,7 @@ func GetAllActions(userId string) ([]string, error) {
res := []string{} res := []string{}
for _, enforcer := range enforcers { for _, enforcer := range enforcers {
items := enforcer.GetAllObjects() items := enforcer.GetAllActions()
res = append(res, items...) res = append(res, items...)
} }
return res, nil return res, nil

View File

@ -50,7 +50,7 @@ func maskPassword(recordString string) string {
} }
func NewRecord(ctx *context.Context) (*casvisorsdk.Record, error) { func NewRecord(ctx *context.Context) (*casvisorsdk.Record, error) {
ip := strings.Replace(util.GetIPFromRequest(ctx.Request), ": ", "", -1) clientIp := strings.Replace(util.GetClientIpFromRequest(ctx.Request), ": ", "", -1)
action := strings.Replace(ctx.Request.URL.Path, "/api/", "", -1) action := strings.Replace(ctx.Request.URL.Path, "/api/", "", -1)
requestUri := util.FilterQuery(ctx.Request.RequestURI, []string{"accessToken"}) requestUri := util.FilterQuery(ctx.Request.RequestURI, []string{"accessToken"})
if len(requestUri) > 1000 { if len(requestUri) > 1000 {
@ -83,7 +83,7 @@ func NewRecord(ctx *context.Context) (*casvisorsdk.Record, error) {
record := casvisorsdk.Record{ record := casvisorsdk.Record{
Name: util.GenerateId(), Name: util.GenerateId(),
CreatedTime: util.GetCurrentTime(), CreatedTime: util.GetCurrentTime(),
ClientIp: ip, ClientIp: clientIp,
User: "", User: "",
Method: ctx.Request.Method, Method: ctx.Request.Method,
RequestUri: requestUri, RequestUri: requestUri,

View File

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

View File

@ -102,14 +102,6 @@ func GetTokenByAccessToken(accessToken string) (*Token, error) {
return nil, err return nil, err
} }
if !existed {
token = Token{AccessToken: accessToken}
existed, err = ormer.Engine.Get(&token)
if err != nil {
return nil, err
}
}
if !existed { if !existed {
return nil, nil return nil, nil
} }
@ -123,14 +115,6 @@ func GetTokenByRefreshToken(refreshToken string) (*Token, error) {
return nil, err return nil, err
} }
if !existed {
token = Token{RefreshToken: refreshToken}
existed, err = ormer.Engine.Get(&token)
if err != nil {
return nil, err
}
}
if !existed { if !existed {
return nil, nil return nil, nil
} }

View File

@ -332,6 +332,9 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
if err != nil { if err != nil {
return nil, err return nil, err
} }
if user == nil {
return "", fmt.Errorf("The user: %s doesn't exist", util.GetId(application.Organization, token.User))
}
if user.IsForbidden { if user.IsForbidden {
return &TokenError{ return &TokenError{

View File

@ -200,12 +200,14 @@ type User struct {
Permissions []*Permission `json:"permissions"` Permissions []*Permission `json:"permissions"`
Groups []string `xorm:"groups varchar(1000)" json:"groups"` Groups []string `xorm:"groups varchar(1000)" json:"groups"`
LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"` LastChangePasswordTime string `xorm:"varchar(100)" json:"lastChangePasswordTime"`
SigninWrongTimes int `json:"signinWrongTimes"` LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"`
SigninWrongTimes int `json:"signinWrongTimes"`
ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"` ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"`
MfaAccounts []MfaAccount `xorm:"mfaAccounts blob" json:"mfaAccounts"` MfaAccounts []MfaAccount `xorm:"mfaAccounts blob" json:"mfaAccounts"`
NeedUpdatePassword bool `json:"needUpdatePassword"` NeedUpdatePassword bool `json:"needUpdatePassword"`
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
} }
type Userinfo struct { type Userinfo struct {
@ -689,14 +691,14 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
"owner", "display_name", "avatar", "first_name", "last_name", "owner", "display_name", "avatar", "first_name", "last_name",
"location", "address", "country_code", "region", "language", "affiliation", "title", "id_card_type", "id_card", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application", "location", "address", "country_code", "region", "language", "affiliation", "title", "id_card_type", "id_card", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application",
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts", "face_ids", "mfaAccounts", "is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts", "face_ids", "mfaAccounts",
"signin_wrong_times", "last_signin_wrong_time", "groups", "access_key", "access_secret", "mfa_phone_enabled", "mfa_email_enabled", "signin_wrong_times", "last_change_password_time", "last_signin_wrong_time", "groups", "access_key", "access_secret", "mfa_phone_enabled", "mfa_email_enabled",
"github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs", "github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs",
"baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "line", "amazon", "baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "line", "amazon",
"auth0", "battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox", "auth0", "battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox",
"eveonline", "fitbit", "gitea", "heroku", "influxcloud", "instagram", "intercom", "kakao", "lastfm", "mailru", "meetup", "eveonline", "fitbit", "gitea", "heroku", "influxcloud", "instagram", "intercom", "kakao", "lastfm", "mailru", "meetup",
"microsoftonline", "naver", "nextcloud", "onedrive", "oura", "patreon", "paypal", "salesforce", "shopify", "soundcloud", "microsoftonline", "naver", "nextcloud", "onedrive", "oura", "patreon", "paypal", "salesforce", "shopify", "soundcloud",
"spotify", "strava", "stripe", "type", "tiktok", "tumblr", "twitch", "twitter", "typetalk", "uber", "vk", "wepay", "xero", "yahoo", "spotify", "strava", "stripe", "type", "tiktok", "tumblr", "twitch", "twitter", "typetalk", "uber", "vk", "wepay", "xero", "yahoo",
"yammer", "yandex", "zoom", "custom", "need_update_password", "yammer", "yandex", "zoom", "custom", "need_update_password", "ip_whitelist",
} }
} }
if isAdmin { if isAdmin {
@ -815,6 +817,10 @@ func AddUser(user *User) (bool, error) {
user.UpdateUserPassword(organization) user.UpdateUserPassword(organization)
} }
if user.CreatedTime == "" {
user.CreatedTime = util.GetCurrentTime()
}
err = user.UpdateUserHash() err = user.UpdateUserHash()
if err != nil { if err != nil {
return false, err return false, err

View File

@ -557,6 +557,14 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
itemsChanged = append(itemsChanged, item) itemsChanged = append(itemsChanged, item)
} }
} }
if oldUser.IpWhitelist != newUser.IpWhitelist {
item := GetAccountItemByName("IP whitelist", organization)
if item == nil {
newUser.IpWhitelist = oldUser.IpWhitelist
} else {
itemsChanged = append(itemsChanged, item)
}
}
if oldUser.Balance != newUser.Balance { if oldUser.Balance != newUser.Balance {
item := GetAccountItemByName("Balance", organization) item := GetAccountItemByName("Balance", organization)

View File

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

View File

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

View File

@ -23,50 +23,50 @@ import (
"github.com/beego/beego/logs" "github.com/beego/beego/logs"
) )
func GetIPInfo(clientIP string) string { func getIpInfo(clientIp string) string {
if clientIP == "" { if clientIp == "" {
return "" return ""
} }
ips := strings.Split(clientIP, ",") ips := strings.Split(clientIp, ",")
res := "" res := strings.TrimSpace(ips[0])
for i := range ips { //res := ""
ip := strings.TrimSpace(ips[i]) //for i := range ips {
// desc := GetDescFromIP(ip) // ip := strings.TrimSpace(ips[i])
ipstr := fmt.Sprintf("%s: %s", ip, "") // ipstr := fmt.Sprintf("%s: %s", ip, "")
if i != len(ips)-1 { // if i != len(ips)-1 {
res += ipstr + " -> " // res += ipstr + " -> "
} else { // } else {
res += ipstr // res += ipstr
} // }
} //}
return res return res
} }
func GetIPFromRequest(req *http.Request) string { func GetClientIpFromRequest(req *http.Request) string {
clientIP := req.Header.Get("x-forwarded-for") clientIp := req.Header.Get("x-forwarded-for")
if clientIP == "" { if clientIp == "" {
ipPort := strings.Split(req.RemoteAddr, ":") ipPort := strings.Split(req.RemoteAddr, ":")
if len(ipPort) >= 1 && len(ipPort) <= 2 { if len(ipPort) >= 1 && len(ipPort) <= 2 {
clientIP = ipPort[0] clientIp = ipPort[0]
} else if len(ipPort) > 2 { } else if len(ipPort) > 2 {
idx := strings.LastIndex(req.RemoteAddr, ":") idx := strings.LastIndex(req.RemoteAddr, ":")
clientIP = req.RemoteAddr[0:idx] clientIp = req.RemoteAddr[0:idx]
clientIP = strings.TrimLeft(clientIP, "[") clientIp = strings.TrimLeft(clientIp, "[")
clientIP = strings.TrimRight(clientIP, "]") clientIp = strings.TrimRight(clientIp, "]")
} }
} }
return GetIPInfo(clientIP) return getIpInfo(clientIp)
} }
func LogInfo(ctx *context.Context, f string, v ...interface{}) { func LogInfo(ctx *context.Context, f string, v ...interface{}) {
ipString := fmt.Sprintf("(%s) ", GetIPFromRequest(ctx.Request)) ipString := fmt.Sprintf("(%s) ", GetClientIpFromRequest(ctx.Request))
logs.Info(ipString+f, v...) logs.Info(ipString+f, v...)
} }
func LogWarning(ctx *context.Context, f string, v ...interface{}) { func LogWarning(ctx *context.Context, f string, v ...interface{}) {
ipString := fmt.Sprintf("(%s) ", GetIPFromRequest(ctx.Request)) ipString := fmt.Sprintf("(%s) ", GetClientIpFromRequest(ctx.Request))
logs.Warning(ipString+f, v...) logs.Warning(ipString+f, v...)
} }

View File

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

View File

@ -46,12 +46,18 @@ require("codemirror/mode/css/css");
const {Option} = Select; const {Option} = Select;
const template = `<style> const template = `<style>
.login-panel{ .login-panel {
padding: 40px 70px 0 70px; padding: 40px 70px 0 70px;
border-radius: 10px; border-radius: 10px;
background-color: #ffffff; background-color: #ffffff;
box-shadow: 0 0 30px 20px rgba(0, 0, 0, 0.20); box-shadow: 0 0 30px 20px rgba(0, 0, 0, 0.20);
} }
.login-panel-dark {
padding: 40px 70px 0 70px;
border-radius: 10px;
background-color: #333333;
box-shadow: 0 0 30px 20px rgba(255, 255, 255, 0.20);
}
</style>`; </style>`;
const previewGrid = Setting.isMobile() ? 22 : 11; const previewGrid = Setting.isMobile() ? 22 : 11;
@ -592,6 +598,16 @@ class ApplicationEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:IP whitelist"), i18next.t("general:IP whitelist - Tooltip"))} :
</Col>
<Col span={22} >
<Input placeholder = {this.state.application.organizationObj?.ipWhitelist} value={this.state.application.ipWhitelist} onChange={e => {
this.updateApplicationField("ipWhitelist", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("signup:Terms of Use"), i18next.t("signup:Terms of Use - Tooltip"))} : {Setting.getLabel(i18next.t("signup:Terms of Use"), i18next.t("signup:Terms of Use - Tooltip"))} :
@ -749,7 +765,7 @@ class ApplicationEditPage extends React.Component {
/> />
<br /> <br />
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => { <Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
copy(`${window.location.origin}/api/saml/metadata?application=admin/${encodeURIComponent(this.state.applicationName)}&post=${this.state.application.enableSamlPostBinding}`); copy(`${window.location.origin}/api/saml/metadata?application=admin/${encodeURIComponent(this.state.applicationName)}&enablePostBinding=${this.state.application.enableSamlPostBinding}`);
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully")); Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}} }}
> >

View File

@ -34,6 +34,7 @@ import PaymentResultPage from "./PaymentResultPage";
import QrCodePage from "./QrCodePage"; import QrCodePage from "./QrCodePage";
import CaptchaPage from "./CaptchaPage"; import CaptchaPage from "./CaptchaPage";
import CustomHead from "./basic/CustomHead"; import CustomHead from "./basic/CustomHead";
import * as Util from "./auth/Util";
class EntryPage extends React.Component { class EntryPage extends React.Component {
constructor(props) { constructor(props) {
@ -94,6 +95,14 @@ class EntryPage extends React.Component {
}); });
}; };
if (this.state.application?.ipRestriction) {
return Util.renderMessageLarge(this, this.state.application.ipRestriction);
}
if (this.state.application?.organizationObj?.ipRestriction) {
return Util.renderMessageLarge(this, this.state.application.organizationObj.ipRestriction);
}
const isDarkMode = this.props.themeAlgorithm.includes("dark"); const isDarkMode = this.props.themeAlgorithm.includes("dark");
return ( return (

View File

@ -198,11 +198,11 @@ function ManagementPage(props) {
</div> </div>
</Tooltip> </Tooltip>
<OpenTour /> <OpenTour />
{Setting.isAdminUser(props.account) && !Setting.isMobile() && (props.uri.indexOf("/trees") === -1) && {Setting.isAdminUser(props.account) && (props.uri.indexOf("/trees") === -1) &&
<OrganizationSelect <OrganizationSelect
initValue={Setting.getOrganization()} initValue={Setting.getOrganization()}
withAll={true} withAll={true}
style={{marginRight: "20px", width: "180px", display: "flex"}} style={{marginRight: "20px", width: "180px", display: !Setting.isMobile() ? "flex" : "none"}}
onChange={(value) => { onChange={(value) => {
Setting.setOrganization(value); Setting.setOrganization(value);
}} }}

View File

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

View File

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

View File

@ -908,7 +908,7 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
)} )}
{["Custom HTTP SMS", "Qiniu Cloud Kodo", "Synology", "Casdoor"].includes(this.state.provider.type) ? null : ( {["Custom HTTP SMS", "Synology", "Casdoor"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}> <Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} : {Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :

View File

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

View File

@ -1557,3 +1557,7 @@ export function getCurrencyText(product) {
return "(Unknown currency)"; return "(Unknown currency)";
} }
} }
export function isDarkTheme(themeAlgorithm) {
return themeAlgorithm && themeAlgorithm.includes("dark");
}

View File

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

View File

@ -227,7 +227,26 @@ class LoginPage extends React.Component {
return "password"; return "password";
} }
getPlaceholder() { getCurrentLoginMethod() {
if (this.state.loginMethod === "password") {
return "Password";
} else if (this.state.loginMethod?.includes("verificationCode")) {
return "Verification code";
} else if (this.state.loginMethod === "webAuthn") {
return "WebAuthn";
} else if (this.state.loginMethod === "ldap") {
return "LDAP";
} else if (this.state.loginMethod === "faceId") {
return "Face ID";
} else {
return "Password";
}
}
getPlaceholder(defaultPlaceholder = null) {
if (defaultPlaceholder) {
return defaultPlaceholder;
}
switch (this.state.loginMethod) { switch (this.state.loginMethod) {
case "verificationCode": return i18next.t("login:Email or phone"); case "verificationCode": return i18next.t("login:Email or phone");
case "verificationCodeEmail": return i18next.t("login:Email"); case "verificationCodeEmail": return i18next.t("login:Email");
@ -262,17 +281,7 @@ class LoginPage extends React.Component {
values["organization"] = this.getApplicationObj().organization; values["organization"] = this.getApplicationObj().organization;
} }
if (this.state.loginMethod === "password") { values["signinMethod"] = this.getCurrentLoginMethod();
values["signinMethod"] = "Password";
} else if (this.state.loginMethod?.includes("verificationCode")) {
values["signinMethod"] = "Verification code";
} else if (this.state.loginMethod === "webAuthn") {
values["signinMethod"] = "WebAuthn";
} else if (this.state.loginMethod === "ldap") {
values["signinMethod"] = "LDAP";
} else if (this.state.loginMethod === "faceId") {
values["signinMethod"] = "Face ID";
}
const oAuthParams = Util.getOAuthGetParameters(); const oAuthParams = Util.getOAuthGetParameters();
values["type"] = oAuthParams?.responseType ?? this.state.type; values["type"] = oAuthParams?.responseType ?? this.state.type;
@ -409,6 +418,7 @@ class LoginPage extends React.Component {
if (this.state.type === "cas") { if (this.state.type === "cas") {
// CAS // CAS
const casParams = Util.getCasParameters(); const casParams = Util.getCasParameters();
values["signinMethod"] = this.getCurrentLoginMethod();
values["type"] = this.state.type; values["type"] = this.state.type;
AuthBackend.loginCas(values, casParams).then((res) => { AuthBackend.loginCas(values, casParams).then((res) => {
const loginHandler = (res) => { const loginHandler = (res) => {
@ -437,8 +447,8 @@ class LoginPage extends React.Component {
formValues={values} formValues={values}
authParams={casParams} authParams={casParams}
application={this.getApplicationObj()} application={this.getApplicationObj()}
onFail={() => { onFail={(errorMessage) => {
Setting.showMessage("error", i18next.t("mfa:Verification failed")); Setting.showMessage("error", errorMessage);
}} }}
onSuccess={(res) => loginHandler(res)} onSuccess={(res) => loginHandler(res)}
/>); />);
@ -478,6 +488,10 @@ class LoginPage extends React.Component {
const accessToken = res.data; const accessToken = res.data;
Setting.goToLink(`${oAuthParams.redirectUri}#${amendatoryResponseType}=${accessToken}&state=${oAuthParams.state}&token_type=bearer`); Setting.goToLink(`${oAuthParams.redirectUri}#${amendatoryResponseType}=${accessToken}&state=${oAuthParams.state}&token_type=bearer`);
} else if (responseType === "saml") { } else if (responseType === "saml") {
if (res.data === RequiredMfa) {
this.props.onLoginSuccess(window.location.href);
return;
}
if (res.data2.needUpdatePassword) { if (res.data2.needUpdatePassword) {
sessionStorage.setItem("signinUrl", window.location.href); sessionStorage.setItem("signinUrl", window.location.href);
Setting.goToLink(this, `/forget/${this.state.applicationName}`); Setting.goToLink(this, `/forget/${this.state.applicationName}`);
@ -506,8 +520,8 @@ class LoginPage extends React.Component {
formValues={values} formValues={values}
authParams={oAuthParams} authParams={oAuthParams}
application={this.getApplicationObj()} application={this.getApplicationObj()}
onFail={() => { onFail={(errorMessage) => {
Setting.showMessage("error", i18next.t("mfa:Verification failed")); Setting.showMessage("error", errorMessage);
}} }}
onSuccess={(res) => loginHandler(res)} onSuccess={(res) => loginHandler(res)}
/>); />);
@ -672,7 +686,7 @@ class LoginPage extends React.Component {
id="input" id="input"
className="login-username-input" className="login-username-input"
prefix={<UserOutlined className="site-form-item-icon" />} prefix={<UserOutlined className="site-form-item-icon" />}
placeholder={this.getPlaceholder()} placeholder={this.getPlaceholder(signinItem.placeholder)}
onChange={e => { onChange={e => {
this.setState({ this.setState({
username: e.target.value, username: e.target.value,
@ -1086,7 +1100,7 @@ class LoginPage extends React.Component {
className="login-password-input" className="login-password-input"
prefix={<LockOutlined className="site-form-item-icon" />} prefix={<LockOutlined className="site-form-item-icon" />}
type="password" type="password"
placeholder={i18next.t("general:Password")} placeholder={signinItem.placeholder ? signinItem.placeholder : i18next.t("general:Password")}
disabled={this.state.loginMethod === "password" ? !Setting.isPasswordEnabled(application) : !Setting.isLdapEnabled(application)} disabled={this.state.loginMethod === "password" ? !Setting.isPasswordEnabled(application) : !Setting.isLdapEnabled(application)}
/> />
</Form.Item> </Form.Item>
@ -1293,7 +1307,7 @@ class LoginPage extends React.Component {
<div className="login-content" style={{margin: this.props.preview ?? this.parseOffset(application.formOffset)}}> <div className="login-content" style={{margin: this.props.preview ?? this.parseOffset(application.formOffset)}}>
{Setting.inIframe() || Setting.isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCss}} />} {Setting.inIframe() || Setting.isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCss}} />}
{Setting.inIframe() || !Setting.isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCssMobile}} />} {Setting.inIframe() || !Setting.isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCssMobile}} />}
<div className="login-panel"> <div className={Setting.isDarkTheme(this.props.themeAlgorithm) ? "login-panel-dark" : "login-panel"}>
<div className="side-image" style={{display: application.formOffset !== 4 ? "none" : null}}> <div className="side-image" style={{display: application.formOffset !== 4 ? "none" : null}}>
<div dangerouslySetInnerHTML={{__html: application.formSideHtml}} /> <div dangerouslySetInnerHTML={{__html: application.formSideHtml}} />
</div> </div>

View File

@ -37,7 +37,7 @@ class MfaSetupPage extends React.Component {
this.state = { this.state = {
account: props.account, account: props.account,
application: null, application: null,
applicationName: props.account.signupApplication ?? "", applicationName: props.account.signupApplication ?? localStorage.getItem("applicationName") ?? "",
current: location.state?.from !== undefined ? 1 : 0, current: location.state?.from !== undefined ? 1 : 0,
mfaProps: null, mfaProps: null,
mfaType: params.get("mfaType") ?? SmsMfaType, mfaType: params.get("mfaType") ?? SmsMfaType,

View File

@ -14,6 +14,7 @@
import CryptoJS from "crypto-js"; import CryptoJS from "crypto-js";
import i18next from "i18next"; import i18next from "i18next";
import {Buffer} from "buffer";
export function getRandomKeyForObfuscator(obfuscatorType) { export function getRandomKeyForObfuscator(obfuscatorType) {
if (obfuscatorType === "DES") { if (obfuscatorType === "DES") {

View File

@ -842,7 +842,7 @@ class SignupPage extends React.Component {
<div className="login-content" style={{margin: this.props.preview ?? this.parseOffset(application.formOffset)}}> <div className="login-content" style={{margin: this.props.preview ?? this.parseOffset(application.formOffset)}}>
{Setting.inIframe() || Setting.isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCss}} />} {Setting.inIframe() || Setting.isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCss}} />}
{Setting.inIframe() || !Setting.isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCssMobile}} />} {Setting.inIframe() || !Setting.isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCssMobile}} />}
<div className="login-panel" > <div className={Setting.isDarkTheme(this.props.themeAlgorithm) ? "login-panel-dark" : "login-panel"}>
<div className="side-image" style={{display: application.formOffset !== 4 ? "none" : null}}> <div className="side-image" style={{display: application.formOffset !== 4 ? "none" : null}}>
<div dangerouslySetInnerHTML={{__html: application.formSideHtml}} /> <div dangerouslySetInnerHTML={{__html: application.formSideHtml}} />
</div> </div>

View File

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

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import React, {useState} from "react"; import React, {Fragment, useState} from "react";
import i18next from "i18next"; import i18next from "i18next";
import {Button, Input} from "antd"; import {Button, Input} from "antd";
import * as AuthBackend from "../AuthBackend"; import * as AuthBackend from "../AuthBackend";
@ -67,24 +67,32 @@ export function MfaAuthVerifyForm({formValues, authParams, mfaProps, application
if (mfaType !== RecoveryMfaType) { if (mfaType !== RecoveryMfaType) {
return ( return (
<div style={{width: 300, height: 350}}> <div style={{width: 320, height: 350}}>
<div style={{marginBottom: 24, textAlign: "center", fontSize: "24px"}}> <div style={{marginBottom: 24, textAlign: "center", fontSize: "24px"}}>
{i18next.t("mfa:Multi-factor authentication")} {i18next.t("mfa:Multi-factor authentication")}
</div> </div>
<div style={{marginBottom: 24}}>
{i18next.t("mfa:You have enabled multi-factor authentication, please enter the authentication code")}
</div>
{mfaType === SmsMfaType || mfaType === EmailMfaType ? ( {mfaType === SmsMfaType || mfaType === EmailMfaType ? (
<MfaVerifySmsForm <Fragment>
mfaProps={mfaProps} <div style={{marginBottom: 24}}>
method={mfaAuth} {i18next.t("mfa:You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue")}
onFinish={verify} </div>
application={application} <MfaVerifySmsForm
/>) : ( mfaProps={mfaProps}
<MfaVerifyTotpForm method={mfaAuth}
mfaProps={mfaProps} onFinish={verify}
onFinish={verify} application={application}
/> />
</Fragment>
) : (
<Fragment>
<div style={{marginBottom: 24}}>
{i18next.t("mfa:You have enabled Multi-Factor Authentication, please enter the TOTP code")}
</div>
<MfaVerifyTotpForm
mfaProps={mfaProps}
onFinish={verify}
/>
</Fragment>
)} )}
<span style={{float: "right"}}> <span style={{float: "right"}}>
{i18next.t("mfa:Have problems?")} {i18next.t("mfa:Have problems?")}

View File

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

View File

@ -562,7 +562,8 @@
"Verification failed": "Verification failed", "Verification failed": "Verification failed",
"Verify Code": "Verify Code", "Verify Code": "Verify Code",
"Verify Password": "Verify Password", "Verify Password": "Verify Password",
"You have enabled multi-factor authentication, please enter the authentication code": "You have enabled multi-factor authentication, please enter the authentication code", "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
"You have enabled Multi-Factor Authentication, please enter the TOTP code": "You have enabled Multi-Factor Authentication, please enter the TOTP code",
"Your email is": "Your email is", "Your email is": "Your email is",
"Your phone is": "Your phone is", "Your phone is": "Your phone is",
"preferred": "preferred" "preferred": "preferred"

View File

@ -562,7 +562,8 @@
"Verification failed": "Ověření selhalo", "Verification failed": "Ověření selhalo",
"Verify Code": "Ověřit kód", "Verify Code": "Ověřit kód",
"Verify Password": "Ověřit heslo", "Verify Password": "Ověřit heslo",
"You have enabled multi-factor authentication, please enter the authentication code": "You have enabled multi-factor authentication, please enter the authentication code", "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
"You have enabled Multi-Factor Authentication, please enter the TOTP code": "You have enabled Multi-Factor Authentication, please enter the TOTP code",
"Your email is": "Váš email je", "Your email is": "Váš email je",
"Your phone is": "Váš telefon je", "Your phone is": "Váš telefon je",
"preferred": "preferované" "preferred": "preferované"

View File

@ -562,7 +562,8 @@
"Verification failed": "Verification failed", "Verification failed": "Verification failed",
"Verify Code": "Verify Code", "Verify Code": "Verify Code",
"Verify Password": "Verify Password", "Verify Password": "Verify Password",
"You have enabled multi-factor authentication, please enter the authentication code": "You have enabled multi-factor authentication, please enter the authentication code", "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
"You have enabled Multi-Factor Authentication, please enter the TOTP code": "You have enabled Multi-Factor Authentication, please enter the TOTP code",
"Your email is": "Your email is", "Your email is": "Your email is",
"Your phone is": "Your phone is", "Your phone is": "Your phone is",
"preferred": "preferred" "preferred": "preferred"

View File

@ -562,7 +562,8 @@
"Verification failed": "Verification failed", "Verification failed": "Verification failed",
"Verify Code": "Verify Code", "Verify Code": "Verify Code",
"Verify Password": "Verify Password", "Verify Password": "Verify Password",
"You have enabled multi-factor authentication, please enter the authentication code": "You have enabled multi-factor authentication, please enter the authentication code", "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
"You have enabled Multi-Factor Authentication, please enter the TOTP code": "You have enabled Multi-Factor Authentication, please enter the TOTP code",
"Your email is": "Your email is", "Your email is": "Your email is",
"Your phone is": "Your phone is", "Your phone is": "Your phone is",
"preferred": "preferred" "preferred": "preferred"

View File

@ -562,7 +562,8 @@
"Verification failed": "Verification failed", "Verification failed": "Verification failed",
"Verify Code": "Verify Code", "Verify Code": "Verify Code",
"Verify Password": "Verify Password", "Verify Password": "Verify Password",
"You have enabled multi-factor authentication, please enter the authentication code": "You have enabled multi-factor authentication, please enter the authentication code", "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
"You have enabled Multi-Factor Authentication, please enter the TOTP code": "You have enabled Multi-Factor Authentication, please enter the TOTP code",
"Your email is": "Your email is", "Your email is": "Your email is",
"Your phone is": "Your phone is", "Your phone is": "Your phone is",
"preferred": "preferred" "preferred": "preferred"

File diff suppressed because it is too large Load Diff

View File

@ -562,7 +562,8 @@
"Verification failed": "Verification failed", "Verification failed": "Verification failed",
"Verify Code": "Verify Code", "Verify Code": "Verify Code",
"Verify Password": "Verify Password", "Verify Password": "Verify Password",
"You have enabled multi-factor authentication, please enter the authentication code": "You have enabled multi-factor authentication, please enter the authentication code", "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
"You have enabled Multi-Factor Authentication, please enter the TOTP code": "You have enabled Multi-Factor Authentication, please enter the TOTP code",
"Your email is": "Your email is", "Your email is": "Your email is",
"Your phone is": "Your phone is", "Your phone is": "Your phone is",
"preferred": "preferred" "preferred": "preferred"

View File

@ -562,7 +562,8 @@
"Verification failed": "Échec de la vérification", "Verification failed": "Échec de la vérification",
"Verify Code": "Vérifier le code", "Verify Code": "Vérifier le code",
"Verify Password": "Confirmez le mot de passe", "Verify Password": "Confirmez le mot de passe",
"You have enabled multi-factor authentication, please enter the authentication code": "You have enabled multi-factor authentication, please enter the authentication code", "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
"You have enabled Multi-Factor Authentication, please enter the TOTP code": "You have enabled Multi-Factor Authentication, please enter the TOTP code",
"Your email is": "Votre e-mail est", "Your email is": "Votre e-mail est",
"Your phone is": "Votre téléphone est", "Your phone is": "Votre téléphone est",
"preferred": "préféré" "preferred": "préféré"

View File

@ -562,7 +562,8 @@
"Verification failed": "Verification failed", "Verification failed": "Verification failed",
"Verify Code": "Verify Code", "Verify Code": "Verify Code",
"Verify Password": "Verify Password", "Verify Password": "Verify Password",
"You have enabled multi-factor authentication, please enter the authentication code": "You have enabled multi-factor authentication, please enter the authentication code", "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
"You have enabled Multi-Factor Authentication, please enter the TOTP code": "You have enabled Multi-Factor Authentication, please enter the TOTP code",
"Your email is": "Your email is", "Your email is": "Your email is",
"Your phone is": "Your phone is", "Your phone is": "Your phone is",
"preferred": "preferred" "preferred": "preferred"

View File

@ -562,7 +562,8 @@
"Verification failed": "Verification failed", "Verification failed": "Verification failed",
"Verify Code": "Verify Code", "Verify Code": "Verify Code",
"Verify Password": "Verify Password", "Verify Password": "Verify Password",
"You have enabled multi-factor authentication, please enter the authentication code": "You have enabled multi-factor authentication, please enter the authentication code", "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
"You have enabled Multi-Factor Authentication, please enter the TOTP code": "You have enabled Multi-Factor Authentication, please enter the TOTP code",
"Your email is": "Your email is", "Your email is": "Your email is",
"Your phone is": "Your phone is", "Your phone is": "Your phone is",
"preferred": "preferred" "preferred": "preferred"

View File

@ -562,7 +562,8 @@
"Verification failed": "Verification failed", "Verification failed": "Verification failed",
"Verify Code": "Verify Code", "Verify Code": "Verify Code",
"Verify Password": "Verify Password", "Verify Password": "Verify Password",
"You have enabled multi-factor authentication, please enter the authentication code": "You have enabled multi-factor authentication, please enter the authentication code", "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
"You have enabled Multi-Factor Authentication, please enter the TOTP code": "You have enabled Multi-Factor Authentication, please enter the TOTP code",
"Your email is": "Your email is", "Your email is": "Your email is",
"Your phone is": "Your phone is", "Your phone is": "Your phone is",
"preferred": "preferred" "preferred": "preferred"

View File

@ -562,7 +562,8 @@
"Verification failed": "Verification failed", "Verification failed": "Verification failed",
"Verify Code": "Verify Code", "Verify Code": "Verify Code",
"Verify Password": "Verify Password", "Verify Password": "Verify Password",
"You have enabled multi-factor authentication, please enter the authentication code": "You have enabled multi-factor authentication, please enter the authentication code", "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
"You have enabled Multi-Factor Authentication, please enter the TOTP code": "You have enabled Multi-Factor Authentication, please enter the TOTP code",
"Your email is": "Your email is", "Your email is": "Your email is",
"Your phone is": "Your phone is", "Your phone is": "Your phone is",
"preferred": "preferred" "preferred": "preferred"

View File

@ -562,7 +562,8 @@
"Verification failed": "Verification failed", "Verification failed": "Verification failed",
"Verify Code": "Verify Code", "Verify Code": "Verify Code",
"Verify Password": "Verify Password", "Verify Password": "Verify Password",
"You have enabled multi-factor authentication, please enter the authentication code": "You have enabled multi-factor authentication, please enter the authentication code", "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
"You have enabled Multi-Factor Authentication, please enter the TOTP code": "You have enabled Multi-Factor Authentication, please enter the TOTP code",
"Your email is": "Your email is", "Your email is": "Your email is",
"Your phone is": "Your phone is", "Your phone is": "Your phone is",
"preferred": "preferred" "preferred": "preferred"

View File

@ -562,7 +562,8 @@
"Verification failed": "Verification failed", "Verification failed": "Verification failed",
"Verify Code": "Verify Code", "Verify Code": "Verify Code",
"Verify Password": "Verify Password", "Verify Password": "Verify Password",
"You have enabled multi-factor authentication, please enter the authentication code": "You have enabled multi-factor authentication, please enter the authentication code", "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
"You have enabled Multi-Factor Authentication, please enter the TOTP code": "You have enabled Multi-Factor Authentication, please enter the TOTP code",
"Your email is": "Your email is", "Your email is": "Your email is",
"Your phone is": "Your phone is", "Your phone is": "Your phone is",
"preferred": "preferred" "preferred": "preferred"

View File

@ -562,7 +562,8 @@
"Verification failed": "Verification failed", "Verification failed": "Verification failed",
"Verify Code": "Verify Code", "Verify Code": "Verify Code",
"Verify Password": "Verify Password", "Verify Password": "Verify Password",
"You have enabled multi-factor authentication, please enter the authentication code": "You have enabled multi-factor authentication, please enter the authentication code", "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
"You have enabled Multi-Factor Authentication, please enter the TOTP code": "You have enabled Multi-Factor Authentication, please enter the TOTP code",
"Your email is": "Your email is", "Your email is": "Your email is",
"Your phone is": "Your phone is", "Your phone is": "Your phone is",
"preferred": "preferred" "preferred": "preferred"

View File

@ -562,7 +562,8 @@
"Verification failed": "Verification failed", "Verification failed": "Verification failed",
"Verify Code": "Verify Code", "Verify Code": "Verify Code",
"Verify Password": "Verify Password", "Verify Password": "Verify Password",
"You have enabled multi-factor authentication, please enter the authentication code": "You have enabled multi-factor authentication, please enter the authentication code", "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
"You have enabled Multi-Factor Authentication, please enter the TOTP code": "You have enabled Multi-Factor Authentication, please enter the TOTP code",
"Your email is": "Your email is", "Your email is": "Your email is",
"Your phone is": "Your phone is", "Your phone is": "Your phone is",
"preferred": "preferred" "preferred": "preferred"

View File

@ -562,7 +562,8 @@
"Verification failed": "Verification failed", "Verification failed": "Verification failed",
"Verify Code": "Verify Code", "Verify Code": "Verify Code",
"Verify Password": "Verify Password", "Verify Password": "Verify Password",
"You have enabled multi-factor authentication, please enter the authentication code": "You have enabled multi-factor authentication, please enter the authentication code", "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
"You have enabled Multi-Factor Authentication, please enter the TOTP code": "You have enabled Multi-Factor Authentication, please enter the TOTP code",
"Your email is": "Your email is", "Your email is": "Your email is",
"Your phone is": "Your phone is", "Your phone is": "Your phone is",
"preferred": "preferred" "preferred": "preferred"

View File

@ -562,7 +562,8 @@
"Verification failed": "Verification failed", "Verification failed": "Verification failed",
"Verify Code": "Verify Code", "Verify Code": "Verify Code",
"Verify Password": "Verify Password", "Verify Password": "Verify Password",
"You have enabled multi-factor authentication, please enter the authentication code": "You have enabled multi-factor authentication, please enter the authentication code", "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
"You have enabled Multi-Factor Authentication, please enter the TOTP code": "You have enabled Multi-Factor Authentication, please enter the TOTP code",
"Your email is": "Your email is", "Your email is": "Your email is",
"Your phone is": "Your phone is", "Your phone is": "Your phone is",
"preferred": "preferred" "preferred": "preferred"

View File

@ -562,7 +562,8 @@
"Verification failed": "Проверка не удалась", "Verification failed": "Проверка не удалась",
"Verify Code": "Verify Code", "Verify Code": "Verify Code",
"Verify Password": "Verify Password", "Verify Password": "Verify Password",
"You have enabled multi-factor authentication, please enter the authentication code": "You have enabled multi-factor authentication, please enter the authentication code", "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
"You have enabled Multi-Factor Authentication, please enter the TOTP code": "You have enabled Multi-Factor Authentication, please enter the TOTP code",
"Your email is": "Ваш email", "Your email is": "Ваш email",
"Your phone is": "Ваш телефон", "Your phone is": "Ваш телефон",
"preferred": "preferred" "preferred": "preferred"
@ -971,7 +972,7 @@
"Please input your affiliation!": "Пожалуйста, укажите свою принадлежность!", "Please input your affiliation!": "Пожалуйста, укажите свою принадлежность!",
"Please input your display name!": "Пожалуйста, введите своё отображаемое имя!", "Please input your display name!": "Пожалуйста, введите своё отображаемое имя!",
"Please input your first name!": "Пожалуйста, введите свое имя!", "Please input your first name!": "Пожалуйста, введите свое имя!",
"Please input your invitation code!": "Please input your invitation code!", "Please input your invitation code!": "Пожалуйста, введите код приглашения!",
"Please input your last name!": "Введите свою фамилию!", "Please input your last name!": "Введите свою фамилию!",
"Please input your phone number!": "Пожалуйста, введите свой номер телефона!", "Please input your phone number!": "Пожалуйста, введите свой номер телефона!",
"Please input your real name!": "Пожалуйста, введите своё настоящее имя!", "Please input your real name!": "Пожалуйста, введите своё настоящее имя!",
@ -1162,9 +1163,9 @@
"MFA accounts": "MFA accounts", "MFA accounts": "MFA accounts",
"Managed accounts": "Управляемые аккаунты", "Managed accounts": "Управляемые аккаунты",
"Modify password...": "Изменить пароль...", "Modify password...": "Изменить пароль...",
"Multi-factor authentication": "Multi-factor authentication", "Multi-factor authentication": "Многофакторная аутентификация",
"Need update password": "Need update password", "Need update password": "Необходимо обновить пароль",
"Need update password - Tooltip": "Force user update password after login", "Need update password - Tooltip": "Заставить пользователя обновить пароль после входа в систему",
"New Email": "Новое электронное письмо", "New Email": "Новое электронное письмо",
"New Password": "Новый пароль", "New Password": "Новый пароль",
"New User": "Новый пользователь", "New User": "Новый пользователь",
@ -1188,26 +1189,26 @@
"Set password...": "Установить пароль...", "Set password...": "Установить пароль...",
"Tag": "Метка", "Tag": "Метка",
"Tag - Tooltip": "Тег пользователя", "Tag - Tooltip": "Тег пользователя",
"The password must contain at least one special character": "The password must contain at least one special character", "The password must contain at least one special character": "Пароль должен содержать хотя бы один специальный символ",
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "The password must contain at least one uppercase letter, one lowercase letter and one digit", "The password must contain at least one uppercase letter, one lowercase letter and one digit": "Пароль должен содержать как минимум одну заглавную букву, одну строчную букву и одну цифру",
"The password must have at least 6 characters": "The password must have at least 6 characters", "The password must have at least 6 characters": "Пароль должен быть минимум 6 символов",
"The password must have at least 8 characters": "The password must have at least 8 characters", "The password must have at least 8 characters": "Пароль должен быть минимум 8 символов",
"The password must not contain any repeated characters": "The password must not contain any repeated characters", "The password must not contain any repeated characters": "Пароль не должен содержать повторяющиеся символы",
"This field value doesn't match the pattern rule": "This field value doesn't match the pattern rule", "This field value doesn't match the pattern rule": "Значение поля не соответствует шаблону",
"Title": "Заголовок", "Title": "Заголовок",
"Title - Tooltip": "Положение в аффилиации", "Title - Tooltip": "Положение в аффилиации",
"Two passwords you typed do not match.": "Два введенных вами пароля не совпадают.", "Two passwords you typed do not match.": "Два введенных вами пароля не совпадают.",
"Unlink": "Отсоединить", "Unlink": "Отсоединить",
"Upload (.xlsx)": "Загрузить (.xlsx)", "Upload (.xlsx)": "Загрузить (.xlsx)",
"Upload ID card back picture": "Upload ID card back picture", "Upload ID card back picture": "Загрузите заднюю сторону удостоверения личности",
"Upload ID card front picture": "Upload ID card front picture", "Upload ID card front picture": "Загрузите переднюю сторону удостоверения личности",
"Upload ID card with person picture": "Upload ID card with person picture", "Upload ID card with person picture": "Загрузите удостоверение личности с фотографией",
"Upload a photo": "Загрузить фото", "Upload a photo": "Загрузить фото",
"User Profile": "User Profile", "User Profile": "Профиль пользователя",
"Values": "Значения", "Values": "Значения",
"Verification code sent": "Код подтверждения отправлен", "Verification code sent": "Код подтверждения отправлен",
"WebAuthn credentials": "WebAuthn удостоверения", "WebAuthn credentials": "WebAuthn удостоверения",
"You have changed the username, please save your change first before modifying the password": "You have changed the username, please save your change first before modifying the password", "You have changed the username, please save your change first before modifying the password": "Имя было изменено, сохраните изменения перед сменой пароля",
"input password": "введите пароль" "input password": "введите пароль"
}, },
"verification": { "verification": {

View File

@ -562,7 +562,8 @@
"Verification failed": "Overenie zlyhalo", "Verification failed": "Overenie zlyhalo",
"Verify Code": "Overiť kód", "Verify Code": "Overiť kód",
"Verify Password": "Overiť heslo", "Verify Password": "Overiť heslo",
"You have enabled multi-factor authentication, please enter the authentication code": "You have enabled multi-factor authentication, please enter the authentication code", "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
"You have enabled Multi-Factor Authentication, please enter the TOTP code": "You have enabled Multi-Factor Authentication, please enter the TOTP code",
"Your email is": "Váš email je", "Your email is": "Váš email je",
"Your phone is": "Váš telefón je", "Your phone is": "Váš telefón je",
"preferred": "preferované" "preferred": "preferované"

View File

@ -562,7 +562,8 @@
"Verification failed": "Verification failed", "Verification failed": "Verification failed",
"Verify Code": "Verify Code", "Verify Code": "Verify Code",
"Verify Password": "Verify Password", "Verify Password": "Verify Password",
"You have enabled multi-factor authentication, please enter the authentication code": "You have enabled multi-factor authentication, please enter the authentication code", "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
"You have enabled Multi-Factor Authentication, please enter the TOTP code": "You have enabled Multi-Factor Authentication, please enter the TOTP code",
"Your email is": "Your email is", "Your email is": "Your email is",
"Your phone is": "Your phone is", "Your phone is": "Your phone is",
"preferred": "preferred" "preferred": "preferred"

View File

@ -562,7 +562,8 @@
"Verification failed": "Doğrulama başarısız", "Verification failed": "Doğrulama başarısız",
"Verify Code": "Kodu doğrula", "Verify Code": "Kodu doğrula",
"Verify Password": "Parolayı Doğrula", "Verify Password": "Parolayı Doğrula",
"You have enabled multi-factor authentication, please enter the authentication code": "You have enabled multi-factor authentication, please enter the authentication code", "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
"You have enabled Multi-Factor Authentication, please enter the TOTP code": "You have enabled Multi-Factor Authentication, please enter the TOTP code",
"Your email is": "E-postanız", "Your email is": "E-postanız",
"Your phone is": "Telefon numaranız", "Your phone is": "Telefon numaranız",
"preferred": "tercih edilen" "preferred": "tercih edilen"

View File

@ -562,7 +562,8 @@
"Verification failed": "Не вдалося перевірити", "Verification failed": "Не вдалося перевірити",
"Verify Code": "Підтвердити код", "Verify Code": "Підтвердити код",
"Verify Password": "Підтвердіть пароль", "Verify Password": "Підтвердіть пароль",
"You have enabled multi-factor authentication, please enter the authentication code": "You have enabled multi-factor authentication, please enter the authentication code", "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
"You have enabled Multi-Factor Authentication, please enter the TOTP code": "You have enabled Multi-Factor Authentication, please enter the TOTP code",
"Your email is": "Ваша електронна адреса", "Your email is": "Ваша електронна адреса",
"Your phone is": "Ваш телефон", "Your phone is": "Ваш телефон",
"preferred": "бажаний" "preferred": "бажаний"

View File

@ -562,7 +562,8 @@
"Verification failed": "Verification failed", "Verification failed": "Verification failed",
"Verify Code": "Verify Code", "Verify Code": "Verify Code",
"Verify Password": "Verify Password", "Verify Password": "Verify Password",
"You have enabled multi-factor authentication, please enter the authentication code": "You have enabled multi-factor authentication, please enter the authentication code", "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
"You have enabled Multi-Factor Authentication, please enter the TOTP code": "You have enabled Multi-Factor Authentication, please enter the TOTP code",
"Your email is": "Your email is", "Your email is": "Your email is",
"Your phone is": "Your phone is", "Your phone is": "Your phone is",
"preferred": "preferred" "preferred": "preferred"

View File

@ -562,7 +562,8 @@
"Verification failed": "验证失败", "Verification failed": "验证失败",
"Verify Code": "验证码", "Verify Code": "验证码",
"Verify Password": "验证密码", "Verify Password": "验证密码",
"You have enabled multi-factor authentication, please enter the authentication code": "您已经启用多因素认证,请输入认证码", "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "您已经启用多因素认证, 请点击 '发送验证码' 继续",
"You have enabled Multi-Factor Authentication, please enter the TOTP code": "您已经启用多因素认证请输入TOTP认证码",
"Your email is": "你的电子邮件", "Your email is": "你的电子邮件",
"Your phone is": "你的手机号", "Your phone is": "你的手机号",
"preferred": "首选" "preferred": "首选"

View File

@ -104,6 +104,7 @@ class AccountTable extends React.Component {
{name: "Is forbidden", label: i18next.t("user:Is forbidden")}, {name: "Is forbidden", label: i18next.t("user:Is forbidden")},
{name: "Is deleted", label: i18next.t("user:Is deleted")}, {name: "Is deleted", label: i18next.t("user:Is deleted")},
{name: "Need update password", label: i18next.t("user:Need update password")}, {name: "Need update password", label: i18next.t("user:Need update password")},
{name: "IP whitelist", label: i18next.t("general:IP whitelist")},
{name: "Multi-factor authentication", label: i18next.t("user:Multi-factor authentication")}, {name: "Multi-factor authentication", label: i18next.t("user:Multi-factor authentication")},
{name: "WebAuthn credentials", label: i18next.t("user:WebAuthn credentials")}, {name: "WebAuthn credentials", label: i18next.t("user:WebAuthn credentials")},
{name: "Managed accounts", label: i18next.t("user:Managed accounts")}, {name: "Managed accounts", label: i18next.t("user:Managed accounts")},

View File

@ -14,9 +14,10 @@
import React from "react"; import React from "react";
import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons"; import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
import {Alert, Button, Col, Image, Input, Popover, QRCode, Row, Table, Tooltip} from "antd"; import {Button, Col, Image, Input, Popover, Row, Table, Tooltip} from "antd";
import * as Setting from "../Setting"; import * as Setting from "../Setting";
import i18next from "i18next"; import i18next from "i18next";
import {CasdoorAppQrCode, CasdoorAppUrl} from "../common/CasdoorAppConnector";
class MfaAccountTable extends React.Component { class MfaAccountTable extends React.Component {
constructor(props) { constructor(props) {
@ -76,42 +77,6 @@ class MfaAccountTable extends React.Component {
this.updateTable(table); this.updateTable(table);
} }
getQrUrl() {
const {accessToken} = this.props;
let qrUrl = `casdoor-app://login?serverUrl=${window.location.origin}&accessToken=${accessToken}`;
let error = null;
if (!accessToken) {
qrUrl = "";
error = i18next.t("general:Access token is empty");
}
if (qrUrl.length >= 2000) {
qrUrl = "";
error = i18next.t("general:QR code is too large");
}
return {qrUrl, error};
}
renderQrCode() {
const {qrUrl, error} = this.getQrUrl();
if (error) {
return <Alert message={error} type="error" showIcon />;
} else {
return (
<QRCode
value={qrUrl}
icon={this.state.icon}
errorLevel="M"
size={230}
bordered={false}
/>
);
}
}
renderTable(table) { renderTable(table) {
const columns = [ const columns = [
{ {
@ -194,10 +159,25 @@ class MfaAccountTable extends React.Component {
title={() => ( title={() => (
<div> <div>
{this.props.title}&nbsp;&nbsp;&nbsp;&nbsp; {this.props.title}&nbsp;&nbsp;&nbsp;&nbsp;
<Button style={{marginRight: "10px"}} type="primary" size="small" onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button> <Button style={{marginRight: "10px"}} type="primary" size="small" onClick={() => this.addRow(table)}>
<Popover trigger="focus" overlayInnerStyle={{padding: 0}} {i18next.t("general:Add")}
content={this.renderQrCode()}> </Button>
<Button style={{marginLeft: "5px"}} type="primary" size="small">{i18next.t("general:QR Code")}</Button> <Popover
trigger="focus"
overlayInnerStyle={{padding: 0}}
content={<CasdoorAppQrCode accessToken={this.props.accessToken} icon={this.state.icon} />}
>
<Button style={{marginRight: "10px"}} type="primary" size="small">
{i18next.t("general:QR Code")}
</Button>
</Popover>
<Popover
trigger="click"
content={<CasdoorAppUrl accessToken={this.props.accessToken} />}
>
<Button type="primary" size="small">
{i18next.t("general:Show URL")}
</Button>
</Popover> </Popover>
</div> </div>
)} )}