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:
113
frontend/src/api/auth.ts
Normal file
113
frontend/src/api/auth.ts
Normal 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;
|
||||
};
|
||||
106
frontend/src/api/business.ts
Normal file
106
frontend/src/api/business.ts
Normal 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;
|
||||
};
|
||||
86
frontend/src/api/client.ts
Normal file
86
frontend/src/api/client.ts
Normal 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;
|
||||
59
frontend/src/api/config.ts
Normal file
59
frontend/src/api/config.ts
Normal 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';
|
||||
};
|
||||
51
frontend/src/api/customDomains.ts
Normal file
51
frontend/src/api/customDomains.ts
Normal 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
181
frontend/src/api/domains.ts
Normal 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
93
frontend/src/api/oauth.ts
Normal 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}/`);
|
||||
};
|
||||
433
frontend/src/api/payments.ts
Normal file
433
frontend/src/api/payments.ts
Normal 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 || {});
|
||||
56
frontend/src/api/platform.ts
Normal file
56
frontend/src/api/platform.ts
Normal 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;
|
||||
};
|
||||
90
frontend/src/api/platformOAuth.ts
Normal file
90
frontend/src/api/platformOAuth.ts
Normal 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
210
frontend/src/api/profile.ts
Normal 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/`);
|
||||
};
|
||||
Reference in New Issue
Block a user