spb/groupe-ka Public
Groupe KA — site du holding + KA ID (compte unique & SSO des 7 plateformes). Next.js 16, SQLite, Google & Apple login.
TypeScript 85.5%
HTML 8.9%
CSS 5.5%
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// Photo de profil du KA ID — téléversée ici, elle apparaît sur la carte de3// membre du site, sur la carte Apple Wallet (vignette) et, via le jeton SSO,4// sur toutes les plateformes du groupe à la prochaine connexion.5// POST multipart (champ « photo », JPG/PNG/WebP ≤ 8 Mo) → carré 512 px JPEG6// DELETE → retire la photo (retour à l'initiale ; une connexion Google7// ultérieure remettra la photo Google)8import { NextRequest, NextResponse } from "next/server";9import fs from "fs/promises";10import path from "path";11import sharp from "sharp";12import { getSessionUser, BASE_URL } from "@/lib/auth";13import { db, ensureKaId } from "@/lib/db";1415const AVATAR_DIR = path.join(process.cwd(), "data", "avatars");16const MAX_BYTES = 8 * 1024 * 1024;1718export async function POST(req: NextRequest) {19 const user = await getSessionUser();20 if (!user)21 return NextResponse.json({ error: "Non connecté." }, { status: 401 });2223 const fd = await req.formData().catch(() => null);24 const file = fd?.get("photo");25 if (!(file instanceof File) || file.size === 0)26 return NextResponse.json(27 { error: "Aucune image reçue (champ « photo »)." },28 { status: 400 },29 );30 if (file.size > MAX_BYTES)31 return NextResponse.json(32 { error: "Image trop lourde — 8 Mo maximum." },33 { status: 413 },34 );35 if (!/^image\/(jpeg|png|webp|heic|heif|gif)$/i.test(file.type))36 return NextResponse.json(37 { error: "Format refusé — JPG, PNG ou WebP." },38 { status: 415 },39 );4041 const kaId = user.kaId ?? ensureKaId(user.id);42 let out: Buffer;43 try {44 out = await sharp(Buffer.from(await file.arrayBuffer()))45 .rotate() // respecte l'orientation EXIF46 .resize(512, 512, { fit: "cover", position: "attention" })47 .jpeg({ quality: 88 })48 .toBuffer();49 } catch {50 return NextResponse.json(51 { error: "Image illisible — réessayez avec un JPG ou un PNG." },52 { status: 422 },53 );54 }5556 await fs.mkdir(AVATAR_DIR, { recursive: true });57 await fs.writeFile(path.join(AVATAR_DIR, `${kaId}.jpg`), out);5859 // URL absolue (+cache-bust) : servie par /avatars/{kaId}, transportée60 // telle quelle dans le jeton SSO vers les plateformes.61 const url = `${BASE_URL}/avatars/${kaId}?v=${out.length.toString(36)}${Math.floor(out.byteLength % 997)}`;62 db.prepare("UPDATE users SET avatar_url = ? WHERE id = ?").run(url, user.id);63 return NextResponse.json({ ok: true, avatarUrl: url });64}6566export async function DELETE() {67 const user = await getSessionUser();68 if (!user)69 return NextResponse.json({ error: "Non connecté." }, { status: 401 });70 const kaId = user.kaId ?? ensureKaId(user.id);71 await fs.rm(path.join(AVATAR_DIR, `${kaId}.jpg`), { force: true });72 db.prepare("UPDATE users SET avatar_url = NULL WHERE id = ?").run(user.id);73 return NextResponse.json({ ok: true });74}75