Merge main into feat/owner-carbet-crud (integrate SYS-2 schema + SYS-3 auth)
main now contains the Prisma schema (SYS-2) and NextAuth (SYS-3) that the owner carbet CRUD depends on. Integrating them so the branch compiles and the PR is cleanly mergeable. - package.json: union of S3 SDK (@aws-sdk/client-s3) + auth deps. - No source conflicts; espace-hote "Gérer mes carbets" link already in main. - Verified: tsc --noEmit OK, next build OK (all carbet + auth routes compile). Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
commit
d5d2ad2228
15 changed files with 1338 additions and 28 deletions
167
README.md
167
README.md
|
|
@ -1,20 +1,85 @@
|
|||
# Karbé
|
||||
|
||||
Karbé — marketplace de location de carbets fluviaux de Guyane.
|
||||
> Marketplace de location de **carbets fluviaux** de Guyane.
|
||||
|
||||
Connecter voyageurs et hôtes pour des séjours authentiques le long des fleuves
|
||||
de Guyane, au cœur de la forêt amazonienne.
|
||||
Karbé connecte les voyageurs et les hôtes pour des séjours authentiques le long
|
||||
des fleuves de Guyane, au cœur de la forêt amazonienne. La plateforme permet aux
|
||||
propriétaires de publier leurs carbets, et aux voyageurs de rechercher, réserver
|
||||
et payer leur séjour en ligne.
|
||||
|
||||
## Stack
|
||||
## Sommaire
|
||||
|
||||
- [Next.js 16](https://nextjs.org/) (App Router, TypeScript)
|
||||
- [Tailwind CSS v4](https://tailwindcss.com/)
|
||||
- [Prisma](https://www.prisma.io/) (datasource PostgreSQL)
|
||||
- [Présentation](#présentation)
|
||||
- [Fonctionnalités](#fonctionnalités)
|
||||
- [Stack technique](#stack-technique)
|
||||
- [Prérequis](#prérequis)
|
||||
- [Installation](#installation)
|
||||
- [Variables d'environnement](#variables-denvironnement)
|
||||
- [Développement](#développement)
|
||||
- [Base de données (Prisma)](#base-de-données-prisma)
|
||||
- [Scripts npm](#scripts-npm)
|
||||
- [Structure du projet](#structure-du-projet)
|
||||
- [Conventions Git & contribution](#conventions-git--contribution)
|
||||
- [Documentation complémentaire](#documentation-complémentaire)
|
||||
- [Licence](#licence)
|
||||
|
||||
## Présentation
|
||||
|
||||
Le **carbet** est l'habitat traditionnel amazonien : un abri ouvert, souvent en
|
||||
bois, installé au bord des fleuves. En Guyane, les carbets fluviaux sont un mode
|
||||
d'hébergement prisé pour découvrir la forêt, naviguer sur les fleuves (Maroni,
|
||||
Oyapock, Approuague…) et vivre une expérience proche de la nature.
|
||||
|
||||
Karbé est une **marketplace à deux faces** :
|
||||
|
||||
- **Voyageurs** — recherchent un carbet, consultent les disponibilités, réservent
|
||||
et paient en ligne, échangent avec l'hôte et laissent un avis après le séjour.
|
||||
- **Hôtes (loueurs)** — créent leur profil, publient et gèrent leurs carbets
|
||||
(photos, tarifs, calendrier de disponibilité) et suivent leurs réservations.
|
||||
|
||||
La plateforme prévoit également une dimension **B2B** (créneaux réservés aux
|
||||
comités d'entreprise) et un modèle d'**abonnement loueur**. Voir la
|
||||
[roadmap produit](./ROADMAP.md) pour le détail des phases.
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
| Domaine | Description |
|
||||
| --- | --- |
|
||||
| Comptes multi-rôles | Voyageur, hôte et administrateur (authentification NextAuth). |
|
||||
| Fiches carbet | Titre, description, fleuve, localité, géolocalisation, capacité, équipements, photos. |
|
||||
| Recherche publique | Listing et fiche carbet rendus côté serveur (SSR) pour le SEO. |
|
||||
| Disponibilités & tarifs | Calendrier par date, prix personnalisés, nuits minimum. |
|
||||
| Réservation | Sélection de dates, calcul du prix (nuitée, ménage, frais de service). |
|
||||
| Paiement | Encaissement via Stripe (réservation + abonnement loueur). |
|
||||
| Messagerie | Conversation par réservation entre voyageur et hôte. |
|
||||
| Avis & notes | Évaluation du séjour après le check-out. |
|
||||
| Conformité | CGV, RGPD, mentions légales. |
|
||||
|
||||
> Toutes ces fonctionnalités ne sont pas encore livrées : voir la
|
||||
> [roadmap](./ROADMAP.md) pour l'état d'avancement par phase.
|
||||
|
||||
## Stack technique
|
||||
|
||||
- **[Next.js 16](https://nextjs.org/)** — App Router, TypeScript, rendu serveur.
|
||||
- **[React 19](https://react.dev/)**.
|
||||
- **[Tailwind CSS v4](https://tailwindcss.com/)** — styles utilitaires.
|
||||
- **[Prisma 7](https://www.prisma.io/)** — ORM, datasource **PostgreSQL**. Le
|
||||
client est généré dans `src/generated/prisma`.
|
||||
- **[NextAuth](https://authjs.dev/)** — authentification multi-rôles.
|
||||
- **[Stripe](https://stripe.com/)** — paiements (prévu).
|
||||
|
||||
Pour les choix d'architecture et les flux détaillés, voir
|
||||
[`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md).
|
||||
|
||||
> **Note :** ce dépôt utilise une version de Next.js qui peut différer de la
|
||||
> documentation publique. En cas de doute sur une API, consultez la
|
||||
> documentation embarquée dans `node_modules/next/dist/docs/`.
|
||||
|
||||
## Prérequis
|
||||
|
||||
- Node.js >= 20
|
||||
- Une base de données PostgreSQL
|
||||
- **Node.js >= 20**
|
||||
- **npm** (fourni avec Node.js)
|
||||
- Une base de données **PostgreSQL** accessible (locale ou distante)
|
||||
|
||||
## Installation
|
||||
|
||||
|
|
@ -33,8 +98,27 @@ de Guyane, au cœur de la forêt amazonienne.
|
|||
cp .env.example .env
|
||||
```
|
||||
|
||||
Puis renseignez `DATABASE_URL` (connexion PostgreSQL) et `NEXTAUTH_SECRET`
|
||||
dans le fichier `.env`.
|
||||
Renseignez ensuite les valeurs dans `.env` (voir ci-dessous).
|
||||
|
||||
3. Préparer la base de données :
|
||||
|
||||
```bash
|
||||
npx prisma migrate dev
|
||||
```
|
||||
|
||||
## Variables d'environnement
|
||||
|
||||
Le fichier [`.env.example`](./.env.example) liste les variables attendues.
|
||||
Copiez-le en `.env` et renseignez vos valeurs.
|
||||
|
||||
| Variable | Rôle |
|
||||
| --- | --- |
|
||||
| `DATABASE_URL` | Chaîne de connexion PostgreSQL utilisée par Prisma. |
|
||||
| `AUTH_SECRET` / `NEXTAUTH_SECRET` | Secret de signature des sessions NextAuth. Générer avec `openssl rand -base64 32`. |
|
||||
|
||||
> Le fichier `.env` ne doit **jamais** être commité (il est ignoré par Git).
|
||||
> Au fur et à mesure de l'ajout des intégrations (ex. Stripe), de nouvelles
|
||||
> variables seront ajoutées à `.env.example`.
|
||||
|
||||
## Développement
|
||||
|
||||
|
|
@ -46,22 +130,61 @@ L'application est disponible sur [http://localhost:3000](http://localhost:3000).
|
|||
|
||||
## Base de données (Prisma)
|
||||
|
||||
Le schéma vit dans `prisma/schema.prisma` (volontairement minimal pour le
|
||||
moment, les modèles seront ajoutés au fur et à mesure).
|
||||
Le schéma vit dans [`prisma/schema.prisma`](./prisma/schema.prisma). Il décrit
|
||||
les entités de Karbé (utilisateurs, carbets, réservations, paiements, avis,
|
||||
messagerie…). Le modèle de données est détaillé dans
|
||||
[`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md#modèle-de-données).
|
||||
|
||||
```bash
|
||||
# Régénérer le client après une modification du schéma
|
||||
# Régénérer le client Prisma après une modification du schéma
|
||||
npx prisma generate
|
||||
|
||||
# Créer / appliquer une migration en développement
|
||||
npx prisma migrate dev
|
||||
npx prisma migrate dev --name <nom_de_la_migration>
|
||||
|
||||
# Inspecter la base via une interface web
|
||||
npx prisma studio
|
||||
```
|
||||
|
||||
## Scripts
|
||||
## Scripts npm
|
||||
|
||||
| Script | Description |
|
||||
| --------------- | ------------------------------------ |
|
||||
| `npm run dev` | Démarre le serveur de développement |
|
||||
| `npm run build` | Build de production |
|
||||
| `npm run start` | Démarre le serveur de production |
|
||||
| `npm run lint` | Lance ESLint |
|
||||
| Script | Description |
|
||||
| --- | --- |
|
||||
| `npm run dev` | Démarre le serveur de développement. |
|
||||
| `npm run build` | Build de production. |
|
||||
| `npm run start` | Démarre le serveur de production. |
|
||||
| `npm run lint` | Lance ESLint. |
|
||||
|
||||
## Structure du projet
|
||||
|
||||
```text
|
||||
karbe/
|
||||
├── prisma/
|
||||
│ ├── schema.prisma # Modèle de données (entités Karbé)
|
||||
│ └── migrations/ # Migrations SQL générées par Prisma
|
||||
├── public/ # Fichiers statiques
|
||||
├── src/
|
||||
│ ├── app/ # Routes App Router (pages, layouts, route handlers)
|
||||
│ └── generated/prisma/ # Client Prisma généré (ne pas éditer à la main)
|
||||
├── docs/
|
||||
│ └── ARCHITECTURE.md # Documentation d'architecture
|
||||
├── ROADMAP.md # Roadmap produit (MVP → V2 → V3)
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Conventions Git & contribution
|
||||
|
||||
- La branche **`main` est protégée** : aucun commit direct.
|
||||
- Travaillez sur une branche dédiée nommée **`feat/<sujet>`** (ou `fix/<sujet>`).
|
||||
- Ouvrez une **Pull Request vers `main`** pour toute modification.
|
||||
- Avant d'ouvrir la PR, vérifiez que `npm run lint` et `npm run build` passent.
|
||||
|
||||
## Documentation complémentaire
|
||||
|
||||
- [Roadmap produit](./ROADMAP.md) — phases MVP → V2 → V3.
|
||||
- [Documentation d'architecture](./docs/ARCHITECTURE.md) — stack, modèle de
|
||||
données et flux principaux.
|
||||
|
||||
## Licence
|
||||
|
||||
Voir le fichier [`LICENSE`](./LICENSE).
|
||||
|
|
|
|||
133
ROADMAP.md
Normal file
133
ROADMAP.md
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
# Roadmap produit — Karbé
|
||||
|
||||
Cette roadmap décrit la trajectoire produit de **Karbé**, la marketplace de
|
||||
location de carbets fluviaux de Guyane. Elle est organisée en trois phases :
|
||||
**MVP** (premier produit utilisable de bout en bout), **V2** (montée en gamme et
|
||||
expérience), **V3** (passage à l'échelle et nouveaux marchés).
|
||||
|
||||
> Les phases sont indicatives et sont amenées à évoluer en fonction des retours
|
||||
> utilisateurs et des priorités business. Les statuts ✅ / 🚧 / ⬜ reflètent
|
||||
> l'avancement au moment de la rédaction.
|
||||
|
||||
## Vision
|
||||
|
||||
Devenir **la** plateforme de référence pour la découverte et la réservation
|
||||
d'hébergements fluviaux et nature en Guyane, en valorisant l'offre des hôtes
|
||||
locaux et en proposant aux voyageurs (particuliers comme comités d'entreprise)
|
||||
une expérience de réservation simple, fiable et sécurisée.
|
||||
|
||||
## Objectifs par phase
|
||||
|
||||
| Phase | Objectif | Public visé |
|
||||
| --- | --- | --- |
|
||||
| **MVP** | Réserver et payer un carbet de bout en bout. | Voyageurs particuliers + hôtes |
|
||||
| **V2** | Fidéliser, fluidifier et industrialiser l'usage. | Voyageurs récurrents, hôtes pros, comités d'entreprise |
|
||||
| **V3** | Passer à l'échelle et ouvrir de nouveaux usages/marchés. | Partenaires, B2B, international |
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — MVP
|
||||
|
||||
**But :** un voyageur peut trouver un carbet, le réserver, le payer ; un hôte
|
||||
peut publier et gérer son offre. La boucle économique de base fonctionne.
|
||||
|
||||
### Périmètre fonctionnel
|
||||
|
||||
| # | Fonctionnalité | Description | Statut |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | Socle technique | Scaffold Next.js + TypeScript + Prisma + Tailwind. | ✅ |
|
||||
| 2 | Modèle de données | Schéma Prisma complet + migrations (entités Karbé). | ✅ |
|
||||
| 3 | Authentification multi-rôles | Inscription / connexion Voyageur, Hôte, Admin (NextAuth). | 🚧 |
|
||||
| 4 | Interface propriétaire | CRUD des carbets + upload des photos. | 🚧 |
|
||||
| 5 | Recherche & fiche publique | Listing et fiche carbet en SSR (SEO). | ⬜ |
|
||||
| 6 | Réservation & disponibilités | Calendrier de dispo, sélection de dates, créneaux comités d'entreprise. | ⬜ |
|
||||
| 7 | Paiement | Encaissement Stripe (réservation + abonnement loueur). | ⬜ |
|
||||
| 8 | Avis & notes | Évaluation du séjour après le check-out. | ⬜ |
|
||||
| 9 | Conformité | CGV, RGPD, mentions légales. | 🚧 |
|
||||
| 10 | Documentation | README, roadmap, doc d'architecture. | 🚧 |
|
||||
|
||||
### Critères de sortie du MVP
|
||||
|
||||
- Un voyageur peut s'inscrire, rechercher un carbet, réserver des dates
|
||||
disponibles et payer.
|
||||
- Un hôte peut publier un carbet avec photos, tarifs et calendrier.
|
||||
- Les pages publiques sont indexables (SSR/SEO).
|
||||
- Les obligations légales (CGV, RGPD, mentions légales) sont en place.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — V2
|
||||
|
||||
**But :** améliorer l'expérience, fidéliser les utilisateurs et outiller les
|
||||
hôtes professionnels et le canal B2B (comités d'entreprise).
|
||||
|
||||
### Pistes fonctionnelles
|
||||
|
||||
- **Recherche avancée** — carte interactive (fleuves/localités), filtres
|
||||
(équipements, capacité, prix, note), tri pertinent.
|
||||
- **Favoris & wishlists** — exploitation du modèle de favoris pour sauvegarder
|
||||
et comparer des carbets.
|
||||
- **Messagerie enrichie** — notifications (e-mail / in-app), messages
|
||||
pré-séjour et post-séjour, modèles de réponse pour les hôtes.
|
||||
- **Tableau de bord hôte** — statistiques (taux d'occupation, revenus),
|
||||
gestion des paiements/payouts, politiques d'annulation paramétrables.
|
||||
- **Gestion des annulations & remboursements** — règles claires côté voyageur
|
||||
et hôte, remboursements automatisés via Stripe.
|
||||
- **Portail comités d'entreprise (CE)** — espace dédié, créneaux et tarifs
|
||||
négociés, facturation centralisée.
|
||||
- **Modération des avis** — signalement, réponses des hôtes, contrôle qualité.
|
||||
- **Synchronisation de calendrier** — import/export iCal pour éviter les
|
||||
doubles réservations.
|
||||
- **Internationalisation (i18n)** — préparation multilingue (FR puis EN/PT,
|
||||
pertinent pour la zone transfrontalière Brésil/Suriname).
|
||||
- **Optimisation mobile** — parcours responsive soigné, performance sur réseaux
|
||||
contraints.
|
||||
|
||||
### Critères de sortie de la V2
|
||||
|
||||
- Les hôtes pilotent leur activité depuis un tableau de bord.
|
||||
- Le canal comités d'entreprise est opérationnel.
|
||||
- Le taux de conversion et la rétention sont mesurés et suivis.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — V3
|
||||
|
||||
**But :** passer à l'échelle, ouvrir de nouveaux usages et marchés, et
|
||||
renforcer la confiance.
|
||||
|
||||
### Pistes fonctionnelles
|
||||
|
||||
- **Applications mobiles** (iOS / Android) ou PWA installable.
|
||||
- **Tarification dynamique** — recommandations de prix selon la saison et la
|
||||
demande.
|
||||
- **Recommandations personnalisées** — moteur de suggestions de carbets.
|
||||
- **Vérification d'identité (KYC)** et label « hôte vérifié » renforcé.
|
||||
- **Assurance séjour / annulation** via un partenaire.
|
||||
- **Intégrations partenaires** — tour-opérateurs, transport fluvial,
|
||||
activités (pêche, randonnée, excursions).
|
||||
- **Programme de fidélité** — points, parrainage, offres récurrentes.
|
||||
- **API publique** — ouverture aux partenaires et revendeurs.
|
||||
- **Mode hors-ligne / faible connectivité** — consultation et préparation de
|
||||
réservation adaptées aux zones isolées de Guyane.
|
||||
- **Expansion géographique** — autres territoires (Antilles, Amazonie
|
||||
transfrontalière), multi-devises.
|
||||
|
||||
### Critères de sortie de la V3
|
||||
|
||||
- Présence mobile native ou PWA.
|
||||
- Écosystème de partenaires intégré via API.
|
||||
- Modèle économique diversifié (commissions, abonnements, partenariats).
|
||||
|
||||
---
|
||||
|
||||
## Synthèse
|
||||
|
||||
```text
|
||||
MVP ──▶ Réserver & payer un carbet de bout en bout
|
||||
V2 ──▶ Expérience, fidélisation, hôtes pros & comités d'entreprise
|
||||
V3 ──▶ Mobile, partenariats, API, passage à l'échelle
|
||||
```
|
||||
|
||||
Pour le détail technique des fonctionnalités, voir
|
||||
[`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md).
|
||||
282
docs/ARCHITECTURE.md
Normal file
282
docs/ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
# Architecture — Karbé
|
||||
|
||||
Ce document décrit l'architecture technique de **Karbé**, la marketplace de
|
||||
location de carbets fluviaux de Guyane : la **stack**, le **modèle de données**
|
||||
et les **flux principaux**.
|
||||
|
||||
- Pour démarrer le projet, voir le [README](../README.md).
|
||||
- Pour la trajectoire produit, voir la [roadmap](../ROADMAP.md).
|
||||
|
||||
## Sommaire
|
||||
|
||||
- [Vue d'ensemble](#vue-densemble)
|
||||
- [Stack technique](#stack-technique)
|
||||
- [Organisation du code](#organisation-du-code)
|
||||
- [Modèle de données](#modèle-de-données)
|
||||
- [Flux principaux](#flux-principaux)
|
||||
- [Sécurité & conformité](#sécurité--conformité)
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Karbé est une application **Next.js (App Router)** full-stack : le même projet
|
||||
sert le rendu des pages (React Server Components + Client Components) et expose
|
||||
la logique serveur (route handlers, server actions). La persistance est assurée
|
||||
par **PostgreSQL** via **Prisma**. L'authentification repose sur **NextAuth**,
|
||||
et les paiements sur **Stripe**.
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────┐
|
||||
Navigateur │ Next.js (App Router) │
|
||||
(voyageur / │ │
|
||||
hôte / admin)│ React Server / Client Components │
|
||||
│ │ Route handlers / Server actions │
|
||||
│ HTTPS │ │ │ │
|
||||
└───────▶│ NextAuth Prisma Client │
|
||||
│ (sessions, (requêtes) │
|
||||
│ rôles) │ │
|
||||
└─────────────────────────────────┼─────────────┘
|
||||
│
|
||||
┌────────────────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
PostgreSQL Stripe Stockage
|
||||
(données) (paiements) médias (photos)
|
||||
```
|
||||
|
||||
> **Note de version :** ce dépôt utilise une version récente de Next.js dont
|
||||
> certaines API peuvent différer de la documentation publique. La référence
|
||||
> embarquée se trouve dans `node_modules/next/dist/docs/`.
|
||||
|
||||
## Stack technique
|
||||
|
||||
| Couche | Technologie | Rôle |
|
||||
| --- | --- | --- |
|
||||
| Framework | **Next.js 16** (App Router) | Rendu SSR/RSC, routing, logique serveur. |
|
||||
| UI | **React 19** + **Tailwind CSS v4** | Composants et styles. |
|
||||
| Langage | **TypeScript** | Typage statique de bout en bout. |
|
||||
| ORM | **Prisma 7** | Accès aux données, migrations, typage. Client généré dans `src/generated/prisma`. |
|
||||
| Base de données | **PostgreSQL** | Persistance relationnelle. |
|
||||
| Authentification | **NextAuth** | Sessions et contrôle d'accès par rôle. |
|
||||
| Paiement | **Stripe** | Encaissement réservations + abonnement loueur. |
|
||||
| Lint | **ESLint** (`eslint-config-next`) | Qualité de code. |
|
||||
|
||||
## Organisation du code
|
||||
|
||||
```text
|
||||
src/
|
||||
├── app/ # App Router : pages, layouts, route handlers
|
||||
│ ├── layout.tsx # Layout racine
|
||||
│ ├── page.tsx # Page d'accueil
|
||||
│ └── globals.css # Styles globaux (Tailwind)
|
||||
└── generated/prisma/ # Client Prisma généré (NE PAS éditer à la main)
|
||||
|
||||
prisma/
|
||||
├── schema.prisma # Source de vérité du modèle de données
|
||||
└── migrations/ # Migrations SQL
|
||||
```
|
||||
|
||||
Principes :
|
||||
|
||||
- **App Router** — chaque dossier de `src/app` est un segment de route. Les
|
||||
pages sont des **Server Components** par défaut ; on bascule en Client
|
||||
Component (`"use client"`) seulement pour l'interactivité.
|
||||
- **Accès données côté serveur** — les requêtes Prisma s'exécutent côté serveur
|
||||
(Server Components, route handlers, server actions), jamais dans le
|
||||
navigateur.
|
||||
- **Client Prisma généré** — importé depuis `src/generated/prisma` ; il est
|
||||
régénéré via `npx prisma generate` (script `postinstall`).
|
||||
|
||||
## Modèle de données
|
||||
|
||||
Le modèle est défini dans [`prisma/schema.prisma`](../prisma/schema.prisma) et
|
||||
porté par PostgreSQL. Il couvre cinq grands domaines : **comptes**, **offre
|
||||
(carbets)**, **réservation**, **paiement** et **relation (messagerie & avis)**.
|
||||
|
||||
### Énumérations
|
||||
|
||||
| Enum | Valeurs |
|
||||
| --- | --- |
|
||||
| `UserRole` | `TRAVELER`, `HOST`, `ADMIN` |
|
||||
| `CarbetStatus` | `DRAFT`, `PUBLISHED`, `ARCHIVED` |
|
||||
| `BookingStatus` | `PENDING`, `CONFIRMED`, `CANCELLED`, `COMPLETED` |
|
||||
| `PaymentStatus` | `PENDING`, `AUTHORIZED`, `SUCCEEDED`, `FAILED`, `REFUNDED` |
|
||||
| `MessageSenderType` | `TRAVELER`, `HOST`, `SYSTEM` |
|
||||
|
||||
### Schéma relationnel
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
User ||--o| HostProfile : "a (si hôte)"
|
||||
User ||--o{ Booking : "réserve"
|
||||
User ||--o{ Review : "rédige"
|
||||
User ||--o{ FavoriteCarbet : "favoris"
|
||||
HostProfile ||--o{ Carbet : "publie"
|
||||
Carbet ||--o{ CarbetPhoto : "photos"
|
||||
Carbet ||--o{ CarbetAvailability : "disponibilités"
|
||||
Carbet ||--o{ Booking : "réservations"
|
||||
Carbet ||--o{ Review : "avis"
|
||||
Carbet ||--o{ FavoriteCarbet : "favoris"
|
||||
Carbet }o--o{ Amenity : "équipements (CarbetAmenity)"
|
||||
Booking ||--o{ Payment : "paiements"
|
||||
Booking ||--o| Conversation : "fil de discussion"
|
||||
Booking ||--o| Review : "avis"
|
||||
Conversation ||--o{ Message : "messages"
|
||||
```
|
||||
|
||||
### Entités principales
|
||||
|
||||
#### Comptes
|
||||
|
||||
- **`User`** — compte unique pour tous les rôles (`role` : `TRAVELER`, `HOST`,
|
||||
`ADMIN`). Champs clés : `email` (unique), `passwordHash`, `firstName`,
|
||||
`lastName`, `phone?`, `avatarUrl?`, `isActive`. Relations : un éventuel
|
||||
`HostProfile`, ses `bookings`, ses `reviews`, ses `favoriteCarbets`, et ses
|
||||
conversations (en tant que voyageur ou hôte).
|
||||
- **`HostProfile`** — profil hôte en **1:1** avec `User` (relation optionnelle :
|
||||
seuls les hôtes en ont un). Contient `bio?`, `verificationAt?` (date de
|
||||
vérification), `payoutInfo?` (infos de versement) et la liste des `carbets`.
|
||||
|
||||
#### Offre (carbets)
|
||||
|
||||
- **`Carbet`** — l'annonce. Champs : `title`, `slug` (unique, pour le SEO),
|
||||
`description`, localisation (`river`, `locality`, `latitude?`, `longitude?`),
|
||||
capacité (`maxGuests`, `bedrooms`, `beds`, `bathrooms`), tarification
|
||||
(`basePricePerNight`, `cleaningFee`, `serviceFee`), `status`
|
||||
(`DRAFT`/`PUBLISHED`/`ARCHIVED`) et `publishedAt?`. Indexé par hôte, statut,
|
||||
(fleuve, localité) et prix pour la recherche.
|
||||
- **`Amenity`** / **`CarbetAmenity`** — catalogue d'équipements et table de
|
||||
liaison **N:N** entre carbets et équipements (clé composite
|
||||
`[carbetId, amenityId]`).
|
||||
- **`CarbetPhoto`** — photos d'un carbet (`url`, `alt?`, `sortOrder`).
|
||||
- **`CarbetAvailability`** — calendrier : une ligne par `date` et par carbet
|
||||
(`@@unique([carbetId, date])`), avec `isAvailable`, `customPrice?` (tarif
|
||||
spécifique) et `minNights` (séjour minimum).
|
||||
|
||||
#### Réservation
|
||||
|
||||
- **`Booking`** — réservation reliant un `Carbet` et un `User` (voyageur).
|
||||
Champs : `checkIn`, `checkOut`, `guests`, `status`, instantané de
|
||||
tarification (`nightlyRate`, `cleaningFee`, `serviceFee`, `totalAmount`,
|
||||
`currency` = `EUR`), `notes?`, et champs d'annulation (`canceledAt?`,
|
||||
`cancellationReason?`). Les suppressions de carbet/voyageur sont en
|
||||
`Restrict` pour préserver l'historique des réservations.
|
||||
|
||||
#### Paiement
|
||||
|
||||
- **`Payment`** — un ou plusieurs paiements rattachés à une `Booking`. Champs :
|
||||
`provider`, `providerPaymentId?` (unique, ex. id Stripe), `amount`,
|
||||
`currency`, `status` (`PENDING` → `AUTHORIZED` → `SUCCEEDED`/`FAILED`,
|
||||
`REFUNDED`), `paidAt?`, `refundedAt?` et détails d'échec
|
||||
(`failureCode?`, `failureMessage?`).
|
||||
|
||||
#### Relation : messagerie & avis
|
||||
|
||||
- **`Conversation`** — un fil **1:1 avec une `Booking`**, reliant le voyageur
|
||||
(`travelerId`) et l'hôte (`hostId`).
|
||||
- **`Message`** — message d'une conversation (`senderType` : `TRAVELER`,
|
||||
`HOST` ou `SYSTEM`, `senderUserId?`, `content`, `sentAt`, `readAt?`).
|
||||
- **`Review`** — avis **1:1 avec une `Booking`** (un avis par séjour), portant
|
||||
une `rating`, un `title?` et un `comment?`, rattaché au carbet et au voyageur.
|
||||
- **`FavoriteCarbet`** — table de liaison **N:N** (wishlist) entre `User` et
|
||||
`Carbet` (clé composite `[userId, carbetId]`).
|
||||
|
||||
### Conventions
|
||||
|
||||
- **Identifiants** : `cuid()` (chaînes) en clé primaire.
|
||||
- **Horodatage** : `createdAt` / `updatedAt` sur la plupart des entités.
|
||||
- **Montants** : `Decimal(10,2)` pour les prix, `Decimal(9,6)` pour les
|
||||
coordonnées GPS.
|
||||
- **Suppressions** : `Cascade` pour les données dépendantes (photos, messages,
|
||||
disponibilités) ; `Restrict` pour préserver l'historique financier
|
||||
(réservations, paiements, avis).
|
||||
- **Index** : posés sur les colonnes de recherche/jointure fréquentes (statut,
|
||||
fleuve+localité, prix, dates, clés étrangères).
|
||||
|
||||
## Flux principaux
|
||||
|
||||
### 1. Authentification & rôles
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor U as Utilisateur
|
||||
participant App as Next.js
|
||||
participant Auth as NextAuth
|
||||
participant DB as PostgreSQL
|
||||
|
||||
U->>App: Inscription / Connexion (email + mot de passe)
|
||||
App->>Auth: Vérifie les identifiants
|
||||
Auth->>DB: Lit User (passwordHash, role, isActive)
|
||||
Auth-->>App: Session (id + role)
|
||||
App-->>U: Accès adapté au rôle (TRAVELER / HOST / ADMIN)
|
||||
```
|
||||
|
||||
Le `role` porté par la session conditionne l'accès : espace voyageur, interface
|
||||
hôte (gestion des carbets), ou back-office admin.
|
||||
|
||||
### 2. Publication d'un carbet (hôte)
|
||||
|
||||
1. Un `User` de rôle `HOST` (avec `HostProfile`) crée un `Carbet` en `DRAFT`.
|
||||
2. Il ajoute des `CarbetPhoto`, sélectionne des `Amenity` (via `CarbetAmenity`)
|
||||
et renseigne le calendrier `CarbetAvailability` (dates, prix, nuits min).
|
||||
3. Il publie : `status` passe à `PUBLISHED` et `publishedAt` est renseigné. Le
|
||||
carbet devient visible dans la recherche publique.
|
||||
|
||||
### 3. Recherche & consultation (public, SSR/SEO)
|
||||
|
||||
1. La page de listing interroge les `Carbet` en `PUBLISHED` (filtres : fleuve,
|
||||
localité, capacité, prix…), rendue **côté serveur** pour l'indexation.
|
||||
2. La fiche carbet est servie via son `slug` unique (URL stable, SEO-friendly)
|
||||
et affiche photos, équipements, disponibilités et avis.
|
||||
|
||||
### 4. Réservation & paiement
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor V as Voyageur
|
||||
participant App as Next.js
|
||||
participant DB as PostgreSQL
|
||||
participant Stripe as Stripe
|
||||
|
||||
V->>App: Choisit des dates + nombre de voyageurs
|
||||
App->>DB: Vérifie CarbetAvailability (dispo + minNights)
|
||||
App->>App: Calcule le total (nuitées, ménage, frais de service)
|
||||
App->>DB: Crée Booking (status = PENDING)
|
||||
V->>App: Procède au paiement
|
||||
App->>Stripe: Crée l'intention de paiement
|
||||
Stripe-->>App: Webhook / callback (succès ou échec)
|
||||
alt Paiement réussi
|
||||
App->>DB: Payment = SUCCEEDED, Booking = CONFIRMED
|
||||
App-->>V: Confirmation de réservation
|
||||
else Paiement échoué
|
||||
App->>DB: Payment = FAILED, Booking reste PENDING
|
||||
App-->>V: Échec, nouvelle tentative possible
|
||||
end
|
||||
```
|
||||
|
||||
À l'issue du séjour, la `Booking` passe en `COMPLETED`. Une annulation renseigne
|
||||
`canceledAt`/`cancellationReason` et peut déclencher un `REFUNDED` côté
|
||||
`Payment`.
|
||||
|
||||
### 5. Messagerie
|
||||
|
||||
À la création d'une réservation, une `Conversation` (1:1 avec la `Booking`) est
|
||||
ouverte entre le voyageur et l'hôte. Les `Message` portent un `senderType`
|
||||
(`TRAVELER`, `HOST`, `SYSTEM` pour les notifications automatiques) et un statut
|
||||
de lecture (`readAt`).
|
||||
|
||||
### 6. Avis
|
||||
|
||||
Après un séjour `COMPLETED`, le voyageur peut déposer un `Review` (un seul par
|
||||
réservation, contrainte d'unicité sur `bookingId`). La note alimente la
|
||||
réputation du carbet affichée sur sa fiche.
|
||||
|
||||
## Sécurité & conformité
|
||||
|
||||
- **Mots de passe** stockés hachés (`passwordHash`), jamais en clair.
|
||||
- **Secrets** (`AUTH_SECRET`/`NEXTAUTH_SECRET`, clés Stripe, `DATABASE_URL`) en
|
||||
variables d'environnement, hors du dépôt (voir `.env.example`).
|
||||
- **Contrôle d'accès** par rôle (`UserRole`) appliqué côté serveur.
|
||||
- **Intégrité financière** : suppressions en `Restrict` sur les réservations,
|
||||
paiements et avis pour conserver l'historique.
|
||||
- **RGPD & mentions légales** : pages dédiées (CGV, politique de
|
||||
confidentialité, mentions légales) — voir la [roadmap](../ROADMAP.md).
|
||||
```
|
||||
292
prisma/migrations/20260529000000_init_karbe_schema/migration.sql
Normal file
292
prisma/migrations/20260529000000_init_karbe_schema/migration.sql
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
-- CreateSchema
|
||||
CREATE SCHEMA IF NOT EXISTS "public";
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "UserRole" AS ENUM ('OWNER', 'CE_MANAGER', 'CE_MEMBER', 'TOURIST', 'ADMIN');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "CarbetStatus" AS ENUM ('DRAFT', 'PUBLISHED', 'ARCHIVED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "MediaType" AS ENUM ('PHOTO', 'VIDEO');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "AvailabilityScope" AS ENUM ('PUBLIC', 'CE_ONLY');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "AvailabilityBlockReason" AS ENUM ('NONE', 'CE_BLOCKED', 'WEEKEND_BLOCKED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "BookingStatus" AS ENUM ('PENDING', 'CONFIRMED', 'CANCELLED', 'COMPLETED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "PaymentStatus" AS ENUM ('PENDING', 'AUTHORIZED', 'SUCCEEDED', 'FAILED', 'REFUNDED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SubscriptionStatus" AS ENUM ('TRIAL', 'ACTIVE', 'PAST_DUE', 'CANCELED');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Organization" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Organization_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"passwordHash" TEXT NOT NULL,
|
||||
"firstName" TEXT NOT NULL,
|
||||
"lastName" TEXT NOT NULL,
|
||||
"phone" TEXT,
|
||||
"role" "UserRole" NOT NULL,
|
||||
"organizationId" TEXT,
|
||||
"avatarUrl" TEXT,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Carbet" (
|
||||
"id" TEXT NOT NULL,
|
||||
"ownerId" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"description" TEXT NOT NULL,
|
||||
"river" TEXT NOT NULL,
|
||||
"latitude" DECIMAL(9,6) NOT NULL,
|
||||
"longitude" DECIMAL(9,6) NOT NULL,
|
||||
"embarkPoint" TEXT NOT NULL,
|
||||
"pirogueDurationMin" INTEGER NOT NULL,
|
||||
"capacity" INTEGER NOT NULL,
|
||||
"status" "CarbetStatus" NOT NULL DEFAULT 'DRAFT',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Carbet_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Amenity" (
|
||||
"id" TEXT NOT NULL,
|
||||
"key" TEXT NOT NULL,
|
||||
"label" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Amenity_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "CarbetAmenity" (
|
||||
"carbetId" TEXT NOT NULL,
|
||||
"amenityId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "CarbetAmenity_pkey" PRIMARY KEY ("carbetId","amenityId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Media" (
|
||||
"id" TEXT NOT NULL,
|
||||
"carbetId" TEXT NOT NULL,
|
||||
"type" "MediaType" NOT NULL,
|
||||
"s3Key" TEXT NOT NULL,
|
||||
"s3Url" TEXT NOT NULL,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Media_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Availability" (
|
||||
"id" TEXT NOT NULL,
|
||||
"carbetId" TEXT NOT NULL,
|
||||
"startDate" TIMESTAMP(3) NOT NULL,
|
||||
"endDate" TIMESTAMP(3) NOT NULL,
|
||||
"scope" "AvailabilityScope" NOT NULL DEFAULT 'PUBLIC',
|
||||
"blockReason" "AvailabilityBlockReason" NOT NULL DEFAULT 'NONE',
|
||||
"isAvailable" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Availability_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Booking" (
|
||||
"id" TEXT NOT NULL,
|
||||
"carbetId" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"startDate" TIMESTAMP(3) NOT NULL,
|
||||
"endDate" TIMESTAMP(3) NOT NULL,
|
||||
"guestCount" INTEGER NOT NULL,
|
||||
"status" "BookingStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"amount" DECIMAL(10,2) NOT NULL,
|
||||
"currency" TEXT NOT NULL DEFAULT 'EUR',
|
||||
"paymentStatus" "PaymentStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Booking_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Subscription" (
|
||||
"id" TEXT NOT NULL,
|
||||
"ownerId" TEXT NOT NULL,
|
||||
"carbetId" TEXT NOT NULL,
|
||||
"provider" TEXT NOT NULL,
|
||||
"providerSubId" TEXT,
|
||||
"status" "SubscriptionStatus" NOT NULL DEFAULT 'TRIAL',
|
||||
"startedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"renewedAt" TIMESTAMP(3),
|
||||
"canceledAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Subscription_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Review" (
|
||||
"id" TEXT NOT NULL,
|
||||
"bookingId" TEXT NOT NULL,
|
||||
"carbetId" TEXT NOT NULL,
|
||||
"authorId" TEXT NOT NULL,
|
||||
"rating" INTEGER NOT NULL,
|
||||
"comment" TEXT,
|
||||
"hostResponse" TEXT,
|
||||
"hostRespondedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Review_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Organization_slug_key" ON "Organization"("slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Organization_name_idx" ON "Organization"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "User_organizationId_idx" ON "User"("organizationId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "User_role_idx" ON "User"("role");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Carbet_slug_key" ON "Carbet"("slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Carbet_ownerId_idx" ON "Carbet"("ownerId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Carbet_status_idx" ON "Carbet"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Carbet_river_idx" ON "Carbet"("river");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Amenity_key_key" ON "Amenity"("key");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "CarbetAmenity_amenityId_idx" ON "CarbetAmenity"("amenityId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Media_carbetId_sortOrder_idx" ON "Media"("carbetId", "sortOrder");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Availability_carbetId_idx" ON "Availability"("carbetId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Availability_scope_blockReason_idx" ON "Availability"("scope", "blockReason");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Availability_startDate_endDate_idx" ON "Availability"("startDate", "endDate");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Booking_carbetId_idx" ON "Booking"("carbetId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Booking_tenantId_idx" ON "Booking"("tenantId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Booking_status_paymentStatus_idx" ON "Booking"("status", "paymentStatus");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Booking_startDate_endDate_idx" ON "Booking"("startDate", "endDate");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Subscription_providerSubId_key" ON "Subscription"("providerSubId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Subscription_ownerId_idx" ON "Subscription"("ownerId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Subscription_carbetId_idx" ON "Subscription"("carbetId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Subscription_status_idx" ON "Subscription"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Review_bookingId_key" ON "Review"("bookingId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Review_carbetId_idx" ON "Review"("carbetId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Review_authorId_idx" ON "Review"("authorId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "User" ADD CONSTRAINT "User_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Carbet" ADD CONSTRAINT "Carbet_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CarbetAmenity" ADD CONSTRAINT "CarbetAmenity_carbetId_fkey" FOREIGN KEY ("carbetId") REFERENCES "Carbet"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CarbetAmenity" ADD CONSTRAINT "CarbetAmenity_amenityId_fkey" FOREIGN KEY ("amenityId") REFERENCES "Amenity"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Media" ADD CONSTRAINT "Media_carbetId_fkey" FOREIGN KEY ("carbetId") REFERENCES "Carbet"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Availability" ADD CONSTRAINT "Availability_carbetId_fkey" FOREIGN KEY ("carbetId") REFERENCES "Carbet"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Booking" ADD CONSTRAINT "Booking_carbetId_fkey" FOREIGN KEY ("carbetId") REFERENCES "Carbet"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Booking" ADD CONSTRAINT "Booking_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_carbetId_fkey" FOREIGN KEY ("carbetId") REFERENCES "Carbet"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Review" ADD CONSTRAINT "Review_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Review" ADD CONSTRAINT "Review_carbetId_fkey" FOREIGN KEY ("carbetId") REFERENCES "Carbet"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Review" ADD CONSTRAINT "Review_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
1
prisma/migrations/migration_lock.toml
Normal file
1
prisma/migrations/migration_lock.toml
Normal file
|
|
@ -0,0 +1 @@
|
|||
provider = "postgresql"
|
||||
|
|
@ -1,8 +1,3 @@
|
|||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
// Get a free hosted Postgres database in seconds: `npx create-db`
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client"
|
||||
output = "../src/generated/prisma"
|
||||
|
|
@ -12,4 +7,239 @@ datasource db {
|
|||
provider = "postgresql"
|
||||
}
|
||||
|
||||
// Les modèles Karbé (carbets, réservations, utilisateurs…) seront ajoutés ici.
|
||||
enum UserRole {
|
||||
OWNER
|
||||
CE_MANAGER
|
||||
CE_MEMBER
|
||||
TOURIST
|
||||
ADMIN
|
||||
}
|
||||
|
||||
enum CarbetStatus {
|
||||
DRAFT
|
||||
PUBLISHED
|
||||
ARCHIVED
|
||||
}
|
||||
|
||||
enum MediaType {
|
||||
PHOTO
|
||||
VIDEO
|
||||
}
|
||||
|
||||
enum AvailabilityScope {
|
||||
PUBLIC
|
||||
CE_ONLY
|
||||
}
|
||||
|
||||
enum AvailabilityBlockReason {
|
||||
NONE
|
||||
CE_BLOCKED
|
||||
WEEKEND_BLOCKED
|
||||
}
|
||||
|
||||
enum BookingStatus {
|
||||
PENDING
|
||||
CONFIRMED
|
||||
CANCELLED
|
||||
COMPLETED
|
||||
}
|
||||
|
||||
enum PaymentStatus {
|
||||
PENDING
|
||||
AUTHORIZED
|
||||
SUCCEEDED
|
||||
FAILED
|
||||
REFUNDED
|
||||
}
|
||||
|
||||
enum SubscriptionStatus {
|
||||
TRIAL
|
||||
ACTIVE
|
||||
PAST_DUE
|
||||
CANCELED
|
||||
}
|
||||
|
||||
model Organization {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
slug String @unique
|
||||
description String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
members User[]
|
||||
|
||||
@@index([name])
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
passwordHash String
|
||||
firstName String
|
||||
lastName String
|
||||
phone String?
|
||||
role UserRole
|
||||
organizationId String?
|
||||
avatarUrl String?
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: SetNull)
|
||||
carbets Carbet[] @relation("CarbetOwner")
|
||||
bookings Booking[] @relation("BookingTenant")
|
||||
reviews Review[] @relation("ReviewAuthor")
|
||||
subscriptions Subscription[]
|
||||
|
||||
@@index([organizationId])
|
||||
@@index([role])
|
||||
}
|
||||
|
||||
model Carbet {
|
||||
id String @id @default(cuid())
|
||||
ownerId String
|
||||
title String
|
||||
slug String @unique
|
||||
description String
|
||||
river String
|
||||
latitude Decimal @db.Decimal(9, 6)
|
||||
longitude Decimal @db.Decimal(9, 6)
|
||||
embarkPoint String
|
||||
pirogueDurationMin Int
|
||||
capacity Int
|
||||
status CarbetStatus @default(DRAFT)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
owner User @relation("CarbetOwner", fields: [ownerId], references: [id], onDelete: Restrict)
|
||||
amenities CarbetAmenity[]
|
||||
media Media[]
|
||||
availabilities Availability[]
|
||||
bookings Booking[]
|
||||
reviews Review[]
|
||||
subscriptions Subscription[]
|
||||
|
||||
@@index([ownerId])
|
||||
@@index([status])
|
||||
@@index([river])
|
||||
}
|
||||
|
||||
model Amenity {
|
||||
id String @id @default(cuid())
|
||||
key String @unique
|
||||
label String
|
||||
description String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
carbets CarbetAmenity[]
|
||||
}
|
||||
|
||||
model CarbetAmenity {
|
||||
carbetId String
|
||||
amenityId String
|
||||
|
||||
carbet Carbet @relation(fields: [carbetId], references: [id], onDelete: Cascade)
|
||||
amenity Amenity @relation(fields: [amenityId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([carbetId, amenityId])
|
||||
@@index([amenityId])
|
||||
}
|
||||
|
||||
model Media {
|
||||
id String @id @default(cuid())
|
||||
carbetId String
|
||||
type MediaType
|
||||
s3Key String
|
||||
s3Url String
|
||||
sortOrder Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
carbet Carbet @relation(fields: [carbetId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([carbetId, sortOrder])
|
||||
}
|
||||
|
||||
model Availability {
|
||||
id String @id @default(cuid())
|
||||
carbetId String
|
||||
startDate DateTime
|
||||
endDate DateTime
|
||||
scope AvailabilityScope @default(PUBLIC)
|
||||
blockReason AvailabilityBlockReason @default(NONE)
|
||||
isAvailable Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
carbet Carbet @relation(fields: [carbetId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([carbetId])
|
||||
@@index([scope, blockReason])
|
||||
@@index([startDate, endDate])
|
||||
}
|
||||
|
||||
model Booking {
|
||||
id String @id @default(cuid())
|
||||
carbetId String
|
||||
tenantId String
|
||||
startDate DateTime
|
||||
endDate DateTime
|
||||
guestCount Int
|
||||
status BookingStatus @default(PENDING)
|
||||
amount Decimal @db.Decimal(10, 2)
|
||||
currency String @default("EUR")
|
||||
paymentStatus PaymentStatus @default(PENDING)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
carbet Carbet @relation(fields: [carbetId], references: [id], onDelete: Restrict)
|
||||
tenant User @relation("BookingTenant", fields: [tenantId], references: [id], onDelete: Restrict)
|
||||
|
||||
review Review?
|
||||
|
||||
@@index([carbetId])
|
||||
@@index([tenantId])
|
||||
@@index([status, paymentStatus])
|
||||
@@index([startDate, endDate])
|
||||
}
|
||||
|
||||
model Subscription {
|
||||
id String @id @default(cuid())
|
||||
ownerId String
|
||||
carbetId String
|
||||
provider String
|
||||
providerSubId String? @unique
|
||||
status SubscriptionStatus @default(TRIAL)
|
||||
startedAt DateTime @default(now())
|
||||
renewedAt DateTime?
|
||||
canceledAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
owner User @relation(fields: [ownerId], references: [id], onDelete: Restrict)
|
||||
carbet Carbet @relation(fields: [carbetId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([ownerId])
|
||||
@@index([carbetId])
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
model Review {
|
||||
id String @id @default(cuid())
|
||||
bookingId String @unique
|
||||
carbetId String
|
||||
authorId String
|
||||
rating Int
|
||||
comment String?
|
||||
hostResponse String?
|
||||
hostRespondedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Cascade)
|
||||
carbet Carbet @relation(fields: [carbetId], references: [id], onDelete: Restrict)
|
||||
author User @relation("ReviewAuthor", fields: [authorId], references: [id], onDelete: Restrict)
|
||||
|
||||
@@index([carbetId])
|
||||
@@index([authorId])
|
||||
}
|
||||
|
|
|
|||
14
src/app/admin/page.tsx
Normal file
14
src/app/admin/page.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { requireRole } from "@/lib/authorization";
|
||||
|
||||
export default async function AdminPage() {
|
||||
const session = await requireRole(["ADMIN"]);
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-4xl px-6 py-12">
|
||||
<h1 className="text-3xl font-semibold">Espace administrateur</h1>
|
||||
<p className="mt-4 text-zinc-700">
|
||||
Accès autorisé pour {session.user.email} ({session.user.role}).
|
||||
</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
3
src/app/api/auth/[...nextauth]/route.ts
Normal file
3
src/app/api/auth/[...nextauth]/route.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { handlers } from "@/auth";
|
||||
|
||||
export const { GET, POST } = handlers;
|
||||
54
src/app/connexion/page.tsx
Normal file
54
src/app/connexion/page.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { redirect } from "next/navigation";
|
||||
|
||||
import { auth, signIn } from "@/auth";
|
||||
|
||||
export default async function SignInPage() {
|
||||
const session = await auth();
|
||||
if (session?.user?.id) {
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="mx-auto flex min-h-[70vh] max-w-md items-center px-6 py-12">
|
||||
<form
|
||||
action={async (formData) => {
|
||||
"use server";
|
||||
await signIn("credentials", formData);
|
||||
}}
|
||||
className="w-full space-y-4 rounded-xl border border-zinc-200 bg-white p-6"
|
||||
>
|
||||
<h1 className="text-2xl font-semibold">Connexion</h1>
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="email" className="text-sm text-zinc-700">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
className="w-full rounded-md border border-zinc-300 px-3 py-2"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="password" className="text-sm text-zinc-700">
|
||||
Mot de passe
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
className="w-full rounded-md border border-zinc-300 px-3 py-2"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full rounded-md bg-black px-3 py-2 text-white"
|
||||
>
|
||||
Se connecter
|
||||
</button>
|
||||
</form>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
25
src/app/espace-hote/page.tsx
Normal file
25
src/app/espace-hote/page.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import Link from "next/link";
|
||||
|
||||
import { requireRole } from "@/lib/authorization";
|
||||
|
||||
export default async function HostPage() {
|
||||
const session = await requireRole(["OWNER", "ADMIN"]);
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-4xl px-6 py-12">
|
||||
<h1 className="text-3xl font-semibold">Espace hôte</h1>
|
||||
<p className="mt-4 text-zinc-700">
|
||||
Accès autorisé pour {session.user.email} ({session.user.role}).
|
||||
</p>
|
||||
|
||||
<div className="mt-8">
|
||||
<Link
|
||||
href="/espace-hote/carbets"
|
||||
className="inline-block rounded-md bg-emerald-600 px-5 py-2.5 text-sm font-semibold text-white hover:bg-emerald-700"
|
||||
>
|
||||
Gérer mes carbets
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
75
src/auth.ts
Normal file
75
src/auth.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import NextAuth from "next-auth";
|
||||
import Credentials from "next-auth/providers/credentials";
|
||||
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { verifyPassword } from "@/lib/password";
|
||||
|
||||
export const { handlers, auth, signIn, signOut } = NextAuth({
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
},
|
||||
providers: [
|
||||
Credentials({
|
||||
name: "Email et mot de passe",
|
||||
credentials: {
|
||||
email: { label: "Email", type: "email" },
|
||||
password: { label: "Mot de passe", type: "password" },
|
||||
},
|
||||
async authorize(credentials) {
|
||||
const email = credentials?.email?.toString().trim().toLowerCase();
|
||||
const password = credentials?.password?.toString() ?? "";
|
||||
|
||||
if (!email || !password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
role: true,
|
||||
isActive: true,
|
||||
passwordHash: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isValid = await verifyPassword(password, user.passwordHash);
|
||||
if (!isValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: `${user.firstName} ${user.lastName}`.trim(),
|
||||
role: user.role,
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
async jwt({ token, user }) {
|
||||
if (user?.role) {
|
||||
token.role = user.role;
|
||||
}
|
||||
return token;
|
||||
},
|
||||
async session({ session, token }) {
|
||||
if (session.user) {
|
||||
session.user.id = token.sub ?? "";
|
||||
session.user.role = token.role;
|
||||
}
|
||||
return session;
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
signIn: "/connexion",
|
||||
},
|
||||
});
|
||||
23
src/lib/authorization.ts
Normal file
23
src/lib/authorization.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { redirect } from "next/navigation";
|
||||
|
||||
import { auth } from "@/auth";
|
||||
import type { UserRole } from "@/generated/prisma/enums";
|
||||
|
||||
export async function requireAuth() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
redirect("/connexion");
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function requireRole(allowedRoles: UserRole[]) {
|
||||
const session = await requireAuth();
|
||||
|
||||
if (!session.user.role || !allowedRoles.includes(session.user.role)) {
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
12
src/lib/password.ts
Normal file
12
src/lib/password.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import bcrypt from "bcryptjs";
|
||||
|
||||
export async function verifyPassword(
|
||||
plainTextPassword: string,
|
||||
hashedPassword: string,
|
||||
): Promise<boolean> {
|
||||
return bcrypt.compare(plainTextPassword, hashedPassword);
|
||||
}
|
||||
|
||||
export async function hashPassword(plainTextPassword: string): Promise<string> {
|
||||
return bcrypt.hash(plainTextPassword, 12);
|
||||
}
|
||||
20
src/lib/prisma.ts
Normal file
20
src/lib/prisma.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { PrismaPg } from "@prisma/adapter-pg";
|
||||
|
||||
import { PrismaClient } from "@/generated/prisma/client";
|
||||
|
||||
const connectionString = process.env.DATABASE_URL;
|
||||
if (!connectionString) {
|
||||
throw new Error("DATABASE_URL is required");
|
||||
}
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma?: PrismaClient;
|
||||
};
|
||||
|
||||
const adapter = new PrismaPg({ connectionString });
|
||||
|
||||
export const prisma = globalForPrisma.prisma ?? new PrismaClient({ adapter });
|
||||
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
globalForPrisma.prisma = prisma;
|
||||
}
|
||||
23
src/types/next-auth.d.ts
vendored
Normal file
23
src/types/next-auth.d.ts
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import type { DefaultSession } from "next-auth";
|
||||
import type { JWT as DefaultJWT } from "next-auth/jwt";
|
||||
|
||||
import type { UserRole } from "@/generated/prisma/enums";
|
||||
|
||||
declare module "next-auth" {
|
||||
interface Session {
|
||||
user: {
|
||||
id: string;
|
||||
role?: UserRole;
|
||||
} & DefaultSession["user"];
|
||||
}
|
||||
|
||||
interface User {
|
||||
role?: UserRole;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "next-auth/jwt" {
|
||||
interface JWT extends DefaultJWT {
|
||||
role?: UserRole;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue