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
|
REFRESH_TOKEN_TTL_DAYS=30
|
||||||
COOKIE_DOMAIN=
|
COOKIE_DOMAIN=
|
||||||
COOKIE_SECURE=false
|
COOKIE_SECURE=false
|
||||||
|
LIMITER_STORE=redis
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { indexEntities } from '@adonisjs/core'
|
import { indexEntities } from '@adonisjs/core'
|
||||||
import { defineConfig } from '@adonisjs/core/app'
|
import { defineConfig } from '@adonisjs/core/app'
|
||||||
import { generateRegistry } from '@tuyau/core/hooks'
|
import { generateRegistry } from '@tuyau/core/hooks'
|
||||||
|
import { indexPolicies } from '@adonisjs/bouncer'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
/*
|
/*
|
||||||
@ -28,6 +29,7 @@ export default defineConfig({
|
|||||||
() => import('@adonisjs/core/commands'),
|
() => import('@adonisjs/core/commands'),
|
||||||
() => import('@adonisjs/lucid/commands'),
|
() => import('@adonisjs/lucid/commands'),
|
||||||
() => import('@adonisjs/session/commands'),
|
() => import('@adonisjs/session/commands'),
|
||||||
|
() => import('@adonisjs/bouncer/commands')
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -53,6 +55,10 @@ export default defineConfig({
|
|||||||
() => import('@adonisjs/cors/cors_provider'),
|
() => import('@adonisjs/cors/cors_provider'),
|
||||||
() => import('@adonisjs/auth/auth_provider'),
|
() => import('@adonisjs/auth/auth_provider'),
|
||||||
() => import('#providers/api_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 },
|
transformers: { enabled: true },
|
||||||
}),
|
}),
|
||||||
generateRegistry(),
|
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": {
|
"dependencies": {
|
||||||
"@adonisjs/auth": "^10.1.0",
|
"@adonisjs/auth": "^10.1.0",
|
||||||
|
"@adonisjs/bouncer": "^4.0.0",
|
||||||
"@adonisjs/core": "^7.3.1",
|
"@adonisjs/core": "^7.3.1",
|
||||||
"@adonisjs/cors": "^3.0.0",
|
"@adonisjs/cors": "^3.0.0",
|
||||||
|
"@adonisjs/drive": "^4.0.0",
|
||||||
|
"@adonisjs/limiter": "^3.0.1",
|
||||||
"@adonisjs/lucid": "^22.4.2",
|
"@adonisjs/lucid": "^22.4.2",
|
||||||
|
"@adonisjs/mail": "^10.2.0",
|
||||||
"@adonisjs/session": "^8.1.0",
|
"@adonisjs/session": "^8.1.0",
|
||||||
"@adonisjs/shield": "^9.0.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",
|
"@japa/api-client": "^3.2.1",
|
||||||
"@tuyau/core": "^1.2.2",
|
"@tuyau/core": "^1.2.2",
|
||||||
"@vinejs/vine": "^4.3.1",
|
"@vinejs/vine": "^4.3.1",
|
||||||
"better-sqlite3": "^12.9.0",
|
"better-sqlite3": "^12.9.0",
|
||||||
|
"bullmq": "^5.76.5",
|
||||||
|
"ioredis": "^5.10.1",
|
||||||
"luxon": "^3.7.2",
|
"luxon": "^3.7.2",
|
||||||
"pg": "^8.20.0",
|
"pg": "^8.20.0",
|
||||||
"reflect-metadata": "^0.2.2"
|
"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(),
|
REFRESH_TOKEN_TTL_DAYS: Env.schema.number.optional(),
|
||||||
COOKIE_DOMAIN: Env.schema.string.optional(),
|
COOKIE_DOMAIN: Env.schema.string.optional(),
|
||||||
COOKIE_SECURE: Env.schema.boolean.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/shield/shield_middleware'),
|
||||||
() => import('@adonisjs/auth/initialize_auth_middleware'),
|
() => import('@adonisjs/auth/initialize_auth_middleware'),
|
||||||
() => import('#middleware/silent_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