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

113
frontend/src/api/auth.ts Normal file
View File

@@ -0,0 +1,113 @@
/**
* Authentication API
*/
import apiClient from './client';
export interface LoginCredentials {
username: string;
password: string;
}
import { UserRole } from '../types';
export interface MasqueradeStackEntry {
user_id: number;
username: string;
role: UserRole;
business_id?: number;
business_subdomain?: string;
}
export interface LoginResponse {
access: string;
refresh: string;
user: {
id: number;
username: string;
email: string;
name: string;
role: UserRole;
avatar_url?: string;
email_verified?: boolean;
is_staff: boolean;
is_superuser: boolean;
business?: number;
business_name?: string;
business_subdomain?: string;
};
masquerade_stack?: MasqueradeStackEntry[];
}
export interface User {
id: number;
username: string;
email: string;
name: string;
role: UserRole;
avatar_url?: string;
email_verified?: boolean;
is_staff: boolean;
is_superuser: boolean;
business?: number;
business_name?: string;
business_subdomain?: string;
}
/**
* Login user
*/
export const login = async (credentials: LoginCredentials): Promise<LoginResponse> => {
const response = await apiClient.post<LoginResponse>('/api/auth/login/', credentials);
return response.data;
};
/**
* Logout user
*/
export const logout = async (): Promise<void> => {
await apiClient.post('/api/auth/logout/');
};
/**
* Get current user
*/
export const getCurrentUser = async (): Promise<User> => {
const response = await apiClient.get<User>('/api/auth/me/');
return response.data;
};
/**
* Refresh access token
*/
export const refreshToken = async (refresh: string): Promise<{ access: string }> => {
const response = await apiClient.post('/api/auth/refresh/', { refresh });
return response.data;
};
/**
* Masquerade as another user
*/
export const masquerade = async (
username: string,
masquerade_stack?: MasqueradeStackEntry[]
): Promise<LoginResponse> => {
const response = await apiClient.post<LoginResponse>(
`/api/users/${username}/masquerade/`,
{ masquerade_stack }
);
return response.data;
};
/**
* Stop masquerading and return to previous user
*/
export const stopMasquerade = async (
masquerade_stack: MasqueradeStackEntry[]
): Promise<LoginResponse> => {
const response = await apiClient.post<LoginResponse>(
'/api/users/stop_masquerade/',
{ masquerade_stack }
);
return response.data;
};

View File

@@ -0,0 +1,106 @@
/**
* Business API - Resources and Users
*/
import apiClient from './client';
import { User, Resource, BusinessOAuthSettings, BusinessOAuthSettingsResponse, BusinessOAuthCredentials } from '../types';
/**
* Get all resources for the current business
*/
export const getResources = async (): Promise<Resource[]> => {
const response = await apiClient.get<Resource[]>('/api/resources/');
return response.data;
};
/**
* Get all users for the current business
*/
export const getBusinessUsers = async (): Promise<User[]> => {
const response = await apiClient.get<User[]>('/api/business/users/');
return response.data;
};
/**
* Get business OAuth settings and available platform providers
*/
export const getBusinessOAuthSettings = async (): Promise<BusinessOAuthSettingsResponse> => {
const response = await apiClient.get<{
business_settings: {
oauth_enabled_providers: string[];
oauth_allow_registration: boolean;
oauth_auto_link_by_email: boolean;
};
available_providers: string[];
}>('/api/business/oauth-settings/');
// Transform snake_case to camelCase
return {
businessSettings: {
enabledProviders: response.data.business_settings.oauth_enabled_providers || [],
allowRegistration: response.data.business_settings.oauth_allow_registration,
autoLinkByEmail: response.data.business_settings.oauth_auto_link_by_email,
},
availableProviders: response.data.available_providers || [],
};
};
/**
* Update business OAuth settings
*/
export const updateBusinessOAuthSettings = async (
settings: Partial<BusinessOAuthSettings>
): Promise<BusinessOAuthSettingsResponse> => {
// Transform camelCase to snake_case for backend
const backendData: Record<string, any> = {};
if (settings.enabledProviders !== undefined) {
backendData.oauth_enabled_providers = settings.enabledProviders;
}
if (settings.allowRegistration !== undefined) {
backendData.oauth_allow_registration = settings.allowRegistration;
}
if (settings.autoLinkByEmail !== undefined) {
backendData.oauth_auto_link_by_email = settings.autoLinkByEmail;
}
const response = await apiClient.patch<{
business_settings: {
oauth_enabled_providers: string[];
oauth_allow_registration: boolean;
oauth_auto_link_by_email: boolean;
};
available_providers: string[];
}>('/api/business/oauth-settings/update/', backendData);
// Transform snake_case to camelCase
return {
businessSettings: {
enabledProviders: response.data.business_settings.oauth_enabled_providers || [],
allowRegistration: response.data.business_settings.oauth_allow_registration,
autoLinkByEmail: response.data.business_settings.oauth_auto_link_by_email,
},
availableProviders: response.data.available_providers || [],
};
};
/**
* Get business OAuth credentials (custom credentials for paid tiers)
*/
export const getBusinessOAuthCredentials = async (): Promise<BusinessOAuthCredentials> => {
const response = await apiClient.get<BusinessOAuthCredentials>('/api/business/oauth-credentials/');
return response.data;
};
/**
* Update business OAuth credentials (custom credentials for paid tiers)
*/
export const updateBusinessOAuthCredentials = async (
credentials: Partial<BusinessOAuthCredentials>
): Promise<BusinessOAuthCredentials> => {
const response = await apiClient.patch<BusinessOAuthCredentials>(
'/api/business/oauth-credentials/update/',
credentials
);
return response.data;
};

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;

