From e2cf2f02f5982287fc4960386b64707b5ffe1156 Mon Sep 17 00:00:00 2001 From: Samet Ekin Polat Date: Wed, 1 Apr 2026 00:17:11 +0300 Subject: [PATCH 01/14] #13 page created with ui kit --- src/app/login/page.tsx | 69 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/app/login/page.tsx diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 0000000..c400a31 --- /dev/null +++ b/src/app/login/page.tsx @@ -0,0 +1,69 @@ +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import Link from "next/link"; + +export default function LoginPage() { + return ( +
+ + + + Login + + + Enter your username/email and password to access your account. + + + + +
+ + +
+ +
+
+ + {/* forgot password route later */} + + Forgot password? + +
+ +
+
+ + + + +
+ Don't have an account?{" "} + + Sign up + +
+
+
+
+ ); +} \ No newline at end of file From d718443d545591475bac3852a4ce67813f5e9a82 Mon Sep 17 00:00:00 2001 From: Samet Ekin Polat Date: Wed, 1 Apr 2026 00:17:11 +0300 Subject: [PATCH 02/14] #13 page created with ui kit --- src/app/login/page.tsx | 69 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/app/login/page.tsx diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 0000000..c400a31 --- /dev/null +++ b/src/app/login/page.tsx @@ -0,0 +1,69 @@ +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import Link from "next/link"; + +export default function LoginPage() { + return ( +
+ + + + Login + + + Enter your username/email and password to access your account. + + + + +
+ + +
+ +
+
+ + {/* forgot password route later */} + + Forgot password? + +
+ +
+
+ + + + +
+ Don't have an account?{" "} + + Sign up + +
+
+
+
+ ); +} \ No newline at end of file From 2f9cba5397a819652c1f7f800df20f36cdd2715b Mon Sep 17 00:00:00 2001 From: Samet Ekin Polat Date: Wed, 1 Apr 2026 01:47:31 +0300 Subject: [PATCH 03/14] #9 er diagrams for database uploaded as svg file --- .../er diagram/AUTH & USER PROFILES.svg | 20 +++++++++++++++++++ .../er diagram/COMMUNITIES & MODERATION.svg | 20 +++++++++++++++++++ .../er diagram/CONTENT & INTERACTIONS.svg | 20 +++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 docs/database/er diagram/AUTH & USER PROFILES.svg create mode 100644 docs/database/er diagram/COMMUNITIES & MODERATION.svg create mode 100644 docs/database/er diagram/CONTENT & INTERACTIONS.svg diff --git a/docs/database/er diagram/AUTH & USER PROFILES.svg b/docs/database/er diagram/AUTH & USER PROFILES.svg new file mode 100644 index 0000000..de96abb --- /dev/null +++ b/docs/database/er diagram/AUTH & USER PROFILES.svg @@ -0,0 +1,20 @@ +1*1*1111usersiduuidnamevarcharusernamevarcharemailvarcharemail_verifiedtimestampimagevarcharpassword_hashvarcharkarma_scoreintbanner_image_urlvarcharcreated_attimestampaccountsiduuiduser_iduuidtypevarcharprovidervarcharprovider_account_idvarcharrefresh_tokentextaccess_tokentextexpires_atinttoken_typevarcharscopevarcharid_tokentextsession_statevarcharsessionsiduuidsession_tokenvarcharuser_iduuidexpirestimestampverification_tokensidentifiervarchartokenvarcharexpirestimestampuser_preferencesuser_iduuidinterface_languagevarchardo_not_translatejsonbthemevarcharupdated_attimestampglobal_moderatorsuser_iduuidpermissionsjsonbpromoted_attimestamp \ No newline at end of file diff --git a/docs/database/er diagram/COMMUNITIES & MODERATION.svg b/docs/database/er diagram/COMMUNITIES & MODERATION.svg new file mode 100644 index 0000000..dc91573 --- /dev/null +++ b/docs/database/er diagram/COMMUNITIES & MODERATION.svg @@ -0,0 +1,20 @@ +1*0..111*1*1*1*1*1*1*1*1*1*1*1*1*0..1*0..1*0..1*1*1*1*1*0..1*0..1*0..1*0..1*usersiduuidusernamevarcharpostsiduuidcommentsiduuidcommunitiesiduuidnamevarchardescriptiontextowner_iduuidis_user_profilebooleanlinked_user_iduuidis_nsfwbooleancreated_attimestampcommunity_rulesiduuidcommunity_iduuidtitlevarchardescriptiontextdisplay_orderintcreated_attimestampcommunity_invitesiduuidcommunity_iduuidinviter_iduuidinvitee_iduuidstatusinvite_statuscreated_attimestampcommunity_membersuser_iduuidcommunity_iduuidjoined_attimestampcommunity_moderatorsuser_iduuidcommunity_iduuidcan_manage_settingsbooleancan_manage_postsbooleancan_restrict_usersbooleanassigned_attimestampcommunity_restrictionsiduuidcommunity_iduuiduser_iduuidmoderator_iduuidtyperestriction_typereasontextexpires_attimestampcreated_attimestampmod_logsiduuidcommunity_iduuidmoderator_iduuidactionmod_action_typetarget_user_iduuidtarget_post_iduuidtarget_comment_iduuiddetailsjsonbcreated_attimestampuser_blocksblocker_iduuidblocked_iduuidcreated_attimestampreportsiduuidreporter_iduuidcommunity_iduuidreported_user_iduuidpost_iduuidcomment_iduuidrule_iduuidcustom_reasontextstatusreport_statuscreated_attimestamp \ No newline at end of file diff --git a/docs/database/er diagram/CONTENT & INTERACTIONS.svg b/docs/database/er diagram/CONTENT & INTERACTIONS.svg new file mode 100644 index 0000000..263691c --- /dev/null +++ b/docs/database/er diagram/CONTENT & INTERACTIONS.svg @@ -0,0 +1,20 @@ +1*1*1*0..1*1*1*0..1*1*1*1*0..1*0..1*1*1*1*1*1*1*1*1*1*0..1*0..1*0..1*0..1*1*1*1*1**1usersiduuidusernamevarcharcommunitiesiduuidnamevarcharcommunity_invitesiduuidcommunity_moderatorsuser_iduuidcommunity_iduuid(user_id, community_id)post_flairsiduuidcommunity_iduuidnamevarcharcolor_hexvarcharpostsiduuidcommunity_iduuiduser_iduuidflair_iduuidtitlevarcharbodytextlink_urlvarcharstatuspost_statusis_nsfwbooleanis_spoilerbooleanis_pinnedbooleanupvotesintdownvotesintis_deletedbooleansearch_vectortsvectorcreated_attimestampupdated_attimestampcommentsiduuidpost_iduuiduser_iduuidparent_comment_iduuidbodytextis_pinnedbooleanupvotesintdownvotesintis_deletedbooleansearch_vectortsvectorcreated_attimestampupdated_attimestamppost_edit_historyiduuidpost_iduuidprevious_titlevarcharprevious_bodytextchanged_attimestampcomment_edit_historyiduuidcomment_iduuidprevious_bodytextchanged_attimestampmediaiduuiduser_iduuidpost_iduuidcomment_iduuidmedia_urlvarchartypemedia_typeuploaded_attimestamppost_votesuser_iduuidpost_iduuidvote_valueintcomment_votesuser_iduuidcomment_iduuidvote_valueintsaved_postsuser_iduuidpost_iduuidsaved_attimestampsaved_commentsuser_iduuidcomment_iduuidsaved_attimestampnotificationsiduuiduser_iduuidactor_iduuidtypenotification_typepost_iduuidcomment_iduuidinvite_iduuidis_readbooleancreated_attimestampdirect_messagesiduuidsender_iduuidreceiver_iduuidcontenttextdeleted_by_senderbooleandeleted_by_receiverbooleanis_readbooleansent_attimestampeventsiduuidcommunity_iduuidcreator_iduuidtitlevarchardescriptiontextstart_timetimestampend_timetimestamp(creator_id, community_id) \ No newline at end of file From 3e95cec94d3df6050304e55bd2ced55a5c767e9a Mon Sep 17 00:00:00 2001 From: Samet Ekin Polat Date: Thu, 2 Apr 2026 21:33:56 +0300 Subject: [PATCH 04/14] #9 #14 add Prisma client and initial database schema with migrations --- package-lock.json | 908 +++++++++++++++++- package.json | 18 +- .../20260331232239_init/migration.sql | 587 +++++++++++ .../20260331232534_init/migration.sql | 2 + prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 530 ++++++++++ 6 files changed, 2046 insertions(+), 2 deletions(-) create mode 100644 prisma/migrations/20260331232239_init/migration.sql create mode 100644 prisma/migrations/20260331232534_init/migration.sql create mode 100644 prisma/migrations/migration_lock.toml diff --git a/package-lock.json b/package-lock.json index c3fcada..2c14682 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,9 @@ "": { "name": "arel_social", "version": "0.1.0", + "license": "ISC", "dependencies": { + "@prisma/client": "^7.6.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.7.0", @@ -27,6 +29,7 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.2.1", + "prisma": "^7.6.0", "tailwindcss": "^4", "typescript": "^5" } @@ -449,6 +452,29 @@ "node": ">=6.9.0" } }, + "node_modules/@clack/core": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.5.0.tgz", + "integrity": "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@clack/prompts": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.11.0.tgz", + "integrity": "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@clack/core": "0.5.0", + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, "node_modules/@dotenvx/dotenvx": { "version": "1.59.1", "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.59.1.tgz", @@ -646,6 +672,36 @@ "@noble/ciphers": "^1.0.0" } }, + "node_modules/@electric-sql/pglite": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz", + "integrity": "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@electric-sql/pglite-socket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.1.tgz", + "integrity": "sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "pglite-server": "dist/scripts/server.js" + }, + "peerDependencies": { + "@electric-sql/pglite": "0.4.1" + } + }, + "node_modules/@electric-sql/pglite-tools": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.3.1.tgz", + "integrity": "sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA==", + "devOptional": true, + "license": "Apache-2.0", + "peerDependencies": { + "@electric-sql/pglite": "0.4.1" + } + }, "node_modules/@emnapi/core": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", @@ -1567,6 +1623,13 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "devOptional": true, + "license": "MIT" + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.29.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", @@ -1921,6 +1984,246 @@ "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", "license": "MIT" }, + "node_modules/@prisma/client": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.6.0.tgz", + "integrity": "sha512-7Pe/1ayh3GgWPEg4mmT4ax77LJ1wC+XlnIFvQ94bLP2DsUnOpnruQQR3Jw7r+Frthk94QqDNxo3FjSg8h9PXeQ==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/client-runtime-utils": "7.6.0" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/client-runtime-utils": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.6.0.tgz", + "integrity": "sha512-fD7jlqubsZvVODKvsp9lOpXVecx2aWGxC2l35Ioz2t+teUJ5CfR0SAMsi7UkU1VvaZmmm+DS6BdujF622nY7tQ==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/config": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.6.0.tgz", + "integrity": "sha512-MuAz1MK4PeG5/03YzfzX3CnFVHQ6qePGwUpQRzPzX5tT0ffJ3Tzi9zJZbBc+VzEGFCM8ghW/gTVDR85Syjt+Yw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.20.0", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.6.0.tgz", + "integrity": "sha512-LpHr3qos4lQZ6sxwjStf59YBht7m9/QF7NSQsMH6qGENWZu2w3UkQUGn1h5iRkDjnWRj3VHykOu9qFhps4ADvA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/dev": { + "version": "0.24.3", + "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.3.tgz", + "integrity": "sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "@electric-sql/pglite": "0.4.1", + "@electric-sql/pglite-socket": "0.1.1", + "@electric-sql/pglite-tools": "0.3.1", + "@hono/node-server": "1.19.11", + "@prisma/get-platform": "7.2.0", + "@prisma/query-plan-executor": "7.2.0", + "@prisma/streams-local": "0.1.2", + "foreground-child": "3.3.1", + "get-port-please": "3.2.0", + "hono": "^4.12.8", + "http-status-codes": "2.3.0", + "pathe": "2.0.3", + "proper-lockfile": "4.1.2", + "remeda": "2.33.4", + "std-env": "3.10.0", + "valibot": "1.2.0", + "zeptomatch": "2.1.0" + } + }, + "node_modules/@prisma/dev/node_modules/@hono/node-server": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@prisma/engines": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.6.0.tgz", + "integrity": "sha512-Sn5edRzhHqgRV2M+A0eIbY442B4mReWWf3pKs/LKreYgW7oa/up8JtK/s4iv/EQA097cyboZ08mmkpbLp+tZ3w==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.6.0", + "@prisma/engines-version": "7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711", + "@prisma/fetch-engine": "7.6.0", + "@prisma/get-platform": "7.6.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711.tgz", + "integrity": "sha512-r51DLcJ8bDRSrBEJF3J4cinoWyGA7rfP2mG6lD90VqIbGNOkbfcLcXalSVjq5Y6brQS3vcjrq4GbyUb1Cb7vkw==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.6.0.tgz", + "integrity": "sha512-ohZDwXvtmnbzOcutR2D13lDWpZP1wQjmPyztmt0AwXLzQI7q95EE7NYCvS+M6N6SivT+BM0NOqLmTH3wms4L3A==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.6.0" + } + }, + "node_modules/@prisma/fetch-engine": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.6.0.tgz", + "integrity": "sha512-N575Ni95c3FkduWY/eKTHqNYgNbceZ1tQaSknVtJjpKmiiBXmniESn/GTxsDvICC4ZeiNrXxioGInzQrCdx16w==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.6.0", + "@prisma/engines-version": "7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711", + "@prisma/get-platform": "7.6.0" + } + }, + "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.6.0.tgz", + "integrity": "sha512-ohZDwXvtmnbzOcutR2D13lDWpZP1wQjmPyztmt0AwXLzQI7q95EE7NYCvS+M6N6SivT+BM0NOqLmTH3wms4L3A==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.6.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", + "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.2.0" + } + }, + "node_modules/@prisma/get-platform/node_modules/@prisma/debug": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", + "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/query-plan-executor": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz", + "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/streams-local": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@prisma/streams-local/-/streams-local-0.1.2.tgz", + "integrity": "sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "ajv": "^8.12.0", + "better-result": "^2.7.0", + "env-paths": "^3.0.0", + "proper-lockfile": "^4.1.2" + }, + "engines": { + "bun": ">=1.3.6", + "node": ">=22.0.0" + } + }, + "node_modules/@prisma/streams-local/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@prisma/streams-local/node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@prisma/streams-local/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@prisma/studio-core": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.27.3.tgz", + "integrity": "sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@radix-ui/react-toggle": "1.1.10", + "chart.js": "4.5.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0", + "pnpm": "8" + }, + "peerDependencies": { + "@types/react": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -3444,6 +3747,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "devOptional": true, + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -4834,6 +5144,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/axe-core": { "version": "4.11.2", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.2.tgz", @@ -4873,6 +5193,19 @@ "node": ">=6.0.0" } }, + "node_modules/better-result": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/better-result/-/better-result-2.7.0.tgz", + "integrity": "sha512-7zrmXjAK8u8Z6SOe4R65XObOR5X+Y2I/VVku3t5cPOGQ8/WsBcfFmfnIPiEl5EBMDOzPHRwbiPbMtQBKYdw7RA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@clack/prompts": "^0.11.0" + }, + "bin": { + "better-result": "bin/cli.mjs" + } + }, "node_modules/body-parser": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", @@ -4977,6 +5310,48 @@ "node": ">= 0.8" } }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -5071,6 +5446,45 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -5246,6 +5660,23 @@ "dev": true, "license": "MIT" }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/content-disposition": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", @@ -5485,6 +5916,16 @@ "node": ">=0.10.0" } }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/default-browser": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", @@ -5561,6 +6002,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -5570,6 +6028,13 @@ "node": ">= 0.8" } }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "devOptional": true, + "license": "MIT" + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -5657,6 +6122,17 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/effect": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz", + "integrity": "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.329", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.329.tgz", @@ -5670,6 +6146,16 @@ "dev": true, "license": "MIT" }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -6474,6 +6960,36 @@ "express": ">= 4.11" } }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6687,6 +7203,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -6777,6 +7310,16 @@ "integrity": "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==", "license": "MIT" }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -6862,6 +7405,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-port-please": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz", + "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==", + "devOptional": true, + "license": "MIT" + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -6922,6 +7472,24 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -6983,6 +7551,20 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/grammex": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz", + "integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/graphmatch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz", + "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/graphql": { "version": "16.13.2", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.2.tgz", @@ -7136,6 +7718,13 @@ "url": "https://opencollective.com/express" } }, + "node_modules/http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", + "devOptional": true, + "license": "MIT" + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -7623,6 +8212,13 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "devOptional": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -7854,7 +8450,7 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -8367,6 +8963,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "devOptional": true, + "license": "Apache-2.0" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -8389,6 +8992,22 @@ "yallist": "^3.0.2" } }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "devOptional": true, + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/lucide-react": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.7.0.tgz", @@ -8606,6 +9225,40 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/mysql2": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", + "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -8804,6 +9457,13 @@ "url": "https://opencollective.com/node-fetch" } }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "devOptional": true, + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.36", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", @@ -8838,6 +9498,31 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/nypm": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", + "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.2.0", + "pathe": "^2.0.3", + "tinyexec": "^1.0.2" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz", + "integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -8968,6 +9653,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "devOptional": true, + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -9222,6 +9914,20 @@ "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "devOptional": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -9249,6 +9955,18 @@ "node": ">=16.20.0" } }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -9300,6 +10018,20 @@ "node": ">=4" } }, + "node_modules/postgres": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", + "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", + "devOptional": true, + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, "node_modules/powershell-utils": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", @@ -9337,6 +10069,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/prisma": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.6.0.tgz", + "integrity": "sha512-OKJIPT81K3+F+AayIkY/Y3mkF2NWoFh7lZApaaqPYy7EHILKdO0VsmGkP+hDKYTySHsFSyLWXm/JgcR1B8fY1Q==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "7.6.0", + "@prisma/dev": "0.24.3", + "@prisma/engines": "7.6.0", + "@prisma/studio-core": "0.27.3", + "mysql2": "3.15.3", + "postgres": "3.4.7" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "better-sqlite3": ">=9.0.0", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -9371,6 +10137,25 @@ "react-is": "^16.13.1" } }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "devOptional": true, + "license": "ISC" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -9394,6 +10179,23 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/qs": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", @@ -9530,6 +10332,17 @@ "node": ">= 0.10" } }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", @@ -9627,6 +10440,20 @@ } } }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/recast": { "version": "0.23.11", "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", @@ -9687,6 +10514,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/remeda": { + "version": "2.33.4", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz", + "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==", + "devOptional": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/remeda" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -9761,6 +10598,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/rettime": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.10.1.tgz", @@ -9940,6 +10787,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", + "devOptional": true + }, "node_modules/serve-static": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", @@ -10297,6 +11150,16 @@ "node": ">=0.10.0" } }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -10313,6 +11176,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/stdin-discarder": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", @@ -10645,6 +11515,16 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -11164,6 +12044,21 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/valibot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", + "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/validate-npm-package-name": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", @@ -11502,6 +12397,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zeptomatch": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", + "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "grammex": "^3.1.11", + "graphmatch": "^1.1.0" + } + }, "node_modules/zod": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", diff --git a/package.json b/package.json index 1c8a46c..7e8f9cb 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "eslint" }, "dependencies": { + "@prisma/client": "^7.6.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.7.0", @@ -28,7 +29,22 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.2.1", + "prisma": "^7.6.0", "tailwindcss": "^4", "typescript": "^5" - } + }, + "description": "This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).", + "main": "index.js", + "repository": { + "type": "git", + "url": "git+https://github.com/sametekinpolat/SociArel.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "bugs": { + "url": "https://github.com/sametekinpolat/SociArel/issues" + }, + "homepage": "https://github.com/sametekinpolat/SociArel#readme" } diff --git a/prisma/migrations/20260331232239_init/migration.sql b/prisma/migrations/20260331232239_init/migration.sql new file mode 100644 index 0000000..5bc345f --- /dev/null +++ b/prisma/migrations/20260331232239_init/migration.sql @@ -0,0 +1,587 @@ +-- CreateEnum +CREATE TYPE "MediaType" AS ENUM ('IMAGE', 'VIDEO'); + +-- CreateEnum +CREATE TYPE "ReportStatus" AS ENUM ('PENDING', 'RESOLVED', 'DISMISSED'); + +-- CreateEnum +CREATE TYPE "NotificationType" AS ENUM ('REPLY', 'MENTION', 'DIRECT_MESSAGE', 'COMMUNITY_INVITE'); + +-- CreateEnum +CREATE TYPE "PostStatus" AS ENUM ('DRAFT', 'PUBLISHED', 'REMOVED'); + +-- CreateEnum +CREATE TYPE "RestrictionType" AS ENUM ('BAN', 'MUTE'); + +-- CreateEnum +CREATE TYPE "ModActionType" AS ENUM ('REMOVE_POST', 'REMOVE_COMMENT', 'BAN_USER', 'MUTE_USER', 'UPDATE_SETTINGS'); + +-- CreateEnum +CREATE TYPE "InviteStatus" AS ENUM ('PENDING', 'ACCEPTED', 'REJECTED'); + +-- CreateTable +CREATE TABLE "User" ( + "id" UUID NOT NULL, + "name" TEXT, + "username" TEXT, + "email" TEXT, + "emailVerified" TIMESTAMP(3), + "image" TEXT, + "passwordHash" TEXT, + "karmaScore" INTEGER NOT NULL DEFAULT 0, + "bannerImageUrl" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Account" ( + "id" UUID NOT NULL, + "userId" UUID NOT NULL, + "type" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "providerAccountId" TEXT NOT NULL, + "refresh_token" TEXT, + "access_token" TEXT, + "expires_at" INTEGER, + "token_type" TEXT, + "scope" TEXT, + "id_token" TEXT, + "session_state" TEXT, + + CONSTRAINT "Account_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Session" ( + "id" UUID NOT NULL, + "sessionToken" TEXT NOT NULL, + "userId" UUID NOT NULL, + "expires" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Session_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "VerificationToken" ( + "identifier" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL +); + +-- CreateTable +CREATE TABLE "UserPreference" ( + "userId" UUID NOT NULL, + "interfaceLanguage" TEXT NOT NULL DEFAULT 'en', + "doNotTranslate" JSONB, + "theme" TEXT NOT NULL DEFAULT 'system', + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "UserPreference_pkey" PRIMARY KEY ("userId") +); + +-- CreateTable +CREATE TABLE "GlobalModerator" ( + "userId" UUID NOT NULL, + "permissions" JSONB NOT NULL, + "promotedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "GlobalModerator_pkey" PRIMARY KEY ("userId") +); + +-- CreateTable +CREATE TABLE "Community" ( + "id" UUID NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "ownerId" UUID NOT NULL, + "isUserProfile" BOOLEAN NOT NULL DEFAULT false, + "linkedUserId" UUID, + "isNsfw" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Community_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "CommunityRule" ( + "id" UUID NOT NULL, + "communityId" UUID NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "displayOrder" INTEGER NOT NULL DEFAULT 0, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "CommunityRule_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "CommunityInvite" ( + "id" UUID NOT NULL, + "communityId" UUID NOT NULL, + "inviterId" UUID NOT NULL, + "inviteeId" UUID NOT NULL, + "status" "InviteStatus" NOT NULL DEFAULT 'PENDING', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "CommunityInvite_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "CommunityMember" ( + "userId" UUID NOT NULL, + "communityId" UUID NOT NULL, + "joinedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "CommunityMember_pkey" PRIMARY KEY ("userId","communityId") +); + +-- CreateTable +CREATE TABLE "CommunityModerator" ( + "userId" UUID NOT NULL, + "communityId" UUID NOT NULL, + "canManageSettings" BOOLEAN NOT NULL DEFAULT false, + "canManagePosts" BOOLEAN NOT NULL DEFAULT false, + "canRestrictUsers" BOOLEAN NOT NULL DEFAULT false, + "assignedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "CommunityModerator_pkey" PRIMARY KEY ("userId","communityId") +); + +-- CreateTable +CREATE TABLE "CommunityRestriction" ( + "id" UUID NOT NULL, + "communityId" UUID NOT NULL, + "userId" UUID NOT NULL, + "moderatorId" UUID NOT NULL, + "type" "RestrictionType" NOT NULL, + "reason" TEXT, + "expiresAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "CommunityRestriction_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ModLog" ( + "id" UUID NOT NULL, + "communityId" UUID NOT NULL, + "moderatorId" UUID NOT NULL, + "action" "ModActionType" NOT NULL, + "targetUserId" UUID, + "targetPostId" UUID, + "targetCommentId" UUID, + "details" JSONB, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ModLog_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UserBlock" ( + "blockerId" UUID NOT NULL, + "blockedId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "UserBlock_pkey" PRIMARY KEY ("blockerId","blockedId") +); + +-- CreateTable +CREATE TABLE "PostFlair" ( + "id" UUID NOT NULL, + "communityId" UUID NOT NULL, + "name" TEXT NOT NULL, + "colorHex" TEXT, + + CONSTRAINT "PostFlair_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Post" ( + "id" UUID NOT NULL, + "communityId" UUID NOT NULL, + "userId" UUID NOT NULL, + "flairId" UUID, + "title" TEXT NOT NULL, + "body" TEXT, + "linkUrl" TEXT, + "status" "PostStatus" NOT NULL DEFAULT 'DRAFT', + "isNsfw" BOOLEAN NOT NULL DEFAULT false, + "isSpoiler" BOOLEAN NOT NULL DEFAULT false, + "isPinned" BOOLEAN NOT NULL DEFAULT false, + "upvotes" INTEGER NOT NULL DEFAULT 0, + "downvotes" INTEGER NOT NULL DEFAULT 0, + "isDeleted" BOOLEAN NOT NULL DEFAULT false, + "searchVector" tsvector, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Post_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Comment" ( + "id" UUID NOT NULL, + "postId" UUID NOT NULL, + "userId" UUID NOT NULL, + "parentCommentId" UUID, + "body" TEXT NOT NULL, + "isPinned" BOOLEAN NOT NULL DEFAULT false, + "upvotes" INTEGER NOT NULL DEFAULT 0, + "downvotes" INTEGER NOT NULL DEFAULT 0, + "isDeleted" BOOLEAN NOT NULL DEFAULT false, + "searchVector" tsvector, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Comment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PostEditHistory" ( + "id" UUID NOT NULL, + "postId" UUID NOT NULL, + "previousTitle" TEXT NOT NULL, + "previousBody" TEXT, + "changedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "PostEditHistory_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "CommentEditHistory" ( + "id" UUID NOT NULL, + "commentId" UUID NOT NULL, + "previousBody" TEXT NOT NULL, + "changedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "CommentEditHistory_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PostVote" ( + "userId" UUID NOT NULL, + "postId" UUID NOT NULL, + "voteValue" INTEGER NOT NULL, + + CONSTRAINT "PostVote_pkey" PRIMARY KEY ("userId","postId") +); + +-- CreateTable +CREATE TABLE "CommentVote" ( + "userId" UUID NOT NULL, + "commentId" UUID NOT NULL, + "voteValue" INTEGER NOT NULL, + + CONSTRAINT "CommentVote_pkey" PRIMARY KEY ("userId","commentId") +); + +-- CreateTable +CREATE TABLE "SavedPost" ( + "userId" UUID NOT NULL, + "postId" UUID NOT NULL, + "savedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "SavedPost_pkey" PRIMARY KEY ("userId","postId") +); + +-- CreateTable +CREATE TABLE "SavedComment" ( + "userId" UUID NOT NULL, + "commentId" UUID NOT NULL, + "savedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "SavedComment_pkey" PRIMARY KEY ("userId","commentId") +); + +-- CreateTable +CREATE TABLE "Report" ( + "id" UUID NOT NULL, + "reporterId" UUID NOT NULL, + "communityId" UUID NOT NULL, + "reportedUserId" UUID, + "postId" UUID, + "commentId" UUID, + "ruleId" UUID, + "customReason" TEXT, + "status" "ReportStatus" NOT NULL DEFAULT 'PENDING', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Report_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Notification" ( + "id" UUID NOT NULL, + "userId" UUID NOT NULL, + "actorId" UUID, + "type" "NotificationType" NOT NULL, + "postId" UUID, + "commentId" UUID, + "inviteId" UUID, + "isRead" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Notification_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Media" ( + "id" UUID NOT NULL, + "userId" UUID NOT NULL, + "postId" UUID, + "commentId" UUID, + "mediaUrl" TEXT NOT NULL, + "type" "MediaType" NOT NULL, + "uploadedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Media_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "DirectMessage" ( + "id" UUID NOT NULL, + "senderId" UUID NOT NULL, + "receiverId" UUID NOT NULL, + "content" TEXT NOT NULL, + "deletedBySender" BOOLEAN NOT NULL DEFAULT false, + "deletedByReceiver" BOOLEAN NOT NULL DEFAULT false, + "isRead" BOOLEAN NOT NULL DEFAULT false, + "sentAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "DirectMessage_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Event" ( + "id" UUID NOT NULL, + "communityId" UUID NOT NULL, + "creatorId" UUID NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "startTime" TIMESTAMP(3) NOT NULL, + "endTime" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Event_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken"); + +-- CreateIndex +CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token"); + +-- CreateIndex +CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token"); + +-- CreateIndex +CREATE UNIQUE INDEX "Community_name_key" ON "Community"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "Community_linkedUserId_key" ON "Community"("linkedUserId"); + +-- AddForeignKey +ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserPreference" ADD CONSTRAINT "UserPreference_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "GlobalModerator" ADD CONSTRAINT "GlobalModerator_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Community" ADD CONSTRAINT "Community_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Community" ADD CONSTRAINT "Community_linkedUserId_fkey" FOREIGN KEY ("linkedUserId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CommunityRule" ADD CONSTRAINT "CommunityRule_communityId_fkey" FOREIGN KEY ("communityId") REFERENCES "Community"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CommunityInvite" ADD CONSTRAINT "CommunityInvite_communityId_fkey" FOREIGN KEY ("communityId") REFERENCES "Community"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CommunityInvite" ADD CONSTRAINT "CommunityInvite_inviterId_fkey" FOREIGN KEY ("inviterId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CommunityInvite" ADD CONSTRAINT "CommunityInvite_inviteeId_fkey" FOREIGN KEY ("inviteeId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CommunityMember" ADD CONSTRAINT "CommunityMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CommunityMember" ADD CONSTRAINT "CommunityMember_communityId_fkey" FOREIGN KEY ("communityId") REFERENCES "Community"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CommunityModerator" ADD CONSTRAINT "CommunityModerator_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CommunityModerator" ADD CONSTRAINT "CommunityModerator_communityId_fkey" FOREIGN KEY ("communityId") REFERENCES "Community"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CommunityRestriction" ADD CONSTRAINT "CommunityRestriction_communityId_fkey" FOREIGN KEY ("communityId") REFERENCES "Community"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CommunityRestriction" ADD CONSTRAINT "CommunityRestriction_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CommunityRestriction" ADD CONSTRAINT "CommunityRestriction_moderatorId_fkey" FOREIGN KEY ("moderatorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ModLog" ADD CONSTRAINT "ModLog_communityId_fkey" FOREIGN KEY ("communityId") REFERENCES "Community"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ModLog" ADD CONSTRAINT "ModLog_moderatorId_fkey" FOREIGN KEY ("moderatorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ModLog" ADD CONSTRAINT "ModLog_targetUserId_fkey" FOREIGN KEY ("targetUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ModLog" ADD CONSTRAINT "ModLog_targetPostId_fkey" FOREIGN KEY ("targetPostId") REFERENCES "Post"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ModLog" ADD CONSTRAINT "ModLog_targetCommentId_fkey" FOREIGN KEY ("targetCommentId") REFERENCES "Comment"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserBlock" ADD CONSTRAINT "UserBlock_blockerId_fkey" FOREIGN KEY ("blockerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserBlock" ADD CONSTRAINT "UserBlock_blockedId_fkey" FOREIGN KEY ("blockedId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PostFlair" ADD CONSTRAINT "PostFlair_communityId_fkey" FOREIGN KEY ("communityId") REFERENCES "Community"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Post" ADD CONSTRAINT "Post_communityId_fkey" FOREIGN KEY ("communityId") REFERENCES "Community"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Post" ADD CONSTRAINT "Post_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Post" ADD CONSTRAINT "Post_flairId_fkey" FOREIGN KEY ("flairId") REFERENCES "PostFlair"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Comment" ADD CONSTRAINT "Comment_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Comment" ADD CONSTRAINT "Comment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Comment" ADD CONSTRAINT "Comment_parentCommentId_fkey" FOREIGN KEY ("parentCommentId") REFERENCES "Comment"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PostEditHistory" ADD CONSTRAINT "PostEditHistory_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CommentEditHistory" ADD CONSTRAINT "CommentEditHistory_commentId_fkey" FOREIGN KEY ("commentId") REFERENCES "Comment"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PostVote" ADD CONSTRAINT "PostVote_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PostVote" ADD CONSTRAINT "PostVote_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CommentVote" ADD CONSTRAINT "CommentVote_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CommentVote" ADD CONSTRAINT "CommentVote_commentId_fkey" FOREIGN KEY ("commentId") REFERENCES "Comment"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "SavedPost" ADD CONSTRAINT "SavedPost_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "SavedPost" ADD CONSTRAINT "SavedPost_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "SavedComment" ADD CONSTRAINT "SavedComment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "SavedComment" ADD CONSTRAINT "SavedComment_commentId_fkey" FOREIGN KEY ("commentId") REFERENCES "Comment"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Report" ADD CONSTRAINT "Report_reporterId_fkey" FOREIGN KEY ("reporterId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Report" ADD CONSTRAINT "Report_communityId_fkey" FOREIGN KEY ("communityId") REFERENCES "Community"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Report" ADD CONSTRAINT "Report_reportedUserId_fkey" FOREIGN KEY ("reportedUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Report" ADD CONSTRAINT "Report_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Report" ADD CONSTRAINT "Report_commentId_fkey" FOREIGN KEY ("commentId") REFERENCES "Comment"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Report" ADD CONSTRAINT "Report_ruleId_fkey" FOREIGN KEY ("ruleId") REFERENCES "CommunityRule"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Notification" ADD CONSTRAINT "Notification_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Notification" ADD CONSTRAINT "Notification_actorId_fkey" FOREIGN KEY ("actorId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Notification" ADD CONSTRAINT "Notification_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Notification" ADD CONSTRAINT "Notification_commentId_fkey" FOREIGN KEY ("commentId") REFERENCES "Comment"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Notification" ADD CONSTRAINT "Notification_inviteId_fkey" FOREIGN KEY ("inviteId") REFERENCES "CommunityInvite"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Media" ADD CONSTRAINT "Media_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Media" ADD CONSTRAINT "Media_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Media" ADD CONSTRAINT "Media_commentId_fkey" FOREIGN KEY ("commentId") REFERENCES "Comment"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DirectMessage" ADD CONSTRAINT "DirectMessage_senderId_fkey" FOREIGN KEY ("senderId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DirectMessage" ADD CONSTRAINT "DirectMessage_receiverId_fkey" FOREIGN KEY ("receiverId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Event" ADD CONSTRAINT "Event_communityId_fkey" FOREIGN KEY ("communityId") REFERENCES "Community"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Event" ADD CONSTRAINT "Event_creatorId_communityId_fkey" FOREIGN KEY ("creatorId", "communityId") REFERENCES "CommunityModerator"("userId", "communityId") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- 1. Create a function that calculates the vector +CREATE OR REPLACE FUNCTION post_search_vector_update() RETURNS trigger AS $$ +BEGIN + NEW."searchVector" := + setweight(to_tsvector('english', coalesce(NEW.title, '')), 'A') || + setweight(to_tsvector('english', coalesce(NEW.body, '')), 'B'); + RETURN NEW; +END +$$ LANGUAGE plpgsql; + +-- 2. Create a trigger that runs this function EVERY TIME a post is created or updated +CREATE TRIGGER post_search_vector_trigger +BEFORE INSERT OR UPDATE ON "Post" +FOR EACH ROW EXECUTE FUNCTION post_search_vector_update(); + +-- 3. Create the index to make searching lightning fast +CREATE INDEX post_search_idx ON "Post" USING GIN ("searchVector"); \ No newline at end of file diff --git a/prisma/migrations/20260331232534_init/migration.sql b/prisma/migrations/20260331232534_init/migration.sql new file mode 100644 index 0000000..d5035c0 --- /dev/null +++ b/prisma/migrations/20260331232534_init/migration.sql @@ -0,0 +1,2 @@ +-- DropIndex +DROP INDEX "post_search_idx"; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 10c4de8..ebb1fbc 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -3,6 +3,7 @@ // Get a free hosted Postgres database in seconds: `npx create-db` + generator client { provider = "prisma-client" output = "../src/generated/prisma" @@ -11,3 +12,532 @@ generator client { datasource db { provider = "postgresql" } + + +// ENUMS + + +enum MediaType { + IMAGE + VIDEO +} + +enum ReportStatus { + PENDING + RESOLVED + DISMISSED +} + +enum NotificationType { + REPLY + MENTION + DIRECT_MESSAGE + COMMUNITY_INVITE +} + +enum PostStatus { + DRAFT + PUBLISHED + REMOVED +} + +enum RestrictionType { + BAN + MUTE +} + +enum ModActionType { + REMOVE_POST + REMOVE_COMMENT + BAN_USER + MUTE_USER + UPDATE_SETTINGS +} + +enum InviteStatus { + PENDING + ACCEPTED + REJECTED +} + + +//AUTH.JS & CORE USER PROFILES + + +model User { + id String @id @default(uuid()) @db.Uuid + name String? + username String? @unique + email String? @unique + emailVerified DateTime? + image String? + passwordHash String? + karmaScore Int @default(0) // Requires Postgres Trigger or $transaction to sync + bannerImageUrl String? + createdAt DateTime @default(now()) + + // Auth.js Relations + accounts Account[] + sessions Session[] + + // One-to-One Profiles + preferences UserPreference? + globalMod GlobalModerator? + + // Content Relations + posts Post[] + comments Comment[] + media Media[] + + // Community and Ownership Relations + ownedCommunities Community[] @relation("CommunityOwner") + linkedCommunity Community? @relation("UserLinkedCommunity") + memberships CommunityMember[] + moderations CommunityModerator[] + + // Invites + invitesSent CommunityInvite[] @relation("Inviter") + invitesReceived CommunityInvite[] @relation("Invitee") + + // Voting, Saves, Reports + postVotes PostVote[] + commentVotes CommentVote[] + savedPosts SavedPost[] + savedComments SavedComment[] + reportsMade Report[] @relation("Reporter") + reportsTargeted Report[] @relation("ReportedUser") + + // Moderation Relations + issuedRestrictions CommunityRestriction[] @relation("ModWhoRestricted") + receivedRestrictions CommunityRestriction[] @relation("RestrictedUser") + modLogsActed ModLog[] @relation("ModWhoActed") + modLogsTargeted ModLog[] @relation("TargetedUser") + + // Interactions + blocksInitiated UserBlock[] @relation("Blocker") + blocksReceived UserBlock[] @relation("Blocked") + messagesSent DirectMessage[] @relation("MessageSender") + messagesReceived DirectMessage[] @relation("MessageReceiver") + notifications Notification[] @relation("UserNotification") + triggeredNotifs Notification[] @relation("NotificationActor") +} + +model Account { + id String @id @default(uuid()) @db.Uuid + userId String @db.Uuid + type String + provider String + providerAccountId String + refresh_token String? @db.Text + access_token String? @db.Text + expires_at Int? + token_type String? + scope String? + id_token String? @db.Text + session_state String? + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) +} + +model Session { + id String @id @default(uuid()) @db.Uuid + sessionToken String @unique + userId String @db.Uuid + expires DateTime + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + +model VerificationToken { + identifier String + token String @unique + expires DateTime + + @@unique([identifier, token]) +} + +model UserPreference { + userId String @id @db.Uuid + interfaceLanguage String @default("en") + doNotTranslate Json? // Maps to Postgres JSONB automatically + theme String @default("system") + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + +model GlobalModerator { + userId String @id @db.Uuid + permissions Json // Maps to Postgres JSONB automatically + promotedAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + + +// COMMUNITIES, RULES, INVITES + + +model Community { + id String @id @default(uuid()) @db.Uuid + name String @unique + description String? + ownerId String @db.Uuid + isUserProfile Boolean @default(false) + linkedUserId String? @unique @db.Uuid + isNsfw Boolean @default(false) + createdAt DateTime @default(now()) + + owner User @relation("CommunityOwner", fields: [ownerId], references: [id]) + linkedUser User? @relation("UserLinkedCommunity", fields: [linkedUserId], references: [id], onDelete: Cascade) + + rules CommunityRule[] + invites CommunityInvite[] + members CommunityMember[] + moderators CommunityModerator[] + restrictions CommunityRestriction[] + modLogs ModLog[] + flairs PostFlair[] + posts Post[] + reports Report[] + events Event[] +} + +model CommunityRule { + id String @id @default(uuid()) @db.Uuid + communityId String @db.Uuid + title String + description String? + displayOrder Int @default(0) + createdAt DateTime @default(now()) + + community Community @relation(fields: [communityId], references: [id], onDelete: Cascade) + reports Report[] +} + +model CommunityInvite { + id String @id @default(uuid()) @db.Uuid + communityId String @db.Uuid + inviterId String @db.Uuid + inviteeId String @db.Uuid + status InviteStatus @default(PENDING) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + community Community @relation(fields: [communityId], references: [id], onDelete: Cascade) + inviter User @relation("Inviter", fields: [inviterId], references: [id]) + invitee User @relation("Invitee", fields: [inviteeId], references: [id], onDelete: Cascade) + notifications Notification[] +} + + +// MEMBERSHIPS, MODERATION, RESTRICTIONS + + +model CommunityMember { + userId String @db.Uuid + communityId String @db.Uuid + joinedAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + community Community @relation(fields: [communityId], references: [id], onDelete: Cascade) + + @@id([userId, communityId]) +} + +model CommunityModerator { + userId String @db.Uuid + communityId String @db.Uuid + canManageSettings Boolean @default(false) + canManagePosts Boolean @default(false) + canRestrictUsers Boolean @default(false) + assignedAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + community Community @relation(fields: [communityId], references: [id], onDelete: Cascade) + eventsCreated Event[] + + @@id([userId, communityId]) // Allows the composite reference for Events +} + +model CommunityRestriction { + id String @id @default(uuid()) @db.Uuid + communityId String @db.Uuid + userId String @db.Uuid + moderatorId String @db.Uuid + type RestrictionType + reason String? + expiresAt DateTime? + createdAt DateTime @default(now()) + + community Community @relation(fields: [communityId], references: [id], onDelete: Cascade) + user User @relation("RestrictedUser", fields: [userId], references: [id], onDelete: Cascade) + moderator User @relation("ModWhoRestricted", fields: [moderatorId], references: [id]) +} + +model ModLog { + id String @id @default(uuid()) @db.Uuid + communityId String @db.Uuid + moderatorId String @db.Uuid + action ModActionType + targetUserId String? @db.Uuid + targetPostId String? @db.Uuid + targetCommentId String? @db.Uuid + details Json? // Maps to Postgres JSONB automatically + createdAt DateTime @default(now()) + + community Community @relation(fields: [communityId], references: [id], onDelete: Cascade) + moderator User @relation("ModWhoActed", fields: [moderatorId], references: [id]) + targetUser User? @relation("TargetedUser", fields: [targetUserId], references: [id]) + targetPost Post? @relation(fields: [targetPostId], references: [id]) + targetComment Comment? @relation(fields: [targetCommentId], references: [id]) +} + +model UserBlock { + blockerId String @db.Uuid + blockedId String @db.Uuid + createdAt DateTime @default(now()) + + blocker User @relation("Blocker", fields: [blockerId], references: [id], onDelete: Cascade) + blocked User @relation("Blocked", fields: [blockedId], references: [id], onDelete: Cascade) + + @@id([blockerId, blockedId]) +} + + +// POSTS, COMMENTS, FLAIRS & HISTORY + + +model PostFlair { + id String @id @default(uuid()) @db.Uuid + communityId String @db.Uuid + name String + colorHex String? + + community Community @relation(fields: [communityId], references: [id], onDelete: Cascade) + posts Post[] +} + +model Post { + id String @id @default(uuid()) @db.Uuid + communityId String @db.Uuid + userId String @db.Uuid + flairId String? @db.Uuid + title String + body String? + linkUrl String? + status PostStatus @default(DRAFT) + isNsfw Boolean @default(false) + isSpoiler Boolean @default(false) + isPinned Boolean @default(false) + upvotes Int @default(0) + downvotes Int @default(0) + isDeleted Boolean @default(false) + + // Requires raw SQL migration to actually index this column + searchVector Unsupported("tsvector")? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + community Community @relation(fields: [communityId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + flair PostFlair? @relation(fields: [flairId], references: [id]) + + comments Comment[] + editHistory PostEditHistory[] + votes PostVote[] + saves SavedPost[] + reports Report[] + notifications Notification[] + media Media[] + modLogs ModLog[] +} + +model Comment { + id String @id @default(uuid()) @db.Uuid + postId String @db.Uuid + userId String @db.Uuid + parentCommentId String? @db.Uuid + body String + isPinned Boolean @default(false) + upvotes Int @default(0) + downvotes Int @default(0) + isDeleted Boolean @default(false) + + searchVector Unsupported("tsvector")? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + post Post @relation(fields: [postId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // Self-referencing thread logic + parentComment Comment? @relation("CommentReplies", fields: [parentCommentId], references: [id]) + replies Comment[] @relation("CommentReplies") + + editHistory CommentEditHistory[] + votes CommentVote[] + saves SavedComment[] + reports Report[] + notifications Notification[] + media Media[] + modLogs ModLog[] +} + +model PostEditHistory { + id String @id @default(uuid()) @db.Uuid + postId String @db.Uuid + previousTitle String + previousBody String? + changedAt DateTime @default(now()) + + post Post @relation(fields: [postId], references: [id], onDelete: Cascade) +} + +model CommentEditHistory { + id String @id @default(uuid()) @db.Uuid + commentId String @db.Uuid + previousBody String + changedAt DateTime @default(now()) + + comment Comment @relation(fields: [commentId], references: [id], onDelete: Cascade) +} + + +// VOTES, SAVES, REPORTS + + +model PostVote { + userId String @db.Uuid + postId String @db.Uuid + voteValue Int + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + post Post @relation(fields: [postId], references: [id], onDelete: Cascade) + + @@id([userId, postId]) +} + +model CommentVote { + userId String @db.Uuid + commentId String @db.Uuid + voteValue Int + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + comment Comment @relation(fields: [commentId], references: [id], onDelete: Cascade) + + @@id([userId, commentId]) +} + +model SavedPost { + userId String @db.Uuid + postId String @db.Uuid + savedAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + post Post @relation(fields: [postId], references: [id], onDelete: Cascade) + + @@id([userId, postId]) +} + +model SavedComment { + userId String @db.Uuid + commentId String @db.Uuid + savedAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + comment Comment @relation(fields: [commentId], references: [id], onDelete: Cascade) + + @@id([userId, commentId]) +} + +model Report { + id String @id @default(uuid()) @db.Uuid + reporterId String @db.Uuid + communityId String @db.Uuid + reportedUserId String? @db.Uuid + postId String? @db.Uuid + commentId String? @db.Uuid + ruleId String? @db.Uuid + customReason String? + status ReportStatus @default(PENDING) + createdAt DateTime @default(now()) + + reporter User @relation("Reporter", fields: [reporterId], references: [id]) + community Community @relation(fields: [communityId], references: [id], onDelete: Cascade) + reportedUser User? @relation("ReportedUser", fields: [reportedUserId], references: [id]) + post Post? @relation(fields: [postId], references: [id]) + comment Comment? @relation(fields: [commentId], references: [id]) + rule CommunityRule? @relation(fields: [ruleId], references: [id]) + + // Note: Enforce CHECK (num_nonnulls(reportedUserId, postId, commentId) = 1) via raw SQL migration +} + +// ----------------------------------------------------------------------------- +// LEVEL 7: MEDIA, MESSAGING, EVENTS & NOTIFICATIONS +// ----------------------------------------------------------------------------- + +model Notification { + id String @id @default(uuid()) @db.Uuid + userId String @db.Uuid + actorId String? @db.Uuid + type NotificationType + postId String? @db.Uuid + commentId String? @db.Uuid + inviteId String? @db.Uuid + isRead Boolean @default(false) + createdAt DateTime @default(now()) + + user User @relation("UserNotification", fields: [userId], references: [id], onDelete: Cascade) + actor User? @relation("NotificationActor", fields: [actorId], references: [id]) + post Post? @relation(fields: [postId], references: [id], onDelete: Cascade) + comment Comment? @relation(fields: [commentId], references: [id], onDelete: Cascade) + invite CommunityInvite? @relation(fields: [inviteId], references: [id], onDelete: Cascade) +} + +model Media { + id String @id @default(uuid()) @db.Uuid + userId String @db.Uuid + postId String? @db.Uuid + commentId String? @db.Uuid + mediaUrl String + type MediaType + uploadedAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + post Post? @relation(fields: [postId], references: [id], onDelete: Cascade) + comment Comment? @relation(fields: [commentId], references: [id], onDelete: Cascade) + + // Note: Enforce CHECK (num_nonnulls(postId, commentId) = 1) via raw SQL migration +} + +model DirectMessage { + id String @id @default(uuid()) @db.Uuid + senderId String @db.Uuid + receiverId String @db.Uuid + content String + deletedBySender Boolean @default(false) + deletedByReceiver Boolean @default(false) + isRead Boolean @default(false) + sentAt DateTime @default(now()) + + sender User @relation("MessageSender", fields: [senderId], references: [id], onDelete: Cascade) + receiver User @relation("MessageReceiver", fields: [receiverId], references: [id], onDelete: Cascade) +} + +model Event { + id String @id @default(uuid()) @db.Uuid + communityId String @db.Uuid + creatorId String @db.Uuid + title String + description String? + startTime DateTime + endTime DateTime + + community Community @relation(fields: [communityId], references: [id], onDelete: Cascade) + creatorMod CommunityModerator @relation(fields: [creatorId, communityId], references: [userId, communityId]) +} \ No newline at end of file From 08f7c5011ed63cbac6dcd981361e9d904a502151 Mon Sep 17 00:00:00 2001 From: Samet Ekin Polat Date: Thu, 2 Apr 2026 22:18:31 +0300 Subject: [PATCH 05/14] fix: #24 this occured because of installation of prisma client, error fixed also I updated next js 16.2.1 to 16.2.2 --- package-lock.json | 80 +++++++++++++++++++++++------------------------ package.json | 4 +-- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2c14682..636f22c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.7.0", - "next": "16.2.1", + "next": "^16.2.2", "next-themes": "^0.4.6", "radix-ui": "^1.4.3", "react": "19.2.4", @@ -1723,9 +1723,9 @@ } }, "node_modules/@next/env": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.1.tgz", - "integrity": "sha512-n8P/HCkIWW+gVal2Z8XqXJ6aB3J0tuM29OcHpCsobWlChH/SITBs1DFBk/HajgrwDkqqBXPbuUuzgDvUekREPg==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.2.tgz", + "integrity": "sha512-LqSGz5+xGk9EL/iBDr2yo/CgNQV6cFsNhRR2xhSXYh7B/hb4nePCxlmDvGEKG30NMHDFf0raqSyOZiQrO7BkHQ==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -1739,9 +1739,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.1.tgz", - "integrity": "sha512-BwZ8w8YTaSEr2HIuXLMLxIdElNMPvY9fLqb20LX9A9OMGtJilhHLbCL3ggyd0TwjmMcTxi0XXt+ur1vWUoxj2Q==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.2.tgz", + "integrity": "sha512-B92G3ulrwmkDSEJEp9+XzGLex5wC1knrmCSIylyVeiAtCIfvEJYiN3v5kXPlYt5R4RFlsfO/v++aKV63Acrugg==", "cpu": [ "arm64" ], @@ -1755,9 +1755,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.1.tgz", - "integrity": "sha512-/vrcE6iQSJq3uL3VGVHiXeaKbn8Es10DGTGRJnRZlkNQQk3kaNtAJg8Y6xuAlrx/6INKVjkfi5rY0iEXorZ6uA==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.2.tgz", + "integrity": "sha512-7ZwSgNKJNQiwW0CKhNm9B1WS2L1Olc4B2XY0hPYCAL3epFnugMhuw5TMWzMilQ3QCZcCHoYm9NGWTHbr5REFxw==", "cpu": [ "x64" ], @@ -1771,9 +1771,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.1.tgz", - "integrity": "sha512-uLn+0BK+C31LTVbQ/QU+UaVrV0rRSJQ8RfniQAHPghDdgE+SlroYqcmFnO5iNjNfVWCyKZHYrs3Nl0mUzWxbBw==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.2.tgz", + "integrity": "sha512-c3m8kBHMziMgo2fICOP/cd/5YlrxDU5YYjAJeQLyFsCqVF8xjOTH/QYG4a2u48CvvZZSj1eHQfBCbyh7kBr30Q==", "cpu": [ "arm64" ], @@ -1790,9 +1790,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.1.tgz", - "integrity": "sha512-ssKq6iMRnHdnycGp9hCuGnXJZ0YPr4/wNwrfE5DbmvEcgl9+yv97/Kq3TPVDfYome1SW5geciLB9aiEqKXQjlQ==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.2.tgz", + "integrity": "sha512-VKLuscm0P/mIfzt+SDdn2+8TNNJ7f0qfEkA+az7OqQbjzKdBxAHs0UvuiVoCtbwX+dqMEL9U54b5wQ/aN3dHeg==", "cpu": [ "arm64" ], @@ -1809,9 +1809,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.1.tgz", - "integrity": "sha512-HQm7SrHRELJ30T1TSmT706IWovFFSRGxfgUkyWJZF/RKBMdbdRWJuFrcpDdE5vy9UXjFOx6L3mRdqH04Mmx0hg==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.2.tgz", + "integrity": "sha512-kU3OPHJq6sBUjOk7wc5zJ7/lipn8yGldMoAv4z67j6ov6Xo/JvzA7L7LCsyzzsXmgLEhk3Qkpwqaq/1+XpNR3g==", "cpu": [ "x64" ], @@ -1828,9 +1828,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.1.tgz", - "integrity": "sha512-aV2iUaC/5HGEpbBkE+4B8aHIudoOy5DYekAKOMSHoIYQ66y/wIVeaRx8MS2ZMdxe/HIXlMho4ubdZs/J8441Tg==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.2.tgz", + "integrity": "sha512-CKXRILyErMtUftp+coGcZ38ZwE/Aqq45VMCcRLr2I4OXKrgxIBDXHnBgeX/UMil0S09i2JXaDL3Q+TN8D/cKmg==", "cpu": [ "x64" ], @@ -1847,9 +1847,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.1.tgz", - "integrity": "sha512-IXdNgiDHaSk0ZUJ+xp0OQTdTgnpx1RCfRTalhn3cjOP+IddTMINwA7DXZrwTmGDO8SUr5q2hdP/du4DcrB1GxA==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.2.tgz", + "integrity": "sha512-sS/jSk5VUoShUqINJFvNjVT7JfR5ORYj/+/ZpOYbbIohv/lQfduWnGAycq2wlknbOql2xOR0DoV0s6Xfcy49+g==", "cpu": [ "arm64" ], @@ -1863,9 +1863,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.1.tgz", - "integrity": "sha512-qvU+3a39Hay+ieIztkGSbF7+mccbbg1Tk25hc4JDylf8IHjYmY/Zm64Qq1602yPyQqvie+vf5T/uPwNxDNIoeg==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.2.tgz", + "integrity": "sha512-aHaKceJgdySReT7qeck5oShucxWRiiEuwCGK8HHALe6yZga8uyFpLkPgaRw3kkF04U7ROogL/suYCNt/+CuXGA==", "cpu": [ "x64" ], @@ -9310,12 +9310,12 @@ } }, "node_modules/next": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/next/-/next-16.2.1.tgz", - "integrity": "sha512-VaChzNL7o9rbfdt60HUj8tev4m6d7iC1igAy157526+cJlXOQu5LzsBXNT+xaJnTP/k+utSX5vMv7m0G+zKH+Q==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.2.tgz", + "integrity": "sha512-i6AJdyVa4oQjyvX/6GeER8dpY/xlIV+4NMv/svykcLtURJSy/WzDnnUk/TM4d0uewFHK7xSQz4TbIwPgjky+3A==", "license": "MIT", "dependencies": { - "@next/env": "16.2.1", + "@next/env": "16.2.2", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -9329,14 +9329,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.2.1", - "@next/swc-darwin-x64": "16.2.1", - "@next/swc-linux-arm64-gnu": "16.2.1", - "@next/swc-linux-arm64-musl": "16.2.1", - "@next/swc-linux-x64-gnu": "16.2.1", - "@next/swc-linux-x64-musl": "16.2.1", - "@next/swc-win32-arm64-msvc": "16.2.1", - "@next/swc-win32-x64-msvc": "16.2.1", + "@next/swc-darwin-arm64": "16.2.2", + "@next/swc-darwin-x64": "16.2.2", + "@next/swc-linux-arm64-gnu": "16.2.2", + "@next/swc-linux-arm64-musl": "16.2.2", + "@next/swc-linux-x64-gnu": "16.2.2", + "@next/swc-linux-x64-musl": "16.2.2", + "@next/swc-win32-arm64-msvc": "16.2.2", + "@next/swc-win32-x64-msvc": "16.2.2", "sharp": "^0.34.5" }, "peerDependencies": { diff --git a/package.json b/package.json index 7e8f9cb..4a276f3 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.7.0", - "next": "16.2.1", + "next": "^16.2.2", "next-themes": "^0.4.6", "radix-ui": "^1.4.3", "react": "19.2.4", @@ -42,7 +42,7 @@ "keywords": [], "author": "", "license": "ISC", - "type": "commonjs", + "type": "module", "bugs": { "url": "https://github.com/sametekinpolat/SociArel/issues" }, From a25b80b2bf3a0cab714a0b15426c7a2a63a284e0 Mon Sep 17 00:00:00 2001 From: Samet Ekin Polat Date: Fri, 3 Apr 2026 18:05:21 +0300 Subject: [PATCH 06/14] #26 implement email verification functionality with rate limiting --- src/app/(auth)/verify-email/page.tsx | 104 +++++++++++++++++ src/app/(auth)/verify/page.tsx | 86 ++++++++++++++ src/lib/mail.ts | 36 ++++++ src/lib/rate-limit.ts | 169 +++++++++++++++++++++++++++ 4 files changed, 395 insertions(+) create mode 100644 src/app/(auth)/verify-email/page.tsx create mode 100644 src/app/(auth)/verify/page.tsx create mode 100644 src/lib/mail.ts create mode 100644 src/lib/rate-limit.ts diff --git a/src/app/(auth)/verify-email/page.tsx b/src/app/(auth)/verify-email/page.tsx new file mode 100644 index 0000000..284f930 --- /dev/null +++ b/src/app/(auth)/verify-email/page.tsx @@ -0,0 +1,104 @@ +"use client"; + +import { Suspense, useState, useEffect } from "react"; +import { useSearchParams } from "next/navigation"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Mail, RefreshCw, ArrowLeft } from "lucide-react"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { resendVerificationEmailAction } from "@/actions/auth"; +import { CheckCircle2, XCircle } from "lucide-react"; + +function VerifyEmailInner() { + const searchParams = useSearchParams(); + const email = searchParams.get("email"); + + const [timeRemaining, setTimeRemaining] = useState(0); + const [resending, setResending] = useState(false); + const [message, setMessage] = useState<{ type: "success" | "error", text: string } | null>(null); + + useEffect(() => { + let timer: NodeJS.Timeout; + if (timeRemaining > 0) { + timer = setTimeout(() => { + setTimeRemaining((prev) => prev - 1); + }, 1000); + } + return () => clearTimeout(timer); + }, [timeRemaining]); + + const handleResend = async () => { + if (!email || timeRemaining > 0) return; + + setResending(true); + setMessage(null); + + const res = await resendVerificationEmailAction(email); + if (res?.error) { + setMessage({ type: "error", text: res.error }); + setResending(false); + } else { + setMessage({ type: "success", text: "A new verification link has been sent to your email." }); + setTimeRemaining(60); // 60 seconds cooldown + setResending(false); + } + }; + + return ( +
+ + +
+ +
+ + Check your email + + + We've sent a verification link to {email ? {email} : "your email address"}. Please click the link to verify your account so you can log in. + +
+ + + + {message && ( +
+ {message.type === "success" ? : } + {message.text} +
+ )} + + + + + + +
+
+
+ ); +} + +export default function VerifyEmailNotice() { + return ( + }> + + + ); +} diff --git a/src/app/(auth)/verify/page.tsx b/src/app/(auth)/verify/page.tsx new file mode 100644 index 0000000..edd63e5 --- /dev/null +++ b/src/app/(auth)/verify/page.tsx @@ -0,0 +1,86 @@ +import { prisma } from "@/lib/prisma"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { CheckCircle, XCircle } from "lucide-react"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; + +export default async function VerifyPage(props: { + searchParams: Promise<{ token?: string }>; +}) { + const searchParams = await props.searchParams; + const token = searchParams.token; + + if (!token) { + return ; + } + + const existingToken = await prisma.verificationToken.findFirst({ + where: { token }, + }); + + if (!existingToken) { + return ; + } + + const hasExpired = new Date(existingToken.expires).getTime() < new Date().getTime(); + if (hasExpired) { + return ; + } + + // Find user and verify + const user = await prisma.user.findFirst({ + where: { email: existingToken.identifier }, + }); + + if (!user) { + return ; + } + + // Update user inside a transaction and clean up + await prisma.$transaction([ + prisma.user.update({ + where: { id: user.id }, + data: { emailVerified: new Date() }, + }), + prisma.verificationToken.delete({ + where: { token }, + }), + ]); + + return ( + + ); +} + +function StatusCard({ error, success }: { error?: string; success?: string }) { + return ( +
+ + +
+ {success ? : } +
+ + {success ? "Verification Complete" : "Verification Failed"} + + + {success || error} + +
+ + + + + +
+
+ ); +} diff --git a/src/lib/mail.ts b/src/lib/mail.ts new file mode 100644 index 0000000..7e9e2ed --- /dev/null +++ b/src/lib/mail.ts @@ -0,0 +1,36 @@ +import nodemailer from "nodemailer"; + +export const sendVerificationEmail = async (email: string, token: string) => { + const confirmLink = `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/verify?token=${token}`; + + // Log link to terminal for easy local testing + console.log(`\n\n-------------------------`); + console.log(` Mock Verification Email dispatched to ${email}`); + console.log(` Verification Link: ${confirmLink}`); + console.log(`-------------------------\n\n`); + + try { + const transporter = nodemailer.createTransport({ + host: process.env.SMTP_HOST || "", + port: Number(process.env.SMTP_PORT) || 587, + secure: false, + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASSWORD, + }, + }); + + await transporter.sendMail({ + from: process.env.SMTP_FROM_EMAIL || '"Arel Social" ', + to: email, + subject: "Verify your Email - Arel Social", + html: ` +

Welcome to Arel Social!

+

Please click the link below to verify your email and activate your account.

+

Verify my email

+ `, + }); + } catch (err) { + console.error("Failed to send verification email. (Did you configure SMTP in .env?)", err); + } +}; diff --git a/src/lib/rate-limit.ts b/src/lib/rate-limit.ts new file mode 100644 index 0000000..5211349 --- /dev/null +++ b/src/lib/rate-limit.ts @@ -0,0 +1,169 @@ +import { RateLimiterRedis } from 'rate-limiter-flexible'; +import Redis, { RedisOptions } from 'ioredis'; + +function buildRedisOptions(): RedisOptions { + const commonOptions: RedisOptions = { + enableOfflineQueue: false, + maxRetriesPerRequest: 1, + }; + + const redisUrl = process.env.REDIS_URL; + if (!redisUrl) { + return { + ...commonOptions, + host: '127.0.0.1', + port: 6379, + }; + } + + try { + const parsed = new URL(redisUrl); + const dbFromPath = parsed.pathname && parsed.pathname !== '/' + ? Number(parsed.pathname.slice(1)) + : undefined; + + return { + ...commonOptions, + host: parsed.hostname || '127.0.0.1', + port: parsed.port ? Number(parsed.port) : 6379, + username: parsed.username || undefined, + password: parsed.password || undefined, + db: Number.isNaN(dbFromPath) ? undefined : dbFromPath, + ...(parsed.protocol === 'rediss:' ? { tls: {} } : {}), + }; + } catch { + return { + ...commonOptions, + host: '127.0.0.1', + port: 6379, + }; + } +} + +const redisClient = new Redis(buildRedisOptions()); +let redisReady = false; +let loggedRedisUnavailable = false; + +function getErrorMessage(reason: unknown) { + if (reason instanceof Error) return reason.message; + return String(reason); +} + +function logRedisUnavailableOnce(reason?: unknown) { + if (!loggedRedisUnavailable) { + const suffix = reason ? ` Reason: ${getErrorMessage(reason)}` : ''; + console.warn(`WARNING: Rate limiting skipped because local Redis is offline or unreachable.${suffix}`); + loggedRedisUnavailable = true; + } +} + +redisClient.on('error', () => { + redisReady = false; +}); + +redisClient.on('ready', () => { + redisReady = true; + loggedRedisUnavailable = false; +}); + +redisClient.on('close', () => { + redisReady = false; +}); + +async function ensureRedisReady() { + if (redisReady) return true; + + try { + if (redisClient.status === 'wait') { + await redisClient.connect(); + } + + if (!redisReady) { + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + cleanup(); + reject(new Error('Redis ready timeout')); + }, 1500); + + const onReady = () => { + redisReady = true; + cleanup(); + resolve(); + }; + + const onError = (error: Error) => { + cleanup(); + reject(error); + }; + + const cleanup = () => { + clearTimeout(timeout); + redisClient.off('ready', onReady); + redisClient.off('error', onError); + }; + + redisClient.once('ready', onReady); + redisClient.once('error', onError); + }); + } + + await redisClient.ping(); + redisReady = true; + loggedRedisUnavailable = false; + return true; + } catch (error) { + logRedisUnavailableOnce(error); + return false; + } +} + +// Allow 5 login attempts per 1 minute +export const loginRateLimit = new RateLimiterRedis({ + storeClient: redisClient, + keyPrefix: 'login', + points: 5, + duration: 60, + inMemoryBlockOnConsumed: 6, +}); + +// Allow 3 registration attempts per 10 minutes +export const registerRateLimit = new RateLimiterRedis({ + storeClient: redisClient, + keyPrefix: 'register', + points: 3, + duration: 600, // 10 minutes + inMemoryBlockOnConsumed: 4, +}); + +// Allow 1 resend verification email per 5 minutes +export const emailRateLimit = new RateLimiterRedis({ + storeClient: redisClient, + keyPrefix: 'email_resend', + points: 1, + duration: 300, +}); + +// Helper function to handle rate limits +export async function checkRateLimit( + limiter: RateLimiterRedis, + identifier: string +) { + const isRedisAvailable = await ensureRedisReady(); + if (!isRedisAvailable) { + return { success: true }; + } + + try { + await limiter.consume(identifier, 1); + return { success: true }; + } catch (rejection) { + + if (rejection instanceof Error) { + logRedisUnavailableOnce(); + return { success: true }; + } + // Rate limit hit + return { success: false }; + } +} + From 2f42dd59e7eeb81f257fab5c9463345c473c33b1 Mon Sep 17 00:00:00 2001 From: Samet Ekin Polat Date: Fri, 3 Apr 2026 18:06:51 +0300 Subject: [PATCH 07/14] #27 add onboarding page for user registration with username and email input --- src/app/(auth)/onboarding/page.tsx | 167 +++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 src/app/(auth)/onboarding/page.tsx diff --git a/src/app/(auth)/onboarding/page.tsx b/src/app/(auth)/onboarding/page.tsx new file mode 100644 index 0000000..bada211 --- /dev/null +++ b/src/app/(auth)/onboarding/page.tsx @@ -0,0 +1,167 @@ +"use client"; + +import { useTransition, useState, useEffect, useRef } from "react"; +import { useRouter } from "next/navigation"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useSession, signOut } from "next-auth/react"; +import { completeOnboardingAction, checkUsernameAvailability } from "@/actions/auth"; +import { Loader2, XCircle } from "lucide-react"; + +export default function OnboardingPage() { + const router = useRouter(); + const { data: session, status } = useSession(); + const [isPending, startTransition] = useTransition(); + const [error, setError] = useState(null); + + const [username, setUsername] = useState(""); + const [isCheckingUsername, setIsCheckingUsername] = useState(false); + const [usernameAvailable, setUsernameAvailable] = useState(null); + const usernameCheckTimeoutRef = useRef | null>(null); + + useEffect(() => { + if (status === "unauthenticated") { + router.replace("/login"); + } + }, [status, router]); + + useEffect(() => { + return () => { + if (usernameCheckTimeoutRef.current) { + clearTimeout(usernameCheckTimeoutRef.current); + } + }; + }, []); + + const handleUsernameChange = (value: string) => { + setUsername(value); + + if (usernameCheckTimeoutRef.current) { + clearTimeout(usernameCheckTimeoutRef.current); + } + + if (value.length < 3) { + setUsernameAvailable(null); + setIsCheckingUsername(false); + return; + } + + setIsCheckingUsername(true); + usernameCheckTimeoutRef.current = setTimeout(async () => { + const isAvailable = await checkUsernameAvailability(value); + setUsernameAvailable(isAvailable); + setIsCheckingUsername(false); + }, 500); + }; + + if (status === "loading") { + return
; + } + + if (status === "unauthenticated" || !session?.user) { + return null; + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + if (!session?.user) return; + + const formData = new FormData(e.currentTarget); + formData.append("userId", session.user.id!); + + if (usernameAvailable === false) { + setError("Please choose an available username."); + return; + } + + startTransition(async () => { + const result = await completeOnboardingAction(formData); + if (result?.error) { + setError(result.error); + } else if (result?.success && result.email) { + // Automatically redirects them out + await signOut({ callbackUrl: `/verify-email?email=${encodeURIComponent(result.email)}` }); + } + }); + }; + + return ( +
+ + + + Welcome! Let's finish setting up. + + + Please provide your preferred username and an official university email address to complete your profile. + + + + +
+ {error && ( +
+ + {error} +
+ )} + +
+ +
+ handleUsernameChange(e.target.value)} + /> + {isCheckingUsername && ( +
+ +
+ )} +
+ + {!isCheckingUsername && usernameAvailable === true && ( +

This username is free you can take

+ )} + {!isCheckingUsername && usernameAvailable === false && ( +

This username is taken

+ )} +
+ +
+ + +
+ + +
+
+
+
+ ); +} From 801ae9034e2f5034e1f0ac46c1e9a795dd92a132 Mon Sep 17 00:00:00 2001 From: Samet Ekin Polat Date: Fri, 3 Apr 2026 18:07:59 +0300 Subject: [PATCH 08/14] #12 feat(auth): implement authentication flow with login, registration, and email verification - Added login and registration pages with form handling and validation. - Integrated GitHub OAuth and credentials provider for authentication. - Implemented session management and user onboarding flow. - Created API routes for authentication and session handling. - Added Prisma adapter for database interactions and user management. - Introduced rate limiting for login and registration actions. - Implemented email verification token generation and sending. - Added validation schemas for login and registration forms using Zod. - Updated layout to include session provider for managing user sessions. --- package-lock.json | 678 +++++++++++++++++++++--- package.json | 18 +- src/actions/auth.ts | 213 ++++++++ src/app/(auth)/login/page.tsx | 145 +++++ src/app/(auth)/register/page.tsx | 318 +++++++++++ src/app/api/auth/[...nextauth]/route.ts | 3 + src/app/layout.tsx | 23 +- src/app/login/page.tsx | 69 --- src/app/page.tsx | 46 +- src/auth.config.ts | 50 ++ src/auth.ts | 78 +++ src/components/session-provider.tsx | 7 + src/lib/prisma.ts | 13 + src/lib/tokens.ts | 28 + src/lib/validations/auth.ts | 27 + src/proxy.ts | 9 + src/types/next-auth.d.ts | 22 + 17 files changed, 1577 insertions(+), 170 deletions(-) create mode 100644 src/actions/auth.ts create mode 100644 src/app/(auth)/login/page.tsx create mode 100644 src/app/(auth)/register/page.tsx create mode 100644 src/app/api/auth/[...nextauth]/route.ts delete mode 100644 src/app/login/page.tsx create mode 100644 src/auth.config.ts create mode 100644 src/auth.ts create mode 100644 src/components/session-provider.tsx create mode 100644 src/lib/prisma.ts create mode 100644 src/lib/tokens.ts create mode 100644 src/lib/validations/auth.ts create mode 100644 src/proxy.ts create mode 100644 src/types/next-auth.d.ts diff --git a/package-lock.json b/package-lock.json index 636f22c..b7bbf8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,22 +9,35 @@ "version": "0.1.0", "license": "ISC", "dependencies": { + "@auth/prisma-adapter": "^2.11.1", + "@prisma/adapter-pg": "^7.6.0", "@prisma/client": "^7.6.0", + "bcryptjs": "^3.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "ioredis": "^5.10.1", "lucide-react": "^1.7.0", "next": "^16.2.2", + "next-auth": "^5.0.0-beta.30", "next-themes": "^0.4.6", + "nodemailer": "^8.0.4", + "pg": "^8.20.0", "radix-ui": "^1.4.3", + "rate-limiter-flexible": "^10.0.1", "react": "19.2.4", "react-dom": "19.2.4", "shadcn": "^4.1.1", "tailwind-merge": "^3.5.0", - "tw-animate-css": "^1.4.0" + "tw-animate-css": "^1.4.0", + "zod": "^4.3.6" }, "devDependencies": { + "@playwright/test": "^1.59.1", "@tailwindcss/postcss": "^4", + "@types/bcryptjs": "^2.4.6", "@types/node": "^20", + "@types/nodemailer": "^7.0.11", + "@types/pg": "^8.20.0", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", @@ -47,6 +60,76 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@auth/core": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.41.0.tgz", + "integrity": "sha512-Wd7mHPQ/8zy6Qj7f4T46vg3aoor8fskJm6g2Zyj064oQ3+p0xNZXAV60ww0hY+MbTesfu29kK14Zk5d5JTazXQ==", + "license": "ISC", + "dependencies": { + "@panva/hkdf": "^1.2.1", + "jose": "^6.0.6", + "oauth4webapi": "^3.3.0", + "preact": "10.24.3", + "preact-render-to-string": "6.5.11" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^6.8.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/@auth/prisma-adapter": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@auth/prisma-adapter/-/prisma-adapter-2.11.1.tgz", + "integrity": "sha512-Ke7DXP0Fy0Mlmjz/ZJLXwQash2UkA4621xCM0rMtEczr1kppLc/njCbUkHkIQ/PnmILjqSPEKeTjDPsYruvkug==", + "license": "ISC", + "dependencies": { + "@auth/core": "0.41.1" + }, + "peerDependencies": { + "@prisma/client": ">=2.26.0 || >=3 || >=4 || >=5 || >=6" + } + }, + "node_modules/@auth/prisma-adapter/node_modules/@auth/core": { + "version": "0.41.1", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.41.1.tgz", + "integrity": "sha512-t9cJ2zNYAdWMacGRMT6+r4xr1uybIdmYa49calBPeTqwgAFPV/88ac9TEvCR85pvATiSPt8VaNf+Gt24JIT/uw==", + "license": "ISC", + "dependencies": { + "@panva/hkdf": "^1.2.1", + "jose": "^6.0.6", + "oauth4webapi": "^3.3.0", + "preact": "10.24.3", + "preact-render-to-string": "6.5.11" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^7.0.7" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -456,7 +539,7 @@ "version": "0.5.0", "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.5.0.tgz", "integrity": "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "picocolors": "^1.0.0", @@ -467,7 +550,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.11.0.tgz", "integrity": "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@clack/core": "0.5.0", @@ -676,14 +759,14 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz", "integrity": "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==", - "devOptional": true, + "dev": true, "license": "Apache-2.0" }, "node_modules/@electric-sql/pglite-socket": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.1.tgz", "integrity": "sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "pglite-server": "dist/scripts/server.js" @@ -696,7 +779,7 @@ "version": "0.3.1", "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.3.1.tgz", "integrity": "sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "peerDependencies": { "@electric-sql/pglite": "0.4.1" @@ -1578,6 +1661,12 @@ } } }, + "node_modules/@ioredis/commands": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", + "license": "MIT" + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1627,7 +1716,7 @@ "version": "0.3.4", "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@modelcontextprotocol/sdk": { @@ -1984,6 +2073,43 @@ "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", "license": "MIT" }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@prisma/adapter-pg": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.6.0.tgz", + "integrity": "sha512-BjHNmJqqa42NqJSDPnXUfwUofWo8LJY7Ui2gqxN4DmAOb+H/gGKv+hln2Xq/1kSJXPW5AXMXuNiPDMpywvyIOw==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/driver-adapter-utils": "7.6.0", + "@types/pg": "^8.16.0", + "pg": "^8.16.3", + "postgres-array": "3.0.4" + } + }, "node_modules/@prisma/client": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.6.0.tgz", @@ -2018,7 +2144,7 @@ "version": "7.6.0", "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.6.0.tgz", "integrity": "sha512-MuAz1MK4PeG5/03YzfzX3CnFVHQ6qePGwUpQRzPzX5tT0ffJ3Tzi9zJZbBc+VzEGFCM8ghW/gTVDR85Syjt+Yw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "c12": "3.1.0", @@ -2031,14 +2157,13 @@ "version": "7.6.0", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.6.0.tgz", "integrity": "sha512-LpHr3qos4lQZ6sxwjStf59YBht7m9/QF7NSQsMH6qGENWZu2w3UkQUGn1h5iRkDjnWRj3VHykOu9qFhps4ADvA==", - "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/dev": { "version": "0.24.3", "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.3.tgz", "integrity": "sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "@electric-sql/pglite": "0.4.1", @@ -2064,7 +2189,7 @@ "version": "1.19.11", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=18.14.1" @@ -2073,11 +2198,20 @@ "hono": "^4" } }, + "node_modules/@prisma/driver-adapter-utils": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.6.0.tgz", + "integrity": "sha512-D8j3p0RnhLuufMaRLX6QqtGgPC5Ao3l5oFP6Q5AL0rTHi4vna+NzGEipwCsfvcSvaGFCbsH3lsTMbb4WvY+ovA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.6.0" + } + }, "node_modules/@prisma/engines": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.6.0.tgz", "integrity": "sha512-Sn5edRzhHqgRV2M+A0eIbY442B4mReWWf3pKs/LKreYgW7oa/up8JtK/s4iv/EQA097cyboZ08mmkpbLp+tZ3w==", - "devOptional": true, + "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -2091,14 +2225,14 @@ "version": "7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711.tgz", "integrity": "sha512-r51DLcJ8bDRSrBEJF3J4cinoWyGA7rfP2mG6lD90VqIbGNOkbfcLcXalSVjq5Y6brQS3vcjrq4GbyUb1Cb7vkw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0" }, "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.6.0.tgz", "integrity": "sha512-ohZDwXvtmnbzOcutR2D13lDWpZP1wQjmPyztmt0AwXLzQI7q95EE7NYCvS+M6N6SivT+BM0NOqLmTH3wms4L3A==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "7.6.0" @@ -2108,7 +2242,7 @@ "version": "7.6.0", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.6.0.tgz", "integrity": "sha512-N575Ni95c3FkduWY/eKTHqNYgNbceZ1tQaSknVtJjpKmiiBXmniESn/GTxsDvICC4ZeiNrXxioGInzQrCdx16w==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "7.6.0", @@ -2120,7 +2254,7 @@ "version": "7.6.0", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.6.0.tgz", "integrity": "sha512-ohZDwXvtmnbzOcutR2D13lDWpZP1wQjmPyztmt0AwXLzQI7q95EE7NYCvS+M6N6SivT+BM0NOqLmTH3wms4L3A==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "7.6.0" @@ -2130,7 +2264,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "7.2.0" @@ -2140,21 +2274,21 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0" }, "node_modules/@prisma/query-plan-executor": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz", "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==", - "devOptional": true, + "dev": true, "license": "Apache-2.0" }, "node_modules/@prisma/streams-local": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@prisma/streams-local/-/streams-local-0.1.2.tgz", "integrity": "sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "ajv": "^8.12.0", @@ -2171,7 +2305,7 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -2188,7 +2322,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" @@ -2201,14 +2335,14 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@prisma/studio-core": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.27.3.tgz", "integrity": "sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "@radix-ui/react-toggle": "1.1.10", @@ -3751,7 +3885,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@swc/helpers": { @@ -4132,6 +4266,13 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -4157,17 +4298,37 @@ "version": "20.19.37", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", - "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/nodemailer": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.11.tgz", + "integrity": "sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -4177,7 +4338,7 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -5148,7 +5309,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 6.0.0" @@ -5193,11 +5354,20 @@ "node": ">=6.0.0" } }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/better-result": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/better-result/-/better-result-2.7.0.tgz", "integrity": "sha512-7zrmXjAK8u8Z6SOe4R65XObOR5X+Y2I/VVku3t5cPOGQ8/WsBcfFmfnIPiEl5EBMDOzPHRwbiPbMtQBKYdw7RA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@clack/prompts": "^0.11.0" @@ -5314,7 +5484,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "chokidar": "^4.0.3", @@ -5343,7 +5513,7 @@ "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "devOptional": true, + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -5450,7 +5620,7 @@ "version": "4.5.1", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@kurkle/color": "^0.3.0" @@ -5463,7 +5633,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "readdirp": "^4.0.1" @@ -5479,7 +5649,7 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "consola": "^3.2.3" @@ -5620,6 +5790,15 @@ "node": ">=6" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/code-block-writer": { "version": "13.0.3", "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", @@ -5664,14 +5843,14 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/consola": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": "^14.18.0 || >=16.10.0" @@ -5796,7 +5975,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -5920,7 +6099,7 @@ "version": "7.1.5", "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=16.0.0" @@ -6006,14 +6185,13 @@ "version": "6.1.4", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/denque": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=0.10" @@ -6032,7 +6210,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/detect-libc": { @@ -6126,7 +6304,7 @@ "version": "3.20.0", "resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz", "integrity": "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -6150,7 +6328,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -6964,14 +7142,14 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/fast-check": { "version": "3.23.2", "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", - "devOptional": true, + "dev": true, "funding": [ { "type": "individual", @@ -7207,7 +7385,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -7264,6 +7442,21 @@ "node": ">=14.14" } }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -7314,7 +7507,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "is-property": "^1.0.2" @@ -7409,7 +7602,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz", "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/get-proto": { @@ -7476,7 +7669,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "citty": "^0.1.6", @@ -7555,14 +7748,14 @@ "version": "3.1.12", "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz", "integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/graphmatch": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz", "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/graphql": { @@ -7722,7 +7915,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/https-proxy-agent": { @@ -7819,6 +8012,30 @@ "node": ">= 0.4" } }, + "node_modules/ioredis": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", + "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.5.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/ip-address": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", @@ -8216,7 +8433,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/is-regex": { @@ -8450,7 +8667,7 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -8916,6 +9133,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -8967,7 +9196,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "devOptional": true, + "dev": true, "license": "Apache-2.0" }, "node_modules/loose-envify": { @@ -8996,7 +9225,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "bun": ">=1.0.0", @@ -9229,7 +9458,7 @@ "version": "3.15.3", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "aws-ssl-profiles": "^1.1.1", @@ -9250,7 +9479,7 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "lru.min": "^1.1.0" @@ -9362,6 +9591,33 @@ } } }, + "node_modules/next-auth": { + "version": "5.0.0-beta.30", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.30.tgz", + "integrity": "sha512-+c51gquM3F6nMVmoAusRJ7RIoY0K4Ts9HCCwyy/BRoe4mp3msZpOzYMyb5LAYc1wSo74PMQkGDcaghIO7W6Xjg==", + "license": "ISC", + "dependencies": { + "@auth/core": "0.41.0" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "next": "^14.0.0-0 || ^15.0.0 || ^16.0.0", + "nodemailer": "^7.0.7", + "react": "^18.2.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, "node_modules/next-themes": { "version": "0.4.6", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", @@ -9461,7 +9717,7 @@ "version": "1.6.7", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/node-releases": { @@ -9470,6 +9726,15 @@ "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", "license": "MIT" }, + "node_modules/nodemailer": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.4.tgz", + "integrity": "sha512-k+jf6N8PfQJ0Fe8ZhJlgqU5qJU44Lpvp2yvidH3vp1lPnVQMgi4yEEMPXg5eJS1gFIJTVq1NHBk7Ia9ARdSBdQ==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/npm-run-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", @@ -9502,7 +9767,7 @@ "version": "0.6.5", "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "citty": "^0.2.0", @@ -9520,9 +9785,18 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz", "integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==", - "devOptional": true, + "dev": true, "license": "MIT" }, + "node_modules/oauth4webapi": { + "version": "3.8.5", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.5.tgz", + "integrity": "sha512-A8jmyUckVhRJj5lspguklcl90Ydqk61H3dcU0oLhH3Yv13KpAliKTt5hknpGGPZSSfOwGyraNEFmofDYH+1kSg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -9657,7 +9931,7 @@ "version": "2.0.11", "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/on-finished": { @@ -9918,16 +10192,114 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", - "devOptional": true, + "dev": true, + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", "license": "MIT" }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-types/node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -9959,7 +10331,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "confbox": "^0.2.2", @@ -9967,6 +10339,38 @@ "pathe": "^2.0.3" } }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -10022,7 +10426,7 @@ "version": "3.4.7", "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", - "devOptional": true, + "dev": true, "license": "Unlicense", "engines": { "node": ">=12" @@ -10032,6 +10436,45 @@ "url": "https://github.com/sponsors/porsager" } }, + "node_modules/postgres-array": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", + "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/powershell-utils": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", @@ -10044,6 +10487,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/preact": { + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", + "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", + "license": "MIT", + "peerDependencies": { + "preact": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -10073,7 +10535,7 @@ "version": "7.6.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.6.0.tgz", "integrity": "sha512-OKJIPT81K3+F+AayIkY/Y3mkF2NWoFh7lZApaaqPYy7EHILKdO0VsmGkP+hDKYTySHsFSyLWXm/JgcR1B8fY1Q==", - "devOptional": true, + "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -10141,7 +10603,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -10153,7 +10615,7 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/proxy-addr": { @@ -10183,7 +10645,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "devOptional": true, + "dev": true, "funding": [ { "type": "individual", @@ -10317,6 +10779,12 @@ "node": ">= 0.6" } }, + "node_modules/rate-limiter-flexible": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-10.0.1.tgz", + "integrity": "sha512-3G6GMFz5Oz5nVnDv9gQ1LLMdExR4B1lOjogPIjehtgyxPMIkY09BGyk2eCYt36/OkV/0t12GEt6J6HpTl6RzZg==", + "license": "ISC" + }, "node_modules/raw-body": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", @@ -10336,7 +10804,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "defu": "^6.1.4", @@ -10444,7 +10912,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 14.18.0" @@ -10470,6 +10938,27 @@ "node": ">= 4" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -10518,7 +11007,7 @@ "version": "2.33.4", "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz", "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==", - "devOptional": true, + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/remeda" @@ -10602,7 +11091,7 @@ "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 4" @@ -10791,7 +11280,7 @@ "version": "0.0.5", "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", - "devOptional": true + "dev": true }, "node_modules/serve-static": { "version": "2.2.1", @@ -11150,11 +11639,20 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sqlstring": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -11167,6 +11665,12 @@ "dev": true, "license": "MIT" }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -11180,7 +11684,7 @@ "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/stdin-discarder": { @@ -11519,7 +12023,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -11812,7 +12316,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -11869,7 +12373,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "devOptional": true, "license": "MIT" }, "node_modules/unicorn-magic": { @@ -12048,7 +12551,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "typescript": ">=5" @@ -12277,6 +12780,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -12401,7 +12913,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "grammex": "^3.1.11", diff --git a/package.json b/package.json index 4a276f3..02cfe71 100644 --- a/package.json +++ b/package.json @@ -6,25 +6,39 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "test": "npx playwright test" }, "dependencies": { + "@auth/prisma-adapter": "^2.11.1", + "@prisma/adapter-pg": "^7.6.0", "@prisma/client": "^7.6.0", + "bcryptjs": "^3.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "ioredis": "^5.10.1", "lucide-react": "^1.7.0", "next": "^16.2.2", + "next-auth": "^5.0.0-beta.30", "next-themes": "^0.4.6", + "nodemailer": "^8.0.4", + "pg": "^8.20.0", "radix-ui": "^1.4.3", + "rate-limiter-flexible": "^10.0.1", "react": "19.2.4", "react-dom": "19.2.4", "shadcn": "^4.1.1", "tailwind-merge": "^3.5.0", - "tw-animate-css": "^1.4.0" + "tw-animate-css": "^1.4.0", + "zod": "^4.3.6" }, "devDependencies": { + "@playwright/test": "^1.59.1", "@tailwindcss/postcss": "^4", + "@types/bcryptjs": "^2.4.6", "@types/node": "^20", + "@types/nodemailer": "^7.0.11", + "@types/pg": "^8.20.0", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", diff --git a/src/actions/auth.ts b/src/actions/auth.ts new file mode 100644 index 0000000..30da779 --- /dev/null +++ b/src/actions/auth.ts @@ -0,0 +1,213 @@ +"use server"; + +import { signIn, signOut } from "@/auth"; +import { AuthError } from "next-auth"; +import { prisma } from "@/lib/prisma"; +import bcrypt from "bcryptjs"; +import { generateVerificationToken } from "@/lib/tokens"; +import { sendVerificationEmail } from "@/lib/mail"; +import { LoginSchema, RegisterSchema, OnboardingSchema } from "@/lib/validations/auth"; +import { checkRateLimit, loginRateLimit, registerRateLimit, emailRateLimit } from "@/lib/rate-limit"; +import { headers } from "next/headers"; + +async function getIp() { + const currentHeaders = await headers(); + const forwardedFor = currentHeaders.get("x-forwarded-for"); + const realIp = currentHeaders.get("x-real-ip"); + if (forwardedFor) return forwardedFor.split(",")[0].trim(); + if (realIp) return realIp.trim(); + return "127.0.0.1"; // DEFAULT IP IF LOCAL +} + +export async function loginAction(formData: FormData) { + const ip = await getIp(); + const { success: rlSuccess } = await checkRateLimit(loginRateLimit, ip); + + if (!rlSuccess) { + return { error: "Too many login attempts. Please try again later." }; + } + + const rawData = Object.fromEntries(formData.entries()); + const validated = LoginSchema.safeParse(rawData); + + if (!validated.success) { + return { error: validated.error.issues[0].message }; + } + + const { identifier, password } = validated.data; + + try { + await signIn("credentials", { + identifier, + password, + redirect: false, + }); + return { success: true }; + } catch (error) { + if (error instanceof AuthError) { + if (error.type === "CredentialsSignin" || error.message.includes("CredentialsSignin")) { + return { error: "Invalid credentials" }; + } + if (error.type === "AccessDenied") { + return { error: "Please verify your email to log in.", requiresVerification: true }; + } + return { error: "Something went wrong during login" }; + } + throw error; + } +} + +export async function loginWithGithub() { + await signIn("github"); +} + +export async function logoutAction() { + await signOut(); +} + +export async function checkUsernameAvailability(username: string) { + if (!username || username.trim() === "") return null; + const existing = await prisma.user.findUnique({ + where: { username } + }); + return !existing; +} + +export async function resendVerificationEmailAction(email: string) { + const ip = await getIp(); + const { success: rlSuccess } = await checkRateLimit(emailRateLimit, ip); + + if (!rlSuccess) { + return { error: "Too many emails sent. Please wait before trying again." }; + } + + if (!email) return { error: "No email provided." }; + try { + const existingUser = await prisma.user.findFirst({ where: { email } }); + if (!existingUser) return { error: "User not found." }; + if (existingUser.emailVerified) return { error: "Email is already verified." }; + + const verificationToken = await generateVerificationToken(email); + + // try sending email fail if error is thrown + await sendVerificationEmail(verificationToken.identifier, verificationToken.token); + + return { success: true }; + } catch (err) { + console.error("Resend error:", err); + return { error: "Failed to send the verification email. Please try again or check SMTP config." }; + } +} + +export async function registerAction(formData: FormData) { + const ip = await getIp(); + const { success: rlSuccess } = await checkRateLimit(registerRateLimit, ip); + + if (!rlSuccess) { + return { error: "Too many registrations from this IP. Please try again later." }; + } + + const rawData = Object.fromEntries(formData.entries()); + const validated = RegisterSchema.safeParse(rawData); + + if (!validated.success) { + return { error: "Validation failed: " + validated.error.issues[0].message }; + } + + const { email, username, password } = validated.data; + + try { + const existingUser = await prisma.user.findFirst({ + where: { + OR: [{ email }, { username }] + } + }); + + if (existingUser) { + if (existingUser.email === email) return { error: "Email already in use" }; + if (existingUser.username === username) return { error: "Username already taken" }; + return { error: "User already exists" }; + } + + const passwordHash = await bcrypt.hash(password, 12); + + const user = await prisma.user.create({ + data: { + email, + username, + passwordHash, + } + }); + + + + + const verificationToken = await generateVerificationToken(user.email!); + + try { + await sendVerificationEmail(verificationToken.identifier, verificationToken.token); + return { success: true, emailSent: true, email: user.email }; + } catch { + return { error: "Account created but failed to send verification email. Please try logging in and requesting a new one.", emailSent: false, email: user.email }; + } + + } catch (error) { + console.error("Registration error:", error); + return { error: "An error occurred during registration" }; + } +} + +export async function completeOnboardingAction(formData: FormData) { + const ip = await getIp(); + const { success: rlSuccess } = await checkRateLimit(registerRateLimit, ip); + + if (!rlSuccess) { + return { error: "Too many requests. Please try again later." }; + } + + const rawData = Object.fromEntries(formData.entries()); + const validated = OnboardingSchema.safeParse(rawData); + + if (!validated.success) { + return { error: "Validation failed: " + validated.error.issues[0].message }; + } + + const { email, username, userId } = validated.data; + + try { + const existingUser = await prisma.user.findFirst({ + where: { + OR: [{ email }, { username }], + NOT: { id: userId } + } + }); + + if (existingUser) { + if (existingUser.email === email) return { error: "Email already in use" }; + if (existingUser.username === username) return { error: "Username already taken" }; + } + + await prisma.user.update({ + where: { id: userId }, + data: { + email, + username, + emailVerified: null, + } + }); + + const verificationToken = await generateVerificationToken(email); + + try { + await sendVerificationEmail(verificationToken.identifier, verificationToken.token); + return { success: true, email }; + } catch { + return { error: "Onboarding successful but failed to send verification email. Please try logging in and requesting a new one.", email }; + } + } catch (err) { + console.error("Onboarding error:", err); + return { error: "Something went wrong during onboarding." }; + } +} + + diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx new file mode 100644 index 0000000..6a7d6b0 --- /dev/null +++ b/src/app/(auth)/login/page.tsx @@ -0,0 +1,145 @@ +"use client"; + +import { useTransition, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import Link from "next/link"; +import { loginAction, loginWithGithub } from "@/actions/auth"; +import { XCircle } from "lucide-react"; + +export default function LoginPage() { + const [isPending, startTransition] = useTransition(); + const [error, setError] = useState(null); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + const formData = new FormData(e.currentTarget); + + startTransition(async () => { + try { + const result = await loginAction(formData); + if (result?.requiresVerification) { + window.location.href = `/verify-email?email=${encodeURIComponent(formData.get("identifier") as string)}`; + } else if (result?.error) { + setError(result.error); + } else if (result?.success) { + window.location.href = "/"; + } + } catch { + } + }); + }; + + return ( +
+ + + + Login + + + Enter your username/email and password to access your account. + + + + +
+ {error && ( +
+ + {error} +
+ )} +
+ + +
+ +
+
+ + + Forgot password? + +
+ +
+ +
+ +
+
+ +
+
+ + Or continue with + +
+
+ +
+ +
+
+ + +
+ Don't have an account?{" "} + + Sign up + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/app/(auth)/register/page.tsx b/src/app/(auth)/register/page.tsx new file mode 100644 index 0000000..0decb31 --- /dev/null +++ b/src/app/(auth)/register/page.tsx @@ -0,0 +1,318 @@ +"use client"; + +import { useTransition, useState, useEffect, useRef } from "react"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import Link from "next/link"; +import { registerAction, loginWithGithub, checkUsernameAvailability } from "@/actions/auth"; +import { Loader2, CheckCircle2, XCircle } from "lucide-react"; + +export default function RegisterPage() { + const [isPending, startTransition] = useTransition(); + const [error, setError] = useState(null); + + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [isPasswordFocused, setIsPasswordFocused] = useState(false); + const [isConfirmFocused, setIsConfirmFocused] = useState(false); + + const [isCheckingUsername, setIsCheckingUsername] = useState(false); + const [usernameAvailable, setUsernameAvailable] = useState(null); + const usernameCheckTimeoutRef = useRef | null>(null); + + useEffect(() => { + return () => { + if (usernameCheckTimeoutRef.current) { + clearTimeout(usernameCheckTimeoutRef.current); + } + }; + }, []); + + const handleUsernameChange = (value: string) => { + setUsername(value); + + if (usernameCheckTimeoutRef.current) { + clearTimeout(usernameCheckTimeoutRef.current); + } + + if (value.length < 3) { + setUsernameAvailable(null); + setIsCheckingUsername(false); + return; + } + + setIsCheckingUsername(true); + usernameCheckTimeoutRef.current = setTimeout(async () => { + const isAvailable = await checkUsernameAvailability(value); + setUsernameAvailable(isAvailable); + setIsCheckingUsername(false); + }, 500); + }; + + // Password requirements state + const reqs = { + length: password.length >= 8, + uppercase: /[A-Z]/.test(password), + lowercase: /[a-z]/.test(password), + number: /[0-9]/.test(password), + }; + + const fulfilledCount = Object.values(reqs).filter(Boolean).length; + const totalReqs = Object.keys(reqs).length; + const strengthPercentage = (fulfilledCount / totalReqs) * 100; + + // Determine progress bar color + let progressColor = "bg-destructive"; + if (fulfilledCount === 2) progressColor = "bg-orange-500"; + if (fulfilledCount === 3) progressColor = "bg-yellow-500"; + if (fulfilledCount === 4) progressColor = "bg-green-500"; + + const passwordsMatch = password.length > 0 && password === confirmPassword; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + const formData = new FormData(e.currentTarget); + + if (usernameAvailable === false) { + setError("Please choose an available username before submitting."); + return; + } + + if (fulfilledCount < totalReqs) { + setError("Please fulfill all password requirements."); + return; + } + + if (!passwordsMatch) { + setError("Passwords do not match."); + return; + } + + startTransition(async () => { + try { + const result = await registerAction(formData); + if (result?.error) { + setError(result.error); + } else if (result?.success && result.email) { + window.location.href = `/verify-email?email=${encodeURIComponent(result.email)}`; + } else { + window.location.href = "/verify-email"; + } + } catch { + // Handled next.js redirect from auth.js action wrapper. + } + }); + }; + + return ( +
+ + + + Create an Account + + + Join now. We will send a verification link to your email. + + + + +
+ {error && ( +
+ {error} +
+ )} + +
+ +
+ handleUsernameChange(e.target.value)} + /> + {isCheckingUsername && ( +
+ +
+ )} +
+ + {!isCheckingUsername && usernameAvailable === true && ( +

This username is free you can take

+ )} + {!isCheckingUsername && usernameAvailable === false && ( +

This username is taken

+ )} +
+ +
+ + +
+ +
+ + setPassword(e.target.value)} + onFocus={() => setIsPasswordFocused(true)} + onBlur={() => setIsPasswordFocused(false)} + /> + + {/* password strenght */} +
0) ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0" + }`} + > +
+
+
+
+
+ + {fulfilledCount < totalReqs && ( +
    + {!reqs.length &&
  • • At least 8 characters long
  • } + {!reqs.uppercase &&
  • • One uppercase letter
  • } + {!reqs.lowercase &&
  • • One lowercase letter
  • } + {!reqs.number &&
  • • One number
  • } +
+ )} + {fulfilledCount === totalReqs && ( +
+ Password meets all requirements +
+ )} +
+
+
+
+ +
+ + setConfirmPassword(e.target.value)} + onFocus={() => setIsConfirmFocused(true)} + onBlur={() => setIsConfirmFocused(false)} + /> + + {/* confirm password */} +
0) ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0" + }`} + > +
+
+ {passwordsMatch ? ( +
+ Passwords are matching +
+ ) : confirmPassword.length > 0 ? ( +
+ Passwords do not match +
+ ) : null} +
+
+
+
+ + + + +
+
+ +
+
+ + Or continue with + +
+
+ +
+ +
+ + + +
+ Already have an account?{" "} + + Sign In + +
+
+ +
+ ); +} diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..86c9f3d --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,3 @@ +import { handlers } from "@/auth"; + +export const { GET, POST } = handlers; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 08b7704..3ed7914 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,6 +3,7 @@ import { Geist, Geist_Mono, Inter, IBM_Plex_Sans } from "next/font/google"; import "./globals.css"; import { cn } from "@/lib/utils"; import { ThemeProvider } from "@/components/theme-provider"; +import { SessionProvider } from "@/components/session-provider"; const ibmPlexSansHeading = IBM_Plex_Sans({ subsets: ['latin'], @@ -36,7 +37,7 @@ export default function RootLayout({ - - - {children} - + + + + {children} + + ); diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx deleted file mode 100644 index c400a31..0000000 --- a/src/app/login/page.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import Link from "next/link"; - -export default function LoginPage() { - return ( -
- - - - Login - - - Enter your username/email and password to access your account. - - - - -
- - -
- -
-
- - {/* forgot password route later */} - - Forgot password? - -
- -
-
- - - - -
- Don't have an account?{" "} - - Sign up - -
-
-
-
- ); -} \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx index b9e615b..9c95b31 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -16,6 +16,14 @@ import { SheetTrigger, } from "@/components/ui/sheet"; +import { useSession, signOut } from "next-auth/react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; const SidebarNavLinks = () => (

- Try making your browser window small to test the mobile view. You will see the new mobile Sheet sidebar and the expanding search bar! + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sapien augue. Sed at nunc nec ipsum convallis tincidunt.

))} From ecbe9ba11b36497482dc3e068abe3061a88263a7 Mon Sep 17 00:00:00 2001 From: Samet Ekin Polat Date: Sat, 25 Apr 2026 16:01:24 +0300 Subject: [PATCH 13/14] fix #47: update dependencies and improve email verification handling --- package-lock.json | 20 ++++++++++---------- package.json | 6 +++--- src/actions/auth.ts | 32 +++++++++++++++++++++++++++++++- src/app/(auth)/login/page.tsx | 4 ++-- src/lib/mail.ts | 14 ++++++++++++-- 5 files changed, 58 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index a1f9885..6b160a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,8 +17,8 @@ "clsx": "^2.1.1", "ioredis": "^5.10.1", "lucide-react": "^1.7.0", - "next": "^16.2.2", - "next-auth": "^5.0.0-beta.30", + "next": "16.2.4", + "next-auth": "5.0.0-beta.31", "next-themes": "^0.4.6", "nodemailer": "^7.0.13", "pg": "^8.20.0", @@ -41,7 +41,7 @@ "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", - "eslint-config-next": "16.2.1", + "eslint-config-next": "16.2.4", "prisma": "^7.6.0", "tailwindcss": "^4", "typescript": "^5" @@ -1750,9 +1750,9 @@ "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.1.tgz", - "integrity": "sha512-r0epZGo24eT4g08jJlg2OEryBphXqO8aL18oajoTKLzHJ6jVr6P6FI58DLMug04MwD3j8Fj0YK0slyzneKVyzA==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.4.tgz", + "integrity": "sha512-tOX826JJ96gYK/go18sPUgMq9FK1tqxBFfUCEufJb5XIkWFFmpgU7mahJANKGkHs7F41ir3tReJ3Lv5La0RvhA==", "dev": true, "license": "MIT", "dependencies": { @@ -6483,13 +6483,13 @@ } }, "node_modules/eslint-config-next": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.1.tgz", - "integrity": "sha512-qhabwjQZ1Mk53XzXvmogf8KQ0tG0CQXF0CZ56+2/lVhmObgmaqj7x5A1DSrWdZd3kwI7GTPGUjFne+krRxYmFg==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.4.tgz", + "integrity": "sha512-A6ekXYFj/YQxBPMl45g3e+U8zJo+X2+ZQwcz34pPKjpc/3S4roBA2Rd9xWB4FKuSxhofo1/95WjzmUY+wHrOhg==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "16.2.1", + "@next/eslint-plugin-next": "16.2.4", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", diff --git a/package.json b/package.json index 96e261a..dd04150 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,8 @@ "clsx": "^2.1.1", "ioredis": "^5.10.1", "lucide-react": "^1.7.0", - "next": "^16.2.2", - "next-auth": "^5.0.0-beta.30", + "next": "16.2.4", + "next-auth": "5.0.0-beta.31", "next-themes": "^0.4.6", "nodemailer": "^7.0.13", "pg": "^8.20.0", @@ -42,7 +42,7 @@ "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", - "eslint-config-next": "16.2.1", + "eslint-config-next": "16.2.4", "prisma": "^7.6.0", "tailwindcss": "^4", "typescript": "^5" diff --git a/src/actions/auth.ts b/src/actions/auth.ts index 30da779..2395c6c 100644 --- a/src/actions/auth.ts +++ b/src/actions/auth.ts @@ -35,6 +35,18 @@ export async function loginAction(formData: FormData) { } const { identifier, password } = validated.data; + const existingUser = await prisma.user.findFirst({ + where: { + OR: [ + { email: identifier }, + { username: identifier } + ] + }, + select: { + email: true, + emailVerified: true, + } + }); try { await signIn("credentials", { @@ -49,10 +61,28 @@ export async function loginAction(formData: FormData) { return { error: "Invalid credentials" }; } if (error.type === "AccessDenied") { - return { error: "Please verify your email to log in.", requiresVerification: true }; + return { + error: "Please verify your email to log in.", + requiresVerification: true, + email: existingUser?.email ?? identifier, + }; } return { error: "Something went wrong during login" }; } + + if ( + error instanceof Error && + error.message.includes("AccessDenied") && + existingUser?.email && + !existingUser.emailVerified + ) { + return { + error: "Please verify your email to log in.", + requiresVerification: true, + email: existingUser.email, + }; + } + throw error; } } diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index 6a7d6b0..754f21c 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -29,7 +29,7 @@ export default function LoginPage() { try { const result = await loginAction(formData); if (result?.requiresVerification) { - window.location.href = `/verify-email?email=${encodeURIComponent(formData.get("identifier") as string)}`; + window.location.href = `/verify-email?email=${encodeURIComponent(result.email ?? (formData.get("identifier") as string))}`; } else if (result?.error) { setError(result.error); } else if (result?.success) { @@ -142,4 +142,4 @@ export default function LoginPage() { ); -} \ No newline at end of file +} diff --git a/src/lib/mail.ts b/src/lib/mail.ts index 7e9e2ed..63f8b0b 100644 --- a/src/lib/mail.ts +++ b/src/lib/mail.ts @@ -2,6 +2,12 @@ import nodemailer from "nodemailer"; export const sendVerificationEmail = async (email: string, token: string) => { const confirmLink = `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/verify?token=${token}`; + const hasSmtpConfig = Boolean( + process.env.SMTP_HOST && + process.env.SMTP_PORT && + process.env.SMTP_USER && + process.env.SMTP_PASSWORD + ); // Log link to terminal for easy local testing console.log(`\n\n-------------------------`); @@ -9,10 +15,14 @@ export const sendVerificationEmail = async (email: string, token: string) => { console.log(` Verification Link: ${confirmLink}`); console.log(`-------------------------\n\n`); + if (!hasSmtpConfig) { + return; + } + try { const transporter = nodemailer.createTransport({ - host: process.env.SMTP_HOST || "", - port: Number(process.env.SMTP_PORT) || 587, + host: process.env.SMTP_HOST, + port: Number(process.env.SMTP_PORT), secure: false, auth: { user: process.env.SMTP_USER, From 70b51a43ac8bc27655fea1702f485a8e16935068 Mon Sep 17 00:00:00 2001 From: Samet Ekin Polat Date: Sat, 25 Apr 2026 16:54:55 +0300 Subject: [PATCH 14/14] #22 feat: implement post creation, updating, and deletion actions with validation post cards designd inittially --- src/actions/posts.ts | 204 +++++++++ src/app/page.tsx | 282 ++---------- src/components/home-page-client.tsx | 670 ++++++++++++++++++++++++++++ 3 files changed, 923 insertions(+), 233 deletions(-) create mode 100644 src/actions/posts.ts create mode 100644 src/components/home-page-client.tsx diff --git a/src/actions/posts.ts b/src/actions/posts.ts new file mode 100644 index 0000000..83d2ea0 --- /dev/null +++ b/src/actions/posts.ts @@ -0,0 +1,204 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { z } from "zod"; +import { auth } from "@/auth"; +import { prisma } from "@/lib/prisma"; +import { PostStatus } from "@/generated/prisma/client"; + +const CreatePostSchema = z.object({ + title: z.string().trim().min(3, "Title must be at least 3 characters.").max(120, "Title is too long."), + body: z + .string() + .trim() + .max(2000, "Post content is too long.") + .optional() + .transform((value) => value || ""), +}); + +export type CreatePostState = { + error?: string; + success?: string; +}; + +export type PostActionResult = { + error?: string; + success?: string; +}; + +async function getOrCreateDemoCommunity(userId: string) { + const existingCommunity = await prisma.community.findFirst({ + where: { isUserProfile: false }, + orderBy: { createdAt: "asc" }, + select: { id: true }, + }); + + if (existingCommunity) { + return existingCommunity; + } + + return prisma.community.create({ + data: { + name: "demo-feed", + description: "Temporary community used for initial posting.", + ownerId: userId, + }, + select: { id: true }, + }); +} + +export async function createPostAction( + _prevState: CreatePostState, + formData: FormData +): Promise { + const session = await auth(); + + if (!session?.user?.id) { + return { error: "Please log in before creating a post." }; + } + + const validated = CreatePostSchema.safeParse({ + title: formData.get("title"), + body: formData.get("body"), + }); + + if (!validated.success) { + return { error: validated.error.issues[0]?.message ?? "Post could not be created." }; + } + + const { title, body } = validated.data; + + try { + const community = await getOrCreateDemoCommunity(session.user.id); + + await prisma.$transaction([ + prisma.communityMember.upsert({ + where: { + userId_communityId: { + userId: session.user.id, + communityId: community.id, + }, + }, + update: {}, + create: { + userId: session.user.id, + communityId: community.id, + }, + }), + prisma.post.create({ + data: { + title, + body: body || null, + status: PostStatus.PUBLISHED, + communityId: community.id, + userId: session.user.id, + }, + }), + ]); + + revalidatePath("/"); + + return { success: "Your post is now live in the feed." }; + } catch (error) { + console.error("createPostAction failed", error); + return { error: "Something went wrong while saving the post." }; + } +} + +export async function updatePostAction(formData: FormData): Promise { + const session = await auth(); + + if (!session?.user?.id) { + return { error: "Please log in before editing a post." }; + } + + const postId = String(formData.get("postId") || ""); + const validated = CreatePostSchema.safeParse({ + title: formData.get("title"), + body: formData.get("body"), + }); + + if (!postId) { + return { error: "Post could not be found." }; + } + + if (!validated.success) { + return { error: validated.error.issues[0]?.message ?? "Post could not be updated." }; + } + + const existingPost = await prisma.post.findUnique({ + where: { id: postId }, + select: { userId: true, isDeleted: true }, + }); + + if (!existingPost || existingPost.isDeleted) { + return { error: "Post could not be found." }; + } + + if (existingPost.userId !== session.user.id) { + return { error: "You are not allowed to edit this post." }; + } + + const { title, body } = validated.data; + + try { + await prisma.post.update({ + where: { id: postId }, + data: { + title, + body: body || null, + }, + }); + + revalidatePath("/"); + + return { success: "Post updated." }; + } catch (error) { + console.error("updatePostAction failed", error); + return { error: "Something went wrong while updating the post." }; + } +} + +export async function deletePostAction(formData: FormData): Promise { + const session = await auth(); + + if (!session?.user?.id) { + return { error: "Please log in before deleting a post." }; + } + + const postId = String(formData.get("postId") || ""); + + if (!postId) { + return { error: "Post could not be found." }; + } + + const existingPost = await prisma.post.findUnique({ + where: { id: postId }, + select: { userId: true, isDeleted: true, status: true }, + }); + + if (!existingPost || existingPost.isDeleted) { + return { error: "Post could not be found." }; + } + + if (existingPost.userId !== session.user.id) { + return { error: "You are not allowed to delete this post." }; + } + + try { + await prisma.post.update({ + where: { id: postId }, + data: { + isDeleted: true, + status: PostStatus.REMOVED, + }, + }); + + revalidatePath("/"); + + return { success: "Post deleted." }; + } catch (error) { + console.error("deletePostAction failed", error); + return { error: "Something went wrong while deleting the post." }; + } +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 3dd1bcd..b25d4d8 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,233 +1,49 @@ -"use client"; - -import { useState } from "react"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Menu, Search, User, Home, Hash, Bell, Mail, ArrowLeft } from "lucide-react"; -import Link from "next/link"; -import { ModeToggle } from "@/components/ui/mode-toggle"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { cn } from "@/lib/utils"; -import { - Sheet, - SheetContent, - SheetHeader, - SheetTitle, - SheetTrigger, -} from "@/components/ui/sheet"; - -import { useSession, signOut } from "next-auth/react"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; - -const SidebarNavLinks = () => ( - -); - -export default function HomePage() { - const [isSidebarOpen, setIsSidebarOpen] = useState(true); - const [isMobileSearchActive, setIsMobileSearchActive] = useState(false); - const { data: session, status } = useSession(); - const isLoading = status === "loading"; - - return ( -
- - {/*TOP NAVBAR*/} - {isMobileSearchActive ? ( -
- - - -
- ) : ( -
-
- - - - - - - - - - Menu - - - - - - - - SocialApp -
- -
-
- - -
-
- -
- - - - - {isLoading ? ( - - ) : session?.user ? ( - - - - - -
-

{session.user.username || session.user.name || "User"}

-

{session.user.email}

-
- - signOut()}> - Log out - -
-
- ) : ( - - - - )} -
-
- )} - - {/* --- BOTTOM LAYOUT AREA --- */} -
- -
- - - -
- - -
-
- -
- -
- -
-
- - {Array.from({ length: 10 }).map((_, i) => ( -
-
-
-
User {i + 1}
-
-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sapien augue. Sed at nunc nec ipsum convallis tincidunt. -

-
- ))} - -
-
-
- - {isMobileSearchActive && ( -
-
-

Recent Searches

-
- -
- -
-
-
- )} - -
-
- ); -} \ No newline at end of file +import { PostStatus } from "@/generated/prisma/client"; +import { HomePageClient } from "@/components/home-page-client"; +import { prisma } from "@/lib/prisma"; + +export default async function HomePage() { + const posts = await prisma.post.findMany({ + where: { + status: PostStatus.PUBLISHED, + isDeleted: false, + }, + orderBy: [{ isPinned: "desc" }, { createdAt: "desc" }], + include: { + user: { + select: { + name: true, + username: true, + email: true, + }, + }, + community: { + select: { + name: true, + }, + }, + _count: { + select: { + comments: true, + }, + }, + }, + take: 25, + }); + + const feedPosts = posts.map((post) => ({ + id: post.id, + title: post.title, + body: post.body, + createdAt: post.createdAt.toISOString(), + upvotes: post.upvotes, + downvotes: post.downvotes, + commentCount: post._count.comments, + communityName: post.community.name, + authorName: post.user.name || post.user.username || post.user.email || "Anonymous", + authorHandle: post.user.username || post.user.email?.split("@")[0] || "anonymous", + authorId: post.userId, + })); + + return ; +} diff --git a/src/components/home-page-client.tsx b/src/components/home-page-client.tsx new file mode 100644 index 0000000..f9beccb --- /dev/null +++ b/src/components/home-page-client.tsx @@ -0,0 +1,670 @@ +"use client"; + +import { startTransition, useEffect, useRef, useState, useTransition } from "react"; +import Link from "next/link"; +import { signOut, useSession } from "next-auth/react"; +import { + ArrowLeft, + Bell, + ChevronDown, + Ellipsis, + Hash, + Home, + Mail, + Menu, + MessageSquare, + Search, + User, + X, +} from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { ModeToggle } from "@/components/ui/mode-toggle"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"; +import { + createPostAction, + deletePostAction, + updatePostAction, +} from "@/actions/posts"; +import { cn } from "@/lib/utils"; + +type FeedPost = { + id: string; + title: string; + body: string | null; + createdAt: string; + upvotes: number; + downvotes: number; + commentCount: number; + communityName: string; + authorName: string; + authorHandle: string; + authorId: string; +}; + +const SidebarNavLinks = () => ( + +); + +function formatRelativeDate(dateString: string) { + const date = new Date(dateString); + const diffMs = Date.now() - date.getTime(); + const diffMinutes = Math.max(1, Math.floor(diffMs / (1000 * 60))); + + if (diffMinutes < 60) return `${diffMinutes}m ago`; + + const diffHours = Math.floor(diffMinutes / 60); + if (diffHours < 24) return `${diffHours}h ago`; + + const diffDays = Math.floor(diffHours / 24); + if (diffDays < 7) return `${diffDays}d ago`; + + return new Intl.DateTimeFormat("en", { + day: "numeric", + month: "short", + year: "numeric", + }).format(date); +} + +function PostComposer() { + const { data: session } = useSession(); + const composerRef = useRef(null); + const [isExpanded, setIsExpanded] = useState(false); + const [isMobileComposerOpen, setIsMobileComposerOpen] = useState(false); + const [isMobileViewport, setIsMobileViewport] = useState(false); + const [statusMessage, setStatusMessage] = useState<{ type: "error" | "success"; text: string } | null>(null); + const [title, setTitle] = useState(""); + const [body, setBody] = useState(""); + const [isPending, startPostTransition] = useTransition(); + + useEffect(() => { + const mediaQuery = window.matchMedia("(max-width: 767px)"); + + const syncViewport = (event?: MediaQueryListEvent) => { + const matches = event?.matches ?? mediaQuery.matches; + setIsMobileViewport(matches); + if (matches) { + setIsExpanded(false); + } else { + setIsMobileComposerOpen(false); + } + }; + + syncViewport(); + mediaQuery.addEventListener("change", syncViewport); + + return () => mediaQuery.removeEventListener("change", syncViewport); + }, []); + + useEffect(() => { + if (!isExpanded || isMobileViewport) { + return; + } + + const handlePointerDown = (event: MouseEvent) => { + if (!composerRef.current?.contains(event.target as Node)) { + setIsExpanded(false); + } + }; + + document.addEventListener("mousedown", handlePointerDown); + + return () => document.removeEventListener("mousedown", handlePointerDown); + }, [isExpanded, isMobileViewport]); + + async function submitPost(options?: { closeMobileComposer?: boolean }) { + setStatusMessage(null); + + startPostTransition(async () => { + const formData = new FormData(); + formData.set("title", title); + formData.set("body", body); + + const result = await createPostAction({}, formData); + + if (result.error) { + setStatusMessage({ type: "error", text: result.error }); + return; + } + + setTitle(""); + setBody(""); + setStatusMessage({ type: "success", text: result.success ?? "Post published." }); + + if (options?.closeMobileComposer) { + setIsMobileComposerOpen(false); + } else { + setIsExpanded(false); + } + }); + } + + const sharedBodyFieldClassName = + "flex min-h-32 w-full rounded-xl border border-sky-200/70 bg-background/80 px-3 py-3 text-sm shadow-xs outline-none transition-colors placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/20 disabled:cursor-not-allowed disabled:opacity-50 dark:border-sky-900/60"; + + const messageBlock = statusMessage ? ( +

+ {statusMessage.text} +

+ ) : null; + + if (!session?.user) { + return null; + } + + return ( + <> +
+ + +
+
+ {(session.user.username?.[0] || session.user.email?.[0] || session.user.name?.[0] || "U").toUpperCase()} +
+
+ Create a post + { + setTitle(event.target.value); + if (!isMobileViewport) { + setIsExpanded(true); + } + }} + onFocus={() => { + setStatusMessage(null); + if (isMobileViewport) { + setIsMobileComposerOpen(true); + return; + } + setIsExpanded(true); + }} + readOnly={isMobileViewport} + placeholder="Share something with the community" + className="h-11 border-sky-200/70 bg-background/80 text-base dark:border-sky-900/60" + /> +
+
+
+
+
+
{ + event.preventDefault(); + void submitPost(); + }} + > + +