# 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 (
setSlug(e.target.value.toLowerCase())} placeholder="techstart" maxLength={50} /> URL: /tenants/{slug}
{error &&

{error}

}
); } 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) return
Loading tenant...
; if (error) return
Error: {error}
; if (!tenant) return null; return (

{tenant.name}

Slug: {tenant.slug}

Status: {tenant.status}

Created: {tenant.createdAt.toLocaleDateString()}

Updated: {tenant.updatedAt.toLocaleDateString()}

); } export default TenantProfile; ``` ### Search Tenant by Slug ```javascript import TenantService from '../../services/API/TenantService'; function TenantLookup() { const [slug, setSlug] = useState(''); const [tenant, setTenant] = useState(null); const handleSearch = async () => { try { const data = await TenantService.getTenantBySlug(slug); setTenant(data); } catch (error) { console.error("Tenant not found:", error.message); setTenant(null); } }; return (
setSlug(e.target.value)} placeholder="Enter tenant slug" /> {tenant && (

{tenant.name}

ID: {tenant.id}

)}
); } ``` ### Validate Slug in Real-Time ```javascript import React, { useState, useEffect } from 'react'; import TenantService from '../../services/API/TenantService'; function SlugInput({ value, onChange }) { const [validation, setValidation] = useState({ valid: true, error: null }); useEffect(() => { if (value) { const result = TenantService.validateSlug(value); setValidation(result); } }, [value]); return (
onChange(e.target.value.toLowerCase())} className={validation.valid ? '' : 'error'} /> {!validation.valid && ( {validation.error} )} {validation.valid && value && ( ✓ Valid slug )}
); } ``` ## Testing the Tenant API ### 1. Prerequisites - Backend running at `http://localhost:8000` - Frontend running at `http://localhost:5173` - User logged in (valid JWT token) ### 2. Test Create Tenant ```javascript // In browser console after login import TenantService from './services/API/TenantService'; // Generate slug from name const slug = TenantService.generateSlug("My New Company"); console.log("Generated slug:", slug); // "my-new-company" // Validate before creating const validation = TenantService.validateSlug(slug); console.log("Slug valid:", validation.valid); // true // Create tenant const tenant = await TenantService.createTenant({ name: "My New Company", slug: slug }); console.log("Created tenant:", tenant); ``` ### 3. Test Get Tenant by ID ```javascript // Using tenant ID from creation const tenant = await TenantService.getTenantById("850e8400-..."); console.log("Tenant by ID:", tenant); ``` ### 4. Test Get Tenant by Slug ```javascript const tenant = await TenantService.getTenantBySlug("my-new-company"); console.log("Tenant by slug:", tenant); ``` ### 5. 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') # 2. Create tenant curl -X POST http://localhost:8000/api/v1/tenants \ -H "Content-Type: application/json" \ -H "Authorization: JWT $ACCESS_TOKEN" \ -d '{ "name": "Test Company", "slug": "test-company" }' | jq # 3. Get tenant by ID (use ID from creation response) TENANT_ID="850e8400-e29b-41d4-a716-446655440000" curl -X GET "http://localhost:8000/api/v1/tenants/$TENANT_ID" \ -H "Authorization: JWT $ACCESS_TOKEN" | jq # 4. Get tenant by slug curl -X GET "http://localhost:8000/api/v1/tenants/slug/test-company" \ -H "Authorization: JWT $ACCESS_TOKEN" | jq # 5. Test slug conflict (should fail with 409) curl -X POST http://localhost:8000/api/v1/tenants \ -H "Content-Type: application/json" \ -H "Authorization: JWT $ACCESS_TOKEN" \ -d '{ "name": "Another Company", "slug": "test-company" }' | jq # Expected: 409 Conflict ``` ### 6. Test Slug Generation ```javascript // Test various slug generation scenarios const tests = [ { input: "TechStart Inc", expected: "techstart-inc" }, { input: "My Company!", expected: "my-company" }, { input: " Spaces Everywhere ", expected: "spaces-everywhere" }, { input: "Multiple---Hyphens", expected: "multiple-hyphens" }, { input: "123 Numbers", expected: "123-numbers" }, ]; tests.forEach(({ input, expected }) => { const result = TenantService.generateSlug(input); console.log(`Input: "${input}"`); console.log(`Expected: "${expected}"`); console.log(`Got: "${result}"`); console.log(`✓ Pass: ${result === expected}\n`); }); ``` ## Integration with AuthManager The tenant created during registration is stored in AuthManager: ```javascript import { useAuth } from './services/Services'; import TenantService from './services/API/TenantService'; const { authManager } = useAuth(); // Get stored tenant from AuthManager const storedTenant = authManager.getTenant(); console.log("Stored tenant:", storedTenant); // { id: "...", name: "...", slug: "..." } // Fetch fresh tenant data from API const freshTenant = await TenantService.getTenantById(storedTenant.id); console.log("Fresh tenant:", freshTenant); // Compare if (storedTenant.name !== freshTenant.name) { console.warn("Tenant data has changed"); } ``` ## Multi-Tenant Context Tenants provide isolation for multi-tenant applications: ```javascript // Get current user's tenant import MeService from './services/API/MeService'; import TenantService from './services/API/TenantService'; const profile = await MeService.getMe(); const tenantId = profile.tenantId; // Get full tenant details const tenant = await TenantService.getTenantById(tenantId); console.log("Current tenant:", tenant.name); // Use tenant context for operations console.log("Working in tenant:", tenant.slug); ``` ## Use Cases ### 1. Organization Switcher Allow users to switch between tenants (if they belong to multiple). ### 2. Tenant Profile Display Show tenant information in dashboard header or settings. ### 3. Tenant Creation Wizard Guide users through creating a new tenant/organization. ### 4. Tenant Settings Page Display and edit tenant information. ### 5. Multi-Tenant Routing Use tenant slug in URLs (e.g., `/t/acme-corp/dashboard`). ## Troubleshooting ### "Tenant slug already exists" error **Possible causes:** 1. Slug is already taken by another tenant 2. User trying to create duplicate tenant **Solution:** - Try a different slug - Add numbers or suffix to make it unique - Use the slug validation before submitting ### "Tenant not found" error **Possible causes:** 1. Tenant ID is incorrect 2. Tenant was deleted 3. User doesn't have access to tenant **Solution:** - Verify tenant ID is correct UUID format - Check if tenant still exists - Verify user has proper access rights ### Slug validation fails unexpectedly **Possible causes:** 1. Special characters in slug 2. Uppercase letters 3. Leading/trailing spaces or hyphens **Solution:** - Use `generateSlug()` to auto-generate valid slug - Use `validateSlug()` before submitting - Convert to lowercase and trim whitespace ## Related Files ### Created Files ``` src/services/API/TenantService.js docs/TENANT_API.md ``` ### Backend Reference Files ``` cloud/maplepress-backend/docs/API.md (lines 416-558) cloud/maplepress-backend/internal/usecase/tenant/ cloud/maplepress-backend/internal/repository/tenant/ ``` ## Related Documentation - [REGISTRATION_API.md](./REGISTRATION_API.md) - Initial tenant creation during registration - [ME_API.md](./ME_API.md) - User profile includes tenant ID - [FRONTEND_ARCHITECTURE.md](./FRONTEND_ARCHITECTURE.md) - Architecture overview - [README.md](./README.md) - Documentation index - [Backend API Documentation](../../../cloud/maplepress-backend/docs/API.md) - Complete API reference ## Summary The Tenant API implementation provides: 1. **Tenant Creation**: Create new organizations with validation 2. **Tenant Retrieval**: Get tenant by ID or slug 3. **Slug Generation**: Auto-generate URL-friendly slugs 4. **Validation Helpers**: Client-side validation before API calls 5. **Error Handling**: Clear error messages and graceful failures 6. **Multi-Tenant Support**: Foundation for multi-tenant architecture This is essential for managing organizations in a multi-tenant SaaS application. --- **Last Updated**: October 30, 2024 **Frontend Version**: 0.0.0 **Documentation Version**: 1.0.0