View File

@@ -0,0 +1,59 @@
/**
* API Configuration
* Centralized configuration for API endpoints and settings
*/
// Determine API base URL based on environment
const getApiBaseUrl = (): string => {
// In production, this would be set via environment variable
if (import.meta.env.VITE_API_URL) {
return import.meta.env.VITE_API_URL;
}
// Development: use api subdomain
return 'http://api.lvh.me:8000';
};
export const API_BASE_URL = getApiBaseUrl();
/**
* Extract subdomain from current hostname
* Returns null if on root domain or invalid subdomain
*/
export const getSubdomain = (): string | null => {
const hostname = window.location.hostname;
const parts = hostname.split('.');
// lvh.me without subdomain (root domain) - no business context
if (hostname === 'lvh.me') {
return null;
}
// Has subdomain
if (parts.length > 1) {
const subdomain = parts[0];
// Exclude special subdomains
if (['www', 'api', 'platform'].includes(subdomain)) {
return subdomain === 'platform' ? null : subdomain;
}
return subdomain;
}
return null;
};
/**
* Check if current page is platform site
*/
export const isPlatformSite = (): boolean => {
const hostname = window.location.hostname;
return hostname.startsWith('platform.');
};
/**
* Check if current page is business site
*/
export const isBusinessSite = (): boolean => {
const subdomain = getSubdomain();
return subdomain !== null && subdomain !== 'platform';
};

View File

@@ -0,0 +1,51 @@
/**
* Custom Domains API - Manage custom domains for businesses
*/
import apiClient from './client';
import { CustomDomain } from '../types';
/**
* Get all custom domains for the current business
*/
export const getCustomDomains = async (): Promise<CustomDomain[]> => {
const response = await apiClient.get<CustomDomain[]>('/api/business/domains/');
return response.data;
};
/**
* Add a new custom domain
*/
export const addCustomDomain = async (domain: string): Promise<CustomDomain> => {
const response = await apiClient.post<CustomDomain>('/api/business/domains/', {
domain: domain.toLowerCase().trim(),
});
return response.data;
};
/**
* Delete a custom domain
*/
export const deleteCustomDomain = async (domainId: number): Promise<void> => {
await apiClient.delete(`/api/business/domains/${domainId}/`);
};
/**
* Verify a custom domain by checking DNS
*/
export const verifyCustomDomain = async (domainId: number): Promise<{ verified: boolean; message: string }> => {
const response = await apiClient.post<{ verified: boolean; message: string }>(
`/api/business/domains/${domainId}/verify/`
);
return response.data;
};
/**
* Set a custom domain as the primary domain
*/
export const setPrimaryDomain = async (domainId: number): Promise<CustomDomain> => {
const response = await apiClient.post<CustomDomain>(
`/api/business/domains/${domainId}/set-primary/`
);
return response.data;
};

181
frontend/src/api/domains.ts Normal file
View File

