amalia/13-apr-26 #7
36
src/app.ts
36
src/app.ts
@@ -3,6 +3,7 @@ import { html } from '@elysiajs/html'
|
||||
import { Elysia } from 'elysia'
|
||||
import { prisma } from './lib/db'
|
||||
import { env } from './lib/env'
|
||||
import { createSystemLog } from './lib/logger'
|
||||
|
||||
export function createApp() {
|
||||
return new Elysia()
|
||||
@@ -43,13 +44,20 @@ export function createApp() {
|
||||
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000) // 24 hours
|
||||
await prisma.session.create({ data: { token, userId: user.id, expiresAt } })
|
||||
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 } }
|
||||
})
|
||||
|
||||
.post('/api/auth/logout', async ({ request, set }) => {
|
||||
const cookie = request.headers.get('cookie') ?? ''
|
||||
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'
|
||||
return { ok: true }
|
||||
})
|
||||
@@ -139,6 +147,8 @@ export function createApp() {
|
||||
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000)
|
||||
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.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 }) => {
|
||||
const page = Number(query.page) || 1
|
||||
const limit = Number(query.limit) || 20
|
||||
@@ -321,6 +350,9 @@ export function createApp() {
|
||||
}
|
||||
|
||||
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({
|
||||
data: {
|
||||
app: body.app,
|
||||
@@ -339,7 +371,7 @@ export function createApp() {
|
||||
} : undefined,
|
||||
logs: {
|
||||
create: {
|
||||
userId: userId || (await prisma.user.findFirst({ where: { role: 'SUPER_ADMIN' } }))?.id || '',
|
||||
userId: actingUserId,
|
||||
status: body.status || 'OPEN',
|
||||
description: 'Bug reported initially.',
|
||||
},
|
||||
|
||||
@@ -34,4 +34,5 @@ export const API_URLS = {
|
||||
getBugs: (page: number, search: string, app: string, status: string) =>
|
||||
`/api/bugs?page=${page}&search=${encodeURIComponent(search)}&app=${app}&status=${status}`,
|
||||
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 { ErrorDataTable } from '@/frontend/components/ErrorDataTable'
|
||||
import { SummaryCard } from '@/frontend/components/SummaryCard'
|
||||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
Button,
|
||||
Group,
|
||||
Modal,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
Modal,
|
||||
Button,
|
||||
TextInput,
|
||||
Switch,
|
||||
Badge,
|
||||
Text,
|
||||
Textarea,
|
||||
Skeleton
|
||||
TextInput,
|
||||
Title
|
||||
} from '@mantine/core'
|
||||
import { useDisclosure } from '@mantine/hooks'
|
||||
import { createFileRoute, useParams, useNavigate } from '@tanstack/react-router'
|
||||
import useSWR from 'swr'
|
||||
import { notifications } from '@mantine/notifications'
|
||||
import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router'
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
TbActivity,
|
||||
TbAlertTriangle,
|
||||
TbBuildingCommunity,
|
||||
TbRefresh,
|
||||
TbVersions
|
||||
} from 'react-icons/tb'
|
||||
import useSWR from 'swr'
|
||||
import { API_URLS } from '../config/api'
|
||||
|
||||
export const Route = createFileRoute('/apps/$appId/')({
|
||||
@@ -89,6 +86,12 @@ function AppOverviewPage() {
|
||||
})
|
||||
|
||||
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({
|
||||
title: 'Update Successful',
|
||||
message: 'Application version information has been updated.',
|
||||
@@ -118,29 +121,29 @@ function AppOverviewPage() {
|
||||
<>
|
||||
<Modal opened={versionModalOpened} onClose={closeVersionModal} title="Update Version Information" radius="md">
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Active Version"
|
||||
<TextInput
|
||||
label="Active Version"
|
||||
placeholder="e.g. 2.0.5"
|
||||
value={latestVersion}
|
||||
value={latestVersion}
|
||||
onChange={(e) => setLatestVersion(e.currentTarget.value)}
|
||||
/>
|
||||
<TextInput
|
||||
label="Minimum Version"
|
||||
<TextInput
|
||||
label="Minimum Version"
|
||||
placeholder="e.g. 2.0.0"
|
||||
value={minVersion}
|
||||
value={minVersion}
|
||||
onChange={(e) => setMinVersion(e.currentTarget.value)}
|
||||
/>
|
||||
<Textarea
|
||||
label="Update Message"
|
||||
placeholder="Enter release notes or update message..."
|
||||
<Textarea
|
||||
label="Update Message"
|
||||
placeholder="Enter release notes or update message..."
|
||||
value={messageUpdate}
|
||||
onChange={(e) => setMessageUpdate(e.currentTarget.value)}
|
||||
minRows={3}
|
||||
autosize
|
||||
/>
|
||||
<Switch
|
||||
label="Maintenance Mode"
|
||||
description="Enable to put the app in maintenance mode for users."
|
||||
<Switch
|
||||
label="Maintenance Mode"
|
||||
description="Enable to put the app in maintenance mode for users."
|
||||
checked={maintenance}
|
||||
onChange={(e) => setMaintenance(e.currentTarget.checked)}
|
||||
/>
|
||||
|
||||
@@ -169,6 +169,12 @@ function UsersIndexPage() {
|
||||
const result = await res.json()
|
||||
|
||||
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({
|
||||
title: 'Success',
|
||||
message: 'User has been created successfully.',
|
||||
@@ -252,6 +258,12 @@ function UsersIndexPage() {
|
||||
const result = await res.json()
|
||||
|
||||
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({
|
||||
title: 'Success',
|
||||
message: 'User has been updated successfully.',
|
||||
|
||||
@@ -196,6 +196,12 @@ function VillageDetailPage() {
|
||||
})
|
||||
|
||||
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({
|
||||
title: 'Success',
|
||||
message: 'Village data has been updated successfully.',
|
||||
@@ -238,6 +244,12 @@ function VillageDetailPage() {
|
||||
})
|
||||
|
||||
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({
|
||||
title: 'Success',
|
||||
message: `Village status has been ${!village.isActive ? 'activated' : 'deactivated'}.`,
|
||||
@@ -477,9 +489,9 @@ function VillageDetailPage() {
|
||||
<Button variant="light" color="gray" onClick={closeConfirmModal} radius="md">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color={village.isActive ? 'red' : 'green'}
|
||||
onClick={handleConfirmToggle}
|
||||
<Button
|
||||
color={village.isActive ? 'red' : 'green'}
|
||||
onClick={handleConfirmToggle}
|
||||
loading={isUpdating}
|
||||
radius="md"
|
||||
>
|
||||
@@ -488,7 +500,7 @@ function VillageDetailPage() {
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
|
||||
{/* ── Edit Village Modal ── */}
|
||||
<Modal
|
||||
opened={editModalOpened}
|
||||
|
||||
@@ -283,6 +283,12 @@ function AppVillagesIndexPage() {
|
||||
})
|
||||
|
||||
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({
|
||||
title: 'Success',
|
||||
message: 'Village has been successfully registered.',
|
||||
|
||||
@@ -92,6 +92,12 @@ function ListErrorsPage() {
|
||||
})
|
||||
|
||||
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({
|
||||
title: 'Success',
|
||||
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