# 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 (
); } 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) returnSlug: {tenant.slug}
Status: {tenant.status}
Created: {tenant.createdAt.toLocaleDateString()}
Updated: {tenant.updatedAt.toLocaleDateString()}
ID: {tenant.id}