Loading portal-gui/src/app/guards/role-guard.tsx +1 −1 Changes for portal-gui/src/app/guards/role-guard.tsx: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -10,7 +10,7 @@ export function RoleGuard({ allowedRoles, children }: RoleGuardProps) { return <Navigate to={ROUTES.LOGIN} replace />; } if (!allowedRoles.includes(user.role)) { if (!allowedRoles.some((role) => user.roles.includes(role))) { return <Navigate to={ROUTES.FORBIDDEN} replace />; } Loading portal-gui/src/app/router.tsx +1 −1 Changes for portal-gui/src/app/router.tsx: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -55,7 +55,7 @@ export const router = createBrowserRouter([ { path: ROUTES.SETTINGS, element: ( <RoleGuard allowedRoles={['admin']}> <RoleGuard allowedRoles={['platform-admin']}> <SettingsPage /> </RoleGuard> ), Loading portal-gui/src/components/common/nav-user.tsx +6 −4 Changes for portal-gui/src/components/common/nav-user.tsx: 6 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -36,6 +36,8 @@ export function NavUser() { return null; } const displayName = user.username ?? user.email ?? user.subject; return ( <SidebarMenu> <SidebarMenuItem> Loading @@ -46,12 +48,12 @@ export function NavUser() { className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground" > <Avatar className="h-8 w-8 rounded-lg grayscale"> <AvatarImage alt={user.name} /> <AvatarImage alt={displayName} /> <AvatarFallback className="rounded-lg">CN</AvatarFallback> </Avatar> <div className="grid flex-1 text-left text-sm leading-tight"> <span className="truncate font-medium font-semibold"> {user.name} {displayName} </span> </div> <EllipsisVerticalIcon className="ml-auto size-4" /> Loading @@ -66,11 +68,11 @@ export function NavUser() { <DropdownMenuLabel className="p-0 font-normal"> <div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm"> <Avatar className="h-8 w-8 rounded-lg"> <AvatarImage alt={user.name} /> <AvatarImage alt={displayName} /> <AvatarFallback className="rounded-lg">CN</AvatarFallback> </Avatar> <div className="grid flex-1 text-left text-sm leading-tight"> <span className="truncate font-medium">{user.name}</span> <span className="truncate font-medium">{displayName}</span> <span className="truncate text-xs text-muted-foreground"> {user.email} </span> Loading portal-gui/src/features/auth/auth-provider.tsx +3 −37 Changes for portal-gui/src/features/auth/auth-provider.tsx: 3 added lines, 37 removed lines. Original line number Diff line number Diff line import { useState } from 'react'; import { AuthContext } from './auth-context'; import type { SessionUser } from '@/types/authentication'; import { mockUser, mockCredentials } from '@/lib/constants'; function readStoredUser(): SessionUser | null { const stored = localStorage.getItem('user'); if (!stored) { return null; } try { return JSON.parse(stored) as SessionUser; } catch { // Malformed JSON — clear the bad value and start unauthenticated. localStorage.removeItem('user'); return null; } } import type { CurrentUser } from '@/types/authentication'; export function AuthProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState<SessionUser | null>(readStoredUser); const login = async (email: string, password: string) => { if (email === mockUser.email && password === mockCredentials.password) { const sessionUser = { name: mockUser.name, email: mockUser.email, role: mockUser.role, }; setUser(sessionUser); localStorage.setItem('user', JSON.stringify(sessionUser)); return true; } return false; }; const [user, setUser] = useState<CurrentUser | null>(null); const login = async () => false; const logout = () => { setUser(null); localStorage.removeItem('user'); }; return ( Loading portal-gui/src/features/settings/settings.tsx +27 −12 Changes for portal-gui/src/features/settings/settings.tsx: 27 added lines, 12 removed lines. Original line number Diff line number Diff line Loading @@ -9,14 +9,19 @@ import { } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { mockUser } from '@/lib/constants'; import type { User } from '@/types/authentication'; import { useAuth } from '@/features/auth/auth-context'; import { isRole } from '@/types/authentication'; export function SettingsPage() { const { user } = useAuth(); const [isEditing, setIsEditing] = useState(false); const [formValues, setFormValues] = useState<User>(mockUser); const updateField = (field: 'name' | 'email', value: string) => { const savedValues = { username: user?.username ?? '', email: user?.email ?? '', }; const [formValues, setFormValues] = useState(savedValues); const updateField = (field: 'username' | 'email', value: string) => { setFormValues((previousValues) => ({ ...previousValues, [field]: value, Loading @@ -26,7 +31,7 @@ export function SettingsPage() { setIsEditing(false); }; const handleCancel = () => { setFormValues(mockUser); setFormValues(savedValues); setIsEditing(false); }; Loading @@ -49,12 +54,14 @@ export function SettingsPage() { <CardContent className="space-y-4"> <div className="grid gap-2"> <Label htmlFor="name">Name</Label> <Label htmlFor="username">Username</Label> <Input id="name" value={formValues.name} id="username" value={formValues.username} readOnly={!isEditing} onChange={(event) => updateField('name', event.target.value)} onChange={(event) => updateField('username', event.target.value) } /> </div> Loading @@ -70,13 +77,21 @@ export function SettingsPage() { </div> <div className="grid gap-2"> <Label htmlFor="role">Role</Label> <Input id="role" value={mockUser.role} readOnly /> <Label htmlFor="roles">Roles</Label> <Input id="roles" value={user?.roles.filter(isRole).join(', ') ?? ''} readOnly /> </div> <div className="grid gap-2"> <Label htmlFor="organization">Organization</Label> <Input id="organization" value={mockUser.organization} readOnly /> <Input id="organization" value={user?.organization ?? ''} readOnly /> </div> {isEditing ? ( Loading Loading
portal-gui/src/app/guards/role-guard.tsx +1 −1 Changes for portal-gui/src/app/guards/role-guard.tsx: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -10,7 +10,7 @@ export function RoleGuard({ allowedRoles, children }: RoleGuardProps) { return <Navigate to={ROUTES.LOGIN} replace />; } if (!allowedRoles.includes(user.role)) { if (!allowedRoles.some((role) => user.roles.includes(role))) { return <Navigate to={ROUTES.FORBIDDEN} replace />; } Loading
portal-gui/src/app/router.tsx +1 −1 Changes for portal-gui/src/app/router.tsx: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -55,7 +55,7 @@ export const router = createBrowserRouter([ { path: ROUTES.SETTINGS, element: ( <RoleGuard allowedRoles={['admin']}> <RoleGuard allowedRoles={['platform-admin']}> <SettingsPage /> </RoleGuard> ), Loading
portal-gui/src/components/common/nav-user.tsx +6 −4 Changes for portal-gui/src/components/common/nav-user.tsx: 6 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -36,6 +36,8 @@ export function NavUser() { return null; } const displayName = user.username ?? user.email ?? user.subject; return ( <SidebarMenu> <SidebarMenuItem> Loading @@ -46,12 +48,12 @@ export function NavUser() { className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground" > <Avatar className="h-8 w-8 rounded-lg grayscale"> <AvatarImage alt={user.name} /> <AvatarImage alt={displayName} /> <AvatarFallback className="rounded-lg">CN</AvatarFallback> </Avatar> <div className="grid flex-1 text-left text-sm leading-tight"> <span className="truncate font-medium font-semibold"> {user.name} {displayName} </span> </div> <EllipsisVerticalIcon className="ml-auto size-4" /> Loading @@ -66,11 +68,11 @@ export function NavUser() { <DropdownMenuLabel className="p-0 font-normal"> <div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm"> <Avatar className="h-8 w-8 rounded-lg"> <AvatarImage alt={user.name} /> <AvatarImage alt={displayName} /> <AvatarFallback className="rounded-lg">CN</AvatarFallback> </Avatar> <div className="grid flex-1 text-left text-sm leading-tight"> <span className="truncate font-medium">{user.name}</span> <span className="truncate font-medium">{displayName}</span> <span className="truncate text-xs text-muted-foreground"> {user.email} </span> Loading
portal-gui/src/features/auth/auth-provider.tsx +3 −37 Changes for portal-gui/src/features/auth/auth-provider.tsx: 3 added lines, 37 removed lines. Original line number Diff line number Diff line import { useState } from 'react'; import { AuthContext } from './auth-context'; import type { SessionUser } from '@/types/authentication'; import { mockUser, mockCredentials } from '@/lib/constants'; function readStoredUser(): SessionUser | null { const stored = localStorage.getItem('user'); if (!stored) { return null; } try { return JSON.parse(stored) as SessionUser; } catch { // Malformed JSON — clear the bad value and start unauthenticated. localStorage.removeItem('user'); return null; } } import type { CurrentUser } from '@/types/authentication'; export function AuthProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState<SessionUser | null>(readStoredUser); const login = async (email: string, password: string) => { if (email === mockUser.email && password === mockCredentials.password) { const sessionUser = { name: mockUser.name, email: mockUser.email, role: mockUser.role, }; setUser(sessionUser); localStorage.setItem('user', JSON.stringify(sessionUser)); return true; } return false; }; const [user, setUser] = useState<CurrentUser | null>(null); const login = async () => false; const logout = () => { setUser(null); localStorage.removeItem('user'); }; return ( Loading
portal-gui/src/features/settings/settings.tsx +27 −12 Changes for portal-gui/src/features/settings/settings.tsx: 27 added lines, 12 removed lines. Original line number Diff line number Diff line Loading @@ -9,14 +9,19 @@ import { } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { mockUser } from '@/lib/constants'; import type { User } from '@/types/authentication'; import { useAuth } from '@/features/auth/auth-context'; import { isRole } from '@/types/authentication'; export function SettingsPage() { const { user } = useAuth(); const [isEditing, setIsEditing] = useState(false); const [formValues, setFormValues] = useState<User>(mockUser); const updateField = (field: 'name' | 'email', value: string) => { const savedValues = { username: user?.username ?? '', email: user?.email ?? '', }; const [formValues, setFormValues] = useState(savedValues); const updateField = (field: 'username' | 'email', value: string) => { setFormValues((previousValues) => ({ ...previousValues, [field]: value, Loading @@ -26,7 +31,7 @@ export function SettingsPage() { setIsEditing(false); }; const handleCancel = () => { setFormValues(mockUser); setFormValues(savedValues); setIsEditing(false); }; Loading @@ -49,12 +54,14 @@ export function SettingsPage() { <CardContent className="space-y-4"> <div className="grid gap-2"> <Label htmlFor="name">Name</Label> <Label htmlFor="username">Username</Label> <Input id="name" value={formValues.name} id="username" value={formValues.username} readOnly={!isEditing} onChange={(event) => updateField('name', event.target.value)} onChange={(event) => updateField('username', event.target.value) } /> </div> Loading @@ -70,13 +77,21 @@ export function SettingsPage() { </div> <div className="grid gap-2"> <Label htmlFor="role">Role</Label> <Input id="role" value={mockUser.role} readOnly /> <Label htmlFor="roles">Roles</Label> <Input id="roles" value={user?.roles.filter(isRole).join(', ') ?? ''} readOnly /> </div> <div className="grid gap-2"> <Label htmlFor="organization">Organization</Label> <Input id="organization" value={mockUser.organization} readOnly /> <Input id="organization" value={user?.organization ?? ''} readOnly /> </div> {isEditing ? ( Loading