chore: reformat database schema files for readability

Applied consistent formatting and indentation across all schema files using Drizzle ORM to enhance code clarity and maintainability.
This commit is contained in:
Mathis HERRIOT
2026-01-05 15:59:15 +01:00
parent cadc497dec
commit b22129c4dd
10 changed files with 388 additions and 207 deletions

View File

@@ -6,28 +6,30 @@
* is strictly prohibited. * is strictly prohibited.
*/ */
import {Injectable, Logger, OnModuleDestroy, OnModuleInit} from "@nestjs/common"; import {
Injectable,
Logger,
OnModuleDestroy,
OnModuleInit,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config"; import { ConfigService } from "@nestjs/config";
import { drizzle } from "drizzle-orm/node-postgres"; import { drizzle } from "drizzle-orm/node-postgres";
import { migrate } from "drizzle-orm/node-postgres/migrator"; import { migrate } from "drizzle-orm/node-postgres/migrator";
import * as schema from "./schemas"; import * as schema from "./schemas";
import {Pool} from "pg"; import { Pool } from "pg";
@Injectable() @Injectable()
export class DatabaseService implements OnModuleInit, OnModuleDestroy { export class DatabaseService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(DatabaseService.name); private readonly logger = new Logger(DatabaseService.name);
private readonly pool!: Pool private readonly pool!: Pool;
public readonly db: ReturnType<typeof drizzle>; public readonly db: ReturnType<typeof drizzle>;
constructor( constructor(private configService: ConfigService) {
private configService: ConfigService
) {
// Create the PostgreSQL client // Create the PostgreSQL client
const connectionString = this.getDatabaseConnectionString(); const connectionString = this.getDatabaseConnectionString();
this.pool = new Pool({ connectionString }); this.pool = new Pool({ connectionString });
// Recreate drizzle with initialized pool // Recreate drizzle with initialized pool
this.db = drizzle(this.pool, { schema }); this.db = drizzle(this.pool, { schema });
} }
async onModuleInit() { async onModuleInit() {
@@ -59,7 +61,9 @@ export class DatabaseService implements OnModuleInit, OnModuleDestroy {
// Get the database connection string from environment variables // Get the database connection string from environment variables
private getDatabaseConnectionString(): string { private getDatabaseConnectionString(): string {
this.logger.debug('Getting database connection string from environment variables'); this.logger.debug(
"Getting database connection string from environment variables",
);
const password = this.configService.get<string>("POSTGRES_PASSWORD"); const password = this.configService.get<string>("POSTGRES_PASSWORD");
const username = this.configService.get<string>("POSTGRES_USER"); const username = this.configService.get<string>("POSTGRES_USER");
@@ -80,7 +84,9 @@ export class DatabaseService implements OnModuleInit, OnModuleDestroy {
throw new Error(errorMessage); throw new Error(errorMessage);
} }
this.logger.debug(`Database connection configured for ${username}@${host}:${port}/${database}`); this.logger.debug(
`Database connection configured for ${username}@${host}:${port}/${database}`,
);
return `postgres://${username}:${password}@${host}:${port}/${database}`; return `postgres://${username}:${password}@${host}:${port}/${database}`;
} }
} }

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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