From 7dd666f06d24e4976f551902409d17a3468de362 Mon Sep 17 00:00:00 2001 From: omer alpi Date: Mon, 23 Feb 2026 09:08:20 +0300 Subject: [PATCH 1/2] wip --- package.json | 4 +- pnpm-lock.yaml | 465 ++++++++++-------- src/app/api/auth/me/route.ts | 20 + src/app/api/auth/referral/route.ts | 28 ++ src/app/api/v1/auth/me/route.ts | 20 + src/app/api/v1/badges/check/route.ts | 29 ++ src/app/api/v1/badges/me/route.ts | 20 + src/app/api/v1/badges/progress/route.ts | 20 + src/app/api/v1/badges/route.ts | 20 + src/app/api/v1/badges/stats/route.ts | 20 + .../api/v1/badges/users/[username]/route.ts | 27 + src/app/api/v1/discord/auth-url/route.ts | 20 + src/app/api/v1/discord/callback/route.ts | 28 ++ src/app/api/v1/discord/disconnect/route.ts | 20 + src/app/api/v1/discord/status/route.ts | 20 + .../conversations/[id]/messages/route.ts | 40 ++ .../messages/conversations/[id]/read/route.ts | 27 + .../api/v1/messages/conversations/route.ts | 32 ++ .../messages/conversations/with/[id]/route.ts | 27 + src/app/api/v1/messages/route.ts | 29 ++ src/app/api/v1/messages/unread-count/route.ts | 20 + .../v1/nfts/collections/[id]/nfts/route.ts | 40 ++ src/app/api/v1/nfts/collections/[id]/route.ts | 53 ++ src/app/api/v1/nfts/collections/route.ts | 56 +++ src/app/api/v1/nfts/me/route.ts | 20 + src/app/api/v1/nfts/mint/route.ts | 29 ++ .../v1/nfts/users/[id]/collections/route.ts | 40 ++ .../api/v1/notifications/[id]/read/route.ts | 27 + .../api/v1/notifications/read-all/route.ts | 20 + src/app/api/v1/notifications/route.ts | 32 ++ .../v1/notifications/unread-count/route.ts | 20 + src/app/api/v1/points/history/route.ts | 34 ++ src/app/api/v1/points/leaderboard/route.ts | 32 ++ src/app/api/v1/points/route.ts | 43 ++ src/app/api/v1/points/stats/route.ts | 20 + src/app/api/v1/posts/[id]/like/route.ts | 42 ++ src/app/api/v1/posts/[id]/replies/route.ts | 66 +++ src/app/api/v1/posts/[id]/repost/route.ts | 42 ++ src/app/api/v1/posts/[id]/route.ts | 42 ++ src/app/api/v1/posts/[id]/save/route.ts | 42 ++ src/app/api/v1/posts/bookmarks/route.ts | 32 ++ src/app/api/v1/posts/route.ts | 110 +++++ src/app/api/v1/posts/search/route.ts | 34 ++ .../api/v1/posts/trending-hashtags/route.ts | 30 ++ src/app/api/v1/referrals/accept/route.ts | 28 ++ src/app/api/v1/referrals/code/route.ts | 33 ++ src/app/api/v1/referrals/lookup/route.ts | 30 ++ src/app/api/v1/referrals/stats/route.ts | 20 + src/app/api/v1/streak/activities/route.ts | 30 ++ src/app/api/v1/streak/route.ts | 33 ++ src/app/api/v1/streak/stats/route.ts | 20 + src/app/api/v1/unread-counts/route.ts | 29 ++ .../v1/users/[username]/followers/route.ts | 41 ++ .../v1/users/[username]/following/route.ts | 41 ++ src/app/api/v1/users/[username]/route.ts | 27 + .../[username]/transactions/refresh/route.ts | 27 + src/app/api/v1/users/id/[id]/follow/route.ts | 42 ++ src/app/api/v1/users/id/[id]/route.ts | 27 + src/app/api/v1/users/landing-stats/route.ts | 20 + src/app/api/v1/users/me/profile/route.ts | 39 ++ src/app/api/v1/users/me/wallet/route.ts | 28 ++ src/app/api/v1/users/random/route.ts | 30 ++ src/app/api/v1/users/search/route.ts | 32 ++ src/app/api/v1/users/suggestions/route.ts | 30 ++ src/app/api/v1/users/top-crypto/route.ts | 20 + src/lib/chopin-auth.ts | 86 ++++ src/lib/session.ts | 14 +- src/server/api/routers/post.ts | 75 +-- src/server/api/trpc.ts | 4 +- src/server/http/trpc-to-http.ts | 30 ++ tsconfig.json | 7 +- 71 files changed, 2429 insertions(+), 256 deletions(-) create mode 100644 src/app/api/auth/me/route.ts create mode 100644 src/app/api/auth/referral/route.ts create mode 100644 src/app/api/v1/auth/me/route.ts create mode 100644 src/app/api/v1/badges/check/route.ts create mode 100644 src/app/api/v1/badges/me/route.ts create mode 100644 src/app/api/v1/badges/progress/route.ts create mode 100644 src/app/api/v1/badges/route.ts create mode 100644 src/app/api/v1/badges/stats/route.ts create mode 100644 src/app/api/v1/badges/users/[username]/route.ts create mode 100644 src/app/api/v1/discord/auth-url/route.ts create mode 100644 src/app/api/v1/discord/callback/route.ts create mode 100644 src/app/api/v1/discord/disconnect/route.ts create mode 100644 src/app/api/v1/discord/status/route.ts create mode 100644 src/app/api/v1/messages/conversations/[id]/messages/route.ts create mode 100644 src/app/api/v1/messages/conversations/[id]/read/route.ts create mode 100644 src/app/api/v1/messages/conversations/route.ts create mode 100644 src/app/api/v1/messages/conversations/with/[id]/route.ts create mode 100644 src/app/api/v1/messages/route.ts create mode 100644 src/app/api/v1/messages/unread-count/route.ts create mode 100644 src/app/api/v1/nfts/collections/[id]/nfts/route.ts create mode 100644 src/app/api/v1/nfts/collections/[id]/route.ts create mode 100644 src/app/api/v1/nfts/collections/route.ts create mode 100644 src/app/api/v1/nfts/me/route.ts create mode 100644 src/app/api/v1/nfts/mint/route.ts create mode 100644 src/app/api/v1/nfts/users/[id]/collections/route.ts create mode 100644 src/app/api/v1/notifications/[id]/read/route.ts create mode 100644 src/app/api/v1/notifications/read-all/route.ts create mode 100644 src/app/api/v1/notifications/route.ts create mode 100644 src/app/api/v1/notifications/unread-count/route.ts create mode 100644 src/app/api/v1/points/history/route.ts create mode 100644 src/app/api/v1/points/leaderboard/route.ts create mode 100644 src/app/api/v1/points/route.ts create mode 100644 src/app/api/v1/points/stats/route.ts create mode 100644 src/app/api/v1/posts/[id]/like/route.ts create mode 100644 src/app/api/v1/posts/[id]/replies/route.ts create mode 100644 src/app/api/v1/posts/[id]/repost/route.ts create mode 100644 src/app/api/v1/posts/[id]/route.ts create mode 100644 src/app/api/v1/posts/[id]/save/route.ts create mode 100644 src/app/api/v1/posts/bookmarks/route.ts create mode 100644 src/app/api/v1/posts/route.ts create mode 100644 src/app/api/v1/posts/search/route.ts create mode 100644 src/app/api/v1/posts/trending-hashtags/route.ts create mode 100644 src/app/api/v1/referrals/accept/route.ts create mode 100644 src/app/api/v1/referrals/code/route.ts create mode 100644 src/app/api/v1/referrals/lookup/route.ts create mode 100644 src/app/api/v1/referrals/stats/route.ts create mode 100644 src/app/api/v1/streak/activities/route.ts create mode 100644 src/app/api/v1/streak/route.ts create mode 100644 src/app/api/v1/streak/stats/route.ts create mode 100644 src/app/api/v1/unread-counts/route.ts create mode 100644 src/app/api/v1/users/[username]/followers/route.ts create mode 100644 src/app/api/v1/users/[username]/following/route.ts create mode 100644 src/app/api/v1/users/[username]/route.ts create mode 100644 src/app/api/v1/users/[username]/transactions/refresh/route.ts create mode 100644 src/app/api/v1/users/id/[id]/follow/route.ts create mode 100644 src/app/api/v1/users/id/[id]/route.ts create mode 100644 src/app/api/v1/users/landing-stats/route.ts create mode 100644 src/app/api/v1/users/me/profile/route.ts create mode 100644 src/app/api/v1/users/me/wallet/route.ts create mode 100644 src/app/api/v1/users/random/route.ts create mode 100644 src/app/api/v1/users/search/route.ts create mode 100644 src/app/api/v1/users/suggestions/route.ts create mode 100644 src/app/api/v1/users/top-crypto/route.ts create mode 100644 src/lib/chopin-auth.ts create mode 100644 src/server/http/trpc-to-http.ts diff --git a/package.json b/package.json index c941593..a81c012 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@hookform/resolvers": "^4.1.2", "@keplr-wallet/provider-extension": "^0.12.223", "@next/third-parties": "^15.3.2", + "@openauthjs/openauth": "^0.4.3", "@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", @@ -45,7 +46,7 @@ "jose": "^6.0.8", "jsonwebtoken": "^9.0.2", "lucide-react": "^0.475.0", - "next": "^15.2.0", + "next": "^16.1.6", "next-themes": "^0.4.4", "pg": "^8.13.3", "react": "^19.0.0", @@ -56,6 +57,7 @@ "superjson": "^2.2.2", "tailwind-merge": "^3.0.1", "tailwindcss-animate": "^1.0.7", + "valibot": "^1.2.0", "zod": "^3.24.2" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aff6cdd..326a17f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: dependencies: '@chopinframework/next': specifier: 1.1.8 - version: 1.1.8(arctic@2.3.4)(hono@4.7.2)(next@15.3.2(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(server-only@0.0.1)(typescript@5.7.3)(zod@3.24.2) + version: 1.1.8(arctic@2.3.4)(hono@4.7.2)(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(server-only@0.0.1)(typescript@5.7.3)(zod@3.24.2) '@chopinframework/react': specifier: ^0.1.1 version: 0.1.1(react@19.0.0) @@ -22,7 +22,10 @@ importers: version: 0.12.223(starknet@6.24.1) '@next/third-parties': specifier: ^15.3.2 - version: 15.3.2(next@15.3.2(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) + version: 15.3.2(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) + '@openauthjs/openauth': + specifier: ^0.4.3 + version: 0.4.3(arctic@2.3.4)(hono@4.7.2) '@radix-ui/react-avatar': specifier: ^1.1.3 version: 1.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -67,7 +70,7 @@ importers: version: 11.0.0-rc.795(@trpc/server@11.0.0-rc.795(typescript@5.7.3))(typescript@5.7.3) '@trpc/next': specifier: 11.0.0-rc.795 - version: 11.0.0-rc.795(@tanstack/react-query@5.66.7(react@19.0.0))(@trpc/client@11.0.0-rc.795(@trpc/server@11.0.0-rc.795(typescript@5.7.3))(typescript@5.7.3))(@trpc/react-query@11.0.0-rc.795(@tanstack/react-query@5.66.7(react@19.0.0))(@trpc/client@11.0.0-rc.795(@trpc/server@11.0.0-rc.795(typescript@5.7.3))(typescript@5.7.3))(@trpc/server@11.0.0-rc.795(typescript@5.7.3))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(@trpc/server@11.0.0-rc.795(typescript@5.7.3))(next@15.3.2(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3) + version: 11.0.0-rc.795(@tanstack/react-query@5.66.7(react@19.0.0))(@trpc/client@11.0.0-rc.795(@trpc/server@11.0.0-rc.795(typescript@5.7.3))(typescript@5.7.3))(@trpc/react-query@11.0.0-rc.795(@tanstack/react-query@5.66.7(react@19.0.0))(@trpc/client@11.0.0-rc.795(@trpc/server@11.0.0-rc.795(typescript@5.7.3))(typescript@5.7.3))(@trpc/server@11.0.0-rc.795(typescript@5.7.3))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(@trpc/server@11.0.0-rc.795(typescript@5.7.3))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3) '@trpc/react-query': specifier: 11.0.0-rc.795 version: 11.0.0-rc.795(@tanstack/react-query@5.66.7(react@19.0.0))(@trpc/client@11.0.0-rc.795(@trpc/server@11.0.0-rc.795(typescript@5.7.3))(typescript@5.7.3))(@trpc/server@11.0.0-rc.795(typescript@5.7.3))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3) @@ -76,7 +79,7 @@ importers: version: 11.0.0-rc.795(typescript@5.7.3) '@vercel/speed-insights': specifier: ^1.2.0 - version: 1.2.0(next@15.3.2(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) + version: 1.2.0(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -111,8 +114,8 @@ importers: specifier: ^0.475.0 version: 0.475.0(react@19.0.0) next: - specifier: ^15.2.0 - version: 15.3.2(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: ^16.1.6 + version: 16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next-themes: specifier: ^0.4.4 version: 0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -143,6 +146,9 @@ importers: tailwindcss-animate: specifier: ^1.0.7 version: 1.0.7(tailwindcss@3.4.17) + valibot: + specifier: ^1.2.0 + version: 1.2.0(typescript@5.7.3) zod: specifier: ^3.24.2 version: 3.24.2 @@ -226,8 +232,8 @@ packages: '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} - '@emnapi/runtime@1.4.3': - resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} @@ -585,112 +591,139 @@ packages: resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==} engines: {node: '>=18.18'} - '@img/sharp-darwin-arm64@0.34.1': - resolution: {integrity: sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A==} + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.34.1': - resolution: {integrity: sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q==} + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.1.0': - resolution: {integrity: sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==} + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.1.0': - resolution: {integrity: sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==} + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.1.0': - resolution: {integrity: sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==} + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linux-arm@1.1.0': - resolution: {integrity: sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==} + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - '@img/sharp-libvips-linux-ppc64@1.1.0': - resolution: {integrity: sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==} + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - '@img/sharp-libvips-linux-s390x@1.1.0': - resolution: {integrity: sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==} + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - '@img/sharp-libvips-linux-x64@1.1.0': - resolution: {integrity: sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==} + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - '@img/sharp-libvips-linuxmusl-arm64@1.1.0': - resolution: {integrity: sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==} + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linuxmusl-x64@1.1.0': - resolution: {integrity: sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==} + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - '@img/sharp-linux-arm64@0.34.1': - resolution: {integrity: sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ==} + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linux-arm@0.34.1': - resolution: {integrity: sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA==} + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - '@img/sharp-linux-s390x@0.34.1': - resolution: {integrity: sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA==} + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - '@img/sharp-linux-x64@0.34.1': - resolution: {integrity: sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA==} + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-linuxmusl-arm64@0.34.1': - resolution: {integrity: sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ==} + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linuxmusl-x64@0.34.1': - resolution: {integrity: sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg==} + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-wasm32@0.34.1': - resolution: {integrity: sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg==} + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] - '@img/sharp-win32-ia32@0.34.1': - resolution: {integrity: sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw==} + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.34.1': - resolution: {integrity: sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw==} + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] @@ -727,8 +760,8 @@ packages: peerDependencies: starknet: ^6 - '@next/env@15.3.2': - resolution: {integrity: sha512-xURk++7P7qR9JG1jJtLzPzf0qEvqCN0A/T3DXf8IPMKo9/6FfjxtEffRJIIew/bIL4T3C2jLLqBor8B/zVlx6g==} + '@next/env@16.1.6': + resolution: {integrity: sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==} '@next/eslint-plugin-next@15.1.7': resolution: {integrity: sha512-kRP7RjSxfTO13NE317ek3mSGzoZlI33nc/i5hs1KaWpK+egs85xg0DJ4p32QEiHnR0mVjuUfhRIun7awqfL7pQ==} @@ -736,50 +769,50 @@ packages: '@next/eslint-plugin-next@15.2.0': resolution: {integrity: sha512-jHFUG2OwmAuOASqq253RAEG/5BYcPHn27p1NoWZDCf4OdvdK0yRYWX92YKkL+Mk2s+GyJrmd/GATlL5b2IySpw==} - '@next/swc-darwin-arm64@15.3.2': - resolution: {integrity: sha512-2DR6kY/OGcokbnCsjHpNeQblqCZ85/1j6njYSkzRdpLn5At7OkSdmk7WyAmB9G0k25+VgqVZ/u356OSoQZ3z0g==} + '@next/swc-darwin-arm64@16.1.6': + resolution: {integrity: sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.3.2': - resolution: {integrity: sha512-ro/fdqaZWL6k1S/5CLv1I0DaZfDVJkWNaUU3un8Lg6m0YENWlDulmIWzV96Iou2wEYyEsZq51mwV8+XQXqMp3w==} + '@next/swc-darwin-x64@16.1.6': + resolution: {integrity: sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.3.2': - resolution: {integrity: sha512-covwwtZYhlbRWK2HlYX9835qXum4xYZ3E2Mra1mdQ+0ICGoMiw1+nVAn4d9Bo7R3JqSmK1grMq/va+0cdh7bJA==} + '@next/swc-linux-arm64-gnu@16.1.6': + resolution: {integrity: sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.3.2': - resolution: {integrity: sha512-KQkMEillvlW5Qk5mtGA/3Yz0/tzpNlSw6/3/ttsV1lNtMuOHcGii3zVeXZyi4EJmmLDKYcTcByV2wVsOhDt/zg==} + '@next/swc-linux-arm64-musl@16.1.6': + resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.3.2': - resolution: {integrity: sha512-uRBo6THWei0chz+Y5j37qzx+BtoDRFIkDzZjlpCItBRXyMPIg079eIkOCl3aqr2tkxL4HFyJ4GHDes7W8HuAUg==} + '@next/swc-linux-x64-gnu@16.1.6': + resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.3.2': - resolution: {integrity: sha512-+uxFlPuCNx/T9PdMClOqeE8USKzj8tVz37KflT3Kdbx/LOlZBRI2yxuIcmx1mPNK8DwSOMNCr4ureSet7eyC0w==} + '@next/swc-linux-x64-musl@16.1.6': + resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.3.2': - resolution: {integrity: sha512-LLTKmaI5cfD8dVzh5Vt7+OMo+AIOClEdIU/TSKbXXT2iScUTSxOGoBhfuv+FU8R9MLmrkIL1e2fBMkEEjYAtPQ==} + '@next/swc-win32-arm64-msvc@16.1.6': + resolution: {integrity: sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.3.2': - resolution: {integrity: sha512-aW5B8wOPioJ4mBdMDXkt5f3j8pUr9W8AnlX0Df35uRWNT1Y6RIybxjnSUe+PhM+M1bwgyY8PHLmXZC6zT1o5tA==} + '@next/swc-win32-x64-msvc@16.1.6': + resolution: {integrity: sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -820,6 +853,12 @@ packages: arctic: ^2.2.2 hono: ^4.0.0 + '@openauthjs/openauth@0.4.3': + resolution: {integrity: sha512-RlnjqvHzqcbFVymEwhlUEuac4utA5h4nhSK/i2szZuQmxTIqbGUxZ+nM+avM+VV4Ing+/ZaNLKILoXS3yrkOOw==} + peerDependencies: + arctic: ^2.2.2 + hono: ^4.0.0 + '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} @@ -1465,9 +1504,6 @@ packages: '@starknet-io/types-js@0.7.10': resolution: {integrity: sha512-1VtCqX4AHWJlRRSYGSn+4X1mqolI1Tdq62IwzoU2vUuEE72S1OlEeGhpvd6XsdqXcfHmVzYfj8k1XtKBQqwo9w==} - '@swc/counter@0.1.3': - resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -1745,6 +1781,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + baseline-browser-mapping@2.9.19: + resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} + hasBin: true + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -1765,10 +1805,6 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - busboy@1.6.0: - resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} - engines: {node: '>=10.16.0'} - call-bind-apply-helpers@1.0.1: resolution: {integrity: sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==} engines: {node: '>= 0.4'} @@ -1832,13 +1868,6 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - color-string@1.9.1: - resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} - - color@4.2.3: - resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} - engines: {node: '>=12.5.0'} - commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -1912,8 +1941,8 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} - detect-libc@2.0.3: - resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} detect-node-es@1.1.0: @@ -2432,9 +2461,6 @@ packages: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} - is-arrayish@0.3.2: - resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} - is-async-function@2.1.1: resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} engines: {node: '>= 0.4'} @@ -2731,13 +2757,13 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@15.3.2: - resolution: {integrity: sha512-CA3BatMyHkxZ48sgOCLdVHjFU36N7TF1HhqAHLFOkV6buwZnvMI84Cug8xD56B9mCuKrqXnLn94417GrZ/jjCQ==} - engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + next@16.1.6: + resolution: {integrity: sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==} + engines: {node: '>=20.9.0'} hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 - '@playwright/test': ^1.41.2 + '@playwright/test': ^1.51.1 babel-plugin-react-compiler: '*' react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 @@ -3149,6 +3175,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + server-only@0.0.1: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} @@ -3167,8 +3198,8 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} - sharp@0.34.1: - resolution: {integrity: sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg==} + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} shebang-command@2.0.0: @@ -3203,9 +3234,6 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - simple-swizzle@0.2.2: - resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} - sonner@2.0.1: resolution: {integrity: sha512-FRBphaehZ5tLdLcQ8g2WOIRE+Y7BCfWi5Zyd8bCvBjiW8TxxAyoWZIxS661Yz6TGPqFQ4VLzOF89WEYhfynSFQ==} peerDependencies: @@ -3233,10 +3261,6 @@ packages: starknet@6.24.1: resolution: {integrity: sha512-g7tiCt73berhcNi41otlN3T3kxZnIvZhMi8WdC21Y6GC6zoQgbI2z1t7JAZF9c4xZiomlanwVnurcpyfEdyMpg==} - streamsearch@1.1.0: - resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} - engines: {node: '>=10.0.0'} - string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -3447,6 +3471,14 @@ packages: typescript: optional: true + valibot@1.2.0: + resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -3526,11 +3558,11 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@chopinframework/next@1.1.8(arctic@2.3.4)(hono@4.7.2)(next@15.3.2(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(server-only@0.0.1)(typescript@5.7.3)(zod@3.24.2)': + '@chopinframework/next@1.1.8(arctic@2.3.4)(hono@4.7.2)(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(server-only@0.0.1)(typescript@5.7.3)(zod@3.24.2)': dependencies: '@chopinframework/sdk': 0.2.1(zod@3.24.2) '@openauthjs/openauth': 0.3.9(arctic@2.3.4)(hono@4.7.2) - next: 15.3.2(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) server-only: 0.0.1 valibot: 1.0.0-rc.1(typescript@5.7.3) transitivePeerDependencies: @@ -3550,7 +3582,7 @@ snapshots: '@drizzle-team/brocli@0.10.2': {} - '@emnapi/runtime@1.4.3': + '@emnapi/runtime@1.8.1': dependencies: tslib: 2.8.1 optional: true @@ -3781,82 +3813,101 @@ snapshots: '@humanwhocodes/retry@0.4.1': {} - '@img/sharp-darwin-arm64@0.34.1': + '@img/colour@1.0.0': + optional: true + + '@img/sharp-darwin-arm64@0.34.5': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.1.0 + '@img/sharp-libvips-darwin-arm64': 1.2.4 optional: true - '@img/sharp-darwin-x64@0.34.1': + '@img/sharp-darwin-x64@0.34.5': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.1.0 + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': optional: true - '@img/sharp-libvips-darwin-arm64@1.1.0': + '@img/sharp-libvips-darwin-x64@1.2.4': optional: true - '@img/sharp-libvips-darwin-x64@1.1.0': + '@img/sharp-libvips-linux-arm64@1.2.4': optional: true - '@img/sharp-libvips-linux-arm64@1.1.0': + '@img/sharp-libvips-linux-arm@1.2.4': optional: true - '@img/sharp-libvips-linux-arm@1.1.0': + '@img/sharp-libvips-linux-ppc64@1.2.4': optional: true - '@img/sharp-libvips-linux-ppc64@1.1.0': + '@img/sharp-libvips-linux-riscv64@1.2.4': optional: true - '@img/sharp-libvips-linux-s390x@1.1.0': + '@img/sharp-libvips-linux-s390x@1.2.4': optional: true - '@img/sharp-libvips-linux-x64@1.1.0': + '@img/sharp-libvips-linux-x64@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.1.0': + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.1.0': + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 optional: true - '@img/sharp-linux-arm64@0.34.1': + '@img/sharp-linux-arm@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.1.0 + '@img/sharp-libvips-linux-arm': 1.2.4 optional: true - '@img/sharp-linux-arm@0.34.1': + '@img/sharp-linux-ppc64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.1.0 + '@img/sharp-libvips-linux-ppc64': 1.2.4 optional: true - '@img/sharp-linux-s390x@0.34.1': + '@img/sharp-linux-riscv64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.1.0 + '@img/sharp-libvips-linux-riscv64': 1.2.4 optional: true - '@img/sharp-linux-x64@0.34.1': + '@img/sharp-linux-s390x@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.1.0 + '@img/sharp-libvips-linux-s390x': 1.2.4 optional: true - '@img/sharp-linuxmusl-arm64@0.34.1': + '@img/sharp-linux-x64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.1.0 + '@img/sharp-libvips-linux-x64': 1.2.4 optional: true - '@img/sharp-linuxmusl-x64@0.34.1': + '@img/sharp-linuxmusl-arm64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.1.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 optional: true - '@img/sharp-wasm32@0.34.1': + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.4.3 + '@emnapi/runtime': 1.8.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': optional: true - '@img/sharp-win32-ia32@0.34.1': + '@img/sharp-win32-ia32@0.34.5': optional: true - '@img/sharp-win32-x64@0.34.1': + '@img/sharp-win32-x64@0.34.5': optional: true '@isaacs/cliui@8.0.2': @@ -3897,7 +3948,7 @@ snapshots: long: 4.0.0 starknet: 6.24.1 - '@next/env@15.3.2': {} + '@next/env@16.1.6': {} '@next/eslint-plugin-next@15.1.7': dependencies: @@ -3907,33 +3958,33 @@ snapshots: dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@15.3.2': + '@next/swc-darwin-arm64@16.1.6': optional: true - '@next/swc-darwin-x64@15.3.2': + '@next/swc-darwin-x64@16.1.6': optional: true - '@next/swc-linux-arm64-gnu@15.3.2': + '@next/swc-linux-arm64-gnu@16.1.6': optional: true - '@next/swc-linux-arm64-musl@15.3.2': + '@next/swc-linux-arm64-musl@16.1.6': optional: true - '@next/swc-linux-x64-gnu@15.3.2': + '@next/swc-linux-x64-gnu@16.1.6': optional: true - '@next/swc-linux-x64-musl@15.3.2': + '@next/swc-linux-x64-musl@16.1.6': optional: true - '@next/swc-win32-arm64-msvc@15.3.2': + '@next/swc-win32-arm64-msvc@16.1.6': optional: true - '@next/swc-win32-x64-msvc@15.3.2': + '@next/swc-win32-x64-msvc@16.1.6': optional: true - '@next/third-parties@15.3.2(next@15.3.2(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)': + '@next/third-parties@15.3.2(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)': dependencies: - next: 15.3.2(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: 19.0.0 third-party-capital: 1.0.20 @@ -3965,6 +4016,14 @@ snapshots: hono: 4.7.2 jose: 5.9.6 + '@openauthjs/openauth@0.4.3(arctic@2.3.4)(hono@4.7.2)': + dependencies: + '@standard-schema/spec': 1.0.0-beta.3 + arctic: 2.3.4 + aws4fetch: 1.0.20 + hono: 4.7.2 + jose: 5.9.6 + '@opentelemetry/api@1.9.0': optional: true @@ -4553,8 +4612,6 @@ snapshots: '@starknet-io/types-js@0.7.10': {} - '@swc/counter@0.1.3': {} - '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -4579,11 +4636,11 @@ snapshots: '@trpc/server': 11.0.0-rc.795(typescript@5.7.3) typescript: 5.7.3 - '@trpc/next@11.0.0-rc.795(@tanstack/react-query@5.66.7(react@19.0.0))(@trpc/client@11.0.0-rc.795(@trpc/server@11.0.0-rc.795(typescript@5.7.3))(typescript@5.7.3))(@trpc/react-query@11.0.0-rc.795(@tanstack/react-query@5.66.7(react@19.0.0))(@trpc/client@11.0.0-rc.795(@trpc/server@11.0.0-rc.795(typescript@5.7.3))(typescript@5.7.3))(@trpc/server@11.0.0-rc.795(typescript@5.7.3))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(@trpc/server@11.0.0-rc.795(typescript@5.7.3))(next@15.3.2(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3)': + '@trpc/next@11.0.0-rc.795(@tanstack/react-query@5.66.7(react@19.0.0))(@trpc/client@11.0.0-rc.795(@trpc/server@11.0.0-rc.795(typescript@5.7.3))(typescript@5.7.3))(@trpc/react-query@11.0.0-rc.795(@tanstack/react-query@5.66.7(react@19.0.0))(@trpc/client@11.0.0-rc.795(@trpc/server@11.0.0-rc.795(typescript@5.7.3))(typescript@5.7.3))(@trpc/server@11.0.0-rc.795(typescript@5.7.3))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(@trpc/server@11.0.0-rc.795(typescript@5.7.3))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3)': dependencies: '@trpc/client': 11.0.0-rc.795(@trpc/server@11.0.0-rc.795(typescript@5.7.3))(typescript@5.7.3) '@trpc/server': 11.0.0-rc.795(typescript@5.7.3) - next: 15.3.2(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) typescript: 5.7.3 @@ -4714,9 +4771,9 @@ snapshots: '@typescript-eslint/types': 8.24.0 eslint-visitor-keys: 4.2.0 - '@vercel/speed-insights@1.2.0(next@15.3.2(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)': + '@vercel/speed-insights@1.2.0(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)': optionalDependencies: - next: 15.3.2(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: 19.0.0 abi-wan-kanabi@2.2.4: @@ -4854,6 +4911,8 @@ snapshots: balanced-match@1.0.2: {} + baseline-browser-mapping@2.9.19: {} + binary-extensions@2.3.0: {} brace-expansion@1.1.11: @@ -4873,10 +4932,6 @@ snapshots: buffer-from@1.1.2: {} - busboy@1.6.0: - dependencies: - streamsearch: 1.1.0 - call-bind-apply-helpers@1.0.1: dependencies: es-errors: 1.3.0 @@ -4949,18 +5004,6 @@ snapshots: color-name@1.1.4: {} - color-string@1.9.1: - dependencies: - color-name: 1.1.4 - simple-swizzle: 0.2.2 - optional: true - - color@4.2.3: - dependencies: - color-convert: 2.0.1 - color-string: 1.9.1 - optional: true - commander@4.1.1: {} concat-map@0.0.1: {} @@ -5025,7 +5068,7 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 - detect-libc@2.0.3: + detect-libc@2.1.2: optional: true detect-node-es@1.1.0: {} @@ -5660,9 +5703,6 @@ snapshots: call-bound: 1.0.3 get-intrinsic: 1.2.7 - is-arrayish@0.3.2: - optional: true - is-async-function@2.1.1: dependencies: async-function: 1.0.0 @@ -5960,28 +6000,27 @@ snapshots: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) - next@15.3.2(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: - '@next/env': 15.3.2 - '@swc/counter': 0.1.3 + '@next/env': 16.1.6 '@swc/helpers': 0.5.15 - busboy: 1.6.0 + baseline-browser-mapping: 2.9.19 caniuse-lite: 1.0.30001699 postcss: 8.4.31 react: 19.0.0 react-dom: 19.0.0(react@19.0.0) styled-jsx: 5.1.6(react@19.0.0) optionalDependencies: - '@next/swc-darwin-arm64': 15.3.2 - '@next/swc-darwin-x64': 15.3.2 - '@next/swc-linux-arm64-gnu': 15.3.2 - '@next/swc-linux-arm64-musl': 15.3.2 - '@next/swc-linux-x64-gnu': 15.3.2 - '@next/swc-linux-x64-musl': 15.3.2 - '@next/swc-win32-arm64-msvc': 15.3.2 - '@next/swc-win32-x64-msvc': 15.3.2 + '@next/swc-darwin-arm64': 16.1.6 + '@next/swc-darwin-x64': 16.1.6 + '@next/swc-linux-arm64-gnu': 16.1.6 + '@next/swc-linux-arm64-musl': 16.1.6 + '@next/swc-linux-x64-gnu': 16.1.6 + '@next/swc-linux-x64-musl': 16.1.6 + '@next/swc-win32-arm64-msvc': 16.1.6 + '@next/swc-win32-x64-msvc': 16.1.6 '@opentelemetry/api': 1.9.0 - sharp: 0.34.1 + sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -6354,6 +6393,9 @@ snapshots: semver@7.7.1: {} + semver@7.7.4: + optional: true + server-only@0.0.1: {} set-cookie-parser@2.7.1: {} @@ -6380,32 +6422,36 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 - sharp@0.34.1: + sharp@0.34.5: dependencies: - color: 4.2.3 - detect-libc: 2.0.3 - semver: 7.7.1 + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.4 optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.1 - '@img/sharp-darwin-x64': 0.34.1 - '@img/sharp-libvips-darwin-arm64': 1.1.0 - '@img/sharp-libvips-darwin-x64': 1.1.0 - '@img/sharp-libvips-linux-arm': 1.1.0 - '@img/sharp-libvips-linux-arm64': 1.1.0 - '@img/sharp-libvips-linux-ppc64': 1.1.0 - '@img/sharp-libvips-linux-s390x': 1.1.0 - '@img/sharp-libvips-linux-x64': 1.1.0 - '@img/sharp-libvips-linuxmusl-arm64': 1.1.0 - '@img/sharp-libvips-linuxmusl-x64': 1.1.0 - '@img/sharp-linux-arm': 0.34.1 - '@img/sharp-linux-arm64': 0.34.1 - '@img/sharp-linux-s390x': 0.34.1 - '@img/sharp-linux-x64': 0.34.1 - '@img/sharp-linuxmusl-arm64': 0.34.1 - '@img/sharp-linuxmusl-x64': 0.34.1 - '@img/sharp-wasm32': 0.34.1 - '@img/sharp-win32-ia32': 0.34.1 - '@img/sharp-win32-x64': 0.34.1 + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 optional: true shebang-command@2.0.0: @@ -6446,11 +6492,6 @@ snapshots: signal-exit@4.1.0: {} - simple-swizzle@0.2.2: - dependencies: - is-arrayish: 0.3.2 - optional: true - sonner@2.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: react: 19.0.0 @@ -6485,8 +6526,6 @@ snapshots: transitivePeerDependencies: - encoding - streamsearch@1.1.0: {} - string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -6743,6 +6782,10 @@ snapshots: optionalDependencies: typescript: 5.7.3 + valibot@1.2.0(typescript@5.7.3): + optionalDependencies: + typescript: 5.7.3 + webidl-conversions@3.0.1: {} whatwg-fetch@3.6.20: {} diff --git a/src/app/api/auth/me/route.ts b/src/app/api/auth/me/route.ts new file mode 100644 index 0000000..39cb593 --- /dev/null +++ b/src/app/api/auth/me/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +export async function GET(req: NextRequest) { + try { + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.auth.me(); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/auth/referral/route.ts b/src/app/api/auth/referral/route.ts new file mode 100644 index 0000000..5d9180a --- /dev/null +++ b/src/app/api/auth/referral/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Body = z.object({ + referralCode: z.string().min(1), +}); + +export async function POST(req: NextRequest) { + try { + const json = await req.json(); + const input = Body.parse(json); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.auth.validateWithReferral(input); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/auth/me/route.ts b/src/app/api/v1/auth/me/route.ts new file mode 100644 index 0000000..39cb593 --- /dev/null +++ b/src/app/api/v1/auth/me/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +export async function GET(req: NextRequest) { + try { + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.auth.me(); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/badges/check/route.ts b/src/app/api/v1/badges/check/route.ts new file mode 100644 index 0000000..311cb63 --- /dev/null +++ b/src/app/api/v1/badges/check/route.ts @@ -0,0 +1,29 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const CheckBody = z.object({ + activityType: z.enum(["like", "post", "streak", "points"]), + currentValue: z.number(), +}); + +export async function POST(req: NextRequest) { + try { + const json = await req.json(); + const input = CheckBody.parse(json); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.badge.checkAndAwardBadges(input); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/badges/me/route.ts b/src/app/api/v1/badges/me/route.ts new file mode 100644 index 0000000..5a476ed --- /dev/null +++ b/src/app/api/v1/badges/me/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +export async function GET(req: NextRequest) { + try { + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.badge.getUserBadges(); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/badges/progress/route.ts b/src/app/api/v1/badges/progress/route.ts new file mode 100644 index 0000000..00dee5b --- /dev/null +++ b/src/app/api/v1/badges/progress/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +export async function GET(req: NextRequest) { + try { + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.badge.getBadgeProgress(); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/badges/route.ts b/src/app/api/v1/badges/route.ts new file mode 100644 index 0000000..1034a1b --- /dev/null +++ b/src/app/api/v1/badges/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +export async function GET(req: NextRequest) { + try { + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.badge.getAllBadges(); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/badges/stats/route.ts b/src/app/api/v1/badges/stats/route.ts new file mode 100644 index 0000000..cd52f44 --- /dev/null +++ b/src/app/api/v1/badges/stats/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +export async function GET(req: NextRequest) { + try { + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.badge.getBadgeStats(); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/badges/users/[username]/route.ts b/src/app/api/v1/badges/users/[username]/route.ts new file mode 100644 index 0000000..68b8863 --- /dev/null +++ b/src/app/api/v1/badges/users/[username]/route.ts @@ -0,0 +1,27 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Params = z.object({ + username: z.string().min(1), +}); + +export async function GET(req: NextRequest, { params }: { params: Promise<{ username: string }> }) { + try { + const { username } = Params.parse(await params); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.badge.getUserBadgesByUsername({ username }); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/discord/auth-url/route.ts b/src/app/api/v1/discord/auth-url/route.ts new file mode 100644 index 0000000..0897e47 --- /dev/null +++ b/src/app/api/v1/discord/auth-url/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +export async function GET(req: NextRequest) { + try { + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.discord.getAuthUrl(); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/discord/callback/route.ts b/src/app/api/v1/discord/callback/route.ts new file mode 100644 index 0000000..029a8a7 --- /dev/null +++ b/src/app/api/v1/discord/callback/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const CallbackBody = z.object({ + code: z.string().min(1), +}); + +export async function POST(req: NextRequest) { + try { + const json = await req.json(); + const input = CallbackBody.parse(json); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.discord.handleCallback(input); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/discord/disconnect/route.ts b/src/app/api/v1/discord/disconnect/route.ts new file mode 100644 index 0000000..fdf3fb5 --- /dev/null +++ b/src/app/api/v1/discord/disconnect/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +export async function POST(req: NextRequest) { + try { + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.discord.disconnect(); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/discord/status/route.ts b/src/app/api/v1/discord/status/route.ts new file mode 100644 index 0000000..ce1881c --- /dev/null +++ b/src/app/api/v1/discord/status/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +export async function GET(req: NextRequest) { + try { + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.discord.getConnectionStatus(); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/messages/conversations/[id]/messages/route.ts b/src/app/api/v1/messages/conversations/[id]/messages/route.ts new file mode 100644 index 0000000..ab7625a --- /dev/null +++ b/src/app/api/v1/messages/conversations/[id]/messages/route.ts @@ -0,0 +1,40 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Params = z.object({ + id: z.coerce.number(), +}); + +const Query = z.object({ + limit: z.coerce.number().min(1).max(50).default(20), + cursor: z.coerce.number().nullish(), +}); + +export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + try { + const { id } = Params.parse(await params); + const url = new URL(req.url); + const query = Query.parse({ + limit: url.searchParams.get("limit") ?? undefined, + cursor: url.searchParams.get("cursor") ?? undefined, + }); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.message.getMessages({ + conversationId: id, + ...query, + }); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/messages/conversations/[id]/read/route.ts b/src/app/api/v1/messages/conversations/[id]/read/route.ts new file mode 100644 index 0000000..7b0e7cc --- /dev/null +++ b/src/app/api/v1/messages/conversations/[id]/read/route.ts @@ -0,0 +1,27 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Params = z.object({ + id: z.coerce.number(), +}); + +export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + try { + const { id } = Params.parse(await params); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + await caller.message.markConversationAsRead({ conversationId: id }); + return NextResponse.json({ success: true }, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/messages/conversations/route.ts b/src/app/api/v1/messages/conversations/route.ts new file mode 100644 index 0000000..61fe5dd --- /dev/null +++ b/src/app/api/v1/messages/conversations/route.ts @@ -0,0 +1,32 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Query = z.object({ + limit: z.coerce.number().min(1).max(50).default(20), + cursor: z.coerce.number().nullish(), +}); + +export async function GET(req: NextRequest) { + try { + const url = new URL(req.url); + const input = Query.parse({ + limit: url.searchParams.get("limit") ?? undefined, + cursor: url.searchParams.get("cursor") ?? undefined, + }); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.message.getConversations(input); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/messages/conversations/with/[id]/route.ts b/src/app/api/v1/messages/conversations/with/[id]/route.ts new file mode 100644 index 0000000..098dcba --- /dev/null +++ b/src/app/api/v1/messages/conversations/with/[id]/route.ts @@ -0,0 +1,27 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Params = z.object({ + id: z.coerce.number(), +}); + +export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + try { + const { id } = Params.parse(await params); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.message.getConversation({ userId: id }); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/messages/route.ts b/src/app/api/v1/messages/route.ts new file mode 100644 index 0000000..29dc909 --- /dev/null +++ b/src/app/api/v1/messages/route.ts @@ -0,0 +1,29 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const SendBody = z.object({ + recipientId: z.number(), + content: z.string().min(1).max(500), +}); + +export async function POST(req: NextRequest) { + try { + const json = await req.json(); + const input = SendBody.parse(json); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.message.sendMessage(input); + return NextResponse.json(data, { status: 201 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/messages/unread-count/route.ts b/src/app/api/v1/messages/unread-count/route.ts new file mode 100644 index 0000000..c9276cf --- /dev/null +++ b/src/app/api/v1/messages/unread-count/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +export async function GET(req: NextRequest) { + try { + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.message.getUnreadCount(); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/nfts/collections/[id]/nfts/route.ts b/src/app/api/v1/nfts/collections/[id]/nfts/route.ts new file mode 100644 index 0000000..ff7772e --- /dev/null +++ b/src/app/api/v1/nfts/collections/[id]/nfts/route.ts @@ -0,0 +1,40 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Params = z.object({ + id: z.coerce.number(), +}); + +const Query = z.object({ + limit: z.coerce.number().min(1).max(100).default(50), + cursor: z.coerce.number().nullish(), +}); + +export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + try { + const { id } = Params.parse(await params); + const url = new URL(req.url); + const query = Query.parse({ + limit: url.searchParams.get("limit") ?? undefined, + cursor: url.searchParams.get("cursor") ?? undefined, + }); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.nft.getCollectionNFTs({ + collectionId: id, + ...query, + }); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/nfts/collections/[id]/route.ts b/src/app/api/v1/nfts/collections/[id]/route.ts new file mode 100644 index 0000000..4acd8ee --- /dev/null +++ b/src/app/api/v1/nfts/collections/[id]/route.ts @@ -0,0 +1,53 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Params = z.object({ + id: z.coerce.number(), +}); + +const UpdateBody = z.object({ + name: z.string().min(3).max(255), + symbol: z.string().min(2).max(10), + description: z.string().max(1000).nullable(), +}); + +export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + try { + const { id } = Params.parse(await params); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.nft.getCollectionById({ collectionId: id }); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} + +export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + try { + const { id } = Params.parse(await params); + const json = await req.json(); + const input = UpdateBody.parse(json); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.nft.updateCollection({ + id, + ...input, + }); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/nfts/collections/route.ts b/src/app/api/v1/nfts/collections/route.ts new file mode 100644 index 0000000..e69b785 --- /dev/null +++ b/src/app/api/v1/nfts/collections/route.ts @@ -0,0 +1,56 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const ListQuery = z.object({ + type: z.enum(["all", "my"]), + limit: z.coerce.number().min(1).max(100).default(50), + cursor: z.coerce.number().nullish(), +}); + +const CreateBody = z.object({ + name: z.string().min(3).max(255), + symbol: z.string().min(2).max(10), + description: z.string().max(1000).nullable(), +}); + +export async function GET(req: NextRequest) { + try { + const url = new URL(req.url); + const input = ListQuery.parse({ + type: url.searchParams.get("type") ?? undefined, + limit: url.searchParams.get("limit") ?? undefined, + cursor: url.searchParams.get("cursor") ?? undefined, + }); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.nft.getCollections(input); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} + +export async function POST(req: NextRequest) { + try { + const json = await req.json(); + const input = CreateBody.parse(json); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.nft.createCollection(input); + return NextResponse.json(data, { status: 201 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/nfts/me/route.ts b/src/app/api/v1/nfts/me/route.ts new file mode 100644 index 0000000..8e9039c --- /dev/null +++ b/src/app/api/v1/nfts/me/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +export async function GET(req: NextRequest) { + try { + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.nft.getMyNFTs(); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/nfts/mint/route.ts b/src/app/api/v1/nfts/mint/route.ts new file mode 100644 index 0000000..cbf29e9 --- /dev/null +++ b/src/app/api/v1/nfts/mint/route.ts @@ -0,0 +1,29 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const MintBody = z.object({ + postId: z.number(), + collectionId: z.number().optional(), +}); + +export async function POST(req: NextRequest) { + try { + const json = await req.json(); + const input = MintBody.parse(json); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.nft.mintPostAsNFT(input); + return NextResponse.json(data, { status: 201 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/nfts/users/[id]/collections/route.ts b/src/app/api/v1/nfts/users/[id]/collections/route.ts new file mode 100644 index 0000000..6d0aff2 --- /dev/null +++ b/src/app/api/v1/nfts/users/[id]/collections/route.ts @@ -0,0 +1,40 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Params = z.object({ + id: z.coerce.number(), +}); + +const Query = z.object({ + limit: z.coerce.number().min(1).max(100).default(50), + cursor: z.coerce.number().nullish(), +}); + +export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + try { + const { id } = Params.parse(await params); + const url = new URL(req.url); + const query = Query.parse({ + limit: url.searchParams.get("limit") ?? undefined, + cursor: url.searchParams.get("cursor") ?? undefined, + }); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.nft.getUserCollections({ + userId: id, + ...query, + }); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/notifications/[id]/read/route.ts b/src/app/api/v1/notifications/[id]/read/route.ts new file mode 100644 index 0000000..c813108 --- /dev/null +++ b/src/app/api/v1/notifications/[id]/read/route.ts @@ -0,0 +1,27 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Params = z.object({ + id: z.coerce.number(), +}); + +export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + try { + const { id } = Params.parse(await params); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + await caller.notification.markAsRead({ notificationId: id }); + return NextResponse.json({ success: true }, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/notifications/read-all/route.ts b/src/app/api/v1/notifications/read-all/route.ts new file mode 100644 index 0000000..819ccb5 --- /dev/null +++ b/src/app/api/v1/notifications/read-all/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +export async function POST(req: NextRequest) { + try { + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + await caller.notification.markAllAsRead(); + return NextResponse.json({ success: true }, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/notifications/route.ts b/src/app/api/v1/notifications/route.ts new file mode 100644 index 0000000..3025026 --- /dev/null +++ b/src/app/api/v1/notifications/route.ts @@ -0,0 +1,32 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Query = z.object({ + limit: z.coerce.number().min(1).max(100).default(50), + cursor: z.coerce.number().default(0), +}); + +export async function GET(req: NextRequest) { + try { + const url = new URL(req.url); + const input = Query.parse({ + limit: url.searchParams.get("limit") ?? undefined, + cursor: url.searchParams.get("cursor") ?? undefined, + }); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.notification.list(input); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/notifications/unread-count/route.ts b/src/app/api/v1/notifications/unread-count/route.ts new file mode 100644 index 0000000..fe9cb01 --- /dev/null +++ b/src/app/api/v1/notifications/unread-count/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +export async function GET(req: NextRequest) { + try { + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.notification.getUnreadCount(); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/points/history/route.ts b/src/app/api/v1/points/history/route.ts new file mode 100644 index 0000000..d68b620 --- /dev/null +++ b/src/app/api/v1/points/history/route.ts @@ -0,0 +1,34 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Query = z.object({ + limit: z.coerce.number().min(1).max(100).default(20), + cursor: z.coerce.number().default(0), + activityType: z.enum(["like", "post", "streak", "badge", "follow", "comment"]).optional(), +}); + +export async function GET(req: NextRequest) { + try { + const url = new URL(req.url); + const input = Query.parse({ + limit: url.searchParams.get("limit") ?? undefined, + cursor: url.searchParams.get("cursor") ?? undefined, + activityType: url.searchParams.get("activityType") ?? undefined, + }); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.points.getPointsHistory(input); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/points/leaderboard/route.ts b/src/app/api/v1/points/leaderboard/route.ts new file mode 100644 index 0000000..114559d --- /dev/null +++ b/src/app/api/v1/points/leaderboard/route.ts @@ -0,0 +1,32 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Query = z.object({ + limit: z.coerce.number().min(1).max(100).default(10), + timeframe: z.enum(["all", "week", "month"]).default("all"), +}); + +export async function GET(req: NextRequest) { + try { + const url = new URL(req.url); + const input = Query.parse({ + limit: url.searchParams.get("limit") ?? undefined, + timeframe: url.searchParams.get("timeframe") ?? undefined, + }); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.points.getLeaderboard(input); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/points/route.ts b/src/app/api/v1/points/route.ts new file mode 100644 index 0000000..ed406c1 --- /dev/null +++ b/src/app/api/v1/points/route.ts @@ -0,0 +1,43 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const AddPointsBody = z.object({ + activityType: z.enum(["like", "post", "streak", "badge", "follow", "comment"]), + points: z.number().min(1).max(100), + metadata: z.record(z.any()).optional(), +}); + +export async function GET(req: NextRequest) { + try { + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.points.getUserPoints(); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} + +export async function POST(req: NextRequest) { + try { + const json = await req.json(); + const input = AddPointsBody.parse(json); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.points.addPoints(input); + return NextResponse.json(data, { status: 201 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/points/stats/route.ts b/src/app/api/v1/points/stats/route.ts new file mode 100644 index 0000000..294752c --- /dev/null +++ b/src/app/api/v1/points/stats/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +export async function GET(req: NextRequest) { + try { + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.points.getPointsStats(); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/posts/[id]/like/route.ts b/src/app/api/v1/posts/[id]/like/route.ts new file mode 100644 index 0000000..5aa5bee --- /dev/null +++ b/src/app/api/v1/posts/[id]/like/route.ts @@ -0,0 +1,42 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Params = z.object({ + id: z.coerce.number(), +}); + +export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + try { + const { id } = Params.parse(await params); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + await caller.post.like({ postId: id }); + return NextResponse.json({ success: true }, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} + +export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + try { + const { id } = Params.parse(await params); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + await caller.post.unlike({ postId: id }); + return NextResponse.json({ success: true }, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/posts/[id]/replies/route.ts b/src/app/api/v1/posts/[id]/replies/route.ts new file mode 100644 index 0000000..71f25a0 --- /dev/null +++ b/src/app/api/v1/posts/[id]/replies/route.ts @@ -0,0 +1,66 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Params = z.object({ + id: z.coerce.number(), +}); + +const ListQuery = z.object({ + limit: z.coerce.number().min(1).max(100).default(20), + cursor: z.coerce.number().default(0), +}); + +const ReplyBody = z.object({ + content: z.string().min(1).max(280), + image: z.string().nullable().optional(), +}); + +export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + try { + const { id } = Params.parse(await params); + const url = new URL(req.url); + const query = ListQuery.parse({ + limit: url.searchParams.get("limit") ?? undefined, + cursor: url.searchParams.get("cursor") ?? undefined, + }); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.post.getReplies({ + postId: id, + ...query, + }); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} + +export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + try { + const { id } = Params.parse(await params); + const json = await req.json(); + const input = ReplyBody.parse(json); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.post.reply({ + replyToId: id, + ...input, + }); + + return NextResponse.json(data, { status: 201 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/posts/[id]/repost/route.ts b/src/app/api/v1/posts/[id]/repost/route.ts new file mode 100644 index 0000000..ebadc62 --- /dev/null +++ b/src/app/api/v1/posts/[id]/repost/route.ts @@ -0,0 +1,42 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Params = z.object({ + id: z.coerce.number(), +}); + +export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + try { + const { id } = Params.parse(await params); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + await caller.post.repost({ postId: id }); + return NextResponse.json({ success: true }, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} + +export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + try { + const { id } = Params.parse(await params); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + await caller.post.unrepost({ postId: id }); + return NextResponse.json({ success: true }, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/posts/[id]/route.ts b/src/app/api/v1/posts/[id]/route.ts new file mode 100644 index 0000000..b84d214 --- /dev/null +++ b/src/app/api/v1/posts/[id]/route.ts @@ -0,0 +1,42 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Params = z.object({ + id: z.coerce.number(), +}); + +export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + try { + const { id } = Params.parse(await params); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.post.getById({ postId: id }); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} + +export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + try { + const { id } = Params.parse(await params); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.post.delete({ postId: id }); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/posts/[id]/save/route.ts b/src/app/api/v1/posts/[id]/save/route.ts new file mode 100644 index 0000000..1a05027 --- /dev/null +++ b/src/app/api/v1/posts/[id]/save/route.ts @@ -0,0 +1,42 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Params = z.object({ + id: z.coerce.number(), +}); + +export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + try { + const { id } = Params.parse(await params); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + await caller.post.save({ postId: id }); + return NextResponse.json({ success: true }, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} + +export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + try { + const { id } = Params.parse(await params); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + await caller.post.unsave({ postId: id }); + return NextResponse.json({ success: true }, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/posts/bookmarks/route.ts b/src/app/api/v1/posts/bookmarks/route.ts new file mode 100644 index 0000000..c38548d --- /dev/null +++ b/src/app/api/v1/posts/bookmarks/route.ts @@ -0,0 +1,32 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Query = z.object({ + limit: z.coerce.number().min(1).max(100).default(20), + cursor: z.coerce.number().default(0), +}); + +export async function GET(req: NextRequest) { + try { + const url = new URL(req.url); + const input = Query.parse({ + limit: url.searchParams.get("limit") ?? undefined, + cursor: url.searchParams.get("cursor") ?? undefined, + }); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.post.bookmarks(input); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/posts/route.ts b/src/app/api/v1/posts/route.ts new file mode 100644 index 0000000..19aab94 --- /dev/null +++ b/src/app/api/v1/posts/route.ts @@ -0,0 +1,110 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const FeedQuery = z.object({ + type: z.enum(["for-you", "following", "user", "replies", "interests"]).default("for-you"), + userId: z.coerce.number().optional(), + limit: z.coerce.number().min(1).max(100).default(20), + cursor: z.coerce.number().default(0), +}); + +const CreateBody = z.object({ + content: z.string().min(1).max(280), + image: z.string().nullable().optional(), +}); + +export async function GET(req: NextRequest) { + try { + const url = new URL(req.url); + const input = FeedQuery.parse({ + type: url.searchParams.get("type") ?? undefined, + userId: url.searchParams.get("userId") ?? undefined, + limit: url.searchParams.get("limit") ?? undefined, + cursor: url.searchParams.get("cursor") ?? undefined, + }); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.post.feed(input); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} + +import { TRPCError } from "@trpc/server"; + +// TODO: kendi storage'ına göre doldur +async function uploadImageToStorage(file: File): Promise { + // güvenlik: type/size kontrolü + const maxBytes = 5 * 1024 * 1024; + if (!file.type.startsWith("image/")) { + throw new TRPCError({ code: "BAD_REQUEST", message: "image must be an image file" }); + } + if (file.size > maxBytes) { + throw new TRPCError({ code: "BAD_REQUEST", message: "image too large" }); + } + + // File -> Buffer + const arrayBuffer = await file.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + + // burada buffer'ı S3/MinIO/Cloudinary'ye upload edip public URL döndür + // return uploadedUrl; + + throw new Error("uploadImageToStorage not implemented"); +} + +export async function POST(req: NextRequest) { + try { + const contentType = req.headers.get("content-type") ?? ""; + + let payload: { content: string; image?: string | null }; + + if (contentType.includes("multipart/form-data")) { + const fd = await req.formData(); + + const content = String(fd.get("content") ?? ""); + const imageField = fd.get("image"); + + let imageUrl: string | null = null; + + // image alanı File ise upload et + if (imageField && imageField instanceof File && imageField.size > 0) { + imageUrl = await uploadImageToStorage(imageField); + } else if (typeof imageField === "string") { + // bazı client’lar URL string gönderebilir + imageUrl = imageField || null; + } + + payload = { content, image: imageUrl }; + } else if (contentType.includes("application/json")) { + const json = await req.json(); + payload = json; + } else { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `Unsupported Content-Type: ${contentType}`, + }); + } + + const input = CreateBody.parse(payload); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.post.create(input); + return NextResponse.json(data, { status: 201 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/posts/search/route.ts b/src/app/api/v1/posts/search/route.ts new file mode 100644 index 0000000..ee6678e --- /dev/null +++ b/src/app/api/v1/posts/search/route.ts @@ -0,0 +1,34 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Query = z.object({ + query: z.string().min(1), + limit: z.coerce.number().min(1).max(100).default(20), + cursor: z.coerce.number().default(0), +}); + +export async function GET(req: NextRequest) { + try { + const url = new URL(req.url); + const input = Query.parse({ + query: url.searchParams.get("query") ?? undefined, + limit: url.searchParams.get("limit") ?? undefined, + cursor: url.searchParams.get("cursor") ?? undefined, + }); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.post.search(input); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/posts/trending-hashtags/route.ts b/src/app/api/v1/posts/trending-hashtags/route.ts new file mode 100644 index 0000000..1c1bb2b --- /dev/null +++ b/src/app/api/v1/posts/trending-hashtags/route.ts @@ -0,0 +1,30 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Query = z.object({ + limit: z.coerce.number().min(1).max(10).default(5), +}); + +export async function GET(req: NextRequest) { + try { + const url = new URL(req.url); + const input = Query.parse({ + limit: url.searchParams.get("limit") ?? undefined, + }); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.post.getTrendingHashtags(input); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/referrals/accept/route.ts b/src/app/api/v1/referrals/accept/route.ts new file mode 100644 index 0000000..fac05a0 --- /dev/null +++ b/src/app/api/v1/referrals/accept/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Body = z.object({ + referralCode: z.string().min(1), +}); + +export async function POST(req: NextRequest) { + try { + const json = await req.json(); + const input = Body.parse(json); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.referral.createReferral(input); + return NextResponse.json(data, { status: 201 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/referrals/code/route.ts b/src/app/api/v1/referrals/code/route.ts new file mode 100644 index 0000000..02d230e --- /dev/null +++ b/src/app/api/v1/referrals/code/route.ts @@ -0,0 +1,33 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +export async function GET(req: NextRequest) { + try { + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.referral.getReferralCode(); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} + +export async function POST(req: NextRequest) { + try { + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.referral.generateReferralCode(); + return NextResponse.json(data, { status: 201 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/referrals/lookup/route.ts b/src/app/api/v1/referrals/lookup/route.ts new file mode 100644 index 0000000..2eda020 --- /dev/null +++ b/src/app/api/v1/referrals/lookup/route.ts @@ -0,0 +1,30 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Query = z.object({ + referralCode: z.string().min(1), +}); + +export async function GET(req: NextRequest) { + try { + const url = new URL(req.url); + const input = Query.parse({ + referralCode: url.searchParams.get("referralCode") ?? undefined, + }); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.referral.getUserByReferralCode(input); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/referrals/stats/route.ts b/src/app/api/v1/referrals/stats/route.ts new file mode 100644 index 0000000..e8078ad --- /dev/null +++ b/src/app/api/v1/referrals/stats/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +export async function GET(req: NextRequest) { + try { + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.referral.getReferralStats(); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/streak/activities/route.ts b/src/app/api/v1/streak/activities/route.ts new file mode 100644 index 0000000..8cda927 --- /dev/null +++ b/src/app/api/v1/streak/activities/route.ts @@ -0,0 +1,30 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Query = z.object({ + limit: z.coerce.number().min(1).max(50).default(10), +}); + +export async function GET(req: NextRequest) { + try { + const url = new URL(req.url); + const input = Query.parse({ + limit: url.searchParams.get("limit") ?? undefined, + }); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.streak.getRecentActivities(input); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/streak/route.ts b/src/app/api/v1/streak/route.ts new file mode 100644 index 0000000..ca8f168 --- /dev/null +++ b/src/app/api/v1/streak/route.ts @@ -0,0 +1,33 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +export async function GET(req: NextRequest) { + try { + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.streak.getStreak(); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} + +export async function POST(req: NextRequest) { + try { + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.streak.updateStreak(); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/streak/stats/route.ts b/src/app/api/v1/streak/stats/route.ts new file mode 100644 index 0000000..117f6cb --- /dev/null +++ b/src/app/api/v1/streak/stats/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +export async function GET(req: NextRequest) { + try { + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.streak.getStreakStats(); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/unread-counts/route.ts b/src/app/api/v1/unread-counts/route.ts new file mode 100644 index 0000000..07f321c --- /dev/null +++ b/src/app/api/v1/unread-counts/route.ts @@ -0,0 +1,29 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +export async function GET(req: NextRequest) { + try { + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const [messagesCount, notificationsCount] = await Promise.all([ + caller.message.getUnreadCount(), + caller.notification.getUnreadCount(), + ]); + + const data = { + messages_unread_count: messagesCount, + notifications_unread_count: notificationsCount, + }; + + return NextResponse.json({ data }, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/users/[username]/followers/route.ts b/src/app/api/v1/users/[username]/followers/route.ts new file mode 100644 index 0000000..cc793af --- /dev/null +++ b/src/app/api/v1/users/[username]/followers/route.ts @@ -0,0 +1,41 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Params = z.object({ + username: z.string().min(1), +}); + +const Query = z.object({ + limit: z.coerce.number().min(1).max(100).default(50), + cursor: z.coerce.number().nullish(), +}); + +export async function GET(req: NextRequest, { params }: { params: Promise<{ username: string }> }) { + try { + const { username } = Params.parse(await params); + const url = new URL(req.url); + const query = Query.parse({ + limit: url.searchParams.get("limit") ?? undefined, + cursor: url.searchParams.get("cursor") ?? undefined, + }); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.user.getFollowers({ + username, + ...query, + }); + + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/users/[username]/following/route.ts b/src/app/api/v1/users/[username]/following/route.ts new file mode 100644 index 0000000..9ebae8a --- /dev/null +++ b/src/app/api/v1/users/[username]/following/route.ts @@ -0,0 +1,41 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Params = z.object({ + username: z.string().min(1), +}); + +const Query = z.object({ + limit: z.coerce.number().min(1).max(100).default(50), + cursor: z.coerce.number().nullish(), +}); + +export async function GET(req: NextRequest, { params }: { params: Promise<{ username: string }> }) { + try { + const { username } = Params.parse(await params); + const url = new URL(req.url); + const query = Query.parse({ + limit: url.searchParams.get("limit") ?? undefined, + cursor: url.searchParams.get("cursor") ?? undefined, + }); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.user.getFollowing({ + username, + ...query, + }); + + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/users/[username]/route.ts b/src/app/api/v1/users/[username]/route.ts new file mode 100644 index 0000000..3937985 --- /dev/null +++ b/src/app/api/v1/users/[username]/route.ts @@ -0,0 +1,27 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Params = z.object({ + username: z.string().min(1), +}); + +export async function GET(req: NextRequest, { params }: { params: Promise<{ username: string }> }) { + try { + const { username } = Params.parse(await params); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.user.getProfileByUsername({ username }); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/users/[username]/transactions/refresh/route.ts b/src/app/api/v1/users/[username]/transactions/refresh/route.ts new file mode 100644 index 0000000..2cb2623 --- /dev/null +++ b/src/app/api/v1/users/[username]/transactions/refresh/route.ts @@ -0,0 +1,27 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Params = z.object({ + username: z.string().min(1), +}); + +export async function POST(req: NextRequest, { params }: { params: Promise<{ username: string }> }) { + try { + const { username } = Params.parse(await params); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.user.updateTransactionCount({ userUsername: username }); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/users/id/[id]/follow/route.ts b/src/app/api/v1/users/id/[id]/follow/route.ts new file mode 100644 index 0000000..a3acb04 --- /dev/null +++ b/src/app/api/v1/users/id/[id]/follow/route.ts @@ -0,0 +1,42 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Params = z.object({ + id: z.coerce.number(), +}); + +export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + try { + const { id } = Params.parse(await params); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + await caller.user.follow({ userId: id }); + return NextResponse.json({ success: true }, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} + +export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + try { + const { id } = Params.parse(await params); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + await caller.user.unfollow({ userId: id }); + return NextResponse.json({ success: true }, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/users/id/[id]/route.ts b/src/app/api/v1/users/id/[id]/route.ts new file mode 100644 index 0000000..67ce3db --- /dev/null +++ b/src/app/api/v1/users/id/[id]/route.ts @@ -0,0 +1,27 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Params = z.object({ + id: z.coerce.number(), +}); + +export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + try { + const { id } = Params.parse(await params); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.user.getById({ userId: id }); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/users/landing-stats/route.ts b/src/app/api/v1/users/landing-stats/route.ts new file mode 100644 index 0000000..90fec62 --- /dev/null +++ b/src/app/api/v1/users/landing-stats/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +export async function GET(req: NextRequest) { + try { + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.user.getLandingStats(); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/users/me/profile/route.ts b/src/app/api/v1/users/me/profile/route.ts new file mode 100644 index 0000000..28c3ee2 --- /dev/null +++ b/src/app/api/v1/users/me/profile/route.ts @@ -0,0 +1,39 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const UpdateProfileBody = z.object({ + username: z.string() + .min(3, "Username must be at least 3 characters") + .max(20, "Username must be less than 20 characters") + .regex( + /^[a-zA-Z0-9_]+$/, + "Username can only contain letters, numbers and underscores" + ), + bio: z.string() + .max(160, "Bio must be less than 160 characters") + .nullable(), + image: z.string().nullable(), + cover: z.string().nullable(), +}); + +export async function PATCH(req: NextRequest) { + try { + const json = await req.json(); + const input = UpdateProfileBody.parse(json); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.user.updateProfile(input); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/users/me/wallet/route.ts b/src/app/api/v1/users/me/wallet/route.ts new file mode 100644 index 0000000..2bb5d5a --- /dev/null +++ b/src/app/api/v1/users/me/wallet/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const UpdateWalletBody = z.object({ + walletAddress: z.string().nullable(), +}); + +export async function PATCH(req: NextRequest) { + try { + const json = await req.json(); + const input = UpdateWalletBody.parse(json); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.user.updateWalletAddress(input); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/users/random/route.ts b/src/app/api/v1/users/random/route.ts new file mode 100644 index 0000000..2f78dfa --- /dev/null +++ b/src/app/api/v1/users/random/route.ts @@ -0,0 +1,30 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Query = z.object({ + limit: z.coerce.number().min(1).max(10).default(3), +}); + +export async function GET(req: NextRequest) { + try { + const url = new URL(req.url); + const input = Query.parse({ + limit: url.searchParams.get("limit") ?? undefined, + }); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.user.getRandomUsers(input); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/users/search/route.ts b/src/app/api/v1/users/search/route.ts new file mode 100644 index 0000000..700ec6f --- /dev/null +++ b/src/app/api/v1/users/search/route.ts @@ -0,0 +1,32 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Query = z.object({ + query: z.string().min(1).max(50), + limit: z.coerce.number().min(1).max(20).default(5), +}); + +export async function GET(req: NextRequest) { + try { + const url = new URL(req.url); + const input = Query.parse({ + query: url.searchParams.get("query") ?? undefined, + limit: url.searchParams.get("limit") ?? undefined, + }); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.user.search(input); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/users/suggestions/route.ts b/src/app/api/v1/users/suggestions/route.ts new file mode 100644 index 0000000..8ed1d5c --- /dev/null +++ b/src/app/api/v1/users/suggestions/route.ts @@ -0,0 +1,30 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +const Query = z.object({ + limit: z.coerce.number().min(1).max(10).default(3), +}); + +export async function GET(req: NextRequest) { + try { + const url = new URL(req.url); + const input = Query.parse({ + limit: url.searchParams.get("limit") ?? undefined, + }); + + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.user.getRandomSuggestions(input); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/v1/users/top-crypto/route.ts b/src/app/api/v1/users/top-crypto/route.ts new file mode 100644 index 0000000..0d19256 --- /dev/null +++ b/src/app/api/v1/users/top-crypto/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { appRouter } from "@/server/api/routers"; +import { createTRPCContext } from "@/server/api/trpc"; +import { toHttpError } from "@/server/http/trpc-to-http"; + +export const runtime = "nodejs"; + +export async function GET(req: NextRequest) { + try { + const ctx = await createTRPCContext(req); + const caller = appRouter.createCaller(ctx); + + const data = await caller.user.getTopCryptoAccounts(); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + const { status, body } = toHttpError(err); + return NextResponse.json(body, { status }); + } +} diff --git a/src/lib/chopin-auth.ts b/src/lib/chopin-auth.ts new file mode 100644 index 0000000..c105490 --- /dev/null +++ b/src/lib/chopin-auth.ts @@ -0,0 +1,86 @@ +import { createClient } from "@openauthjs/openauth/client"; +import { createSubjects } from "@openauthjs/openauth/subject"; +import { object, string } from "valibot"; + +const CHOPIN_CLIENT_ID = process.env.CHOPIN_CLIENT_ID ?? "prealpha"; +const CHOPIN_ISSUER = + process.env.CHOPIN_ISSUER ?? "https://prealpha-login.chopin.sh"; + +export const chopinSubjects = createSubjects({ + user: object({ + id: string(), + }), +}); + +export const chopinClient = createClient({ + clientID: CHOPIN_CLIENT_ID, + issuer: CHOPIN_ISSUER, +}); + +export async function authorizeWithChopin( + redirectUri: string, + opts?: { provider?: string } +) { + const result = await chopinClient.authorize(redirectUri, "code", { + pkce: true, + provider: opts?.provider, + }); + + return result; +} + +export async function exchangeAuthorizationCode( + code: string, + redirectUri: string, + verifier?: string +) { + const result = await chopinClient.exchange(code, redirectUri, verifier); + + if (result.err) { + throw new Error("Invalid authorization code"); + } + + return result.tokens; +} + +export function extractBearerToken(header: string | null): string | null { + if (!header) { + return null; + } + + const match = header.match(/^Bearer\s+(.+)$/i); + return match?.[1]?.trim() ?? null; +} + +export async function verifyAccessToken(token: string) { + try { + const verified = await chopinClient.verify(chopinSubjects, token); + + if (verified.err || !verified.subject) { + return null; + } + + const address = verified.subject.properties?.id; + if (!address) { + return null; + } + + return { address }; + } catch { + return null; + } +} + +export async function resolveAddress(req?: Request) { + if (!req) { + return null; + } + + const token = extractBearerToken(req.headers.get("authorization")); + if (!token) { + return null; + } + + const verified = await verifyAccessToken(token); + return verified?.address ?? null; +} diff --git a/src/lib/session.ts b/src/lib/session.ts index 9244db0..8229eb5 100644 --- a/src/lib/session.ts +++ b/src/lib/session.ts @@ -1,5 +1,5 @@ -import { getAddress } from '@chopinframework/next'; -import { eq } from "drizzle-orm"; +import { getAddress } from "@chopinframework/next"; +import { resolveAddress } from "./chopin-auth"; import { db } from "./db"; import { users } from "./db/schema"; @@ -9,18 +9,14 @@ function generateRandomUsername(): string { .join(''); } -export async function validateRequest() { +export async function validateRequest(req?: Request) { try { - const address = await getAddress(); + const address = (await resolveAddress(req)) ?? (await getAddress()); if (!address) { return null; } - const existingUser = await db.query.users.findFirst({ - where: eq(users.address, address), - }); - const [user] = await db .insert(users) .values({ @@ -46,4 +42,4 @@ export async function validateRequest() { } throw new Error('Authentication failed'); } -} \ No newline at end of file +} diff --git a/src/server/api/routers/post.ts b/src/server/api/routers/post.ts index bc3fdcf..1d64578 100644 --- a/src/server/api/routers/post.ts +++ b/src/server/api/routers/post.ts @@ -167,39 +167,54 @@ export const postRouter = createTRPCRouter({ throw new TRPCError({ code: "NOT_FOUND" }); } - await ctx.db.insert(likes).values({ - postId: input.postId, - userId: ctx.session.user.id, - }); - - await ctx.db.insert(userActivities).values({ - userId: ctx.session.user.id, - activityType: "like", - points: 1, - metadata: JSON.stringify({ postId: input.postId, postAuthor: post.authorId }), + const existingLike = await ctx.db.query.likes.findFirst({ + where: and( + eq(likes.postId, input.postId), + eq(likes.userId, ctx.session.user.id) + ), }); - const likesCount = await ctx.db - .select({ count: sql`count(*)` }) - .from(userActivities) - .where( - and( - eq(userActivities.userId, ctx.session.user.id), - eq(userActivities.activityType, "like") - ) - ); - - const totalLikes = likesCount[0]?.count || 0; - await checkAndAwardBadges(ctx.db, ctx.session.user.id, "likes", totalLikes); - - if (post.authorId !== ctx.session.user.id) { - await createNotification(ctx.db, { - userId: post.authorId, - actorId: ctx.session.user.id, - type: "like", - targetId: input.postId, - targetType: "post", + if (existingLike) { + await ctx.db.delete(likes).where(and( + eq(likes.postId, input.postId), + eq(likes.userId, ctx.session.user.id) + )); + }else{ + await ctx.db.insert(likes).values({ + postId: input.postId, + userId: ctx.session.user.id, }); + + await ctx.db.insert(userActivities).values({ + userId: ctx.session.user.id, + activityType: "like", + points: 1, + metadata: JSON.stringify({ postId: input.postId, postAuthor: post.authorId }), + }); + + const likesCount = await ctx.db + .select({ count: sql`count(*)` }) + .from(userActivities) + .where( + and( + eq(userActivities.userId, ctx.session.user.id), + eq(userActivities.activityType, "like") + ) + ); + + const totalLikes = likesCount[0]?.count || 0; + await checkAndAwardBadges(ctx.db, ctx.session.user.id, "likes", totalLikes); + + if (post.authorId !== ctx.session.user.id) { + await createNotification(ctx.db, { + userId: post.authorId, + actorId: ctx.session.user.id, + type: "like", + targetId: input.postId, + targetType: "post", + }); + } + } }), diff --git a/src/server/api/trpc.ts b/src/server/api/trpc.ts index 59e9986..e527945 100644 --- a/src/server/api/trpc.ts +++ b/src/server/api/trpc.ts @@ -3,8 +3,8 @@ import { validateRequest } from "@/lib/session"; import { TRPCError, initTRPC } from "@trpc/server"; import superjson from 'superjson'; -export const createTRPCContext = async (req: Request) => { - const session = await validateRequest(); +export const createTRPCContext = async (req?: Request) => { + const session = await validateRequest(req); return { session, diff --git a/src/server/http/trpc-to-http.ts b/src/server/http/trpc-to-http.ts new file mode 100644 index 0000000..2811312 --- /dev/null +++ b/src/server/http/trpc-to-http.ts @@ -0,0 +1,30 @@ +import { TRPCError } from "@trpc/server"; + +const codeToStatus: Record = { + BAD_REQUEST: 400, + UNAUTHORIZED: 401, + FORBIDDEN: 403, + NOT_FOUND: 404, + CONFLICT: 409, + TOO_MANY_REQUESTS: 429, + INTERNAL_SERVER_ERROR: 500, +}; + +export function toHttpError(err: unknown): { status: number; body: any } { + console.log(err); + + if (err instanceof SyntaxError) { + return { status: 400, body: { error: "BAD_REQUEST", message: "Invalid JSON body" } }; + } + + if (err && typeof err === "object" && "name" in err && (err as any).name === "ZodError") { + return { status: 400, body: { error: "BAD_REQUEST", details: (err as any).issues } }; + } + + if (err instanceof TRPCError) { + const status = codeToStatus[err.code] ?? 500; + return { status, body: { error: err.code, message: err.message } }; + } + + return { status: 500, body: { error: "INTERNAL_SERVER_ERROR" } }; +} diff --git a/tsconfig.json b/tsconfig.json index 0441073..b575f7d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve", + "jsx": "react-jsx", "incremental": true, "plugins": [ { @@ -32,9 +32,10 @@ "next-env.d.ts", "**/*.ts", "**/*.tsx", - ".next/types/**/*.ts" + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" ], "exclude": [ "node_modules" ] -} \ No newline at end of file +} From b43573a9943427c0b10cf8fb6edffa856c0e8d1b Mon Sep 17 00:00:00 2001 From: omeralpi Date: Mon, 2 Mar 2026 17:31:25 +0300 Subject: [PATCH 2/2] wip --- src/app/api/upload/route.ts | 13 +++-- src/app/api/v1/posts/[id]/replies/route.ts | 1 + src/app/api/v1/posts/route.ts | 52 ++----------------- .../id => users-id}/[id]/follow/route.ts | 0 .../v1/{users/id => users-id}/[id]/route.ts | 0 src/components/compose-form.tsx | 7 ++- src/components/edit-profile-dialog.tsx | 13 ++++- 7 files changed, 32 insertions(+), 54 deletions(-) rename src/app/api/v1/{users/id => users-id}/[id]/follow/route.ts (100%) rename src/app/api/v1/{users/id => users-id}/[id]/route.ts (100%) diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts index 5b1f21d..5fa9ef1 100644 --- a/src/app/api/upload/route.ts +++ b/src/app/api/upload/route.ts @@ -18,7 +18,7 @@ export async function POST(req: Request) { const formData = await req.formData(); const file = formData.get("file"); - if (!file || typeof file !== "object" || !("type" in file) || !("size" in file)) { + if (!(file instanceof File)) { return NextResponse.json( { error: "No file uploaded" }, { status: 400 } @@ -60,7 +60,14 @@ export async function POST(req: Request) { ); } - return NextResponse.json(data); + if (typeof data?.secure_url !== "string") { + return NextResponse.json( + { error: "Upload response did not include a URL" }, + { status: 502 } + ); + } + + return NextResponse.json({ url: data.secure_url }); } catch (error) { console.error("Upload error:", error); @@ -69,4 +76,4 @@ export async function POST(req: Request) { { status: 500 } ); } -} \ No newline at end of file +} diff --git a/src/app/api/v1/posts/[id]/replies/route.ts b/src/app/api/v1/posts/[id]/replies/route.ts index 71f25a0..79d9922 100644 --- a/src/app/api/v1/posts/[id]/replies/route.ts +++ b/src/app/api/v1/posts/[id]/replies/route.ts @@ -37,6 +37,7 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id: postId: id, ...query, }); + return NextResponse.json(data, { status: 200 }); } catch (err) { const { status, body } = toHttpError(err); diff --git a/src/app/api/v1/posts/route.ts b/src/app/api/v1/posts/route.ts index 19aab94..78f8c2d 100644 --- a/src/app/api/v1/posts/route.ts +++ b/src/app/api/v1/posts/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; +import { TRPCError } from "@trpc/server"; import { appRouter } from "@/server/api/routers"; import { createTRPCContext } from "@/server/api/trpc"; @@ -40,63 +41,18 @@ export async function GET(req: NextRequest) { } } -import { TRPCError } from "@trpc/server"; - -// TODO: kendi storage'ına göre doldur -async function uploadImageToStorage(file: File): Promise { - // güvenlik: type/size kontrolü - const maxBytes = 5 * 1024 * 1024; - if (!file.type.startsWith("image/")) { - throw new TRPCError({ code: "BAD_REQUEST", message: "image must be an image file" }); - } - if (file.size > maxBytes) { - throw new TRPCError({ code: "BAD_REQUEST", message: "image too large" }); - } - - // File -> Buffer - const arrayBuffer = await file.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - - // burada buffer'ı S3/MinIO/Cloudinary'ye upload edip public URL döndür - // return uploadedUrl; - - throw new Error("uploadImageToStorage not implemented"); -} - export async function POST(req: NextRequest) { try { const contentType = req.headers.get("content-type") ?? ""; - - let payload: { content: string; image?: string | null }; - - if (contentType.includes("multipart/form-data")) { - const fd = await req.formData(); - - const content = String(fd.get("content") ?? ""); - const imageField = fd.get("image"); - - let imageUrl: string | null = null; - - // image alanı File ise upload et - if (imageField && imageField instanceof File && imageField.size > 0) { - imageUrl = await uploadImageToStorage(imageField); - } else if (typeof imageField === "string") { - // bazı client’lar URL string gönderebilir - imageUrl = imageField || null; - } - - payload = { content, image: imageUrl }; - } else if (contentType.includes("application/json")) { - const json = await req.json(); - payload = json; - } else { + if (!contentType.includes("application/json")) { throw new TRPCError({ code: "BAD_REQUEST", message: `Unsupported Content-Type: ${contentType}`, }); } - const input = CreateBody.parse(payload); + const json = await req.json(); + const input = CreateBody.parse(json); const ctx = await createTRPCContext(req); const caller = appRouter.createCaller(ctx); diff --git a/src/app/api/v1/users/id/[id]/follow/route.ts b/src/app/api/v1/users-id/[id]/follow/route.ts similarity index 100% rename from src/app/api/v1/users/id/[id]/follow/route.ts rename to src/app/api/v1/users-id/[id]/follow/route.ts diff --git a/src/app/api/v1/users/id/[id]/route.ts b/src/app/api/v1/users-id/[id]/route.ts similarity index 100% rename from src/app/api/v1/users/id/[id]/route.ts rename to src/app/api/v1/users-id/[id]/route.ts diff --git a/src/components/compose-form.tsx b/src/components/compose-form.tsx index f43068e..a7d938d 100644 --- a/src/components/compose-form.tsx +++ b/src/components/compose-form.tsx @@ -92,7 +92,12 @@ export function ComposeForm({ throw new Error(data.error || 'Upload failed'); } - setImage(data.secure_url); + const imageUrl = data.url ?? data.secure_url; + if (typeof imageUrl !== "string") { + throw new Error("Upload response missing URL"); + } + + setImage(imageUrl); } catch (error) { toast.error('Image upload failed. Please try again.'); console.error('Upload error:', error); diff --git a/src/components/edit-profile-dialog.tsx b/src/components/edit-profile-dialog.tsx index 392586f..3a5fc39 100644 --- a/src/components/edit-profile-dialog.tsx +++ b/src/components/edit-profile-dialog.tsx @@ -62,7 +62,16 @@ const uploadFile = async (file: File): Promise => { }); const data = await response.json(); - return data.secure_url; + if (!response.ok) { + throw new Error(data.error || "Upload failed"); + } + + const imageUrl = data.url ?? data.secure_url; + if (typeof imageUrl !== "string") { + throw new Error("Upload response missing URL"); + } + + return imageUrl; }; export function EditProfileDialog({ @@ -293,4 +302,4 @@ export function EditProfileDialog({ ); -} \ No newline at end of file +}