feat: add auth service with user, session, and API key management

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-03 13:20:27 +02:00
parent 32c7b41ce5
commit 7c4fa9d9d2
2 changed files with 329 additions and 0 deletions

View File

@@ -0,0 +1,158 @@
import { randomBytes } from "node:crypto";
import { count, eq } from "drizzle-orm";
import { db as prodDb } from "../../db/index.ts";
import { apiKeys, sessions, users } from "../../db/schema.ts";
type Db = typeof prodDb;
// ── User Management ──────────────────────────────────────────────────
export async function createUser(
db: Db = prodDb,
username: string,
password: string,
) {
const passwordHash = await Bun.password.hash(password);
return db.insert(users).values({ username, passwordHash }).returning().get();
}
export async function verifyPassword(
db: Db = prodDb,
username: string,
password: string,
) {
const user = db
.select()
.from(users)
.where(eq(users.username, username))
.get();
if (!user) return null;
const valid = await Bun.password.verify(password, user.passwordHash);
return valid ? user : null;
}
export function getUserCount(db: Db = prodDb): number {
const result = db.select({ value: count() }).from(users).get();
return result?.value ?? 0;
}
export async function changePassword(
db: Db = prodDb,
username: string,
currentPassword: string,
newPassword: string,
): Promise<boolean> {
const user = await verifyPassword(db, username, currentPassword);
if (!user) return false;
const newHash = await Bun.password.hash(newPassword);
db.update(users)
.set({ passwordHash: newHash })
.where(eq(users.id, user.id))
.run();
return true;
}
// ── Session Management ───────────────────────────────────────────────
export function createSession(
db: Db = prodDb,
userId: number,
expiryDays = 30,
) {
const id = randomBytes(32).toString("hex");
const expiresAt = new Date(Date.now() + expiryDays * 24 * 60 * 60 * 1000);
return db
.insert(sessions)
.values({ id, userId, expiresAt })
.returning()
.get();
}
export function getSession(db: Db = prodDb, sessionId: string) {
const session = db
.select()
.from(sessions)
.where(eq(sessions.id, sessionId))
.get();
if (!session) return null;
if (session.expiresAt < new Date()) {
db.delete(sessions).where(eq(sessions.id, sessionId)).run();
return null;
}
return session;
}
export function deleteSession(db: Db = prodDb, sessionId: string) {
db.delete(sessions).where(eq(sessions.id, sessionId)).run();
}
export function refreshSession(
db: Db = prodDb,
sessionId: string,
expiryDays = 30,
) {
const expiresAt = new Date(Date.now() + expiryDays * 24 * 60 * 60 * 1000);
db.update(sessions)
.set({ expiresAt })
.where(eq(sessions.id, sessionId))
.run();
}
// ── API Key Management ───────────────────────────────────────────────
export async function createApiKey(db: Db = prodDb, name: string) {
const rawKey = randomBytes(32).toString("hex");
const keyHash = await Bun.password.hash(rawKey);
const keyPrefix = rawKey.slice(0, 8);
const record = db
.insert(apiKeys)
.values({ name, keyHash, keyPrefix })
.returning()
.get();
return { ...record, rawKey };
}
export async function verifyApiKey(
db: Db = prodDb,
rawKey: string,
): Promise<boolean> {
const prefix = rawKey.slice(0, 8);
const candidates = db
.select()
.from(apiKeys)
.where(eq(apiKeys.keyPrefix, prefix))
.all();
for (const candidate of candidates) {
const valid = await Bun.password.verify(rawKey, candidate.keyHash);
if (valid) return true;
}
return false;
}
export function listApiKeys(db: Db = prodDb) {
return db
.select({
id: apiKeys.id,
name: apiKeys.name,
keyPrefix: apiKeys.keyPrefix,
createdAt: apiKeys.createdAt,
})
.from(apiKeys)
.all();
}
export function deleteApiKey(db: Db = prodDb, id: number) {
db.delete(apiKeys).where(eq(apiKeys.id, id)).run();
}