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:
@@ -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 }) => {
|
||||
|
||||
Reference in New Issue
Block a user