diff --git a/src/app.ts b/src/app.ts index 64982eb..5c70a0c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -203,6 +203,10 @@ export function createApp() { }) } + if (!user.active) { + return new Response(null, { status: 302, headers: { Location: '/login?error=account_disabled' } }) + } + if (env.SUPER_ADMIN_EMAILS.includes(user.email) && user.role !== 'DEVELOPER') { user = await prisma.user.update({ where: { id: user.id }, data: { role: 'DEVELOPER' } }) } @@ -233,6 +237,10 @@ export function createApp() { set.status = 401 return { error: 'Email atau password salah' } } + if (!user.active) { + set.status = 403 + return { error: 'Akun Anda telah dinonaktifkan. Hubungi admin untuk informasi lebih lanjut.' } + } // Auto-promote super admin from env if (env.SUPER_ADMIN_EMAILS.includes(user.email) && user.role !== 'DEVELOPER') { user = await prisma.user.update({ where: { id: user.id }, data: { role: 'DEVELOPER' } }) @@ -281,13 +289,18 @@ export function createApp() { if (!token) { set.status = 401; return { user: null } } const session = await prisma.session.findUnique({ where: { token }, - include: { user: { select: { id: true, name: true, email: true, role: true, image: true } } }, + include: { user: { select: { id: true, name: true, email: true, role: true, image: true, active: true } } }, }) if (!session || session.expiresAt < new Date()) { if (session) await prisma.session.delete({ where: { id: session.id } }) set.status = 401 return { user: null } } + if (!session.user.active) { + await prisma.session.deleteMany({ where: { userId: session.user.id } }) + set.status = 401 + return { user: null } + } return { user: session.user } }, { detail: { @@ -641,6 +654,10 @@ export function createApp() { }, }) + if (body.active === false) { + await prisma.session.deleteMany({ where: { userId: id } }) + } + if (userId) { await createSystemLog(userId, 'UPDATE', `Updated user: ${user.name} (${user.email})`) } @@ -1054,6 +1071,7 @@ export function createApp() { select: { id: true, name: true, email: true, role: true, active: true, createdAt: true }, }) await appLog('info', `Role changed: ${user.email} ${target?.role} → ${role}`) + await createSystemLog(auth.userId, 'UPDATE', `Role changed: ${user.name} (${user.email}) ${target?.role} → ${role}`) return { user } }) @@ -1069,6 +1087,7 @@ export function createApp() { }) if (!active) await prisma.session.deleteMany({ where: { userId: params.id } }) await appLog('info', `User ${active ? 'activated' : 'deactivated'}: ${user.email}`) + await createSystemLog(auth.userId, active ? 'UPDATE' : 'DELETE', `User ${active ? 'activated' : 'deactivated'}: ${user.name} (${user.email})`) return { user } }) diff --git a/src/frontend/routes/login.tsx b/src/frontend/routes/login.tsx index 7432d4f..ff41ee9 100644 --- a/src/frontend/routes/login.tsx +++ b/src/frontend/routes/login.tsx @@ -66,6 +66,7 @@ function LoginPage() { 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.' )} diff --git a/src/frontend/routes/users.tsx b/src/frontend/routes/users.tsx index 0de38a5..a87e060 100644 --- a/src/frontend/routes/users.tsx +++ b/src/frontend/routes/users.tsx @@ -4,6 +4,7 @@ import { ActionIcon, Avatar, Badge, + Box, Button, Card, Container, @@ -23,6 +24,7 @@ import { TextInput, ThemeIcon, Title, + Tooltip, } from '@mantine/core' import { useDisclosure } from '@mantine/hooks' import { notifications } from '@mantine/notifications' @@ -37,7 +39,8 @@ import { TbSearch, TbShieldCheck, TbTrash, - TbUserCheck + TbUserCheck, + TbUserPlus, } from 'react-icons/tb' import useSWR from 'swr' import { API_URLS } from '../config/api' @@ -229,6 +232,28 @@ function UsersPage() { } } + // ── Activate User ── + const handleActivateUser = async (user: any) => { + try { + const res = await fetch(`/api/operators/${user.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ active: true }), + }) + if (res.ok) { + notifications.show({ title: 'Success', message: `${user.name} telah diaktifkan kembali.`, color: 'teal', icon: }) + mutateOperators() + mutateStats() + } else { + const err = await res.json() + throw new Error(err.error || 'Failed to activate user') + } + } catch (e: any) { + notifications.show({ title: 'Error', message: e.message || 'Something went wrong.', color: 'red', icon: }) + } + } + return ( @@ -306,33 +331,62 @@ function UsersPage() { ) : ( operators.map((user: any) => ( - + - - {user.name.charAt(0)} - + + + {user.name.charAt(0)} + + {user.active === false && ( + + )} + - {user.name} + + {user.name} + {user.active === false && ( + Inactive + )} + {user.email} - - + + {user.role} - - {new Date(user.createdAt).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' })} + + + {new Date(user.createdAt).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' })} + - handleOpenEdit(user)}> - - - handleOpenDelete(user)}> - - + {user.active === false ? ( + + handleActivateUser(user)}> + + + + ) : ( + <> + handleOpenEdit(user)}> + + + handleOpenDelete(user)}> + + + + )}