@@ -0,0 +1,181 @@
/**
* Domains API - NameSilo Integration for Domain Registration
*/
import apiClient from './client';
// Types
export interface DomainAvailability {
domain: string;
available: boolean;
price: number | null;
premium: boolean;
premium_price: number | null;
}
export interface DomainPrice {
tld: string;
registration: number;
renewal: number;
transfer: number;
}
export interface RegistrantContact {
first_name: string;
last_name: string;
email: string;
phone: string;
address: string;
city: string;
state: string;
zip_code: string;
country: string;
}
export interface DomainRegisterRequest {
domain: string;
years: number;
whois_privacy: boolean;
auto_renew: boolean;
nameservers?: string[];
contact: RegistrantContact;
auto_configure: boolean;
}
export interface DomainRegistration {
id: number;
domain: string;
status: 'pending' | 'active' | 'expired' | 'transferred' | 'failed';
registered_at: string | null;
expires_at: string | null;
auto_renew: boolean;
whois_privacy: boolean;
purchase_price: number | null;
renewal_price: number | null;
nameservers: string[];
days_until_expiry: number | null;
is_expiring_soon: boolean;
created_at: string;
// Detail fields
registrant_first_name?: string;
registrant_last_name?: string;
registrant_email?: string;
}
export interface DomainSearchHistory {
id: number;
searched_domain: string;
was_available: boolean;
price: number | null;
searched_at: string;
}
// API Functions
/**
* Search for domain availability
*/
export const searchDomains = async (
query: string,
tlds: string[] = ['.com', '.net', '.org']
): Promise<DomainAvailability[]> => {
const response = await apiClient.post<DomainAvailability[]>('/api/domains/search/search/', {
query,
tlds,
});
return response.data;
};
/**
* Get TLD pricing
*/
export const getDomainPrices = async (): Promise<DomainPrice[]> => {
const response = await apiClient.get<DomainPrice[]>('/api/domains/search/prices/');
return response.data;
};
/**
* Register a new domain
*/
export const registerDomain = async (
data: DomainRegisterRequest
): Promise<DomainRegistration> => {
const response = await apiClient.post<DomainRegistration>('/api/domains/search/register/', data);
return response.data;
};
/**
* Get all registered domains for current business
*/
export const getRegisteredDomains = async (): Promise<DomainRegistration[]> => {
const response = await apiClient.get<DomainRegistration[]>('/api/domains/registrations/');
return response.data;
};
/**
* Get a single domain registration
*/
export const getDomainRegistration = async (id: number): Promise<DomainRegistration> => {
const response = await apiClient.get<DomainRegistration>(`/api/domains/registrations/${id}/`);
return response.data;
};
/**
* Update nameservers for a domain
*/
export const updateNameservers = async (
id: number,
nameservers: string[]
): Promise<DomainRegistration> => {
const response = await apiClient.post<DomainRegistration>(
`/api/domains/registrations/${id}/update_nameservers/`,
{ nameservers }
);
return response.data;
};
/**
* Toggle auto-renewal for a domain
*/
export const toggleAutoRenew = async (
id: number,
autoRenew: boolean
): Promise<DomainRegistration> => {
const response = await apiClient.post<DomainRegistration>(
`/api/domains/registrations/${id}/toggle_auto_renew/`,
{ auto_renew: autoRenew }
);
return response.data;
};
/**
* Renew a domain
*/
export const renewDomain = async (
id: number,
years: number = 1
): Promise<DomainRegistration> => {
const response = await apiClient.post<DomainRegistration>(
`/api/domains/registrations/${id}/renew/`,
{ years }
);
return response.data;
};
/**
* Sync domain info from NameSilo
*/
export const syncDomain = async (id: number): Promise<DomainRegistration> => {
const response = await apiClient.post<DomainRegistration>(
`/api/domains/registrations/${id}/sync/`
);
return response.data;
};
/**
* Get domain search history
*/
export const getSearchHistory = async (): Promise<DomainSearchHistory[]> => {
const response = await apiClient.get<DomainSearchHistory[]>('/api/domains/history/');
return response.data;
};

93
frontend/src/api/oauth.ts Normal file
View File

