diff --git a/app/api/[[...slugs]]/route.ts b/app/api/[[...slugs]]/route.ts index ef1fa57..08b784e 100644 --- a/app/api/[[...slugs]]/route.ts +++ b/app/api/[[...slugs]]/route.ts @@ -2,6 +2,7 @@ import { Elysia } from "elysia" import { auth } from "@/lib/auth" import { rateLimit } from "@/lib/auth/rate-limit" import { healthRoutes } from "@/lib/api/health" +import { userRoutes } from "@/lib/api/user" const betterAuth = new Elysia({ name: "better-auth" }) .mount(auth.handler) @@ -35,6 +36,7 @@ export const app = new Elysia({ prefix: "/api" }) .use(rateLimit(60, 100)) .use(betterAuth) .use(healthRoutes) + .use(userRoutes) .get("/", () => ({ name: "DeckyVault API", version: "2026.0.1", diff --git a/lib/api/user.ts b/lib/api/user.ts new file mode 100644 index 0000000..0ef89ca --- /dev/null +++ b/lib/api/user.ts @@ -0,0 +1,106 @@ +import { Elysia, t } from "elysia" +import { auth } from "@/lib/auth" +import { db } from "@/lib/db/index" +import { user, performanceEntries } from "@/lib/db/schema" +import { eq, sql } from "drizzle-orm" +export const userRoutes = new Elysia({ prefix: "/user" }) + .get( + "/profile/:id", + async ({ params, set }) => { + const [profile] = await db + .select({ + id: user.id, + name: user.name, + image: user.image, + role: user.role, + createdAt: user.createdAt, + emailVerified: user.emailVerified, + }) + .from(user) + .where(eq(user.id, params.id)) + .limit(1) + + if (!profile) { + set.status = 404 + return { error: "User not found" } + } + + // Count contributions (performance entries) + const [{ count: contributions }] = await db + .select({ count: sql`count(*)::int` }) + .from(performanceEntries) + .where(eq(performanceEntries.userId, params.id)) + + // Reputation = contributions * 10 (simple formula for now) + const reputation = contributions * 10 + + return { + ...profile, + contributions, + reputation, + verified: profile.emailVerified, + // Hide email from public profiles + email: undefined, + emailVerified: undefined, + } + }, + { + params: t.Object({ + id: t.String(), + }), + }, + ) + .get( + "/me", + async ({ request, set }) => { + const session = await auth.api.getSession({ + headers: request.headers, + }) + + if (!session) { + set.status = 401 + return { error: "Unauthorized" } + } + + const [profile] = await db + .select() + .from(user) + .where(eq(user.id, session.user.id)) + .limit(1) + + if (!profile) { + set.status = 404 + return { error: "User not found" } + } + + const [{ count: contributions }] = await db + .select({ count: sql`count(*)::int` }) + .from(performanceEntries) + .where(eq(performanceEntries.userId, session.user.id)) + + return { + ...profile, + contributions, + reputation: contributions * 10, + } + }, + ) + .get( + "/me/sessions", + async ({ request, set }) => { + const session = await auth.api.getSession({ + headers: request.headers, + }) + + if (!session) { + set.status = 401 + return { error: "Unauthorized" } + } + + const sessions = await auth.api.listSessions({ + headers: request.headers, + }) + + return sessions + }, + ) diff --git a/lib/auth/rate-limit.ts b/lib/auth/rate-limit.ts index d55f7e2..5a7b170 100644 --- a/lib/auth/rate-limit.ts +++ b/lib/auth/rate-limit.ts @@ -69,10 +69,7 @@ export const rateLimit = ( } // These headers are informational — clients can use them to throttle - set.headers = { - ...set.headers, - "X-RateLimit-Limit": String(max), - "X-RateLimit-Remaining": String(result.remaining), - "X-RateLimit-Reset": String(Math.ceil(result.resetAt / 1000)), - } + set.headers["X-RateLimit-Limit"] = String(max) + set.headers["X-RateLimit-Remaining"] = String(result.remaining) + set.headers["X-RateLimit-Reset"] = String(Math.ceil(result.resetAt / 1000)) }) diff --git a/types/api.ts b/types/api.ts new file mode 100644 index 0000000..45a86ee --- /dev/null +++ b/types/api.ts @@ -0,0 +1,24 @@ +export type ApiError = { + error: string + code?: string + status: number +} + +export type UserProfile = { + id: string + name: string + email: string + image: string | null + role: string | null + createdAt: string + contributions: number + reputation: number + verified: boolean +} + +export type PaginatedResponse = { + data: T[] + total: number + limit: number + offset: number +}