/* |-------------------------------------------------------------------------- | Routes file |-------------------------------------------------------------------------- | | Toutes les routes sont sous /api/v1/. Les groupes auth et account sont | importés depuis le contrôleur généré par Tuyau pour garantir le contrat | client typé (cf. docs/tech/backend.md §8). | */ import { middleware } from '#start/kernel' import router from '@adonisjs/core/services/router' import { controllers } from '#generated/controllers' import app from '@adonisjs/core/services/app' import { existsSync } from 'node:fs' import { resolve } from 'node:path' router .group(() => { /** * Auth — public. /refresh utilise le cookie httpOnly `rubis_refresh` * posé par signup/login pour émettre une nouvelle AuthSession. */ router .group(() => { router.post('signup', [controllers.NewAccount, 'store']).as('signup') router.post('login', [controllers.AccessTokens, 'store']).as('login') router.post('refresh', [controllers.Refresh, 'handle']).as('refresh') }) .prefix('auth') .as('auth') /** * Check-in — public (pas d'auth Bearer). Token signé en URL, * lookup hash en DB. Redirige vers le SPA avec ?checkin=... pour * que l'UI affiche un toast. */ router .group(() => { router .get(':token/paid', [controllers.Checkin, 'respondPaid']) .as('paid') router .get(':token/pending', [controllers.Checkin, 'respondPending']) .as('pending') }) .prefix('checkin') .as('checkin') /** * Compte courant — auth requise. */ router .group(() => { router.get('profile', [controllers.Profile, 'show']).as('profile.show') router.patch('profile', [controllers.Profile, 'update']).as('profile.update') router.post('logout', [controllers.AccessTokens, 'destroy']).as('logout') }) .prefix('account') .as('account') .use(middleware.auth()) /** * Organisation rattachée à l'utilisateur courant — auth requise. */ router .group(() => { router.get('me', [controllers.Organizations, 'show']).as('show') router.patch('me', [controllers.Organizations, 'update']).as('update') }) .prefix('organizations') .as('organizations') .use(middleware.auth()) /** * Clients — auth requise, scope par organization de l'utilisateur courant. */ router .group(() => { router.get('', [controllers.Clients, 'index']).as('index') router.post('', [controllers.Clients, 'store']).as('store') router.get(':id', [controllers.Clients, 'show']).as('show').where('id', router.matchers.uuid()) router.patch(':id', [controllers.Clients, 'update']).as('update').where('id', router.matchers.uuid()) }) .prefix('clients') .as('clients') .use(middleware.auth()) /** * IA — auth requise. Génération de templates de relance avec Mistral * pour le wizard de création de plan custom. */ router .group(() => { router.post('generate-relance', [controllers.Ai, 'generateRelance']).as('generate-relance') }) .prefix('ai') .as('ai') .use(middleware.auth()) /** * Plans — auth requise. Lookup par slug (stable et lisible : * /parametres/plans/standard-30j). POST = création d'un plan custom, * slug auto-généré depuis le name. */ router .group(() => { router.get('', [controllers.Plans, 'index']).as('index') router.post('', [controllers.Plans, 'store']).as('store') router.get(':slug', [controllers.Plans, 'show']).as('show') router.patch(':slug', [controllers.Plans, 'update']).as('update') }) .prefix('plans') .as('plans') .use(middleware.auth()) /** * Dashboard — auth requise. Calculs agrégés on-the-fly (pas de cache V1). */ router .group(() => { router.get('kpis', [controllers.Dashboard, 'kpis']).as('kpis') router.get('activity', [controllers.Dashboard, 'activity']).as('activity') router.get('top-late', [controllers.Dashboard, 'topLate']).as('top-late') }) .prefix('dashboard') .as('dashboard') .use(middleware.auth()) /** * Invoices — auth requise. Ordre IMPORTANT : les routes statiques * (/upload, /counts, /import-batch/...) sont déclarées AVANT /:id * sinon `:id` matche les segments littéraux. */ router .group(() => { router.get('', [controllers.Invoices, 'index']).as('index') router.post('', [controllers.Invoices, 'store']).as('store') router.get('counts', [controllers.Invoices, 'counts']).as('counts') // OCR / Import batch (cf. ImportBatchesController) router.post('upload', [controllers.ImportBatches, 'upload']).as('upload') router .get('import-batch/:id', [controllers.ImportBatches, 'show']) .as('import-batch.show') .where('id', router.matchers.uuid()) router .post('import-batch/:id/drafts/:draftId/validate', [ controllers.ImportBatches, 'validateDraft', ]) .as('import-batch.draft.validate') .where('id', router.matchers.uuid()) .where('draftId', router.matchers.uuid()) router .post('import-batch/:id/drafts/:draftId/skip', [ controllers.ImportBatches, 'skipDraft', ]) .as('import-batch.draft.skip') .where('id', router.matchers.uuid()) .where('draftId', router.matchers.uuid()) router .delete('import-batch/:id', [controllers.ImportBatches, 'destroy']) .as('import-batch.destroy') .where('id', router.matchers.uuid()) router.get(':id', [controllers.Invoices, 'show']).as('show').where('id', router.matchers.uuid()) router .post(':id/mark-paid', [controllers.Invoices, 'markPaid']) .as('mark-paid') .where('id', router.matchers.uuid()) }) .prefix('invoices') .as('invoices') .use(middleware.auth()) }) .prefix('/api/v1') /** * SPA fallback — sert `public/index.html` pour toute route non-API et non-asset * statique (le static middleware aura déjà répondu pour les vrais fichiers * via etag / 200). Permet à TanStack Router côté front de gérer son routing * sans 404 quand l'utilisateur recharge sur /factures, /plans/nouveau, etc. * * On ne match PAS /api/v1/* (gardé en 404 propre via le routeur ci-dessus). */ router.get('*', async ({ request, response }) => { if (request.url().startsWith('/api/')) { return response.status(404).json({ errors: [{ code: 'not_found', message: 'Route introuvable' }], }) } const indexPath = resolve(app.publicPath('index.html')) if (existsSync(indexPath)) { return response.download(indexPath) } // En dev sans build front, on renvoie un message clair. return response.status(503).send('SPA not built — run `pnpm --filter @rubis/web build`') })