@@ -0,0 +1,93 @@
/**
* OAuth API
* Handles OAuth authentication flows with various providers
*/
import apiClient from './client';
export interface OAuthProvider {
name: string;
display_name: string;
icon: string;
}
export interface OAuthAuthorizationResponse {
authorization_url: string;
}
export interface OAuthTokenResponse {
access: string;
refresh: string;
user: {
id: number;
username: string;
email: string;
name: string;
role: string;
avatar_url?: string;
is_staff: boolean;
is_superuser: boolean;
business?: number;
business_name?: string;
business_subdomain?: string;
};
}
export interface OAuthConnection {
id: string;
provider: string;
provider_user_id: string;
email?: string;
connected_at: string;
}
/**
* Get list of enabled OAuth providers
*/
export const getOAuthProviders = async (): Promise<OAuthProvider[]> => {
const response = await apiClient.get<{ providers: OAuthProvider[] }>('/api/auth/oauth/providers/');
return response.data.providers;
};
/**
* Initiate OAuth flow - get authorization URL
*/
export const initiateOAuth = async (provider: string): Promise<OAuthAuthorizationResponse> => {
const response = await apiClient.get<OAuthAuthorizationResponse>(
`/api/auth/oauth/${provider}/authorize/`
);
return response.data;
};
/**
* Handle OAuth callback - exchange code for tokens
*/
export const handleOAuthCallback = async (
provider: string,
code: string,
state: string
): Promise<OAuthTokenResponse> => {
const response = await apiClient.post<OAuthTokenResponse>(
`/api/auth/oauth/${provider}/callback/`,
{
code,
state,
}
);
return response.data;
};
/**
* Get user's connected OAuth accounts
*/
export const getOAuthConnections = async (): Promise<OAuthConnection[]> => {
const response = await apiClient.get<{ connections: OAuthConnection[] }>('/api/auth/oauth/connections/');
return response.data.connections;
};
/**
* Disconnect an OAuth account
*/
export const disconnectOAuth = async (provider: string): Promise<void> => {
await apiClient.delete(`/api/auth/oauth/connections/${provider}/`);
};

View File

