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/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..aeacda9
--- /dev/null
+++ b/backend/src/api-keys/dto/create-api-key.dto.ts
@@ -0,0 +1,18 @@
+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/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/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/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();
+ }
+}
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..5cc6902 100644
--- a/backend/src/contents/dto/upload-content.dto.ts
+++ b/backend/src/contents/dto/upload-content.dto.ts
@@ -1,9 +1,11 @@
import {
+ IsArray,
IsEnum,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
+ MaxLength,
} from "class-validator";
import { ContentType } from "./create-content.dto";
@@ -13,6 +15,7 @@ export class UploadContentDto {
@IsString()
@IsNotEmpty()
+ @MaxLength(255)
title!: string;
@IsOptional()
@@ -20,6 +23,8 @@ export class UploadContentDto {
categoryId?: string;
@IsOptional()
+ @IsArray()
@IsString({ each: true })
+ @MaxLength(64, { each: true })
tags?: string[];
}
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"),
diff --git a/backend/src/reports/dto/create-report.dto.ts b/backend/src/reports/dto/create-report.dto.ts
index 4a5284d..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 } from "class-validator";
+import {
+ IsEnum,
+ IsOptional,
+ IsString,
+ IsUUID,
+ MaxLength,
+} from "class-validator";
export enum ReportReason {
INAPPROPRIATE = "inappropriate",
@@ -21,5 +27,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;
}
diff --git a/frontend/src/app/(dashboard)/layout.tsx b/frontend/src/app/(dashboard)/layout.tsx
index 181c20b..fe976cb 100644
--- a/frontend/src/app/(dashboard)/layout.tsx
+++ b/frontend/src/app/(dashboard)/layout.tsx
@@ -7,6 +7,7 @@ import {
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar";
+import { UserNavMobile } from "@/components/user-nav-mobile";
export default function DashboardLayout({
children,
@@ -16,26 +17,31 @@ export default function DashboardLayout({
modal: React.ReactNode;
}) {
return (
-