Replaced the blank base64 encoded logo with the actual SmoothSchedule logo in the email rendering pipeline. A Playwright E2E test was run to verify that the logo is correctly displayed in the email preview modal, ensuring it loads with natural dimensions and is visible.
240 lines
7.5 KiB
TypeScript
240 lines
7.5 KiB
TypeScript
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 {
|
||
email: string;
|
||
password: string;
|
||
role: string;
|
||
label: string;
|
||
color: string;
|
||
category: 'platform' | 'business' | 'customer';
|
||
}
|
||
|
||
const testUsers: TestUser[] = [
|
||
{
|
||
email: 'superuser@platform.com',
|
||
password: 'test123',
|
||
role: 'SUPERUSER',
|
||
label: 'Platform Superuser',
|
||
color: 'bg-purple-600 hover:bg-purple-700',
|
||
category: 'platform',
|
||
},
|
||
{
|
||
email: 'manager@platform.com',
|
||
password: 'test123',
|
||
role: 'PLATFORM_MANAGER',
|
||
label: 'Platform Manager',
|
||
color: 'bg-blue-600 hover:bg-blue-700',
|
||
category: 'platform',
|
||
},
|
||
{
|
||
email: 'sales@platform.com',
|
||
password: 'test123',
|
||
role: 'PLATFORM_SALES',
|
||
label: 'Platform Sales',
|
||
color: 'bg-green-600 hover:bg-green-700',
|
||
category: 'platform',
|
||
},
|
||
{
|
||
email: 'support@platform.com',
|
||
password: 'test123',
|
||
role: 'PLATFORM_SUPPORT',
|
||
label: 'Platform Support',
|
||
color: 'bg-yellow-600 hover:bg-yellow-700',
|
||
category: 'platform',
|
||
},
|
||
{
|
||
email: 'owner@demo.com',
|
||
password: 'test123',
|
||
role: 'TENANT_OWNER',
|
||
label: 'Business Owner',
|
||
color: 'bg-indigo-600 hover:bg-indigo-700',
|
||
category: 'business',
|
||
},
|
||
{
|
||
email: 'manager@demo.com',
|
||
password: 'test123',
|
||
role: 'TENANT_MANAGER',
|
||
label: 'Business Manager',
|
||
color: 'bg-pink-600 hover:bg-pink-700',
|
||
category: 'business',
|
||
},
|
||
{
|
||
email: 'staff@demo.com',
|
||
password: 'test123',
|
||
role: 'TENANT_STAFF',
|
||
label: 'Staff Member',
|
||
color: 'bg-teal-600 hover:bg-teal-700',
|
||
category: 'business',
|
||
},
|
||
{
|
||
email: 'customer@demo.com',
|
||
password: 'test123',
|
||
role: 'CUSTOMER',
|
||
label: 'Customer',
|
||
color: 'bg-orange-600 hover:bg-orange-700',
|
||
category: 'customer',
|
||
},
|
||
];
|
||
|
||
type UserFilter = 'all' | 'platform' | 'business';
|
||
|
||
interface DevQuickLoginProps {
|
||
embedded?: boolean;
|
||
filter?: UserFilter;
|
||
}
|
||
|
||
export function DevQuickLogin({ embedded = false, filter = 'all' }: 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;
|
||
}
|
||
|
||
// Filter users based on the filter prop
|
||
const filteredUsers = testUsers.filter((user) => {
|
||
if (filter === 'all') return true;
|
||
if (filter === 'platform') return user.category === 'platform';
|
||
if (filter === 'business') return user.category === 'business';
|
||
return true;
|
||
});
|
||
|
||
const handleQuickLogin = async (user: TestUser) => {
|
||
setLoading(user.email);
|
||
try {
|
||
// Call custom login API that supports email login
|
||
const response = await apiClient.post('/auth/login/', {
|
||
email: user.email,
|
||
password: user.password,
|
||
});
|
||
|
||
// Store token in cookie (use 'access_token' to match what client.ts expects)
|
||
setCookie('access_token', response.data.access, 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, '/dashboard');
|
||
return;
|
||
}
|
||
|
||
// Already on correct subdomain - navigate to dashboard
|
||
window.location.href = '/dashboard';
|
||
} 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">
|
||
{filteredUsers.map((user) => (
|
||
<button
|
||
key={user.email}
|
||
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.email ? (
|
||
<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>
|
||
);
|
||
}
|