792 lines
20 KiB
Markdown
792 lines
20 KiB
Markdown
# 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 (
|
|
<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
|
|
|
|
```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 <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
|
|
|
|
```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 (
|
|
<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
|
|
|
|
```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 (
|
|
<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
|
|
|
|
```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
|