diff --git a/apps/docs/package.json b/apps/docs/package.json index 881a763..21eff63 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -1,6 +1,6 @@ { "name": "palmr-docs", - "version": "3.2.4-beta", + "version": "3.2.5-beta", "description": "Docs for Palmr", "private": true, "author": "Daniel Luiz Alves ", diff --git a/apps/server/package.json b/apps/server/package.json index 4b05dc3..1cfe3a5 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,6 +1,6 @@ { "name": "palmr-api", - "version": "3.2.4-beta", + "version": "3.2.5-beta", "description": "API for Palmr", "private": true, "author": "Daniel Luiz Alves ", diff --git a/apps/server/src/modules/auth-providers/service.ts b/apps/server/src/modules/auth-providers/service.ts index 5e7aa4a..d0c9c31 100644 --- a/apps/server/src/modules/auth-providers/service.ts +++ b/apps/server/src/modules/auth-providers/service.ts @@ -617,6 +617,11 @@ export class AuthProvidersService { return await this.linkProviderToExistingUser(existingUser, provider.id, String(externalId), userInfo); } + // Check if auto-registration is disabled + if (provider.autoRegister === false) { + throw new Error(`User registration via ${provider.displayName || provider.name} is disabled`); + } + return await this.createNewUserWithProvider(userInfo, provider.id, String(externalId)); } diff --git a/apps/server/src/modules/file/controller.ts b/apps/server/src/modules/file/controller.ts index 21c08ab..e4bccec 100644 --- a/apps/server/src/modules/file/controller.ts +++ b/apps/server/src/modules/file/controller.ts @@ -1,3 +1,4 @@ +import * as fs from "fs"; import bcrypt from "bcryptjs"; import { FastifyReply, FastifyRequest } from "fastify"; @@ -8,6 +9,7 @@ import { generateUniqueFileNameForRename, parseFileName, } from "../../utils/file-name-generator"; +import { getContentType } from "../../utils/mime-types"; import { ConfigService } from "../config/service"; import { CheckFileInput, @@ -200,11 +202,10 @@ export class FileController { async getDownloadUrl(request: FastifyRequest, reply: FastifyReply) { try { - const { objectName: encodedObjectName } = request.params as { + const { objectName, password } = request.query as { objectName: string; + password?: string; }; - const objectName = decodeURIComponent(encodedObjectName); - const { password } = request.query as { password?: string }; if (!objectName) { return reply.status(400).send({ error: "The 'objectName' parameter is required." }); @@ -218,7 +219,8 @@ export class FileController { let hasAccess = false; - console.log("Requested file with password " + password); + // Don't log raw passwords. Log only whether a password was provided (for debugging access flow). + console.log(`Requested file access for object="${objectName}" passwordProvided=${password ? true : false}`); const shares = await prisma.share.findMany({ where: { @@ -270,6 +272,118 @@ export class FileController { } } + async downloadFile(request: FastifyRequest, reply: FastifyReply) { + try { + const { objectName, password } = request.query as { + objectName: string; + password?: string; + }; + + if (!objectName) { + return reply.status(400).send({ error: "The 'objectName' parameter is required." }); + } + + const fileRecord = await prisma.file.findFirst({ where: { objectName } }); + + if (!fileRecord) { + if (objectName.startsWith("reverse-shares/")) { + const reverseShareFile = await prisma.reverseShareFile.findFirst({ + where: { objectName }, + include: { + reverseShare: true, + }, + }); + + if (!reverseShareFile) { + return reply.status(404).send({ error: "File not found." }); + } + + try { + await request.jwtVerify(); + const userId = (request as any).user?.userId; + + if (!userId || reverseShareFile.reverseShare.creatorId !== userId) { + return reply.status(401).send({ error: "Unauthorized access to file." }); + } + } catch (err) { + return reply.status(401).send({ error: "Unauthorized access to file." }); + } + + const storageProvider = (this.fileService as any).storageProvider; + const filePath = storageProvider.getFilePath(objectName); + + const contentType = getContentType(reverseShareFile.name); + const fileName = reverseShareFile.name; + + reply.header("Content-Type", contentType); + reply.header("Content-Disposition", `inline; filename="${encodeURIComponent(fileName)}"`); + + const stream = fs.createReadStream(filePath); + return reply.send(stream); + } + + return reply.status(404).send({ error: "File not found." }); + } + + let hasAccess = false; + + const shares = await prisma.share.findMany({ + where: { + files: { + some: { + id: fileRecord.id, + }, + }, + }, + include: { + security: true, + }, + }); + + for (const share of shares) { + if (!share.security.password) { + hasAccess = true; + break; + } else if (password) { + const isPasswordValid = await bcrypt.compare(password, share.security.password); + if (isPasswordValid) { + hasAccess = true; + break; + } + } + } + + if (!hasAccess) { + try { + await request.jwtVerify(); + const userId = (request as any).user?.userId; + if (userId && fileRecord.userId === userId) { + hasAccess = true; + } + } catch (err) {} + } + + if (!hasAccess) { + return reply.status(401).send({ error: "Unauthorized access to file." }); + } + + const storageProvider = (this.fileService as any).storageProvider; + const filePath = storageProvider.getFilePath(objectName); + + const contentType = getContentType(fileRecord.name); + const fileName = fileRecord.name; + + reply.header("Content-Type", contentType); + reply.header("Content-Disposition", `inline; filename="${encodeURIComponent(fileName)}"`); + + const stream = fs.createReadStream(filePath); + return reply.send(stream); + } catch (error) { + console.error("Error in downloadFile:", error); + return reply.status(500).send({ error: "Internal server error." }); + } + } + async listFiles(request: FastifyRequest, reply: FastifyReply) { try { await request.jwtVerify(); @@ -471,6 +585,51 @@ export class FileController { } } + async embedFile(request: FastifyRequest, reply: FastifyReply) { + try { + const { id } = request.params as { id: string }; + + if (!id) { + return reply.status(400).send({ error: "File ID is required." }); + } + + const fileRecord = await prisma.file.findUnique({ where: { id } }); + + if (!fileRecord) { + return reply.status(404).send({ error: "File not found." }); + } + + const extension = fileRecord.extension.toLowerCase(); + const imageExts = ["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "ico", "avif"]; + const videoExts = ["mp4", "webm", "ogg", "mov", "avi", "mkv", "flv", "wmv"]; + const audioExts = ["mp3", "wav", "ogg", "m4a", "flac", "aac", "wma"]; + + const isMedia = imageExts.includes(extension) || videoExts.includes(extension) || audioExts.includes(extension); + + if (!isMedia) { + return reply.status(403).send({ + error: "Embed is only allowed for images, videos, and audio files.", + }); + } + + const storageProvider = (this.fileService as any).storageProvider; + const filePath = storageProvider.getFilePath(fileRecord.objectName); + + const contentType = getContentType(fileRecord.name); + const fileName = fileRecord.name; + + reply.header("Content-Type", contentType); + reply.header("Content-Disposition", `inline; filename="${encodeURIComponent(fileName)}"`); + reply.header("Cache-Control", "public, max-age=31536000"); // Cache por 1 ano + + const stream = fs.createReadStream(filePath); + return reply.send(stream); + } catch (error) { + console.error("Error in embedFile:", error); + return reply.status(500).send({ error: "Internal server error." }); + } + } + private async getAllUserFilesRecursively(userId: string): Promise { const rootFiles = await prisma.file.findMany({ where: { userId, folderId: null }, diff --git a/apps/server/src/modules/file/routes.ts b/apps/server/src/modules/file/routes.ts index 659021b..e6a5eba 100644 --- a/apps/server/src/modules/file/routes.ts +++ b/apps/server/src/modules/file/routes.ts @@ -106,17 +106,15 @@ export async function fileRoutes(app: FastifyInstance) { ); app.get( - "/files/:objectName/download", + "/files/download-url", { schema: { tags: ["File"], operationId: "getDownloadUrl", summary: "Get Download URL", description: "Generates a pre-signed URL for downloading a file", - params: z.object({ - objectName: z.string().min(1, "The objectName is required"), - }), querystring: z.object({ + objectName: z.string().min(1, "The objectName is required"), password: z.string().optional().describe("Share password if required"), }), response: { @@ -133,6 +131,46 @@ export async function fileRoutes(app: FastifyInstance) { fileController.getDownloadUrl.bind(fileController) ); + app.get( + "/embed/:id", + { + schema: { + tags: ["File"], + operationId: "embedFile", + summary: "Embed File (Public Access)", + description: + "Returns a media file (image/video/audio) for public embedding without authentication. Only works for media files.", + params: z.object({ + id: z.string().min(1, "File ID is required").describe("The file ID"), + }), + response: { + 400: z.object({ error: z.string().describe("Error message") }), + 403: z.object({ error: z.string().describe("Error message - not a media file") }), + 404: z.object({ error: z.string().describe("Error message") }), + 500: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + fileController.embedFile.bind(fileController) + ); + + app.get( + "/files/download", + { + schema: { + tags: ["File"], + operationId: "downloadFile", + summary: "Download File", + description: "Downloads a file directly (returns file content)", + querystring: z.object({ + objectName: z.string().min(1, "The objectName is required"), + password: z.string().optional().describe("Share password if required"), + }), + }, + }, + fileController.downloadFile.bind(fileController) + ); + app.get( "/files", { diff --git a/apps/server/src/modules/filesystem/controller.ts b/apps/server/src/modules/filesystem/controller.ts index 4250283..e736877 100644 --- a/apps/server/src/modules/filesystem/controller.ts +++ b/apps/server/src/modules/filesystem/controller.ts @@ -84,7 +84,6 @@ export class FilesystemController { const result = await this.handleChunkedUpload(request, chunkMetadata, tokenData.objectName); if (result.isComplete) { - provider.consumeUploadToken(token); reply.status(200).send({ message: "File uploaded successfully", objectName: result.finalPath, @@ -104,7 +103,6 @@ export class FilesystemController { } } else { await this.uploadFileStream(request, provider, tokenData.objectName); - provider.consumeUploadToken(token); reply.status(200).send({ message: "File uploaded successfully" }); } } catch (error) { @@ -271,8 +269,6 @@ export class FilesystemController { reply.header("Content-Length", fileSize); await this.downloadFileStream(reply, provider, tokenData.objectName, downloadId); } - - provider.consumeDownloadToken(token); } finally { this.memoryManager.endDownload(downloadId); } diff --git a/apps/server/src/providers/filesystem-storage.provider.ts b/apps/server/src/providers/filesystem-storage.provider.ts index 78e7ab1..d6a47d3 100644 --- a/apps/server/src/providers/filesystem-storage.provider.ts +++ b/apps/server/src/providers/filesystem-storage.provider.ts @@ -192,13 +192,9 @@ export class FilesystemStorageProvider implements StorageProvider { return `/api/filesystem/upload/${token}`; } - async getPresignedGetUrl(objectName: string, expires: number, fileName?: string): Promise { - const token = crypto.randomBytes(32).toString("hex"); - const expiresAt = Date.now() + expires * 1000; - - this.downloadTokens.set(token, { objectName, expiresAt, fileName }); - - return `/api/filesystem/download/${token}`; + async getPresignedGetUrl(objectName: string): Promise { + const encodedObjectName = encodeURIComponent(objectName); + return `/api/files/download?objectName=${encodedObjectName}`; } async deleteObject(objectName: string): Promise { @@ -636,13 +632,8 @@ export class FilesystemStorageProvider implements StorageProvider { return { objectName: data.objectName, fileName: data.fileName }; } - consumeUploadToken(token: string): void { - this.uploadTokens.delete(token); - } - - consumeDownloadToken(token: string): void { - this.downloadTokens.delete(token); - } + // Tokens are automatically cleaned up by cleanExpiredTokens() every 5 minutes + // No need to manually consume tokens - allows reuse for previews, range requests, etc. private async cleanupTempFile(tempPath: string): Promise { try { diff --git a/apps/web/messages/ar-SA.json b/apps/web/messages/ar-SA.json index f8d3934..0f7a10f 100644 --- a/apps/web/messages/ar-SA.json +++ b/apps/web/messages/ar-SA.json @@ -150,7 +150,9 @@ "move": "نقل", "rename": "إعادة تسمية", "search": "بحث", - "share": "مشاركة" + "share": "مشاركة", + "copied": "تم النسخ", + "copy": "نسخ" }, "createShare": { "title": "إنشاء مشاركة", @@ -302,6 +304,7 @@ }, "filePreview": { "title": "معاينة الملف", + "description": "معاينة وتنزيل الملف", "loading": "جاري التحميل...", "notAvailable": "المعاينة غير متاحة لهذا النوع من الملفات.", "downloadToView": "استخدم زر التحميل لتنزيل الملف.", @@ -1932,5 +1935,17 @@ "passwordRequired": "كلمة المرور مطلوبة", "nameRequired": "الاسم مطلوب", "required": "هذا الحقل مطلوب" + }, + "embedCode": { + "title": "تضمين الصورة", + "description": "استخدم هذه الأكواد لتضمين هذه الصورة في المنتديات أو المواقع الإلكترونية أو المنصات الأخرى", + "tabs": { + "directLink": "رابط مباشر", + "html": "HTML", + "bbcode": "BBCode" + }, + "directLinkDescription": "عنوان URL مباشر لملف الصورة", + "htmlDescription": "استخدم هذا الكود لتضمين الصورة في صفحات HTML", + "bbcodeDescription": "استخدم هذا الكود لتضمين الصورة في المنتديات التي تدعم BBCode" } -} +} \ No newline at end of file diff --git a/apps/web/messages/de-DE.json b/apps/web/messages/de-DE.json index c142241..576a730 100644 --- a/apps/web/messages/de-DE.json +++ b/apps/web/messages/de-DE.json @@ -150,7 +150,9 @@ "move": "Verschieben", "rename": "Umbenennen", "search": "Suchen", - "share": "Teilen" + "share": "Teilen", + "copied": "Kopiert", + "copy": "Kopieren" }, "createShare": { "title": "Freigabe Erstellen", @@ -302,6 +304,7 @@ }, "filePreview": { "title": "Datei-Vorschau", + "description": "Vorschau und Download der Datei", "loading": "Laden...", "notAvailable": "Vorschau für diesen Dateityp nicht verfügbar.", "downloadToView": "Verwenden Sie die Download-Schaltfläche, um die Datei herunterzuladen.", @@ -1930,5 +1933,17 @@ "passwordRequired": "Passwort ist erforderlich", "nameRequired": "Name ist erforderlich", "required": "Dieses Feld ist erforderlich" + }, + "embedCode": { + "title": "Bild einbetten", + "description": "Verwenden Sie diese Codes, um dieses Bild in Foren, Websites oder anderen Plattformen einzubetten", + "tabs": { + "directLink": "Direkter Link", + "html": "HTML", + "bbcode": "BBCode" + }, + "directLinkDescription": "Direkte URL zur Bilddatei", + "htmlDescription": "Verwenden Sie diesen Code, um das Bild in HTML-Seiten einzubetten", + "bbcodeDescription": "Verwenden Sie diesen Code, um das Bild in Foren einzubetten, die BBCode unterstützen" } -} +} \ No newline at end of file diff --git a/apps/web/messages/en-US.json b/apps/web/messages/en-US.json index 469c1f8..087cf76 100644 --- a/apps/web/messages/en-US.json +++ b/apps/web/messages/en-US.json @@ -150,7 +150,9 @@ "rename": "Rename", "move": "Move", "share": "Share", - "search": "Search" + "search": "Search", + "copy": "Copy", + "copied": "Copied" }, "createShare": { "title": "Create Share", @@ -302,6 +304,7 @@ }, "filePreview": { "title": "Preview File", + "description": "Preview and download file", "loading": "Loading...", "notAvailable": "Preview not available for this file type", "downloadToView": "Use the download button to view this file", @@ -1881,6 +1884,18 @@ "userr": "User" } }, + "embedCode": { + "title": "Embed Image", + "description": "Use these codes to embed this image in forums, websites, or other platforms", + "tabs": { + "directLink": "Direct Link", + "html": "HTML", + "bbcode": "BBCode" + }, + "directLinkDescription": "Direct URL to the image file", + "htmlDescription": "Use this code to embed the image in HTML pages", + "bbcodeDescription": "Use this code to embed the image in forums that support BBCode" + }, "validation": { "firstNameRequired": "First name is required", "lastNameRequired": "Last name is required", @@ -1896,4 +1911,4 @@ "nameRequired": "Name is required", "required": "This field is required" } -} +} \ No newline at end of file diff --git a/apps/web/messages/es-ES.json b/apps/web/messages/es-ES.json index 309abc5..5d01b3b 100644 --- a/apps/web/messages/es-ES.json +++ b/apps/web/messages/es-ES.json @@ -150,7 +150,9 @@ "move": "Mover", "rename": "Renombrar", "search": "Buscar", - "share": "Compartir" + "share": "Compartir", + "copied": "Copiado", + "copy": "Copiar" }, "createShare": { "title": "Crear Compartir", @@ -302,6 +304,7 @@ }, "filePreview": { "title": "Vista Previa del Archivo", + "description": "Vista previa y descarga de archivo", "loading": "Cargando...", "notAvailable": "Vista previa no disponible para este tipo de archivo.", "downloadToView": "Use el botón de descarga para descargar el archivo.", @@ -1930,5 +1933,17 @@ "passwordRequired": "Se requiere la contraseña", "nameRequired": "El nombre es obligatorio", "required": "Este campo es obligatorio" + }, + "embedCode": { + "title": "Insertar imagen", + "description": "Utiliza estos códigos para insertar esta imagen en foros, sitios web u otras plataformas", + "tabs": { + "directLink": "Enlace directo", + "html": "HTML", + "bbcode": "BBCode" + }, + "directLinkDescription": "URL directa al archivo de imagen", + "htmlDescription": "Utiliza este código para insertar la imagen en páginas HTML", + "bbcodeDescription": "Utiliza este código para insertar la imagen en foros que admiten BBCode" } -} +} \ No newline at end of file diff --git a/apps/web/messages/fr-FR.json b/apps/web/messages/fr-FR.json index 1c75b33..1cb2277 100644 --- a/apps/web/messages/fr-FR.json +++ b/apps/web/messages/fr-FR.json @@ -150,7 +150,9 @@ "move": "Déplacer", "rename": "Renommer", "search": "Rechercher", - "share": "Partager" + "share": "Partager", + "copied": "Copié", + "copy": "Copier" }, "createShare": { "title": "Créer un Partage", @@ -302,6 +304,7 @@ }, "filePreview": { "title": "Aperçu du Fichier", + "description": "Aperçu et téléchargement du fichier", "loading": "Chargement...", "notAvailable": "Aperçu non disponible pour ce type de fichier.", "downloadToView": "Utilisez le bouton de téléchargement pour télécharger le fichier.", @@ -1930,5 +1933,17 @@ "passwordRequired": "Le mot de passe est requis", "nameRequired": "Nome é obrigatório", "required": "Este campo é obrigatório" + }, + "embedCode": { + "title": "Intégrer l'image", + "description": "Utilisez ces codes pour intégrer cette image dans des forums, sites web ou autres plateformes", + "tabs": { + "directLink": "Lien direct", + "html": "HTML", + "bbcode": "BBCode" + }, + "directLinkDescription": "URL directe vers le fichier image", + "htmlDescription": "Utilisez ce code pour intégrer l'image dans des pages HTML", + "bbcodeDescription": "Utilisez ce code pour intégrer l'image dans des forums prenant en charge BBCode" } -} +} \ No newline at end of file diff --git a/apps/web/messages/hi-IN.json b/apps/web/messages/hi-IN.json index 1631063..fa16cc4 100644 --- a/apps/web/messages/hi-IN.json +++ b/apps/web/messages/hi-IN.json @@ -150,7 +150,9 @@ "move": "स्थानांतरित करें", "rename": "नाम बदलें", "search": "खोजें", - "share": "साझा करें" + "share": "साझा करें", + "copied": "कॉपी किया गया", + "copy": "कॉपी करें" }, "createShare": { "title": "साझाकरण बनाएं", @@ -302,6 +304,7 @@ }, "filePreview": { "title": "फ़ाइल पूर्वावलोकन", + "description": "फ़ाइल पूर्वावलोकन और डाउनलोड", "loading": "लोड हो रहा है...", "notAvailable": "इस फ़ाइल प्रकार के लिए पूर्वावलोकन उपलब्ध नहीं है।", "downloadToView": "फ़ाइल डाउनलोड करने के लिए डाउनलोड बटन का उपयोग करें।", @@ -1930,5 +1933,17 @@ "passwordRequired": "पासवर्ड आवश्यक है", "nameRequired": "नाम आवश्यक है", "required": "यह फ़ील्ड आवश्यक है" + }, + "embedCode": { + "title": "छवि एम्बेड करें", + "description": "इस छवि को मंचों, वेबसाइटों या अन्य प्लेटफार्मों में एम्बेड करने के लिए इन कोड का उपयोग करें", + "tabs": { + "directLink": "सीधा लिंक", + "html": "HTML", + "bbcode": "BBCode" + }, + "directLinkDescription": "छवि फ़ाइल का सीधा URL", + "htmlDescription": "HTML पेजों में छवि एम्बेड करने के लिए इस कोड का उपयोग करें", + "bbcodeDescription": "BBCode का समर्थन करने वाले मंचों में छवि एम्बेड करने के लिए इस कोड का उपयोग करें" } -} +} \ No newline at end of file diff --git a/apps/web/messages/it-IT.json b/apps/web/messages/it-IT.json index 1c4a2a8..7056fef 100644 --- a/apps/web/messages/it-IT.json +++ b/apps/web/messages/it-IT.json @@ -150,7 +150,9 @@ "move": "Sposta", "rename": "Rinomina", "search": "Cerca", - "share": "Condividi" + "share": "Condividi", + "copied": "Copiato", + "copy": "Copia" }, "createShare": { "title": "Crea Condivisione", @@ -302,6 +304,7 @@ }, "filePreview": { "title": "Anteprima File", + "description": "Anteprima e download del file", "loading": "Caricamento...", "notAvailable": "Anteprima non disponibile per questo tipo di file.", "downloadToView": "Utilizzare il pulsante di download per scaricare il file.", @@ -1930,5 +1933,17 @@ "passwordMinLength": "La password deve contenere almeno 6 caratteri", "nameRequired": "Il nome è obbligatorio", "required": "Questo campo è obbligatorio" + }, + "embedCode": { + "title": "Incorpora immagine", + "description": "Usa questi codici per incorporare questa immagine in forum, siti web o altre piattaforme", + "tabs": { + "directLink": "Link diretto", + "html": "HTML", + "bbcode": "BBCode" + }, + "directLinkDescription": "URL diretto al file immagine", + "htmlDescription": "Usa questo codice per incorporare l'immagine nelle pagine HTML", + "bbcodeDescription": "Usa questo codice per incorporare l'immagine nei forum che supportano BBCode" } -} +} \ No newline at end of file diff --git a/apps/web/messages/ja-JP.json b/apps/web/messages/ja-JP.json index cdcb190..096c425 100644 --- a/apps/web/messages/ja-JP.json +++ b/apps/web/messages/ja-JP.json @@ -150,7 +150,9 @@ "move": "移動", "rename": "名前を変更", "search": "検索", - "share": "共有" + "share": "共有", + "copied": "コピーしました", + "copy": "コピー" }, "createShare": { "title": "共有を作成", @@ -302,6 +304,7 @@ }, "filePreview": { "title": "ファイルプレビュー", + "description": "ファイルをプレビューしてダウンロード", "loading": "読み込み中...", "notAvailable": "このファイルタイプのプレビューは利用できません。", "downloadToView": "ダウンロードボタンを使用してファイルをダウンロードしてください。", @@ -1930,5 +1933,17 @@ "passwordRequired": "パスワードは必須です", "nameRequired": "名前は必須です", "required": "このフィールドは必須です" + }, + "embedCode": { + "title": "画像を埋め込む", + "description": "これらのコードを使用して、この画像をフォーラム、ウェブサイト、またはその他のプラットフォームに埋め込みます", + "tabs": { + "directLink": "直接リンク", + "html": "HTML", + "bbcode": "BBCode" + }, + "directLinkDescription": "画像ファイルへの直接URL", + "htmlDescription": "このコードを使用してHTMLページに画像を埋め込みます", + "bbcodeDescription": "BBCodeをサポートするフォーラムに画像を埋め込むには、このコードを使用します" } -} +} \ No newline at end of file diff --git a/apps/web/messages/ko-KR.json b/apps/web/messages/ko-KR.json index bbc7283..a274947 100644 --- a/apps/web/messages/ko-KR.json +++ b/apps/web/messages/ko-KR.json @@ -150,7 +150,9 @@ "move": "이동", "rename": "이름 변경", "search": "검색", - "share": "공유" + "share": "공유", + "copied": "복사됨", + "copy": "복사" }, "createShare": { "title": "공유 생성", @@ -302,6 +304,7 @@ }, "filePreview": { "title": "파일 미리보기", + "description": "파일 미리보기 및 다운로드", "loading": "로딩 중...", "notAvailable": "이 파일 유형에 대한 미리보기를 사용할 수 없습니다.", "downloadToView": "다운로드 버튼을 사용하여 파일을 다운로드하세요.", @@ -1930,5 +1933,17 @@ "passwordRequired": "비밀번호는 필수입니다", "nameRequired": "이름은 필수입니다", "required": "이 필드는 필수입니다" + }, + "embedCode": { + "title": "이미지 삽입", + "description": "이 코드를 사용하여 포럼, 웹사이트 또는 기타 플랫폼에 이 이미지를 삽입하세요", + "tabs": { + "directLink": "직접 링크", + "html": "HTML", + "bbcode": "BBCode" + }, + "directLinkDescription": "이미지 파일에 대한 직접 URL", + "htmlDescription": "이 코드를 사용하여 HTML 페이지에 이미지를 삽입하세요", + "bbcodeDescription": "BBCode를 지원하는 포럼에 이미지를 삽입하려면 이 코드를 사용하세요" } -} +} \ No newline at end of file diff --git a/apps/web/messages/nl-NL.json b/apps/web/messages/nl-NL.json index 678576e..5e2faab 100644 --- a/apps/web/messages/nl-NL.json +++ b/apps/web/messages/nl-NL.json @@ -150,7 +150,9 @@ "move": "Verplaatsen", "rename": "Hernoemen", "search": "Zoeken", - "share": "Delen" + "share": "Delen", + "copied": "Gekopieerd", + "copy": "Kopiëren" }, "createShare": { "title": "Delen Maken", @@ -302,6 +304,7 @@ }, "filePreview": { "title": "Bestandsvoorbeeld", + "description": "Bestand bekijken en downloaden", "loading": "Laden...", "notAvailable": "Voorbeeld niet beschikbaar voor dit bestandstype.", "downloadToView": "Gebruik de downloadknop om het bestand te downloaden.", @@ -1930,5 +1933,17 @@ "passwordMinLength": "Wachtwoord moet minimaal 6 tekens bevatten", "nameRequired": "Naam is verplicht", "required": "Dit veld is verplicht" + }, + "embedCode": { + "title": "Afbeelding insluiten", + "description": "Gebruik deze codes om deze afbeelding in te sluiten in forums, websites of andere platforms", + "tabs": { + "directLink": "Directe link", + "html": "HTML", + "bbcode": "BBCode" + }, + "directLinkDescription": "Directe URL naar het afbeeldingsbestand", + "htmlDescription": "Gebruik deze code om de afbeelding in te sluiten in HTML-pagina's", + "bbcodeDescription": "Gebruik deze code om de afbeelding in te sluiten in forums die BBCode ondersteunen" } -} +} \ No newline at end of file diff --git a/apps/web/messages/pl-PL.json b/apps/web/messages/pl-PL.json index 910f793..5fc4c14 100644 --- a/apps/web/messages/pl-PL.json +++ b/apps/web/messages/pl-PL.json @@ -150,7 +150,9 @@ "move": "Przenieś", "rename": "Zmień nazwę", "search": "Szukaj", - "share": "Udostępnij" + "share": "Udostępnij", + "copied": "Skopiowano", + "copy": "Kopiuj" }, "createShare": { "title": "Utwórz Udostępnienie", @@ -302,6 +304,7 @@ }, "filePreview": { "title": "Podgląd pliku", + "description": "Podgląd i pobieranie pliku", "loading": "Ładowanie...", "notAvailable": "Podgląd niedostępny dla tego typu pliku", "downloadToView": "Użyj przycisku pobierania, aby wyświetlić ten plik", @@ -1930,5 +1933,17 @@ "passwordMinLength": "Hasło musi mieć co najmniej 6 znaków", "nameRequired": "Nazwa jest wymagana", "required": "To pole jest wymagane" + }, + "embedCode": { + "title": "Osadź obraz", + "description": "Użyj tych kodów, aby osadzić ten obraz na forach, stronach internetowych lub innych platformach", + "tabs": { + "directLink": "Link bezpośredni", + "html": "HTML", + "bbcode": "BBCode" + }, + "directLinkDescription": "Bezpośredni adres URL pliku obrazu", + "htmlDescription": "Użyj tego kodu, aby osadzić obraz na stronach HTML", + "bbcodeDescription": "Użyj tego kodu, aby osadzić obraz na forach obsługujących BBCode" } -} +} \ No newline at end of file diff --git a/apps/web/messages/pt-BR.json b/apps/web/messages/pt-BR.json index 3501a09..732bb73 100644 --- a/apps/web/messages/pt-BR.json +++ b/apps/web/messages/pt-BR.json @@ -150,7 +150,9 @@ "move": "Mover", "rename": "Renomear", "search": "Pesquisar", - "share": "Compartilhar" + "share": "Compartilhar", + "copied": "Copiado", + "copy": "Copiar" }, "createShare": { "title": "Criar compartilhamento", @@ -302,6 +304,7 @@ }, "filePreview": { "title": "Visualizar Arquivo", + "description": "Visualizar e baixar arquivo", "loading": "Carregando...", "notAvailable": "Preview não disponível para este tipo de arquivo.", "downloadToView": "Use o botão de download para baixar o arquivo.", @@ -1931,5 +1934,17 @@ "lastNameRequired": "O sobrenome é necessário", "usernameLength": "O nome de usuário deve ter pelo menos 3 caracteres", "usernameSpaces": "O nome de usuário não pode conter espaços" + }, + "embedCode": { + "title": "Incorporar imagem", + "description": "Use estes códigos para incorporar esta imagem em fóruns, sites ou outras plataformas", + "tabs": { + "directLink": "Link direto", + "html": "HTML", + "bbcode": "BBCode" + }, + "directLinkDescription": "URL direto para o arquivo de imagem", + "htmlDescription": "Use este código para incorporar a imagem em páginas HTML", + "bbcodeDescription": "Use este código para incorporar a imagem em fóruns que suportam BBCode" } -} +} \ No newline at end of file diff --git a/apps/web/messages/ru-RU.json b/apps/web/messages/ru-RU.json index 857caf2..6e9503f 100644 --- a/apps/web/messages/ru-RU.json +++ b/apps/web/messages/ru-RU.json @@ -150,7 +150,9 @@ "move": "Переместить", "rename": "Переименовать", "search": "Поиск", - "share": "Поделиться" + "share": "Поделиться", + "copied": "Скопировано", + "copy": "Копировать" }, "createShare": { "title": "Создать общий доступ", @@ -302,6 +304,7 @@ }, "filePreview": { "title": "Предварительный просмотр файла", + "description": "Просмотр и загрузка файла", "loading": "Загрузка...", "notAvailable": "Предварительный просмотр недоступен для этого типа файла.", "downloadToView": "Используйте кнопку загрузки для скачивания файла.", @@ -1930,5 +1933,17 @@ "passwordRequired": "Требуется пароль", "nameRequired": "Требуется имя", "required": "Это поле обязательно" + }, + "embedCode": { + "title": "Встроить изображение", + "description": "Используйте эти коды для встраивания этого изображения на форумах, веб-сайтах или других платформах", + "tabs": { + "directLink": "Прямая ссылка", + "html": "HTML", + "bbcode": "BBCode" + }, + "directLinkDescription": "Прямой URL-адрес файла изображения", + "htmlDescription": "Используйте этот код для встраивания изображения в HTML-страницы", + "bbcodeDescription": "Используйте этот код для встраивания изображения на форумах, поддерживающих BBCode" } -} +} \ No newline at end of file diff --git a/apps/web/messages/tr-TR.json b/apps/web/messages/tr-TR.json index 13256eb..234f93b 100644 --- a/apps/web/messages/tr-TR.json +++ b/apps/web/messages/tr-TR.json @@ -150,7 +150,9 @@ "move": "Taşı", "rename": "Yeniden Adlandır", "search": "Ara", - "share": "Paylaş" + "share": "Paylaş", + "copied": "Kopyalandı", + "copy": "Kopyala" }, "createShare": { "title": "Paylaşım Oluştur", @@ -302,6 +304,7 @@ }, "filePreview": { "title": "Dosya Önizleme", + "description": "Dosyayı önizleyin ve indirin", "loading": "Yükleniyor...", "notAvailable": "Bu dosya türü için önizleme mevcut değil.", "downloadToView": "Dosyayı indirmek için indirme düğmesini kullanın.", @@ -1930,5 +1933,17 @@ "passwordRequired": "Şifre gerekli", "nameRequired": "İsim gereklidir", "required": "Bu alan zorunludur" + }, + "embedCode": { + "title": "Resmi Yerleştir", + "description": "Bu görüntüyü forumlara, web sitelerine veya diğer platformlara yerleştirmek için bu kodları kullanın", + "tabs": { + "directLink": "Doğrudan Bağlantı", + "html": "HTML", + "bbcode": "BBCode" + }, + "directLinkDescription": "Resim dosyasının doğrudan URL'si", + "htmlDescription": "Resmi HTML sayfalarına yerleştirmek için bu kodu kullanın", + "bbcodeDescription": "BBCode destekleyen forumlara resmi yerleştirmek için bu kodu kullanın" } -} +} \ No newline at end of file diff --git a/apps/web/messages/zh-CN.json b/apps/web/messages/zh-CN.json index 00cb2d8..1dc439d 100644 --- a/apps/web/messages/zh-CN.json +++ b/apps/web/messages/zh-CN.json @@ -150,7 +150,9 @@ "move": "移动", "rename": "重命名", "search": "搜索", - "share": "分享" + "share": "分享", + "copied": "已复制", + "copy": "复制" }, "createShare": { "title": "创建分享", @@ -302,6 +304,7 @@ }, "filePreview": { "title": "文件预览", + "description": "预览和下载文件", "loading": "加载中...", "notAvailable": "此文件类型不支持预览。", "downloadToView": "使用下载按钮下载文件。", @@ -1930,5 +1933,17 @@ "passwordRequired": "密码为必填项", "nameRequired": "名称为必填项", "required": "此字段为必填项" + }, + "embedCode": { + "title": "嵌入图片", + "description": "使用这些代码将此图片嵌入到论坛、网站或其他平台中", + "tabs": { + "directLink": "直接链接", + "html": "HTML", + "bbcode": "BBCode" + }, + "directLinkDescription": "图片文件的直接URL", + "htmlDescription": "使用此代码将图片嵌入HTML页面", + "bbcodeDescription": "使用此代码将图片嵌入支持BBCode的论坛" } -} +} \ No newline at end of file diff --git a/apps/web/package.json b/apps/web/package.json index 168a22a..438299f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "palmr-web", - "version": "3.2.4-beta", + "version": "3.2.5-beta", "description": "Frontend for Palmr", "private": true, "author": "Daniel Luiz Alves ", diff --git a/apps/web/src/app/(shares)/reverse-shares/components/received-files-modal.tsx b/apps/web/src/app/(shares)/reverse-shares/components/received-files-modal.tsx index f7fbcc9..4eea8eb 100644 --- a/apps/web/src/app/(shares)/reverse-shares/components/received-files-modal.tsx +++ b/apps/web/src/app/(shares)/reverse-shares/components/received-files-modal.tsx @@ -900,16 +900,7 @@ export function ReceivedFilesModal({ {previewFile && ( - setPreviewFile(null)} - file={{ - id: previewFile.id, - name: previewFile.name, - objectName: previewFile.objectName, - extension: previewFile.extension, - }} - /> + setPreviewFile(null)} file={previewFile} /> )} ); diff --git a/apps/web/src/app/(shares)/reverse-shares/components/received-files-section.tsx b/apps/web/src/app/(shares)/reverse-shares/components/received-files-section.tsx index 990e45c..ad5d494 100644 --- a/apps/web/src/app/(shares)/reverse-shares/components/received-files-section.tsx +++ b/apps/web/src/app/(shares)/reverse-shares/components/received-files-section.tsx @@ -7,23 +7,11 @@ import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { deleteReverseShareFile } from "@/http/endpoints/reverse-shares"; +import type { ReverseShareFile } from "@/http/endpoints/reverse-shares/types"; import { downloadReverseShareWithQueue } from "@/utils/download-queue-utils"; import { getFileIcon } from "@/utils/file-icons"; import { ReverseShareFilePreviewModal } from "./reverse-share-file-preview-modal"; -interface ReverseShareFile { - id: string; - name: string; - description: string | null; - extension: string; - size: string; - objectName: string; - uploaderEmail: string | null; - uploaderName: string | null; - createdAt: string; - updatedAt: string; -} - interface ReceivedFilesSectionProps { files: ReverseShareFile[]; onFileDeleted?: () => void; @@ -159,16 +147,7 @@ export function ReceivedFilesSection({ files, onFileDeleted }: ReceivedFilesSect {previewFile && ( - setPreviewFile(null)} - file={{ - id: previewFile.id, - name: previewFile.name, - objectName: previewFile.objectName, - extension: previewFile.extension, - }} - /> + setPreviewFile(null)} file={previewFile} /> )} ); diff --git a/apps/web/src/app/(shares)/reverse-shares/components/reverse-share-file-preview-modal.tsx b/apps/web/src/app/(shares)/reverse-shares/components/reverse-share-file-preview-modal.tsx index 8f21cad..4439335 100644 --- a/apps/web/src/app/(shares)/reverse-shares/components/reverse-share-file-preview-modal.tsx +++ b/apps/web/src/app/(shares)/reverse-shares/components/reverse-share-file-preview-modal.tsx @@ -1,26 +1,20 @@ "use client"; import { FilePreviewModal } from "@/components/modals/file-preview-modal"; +import type { ReverseShareFile } from "@/http/endpoints/reverse-shares/types"; interface ReverseShareFilePreviewModalProps { isOpen: boolean; onClose: () => void; - file: { - id: string; - name: string; - objectName: string; - extension?: string; - } | null; + file: ReverseShareFile | null; } export function ReverseShareFilePreviewModal({ isOpen, onClose, file }: ReverseShareFilePreviewModalProps) { if (!file) return null; const adaptedFile = { - name: file.name, - objectName: file.objectName, - type: file.extension, - id: file.id, + ...file, + description: file.description ?? undefined, }; return ; diff --git a/apps/web/src/app/(shares)/reverse-shares/components/reverse-shares-search.tsx b/apps/web/src/app/(shares)/reverse-shares/components/reverse-shares-search.tsx index 69ce666..5c584aa 100644 --- a/apps/web/src/app/(shares)/reverse-shares/components/reverse-shares-search.tsx +++ b/apps/web/src/app/(shares)/reverse-shares/components/reverse-shares-search.tsx @@ -30,7 +30,7 @@ export function ReverseSharesSearch({

{t("reverseShares.search.title")}

- +
+

{t("embedCode.directLinkDescription")}

+ + + +
+ + +
+

{t("embedCode.htmlDescription")}

+
+ + +
+ + +
+

{t("embedCode.bbcodeDescription")}

+
+ +
+ + + ); +} diff --git a/apps/web/src/components/files/media-embed-link.tsx b/apps/web/src/components/files/media-embed-link.tsx new file mode 100644 index 0000000..d95c476 --- /dev/null +++ b/apps/web/src/components/files/media-embed-link.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { IconCheck, IconCopy } from "@tabler/icons-react"; +import { useTranslations } from "next-intl"; + +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Label } from "@/components/ui/label"; + +interface MediaEmbedLinkProps { + fileId: string; +} + +export function MediaEmbedLink({ fileId }: MediaEmbedLinkProps) { + const t = useTranslations(); + const [copied, setCopied] = useState(false); + const [embedUrl, setEmbedUrl] = useState(""); + + useEffect(() => { + if (typeof window !== "undefined") { + const origin = window.location.origin; + const url = `${origin}/e/${fileId}`; + setEmbedUrl(url); + } + }, [fileId]); + + const copyToClipboard = async () => { + try { + await navigator.clipboard.writeText(embedUrl); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (error) { + console.error("Failed to copy:", error); + } + }; + + return ( + + +
+
+ +

{t("embedCode.directLinkDescription")}

+
+ +
+ + +
+
+
+
+ ); +} diff --git a/apps/web/src/components/modals/file-preview-modal.tsx b/apps/web/src/components/modals/file-preview-modal.tsx index 61d5db9..72bfdad 100644 --- a/apps/web/src/components/modals/file-preview-modal.tsx +++ b/apps/web/src/components/modals/file-preview-modal.tsx @@ -3,10 +3,20 @@ import { IconDownload } from "@tabler/icons-react"; import { useTranslations } from "next-intl"; +import { EmbedCodeDisplay } from "@/components/files/embed-code-display"; +import { MediaEmbedLink } from "@/components/files/media-embed-link"; import { Button } from "@/components/ui/button"; -import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { useFilePreview } from "@/hooks/use-file-preview"; import { getFileIcon } from "@/utils/file-icons"; +import { getFileType } from "@/utils/file-types"; import { FilePreviewRenderer } from "./previews"; interface FilePreviewModalProps { @@ -32,6 +42,10 @@ export function FilePreviewModal({ }: FilePreviewModalProps) { const t = useTranslations(); const previewState = useFilePreview({ file, isOpen, isReverseShare, sharePassword }); + const fileType = getFileType(file.name); + const isImage = fileType === "image"; + const isVideo = fileType === "video"; + const isAudio = fileType === "audio"; return ( @@ -44,6 +58,7 @@ export function FilePreviewModal({ })()} {file.name} + {t("filePreview.description")}
+ {isImage && previewState.previewUrl && !previewState.isLoading && file.id && ( +
+ +
+ )} + {(isVideo || isAudio) && !previewState.isLoading && file.id && ( +
+ +
+ )}