25 KiB
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
-
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 fromuseAuthentication.ts - Needs modification: Remove password fields, add email-only registration
- Contains registration form with:
-
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
- Contains login form with:
-
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
- Contains
-
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-requestroute - Style should match existing link styles (use
styles.textButton)
Implementation:
// Add after the disclaimer div, before the login button
<div className={styles.passwordResetLink}>
<button
className={styles.textButton}
onClick={() => navigate("/password-reset-request")}
>
Passwort zurücksetzen
</button>
</div>
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:
// 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<RegisterFormData>({
username: '',
// password: '', // REMOVED
// confirmPassword: '', // REMOVED
email: '',
fullName: ''
});
Update validation:
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:
// 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:
// Remove these divs:
{/* <div className={styles.floatingLabelInput}>
<input type="password" name="password" ... />
<label>Passwort</label>
</div>
<div className={styles.floatingLabelInput}>
<input type="password" name="confirmPassword" ... />
<label>Passwort bestätigen</label>
</div> */}
Add info message:
// Add after the disclaimer, before the submit button:
<div className={styles.infoMessage}>
<p>Nach der Registrierung erhalten Sie eine E-Mail mit einem Link zum Setzen Ihres Passworts.</p>
<p className={styles.spamReminder}>Bitte prüfen Sie auch Ihren Spam-Ordner, falls Sie keine E-Mail erhalten.</p>
</div>
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.tsxandLogin.tsx - Single email input field
- Submit button
- Link back to login page
- Success/error message area
Implementation:
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<string | null>(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 (
<div className={styles.container}>
<div className={styles.mainContent}>
<div className={styles.logo}>
<div className={styles.logoText}>
<span className={styles.logoPower}>Power</span>
<span className={styles.logoOn}>On</span>
</div>
</div>
<div className={styles.loginSection}>
<div className={styles.loginBox}>
<div className={styles.loginForm}>
{error && (
<div className={styles.error}>{error}</div>
)}
{successMessage && (
<div className={styles.success}>{successMessage}</div>
)}
<h2 className={styles.title}>Passwort zurücksetzen</h2>
<div className={styles.floatingLabelInput}>
<input
type="email"
name="email"
placeholder=" "
value={email}
onChange={(e) => setEmail(e.target.value)}
onFocus={() => setEmailFocused(true)}
onBlur={() => setEmailFocused(false)}
className={`${styles.input} ${emailFocused || email ? styles.focused : ''}`}
/>
<label className={emailFocused || email ? styles.focusedLabel : styles.label}>
E-Mail-Adresse
</label>
</div>
<button
className={`${styles.button} ${styles.loginButton}`}
onClick={handleSubmit}
disabled={isLoading || !email}
>
{isLoading ? "wird geladen..." : "Reset-Link anfordern"}
</button>
<div className={styles.registerLink}>
<span>Zurück zum</span>
<button
className={styles.textButton}
onClick={() => navigate("/login")}
>
Login
</button>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
export default PasswordResetRequest;
CSS Module (src/pages/PasswordResetRequest.module.css):
- Copy styles from
Register.module.cssorLogin.module.css - Add styles for
.successmessage (green background) - Add styles for
.titleheading
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=<UUID>) - Success/error message area
Implementation:
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<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(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 (
<div className={styles.container}>
<div className={styles.mainContent}>
<div className={styles.error}>
Ungültiger oder fehlender Reset-Token. Bitte fordern Sie einen neuen Reset-Link an.
</div>
<button
className={styles.button}
onClick={() => navigate('/password-reset-request')}
>
Reset-Link anfordern
</button>
</div>
</div>
);
}
return (
<div className={styles.container}>
<div className={styles.mainContent}>
<div className={styles.logo}>
<div className={styles.logoText}>
<span className={styles.logoPower}>Power</span>
<span className={styles.logoOn}>On</span>
</div>
</div>
<div className={styles.loginSection}>
<div className={styles.loginBox}>
<div className={styles.loginForm}>
<h2 className={styles.title}>Neues Passwort setzen</h2>
{(validationError || error) && (
<div className={styles.error}>{validationError || error}</div>
)}
{successMessage && (
<div className={styles.success}>{successMessage}</div>
)}
<div className={styles.floatingLabelInput}>
<input
type="password"
name="password"
placeholder=" "
value={password}
onChange={(e) => setPassword(e.target.value)}
onFocus={() => setPasswordFocused(true)}
onBlur={() => setPasswordFocused(false)}
className={`${styles.input} ${passwordFocused || password ? styles.focused : ''}`}
/>
<label className={passwordFocused || password ? styles.focusedLabel : styles.label}>
Neues Passwort
</label>
</div>
<small className={styles.passwordHint}>Mindestens 8 Zeichen</small>
<div className={styles.floatingLabelInput}>
<input
type="password"
name="confirmPassword"
placeholder=" "
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
onFocus={() => setConfirmPasswordFocused(true)}
onBlur={() => setConfirmPasswordFocused(false)}
className={`${styles.input} ${confirmPasswordFocused || confirmPassword ? styles.focused : ''}`}
/>
<label className={confirmPasswordFocused || confirmPassword ? styles.focusedLabel : styles.label}>
Passwort bestätigen
</label>
</div>
<button
className={`${styles.button} ${styles.loginButton}`}
onClick={handleSubmit}
disabled={isLoading || !password || !confirmPassword}
>
{isLoading ? "wird geladen..." : "Passwort setzen"}
</button>
<div className={styles.registerLink}>
<span>Zurück zum</span>
<button
className={styles.textButton}
onClick={() => navigate("/login")}
>
Login
</button>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
export default ResetPassword;
CSS Module (src/pages/ResetPassword.module.css):
- Copy styles from
Register.module.css - Add styles for
.successmessage - Add styles for
.titleheading - Add styles for
.passwordHint(small text below password field)
5. Authentication Hooks (src/hooks/useAuthentication.ts)
New Hooks Required:
usePasswordResetRequest()
export function usePasswordResetRequest() {
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const requestPasswordReset = async (email: string): Promise<void> => {
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
};
}
useResetPassword()
export function useResetPassword() {
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const resetPassword = async (token: string, password: string): Promise<void> => {
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:
const register = async (userData: RegisterData): Promise<RegisterResponse> => {
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:
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:
import PasswordResetRequest from './pages/PasswordResetRequest';
import ResetPassword from './pages/ResetPassword';
// In your routes:
<Route path="/password-reset-request" element={<PasswordResetRequest />} />
<Route path="/reset" element={<ResetPassword />} />
UI/UX Considerations
Error Handling
-
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)
-
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
-
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
-
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." -
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." -
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
-
Backend Changes First (prerequisites):
- Add resetToken fields to UserInDB model
- Implement password reset endpoints
- Implement email sending functionality
-
Frontend Hooks:
- Add
usePasswordResetRequest()hook - Add
useResetPassword()hook - Update
useRegister()hook (remove password requirement)
- Add
-
Frontend Pages:
- Create
PasswordResetRequest.tsx - Create
ResetPassword.tsx - Update
Login.tsx(add reset button) - Update
Register.tsx(remove password fields)
- Create
-
Routing:
- Add routes for new pages
-
Styling:
- Create CSS modules for new pages
- Add success message styles
- Ensure consistent styling
-
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.tsxuses German) - CSS modules should match existing patterns from
Register.module.cssandLogin.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.tsinstance - Hooks should follow existing patterns from
useAuthentication.ts - Token validation should check UUID format before making API call
- Use React Router's
useSearchParamsfor 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)