557 lines
14 KiB
Markdown
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
|