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 66.3% HTML 29.4% CSS 4.3%
3.8 KB · 116 lines typescript
Raw Blame History
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// Retour du flux Google OAuth : échange du code, vérification de3// l'id_token (JWKS Google), création/liaison du KA ID, ouverture de session.4import { NextRequest, NextResponse } from "next/server";5import { createRemoteJWKSet, jwtVerify } from "jose";6import { db, findUserByEmail, findUserByGoogleSub, ensureKaId, touchLastLogin } from "@/lib/db";7import {8  BASE_URL,9  mintSessionToken,10  SESSION_COOKIE,11  sessionCookieOptions,12  safeNext,13} from "@/lib/auth";1415const GOOGLE_JWKS = createRemoteJWKSet(16  new URL("https://www.googleapis.com/oauth2/v3/certs"),17);1819function fail(reason: string) {20  return NextResponse.redirect(21    new URL(`/connexion?error=${encodeURIComponent(reason)}`, BASE_URL),22  );23}2425export async function GET(req: NextRequest) {26  const clientId = process.env.GOOGLE_CLIENT_ID;27  const clientSecret = process.env.GOOGLE_CLIENT_SECRET;28  if (!clientId || !clientSecret) return fail("google-non-configure");2930  const code = req.nextUrl.searchParams.get("code");31  const state = req.nextUrl.searchParams.get("state");32  const cookieState = req.cookies.get("ka_oauth_state")?.value;33  const next = safeNext(req.cookies.get("ka_oauth_next")?.value);34  if (!code || !state || !cookieState || state !== cookieState)35    return fail("etat-oauth-invalide");3637  // Échange code → jetons38  const tokenRes = await fetch("https://oauth2.googleapis.com/token", {39    method: "POST",40    headers: { "Content-Type": "application/x-www-form-urlencoded" },41    body: new URLSearchParams({42      code,43      client_id: clientId,44      client_secret: clientSecret,45      redirect_uri: `${BASE_URL}/api/auth/callback/google`,46      grant_type: "authorization_code",47    }),48  });49  if (!tokenRes.ok) return fail("echange-google-refuse");50  const tokens = (await tokenRes.json()) as { id_token?: string };51  if (!tokens.id_token) return fail("id-token-absent");5253  // Vérification cryptographique de l'id_token54  let claims: {55    sub?: string;56    email?: string;57    email_verified?: boolean;58    name?: string;59    picture?: string;60  };61  try {62    const { payload } = await jwtVerify(tokens.id_token, GOOGLE_JWKS, {63      issuer: ["https://accounts.google.com", "accounts.google.com"],64      audience: clientId,65    });66    claims = payload as typeof claims;67  } catch {68    return fail("id-token-invalide");69  }70  if (!claims.sub || !claims.email || claims.email_verified === false)71    return fail("profil-google-incomplet");7273  const email = claims.email.toLowerCase();74  const name = claims.name ?? email.split("@")[0];75  const picture = claims.picture ?? null;7677  // Upsert : par google_sub, sinon liaison par courriel, sinon création.78  let user = findUserByGoogleSub(claims.sub);79  if (user) {80    db.prepare("UPDATE users SET name = ?, avatar_url = ? WHERE id = ?").run(81      name,82      picture,83      user.id,84    );85  } else {86    const byEmail = findUserByEmail(email);87    if (byEmail) {88      db.prepare(89        "UPDATE users SET google_sub = ?, avatar_url = COALESCE(?, avatar_url) WHERE id = ?",90      ).run(claims.sub, picture, byEmail.id);91      user = { ...byEmail, google_sub: claims.sub };92    } else {93      const info = db94        .prepare(95          "INSERT INTO users (email, name, avatar_url, google_sub) VALUES (?, ?, ?, ?)",96        )97        .run(email, name, picture, claims.sub);98      user = findUserByGoogleSub(claims.sub)!;99      ensureKaId(user.id);100      void info;101    }102  }103104  ensureKaId(user!.id);105  touchLastLogin(user!.id);106  const res = NextResponse.redirect(new URL(next, BASE_URL));107  res.cookies.set(108    SESSION_COOKIE,109    await mintSessionToken(user!.id),110    sessionCookieOptions,111  );112  res.cookies.set("ka_oauth_state", "", { path: "/", maxAge: 0 });113  res.cookies.set("ka_oauth_next", "", { path: "/", maxAge: 0 });114  return res;115}116