forked from casdoor/casdoor
Compare commits
28 Commits
v2.255.1
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2885b54d1 | ||
|
|
588015f0bc | ||
|
|
72b70c3b03 | ||
|
|
a1c56894c7 | ||
|
|
a9ae9394c7 | ||
|
|
5f0fa5f23e | ||
|
|
f99aa047a9 | ||
|
|
1d22b7ebd0 | ||
|
|
d147053329 | ||
|
|
0f8cd92be4 | ||
|
|
7ea6f1296d | ||
|
|
db8c649f5e | ||
|
|
a06d003589 | ||
|
|
33298e44d4 | ||
|
|
f4d86f8d92 | ||
|
|
af4337a1ae | ||
|
|
81e650df65 | ||
|
|
fcea1e4c07 | ||
|
|
639a8a47b1 | ||
|
|
43f61d4426 | ||
|
|
e90cdb8a74 | ||
|
|
bfe8955250 | ||
|
|
36b9c4602a | ||
|
|
18117833e1 | ||
|
|
78dde97b64 | ||
|
|
3a06c66057 | ||
|
|
aa59901400 | ||
|
|
8e03b2d97c |
12
Dockerfile
12
Dockerfile
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -35,8 +35,6 @@ import (
|
||||
// @router /place-order [post]
|
||||
func (c *ApiController) PlaceOrder() {
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
pricingName := c.Ctx.Input.Query("pricingName")
|
||||
planName := c.Ctx.Input.Query("planName")
|
||||
paidUserName := c.Ctx.Input.Query("userName")
|
||||
|
||||
var req struct {
|
||||
@@ -82,7 +80,7 @@ func (c *ApiController) PlaceOrder() {
|
||||
return
|
||||
}
|
||||
|
||||
order, err := object.PlaceOrder(owner, productInfos, user, pricingName, planName)
|
||||
order, err := object.PlaceOrder(owner, productInfos, user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
13
idp/goth.go
13
idp/goth.go
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
39
ldap/util.go
39
ldap/util.go
@@ -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"
|
||||
|
||||
@@ -128,7 +128,7 @@ type Application struct {
|
||||
ForgetUrl string `xorm:"varchar(200)" json:"forgetUrl"`
|
||||
AffiliationUrl string `xorm:"varchar(100)" json:"affiliationUrl"`
|
||||
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
|
||||
TermsOfUse string `xorm:"varchar(100)" json:"termsOfUse"`
|
||||
TermsOfUse string `xorm:"varchar(200)" json:"termsOfUse"`
|
||||
SignupHtml string `xorm:"mediumtext" json:"signupHtml"`
|
||||
SigninHtml string `xorm:"mediumtext" json:"signinHtml"`
|
||||
ThemeData *ThemeData `xorm:"json" json:"themeData"`
|
||||
|
||||
@@ -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"},
|
||||
@@ -296,26 +298,47 @@ func initBuiltInLdap() {
|
||||
}
|
||||
|
||||
func initBuiltInProvider() {
|
||||
provider, err := GetProvider(util.GetId("admin", "provider_captcha_default"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
providers := []*Provider{
|
||||
{
|
||||
Owner: "admin",
|
||||
Name: "provider_captcha_default",
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
DisplayName: "Captcha Default",
|
||||
Category: "Captcha",
|
||||
Type: "Default",
|
||||
},
|
||||
{
|
||||
Owner: "admin",
|
||||
Name: "provider_balance",
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
DisplayName: "Balance",
|
||||
Category: "Payment",
|
||||
Type: "Balance",
|
||||
},
|
||||
{
|
||||
Owner: "admin",
|
||||
Name: "provider_payment_dummy",
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
DisplayName: "Dummy Payment",
|
||||
Category: "Payment",
|
||||
Type: "Dummy",
|
||||
},
|
||||
}
|
||||
|
||||
if provider != nil {
|
||||
return
|
||||
}
|
||||
for _, provider := range providers {
|
||||
existingProvider, err := GetProvider(util.GetId("admin", provider.Name))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
provider = &Provider{
|
||||
Owner: "admin",
|
||||
Name: "provider_captcha_default",
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
DisplayName: "Captcha Default",
|
||||
Category: "Captcha",
|
||||
Type: "Default",
|
||||
}
|
||||
_, err = AddProvider(provider)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
if existingProvider != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = AddProvider(provider)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -32,10 +32,6 @@ type Order struct {
|
||||
Products []string `xorm:"varchar(1000)" json:"products"` // Support for multiple products per order. Using varchar(1000) for simple JSON array storage; can be refactored to separate table if needed
|
||||
ProductInfos []ProductInfo `xorm:"mediumtext" json:"productInfos"`
|
||||
|
||||
// Subscription Info (for subscription orders)
|
||||
PricingName string `xorm:"varchar(100)" json:"pricingName"`
|
||||
PlanName string `xorm:"varchar(100)" json:"planName"`
|
||||
|
||||
// User Info
|
||||
User string `xorm:"varchar(100)" json:"user"`
|
||||
|
||||
@@ -54,6 +50,7 @@ type Order struct {
|
||||
}
|
||||
|
||||
type ProductInfo struct {
|
||||
Owner string `json:"owner"`
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Image string `json:"image,omitempty"`
|
||||
@@ -62,6 +59,8 @@ type ProductInfo struct {
|
||||
Currency string `json:"currency,omitempty"`
|
||||
IsRecharge bool `json:"isRecharge,omitempty"`
|
||||
Quantity int `json:"quantity,omitempty"`
|
||||
PricingName string `json:"pricingName,omitempty"`
|
||||
PlanName string `json:"planName,omitempty"`
|
||||
}
|
||||
|
||||
func GetOrderCount(owner, field, value string) (int64, error) {
|
||||
|
||||
@@ -23,25 +23,27 @@ import (
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
func PlaceOrder(owner string, reqProductInfos []ProductInfo, user *User, pricingName string, planName string) (*Order, error) {
|
||||
func PlaceOrder(owner string, reqProductInfos []ProductInfo, user *User) (*Order, error) {
|
||||
if len(reqProductInfos) == 0 {
|
||||
return nil, fmt.Errorf("order has no products")
|
||||
}
|
||||
|
||||
productNames := make([]string, 0, len(reqProductInfos))
|
||||
reqInfoMap := make(map[string]ProductInfo, len(reqProductInfos))
|
||||
for _, reqInfo := range reqProductInfos {
|
||||
if reqInfo.Name == "" {
|
||||
return nil, fmt.Errorf("product name cannot be empty")
|
||||
}
|
||||
productNames = append(productNames, reqInfo.Name)
|
||||
reqInfoMap[reqInfo.Name] = reqInfo
|
||||
}
|
||||
|
||||
products, err := getOrderProducts(owner, productNames)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
productMap := make(map[string]Product, len(reqProductInfos))
|
||||
for _, product := range products {
|
||||
productMap[product.Name] = product
|
||||
}
|
||||
|
||||
orderCurrency := products[0].Currency
|
||||
if orderCurrency == "" {
|
||||
@@ -54,12 +56,12 @@ func PlaceOrder(owner string, reqProductInfos []ProductInfo, user *User, pricing
|
||||
|
||||
var productInfos []ProductInfo
|
||||
orderPrice := 0.0
|
||||
for _, product := range products {
|
||||
reqInfo := reqInfoMap[product.Name]
|
||||
for _, productInfo := range reqProductInfos {
|
||||
product := productMap[productInfo.Name]
|
||||
|
||||
var productPrice float64
|
||||
if product.IsRecharge {
|
||||
productPrice = reqInfo.Price
|
||||
productPrice = productInfo.Price
|
||||
if productPrice <= 0 {
|
||||
return nil, fmt.Errorf("the custom price should be greater than zero")
|
||||
}
|
||||
@@ -67,15 +69,20 @@ func PlaceOrder(owner string, reqProductInfos []ProductInfo, user *User, pricing
|
||||
productPrice = product.Price
|
||||
}
|
||||
productInfos = append(productInfos, ProductInfo{
|
||||
Owner: owner,
|
||||
Name: product.Name,
|
||||
DisplayName: product.DisplayName,
|
||||
Image: product.Image,
|
||||
Detail: product.Detail,
|
||||
Price: productPrice,
|
||||
Currency: product.Currency,
|
||||
IsRecharge: product.IsRecharge,
|
||||
Quantity: productInfo.Quantity,
|
||||
PricingName: productInfo.PricingName,
|
||||
PlanName: productInfo.PlanName,
|
||||
})
|
||||
|
||||
orderPrice += productPrice
|
||||
orderPrice += productPrice * float64(productInfo.Quantity)
|
||||
}
|
||||
|
||||
orderName := fmt.Sprintf("order_%v", util.GenerateTimeId())
|
||||
@@ -86,8 +93,6 @@ func PlaceOrder(owner string, reqProductInfos []ProductInfo, user *User, pricing
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
Products: productNames,
|
||||
ProductInfos: productInfos,
|
||||
PricingName: pricingName,
|
||||
PlanName: planName,
|
||||
User: user.Name,
|
||||
Payment: "", // Payment will be set when user pays
|
||||
Price: orderPrice,
|
||||
@@ -159,15 +164,20 @@ func PayOrder(providerName, host, paymentEnv string, order *Order, lang string)
|
||||
returnUrl := fmt.Sprintf("%s/payments/%s/%s/result", originFrontend, owner, paymentName)
|
||||
notifyUrl := fmt.Sprintf("%s/api/notify-payment/%s/%s", originBackend, owner, paymentName)
|
||||
|
||||
orderProductInfos := order.ProductInfos
|
||||
// Create a subscription when pricing and plan are provided
|
||||
// This allows both free users and paid users to subscribe to plans
|
||||
if order.PricingName != "" && order.PlanName != "" {
|
||||
plan, err := GetPlan(util.GetId(owner, order.PlanName))
|
||||
for i, productInfo := range orderProductInfos {
|
||||
if productInfo.PricingName == "" || productInfo.PlanName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
plan, err := GetPlan(util.GetId(owner, productInfo.PlanName))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if plan == nil {
|
||||
return nil, nil, fmt.Errorf("the plan: %s does not exist", order.PlanName)
|
||||
return nil, nil, fmt.Errorf("the plan: %s does not exist", productInfo.PlanName)
|
||||
}
|
||||
|
||||
sub, err := NewSubscription(owner, user.Name, plan.Name, paymentName, plan.Period)
|
||||
@@ -183,7 +193,9 @@ func PayOrder(providerName, host, paymentEnv string, order *Order, lang string)
|
||||
return nil, nil, fmt.Errorf("failed to add subscription: %s", sub.Name)
|
||||
}
|
||||
|
||||
returnUrl = fmt.Sprintf("%s/buy-plan/%s/%s/result?subscription=%s", originFrontend, owner, order.PricingName, sub.Name)
|
||||
if i == 0 {
|
||||
returnUrl = fmt.Sprintf("%s/buy-plan/%s/%s/result?subscription=%s", originFrontend, owner, productInfo.PricingName, sub.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if baseProduct.SuccessUrl != "" {
|
||||
@@ -294,10 +306,10 @@ func PayOrder(providerName, host, paymentEnv string, order *Order, lang string)
|
||||
|
||||
hasRecharge := false
|
||||
rechargeAmount := 0.0
|
||||
for _, productInfo := range order.ProductInfos {
|
||||
for _, productInfo := range orderProductInfos {
|
||||
if productInfo.IsRecharge {
|
||||
hasRecharge = true
|
||||
rechargeAmount += productInfo.Price
|
||||
rechargeAmount += productInfo.Price * float64(productInfo.Quantity)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,7 +355,7 @@ func PayOrder(providerName, host, paymentEnv string, order *Order, lang string)
|
||||
|
||||
// Update product stock after order state is persisted (for instant payment methods)
|
||||
if provider.Type == "Dummy" || provider.Type == "Balance" {
|
||||
err = UpdateProductStock(products)
|
||||
err = UpdateProductStock(orderProductInfos)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
@@ -313,10 +375,11 @@ func NotifyPayment(body []byte, owner string, paymentName string, lang string) (
|
||||
|
||||
hasRecharge := false
|
||||
rechargeAmount := 0.0
|
||||
for _, productInfo := range order.ProductInfos {
|
||||
orderProductInfos := order.ProductInfos
|
||||
for _, productInfo := range orderProductInfos {
|
||||
if productInfo.IsRecharge {
|
||||
hasRecharge = true
|
||||
rechargeAmount += productInfo.Price
|
||||
rechargeAmount += productInfo.Price * float64(productInfo.Quantity)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,7 +409,7 @@ func NotifyPayment(body []byte, owner string, paymentName string, lang string) (
|
||||
}
|
||||
}
|
||||
|
||||
err = UpdateProductStock(products)
|
||||
err = UpdateProductStock(orderProductInfos)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -98,21 +98,21 @@ func GetProduct(id string) (*Product, error) {
|
||||
return getProduct(owner, name)
|
||||
}
|
||||
|
||||
func UpdateProductStock(products []Product) error {
|
||||
func UpdateProductStock(productInfos []ProductInfo) error {
|
||||
var (
|
||||
affected int64
|
||||
err error
|
||||
)
|
||||
for _, product := range products {
|
||||
for _, product := range productInfos {
|
||||
if product.IsRecharge {
|
||||
affected, err = ormer.Engine.ID(core.PK{product.Owner, product.Name}).
|
||||
Incr("sold", 1).
|
||||
Incr("sold", product.Quantity).
|
||||
Update(&Product{})
|
||||
} else {
|
||||
affected, err = ormer.Engine.ID(core.PK{product.Owner, product.Name}).
|
||||
Where("quantity > 0").
|
||||
Decr("quantity", 1).
|
||||
Incr("sold", 1).
|
||||
Where("quantity >= ?", product.Quantity).
|
||||
Decr("quantity", product.Quantity).
|
||||
Incr("sold", product.Quantity).
|
||||
Update(&Product{})
|
||||
}
|
||||
|
||||
@@ -172,13 +172,37 @@ func checkProduct(product *Product) error {
|
||||
return fmt.Errorf("the product not exist")
|
||||
}
|
||||
|
||||
for _, providerName := range product.Providers {
|
||||
provider, err := getProvider(product.Owner, providerName)
|
||||
if product.Currency == "" {
|
||||
return fmt.Errorf("currency cannot be empty")
|
||||
}
|
||||
|
||||
if len(product.Providers) == 0 {
|
||||
providers, err := GetProvidersByCategory(product.Owner, "Payment")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if provider != nil && provider.Type == "Alipay" && product.Currency != "CNY" {
|
||||
return fmt.Errorf("alipay provider only supports CNY, got: %s", product.Currency)
|
||||
if len(providers) == 0 {
|
||||
return fmt.Errorf("no payment provider available")
|
||||
}
|
||||
|
||||
for _, provider := range providers {
|
||||
if provider.Type != "Alipay" || product.Currency == "CNY" {
|
||||
product.Providers = append(product.Providers, provider.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(product.Providers) == 0 {
|
||||
return fmt.Errorf("no compatible payment provider available for currency: %s", product.Currency)
|
||||
}
|
||||
} else {
|
||||
for _, providerName := range product.Providers {
|
||||
provider, err := getProvider(product.Owner, providerName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if provider != nil && provider.Type == "Alipay" && product.Currency != "CNY" {
|
||||
return fmt.Errorf("alipay provider only supports CNY, got: %s", product.Currency)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -77,6 +77,7 @@ type Provider struct {
|
||||
|
||||
ProviderUrl string `xorm:"varchar(200)" json:"providerUrl"`
|
||||
EnableProxy bool `json:"enableProxy"`
|
||||
EnablePkce bool `json:"enablePkce"`
|
||||
}
|
||||
|
||||
func GetMaskedProvider(provider *Provider, isMaskEnabled bool) *Provider {
|
||||
@@ -132,6 +133,16 @@ func GetProviders(owner string) ([]*Provider, error) {
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
func GetProvidersByCategory(owner string, category string) ([]*Provider, error) {
|
||||
providers := []*Provider{}
|
||||
err := ormer.Engine.Where("(owner = ? or owner = ?) and category = ?", "admin", owner, category).Desc("created_time").Find(&providers, &Provider{})
|
||||
if err != nil {
|
||||
return providers, err
|
||||
}
|
||||
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
func GetGlobalProviders() ([]*Provider, error) {
|
||||
providers := []*Provider{}
|
||||
err := ormer.Engine.Desc("created_time").Find(&providers)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
@@ -126,12 +127,11 @@ func (p *AzureAdSyncerProvider) getAzureAdAccessToken() (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", tokenUrl, nil)
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", tokenUrl, strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req.URL.RawQuery = data.Encode()
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
|
||||
@@ -49,6 +49,12 @@ func GetSyncerProvider(syncer *Syncer) SyncerProvider {
|
||||
return &ActiveDirectorySyncerProvider{Syncer: syncer}
|
||||
case "DingTalk":
|
||||
return &DingtalkSyncerProvider{Syncer: syncer}
|
||||
case "Lark":
|
||||
return &LarkSyncerProvider{Syncer: syncer}
|
||||
case "Okta":
|
||||
return &OktaSyncerProvider{Syncer: syncer}
|
||||
case "SCIM":
|
||||
return &SCIMSyncerProvider{Syncer: syncer}
|
||||
case "Keycloak":
|
||||
return &KeycloakSyncerProvider{
|
||||
DatabaseSyncerProvider: DatabaseSyncerProvider{Syncer: syncer},
|
||||
|
||||
416
object/syncer_lark.go
Normal file
416
object/syncer_lark.go
Normal file
@@ -0,0 +1,416 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
// LarkSyncerProvider implements SyncerProvider for Lark API-based syncers
|
||||
type LarkSyncerProvider struct {
|
||||
Syncer *Syncer
|
||||
}
|
||||
|
||||
// InitAdapter initializes the Lark syncer (no database adapter needed)
|
||||
func (p *LarkSyncerProvider) InitAdapter() error {
|
||||
// Lark syncer doesn't need database adapter
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOriginalUsers retrieves all users from Lark API
|
||||
func (p *LarkSyncerProvider) GetOriginalUsers() ([]*OriginalUser, error) {
|
||||
return p.getLarkUsers()
|
||||
}
|
||||
|
||||
// AddUser adds a new user to Lark (not supported for read-only API)
|
||||
func (p *LarkSyncerProvider) AddUser(user *OriginalUser) (bool, error) {
|
||||
// Lark syncer is typically read-only
|
||||
return false, fmt.Errorf("adding users to Lark is not supported")
|
||||
}
|
||||
|
||||
// UpdateUser updates an existing user in Lark (not supported for read-only API)
|
||||
func (p *LarkSyncerProvider) UpdateUser(user *OriginalUser) (bool, error) {
|
||||
// Lark syncer is typically read-only
|
||||
return false, fmt.Errorf("updating users in Lark is not supported")
|
||||
}
|
||||
|
||||
// TestConnection tests the Lark API connection
|
||||
func (p *LarkSyncerProvider) TestConnection() error {
|
||||
_, err := p.getLarkAccessToken()
|
||||
return err
|
||||
}
|
||||
|
||||
// Close closes any open connections (no-op for Lark API-based syncer)
|
||||
func (p *LarkSyncerProvider) Close() error {
|
||||
// Lark syncer doesn't maintain persistent connections
|
||||
return nil
|
||||
}
|
||||
|
||||
type LarkAccessTokenResp struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
TenantAccessToken string `json:"tenant_access_token"`
|
||||
Expire int `json:"expire"`
|
||||
}
|
||||
|
||||
type LarkUser struct {
|
||||
UserId string `json:"user_id"`
|
||||
UnionId string `json:"union_id"`
|
||||
OpenId string `json:"open_id"`
|
||||
Name string `json:"name"`
|
||||
EnName string `json:"en_name"`
|
||||
Email string `json:"email"`
|
||||
Mobile string `json:"mobile"`
|
||||
Gender int `json:"gender"`
|
||||
Avatar *LarkAvatar `json:"avatar"`
|
||||
Status *LarkStatus `json:"status"`
|
||||
DepartmentIds []string `json:"department_ids"`
|
||||
JobTitle string `json:"job_title"`
|
||||
}
|
||||
|
||||
type LarkAvatar struct {
|
||||
Avatar72 string `json:"avatar_72"`
|
||||
Avatar240 string `json:"avatar_240"`
|
||||
Avatar640 string `json:"avatar_640"`
|
||||
AvatarOrigin string `json:"avatar_origin"`
|
||||
}
|
||||
|
||||
type LarkStatus struct {
|
||||
IsFrozen bool `json:"is_frozen"`
|
||||
IsResigned bool `json:"is_resigned"`
|
||||
IsActivated bool `json:"is_activated"`
|
||||
IsExited bool `json:"is_exited"`
|
||||
}
|
||||
|
||||
type LarkUserListResp struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data struct {
|
||||
Items []*LarkUser `json:"items"`
|
||||
HasMore bool `json:"has_more"`
|
||||
PageToken string `json:"page_token"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type LarkDeptListResp struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data struct {
|
||||
Items []struct {
|
||||
DepartmentId string `json:"department_id"`
|
||||
} `json:"items"`
|
||||
HasMore bool `json:"has_more"`
|
||||
PageToken string `json:"page_token"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// getLarkDomain returns the Lark API domain based on whether global endpoint is used
|
||||
func (p *LarkSyncerProvider) getLarkDomain() string {
|
||||
// syncer.Host can be used to specify custom endpoint
|
||||
// If empty, default to global endpoint (larksuite.com)
|
||||
if p.Syncer.Host != "" {
|
||||
return p.Syncer.Host
|
||||
}
|
||||
return "https://open.larksuite.com"
|
||||
}
|
||||
|
||||
// getLarkAccessToken gets access token from Lark API
|
||||
func (p *LarkSyncerProvider) getLarkAccessToken() (string, error) {
|
||||
// syncer.User should be the app_id
|
||||
// syncer.Password should be the app_secret
|
||||
appId := p.Syncer.User
|
||||
if appId == "" {
|
||||
return "", fmt.Errorf("app_id (user field) is required for Lark syncer")
|
||||
}
|
||||
|
||||
appSecret := p.Syncer.Password
|
||||
if appSecret == "" {
|
||||
return "", fmt.Errorf("app_secret (password field) is required for Lark syncer")
|
||||
}
|
||||
|
||||
domain := p.getLarkDomain()
|
||||
apiUrl := fmt.Sprintf("%s/open-apis/auth/v3/tenant_access_token/internal", domain)
|
||||
|
||||
postData := map[string]string{
|
||||
"app_id": appId,
|
||||
"app_secret": appSecret,
|
||||
}
|
||||
|
||||
data, err := p.postJSON(apiUrl, postData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var tokenResp LarkAccessTokenResp
|
||||
err = json.Unmarshal(data, &tokenResp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if tokenResp.Code != 0 {
|
||||
return "", fmt.Errorf("failed to get access token: code=%d, msg=%s",
|
||||
tokenResp.Code, tokenResp.Msg)
|
||||
}
|
||||
|
||||
return tokenResp.TenantAccessToken, nil
|
||||
}
|
||||
|
||||
// getLarkDepartments gets all department IDs from Lark API
|
||||
func (p *LarkSyncerProvider) getLarkDepartments(accessToken string) ([]string, error) {
|
||||
domain := p.getLarkDomain()
|
||||
allDeptIds := []string{"0"} // Start with root department
|
||||
pageToken := ""
|
||||
|
||||
for {
|
||||
apiUrl := fmt.Sprintf("%s/open-apis/contact/v3/departments?parent_department_id=0&fetch_child=true&page_size=50", domain)
|
||||
if pageToken != "" {
|
||||
apiUrl += fmt.Sprintf("&page_token=%s", pageToken)
|
||||
}
|
||||
|
||||
data, err := p.getWithAuth(apiUrl, accessToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var deptResp LarkDeptListResp
|
||||
err = json.Unmarshal(data, &deptResp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if deptResp.Code != 0 {
|
||||
return nil, fmt.Errorf("failed to get departments: code=%d, msg=%s",
|
||||
deptResp.Code, deptResp.Msg)
|
||||
}
|
||||
|
||||
for _, dept := range deptResp.Data.Items {
|
||||
allDeptIds = append(allDeptIds, dept.DepartmentId)
|
||||
}
|
||||
|
||||
if !deptResp.Data.HasMore {
|
||||
break
|
||||
}
|
||||
pageToken = deptResp.Data.PageToken
|
||||
}
|
||||
|
||||
return allDeptIds, nil
|
||||
}
|
||||
|
||||
// getLarkUsersFromDept gets users from a specific department
|
||||
func (p *LarkSyncerProvider) getLarkUsersFromDept(accessToken string, deptId string) ([]*LarkUser, error) {
|
||||
domain := p.getLarkDomain()
|
||||
allUsers := []*LarkUser{}
|
||||
pageToken := ""
|
||||
|
||||
for {
|
||||
apiUrl := fmt.Sprintf("%s/open-apis/contact/v3/users/find_by_department?department_id=%s&page_size=50", domain, deptId)
|
||||
if pageToken != "" {
|
||||
apiUrl += fmt.Sprintf("&page_token=%s", pageToken)
|
||||
}
|
||||
|
||||
data, err := p.getWithAuth(apiUrl, accessToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var userResp LarkUserListResp
|
||||
err = json.Unmarshal(data, &userResp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if userResp.Code != 0 {
|
||||
return nil, fmt.Errorf("failed to get users from dept %s: code=%d, msg=%s",
|
||||
deptId, userResp.Code, userResp.Msg)
|
||||
}
|
||||
|
||||
allUsers = append(allUsers, userResp.Data.Items...)
|
||||
|
||||
if !userResp.Data.HasMore {
|
||||
break
|
||||
}
|
||||
pageToken = userResp.Data.PageToken
|
||||
}
|
||||
|
||||
return allUsers, nil
|
||||
}
|
||||
|
||||
// postJSON sends a POST request with JSON body
|
||||
func (p *LarkSyncerProvider) postJSON(url string, data interface{}) ([]byte, error) {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(jsonData))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respData, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return respData, nil
|
||||
}
|
||||
|
||||
// getWithAuth sends a GET request with authorization header
|
||||
func (p *LarkSyncerProvider) getWithAuth(url string, accessToken string) ([]byte, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// getLarkUsers gets all users from Lark API
|
||||
func (p *LarkSyncerProvider) getLarkUsers() ([]*OriginalUser, error) {
|
||||
// Get access token
|
||||
accessToken, err := p.getLarkAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get all departments
|
||||
deptIds, err := p.getLarkDepartments(accessToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get users from all departments (deduplicate by user_id)
|
||||
userMap := make(map[string]*LarkUser)
|
||||
for _, deptId := range deptIds {
|
||||
users, err := p.getLarkUsersFromDept(accessToken, deptId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
// Deduplicate users by user_id
|
||||
if _, exists := userMap[user.UserId]; !exists {
|
||||
userMap[user.UserId] = user
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert Lark users to Casdoor OriginalUser
|
||||
originalUsers := []*OriginalUser{}
|
||||
for _, larkUser := range userMap {
|
||||
originalUser := p.larkUserToOriginalUser(larkUser)
|
||||
originalUsers = append(originalUsers, originalUser)
|
||||
}
|
||||
|
||||
return originalUsers, nil
|
||||
}
|
||||
|
||||
// larkUserToOriginalUser converts Lark user to Casdoor OriginalUser
|
||||
func (p *LarkSyncerProvider) larkUserToOriginalUser(larkUser *LarkUser) *OriginalUser {
|
||||
// Use user_id as name, fallback to union_id or open_id
|
||||
userName := larkUser.UserId
|
||||
if userName == "" && larkUser.UnionId != "" {
|
||||
userName = larkUser.UnionId
|
||||
}
|
||||
if userName == "" && larkUser.OpenId != "" {
|
||||
userName = larkUser.OpenId
|
||||
}
|
||||
|
||||
user := &OriginalUser{
|
||||
Id: larkUser.UserId,
|
||||
Name: userName,
|
||||
DisplayName: larkUser.Name,
|
||||
Email: larkUser.Email,
|
||||
Phone: larkUser.Mobile,
|
||||
Title: larkUser.JobTitle,
|
||||
Address: []string{},
|
||||
Properties: map[string]string{},
|
||||
Groups: []string{},
|
||||
}
|
||||
|
||||
// Set avatar if available
|
||||
if larkUser.Avatar != nil {
|
||||
if larkUser.Avatar.Avatar240 != "" {
|
||||
user.Avatar = larkUser.Avatar.Avatar240
|
||||
} else if larkUser.Avatar.Avatar72 != "" {
|
||||
user.Avatar = larkUser.Avatar.Avatar72
|
||||
}
|
||||
}
|
||||
|
||||
// Set gender
|
||||
switch larkUser.Gender {
|
||||
case 1:
|
||||
user.Gender = "Male"
|
||||
case 2:
|
||||
user.Gender = "Female"
|
||||
default:
|
||||
user.Gender = ""
|
||||
}
|
||||
|
||||
// Set IsForbidden based on status
|
||||
// User is forbidden if frozen, resigned, not activated, or exited
|
||||
if larkUser.Status != nil {
|
||||
if larkUser.Status.IsFrozen || larkUser.Status.IsResigned || !larkUser.Status.IsActivated || larkUser.Status.IsExited {
|
||||
user.IsForbidden = true
|
||||
} else {
|
||||
user.IsForbidden = false
|
||||
}
|
||||
}
|
||||
|
||||
// Set CreatedTime to current time if not set
|
||||
if user.CreatedTime == "" {
|
||||
user.CreatedTime = util.GetCurrentTime()
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
298
object/syncer_okta.go
Normal file
298
object/syncer_okta.go
Normal file
@@ -0,0 +1,298 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
// OktaSyncerProvider implements SyncerProvider for Okta API-based syncers
|
||||
type OktaSyncerProvider struct {
|
||||
Syncer *Syncer
|
||||
}
|
||||
|
||||
// InitAdapter initializes the Okta syncer (no database adapter needed)
|
||||
func (p *OktaSyncerProvider) InitAdapter() error {
|
||||
// Okta syncer doesn't need database adapter
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOriginalUsers retrieves all users from Okta API
|
||||
func (p *OktaSyncerProvider) GetOriginalUsers() ([]*OriginalUser, error) {
|
||||
return p.getOktaOriginalUsers()
|
||||
}
|
||||
|
||||
// AddUser adds a new user to Okta (not supported for read-only API)
|
||||
func (p *OktaSyncerProvider) AddUser(user *OriginalUser) (bool, error) {
|
||||
// Okta syncer is typically read-only
|
||||
return false, fmt.Errorf("adding users to Okta is not supported")
|
||||
}
|
||||
|
||||
// UpdateUser updates an existing user in Okta (not supported for read-only API)
|
||||
func (p *OktaSyncerProvider) UpdateUser(user *OriginalUser) (bool, error) {
|
||||
// Okta syncer is typically read-only
|
||||
return false, fmt.Errorf("updating users in Okta is not supported")
|
||||
}
|
||||
|
||||
// TestConnection tests the Okta API connection
|
||||
func (p *OktaSyncerProvider) TestConnection() error {
|
||||
// Try to fetch first page of users to verify connection
|
||||
_, _, err := p.getOktaUsers("")
|
||||
return err
|
||||
}
|
||||
|
||||
// Close closes any open connections (no-op for Okta API-based syncer)
|
||||
func (p *OktaSyncerProvider) Close() error {
|
||||
// Okta syncer doesn't maintain persistent connections
|
||||
return nil
|
||||
}
|
||||
|
||||
// OktaUser represents a user from Okta API
|
||||
type OktaUser struct {
|
||||
Id string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
Created string `json:"created"`
|
||||
Profile struct {
|
||||
Login string `json:"login"`
|
||||
Email string `json:"email"`
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
DisplayName string `json:"displayName"`
|
||||
MobilePhone string `json:"mobilePhone"`
|
||||
PrimaryPhone string `json:"primaryPhone"`
|
||||
StreetAddress string `json:"streetAddress"`
|
||||
City string `json:"city"`
|
||||
State string `json:"state"`
|
||||
ZipCode string `json:"zipCode"`
|
||||
CountryCode string `json:"countryCode"`
|
||||
PostalAddress string `json:"postalAddress"`
|
||||
PreferredLanguage string `json:"preferredLanguage"`
|
||||
Locale string `json:"locale"`
|
||||
Timezone string `json:"timezone"`
|
||||
Title string `json:"title"`
|
||||
Department string `json:"department"`
|
||||
Organization string `json:"organization"`
|
||||
} `json:"profile"`
|
||||
}
|
||||
|
||||
// parseLinkHeader parses the HTTP Link header
|
||||
// Format: <https://example.com/api/v1/users?after=xyz>; rel="next"
|
||||
func parseLinkHeader(header string) map[string]string {
|
||||
links := make(map[string]string)
|
||||
parts := strings.Split(header, ",")
|
||||
for _, part := range parts {
|
||||
section := strings.Split(strings.TrimSpace(part), ";")
|
||||
if len(section) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
url := strings.Trim(strings.TrimSpace(section[0]), "<>")
|
||||
rel := strings.TrimSpace(section[1])
|
||||
|
||||
if strings.HasPrefix(rel, "rel=\"") && strings.HasSuffix(rel, "\"") {
|
||||
relValue := rel[5 : len(rel)-1]
|
||||
links[relValue] = url
|
||||
}
|
||||
}
|
||||
return links
|
||||
}
|
||||
|
||||
// getOktaUsers retrieves users from Okta API with pagination support
|
||||
// Returns users and the next page link (if any)
|
||||
func (p *OktaSyncerProvider) getOktaUsers(nextLink string) ([]*OktaUser, string, error) {
|
||||
// syncer.Host should be the Okta domain (e.g., "dev-12345.okta.com" or full URL)
|
||||
// syncer.Password should be the API token
|
||||
|
||||
domain := p.Syncer.Host
|
||||
if domain == "" {
|
||||
return nil, "", fmt.Errorf("Okta domain (host field) is required for Okta syncer")
|
||||
}
|
||||
|
||||
apiToken := p.Syncer.Password
|
||||
if apiToken == "" {
|
||||
return nil, "", fmt.Errorf("API token (password field) is required for Okta syncer")
|
||||
}
|
||||
|
||||
// Construct API URL
|
||||
var apiUrl string
|
||||
if nextLink != "" {
|
||||
apiUrl = nextLink
|
||||
} else {
|
||||
// Remove https:// or http:// prefix if present in domain
|
||||
domain = strings.TrimPrefix(strings.TrimPrefix(domain, "https://"), "http://")
|
||||
apiUrl = fmt.Sprintf("https://%s/api/v1/users?limit=200", domain)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", apiUrl, nil)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "SSWS "+apiToken)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
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 from Okta: status=%d, body=%s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
var users []*OktaUser
|
||||
err = json.Unmarshal(body, &users)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// Parse Link header for next page
|
||||
// Link header format: <https://...>; rel="next"
|
||||
nextPageLink := ""
|
||||
linkHeader := resp.Header.Get("Link")
|
||||
if linkHeader != "" {
|
||||
links := parseLinkHeader(linkHeader)
|
||||
if next, ok := links["next"]; ok {
|
||||
nextPageLink = next
|
||||
}
|
||||
}
|
||||
|
||||
return users, nextPageLink, nil
|
||||
}
|
||||
|
||||
// oktaUserToOriginalUser converts Okta user to Casdoor OriginalUser
|
||||
func (p *OktaSyncerProvider) oktaUserToOriginalUser(oktaUser *OktaUser) *OriginalUser {
|
||||
user := &OriginalUser{
|
||||
Id: oktaUser.Id,
|
||||
Name: oktaUser.Profile.Login,
|
||||
DisplayName: oktaUser.Profile.DisplayName,
|
||||
FirstName: oktaUser.Profile.FirstName,
|
||||
LastName: oktaUser.Profile.LastName,
|
||||
Email: oktaUser.Profile.Email,
|
||||
Phone: oktaUser.Profile.MobilePhone,
|
||||
Title: oktaUser.Profile.Title,
|
||||
Language: oktaUser.Profile.PreferredLanguage,
|
||||
Address: []string{},
|
||||
Properties: map[string]string{},
|
||||
Groups: []string{},
|
||||
}
|
||||
|
||||
// Build address from street, city, state, zip
|
||||
if oktaUser.Profile.StreetAddress != "" {
|
||||
user.Address = append(user.Address, oktaUser.Profile.StreetAddress)
|
||||
}
|
||||
if oktaUser.Profile.City != "" {
|
||||
user.Address = append(user.Address, oktaUser.Profile.City)
|
||||
}
|
||||
if oktaUser.Profile.State != "" {
|
||||
user.Address = append(user.Address, oktaUser.Profile.State)
|
||||
}
|
||||
if oktaUser.Profile.ZipCode != "" {
|
||||
user.Address = append(user.Address, oktaUser.Profile.ZipCode)
|
||||
}
|
||||
|
||||
// Store additional properties
|
||||
if oktaUser.Profile.Department != "" {
|
||||
user.Properties["department"] = oktaUser.Profile.Department
|
||||
}
|
||||
if oktaUser.Profile.Organization != "" {
|
||||
user.Properties["organization"] = oktaUser.Profile.Organization
|
||||
}
|
||||
if oktaUser.Profile.Timezone != "" {
|
||||
user.Properties["timezone"] = oktaUser.Profile.Timezone
|
||||
}
|
||||
|
||||
// Set IsForbidden based on status
|
||||
// Okta status values: STAGED, PROVISIONED, ACTIVE, RECOVERY, PASSWORD_EXPIRED, LOCKED_OUT, SUSPENDED, DEPROVISIONED
|
||||
if oktaUser.Status == "SUSPENDED" || oktaUser.Status == "DEPROVISIONED" || oktaUser.Status == "LOCKED_OUT" {
|
||||
user.IsForbidden = true
|
||||
} else {
|
||||
user.IsForbidden = false
|
||||
}
|
||||
|
||||
// If display name is 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))
|
||||
}
|
||||
|
||||
// If email is empty, use login as email (typically login is an email)
|
||||
if user.Email == "" && oktaUser.Profile.Login != "" {
|
||||
user.Email = oktaUser.Profile.Login
|
||||
}
|
||||
|
||||
// If mobile phone is empty, try primary phone
|
||||
if user.Phone == "" && oktaUser.Profile.PrimaryPhone != "" {
|
||||
user.Phone = oktaUser.Profile.PrimaryPhone
|
||||
}
|
||||
|
||||
// Set CreatedTime to current time if not set
|
||||
if user.CreatedTime == "" {
|
||||
user.CreatedTime = util.GetCurrentTime()
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
// getOktaOriginalUsers is the main entry point for Okta syncer
|
||||
func (p *OktaSyncerProvider) getOktaOriginalUsers() ([]*OriginalUser, error) {
|
||||
allUsers := []*OktaUser{}
|
||||
nextLink := ""
|
||||
|
||||
// Fetch all users with pagination
|
||||
for {
|
||||
users, next, err := p.getOktaUsers(nextLink)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allUsers = append(allUsers, users...)
|
||||
|
||||
// If there's no next link, we've fetched all users
|
||||
if next == "" {
|
||||
break
|
||||
}
|
||||
|
||||
nextLink = next
|
||||
}
|
||||
|
||||
// Convert Okta users to Casdoor OriginalUser
|
||||
originalUsers := []*OriginalUser{}
|
||||
for _, oktaUser := range allUsers {
|
||||
originalUser := p.oktaUserToOriginalUser(oktaUser)
|
||||
originalUsers = append(originalUsers, originalUser)
|
||||
}
|
||||
|
||||
return originalUsers, nil
|
||||
}
|
||||
337
object/syncer_scim.go
Normal file
337
object/syncer_scim.go
Normal 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
198
object/syncer_scim_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
119
object/user.go
119
object/user.go
@@ -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"`
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,11 +17,92 @@ import {Link} from "react-router-dom";
|
||||
import {Button, Table} from "antd";
|
||||
import * as Setting from "./Setting";
|
||||
import * as UserBackend from "./backend/UserBackend";
|
||||
import * as OrderBackend from "./backend/OrderBackend";
|
||||
import * as ProductBackend from "./backend/ProductBackend";
|
||||
import i18next from "i18next";
|
||||
import BaseListPage from "./BaseListPage";
|
||||
import PopconfirmModal from "./common/modal/PopconfirmModal";
|
||||
|
||||
class CartListPage extends BaseListPage {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
...this.state,
|
||||
data: [],
|
||||
user: null,
|
||||
isPlacingOrder: false,
|
||||
loading: false,
|
||||
pagination: {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
},
|
||||
searchText: "",
|
||||
searchedColumn: "",
|
||||
};
|
||||
}
|
||||
|
||||
clearCart() {
|
||||
const user = Setting.deepCopy(this.state.user);
|
||||
if (user === undefined || user === null) {
|
||||
Setting.showMessage("error", i18next.t("general:Failed to delete"));
|
||||
return;
|
||||
}
|
||||
|
||||
user.cart = [];
|
||||
UserBackend.updateUser(user.owner, user.name, user)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
|
||||
this.fetch();
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
placeOrder() {
|
||||
if (this.state.isPlacingOrder) {
|
||||
return;
|
||||
}
|
||||
|
||||
const owner = this.state.user?.owner || this.props.account.owner;
|
||||
const carts = this.state.data || [];
|
||||
if (carts.length === 0) {
|
||||
Setting.showMessage("error", i18next.t("product:Product list cannot be empty"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({isPlacingOrder: true});
|
||||
|
||||
const productInfos = carts.map(item => ({
|
||||
name: item.name,
|
||||
price: item.price,
|
||||
quantity: item.quantity,
|
||||
pricingName: item.pricingName,
|
||||
planName: item.planName,
|
||||
}));
|
||||
|
||||
OrderBackend.placeOrder(owner, productInfos, this.state.user?.name)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
const order = res.data;
|
||||
Setting.showMessage("success", i18next.t("product:Order created successfully"));
|
||||
Setting.goToLink(`/orders/${order.owner}/${order.name}/pay`);
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("product:Failed to create order")}: ${res.msg}`);
|
||||
this.setState({isPlacingOrder: false});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
this.setState({isPlacingOrder: false});
|
||||
});
|
||||
}
|
||||
|
||||
deleteCart(record) {
|
||||
const user = Setting.deepCopy(this.state.user);
|
||||
if (user === undefined || user === null || !Array.isArray(user.cart)) {
|
||||
@@ -29,7 +110,7 @@ class CartListPage extends BaseListPage {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = user.cart.findIndex(item => item.name === record.name && item.price === record.price);
|
||||
const index = user.cart.findIndex(item => item.name === record.name && item.price === record.price && (item.pricingName || "") === (record.pricingName || "") && (item.planName || "") === (record.planName || ""));
|
||||
if (index === -1) {
|
||||
Setting.showMessage("error", i18next.t("general:Failed to delete"));
|
||||
return;
|
||||
@@ -52,8 +133,18 @@ class CartListPage extends BaseListPage {
|
||||
}
|
||||
|
||||
renderTable(carts) {
|
||||
const isEmpty = carts === undefined || carts === null || carts.length === 0;
|
||||
const owner = this.state.user?.owner || this.props.account.owner;
|
||||
|
||||
let total = 0;
|
||||
let currency = "";
|
||||
if (carts && carts.length > 0) {
|
||||
carts.forEach(item => {
|
||||
total += item.price * item.quantity;
|
||||
});
|
||||
currency = carts[0].currency;
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("general:Name"),
|
||||
@@ -91,22 +182,46 @@ 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).toFixed(2);
|
||||
return Setting.getPriceDisplay(subtotal, record.currency);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("pricing:Pricing name"),
|
||||
dataIndex: "pricingName",
|
||||
key: "pricingName",
|
||||
width: "140px",
|
||||
sorter: true,
|
||||
render: (text, record) => {
|
||||
if (!text) {return null;}
|
||||
return (
|
||||
<Link to={`/pricings/${owner}/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("plan:Plan name"),
|
||||
dataIndex: "planName",
|
||||
key: "planName",
|
||||
width: "140px",
|
||||
sorter: true,
|
||||
render: (text, record) => {
|
||||
if (!text) {return null;}
|
||||
return (
|
||||
<Link to={`/plans/${owner}/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("product:Quantity"),
|
||||
@@ -125,7 +240,7 @@ class CartListPage extends BaseListPage {
|
||||
return (
|
||||
<div style={{display: "flex", flexWrap: "wrap", gap: "8px"}}>
|
||||
<Button type="primary" onClick={() => this.props.history.push(`/products/${owner}/${record.name}/buy`)}>
|
||||
{i18next.t("product:Buy")}
|
||||
{i18next.t("product:Detail")}
|
||||
</Button>
|
||||
<PopconfirmModal
|
||||
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
|
||||
@@ -138,36 +253,58 @@ class CartListPage extends BaseListPage {
|
||||
},
|
||||
];
|
||||
|
||||
const paginationProps = {
|
||||
total: this.state.pagination.total,
|
||||
showQuickJumper: true,
|
||||
showSizeChanger: true,
|
||||
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table
|
||||
scroll={{x: "max-content"}}
|
||||
columns={columns}
|
||||
dataSource={carts}
|
||||
rowKey={(record, index) => `${record.name}-${index}`}
|
||||
rowKey={(record, index) => `${record.name}-${record.pricingName}-${record.planName}-${index}`}
|
||||
size="middle"
|
||||
bordered
|
||||
pagination={paginationProps}
|
||||
pagination={false}
|
||||
title={() => {
|
||||
return (
|
||||
<div>
|
||||
{i18next.t("general:Carts")}
|
||||
<Button type="primary" size="small" onClick={() => this.props.history.push("/product-store")}>{i18next.t("general:Add")}</Button>
|
||||
{i18next.t("general:Cart")}
|
||||
<Button size="small" onClick={() => this.props.history.push("/product-store")}>{i18next.t("general:Add")}</Button>
|
||||
|
||||
<Button size="small">{i18next.t("general:Place Order")}</Button>
|
||||
<PopconfirmModal
|
||||
size="small"
|
||||
style={{marginRight: "8px"}}
|
||||
text={i18next.t("general:Clear")}
|
||||
title={i18next.t("general:Sure to delete") + `: ${i18next.t("general:Cart")} ?`}
|
||||
onConfirm={() => this.clearCart()}
|
||||
disabled={isEmpty}
|
||||
/>
|
||||
<Button type="primary" size="small" onClick={() => this.placeOrder()} disabled={isEmpty || this.state.isPlacingOrder} loading={this.state.isPlacingOrder}>{i18next.t("general:Place Order")}</Button>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
loading={this.state.loading}
|
||||
onChange={this.handleTableChange}
|
||||
/>
|
||||
|
||||
{!isEmpty && (
|
||||
<div style={{marginTop: "20px", display: "flex", flexDirection: "column", justifyContent: "center", alignItems: "center", gap: "20px"}}>
|
||||
<div style={{display: "flex", alignItems: "center", fontSize: "18px", fontWeight: "bold"}}>
|
||||
{i18next.t("product:Total Price")}:
|
||||
<span style={{color: "red", fontSize: "28px"}}>
|
||||
{Setting.getCurrencySymbol(currency)}{total.toFixed(2)} ({Setting.getCurrencyText(currency)})
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
style={{height: "50px", fontSize: "20px", padding: "0 40px", borderRadius: "5px"}}
|
||||
onClick={() => this.placeOrder()}
|
||||
disabled={this.state.isPlacingOrder}
|
||||
loading={this.state.isPlacingOrder}
|
||||
>
|
||||
{i18next.t("general:Place Order")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -178,23 +315,42 @@ class CartListPage extends BaseListPage {
|
||||
const userName = this.props.account.name;
|
||||
|
||||
UserBackend.getUser(organizationName, userName)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
.then(async(res) => {
|
||||
if (res.status === "ok") {
|
||||
const cartData = res.data.cart || [];
|
||||
|
||||
const productPromises = cartData.map(item =>
|
||||
ProductBackend.getProduct(organizationName, item.name)
|
||||
.then(pRes => {
|
||||
if (pRes.status === "ok" && pRes.data) {
|
||||
return {
|
||||
...pRes.data,
|
||||
pricingName: item.pricingName,
|
||||
planName: item.planName,
|
||||
quantity: item.quantity,
|
||||
price: pRes.data.isRecharge ? item.price : pRes.data.price,
|
||||
};
|
||||
}
|
||||
return item;
|
||||
})
|
||||
.catch(() => item)
|
||||
);
|
||||
|
||||
const fullCartData = await Promise.all(productPromises);
|
||||
|
||||
this.setState({
|
||||
data: cartData,
|
||||
loading: false,
|
||||
data: fullCartData,
|
||||
user: res.data,
|
||||
pagination: {
|
||||
...params.pagination,
|
||||
total: cartData.length,
|
||||
total: fullCartData.length,
|
||||
},
|
||||
searchText: params.searchText,
|
||||
searchedColumn: params.searchedColumn,
|
||||
});
|
||||
} else {
|
||||
this.setState({loading: false});
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
})
|
||||
|
||||
@@ -350,7 +350,7 @@ function ManagementPage(props) {
|
||||
res.push(Setting.getItem(<Link style={{color: textColor}} to="/products">{i18next.t("general:Business & Payments")}</Link>, "/business", <DollarTwoTone twoToneColor={twoToneColor} />, [
|
||||
Setting.getItem(<Link to="/product-store">{i18next.t("general:Product Store")}</Link>, "/product-store"),
|
||||
Setting.getItem(<Link to="/products">{i18next.t("general:Products")}</Link>, "/products"),
|
||||
Setting.getItem(<Link to="/carts">{i18next.t("general:Carts")}</Link>, "/carts"),
|
||||
Setting.getItem(<Link to="/cart">{i18next.t("general:Cart")}</Link>, "/cart"),
|
||||
Setting.getItem(<Link to="/orders">{i18next.t("general:Orders")}</Link>, "/orders"),
|
||||
Setting.getItem(<Link to="/payments">{i18next.t("general:Payments")}</Link>, "/payments"),
|
||||
Setting.getItem(<Link to="/plans">{i18next.t("general:Plans")}</Link>, "/plans"),
|
||||
@@ -491,7 +491,7 @@ function ManagementPage(props) {
|
||||
<Route exact path="/products" render={(props) => renderLoginIfNotLoggedIn(<ProductListPage account={account} {...props} />)} />
|
||||
<Route exact path="/products/:organizationName/:productName" render={(props) => renderLoginIfNotLoggedIn(<ProductEditPage account={account} {...props} />)} />
|
||||
<Route exact path="/products/:organizationName/:productName/buy" render={(props) => renderLoginIfNotLoggedIn(<ProductBuyPage account={account} {...props} />)} />
|
||||
<Route exact path="/carts" render={(props) => renderLoginIfNotLoggedIn(<CartListPage account={account} {...props} />)} />
|
||||
<Route exact path="/cart" render={(props) => renderLoginIfNotLoggedIn(<CartListPage account={account} {...props} />)} />
|
||||
<Route exact path="/orders" render={(props) => renderLoginIfNotLoggedIn(<OrderListPage account={account} {...props} />)} />
|
||||
<Route exact path="/orders/:organizationName/:orderName" render={(props) => renderLoginIfNotLoggedIn(<OrderEditPage account={account} {...props} />)} />
|
||||
<Route exact path="/orders/:organizationName/:orderName/pay" render={(props) => renderLoginIfNotLoggedIn(<OrderPayPage account={account} {...props} />)} />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -256,25 +256,26 @@ class OrderListPage extends BaseListPage {
|
||||
title: i18next.t("general:Action"),
|
||||
dataIndex: "",
|
||||
key: "op",
|
||||
width: "300px",
|
||||
width: "320px",
|
||||
fixed: (Setting.isMobile()) ? "false" : "right",
|
||||
render: (text, record, index) => {
|
||||
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`)} disabled={record.state !== "Created"}>
|
||||
{i18next.t("order:Pay")}
|
||||
<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>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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,11 +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) {
|
||||
return `${Setting.getCurrencySymbol(this.state.order?.currency)}${product.price} (${Setting.getCurrencyText(this.state.order)})`;
|
||||
const price = product.price * (product.quantity ?? 1);
|
||||
return `${Setting.getCurrencySymbol(this.state.order?.currency)}${price.toFixed(2)} (${Setting.getCurrencyText(this.state.order?.currency)})`;
|
||||
}
|
||||
|
||||
// Call Wechat Pay via jsapi
|
||||
@@ -180,6 +181,7 @@ class OrderPayPage extends React.Component {
|
||||
getPayButton(provider, onClick) {
|
||||
const providerTypeMap = {
|
||||
"Dummy": i18next.t("product:Dummy"),
|
||||
"Balance": i18next.t("user:Balance"),
|
||||
"Alipay": i18next.t("product:Alipay"),
|
||||
"WeChat Pay": i18next.t("product:WeChat Pay"),
|
||||
"PayPal": i18next.t("product:PayPal"),
|
||||
@@ -217,25 +219,48 @@ class OrderPayPage extends React.Component {
|
||||
}
|
||||
|
||||
renderProduct(product) {
|
||||
const isSubscriptionOrder = product.pricingName && product.planName;
|
||||
|
||||
return (
|
||||
<React.Fragment key={product.name}>
|
||||
<Descriptions.Item label={i18next.t("general:Name")} span={3}>
|
||||
<span style={{fontSize: 20}}>
|
||||
{Setting.getLanguageText(product?.displayName)}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("product:Image")} span={3}>
|
||||
<img src={product?.image} alt={Setting.getLanguageText(product?.displayName)} height={90} style={{marginBottom: "20px"}} />
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("product:Price")} span={3}>
|
||||
<span style={{fontSize: 18, fontWeight: "bold"}}>
|
||||
{this.getProductPrice(product)}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("product:Detail")} span={3}>
|
||||
<span style={{fontSize: 16}}>{Setting.getLanguageText(product?.detail)}</span>
|
||||
</Descriptions.Item>
|
||||
</React.Fragment>
|
||||
<div key={product.name} style={{marginBottom: "20px", border: "1px solid #f0f0f0", borderRadius: "2px", padding: "1px"}}>
|
||||
<Descriptions bordered column={2} size="middle" labelStyle={{width: "150px"}}>
|
||||
<Descriptions.Item label={i18next.t("general:Name")} span={2}>
|
||||
<span style={{fontSize: 20, fontWeight: "500"}}>
|
||||
{Setting.getLanguageText(product?.displayName)}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("product:Image")} span={2}>
|
||||
<img src={product?.image} alt={Setting.getLanguageText(product?.displayName)} height={90} style={{objectFit: "contain"}} />
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item label={i18next.t("product:Price")} span={1}>
|
||||
<span style={{fontSize: 18, fontWeight: "bold"}}>
|
||||
{this.getProductPrice(product)}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("product:Quantity")} span={1}>
|
||||
<span style={{fontSize: 18}}>
|
||||
{product.quantity ?? 1}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
|
||||
{product?.detail && (
|
||||
<Descriptions.Item label={i18next.t("product:Detail")} span={2}>
|
||||
<span style={{fontSize: 16}}>{Setting.getLanguageText(product?.detail)}</span>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
{isSubscriptionOrder && (
|
||||
<>
|
||||
<Descriptions.Item label={i18next.t("subscription:Subscription plan")} span={1}>
|
||||
<span style={{fontSize: 16}}>{Setting.getLanguageText(product?.planName)}</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("subscription:Subscription pricing")} span={1}>
|
||||
<span style={{fontSize: 16}}>{Setting.getLanguageText(product?.pricingName)}</span>
|
||||
</Descriptions.Item>
|
||||
</>
|
||||
)}
|
||||
</Descriptions>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -246,19 +271,17 @@ class OrderPayPage extends React.Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isSubscriptionOrder = order.pricingName && order.planName;
|
||||
|
||||
return (
|
||||
<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 Information")}</span>} bordered column={3}>
|
||||
<Descriptions.Item label={i18next.t("order: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:Order Status")}>
|
||||
<Descriptions.Item label={i18next.t("general:Status")}>
|
||||
<span style={{fontSize: 16}}>
|
||||
{order.state}
|
||||
</span>
|
||||
@@ -277,26 +300,14 @@ class OrderPayPage extends React.Component {
|
||||
</div>
|
||||
|
||||
<div style={{marginBottom: "20px"}}>
|
||||
<Descriptions title={<span style={Setting.isMobile() ? {fontSize: 18} : {fontSize: 24}}>{i18next.t("product:Product Information")}</span>} bordered column={3}>
|
||||
{productInfos.map(product => this.renderProduct(product))}
|
||||
</Descriptions>
|
||||
<div style={{fontSize: Setting.isMobile() ? 18 : 24, fontWeight: "bold", marginBottom: "16px", color: "rgba(0, 0, 0, 0.85)"}}>
|
||||
{i18next.t("product:Information")}
|
||||
</div>
|
||||
{productInfos.map(product => this.renderProduct(product))}
|
||||
</div>
|
||||
|
||||
{isSubscriptionOrder && (
|
||||
<div style={{marginBottom: "20px"}}>
|
||||
<Descriptions title={<span style={Setting.isMobile() ? {fontSize: 18} : {fontSize: 24}}>{i18next.t("subscription:Subscription Information")}</span>} bordered column={3}>
|
||||
<Descriptions.Item label={i18next.t("general:Plan")} span={3}>
|
||||
<span style={{fontSize: 16}}>{order.planName}</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("general:Pricing")} span={3}>
|
||||
<span style={{fontSize: 16}}>{order.pricingName}</span>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Descriptions title={<span style={Setting.isMobile() ? {fontSize: 18} : {fontSize: 24}}>{i18next.t("payment:Payment Information")}</span>} bordered column={3}>
|
||||
<Descriptions title={<span style={Setting.isMobile() ? {fontSize: 18} : {fontSize: 24}}>{i18next.t("order:Payment")}</span>} bordered column={3}>
|
||||
<Descriptions.Item label={i18next.t("product:Price")} span={3}>
|
||||
<span style={{fontSize: 28, color: "red", fontWeight: "bold"}}>
|
||||
{this.getPrice(order)}
|
||||
|
||||
@@ -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"))} :
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -39,7 +39,7 @@ class ProductBuyPage extends React.Component {
|
||||
plan: null,
|
||||
isPlacingOrder: false,
|
||||
isAddingToCart: false,
|
||||
customPrice: 0,
|
||||
customPrice: 100,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -108,10 +108,16 @@ class ProductBuyPage extends React.Component {
|
||||
product: res.data,
|
||||
});
|
||||
|
||||
if (res.data.isRecharge && res.data.rechargeOptions?.length > 0) {
|
||||
this.setState({
|
||||
customPrice: res.data.rechargeOptions[0],
|
||||
});
|
||||
if (res.data.isRecharge) {
|
||||
if (res.data.rechargeOptions?.length > 0) {
|
||||
this.setState({
|
||||
customPrice: res.data.rechargeOptions[0],
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
customPrice: 100,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
Setting.showMessage("error", err.message);
|
||||
@@ -128,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) {
|
||||
@@ -157,6 +163,8 @@ class ProductBuyPage extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
const pricingName = this.state.pricingName || "";
|
||||
const planName = this.state.planName || "";
|
||||
if (cart.length > 0) {
|
||||
const firstItem = cart[0];
|
||||
if (firstItem.currency && product.currency && firstItem.currency !== product.currency) {
|
||||
@@ -166,20 +174,18 @@ class ProductBuyPage extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
const existingItemIndex = cart.findIndex(item => item.name === product.name && item.price === actualPrice);
|
||||
const existingItemIndex = cart.findIndex(item => item.name === product.name && item.price === actualPrice && (item.pricingName || "") === pricingName && (item.planName || "") === planName);
|
||||
|
||||
if (existingItemIndex !== -1) {
|
||||
cart[existingItemIndex].quantity += 1;
|
||||
} else {
|
||||
const newProductInfo = {
|
||||
name: product.name,
|
||||
displayName: product.displayName,
|
||||
image: product.image,
|
||||
detail: product.detail,
|
||||
price: actualPrice,
|
||||
currency: product.currency,
|
||||
pricingName: pricingName,
|
||||
planName: planName,
|
||||
quantity: 1,
|
||||
isRecharge: product.isRecharge,
|
||||
};
|
||||
cart.push(newProductInfo);
|
||||
}
|
||||
@@ -222,9 +228,12 @@ class ProductBuyPage extends React.Component {
|
||||
const productInfos = [{
|
||||
name: product.name,
|
||||
price: product.isRecharge ? customPrice : product.price,
|
||||
pricingName: pricingName,
|
||||
planName: planName,
|
||||
quantity: 1,
|
||||
}];
|
||||
|
||||
OrderBackend.placeOrder(product.owner, productInfos, pricingName, planName, this.state.userName ?? "")
|
||||
OrderBackend.placeOrder(product.owner, productInfos, this.state.userName ?? "")
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
const order = res.data;
|
||||
@@ -292,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>
|
||||
);
|
||||
@@ -310,7 +319,7 @@ class ProductBuyPage extends React.Component {
|
||||
const hasOptions = product.rechargeOptions && product.rechargeOptions.length > 0;
|
||||
const disableCustom = product.disableCustomRecharge;
|
||||
const isRechargeUnpurchasable = product.isRecharge && !hasOptions && disableCustom;
|
||||
const isSubscription = product.tag === "Subscription";
|
||||
const isAmountZero = product.isRecharge && (this.state.customPrice === 0 || this.state.customPrice === null);
|
||||
|
||||
return (
|
||||
<div style={{display: "flex", justifyContent: "center", alignItems: "center", gap: "20px"}}>
|
||||
@@ -325,29 +334,27 @@ class ProductBuyPage extends React.Component {
|
||||
paddingRight: "60px",
|
||||
}}
|
||||
onClick={() => this.placeOrder(product)}
|
||||
disabled={this.state.isPlacingOrder || isRechargeUnpurchasable}
|
||||
disabled={this.state.isPlacingOrder || isRechargeUnpurchasable || isAmountZero}
|
||||
loading={this.state.isPlacingOrder}
|
||||
>
|
||||
{i18next.t("order:Place Order")}
|
||||
</Button>
|
||||
{!isSubscription && (
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
style={{
|
||||
height: "50px",
|
||||
fontSize: "18px",
|
||||
borderRadius: "30px",
|
||||
paddingLeft: "30px",
|
||||
paddingRight: "30px",
|
||||
}}
|
||||
onClick={() => this.addToCart(product)}
|
||||
disabled={isRechargeUnpurchasable || this.state.isAddingToCart}
|
||||
loading={this.state.isAddingToCart}
|
||||
>
|
||||
{i18next.t("product:Add to cart")}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="default"
|
||||
size="large"
|
||||
style={{
|
||||
height: "50px",
|
||||
fontSize: "18px",
|
||||
borderRadius: "30px",
|
||||
paddingLeft: "30px",
|
||||
paddingRight: "30px",
|
||||
}}
|
||||
onClick={() => this.addToCart(product)}
|
||||
disabled={isRechargeUnpurchasable || this.state.isAddingToCart || isAmountZero}
|
||||
loading={this.state.isAddingToCart}
|
||||
>
|
||||
{i18next.t("product:Add to cart")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -374,6 +374,14 @@ class ProductEditPage extends React.Component {
|
||||
|
||||
submitProductEdit(exitAfterSave) {
|
||||
const product = Setting.deepCopy(this.state.product);
|
||||
if (!product.currency) {
|
||||
Setting.showMessage("error", i18next.t("product:Please select a currency"));
|
||||
return;
|
||||
}
|
||||
if (!product.isCreatedByPlan && (!product.providers || product.providers.length === 0)) {
|
||||
Setting.showMessage("error", i18next.t("product:Please select at least one payment provider"));
|
||||
return;
|
||||
}
|
||||
if (product.isRecharge && product.disableCustomRecharge && (!product.rechargeOptions || product.rechargeOptions.length === 0)) {
|
||||
Setting.showMessage("error", i18next.t("product:Please add at least one recharge option when custom amount is disabled"));
|
||||
return;
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -29,7 +29,7 @@ class ProductStorePage extends React.Component {
|
||||
this.state = {
|
||||
products: [],
|
||||
loading: true,
|
||||
isAddingToCart: false,
|
||||
addingToCartProducts: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -60,11 +60,11 @@ class ProductStorePage extends React.Component {
|
||||
}
|
||||
|
||||
addToCart(product) {
|
||||
if (this.state.isAddingToCart) {
|
||||
if (this.state.addingToCartProducts.includes(product.name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({isAddingToCart: true});
|
||||
this.setState(prevState => ({addingToCartProducts: [...prevState.addingToCartProducts, product.name]}));
|
||||
|
||||
const userOwner = this.props.account.owner;
|
||||
const userName = this.props.account.name;
|
||||
@@ -77,10 +77,9 @@ class ProductStorePage extends React.Component {
|
||||
|
||||
if (cart.length > 0) {
|
||||
const firstItem = cart[0];
|
||||
|
||||
if (firstItem.currency && product.currency && firstItem.currency !== product.currency) {
|
||||
Setting.showMessage("error", i18next.t("product:The currency of the product you are adding is different from the currency of the items in the cart"));
|
||||
this.setState({isAddingToCart: false});
|
||||
this.setState(prevState => ({addingToCartProducts: prevState.addingToCartProducts.filter(name => name !== product.name)}));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -92,13 +91,11 @@ class ProductStorePage extends React.Component {
|
||||
} else {
|
||||
const newCartProductInfo = {
|
||||
name: product.name,
|
||||
displayName: product.displayName,
|
||||
image: product.image,
|
||||
detail: product.detail,
|
||||
price: product.price,
|
||||
currency: product.currency,
|
||||
pricingName: "",
|
||||
planName: "",
|
||||
quantity: 1,
|
||||
isRecharge: product.isRecharge || false,
|
||||
};
|
||||
cart.push(newCartProductInfo);
|
||||
}
|
||||
@@ -116,16 +113,16 @@ class ProductStorePage extends React.Component {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
})
|
||||
.finally(() => {
|
||||
this.setState({isAddingToCart: false});
|
||||
this.setState(prevState => ({addingToCartProducts: prevState.addingToCartProducts.filter(name => name !== product.name)}));
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
this.setState({isAddingToCart: false});
|
||||
this.setState(prevState => ({addingToCartProducts: prevState.addingToCartProducts.filter(name => name !== product.name)}));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
this.setState({isAddingToCart: false});
|
||||
this.setState(prevState => ({addingToCartProducts: prevState.addingToCartProducts.filter(name => name !== product.name)}));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -134,8 +131,7 @@ class ProductStorePage extends React.Component {
|
||||
}
|
||||
|
||||
renderProductCard(product) {
|
||||
const isSubscription = product.tag === "Subscription";
|
||||
|
||||
const isAdding = this.state.addingToCartProducts.includes(product.name);
|
||||
return (
|
||||
<Col xs={24} sm={12} md={8} lg={6} key={`${product.owner}/${product.name}`} style={{marginBottom: "20px"}}>
|
||||
<Card
|
||||
@@ -163,16 +159,16 @@ class ProductStorePage extends React.Component {
|
||||
>
|
||||
{i18next.t("product:Buy")}
|
||||
</Button>
|
||||
{!product.isRecharge && !isSubscription && (
|
||||
{!product.isRecharge && (
|
||||
<Button
|
||||
key="add"
|
||||
type="primary"
|
||||
type="default"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
this.addToCart(product);
|
||||
}}
|
||||
disabled={this.state.isAddingToCart}
|
||||
loading={this.state.isAddingToCart}
|
||||
disabled={isAdding}
|
||||
loading={isAdding}
|
||||
>
|
||||
{i18next.t("product:Add to cart")}
|
||||
</Button>
|
||||
|
||||
@@ -898,6 +898,16 @@ class ProviderEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Enable PKCE"), i18next.t("provider:Enable PKCE - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Switch checked={this.state.provider.enablePkce} onChange={checked => {
|
||||
this.updateProviderField("enablePkce", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
) : null
|
||||
}
|
||||
|
||||
@@ -1115,6 +1115,10 @@ export function getAvatarColor(s) {
|
||||
}
|
||||
|
||||
export function getLanguageText(text) {
|
||||
if (!text) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (!text.includes("|")) {
|
||||
return text;
|
||||
}
|
||||
@@ -1864,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);
|
||||
@@ -1882,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}`;
|
||||
@@ -2009,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)";
|
||||
|
||||
@@ -512,6 +512,204 @@ class SyncerEditPage extends React.Component {
|
||||
"values": [],
|
||||
},
|
||||
];
|
||||
case "Lark":
|
||||
return [
|
||||
{
|
||||
"name": "user_id",
|
||||
"type": "string",
|
||||
"casdoorName": "Id",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string",
|
||||
"casdoorName": "DisplayName",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "email",
|
||||
"type": "string",
|
||||
"casdoorName": "Email",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "mobile",
|
||||
"type": "string",
|
||||
"casdoorName": "Phone",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "avatar",
|
||||
"type": "string",
|
||||
"casdoorName": "Avatar",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "job_title",
|
||||
"type": "string",
|
||||
"casdoorName": "Title",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "gender",
|
||||
"type": "number",
|
||||
"casdoorName": "Gender",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
];
|
||||
case "Okta":
|
||||
return [
|
||||
{
|
||||
"name": "id",
|
||||
"type": "string",
|
||||
"casdoorName": "Id",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "profile.login",
|
||||
"type": "string",
|
||||
"casdoorName": "Name",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "profile.displayName",
|
||||
"type": "string",
|
||||
"casdoorName": "DisplayName",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "profile.firstName",
|
||||
"type": "string",
|
||||
"casdoorName": "FirstName",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "profile.lastName",
|
||||
"type": "string",
|
||||
"casdoorName": "LastName",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "profile.email",
|
||||
"type": "string",
|
||||
"casdoorName": "Email",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "profile.mobilePhone",
|
||||
"type": "string",
|
||||
"casdoorName": "Phone",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "profile.title",
|
||||
"type": "string",
|
||||
"casdoorName": "Title",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "profile.preferredLanguage",
|
||||
"type": "string",
|
||||
"casdoorName": "Language",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"type": "string",
|
||||
"casdoorName": "IsForbidden",
|
||||
"isHashed": true,
|
||||
"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 [];
|
||||
}
|
||||
@@ -568,14 +766,14 @@ class SyncerEditPage extends React.Component {
|
||||
});
|
||||
})}>
|
||||
{
|
||||
["Database", "Keycloak", "WeCom", "Azure AD", "Active Directory", "Google Workspace", "DingTalk"]
|
||||
["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" ? 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"))} :
|
||||
@@ -627,10 +825,10 @@ class SyncerEditPage extends React.Component {
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.syncer.type === "WeCom" || this.state.syncer.type === "DingTalk" ? null : (
|
||||
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 => {
|
||||
@@ -641,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" ? 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"))} :
|
||||
@@ -661,9 +859,11 @@ class SyncerEditPage extends React.Component {
|
||||
{Setting.getLabel(
|
||||
this.state.syncer.type === "WeCom" ? i18next.t("syncer:Corp ID") :
|
||||
this.state.syncer.type === "DingTalk" ? i18next.t("provider:App Key") :
|
||||
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 === "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") :
|
||||
this.state.syncer.type === "SCIM" ? i18next.t("syncer:Username (optional)") :
|
||||
i18next.t("general:User"),
|
||||
i18next.t("general:User - Tooltip")
|
||||
)} :
|
||||
</Col>
|
||||
@@ -680,9 +880,11 @@ class SyncerEditPage extends React.Component {
|
||||
{Setting.getLabel(
|
||||
this.state.syncer.type === "WeCom" ? i18next.t("syncer:Corp secret") :
|
||||
this.state.syncer.type === "DingTalk" ? 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 === "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") :
|
||||
this.state.syncer.type === "SCIM" ? i18next.t("syncer:API Token / Password") :
|
||||
i18next.t("general:Password"),
|
||||
i18next.t("general:Password - Tooltip")
|
||||
)} :
|
||||
</Col>
|
||||
@@ -701,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" ? 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"))} :
|
||||
@@ -797,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" ? 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"))} :
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 !== "") {
|
||||
@@ -497,7 +544,11 @@ export function getAuthUrl(application, provider, method, code) {
|
||||
} else if (provider.type === "Kwai") {
|
||||
return `${endpoint}?app_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
|
||||
} else if (type === "Custom") {
|
||||
return `${provider.customAuthUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${provider.scopes}&response_type=code&state=${state}`;
|
||||
let authUrl = `${provider.customAuthUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${provider.scopes}&response_type=code&state=${state}`;
|
||||
if (provider.enablePkce) {
|
||||
authUrl += `&code_challenge=${codeChallenge}&code_challenge_method=S256`;
|
||||
}
|
||||
return authUrl;
|
||||
} else if (provider.type === "Bilibili") {
|
||||
return `${endpoint}#/?client_id=${provider.clientId}&return_url=${redirectUri}&state=${state}&response_type=code`;
|
||||
} else if (provider.type === "Deezer") {
|
||||
|
||||
@@ -90,8 +90,8 @@ export function deleteOrder(order) {
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function placeOrder(owner, productInfos, pricingName = "", planName = "", userName = "") {
|
||||
return fetch(`${Setting.ServerUrl}/api/place-order?owner=${encodeURIComponent(owner)}&pricingName=${encodeURIComponent(pricingName)}&planName=${encodeURIComponent(planName)}&userName=${encodeURIComponent(userName)}`, {
|
||||
export function placeOrder(owner, productInfos, userName = "") {
|
||||
return fetch(`${Setting.ServerUrl}/api/place-order?owner=${encodeURIComponent(owner)}&userName=${encodeURIComponent(userName)}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify({productInfos}),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
@@ -969,6 +972,8 @@
|
||||
"Email regex - Tooltip": "Only emails matching this regular expression can register or sign in",
|
||||
"Email title": "Email title",
|
||||
"Email title - Tooltip": "Subject of the email",
|
||||
"Enable PKCE": "Enable PKCE",
|
||||
"Enable PKCE - Tooltip": "Enable PKCE (Proof Key for Code Exchange) for enhanced OAuth 2.0 security",
|
||||
"Enable proxy": "Enable proxy",
|
||||
"Enable proxy - Tooltip": "Enable socks5 Proxy when sending email or sms",
|
||||
"Endpoint": "Endpoint",
|
||||
@@ -1299,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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "残高",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Баланс",
|
||||
|
||||
@@ -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ư",
|
||||
|
||||
@@ -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": "余额",
|
||||
|
||||
@@ -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",
|
||||
|
||||
232
web/src/table/AddressTable.js
Normal file
232
web/src/table/AddressTable.js
Normal 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}
|
||||
<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;
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user