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