upd: menerapkan log pada semua aksi
This commit is contained in:
36
src/app.ts
36
src/app.ts
@@ -3,6 +3,7 @@ import { html } from '@elysiajs/html'
|
|||||||
import { Elysia } from 'elysia'
|
import { Elysia } from 'elysia'
|
||||||
import { prisma } from './lib/db'
|
import { prisma } from './lib/db'
|
||||||
import { env } from './lib/env'
|
import { env } from './lib/env'
|
||||||
|
import { createSystemLog } from './lib/logger'
|
||||||
|
|
||||||
export function createApp() {
|
export function createApp() {
|
||||||
return new Elysia()
|
return new Elysia()
|
||||||
@@ -43,13 +44,20 @@ export function createApp() {
|
|||||||
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000) // 24 hours
|
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000) // 24 hours
|
||||||
await prisma.session.create({ data: { token, userId: user.id, expiresAt } })
|
await prisma.session.create({ data: { token, userId: user.id, expiresAt } })
|
||||||
set.headers['set-cookie'] = `session=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400`
|
set.headers['set-cookie'] = `session=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400`
|
||||||
|
await createSystemLog(user.id, 'LOGIN', 'Logged in successfully')
|
||||||
return { user: { id: user.id, name: user.name, email: user.email, role: user.role } }
|
return { user: { id: user.id, name: user.name, email: user.email, role: user.role } }
|
||||||
})
|
})
|
||||||
|
|
||||||
.post('/api/auth/logout', async ({ request, set }) => {
|
.post('/api/auth/logout', async ({ request, set }) => {
|
||||||
const cookie = request.headers.get('cookie') ?? ''
|
const cookie = request.headers.get('cookie') ?? ''
|
||||||
const token = cookie.match(/session=([^;]+)/)?.[1]
|
const token = cookie.match(/session=([^;]+)/)?.[1]
|
||||||
if (token) await prisma.session.deleteMany({ where: { token } })
|
if (token) {
|
||||||
|
const sessionObj = await prisma.session.findUnique({ where: { token } })
|
||||||
|
if (sessionObj) {
|
||||||
|
await createSystemLog(sessionObj.userId, 'LOGOUT', 'Logged out successfully')
|
||||||
|
await prisma.session.deleteMany({ where: { token } })
|
||||||
|
}
|
||||||
|
}
|
||||||
set.headers['set-cookie'] = 'session=; Path=/; HttpOnly; Max-Age=0'
|
set.headers['set-cookie'] = 'session=; Path=/; HttpOnly; Max-Age=0'
|
||||||
return { ok: true }
|
return { ok: true }
|
||||||
})
|
})
|
||||||
@@ -139,6 +147,8 @@ export function createApp() {
|
|||||||
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000)
|
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000)
|
||||||
await prisma.session.create({ data: { token, userId: user.id, expiresAt } })
|
await prisma.session.create({ data: { token, userId: user.id, expiresAt } })
|
||||||
|
|
||||||
|
await createSystemLog(user.id, 'LOGIN', 'Logged in via Google')
|
||||||
|
|
||||||
set.headers['set-cookie'] = `session=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400`
|
set.headers['set-cookie'] = `session=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400`
|
||||||
set.status = 302; set.headers['location'] = user.role === 'SUPER_ADMIN' ? '/dashboard' : '/profile'
|
set.status = 302; set.headers['location'] = user.role === 'SUPER_ADMIN' ? '/dashboard' : '/profile'
|
||||||
})
|
})
|
||||||
@@ -203,6 +213,25 @@ export function createApp() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
.post('/api/logs', async ({ request, set }) => {
|
||||||
|
const cookie = request.headers.get('cookie') ?? ''
|
||||||
|
const token = cookie.match(/session=([^;]+)/)?.[1]
|
||||||
|
let userId: string | undefined
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
const session = await prisma.session.findUnique({ where: { token } })
|
||||||
|
if (session && session.expiresAt > new Date()) {
|
||||||
|
userId = session.userId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = (await request.json()) as { type: string, message: string }
|
||||||
|
const actingUserId = userId || (await prisma.user.findFirst({ where: { role: 'SUPER_ADMIN' } }))?.id || ''
|
||||||
|
|
||||||
|
await createSystemLog(actingUserId, body.type as any, body.message)
|
||||||
|
return { ok: true }
|
||||||
|
})
|
||||||
|
|
||||||
.get('/api/operators', async ({ query }) => {
|
.get('/api/operators', async ({ query }) => {
|
||||||
const page = Number(query.page) || 1
|
const page = Number(query.page) || 1
|
||||||
const limit = Number(query.limit) || 20
|
const limit = Number(query.limit) || 20
|
||||||
@@ -321,6 +350,9 @@ export function createApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const body = (await request.json()) as any
|
const body = (await request.json()) as any
|
||||||
|
const defaultAdmin = await prisma.user.findFirst({ where: { role: 'SUPER_ADMIN' } })
|
||||||
|
const actingUserId = userId || defaultAdmin?.id || ''
|
||||||
|
|
||||||
const bug = await prisma.bug.create({
|
const bug = await prisma.bug.create({
|
||||||
data: {
|
data: {
|
||||||
app: body.app,
|
app: body.app,
|
||||||
@@ -339,7 +371,7 @@ export function createApp() {
|
|||||||
} : undefined,
|
} : undefined,
|
||||||
logs: {
|
logs: {
|
||||||
create: {
|
create: {
|
||||||
userId: userId || (await prisma.user.findFirst({ where: { role: 'SUPER_ADMIN' } }))?.id || '',
|
userId: actingUserId,
|
||||||
status: body.status || 'OPEN',
|
status: body.status || 'OPEN',
|
||||||
description: 'Bug reported initially.',
|
description: 'Bug reported initially.',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,4 +34,5 @@ export const API_URLS = {
|
|||||||
getBugs: (page: number, search: string, app: string, status: string) =>
|
getBugs: (page: number, search: string, app: string, status: string) =>
|
||||||
`/api/bugs?page=${page}&search=${encodeURIComponent(search)}&app=${app}&status=${status}`,
|
`/api/bugs?page=${page}&search=${encodeURIComponent(search)}&app=${app}&status=${status}`,
|
||||||
createBug: () => `/api/bugs`,
|
createBug: () => `/api/bugs`,
|
||||||
|
createLog: () => `/api/logs`,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,30 @@
|
|||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { notifications } from '@mantine/notifications'
|
|
||||||
import { VillageActivityLineChart, VillageComparisonBarChart } from '@/frontend/components/DashboardCharts'
|
import { VillageActivityLineChart, VillageComparisonBarChart } from '@/frontend/components/DashboardCharts'
|
||||||
import { ErrorDataTable } from '@/frontend/components/ErrorDataTable'
|
import { ErrorDataTable } from '@/frontend/components/ErrorDataTable'
|
||||||
import { SummaryCard } from '@/frontend/components/SummaryCard'
|
import { SummaryCard } from '@/frontend/components/SummaryCard'
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
Badge,
|
||||||
|
Button,
|
||||||
Group,
|
Group,
|
||||||
|
Modal,
|
||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
|
||||||
Title,
|
|
||||||
Modal,
|
|
||||||
Button,
|
|
||||||
TextInput,
|
|
||||||
Switch,
|
Switch,
|
||||||
Badge,
|
Text,
|
||||||
Textarea,
|
Textarea,
|
||||||
Skeleton
|
TextInput,
|
||||||
|
Title
|
||||||
} from '@mantine/core'
|
} from '@mantine/core'
|
||||||
import { useDisclosure } from '@mantine/hooks'
|
import { useDisclosure } from '@mantine/hooks'
|
||||||
import { createFileRoute, useParams, useNavigate } from '@tanstack/react-router'
|
import { notifications } from '@mantine/notifications'
|
||||||
import useSWR from 'swr'
|
import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
TbActivity,
|
TbActivity,
|
||||||
TbAlertTriangle,
|
TbAlertTriangle,
|
||||||
TbBuildingCommunity,
|
TbBuildingCommunity,
|
||||||
TbRefresh,
|
|
||||||
TbVersions
|
TbVersions
|
||||||
} from 'react-icons/tb'
|
} from 'react-icons/tb'
|
||||||
|
import useSWR from 'swr'
|
||||||
import { API_URLS } from '../config/api'
|
import { API_URLS } from '../config/api'
|
||||||
|
|
||||||
export const Route = createFileRoute('/apps/$appId/')({
|
export const Route = createFileRoute('/apps/$appId/')({
|
||||||
@@ -89,6 +86,12 @@ function AppOverviewPage() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
|
await fetch(API_URLS.createLog(), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ type: 'UPDATE', message: `Update version information: ${JSON.stringify({ latestVersion, minVersion, maintenance, messageUpdate })}` })
|
||||||
|
}).catch(console.error)
|
||||||
|
|
||||||
notifications.show({
|
notifications.show({
|
||||||
title: 'Update Successful',
|
title: 'Update Successful',
|
||||||
message: 'Application version information has been updated.',
|
message: 'Application version information has been updated.',
|
||||||
@@ -118,29 +121,29 @@ function AppOverviewPage() {
|
|||||||
<>
|
<>
|
||||||
<Modal opened={versionModalOpened} onClose={closeVersionModal} title="Update Version Information" radius="md">
|
<Modal opened={versionModalOpened} onClose={closeVersionModal} title="Update Version Information" radius="md">
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Active Version"
|
label="Active Version"
|
||||||
placeholder="e.g. 2.0.5"
|
placeholder="e.g. 2.0.5"
|
||||||
value={latestVersion}
|
value={latestVersion}
|
||||||
onChange={(e) => setLatestVersion(e.currentTarget.value)}
|
onChange={(e) => setLatestVersion(e.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Minimum Version"
|
label="Minimum Version"
|
||||||
placeholder="e.g. 2.0.0"
|
placeholder="e.g. 2.0.0"
|
||||||
value={minVersion}
|
value={minVersion}
|
||||||
onChange={(e) => setMinVersion(e.currentTarget.value)}
|
onChange={(e) => setMinVersion(e.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
<Textarea
|
<Textarea
|
||||||
label="Update Message"
|
label="Update Message"
|
||||||
placeholder="Enter release notes or update message..."
|
placeholder="Enter release notes or update message..."
|
||||||
value={messageUpdate}
|
value={messageUpdate}
|
||||||
onChange={(e) => setMessageUpdate(e.currentTarget.value)}
|
onChange={(e) => setMessageUpdate(e.currentTarget.value)}
|
||||||
minRows={3}
|
minRows={3}
|
||||||
autosize
|
autosize
|
||||||
/>
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
label="Maintenance Mode"
|
label="Maintenance Mode"
|
||||||
description="Enable to put the app in maintenance mode for users."
|
description="Enable to put the app in maintenance mode for users."
|
||||||
checked={maintenance}
|
checked={maintenance}
|
||||||
onChange={(e) => setMaintenance(e.currentTarget.checked)}
|
onChange={(e) => setMaintenance(e.currentTarget.checked)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -169,6 +169,12 @@ function UsersIndexPage() {
|
|||||||
const result = await res.json()
|
const result = await res.json()
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
await fetch(API_URLS.createLog(), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ type: 'CREATE', message: `Didaftarkan user (${appId}) baru: ${form.name}-${form.nik}` })
|
||||||
|
}).catch(console.error)
|
||||||
|
|
||||||
notifications.show({
|
notifications.show({
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
message: 'User has been created successfully.',
|
message: 'User has been created successfully.',
|
||||||
@@ -252,6 +258,12 @@ function UsersIndexPage() {
|
|||||||
const result = await res.json()
|
const result = await res.json()
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
await fetch(API_URLS.createLog(), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ type: 'UPDATE', message: `Data user (${appId}) diperbarui: ${editForm.name}-${editForm.id}` })
|
||||||
|
}).catch(console.error)
|
||||||
|
|
||||||
notifications.show({
|
notifications.show({
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
message: 'User has been updated successfully.',
|
message: 'User has been updated successfully.',
|
||||||
|
|||||||
@@ -196,6 +196,12 @@ function VillageDetailPage() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
await fetch(API_URLS.createLog(), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ type: 'UPDATE', message: `Data desa (${appId}) diperbarui: ${editForm.name}-${village.id}` })
|
||||||
|
}).catch(console.error)
|
||||||
|
|
||||||
notifications.show({
|
notifications.show({
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
message: 'Village data has been updated successfully.',
|
message: 'Village data has been updated successfully.',
|
||||||
@@ -238,6 +244,12 @@ function VillageDetailPage() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
await fetch(API_URLS.createLog(), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ type: 'UPDATE', message: `Status desa (${appId}) diperbarui (${!village.isActive ? 'activated' : 'deactivated'}): ${village.name}-${village.id}` })
|
||||||
|
}).catch(console.error)
|
||||||
|
|
||||||
notifications.show({
|
notifications.show({
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
message: `Village status has been ${!village.isActive ? 'activated' : 'deactivated'}.`,
|
message: `Village status has been ${!village.isActive ? 'activated' : 'deactivated'}.`,
|
||||||
@@ -477,9 +489,9 @@ function VillageDetailPage() {
|
|||||||
<Button variant="light" color="gray" onClick={closeConfirmModal} radius="md">
|
<Button variant="light" color="gray" onClick={closeConfirmModal} radius="md">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color={village.isActive ? 'red' : 'green'}
|
color={village.isActive ? 'red' : 'green'}
|
||||||
onClick={handleConfirmToggle}
|
onClick={handleConfirmToggle}
|
||||||
loading={isUpdating}
|
loading={isUpdating}
|
||||||
radius="md"
|
radius="md"
|
||||||
>
|
>
|
||||||
@@ -488,7 +500,7 @@ function VillageDetailPage() {
|
|||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* ── Edit Village Modal ── */}
|
{/* ── Edit Village Modal ── */}
|
||||||
<Modal
|
<Modal
|
||||||
opened={editModalOpened}
|
opened={editModalOpened}
|
||||||
|
|||||||
@@ -283,6 +283,12 @@ function AppVillagesIndexPage() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
await fetch(API_URLS.createLog(), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ type: 'CREATE', message: `Desa baru didaftarkan: ${form.name}` })
|
||||||
|
}).catch(console.error)
|
||||||
|
|
||||||
notifications.show({
|
notifications.show({
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
message: 'Village has been successfully registered.',
|
message: 'Village has been successfully registered.',
|
||||||
|
|||||||
@@ -92,6 +92,12 @@ function ListErrorsPage() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
await fetch(API_URLS.createLog(), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ type: 'CREATE', message: `Report bug baru ditambahkan: ${createForm.description.substring(0, 50)}${createForm.description.length > 50 ? '...' : ''}` })
|
||||||
|
}).catch(console.error)
|
||||||
|
|
||||||
notifications.show({
|
notifications.show({
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
message: 'Bug report has been created.',
|
message: 'Bug report has been created.',
|
||||||
|
|||||||
18
src/lib/logger.ts
Normal file
18
src/lib/logger.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { prisma } from './db'
|
||||||
|
import { LogType } from '../../generated/prisma'
|
||||||
|
|
||||||
|
export async function createSystemLog(userId: string, type: LogType, message: string) {
|
||||||
|
try {
|
||||||
|
return await prisma.log.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
type,
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Logger Error]', error)
|
||||||
|
// Don't throw, we don't want logging errors to break the main application flow
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user