diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/frontend/.env.production b/frontend/.env.production index 98574e2..398f6df 100644 --- a/frontend/.env.production +++ b/frontend/.env.production @@ -1,3 +1,3 @@ # Production environment variables -# Set VITE_API_URL to your production API URL -VITE_API_URL=https://smoothschedule.com +# Use relative API URL - will use same origin as the page +VITE_API_URL=/api diff --git a/frontend/playwright-report/data/26a8490232a31d7ac35c48d597d4cf1e7b398d00.md b/frontend/playwright-report/data/26a8490232a31d7ac35c48d597d4cf1e7b398d00.md deleted file mode 100644 index 3504e40..0000000 --- a/frontend/playwright-report/data/26a8490232a31d7ac35c48d597d4cf1e7b398d00.md +++ /dev/null @@ -1,84 +0,0 @@ -# Page snapshot - -```yaml -- generic [ref=e3]: - - generic [ref=e7]: - - generic [ref=e9]: - - img [ref=e10] - - generic [ref=e16]: Smooth Schedule - - generic [ref=e17]: - - heading "Orchestrate your business with precision." [level=1] [ref=e18] - - paragraph [ref=e19]: The all-in-one scheduling platform for businesses of all sizes. Manage resources, staff, and bookings effortlessly. - - generic [ref=e24]: © 2025 Smooth Schedule Inc. - - generic [ref=e26]: - - generic [ref=e27]: - - heading "Welcome back" [level=2] [ref=e28] - - paragraph [ref=e29]: Please enter your details to sign in. - - generic [ref=e30]: - - generic [ref=e31]: - - generic [ref=e32]: - - generic [ref=e33]: Username - - generic [ref=e34]: - - generic: - - img - - textbox "Username" [active] [ref=e35]: - - /placeholder: Enter your username - - text: superuser - - generic [ref=e36]: - - generic [ref=e37]: Password - - generic [ref=e38]: - - generic: - - img - - textbox "Password" [ref=e39]: - - /placeholder: •••••••• - - button "Sign in" [ref=e40]: - - generic [ref=e41]: - - text: Sign in - - img [ref=e42] - - generic [ref=e50]: Or continue with - - button "🇺🇸 English" [ref=e53]: - - img [ref=e54] - - generic [ref=e58]: 🇺🇸 - - generic [ref=e59]: English - - img [ref=e60] - - generic [ref=e62]: - - heading "🔓 Quick Login (Dev Only)" [level=3] [ref=e64]: - - generic [ref=e65]: 🔓 - - generic [ref=e66]: Quick Login (Dev Only) - - generic [ref=e67]: - - button "Platform Superuser SUPERUSER" [ref=e68]: - - generic [ref=e69]: - - generic [ref=e70]: Platform Superuser - - generic [ref=e71]: SUPERUSER - - button "Platform Manager PLATFORM_MANAGER" [ref=e72]: - - generic [ref=e73]: - - generic [ref=e74]: Platform Manager - - generic [ref=e75]: PLATFORM_MANAGER - - button "Platform Sales PLATFORM_SALES" [ref=e76]: - - generic [ref=e77]: - - generic [ref=e78]: Platform Sales - - generic [ref=e79]: PLATFORM_SALES - - button "Platform Support PLATFORM_SUPPORT" [ref=e80]: - - generic [ref=e81]: - - generic [ref=e82]: Platform Support - - generic [ref=e83]: PLATFORM_SUPPORT - - button "Business Owner TENANT_OWNER" [ref=e84]: - - generic [ref=e85]: - - generic [ref=e86]: Business Owner - - generic [ref=e87]: TENANT_OWNER - - button "Business Manager TENANT_MANAGER" [ref=e88]: - - generic [ref=e89]: - - generic [ref=e90]: Business Manager - - generic [ref=e91]: TENANT_MANAGER - - button "Staff Member TENANT_STAFF" [ref=e92]: - - generic [ref=e93]: - - generic [ref=e94]: Staff Member - - generic [ref=e95]: TENANT_STAFF - - button "Customer CUSTOMER" [ref=e96]: - - generic [ref=e97]: - - generic [ref=e98]: Customer - - generic [ref=e99]: CUSTOMER - - generic [ref=e100]: - - text: "Password for all:" - - code [ref=e101]: test123 -``` \ No newline at end of file diff --git a/frontend/playwright-report/data/2f08f431e3b3b985364cd06af0a236e74b243e90.png b/frontend/playwright-report/data/2f08f431e3b3b985364cd06af0a236e74b243e90.png deleted file mode 100644 index 1da97a5..0000000 Binary files a/frontend/playwright-report/data/2f08f431e3b3b985364cd06af0a236e74b243e90.png and /dev/null differ diff --git a/frontend/playwright-report/data/3e9ea161bef287bc24525c872839cd9b70824220.md b/frontend/playwright-report/data/3e9ea161bef287bc24525c872839cd9b70824220.md deleted file mode 100644 index 2476c8b..0000000 --- a/frontend/playwright-report/data/3e9ea161bef287bc24525c872839cd9b70824220.md +++ /dev/null @@ -1,84 +0,0 @@ -# Page snapshot - -```yaml -- generic [ref=e3]: - - generic [ref=e7]: - - generic [ref=e9]: - - img [ref=e10] - - generic [ref=e16]: Smooth Schedule - - generic [ref=e17]: - - heading "Orchestrate your business with precision." [level=1] [ref=e18] - - paragraph [ref=e19]: The all-in-one scheduling platform for businesses of all sizes. Manage resources, staff, and bookings effortlessly. - - generic [ref=e24]: © 2025 Smooth Schedule Inc. - - generic [ref=e26]: - - generic [ref=e27]: - - heading "Welcome back" [level=2] [ref=e28] - - paragraph [ref=e29]: Please enter your details to sign in. - - generic [ref=e30]: - - generic [ref=e31]: - - generic [ref=e32]: - - generic [ref=e33]: Username - - generic [ref=e34]: - - generic: - - img - - textbox "Username" [active] [ref=e35]: - - /placeholder: Enter your username - - text: superuser - - generic [ref=e36]: - - generic [ref=e37]: Password - - generic [ref=e38]: - - generic: - - img - - textbox "Password" [ref=e39]: - - /placeholder: •••••••• - - button "Sign in" [ref=e40]: - - generic [ref=e41]: - - text: Sign in - - img [ref=e42] - - generic [ref=e49]: Or continue with - - button "🇺🇸 English" [ref=e52]: - - img [ref=e53] - - generic [ref=e56]: 🇺🇸 - - generic [ref=e57]: English - - img [ref=e58] - - generic [ref=e60]: - - heading "🔓 Quick Login (Dev Only)" [level=3] [ref=e62]: - - generic [ref=e63]: 🔓 - - generic [ref=e64]: Quick Login (Dev Only) - - generic [ref=e65]: - - button "Platform Superuser SUPERUSER" [ref=e66]: - - generic [ref=e67]: - - generic [ref=e68]: Platform Superuser - - generic [ref=e69]: SUPERUSER - - button "Platform Manager PLATFORM_MANAGER" [ref=e70]: - - generic [ref=e71]: - - generic [ref=e72]: Platform Manager - - generic [ref=e73]: PLATFORM_MANAGER - - button "Platform Sales PLATFORM_SALES" [ref=e74]: - - generic [ref=e75]: - - generic [ref=e76]: Platform Sales - - generic [ref=e77]: PLATFORM_SALES - - button "Platform Support PLATFORM_SUPPORT" [ref=e78]: - - generic [ref=e79]: - - generic [ref=e80]: Platform Support - - generic [ref=e81]: PLATFORM_SUPPORT - - button "Business Owner TENANT_OWNER" [ref=e82]: - - generic [ref=e83]: - - generic [ref=e84]: Business Owner - - generic [ref=e85]: TENANT_OWNER - - button "Business Manager TENANT_MANAGER" [ref=e86]: - - generic [ref=e87]: - - generic [ref=e88]: Business Manager - - generic [ref=e89]: TENANT_MANAGER - - button "Staff Member TENANT_STAFF" [ref=e90]: - - generic [ref=e91]: - - generic [ref=e92]: Staff Member - - generic [ref=e93]: TENANT_STAFF - - button "Customer CUSTOMER" [ref=e94]: - - generic [ref=e95]: - - generic [ref=e96]: Customer - - generic [ref=e97]: CUSTOMER - - generic [ref=e98]: - - text: "Password for all:" - - code [ref=e99]: test123 -``` \ No newline at end of file diff --git a/frontend/playwright-report/data/b12753f2e5ebba05959c238fad5732ef12da1299.png b/frontend/playwright-report/data/b12753f2e5ebba05959c238fad5732ef12da1299.png deleted file mode 100644 index d2cbd49..0000000 Binary files a/frontend/playwright-report/data/b12753f2e5ebba05959c238fad5732ef12da1299.png and /dev/null differ diff --git a/frontend/playwright-report/data/d822700a7efe85e9f2b6c736f6649e4f55bf86b8.png b/frontend/playwright-report/data/d822700a7efe85e9f2b6c736f6649e4f55bf86b8.png deleted file mode 100644 index 03a1fb2..0000000 Binary files a/frontend/playwright-report/data/d822700a7efe85e9f2b6c736f6649e4f55bf86b8.png and /dev/null differ diff --git a/frontend/playwright-report/index.html b/frontend/playwright-report/index.html index 6b7d45d..70d7d0f 100644 --- a/frontend/playwright-report/index.html +++ b/frontend/playwright-report/index.html @@ -82,4 +82,4 @@ Error generating stack: `+n.message+`
- \ No newline at end of file + \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ca52806..78e9406 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -188,11 +188,6 @@ const AppContent: React.FC = () => { setCookie('access_token', accessToken, 7); setCookie('refresh_token', refreshToken, 7); - // Clear session cookie to prevent interference with JWT - // (Django session cookie might take precedence over JWT) - document.cookie = 'sessionid=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.lvh.me'; - document.cookie = 'sessionid=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; - // Clean URL const newUrl = window.location.pathname + window.location.hash; window.history.replaceState({}, '', newUrl); @@ -215,7 +210,9 @@ const AppContent: React.FC = () => { // Helper to detect root domain (for marketing site) const isRootDomain = (): boolean => { const hostname = window.location.hostname; - return hostname === 'lvh.me' || hostname === 'localhost' || hostname === '127.0.0.1'; + // Root domain has no subdomain (just the base domain like smoothschedule.com or lvh.me) + const parts = hostname.split('.'); + return hostname === 'localhost' || hostname === '127.0.0.1' || parts.length === 2; }; // On root domain, ALWAYS show marketing site (even if logged in) @@ -242,8 +239,23 @@ const AppContent: React.FC = () => { ); } - // Not authenticated - show marketing pages + // Not authenticated - redirect to root domain for login if on subdomain if (!user) { + // If on a subdomain, redirect to root domain login page + const currentHostname = window.location.hostname; + const hostnameParts = currentHostname.split('.'); + const baseDomain = hostnameParts.length >= 2 + ? hostnameParts.slice(-2).join('.') + : currentHostname; + const isRootDomainForUnauthUser = currentHostname === baseDomain || currentHostname === 'localhost'; + + if (!isRootDomainForUnauthUser) { + // Redirect to root domain login + const protocol = window.location.protocol; + window.location.href = `${protocol}//${baseDomain}/login`; + return ; + } + return ( }> @@ -272,38 +284,43 @@ const AppContent: React.FC = () => { // Subdomain validation for logged-in users const currentHostname = window.location.hostname; - const isPlatformDomain = currentHostname === 'platform.lvh.me'; - const currentSubdomain = currentHostname.split('.')[0]; - const isBusinessSubdomain = !isRootDomain() && !isPlatformDomain && currentSubdomain !== 'api'; + const hostnameParts = currentHostname.split('.'); + const baseDomain = hostnameParts.length >= 2 + ? hostnameParts.slice(-2).join('.') + : currentHostname; + const protocol = window.location.protocol; + const isPlatformDomain = currentHostname === `platform.${baseDomain}`; + const currentSubdomain = hostnameParts[0]; + const isBusinessSubdomain = !isRootDomain() && !isPlatformDomain && currentSubdomain !== 'api' && currentHostname !== baseDomain; const isPlatformUser = ['superuser', 'platform_manager', 'platform_support'].includes(user.role); const isBusinessUser = ['owner', 'manager', 'staff', 'resource'].includes(user.role); const isCustomer = user.role === 'customer'; - // RULE: Platform users must be on platform subdomain (not business subdomains) + // RULE: Platform users on business subdomains should be redirected to platform subdomain if (isPlatformUser && isBusinessSubdomain) { const port = window.location.port ? `:${window.location.port}` : ''; - window.location.href = `http://platform.lvh.me${port}/`; + window.location.href = `${protocol}//platform.${baseDomain}${port}/`; return ; } // RULE: Business users must be on their own business subdomain if (isBusinessUser && isBusinessSubdomain && user.business_subdomain && user.business_subdomain !== currentSubdomain) { const port = window.location.port ? `:${window.location.port}` : ''; - window.location.href = `http://${user.business_subdomain}.lvh.me${port}/`; + window.location.href = `${protocol}//${user.business_subdomain}.${baseDomain}${port}/`; return ; } // RULE: Customers must be on their business subdomain if (isCustomer && isPlatformDomain && user.business_subdomain) { const port = window.location.port ? `:${window.location.port}` : ''; - window.location.href = `http://${user.business_subdomain}.lvh.me${port}/`; + window.location.href = `${protocol}//${user.business_subdomain}.${baseDomain}${port}/`; return ; } if (isCustomer && isBusinessSubdomain && user.business_subdomain && user.business_subdomain !== currentSubdomain) { const port = window.location.port ? `:${window.location.port}` : ''; - window.location.href = `http://${user.business_subdomain}.lvh.me${port}/`; + window.location.href = `${protocol}//${user.business_subdomain}.${baseDomain}${port}/`; return ; } @@ -439,8 +456,7 @@ const AppContent: React.FC = () => { if (businessError || !business) { // If user has a business subdomain, redirect them there if (user.business_subdomain) { - const port = window.location.port ? `:${window.location.port}` : ''; - window.location.href = `http://${user.business_subdomain}.lvh.me${port}/`; + window.location.href = buildSubdomainUrl(user.business_subdomain, '/'); return ; } diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts index 05fdb07..0aeda74 100644 --- a/frontend/src/api/auth.ts +++ b/frontend/src/api/auth.ts @@ -64,7 +64,7 @@ export interface User { * Login user */ export const login = async (credentials: LoginCredentials): Promise => { - const response = await apiClient.post('/api/auth/login/', credentials); + const response = await apiClient.post('/auth/login/', credentials); return response.data; }; @@ -72,14 +72,14 @@ export const login = async (credentials: LoginCredentials): Promise => { - await apiClient.post('/api/auth/logout/'); + await apiClient.post('/auth/logout/'); }; /** * Get current user */ export const getCurrentUser = async (): Promise => { - const response = await apiClient.get('/api/auth/me/'); + const response = await apiClient.get('/auth/me/'); return response.data; }; @@ -87,7 +87,7 @@ export const getCurrentUser = async (): Promise => { * Refresh access token */ export const refreshToken = async (refresh: string): Promise<{ access: string }> => { - const response = await apiClient.post('/api/auth/refresh/', { refresh }); + const response = await apiClient.post('/auth/refresh/', { refresh }); return response.data; }; @@ -99,7 +99,7 @@ export const masquerade = async ( hijack_history?: MasqueradeStackEntry[] ): Promise => { const response = await apiClient.post( - '/api/auth/hijack/acquire/', + '/auth/hijack/acquire/', { user_pk, hijack_history } ); return response.data; @@ -112,7 +112,7 @@ export const stopMasquerade = async ( masquerade_stack: MasqueradeStackEntry[] ): Promise => { const response = await apiClient.post( - '/api/auth/hijack/release/', + '/auth/hijack/release/', { masquerade_stack } ); return response.data; diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 016f88e..dc44613 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -71,7 +71,7 @@ apiClient.interceptors.response.use( // Try to refresh token (from cookie) const refreshToken = getCookie('refresh_token'); if (refreshToken) { - const response = await axios.post(`${API_BASE_URL}/api/auth/refresh/`, { + const response = await axios.post(`${API_BASE_URL}/auth/refresh/`, { refresh: refreshToken, }); diff --git a/frontend/src/api/config.ts b/frontend/src/api/config.ts index aa651ce..3dbb89f 100644 --- a/frontend/src/api/config.ts +++ b/frontend/src/api/config.ts @@ -3,6 +3,8 @@ * Centralized configuration for API endpoints and settings */ +import { getBaseDomain, isRootDomain } from '../utils/domain'; + // Determine API base URL based on environment const getApiBaseUrl = (): string => { // In production, this would be set via environment variable @@ -10,8 +12,15 @@ const getApiBaseUrl = (): string => { return import.meta.env.VITE_API_URL; } - // Development: use api subdomain - return 'http://api.lvh.me:8000'; + // Development: build API URL dynamically based on current domain + const baseDomain = getBaseDomain(); + const protocol = window.location.protocol; + + // For localhost or lvh.me, use port 8000 + const isDev = baseDomain === 'localhost' || baseDomain === 'lvh.me'; + const port = isDev ? ':8000' : ''; + + return `${protocol}//api.${baseDomain}${port}`; }; export const API_BASE_URL = getApiBaseUrl(); @@ -24,8 +33,8 @@ 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') { + // Root domain (no subdomain) - no business context + if (isRootDomain()) { return null; } diff --git a/frontend/src/api/mfa.ts b/frontend/src/api/mfa.ts index 31c4833..9efbf69 100644 --- a/frontend/src/api/mfa.ts +++ b/frontend/src/api/mfa.ts @@ -90,7 +90,7 @@ export interface MFAVerifyResponse { * Get current MFA status */ export const getMFAStatus = async (): Promise => { - const response = await apiClient.get('/api/auth/mfa/status/'); + const response = await apiClient.get('/auth/mfa/status/'); return response.data; }; @@ -102,7 +102,7 @@ export const getMFAStatus = async (): Promise => { * Send phone verification code */ export const sendPhoneVerification = async (phone: string): Promise<{ success: boolean; message: string }> => { - const response = await apiClient.post('/api/auth/mfa/phone/send/', { phone }); + const response = await apiClient.post('/auth/mfa/phone/send/', { phone }); return response.data; }; @@ -110,7 +110,7 @@ export const sendPhoneVerification = async (phone: string): Promise<{ success: b * Verify phone number with code */ export const verifyPhone = async (code: string): Promise<{ success: boolean; message: string }> => { - const response = await apiClient.post('/api/auth/mfa/phone/verify/', { code }); + const response = await apiClient.post('/auth/mfa/phone/verify/', { code }); return response.data; }; @@ -118,7 +118,7 @@ export const verifyPhone = async (code: string): Promise<{ success: boolean; mes * Enable SMS MFA (requires verified phone) */ export const enableSMSMFA = async (): Promise => { - const response = await apiClient.post('/api/auth/mfa/sms/enable/'); + const response = await apiClient.post('/auth/mfa/sms/enable/'); return response.data; }; @@ -130,7 +130,7 @@ export const enableSMSMFA = async (): Promise => { * Initialize TOTP setup (returns QR code and secret) */ export const setupTOTP = async (): Promise => { - const response = await apiClient.post('/api/auth/mfa/totp/setup/'); + const response = await apiClient.post('/auth/mfa/totp/setup/'); return response.data; }; @@ -138,7 +138,7 @@ export const setupTOTP = async (): Promise => { * Verify TOTP code to complete setup */ export const verifyTOTPSetup = async (code: string): Promise => { - const response = await apiClient.post('/api/auth/mfa/totp/verify/', { code }); + const response = await apiClient.post('/auth/mfa/totp/verify/', { code }); return response.data; }; @@ -150,7 +150,7 @@ export const verifyTOTPSetup = async (code: string): Promise * Generate new backup codes (invalidates old ones) */ export const generateBackupCodes = async (): Promise => { - const response = await apiClient.post('/api/auth/mfa/backup-codes/'); + const response = await apiClient.post('/auth/mfa/backup-codes/'); return response.data; }; @@ -158,7 +158,7 @@ export const generateBackupCodes = async (): Promise => { * Get backup codes status */ export const getBackupCodesStatus = async (): Promise => { - const response = await apiClient.get('/api/auth/mfa/backup-codes/status/'); + const response = await apiClient.get('/auth/mfa/backup-codes/status/'); return response.data; }; @@ -170,7 +170,7 @@ export const getBackupCodesStatus = async (): Promise => { * Disable MFA (requires password or valid MFA code) */ export const disableMFA = async (credentials: { password?: string; mfa_code?: string }): Promise<{ success: boolean; message: string }> => { - const response = await apiClient.post('/api/auth/mfa/disable/', credentials); + const response = await apiClient.post('/auth/mfa/disable/', credentials); return response.data; }; @@ -182,7 +182,7 @@ export const disableMFA = async (credentials: { password?: string; mfa_code?: st * Send MFA code for login (SMS only) */ export const sendMFALoginCode = async (userId: number, method: 'SMS' | 'TOTP' = 'SMS'): Promise<{ success: boolean; message: string; method: string }> => { - const response = await apiClient.post('/api/auth/mfa/login/send/', { user_id: userId, method }); + const response = await apiClient.post('/auth/mfa/login/send/', { user_id: userId, method }); return response.data; }; @@ -195,7 +195,7 @@ export const verifyMFALogin = async ( method: 'SMS' | 'TOTP' | 'BACKUP', trustDevice: boolean = false ): Promise => { - const response = await apiClient.post('/api/auth/mfa/login/verify/', { + const response = await apiClient.post('/auth/mfa/login/verify/', { user_id: userId, code, method, @@ -212,7 +212,7 @@ export const verifyMFALogin = async ( * List trusted devices */ export const listTrustedDevices = async (): Promise<{ devices: TrustedDevice[] }> => { - const response = await apiClient.get('/api/auth/mfa/devices/'); + const response = await apiClient.get('/auth/mfa/devices/'); return response.data; }; @@ -220,7 +220,7 @@ export const listTrustedDevices = async (): Promise<{ devices: TrustedDevice[] } * Revoke a specific trusted device */ export const revokeTrustedDevice = async (deviceId: number): Promise<{ success: boolean; message: string }> => { - const response = await apiClient.delete(`/api/auth/mfa/devices/${deviceId}/`); + const response = await apiClient.delete(`/auth/mfa/devices/${deviceId}/`); return response.data; }; @@ -228,6 +228,6 @@ export const revokeTrustedDevice = async (deviceId: number): Promise<{ success: * Revoke all trusted devices */ export const revokeAllTrustedDevices = async (): Promise<{ success: boolean; message: string; count: number }> => { - const response = await apiClient.delete('/api/auth/mfa/devices/revoke-all/'); + const response = await apiClient.delete('/auth/mfa/devices/revoke-all/'); return response.data; }; diff --git a/frontend/src/api/notifications.ts b/frontend/src/api/notifications.ts index ad7c7db..a0f066f 100644 --- a/frontend/src/api/notifications.ts +++ b/frontend/src/api/notifications.ts @@ -29,7 +29,7 @@ export const getNotifications = async (params?: { read?: boolean; limit?: number queryParams.append('limit', String(params.limit)); } const query = queryParams.toString(); - const url = query ? `/api/notifications/?${query}` : '/api/notifications/'; + const url = query ? `/notifications/?${query}` : '/notifications/'; const response = await apiClient.get(url); return response.data; }; @@ -38,7 +38,7 @@ export const getNotifications = async (params?: { read?: boolean; limit?: number * Get count of unread notifications */ export const getUnreadCount = async (): Promise => { - const response = await apiClient.get('/api/notifications/unread_count/'); + const response = await apiClient.get('/notifications/unread_count/'); return response.data.count; }; @@ -46,19 +46,19 @@ export const getUnreadCount = async (): Promise => { * Mark a single notification as read */ export const markNotificationRead = async (id: number): Promise => { - await apiClient.post(`/api/notifications/${id}/mark_read/`); + await apiClient.post(`/notifications/${id}/mark_read/`); }; /** * Mark all notifications as read */ export const markAllNotificationsRead = async (): Promise => { - await apiClient.post('/api/notifications/mark_all_read/'); + await apiClient.post('/notifications/mark_all_read/'); }; /** * Delete all read notifications */ export const clearAllNotifications = async (): Promise => { - await apiClient.delete('/api/notifications/clear_all/'); + await apiClient.delete('/notifications/clear_all/'); }; diff --git a/frontend/src/api/payments.ts b/frontend/src/api/payments.ts index 8e60b26..562a862 100644 --- a/frontend/src/api/payments.ts +++ b/frontend/src/api/payments.ts @@ -95,7 +95,7 @@ export interface AccountSessionResponse { * Returns the complete payment setup for the business. */ export const getPaymentConfig = () => - apiClient.get('/api/payments/config/status/'); + apiClient.get('/payments/config/status/'); // ============================================================================ // API Keys (Free Tier) @@ -105,14 +105,14 @@ export const getPaymentConfig = () => * Get current API key configuration (masked keys). */ export const getApiKeys = () => - apiClient.get('/api/payments/api-keys/'); + apiClient.get('/payments/api-keys/'); /** * Save API keys. * Validates and stores the provided Stripe API keys. */ export const saveApiKeys = (secretKey: string, publishableKey: string) => - apiClient.post('/api/payments/api-keys/', { + apiClient.post('/payments/api-keys/', { secret_key: secretKey, publishable_key: publishableKey, }); @@ -122,7 +122,7 @@ export const saveApiKeys = (secretKey: string, publishableKey: string) => * Tests the keys against Stripe API. */ export const validateApiKeys = (secretKey: string, publishableKey: string) => - apiClient.post('/api/payments/api-keys/validate/', { + apiClient.post('/payments/api-keys/validate/', { secret_key: secretKey, publishable_key: publishableKey, }); @@ -132,13 +132,13 @@ export const validateApiKeys = (secretKey: string, publishableKey: string) => * Tests stored keys and updates their status. */ export const revalidateApiKeys = () => - apiClient.post('/api/payments/api-keys/revalidate/'); + apiClient.post('/payments/api-keys/revalidate/'); /** * Delete stored API keys. */ export const deleteApiKeys = () => - apiClient.delete<{ success: boolean; message: string }>('/api/payments/api-keys/delete/'); + apiClient.delete<{ success: boolean; message: string }>('/payments/api-keys/delete/'); // ============================================================================ // Stripe Connect (Paid Tiers) @@ -148,14 +148,14 @@ export const deleteApiKeys = () => * Get current Connect account status. */ export const getConnectStatus = () => - apiClient.get('/api/payments/connect/status/'); + apiClient.get('/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('/api/payments/connect/onboard/', { + apiClient.post('/payments/connect/onboard/', { refresh_url: refreshUrl, return_url: returnUrl, }); @@ -165,7 +165,7 @@ export const initiateConnectOnboarding = (refreshUrl: string, returnUrl: string) * 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/', { + apiClient.post<{ url: string }>('/payments/connect/refresh-link/', { refresh_url: refreshUrl, return_url: returnUrl, }); @@ -175,14 +175,14 @@ export const refreshConnectOnboardingLink = (refreshUrl: string, returnUrl: stri * Returns a client_secret for initializing Stripe's embedded Connect components. */ export const createAccountSession = () => - apiClient.post('/api/payments/connect/account-session/'); + apiClient.post('/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('/api/payments/connect/refresh-status/'); + apiClient.post('/payments/connect/refresh-status/'); // ============================================================================ // Transaction Analytics @@ -319,7 +319,7 @@ export const getTransactions = (filters?: TransactionFilters) => { const queryString = params.toString(); return apiClient.get( - `/api/payments/transactions/${queryString ? `?${queryString}` : ''}` + `/payments/transactions/${queryString ? `?${queryString}` : ''}` ); }; @@ -327,7 +327,7 @@ export const getTransactions = (filters?: TransactionFilters) => { * Get a single transaction by ID. */ export const getTransaction = (id: number) => - apiClient.get(`/api/payments/transactions/${id}/`); + apiClient.get(`/payments/transactions/${id}/`); /** * Get transaction summary/analytics. @@ -339,7 +339,7 @@ export const getTransactionSummary = (filters?: Pick( - `/api/payments/transactions/summary/${queryString ? `?${queryString}` : ''}` + `/payments/transactions/summary/${queryString ? `?${queryString}` : ''}` ); }; @@ -347,26 +347,26 @@ export const getTransactionSummary = (filters?: Pick - apiClient.get(`/api/payments/transactions/charges/?limit=${limit}`); + apiClient.get(`/payments/transactions/charges/?limit=${limit}`); /** * Get payouts from Stripe API. */ export const getStripePayouts = (limit: number = 20) => - apiClient.get(`/api/payments/transactions/payouts/?limit=${limit}`); + apiClient.get(`/payments/transactions/payouts/?limit=${limit}`); /** * Get current balance from Stripe API. */ export const getStripeBalance = () => - apiClient.get('/api/payments/transactions/balance/'); + apiClient.get('/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, { + apiClient.post('/payments/transactions/export/', request, { responseType: 'blob', }); @@ -422,7 +422,7 @@ export interface RefundResponse { * Get detailed transaction information including refund data. */ export const getTransactionDetail = (id: number) => - apiClient.get(`/api/payments/transactions/${id}/`); + apiClient.get(`/payments/transactions/${id}/`); /** * Issue a refund for a transaction. @@ -430,4 +430,4 @@ export const getTransactionDetail = (id: number) => * @param request - Optional refund request with amount and reason */ export const refundTransaction = (transactionId: number, request?: RefundRequest) => - apiClient.post(`/api/payments/transactions/${transactionId}/refund/`, request || {}); + apiClient.post(`/payments/transactions/${transactionId}/refund/`, request || {}); diff --git a/frontend/src/api/platformOAuth.ts b/frontend/src/api/platformOAuth.ts index 6ececa8..95d1ddd 100644 --- a/frontend/src/api/platformOAuth.ts +++ b/frontend/src/api/platformOAuth.ts @@ -75,7 +75,7 @@ export interface PlatformOAuthSettingsUpdate { * Get platform OAuth settings */ export const getPlatformOAuthSettings = async (): Promise => { - const { data } = await apiClient.get('/api/platform/settings/oauth/'); + const { data } = await apiClient.get('/platform/settings/oauth/'); return data; }; @@ -85,6 +85,6 @@ export const getPlatformOAuthSettings = async (): Promise export const updatePlatformOAuthSettings = async ( settings: PlatformOAuthSettingsUpdate ): Promise => { - const { data } = await apiClient.post('/api/platform/settings/oauth/', settings); + const { data } = await apiClient.post('/platform/settings/oauth/', settings); return data; }; diff --git a/frontend/src/api/profile.ts b/frontend/src/api/profile.ts index cba63fb..0c3b45f 100644 --- a/frontend/src/api/profile.ts +++ b/frontend/src/api/profile.ts @@ -71,43 +71,43 @@ export interface LoginHistoryEntry { // Profile API export const getProfile = async (): Promise => { - const response = await apiClient.get('/api/auth/profile/'); + const response = await apiClient.get('/auth/profile/'); return response.data; }; export const updateProfile = async (data: Partial): Promise => { - const response = await apiClient.patch('/api/auth/profile/', data); + const response = await apiClient.patch('/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, { + const response = await apiClient.post('/auth/profile/avatar/', formData, { headers: { 'Content-Type': 'multipart/form-data' }, }); return response.data; }; export const deleteAvatar = async (): Promise => { - await apiClient.delete('/api/auth/profile/avatar/'); + await apiClient.delete('/auth/profile/avatar/'); }; // Email API export const sendVerificationEmail = async (): Promise => { - await apiClient.post('/api/auth/email/verify/send/'); + await apiClient.post('/auth/email/verify/send/'); }; export const verifyEmail = async (token: string): Promise => { - await apiClient.post('/api/auth/email/verify/confirm/', { token }); + await apiClient.post('/auth/email/verify/confirm/', { token }); }; export const requestEmailChange = async (newEmail: string): Promise => { - await apiClient.post('/api/auth/email/change/', { new_email: newEmail }); + await apiClient.post('/auth/email/change/', { new_email: newEmail }); }; export const confirmEmailChange = async (token: string): Promise => { - await apiClient.post('/api/auth/email/change/confirm/', { token }); + await apiClient.post('/auth/email/change/confirm/', { token }); }; // Password API @@ -115,7 +115,7 @@ export const changePassword = async ( currentPassword: string, newPassword: string ): Promise => { - await apiClient.post('/api/auth/password/change/', { + await apiClient.post('/auth/password/change/', { current_password: currentPassword, new_password: newPassword, }); @@ -123,12 +123,12 @@ export const changePassword = async ( // 2FA API (using new MFA endpoints) export const setupTOTP = async (): Promise => { - const response = await apiClient.post('/api/auth/mfa/totp/setup/'); + const response = await apiClient.post('/auth/mfa/totp/setup/'); return response.data; }; export const verifyTOTP = async (code: string): Promise => { - const response = await apiClient.post('/api/auth/mfa/totp/verify/', { code }); + const response = await apiClient.post('/auth/mfa/totp/verify/', { code }); // Map response to expected format return { success: response.data.success, @@ -137,46 +137,46 @@ export const verifyTOTP = async (code: string): Promise => { }; export const disableTOTP = async (code: string): Promise => { - await apiClient.post('/api/auth/mfa/disable/', { mfa_code: code }); + await apiClient.post('/auth/mfa/disable/', { mfa_code: code }); }; export const getRecoveryCodes = async (): Promise => { - const response = await apiClient.get('/api/auth/mfa/backup-codes/status/'); + const response = await apiClient.get('/auth/mfa/backup-codes/status/'); // Note: Actual codes are only shown when generated, not retrievable later return []; }; export const regenerateRecoveryCodes = async (): Promise => { - const response = await apiClient.post('/api/auth/mfa/backup-codes/'); + const response = await apiClient.post('/auth/mfa/backup-codes/'); return response.data.backup_codes; }; // Sessions API export const getSessions = async (): Promise => { - const response = await apiClient.get('/api/auth/sessions/'); + const response = await apiClient.get('/auth/sessions/'); return response.data; }; export const revokeSession = async (sessionId: string): Promise => { - await apiClient.delete(`/api/auth/sessions/${sessionId}/`); + await apiClient.delete(`/auth/sessions/${sessionId}/`); }; export const revokeOtherSessions = async (): Promise => { - await apiClient.post('/api/auth/sessions/revoke-others/'); + await apiClient.post('/auth/sessions/revoke-others/'); }; export const getLoginHistory = async (): Promise => { - const response = await apiClient.get('/api/auth/login-history/'); + const response = await apiClient.get('/auth/login-history/'); return response.data; }; // Phone Verification API export const sendPhoneVerification = async (phone: string): Promise => { - await apiClient.post('/api/auth/phone/verify/send/', { phone }); + await apiClient.post('/auth/phone/verify/send/', { phone }); }; export const verifyPhoneCode = async (code: string): Promise => { - await apiClient.post('/api/auth/phone/verify/confirm/', { code }); + await apiClient.post('/auth/phone/verify/confirm/', { code }); }; // Multiple Email Management API @@ -189,27 +189,27 @@ export interface UserEmail { } export const getUserEmails = async (): Promise => { - const response = await apiClient.get('/api/auth/emails/'); + const response = await apiClient.get('/auth/emails/'); return response.data; }; export const addUserEmail = async (email: string): Promise => { - const response = await apiClient.post('/api/auth/emails/', { email }); + const response = await apiClient.post('/auth/emails/', { email }); return response.data; }; export const deleteUserEmail = async (emailId: number): Promise => { - await apiClient.delete(`/api/auth/emails/${emailId}/`); + await apiClient.delete(`/auth/emails/${emailId}/`); }; export const sendUserEmailVerification = async (emailId: number): Promise => { - await apiClient.post(`/api/auth/emails/${emailId}/send-verification/`); + await apiClient.post(`/auth/emails/${emailId}/send-verification/`); }; export const verifyUserEmail = async (emailId: number, token: string): Promise => { - await apiClient.post(`/api/auth/emails/${emailId}/verify/`, { token }); + await apiClient.post(`/auth/emails/${emailId}/verify/`, { token }); }; export const setPrimaryEmail = async (emailId: number): Promise => { - await apiClient.post(`/api/auth/emails/${emailId}/set-primary/`); + await apiClient.post(`/auth/emails/${emailId}/set-primary/`); }; diff --git a/frontend/src/api/ticketEmailSettings.ts b/frontend/src/api/ticketEmailSettings.ts index 3b0c8e6..70b2644 100644 --- a/frontend/src/api/ticketEmailSettings.ts +++ b/frontend/src/api/ticketEmailSettings.ts @@ -122,7 +122,7 @@ export interface IncomingTicketEmail { * Get ticket email settings */ export const getTicketEmailSettings = async (): Promise => { - const response = await apiClient.get('/api/tickets/email-settings/'); + const response = await apiClient.get('/tickets/email-settings/'); return response.data; }; @@ -132,7 +132,7 @@ export const getTicketEmailSettings = async (): Promise => export const updateTicketEmailSettings = async ( data: TicketEmailSettingsUpdate ): Promise => { - const response = await apiClient.patch('/api/tickets/email-settings/', data); + const response = await apiClient.patch('/tickets/email-settings/', data); return response.data; }; @@ -140,7 +140,7 @@ export const updateTicketEmailSettings = async ( * Test IMAP connection */ export const testImapConnection = async (): Promise => { - const response = await apiClient.post('/api/tickets/email-settings/test-imap/'); + const response = await apiClient.post('/tickets/email-settings/test-imap/'); return response.data; }; @@ -148,7 +148,7 @@ export const testImapConnection = async (): Promise => { * Test SMTP connection */ export const testSmtpConnection = async (): Promise => { - const response = await apiClient.post('/api/tickets/email-settings/test-smtp/'); + const response = await apiClient.post('/tickets/email-settings/test-smtp/'); return response.data; }; @@ -159,7 +159,7 @@ export const testEmailConnection = testImapConnection; * Manually trigger email fetch */ export const fetchEmailsNow = async (): Promise => { - const response = await apiClient.post('/api/tickets/email-settings/fetch-now/'); + const response = await apiClient.post('/tickets/email-settings/fetch-now/'); return response.data; }; @@ -170,7 +170,7 @@ export const getIncomingEmails = async (params?: { status?: string; ticket?: number; }): Promise => { - const response = await apiClient.get('/api/tickets/incoming-emails/', { params }); + const response = await apiClient.get('/tickets/incoming-emails/', { params }); return response.data; }; @@ -183,7 +183,7 @@ export const reprocessIncomingEmail = async (id: number): Promise<{ comment_id?: number; ticket_id?: number; }> => { - const response = await apiClient.post(`/api/tickets/incoming-emails/${id}/reprocess/`); + const response = await apiClient.post(`/tickets/incoming-emails/${id}/reprocess/`); return response.data; }; @@ -193,7 +193,7 @@ export const reprocessIncomingEmail = async (id: number): Promise<{ * Also checks MX records for custom domains using Google Workspace or Microsoft 365 */ export const detectEmailProvider = async (email: string): Promise => { - const response = await apiClient.post('/api/tickets/email-settings/detect/', { email }); + const response = await apiClient.post('/tickets/email-settings/detect/', { email }); return response.data; }; @@ -225,7 +225,7 @@ export interface OAuthCredential { * Get OAuth configuration status */ export const getOAuthStatus = async (): Promise => { - const response = await apiClient.get('/api/oauth/status/'); + const response = await apiClient.get('/oauth/status/'); return response.data; }; @@ -233,7 +233,7 @@ export const getOAuthStatus = async (): Promise => { * Initiate Google OAuth flow */ export const initiateGoogleOAuth = async (purpose: string = 'email'): Promise => { - const response = await apiClient.post('/api/oauth/google/initiate/', { purpose }); + const response = await apiClient.post('/oauth/google/initiate/', { purpose }); return response.data; }; @@ -241,7 +241,7 @@ export const initiateGoogleOAuth = async (purpose: string = 'email'): Promise => { - const response = await apiClient.post('/api/oauth/microsoft/initiate/', { purpose }); + const response = await apiClient.post('/oauth/microsoft/initiate/', { purpose }); return response.data; }; @@ -249,7 +249,7 @@ export const initiateMicrosoftOAuth = async (purpose: string = 'email'): Promise * List OAuth credentials */ export const getOAuthCredentials = async (): Promise => { - const response = await apiClient.get('/api/oauth/credentials/'); + const response = await apiClient.get('/oauth/credentials/'); return response.data; }; @@ -257,6 +257,6 @@ export const getOAuthCredentials = async (): Promise => { * Delete OAuth credential */ export const deleteOAuthCredential = async (id: number): Promise<{ success: boolean; message: string }> => { - const response = await apiClient.delete(`/api/oauth/credentials/${id}/`); + const response = await apiClient.delete(`/oauth/credentials/${id}/`); return response.data; }; diff --git a/frontend/src/api/tickets.ts b/frontend/src/api/tickets.ts index c59fd78..2829940 100644 --- a/frontend/src/api/tickets.ts +++ b/frontend/src/api/tickets.ts @@ -17,52 +17,52 @@ export const getTickets = async (filters?: TicketFilters): Promise => if (filters?.ticketType) params.append('ticket_type', filters.ticketType); if (filters?.assignee) params.append('assignee', filters.assignee); - const response = await apiClient.get(`/api/tickets/${params.toString() ? `?${params.toString()}` : ''}`); + const response = await apiClient.get(`/tickets/${params.toString() ? `?${params.toString()}` : ''}`); return response.data; }; export const getTicket = async (id: string): Promise => { - const response = await apiClient.get(`/api/tickets/${id}/`); + const response = await apiClient.get(`/tickets/${id}/`); return response.data; }; export const createTicket = async (data: Partial): Promise => { - const response = await apiClient.post('/api/tickets/', data); + const response = await apiClient.post('/tickets/', data); return response.data; }; export const updateTicket = async (id: string, data: Partial): Promise => { - const response = await apiClient.patch(`/api/tickets/${id}/`, data); + const response = await apiClient.patch(`/tickets/${id}/`, data); return response.data; }; export const deleteTicket = async (id: string): Promise => { - await apiClient.delete(`/api/tickets/${id}/`); + await apiClient.delete(`/tickets/${id}/`); }; export const getTicketComments = async (ticketId: string): Promise => { - const response = await apiClient.get(`/api/tickets/${ticketId}/comments/`); + const response = await apiClient.get(`/tickets/${ticketId}/comments/`); return response.data; }; export const createTicketComment = async (ticketId: string, data: Partial): Promise => { - const response = await apiClient.post(`/api/tickets/${ticketId}/comments/`, data); + const response = await apiClient.post(`/tickets/${ticketId}/comments/`, data); return response.data; }; // Ticket Templates export const getTicketTemplates = async (): Promise => { - const response = await apiClient.get('/api/tickets/templates/'); + const response = await apiClient.get('/tickets/templates/'); return response.data; }; export const getTicketTemplate = async (id: string): Promise => { - const response = await apiClient.get(`/api/tickets/templates/${id}/`); + const response = await apiClient.get(`/tickets/templates/${id}/`); return response.data; }; // Canned Responses export const getCannedResponses = async (): Promise => { - const response = await apiClient.get('/api/tickets/canned-responses/'); + const response = await apiClient.get('/tickets/canned-responses/'); return response.data; }; diff --git a/frontend/src/components/DevQuickLogin.tsx b/frontend/src/components/DevQuickLogin.tsx index 8f928dd..71348f1 100644 --- a/frontend/src/components/DevQuickLogin.tsx +++ b/frontend/src/components/DevQuickLogin.tsx @@ -88,7 +88,7 @@ export function DevQuickLogin({ embedded = false }: DevQuickLoginProps) { setLoading(user.username); try { // Call token auth API - const response = await apiClient.post('/api/auth-token/', { + const response = await apiClient.post('/auth-token/', { username: user.username, password: user.password, }); @@ -97,7 +97,7 @@ export function DevQuickLogin({ embedded = false }: DevQuickLoginProps) { setCookie('access_token', response.data.token, 7); // Fetch user data to determine redirect - const userResponse = await apiClient.get('/api/auth/me/'); + const userResponse = await apiClient.get('/auth/me/'); const userData = userResponse.data; // Determine the correct subdomain based on user role diff --git a/frontend/src/components/marketing/Navbar.tsx b/frontend/src/components/marketing/Navbar.tsx index b3efc8c..bbb8dc0 100644 --- a/frontend/src/components/marketing/Navbar.tsx +++ b/frontend/src/components/marketing/Navbar.tsx @@ -5,6 +5,7 @@ import { Menu, X, Sun, Moon } from 'lucide-react'; import SmoothScheduleLogo from '../SmoothScheduleLogo'; import LanguageSelector from '../LanguageSelector'; import { User } from '../../api/auth'; +import { buildSubdomainUrl } from '../../utils/domain'; interface NavbarProps { darkMode: boolean; @@ -47,10 +48,10 @@ const Navbar: React.FC = ({ darkMode, toggleTheme, user }) => { const protocol = window.location.protocol; if (['superuser', 'platform_manager', 'platform_support'].includes(user.role)) { - return `${protocol}//platform.lvh.me${port}/`; + return buildSubdomainUrl('platform', '/'); } if (user.business_subdomain) { - return `${protocol}//${user.business_subdomain}.lvh.me${port}/`; + return buildSubdomainUrl(user.business_subdomain, '/'); } return '/login'; }; diff --git a/frontend/src/hooks/useApiTokens.ts b/frontend/src/hooks/useApiTokens.ts index d9b4955..0f2262a 100644 --- a/frontend/src/hooks/useApiTokens.ts +++ b/frontend/src/hooks/useApiTokens.ts @@ -77,26 +77,26 @@ export const SCOPE_PRESETS = { // API Functions const fetchApiTokens = async (): Promise => { - const response = await apiClient.get('/api/v1/tokens/'); + const response = await apiClient.get('/v1/tokens/'); return response.data; }; const createApiToken = async (data: CreateTokenData): Promise => { - const response = await apiClient.post('/api/v1/tokens/', data); + const response = await apiClient.post('/v1/tokens/', data); return response.data; }; const revokeApiToken = async (tokenId: string): Promise => { - await apiClient.delete(`/api/v1/tokens/${tokenId}/`); + await apiClient.delete(`/v1/tokens/${tokenId}/`); }; const updateApiToken = async ({ tokenId, data }: { tokenId: string; data: Partial & { is_active?: boolean } }): Promise => { - const response = await apiClient.patch(`/api/v1/tokens/${tokenId}/`, data); + const response = await apiClient.patch(`/v1/tokens/${tokenId}/`, data); return response.data; }; const fetchTestTokensForDocs = async (): Promise => { - const response = await apiClient.get('/api/v1/tokens/test-tokens/'); + const response = await apiClient.get('/v1/tokens/test-tokens/'); return response.data; }; diff --git a/frontend/src/hooks/useAppointmentWebSocket.ts b/frontend/src/hooks/useAppointmentWebSocket.ts index 408ad9c..4800738 100644 --- a/frontend/src/hooks/useAppointmentWebSocket.ts +++ b/frontend/src/hooks/useAppointmentWebSocket.ts @@ -7,6 +7,7 @@ import { useEffect, useRef, useCallback, useState } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { getCookie } from '../utils/cookies'; import { getSubdomain } from '../api/config'; +import { getWebSocketUrl } from '../utils/domain'; import { Appointment } from '../types'; interface WebSocketMessage { @@ -87,7 +88,7 @@ export function useAppointmentWebSocket(options: UseAppointmentWebSocketOptions }, [onConnected, onDisconnected, onError]); // Get WebSocket URL - not a callback to avoid recreating - const getWebSocketUrl = () => { + const getWsUrl = () => { const token = getCookie('access_token'); const subdomain = getSubdomain(); @@ -95,11 +96,8 @@ export function useAppointmentWebSocket(options: UseAppointmentWebSocketOptions return null; } - // Determine WebSocket host - use api subdomain for WebSocket - const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsHost = `api.lvh.me:8000`; // In production, this would come from config - - return `${wsProtocol}//${wsHost}/ws/appointments/?token=${token}&subdomain=${subdomain}`; + // Use the getWebSocketUrl utility from domain.ts + return `${getWebSocketUrl()}appointments/?token=${token}&subdomain=${subdomain}`; }; const updateQueryCache = useCallback((message: WebSocketMessage) => { @@ -160,7 +158,7 @@ export function useAppointmentWebSocket(options: UseAppointmentWebSocketOptions return; } - const url = getWebSocketUrl(); + const url = getWsUrl(); if (!url) { console.log('WebSocket: Missing token or subdomain, skipping connection'); return; diff --git a/frontend/src/hooks/useAppointments.ts b/frontend/src/hooks/useAppointments.ts index 3f14f55..e8273ad 100644 --- a/frontend/src/hooks/useAppointments.ts +++ b/frontend/src/hooks/useAppointments.ts @@ -39,7 +39,7 @@ export const useAppointments = (filters?: AppointmentFilters) => { params.append('end_date', endOfDay.toISOString()); } - const { data } = await apiClient.get(`/api/appointments/?${params}`); + const { data } = await apiClient.get(`/appointments/?${params}`); // Transform backend format to frontend format return data.map((a: any) => ({ @@ -73,7 +73,7 @@ export const useAppointment = (id: string) => { return useQuery({ queryKey: ['appointments', id], queryFn: async () => { - const { data } = await apiClient.get(`/api/appointments/${id}/`); + const { data } = await apiClient.get(`/appointments/${id}/`); return { id: String(data.id), @@ -115,7 +115,7 @@ export const useCreateAppointment = () => { backendData.customer = parseInt(appointmentData.customerId); } - const { data } = await apiClient.post('/api/appointments/', backendData); + const { data } = await apiClient.post('/appointments/', backendData); return data; }, onSuccess: () => { @@ -154,7 +154,7 @@ export const useUpdateAppointment = () => { if (updates.status) backendData.status = updates.status; if (updates.notes !== undefined) backendData.notes = updates.notes; - const { data } = await apiClient.patch(`/api/appointments/${id}/`, backendData); + const { data } = await apiClient.patch(`/appointments/${id}/`, backendData); return data; }, // Optimistic update: update UI immediately before API call completes @@ -208,7 +208,7 @@ export const useDeleteAppointment = () => { return useMutation({ mutationFn: async (id: string) => { - await apiClient.delete(`/api/appointments/${id}/`); + await apiClient.delete(`/appointments/${id}/`); return id; }, // Optimistic update: remove from UI immediately @@ -264,7 +264,7 @@ export const useRescheduleAppointment = () => { newStartTime: Date; newResourceId?: string | null; }) => { - const appointment = await apiClient.get(`/api/appointments/${id}/`); + const appointment = await apiClient.get(`/appointments/${id}/`); const durationMinutes = appointment.data.duration_minutes; return updateMutation.mutateAsync({ diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts index cf359ff..2820551 100644 --- a/frontend/src/hooks/useAuth.ts +++ b/frontend/src/hooks/useAuth.ts @@ -14,6 +14,7 @@ import { MasqueradeStackEntry } from '../api/auth'; import { getCookie, setCookie, deleteCookie } from '../utils/cookies'; +import { getBaseDomain, buildSubdomainUrl } from '../utils/domain'; /** * Helper hook to set auth tokens (used by invitation acceptance) @@ -67,7 +68,7 @@ export const useLogin = () => { return useMutation({ mutationFn: login, onSuccess: (data) => { - // Store tokens in cookies (domain=.lvh.me for cross-subdomain access) + // Store tokens in cookies for cross-subdomain access setCookie('access_token', data.access, 7); setCookie('refresh_token', data.refresh, 7); @@ -132,6 +133,7 @@ export const useMasquerade = () => { const user = data.user; const currentHostname = window.location.hostname; const currentPort = window.location.port; + const baseDomain = getBaseDomain(); let targetSubdomain: string | null = null; @@ -141,13 +143,14 @@ export const useMasquerade = () => { targetSubdomain = user.business_subdomain; } - const needsRedirect = targetSubdomain && currentHostname !== `${targetSubdomain}.lvh.me`; + const needsRedirect = targetSubdomain && currentHostname !== `${targetSubdomain}.${baseDomain}`; if (needsRedirect) { // CRITICAL: Clear the session cookie BEFORE redirect // Call logout API to clear HttpOnly sessionid cookie try { - await fetch('http://api.lvh.me:8000/api/auth/logout/', { + const apiUrl = import.meta.env.VITE_API_URL || `${window.location.protocol}//${baseDomain}`; + await fetch(`${apiUrl}/api/auth/logout/`, { method: 'POST', credentials: 'include', }); @@ -155,10 +158,9 @@ export const useMasquerade = () => { // Continue anyway } - const portStr = currentPort ? `:${currentPort}` : ''; // Pass tokens AND masquerading stack in URL (for cross-domain transfer) const stackEncoded = encodeURIComponent(JSON.stringify(data.masquerade_stack || [])); - const redirectUrl = `http://${targetSubdomain}.lvh.me${portStr}/?access_token=${data.access}&refresh_token=${data.refresh}&masquerade_stack=${stackEncoded}`; + const redirectUrl = buildSubdomainUrl(targetSubdomain, `/?access_token=${data.access}&refresh_token=${data.refresh}&masquerade_stack=${stackEncoded}`); window.location.href = redirectUrl; return; @@ -204,6 +206,7 @@ export const useStopMasquerade = () => { const user = data.user; const currentHostname = window.location.hostname; const currentPort = window.location.port; + const baseDomain = getBaseDomain(); let targetSubdomain: string | null = null; @@ -213,12 +216,13 @@ export const useStopMasquerade = () => { targetSubdomain = user.business_subdomain; } - const needsRedirect = targetSubdomain && currentHostname !== `${targetSubdomain}.lvh.me`; + const needsRedirect = targetSubdomain && currentHostname !== `${targetSubdomain}.${baseDomain}`; if (needsRedirect) { // CRITICAL: Clear the session cookie BEFORE redirect try { - await fetch('http://api.lvh.me:8000/api/auth/logout/', { + const apiUrl = import.meta.env.VITE_API_URL || `${window.location.protocol}//${baseDomain}`; + await fetch(`${apiUrl}/api/auth/logout/`, { method: 'POST', credentials: 'include', }); @@ -226,10 +230,9 @@ export const useStopMasquerade = () => { // Continue anyway } - const portStr = currentPort ? `:${currentPort}` : ''; // Pass tokens AND masquerading stack in URL (for cross-domain transfer) const stackEncoded = encodeURIComponent(JSON.stringify(data.masquerade_stack || [])); - const redirectUrl = `http://${targetSubdomain}.lvh.me${portStr}/?access_token=${data.access}&refresh_token=${data.refresh}&masquerade_stack=${stackEncoded}`; + const redirectUrl = buildSubdomainUrl(targetSubdomain, `/?access_token=${data.access}&refresh_token=${data.refresh}&masquerade_stack=${stackEncoded}`); window.location.href = redirectUrl; return; diff --git a/frontend/src/hooks/useBusiness.ts b/frontend/src/hooks/useBusiness.ts index 38dd8cb..701c4b7 100644 --- a/frontend/src/hooks/useBusiness.ts +++ b/frontend/src/hooks/useBusiness.ts @@ -23,7 +23,7 @@ export const useCurrentBusiness = () => { return null; // No token, return null instead of making request } - const { data } = await apiClient.get('/api/business/current/'); + const { data } = await apiClient.get('/business/current/'); // Transform backend format to frontend format return { @@ -96,7 +96,7 @@ export const useUpdateBusiness = () => { backendData.customer_dashboard_content = updates.customerDashboardContent; } - const { data } = await apiClient.patch('/api/business/current/update/', backendData); + const { data } = await apiClient.patch('/business/current/update/', backendData); return data; }, onSuccess: () => { @@ -112,7 +112,7 @@ export const useResources = () => { return useQuery({ queryKey: ['resources'], queryFn: async () => { - const { data } = await apiClient.get('/api/resources/'); + const { data } = await apiClient.get('/resources/'); return data; }, staleTime: 5 * 60 * 1000, // 5 minutes @@ -127,7 +127,7 @@ export const useCreateResource = () => { return useMutation({ mutationFn: async (resourceData: { name: string; type: string; user_id?: string }) => { - const { data } = await apiClient.post('/api/resources/', resourceData); + const { data } = await apiClient.post('/resources/', resourceData); return data; }, onSuccess: () => { @@ -143,7 +143,7 @@ export const useBusinessUsers = () => { return useQuery({ queryKey: ['businessUsers'], queryFn: async () => { - const { data } = await apiClient.get('/api/staff/'); + const { data } = await apiClient.get('/staff/'); return data; }, staleTime: 5 * 60 * 1000, // 5 minutes diff --git a/frontend/src/hooks/useCustomers.ts b/frontend/src/hooks/useCustomers.ts index 06f5bf6..255c3b1 100644 --- a/frontend/src/hooks/useCustomers.ts +++ b/frontend/src/hooks/useCustomers.ts @@ -22,7 +22,7 @@ export const useCustomers = (filters?: CustomerFilters) => { if (filters?.status) params.append('status', filters.status); if (filters?.search) params.append('search', filters.search); - const { data } = await apiClient.get(`/api/customers/?${params}`); + const { data } = await apiClient.get(`/customers/?${params}`); // Transform backend format to frontend format return data.map((c: any) => ({ @@ -66,7 +66,7 @@ export const useCreateCustomer = () => { tags: customerData.tags, }; - const { data } = await apiClient.post('/api/customers/', backendData); + const { data } = await apiClient.post('/customers/', backendData); return data; }, onSuccess: () => { @@ -93,7 +93,7 @@ export const useUpdateCustomer = () => { tags: updates.tags, }; - const { data } = await apiClient.patch(`/api/customers/${id}/`, backendData); + const { data } = await apiClient.patch(`/customers/${id}/`, backendData); return data; }, onSuccess: () => { @@ -110,7 +110,7 @@ export const useDeleteCustomer = () => { return useMutation({ mutationFn: async (id: string) => { - await apiClient.delete(`/api/customers/${id}/`); + await apiClient.delete(`/customers/${id}/`); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['customers'] }); diff --git a/frontend/src/hooks/useInvitations.ts b/frontend/src/hooks/useInvitations.ts index c5d3b40..5868666 100644 --- a/frontend/src/hooks/useInvitations.ts +++ b/frontend/src/hooks/useInvitations.ts @@ -60,7 +60,7 @@ export const useInvitations = () => { return useQuery({ queryKey: ['invitations'], queryFn: async () => { - const { data } = await apiClient.get('/api/staff/invitations/'); + const { data } = await apiClient.get('/staff/invitations/'); return data; }, }); @@ -74,7 +74,7 @@ export const useCreateInvitation = () => { return useMutation({ mutationFn: async (invitationData: CreateInvitationData) => { - const { data } = await apiClient.post('/api/staff/invitations/', invitationData); + const { data } = await apiClient.post('/staff/invitations/', invitationData); return data; }, onSuccess: () => { @@ -91,7 +91,7 @@ export const useCancelInvitation = () => { return useMutation({ mutationFn: async (invitationId: number) => { - await apiClient.delete(`/api/staff/invitations/${invitationId}/`); + await apiClient.delete(`/staff/invitations/${invitationId}/`); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['invitations'] }); @@ -105,7 +105,7 @@ export const useCancelInvitation = () => { export const useResendInvitation = () => { return useMutation({ mutationFn: async (invitationId: number) => { - const { data } = await apiClient.post(`/api/staff/invitations/${invitationId}/resend/`); + const { data } = await apiClient.post(`/staff/invitations/${invitationId}/resend/`); return data; }, }); @@ -118,7 +118,7 @@ export const useInvitationDetails = (token: string | null) => { return useQuery({ queryKey: ['invitation', token], queryFn: async () => { - const { data } = await apiClient.get(`/api/staff/invitations/token/${token}/`); + const { data } = await apiClient.get(`/staff/invitations/token/${token}/`); return data; }, enabled: !!token, @@ -142,7 +142,7 @@ export const useAcceptInvitation = () => { lastName: string; password: string; }) => { - const { data } = await apiClient.post(`/api/staff/invitations/token/${token}/accept/`, { + const { data } = await apiClient.post(`/staff/invitations/token/${token}/accept/`, { first_name: firstName, last_name: lastName, password, @@ -158,7 +158,7 @@ export const useAcceptInvitation = () => { export const useDeclineInvitation = () => { return useMutation({ mutationFn: async (token: string) => { - const { data } = await apiClient.post(`/api/staff/invitations/token/${token}/decline/`); + const { data } = await apiClient.post(`/staff/invitations/token/${token}/decline/`); return data; }, }); diff --git a/frontend/src/hooks/usePlatformSettings.ts b/frontend/src/hooks/usePlatformSettings.ts index 8eddd98..8e91b04 100644 --- a/frontend/src/hooks/usePlatformSettings.ts +++ b/frontend/src/hooks/usePlatformSettings.ts @@ -67,7 +67,7 @@ export const usePlatformSettings = () => { return useQuery({ queryKey: ['platformSettings'], queryFn: async () => { - const { data } = await apiClient.get('/api/platform/settings/'); + const { data } = await apiClient.get('/platform/settings/'); return data; }, staleTime: 5 * 60 * 1000, // 5 minutes @@ -82,7 +82,7 @@ export const useUpdateStripeKeys = () => { return useMutation({ mutationFn: async (keys: StripeKeysUpdate) => { - const { data } = await apiClient.post('/api/platform/settings/stripe/keys/', keys); + const { data } = await apiClient.post('/platform/settings/stripe/keys/', keys); return data; }, onSuccess: (data) => { @@ -99,7 +99,7 @@ export const useValidateStripeKeys = () => { return useMutation({ mutationFn: async () => { - const { data } = await apiClient.post('/api/platform/settings/stripe/validate/'); + const { data } = await apiClient.post('/platform/settings/stripe/validate/'); return data; }, onSuccess: (data) => { @@ -117,7 +117,7 @@ export const useSubscriptionPlans = () => { return useQuery({ queryKey: ['subscriptionPlans'], queryFn: async () => { - const { data } = await apiClient.get('/api/platform/subscription-plans/'); + const { data } = await apiClient.get('/platform/subscription-plans/'); return data; }, staleTime: 5 * 60 * 1000, @@ -132,7 +132,7 @@ export const useCreateSubscriptionPlan = () => { return useMutation({ mutationFn: async (plan: SubscriptionPlanCreate) => { - const { data } = await apiClient.post('/api/platform/subscription-plans/', plan); + const { data } = await apiClient.post('/platform/subscription-plans/', plan); return data; }, onSuccess: () => { @@ -149,7 +149,7 @@ export const useUpdateSubscriptionPlan = () => { return useMutation({ mutationFn: async ({ id, ...updates }: Partial & { id: number }) => { - const { data } = await apiClient.patch(`/api/platform/subscription-plans/${id}/`, updates); + const { data } = await apiClient.patch(`/platform/subscription-plans/${id}/`, updates); return data; }, onSuccess: () => { @@ -166,7 +166,7 @@ export const useDeleteSubscriptionPlan = () => { return useMutation({ mutationFn: async (id: number) => { - const { data } = await apiClient.delete(`/api/platform/subscription-plans/${id}/`); + const { data } = await apiClient.delete(`/platform/subscription-plans/${id}/`); return data; }, onSuccess: () => { @@ -183,7 +183,7 @@ export const useSyncPlansWithStripe = () => { return useMutation({ mutationFn: async () => { - const { data } = await apiClient.post('/api/platform/subscription-plans/sync_with_stripe/'); + const { data } = await apiClient.post('/platform/subscription-plans/sync_with_stripe/'); return data; }, onSuccess: () => { diff --git a/frontend/src/hooks/useResourceTypes.ts b/frontend/src/hooks/useResourceTypes.ts index 41735d0..2771df4 100644 --- a/frontend/src/hooks/useResourceTypes.ts +++ b/frontend/src/hooks/useResourceTypes.ts @@ -13,7 +13,7 @@ export const useResourceTypes = () => { return useQuery({ queryKey: ['resourceTypes'], queryFn: async () => { - const { data } = await apiClient.get('/api/resource-types/'); + const { data } = await apiClient.get('/resource-types/'); return data; }, // Provide default types if API doesn't have them yet @@ -48,7 +48,7 @@ export const useCreateResourceType = () => { return useMutation({ mutationFn: async (newType: Omit) => { - const { data } = await apiClient.post('/api/resource-types/', newType); + const { data } = await apiClient.post('/resource-types/', newType); return data; }, onSuccess: () => { @@ -65,7 +65,7 @@ export const useUpdateResourceType = () => { return useMutation({ mutationFn: async ({ id, updates }: { id: string; updates: Partial }) => { - const { data } = await apiClient.patch(`/api/resource-types/${id}/`, updates); + const { data } = await apiClient.patch(`/resource-types/${id}/`, updates); return data; }, onSuccess: () => { @@ -82,7 +82,7 @@ export const useDeleteResourceType = () => { return useMutation({ mutationFn: async (id: string) => { - await apiClient.delete(`/api/resource-types/${id}/`); + await apiClient.delete(`/resource-types/${id}/`); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['resourceTypes'] }); diff --git a/frontend/src/hooks/useResources.ts b/frontend/src/hooks/useResources.ts index afae0f2..abc5ba0 100644 --- a/frontend/src/hooks/useResources.ts +++ b/frontend/src/hooks/useResources.ts @@ -20,7 +20,7 @@ export const useResources = (filters?: ResourceFilters) => { const params = new URLSearchParams(); if (filters?.type) params.append('type', filters.type); - const { data } = await apiClient.get(`/api/resources/?${params}`); + const { data } = await apiClient.get(`/resources/?${params}`); // Transform backend format to frontend format return data.map((r: any) => ({ @@ -42,7 +42,7 @@ export const useResource = (id: string) => { return useQuery({ queryKey: ['resources', id], queryFn: async () => { - const { data } = await apiClient.get(`/api/resources/${id}/`); + const { data } = await apiClient.get(`/resources/${id}/`); return { id: String(data.id), @@ -72,7 +72,7 @@ export const useCreateResource = () => { timezone: 'UTC', // Default timezone }; - const { data } = await apiClient.post('/api/resources/', backendData); + const { data } = await apiClient.post('/resources/', backendData); return data; }, onSuccess: () => { @@ -102,7 +102,7 @@ export const useUpdateResource = () => { backendData.saved_lane_count = updates.savedLaneCount; } - const { data } = await apiClient.patch(`/api/resources/${id}/`, backendData); + const { data } = await apiClient.patch(`/resources/${id}/`, backendData); return data; }, onSuccess: () => { @@ -119,7 +119,7 @@ export const useDeleteResource = () => { return useMutation({ mutationFn: async (id: string) => { - await apiClient.delete(`/api/resources/${id}/`); + await apiClient.delete(`/resources/${id}/`); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['resources'] }); diff --git a/frontend/src/hooks/useServices.ts b/frontend/src/hooks/useServices.ts index 94c3ebb..638e6d6 100644 --- a/frontend/src/hooks/useServices.ts +++ b/frontend/src/hooks/useServices.ts @@ -13,7 +13,7 @@ export const useServices = () => { return useQuery({ queryKey: ['services'], queryFn: async () => { - const { data } = await apiClient.get('/api/services/'); + const { data } = await apiClient.get('/services/'); // Transform backend format to frontend format return data.map((s: any) => ({ @@ -37,7 +37,7 @@ export const useService = (id: string) => { return useQuery({ queryKey: ['services', id], queryFn: async () => { - const { data } = await apiClient.get(`/api/services/${id}/`); + const { data } = await apiClient.get(`/services/${id}/`); return { id: String(data.id), @@ -70,7 +70,7 @@ export const useCreateService = () => { photos: serviceData.photos || [], }; - const { data } = await apiClient.post('/api/services/', backendData); + const { data } = await apiClient.post('/services/', backendData); return data; }, onSuccess: () => { @@ -94,7 +94,7 @@ export const useUpdateService = () => { if (updates.description !== undefined) backendData.description = updates.description; if (updates.photos !== undefined) backendData.photos = updates.photos; - const { data } = await apiClient.patch(`/api/services/${id}/`, backendData); + const { data } = await apiClient.patch(`/services/${id}/`, backendData); return data; }, onSuccess: () => { @@ -111,7 +111,7 @@ export const useDeleteService = () => { return useMutation({ mutationFn: async (id: string) => { - await apiClient.delete(`/api/services/${id}/`); + await apiClient.delete(`/services/${id}/`); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['services'] }); @@ -129,7 +129,7 @@ export const useReorderServices = () => { mutationFn: async (orderedIds: string[]) => { // Convert string IDs to numbers for the backend const order = orderedIds.map(id => parseInt(id, 10)); - const { data } = await apiClient.post('/api/services/reorder/', { order }); + const { data } = await apiClient.post('/services/reorder/', { order }); return data; }, onSuccess: () => { diff --git a/frontend/src/hooks/useUserNotifications.ts b/frontend/src/hooks/useUserNotifications.ts index 5a61298..df0671c 100644 --- a/frontend/src/hooks/useUserNotifications.ts +++ b/frontend/src/hooks/useUserNotifications.ts @@ -6,6 +6,7 @@ import { useEffect, useRef, useCallback } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { getCookie } from '../utils/cookies'; +import { getWebSocketUrl } from '../utils/domain'; import { UserEmail } from '../api/profile'; interface WebSocketMessage { @@ -104,10 +105,8 @@ export function useUserNotifications(options: UseUserNotificationsOptions = {}) wsRef.current.close(); } - // Determine WebSocket host - use api subdomain for WebSocket - const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsHost = `api.lvh.me:8000`; // In production, this would come from config - const url = `${wsProtocol}//${wsHost}/ws/user/?token=${token}`; + // Build WebSocket URL dynamically + const url = getWebSocketUrl(`user/?token=${token}`); console.log('UserNotifications WebSocket: Connecting'); const ws = new WebSocket(url); diff --git a/frontend/src/hooks/useUsers.ts b/frontend/src/hooks/useUsers.ts index 035bf84..b7ec15f 100644 --- a/frontend/src/hooks/useUsers.ts +++ b/frontend/src/hooks/useUsers.ts @@ -21,7 +21,7 @@ export const useUsers = () => { return useQuery({ queryKey: ['staff'], queryFn: async () => { - const response = await apiClient.get('/api/staff/'); + const response = await apiClient.get('/staff/'); return response.data; }, }); @@ -35,7 +35,7 @@ export const useStaffForAssignment = () => { return useQuery<{ id: string; name: string; email: string; role: string }[]>({ queryKey: ['staffForAssignment'], queryFn: async () => { - const response = await apiClient.get('/api/staff/'); + const response = await apiClient.get('/staff/'); return response.data.map((user: StaffUser) => ({ id: String(user.id), name: user.name || user.email, // 'name' field from serializer (full_name) @@ -54,7 +54,7 @@ export const usePlatformStaffForAssignment = () => { return useQuery<{ id: string; name: string; email: string; role: string }[]>({ queryKey: ['platformStaffForAssignment'], queryFn: async () => { - const response = await apiClient.get('/api/platform/users/'); + const response = await apiClient.get('/platform/users/'); // Filter to only platform-level roles and format for dropdown const platformRoles = ['superuser', 'platform_manager', 'platform_support']; return response.data @@ -77,7 +77,7 @@ export const useUpdateStaffPermissions = () => { return useMutation({ mutationFn: async ({ userId, permissions }: { userId: string | number; permissions: Record }) => { - const response = await apiClient.patch(`/api/staff/${userId}/`, { permissions }); + const response = await apiClient.patch(`/staff/${userId}/`, { permissions }); return response.data; }, onSuccess: () => { diff --git a/frontend/src/pages/EmailVerificationRequired.tsx b/frontend/src/pages/EmailVerificationRequired.tsx index 434d2d0..0e0ca5a 100644 --- a/frontend/src/pages/EmailVerificationRequired.tsx +++ b/frontend/src/pages/EmailVerificationRequired.tsx @@ -25,7 +25,7 @@ const EmailVerificationRequired: React.FC = () => { setSent(false); try { - await apiClient.post('/api/auth/email/verify/send/'); + await apiClient.post('/auth/email/verify/send/'); setSent(true); setTimeout(() => setSent(false), 5000); // Hide success message after 5 seconds } catch (err: any) { diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index 23c3e44..ab66878 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -46,11 +46,20 @@ const LoginPage: React.FC = () => { const currentHostname = window.location.hostname; const currentPort = window.location.port; const portStr = currentPort ? `:${currentPort}` : ''; + const protocol = window.location.protocol; + + // Extract base domain from current hostname + // For lvh.me: smoothschedule.com becomes base, subdomain.smoothschedule.com has subdomain + // For production: smoothschedule.com becomes base, subdomain.smoothschedule.com has subdomain + const hostnameParts = currentHostname.split('.'); + const baseDomain = hostnameParts.length >= 2 + ? hostnameParts.slice(-2).join('.') + : currentHostname; // Check domain type - const isRootDomain = currentHostname === 'lvh.me' || currentHostname === 'localhost'; - const isPlatformDomain = currentHostname === 'platform.lvh.me'; - const currentSubdomain = currentHostname.split('.')[0]; + const isRootDomain = currentHostname === baseDomain || currentHostname === 'localhost'; + const isPlatformDomain = currentHostname === `platform.${baseDomain}`; + const currentSubdomain = hostnameParts[0]; const isBusinessSubdomain = !isRootDomain && !isPlatformDomain && currentSubdomain !== 'api'; // Platform users (superuser, platform_manager, platform_support) @@ -95,19 +104,21 @@ const LoginPage: React.FC = () => { // Determine target subdomain for redirect let targetSubdomain: string | null = null; - if (isPlatformUser) { + // Platform users should be redirected to platform subdomain if not already there + if (isPlatformUser && !isPlatformDomain) { targetSubdomain = 'platform'; - } else if (user.business_subdomain) { + } else if (isBusinessUser && user.business_subdomain && !isBusinessSubdomain) { + // Business users should be on their business subdomain targetSubdomain = user.business_subdomain; } // Check if we need to redirect to a different subdomain - const isOnTargetSubdomain = currentHostname === `${targetSubdomain}.lvh.me`; - const needsRedirect = targetSubdomain && !isOnTargetSubdomain; + const needsRedirect = targetSubdomain !== null; if (needsRedirect) { // Pass tokens in URL to ensure they're available immediately on the new subdomain - window.location.href = `http://${targetSubdomain}.lvh.me${portStr}/?access_token=${data.access}&refresh_token=${data.refresh}`; + const targetHostname = `${targetSubdomain}.${baseDomain}`; + window.location.href = `${protocol}//${targetHostname}${portStr}/?access_token=${data.access}&refresh_token=${data.refresh}`; return; } diff --git a/frontend/src/pages/MFAVerifyPage.tsx b/frontend/src/pages/MFAVerifyPage.tsx index 5a0dc43..f4b27a0 100644 --- a/frontend/src/pages/MFAVerifyPage.tsx +++ b/frontend/src/pages/MFAVerifyPage.tsx @@ -8,6 +8,7 @@ import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { sendMFALoginCode, verifyMFALogin } from '../api/mfa'; import { setCookie } from '../utils/cookies'; +import { buildSubdomainUrl } from '../utils/domain'; import SmoothScheduleLogo from '../components/SmoothScheduleLogo'; import { AlertCircle, @@ -146,8 +147,6 @@ const MFAVerifyPage: React.FC = () => { // Get redirect info from user const user = response.user; const currentHostname = window.location.hostname; - const currentPort = window.location.port; - const portStr = currentPort ? `:${currentPort}` : ''; // Determine target subdomain const isPlatformUser = ['superuser', 'platform_manager', 'platform_support'].includes(user.role); @@ -160,11 +159,12 @@ const MFAVerifyPage: React.FC = () => { } // Check if we need to redirect - const isOnTargetSubdomain = currentHostname === `${targetSubdomain}.lvh.me`; - const needsRedirect = targetSubdomain && !isOnTargetSubdomain; + const targetHostname = targetSubdomain ? `${targetSubdomain}.${window.location.hostname.split('.').slice(-2).join('.')}` : null; + const needsRedirect = targetSubdomain && targetHostname && currentHostname !== targetHostname; - if (needsRedirect) { - window.location.href = `http://${targetSubdomain}.lvh.me${portStr}/?access_token=${response.access}&refresh_token=${response.refresh}`; + if (needsRedirect && targetSubdomain) { + const targetUrl = buildSubdomainUrl(targetSubdomain, `/?access_token=${response.access}&refresh_token=${response.refresh}`); + window.location.href = targetUrl; return; } diff --git a/frontend/src/pages/OAuthCallback.tsx b/frontend/src/pages/OAuthCallback.tsx index fa44883..77a26f0 100644 --- a/frontend/src/pages/OAuthCallback.tsx +++ b/frontend/src/pages/OAuthCallback.tsx @@ -8,6 +8,7 @@ import { useNavigate, useParams, useLocation } from 'react-router-dom'; import { Loader2, AlertCircle, CheckCircle } from 'lucide-react'; import { handleOAuthCallback } from '../api/oauth'; import { setCookie } from '../utils/cookies'; +import { getCookieDomain, buildSubdomainUrl } from '../utils/domain'; import SmoothScheduleLogo from '../components/SmoothScheduleLogo'; const OAuthCallback: React.FC = () => { @@ -56,7 +57,8 @@ const OAuthCallback: React.FC = () => { setCookie('refresh_token', response.refresh, 7); // Clear session cookie to prevent interference with JWT - document.cookie = 'sessionid=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.lvh.me'; + const cookieDomain = getCookieDomain(); + document.cookie = `sessionid=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${cookieDomain}`; document.cookie = 'sessionid=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; setStatus('success'); @@ -64,27 +66,27 @@ const OAuthCallback: React.FC = () => { // Determine redirect URL based on user role const user = response.user; const currentHostname = window.location.hostname; - const currentPort = window.location.port; let targetUrl = '/'; let needsRedirect = false; + let targetSubdomain: string | null = null; // Platform users (superuser, platform_manager, platform_support) if (['superuser', 'platform_manager', 'platform_support'].includes(user.role)) { - const targetHostname = 'platform.lvh.me'; - needsRedirect = currentHostname !== targetHostname; - if (needsRedirect) { - const portStr = currentPort ? `:${currentPort}` : ''; - targetUrl = `http://${targetHostname}${portStr}/`; - } + targetSubdomain = 'platform'; } // Business users - redirect to their business subdomain else if (user.business_subdomain) { - const targetHostname = `${user.business_subdomain}.lvh.me`; + targetSubdomain = user.business_subdomain; + } + + // Check if redirect is needed + if (targetSubdomain) { + const baseDomain = window.location.hostname.split('.').slice(-2).join('.'); + const targetHostname = `${targetSubdomain}.${baseDomain}`; needsRedirect = currentHostname !== targetHostname; if (needsRedirect) { - const portStr = currentPort ? `:${currentPort}` : ''; - targetUrl = `http://${targetHostname}${portStr}/`; + targetUrl = buildSubdomainUrl(targetSubdomain, '/'); } } @@ -146,20 +148,8 @@ const OAuthCallback: React.FC = () => { }, [provider, location, navigate]); const handleTryAgain = () => { - const currentHostname = window.location.hostname; - const currentPort = window.location.port; - const portStr = currentPort ? `:${currentPort}` : ''; - - // Redirect to login page - if (currentHostname.includes('platform.lvh.me')) { - window.location.href = `http://platform.lvh.me${portStr}/login`; - } else if (currentHostname.includes('.lvh.me')) { - // On business subdomain - go to their login - window.location.href = `http://${currentHostname}${portStr}/login`; - } else { - // Fallback - navigate('/login'); - } + // Simply navigate to login on current subdomain + navigate('/login'); }; return ( diff --git a/frontend/src/pages/TenantOnboardPage.tsx b/frontend/src/pages/TenantOnboardPage.tsx index c7cff62..c36adbd 100644 --- a/frontend/src/pages/TenantOnboardPage.tsx +++ b/frontend/src/pages/TenantOnboardPage.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react'; import { useSearchParams, useNavigate } from 'react-router-dom'; import { CheckCircle, Mail, Lock, User, Building2, CreditCard, ArrowRight, ArrowLeft, Loader } from 'lucide-react'; import { useInvitationByToken, useAcceptInvitation } from '../hooks/usePlatform'; +import { getBaseDomain, buildSubdomainUrl } from '../utils/domain'; const TenantOnboardPage: React.FC = () => { const [searchParams] = useSearchParams(); @@ -387,12 +388,12 @@ const TenantOnboardPage: React.FC = () => { placeholder="mybusiness" /> - .lvh.me + .{getBaseDomain()} {errors.subdomain &&

{errors.subdomain}

}

- This will be your business URL: {formData.subdomain || 'your-business'}.lvh.me + This will be your business URL: {formData.subdomain || 'your-business'}.{getBaseDomain()}

@@ -470,7 +471,7 @@ const TenantOnboardPage: React.FC = () => {

What's Next?

  • ✓ Your account has been created
  • -
  • ✓ Business subdomain: {formData.subdomain}.lvh.me
  • +
  • ✓ Business subdomain: {formData.subdomain}.{getBaseDomain()}
  • ✓ You can now log in and start using SmoothSchedule
@@ -478,7 +479,7 @@ const TenantOnboardPage: React.FC = () => {