feat(api): install + configure bouncer, mail, limiter, drive, bullmq
Stack backend complète selon docs/tech/backend.md §2 : - @adonisjs/bouncer : configure standard, middleware initialize_bouncer simplifié (API JSON-only, pas d'Edge views). - @adonisjs/limiter : store Redis par défaut, throttler global défini dans start/limiter.ts. - @adonisjs/mail : transports SMTP (Mailpit en dev) + Resend (prod). - @adonisjs/drive : services fs (fallback) + S3 (MinIO en dev, prod plus tard). - bullmq + ioredis : config queue.ts définit la connection Redis et la liste des queues (ocr, relances, checkins, kpis). Worker à câbler dans le commit suivant. - @aws-sdk/client-s3 + s3-request-presigner pour le driver flydrive S3. Pas de @rlanz/bull-queue : peer Adonis 6.5, plus maintenu — on consomme BullMQ directement.
This commit is contained in:
parent
a8c7ab539a
commit
274f2a8270
@ -68,3 +68,4 @@ ACCESS_TOKEN_TTL_MINUTES=30
|
||||
REFRESH_TOKEN_TTL_DAYS=30
|
||||
COOKIE_DOMAIN=
|
||||
COOKIE_SECURE=false
|
||||
LIMITER_STORE=redis
|
||||
@ -1,6 +1,7 @@
|
||||
import { indexEntities } from '@adonisjs/core'
|
||||
import { defineConfig } from '@adonisjs/core/app'
|
||||
import { generateRegistry } from '@tuyau/core/hooks'
|
||||
import { indexPolicies } from '@adonisjs/bouncer'
|
||||
|
||||
export default defineConfig({
|
||||
/*
|
||||
@ -28,6 +29,7 @@ export default defineConfig({
|
||||
() => import('@adonisjs/core/commands'),
|
||||
() => import('@adonisjs/lucid/commands'),
|
||||
() => import('@adonisjs/session/commands'),
|
||||
() => import('@adonisjs/bouncer/commands')
|
||||
],
|
||||
|
||||
/*
|
||||
@ -53,6 +55,10 @@ export default defineConfig({
|
||||
() => import('@adonisjs/cors/cors_provider'),
|
||||
() => import('@adonisjs/auth/auth_provider'),
|
||||
() => import('#providers/api_provider'),
|
||||
() => import('@adonisjs/bouncer/bouncer_provider'),
|
||||
() => import('@adonisjs/limiter/limiter_provider'),
|
||||
() => import('@adonisjs/mail/mail_provider'),
|
||||
() => import('@adonisjs/drive/drive_provider'),
|
||||
],
|
||||
|
||||
/*
|
||||
@ -111,6 +117,7 @@ export default defineConfig({
|
||||
transformers: { enabled: true },
|
||||
}),
|
||||
generateRegistry(),
|
||||
indexPolicies()
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
23
apps/api/app/abilities/main.ts
Normal file
23
apps/api/app/abilities/main.ts
Normal file
@ -0,0 +1,23 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Bouncer abilities
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You may export multiple abilities from this file and pre-register them
|
||||
| when creating the Bouncer instance.
|
||||
|
|
||||
| Pre-registered policies and abilities can be referenced as a string by their
|
||||
| name. Also they are must if want to perform authorization inside Edge
|
||||
| templates.
|
||||
|
|
||||
*/
|
||||
|
||||
import { Bouncer } from '@adonisjs/bouncer'
|
||||
|
||||
/**
|
||||
* Delete the following ability to start from
|
||||
* scratch
|
||||
*/
|
||||
export const editUser = Bouncer.ability(() => {
|
||||
return true
|
||||
})
|
||||
37
apps/api/app/middleware/initialize_bouncer_middleware.ts
Normal file
37
apps/api/app/middleware/initialize_bouncer_middleware.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import * as abilities from '#abilities/main'
|
||||
import { policies } from '#generated/policies'
|
||||
|
||||
import { Bouncer } from '@adonisjs/bouncer'
|
||||
import type { HttpContext } from '@adonisjs/core/http'
|
||||
import type { NextFn } from '@adonisjs/core/types/http'
|
||||
|
||||
/**
|
||||
* Init bouncer middleware is used to create a bouncer instance
|
||||
* during an HTTP request
|
||||
*/
|
||||
export default class InitializeBouncerMiddleware {
|
||||
async handle(ctx: HttpContext, next: NextFn) {
|
||||
/**
|
||||
* Create bouncer instance for the ongoing HTTP request.
|
||||
* We will pull the user from the HTTP context.
|
||||
*/
|
||||
ctx.bouncer = new Bouncer(
|
||||
() => ctx.auth.user || null,
|
||||
abilities,
|
||||
policies
|
||||
).setContainerResolver(ctx.containerResolver)
|
||||
|
||||
// API JSON-only : pas d'intégration Edge views à partager.
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@adonisjs/core/http' {
|
||||
export interface HttpContext {
|
||||
bouncer: Bouncer<
|
||||
Exclude<HttpContext['auth']['user'], undefined>,
|
||||
typeof abilities,
|
||||
typeof policies
|
||||
>
|
||||
}
|
||||
}
|
||||
39
apps/api/config/drive.ts
Normal file
39
apps/api/config/drive.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { defineConfig, services } from '@adonisjs/drive'
|
||||
import type { InferDriveDisks } from '@adonisjs/drive/types'
|
||||
import env from '#start/env'
|
||||
|
||||
const driveConfig = defineConfig({
|
||||
default: env.get('DRIVE_DISK', 's3'),
|
||||
|
||||
/**
|
||||
* Stockage local (filesystem) — utilisé en fallback si MinIO indisponible.
|
||||
* Bucket par défaut : storage/uploads (ignoré par git).
|
||||
*/
|
||||
services: {
|
||||
fs: services.fs({
|
||||
location: 'storage/uploads',
|
||||
visibility: 'private',
|
||||
}),
|
||||
|
||||
/**
|
||||
* MinIO via le driver S3 (S3-compatible).
|
||||
*/
|
||||
s3: services.s3({
|
||||
credentials: {
|
||||
accessKeyId: env.get('S3_ACCESS_KEY', ''),
|
||||
secretAccessKey: env.get('S3_SECRET_KEY', ''),
|
||||
},
|
||||
endpoint: env.get('S3_ENDPOINT'),
|
||||
region: env.get('S3_REGION', 'fr-par'),
|
||||
bucket: env.get('S3_BUCKET', 'rubis-invoices'),
|
||||
forcePathStyle: env.get('S3_FORCE_PATH_STYLE', true),
|
||||
visibility: 'private',
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export default driveConfig
|
||||
|
||||
declare module '@adonisjs/drive/types' {
|
||||
export interface DriveDisks extends InferDriveDisks<typeof driveConfig> {}
|
||||
}
|
||||
31
apps/api/config/limiter.ts
Normal file
31
apps/api/config/limiter.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import env from '#start/env'
|
||||
import { defineConfig, stores } from '@adonisjs/limiter'
|
||||
import type { InferLimiters } from '@adonisjs/limiter/types'
|
||||
|
||||
const limiterConfig = defineConfig({
|
||||
default: env.get('LIMITER_STORE'),
|
||||
stores: {
|
||||
|
||||
/**
|
||||
* Redis store to save rate limiting data inside a
|
||||
* redis database.
|
||||
*
|
||||
* It is recommended to use a separate database for
|
||||
* the limiter connection.
|
||||
*/
|
||||
redis: stores.redis({}),
|
||||
|
||||
|
||||
/**
|
||||
* Memory store could be used during
|
||||
* testing
|
||||
*/
|
||||
memory: stores.memory({})
|
||||
},
|
||||
})
|
||||
|
||||
export default limiterConfig
|
||||
|
||||
declare module '@adonisjs/limiter/types' {
|
||||
export interface LimitersList extends InferLimiters<typeof limiterConfig> {}
|
||||
}
|
||||
46
apps/api/config/mail.ts
Normal file
46
apps/api/config/mail.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import env from '#start/env'
|
||||
import { defineConfig, transports } from '@adonisjs/mail'
|
||||
import type { InferMailers } from '@adonisjs/mail/types'
|
||||
|
||||
const mailConfig = defineConfig({
|
||||
default: env.get('MAIL_DRIVER', 'smtp'),
|
||||
|
||||
from: {
|
||||
address: env.get('MAIL_FROM_ADDRESS', 'relances@rubis-sur-l-ongle.fr'),
|
||||
name: env.get('MAIL_FROM_NAME', "Rubis Sur l'Ongle"),
|
||||
},
|
||||
|
||||
/**
|
||||
* Variables partagées par tous les templates Edge (logo, URL de base…).
|
||||
*/
|
||||
globals: {
|
||||
brandName: "Rubis Sur l'Ongle",
|
||||
appUrl: env.get('APP_URL'),
|
||||
},
|
||||
|
||||
mailers: {
|
||||
/**
|
||||
* SMTP — Mailpit en dev (catch-all sur localhost:1025), n'importe quel
|
||||
* relais SMTP en prod si on ne veut pas de provider tiers.
|
||||
*/
|
||||
smtp: transports.smtp({
|
||||
host: env.get('SMTP_HOST', 'localhost'),
|
||||
port: env.get('SMTP_PORT', 1025),
|
||||
// Auth optionnelle — pas requise pour Mailpit
|
||||
}),
|
||||
|
||||
/**
|
||||
* Resend — provider transactionnel par défaut en prod (cf. ADR-021).
|
||||
*/
|
||||
resend: transports.resend({
|
||||
key: env.get('RESEND_API_KEY', ''),
|
||||
baseUrl: 'https://api.resend.com',
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export default mailConfig
|
||||
|
||||
declare module '@adonisjs/mail/types' {
|
||||
export interface MailersList extends InferMailers<typeof mailConfig> {}
|
||||
}
|
||||
29
apps/api/config/queue.ts
Normal file
29
apps/api/config/queue.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import env from '#start/env'
|
||||
import { type RedisOptions } from 'ioredis'
|
||||
|
||||
/**
|
||||
* Connexion Redis partagée pour BullMQ. On garde un objet d'options
|
||||
* (et pas une instance) parce que BullMQ instancie ses propres clients
|
||||
* pour chaque queue/worker.
|
||||
*/
|
||||
export const redisConnection: RedisOptions = {
|
||||
host: env.get('REDIS_HOST', 'localhost'),
|
||||
port: env.get('REDIS_PORT', 6379),
|
||||
password: env.get('REDIS_PASSWORD') || undefined,
|
||||
// Requis par BullMQ pour les blocking commands.
|
||||
maxRetriesPerRequest: null,
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste des queues. La concurrence est appliquée côté worker.
|
||||
* Ajouter une queue ici → ajouter un Worker correspondant dans #start/queue.ts.
|
||||
*/
|
||||
export const queueNames = ['ocr', 'relances', 'checkins', 'kpis'] as const
|
||||
export type QueueName = (typeof queueNames)[number]
|
||||
|
||||
export const queueConcurrency: Record<QueueName, number> = {
|
||||
ocr: 2,
|
||||
relances: 5,
|
||||
checkins: 5,
|
||||
kpis: 1,
|
||||
}
|
||||
@ -58,15 +58,23 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@adonisjs/auth": "^10.1.0",
|
||||
"@adonisjs/bouncer": "^4.0.0",
|
||||
"@adonisjs/core": "^7.3.1",
|
||||
"@adonisjs/cors": "^3.0.0",
|
||||
"@adonisjs/drive": "^4.0.0",
|
||||
"@adonisjs/limiter": "^3.0.1",
|
||||
"@adonisjs/lucid": "^22.4.2",
|
||||
"@adonisjs/mail": "^10.2.0",
|
||||
"@adonisjs/session": "^8.1.0",
|
||||
"@adonisjs/shield": "^9.0.0",
|
||||
"@aws-sdk/client-s3": "^3.1043.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.1043.0",
|
||||
"@japa/api-client": "^3.2.1",
|
||||
"@tuyau/core": "^1.2.2",
|
||||
"@vinejs/vine": "^4.3.1",
|
||||
"better-sqlite3": "^12.9.0",
|
||||
"bullmq": "^5.76.5",
|
||||
"ioredis": "^5.10.1",
|
||||
"luxon": "^3.7.2",
|
||||
"pg": "^8.20.0",
|
||||
"reflect-metadata": "^0.2.2"
|
||||
|
||||
@ -64,4 +64,11 @@ export default await Env.create(new URL('../', import.meta.url), {
|
||||
REFRESH_TOKEN_TTL_DAYS: Env.schema.number.optional(),
|
||||
COOKIE_DOMAIN: Env.schema.string.optional(),
|
||||
COOKIE_SECURE: Env.schema.boolean.optional(),
|
||||
|
||||
/*
|
||||
|----------------------------------------------------------
|
||||
| Variables for configuring the limiter package
|
||||
|----------------------------------------------------------
|
||||
*/
|
||||
LIMITER_STORE: Env.schema.enum(['redis', 'memory'] as const)
|
||||
})
|
||||
|
||||
@ -38,6 +38,7 @@ router.use([
|
||||
() => import('@adonisjs/shield/shield_middleware'),
|
||||
() => import('@adonisjs/auth/initialize_auth_middleware'),
|
||||
() => import('#middleware/silent_auth_middleware'),
|
||||
() => import('#middleware/initialize_bouncer_middleware')
|
||||
])
|
||||
|
||||
/**
|
||||
|
||||
16
apps/api/start/limiter.ts
Normal file
16
apps/api/start/limiter.ts
Normal file
@ -0,0 +1,16 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Define HTTP limiters
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The "limiter.define" method creates an HTTP middleware to apply rate
|
||||
| limits on a route or a group of routes. Feel free to define as many
|
||||
| throttle middleware as needed.
|
||||
|
|
||||
*/
|
||||
|
||||
import limiter from '@adonisjs/limiter/services/main'
|
||||
|
||||
export const throttle = limiter.define('global', () => {
|
||||
return limiter.allowRequests(10).every('1 minute')
|
||||
})
|
||||
1617
pnpm-lock.yaml
generated
1617
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user