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

@@ -2,20 +2,23 @@ import { useState, useMemo } from "react";
import { useTasks } from "./hooks/useTasks";
import { TaskCard } from "./components/TaskCard";
import { CreateTaskModal } from "./components/CreateTaskModal";
import { LoginPage } from "./components/LoginPage";
import { useSession, signOut } from "./lib/auth-client";
import { updateTask, reorderTasks, createTask } from "./lib/api";
import type { TaskStatus } from "./lib/types";
// Token stored in localStorage for dashboard admin operations
// Token stored in localStorage for bearer-token admin operations
function getToken(): string {
return localStorage.getItem("hammer-queue-token") || "";
}
function App() {
function Dashboard() {
const { tasks, loading, error, refresh } = useTasks(5000);
const [showCreate, setShowCreate] = useState(false);
const [showCompleted, setShowCompleted] = useState(false);
const [tokenInput, setTokenInput] = useState("");
const [showTokenInput, setShowTokenInput] = useState(false);
const session = useSession();
const token = getToken();
const hasToken = !!token;
@@ -77,6 +80,11 @@ function App() {
setShowTokenInput(false);
};
const handleLogout = async () => {
await signOut();
window.location.reload();
};
return (
<div className="min-h-screen bg-gray-50">
{/* Header */}
@@ -87,34 +95,34 @@ function App() {
<h1 className="text-xl font-bold text-gray-900">Hammer Queue</h1>
<span className="text-xs text-gray-400 mt-1">Task Dashboard</span>
</div>
<div className="flex items-center gap-2">
{hasToken ? (
<>
<button
onClick={() => setShowCreate(true)}
className="text-sm bg-amber-500 text-white px-3 py-1.5 rounded-lg hover:bg-amber-600 transition font-medium"
>
+ New Task
</button>
<button
onClick={() => {
localStorage.removeItem("hammer-queue-token");
refresh();
}}
className="text-xs text-gray-400 hover:text-gray-600 px-2 py-1"
title="Log out"
>
🔓
</button>
</>
) : (
<div className="flex items-center gap-3">
{hasToken && (
<button
onClick={() => setShowTokenInput(true)}
className="text-sm text-gray-500 hover:text-gray-700 px-3 py-1.5 border rounded-lg"
onClick={() => setShowCreate(true)}
className="text-sm bg-amber-500 text-white px-3 py-1.5 rounded-lg hover:bg-amber-600 transition font-medium"
>
🔑 Set Token
+ New Task
</button>
)}
{!hasToken && (
<button
onClick={() => setShowTokenInput(true)}
className="text-xs text-gray-400 hover:text-gray-600 px-2 py-1 border border-gray-200 rounded-lg"
title="Set API token for admin actions"
>
🔑 Admin
</button>
)}
<div className="flex items-center gap-2 text-sm text-gray-500">
<span className="hidden sm:inline">{session.data?.user?.email}</span>
<button
onClick={handleLogout}
className="text-xs text-gray-400 hover:text-red-500 px-2 py-1 border border-gray-200 rounded-lg transition"
title="Sign out"
>
Sign Out
</button>
</div>
</div>
</div>
</header>
@@ -123,7 +131,10 @@ function App() {
{showTokenInput && (
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-2xl p-6 w-full max-w-sm mx-4">
<h2 className="text-lg font-bold mb-3">API Token</h2>
<h2 className="text-lg font-bold mb-2">API Token</h2>
<p className="text-xs text-gray-500 mb-3">
Enter the bearer token for admin actions (create, update, delete tasks).
</p>
<input
type="password"
placeholder="Bearer token..."
@@ -264,4 +275,22 @@ function App() {
);
}
function App() {
const session = useSession();
if (session.isPending) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-gray-400">Loading...</div>
</div>
);
}
if (!session.data) {
return <LoginPage onSuccess={() => window.location.reload()} />;
}
return <Dashboard />;
}
export default App;