From 510394269878d27b76ca969cc02feda0fc85fc7f Mon Sep 17 00:00:00 2001 From: Jules Neny Date: Wed, 6 May 2026 15:56:19 +0200 Subject: [PATCH] feat(codev): M1 - NocoDB table schema + 3 endpoints API + runtimeConfig --- nuxt.config.ts | 3 ++ server/api/codev/auth.post.ts | 31 ++++++++++++++++ server/api/codev/fiches.get.ts | 31 ++++++++++++++++ server/api/codev/fiches.post.ts | 63 +++++++++++++++++++++++++++++++++ types/codev.ts | 18 ++++++++++ 5 files changed, 146 insertions(+) create mode 100644 server/api/codev/auth.post.ts create mode 100644 server/api/codev/fiches.get.ts create mode 100644 server/api/codev/fiches.post.ts create mode 100644 types/codev.ts diff --git a/nuxt.config.ts b/nuxt.config.ts index 9c62d00..f91f43a 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -14,6 +14,9 @@ export default defineNuxtConfig({ redisUrl: process.env.REDIS_URL || 'redis://127.0.0.1:6379', resendApiKey: process.env.RESEND_API_KEY, emailJules: process.env.EMAIL_JULES || 'jules@trans-former.fr', + codevTableId: '', // NUXT_CODEV_TABLE_ID + codevPassword: 'merci', // NUXT_CODEV_PASSWORD - défaut "merci", overridable + codevBaseId: '', // NUXT_CODEV_BASE_ID - base NocoDB (ex: pipilvsi7dibo80) }, // Leaflet ne fonctionne pas en SSR — forcer le rendu côté client diff --git a/server/api/codev/auth.post.ts b/server/api/codev/auth.post.ts new file mode 100644 index 0000000..f947db8 --- /dev/null +++ b/server/api/codev/auth.post.ts @@ -0,0 +1,31 @@ +import { z } from 'zod' + +const AuthSchema = z.object({ + password: z.string().min(1).max(100), +}) + +export default defineEventHandler(async (event) => { + const body = await readBody(event) + const parsed = AuthSchema.safeParse(body) + + if (!parsed.success) { + throw createError({ statusCode: 422, statusMessage: 'Mot de passe invalide' }) + } + + const config = useRuntimeConfig() + const expected = config.codevPassword || 'merci' + + if (parsed.data.password.trim().toLowerCase() !== expected.trim().toLowerCase()) { + throw createError({ statusCode: 401, statusMessage: 'Mauvais mot de passe' }) + } + + setCookie(event, 'codev_session', 'ok', { + httpOnly: true, + sameSite: 'lax', + secure: process.env.NODE_ENV === 'production', + maxAge: 60 * 60 * 24, // 24h + path: '/', + }) + + return { status: 200, ok: true } +}) diff --git a/server/api/codev/fiches.get.ts b/server/api/codev/fiches.get.ts new file mode 100644 index 0000000..cd65f37 --- /dev/null +++ b/server/api/codev/fiches.get.ts @@ -0,0 +1,31 @@ +import type { CodevFiche } from '~/types/codev' + +export default defineEventHandler(async (event): Promise<{ list: CodevFiche[] }> => { + const config = useRuntimeConfig() + const tableId = config.codevTableId + + if (!tableId) { + throw createError({ statusCode: 500, message: 'codevTableId non configuré' }) + } + + const url = `${config.nocodbUrl}/api/v2/tables/${tableId}/records?sort=created_at&limit=200` + + const data: any = await $fetch(url, { + headers: { 'xc-token': config.nocodbToken }, + }).catch(() => ({ list: [] })) + + // Mapper chaque record NocoDB vers CodevFiche + const list: CodevFiche[] = (data?.list ?? []).map((r: any) => ({ + id: r.Id ?? r.id, + nom: r.nom || '', + besoin: r.besoin || '', + offre: r.offre || '', + hashtags: (r.hashtags || '') + .split(',') + .map((h: string) => h.trim().toLowerCase().replace(/^#/, '')) + .filter(Boolean), + created_at: r.created_at || r.CreatedAt || new Date().toISOString(), + })) + + return { list } +}) diff --git a/server/api/codev/fiches.post.ts b/server/api/codev/fiches.post.ts new file mode 100644 index 0000000..be1d3ce --- /dev/null +++ b/server/api/codev/fiches.post.ts @@ -0,0 +1,63 @@ +import { z } from 'zod' + +const FicheSchema = z.object({ + nom: z.string().min(2).max(50).trim(), + besoin: z.string().min(5).max(300).trim(), + offre: z.string().min(5).max(300).trim(), + hashtags: z.array(z.string().max(30)).max(3).default([]), +}) + +export default defineEventHandler(async (event) => { + const body = await readBody(event) + const parsed = FicheSchema.safeParse(body) + + if (!parsed.success) { + throw createError({ + statusCode: 422, + statusMessage: 'Validation échouée', + data: parsed.error.flatten().fieldErrors, + }) + } + + const config = useRuntimeConfig() + const tableId = config.codevTableId + const baseId = config.codevBaseId || 'pipilvsi7dibo80' + + const payload = { + nom: parsed.data.nom, + besoin: parsed.data.besoin, + offre: parsed.data.offre, + hashtags: parsed.data.hashtags + .map((h) => h.trim().toLowerCase().replace(/^#/, '')) + .filter(Boolean) + .slice(0, 3) + .join(','), + created_at: new Date().toISOString(), + } + + // NocoDB v1 endpoint pour INSERT (cf. submit/index.post.ts pour le pattern) + const insertUrl = `${config.nocodbUrl}/api/v1/db/data/noco/${baseId}/${tableId}` + + let inserted: any + try { + inserted = await $fetch(insertUrl, { + method: 'POST', + headers: { + 'xc-token': config.nocodbToken, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }) + } catch (e: any) { + console.error('[codev/fiches.post] NocoDB insert error:', e?.message ?? e) + throw createError({ + statusCode: 502, + statusMessage: 'Erreur serveur, réessaie', + }) + } + + return { + status: 201, + id: inserted?.Id ?? inserted?.id ?? null, + } +}) diff --git a/types/codev.ts b/types/codev.ts new file mode 100644 index 0000000..c2e1ef7 --- /dev/null +++ b/types/codev.ts @@ -0,0 +1,18 @@ +export interface CodevFiche { + id: number + nom: string + besoin: string + offre: string + hashtags: string[] // parsé depuis CSV NocoDB + created_at: string // ISO +} + +export interface CodevMatch { + fromId: number + toId: number + score: number // 0-1 + mode: 'solution' | 'alliance' | 'surprise' + // solution : fromId.besoin matche toId.offre (orienté) + // alliance : symétrique sur besoin + // surprise : symétrique sur offre +}