232 lines
7.8 KiB
TypeScript
232 lines
7.8 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { Box, Button, TextField, Container, Alert, Paper } from '@mui/material';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { useAuthStore } from '../../store/authStore';
|
|
import { accountService } from '../../api/accountService';
|
|
import type { Organization } from '../../api/accountService';
|
|
import { DbSelectionDialog } from './DbSelectionDialog';
|
|
import { useToastStore } from '../../store/toastStore';
|
|
|
|
export const Login: React.FC = () => {
|
|
const [username, setUsername] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [isInvalid, setIsInvalid] = useState(false);
|
|
const [loading, setLoading] = useState(false);
|
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
const [dialogOrgs, setDialogOrgs] = useState<Organization[]>([]);
|
|
const [tempLoginResult, setTempLoginResult] = useState<Awaited<ReturnType<typeof accountService.login>> | null>(null);
|
|
|
|
const { login, isAuthenticated } = useAuthStore();
|
|
const navigate = useNavigate();
|
|
|
|
// If already logged in, redirect
|
|
useEffect(() => {
|
|
if (isAuthenticated) {
|
|
navigate('/activity-feeds', { replace: true });
|
|
}
|
|
}, [isAuthenticated, navigate]);
|
|
|
|
const commitLogin = (
|
|
result: NonNullable<typeof tempLoginResult>,
|
|
org: { org_id: number | string; db_name: string },
|
|
) => {
|
|
login({
|
|
access_token: result.access_token,
|
|
refresh_token: result.refresh_token,
|
|
user_id: result.user.user_id,
|
|
username: result.user.username,
|
|
email: result.user.email,
|
|
role: result.user.role_name,
|
|
org_id: String(org.org_id),
|
|
db_name: org.db_name,
|
|
});
|
|
navigate('/activity-feeds', { replace: true });
|
|
};
|
|
|
|
const handleLogin = async () => {
|
|
setIsInvalid(false);
|
|
setLoading(true);
|
|
|
|
try {
|
|
const result = await accountService.login(username, password);
|
|
|
|
if (!result?.user) {
|
|
setIsInvalid(true);
|
|
useToastStore.getState().showToast('Invalid username or password.', 'error');
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
// Org entry is driven by where the user actually has work: supervisors
|
|
// get the full list, everyone else only orgs with records assigned to
|
|
// (or audited by) them. One org -> straight in; several -> pick;
|
|
// none -> land in their profile org with an empty feed.
|
|
let orgs: Organization[] = [];
|
|
try {
|
|
orgs = await accountService.getMyOrganizations(result.access_token);
|
|
} catch {
|
|
orgs = [];
|
|
}
|
|
|
|
if (orgs.length === 1) {
|
|
commitLogin(result, orgs[0]);
|
|
} else if (orgs.length === 0) {
|
|
useToastStore.getState().showToast('No work assigned to you yet.', 'info');
|
|
commitLogin(result, { org_id: result.user.org_id, db_name: result.user.db_name });
|
|
} else {
|
|
setTempLoginResult(result);
|
|
setDialogOrgs(orgs);
|
|
setDialogOpen(true);
|
|
}
|
|
} catch (error) {
|
|
setIsInvalid(true);
|
|
useToastStore.getState().showToast('Invalid username or password.', 'error');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleDbSelection = (selectedOrg?: Organization) => {
|
|
setDialogOpen(false);
|
|
if (selectedOrg && tempLoginResult) {
|
|
commitLogin(tempLoginResult, selectedOrg);
|
|
}
|
|
setTempLoginResult(null);
|
|
};
|
|
|
|
return (
|
|
<Box sx={{
|
|
width: '100vw',
|
|
height: '100vh',
|
|
display: 'flex',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
background: 'radial-gradient(circle at center, #0f172a 0%, #0b1121 100%)',
|
|
overflow: 'hidden'
|
|
}}>
|
|
<Container component="main" maxWidth="xs">
|
|
<Paper sx={{
|
|
p: 4,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
bgcolor: 'rgba(15, 23, 42, 0.65)',
|
|
backdropFilter: 'blur(12px)',
|
|
border: '1px solid rgba(255, 255, 255, 0.08)',
|
|
borderRadius: 4,
|
|
boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.5)'
|
|
}}>
|
|
{/* Logo */}
|
|
<Box sx={{ mb: 4, display: 'flex', justifyContent: 'center' }}>
|
|
<img
|
|
src="/assets/images/logo/side-bar-logo.png"
|
|
alt="Logo"
|
|
width="220"
|
|
onError={(e) => {
|
|
e.currentTarget.src = "/images/SeekRightLogo.png";
|
|
}}
|
|
/>
|
|
</Box>
|
|
|
|
<Box component="form" sx={{ width: '100%' }}>
|
|
<TextField
|
|
margin="normal"
|
|
required
|
|
fullWidth
|
|
id="username"
|
|
placeholder="Username"
|
|
name="username"
|
|
autoFocus
|
|
value={username}
|
|
onChange={(e) => setUsername(e.target.value)}
|
|
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
|
|
slotProps={{
|
|
input: {
|
|
sx: {
|
|
borderRadius: '50px',
|
|
color: '#f8fafc',
|
|
bgcolor: 'rgba(255, 255, 255, 0.02)',
|
|
'& fieldset': { borderColor: 'rgba(255, 255, 255, 0.12)' },
|
|
'&:hover fieldset': { borderColor: 'rgba(255, 255, 255, 0.25)' },
|
|
'&.Mui-focused fieldset': { borderColor: '#3b82f6' }
|
|
}
|
|
}
|
|
}}
|
|
sx={{
|
|
mb: 1.5,
|
|
'& .MuiInputBase-input': { padding: '12px 18px', fontSize: '0.9rem' }
|
|
}}
|
|
/>
|
|
<TextField
|
|
margin="normal"
|
|
required
|
|
fullWidth
|
|
name="password"
|
|
placeholder="Password"
|
|
type="password"
|
|
id="password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
|
|
slotProps={{
|
|
input: {
|
|
sx: {
|
|
borderRadius: '50px',
|
|
color: '#f8fafc',
|
|
bgcolor: 'rgba(255, 255, 255, 0.02)',
|
|
'& fieldset': { borderColor: 'rgba(255, 255, 255, 0.12)' },
|
|
'&:hover fieldset': { borderColor: 'rgba(255, 255, 255, 0.25)' },
|
|
'&.Mui-focused fieldset': { borderColor: '#3b82f6' }
|
|
}
|
|
}
|
|
}}
|
|
sx={{
|
|
mt: 1.5,
|
|
mb: 3,
|
|
'& .MuiInputBase-input': { padding: '12px 18px', fontSize: '0.9rem' }
|
|
}}
|
|
/>
|
|
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 1, mb: 1 }}>
|
|
<Button
|
|
variant="contained"
|
|
disabled={!username || !password || loading}
|
|
onClick={handleLogin}
|
|
sx={{
|
|
borderRadius: '50px',
|
|
backgroundColor: '#3b82f6',
|
|
color: '#fff',
|
|
fontWeight: 'bold',
|
|
width: '100%',
|
|
py: 1.5,
|
|
boxShadow: 'none',
|
|
textTransform: 'none',
|
|
fontSize: '1rem',
|
|
'&:hover': {
|
|
backgroundColor: '#2563eb',
|
|
boxShadow: 'none',
|
|
},
|
|
'&.Mui-disabled': {
|
|
backgroundColor: 'rgba(59, 130, 246, 0.3)',
|
|
color: 'rgba(255, 255, 255, 0.3)'
|
|
}
|
|
}}
|
|
>
|
|
{loading ? 'Logging in...' : 'LOGIN'}
|
|
</Button>
|
|
</Box>
|
|
|
|
{isInvalid && (
|
|
<Alert severity="error" sx={{ mt: 3, borderRadius: 2 }}>
|
|
Invalid Credentials
|
|
</Alert>
|
|
)}
|
|
</Box>
|
|
</Paper>
|
|
</Container>
|
|
|
|
<DbSelectionDialog open={dialogOpen} organizations={dialogOrgs} onClose={handleDbSelection} />
|
|
</Box>
|
|
);
|
|
};
|