concepts updated
This commit is contained in:
parent
7e04d70683
commit
6c95f6ccb4
4 changed files with 2651 additions and 0 deletions
1514
implementation/implementation_ai-call-looping-architecture_done.md
Normal file
1514
implementation/implementation_ai-call-looping-architecture_done.md
Normal file
File diff suppressed because it is too large
Load diff
250
reviews/20260111 doc_session_handling_analysis.md
Normal file
250
reviews/20260111 doc_session_handling_analysis.md
Normal file
|
|
@ -0,0 +1,250 @@
|
||||||
|
# Session Handling Analysis for Horizontal Scaling
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
**✅ YES, your application is STATELESS and ready for horizontal scaling with load balancers.**
|
||||||
|
|
||||||
|
The session handling architecture is designed to work across multiple gateway instances without requiring sticky sessions or shared in-memory storage. Users will **NOT** lose their sessions when API calls hit different gateway instances.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
### Backend Session Management (Gateway)
|
||||||
|
|
||||||
|
#### 1. **JWT Token-Based Authentication**
|
||||||
|
- **Location**: `gateway/modules/auth/authentication.py`
|
||||||
|
- **Token Storage**: JWT tokens are stored in **httpOnly cookies** (`auth_token` and `refresh_token`)
|
||||||
|
- **Token Format**: Self-contained JWT tokens with claims including:
|
||||||
|
- `sub` (username)
|
||||||
|
- `userId`
|
||||||
|
- `mandateId`
|
||||||
|
- `jti` (token ID)
|
||||||
|
- `sid` (session ID)
|
||||||
|
- `authenticationAuthority`
|
||||||
|
- `exp` (expiration)
|
||||||
|
|
||||||
|
#### 2. **Database-Backed Token Validation**
|
||||||
|
- **Location**: `gateway/modules/interfaces/interfaceDbAppObjects.py`
|
||||||
|
- **Token Table**: All tokens are stored in a `Token` database table with fields:
|
||||||
|
- `id` (jti - token ID)
|
||||||
|
- `userId`
|
||||||
|
- `authority`
|
||||||
|
- `sessionId`
|
||||||
|
- `mandateId`
|
||||||
|
- `status` (ACTIVE/REVOKED)
|
||||||
|
- `expiresAt`
|
||||||
|
- `revokedAt`, `revokedBy`, `reason`
|
||||||
|
|
||||||
|
- **Validation Process** (per request):
|
||||||
|
1. JWT token is extracted from httpOnly cookie or Authorization header
|
||||||
|
2. Token is decoded and validated (signature, expiration)
|
||||||
|
3. Token ID (`jti`) is extracted from the JWT payload
|
||||||
|
4. **Database query** is performed to verify:
|
||||||
|
- Token exists in database
|
||||||
|
- Token status is ACTIVE
|
||||||
|
- Token matches user, session, and mandate context
|
||||||
|
5. User is retrieved from database based on token claims
|
||||||
|
|
||||||
|
**Key Code Reference** (`gateway/modules/auth/authentication.py:141-191`):
|
||||||
|
```python
|
||||||
|
# For LOCAL gateway JWTs, enforce DB-backed token validity and revocation
|
||||||
|
if tokenId:
|
||||||
|
db_tokens = dbApp.getRecordset(Token, recordFilter={"id": tokenId})
|
||||||
|
|
||||||
|
if db_tokens:
|
||||||
|
db_token = db_tokens[0]
|
||||||
|
token_authority = str(db_token.get("authority", "")).lower()
|
||||||
|
if token_authority == str(AuthAuthority.LOCAL.value):
|
||||||
|
# Must be active and match user/session/mandate
|
||||||
|
active_token = appInterface.findActiveTokenById(
|
||||||
|
tokenId=tokenId,
|
||||||
|
userId=user.id,
|
||||||
|
authority=AuthAuthority.LOCAL,
|
||||||
|
sessionId=sessionId,
|
||||||
|
mandateId=str(mandateId) if mandateId else None,
|
||||||
|
)
|
||||||
|
if not active_token:
|
||||||
|
raise credentialsException
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. **No In-Memory Session Storage**
|
||||||
|
- ✅ **No Redis** - No Redis or similar caching layer found
|
||||||
|
- ✅ **No Memcached** - No memcached usage found
|
||||||
|
- ✅ **No In-Memory Sessions** - All session state is in the database
|
||||||
|
- ✅ **Stateless Design** - Each request is independently validated
|
||||||
|
|
||||||
|
#### 4. **Session Management**
|
||||||
|
- **Session ID**: Generated on login (`uuid.uuid4()`) and stored in:
|
||||||
|
- JWT token claim (`sid`)
|
||||||
|
- Token database record (`sessionId`)
|
||||||
|
- **Logout**: Revokes all tokens for a session by updating database records (sets `status=REVOKED`)
|
||||||
|
- **Token Refresh**: Creates new tokens and stores them in database
|
||||||
|
|
||||||
|
**Key Code Reference** (`gateway/modules/routes/routeSecurityLocal.py:92-131`):
|
||||||
|
```python
|
||||||
|
# Create session id and include in token claims for session-scoped logout
|
||||||
|
session_id = str(uuid.uuid4())
|
||||||
|
token_data["sid"] = session_id
|
||||||
|
|
||||||
|
# Create access token + set cookie
|
||||||
|
access_token, _access_expires = createAccessToken(token_data)
|
||||||
|
setAccessTokenCookie(response, access_token)
|
||||||
|
|
||||||
|
# Save access token to database
|
||||||
|
token = Token(
|
||||||
|
id=jti,
|
||||||
|
userId=user.id,
|
||||||
|
authority=AuthAuthority.LOCAL,
|
||||||
|
tokenAccess=access_token,
|
||||||
|
tokenType="bearer",
|
||||||
|
expiresAt=expires_at.timestamp(),
|
||||||
|
sessionId=session_id,
|
||||||
|
mandateId=str(user.mandateId)
|
||||||
|
)
|
||||||
|
userInterface.saveAccessToken(token)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend Session Management
|
||||||
|
|
||||||
|
#### 1. **Cookie-Based Token Storage**
|
||||||
|
- **Location**: `frontend_agents/public/js/security/auth.js` and `frontend_agents/public/js/shared/apiCalls.js`
|
||||||
|
- **Storage Method**: Tokens are stored in **httpOnly cookies** (not localStorage or sessionStorage)
|
||||||
|
- **Automatic Transmission**: Cookies are automatically sent with requests using `credentials: 'include'`
|
||||||
|
|
||||||
|
**Key Code Reference** (`frontend_agents/public/js/shared/apiCalls.js:151-153`):
|
||||||
|
```javascript
|
||||||
|
// Note: With httpOnly cookies, we don't need to manually add Authorization header
|
||||||
|
// The browser automatically includes cookies with credentials: 'include'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. **CSRF Token Storage**
|
||||||
|
- **Location**: `sessionStorage` (client-side only)
|
||||||
|
- **Purpose**: CSRF protection, not session state
|
||||||
|
- **Note**: CSRF tokens can be regenerated if lost, so this doesn't affect session persistence
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Horizontal Scaling Compatibility
|
||||||
|
|
||||||
|
### ✅ **Fully Compatible - No Issues**
|
||||||
|
|
||||||
|
#### Why It Works:
|
||||||
|
|
||||||
|
1. **Stateless Backend**
|
||||||
|
- Each gateway instance validates tokens independently
|
||||||
|
- No shared in-memory state between instances
|
||||||
|
- All state is in the shared database
|
||||||
|
|
||||||
|
2. **Database as Single Source of Truth**
|
||||||
|
- Token validation queries the database on every request
|
||||||
|
- Token revocation updates the database
|
||||||
|
- All instances see the same token state
|
||||||
|
|
||||||
|
3. **Cookie-Based Tokens**
|
||||||
|
- Cookies are sent by the browser to whichever instance handles the request
|
||||||
|
- No server-side session storage needed
|
||||||
|
- Load balancer doesn't need sticky sessions
|
||||||
|
|
||||||
|
4. **JWT Self-Contained Claims**
|
||||||
|
- Token contains all necessary user context
|
||||||
|
- Database validation ensures token hasn't been revoked
|
||||||
|
- No need to look up session state from another instance
|
||||||
|
|
||||||
|
### Load Balancer Configuration
|
||||||
|
|
||||||
|
**Recommended Settings:**
|
||||||
|
- ✅ **Session Affinity**: **NOT REQUIRED** (can use round-robin or least-connections)
|
||||||
|
- ✅ **Health Checks**: Standard HTTP health checks
|
||||||
|
- ✅ **Cookie Handling**: No special configuration needed (browser handles cookies automatically)
|
||||||
|
|
||||||
|
### Potential Considerations
|
||||||
|
|
||||||
|
#### 1. **Database Connection Pooling**
|
||||||
|
- Ensure each gateway instance has proper database connection pooling
|
||||||
|
- Database should handle concurrent connections from multiple instances
|
||||||
|
- **Status**: ✅ Should work fine if database is configured for multiple connections
|
||||||
|
|
||||||
|
#### 2. **CSRF Token Regeneration**
|
||||||
|
- CSRF tokens stored in `sessionStorage` may be lost if user switches instances
|
||||||
|
- **Impact**: Minimal - CSRF tokens are regenerated automatically
|
||||||
|
- **Code Reference**: `frontend_agents/public/js/shared/apiCalls.js:186-203` handles CSRF token generation
|
||||||
|
|
||||||
|
#### 3. **Token Refresh Race Conditions**
|
||||||
|
- If multiple requests refresh tokens simultaneously, ensure database handles concurrent updates
|
||||||
|
- **Status**: ✅ Current implementation uses database transactions (via `saveAccessToken`)
|
||||||
|
|
||||||
|
#### 4. **Cookie Domain and Path**
|
||||||
|
- Ensure cookies are set with correct domain/path for load balancer
|
||||||
|
- **Current Settings** (`gateway/modules/auth/jwtService.py:58-66`):
|
||||||
|
- `path="/"` ✅
|
||||||
|
- `samesite="strict"` ✅
|
||||||
|
- `httponly=True` ✅
|
||||||
|
- `secure` (based on HTTPS) ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Recommendations
|
||||||
|
|
||||||
|
### 1. **Multi-Instance Test**
|
||||||
|
- Deploy 2+ gateway instances behind a load balancer
|
||||||
|
- Login on one instance
|
||||||
|
- Make requests that hit different instances
|
||||||
|
- Verify session persists across instances
|
||||||
|
|
||||||
|
### 2. **Token Revocation Test**
|
||||||
|
- Login on instance A
|
||||||
|
- Logout on instance B
|
||||||
|
- Verify token is revoked (cannot make requests on instance A)
|
||||||
|
|
||||||
|
### 3. **Concurrent Request Test**
|
||||||
|
- Make multiple simultaneous requests
|
||||||
|
- Verify all requests succeed regardless of which instance handles them
|
||||||
|
|
||||||
|
### 4. **Database Connection Test**
|
||||||
|
- Monitor database connections from multiple instances
|
||||||
|
- Verify connection pooling works correctly
|
||||||
|
- Check for connection leaks
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
| Aspect | Status | Notes |
|
||||||
|
|--------|--------|-------|
|
||||||
|
| **Stateless Backend** | ✅ YES | No in-memory session storage |
|
||||||
|
| **Database-Backed** | ✅ YES | All token state in database |
|
||||||
|
| **Cookie-Based** | ✅ YES | httpOnly cookies, auto-sent by browser |
|
||||||
|
| **Load Balancer Ready** | ✅ YES | No sticky sessions needed |
|
||||||
|
| **Horizontal Scaling** | ✅ READY | Can scale to multiple instances |
|
||||||
|
|
||||||
|
### Conclusion
|
||||||
|
|
||||||
|
**Your application is fully ready for horizontal scaling.** The session handling architecture is stateless and database-backed, which means:
|
||||||
|
|
||||||
|
1. ✅ Users will **NOT** lose sessions when requests hit different instances
|
||||||
|
2. ✅ Load balancer can use **round-robin** or **least-connections** (no sticky sessions needed)
|
||||||
|
3. ✅ Token validation works independently on each instance
|
||||||
|
4. ✅ Token revocation works across all instances (via database)
|
||||||
|
|
||||||
|
The only shared state is in the database, which you've confirmed will be a single logical instance. This is the correct architecture for horizontal scaling.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Analyzed
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- `gateway/modules/auth/authentication.py` - Token validation
|
||||||
|
- `gateway/modules/auth/jwtService.py` - JWT creation and cookie management
|
||||||
|
- `gateway/modules/routes/routeSecurityLocal.py` - Login/logout endpoints
|
||||||
|
- `gateway/modules/interfaces/interfaceDbAppObjects.py` - Token database operations
|
||||||
|
- `gateway/modules/datamodels/datamodelSecurity.py` - Token data model
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- `frontend_agents/public/js/security/auth.js` - Authentication logic
|
||||||
|
- `frontend_agents/public/js/shared/apiCalls.js` - API calls with cookie handling
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Analysis Date: 2025-01-27*
|
||||||
|
*Analyzed by: AI Assistant*
|
||||||
94
reviews/20260111 doc_session_handling_summary_operations.md
Normal file
94
reviews/20260111 doc_session_handling_summary_operations.md
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
# PowerOn Gateway - Session Handling for Horizontal Scaling
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
**Status: ✅ READY FOR HORIZONTAL SCALING**
|
||||||
|
|
||||||
|
The PowerOn Gateway uses a stateless, database-backed session architecture that supports horizontal scaling with load balancers. User sessions persist across multiple gateway instances without requiring sticky sessions or shared in-memory storage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
### Session Management Approach
|
||||||
|
|
||||||
|
**Authentication Method**: JWT tokens stored in httpOnly cookies
|
||||||
|
- Access token: `auth_token` cookie
|
||||||
|
- Refresh token: `refresh_token` cookie
|
||||||
|
- Tokens contain user context (userId, mandateId, sessionId)
|
||||||
|
|
||||||
|
**Token Validation**: Database-backed
|
||||||
|
- All tokens stored in `Token` database table
|
||||||
|
- Each request validates token against database
|
||||||
|
- Token status: ACTIVE or REVOKED
|
||||||
|
- No in-memory session storage (no Redis/Memcached)
|
||||||
|
|
||||||
|
**Key Characteristics**:
|
||||||
|
- ✅ Stateless backend design
|
||||||
|
- ✅ Database as single source of truth
|
||||||
|
- ✅ Cookie-based token transmission
|
||||||
|
- ✅ Independent token validation per instance
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Load Balancer Configuration
|
||||||
|
|
||||||
|
### Recommended Settings
|
||||||
|
|
||||||
|
| Setting | Value | Notes |
|
||||||
|
|---------|-------|-------|
|
||||||
|
| **Session Affinity** | **NOT REQUIRED** | Can use round-robin or least-connections |
|
||||||
|
| **Health Checks** | Standard HTTP | Standard endpoint health checks |
|
||||||
|
| **Cookie Handling** | Default | Browser handles cookies automatically |
|
||||||
|
| **Sticky Sessions** | **NOT NEEDED** | Gateway instances are stateless |
|
||||||
|
|
||||||
|
### Cookie Configuration
|
||||||
|
|
||||||
|
Current cookie settings (configured in code):
|
||||||
|
- `path="/"` - Available across all paths
|
||||||
|
- `samesite="strict"` - CSRF protection
|
||||||
|
- `httponly=True` - XSS protection
|
||||||
|
- `secure` - Enabled when using HTTPS
|
||||||
|
|
||||||
|
**No special load balancer cookie configuration required.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deployment Requirements
|
||||||
|
|
||||||
|
### Database Configuration
|
||||||
|
|
||||||
|
**Requirements for Logical Database**:
|
||||||
|
- Single logical database instance (shared across all gateway instances)
|
||||||
|
- Database must be accessible from all gateway instances
|
||||||
|
- Database must support concurrent connections from multiple instances
|
||||||
|
- Each gateway instance requires proper database connection pooling
|
||||||
|
- Database should handle concurrent token validation queries efficiently
|
||||||
|
|
||||||
|
**Token Table**: Contains all session state
|
||||||
|
- Token ID (jti)
|
||||||
|
- User ID, Session ID, Mandate ID
|
||||||
|
- Status (ACTIVE/REVOKED)
|
||||||
|
- Expiration timestamps
|
||||||
|
|
||||||
|
### Gateway Instance Configuration
|
||||||
|
|
||||||
|
Each gateway instance:
|
||||||
|
- ✅ Operates independently
|
||||||
|
- ✅ Validates tokens via database queries
|
||||||
|
- ✅ No shared state with other instances
|
||||||
|
- ✅ Can be added/removed without affecting active sessions
|
||||||
|
|
||||||
|
|
||||||
|
### Key Points for Operations
|
||||||
|
|
||||||
|
1. ✅ **No sticky sessions required** - Load balancer can distribute requests freely
|
||||||
|
2. ✅ **Shared logical database required** - All instances access the same database
|
||||||
|
3. ✅ **Instances are independent** - Can add/remove instances without downtime
|
||||||
|
4. ✅ **Sessions persist across instances** - Users won't lose sessions during failover
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
*Document prepared for Operations Center*, Patrick Motsch, PowerON AG, 2026-01-11
|
||||||
|
|
||||||
|
|
@ -0,0 +1,793 @@
|
||||||
|
# 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
|
||||||
|
<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:**
|
||||||
|
```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<RegisterFormData>({
|
||||||
|
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:
|
||||||
|
{/* <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:**
|
||||||
|
```tsx
|
||||||
|
// 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.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<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.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=<UUID>`)
|
||||||
|
- 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<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 `.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<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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **`useResetPassword()`**
|
||||||
|
```tsx
|
||||||
|
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:**
|
||||||
|
```tsx
|
||||||
|
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:**
|
||||||
|
```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:
|
||||||
|
<Route path="/password-reset-request" element={<PasswordResetRequest />} />
|
||||||
|
<Route path="/reset" element={<ResetPassword />} />
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
```
|
||||||
Loading…
Reference in a new issue