support Nextcloud OAuth 2.0 authentication

This commit is contained in:
MaysWind
2025-10-21 01:52:28 +08:00
parent 600ae2bd58
commit 53a8ad71c6
74 changed files with 2046 additions and 241 deletions
+3
View File
@@ -0,0 +1,3 @@
export const OAUTH2_PROVIDER_DISPLAY_NAME: Record<string, string> = {
'nextcloud': 'Nextcloud'
}
+16
View File
@@ -3,6 +3,14 @@ function getServerSetting(key: string): string | number | boolean | Record<strin
return settings[key];
}
export function isInternalAuthEnabled(): boolean {
return getServerSetting('a') !== 0;
}
export function isOAuth2Enabled(): boolean {
return getServerSetting('o') === 1;
}
export function isUserRegistrationEnabled(): boolean {
return getServerSetting('r') === 1;
}
@@ -31,6 +39,14 @@ export function isDataImportingEnabled(): boolean {
return getServerSetting('i') === 1;
}
export function getOAuth2Provider(): string {
return getServerSetting('op') as string;
}
export function getOIDCCustomDisplayNames(): Record<string, string>{
return getServerSetting('ocn') as Record<string, string>;
}
export function isMCPServerEnabled(): boolean {
return getServerSetting('mcp') === 1;
}
+13
View File
@@ -135,6 +135,9 @@ import type {
UserProfileUpdateRequest,
UserProfileUpdateResponse
} from '@/models/user.ts';
import type {
OAuth2CallbackLoginRequest
} from '@/models/oauth2.ts';
import type {
UserApplicationCloudSettingsUpdateRequest
} from '@/models/user_app_cloud_setting.ts';
@@ -265,6 +268,13 @@ export default {
}
});
},
authorizeOAuth2: ({ req, token }: { req: OAuth2CallbackLoginRequest, token: string }): ApiResponsePromise<AuthResponse> => {
return axios.post<ApiResponse<AuthResponse>>('oauth2/authorize.json', req, {
headers: {
Authorization: `Bearer ${token}`
}
});
},
register: (req: UserRegisterRequest): ApiResponsePromise<RegisterResponse> => {
return axios.post<ApiResponse<RegisterResponse>>('register.json', req);
},
@@ -695,6 +705,9 @@ export default {
cancelRequest: (cancelableUuid: string) => {
cancelableRequests[cancelableUuid] = true;
},
generateOAuth2LoginUrl: (platform: 'mobile' | 'desktop', clientSessionId: string): string => {
return `${getBasePath()}/oauth2/login?platform=${platform}&client_session_id=${clientSessionId}`;
},
generateQrCodeUrl: (qrCodeName: string): string => {
return `${getBasePath()}${BASE_QRCODE_PATH}/${qrCodeName}.png`;
},
+19 -1
View File
@@ -108,6 +108,7 @@
},
"misc": {
"multiTextJoinSeparator": ", ",
"loginWithCustomProvider": "Log in with {name}",
"hoursBehindDefaultTimezone": "{hours} Stunde(n) hinter der Standardzeitzone",
"hoursAheadOfDefaultTimezone": "{hours} Stunde(n) vor der Standardzeitzone",
"hoursMinutesBehindDefaultTimezone": "{hours} Stunde(n) und {minutes} Minute(n) hinter der Standardzeitzone",
@@ -129,7 +130,8 @@
"moveTransactionsInAccountTip": "You CANNOT undo this action. This will move all transactions from {fromAccount} to {toAccount}.",
"clearTransactionsInAccountTip": "You CANNOT undo this action. This will clear your transactions data in {account}. Please enter your current password to confirm.",
"accountActivationAndResendValidationEmailTip": "Ein Aktivierungslink wurde an Ihre E-Mail-Adresse gesendet: {email}. Wenn Sie die E-Mail nicht erhalten haben, geben Sie bitte das Passwort erneut ein und klicken Sie auf die Schaltfläche unten, um die Bestätigungs-E-Mail erneut zu senden.",
"resendValidationEmailTip": "Wenn Sie die E-Mail nicht erhalten haben, geben Sie bitte das Passwort erneut ein und klicken Sie auf die Schaltfläche unten, um die Bestätigungs-E-Mail an: {email} erneut zu senden."
"resendValidationEmailTip": "Wenn Sie die E-Mail nicht erhalten haben, geben Sie bitte das Passwort erneut ein und klicken Sie auf die Schaltfläche unten, um die Bestätigungs-E-Mail an: {email} erneut zu senden.",
"oauth2bindTip": "You're signing in to the {userName} user using {providerName}. Please enter your ezBookkeeping password to verify."
}
},
"dataExport": {
@@ -1086,6 +1088,7 @@
"user avatar file extension invalid": "Dateierweiterung des Benutzeravatars ist ungültig",
"exceed the maximum size of user avatar file": "Hochgeladener Benutzeravatar überschreitet die maximal zulässige Dateigröße",
"not permitted to perform this action": "Sie sind nicht berechtigt, diese Aktion auszuführen",
"cannot login by password": "You cannot login by password",
"unauthorized access": "Unbefugter Zugriff",
"current token is invalid": "Aktuelles Token ist ungültig",
"current token is expired": "Aktuelles Token ist abgelaufen",
@@ -1238,6 +1241,17 @@
"image for AI recognition is empty": "Image for AI recognition file is empty",
"exceed the maximum size of image file for AI recognition": "The uploaded image for AI recognition exceeds the maximum allowed file size",
"no transaction information detected": "No transaction information detected",
"user external auth is not found": "User external authentication is not found",
"oauth 2.0 not enabled": "OAuth 2.0 is not enabled",
"oauth 2.0 auto registration not enabled": "OAuth 2.0 auto registration is not enabled",
"invalid oauth 2.0 login request": "Invalid OAuth 2.0 login request",
"invalid oauth 2.0 callback": "Invalid OAuth 2.0 callback",
"missing state in oauth 2.0 callback": "Missing state parameter in OAuth 2.0 callback",
"missing code in oauth 2.0 callback": "Missing code parameter in OAuth 2.0 callback",
"invalid state in oauth 2.0 callback": "Invalid state parameter in OAuth 2.0 callback",
"cannot retrieve oauth 2.0 token": "Cannot retrieve OAuth 2.0 token",
"invalid oauth 2.0 token": "Invalid OAuth 2.0 token",
"cannot retrieve user info from oauth 2.0 provider": "Cannot retrieve user info from OAuth 2.0 provider",
"query items cannot be blank": "Abfrageelemente dürfen nicht leer sein",
"query items too much": "Zu viele Abfrageelemente",
"query items have invalid item": "Ungültiges Element in Abfrageelementen",
@@ -1391,6 +1405,7 @@
"Operation": "Vorgang",
"Open": "Open",
"Close": "Schließen",
"or": "or",
"Submit": "Einreichen",
"Add": "Hinzufügen",
"Import": "Importieren",
@@ -1572,7 +1587,10 @@
"This month or later": "Dieser Monat oder später",
"This year or later": "Dieses Jahr oder später",
"Log In": "Anmelden",
"Log in with OAuth 2.0": "Log in with OAuth 2.0",
"Log in with Connect ID": "Log in with Connect ID",
"Click here to log in": "Hier klicken, um sich anzumelden",
"Logging in...": "Logging in...",
"Back to login page": "Zurück zur Anmeldeseite",
"Back to home page": "Zurück zur Startseite",
"Don't have an account?": "Sie haben kein Konto?",
+19 -1
View File
@@ -108,6 +108,7 @@
},
"misc": {
"multiTextJoinSeparator": ", ",
"loginWithCustomProvider": "Log in with {name}",
"hoursBehindDefaultTimezone": "{hours} hour(s) behind default timezone",
"hoursAheadOfDefaultTimezone": "{hours} hour(s) ahead of default timezone",
"hoursMinutesBehindDefaultTimezone": "{hours} hour(s) and {minutes} minutes behind default timezone",
@@ -129,7 +130,8 @@
"moveTransactionsInAccountTip": "You CANNOT undo this action. This will move all transactions from {fromAccount} to {toAccount}.",
"clearTransactionsInAccountTip": "You CANNOT undo this action. This will clear your transactions data in {account}. Please enter your current password to confirm.",
"accountActivationAndResendValidationEmailTip": "Account activation link has been sent to your email address: {email}, If you don't receive the mail, please fill password again and click the button below to resend the validation mail.",
"resendValidationEmailTip": "If you don't receive the mail, please fill password again and click the button below to resend the validation mail to: {email}"
"resendValidationEmailTip": "If you don't receive the mail, please fill password again and click the button below to resend the validation mail to: {email}",
"oauth2bindTip": "You're signing in to the {userName} user using {providerName}. Please enter your ezBookkeeping password to verify."
}
},
"dataExport": {
@@ -1086,6 +1088,7 @@
"user avatar file extension invalid": "User avatar file extension is invalid",
"exceed the maximum size of user avatar file": "The uploaded user avatar exceeds the maximum allowed file size",
"not permitted to perform this action": "You are not permitted to perform this action",
"cannot login by password": "You cannot login by password",
"unauthorized access": "Unauthorized access",
"current token is invalid": "Current token is invalid",
"current token is expired": "Current token is expired",
@@ -1238,6 +1241,17 @@
"image for AI recognition is empty": "Image for AI recognition file is empty",
"exceed the maximum size of image file for AI recognition": "The uploaded image for AI recognition exceeds the maximum allowed file size",
"no transaction information detected": "No transaction information detected",
"user external auth is not found": "User external authentication is not found",
"oauth 2.0 not enabled": "OAuth 2.0 is not enabled",
"oauth 2.0 auto registration not enabled": "OAuth 2.0 auto registration is not enabled",
"invalid oauth 2.0 login request": "Invalid OAuth 2.0 login request",
"invalid oauth 2.0 callback": "Invalid OAuth 2.0 callback",
"missing state in oauth 2.0 callback": "Missing state parameter in OAuth 2.0 callback",
"missing code in oauth 2.0 callback": "Missing code parameter in OAuth 2.0 callback",
"invalid state in oauth 2.0 callback": "Invalid state parameter in OAuth 2.0 callback",
"cannot retrieve oauth 2.0 token": "Cannot retrieve OAuth 2.0 token",
"invalid oauth 2.0 token": "Invalid OAuth 2.0 token",
"cannot retrieve user info from oauth 2.0 provider": "Cannot retrieve user info from OAuth 2.0 provider",
"query items cannot be blank": "There are no query items",
"query items too much": "There are too many query items",
"query items have invalid item": "There is invalid item in query items",
@@ -1391,6 +1405,7 @@
"Operation": "Operation",
"Open": "Open",
"Close": "Close",
"or": "or",
"Submit": "Submit",
"Add": "Add",
"Import": "Import",
@@ -1572,7 +1587,10 @@
"This month or later": "This month or later",
"This year or later": "This year or later",
"Log In": "Log In",
"Log in with OAuth 2.0": "Log in with OAuth 2.0",
"Log in with Connect ID": "Log in with Connect ID",
"Click here to log in": "Click here to log in",
"Logging in...": "Logging in...",
"Back to login page": "Back to login page",
"Back to home page": "Back to home page",
"Don't have an account?": "Don't have an account?",
+19 -1
View File
@@ -108,6 +108,7 @@
},
"misc": {
"multiTextJoinSeparator": ", ",
"loginWithCustomProvider": "Log in with {name}",
"hoursBehindDefaultTimezone": "{hours} hora(s) de retraso en la zona horaria predeterminada",
"hoursAheadOfDefaultTimezone": "{hours} hora(s) por delante de la zona horaria predeterminada",
"hoursMinutesBehindDefaultTimezone": "{hours} hora(s) y {minutes} minutos de retraso en la zona horaria predeterminada",
@@ -129,7 +130,8 @@
"moveTransactionsInAccountTip": "You CANNOT undo this action. This will move all transactions from {fromAccount} to {toAccount}.",
"clearTransactionsInAccountTip": "You CANNOT undo this action. This will clear your transactions data in {account}. Please enter your current password to confirm.",
"accountActivationAndResendValidationEmailTip": "El enlace de activación de la cuenta se envió a su dirección de correo electrónico: {email}. Si no recibe el correo, ingrese la contraseña nuevamente y haga clic en el botón a continuación para reenviar el correo de validación.",
"resendValidationEmailTip": "Si no recibe el correo, complete nuevamente la contraseña y haga clic en el botón a continuación para reenviar el correo de validación a: {email}"
"resendValidationEmailTip": "Si no recibe el correo, complete nuevamente la contraseña y haga clic en el botón a continuación para reenviar el correo de validación a: {email}",
"oauth2bindTip": "You're signing in to the {userName} user using {providerName}. Please enter your ezBookkeeping password to verify."
}
},
"dataExport": {
@@ -1086,6 +1088,7 @@
"user avatar file extension invalid": "La extensión del archivo de avatar del usuario no es válida",
"exceed the maximum size of user avatar file": "El avatar de usuario subido supera el tamaño de archivo máximo permitido",
"not permitted to perform this action": "No tienes permiso para realizar esta acción.",
"cannot login by password": "You cannot login by password",
"unauthorized access": "Acceso no autorizado",
"current token is invalid": "El token actual no es válido",
"current token is expired": "El token actual ha caducado",
@@ -1238,6 +1241,17 @@
"image for AI recognition is empty": "Image for AI recognition file is empty",
"exceed the maximum size of image file for AI recognition": "The uploaded image for AI recognition exceeds the maximum allowed file size",
"no transaction information detected": "No transaction information detected",
"user external auth is not found": "User external authentication is not found",
"oauth 2.0 not enabled": "OAuth 2.0 is not enabled",
"oauth 2.0 auto registration not enabled": "OAuth 2.0 auto registration is not enabled",
"invalid oauth 2.0 login request": "Invalid OAuth 2.0 login request",
"invalid oauth 2.0 callback": "Invalid OAuth 2.0 callback",
"missing state in oauth 2.0 callback": "Missing state parameter in OAuth 2.0 callback",
"missing code in oauth 2.0 callback": "Missing code parameter in OAuth 2.0 callback",
"invalid state in oauth 2.0 callback": "Invalid state parameter in OAuth 2.0 callback",
"cannot retrieve oauth 2.0 token": "Cannot retrieve OAuth 2.0 token",
"invalid oauth 2.0 token": "Invalid OAuth 2.0 token",
"cannot retrieve user info from oauth 2.0 provider": "Cannot retrieve user info from OAuth 2.0 provider",
"query items cannot be blank": "--",
"query items too much": "--",
"query items have invalid item": "Hay un elemento no válido en los elementos de consulta",
@@ -1391,6 +1405,7 @@
"Operation": "Operación",
"Open": "Open",
"Close": "Cerrar",
"or": "or",
"Submit": "Enviar",
"Add": "Agregar",
"Import": "Importar",
@@ -1572,7 +1587,10 @@
"This month or later": "Este mes o más tarde",
"This year or later": "Este año o más tarde",
"Log In": "Acceso",
"Log in with OAuth 2.0": "Log in with OAuth 2.0",
"Log in with Connect ID": "Log in with Connect ID",
"Click here to log in": "Haga clic aquí para iniciar sesión",
"Logging in...": "Logging in...",
"Back to login page": "Volver a la página de inicio de sesión",
"Back to home page": "Volver a la página de inicio",
"Don't have an account?": "¿No tienes una cuenta?",
+19 -1
View File
@@ -108,6 +108,7 @@
},
"misc": {
"multiTextJoinSeparator": ", ",
"loginWithCustomProvider": "Log in with {name}",
"hoursBehindDefaultTimezone": "{hours} heure(s) de retard sur le fuseau horaire par défaut",
"hoursAheadOfDefaultTimezone": "{hours} heure(s) d'avance sur le fuseau horaire par défaut",
"hoursMinutesBehindDefaultTimezone": "{hours} heure(s) et {minutes} minutes de retard sur le fuseau horaire par défaut",
@@ -129,7 +130,8 @@
"moveTransactionsInAccountTip": "You CANNOT undo this action. This will move all transactions from {fromAccount} to {toAccount}.",
"clearTransactionsInAccountTip": "You CANNOT undo this action. This will clear your transactions data in {account}. Please enter your current password to confirm.",
"accountActivationAndResendValidationEmailTip": "Le lien d'activation du compte a été envoyé à votre adresse e-mail : {email}, Si vous ne recevez pas le mail, veuillez remplir à nouveau le mot de passe et cliquer sur le bouton ci-dessous pour renvoyer l'e-mail de validation.",
"resendValidationEmailTip": "Si vous ne recevez pas le mail, veuillez remplir à nouveau le mot de passe et cliquer sur le bouton ci-dessous pour renvoyer l'e-mail de validation à : {email}"
"resendValidationEmailTip": "Si vous ne recevez pas le mail, veuillez remplir à nouveau le mot de passe et cliquer sur le bouton ci-dessous pour renvoyer l'e-mail de validation à : {email}",
"oauth2bindTip": "You're signing in to the {userName} user using {providerName}. Please enter your ezBookkeeping password to verify."
}
},
"dataExport": {
@@ -1086,6 +1088,7 @@
"user avatar file extension invalid": "L'extension du fichier d'avatar utilisateur est invalide",
"exceed the maximum size of user avatar file": "L'avatar utilisateur téléchargé dépasse la taille de fichier maximale autorisée",
"not permitted to perform this action": "Vous n'êtes pas autorisé à effectuer cette action",
"cannot login by password": "You cannot login by password",
"unauthorized access": "Accès non autorisé",
"current token is invalid": "Le token actuel est invalide",
"current token is expired": "Le token actuel a expiré",
@@ -1238,6 +1241,17 @@
"image for AI recognition is empty": "Le fichier d'image pour la reconnaissance IA est vide",
"exceed the maximum size of image file for AI recognition": "L'image téléchargée pour la reconnaissance IA dépasse la taille de fichier maximale autorisée",
"no transaction information detected": "Aucune information de transaction détectée",
"user external auth is not found": "User external authentication is not found",
"oauth 2.0 not enabled": "OAuth 2.0 is not enabled",
"oauth 2.0 auto registration not enabled": "OAuth 2.0 auto registration is not enabled",
"invalid oauth 2.0 login request": "Invalid OAuth 2.0 login request",
"invalid oauth 2.0 callback": "Invalid OAuth 2.0 callback",
"missing state in oauth 2.0 callback": "Missing state parameter in OAuth 2.0 callback",
"missing code in oauth 2.0 callback": "Missing code parameter in OAuth 2.0 callback",
"invalid state in oauth 2.0 callback": "Invalid state parameter in OAuth 2.0 callback",
"cannot retrieve oauth 2.0 token": "Cannot retrieve OAuth 2.0 token",
"invalid oauth 2.0 token": "Invalid OAuth 2.0 token",
"cannot retrieve user info from oauth 2.0 provider": "Cannot retrieve user info from OAuth 2.0 provider",
"query items cannot be blank": "Il n'y a pas d'éléments de requête",
"query items too much": "Il y a trop d'éléments de requête",
"query items have invalid item": "Il y a un élément invalide dans les éléments de requête",
@@ -1391,6 +1405,7 @@
"Operation": "Opération",
"Open": "Ouvrir",
"Close": "Fermer",
"or": "or",
"Submit": "Soumettre",
"Add": "Ajouter",
"Import": "Importer",
@@ -1572,7 +1587,10 @@
"This month or later": "Ce mois ou plus tard",
"This year or later": "Cette année ou plus tard",
"Log In": "Se connecter",
"Log in with OAuth 2.0": "Log in with OAuth 2.0",
"Log in with Connect ID": "Log in with Connect ID",
"Click here to log in": "Cliquez ici pour vous connecter",
"Logging in...": "Logging in...",
"Back to login page": "Retour à la page de connexion",
"Back to home page": "Retour à la page d'accueil",
"Don't have an account?": "Vous n'avez pas de compte ?",
+50 -6
View File
@@ -160,6 +160,7 @@ import { UTC_TIMEZONE, ALL_TIMEZONES } from '@/consts/timezone.ts';
import { ALL_CURRENCIES } from '@/consts/currency.ts';
import { DEFAULT_EXPENSE_CATEGORIES, DEFAULT_INCOME_CATEGORIES, DEFAULT_TRANSFER_CATEGORIES } from '@/consts/category.ts';
import { KnownErrorCode, SPECIFIED_API_NOT_FOUND_ERRORS, PARAMETERIZED_ERRORS } from '@/consts/api.ts';
import { OAUTH2_PROVIDER_DISPLAY_NAME } from '@/consts/oauth2.ts';
import { DEFAULT_DOCUMENT_LANGUAGE_FOR_IMPORT_FILE, SUPPORTED_DOCUMENT_LANGUAGES_FOR_IMPORT_FILE, SUPPORTED_IMPORT_FILE_CATEGORY_AND_TYPES } from '@/consts/file.ts';
import {
@@ -870,18 +871,18 @@ export function useI18n() {
return textArray.join(separator);
}
function getServerTipContent(tipConfig: Record<string, string>): string {
if (!tipConfig) {
function getServerMultiLanguageConfigContent(multiLanguageConfig: Record<string, string>): string {
if (!multiLanguageConfig) {
return '';
}
const currentLanguage = getCurrentLanguageTag();
if (isString(tipConfig[currentLanguage])) {
return tipConfig[currentLanguage];
if (isString(multiLanguageConfig[currentLanguage])) {
return multiLanguageConfig[currentLanguage];
}
return tipConfig['default'] || '';
return multiLanguageConfig['default'] || '';
}
function getCurrentLanguageTag(): string {
@@ -2139,6 +2140,46 @@ export function useI18n() {
return ret;
}
function getLocalizedOAuth2ProviderName(oauth2Provider: string, oidcDisplayNames: Record<string, string>): string {
if (oauth2Provider === 'oidc') {
const providerDisplayName = getServerMultiLanguageConfigContent(oidcDisplayNames);
if (providerDisplayName) {
return providerDisplayName;
} else {
return 'Connect ID';
}
} else {
const providerDisplayName = OAUTH2_PROVIDER_DISPLAY_NAME[oauth2Provider];
if (providerDisplayName) {
return providerDisplayName;
} else {
return 'OAuth 2.0';
}
}
}
function getLocalizedOAuth2LoginText(oauth2Provider: string, oidcDisplayNames: Record<string, string>): string {
if (oauth2Provider === 'oidc') {
const providerDisplayName = getServerMultiLanguageConfigContent(oidcDisplayNames);
if (providerDisplayName) {
return t('format.misc.loginWithCustomProvider', { name: providerDisplayName });
} else {
return t('Log in with Connect ID');
}
} else {
const providerDisplayName = OAUTH2_PROVIDER_DISPLAY_NAME[oauth2Provider];
if (providerDisplayName) {
return t('format.misc.loginWithCustomProvider', { name: providerDisplayName });
} else {
return t('Log in with OAuth 2.0');
}
}
}
function setLanguage(languageKey: string | null, force?: boolean): LocaleDefaultSettings | null {
if (!languageKey) {
languageKey = getDefaultLanguage();
@@ -2266,7 +2307,7 @@ export function useI18n() {
ti: translateIf,
te: translateError,
joinMultiText,
getServerTipContent,
getServerMultiLanguageConfigContent,
// get current language info
getCurrentLanguageTag,
getCurrentLanguageInfo,
@@ -2411,6 +2452,9 @@ export function useI18n() {
getAdaptiveAmountRate,
getAmountPrependAndAppendText,
getCategorizedAccountsWithDisplayBalance,
// other format functions
getLocalizedOAuth2ProviderName,
getLocalizedOAuth2LoginText,
// localization setting functions
setLanguage,
setTimeZone,
+19 -1
View File
@@ -108,6 +108,7 @@
},
"misc": {
"multiTextJoinSeparator": ", ",
"loginWithCustomProvider": "Log in with {name}",
"hoursBehindDefaultTimezone": "Indietro di {hours} ore rispetto al fuso orario standard",
"hoursAheadOfDefaultTimezone": "Avanti di {hours} ore rispetto al fuso orario standard",
"hoursMinutesBehindDefaultTimezone": "Indietro di {hours} ore e {minutes} minuti rispetto al fuso orario standard",
@@ -129,7 +130,8 @@
"moveTransactionsInAccountTip": "You CANNOT undo this action. This will move all transactions from {fromAccount} to {toAccount}.",
"clearTransactionsInAccountTip": "You CANNOT undo this action. This will clear your transactions data in {account}. Please enter your current password to confirm.",
"accountActivationAndResendValidationEmailTip": "Abbiamo inviato un link per l'attivazione del tuo account all'indirizzo {email}. Se non hai ricevuto la mail, inserisci nuovamente la password e premi il bottone per ritentare l'invio.",
"resendValidationEmailTip": "Se non hai ricevuto la mail, inserisci nuovamente la password e premi il bottone per ritentare l'invio all'indirizzo: {email}"
"resendValidationEmailTip": "Se non hai ricevuto la mail, inserisci nuovamente la password e premi il bottone per ritentare l'invio all'indirizzo: {email}",
"oauth2bindTip": "You're signing in to the {userName} user using {providerName}. Please enter your ezBookkeeping password to verify."
}
},
"dataExport": {
@@ -1086,6 +1088,7 @@
"user avatar file extension invalid": "Estensione del file avatar utente non valida",
"exceed the maximum size of user avatar file": "L'avatar utente caricato supera la dimensione massima consentita del file",
"not permitted to perform this action": "Non sei autorizzato a eseguire questa azione",
"cannot login by password": "You cannot login by password",
"unauthorized access": "Accesso non autorizzato",
"current token is invalid": "Token corrente non valido",
"current token is expired": "Token corrente scaduto",
@@ -1238,6 +1241,17 @@
"image for AI recognition is empty": "Image for AI recognition file is empty",
"exceed the maximum size of image file for AI recognition": "The uploaded image for AI recognition exceeds the maximum allowed file size",
"no transaction information detected": "No transaction information detected",
"user external auth is not found": "User external authentication is not found",
"oauth 2.0 not enabled": "OAuth 2.0 is not enabled",
"oauth 2.0 auto registration not enabled": "OAuth 2.0 auto registration is not enabled",
"invalid oauth 2.0 login request": "Invalid OAuth 2.0 login request",
"invalid oauth 2.0 callback": "Invalid OAuth 2.0 callback",
"missing state in oauth 2.0 callback": "Missing state parameter in OAuth 2.0 callback",
"missing code in oauth 2.0 callback": "Missing code parameter in OAuth 2.0 callback",
"invalid state in oauth 2.0 callback": "Invalid state parameter in OAuth 2.0 callback",
"cannot retrieve oauth 2.0 token": "Cannot retrieve OAuth 2.0 token",
"invalid oauth 2.0 token": "Invalid OAuth 2.0 token",
"cannot retrieve user info from oauth 2.0 provider": "Cannot retrieve user info from OAuth 2.0 provider",
"query items cannot be blank": "Non ci sono elementi di query",
"query items too much": "Ci sono troppi elementi di query",
"query items have invalid item": "C'è un elemento non valido negli elementi di query",
@@ -1391,6 +1405,7 @@
"Operation": "Operazione",
"Open": "Open",
"Close": "Chiudi",
"or": "or",
"Submit": "Invia",
"Add": "Aggiungi",
"Import": "Importa",
@@ -1572,7 +1587,10 @@
"This month or later": "Questo mese o successivo",
"This year or later": "Quest'anno o successivo",
"Log In": "Accedi",
"Log in with OAuth 2.0": "Log in with OAuth 2.0",
"Log in with Connect ID": "Log in with Connect ID",
"Click here to log in": "Clicca qui per accedere",
"Logging in...": "Logging in...",
"Back to login page": "Torna alla pagina di accesso",
"Back to home page": "Torna alla home page",
"Don't have an account?": "Non hai un account?",
+19 -1
View File
@@ -108,6 +108,7 @@
},
"misc": {
"multiTextJoinSeparator": "、",
"loginWithCustomProvider": "Log in with {name}",
"hoursBehindDefaultTimezone": "デフォルトのタイムゾーンより{hours}時間遅れています",
"hoursAheadOfDefaultTimezone": "デフォルトのタイムゾーンから{hours}時間進んでいます",
"hoursMinutesBehindDefaultTimezone": "デフォルトのタイムゾーンより{hours}時間{minutes}分遅れています",
@@ -129,7 +130,8 @@
"moveTransactionsInAccountTip": "You CANNOT undo this action. This will move all transactions from {fromAccount} to {toAccount}.",
"clearTransactionsInAccountTip": "You CANNOT undo this action. This will clear your transactions data in {account}. Please enter your current password to confirm.",
"accountActivationAndResendValidationEmailTip": "アカウントの有効化リンクがメールアドレスに送信されました:{email}、メールが届かない場合はパスワードをもう一度入力して下のボタンをクリックして認証メールを再送信してください。",
"resendValidationEmailTip": "メールが届かない場合は、パスワードをもう一度入力の上、以下のボタンをクリックして検証メールを再送信してください: {email}"
"resendValidationEmailTip": "メールが届かない場合は、パスワードをもう一度入力の上、以下のボタンをクリックして検証メールを再送信してください: {email}",
"oauth2bindTip": "You're signing in to the {userName} user using {providerName}. Please enter your ezBookkeeping password to verify."
}
},
"dataExport": {
@@ -1086,6 +1088,7 @@
"user avatar file extension invalid": "ユーザーアバターファイルの拡張子が無効です",
"exceed the maximum size of user avatar file": "アップロードされたユーザーアバターは最大ファイルサイズを超えています",
"not permitted to perform this action": "このアクションを実行は許可されていません",
"cannot login by password": "You cannot login by password",
"unauthorized access": "不正アクセス",
"current token is invalid": "現在のトークンは無効です",
"current token is expired": "現在のトークンの有効期限が切れています",
@@ -1238,6 +1241,17 @@
"image for AI recognition is empty": "Image for AI recognition file is empty",
"exceed the maximum size of image file for AI recognition": "The uploaded image for AI recognition exceeds the maximum allowed file size",
"no transaction information detected": "No transaction information detected",
"user external auth is not found": "User external authentication is not found",
"oauth 2.0 not enabled": "OAuth 2.0 is not enabled",
"oauth 2.0 auto registration not enabled": "OAuth 2.0 auto registration is not enabled",
"invalid oauth 2.0 login request": "Invalid OAuth 2.0 login request",
"invalid oauth 2.0 callback": "Invalid OAuth 2.0 callback",
"missing state in oauth 2.0 callback": "Missing state parameter in OAuth 2.0 callback",
"missing code in oauth 2.0 callback": "Missing code parameter in OAuth 2.0 callback",
"invalid state in oauth 2.0 callback": "Invalid state parameter in OAuth 2.0 callback",
"cannot retrieve oauth 2.0 token": "Cannot retrieve OAuth 2.0 token",
"invalid oauth 2.0 token": "Invalid OAuth 2.0 token",
"cannot retrieve user info from oauth 2.0 provider": "Cannot retrieve user info from OAuth 2.0 provider",
"query items cannot be blank": "クエリ項目がありません",
"query items too much": "クエリ項目が多すぎます",
"query items have invalid item": "クエリ項目に無効な項目があります",
@@ -1391,6 +1405,7 @@
"Operation": "操作",
"Open": "Open",
"Close": "閉じる",
"or": "or",
"Submit": "送信",
"Add": "追加",
"Import": "インポート",
@@ -1572,7 +1587,10 @@
"This month or later": "今月以降",
"This year or later": "今年以降",
"Log In": "ログイン",
"Log in with OAuth 2.0": "Log in with OAuth 2.0",
"Log in with Connect ID": "Log in with Connect ID",
"Click here to log in": "ここをクリックしてログインしてください",
"Logging in...": "Logging in...",
"Back to login page": "ログインページに戻る",
"Back to home page": "Back to home page",
"Don't have an account?": "Don't have an account?",
+19 -1
View File
@@ -108,6 +108,7 @@
},
"misc": {
"multiTextJoinSeparator": ", ",
"loginWithCustomProvider": "Log in with {name}",
"hoursBehindDefaultTimezone": "기본 시간대보다 {hours}시간 느립니다",
"hoursAheadOfDefaultTimezone": "기본 시간대보다 {hours}시간 빠릅니다",
"hoursMinutesBehindDefaultTimezone": "기본 시간대보다 {hours}시간 {minutes}분 느립니다",
@@ -129,7 +130,8 @@
"moveTransactionsInAccountTip": "You CANNOT undo this action. This will move all transactions from {fromAccount} to {toAccount}.",
"clearTransactionsInAccountTip": "이 작업은 되돌릴 수 없습니다. {account}의 거래 데이터를 지웁니다. 계속하시려면 현재 비밀번호를 입력하세요.",
"accountActivationAndResendValidationEmailTip": "계정 활성화 링크가 귀하의 이메일 주소({email})로 전송되었습니다. 메일을 받지 못하신 경우, 비밀번호를 다시 입력하고 아래 버튼을 클릭하여 확인 메일을 재전송하십시오.",
"resendValidationEmailTip": "메일을 받지 못하신 경우, 비밀번호를 다시 입력하고 아래 버튼을 클릭하여 확인 메일을 {email}로 재전송하십시오."
"resendValidationEmailTip": "메일을 받지 못하신 경우, 비밀번호를 다시 입력하고 아래 버튼을 클릭하여 확인 메일을 {email}로 재전송하십시오.",
"oauth2bindTip": "You're signing in to the {userName} user using {providerName}. Please enter your ezBookkeeping password to verify."
}
},
"dataExport": {
@@ -1086,6 +1088,7 @@
"user avatar file extension invalid": "사용자 아바타 파일 확장자가 유효하지 않습니다",
"exceed the maximum size of user avatar file": "업로드된 사용자 아바타가 허용된 최대 파일 크기를 초과합니다",
"not permitted to perform this action": "이 작업을 수행할 수 있는 권한이 없습니다",
"cannot login by password": "You cannot login by password",
"unauthorized access": "권한이 없는 접근입니다",
"current token is invalid": "현재 토큰이 유효하지 않습니다",
"current token is expired": "현재 토큰이 만료되었습니다",
@@ -1238,6 +1241,17 @@
"image for AI recognition is empty": "AI 인식을 위한 이미지 파일이 비어 있습니다.",
"exceed the maximum size of image file for AI recognition": "AI 인식을 위한 업로드된 이미지가 허용된 최대 파일 크기를 초과합니다.",
"no transaction information detected": "거래 정보가 감지되지 않았습니다.",
"user external auth is not found": "User external authentication is not found",
"oauth 2.0 not enabled": "OAuth 2.0 is not enabled",
"oauth 2.0 auto registration not enabled": "OAuth 2.0 auto registration is not enabled",
"invalid oauth 2.0 login request": "Invalid OAuth 2.0 login request",
"invalid oauth 2.0 callback": "Invalid OAuth 2.0 callback",
"missing state in oauth 2.0 callback": "Missing state parameter in OAuth 2.0 callback",
"missing code in oauth 2.0 callback": "Missing code parameter in OAuth 2.0 callback",
"invalid state in oauth 2.0 callback": "Invalid state parameter in OAuth 2.0 callback",
"cannot retrieve oauth 2.0 token": "Cannot retrieve OAuth 2.0 token",
"invalid oauth 2.0 token": "Invalid OAuth 2.0 token",
"cannot retrieve user info from oauth 2.0 provider": "Cannot retrieve user info from OAuth 2.0 provider",
"query items cannot be blank": "쿼리 항목이 비어 있을 수 없습니다.",
"query items too much": "쿼리 항목이 너무 많습니다.",
"query items have invalid item": "쿼리 항목에 유효하지 않은 항목이 있습니다.",
@@ -1391,6 +1405,7 @@
"Operation": "작업",
"Open": "열기",
"Close": "닫기",
"or": "or",
"Submit": "제출",
"Add": "추가",
"Import": "가져오기",
@@ -1572,7 +1587,10 @@
"This month or later": "이번 달 이후",
"This year or later": "올해 이후",
"Log In": "로그인",
"Log in with OAuth 2.0": "Log in with OAuth 2.0",
"Log in with Connect ID": "Log in with Connect ID",
"Click here to log in": "여기를 클릭하여 로그인",
"Logging in...": "Logging in...",
"Back to login page": "로그인 페이지로 돌아가기",
"Back to home page": "홈 페이지로 돌아가기",
"Don't have an account?": "계정이 없으신가요?",
+19 -1
View File
@@ -108,6 +108,7 @@
},
"misc": {
"multiTextJoinSeparator": ", ",
"loginWithCustomProvider": "Log in with {name}",
"hoursBehindDefaultTimezone": "{hours} uur achter standaardtijdzone",
"hoursAheadOfDefaultTimezone": "{hours} uur voor op standaardtijdzone",
"hoursMinutesBehindDefaultTimezone": "{hours} uur en {minutes} minuten achter standaardtijdzone",
@@ -129,7 +130,8 @@
"moveTransactionsInAccountTip": "You CANNOT undo this action. This will move all transactions from {fromAccount} to {toAccount}.",
"clearTransactionsInAccountTip": "You CANNOT undo this action. This will clear your transactions data in {account}. Please enter your current password to confirm.",
"accountActivationAndResendValidationEmailTip": "Een activatielink is verzonden naar je e-mailadres: {email}. Als je de e-mail niet ontvangt, vul dan je wachtwoord opnieuw in en klik op de knop hieronder om de validatiemail opnieuw te verzenden.",
"resendValidationEmailTip": "Als je de e-mail niet ontvangt, vul dan je wachtwoord opnieuw in en klik op de knop hieronder om de validatiemail opnieuw te verzenden naar: {email}"
"resendValidationEmailTip": "Als je de e-mail niet ontvangt, vul dan je wachtwoord opnieuw in en klik op de knop hieronder om de validatiemail opnieuw te verzenden naar: {email}",
"oauth2bindTip": "You're signing in to the {userName} user using {providerName}. Please enter your ezBookkeeping password to verify."
}
},
"dataExport": {
@@ -1086,6 +1088,7 @@
"user avatar file extension invalid": "Bestandsextensie van gebruikersavatar is ongeldig",
"exceed the maximum size of user avatar file": "Geüploade avatar overschrijdt de maximaal toegestane bestandsgrootte",
"not permitted to perform this action": "Je hebt geen toestemming om deze actie uit te voeren",
"cannot login by password": "You cannot login by password",
"unauthorized access": "Ongeautoriseerde toegang",
"current token is invalid": "Huidige token is ongeldig",
"current token is expired": "Huidige token is verlopen",
@@ -1238,6 +1241,17 @@
"image for AI recognition is empty": "Image for AI recognition file is empty",
"exceed the maximum size of image file for AI recognition": "The uploaded image for AI recognition exceeds the maximum allowed file size",
"no transaction information detected": "No transaction information detected",
"user external auth is not found": "User external authentication is not found",
"oauth 2.0 not enabled": "OAuth 2.0 is not enabled",
"oauth 2.0 auto registration not enabled": "OAuth 2.0 auto registration is not enabled",
"invalid oauth 2.0 login request": "Invalid OAuth 2.0 login request",
"invalid oauth 2.0 callback": "Invalid OAuth 2.0 callback",
"missing state in oauth 2.0 callback": "Missing state parameter in OAuth 2.0 callback",
"missing code in oauth 2.0 callback": "Missing code parameter in OAuth 2.0 callback",
"invalid state in oauth 2.0 callback": "Invalid state parameter in OAuth 2.0 callback",
"cannot retrieve oauth 2.0 token": "Cannot retrieve OAuth 2.0 token",
"invalid oauth 2.0 token": "Invalid OAuth 2.0 token",
"cannot retrieve user info from oauth 2.0 provider": "Cannot retrieve user info from OAuth 2.0 provider",
"query items cannot be blank": "Geen zoekitems opgegeven",
"query items too much": "Te veel zoekitems",
"query items have invalid item": "Ongeldig item in zoekitems",
@@ -1391,6 +1405,7 @@
"Operation": "Bewerking",
"Open": "Openen",
"Close": "Sluiten",
"or": "or",
"Submit": "Verzenden",
"Add": "Toevoegen",
"Import": "Importeren",
@@ -1572,7 +1587,10 @@
"This month or later": "Deze maand of later",
"This year or later": "Dit jaar of later",
"Log In": "Inloggen",
"Log in with OAuth 2.0": "Log in with OAuth 2.0",
"Log in with Connect ID": "Log in with Connect ID",
"Click here to log in": "Klik hier om in te loggen",
"Logging in...": "Logging in...",
"Back to login page": "Terug naar inlogpagina",
"Back to home page": "Terug naar startpagina",
"Don't have an account?": "Nog geen account?",
+19 -1
View File
@@ -108,6 +108,7 @@
},
"misc": {
"multiTextJoinSeparator": ", ",
"loginWithCustomProvider": "Log in with {name}",
"hoursBehindDefaultTimezone": "{hours} hora(s) atrás do fuso horário padrão",
"hoursAheadOfDefaultTimezone": "{hours} hora(s) à frente do fuso horário padrão",
"hoursMinutesBehindDefaultTimezone": "{hours} hora(s) e {minutes} minutos atrás do fuso horário padrão",
@@ -129,7 +130,8 @@
"moveTransactionsInAccountTip": "You CANNOT undo this action. This will move all transactions from {fromAccount} to {toAccount}.",
"clearTransactionsInAccountTip": "You CANNOT undo this action. This will clear your transactions data in {account}. Please enter your current password to confirm.",
"accountActivationAndResendValidationEmailTip": "O link de ativação da conta foi enviado para seu endereço de e-mail: {email}. Se você não receber o e-mail, por favor preencha a senha novamente e clique no botão abaixo para reenviar o e-mail de validação.",
"resendValidationEmailTip": "Se você não receber o e-mail, por favor preencha a senha novamente e clique no botão abaixo para reenviar o e-mail de validação para: {email}"
"resendValidationEmailTip": "Se você não receber o e-mail, por favor preencha a senha novamente e clique no botão abaixo para reenviar o e-mail de validação para: {email}",
"oauth2bindTip": "You're signing in to the {userName} user using {providerName}. Please enter your ezBookkeeping password to verify."
}
},
"dataExport": {
@@ -1086,6 +1088,7 @@
"user avatar file extension invalid": "A extensão do arquivo de avatar do usuário é inválida",
"exceed the maximum size of user avatar file": "O avatar de usuário enviado excede o tamanho máximo permitido de arquivo",
"not permitted to perform this action": "Você não tem permissão para realizar esta ação",
"cannot login by password": "You cannot login by password",
"unauthorized access": "Acesso não autorizado",
"current token is invalid": "Token atual é inválido",
"current token is expired": "Token atual está expirado",
@@ -1238,6 +1241,17 @@
"image for AI recognition is empty": "Image for AI recognition file is empty",
"exceed the maximum size of image file for AI recognition": "The uploaded image for AI recognition exceeds the maximum allowed file size",
"no transaction information detected": "No transaction information detected",
"user external auth is not found": "User external authentication is not found",
"oauth 2.0 not enabled": "OAuth 2.0 is not enabled",
"oauth 2.0 auto registration not enabled": "OAuth 2.0 auto registration is not enabled",
"invalid oauth 2.0 login request": "Invalid OAuth 2.0 login request",
"invalid oauth 2.0 callback": "Invalid OAuth 2.0 callback",
"missing state in oauth 2.0 callback": "Missing state parameter in OAuth 2.0 callback",
"missing code in oauth 2.0 callback": "Missing code parameter in OAuth 2.0 callback",
"invalid state in oauth 2.0 callback": "Invalid state parameter in OAuth 2.0 callback",
"cannot retrieve oauth 2.0 token": "Cannot retrieve OAuth 2.0 token",
"invalid oauth 2.0 token": "Invalid OAuth 2.0 token",
"cannot retrieve user info from oauth 2.0 provider": "Cannot retrieve user info from OAuth 2.0 provider",
"query items cannot be blank": "Não há itens de consulta",
"query items too much": "Há muitos itens de consulta",
"query items have invalid item": "Há item inválido nos itens de consulta",
@@ -1391,6 +1405,7 @@
"Operation": "Operação",
"Open": "Open",
"Close": "Fechar",
"or": "or",
"Submit": "Enviar",
"Add": "Adicionar",
"Import": "Importar",
@@ -1572,7 +1587,10 @@
"This month or later": "Este mês ou depois",
"This year or later": "Este ano ou depois",
"Log In": "Fazer Login",
"Log in with OAuth 2.0": "Log in with OAuth 2.0",
"Log in with Connect ID": "Log in with Connect ID",
"Click here to log in": "Clique aqui para fazer login",
"Logging in...": "Logging in...",
"Back to login page": "Voltar para a página de login",
"Back to home page": "Voltar para a página inicial",
"Don't have an account?": "Não tem uma conta?",
+19 -1
View File
@@ -108,6 +108,7 @@
},
"misc": {
"multiTextJoinSeparator": ", ",
"loginWithCustomProvider": "Log in with {name}",
"hoursBehindDefaultTimezone": "{hours} час(ов) позади часового пояса по умолчанию",
"hoursAheadOfDefaultTimezone": "{hours} час(ов) впереди часового пояса по умолчанию",
"hoursMinutesBehindDefaultTimezone": "{hours} час(ов) и {minutes} минут позади часового пояса по умолчанию",
@@ -129,7 +130,8 @@
"moveTransactionsInAccountTip": "You CANNOT undo this action. This will move all transactions from {fromAccount} to {toAccount}.",
"clearTransactionsInAccountTip": "You CANNOT undo this action. This will clear your transactions data in {account}. Please enter your current password to confirm.",
"accountActivationAndResendValidationEmailTip": "Ссылка для активации учетной записи была отправлена на ваш электронный адрес: {email}. Если вы не получили письмо, заполните пароль снова и нажмите кнопку ниже, чтобы отправить письмо повторно.",
"resendValidationEmailTip": "Если вы не получили письмо, заполните пароль снова и нажмите кнопку ниже, чтобы отправить письмо повторно на: {email}"
"resendValidationEmailTip": "Если вы не получили письмо, заполните пароль снова и нажмите кнопку ниже, чтобы отправить письмо повторно на: {email}",
"oauth2bindTip": "You're signing in to the {userName} user using {providerName}. Please enter your ezBookkeeping password to verify."
}
},
"dataExport": {
@@ -1086,6 +1088,7 @@
"user avatar file extension invalid": "Недопустимое расширение файла аватара пользователя",
"exceed the maximum size of user avatar file": "Загруженный аватар пользователя превышает максимально допустимый размер файла",
"not permitted to perform this action": "Вам не разрешено выполнять это действие",
"cannot login by password": "You cannot login by password",
"unauthorized access": "Несанкционированный доступ",
"current token is invalid": "Текущий токен недействителен",
"current token is expired": "Текущий токен истек",
@@ -1238,6 +1241,17 @@
"image for AI recognition is empty": "Image for AI recognition file is empty",
"exceed the maximum size of image file for AI recognition": "The uploaded image for AI recognition exceeds the maximum allowed file size",
"no transaction information detected": "No transaction information detected",
"user external auth is not found": "User external authentication is not found",
"oauth 2.0 not enabled": "OAuth 2.0 is not enabled",
"oauth 2.0 auto registration not enabled": "OAuth 2.0 auto registration is not enabled",
"invalid oauth 2.0 login request": "Invalid OAuth 2.0 login request",
"invalid oauth 2.0 callback": "Invalid OAuth 2.0 callback",
"missing state in oauth 2.0 callback": "Missing state parameter in OAuth 2.0 callback",
"missing code in oauth 2.0 callback": "Missing code parameter in OAuth 2.0 callback",
"invalid state in oauth 2.0 callback": "Invalid state parameter in OAuth 2.0 callback",
"cannot retrieve oauth 2.0 token": "Cannot retrieve OAuth 2.0 token",
"invalid oauth 2.0 token": "Invalid OAuth 2.0 token",
"cannot retrieve user info from oauth 2.0 provider": "Cannot retrieve user info from OAuth 2.0 provider",
"query items cannot be blank": "Нет элементов запроса",
"query items too much": "Слишком много элементов запроса",
"query items have invalid item": "В элементах запроса присутствует недопустимый элемент",
@@ -1391,6 +1405,7 @@
"Operation": "Операция",
"Open": "Open",
"Close": "Закрыть",
"or": "or",
"Submit": "Отправить",
"Add": "Добавить",
"Import": "Импорт",
@@ -1572,7 +1587,10 @@
"This month or later": "В этом месяце или позже",
"This year or later": "В этом году или позже",
"Log In": "Войти",
"Log in with OAuth 2.0": "Log in with OAuth 2.0",
"Log in with Connect ID": "Log in with Connect ID",
"Click here to log in": "Нажмите здесь, чтобы войти",
"Logging in...": "Logging in...",
"Back to login page": "Вернуться на страницу входа",
"Back to home page": "Вернуться на главную страницу",
"Don't have an account?": "Нет учетной записи?",
+19 -1
View File
@@ -108,6 +108,7 @@
},
"misc": {
"multiTextJoinSeparator": ", ",
"loginWithCustomProvider": "Log in with {name}",
"hoursBehindDefaultTimezone": "ช้ากว่าเขตเวลาเริ่มต้น {hours} ชั่วโมง",
"hoursAheadOfDefaultTimezone": "เร็วกว่าเขตเวลาเริ่มต้น {hours} ชั่วโมง",
"hoursMinutesBehindDefaultTimezone": "ช้ากว่าเขตเวลาเริ่มต้น {hours} ชั่วโมง {minutes} นาที",
@@ -129,7 +130,8 @@
"moveTransactionsInAccountTip": "You CANNOT undo this action. This will move all transactions from {fromAccount} to {toAccount}.",
"clearTransactionsInAccountTip": "คุณไม่สามารถยกเลิกการกระทำนี้ได้ การกระทำนี้จะลบข้อมูลธุรกรรมทั้งหมดใน {account} โปรดป้อนรหัสผ่านปัจจุบันเพื่อยืนยัน",
"accountActivationAndResendValidationEmailTip": "ลิงก์สำหรับเปิดใช้งานบัญชีได้ถูกส่งไปยังอีเมลของคุณแล้ว: {email} หากคุณไม่ได้รับอีเมล โปรดกรอกรหัสผ่านอีกครั้งแล้วกดปุ่มด้านล่างเพื่อส่งอีเมลยืนยันอีกครั้ง",
"resendValidationEmailTip": "หากคุณไม่ได้รับอีเมล โปรดกรอกรหัสผ่านอีกครั้งแล้วกดปุ่มด้านล่างเพื่อส่งอีเมลยืนยันไปยัง: {email}"
"resendValidationEmailTip": "หากคุณไม่ได้รับอีเมล โปรดกรอกรหัสผ่านอีกครั้งแล้วกดปุ่มด้านล่างเพื่อส่งอีเมลยืนยันไปยัง: {email}",
"oauth2bindTip": "You're signing in to the {userName} user using {providerName}. Please enter your ezBookkeeping password to verify."
}
},
"dataExport": {
@@ -1086,6 +1088,7 @@
"user avatar file extension invalid": "นามสกุลไฟล์รูปประจำตัวผู้ใช้ไม่ถูกต้อง",
"exceed the maximum size of user avatar file": "ขนาดไฟล์รูปประจำตัวผู้ใช้เกินขนาดสูงสุดที่อนุญาต",
"not permitted to perform this action": "คุณไม่ได้รับอนุญาตให้ดำเนินการนี้",
"cannot login by password": "You cannot login by password",
"unauthorized access": "การเข้าถึงไม่ได้รับอนุญาต",
"current token is invalid": "โทเค็นปัจจุบันไม่ถูกต้อง",
"current token is expired": "โทเค็นปัจจุบันหมดอายุ",
@@ -1238,6 +1241,17 @@
"image for AI recognition is empty": "ไฟล์รูปภาพสำหรับการจดจำด้วย AI ว่างเปล่า",
"exceed the maximum size of image file for AI recognition": "ไฟล์รูปภาพสำหรับการจดจำด้วย AI เกินขนาดสูงสุดที่อนุญาต",
"no transaction information detected": "ไม่พบข้อมูลธุรกรรม",
"user external auth is not found": "User external authentication is not found",
"oauth 2.0 not enabled": "OAuth 2.0 is not enabled",
"oauth 2.0 auto registration not enabled": "OAuth 2.0 auto registration is not enabled",
"invalid oauth 2.0 login request": "Invalid OAuth 2.0 login request",
"invalid oauth 2.0 callback": "Invalid OAuth 2.0 callback",
"missing state in oauth 2.0 callback": "Missing state parameter in OAuth 2.0 callback",
"missing code in oauth 2.0 callback": "Missing code parameter in OAuth 2.0 callback",
"invalid state in oauth 2.0 callback": "Invalid state parameter in OAuth 2.0 callback",
"cannot retrieve oauth 2.0 token": "Cannot retrieve OAuth 2.0 token",
"invalid oauth 2.0 token": "Invalid OAuth 2.0 token",
"cannot retrieve user info from oauth 2.0 provider": "Cannot retrieve user info from OAuth 2.0 provider",
"query items cannot be blank": "ไม่มีรายการสำหรับค้นหา",
"query items too much": "รายการค้นหามากเกินไป",
"query items have invalid item": "มีรายการไม่ถูกต้องในรายการค้นหา",
@@ -1391,6 +1405,7 @@
"Operation": "การดำเนินการ",
"Open": "เปิด",
"Close": "ปิด",
"or": "or",
"Submit": "ส่ง",
"Add": "เพิ่ม",
"Import": "นำเข้า",
@@ -1572,7 +1587,10 @@
"This month or later": "เดือนนี้หรือหลังจากนี้",
"This year or later": "ปีนี้หรือหลังจากนี้",
"Log In": "เข้าสู่ระบบ",
"Log in with OAuth 2.0": "Log in with OAuth 2.0",
"Log in with Connect ID": "Log in with Connect ID",
"Click here to log in": "คลิกที่นี่เพื่อเข้าสู่ระบบ",
"Logging in...": "Logging in...",
"Back to login page": "กลับไปยังหน้าล็อกอิน",
"Back to home page": "กลับไปยังหน้าหลัก",
"Don't have an account?": "ยังไม่มีบัญชี?",
+19 -1
View File
@@ -108,6 +108,7 @@
},
"misc": {
"multiTextJoinSeparator": ", ",
"loginWithCustomProvider": "Log in with {name}",
"hoursBehindDefaultTimezone": "{hours} год позаду часового поясу за замовчуванням",
"hoursAheadOfDefaultTimezone": "{hours} год попереду часового поясу за замовчуванням",
"hoursMinutesBehindDefaultTimezone": "{hours} год і {minutes} хв позаду часового поясу за замовчуванням",
@@ -129,7 +130,8 @@
"moveTransactionsInAccountTip": "You CANNOT undo this action. This will move all transactions from {fromAccount} to {toAccount}.",
"clearTransactionsInAccountTip": "You CANNOT undo this action. This will clear your transactions data in {account}. Please enter your current password to confirm.",
"accountActivationAndResendValidationEmailTip": "Посилання для активації облікового запису було надіслано на вашу електронну адресу: {email}. Якщо ви не отримали лист, введіть пароль ще раз і натисніть кнопку нижче, щоб надіслати лист повторно.",
"resendValidationEmailTip": "Якщо ви не отримали лист, введіть пароль ще раз і натисніть кнопку нижче, щоб надіслати лист повторно на адресу: {email}"
"resendValidationEmailTip": "Якщо ви не отримали лист, введіть пароль ще раз і натисніть кнопку нижче, щоб надіслати лист повторно на адресу: {email}",
"oauth2bindTip": "You're signing in to the {userName} user using {providerName}. Please enter your ezBookkeeping password to verify."
}
},
"dataExport": {
@@ -1086,6 +1088,7 @@
"user avatar file extension invalid": "Недопустиме розширення файлу аватара користувача",
"exceed the maximum size of user avatar file": "Завантажений аватар перевищує максимально допустимий розмір",
"not permitted to perform this action": "Вам не дозволено виконувати цю дію",
"cannot login by password": "You cannot login by password",
"unauthorized access": "Несанкціонований доступ",
"current token is invalid": "Поточний токен недійсний",
"current token is expired": "Поточний токен прострочений",
@@ -1238,6 +1241,17 @@
"image for AI recognition is empty": "Image for AI recognition file is empty",
"exceed the maximum size of image file for AI recognition": "The uploaded image for AI recognition exceeds the maximum allowed file size",
"no transaction information detected": "No transaction information detected",
"user external auth is not found": "User external authentication is not found",
"oauth 2.0 not enabled": "OAuth 2.0 is not enabled",
"oauth 2.0 auto registration not enabled": "OAuth 2.0 auto registration is not enabled",
"invalid oauth 2.0 login request": "Invalid OAuth 2.0 login request",
"invalid oauth 2.0 callback": "Invalid OAuth 2.0 callback",
"missing state in oauth 2.0 callback": "Missing state parameter in OAuth 2.0 callback",
"missing code in oauth 2.0 callback": "Missing code parameter in OAuth 2.0 callback",
"invalid state in oauth 2.0 callback": "Invalid state parameter in OAuth 2.0 callback",
"cannot retrieve oauth 2.0 token": "Cannot retrieve OAuth 2.0 token",
"invalid oauth 2.0 token": "Invalid OAuth 2.0 token",
"cannot retrieve user info from oauth 2.0 provider": "Cannot retrieve user info from OAuth 2.0 provider",
"query items cannot be blank": "Елементи запиту не можуть бути порожніми",
"query items too much": "Занадто багато елементів запиту",
"query items have invalid item": "Запит містить недійсний елемент",
@@ -1391,6 +1405,7 @@
"Operation": "Дія",
"Open": "Open",
"Close": "Закрити",
"or": "or",
"Submit": "Підтвердити",
"Add": "Додати",
"Import": "Імпортувати",
@@ -1572,7 +1587,10 @@
"This month or later": "Цього місяця або пізніше",
"This year or later": "Цього року або пізніше",
"Log In": "Увійти",
"Log in with OAuth 2.0": "Log in with OAuth 2.0",
"Log in with Connect ID": "Log in with Connect ID",
"Click here to log in": "Натисніть тут, щоб увійти",
"Logging in...": "Logging in...",
"Back to login page": "Повернутися на сторінку входу",
"Back to home page": "Повернутися на головну сторінку",
"Don't have an account?": "Немає облікового запису?",
+19 -1
View File
@@ -108,6 +108,7 @@
},
"misc": {
"multiTextJoinSeparator": ", ",
"loginWithCustomProvider": "Log in with {name}",
"hoursBehindDefaultTimezone": "{hours} giờ sau múi giờ mặc định",
"hoursAheadOfDefaultTimezone": "{hours} giờ trước múi giờ mặc định",
"hoursMinutesBehindDefaultTimezone": "{hours} giờ và {minutes} phút sau múi giờ mặc định",
@@ -129,7 +130,8 @@
"moveTransactionsInAccountTip": "You CANNOT undo this action. This will move all transactions from {fromAccount} to {toAccount}.",
"clearTransactionsInAccountTip": "You CANNOT undo this action. This will clear your transactions data in {account}. Please enter your current password to confirm.",
"accountActivationAndResendValidationEmailTip": "Liên kết kích hoạt tài khoản đã được gửi tới email của bạn: {email}. Nếu bạn không nhận được email, vui lòng nhập lại mật khẩu và nhấp nút bên dưới để gửi lại email xác nhận.",
"resendValidationEmailTip": "Nếu bạn không nhận được email, vui lòng nhập lại mật khẩu và nhấp nút bên dưới để gửi lại email xác nhận tới: {email}"
"resendValidationEmailTip": "Nếu bạn không nhận được email, vui lòng nhập lại mật khẩu và nhấp nút bên dưới để gửi lại email xác nhận tới: {email}",
"oauth2bindTip": "You're signing in to the {userName} user using {providerName}. Please enter your ezBookkeeping password to verify."
}
},
"dataExport": {
@@ -1086,6 +1088,7 @@
"user avatar file extension invalid": "Đuôi tệp avatar người dùng không hợp lệ",
"exceed the maximum size of user avatar file": "Avatar người dùng đã tải lên vượt quá kích thước tệp tối đa cho phép",
"not permitted to perform this action": "Bạn không được phép thực hiện hành động này",
"cannot login by password": "You cannot login by password",
"unauthorized access": "Truy cập trái phép",
"current token is invalid": "Mã thông báo hiện tại không hợp lệ",
"current token is expired": "Mã thông báo hiện tại đã hết hạn",
@@ -1238,6 +1241,17 @@
"image for AI recognition is empty": "Image for AI recognition file is empty",
"exceed the maximum size of image file for AI recognition": "The uploaded image for AI recognition exceeds the maximum allowed file size",
"no transaction information detected": "No transaction information detected",
"user external auth is not found": "User external authentication is not found",
"oauth 2.0 not enabled": "OAuth 2.0 is not enabled",
"oauth 2.0 auto registration not enabled": "OAuth 2.0 auto registration is not enabled",
"invalid oauth 2.0 login request": "Invalid OAuth 2.0 login request",
"invalid oauth 2.0 callback": "Invalid OAuth 2.0 callback",
"missing state in oauth 2.0 callback": "Missing state parameter in OAuth 2.0 callback",
"missing code in oauth 2.0 callback": "Missing code parameter in OAuth 2.0 callback",
"invalid state in oauth 2.0 callback": "Invalid state parameter in OAuth 2.0 callback",
"cannot retrieve oauth 2.0 token": "Cannot retrieve OAuth 2.0 token",
"invalid oauth 2.0 token": "Invalid OAuth 2.0 token",
"cannot retrieve user info from oauth 2.0 provider": "Cannot retrieve user info from OAuth 2.0 provider",
"query items cannot be blank": "Không có mục truy vấn",
"query items too much": "Có quá nhiều mục truy vấn",
"query items have invalid item": "Có mục không hợp lệ trong các mục truy vấn",
@@ -1391,6 +1405,7 @@
"Operation": "Thao tác",
"Open": "Open",
"Close": "Đóng",
"or": "or",
"Submit": "Gửi",
"Add": "Thêm",
"Import": "Nhập",
@@ -1572,7 +1587,10 @@
"This month or later": "Tháng này trở đi",
"This year or later": "Năm nay trở đi",
"Log In": "Đăng nhập",
"Log in with OAuth 2.0": "Log in with OAuth 2.0",
"Log in with Connect ID": "Log in with Connect ID",
"Click here to log in": "Nhấp vào đây để đăng nhập",
"Logging in...": "Logging in...",
"Back to login page": "Quay lại trang đăng nhập",
"Back to home page": "Quay lại trang chủ",
"Don't have an account?": "Bạn chưa có tài khoản?",
+19 -1
View File
@@ -108,6 +108,7 @@
},
"misc": {
"multiTextJoinSeparator": "、",
"loginWithCustomProvider": "使用 {name} 登录",
"hoursBehindDefaultTimezone": "比默认时区晚{hours}小时",
"hoursAheadOfDefaultTimezone": "比默认时区早{hours}小时",
"hoursMinutesBehindDefaultTimezone": "比默认时区晚{hours}小时{minutes}分",
@@ -129,7 +130,8 @@
"moveTransactionsInAccountTip": "您不能撤销该操作。该操作将会把 {fromAccount} 账户中所有的交易数据移动到 {toAccount}。",
"clearTransactionsInAccountTip": "您不能撤销该操作。该操作将会清除您在 {account} 账户中的交易数据。请输入您当前的密码以确认。",
"accountActivationAndResendValidationEmailTip": "账号激活链接已经发送到您的邮箱地址:{email},如果您没有收到邮件,请再次输入密码并点击下方的按钮重新发送验证邮件。",
"resendValidationEmailTip": "如果您没有收到邮件,请再次输入密码并点击下方的按钮重新发送验证邮件到:{email}"
"resendValidationEmailTip": "如果您没有收到邮件,请再次输入密码并点击下方的按钮重新发送验证邮件到:{email}",
"oauth2bindTip": "您正在使用 {providerName} 登录 \"{userName}\" 用户,请输入你的 ezBookkeeping 的密码进行验证。"
}
},
"dataExport": {
@@ -1086,6 +1088,7 @@
"user avatar file extension invalid": "用户头像文件扩展名无效",
"exceed the maximum size of user avatar file": "上传的用户头像超出了允许的最大文件大小",
"not permitted to perform this action": "您不能执行该操作",
"cannot login by password": "您不能使用密码登录",
"unauthorized access": "未授权的登录",
"current token is invalid": "当前认证令牌无效",
"current token is expired": "当前认证令牌已过期",
@@ -1238,6 +1241,17 @@
"image for AI recognition is empty": "用于AI识别的图片为空",
"exceed the maximum size of image file for AI recognition": "用于AI识别的图片超出了允许的最大文件大小",
"no transaction information detected": "没有检测到交易信息",
"user external auth is not found": "用户外部认证信息不存在",
"oauth 2.0 not enabled": "OAuth 2.0 没有启用",
"oauth 2.0 auto registration not enabled": "OAuth 2.0 自动注册没有启用",
"invalid oauth 2.0 login request": "无效的 OAuth 2.0 登录请求",
"invalid oauth 2.0 callback": "无效的 OAuth 2.0 回调请求",
"missing state in oauth 2.0 callback": "OAuth 2.0 回调中缺少 state 参数",
"missing code in oauth 2.0 callback": "OAuth 2.0 回调中缺少 code 参数",
"invalid state in oauth 2.0 callback": "OAuth 2.0 回调中的 state 参数无效",
"cannot retrieve oauth 2.0 token": "无法获取 OAuth 2.0 令牌",
"invalid oauth 2.0 token": "无效的 OAuth 2.0 令牌",
"cannot retrieve user info from oauth 2.0 provider": "无法从 OAuth 2.0 提供者获取用户信息",
"query items cannot be blank": "请求项目不能为空",
"query items too much": "请求项目过多",
"query items have invalid item": "请求项目中有非法项目",
@@ -1391,6 +1405,7 @@
"Operation": "操作",
"Open": "打开",
"Close": "关闭",
"or": "或",
"Submit": "提交",
"Add": "添加",
"Import": "导入",
@@ -1572,7 +1587,10 @@
"This month or later": "本月或更晚",
"This year or later": "今年或更晚",
"Log In": "登录",
"Log in with OAuth 2.0": "使用 OAuth 2.0 登录",
"Log in with Connect ID": "使用 Connect ID 登录",
"Click here to log in": "点击这里登录",
"Logging in...": "正在登录...",
"Back to login page": "返回登录页",
"Back to home page": "返回首页",
"Don't have an account?": "还没有账号?",
+19 -1
View File
@@ -108,6 +108,7 @@
},
"misc": {
"multiTextJoinSeparator": "、",
"loginWithCustomProvider": "使用 {name} 登入",
"hoursBehindDefaultTimezone": "比預設時區晚{hours}小時",
"hoursAheadOfDefaultTimezone": "比預設時區早{hours}小時",
"hoursMinutesBehindDefaultTimezone": "比預設時區晚{hours}小時{minutes}分",
@@ -129,7 +130,8 @@
"moveTransactionsInAccountTip": "您不能還原此操作。此操作將會把 {fromAccount} 帳戶中的所有交易資料移動到 {toAccount}。",
"clearTransactionsInAccountTip": "您不能還原此操作。此操作將會清除您在 {account} 帳戶中的交易資料。請輸入您目前的密碼以確認。",
"accountActivationAndResendValidationEmailTip": "帳號啟用連結已經傳送到您的信箱地址:{email},如果您沒有收到郵件,請再次輸入密碼並點擊下方的按鈕重新發送驗證郵件。",
"resendValidationEmailTip": "如果您沒有收到郵件,請再次輸入密碼並點擊下方的按鈕重新發送驗證郵件到:{email}"
"resendValidationEmailTip": "如果您沒有收到郵件,請再次輸入密碼並點擊下方的按鈕重新發送驗證郵件到:{email}",
"oauth2bindTip": "您正在使用 {providerName} 登入 \"{userName}\" 使用者,請輸入您的 ezBookkeeping 的密碼以進行驗證。"
}
},
"dataExport": {
@@ -1086,6 +1088,7 @@
"user avatar file extension invalid": "使用者頭像檔案副檔名無效",
"exceed the maximum size of user avatar file": "上傳的使用者頭像超過允許的最大檔案大小",
"not permitted to perform this action": "您不能執行該操作",
"cannot login by password": "您不能使用密碼登入",
"unauthorized access": "未授權的登入",
"current token is invalid": "目前認證令牌無效",
"current token is expired": "目前認證令牌已過期",
@@ -1238,6 +1241,17 @@
"image for AI recognition is empty": "用於AI識別的圖片檔案為空",
"exceed the maximum size of image file for AI recognition": "用於AI識別的圖片超出了允許的最大檔案大小",
"no transaction information detected": "沒有檢測到交易資訊",
"user external auth is not found": "使用者外部驗證資訊不存在",
"oauth 2.0 not enabled": "OAuth 2.0 未啟用",
"oauth 2.0 auto registration not enabled": "OAuth 2.0 自動註冊未啟用",
"invalid oauth 2.0 login request": "無效的 OAuth 2.0 登入請求",
"invalid oauth 2.0 callback": "無效的 OAuth 2.0 回調請求",
"missing state in oauth 2.0 callback": "OAuth 2.0 回調中缺少 state 參數",
"missing code in oauth 2.0 callback": "OAuth 2.0 回調中缺少 code 參數",
"invalid state in oauth 2.0 callback": "OAuth 2.0 回調中的 state 參數無效",
"cannot retrieve oauth 2.0 token": "無法獲取 OAuth 2.0 令牌",
"invalid oauth 2.0 token": "無效的 OAuth 2.0 令牌",
"cannot retrieve user info from oauth 2.0 provider": "無法從 OAuth 2.0 提供者獲取使用者資訊",
"query items cannot be blank": "查詢項目不能為空",
"query items too much": "查詢項目過多",
"query items have invalid item": "查詢項目中有非法項目",
@@ -1391,6 +1405,7 @@
"Operation": "操作",
"Open": "開啟",
"Close": "關閉",
"or": "或",
"Submit": "提交",
"Add": "新增",
"Import": "匯入",
@@ -1572,7 +1587,10 @@
"This month or later": "本月或更晚",
"This year or later": "今年或更晚",
"Log In": "登入",
"Log in with OAuth 2.0": "使用 OAuth 2.0 登入",
"Log in with Connect ID": "使用 Connect ID 登入",
"Click here to log in": "點擊這裡登入",
"Logging in...": "正在登入...",
"Back to login page": "返回登入頁面",
"Back to home page": "返回首頁",
"Don't have an account?": "還沒有帳號?",
+4
View File
@@ -0,0 +1,4 @@
export interface OAuth2CallbackLoginRequest {
readonly provider?: string;
readonly password?: string;
}
+12
View File
@@ -9,6 +9,7 @@ import SignUpPage from '@/views/desktop/SignupPage.vue';
import VerifyEmailPage from '@/views/desktop/VerifyEmailPage.vue';
import ForgetPasswordPage from '@/views/desktop/ForgetPasswordPage.vue';
import ResetPasswordPage from '@/views/desktop/ResetPasswordPage.vue';
import OAuth2CallbackPage from '@/views/desktop/OAuth2CallbackPage.vue';
import UnlockPage from '@/views/desktop/UnlockPage.vue';
import HomePage from '@/views/desktop/HomePage.vue';
@@ -226,6 +227,17 @@ const router = createRouter({
token: route.query['token']
})
},
{
path: '/oauth2_callback',
component: OAuth2CallbackPage,
props: route => ({
token: route.query['token'],
provider: route.query['provider'],
platform: route.query['platform'],
userName: route.query['userName'],
error: route.query['error']
})
},
{
path: '/unlock',
component: UnlockPage,
+57 -1
View File
@@ -21,8 +21,8 @@ import type {
UserProfileUpdateRequest,
UserProfileUpdateResponse
} from '@/models/user.ts';
import type { LocalizedPresetCategory } from '@/core/category.ts';
import type { ForgetPasswordRequest } from '@/models/forget_password.ts';
import type { LocalizedPresetCategory } from '@/core/category.ts';
import {
isObject,
@@ -77,6 +77,10 @@ export const useRootStore = defineStore('root', () => {
currentNotification.value = content;
}
function generateOAuth2LoginUrl(platform: 'mobile' | 'desktop', clientSessionId: string): string {
return services.generateOAuth2LoginUrl(platform, clientSessionId);
}
function authorize(req: UserLoginRequest): Promise<AuthResponse> {
return new Promise((resolve, reject) => {
services.authorize(req).then(response => {
@@ -187,6 +191,56 @@ export const useRootStore = defineStore('root', () => {
});
}
function authorizeOAuth2({ provider, password, token }: { provider: string, password?: string, token: string }): Promise<AuthResponse> {
return new Promise((resolve, reject) => {
services.authorizeOAuth2({
req: {
provider,
password
},
token
}).then(response => {
const data = response.data;
if (!data || !data.success || !data.result || !data.result.token) {
reject({ message: 'Unable to log in' });
return;
}
if (settingsStore.appSettings.applicationLock || hasUserAppLockState()) {
const appLockState = getUserAppLockState();
if (!appLockState || appLockState.username !== data.result.user?.username) {
clearCurrentTokenAndUserInfo(true);
settingsStore.setEnableApplicationLock(false);
settingsStore.setEnableApplicationLockWebAuthn(false);
clearWebAuthnConfig();
}
}
settingsStore.setApplicationSettingsFromCloudSettings(data.result.applicationCloudSettings);
updateCurrentToken(data.result.token);
if (data.result.user && isObject(data.result.user)) {
userStore.storeUserBasicInfo(data.result.user);
}
resolve(data.result);
}).catch(error => {
logger.error('failed to login', error);
if (error && error.processed) {
reject(error);
} else if (error.response && error.response.data && error.response.data.errorMessage) {
reject({ error: error.response.data });
} else {
reject({ message: 'Unable to log in' });
}
});
});
}
function register({ user, presetCategories }: { user: User, presetCategories?: LocalizedPresetCategory[] }): Promise<RegisterResponse> {
return new Promise((resolve, reject) => {
services.register(user.toRegisterRequest(presetCategories)).then(response => {
@@ -588,8 +642,10 @@ export const useRootStore = defineStore('root', () => {
currentNotification,
// functions
setNotificationContent,
generateOAuth2LoginUrl,
authorize,
authorize2FA,
authorizeOAuth2,
register,
lock,
logout,
+10 -4
View File
@@ -8,12 +8,12 @@ import { useExchangeRatesStore } from '@/stores/exchangeRates.ts';
import type { AuthResponse } from '@/models/auth_response.ts';
import { getLoginPageTips } from '@/lib/server_settings.ts';
import { getOAuth2Provider, getOIDCCustomDisplayNames, getLoginPageTips } from '@/lib/server_settings.ts';
import { getClientDisplayVersion } from '@/lib/version.ts';
import { setExpenseAndIncomeAmountColor } from '@/lib/ui/common.ts';
export function useLoginPageBase() {
const { getServerTipContent, setLanguage } = useI18n();
export function useLoginPageBase(platform: 'mobile' | 'desktop') {
const { getServerMultiLanguageConfigContent, getLocalizedOAuth2LoginText, setLanguage } = useI18n();
const rootStore = useRootStore();
const settingsStore = useSettingsStore();
@@ -27,6 +27,7 @@ export function useLoginPageBase() {
const backupCode = ref<string>('');
const tempToken = ref<string>('');
const twoFAVerifyType = ref<string>('passcode');
const oauth2ClientSessionId = ref<string>('');
const logining = ref<boolean>(false);
const verifying = ref<boolean>(false);
@@ -40,7 +41,9 @@ export function useLoginPageBase() {
}
});
const tips = computed<string>(() => getServerTipContent(getLoginPageTips()));
const oauth2LoginUrl = computed<string>(() => rootStore.generateOAuth2LoginUrl(platform, oauth2ClientSessionId.value));
const oauth2LoginDisplayName = computed<string>(() => getLocalizedOAuth2LoginText(getOAuth2Provider(), getOIDCCustomDisplayNames()));
const tips = computed<string>(() => getServerMultiLanguageConfigContent(getLoginPageTips()));
function doAfterLogin(authResponse: AuthResponse): void {
if (authResponse.user) {
@@ -69,11 +72,14 @@ export function useLoginPageBase() {
backupCode,
tempToken,
twoFAVerifyType,
oauth2ClientSessionId,
logining,
verifying,
// computed states
inputIsEmpty,
twoFAInputIsEmpty,
oauth2LoginUrl,
oauth2LoginDisplayName,
tips,
// functions
doAfterLogin
+29 -7
View File
@@ -24,14 +24,14 @@
<v-card variant="flat" class="w-100 mt-0 px-4 pt-12" max-width="500">
<v-card-text>
<h4 class="text-h4 mb-2">{{ tt('Welcome to ezBookkeeping') }}</h4>
<p class="mb-0">{{ tt('Please log in with your ezBookkeeping account') }}</p>
<p class="mb-0" v-if="isInternalAuthEnabled()">{{ tt('Please log in with your ezBookkeeping account') }}</p>
<p class="mt-1 mb-0" v-if="tips">{{ tips }}</p>
</v-card-text>
<v-card-text class="pb-0 mb-6">
<v-form>
<v-row>
<v-col cols="12">
<v-col cols="12" v-if="isInternalAuthEnabled()">
<v-text-field
type="text"
autocomplete="username"
@@ -45,7 +45,7 @@
/>
</v-col>
<v-col cols="12">
<v-col cols="12" v-if="isInternalAuthEnabled()">
<v-text-field
autocomplete="current-password"
ref="passwordInput"
@@ -101,10 +101,20 @@
<v-col cols="12">
<v-btn block :disabled="inputIsEmpty || logining || verifying"
@click="login" v-if="!show2faInput">
@click="login" v-if="isInternalAuthEnabled() && !show2faInput">
{{ tt('Log In') }}
<v-progress-circular indeterminate size="22" class="ms-2" v-if="logining"></v-progress-circular>
</v-btn>
<v-col cols="12" class="d-flex align-center px-0" v-if="isInternalAuthEnabled() && isOAuth2Enabled()">
<v-divider class="me-3" />
{{ tt('or') }}
<v-divider class="ms-3" />
</v-col>
<v-btn block :disabled="logining || verifying" :href="oauth2LoginUrl" v-if="isOAuth2Enabled()">
{{ oauth2LoginDisplayName }}
</v-btn>
<v-btn block :disabled="twoFAInputIsEmpty || logining || verifying"
@click="verify" v-else-if="show2faInput">
{{ tt('Continue') }}
@@ -112,7 +122,7 @@
</v-btn>
</v-col>
<v-col cols="12" class="text-center text-base">
<v-col cols="12" class="text-center text-base" v-if="isInternalAuthEnabled()">
<span class="me-1">{{ tt('Don\'t have an account?') }}</span>
<router-link class="text-primary" to="/signup"
:class="{'disabled': !isUserRegistrationEnabled()}">
@@ -170,7 +180,14 @@ import { ThemeType } from '@/core/theme.ts';
import { APPLICATION_LOGO_PATH } from '@/consts/asset.ts';
import { KnownErrorCode } from '@/consts/api.ts';
import { isUserRegistrationEnabled, isUserForgetPasswordEnabled, isUserVerifyEmailEnabled } from '@/lib/server_settings.ts';
import { generateRandomUUID } from '@/lib/misc.ts';
import {
isUserRegistrationEnabled,
isUserForgetPasswordEnabled,
isUserVerifyEmailEnabled,
isInternalAuthEnabled,
isOAuth2Enabled
} from '@/lib/server_settings.ts';
import {
mdiOnepassword,
@@ -194,13 +211,16 @@ const {
backupCode,
tempToken,
twoFAVerifyType,
oauth2ClientSessionId,
logining,
verifying,
inputIsEmpty,
twoFAInputIsEmpty,
oauth2LoginUrl,
oauth2LoginDisplayName,
tips,
doAfterLogin
} = useLoginPageBase();
} = useLoginPageBase('desktop');
const passwordInput = useTemplateRef<VTextField>('passwordInput');
const passcodeInput = useTemplateRef<VTextField>('passcodeInput');
@@ -301,4 +321,6 @@ function verify(): void {
}
});
}
oauth2ClientSessionId.value = generateRandomUUID();
</script>
+215
View File
@@ -0,0 +1,215 @@
<template>
<div class="layout-wrapper">
<router-link to="/">
<div class="auth-logo d-flex align-start gap-x-3">
<img alt="logo" class="login-page-logo" :src="APPLICATION_LOGO_PATH" />
<h1 class="font-weight-medium leading-normal text-2xl">{{ tt('global.app.title') }}</h1>
</div>
</router-link>
<v-row no-gutters class="auth-wrapper">
<v-col cols="12" md="8" class="auth-image-background d-none d-md-flex align-center justify-center position-relative">
<div class="d-flex auth-img-footer" v-if="!isDarkMode">
<v-img class="img-with-direction" src="img/desktop/background.svg"/>
</div>
<div class="d-flex auth-img-footer" v-if="isDarkMode">
<v-img class="img-with-direction" src="img/desktop/background-dark.svg"/>
</div>
<div class="d-flex align-center justify-center w-100 pt-10">
<v-img class="img-with-direction" max-width="300px" src="img/desktop/people2.svg" v-if="!isDarkMode"/>
<v-img class="img-with-direction" max-width="300px" src="img/desktop/people2-dark.svg" v-else-if="isDarkMode"/>
</div>
</v-col>
<v-col cols="12" md="4" class="auth-card d-flex flex-column">
<div class="d-flex align-center justify-center h-100">
<v-card variant="flat" class="w-100 mt-0 px-4 pt-12" max-width="500">
<v-card-text>
<h4 class="text-h4 mb-2">{{ oauth2LoginDisplayName }}</h4>
<p class="mb-0" v-if="!error && platform && token && !userName">{{ tt('Logging in...') }}</p>
<p class="mb-0" v-else-if="!error && userName">{{ tt('format.misc.oauth2bindTip', { providerName: oauth2ProviderDisplayName, userName: userName }) }}</p>
<p class="mb-0" v-else-if="error">{{ tt(error) }}</p>
<p class="mb-0" v-else>{{ tt('An error occurred') }}</p>
</v-card-text>
<v-card-text class="pb-0 mb-6" v-if="!error && userName">
<v-form>
<v-row>
<v-col cols="12">
<v-text-field
type="password"
autocomplete="password"
:autofocus="true"
:disabled="logining"
:label="tt('Password')"
:placeholder="tt('Your password')"
v-model="password"
@keyup.enter="verify"
/>
</v-col>
<v-col cols="12">
<v-btn block type="submit" :disabled="!password || logining" @click="verify">
{{ tt('Continue') }}
<v-progress-circular indeterminate size="22" class="ms-2" v-if="logining"></v-progress-circular>
</v-btn>
</v-col>
<v-col cols="12">
<router-link class="d-flex align-center justify-center" to="/login"
:class="{ 'disabled': logining }">
<v-icon class="icon-with-direction" :icon="mdiChevronLeft"/>
<span>{{ tt('Back to login page') }}</span>
</router-link>
</v-col>
</v-row>
</v-form>
</v-card-text>
</v-card>
</div>
<v-spacer/>
<div class="d-flex align-center justify-center">
<v-card variant="flat" class="w-100 px-4 pb-4" max-width="500">
<v-card-text class="pt-0">
<v-row>
<v-col cols="12" class="text-center">
<language-select-button :disabled="logining" />
</v-col>
<v-col cols="12" class="d-flex align-center pt-0">
<v-divider />
</v-col>
<v-col cols="12" class="text-center text-sm">
<span>Powered by </span>
<a href="https://github.com/mayswind/ezbookkeeping" target="_blank">ezBookkeeping</a>&nbsp;<span>{{ version }}</span>
</v-col>
</v-row>
</v-card-text>
</v-card>
</div>
</v-col>
</v-row>
<snack-bar ref="snackbar" />
</div>
</template>
<script setup lang="ts">
import SnackBar from '@/components/desktop/SnackBar.vue';
import { computed, useTemplateRef } from 'vue';
import { useRouter } from 'vue-router';
import { useTheme } from 'vuetify';
import { useI18n } from '@/locales/helpers.ts';
import { useLoginPageBase } from '@/views/base/LoginPageBase.ts';
import { useRootStore } from '@/stores/index.ts';
import { ThemeType } from '@/core/theme.ts';
import { APPLICATION_LOGO_PATH } from '@/consts/asset.ts';
import { KnownErrorCode } from '@/consts/api.ts';
import {
isUserVerifyEmailEnabled,
getOAuth2Provider,
getOIDCCustomDisplayNames
} from '@/lib/server_settings.ts';
import {
mdiChevronLeft
} from '@mdi/js';
type SnackBarType = InstanceType<typeof SnackBar>;
const props = defineProps<{
token?: string;
provider?: string;
platform?: string;
userName?: string;
error?: string;
}>();
const router = useRouter();
const theme = useTheme();
const { tt, getLocalizedOAuth2ProviderName, getLocalizedOAuth2LoginText } = useI18n();
const rootStore = useRootStore();
const {
version,
password,
logining,
doAfterLogin
} = useLoginPageBase('desktop');
const snackbar = useTemplateRef<SnackBarType>('snackbar');
const isDarkMode = computed<boolean>(() => theme.global.name.value === ThemeType.Dark);
const oauth2ProviderDisplayName = computed<string>(() => getLocalizedOAuth2ProviderName(getOAuth2Provider(), getOIDCCustomDisplayNames()));
const oauth2LoginDisplayName = computed<string>(() => getLocalizedOAuth2LoginText(getOAuth2Provider(), getOIDCCustomDisplayNames()));
const inputProblemMessage = computed<string | null>(() => {
if (!password.value) {
return 'Password cannot be blank';
} else {
return null;
}
});
function verify(): void {
const problemMessage = inputProblemMessage.value;
if (problemMessage) {
snackbar.value?.showMessage(problemMessage);
return;
}
logining.value = true;
rootStore.authorizeOAuth2({
provider: props.provider || '',
password: password.value,
token: props.token || ''
}).then(authResponse => {
logining.value = false;
doAfterLogin(authResponse);
router.replace('/');
}).catch(error => {
logining.value = false;
if (isUserVerifyEmailEnabled() && error.error && error.error.errorCode === KnownErrorCode.UserEmailNotVerified && error.error.context && error.error.context.email) {
router.push(`/verify_email?email=${encodeURIComponent(error.error.context.email)}&emailSent=${error.error.context.hasValidEmailVerifyToken || false}`);
return;
}
if (!error.processed) {
snackbar.value?.showError(error);
}
});
}
if (!props.error && props.platform && props.token && !props.userName) {
logining.value = true;
rootStore.authorizeOAuth2({
provider: props.provider || '',
token: props.token || ''
}).then(authResponse => {
logining.value = false;
doAfterLogin(authResponse);
router.replace('/');
}).catch(error => {
logining.value = false;
if (isUserVerifyEmailEnabled() && error.error && error.error.errorCode === KnownErrorCode.UserEmailNotVerified && error.error.context && error.error.context.email) {
router.push(`/verify_email?email=${encodeURIComponent(error.error.context.email)}&emailSent=${error.error.context.hasValidEmailVerifyToken || false}`);
return;
}
if (!error.processed) {
snackbar.value?.showError(error);
}
});
}
</script>
+51 -5
View File
@@ -9,7 +9,7 @@
<f7-block-footer>{{ tips }}</f7-block-footer>
</f7-list>
<f7-list form dividers class="margin-bottom-half">
<f7-list form dividers class="margin-bottom-half" v-if="isInternalAuthEnabled()">
<f7-list-input
type="text"
autocomplete="username"
@@ -47,8 +47,14 @@
</f7-list>
<f7-list class="margin-vertical-half">
<f7-list-button :class="{ 'disabled': inputIsEmpty || logining }" :text="tt('Log In')" @click="login"></f7-list-button>
<f7-block-footer>
<f7-list-button :class="{ 'disabled': inputIsEmpty || logining }" :text="tt('Log In')" @click="login" v-if="isInternalAuthEnabled()"></f7-list-button>
<f7-list-item class="login-divider display-flex align-items-center" v-if="isInternalAuthEnabled() && isOAuth2Enabled()">
<hr class="margin-inline-end-half" />
<small>{{ tt('or') }}</small>
<hr class="margin-inline-start-half" />
</f7-list-item>
<f7-list-button external :class="{ 'disabled': logining }" :href="oauth2LoginUrl" :text="oauth2LoginDisplayName" v-if="isOAuth2Enabled()"></f7-list-button>
<f7-block-footer v-if="isInternalAuthEnabled()">
<span>{{ tt('Don\'t have an account?') }}</span>&nbsp;
<f7-link :class="{'disabled': !isUserRegistrationEnabled()}" href="/signup" :text="tt('Create an account')"></f7-link>
</f7-block-footer>
@@ -176,7 +182,15 @@ import { useRootStore } from '@/stores/index.ts';
import { APPLICATION_LOGO_PATH } from '@/consts/asset.ts';
import { KnownErrorCode } from '@/consts/api.ts';
import { isUserRegistrationEnabled, isUserForgetPasswordEnabled, isUserVerifyEmailEnabled } from '@/lib/server_settings.ts';
import { generateRandomUUID } from '@/lib/misc.ts';
import {
isUserRegistrationEnabled,
isUserForgetPasswordEnabled,
isUserVerifyEmailEnabled,
isInternalAuthEnabled,
isOAuth2Enabled
} from '@/lib/server_settings.ts';
import { getDesktopVersionPath } from '@/lib/version.ts';
import { useI18nUIComponents, showLoading, hideLoading, isModalShowing } from '@/lib/ui/mobile.ts';
@@ -197,13 +211,16 @@ const {
backupCode,
tempToken,
twoFAVerifyType,
oauth2ClientSessionId,
logining,
verifying,
inputIsEmpty,
twoFAInputIsEmpty,
oauth2LoginUrl,
oauth2LoginDisplayName,
tips,
doAfterLogin
} = useLoginPageBase();
} = useLoginPageBase('mobile');
const forgetPasswordEmail = ref<string>('');
const resendVerifyEmail = ref<string>('');
@@ -389,4 +406,33 @@ function switch2FAVerifyType(): void {
twoFAVerifyType.value = 'passcode';
}
}
oauth2ClientSessionId.value = generateRandomUUID();
</script>
<style>
.login-divider > .item-content {
width: 100%;
min-height: 0;
> .item-inner {
padding-top: 0;
padding-bottom: 0;
min-height: 0;
> small {
opacity: 0.7;
}
> hr {display: block;
flex: 1 1 100%;
height: 0;
max-height: 0;
border-style: solid;
border-width: thin 0 0 0;
opacity: 0.12;
transition: inherit;
}
}
}
</style>