feat(api): add user profile routes (public profile, me, sessions)
This commit is contained in:
@@ -2,6 +2,7 @@ import { Elysia } from "elysia"
|
|||||||
import { auth } from "@/lib/auth"
|
import { auth } from "@/lib/auth"
|
||||||
import { rateLimit } from "@/lib/auth/rate-limit"
|
import { rateLimit } from "@/lib/auth/rate-limit"
|
||||||
import { healthRoutes } from "@/lib/api/health"
|
import { healthRoutes } from "@/lib/api/health"
|
||||||
|
import { userRoutes } from "@/lib/api/user"
|
||||||
|
|
||||||
const betterAuth = new Elysia({ name: "better-auth" })
|
const betterAuth = new Elysia({ name: "better-auth" })
|
||||||
.mount(auth.handler)
|
.mount(auth.handler)
|
||||||
@@ -35,6 +36,7 @@ export const app = new Elysia({ prefix: "/api" })
|
|||||||
.use(rateLimit(60, 100))
|
.use(rateLimit(60, 100))
|
||||||
.use(betterAuth)
|
.use(betterAuth)
|
||||||
.use(healthRoutes)
|
.use(healthRoutes)
|
||||||
|
.use(userRoutes)
|
||||||
.get("/", () => ({
|
.get("/", () => ({
|
||||||
name: "DeckyVault API",
|
name: "DeckyVault API",
|
||||||
version: "2026.0.1",
|
version: "2026.0.1",
|
||||||
|
|||||||
+106
@@ -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
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -69,10 +69,7 @@ export const rateLimit = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// These headers are informational — clients can use them to throttle
|
// These headers are informational — clients can use them to throttle
|
||||||
set.headers = {
|
set.headers["X-RateLimit-Limit"] = String(max)
|
||||||
...set.headers,
|
set.headers["X-RateLimit-Remaining"] = String(result.remaining)
|
||||||
"X-RateLimit-Limit": String(max),
|
set.headers["X-RateLimit-Reset"] = String(Math.ceil(result.resetAt / 1000))
|
||||||
"X-RateLimit-Remaining": String(result.remaining),
|
|
||||||
"X-RateLimit-Reset": String(Math.ceil(result.resetAt / 1000)),
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user