JavaScript 56.7%
TypeScript 42.6%
1/**2 * Accès aux liens de partage publics : validité, mot de passe (cookie signé après vérification),3 * compteur d'accès, portée (fichier ou dossier + descendants).4 */5import crypto from "crypto";6import { NextRequest, NextResponse } from "next/server";7import { prisma } from "./prisma";8import { isDescendantOf } from "./trash";910export type ShareWithTargets = NonNullable<Awaited<ReturnType<typeof loadShare>>>;1112export async function loadShare(token: string) {13 return prisma.sharedLink.findUnique({14 where: { token },15 include: { file: true, folder: true },16 });17}1819/** null si valide, sinon une réponse d'erreur prête à renvoyer. */20export function shareStatusError(share: ShareWithTargets | null): NextResponse | null {21 if (!share || !share.active) return NextResponse.json({ error: "Lien introuvable ou inactif" }, { status: 404 });22 if (share.expiresAt && new Date() > share.expiresAt) return NextResponse.json({ error: "Ce lien a expiré" }, { status: 410 });23 if (share.file && share.file.deletedAt) return NextResponse.json({ error: "Le fichier partagé n'existe plus" }, { status: 404 });24 if (!share.file && !share.folder) return NextResponse.json({ error: "La cible du partage n'existe plus" }, { status: 404 });25 return null;26}2728const SECRET = process.env.SESSION_PASSWORD || "spb-share-secret";2930export function shareCookieName(token: string): string {31 return `spb_share_${token.slice(0, 24)}`;32}3334/** Signature liée au lien ET au hash du mot de passe (changer le mot de passe invalide les cookies). */35export function shareCookieValue(share: { token: string; passwordHash: string | null }): string {36 return crypto.createHmac("sha256", SECRET).update(`${share.token}:${share.passwordHash ?? ""}`).digest("hex");37}3839/** Le visiteur a-t-il déjà validé le mot de passe (cookie signé) ? */40export function hasPasswordAccess(request: NextRequest, share: { token: string; passwordHash: string | null }): boolean {41 if (!share.passwordHash) return true;42 const c = request.cookies.get(shareCookieName(share.token))?.value;43 if (!c) return false;44 const expected = shareCookieValue(share);45 return c.length === expected.length && crypto.timingSafeEqual(Buffer.from(c), Buffer.from(expected));46}4748export function grantPasswordAccess(response: NextResponse, share: { token: string; passwordHash: string | null }) {49 response.cookies.set(shareCookieName(share.token), shareCookieValue(share), {50 httpOnly: true, sameSite: "lax", secure: process.env.NODE_ENV === "production", path: "/", maxAge: 60 * 60 * 6,51 });52}5354/** Informations publiques d'un partage (sans contenu). */55export function shareInfo(share: ShareWithTargets) {56 const base = { token: share.token, mode: share.mode, expiresAt: share.expiresAt?.toISOString() || null };57 if (share.file) {58 return { ...base, type: "file" as const, id: share.file.id, name: share.file.name, mimeType: share.file.mimeType, size: Number(share.file.size) };59 }60 return { ...base, type: "folder" as const, id: share.folder!.id, name: share.folder!.name };61}6263export async function touchShare(id: string) {64 await prisma.sharedLink.update({ where: { id }, data: { accessCount: { increment: 1 }, lastAccessAt: new Date() } }).catch(() => {});65}6667/** Un dossier demandé appartient-il au sous-arbre partagé ? */68export async function folderInShare(share: ShareWithTargets, folderId: string): Promise<boolean> {69 if (!share.folder) return false;70 return isDescendantOf(folderId, share.folder.id);71}7273/** Un fichier demandé appartient-il au partage (fichier lui-même ou fichier du sous-arbre partagé) ? */74export async function fileInShare(share: ShareWithTargets, fileId: string) {75 if (share.file) return share.file.id === fileId ? share.file : null;76 if (!share.folder) return null;77 const f = await prisma.file.findUnique({ where: { id: fileId } });78 if (!f || f.deletedAt || !f.folderId) return null;79 return (await isDescendantOf(f.folderId, share.folder.id)) ? f : null;80}81