SPB Git

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%
2.9 KB · 75 lines typescript
Raw Blame History
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