@@ -0,0 +1,433 @@
/**
* Payments API
* Functions for managing payment configuration (API keys and Connect)
*/
import apiClient from './client';
// ============================================================================
// Types
// ============================================================================
export type PaymentMode = 'direct_api' | 'connect' | 'none';
export type KeyStatus = 'active' | 'invalid' | 'deprecated';
export type AccountStatus = 'pending' | 'onboarding' | 'active' | 'restricted' | 'rejected';
export interface ApiKeysInfo {
id: number;
status: KeyStatus;
secret_key_masked: string;
publishable_key_masked: string;
last_validated_at: string | null;
stripe_account_id: string;
stripe_account_name: string;
validation_error: string;
created_at: string;
updated_at: string;
}
export interface ConnectAccountInfo {
id: number;
business: number;
business_name: string;
business_subdomain: string;
stripe_account_id: string;
account_type: 'standard' | 'express' | 'custom';
status: AccountStatus;
charges_enabled: boolean;
payouts_enabled: boolean;
details_submitted: boolean;
onboarding_complete: boolean;
onboarding_link: string | null;
onboarding_link_expires_at: string | null;
is_onboarding_link_valid: boolean;
created_at: string;
updated_at: string;
}
export interface PaymentConfig {
payment_mode: PaymentMode;
tier: string;
can_accept_payments: boolean;
api_keys: ApiKeysInfo | null;
connect_account: ConnectAccountInfo | null;
}
export interface ApiKeysValidationResult {
valid: boolean;
account_id?: string;
account_name?: string;
environment?: string;
error?: string;
}
export interface ApiKeysCurrentResponse {
configured: boolean;
id?: number;
status?: KeyStatus;
secret_key_masked?: string;
publishable_key_masked?: string;
last_validated_at?: string | null;
stripe_account_id?: string;
stripe_account_name?: string;
validation_error?: string;
message?: string;
}
export interface ConnectOnboardingResponse {
account_type: 'standard' | 'custom';
url: string;
stripe_account_id?: string;
}
export interface AccountSessionResponse {
client_secret: string;
stripe_account_id: string;
publishable_key: string;
}
// ============================================================================
// Unified Configuration
// ============================================================================
/**
* Get unified payment configuration status.
* Returns the complete payment setup for the business.
*/
export const getPaymentConfig = () =>
apiClient.get<PaymentConfig>('/api/payments/config/status/');
// ============================================================================
// API Keys (Free Tier)
// ============================================================================
/**
* Get current API key configuration (masked keys).
*/
export const getApiKeys = () =>
apiClient.get<ApiKeysCurrentResponse>('/api/payments/api-keys/');
/**
* Save API keys.
* Validates and stores the provided Stripe API keys.
*/
export const saveApiKeys = (secretKey: string, publishableKey: string) =>
apiClient.post<ApiKeysInfo>('/api/payments/api-keys/', {
secret_key: secretKey,
publishable_key: publishableKey,
});
/**
* Validate API keys without saving.
* Tests the keys against Stripe API.
*/
export const validateApiKeys = (secretKey: string, publishableKey: string) =>
apiClient.post<ApiKeysValidationResult>('/api/payments/api-keys/validate/', {
secret_key: secretKey,
publishable_key: publishableKey,
});
/**
* Re-validate stored API keys.
* Tests stored keys and updates their status.
*/
export const revalidateApiKeys = () =>
apiClient.post<ApiKeysValidationResult>('/api/payments/api-keys/revalidate/');
/**
* Delete stored API keys.
*/
export const deleteApiKeys = () =>
apiClient.delete<{ success: boolean; message: string }>('/api/payments/api-keys/delete/');
// ============================================================================
// Stripe Connect (Paid Tiers)
// ============================================================================
/**
* Get current Connect account status.
*/
export const getConnectStatus = () =>
apiClient.get<ConnectAccountInfo>('/api/payments/connect/status/');
/**
* Initiate Connect account onboarding.
* Returns a URL to redirect the user for Stripe onboarding.
*/
export const initiateConnectOnboarding = (refreshUrl: string, returnUrl: string) =>
apiClient.post<ConnectOnboardingResponse>('/api/payments/connect/onboard/', {
refresh_url: refreshUrl,
return_url: returnUrl,
});
/**
* Refresh Connect onboarding link.
* For custom Connect accounts that need a new onboarding link.
*/
export const refreshConnectOnboardingLink = (refreshUrl: string, returnUrl: string) =>
apiClient.post<{ url: string }>('/api/payments/connect/refresh-link/', {
refresh_url: refreshUrl,
return_url: returnUrl,
});
/**
* Create an Account Session for embedded Connect onboarding.
* Returns a client_secret for initializing Stripe's embedded Connect components.
*/
export const createAccountSession = () =>
apiClient.post<AccountSessionResponse>('/api/payments/connect/account-session/');
/**
* Refresh Connect account status from Stripe.
* Syncs the local account record with the current state in Stripe.
*/
export const refreshConnectStatus = () =>
apiClient.post<ConnectAccountInfo>('/api/payments/connect/refresh-status/');
// ============================================================================
// Transaction Analytics
// ============================================================================
export interface Transaction {
id: number;
business: number;
business_name: string;
stripe_payment_intent_id: string;
stripe_charge_id: string;
transaction_type: 'payment' | 'refund' | 'application_fee';
status: 'pending' | 'succeeded' | 'failed' | 'refunded' | 'partially_refunded';
amount: number;
amount_display: string;
application_fee_amount: number;
fee_display: string;
net_amount: number;
currency: string;
customer_email: string;
customer_name: string;
created_at: string;
updated_at: string;
stripe_data?: Record<string, unknown>;
}
export interface TransactionListResponse {
results: Transaction[];
count: number;
page: number;
page_size: number;
total_pages: number;
}
export interface TransactionSummary {
total_transactions: number;
total_volume: number;
total_volume_display: string;
total_fees: number;
total_fees_display: string;
net_revenue: number;
net_revenue_display: string;
successful_transactions: number;
failed_transactions: number;
refunded_transactions: number;
average_transaction: number;
average_transaction_display: string;
}
export interface TransactionFilters {
start_date?: string;
end_date?: string;
status?: 'all' | 'succeeded' | 'pending' | 'failed' | 'refunded';
transaction_type?: 'all' | 'payment' | 'refund' | 'application_fee';
page?: number;
page_size?: number;
}
export interface StripeCharge {
id: string;
amount: number;
amount_display: string;
amount_refunded: number;
currency: string;
status: string;
paid: boolean;
refunded: boolean;
description: string | null;
receipt_email: string | null;
receipt_url: string | null;
created: number;
payment_method_details: Record<string, unknown> | null;
billing_details: Record<string, unknown> | null;
}
export interface ChargesResponse {
charges: StripeCharge[];
has_more: boolean;
}
export interface StripePayout {
id: string;
amount: number;
amount_display: string;
currency: string;
status: string;
arrival_date: number | null;
created: number;
description: string | null;
destination: string | null;
failure_message: string | null;
method: string;
type: string;
}
export interface PayoutsResponse {
payouts: StripePayout[];
has_more: boolean;
}
export interface BalanceItem {
amount: number;
currency: string;
amount_display: string;
}
export interface BalanceResponse {
available: BalanceItem[];
pending: BalanceItem[];
available_total: number;
pending_total: number;
}
export interface ExportRequest {
format: 'csv' | 'xlsx' | 'pdf' | 'quickbooks';
start_date?: string;
end_date?: string;
include_details?: boolean;
}
/**
* Get list of transactions with optional filtering.
*/
export const getTransactions = (filters?: TransactionFilters) => {
const params = new URLSearchParams();
if (filters?.start_date) params.append('start_date', filters.start_date);
if (filters?.end_date) params.append('end_date', filters.end_date);
if (filters?.status && filters.status !== 'all') params.append('status', filters.status);
if (filters?.transaction_type && filters.transaction_type !== 'all') {
params.append('transaction_type', filters.transaction_type);
}
if (filters?.page) params.append('page', String(filters.page));
if (filters?.page_size) params.append('page_size', String(filters.page_size));
const queryString = params.toString();
return apiClient.get<TransactionListResponse>(
`/api/payments/transactions/${queryString ? `?${queryString}` : ''}`
);
};
/**
* Get a single transaction by ID.
*/
export const getTransaction = (id: number) =>
apiClient.get<Transaction>(`/api/payments/transactions/${id}/`);
/**
* Get transaction summary/analytics.
*/
export const getTransactionSummary = (filters?: Pick<TransactionFilters, 'start_date' | 'end_date'>) => {
const params = new URLSearchParams();
if (filters?.start_date) params.append('start_date', filters.start_date);
if (filters?.end_date) params.append('end_date', filters.end_date);
const queryString = params.toString();
return apiClient.get<TransactionSummary>(
`/api/payments/transactions/summary/${queryString ? `?${queryString}` : ''}`
);
};
/**
* Get charges from Stripe API.
*/
export const getStripeCharges = (limit: number = 20) =>
apiClient.get<ChargesResponse>(`/api/payments/transactions/charges/?limit=${limit}`);
/**
* Get payouts from Stripe API.
*/
export const getStripePayouts = (limit: number = 20) =>
apiClient.get<PayoutsResponse>(`/api/payments/transactions/payouts/?limit=${limit}`);
/**
* Get current balance from Stripe API.
*/
export const getStripeBalance = () =>
apiClient.get<BalanceResponse>('/api/payments/transactions/balance/');
/**
* Export transaction data.
* Returns the file data directly for download.
*/
export const exportTransactions = (request: ExportRequest) =>
apiClient.post('/api/payments/transactions/export/', request, {
responseType: 'blob',
});
// ============================================================================
// Transaction Details & Refunds
// ============================================================================
export interface RefundInfo {
id: string;
amount: number;
amount_display: string;
status: string;
reason: string | null;
created: number;
}
export interface PaymentMethodInfo {
type: string;
brand?: string;
last4?: string;
exp_month?: number;
exp_year?: number;
funding?: string;
bank_name?: string;
}
export interface TransactionDetail extends Transaction {
refunds: RefundInfo[];
refundable_amount: number;
total_refunded: number;
can_refund: boolean;
payment_method_info: PaymentMethodInfo | null;
description: string;
}
export interface RefundRequest {
amount?: number;
reason?: 'duplicate' | 'fraudulent' | 'requested_by_customer';
metadata?: Record<string, string>;
}
export interface RefundResponse {
success: boolean;
refund_id: string;
amount: number;
amount_display: string;
status: string;
reason: string | null;
transaction_status: string;
}
/**
* Get detailed transaction information including refund data.
*/
export const getTransactionDetail = (id: number) =>
apiClient.get<TransactionDetail>(`/api/payments/transactions/${id}/`);
/**
* Issue a refund for a transaction.
* @param transactionId - The ID of the transaction to refund
* @param request - Optional refund request with amount and reason
*/
export const refundTransaction = (transactionId: number, request?: RefundRequest) =>
apiClient.post<RefundResponse>(`/api/payments/transactions/${transactionId}/refund/`, request || {});

