Compare commits

...

4 Commits

9 changed files with 116 additions and 4 deletions

View File

@@ -22,4 +22,8 @@ export const Errors = {
INVALID_TOKEN: new AppError('INVALID_TOKEN', 400, 'Invalid or already used token'),
TOKEN_EXPIRED: new AppError('TOKEN_EXPIRED', 400, 'Token has expired'),
ALREADY_CONFIRMED: new AppError('ALREADY_CONFIRMED', 400, 'User is already confirmed'),
//Auth errors
UNAUTHORIZED: new AppError('UNAUTHORIZED', 401, 'Non authentifié'),
USER_NOT_FOUND: new AppError('USER_NOT_FOUND', 404, 'Utilisateur introuvable'),
}

View File

@@ -0,0 +1,19 @@
import { FastifyRequest, FastifyReply } from 'fastify'
import { verifyAuthToken } from '../services/authToken.service.js'
import { Errors } from '../errors/AppError.js'
export async function verifyAuth(request: FastifyRequest, reply: FastifyReply) {
const token = request.cookies['authToken']
console.log('token reçu:', token)
if (!token) throw Errors.UNAUTHORIZED
try {
const payload = await verifyAuthToken(request.server, request.server.prisma, token)
console.log('payload:', payload)
request.user = payload
} catch (e) {
console.log('erreur:', e)
throw Errors.UNAUTHORIZED
}
}

View File

@@ -1,7 +1,8 @@
import { FastifyInstance } from 'fastify'
import { RegisterSchema, LoginSchema } from '../schemas/auth.schema.js'
import { registerUser, loginUser } from '../services/auth.service.js'
import { registerUser, loginUser, logoutUser } from '../services/auth.service.js'
import { signAuthToken } from '../services/authToken.service.js'
import { verifyAuth } from '../middleware/verifyAuth.js'
export default async function authRoutes(fastify: FastifyInstance) {
fastify.post('/auth/register', async (request, reply) => {
@@ -19,6 +20,7 @@ export default async function authRoutes(fastify: FastifyInstance) {
httpOnly: true,
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 7,
path: '/',
})
return reply.status(201).send({ user })
@@ -34,8 +36,15 @@ export default async function authRoutes(fastify: FastifyInstance) {
httpOnly: true,
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 7,
path: '/',
})
return reply.status(200).send({ user })
})
fastify.post('/auth/logout', { preHandler: verifyAuth }, async (request, reply) => {
await logoutUser(fastify.prisma, request.user.userId)
reply.clearCookie('authToken', { path: '/' })
return reply.status(200).send({ message: 'Déconnecté avec succès' })
})
}

View File

@@ -1,6 +1,10 @@
import { FastifyInstance } from 'fastify'
import { Errors } from '../errors/AppError'
import { confirmEmail } from '../services/user.service'
import { verifyAuth } from '../middleware/verifyAuth'
import { UpdateDisplayNameSchema } from '../schemas/user.schema.js'
import { updateDisplayName } from '../services/user.service.js'
export default async function userRoutes(fastify: FastifyInstance) {
fastify.get('/users', async (request, reply) => {
@@ -27,6 +31,12 @@ export default async function userRoutes(fastify: FastifyInstance) {
}
const result = await confirmEmail(fastify.prisma, token)
return reply.status(200).send(result)
})
return reply.status(200).send(result)
})
fastify.patch('/user/display-name', { preHandler: verifyAuth }, async (request, reply) => {
const body = UpdateDisplayNameSchema.parse(request.body)
const user = await updateDisplayName(fastify.prisma, request.user.userId, body)
return reply.status(200).send({ user })
})
}

View File

@@ -0,0 +1,7 @@
import { z } from 'zod'
export const UpdateDisplayNameSchema = z.object({
displayName: z.string().min(2).max(32),
})
export type UpdateDisplayNameInput = z.infer<typeof UpdateDisplayNameSchema>

View File

@@ -6,6 +6,7 @@ import { RegisterInput, LoginInput } from '../schemas/auth.schema.js'
import { sendConfirmationMail } from './mail.service.js'
import { createActionToken } from './actionToken.service.js'
import { Errors } from '../errors/AppError.js'
import { generateGravatarUrl } from './avatar.service.js'
export async function registerUser(
prisma: PrismaClient,
@@ -25,12 +26,13 @@ export async function registerUser(
email: input.email,
passwordHash,
displayName,
avatar: '',
avatar: generateGravatarUrl(input.email),
},
select: {
id: true,
email: true,
displayName: true,
avatar:true,
isConfirmed: true,
createdAt: true,
},
@@ -63,9 +65,17 @@ export async function loginUser(prisma: PrismaClient, input: LoginInput) {
id: user.id,
email: user.email,
displayName: user.displayName,
avatar: user.avatar,
isConfirmed: user.isConfirmed,
createdAt: user.createdAt,
tokenVersion: user.tokenVersion,
},
}
}
export async function logoutUser(prisma: PrismaClient, userId: string) {
await prisma.user.update({
where: { id: userId },
data: { tokenVersion: { increment: 1 } },
})
}

View File

@@ -0,0 +1,10 @@
import crypto from 'crypto'
export function generateGravatarUrl(email: string, size = 200): string {
const hash = crypto
.createHash('md5')
.update(email.trim().toLowerCase())
.digest('hex')
return `https://www.gravatar.com/avatar/${hash}?s=${size}&d=identicon`
}

View File

@@ -1,5 +1,6 @@
import { PrismaClient } from '../generated/prisma/client.js'
import { Errors } from '../errors/AppError.js'
import { UpdateDisplayNameInput } from '../schemas/user.schema.js'
export async function confirmEmail(prisma: PrismaClient, token: string) {
const actionToken = await prisma.actionToken.findUnique({
@@ -34,4 +35,26 @@ export async function confirmEmail(prisma: PrismaClient, token: string) {
])
return { success: true }
}
export async function updateDisplayName(
prisma: PrismaClient,
userId: string,
input: UpdateDisplayNameInput
) {
const user = await prisma.user.findUnique({ where: { id: userId } })
if (!user) throw Errors.USER_NOT_FOUND
return await prisma.user.update({
where: { id: userId },
data: { displayName: input.displayName },
select: {
id: true,
email: true,
displayName: true,
avatar: true,
isConfirmed: true,
createdAt: true,
},
})
}

View File

@@ -4,4 +4,24 @@ declare module 'fastify' {
interface FastifyInstance {
prisma: PrismaClient
}
interface FastifyRequest {
user: {
userId: string
tokenVersion: number
}
}
}
declare module '@fastify/jwt' {
interface FastifyJWT {
payload: {
userId: string
tokenVersion: number
}
user: {
userId: string
tokenVersion: number
}
}
}