Initial commit: SmoothSchedule multi-tenant scheduling platform

This commit includes:
- Django backend with multi-tenancy (django-tenants)
- React + TypeScript frontend with Vite
- Platform administration API with role-based access control
- Authentication system with token-based auth
- Quick login dev tools for testing different user roles
- CORS and CSRF configuration for local development
- Docker development environment setup

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
poduck
2025-11-27 01:43:20 -05:00
commit 2e111364a2
567 changed files with 96410 additions and 0 deletions

View File

@@ -0,0 +1,86 @@
/**
* API Client
* Axios instance configured for SmoothSchedule API
*/
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
import { API_BASE_URL, getSubdomain } from './config';
import { getCookie } from '../utils/cookies';
// Create axios instance
const apiClient = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
withCredentials: true, // For CORS with credentials
});
// Request interceptor - add auth token and business subdomain
apiClient.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// Add business subdomain header if on business site
const subdomain = getSubdomain();
if (subdomain && subdomain !== 'platform') {
config.headers['X-Business-Subdomain'] = subdomain;
}
// Add auth token if available (from cookie)
const token = getCookie('access_token');
if (token) {
// Use 'Token' prefix for Django REST Framework Token Authentication
config.headers['Authorization'] = `Token ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor - handle errors and token refresh
apiClient.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
// Handle 401 Unauthorized - token expired
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
// Try to refresh token (from cookie)
const refreshToken = getCookie('refresh_token');
if (refreshToken) {
const response = await axios.post(`${API_BASE_URL}/api/auth/refresh/`, {
refresh: refreshToken,
});
const { access } = response.data;
// Import setCookie dynamically to avoid circular dependency
const { setCookie } = await import('../utils/cookies');
setCookie('access_token', access, 7);
// Retry original request with new token
if (originalRequest.headers) {
originalRequest.headers['Authorization'] = `Bearer ${access}`;
}
return apiClient(originalRequest);
}
} catch (refreshError) {
// Refresh failed - clear tokens and redirect to login
const { deleteCookie } = await import('../utils/cookies');
deleteCookie('access_token');
deleteCookie('refresh_token');
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
export default apiClient;