# User Registration & Password Reset - UI Adaptations ## Overview This document describes the necessary UI changes and adaptations required in the Nyla frontend (`frontend_nyla`) to implement the magic link-based user authentication process described in `doc_userauth_process_concept.md`. **Last Updated**: Based on codebase analysis of current Nyla frontend implementation. **Frontend Stack**: React 19, TypeScript, Vite, React Router **Pattern**: Hooks-based architecture (`useAuthentication.ts`), component-based pages ## Current Frontend State ### Existing Pages 1. **`src/pages/Register.tsx`** - Contains registration form with: - Username field (with availability check) - Password field (required) - Confirm password field (required) - Email field - Full name field - Language selector (defaults to 'de') - Uses `useRegister()` hook from `useAuthentication.ts` - **Needs modification**: Remove password fields, add email-only registration 2. **`src/pages/Login.tsx`** - Contains login form with: - Username field - Password field - Microsoft authentication button - Google authentication button (placeholder) - Registration link - **Missing**: Password reset button/link 3. **`src/hooks/useAuthentication.ts`** - Contains `useRegister()` hook that sends password - Contains `useAuth()` hook for login - Contains `useUsernameAvailability()` hook - **Needs modification**: Update `useRegister()` to handle no-password registration - **Missing**: Password reset request and reset password hooks 4. **`src/api.ts`** - Axios instance with interceptors - Base URL from environment variables - **No changes needed** - existing API setup is sufficient ## Required UI Changes ### 1. Login Page (`src/pages/Login.tsx`) #### Changes Required: - Add "Password Reset" link/button below the login form - Link should navigate to `/password-reset-request` route - Style should match existing link styles (use `styles.textButton`) #### Implementation: ```tsx // Add after the disclaimer div, before the login button
``` #### Styling Considerations: - Use existing CSS classes from `Login.module.css` - Match styling with registration link - Ensure responsive design matches login page layout ### 2. Registration Page (`src/pages/Register.tsx`) #### Changes Required: - **Remove** password and confirm password fields - **Keep** username, email, fullName fields - Update form validation to not require password - Update success message to indicate email will be sent - Add spam folder reminder message #### Implementation Changes: **Remove password-related state:** ```tsx // Remove these lines: const [passwordFocused, setPasswordFocused] = useState(false); const [confirmPasswordFocused, setConfirmPasswordFocused] = useState(false); // Update formData interface: interface RegisterFormData { username: string; // password: string; // REMOVED // confirmPassword: string; // REMOVED email: string; fullName: string; } // Update initial state: const [formData, setFormData] = useState({ username: '', // password: '', // REMOVED // confirmPassword: '', // REMOVED email: '', fullName: '' }); ``` **Update validation:** ```tsx const validateForm = (): boolean => { // Remove password checks if (!formData.username || !formData.email || !formData.fullName) { setValidationError('Bitte füllen Sie alle Pflichtfelder aus.'); return false; } if (!formData.email.includes('@')) { setValidationError('Bitte geben Sie eine gültige E-Mail-Adresse ein.'); return false; } return true; }; ``` **Update success message:** ```tsx // After successful registration: await register(registrationData); navigate('/login', { state: { registered: true, message: 'Registrierung erfolgreich! Bitte prüfen Sie Ihre E-Mail, um Ihr Passwort zu setzen. Falls Sie keine E-Mail erhalten, prüfen Sie bitte auch Ihren Spam-Ordner.' } }); ``` **Remove password input fields from JSX:** ```tsx // Remove these divs: {/*
*/} ``` **Add info message:** ```tsx // Add after the disclaimer, before the submit button:

Nach der Registrierung erhalten Sie eine E-Mail mit einem Link zum Setzen Ihres Passworts.

Bitte prüfen Sie auch Ihren Spam-Ordner, falls Sie keine E-Mail erhalten.

