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:
@@ -2,6 +2,7 @@ import { Elysia, t } from "elysia";
|
||||
import { db } from "../db";
|
||||
import { tasks, type ProgressNote } from "../db/schema";
|
||||
import { eq, asc, desc, sql, inArray } from "drizzle-orm";
|
||||
import { auth } from "../lib/auth";
|
||||
|
||||
const BEARER_TOKEN = process.env.API_BEARER_TOKEN || "hammer-dev-token";
|
||||
|
||||
@@ -14,16 +15,27 @@ const statusOrder = sql`CASE
|
||||
WHEN ${tasks.status} = 'cancelled' THEN 4
|
||||
ELSE 5 END`;
|
||||
|
||||
function requireAuth(headers: Record<string, string | undefined>) {
|
||||
const auth = headers["authorization"];
|
||||
if (!auth || auth !== `Bearer ${BEARER_TOKEN}`) {
|
||||
function requireBearerAuth(headers: Record<string, string | undefined>) {
|
||||
const authHeader = headers["authorization"];
|
||||
if (!authHeader || authHeader !== `Bearer ${BEARER_TOKEN}`) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
}
|
||||
|
||||
async function requireSessionOrBearer(request: Request, headers: Record<string, string | undefined>) {
|
||||
// Check bearer token first
|
||||
const authHeader = headers["authorization"];
|
||||
if (authHeader === `Bearer ${BEARER_TOKEN}`) return;
|
||||
|
||||
// Check session
|
||||
const session = await auth.api.getSession({ headers: request.headers });
|
||||
if (!session) throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
export const taskRoutes = new Elysia({ prefix: "/api/tasks" })
|
||||
// GET all tasks - public (read-only dashboard)
|
||||
.get("/", async () => {
|
||||
// GET all tasks - requires session or bearer auth
|
||||
.get("/", async ({ request, headers }) => {
|
||||
await requireSessionOrBearer(request, headers);
|
||||
const allTasks = await db
|
||||
.select()
|
||||
.from(tasks)
|
||||
@@ -35,7 +47,7 @@ export const taskRoutes = new Elysia({ prefix: "/api/tasks" })
|
||||
.post(
|
||||
"/",
|
||||
async ({ body, headers }) => {
|
||||
requireAuth(headers);
|
||||
requireBearerAuth(headers);
|
||||
// Get max position for queued tasks
|
||||
const maxPos = await db
|
||||
.select({ max: sql<number>`COALESCE(MAX(${tasks.position}), 0)` })
|
||||
@@ -93,7 +105,7 @@ export const taskRoutes = new Elysia({ prefix: "/api/tasks" })
|
||||
.patch(
|
||||
"/:id",
|
||||
async ({ params, body, headers }) => {
|
||||
requireAuth(headers);
|
||||
requireBearerAuth(headers);
|
||||
const updates: Record<string, any> = { updatedAt: new Date() };
|
||||
if (body.title !== undefined) updates.title = body.title;
|
||||
if (body.description !== undefined) updates.description = body.description;
|
||||
@@ -139,7 +151,7 @@ export const taskRoutes = new Elysia({ prefix: "/api/tasks" })
|
||||
.post(
|
||||
"/:id/notes",
|
||||
async ({ params, body, headers }) => {
|
||||
requireAuth(headers);
|
||||
requireBearerAuth(headers);
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(tasks)
|
||||
@@ -170,7 +182,7 @@ export const taskRoutes = new Elysia({ prefix: "/api/tasks" })
|
||||
.patch(
|
||||
"/reorder",
|
||||
async ({ body, headers }) => {
|
||||
requireAuth(headers);
|
||||
requireBearerAuth(headers);
|
||||
// body.ids is an ordered array of task IDs
|
||||
const updates = body.ids.map((id: string, index: number) =>
|
||||
db
|
||||
@@ -190,7 +202,7 @@ export const taskRoutes = new Elysia({ prefix: "/api/tasks" })
|
||||
.delete(
|
||||
"/:id",
|
||||
async ({ params, headers }) => {
|
||||
requireAuth(headers);
|
||||
requireBearerAuth(headers);
|
||||
const deleted = await db
|
||||
.delete(tasks)
|
||||
.where(eq(tasks.id, params.id))
|
||||
|
||||
Reference in New Issue
Block a user