Feat: Implement disable password authentication (#168)

This commit is contained in:
Daniel Luiz Alves
2025-07-21 18:02:39 -03:00
committed by GitHub
32 changed files with 460 additions and 67 deletions

View File

@@ -147,6 +147,12 @@ const defaultConfigs = [
type: "boolean", type: "boolean",
group: "auth-providers", group: "auth-providers",
}, },
{
key: "passwordAuthEnabled",
value: "true",
type: "boolean",
group: "security",
},
{ {
key: "serverUrl", key: "serverUrl",
value: "http://localhost:3333", value: "http://localhost:3333",

View File

@@ -46,6 +46,17 @@ export class AppService {
throw new Error("JWT Secret cannot be updated through this endpoint"); throw new Error("JWT Secret cannot be updated through this endpoint");
} }
if (key === "passwordAuthEnabled") {
if (value === "false") {
const canDisable = await this.configService.validatePasswordAuthDisable();
if (!canDisable) {
throw new Error(
"Password authentication cannot be disabled. At least one authentication provider must be active."
);
}
}
}
const config = await prisma.appConfig.findUnique({ const config = await prisma.appConfig.findUnique({
where: { key }, where: { key },
}); });
@@ -64,6 +75,15 @@ export class AppService {
if (updates.some((update) => update.key === "jwtSecret")) { if (updates.some((update) => update.key === "jwtSecret")) {
throw new Error("JWT Secret cannot be updated through this endpoint"); throw new Error("JWT Secret cannot be updated through this endpoint");
} }
const passwordAuthUpdate = updates.find((update) => update.key === "passwordAuthEnabled");
if (passwordAuthUpdate && passwordAuthUpdate.value === "false") {
const canDisable = await this.configService.validatePasswordAuthDisable();
if (!canDisable) {
throw new Error(
"Password authentication cannot be disabled. At least one authentication provider must be active."
);
}
}
const keys = updates.map((update) => update.key); const keys = updates.map((update) => update.key);
const existingConfigs = await prisma.appConfig.findMany({ const existingConfigs = await prisma.appConfig.findMany({

View File

@@ -1,5 +1,6 @@
import { FastifyReply, FastifyRequest } from "fastify"; import { FastifyReply, FastifyRequest } from "fastify";
import { ConfigService } from "../config/service";
import { UpdateAuthProviderSchema } from "./dto"; import { UpdateAuthProviderSchema } from "./dto";
import { AuthProvidersService } from "./service"; import { AuthProvidersService } from "./service";
import { import {
@@ -39,9 +40,11 @@ const ERROR_MESSAGES = {
export class AuthProvidersController { export class AuthProvidersController {
private authProvidersService: AuthProvidersService; private authProvidersService: AuthProvidersService;
private configService: ConfigService;
constructor() { constructor() {
this.authProvidersService = new AuthProvidersService(); this.authProvidersService = new AuthProvidersService();
this.configService = new ConfigService();
} }
private buildRequestContext(request: FastifyRequest): RequestContext { private buildRequestContext(request: FastifyRequest): RequestContext {
@@ -223,13 +226,24 @@ export class AuthProvidersController {
try { try {
const { id } = request.params; const { id } = request.params;
const data = request.body; const data = request.body as any;
const existingProvider = await this.authProvidersService.getProviderById(id); const existingProvider = await this.authProvidersService.getProviderById(id);
if (!existingProvider) { if (!existingProvider) {
return this.sendErrorResponse(reply, 404, ERROR_MESSAGES.PROVIDER_NOT_FOUND); return this.sendErrorResponse(reply, 404, ERROR_MESSAGES.PROVIDER_NOT_FOUND);
} }
if (data.enabled === false && existingProvider.enabled === true) {
const canDisable = await this.configService.validateAllProvidersDisable();
if (!canDisable) {
return this.sendErrorResponse(
reply,
400,
"Cannot disable the last authentication provider when password authentication is disabled"
);
}
}
const isOfficial = this.authProvidersService.isOfficialProvider(existingProvider.name); const isOfficial = this.authProvidersService.isOfficialProvider(existingProvider.name);
if (isOfficial) { if (isOfficial) {
@@ -300,6 +314,17 @@ export class AuthProvidersController {
return this.sendErrorResponse(reply, 400, ERROR_MESSAGES.OFFICIAL_CANNOT_DELETE); return this.sendErrorResponse(reply, 400, ERROR_MESSAGES.OFFICIAL_CANNOT_DELETE);
} }
if (provider.enabled) {
const canDisable = await this.configService.validateAllProvidersDisable();
if (!canDisable) {
return this.sendErrorResponse(
reply,
400,
"Cannot delete the last authentication provider when password authentication is disabled"
);
}
}
await this.authProvidersService.deleteProvider(id); await this.authProvidersService.deleteProvider(id);
return this.sendSuccessResponse(reply, undefined, "Provider deleted successfully"); return this.sendSuccessResponse(reply, undefined, "Provider deleted successfully");
} catch (error) { } catch (error) {

View File

@@ -1,6 +1,7 @@
import { FastifyReply, FastifyRequest } from "fastify"; import { FastifyReply, FastifyRequest } from "fastify";
import { env } from "../../env"; import { env } from "../../env";
import { ConfigService } from "../config/service";
import { import {
CompleteTwoFactorLoginSchema, CompleteTwoFactorLoginSchema,
createResetPasswordSchema, createResetPasswordSchema,
@@ -11,6 +12,7 @@ import { AuthService } from "./service";
export class AuthController { export class AuthController {
private authService = new AuthService(); private authService = new AuthService();
private configService = new ConfigService();
private getClientInfo(request: FastifyRequest) { private getClientInfo(request: FastifyRequest) {
const realIP = request.headers["x-real-ip"] as string; const realIP = request.headers["x-real-ip"] as string;
@@ -169,4 +171,15 @@ export class AuthController {
return reply.status(400).send({ error: error.message }); return reply.status(400).send({ error: error.message });
} }
} }
async getAuthConfig(request: FastifyRequest, reply: FastifyReply) {
try {
const passwordAuthEnabled = await this.configService.getValue("passwordAuthEnabled");
return reply.send({
passwordAuthEnabled: passwordAuthEnabled === "true",
});
} catch (error: any) {
return reply.status(400).send({ error: error.message });
}
}
} }

View File

@@ -280,4 +280,23 @@ export async function authRoutes(app: FastifyInstance) {
}, },
authController.removeAllTrustedDevices.bind(authController) authController.removeAllTrustedDevices.bind(authController)
); );
app.get(
"/auth/config",
{
schema: {
tags: ["Authentication"],
operationId: "getAuthConfig",
summary: "Get Authentication Configuration",
description: "Get authentication configuration settings",
response: {
200: z.object({
passwordAuthEnabled: z.boolean().describe("Whether password authentication is enabled"),
}),
400: z.object({ error: z.string().describe("Error message") }),
},
},
},
authController.getAuthConfig.bind(authController)
);
} }

View File

@@ -18,6 +18,11 @@ export class AuthService {
private trustedDeviceService = new TrustedDeviceService(); private trustedDeviceService = new TrustedDeviceService();
async login(data: LoginInput, userAgent?: string, ipAddress?: string) { async login(data: LoginInput, userAgent?: string, ipAddress?: string) {
const passwordAuthEnabled = await this.configService.getValue("passwordAuthEnabled");
if (passwordAuthEnabled === "false") {
throw new Error("Password authentication is disabled. Please use an external authentication provider.");
}
const user = await this.userRepository.findUserByEmailOrUsername(data.emailOrUsername); const user = await this.userRepository.findUserByEmailOrUsername(data.emailOrUsername);
if (!user) { if (!user) {
throw new Error("Invalid credentials"); throw new Error("Invalid credentials");
@@ -146,6 +151,11 @@ export class AuthService {
} }
async requestPasswordReset(email: string, origin: string) { async requestPasswordReset(email: string, origin: string) {
const passwordAuthEnabled = await this.configService.getValue("passwordAuthEnabled");
if (passwordAuthEnabled === "false") {
throw new Error("Password authentication is disabled. Password reset is not available.");
}
const user = await this.userRepository.findUserByEmail(email); const user = await this.userRepository.findUserByEmail(email);
if (!user) { if (!user) {
return; return;
@@ -171,6 +181,11 @@ export class AuthService {
} }
async resetPassword(token: string, newPassword: string) { async resetPassword(token: string, newPassword: string) {
const passwordAuthEnabled = await this.configService.getValue("passwordAuthEnabled");
if (passwordAuthEnabled === "false") {
throw new Error("Password authentication is disabled. Password reset is not available.");
}
const resetRequest = await prisma.passwordReset.findFirst({ const resetRequest = await prisma.passwordReset.findFirst({
where: { where: {
token, token,

View File

@@ -13,6 +13,26 @@ export class ConfigService {
return config.value; return config.value;
} }
async setValue(key: string, value: string): Promise<void> {
await prisma.appConfig.update({
where: { key },
data: { value },
});
}
async validatePasswordAuthDisable(): Promise<boolean> {
const enabledProviders = await prisma.authProvider.findMany({
where: { enabled: true },
});
return enabledProviders.length > 0;
}
async validateAllProvidersDisable(): Promise<boolean> {
const passwordAuthEnabled = await this.getValue("passwordAuthEnabled");
return passwordAuthEnabled === "true";
}
async getGroupConfigs(group: string) { async getGroupConfigs(group: string) {
const configs = await prisma.appConfig.findMany({ const configs = await prisma.appConfig.findMany({
where: { group }, where: { group },

View File

@@ -313,7 +313,8 @@
"title": "نسيت كلمة المرور", "title": "نسيت كلمة المرور",
"description": "أدخل بريدك الإلكتروني وسنرسل لك تعليمات إعادة تعيين كلمة المرور.", "description": "أدخل بريدك الإلكتروني وسنرسل لك تعليمات إعادة تعيين كلمة المرور.",
"resetInstructions": "تم إرسال تعليمات إعادة التعيين إلى بريدك الإلكتروني", "resetInstructions": "تم إرسال تعليمات إعادة التعيين إلى بريدك الإلكتروني",
"pageTitle": "نسيت كلمة المرور" "pageTitle": "نسيت كلمة المرور",
"passwordAuthDisabled": "تم تعطيل المصادقة بكلمة المرور. يرجى الاتصال بالمسؤول أو استخدام مزود مصادقة خارجي."
}, },
"generateShareLink": { "generateShareLink": {
"generateTitle": "إنشاء رابط المشاركة", "generateTitle": "إنشاء رابط المشاركة",
@@ -1130,6 +1131,10 @@
"smtpTrustSelfSigned": { "smtpTrustSelfSigned": {
"title": "الوثوق بالشهادات الموقعة ذاتياً", "title": "الوثوق بالشهادات الموقعة ذاتياً",
"description": "قم بتمكين هذا للوثوق بشهادات SSL/TLS الموقعة ذاتياً (مفيد لبيئات التطوير)" "description": "قم بتمكين هذا للوثوق بشهادات SSL/TLS الموقعة ذاتياً (مفيد لبيئات التطوير)"
},
"passwordAuthEnabled": {
"title": "المصادقة بالكلمة السرية",
"description": "تمكين أو تعطيل المصادقة بالكلمة السرية"
} }
}, },
"buttons": { "buttons": {
@@ -1139,7 +1144,8 @@
}, },
"errors": { "errors": {
"loadFailed": "فشل في تحميل الإعدادات", "loadFailed": "فشل في تحميل الإعدادات",
"updateFailed": "فشل في تحديث الإعدادات" "updateFailed": "فشل في تحديث الإعدادات",
"passwordAuthRequiresProvider": "لا يمكن تعطيل المصادقة بالكلمة السرية دون وجود على الأقل موفرين مصادقة مفعلين"
}, },
"messages": { "messages": {
"noChanges": "لا توجد تغييرات للحفظ", "noChanges": "لا توجد تغييرات للحفظ",

View File

@@ -313,7 +313,8 @@
"title": "Passwort vergessen", "title": "Passwort vergessen",
"description": "Geben Sie Ihre E-Mail-Adresse ein und wir senden Ihnen Anweisungen zum Zurücksetzen Ihres Passworts.", "description": "Geben Sie Ihre E-Mail-Adresse ein und wir senden Ihnen Anweisungen zum Zurücksetzen Ihres Passworts.",
"resetInstructions": "Anweisungen zum Zurücksetzen wurden an Ihre E-Mail gesendet", "resetInstructions": "Anweisungen zum Zurücksetzen wurden an Ihre E-Mail gesendet",
"pageTitle": "Passwort vergessen" "pageTitle": "Passwort vergessen",
"passwordAuthDisabled": "Passwortauthentifizierung ist deaktiviert. Bitte kontaktieren Sie Ihren Administrator oder verwenden Sie einen externen Authentifizierungsanbieter."
}, },
"generateShareLink": { "generateShareLink": {
"generateTitle": "Freigabe-Link generieren", "generateTitle": "Freigabe-Link generieren",
@@ -1128,6 +1129,10 @@
"tls": "STARTTLS (Port 587)", "tls": "STARTTLS (Port 587)",
"none": "Keine (Unsicher)" "none": "Keine (Unsicher)"
} }
},
"passwordAuthEnabled": {
"title": "Passwort-Authentifizierung",
"description": "Passwort-basierte Authentifizierung aktivieren oder deaktivieren"
} }
}, },
"buttons": { "buttons": {
@@ -1137,7 +1142,8 @@
}, },
"errors": { "errors": {
"loadFailed": "Fehler beim Laden der Einstellungen", "loadFailed": "Fehler beim Laden der Einstellungen",
"updateFailed": "Fehler beim Aktualisieren der Einstellungen" "updateFailed": "Fehler beim Aktualisieren der Einstellungen",
"passwordAuthRequiresProvider": "Passwort-basierte Authentifizierung kann nicht deaktiviert werden, wenn kein aktiver Authentifizierungsanbieter vorhanden ist"
}, },
"messages": { "messages": {
"noChanges": "Keine Änderungen zum Speichern", "noChanges": "Keine Änderungen zum Speichern",

View File

@@ -313,7 +313,8 @@
"title": "Forgot Password", "title": "Forgot Password",
"description": "Enter your email address and we'll send you instructions to reset your password", "description": "Enter your email address and we'll send you instructions to reset your password",
"resetInstructions": "Reset instructions sent to your email", "resetInstructions": "Reset instructions sent to your email",
"pageTitle": "Forgot Password" "pageTitle": "Forgot Password",
"passwordAuthDisabled": "Password authentication is disabled. Please contact your administrator or use an external authentication provider."
}, },
"generateShareLink": { "generateShareLink": {
"generateTitle": "Generate Share Link", "generateTitle": "Generate Share Link",
@@ -1131,6 +1132,10 @@
"serverUrl": { "serverUrl": {
"title": "Server URL", "title": "Server URL",
"description": "Base URL of the Palmr server (e.g.: https://palmr.example.com)" "description": "Base URL of the Palmr server (e.g.: https://palmr.example.com)"
},
"passwordAuthEnabled": {
"title": "Password Authentication",
"description": "Enable or disable password-based authentication"
} }
}, },
"buttons": { "buttons": {
@@ -1140,7 +1145,8 @@
}, },
"errors": { "errors": {
"loadFailed": "Failed to load settings", "loadFailed": "Failed to load settings",
"updateFailed": "Failed to update settings" "updateFailed": "Failed to update settings",
"passwordAuthRequiresProvider": "Cannot disable password authentication without having at least one active authentication provider"
}, },
"messages": { "messages": {
"noChanges": "No changes to save", "noChanges": "No changes to save",

View File

@@ -313,7 +313,8 @@
"title": "Recuperar contraseña", "title": "Recuperar contraseña",
"description": "Introduce tu dirección de correo electrónico y te enviaremos instrucciones para restablecer tu contraseña.", "description": "Introduce tu dirección de correo electrónico y te enviaremos instrucciones para restablecer tu contraseña.",
"resetInstructions": "Instrucciones de restablecimiento enviadas a tu correo electrónico", "resetInstructions": "Instrucciones de restablecimiento enviadas a tu correo electrónico",
"pageTitle": "Recuperar contraseña" "pageTitle": "Recuperar contraseña",
"passwordAuthDisabled": "La autenticación por contraseña está deshabilitada. Por favor, contacta a tu administrador o usa un proveedor de autenticación externo."
}, },
"generateShareLink": { "generateShareLink": {
"generateTitle": "Generar enlace de compartir", "generateTitle": "Generar enlace de compartir",
@@ -1128,6 +1129,10 @@
"tls": "STARTTLS (Puerto 587)", "tls": "STARTTLS (Puerto 587)",
"none": "Ninguno (Inseguro)" "none": "Ninguno (Inseguro)"
} }
},
"passwordAuthEnabled": {
"title": "Autenticación por Contraseña",
"description": "Habilitar o deshabilitar la autenticación basada en contraseña"
} }
}, },
"buttons": { "buttons": {
@@ -1137,7 +1142,8 @@
}, },
"errors": { "errors": {
"loadFailed": "Error al cargar la configuración", "loadFailed": "Error al cargar la configuración",
"updateFailed": "Error al actualizar la configuración" "updateFailed": "Error al actualizar la configuración",
"passwordAuthRequiresProvider": "No se puede deshabilitar la autenticación por contraseña sin tener al menos un proveedor de autenticación activo"
}, },
"messages": { "messages": {
"noChanges": "No hay cambios para guardar", "noChanges": "No hay cambios para guardar",

View File

@@ -313,7 +313,8 @@
"title": "Mot de Passe Oublié", "title": "Mot de Passe Oublié",
"description": "Entrez votre adresse email et nous vous enverrons les instructions pour réinitialiser votre mot de passe.", "description": "Entrez votre adresse email et nous vous enverrons les instructions pour réinitialiser votre mot de passe.",
"resetInstructions": "Instructions de réinitialisation envoyées à votre email", "resetInstructions": "Instructions de réinitialisation envoyées à votre email",
"pageTitle": "Mot de Passe Oublié" "pageTitle": "Mot de Passe Oublié",
"passwordAuthDisabled": "L'authentification par mot de passe est désactivée. Veuillez contacter votre administrateur ou utiliser un fournisseur d'authentification externe."
}, },
"generateShareLink": { "generateShareLink": {
"generateTitle": "Générer un lien de partage", "generateTitle": "Générer un lien de partage",
@@ -1131,6 +1132,10 @@
"smtpTrustSelfSigned": { "smtpTrustSelfSigned": {
"title": "Faire Confiance aux Certificats Auto-signés", "title": "Faire Confiance aux Certificats Auto-signés",
"description": "Activez cette option pour faire confiance aux certificats SSL/TLS auto-signés (utile pour les environnements de développement)" "description": "Activez cette option pour faire confiance aux certificats SSL/TLS auto-signés (utile pour les environnements de développement)"
},
"passwordAuthEnabled": {
"title": "Authentification par Mot de Passe",
"description": "Activer ou désactiver l'authentification basée sur mot de passe"
} }
}, },
"buttons": { "buttons": {
@@ -1140,7 +1145,8 @@
}, },
"errors": { "errors": {
"loadFailed": "Échec du chargement des paramètres", "loadFailed": "Échec du chargement des paramètres",
"updateFailed": "Échec de la mise à jour des paramètres" "updateFailed": "Échec de la mise à jour des paramètres",
"passwordAuthRequiresProvider": "Impossible de désactiver l'authentification par mot de passe sans avoir au moins un fournisseur d'authentification actif"
}, },
"messages": { "messages": {
"noChanges": "Aucun changement à enregistrer", "noChanges": "Aucun changement à enregistrer",

View File

@@ -313,7 +313,8 @@
"title": "पासवर्ड भूल गए", "title": "पासवर्ड भूल गए",
"description": "अपना ईमेल पता दर्ज करें और हम आपको पासवर्ड रीसेट करने के निर्देश भेजेंगे।", "description": "अपना ईमेल पता दर्ज करें और हम आपको पासवर्ड रीसेट करने के निर्देश भेजेंगे।",
"resetInstructions": "रीसेट निर्देश आपके ईमेल पर भेज दिए गए हैं", "resetInstructions": "रीसेट निर्देश आपके ईमेल पर भेज दिए गए हैं",
"pageTitle": "पासवर्ड भूल गए" "pageTitle": "पासवर्ड भूल गए",
"passwordAuthDisabled": "पासवर्ड ऑथेंटिकेशन अक्टिवेटेड है। कृपया अपने एडमिन से संपर्क करें या एक बाहरी ऑथेंटिकेशन प्रोवाइडर का उपयोग करें।"
}, },
"generateShareLink": { "generateShareLink": {
"generateTitle": "साझाकरण लिंक उत्पन्न करें", "generateTitle": "साझाकरण लिंक उत्पन्न करें",
@@ -1128,6 +1129,10 @@
"smtpTrustSelfSigned": { "smtpTrustSelfSigned": {
"title": "स्व-हस्ताक्षरित प्रमाणपत्रों पर विश्वास करें", "title": "स्व-हस्ताक्षरित प्रमाणपत्रों पर विश्वास करें",
"description": "स्व-हस्ताक्षरित SSL/TLS प्रमाणपत्रों पर विश्वास करने के लिए इसे सक्षम करें (विकास वातावरण के लिए उपयोगी)" "description": "स्व-हस्ताक्षरित SSL/TLS प्रमाणपत्रों पर विश्वास करने के लिए इसे सक्षम करें (विकास वातावरण के लिए उपयोगी)"
},
"passwordAuthEnabled": {
"title": "पासवर्ड प्रमाणीकरण",
"description": "पासवर्ड आधारित प्रमाणीकरण सक्षम या अक्षम करें"
} }
}, },
"buttons": { "buttons": {
@@ -1137,7 +1142,8 @@
}, },
"errors": { "errors": {
"loadFailed": "सेटिंग्स लोड करने में विफल", "loadFailed": "सेटिंग्स लोड करने में विफल",
"updateFailed": "सेटिंग्स अपडेट करने में विफल" "updateFailed": "सेटिंग्स अपडेट करने में विफल",
"passwordAuthRequiresProvider": "कम से कम एक सक्रिय प्रमाणीकरण प्रदाता के बिना पासवर्ड प्रमाणीकरण अक्षम नहीं किया जा सकता"
}, },
"messages": { "messages": {
"noChanges": "सहेजने के लिए कोई परिवर्तन नहीं", "noChanges": "सहेजने के लिए कोई परिवर्तन नहीं",

View File

@@ -313,7 +313,8 @@
"title": "Parola d'accesso Dimenticata", "title": "Parola d'accesso Dimenticata",
"description": "Inserisci il tuo indirizzo email e ti invieremo le istruzioni per reimpostare la parola d'accesso.", "description": "Inserisci il tuo indirizzo email e ti invieremo le istruzioni per reimpostare la parola d'accesso.",
"resetInstructions": "Istruzioni di reimpostazione inviate alla tua email", "resetInstructions": "Istruzioni di reimpostazione inviate alla tua email",
"pageTitle": "Parola d'accesso Dimenticata" "pageTitle": "Parola d'accesso Dimenticata",
"passwordAuthDisabled": "L'autenticazione tramite password è disabilitata. Contatta il tuo amministratore o utilizza un provider di autenticazione esterno."
}, },
"generateShareLink": { "generateShareLink": {
"generateTitle": "Genera link di condivisione", "generateTitle": "Genera link di condivisione",
@@ -1128,6 +1129,10 @@
"smtpTrustSelfSigned": { "smtpTrustSelfSigned": {
"title": "Accetta Certificati Auto-Firmati", "title": "Accetta Certificati Auto-Firmati",
"description": "Abilita questa opzione per accettare certificati SSL/TLS auto-firmati (utile per ambienti di sviluppo)" "description": "Abilita questa opzione per accettare certificati SSL/TLS auto-firmati (utile per ambienti di sviluppo)"
},
"passwordAuthEnabled": {
"title": "Autenticazione Password",
"description": "Abilita o disabilita l'autenticazione basata su password"
} }
}, },
"buttons": { "buttons": {
@@ -1137,7 +1142,8 @@
}, },
"errors": { "errors": {
"loadFailed": "Errore durante il caricamento delle impostazioni", "loadFailed": "Errore durante il caricamento delle impostazioni",
"updateFailed": "Errore durante l'aggiornamento delle impostazioni" "updateFailed": "Errore durante l'aggiornamento delle impostazioni",
"passwordAuthRequiresProvider": "Impossibile disabilitare l'autenticazione password senza avere almeno un provider di autenticazione attivo"
}, },
"messages": { "messages": {
"noChanges": "Nessuna modifica da salvare", "noChanges": "Nessuna modifica da salvare",

View File

@@ -313,7 +313,8 @@
"title": "パスワードをお忘れですか?", "title": "パスワードをお忘れですか?",
"description": "メールアドレスを入力すると、パスワードリセットの指示を送信します。", "description": "メールアドレスを入力すると、パスワードリセットの指示を送信します。",
"resetInstructions": "パスワードリセットの指示がメールに送信されました", "resetInstructions": "パスワードリセットの指示がメールに送信されました",
"pageTitle": "パスワードをお忘れですか?" "pageTitle": "パスワードをお忘れですか?",
"passwordAuthDisabled": "パスワード認証が無効になっています。管理者に連絡するか、外部認証プロバイダーを使用してください。"
}, },
"generateShareLink": { "generateShareLink": {
"generateTitle": "共有リンクを生成", "generateTitle": "共有リンクを生成",
@@ -1128,6 +1129,10 @@
"smtpTrustSelfSigned": { "smtpTrustSelfSigned": {
"title": "自己署名証明書を信頼", "title": "自己署名証明書を信頼",
"description": "自己署名SSL/TLS証明書を信頼するように設定します開発環境で便利" "description": "自己署名SSL/TLS証明書を信頼するように設定します開発環境で便利"
},
"passwordAuthEnabled": {
"title": "パスワード認証",
"description": "パスワード認証を有効または無効にする"
} }
}, },
"buttons": { "buttons": {
@@ -1137,7 +1142,8 @@
}, },
"errors": { "errors": {
"loadFailed": "設定の読み込みに失敗しました", "loadFailed": "設定の読み込みに失敗しました",
"updateFailed": "設定の更新に失敗しました" "updateFailed": "設定の更新に失敗しました",
"passwordAuthRequiresProvider": "少なくとも1つのアクティブな認証プロバイダーがない場合、パスワード認証を無効にできません"
}, },
"messages": { "messages": {
"noChanges": "保存する変更はありません", "noChanges": "保存する変更はありません",

View File

@@ -313,7 +313,8 @@
"title": "비밀번호를 잊으셨나요?", "title": "비밀번호를 잊으셨나요?",
"description": "이메일 주소를 입력하면 비밀번호 재설정 지침을 보내드립니다.", "description": "이메일 주소를 입력하면 비밀번호 재설정 지침을 보내드립니다.",
"resetInstructions": "비밀번호 재설정 지침이 이메일로 전송되었습니다", "resetInstructions": "비밀번호 재설정 지침이 이메일로 전송되었습니다",
"pageTitle": "비밀번호를 잊으셨나요?" "pageTitle": "비밀번호를 잊으셨나요?",
"passwordAuthDisabled": "비밀번호 인증이 비활성화되어 있습니다. 관리자에게 문의하거나 외부 인증 공급자를 사용하세요."
}, },
"generateShareLink": { "generateShareLink": {
"generateTitle": "공유 링크 생성", "generateTitle": "공유 링크 생성",
@@ -1128,6 +1129,10 @@
"smtpTrustSelfSigned": { "smtpTrustSelfSigned": {
"title": "자체 서명된 인증서 신뢰", "title": "자체 서명된 인증서 신뢰",
"description": "자체 서명된 SSL/TLS 인증서를 신뢰하려면 활성화하세요 (개발 환경에서 유용)" "description": "자체 서명된 SSL/TLS 인증서를 신뢰하려면 활성화하세요 (개발 환경에서 유용)"
},
"passwordAuthEnabled": {
"title": "비밀번호 인증",
"description": "비밀번호 기반 인증 활성화 또는 비활성화"
} }
}, },
"buttons": { "buttons": {
@@ -1137,7 +1142,8 @@
}, },
"errors": { "errors": {
"loadFailed": "설정을 불러오는데 실패했습니다", "loadFailed": "설정을 불러오는데 실패했습니다",
"updateFailed": "설정 업데이트에 실패했습니다" "updateFailed": "설정 업데이트에 실패했습니다",
"passwordAuthRequiresProvider": "최소 하나의 활성 인증 제공자가 없으면 비밀번호 인증을 비활성화할 수 없습니다"
}, },
"messages": { "messages": {
"noChanges": "저장할 변경 사항이 없습니다", "noChanges": "저장할 변경 사항이 없습니다",

View File

@@ -313,7 +313,8 @@
"title": "Wachtwoord Vergeten", "title": "Wachtwoord Vergeten",
"description": "Voer je e-mailadres in en we sturen je instructies om je wachtwoord te resetten.", "description": "Voer je e-mailadres in en we sturen je instructies om je wachtwoord te resetten.",
"resetInstructions": "Reset instructies verzonden naar je e-mail", "resetInstructions": "Reset instructies verzonden naar je e-mail",
"pageTitle": "Wachtwoord Vergeten" "pageTitle": "Wachtwoord Vergeten",
"passwordAuthDisabled": "Wachtwoordauthenticatie is uitgeschakeld. Neem contact op met uw beheerder of gebruik een externe authenticatieprovider."
}, },
"generateShareLink": { "generateShareLink": {
"generateTitle": "Deel-link genereren", "generateTitle": "Deel-link genereren",
@@ -1128,6 +1129,10 @@
"smtpTrustSelfSigned": { "smtpTrustSelfSigned": {
"title": "Vertrouw Zelf-Ondertekende Certificaten", "title": "Vertrouw Zelf-Ondertekende Certificaten",
"description": "Schakel dit in om zelf-ondertekende SSL/TLS certificaten te vertrouwen (handig voor ontwikkelomgevingen)" "description": "Schakel dit in om zelf-ondertekende SSL/TLS certificaten te vertrouwen (handig voor ontwikkelomgevingen)"
},
"passwordAuthEnabled": {
"title": "Wachtwoord Authenticatie",
"description": "Wachtwoord-gebaseerde authenticatie inschakelen of uitschakelen"
} }
}, },
"buttons": { "buttons": {
@@ -1137,7 +1142,8 @@
}, },
"errors": { "errors": {
"loadFailed": "Fout bij het laden van instellingen", "loadFailed": "Fout bij het laden van instellingen",
"updateFailed": "Fout bij het bijwerken van instellingen" "updateFailed": "Fout bij het bijwerken van instellingen",
"passwordAuthRequiresProvider": "Wachtwoordauthenticatie kan niet worden uitgeschakeld zonder ten minste één actieve authenticatieprovider"
}, },
"messages": { "messages": {
"noChanges": "Geen wijzigingen om op te slaan", "noChanges": "Geen wijzigingen om op te slaan",

View File

@@ -313,7 +313,8 @@
"title": "Zapomniałeś hasła?", "title": "Zapomniałeś hasła?",
"description": "Wprowadź swój adres e-mail, a wyślemy Ci instrukcje resetowania hasła", "description": "Wprowadź swój adres e-mail, a wyślemy Ci instrukcje resetowania hasła",
"resetInstructions": "Instrukcje resetowania wysłane na Twój adres e-mail", "resetInstructions": "Instrukcje resetowania wysłane na Twój adres e-mail",
"pageTitle": "Zapomniałeś hasła?" "pageTitle": "Zapomniałeś hasła?",
"passwordAuthDisabled": "Uwierzytelnianie hasłem jest wyłączone. Skontaktuj się z administratorem lub użyj zewnętrznego dostawcy uwierzytelniania."
}, },
"generateShareLink": { "generateShareLink": {
"generateTitle": "Generuj link do udostępniania", "generateTitle": "Generuj link do udostępniania",
@@ -1128,6 +1129,10 @@
"smtpTrustSelfSigned": { "smtpTrustSelfSigned": {
"title": "Zaufaj certyfikatom samopodpisanym", "title": "Zaufaj certyfikatom samopodpisanym",
"description": "Włącz tę opcję, aby zaufać samopodpisanym certyfikatom SSL/TLS (przydatne w środowiskach deweloperskich)" "description": "Włącz tę opcję, aby zaufać samopodpisanym certyfikatom SSL/TLS (przydatne w środowiskach deweloperskich)"
},
"passwordAuthEnabled": {
"title": "Uwierzytelnianie hasłem",
"description": "Włącz lub wyłącz uwierzytelnianie oparte na haśle"
} }
}, },
"buttons": { "buttons": {
@@ -1137,7 +1142,8 @@
}, },
"errors": { "errors": {
"loadFailed": "Nie udało się załadować ustawień", "loadFailed": "Nie udało się załadować ustawień",
"updateFailed": "Nie udało się zaktualizować ustawień" "updateFailed": "Nie udało się zaktualizować ustawień",
"passwordAuthRequiresProvider": "Uwierzytelnianie oparte na haśle nie może być wyłączone, jeśli nie ma co najmniej jednego aktywnego dostawcy uwierzytelniania"
}, },
"messages": { "messages": {
"noChanges": "Brak zmian do zapisania", "noChanges": "Brak zmian do zapisania",

View File

@@ -313,7 +313,8 @@
"title": "Esqueceu a Senha", "title": "Esqueceu a Senha",
"description": "Digite seu endereço de email e enviaremos instruções para redefinir sua senha.", "description": "Digite seu endereço de email e enviaremos instruções para redefinir sua senha.",
"resetInstructions": "Instruções de redefinição enviadas para seu email", "resetInstructions": "Instruções de redefinição enviadas para seu email",
"pageTitle": "Esqueceu a Senha" "pageTitle": "Esqueceu a Senha",
"passwordAuthDisabled": "A autenticação por senha está desativada. Por favor, contate seu administrador ou use um provedor de autenticação externo."
}, },
"generateShareLink": { "generateShareLink": {
"generateTitle": "Gerar link de compartilhamento", "generateTitle": "Gerar link de compartilhamento",
@@ -1136,6 +1137,10 @@
"smtpTrustSelfSigned": { "smtpTrustSelfSigned": {
"title": "Confiar em Certificados Auto-Assinados", "title": "Confiar em Certificados Auto-Assinados",
"description": "Ative isso para confiar em certificados SSL/TLS auto-assinados (útil para ambientes de desenvolvimento)" "description": "Ative isso para confiar em certificados SSL/TLS auto-assinados (útil para ambientes de desenvolvimento)"
},
"passwordAuthEnabled": {
"title": "Autenticação por Senha",
"description": "Ative ou desative a autenticação baseada em senha"
} }
}, },
"buttons": { "buttons": {
@@ -1145,7 +1150,8 @@
}, },
"errors": { "errors": {
"loadFailed": "Falha ao carregar configurações", "loadFailed": "Falha ao carregar configurações",
"updateFailed": "Falha ao atualizar configurações" "updateFailed": "Falha ao atualizar configurações",
"passwordAuthRequiresProvider": "Não é possível desabilitar a autenticação por senha sem ter pelo menos um provedor de autenticação ativo"
}, },
"messages": { "messages": {
"noChanges": "Nenhuma alteração para salvar", "noChanges": "Nenhuma alteração para salvar",

View File

@@ -313,7 +313,8 @@
"title": "Забыли пароль", "title": "Забыли пароль",
"description": "Введите адрес электронной почты, и мы отправим вам инструкции по сбросу пароля.", "description": "Введите адрес электронной почты, и мы отправим вам инструкции по сбросу пароля.",
"resetInstructions": "Инструкции по сбросу отправлены на вашу электронную почту", "resetInstructions": "Инструкции по сбросу отправлены на вашу электронную почту",
"pageTitle": "Забыли пароль" "pageTitle": "Забыли пароль",
"passwordAuthDisabled": "Парольная аутентификация отключена. Пожалуйста, свяжитесь с администратором или используйте внешний провайдер аутентификации."
}, },
"generateShareLink": { "generateShareLink": {
"generateTitle": "Создать ссылку для обмена", "generateTitle": "Создать ссылку для обмена",
@@ -1128,6 +1129,10 @@
"smtpTrustSelfSigned": { "smtpTrustSelfSigned": {
"title": "Доверять самоподписанным сертификатам", "title": "Доверять самоподписанным сертификатам",
"description": "Включите это для доверия самоподписанным SSL/TLS сертификатам (полезно для сред разработки)" "description": "Включите это для доверия самоподписанным SSL/TLS сертификатам (полезно для сред разработки)"
},
"passwordAuthEnabled": {
"title": "Парольная аутентификация",
"description": "Включить или отключить парольную аутентификацию"
} }
}, },
"buttons": { "buttons": {
@@ -1137,7 +1142,8 @@
}, },
"errors": { "errors": {
"loadFailed": "Ошибка загрузки настроек", "loadFailed": "Ошибка загрузки настроек",
"updateFailed": "Ошибка обновления настроек" "updateFailed": "Ошибка обновления настроек",
"passwordAuthRequiresProvider": "Парольную аутентификацию нельзя отключить, если нет хотя бы одного активного поставщика аутентификации"
}, },
"messages": { "messages": {
"noChanges": "Изменений для сохранения нет", "noChanges": "Изменений для сохранения нет",

View File

@@ -313,7 +313,8 @@
"title": "Şifrenizi mi Unuttunuz?", "title": "Şifrenizi mi Unuttunuz?",
"description": "E-posta adresinizi girin, şifre sıfırlama talimatlarını göndereceğiz.", "description": "E-posta adresinizi girin, şifre sıfırlama talimatlarını göndereceğiz.",
"resetInstructions": "Şifre sıfırlama talimatları e-posta adresinize gönderildi", "resetInstructions": "Şifre sıfırlama talimatları e-posta adresinize gönderildi",
"pageTitle": "Şifrenizi mi Unuttunuz?" "pageTitle": "Şifrenizi mi Unuttunuz?",
"passwordAuthDisabled": "Şifre doğrulama devre dışı. Lütfen yöneticinize başvurun veya dış doğrulama sağlayıcısı kullanın."
}, },
"generateShareLink": { "generateShareLink": {
"generateTitle": "Paylaşım Bağlantısı Oluştur", "generateTitle": "Paylaşım Bağlantısı Oluştur",
@@ -1128,6 +1129,10 @@
"smtpTrustSelfSigned": { "smtpTrustSelfSigned": {
"title": "Kendinden İmzalı Sertifikalara Güven", "title": "Kendinden İmzalı Sertifikalara Güven",
"description": "Kendinden imzalı SSL/TLS sertifikalarına güvenmek için bunu etkinleştirin (geliştirme ortamları için kullanışlıdır)" "description": "Kendinden imzalı SSL/TLS sertifikalarına güvenmek için bunu etkinleştirin (geliştirme ortamları için kullanışlıdır)"
},
"passwordAuthEnabled": {
"title": "Şifre Doğrulama",
"description": "Şifre tabanlı doğrulamayı etkinleştirme veya devre dışı bırakma"
} }
}, },
"buttons": { "buttons": {
@@ -1137,7 +1142,8 @@
}, },
"errors": { "errors": {
"loadFailed": "Ayarlar yüklenemedi", "loadFailed": "Ayarlar yüklenemedi",
"updateFailed": "Ayarlar güncellenemedi" "updateFailed": "Ayarlar güncellenemedi",
"passwordAuthRequiresProvider": "En az bir aktif kimlik doğrulama sağlayıcısı olmadan şifre doğrulaması devre dışı bırakılamaz"
}, },
"messages": { "messages": {
"noChanges": "Kaydedilecek değişiklik yok", "noChanges": "Kaydedilecek değişiklik yok",

View File

@@ -313,7 +313,8 @@
"title": "忘记密码?", "title": "忘记密码?",
"description": "请输入您的电子邮件,我们将发送密码重置指令给您。", "description": "请输入您的电子邮件,我们将发送密码重置指令给您。",
"resetInstructions": "密码重置指令已发送到您的电子邮件", "resetInstructions": "密码重置指令已发送到您的电子邮件",
"pageTitle": "忘记密码?" "pageTitle": "忘记密码?",
"passwordAuthDisabled": "密码认证已禁用。请联系您的管理员或使用外部认证提供商。"
}, },
"generateShareLink": { "generateShareLink": {
"generateTitle": "生成分享链接", "generateTitle": "生成分享链接",
@@ -1128,6 +1129,10 @@
"smtpTrustSelfSigned": { "smtpTrustSelfSigned": {
"title": "信任自签名证书", "title": "信任自签名证书",
"description": "启用此选项以信任自签名SSL/TLS证书对开发环境有用" "description": "启用此选项以信任自签名SSL/TLS证书对开发环境有用"
},
"passwordAuthEnabled": {
"title": "密码认证",
"description": "启用或禁用基于密码的认证"
} }
}, },
"buttons": { "buttons": {
@@ -1137,7 +1142,8 @@
}, },
"errors": { "errors": {
"loadFailed": "加载设置失败", "loadFailed": "加载设置失败",
"updateFailed": "更新设置失败" "updateFailed": "更新设置失败",
"passwordAuthRequiresProvider": "没有至少一个活动认证提供者时,无法禁用密码认证"
}, },
"messages": { "messages": {
"noChanges": "没有需要保存的更改", "noChanges": "没有需要保存的更改",

View File

@@ -0,0 +1,32 @@
import { NextResponse } from "next/server";
const API_BASE_URL = process.env.API_BASE_URL || "http://localhost:3333";
export async function GET() {
try {
const url = `${API_BASE_URL}/auth/config`;
const apiRes = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
redirect: "manual",
});
const resBody = await apiRes.text();
const res = new NextResponse(resBody, {
status: apiRes.status,
statusText: apiRes.statusText,
});
apiRes.headers.forEach((value, key) => {
res.headers.set(key, value);
});
return res;
} catch (error) {
console.error("Error proxying auth config request:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}

View File

@@ -1,5 +1,6 @@
"use client"; "use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import axios from "axios"; import axios from "axios";
@@ -8,7 +9,7 @@ import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { requestPasswordReset } from "@/http/endpoints"; import { getAuthConfig, requestPasswordReset } from "@/http/endpoints";
export type ForgotPasswordFormData = { export type ForgotPasswordFormData = {
email: string; email: string;
@@ -17,16 +18,39 @@ export type ForgotPasswordFormData = {
export function useForgotPassword() { export function useForgotPassword() {
const t = useTranslations(); const t = useTranslations();
const router = useRouter(); const router = useRouter();
const [passwordAuthEnabled, setPasswordAuthEnabled] = useState(true);
const [authConfigLoading, setAuthConfigLoading] = useState(true);
const forgotPasswordSchema = z.object({ const forgotPasswordSchema = z.object({
email: z.string().email(t("validation.invalidEmail")), email: z.string().email(t("validation.invalidEmail")),
}); });
useEffect(() => {
const fetchAuthConfig = async () => {
try {
const response = await getAuthConfig();
setPasswordAuthEnabled((response as any).data.passwordAuthEnabled);
} catch (error) {
console.error("Failed to fetch auth config:", error);
setPasswordAuthEnabled(true);
} finally {
setAuthConfigLoading(false);
}
};
fetchAuthConfig();
}, []);
const form = useForm<ForgotPasswordFormData>({ const form = useForm<ForgotPasswordFormData>({
resolver: zodResolver(forgotPasswordSchema), resolver: zodResolver(forgotPasswordSchema),
}); });
const onSubmit = async (data: ForgotPasswordFormData) => { const onSubmit = async (data: ForgotPasswordFormData) => {
if (!passwordAuthEnabled) {
toast.error(t("errors.passwordAuthDisabled"));
return;
}
try { try {
await requestPasswordReset({ await requestPasswordReset({
email: data.email, email: data.email,
@@ -46,5 +70,7 @@ export function useForgotPassword() {
return { return {
form, form,
onSubmit, onSubmit,
passwordAuthEnabled,
authConfigLoading,
}; };
} }

View File

@@ -1,6 +1,8 @@
"use client"; "use client";
import Link from "next/link";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useTranslations } from "next-intl";
import { DefaultFooter } from "@/components/ui/default-footer"; import { DefaultFooter } from "@/components/ui/default-footer";
import { StaticBackgroundLights } from "../login/components/static-background-lights"; import { StaticBackgroundLights } from "../login/components/static-background-lights";
@@ -10,6 +12,7 @@ import { useForgotPassword } from "./hooks/use-forgot-password";
export default function ForgotPasswordPage() { export default function ForgotPasswordPage() {
const forgotPassword = useForgotPassword(); const forgotPassword = useForgotPassword();
const t = useTranslations("ForgotPassword");
return ( return (
<div className="relative flex min-h-screen flex-col"> <div className="relative flex min-h-screen flex-col">
@@ -22,7 +25,24 @@ export default function ForgotPasswordPage() {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
> >
<ForgotPasswordHeader /> <ForgotPasswordHeader />
{forgotPassword.authConfigLoading ? (
<div className="flex justify-center items-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
) : !forgotPassword.passwordAuthEnabled ? (
<div className="mt-8 space-y-4">
<div className="text-center p-4 bg-muted/50 rounded-lg">
<p className="text-muted-foreground">{t("forgotPassword.passwordAuthDisabled")}</p>
</div>
<div className="text-center">
<Link className="text-muted-foreground hover:text-primary text-sm" href="/login">
{t("forgotPassword.backToLogin")}
</Link>
</div>
</div>
) : (
<ForgotPasswordForm form={forgotPassword.form} onSubmit={forgotPassword.onSubmit} /> <ForgotPasswordForm form={forgotPassword.form} onSubmit={forgotPassword.onSubmit} />
)}
</motion.div> </motion.div>
</div> </div>
</div> </div>

View File

@@ -1,3 +1,4 @@
import { useEffect, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
@@ -6,6 +7,7 @@ import { useForm } from "react-hook-form";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { getEnabledProviders } from "@/http/endpoints";
import { createLoginSchema, type LoginFormValues } from "../schemas/schema"; import { createLoginSchema, type LoginFormValues } from "../schemas/schema";
import { MultiProviderButtons } from "./multi-provider-buttons"; import { MultiProviderButtons } from "./multi-provider-buttons";
import { PasswordVisibilityToggle } from "./password-visibility-toggle"; import { PasswordVisibilityToggle } from "./password-visibility-toggle";
@@ -15,21 +17,50 @@ interface LoginFormProps {
isVisible: boolean; isVisible: boolean;
onToggleVisibility: () => void; onToggleVisibility: () => void;
onSubmit: (data: LoginFormValues) => Promise<void>; onSubmit: (data: LoginFormValues) => Promise<void>;
passwordAuthEnabled: boolean;
authConfigLoading: boolean;
} }
export function LoginForm({ error, isVisible, onToggleVisibility, onSubmit }: LoginFormProps) { export function LoginForm({
error,
isVisible,
onToggleVisibility,
onSubmit,
passwordAuthEnabled,
authConfigLoading,
}: LoginFormProps) {
const t = useTranslations(); const t = useTranslations();
const loginSchema = createLoginSchema(t); const [hasEnabledProviders, setHasEnabledProviders] = useState(false);
const [providersLoading, setProvidersLoading] = useState(true);
const loginSchema = createLoginSchema(t, passwordAuthEnabled);
const form = useForm<LoginFormValues>({ const form = useForm<LoginFormValues>({
resolver: zodResolver(loginSchema), resolver: zodResolver(loginSchema),
defaultValues: { defaultValues: {
emailOrUsername: "", emailOrUsername: "",
password: "", password: passwordAuthEnabled ? "" : undefined,
}, },
}); });
const isSubmitting = form.formState.isSubmitting; const isSubmitting = form.formState.isSubmitting;
useEffect(() => {
const checkProviders = async () => {
try {
const response = await getEnabledProviders();
const data = response.data as any;
setHasEnabledProviders(data.success && data.data && data.data.length > 0);
} catch (error) {
console.error("Error checking providers:", error);
setHasEnabledProviders(false);
} finally {
setProvidersLoading(false);
}
};
checkProviders();
}, []);
const renderErrorMessage = () => const renderErrorMessage = () =>
error && ( error && (
<p className="text-destructive text-sm text-center bg-destructive/10 p-2 rounded-md"> <p className="text-destructive text-sm text-center bg-destructive/10 p-2 rounded-md">
@@ -84,13 +115,41 @@ export function LoginForm({ error, isVisible, onToggleVisibility, onSubmit }: Lo
/> />
); );
if (authConfigLoading || providersLoading) {
return (
<div className="flex justify-center items-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
);
}
if (!passwordAuthEnabled && hasEnabledProviders) {
return (
<>
{renderErrorMessage()}
<MultiProviderButtons showSeparator={false} />
</>
);
}
if (!passwordAuthEnabled && !hasEnabledProviders) {
return (
<>
{renderErrorMessage()}
<div className="text-center py-8">
<p className="text-destructive text-sm">{t("login.noAuthMethodsAvailable")}</p>
</div>
</>
);
}
return ( return (
<> <>
{renderErrorMessage()} {renderErrorMessage()}
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4"> <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4">
{renderEmailOrUsernameField()} {renderEmailOrUsernameField()}
{renderPasswordField()} {passwordAuthEnabled && renderPasswordField()}
<Button className="w-full mt-4 cursor-pointer" variant="default" size="lg" type="submit"> <Button className="w-full mt-4 cursor-pointer" variant="default" size="lg" type="submit">
{isSubmitting ? t("login.signingIn") : t("login.signIn")} {isSubmitting ? t("login.signingIn") : t("login.signIn")}
</Button> </Button>
@@ -99,11 +158,13 @@ export function LoginForm({ error, isVisible, onToggleVisibility, onSubmit }: Lo
<MultiProviderButtons /> <MultiProviderButtons />
{passwordAuthEnabled && (
<div className="flex w-full items-center justify-center px-1 mt-2"> <div className="flex w-full items-center justify-center px-1 mt-2">
<Link className="text-muted-foreground hover:text-primary text-sm" href="/forgot-password"> <Link className="text-muted-foreground hover:text-primary text-sm" href="/forgot-password">
{t("login.forgotPassword")} {t("login.forgotPassword")}
</Link> </Link>
</div> </div>
)}
</> </>
); );
} }

View File

@@ -9,7 +9,11 @@ import { useAppInfo } from "@/contexts/app-info-context";
import { getEnabledProviders } from "@/http/endpoints"; import { getEnabledProviders } from "@/http/endpoints";
import type { EnabledAuthProvider } from "@/http/endpoints/auth/types"; import type { EnabledAuthProvider } from "@/http/endpoints/auth/types";
export function MultiProviderButtons() { interface MultiProviderButtonsProps {
showSeparator?: boolean;
}
export function MultiProviderButtons({ showSeparator = true }: MultiProviderButtonsProps) {
const [providers, setProviders] = useState<EnabledAuthProvider[]>([]); const [providers, setProviders] = useState<EnabledAuthProvider[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const { firstAccess } = useAppInfo(); const { firstAccess } = useAppInfo();
@@ -67,6 +71,7 @@ export function MultiProviderButtons() {
return ( return (
<div className="space-y-3"> <div className="space-y-3">
{showSeparator && (
<div className="relative"> <div className="relative">
<div className="absolute inset-0 flex items-center"> <div className="absolute inset-0 flex items-center">
<span className="w-full border-t" /> <span className="w-full border-t" />
@@ -75,6 +80,7 @@ export function MultiProviderButtons() {
<span className="bg-background px-2 text-muted-foreground">Or continue with</span> <span className="bg-background px-2 text-muted-foreground">Or continue with</span>
</div> </div>
</div> </div>
)}
<div className="space-y-2"> <div className="space-y-2">
{providers.map((provider) => ( {providers.map((provider) => (

View File

@@ -8,7 +8,7 @@ import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { useAuth } from "@/contexts/auth-context"; import { useAuth } from "@/contexts/auth-context";
import { getCurrentUser, login } from "@/http/endpoints"; import { getAuthConfig, getCurrentUser, login } from "@/http/endpoints";
import { completeTwoFactorLogin } from "@/http/endpoints/auth/two-factor"; import { completeTwoFactorLogin } from "@/http/endpoints/auth/two-factor";
import type { LoginResponse } from "@/http/endpoints/auth/two-factor/types"; import type { LoginResponse } from "@/http/endpoints/auth/two-factor/types";
import { LoginFormValues } from "../schemas/schema"; import { LoginFormValues } from "../schemas/schema";
@@ -31,6 +31,8 @@ export function useLogin() {
const [twoFactorUserId, setTwoFactorUserId] = useState<string | null>(null); const [twoFactorUserId, setTwoFactorUserId] = useState<string | null>(null);
const [twoFactorCode, setTwoFactorCode] = useState(""); const [twoFactorCode, setTwoFactorCode] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [passwordAuthEnabled, setPasswordAuthEnabled] = useState(true);
const [authConfigLoading, setAuthConfigLoading] = useState(true);
useEffect(() => { useEffect(() => {
const errorParam = searchParams.get("error"); const errorParam = searchParams.get("error");
@@ -60,6 +62,22 @@ export function useLogin() {
} }
}, [searchParams, t]); }, [searchParams, t]);
useEffect(() => {
const fetchAuthConfig = async () => {
try {
const response = await getAuthConfig();
setPasswordAuthEnabled((response as any).data.passwordAuthEnabled);
} catch (error) {
console.error("Failed to fetch auth config:", error);
setPasswordAuthEnabled(true);
} finally {
setAuthConfigLoading(false);
}
};
fetchAuthConfig();
}, []);
const toggleVisibility = () => setIsVisible(!isVisible); const toggleVisibility = () => setIsVisible(!isVisible);
const onSubmit = async (data: LoginFormValues) => { const onSubmit = async (data: LoginFormValues) => {
@@ -67,7 +85,12 @@ export function useLogin() {
setIsSubmitting(true); setIsSubmitting(true);
try { try {
const response = await login(data); if (!passwordAuthEnabled) {
setError(t("errors.passwordAuthDisabled"));
return;
}
const response = await login(data as any);
const loginData = response.data as LoginResponse; const loginData = response.data as LoginResponse;
if (loginData.requiresTwoFactor && loginData.userId) { if (loginData.requiresTwoFactor && loginData.userId) {
@@ -77,7 +100,6 @@ export function useLogin() {
} }
if (loginData.user) { if (loginData.user) {
// Após login bem-sucedido, buscar dados completos do usuário incluindo a imagem
try { try {
const userResponse = await getCurrentUser(); const userResponse = await getCurrentUser();
if (userResponse?.data?.user) { if (userResponse?.data?.user) {
@@ -92,7 +114,6 @@ export function useLogin() {
console.warn("Failed to fetch complete user data, using login data:", userErr); console.warn("Failed to fetch complete user data, using login data:", userErr);
} }
// Fallback para dados do login se falhar ao buscar dados completos
const { isAdmin, ...userData } = loginData.user; const { isAdmin, ...userData } = loginData.user;
setUser({ ...userData, image: null }); setUser({ ...userData, image: null });
setIsAdmin(isAdmin); setIsAdmin(isAdmin);
@@ -129,7 +150,6 @@ export function useLogin() {
rememberDevice: rememberDevice, rememberDevice: rememberDevice,
}); });
// Após two-factor login bem-sucedido, buscar dados completos do usuário incluindo a imagem
try { try {
const userResponse = await getCurrentUser(); const userResponse = await getCurrentUser();
if (userResponse?.data?.user) { if (userResponse?.data?.user) {
@@ -144,7 +164,6 @@ export function useLogin() {
console.warn("Failed to fetch complete user data after 2FA, using response data:", userErr); console.warn("Failed to fetch complete user data after 2FA, using response data:", userErr);
} }
// Fallback para dados da resposta se falhar ao buscar dados completos
const { isAdmin, ...userData } = response.data.user; const { isAdmin, ...userData } = response.data.user;
setUser({ ...userData, image: userData.image ?? null }); setUser({ ...userData, image: userData.image ?? null });
setIsAdmin(isAdmin); setIsAdmin(isAdmin);
@@ -172,5 +191,7 @@ export function useLogin() {
setTwoFactorCode, setTwoFactorCode,
onTwoFactorSubmit, onTwoFactorSubmit,
isSubmitting, isSubmitting,
passwordAuthEnabled,
authConfigLoading,
}; };
} }

View File

@@ -53,6 +53,8 @@ export default function LoginPage() {
isVisible={login.isVisible} isVisible={login.isVisible}
onSubmit={login.onSubmit} onSubmit={login.onSubmit}
onToggleVisibility={login.toggleVisibility} onToggleVisibility={login.toggleVisibility}
passwordAuthEnabled={login.passwordAuthEnabled}
authConfigLoading={login.authConfigLoading}
/> />
)} )}
</motion.div> </motion.div>

View File

@@ -3,10 +3,10 @@ import * as z from "zod";
type TFunction = ReturnType<typeof useTranslations>; type TFunction = ReturnType<typeof useTranslations>;
export const createLoginSchema = (t: TFunction) => export const createLoginSchema = (t: TFunction, passwordAuthEnabled: boolean = true) =>
z.object({ z.object({
emailOrUsername: z.string().min(1, t("validation.emailOrUsernameRequired")), emailOrUsername: z.string().min(1, t("validation.emailOrUsernameRequired")),
password: z.string().min(1, t("validation.passwordRequired")), password: passwordAuthEnabled ? z.string().min(1, t("validation.passwordRequired")) : z.string().optional(),
}); });
export type LoginFormValues = z.infer<ReturnType<typeof createLoginSchema>>; export type LoginFormValues = z.infer<ReturnType<typeof createLoginSchema>>;

View File

@@ -172,9 +172,20 @@ export function useSettings() {
} }
await refreshAppInfo(); await refreshAppInfo();
} catch { } catch (error: any) {
const errorMessage = error?.response?.data?.error || error?.message || "";
if (
errorMessage.includes("autenticação por senha") ||
errorMessage.includes("provedor de autenticação ativo") ||
errorMessage.includes("password authentication") ||
errorMessage.includes("authentication provider")
) {
toast.error(t("settings.errors.passwordAuthRequiresProvider"));
} else {
toast.error(t("settings.errors.updateFailed")); toast.error(t("settings.errors.updateFailed"));
} }
}
}; };
const toggleCollapse = (group: string) => { const toggleCollapse = (group: string) => {

View File

@@ -99,3 +99,9 @@ export const updateProvidersOrder = <TData = UpdateProvidersOrderResult>(
): Promise<TData> => { ): Promise<TData> => {
return apiInstance.put(`/api/auth/providers/order`, updateProvidersOrderBody, options); return apiInstance.put(`/api/auth/providers/order`, updateProvidersOrderBody, options);
}; };
export const getAuthConfig = <TData = { passwordAuthEnabled: boolean }>(
options?: AxiosRequestConfig
): Promise<TData> => {
return apiInstance.get(`/api/auth/config`, options);
};