mise en place du système d'authentification
This commit is contained in:
@@ -1,11 +1,18 @@
|
||||
import Fastify from 'fastify'
|
||||
import prismaPlugin from './plugins/prisma'
|
||||
import cookie from '@fastify/cookie'
|
||||
import prismaPlugin from './plugins/prisma.js'
|
||||
import mailerPlugin from './plugins/mailer.js'
|
||||
import authRoutes from './routes/auth.js'
|
||||
import userRoutes from './routes/users.js'
|
||||
|
||||
export default function buildApp() {
|
||||
const app = Fastify({ logger: true })
|
||||
|
||||
app.register(cookie)
|
||||
app.register(prismaPlugin)
|
||||
app.register(mailerPlugin)
|
||||
|
||||
app.register(authRoutes, { prefix: '/api' })
|
||||
app.register(userRoutes, { prefix: '/api' })
|
||||
|
||||
app.get('/health', async () => ({ status: 'ok' }))
|
||||
|
||||
22
src/plugins/mailer.ts
Normal file
22
src/plugins/mailer.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import fp from 'fastify-plugin'
|
||||
import { FastifyInstance } from 'fastify'
|
||||
import nodemailer, { Transporter } from 'nodemailer'
|
||||
|
||||
declare module 'fastify' {
|
||||
interface FastifyInstance {
|
||||
mailer: Transporter
|
||||
}
|
||||
}
|
||||
|
||||
export default fp(async (fastify: FastifyInstance) => {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST,
|
||||
port: Number(process.env.SMTP_PORT),
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS,
|
||||
},
|
||||
})
|
||||
|
||||
fastify.decorate('mailer', transporter)
|
||||
})
|
||||
41
src/routes/auth.ts
Normal file
41
src/routes/auth.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { FastifyInstance } from 'fastify'
|
||||
import { ZodError } from 'zod'
|
||||
import { RegisterSchema } from '../schemas/auth.schema.js'
|
||||
import { registerUser } from '../services/auth.service.js'
|
||||
|
||||
export default async function authRoutes(fastify: FastifyInstance) {
|
||||
fastify.post('/auth/register', async (request, reply) => {
|
||||
// Validation Zod
|
||||
let body
|
||||
try {
|
||||
body = RegisterSchema.parse(request.body)
|
||||
} catch (err) {
|
||||
if (err instanceof ZodError) {
|
||||
return reply.status(400).send({ error: 'VALIDATION_ERROR', details: err.errors })
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
// Register
|
||||
try {
|
||||
const { user, authToken } = await registerUser(
|
||||
fastify.prisma,
|
||||
fastify.mailer,
|
||||
body
|
||||
)
|
||||
|
||||
reply.setCookie('authToken', authToken, {
|
||||
httpOnly: true,
|
||||
sameSite: 'strict',
|
||||
maxAge: 60 * 60 * 24 * 7, // 7 jours en secondes
|
||||
})
|
||||
|
||||
return reply.status(201).send({ user })
|
||||
} catch (err: any) {
|
||||
if (err.message === 'EMAIL_TAKEN') {
|
||||
return reply.status(409).send({ error: 'EMAIL_TAKEN' })
|
||||
}
|
||||
throw err
|
||||
}
|
||||
})
|
||||
}
|
||||
9
src/schemas/auth.schema.ts
Normal file
9
src/schemas/auth.schema.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const RegisterSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
displayName: z.string().min(2).max(50),
|
||||
})
|
||||
|
||||
export type RegisterInput = z.infer<typeof RegisterSchema>
|
||||
70
src/services/auth.service.ts
Normal file
70
src/services/auth.service.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import argon2 from 'argon2'
|
||||
import crypto from 'crypto'
|
||||
import { PrismaClient } from '../generated/prisma/client.js'
|
||||
import { Transporter } from 'nodemailer'
|
||||
import { RegisterInput } from '../schemas/auth.schema.js'
|
||||
import { generateToken, generateAuthTokenExpiry, generateActionTokenExpiry } from './token.service.js'
|
||||
import { sendConfirmationMail } from './mail.service.js'
|
||||
|
||||
export async function registerUser(
|
||||
prisma: PrismaClient,
|
||||
mailer: Transporter,
|
||||
input: RegisterInput
|
||||
) {
|
||||
// 1. Vérif email unique
|
||||
const existing = await prisma.user.findUnique({
|
||||
where: { email: input.email },
|
||||
})
|
||||
if (existing) {
|
||||
throw new Error('EMAIL_TAKEN')
|
||||
}
|
||||
|
||||
// 2. Hash du mot de passe
|
||||
const passwordHash = await argon2.hash(input.password)
|
||||
|
||||
// 3. Création de l'user
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
id: crypto.randomUUID(),
|
||||
email: input.email,
|
||||
passwordHash,
|
||||
displayName: input.displayName,
|
||||
avatar: '',
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
displayName: true,
|
||||
isConfirmed: true,
|
||||
createdAt: true,
|
||||
},
|
||||
})
|
||||
|
||||
// 4. ActionToken pour confirmation mail
|
||||
const confirmToken = generateToken()
|
||||
await prisma.actionToken.create({
|
||||
data: {
|
||||
id: crypto.randomUUID(),
|
||||
userId: user.id,
|
||||
token: confirmToken,
|
||||
type: 'email-confirm',
|
||||
expiresAt: generateActionTokenExpiry(1440), // 24h
|
||||
},
|
||||
})
|
||||
|
||||
// 5. Envoi du mail
|
||||
await sendConfirmationMail(mailer, user.email, confirmToken)
|
||||
|
||||
// 6. AuthToken pour la persistance
|
||||
const authToken = generateToken()
|
||||
await prisma.authToken.create({
|
||||
data: {
|
||||
id: crypto.randomUUID(),
|
||||
userId: user.id,
|
||||
token: authToken,
|
||||
expiresAt: generateAuthTokenExpiry(),
|
||||
},
|
||||
})
|
||||
|
||||
return { user, authToken }
|
||||
}
|
||||
20
src/services/mail.service.ts
Normal file
20
src/services/mail.service.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Transporter } from 'nodemailer'
|
||||
|
||||
export async function sendConfirmationMail(
|
||||
mailer: Transporter,
|
||||
email: string,
|
||||
token: string
|
||||
): Promise<void> {
|
||||
const url = `${process.env.APP_URL}/auth/confirm-email?token=${token}`
|
||||
|
||||
await mailer.sendMail({
|
||||
from: process.env.MAIL_FROM,
|
||||
to: email,
|
||||
subject: 'Confirmez votre adresse mail',
|
||||
html: `
|
||||
<p>Merci de vous être inscrit !</p>
|
||||
<p>Cliquez sur ce lien pour confirmer votre adresse mail (valable 24h) :</p>
|
||||
<a href="${url}">${url}</a>
|
||||
`,
|
||||
})
|
||||
}
|
||||
17
src/services/token.service.ts
Normal file
17
src/services/token.service.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import crypto from 'crypto'
|
||||
|
||||
export function generateToken(length = 32): string {
|
||||
return crypto.randomBytes(length).toString('hex')
|
||||
}
|
||||
|
||||
export function generateAuthTokenExpiry(): Date {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() + 7)
|
||||
return date
|
||||
}
|
||||
|
||||
export function generateActionTokenExpiry(minutes = 1440): Date {
|
||||
const date = new Date()
|
||||
date.setMinutes(date.getMinutes() + minutes)
|
||||
return date
|
||||
}
|
||||
Reference in New Issue
Block a user