mirror of
https://github.com/casdoor/casdoor.git
synced 2025-08-22 04:10:32 +08:00
Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a45d2b87c1 | ||
![]() |
8484465d09 | ||
![]() |
dff65eee20 | ||
![]() |
596016456c | ||
![]() |
673261c258 | ||
![]() |
3c5985a3c0 | ||
![]() |
4f3d62520a | ||
![]() |
96f8b3d937 | ||
![]() |
7ab5a5ade1 | ||
![]() |
5cbd0a96ca | ||
![]() |
7ccd8c4d4f | ||
![]() |
b0fa3fc484 | ||
![]() |
af01c4226a | ||
![]() |
7a3d85a29a | ||
![]() |
fd5ccd8d41 | ||
![]() |
a439c5195d | ||
![]() |
ba2e997d54 | ||
![]() |
0818de85d1 | ||
![]() |
457c6098a4 | ||
![]() |
60f979fbb5 | ||
![]() |
ff53e44fa6 | ||
![]() |
1832de47db | ||
![]() |
535eb0c465 | ||
![]() |
c190634cf3 | ||
![]() |
f7559aa040 |
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -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
|
||||||
|
@@ -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, *, *
|
||||||
`
|
`
|
||||||
|
@@ -854,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")
|
||||||
|
114
controllers/casbin_cli_api.go
Normal file
114
controllers/casbin_cli_api.go
Normal 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)
|
||||||
|
}
|
@@ -561,8 +561,9 @@ func (c *ApiController) SetPassword() {
|
|||||||
targetUser.Password = newPassword
|
targetUser.Password = newPassword
|
||||||
targetUser.UpdateUserPassword(organization)
|
targetUser.UpdateUserPassword(organization)
|
||||||
targetUser.NeedUpdatePassword = false
|
targetUser.NeedUpdatePassword = false
|
||||||
|
targetUser.LastChangePasswordTime = util.GetCurrentTime()
|
||||||
|
|
||||||
_, 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
|
||||||
|
@@ -294,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)
|
||||||
|
@@ -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 را فراخوانی کنید"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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")
|
||||||
}
|
}
|
||||||
|
@@ -43,6 +43,8 @@ func CheckEntryIp(clientIp string, user *User, application *Application, organiz
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
application.IpRestriction = err.Error() + application.Name
|
application.IpRestriction = err.Error() + application.Name
|
||||||
return fmt.Errorf(err.Error() + application.Name)
|
return fmt.Errorf(err.Error() + application.Name)
|
||||||
|
} else {
|
||||||
|
application.IpRestriction = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
if organization == nil && application.OrganizationObj != nil {
|
if organization == nil && application.OrganizationObj != nil {
|
||||||
@@ -55,6 +57,8 @@ func CheckEntryIp(clientIp string, user *User, application *Application, organiz
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
organization.IpRestriction = err.Error() + organization.Name
|
organization.IpRestriction = err.Error() + organization.Name
|
||||||
return fmt.Errorf(err.Error() + organization.Name)
|
return fmt.Errorf(err.Error() + organization.Name)
|
||||||
|
} else {
|
||||||
|
organization.IpRestriction = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
53
object/check_password_expired.go
Normal file
53
object/check_password_expired.go
Normal 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
|
||||||
|
}
|
@@ -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"`
|
||||||
|
@@ -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())
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -200,8 +200,9 @@ type User struct {
|
|||||||
Permissions []*Permission `json:"permissions"`
|
Permissions []*Permission `json:"permissions"`
|
||||||
Groups []string `xorm:"groups varchar(1000)" json:"groups"`
|
Groups []string `xorm:"groups varchar(1000)" json:"groups"`
|
||||||
|
|
||||||
LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"`
|
LastChangePasswordTime string `xorm:"varchar(100)" json:"lastChangePasswordTime"`
|
||||||
SigninWrongTimes int `json:"signinWrongTimes"`
|
LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"`
|
||||||
|
SigninWrongTimes int `json:"signinWrongTimes"`
|
||||||
|
|
||||||
ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"`
|
ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"`
|
||||||
MfaAccounts []MfaAccount `xorm:"mfaAccounts blob" json:"mfaAccounts"`
|
MfaAccounts []MfaAccount `xorm:"mfaAccounts blob" json:"mfaAccounts"`
|
||||||
@@ -690,7 +691,7 @@ 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",
|
||||||
@@ -816,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
|
||||||
|
@@ -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")
|
||||||
|
@@ -25,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() {
|
||||||
@@ -36,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 {
|
||||||
|
@@ -603,7 +603,7 @@ class ApplicationEditPage extends React.Component {
|
|||||||
{Setting.getLabel(i18next.t("general:IP whitelist"), i18next.t("general:IP whitelist - Tooltip"))} :
|
{Setting.getLabel(i18next.t("general:IP whitelist"), i18next.t("general:IP whitelist - Tooltip"))} :
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<Input placeholder = {this.state.application.organizationObj?.ipWhitelist} value={this.state.application.ipWhiteList} onChange={e => {
|
<Input placeholder = {this.state.application.organizationObj?.ipWhitelist} value={this.state.application.ipWhitelist} onChange={e => {
|
||||||
this.updateApplicationField("ipWhitelist", e.target.value);
|
this.updateApplicationField("ipWhitelist", e.target.value);
|
||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
@@ -765,7 +765,7 @@ class ApplicationEditPage extends React.Component {
|
|||||||
/>
|
/>
|
||||||
<br />
|
<br />
|
||||||
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
|
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
|
||||||
copy(`${window.location.origin}/api/saml/metadata?application=admin/${encodeURIComponent(this.state.applicationName)}&post=${this.state.application.enableSamlPostBinding}`);
|
copy(`${window.location.origin}/api/saml/metadata?application=admin/${encodeURIComponent(this.state.applicationName)}&enablePostBinding=${this.state.application.enableSamlPostBinding}`);
|
||||||
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
|
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@@ -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);
|
||||||
}}
|
}}
|
||||||
|
@@ -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"))} :
|
||||||
|
@@ -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: "",
|
||||||
|
@@ -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"))} :
|
||||||
|
@@ -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}`))}
|
||||||
/>
|
/>
|
||||||
|
@@ -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"}} >
|
||||||
|
@@ -243,7 +243,10 @@ class LoginPage extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getPlaceholder() {
|
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");
|
||||||
@@ -444,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)}
|
||||||
/>);
|
/>);
|
||||||
@@ -485,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}`);
|
||||||
@@ -513,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)}
|
||||||
/>);
|
/>);
|
||||||
@@ -679,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,
|
||||||
@@ -1093,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>
|
||||||
|
@@ -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,
|
||||||
|
@@ -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
|
||||||
|
121
web/src/common/CasdoorAppConnector.js
Normal file
121
web/src/common/CasdoorAppConnector.js
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
File diff suppressed because it is too large
Load Diff
@@ -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}
|
{this.props.title}
|
||||||
<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>
|
||||||
)}
|
)}
|
||||||
|
Reference in New Issue
Block a user