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:
231
legacy_reference/frontend/src/hooks/useAuth.ts
Normal file
231
legacy_reference/frontend/src/hooks/useAuth.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* Authentication Hooks
|
||||
*/
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
login,
|
||||
logout,
|
||||
getCurrentUser,
|
||||
masquerade,
|
||||
stopMasquerade,
|
||||
LoginCredentials,
|
||||
User,
|
||||
MasqueradeStackEntry
|
||||
} from '../api/auth';
|
||||
import { getCookie, setCookie, deleteCookie } from '../utils/cookies';
|
||||
|
||||
/**
|
||||
* Hook to get current user
|
||||
*/
|
||||
export const useCurrentUser = () => {
|
||||
return useQuery<User | null, Error>({
|
||||
queryKey: ['currentUser'],
|
||||
queryFn: async () => {
|
||||
// Check if token exists before making request (from cookie)
|
||||
const token = getCookie('access_token');
|
||||
|
||||
if (!token) {
|
||||
return null; // No token, return null instead of making request
|
||||
}
|
||||
try {
|
||||
return await getCurrentUser();
|
||||
} catch (error) {
|
||||
// If getCurrentUser fails (e.g., 401), return null
|
||||
// The API client interceptor will handle token refresh
|
||||
console.error('Failed to get current user:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
retry: 1, // Retry once in case of token refresh
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
refetchOnMount: true, // Always refetch when component mounts
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to login
|
||||
*/
|
||||
export const useLogin = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: login,
|
||||
onSuccess: (data) => {
|
||||
// Store tokens in cookies (domain=.lvh.me for cross-subdomain access)
|
||||
setCookie('access_token', data.access, 7);
|
||||
setCookie('refresh_token', data.refresh, 7);
|
||||
|
||||
// Set user in cache
|
||||
queryClient.setQueryData(['currentUser'], data.user);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to logout
|
||||
*/
|
||||
export const useLogout = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: logout,
|
||||
onSuccess: () => {
|
||||
// Clear tokens (from cookies)
|
||||
deleteCookie('access_token');
|
||||
deleteCookie('refresh_token');
|
||||
|
||||
// Clear user cache
|
||||
queryClient.removeQueries({ queryKey: ['currentUser'] });
|
||||
queryClient.clear();
|
||||
|
||||
// Redirect to login page
|
||||
window.location.href = '/login';
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if user is authenticated
|
||||
*/
|
||||
export const useIsAuthenticated = (): boolean => {
|
||||
const { data: user, isLoading } = useCurrentUser();
|
||||
return !isLoading && !!user;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to masquerade as another user
|
||||
*/
|
||||
export const useMasquerade = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (username: string) => {
|
||||
// Get current masquerading stack from localStorage
|
||||
const stackJson = localStorage.getItem('masquerade_stack');
|
||||
const currentStack: MasqueradeStackEntry[] = stackJson ? JSON.parse(stackJson) : [];
|
||||
|
||||
// Call masquerade API with current stack
|
||||
return masquerade(username, currentStack);
|
||||
},
|
||||
onSuccess: async (data) => {
|
||||
// Store the updated masquerading stack
|
||||
if (data.masquerade_stack) {
|
||||
localStorage.setItem('masquerade_stack', JSON.stringify(data.masquerade_stack));
|
||||
}
|
||||
|
||||
const user = data.user;
|
||||
const currentHostname = window.location.hostname;
|
||||
const currentPort = window.location.port;
|
||||
|
||||
let targetSubdomain: string | null = null;
|
||||
|
||||
if (['superuser', 'platform_manager', 'platform_support'].includes(user.role)) {
|
||||
targetSubdomain = 'platform';
|
||||
} else if (user.business_subdomain) {
|
||||
targetSubdomain = user.business_subdomain;
|
||||
}
|
||||
|
||||
const needsRedirect = targetSubdomain && currentHostname !== `${targetSubdomain}.lvh.me`;
|
||||
|
||||
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/', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
});
|
||||
} catch (e) {
|
||||
// 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}`;
|
||||
|
||||
window.location.href = redirectUrl;
|
||||
return;
|
||||
}
|
||||
|
||||
// If no redirect needed (same subdomain), we can just set cookies and reload
|
||||
setCookie('access_token', data.access, 7);
|
||||
setCookie('refresh_token', data.refresh, 7);
|
||||
queryClient.setQueryData(['currentUser'], data.user);
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to stop masquerading and return to previous user
|
||||
*/
|
||||
export const useStopMasquerade = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
// Get current masquerading stack from localStorage
|
||||
const stackJson = localStorage.getItem('masquerade_stack');
|
||||
const currentStack: MasqueradeStackEntry[] = stackJson ? JSON.parse(stackJson) : [];
|
||||
|
||||
if (currentStack.length === 0) {
|
||||
throw new Error('No masquerading session to stop');
|
||||
}
|
||||
|
||||
// Call stop_masquerade API with current stack
|
||||
return stopMasquerade(currentStack);
|
||||
},
|
||||
onSuccess: async (data) => {
|
||||
// Update the masquerading stack
|
||||
if (data.masquerade_stack && data.masquerade_stack.length > 0) {
|
||||
localStorage.setItem('masquerade_stack', JSON.stringify(data.masquerade_stack));
|
||||
} else {
|
||||
// Clear the stack if empty
|
||||
localStorage.removeItem('masquerade_stack');
|
||||
}
|
||||
|
||||
const user = data.user;
|
||||
const currentHostname = window.location.hostname;
|
||||
const currentPort = window.location.port;
|
||||
|
||||
let targetSubdomain: string | null = null;
|
||||
|
||||
if (['superuser', 'platform_manager', 'platform_support'].includes(user.role)) {
|
||||
targetSubdomain = 'platform';
|
||||
} else if (user.business_subdomain) {
|
||||
targetSubdomain = user.business_subdomain;
|
||||
}
|
||||
|
||||
const needsRedirect = targetSubdomain && currentHostname !== `${targetSubdomain}.lvh.me`;
|
||||
|
||||
if (needsRedirect) {
|
||||
// CRITICAL: Clear the session cookie BEFORE redirect
|
||||
try {
|
||||
await fetch('http://api.lvh.me:8000/api/auth/logout/', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
});
|
||||
} catch (e) {
|
||||
// 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}`;
|
||||
|
||||
window.location.href = redirectUrl;
|
||||
return;
|
||||
}
|
||||
|
||||
// If no redirect needed (same subdomain), we can just set cookies and reload
|
||||
setCookie('access_token', data.access, 7);
|
||||
setCookie('refresh_token', data.refresh, 7);
|
||||
queryClient.setQueryData(['currentUser'], data.user);
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user