Compare commits

..

18 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
c2885b54d1 fix: install lsof in ALLINONE Docker image for process management
Co-authored-by: nomeguy <85475922+nomeguy@users.noreply.github.com>
2026-02-02 13:52:03 +00:00
copilot-swe-agent[bot]
588015f0bc Initial plan 2026-02-02 13:50:24 +00:00
DacongDA
72b70c3b03 feat: use sqlite DB instead of mariadb for all-in-one Docker image (#4949) 2026-02-02 00:13:14 +08:00
DacongDA
a1c56894c7 feat: add tabs to user edit page (#4945) 2026-02-01 14:01:28 +08:00
Yang Luo
a9ae9394c7 feat: add Linux machine login via LDAP with POSIX attributes (#4944) 2026-01-31 22:37:29 +08:00
Yang Luo
5f0fa5f23e feat: fix properties field xlsx import issue in user list page (#4943) 2026-01-31 01:49:36 +08:00
Yang Luo
f99aa047a9 feat: add Org.AccountItems.Tab field to have tabs in user edit page (#4892) 2026-01-30 21:56:35 +08:00
Yang Luo
1d22b7ebd0 feat: prevent duplicate webhook events from redundant payment notifications (#4936) 2026-01-30 21:56:09 +08:00
IsAurora6
d147053329 feat: Optimize the display of the products column on the order and payment, adjust the color of the “Add to Cart” button. (#4933) 2026-01-30 14:03:15 +08:00
IsAurora6
0f8cd92be4 feat: resolve returnUrl redirection failure of UserEditPage (#4931) 2026-01-29 09:37:47 +08:00
DacongDA
7ea6f1296d feat: fix i18n/generate.go bug in handling "\" (#4930) 2026-01-28 23:35:23 +08:00
Yang Luo
db8c649f5e feat: include payment status in notify-payment webhook payload (#4929) 2026-01-28 19:59:10 +08:00
DacongDA
a06d003589 feat: make codeChallenge dynamic for custom OAuth provider (#4924) 2026-01-28 17:56:28 +08:00
Jacob
33298e44d4 feat(ldap-sync): support syncing phone country code and formatting mobile number (#4919) 2026-01-28 14:09:52 +08:00
IsAurora6
f4d86f8d92 feat: fix incorrect clearing of the returnUrl path parameter in redirects (#4920) 2026-01-28 10:51:44 +08:00
Yang Luo
af4337a1ae feat: add multi-address support to user edit page (#4916) 2026-01-27 21:46:41 +08:00
IsAurora6
81e650df65 feat: Optimize the display of the order price column and improve parameter passing in the OrderPay view mode. (#4912) 2026-01-27 12:17:15 +08:00
Yang Luo
fcea1e4c07 feat: add SCIM 2.0 syncer (#4909) 2026-01-27 01:47:50 +08:00
65 changed files with 1744 additions and 384 deletions

View File

@@ -51,22 +51,14 @@ COPY --from=FRONT --chown=$USER:$USER /web/build ./web/build
ENTRYPOINT ["/server"]
FROM debian:latest AS db
RUN apt update \
&& apt install -y \
mariadb-server \
mariadb-client \
&& rm -rf /var/lib/apt/lists/*
FROM db AS ALLINONE
FROM debian:latest AS ALLINONE
LABEL MAINTAINER="https://casdoor.org/"
ARG TARGETOS
ARG TARGETARCH
ENV BUILDX_ARCH="${TARGETOS:-linux}_${TARGETARCH:-amd64}"
RUN apt update
RUN apt install -y ca-certificates && update-ca-certificates
RUN apt install -y ca-certificates lsof && update-ca-certificates
WORKDIR /
COPY --from=BACK /go/src/casdoor/server_${BUILDX_ARCH} ./server

View File

@@ -739,6 +739,7 @@ func (c *ApiController) Login() {
} else if provider.Category == "OAuth" || provider.Category == "Web3" {
// OAuth
idpInfo := object.FromProviderToIdpInfo(c.Ctx, provider)
idpInfo.CodeVerifier = authForm.CodeVerifier
var idProvider idp.IdProvider
idProvider, err = idp.GetIdProvider(idpInfo, authForm.RedirectUri)
if err != nil {

View File

@@ -1,8 +1,10 @@
#!/bin/bash
if [ "${MYSQL_ROOT_PASSWORD}" = "" ] ;then MYSQL_ROOT_PASSWORD=123456 ;fi
service mariadb start
if [ -z "${driverName:-}" ]; then
export driverName=sqlite
fi
if [ -z "${dataSourceName:-}" ]; then
export dataSourceName="file:casdoor.db?cache=shared"
fi
mysqladmin -u root password ${MYSQL_ROOT_PASSWORD}
exec /server --createDatabase=true
exec /server

View File

@@ -46,6 +46,7 @@ type AuthForm struct {
State string `json:"state"`
RedirectUri string `json:"redirectUri"`
Method string `json:"method"`
CodeVerifier string `json:"codeVerifier"`
EmailCode string `json:"emailCode"`
PhoneCode string `json:"phoneCode"`

View File

@@ -19,6 +19,7 @@ import (
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/casdoor/casdoor/util"
@@ -47,7 +48,11 @@ func getAllI18nStringsFrontend(fileContent string) []string {
}
for _, match := range matches {
res = append(res, match[1])
target, err := strconv.Unquote("\"" + match[1] + "\"")
if err != nil {
target = match[1]
}
res = append(res, target)
}
return res
}
@@ -61,7 +66,12 @@ func getAllI18nStringsBackend(fileContent string, isObjectPackage bool) []string
}
for _, match := range matches {
match := strings.SplitN(match[1], ",", 2)
res = append(res, match[1][2:])
target, err := strconv.Unquote("\"" + match[1][2:] + "\"")
if err != nil {
target = match[1][2:]
}
res = append(res, target)
}
} else {
matches := reI18nBackendController.FindAllStringSubmatch(fileContent, -1)
@@ -69,7 +79,11 @@ func getAllI18nStringsBackend(fileContent string, isObjectPackage bool) []string
return res
}
for _, match := range matches {
res = append(res, match[1][1:])
target, err := strconv.Unquote("\"" + match[1][1:] + "\"")
if err != nil {
target = match[1][1:]
}
res = append(res, target)
}
}

View File

@@ -83,8 +83,8 @@
"The user is forbidden to sign in, please contact the administrator": "Dem Benutzer ist der Zugang verboten, bitte kontaktieren Sie den Administrator",
"The user: %s doesn't exist in LDAP server": "Der Benutzer: %s existiert nicht im LDAP-Server",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "Der Benutzername darf nur alphanumerische Zeichen, Unterstriche oder Bindestriche enthalten, keine aufeinanderfolgenden Bindestriche oder Unterstriche haben und darf nicht mit einem Bindestrich oder Unterstrich beginnen oder enden.",
"The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex": "Der Wert \\\"%s\\\" für das Kontenfeld \\\"%s\\\" stimmt nicht mit dem Kontenelement-Regex überein",
"The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"": "Der Wert \\\"%s\\\" für das Registrierungsfeld \\\"%s\\\" stimmt nicht mit dem Registrierungselement-Regex der Anwendung \\\"%s\\\" überein",
"The value \"%s\" for account field \"%s\" doesn't match the account item regex": "Der Wert \"%s\" für das Kontenfeld \"%s\" stimmt nicht mit dem Kontenelement-Regex überein",
"The value \"%s\" for signup field \"%s\" doesn't match the signup item regex of the application \"%s\"": "Der Wert \"%s\" für das Registrierungsfeld \"%s\" stimmt nicht mit dem Registrierungselement-Regex der Anwendung \"%s\" überein",
"Username already exists": "Benutzername existiert bereits",
"Username cannot be an email address": "Benutzername kann keine E-Mail-Adresse sein",
"Username cannot contain white spaces": "Benutzername darf keine Leerzeichen enthalten",
@@ -94,7 +94,7 @@
"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.": "Benutzername unterstützt E-Mail-Format. Der Benutzername darf nur alphanumerische Zeichen, Unterstriche oder Bindestriche enthalten, keine aufeinanderfolgenden Bindestriche oder Unterstriche haben und darf nicht mit einem Bindestrich oder Unterstrich beginnen oder enden. Achten Sie auch auf das E-Mail-Format.",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Sie haben zu oft das falsche Passwort oder den falschen Code eingegeben. Bitte warten Sie %d Minuten und versuchen Sie es erneut",
"Your IP address: %s has been banned according to the configuration of: ": "Ihre IP-Adresse: %s wurde laut Konfiguration gesperrt von: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Ihr Passwort ist abgelaufen. Bitte setzen Sie Ihr Passwort zurück, indem Sie auf \\\"Passwort vergessen\\\" klicken",
"Your password has expired. Please reset your password by clicking \"Forgot password\"": "Ihr Passwort ist abgelaufen. Bitte setzen Sie Ihr Passwort zurück, indem Sie auf \"Passwort vergessen\" klicken",
"Your region is not allow to signup by phone": "Ihre Region ist nicht berechtigt, sich telefonisch anzumelden",
"password or code is incorrect": "Passwort oder Code ist falsch",
"password or code is incorrect, you have %s remaining chances": "Das Passwort oder der Code ist falsch. Du hast noch %s Versuche übrig",
@@ -137,7 +137,7 @@
"adding a new user to the 'built-in' organization is currently disabled. Please note: all users in the 'built-in' organization are global administrators in Casdoor. Refer to the docs: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. If you still wish to create a user for the 'built-in' organization, go to the organization's settings page and enable the 'Has privilege consent' option.": "Das Hinzufügen eines neuen Benutzers zur 'eingebauten' Organisation ist derzeit deaktiviert. Bitte beachten Sie: Alle Benutzer in der 'eingebauten' Organisation sind globale Administratoren in Casdoor. Siehe die Docs: https://casdoor.org/docs/basic/core-concepts#how -does-casdoor-manage-sich selbst. Wenn Sie immer noch einen Benutzer für die 'eingebaute' Organisation erstellen möchten, gehen Sie auf die Einstellungsseite der Organisation und aktivieren Sie die Option 'Habt Berechtigungszustimmung'."
},
"permission": {
"The permission: \\\"%s\\\" doesn't exist": "Die Berechtigung: \\\"%s\\\" existiert nicht"
"The permission: \"%s\" doesn't exist": "Die Berechtigung: \"%s\" existiert nicht"
},
"provider": {
"Invalid application id": "Ungültige Anwendungs-ID",
@@ -196,8 +196,8 @@
"Unknown type": "Unbekannter Typ",
"Wrong verification code!": "Falscher Bestätigungscode!",
"You should verify your code in %d min!": "Du solltest deinen Code in %d Minuten verifizieren!",
"please add a SMS provider to the \\\"Providers\\\" list for the application: %s": "Bitte fügen Sie einen SMS-Anbieter zur \\\"Providers\\\"-Liste für die Anwendung hinzu: %s",
"please add an Email provider to the \\\"Providers\\\" list for the application: %s": "Bitte fügen Sie einen E-Mail-Anbieter zur \\\"Providers\\\"-Liste für die Anwendung hinzu: %s",
"please add a SMS provider to the \"Providers\" list for the application: %s": "Bitte fügen Sie einen SMS-Anbieter zur \"Providers\"-Liste für die Anwendung hinzu: %s",
"please add an Email provider to the \"Providers\" list for the application: %s": "Bitte fügen Sie einen E-Mail-Anbieter zur \"Providers\"-Liste für die Anwendung hinzu: %s",
"the user does not exist, please sign up first": "Der Benutzer existiert nicht, bitte zuerst anmelden"
},
"webauthn": {

View File

@@ -83,8 +83,8 @@
"The user is forbidden to sign in, please contact the administrator": "The user is forbidden to sign in, please contact the administrator",
"The user: %s doesn't exist in LDAP server": "The user: %s doesn't exist in LDAP server",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.",
"The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex": "The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex",
"The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"": "The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"",
"The value \"%s\" for account field \"%s\" doesn't match the account item regex": "The value \"%s\" for account field \"%s\" doesn't match the account item regex",
"The value \"%s\" for signup field \"%s\" doesn't match the signup item regex of the application \"%s\"": "The value \"%s\" for signup field \"%s\" doesn't match the signup item regex of the application \"%s\"",
"Username already exists": "Username already exists",
"Username cannot be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces",
@@ -94,7 +94,7 @@
"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.": "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.",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
"Your IP address: %s has been banned according to the configuration of: ": "Your IP address: %s has been banned according to the configuration of: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"",
"Your password has expired. Please reset your password by clicking \"Forgot password\"": "Your password has expired. Please reset your password by clicking \"Forgot password\"",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %s remaining chances": "password or code is incorrect, you have %s remaining chances",
@@ -137,7 +137,7 @@
"adding a new user to the 'built-in' organization is currently disabled. Please note: all users in the 'built-in' organization are global administrators in Casdoor. Refer to the docs: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. If you still wish to create a user for the 'built-in' organization, go to the organization's settings page and enable the 'Has privilege consent' option.": "adding a new user to the 'built-in' organization is currently disabled. Please note: all users in the 'built-in' organization are global administrators in Casdoor. Refer to the docs: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. If you still wish to create a user for the 'built-in' organization, go to the organization's settings page and enable the 'Has privilege consent' option."
},
"permission": {
"The permission: \\\"%s\\\" doesn't exist": "The permission: \\\"%s\\\" doesn't exist"
"The permission: \"%s\" doesn't exist": "The permission: \"%s\" doesn't exist"
},
"provider": {
"Invalid application id": "Invalid application id",
@@ -196,8 +196,8 @@
"Unknown type": "Unknown type",
"Wrong verification code!": "Wrong verification code!",
"You should verify your code in %d min!": "You should verify your code in %d min!",
"please add a SMS provider to the \\\"Providers\\\" list for the application: %s": "please add a SMS provider to the \\\"Providers\\\" list for the application: %s",
"please add an Email provider to the \\\"Providers\\\" list for the application: %s": "please add an Email provider to the \\\"Providers\\\" list for the application: %s",
"please add a SMS provider to the \"Providers\" list for the application: %s": "please add a SMS provider to the \"Providers\" list for the application: %s",
"please add an Email provider to the \"Providers\" list for the application: %s": "please add an Email provider to the \"Providers\" list for the application: %s",
"the user does not exist, please sign up first": "the user does not exist, please sign up first"
},
"webauthn": {

View File

@@ -83,8 +83,8 @@
"The user is forbidden to sign in, please contact the administrator": "El usuario no está autorizado a iniciar sesión, por favor contacte al administrador",
"The user: %s doesn't exist in LDAP server": "El usuario: %s no existe en el servidor 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.": "El nombre de usuario solo puede contener caracteres alfanuméricos, guiones bajos o guiones, no puede tener guiones o subrayados consecutivos, y no puede comenzar ni terminar con un guión o subrayado.",
"The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex": "El valor \\\"%s\\\" para el campo de cuenta \\\"%s\\\" no coincide con la expresión regular del elemento de cuenta",
"The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"": "El valor \\\"%s\\\" para el campo de registro \\\"%s\\\" no coincide con la expresión regular del elemento de registro de la aplicación \\\"%s\\\"",
"The value \"%s\" for account field \"%s\" doesn't match the account item regex": "El valor \"%s\" para el campo de cuenta \"%s\" no coincide con la expresión regular del elemento de cuenta",
"The value \"%s\" for signup field \"%s\" doesn't match the signup item regex of the application \"%s\"": "El valor \"%s\" para el campo de registro \"%s\" no coincide con la expresión regular del elemento de registro de la aplicación \"%s\"",
"Username already exists": "El nombre de usuario ya existe",
"Username cannot be an email address": "Nombre de usuario no puede ser una dirección de correo electrónico",
"Username cannot contain white spaces": "Nombre de usuario no puede contener espacios en blanco",
@@ -94,7 +94,7 @@
"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.": "El nombre de usuario admite formato de correo electrónico. Además, el nombre de usuario solo puede contener caracteres alfanuméricos, guiones bajos o guiones, no puede tener guiones bajos o guiones consecutivos y no puede comenzar ni terminar con un guión o guión bajo. También preste atención al formato del correo electrónico.",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Has ingresado la contraseña o código incorrecto demasiadas veces, por favor espera %d minutos e intenta de nuevo",
"Your IP address: %s has been banned according to the configuration of: ": "Su dirección IP: %s ha sido bloqueada según la configuración de: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Su contraseña ha expirado. Restablezca su contraseña haciendo clic en \\\"Olvidé mi contraseña\\\"",
"Your password has expired. Please reset your password by clicking \"Forgot password\"": "Su contraseña ha expirado. Restablezca su contraseña haciendo clic en \"Olvidé mi contraseña\"",
"Your region is not allow to signup by phone": "Tu región no está permitida para registrarse por teléfono",
"password or code is incorrect": "contraseña o código incorrecto",
"password or code is incorrect, you have %s remaining chances": "Contraseña o código incorrecto, tienes %s intentos restantes",
@@ -137,7 +137,7 @@
"adding a new user to the 'built-in' organization is currently disabled. Please note: all users in the 'built-in' organization are global administrators in Casdoor. Refer to the docs: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. If you still wish to create a user for the 'built-in' organization, go to the organization's settings page and enable the 'Has privilege consent' option.": "La adición de un nuevo usuario a la organización 'integrada' está actualmente deshabilitada. Tenga en cuenta: todos los usuarios de la organización 'integrada' son administradores globales en Casdoor. Consulte los docs: https://casdoor.org/docs/basic/core-concepts#how -does-casdoor-manage-itself. Si todavía desea crear un usuario para la organización 'integrada', vaya a la página de configuración de la organización y habilite la opción 'Tiene consentimiento de privilegios'."
},
"permission": {
"The permission: \\\"%s\\\" doesn't exist": "El permiso: \\\"%s\\\" no existe"
"The permission: \"%s\" doesn't exist": "El permiso: \"%s\" no existe"
},
"provider": {
"Invalid application id": "Identificación de aplicación no válida",
@@ -196,8 +196,8 @@
"Unknown type": "Tipo desconocido",
"Wrong verification code!": "¡Código de verificación incorrecto!",
"You should verify your code in %d min!": "¡Deberías verificar tu código en %d minutos!",
"please add a SMS provider to the \\\"Providers\\\" list for the application: %s": "agregue un proveedor de SMS a la lista \\\"Proveedores\\\" para la aplicación: %s",
"please add an Email provider to the \\\"Providers\\\" list for the application: %s": "agregue un proveedor de correo electrónico a la lista \\\"Proveedores\\\" para la aplicación: %s",
"please add a SMS provider to the \"Providers\" list for the application: %s": "agregue un proveedor de SMS a la lista \"Proveedores\" para la aplicación: %s",
"please add an Email provider to the \"Providers\" list for the application: %s": "agregue un proveedor de correo electrónico a la lista \"Proveedores\" para la aplicación: %s",
"the user does not exist, please sign up first": "El usuario no existe, por favor regístrese primero"
},
"webauthn": {

View File

@@ -83,8 +83,8 @@
"The user is forbidden to sign in, please contact the administrator": "L'utilisateur est interdit de se connecter, veuillez contacter l'administrateur",
"The user: %s doesn't exist in LDAP server": "L'utilisateur : %s n'existe pas sur le serveur 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.": "Le nom d'utilisateur ne peut contenir que des caractères alphanumériques, des traits soulignés ou des tirets, ne peut pas avoir de tirets ou de traits soulignés consécutifs et ne peut pas commencer ou se terminer par un tiret ou un trait souligné.",
"The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex": "La valeur \\\"%s\\\" pour le champ de compte \\\"%s\\\" ne correspond pas à l'expression régulière de l'élément de compte",
"The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"": "La valeur \\\"%s\\\" pour le champ d'inscription \\\"%s\\\" ne correspond pas à l'expression régulière de l'élément d'inscription de l'application \\\"%s\\\"",
"The value \"%s\" for account field \"%s\" doesn't match the account item regex": "La valeur \"%s\" pour le champ de compte \"%s\" ne correspond pas à l'expression régulière de l'élément de compte",
"The value \"%s\" for signup field \"%s\" doesn't match the signup item regex of the application \"%s\"": "La valeur \"%s\" pour le champ d'inscription \"%s\" ne correspond pas à l'expression régulière de l'élément d'inscription de l'application \"%s\"",
"Username already exists": "Nom d'utilisateur existe déjà",
"Username cannot be an email address": "Nom d'utilisateur ne peut pas être une adresse e-mail",
"Username cannot contain white spaces": "Nom d'utilisateur ne peut pas contenir d'espaces blancs",
@@ -94,7 +94,7 @@
"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.": "Le nom d'utilisateur prend en charge le format e-mail. De plus, il ne peut contenir que des caractères alphanumériques, des tirets bas ou des traits d'union, ne peut pas avoir de traits d'union ou de tirets bas consécutifs, et ne peut pas commencer ou se terminer par un trait d'union ou un tiret bas. Faites également attention au format de l'e-mail.",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Vous avez entré le mauvais mot de passe ou code plusieurs fois, veuillez attendre %d minutes et réessayer",
"Your IP address: %s has been banned according to the configuration of: ": "Votre adresse IP : %s a été bannie selon la configuration de : ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Votre mot de passe a expiré. Veuillez le réinitialiser en cliquant sur \\\"Mot de passe oublié\\\"",
"Your password has expired. Please reset your password by clicking \"Forgot password\"": "Votre mot de passe a expiré. Veuillez le réinitialiser en cliquant sur \"Mot de passe oublié\"",
"Your region is not allow to signup by phone": "Votre région n'est pas autorisée à s'inscrire par téléphone",
"password or code is incorrect": "mot de passe ou code incorrect",
"password or code is incorrect, you have %s remaining chances": "Le mot de passe ou le code est incorrect, il vous reste %s chances",
@@ -137,7 +137,7 @@
"adding a new user to the 'built-in' organization is currently disabled. Please note: all users in the 'built-in' organization are global administrators in Casdoor. Refer to the docs: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. If you still wish to create a user for the 'built-in' organization, go to the organization's settings page and enable the 'Has privilege consent' option.": "L'ajout d'un nouvel utilisateur à l'organisation « built-in » (intégrée) est actuellement désactivé. Veuillez noter : Tous les utilisateurs de l'organisation « built-in » sont des administrateurs globaux dans Casdoor. Consulter la documentation : https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. Si vous souhaitez encore créer un utilisateur pour l'organisation « built-in », accédez à la page des paramètres de l'organisation et activez l'option « A le consentement aux privilèges »."
},
"permission": {
"The permission: \\\"%s\\\" doesn't exist": "La permission : \\\"%s\\\" n'existe pas"
"The permission: \"%s\" doesn't exist": "La permission : \"%s\" n'existe pas"
},
"provider": {
"Invalid application id": "Identifiant d'application invalide",
@@ -196,8 +196,8 @@
"Unknown type": "Type inconnu",
"Wrong verification code!": "Mauvais code de vérification !",
"You should verify your code in %d min!": "Vous devriez vérifier votre code en %d min !",
"please add a SMS provider to the \\\"Providers\\\" list for the application: %s": "veuillez ajouter un fournisseur SMS à la liste \\\"Providers\\\" pour l'application : %s",
"please add an Email provider to the \\\"Providers\\\" list for the application: %s": "veuillez ajouter un fournisseur d'e-mail à la liste \\\"Providers\\\" pour l'application : %s",
"please add a SMS provider to the \"Providers\" list for the application: %s": "veuillez ajouter un fournisseur SMS à la liste \"Providers\" pour l'application : %s",
"please add an Email provider to the \"Providers\" list for the application: %s": "veuillez ajouter un fournisseur d'e-mail à la liste \"Providers\" pour l'application : %s",
"the user does not exist, please sign up first": "L'utilisateur n'existe pas, veuillez vous inscrire d'abord"
},
"webauthn": {

View File

@@ -83,8 +83,8 @@
"The user is forbidden to sign in, please contact the administrator": "ユーザーはサインインできません。管理者に連絡してください",
"The user: %s doesn't exist in LDAP server": "ユーザー「%s」は LDAP サーバーに存在しません",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "ユーザー名には英数字、アンダースコア、ハイフンしか含めることができません。連続したハイフンまたはアンダースコアは不可であり、ハイフンまたはアンダースコアで始まるまたは終わることもできません。",
"The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex": "アカウントフィールド「%s」の値「%s」がアカウント項目の正規表現に一致しません",
"The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"": "アプリケーション「%s」のサインアップ項目「%s」の値「%s」が正規表現に一致しません",
"The value \"%s\" for account field \"%s\" doesn't match the account item regex": "アカウントフィールド「%s」の値「%s」がアカウント項目の正規表現に一致しません",
"The value \"%s\" for signup field \"%s\" doesn't match the signup item regex of the application \"%s\"": "アプリケーション「%s」のサインアップ項目「%s」の値「%s」が正規表現に一致しません",
"Username already exists": "ユーザー名はすでに存在しています",
"Username cannot be an email address": "ユーザー名には電子メールアドレスを使用できません",
"Username cannot contain white spaces": "ユーザ名にはスペースを含めることはできません",
@@ -94,7 +94,7 @@
"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.": "ユーザー名はメール形式もサポートします。ユーザー名は英数字、アンダースコア、またはハイフンのみを含め、連続するハイフンやアンダースコアは使用できません。また、ハイフンまたはアンダースコアで始まったり終わったりすることもできません。メール形式にも注意してください。",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "あなたは間違ったパスワードまたはコードを何度も入力しました。%d 分間待ってから再度お試しください",
"Your IP address: %s has been banned according to the configuration of: ": "あなたの IP アドレス「%s」は設定によりアクセスが禁止されています: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "パスワードの有効期限が切れています。「パスワードを忘れた方はこちら」をクリックしてリセットしてください",
"Your password has expired. Please reset your password by clicking \"Forgot password\"": "パスワードの有効期限が切れています。「パスワードを忘れた方はこちら」をクリックしてリセットしてください",
"Your region is not allow to signup by phone": "あなたの地域は電話でサインアップすることができません",
"password or code is incorrect": "パスワードまたはコードが正しくありません",
"password or code is incorrect, you have %s remaining chances": "パスワードまたはコードが間違っています。あと %s 回の試行機会があります",
@@ -137,7 +137,7 @@
"adding a new user to the 'built-in' organization is currently disabled. Please note: all users in the 'built-in' organization are global administrators in Casdoor. Refer to the docs: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. If you still wish to create a user for the 'built-in' organization, go to the organization's settings page and enable the 'Has privilege consent' option.": "「built-in」組み込み組織への新しいユーザーの追加は現在無効になっています。注意「built-in」組織のすべてのユーザーは、Casdoor のグローバル管理者です。ドキュメントを参照してくださいhttps://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself。「built-in」組織のユーザーを作成したい場合は、組織の設定ページに移動し、「特権同意を持つ」オプションを有効にしてください。"
},
"permission": {
"The permission: \\\"%s\\\" doesn't exist": "権限「%s」は存在しません"
"The permission: \"%s\" doesn't exist": "権限「%s」は存在しません"
},
"provider": {
"Invalid application id": "アプリケーションIDが無効です",
@@ -196,8 +196,8 @@
"Unknown type": "不明なタイプ",
"Wrong verification code!": "誤った検証コードです!",
"You should verify your code in %d min!": "あなたは%d分であなたのコードを確認する必要があります",
"please add a SMS provider to the \\\"Providers\\\" list for the application: %s": "アプリケーション「%s」の「Providers」リストに SMS プロバイダを追加してください",
"please add an Email provider to the \\\"Providers\\\" list for the application: %s": "アプリケーション「%s」の「Providers」リストにメールプロバイダを追加してください",
"please add a SMS provider to the \"Providers\" list for the application: %s": "アプリケーション「%s」の「Providers」リストに SMS プロバイダを追加してください",
"please add an Email provider to the \"Providers\" list for the application: %s": "アプリケーション「%s」の「Providers」リストにメールプロバイダを追加してください",
"the user does not exist, please sign up first": "ユーザーは存在しません。まず登録してください"
},
"webauthn": {

View File

@@ -83,8 +83,8 @@
"The user is forbidden to sign in, please contact the administrator": "Użytkownikowi zabroniono logowania, skontaktuj się z administratorem",
"The user: %s doesn't exist in LDAP server": "Użytkownik: %s nie istnieje w serwerze 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.": "Nazwa użytkownika może zawierać tylko znaki alfanumeryczne, podkreślenia lub myślniki, nie może mieć kolejnych myślników lub podkreśleń i nie może zaczynać się ani kończyć myślnikiem lub podkreśleniem.",
"The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex": "Wartość \\\"%s\\\" dla pola konta \\\"%s\\\" nie pasuje do wyrażenia regularnego elementu konta",
"The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"": "Wartość \\\"%s\\\" dla pola rejestracji \\\"%s\\\" nie pasuje do wyrażenia regularnego elementu rejestracji aplikacji \\\"%s\\\"",
"The value \"%s\" for account field \"%s\" doesn't match the account item regex": "Wartość \"%s\" dla pola konta \"%s\" nie pasuje do wyrażenia regularnego elementu konta",
"The value \"%s\" for signup field \"%s\" doesn't match the signup item regex of the application \"%s\"": "Wartość \"%s\" dla pola rejestracji \"%s\" nie pasuje do wyrażenia regularnego elementu rejestracji aplikacji \"%s\"",
"Username already exists": "Nazwa użytkownika już istnieje",
"Username cannot be an email address": "Nazwa użytkownika nie może być adresem email",
"Username cannot contain white spaces": "Nazwa użytkownika nie może zawierać spacji",
@@ -94,7 +94,7 @@
"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.": "Nazwa użytkownika obsługuje format email. Również nazwa użytkownika może zawierać tylko znaki alfanumeryczne, podkreślenia lub myślniki, nie może mieć kolejnych myślników lub podkreśleń i nie może zaczynać się ani kończyć myślnikiem lub podkreśleniem. Zwróć też uwagę na format email.",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Wprowadziłeś złe hasło lub kod zbyt wiele razy, poczekaj %d minut i spróbuj ponownie",
"Your IP address: %s has been banned according to the configuration of: ": "Twój adres IP: %s został zablokowany zgodnie z konfiguracją: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Twoje hasło wygasło. Zresetuj hasło klikając \\\"Zapomniałem hasła\\\"",
"Your password has expired. Please reset your password by clicking \"Forgot password\"": "Twoje hasło wygasło. Zresetuj hasło klikając \"Zapomniałem hasła\"",
"Your region is not allow to signup by phone": "Twój region nie pozwala na rejestrację przez telefon",
"password or code is incorrect": "hasło lub kod jest nieprawidłowe",
"password or code is incorrect, you have %s remaining chances": "hasło lub kod jest nieprawidłowe, masz jeszcze %s prób",
@@ -137,7 +137,7 @@
"adding a new user to the 'built-in' organization is currently disabled. Please note: all users in the 'built-in' organization are global administrators in Casdoor. Refer to the docs: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. If you still wish to create a user for the 'built-in' organization, go to the organization's settings page and enable the 'Has privilege consent' option.": "Dodawanie nowego użytkownika do organizacji „built-in' (wbudowanej) jest obecnie wyłączone. Należy zauważyć, że wszyscy użytkownicy w organizacji „built-in' są globalnymi administratorami w Casdoor. Zobacz dokumentację: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. Jeśli nadal chcesz utworzyć użytkownika dla organizacji „built-in', przejdź do strony ustawień organizacji i włącz opcję „Ma zgodę na uprawnienia'."
},
"permission": {
"The permission: \\\"%s\\\" doesn't exist": "Uprawnienie: \\\"%s\\\" nie istnieje"
"The permission: \"%s\" doesn't exist": "Uprawnienie: \"%s\" nie istnieje"
},
"provider": {
"Invalid application id": "Nieprawidłowe id aplikacji",
@@ -196,8 +196,8 @@
"Unknown type": "Nieznany typ",
"Wrong verification code!": "Zły kod weryfikacyjny!",
"You should verify your code in %d min!": "Powinieneś zweryfikować swój kod w ciągu %d min!",
"please add a SMS provider to the \\\"Providers\\\" list for the application: %s": "proszę dodać dostawcę SMS do listy \\\"Providers\\\" dla aplikacji: %s",
"please add an Email provider to the \\\"Providers\\\" list for the application: %s": "proszę dodać dostawcę email do listy \\\"Providers\\\" dla aplikacji: %s",
"please add a SMS provider to the \"Providers\" list for the application: %s": "proszę dodać dostawcę SMS do listy \"Providers\" dla aplikacji: %s",
"please add an Email provider to the \"Providers\" list for the application: %s": "proszę dodać dostawcę email do listy \"Providers\" dla aplikacji: %s",
"the user does not exist, please sign up first": "użytkownik nie istnieje, najpierw się zarejestruj"
},
"webauthn": {

View File

@@ -83,8 +83,8 @@
"The user is forbidden to sign in, please contact the administrator": "O usuário está proibido de entrar, entre em contato com o administrador",
"The user: %s doesn't exist in LDAP server": "O usuário: %s não existe no servidor 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.": "O nome de usuário pode conter apenas caracteres alfanuméricos, sublinhados ou hífens, não pode ter hífens ou sublinhados consecutivos e não pode começar ou terminar com hífen ou sublinhado.",
"The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex": "O valor \\\"%s\\\" para o campo de conta \\\"%s\\\" não corresponde à expressão regular definida",
"The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"": "O valor \\\"%s\\\" para o campo de registro \\\"%s\\\" não corresponde à expressão regular definida na aplicação \\\"%s\\\"",
"The value \"%s\" for account field \"%s\" doesn't match the account item regex": "O valor \"%s\" para o campo de conta \"%s\" não corresponde à expressão regular definida",
"The value \"%s\" for signup field \"%s\" doesn't match the signup item regex of the application \"%s\"": "O valor \"%s\" para o campo de registro \"%s\" não corresponde à expressão regular definida na aplicação \"%s\"",
"Username already exists": "O nome de usuário já existe",
"Username cannot be an email address": "O nome de usuário não pode ser um endereço de e-mail",
"Username cannot contain white spaces": "O nome de usuário não pode conter espaços em branco",
@@ -94,7 +94,7 @@
"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.": "O nome de usuário suporta o formato de e-mail. Além disso, pode conter apenas caracteres alfanuméricos, sublinhados ou hífens, não pode ter hífens ou sublinhados consecutivos e não pode começar ou terminar com hífen ou sublinhado. Também preste atenção ao formato do e-mail.",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Você digitou a senha ou o código incorretos muitas vezes. Aguarde %d minutos e tente novamente",
"Your IP address: %s has been banned according to the configuration of: ": "Seu endereço IP: %s foi bloqueado de acordo com a configuração de: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Sua senha expirou. Por favor, redefina-a clicando em \\\"Esqueci a senha\\\"",
"Your password has expired. Please reset your password by clicking \"Forgot password\"": "Sua senha expirou. Por favor, redefina-a clicando em \"Esqueci a senha\"",
"Your region is not allow to signup by phone": "Sua região não permite cadastro por telefone",
"password or code is incorrect": "Senha ou código incorreto",
"password or code is incorrect, you have %s remaining chances": "Senha ou código incorreto, você tem %s tentativas restantes",
@@ -137,7 +137,7 @@
"adding a new user to the 'built-in' organization is currently disabled. Please note: all users in the 'built-in' organization are global administrators in Casdoor. Refer to the docs: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. If you still wish to create a user for the 'built-in' organization, go to the organization's settings page and enable the 'Has privilege consent' option.": "A adição de novos usuários à organização 'built-in' está desativada. Observe que todos os usuários nessa organização são administradores globais no Casdoor. Consulte a documentação: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. Se ainda desejar criar um usuário, vá até a página de configurações da organização e habilite a opção 'Possui consentimento de privilégios'."
},
"permission": {
"The permission: \\\"%s\\\" doesn't exist": "A permissão: \\\"%s\\\" não existe"
"The permission: \"%s\" doesn't exist": "A permissão: \"%s\" não existe"
},
"provider": {
"Invalid application id": "ID de aplicativo inválido",
@@ -196,8 +196,8 @@
"Unknown type": "Tipo desconhecido",
"Wrong verification code!": "Código de verificação incorreto!",
"You should verify your code in %d min!": "Você deve verificar seu código em %d minuto(s)!",
"please add a SMS provider to the \\\"Providers\\\" list for the application: %s": "Adicione um provedor de SMS à lista \\\"Providers\\\" do aplicativo: %s",
"please add an Email provider to the \\\"Providers\\\" list for the application: %s": "Adicione um provedor de e-mail à lista \\\"Providers\\\" do aplicativo: %s",
"please add a SMS provider to the \"Providers\" list for the application: %s": "Adicione um provedor de SMS à lista \"Providers\" do aplicativo: %s",
"please add an Email provider to the \"Providers\" list for the application: %s": "Adicione um provedor de e-mail à lista \"Providers\" do aplicativo: %s",
"the user does not exist, please sign up first": "O usuário não existe, cadastre-se primeiro"
},
"webauthn": {

View File

@@ -83,8 +83,8 @@
"The user is forbidden to sign in, please contact the administrator": "Kullanıcı giriş yapmaktan men edildi, lütfen yönetici ile iletişime geçin",
"The user: %s doesn't exist in LDAP server": "Kullanıcı: %s LDAP sunucusunda mevcut değil",
"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.": "Kullanıcı adı yalnızca alfanümerik karakterler, alt çizgi veya tire içerebilir, ardışık tire veya alt çizgi içeremez ve tire veya alt çizgi ile başlayamaz veya bitemez.",
"The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex": "Hesap alanı \\\"%s\\\" için \\\"%s\\\" değeri, hesap öğesi regex'iyle eşleşmiyor",
"The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"": "Kayıt alanı \\\"%s\\\" için \\\"%s\\\" değeri, \\\"%s\\\" uygulamasının kayıt öğesi regex'iyle eşleşmiyor",
"The value \"%s\" for account field \"%s\" doesn't match the account item regex": "Hesap alanı \"%s\" için \"%s\" değeri, hesap öğesi regex'iyle eşleşmiyor",
"The value \"%s\" for signup field \"%s\" doesn't match the signup item regex of the application \"%s\"": "Kayıt alanı \"%s\" için \"%s\" değeri, \"%s\" uygulamasının kayıt öğesi regex'iyle eşleşmiyor",
"Username already exists": "Kullanıcı adı zaten var",
"Username cannot be an email address": "Kullanıcı adı e-posta adresi olamaz",
"Username cannot contain white spaces": "Kullanıcı adı boşluk içeremez",
@@ -94,7 +94,7 @@
"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.": "Kullanıcı adı e-posta biçimini destekler. Ayrıca kullanıcı adı yalnızca alfanümerik karakterler, alt çizgiler veya tireler içerebilir, ardışık tireler veya alt çizgiler olamaz ve tire veya alt çizgi ile başlayıp bitemez. Ayrıca e-posta biçimine dikkat edin.",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Çok fazla hatalı şifre veya kod girdiniz, lütfen %d dakika bekleyin ve tekrar deneyin",
"Your IP address: %s has been banned according to the configuration of: ": "IP adresiniz: %s, yapılandırmaya göre yasaklandı: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Şifrenizin süresi doldu. Lütfen \\\"Şifremi unuttum\\\"a tıklayarak şifrenizi sıfırlayın",
"Your password has expired. Please reset your password by clicking \"Forgot password\"": "Şifrenizin süresi doldu. Lütfen \"Şifremi unuttum\"a tıklayarak şifrenizi sıfırlayın",
"Your region is not allow to signup by phone": "Bölgeniz telefonla kayıt yapmaya izin verilmiyor",
"password or code is incorrect": "şifre veya kod yanlış",
"password or code is incorrect, you have %s remaining chances": "şifre veya kod yanlış, %s hakkınız kaldı",
@@ -137,7 +137,7 @@
"adding a new user to the 'built-in' organization is currently disabled. Please note: all users in the 'built-in' organization are global administrators in Casdoor. Refer to the docs: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. If you still wish to create a user for the 'built-in' organization, go to the organization's settings page and enable the 'Has privilege consent' option.": "'built-in' (yerleşik) organizasyona yeni bir kullanıcı ekleme şu anda devre dışı bırakılmıştır. Not: 'built-in' organizasyonundaki tüm kullanıcılar Casdoor'da genel yöneticilerdir. Belgelere bakın: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. Hala 'built-in' organizasyonu için bir kullanıcı oluşturmak isterseniz, organizasyonun ayarları sayfasına gidin ve 'Yetki onayı var' seçeneğini etkinleştirin."
},
"permission": {
"The permission: \\\"%s\\\" doesn't exist": "İzin: \\\"%s\\\" mevcut değil"
"The permission: \"%s\" doesn't exist": "İzin: \"%s\" mevcut değil"
},
"provider": {
"Invalid application id": "Geçersiz uygulama id",
@@ -196,8 +196,8 @@
"Unknown type": "Bilinmeyen tür",
"Wrong verification code!": "Yanlış doğrulama kodu!",
"You should verify your code in %d min!": "Kodunuzu %d dakika içinde doğrulamalısınız!",
"please add a SMS provider to the \\\"Providers\\\" list for the application: %s": "lütfen uygulama için \\\"Sağlayıcılar\\\" listesine bir SMS sağlayıcı ekleyin: %s",
"please add an Email provider to the \\\"Providers\\\" list for the application: %s": "lütfen uygulama için \\\"Sağlayıcılar\\\" listesine bir E-posta sağlayıcı ekleyin: %s",
"please add a SMS provider to the \"Providers\" list for the application: %s": "lütfen uygulama için \"Sağlayıcılar\" listesine bir SMS sağlayıcı ekleyin: %s",
"please add an Email provider to the \"Providers\" list for the application: %s": "lütfen uygulama için \"Sağlayıcılar\" listesine bir E-posta sağlayıcı ekleyin: %s",
"the user does not exist, please sign up first": "kullanıcı mevcut değil, lütfen önce kaydolun"
},
"webauthn": {

View File

@@ -83,8 +83,8 @@
"The user is forbidden to sign in, please contact the administrator": "Користувачу заборонено вхід, зверніться до адміністратора",
"The user: %s doesn't exist in LDAP server": "Користувач: %s не існує на сервері LDAP",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "Ім’я користувача може містити лише буквено-цифрові символи, підкреслення або дефіси, не може мати послідовні дефіси або підкреслення та не може починатися або закінчуватися дефісом або підкресленням.",
"The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex": "Värdet \\\"%s\\\" för kontofältet \\\"%s\\\" matchar inte kontots regex",
"The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"": "Värdet \\\"%s\\\" för registreringsfältet \\\"%s\\\" matchar inte registreringsfältets regex för applikationen \\\"%s\\\"",
"The value \"%s\" for account field \"%s\" doesn't match the account item regex": "Värdet \"%s\" för kontofältet \"%s\" matchar inte kontots regex",
"The value \"%s\" for signup field \"%s\" doesn't match the signup item regex of the application \"%s\"": "Värdet \"%s\" för registreringsfältet \"%s\" matchar inte registreringsfältets regex för applikationen \"%s\"",
"Username already exists": "Ім’я користувача вже існує",
"Username cannot be an email address": "Ім’я користувача не може бути email-адресою",
"Username cannot contain white spaces": "Ім’я користувача не може містити пробіли",
@@ -94,7 +94,7 @@
"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.": "Ім’я користувача підтримує формат email. Також може містити лише буквено-цифрові символи, підкреслення або дефіси, не може мати послідовні дефіси або підкреслення та не може починатися або закінчуватися дефісом або підкресленням. Зверніть увагу на формат email.",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Ви ввели неправильний пароль або код забагато разів, зачекайте %d хвилин і спробуйте знову",
"Your IP address: %s has been banned according to the configuration of: ": "Ваша IP-адреса: %s заблокована відповідно до конфігурації: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Ditt lösenord har gått ut. Återställ det genom att klicka på \\\"Glömt lösenord\\\"",
"Your password has expired. Please reset your password by clicking \"Forgot password\"": "Ditt lösenord har gått ut. Återställ det genom att klicka på \"Glömt lösenord\"",
"Your region is not allow to signup by phone": "У вашому регіоні реєстрація за телефоном недоступна",
"password or code is incorrect": "пароль або код неправильний",
"password or code is incorrect, you have %s remaining chances": "пароль або код неправильний, у вас залишилось %s спроб",
@@ -137,7 +137,7 @@
"adding a new user to the 'built-in' organization is currently disabled. Please note: all users in the 'built-in' organization are global administrators in Casdoor. Refer to the docs: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. If you still wish to create a user for the 'built-in' organization, go to the organization's settings page and enable the 'Has privilege consent' option.": "Додавання нового користувача до організації «built-in» (вбудованої) на даний момент вимкнено. Зауважте: усі користувачі в організації «built-in» є глобальними адміністраторами в Casdoor. Дивіться документацію: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. Якщо ви все ще хочете створити користувача для організації «built-in», перейдіть на сторінку налаштувань організації та увімкніть опцію «Має згоду на привілеї»."
},
"permission": {
"The permission: \\\"%s\\\" doesn't exist": "Behörigheten: \\\"%s\\\" finns inte"
"The permission: \"%s\" doesn't exist": "Behörigheten: \"%s\" finns inte"
},
"provider": {
"Invalid application id": "Недійсний id додатка",
@@ -196,8 +196,8 @@
"Unknown type": "Невідомий тип",
"Wrong verification code!": "Неправильний код підтвердження!",
"You should verify your code in %d min!": "Ви маєте підтвердити код за %d хв!",
"please add a SMS provider to the \\\"Providers\\\" list for the application: %s": "lägg till en SMS-leverantör i listan \\\"Leverantörer\\\" för applikationen: %s",
"please add an Email provider to the \\\"Providers\\\" list for the application: %s": "lägg till en e-postleverantör i listan \\\"Leverantörer\\\" för applikationen: %s",
"please add a SMS provider to the \"Providers\" list for the application: %s": "lägg till en SMS-leverantör i listan \"Leverantörer\" för applikationen: %s",
"please add an Email provider to the \"Providers\" list for the application: %s": "lägg till en e-postleverantör i listan \"Leverantörer\" för applikationen: %s",
"the user does not exist, please sign up first": "користувача не існує, спочатку зареєструйтесь"
},
"webauthn": {

View File

@@ -83,8 +83,8 @@
"The user is forbidden to sign in, please contact the administrator": "Người dùng bị cấm đăng nhập, vui lòng liên hệ với quản trị viên",
"The user: %s doesn't exist in LDAP server": "Người dùng: %s không tồn tại trên máy chủ 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.": "Tên người dùng chỉ có thể chứa các ký tự chữ và số, gạch dưới hoặc gạch ngang, không được có hai ký tự gạch dưới hoặc gạch ngang liền kề và không được bắt đầu hoặc kết thúc bằng dấu gạch dưới hoặc gạch ngang.",
"The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex": "Giá trị \\\"%s\\\" cho trường tài khoản \\\"%s\\\" không khớp với biểu thức chính quy của mục tài khoản",
"The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"": "Giá trị \\\"%s\\\" cho trường đăng ký \\\"%s\\\" không khớp với biểu thức chính quy của mục đăng ký trong ứng dụng \\\"%s\\\"",
"The value \"%s\" for account field \"%s\" doesn't match the account item regex": "Giá trị \"%s\" cho trường tài khoản \"%s\" không khớp với biểu thức chính quy của mục tài khoản",
"The value \"%s\" for signup field \"%s\" doesn't match the signup item regex of the application \"%s\"": "Giá trị \"%s\" cho trường đăng ký \"%s\" không khớp với biểu thức chính quy của mục đăng ký trong ứng dụng \"%s\"",
"Username already exists": "Tên đăng nhập đã tồn tại",
"Username cannot be an email address": "Tên người dùng không thể là địa chỉ email",
"Username cannot contain white spaces": "Tên người dùng không thể chứa khoảng trắng",
@@ -94,7 +94,7 @@
"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.": "Tên người dùng hỗ trợ định dạng email. Ngoài ra, tên người dùng chỉ có thể chứa ký tự chữ và số, gạch dưới hoặc gạch ngang, không được có gạch ngang hoặc gạch dưới liên tiếp và không được bắt đầu hoặc kết thúc bằng gạch ngang hoặc gạch dưới. Đồng thời lưu ý định dạng email.",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Bạn đã nhập sai mật khẩu hoặc mã quá nhiều lần, vui lòng đợi %d phút và thử lại",
"Your IP address: %s has been banned according to the configuration of: ": "Địa chỉ IP của bạn: %s đã bị cấm theo cấu hình của: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Mật khẩu của bạn đã hết hạn. Vui lòng đặt lại mật khẩu bằng cách nhấp vào \\\"Quên mật khẩu\\\"",
"Your password has expired. Please reset your password by clicking \"Forgot password\"": "Mật khẩu của bạn đã hết hạn. Vui lòng đặt lại mật khẩu bằng cách nhấp vào \"Quên mật khẩu\"",
"Your region is not allow to signup by phone": "Vùng của bạn không được phép đăng ký bằng điện thoại",
"password or code is incorrect": "mật khẩu hoặc mã không đúng",
"password or code is incorrect, you have %s remaining chances": "Mật khẩu hoặc mã không chính xác, bạn còn %s lần cơ hội",
@@ -137,7 +137,7 @@
"adding a new user to the 'built-in' organization is currently disabled. Please note: all users in the 'built-in' organization are global administrators in Casdoor. Refer to the docs: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. If you still wish to create a user for the 'built-in' organization, go to the organization's settings page and enable the 'Has privilege consent' option.": "Thêm người dùng mới vào tổ chức 'built-in' (tích hợp sẵn) hiện đang bị vô hiệu hóa. Lưu ý: Tất cả người dùng trong tổ chức 'built-in' đều là quản trị viên toàn cầu trong Casdoor. Xem tài liệu: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. Nếu bạn vẫn muốn tạo người dùng cho tổ chức 'built-in', hãy truy cập trang cài đặt của tổ chức và bật tùy chọn 'Có đồng ý quyền đặc biệt'."
},
"permission": {
"The permission: \\\"%s\\\" doesn't exist": "Quyền: \\\"%s\\\" không tồn tại"
"The permission: \"%s\" doesn't exist": "Quyền: \"%s\" không tồn tại"
},
"provider": {
"Invalid application id": "Sai ID ứng dụng",
@@ -196,8 +196,8 @@
"Unknown type": "Loại không xác định",
"Wrong verification code!": "Mã xác thực sai!",
"You should verify your code in %d min!": "Bạn nên kiểm tra mã của mình trong %d phút!",
"please add a SMS provider to the \\\"Providers\\\" list for the application: %s": "vui lòng thêm nhà cung cấp SMS vào danh sách \\\"Providers\\\" cho ứng dụng: %s",
"please add an Email provider to the \\\"Providers\\\" list for the application: %s": "vui lòng thêm nhà cung cấp Email vào danh sách \\\"Providers\\\" cho ứng dụng: %s",
"please add a SMS provider to the \"Providers\" list for the application: %s": "vui lòng thêm nhà cung cấp SMS vào danh sách \"Providers\" cho ứng dụng: %s",
"please add an Email provider to the \"Providers\" list for the application: %s": "vui lòng thêm nhà cung cấp Email vào danh sách \"Providers\" cho ứng dụng: %s",
"the user does not exist, please sign up first": "Người dùng không tồn tại, vui lòng đăng ký trước"
},
"webauthn": {

View File

@@ -83,8 +83,8 @@
"The user is forbidden to sign in, please contact the administrator": "该用户被禁止登录,请联系管理员",
"The user: %s doesn't exist in LDAP server": "用户: %s 在LDAP服务器中未找到",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "用户名只能包含字母数字字符、下划线或连字符,不能有连续的连字符或下划线,也不能以连字符或下划线开头或结尾",
"The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex": "值 \\\"%s\\\"在账户信息字段\\\"%s\\\" 中与应用的账户项正则表达式不匹配",
"The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"": "值\\\"%s\\\"在注册字段\\\"%s\\\"中与应用\\\"%s\\\"的注册项正则表达式不匹配",
"The value \"%s\" for account field \"%s\" doesn't match the account item regex": "值 \"%s\"在账户信息字段\"%s\" 中与应用的账户项正则表达式不匹配",
"The value \"%s\" for signup field \"%s\" doesn't match the signup item regex of the application \"%s\"": "值\"%s\"在注册字段\"%s\"中与应用\"%s\"的注册项正则表达式不匹配",
"Username already exists": "用户名已存在",
"Username cannot be an email address": "用户名不可以是邮箱地址",
"Username cannot contain white spaces": "用户名禁止包含空格",
@@ -94,7 +94,7 @@
"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.": "用户名支持电子邮件格式。此外,用户名只能包含字母数字字符、下划线或连字符,不能包含连续的连字符或下划线,也不能以连字符或下划线开头或结尾。同时请注意电子邮件格式。",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "密码错误次数已达上限,请在 %d 分后重试",
"Your IP address: %s has been banned according to the configuration of: ": "您的IP地址%s 根据以下配置已被禁止: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "您的密码已过期。请点击 \\\"忘记密码\\\" 以重置密码",
"Your password has expired. Please reset your password by clicking \"Forgot password\"": "您的密码已过期。请点击 \"忘记密码\" 以重置密码",
"Your region is not allow to signup by phone": "所在地区不支持手机号注册",
"password or code is incorrect": "密码错误",
"password or code is incorrect, you have %s remaining chances": "密码错误,您还有 %s 次尝试的机会",
@@ -137,7 +137,7 @@
"adding a new user to the 'built-in' organization is currently disabled. Please note: all users in the 'built-in' organization are global administrators in Casdoor. Refer to the docs: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. If you still wish to create a user for the 'built-in' organization, go to the organization's settings page and enable the 'Has privilege consent' option.": "目前,向'built-in'组织添加新用户的功能已禁用。请注意:'built-in'组织中的所有用户均为Casdoor的全局管理员。请参阅文档https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself。如果您仍希望为built-in组织创建用户请转到该组织的设置页面并启用“特权同意”选项。"
},
"permission": {
"The permission: \\\"%s\\\" doesn't exist": "权限: \\\"%s\\\" 不存在"
"The permission: \"%s\" doesn't exist": "权限: \"%s\" 不存在"
},
"provider": {
"Invalid application id": "无效的应用ID",
@@ -196,8 +196,8 @@
"Unknown type": "未知类型",
"Wrong verification code!": "验证码错误!",
"You should verify your code in %d min!": "请在 %d 分钟内输入正确验证码",
"please add a SMS provider to the \\\"Providers\\\" list for the application: %s": "请添加一个SMS提供商到应用 %s 的 \\\"提供商 \\\"列表",
"please add an Email provider to the \\\"Providers\\\" list for the application: %s": "请添加一个Email提供商到应用 %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": "请添加一个Email提供商到应用 %s 的 \"提供商 \"列表",
"the user does not exist, please sign up first": "用户不存在,请先注册"
},
"webauthn": {

View File

@@ -31,11 +31,12 @@ type CustomIdProvider struct {
Client *http.Client
Config *oauth2.Config
UserInfoURL string
TokenURL string
AuthURL string
UserMapping map[string]string
Scopes []string
UserInfoURL string
TokenURL string
AuthURL string
UserMapping map[string]string
Scopes []string
CodeVerifier string
}
func NewCustomIdProvider(idpInfo *ProviderInfo, redirectUrl string) *CustomIdProvider {
@@ -53,6 +54,7 @@ func NewCustomIdProvider(idpInfo *ProviderInfo, redirectUrl string) *CustomIdPro
idp.UserInfoURL = idpInfo.UserInfoURL
idp.UserMapping = idpInfo.UserMapping
idp.CodeVerifier = idpInfo.CodeVerifier
return idp
}
@@ -62,7 +64,11 @@ func (idp *CustomIdProvider) SetHttpClient(client *http.Client) {
func (idp *CustomIdProvider) GetToken(code string) (*oauth2.Token, error) {
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, idp.Client)
return idp.Config.Exchange(ctx, code)
var oauth2Opts []oauth2.AuthCodeOption
if idp.CodeVerifier != "" {
oauth2Opts = append(oauth2Opts, oauth2.VerifierOption(idp.CodeVerifier))
}
return idp.Config.Exchange(ctx, code, oauth2Opts...)
}
func getNestedValue(data map[string]interface{}, path string) (interface{}, error) {

View File

@@ -87,8 +87,9 @@ import (
)
type GothIdProvider struct {
Provider goth.Provider
Session goth.Session
Provider goth.Provider
Session goth.Session
CodeVerifier string
}
func NewGothIdProvider(providerType string, clientId string, clientSecret string, clientId2 string, clientSecret2 string, redirectUrl string, hostUrl string) (*GothIdProvider, error) {
@@ -448,7 +449,13 @@ func (idp *GothIdProvider) GetToken(code string) (*oauth2.Token, error) {
value = url.Values{}
value.Add("code", code)
if idp.Provider.Name() == "twitterv2" || idp.Provider.Name() == "fitbit" {
value.Add("oauth_verifier", "casdoor-verifier")
// Use dynamic code verifier if provided, otherwise fall back to static one
verifier := idp.CodeVerifier
if verifier == "" {
verifier = "casdoor-verifier"
}
// RFC 7636 PKCE uses 'code_verifier' parameter
value.Add("code_verifier", verifier)
}
}
accessToken, err := idp.Session.Authorize(idp.Provider, value)

View File

@@ -45,6 +45,7 @@ type ProviderInfo struct {
HostUrl string
RedirectUrl string
DisableSsl bool
CodeVerifier string
TokenURL string
AuthURL string
@@ -128,7 +129,9 @@ func GetIdProvider(idpInfo *ProviderInfo, redirectUrl string) (IdProvider, error
case "Web3Onboard":
return NewWeb3OnboardIdProvider(), nil
case "Twitter":
return NewTwitterIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
provider := NewTwitterIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
provider.CodeVerifier = idpInfo.CodeVerifier
return provider, nil
case "Telegram":
return NewTelegramIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
default:

View File

@@ -28,8 +28,9 @@ import (
)
type TwitterIdProvider struct {
Client *http.Client
Config *oauth2.Config
Client *http.Client
Config *oauth2.Config
CodeVerifier string
}
func NewTwitterIdProvider(clientId string, clientSecret string, redirectUrl string) *TwitterIdProvider {
@@ -84,7 +85,12 @@ func (idp *TwitterIdProvider) GetToken(code string) (*oauth2.Token, error) {
params := url.Values{}
// params.Add("client_id", idp.Config.ClientID)
params.Add("redirect_uri", idp.Config.RedirectURL)
params.Add("code_verifier", "casdoor-verifier")
// Use dynamic code verifier if provided, otherwise fall back to static one
verifier := idp.CodeVerifier
if verifier == "" {
verifier = "casdoor-verifier"
}
params.Add("code_verifier", verifier)
params.Add("code", code)
params.Add("grant_type", "authorization_code")
req, err := http.NewRequest("POST", "https://api.twitter.com/2/oauth2/token", strings.NewReader(params.Encode()))

View File

@@ -75,6 +75,8 @@
{"name": "Country code", "visible": true, "viewRule": "Public", "modifyRule": "Admin"},
{"name": "Country/Region", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "Location", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "Address", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "Addresses", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "Affiliation", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "Title", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "ID card type", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
@@ -236,6 +238,7 @@
"phone": "",
"countryCode": "",
"address": [],
"addresses": [],
"affiliation": "",
"tag": "",
"score": 2000,

View File

@@ -216,6 +216,16 @@ func handleSearch(w ldap.ResponseWriter, m *ldap.Message) {
e.AddAttribute("mobile", message.AttributeValue(user.Phone))
e.AddAttribute("sn", message.AttributeValue(user.LastName))
e.AddAttribute("givenName", message.AttributeValue(user.FirstName))
// Add POSIX attributes for Linux machine login support
e.AddAttribute("loginShell", getAttribute("loginShell", user))
e.AddAttribute("gecos", getAttribute("gecos", user))
// Add SSH public key if available
sshKey := getAttribute("sshPublicKey", user)
if sshKey != "" {
e.AddAttribute("sshPublicKey", sshKey)
}
// Add objectClass for posixAccount
e.AddAttribute("objectClass", "posixAccount")
for _, group := range user.Groups {
e.AddAttribute(ldapMemberOfAttr, message.AttributeValue(group))
}

View File

@@ -83,6 +83,45 @@ var ldapAttributesMapping = map[string]FieldRelation{
return message.AttributeValue(getUserPasswordWithType(user))
},
},
"loginShell": {
userField: "loginShell",
notSearchable: true,
fieldMapper: func(user *object.User) message.AttributeValue {
// Check user properties first, otherwise return default shell
if user.Properties != nil {
if shell, ok := user.Properties["loginShell"]; ok && shell != "" {
return message.AttributeValue(shell)
}
}
return message.AttributeValue("/bin/bash")
},
},
"gecos": {
userField: "gecos",
notSearchable: true,
fieldMapper: func(user *object.User) message.AttributeValue {
// GECOS field typically contains full name and other user info
// Format: Full Name,Room Number,Work Phone,Home Phone,Other
gecos := user.DisplayName
if gecos == "" {
gecos = user.Name
}
return message.AttributeValue(gecos)
},
},
"sshPublicKey": {
userField: "sshPublicKey",
notSearchable: true,
fieldMapper: func(user *object.User) message.AttributeValue {
// Return SSH public key from user properties
if user.Properties != nil {
if sshKey, ok := user.Properties["sshPublicKey"]; ok && sshKey != "" {
return message.AttributeValue(sshKey)
}
}
return message.AttributeValue("")
},
},
}
const ldapMemberOfAttr = "memberOf"

View File

@@ -61,6 +61,8 @@ func getBuiltInAccountItems() []*AccountItem {
{Name: "Country code", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "Country/Region", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Location", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Address", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Addresses", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Affiliation", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Title", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "ID card type", Visible: true, ViewRule: "Public", ModifyRule: "Self"},

View File

@@ -26,10 +26,35 @@ import (
"github.com/casdoor/casdoor/i18n"
"github.com/casdoor/casdoor/util"
goldap "github.com/go-ldap/ldap/v3"
"github.com/nyaruka/phonenumbers"
"github.com/thanhpk/randstr"
"golang.org/x/text/encoding/unicode"
)
// formatUserPhone processes phone number for a user based on their CountryCode
func formatUserPhone(u *User) {
if u.Phone == "" {
return
}
// 1. Normalize hint (e.g., "China" -> "CN") for the parser
countryHint := u.CountryCode
if strings.EqualFold(countryHint, "China") {
countryHint = "CN"
}
if len(countryHint) != 2 {
countryHint = "" // Only 2-letter codes are valid hints
}
// 2. Try parsing (Strictly using countryHint from LDAP)
num, err := phonenumbers.Parse(u.Phone, countryHint)
if err == nil && num != nil && phonenumbers.IsValidNumber(num) {
// Store a clean national number (digits only, without country prefix)
u.Phone = fmt.Sprint(num.GetNationalNumber())
}
}
type LdapConn struct {
Conn *goldap.Conn
IsAD bool
@@ -289,6 +314,7 @@ func AutoAdjustLdapUser(users []LdapUser) []LdapUser {
Mobile: util.ReturnAnyNotEmpty(user.Mobile, user.MobileTelephoneNumber, user.TelephoneNumber),
Address: util.ReturnAnyNotEmpty(user.Address, user.PostalAddress, user.RegisteredAddress),
Country: util.ReturnAnyNotEmpty(user.Country, user.CountryName),
CountryName: user.CountryName,
Attributes: user.Attributes,
}
}
@@ -361,6 +387,7 @@ func SyncLdapUsers(owner string, syncUsers []LdapUser, ldapId string) (existUser
Avatar: organization.DefaultAvatar,
Email: syncUser.Email,
Phone: syncUser.Mobile,
CountryCode: syncUser.Country,
Address: []string{syncUser.Address},
Region: util.ReturnAnyNotEmpty(syncUser.Country, syncUser.CountryName),
Affiliation: affiliation,
@@ -369,6 +396,7 @@ func SyncLdapUsers(owner string, syncUsers []LdapUser, ldapId string) (existUser
Ldap: syncUser.Uuid,
Properties: syncUser.Attributes,
}
formatUserPhone(newUser)
if ldap.DefaultGroup != "" {
newUser.Groups = []string{ldap.DefaultGroup}

View File

@@ -32,6 +32,7 @@ type AccountItem struct {
ViewRule string `json:"viewRule"`
ModifyRule string `json:"modifyRule"`
Regex string `json:"regex"`
Tab string `json:"tab"`
}
type ThemeData struct {
@@ -88,6 +89,7 @@ type Organization struct {
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
MfaRememberInHours int `json:"mfaRememberInHours"`
AccountMenu string `xorm:"varchar(20)" json:"accountMenu"`
AccountItems []*AccountItem `xorm:"mediumtext" json:"accountItems"`
OrgBalance float64 `json:"orgBalance"`

View File

@@ -50,7 +50,8 @@ type Payment struct {
InvoiceRemark string `xorm:"varchar(100)" json:"invoiceRemark"`
InvoiceUrl string `xorm:"varchar(255)" json:"invoiceUrl"`
// Order Info
Order string `xorm:"varchar(100)" json:"order"` // Internal order name
Order string `xorm:"varchar(100)" json:"order"` // Internal order name
OrderObj *Order `xorm:"-" json:"orderObj,omitempty"`
OutOrderId string `xorm:"varchar(100)" json:"outOrderId"` // External payment provider's order ID
PayUrl string `xorm:"varchar(2000)" json:"payUrl"`
SuccessUrl string `xorm:"varchar(2000)" json:"successUrl"` // `successUrl` is redirected from `payUrl` after pay success
@@ -70,6 +71,11 @@ func GetPayments(owner string) ([]*Payment, error) {
return nil, err
}
err = ExtendPaymentWithOrder(payments)
if err != nil {
return nil, err
}
return payments, nil
}
@@ -80,6 +86,11 @@ func GetUserPayments(owner, user string) ([]*Payment, error) {
return nil, err
}
err = ExtendPaymentWithOrder(payments)
if err != nil {
return nil, err
}
return payments, nil
}
@@ -91,9 +102,49 @@ func GetPaginationPayments(owner string, offset, limit int, field, value, sortFi
return nil, err
}
err = ExtendPaymentWithOrder(payments)
if err != nil {
return nil, err
}
return payments, nil
}
func ExtendPaymentWithOrder(payments []*Payment) error {
ownerOrdersMap := make(map[string][]string)
for _, payment := range payments {
if payment.Order != "" {
ownerOrdersMap[payment.Owner] = append(ownerOrdersMap[payment.Owner], payment.Order)
}
}
ordersMap := make(map[string]*Order)
for owner, orderNames := range ownerOrdersMap {
if len(orderNames) == 0 {
continue
}
var orders []*Order
err := ormer.Engine.In("name", orderNames).Find(&orders, &Order{Owner: owner})
if err != nil {
return err
}
for _, order := range orders {
ordersMap[util.GetId(order.Owner, order.Name)] = order
}
}
for _, payment := range payments {
if payment.Order != "" {
orderId := util.GetId(payment.Owner, payment.Order)
if order, ok := ordersMap[orderId]; ok {
payment.OrderObj = order
}
}
}
return nil
}
func getPayment(owner string, name string) (*Payment, error) {
if owner == "" || name == "" {
return nil, nil
@@ -210,18 +261,29 @@ func NotifyPayment(body []byte, owner string, paymentName string, lang string) (
}
// Check if payment is already in a terminal state to prevent duplicate processing
if payment.State == pp.PaymentStatePaid || payment.State == pp.PaymentStateError ||
payment.State == pp.PaymentStateCanceled || payment.State == pp.PaymentStateTimeout {
if pp.IsTerminalState(payment.State) {
return payment, nil
}
// Determine the new payment state
var newState pp.PaymentState
var newMessage string
if err != nil {
payment.State = pp.PaymentStateError
payment.Message = err.Error()
newState = pp.PaymentStateError
newMessage = err.Error()
} else {
payment.State = notifyResult.PaymentStatus
payment.Message = notifyResult.NotifyMessage
newState = notifyResult.PaymentStatus
newMessage = notifyResult.NotifyMessage
}
// Check if the payment state would actually change
// This prevents duplicate webhook events when providers send redundant notifications
if payment.State == newState {
return payment, nil
}
payment.State = newState
payment.Message = newMessage
_, err = UpdatePayment(payment.GetId(), payment)
if err != nil {
return nil, err

View File

@@ -80,7 +80,7 @@ func NewRecord(ctx *context.Context) (*casvisorsdk.Record, error) {
return nil, err
}
if action != "buy-product" {
if action != "buy-product" && action != "notify-payment" {
resp.Data = nil
}

View File

@@ -53,6 +53,8 @@ func GetSyncerProvider(syncer *Syncer) SyncerProvider {
return &LarkSyncerProvider{Syncer: syncer}
case "Okta":
return &OktaSyncerProvider{Syncer: syncer}
case "SCIM":
return &SCIMSyncerProvider{Syncer: syncer}
case "Keycloak":
return &KeycloakSyncerProvider{
DatabaseSyncerProvider: DatabaseSyncerProvider{Syncer: syncer},

337
object/syncer_scim.go Normal file
View File

@@ -0,0 +1,337 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/casdoor/casdoor/util"
)
// SCIMSyncerProvider implements SyncerProvider for SCIM 2.0 API-based syncers
type SCIMSyncerProvider struct {
Syncer *Syncer
}
// InitAdapter initializes the SCIM syncer (no database adapter needed)
func (p *SCIMSyncerProvider) InitAdapter() error {
// SCIM syncer doesn't need database adapter
return nil
}
// GetOriginalUsers retrieves all users from SCIM API
func (p *SCIMSyncerProvider) GetOriginalUsers() ([]*OriginalUser, error) {
return p.getSCIMUsers()
}
// AddUser adds a new user to SCIM (not supported for read-only API)
func (p *SCIMSyncerProvider) AddUser(user *OriginalUser) (bool, error) {
// SCIM syncer is typically read-only
return false, fmt.Errorf("adding users to SCIM is not supported")
}
// UpdateUser updates an existing user in SCIM (not supported for read-only API)
func (p *SCIMSyncerProvider) UpdateUser(user *OriginalUser) (bool, error) {
// SCIM syncer is typically read-only
return false, fmt.Errorf("updating users in SCIM is not supported")
}
// TestConnection tests the SCIM API connection
func (p *SCIMSyncerProvider) TestConnection() error {
// Test by trying to fetch users with a limit of 1
endpoint := p.buildSCIMEndpoint()
endpoint = fmt.Sprintf("%s?startIndex=1&count=1", endpoint)
req, err := p.createSCIMRequest("GET", endpoint, nil)
if err != nil {
return err
}
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("SCIM connection test failed: status=%d, body=%s", resp.StatusCode, string(body))
}
return nil
}
// Close closes any open connections (no-op for SCIM API-based syncer)
func (p *SCIMSyncerProvider) Close() error {
// SCIM syncer doesn't maintain persistent connections
return nil
}
// SCIMName represents a SCIM user name structure
type SCIMName struct {
FamilyName string `json:"familyName"`
GivenName string `json:"givenName"`
Formatted string `json:"formatted"`
}
// SCIMEmail represents a SCIM user email structure
type SCIMEmail struct {
Value string `json:"value"`
Type string `json:"type"`
Primary bool `json:"primary"`
}
// SCIMPhoneNumber represents a SCIM user phone number structure
type SCIMPhoneNumber struct {
Value string `json:"value"`
Type string `json:"type"`
Primary bool `json:"primary"`
}
// SCIMAddress represents a SCIM user address structure
type SCIMAddress struct {
StreetAddress string `json:"streetAddress"`
Locality string `json:"locality"`
Region string `json:"region"`
PostalCode string `json:"postalCode"`
Country string `json:"country"`
Formatted string `json:"formatted"`
Type string `json:"type"`
Primary bool `json:"primary"`
}
// SCIMUser represents a SCIM 2.0 user resource
type SCIMUser struct {
ID string `json:"id"`
ExternalID string `json:"externalId"`
UserName string `json:"userName"`
Name SCIMName `json:"name"`
DisplayName string `json:"displayName"`
NickName string `json:"nickName"`
ProfileURL string `json:"profileUrl"`
Title string `json:"title"`
UserType string `json:"userType"`
PreferredLan string `json:"preferredLanguage"`
Locale string `json:"locale"`
Timezone string `json:"timezone"`
Active bool `json:"active"`
Emails []SCIMEmail `json:"emails"`
PhoneNumbers []SCIMPhoneNumber `json:"phoneNumbers"`
Addresses []SCIMAddress `json:"addresses"`
}
// SCIMListResponse represents a SCIM list response
type SCIMListResponse struct {
TotalResults int `json:"totalResults"`
ItemsPerPage int `json:"itemsPerPage"`
StartIndex int `json:"startIndex"`
Resources []*SCIMUser `json:"Resources"`
}
// buildSCIMEndpoint builds the SCIM API endpoint URL
func (p *SCIMSyncerProvider) buildSCIMEndpoint() string {
// syncer.Host should be the SCIM server URL (e.g., https://example.com/scim/v2)
host := strings.TrimSuffix(p.Syncer.Host, "/")
return fmt.Sprintf("%s/Users", host)
}
// createSCIMRequest creates an HTTP request with proper authentication
func (p *SCIMSyncerProvider) createSCIMRequest(method, url string, body io.Reader) (*http.Request, error) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, method, url, body)
if err != nil {
return nil, err
}
// Set SCIM headers
req.Header.Set("Content-Type", "application/scim+json")
req.Header.Set("Accept", "application/scim+json")
// Add authentication
// syncer.User should be the authentication token or username
// syncer.Password should be the password or API key
if p.Syncer.User != "" && p.Syncer.Password != "" {
// Try Basic Auth
req.SetBasicAuth(p.Syncer.User, p.Syncer.Password)
} else if p.Syncer.Password != "" {
// Try Bearer token (assuming password field contains the token)
req.Header.Set("Authorization", "Bearer "+p.Syncer.Password)
} else if p.Syncer.User != "" {
// Try Bearer token (assuming user field contains the token)
req.Header.Set("Authorization", "Bearer "+p.Syncer.User)
}
return req, nil
}
// getSCIMUsers retrieves all users from SCIM API with pagination
func (p *SCIMSyncerProvider) getSCIMUsers() ([]*OriginalUser, error) {
allUsers := []*SCIMUser{}
startIndex := 1
count := 100 // Fetch 100 users per page
for {
endpoint := p.buildSCIMEndpoint()
endpoint = fmt.Sprintf("%s?startIndex=%d&count=%d", endpoint, startIndex, count)
req, err := p.createSCIMRequest("GET", endpoint, nil)
if err != nil {
return nil, err
}
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("failed to get users: status=%d, body=%s", resp.StatusCode, string(body))
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var listResp SCIMListResponse
err = json.Unmarshal(body, &listResp)
if err != nil {
return nil, err
}
allUsers = append(allUsers, listResp.Resources...)
// Check if we've fetched all users
if len(allUsers) >= listResp.TotalResults {
break
}
// Move to the next page
startIndex += count
}
// Convert SCIM users to Casdoor OriginalUser
originalUsers := []*OriginalUser{}
for _, scimUser := range allUsers {
originalUser := p.scimUserToOriginalUser(scimUser)
originalUsers = append(originalUsers, originalUser)
}
return originalUsers, nil
}
// scimUserToOriginalUser converts SCIM user to Casdoor OriginalUser
func (p *SCIMSyncerProvider) scimUserToOriginalUser(scimUser *SCIMUser) *OriginalUser {
user := &OriginalUser{
Id: scimUser.ID,
ExternalId: scimUser.ExternalID,
Name: scimUser.UserName,
DisplayName: scimUser.DisplayName,
FirstName: scimUser.Name.GivenName,
LastName: scimUser.Name.FamilyName,
Title: scimUser.Title,
Language: scimUser.PreferredLan,
Address: []string{},
Properties: map[string]string{},
Groups: []string{},
}
// If display name is from name structure
if user.DisplayName == "" && scimUser.Name.Formatted != "" {
user.DisplayName = scimUser.Name.Formatted
}
// If display name is still empty, construct from first and last name
if user.DisplayName == "" && (user.FirstName != "" || user.LastName != "") {
user.DisplayName = strings.TrimSpace(fmt.Sprintf("%s %s", user.FirstName, user.LastName))
}
// Extract primary email or first email
if len(scimUser.Emails) > 0 {
for _, email := range scimUser.Emails {
if email.Primary {
user.Email = email.Value
break
}
}
// If no primary email, use the first one
if user.Email == "" && len(scimUser.Emails) > 0 {
user.Email = scimUser.Emails[0].Value
}
}
// Extract primary phone or first phone
if len(scimUser.PhoneNumbers) > 0 {
for _, phone := range scimUser.PhoneNumbers {
if phone.Primary {
user.Phone = phone.Value
break
}
}
// If no primary phone, use the first one
if user.Phone == "" && len(scimUser.PhoneNumbers) > 0 {
user.Phone = scimUser.PhoneNumbers[0].Value
}
}
// Extract primary address or first address
if len(scimUser.Addresses) > 0 {
for _, addr := range scimUser.Addresses {
if addr.Primary {
if addr.Formatted != "" {
user.Address = []string{addr.Formatted}
} else {
user.Address = []string{addr.StreetAddress, addr.Locality, addr.Region, addr.PostalCode, addr.Country}
}
user.Location = addr.Locality
user.Region = addr.Region
break
}
}
// If no primary address, use the first one
if len(user.Address) == 0 && len(scimUser.Addresses) > 0 {
addr := scimUser.Addresses[0]
if addr.Formatted != "" {
user.Address = []string{addr.Formatted}
} else {
user.Address = []string{addr.StreetAddress, addr.Locality, addr.Region, addr.PostalCode, addr.Country}
}
user.Location = addr.Locality
user.Region = addr.Region
}
}
// Set IsForbidden based on Active status
user.IsForbidden = !scimUser.Active
// Set CreatedTime to current time if not set
if user.CreatedTime == "" {
user.CreatedTime = util.GetCurrentTime()
}
return user
}

198
object/syncer_scim_test.go Normal file
View File

@@ -0,0 +1,198 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"testing"
)
func TestSCIMUserToOriginalUser(t *testing.T) {
provider := &SCIMSyncerProvider{
Syncer: &Syncer{
Host: "https://example.com/scim/v2",
User: "testuser",
Password: "testtoken",
},
}
// Test case 1: Full SCIM user with all fields
scimUser := &SCIMUser{
ID: "user-123",
ExternalID: "ext-123",
UserName: "john.doe",
DisplayName: "John Doe",
Name: SCIMName{
GivenName: "John",
FamilyName: "Doe",
Formatted: "John Doe",
},
Title: "Software Engineer",
PreferredLan: "en-US",
Active: true,
Emails: []SCIMEmail{
{Value: "john.doe@example.com", Primary: true, Type: "work"},
{Value: "john@personal.com", Primary: false, Type: "home"},
},
PhoneNumbers: []SCIMPhoneNumber{
{Value: "+1-555-1234", Primary: true, Type: "work"},
{Value: "+1-555-5678", Primary: false, Type: "mobile"},
},
Addresses: []SCIMAddress{
{
StreetAddress: "123 Main St",
Locality: "San Francisco",
Region: "CA",
PostalCode: "94102",
Country: "USA",
Formatted: "123 Main St, San Francisco, CA 94102, USA",
Primary: true,
Type: "work",
},
},
}
originalUser := provider.scimUserToOriginalUser(scimUser)
// Verify basic fields
if originalUser.Id != "user-123" {
t.Errorf("Expected Id to be 'user-123', got '%s'", originalUser.Id)
}
if originalUser.ExternalId != "ext-123" {
t.Errorf("Expected ExternalId to be 'ext-123', got '%s'", originalUser.ExternalId)
}
if originalUser.Name != "john.doe" {
t.Errorf("Expected Name to be 'john.doe', got '%s'", originalUser.Name)
}
if originalUser.DisplayName != "John Doe" {
t.Errorf("Expected DisplayName to be 'John Doe', got '%s'", originalUser.DisplayName)
}
if originalUser.FirstName != "John" {
t.Errorf("Expected FirstName to be 'John', got '%s'", originalUser.FirstName)
}
if originalUser.LastName != "Doe" {
t.Errorf("Expected LastName to be 'Doe', got '%s'", originalUser.LastName)
}
if originalUser.Title != "Software Engineer" {
t.Errorf("Expected Title to be 'Software Engineer', got '%s'", originalUser.Title)
}
if originalUser.Language != "en-US" {
t.Errorf("Expected Language to be 'en-US', got '%s'", originalUser.Language)
}
// Verify primary email is selected
if originalUser.Email != "john.doe@example.com" {
t.Errorf("Expected Email to be 'john.doe@example.com', got '%s'", originalUser.Email)
}
// Verify primary phone is selected
if originalUser.Phone != "+1-555-1234" {
t.Errorf("Expected Phone to be '+1-555-1234', got '%s'", originalUser.Phone)
}
// Verify address fields
if originalUser.Location != "San Francisco" {
t.Errorf("Expected Location to be 'San Francisco', got '%s'", originalUser.Location)
}
if originalUser.Region != "CA" {
t.Errorf("Expected Region to be 'CA', got '%s'", originalUser.Region)
}
// Verify active status is inverted to IsForbidden
if originalUser.IsForbidden != false {
t.Errorf("Expected IsForbidden to be false for active user, got %v", originalUser.IsForbidden)
}
// Test case 2: Inactive SCIM user
inactiveUser := &SCIMUser{
ID: "user-456",
UserName: "jane.doe",
Active: false,
}
inactiveOriginalUser := provider.scimUserToOriginalUser(inactiveUser)
if inactiveOriginalUser.IsForbidden != true {
t.Errorf("Expected IsForbidden to be true for inactive user, got %v", inactiveOriginalUser.IsForbidden)
}
// Test case 3: SCIM user with no primary email/phone (should use first)
noPrimaryUser := &SCIMUser{
ID: "user-789",
UserName: "bob.smith",
Emails: []SCIMEmail{
{Value: "bob@example.com", Primary: false, Type: "work"},
{Value: "bob@personal.com", Primary: false, Type: "home"},
},
PhoneNumbers: []SCIMPhoneNumber{
{Value: "+1-555-9999", Primary: false, Type: "work"},
},
}
noPrimaryOriginalUser := provider.scimUserToOriginalUser(noPrimaryUser)
if noPrimaryOriginalUser.Email != "bob@example.com" {
t.Errorf("Expected first email when no primary, got '%s'", noPrimaryOriginalUser.Email)
}
if noPrimaryOriginalUser.Phone != "+1-555-9999" {
t.Errorf("Expected first phone when no primary, got '%s'", noPrimaryOriginalUser.Phone)
}
// Test case 4: Display name construction from first/last name when empty
noDisplayNameUser := &SCIMUser{
ID: "user-101",
UserName: "alice.jones",
Name: SCIMName{
GivenName: "Alice",
FamilyName: "Jones",
},
}
noDisplayNameOriginalUser := provider.scimUserToOriginalUser(noDisplayNameUser)
if noDisplayNameOriginalUser.DisplayName != "Alice Jones" {
t.Errorf("Expected DisplayName to be constructed as 'Alice Jones', got '%s'", noDisplayNameOriginalUser.DisplayName)
}
}
func TestSCIMBuildEndpoint(t *testing.T) {
tests := []struct {
host string
expected string
}{
{"https://example.com/scim/v2", "https://example.com/scim/v2/Users"},
{"https://example.com/scim/v2/", "https://example.com/scim/v2/Users"},
{"http://localhost:8080/scim", "http://localhost:8080/scim/Users"},
}
for _, test := range tests {
provider := &SCIMSyncerProvider{
Syncer: &Syncer{Host: test.host},
}
endpoint := provider.buildSCIMEndpoint()
if endpoint != test.expected {
t.Errorf("For host '%s', expected endpoint '%s', got '%s'", test.host, test.expected, endpoint)
}
}
}
func TestGetSyncerProviderSCIM(t *testing.T) {
syncer := &Syncer{
Type: "SCIM",
Host: "https://example.com/scim/v2",
}
provider := GetSyncerProvider(syncer)
if _, ok := provider.(*SCIMSyncerProvider); !ok {
t.Errorf("Expected SCIMSyncerProvider for type 'SCIM', got %T", provider)
}
}

View File

@@ -58,60 +58,61 @@ type User struct {
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
DeletedTime string `xorm:"varchar(100)" json:"deletedTime"`
Id string `xorm:"varchar(100) index" json:"id"`
ExternalId string `xorm:"varchar(100) index" json:"externalId"`
Type string `xorm:"varchar(100)" json:"type"`
Password string `xorm:"varchar(150)" json:"password"`
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
FirstName string `xorm:"varchar(100)" json:"firstName"`
LastName string `xorm:"varchar(100)" json:"lastName"`
Avatar string `xorm:"text" json:"avatar"`
AvatarType string `xorm:"varchar(100)" json:"avatarType"`
PermanentAvatar string `xorm:"varchar(500)" json:"permanentAvatar"`
Email string `xorm:"varchar(100) index" json:"email"`
EmailVerified bool `json:"emailVerified"`
Phone string `xorm:"varchar(100) index" json:"phone"`
CountryCode string `xorm:"varchar(6)" json:"countryCode"`
Region string `xorm:"varchar(100)" json:"region"`
Location string `xorm:"varchar(100)" json:"location"`
Address []string `json:"address"`
Affiliation string `xorm:"varchar(100)" json:"affiliation"`
Title string `xorm:"varchar(100)" json:"title"`
IdCardType string `xorm:"varchar(100)" json:"idCardType"`
IdCard string `xorm:"varchar(100) index" json:"idCard"`
RealName string `xorm:"varchar(100)" json:"realName"`
IsVerified bool `json:"isVerified"`
Homepage string `xorm:"varchar(100)" json:"homepage"`
Bio string `xorm:"varchar(100)" json:"bio"`
Tag string `xorm:"varchar(100)" json:"tag"`
Language string `xorm:"varchar(100)" json:"language"`
Gender string `xorm:"varchar(100)" json:"gender"`
Birthday string `xorm:"varchar(100)" json:"birthday"`
Education string `xorm:"varchar(100)" json:"education"`
Score int `json:"score"`
Karma int `json:"karma"`
Ranking int `json:"ranking"`
Balance float64 `json:"balance"`
BalanceCredit float64 `json:"balanceCredit"`
Currency string `xorm:"varchar(100)" json:"currency"`
BalanceCurrency string `xorm:"varchar(100)" json:"balanceCurrency"`
IsDefaultAvatar bool `json:"isDefaultAvatar"`
IsOnline bool `json:"isOnline"`
IsAdmin bool `json:"isAdmin"`
IsForbidden bool `json:"isForbidden"`
IsDeleted bool `json:"isDeleted"`
SignupApplication string `xorm:"varchar(100)" json:"signupApplication"`
Hash string `xorm:"varchar(100)" json:"hash"`
PreHash string `xorm:"varchar(100)" json:"preHash"`
RegisterType string `xorm:"varchar(100)" json:"registerType"`
RegisterSource string `xorm:"varchar(100)" json:"registerSource"`
AccessKey string `xorm:"varchar(100)" json:"accessKey"`
AccessSecret string `xorm:"varchar(100)" json:"accessSecret"`
AccessToken string `xorm:"mediumtext" json:"accessToken"`
OriginalToken string `xorm:"mediumtext" json:"originalToken"`
OriginalRefreshToken string `xorm:"mediumtext" json:"originalRefreshToken"`
Id string `xorm:"varchar(100) index" json:"id"`
ExternalId string `xorm:"varchar(100) index" json:"externalId"`
Type string `xorm:"varchar(100)" json:"type"`
Password string `xorm:"varchar(150)" json:"password"`
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
FirstName string `xorm:"varchar(100)" json:"firstName"`
LastName string `xorm:"varchar(100)" json:"lastName"`
Avatar string `xorm:"text" json:"avatar"`
AvatarType string `xorm:"varchar(100)" json:"avatarType"`
PermanentAvatar string `xorm:"varchar(500)" json:"permanentAvatar"`
Email string `xorm:"varchar(100) index" json:"email"`
EmailVerified bool `json:"emailVerified"`
Phone string `xorm:"varchar(100) index" json:"phone"`
CountryCode string `xorm:"varchar(6)" json:"countryCode"`
Region string `xorm:"varchar(100)" json:"region"`
Location string `xorm:"varchar(100)" json:"location"`
Address []string `json:"address"`
Addresses []*Address `xorm:"addresses blob" json:"addresses"`
Affiliation string `xorm:"varchar(100)" json:"affiliation"`
Title string `xorm:"varchar(100)" json:"title"`
IdCardType string `xorm:"varchar(100)" json:"idCardType"`
IdCard string `xorm:"varchar(100) index" json:"idCard"`
RealName string `xorm:"varchar(100)" json:"realName"`
IsVerified bool `json:"isVerified"`
Homepage string `xorm:"varchar(100)" json:"homepage"`
Bio string `xorm:"varchar(100)" json:"bio"`
Tag string `xorm:"varchar(100)" json:"tag"`
Language string `xorm:"varchar(100)" json:"language"`
Gender string `xorm:"varchar(100)" json:"gender"`
Birthday string `xorm:"varchar(100)" json:"birthday"`
Education string `xorm:"varchar(100)" json:"education"`
Score int `json:"score"`
Karma int `json:"karma"`
Ranking int `json:"ranking"`
Balance float64 `json:"balance"`
BalanceCredit float64 `json:"balanceCredit"`
Currency string `xorm:"varchar(100)" json:"currency"`
BalanceCurrency string `xorm:"varchar(100)" json:"balanceCurrency"`
IsDefaultAvatar bool `json:"isDefaultAvatar"`
IsOnline bool `json:"isOnline"`
IsAdmin bool `json:"isAdmin"`
IsForbidden bool `json:"isForbidden"`
IsDeleted bool `json:"isDeleted"`
SignupApplication string `xorm:"varchar(100)" json:"signupApplication"`
Hash string `xorm:"varchar(100)" json:"hash"`
PreHash string `xorm:"varchar(100)" json:"preHash"`
RegisterType string `xorm:"varchar(100)" json:"registerType"`
RegisterSource string `xorm:"varchar(100)" json:"registerSource"`
AccessKey string `xorm:"varchar(100)" json:"accessKey"`
AccessSecret string `xorm:"varchar(100)" json:"accessSecret"`
AccessToken string `xorm:"mediumtext" json:"accessToken"`
OriginalToken string `xorm:"mediumtext" json:"originalToken"`
OriginalRefreshToken string `xorm:"mediumtext" json:"originalRefreshToken"`
CreatedIp string `xorm:"varchar(100)" json:"createdIp"`
LastSigninTime string `xorm:"varchar(100)" json:"lastSigninTime"`
@@ -274,6 +275,16 @@ type MfaAccount struct {
Origin string `xorm:"varchar(100)" json:"origin"`
}
type Address struct {
Tag string `xorm:"varchar(100)" json:"tag"`
Line1 string `xorm:"varchar(100)" json:"line1"`
Line2 string `xorm:"varchar(100)" json:"line2"`
City string `xorm:"varchar(100)" json:"city"`
State string `xorm:"varchar(100)" json:"state"`
ZipCode string `xorm:"varchar(100)" json:"zipCode"`
Region string `xorm:"varchar(100)" json:"region"`
}
type FaceId struct {
Name string `xorm:"varchar(100) notnull pk" json:"name"`
FaceIdData []float64 `json:"faceIdData"`

View File

@@ -918,6 +918,8 @@ func StringArrayToStruct[T any](stringArray [][]string) ([]*T, error) {
err = setReflectAttr[[]MfaAccount](&fv, v)
case reflect.TypeOf([]webauthn.Credential{}):
err = setReflectAttr[[]webauthn.Credential](&fv, v)
case reflect.TypeOf(map[string]string{}):
err = setReflectAttr[map[string]string](&fv, v)
}
if err != nil {

View File

@@ -24,6 +24,12 @@ const (
PaymentStateError PaymentState = "Error"
)
// IsTerminalState checks if a payment state is terminal (cannot transition to other states)
func IsTerminalState(state PaymentState) bool {
return state == PaymentStatePaid || state == PaymentStateError ||
state == PaymentStateCanceled || state == PaymentStateTimeout
}
const (
PaymentEnvWechatBrowser = "WechatBrowser"
)

View File

@@ -420,7 +420,17 @@ class App extends Component {
}
if (query !== "") {
window.history.replaceState({}, document.title, this.getUrlWithoutQuery());
const returnUrl = params.get("returnUrl");
let newUrl;
if (returnUrl) {
const newParams = new URLSearchParams();
newParams.set("returnUrl", returnUrl);
newUrl = window.location.pathname + "?" + newParams.toString();
} else {
newUrl = this.getUrlWithoutQuery();
}
window.history.replaceState({}, document.title, newUrl);
}
AuthBackend.getAccount(query)

View File

@@ -182,29 +182,15 @@ class CartListPage extends BaseListPage {
);
},
},
{
title: i18next.t("payment:Currency"),
dataIndex: "currency",
key: "currency",
width: "120px",
sorter: true,
render: (text, record, index) => {
return Setting.getCurrencyWithFlag(text);
},
},
{
title: i18next.t("product:Price"),
dataIndex: "price",
key: "price",
width: "120px",
width: "160px",
sorter: true,
render: (text, record) => {
const subtotal = record.price * record.quantity;
return (
<span>
{Setting.getCurrencySymbol(record.currency)}{subtotal.toFixed(2)}
</span>
);
const subtotal = (record.price * record.quantity).toFixed(2);
return Setting.getPriceDisplay(subtotal, record.currency);
},
},
{
@@ -304,7 +290,7 @@ class CartListPage extends BaseListPage {
<div style={{display: "flex", alignItems: "center", fontSize: "18px", fontWeight: "bold"}}>
{i18next.t("product:Total Price")}:&nbsp;
<span style={{color: "red", fontSize: "28px"}}>
{Setting.getCurrencySymbol(currency)}{total.toFixed(2)} ({Setting.getCurrencyText({currency: currency})})
{Setting.getCurrencySymbol(currency)}{total.toFixed(2)} ({Setting.getCurrencyText(currency)})
</span>
</div>
<Button

View File

@@ -138,22 +138,14 @@ class OrderListPage extends BaseListPage {
return Setting.getFormattedDate(text);
},
},
{
title: i18next.t("general:Display name"),
dataIndex: "displayName",
key: "displayName",
width: "170px",
sorter: true,
...this.getColumnSearchProps("displayName"),
},
{
title: i18next.t("order:Products"),
dataIndex: "products",
key: "products",
...this.getColumnSearchProps("products"),
render: (text, record, index) => {
const products = record?.products || [];
if (products.length === 0) {
const productInfos = record?.productInfos || [];
if (productInfos.length === 0) {
return `(${i18next.t("general:empty")})`;
}
return (
@@ -161,21 +153,26 @@ class OrderListPage extends BaseListPage {
<List
size="small"
locale={{emptyText: " "}}
dataSource={products}
dataSource={productInfos}
style={{
paddingTop: 8,
paddingBottom: 8,
}}
renderItem={(productName, i) => {
renderItem={(productInfo, i) => {
const price = productInfo.price * (productInfo.quantity || 1);
const currency = record.currency || "USD";
return (
<List.Item>
<div style={{display: "inline"}}>
<Tooltip placement="topLeft" title={i18next.t("general:Edit")}>
<Button style={{marginRight: "5px"}} icon={<EditOutlined />} size="small" onClick={() => Setting.goToLinkSoft(this, `/products/${record.owner}/${productName}`)} />
<Button style={{marginRight: "5px"}} icon={<EditOutlined />} size="small" onClick={() => Setting.goToLinkSoft(this, `/products/${record.owner}/${productInfo.name}`)} />
</Tooltip>
<Link to={`/products/${record.owner}/${productName}`}>
{productName}
<Link to={`/products/${record.owner}/${productInfo.name}`}>
{productInfo.displayName || productInfo.name}
</Link>
<span style={{marginLeft: "8px", color: "#666"}}>
{Setting.getPriceDisplay(price, currency)}
</span>
</div>
</List.Item>
);
@@ -186,20 +183,23 @@ class OrderListPage extends BaseListPage {
},
},
{
title: i18next.t("order:Payment"),
dataIndex: "payment",
key: "payment",
width: "140px",
title: i18next.t("order:Price"),
dataIndex: "price",
key: "price",
width: "160px",
sorter: true,
...this.getColumnSearchProps("payment"),
...this.getColumnSearchProps("price"),
render: (text, record, index) => {
if (text === "") {
return "(empty)";
}
return (
<Link to={`/payments/${record.owner}/${text}`}>
{text}
const price = (record.price || 0).toFixed(2);
const currency = record.currency || "USD";
const priceDisplay = Setting.getPriceDisplay(price, currency);
return record.payment ? (
<Link to={`/payments/${record.owner}/${record.payment}`}>
{priceDisplay}
</Link>
) : (
<span>{priceDisplay}</span>
);
},
},
@@ -262,19 +262,20 @@ class OrderListPage extends BaseListPage {
const isAdmin = Setting.isLocalAdminUser(this.props.account);
return (
<div style={{display: "flex", flexWrap: "wrap", gap: "8px"}}>
<Button onClick={() => this.props.history.push(`/orders/${record.owner}/${record.name}/pay` + (record.state === "Created" ? "" : "?view=true"))}>
<Button onClick={() => this.props.history.push(`/orders/${record.owner}/${record.name}/pay`)}>
{record.state === "Created" ? i18next.t("order:Pay") : i18next.t("general:Detail")}
</Button>
<Button danger onClick={() => this.cancelOrder(record)} disabled={record.state !== "Created" || !isAdmin}>
{i18next.t("general:Cancel")}
</Button>
<Button type="primary" onClick={() => this.props.history.push({pathname: `/orders/${record.owner}/${record.name}`, mode: isAdmin ? "edit" : "view"})}>{isAdmin ? i18next.t("general:Edit") : i18next.t("general:View")}</Button>
<PopconfirmModal
disabled={!isAdmin}
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
onConfirm={() => this.deleteOrder(index)}
>
</PopconfirmModal>
{isAdmin && (
<PopconfirmModal
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
onConfirm={() => this.deleteOrder(index)}
>
</PopconfirmModal>
)}
</div>
);
},

View File

@@ -22,7 +22,6 @@ import * as Setting from "./Setting";
class OrderPayPage extends React.Component {
constructor(props) {
super(props);
const params = new URLSearchParams(window.location.search);
this.state = {
owner: props?.match?.params?.organizationName ?? props?.match?.params?.owner ?? null,
orderName: props?.match?.params?.orderName ?? null,
@@ -31,7 +30,7 @@ class OrderPayPage extends React.Component {
productInfos: [],
paymentEnv: "",
isProcessingPayment: false,
isViewMode: params.get("view") === "true",
isViewMode: false,
};
}
@@ -61,6 +60,7 @@ class OrderPayPage extends React.Component {
this.setState({
order: res.data,
productInfos: res.data?.productInfos,
isViewMode: res.data?.state !== "Created",
}, () => {
this.getProduct();
});
@@ -90,12 +90,12 @@ class OrderPayPage extends React.Component {
}
getPrice(order) {
return `${Setting.getCurrencySymbol(order?.currency)}${order?.price} (${Setting.getCurrencyText(order)})`;
return `${Setting.getCurrencySymbol(order?.currency)}${order?.price} (${Setting.getCurrencyText(order?.currency)})`;
}
getProductPrice(product) {
const price = product.price * (product.quantity ?? 1);
return `${Setting.getCurrencySymbol(this.state.order?.currency)}${price.toFixed(2)} (${Setting.getCurrencyText(this.state.order)})`;
return `${Setting.getCurrencySymbol(this.state.order?.currency)}${price.toFixed(2)} (${Setting.getCurrencyText(this.state.order?.currency)})`;
}
// Call Wechat Pay via jsapi
@@ -275,13 +275,13 @@ class OrderPayPage extends React.Component {
<div className="login-content">
<Spin spinning={this.state.isProcessingPayment} size="large" tip={i18next.t("product:Processing payment...")} style={{paddingTop: "10%"}} >
<div style={{marginBottom: "20px"}}>
<Descriptions title={<span style={Setting.isMobile() ? {fontSize: 18} : {fontSize: 24}}>{i18next.t("order:Order")}</span>} bordered column={3}>
<Descriptions.Item label={i18next.t("order:ID")} span={3}>
<Descriptions title={<span style={Setting.isMobile() ? {fontSize: 18} : {fontSize: 24}}>{i18next.t("general:Order")}</span>} bordered column={3}>
<Descriptions.Item label={i18next.t("general:ID")} span={3}>
<span style={{fontSize: 16}}>
{order.name}
</span>
</Descriptions.Item>
<Descriptions.Item label={i18next.t("order:Status")}>
<Descriptions.Item label={i18next.t("general:Status")}>
<span style={{fontSize: 16}}>
{order.state}
</span>

View File

@@ -678,6 +678,16 @@ class OrganizationEditPage extends React.Component {
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("organization:Account menu"), i18next.t("organization:Account menu - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.organization.accountMenu || "Horizontal"} onChange={(value => {this.updateOrganizationField("accountMenu", value);})}
options={[{value: "Horizontal", label: i18next.t("general:Horizontal")}, {value: "Vertical", label: i18next.t("general:Vertical")}].map(item => Setting.getOption(item.label, item.value))}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("organization:Account items"), i18next.t("organization:Account items - Tooltip"))} :

View File

@@ -74,8 +74,7 @@ class PaymentEditPage extends React.Component {
goToViewOrder() {
const payment = this.state.payment;
if (payment && payment.order) {
const viewUrl = `/orders/${payment.owner}/${payment.order}/pay?view=true`;
this.props.history.push(viewUrl);
this.props.history.push(`/orders/${payment.owner}/${payment.order}/pay`);
} else {
Setting.showMessage("error", i18next.t("order:Order not found"));
}

View File

@@ -180,8 +180,8 @@ class PaymentListPage extends BaseListPage {
key: "products",
...this.getColumnSearchProps("products"),
render: (text, record, index) => {
const products = record?.products || [];
if (products.length === 0) {
const productInfos = record?.orderObj?.productInfos || [];
if (productInfos.length === 0) {
return `(${i18next.t("general:empty")})`;
}
return (
@@ -189,21 +189,26 @@ class PaymentListPage extends BaseListPage {
<List
size="small"
locale={{emptyText: " "}}
dataSource={products}
dataSource={productInfos}
style={{
paddingTop: 8,
paddingBottom: 8,
}}
renderItem={(productName, i) => {
renderItem={(productInfo, i) => {
const price = productInfo.price * (productInfo.quantity || 1);
const currency = record.currency || "USD";
return (
<List.Item>
<div style={{display: "inline"}}>
<Tooltip placement="topLeft" title={i18next.t("general:Edit")}>
<Button style={{marginRight: "5px"}} icon={<EditOutlined />} size="small" onClick={() => Setting.goToLinkSoft(this, `/products/${record.owner}/${productName}`)} />
<Button style={{marginRight: "5px"}} icon={<EditOutlined />} size="small" onClick={() => Setting.goToLinkSoft(this, `/products/${record.owner}/${productInfo.name}`)} />
</Tooltip>
<Link to={`/products/${record.owner}/${productName}`}>
{productName}
<Link to={`/products/${record.owner}/${productInfo.name}`}>
{productInfo.displayName || productInfo.name}
</Link>
<span style={{marginLeft: "8px", color: "#666"}}>
{Setting.getPriceDisplay(price, currency)}
</span>
</div>
</List.Item>
);
@@ -217,19 +222,11 @@ class PaymentListPage extends BaseListPage {
title: i18next.t("product:Price"),
dataIndex: "price",
key: "price",
width: "120px",
width: "160px",
sorter: true,
...this.getColumnSearchProps("price"),
},
{
title: i18next.t("payment:Currency"),
dataIndex: "currency",
key: "currency",
width: "120px",
sorter: true,
...this.getColumnSearchProps("currency"),
render: (text, record, index) => {
return Setting.getCurrencyWithFlag(text);
return Setting.getPriceDisplay(record.price, record.currency);
},
},
{

View File

@@ -150,8 +150,7 @@ class PaymentResultPage extends React.Component {
goToViewOrder() {
const payment = this.state.payment;
if (payment && payment.order) {
const viewUrl = `/orders/${payment.owner}/${payment.order}/pay?view=true`;
this.props.history.push(viewUrl);
this.props.history.push(`/orders/${payment.owner}/${payment.order}/pay`);
} else {
Setting.showMessage("error", i18next.t("order:Order not found"));
}
@@ -178,7 +177,7 @@ class PaymentResultPage extends React.Component {
<Result
status="success"
title={`${i18next.t("payment:Recharged successfully")}`}
subTitle={`${i18next.t("payment:You have successfully recharged")} ${payment.price} ${Setting.getCurrencyText(payment)}, ${i18next.t("payment:Your current balance is")} ${this.state.user?.balance} ${Setting.getCurrencyText(payment)}`}
subTitle={`${i18next.t("payment:You have successfully recharged")} ${payment.price} ${Setting.getCurrencyText(payment?.currency)}, ${i18next.t("payment:Your current balance is")} ${this.state.user?.balance} ${Setting.getCurrencyText(payment?.currency)}`}
extra={[
<Button type="primary" key="viewOrder" onClick={() => {
this.goToViewOrder();

View File

@@ -129,23 +129,15 @@ class PlanListPage extends BaseListPage {
sorter: true,
...this.getColumnSearchProps("displayName"),
},
{
title: i18next.t("payment:Currency"),
dataIndex: "currency",
key: "currency",
width: "120px",
sorter: true,
...this.getColumnSearchProps("currency"),
render: (text, record, index) => {
return Setting.getCurrencyWithFlag(text);
},
},
{
title: i18next.t("plan:Price"),
dataIndex: "price",
key: "price",
width: "130px",
width: "160px",
...this.getColumnSearchProps("price"),
render: (text, record, index) => {
return Setting.getPriceDisplay(record.price, record.currency);
},
},
{
title: i18next.t("plan:Period"),

View File

@@ -134,7 +134,7 @@ class ProductBuyPage extends React.Component {
}
getPrice(product) {
return `${Setting.getCurrencySymbol(product?.currency)}${product?.price} (${Setting.getCurrencyText(product)})`;
return `${Setting.getCurrencySymbol(product?.currency)}${product?.price} (${Setting.getCurrencyText(product?.currency)})`;
}
addToCart(product) {
@@ -301,7 +301,7 @@ class ProductBuyPage extends React.Component {
onChange={(e) => {this.setState({customPrice: e});}}
disabled={disableCustom}
/>
<span style={{fontSize: 16}}>{Setting.getCurrencyText(product)}</span>
<span style={{fontSize: 16}}>{Setting.getCurrencyText(product?.currency)}</span>
</Space>
</Space>
);
@@ -340,7 +340,7 @@ class ProductBuyPage extends React.Component {
{i18next.t("order:Place Order")}
</Button>
<Button
type="primary"
type="default"
size="large"
style={{
height: "50px",

View File

@@ -152,24 +152,16 @@ class ProductListPage extends BaseListPage {
sorter: true,
...this.getColumnSearchProps("tag"),
},
{
title: i18next.t("payment:Currency"),
dataIndex: "currency",
key: "currency",
width: "120px",
sorter: true,
...this.getColumnSearchProps("currency"),
render: (text, record, index) => {
return Setting.getCurrencyWithFlag(text);
},
},
{
title: i18next.t("product:Price"),
dataIndex: "price",
key: "price",
width: "120px",
width: "160px",
sorter: true,
...this.getColumnSearchProps("price"),
render: (text, record, index) => {
return Setting.getPriceDisplay(record.price, record.currency);
},
},
{
title: i18next.t("product:Quantity"),

View File

@@ -162,7 +162,7 @@ class ProductStorePage extends React.Component {
{!product.isRecharge && (
<Button
key="add"
type="primary"
type="default"
onClick={(e) => {
e.stopPropagation();
this.addToCart(product);

View File

@@ -1868,6 +1868,17 @@ export function getCurrencyCountryCode(currency) {
return currencyToCountry[currency?.toUpperCase()] || null;
}
export function getCurrencyFlag(currency) {
const countryCode = getCurrencyCountryCode(currency);
if (!countryCode) {
return null;
}
return (
<img src={`${StaticBaseUrl}/flag-icons/${countryCode}.svg`} alt={`${currency} flag`} height={20} style={{marginRight: 5}} />
);
}
export function getCurrencyWithFlag(currency) {
const translationKey = `currency:${currency}`;
const translatedText = i18next.t(translationKey);
@@ -1886,6 +1897,16 @@ export function getCurrencyWithFlag(currency) {
);
}
export function getPriceDisplay(price, currency) {
const priceValue = price || 0;
const currencyValue = currency || "USD";
return (
<>
{getCurrencyFlag(currencyValue)} {getCurrencySymbol(currencyValue)}{priceValue} ({getCurrencyText(currencyValue)})
</>
);
}
export function getFriendlyUserName(account) {
if (account.firstName !== "" && account.lastName !== "") {
return `${account.firstName}, ${account.lastName}`;
@@ -2013,58 +2034,58 @@ export function getDefaultInvitationHtmlEmailContent() {
</html>`;
}
export function getCurrencyText(product) {
if (product?.currency === "USD") {
export function getCurrencyText(currency) {
if (currency === "USD") {
return i18next.t("currency:USD");
} else if (product?.currency === "CNY") {
} else if (currency === "CNY") {
return i18next.t("currency:CNY");
} else if (product?.currency === "EUR") {
} else if (currency === "EUR") {
return i18next.t("currency:EUR");
} else if (product?.currency === "JPY") {
} else if (currency === "JPY") {
return i18next.t("currency:JPY");
} else if (product?.currency === "GBP") {
} else if (currency === "GBP") {
return i18next.t("currency:GBP");
} else if (product?.currency === "AUD") {
} else if (currency === "AUD") {
return i18next.t("currency:AUD");
} else if (product?.currency === "CAD") {
} else if (currency === "CAD") {
return i18next.t("currency:CAD");
} else if (product?.currency === "CHF") {
} else if (currency === "CHF") {
return i18next.t("currency:CHF");
} else if (product?.currency === "HKD") {
} else if (currency === "HKD") {
return i18next.t("currency:HKD");
} else if (product?.currency === "SGD") {
} else if (currency === "SGD") {
return i18next.t("currency:SGD");
} else if (product?.currency === "BRL") {
} else if (currency === "BRL") {
return i18next.t("currency:BRL");
} else if (product?.currency === "PLN") {
} else if (currency === "PLN") {
return i18next.t("currency:PLN");
} else if (product?.currency === "KRW") {
} else if (currency === "KRW") {
return i18next.t("currency:KRW");
} else if (product?.currency === "INR") {
} else if (currency === "INR") {
return i18next.t("currency:INR");
} else if (product?.currency === "RUB") {
} else if (currency === "RUB") {
return i18next.t("currency:RUB");
} else if (product?.currency === "MXN") {
} else if (currency === "MXN") {
return i18next.t("currency:MXN");
} else if (product?.currency === "ZAR") {
} else if (currency === "ZAR") {
return i18next.t("currency:ZAR");
} else if (product?.currency === "TRY") {
} else if (currency === "TRY") {
return i18next.t("currency:TRY");
} else if (product?.currency === "SEK") {
} else if (currency === "SEK") {
return i18next.t("currency:SEK");
} else if (product?.currency === "NOK") {
} else if (currency === "NOK") {
return i18next.t("currency:NOK");
} else if (product?.currency === "DKK") {
} else if (currency === "DKK") {
return i18next.t("currency:DKK");
} else if (product?.currency === "THB") {
} else if (currency === "THB") {
return i18next.t("currency:THB");
} else if (product?.currency === "MYR") {
} else if (currency === "MYR") {
return i18next.t("currency:MYR");
} else if (product?.currency === "TWD") {
} else if (currency === "TWD") {
return i18next.t("currency:TWD");
} else if (product?.currency === "CZK") {
} else if (currency === "CZK") {
return i18next.t("currency:CZK");
} else if (product?.currency === "HUF") {
} else if (currency === "HUF") {
return i18next.t("currency:HUF");
} else {
return "(Unknown currency)";

View File

@@ -637,6 +637,79 @@ class SyncerEditPage extends React.Component {
"values": [],
},
];
case "SCIM":
return [
{
"name": "id",
"type": "string",
"casdoorName": "Id",
"isHashed": true,
"values": [],
},
{
"name": "userName",
"type": "string",
"casdoorName": "Name",
"isHashed": true,
"values": [],
},
{
"name": "displayName",
"type": "string",
"casdoorName": "DisplayName",
"isHashed": true,
"values": [],
},
{
"name": "name.givenName",
"type": "string",
"casdoorName": "FirstName",
"isHashed": true,
"values": [],
},
{
"name": "name.familyName",
"type": "string",
"casdoorName": "LastName",
"isHashed": true,
"values": [],
},
{
"name": "emails",
"type": "string",
"casdoorName": "Email",
"isHashed": true,
"values": [],
},
{
"name": "phoneNumbers",
"type": "string",
"casdoorName": "Phone",
"isHashed": true,
"values": [],
},
{
"name": "title",
"type": "string",
"casdoorName": "Title",
"isHashed": true,
"values": [],
},
{
"name": "preferredLanguage",
"type": "string",
"casdoorName": "Language",
"isHashed": true,
"values": [],
},
{
"name": "active",
"type": "boolean",
"casdoorName": "IsForbidden",
"isHashed": true,
"values": [],
},
];
default:
return [];
}
@@ -693,14 +766,14 @@ class SyncerEditPage extends React.Component {
});
})}>
{
["Database", "Keycloak", "WeCom", "Azure AD", "Active Directory", "Google Workspace", "DingTalk", "Lark", "Okta"]
["Database", "Keycloak", "WeCom", "Azure AD", "Active Directory", "Google Workspace", "DingTalk", "Lark", "Okta", "SCIM"]
.map((item, index) => <Option key={index} value={item}>{item}</Option>)
}
</Select>
</Col>
</Row>
{
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Active Directory" || this.state.syncer.type === "Google Workspace" || this.state.syncer.type === "DingTalk" || this.state.syncer.type === "Lark" || this.state.syncer.type === "Okta" ? null : (
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Active Directory" || this.state.syncer.type === "Google Workspace" || this.state.syncer.type === "DingTalk" || this.state.syncer.type === "Lark" || this.state.syncer.type === "Okta" || this.state.syncer.type === "SCIM" ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("syncer:Database type"), i18next.t("syncer:Database type - Tooltip"))} :
@@ -755,7 +828,7 @@ class SyncerEditPage extends React.Component {
this.state.syncer.type === "WeCom" || this.state.syncer.type === "DingTalk" || this.state.syncer.type === "Lark" ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(this.state.syncer.type === "Azure AD" ? i18next.t("provider:Tenant ID") : this.state.syncer.type === "Google Workspace" ? i18next.t("syncer:Admin Email") : this.state.syncer.type === "Active Directory" ? i18next.t("ldap:Server") : i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} :
{Setting.getLabel(this.state.syncer.type === "Azure AD" ? i18next.t("provider:Tenant ID") : this.state.syncer.type === "Google Workspace" ? i18next.t("syncer:Admin Email") : this.state.syncer.type === "Active Directory" ? i18next.t("ldap:Server") : this.state.syncer.type === "SCIM" ? i18next.t("syncer:SCIM Server URL") : i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} :
</Col>
<Col span={22} >
<Input prefix={<LinkOutlined />} value={this.state.syncer.host} onChange={e => {
@@ -766,7 +839,7 @@ class SyncerEditPage extends React.Component {
)
}
{
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Google Workspace" || this.state.syncer.type === "DingTalk" || this.state.syncer.type === "Lark" || this.state.syncer.type === "Okta" ? null : (
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Google Workspace" || this.state.syncer.type === "DingTalk" || this.state.syncer.type === "Lark" || this.state.syncer.type === "Okta" || this.state.syncer.type === "SCIM" ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(this.state.syncer.type === "Active Directory" ? i18next.t("provider:LDAP port") : i18next.t("provider:Port"), i18next.t("provider:Port - Tooltip"))} :
@@ -789,7 +862,8 @@ class SyncerEditPage extends React.Component {
this.state.syncer.type === "Lark" ? i18next.t("provider:App ID") :
this.state.syncer.type === "Azure AD" ? i18next.t("provider:Client ID") :
this.state.syncer.type === "Active Directory" ? i18next.t("syncer:Bind DN") :
i18next.t("general:User"),
this.state.syncer.type === "SCIM" ? i18next.t("syncer:Username (optional)") :
i18next.t("general:User"),
i18next.t("general:User - Tooltip")
)} :
</Col>
@@ -809,7 +883,8 @@ class SyncerEditPage extends React.Component {
this.state.syncer.type === "Lark" ? i18next.t("provider:App secret") :
this.state.syncer.type === "Azure AD" ? i18next.t("provider:Client secret") :
this.state.syncer.type === "Google Workspace" ? i18next.t("syncer:Service account key") :
i18next.t("general:Password"),
this.state.syncer.type === "SCIM" ? i18next.t("syncer:API Token / Password") :
i18next.t("general:Password"),
i18next.t("general:Password - Tooltip")
)} :
</Col>
@@ -828,7 +903,7 @@ class SyncerEditPage extends React.Component {
</Col>
</Row>
{
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Google Workspace" || this.state.syncer.type === "DingTalk" || this.state.syncer.type === "Lark" || this.state.syncer.type === "Okta" ? null : (
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Google Workspace" || this.state.syncer.type === "DingTalk" || this.state.syncer.type === "Lark" || this.state.syncer.type === "Okta" || this.state.syncer.type === "SCIM" ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(this.state.syncer.type === "Active Directory" ? i18next.t("syncer:Base DN") : i18next.t("syncer:Database"), i18next.t("syncer:Database - Tooltip"))} :
@@ -924,7 +999,7 @@ class SyncerEditPage extends React.Component {
) : null
}
{
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Google Workspace" || this.state.syncer.type === "DingTalk" || this.state.syncer.type === "Lark" || this.state.syncer.type === "Okta" ? null : (
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Google Workspace" || this.state.syncer.type === "DingTalk" || this.state.syncer.type === "Lark" || this.state.syncer.type === "Okta" || this.state.syncer.type === "SCIM" ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("syncer:Table"), i18next.t("syncer:Table - Tooltip"))} :

View File

@@ -13,7 +13,10 @@
// limitations under the License.
import React from "react";
import {Button, Card, Col, Form, Input, InputNumber, List, Result, Row, Select, Space, Spin, Switch, Tag, Tooltip} from "antd";
import {
Button, Card, Col, Form, Input, InputNumber, Layout, List,
Menu, Result, Row, Select, Space, Spin, Switch, Tabs, Tag, Tooltip
} from "antd";
import {withRouter} from "react-router-dom";
import {TotpMfaType} from "./auth/MfaSetupPage";
import * as GroupBackend from "./backend/GroupBackend";
@@ -33,6 +36,7 @@ import SamlWidget from "./common/SamlWidget";
import RegionSelect from "./common/select/RegionSelect";
import WebAuthnCredentialTable from "./table/WebauthnCredentialTable";
import ManagedAccountTable from "./table/ManagedAccountTable";
import AddressTable from "./table/AddressTable";
import PropertyTable from "./table/propertyTable";
import {CountryCodeSelect} from "./common/select/CountryCodeSelect";
import PopconfirmModal from "./common/modal/PopconfirmModal";
@@ -45,6 +49,8 @@ import MfaAccountTable from "./table/MfaAccountTable";
import MfaTable from "./table/MfaTable";
import TransactionTable from "./table/TransactionTable";
import * as TransactionBackend from "./backend/TransactionBackend";
import {Content, Header} from "antd/es/layout/layout";
import Sider from "antd/es/layout/Sider";
const {Option} = Select;
@@ -66,6 +72,8 @@ class UserEditPage extends React.Component {
idCardInfo: ["ID card front", "ID card back", "ID card with person"],
openFaceRecognitionModal: false,
transactions: [],
activeMenuKey: window.location.hash?.slice(1) || "",
menuMode: "Horizontal",
};
}
@@ -174,6 +182,7 @@ class UserEditPage extends React.Component {
}
this.setState({
menuMode: res.data?.organizationObj?.accountMenu ?? "Horizontal",
application: res.data,
});
});
@@ -597,6 +606,16 @@ class UserEditPage extends React.Component {
</Row>
</React.Fragment>
);
} else if (accountItem.name === "Addresses") {
return (
<AddressTable
title={i18next.t("user:Addresses")}
table={this.state.user.addresses}
onUpdateTable={(value) => {
this.updateUserField("addresses", value);
}}
/>
);
} else if (accountItem.name === "Affiliation") {
return (
(this.state.application === null || this.state.user === null) ? null : (
@@ -1322,6 +1341,152 @@ class UserEditPage extends React.Component {
);
}
isAccountItemVisible(item) {
if (!item.visible) {
return false;
}
const isAdmin = Setting.isLocalAdminUser(this.props.account);
if (item.viewRule === "Self") {
if (!this.isSelfOrAdmin()) {
return false;
}
} else if (item.viewRule === "Admin") {
if (!isAdmin) {
return false;
}
}
return true;
}
getAccountItemsByTab(tab) {
const accountItems = this.getUserOrganization()?.accountItems || [];
return accountItems.filter(item => {
if (!this.isAccountItemVisible(item)) {
return false;
}
const itemTab = item.tab || "";
return itemTab === tab;
});
}
getUniqueTabs() {
const accountItems = this.getUserOrganization()?.accountItems || [];
const tabs = new Set();
accountItems.forEach(item => {
if (this.isAccountItemVisible(item)) {
tabs.add(item.tab || "");
}
});
return Array.from(tabs).sort((a, b) => {
// Empty string (default tab) comes first
if (a === "") {
return -1;
}
if (b === "") {
return 1;
}
return a.localeCompare(b);
});
}
renderUserForm() {
const tabs = this.getUniqueTabs();
// If there are no tabs or only one tab (default), render without tab navigation
if (tabs.length === 0 || (tabs.length === 1 && tabs[0] === "")) {
const accountItems = this.getAccountItemsByTab("");
return (
<Form>
{accountItems.map(accountItem => (
<React.Fragment key={accountItem.name}>
<Form.Item name={accountItem.name}
validateTrigger="onChange"
rules={[
{
pattern: accountItem.regex ? new RegExp(accountItem.regex, "g") : null,
message: i18next.t("user:This field value doesn't match the pattern rule"),
},
]}
style={{margin: 0}}>
{this.renderAccountItem(accountItem)}
</Form.Item>
</React.Fragment>
))}
</Form>
);
}
// Render with tabs
const activeKey = this.state.activeMenuKey || tabs[0] || "";
return (
<Layout style={{background: "inherit"}}>
{
this.state.menuMode === "Vertical" ? null : (
<Header style={{background: "inherit", padding: "0px"}}>
<Tabs
onChange={(key) => {
this.setState({activeMenuKey: key});
window.location.hash = key;
}}
type="card"
activeKey={activeKey}
items={tabs.map(tab => ({
label: tab === "" ? i18next.t("user:Default") : tab,
key: tab,
}))}
/>
</Header>
)
}
<Layout style={{background: "inherit", maxHeight: "70vh", overflow: "auto"}}>
{
this.state.menuMode === "Vertical" ? (
<Sider width={200} style={{background: "inherit", position: "sticky", top: 0}}>
<Menu
mode="vertical"
selectedKeys={[activeKey]}
onClick={({key}) => {
this.setState({activeMenuKey: key});
window.location.hash = key;
}}
style={{marginBottom: "20px", height: "100%"}}
items={tabs.map(tab => ({
label: tab === "" ? i18next.t("user:Default") : tab,
key: tab,
}))}
/>
</Sider>) : null
}
<Content style={{padding: "15px"}}>
<Form>
{this.getAccountItemsByTab(activeKey).map(accountItem => (
<React.Fragment key={accountItem.name}>
<Form.Item name={accountItem.name}
validateTrigger="onChange"
rules={[
{
pattern: accountItem.regex ? new RegExp(accountItem.regex, "g") : null,
message: i18next.t("user:This field value doesn't match the pattern rule"),
},
]}
style={{margin: 0}}>
{this.renderAccountItem(accountItem)}
</Form.Item>
</React.Fragment>
))}
</Form>
</Content>
</Layout>
</Layout>
);
}
renderUser() {
return (
<div>
@@ -1335,42 +1500,7 @@ class UserEditPage extends React.Component {
</div>
)
} style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
<Form>
{
this.getUserOrganization()?.accountItems?.map(accountItem => {
if (!accountItem.visible) {
return null;
}
const isAdmin = Setting.isLocalAdminUser(this.props.account);
if (accountItem.viewRule === "Self") {
if (!this.isSelfOrAdmin()) {
return null;
}
} else if (accountItem.viewRule === "Admin") {
if (!isAdmin) {
return null;
}
}
return (
<React.Fragment key={accountItem.name}>
<Form.Item name={accountItem.name}
validateTrigger="onChange"
rules={[
{
pattern: accountItem.regex ? new RegExp(accountItem.regex, "g") : null,
message: i18next.t("user:This field value doesn't match the pattern rule"),
},
]}
style={{margin: 0}}>
{this.renderAccountItem(accountItem)}
</Form.Item>
</React.Fragment>
);
})
}
</Form>
{this.renderUserForm()}
</Card>
</div>
);
@@ -1410,29 +1540,24 @@ class UserEditPage extends React.Component {
organizationName: this.state.user.owner,
userName: this.state.user.name,
});
if (this.props.history !== undefined) {
if (exitAfterSave) {
const userListUrl = sessionStorage.getItem("userListUrl");
if (userListUrl !== null) {
this.props.history.push(userListUrl);
} else {
if (Setting.isLocalAdminUser(this.props.account)) {
this.props.history.push("/users");
} else {
this.props.history.push("/");
}
}
if (exitAfterSave) {
if (this.state.returnUrl) {
window.location.href = this.state.returnUrl;
return;
}
const userListUrl = sessionStorage.getItem("userListUrl");
if (userListUrl !== null) {
this.props.history.push(userListUrl);
} else {
if (location.pathname !== "/account") {
this.props.history.push(`/users/${this.state.user.owner}/${this.state.user.name}`);
if (Setting.isLocalAdminUser(this.props.account)) {
this.props.history.push("/users");
} else {
this.props.history.push("/");
}
}
} else {
if (exitAfterSave) {
if (this.state.returnUrl) {
window.location.href = this.state.returnUrl;
}
if (location.pathname !== "/account") {
this.props.history.push(`/users/${this.state.user.owner}/${this.state.user.name}`);
}
}
} else {

View File

@@ -17,6 +17,7 @@ import {Spin} from "antd";
import {withRouter} from "react-router-dom";
import * as AuthBackend from "./AuthBackend";
import * as Util from "./Util";
import * as Provider from "./Provider";
import {authConfig} from "./Auth";
import * as Setting from "../Setting";
import i18next from "i18next";
@@ -146,6 +147,9 @@ class AuthCallback extends React.Component {
const redirectUri = `${window.location.origin}/callback`;
// Retrieve the code verifier for PKCE if it exists
const codeVerifier = Provider.getCodeVerifier(params.get("state"));
const body = {
type: this.getResponseType(),
application: applicationName,
@@ -156,8 +160,14 @@ class AuthCallback extends React.Component {
state: applicationName,
redirectUri: redirectUri,
method: method,
codeVerifier: codeVerifier, // Include PKCE code verifier
};
// Clean up the stored code verifier after using it
if (codeVerifier) {
Provider.clearCodeVerifier(params.get("state"));
}
if (this.getResponseType() === "cas") {
// user is using casdoor as cas sso server, and wants the ticket to be acquired
AuthBackend.loginCas(body, {"service": casService}).then((res) => {

View File

@@ -14,9 +14,52 @@
import React from "react";
import {Tooltip} from "antd";
import CryptoJS from "crypto-js";
import * as Util from "./Util";
import * as Setting from "../Setting";
// PKCE helper functions
function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64UrlEncode(array);
}
function base64UrlEncode(buffer) {
const base64 = btoa(String.fromCharCode.apply(null, buffer));
return base64
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
}
function generateCodeChallenge(verifier) {
// Convert verifier to UTF-8 bytes and compute SHA-256 hash
const hash = CryptoJS.SHA256(CryptoJS.enc.Utf8.parse(verifier));
const base64Hash = CryptoJS.enc.Base64.stringify(hash);
return base64Hash
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
}
function storeCodeVerifier(state, verifier) {
localStorage.setItem("pkce_verifier", `${state}#${verifier}`);
}
export function getCodeVerifier(state) {
const verifierStore = localStorage.getItem("pkce_verifier");
const [storedState, verifier] = verifierStore ? verifierStore.split("#") : [null, null];
if (storedState !== state) {
return null;
}
return verifier;
}
export function clearCodeVerifier(state) {
localStorage.removeItem("pkce_verifier");
}
const authInfo = {
Google: {
scope: "profile+email",
@@ -402,7 +445,11 @@ export function getAuthUrl(application, provider, method, code) {
applicationName = `${application.name}-org-${application.organization}`;
}
const state = Util.getStateFromQueryParams(applicationName, provider.name, method, isShortState);
const codeChallenge = "P3S-a7dr8bgM4bF6vOyiKkKETDl16rcAzao9F8UIL1Y"; // SHA256(Base64-URL-encode("casdoor-verifier"))
// Generate PKCE code verifier and challenge dynamically
const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);
storeCodeVerifier(state, codeVerifier);
if (provider.type === "AzureAD") {
if (provider.domain !== "") {

View File

@@ -100,7 +100,7 @@
"Order - Tooltip": "Je kleiner der Wert, desto höher rangiert er auf der Apps-Seite",
"Org choice mode": "Organisationsauswahlmodus",
"Org choice mode - Tooltip": "Organisationsauswahlmodus",
"Please enable \\\"Signin session\\\" first before enabling \\\"Auto signin\\\"": "Bitte aktivieren Sie zuerst \\\"Anmeldesitzung\\\", bevor Sie \\\"Automatische Anmeldung\\\" aktivieren.",
"Please enable \"Signin session\" first before enabling \"Auto signin\"": "Bitte aktivieren Sie zuerst \"Anmeldesitzung\", bevor Sie \"Automatische Anmeldung\" aktivieren.",
"Please input your application!": "Bitte geben Sie Ihre Anwendung ein!",
"Please input your organization!": "Bitte geben Sie Ihre Organisation ein!",
"Please select a HTML file": "Bitte wählen Sie eine HTML-Datei aus",
@@ -1269,6 +1269,17 @@
"Address": "Adresse",
"Address - Tooltip": "Wohnadresse",
"Address line": "Adresszeile",
"Addresses": "Adressen",
"Addresses - Tooltip": "Mehrere Adressen des Benutzers",
"Tag": "Tag",
"Line 1": "Zeile 1",
"Line 2": "Zeile 2",
"City": "Stadt",
"State": "Bundesland",
"Zip code": "Postleitzahl",
"Home": "Zuhause",
"Work": "Arbeit",
"Other": "Andere",
"Affiliation": "Zugehörigkeit",
"Affiliation - Tooltip": "Arbeitgeber, wie Firmenname oder Organisationsname",
"Balance": "Guthaben",

View File

@@ -100,7 +100,7 @@
"Order - Tooltip": "The smaller the value, the higher it ranks in the Apps page",
"Org choice mode": "Org choice mode",
"Org choice mode - Tooltip": "Method used to select the organization to log in",
"Please enable \\\"Signin session\\\" first before enabling \\\"Auto signin\\\"": "Please enable \\\"Signin session\\\" first before enabling \\\"Auto signin\\\"",
"Please enable \"Signin session\" first before enabling \"Auto signin\"": "Please enable \"Signin session\" first before enabling \"Auto signin\"",
"Please input your application!": "Please input your application!",
"Please input your organization!": "Please input your organization!",
"Please select a HTML file": "Please select a HTML file",
@@ -956,6 +956,9 @@
"Copy": "Copy",
"Corp ID": "Corp ID",
"Corp Secret": "Corp Secret",
"SCIM Server URL": "SCIM Server URL",
"Username (optional)": "Username (optional)",
"API Token / Password": "API Token / Password",
"DB test": "DB test",
"DB test - Tooltip": "DB test - Tooltip",
"Disable SSL": "Disable SSL",
@@ -1301,6 +1304,17 @@
"Address": "Address",
"Address - Tooltip": "Residential address",
"Address line": "Address line",
"Addresses": "Addresses",
"Addresses - Tooltip": "Multiple addresses of the user",
"Tag": "Tag",
"Line 1": "Line 1",
"Line 2": "Line 2",
"City": "City",
"State": "State",
"Zip code": "Zip code",
"Home": "Home",
"Work": "Work",
"Other": "Other",
"Affiliation": "Affiliation",
"Affiliation - Tooltip": "Employer, such as company name or organization name",
"Balance": "Balance",

View File

@@ -100,7 +100,7 @@
"Order - Tooltip": "Cuanto menor sea el valor, más alto se clasificará en la página de Aplicaciones",
"Org choice mode": "Modo de selección de organización",
"Org choice mode - Tooltip": "Modo de elección de organización",
"Please enable \\\"Signin session\\\" first before enabling \\\"Auto signin\\\"": "Por favor, habilita \\\"Sesión de inicio de sesión\\\" primero antes de habilitar \\\"Inicio de sesión automático\\\"",
"Please enable \"Signin session\" first before enabling \"Auto signin\"": "Por favor, habilita \"Sesión de inicio de sesión\" primero antes de habilitar \"Inicio de sesión automático\"",
"Please input your application!": "¡Por favor, ingrese su solicitud!",
"Please input your organization!": "¡Por favor, ingrese su organización!",
"Please select a HTML file": "Por favor, seleccione un archivo HTML",
@@ -1269,6 +1269,17 @@
"Address": "Dirección",
"Address - Tooltip": "Dirección residencial",
"Address line": "Línea de dirección",
"Addresses": "Direcciones",
"Addresses - Tooltip": "Múltiples direcciones del usuario",
"Tag": "Etiqueta",
"Line 1": "Línea 1",
"Line 2": "Línea 2",
"City": "Ciudad",
"State": "Estado",
"Zip code": "Código postal",
"Home": "Casa",
"Work": "Trabajo",
"Other": "Otro",
"Affiliation": "Afiliación",
"Affiliation - Tooltip": "Empleador, como el nombre de una empresa u organización",
"Balance": "Saldo",

View File

@@ -100,7 +100,7 @@
"Order - Tooltip": "Plus la valeur est petite, plus elle est classée haut dans la page Applications",
"Org choice mode": "Mode de choix d'organisation",
"Org choice mode - Tooltip": "Mode de choix d'organisation",
"Please enable \\\"Signin session\\\" first before enabling \\\"Auto signin\\\"": "Veuillez activer \\\"Session de connexion\\\" avant d'activer \\\"Connexion automatique\\\"",
"Please enable \"Signin session\" first before enabling \"Auto signin\"": "Veuillez activer \"Session de connexion\" avant d'activer \"Connexion automatique\"",
"Please input your application!": "Veuillez saisir votre application !",
"Please input your organization!": "Veuillez saisir votre organisation !",
"Please select a HTML file": "Veuillez sélectionner un fichier HTML",
@@ -1269,6 +1269,17 @@
"Address": "Adresse",
"Address - Tooltip": "Adresse résidentielle",
"Address line": "Adresse",
"Addresses": "Adresses",
"Addresses - Tooltip": "Plusieurs adresses de l'utilisateur",
"Tag": "Étiquette",
"Line 1": "Ligne 1",
"Line 2": "Ligne 2",
"City": "Ville",
"State": "État",
"Zip code": "Code postal",
"Home": "Domicile",
"Work": "Travail",
"Other": "Autre",
"Affiliation": "Affiliation",
"Affiliation - Tooltip": "Employeur, tel que le nom de l'entreprise ou de l'organisation",
"Balance": "Solde",

View File

@@ -100,7 +100,7 @@
"Order - Tooltip": "値が小さいほど、アプリページで上位にランク付けされます",
"Org choice mode": "組織選択モード",
"Org choice mode - Tooltip": "組織選択モード",
"Please enable \\\"Signin session\\\" first before enabling \\\"Auto signin\\\"": "\\\"自動サインイン\\\"を有効にする前に、まず\\\"サインインセッション\\\"を有効にしてください",
"Please enable \"Signin session\" first before enabling \"Auto signin\"": "\"自動サインイン\"を有効にする前に、まず\"サインインセッション\"を有効にしてください",
"Please input your application!": "あなたの申請を入力してください!",
"Please input your organization!": "あなたの組織を入力してください!",
"Please select a HTML file": "HTMLファイルを選択してください",
@@ -1269,6 +1269,17 @@
"Address": "住所",
"Address - Tooltip": "住所",
"Address line": "住所",
"Addresses": "住所一覧",
"Addresses - Tooltip": "ユーザーの複数の住所",
"Tag": "タグ",
"Line 1": "住所1行目",
"Line 2": "住所2行目",
"City": "都市",
"State": "都道府県",
"Zip code": "郵便番号",
"Home": "自宅",
"Work": "職場",
"Other": "その他",
"Affiliation": "所属",
"Affiliation - Tooltip": "企業名や団体名などの雇用主",
"Balance": "残高",

View File

@@ -100,7 +100,7 @@
"Order - Tooltip": "Im mniejsza wartość, tym wyżej jest rangowana na stronie Aplikacji",
"Org choice mode": "Tryb wyboru organizacji",
"Org choice mode - Tooltip": "Tryb wyboru organizacji - Tooltip",
"Please enable \\\"Signin session\\\" first before enabling \\\"Auto signin\\\"": "Najpierw włącz \\\"sesję logowania\\\", zanim włączysz \\\"automatyczne logowanie\\\"",
"Please enable \"Signin session\" first before enabling \"Auto signin\"": "Najpierw włącz \"sesję logowania\", zanim włączysz \"automatyczne logowanie\"",
"Please input your application!": "Proszę wprowadzić swoją aplikację!",
"Please input your organization!": "Proszę wprowadzić swoją organizację!",
"Please select a HTML file": "Proszę wybrać plik HTML",
@@ -1269,6 +1269,17 @@
"Address": "Adres",
"Address - Tooltip": "Adres zamieszkania",
"Address line": "Linia adresu",
"Addresses": "Adresy",
"Addresses - Tooltip": "Wiele adresów użytkownika",
"Tag": "Tag",
"Line 1": "Linia 1",
"Line 2": "Linia 2",
"City": "Miasto",
"State": "Stan",
"Zip code": "Kod pocztowy",
"Home": "Dom",
"Work": "Praca",
"Other": "Inne",
"Affiliation": "Przynależność",
"Affiliation - Tooltip": "Pracodawca, np. nazwa firmy lub organizacji",
"Balance": "Saldo",

View File

@@ -100,7 +100,7 @@
"Order - Tooltip": "Quanto menor o valor, maior a classificação na página de Aplicativos",
"Org choice mode": "Modo de escolha da organização",
"Org choice mode - Tooltip": "Modo de escolha de organização",
"Please enable \\\"Signin session\\\" first before enabling \\\"Auto signin\\\"": "Por favor, habilite a \\\"Sessão de login\\\" primeiro antes de habilitar o \\\"Login automático\\\"",
"Please enable \"Signin session\" first before enabling \"Auto signin\"": "Por favor, habilite a \"Sessão de login\" primeiro antes de habilitar o \"Login automático\"",
"Please input your application!": "Por favor, insira o nome da sua aplicação!",
"Please input your organization!": "Por favor, insira o nome da sua organização!",
"Please select a HTML file": "Por favor, selecione um arquivo HTML",
@@ -1269,6 +1269,17 @@
"Address": "Endereço",
"Address - Tooltip": "Endereço residencial",
"Address line": "Linha de endereço",
"Addresses": "Endereços",
"Addresses - Tooltip": "Múltiplos endereços do usuário",
"Tag": "Etiqueta",
"Line 1": "Linha 1",
"Line 2": "Linha 2",
"City": "Cidade",
"State": "Estado",
"Zip code": "Código postal",
"Home": "Casa",
"Work": "Trabalho",
"Other": "Outro",
"Affiliation": "Afiliação",
"Affiliation - Tooltip": "Empregador, como nome da empresa ou organização",
"Balance": "Saldo",

View File

@@ -100,7 +100,7 @@
"Order - Tooltip": "Değer ne kadar küçükse, Uygulamalar sayfasında o kadar yüksek sıralanır",
"Org choice mode": "Organizasyon seçim modu",
"Org choice mode - Tooltip": "Organizasyon seçim modu",
"Please enable \\\"Signin session\\\" first before enabling \\\"Auto signin\\\"": "Lütfen \\\"Oturum açma oturumu\\\"nu etkinleştirmeden önce \\\"Otomatik oturum açma\\\"yı etkinleştirin",
"Please enable \"Signin session\" first before enabling \"Auto signin\"": "Lütfen \"Oturum açma oturumu\"nu etkinleştirmeden önce \"Otomatik oturum açma\"yı etkinleştirin",
"Please input your application!": "Lütfen uygulamanızı girin!",
"Please input your organization!": "Lütfen organizasyonunuzu girin!",
"Please select a HTML file": "Lütfen bir HTML dosyası seçin",
@@ -1269,6 +1269,17 @@
"Address": "Adres",
"Address - Tooltip": "Ev adresi",
"Address line": "Adres satırı",
"Addresses": "Adresler",
"Addresses - Tooltip": "Kullanıcının birden fazla adresi",
"Tag": "Etiket",
"Line 1": "Satır 1",
"Line 2": "Satır 2",
"City": "Şehir",
"State": "Eyalet",
"Zip code": "Posta kodu",
"Home": "Ev",
"Work": "İş",
"Other": "Diğer",
"Affiliation": "İlişki",
"Affiliation - Tooltip": "İşveren, örneğin şirket adı veya organizasyon adı",
"Balance": "Bakiye",

View File

@@ -100,7 +100,7 @@
"Order - Tooltip": "Чим менше значення, тим вище воно ранжується на сторінці програм",
"Org choice mode": "Режим вибору організації",
"Org choice mode - Tooltip": "Режим вибору організації підказка",
"Please enable \\\"Signin session\\\" first before enabling \\\"Auto signin\\\"": "Спочатку увімкніть \\\"Сесію входу\\\", перш ніж увімкнути \\\"Автоматичний вхід\\\"",
"Please enable \"Signin session\" first before enabling \"Auto signin\"": "Спочатку увімкніть \"Сесію входу\", перш ніж увімкнути \"Автоматичний вхід\"",
"Please input your application!": "Будь ласка, введіть свою заявку!",
"Please input your organization!": "Будь ласка, введіть вашу організацію!",
"Please select a HTML file": "Виберіть файл HTML",
@@ -1269,6 +1269,17 @@
"Address": "Адреса",
"Address - Tooltip": "Адреса місця проживання",
"Address line": "Адреса (рядок)",
"Addresses": "Адреси",
"Addresses - Tooltip": "Декілька адрес користувача",
"Tag": "Тег",
"Line 1": "Рядок 1",
"Line 2": "Рядок 2",
"City": "Місто",
"State": "Область",
"Zip code": "Поштовий індекс",
"Home": "Дім",
"Work": "Робота",
"Other": "Інше",
"Affiliation": "Приналежність",
"Affiliation - Tooltip": "Роботодавець, наприклад назва компанії чи організації",
"Balance": "Баланс",

View File

@@ -100,7 +100,7 @@
"Order - Tooltip": "Giá trị càng nhỏ, xếp hạng càng cao trong trang Ứng dụng",
"Org choice mode": "Chế độ chọn tổ chức",
"Org choice mode - Tooltip": "Chế độ chọn tổ chức",
"Please enable \\\"Signin session\\\" first before enabling \\\"Auto signin\\\"": "Vui lòng kích hoạt \\\"Phiên đăng nhập\\\" trước khi kích hoạt \\\"Đăng nhập tự động\\\"",
"Please enable \"Signin session\" first before enabling \"Auto signin\"": "Vui lòng kích hoạt \"Phiên đăng nhập\" trước khi kích hoạt \"Đăng nhập tự động\"",
"Please input your application!": "Vui lòng nhập ứng dụng của bạn!",
"Please input your organization!": "Vui lòng nhập tổ chức của bạn!",
"Please select a HTML file": "Vui lòng chọn tệp HTML",
@@ -1269,6 +1269,17 @@
"Address": "Địa chỉ",
"Address - Tooltip": "Địa chỉ cư trú",
"Address line": "Dòng địa chỉ",
"Addresses": "Các địa chỉ",
"Addresses - Tooltip": "Nhiều địa chỉ của người dùng",
"Tag": "Thẻ",
"Line 1": "Dòng 1",
"Line 2": "Dòng 2",
"City": "Thành phố",
"State": "Tiểu bang",
"Zip code": "Mã bưu điện",
"Home": "Nhà",
"Work": "Làm việc",
"Other": "Khác",
"Affiliation": "Liên kết",
"Affiliation - Tooltip": "Nhà tuyển dụng, chẳng hạn như tên công ty hoặc tổ chức",
"Balance": "Số dư",

View File

@@ -100,7 +100,7 @@
"Order - Tooltip": "数值越小,在应用列表页面中排序越靠前",
"Org choice mode": "组织选择模式",
"Org choice mode - Tooltip": "采用什么方式选择要登录的组织",
"Please enable \\\"Signin session\\\" first before enabling \\\"Auto signin\\\"": "开启 \\\"保持登录会话\\\" 后才能开启 \\\"自动登录\\\"",
"Please enable \"Signin session\" first before enabling \"Auto signin\"": "开启 \"保持登录会话\" 后才能开启 \"自动登录\"",
"Please input your application!": "请输入你的应用",
"Please input your organization!": "请输入你的组织",
"Please select a HTML file": "请选择一个HTML文件",
@@ -941,6 +941,9 @@
"Copy": "复制",
"Corp ID": "企业 ID",
"Corp Secret": "企业密钥",
"SCIM Server URL": "SCIM 服务器 URL",
"Username (optional)": "用户名(可选)",
"API Token / Password": "API 令牌 / 密码",
"DB test": "DB test",
"DB test - Tooltip": "DB test - Tooltip",
"Disable SSL": "禁用SSL",
@@ -1284,6 +1287,17 @@
"Address": "地址",
"Address - Tooltip": "居住地址",
"Address line": "地址",
"Addresses": "地址列表",
"Addresses - Tooltip": "用户的多个地址",
"Tag": "标签",
"Line 1": "地址行 1",
"Line 2": "地址行 2",
"City": "城市",
"State": "州/省",
"Zip code": "邮政编码",
"Home": "家庭",
"Work": "工作",
"Other": "其他",
"Affiliation": "工作单位",
"Affiliation - Tooltip": "工作单位,如公司、组织名称",
"Balance": "余额",

View File

@@ -38,7 +38,7 @@ class AccountTable extends React.Component {
}
addRow(table) {
const row = {name: Setting.getNewRowNameForTable(table, "Please select an account item"), visible: true, viewRule: "Public", modifyRule: "Self"};
const row = {name: Setting.getNewRowNameForTable(table, "Please select an account item"), visible: true, viewRule: "Public", modifyRule: "Self", tab: ""};
if (table === undefined) {
table = [];
}
@@ -93,6 +93,19 @@ class AccountTable extends React.Component {
);
},
},
{
title: i18next.t("general:Tab"),
dataIndex: "tab",
key: "tab",
width: "150px",
render: (text, record, index) => {
return (
<Input value={text} placeholder={i18next.t("user:Default")} onChange={e => {
this.updateField(table, index, "tab", e.target.value);
}} />
);
},
},
{
title: i18next.t("signup:Regex"),
dataIndex: "regex",

View File

@@ -0,0 +1,232 @@
// Copyright 2026 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 {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
import {Button, Col, Input, Row, Select, Table, Tooltip} from "antd";
import * as Setting from "../Setting";
import i18next from "i18next";
import RegionSelect from "../common/select/RegionSelect";
const {Option} = Select;
class AddressTable extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
addresses: this.props.table !== null ? this.props.table.map((item, index) => {
item.key = index;
return item;
}) : [],
};
}
count = this.props.table?.length ?? 0;
updateTable(table) {
this.setState({
addresses: table,
});
this.props.onUpdateTable([...table].map((item) => {
const newItem = Setting.deepCopy(item);
delete newItem.key;
return newItem;
}));
}
updateField(table, index, key, value) {
table[index][key] = value;
this.updateTable(table);
}
addRow(table) {
const row = {key: this.count, tag: "", line1: "", line2: "", city: "", state: "", zipCode: "", region: ""};
if (table === undefined || table === null) {
table = [];
}
this.count += 1;
table = Setting.addRow(table, row);
this.updateTable(table);
}
deleteRow(table, i) {
table = Setting.deleteRow(table, i);
this.updateTable(table);
}
upRow(table, i) {
table = Setting.swapRow(table, i - 1, i);
this.updateTable(table);
}
downRow(table, i) {
table = Setting.swapRow(table, i, i + 1);
this.updateTable(table);
}
renderTable(table) {
const columns = [
{
title: i18next.t("user:Tag"),
dataIndex: "tag",
key: "tag",
width: "100px",
render: (text, record, index) => {
return (
<Select virtual={false} style={{width: "100%"}}
value={text}
onChange={value => {
this.updateField(table, index, "tag", value);
}} >
<Option value="Home">{i18next.t("user:Home")}</Option>
<Option value="Work">{i18next.t("user:Work")}</Option>
<Option value="Other">{i18next.t("user:Other")}</Option>
</Select>
);
},
},
{
title: i18next.t("user:Line 1"),
dataIndex: "line1",
key: "line1",
width: "150px",
render: (text, record, index) => {
return (
<Input value={text} onChange={e => {
this.updateField(table, index, "line1", e.target.value);
}} />
);
},
},
{
title: i18next.t("user:Line 2"),
dataIndex: "line2",
key: "line2",
width: "150px",
render: (text, record, index) => {
return (
<Input value={text} onChange={e => {
this.updateField(table, index, "line2", e.target.value);
}} />
);
},
},
{
title: i18next.t("user:City"),
dataIndex: "city",
key: "city",
width: "120px",
render: (text, record, index) => {
return (
<Input value={text} onChange={e => {
this.updateField(table, index, "city", e.target.value);
}} />
);
},
},
{
title: i18next.t("user:State"),
dataIndex: "state",
key: "state",
width: "100px",
render: (text, record, index) => {
return (
<Input value={text} onChange={e => {
this.updateField(table, index, "state", e.target.value);
}} />
);
},
},
{
title: i18next.t("user:Zip code"),
dataIndex: "zipCode",
key: "zipCode",
width: "100px",
render: (text, record, index) => {
return (
<Input value={text} onChange={e => {
this.updateField(table, index, "zipCode", e.target.value);
}} />
);
},
},
{
title: i18next.t("user:Region"),
dataIndex: "region",
key: "region",
width: "150px",
render: (text, record, index) => {
return (
<RegionSelect
value={text}
onChange={value => {
this.updateField(table, index, "region", value);
}}
/>
);
},
},
{
title: i18next.t("general:Action"),
key: "action",
width: "100px",
render: (text, record, index) => {
return (
<div>
<Tooltip placement="bottomLeft" title={i18next.t("general:Up")}>
<Button style={{marginRight: "5px"}} disabled={index === 0} icon={<UpOutlined />} size="small" onClick={() => this.upRow(table, index)} />
</Tooltip>
<Tooltip placement="topLeft" title={i18next.t("general:Down")}>
<Button style={{marginRight: "5px"}} disabled={index === table.length - 1} icon={<DownOutlined />} size="small" onClick={() => this.downRow(table, index)} />
</Tooltip>
<Tooltip placement="topLeft" title={i18next.t("general:Delete")}>
<Button icon={<DeleteOutlined />} size="small" onClick={() => this.deleteRow(table, index)} />
</Tooltip>
</div>
);
},
},
];
return (
<Table scroll={{x: "max-content"}} rowKey="key" columns={columns} dataSource={table} size="middle" bordered pagination={false}
title={() => (
<div>
{this.props.title}&nbsp;&nbsp;&nbsp;&nbsp;
<Button style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button>
</div>
)}
/>
);
}
render() {
return (
<div>
<Row style={{marginTop: "20px"}} >
<Col span={24}>
{
this.renderTable(this.state.addresses)
}
</Col>
</Row>
</div>
);
}
}
export default AddressTable;

View File

@@ -275,22 +275,12 @@ export function getTransactionTableColumns(options = {}) {
title: i18next.t("transaction:Amount"),
dataIndex: "amount",
key: "amount",
width: "120px",
width: "180px",
sorter: getSorter("amount"),
...(getColumnSearchProps ? getColumnSearchProps("amount") : {}),
fixed: (Setting.isMobile()) ? "false" : "right",
});
columns.push({
title: i18next.t("payment:Currency"),
dataIndex: "currency",
key: "currency",
width: "120px",
sorter: getSorter("currency"),
...(getColumnSearchProps ? getColumnSearchProps("currency") : {}),
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
return Setting.getCurrencyWithFlag(text);
return Setting.getPriceDisplay(record.amount, record.currency);
},
});