feat: merge url_api & api_key to App, add application settings page
This commit is contained in:
115
src/app.ts
115
src/app.ts
@@ -352,7 +352,8 @@ export function createApp() {
|
||||
// ─── Apps API ──────────────────────────────────────
|
||||
.get('/api/apps', async ({ query }) => {
|
||||
const search = query.search || ''
|
||||
const where: any = {}
|
||||
const all = query.all === 'true'
|
||||
const where: any = all ? {} : { active: true }
|
||||
if (search) {
|
||||
where.name = { contains: search, mode: 'insensitive' }
|
||||
}
|
||||
@@ -372,15 +373,18 @@ export function createApp() {
|
||||
status: app.maintenance ? 'warning' : app.bugs.length > 0 ? 'error' : 'active',
|
||||
errors: app.bugs.length,
|
||||
version: app.version ?? '-',
|
||||
minVersion: app.minVersion,
|
||||
maintenance: app.maintenance,
|
||||
active: app.active,
|
||||
urlApi: app.urlApi,
|
||||
}))
|
||||
}, {
|
||||
query: t.Object({
|
||||
search: t.Optional(t.String({ description: 'Filter berdasarkan nama aplikasi' })),
|
||||
search: t.Optional(t.String()),
|
||||
all: t.Optional(t.String()),
|
||||
}),
|
||||
detail: {
|
||||
summary: 'List Apps',
|
||||
description: 'Mengembalikan semua aplikasi yang dimonitor beserta status (active/warning/error), jumlah bug OPEN, versi, dan mode maintenance.',
|
||||
tags: ['Apps'],
|
||||
},
|
||||
})
|
||||
@@ -407,6 +411,7 @@ export function createApp() {
|
||||
version: app.version ?? '-',
|
||||
minVersion: app.minVersion,
|
||||
maintenance: app.maintenance,
|
||||
urlApi: app.urlApi,
|
||||
totalBugs: app._count.bugs,
|
||||
}
|
||||
}, {
|
||||
@@ -420,6 +425,72 @@ export function createApp() {
|
||||
},
|
||||
})
|
||||
|
||||
.post('/api/apps', async ({ body, request, set }) => {
|
||||
const auth = await requireDeveloper(request, set)
|
||||
if (!auth) return { error: set.status === 401 ? 'Unauthorized' : 'Forbidden' }
|
||||
const { id, name, version, minVersion, maintenance, urlApi, apiKey } = body as any
|
||||
if (!id || !name) { set.status = 400; return { error: 'id and name are required' } }
|
||||
const existing = await prisma.app.findUnique({ where: { id } })
|
||||
if (existing) { set.status = 409; return { error: 'App with this ID already exists' } }
|
||||
const app = await prisma.app.create({
|
||||
data: { id, name, version: version || null, minVersion: minVersion || null, maintenance: maintenance ?? false, urlApi: urlApi || null, apiKey: apiKey || null },
|
||||
})
|
||||
await createSystemLog(auth.userId, 'CREATE', `Created app: ${app.id}`)
|
||||
return { id: app.id, name: app.name, version: app.version, minVersion: app.minVersion, maintenance: app.maintenance, urlApi: app.urlApi }
|
||||
}, {
|
||||
detail: { summary: 'Create App', tags: ['Apps'] },
|
||||
})
|
||||
|
||||
.patch('/api/apps/:appId', async ({ params: { appId }, body, request, set }) => {
|
||||
const auth = await requireDeveloper(request, set)
|
||||
if (!auth) return { error: set.status === 401 ? 'Unauthorized' : 'Forbidden' }
|
||||
const app = await prisma.app.findUnique({ where: { id: appId } })
|
||||
if (!app) { set.status = 404; return { error: 'App not found' } }
|
||||
const { name, version, minVersion, maintenance, urlApi, apiKey } = body as any
|
||||
const updated = await prisma.app.update({
|
||||
where: { id: appId },
|
||||
data: {
|
||||
...(name !== undefined && { name }),
|
||||
...(version !== undefined && { version: version || null }),
|
||||
...(minVersion !== undefined && { minVersion: minVersion || null }),
|
||||
...(maintenance !== undefined && { maintenance }),
|
||||
...(urlApi !== undefined && { urlApi: urlApi || null }),
|
||||
...(apiKey !== undefined && { apiKey: apiKey || null }),
|
||||
},
|
||||
})
|
||||
await createSystemLog(auth.userId, 'UPDATE', `Updated app: ${appId}`)
|
||||
return { id: updated.id, name: updated.name, version: updated.version, minVersion: updated.minVersion, maintenance: updated.maintenance, urlApi: updated.urlApi }
|
||||
}, {
|
||||
params: t.Object({ appId: t.String() }),
|
||||
detail: { summary: 'Update App', tags: ['Apps'] },
|
||||
})
|
||||
|
||||
.delete('/api/apps/:appId', async ({ params: { appId }, request, set }) => {
|
||||
const auth = await requireDeveloper(request, set)
|
||||
if (!auth) return { error: set.status === 401 ? 'Unauthorized' : 'Forbidden' }
|
||||
const app = await prisma.app.findUnique({ where: { id: appId } })
|
||||
if (!app) { set.status = 404; return { error: 'App not found' } }
|
||||
await prisma.app.update({ where: { id: appId }, data: { active: false } })
|
||||
await createSystemLog(auth.userId, 'UPDATE', `Deactivated app: ${appId}`)
|
||||
return { success: true }
|
||||
}, {
|
||||
params: t.Object({ appId: t.String() }),
|
||||
detail: { summary: 'Deactivate App', tags: ['Apps'] },
|
||||
})
|
||||
|
||||
.post('/api/apps/:appId/activate', async ({ params: { appId }, request, set }) => {
|
||||
const auth = await requireDeveloper(request, set)
|
||||
if (!auth) return { error: set.status === 401 ? 'Unauthorized' : 'Forbidden' }
|
||||
const app = await prisma.app.findUnique({ where: { id: appId } })
|
||||
if (!app) { set.status = 404; return { error: 'App not found' } }
|
||||
await prisma.app.update({ where: { id: appId }, data: { active: true } })
|
||||
await createSystemLog(auth.userId, 'UPDATE', `Activated app: ${appId}`)
|
||||
return { success: true }
|
||||
}, {
|
||||
params: t.Object({ appId: t.String() }),
|
||||
detail: { summary: 'Activate App', tags: ['Apps'] },
|
||||
})
|
||||
|
||||
// ─── Logs API ──────────────────────────────────────
|
||||
.get('/api/logs', async ({ query }) => {
|
||||
const page = Number(query.page) || 1
|
||||
@@ -1553,47 +1624,17 @@ export function createApp() {
|
||||
return { sessions: result, summary: { totalSessions: result.length, activeSessions: active, expiredSessions: expired, onlineUsers: onlineIds.size, byRole } }
|
||||
})
|
||||
|
||||
// ─── App Config ────────────────────────────────────────────────────────────
|
||||
|
||||
.get('/api/admin/config', async ({ request, set }) => {
|
||||
const auth = await requireDeveloper(request, set)
|
||||
if (!auth) return { error: set.status === 401 ? 'Unauthorized' : 'Forbidden' }
|
||||
const configs = await prisma.appConfig.findMany({ orderBy: { key: 'asc' } })
|
||||
return { configs: configs.map((c) => ({ key: c.key, value: c.value, updatedAt: c.updatedAt.toISOString() })) }
|
||||
}, {
|
||||
detail: { summary: 'Get App Config', tags: ['Admin'] },
|
||||
})
|
||||
|
||||
.put('/api/admin/config', async ({ request, set }) => {
|
||||
const auth = await requireDeveloper(request, set)
|
||||
if (!auth) return { error: set.status === 401 ? 'Unauthorized' : 'Forbidden' }
|
||||
const body = await request.json() as { key: string; value: string }
|
||||
if (!body.key || typeof body.value !== 'string') { set.status = 400; return { error: 'key and value required' } }
|
||||
const config = await prisma.appConfig.upsert({
|
||||
where: { key: body.key },
|
||||
update: { value: body.value },
|
||||
create: { key: body.key, value: body.value },
|
||||
})
|
||||
await createSystemLog(auth.userId, 'UPDATE', `Updated app config: ${body.key}`)
|
||||
return { key: config.key, value: config.value, updatedAt: config.updatedAt.toISOString() }
|
||||
}, {
|
||||
detail: { summary: 'Update App Config', tags: ['Admin'] },
|
||||
})
|
||||
|
||||
// ─── Desa Plus Proxy ───────────────────────────────────────────────────────
|
||||
|
||||
.all('/api/proxy/desa-plus/*', async ({ request, set }) => {
|
||||
const [baseConfig, apiKeyConfig] = await Promise.all([
|
||||
prisma.appConfig.findUnique({ where: { key: 'URL_API_DESA_PLUS' } }),
|
||||
prisma.appConfig.findUnique({ where: { key: 'API_KEY_DESA_PLUS' } }),
|
||||
])
|
||||
if (!baseConfig?.value) { set.status = 503; return { error: 'URL_API_DESA_PLUS belum dikonfigurasi. Set di /dev → Settings.' } }
|
||||
const base = baseConfig.value.replace(/\/$/, '')
|
||||
const app = await prisma.app.findUnique({ where: { id: 'desa-plus' } })
|
||||
if (!app?.urlApi) { set.status = 503; return { error: 'urlApi belum dikonfigurasi untuk app desa-plus.' } }
|
||||
const base = app.urlApi.replace(/\/$/, '')
|
||||
const url = new URL(request.url)
|
||||
const upstream = `${base}${url.pathname.replace('/api/proxy/desa-plus', '')}${url.search}`
|
||||
const headers = new Headers(request.headers)
|
||||
headers.delete('host')
|
||||
if (apiKeyConfig?.value) headers.set('X-API-Key', apiKeyConfig.value)
|
||||
if (app.apiKey) headers.set('X-API-Key', app.apiKey)
|
||||
try {
|
||||
const res = await fetch(upstream, { method: request.method, headers, body: request.method !== 'GET' && request.method !== 'HEAD' ? request.body : undefined })
|
||||
const contentType = res.headers.get('content-type') ?? 'application/json'
|
||||
|
||||
Reference in New Issue
Block a user