feat: add security settings tab with password and passkey management

This commit is contained in:
2026-04-26 11:11:48 -05:00
parent 9b8b4290dc
commit bea155005f
2 changed files with 517 additions and 1 deletions
+96 -1
View File
@@ -1,8 +1,9 @@
import { Elysia, t } from "elysia"
import { auth } from "@/lib/auth"
import { db } from "@/lib/db/index"
import { user, performanceEntries, games, gameVersions, hardware } from "@/lib/db/schema"
import { user, performanceEntries, games, gameVersions, hardware, account, passkey } from "@/lib/db/schema"
import { eq, sql, and, desc } from "drizzle-orm"
import { hashPassword } from "better-auth/crypto"
export const userRoutes = new Elysia({ prefix: "/user" })
.get(
@@ -136,6 +137,100 @@ export const userRoutes = new Elysia({ prefix: "/user" })
return sessions
},
)
.get(
"/me/auth-methods",
async ({ request, set }) => {
const session = await auth.api.getSession({
headers: request.headers,
})
if (!session) {
set.status = 401
return { error: "Unauthorized" }
}
// Count accounts by provider
const accounts = await db
.select({ providerId: account.providerId, id: account.id })
.from(account)
.where(eq(account.userId, session.user.id))
// Count passkeys
const [passkeyRow] = await db
.select({ count: sql<number>`count(*)::int` })
.from(passkey)
.where(eq(passkey.userId, session.user.id))
// Check if user has a password (from accounts where providerId is "credential")
const hasPassword = accounts.some((a) => a.providerId === "credential")
// OAuth providers
const oauthProviders = accounts
.filter((a) => a.providerId !== "credential")
.map((a) => ({
providerId: a.providerId,
id: a.id,
}))
// Total auth methods = passwords + passkeys + oauth accounts
const totalAuthMethods =
(hasPassword ? 1 : 0) + (passkeyRow?.count ?? 0) + oauthProviders.length
return {
hasPassword,
passkeyCount: passkeyRow?.count ?? 0,
oauthProviders,
totalAuthMethods,
}
},
)
.post(
"/me/set-password",
async ({ request, body, set }) => {
const session = await auth.api.getSession({
headers: request.headers,
})
if (!session) {
set.status = 401
return { error: "Unauthorized" }
}
// Check if user already has a password
const existing = await db
.select({ id: account.id })
.from(account)
.where(
and(
eq(account.userId, session.user.id),
eq(account.providerId, "credential")
)
)
.limit(1)
if (existing.length > 0) {
set.status = 400
return { error: "Password already set" }
}
const hashed = await hashPassword(body.newPassword)
await db.insert(account).values({
id: crypto.randomUUID(),
userId: session.user.id,
providerId: "credential",
accountId: session.user.id,
password: hashed,
})
return { success: true }
},
{
body: t.Object({
newPassword: t.String({ minLength: 8 }),
}),
},
)
.get(
"/profile/:id/contributions",
async ({ params, query, set }) => {