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:
@@ -1,34 +1,26 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import { useTasks } from "./hooks/useTasks";
|
||||
import { useCurrentUser } from "./hooks/useCurrentUser";
|
||||
import { TaskCard } from "./components/TaskCard";
|
||||
import { TaskDetailPanel } from "./components/TaskDetailPanel";
|
||||
import { CreateTaskModal } from "./components/CreateTaskModal";
|
||||
import { AdminPage } from "./components/AdminPage";
|
||||
import { LoginPage } from "./components/LoginPage";
|
||||
import { useSession, signOut } from "./lib/auth-client";
|
||||
import { updateTask, reorderTasks, createTask } from "./lib/api";
|
||||
import type { Task, TaskStatus } from "./lib/types";
|
||||
|
||||
// Token stored in localStorage for bearer-token admin operations
|
||||
function getToken(): string {
|
||||
return localStorage.getItem("hammer-queue-token") || "";
|
||||
}
|
||||
import type { TaskStatus } from "./lib/types";
|
||||
|
||||
function Dashboard() {
|
||||
const { tasks, loading, error, refresh } = useTasks(5000);
|
||||
const { user, isAdmin, isAuthenticated } = useCurrentUser();
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
const [showCompleted, setShowCompleted] = useState(false);
|
||||
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
|
||||
const [tokenInput, setTokenInput] = useState("");
|
||||
const [showTokenInput, setShowTokenInput] = useState(false);
|
||||
const session = useSession();
|
||||
const [selectedTask, setSelectedTask] = useState<string | null>(null);
|
||||
const [showAdmin, setShowAdmin] = useState(false);
|
||||
|
||||
const token = getToken();
|
||||
const hasToken = !!token;
|
||||
|
||||
// Keep selected task in sync with refreshed data
|
||||
const selectedTaskData = useMemo(() => {
|
||||
if (!selectedTask) return null;
|
||||
return tasks.find((t) => t.id === selectedTask.id) || null;
|
||||
return tasks.find((t) => t.id === selectedTask) || null;
|
||||
}, [tasks, selectedTask]);
|
||||
|
||||
const activeTasks = useMemo(() => tasks.filter((t) => t.status === "active"), [tasks]);
|
||||
@@ -40,31 +32,27 @@ function Dashboard() {
|
||||
);
|
||||
|
||||
const handleStatusChange = async (id: string, status: TaskStatus) => {
|
||||
if (!hasToken) {
|
||||
setShowTokenInput(true);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await updateTask(id, { status }, token);
|
||||
await updateTask(id, { status });
|
||||
refresh();
|
||||
} catch (e) {
|
||||
alert("Failed to update task. Check your token.");
|
||||
alert("Failed to update task.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleMoveUp = async (index: number) => {
|
||||
if (index === 0 || !hasToken) return;
|
||||
if (index === 0) return;
|
||||
const ids = queuedTasks.map((t) => t.id);
|
||||
[ids[index - 1], ids[index]] = [ids[index], ids[index - 1]];
|
||||
await reorderTasks(ids, token);
|
||||
await reorderTasks(ids);
|
||||
refresh();
|
||||
};
|
||||
|
||||
const handleMoveDown = async (index: number) => {
|
||||
if (index >= queuedTasks.length - 1 || !hasToken) return;
|
||||
if (index >= queuedTasks.length - 1) return;
|
||||
const ids = queuedTasks.map((t) => t.id);
|
||||
[ids[index], ids[index + 1]] = [ids[index + 1], ids[index]];
|
||||
await reorderTasks(ids, token);
|
||||
await reorderTasks(ids);
|
||||
refresh();
|
||||
};
|
||||
|
||||
@@ -74,25 +62,19 @@ function Dashboard() {
|
||||
source?: string;
|
||||
priority?: string;
|
||||
}) => {
|
||||
if (!hasToken) {
|
||||
setShowTokenInput(true);
|
||||
return;
|
||||
}
|
||||
await createTask(task, token);
|
||||
await createTask(task);
|
||||
refresh();
|
||||
};
|
||||
|
||||
const handleSetToken = () => {
|
||||
localStorage.setItem("hammer-queue-token", tokenInput);
|
||||
setTokenInput("");
|
||||
setShowTokenInput(false);
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
await signOut();
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
if (showAdmin) {
|
||||
return <AdminPage onBack={() => setShowAdmin(false)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Header */}
|
||||
@@ -104,25 +86,28 @@ function Dashboard() {
|
||||
<span className="text-xs text-gray-400 mt-1">Task Dashboard</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{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>
|
||||
{isAdmin && (
|
||||
<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"
|
||||
onClick={() => setShowAdmin(true)}
|
||||
className="text-xs text-purple-600 hover:text-purple-800 px-2 py-1 border border-purple-200 rounded-lg transition"
|
||||
title="Admin panel"
|
||||
>
|
||||
+ 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
|
||||
⚙️ Admin
|
||||
</button>
|
||||
)}
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||
<span className="hidden sm:inline">{session.data?.user?.email}</span>
|
||||
<span className="hidden sm:inline">{user?.email}</span>
|
||||
{isAdmin && (
|
||||
<span className="text-xs bg-purple-100 text-purple-700 px-1.5 py-0.5 rounded-full font-medium">
|
||||
admin
|
||||
</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"
|
||||
@@ -135,41 +120,6 @@ function Dashboard() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Token input modal */}
|
||||
{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-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..."
|
||||
value={tokenInput}
|
||||
onChange={(e) => setTokenInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSetToken()}
|
||||
className="w-full border rounded-lg px-3 py-2 text-sm mb-3 focus:outline-none focus:ring-2 focus:ring-amber-400"
|
||||
autoFocus
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleSetToken}
|
||||
className="flex-1 bg-amber-500 text-white rounded-lg py-2 text-sm font-medium hover:bg-amber-600"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowTokenInput(false)}
|
||||
className="px-4 py-2 text-sm text-gray-600"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CreateTaskModal
|
||||
open={showCreate}
|
||||
onClose={() => setShowCreate(false)}
|
||||
@@ -203,7 +153,7 @@ function Dashboard() {
|
||||
task={task}
|
||||
onStatusChange={handleStatusChange}
|
||||
isActive
|
||||
onClick={() => setSelectedTask(task)}
|
||||
onClick={() => setSelectedTask(task.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -222,7 +172,7 @@ function Dashboard() {
|
||||
key={task.id}
|
||||
task={task}
|
||||
onStatusChange={handleStatusChange}
|
||||
onClick={() => setSelectedTask(task)}
|
||||
onClick={() => setSelectedTask(task.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -249,7 +199,7 @@ function Dashboard() {
|
||||
onMoveDown={() => handleMoveDown(i)}
|
||||
isFirst={i === 0}
|
||||
isLast={i === queuedTasks.length - 1}
|
||||
onClick={() => setSelectedTask(task)}
|
||||
onClick={() => setSelectedTask(task.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -271,7 +221,7 @@ function Dashboard() {
|
||||
key={task.id}
|
||||
task={task}
|
||||
onStatusChange={handleStatusChange}
|
||||
onClick={() => setSelectedTask(task)}
|
||||
onClick={() => setSelectedTask(task.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -289,14 +239,14 @@ function Dashboard() {
|
||||
setSelectedTask(null);
|
||||
}}
|
||||
onTaskUpdated={refresh}
|
||||
hasToken={hasToken}
|
||||
token={token}
|
||||
hasToken={isAuthenticated}
|
||||
token=""
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="text-center text-xs text-gray-300 py-4">
|
||||
Hammer Queue v0.1 · Auto-refreshes every 5s
|
||||
Hammer Queue v0.2 · Auto-refreshes every 5s
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user