monorepo/web/maplepress-frontend/docs/API/USER_API.md

557 lines
14 KiB
Markdown

# 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