feat: add auth routes for login, setup, and API key management
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
187
src/server/routes/auth.ts
Normal file
187
src/server/routes/auth.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { zValidator } from "@hono/zod-validator";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { Hono } from "hono";
|
||||
import { deleteCookie, getCookie, setCookie } from "hono/cookie";
|
||||
import { z } from "zod";
|
||||
import { users } from "../../db/schema.ts";
|
||||
import { requireAuth } from "../middleware/auth.ts";
|
||||
import {
|
||||
changePassword,
|
||||
createApiKey,
|
||||
createSession,
|
||||
createUser,
|
||||
deleteApiKey,
|
||||
deleteSession,
|
||||
getSession,
|
||||
getUserCount,
|
||||
listApiKeys,
|
||||
verifyPassword,
|
||||
} from "../services/auth.service.ts";
|
||||
|
||||
type Env = { Variables: { db?: any } };
|
||||
|
||||
const loginSchema = z.object({
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
});
|
||||
const setupSchema = z.object({
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(6),
|
||||
});
|
||||
const changePasswordSchema = z.object({
|
||||
currentPassword: z.string().min(1),
|
||||
newPassword: z.string().min(6),
|
||||
});
|
||||
const createKeySchema = z.object({ name: z.string().min(1) });
|
||||
|
||||
const COOKIE_NAME = "gearbox_session";
|
||||
const COOKIE_MAX_AGE = 30 * 24 * 60 * 60; // 30 days in seconds
|
||||
|
||||
const app = new Hono<Env>();
|
||||
|
||||
// ── Public routes ───────────────────────────────────────────────────
|
||||
|
||||
app.get("/me", (c) => {
|
||||
const db = c.get("db");
|
||||
const sessionId = getCookie(c, COOKIE_NAME);
|
||||
|
||||
if (sessionId) {
|
||||
const session = getSession(db, sessionId);
|
||||
if (session) {
|
||||
return c.json({
|
||||
user: { id: session.userId },
|
||||
setupRequired: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const setupRequired = getUserCount(db) === 0;
|
||||
return c.json({ user: null, setupRequired });
|
||||
});
|
||||
|
||||
app.post("/setup", zValidator("json", setupSchema), async (c) => {
|
||||
const db = c.get("db");
|
||||
|
||||
if (getUserCount(db) > 0) {
|
||||
return c.json({ error: "Setup already completed" }, 403);
|
||||
}
|
||||
|
||||
const { username, password } = c.req.valid("json");
|
||||
const user = await createUser(db, username, password);
|
||||
const session = createSession(db, user.id);
|
||||
|
||||
setCookie(c, COOKIE_NAME, session.id, {
|
||||
httpOnly: true,
|
||||
sameSite: "Lax",
|
||||
path: "/",
|
||||
maxAge: COOKIE_MAX_AGE,
|
||||
});
|
||||
|
||||
return c.json({ username: user.username }, 201);
|
||||
});
|
||||
|
||||
app.post("/login", zValidator("json", loginSchema), async (c) => {
|
||||
const db = c.get("db");
|
||||
const { username, password } = c.req.valid("json");
|
||||
|
||||
const user = await verifyPassword(db, username, password);
|
||||
if (!user) {
|
||||
return c.json({ error: "Invalid credentials" }, 401);
|
||||
}
|
||||
|
||||
const session = createSession(db, user.id);
|
||||
|
||||
setCookie(c, COOKIE_NAME, session.id, {
|
||||
httpOnly: true,
|
||||
sameSite: "Lax",
|
||||
path: "/",
|
||||
maxAge: COOKIE_MAX_AGE,
|
||||
});
|
||||
|
||||
return c.json({ username: user.username });
|
||||
});
|
||||
|
||||
app.post("/logout", (c) => {
|
||||
const db = c.get("db");
|
||||
const sessionId = getCookie(c, COOKIE_NAME);
|
||||
|
||||
if (sessionId) {
|
||||
deleteSession(db, sessionId);
|
||||
}
|
||||
|
||||
deleteCookie(c, COOKIE_NAME, { path: "/" });
|
||||
return c.json({ ok: true });
|
||||
});
|
||||
|
||||
// ── Protected routes ────────────────────────────────────────────────
|
||||
|
||||
app.put(
|
||||
"/password",
|
||||
requireAuth,
|
||||
zValidator("json", changePasswordSchema),
|
||||
async (c) => {
|
||||
const db = c.get("db");
|
||||
const sessionId = getCookie(c, COOKIE_NAME)!;
|
||||
const session = getSession(db, sessionId)!;
|
||||
|
||||
const userRecord = db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.id, session.userId))
|
||||
.get();
|
||||
|
||||
if (!userRecord) {
|
||||
return c.json({ error: "User not found" }, 404);
|
||||
}
|
||||
|
||||
const { currentPassword, newPassword } = c.req.valid("json");
|
||||
const changed = await changePassword(
|
||||
db,
|
||||
userRecord.username,
|
||||
currentPassword,
|
||||
newPassword,
|
||||
);
|
||||
|
||||
if (!changed) {
|
||||
return c.json({ error: "Invalid current password" }, 401);
|
||||
}
|
||||
|
||||
return c.json({ ok: true });
|
||||
},
|
||||
);
|
||||
|
||||
app.get("/keys", requireAuth, (c) => {
|
||||
const db = c.get("db");
|
||||
const keys = listApiKeys(db);
|
||||
return c.json(keys);
|
||||
});
|
||||
|
||||
app.post(
|
||||
"/keys",
|
||||
requireAuth,
|
||||
zValidator("json", createKeySchema),
|
||||
async (c) => {
|
||||
const db = c.get("db");
|
||||
const { name } = c.req.valid("json");
|
||||
const result = await createApiKey(db, name);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
id: result.id,
|
||||
name: result.name,
|
||||
key: result.rawKey,
|
||||
prefix: result.keyPrefix,
|
||||
},
|
||||
201,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
app.delete("/keys/:id", requireAuth, (c) => {
|
||||
const db = c.get("db");
|
||||
const id = Number(c.req.param("id"));
|
||||
deleteApiKey(db, id);
|
||||
return c.json({ ok: true });
|
||||
});
|
||||
|
||||
export const authRoutes = app;
|
||||
Reference in New Issue
Block a user