237 lines
9 KiB
TypeScript
237 lines
9 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { Link, useNavigate } from 'react-router-dom';
|
|
import { useForm } from 'react-hook-form';
|
|
import { useAuthStore } from '../store/authStore';
|
|
import { LogIn, Mail, Lock, Building } from 'lucide-react';
|
|
import { apiClient } from '../services/api/config';
|
|
import { useLanguage } from '../context/LanguageContext';
|
|
|
|
interface LoginFormData {
|
|
tenantName?: string;
|
|
userNameOrEmailAddress: string;
|
|
password: string;
|
|
rememberMe?: boolean;
|
|
}
|
|
|
|
interface TenantInfo {
|
|
success: boolean;
|
|
tenantId?: string;
|
|
name?: string;
|
|
isActive?: boolean;
|
|
}
|
|
|
|
const LoginWithTenant: React.FC = () => {
|
|
const navigate = useNavigate();
|
|
const { login, isLoading } = useAuthStore();
|
|
const { t } = useLanguage();
|
|
const [isMultiTenancyEnabled, setIsMultiTenancyEnabled] = useState(false);
|
|
const [tenantInfo, setTenantInfo] = useState<TenantInfo | null>(null);
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
formState: { errors },
|
|
watch,
|
|
} = useForm<LoginFormData>({
|
|
defaultValues: {
|
|
rememberMe: true
|
|
}
|
|
});
|
|
|
|
const tenantName = watch('tenantName');
|
|
|
|
useEffect(() => {
|
|
checkMultiTenancy();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (tenantName) {
|
|
checkTenant(tenantName);
|
|
} else {
|
|
setTenantInfo(null);
|
|
localStorage.removeItem('tenant_id');
|
|
}
|
|
}, [tenantName]);
|
|
|
|
const checkMultiTenancy = async () => {
|
|
try {
|
|
const response = await apiClient.get('/api/abp/application-configuration');
|
|
setIsMultiTenancyEnabled(response.data.multiTenancy?.isEnabled || false);
|
|
} catch (error) {
|
|
console.error('Failed to check multi-tenancy:', error);
|
|
}
|
|
};
|
|
|
|
const checkTenant = async (name: string) => {
|
|
try {
|
|
const response = await apiClient.post<TenantInfo>('/api/abp/multi-tenancy/tenants/by-name/' + name);
|
|
if (response.data.success && response.data.tenantId) {
|
|
setTenantInfo(response.data);
|
|
localStorage.setItem('tenant_id', response.data.tenantId);
|
|
} else {
|
|
setTenantInfo({ success: false });
|
|
localStorage.removeItem('tenant_id');
|
|
}
|
|
} catch (error) {
|
|
setTenantInfo({ success: false });
|
|
localStorage.removeItem('tenant_id');
|
|
}
|
|
};
|
|
|
|
const onSubmit = async (data: LoginFormData) => {
|
|
try {
|
|
await login({
|
|
username: data.userNameOrEmailAddress,
|
|
password: data.password,
|
|
});
|
|
navigate('/');
|
|
} catch (error) {
|
|
// Error is handled in the store
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center py-24 px-4 sm:px-6 lg:px-8">
|
|
<div className="max-w-md w-full">
|
|
<div className="bg-white rounded-lg shadow-xl p-8">
|
|
<div className="text-center mb-4">
|
|
<div className="inline-flex items-center justify-center w-16 h-16 bg-blue-600 rounded-full mb-4">
|
|
<LogIn className="h-8 w-8 text-white" />
|
|
</div>
|
|
<h2 className="text-3xl font-bold text-gray-900">
|
|
{t('login.welcome')}
|
|
</h2>
|
|
<p className="mt-2 text-sm text-gray-600">
|
|
{t('login.subtitle')}{' '}
|
|
<Link
|
|
to="/register"
|
|
className="font-medium text-blue-600 hover:text-blue-500 transition-colors"
|
|
>
|
|
{t('login.createAccount')}
|
|
</Link>
|
|
</p>
|
|
</div>
|
|
|
|
<form className="space-y-6" onSubmit={handleSubmit(onSubmit)}>
|
|
{/* Tenant Field - Only show if multi-tenancy is enabled */}
|
|
{isMultiTenancyEnabled && (
|
|
<div>
|
|
<label htmlFor="tenant" className="block text-sm font-medium text-gray-700 mb-2">
|
|
{t('login.organization')}
|
|
</label>
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
<Building className="h-5 w-5 text-gray-400" />
|
|
</div>
|
|
<input
|
|
{...register('tenantName')}
|
|
type="text"
|
|
autoComplete="organization"
|
|
className="appearance-none relative block w-full px-3 py-3 pl-3 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
|
placeholder={t('login.organizationPlaceholder')}
|
|
/>
|
|
</div>
|
|
{tenantInfo && !tenantInfo.success && tenantName && (
|
|
<p className="mt-2 text-sm text-red-600">
|
|
{t('login.organizationNotFound')}
|
|
</p>
|
|
)}
|
|
{tenantInfo && tenantInfo.success && (
|
|
<p className="mt-2 text-sm text-green-600">
|
|
✓ {tenantInfo.name} {t('login.organizationFound')}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Username/Email Field */}
|
|
<div>
|
|
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">
|
|
{t('login.email')}
|
|
</label>
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
<Mail className="h-5 w-5 text-gray-400" />
|
|
</div>
|
|
<input
|
|
{...register('userNameOrEmailAddress', {
|
|
required: t('login.emailRequired'),
|
|
})}
|
|
type="text"
|
|
autoComplete="username"
|
|
className="appearance-none relative block w-full px-3 py-3 pl-3 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
|
placeholder={t('login.emailPlaceholder')}
|
|
/>
|
|
</div>
|
|
{errors.userNameOrEmailAddress && (
|
|
<p className="mt-2 text-sm text-red-600">
|
|
{errors.userNameOrEmailAddress.message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Password Field */}
|
|
<div>
|
|
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2">
|
|
{t('login.password')}
|
|
</label>
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
<Lock className="h-5 w-5 text-gray-400" />
|
|
</div>
|
|
<input
|
|
{...register('password', {
|
|
required: t('login.passwordRequired'),
|
|
})}
|
|
type="password"
|
|
autoComplete="current-password"
|
|
className="appearance-none relative block w-full px-3 py-3 pl-3 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
|
placeholder={t('login.passwordPlaceholder')}
|
|
/>
|
|
</div>
|
|
{errors.password && (
|
|
<p className="mt-2 text-sm text-red-600">
|
|
{errors.password.message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center">
|
|
<input
|
|
{...register('rememberMe')}
|
|
id="remember-me"
|
|
type="checkbox"
|
|
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
|
/>
|
|
<label htmlFor="remember-me" className="ml-2 block text-sm text-gray-700">
|
|
{t('login.rememberMe')}
|
|
</label>
|
|
</div>
|
|
|
|
<div className="text-sm">
|
|
<a href="#" className="font-medium text-blue-600 hover:text-blue-500 transition-colors">
|
|
{t('login.forgotPassword')}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<button
|
|
type="submit"
|
|
disabled={isLoading || (tenantInfo !== null && !tenantInfo.success && !!tenantName)}
|
|
className="group relative w-full flex justify-center py-3 px-4 border border-transparent text-sm font-medium rounded-lg text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
|
|
>
|
|
<span className="absolute left-0 inset-y-0 flex items-center pl-3">
|
|
<LogIn className="h-5 w-5 text-blue-500 group-hover:text-blue-400" />
|
|
</span>
|
|
{isLoading ? t('login.signingIn') : t('login.signIn')}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default LoginWithTenant;
|