Initial commit: Open sourcing all of the Maple Open Technologies code.

This commit is contained in:
Bartlomiej Mika 2025-12-02 14:33:08 -05:00
commit 755d54a99d
2010 changed files with 448675 additions and 0 deletions

View file

@ -0,0 +1,792 @@
# 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