Files
smoothschedule/frontend/src/components/DevQuickLogin.tsx
poduck 05ebd0f2bb feat: Email templates, bulk delete, communication credits, plan features
- Add email template presets for Browse Templates tab (12 templates)
- Add bulk selection and deletion for My Templates tab
- Add communication credits system with Twilio integration
- Add payment views for credit purchases and auto-reload
- Add SMS reminder and masked calling plan permissions
- Fix appointment status mapping (frontend/backend mismatch)
- Clear masquerade stack on login/logout for session hygiene
- Update platform settings with credit configuration
- Add new migrations for Twilio and Stripe payment fields

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-02 01:42:38 -05:00

220 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react';
import apiClient from '../api/client';
import { setCookie } from '../utils/cookies';
import { useQueryClient } from '@tanstack/react-query';
import { getBaseDomain, buildSubdomainUrl } from '../utils/domain';
export interface TestUser {
username: string;
password: string;
role: string;
label: string;
color: string;
}
const testUsers: TestUser[] = [
{
username: 'superuser',
password: 'test123',
role: 'SUPERUSER',
label: 'Platform Superuser',
color: 'bg-purple-600 hover:bg-purple-700',
},
{
username: 'platform_manager',
password: 'test123',
role: 'PLATFORM_MANAGER',
label: 'Platform Manager',
color: 'bg-blue-600 hover:bg-blue-700',
},
{
username: 'platform_sales',
password: 'test123',
role: 'PLATFORM_SALES',
label: 'Platform Sales',
color: 'bg-green-600 hover:bg-green-700',
},
{
username: 'platform_support',
password: 'test123',
role: 'PLATFORM_SUPPORT',
label: 'Platform Support',
color: 'bg-yellow-600 hover:bg-yellow-700',
},
{
username: 'tenant_owner',
password: 'test123',
role: 'TENANT_OWNER',
label: 'Business Owner',
color: 'bg-indigo-600 hover:bg-indigo-700',
},
{
username: 'tenant_manager',
password: 'test123',
role: 'TENANT_MANAGER',
label: 'Business Manager',
color: 'bg-pink-600 hover:bg-pink-700',
},
{
username: 'tenant_staff',
password: 'test123',
role: 'TENANT_STAFF',
label: 'Staff Member',
color: 'bg-teal-600 hover:bg-teal-700',
},
{
username: 'customer',
password: 'test123',
role: 'CUSTOMER',
label: 'Customer',
color: 'bg-orange-600 hover:bg-orange-700',
},
];
interface DevQuickLoginProps {
embedded?: boolean;
}
export function DevQuickLogin({ embedded = false }: DevQuickLoginProps) {
const queryClient = useQueryClient();
const [loading, setLoading] = useState<string | null>(null);
const [isMinimized, setIsMinimized] = useState(false);
// Only show in development
if (import.meta.env.PROD) {
return null;
}
const handleQuickLogin = async (user: TestUser) => {
setLoading(user.username);
try {
// Call token auth API
const response = await apiClient.post('/auth-token/', {
username: user.username,
password: user.password,
});
// Store token in cookie (use 'access_token' to match what client.ts expects)
setCookie('access_token', response.data.token, 7);
// Clear any existing masquerade stack - this is a fresh login
localStorage.removeItem('masquerade_stack');
// Fetch user data to determine redirect
const userResponse = await apiClient.get('/auth/me/');
const userData = userResponse.data;
// Determine the correct subdomain based on user role
const currentHostname = window.location.hostname;
const currentPort = window.location.port;
let targetSubdomain: string | null = null;
// Platform users (superuser, platform_manager, platform_support)
if (['superuser', 'platform_manager', 'platform_support'].includes(userData.role)) {
targetSubdomain = 'platform';
}
// Business users - redirect to their business subdomain
else if (userData.business_subdomain) {
targetSubdomain = userData.business_subdomain;
}
// Check if we need to redirect to a different subdomain
const baseDomain = getBaseDomain();
const isOnTargetSubdomain = currentHostname === (targetSubdomain ? `${targetSubdomain}.${baseDomain}` : baseDomain);
const needsRedirect = targetSubdomain && !isOnTargetSubdomain;
if (needsRedirect) {
// Redirect to the correct subdomain
window.location.href = buildSubdomainUrl(targetSubdomain, '/');
return;
}
// Already on correct subdomain - just reload to update auth state
window.location.reload();
} catch (error: any) {
console.error('Quick login failed:', error);
alert(`Failed to login as ${user.label}: ${error.message || 'Unknown error'}`);
} finally {
setLoading(null);
}
};
if (!embedded && isMinimized) {
return (
<div className="fixed bottom-4 right-4 z-50">
<button
onClick={() => setIsMinimized(false)}
className="bg-gray-800 text-white px-4 py-2 rounded-lg shadow-lg hover:bg-gray-700 transition-colors"
>
🔓 Quick Login
</button>
</div>
);
}
const containerClasses = embedded
? "w-full bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 mt-6"
: "fixed bottom-4 right-4 z-50 bg-white rounded-lg shadow-2xl border-2 border-gray-300 p-4 max-w-md";
return (
<div className={containerClasses}>
<div className="flex items-center justify-between mb-3">
<h3 className="font-bold text-gray-800 dark:text-white flex items-center gap-2">
<span>🔓</span>
<span>Quick Login (Dev Only)</span>
</h3>
{!embedded && (
<button
onClick={() => setIsMinimized(true)}
className="text-gray-500 hover:text-gray-700 text-xl leading-none"
>
×
</button>
)}
</div>
<div className="grid grid-cols-2 gap-2">
{testUsers.map((user) => (
<button
key={user.username}
onClick={() => handleQuickLogin(user)}
disabled={loading !== null}
className={`${user.color} text-white px-3 py-2 rounded text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed`}
>
{loading === user.username ? (
<span className="flex items-center justify-center">
<svg className="animate-spin h-4 w-4 mr-2" viewBox="0 0 24 24">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
fill="none"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
Logging in...
</span>
) : (
<div className="text-left">
<div className="font-semibold">{user.label}</div>
<div className="text-xs opacity-90">{user.role}</div>
</div>
)}
</button>
))}
</div>
<div className="mt-3 text-xs text-gray-500 dark:text-gray-400 text-center">
Password for all: <code className="bg-gray-100 dark:bg-gray-700 px-1 rounded">test123</code>
</div>
</div>
);
}