From b45909f86e2e73e6351b0457b6019de114ecc017 Mon Sep 17 00:00:00 2001 From: whidix Date: Sun, 1 Mar 2026 19:52:54 +0100 Subject: [PATCH] feat: add user admin status and profile management - Updated user schema to include isAdmin field. - Enhanced authentication hooks to fetch and set user admin status. - Created ProfileButton component for user profile actions. - Implemented profile and password update functionality. - Added session management for user accounts. - Developed login and signup pages with form handling. - Introduced layout server for user session data. - Updated daily page to reflect character changes. --- drizzle/0002_large_gwen_stacy.sql | 1 + drizzle/meta/0002_snapshot.json | 1084 +++++++++++++++++++++++ drizzle/meta/_journal.json | 7 + src/app.d.ts | 9 +- src/hooks.server.ts | 9 + src/lib/components/ProfileButton.svelte | 111 +++ src/lib/server/db/auth.schema.ts | 1 + src/routes/+layout.server.ts | 8 + src/routes/+layout.svelte | 18 +- src/routes/daily/+page.svelte | 15 +- src/routes/login/+page.server.ts | 68 ++ src/routes/login/+page.svelte | 151 ++++ src/routes/profile/+page.server.ts | 119 +++ src/routes/profile/+page.svelte | 334 +++++++ 14 files changed, 1913 insertions(+), 22 deletions(-) create mode 100644 drizzle/0002_large_gwen_stacy.sql create mode 100644 drizzle/meta/0002_snapshot.json create mode 100644 src/lib/components/ProfileButton.svelte create mode 100644 src/routes/+layout.server.ts create mode 100644 src/routes/login/+page.server.ts create mode 100644 src/routes/login/+page.svelte create mode 100644 src/routes/profile/+page.server.ts create mode 100644 src/routes/profile/+page.svelte diff --git a/drizzle/0002_large_gwen_stacy.sql b/drizzle/0002_large_gwen_stacy.sql new file mode 100644 index 0000000..a5ccfcc --- /dev/null +++ b/drizzle/0002_large_gwen_stacy.sql @@ -0,0 +1 @@ +ALTER TABLE `user` ADD `is_admin` integer DEFAULT false NOT NULL; \ No newline at end of file diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..7cc79cd --- /dev/null +++ b/drizzle/meta/0002_snapshot.json @@ -0,0 +1,1084 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "4fa96ce4-93c4-4d2d-9f9a-5badf47dfb05", + "prevId": "23b693a1-eebd-499e-9755-27a732e1afc1", + "tables": { + "arc": { + "name": "arc", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "startChapter": { + "name": "startChapter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "endChapter": { + "name": "endChapter", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "character": { + "name": "character", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "gender": { + "name": "gender", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "age": { + "name": "age", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "affiliations": { + "name": "affiliations", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "devilFruitId": { + "name": "devilFruitId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hakiObservation": { + "name": "hakiObservation", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "hakiArmament": { + "name": "hakiArmament", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "hakiConqueror": { + "name": "hakiConqueror", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "bounty": { + "name": "bounty", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "height": { + "name": "height", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "firstAppearance": { + "name": "firstAppearance", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pictureUrl": { + "name": "pictureUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "epithets": { + "name": "epithets", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "arcId": { + "name": "arcId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "isInDailyMode": { + "name": "isInDailyMode", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "character_devilFruitId_devilFruit_id_fk": { + "name": "character_devilFruitId_devilFruit_id_fk", + "tableFrom": "character", + "tableTo": "devilFruit", + "columnsFrom": [ + "devilFruitId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "character_arcId_arc_id_fk": { + "name": "character_arcId_arc_id_fk", + "tableFrom": "character", + "tableTo": "arc", + "columnsFrom": [ + "arcId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "characterHistory": { + "name": "characterHistory", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "characterId": { + "name": "characterId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "won": { + "name": "won", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "characterHistory_characterId_character_id_fk": { + "name": "characterHistory_characterId_character_id_fk", + "tableFrom": "characterHistory", + "tableTo": "character", + "columnsFrom": [ + "characterId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "characterOverride": { + "name": "characterOverride", + "columns": { + "characterId": { + "name": "characterId", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "gender": { + "name": "gender", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "age": { + "name": "age", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "affiliations": { + "name": "affiliations", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "devilFruitId": { + "name": "devilFruitId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hakiObservation": { + "name": "hakiObservation", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hakiArmament": { + "name": "hakiArmament", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hakiConqueror": { + "name": "hakiConqueror", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bounty": { + "name": "bounty", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "firstAppearance": { + "name": "firstAppearance", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pictureUrl": { + "name": "pictureUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "epithets": { + "name": "epithets", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "arcId": { + "name": "arcId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "characterOverride_characterId_character_id_fk": { + "name": "characterOverride_characterId_character_id_fk", + "tableFrom": "characterOverride", + "tableTo": "character", + "columnsFrom": [ + "characterId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "characterOverride_devilFruitId_devilFruit_id_fk": { + "name": "characterOverride_devilFruitId_devilFruit_id_fk", + "tableFrom": "characterOverride", + "tableTo": "devilFruit", + "columnsFrom": [ + "devilFruitId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "characterOverride_arcId_arc_id_fk": { + "name": "characterOverride_arcId_arc_id_fk", + "tableFrom": "characterOverride", + "tableTo": "arc", + "columnsFrom": [ + "arcId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "characterScrapeValidation": { + "name": "characterScrapeValidation", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "gender": { + "name": "gender", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "age": { + "name": "age", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "affiliations": { + "name": "affiliations", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "devilFruitId": { + "name": "devilFruitId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hakiObservation": { + "name": "hakiObservation", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "hakiArmament": { + "name": "hakiArmament", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "hakiConqueror": { + "name": "hakiConqueror", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "bounty": { + "name": "bounty", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "firstAppearance": { + "name": "firstAppearance", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pictureUrl": { + "name": "pictureUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "epithets": { + "name": "epithets", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "arcId": { + "name": "arcId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "characterScrapeValidation_devilFruitId_devilFruit_id_fk": { + "name": "characterScrapeValidation_devilFruitId_devilFruit_id_fk", + "tableFrom": "characterScrapeValidation", + "tableTo": "devilFruit", + "columnsFrom": [ + "devilFruitId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "characterScrapeValidation_arcId_arc_id_fk": { + "name": "characterScrapeValidation_arcId_arc_id_fk", + "tableFrom": "characterScrapeValidation", + "tableTo": "arc", + "columnsFrom": [ + "arcId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "config": { + "name": "config", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "devilFruit": { + "name": "devilFruit", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "devilFruit_name_unique": { + "name": "devilFruit_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_token_unique": { + "name": "session_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + "identifier" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 34c83da..4f6d47b 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1772383366179, "tag": "0001_nostalgic_hercules", "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1772390182445, + "tag": "0002_large_gwen_stacy", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/app.d.ts b/src/app.d.ts index 2ae6f13..b9b1ed3 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,18 +1,11 @@ import type { User, Session } from 'better-auth/minimal'; -// See https://svelte.dev/docs/kit/types#app.d.ts -// for information about these interfaces declare global { namespace App { interface Locals { - user?: User; + user?: User & { isAdmin?: boolean }; session?: Session; } - - // interface Error {} - // interface PageData {} - // interface PageState {} - // interface Platform {} } } diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 22e93f9..3bcead8 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,6 +1,9 @@ import type { Handle } from '@sveltejs/kit'; import { building } from '$app/environment'; import { auth } from '$lib/server/auth'; +import { db } from '$lib/server/db'; +import { eq } from 'drizzle-orm'; +import { user as userTable } from '$lib/server/db/auth.schema'; import { svelteKitHandler } from 'better-auth/svelte-kit'; const handleBetterAuth: Handle = async ({ event, resolve }) => { @@ -9,6 +12,12 @@ const handleBetterAuth: Handle = async ({ event, resolve }) => { if (session) { event.locals.session = session.session; event.locals.user = session.user; + + // Fetch the isAdmin field from the database + const dbUser = await db.select({ isAdmin: userTable.isAdmin }).from(userTable).where(eq(userTable.id, session.user.id)).limit(1); + if (dbUser.length > 0) { + (event.locals.user as any).isAdmin = dbUser[0].isAdmin; + } } return svelteKitHandler({ event, resolve, auth, building }); diff --git a/src/lib/components/ProfileButton.svelte b/src/lib/components/ProfileButton.svelte new file mode 100644 index 0000000..50ba784 --- /dev/null +++ b/src/lib/components/ProfileButton.svelte @@ -0,0 +1,111 @@ + + +
+ {#if user} + + + {#if isMenuOpen} +
+ + Voir mon profil + + {#if (user as any).isAdmin} + + Admin + + {/if} + +
+ {/if} + {:else} + + Se connecter + + {/if} +
diff --git a/src/lib/server/db/auth.schema.ts b/src/lib/server/db/auth.schema.ts index 66fc633..7af6d80 100644 --- a/src/lib/server/db/auth.schema.ts +++ b/src/lib/server/db/auth.schema.ts @@ -9,6 +9,7 @@ export const user = sqliteTable("user", { .default(false) .notNull(), image: text("image"), + isAdmin: integer("is_admin", { mode: "boolean" }).default(false).notNull(), createdAt: integer("created_at", { mode: "timestamp_ms" }) .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) .notNull(), diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts new file mode 100644 index 0000000..01d6551 --- /dev/null +++ b/src/routes/+layout.server.ts @@ -0,0 +1,8 @@ +import type { LayoutServerLoad } from './$types'; + +export const load: LayoutServerLoad = ({ locals }) => { + return { + user: locals.user || null, + session: locals.session || null + }; +}; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 0d8eb03..8030a96 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,9 +1,23 @@ -{@render children()} + +
+
+ +
+
+ {@render children()} +
+
diff --git a/src/routes/daily/+page.svelte b/src/routes/daily/+page.svelte index bb49f72..54dce03 100644 --- a/src/routes/daily/+page.svelte +++ b/src/routes/daily/+page.svelte @@ -154,7 +154,7 @@ }).catch(err => console.error('Failed to record win:', err)); // Check if it's gecko_moria for special animation - if (dailyCharacter.id === 'gecko_moria') { + if (dailyCharacter.id === 'gecko_moria_gecko_moria') { isGeckoMoriaWin = true; } } @@ -297,17 +297,8 @@ >
- -
- - + +

diff --git a/src/routes/login/+page.server.ts b/src/routes/login/+page.server.ts new file mode 100644 index 0000000..6ee5d54 --- /dev/null +++ b/src/routes/login/+page.server.ts @@ -0,0 +1,68 @@ +import { fail, redirect } from '@sveltejs/kit'; +import type { Actions } from './$types'; +import type { PageServerLoad } from './$types'; +import { auth } from '$lib/server/auth'; +import { APIError } from 'better-auth/api'; + +export const load: PageServerLoad = async (event) => { + if (event.locals.user) { + return redirect(302, '/'); + } + return {}; +}; + +export const actions: Actions = { + signInEmail: async (event) => { + const formData = await event.request.formData(); + const email = formData.get('email')?.toString() ?? ''; + const password = formData.get('password')?.toString() ?? ''; + + try { + await auth.api.signInEmail({ + body: { + email, + password, + callbackURL: '/auth/verification-success' + } + }); + } catch (error) { + if (error instanceof APIError) { + return fail(400, { message: error.message || 'Signin failed' }); + } + return fail(500, { message: 'Unexpected error' }); + } + + return redirect(302, '/'); + }, + signUpEmail: async (event) => { + const formData = await event.request.formData(); + const email = formData.get('email')?.toString() ?? ''; + const password = formData.get('password')?.toString() ?? ''; + const name = formData.get('name')?.toString() ?? ''; + + try { + await auth.api.signUpEmail({ + body: { + email, + password, + name, + callbackURL: '/auth/verification-success' + } + }); + } catch (error) { + if (error instanceof APIError) { + return fail(400, { message: error.message || 'Registration failed' }); + } + return fail(500, { message: 'Unexpected error' }); + } + + return redirect(302, '/'); + }, + logout: async (event) => { + await auth.api.signOut({ + headers: event.request.headers + }); + + return redirect(302, '/'); + } +}; diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte new file mode 100644 index 0000000..be7a465 --- /dev/null +++ b/src/routes/login/+page.svelte @@ -0,0 +1,151 @@ + + + + OnePieceDle - {isSignUp ? 'Inscription' : 'Connexion'} + + +
+
+
+ +
+
+ +
+

+ OnePieceDle +

+

+ {isSignUp ? 'Créer votre compte' : 'Bienvenue, pirate'} +

+
+ + +
+
{ + isLoading = true; + return async ({ update }) => { + isLoading = false; + await update(); + }; + }} + class="space-y-6" + > + +
+ + +
+ + +
+ + +
+ + + {#if isSignUp} +
+ + +
+ {/if} + + + {#if form?.message} +
+ {form.message} +
+ {/if} + + + +
+ + +
+

+ {isSignUp ? 'Vous avez déjà un compte ?' : "Vous n'avez pas de compte ?"} + +

+
+
+ + + +
+
+
diff --git a/src/routes/profile/+page.server.ts b/src/routes/profile/+page.server.ts new file mode 100644 index 0000000..38d0c0e --- /dev/null +++ b/src/routes/profile/+page.server.ts @@ -0,0 +1,119 @@ +import { fail, redirect } from '@sveltejs/kit'; +import type { Actions, PageServerLoad } from './$types'; +import { auth } from '$lib/server/auth'; +import { db } from '$lib/server/db'; +import { session } from '$lib/server/db/auth.schema'; +import { eq } from 'drizzle-orm'; +import { APIError } from 'better-auth/api'; + +export const load: PageServerLoad = async (event) => { + if (!event.locals.user) { + return redirect(302, '/login'); + } + + // Fetch all sessions for this user + const userSessions = await db + .select() + .from(session) + .where(eq(session.userId, event.locals.user.id)); + + return { + user: event.locals.user, + sessions: userSessions + }; +}; + +export const actions: Actions = { + updateProfile: async (event) => { + if (!event.locals.user) { + return redirect(302, '/login'); + } + + const formData = await event.request.formData(); + const name = formData.get('name')?.toString() ?? ''; + + if (!name.trim()) { + return fail(400, { message: 'Le nom ne peut pas être vide' }); + } + + try { + await auth.api.updateUser({ + body: { + name: name.trim() + }, + headers: event.request.headers + }); + } catch (error) { + if (error instanceof APIError) { + return fail(400, { message: error.message || 'Erreur lors de la mise à jour' }); + } + return fail(500, { message: 'Erreur inattendue' }); + } + + return { success: true }; + }, + changePassword: async (event) => { + if (!event.locals.user) { + return redirect(302, '/login'); + } + + const formData = await event.request.formData(); + const oldPassword = formData.get('oldPassword')?.toString() ?? ''; + const newPassword = formData.get('newPassword')?.toString() ?? ''; + const confirmPassword = formData.get('confirmPassword')?.toString() ?? ''; + + if (!oldPassword.trim()) { + return fail(400, { message: 'Le mot de passe actuel est requis' }); + } + + if (!newPassword.trim()) { + return fail(400, { message: 'Le nouveau mot de passe est requis' }); + } + + if (newPassword !== confirmPassword) { + return fail(400, { message: 'Les mots de passe ne correspondent pas' }); + } + + if (newPassword.length < 8) { + return fail(400, { message: 'Le mot de passe doit contenir au moins 8 caractères' }); + } + + try { + await auth.api.changePassword({ + body: { + currentPassword: oldPassword, + newPassword + }, + headers: event.request.headers + }); + } catch (error) { + if (error instanceof APIError) { + return fail(400, { message: error.message || 'Erreur lors du changement de mot de passe' }); + } + return fail(500, { message: 'Erreur inattendue' }); + } + + return { success: true }; + }, + revokeSession: async (event) => { + if (!event.locals.user) { + return redirect(302, '/login'); + } + + const formData = await event.request.formData(); + const sessionId = formData.get('sessionId')?.toString() ?? ''; + + if (!sessionId) { + return fail(400, { message: 'ID de session manquant' }); + } + + try { + // Delete the session from database + await db.delete(session).where(eq(session.id, sessionId)); + } catch (error) { + return fail(500, { message: 'Erreur lors de la révocation de la session' }); + } + + return { success: true, message: 'Session révoquée avec succès' }; + } +}; diff --git a/src/routes/profile/+page.svelte b/src/routes/profile/+page.svelte new file mode 100644 index 0000000..47317dd --- /dev/null +++ b/src/routes/profile/+page.svelte @@ -0,0 +1,334 @@ + + + + Mon Profil - OnePieceDle + + +
+
+
+ +
+
+ +
+

+ Mon Profil +

+

+ Modifie les informations de ton profil +

+
+ + +
+ + + +
+ + + {#if activeTab === 'profile'} +
+ +
+ {#if data.user.image} + {data.user.name + {:else} +
+ {data.user.name?.charAt(0).toUpperCase() || 'U'} +
+ {/if} +
+

Email

+

{data.user.email}

+
+
+ + +
{ + isLoading = true; + return async ({ update }) => { + isLoading = false; + await update(); + }; + }} + onsubmit={handleSubmit} + class="space-y-6" + > + +
+ + +
+ + + {#if form && form.message && form.success !== true} +
+ {form.message} +
+ {/if} + + + {#if showSuccess} +
+ Profil mis à jour avec succès ! +
+ {/if} + + + +
+
+ {/if} + + + {#if activeTab === 'password'} +
+

+ Changer le mot de passe +

+ + +
{ + isLoading = true; + return async ({ update }) => { + isLoading = false; + oldPassword = ''; + newPassword = ''; + confirmPassword = ''; + await update(); + }; + }} + class="space-y-6" + > + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + {#if form && form.message && form.success !== true} +
+ {form.message} +
+ {/if} + + + {#if showSuccess} +
+ Mot de passe changé avec succès ! +
+ {/if} + + + +
+
+ {/if} + + + {#if activeTab === 'sessions'} +
+

+ Sessions actives +

+ + {#if sessions.length === 0} +

Aucune session active

+ {:else} +
+ {#each sessions as sess} +
+
+

+ {sess.userAgent || 'Appareil inconnu'} +

+

+ IP: {sess.ipAddress || 'Inconnue'} +

+

+ Créée: {new Date(sess.createdAt).toLocaleDateString('fr-FR', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + })} +

+
+
{ + return async ({ update }) => { + await update(); + }; + }} + > + + +
+
+ {/each} +
+ {/if} +
+ {/if} + + + +
+
+