View File

@@ -0,0 +1,56 @@
/**
* Platform API
* API functions for platform-level operations (businesses, users, etc.)
*/
import apiClient from './client';
export interface PlatformBusiness {
id: number;
name: string;
subdomain: string;
tier: string;
is_active: boolean;
created_at: string;
user_count: number;
}
export interface PlatformUser {
id: number;
email: string;
username: string;
name?: string;
role?: string;
is_active: boolean;
is_staff: boolean;
is_superuser: boolean;
business: number | null;
business_name?: string;
business_subdomain?: string;
date_joined: string;
last_login?: string;
}
/**
* Get all businesses (platform admin only)
*/
export const getBusinesses = async (): Promise<PlatformBusiness[]> => {
const response = await apiClient.get<PlatformBusiness[]>('/api/platform/businesses/');
return response.data;
};
/**
* Get all users (platform admin only)
*/
export const getUsers = async (): Promise<PlatformUser[]> => {
const response = await apiClient.get<PlatformUser[]>('/api/platform/users/');
return response.data;
};
/**
* Get users for a specific business
*/
export const getBusinessUsers = async (businessId: number): Promise<PlatformUser[]> => {
const response = await apiClient.get<PlatformUser[]>(`/api/platform/users/?business=${businessId}`);
return response.data;
};

