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:
29
.env.example
Normal file
29
.env.example
Normal 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
175
README.md
Normal 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
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user