diff --git a/.gitignore b/.gitignore index 323e9c9..cc6d117 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ apps/server/dist/* #DEFAULT .env +.steering data/ node_modules/ \ No newline at end of file diff --git a/apps/docs/package.json b/apps/docs/package.json index 373a62c..f91268a 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -1,6 +1,6 @@ { "name": "palmr-docs", - "version": "3.1.3-beta", + "version": "3.1.4-beta", "description": "Docs for Palmr", "private": true, "author": "Daniel Luiz Alves ", diff --git a/apps/server/package.json b/apps/server/package.json index 4d25c8f..37ee0c3 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,6 +1,6 @@ { "name": "palmr-api", - "version": "3.1.3-beta", + "version": "3.1.4-beta", "description": "API for Palmr", "private": true, "author": "Daniel Luiz Alves ", diff --git a/apps/server/prisma/seed.js b/apps/server/prisma/seed.js index b0320a3..b567d00 100644 --- a/apps/server/prisma/seed.js +++ b/apps/server/prisma/seed.js @@ -147,6 +147,12 @@ const defaultConfigs = [ type: "boolean", group: "auth-providers", }, + { + key: "passwordAuthEnabled", + value: "true", + type: "boolean", + group: "security", + }, { key: "serverUrl", value: "http://localhost:3333", diff --git a/apps/server/src/modules/app/controller.ts b/apps/server/src/modules/app/controller.ts index 0db59f7..131c6aa 100644 --- a/apps/server/src/modules/app/controller.ts +++ b/apps/server/src/modules/app/controller.ts @@ -18,6 +18,15 @@ export class AppController { } } + async getSystemInfo(request: FastifyRequest, reply: FastifyReply) { + try { + const systemInfo = await this.appService.getSystemInfo(); + return reply.send(systemInfo); + } catch (error: any) { + return reply.status(400).send({ error: error.message }); + } + } + async getAllConfigs(request: FastifyRequest, reply: FastifyReply) { try { const configs = await this.appService.getAllConfigs(); diff --git a/apps/server/src/modules/app/routes.ts b/apps/server/src/modules/app/routes.ts index 3708d1b..9cb7474 100644 --- a/apps/server/src/modules/app/routes.ts +++ b/apps/server/src/modules/app/routes.ts @@ -53,6 +53,26 @@ export async function appRoutes(app: FastifyInstance) { appController.getAppInfo.bind(appController) ); + app.get( + "/app/system-info", + { + schema: { + tags: ["App"], + operationId: "getSystemInfo", + summary: "Get system information", + description: "Get system information including storage provider", + response: { + 200: z.object({ + storageProvider: z.enum(["s3", "filesystem"]).describe("The active storage provider"), + s3Enabled: z.boolean().describe("Whether S3 storage is enabled"), + }), + 400: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + appController.getSystemInfo.bind(appController) + ); + app.patch( "/app/configs/:key", { diff --git a/apps/server/src/modules/app/service.ts b/apps/server/src/modules/app/service.ts index 8e053cc..66ce7e8 100644 --- a/apps/server/src/modules/app/service.ts +++ b/apps/server/src/modules/app/service.ts @@ -1,3 +1,4 @@ +import { isS3Enabled } from "../../config/storage.config"; import { prisma } from "../../shared/prisma"; import { ConfigService } from "../config/service"; @@ -20,6 +21,13 @@ export class AppService { }; } + async getSystemInfo() { + return { + storageProvider: isS3Enabled ? "s3" : "filesystem", + s3Enabled: isS3Enabled, + }; + } + async getAllConfigs() { return prisma.appConfig.findMany({ where: { @@ -38,6 +46,17 @@ export class AppService { 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({ where: { key }, }); @@ -56,6 +75,15 @@ export class AppService { if (updates.some((update) => update.key === "jwtSecret")) { 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 existingConfigs = await prisma.appConfig.findMany({ diff --git a/apps/server/src/modules/auth-providers/controller.ts b/apps/server/src/modules/auth-providers/controller.ts index 2a461e3..55142fb 100644 --- a/apps/server/src/modules/auth-providers/controller.ts +++ b/apps/server/src/modules/auth-providers/controller.ts @@ -1,5 +1,6 @@ import { FastifyReply, FastifyRequest } from "fastify"; +import { ConfigService } from "../config/service"; import { UpdateAuthProviderSchema } from "./dto"; import { AuthProvidersService } from "./service"; import { @@ -39,9 +40,11 @@ const ERROR_MESSAGES = { export class AuthProvidersController { private authProvidersService: AuthProvidersService; + private configService: ConfigService; constructor() { this.authProvidersService = new AuthProvidersService(); + this.configService = new ConfigService(); } private buildRequestContext(request: FastifyRequest): RequestContext { @@ -223,13 +226,24 @@ export class AuthProvidersController { try { const { id } = request.params; - const data = request.body; + const data = request.body as any; const existingProvider = await this.authProvidersService.getProviderById(id); if (!existingProvider) { 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); if (isOfficial) { @@ -300,6 +314,17 @@ export class AuthProvidersController { 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); return this.sendSuccessResponse(reply, undefined, "Provider deleted successfully"); } catch (error) { diff --git a/apps/server/src/modules/auth/controller.ts b/apps/server/src/modules/auth/controller.ts index e254fdd..20a6148 100644 --- a/apps/server/src/modules/auth/controller.ts +++ b/apps/server/src/modules/auth/controller.ts @@ -1,6 +1,7 @@ import { FastifyReply, FastifyRequest } from "fastify"; import { env } from "../../env"; +import { ConfigService } from "../config/service"; import { CompleteTwoFactorLoginSchema, createResetPasswordSchema, @@ -11,6 +12,7 @@ import { AuthService } from "./service"; export class AuthController { private authService = new AuthService(); + private configService = new ConfigService(); private getClientInfo(request: FastifyRequest) { const realIP = request.headers["x-real-ip"] as string; @@ -169,4 +171,15 @@ export class AuthController { 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 }); + } + } } diff --git a/apps/server/src/modules/auth/routes.ts b/apps/server/src/modules/auth/routes.ts index 9f89622..af5902b 100644 --- a/apps/server/src/modules/auth/routes.ts +++ b/apps/server/src/modules/auth/routes.ts @@ -280,4 +280,23 @@ export async function authRoutes(app: FastifyInstance) { }, 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) + ); } diff --git a/apps/server/src/modules/auth/service.ts b/apps/server/src/modules/auth/service.ts index 06cd859..3fcf48e 100644 --- a/apps/server/src/modules/auth/service.ts +++ b/apps/server/src/modules/auth/service.ts @@ -18,6 +18,11 @@ export class AuthService { private trustedDeviceService = new TrustedDeviceService(); 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); if (!user) { throw new Error("Invalid credentials"); @@ -146,6 +151,11 @@ export class AuthService { } 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); if (!user) { return; @@ -171,6 +181,11 @@ export class AuthService { } 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({ where: { token, diff --git a/apps/server/src/modules/config/service.ts b/apps/server/src/modules/config/service.ts index b6cd20d..fc91fe2 100644 --- a/apps/server/src/modules/config/service.ts +++ b/apps/server/src/modules/config/service.ts @@ -13,6 +13,26 @@ export class ConfigService { return config.value; } + async setValue(key: string, value: string): Promise { + await prisma.appConfig.update({ + where: { key }, + data: { value }, + }); + } + + async validatePasswordAuthDisable(): Promise { + const enabledProviders = await prisma.authProvider.findMany({ + where: { enabled: true }, + }); + + return enabledProviders.length > 0; + } + + async validateAllProvidersDisable(): Promise { + const passwordAuthEnabled = await this.getValue("passwordAuthEnabled"); + return passwordAuthEnabled === "true"; + } + async getGroupConfigs(group: string) { const configs = await prisma.appConfig.findMany({ where: { group }, diff --git a/apps/server/src/modules/email/service.ts b/apps/server/src/modules/email/service.ts index bd82cca..4fea5f6 100644 --- a/apps/server/src/modules/email/service.ts +++ b/apps/server/src/modules/email/service.ts @@ -167,7 +167,7 @@ export class EmailService { }); } - async sendShareNotification(to: string, shareLink: string, shareName?: string) { + async sendShareNotification(to: string, shareLink: string, shareName?: string, senderName?: string) { const transporter = await this.createTransporter(); if (!transporter) { throw new Error("SMTP is not enabled"); @@ -178,19 +178,151 @@ export class EmailService { const appName = await this.configService.getValue("appName"); const shareTitle = shareName || "Files"; + const sender = senderName || "Someone"; await transporter.sendMail({ from: `"${fromName}" <${fromEmail}>`, to, subject: `${appName} - ${shareTitle} shared with you`, html: ` -

${appName} - Shared Files

-

Someone has shared "${shareTitle}" with you.

-

Click the link below to access the shared files:

- - Access Shared Files - -

Note: This share may have an expiration date or view limit.

+ + + + + + ${appName} - Shared Files + + +
+ +
+

${appName}

+

Shared Files

+
+ + +
+
+

Files Shared With You

+

+ ${sender} has shared "${shareTitle}" with you. +

+
+ + + + + +
+

+ Important: This share may have an expiration date or view limit. Access it as soon as possible to ensure availability. +

+
+
+ + +
+

+ This email was sent by ${appName} +

+

+ If you didn't expect this email, you can safely ignore it. +

+

+ Powered by Kyantech Solutions +

+
+
+ + + `, + }); + } + + async sendReverseShareBatchFileNotification( + recipientEmail: string, + reverseShareName: string, + fileCount: number, + fileList: string, + uploaderName: string + ) { + const transporter = await this.createTransporter(); + if (!transporter) { + throw new Error("SMTP is not enabled"); + } + + const fromName = await this.configService.getValue("smtpFromName"); + const fromEmail = await this.configService.getValue("smtpFromEmail"); + const appName = await this.configService.getValue("appName"); + + await transporter.sendMail({ + from: `"${fromName}" <${fromEmail}>`, + to: recipientEmail, + subject: `${appName} - ${fileCount} file${fileCount > 1 ? "s" : ""} uploaded to "${reverseShareName}"`, + html: ` + + + + + + ${appName} - File Upload Notification + + +
+ +
+

${appName}

+

File Upload Notification

+
+ + +
+
+

New File Uploaded

+

+ ${uploaderName} has uploaded ${fileCount} file${fileCount > 1 ? "s" : ""} to your reverse share "${reverseShareName}". +

+
+ + +
+

Files (${fileCount}):

+
    + ${fileList + .split(", ") + .map((file) => `
  • ${file}
  • `) + .join("")} +
+
+ + +
+

+ You can now access and manage these files through your dashboard. +

+
+ +
+ + +
+

+ This email was sent by ${appName} +

+

+ If you didn't expect this email, you can safely ignore it. +

+

+ Powered by Kyantech Solutions +

+
+
+ + `, }); } diff --git a/apps/server/src/modules/filesystem/controller.ts b/apps/server/src/modules/filesystem/controller.ts index 07be54d..b3d04ee 100644 --- a/apps/server/src/modules/filesystem/controller.ts +++ b/apps/server/src/modules/filesystem/controller.ts @@ -226,8 +226,8 @@ export class FilesystemController { if (isLargeFile) { await this.downloadLargeFile(reply, provider, filePath); } else { - const buffer = await provider.downloadFile(tokenData.objectName); - reply.send(buffer); + const stream = provider.createDecryptedReadStream(tokenData.objectName); + reply.send(stream); } } @@ -255,8 +255,14 @@ export class FilesystemController { start: number, end: number ) { - const buffer = await provider.downloadFile(objectName); - const chunk = buffer.slice(start, end + 1); - reply.send(chunk); + const filePath = provider.getFilePath(objectName); + const readStream = fs.createReadStream(filePath, { start, end }); + const decryptStream = provider.createDecryptStream(); + + try { + await pipeline(readStream, decryptStream, reply.raw); + } catch (error) { + throw error; + } } } diff --git a/apps/server/src/modules/reverse-share/service.ts b/apps/server/src/modules/reverse-share/service.ts index fb1a64b..dfaa398 100644 --- a/apps/server/src/modules/reverse-share/service.ts +++ b/apps/server/src/modules/reverse-share/service.ts @@ -1,7 +1,9 @@ import { PrismaClient } from "@prisma/client"; import { env } from "../../env"; +import { EmailService } from "../email/service"; import { FileService } from "../file/service"; +import { UserService } from "../user/service"; import { CreateReverseShareInput, ReverseShareResponseSchema, @@ -41,6 +43,19 @@ const prisma = new PrismaClient(); export class ReverseShareService { private reverseShareRepository = new ReverseShareRepository(); private fileService = new FileService(); + private emailService = new EmailService(); + private userService = new UserService(); + + private uploadSessions = new Map< + string, + { + reverseShareId: string; + uploaderName: string; + uploaderEmail?: string; + files: string[]; + timeout: NodeJS.Timeout; + } + >(); async createReverseShare(data: CreateReverseShareInput, creatorId: string) { const reverseShare = await this.reverseShareRepository.create(data, creatorId); @@ -295,6 +310,8 @@ export class ReverseShareService { size: BigInt(fileData.size), }); + this.addFileToUploadSession(reverseShare, fileData); + return this.formatFileResponse(file); } @@ -345,6 +362,8 @@ export class ReverseShareService { size: BigInt(fileData.size), }); + this.addFileToUploadSession(reverseShare, fileData); + return this.formatFileResponse(file); } @@ -637,6 +656,55 @@ export class ReverseShareService { }; } + private generateSessionKey(reverseShareId: string, uploaderIdentifier: string): string { + return `${reverseShareId}-${uploaderIdentifier}`; + } + + private async sendBatchFileUploadNotification(reverseShare: any, uploaderName: string, fileNames: string[]) { + try { + const creator = await this.userService.getUserById(reverseShare.creatorId); + const reverseShareName = reverseShare.name || "Unnamed Reverse Share"; + const fileCount = fileNames.length; + const fileList = fileNames.join(", "); + + await this.emailService.sendReverseShareBatchFileNotification( + creator.email, + reverseShareName, + fileCount, + fileList, + uploaderName + ); + } catch (error) { + console.error("Failed to send reverse share batch file notification:", error); + } + } + + private addFileToUploadSession(reverseShare: any, fileData: UploadToReverseShareInput) { + const uploaderIdentifier = fileData.uploaderEmail || fileData.uploaderName || "anonymous"; + const sessionKey = this.generateSessionKey(reverseShare.id, uploaderIdentifier); + const uploaderName = fileData.uploaderName || "Someone"; + + const existingSession = this.uploadSessions.get(sessionKey); + if (existingSession) { + clearTimeout(existingSession.timeout); + existingSession.files.push(fileData.name); + } else { + this.uploadSessions.set(sessionKey, { + reverseShareId: reverseShare.id, + uploaderName, + uploaderEmail: fileData.uploaderEmail, + files: [fileData.name], + timeout: null as any, + }); + } + + const session = this.uploadSessions.get(sessionKey)!; + session.timeout = setTimeout(async () => { + await this.sendBatchFileUploadNotification(reverseShare, session.uploaderName, session.files); + this.uploadSessions.delete(sessionKey); + }, 5000); + } + private formatReverseShareResponse(reverseShare: ReverseShareData) { const result = { id: reverseShare.id, diff --git a/apps/server/src/modules/share/service.ts b/apps/server/src/modules/share/service.ts index ab9a505..7ab0de2 100644 --- a/apps/server/src/modules/share/service.ts +++ b/apps/server/src/modules/share/service.ts @@ -2,6 +2,7 @@ import bcrypt from "bcryptjs"; import { prisma } from "../../shared/prisma"; import { EmailService } from "../email/service"; +import { UserService } from "../user/service"; import { CreateShareInput, ShareResponseSchema, UpdateShareInput } from "./dto"; import { IShareRepository, PrismaShareRepository } from "./repository"; @@ -9,6 +10,7 @@ export class ShareService { constructor(private readonly shareRepository: IShareRepository = new PrismaShareRepository()) {} private emailService = new EmailService(); + private userService = new UserService(); private formatShareResponse(share: any) { return { @@ -339,11 +341,26 @@ export class ShareService { throw new Error("No recipients found for this share"); } + // Get sender information + let senderName = "Someone"; + try { + const sender = await this.userService.getUserById(userId); + if (sender.firstName && sender.lastName) { + senderName = `${sender.firstName} ${sender.lastName}`; + } else if (sender.firstName) { + senderName = sender.firstName; + } else if (sender.username) { + senderName = sender.username; + } + } catch (error) { + console.error(`Failed to get sender information for user ${userId}:`, error); + } + const notifiedRecipients: string[] = []; for (const recipient of share.recipients) { try { - await this.emailService.sendShareNotification(recipient.email, shareLink, share.name || undefined); + await this.emailService.sendShareNotification(recipient.email, shareLink, share.name || undefined, senderName); notifiedRecipients.push(recipient.email); } catch (error) { console.error(`Failed to send email to ${recipient.email}:`, error); diff --git a/apps/server/src/providers/filesystem-storage.provider.ts b/apps/server/src/providers/filesystem-storage.provider.ts index ba28df3..b744a8b 100644 --- a/apps/server/src/providers/filesystem-storage.provider.ts +++ b/apps/server/src/providers/filesystem-storage.provider.ts @@ -32,6 +32,14 @@ export class FilesystemStorageProvider implements StorageProvider { return FilesystemStorageProvider.instance; } + public createDecryptedReadStream(objectName: string): NodeJS.ReadableStream { + const filePath = this.getFilePath(objectName); + const fileStream = fsSync.createReadStream(filePath); + const decryptStream = this.createDecryptStream(); + + return fileStream.pipe(decryptStream); + } + private async ensureUploadsDir(): Promise { try { await fs.access(this.uploadsDir); diff --git a/apps/web/messages/ar-SA.json b/apps/web/messages/ar-SA.json index 185b341..29246ea 100644 --- a/apps/web/messages/ar-SA.json +++ b/apps/web/messages/ar-SA.json @@ -313,7 +313,8 @@ "title": "نسيت كلمة المرور", "description": "أدخل بريدك الإلكتروني وسنرسل لك تعليمات إعادة تعيين كلمة المرور.", "resetInstructions": "تم إرسال تعليمات إعادة التعيين إلى بريدك الإلكتروني", - "pageTitle": "نسيت كلمة المرور" + "pageTitle": "نسيت كلمة المرور", + "passwordAuthDisabled": "تم تعطيل المصادقة بكلمة المرور. يرجى الاتصال بالمسؤول أو استخدام مزود مصادقة خارجي." }, "generateShareLink": { "generateTitle": "إنشاء رابط المشاركة", @@ -629,7 +630,7 @@ }, "status": { "active": "نشط", - "inactive": "غير نشط", + "inactive": "غير نشط", "expired": "منتهي الصلاحية", "protected": "محمي", "public": "عام" @@ -1130,6 +1131,10 @@ "smtpTrustSelfSigned": { "title": "الوثوق بالشهادات الموقعة ذاتياً", "description": "قم بتمكين هذا للوثوق بشهادات SSL/TLS الموقعة ذاتياً (مفيد لبيئات التطوير)" + }, + "passwordAuthEnabled": { + "title": "المصادقة بالكلمة السرية", + "description": "تمكين أو تعطيل المصادقة بالكلمة السرية" } }, "buttons": { @@ -1139,7 +1144,8 @@ }, "errors": { "loadFailed": "فشل في تحميل الإعدادات", - "updateFailed": "فشل في تحديث الإعدادات" + "updateFailed": "فشل في تحديث الإعدادات", + "passwordAuthRequiresProvider": "لا يمكن تعطيل المصادقة بالكلمة السرية دون وجود على الأقل موفرين مصادقة مفعلين" }, "messages": { "noChanges": "لا توجد تغييرات للحفظ", @@ -1744,4 +1750,4 @@ "description": "امسح رمز QR هذا للوصول إلى الرابط.", "download": "تحميل رمز QR" } -} +} \ No newline at end of file diff --git a/apps/web/messages/de-DE.json b/apps/web/messages/de-DE.json index 1c63c3b..3853202 100644 --- a/apps/web/messages/de-DE.json +++ b/apps/web/messages/de-DE.json @@ -313,7 +313,8 @@ "title": "Passwort vergessen", "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", - "pageTitle": "Passwort vergessen" + "pageTitle": "Passwort vergessen", + "passwordAuthDisabled": "Passwortauthentifizierung ist deaktiviert. Bitte kontaktieren Sie Ihren Administrator oder verwenden Sie einen externen Authentifizierungsanbieter." }, "generateShareLink": { "generateTitle": "Freigabe-Link generieren", @@ -628,7 +629,7 @@ "viewQrCode": "QR-Code anzeigen" }, "status": { - "active": "Aktiv", + "active": "Aktiv", "inactive": "Inaktiv", "expired": "Abgelaufen", "protected": "Geschützt", @@ -636,7 +637,7 @@ }, "actions": { "copyLink": "Link kopieren", - "editAlias": "Alias bearbeiten", + "editAlias": "Alias bearbeiten", "createAlias": "Alias erstellen", "viewDetails": "Details anzeigen", "edit": "Bearbeiten", @@ -1128,6 +1129,10 @@ "tls": "STARTTLS (Port 587)", "none": "Keine (Unsicher)" } + }, + "passwordAuthEnabled": { + "title": "Passwort-Authentifizierung", + "description": "Passwort-basierte Authentifizierung aktivieren oder deaktivieren" } }, "buttons": { @@ -1137,7 +1142,8 @@ }, "errors": { "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": { "noChanges": "Keine Änderungen zum Speichern", @@ -1245,7 +1251,7 @@ "editSecurity": "Sicherheit bearbeiten", "editExpiration": "Ablauf bearbeiten", "clickToEnlargeQrCode": "Klicken Sie zum Vergrößern des QR-Codes", - "downloadQrCode": "QR-Code herunterladen", + "downloadQrCode": "QR-Code herunterladen", "qrCode": "QR-Code" }, "shareExpiration": { @@ -1742,4 +1748,4 @@ "description": "Scannen Sie diesen QR-Code, um auf den Link zuzugreifen.", "download": "QR-Code herunterladen" } -} +} \ No newline at end of file diff --git a/apps/web/messages/en-US.json b/apps/web/messages/en-US.json index bfeddae..f1ebe72 100644 --- a/apps/web/messages/en-US.json +++ b/apps/web/messages/en-US.json @@ -313,7 +313,8 @@ "title": "Forgot Password", "description": "Enter your email address and we'll send you instructions to reset your password", "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": { "generateTitle": "Generate Share Link", @@ -1131,6 +1132,10 @@ "serverUrl": { "title": "Server URL", "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": { @@ -1140,7 +1145,8 @@ }, "errors": { "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": { "noChanges": "No changes to save", diff --git a/apps/web/messages/es-ES.json b/apps/web/messages/es-ES.json index 79e93ca..a9f9d42 100644 --- a/apps/web/messages/es-ES.json +++ b/apps/web/messages/es-ES.json @@ -313,7 +313,8 @@ "title": "Recuperar 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", - "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": { "generateTitle": "Generar enlace de compartir", @@ -1128,6 +1129,10 @@ "tls": "STARTTLS (Puerto 587)", "none": "Ninguno (Inseguro)" } + }, + "passwordAuthEnabled": { + "title": "Autenticación por Contraseña", + "description": "Habilitar o deshabilitar la autenticación basada en contraseña" } }, "buttons": { @@ -1137,7 +1142,8 @@ }, "errors": { "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": { "noChanges": "No hay cambios para guardar", @@ -1742,4 +1748,4 @@ "description": "Escanea este código QR para acceder al enlace.", "download": "Descargar Código QR" } -} +} \ No newline at end of file diff --git a/apps/web/messages/fr-FR.json b/apps/web/messages/fr-FR.json index 15f018e..b6093d8 100644 --- a/apps/web/messages/fr-FR.json +++ b/apps/web/messages/fr-FR.json @@ -313,7 +313,8 @@ "title": "Mot de Passe Oublié", "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", - "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": { "generateTitle": "Générer un lien de partage", @@ -1131,6 +1132,10 @@ "smtpTrustSelfSigned": { "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)" + }, + "passwordAuthEnabled": { + "title": "Authentification par Mot de Passe", + "description": "Activer ou désactiver l'authentification basée sur mot de passe" } }, "buttons": { @@ -1140,7 +1145,8 @@ }, "errors": { "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": { "noChanges": "Aucun changement à enregistrer", diff --git a/apps/web/messages/hi-IN.json b/apps/web/messages/hi-IN.json index 7422a9d..6445b36 100644 --- a/apps/web/messages/hi-IN.json +++ b/apps/web/messages/hi-IN.json @@ -313,7 +313,8 @@ "title": "पासवर्ड भूल गए", "description": "अपना ईमेल पता दर्ज करें और हम आपको पासवर्ड रीसेट करने के निर्देश भेजेंगे।", "resetInstructions": "रीसेट निर्देश आपके ईमेल पर भेज दिए गए हैं", - "pageTitle": "पासवर्ड भूल गए" + "pageTitle": "पासवर्ड भूल गए", + "passwordAuthDisabled": "पासवर्ड ऑथेंटिकेशन अक्टिवेटेड है। कृपया अपने एडमिन से संपर्क करें या एक बाहरी ऑथेंटिकेशन प्रोवाइडर का उपयोग करें।" }, "generateShareLink": { "generateTitle": "साझाकरण लिंक उत्पन्न करें", @@ -1128,6 +1129,10 @@ "smtpTrustSelfSigned": { "title": "स्व-हस्ताक्षरित प्रमाणपत्रों पर विश्वास करें", "description": "स्व-हस्ताक्षरित SSL/TLS प्रमाणपत्रों पर विश्वास करने के लिए इसे सक्षम करें (विकास वातावरण के लिए उपयोगी)" + }, + "passwordAuthEnabled": { + "title": "पासवर्ड प्रमाणीकरण", + "description": "पासवर्ड आधारित प्रमाणीकरण सक्षम या अक्षम करें" } }, "buttons": { @@ -1137,7 +1142,8 @@ }, "errors": { "loadFailed": "सेटिंग्स लोड करने में विफल", - "updateFailed": "सेटिंग्स अपडेट करने में विफल" + "updateFailed": "सेटिंग्स अपडेट करने में विफल", + "passwordAuthRequiresProvider": "कम से कम एक सक्रिय प्रमाणीकरण प्रदाता के बिना पासवर्ड प्रमाणीकरण अक्षम नहीं किया जा सकता" }, "messages": { "noChanges": "सहेजने के लिए कोई परिवर्तन नहीं", diff --git a/apps/web/messages/it-IT.json b/apps/web/messages/it-IT.json index c600733..6bb71da 100644 --- a/apps/web/messages/it-IT.json +++ b/apps/web/messages/it-IT.json @@ -313,7 +313,8 @@ "title": "Parola d'accesso Dimenticata", "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", - "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": { "generateTitle": "Genera link di condivisione", @@ -1128,6 +1129,10 @@ "smtpTrustSelfSigned": { "title": "Accetta Certificati Auto-Firmati", "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": { @@ -1137,7 +1142,8 @@ }, "errors": { "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": { "noChanges": "Nessuna modifica da salvare", diff --git a/apps/web/messages/ja-JP.json b/apps/web/messages/ja-JP.json index 6460648..f96a23e 100644 --- a/apps/web/messages/ja-JP.json +++ b/apps/web/messages/ja-JP.json @@ -313,7 +313,8 @@ "title": "パスワードをお忘れですか?", "description": "メールアドレスを入力すると、パスワードリセットの指示を送信します。", "resetInstructions": "パスワードリセットの指示がメールに送信されました", - "pageTitle": "パスワードをお忘れですか?" + "pageTitle": "パスワードをお忘れですか?", + "passwordAuthDisabled": "パスワード認証が無効になっています。管理者に連絡するか、外部認証プロバイダーを使用してください。" }, "generateShareLink": { "generateTitle": "共有リンクを生成", @@ -1128,6 +1129,10 @@ "smtpTrustSelfSigned": { "title": "自己署名証明書を信頼", "description": "自己署名SSL/TLS証明書を信頼するように設定します(開発環境で便利)" + }, + "passwordAuthEnabled": { + "title": "パスワード認証", + "description": "パスワード認証を有効または無効にする" } }, "buttons": { @@ -1137,7 +1142,8 @@ }, "errors": { "loadFailed": "設定の読み込みに失敗しました", - "updateFailed": "設定の更新に失敗しました" + "updateFailed": "設定の更新に失敗しました", + "passwordAuthRequiresProvider": "少なくとも1つのアクティブな認証プロバイダーがない場合、パスワード認証を無効にできません" }, "messages": { "noChanges": "保存する変更はありません", diff --git a/apps/web/messages/ko-KR.json b/apps/web/messages/ko-KR.json index 4ed9d04..083e39d 100644 --- a/apps/web/messages/ko-KR.json +++ b/apps/web/messages/ko-KR.json @@ -313,7 +313,8 @@ "title": "비밀번호를 잊으셨나요?", "description": "이메일 주소를 입력하면 비밀번호 재설정 지침을 보내드립니다.", "resetInstructions": "비밀번호 재설정 지침이 이메일로 전송되었습니다", - "pageTitle": "비밀번호를 잊으셨나요?" + "pageTitle": "비밀번호를 잊으셨나요?", + "passwordAuthDisabled": "비밀번호 인증이 비활성화되어 있습니다. 관리자에게 문의하거나 외부 인증 공급자를 사용하세요." }, "generateShareLink": { "generateTitle": "공유 링크 생성", @@ -1128,6 +1129,10 @@ "smtpTrustSelfSigned": { "title": "자체 서명된 인증서 신뢰", "description": "자체 서명된 SSL/TLS 인증서를 신뢰하려면 활성화하세요 (개발 환경에서 유용)" + }, + "passwordAuthEnabled": { + "title": "비밀번호 인증", + "description": "비밀번호 기반 인증 활성화 또는 비활성화" } }, "buttons": { @@ -1137,7 +1142,8 @@ }, "errors": { "loadFailed": "설정을 불러오는데 실패했습니다", - "updateFailed": "설정 업데이트에 실패했습니다" + "updateFailed": "설정 업데이트에 실패했습니다", + "passwordAuthRequiresProvider": "최소 하나의 활성 인증 제공자가 없으면 비밀번호 인증을 비활성화할 수 없습니다" }, "messages": { "noChanges": "저장할 변경 사항이 없습니다", diff --git a/apps/web/messages/nl-NL.json b/apps/web/messages/nl-NL.json index 1e0cbae..95beffa 100644 --- a/apps/web/messages/nl-NL.json +++ b/apps/web/messages/nl-NL.json @@ -313,7 +313,8 @@ "title": "Wachtwoord Vergeten", "description": "Voer je e-mailadres in en we sturen je instructies om je wachtwoord te resetten.", "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": { "generateTitle": "Deel-link genereren", @@ -1128,6 +1129,10 @@ "smtpTrustSelfSigned": { "title": "Vertrouw Zelf-Ondertekende Certificaten", "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": { @@ -1137,7 +1142,8 @@ }, "errors": { "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": { "noChanges": "Geen wijzigingen om op te slaan", diff --git a/apps/web/messages/pl-PL.json b/apps/web/messages/pl-PL.json index 6607ff3..97f06a9 100644 --- a/apps/web/messages/pl-PL.json +++ b/apps/web/messages/pl-PL.json @@ -313,7 +313,8 @@ "title": "Zapomniałeś 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", - "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": { "generateTitle": "Generuj link do udostępniania", @@ -1128,6 +1129,10 @@ "smtpTrustSelfSigned": { "title": "Zaufaj certyfikatom samopodpisanym", "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": { @@ -1137,7 +1142,8 @@ }, "errors": { "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": { "noChanges": "Brak zmian do zapisania", diff --git a/apps/web/messages/pt-BR.json b/apps/web/messages/pt-BR.json index fa70132..cd0686c 100644 --- a/apps/web/messages/pt-BR.json +++ b/apps/web/messages/pt-BR.json @@ -313,7 +313,8 @@ "title": "Esqueceu a 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", - "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": { "generateTitle": "Gerar link de compartilhamento", @@ -1136,6 +1137,10 @@ "smtpTrustSelfSigned": { "title": "Confiar em Certificados Auto-Assinados", "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": { @@ -1145,7 +1150,8 @@ }, "errors": { "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": { "noChanges": "Nenhuma alteração para salvar", diff --git a/apps/web/messages/ru-RU.json b/apps/web/messages/ru-RU.json index 614f2bf..2e13bb0 100644 --- a/apps/web/messages/ru-RU.json +++ b/apps/web/messages/ru-RU.json @@ -313,7 +313,8 @@ "title": "Забыли пароль", "description": "Введите адрес электронной почты, и мы отправим вам инструкции по сбросу пароля.", "resetInstructions": "Инструкции по сбросу отправлены на вашу электронную почту", - "pageTitle": "Забыли пароль" + "pageTitle": "Забыли пароль", + "passwordAuthDisabled": "Парольная аутентификация отключена. Пожалуйста, свяжитесь с администратором или используйте внешний провайдер аутентификации." }, "generateShareLink": { "generateTitle": "Создать ссылку для обмена", @@ -1128,6 +1129,10 @@ "smtpTrustSelfSigned": { "title": "Доверять самоподписанным сертификатам", "description": "Включите это для доверия самоподписанным SSL/TLS сертификатам (полезно для сред разработки)" + }, + "passwordAuthEnabled": { + "title": "Парольная аутентификация", + "description": "Включить или отключить парольную аутентификацию" } }, "buttons": { @@ -1137,7 +1142,8 @@ }, "errors": { "loadFailed": "Ошибка загрузки настроек", - "updateFailed": "Ошибка обновления настроек" + "updateFailed": "Ошибка обновления настроек", + "passwordAuthRequiresProvider": "Парольную аутентификацию нельзя отключить, если нет хотя бы одного активного поставщика аутентификации" }, "messages": { "noChanges": "Изменений для сохранения нет", diff --git a/apps/web/messages/tr-TR.json b/apps/web/messages/tr-TR.json index 59b1b32..b0ff37e 100644 --- a/apps/web/messages/tr-TR.json +++ b/apps/web/messages/tr-TR.json @@ -313,7 +313,8 @@ "title": "Şifrenizi mi Unuttunuz?", "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", - "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": { "generateTitle": "Paylaşım Bağlantısı Oluştur", @@ -1128,6 +1129,10 @@ "smtpTrustSelfSigned": { "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)" + }, + "passwordAuthEnabled": { + "title": "Şifre Doğrulama", + "description": "Şifre tabanlı doğrulamayı etkinleştirme veya devre dışı bırakma" } }, "buttons": { @@ -1137,7 +1142,8 @@ }, "errors": { "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": { "noChanges": "Kaydedilecek değişiklik yok", diff --git a/apps/web/messages/zh-CN.json b/apps/web/messages/zh-CN.json index 485e7ad..e3f8012 100644 --- a/apps/web/messages/zh-CN.json +++ b/apps/web/messages/zh-CN.json @@ -313,7 +313,8 @@ "title": "忘记密码?", "description": "请输入您的电子邮件,我们将发送密码重置指令给您。", "resetInstructions": "密码重置指令已发送到您的电子邮件", - "pageTitle": "忘记密码?" + "pageTitle": "忘记密码?", + "passwordAuthDisabled": "密码认证已禁用。请联系您的管理员或使用外部认证提供商。" }, "generateShareLink": { "generateTitle": "生成分享链接", @@ -1128,6 +1129,10 @@ "smtpTrustSelfSigned": { "title": "信任自签名证书", "description": "启用此选项以信任自签名SSL/TLS证书(对开发环境有用)" + }, + "passwordAuthEnabled": { + "title": "密码认证", + "description": "启用或禁用基于密码的认证" } }, "buttons": { @@ -1137,7 +1142,8 @@ }, "errors": { "loadFailed": "加载设置失败", - "updateFailed": "更新设置失败" + "updateFailed": "更新设置失败", + "passwordAuthRequiresProvider": "没有至少一个活动认证提供者时,无法禁用密码认证" }, "messages": { "noChanges": "没有需要保存的更改", diff --git a/apps/web/package.json b/apps/web/package.json index b270004..f46e21e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "palmr-web", - "version": "3.1.3-beta", + "version": "3.1.4-beta", "description": "Frontend for Palmr", "private": true, "author": "Daniel Luiz Alves ", diff --git a/apps/web/src/app/(shares)/r/[alias]/components/file-upload-section.tsx b/apps/web/src/app/(shares)/r/[alias]/components/file-upload-section.tsx index 387a389..a40acad 100644 --- a/apps/web/src/app/(shares)/r/[alias]/components/file-upload-section.tsx +++ b/apps/web/src/app/(shares)/r/[alias]/components/file-upload-section.tsx @@ -14,6 +14,7 @@ import { Label } from "@/components/ui/label"; import { Progress } from "@/components/ui/progress"; import { Textarea } from "@/components/ui/textarea"; import { getPresignedUrlForUploadByAlias, registerFileUploadByAlias } from "@/http/endpoints"; +import { getSystemInfo } from "@/http/endpoints/app"; import { ChunkedUploader } from "@/utils/chunked-upload"; import { formatFileSize } from "@/utils/format-file-size"; import { FILE_STATUS, UPLOAD_CONFIG, UPLOAD_PROGRESS } from "../constants"; @@ -25,9 +26,24 @@ export function FileUploadSection({ reverseShare, password, alias, onUploadSucce const [uploaderEmail, setUploaderEmail] = useState(""); const [description, setDescription] = useState(""); const [isUploading, setIsUploading] = useState(false); + const [isS3Enabled, setIsS3Enabled] = useState(null); const t = useTranslations(); + useEffect(() => { + const fetchSystemInfo = async () => { + try { + const response = await getSystemInfo(); + setIsS3Enabled(response.data.s3Enabled); + } catch (error) { + console.warn("Failed to fetch system info, defaulting to filesystem mode:", error); + setIsS3Enabled(false); + } + }; + + fetchSystemInfo(); + }, []); + const validateFileSize = useCallback( (file: File): string | null => { if (!reverseShare.maxFileSize) return null; @@ -139,7 +155,7 @@ export function FileUploadSection({ reverseShare, password, alias, onUploadSucce presignedUrl: string, onProgress?: (progress: number) => void ): Promise => { - const shouldUseChunked = ChunkedUploader.shouldUseChunkedUpload(file.size); + const shouldUseChunked = ChunkedUploader.shouldUseChunkedUpload(file.size, isS3Enabled ?? undefined); if (shouldUseChunked) { const chunkSize = ChunkedUploader.calculateOptimalChunkSize(file.size); @@ -148,6 +164,7 @@ export function FileUploadSection({ reverseShare, password, alias, onUploadSucce file, url: presignedUrl, chunkSize, + isS3Enabled: isS3Enabled ?? undefined, onProgress, }); diff --git a/apps/web/src/app/api/(proxy)/app/system-info/route.ts b/apps/web/src/app/api/(proxy)/app/system-info/route.ts new file mode 100644 index 0000000..40b8917 --- /dev/null +++ b/apps/web/src/app/api/(proxy)/app/system-info/route.ts @@ -0,0 +1,31 @@ +import { NextRequest, NextResponse } from "next/server"; + +const API_BASE_URL = process.env.API_BASE_URL || "http://localhost:3333"; + +export async function GET(req: NextRequest) { + const cookieHeader = req.headers.get("cookie"); + const url = `${API_BASE_URL}/app/system-info`; + + const apiRes = await fetch(url, { + method: "GET", + headers: { + cookie: cookieHeader || "", + }, + redirect: "manual", + }); + + const resBody = await apiRes.text(); + const res = new NextResponse(resBody, { + status: apiRes.status, + headers: { + "Content-Type": "application/json", + }, + }); + + const setCookie = apiRes.headers.getSetCookie?.() || []; + if (setCookie.length > 0) { + res.headers.set("Set-Cookie", setCookie.join(",")); + } + + return res; +} diff --git a/apps/web/src/app/api/(proxy)/auth/config/route.ts b/apps/web/src/app/api/(proxy)/auth/config/route.ts new file mode 100644 index 0000000..f07561b --- /dev/null +++ b/apps/web/src/app/api/(proxy)/auth/config/route.ts @@ -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 }); + } +} diff --git a/apps/web/src/app/forgot-password/hooks/use-forgot-password.ts b/apps/web/src/app/forgot-password/hooks/use-forgot-password.ts index e6974f8..c61e12e 100644 --- a/apps/web/src/app/forgot-password/hooks/use-forgot-password.ts +++ b/apps/web/src/app/forgot-password/hooks/use-forgot-password.ts @@ -1,5 +1,6 @@ "use client"; +import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { zodResolver } from "@hookform/resolvers/zod"; import axios from "axios"; @@ -8,7 +9,7 @@ import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; -import { requestPasswordReset } from "@/http/endpoints"; +import { getAuthConfig, requestPasswordReset } from "@/http/endpoints"; export type ForgotPasswordFormData = { email: string; @@ -17,16 +18,39 @@ export type ForgotPasswordFormData = { export function useForgotPassword() { const t = useTranslations(); const router = useRouter(); + const [passwordAuthEnabled, setPasswordAuthEnabled] = useState(true); + const [authConfigLoading, setAuthConfigLoading] = useState(true); const forgotPasswordSchema = z.object({ 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({ resolver: zodResolver(forgotPasswordSchema), }); const onSubmit = async (data: ForgotPasswordFormData) => { + if (!passwordAuthEnabled) { + toast.error(t("errors.passwordAuthDisabled")); + return; + } + try { await requestPasswordReset({ email: data.email, @@ -46,5 +70,7 @@ export function useForgotPassword() { return { form, onSubmit, + passwordAuthEnabled, + authConfigLoading, }; } diff --git a/apps/web/src/app/forgot-password/page.tsx b/apps/web/src/app/forgot-password/page.tsx index 210c081..de1f989 100644 --- a/apps/web/src/app/forgot-password/page.tsx +++ b/apps/web/src/app/forgot-password/page.tsx @@ -1,6 +1,8 @@ "use client"; +import Link from "next/link"; import { motion } from "framer-motion"; +import { useTranslations } from "next-intl"; import { DefaultFooter } from "@/components/ui/default-footer"; import { StaticBackgroundLights } from "../login/components/static-background-lights"; @@ -10,6 +12,7 @@ import { useForgotPassword } from "./hooks/use-forgot-password"; export default function ForgotPasswordPage() { const forgotPassword = useForgotPassword(); + const t = useTranslations("ForgotPassword"); return (
@@ -22,7 +25,24 @@ export default function ForgotPasswordPage() { initial={{ opacity: 0, y: 20 }} > - + {forgotPassword.authConfigLoading ? ( +
+
+
+ ) : !forgotPassword.passwordAuthEnabled ? ( +
+
+

{t("forgotPassword.passwordAuthDisabled")}

+
+
+ + {t("forgotPassword.backToLogin")} + +
+
+ ) : ( + + )}
diff --git a/apps/web/src/app/login/components/login-form.tsx b/apps/web/src/app/login/components/login-form.tsx index ed5e575..7fee174 100644 --- a/apps/web/src/app/login/components/login-form.tsx +++ b/apps/web/src/app/login/components/login-form.tsx @@ -1,3 +1,4 @@ +import { useEffect, useState } from "react"; import Link from "next/link"; import { zodResolver } from "@hookform/resolvers/zod"; import { useTranslations } from "next-intl"; @@ -6,6 +7,7 @@ import { useForm } from "react-hook-form"; import { Button } from "@/components/ui/button"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; +import { getEnabledProviders } from "@/http/endpoints"; import { createLoginSchema, type LoginFormValues } from "../schemas/schema"; import { MultiProviderButtons } from "./multi-provider-buttons"; import { PasswordVisibilityToggle } from "./password-visibility-toggle"; @@ -15,21 +17,50 @@ interface LoginFormProps { isVisible: boolean; onToggleVisibility: () => void; onSubmit: (data: LoginFormValues) => Promise; + 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 loginSchema = createLoginSchema(t); + const [hasEnabledProviders, setHasEnabledProviders] = useState(false); + const [providersLoading, setProvidersLoading] = useState(true); + + const loginSchema = createLoginSchema(t, passwordAuthEnabled); const form = useForm({ resolver: zodResolver(loginSchema), defaultValues: { emailOrUsername: "", - password: "", + password: passwordAuthEnabled ? "" : undefined, }, }); 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 = () => error && (

@@ -84,13 +115,41 @@ export function LoginForm({ error, isVisible, onToggleVisibility, onSubmit }: Lo /> ); + if (authConfigLoading || providersLoading) { + return ( +

+
+
+ ); + } + + if (!passwordAuthEnabled && hasEnabledProviders) { + return ( + <> + {renderErrorMessage()} + + + ); + } + + if (!passwordAuthEnabled && !hasEnabledProviders) { + return ( + <> + {renderErrorMessage()} +
+

{t("login.noAuthMethodsAvailable")}

+
+ + ); + } + return ( <> {renderErrorMessage()}
{renderEmailOrUsernameField()} - {renderPasswordField()} + {passwordAuthEnabled && renderPasswordField()} @@ -99,11 +158,13 @@ export function LoginForm({ error, isVisible, onToggleVisibility, onSubmit }: Lo -
- - {t("login.forgotPassword")} - -
+ {passwordAuthEnabled && ( +
+ + {t("login.forgotPassword")} + +
+ )} ); } diff --git a/apps/web/src/app/login/components/multi-provider-buttons.tsx b/apps/web/src/app/login/components/multi-provider-buttons.tsx index 7006d19..cd52030 100644 --- a/apps/web/src/app/login/components/multi-provider-buttons.tsx +++ b/apps/web/src/app/login/components/multi-provider-buttons.tsx @@ -9,7 +9,11 @@ import { useAppInfo } from "@/contexts/app-info-context"; import { getEnabledProviders } from "@/http/endpoints"; 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([]); const [loading, setLoading] = useState(true); const { firstAccess } = useAppInfo(); @@ -67,14 +71,16 @@ export function MultiProviderButtons() { return (
-
-
- + {showSeparator && ( +
+
+ +
+
+ Or continue with +
-
- Or continue with -
-
+ )}
{providers.map((provider) => ( diff --git a/apps/web/src/app/login/hooks/use-login.ts b/apps/web/src/app/login/hooks/use-login.ts index 00fea6a..dc45c6b 100644 --- a/apps/web/src/app/login/hooks/use-login.ts +++ b/apps/web/src/app/login/hooks/use-login.ts @@ -8,7 +8,7 @@ import { toast } from "sonner"; import { z } from "zod"; 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 type { LoginResponse } from "@/http/endpoints/auth/two-factor/types"; import { LoginFormValues } from "../schemas/schema"; @@ -31,6 +31,8 @@ export function useLogin() { const [twoFactorUserId, setTwoFactorUserId] = useState(null); const [twoFactorCode, setTwoFactorCode] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); + const [passwordAuthEnabled, setPasswordAuthEnabled] = useState(true); + const [authConfigLoading, setAuthConfigLoading] = useState(true); useEffect(() => { const errorParam = searchParams.get("error"); @@ -60,6 +62,22 @@ export function useLogin() { } }, [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 onSubmit = async (data: LoginFormValues) => { @@ -67,7 +85,12 @@ export function useLogin() { setIsSubmitting(true); 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; if (loginData.requiresTwoFactor && loginData.userId) { @@ -77,7 +100,6 @@ export function useLogin() { } if (loginData.user) { - // Após login bem-sucedido, buscar dados completos do usuário incluindo a imagem try { const userResponse = await getCurrentUser(); if (userResponse?.data?.user) { @@ -92,7 +114,6 @@ export function useLogin() { 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; setUser({ ...userData, image: null }); setIsAdmin(isAdmin); @@ -129,7 +150,6 @@ export function useLogin() { rememberDevice: rememberDevice, }); - // Após two-factor login bem-sucedido, buscar dados completos do usuário incluindo a imagem try { const userResponse = await getCurrentUser(); 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); } - // Fallback para dados da resposta se falhar ao buscar dados completos const { isAdmin, ...userData } = response.data.user; setUser({ ...userData, image: userData.image ?? null }); setIsAdmin(isAdmin); @@ -172,5 +191,7 @@ export function useLogin() { setTwoFactorCode, onTwoFactorSubmit, isSubmitting, + passwordAuthEnabled, + authConfigLoading, }; } diff --git a/apps/web/src/app/login/page.tsx b/apps/web/src/app/login/page.tsx index a124f96..2db0e38 100644 --- a/apps/web/src/app/login/page.tsx +++ b/apps/web/src/app/login/page.tsx @@ -53,6 +53,8 @@ export default function LoginPage() { isVisible={login.isVisible} onSubmit={login.onSubmit} onToggleVisibility={login.toggleVisibility} + passwordAuthEnabled={login.passwordAuthEnabled} + authConfigLoading={login.authConfigLoading} /> )} diff --git a/apps/web/src/app/login/schemas/schema.ts b/apps/web/src/app/login/schemas/schema.ts index b8ff95d..b9c5e2a 100644 --- a/apps/web/src/app/login/schemas/schema.ts +++ b/apps/web/src/app/login/schemas/schema.ts @@ -3,10 +3,10 @@ import * as z from "zod"; type TFunction = ReturnType; -export const createLoginSchema = (t: TFunction) => +export const createLoginSchema = (t: TFunction, passwordAuthEnabled: boolean = true) => z.object({ 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>; diff --git a/apps/web/src/app/settings/hooks/use-settings.ts b/apps/web/src/app/settings/hooks/use-settings.ts index 759792a..62d71ec 100644 --- a/apps/web/src/app/settings/hooks/use-settings.ts +++ b/apps/web/src/app/settings/hooks/use-settings.ts @@ -172,8 +172,19 @@ export function useSettings() { } await refreshAppInfo(); - } catch { - toast.error(t("settings.errors.updateFailed")); + } 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")); + } } }; diff --git a/apps/web/src/components/general/global-drop-zone.tsx b/apps/web/src/components/general/global-drop-zone.tsx index 6cef4e3..b944085 100644 --- a/apps/web/src/components/general/global-drop-zone.tsx +++ b/apps/web/src/components/general/global-drop-zone.tsx @@ -9,6 +9,7 @@ import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Progress } from "@/components/ui/progress"; import { checkFile, getPresignedUrl, registerFile } from "@/http/endpoints"; +import { getSystemInfo } from "@/http/endpoints/app"; import { ChunkedUploader } from "@/utils/chunked-upload"; import { getFileIcon } from "@/utils/file-icons"; import { generateSafeFileName } from "@/utils/file-utils"; @@ -43,6 +44,7 @@ export function GlobalDropZone({ onSuccess, children }: GlobalDropZoneProps) { const [isDragOver, setIsDragOver] = useState(false); const [fileUploads, setFileUploads] = useState([]); const [hasShownSuccessToast, setHasShownSuccessToast] = useState(false); + const [isS3Enabled, setIsS3Enabled] = useState(null); const generateFileId = useCallback(() => { return Date.now().toString() + Math.random().toString(36).substr(2, 9); @@ -124,7 +126,7 @@ export function GlobalDropZone({ onSuccess, children }: GlobalDropZoneProps) { const abortController = new AbortController(); setFileUploads((prev) => prev.map((u) => (u.id === id ? { ...u, abortController } : u))); - const shouldUseChunked = ChunkedUploader.shouldUseChunkedUpload(file.size); + const shouldUseChunked = ChunkedUploader.shouldUseChunkedUpload(file.size, isS3Enabled ?? undefined); if (shouldUseChunked) { const chunkSize = ChunkedUploader.calculateOptimalChunkSize(file.size); @@ -134,6 +136,7 @@ export function GlobalDropZone({ onSuccess, children }: GlobalDropZoneProps) { url, chunkSize, signal: abortController.signal, + isS3Enabled: isS3Enabled ?? undefined, onProgress: (progress) => { setFileUploads((prev) => prev.map((u) => (u.id === id ? { ...u, progress } : u))); }, @@ -196,7 +199,7 @@ export function GlobalDropZone({ onSuccess, children }: GlobalDropZoneProps) { ); } }, - [t] + [t, isS3Enabled] ); const handleDrop = useCallback( @@ -256,6 +259,20 @@ export function GlobalDropZone({ onSuccess, children }: GlobalDropZoneProps) { [uploadFile, t, createFileUpload] ); + useEffect(() => { + const fetchSystemInfo = async () => { + try { + const response = await getSystemInfo(); + setIsS3Enabled(response.data.s3Enabled); + } catch (error) { + console.warn("Failed to fetch system info, defaulting to filesystem mode:", error); + setIsS3Enabled(false); + } + }; + + fetchSystemInfo(); + }, []); + useEffect(() => { document.addEventListener("dragover", handleDragOver); document.addEventListener("dragleave", handleDragLeave); diff --git a/apps/web/src/components/modals/upload-file-modal.tsx b/apps/web/src/components/modals/upload-file-modal.tsx index 1fba594..7c5d901 100644 --- a/apps/web/src/components/modals/upload-file-modal.tsx +++ b/apps/web/src/components/modals/upload-file-modal.tsx @@ -10,6 +10,7 @@ import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Progress } from "@/components/ui/progress"; import { checkFile, getPresignedUrl, registerFile } from "@/http/endpoints"; +import { getSystemInfo } from "@/http/endpoints/app"; import { ChunkedUploader } from "@/utils/chunked-upload"; import { getFileIcon } from "@/utils/file-icons"; import { generateSafeFileName } from "@/utils/file-utils"; @@ -87,8 +88,23 @@ export function UploadFileModal({ isOpen, onClose, onSuccess }: UploadFileModalP const [isDragOver, setIsDragOver] = useState(false); const [showConfirmation, setShowConfirmation] = useState(false); const [hasShownSuccessToast, setHasShownSuccessToast] = useState(false); + const [isS3Enabled, setIsS3Enabled] = useState(null); const fileInputRef = useRef(null); + useEffect(() => { + const fetchSystemInfo = async () => { + try { + const response = await getSystemInfo(); + setIsS3Enabled(response.data.s3Enabled); + } catch (error) { + console.warn("Failed to fetch system info, defaulting to filesystem mode:", error); + setIsS3Enabled(false); + } + }; + + fetchSystemInfo(); + }, []); + useEffect(() => { return () => { fileUploads.forEach((upload) => { @@ -252,7 +268,7 @@ export function UploadFileModal({ isOpen, onClose, onSuccess }: UploadFileModalP const abortController = new AbortController(); setFileUploads((prev) => prev.map((u) => (u.id === id ? { ...u, abortController } : u))); - const shouldUseChunked = ChunkedUploader.shouldUseChunkedUpload(file.size); + const shouldUseChunked = ChunkedUploader.shouldUseChunkedUpload(file.size, isS3Enabled ?? undefined); if (shouldUseChunked) { const chunkSize = ChunkedUploader.calculateOptimalChunkSize(file.size); @@ -262,6 +278,7 @@ export function UploadFileModal({ isOpen, onClose, onSuccess }: UploadFileModalP url, chunkSize, signal: abortController.signal, + isS3Enabled: isS3Enabled ?? undefined, onProgress: (progress) => { setFileUploads((prev) => prev.map((u) => (u.id === id ? { ...u, progress } : u))); }, diff --git a/apps/web/src/http/endpoints/app/index.ts b/apps/web/src/http/endpoints/app/index.ts index 3801432..4025595 100644 --- a/apps/web/src/http/endpoints/app/index.ts +++ b/apps/web/src/http/endpoints/app/index.ts @@ -7,6 +7,7 @@ import type { CheckUploadAllowedResult, GetAppInfoResult, GetDiskSpaceResult, + GetSystemInfoResult, RemoveLogoResult, UploadLogoBody, UploadLogoResult, @@ -20,6 +21,14 @@ export const getAppInfo = (options?: AxiosRequestConfi return apiInstance.get(`/api/app/info`, options); }; +/** + * Get system information including storage provider + * @summary Get system information + */ +export const getSystemInfo = (options?: AxiosRequestConfig): Promise => { + return apiInstance.get(`/api/app/system-info`, options); +}; + /** * Upload a new app logo (admin only) * @summary Upload app logo diff --git a/apps/web/src/http/endpoints/app/types.ts b/apps/web/src/http/endpoints/app/types.ts index f4b6324..ca1c2d2 100644 --- a/apps/web/src/http/endpoints/app/types.ts +++ b/apps/web/src/http/endpoints/app/types.ts @@ -32,6 +32,11 @@ export interface GetAppInfo200 { firstUserAccess: boolean; } +export interface GetSystemInfo200 { + storageProvider: "s3" | "filesystem"; + s3Enabled: boolean; +} + export interface RemoveLogo200 { message: string; } @@ -49,6 +54,7 @@ export interface UploadLogoBody { } export type GetAppInfoResult = AxiosResponse; +export type GetSystemInfoResult = AxiosResponse; export type UploadLogoResult = AxiosResponse; export type RemoveLogoResult = AxiosResponse; export type CheckHealthResult = AxiosResponse; diff --git a/apps/web/src/http/endpoints/auth/index.ts b/apps/web/src/http/endpoints/auth/index.ts index 2b9b9c2..99bba81 100644 --- a/apps/web/src/http/endpoints/auth/index.ts +++ b/apps/web/src/http/endpoints/auth/index.ts @@ -99,3 +99,9 @@ export const updateProvidersOrder = ( ): Promise => { return apiInstance.put(`/api/auth/providers/order`, updateProvidersOrderBody, options); }; + +export const getAuthConfig = ( + options?: AxiosRequestConfig +): Promise => { + return apiInstance.get(`/api/auth/config`, options); +}; diff --git a/apps/web/src/utils/chunked-upload.ts b/apps/web/src/utils/chunked-upload.ts index e259535..a37317a 100644 --- a/apps/web/src/utils/chunked-upload.ts +++ b/apps/web/src/utils/chunked-upload.ts @@ -7,6 +7,7 @@ export interface ChunkedUploadOptions { onProgress?: (progress: number) => void; onChunkComplete?: (chunkIndex: number, totalChunks: number) => void; signal?: AbortSignal; + isS3Enabled?: boolean; } export interface ChunkedUploadResult { @@ -23,7 +24,7 @@ export class ChunkedUploader { static async uploadFile(options: ChunkedUploadOptions): Promise { const { file, url, chunkSize, onProgress, onChunkComplete, signal } = options; - if (!this.shouldUseChunkedUpload(file.size)) { + if (!this.shouldUseChunkedUpload(file.size, options.isS3Enabled)) { throw new Error( `File ${file.name} (${(file.size / (1024 * 1024)).toFixed(2)}MB) should not use chunked upload. Use regular upload instead.` ); @@ -238,8 +239,13 @@ export class ChunkedUploader { /** * Check if file should use chunked upload + * Only use chunked upload for filesystem storage, not for S3 */ - static shouldUseChunkedUpload(fileSize: number): boolean { + static shouldUseChunkedUpload(fileSize: number, isS3Enabled?: boolean): boolean { + if (isS3Enabled) { + return false; + } + const threshold = 100 * 1024 * 1024; // 100MB const shouldUse = fileSize > threshold; diff --git a/infra/update-versions.sh b/infra/update-versions.sh index 91508f7..134fb77 100755 --- a/infra/update-versions.sh +++ b/infra/update-versions.sh @@ -31,6 +31,7 @@ update_package_json() { update_package_json "apps/web/package.json" "Web App" update_package_json "apps/docs/package.json" "Documentation" update_package_json "apps/server/package.json" "API Server" +update_package_json "./package.json" "Monorepo" echo "🎉 Version update completed!" echo "📦 All package.json files now have version: $VERSION" \ No newline at end of file diff --git a/package.json b/package.json index d239c58..035a7cd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "palmr-monorepo", - "version": "3.1-beta", + "version": "3.1.4-beta", "description": "Palmr monorepo with Husky configuration", "private": true, "packageManager": "pnpm@10.6.0",