View File

@@ -0,0 +1,90 @@
/**
* Platform OAuth Settings API
*/
import apiClient from './client';
export interface OAuthProviderConfig {
enabled: boolean;
client_id: string;
client_secret: string;
// Apple-specific fields
team_id?: string;
key_id?: string;
// Microsoft-specific field
tenant_id?: string;
}
export interface PlatformOAuthSettings {
// Global setting
oauth_allow_registration: boolean;
// Provider configurations
google: OAuthProviderConfig;
apple: OAuthProviderConfig;
facebook: OAuthProviderConfig;
linkedin: OAuthProviderConfig;
microsoft: OAuthProviderConfig;
twitter: OAuthProviderConfig;
twitch: OAuthProviderConfig;
}
export interface PlatformOAuthSettingsUpdate {
oauth_allow_registration?: boolean;
// Google
oauth_google_enabled?: boolean;
oauth_google_client_id?: string;
oauth_google_client_secret?: string;
// Apple
oauth_apple_enabled?: boolean;
oauth_apple_client_id?: string;
oauth_apple_client_secret?: string;
oauth_apple_team_id?: string;
oauth_apple_key_id?: string;
// Facebook
oauth_facebook_enabled?: boolean;
oauth_facebook_client_id?: string;
oauth_facebook_client_secret?: string;
// LinkedIn
oauth_linkedin_enabled?: boolean;
oauth_linkedin_client_id?: string;
oauth_linkedin_client_secret?: string;
// Microsoft
oauth_microsoft_enabled?: boolean;
oauth_microsoft_client_id?: string;
oauth_microsoft_client_secret?: string;
oauth_microsoft_tenant_id?: string;
// Twitter (X)
oauth_twitter_enabled?: boolean;
oauth_twitter_client_id?: string;
oauth_twitter_client_secret?: string;
// Twitch
oauth_twitch_enabled?: boolean;
oauth_twitch_client_id?: string;
oauth_twitch_client_secret?: string;
}
/**
* Get platform OAuth settings
*/
export const getPlatformOAuthSettings = async (): Promise<PlatformOAuthSettings> => {
const { data } = await apiClient.get('/api/platform/settings/oauth/');
return data;
};
/**
* Update platform OAuth settings
*/
export const updatePlatformOAuthSettings = async (
settings: PlatformOAuthSettingsUpdate
): Promise<PlatformOAuthSettings> => {
const { data } = await apiClient.post('/api/platform/settings/oauth/', settings);
return data;
};

210
frontend/src/api/profile.ts Normal file
View File

