feat(api): add user profile routes (public profile, me, sessions)

This commit is contained in:
2026-04-25 14:43:32 +08:00
parent 3ef9649322
commit d6dc701e3f
4 changed files with 135 additions and 6 deletions
+2
View File
@@ -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",
+106
View File
@@ -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<number>`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<number>`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
},
)
+3 -6
View File
@@ -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))
})
+24
View File
@@ -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<T> = {
data: T[]
total: number
limit: number
offset: number
}