From 5f2672021e49482799876f944e16b152922823a4 Mon Sep 17 00:00:00 2001
From: Mathis HERRIOT <197931332+0x485254@users.noreply.github.com>
Date: Wed, 14 Jan 2026 20:41:25 +0100
Subject: [PATCH 1/5] feat(middleware): add crawler detection middleware for
suspicious requests
Introduce `CrawlerDetectionMiddleware` to identify and log potential crawlers or bots accessing suspicious paths or using bot-like user agents. Middleware applied globally to all routes in `AppModule`.
---
backend/src/app.module.ts | 9 ++-
.../crawler-detection.middleware.ts | 67 +++++++++++++++++++
2 files changed, 74 insertions(+), 2 deletions(-)
create mode 100644 backend/src/common/middlewares/crawler-detection.middleware.ts
diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts
index e5085d4..41608f7 100644
--- a/backend/src/app.module.ts
+++ b/backend/src/app.module.ts
@@ -1,5 +1,5 @@
import { CacheModule } from "@nestjs/cache-manager";
-import { Module } from "@nestjs/common";
+import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { ScheduleModule } from "@nestjs/schedule";
import { ThrottlerModule } from "@nestjs/throttler";
@@ -10,6 +10,7 @@ import { AppService } from "./app.service";
import { AuthModule } from "./auth/auth.module";
import { CategoriesModule } from "./categories/categories.module";
import { CommonModule } from "./common/common.module";
+import { CrawlerDetectionMiddleware } from "./common/middlewares/crawler-detection.middleware";
import { validateEnv } from "./config/env.schema";
import { ContentsModule } from "./contents/contents.module";
import { CryptoModule } from "./crypto/crypto.module";
@@ -71,4 +72,8 @@ import { UsersModule } from "./users/users.module";
controllers: [AppController, HealthController],
providers: [AppService],
})
-export class AppModule {}
+export class AppModule implements NestModule {
+ configure(consumer: MiddlewareConsumer) {
+ consumer.apply(CrawlerDetectionMiddleware).forRoutes("*");
+ }
+}
diff --git a/backend/src/common/middlewares/crawler-detection.middleware.ts b/backend/src/common/middlewares/crawler-detection.middleware.ts
new file mode 100644
index 0000000..01149d1
--- /dev/null
+++ b/backend/src/common/middlewares/crawler-detection.middleware.ts
@@ -0,0 +1,67 @@
+import { Injectable, Logger, NestMiddleware } from "@nestjs/common";
+import type { NextFunction, Request, Response } from "express";
+
+@Injectable()
+export class CrawlerDetectionMiddleware implements NestMiddleware {
+ private readonly logger = new Logger("CrawlerDetection");
+
+ private readonly SUSPICIOUS_PATTERNS = [
+ /\.env/,
+ /wp-admin/,
+ /wp-login/,
+ /\.git/,
+ /\.php$/,
+ /xmlrpc/,
+ /config/,
+ /setup/,
+ /wp-config/,
+ /_next/,
+ /install/,
+ /admin/,
+ /phpmyadmin/,
+ /sql/,
+ /backup/,
+ /db\./,
+ /backup\./,
+ /cgi-bin/,
+ /\.well-known\/security\.txt/, // Bien que légitime, souvent scanné
+ ];
+
+ private readonly BOT_USER_AGENTS = [
+ /bot/i,
+ /crawler/i,
+ /spider/i,
+ /python/i,
+ /curl/i,
+ /wget/i,
+ /nmap/i,
+ /nikto/i,
+ /zgrab/i,
+ /masscan/i,
+ ];
+
+ use(req: Request, res: Response, next: NextFunction) {
+ const { method, url, ip } = req;
+ const userAgent = req.get("user-agent") || "unknown";
+
+ res.on("finish", () => {
+ if (res.statusCode === 404) {
+ const isSuspiciousPath = this.SUSPICIOUS_PATTERNS.some((pattern) =>
+ pattern.test(url),
+ );
+ const isBotUserAgent = this.BOT_USER_AGENTS.some((pattern) =>
+ pattern.test(userAgent),
+ );
+
+ if (isSuspiciousPath || isBotUserAgent) {
+ this.logger.warn(
+ `Potential crawler detected: [${ip}] ${method} ${url} - User-Agent: ${userAgent}`,
+ );
+ // Ici, on pourrait ajouter une logique pour bannir l'IP temporairement via Redis
+ }
+ }
+ });
+
+ next();
+ }
+}
--
2.49.1
From 5671ba60a61399239ad125852f38d3b70d873875 Mon Sep 17 00:00:00 2001
From: Mathis HERRIOT <197931332+0x485254@users.noreply.github.com>
Date: Wed, 14 Jan 2026 20:41:45 +0100
Subject: [PATCH 2/5] feat(dto): enforce field length constraints across DTOs
Add `@MaxLength` validations to limit string field lengths in multiple DTOs, ensuring consistent data validation and integrity. Integrate `CreateApiKeyDto` in the API keys controller for improved type safety.
---
backend/src/api-keys/api-keys.controller.ts | 8 ++++----
backend/src/api-keys/dto/create-api-key.dto.ts | 12 ++++++++++++
backend/src/categories/dto/create-category.dto.ts | 5 ++++-
backend/src/contents/dto/create-content.dto.ts | 5 +++++
backend/src/contents/dto/upload-content.dto.ts | 4 ++++
backend/src/reports/dto/create-report.dto.ts | 3 ++-
backend/src/users/dto/update-consent.dto.ts | 4 +++-
7 files changed, 34 insertions(+), 7 deletions(-)
create mode 100644 backend/src/api-keys/dto/create-api-key.dto.ts
diff --git a/backend/src/api-keys/api-keys.controller.ts b/backend/src/api-keys/api-keys.controller.ts
index 2d65beb..0c75958 100644
--- a/backend/src/api-keys/api-keys.controller.ts
+++ b/backend/src/api-keys/api-keys.controller.ts
@@ -11,6 +11,7 @@ import {
import { AuthGuard } from "../auth/guards/auth.guard";
import type { AuthenticatedRequest } from "../common/interfaces/request.interface";
import { ApiKeysService } from "./api-keys.service";
+import { CreateApiKeyDto } from "./dto/create-api-key.dto";
@Controller("api-keys")
@UseGuards(AuthGuard)
@@ -20,13 +21,12 @@ export class ApiKeysController {
@Post()
create(
@Req() req: AuthenticatedRequest,
- @Body("name") name: string,
- @Body("expiresAt") expiresAt?: string,
+ @Body() createApiKeyDto: CreateApiKeyDto,
) {
return this.apiKeysService.create(
req.user.sub,
- name,
- expiresAt ? new Date(expiresAt) : undefined,
+ createApiKeyDto.name,
+ createApiKeyDto.expiresAt ? new Date(createApiKeyDto.expiresAt) : undefined,
);
}
diff --git a/backend/src/api-keys/dto/create-api-key.dto.ts b/backend/src/api-keys/dto/create-api-key.dto.ts
new file mode 100644
index 0000000..91601a6
--- /dev/null
+++ b/backend/src/api-keys/dto/create-api-key.dto.ts
@@ -0,0 +1,12 @@
+import { IsDateString, IsNotEmpty, IsOptional, IsString, MaxLength } from "class-validator";
+
+export class CreateApiKeyDto {
+ @IsString()
+ @IsNotEmpty()
+ @MaxLength(128)
+ name!: string;
+
+ @IsOptional()
+ @IsDateString()
+ expiresAt?: string;
+}
diff --git a/backend/src/categories/dto/create-category.dto.ts b/backend/src/categories/dto/create-category.dto.ts
index 52e0884..1459f00 100644
--- a/backend/src/categories/dto/create-category.dto.ts
+++ b/backend/src/categories/dto/create-category.dto.ts
@@ -1,15 +1,18 @@
-import { IsNotEmpty, IsOptional, IsString } from "class-validator";
+import { IsNotEmpty, IsOptional, IsString, MaxLength } from "class-validator";
export class CreateCategoryDto {
@IsString()
@IsNotEmpty()
+ @MaxLength(64)
name!: string;
@IsOptional()
@IsString()
+ @MaxLength(255)
description?: string;
@IsOptional()
@IsString()
+ @MaxLength(512)
iconUrl?: string;
}
diff --git a/backend/src/contents/dto/create-content.dto.ts b/backend/src/contents/dto/create-content.dto.ts
index 096601e..7c0aa92 100644
--- a/backend/src/contents/dto/create-content.dto.ts
+++ b/backend/src/contents/dto/create-content.dto.ts
@@ -6,6 +6,7 @@ import {
IsOptional,
IsString,
IsUUID,
+ MaxLength,
} from "class-validator";
export enum ContentType {
@@ -19,14 +20,17 @@ export class CreateContentDto {
@IsString()
@IsNotEmpty()
+ @MaxLength(255)
title!: string;
@IsString()
@IsNotEmpty()
+ @MaxLength(512)
storageKey!: string;
@IsString()
@IsNotEmpty()
+ @MaxLength(128)
mimeType!: string;
@IsInt()
@@ -39,5 +43,6 @@ export class CreateContentDto {
@IsOptional()
@IsArray()
@IsString({ each: true })
+ @MaxLength(64, { each: true })
tags?: string[];
}
diff --git a/backend/src/contents/dto/upload-content.dto.ts b/backend/src/contents/dto/upload-content.dto.ts
index ca4b284..a67c3d7 100644
--- a/backend/src/contents/dto/upload-content.dto.ts
+++ b/backend/src/contents/dto/upload-content.dto.ts
@@ -4,6 +4,7 @@ import {
IsOptional,
IsString,
IsUUID,
+ MaxLength,
} from "class-validator";
import { ContentType } from "./create-content.dto";
@@ -13,6 +14,7 @@ export class UploadContentDto {
@IsString()
@IsNotEmpty()
+ @MaxLength(255)
title!: string;
@IsOptional()
@@ -20,6 +22,8 @@ export class UploadContentDto {
categoryId?: string;
@IsOptional()
+ @IsArray()
@IsString({ each: true })
+ @MaxLength(64, { each: true })
tags?: string[];
}
diff --git a/backend/src/reports/dto/create-report.dto.ts b/backend/src/reports/dto/create-report.dto.ts
index 4a5284d..53a9bae 100644
--- a/backend/src/reports/dto/create-report.dto.ts
+++ b/backend/src/reports/dto/create-report.dto.ts
@@ -1,4 +1,4 @@
-import { IsEnum, IsOptional, IsString, IsUUID } from "class-validator";
+import { IsEnum, IsOptional, IsString, IsUUID, MaxLength } from "class-validator";
export enum ReportReason {
INAPPROPRIATE = "inappropriate",
@@ -21,5 +21,6 @@ export class CreateReportDto {
@IsOptional()
@IsString()
+ @MaxLength(1000)
description?: string;
}
diff --git a/backend/src/users/dto/update-consent.dto.ts b/backend/src/users/dto/update-consent.dto.ts
index 3456a81..aa97e7e 100644
--- a/backend/src/users/dto/update-consent.dto.ts
+++ b/backend/src/users/dto/update-consent.dto.ts
@@ -1,11 +1,13 @@
-import { IsNotEmpty, IsString } from "class-validator";
+import { IsNotEmpty, IsString, MaxLength } from "class-validator";
export class UpdateConsentDto {
@IsString()
@IsNotEmpty()
+ @MaxLength(16)
termsVersion!: string;
@IsString()
@IsNotEmpty()
+ @MaxLength(16)
privacyVersion!: string;
}
--
2.49.1
From ff6fc1c6b3b36cc6dd965add0213154a1408e7ae Mon Sep 17 00:00:00 2001
From: Mathis HERRIOT <197931332+0x485254@users.noreply.github.com>
Date: Wed, 14 Jan 2026 20:50:38 +0100
Subject: [PATCH 3/5] feat(ui): enhance mobile user experience and
authentication handling
Add `UserNavMobile` component for improved mobile navigation. Update dashboard and profile pages to include authentication checks with loading states and login prompts. Introduce category-specific content tabs on the profile page. Apply sidebar enhancements, including new sections for user favorites and memes.
---
frontend/src/app/(dashboard)/layout.tsx | 44 ++++++++++-------
frontend/src/app/(dashboard)/profile/page.tsx | 49 ++++++++++++++++---
frontend/src/app/(dashboard)/upload/page.tsx | 46 +++++++++++++++--
frontend/src/components/app-sidebar.tsx | 35 ++++++++++++-
frontend/src/components/user-nav-mobile.tsx | 39 +++++++++++++++
5 files changed, 182 insertions(+), 31 deletions(-)
create mode 100644 frontend/src/components/user-nav-mobile.tsx
diff --git a/frontend/src/app/(dashboard)/layout.tsx b/frontend/src/app/(dashboard)/layout.tsx
index 181c20b..89e2f02 100644
--- a/frontend/src/app/(dashboard)/layout.tsx
+++ b/frontend/src/app/(dashboard)/layout.tsx
@@ -2,6 +2,7 @@ import * as React from "react";
import { AppSidebar } from "@/components/app-sidebar";
import { MobileFilters } from "@/components/mobile-filters";
import { SearchSidebar } from "@/components/search-sidebar";
+import { UserNavMobile } from "@/components/user-nav-mobile";
import {
SidebarInset,
SidebarProvider,
@@ -16,26 +17,31 @@ export default function DashboardLayout({
modal: React.ReactNode;
}) {
return (
-
-
-
-
-
-
- {children}
- {modal}
-
+
+
+
+
+
+
+
+ {children}
+ {modal}
+
+
+
+
+
-
+
-
-
-
-
-
-
+
+
+
);
}
diff --git a/frontend/src/app/(dashboard)/profile/page.tsx b/frontend/src/app/(dashboard)/profile/page.tsx
index cc02784..d1c1746 100644
--- a/frontend/src/app/(dashboard)/profile/page.tsx
+++ b/frontend/src/app/(dashboard)/profile/page.tsx
@@ -1,19 +1,23 @@
"use client";
-import { Calendar, LogOut, Settings } from "lucide-react";
+import { Calendar, LogIn, LogOut, Settings } from "lucide-react";
import Link from "next/link";
-import { redirect } from "next/navigation";
+import { useSearchParams } from "next/navigation";
import * as React from "react";
import { ContentList } from "@/components/content-list";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useAuth } from "@/providers/auth-provider";
import { ContentService } from "@/services/content.service";
import { FavoriteService } from "@/services/favorite.service";
+import { Spinner } from "@/components/ui/spinner";
export default function ProfilePage() {
const { user, isAuthenticated, isLoading, logout } = useAuth();
+ const searchParams = useSearchParams();
+ const tab = searchParams.get("tab") || "memes";
const fetchMyMemes = React.useCallback(
(params: { limit: number; offset: number }) =>
@@ -26,9 +30,36 @@ export default function ProfilePage() {
[],
);
- if (isLoading) return null;
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
if (!isAuthenticated || !user) {
- redirect("/login");
+ return (
+
+
+
+
+
+
+ Profil inaccessible
+
+ Vous devez être connecté pour voir votre profil, vos mèmes et vos
+ favoris.
+
+
+
+
+
+
+
+ );
}
return (
@@ -79,10 +110,14 @@ export default function ProfilePage() {
-
+
- Mes Mèmes
- Mes Favoris
+
+ Mes Mèmes
+
+
+ Mes Favoris
+
diff --git a/frontend/src/app/(dashboard)/upload/page.tsx b/frontend/src/app/(dashboard)/upload/page.tsx
index 2567fbf..2049f8f 100644
--- a/frontend/src/app/(dashboard)/upload/page.tsx
+++ b/frontend/src/app/(dashboard)/upload/page.tsx
@@ -1,15 +1,16 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
-import { Image as ImageIcon, Loader2, Upload, X } from "lucide-react";
+import { Image as ImageIcon, Loader2, LogIn, Upload, X } from "lucide-react";
import NextImage from "next/image";
+import Link from "next/link";
import { useRouter } from "next/navigation";
import * as React from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import * as z from "zod";
import { Button } from "@/components/ui/button";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
Form,
FormControl,
@@ -27,8 +28,10 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
+import { useAuth } from "@/providers/auth-provider";
import { CategoryService } from "@/services/category.service";
import { ContentService } from "@/services/content.service";
+import { Spinner } from "@/components/ui/spinner";
import type { Category } from "@/types/content";
const uploadSchema = z.object({
@@ -42,6 +45,7 @@ type UploadFormValues = z.infer;
export default function UploadPage() {
const router = useRouter();
+ const { isAuthenticated, isLoading } = useAuth();
const [categories, setCategories] = React.useState([]);
const [file, setFile] = React.useState(null);
const [preview, setPreview] = React.useState(null);
@@ -57,8 +61,42 @@ export default function UploadPage() {
});
React.useEffect(() => {
- CategoryService.getAll().then(setCategories).catch(console.error);
- }, []);
+ if (isAuthenticated) {
+ CategoryService.getAll().then(setCategories).catch(console.error);
+ }
+ }, [isAuthenticated]);
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (!isAuthenticated) {
+ return (
+
+
+
+
+
+
+ Connexion requise
+
+ Vous devez être connecté pour partager vos meilleurs mèmes avec la
+ communauté.
+
+
+
+
+
+
+
+ );
+ }
const handleFileChange = (e: React.ChangeEvent) => {
const selectedFile = e.target.files?.[0];
diff --git a/frontend/src/components/app-sidebar.tsx b/frontend/src/components/app-sidebar.tsx
index 13e7536..49905bf 100644
--- a/frontend/src/components/app-sidebar.tsx
+++ b/frontend/src/components/app-sidebar.tsx
@@ -3,7 +3,9 @@
import {
ChevronRight,
Clock,
+ Heart,
HelpCircle,
+ History,
Home,
LayoutGrid,
LogIn,
@@ -14,7 +16,7 @@ import {
User as UserIcon,
} from "lucide-react";
import Link from "next/link";
-import { usePathname } from "next/navigation";
+import { usePathname, useSearchParams } from "next/navigation";
import * as React from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
@@ -68,6 +70,7 @@ const mainNav = [
export function AppSidebar() {
const pathname = usePathname();
+ const searchParams = useSearchParams();
const { user, logout, isAuthenticated } = useAuth();
const [categories, setCategories] = React.useState([]);
@@ -149,6 +152,36 @@ export function AppSidebar() {
+
+
+ Ma Bibliothèque
+
+
+
+
+
+ Mes Favoris
+
+
+
+
+
+
+
+ Mes Mèmes
+
+
+
+
+
diff --git a/frontend/src/components/user-nav-mobile.tsx b/frontend/src/components/user-nav-mobile.tsx
new file mode 100644
index 0000000..c70387f
--- /dev/null
+++ b/frontend/src/components/user-nav-mobile.tsx
@@ -0,0 +1,39 @@
+"use client";
+
+import { LogIn, User as UserIcon } from "lucide-react";
+import Link from "next/link";
+import * as React from "react";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { Button } from "@/components/ui/button";
+import { useAuth } from "@/providers/auth-provider";
+
+export function UserNavMobile() {
+ const { user, isAuthenticated, isLoading } = useAuth();
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (!isAuthenticated || !user) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}
--
2.49.1
From a4ce48a91c9217154e22dacab55ea898c01c5200 Mon Sep 17 00:00:00 2001
From: Mathis HERRIOT <197931332+0x485254@users.noreply.github.com>
Date: Wed, 14 Jan 2026 20:51:24 +0100
Subject: [PATCH 4/5] feat(database): update `passwordHash` length and add
migration snapshot
Increase `passwordHash` field length to 100 in the `users` schema to accommodate larger hashes. Add migration snapshot `0005_snapshot.json` to capture database state changes.
---
.../.migrations/0005_perpetual_silverclaw.sql | 1 +
backend/.migrations/meta/0005_snapshot.json | 1640 +++++++++++++++++
backend/.migrations/meta/_journal.json | 7 +
backend/src/database/schemas/users.ts | 2 +-
4 files changed, 1649 insertions(+), 1 deletion(-)
create mode 100644 backend/.migrations/0005_perpetual_silverclaw.sql
create mode 100644 backend/.migrations/meta/0005_snapshot.json
diff --git a/backend/.migrations/0005_perpetual_silverclaw.sql b/backend/.migrations/0005_perpetual_silverclaw.sql
new file mode 100644
index 0000000..4bcd686
--- /dev/null
+++ b/backend/.migrations/0005_perpetual_silverclaw.sql
@@ -0,0 +1 @@
+ALTER TABLE "users" ALTER COLUMN "password_hash" SET DATA TYPE varchar(100);
\ No newline at end of file
diff --git a/backend/.migrations/meta/0005_snapshot.json b/backend/.migrations/meta/0005_snapshot.json
new file mode 100644
index 0000000..35387b6
--- /dev/null
+++ b/backend/.migrations/meta/0005_snapshot.json
@@ -0,0 +1,1640 @@
+{
+ "id": "6dbf8343-dced-4b01-a3fd-8c246b0b88e8",
+ "prevId": "a61c1c19-d082-4e21-b7e9-4e4d9e1feb04",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.api_keys": {
+ "name": "api_keys",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "key_hash": {
+ "name": "key_hash",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "prefix": {
+ "name": "prefix",
+ "type": "varchar(8)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "last_used_at": {
+ "name": "last_used_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "api_keys_user_id_idx": {
+ "name": "api_keys_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "api_keys_key_hash_idx": {
+ "name": "api_keys_key_hash_idx",
+ "columns": [
+ {
+ "expression": "key_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "api_keys_user_id_users_uuid_fk": {
+ "name": "api_keys_user_id_users_uuid_fk",
+ "tableFrom": "api_keys",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "uuid"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "api_keys_key_hash_unique": {
+ "name": "api_keys_key_hash_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "key_hash"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.audit_logs": {
+ "name": "audit_logs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "action": {
+ "name": "action",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_type": {
+ "name": "entity_type",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_id": {
+ "name": "entity_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "details": {
+ "name": "details",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ip_hash": {
+ "name": "ip_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "audit_logs_user_id_idx": {
+ "name": "audit_logs_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_logs_action_idx": {
+ "name": "audit_logs_action_idx",
+ "columns": [
+ {
+ "expression": "action",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_logs_entity_idx": {
+ "name": "audit_logs_entity_idx",
+ "columns": [
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_logs_created_at_idx": {
+ "name": "audit_logs_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "audit_logs_user_id_users_uuid_fk": {
+ "name": "audit_logs_user_id_users_uuid_fk",
+ "tableFrom": "audit_logs",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "uuid"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.categories": {
+ "name": "categories",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "icon_url": {
+ "name": "icon_url",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "categories_slug_idx": {
+ "name": "categories_slug_idx",
+ "columns": [
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "categories_name_unique": {
+ "name": "categories_name_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "name"
+ ]
+ },
+ "categories_slug_unique": {
+ "name": "categories_slug_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "slug"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.contents": {
+ "name": "contents",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "content_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "category_id": {
+ "name": "category_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "storage_key": {
+ "name": "storage_key",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "mime_type": {
+ "name": "mime_type",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "views": {
+ "name": "views",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "usage_count": {
+ "name": "usage_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "contents_user_id_idx": {
+ "name": "contents_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "contents_storage_key_idx": {
+ "name": "contents_storage_key_idx",
+ "columns": [
+ {
+ "expression": "storage_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "contents_deleted_at_idx": {
+ "name": "contents_deleted_at_idx",
+ "columns": [
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "contents_user_id_users_uuid_fk": {
+ "name": "contents_user_id_users_uuid_fk",
+ "tableFrom": "contents",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "uuid"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "contents_category_id_categories_id_fk": {
+ "name": "contents_category_id_categories_id_fk",
+ "tableFrom": "contents",
+ "tableTo": "categories",
+ "columnsFrom": [
+ "category_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "contents_slug_unique": {
+ "name": "contents_slug_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "slug"
+ ]
+ },
+ "contents_storage_key_unique": {
+ "name": "contents_storage_key_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "storage_key"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.contents_to_tags": {
+ "name": "contents_to_tags",
+ "schema": "",
+ "columns": {
+ "content_id": {
+ "name": "content_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tag_id": {
+ "name": "tag_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "contents_to_tags_content_id_contents_id_fk": {
+ "name": "contents_to_tags_content_id_contents_id_fk",
+ "tableFrom": "contents_to_tags",
+ "tableTo": "contents",
+ "columnsFrom": [
+ "content_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "contents_to_tags_tag_id_tags_id_fk": {
+ "name": "contents_to_tags_tag_id_tags_id_fk",
+ "tableFrom": "contents_to_tags",
+ "tableTo": "tags",
+ "columnsFrom": [
+ "tag_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "contents_to_tags_content_id_tag_id_pk": {
+ "name": "contents_to_tags_content_id_tag_id_pk",
+ "columns": [
+ "content_id",
+ "tag_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.favorites": {
+ "name": "favorites",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content_id": {
+ "name": "content_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "favorites_user_id_users_uuid_fk": {
+ "name": "favorites_user_id_users_uuid_fk",
+ "tableFrom": "favorites",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "uuid"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "favorites_content_id_contents_id_fk": {
+ "name": "favorites_content_id_contents_id_fk",
+ "tableFrom": "favorites",
+ "tableTo": "contents",
+ "columnsFrom": [
+ "content_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "favorites_user_id_content_id_pk": {
+ "name": "favorites_user_id_content_id_pk",
+ "columns": [
+ "user_id",
+ "content_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.permissions": {
+ "name": "permissions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "permissions_slug_idx": {
+ "name": "permissions_slug_idx",
+ "columns": [
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "permissions_name_unique": {
+ "name": "permissions_name_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "name"
+ ]
+ },
+ "permissions_slug_unique": {
+ "name": "permissions_slug_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "slug"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.roles": {
+ "name": "roles",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "roles_slug_idx": {
+ "name": "roles_slug_idx",
+ "columns": [
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "roles_name_unique": {
+ "name": "roles_name_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "name"
+ ]
+ },
+ "roles_slug_unique": {
+ "name": "roles_slug_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "slug"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.roles_to_permissions": {
+ "name": "roles_to_permissions",
+ "schema": "",
+ "columns": {
+ "role_id": {
+ "name": "role_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission_id": {
+ "name": "permission_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "roles_to_permissions_role_id_roles_id_fk": {
+ "name": "roles_to_permissions_role_id_roles_id_fk",
+ "tableFrom": "roles_to_permissions",
+ "tableTo": "roles",
+ "columnsFrom": [
+ "role_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "roles_to_permissions_permission_id_permissions_id_fk": {
+ "name": "roles_to_permissions_permission_id_permissions_id_fk",
+ "tableFrom": "roles_to_permissions",
+ "tableTo": "permissions",
+ "columnsFrom": [
+ "permission_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "roles_to_permissions_role_id_permission_id_pk": {
+ "name": "roles_to_permissions_role_id_permission_id_pk",
+ "columns": [
+ "role_id",
+ "permission_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.users_to_roles": {
+ "name": "users_to_roles",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role_id": {
+ "name": "role_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "users_to_roles_user_id_users_uuid_fk": {
+ "name": "users_to_roles_user_id_users_uuid_fk",
+ "tableFrom": "users_to_roles",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "uuid"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "users_to_roles_role_id_roles_id_fk": {
+ "name": "users_to_roles_role_id_roles_id_fk",
+ "tableFrom": "users_to_roles",
+ "tableTo": "roles",
+ "columnsFrom": [
+ "role_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "users_to_roles_user_id_role_id_pk": {
+ "name": "users_to_roles_user_id_role_id_pk",
+ "columns": [
+ "user_id",
+ "role_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.reports": {
+ "name": "reports",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "reporter_id": {
+ "name": "reporter_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content_id": {
+ "name": "content_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag_id": {
+ "name": "tag_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reason": {
+ "name": "reason",
+ "type": "report_reason",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "report_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "reports_reporter_id_idx": {
+ "name": "reports_reporter_id_idx",
+ "columns": [
+ {
+ "expression": "reporter_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "reports_content_id_idx": {
+ "name": "reports_content_id_idx",
+ "columns": [
+ {
+ "expression": "content_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "reports_tag_id_idx": {
+ "name": "reports_tag_id_idx",
+ "columns": [
+ {
+ "expression": "tag_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "reports_status_idx": {
+ "name": "reports_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "reports_expires_at_idx": {
+ "name": "reports_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "reports_reporter_id_users_uuid_fk": {
+ "name": "reports_reporter_id_users_uuid_fk",
+ "tableFrom": "reports",
+ "tableTo": "users",
+ "columnsFrom": [
+ "reporter_id"
+ ],
+ "columnsTo": [
+ "uuid"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "reports_content_id_contents_id_fk": {
+ "name": "reports_content_id_contents_id_fk",
+ "tableFrom": "reports",
+ "tableTo": "contents",
+ "columnsFrom": [
+ "content_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "reports_tag_id_tags_id_fk": {
+ "name": "reports_tag_id_tags_id_fk",
+ "tableFrom": "reports",
+ "tableTo": "tags",
+ "columnsFrom": [
+ "tag_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.sessions": {
+ "name": "sessions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ip_hash": {
+ "name": "ip_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_valid": {
+ "name": "is_valid",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "sessions_user_id_idx": {
+ "name": "sessions_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "sessions_refresh_token_idx": {
+ "name": "sessions_refresh_token_idx",
+ "columns": [
+ {
+ "expression": "refresh_token",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "sessions_expires_at_idx": {
+ "name": "sessions_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "sessions_user_id_users_uuid_fk": {
+ "name": "sessions_user_id_users_uuid_fk",
+ "tableFrom": "sessions",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "uuid"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "sessions_refresh_token_unique": {
+ "name": "sessions_refresh_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "refresh_token"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tags": {
+ "name": "tags",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "tags_slug_idx": {
+ "name": "tags_slug_idx",
+ "columns": [
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "tags_user_id_users_uuid_fk": {
+ "name": "tags_user_id_users_uuid_fk",
+ "tableFrom": "tags",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "uuid"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "tags_name_unique": {
+ "name": "tags_name_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "name"
+ ]
+ },
+ "tags_slug_unique": {
+ "name": "tags_slug_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "slug"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.users": {
+ "name": "users",
+ "schema": "",
+ "columns": {
+ "uuid": {
+ "name": "uuid",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "status": {
+ "name": "status",
+ "type": "user_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "email": {
+ "name": "email",
+ "type": "bytea",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email_hash": {
+ "name": "email_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "username": {
+ "name": "username",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "password_hash": {
+ "name": "password_hash",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "two_factor_secret": {
+ "name": "two_factor_secret",
+ "type": "bytea",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_two_factor_enabled": {
+ "name": "is_two_factor_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "terms_version": {
+ "name": "terms_version",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "privacy_version": {
+ "name": "privacy_version",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gdpr_accepted_at": {
+ "name": "gdpr_accepted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_login_at": {
+ "name": "last_login_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "users_uuid_idx": {
+ "name": "users_uuid_idx",
+ "columns": [
+ {
+ "expression": "uuid",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "users_email_hash_idx": {
+ "name": "users_email_hash_idx",
+ "columns": [
+ {
+ "expression": "email_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "users_username_idx": {
+ "name": "users_username_idx",
+ "columns": [
+ {
+ "expression": "username",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "users_status_idx": {
+ "name": "users_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "users_email_hash_unique": {
+ "name": "users_email_hash_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email_hash"
+ ]
+ },
+ "users_username_unique": {
+ "name": "users_username_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "username"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.content_type": {
+ "name": "content_type",
+ "schema": "public",
+ "values": [
+ "meme",
+ "gif"
+ ]
+ },
+ "public.report_reason": {
+ "name": "report_reason",
+ "schema": "public",
+ "values": [
+ "inappropriate",
+ "spam",
+ "copyright",
+ "other"
+ ]
+ },
+ "public.report_status": {
+ "name": "report_status",
+ "schema": "public",
+ "values": [
+ "pending",
+ "reviewed",
+ "resolved",
+ "dismissed"
+ ]
+ },
+ "public.user_status": {
+ "name": "user_status",
+ "schema": "public",
+ "values": [
+ "active",
+ "verification",
+ "suspended",
+ "pending",
+ "deleted"
+ ]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/backend/.migrations/meta/_journal.json b/backend/.migrations/meta/_journal.json
index 3c9f261..d88fd1c 100644
--- a/backend/.migrations/meta/_journal.json
+++ b/backend/.migrations/meta/_journal.json
@@ -36,6 +36,13 @@
"when": 1768417827439,
"tag": "0004_cheerful_dakota_north",
"breakpoints": true
+ },
+ {
+ "idx": 5,
+ "version": "7",
+ "when": 1768420201679,
+ "tag": "0005_perpetual_silverclaw",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/backend/src/database/schemas/users.ts b/backend/src/database/schemas/users.ts
index 6af70ee..fb9c55f 100644
--- a/backend/src/database/schemas/users.ts
+++ b/backend/src/database/schemas/users.ts
@@ -29,7 +29,7 @@ export const users = pgTable(
displayName: varchar("display_name", { length: 32 }),
username: varchar("username", { length: 32 }).notNull().unique(),
- passwordHash: varchar("password_hash", { length: 95 }).notNull(),
+ passwordHash: varchar("password_hash", { length: 100 }).notNull(),
// Sécurité
twoFactorSecret: pgpEncrypted("two_factor_secret"),
--
2.49.1
From 975e29dea1f90423ae2cbb4c9822f0a673ee8f90 Mon Sep 17 00:00:00 2001
From: Mathis HERRIOT <197931332+0x485254@users.noreply.github.com>
Date: Wed, 14 Jan 2026 21:06:38 +0100
Subject: [PATCH 5/5] refactor: format imports, fix indentation, and improve
readability across files
Apply consistent import ordering and indentation in frontend and backend files. Ensure better maintainability and adherence to code style standards.
---
backend/src/api-keys/dto/create-api-key.dto.ts | 8 +++++++-
backend/src/contents/dto/upload-content.dto.ts | 1 +
backend/src/reports/dto/create-report.dto.ts | 8 +++++++-
frontend/src/app/(dashboard)/layout.tsx | 2 +-
frontend/src/app/(dashboard)/profile/page.tsx | 10 ++++++++--
frontend/src/app/(dashboard)/upload/page.tsx | 10 ++++++++--
frontend/src/components/app-sidebar.tsx | 8 ++++++--
frontend/src/components/user-nav-mobile.tsx | 4 +---
8 files changed, 39 insertions(+), 12 deletions(-)
diff --git a/backend/src/api-keys/dto/create-api-key.dto.ts b/backend/src/api-keys/dto/create-api-key.dto.ts
index 91601a6..aeacda9 100644
--- a/backend/src/api-keys/dto/create-api-key.dto.ts
+++ b/backend/src/api-keys/dto/create-api-key.dto.ts
@@ -1,4 +1,10 @@
-import { IsDateString, IsNotEmpty, IsOptional, IsString, MaxLength } from "class-validator";
+import {
+ IsDateString,
+ IsNotEmpty,
+ IsOptional,
+ IsString,
+ MaxLength,
+} from "class-validator";
export class CreateApiKeyDto {
@IsString()
diff --git a/backend/src/contents/dto/upload-content.dto.ts b/backend/src/contents/dto/upload-content.dto.ts
index a67c3d7..5cc6902 100644
--- a/backend/src/contents/dto/upload-content.dto.ts
+++ b/backend/src/contents/dto/upload-content.dto.ts
@@ -1,4 +1,5 @@
import {
+ IsArray,
IsEnum,
IsNotEmpty,
IsOptional,
diff --git a/backend/src/reports/dto/create-report.dto.ts b/backend/src/reports/dto/create-report.dto.ts
index 53a9bae..a3c86ab 100644
--- a/backend/src/reports/dto/create-report.dto.ts
+++ b/backend/src/reports/dto/create-report.dto.ts
@@ -1,4 +1,10 @@
-import { IsEnum, IsOptional, IsString, IsUUID, MaxLength } from "class-validator";
+import {
+ IsEnum,
+ IsOptional,
+ IsString,
+ IsUUID,
+ MaxLength,
+} from "class-validator";
export enum ReportReason {
INAPPROPRIATE = "inappropriate",
diff --git a/frontend/src/app/(dashboard)/layout.tsx b/frontend/src/app/(dashboard)/layout.tsx
index 89e2f02..fe976cb 100644
--- a/frontend/src/app/(dashboard)/layout.tsx
+++ b/frontend/src/app/(dashboard)/layout.tsx
@@ -2,12 +2,12 @@ import * as React from "react";
import { AppSidebar } from "@/components/app-sidebar";
import { MobileFilters } from "@/components/mobile-filters";
import { SearchSidebar } from "@/components/search-sidebar";
-import { UserNavMobile } from "@/components/user-nav-mobile";
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar";
+import { UserNavMobile } from "@/components/user-nav-mobile";
export default function DashboardLayout({
children,
diff --git a/frontend/src/app/(dashboard)/profile/page.tsx b/frontend/src/app/(dashboard)/profile/page.tsx
index d1c1746..e2f6765 100644
--- a/frontend/src/app/(dashboard)/profile/page.tsx
+++ b/frontend/src/app/(dashboard)/profile/page.tsx
@@ -7,12 +7,18 @@ import * as React from "react";
import { ContentList } from "@/components/content-list";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Spinner } from "@/components/ui/spinner";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useAuth } from "@/providers/auth-provider";
import { ContentService } from "@/services/content.service";
import { FavoriteService } from "@/services/favorite.service";
-import { Spinner } from "@/components/ui/spinner";
export default function ProfilePage() {
const { user, isAuthenticated, isLoading, logout } = useAuth();
diff --git a/frontend/src/app/(dashboard)/upload/page.tsx b/frontend/src/app/(dashboard)/upload/page.tsx
index 2049f8f..c817953 100644
--- a/frontend/src/app/(dashboard)/upload/page.tsx
+++ b/frontend/src/app/(dashboard)/upload/page.tsx
@@ -10,7 +10,13 @@ import { useForm } from "react-hook-form";
import { toast } from "sonner";
import * as z from "zod";
import { Button } from "@/components/ui/button";
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
import {
Form,
FormControl,
@@ -28,10 +34,10 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
+import { Spinner } from "@/components/ui/spinner";
import { useAuth } from "@/providers/auth-provider";
import { CategoryService } from "@/services/category.service";
import { ContentService } from "@/services/content.service";
-import { Spinner } from "@/components/ui/spinner";
import type { Category } from "@/types/content";
const uploadSchema = z.object({
diff --git a/frontend/src/components/app-sidebar.tsx b/frontend/src/components/app-sidebar.tsx
index 49905bf..c89c51b 100644
--- a/frontend/src/components/app-sidebar.tsx
+++ b/frontend/src/components/app-sidebar.tsx
@@ -159,7 +159,9 @@ export function AppSidebar() {
@@ -171,7 +173,9 @@ export function AppSidebar() {
diff --git a/frontend/src/components/user-nav-mobile.tsx b/frontend/src/components/user-nav-mobile.tsx
index c70387f..a87b737 100644
--- a/frontend/src/components/user-nav-mobile.tsx
+++ b/frontend/src/components/user-nav-mobile.tsx
@@ -29,9 +29,7 @@ export function UserNavMobile() {
-
- {user.username.slice(0, 2).toUpperCase()}
-
+ {user.username.slice(0, 2).toUpperCase()}
--
2.49.1