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

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/json
  • Authorization: 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 address
  • userData.name (string, required): User's full name
  • tenantId (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);

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");
}

Created Files

src/services/API/UserService.js
docs/USER_API.md

Backend Reference Files

cloud/maplepress-backend/docs/API.md (lines 560-660)

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