diff --git a/package.json b/package.json index 8827460..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", @@ -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 aac3ef5..a7f200f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@next/third-parties': specifier: ^15.3.2 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) @@ -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 @@ -847,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'} @@ -3459,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==} @@ -3885,8 +3905,7 @@ snapshots: optional: true '@img/sharp-win32-ia32@0.34.5': - optional: true - + optional: truemain '@img/sharp-win32-x64@0.34.5': optional: true @@ -3996,6 +4015,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 @@ -6754,6 +6781,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/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/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..79d9922 --- /dev/null +++ b/src/app/api/v1/posts/[id]/replies/route.ts @@ -0,0 +1,67 @@ +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..78f8c2d --- /dev/null +++ b/src/app/api/v1/posts/route.ts @@ -0,0 +1,66 @@ +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"; +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 }); + } +} + +export async function POST(req: NextRequest) { + try { + const contentType = req.headers.get("content-type") ?? ""; + if (!contentType.includes("application/json")) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `Unsupported Content-Type: ${contentType}`, + }); + } + + const json = await req.json(); + const input = CreateBody.parse(json); + + 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-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/[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/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/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 +} 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" } }; +}