180 lines
7.3 KiB
TypeScript
180 lines
7.3 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'
|
|
|
|
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}!`,
|
|
}))
|
|
}
|