Compare commits

..

20 Commits

Author SHA1 Message Date
Mathis HERRIOT
ac5cb96f97 chore(docs): remove deprecated test documentation file
Some checks failed
Lint / lint (push) Failing after 4m56s
2026-01-05 14:16:00 +01:00
Mathis HERRIOT
2389d2c2c6 feat(docs): add comprehensive technical documentation for Memegoat
Includes detailed sections on architecture, stack, data model, security measures, deployment procedures, compliance (GDPR), and API integrations.
2026-01-05 14:15:56 +01:00
Mathis HERRIOT
694031c05b feat: add API keys schema with Drizzle ORM integration 2026-01-05 14:15:35 +01:00
Mathis HERRIOT
cbf7bfcb0a feat: add audit logs schema with Drizzle ORM integration 2026-01-05 14:15:32 +01:00
Mathis HERRIOT
9fb890699a feat: add content schema with Drizzle ORM integration 2026-01-05 14:15:28 +01:00
Mathis HERRIOT
9439c004e2 feat: add RBAC schemas with Drizzle ORM integration 2026-01-05 14:15:22 +01:00
Mathis HERRIOT
27954daf64 feat: add reports schema with Drizzle ORM integration 2026-01-05 14:15:18 +01:00
Mathis HERRIOT
7001082fb2 feat: add sessions schema with Drizzle ORM integration 2026-01-05 14:15:13 +01:00
Mathis HERRIOT
04ca5090df feat: add tags schema with Drizzle ORM integration 2026-01-05 14:14:57 +01:00
Mathis HERRIOT
19ceac1303 feat: export additional schemas for RBAC, sessions, API keys, tags, content, reports, and audit logs 2026-01-05 14:14:51 +01:00
Mathis HERRIOT
381ca24501 feat: enhance user schema with PGP encryption, GDPR fields, and 2FA support 2026-01-05 14:14:42 +01:00
Mathis HERRIOT
eefe2906ed feat: add database scripts for Drizzle ORM (generate, migrate, studio) in backend package.json 2026-01-05 14:14:22 +01:00
Mathis HERRIOT
8ee0491c96 chore: update Drizzle ORM schema path in configuration 2026-01-05 14:14:08 +01:00
Mathis HERRIOT
73aea94d88 chore: update pnpm-lock.yaml with dependency additions and updates, including Drizzle ORM, PostgreSQL support, and dotenv integration 2026-01-05 12:10:57 +01:00
Mathis HERRIOT
7761e26d32 feat: add user schema with PostgreSQL integration using Drizzle ORM 2026-01-05 12:10:49 +01:00
Mathis HERRIOT
6c4f1694ba chore: remove GitHub Actions workflows for linting backend, frontend, and documentation 2026-01-05 12:10:40 +01:00
Mathis HERRIOT
0a84ad1595 chore: add GitHub Actions workflow for linting frontend, backend, and documentation 2026-01-05 12:10:29 +01:00
Mathis HERRIOT
43b4334971 feat: add Drizzle ORM configuration for PostgreSQL integration in backend 2026-01-05 12:08:01 +01:00
Mathis HERRIOT
07f905d7c9 feat: add support for dotenv, PostgreSQL, and Drizzle ORM in backend dependencies 2026-01-05 12:07:46 +01:00
Mathis HERRIOT
72f3bb7723 feat: implement database module and service with PostgreSQL integration and migrations support 2026-01-05 12:07:12 +01:00
26 changed files with 1685 additions and 121 deletions

View File

@@ -1,25 +0,0 @@
name: Backend Lint
on:
push:
paths:
- 'backend/**'
pull_request:
paths:
- 'backend/**'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Run lint
run: pnpm -F @memegoat/backend lint

View File

@@ -1,25 +0,0 @@
name: Documentation Lint
on:
push:
paths:
- 'documentation/**'
pull_request:
paths:
- 'documentation/**'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Run lint
run: pnpm -F @memegoat/documentation lint

View File

@@ -1,11 +1,15 @@
name: Frontend Lint
name: Lint
on:
push:
paths:
- 'frontend/**'
- 'backend/**'
- 'documentation/**'
pull_request:
paths:
- 'frontend/**'
- 'backend/**'
- 'documentation/**'
jobs:
lint:
@@ -21,5 +25,12 @@ jobs:
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Run lint
- name: Lint Frontend
if: success() || failure()
run: pnpm -F @memegoat/frontend lint
- name: Lint Backend
if: success() || failure()
run: pnpm -F @memegoat/backend lint
- name: Lint Documentation
if: success() || failure()
run: pnpm -F @bypass/documentation lint

