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

20 KiB

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

{
  "name": "TechStart Inc",
  "slug": "techstart"
}

Tenant Response

{
  "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).

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:

{
  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.

const tenant = await TenantService.getTenantById("850e8400-...");
console.log(tenant.name); // "TechStart Inc"

Parameters:

  • tenantId (string, required): Tenant ID (UUID format)

Returns:

{
  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.

const tenant = await TenantService.getTenantBySlug("techstart");
console.log(tenant.id); // "850e8400-..."

Parameters:

  • slug (string, required): Tenant slug

Returns:

{
  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.

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.

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:

{
  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.

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:

{
  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

// 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

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 (
    <form onSubmit={handleSubmit}>
      <div>
        <label>Tenant Name:</label>
        <input
          type="text"
          value={name}
          onChange={handleNameChange}
          placeholder="TechStart Inc"
          maxLength={100}
        />
      </div>

      <div>
        <label>Tenant Slug:</label>
        <input
          type="text"
          value={slug}
          onChange={(e) => setSlug(e.target.value.toLowerCase())}
          placeholder="techstart"
          maxLength={50}
        />
        <small>URL: /tenants/{slug}</small>
      </div>

      {error && <p className="error">{error}</p>}

      <button type="submit" disabled={loading}>
        {loading ? 'Creating...' : 'Create Tenant'}
      </button>
    </form>
  );
}

export default CreateTenantForm;

Display Tenant Information

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

  return (
    <div className="tenant-profile">
      <h2>{tenant.name}</h2>
      <p>Slug: {tenant.slug}</p>
      <p>Status: {tenant.status}</p>
      <p>Created: {tenant.createdAt.toLocaleDateString()}</p>
      <p>Updated: {tenant.updatedAt.toLocaleDateString()}</p>
    </div>
  );
}

export default TenantProfile;

Search Tenant by Slug

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 (
    <div>
      <input
        value={slug}
        onChange={(e) => setSlug(e.target.value)}
        placeholder="Enter tenant slug"
      />
      <button onClick={handleSearch}>Search</button>

      {tenant && (
        <div>
          <h3>{tenant.name}</h3>
          <p>ID: {tenant.id}</p>
        </div>
      )}
    </div>
  );
}

Validate Slug in Real-Time

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 (
    <div>
      <input
        type="text"
        value={value}
        onChange={(e) => onChange(e.target.value.toLowerCase())}
        className={validation.valid ? '' : 'error'}
      />
      {!validation.valid && (
        <span className="error-message">{validation.error}</span>
      )}
      {validation.valid && value && (
        <span className="success-message"> Valid slug</span>
      )}
    </div>
  );
}

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

// 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

// Using tenant ID from creation
const tenant = await TenantService.getTenantById("850e8400-...");
console.log("Tenant by ID:", tenant);

4. Test Get Tenant by Slug

const tenant = await TenantService.getTenantBySlug("my-new-company");
console.log("Tenant by slug:", tenant);

5. 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')

# 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

// 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:

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:

// 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

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/

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