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} -
+ + + + +
+
+ +
+ MemeGoat +
+ +
+
+ {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