19
backend/drizzle.config.ts Normal file
View File

@@ -0,0 +1,19 @@
import { defineConfig } from 'drizzle-kit';
import * as process from "node:process";
export default defineConfig({
schema: './src/database/schemas/index.ts',
out: '.migrations',
dialect: "postgresql",
casing: "snake_case",
dbCredentials: {
host: String(process.env.POSTGRES_HOST || "localhost"),
port: Number(process.env.POSTGRES_PORT || 5432),
database: String(process.env.POSTGRES_DB || "app"),
user: String(process.env.POSTGRES_USER || "app"),
password: String(process.env.POSTGRES_PASSWORD || "app"),
ssl: false,
},
verbose: true,
strict: true,
});

View File

@@ -17,12 +17,19 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
"test:e2e": "jest --config ./test/jest-e2e.json",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio"
},
"dependencies": {
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.0.1",
"dotenv": "^17.2.3",
"drizzle-orm": "^0.45.1",
"pg": "^8.16.3",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
},
@@ -33,7 +40,9 @@
"@types/express": "^5.0.0",
"@types/jest": "^30.0.0",
"@types/node": "^22.10.7",
"@types/pg": "^8.16.0",
"@types/supertest": "^6.0.2",
"drizzle-kit": "^0.31.8",
"globals": "^16.0.0",
"jest": "^30.0.0",
"source-map-support": "^0.5.21",
@@ -42,6 +51,7 @@
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.21.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},

View File

@@ -0,0 +1,10 @@
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { DatabaseService } from "./database.service";
@Module({
imports: [ConfigModule],
providers: [DatabaseService],
exports: [DatabaseService],
})
export class DatabaseModule {}

View File

