Ajout du readme, du .env.example et suppression de la route pour fetch tous les users. Allegé les réponses de l'API sur les routes users et du logout

This commit is contained in:
2026-04-06 08:30:13 +02:00
parent 770c80c739
commit 53d2db737e
5 changed files with 214 additions and 23 deletions

29
.env.example Normal file
View File

@@ -0,0 +1,29 @@
# Environment variables declared in this file are NOT automatically loaded by Prisma.
# Please add `import "dotenv/config";` to your `prisma.config.ts` file, or use the Prisma CLI with Bun
# to load environment variables from .env files: https://pris.ly/prisma-config-env-vars.
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
# The following `prisma+postgres` URL is similar to the URL produced by running a local Prisma Postgres
# server with the `prisma dev` CLI command, when not choosing any non-default ports or settings. The API key, unlike the
# one found in a remote Prisma Postgres URL, does not contain any sensitive information.
DATABASE_URL="postgresql://user:password@localhost:5432/dbname"
# Envois de mails
SMTP_HOST="smtp.example.com"
SMTP_PORT=587
SMTP_USER="your@email.com"
SMTP_PASS="your-smtp-password"
MAIL_FROM="no-reply@example.com"
APP_URL=BACK_OFFICE_SERVER_URL
FRONT_URL=YOUR_FRONT_URL #is used for links in emails
#Secrets
JWT_SECRET="your-jwt-secret"

175
README.md Normal file
View File

@@ -0,0 +1,175 @@
# 🗂️ Fastify User Management Boilerplate
A production-ready back-office boilerplate for **user account management**, built with Fastify, Prisma, Zod, and PostgreSQL. Designed to be forked and extended as the backend foundation of any application requiring authentication and account lifecycle management.
---
## ✨ Features
- **Authentication** JWT-based auth with cookie support and token versioning (revocation on password change)
- **Registration & Login** Secure password hashing with Argon2
- **Google OAuth** Login or register via Google access token, with automatic account merge detection
- **Password recovery** Forgot password flow with time-limited action tokens
- **Password change** Authenticated password update with session revocation
- **Email change** Two-step flow with a 15-minute confirmation window and a 24-hour rollback period
- **Account deletion** Soft-delete with a 7-day cancellation window
- **Input validation** Zod schemas with shared password rules
- **Email notifications** Nodemailer integration (confirmation, recovery, alerts)
- **Scheduled tasks** node-cron for background jobs (e.g. purging expired accounts)
- **Security-first error codes** Intentionally vague error responses to prevent user enumeration
---
## 🧱 Stack
| Layer | Technology |
|---|---|
| Framework | [Fastify](https://fastify.dev/) v5 |
| ORM | [Prisma](https://www.prisma.io/) v7 |
| Database | PostgreSQL (via `pg` + `@prisma/adapter-pg`) |
| Validation | [Zod](https://zod.dev/) v4 |
| Auth | `@fastify/jwt` + `@fastify/cookie` |
| Password hashing | [Argon2](https://github.com/ranisalt/node-argon2) |
| Email | [Nodemailer](https://nodemailer.com/) |
| Scheduled jobs | [node-cron](https://github.com/node-cron/node-cron) |
| Runtime | Node.js + [tsx](https://github.com/privatenumber/tsx) |
| Language | TypeScript |
---
## 📁 Project Structure
```
src/
├── app.ts # Fastify app factory & plugin registration
├── server.ts # Entry point
├── routes/ # Route definitions (declared before services)
├── services/ # Business logic
├── schemas/ # Zod validation schemas (incl. shared.schema.ts)
├── plugins/ # Fastify plugins (JWT, cookies, DB, etc.)
├── middleware/ # Auth guards and request hooks
├── errors/ # Custom error classes and error codes
├── types/ # TypeScript type definitions
└── generated/ # Prisma generated client
```
---
## 🚀 Getting Started
### Prerequisites
- Node.js ≥ 20
- PostgreSQL database
- An SMTP server (or service like Mailtrap / Resend for dev)
### Installation
```bash
git clone https://github.com/your-username/your-repo.git
cd your-repo
npm install
```
### Environment
Rename `.env.example` to `.env` and fill in the values:
```env
# Database
DATABASE_URL="postgresql://user:password@localhost:5432/dbname"
# Email
SMTP_HOST="smtp.example.com"
SMTP_PORT=587
SMTP_USER="your@email.com"
SMTP_PASS="your-smtp-password"
MAIL_FROM="no-reply@example.com"
# URLs
APP_URL=BACK_OFFICE_SERVER_URL
FRONT_URL=YOUR_FRONT_URL # Used for links in emails
# Secrets
JWT_SECRET="your-jwt-secret"
```
### Database
```bash
npx prisma migrate dev
```
### Run (development)
```bash
npm run dev
```
---
## 🔐 Auth Flow Overview
- On login, a **JWT is issued** (stored in an HTTP-only cookie) containing the user's ID and a `tokenVersion`.
- On **password change**, `tokenVersion` is incremented, invalidating all previously issued tokens.
- Action tokens (password reset, email confirmation, etc.) are **single-use** and deleted on consumption.
- The `lang` field on requests is derived from the `Accept-Language` header (defaults to `fr`).
### Google OAuth
The Google flow accepts an `access_token` obtained by the client after the Google sign-in, and exchanges it for user info via the [Google UserInfo API](https://www.googleapis.com/oauth2/v3/userinfo). Three cases are handled:
| Situation | Behaviour |
|---|---|
| Email exists, no `googleId` | Returns `mergeRequired: true` — the client must prompt the user to link their accounts |
| Existing Google account | Logs the user in directly (checks for pending deletion) |
| New user | Creates the account with `googleId`, picture and display name from Google; sends a confirmation email if the address is not already verified by Google |
User preferences (`language`, `theme`) are automatically created on first Google sign-in.
---
## 🛣️ API Routes
### Auth — `/auth`
| Method | Route | Auth | Description |
|---|---|---|---|
| `POST` | `/auth/register` | — | Create a new account |
| `POST` | `/auth/login` | — | Login and receive a JWT cookie |
| `POST` | `/auth/logout` | ✅ | Invalidate the session cookie |
| `POST` | `/auth/google` | — | Login or register via Google access token |
### User — `/user`
| Method | Route | Auth | Description |
|---|---|---|---|
| `GET` | `/users` | — | List all users |
| `GET` | `/user/confirm?token=` | — | Confirm email address |
| `PATCH` | `/user/display-name` | ✅ | Update display name |
| `POST` | `/user/pwd-recovery-request` | — | Request a password reset email |
| `POST` | `/user/pwd-recovery` | — | Reset password via token |
| `PATCH` | `/user/pwd-change` | ✅ | Change password (re-issues JWT cookie) |
| `POST` | `/user/email-change-request` | ✅ | Request an email change (sends confirmation to new address) |
| `GET` | `/user/email-change-confirm?token=` | — | Confirm the new email address |
| `GET` | `/user/email-change-rollback?token=` | — | Rollback to previous email (24h window) |
| `DELETE` | `/user/account` | ✅ | Schedule account deletion (7-day window) |
| `GET` | `/user/delete-cancel?token=` | — | Cancel scheduled account deletion |
> ✅ = requires a valid `authToken` cookie
---
## 📋 Available Scripts
```bash
npm run dev # Start development server with tsx
npm run build # Compile TypeScript to dist/
npm run start # Run compiled server
```
---
## 📄 License
ISC

View File

@@ -42,7 +42,7 @@ export default async function authRoutes(fastify: FastifyInstance) {
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' })
return reply.status(200).send({ success : true })
})
fastify.post('/auth/google', async (request, reply) => {

View File

@@ -10,21 +10,7 @@ import { emailChangeRequest, emailChangeConfirm, emailRollback } from '../servic
import { scheduleAccountDeletion, cancelAccountDeletion } from '../services/accountDeletion.service.js'
export default async function userRoutes(fastify: FastifyInstance) {
fastify.get('/users', async (request, reply) => {
const users = await fastify.prisma.user.findMany({
select: {
id: true,
email: true,
displayName: true,
isConfirmed: true,
isGoogleUser: true,
avatar: true,
createdAt: true,
},
})
return users
})
fastify.get('/user/confirm', async (request, reply) => {
const { token } = request.query as { token?: string }
@@ -64,7 +50,7 @@ export default async function userRoutes(fastify: FastifyInstance) {
if (!token || !newPassword) throw Errors.VALIDATION_ERROR
await confirmPasswordRecovery(fastify.prisma, token, newPassword)
return reply.status(200).send({ message: 'Mot de passe mis à jour avec succès.' })
return reply.status(200).send({ success : true })
})
fastify.patch('/user/pwd-change', { preHandler: verifyAuth }, async (request, reply) => {
@@ -80,7 +66,7 @@ export default async function userRoutes(fastify: FastifyInstance) {
path: '/',
})
return reply.status(200).send({ message: 'Mot de passe mis à jour avec succès.' })
return reply.status(200).send({ success : true })
})
fastify.post('/user/email-change-request', { preHandler: verifyAuth }, async (request, reply) => {
@@ -92,7 +78,7 @@ export default async function userRoutes(fastify: FastifyInstance) {
await emailChangeRequest(fastify.prisma, fastify.mailer, request.user.userId, newEmail, validLang)
return reply.status(200).send({ message: 'Un email de confirmation a été envoyé à votre nouvelle adresse.' })
return reply.status(200).send({ success : true })
})
fastify.get('/user/email-change-confirm', async (request, reply) => {
@@ -101,7 +87,7 @@ export default async function userRoutes(fastify: FastifyInstance) {
await emailChangeConfirm(fastify.prisma, token)
return reply.status(200).send({ message: 'Email mis à jour avec succès.' })
return reply.status(200).send({ success : true })
})
fastify.get('/user/email-change-rollback', async (request, reply) => {
@@ -110,12 +96,12 @@ export default async function userRoutes(fastify: FastifyInstance) {
await emailRollback(fastify.prisma, token)
return reply.status(200).send({ message: 'Email restauré avec succès.' })
return reply.status(200).send({ success : true })
})
fastify.delete('/user/account', { preHandler: verifyAuth }, async (request, reply) => {
await scheduleAccountDeletion(fastify.prisma, fastify.mailer, request.user.userId)
return reply.status(200).send({ message: 'Votre compte sera supprimé dans 7 jours.' })
return reply.status(200).send({ success : true })
})
fastify.get('/user/delete-cancel', async (request, reply) => {
@@ -123,6 +109,6 @@ export default async function userRoutes(fastify: FastifyInstance) {
if (!token) throw Errors.INVALID_TOKEN
await cancelAccountDeletion(fastify.prisma, token)
return reply.status(200).send({ message: 'Suppression annulée, votre compte est restauré.' })
return reply.status(200).send({ success : true })
})
}

View File

@@ -51,7 +51,8 @@ export async function changePassword(
})
if (!user) throw Errors.USER_NOT_FOUND
if (!user.passwordHash) throw Errors.UNAUTHORIZED
const isValid = await argon2.verify(user.passwordHash, oldPassword)
if (!isValid) throw Errors.INVALID_CREDENTIALS