feat: session-based auth, admin roles, user management
- All logged-in users can create/edit/manage tasks (no bearer token needed) - Added user role system (user/admin) - Donovan's account auto-promoted to admin on startup - Admin page: view users, change roles, delete users - /api/me endpoint returns current user info + role - /api/admin/* routes (admin-only) - Removed bearer token UI from frontend - Bearer token still works for API/bot access
This commit is contained in:
@@ -64,6 +64,7 @@ export const users = pgTable("users", {
|
||||
email: text("email").notNull().unique(),
|
||||
emailVerified: boolean("email_verified").notNull().default(false),
|
||||
image: text("image"),
|
||||
role: text("role").notNull().default("user"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Elysia } from "elysia";
|
||||
import { cors } from "@elysiajs/cors";
|
||||
import { taskRoutes } from "./routes/tasks";
|
||||
import { adminRoutes } from "./routes/admin";
|
||||
import { auth } from "./lib/auth";
|
||||
import { db } from "./db";
|
||||
import { tasks } from "./db/schema";
|
||||
import { isNull, asc, sql } from "drizzle-orm";
|
||||
import { tasks, users } from "./db/schema";
|
||||
import { isNull, asc, sql, eq } from "drizzle-orm";
|
||||
|
||||
const PORT = process.env.PORT || 3100;
|
||||
|
||||
@@ -34,6 +35,20 @@ async function backfillTaskNumbers() {
|
||||
|
||||
backfillTaskNumbers().catch(console.error);
|
||||
|
||||
// Ensure donovan@donovankelly.xyz is admin
|
||||
async function ensureAdmin() {
|
||||
const adminEmail = "donovan@donovankelly.xyz";
|
||||
const result = await db
|
||||
.update(users)
|
||||
.set({ role: "admin" })
|
||||
.where(eq(users.email, adminEmail))
|
||||
.returning({ id: users.id, email: users.email, role: users.role });
|
||||
if (result.length) {
|
||||
console.log(`Admin role ensured for ${adminEmail}`);
|
||||
}
|
||||
}
|
||||
ensureAdmin().catch(console.error);
|
||||
|
||||
const app = new Elysia()
|
||||
.use(
|
||||
cors({
|
||||
@@ -98,6 +113,27 @@ const app = new Elysia()
|
||||
})
|
||||
|
||||
.use(taskRoutes)
|
||||
.use(adminRoutes)
|
||||
|
||||
// Current user info (role, etc.)
|
||||
.get("/api/me", async ({ request }) => {
|
||||
try {
|
||||
const session = await auth.api.getSession({ headers: request.headers });
|
||||
if (!session?.user) return { authenticated: false };
|
||||
return {
|
||||
authenticated: true,
|
||||
user: {
|
||||
id: session.user.id,
|
||||
name: session.user.name,
|
||||
email: session.user.email,
|
||||
role: (session.user as any).role || "user",
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
return { authenticated: false };
|
||||
}
|
||||
})
|
||||
|
||||
.get("/health", () => ({ status: "ok", service: "hammer-queue" }))
|
||||
.onError(({ error, set }) => {
|
||||
const msg = error?.message || String(error);
|
||||
|
||||
89
backend/src/routes/admin.ts
Normal file
89
backend/src/routes/admin.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { Elysia, t } from "elysia";
|
||||
import { db } from "../db";
|
||||
import { users } from "../db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { auth } from "../lib/auth";
|
||||
|
||||
const BEARER_TOKEN = process.env.API_BEARER_TOKEN || "hammer-dev-token";
|
||||
|
||||
async function requireAdmin(request: Request, headers: Record<string, string | undefined>) {
|
||||
const authHeader = headers["authorization"];
|
||||
if (authHeader === `Bearer ${BEARER_TOKEN}`) return;
|
||||
|
||||
try {
|
||||
const session = await auth.api.getSession({ headers: request.headers });
|
||||
if (session?.user && (session.user as any).role === "admin") return;
|
||||
} catch {}
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
export const adminRoutes = new Elysia({ prefix: "/api/admin" })
|
||||
.onError(({ error, set }) => {
|
||||
const msg = error?.message || String(error);
|
||||
if (msg === "Unauthorized") {
|
||||
set.status = 401;
|
||||
return { error: "Unauthorized" };
|
||||
}
|
||||
console.error("Admin route error:", msg);
|
||||
set.status = 500;
|
||||
return { error: "Internal server error" };
|
||||
})
|
||||
|
||||
// GET all users
|
||||
.get("/users", async ({ request, headers }) => {
|
||||
await requireAdmin(request, headers);
|
||||
const allUsers = await db.select({
|
||||
id: users.id,
|
||||
name: users.name,
|
||||
email: users.email,
|
||||
role: users.role,
|
||||
createdAt: users.createdAt,
|
||||
}).from(users);
|
||||
return allUsers;
|
||||
})
|
||||
|
||||
// PATCH update user role
|
||||
.patch(
|
||||
"/users/:id/role",
|
||||
async ({ params, body, request, headers }) => {
|
||||
await requireAdmin(request, headers);
|
||||
const updated = await db
|
||||
.update(users)
|
||||
.set({ role: body.role })
|
||||
.where(eq(users.id, params.id))
|
||||
.returning({
|
||||
id: users.id,
|
||||
name: users.name,
|
||||
email: users.email,
|
||||
role: users.role,
|
||||
});
|
||||
if (!updated.length) throw new Error("User not found");
|
||||
return updated[0];
|
||||
},
|
||||
{
|
||||
params: t.Object({ id: t.String() }),
|
||||
body: t.Object({ role: t.String() }),
|
||||
}
|
||||
)
|
||||
|
||||
// DELETE user
|
||||
.delete(
|
||||
"/users/:id",
|
||||
async ({ params, request, headers }) => {
|
||||
await requireAdmin(request, headers);
|
||||
// Don't allow deleting yourself
|
||||
const session = await auth.api.getSession({ headers: request.headers });
|
||||
if (session?.user?.id === params.id) {
|
||||
throw new Error("Cannot delete yourself");
|
||||
}
|
||||
const deleted = await db
|
||||
.delete(users)
|
||||
.where(eq(users.id, params.id))
|
||||
.returning();
|
||||
if (!deleted.length) throw new Error("User not found");
|
||||
return { success: true };
|
||||
},
|
||||
{
|
||||
params: t.Object({ id: t.String() }),
|
||||
}
|
||||
);
|
||||
@@ -68,6 +68,19 @@ async function requireSessionOrBearer(request: Request, headers: Record<string,
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
async function requireAdmin(request: Request, headers: Record<string, string | undefined>) {
|
||||
// Bearer token = admin access
|
||||
const authHeader = headers["authorization"];
|
||||
if (authHeader === `Bearer ${BEARER_TOKEN}`) return;
|
||||
|
||||
// Check session + role
|
||||
try {
|
||||
const session = await auth.api.getSession({ headers: request.headers });
|
||||
if (session?.user && (session.user as any).role === "admin") return;
|
||||
} catch {}
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
// Resolve a task by UUID or sequential number (e.g. "5" or "HQ-5")
|
||||
async function resolveTask(idOrNumber: string) {
|
||||
// Strip "HQ-" prefix if present
|
||||
|
||||
Reference in New Issue
Block a user