567 lines
18 KiB
TypeScript
567 lines
18 KiB
TypeScript
import { cors } from '@elysiajs/cors'
|
|
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()
|
|
.use(cors())
|
|
.use(html())
|
|
|
|
// ─── Global Error Handler ────────────────────────
|
|
.onError(({ code, error }) => {
|
|
if (code === 'NOT_FOUND') {
|
|
return new Response(JSON.stringify({ error: 'Not Found', status: 404 }), {
|
|
status: 404,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
})
|
|
}
|
|
console.error('[Server Error]', error)
|
|
return new Response(JSON.stringify({ error: 'Internal Server Error', status: 500 }), {
|
|
status: 500,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
})
|
|
})
|
|
|
|
// API routes
|
|
.get('/health', () => ({ status: 'ok' }))
|
|
|
|
// ─── Auth API ──────────────────────────────────────
|
|
.post('/api/auth/login', async ({ request, set }) => {
|
|
const { email, password } = (await request.json()) as { email: string; password: string }
|
|
let user = await prisma.user.findUnique({ where: { email } })
|
|
if (!user || !(await Bun.password.verify(password, user.password))) {
|
|
set.status = 401
|
|
return { error: 'Email atau password salah' }
|
|
}
|
|
// 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' } })
|
|
}
|
|
const token = crypto.randomUUID()
|
|
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) {
|
|
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 }
|
|
})
|
|
|
|
.get('/api/auth/session', async ({ request, set }) => {
|
|
const cookie = request.headers.get('cookie') ?? ''
|
|
const token = cookie.match(/session=([^;]+)/)?.[1]
|
|
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 } } },
|
|
})
|
|
if (!session || session.expiresAt < new Date()) {
|
|
if (session) await prisma.session.delete({ where: { id: session.id } })
|
|
set.status = 401
|
|
return { user: null }
|
|
}
|
|
return { user: session.user }
|
|
})
|
|
|
|
|
|
|
|
// ─── Monitoring API ────────────────────────────────
|
|
.get('/api/dashboard/stats', async () => {
|
|
const newErrors = await prisma.bug.count({ where: { status: 'OPEN' } })
|
|
const users = await prisma.user.count()
|
|
return {
|
|
totalApps: 1,
|
|
newErrors: newErrors,
|
|
activeUsers: users,
|
|
trends: { totalApps: 0, newErrors: 12, activeUsers: 5.2 }
|
|
}
|
|
})
|
|
|
|
.get('/api/dashboard/recent-errors', async () => {
|
|
const bugs = await prisma.bug.findMany({
|
|
take: 5,
|
|
orderBy: { createdAt: 'desc' }
|
|
})
|
|
return bugs.map(b => ({
|
|
id: b.id,
|
|
app: b.appId,
|
|
message: b.description,
|
|
version: b.affectedVersion,
|
|
time: b.createdAt.toISOString(),
|
|
severity: b.status
|
|
}))
|
|
})
|
|
|
|
.get('/api/apps', async ({ query }) => {
|
|
const search = (query.search as string) || ''
|
|
const where: any = {}
|
|
if (search) {
|
|
where.name = { contains: search, mode: 'insensitive' }
|
|
}
|
|
|
|
const apps = await prisma.app.findMany({
|
|
where,
|
|
include: {
|
|
_count: { select: { bugs: true } },
|
|
bugs: { where: { status: 'OPEN' }, select: { id: true } },
|
|
},
|
|
orderBy: { name: 'asc' },
|
|
})
|
|
|
|
return apps.map((app) => ({
|
|
id: app.id,
|
|
name: app.name,
|
|
status: app.maintenance ? 'warning' : app.bugs.length > 0 ? 'error' : 'active',
|
|
errors: app.bugs.length,
|
|
version: app.version ?? '-',
|
|
maintenance: app.maintenance,
|
|
}))
|
|
})
|
|
|
|
.get('/api/apps/:appId', async ({ params: { appId }, set }) => {
|
|
const app = await prisma.app.findUnique({
|
|
where: { id: appId },
|
|
include: {
|
|
_count: { select: { bugs: true } },
|
|
bugs: { where: { status: 'OPEN' }, select: { id: true } },
|
|
},
|
|
})
|
|
|
|
if (!app) {
|
|
set.status = 404
|
|
return { error: 'App not found' }
|
|
}
|
|
|
|
return {
|
|
id: app.id,
|
|
name: app.name,
|
|
status: app.maintenance ? 'warning' : app.bugs.length > 0 ? 'error' : 'active',
|
|
errors: app.bugs.length,
|
|
version: app.version ?? '-',
|
|
minVersion: app.minVersion,
|
|
maintenance: app.maintenance,
|
|
totalBugs: app._count.bugs,
|
|
}
|
|
})
|
|
|
|
.get('/api/logs', async ({ query }) => {
|
|
const page = Number(query.page) || 1
|
|
const limit = Number(query.limit) || 20
|
|
const search = (query.search as string) || ''
|
|
const type = query.type as any
|
|
const userId = query.userId as string
|
|
|
|
const where: any = {}
|
|
if (search) {
|
|
where.OR = [
|
|
{ message: { contains: search, mode: 'insensitive' } },
|
|
{ user: { name: { contains: search, mode: 'insensitive' } } }
|
|
]
|
|
}
|
|
if (type && type !== 'all') {
|
|
where.type = type
|
|
}
|
|
if (userId && userId !== 'all') {
|
|
where.userId = userId
|
|
}
|
|
|
|
const [logs, total] = await Promise.all([
|
|
prisma.log.findMany({
|
|
where,
|
|
include: { user: { select: { id: true, name: true, email: true, role: true, image: true } } },
|
|
orderBy: { createdAt: 'desc' },
|
|
skip: (page - 1) * limit,
|
|
take: limit,
|
|
}),
|
|
prisma.log.count({ where })
|
|
])
|
|
|
|
return {
|
|
data: logs,
|
|
totalPages: Math.ceil(total / limit),
|
|
totalItems: total
|
|
}
|
|
})
|
|
|
|
.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: 'DEVELOPER' } }))?.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
|
|
const search = (query.search as string) || ''
|
|
|
|
const where: any = {}
|
|
if (search) {
|
|
where.OR = [
|
|
{ name: { contains: search, mode: 'insensitive' } },
|
|
{ email: { contains: search, mode: 'insensitive' } }
|
|
]
|
|
}
|
|
|
|
const [users, total] = await Promise.all([
|
|
prisma.user.findMany({
|
|
where,
|
|
select: { id: true, name: true, email: true, role: true, active: true, image: true, createdAt: true },
|
|
orderBy: { name: 'asc' },
|
|
skip: (page - 1) * limit,
|
|
take: limit,
|
|
}),
|
|
prisma.user.count({ where })
|
|
])
|
|
|
|
return {
|
|
data: users,
|
|
totalPages: Math.ceil(total / limit),
|
|
totalItems: total
|
|
}
|
|
})
|
|
|
|
.get('/api/operators/stats', async () => {
|
|
const [totalStaff, activeNow, rolesGroup] = await Promise.all([
|
|
prisma.user.count({where: {active: true}}),
|
|
prisma.session.count({
|
|
where: { expiresAt: { gte: new Date() } },
|
|
}),
|
|
prisma.user.groupBy({
|
|
by: ['role'],
|
|
_count: true
|
|
})
|
|
])
|
|
|
|
return {
|
|
totalStaff,
|
|
activeNow,
|
|
rolesCount: rolesGroup.length
|
|
}
|
|
})
|
|
|
|
.post('/api/operators', 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 { name: string; email: string; password: string; role: string }
|
|
|
|
const existing = await prisma.user.findUnique({ where: { email: body.email } })
|
|
if (existing) {
|
|
set.status = 400
|
|
return { error: 'Email sudah terdaftar' }
|
|
}
|
|
|
|
const hashedPassword = await Bun.password.hash(body.password)
|
|
const user = await prisma.user.create({
|
|
data: {
|
|
name: body.name,
|
|
email: body.email,
|
|
password: hashedPassword,
|
|
role: body.role as any,
|
|
},
|
|
})
|
|
|
|
if (userId) {
|
|
await createSystemLog(userId, 'CREATE', `Created new user: ${body.name} (${body.email})`)
|
|
}
|
|
|
|
return { id: user.id, name: user.name, email: user.email, role: user.role }
|
|
})
|
|
|
|
.patch('/api/operators/:id', async ({ params: { id }, 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 { name?: string; email?: string; role?: string; active?: boolean }
|
|
|
|
const user = await prisma.user.update({
|
|
where: { id },
|
|
data: {
|
|
...(body.name !== undefined && { name: body.name }),
|
|
...(body.email !== undefined && { email: body.email }),
|
|
...(body.role !== undefined && { role: body.role as any }),
|
|
...(body.active !== undefined && { active: body.active }),
|
|
},
|
|
})
|
|
|
|
if (userId) {
|
|
await createSystemLog(userId, 'UPDATE', `Updated user: ${user.name} (${user.email})`)
|
|
}
|
|
|
|
return { id: user.id, name: user.name, email: user.email, role: user.role, active: user.active }
|
|
})
|
|
|
|
.delete('/api/operators/:id', async ({ params: { id }, 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 user = await prisma.user.findUnique({ where: { id } })
|
|
if (!user) {
|
|
set.status = 404
|
|
return { error: 'User not found' }
|
|
}
|
|
|
|
// Prevent deleting self
|
|
if (userId === id) {
|
|
set.status = 400
|
|
return { error: 'Cannot delete your own account' }
|
|
}
|
|
|
|
await prisma.session.deleteMany({ where: { userId: id } })
|
|
await prisma.user.update({ where: { id }, data: { active: false } })
|
|
|
|
if (userId) {
|
|
await createSystemLog(userId, 'DELETE', `Deactivated user: ${user.name} (${user.email})`)
|
|
}
|
|
|
|
return { ok: true }
|
|
})
|
|
|
|
.get('/api/logs/operators', async () => {
|
|
return await prisma.user.findMany({
|
|
select: { id: true, name: true, image: true },
|
|
orderBy: { name: 'asc' }
|
|
})
|
|
})
|
|
|
|
.get('/api/bugs', async ({ query }) => {
|
|
const page = Number(query.page) || 1
|
|
const limit = Number(query.limit) || 20
|
|
const search = (query.search as string) || ''
|
|
const app = query.app as any
|
|
const status = query.status as any
|
|
|
|
const where: any = {}
|
|
if (search) {
|
|
where.OR = [
|
|
{ description: { contains: search, mode: 'insensitive' } },
|
|
{ device: { contains: search, mode: 'insensitive' } },
|
|
{ os: { contains: search, mode: 'insensitive' } },
|
|
{ affectedVersion: { contains: search, mode: 'insensitive' } },
|
|
]
|
|
}
|
|
if (app && app !== 'all') {
|
|
where.appId = app
|
|
}
|
|
if (status && status !== 'all') {
|
|
where.status = status
|
|
}
|
|
|
|
const [bugs, total] = await Promise.all([
|
|
prisma.bug.findMany({
|
|
where,
|
|
include: {
|
|
user: { select: { id: true, name: true, email: true, image: true } },
|
|
images: true,
|
|
logs: {
|
|
include: { user: { select: { id: true, name: true, image: true } } },
|
|
orderBy: { createdAt: 'desc' },
|
|
},
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
skip: (page - 1) * limit,
|
|
take: limit,
|
|
}),
|
|
prisma.bug.count({ where }),
|
|
])
|
|
|
|
return {
|
|
data: bugs,
|
|
totalPages: Math.ceil(total / limit),
|
|
totalItems: total,
|
|
}
|
|
})
|
|
|
|
.post('/api/bugs', 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 any
|
|
const defaultAdmin = await prisma.user.findFirst({ where: { role: 'DEVELOPER' } })
|
|
const actingUserId = userId || defaultAdmin?.id || ''
|
|
|
|
const bug = await prisma.bug.create({
|
|
data: {
|
|
appId: body.app,
|
|
affectedVersion: body.affectedVersion,
|
|
device: body.device,
|
|
os: body.os,
|
|
status: body.status || 'OPEN',
|
|
source: body.source || 'USER',
|
|
description: body.description,
|
|
stackTrace: body.stackTrace,
|
|
userId: userId,
|
|
images: body.imageUrl ? {
|
|
create: {
|
|
imageUrl: body.imageUrl
|
|
}
|
|
} : undefined,
|
|
logs: {
|
|
create: {
|
|
userId: actingUserId,
|
|
status: body.status || 'OPEN',
|
|
description: 'Bug reported initially.',
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
return bug
|
|
})
|
|
|
|
.patch('/api/bugs/:id/feedback', async ({ params: { id }, request }) => {
|
|
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 { feedBack: string }
|
|
const defaultAdmin = await prisma.user.findFirst({ where: { role: 'DEVELOPER' } })
|
|
const actingUserId = userId || defaultAdmin?.id || undefined
|
|
|
|
const bug = await prisma.bug.update({
|
|
where: { id },
|
|
data: {
|
|
feedBack: body.feedBack,
|
|
},
|
|
})
|
|
|
|
if (actingUserId) {
|
|
await createSystemLog(actingUserId, 'UPDATE', `Updated bug report feedback - ${id}`)
|
|
}
|
|
|
|
return bug
|
|
})
|
|
|
|
.patch('/api/bugs/:id/status', async ({ params: { id }, request }) => {
|
|
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 { status: string; description?: string }
|
|
const defaultAdmin = await prisma.user.findFirst({ where: { role: 'DEVELOPER' } })
|
|
const actingUserId = userId || defaultAdmin?.id || undefined
|
|
|
|
const bug = await prisma.bug.update({
|
|
where: { id },
|
|
data: {
|
|
status: body.status as any,
|
|
logs: {
|
|
create: {
|
|
userId: actingUserId,
|
|
status: body.status as any,
|
|
description: body.description || `Status updated to ${body.status}`,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
if (actingUserId) {
|
|
await createSystemLog(actingUserId, 'UPDATE', `Updated bug report status to ${body.status}-${id}`)
|
|
}
|
|
|
|
return bug
|
|
})
|
|
|
|
// ─── System Status API ─────────────────────────────
|
|
.get('/api/system/status', async () => {
|
|
try {
|
|
// Check database connectivity
|
|
await prisma.$queryRaw`SELECT 1`
|
|
const activeSessions = await prisma.session.count({
|
|
where: { expiresAt: { gte: new Date() } },
|
|
})
|
|
return {
|
|
status: 'operational',
|
|
database: 'connected',
|
|
activeSessions,
|
|
uptime: process.uptime(),
|
|
}
|
|
} catch {
|
|
return {
|
|
status: 'degraded',
|
|
database: 'disconnected',
|
|
activeSessions: 0,
|
|
uptime: process.uptime(),
|
|
}
|
|
}
|
|
})
|
|
|
|
// ─── Example API ───────────────────────────────────
|
|
.get('/api/hello', () => ({
|
|
message: 'Hello, world!',
|
|
method: 'GET',
|
|
}))
|
|
.put('/api/hello', () => ({
|
|
message: 'Hello, world!',
|
|
method: 'PUT',
|
|
}))
|
|
.get('/api/hello/:name', ({ params }) => ({
|
|
message: `Hello, ${params.name}!`,
|
|
}))
|
|
}
|