14 KiB
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
{
"email": "jane@techstart.com",
"name": "Jane Smith"
}
Headers Required:
Content-Type: application/jsonAuthorization: JWT {access_token}X-Tenant-ID: {tenant_id}(required in development mode)
User Response
{
"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.
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 addressuserData.name(string, required): User's full nametenantId(string, optional): Tenant ID for X-Tenant-ID header
Returns:
{
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.
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:
{
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.
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.
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.
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:
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:
// 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:
// 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
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
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)
// 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
// 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
// Using user ID from creation
const user = await UserService.getUserById(user.id, tenantId);
console.log("Retrieved user:", user);
Test with curl
# 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:
// 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 management (parent context)
- ME_API.md - Current user profile includes tenant ID
- REGISTRATION_API.md - Initial user creation
- FRONTEND_ARCHITECTURE.md - Architecture overview
- README.md - Documentation index
Summary
The User API implementation provides:
- User Creation: Create users within tenant context
- User Retrieval: Get user by ID with tenant isolation
- Validation Helpers: Client-side validation before API calls
- Tenant Context: Multi-tenant data isolation
- 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