File: /var/www/indoadvisory_new/webapp/src/routes/admin.tsx
import { Hono } from 'hono'
import { getCookie, setCookie, deleteCookie } from 'hono/cookie'
import { generateSessionId, validatePassword, isValidSession, type User, type Session } from '../utils/auth'
import { initializeDatabase, createSlug, formatDate, type Article, type ContactInquiry, type SiteSetting } from '../utils/database'
type Bindings = {
DB: D1Database;
}
const admin = new Hono<{ Bindings: Bindings }>()
// Middleware to check admin authentication
admin.use('/*', async (c, next) => {
// Skip auth check for login page and login POST
if (c.req.path === '/admin/login' && (c.req.method === 'GET' || c.req.method === 'POST')) {
return next()
}
const sessionId = getCookie(c, 'admin_session')
if (!sessionId) {
return c.redirect('/admin/login')
}
try {
const session = await c.env.DB.prepare(
'SELECT s.*, u.id as user_id, u.username, u.email, u.name, u.role FROM admin_sessions s JOIN admin_users u ON s.user_id = u.id WHERE s.id = ?'
).bind(sessionId).first() as Session & User
if (!session || !isValidSession(session)) {
deleteCookie(c, 'admin_session')
return c.redirect('/admin/login')
}
c.set('user', session)
return next()
} catch (error) {
return c.redirect('/admin/login')
}
})
// Admin Login Page
admin.get('/login', (c) => {
return c.html(`
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Login - IndoPrivate</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center">
<div class="bg-white p-8 rounded-xl shadow-lg w-full max-w-md">
<div class="text-center mb-8">
<h1 class="text-2xl font-bold text-gray-800">
<i class="fas fa-chart-line text-blue-600 mr-2"></i>
IndoPrivate Admin
</h1>
<p class="text-gray-600 mt-2">Masuk ke dashboard admin</p>
</div>
<form action="/admin/login" method="POST" class="space-y-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Username</label>
<input
type="text"
name="username"
required
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Masukkan username"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Password</label>
<input
type="password"
name="password"
required
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Masukkan password"
/>
</div>
<button
type="submit"
class="w-full bg-blue-600 text-white py-3 px-6 rounded-lg font-semibold hover:bg-blue-700 transition-colors"
>
<i class="fas fa-sign-in-alt mr-2"></i>
Masuk
</button>
</form>
<div class="mt-6 text-center text-sm text-gray-500">
<p>Demo credentials: <strong>admin / admin123</strong></p>
</div>
</div>
</body>
</html>
`)
})
// Admin Login Handler
admin.post('/login', async (c) => {
const { username, password } = await c.req.parseBody()
try {
await initializeDatabase(c.env.DB)
// Insert demo admin if not exists
await c.env.DB.prepare(
'INSERT OR IGNORE INTO admin_users (username, password_hash, email, name, role) VALUES (?, ?, ?, ?, ?)'
).bind('admin', 'admin123', 'admin@indoprivate.co.id', 'Administrator', 'admin').run()
const user = await c.env.DB.prepare(
'SELECT * FROM admin_users WHERE username = ?'
).bind(username).first() as User
if (!user || !validatePassword(password as string, user.password_hash)) {
return c.html(`
<script>
alert('Username atau password salah');
window.location.href = '/admin/login';
</script>
`)
}
// Create session
const sessionId = generateSessionId()
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000) // 24 hours
await c.env.DB.prepare(
'INSERT INTO admin_sessions (id, user_id, expires_at) VALUES (?, ?, ?)'
).bind(sessionId, user.id, expiresAt.toISOString()).run()
setCookie(c, 'admin_session', sessionId, {
maxAge: 24 * 60 * 60,
httpOnly: true,
secure: false, // Set to true in production with HTTPS
sameSite: 'Lax'
})
return c.redirect('/admin/dashboard')
} catch (error) {
return c.html(`
<script>
alert('Terjadi kesalahan sistem');
window.location.href = '/admin/login';
</script>
`)
}
})
// Admin Dashboard
admin.get('/dashboard', async (c) => {
const user = c.get('user') as User
try {
// Get dashboard statistics
const articlesCount = await c.env.DB.prepare('SELECT COUNT(*) as count FROM articles').first() as { count: number }
const inquiriesCount = await c.env.DB.prepare('SELECT COUNT(*) as count FROM contact_inquiries WHERE status = "new"').first() as { count: number }
const publishedArticles = await c.env.DB.prepare('SELECT COUNT(*) as count FROM articles WHERE status = "published"').first() as { count: number }
const clientsCount = await c.env.DB.prepare('SELECT COUNT(*) as count FROM clients WHERE is_active = 1').first() as { count: number }
return c.html(`
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard Admin - IndoPrivate</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
</head>
<body class="bg-gray-100">
${adminNavbar(user)}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-800">Dashboard Admin</h1>
<p class="text-gray-600 mt-2">Selamat datang kembali, ${user.name}</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div class="bg-white p-6 rounded-xl shadow-lg">
<div class="flex items-center">
<div class="bg-blue-100 p-3 rounded-full">
<i class="fas fa-newspaper text-blue-600 text-xl"></i>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-800">Total Artikel</h3>
<p class="text-2xl font-bold text-blue-600">${articlesCount.count}</p>
</div>
</div>
</div>
<div class="bg-white p-6 rounded-xl shadow-lg">
<div class="flex items-center">
<div class="bg-green-100 p-3 rounded-full">
<i class="fas fa-check-circle text-green-600 text-xl"></i>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-800">Artikel Published</h3>
<p class="text-2xl font-bold text-green-600">${publishedArticles.count}</p>
</div>
</div>
</div>
<div class="bg-white p-6 rounded-xl shadow-lg">
<div class="flex items-center">
<div class="bg-orange-100 p-3 rounded-full">
<i class="fas fa-envelope text-orange-600 text-xl"></i>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-800">Inquiry Baru</h3>
<p class="text-2xl font-bold text-orange-600">${inquiriesCount.count}</p>
</div>
</div>
</div>
<div class="bg-white p-6 rounded-xl shadow-lg">
<div class="flex items-center">
<div class="bg-purple-100 p-3 rounded-full">
<i class="fas fa-building text-purple-600 text-xl"></i>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-800">Klien Aktif</h3>
<p class="text-2xl font-bold text-purple-600">${clientsCount.count}</p>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div class="bg-white p-6 rounded-xl shadow-lg">
<h2 class="text-xl font-bold text-gray-800 mb-4">
<i class="fas fa-newspaper mr-2"></i>
Quick Actions
</h2>
<div class="space-y-4">
<a href="/admin/articles/create" class="flex items-center p-4 bg-blue-50 rounded-lg hover:bg-blue-100 transition-colors">
<i class="fas fa-plus text-blue-600 mr-3"></i>
<div>
<h3 class="font-semibold text-gray-800">Buat Artikel Baru</h3>
<p class="text-gray-600 text-sm">Tulis dan publikasikan artikel terbaru</p>
</div>
</a>
<a href="/admin/articles" class="flex items-center p-4 bg-green-50 rounded-lg hover:bg-green-100 transition-colors">
<i class="fas fa-list text-green-600 mr-3"></i>
<div>
<h3 class="font-semibold text-gray-800">Kelola Artikel</h3>
<p class="text-gray-600 text-sm">Edit atau hapus artikel yang ada</p>
</div>
</a>
<a href="/admin/inquiries" class="flex items-center p-4 bg-orange-50 rounded-lg hover:bg-orange-100 transition-colors">
<i class="fas fa-envelope text-orange-600 mr-3"></i>
<div>
<h3 class="font-semibold text-gray-800">Lihat Inquiry</h3>
<p class="text-gray-600 text-sm">Respon pertanyaan dari prospek</p>
</div>
</a>
<a href="/admin/clients" class="flex items-center p-4 bg-purple-50 rounded-lg hover:bg-purple-100 transition-colors">
<i class="fas fa-building text-purple-600 mr-3"></i>
<div>
<h3 class="font-semibold text-gray-800">Kelola Klien</h3>
<p class="text-gray-600 text-sm">Manage client showcase dan portfolio</p>
</div>
</a>
<a href="/admin/team" class="flex items-center p-4 bg-teal-50 rounded-lg hover:bg-teal-100 transition-colors">
<i class="fas fa-users text-teal-600 mr-3"></i>
<div>
<h3 class="font-semibold text-gray-800">Kelola Tim</h3>
<p class="text-gray-600 text-sm">Manage profil anggota tim</p>
</div>
</a>
</div>
</div>
<div class="bg-white p-6 rounded-xl shadow-lg">
<h2 class="text-xl font-bold text-gray-800 mb-4">
<i class="fas fa-chart-bar mr-2"></i>
Website Statistics
</h2>
<div class="space-y-4">
<div class="flex justify-between items-center">
<span class="text-gray-600">Artikel Terbaru</span>
<span class="font-semibold">7 hari terakhir</span>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-600">Total Halaman</span>
<span class="font-semibold">8 halaman</span>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-600">Status Website</span>
<span class="text-green-600 font-semibold">
<i class="fas fa-check-circle mr-1"></i>
Online
</span>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
`)
} catch (error) {
console.error('Dashboard error:', error)
return c.html(`<div>Error loading dashboard</div>`)
}
})
// Admin Navbar Component
function adminNavbar(user: User): string {
return `
<nav class="bg-white shadow-lg border-b">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<div class="flex items-center">
<h1 class="text-xl font-bold text-gray-800">
<i class="fas fa-chart-line text-blue-600 mr-2"></i>
IndoPrivate Admin
</h1>
</div>
<div class="hidden md:flex items-center space-x-6">
<a href="/admin/dashboard" class="text-gray-700 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium">
<i class="fas fa-tachometer-alt mr-1"></i>
Dashboard
</a>
<a href="/admin/articles" class="text-gray-700 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium">
<i class="fas fa-newspaper mr-1"></i>
Artikel
</a>
<a href="/admin/inquiries" class="text-gray-700 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium">
<i class="fas fa-envelope mr-1"></i>
Inquiry
</a>
<a href="/admin/clients" class="text-gray-700 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium">
<i class="fas fa-building mr-1"></i>
Klien
</a>
<a href="/admin/team" class="text-gray-700 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium">
<i class="fas fa-users mr-1"></i>
Tim
</a>
<a href="/admin/settings" class="text-gray-700 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium">
<i class="fas fa-cog mr-1"></i>
Settings
</a>
<a href="/" target="_blank" class="text-gray-700 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium">
<i class="fas fa-external-link-alt mr-1"></i>
Lihat Website
</a>
</div>
<div class="flex items-center">
<span class="text-gray-700 mr-4">
<i class="fas fa-user mr-1"></i>
${user.name}
</span>
<a href="/admin/logout" class="text-red-600 hover:text-red-700 px-3 py-2 rounded-md text-sm font-medium">
<i class="fas fa-sign-out-alt mr-1"></i>
Logout
</a>
</div>
</div>
</div>
</nav>
`
}
// Logout
admin.get('/logout', (c) => {
deleteCookie(c, 'admin_session')
return c.redirect('/admin/login')
})
export default admin