Compare commits
5 Commits
70ca3fc5de
...
5e7e7b69e9
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e7e7b69e9 | |||
| 928fb55dea | |||
| 915813d681 | |||
| ba68ef4695 | |||
| fd688f2ff3 |
129
package-lock.json
generated
129
package-lock.json
generated
@@ -9,14 +9,19 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@fastify/cookie": "^11.0.2",
|
||||
"@prisma/adapter-pg": "^7.6.0",
|
||||
"@prisma/client": "^7.6.0",
|
||||
"argon2": "^0.44.0",
|
||||
"fastify": "^5.8.4",
|
||||
"fastify-plugin": "^5.1.0",
|
||||
"pg": "^8.20.0"
|
||||
"nodemailer": "^8.0.4",
|
||||
"pg": "^8.20.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/nodemailer": "^7.0.11",
|
||||
"prisma": "^7.6.0",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^6.0.2"
|
||||
@@ -75,6 +80,12 @@
|
||||
"@electric-sql/pglite": "0.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@epic-web/invariant": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
|
||||
"integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
|
||||
@@ -538,6 +549,26 @@
|
||||
"fast-uri": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/cookie": {
|
||||
"version": "11.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-11.0.2.tgz",
|
||||
"integrity": "sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.0",
|
||||
"fastify-plugin": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/error": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz",
|
||||
@@ -648,6 +679,15 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@phc/format": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
|
||||
"integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@pinojs/redact": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
|
||||
@@ -1027,6 +1067,16 @@
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/nodemailer": {
|
||||
"version": "7.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.11.tgz",
|
||||
"integrity": "sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/pg": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz",
|
||||
@@ -1088,6 +1138,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/argon2": {
|
||||
"version": "0.44.0",
|
||||
"resolved": "https://registry.npmjs.org/argon2/-/argon2-0.44.0.tgz",
|
||||
"integrity": "sha512-zHPGN3S55sihSQo0dBbK0A5qpi2R31z7HZDZnry3ifOyj8bZZnpZND2gpmhnRGO1V/d555RwBqIK5W4Mrmv3ig==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@phc/format": "^1.0.0",
|
||||
"cross-env": "^10.0.0",
|
||||
"node-addon-api": "^8.5.0",
|
||||
"node-gyp-build": "^4.8.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/atomic-sleep": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
|
||||
@@ -1238,11 +1304,27 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-env": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz",
|
||||
"integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@epic-web/invariant": "^1.0.0",
|
||||
"cross-spawn": "^7.0.6"
|
||||
},
|
||||
"bin": {
|
||||
"cross-env": "dist/bin/cross-env.js",
|
||||
"cross-env-shell": "dist/bin/cross-env-shell.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
@@ -1711,7 +1793,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"devOptional": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
@@ -1843,6 +1924,15 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz",
|
||||
"integrity": "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18 || ^20 || >= 21"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch-native": {
|
||||
"version": "1.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
|
||||
@@ -1850,6 +1940,26 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"node-gyp-build": "bin.js",
|
||||
"node-gyp-build-optional": "optional.js",
|
||||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "8.0.4",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.4.tgz",
|
||||
"integrity": "sha512-k+jf6N8PfQJ0Fe8ZhJlgqU5qJU44Lpvp2yvidH3vp1lPnVQMgi4yEEMPXg5eJS1gFIJTVq1NHBk7Ia9ARdSBdQ==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nypm": {
|
||||
"version": "0.6.5",
|
||||
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz",
|
||||
@@ -1895,7 +2005,6 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -2427,7 +2536,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
@@ -2440,7 +2548,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -2591,7 +2698,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"devOptional": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
@@ -2622,6 +2728,15 @@
|
||||
"grammex": "^3.1.11",
|
||||
"graphmatch": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,14 +16,19 @@
|
||||
"author": "Raffi",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@fastify/cookie": "^11.0.2",
|
||||
"@prisma/adapter-pg": "^7.6.0",
|
||||
"@prisma/client": "^7.6.0",
|
||||
"argon2": "^0.44.0",
|
||||
"fastify": "^5.8.4",
|
||||
"fastify-plugin": "^5.1.0",
|
||||
"pg": "^8.20.0"
|
||||
"nodemailer": "^8.0.4",
|
||||
"pg": "^8.20.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/nodemailer": "^7.0.11",
|
||||
"prisma": "^7.6.0",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^6.0.2"
|
||||
|
||||
11
src/app.ts
11
src/app.ts
@@ -1,11 +1,20 @@
|
||||
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'
|
||||
import errorHandler from './plugins/errorHandler.js'
|
||||
|
||||
export default function buildApp() {
|
||||
const app = Fastify({ logger: true })
|
||||
|
||||
app.register(errorHandler)
|
||||
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' }))
|
||||
|
||||
18
src/errors/AppError.ts
Normal file
18
src/errors/AppError.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export class AppError extends Error {
|
||||
constructor(
|
||||
public readonly code: string,
|
||||
public readonly statusCode: number,
|
||||
message: string
|
||||
) {
|
||||
super(message)
|
||||
this.name = 'AppError'
|
||||
}
|
||||
}
|
||||
|
||||
// Erreurs prédéfinies
|
||||
export const Errors = {
|
||||
EMAIL_TAKEN: new AppError('EMAIL_TAKEN', 409, 'Cette adresse email est déjà utilisée.'),
|
||||
PASSWORD_TOO_WEAK: new AppError('PASSWORD_TOO_WEAK', 400, 'Le mot de passe doit contenir au moins 8 caractères.'),
|
||||
INVALID_CREDENTIALS: new AppError('INVALID_CREDENTIALS', 401, 'Email ou mot de passe incorrect.'),
|
||||
VALIDATION_ERROR: (message: string) => new AppError('VALIDATION_ERROR', 400, message),
|
||||
}
|
||||
34
src/plugins/errorHandler.ts
Normal file
34
src/plugins/errorHandler.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import fp from 'fastify-plugin'
|
||||
import { FastifyInstance } from 'fastify'
|
||||
import { ZodError } from 'zod'
|
||||
import { AppError } from '../errors/AppError.js'
|
||||
|
||||
export default fp(async (fastify: FastifyInstance) => {
|
||||
fastify.setErrorHandler((error, request, reply) => {
|
||||
// Erreur Zod
|
||||
if (error instanceof ZodError) {
|
||||
return reply.status(400).send({
|
||||
error: 'VALIDATION_ERROR',
|
||||
details: error.issues.map((e) => ({
|
||||
field: e.path.join('.'),
|
||||
message: e.message,
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
// Erreur métier
|
||||
if (error instanceof AppError) {
|
||||
return reply.status(error.statusCode).send({
|
||||
error: error.code,
|
||||
message: error.message,
|
||||
})
|
||||
}
|
||||
|
||||
// Erreur inconnue
|
||||
fastify.log.error(error)
|
||||
return reply.status(500).send({
|
||||
error: 'INTERNAL_ERROR',
|
||||
message: 'Une erreur interne est survenue.',
|
||||
})
|
||||
})
|
||||
})
|
||||
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)
|
||||
})
|
||||
40
src/routes/auth.ts
Normal file
40
src/routes/auth.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { FastifyInstance } from 'fastify'
|
||||
import { RegisterSchema } from '../schemas/auth.schema.js'
|
||||
import { registerUser } from '../services/auth.service.js'
|
||||
|
||||
import { LoginSchema } from '../schemas/auth.schema.js'
|
||||
import { loginUser } from '../services/auth.service.js'
|
||||
|
||||
export default async function authRoutes(fastify: FastifyInstance) {
|
||||
fastify.post('/auth/register', async (request, reply) => {
|
||||
const body = RegisterSchema.parse(request.body) // Zod throw → handler global
|
||||
|
||||
const { user, authToken } = await registerUser(
|
||||
fastify.prisma,
|
||||
fastify.mailer,
|
||||
body
|
||||
)
|
||||
|
||||
reply.setCookie('authToken', authToken, {
|
||||
httpOnly: true,
|
||||
sameSite: 'strict',
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
})
|
||||
|
||||
return reply.status(201).send({ user })
|
||||
})
|
||||
|
||||
fastify.post('/auth/login', async (request, reply) => {
|
||||
const body = LoginSchema.parse(request.body)
|
||||
|
||||
const { user, authToken } = await loginUser(fastify.prisma, body)
|
||||
|
||||
reply.setCookie('authToken', authToken, {
|
||||
httpOnly: true,
|
||||
sameSite: 'strict',
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
})
|
||||
|
||||
return reply.status(200).send({ user })
|
||||
})
|
||||
}
|
||||
21
src/schemas/auth.schema.ts
Normal file
21
src/schemas/auth.schema.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const RegisterSchema = z.object({
|
||||
email: z.email({ error: 'Adresse email invalide.' }),
|
||||
password: z
|
||||
.string()
|
||||
.regex(/^.{8,22}$/, { error: 'Le mot de passe doit contenir entre 8 et 22 caractères.' })
|
||||
.regex(/[^A-Za-z0-9]/, { error: 'Le mot de passe doit contenir au moins un caractère spécial.' })
|
||||
.regex(/[A-Z]/, { error: 'Le mot de passe doit contenir au moins une majuscule.' })
|
||||
.regex(/[a-z]/, { error: 'Le mot de passe doit contenir au moins une minuscule.' })
|
||||
.regex(/\d/, { error: 'Le mot de passe doit contenir au moins un chiffre.' }),
|
||||
})
|
||||
|
||||
export type RegisterInput = z.infer<typeof RegisterSchema>
|
||||
|
||||
export const LoginSchema = z.object({
|
||||
email: z.email({ error: 'Adresse email invalide.' }),
|
||||
password: z.string().min(1, { error: 'Le mot de passe est requis.' }),
|
||||
})
|
||||
|
||||
export type LoginInput = z.infer<typeof LoginSchema>
|
||||
113
src/services/auth.service.ts
Normal file
113
src/services/auth.service.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import argon2 from 'argon2'
|
||||
import crypto from 'crypto'
|
||||
import { PrismaClient } from '../generated/prisma/client.js'
|
||||
import { Transporter } from 'nodemailer'
|
||||
import { RegisterInput, LoginInput } from '../schemas/auth.schema.js'
|
||||
import { generateToken, generateAuthTokenExpiry, generateActionTokenExpiry } from './token.service.js'
|
||||
import { sendConfirmationMail } from './mail.service.js'
|
||||
import { Errors } from '../errors/AppError.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 Errors.EMAIL_TAKEN }
|
||||
|
||||
// 2. Hash du mot de passe
|
||||
const passwordHash = await argon2.hash(input.password)
|
||||
|
||||
// 3. Création de l'user
|
||||
const displayName = input.email.split('@')[0]
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
id: crypto.randomUUID(),
|
||||
email: input.email,
|
||||
passwordHash,
|
||||
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 }
|
||||
}
|
||||
|
||||
export async function loginUser(
|
||||
prisma: PrismaClient,
|
||||
input: LoginInput
|
||||
) {
|
||||
// 1. Vérif user existe
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: input.email },
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
throw Errors.INVALID_CREDENTIALS
|
||||
}
|
||||
|
||||
// 2. Vérif password
|
||||
const valid = await argon2.verify(user.passwordHash, input.password)
|
||||
if (!valid) {
|
||||
throw Errors.INVALID_CREDENTIALS
|
||||
}
|
||||
|
||||
// 3. Création AuthToken
|
||||
const authToken = generateToken()
|
||||
await prisma.authToken.create({
|
||||
data: {
|
||||
id: crypto.randomUUID(),
|
||||
userId: user.id,
|
||||
token: authToken,
|
||||
expiresAt: generateAuthTokenExpiry(),
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
displayName: user.displayName,
|
||||
isConfirmed: user.isConfirmed,
|
||||
createdAt: user.createdAt,
|
||||
},
|
||||
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