Initial commit: Open sourcing all of the Maple Open Technologies code.

This commit is contained in:
Bartlomiej Mika 2025-12-02 14:33:08 -05:00
commit 755d54a99d
2010 changed files with 448675 additions and 0 deletions

View 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

View 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

View 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

View 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)

View 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

View 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

View 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

File diff suppressed because it is too large Load diff

View 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

View 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