@@ -0,0 +1,86 @@
/*
* Copyright (C) 2025 Yidhra Studio. - All Rights Reserved
* Updated : 25/04/2025 10:52
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
import {Injectable, Logger, OnModuleDestroy, OnModuleInit} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { drizzle } from "drizzle-orm/node-postgres";
import { migrate } from "drizzle-orm/node-postgres/migrator";
import * as schema from "./schemas";
import {Pool} from "pg";
@Injectable()
export class DatabaseService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(DatabaseService.name);
private readonly pool!: Pool
public readonly db: ReturnType<typeof drizzle>;
constructor(
private configService: ConfigService
) {
// Create the PostgreSQL client
const connectionString = this.getDatabaseConnectionString();
this.pool = new Pool({ connectionString });
// Recreate drizzle with initialized pool
this.db = drizzle(this.pool, { schema });
}
async onModuleInit() {
try {
// Run migrations if in production mode
if (this.configService.get("NODE_ENV") === "production") {
this.logger.log("Running database migrations...");
await migrate(this.db, { migrationsFolder: ".migrations" });
this.logger.log("Database migrations completed successfully");
} else {
this.logger.debug("Skipping migrations in non-production environment");
}
this.logger.log("Database connection established successfully");
} catch (error) {
this.logger.error("Failed to initialize database connection", error);
throw error;
}
}
async onModuleDestroy() {
try {
// Close the database connection
await this.pool.end();
this.logger.log("Database connection closed successfully");
} catch (error) {
this.logger.error("Error closing database connection", error);
}
}
// Get the database connection string from environment variables
private getDatabaseConnectionString(): string {
this.logger.debug('Getting database connection string from environment variables');
const password = this.configService.get<string>("POSTGRES_PASSWORD");
const username = this.configService.get<string>("POSTGRES_USER");
const host = this.configService.get<string>("POSTGRES_HOST");
const port = this.configService.get<string>("POSTGRES_PORT");
const database = this.configService.get<string>("POSTGRES_DB");
const missingVars: string[] = [];
if (!password) missingVars.push("POSTGRES_PASSWORD");
if (!username) missingVars.push("POSTGRES_USER");
if (!host) missingVars.push("POSTGRES_HOST");
if (!port) missingVars.push("POSTGRES_PORT");
if (!database) missingVars.push("POSTGRES_DB");
if (missingVars.length > 0) {
const errorMessage = `Database configuration is missing. Missing variables: ${missingVars.join(", ")}. Please check your .env file.`;
this.logger.error(errorMessage);
throw new Error(errorMessage);
}
this.logger.debug(`Database connection configured for ${username}@${host}:${port}/${database}`);
return `postgres://${username}:${password}@${host}:${port}/${database}`;
}
}

View File

@@ -0,0 +1,18 @@
import { pgTable, varchar, timestamp, uuid, index, boolean } from 'drizzle-orm/pg-core';
import { users } from './users';
export const apiKeys = pgTable('api_keys', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').notNull().references(() => users.uuid, { onDelete: 'cascade' }),
keyHash: varchar('key_hash', { length: 128 }).notNull().unique(), // Haché pour la sécurité (SHA-256)
name: varchar('name', { length: 128 }).notNull(), // Nom donné par l'utilisateur (ex: "My App")
prefix: varchar('prefix', { length: 8 }).notNull(), // Pour identification visuelle (ex: "mg_...")
isActive: boolean('is_active').notNull().default(true),
lastUsedAt: timestamp('last_used_at', { withTimezone: true }),
expiresAt: timestamp('expires_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => ({
userIdIdx: index('api_keys_user_id_idx').on(table.userId),
keyHashIdx: index('api_keys_key_hash_idx').on(table.keyHash),
}));

View File

@@ -0,0 +1,25 @@
import { pgTable, varchar, timestamp, uuid, index, jsonb } from 'drizzle-orm/pg-core';
import { users } from './users';
export const auditLogs = pgTable('audit_logs', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').references(() => users.uuid, { onDelete: 'set null' }), // L'utilisateur qui a fait l'action
action: varchar('action', { length: 64 }).notNull(), // ex: 'PII_ACCESS', 'USER_DELETE', 'ROLE_CHANGE'
entityType: varchar('entity_type', { length: 64 }).notNull(), // ex: 'users', 'contents'
entityId: uuid('entity_id'), // ID de l'entité concernée
// Détails de l'action pour la conformité
details: jsonb('details'), // Données supplémentaires (ex: quelles colonnes ont changé)
ipHash: varchar('ip_hash', { length: 64 }), // IP de l'auteur (hachée pour RGPD)
userAgent: varchar('user_agent', { length: 255 }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => ({
userIdIdx: index('audit_logs_user_id_idx').on(table.userId),
actionIdx: index('audit_logs_action_idx').on(table.action),
entityIdx: index('audit_logs_entity_idx').on(table.entityType, table.entityId),
createdAtIdx: index('audit_logs_created_at_idx').on(table.createdAt),
}));
export type AuditLogInDb = typeof auditLogs.$inferSelect;
export type NewAuditLogInDb = typeof auditLogs.$inferInsert;

View File

@@ -0,0 +1,32 @@
import { pgTable, varchar, timestamp, uuid, pgEnum, index, primaryKey, integer } from 'drizzle-orm/pg-core';
import { users } from './users';
import { tags } from './tags';
export const contentType = pgEnum('content_type', ['meme', 'gif']);
export const contents = pgTable('contents', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').notNull().references(() => users.uuid, { onDelete: 'cascade' }),
type: contentType('type').notNull(),
title: varchar('title', { length: 255 }).notNull(),
storageKey: varchar('storage_key', { length: 512 }).notNull().unique(), // Clé interne S3
mimeType: varchar('mime_type', { length: 128 }).notNull(), // Pour le Content-Type HTTP
fileSize: integer('file_size').notNull(), // Taille en octets
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
deletedAt: timestamp('deleted_at', { withTimezone: true }), // Soft delete
}, (table) => ({
userIdIdx: index('contents_user_id_idx').on(table.userId),
storageKeyIdx: index('contents_storage_key_idx').on(table.storageKey),
deletedAtIdx: index('contents_deleted_at_idx').on(table.deletedAt),
}));
export const contentsToTags = pgTable('contents_to_tags', {
contentId: uuid('content_id').notNull().references(() => contents.id, { onDelete: 'cascade' }),
tagId: uuid('tag_id').notNull().references(() => tags.id, { onDelete: 'cascade' }),
}, (t) => ({
pk: primaryKey({ columns: [t.contentId, t.tagId] }),
}));
export type ContentInDb = typeof contents.$inferSelect;
export type NewContentInDb = typeof contents.$inferInsert;

View File

@@ -0,0 +1,8 @@
export * from './users';
export * from './rbac';
export * from './sessions';
export * from './api_keys';
export * from './tags';
export * from './content';
export * from './reports';
export * from './audit_logs';

View File

@@ -0,0 +1,36 @@
import { pgTable, varchar, timestamp, uuid, primaryKey, index } from 'drizzle-orm/pg-core';
import { users } from './users';
export const roles = pgTable('roles', {
id: uuid('id').primaryKey().defaultRandom(),
name: varchar('name', { length: 64 }).notNull().unique(),
slug: varchar('slug', { length: 64 }).notNull().unique(),
description: varchar('description', { length: 128 }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => ({
slugIdx: index('roles_slug_idx').on(table.slug),
}));
export const permissions = pgTable('permissions', {
id: uuid('id').primaryKey().defaultRandom(),
name: varchar('name', { length: 64 }).notNull().unique(),
slug: varchar('slug', { length: 64 }).notNull().unique(),
description: varchar('description', { length: 128 }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => ({
slugIdx: index('permissions_slug_idx').on(table.slug),
}));
export const rolesToPermissions = pgTable('roles_to_permissions', {
roleId: uuid('role_id').notNull().references(() => roles.id, { onDelete: 'cascade' }),
permissionId: uuid('permission_id').notNull().references(() => permissions.id, { onDelete: 'cascade' }),
}, (t) => ({
pk: primaryKey({ columns: [t.roleId, t.permissionId] }),
}));
export const usersToRoles = pgTable('users_to_roles', {
userId: uuid('user_id').notNull().references(() => users.uuid, { onDelete: 'cascade' }),
roleId: uuid('role_id').notNull().references(() => roles.id, { onDelete: 'cascade' }),
}, (t) => ({
pk: primaryKey({ columns: [t.userId, t.roleId] }),
}));

View File

@@ -0,0 +1,33 @@
import { pgTable, varchar, timestamp, uuid, pgEnum, index, text } from 'drizzle-orm/pg-core';
import { users } from './users';
import { contents } from './content';
import { tags } from './tags';
export const reportStatus = pgEnum('report_status', ['pending', 'reviewed', 'resolved', 'dismissed']);
export const reportReason = pgEnum('report_reason', ['inappropriate', 'spam', 'copyright', 'other']);
export const reports = pgTable('reports', {
id: uuid('id').primaryKey().defaultRandom(),
reporterId: uuid('reporter_id').notNull().references(() => users.uuid, { onDelete: 'cascade' }),
// Le signalement peut porter sur un contenu OU un tag
contentId: uuid('content_id').references(() => contents.id, { onDelete: 'cascade' }),
tagId: uuid('tag_id').references(() => tags.id, { onDelete: 'cascade' }),
reason: reportReason('reason').notNull(),
description: text('description'),
status: reportStatus('status').default('pending').notNull(),
expiresAt: timestamp('expires_at', { withTimezone: true }), // Pour purge automatique RGPD
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => ({
reporterIdx: index('reports_reporter_id_idx').on(table.reporterId),
contentIdx: index('reports_content_id_idx').on(table.contentId),
tagIdx: index('reports_tag_id_idx').on(table.tagId),
statusIdx: index('reports_status_idx').on(table.status),
expiresAtIdx: index('reports_expires_at_idx').on(table.expiresAt),
}));
export type ReportInDb = typeof reports.$inferSelect;
export type NewReportInDb = typeof reports.$inferInsert;

View File

@@ -0,0 +1,18 @@
import { pgTable, varchar, timestamp, uuid, index, boolean } from 'drizzle-orm/pg-core';
import { users } from './users';
export const sessions = pgTable('sessions', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').notNull().references(() => users.uuid, { onDelete: 'cascade' }),
refreshToken: varchar('refresh_token', { length: 512 }).notNull().unique(),
userAgent: varchar('user_agent', { length: 255 }),
ipHash: varchar('ip_hash', { length: 64 }), // IP hachée pour la protection de la vie privée (RGPD)
isValid: boolean('is_valid').notNull().default(true),
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => ({
userIdIdx: index('sessions_user_id_idx').on(table.userId),
refreshTokenIdx: index('sessions_refresh_token_idx').on(table.refreshToken),
expiresAtIdx: index('sessions_expires_at_idx').on(table.expiresAt),
}));

View File

@@ -0,0 +1,14 @@
import { pgTable, varchar, timestamp, uuid, index } from 'drizzle-orm/pg-core';
export const tags = pgTable('tags', {
id: uuid('id').primaryKey().defaultRandom(),
name: varchar('name', { length: 64 }).notNull().unique(),
slug: varchar('slug', { length: 64 }).notNull().unique(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => ({
slugIdx: index('tags_slug_idx').on(table.slug),
}));
export type TagInDb = typeof tags.$inferSelect;
export type NewTagInDb = typeof tags.$inferInsert;

View File

@@ -0,0 +1,46 @@
import {pgTable, varchar, timestamp, uuid, pgEnum, index, boolean, customType} from 'drizzle-orm/pg-core';
// Type personnalisé pour les données chiffrées PGP (stockées en bytea dans Postgres)
const pgpEncrypted = customType<{ data: string; driverData: string }>({
dataType() {
return 'bytea';
},
});
export const userStatus = pgEnum("user_status", ["active", "verification", "suspended", "pending", "deleted"])
export const users = pgTable('users', {
uuid: uuid().primaryKey().defaultRandom(),
status: userStatus('status').default('pending').notNull(),
// Données Personnelles (PII) - Chiffrées nativement
email: pgpEncrypted('email').notNull(),
emailHash: varchar('email_hash', { length: 64 }).notNull().unique(), // Indexé pour recherche rapide et unicité
displayName: varchar('display_name', { length: 32 }),
username: varchar('username', { length: 32 }).notNull().unique(),
passwordHash: varchar('password_hash', { length: 72 }).notNull(),
// Sécurité
twoFactorSecret: pgpEncrypted('two_factor_secret'),
isTwoFactorEnabled: boolean('is_two_factor_enabled').notNull().default(false),
// RGPD & Conformité
termsVersion: varchar('terms_version', { length: 16 }), // Version des CGU acceptées
privacyVersion: varchar('privacy_version', { length: 16 }), // Version de la Privacy Policy acceptée
gdprAcceptedAt: timestamp('gdpr_accepted_at', { withTimezone: true }),
// Dates de cycle de vie (Standards Entreprise)
lastLoginAt: timestamp('last_login_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
deletedAt: timestamp('deleted_at', { withTimezone: true }), // Soft delete (Droit à l'oubli)
}, (table) => ({
uuidIdx: index('users_uuid_idx').on(table.uuid),
emailHashIdx: index('users_email_hash_idx').on(table.emailHash),
usernameIdx: index('users_username_idx').on(table.username),
statusIdx: index('users_status_idx').on(table.status),
}));
export type UserInDb = typeof users.$inferSelect;
export type NewUserInDb = typeof users.$inferInsert;

View File

@@ -0,0 +1,9 @@
{
"index": "Introduction",
"stack": "Stack Technologique",
"database": "Modèle de Données",
"api": "API & Intégrations",
"security": "Sécurité",
"compliance": "Conformité (RGPD)",
"deployment": "Déploiement & Tests"
}

View File

@@ -0,0 +1,21 @@
---
title: API & Intégrations
description: Documentation des API et services tiers
---
## 🔌 API & Intégrations
### Documentation API
Documentation MDX.
### Authentification
Le système utilise plusieurs méthodes d'authentification sécurisées :
- **Sessions (JWT)** : Utilisation de JSON Web Tokens signés pour les sessions utilisateurs via le web. Les sessions sont persistées en base de données (`sessions`) pour permettre la révocation (Logout) et le suivi des appareils connectés.
- **API Keys** : Pour les intégrations programmatiques. Les clés sont hachées en base de données (`key_hash`) et associées à un utilisateur. Elles peuvent être nommées et révoquées individuellement.
- **Double Authentification (2FA)** : Support natif (TOTP) avec secret chiffré en base de données.
### Webhooks / Services Externes
Liste des intégrations tierces.

View File

@@ -0,0 +1,44 @@
---
title: Conformité (RGPD & France)
description: Mesures prises pour la conformité au RGPD et aux réglementations françaises.
---
## ⚖️ Conformité Réglementaire
Le projet Memegoat s'inscrit dans une démarche de respect de la vie privée et de protection des données à caractère personnel, conformément au **Règlement Général sur la Protection des Données (RGPD)** de l'Union Européenne et aux recommandations de la **CNIL**.
### 🛡️ Principes Fondamentaux
- **Minimisation des données** : Seules les données strictement nécessaires au fonctionnement du service sont collectées.
- **Transparence** : Les utilisateurs sont informés de la finalité des traitements de leurs données.
- **Sécurité** : Mise en œuvre de mesures techniques et organisationnelles pour protéger les données.
### 🔒 Mesures Techniques de Protection
Conformément à la section [Sécurité](/docs/security), les mesures suivantes sont appliquées :
- **Chiffrement au repos** : Utilisation de **PGP (pgcrypto)** pour les données identifiantes.
- **Hachage aveugle** : Pour permettre les opérations sur données chiffrées sans compromettre la confidentialité.
- **Hachage des mots de passe** : Utilisation de l'algorithme **Argon2id**.
- **Communications sécurisées** : Utilisation de **TLS 1.3** via Caddy.
### 👤 Droits des Utilisateurs
Conformément au RGPD, les utilisateurs disposent des droits suivants, facilités par l'architecture technique :
- **Droit à l'effacement (droit à l'oubli)** : Mis en œuvre via un mécanisme de **Soft Delete** (`deleted_at`), suivi d'une purge définitive des données après un délai de conservation légal.
- **Droit d'accès et portabilité** : L'utilisation de schémas structurés (Drizzle/PostgreSQL) permet l'extraction facile des données d'un utilisateur sur demande.
- **Gestion du consentement** : Suivi rigoureux des versions de CGU et de politique de confidentialité acceptées (`terms_version`, `privacy_version`, `gdpr_accepted_at`).
### ⏳ Conservation et Purge
- **Purge Automatique** : Les données liées aux signalements (`reports`) disposent d'une date d'expiration (`expires_at`) pour garantir qu'elles ne sont pas conservées au-delà du nécessaire.
- **Anonymisation technique** : Les adresses IP stockées dans les tables `audit_logs` et `sessions` sont hachées (`ip_hash`), ce qui permet d'identifier des comportements malveillants tout en protégeant l'identité réelle de l'utilisateur.
- **Logs d'Audit** : Les journaux d'audit sont conservés pendant une période glissante pour répondre aux obligations de sécurité tout en respectant la minimisation.
### 📍 Hébergement des Données
Les données sont hébergées au sein de l'Union Européenne sur des serveurs dédiés chez **Hetzner**, garantissant un cadre juridique protecteur pour les données des citoyens européens.
### 🔗 Références
- [Site officiel de la CNIL](https://www.cnil.fr/fr/reglement-europeen-protection-donnees)
- [Texte officiel du RGPD](https://eur-lex.europa.eu/legal-content/FR/TXT/?uri=CELEX%3A32016R0679)

View File

@@ -0,0 +1,257 @@
---
title: Modèle de Données
description: Structure et organisation des données
---
## 📊 Modèle de Données
### Conceptuel (MCD)
Le Modèle Conceptuel de Données décrit les grandes entités du système et leurs relations métier, incluant la gestion des accès et la modération.
```mermaid
erDiagram
USER ||--o{ CONTENT : "publie"
USER ||--o{ REPORT : "signale"
USER ||--o{ USER_ROLE : "possede"
USER ||--o{ SESSION : "detient"
USER ||--o{ API_KEY : "genere"
USER ||--o{ AUDIT_LOG : "genere"
CONTENT ||--o{ CONTENT_TAG : "possede"
TAG ||--o{ CONTENT_TAG : "est_lie_a"
CONTENT ||--o{ REPORT : "est_signale"
TAG ||--o{ REPORT : "est_signale"
ROLE ||--o{ USER_ROLE : "attribue_a"
ROLE ||--o{ ROLE_PERMISSION : "possede"
PERMISSION ||--o{ ROLE_PERMISSION : "est_lie_a"
USER {
string username
string email
string display_name
string status
}
CONTENT {
string title
string type
string storage_key
}
TAG {
string name
string slug
}
ROLE {
string name
string slug
}
REPORT {
string reason
string status
}
SESSION {
string refresh_token
boolean is_valid
}
API_KEY {
string name
string prefix
boolean is_active
}
AUDIT_LOG {
string action
string entity_type
jsonb details
}
```
### Logique (MLD)
Le Modèle Logique de Données précise les tables, les colonnes et les clés étrangères (FK).
```mermaid
erDiagram
users {
uuid uuid PK
varchar username
bytea email
varchar email_hash
varchar display_name
varchar password_hash
user_status status
bytea two_factor_secret
boolean is_two_factor_enabled
varchar terms_version
varchar privacy_version
timestamp gdpr_accepted_at
timestamp last_login_at
timestamp created_at
timestamp updated_at
timestamp deleted_at
}
contents {
uuid id PK
uuid user_id FK
content_type type
varchar title
varchar storage_key
varchar mime_type
integer file_size
timestamp created_at
timestamp updated_at
timestamp deleted_at
}
tags {
uuid id PK
varchar name
varchar slug
timestamp created_at
timestamp updated_at
}
contents_to_tags {
uuid content_id PK, FK
uuid tag_id PK, FK
}
roles {
uuid id PK
varchar name
varchar slug
varchar description
timestamp created_at
}
permissions {
uuid id PK
varchar name
varchar slug
varchar description
timestamp created_at
}
roles_to_permissions {
uuid role_id PK, FK
uuid permission_id PK, FK
}
users_to_roles {
uuid user_id PK, FK
uuid role_id PK, FK
}
reports {
uuid id PK
uuid reporter_id FK
uuid content_id FK
uuid tag_id FK
report_reason reason
text description
report_status status
timestamp expires_at
timestamp created_at
timestamp updated_at
}
sessions {
uuid id PK
uuid user_id FK
varchar refresh_token
varchar user_agent
varchar ip_hash
boolean is_valid
timestamp expires_at
timestamp created_at
timestamp updated_at
}
api_keys {
uuid id PK
uuid user_id FK
varchar key_hash
varchar name
varchar prefix
boolean is_active
timestamp last_used_at
timestamp expires_at
timestamp created_at
timestamp updated_at
}
audit_logs {
uuid id PK
uuid user_id FK
varchar action
varchar entity_type
uuid entity_id
jsonb details
varchar ip_hash
varchar user_agent
timestamp created_at
}
users ||--o{ contents : "user_id"
users ||--o{ users_to_roles : "user_id"
roles ||--o{ users_to_roles : "role_id"
roles ||--o{ roles_to_permissions : "role_id"
permissions ||--o{ roles_to_permissions : "permission_id"
contents ||--o{ contents_to_tags : "content_id"
tags ||--o{ contents_to_tags : "tag_id"
users ||--o{ reports : "reporter_id"
contents ||--o{ reports : "content_id"
tags ||--o{ reports : "tag_id"
users ||--o{ sessions : "user_id"
users ||--o{ api_keys : "user_id"
users ||--o{ audit_logs : "user_id"
```
### Physique (MPD)
Le Modèle Physique de Données détaille l'implémentation spécifique pour **PostgreSQL**.
```mermaid
erDiagram
users {
uuid uuid "DEFAULT gen_random_uuid()"
bytea email "ENCRYPTED, NOT NULL"
varchar email_hash "UNIQUE, INDEXED"
varchar username "UNIQUE, NOT NULL"
varchar password_hash "NOT NULL"
bytea two_factor_secret "ENCRYPTED"
boolean is_two_factor_enabled "DEFAULT false"
timestamp gdpr_accepted_at "NULLABLE"
timestamp deleted_at "SOFT DELETE"
}
contents {
uuid id "DEFAULT gen_random_uuid()"
uuid user_id "REFERENCES users(uuid)"
varchar storage_key "UNIQUE, NOT NULL"
integer file_size "NOT NULL"
timestamp deleted_at "SOFT DELETE"
}
reports {
uuid id "DEFAULT gen_random_uuid()"
uuid reporter_id "REFERENCES users(uuid)"
timestamp expires_at "RGPD PURGE"
}
api_keys {
uuid id "DEFAULT gen_random_uuid()"
varchar key_hash "UNIQUE, INDEXED, SHA-256"
varchar prefix "NOT NULL"
}
audit_logs {
uuid id "DEFAULT gen_random_uuid()"
jsonb details "STORED AS JSONB"
varchar ip_hash "RGPD COMPLIANT"
}
sessions {
uuid id "DEFAULT gen_random_uuid()"
varchar refresh_token "UNIQUE, NOT NULL"
varchar ip_hash "RGPD COMPLIANT"
}
```
#### Sécurité et Chiffrement
- **Chiffrement PGP (Native)** : Les colonnes `email` et `two_factor_secret` sont stockées au format `bytea` et chiffrées/déchiffrées via les fonctions `pgp_sym_encrypt` et `pgp_sym_decrypt` de PostgreSQL (via l'extension `pgcrypto`).
- **Hachage aveugle (Blind Indexing)** : La colonne `email_hash` stocke un hash (SHA-256) de l'email pour permettre les recherches d'unicité et les recherches rapides sans déchiffrer la donnée.
#### Index et Optimisations
- **Index B-Tree** systématiques sur toutes les clés étrangères (`user_id`, `role_id`, etc.).
- **Index sur `deleted_at`** : Pour optimiser les requêtes excluant les données supprimées logiciellement.
- **Index unique sur `email_hash`** et `username`.
#### Conformité RGPD
- **Soft Delete** : Implémenté via `deleted_at` pour permettre le "droit à l'oubli" tout en conservant l'intégrité référentielle temporaire.
- **Purge Automatique** : La colonne `expires_at` dans la table `reports` permet de programmer la suppression automatique des données de signalement après traitement.

View File

@@ -0,0 +1,18 @@
---
title: Déploiement & Tests
description: Procédures de déploiement et stratégie de tests
---
## 🚀 Déploiement
### Architecture de service
Un conteneur **Caddy** est utilisé en tant que reverse proxy pour fournir le TLS et la gestion du FQDN.
### Pré-requis
Liste des outils nécessaires (Node.js, pnpm, Docker).
## 🧪 Tests
- **Unitaires** : sur le backend

View File

@@ -1,13 +1,42 @@
---
title: Hello World
description: Your first document
title: Introduction
description: Détails techniques du projet Memegoat
---
Welcome to the docs! You can start writing documents in `/content/docs`.
# 🐐 Détails Techniques - Memegoat
## What is Next?
Ce document regroupe l'ensemble des spécifications techniques du projet Memegoat.
<Cards>
<Card title="Learn more about Next.js" href="https://nextjs.org/docs" />
<Card title="Learn more about Fumadocs" href="https://fumadocs.dev" />
</Cards>
## 🏗️ Architecture Globale
### Vue d'ensemble
Description de l'architecture en monorepo et des interactions entre les services.
### Diagrammes
```mermaid
graph TD
User([Utilisateur])
Caddy[Reverse Proxy: Caddy]
Frontend[Frontend: Next.js]
Backend[Backend: NestJS]
DB[(Database: PostgreSQL)]
Storage[Storage: S3 Compatible]
User <--> Caddy
Caddy <--> Frontend
Caddy <--> Backend
Backend <--> DB
Backend <--> Storage
```
### Navigation
Consultez les différentes sections pour plus de détails :
- [Stack Technologique](/docs/stack)
- [Modèle de Données](/docs/database)
- [Sécurité](/docs/security)
- [Conformité RGPD](/docs/compliance)
- [API & Intégrations](/docs/api)
- [Déploiement](/docs/deployment)

View File

@@ -0,0 +1,26 @@
---
title: Sécurité
description: Mesures de sécurité implémentées
---
## 🔐 Sécurité
### Protection des Données (At Rest)
- **Chiffrement PGP Natif** : Les données identifiantes (PII) comme l'email, le nom d'affichage et le **secret 2FA** sont chiffrées dans PostgreSQL via `pgcrypto` (`pgp_sym_encrypt`). Les clés de déchiffrement ne sont jamais stockées en base de données.
- **Hachage aveugle (Blind Indexing)** : Pour permettre la recherche et l'unicité sur les données chiffrées (comme l'email), un hash non réversible (SHA-256) est stocké séparément (`email_hash`).
- **Hachage des mots de passe** : Utilisation d'**Argon2id** (via `@node-rs/argon2`), configuré selon les recommandations de l'ANSSI pour résister aux attaques par force brute et par table de correspondance.
### Sécurité des Communications (In Transit)
- **TLS 1.3** : Assuré par le reverse proxy **Caddy** avec renouvellement automatique des certificats Let's Encrypt.
- **Protocoles d'Authentification** :
- **Sessions (JWT)** : Les jetons de rafraîchissement (`refresh_token`) sont stockés de manière sécurisée en base de données. L'IP de l'utilisateur est hachée (`ip_hash`) pour concilier sécurité et respect de la vie privée.
- **API Keys** : Les clés API sont hachées en base de données (**SHA-256**) via la colonne `key_hash`. Seul un préfixe est conservé en clair pour l'identification.
### Infrastructure & Défense
- **Rate Limiting** : Protection contre le brute-force et le déni de service (DoS).
- **CORS Policy** : Restriction stricte des origines autorisées.
- **RBAC (Role Based Access Control)** : Gestion granulaire des permissions avec une structure complète de rôles et de permissions liées (`roles`, `permissions`, `roles_to_permissions`).
- **Audit Logs** : Traçabilité complète des actions sensibles via la table `audit_logs`. Elle enregistre l'action, l'entité concernée, les détails au format JSONB, ainsi que l'IP hachée et le User-Agent pour l'imputabilité.

View File

@@ -0,0 +1,27 @@
---
title: Stack Technologique
description: Technologies utilisées dans le projet Memegoat
---
## 🛠️ Stack Technologique
### Frontend
- **Framework** : NextJS
- **Gestion d'état** : Zustand
- **Style** : Tailwind CSS
- **Composants UI** : Shadcn/ui
### Backend
- **Framework** : NestJS
- **Langage** : TypeScript
- **Base de données** : PostgresQL
- **ORM** : DrizzleORM
### Infrastructure & DevOps
- **Conteneurisation** : Docker / Docker Compose
- **Reverse Proxy & TLS** : Caddy
- **CI/CD** : Gitea Actions
- **Hébergement** : Hetzner Dedicated Server

View File

@@ -1,17 +0,0 @@
---
title: Components
description: Components
---
## Code Block
```js
console.log('Hello World');
```
## Cards
<Cards>
<Card title="Learn more about Next.js" href="https://nextjs.org/docs" />
<Card title="Learn more about Fumadocs" href="https://fumadocs.dev" />
</Cards>

920
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff