Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
1623
web/maplepress-frontend/docs/ACCESS_REFRESH_TOKEN_IMPLEMENTATION.md
Normal file
1623
web/maplepress-frontend/docs/ACCESS_REFRESH_TOKEN_IMPLEMENTATION.md
Normal file
File diff suppressed because it is too large
Load diff
827
web/maplepress-frontend/docs/API/ADMIN_API.md
Normal file
827
web/maplepress-frontend/docs/API/ADMIN_API.md
Normal file
|
|
@ -0,0 +1,827 @@
|
|||
# Admin API Implementation (Account Management)
|
||||
|
||||
This document describes the implementation of the Admin API endpoints for the MaplePress frontend, integrated with the MaplePress backend API.
|
||||
|
||||
## Overview
|
||||
|
||||
The Admin API endpoints provide administrative operations for managing user accounts, specifically handling account lockouts that occur due to failed login attempts. These endpoints implement CWE-307 protection (Improper Restriction of Excessive Authentication Attempts) by allowing administrators to check lock status and manually unlock accounts.
|
||||
|
||||
**⚠️ SECURITY**: These endpoints require admin authentication and should only be accessible to users with admin or root roles.
|
||||
|
||||
## Backend API Endpoints
|
||||
|
||||
### Check Account Lock Status
|
||||
**Endpoint**: `GET /api/v1/admin/account-status`
|
||||
**Authentication**: Required (JWT token with admin role)
|
||||
**Query Parameters**: `email` (required)
|
||||
|
||||
### Unlock Locked Account
|
||||
**Endpoint**: `POST /api/v1/admin/unlock-account`
|
||||
**Authentication**: Required (JWT token with admin role)
|
||||
|
||||
**Source Files**:
|
||||
- `cloud/maplepress-backend/internal/interface/http/handler/admin/account_status_handler.go`
|
||||
- `cloud/maplepress-backend/internal/interface/http/handler/admin/unlock_account_handler.go`
|
||||
|
||||
## Request/Response Structures
|
||||
|
||||
### Check Account Status Request
|
||||
```
|
||||
GET /api/v1/admin/account-status?email=user@example.com
|
||||
```
|
||||
|
||||
**Headers Required:**
|
||||
- `Authorization: JWT {access_token}` (admin role required)
|
||||
|
||||
### Check Account Status Response
|
||||
```json
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"is_locked": true,
|
||||
"failed_attempts": 5,
|
||||
"remaining_time": "5 minutes 30 seconds",
|
||||
"remaining_seconds": 330
|
||||
}
|
||||
```
|
||||
|
||||
**When Account Not Locked:**
|
||||
```json
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"is_locked": false,
|
||||
"failed_attempts": 2
|
||||
}
|
||||
```
|
||||
|
||||
### Unlock Account Request
|
||||
```json
|
||||
{
|
||||
"email": "user@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
**Headers Required:**
|
||||
- `Content-Type: application/json`
|
||||
- `Authorization: JWT {access_token}` (admin role required)
|
||||
|
||||
### Unlock Account Response
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Account unlocked successfully",
|
||||
"email": "user@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
**When Account Not Locked:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Account is not locked",
|
||||
"email": "user@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
## Frontend Implementation
|
||||
|
||||
### AdminService (`src/services/API/AdminService.js`)
|
||||
|
||||
Handles all admin operations for account management.
|
||||
|
||||
**Key Features:**
|
||||
- Check account lock status with detailed information
|
||||
- Unlock locked accounts (with security event logging)
|
||||
- Helper to check if account needs unlocking
|
||||
- Client-side email validation
|
||||
- Remaining time formatting
|
||||
- Admin role enforcement with clear error messages
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `getAccountStatus(email)`
|
||||
Check if a user account is locked due to failed login attempts.
|
||||
|
||||
```javascript
|
||||
import AdminService from './services/API/AdminService';
|
||||
|
||||
const status = await AdminService.getAccountStatus("user@example.com");
|
||||
console.log(status);
|
||||
// Output:
|
||||
// {
|
||||
// email: "user@example.com",
|
||||
// isLocked: true,
|
||||
// failedAttempts: 5,
|
||||
// remainingTime: "5 minutes 30 seconds",
|
||||
// remainingSeconds: 330
|
||||
// }
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `email` (string, required): User's email address to check
|
||||
|
||||
**Returns:**
|
||||
```javascript
|
||||
{
|
||||
email: string, // User's email
|
||||
isLocked: boolean, // Whether account is locked
|
||||
failedAttempts: number, // Number of failed login attempts
|
||||
remainingTime: string, // Human-readable time until unlock
|
||||
remainingSeconds: number // Seconds until automatic unlock
|
||||
}
|
||||
```
|
||||
|
||||
**Throws:**
|
||||
- "Email is required" - If email is missing
|
||||
- "Email cannot be empty" - If email is empty after trimming
|
||||
- "Invalid email format" - If email format is invalid
|
||||
- "Admin authentication required. Please log in with admin credentials." - Missing/invalid admin token (401)
|
||||
- "Access denied. Admin privileges required for this operation." - User is not admin (403)
|
||||
|
||||
#### `unlockAccount(email)`
|
||||
Unlock a user account that has been locked due to failed login attempts.
|
||||
|
||||
```javascript
|
||||
const result = await AdminService.unlockAccount("user@example.com");
|
||||
console.log(result);
|
||||
// Output:
|
||||
// {
|
||||
// success: true,
|
||||
// message: "Account unlocked successfully",
|
||||
// email: "user@example.com"
|
||||
// }
|
||||
```
|
||||
|
||||
**⚠️ SECURITY EVENT**: This operation logs a security event (`ACCOUNT_UNLOCKED`) with the admin user ID who performed the unlock operation. This creates an audit trail for security compliance.
|
||||
|
||||
**Parameters:**
|
||||
- `email` (string, required): User's email address to unlock
|
||||
|
||||
**Returns:**
|
||||
```javascript
|
||||
{
|
||||
success: boolean,
|
||||
message: string,
|
||||
email: string
|
||||
}
|
||||
```
|
||||
|
||||
**Throws:**
|
||||
- "Email is required" - If email is missing
|
||||
- "Email cannot be empty" - If email is empty after trimming
|
||||
- "Invalid email format" - If email format is invalid
|
||||
- "Account is not currently locked." - If account is not locked
|
||||
- "Admin authentication required. Please log in with admin credentials." - Missing/invalid admin token (401)
|
||||
- "Access denied. Admin privileges required for this operation." - User is not admin (403)
|
||||
|
||||
#### `needsUnlock(email)`
|
||||
Check if an account needs unlocking (is locked with remaining time).
|
||||
|
||||
```javascript
|
||||
const needs = await AdminService.needsUnlock("user@example.com");
|
||||
console.log(needs); // true or false
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `email` (string, required): User's email address to check
|
||||
|
||||
**Returns:** `boolean` - True if account is locked and needs admin unlock
|
||||
|
||||
**Use Case:** Check before showing "Unlock Account" button in admin UI.
|
||||
|
||||
#### `validateEmail(email)`
|
||||
Validate email format.
|
||||
|
||||
```javascript
|
||||
const result = AdminService.validateEmail("user@example.com");
|
||||
console.log(result); // { valid: true, error: null }
|
||||
|
||||
const invalid = AdminService.validateEmail("invalid-email");
|
||||
console.log(invalid); // { valid: false, error: "Invalid email format" }
|
||||
```
|
||||
|
||||
**Returns:** `{ valid: boolean, error: string|null }`
|
||||
|
||||
**Validation Rules:**
|
||||
- Required (non-empty)
|
||||
- Valid email format (`user@domain.com`)
|
||||
- Maximum 255 characters
|
||||
|
||||
#### `formatRemainingTime(seconds)`
|
||||
Format remaining time for display.
|
||||
|
||||
```javascript
|
||||
const formatted = AdminService.formatRemainingTime(330);
|
||||
console.log(formatted); // "5 minutes 30 seconds"
|
||||
|
||||
const oneHour = AdminService.formatRemainingTime(3661);
|
||||
console.log(oneHour); // "1 hour 1 minute 1 second"
|
||||
```
|
||||
|
||||
**Returns:** `string` (e.g., "5 minutes 30 seconds", "1 hour", "30 seconds")
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Check Account Status Flow
|
||||
```
|
||||
Admin provides email to check
|
||||
↓
|
||||
AdminService.getAccountStatus()
|
||||
↓
|
||||
Validate email format (client-side)
|
||||
↓
|
||||
ApiClient.get() with JWT token (admin role)
|
||||
↓
|
||||
Token automatically refreshed if needed
|
||||
↓
|
||||
GET /api/v1/admin/account-status?email=...
|
||||
↓
|
||||
Backend checks Redis for lock status
|
||||
↓
|
||||
Backend returns lock info and failed attempts
|
||||
↓
|
||||
AdminService transforms response
|
||||
↓
|
||||
Component displays lock status
|
||||
```
|
||||
|
||||
### Unlock Account Flow
|
||||
```
|
||||
Admin requests account unlock
|
||||
↓
|
||||
AdminService.unlockAccount()
|
||||
↓
|
||||
Validate email format (client-side)
|
||||
↓
|
||||
ApiClient.post() with JWT token (admin role)
|
||||
↓
|
||||
Token automatically refreshed if needed
|
||||
↓
|
||||
POST /api/v1/admin/unlock-account
|
||||
↓
|
||||
Backend checks if account is locked
|
||||
↓
|
||||
Backend clears lock status in Redis
|
||||
↓
|
||||
Backend logs security event (ACCOUNT_UNLOCKED)
|
||||
↓
|
||||
Backend returns success response
|
||||
↓
|
||||
AdminService transforms response
|
||||
↓
|
||||
Component displays unlock confirmation
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Error Types
|
||||
|
||||
| Error Condition | Response | Frontend Behavior |
|
||||
|----------------|----------|-------------------|
|
||||
| Missing admin authentication | 401 Unauthorized | "Admin authentication required." |
|
||||
| Insufficient privileges | 403 Forbidden | "Access denied. Admin privileges required." |
|
||||
| Invalid email | 400 Bad Request | Specific validation error |
|
||||
| Account not locked (unlock) | 200 OK | "Account is not locked" (success) |
|
||||
| Server error | 500 Internal Server Error | Generic error message |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Admin Panel - Check Account Status
|
||||
|
||||
```javascript
|
||||
import React, { useState } from 'react';
|
||||
import AdminService from '../../services/API/AdminService';
|
||||
|
||||
function AccountStatusChecker() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [status, setStatus] = useState(null);
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleCheck = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setStatus(null);
|
||||
setLoading(true);
|
||||
|
||||
// Validate before sending
|
||||
const validation = AdminService.validateEmail(email);
|
||||
if (!validation.valid) {
|
||||
setError(validation.error);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const accountStatus = await AdminService.getAccountStatus(email);
|
||||
setStatus(accountStatus);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="account-status-checker">
|
||||
<h2>Check Account Lock Status</h2>
|
||||
|
||||
<form onSubmit={handleCheck}>
|
||||
<div>
|
||||
<label>Email Address:</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="user@example.com"
|
||||
maxLength={255}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && <p className="error">{error}</p>}
|
||||
|
||||
<button type="submit" disabled={loading}>
|
||||
{loading ? 'Checking...' : 'Check Status'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{status && (
|
||||
<div className="status-result">
|
||||
<h3>Account Status for {status.email}</h3>
|
||||
|
||||
{status.isLocked ? (
|
||||
<div className="locked-status">
|
||||
<p className="warning">🔒 Account is LOCKED</p>
|
||||
<p>Failed Attempts: {status.failedAttempts}</p>
|
||||
<p>Automatic Unlock In: {status.remainingTime}</p>
|
||||
<p>({status.remainingSeconds} seconds remaining)</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="unlocked-status">
|
||||
<p className="success">✓ Account is NOT locked</p>
|
||||
<p>Failed Attempts: {status.failedAttempts}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AccountStatusChecker;
|
||||
```
|
||||
|
||||
### Admin Panel - Unlock Account
|
||||
|
||||
```javascript
|
||||
import React, { useState } from 'react';
|
||||
import AdminService from '../../services/API/AdminService';
|
||||
|
||||
function AccountUnlocker() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [result, setResult] = useState(null);
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleUnlock = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setResult(null);
|
||||
setLoading(true);
|
||||
|
||||
// Validate before sending
|
||||
const validation = AdminService.validateEmail(email);
|
||||
if (!validation.valid) {
|
||||
setError(validation.error);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Confirm action
|
||||
const confirmed = confirm(
|
||||
`Are you sure you want to unlock the account for "${email}"?\n\n` +
|
||||
`This will:\n` +
|
||||
`- Clear all failed login attempts\n` +
|
||||
`- Remove the account lock immediately\n` +
|
||||
`- Log a security event with your admin ID\n\n` +
|
||||
`Continue?`
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const unlockResult = await AdminService.unlockAccount(email);
|
||||
setResult(unlockResult);
|
||||
|
||||
// Clear form on success
|
||||
if (unlockResult.success) {
|
||||
setEmail('');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="account-unlocker">
|
||||
<h2>Unlock User Account</h2>
|
||||
<p className="warning">
|
||||
⚠️ Admin action: This operation will be logged for security audit.
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleUnlock}>
|
||||
<div>
|
||||
<label>Email Address:</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="user@example.com"
|
||||
maxLength={255}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && <p className="error">{error}</p>}
|
||||
|
||||
{result && (
|
||||
<div className={result.success ? "success" : "error"}>
|
||||
<p>{result.message}</p>
|
||||
<p>Email: {result.email}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button type="submit" disabled={loading}>
|
||||
{loading ? 'Unlocking...' : 'Unlock Account'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AccountUnlocker;
|
||||
```
|
||||
|
||||
### Combined Admin Panel - Status + Unlock
|
||||
|
||||
```javascript
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import AdminService from '../../services/API/AdminService';
|
||||
|
||||
function AccountManager() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [status, setStatus] = useState(null);
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [unlocking, setUnlocking] = useState(false);
|
||||
|
||||
const checkStatus = async () => {
|
||||
if (!email) return;
|
||||
|
||||
setError('');
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const accountStatus = await AdminService.getAccountStatus(email);
|
||||
setStatus(accountStatus);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
setStatus(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnlock = async () => {
|
||||
if (!confirm(`Unlock account for "${email}"?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setError('');
|
||||
setUnlocking(true);
|
||||
|
||||
try {
|
||||
await AdminService.unlockAccount(email);
|
||||
// Refresh status after unlock
|
||||
await checkStatus();
|
||||
alert(`Account unlocked successfully for ${email}`);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setUnlocking(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="account-manager">
|
||||
<h2>Account Management</h2>
|
||||
|
||||
<div className="search-form">
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Enter email address"
|
||||
maxLength={255}
|
||||
/>
|
||||
<button onClick={checkStatus} disabled={loading || !email}>
|
||||
{loading ? 'Checking...' : 'Check Status'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && <p className="error">{error}</p>}
|
||||
|
||||
{status && (
|
||||
<div className="account-info">
|
||||
<h3>{status.email}</h3>
|
||||
|
||||
<div className="status-details">
|
||||
<p>
|
||||
Status:{" "}
|
||||
<strong className={status.isLocked ? "text-red" : "text-green"}>
|
||||
{status.isLocked ? "🔒 LOCKED" : "✓ Not Locked"}
|
||||
</strong>
|
||||
</p>
|
||||
<p>Failed Attempts: {status.failedAttempts}</p>
|
||||
|
||||
{status.isLocked && (
|
||||
<>
|
||||
<p>Automatic Unlock In: {status.remainingTime}</p>
|
||||
<p className="text-muted">
|
||||
({status.remainingSeconds} seconds)
|
||||
</p>
|
||||
|
||||
<button
|
||||
onClick={handleUnlock}
|
||||
disabled={unlocking}
|
||||
className="btn-danger"
|
||||
>
|
||||
{unlocking ? 'Unlocking...' : 'Unlock Account Now'}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AccountManager;
|
||||
```
|
||||
|
||||
### Locked Users Dashboard
|
||||
|
||||
```javascript
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import AdminService from '../../services/API/AdminService';
|
||||
|
||||
function LockedUsersDashboard({ suspectedEmails }) {
|
||||
const [lockedUsers, setLockedUsers] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const checkAllUsers = async () => {
|
||||
setLoading(true);
|
||||
const locked = [];
|
||||
|
||||
for (const email of suspectedEmails) {
|
||||
try {
|
||||
const status = await AdminService.getAccountStatus(email);
|
||||
if (status.isLocked) {
|
||||
locked.push(status);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to check ${email}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
setLockedUsers(locked);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
checkAllUsers();
|
||||
}, [suspectedEmails]);
|
||||
|
||||
const unlockUser = async (email) => {
|
||||
try {
|
||||
await AdminService.unlockAccount(email);
|
||||
// Remove from locked users list
|
||||
setLockedUsers(prev => prev.filter(user => user.email !== email));
|
||||
} catch (err) {
|
||||
alert(`Failed to unlock ${email}: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <div>Checking locked accounts...</div>;
|
||||
|
||||
if (lockedUsers.length === 0) {
|
||||
return <div>No locked accounts found.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="locked-users-dashboard">
|
||||
<h2>Locked User Accounts ({lockedUsers.length})</h2>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<th>Failed Attempts</th>
|
||||
<th>Unlocks In</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{lockedUsers.map(user => (
|
||||
<tr key={user.email}>
|
||||
<td>{user.email}</td>
|
||||
<td>{user.failedAttempts}</td>
|
||||
<td>{user.remainingTime}</td>
|
||||
<td>
|
||||
<button onClick={() => unlockUser(user.email)}>
|
||||
Unlock Now
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LockedUsersDashboard;
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Check Account Status
|
||||
|
||||
```javascript
|
||||
// In browser console after admin login
|
||||
|
||||
import AdminService from './services/API/AdminService';
|
||||
|
||||
// Validate email
|
||||
const validation = AdminService.validateEmail("user@example.com");
|
||||
console.log("Email valid:", validation.valid);
|
||||
|
||||
// Check account status
|
||||
const status = await AdminService.getAccountStatus("user@example.com");
|
||||
console.log("Account status:", status);
|
||||
console.log("Is locked:", status.isLocked);
|
||||
console.log("Failed attempts:", status.failedAttempts);
|
||||
|
||||
if (status.isLocked) {
|
||||
console.log("Remaining time:", status.remainingTime);
|
||||
console.log("Remaining seconds:", status.remainingSeconds);
|
||||
}
|
||||
```
|
||||
|
||||
### Test Unlock Account
|
||||
|
||||
```javascript
|
||||
// Check if needs unlock
|
||||
const needs = await AdminService.needsUnlock("user@example.com");
|
||||
console.log("Needs unlock:", needs);
|
||||
|
||||
// Unlock account
|
||||
if (needs) {
|
||||
const result = await AdminService.unlockAccount("user@example.com");
|
||||
console.log("Unlock result:", result);
|
||||
console.log("Success:", result.success);
|
||||
console.log("Message:", result.message);
|
||||
}
|
||||
```
|
||||
|
||||
### Test with curl
|
||||
|
||||
```bash
|
||||
# 1. Login as admin and get access token
|
||||
ACCESS_TOKEN=$(curl -X POST http://localhost:8000/api/v1/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "admin@example.com",
|
||||
"password": "AdminPass123!"
|
||||
}' | jq -r '.access_token')
|
||||
|
||||
# 2. Check account status
|
||||
curl -X GET "http://localhost:8000/api/v1/admin/account-status?email=user@example.com" \
|
||||
-H "Authorization: JWT $ACCESS_TOKEN" | jq
|
||||
|
||||
# Example response:
|
||||
# {
|
||||
# "email": "user@example.com",
|
||||
# "is_locked": true,
|
||||
# "failed_attempts": 5,
|
||||
# "remaining_time": "5 minutes 30 seconds",
|
||||
# "remaining_seconds": 330
|
||||
# }
|
||||
|
||||
# 3. Unlock account
|
||||
curl -X POST http://localhost:8000/api/v1/admin/unlock-account \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: JWT $ACCESS_TOKEN" \
|
||||
-d '{
|
||||
"email": "user@example.com"
|
||||
}' | jq
|
||||
|
||||
# Example response:
|
||||
# {
|
||||
# "success": true,
|
||||
# "message": "Account unlocked successfully",
|
||||
# "email": "user@example.com"
|
||||
# }
|
||||
|
||||
# 4. Verify account is unlocked
|
||||
curl -X GET "http://localhost:8000/api/v1/admin/account-status?email=user@example.com" \
|
||||
-H "Authorization: JWT $ACCESS_TOKEN" | jq
|
||||
|
||||
# Should show:
|
||||
# {
|
||||
# "email": "user@example.com",
|
||||
# "is_locked": false,
|
||||
# "failed_attempts": 0
|
||||
# }
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
### Security and Authorization
|
||||
|
||||
1. **Admin Role Required**: Both endpoints require admin authentication
|
||||
2. **Security Event Logging**: Unlock operations log security events for audit trail
|
||||
3. **Admin User ID**: The admin who performs unlock is logged (from JWT)
|
||||
4. **Rate Limiting**: Generic rate limiting applied to prevent abuse
|
||||
|
||||
### Account Lockout Mechanism
|
||||
|
||||
- Accounts are locked after **excessive failed login attempts** (configurable)
|
||||
- Lock duration is typically **15-30 minutes** (configurable)
|
||||
- Failed attempts counter resets after successful login
|
||||
- Automatic unlock occurs when lock time expires
|
||||
- Admin can unlock immediately without waiting
|
||||
|
||||
### Integration with CWE-307 Protection
|
||||
|
||||
These endpoints are part of the security system that protects against:
|
||||
- **CWE-307**: Improper Restriction of Excessive Authentication Attempts
|
||||
- **CWE-770**: Allocation of Resources Without Limits or Throttling
|
||||
|
||||
The login rate limiter tracks:
|
||||
- Failed login attempts per email
|
||||
- Account lock status and expiry
|
||||
- Grace period for automatic unlock
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Verify Admin Role**: Always check user has admin role before showing UI
|
||||
2. **Confirm Before Unlock**: Always require confirmation before unlocking
|
||||
3. **Audit Trail**: Log all admin actions for security compliance
|
||||
4. **User Notification**: Consider notifying users when their account is unlocked by admin
|
||||
5. **Regular Review**: Periodically review locked accounts dashboard
|
||||
|
||||
## Related Files
|
||||
|
||||
### Created Files
|
||||
```
|
||||
src/services/API/AdminService.js
|
||||
docs/ADMIN_API.md
|
||||
```
|
||||
|
||||
### Backend Reference Files
|
||||
```
|
||||
cloud/maplepress-backend/internal/interface/http/handler/admin/account_status_handler.go
|
||||
cloud/maplepress-backend/internal/interface/http/handler/admin/unlock_account_handler.go
|
||||
cloud/maplepress-backend/internal/interface/http/server.go (routes)
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [LOGIN_API.md](./LOGIN_API.md) - User login with rate limiting
|
||||
- [ME_API.md](./ME_API.md) - Current user profile includes role checking
|
||||
- [USER_API.md](./USER_API.md) - User management operations
|
||||
- [FRONTEND_ARCHITECTURE.md](./FRONTEND_ARCHITECTURE.md) - Architecture overview
|
||||
- [README.md](./README.md) - Documentation index
|
||||
|
||||
## Summary
|
||||
|
||||
The Admin API implementation provides:
|
||||
|
||||
1. **Account Status Checking**: View lock status, failed attempts, and remaining time
|
||||
2. **Account Unlocking**: Manually unlock accounts with security event logging
|
||||
3. **Helper Functions**: Email validation and time formatting
|
||||
4. **Security Compliance**: Admin role enforcement and audit trail
|
||||
5. **Error Handling**: Clear error messages and graceful failures
|
||||
|
||||
Essential for managing user account security and providing admin support for locked-out users while maintaining security compliance (CWE-307 protection).
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: October 30, 2024
|
||||
**Frontend Version**: 0.0.0
|
||||
**Documentation Version**: 1.0.0
|
||||
719
web/maplepress-frontend/docs/API/HEALTH_API.md
Normal file
719
web/maplepress-frontend/docs/API/HEALTH_API.md
Normal file
|
|
@ -0,0 +1,719 @@
|
|||
# Health Check API Integration
|
||||
|
||||
This document describes the integration between the MaplePress frontend and the Health Check API endpoint.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Backend API Specification](#backend-api-specification)
|
||||
- [Frontend Implementation](#frontend-implementation)
|
||||
- [Service Methods](#service-methods)
|
||||
- [Usage Examples](#usage-examples)
|
||||
- [Use Cases](#use-cases)
|
||||
- [Error Handling](#error-handling)
|
||||
- [Testing](#testing)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The Health Check API is a simple, unauthenticated endpoint that verifies the MaplePress backend service is running and operational. It's commonly used for:
|
||||
|
||||
- **Monitoring**: Service availability checks
|
||||
- **Load Balancers**: Health probe endpoints
|
||||
- **Startup Verification**: Ensuring backend is ready before initializing frontend
|
||||
- **API Connectivity**: Testing network connectivity to backend
|
||||
|
||||
### Key Features
|
||||
|
||||
- ✅ **No Authentication Required**: Public endpoint
|
||||
- ✅ **Simple Response**: Returns `{ "status": "healthy" }`
|
||||
- ✅ **Fast Response**: Lightweight check with minimal processing
|
||||
- ✅ **Always Available**: Does not depend on database or external services
|
||||
|
||||
---
|
||||
|
||||
## Backend API Specification
|
||||
|
||||
### Endpoint
|
||||
|
||||
```
|
||||
GET /health
|
||||
```
|
||||
|
||||
### Authentication
|
||||
|
||||
**None required** - This is a public endpoint.
|
||||
|
||||
### Request Headers
|
||||
|
||||
No headers required.
|
||||
|
||||
### Request Parameters
|
||||
|
||||
None.
|
||||
|
||||
### Response (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "healthy"
|
||||
}
|
||||
```
|
||||
|
||||
### Response Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| status | string | Health status - always "healthy" when service is running |
|
||||
|
||||
### Error Responses
|
||||
|
||||
- `503 Service Unavailable`: Backend service is down or unreachable
|
||||
- Network errors: Connection refused, timeout, etc.
|
||||
|
||||
### Backend Implementation
|
||||
|
||||
**Handler File**: `cloud/maplepress-backend/internal/interface/http/handler/healthcheck/healthcheck_handler.go`
|
||||
|
||||
```go
|
||||
func (h *Handler) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
response := map[string]string{
|
||||
"status": "healthy",
|
||||
}
|
||||
httpresponse.OK(w, response)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Frontend Implementation
|
||||
|
||||
### Service File
|
||||
|
||||
**Location**: `src/services/API/HealthService.js`
|
||||
|
||||
### Dependencies
|
||||
|
||||
```javascript
|
||||
import ApiClient from "./ApiClient";
|
||||
```
|
||||
|
||||
### Architecture
|
||||
|
||||
The HealthService follows the same three-layer architecture as other API services:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ React Components (UI) │
|
||||
│ - Dashboard, Status Indicators │
|
||||
└─────────────┬───────────────────────┘
|
||||
│
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ HealthService (API Layer) │
|
||||
│ - checkHealth() │
|
||||
│ - isHealthy() │
|
||||
│ - waitUntilHealthy() │
|
||||
│ - getDetailedStatus() │
|
||||
└─────────────┬───────────────────────┘
|
||||
│
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ ApiClient (HTTP Layer) │
|
||||
│ - GET /health │
|
||||
└─────────────┬───────────────────────┘
|
||||
│
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ Backend API │
|
||||
│ GET /health → { status: "healthy" }│
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Service Methods
|
||||
|
||||
### 1. `checkHealth()`
|
||||
|
||||
Check if the backend service is healthy.
|
||||
|
||||
**Signature**:
|
||||
```javascript
|
||||
async function checkHealth(): Promise<Object>
|
||||
```
|
||||
|
||||
**Returns**:
|
||||
```javascript
|
||||
{
|
||||
status: "healthy"
|
||||
}
|
||||
```
|
||||
|
||||
**Throws**: `Error` if service is unreachable or unhealthy
|
||||
|
||||
**Example**:
|
||||
```javascript
|
||||
import HealthService from './services/API/HealthService';
|
||||
|
||||
try {
|
||||
const health = await HealthService.checkHealth();
|
||||
if (health.status === 'healthy') {
|
||||
console.log('✅ Backend is healthy');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Backend is down:', error.message);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. `isHealthy()`
|
||||
|
||||
Simple boolean check for backend health.
|
||||
|
||||
**Signature**:
|
||||
```javascript
|
||||
async function isHealthy(): Promise<boolean>
|
||||
```
|
||||
|
||||
**Returns**: `true` if backend is healthy, `false` otherwise
|
||||
|
||||
**Example**:
|
||||
```javascript
|
||||
const healthy = await HealthService.isHealthy();
|
||||
|
||||
if (healthy) {
|
||||
console.log('✅ Backend is ready');
|
||||
} else {
|
||||
console.error('❌ Backend is not available');
|
||||
}
|
||||
```
|
||||
|
||||
**Use Case**: Quick status checks, conditional rendering
|
||||
|
||||
---
|
||||
|
||||
### 3. `waitUntilHealthy()`
|
||||
|
||||
Wait for the backend to become healthy (with retries).
|
||||
|
||||
**Signature**:
|
||||
```javascript
|
||||
async function waitUntilHealthy(options?: Object): Promise<boolean>
|
||||
```
|
||||
|
||||
**Parameters**:
|
||||
```javascript
|
||||
{
|
||||
maxAttempts: number, // Maximum retry attempts (default: 30)
|
||||
retryDelayMs: number // Delay between attempts in ms (default: 1000)
|
||||
}
|
||||
```
|
||||
|
||||
**Returns**: `true` if backend became healthy, `false` if timeout
|
||||
|
||||
**Example**:
|
||||
```javascript
|
||||
// Wait up to 10 seconds (10 attempts x 1 second)
|
||||
const ready = await HealthService.waitUntilHealthy({
|
||||
maxAttempts: 10,
|
||||
retryDelayMs: 1000
|
||||
});
|
||||
|
||||
if (ready) {
|
||||
console.log('✅ Backend is ready!');
|
||||
// Proceed with app initialization
|
||||
} else {
|
||||
console.error('❌ Backend did not become ready in time');
|
||||
// Show error message to user
|
||||
}
|
||||
```
|
||||
|
||||
**Use Case**: Application startup, waiting for backend deployment
|
||||
|
||||
---
|
||||
|
||||
### 4. `getDetailedStatus()`
|
||||
|
||||
Get detailed health status with timing information.
|
||||
|
||||
**Signature**:
|
||||
```javascript
|
||||
async function getDetailedStatus(): Promise<Object>
|
||||
```
|
||||
|
||||
**Returns**:
|
||||
```javascript
|
||||
{
|
||||
healthy: boolean, // Whether backend is healthy
|
||||
status: string, // "healthy" or "unhealthy"
|
||||
responseTimeMs: number, // Response time in milliseconds
|
||||
timestamp: Date, // When check was performed
|
||||
error?: string // Error message if unhealthy
|
||||
}
|
||||
```
|
||||
|
||||
**Example**:
|
||||
```javascript
|
||||
const status = await HealthService.getDetailedStatus();
|
||||
|
||||
console.log(`Backend Status: ${status.status}`);
|
||||
console.log(`Response Time: ${status.responseTimeMs}ms`);
|
||||
console.log(`Checked At: ${status.timestamp.toISOString()}`);
|
||||
|
||||
if (!status.healthy) {
|
||||
console.error(`Error: ${status.error}`);
|
||||
}
|
||||
```
|
||||
|
||||
**Use Case**: Monitoring dashboards, performance tracking, diagnostics
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Example 1: Application Startup Check
|
||||
|
||||
Check backend health before initializing the app:
|
||||
|
||||
```javascript
|
||||
// In App.jsx or main.jsx
|
||||
import { useEffect, useState } from 'react';
|
||||
import HealthService from './services/API/HealthService';
|
||||
|
||||
function App() {
|
||||
const [backendReady, setBackendReady] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function checkBackend() {
|
||||
try {
|
||||
const healthy = await HealthService.isHealthy();
|
||||
setBackendReady(healthy);
|
||||
|
||||
if (!healthy) {
|
||||
setError('Backend service is not available');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
checkBackend();
|
||||
}, []);
|
||||
|
||||
if (error) {
|
||||
return <div className="error">Backend Error: {error}</div>;
|
||||
}
|
||||
|
||||
if (!backendReady) {
|
||||
return <div className="loading">Connecting to backend...</div>;
|
||||
}
|
||||
|
||||
return <div>App content...</div>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Example 2: Status Indicator Component
|
||||
|
||||
Show real-time backend status:
|
||||
|
||||
```javascript
|
||||
import { useEffect, useState } from 'react';
|
||||
import HealthService from '../services/API/HealthService';
|
||||
|
||||
function BackendStatusIndicator() {
|
||||
const [status, setStatus] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Check status every 30 seconds
|
||||
const checkStatus = async () => {
|
||||
const detail = await HealthService.getDetailedStatus();
|
||||
setStatus(detail);
|
||||
};
|
||||
|
||||
checkStatus();
|
||||
const interval = setInterval(checkStatus, 30000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
if (!status) {
|
||||
return <span>Checking...</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`status-indicator ${status.healthy ? 'healthy' : 'unhealthy'}`}>
|
||||
<span className="status-dot"></span>
|
||||
<span className="status-text">
|
||||
Backend: {status.status} ({status.responseTimeMs}ms)
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Example 3: Deployment Health Check
|
||||
|
||||
Wait for backend after deployment:
|
||||
|
||||
```javascript
|
||||
import HealthService from './services/API/HealthService';
|
||||
|
||||
async function waitForBackend() {
|
||||
console.log('Waiting for backend to become ready...');
|
||||
|
||||
const ready = await HealthService.waitUntilHealthy({
|
||||
maxAttempts: 60, // Wait up to 1 minute
|
||||
retryDelayMs: 1000 // Check every second
|
||||
});
|
||||
|
||||
if (ready) {
|
||||
console.log('✅ Backend is ready!');
|
||||
return true;
|
||||
} else {
|
||||
console.error('❌ Backend deployment timeout');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage in deployment script
|
||||
if (await waitForBackend()) {
|
||||
// Proceed with app initialization
|
||||
} else {
|
||||
// Show deployment error message
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Example 4: Error Recovery
|
||||
|
||||
Retry failed API calls after backend recovers:
|
||||
|
||||
```javascript
|
||||
import HealthService from './services/API/HealthService';
|
||||
import SiteService from './services/API/SiteService';
|
||||
|
||||
async function fetchSitesWithRetry() {
|
||||
try {
|
||||
// Try to fetch sites
|
||||
const sites = await SiteService.listSites();
|
||||
return sites;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch sites:', error);
|
||||
|
||||
// Check if backend is healthy
|
||||
const healthy = await HealthService.isHealthy();
|
||||
|
||||
if (!healthy) {
|
||||
console.log('Backend is down, waiting for recovery...');
|
||||
|
||||
// Wait for backend to recover
|
||||
const recovered = await HealthService.waitUntilHealthy({
|
||||
maxAttempts: 10,
|
||||
retryDelayMs: 2000
|
||||
});
|
||||
|
||||
if (recovered) {
|
||||
console.log('Backend recovered, retrying...');
|
||||
return await SiteService.listSites();
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. **Monitoring & Observability**
|
||||
|
||||
- Service uptime monitoring
|
||||
- Response time tracking
|
||||
- Health dashboard indicators
|
||||
- Alerting on service degradation
|
||||
|
||||
### 2. **Load Balancer Integration**
|
||||
|
||||
- Health probe endpoint for AWS/Azure/GCP load balancers
|
||||
- Kubernetes liveness/readiness probes
|
||||
- Docker healthcheck configuration
|
||||
|
||||
### 3. **Application Lifecycle**
|
||||
|
||||
- Startup health verification
|
||||
- Graceful degradation on backend issues
|
||||
- Post-deployment verification
|
||||
- Environment validation (dev/staging/prod)
|
||||
|
||||
### 4. **User Experience**
|
||||
|
||||
- Show connection status to users
|
||||
- Prevent API calls when backend is down
|
||||
- Display maintenance mode messages
|
||||
- Automatic retry on recovery
|
||||
|
||||
### 5. **Development & Testing**
|
||||
|
||||
- Verify backend is running before tests
|
||||
- E2E test prerequisites
|
||||
- Local development environment checks
|
||||
- CI/CD pipeline health gates
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Network Errors
|
||||
|
||||
```javascript
|
||||
try {
|
||||
await HealthService.checkHealth();
|
||||
} catch (error) {
|
||||
if (error.message.includes('network') || error.message.includes('fetch')) {
|
||||
// Network connectivity issue
|
||||
console.error('Cannot reach backend - check network');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Service Unavailable
|
||||
|
||||
```javascript
|
||||
try {
|
||||
await HealthService.checkHealth();
|
||||
} catch (error) {
|
||||
if (error.message.includes('503') || error.message.includes('unavailable')) {
|
||||
// Backend is down or restarting
|
||||
console.error('Backend service is unavailable');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Timeout Handling
|
||||
|
||||
```javascript
|
||||
// With timeout wrapper
|
||||
async function checkHealthWithTimeout(timeoutMs = 5000) {
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Health check timeout')), timeoutMs)
|
||||
);
|
||||
|
||||
try {
|
||||
return await Promise.race([
|
||||
HealthService.checkHealth(),
|
||||
timeoutPromise
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Health check failed or timed out:', error.message);
|
||||
return { status: 'unhealthy' };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Manual Testing
|
||||
|
||||
1. **Start the backend**:
|
||||
```bash
|
||||
cd cloud/maplepress-backend
|
||||
task dev
|
||||
```
|
||||
|
||||
2. **Test with curl**:
|
||||
```bash
|
||||
curl -X GET http://localhost:8000/health
|
||||
```
|
||||
|
||||
Expected response:
|
||||
```json
|
||||
{ "status": "healthy" }
|
||||
```
|
||||
|
||||
3. **Test from frontend**:
|
||||
```javascript
|
||||
// In browser console
|
||||
import HealthService from './services/API/HealthService.js';
|
||||
|
||||
const health = await HealthService.checkHealth();
|
||||
console.log(health); // { status: "healthy" }
|
||||
```
|
||||
|
||||
### Automated Testing
|
||||
|
||||
```javascript
|
||||
// Example test (using Jest or Vitest)
|
||||
import HealthService from '../services/API/HealthService';
|
||||
|
||||
describe('HealthService', () => {
|
||||
it('should return healthy status when backend is up', async () => {
|
||||
const health = await HealthService.checkHealth();
|
||||
expect(health.status).toBe('healthy');
|
||||
});
|
||||
|
||||
it('should return true for isHealthy()', async () => {
|
||||
const healthy = await HealthService.isHealthy();
|
||||
expect(healthy).toBe(true);
|
||||
});
|
||||
|
||||
it('should include response time in detailed status', async () => {
|
||||
const status = await HealthService.getDetailedStatus();
|
||||
expect(status).toHaveProperty('responseTimeMs');
|
||||
expect(status.responseTimeMs).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: "Unable to connect to backend service"
|
||||
|
||||
**Symptoms**: Network or fetch errors
|
||||
|
||||
**Solutions**:
|
||||
1. Verify backend is running: `curl http://localhost:8000/health`
|
||||
2. Check `VITE_API_BASE_URL` in `.env` file
|
||||
3. Verify no CORS issues in browser console
|
||||
4. Check firewall/network settings
|
||||
|
||||
### Issue: "Backend service is temporarily unavailable"
|
||||
|
||||
**Symptoms**: 503 status code
|
||||
|
||||
**Solutions**:
|
||||
1. Backend may be starting up - wait a few seconds
|
||||
2. Check backend logs: `docker logs mapleopentech_backend`
|
||||
3. Verify backend services (Cassandra, Redis) are running
|
||||
4. Restart backend: `task end && task dev`
|
||||
|
||||
### Issue: Health check timeout
|
||||
|
||||
**Symptoms**: Slow or no response
|
||||
|
||||
**Solutions**:
|
||||
1. Check backend server load
|
||||
2. Verify network latency
|
||||
3. Check if backend is overloaded
|
||||
4. Consider increasing timeout in `waitUntilHealthy()`
|
||||
|
||||
### Issue: Always returns unhealthy
|
||||
|
||||
**Symptoms**: `isHealthy()` always returns false
|
||||
|
||||
**Solutions**:
|
||||
1. Check browser console for errors
|
||||
2. Verify API base URL is correct
|
||||
3. Check CORS configuration
|
||||
4. Test endpoint directly with curl
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. **Use During Initialization**
|
||||
|
||||
Always check backend health during app startup:
|
||||
|
||||
```javascript
|
||||
useEffect(() => {
|
||||
HealthService.isHealthy().then(healthy => {
|
||||
if (!healthy) {
|
||||
showBackendError();
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
```
|
||||
|
||||
### 2. **Periodic Health Checks**
|
||||
|
||||
For long-running apps, check periodically:
|
||||
|
||||
```javascript
|
||||
// Every 5 minutes
|
||||
setInterval(async () => {
|
||||
const healthy = await HealthService.isHealthy();
|
||||
updateStatusIndicator(healthy);
|
||||
}, 300000);
|
||||
```
|
||||
|
||||
### 3. **Handle Failures Gracefully**
|
||||
|
||||
Don't throw errors to users - handle them gracefully:
|
||||
|
||||
```javascript
|
||||
const healthy = await HealthService.isHealthy().catch(() => false);
|
||||
if (!healthy) {
|
||||
showOfflineMode();
|
||||
}
|
||||
```
|
||||
|
||||
### 4. **Log Response Times**
|
||||
|
||||
Monitor performance over time:
|
||||
|
||||
```javascript
|
||||
const status = await HealthService.getDetailedStatus();
|
||||
analytics.track('backend_health', {
|
||||
responseTime: status.responseTimeMs,
|
||||
healthy: status.healthy
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration with Other Services
|
||||
|
||||
The HealthService can be combined with other services for robust error handling:
|
||||
|
||||
```javascript
|
||||
import HealthService from './services/API/HealthService';
|
||||
import AuthManager from './services/Manager/AuthManager';
|
||||
|
||||
async function safeLogin(email, password) {
|
||||
// Check backend health first
|
||||
const healthy = await HealthService.isHealthy();
|
||||
|
||||
if (!healthy) {
|
||||
throw new Error('Backend is currently unavailable. Please try again later.');
|
||||
}
|
||||
|
||||
// Proceed with login
|
||||
return await AuthManager.login({ email, password });
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
The Health Check API provides a simple, reliable way to verify backend availability:
|
||||
|
||||
- ✅ **Simple Integration**: One GET request, no auth required
|
||||
- ✅ **Multiple Helper Methods**: `checkHealth()`, `isHealthy()`, `waitUntilHealthy()`, `getDetailedStatus()`
|
||||
- ✅ **Error Handling**: Comprehensive error detection and user-friendly messages
|
||||
- ✅ **Flexible Usage**: Startup checks, monitoring, status indicators, deployment verification
|
||||
- ✅ **Production Ready**: Tested against backend implementation
|
||||
|
||||
For questions or issues, refer to the [Troubleshooting](#troubleshooting) section or check the main [README](../../README.md).
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: October 30, 2024
|
||||
**Frontend Version**: 0.0.0
|
||||
**Backend API Version**: 1.0.0
|
||||
580
web/maplepress-frontend/docs/API/HELLO_API.md
Normal file
580
web/maplepress-frontend/docs/API/HELLO_API.md
Normal file
|
|
@ -0,0 +1,580 @@
|
|||
# Hello API Implementation
|
||||
|
||||
This document describes the implementation of the Hello API endpoint for the MaplePress frontend, integrated with the MaplePress backend API.
|
||||
|
||||
## Overview
|
||||
|
||||
The Hello API is a simple authenticated endpoint that returns a personalized greeting message. It demonstrates JWT authentication and can be used to verify that access tokens are working correctly. This is useful for testing authentication flows and ensuring the token refresh system is functioning properly.
|
||||
|
||||
## Backend API Endpoint
|
||||
|
||||
**Endpoint**: `POST /api/v1/hello`
|
||||
**Authentication**: Required (JWT token)
|
||||
**Documentation**: `/cloud/maplepress-backend/docs/API.md` (lines 326-372)
|
||||
|
||||
### Request Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Alice"
|
||||
}
|
||||
```
|
||||
|
||||
**Headers Required:**
|
||||
- `Content-Type: application/json`
|
||||
- `Authorization: JWT {access_token}`
|
||||
|
||||
### Response Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Hello, Alice! Welcome to MaplePress Backend."
|
||||
}
|
||||
```
|
||||
|
||||
### Validation Rules
|
||||
|
||||
**Backend Validation:**
|
||||
- Name is required (cannot be empty)
|
||||
- Name length: 1-100 characters
|
||||
- Must contain only printable characters
|
||||
- No HTML tags allowed (XSS prevention)
|
||||
- Input is sanitized and HTML-escaped
|
||||
|
||||
**Security Features:**
|
||||
- CWE-20: Comprehensive input validation
|
||||
- CWE-79: XSS prevention (HTML escaping)
|
||||
- CWE-117: Log injection prevention (name is hashed in logs)
|
||||
- CWE-436: Strict Content-Type validation
|
||||
|
||||
## Frontend Implementation
|
||||
|
||||
### HelloService (`src/services/API/HelloService.js`)
|
||||
|
||||
Handles direct communication with the backend Hello API.
|
||||
|
||||
**Key Features:**
|
||||
- Client-side validation before API call
|
||||
- Request body formatting
|
||||
- Authenticated requests (uses JWT token)
|
||||
- User-friendly error message mapping
|
||||
- XSS prevention (HTML tag validation)
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `hello(name)`
|
||||
Main method to send a hello request.
|
||||
|
||||
```javascript
|
||||
import HelloService from './services/API/HelloService';
|
||||
|
||||
const response = await HelloService.hello("Alice");
|
||||
console.log(response.message);
|
||||
// Output: "Hello, Alice! Welcome to MaplePress Backend."
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `name` (string, required): Name to include in greeting (1-100 characters)
|
||||
|
||||
**Returns:**
|
||||
```javascript
|
||||
{
|
||||
message: string // Personalized greeting message
|
||||
}
|
||||
```
|
||||
|
||||
**Throws:**
|
||||
- "Name is required" - If name is missing or not a string
|
||||
- "Name cannot be empty" - If name is empty after trimming
|
||||
- "Name must be 100 characters or less" - If name exceeds limit
|
||||
- "Name cannot contain HTML tags" - If name contains `<>` tags
|
||||
- "Authentication required. Please log in to continue." - If JWT token is invalid/expired
|
||||
- "Invalid name provided. Please check your input." - If backend validation fails
|
||||
- Generic error message for other failures
|
||||
|
||||
#### `validateName(name)`
|
||||
Client-side validation helper to check name before sending.
|
||||
|
||||
```javascript
|
||||
const validation = HelloService.validateName("Alice");
|
||||
if (!validation.valid) {
|
||||
console.error(validation.error);
|
||||
} else {
|
||||
// Proceed with API call
|
||||
}
|
||||
```
|
||||
|
||||
**Returns:**
|
||||
```javascript
|
||||
{
|
||||
valid: boolean, // true if name is valid
|
||||
error: string|null // Error message if invalid, null if valid
|
||||
}
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
User provides name
|
||||
↓
|
||||
HelloService.validateName() (optional client-side check)
|
||||
↓
|
||||
HelloService.hello(name)
|
||||
↓
|
||||
ApiClient.post() with JWT token
|
||||
↓
|
||||
Token automatically refreshed if needed (ApiClient feature)
|
||||
↓
|
||||
POST /api/v1/hello with Authorization header
|
||||
↓
|
||||
Backend validates JWT token
|
||||
↓
|
||||
Backend validates name (length, characters, no HTML)
|
||||
↓
|
||||
Backend sanitizes and HTML-escapes name
|
||||
↓
|
||||
Backend returns personalized greeting
|
||||
↓
|
||||
Frontend receives response
|
||||
```
|
||||
|
||||
## Validation Rules
|
||||
|
||||
### Client-Side Validation
|
||||
|
||||
1. **Required**: Name cannot be null, undefined, or empty string
|
||||
2. **Type**: Must be a string
|
||||
3. **Length**: 1-100 characters (after trimming)
|
||||
4. **HTML Tags**: Must not contain `<` or `>` characters
|
||||
5. **Printable**: No control characters (0x00-0x1F, 0x7F-0x9F)
|
||||
|
||||
### Backend Validation
|
||||
|
||||
1. **Required**: Name field must be present and non-empty
|
||||
2. **Length**: 1-100 characters
|
||||
3. **Printable**: Only printable characters allowed
|
||||
4. **No HTML**: Validated for HTML tags (XSS prevention)
|
||||
5. **Sanitization**: Input is sanitized and HTML-escaped
|
||||
6. **Logging**: Name is hashed in logs (PII protection)
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Error Types
|
||||
|
||||
| Error Condition | Response | Frontend Behavior |
|
||||
|----------------|----------|-------------------|
|
||||
| Name missing | 400 Bad Request | "Name is required" |
|
||||
| Name empty | 400 Bad Request | "Name cannot be empty" |
|
||||
| Name too long | 400 Bad Request | "Name must be 100 characters or less" |
|
||||
| HTML tags in name | 400 Bad Request | "Name cannot contain HTML tags" |
|
||||
| Invalid JWT token | 401 Unauthorized | "Authentication required. Please log in to continue." |
|
||||
| JWT token expired | 401 Unauthorized | Token auto-refresh triggered, then retry |
|
||||
|
||||
### Error Message Mapping
|
||||
|
||||
```javascript
|
||||
// Backend error → Frontend error
|
||||
"unauthorized" → "Authentication required. Please log in to continue."
|
||||
"name" → "Invalid name provided. Please check your input."
|
||||
Other errors → Original error message or generic fallback
|
||||
```
|
||||
|
||||
## Security Features
|
||||
|
||||
1. **Authentication Required**: Endpoint requires valid JWT token
|
||||
2. **XSS Prevention**: HTML tags are rejected both client and server-side
|
||||
3. **Input Sanitization**: Backend sanitizes and HTML-escapes all input
|
||||
4. **Length Limits**: Prevents buffer overflow attacks
|
||||
5. **Printable Characters Only**: Prevents control character injection
|
||||
6. **PII Protection**: Name is hashed in backend logs
|
||||
7. **Automatic Token Refresh**: ApiClient ensures token is valid before request
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Usage in React Component
|
||||
|
||||
```javascript
|
||||
import React, { useState } from 'react';
|
||||
import { useAuth } from '../services/Services';
|
||||
import HelloService from '../services/API/HelloService';
|
||||
|
||||
function HelloExample() {
|
||||
const { authManager } = useAuth();
|
||||
const [name, setName] = useState('');
|
||||
const [message, setMessage] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setMessage('');
|
||||
setLoading(true);
|
||||
|
||||
// Check authentication
|
||||
if (!authManager.isAuthenticated()) {
|
||||
setError('Please log in first');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Optional: Validate before sending
|
||||
const validation = HelloService.validateName(name);
|
||||
if (!validation.valid) {
|
||||
setError(validation.error);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await HelloService.hello(name);
|
||||
setMessage(response.message);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Enter your name"
|
||||
maxLength={100}
|
||||
/>
|
||||
<button type="submit" disabled={loading}>
|
||||
{loading ? 'Sending...' : 'Say Hello'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{message && <p className="success">{message}</p>}
|
||||
{error && <p className="error">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HelloExample;
|
||||
```
|
||||
|
||||
### Testing Authentication
|
||||
|
||||
```javascript
|
||||
// Test if authentication is working
|
||||
import HelloService from './services/API/HelloService';
|
||||
|
||||
async function testAuthentication() {
|
||||
try {
|
||||
const response = await HelloService.hello("Test User");
|
||||
console.log("✅ Authentication working:", response.message);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("❌ Authentication failed:", error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Validation Testing
|
||||
|
||||
```javascript
|
||||
// Test validation rules
|
||||
const testCases = [
|
||||
{ name: "", expected: "Name cannot be empty" },
|
||||
{ name: "Alice", expected: null },
|
||||
{ name: "<script>alert('xss')</script>", expected: "Name cannot contain HTML tags" },
|
||||
{ name: "A".repeat(101), expected: "Name must be 100 characters or less" },
|
||||
];
|
||||
|
||||
testCases.forEach(({ name, expected }) => {
|
||||
const result = HelloService.validateName(name);
|
||||
console.log(`Input: "${name}"`);
|
||||
console.log(`Expected: ${expected}`);
|
||||
console.log(`Got: ${result.error}`);
|
||||
console.log(`✓ Pass: ${result.error === expected}\n`);
|
||||
});
|
||||
```
|
||||
|
||||
## Testing the Hello API
|
||||
|
||||
### 1. Prerequisites
|
||||
|
||||
- Backend running at `http://localhost:8000`
|
||||
- Frontend running at `http://localhost:5173`
|
||||
- User logged in (valid JWT token)
|
||||
|
||||
### 2. Testing via Dashboard
|
||||
|
||||
You can add a simple test component to the dashboard:
|
||||
|
||||
```javascript
|
||||
// Add to Dashboard.jsx
|
||||
import HelloService from '../../services/API/HelloService';
|
||||
|
||||
// Inside Dashboard component
|
||||
const [helloMessage, setHelloMessage] = useState('');
|
||||
|
||||
const testHello = async () => {
|
||||
try {
|
||||
const response = await HelloService.hello(user.name);
|
||||
setHelloMessage(response.message);
|
||||
} catch (error) {
|
||||
console.error('Hello test failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// In JSX
|
||||
<button onClick={testHello}>Test Hello API</button>
|
||||
{helloMessage && <p>{helloMessage}</p>}
|
||||
```
|
||||
|
||||
### 3. Testing via Browser Console
|
||||
|
||||
```javascript
|
||||
// After logging in, open browser console
|
||||
|
||||
// Test basic hello
|
||||
const response = await window.HelloService.hello("Alice");
|
||||
console.log(response.message);
|
||||
// Expected: "Hello, Alice! Welcome to MaplePress Backend."
|
||||
|
||||
// Test validation
|
||||
const validation = window.HelloService.validateName("<script>test</script>");
|
||||
console.log(validation);
|
||||
// Expected: { valid: false, error: "Name cannot contain HTML tags" }
|
||||
|
||||
// Test with empty name
|
||||
try {
|
||||
await window.HelloService.hello("");
|
||||
} catch (error) {
|
||||
console.log(error.message);
|
||||
// Expected: "Name cannot be empty"
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Testing with curl
|
||||
|
||||
```bash
|
||||
# 1. Login and get access token
|
||||
ACCESS_TOKEN=$(curl -X POST http://localhost:8000/api/v1/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "test@example.com",
|
||||
"password": "SecurePass123!"
|
||||
}' | jq -r '.access_token')
|
||||
|
||||
# 2. Test hello endpoint
|
||||
curl -X POST http://localhost:8000/api/v1/hello \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: JWT $ACCESS_TOKEN" \
|
||||
-d '{"name": "Alice"}' | jq
|
||||
|
||||
# Expected output:
|
||||
# {
|
||||
# "message": "Hello, Alice! Welcome to MaplePress Backend."
|
||||
# }
|
||||
|
||||
# 3. Test with empty name (should fail)
|
||||
curl -X POST http://localhost:8000/api/v1/hello \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: JWT $ACCESS_TOKEN" \
|
||||
-d '{"name": ""}' | jq
|
||||
|
||||
# Expected: 400 Bad Request
|
||||
|
||||
# 4. Test without authentication (should fail)
|
||||
curl -X POST http://localhost:8000/api/v1/hello \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "Alice"}' | jq
|
||||
|
||||
# Expected: 401 Unauthorized
|
||||
```
|
||||
|
||||
### 5. Testing Token Refresh Integration
|
||||
|
||||
```javascript
|
||||
// Test that token refresh works automatically
|
||||
|
||||
// 1. Login
|
||||
// 2. Manually set token expiry to 30 seconds from now
|
||||
const newExpiry = new Date(Date.now() + 30000).toISOString();
|
||||
localStorage.setItem('maplepress_access_expiry', newExpiry);
|
||||
|
||||
// 3. Wait 31 seconds, then call hello
|
||||
await new Promise(resolve => setTimeout(resolve, 31000));
|
||||
const response = await HelloService.hello("Test");
|
||||
|
||||
// 4. Check console logs - should see automatic token refresh
|
||||
// 5. Response should be successful despite expired token
|
||||
console.log(response.message);
|
||||
```
|
||||
|
||||
## Integration with Existing Services
|
||||
|
||||
The HelloService automatically integrates with existing infrastructure:
|
||||
|
||||
### Authentication (AuthManager)
|
||||
```javascript
|
||||
// HelloService uses ApiClient, which automatically:
|
||||
// - Adds JWT token to Authorization header
|
||||
// - Refreshes token if expired
|
||||
// - Handles 401 errors
|
||||
```
|
||||
|
||||
### Token Refresh (RefreshTokenService)
|
||||
```javascript
|
||||
// Automatic token refresh before hello request
|
||||
// If token expires within 1 minute, it's refreshed proactively
|
||||
// If 401 received, token is refreshed and request is retried
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
```javascript
|
||||
// All errors are caught and mapped to user-friendly messages
|
||||
// Authentication errors trigger automatic login redirect (in components)
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Authentication Testing
|
||||
Use the Hello endpoint to verify that JWT authentication is working correctly after login or token refresh.
|
||||
|
||||
```javascript
|
||||
// After login
|
||||
const testAuth = async () => {
|
||||
try {
|
||||
await HelloService.hello("Test");
|
||||
console.log("✅ Authentication successful");
|
||||
} catch (error) {
|
||||
console.error("❌ Authentication failed");
|
||||
// Redirect to login
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Token Validity Check
|
||||
Check if the current access token is valid without making a critical API call.
|
||||
|
||||
```javascript
|
||||
// Before important operation
|
||||
const isTokenValid = async () => {
|
||||
try {
|
||||
await HelloService.hello("TokenCheck");
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 3. User Greeting
|
||||
Display a personalized welcome message on the dashboard.
|
||||
|
||||
```javascript
|
||||
// On dashboard load
|
||||
useEffect(() => {
|
||||
const greetUser = async () => {
|
||||
try {
|
||||
const response = await HelloService.hello(user.name);
|
||||
setGreeting(response.message);
|
||||
} catch (error) {
|
||||
console.error("Failed to load greeting:", error);
|
||||
}
|
||||
};
|
||||
|
||||
greetUser();
|
||||
}, [user.name]);
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Authentication required" error
|
||||
|
||||
**Possible causes:**
|
||||
1. User not logged in
|
||||
2. Access token expired
|
||||
3. Refresh token expired
|
||||
4. Session invalidated
|
||||
|
||||
**Solution:**
|
||||
- Check `authManager.isAuthenticated()` before calling
|
||||
- Login again if session expired
|
||||
- Check browser console for token refresh logs
|
||||
|
||||
### "Name cannot contain HTML tags" error
|
||||
|
||||
**Possible causes:**
|
||||
1. User input contains `<` or `>` characters
|
||||
2. Attempt to inject HTML/JavaScript
|
||||
|
||||
**Solution:**
|
||||
- Sanitize user input before calling HelloService
|
||||
- Use `validateName()` to check input first
|
||||
- Inform user about character restrictions
|
||||
|
||||
### Request succeeds but token expired shortly after
|
||||
|
||||
**Possible causes:**
|
||||
1. Token was refreshed but expiry is still 15 minutes
|
||||
2. High request frequency without refresh
|
||||
|
||||
**Solution:**
|
||||
- Token automatically refreshes 1 minute before expiry
|
||||
- This is expected behavior (15-minute access token lifetime)
|
||||
- Refresh token handles session extension
|
||||
|
||||
### 401 error despite valid token
|
||||
|
||||
**Possible causes:**
|
||||
1. Clock skew between client and server
|
||||
2. Token format incorrect
|
||||
3. Backend session invalidated
|
||||
|
||||
**Solution:**
|
||||
- Check system clock synchronization
|
||||
- Verify token in localStorage is properly formatted
|
||||
- Clear session and login again
|
||||
|
||||
## Related Files
|
||||
|
||||
### Created Files
|
||||
```
|
||||
src/services/API/HelloService.js
|
||||
docs/HELLO_API.md
|
||||
```
|
||||
|
||||
### Backend Reference Files
|
||||
```
|
||||
cloud/maplepress-backend/docs/API.md (lines 326-372)
|
||||
cloud/maplepress-backend/internal/interface/http/handler/gateway/hello_handler.go
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [LOGIN_API.md](./LOGIN_API.md) - Login and JWT token acquisition
|
||||
- [REFRESH_TOKEN_API.md](./REFRESH_TOKEN_API.md) - Automatic token refresh
|
||||
- [FRONTEND_ARCHITECTURE.md](./FRONTEND_ARCHITECTURE.md) - Architecture overview
|
||||
- [README.md](./README.md) - Documentation index
|
||||
- [Backend API Documentation](../../../cloud/maplepress-backend/docs/API.md) - Complete API reference
|
||||
|
||||
## Summary
|
||||
|
||||
The Hello API implementation provides:
|
||||
|
||||
1. **Simple Authentication Test**: Verify JWT tokens are working
|
||||
2. **Security Features**: XSS prevention, input validation, sanitization
|
||||
3. **User-Friendly**: Personalized greeting messages
|
||||
4. **Automatic Integration**: Works seamlessly with token refresh
|
||||
5. **Error Handling**: Clear error messages and graceful failures
|
||||
6. **Validation Helpers**: Client-side validation before API calls
|
||||
|
||||
This endpoint is perfect for testing authentication flows and demonstrating the three-layer service architecture in action.
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: October 30, 2024
|
||||
**Frontend Version**: 0.0.0
|
||||
**Documentation Version**: 1.0.0
|
||||
487
web/maplepress-frontend/docs/API/LOGIN_API.md
Normal file
487
web/maplepress-frontend/docs/API/LOGIN_API.md
Normal file
|
|
@ -0,0 +1,487 @@
|
|||
# Login API Implementation
|
||||
|
||||
This document describes the complete implementation of the user login feature for the MaplePress frontend, integrated with the MaplePress backend API.
|
||||
|
||||
## Overview
|
||||
|
||||
The login feature allows existing users to authenticate with their email and password credentials. Upon successful login, users receive authentication tokens and are automatically logged in to their dashboard.
|
||||
|
||||
## Backend API Endpoint
|
||||
|
||||
**Endpoint**: `POST /api/v1/login`
|
||||
**Authentication**: None required (public endpoint)
|
||||
**Documentation**: `/cloud/maplepress-backend/docs/API.md` (lines 168-228)
|
||||
|
||||
### Request Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"password": "SecurePassword123!"
|
||||
}
|
||||
```
|
||||
|
||||
### Response Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"user_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"user_email": "user@example.com",
|
||||
"user_name": "John Doe",
|
||||
"user_role": "user",
|
||||
"tenant_id": "650e8400-e29b-41d4-a716-446655440000",
|
||||
"session_id": "750e8400-e29b-41d4-a716-446655440000",
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"access_expiry": "2024-10-24T12:15:00Z",
|
||||
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"refresh_expiry": "2024-10-31T00:00:00Z",
|
||||
"login_at": "2024-10-24T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Key Differences from Registration
|
||||
|
||||
The login endpoint differs from registration in several ways:
|
||||
- **Simpler Request**: Only email and password required
|
||||
- **No Tenant Details in Response**: Tenant name/slug not included
|
||||
- **Login Timestamp**: Includes `login_at` instead of `created_at`
|
||||
- **Existing Session**: Authenticates existing user, doesn't create new account
|
||||
|
||||
## Frontend Implementation
|
||||
|
||||
### 1. LoginService (`src/services/API/LoginService.js`)
|
||||
|
||||
Handles direct communication with the backend login API.
|
||||
|
||||
**Key Features:**
|
||||
- Request validation (required fields)
|
||||
- Request body formatting (snake_case for backend)
|
||||
- Response transformation (camelCase for frontend)
|
||||
- User-friendly error message mapping
|
||||
- Rate limit error handling
|
||||
- Account lockout detection
|
||||
|
||||
**Methods:**
|
||||
- `login(credentials)` - Main login method
|
||||
|
||||
**Usage:**
|
||||
```javascript
|
||||
import LoginService from './services/API/LoginService';
|
||||
|
||||
const response = await LoginService.login({
|
||||
email: "user@example.com",
|
||||
password: "SecurePassword123!",
|
||||
});
|
||||
```
|
||||
|
||||
### 2. AuthManager Enhancement (`src/services/Manager/AuthManager.js`)
|
||||
|
||||
Updated to support login functionality while maintaining registration support.
|
||||
|
||||
**New/Updated Methods:**
|
||||
- `login(email, password)` - Login and store auth data
|
||||
- `storeAuthData(authResponse)` - Updated to handle optional tenant fields
|
||||
|
||||
**Key Features:**
|
||||
- Handles both registration and login responses
|
||||
- Gracefully handles missing tenant name/slug from login
|
||||
- Maintains same token storage mechanism
|
||||
- Consistent session management
|
||||
|
||||
**Login Flow:**
|
||||
```javascript
|
||||
const authManager = useAuth().authManager;
|
||||
|
||||
// Login
|
||||
await authManager.login("user@example.com", "password123");
|
||||
|
||||
// Check authentication
|
||||
if (authManager.isAuthenticated()) {
|
||||
const user = authManager.getUser();
|
||||
const tenant = authManager.getTenant();
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Login Page (`src/pages/Auth/Login.jsx`)
|
||||
|
||||
Simple and clean login form ready for production use.
|
||||
|
||||
**Form Fields:**
|
||||
- Email Address (required)
|
||||
- Password (required)
|
||||
|
||||
**Features:**
|
||||
- Email validation (HTML5 + backend)
|
||||
- Loading state during submission
|
||||
- Error message display
|
||||
- Navigation to registration
|
||||
- Navigation back to home
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
User enters credentials
|
||||
↓
|
||||
Login.jsx validates data
|
||||
↓
|
||||
AuthManager.login(email, password)
|
||||
↓
|
||||
LoginService.login(credentials)
|
||||
↓
|
||||
HTTP POST to /api/v1/login
|
||||
↓
|
||||
Backend validates credentials
|
||||
↓
|
||||
Backend returns tokens and user data
|
||||
↓
|
||||
LoginService transforms response
|
||||
↓
|
||||
AuthManager stores tokens in localStorage
|
||||
↓
|
||||
User redirected to /dashboard
|
||||
```
|
||||
|
||||
## Validation Rules
|
||||
|
||||
### Frontend Validation
|
||||
|
||||
1. **Email**: Required, valid email format (HTML5)
|
||||
2. **Password**: Required (no client-side length check for login)
|
||||
|
||||
### Backend Validation
|
||||
|
||||
Backend performs:
|
||||
- Email format validation
|
||||
- Email normalization (lowercase, trim)
|
||||
- Password verification against stored hash
|
||||
- Rate limiting checks
|
||||
- Account lockout checks
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Error Messages
|
||||
|
||||
Errors are mapped to user-friendly messages:
|
||||
|
||||
| Backend Error | Frontend Message |
|
||||
|--------------|------------------|
|
||||
| "Invalid email or password" | "Invalid email or password. Please try again." |
|
||||
| "X attempts remaining" | Original message with attempt counter |
|
||||
| "locked" or "Too many" | "Account temporarily locked due to too many failed attempts. Please try again later." |
|
||||
| "invalid email" | "Invalid email address." |
|
||||
|
||||
### Rate Limiting & Account Lockout
|
||||
|
||||
The backend implements sophisticated rate limiting:
|
||||
|
||||
**Per-IP Rate Limiting:**
|
||||
- Limit: Multiple attempts per 15 minutes
|
||||
- Response: `429 Too Many Requests`
|
||||
- Header: `Retry-After: 900` (15 minutes)
|
||||
|
||||
**Per-Account Rate Limiting:**
|
||||
- Limit: 5 failed attempts
|
||||
- Lockout: 30 minutes after 5 failures
|
||||
- Response: `429 Too Many Requests`
|
||||
- Header: `Retry-After: 1800` (30 minutes)
|
||||
- Warning: Shows remaining attempts when ≤ 3
|
||||
|
||||
**Behavior:**
|
||||
1. First 2 failures: Generic error message
|
||||
2. 3rd-5th failure: Shows remaining attempts
|
||||
3. After 5th failure: Account locked for 30 minutes
|
||||
4. Successful login: Resets all counters
|
||||
|
||||
### Security Event Logging
|
||||
|
||||
Backend logs security events for:
|
||||
- Failed login attempts (with email hash)
|
||||
- Successful logins (with email hash)
|
||||
- IP rate limit exceeded
|
||||
- Account lockouts
|
||||
|
||||
## Security Features
|
||||
|
||||
1. **Token Storage**: Tokens stored in localStorage
|
||||
2. **Token Expiry**: Automatic expiry checking
|
||||
3. **Rate Limiting**: Backend enforces rate limits
|
||||
4. **Account Lockout**: Protects against brute force
|
||||
5. **Email Normalization**: Prevents bypass via casing
|
||||
6. **Secure Logging**: PII never logged, only hashes
|
||||
7. **Password Hash Verification**: Uses bcrypt on backend
|
||||
|
||||
## Token Lifetime
|
||||
|
||||
- **Access Token**: 15 minutes
|
||||
- **Refresh Token**: 7 days
|
||||
- **Session**: 14 days (max inactivity)
|
||||
|
||||
## Testing the Login Flow
|
||||
|
||||
### 1. Prerequisites
|
||||
|
||||
Ensure you have a registered user. If not, register first:
|
||||
|
||||
```bash
|
||||
# Navigate to registration
|
||||
http://localhost:5173/register
|
||||
|
||||
# Or use curl to register via backend
|
||||
curl -X POST http://localhost:8000/api/v1/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "test@example.com",
|
||||
"password": "SecurePass123!",
|
||||
"name": "Test User",
|
||||
"tenant_name": "Test Corp",
|
||||
"tenant_slug": "test-corp",
|
||||
"agree_terms_of_service": true
|
||||
}'
|
||||
```
|
||||
|
||||
### 2. Start Services
|
||||
|
||||
```bash
|
||||
# Backend
|
||||
cd cloud/maplepress-backend
|
||||
task dev
|
||||
|
||||
# Frontend
|
||||
cd web/maplepress-frontend
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 3. Navigate to Login
|
||||
|
||||
Open browser to: `http://localhost:5173/login`
|
||||
|
||||
### 4. Fill Form
|
||||
|
||||
- **Email**: test@example.com
|
||||
- **Password**: SecurePass123!
|
||||
|
||||
### 5. Submit
|
||||
|
||||
Click "Sign In" button
|
||||
|
||||
### 6. Expected Result
|
||||
|
||||
- Loading state appears ("Signing in...")
|
||||
- Request sent to backend
|
||||
- Tokens stored in localStorage
|
||||
- User redirected to `/dashboard`
|
||||
- Dashboard shows user information
|
||||
|
||||
### 7. Verify in Browser Console
|
||||
|
||||
```javascript
|
||||
// Check stored tokens
|
||||
localStorage.getItem('maplepress_access_token')
|
||||
localStorage.getItem('maplepress_user')
|
||||
localStorage.getItem('maplepress_tenant')
|
||||
|
||||
// Check service instance
|
||||
window.maplePressServices.authManager.isAuthenticated() // true
|
||||
window.maplePressServices.authManager.getUser()
|
||||
// Returns: { id: "...", email: "test@example.com", name: "Test User", role: "..." }
|
||||
|
||||
window.maplePressServices.authManager.getTenant()
|
||||
// Returns: { id: "...", name: null, slug: null }
|
||||
// Note: name/slug are null because login endpoint doesn't provide them
|
||||
```
|
||||
|
||||
## Error Testing
|
||||
|
||||
### Test Invalid Credentials
|
||||
|
||||
```javascript
|
||||
// In login form, enter:
|
||||
Email: test@example.com
|
||||
Password: WrongPassword123
|
||||
|
||||
// Expected: "Invalid email or password. Please try again."
|
||||
```
|
||||
|
||||
### Test Rate Limiting
|
||||
|
||||
```javascript
|
||||
// Attempt login 3 times with wrong password
|
||||
// Expected on 3rd attempt: "Invalid email or password. 2 attempts remaining before account lockout."
|
||||
|
||||
// Attempt 2 more times
|
||||
// Expected on 5th attempt: "Account temporarily locked due to too many failed attempts. Please try again later."
|
||||
```
|
||||
|
||||
### Test Missing Fields
|
||||
|
||||
```javascript
|
||||
// Leave email blank
|
||||
// Expected: Browser validation error (HTML5 required)
|
||||
|
||||
// Leave password blank
|
||||
// Expected: Browser validation error (HTML5 required)
|
||||
```
|
||||
|
||||
## Differences from Registration
|
||||
|
||||
| Feature | Registration | Login |
|
||||
|---------|-------------|-------|
|
||||
| Request Fields | 10+ fields | 2 fields only |
|
||||
| Response Fields | Includes tenant name/slug | No tenant name/slug |
|
||||
| Timestamp | `created_at` | `login_at` |
|
||||
| Creates User | Yes | No |
|
||||
| Creates Tenant | Yes | No |
|
||||
| Terms Agreement | Required | Not needed |
|
||||
| Organization Info | Required | Not needed |
|
||||
|
||||
## Token Storage Compatibility
|
||||
|
||||
Both registration and login use the same storage mechanism:
|
||||
|
||||
```javascript
|
||||
// Storage Keys (7 total)
|
||||
maplepress_access_token // JWT access token
|
||||
maplepress_refresh_token // JWT refresh token
|
||||
maplepress_access_expiry // ISO date string
|
||||
maplepress_refresh_expiry // ISO date string
|
||||
maplepress_user // JSON: {id, email, name, role}
|
||||
maplepress_tenant // JSON: {id, name, slug}
|
||||
maplepress_session_id // Session UUID
|
||||
```
|
||||
|
||||
**Note**: After login, `tenant.name` and `tenant.slug` will be `null`. This is expected behavior. If needed, fetch tenant details separately using the `/api/v1/tenants/{id}` endpoint.
|
||||
|
||||
## Session Persistence
|
||||
|
||||
Authentication state persists across:
|
||||
- Page refreshes
|
||||
- Browser restarts (if localStorage not cleared)
|
||||
- Tab changes
|
||||
|
||||
Session is cleared on:
|
||||
- User logout
|
||||
- Token expiry detection
|
||||
- Manual localStorage clear
|
||||
|
||||
## Integration with Dashboard
|
||||
|
||||
The dashboard automatically checks authentication:
|
||||
|
||||
```javascript
|
||||
// Dashboard.jsx
|
||||
useEffect(() => {
|
||||
if (!authManager.isAuthenticated()) {
|
||||
navigate("/login");
|
||||
return;
|
||||
}
|
||||
|
||||
const userData = authManager.getUser();
|
||||
setUser(userData);
|
||||
}, [authManager, navigate]);
|
||||
```
|
||||
|
||||
## API Client Configuration
|
||||
|
||||
The `ApiClient` automatically handles:
|
||||
- JSON content-type headers
|
||||
- Request/response transformation
|
||||
- Error parsing
|
||||
- Authentication headers (for protected endpoints)
|
||||
|
||||
**Note**: Login endpoint doesn't require authentication headers, but subsequent API calls will use the stored access token.
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Planned Features
|
||||
|
||||
- [ ] "Remember Me" checkbox (longer session)
|
||||
- [ ] "Forgot Password" link
|
||||
- [ ] Social authentication (Google, GitHub)
|
||||
- [ ] Two-factor authentication (2FA/TOTP)
|
||||
- [ ] Session management (view active sessions)
|
||||
- [ ] Device fingerprinting
|
||||
- [ ] Suspicious login detection
|
||||
|
||||
### Security Improvements
|
||||
|
||||
- [ ] CSRF token implementation
|
||||
- [ ] HTTP-only cookie option
|
||||
- [ ] Session fingerprinting
|
||||
- [ ] Geolocation tracking
|
||||
- [ ] Email notification on new login
|
||||
- [ ] Passwordless login option
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Invalid email or password" but credentials are correct
|
||||
|
||||
**Possible causes:**
|
||||
1. Email case sensitivity - backend normalizes to lowercase
|
||||
2. Extra whitespace in password field
|
||||
3. User not yet registered
|
||||
4. Account locked due to previous failed attempts
|
||||
|
||||
**Solution:**
|
||||
- Wait 30 minutes if account is locked
|
||||
- Try registering if user doesn't exist
|
||||
- Check browser console for detailed errors
|
||||
|
||||
### Tokens not being stored
|
||||
|
||||
**Possible causes:**
|
||||
1. localStorage disabled in browser
|
||||
2. Private/Incognito mode restrictions
|
||||
3. Browser extension blocking storage
|
||||
|
||||
**Solution:**
|
||||
- Enable localStorage in browser settings
|
||||
- Use regular browser window
|
||||
- Disable blocking extensions temporarily
|
||||
|
||||
### Redirected back to login after successful login
|
||||
|
||||
**Possible causes:**
|
||||
1. Token expiry detection triggered
|
||||
2. Token format invalid
|
||||
3. localStorage cleared between operations
|
||||
|
||||
**Solution:**
|
||||
- Check browser console for errors
|
||||
- Verify localStorage contains tokens
|
||||
- Check token expiry dates
|
||||
|
||||
## Related Files
|
||||
|
||||
### Created Files
|
||||
```
|
||||
src/services/API/LoginService.js
|
||||
```
|
||||
|
||||
### Modified Files
|
||||
```
|
||||
src/services/Manager/AuthManager.js
|
||||
src/pages/Auth/Login.jsx
|
||||
```
|
||||
|
||||
### Backend Reference Files
|
||||
```
|
||||
cloud/maplepress-backend/docs/API.md
|
||||
cloud/maplepress-backend/internal/interface/http/dto/gateway/login_dto.go
|
||||
cloud/maplepress-backend/internal/interface/http/handler/gateway/login_handler.go
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [REGISTRATION_API.md](./REGISTRATION_API.md) - Registration implementation
|
||||
- [ARCHITECTURE.md](./ARCHITECTURE.md) - Frontend architecture overview
|
||||
- [README.md](./README.md) - Getting started guide
|
||||
- [Backend API Documentation](../../cloud/maplepress-backend/docs/API.md) - Complete API reference
|
||||
|
||||
## Support
|
||||
|
||||
For issues:
|
||||
1. Check backend logs: `docker logs mapleopentech_backend`
|
||||
2. Check browser console for errors
|
||||
3. Verify backend is running on port 8000
|
||||
4. Test backend endpoint directly with curl
|
||||
5. Check rate limiting status (wait 30 minutes if locked)
|
||||
676
web/maplepress-frontend/docs/API/ME_API.md
Normal file
676
web/maplepress-frontend/docs/API/ME_API.md
Normal file
|
|
@ -0,0 +1,676 @@
|
|||
# Me API Implementation (User Profile)
|
||||
|
||||
This document describes the implementation of the Me API endpoint for the MaplePress frontend, integrated with the MaplePress backend API.
|
||||
|
||||
## Overview
|
||||
|
||||
The Me API endpoint returns the authenticated user's profile information extracted directly from the JWT token. This is a lightweight endpoint that requires no database queries, making it ideal for displaying user information in the dashboard and verifying the current user's identity.
|
||||
|
||||
## Backend API Endpoint
|
||||
|
||||
**Endpoint**: `GET /api/v1/me`
|
||||
**Authentication**: Required (JWT token)
|
||||
**Documentation**: `/cloud/maplepress-backend/docs/API.md` (lines 374-413)
|
||||
|
||||
### Request Structure
|
||||
|
||||
No request body required. Authentication is provided via the Authorization header:
|
||||
|
||||
**Headers Required:**
|
||||
- `Authorization: JWT {access_token}`
|
||||
|
||||
### Response Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"user_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"email": "john@example.com",
|
||||
"name": "John Doe",
|
||||
"role": "owner",
|
||||
"tenant_id": "650e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
```
|
||||
|
||||
### Key Features
|
||||
|
||||
- **No Database Query**: All data extracted from JWT token claims
|
||||
- **Fast Response**: Minimal processing required
|
||||
- **Current User Only**: Returns data for the authenticated user
|
||||
- **Role Information**: Includes user's role for authorization checks
|
||||
- **Tenant Context**: Includes tenant ID for multi-tenant operations
|
||||
|
||||
## Frontend Implementation
|
||||
|
||||
### MeService (`src/services/API/MeService.js`)
|
||||
|
||||
Handles direct communication with the backend Me API and provides utility methods for user profile operations.
|
||||
|
||||
**Key Features:**
|
||||
- GET request with automatic JWT authentication
|
||||
- Response transformation (snake_case to camelCase)
|
||||
- User-friendly error message mapping
|
||||
- Helper methods for common use cases
|
||||
- Automatic token refresh integration
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `getMe()`
|
||||
Main method to fetch the current user's profile.
|
||||
|
||||
```javascript
|
||||
import MeService from './services/API/MeService';
|
||||
|
||||
const profile = await MeService.getMe();
|
||||
console.log(profile);
|
||||
// Output:
|
||||
// {
|
||||
// userId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
// email: "john@example.com",
|
||||
// name: "John Doe",
|
||||
// role: "owner",
|
||||
// tenantId: "650e8400-e29b-41d4-a716-446655440000"
|
||||
// }
|
||||
```
|
||||
|
||||
**Returns:**
|
||||
```javascript
|
||||
{
|
||||
userId: string, // User's unique identifier (UUID)
|
||||
email: string, // User's email address
|
||||
name: string, // User's full name
|
||||
role: string, // User's role (e.g., "owner", "admin", "user")
|
||||
tenantId: string // User's tenant/organization ID (UUID)
|
||||
}
|
||||
```
|
||||
|
||||
**Throws:**
|
||||
- "Authentication required. Please log in to continue." - If JWT token is missing/invalid
|
||||
- "Invalid or expired authentication token. Please log in again." - If token has expired
|
||||
- Generic error message for other failures
|
||||
|
||||
#### `hasRole(requiredRole)`
|
||||
Check if the current user has a specific role.
|
||||
|
||||
```javascript
|
||||
const isOwner = await MeService.hasRole("owner");
|
||||
if (isOwner) {
|
||||
console.log("User is an owner");
|
||||
}
|
||||
|
||||
const isAdmin = await MeService.hasRole("admin");
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `requiredRole` (string): Role to check for (e.g., "owner", "admin", "user")
|
||||
|
||||
**Returns:** `boolean` - True if user has the required role
|
||||
|
||||
#### `getTenantId()`
|
||||
Get the current user's tenant ID.
|
||||
|
||||
```javascript
|
||||
const tenantId = await MeService.getTenantId();
|
||||
console.log("User's tenant:", tenantId);
|
||||
```
|
||||
|
||||
**Returns:** `string|null` - Tenant ID or null if not available
|
||||
|
||||
#### `isCurrentUser(userId)`
|
||||
Verify that the current user matches a specific user ID.
|
||||
|
||||
```javascript
|
||||
const isSameUser = await MeService.isCurrentUser("550e8400-...");
|
||||
if (isSameUser) {
|
||||
console.log("This is the current user");
|
||||
}
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `userId` (string): User ID to verify against
|
||||
|
||||
**Returns:** `boolean` - True if the current user matches the provided ID
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
User is authenticated (has JWT token)
|
||||
↓
|
||||
Component calls MeService.getMe()
|
||||
↓
|
||||
ApiClient.get("/api/v1/me")
|
||||
↓
|
||||
Token automatically refreshed if needed (ApiClient feature)
|
||||
↓
|
||||
GET /api/v1/me with Authorization header
|
||||
↓
|
||||
Backend JWT middleware extracts user from token
|
||||
↓
|
||||
Backend returns user profile from JWT claims
|
||||
↓
|
||||
MeService transforms response (snake_case → camelCase)
|
||||
↓
|
||||
Component receives user profile
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Error Types
|
||||
|
||||
| Error Condition | Response | Frontend Behavior |
|
||||
|----------------|----------|-------------------|
|
||||
| Missing JWT token | 401 Unauthorized | "Authentication required. Please log in to continue." |
|
||||
| Invalid JWT token | 401 Unauthorized | "Invalid or expired authentication token." |
|
||||
| Expired JWT token | 401 Unauthorized | Token auto-refresh triggered, then retry |
|
||||
| Server error | 500 Internal Server Error | Generic error message |
|
||||
|
||||
### Error Message Mapping
|
||||
|
||||
```javascript
|
||||
// Backend error → Frontend error
|
||||
"unauthorized" → "Authentication required. Please log in to continue."
|
||||
"token" → "Invalid or expired authentication token. Please log in again."
|
||||
Other errors → Original error message or generic fallback
|
||||
```
|
||||
|
||||
## Security Features
|
||||
|
||||
1. **JWT Authentication Required**: Endpoint requires valid JWT token
|
||||
2. **Token Validation**: Backend validates token signature and expiration
|
||||
3. **No PII in Logs**: Backend uses hashed/redacted email in logs (CWE-532)
|
||||
4. **Automatic Token Refresh**: ApiClient ensures token is valid before request
|
||||
5. **Context Extraction**: User data extracted from secure JWT context
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Display User Profile in Dashboard
|
||||
|
||||
```javascript
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import MeService from '../../services/API/MeService';
|
||||
|
||||
function UserProfile() {
|
||||
const [profile, setProfile] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const fetchProfile = async () => {
|
||||
try {
|
||||
const data = await MeService.getMe();
|
||||
setProfile(data);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchProfile();
|
||||
}, []);
|
||||
|
||||
if (loading) return <div>Loading profile...</div>;
|
||||
if (error) return <div>Error: {error}</div>;
|
||||
if (!profile) return null;
|
||||
|
||||
return (
|
||||
<div className="user-profile">
|
||||
<h2>Welcome, {profile.name}!</h2>
|
||||
<p>Email: {profile.email}</p>
|
||||
<p>Role: {profile.role}</p>
|
||||
<p>User ID: {profile.userId}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserProfile;
|
||||
```
|
||||
|
||||
### Role-Based Access Control
|
||||
|
||||
```javascript
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import MeService from '../../services/API/MeService';
|
||||
|
||||
function AdminPanel() {
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const checkAdmin = async () => {
|
||||
const hasAdminRole = await MeService.hasRole("owner");
|
||||
setIsAdmin(hasAdminRole);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
checkAdmin();
|
||||
}, []);
|
||||
|
||||
if (loading) return <div>Checking permissions...</div>;
|
||||
|
||||
if (!isAdmin) {
|
||||
return <div>Access denied. Admin rights required.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="admin-panel">
|
||||
<h2>Admin Panel</h2>
|
||||
{/* Admin content */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AdminPanel;
|
||||
```
|
||||
|
||||
### Sync Profile with AuthManager
|
||||
|
||||
```javascript
|
||||
import { useAuth } from '../../services/Services';
|
||||
import MeService from '../../services/API/MeService';
|
||||
|
||||
function Dashboard() {
|
||||
const { authManager } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
const syncProfile = async () => {
|
||||
try {
|
||||
// Get fresh profile from backend
|
||||
const profile = await MeService.getMe();
|
||||
|
||||
// Get stored profile from AuthManager
|
||||
const storedUser = authManager.getUser();
|
||||
|
||||
// Check if data is in sync
|
||||
if (storedUser.email !== profile.email) {
|
||||
console.warn("Profile data mismatch detected");
|
||||
// Could update AuthManager or prompt user
|
||||
}
|
||||
|
||||
console.log("Profile synced successfully");
|
||||
} catch (error) {
|
||||
console.error("Failed to sync profile:", error);
|
||||
}
|
||||
};
|
||||
|
||||
syncProfile();
|
||||
}, [authManager]);
|
||||
|
||||
return <div>Dashboard</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### Check Tenant Context
|
||||
|
||||
```javascript
|
||||
import MeService from '../../services/API/MeService';
|
||||
|
||||
async function verifyTenantAccess(requiredTenantId) {
|
||||
const userTenantId = await MeService.getTenantId();
|
||||
|
||||
if (userTenantId !== requiredTenantId) {
|
||||
throw new Error("Access denied. Wrong tenant context.");
|
||||
}
|
||||
|
||||
console.log("Tenant access verified");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Usage
|
||||
try {
|
||||
await verifyTenantAccess("650e8400-e29b-41d4-a716-446655440000");
|
||||
// Proceed with tenant-specific operations
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
// Redirect or show error
|
||||
}
|
||||
```
|
||||
|
||||
### Verify Current User
|
||||
|
||||
```javascript
|
||||
import MeService from '../../services/API/MeService';
|
||||
|
||||
function EditProfileButton({ profileOwnerId }) {
|
||||
const [canEdit, setCanEdit] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkOwnership = async () => {
|
||||
const isOwner = await MeService.isCurrentUser(profileOwnerId);
|
||||
setCanEdit(isOwner);
|
||||
};
|
||||
|
||||
checkOwnership();
|
||||
}, [profileOwnerId]);
|
||||
|
||||
if (!canEdit) return null;
|
||||
|
||||
return <button>Edit Profile</button>;
|
||||
}
|
||||
```
|
||||
|
||||
## Testing the Me API
|
||||
|
||||
### 1. Prerequisites
|
||||
|
||||
- Backend running at `http://localhost:8000`
|
||||
- Frontend running at `http://localhost:5173`
|
||||
- User logged in (valid JWT token)
|
||||
|
||||
### 2. Testing via Dashboard
|
||||
|
||||
Add a profile component to the dashboard:
|
||||
|
||||
```javascript
|
||||
// In Dashboard.jsx
|
||||
import MeService from '../../services/API/MeService';
|
||||
|
||||
const [userProfile, setUserProfile] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadProfile = async () => {
|
||||
try {
|
||||
const profile = await MeService.getMe();
|
||||
setUserProfile(profile);
|
||||
console.log("Profile loaded:", profile);
|
||||
} catch (error) {
|
||||
console.error("Failed to load profile:", error);
|
||||
}
|
||||
};
|
||||
|
||||
loadProfile();
|
||||
}, []);
|
||||
|
||||
// In JSX
|
||||
{userProfile && (
|
||||
<div>
|
||||
<h3>{userProfile.name}</h3>
|
||||
<p>{userProfile.email}</p>
|
||||
<p>Role: {userProfile.role}</p>
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
|
||||
### 3. Testing via Browser Console
|
||||
|
||||
```javascript
|
||||
// After logging in, open browser console
|
||||
|
||||
// Test basic profile fetch
|
||||
const profile = await MeService.getMe();
|
||||
console.log(profile);
|
||||
// Expected: User profile object
|
||||
|
||||
// Test role check
|
||||
const isOwner = await MeService.hasRole("owner");
|
||||
console.log("Is owner:", isOwner);
|
||||
|
||||
// Test tenant ID
|
||||
const tenantId = await MeService.getTenantId();
|
||||
console.log("Tenant ID:", tenantId);
|
||||
|
||||
// Test user verification
|
||||
const isCurrent = await MeService.isCurrentUser(profile.userId);
|
||||
console.log("Is current user:", isCurrent); // Should be true
|
||||
```
|
||||
|
||||
### 4. Testing with curl
|
||||
|
||||
```bash
|
||||
# 1. Login and get access token
|
||||
ACCESS_TOKEN=$(curl -X POST http://localhost:8000/api/v1/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "test@example.com",
|
||||
"password": "SecurePass123!"
|
||||
}' | jq -r '.access_token')
|
||||
|
||||
# 2. Test me endpoint
|
||||
curl -X GET http://localhost:8000/api/v1/me \
|
||||
-H "Authorization: JWT $ACCESS_TOKEN" | jq
|
||||
|
||||
# Expected output:
|
||||
# {
|
||||
# "user_id": "550e8400-...",
|
||||
# "email": "test@example.com",
|
||||
# "name": "Test User",
|
||||
# "role": "owner",
|
||||
# "tenant_id": "650e8400-..."
|
||||
# }
|
||||
|
||||
# 3. Test without authentication (should fail)
|
||||
curl -X GET http://localhost:8000/api/v1/me | jq
|
||||
|
||||
# Expected: 401 Unauthorized
|
||||
|
||||
# 4. Test with invalid token (should fail)
|
||||
curl -X GET http://localhost:8000/api/v1/me \
|
||||
-H "Authorization: JWT invalid_token" | jq
|
||||
|
||||
# Expected: 401 Unauthorized
|
||||
```
|
||||
|
||||
### 5. Testing Token Refresh Integration
|
||||
|
||||
```javascript
|
||||
// Test that token refresh works automatically
|
||||
|
||||
// 1. Login
|
||||
// 2. Manually set token expiry to 30 seconds from now
|
||||
const newExpiry = new Date(Date.now() + 30000).toISOString();
|
||||
localStorage.setItem('maplepress_access_expiry', newExpiry);
|
||||
|
||||
// 3. Wait 31 seconds, then call getMe
|
||||
await new Promise(resolve => setTimeout(resolve, 31000));
|
||||
const profile = await MeService.getMe();
|
||||
|
||||
// 4. Check console logs - should see automatic token refresh
|
||||
// 5. Response should be successful despite expired token
|
||||
console.log("Profile after token refresh:", profile);
|
||||
```
|
||||
|
||||
### 6. Testing Against Stored Data
|
||||
|
||||
```javascript
|
||||
// Compare Me API response with stored AuthManager data
|
||||
|
||||
import { useAuth } from './services/Services';
|
||||
import MeService from './services/API/MeService';
|
||||
|
||||
const { authManager } = useAuth();
|
||||
|
||||
// Get stored user
|
||||
const storedUser = authManager.getUser();
|
||||
console.log("Stored user:", storedUser);
|
||||
|
||||
// Get fresh profile from API
|
||||
const profile = await MeService.getMe();
|
||||
console.log("API profile:", profile);
|
||||
|
||||
// Compare
|
||||
console.log("Email match:", storedUser.email === profile.email);
|
||||
console.log("Name match:", storedUser.name === profile.name);
|
||||
console.log("Role match:", storedUser.role === profile.role);
|
||||
```
|
||||
|
||||
## Integration with Existing Services
|
||||
|
||||
The MeService automatically integrates with existing infrastructure:
|
||||
|
||||
### Authentication (AuthManager)
|
||||
```javascript
|
||||
// MeService uses ApiClient, which automatically:
|
||||
// - Adds JWT token to Authorization header
|
||||
// - Refreshes token if expired
|
||||
// - Handles 401 errors
|
||||
```
|
||||
|
||||
### Token Refresh (RefreshTokenService)
|
||||
```javascript
|
||||
// Automatic token refresh before getMe request
|
||||
// If token expires within 1 minute, it's refreshed proactively
|
||||
// If 401 received, token is refreshed and request is retried
|
||||
```
|
||||
|
||||
### Data Consistency
|
||||
```javascript
|
||||
// Me API returns data from JWT token
|
||||
// AuthManager stores data from login/register/refresh
|
||||
// Both should be in sync, but Me API is source of truth
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Dashboard User Display
|
||||
Display the current user's name and email in the dashboard header.
|
||||
|
||||
```javascript
|
||||
const profile = await MeService.getMe();
|
||||
setDashboardHeader(profile.name, profile.email);
|
||||
```
|
||||
|
||||
### 2. Role-Based UI
|
||||
Show or hide UI elements based on user role.
|
||||
|
||||
```javascript
|
||||
const isAdmin = await MeService.hasRole("owner");
|
||||
setShowAdminPanel(isAdmin);
|
||||
```
|
||||
|
||||
### 3. Tenant Context
|
||||
Ensure user is operating in the correct tenant context.
|
||||
|
||||
```javascript
|
||||
const tenantId = await MeService.getTenantId();
|
||||
loadTenantSpecificData(tenantId);
|
||||
```
|
||||
|
||||
### 4. Profile Verification
|
||||
Verify that the current user matches a resource owner.
|
||||
|
||||
```javascript
|
||||
const canEdit = await MeService.isCurrentUser(resourceOwnerId);
|
||||
```
|
||||
|
||||
### 5. Session Validation
|
||||
Periodically verify that the session is still valid.
|
||||
|
||||
```javascript
|
||||
setInterval(async () => {
|
||||
try {
|
||||
await MeService.getMe();
|
||||
console.log("Session still valid");
|
||||
} catch (error) {
|
||||
console.log("Session expired, logging out");
|
||||
authManager.logout();
|
||||
}
|
||||
}, 5 * 60 * 1000); // Every 5 minutes
|
||||
```
|
||||
|
||||
## Data Comparison: JWT vs Stored
|
||||
|
||||
### JWT Token Data (from Me API)
|
||||
- **Source**: JWT token claims
|
||||
- **Updated**: On login, registration, or token refresh
|
||||
- **Authoritative**: Yes (always current)
|
||||
- **Requires Network**: Yes
|
||||
|
||||
### AuthManager Stored Data
|
||||
- **Source**: localStorage (from login/register/refresh responses)
|
||||
- **Updated**: On login, registration, or token refresh
|
||||
- **Authoritative**: No (could be stale)
|
||||
- **Requires Network**: No
|
||||
|
||||
**Recommendation**: Use Me API when you need authoritative current data, use AuthManager for quick access without network calls.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Authentication required" error
|
||||
|
||||
**Possible causes:**
|
||||
1. User not logged in
|
||||
2. Access token expired
|
||||
3. Refresh token expired
|
||||
4. Session invalidated
|
||||
|
||||
**Solution:**
|
||||
- Check `authManager.isAuthenticated()` before calling
|
||||
- Login again if session expired
|
||||
- Check browser console for token refresh logs
|
||||
|
||||
### Profile data doesn't match stored data
|
||||
|
||||
**Possible causes:**
|
||||
1. Token was refreshed but AuthManager not updated
|
||||
2. Profile was updated on another device
|
||||
3. Data corruption in localStorage
|
||||
|
||||
**Solution:**
|
||||
- Me API is source of truth - use its data
|
||||
- Update AuthManager with fresh data if needed
|
||||
- Clear localStorage and login again if corrupted
|
||||
|
||||
### Request succeeds but returns empty fields
|
||||
|
||||
**Possible causes:**
|
||||
1. JWT token missing required claims
|
||||
2. Backend not setting context values correctly
|
||||
3. Token format incorrect
|
||||
|
||||
**Solution:**
|
||||
- Check token structure (decode JWT at jwt.io)
|
||||
- Verify backend is setting all required claims
|
||||
- Re-login to get fresh token
|
||||
|
||||
### 401 error despite valid token
|
||||
|
||||
**Possible causes:**
|
||||
1. Clock skew between client and server
|
||||
2. Token signature invalid
|
||||
3. Token expired but not detected
|
||||
|
||||
**Solution:**
|
||||
- Check system clock synchronization
|
||||
- Clear tokens and login again
|
||||
- Verify token expiry dates in localStorage
|
||||
|
||||
## Related Files
|
||||
|
||||
### Created Files
|
||||
```
|
||||
src/services/API/MeService.js
|
||||
docs/ME_API.md
|
||||
```
|
||||
|
||||
### Backend Reference Files
|
||||
```
|
||||
cloud/maplepress-backend/docs/API.md (lines 374-413)
|
||||
cloud/maplepress-backend/internal/interface/http/handler/gateway/me_handler.go
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [LOGIN_API.md](./LOGIN_API.md) - Login and JWT token acquisition
|
||||
- [REFRESH_TOKEN_API.md](./REFRESH_TOKEN_API.md) - Automatic token refresh
|
||||
- [HELLO_API.md](./HELLO_API.md) - Simple authenticated endpoint testing
|
||||
- [FRONTEND_ARCHITECTURE.md](./FRONTEND_ARCHITECTURE.md) - Architecture overview
|
||||
- [README.md](./README.md) - Documentation index
|
||||
- [Backend API Documentation](../../../cloud/maplepress-backend/docs/API.md) - Complete API reference
|
||||
|
||||
## Summary
|
||||
|
||||
The Me API implementation provides:
|
||||
|
||||
1. **User Profile Access**: Get current user's profile from JWT token
|
||||
2. **Fast & Efficient**: No database queries, data from token claims
|
||||
3. **Helper Methods**: Role checking, tenant ID, user verification
|
||||
4. **Automatic Integration**: Works seamlessly with token refresh
|
||||
5. **Error Handling**: Clear error messages and graceful failures
|
||||
6. **Use Case Focused**: Methods designed for common scenarios
|
||||
|
||||
This endpoint is essential for displaying user information in the dashboard and implementing role-based access control.
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: October 30, 2024
|
||||
**Frontend Version**: 0.0.0
|
||||
**Documentation Version**: 1.0.0
|
||||
631
web/maplepress-frontend/docs/API/REFRESH_TOKEN_API.md
Normal file
631
web/maplepress-frontend/docs/API/REFRESH_TOKEN_API.md
Normal file
|
|
@ -0,0 +1,631 @@
|
|||
# Token Refresh API Implementation
|
||||
|
||||
This document describes the complete implementation of the token refresh feature for the MaplePress frontend, integrated with the MaplePress backend API.
|
||||
|
||||
## Overview
|
||||
|
||||
The token refresh feature allows users to obtain new access tokens without requiring them to log in again. This maintains seamless user sessions by automatically refreshing expired tokens in the background.
|
||||
|
||||
## Backend API Endpoint
|
||||
|
||||
**Endpoint**: `POST /api/v1/refresh`
|
||||
**Authentication**: None required (public endpoint, but requires valid refresh token)
|
||||
**Documentation**: `/cloud/maplepress-backend/docs/API.md` (lines 230-324)
|
||||
|
||||
### Request Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
}
|
||||
```
|
||||
|
||||
### Response Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"user_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"user_email": "john@example.com",
|
||||
"user_name": "John Doe",
|
||||
"user_role": "user",
|
||||
"tenant_id": "650e8400-e29b-41d4-a716-446655440000",
|
||||
"session_id": "750e8400-e29b-41d4-a716-446655440000",
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"access_expiry": "2024-10-24T12:30:00Z",
|
||||
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"refresh_expiry": "2024-10-31T00:15:00Z",
|
||||
"refreshed_at": "2024-10-24T12:15:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Token Rotation**: Both access and refresh tokens are regenerated on each refresh
|
||||
- **Old Token Invalidation**: Previous tokens become invalid immediately
|
||||
- **Session Validation**: Validates that the session still exists and is active
|
||||
- **Automatic Expiry**: Access tokens expire after 15 minutes, refresh tokens after 7 days
|
||||
|
||||
## Frontend Implementation
|
||||
|
||||
### 1. RefreshTokenService (`src/services/API/RefreshTokenService.js`)
|
||||
|
||||
Handles direct communication with the backend refresh API.
|
||||
|
||||
**Key Features:**
|
||||
- Request validation (required refresh token)
|
||||
- Request body formatting (snake_case for backend)
|
||||
- Response transformation (camelCase for frontend)
|
||||
- User-friendly error message mapping
|
||||
- Helper functions for token expiry checking
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `refreshToken(refreshToken)`
|
||||
Main refresh method that calls the backend API.
|
||||
|
||||
```javascript
|
||||
import RefreshTokenService from './services/API/RefreshTokenService';
|
||||
|
||||
const response = await RefreshTokenService.refreshToken("eyJhbGci...");
|
||||
```
|
||||
|
||||
**Returns:**
|
||||
```javascript
|
||||
{
|
||||
userId: string,
|
||||
userEmail: string,
|
||||
userName: string,
|
||||
userRole: string,
|
||||
tenantId: string,
|
||||
sessionId: string,
|
||||
accessToken: string,
|
||||
accessExpiry: Date,
|
||||
refreshToken: string,
|
||||
refreshExpiry: Date,
|
||||
refreshedAt: Date
|
||||
}
|
||||
```
|
||||
|
||||
#### `shouldRefreshToken(accessExpiry, bufferMinutes = 1)`
|
||||
Checks if token should be refreshed based on expiry time.
|
||||
|
||||
```javascript
|
||||
const needsRefresh = RefreshTokenService.shouldRefreshToken(
|
||||
new Date('2024-10-24T12:14:30Z'), // Access expiry
|
||||
1 // Refresh if expiring within 1 minute
|
||||
);
|
||||
```
|
||||
|
||||
#### `isRefreshTokenValid(refreshExpiry)`
|
||||
Checks if the refresh token itself is still valid.
|
||||
|
||||
```javascript
|
||||
const isValid = RefreshTokenService.isRefreshTokenValid(
|
||||
new Date('2024-10-31T00:15:00Z')
|
||||
);
|
||||
```
|
||||
|
||||
### 2. AuthManager Enhancement (`src/services/Manager/AuthManager.js`)
|
||||
|
||||
Updated with complete token refresh functionality and automatic refresh logic.
|
||||
|
||||
**New Features:**
|
||||
- Token refresh on initialization if access token expired
|
||||
- Prevents duplicate refresh requests with `refreshPromise` tracking
|
||||
- Automatic token refresh before API requests
|
||||
- Handles refresh failures gracefully
|
||||
|
||||
**New Methods:**
|
||||
|
||||
#### `async refreshAccessToken()`
|
||||
Refreshes the access token using the stored refresh token.
|
||||
|
||||
```javascript
|
||||
const authManager = useAuth().authManager;
|
||||
|
||||
try {
|
||||
await authManager.refreshAccessToken();
|
||||
console.log("Token refreshed successfully");
|
||||
} catch (error) {
|
||||
console.error("Refresh failed:", error);
|
||||
// User will be logged out automatically
|
||||
}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Prevents duplicate refresh requests (returns same promise if refresh in progress)
|
||||
- Validates refresh token exists and is not expired
|
||||
- Clears session on refresh failure
|
||||
- Implements token rotation (stores new tokens)
|
||||
- Preserves tenant name/slug from previous session
|
||||
|
||||
#### `shouldRefreshToken(bufferMinutes = 1)`
|
||||
Checks if the current access token needs refresh.
|
||||
|
||||
```javascript
|
||||
if (authManager.shouldRefreshToken()) {
|
||||
await authManager.refreshAccessToken();
|
||||
}
|
||||
```
|
||||
|
||||
#### `async ensureValidToken()`
|
||||
Automatically refreshes token if needed. Called before API requests.
|
||||
|
||||
```javascript
|
||||
// Automatically called by ApiClient before each request
|
||||
await authManager.ensureValidToken();
|
||||
```
|
||||
|
||||
### 3. ApiClient Enhancement (`src/services/API/ApiClient.js`)
|
||||
|
||||
Updated to automatically refresh tokens before making requests and handle 401 errors.
|
||||
|
||||
**Automatic Token Refresh:**
|
||||
- Calls `ensureValidToken()` before every API request
|
||||
- Skips refresh for the `/api/v1/refresh` endpoint itself
|
||||
- Can be disabled per-request with `skipTokenRefresh` option
|
||||
|
||||
**401 Error Handling:**
|
||||
- Automatically attempts token refresh on 401 Unauthorized
|
||||
- Retries the original request once with new token
|
||||
- Logs out user if refresh fails
|
||||
|
||||
**Enhanced Request Flow:**
|
||||
```javascript
|
||||
1. Check if token needs refresh (within 1 minute of expiry)
|
||||
2. If yes, refresh token proactively
|
||||
3. Make API request with current token
|
||||
4. If 401 received, force refresh and retry once
|
||||
5. If retry fails, throw error and clear session
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Initial Authentication
|
||||
```
|
||||
User logs in
|
||||
↓
|
||||
Backend returns tokens
|
||||
↓
|
||||
AuthManager stores tokens
|
||||
↓
|
||||
Access Token: valid for 15 minutes
|
||||
Refresh Token: valid for 7 days
|
||||
```
|
||||
|
||||
### Proactive Token Refresh (Before Expiry)
|
||||
```
|
||||
User makes API request
|
||||
↓
|
||||
ApiClient checks token expiry
|
||||
↓
|
||||
Token expires in < 1 minute?
|
||||
↓
|
||||
Yes: Refresh token proactively
|
||||
↓
|
||||
AuthManager.ensureValidToken()
|
||||
↓
|
||||
AuthManager.refreshAccessToken()
|
||||
↓
|
||||
RefreshTokenService.refreshToken()
|
||||
↓
|
||||
POST /api/v1/refresh
|
||||
↓
|
||||
Backend validates session
|
||||
↓
|
||||
Backend returns new tokens
|
||||
↓
|
||||
AuthManager stores new tokens
|
||||
↓
|
||||
Continue with original API request
|
||||
```
|
||||
|
||||
### Reactive Token Refresh (After 401 Error)
|
||||
```
|
||||
API request made
|
||||
↓
|
||||
Backend returns 401 Unauthorized
|
||||
↓
|
||||
ApiClient detects 401
|
||||
↓
|
||||
Attempt token refresh
|
||||
↓
|
||||
Retry original request
|
||||
↓
|
||||
Success: Return response
|
||||
Failure: Clear session and throw error
|
||||
```
|
||||
|
||||
### Token Refresh on App Initialization
|
||||
```
|
||||
App starts
|
||||
↓
|
||||
AuthManager.initialize()
|
||||
↓
|
||||
Load tokens from localStorage
|
||||
↓
|
||||
Access token expired?
|
||||
↓
|
||||
Yes: Check refresh token
|
||||
↓
|
||||
Refresh token valid?
|
||||
↓
|
||||
Yes: Refresh access token
|
||||
No: Clear session
|
||||
```
|
||||
|
||||
## Token Lifecycle
|
||||
|
||||
### Token Expiry Timeline
|
||||
|
||||
```
|
||||
Login (00:00)
|
||||
│
|
||||
├─ Access Token: expires at 00:15 (15 minutes)
|
||||
│ └─ Proactive refresh at 00:14 (1 minute before)
|
||||
│
|
||||
└─ Refresh Token: expires at 7 days later
|
||||
└─ Requires new login when expired
|
||||
```
|
||||
|
||||
### Session Validation
|
||||
|
||||
- **Session Duration**: 14 days of inactivity (backend setting)
|
||||
- **Access Token**: 15 minutes (can be refreshed)
|
||||
- **Refresh Token**: 7 days (single-use due to rotation)
|
||||
|
||||
If the backend session expires (14 days), token refresh will fail even if the refresh token hasn't expired yet.
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Error Types
|
||||
|
||||
| Error Condition | Response | Frontend Behavior |
|
||||
|----------------|----------|-------------------|
|
||||
| Missing refresh token | 400 Bad Request | Clear session, throw error |
|
||||
| Invalid refresh token | 401 Unauthorized | Clear session, show "Session expired" |
|
||||
| Expired refresh token | 401 Unauthorized | Clear session, show "Session expired" |
|
||||
| Session invalidated | 401 Unauthorized | Clear session, show "Session expired or invalidated" |
|
||||
| Server error | 500 Internal Server Error | Clear session, show generic error |
|
||||
|
||||
### Error Messages
|
||||
|
||||
The frontend maps backend errors to user-friendly messages:
|
||||
|
||||
```javascript
|
||||
// Backend: "invalid or expired refresh token"
|
||||
// Frontend: "Your session has expired. Please log in again to continue."
|
||||
|
||||
// Backend: "session not found or expired"
|
||||
// Frontend: "Session has expired or been invalidated. Please log in again."
|
||||
|
||||
// Backend: Generic error
|
||||
// Frontend: "Unable to refresh your session. Please log in again to continue."
|
||||
```
|
||||
|
||||
## Security Features
|
||||
|
||||
1. **Token Rotation**: Each refresh generates new tokens, invalidating old ones
|
||||
2. **Session Validation**: Refresh validates against active backend session
|
||||
3. **Prevents Token Reuse**: Old tokens become invalid immediately
|
||||
4. **CWE-613 Prevention**: Session validation prevents token use after logout
|
||||
5. **Proactive Refresh**: Reduces window of expired token exposure
|
||||
6. **Duplicate Prevention**: `refreshPromise` tracking prevents race conditions
|
||||
7. **Automatic Cleanup**: Failed refresh automatically clears session
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Manual Token Refresh
|
||||
|
||||
```javascript
|
||||
import { useAuth } from './services/Services';
|
||||
|
||||
function MyComponent() {
|
||||
const { authManager } = useAuth();
|
||||
|
||||
const handleManualRefresh = async () => {
|
||||
try {
|
||||
await authManager.refreshAccessToken();
|
||||
console.log("Token refreshed manually");
|
||||
} catch (error) {
|
||||
console.error("Manual refresh failed:", error);
|
||||
// User will be redirected to login
|
||||
}
|
||||
};
|
||||
|
||||
return <button onClick={handleManualRefresh}>Refresh Session</button>;
|
||||
}
|
||||
```
|
||||
|
||||
### Checking Token Status
|
||||
|
||||
```javascript
|
||||
const { authManager } = useAuth();
|
||||
|
||||
// Check if token needs refresh
|
||||
if (authManager.shouldRefreshToken()) {
|
||||
console.log("Token expires soon");
|
||||
}
|
||||
|
||||
// Check if refresh token is valid
|
||||
const refreshExpiry = authManager.refreshExpiry;
|
||||
if (RefreshTokenService.isRefreshTokenValid(refreshExpiry)) {
|
||||
console.log("Refresh token is still valid");
|
||||
}
|
||||
```
|
||||
|
||||
### Automatic Refresh (Built-in)
|
||||
|
||||
```javascript
|
||||
// Automatic refresh is built into ApiClient
|
||||
// No manual intervention needed
|
||||
|
||||
const { apiClient } = useApi();
|
||||
|
||||
// Token will be automatically refreshed if needed
|
||||
const response = await apiClient.get('/api/v1/some-endpoint');
|
||||
```
|
||||
|
||||
## Testing the Token Refresh Flow
|
||||
|
||||
### 1. Prerequisites
|
||||
|
||||
- Backend running at `http://localhost:8000`
|
||||
- Frontend running at `http://localhost:5173`
|
||||
- User already logged in
|
||||
|
||||
### 2. Test Proactive Refresh
|
||||
|
||||
```javascript
|
||||
// In browser console
|
||||
|
||||
// 1. Login first
|
||||
// (Use the login form)
|
||||
|
||||
// 2. Check token expiry
|
||||
const expiry = localStorage.getItem('maplepress_access_expiry');
|
||||
console.log('Token expires at:', expiry);
|
||||
|
||||
// 3. Manually set expiry to 30 seconds from now (for testing)
|
||||
const newExpiry = new Date(Date.now() + 30000).toISOString();
|
||||
localStorage.setItem('maplepress_access_expiry', newExpiry);
|
||||
|
||||
// 4. Make an API request (will trigger proactive refresh after 30 seconds)
|
||||
// The ApiClient will automatically refresh before the request
|
||||
|
||||
// 5. Check that new token was stored
|
||||
setTimeout(() => {
|
||||
const newToken = localStorage.getItem('maplepress_access_token');
|
||||
console.log('New token:', newToken);
|
||||
}, 31000);
|
||||
```
|
||||
|
||||
### 3. Test Refresh on Initialization
|
||||
|
||||
```bash
|
||||
# 1. Login to the app
|
||||
# 2. Open browser DevTools → Application → Local Storage
|
||||
# 3. Find 'maplepress_access_expiry'
|
||||
# 4. Change the date to yesterday
|
||||
# 5. Refresh the page
|
||||
# 6. Check console logs - should see token refresh attempt
|
||||
```
|
||||
|
||||
### 4. Test 401 Handling
|
||||
|
||||
This requires backend cooperation (backend must return 401):
|
||||
|
||||
```javascript
|
||||
// In browser console after login
|
||||
|
||||
// 1. Make access token invalid
|
||||
localStorage.setItem('maplepress_access_token', 'invalid_token');
|
||||
|
||||
// 2. Make an authenticated API request
|
||||
// The ApiClient will receive 401, attempt refresh, and retry
|
||||
|
||||
// 3. Check console for refresh logs
|
||||
```
|
||||
|
||||
### 5. Test Refresh Failure
|
||||
|
||||
```javascript
|
||||
// In browser console
|
||||
|
||||
// 1. Make refresh token invalid
|
||||
localStorage.setItem('maplepress_refresh_token', 'invalid_refresh');
|
||||
|
||||
// 2. Try to refresh
|
||||
window.maplePressServices.authManager.refreshAccessToken()
|
||||
.catch(error => console.log('Expected error:', error));
|
||||
|
||||
// 3. Check that session was cleared
|
||||
window.maplePressServices.authManager.isAuthenticated() // Should be false
|
||||
```
|
||||
|
||||
### 6. Backend Testing with curl
|
||||
|
||||
```bash
|
||||
# 1. Login and get refresh token
|
||||
curl -X POST http://localhost:8000/api/v1/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "test@example.com",
|
||||
"password": "SecurePass123!"
|
||||
}' | jq -r '.refresh_token'
|
||||
|
||||
# 2. Use refresh token to get new tokens
|
||||
curl -X POST http://localhost:8000/api/v1/refresh \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"refresh_token": "eyJhbGci..."
|
||||
}' | jq
|
||||
|
||||
# Expected: New access_token and refresh_token
|
||||
|
||||
# 3. Try to use old refresh token (should fail)
|
||||
curl -X POST http://localhost:8000/api/v1/refresh \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"refresh_token": "eyJhbGci..."
|
||||
}'
|
||||
|
||||
# Expected: 401 Unauthorized
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Implemented Best Practices
|
||||
|
||||
✅ **Proactive Refresh**: Tokens are refreshed 1 minute before expiry
|
||||
✅ **Automatic Refresh**: Built into ApiClient, no manual intervention needed
|
||||
✅ **Token Rotation**: Both tokens regenerated on refresh
|
||||
✅ **Session Validation**: Backend validates session on each refresh
|
||||
✅ **Duplicate Prevention**: Only one refresh request at a time
|
||||
✅ **Error Handling**: Failed refresh clears session and redirects to login
|
||||
✅ **Secure Storage**: Tokens stored in localStorage (consider upgrading to httpOnly cookies for production)
|
||||
|
||||
### Recommended Enhancements
|
||||
|
||||
- [ ] Move to HTTP-only cookies for token storage (more secure than localStorage)
|
||||
- [ ] Implement refresh token allowlist (backend: store valid refresh tokens)
|
||||
- [ ] Add refresh token fingerprinting (tie token to device)
|
||||
- [ ] Implement sliding sessions (extend session on activity)
|
||||
- [ ] Add token refresh monitoring/metrics
|
||||
- [ ] Implement token refresh failure notifications
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Token refresh fails immediately after login
|
||||
|
||||
**Possible causes:**
|
||||
1. Clock skew between client and server
|
||||
2. Token expiry dates incorrect
|
||||
3. localStorage corruption
|
||||
|
||||
**Solution:**
|
||||
- Clear localStorage and login again
|
||||
- Check browser console for expiry dates
|
||||
- Verify system clock is correct
|
||||
|
||||
### Token refresh succeeds but returns 401 on next request
|
||||
|
||||
**Possible causes:**
|
||||
1. Token not being stored correctly
|
||||
2. ApiClient not using updated token
|
||||
3. Race condition between refresh and request
|
||||
|
||||
**Solution:**
|
||||
- Check localStorage after refresh
|
||||
- Verify `authManager.getAccessToken()` returns new token
|
||||
- Check for duplicate refresh requests in network tab
|
||||
|
||||
### Session cleared unexpectedly
|
||||
|
||||
**Possible causes:**
|
||||
1. Refresh token expired (after 7 days)
|
||||
2. Backend session invalidated (after 14 days inactivity)
|
||||
3. User logged out on another device
|
||||
4. Backend restarted and cleared sessions
|
||||
|
||||
**Solution:**
|
||||
- Check `maplepress_refresh_expiry` in localStorage
|
||||
- Verify backend session still exists
|
||||
- Login again to create new session
|
||||
|
||||
### Infinite refresh loop
|
||||
|
||||
**Possible causes:**
|
||||
1. Token expiry dates set incorrectly
|
||||
2. Backend always returning expired tokens
|
||||
3. `skipTokenRefresh` not working
|
||||
|
||||
**Solution:**
|
||||
- Check for `/api/v1/refresh` endpoint exclusion in ApiClient
|
||||
- Verify token expiry dates in localStorage
|
||||
- Check backend logs for refresh endpoint errors
|
||||
|
||||
## Integration with Dashboard
|
||||
|
||||
The dashboard automatically benefits from token refresh:
|
||||
|
||||
```javascript
|
||||
// Dashboard.jsx
|
||||
useEffect(() => {
|
||||
if (!authManager.isAuthenticated()) {
|
||||
navigate("/login");
|
||||
return;
|
||||
}
|
||||
|
||||
// Token will be automatically refreshed if needed
|
||||
const userData = authManager.getUser();
|
||||
setUser(userData);
|
||||
}, [authManager, navigate]);
|
||||
```
|
||||
|
||||
Any API calls made from the dashboard will automatically trigger token refresh if needed.
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Planned Features
|
||||
|
||||
- [ ] **Background Refresh Timer**: Periodic refresh check every 5 minutes
|
||||
- [ ] **Session Extension Prompt**: Ask user to extend session before refresh token expires
|
||||
- [ ] **Multi-tab Synchronization**: Coordinate token refresh across browser tabs
|
||||
- [ ] **Refresh Analytics**: Track refresh success/failure rates
|
||||
- [ ] **Grace Period Handling**: Handle partial network failures gracefully
|
||||
|
||||
### Security Improvements
|
||||
|
||||
- [ ] **HTTP-only Cookies**: Move from localStorage to secure cookies
|
||||
- [ ] **Token Binding**: Bind tokens to specific devices/browsers
|
||||
- [ ] **Refresh Token Allowlist**: Backend maintains list of valid refresh tokens
|
||||
- [ ] **Token Rotation Detection**: Detect and prevent token replay attacks
|
||||
- [ ] **Session Fingerprinting**: Additional validation beyond token
|
||||
|
||||
## Related Files
|
||||
|
||||
### Created Files
|
||||
```
|
||||
src/services/API/RefreshTokenService.js
|
||||
```
|
||||
|
||||
### Modified Files
|
||||
```
|
||||
src/services/Manager/AuthManager.js
|
||||
src/services/API/ApiClient.js
|
||||
```
|
||||
|
||||
### Backend Reference Files
|
||||
```
|
||||
cloud/maplepress-backend/docs/API.md (lines 230-324)
|
||||
cloud/maplepress-backend/internal/interface/http/dto/gateway/refresh_dto.go
|
||||
cloud/maplepress-backend/internal/interface/http/handler/gateway/refresh_handler.go
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [LOGIN_API.md](./LOGIN_API.md) - Login implementation and token storage
|
||||
- [REGISTRATION_API.md](./REGISTRATION_API.md) - Registration implementation
|
||||
- [FRONTEND_ARCHITECTURE.md](./FRONTEND_ARCHITECTURE.md) - Complete architecture guide
|
||||
- [README.md](./README.md) - Documentation index
|
||||
- [Backend API Documentation](../../../cloud/maplepress-backend/docs/API.md) - Complete API reference
|
||||
|
||||
## Summary
|
||||
|
||||
The token refresh implementation provides:
|
||||
|
||||
1. **Seamless Sessions**: Users stay logged in without interruption
|
||||
2. **Automatic Refresh**: No manual intervention required
|
||||
3. **Proactive Strategy**: Tokens refreshed before expiry
|
||||
4. **Reactive Fallback**: Handles unexpected 401 errors
|
||||
5. **Security**: Token rotation prevents reuse
|
||||
6. **Reliability**: Duplicate prevention and error handling
|
||||
|
||||
The implementation follows backend best practices and integrates seamlessly with the existing three-layer service architecture.
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: October 30, 2024
|
||||
**Frontend Version**: 0.0.0
|
||||
**Documentation Version**: 1.0.0
|
||||
480
web/maplepress-frontend/docs/API/REGISTRATION_API.md
Normal file
480
web/maplepress-frontend/docs/API/REGISTRATION_API.md
Normal file
|
|
@ -0,0 +1,480 @@
|
|||
# Registration API Implementation
|
||||
|
||||
This document describes the complete implementation of the user registration feature for the MaplePress frontend, integrated with the MaplePress backend API.
|
||||
|
||||
## Overview
|
||||
|
||||
The registration feature allows new users to create an account and automatically sets up their tenant (organization) in a single step. Upon successful registration, users receive authentication tokens and are automatically logged in.
|
||||
|
||||
## Backend API Endpoint
|
||||
|
||||
**Endpoint**: `POST /api/v1/register`
|
||||
**Authentication**: None required (public endpoint)
|
||||
**Documentation**: `/cloud/maplepress-backend/docs/API.md` (lines 66-166)
|
||||
|
||||
### Request Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"password": "SecurePassword123!",
|
||||
"confirm_password": "SecurePassword123!",
|
||||
"first_name": "John",
|
||||
"last_name": "Doe",
|
||||
"tenant_name": "Acme Corporation",
|
||||
"timezone": "America/New_York",
|
||||
"agree_terms_of_service": true,
|
||||
"agree_promotions": false,
|
||||
"agree_to_tracking_across_third_party_apps_and_services": false
|
||||
}
|
||||
```
|
||||
|
||||
### Response Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"user_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"user_email": "user@example.com",
|
||||
"user_name": "John Doe",
|
||||
"user_role": "manager",
|
||||
"tenant_id": "650e8400-e29b-41d4-a716-446655440000",
|
||||
"tenant_name": "Acme Corporation",
|
||||
"tenant_slug": "acme-corp",
|
||||
"session_id": "750e8400-e29b-41d4-a716-446655440000",
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"access_expiry": "2024-10-24T12:00:00Z",
|
||||
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"refresh_expiry": "2024-11-24T00:00:00Z",
|
||||
"created_at": "2024-10-24T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Frontend Implementation
|
||||
|
||||
### 1. RegisterService (`src/services/API/RegisterService.js`)
|
||||
|
||||
Handles direct communication with the backend registration API.
|
||||
|
||||
**Key Features:**
|
||||
- Request validation (required fields, terms agreement)
|
||||
- Request body formatting (snake_case for backend)
|
||||
- Response transformation (camelCase for frontend)
|
||||
- User-friendly error message mapping
|
||||
- Tenant slug validation and generation utilities
|
||||
|
||||
**Methods:**
|
||||
- `register(data)` - Main registration method
|
||||
- `validateTenantSlug(slug)` - Validate slug format
|
||||
- `generateTenantSlug(name)` - Auto-generate slug from name
|
||||
|
||||
**Usage:**
|
||||
```javascript
|
||||
import RegisterService from './services/API/RegisterService';
|
||||
|
||||
const response = await RegisterService.register({
|
||||
email: "user@example.com",
|
||||
password: "SecurePassword123!",
|
||||
confirmPassword: "SecurePassword123!",
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
tenant_name: "Acme Corp",
|
||||
agree_terms_of_service: true,
|
||||
});
|
||||
```
|
||||
|
||||
### 2. AuthManager (`src/services/Manager/AuthManager.js`)
|
||||
|
||||
Manages authentication state, token storage, and session lifecycle.
|
||||
|
||||
**Key Features:**
|
||||
- Token management (access & refresh tokens)
|
||||
- LocalStorage persistence
|
||||
- Token expiry checking
|
||||
- User and tenant data storage
|
||||
- Session initialization and cleanup
|
||||
|
||||
**New Methods:**
|
||||
- `register(registrationData)` - Register and store auth data
|
||||
- `storeAuthData(authResponse)` - Store tokens and user data
|
||||
- `clearSession()` - Clear all auth data
|
||||
- `isTokenExpired(expiry)` - Check token expiry
|
||||
- `getTenant()` - Get current tenant information
|
||||
|
||||
**Storage Keys:**
|
||||
```javascript
|
||||
maplepress_access_token
|
||||
maplepress_refresh_token
|
||||
maplepress_access_expiry
|
||||
maplepress_refresh_expiry
|
||||
maplepress_user
|
||||
maplepress_tenant
|
||||
maplepress_session_id
|
||||
```
|
||||
|
||||
### 3. Register Page (`src/pages/Auth/Register.jsx`)
|
||||
|
||||
Complete registration form with all required fields.
|
||||
|
||||
**Form Sections:**
|
||||
|
||||
1. **Personal Information**
|
||||
- First Name (required)
|
||||
- Last Name (required)
|
||||
- Email Address (required)
|
||||
- Password (required, min 8 chars)
|
||||
- Confirm Password (required)
|
||||
|
||||
2. **Organization Information**
|
||||
- Organization Name (required, slug auto-generated by backend)
|
||||
|
||||
3. **Terms and Consent**
|
||||
- Terms of Service agreement (required)
|
||||
- Promotional emails (optional)
|
||||
|
||||
**Features:**
|
||||
- Password match validation (backend-validated)
|
||||
- Terms of service requirement
|
||||
- Automatic timezone detection
|
||||
- Loading state during submission
|
||||
- RFC 9457 error message display with field-specific errors
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
User fills form
|
||||
↓
|
||||
Register.jsx validates data
|
||||
↓
|
||||
AuthManager.register(data)
|
||||
↓
|
||||
RegisterService.register(data)
|
||||
↓
|
||||
HTTP POST to /api/v1/register
|
||||
↓
|
||||
Backend validates and creates user + tenant
|
||||
↓
|
||||
Backend returns tokens and user data
|
||||
↓
|
||||
RegisterService transforms response
|
||||
↓
|
||||
AuthManager stores tokens in localStorage
|
||||
↓
|
||||
User redirected to /dashboard
|
||||
```
|
||||
|
||||
## Validation Rules
|
||||
|
||||
### Frontend Validation
|
||||
|
||||
1. **Email**: Required, valid email format
|
||||
2. **Password**:
|
||||
- Required
|
||||
- Minimum 8 characters
|
||||
3. **Confirm Password**:
|
||||
- Required
|
||||
- Must match password (validated by backend)
|
||||
4. **Name**: Required
|
||||
5. **Tenant Name**: Required
|
||||
6. **Tenant Slug**:
|
||||
- Required
|
||||
- Only lowercase letters, numbers, and hyphens
|
||||
- Validated by regex: `/^[a-z0-9-]+$/`
|
||||
7. **Terms of Service**: Must be checked
|
||||
|
||||
### Backend Validation
|
||||
|
||||
Backend performs additional validation:
|
||||
- Email normalization and format validation
|
||||
- Password strength requirements (uppercase, lowercase, number, special char)
|
||||
- Password confirmation matching
|
||||
- Tenant slug uniqueness
|
||||
- Email uniqueness
|
||||
- No HTML in text fields
|
||||
- Input sanitization
|
||||
|
||||
## Error Handling
|
||||
|
||||
### RFC 9457 (Problem Details for HTTP APIs)
|
||||
|
||||
The backend returns validation errors in **RFC 9457** format, which provides a structured, machine-readable error response.
|
||||
|
||||
**Error Response Structure**:
|
||||
```json
|
||||
{
|
||||
"type": "about:blank",
|
||||
"title": "Validation Error",
|
||||
"status": 400,
|
||||
"detail": "One or more validation errors occurred",
|
||||
"errors": {
|
||||
"email": ["Invalid email format"],
|
||||
"password": ["Field is required", "Password must be at least 8 characters"],
|
||||
"confirm_password": ["Field is required", "Passwords do not match"],
|
||||
"first_name": ["Field is required"],
|
||||
"last_name": ["Field is required"],
|
||||
"tenant_name": ["Field is required"],
|
||||
"agree_terms_of_service": ["Must agree to terms of service"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Content-Type**: `application/problem+json`
|
||||
|
||||
**Key Fields**:
|
||||
- `type`: URI reference identifying the problem type (uses `about:blank` for generic errors)
|
||||
- `title`: Short, human-readable summary of the error type
|
||||
- `status`: HTTP status code
|
||||
- `detail`: Human-readable explanation of the error
|
||||
- `errors`: Dictionary/map of field-specific validation errors (extension field)
|
||||
|
||||
### Common Validation Errors
|
||||
|
||||
| Field | Error Message |
|
||||
|-------|--------------|
|
||||
| email | `email: invalid email format` |
|
||||
| email | `email: email is required` |
|
||||
| password | `password: password is required` |
|
||||
| password | `password: password must be at least 8 characters` |
|
||||
| password | `password: password must contain at least one uppercase letter (A-Z)` |
|
||||
| password | `password: password must contain at least one lowercase letter (a-z)` |
|
||||
| password | `password: password must contain at least one number (0-9)` |
|
||||
| password | `password: password must contain at least one special character` |
|
||||
| confirm_password | `confirm_password: field is required` |
|
||||
| confirm_password | `confirm_password: passwords do not match` |
|
||||
| first_name | `first_name: field is required` |
|
||||
| first_name | `first_name: first_name must be between 1 and 100 characters` |
|
||||
| last_name | `last_name: field is required` |
|
||||
| last_name | `last_name: last_name must be between 1 and 100 characters` |
|
||||
| tenant_name | `tenant_name: tenant_name is required` |
|
||||
| tenant_name | `tenant_name: tenant_name must be between 1 and 100 characters` |
|
||||
| agree_terms_of_service | `agree_terms_of_service: must agree to terms of service` |
|
||||
|
||||
### Frontend Error Parsing
|
||||
|
||||
The Register page component (`src/pages/Auth/Register.jsx`) includes a `parseBackendError()` function that:
|
||||
1. Checks if the error object contains RFC 9457 `validationErrors` structure
|
||||
2. Iterates through the errors dictionary and maps each field to its error messages
|
||||
3. Joins multiple error messages for the same field with semicolons
|
||||
4. Displays field-specific errors with red borders and inline messages
|
||||
5. Shows an error summary box at the top with all errors listed
|
||||
|
||||
**Error Parsing Implementation**:
|
||||
```javascript
|
||||
const parseBackendError = (error) => {
|
||||
const fieldErrors = {};
|
||||
let generalError = null;
|
||||
|
||||
// Check if error has RFC 9457 validation errors structure
|
||||
if (error.validationErrors && typeof error.validationErrors === 'object') {
|
||||
// Process each field's errors
|
||||
Object.entries(error.validationErrors).forEach(([fieldName, errorMessages]) => {
|
||||
if (Array.isArray(errorMessages) && errorMessages.length > 0) {
|
||||
// Join multiple error messages for the same field
|
||||
fieldErrors[fieldName] = errorMessages.join('; ');
|
||||
}
|
||||
});
|
||||
|
||||
// Use the detail as general error if available
|
||||
if (error.message) {
|
||||
generalError = error.message;
|
||||
}
|
||||
} else {
|
||||
// Fallback for non-RFC 9457 errors
|
||||
generalError = error.message || "An error occurred. Please try again.";
|
||||
}
|
||||
|
||||
return { fieldErrors, generalError };
|
||||
};
|
||||
```
|
||||
|
||||
**Example Error Display**:
|
||||
|
||||
When submitting an empty form, the user sees:
|
||||
- **Error Summary Box**: Lists all validation errors at the top of the form with the general message "One or more validation errors occurred"
|
||||
- **Highlighted Fields**: Red border on email, password, first_name, last_name, tenant_name, etc.
|
||||
- **Inline Messages**: Error text displayed below each affected field (e.g., "Field is required")
|
||||
|
||||
### ApiClient Error Handling
|
||||
|
||||
The `ApiClient` (`src/services/API/ApiClient.js`) automatically parses RFC 9457 error responses and attaches the validation errors to the error object:
|
||||
|
||||
```javascript
|
||||
// Create error with RFC 9457 data if available
|
||||
const error = new Error(
|
||||
errorData.detail || errorData.message || `HTTP ${response.status}: ${response.statusText}`
|
||||
);
|
||||
|
||||
// Attach RFC 9457 fields to error for parsing
|
||||
if (errorData.errors) {
|
||||
error.validationErrors = errorData.errors; // RFC 9457 validation errors
|
||||
}
|
||||
if (errorData.title) {
|
||||
error.title = errorData.title;
|
||||
}
|
||||
if (errorData.status) {
|
||||
error.status = errorData.status;
|
||||
}
|
||||
|
||||
throw error;
|
||||
```
|
||||
|
||||
**Note**: The frontend **does not perform field validation** - all validation, including password matching, is handled by the backend. The form submits whatever the user enters, and the backend returns comprehensive validation errors in RFC 9457 format.
|
||||
|
||||
## Security Features
|
||||
|
||||
1. **Token Storage**: Tokens stored in localStorage (client-side only)
|
||||
2. **Token Expiry**: Automatic expiry checking on initialization
|
||||
3. **Password Validation**: Client and server-side validation
|
||||
4. **HTTPS Required**: Production should use HTTPS
|
||||
5. **Terms Agreement**: Required before account creation
|
||||
6. **Input Sanitization**: Backend sanitizes all inputs
|
||||
|
||||
## Testing the Registration Flow
|
||||
|
||||
### 1. Start Backend
|
||||
|
||||
```bash
|
||||
cd cloud/mapleopentech-backend
|
||||
task dev
|
||||
```
|
||||
|
||||
### 2. Start Frontend
|
||||
|
||||
```bash
|
||||
cd web/maplepress-frontend
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 3. Navigate to Registration
|
||||
|
||||
Open browser to: `http://localhost:5173/register`
|
||||
|
||||
### 4. Fill Form
|
||||
|
||||
- **First Name**: John (required)
|
||||
- **Last Name**: Doe (required)
|
||||
- **Email**: test@example.com
|
||||
- **Password**: SecurePass123!
|
||||
- **Confirm Password**: SecurePass123!
|
||||
- **Organization Name**: Test Corp (slug auto-generated by backend)
|
||||
- **Terms of Service**: ✓ (checked)
|
||||
|
||||
### 5. Submit
|
||||
|
||||
Click "Create Account" button
|
||||
|
||||
### 6. Expected Result
|
||||
|
||||
- Loading state appears
|
||||
- Request sent to backend
|
||||
- Tokens stored in localStorage
|
||||
- User redirected to `/dashboard`
|
||||
- Dashboard shows user information
|
||||
|
||||
### 7. Verify in Browser Console
|
||||
|
||||
```javascript
|
||||
// Check stored tokens
|
||||
localStorage.getItem('maplepress_access_token')
|
||||
localStorage.getItem('maplepress_user')
|
||||
localStorage.getItem('maplepress_tenant')
|
||||
|
||||
// Check service instance
|
||||
window.maplePressServices.authManager.isAuthenticated()
|
||||
window.maplePressServices.authManager.getUser()
|
||||
window.maplePressServices.authManager.getTenant()
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Create `.env` file:
|
||||
|
||||
```bash
|
||||
# Backend API URL
|
||||
VITE_API_BASE_URL=http://localhost:8000
|
||||
```
|
||||
|
||||
### Production Configuration
|
||||
|
||||
For production:
|
||||
```bash
|
||||
VITE_API_BASE_URL=https://api.maplepress.io
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Planned Features
|
||||
|
||||
- [ ] Email verification flow
|
||||
- [ ] Password strength indicator
|
||||
- [ ] Captcha integration
|
||||
- [ ] Social authentication (Google, GitHub)
|
||||
- [ ] Organization logo upload
|
||||
- [ ] Custom domain support
|
||||
- [ ] Invitation codes/referrals
|
||||
- [ ] Account recovery flow
|
||||
|
||||
### Security Improvements
|
||||
|
||||
- [ ] CSRF token implementation
|
||||
- [ ] Rate limiting on frontend
|
||||
- [ ] Session fingerprinting
|
||||
- [ ] Two-factor authentication
|
||||
- [ ] Password breach checking
|
||||
- [ ] Secure token storage (HTTP-only cookies)
|
||||
|
||||
## File Reference
|
||||
|
||||
### Created Files
|
||||
|
||||
```
|
||||
src/services/API/RegisterService.js
|
||||
```
|
||||
|
||||
### Modified Files
|
||||
|
||||
```
|
||||
src/services/Manager/AuthManager.js
|
||||
src/pages/Auth/Register.jsx
|
||||
```
|
||||
|
||||
### Backend Reference Files
|
||||
|
||||
```
|
||||
cloud/maplepress-backend/docs/API.md
|
||||
cloud/maplepress-backend/internal/interface/http/dto/gateway/register_dto.go
|
||||
cloud/maplepress-backend/internal/interface/http/handler/gateway/register_handler.go
|
||||
```
|
||||
|
||||
## API Client Configuration
|
||||
|
||||
The `ApiClient` automatically handles:
|
||||
- JSON content-type headers
|
||||
- Request/response transformation
|
||||
- Error parsing
|
||||
- Authentication headers (for protected endpoints)
|
||||
|
||||
### Base Configuration
|
||||
|
||||
```javascript
|
||||
API_CONFIG = {
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || "http://localhost:8000",
|
||||
timeout: 30000,
|
||||
}
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
1. Check backend logs: `docker logs mapleopentech_backend`
|
||||
2. Check frontend console for errors
|
||||
3. Verify backend is running on port 8000
|
||||
4. Verify frontend environment variables
|
||||
5. Test backend endpoint directly with curl (see API.md)
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [ARCHITECTURE.md](./ARCHITECTURE.md) - Frontend architecture overview
|
||||
- [README.md](./README.md) - Getting started guide
|
||||
- [Backend API Documentation](../../cloud/maplepress-backend/docs/API.md) - Complete API reference
|
||||
1016
web/maplepress-frontend/docs/API/SITE_API.md
Normal file
1016
web/maplepress-frontend/docs/API/SITE_API.md
Normal file
File diff suppressed because it is too large
Load diff
792
web/maplepress-frontend/docs/API/TENANT_API.md
Normal file
792
web/maplepress-frontend/docs/API/TENANT_API.md
Normal file
|
|
@ -0,0 +1,792 @@
|
|||
# Tenant API Implementation (Tenant Management)
|
||||
|
||||
This document describes the implementation of the Tenant Management API endpoints for the MaplePress frontend, integrated with the MaplePress backend API.
|
||||
|
||||
## Overview
|
||||
|
||||
The Tenant API endpoints manage tenants (organizations) in the multi-tenant system. Each tenant represents an organization with its own users, resources, and isolated data. Tenants are identified by both UUID and a URL-friendly slug.
|
||||
|
||||
## Backend API Endpoints
|
||||
|
||||
### Create Tenant
|
||||
**Endpoint**: `POST /api/v1/tenants`
|
||||
**Authentication**: Required (JWT token)
|
||||
|
||||
### Get Tenant by ID
|
||||
**Endpoint**: `GET /api/v1/tenants/{id}`
|
||||
**Authentication**: Required (JWT token)
|
||||
|
||||
### Get Tenant by Slug
|
||||
**Endpoint**: `GET /api/v1/tenants/slug/{slug}`
|
||||
**Authentication**: Required (JWT token)
|
||||
|
||||
**Documentation**: `/cloud/maplepress-backend/docs/API.md` (lines 416-558)
|
||||
|
||||
## Request/Response Structures
|
||||
|
||||
### Create Tenant Request
|
||||
```json
|
||||
{
|
||||
"name": "TechStart Inc",
|
||||
"slug": "techstart"
|
||||
}
|
||||
```
|
||||
|
||||
### Tenant Response
|
||||
```json
|
||||
{
|
||||
"id": "850e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "TechStart Inc",
|
||||
"slug": "techstart",
|
||||
"status": "active",
|
||||
"created_at": "2024-10-24T00:00:00Z",
|
||||
"updated_at": "2024-10-24T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Frontend Implementation
|
||||
|
||||
### TenantService (`src/services/API/TenantService.js`)
|
||||
|
||||
Handles all tenant management operations with the backend API.
|
||||
|
||||
**Key Features:**
|
||||
- Create new tenants
|
||||
- Retrieve tenant by ID or slug
|
||||
- Client-side validation (name and slug)
|
||||
- Slug generation helper
|
||||
- Response transformation (snake_case to camelCase)
|
||||
- User-friendly error message mapping
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `createTenant(tenantData)`
|
||||
Create a new tenant (organization).
|
||||
|
||||
```javascript
|
||||
import TenantService from './services/API/TenantService';
|
||||
|
||||
const tenant = await TenantService.createTenant({
|
||||
name: "TechStart Inc",
|
||||
slug: "techstart"
|
||||
});
|
||||
|
||||
console.log(tenant);
|
||||
// Output:
|
||||
// {
|
||||
// id: "850e8400-...",
|
||||
// name: "TechStart Inc",
|
||||
// slug: "techstart",
|
||||
// status: "active",
|
||||
// createdAt: Date object
|
||||
// }
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `tenantData.name` (string, required): Tenant/organization name
|
||||
- `tenantData.slug` (string, required): URL-friendly identifier (lowercase, hyphens only)
|
||||
|
||||
**Returns:**
|
||||
```javascript
|
||||
{
|
||||
id: string, // Tenant ID (UUID)
|
||||
name: string, // Tenant name
|
||||
slug: string, // Tenant slug
|
||||
status: string, // Tenant status (e.g., "active")
|
||||
createdAt: Date // Creation timestamp
|
||||
}
|
||||
```
|
||||
|
||||
**Throws:**
|
||||
- "Tenant data is required" - If tenantData is missing
|
||||
- "Tenant name is required" - If name is missing
|
||||
- "Tenant slug is required" - If slug is missing
|
||||
- "Tenant slug must contain only lowercase letters, numbers, and hyphens" - Invalid slug format
|
||||
- "Tenant slug already exists. Please choose a different slug." - Slug conflict (409)
|
||||
- "Authentication required. Please log in to continue." - Missing/invalid token
|
||||
|
||||
#### `getTenantById(tenantId)`
|
||||
Retrieve tenant information by ID.
|
||||
|
||||
```javascript
|
||||
const tenant = await TenantService.getTenantById("850e8400-...");
|
||||
console.log(tenant.name); // "TechStart Inc"
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `tenantId` (string, required): Tenant ID (UUID format)
|
||||
|
||||
**Returns:**
|
||||
```javascript
|
||||
{
|
||||
id: string,
|
||||
name: string,
|
||||
slug: string,
|
||||
status: string,
|
||||
createdAt: Date,
|
||||
updatedAt: Date
|
||||
}
|
||||
```
|
||||
|
||||
**Throws:**
|
||||
- "Tenant ID is required" - If ID is missing
|
||||
- "Invalid tenant ID format" - If ID is not a valid UUID
|
||||
- "Tenant not found." - If tenant doesn't exist (404)
|
||||
- "Authentication required. Please log in to continue." - Missing/invalid token
|
||||
|
||||
#### `getTenantBySlug(slug)`
|
||||
Retrieve tenant information by slug.
|
||||
|
||||
```javascript
|
||||
const tenant = await TenantService.getTenantBySlug("techstart");
|
||||
console.log(tenant.id); // "850e8400-..."
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `slug` (string, required): Tenant slug
|
||||
|
||||
**Returns:**
|
||||
```javascript
|
||||
{
|
||||
id: string,
|
||||
name: string,
|
||||
slug: string,
|
||||
status: string,
|
||||
createdAt: Date,
|
||||
updatedAt: Date
|
||||
}
|
||||
```
|
||||
|
||||
**Throws:**
|
||||
- "Tenant slug is required" - If slug is missing
|
||||
- "Tenant slug cannot be empty" - If slug is empty after trimming
|
||||
- "Tenant not found." - If tenant doesn't exist (404)
|
||||
- "Authentication required. Please log in to continue." - Missing/invalid token
|
||||
|
||||
#### `generateSlug(name)`
|
||||
Generate a URL-friendly slug from a tenant name.
|
||||
|
||||
```javascript
|
||||
const slug = TenantService.generateSlug("TechStart Inc!");
|
||||
console.log(slug); // "techstart-inc"
|
||||
|
||||
const slug2 = TenantService.generateSlug("My Company");
|
||||
console.log(slug2); // "my-company"
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `name` (string): Tenant name
|
||||
|
||||
**Returns:** `string` - Generated slug (lowercase, hyphens, alphanumeric only)
|
||||
|
||||
**Transformation Rules:**
|
||||
- Converts to lowercase
|
||||
- Replaces spaces with hyphens
|
||||
- Removes special characters
|
||||
- Removes consecutive hyphens
|
||||
- Removes leading/trailing hyphens
|
||||
|
||||
#### `validateSlug(slug)`
|
||||
Validate a tenant slug format.
|
||||
|
||||
```javascript
|
||||
const result = TenantService.validateSlug("techstart");
|
||||
console.log(result); // { valid: true, error: null }
|
||||
|
||||
const invalid = TenantService.validateSlug("Tech Start!");
|
||||
console.log(invalid); // { valid: false, error: "Slug must contain..." }
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `slug` (string): Slug to validate
|
||||
|
||||
**Returns:**
|
||||
```javascript
|
||||
{
|
||||
valid: boolean, // true if slug is valid
|
||||
error: string|null // Error message if invalid, null if valid
|
||||
}
|
||||
```
|
||||
|
||||
**Validation Rules:**
|
||||
- Required (non-empty)
|
||||
- Length: 2-50 characters
|
||||
- Format: lowercase letters, numbers, hyphens only
|
||||
- Cannot start or end with hyphen
|
||||
- Cannot contain consecutive hyphens
|
||||
|
||||
#### `validateName(name)`
|
||||
Validate a tenant name.
|
||||
|
||||
```javascript
|
||||
const result = TenantService.validateName("TechStart Inc");
|
||||
console.log(result); // { valid: true, error: null }
|
||||
|
||||
const invalid = TenantService.validateName("A");
|
||||
console.log(invalid); // { valid: false, error: "Name must be at least 2 characters" }
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `name` (string): Name to validate
|
||||
|
||||
**Returns:**
|
||||
```javascript
|
||||
{
|
||||
valid: boolean,
|
||||
error: string|null
|
||||
}
|
||||
```
|
||||
|
||||
**Validation Rules:**
|
||||
- Required (non-empty)
|
||||
- Length: 2-100 characters
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Create Tenant Flow
|
||||
```
|
||||
User provides tenant name and slug
|
||||
↓
|
||||
TenantService.createTenant()
|
||||
↓
|
||||
Validate name and slug (client-side)
|
||||
↓
|
||||
ApiClient.post() with JWT token
|
||||
↓
|
||||
Token automatically refreshed if needed
|
||||
↓
|
||||
POST /api/v1/tenants
|
||||
↓
|
||||
Backend validates (slug uniqueness, format)
|
||||
↓
|
||||
Backend creates tenant in database
|
||||
↓
|
||||
Backend returns tenant data
|
||||
↓
|
||||
TenantService transforms response
|
||||
↓
|
||||
Component receives tenant data
|
||||
```
|
||||
|
||||
### Get Tenant Flow
|
||||
```
|
||||
User requests tenant (by ID or slug)
|
||||
↓
|
||||
TenantService.getTenantById() or getTenantBySlug()
|
||||
↓
|
||||
Validate input format
|
||||
↓
|
||||
ApiClient.get() with JWT token
|
||||
↓
|
||||
Token automatically refreshed if needed
|
||||
↓
|
||||
GET /api/v1/tenants/{id} or /api/v1/tenants/slug/{slug}
|
||||
↓
|
||||
Backend retrieves tenant from database
|
||||
↓
|
||||
Backend returns tenant data
|
||||
↓
|
||||
TenantService transforms response
|
||||
↓
|
||||
Component receives tenant data
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Error Types
|
||||
|
||||
| Error Condition | Response | Frontend Behavior |
|
||||
|----------------|----------|-------------------|
|
||||
| Missing authentication | 401 Unauthorized | "Authentication required. Please log in to continue." |
|
||||
| Invalid tenant data | 400 Bad Request | Specific validation error |
|
||||
| Slug already exists | 409 Conflict | "Tenant slug already exists. Please choose a different slug." |
|
||||
| Tenant not found | 404 Not Found | "Tenant not found." |
|
||||
| Server error | 500 Internal Server Error | Generic error message |
|
||||
|
||||
### Error Message Mapping
|
||||
|
||||
```javascript
|
||||
// Backend error → Frontend error
|
||||
"unauthorized" → "Authentication required. Please log in to continue."
|
||||
"conflict" / "already exists" → "Tenant slug already exists. Please choose a different slug."
|
||||
"not found" → "Tenant not found."
|
||||
"slug" → "Invalid tenant slug. Must contain only lowercase letters, numbers, and hyphens."
|
||||
"name" → "Invalid tenant name provided."
|
||||
```
|
||||
|
||||
## Validation Rules
|
||||
|
||||
### Tenant Name
|
||||
- **Required**: Cannot be empty
|
||||
- **Length**: 2-100 characters
|
||||
- **Format**: Any printable characters allowed
|
||||
|
||||
### Tenant Slug
|
||||
- **Required**: Cannot be empty
|
||||
- **Length**: 2-50 characters
|
||||
- **Format**: Lowercase letters, numbers, hyphens only
|
||||
- **Pattern**: `^[a-z0-9-]+$`
|
||||
- **Restrictions**:
|
||||
- Cannot start or end with hyphen
|
||||
- Cannot contain consecutive hyphens
|
||||
- Must be URL-safe
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Create a New Tenant
|
||||
|
||||
```javascript
|
||||
import React, { useState } from 'react';
|
||||
import TenantService from '../../services/API/TenantService';
|
||||
|
||||
function CreateTenantForm() {
|
||||
const [name, setName] = useState('');
|
||||
const [slug, setSlug] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Auto-generate slug from name
|
||||
const handleNameChange = (e) => {
|
||||
const newName = e.target.value;
|
||||
setName(newName);
|
||||
|
||||
// Generate slug automatically
|
||||
const generatedSlug = TenantService.generateSlug(newName);
|
||||
setSlug(generatedSlug);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
|
||||
// Validate before sending
|
||||
const nameValidation = TenantService.validateName(name);
|
||||
if (!nameValidation.valid) {
|
||||
setError(nameValidation.error);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const slugValidation = TenantService.validateSlug(slug);
|
||||
if (!slugValidation.valid) {
|
||||
setError(slugValidation.error);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const tenant = await TenantService.createTenant({ name, slug });
|
||||
console.log("Tenant created:", tenant);
|
||||
// Redirect or show success message
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label>Tenant Name:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={handleNameChange}
|
||||
placeholder="TechStart Inc"
|
||||
maxLength={100}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Tenant Slug:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={slug}
|
||||
onChange={(e) => setSlug(e.target.value.toLowerCase())}
|
||||
placeholder="techstart"
|
||||
maxLength={50}
|
||||
/>
|
||||
<small>URL: /tenants/{slug}</small>
|
||||
</div>
|
||||
|
||||
{error && <p className="error">{error}</p>}
|
||||
|
||||
<button type="submit" disabled={loading}>
|
||||
{loading ? 'Creating...' : 'Create Tenant'}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateTenantForm;
|
||||
```
|
||||
|
||||
### Display Tenant Information
|
||||
|
||||
```javascript
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import TenantService from '../../services/API/TenantService';
|
||||
|
||||
function TenantProfile({ tenantId }) {
|
||||
const [tenant, setTenant] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTenant = async () => {
|
||||
try {
|
||||
const data = await TenantService.getTenantById(tenantId);
|
||||
setTenant(data);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTenant();
|
||||
}, [tenantId]);
|
||||
|
||||
if (loading) return <div>Loading tenant...</div>;
|
||||
if (error) return <div>Error: {error}</div>;
|
||||
if (!tenant) return null;
|
||||
|
||||
return (
|
||||
<div className="tenant-profile">
|
||||
<h2>{tenant.name}</h2>
|
||||
<p>Slug: {tenant.slug}</p>
|
||||
<p>Status: {tenant.status}</p>
|
||||
<p>Created: {tenant.createdAt.toLocaleDateString()}</p>
|
||||
<p>Updated: {tenant.updatedAt.toLocaleDateString()}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TenantProfile;
|
||||
```
|
||||
|
||||
### Search Tenant by Slug
|
||||
|
||||
```javascript
|
||||
import TenantService from '../../services/API/TenantService';
|
||||
|
||||
function TenantLookup() {
|
||||
const [slug, setSlug] = useState('');
|
||||
const [tenant, setTenant] = useState(null);
|
||||
|
||||
const handleSearch = async () => {
|
||||
try {
|
||||
const data = await TenantService.getTenantBySlug(slug);
|
||||
setTenant(data);
|
||||
} catch (error) {
|
||||
console.error("Tenant not found:", error.message);
|
||||
setTenant(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
value={slug}
|
||||
onChange={(e) => setSlug(e.target.value)}
|
||||
placeholder="Enter tenant slug"
|
||||
/>
|
||||
<button onClick={handleSearch}>Search</button>
|
||||
|
||||
{tenant && (
|
||||
<div>
|
||||
<h3>{tenant.name}</h3>
|
||||
<p>ID: {tenant.id}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Validate Slug in Real-Time
|
||||
|
||||
```javascript
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import TenantService from '../../services/API/TenantService';
|
||||
|
||||
function SlugInput({ value, onChange }) {
|
||||
const [validation, setValidation] = useState({ valid: true, error: null });
|
||||
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
const result = TenantService.validateSlug(value);
|
||||
setValidation(result);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value.toLowerCase())}
|
||||
className={validation.valid ? '' : 'error'}
|
||||
/>
|
||||
{!validation.valid && (
|
||||
<span className="error-message">{validation.error}</span>
|
||||
)}
|
||||
{validation.valid && value && (
|
||||
<span className="success-message">✓ Valid slug</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Testing the Tenant API
|
||||
|
||||
### 1. Prerequisites
|
||||
|
||||
- Backend running at `http://localhost:8000`
|
||||
- Frontend running at `http://localhost:5173`
|
||||
- User logged in (valid JWT token)
|
||||
|
||||
### 2. Test Create Tenant
|
||||
|
||||
```javascript
|
||||
// In browser console after login
|
||||
|
||||
import TenantService from './services/API/TenantService';
|
||||
|
||||
// Generate slug from name
|
||||
const slug = TenantService.generateSlug("My New Company");
|
||||
console.log("Generated slug:", slug); // "my-new-company"
|
||||
|
||||
// Validate before creating
|
||||
const validation = TenantService.validateSlug(slug);
|
||||
console.log("Slug valid:", validation.valid); // true
|
||||
|
||||
// Create tenant
|
||||
const tenant = await TenantService.createTenant({
|
||||
name: "My New Company",
|
||||
slug: slug
|
||||
});
|
||||
|
||||
console.log("Created tenant:", tenant);
|
||||
```
|
||||
|
||||
### 3. Test Get Tenant by ID
|
||||
|
||||
```javascript
|
||||
// Using tenant ID from creation
|
||||
const tenant = await TenantService.getTenantById("850e8400-...");
|
||||
console.log("Tenant by ID:", tenant);
|
||||
```
|
||||
|
||||
### 4. Test Get Tenant by Slug
|
||||
|
||||
```javascript
|
||||
const tenant = await TenantService.getTenantBySlug("my-new-company");
|
||||
console.log("Tenant by slug:", tenant);
|
||||
```
|
||||
|
||||
### 5. Test with curl
|
||||
|
||||
```bash
|
||||
# 1. Login and get access token
|
||||
ACCESS_TOKEN=$(curl -X POST http://localhost:8000/api/v1/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "test@example.com",
|
||||
"password": "SecurePass123!"
|
||||
}' | jq -r '.access_token')
|
||||
|
||||
# 2. Create tenant
|
||||
curl -X POST http://localhost:8000/api/v1/tenants \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: JWT $ACCESS_TOKEN" \
|
||||
-d '{
|
||||
"name": "Test Company",
|
||||
"slug": "test-company"
|
||||
}' | jq
|
||||
|
||||
# 3. Get tenant by ID (use ID from creation response)
|
||||
TENANT_ID="850e8400-e29b-41d4-a716-446655440000"
|
||||
curl -X GET "http://localhost:8000/api/v1/tenants/$TENANT_ID" \
|
||||
-H "Authorization: JWT $ACCESS_TOKEN" | jq
|
||||
|
||||
# 4. Get tenant by slug
|
||||
curl -X GET "http://localhost:8000/api/v1/tenants/slug/test-company" \
|
||||
-H "Authorization: JWT $ACCESS_TOKEN" | jq
|
||||
|
||||
# 5. Test slug conflict (should fail with 409)
|
||||
curl -X POST http://localhost:8000/api/v1/tenants \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: JWT $ACCESS_TOKEN" \
|
||||
-d '{
|
||||
"name": "Another Company",
|
||||
"slug": "test-company"
|
||||
}' | jq
|
||||
# Expected: 409 Conflict
|
||||
```
|
||||
|
||||
### 6. Test Slug Generation
|
||||
|
||||
```javascript
|
||||
// Test various slug generation scenarios
|
||||
|
||||
const tests = [
|
||||
{ input: "TechStart Inc", expected: "techstart-inc" },
|
||||
{ input: "My Company!", expected: "my-company" },
|
||||
{ input: " Spaces Everywhere ", expected: "spaces-everywhere" },
|
||||
{ input: "Multiple---Hyphens", expected: "multiple-hyphens" },
|
||||
{ input: "123 Numbers", expected: "123-numbers" },
|
||||
];
|
||||
|
||||
tests.forEach(({ input, expected }) => {
|
||||
const result = TenantService.generateSlug(input);
|
||||
console.log(`Input: "${input}"`);
|
||||
console.log(`Expected: "${expected}"`);
|
||||
console.log(`Got: "${result}"`);
|
||||
console.log(`✓ Pass: ${result === expected}\n`);
|
||||
});
|
||||
```
|
||||
|
||||
## Integration with AuthManager
|
||||
|
||||
The tenant created during registration is stored in AuthManager:
|
||||
|
||||
```javascript
|
||||
import { useAuth } from './services/Services';
|
||||
import TenantService from './services/API/TenantService';
|
||||
|
||||
const { authManager } = useAuth();
|
||||
|
||||
// Get stored tenant from AuthManager
|
||||
const storedTenant = authManager.getTenant();
|
||||
console.log("Stored tenant:", storedTenant);
|
||||
// { id: "...", name: "...", slug: "..." }
|
||||
|
||||
// Fetch fresh tenant data from API
|
||||
const freshTenant = await TenantService.getTenantById(storedTenant.id);
|
||||
console.log("Fresh tenant:", freshTenant);
|
||||
|
||||
// Compare
|
||||
if (storedTenant.name !== freshTenant.name) {
|
||||
console.warn("Tenant data has changed");
|
||||
}
|
||||
```
|
||||
|
||||
## Multi-Tenant Context
|
||||
|
||||
Tenants provide isolation for multi-tenant applications:
|
||||
|
||||
```javascript
|
||||
// Get current user's tenant
|
||||
import MeService from './services/API/MeService';
|
||||
import TenantService from './services/API/TenantService';
|
||||
|
||||
const profile = await MeService.getMe();
|
||||
const tenantId = profile.tenantId;
|
||||
|
||||
// Get full tenant details
|
||||
const tenant = await TenantService.getTenantById(tenantId);
|
||||
console.log("Current tenant:", tenant.name);
|
||||
|
||||
// Use tenant context for operations
|
||||
console.log("Working in tenant:", tenant.slug);
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Organization Switcher
|
||||
Allow users to switch between tenants (if they belong to multiple).
|
||||
|
||||
### 2. Tenant Profile Display
|
||||
Show tenant information in dashboard header or settings.
|
||||
|
||||
### 3. Tenant Creation Wizard
|
||||
Guide users through creating a new tenant/organization.
|
||||
|
||||
### 4. Tenant Settings Page
|
||||
Display and edit tenant information.
|
||||
|
||||
### 5. Multi-Tenant Routing
|
||||
Use tenant slug in URLs (e.g., `/t/acme-corp/dashboard`).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Tenant slug already exists" error
|
||||
|
||||
**Possible causes:**
|
||||
1. Slug is already taken by another tenant
|
||||
2. User trying to create duplicate tenant
|
||||
|
||||
**Solution:**
|
||||
- Try a different slug
|
||||
- Add numbers or suffix to make it unique
|
||||
- Use the slug validation before submitting
|
||||
|
||||
### "Tenant not found" error
|
||||
|
||||
**Possible causes:**
|
||||
1. Tenant ID is incorrect
|
||||
2. Tenant was deleted
|
||||
3. User doesn't have access to tenant
|
||||
|
||||
**Solution:**
|
||||
- Verify tenant ID is correct UUID format
|
||||
- Check if tenant still exists
|
||||
- Verify user has proper access rights
|
||||
|
||||
### Slug validation fails unexpectedly
|
||||
|
||||
**Possible causes:**
|
||||
1. Special characters in slug
|
||||
2. Uppercase letters
|
||||
3. Leading/trailing spaces or hyphens
|
||||
|
||||
**Solution:**
|
||||
- Use `generateSlug()` to auto-generate valid slug
|
||||
- Use `validateSlug()` before submitting
|
||||
- Convert to lowercase and trim whitespace
|
||||
|
||||
## Related Files
|
||||
|
||||
### Created Files
|
||||
```
|
||||
src/services/API/TenantService.js
|
||||
docs/TENANT_API.md
|
||||
```
|
||||
|
||||
### Backend Reference Files
|
||||
```
|
||||
cloud/maplepress-backend/docs/API.md (lines 416-558)
|
||||
cloud/maplepress-backend/internal/usecase/tenant/
|
||||
cloud/maplepress-backend/internal/repository/tenant/
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [REGISTRATION_API.md](./REGISTRATION_API.md) - Initial tenant creation during registration
|
||||
- [ME_API.md](./ME_API.md) - User profile includes tenant ID
|
||||
- [FRONTEND_ARCHITECTURE.md](./FRONTEND_ARCHITECTURE.md) - Architecture overview
|
||||
- [README.md](./README.md) - Documentation index
|
||||
- [Backend API Documentation](../../../cloud/maplepress-backend/docs/API.md) - Complete API reference
|
||||
|
||||
## Summary
|
||||
|
||||
The Tenant API implementation provides:
|
||||
|
||||
1. **Tenant Creation**: Create new organizations with validation
|
||||
2. **Tenant Retrieval**: Get tenant by ID or slug
|
||||
3. **Slug Generation**: Auto-generate URL-friendly slugs
|
||||
4. **Validation Helpers**: Client-side validation before API calls
|
||||
5. **Error Handling**: Clear error messages and graceful failures
|
||||
6. **Multi-Tenant Support**: Foundation for multi-tenant architecture
|
||||
|
||||
This is essential for managing organizations in a multi-tenant SaaS application.
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: October 30, 2024
|
||||
**Frontend Version**: 0.0.0
|
||||
**Documentation Version**: 1.0.0
|
||||
557
web/maplepress-frontend/docs/API/USER_API.md
Normal file
557
web/maplepress-frontend/docs/API/USER_API.md
Normal file
|
|
@ -0,0 +1,557 @@
|
|||
# User API Implementation (User Management)
|
||||
|
||||
This document describes the implementation of the User Management API endpoints for the MaplePress frontend, integrated with the MaplePress backend API.
|
||||
|
||||
## Overview
|
||||
|
||||
The User API endpoints manage users within a tenant (organization). All operations require both JWT authentication and tenant context via the `X-Tenant-ID` header. Users are scoped to tenants, providing data isolation in the multi-tenant architecture.
|
||||
|
||||
## Backend API Endpoints
|
||||
|
||||
### Create User
|
||||
**Endpoint**: `POST /api/v1/users`
|
||||
**Authentication**: Required (JWT token)
|
||||
**Tenant Context**: Required (`X-Tenant-ID` header)
|
||||
|
||||
### Get User by ID
|
||||
**Endpoint**: `GET /api/v1/users/{id}`
|
||||
**Authentication**: Required (JWT token)
|
||||
**Tenant Context**: Required (`X-Tenant-ID` header)
|
||||
|
||||
**Documentation**: `/cloud/maplepress-backend/docs/API.md` (lines 560-660)
|
||||
|
||||
## Request/Response Structures
|
||||
|
||||
### Create User Request
|
||||
```json
|
||||
{
|
||||
"email": "jane@techstart.com",
|
||||
"name": "Jane Smith"
|
||||
}
|
||||
```
|
||||
|
||||
**Headers Required:**
|
||||
- `Content-Type: application/json`
|
||||
- `Authorization: JWT {access_token}`
|
||||
- `X-Tenant-ID: {tenant_id}` (required in development mode)
|
||||
|
||||
### User Response
|
||||
```json
|
||||
{
|
||||
"id": "950e8400-e29b-41d4-a716-446655440000",
|
||||
"email": "jane@techstart.com",
|
||||
"name": "Jane Smith",
|
||||
"created_at": "2024-10-24T00:00:00Z",
|
||||
"updated_at": "2024-10-24T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Frontend Implementation
|
||||
|
||||
### UserService (`src/services/API/UserService.js`)
|
||||
|
||||
Handles all user management operations with tenant context.
|
||||
|
||||
**Key Features:**
|
||||
- Create new users within a tenant
|
||||
- Retrieve user by ID within tenant context
|
||||
- Client-side validation (email and name)
|
||||
- Tenant context support via X-Tenant-ID header
|
||||
- Response transformation (snake_case to camelCase)
|
||||
- User-friendly error message mapping
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `createUser(userData, tenantId)`
|
||||
Create a new user within a tenant.
|
||||
|
||||
```javascript
|
||||
import UserService from './services/API/UserService';
|
||||
|
||||
const user = await UserService.createUser({
|
||||
email: "jane@techstart.com",
|
||||
name: "Jane Smith"
|
||||
}, "850e8400-..."); // tenant ID
|
||||
|
||||
console.log(user);
|
||||
// Output:
|
||||
// {
|
||||
// id: "950e8400-...",
|
||||
// email: "jane@techstart.com",
|
||||
// name: "Jane Smith",
|
||||
// createdAt: Date object
|
||||
// }
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `userData.email` (string, required): User's email address
|
||||
- `userData.name` (string, required): User's full name
|
||||
- `tenantId` (string, optional): Tenant ID for X-Tenant-ID header
|
||||
|
||||
**Returns:**
|
||||
```javascript
|
||||
{
|
||||
id: string, // User ID (UUID)
|
||||
email: string, // User email
|
||||
name: string, // User name
|
||||
createdAt: Date // Creation timestamp
|
||||
}
|
||||
```
|
||||
|
||||
**Throws:**
|
||||
- "User data is required" - If userData is missing
|
||||
- "User email is required" - If email is missing
|
||||
- "User name is required" - If name is missing
|
||||
- "Invalid email format" - If email format is invalid
|
||||
- "User email already exists in this tenant." - Email conflict (409)
|
||||
- "Tenant context required. Please provide X-Tenant-ID header." - Missing tenant context
|
||||
- "Authentication required. Please log in to continue." - Missing/invalid token
|
||||
|
||||
#### `getUserById(userId, tenantId)`
|
||||
Retrieve user information by ID within tenant context.
|
||||
|
||||
```javascript
|
||||
const user = await UserService.getUserById("950e8400-...", "850e8400-...");
|
||||
console.log(user.name); // "Jane Smith"
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `userId` (string, required): User ID (UUID format)
|
||||
- `tenantId` (string, optional): Tenant ID for X-Tenant-ID header
|
||||
|
||||
**Returns:**
|
||||
```javascript
|
||||
{
|
||||
id: string,
|
||||
email: string,
|
||||
name: string,
|
||||
createdAt: Date,
|
||||
updatedAt: Date
|
||||
}
|
||||
```
|
||||
|
||||
**Throws:**
|
||||
- "User ID is required" - If ID is missing
|
||||
- "Invalid user ID format" - If ID is not a valid UUID
|
||||
- "User not found in this tenant." - If user doesn't exist in tenant (404)
|
||||
- "Tenant context required." - Missing tenant context
|
||||
- "Authentication required." - Missing/invalid token
|
||||
|
||||
#### `validateEmail(email)`
|
||||
Validate an email address format.
|
||||
|
||||
```javascript
|
||||
const result = UserService.validateEmail("jane@example.com");
|
||||
console.log(result); // { valid: true, error: null }
|
||||
|
||||
const invalid = UserService.validateEmail("invalid-email");
|
||||
console.log(invalid); // { valid: false, error: "Invalid email format" }
|
||||
```
|
||||
|
||||
**Returns:** `{ valid: boolean, error: string|null }`
|
||||
|
||||
**Validation Rules:**
|
||||
- Required (non-empty)
|
||||
- Valid email format (`user@domain.com`)
|
||||
- Maximum 255 characters
|
||||
|
||||
#### `validateName(name)`
|
||||
Validate a user name.
|
||||
|
||||
```javascript
|
||||
const result = UserService.validateName("Jane Smith");
|
||||
console.log(result); // { valid: true, error: null }
|
||||
```
|
||||
|
||||
**Returns:** `{ valid: boolean, error: string|null }`
|
||||
|
||||
**Validation Rules:**
|
||||
- Required (non-empty)
|
||||
- Length: 2-100 characters
|
||||
|
||||
#### `isValidUUID(uuid)`
|
||||
Check if a string is a valid UUID.
|
||||
|
||||
```javascript
|
||||
const isValid = UserService.isValidUUID("950e8400-e29b-41d4-a716-446655440000");
|
||||
console.log(isValid); // true
|
||||
```
|
||||
|
||||
**Returns:** `boolean`
|
||||
|
||||
## Important: Tenant Context
|
||||
|
||||
All user operations require tenant context via the `X-Tenant-ID` header. This header can be provided in two ways:
|
||||
|
||||
### Option 1: Explicit Tenant ID
|
||||
Pass tenant ID to each method call:
|
||||
|
||||
```javascript
|
||||
const tenantId = "850e8400-...";
|
||||
const user = await UserService.createUser(userData, tenantId);
|
||||
```
|
||||
|
||||
### Option 2: Automatic from Current User (Recommended)
|
||||
Enhance ApiClient to automatically add X-Tenant-ID from the current user's tenant:
|
||||
|
||||
```javascript
|
||||
// In ApiClient.js
|
||||
import { authManager } from './Services';
|
||||
|
||||
// Add tenant header automatically
|
||||
const tenant = authManager.getTenant();
|
||||
if (tenant && tenant.id) {
|
||||
requestHeaders["X-Tenant-ID"] = tenant.id;
|
||||
}
|
||||
```
|
||||
|
||||
Then use without explicit tenant ID:
|
||||
|
||||
```javascript
|
||||
// Tenant ID automatically added from current user
|
||||
const user = await UserService.createUser(userData);
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Create User Flow
|
||||
```
|
||||
User provides email and name
|
||||
↓
|
||||
UserService.createUser()
|
||||
↓
|
||||
Validate email and name (client-side)
|
||||
↓
|
||||
ApiClient.post() with JWT token + X-Tenant-ID
|
||||
↓
|
||||
Token automatically refreshed if needed
|
||||
↓
|
||||
POST /api/v1/users with tenant context
|
||||
↓
|
||||
Backend validates email uniqueness within tenant
|
||||
↓
|
||||
Backend creates user in database (scoped to tenant)
|
||||
↓
|
||||
Backend returns user data
|
||||
↓
|
||||
UserService transforms response
|
||||
↓
|
||||
Component receives user data
|
||||
```
|
||||
|
||||
### Get User Flow
|
||||
```
|
||||
Component needs user data
|
||||
↓
|
||||
UserService.getUserById()
|
||||
↓
|
||||
Validate UUID format
|
||||
↓
|
||||
ApiClient.get() with JWT token + X-Tenant-ID
|
||||
↓
|
||||
Token automatically refreshed if needed
|
||||
↓
|
||||
GET /api/v1/users/{id} with tenant context
|
||||
↓
|
||||
Backend retrieves user from database (tenant-scoped)
|
||||
↓
|
||||
Backend returns user data
|
||||
↓
|
||||
UserService transforms response
|
||||
↓
|
||||
Component receives user data
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Error Types
|
||||
|
||||
| Error Condition | Response | Frontend Behavior |
|
||||
|----------------|----------|-------------------|
|
||||
| Missing authentication | 401 Unauthorized | "Authentication required." |
|
||||
| Missing tenant context | 400 Bad Request | "Tenant context required." |
|
||||
| Invalid user data | 400 Bad Request | Specific validation error |
|
||||
| Email already exists | 409 Conflict | "User email already exists in this tenant." |
|
||||
| User not found | 404 Not Found | "User not found in this tenant." |
|
||||
| Server error | 500 Internal Server Error | Generic error message |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Create a New User
|
||||
|
||||
```javascript
|
||||
import React, { useState } from 'react';
|
||||
import UserService from '../../services/API/UserService';
|
||||
import MeService from '../../services/API/MeService';
|
||||
|
||||
function CreateUserForm() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [name, setName] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
|
||||
// Validate before sending
|
||||
const emailValidation = UserService.validateEmail(email);
|
||||
if (!emailValidation.valid) {
|
||||
setError(emailValidation.error);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const nameValidation = UserService.validateName(name);
|
||||
if (!nameValidation.valid) {
|
||||
setError(nameValidation.error);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get current user's tenant ID
|
||||
const profile = await MeService.getMe();
|
||||
const tenantId = profile.tenantId;
|
||||
|
||||
// Create user in current tenant
|
||||
const user = await UserService.createUser(
|
||||
{ email, name },
|
||||
tenantId
|
||||
);
|
||||
|
||||
console.log("User created:", user);
|
||||
// Reset form or redirect
|
||||
setEmail('');
|
||||
setName('');
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label>Email:</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="user@example.com"
|
||||
maxLength={255}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Name:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="John Doe"
|
||||
maxLength={100}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && <p className="error">{error}</p>}
|
||||
|
||||
<button type="submit" disabled={loading}>
|
||||
{loading ? 'Creating...' : 'Create User'}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateUserForm;
|
||||
```
|
||||
|
||||
### Display User Profile
|
||||
|
||||
```javascript
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import UserService from '../../services/API/UserService';
|
||||
import MeService from '../../services/API/MeService';
|
||||
|
||||
function UserProfile({ userId }) {
|
||||
const [user, setUser] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUser = async () => {
|
||||
try {
|
||||
// Get tenant context
|
||||
const profile = await MeService.getMe();
|
||||
|
||||
// Get user data
|
||||
const data = await UserService.getUserById(userId, profile.tenantId);
|
||||
setUser(data);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchUser();
|
||||
}, [userId]);
|
||||
|
||||
if (loading) return <div>Loading user...</div>;
|
||||
if (error) return <div>Error: {error}</div>;
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
<div className="user-profile">
|
||||
<h2>{user.name}</h2>
|
||||
<p>Email: {user.email}</p>
|
||||
<p>ID: {user.id}</p>
|
||||
<p>Created: {user.createdAt.toLocaleDateString()}</p>
|
||||
<p>Updated: {user.updatedAt.toLocaleDateString()}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserProfile;
|
||||
```
|
||||
|
||||
### List Users in Tenant (Helper)
|
||||
|
||||
```javascript
|
||||
// Note: There's no list endpoint in the API yet
|
||||
// This is a pattern for when it's added
|
||||
|
||||
async function listUsersInTenant(tenantId) {
|
||||
// This would call GET /api/v1/users with tenant context
|
||||
// For now, you can only get users by ID
|
||||
console.log("List endpoint not yet available");
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Create User
|
||||
|
||||
```javascript
|
||||
// In browser console after login
|
||||
|
||||
import UserService from './services/API/UserService';
|
||||
import MeService from './services/API/MeService';
|
||||
|
||||
// Get tenant context
|
||||
const profile = await MeService.getMe();
|
||||
const tenantId = profile.tenantId;
|
||||
|
||||
// Validate email
|
||||
const validation = UserService.validateEmail("test@example.com");
|
||||
console.log("Email valid:", validation.valid);
|
||||
|
||||
// Create user
|
||||
const user = await UserService.createUser({
|
||||
email: "test@example.com",
|
||||
name: "Test User"
|
||||
}, tenantId);
|
||||
|
||||
console.log("Created user:", user);
|
||||
```
|
||||
|
||||
### Test Get User
|
||||
|
||||
```javascript
|
||||
// Using user ID from creation
|
||||
const user = await UserService.getUserById(user.id, tenantId);
|
||||
console.log("Retrieved user:", user);
|
||||
```
|
||||
|
||||
### Test with curl
|
||||
|
||||
```bash
|
||||
# 1. Login and get access token
|
||||
ACCESS_TOKEN=$(curl -X POST http://localhost:8000/api/v1/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "test@example.com",
|
||||
"password": "SecurePass123!"
|
||||
}' | jq -r '.access_token')
|
||||
|
||||
# Get tenant ID from login response
|
||||
TENANT_ID=$(curl -X GET http://localhost:8000/api/v1/me \
|
||||
-H "Authorization: JWT $ACCESS_TOKEN" | jq -r '.tenant_id')
|
||||
|
||||
# 2. Create user
|
||||
curl -X POST http://localhost:8000/api/v1/users \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: JWT $ACCESS_TOKEN" \
|
||||
-H "X-Tenant-ID: $TENANT_ID" \
|
||||
-d '{
|
||||
"email": "newuser@example.com",
|
||||
"name": "New User"
|
||||
}' | jq
|
||||
|
||||
# 3. Get user by ID (use ID from creation response)
|
||||
USER_ID="950e8400-..."
|
||||
curl -X GET "http://localhost:8000/api/v1/users/$USER_ID" \
|
||||
-H "Authorization: JWT $ACCESS_TOKEN" \
|
||||
-H "X-Tenant-ID: $TENANT_ID" | jq
|
||||
```
|
||||
|
||||
## Multi-Tenant Isolation
|
||||
|
||||
Users are scoped to tenants for data isolation:
|
||||
|
||||
```javascript
|
||||
// Users in tenant A cannot see users in tenant B
|
||||
const tenantA = "850e8400-...";
|
||||
const tenantB = "950e8400-...";
|
||||
|
||||
// Create user in tenant A
|
||||
const userA = await UserService.createUser(userData, tenantA);
|
||||
|
||||
// Try to get user A from tenant B context (will fail - not found)
|
||||
try {
|
||||
const user = await UserService.getUserById(userA.id, tenantB);
|
||||
} catch (error) {
|
||||
console.log("Cannot access user from different tenant");
|
||||
}
|
||||
```
|
||||
|
||||
## Related Files
|
||||
|
||||
### Created Files
|
||||
```
|
||||
src/services/API/UserService.js
|
||||
docs/USER_API.md
|
||||
```
|
||||
|
||||
### Backend Reference Files
|
||||
```
|
||||
cloud/maplepress-backend/docs/API.md (lines 560-660)
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [TENANT_API.md](./TENANT_API.md) - Tenant management (parent context)
|
||||
- [ME_API.md](./ME_API.md) - Current user profile includes tenant ID
|
||||
- [REGISTRATION_API.md](./REGISTRATION_API.md) - Initial user creation
|
||||
- [FRONTEND_ARCHITECTURE.md](./FRONTEND_ARCHITECTURE.md) - Architecture overview
|
||||
- [README.md](./README.md) - Documentation index
|
||||
|
||||
## Summary
|
||||
|
||||
The User API implementation provides:
|
||||
|
||||
1. **User Creation**: Create users within tenant context
|
||||
2. **User Retrieval**: Get user by ID with tenant isolation
|
||||
3. **Validation Helpers**: Client-side validation before API calls
|
||||
4. **Tenant Context**: Multi-tenant data isolation
|
||||
5. **Error Handling**: Clear error messages and graceful failures
|
||||
|
||||
Essential for managing team members within organizations (tenants) in a multi-tenant SaaS application.
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: October 30, 2024
|
||||
**Frontend Version**: 0.0.0
|
||||
**Documentation Version**: 1.0.0
|
||||
224
web/maplepress-frontend/docs/ARCHITECTURE_SIMPLE.md
Normal file
224
web/maplepress-frontend/docs/ARCHITECTURE_SIMPLE.md
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
# MaplePress Frontend Architecture
|
||||
|
||||
This document describes the architecture and organization of the MaplePress React frontend application.
|
||||
|
||||
## Overview
|
||||
|
||||
The MaplePress frontend is built with React 19, Vite, and TailwindCSS, following a clean separation of concerns with a service layer pattern similar to the maplefile-frontend architecture.
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **React 19** - UI framework
|
||||
- **Vite** - Build tool and dev server
|
||||
- **TailwindCSS 4** - Utility-first CSS framework
|
||||
- **react-router** - Client-side routing
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── services/ # Service layer (business logic)
|
||||
│ ├── Manager/ # Manager services (orchestration)
|
||||
│ │ └── AuthManager.js
|
||||
│ ├── API/ # API client services
|
||||
│ │ └── ApiClient.js
|
||||
│ └── Services.jsx # Service registry and DI container
|
||||
│
|
||||
├── pages/ # Page components
|
||||
│ ├── Home/ # Home/landing page
|
||||
│ │ └── IndexPage.jsx
|
||||
│ ├── Auth/ # Authentication pages
|
||||
│ │ ├── Login.jsx
|
||||
│ │ └── Register.jsx
|
||||
│ └── Dashboard/ # Dashboard pages
|
||||
│ └── Dashboard.jsx
|
||||
│
|
||||
├── hooks/ # Custom React hooks (future)
|
||||
├── App.jsx # Main app component with routing
|
||||
└── main.jsx # Application entry point
|
||||
```
|
||||
|
||||
## Architecture Layers
|
||||
|
||||
### 1. Service Layer (`src/services/`)
|
||||
|
||||
The service layer provides a clean separation between UI components and business logic, following dependency injection principles.
|
||||
|
||||
**Key Files:**
|
||||
- `Services.jsx` - Central service registry with React Context
|
||||
- `Manager/AuthManager.js` - Authentication manager
|
||||
- `API/ApiClient.js` - HTTP client for backend API
|
||||
|
||||
**Service Pattern:**
|
||||
```javascript
|
||||
// Services are created with dependency injection
|
||||
const authManager = new AuthManager();
|
||||
const apiClient = ApiClient;
|
||||
|
||||
// Services are provided via React Context
|
||||
<ServiceProvider>
|
||||
{/* App components */}
|
||||
</ServiceProvider>
|
||||
|
||||
// Components access services via hooks
|
||||
const { authManager } = useAuth();
|
||||
```
|
||||
|
||||
### 2. Pages (`src/pages/`)
|
||||
|
||||
Page components represent full-page views. Each page is organized by feature:
|
||||
|
||||
- **Home/** - Landing page with navigation to auth
|
||||
- **Auth/** - Login and registration pages
|
||||
- **Dashboard/** - User dashboard and management
|
||||
|
||||
### 3. Routing (`src/App.jsx`)
|
||||
|
||||
Centralized routing configuration using react-router:
|
||||
|
||||
```javascript
|
||||
<Routes>
|
||||
{/* Public routes */}
|
||||
<Route path="/" element={<IndexPage />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
|
||||
{/* Protected routes */}
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
</Routes>
|
||||
```
|
||||
|
||||
## Service Layer Details
|
||||
|
||||
### AuthManager
|
||||
|
||||
Manages authentication state and operations:
|
||||
|
||||
- `initialize()` - Initialize auth manager
|
||||
- `isAuthenticated()` - Check if user is logged in
|
||||
- `login(email, password)` - User login
|
||||
- `register(email, password, name)` - User registration
|
||||
- `logout()` - User logout
|
||||
- `getAccessToken()` - Get current access token
|
||||
- `getUser()` - Get current user data
|
||||
|
||||
### ApiClient
|
||||
|
||||
Handles HTTP communication with the backend:
|
||||
|
||||
- `get(endpoint, options)` - GET request
|
||||
- `post(endpoint, body, options)` - POST request
|
||||
- `put(endpoint, body, options)` - PUT request
|
||||
- `patch(endpoint, body, options)` - PATCH request
|
||||
- `delete(endpoint, options)` - DELETE request
|
||||
|
||||
Automatically handles:
|
||||
- Authorization headers
|
||||
- JSON serialization
|
||||
- Error handling
|
||||
|
||||
### Service Hooks
|
||||
|
||||
Custom hooks provide easy access to services:
|
||||
|
||||
```javascript
|
||||
// Get all services
|
||||
const services = useServices();
|
||||
|
||||
// Get auth services
|
||||
const { authManager } = useAuth();
|
||||
|
||||
// Get API client
|
||||
const { apiClient } = useApi();
|
||||
```
|
||||
|
||||
## Page Components
|
||||
|
||||
### IndexPage (Home)
|
||||
- Landing page with MaplePress branding
|
||||
- Navigation to login/register
|
||||
- Feature highlights
|
||||
- Responsive design with TailwindCSS
|
||||
|
||||
### Login
|
||||
- Email/password authentication form
|
||||
- Form validation
|
||||
- Error handling
|
||||
- Navigation to register and home
|
||||
|
||||
### Register
|
||||
- User registration form
|
||||
- Password confirmation
|
||||
- Password strength validation
|
||||
- Navigation to login and home
|
||||
|
||||
### Dashboard
|
||||
- Protected route (requires authentication)
|
||||
- User welcome section
|
||||
- Stats display (sites, indexes, API requests)
|
||||
- Quick action buttons
|
||||
- Getting started guide
|
||||
|
||||
## State Management
|
||||
|
||||
Currently using React Context for service layer dependency injection. Future expansions may include:
|
||||
- Local component state with `useState`
|
||||
- Form state management
|
||||
- Potential integration with Redux/Zustand if needed
|
||||
|
||||
## Styling
|
||||
|
||||
Using TailwindCSS 4 utility classes for styling:
|
||||
- Responsive design with mobile-first approach
|
||||
- Consistent color scheme (indigo/blue palette)
|
||||
- Shadow and border utilities for depth
|
||||
- Hover states and transitions
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Start dev server**: `npm run dev`
|
||||
2. **Build for production**: `npm run build`
|
||||
3. **Lint code**: `npm run lint`
|
||||
4. **Preview build**: `npm run preview`
|
||||
|
||||
## API Integration
|
||||
|
||||
The ApiClient is configured to communicate with the MaplePress backend:
|
||||
|
||||
- Base URL: `VITE_API_BASE_URL` environment variable (default: `http://localhost:8000`)
|
||||
- Authentication: Bearer token in Authorization header
|
||||
- Content-Type: `application/json`
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Current implementation provides stubs for:
|
||||
- [ ] Actual backend API integration
|
||||
- [ ] Token storage and refresh logic
|
||||
- [ ] Protected route guards
|
||||
- [ ] User profile management
|
||||
- [ ] API key management
|
||||
- [ ] Site management
|
||||
- [ ] Search index configuration
|
||||
- [ ] Analytics dashboard
|
||||
|
||||
## Testing
|
||||
|
||||
Testing structure to be implemented:
|
||||
- Unit tests for services
|
||||
- Component tests for pages
|
||||
- Integration tests for workflows
|
||||
- E2E tests for critical paths
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding new features:
|
||||
1. Create services in `src/services/` for business logic
|
||||
2. Create pages in `src/pages/` organized by feature
|
||||
3. Add routes in `App.jsx`
|
||||
4. Use service hooks to access backend functionality
|
||||
5. Follow TailwindCSS utility patterns for styling
|
||||
6. Keep components focused and single-responsibility
|
||||
|
||||
## References
|
||||
|
||||
This architecture is based on the maplefile-frontend pattern, adapted for MaplePress requirements.
|
||||
2058
web/maplepress-frontend/docs/FRONTEND_ARCHITECTURE.md
Normal file
2058
web/maplepress-frontend/docs/FRONTEND_ARCHITECTURE.md
Normal file
File diff suppressed because it is too large
Load diff
367
web/maplepress-frontend/docs/README.md
Normal file
367
web/maplepress-frontend/docs/README.md
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
# MaplePress Frontend Documentation
|
||||
|
||||
This directory contains comprehensive documentation for the MaplePress React frontend application.
|
||||
|
||||
## Documentation Index
|
||||
|
||||
### Architecture & Design
|
||||
|
||||
- **[FRONTEND_ARCHITECTURE.md](./FRONTEND_ARCHITECTURE.md)** - Complete architecture guide
|
||||
- Three-layer service architecture (API → Manager → Storage)
|
||||
- Dependency injection system
|
||||
- Component patterns
|
||||
- Authentication & authorization
|
||||
- State management
|
||||
- Best practices and conventions
|
||||
|
||||
- **[ARCHITECTURE_SIMPLE.md](./ARCHITECTURE_SIMPLE.md)** - Quick reference guide
|
||||
- Project structure overview
|
||||
- Key concepts
|
||||
- Technology stack
|
||||
- Development workflow
|
||||
|
||||
### API Integration Guides
|
||||
|
||||
All API integration guides are located in the [API](./API/) subdirectory.
|
||||
|
||||
- **[HEALTH_API.md](./API/HEALTH_API.md)** - Health Check
|
||||
- Backend service health verification
|
||||
- Backend API integration (`GET /health`)
|
||||
- HealthService implementation
|
||||
- Monitoring and availability checks
|
||||
- Application startup verification
|
||||
- Load balancer health probes
|
||||
|
||||
- **[REGISTRATION_API.md](./API/REGISTRATION_API.md)** - User Registration
|
||||
- Complete registration flow implementation
|
||||
- Backend API integration (`POST /api/v1/register`)
|
||||
- RegisterService implementation
|
||||
- Form validation and error handling
|
||||
- Token storage and session management
|
||||
- Testing procedures
|
||||
|
||||
- **[LOGIN_API.md](./API/LOGIN_API.md)** - User Login
|
||||
- Complete login flow implementation
|
||||
- Backend API integration (`POST /api/v1/login`)
|
||||
- LoginService implementation
|
||||
- Rate limiting and account lockout
|
||||
- Token management
|
||||
- Error handling and recovery
|
||||
|
||||
- **[REFRESH_TOKEN_API.md](./API/REFRESH_TOKEN_API.md)** - Token Refresh
|
||||
- Complete token refresh implementation
|
||||
- Backend API integration (`POST /api/v1/refresh`)
|
||||
- RefreshTokenService implementation
|
||||
- Automatic token refresh (proactive and reactive)
|
||||
- Token rotation and security
|
||||
- Session persistence and validation
|
||||
|
||||
- **[HELLO_API.md](./API/HELLO_API.md)** - Hello (Authenticated Endpoint)
|
||||
- Simple authenticated endpoint for testing
|
||||
- Backend API integration (`POST /api/v1/hello`)
|
||||
- HelloService implementation
|
||||
- Input validation and XSS prevention
|
||||
- Authentication verification
|
||||
- Use cases for testing tokens
|
||||
|
||||
- **[ME_API.md](./API/ME_API.md)** - User Profile (Me Endpoint)
|
||||
- Get authenticated user's profile
|
||||
- Backend API integration (`GET /api/v1/me`)
|
||||
- MeService implementation
|
||||
- Role-based access control helpers
|
||||
- Tenant context verification
|
||||
- Profile display and validation
|
||||
|
||||
- **[TENANT_API.md](./API/TENANT_API.md)** - Tenant Management
|
||||
- Create and manage tenants (organizations)
|
||||
- Backend API integration (`POST /api/v1/tenants`, `GET /api/v1/tenants/{id}`, `GET /api/v1/tenants/slug/{slug}`)
|
||||
- TenantService implementation
|
||||
- Slug generation and validation helpers
|
||||
- Multi-tenant architecture support
|
||||
- Organization management
|
||||
|
||||
- **[USER_API.md](./API/USER_API.md)** - User Management
|
||||
- Create and manage users within tenants
|
||||
- Backend API integration (`POST /api/v1/users`, `GET /api/v1/users/{id}`)
|
||||
- UserService implementation
|
||||
- Tenant context and multi-tenant isolation
|
||||
- Email and name validation helpers
|
||||
- Team member management
|
||||
|
||||
- **[SITE_API.md](./API/SITE_API.md)** - WordPress Site Management
|
||||
- Create and manage WordPress sites
|
||||
- Backend API integration (`POST /api/v1/sites`, `GET /api/v1/sites`, `GET /api/v1/sites/{id}`, `DELETE /api/v1/sites/{id}`, `POST /api/v1/sites/{id}/rotate-api-key`)
|
||||
- SiteService implementation
|
||||
- API key management (shown only once, rotation with grace period)
|
||||
- Site usage statistics and storage tracking
|
||||
- Pagination support for large site lists
|
||||
- Hard delete with immediate key invalidation
|
||||
|
||||
- **[ADMIN_API.md](./API/ADMIN_API.md)** - Admin Account Management
|
||||
- Check account lock status and unlock accounts
|
||||
- Backend API integration (`GET /api/v1/admin/account-status`, `POST /api/v1/admin/unlock-account`)
|
||||
- AdminService implementation
|
||||
- CWE-307 protection (excessive authentication attempts)
|
||||
- Security event logging for audit trail
|
||||
- Admin role enforcement
|
||||
- Account lockout management
|
||||
|
||||
## Getting Started
|
||||
|
||||
### For Developers
|
||||
|
||||
If you're new to the project, start here:
|
||||
|
||||
1. **Read [FRONTEND_ARCHITECTURE.md](./FRONTEND_ARCHITECTURE.md)** - Understand the overall architecture
|
||||
2. **Check [HEALTH_API.md](./API/HEALTH_API.md)** - Verify backend connectivity and health
|
||||
3. **Review [REGISTRATION_API.md](./API/REGISTRATION_API.md)** - See a complete feature implementation
|
||||
4. **Check [LOGIN_API.md](./API/LOGIN_API.md)** - Understand authentication flow
|
||||
5. **Read [REFRESH_TOKEN_API.md](./API/REFRESH_TOKEN_API.md)** - Learn about automatic session maintenance
|
||||
6. **Try [HELLO_API.md](./API/HELLO_API.md)** - Test authentication with a simple endpoint
|
||||
7. **Use [ME_API.md](./API/ME_API.md)** - Get user profile and implement role-based access
|
||||
8. **Explore [TENANT_API.md](./API/TENANT_API.md)** - Manage tenants for multi-tenant architecture
|
||||
9. **Implement [SITE_API.md](./API/SITE_API.md)** - Manage WordPress sites with API key management
|
||||
10. **Admin [ADMIN_API.md](./API/ADMIN_API.md)** - Admin operations for account lockout management
|
||||
|
||||
### For Contributors
|
||||
|
||||
When adding new features:
|
||||
|
||||
1. Follow the three-layer service pattern (API → Manager → Storage)
|
||||
2. Use dependency injection via Services.jsx
|
||||
3. Create comprehensive documentation similar to existing API guides
|
||||
4. Include testing procedures and troubleshooting
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
maplepress-frontend/
|
||||
├── docs/ # This directory
|
||||
│ ├── README.md # This file
|
||||
│ ├── FRONTEND_ARCHITECTURE.md # Complete architecture guide
|
||||
│ ├── ARCHITECTURE_SIMPLE.md # Quick reference
|
||||
│ ├── ACCESS_REFRESH_TOKEN_IMPLEMENTATION.md # Token refresh guide
|
||||
│ ├── TOKEN_REFRESH_ANALYSIS.md # Token refresh analysis
|
||||
│ └── API/ # API integration guides
|
||||
│ ├── REGISTRATION_API.md
|
||||
│ ├── LOGIN_API.md
|
||||
│ ├── REFRESH_TOKEN_API.md
|
||||
│ ├── HELLO_API.md
|
||||
│ ├── ME_API.md
|
||||
│ ├── TENANT_API.md
|
||||
│ ├── USER_API.md
|
||||
│ ├── SITE_API.md
|
||||
│ └── ADMIN_API.md
|
||||
│
|
||||
├── src/
|
||||
│ ├── services/ # Service layer
|
||||
│ │ ├── API/ # API services (HTTP)
|
||||
│ │ ├── Manager/ # Manager services (orchestration)
|
||||
│ │ └── Services.jsx # Service registry + DI
|
||||
│ │
|
||||
│ ├── pages/ # Page components
|
||||
│ │ ├── Home/ # Landing page
|
||||
│ │ ├── Auth/ # Login/Register
|
||||
│ │ └── Dashboard/ # User dashboard
|
||||
│ │
|
||||
│ ├── hooks/ # Custom React hooks
|
||||
│ ├── App.jsx # Main app with routing
|
||||
│ └── main.jsx # Entry point
|
||||
│
|
||||
├── README.md # Project overview
|
||||
└── package.json # Dependencies
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Three-Layer Service Pattern
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ React Components │
|
||||
│ (UI Layer) │
|
||||
└─────────────────┬───────────────────────┘
|
||||
│
|
||||
↓
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Manager Layer (Orchestration) │
|
||||
│ - AuthManager │
|
||||
│ - Business logic │
|
||||
│ - State management │
|
||||
└─────────────────┬───────────────────────┘
|
||||
│
|
||||
↓
|
||||
┌─────────────────────────────────────────┐
|
||||
│ API Layer (HTTP Communication) │
|
||||
│ - RegisterService │
|
||||
│ - LoginService │
|
||||
│ - ApiClient │
|
||||
└─────────────────┬───────────────────────┘
|
||||
│
|
||||
↓
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Backend API │
|
||||
│ POST /api/v1/register │
|
||||
│ POST /api/v1/login │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Service Registration (Dependency Injection)
|
||||
|
||||
All services are registered in `Services.jsx`:
|
||||
|
||||
```javascript
|
||||
// Create services with dependencies
|
||||
const authManager = new AuthManager();
|
||||
const apiClient = ApiClient;
|
||||
|
||||
// Register in context
|
||||
const services = {
|
||||
authManager,
|
||||
apiClient,
|
||||
};
|
||||
|
||||
// Use in components
|
||||
const { authManager } = useAuth();
|
||||
```
|
||||
|
||||
## Implementation Status
|
||||
|
||||
### ✅ Completed Features
|
||||
|
||||
- **Authentication System**
|
||||
- User registration with full form validation
|
||||
- User login with rate limiting support
|
||||
- Token storage and management
|
||||
- Session persistence
|
||||
- Token expiry detection
|
||||
|
||||
- **Service Layer**
|
||||
- Dependency injection system
|
||||
- AuthManager (token storage, session management, automatic refresh)
|
||||
- HealthService (backend health checks and monitoring)
|
||||
- RegisterService (registration API)
|
||||
- LoginService (login API)
|
||||
- RefreshTokenService (token refresh API)
|
||||
- HelloService (authenticated test endpoint)
|
||||
- MeService (user profile and role checking)
|
||||
- TenantService (tenant/organization management)
|
||||
- UserService (user management with tenant context)
|
||||
- SiteService (WordPress site management with API key handling)
|
||||
- AdminService (admin account management and lockout handling)
|
||||
- ApiClient (HTTP client with automatic token refresh)
|
||||
|
||||
- **User Interface**
|
||||
- Home/landing page
|
||||
- Registration page (complete)
|
||||
- Login page (complete)
|
||||
- Dashboard page (stub)
|
||||
|
||||
### 🚧 In Progress / Planned
|
||||
|
||||
- Protected route guards
|
||||
- Logout API integration
|
||||
- Password recovery flow
|
||||
- User profile management
|
||||
- Site management UI
|
||||
- API key management
|
||||
- Search configuration
|
||||
|
||||
## Testing
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Build test
|
||||
npm run build
|
||||
|
||||
# Development server
|
||||
npm run dev
|
||||
|
||||
# Linting
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Manual Testing
|
||||
|
||||
See individual API guides for testing procedures:
|
||||
- [Registration Testing](./API/REGISTRATION_API.md#testing-the-registration-flow)
|
||||
- [Login Testing](./API/LOGIN_API.md#testing-the-login-flow)
|
||||
|
||||
## Backend Integration
|
||||
|
||||
### API Endpoints
|
||||
|
||||
The frontend integrates with these backend endpoints:
|
||||
|
||||
- `GET /health` - Backend health check (no auth)
|
||||
- `POST /api/v1/register` - User registration
|
||||
- `POST /api/v1/login` - User login
|
||||
- `POST /api/v1/refresh` - Token refresh
|
||||
- `POST /api/v1/hello` - Authenticated test endpoint
|
||||
- `GET /api/v1/me` - Get user profile
|
||||
- `POST /api/v1/tenants` - Create tenant
|
||||
- `GET /api/v1/tenants/{id}` - Get tenant by ID
|
||||
- `GET /api/v1/tenants/slug/{slug}` - Get tenant by slug
|
||||
- `POST /api/v1/users` - Create user (requires tenant context)
|
||||
- `GET /api/v1/users/{id}` - Get user by ID (requires tenant context)
|
||||
- `POST /api/v1/sites` - Create WordPress site
|
||||
- `GET /api/v1/sites` - List WordPress sites (paginated)
|
||||
- `GET /api/v1/sites/{id}` - Get WordPress site by ID
|
||||
- `DELETE /api/v1/sites/{id}` - Delete WordPress site
|
||||
- `POST /api/v1/sites/{id}/rotate-api-key` - Rotate site API key
|
||||
- `GET /api/v1/admin/account-status` - Check account lock status (admin)
|
||||
- `POST /api/v1/admin/unlock-account` - Unlock locked account (admin)
|
||||
|
||||
### Backend Documentation
|
||||
|
||||
For complete backend API documentation, see:
|
||||
- [Backend API Documentation](../../../cloud/maplepress-backend/docs/API.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding new documentation:
|
||||
|
||||
1. **Create detailed guides** for new features (see existing API docs)
|
||||
2. **Update this README** to include new documentation
|
||||
3. **Follow existing patterns** for consistency
|
||||
4. **Include examples** and testing procedures
|
||||
5. **Add troubleshooting** sections
|
||||
|
||||
### Documentation Standards
|
||||
|
||||
- Use clear, descriptive headings
|
||||
- Include code examples
|
||||
- Provide testing procedures
|
||||
- Add troubleshooting sections
|
||||
- Keep table of contents updated
|
||||
- Use diagrams where helpful
|
||||
|
||||
## Support & Resources
|
||||
|
||||
### Internal Resources
|
||||
|
||||
- [Main README](../README.md) - Project overview and setup
|
||||
- [Backend API Docs](../../../cloud/mapleopentech-backend/docs/API.md) - Complete API reference
|
||||
- [CLAUDE.md](../../../CLAUDE.md) - Repository structure and conventions
|
||||
|
||||
### External Resources
|
||||
|
||||
- [React 19 Documentation](https://react.dev)
|
||||
- [Vite Documentation](https://vite.dev)
|
||||
- [TailwindCSS Documentation](https://tailwindcss.com)
|
||||
- [React Router Documentation](https://reactrouter.com)
|
||||
|
||||
## Questions & Issues
|
||||
|
||||
For questions or issues:
|
||||
1. Check the relevant documentation first
|
||||
2. Review troubleshooting sections in API guides
|
||||
3. Check backend logs for API errors
|
||||
4. Create an issue with detailed information
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: October 30, 2024
|
||||
**Frontend Version**: 0.0.0
|
||||
**Documentation Version**: 1.0.0
|
||||
Loading…
Add table
Add a link
Reference in a new issue