Files
Auditor-Portal-Revamp/src/pages/account/Login.tsx

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>
);
};