feat(api): domaine Organization + endpoints /organizations/me
- Migrations 'organizations' (id, name, siret, monthly_volume_bucket, rubis_count, onboarding_completed_at) + alter users (organization_id FK + signature).
- Modèle Organization avec relation hasMany Users, User étendu avec belongsTo Organization.
- Signup transactionnel : crée une org vide ('') puis l'user, puis émet le access token. Le nom de l'org reste vide tant que l'utilisateur n'a pas franchi la première étape de l'onboarding (PATCH /organizations/me).
- Réponses /auth/* alignées sur le contrat SPA AuthSession : { data: { accessToken, expiresAt, user } }. Drop passwordConfirmation (le SPA n'envoie pas ce champ).
- Endpoints :
- GET /account/profile (déjà), PATCH /account/profile (nouveau, fullName/email/signature).
- GET /organizations/me + PATCH /organizations/me (name/siret/monthlyVolumeBucket).
- Pose automatique d'onboardingCompletedAt à la première mise en place du nom de l'org — remplace l'astuce 'signature !== null' utilisée côté MSW.
- Transformers convertissent les IDs en string (pour matcher packages/shared/src/types).
- HMR boundaries élargies : transformers/validators/services se rechargent maintenant à chaud (sinon les modifs ne sont pas vues sans restart manuel).
This commit is contained in:
parent
274f2a8270
commit
eeb4ce25b8
@ -2,28 +2,41 @@ import User from '#models/user'
|
|||||||
import { loginValidator } from '#validators/user'
|
import { loginValidator } from '#validators/user'
|
||||||
import type { HttpContext } from '@adonisjs/core/http'
|
import type { HttpContext } from '@adonisjs/core/http'
|
||||||
import UserTransformer from '#transformers/user_transformer'
|
import UserTransformer from '#transformers/user_transformer'
|
||||||
|
import env from '#start/env'
|
||||||
|
import { DateTime } from 'luxon'
|
||||||
|
|
||||||
export default class AccessTokensController {
|
export default class AccessTokensController {
|
||||||
|
/**
|
||||||
|
* POST /auth/login — renvoie une AuthSession pour le SPA.
|
||||||
|
*/
|
||||||
async store({ request, serialize }: HttpContext) {
|
async store({ request, serialize }: HttpContext) {
|
||||||
const { email, password } = await request.validateUsing(loginValidator)
|
const { email, password } = await request.validateUsing(loginValidator)
|
||||||
|
|
||||||
const user = await User.verifyCredentials(email, password)
|
const user = await User.verifyCredentials(email, password)
|
||||||
const token = await User.accessTokens.create(user)
|
const accessToken = await User.accessTokens.create(user)
|
||||||
|
|
||||||
|
const ttlMin = env.get('ACCESS_TOKEN_TTL_MINUTES', 30)
|
||||||
|
const expiresAt =
|
||||||
|
accessToken.expiresAt?.toISOString() ??
|
||||||
|
DateTime.now().plus({ minutes: ttlMin }).toISO()!
|
||||||
|
|
||||||
return serialize({
|
return serialize({
|
||||||
|
accessToken: accessToken.value!.release(),
|
||||||
|
expiresAt,
|
||||||
user: UserTransformer.transform(user),
|
user: UserTransformer.transform(user),
|
||||||
token: token.value!.release(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async destroy({ auth }: HttpContext) {
|
/**
|
||||||
|
* POST /account/logout — révoque le token courant.
|
||||||
|
*/
|
||||||
|
async destroy({ auth, response }: HttpContext) {
|
||||||
const user = auth.getUserOrFail()
|
const user = auth.getUserOrFail()
|
||||||
if (user.currentAccessToken) {
|
if (user.currentAccessToken) {
|
||||||
await User.accessTokens.delete(user, user.currentAccessToken.identifier)
|
await User.accessTokens.delete(user, user.currentAccessToken.identifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
response.status(204)
|
||||||
message: 'Logged out successfully',
|
return null
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,48 @@
|
|||||||
import User from '#models/user'
|
import User from '#models/user'
|
||||||
|
import Organization from '#models/organization'
|
||||||
import { signupValidator } from '#validators/user'
|
import { signupValidator } from '#validators/user'
|
||||||
import type { HttpContext } from '@adonisjs/core/http'
|
import type { HttpContext } from '@adonisjs/core/http'
|
||||||
import UserTransformer from '#transformers/user_transformer'
|
import UserTransformer from '#transformers/user_transformer'
|
||||||
|
import db from '@adonisjs/lucid/services/db'
|
||||||
|
import env from '#start/env'
|
||||||
|
import { DateTime } from 'luxon'
|
||||||
|
|
||||||
export default class NewAccountController {
|
export default class NewAccountController {
|
||||||
async store({ request, serialize }: HttpContext) {
|
/**
|
||||||
|
* POST /auth/signup
|
||||||
|
* Crée une organisation vide + un user dans la même transaction,
|
||||||
|
* puis émet un access token et renvoie une AuthSession.
|
||||||
|
*
|
||||||
|
* Le nom de l'org reste vide ("") — c'est la première étape de
|
||||||
|
* l'onboarding qui le remplit (PATCH /organizations/me).
|
||||||
|
*/
|
||||||
|
async store({ request, response, serialize }: HttpContext) {
|
||||||
const { fullName, email, password } = await request.validateUsing(signupValidator)
|
const { fullName, email, password } = await request.validateUsing(signupValidator)
|
||||||
|
|
||||||
const user = await User.create({ fullName, email, password })
|
// org + user créés atomiquement, puis le token (qui passe par un client
|
||||||
const token = await User.accessTokens.create(user)
|
// pg séparé via DbAccessTokensProvider — il doit voir l'user commit).
|
||||||
|
const user = await db.transaction(async (trx) => {
|
||||||
|
const org = await Organization.create({ name: '' }, { client: trx })
|
||||||
|
return User.create(
|
||||||
|
{ email, password, fullName, organizationId: org.id },
|
||||||
|
{ client: trx }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const accessToken = await User.accessTokens.create(user)
|
||||||
|
|
||||||
|
const ttlMin = env.get('ACCESS_TOKEN_TTL_MINUTES', 30)
|
||||||
|
const expiresAt =
|
||||||
|
accessToken.expiresAt?.toISOString() ??
|
||||||
|
DateTime.now().plus({ minutes: ttlMin }).toISO()!
|
||||||
|
|
||||||
|
response.status(201)
|
||||||
|
// serialize() ajoute lui-même le wrap { data: ... } et unwrap les Items
|
||||||
|
// qu'il trouve aux clés directes — donc on lui passe les champs à plat.
|
||||||
return serialize({
|
return serialize({
|
||||||
|
accessToken: accessToken.value!.release(),
|
||||||
|
expiresAt,
|
||||||
user: UserTransformer.transform(user),
|
user: UserTransformer.transform(user),
|
||||||
token: token.value!.release(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
47
apps/api/app/controllers/organizations_controller.ts
Normal file
47
apps/api/app/controllers/organizations_controller.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import Organization from '#models/organization'
|
||||||
|
import OrganizationTransformer from '#transformers/organization_transformer'
|
||||||
|
import { updateOrganizationValidator } from '#validators/organization'
|
||||||
|
import type { HttpContext } from '@adonisjs/core/http'
|
||||||
|
import { Exception } from '@adonisjs/core/exceptions'
|
||||||
|
import { DateTime } from 'luxon'
|
||||||
|
|
||||||
|
export default class OrganizationsController {
|
||||||
|
/**
|
||||||
|
* GET /organizations/me — l'organisation de l'utilisateur courant.
|
||||||
|
*/
|
||||||
|
async show({ auth, serialize }: HttpContext) {
|
||||||
|
const user = auth.getUserOrFail()
|
||||||
|
if (user.organizationId === null) {
|
||||||
|
throw new Exception('Aucune organisation rattachée', { status: 404, code: 'not_found' })
|
||||||
|
}
|
||||||
|
const org = await Organization.findOrFail(user.organizationId)
|
||||||
|
return serialize(OrganizationTransformer.transform(org))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PATCH /organizations/me — onboarding step 2.
|
||||||
|
* Marque `onboardingCompletedAt` dès qu'un nom est posé pour la
|
||||||
|
* première fois (heuristique simple : pour l'instant un nom non vide
|
||||||
|
* suffit à considérer l'organisation comme "configurée").
|
||||||
|
*/
|
||||||
|
async update({ auth, request, serialize }: HttpContext) {
|
||||||
|
const user = auth.getUserOrFail()
|
||||||
|
if (user.organizationId === null) {
|
||||||
|
throw new Exception('Aucune organisation rattachée', { status: 404, code: 'not_found' })
|
||||||
|
}
|
||||||
|
const payload = await request.validateUsing(updateOrganizationValidator)
|
||||||
|
|
||||||
|
const org = await Organization.findOrFail(user.organizationId)
|
||||||
|
const wasUnnamed = org.name.trim().length === 0
|
||||||
|
|
||||||
|
org.merge(payload)
|
||||||
|
|
||||||
|
if (wasUnnamed && (payload.name?.trim().length ?? 0) > 0 && !org.onboardingCompletedAt) {
|
||||||
|
org.onboardingCompletedAt = DateTime.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
await org.save()
|
||||||
|
|
||||||
|
return serialize(OrganizationTransformer.transform(org))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,25 @@
|
|||||||
import UserTransformer from '#transformers/user_transformer'
|
import UserTransformer from '#transformers/user_transformer'
|
||||||
|
import { updateProfileValidator } from '#validators/user'
|
||||||
import type { HttpContext } from '@adonisjs/core/http'
|
import type { HttpContext } from '@adonisjs/core/http'
|
||||||
|
|
||||||
export default class ProfileController {
|
export default class ProfileController {
|
||||||
|
/**
|
||||||
|
* GET /account/profile
|
||||||
|
*/
|
||||||
async show({ auth, serialize }: HttpContext) {
|
async show({ auth, serialize }: HttpContext) {
|
||||||
return serialize(UserTransformer.transform(auth.getUserOrFail()))
|
return serialize(UserTransformer.transform(auth.getUserOrFail()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PATCH /account/profile
|
||||||
|
*/
|
||||||
|
async update({ auth, request, serialize }: HttpContext) {
|
||||||
|
const user = auth.getUserOrFail()
|
||||||
|
const payload = await request.validateUsing(updateProfileValidator)
|
||||||
|
|
||||||
|
user.merge(payload)
|
||||||
|
await user.save()
|
||||||
|
|
||||||
|
return serialize(UserTransformer.transform(user))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
9
apps/api/app/models/organization.ts
Normal file
9
apps/api/app/models/organization.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { OrganizationSchema } from '#database/schema'
|
||||||
|
import { hasMany } from '@adonisjs/lucid/orm'
|
||||||
|
import type { HasMany } from '@adonisjs/lucid/types/relations'
|
||||||
|
import User from '#models/user'
|
||||||
|
|
||||||
|
export default class Organization extends OrganizationSchema {
|
||||||
|
@hasMany(() => User)
|
||||||
|
declare users: HasMany<typeof User>
|
||||||
|
}
|
||||||
@ -3,11 +3,17 @@ import hash from '@adonisjs/core/services/hash'
|
|||||||
import { compose } from '@adonisjs/core/helpers'
|
import { compose } from '@adonisjs/core/helpers'
|
||||||
import { withAuthFinder } from '@adonisjs/auth/mixins/lucid'
|
import { withAuthFinder } from '@adonisjs/auth/mixins/lucid'
|
||||||
import { type AccessToken, DbAccessTokensProvider } from '@adonisjs/auth/access_tokens'
|
import { type AccessToken, DbAccessTokensProvider } from '@adonisjs/auth/access_tokens'
|
||||||
|
import { belongsTo } from '@adonisjs/lucid/orm'
|
||||||
|
import type { BelongsTo } from '@adonisjs/lucid/types/relations'
|
||||||
|
import Organization from '#models/organization'
|
||||||
|
|
||||||
export default class User extends compose(UserSchema, withAuthFinder(hash)) {
|
export default class User extends compose(UserSchema, withAuthFinder(hash)) {
|
||||||
static accessTokens = DbAccessTokensProvider.forModel(User)
|
static accessTokens = DbAccessTokensProvider.forModel(User)
|
||||||
declare currentAccessToken?: AccessToken
|
declare currentAccessToken?: AccessToken
|
||||||
|
|
||||||
|
@belongsTo(() => Organization)
|
||||||
|
declare organization: BelongsTo<typeof Organization>
|
||||||
|
|
||||||
get initials() {
|
get initials() {
|
||||||
const [first, last] = this.fullName ? this.fullName.split(' ') : this.email.split('@')
|
const [first, last] = this.fullName ? this.fullName.split(' ') : this.email.split('@')
|
||||||
if (first && last) {
|
if (first && last) {
|
||||||
|
|||||||
20
apps/api/app/transformers/organization_transformer.ts
Normal file
20
apps/api/app/transformers/organization_transformer.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import type Organization from '#models/organization'
|
||||||
|
import { BaseTransformer } from '@adonisjs/core/transformers'
|
||||||
|
|
||||||
|
export default class OrganizationTransformer extends BaseTransformer<Organization> {
|
||||||
|
toObject() {
|
||||||
|
const o = this.resource
|
||||||
|
return {
|
||||||
|
// IDs sérialisés en string pour rester aligné sur le contrat SPA
|
||||||
|
// (cf. packages/shared/src/types/user.ts).
|
||||||
|
id: String(o.id),
|
||||||
|
name: o.name,
|
||||||
|
siret: o.siret,
|
||||||
|
monthlyVolumeBucket: o.monthlyVolumeBucket,
|
||||||
|
rubisCount: o.rubisCount,
|
||||||
|
onboardingCompletedAt: o.onboardingCompletedAt?.toISO() ?? null,
|
||||||
|
createdAt: o.createdAt.toISO()!,
|
||||||
|
updatedAt: o.updatedAt?.toISO() ?? o.createdAt.toISO()!,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,13 +3,17 @@ import { BaseTransformer } from '@adonisjs/core/transformers'
|
|||||||
|
|
||||||
export default class UserTransformer extends BaseTransformer<User> {
|
export default class UserTransformer extends BaseTransformer<User> {
|
||||||
toObject() {
|
toObject() {
|
||||||
return this.pick(this.resource, [
|
const u = this.resource
|
||||||
'id',
|
return {
|
||||||
'fullName',
|
// IDs sérialisés en string — cf. packages/shared/src/types/user.ts
|
||||||
'email',
|
id: String(u.id),
|
||||||
'createdAt',
|
email: u.email,
|
||||||
'updatedAt',
|
fullName: u.fullName,
|
||||||
'initials',
|
organizationId: u.organizationId !== null ? String(u.organizationId) : null,
|
||||||
])
|
signature: u.signature,
|
||||||
|
initials: u.initials,
|
||||||
|
createdAt: u.createdAt.toISO()!,
|
||||||
|
updatedAt: u.updatedAt?.toISO() ?? u.createdAt.toISO()!,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
apps/api/app/validators/organization.ts
Normal file
14
apps/api/app/validators/organization.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import vine from '@vinejs/vine'
|
||||||
|
|
||||||
|
const MONTHLY_VOLUME_BUCKETS = ['moins-10', '10-50', '50-100', '100-200', 'plus-200'] as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validator pour PATCH /organizations/me. Tous les champs optionnels :
|
||||||
|
* l'utilisateur peut compléter au fil de l'onboarding.
|
||||||
|
*/
|
||||||
|
export const updateOrganizationValidator = vine.create({
|
||||||
|
name: vine.string().minLength(2).maxLength(120).optional(),
|
||||||
|
// SIRET = 14 chiffres exactement, sinon null pour réinitialiser.
|
||||||
|
siret: vine.string().regex(/^\d{14}$/).nullable().optional(),
|
||||||
|
monthlyVolumeBucket: vine.enum(MONTHLY_VOLUME_BUCKETS).nullable().optional(),
|
||||||
|
})
|
||||||
@ -4,23 +4,32 @@ import vine from '@vinejs/vine'
|
|||||||
* Shared rules for email and password.
|
* Shared rules for email and password.
|
||||||
*/
|
*/
|
||||||
const email = () => vine.string().email().maxLength(254)
|
const email = () => vine.string().email().maxLength(254)
|
||||||
const password = () => vine.string().minLength(8).maxLength(32)
|
const password = () => vine.string().minLength(8).maxLength(72)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validator to use when performing self-signup
|
* Validator pour /auth/signup. Contrat aligné sur le SPA (Zod
|
||||||
|
* `registerSchema` dans packages/shared). Pas de passwordConfirmation
|
||||||
|
* côté API : la confirmation visuelle est une affaire de formulaire.
|
||||||
*/
|
*/
|
||||||
export const signupValidator = vine.create({
|
export const signupValidator = vine.create({
|
||||||
fullName: vine.string().nullable(),
|
|
||||||
email: email().unique({ table: 'users', column: 'email' }),
|
email: email().unique({ table: 'users', column: 'email' }),
|
||||||
password: password(),
|
password: password(),
|
||||||
passwordConfirmation: password().sameAs('password'),
|
fullName: vine.string().minLength(2).maxLength(120),
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validator to use before validating user credentials
|
* Validator pour /auth/login.
|
||||||
* during login
|
|
||||||
*/
|
*/
|
||||||
export const loginValidator = vine.create({
|
export const loginValidator = vine.create({
|
||||||
email: email(),
|
email: email(),
|
||||||
password: vine.string(),
|
password: vine.string(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validator pour /account/profile (PATCH). Tous les champs optionnels.
|
||||||
|
*/
|
||||||
|
export const updateProfileValidator = vine.create({
|
||||||
|
fullName: vine.string().minLength(2).maxLength(120).optional(),
|
||||||
|
email: email().optional(),
|
||||||
|
signature: vine.string().maxLength(500).optional(),
|
||||||
|
})
|
||||||
|
|||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { BaseSchema } from '@adonisjs/lucid/schema'
|
||||||
|
|
||||||
|
export default class extends BaseSchema {
|
||||||
|
protected tableName = 'organizations'
|
||||||
|
|
||||||
|
async up() {
|
||||||
|
this.schema.createTable(this.tableName, (table) => {
|
||||||
|
table.increments('id').notNullable()
|
||||||
|
table.string('name', 120).notNullable().defaultTo('')
|
||||||
|
table.string('siret', 14).nullable()
|
||||||
|
table.string('monthly_volume_bucket', 20).nullable()
|
||||||
|
table.integer('rubis_count').notNullable().defaultTo(0)
|
||||||
|
// Onboarding terminé = non-null. Tant que c'est null, le SPA renvoie
|
||||||
|
// l'utilisateur dans le wizard (cf. docs/tech/backend.md §4.2).
|
||||||
|
table.timestamp('onboarding_completed_at').nullable()
|
||||||
|
|
||||||
|
table.timestamp('created_at').notNullable()
|
||||||
|
table.timestamp('updated_at').nullable()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async down() {
|
||||||
|
this.schema.dropTable(this.tableName)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
import { BaseSchema } from '@adonisjs/lucid/schema'
|
||||||
|
|
||||||
|
export default class extends BaseSchema {
|
||||||
|
protected tableName = 'users'
|
||||||
|
|
||||||
|
async up() {
|
||||||
|
this.schema.alterTable(this.tableName, (table) => {
|
||||||
|
// FK vers organizations. Nullable pour permettre la création d'un
|
||||||
|
// utilisateur sans org (cas très limite, surtout V1) — en pratique
|
||||||
|
// le signup crée toujours une org puis l'user dans la même tx.
|
||||||
|
table
|
||||||
|
.integer('organization_id')
|
||||||
|
.unsigned()
|
||||||
|
.nullable()
|
||||||
|
.references('id')
|
||||||
|
.inTable('organizations')
|
||||||
|
.onDelete('CASCADE')
|
||||||
|
|
||||||
|
// Signature email utilisée comme footer des relances.
|
||||||
|
table.text('signature').nullable()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async down() {
|
||||||
|
this.schema.alterTable(this.tableName, (table) => {
|
||||||
|
table.dropColumn('organization_id')
|
||||||
|
table.dropColumn('signature')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,18 +8,7 @@ import { BaseModel, column } from '@adonisjs/lucid/orm'
|
|||||||
import { DateTime } from 'luxon'
|
import { DateTime } from 'luxon'
|
||||||
|
|
||||||
export class AuthAccessTokenSchema extends BaseModel {
|
export class AuthAccessTokenSchema extends BaseModel {
|
||||||
static $columns = [
|
static $columns = ['abilities', 'createdAt', 'expiresAt', 'hash', 'id', 'lastUsedAt', 'name', 'tokenableId', 'type', 'updatedAt'] as const
|
||||||
'abilities',
|
|
||||||
'createdAt',
|
|
||||||
'expiresAt',
|
|
||||||
'hash',
|
|
||||||
'id',
|
|
||||||
'lastUsedAt',
|
|
||||||
'name',
|
|
||||||
'tokenableId',
|
|
||||||
'type',
|
|
||||||
'updatedAt',
|
|
||||||
] as const
|
|
||||||
$columns = AuthAccessTokenSchema.$columns
|
$columns = AuthAccessTokenSchema.$columns
|
||||||
@column()
|
@column()
|
||||||
declare abilities: string
|
declare abilities: string
|
||||||
@ -43,8 +32,29 @@ export class AuthAccessTokenSchema extends BaseModel {
|
|||||||
declare updatedAt: DateTime | null
|
declare updatedAt: DateTime | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class OrganizationSchema extends BaseModel {
|
||||||
|
static $columns = ['createdAt', 'id', 'monthlyVolumeBucket', 'name', 'onboardingCompletedAt', 'rubisCount', 'siret', 'updatedAt'] as const
|
||||||
|
$columns = OrganizationSchema.$columns
|
||||||
|
@column.dateTime({ autoCreate: true })
|
||||||
|
declare createdAt: DateTime
|
||||||
|
@column({ isPrimary: true })
|
||||||
|
declare id: number
|
||||||
|
@column()
|
||||||
|
declare monthlyVolumeBucket: string | null
|
||||||
|
@column()
|
||||||
|
declare name: string
|
||||||
|
@column.dateTime()
|
||||||
|
declare onboardingCompletedAt: DateTime | null
|
||||||
|
@column()
|
||||||
|
declare rubisCount: number
|
||||||
|
@column()
|
||||||
|
declare siret: string | null
|
||||||
|
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||||
|
declare updatedAt: DateTime | null
|
||||||
|
}
|
||||||
|
|
||||||
export class UserSchema extends BaseModel {
|
export class UserSchema extends BaseModel {
|
||||||
static $columns = ['createdAt', 'email', 'fullName', 'id', 'password', 'updatedAt'] as const
|
static $columns = ['createdAt', 'email', 'fullName', 'id', 'organizationId', 'password', 'signature', 'updatedAt'] as const
|
||||||
$columns = UserSchema.$columns
|
$columns = UserSchema.$columns
|
||||||
@column.dateTime({ autoCreate: true })
|
@column.dateTime({ autoCreate: true })
|
||||||
declare createdAt: DateTime
|
declare createdAt: DateTime
|
||||||
@ -54,8 +64,12 @@ export class UserSchema extends BaseModel {
|
|||||||
declare fullName: string | null
|
declare fullName: string | null
|
||||||
@column({ isPrimary: true })
|
@column({ isPrimary: true })
|
||||||
declare id: number
|
declare id: number
|
||||||
|
@column()
|
||||||
|
declare organizationId: number | null
|
||||||
@column({ serializeAs: null })
|
@column({ serializeAs: null })
|
||||||
declare password: string
|
declare password: string
|
||||||
|
@column()
|
||||||
|
declare signature: string | null
|
||||||
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||||
declare updatedAt: DateTime | null
|
declare updatedAt: DateTime | null
|
||||||
}
|
}
|
||||||
|
|||||||
@ -82,7 +82,10 @@
|
|||||||
"hotHook": {
|
"hotHook": {
|
||||||
"boundaries": [
|
"boundaries": [
|
||||||
"./app/controllers/**/*.ts",
|
"./app/controllers/**/*.ts",
|
||||||
"./app/middleware/*.ts"
|
"./app/middleware/*.ts",
|
||||||
|
"./app/transformers/**/*.ts",
|
||||||
|
"./app/validators/**/*.ts",
|
||||||
|
"./app/services/**/*.ts"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"prettier": "@adonisjs/prettier-config"
|
"prettier": "@adonisjs/prettier-config"
|
||||||
|
|||||||
@ -3,7 +3,9 @@
|
|||||||
| Routes file
|
| Routes file
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| The routes file is used for defining the HTTP routes.
|
| 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).
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -17,21 +19,40 @@ router.get('/', () => {
|
|||||||
|
|
||||||
router
|
router
|
||||||
.group(() => {
|
.group(() => {
|
||||||
|
/**
|
||||||
|
* Auth — public.
|
||||||
|
*/
|
||||||
router
|
router
|
||||||
.group(() => {
|
.group(() => {
|
||||||
router.post('signup', [controllers.NewAccount, 'store'])
|
router.post('signup', [controllers.NewAccount, 'store']).as('signup')
|
||||||
router.post('login', [controllers.AccessTokens, 'store'])
|
router.post('login', [controllers.AccessTokens, 'store']).as('login')
|
||||||
})
|
})
|
||||||
.prefix('auth')
|
.prefix('auth')
|
||||||
.as('auth')
|
.as('auth')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compte courant — auth requise.
|
||||||
|
*/
|
||||||
router
|
router
|
||||||
.group(() => {
|
.group(() => {
|
||||||
router.get('profile', [controllers.Profile, 'show'])
|
router.get('profile', [controllers.Profile, 'show']).as('profile.show')
|
||||||
router.post('logout', [controllers.AccessTokens, 'destroy'])
|
router.patch('profile', [controllers.Profile, 'update']).as('profile.update')
|
||||||
|
router.post('logout', [controllers.AccessTokens, 'destroy']).as('logout')
|
||||||
})
|
})
|
||||||
.prefix('account')
|
.prefix('account')
|
||||||
.as('profile')
|
.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())
|
.use(middleware.auth())
|
||||||
})
|
})
|
||||||
.prefix('/api/v1')
|
.prefix('/api/v1')
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user