# TASK-FE-002: Authentication & Authorization UI **ID:** TASK-FE-002 **Title:** Login, Session Management & RBAC UI **Category:** Foundation **Priority:** P0 (Critical) **Effort:** 3-4 days **Dependencies:** TASK-FE-001, TASK-BE-002 **Assigned To:** Frontend Developer --- ## ๐Ÿ“‹ Overview Implement authentication UI including login form, session management with Zustand, and permission-based UI rendering. --- ## ๐ŸŽฏ Objectives 1. Create login page with form validation 2. Implement JWT token management 3. Setup Zustand auth store 4. Create protected route middleware 5. Implement permission-based UI components 6. Add logout functionality --- ## โœ… Acceptance Criteria - [ ] User can login with username/password - [ ] JWT token stored securely - [ ] Unauthorized users redirected to login - [ ] UI elements hidden based on permissions - [ ] Session persists after page reload - [ ] Logout clears session --- ## ๐Ÿ”ง Implementation Steps ### Step 1: Create Auth Store (Zustand) ```typescript // File: src/lib/stores/auth-store.ts import { create } from 'zustand'; import { persist } from 'zustand/middleware'; interface User { user_id: number; username: string; email: string; first_name: string; last_name: string; roles: Array<{ role_name: string; scope: string; scope_id: number; }>; } interface AuthState { user: User | null; token: string | null; isAuthenticated: boolean; setAuth: (user: User, token: string) => void; logout: () => void; hasPermission: (permission: string, scope?: string) => boolean; } export const useAuthStore = create()( persist( (set, get) => ({ user: null, token: null, isAuthenticated: false, setAuth: (user, token) => { set({ user, token, isAuthenticated: true }); localStorage.setItem('auth-token', token); }, logout: () => { set({ user: null, token: null, isAuthenticated: false }); localStorage.removeItem('auth-token'); }, hasPermission: (permission, scope) => { const { user } = get(); if (!user) return false; // Check user roles for permission return user.roles.some((role) => { // Permission logic based on RBAC return true; // Implement actual logic }); }, }), { name: 'auth-storage', } ) ); ``` ### Step 2: Login API Client ```typescript // File: src/lib/api/auth.ts import { apiClient } from './client'; export interface LoginRequest { username: string; password: string; } export interface LoginResponse { access_token: string; user: { user_id: number; username: string; email: string; first_name: string; last_name: string; roles: any[]; }; } export const authApi = { login: async (credentials: LoginRequest): Promise => { const response = await apiClient.post('/auth/login', credentials); return response.data; }, logout: async (): Promise => { await apiClient.post('/auth/logout'); }, getCurrentUser: async () => { const response = await apiClient.get('/auth/me'); return response.data; }, }; ``` ### Step 3: Login Page ```typescript // File: src/app/(public)/login/page.tsx 'use client'; import { useState } from 'react'; import { useRouter } from 'next/navigation'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; import { authApi } from '@/lib/api/auth'; import { useAuthStore } from '@/lib/stores/auth-store'; const loginSchema = z.object({ username: z.string().min(1, 'Username is required'), password: z.string().min(1, 'Password is required'), }); type LoginFormData = z.infer; export default function LoginPage() { const router = useRouter(); const setAuth = useAuthStore((state) => state.setAuth); const [error, setError] = useState(''); const { register, handleSubmit, formState: { errors, isSubmitting }, } = useForm({ resolver: zodResolver(loginSchema), }); const onSubmit = async (data: LoginFormData) => { try { setError(''); const response = await authApi.login(data); setAuth(response.user, response.access_token); router.push('/'); } catch (err: any) { setError(err.response?.data?.message || 'Login failed'); } }; return (
LCBP3-DMS Login
{error && (
{error}
)}
{errors.username && (

{errors.username.message}

)}
{errors.password && (

{errors.password.message}

)}
); } ``` ### Step 4: Protected Route Middleware ```typescript // File: src/middleware.ts import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; export function middleware(request: NextRequest) { const token = request.cookies.get('auth-token'); const isPublicPage = request.nextUrl.pathname.startsWith('/login'); if (!token && !isPublicPage) { return NextResponse.redirect(new URL('/login', request.url)); } if (token && isPublicPage) { return NextResponse.redirect(new URL('/', request.url)); } return NextResponse.next(); } export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], }; ``` ### Step 5: Permission-Based UI Components ```typescript // File: src/components/common/can.tsx 'use client'; import { useAuthStore } from '@/lib/stores/auth-store'; import { ReactNode } from 'react'; interface CanProps { permission: string; scope?: string; children: ReactNode; fallback?: ReactNode; } export function Can({ permission, scope, children, fallback = null, }: CanProps) { const hasPermission = useAuthStore((state) => state.hasPermission); if (!hasPermission(permission, scope)) { return <>{fallback}; } return <>{children}; } ``` ```typescript // Usage example import { Can } from '@/components/common/can'; ; ``` ### Step 6: User Menu Component ```typescript // File: src/components/layout/user-menu.tsx 'use client'; import { useAuthStore } from '@/lib/stores/auth-store'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { Button } from '@/components/ui/button'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { useRouter } from 'next/navigation'; export function UserMenu() { const router = useRouter(); const { user, logout } = useAuthStore(); if (!user) return null; const handleLogout = () => { logout(); router.push('/login'); }; const initials = `${user.first_name[0]}${user.last_name[0]}`.toUpperCase(); return (

{user.first_name} {user.last_name}

{user.email}

router.push('/settings')}> Settings Logout
); } ``` --- ## ๐Ÿงช Testing & Verification ### Test Cases 1. **Login Success** - Enter valid credentials - User redirected to dashboard - Token stored 2. **Login Failure** - Enter invalid credentials - Error message displayed - User stays on login page 3. **Protected Routes** - Access protected route without login โ†’ Redirect to login - Login โ†’ Access protected route successfully 4. **Session Persistence** - Login โ†’ Refresh page โ†’ Still logged in 5. **Logout** - Click logout โ†’ Token cleared โ†’ Redirected to login 6. **Permission-Based UI** - User with permission sees button - User without permission doesn't see button --- ## ๐Ÿ“ฆ Deliverables - [ ] Login page with validation - [ ] Zustand auth store - [ ] Auth API client - [ ] Protected route middleware - [ ] Permission-based UI components - [ ] User menu with logout - [ ] Session persistence --- ## ๐Ÿ”— Related Documents - [ADR-014: State Management](../../05-decisions/ADR-014-state-management.md) - [ADR-013: Form Handling](../../05-decisions/ADR-013-form-handling-validation.md) - [TASK-BE-002: Auth & RBAC](./TASK-BE-002-auth-rbac.md) --- **Created:** 2025-12-01 **Status:** Ready