Add user profile page, password change, reset links, auto-seed RBAC

Backend:
- POST /v1/auth/change-password (current user)
- POST /v1/auth/reset-password/:userId (admin generates 24h signed link)
- POST /v1/auth/reset-password (token-based reset, no auth required)
- GET/PATCH /v1/auth/me (profile read/update)
- Auto-seed system permissions on server startup

Frontend:
- Profile page with name edit, password change, theme/color settings
- Sidebar user link goes to profile page (replaces dropdown)
- Users page: "Reset Password Link" in kebab (copies to clipboard)
- Sign out button below profile link
This commit is contained in:
Ryan Moon
2026-03-28 17:59:55 -05:00
parent 58bf54a251
commit 7dea20e818
7 changed files with 385 additions and 81 deletions

View File

@@ -1,20 +1,7 @@
import { createFileRoute, Outlet, Link, redirect, useRouter } from '@tanstack/react-router'
import { useAuthStore } from '@/stores/auth.store'
import { useThemeStore } from '@/stores/theme.store'
import { themes } from '@/lib/themes'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
} from '@/components/ui/dropdown-menu'
import { Users, UserRound, HelpCircle, Shield, UserCog, Sun, Moon, Monitor, LogOut, User, Palette } from 'lucide-react'
import { Users, UserRound, HelpCircle, Shield, UserCog, LogOut, User } from 'lucide-react'
export const Route = createFileRoute('/_authenticated')({
beforeLoad: () => {
@@ -26,18 +13,10 @@ export const Route = createFileRoute('/_authenticated')({
component: AuthenticatedLayout,
})
function ModeIcon() {
const mode = useThemeStore((s) => s.mode)
if (mode === 'dark') return <Moon className="h-4 w-4" />
if (mode === 'light') return <Sun className="h-4 w-4" />
return <Monitor className="h-4 w-4" />
}
function AuthenticatedLayout() {
const router = useRouter()
const user = useAuthStore((s) => s.user)
const logout = useAuthStore((s) => s.logout)
const { mode, colorTheme, setMode, setColorTheme } = useThemeStore()
function handleLogout() {
logout()
@@ -98,64 +77,24 @@ function AuthenticatedLayout() {
</Link>
</div>
<div className="p-3 border-t border-sidebar-border">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="w-full justify-start gap-2 text-sm text-sidebar-foreground">
<User className="h-4 w-4" />
<span className="truncate">{user?.firstName} {user?.lastName}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-48">
<DropdownMenuLabel className="font-normal">
<p className="text-sm font-medium">{user?.firstName} {user?.lastName}</p>
<p className="text-xs text-muted-foreground">{user?.email}</p>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<ModeIcon />
<span className="ml-2">Mode</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem onClick={() => setMode('light')}>
<Sun className="mr-2 h-4 w-4" />
Light {mode === 'light' && '•'}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setMode('dark')}>
<Moon className="mr-2 h-4 w-4" />
Dark {mode === 'dark' && '•'}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setMode('system')}>
<Monitor className="mr-2 h-4 w-4" />
System {mode === 'system' && '•'}
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Palette className="h-4 w-4" />
<span className="ml-2">Color</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
{themes.map((t) => (
<DropdownMenuItem key={t.name} onClick={() => setColorTheme(t.name)}>
<span
className="mr-2 h-4 w-4 rounded-full border inline-block"
style={{ backgroundColor: `hsl(${t.light.primary})` }}
/>
{t.label} {colorTheme === t.name && '•'}
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout}>
<LogOut className="mr-2 h-4 w-4" />
Sign out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<div className="p-3 border-t border-sidebar-border space-y-1">
<Link
to="/profile"
className="flex items-center gap-2 px-3 py-2 rounded-md text-sm text-sidebar-foreground hover:bg-sidebar-accent w-full"
activeProps={{ className: 'flex items-center gap-2 px-3 py-2 rounded-md text-sm bg-sidebar-accent text-sidebar-accent-foreground w-full' }}
>
<User className="h-4 w-4" />
<span className="truncate">{user?.firstName} {user?.lastName}</span>
</Link>
<Button
variant="ghost"
size="sm"
className="w-full justify-start gap-2 text-sm text-sidebar-foreground/70 hover:text-sidebar-foreground"
onClick={handleLogout}
>
<LogOut className="h-4 w-4" />
Sign out
</Button>
</div>
</nav>
<main className="flex-1 p-6">