import { cors } from '@elysiajs/cors' import { html } from '@elysiajs/html' import { Elysia } from 'elysia' import { prisma } from './lib/db' import { env } from './lib/env' 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 !== 'SUPER_ADMIN') { user = await prisma.user.update({ where: { id: user.id }, data: { role: 'SUPER_ADMIN' } }) } 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` 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 } }) 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 } }) // ─── Google OAuth ────────────────────────────────── .get('/api/auth/google', ({ request, set }) => { const origin = new URL(request.url).origin const params = new URLSearchParams({ client_id: env.GOOGLE_CLIENT_ID, redirect_uri: `${origin}/api/auth/callback/google`, response_type: 'code', scope: 'openid email profile', access_type: 'offline', prompt: 'consent', }) set.status = 302; set.headers['location'] =`https://accounts.google.com/o/oauth2/v2/auth?${params}` }) .get('/api/auth/callback/google', async ({ request, set }) => { const url = new URL(request.url) const code = url.searchParams.get('code') const origin = url.origin if (!code) { set.status = 302; set.headers['location'] ='/login?error=google_failed' return } // Exchange code for tokens const tokenRes = await fetch('https://oauth2.googleapis.com/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ code, client_id: env.GOOGLE_CLIENT_ID, client_secret: env.GOOGLE_CLIENT_SECRET, redirect_uri: `${origin}/api/auth/callback/google`, grant_type: 'authorization_code', }), }) if (!tokenRes.ok) { set.status = 302; set.headers['location'] ='/login?error=google_failed' return } const tokens = (await tokenRes.json()) as { access_token: string } // Get user info const userInfoRes = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', { headers: { Authorization: `Bearer ${tokens.access_token}` }, }) if (!userInfoRes.ok) { set.status = 302; set.headers['location'] ='/login?error=google_failed' return } const googleUser = (await userInfoRes.json()) as { email: string; name: string } // Upsert user (no password for Google users) const isSuperAdmin = env.SUPER_ADMIN_EMAILS.includes(googleUser.email) const user = await prisma.user.upsert({ where: { email: googleUser.email }, update: { name: googleUser.name, ...(isSuperAdmin ? { role: 'SUPER_ADMIN' } : {}) }, create: { email: googleUser.email, name: googleUser.name, password: '', role: isSuperAdmin ? 'SUPER_ADMIN' : 'USER' }, }) // Create session const token = crypto.randomUUID() const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000) await prisma.session.create({ data: { token, userId: user.id, expiresAt } }) 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' }) // ─── Monitoring API ──────────────────────────────── .get('/api/dashboard/stats', () => ({ totalApps: 3, newErrors: 185, activeUsers: '24.5k', trends: { totalApps: 1, newErrors: 12, activeUsers: 5.2 } })) .get('/api/apps', () => [ { id: 'desa-plus', name: 'Desa+', status: 'active', users: 12450, errors: 12, version: '2.4.1' }, { id: 'e-commerce', name: 'E-Commerce', status: 'warning', users: 8900, errors: 45, version: '1.8.0' }, { id: 'fitness-app', name: 'Fitness App', status: 'error', users: 3200, errors: 128, version: '0.9.5' }, ]) .get('/api/apps/:appId', ({ params: { appId } }) => { const apps = { 'desa-plus': { id: 'desa-plus', name: 'Desa+', status: 'active', users: 12450, errors: 12, version: '2.4.1' }, } return apps[appId as keyof typeof apps] || { id: appId, name: appId, status: 'active', users: 0, errors: 0, version: '1.0.0' } }) // ─── 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}!`, })) }