feat: add BetterAuth authentication

- Add better-auth to backend and frontend
- Create auth tables (users, sessions, accounts, verifications)
- Mount BetterAuth handler on /api/auth/*
- Protect GET /api/tasks with session auth
- Add login page with email/password
- Add invite route for creating users
- Add logout button to header
- Cross-subdomain cookies for .donovankelly.xyz
- Fix page title to 'Hammer Queue'
- Keep bearer token for admin mutations (separate from session auth)
- Update docker-compose with BETTER_AUTH_SECRET and COOKIE_DOMAIN
This commit is contained in:
2026-01-28 23:19:52 +00:00
parent 52b6190d43
commit 96d81520b9
16 changed files with 408 additions and 42 deletions

View File

@@ -1,11 +1,73 @@
import { Elysia } from "elysia";
import { cors } from "@elysiajs/cors";
import { taskRoutes } from "./routes/tasks";
import { auth } from "./lib/auth";
const PORT = process.env.PORT || 3100;
const app = new Elysia()
.use(cors())
.use(
cors({
origin: ["https://queue.donovankelly.xyz", "http://localhost:5173"],
credentials: true,
allowedHeaders: ["Content-Type", "Authorization", "Cookie"],
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
})
)
// Mount BetterAuth handler
.all("/api/auth/*", async ({ request }) => {
return auth.handler(request);
})
// Invite route - create a user (bearer token or session auth)
.post("/api/invite", async ({ request, headers, body }) => {
const bearerToken = process.env.API_BEARER_TOKEN || "hammer-dev-token";
const authHeader = headers["authorization"];
// Check bearer token first
let authorized = authHeader === `Bearer ${bearerToken}`;
// If no bearer token, check session
if (!authorized) {
const session = await auth.api.getSession({ headers: request.headers });
authorized = !!session;
}
if (!authorized) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
const { email, password, name } = body as {
email: string;
password: string;
name: string;
};
if (!email || !password || !name) {
return new Response(
JSON.stringify({ error: "email, password, and name are required" }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
try {
const user = await auth.api.signUpEmail({
body: { email, password, name },
});
return new Response(JSON.stringify({ success: true, user }), {
headers: { "Content-Type": "application/json" },
});
} catch (e: any) {
return new Response(
JSON.stringify({ error: e.message || "Failed to create user" }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
})
.use(taskRoutes)
.get("/health", () => ({ status: "ok", service: "hammer-queue" }))
.onError(({ error, set }) => {