``` ### 3. New Page: Password Reset Request (`src/pages/PasswordResetRequest.tsx`) #### Purpose: Allow users to request a password reset by entering their email address. #### Structure: - Similar layout to `Register.tsx` and `Login.tsx` - Single email input field - Submit button - Link back to login page - Success/error message area #### Implementation: ```tsx import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { usePasswordResetRequest } from '../hooks/useAuthentication'; import styles from './PasswordResetRequest.module.css'; function PasswordResetRequest() { const navigate = useNavigate(); const { requestPasswordReset, error, isLoading } = usePasswordResetRequest(); const [email, setEmail] = useState(''); const [emailFocused, setEmailFocused] = useState(false); const [successMessage, setSuccessMessage] = useState(null); useEffect(() => { document.title = "PowerOn AI Platform - Passwort zurücksetzen"; }, []); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!email || !email.includes('@')) { return; } try { await requestPasswordReset(email); setSuccessMessage('Falls ein Konto mit dieser E-Mail-Adresse existiert, wurde ein Reset-Link gesendet. Bitte prüfen Sie Ihre E-Mail und auch Ihren Spam-Ordner.'); // Redirect to login after showing message setTimeout(() => { navigate('/login', { state: { message: 'Falls ein Konto mit dieser E-Mail-Adresse existiert, wurde ein Reset-Link gesendet.' } }); }, 3000); } catch (err) { console.error('Password reset request failed:', err); } }; return (
Power On
{error && (
{error}
)} {successMessage && (
{successMessage}
)}

Passwort zurücksetzen

setEmail(e.target.value)} onFocus={() => setEmailFocused(true)} onBlur={() => setEmailFocused(false)} className={`${styles.input} ${emailFocused || email ? styles.focused : ''}`} />
Zurück zum
); } export default PasswordResetRequest; ``` #### CSS Module (`src/pages/PasswordResetRequest.module.css`): - Copy styles from `Register.module.css` or `Login.module.css` - Add styles for `.success` message (green background) - Add styles for `.title` heading ### 4. New Page: Password Reset (`src/pages/ResetPassword.tsx`) #### Purpose: Allow users to set a new password using the token from the magic link. #### Structure: - Similar layout to `Register.tsx` - Password field (with strength indicator) - Confirm password field - Submit button - Extract token from URL parameter (`?token=`) - Success/error message area #### Implementation: ```tsx import { useState, useEffect } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { useResetPassword } from '../hooks/useAuthentication'; import styles from './ResetPassword.module.css'; function ResetPassword() { const navigate = useNavigate(); const [searchParams] = useSearchParams(); const { resetPassword, error, isLoading } = useResetPassword(); const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [passwordFocused, setPasswordFocused] = useState(false); const [confirmPasswordFocused, setConfirmPasswordFocused] = useState(false); const [validationError, setValidationError] = useState(null); const [successMessage, setSuccessMessage] = useState(null); const token = searchParams.get('token'); useEffect(() => { document.title = "PowerOn AI Platform - Neues Passwort setzen"; // Validate token format (UUID) if (!token || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(token)) { setValidationError('Ungültiger oder fehlender Reset-Token.'); } }, [token]); const validateForm = (): boolean => { if (!password || !confirmPassword) { setValidationError('Bitte füllen Sie alle Felder aus.'); return false; } if (password.length < 8) { setValidationError('Passwort muss mindestens 8 Zeichen lang sein.'); return false; } if (password !== confirmPassword) { setValidationError('Die Passwörter stimmen nicht überein.'); return false; } return true; }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!validateForm() || !token) { return; } try { await resetPassword(token, password); setSuccessMessage('Passwort erfolgreich gesetzt! Sie werden zum Login weitergeleitet...'); // Redirect to login after 3 seconds setTimeout(() => { navigate('/login', { state: { message: 'Passwort erfolgreich gesetzt. Bitte melden Sie sich an.' } }); }, 3000); } catch (err) { console.error('Password reset failed:', err); } }; if (!token) { return (
Ungültiger oder fehlender Reset-Token. Bitte fordern Sie einen neuen Reset-Link an.
); } return (
Power On

Neues Passwort setzen

{(validationError || error) && (
{validationError || error}
)} {successMessage && (
{successMessage}
)}
setPassword(e.target.value)} onFocus={() => setPasswordFocused(true)} onBlur={() => setPasswordFocused(false)} className={`${styles.input} ${passwordFocused || password ? styles.focused : ''}`} />
Mindestens 8 Zeichen
setConfirmPassword(e.target.value)} onFocus={() => setConfirmPasswordFocused(true)} onBlur={() => setConfirmPasswordFocused(false)} className={`${styles.input} ${confirmPasswordFocused || confirmPassword ? styles.focused : ''}`} />
Zurück zum
); } export default ResetPassword; ``` #### CSS Module (`src/pages/ResetPassword.module.css`): - Copy styles from `Register.module.css` - Add styles for `.success` message - Add styles for `.title` heading - Add styles for `.passwordHint` (small text below password field) ### 5. Authentication Hooks (`src/hooks/useAuthentication.ts`) #### New Hooks Required: 1. **`usePasswordResetRequest()`** ```tsx export function usePasswordResetRequest() { const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); const requestPasswordReset = async (email: string): Promise => { setIsLoading(true); setError(null); try { const response = await api.post('/api/local/password-reset-request', { email }); // Backend always returns success (security: don't reveal if email exists) // No need to check response data } catch (error: any) { let errorMessage = 'An error occurred during password reset request'; if (error.response) { if (error.response.data?.detail) { if (Array.isArray(error.response.data.detail)) { errorMessage = error.response.data.detail.map((err: any) => err.msg).join(', '); } else { errorMessage = error.response.data.detail; } } } else if (error.request) { errorMessage = 'No response received from server'; } else { errorMessage = error.message; } setError(errorMessage); throw error; } finally { setIsLoading(false); } }; return { requestPasswordReset, error, isLoading }; } ``` 2. **`useResetPassword()`** ```tsx export function useResetPassword() { const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); const resetPassword = async (token: string, password: string): Promise => { setIsLoading(true); setError(null); try { const response = await api.post('/api/local/password-reset', { token, password }); // Success - password reset completed } catch (error: any) { let errorMessage = 'An error occurred during password reset'; if (error.response) { if (error.response.data?.detail) { if (Array.isArray(error.response.data.detail)) { errorMessage = error.response.data.detail.map((err: any) => err.msg).join(', '); } else { errorMessage = error.response.data.detail; } } } else if (error.request) { errorMessage = 'No response received from server'; } else { errorMessage = error.message; } setError(errorMessage); throw error; } finally { setIsLoading(false); } }; return { resetPassword, error, isLoading }; } ``` #### Update `useRegister()` Hook: **Modify the `register` function:** ```tsx const register = async (userData: RegisterData): Promise => { setIsLoading(true); setError(null); try { // Remove password from dataToSend const dataToSend = { userData: { username: userData.username, email: userData.email, fullName: userData.fullName, language: userData.language || 'de', enabled: userData.enabled !== undefined ? userData.enabled : true, privilege: userData.privilege || 'user' } // password: userData.password // REMOVED }; const response = await api.post('/api/local/register', dataToSend, { headers: { 'Content-Type': 'application/json' } }); return { success: true, message: 'Registration successful. Please check your email to set your password.', user: response.data }; } catch (error: any) { // ... existing error handling ... } finally { setIsLoading(false); } }; ``` **Update `RegisterData` interface:** ```tsx interface RegisterData { username: string; // password: string; // REMOVED - no longer required email: string; fullName: string; language?: string; enabled?: boolean; privilege?: string; } ``` ### 6. Routing Configuration #### Update `src/App.tsx` or routing configuration: Add routes for new pages: ```tsx import PasswordResetRequest from './pages/PasswordResetRequest'; import ResetPassword from './pages/ResetPassword'; // In your routes: } /> } /> ``` ## UI/UX Considerations ### Error Handling 1. **Registration Errors**: - Username already exists → Show error on username field (already implemented) - Email already exists → Show generic success (security: don't reveal email exists) - Email sending fails → Show generic success (don't reveal email issues) 2. **Password Reset Request Errors**: - Invalid email format → Show error on email field - Email not found → Show generic success (security: don't reveal email doesn't exist) - Rate limiting → Show error message 3. **Password Reset Errors**: - Invalid/expired token → Show error message, link back to password reset request - Password too weak → Show specific requirements - Password mismatch → Show error on confirm password field ### Success Messages 1. **Registration Success**: ``` "Registrierung erfolgreich! Bitte prüfen Sie Ihre E-Mail, um Ihr Passwort zu setzen. Falls Sie keine E-Mail erhalten, prüfen Sie bitte auch Ihren Spam-Ordner." ``` 2. **Password Reset Request Success**: ``` "Falls ein Konto mit dieser E-Mail-Adresse existiert, wurde ein Reset-Link gesendet. Bitte prüfen Sie Ihre E-Mail und auch Ihren Spam-Ordner." ``` 3. **Password Reset Success**: ``` "Passwort erfolgreich gesetzt! Sie werden zum Login weitergeleitet..." ``` ### Accessibility - All form fields should have proper labels (already implemented with floating labels) - Error messages should be associated with form fields using ARIA attributes - Success messages should be announced to screen readers - Form validation should provide clear, actionable feedback ### Responsive Design - All pages should work on mobile devices (existing styles should handle this) - Form layouts should adapt to smaller screens - Buttons should be appropriately sized for touch interfaces - Error messages should be readable on all screen sizes ## Testing Checklist ### Registration Flow - [ ] User can register without password - [ ] Email validation works correctly - [ ] Success message displays correctly - [ ] Redirect to login works - [ ] Error handling for duplicate username - [ ] Error handling for duplicate email (should show generic success) ### Password Reset Request Flow - [ ] User can access password reset request page from login - [ ] Email validation works correctly - [ ] Success message displays correctly - [ ] Redirect to login works - [ ] Error handling for invalid email format - [ ] Error handling for rate limiting ### Password Reset Flow - [ ] User can access reset page with valid token - [ ] Token extraction from URL works - [ ] Password validation works correctly - [ ] Password confirmation validation works - [ ] Success message displays correctly - [ ] Redirect to login works after 3 seconds - [ ] Error handling for invalid token - [ ] Error handling for expired token - [ ] Error handling for weak password ### Integration Testing - [ ] End-to-end registration flow works - [ ] End-to-end password reset flow works - [ ] Email links work correctly - [ ] Token expiration handling works - [ ] Multiple reset requests invalidate old tokens ## Implementation Order 1. **Backend Changes First** (prerequisites): - Add resetToken fields to UserInDB model - Implement password reset endpoints - Implement email sending functionality 2. **Frontend Hooks**: - Add `usePasswordResetRequest()` hook - Add `useResetPassword()` hook - Update `useRegister()` hook (remove password requirement) 3. **Frontend Pages**: - Create `PasswordResetRequest.tsx` - Create `ResetPassword.tsx` - Update `Login.tsx` (add reset button) - Update `Register.tsx` (remove password fields) 4. **Routing**: - Add routes for new pages 5. **Styling**: - Create CSS modules for new pages - Add success message styles - Ensure consistent styling 6. **Testing**: - Test each flow independently - Test integration between frontend and backend - Test error scenarios - Test edge cases ## Notes - All text should be in German to match existing UI (`Login.tsx` uses German) - CSS modules should match existing patterns from `Register.module.css` and `Login.module.css` - Form validation should use existing patterns from `Register.tsx` - Error handling should use existing patterns (error state, error display) - Success messages should use new success state pattern - API calls should use existing `api.ts` instance - Hooks should follow existing patterns from `useAuthentication.ts` - Token validation should check UUID format before making API call - Use React Router's `useSearchParams` for token extraction from URL ## File Structure ``` frontend_nyla/ ├── src/ │ ├── pages/ │ │ ├── Login.tsx (MODIFY - add password reset link) │ │ ├── Register.tsx (MODIFY - remove password fields) │ │ ├── PasswordResetRequest.tsx (NEW) │ │ ├── ResetPassword.tsx (NEW) │ │ ├── Login.module.css (MODIFY - add password reset link styles) │ │ ├── Register.module.css (MODIFY - add info message styles) │ │ ├── PasswordResetRequest.module.css (NEW) │ │ └── ResetPassword.module.css (NEW) │ ├── hooks/ │ │ └── useAuthentication.ts (MODIFY - update useRegister, add new hooks) │ └── App.tsx (MODIFY - add routes) ```