Files
monitoring-app/src/frontend/routes/login.tsx
amaliadwiy 06794524fd feat: block inactive users from login and fix activity log on dev operators
- Block inactive users on email/password login (403)
- Block inactive users on Google OAuth (redirect to account_disabled)
- Auto-logout inactive users on session check (deleteMany sessions)
- Delete sessions when user is deactivated via PATCH /api/operators/:id
- Add account_disabled error message on login page
- Show inactive indicator on users table with reactivate button
- Add createSystemLog calls to /api/admin/users role and activate endpoints

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 11:58:31 +08:00

118 lines
3.6 KiB
TypeScript

import { useLogin } from '@/frontend/hooks/useAuth'
import {
Alert,
Button,
Center,
Divider,
Paper,
PasswordInput,
Stack,
Text,
TextInput,
Title,
} from '@mantine/core'
import { createFileRoute, redirect } from '@tanstack/react-router'
import { useState } from 'react'
import { FcGoogle } from 'react-icons/fc'
import { TbAlertCircle, TbLock, TbLogin, TbMail } from 'react-icons/tb'
export const Route = createFileRoute('/login')({
validateSearch: (search: Record<string, unknown>): { error?: string } => ({
error: (search.error as string) || undefined,
}),
beforeLoad: async ({ context }) => {
try {
const data = await context.queryClient.ensureQueryData({
queryKey: ['auth', 'session'],
queryFn: () => fetch('/api/auth/session', { credentials: 'include' }).then((r) => r.json()),
})
if (data?.user) {
const dest = data.user.role === 'DEVELOPER' ? '/dev' : data.user.role === 'USER' ? '/profile' : '/dashboard'
throw redirect({ to: dest })
}
} catch (e) {
if (e instanceof Error) return
throw e
}
},
component: LoginPage,
})
function LoginPage() {
const login = useLogin()
const { error: searchError } = Route.useSearch()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
login.mutate({ email, password })
}
return (
<Center mih="100vh">
<Paper shadow="md" p="xl" radius="md" w={400} withBorder>
<form onSubmit={handleSubmit}>
<Stack gap="md">
<Title order={2} ta="center">
Login
</Title>
{(login.isError || searchError) && (
<Alert icon={<TbAlertCircle size={16} />} color="red" variant="light">
{login.isError ? login.error.message : (
{
google_denied: 'Login dengan Google dibatalkan.',
invalid_state: 'Sesi OAuth tidak valid, silakan coba lagi.',
token_failed: 'Gagal menukar token Google, silakan coba lagi.',
userinfo_failed: 'Gagal mengambil info akun Google, silakan coba lagi.',
account_disabled: 'Akun Anda telah dinonaktifkan. Hubungi admin untuk informasi lebih lanjut.',
}[searchError ?? ''] ?? 'Login dengan Google gagal, silakan coba lagi.'
)}
</Alert>
)}
<TextInput
label="Email"
placeholder="email@example.com"
leftSection={<TbMail size={16} />}
value={email}
onChange={(e) => setEmail(e.currentTarget.value)}
required
/>
<PasswordInput
label="Password"
placeholder="Password"
leftSection={<TbLock size={16} />}
value={password}
onChange={(e) => setPassword(e.currentTarget.value)}
required
/>
<Button
type="submit"
fullWidth
leftSection={<TbLogin size={18} />}
loading={login.isPending}
>
Sign in
</Button>
<Divider label="or" labelPosition="center" />
<Button
variant="default"
fullWidth
leftSection={<FcGoogle size={18} />}
onClick={() => { window.location.href = '/api/auth/google' }}
>
Continue with Google
</Button>
</Stack>
</form>
</Paper>
</Center>
)
}