@@ -0,0 +1,210 @@
import apiClient from './client';
// Types
export interface UserProfile {
id: number;
username: string;
email: string;
name: string;
phone?: string;
phone_verified: boolean;
avatar_url?: string;
email_verified: boolean;
two_factor_enabled: boolean;
totp_confirmed: boolean;
sms_2fa_enabled: boolean;
timezone: string;
locale: string;
notification_preferences: NotificationPreferences;
role: string;
business?: number;
business_name?: string;
business_subdomain?: string;
// Address fields
address_line1?: string;
address_line2?: string;
city?: string;
state?: string;
postal_code?: string;
country?: string;
}
export interface NotificationPreferences {
email: boolean;
sms: boolean;
in_app: boolean;
appointment_reminders: boolean;
marketing: boolean;
}
export interface TOTPSetupResponse {
secret: string;
qr_code: string; // Base64 encoded PNG
provisioning_uri: string;
}
export interface TOTPVerifyResponse {
success: boolean;
recovery_codes: string[];
}
export interface Session {
id: string;
device_info: string;
ip_address: string;
location: string;
created_at: string;
last_activity: string;
is_current: boolean;
}
export interface LoginHistoryEntry {
id: string;
timestamp: string;
ip_address: string;
device_info: string;
location: string;
success: boolean;
failure_reason?: string;
two_factor_method?: string;
}
// Profile API
export const getProfile = async (): Promise<UserProfile> => {
const response = await apiClient.get('/api/auth/profile/');
return response.data;
};
export const updateProfile = async (data: Partial<UserProfile>): Promise<UserProfile> => {
const response = await apiClient.patch('/api/auth/profile/', data);
return response.data;
};
export const uploadAvatar = async (file: File): Promise<{ avatar_url: string }> => {
const formData = new FormData();
formData.append('avatar', file);
const response = await apiClient.post('/api/auth/profile/avatar/', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return response.data;
};
export const deleteAvatar = async (): Promise<void> => {
await apiClient.delete('/api/auth/profile/avatar/');
};
// Email API
export const sendVerificationEmail = async (): Promise<void> => {
await apiClient.post('/api/auth/email/verify/send/');
};
export const verifyEmail = async (token: string): Promise<void> => {
await apiClient.post('/api/auth/email/verify/confirm/', { token });
};
export const requestEmailChange = async (newEmail: string): Promise<void> => {
await apiClient.post('/api/auth/email/change/', { new_email: newEmail });
};
export const confirmEmailChange = async (token: string): Promise<void> => {
await apiClient.post('/api/auth/email/change/confirm/', { token });
};
// Password API
export const changePassword = async (
currentPassword: string,
newPassword: string
): Promise<void> => {
await apiClient.post('/api/auth/password/change/', {
current_password: currentPassword,
new_password: newPassword,
});
};
// 2FA API
export const setupTOTP = async (): Promise<TOTPSetupResponse> => {
const response = await apiClient.post('/api/auth/2fa/totp/setup/');
return response.data;
};
export const verifyTOTP = async (code: string): Promise<TOTPVerifyResponse> => {
const response = await apiClient.post('/api/auth/2fa/totp/verify/', { code });
return response.data;
};
export const disableTOTP = async (code: string): Promise<void> => {
await apiClient.post('/api/auth/2fa/totp/disable/', { code });
};
export const getRecoveryCodes = async (): Promise<string[]> => {
const response = await apiClient.get('/api/auth/2fa/recovery-codes/');
return response.data.codes;
};
export const regenerateRecoveryCodes = async (): Promise<string[]> => {
const response = await apiClient.post('/api/auth/2fa/recovery-codes/regenerate/');
return response.data.codes;
};
// Sessions API
export const getSessions = async (): Promise<Session[]> => {
const response = await apiClient.get('/api/auth/sessions/');
return response.data;
};
export const revokeSession = async (sessionId: string): Promise<void> => {
await apiClient.delete(`/api/auth/sessions/${sessionId}/`);
};
export const revokeOtherSessions = async (): Promise<void> => {
await apiClient.post('/api/auth/sessions/revoke-others/');
};
export const getLoginHistory = async (): Promise<LoginHistoryEntry[]> => {
const response = await apiClient.get('/api/auth/login-history/');
return response.data;
};
// Phone Verification API
export const sendPhoneVerification = async (phone: string): Promise<void> => {
await apiClient.post('/api/auth/phone/verify/send/', { phone });
};
export const verifyPhoneCode = async (code: string): Promise<void> => {
await apiClient.post('/api/auth/phone/verify/confirm/', { code });
};
// Multiple Email Management API
export interface UserEmail {
id: number;
email: string;
is_primary: boolean;
verified: boolean;
created_at: string;
}
export const getUserEmails = async (): Promise<UserEmail[]> => {
const response = await apiClient.get('/api/auth/emails/');
return response.data;
};
export const addUserEmail = async (email: string): Promise<UserEmail> => {
const response = await apiClient.post('/api/auth/emails/', { email });
return response.data;
};
export const deleteUserEmail = async (emailId: number): Promise<void> => {
await apiClient.delete(`/api/auth/emails/${emailId}/`);
};
export const sendUserEmailVerification = async (emailId: number): Promise<void> => {
await apiClient.post(`/api/auth/emails/${emailId}/send-verification/`);
};
export const verifyUserEmail = async (emailId: number, token: string): Promise<void> => {
await apiClient.post(`/api/auth/emails/${emailId}/verify/`, { token });
};
export const setPrimaryEmail = async (emailId: number): Promise<void> => {
await apiClient.post(`/api/auth/emails/${emailId}/set-primary/`);
};