Files
smanab/buku-induk-sma-negeri-1-abiansemal/server.js

795 lines
27 KiB
JavaScript

import express from 'express';
import path from 'path';
import cors from 'cors';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
import bcrypt from 'bcryptjs';
import pool, { initDatabase } from './server/db.js';
dotenv.config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
const PORT = process.env.PORT || 3006;
// Middleware
app.use(cors());
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
// Helper: Convert camelCase to snake_case
const camelToSnake = (str) => str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
const snakeToCamel = (str) => str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
// Helper: Convert object keys
const convertKeysToSnake = (obj) => {
const result = {};
for (const key in obj) {
result[camelToSnake(key)] = obj[key];
}
return result;
};
const convertKeysToCamel = (obj) => {
const result = {};
for (const key in obj) {
result[snakeToCamel(key)] = obj[key];
}
return result;
};
// ==================== AUTH API ====================
// Login
app.post('/api/auth/login', async (req, res) => {
try {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ success: false, message: 'Username dan password diperlukan' });
}
const [users] = await pool.query('SELECT * FROM users WHERE username = ?', [username]);
if (users.length === 0) {
return res.status(401).json({ success: false, message: 'Username atau password salah' });
}
const user = users[0];
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return res.status(401).json({ success: false, message: 'Username atau password salah' });
}
res.json({
success: true,
user: {
id: user.id,
username: user.username,
fullName: user.full_name,
role: user.role
}
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ success: false, message: 'Terjadi kesalahan server' });
}
});
// ==================== STUDENTS API ====================
// Get all students
app.get('/api/students', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM students ORDER BY nama ASC');
const students = rows.map(row => {
const student = convertKeysToCamel(row);
// Normalize field names for frontend compatibility
return {
...student,
id: String(student.id),
fotoDiterimaUrl: student.fotoDiterimaUrl || '',
fotoLulusUrl: student.fotoLulusUrl || '',
};
});
res.json(students);
} catch (error) {
console.error('Error fetching students:', error);
res.status(500).json({ success: false, message: 'Gagal mengambil data siswa' });
}
});
// Get single student
app.get('/api/students/:id', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM students WHERE id = ?', [req.params.id]);
if (rows.length === 0) {
return res.status(404).json({ success: false, message: 'Siswa tidak ditemukan' });
}
const student = convertKeysToCamel(rows[0]);
student.id = String(student.id);
res.json(student);
} catch (error) {
console.error('Error fetching student:', error);
res.status(500).json({ success: false, message: 'Gagal mengambil data siswa' });
}
});
// Create/Update student
app.post('/api/students', async (req, res) => {
try {
const studentData = req.body;
const { id, ...data } = studentData;
// Convert keys to snake_case for database
const dbData = convertKeysToSnake(data);
// Remove undefined/null values and clean up data
const cleanData = {};
for (const [key, value] of Object.entries(dbData)) {
if (value !== undefined && value !== null) {
cleanData[key] = value;
}
}
if (id && id !== '' && !isNaN(parseInt(id))) {
// Update existing student
const fields = Object.keys(cleanData);
const values = Object.values(cleanData);
const setClause = fields.map(f => `${f} = ?`).join(', ');
await pool.query(`UPDATE students SET ${setClause} WHERE id = ?`, [...values, parseInt(id)]);
res.json({ success: true, message: 'Data siswa berhasil diupdate', id: parseInt(id) });
} else {
// Insert new student
const fields = Object.keys(cleanData);
const values = Object.values(cleanData);
const placeholders = fields.map(() => '?').join(', ');
const [result] = await pool.query(
`INSERT INTO students (${fields.join(', ')}) VALUES (${placeholders})`,
values
);
res.json({ success: true, message: 'Data siswa berhasil disimpan', id: result.insertId });
}
} catch (error) {
console.error('Error saving student:', error);
res.status(500).json({ success: false, message: 'Gagal menyimpan data siswa: ' + error.message });
}
});
// Delete student
app.delete('/api/students/:id', async (req, res) => {
try {
const [result] = await pool.query('DELETE FROM students WHERE id = ?', [req.params.id]);
if (result.affectedRows === 0) {
return res.status(404).json({ success: false, message: 'Siswa tidak ditemukan' });
}
res.json({ success: true, message: 'Data siswa berhasil dihapus' });
} catch (error) {
console.error('Error deleting student:', error);
res.status(500).json({ success: false, message: 'Gagal menghapus data siswa' });
}
});
// ==================== SETTINGS API ====================
// Get settings
app.get('/api/settings', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM settings WHERE setting_key = ?', ['app_settings']);
if (rows.length === 0) {
res.json({
schoolName: 'SMA NEGERI 1 ABIANSEMAL',
logoUrl: 'https://iili.io/KN7pUR2.png',
faviconUrl: '',
tahunAjaran: '2025/2026',
margins: { top: 20, right: 20, bottom: 20, left: 20 }
});
} else {
res.json(JSON.parse(rows[0].setting_value));
}
} catch (error) {
console.error('Error fetching settings:', error);
res.status(500).json({ success: false, message: 'Gagal mengambil pengaturan' });
}
});
// Save settings
app.post('/api/settings', async (req, res) => {
try {
const settings = req.body;
const [existing] = await pool.query('SELECT * FROM settings WHERE setting_key = ?', ['app_settings']);
if (existing.length === 0) {
await pool.query('INSERT INTO settings (setting_key, setting_value) VALUES (?, ?)',
['app_settings', JSON.stringify(settings)]);
} else {
await pool.query('UPDATE settings SET setting_value = ? WHERE setting_key = ?',
[JSON.stringify(settings), 'app_settings']);
}
res.json({ success: true, message: 'Pengaturan berhasil disimpan' });
} catch (error) {
console.error('Error saving settings:', error);
res.status(500).json({ success: false, message: 'Gagal menyimpan pengaturan' });
}
});
// ==================== STATISTICS API ====================
// Get student statistics
app.get('/api/stats', async (req, res) => {
try {
const [total] = await pool.query('SELECT COUNT(*) as count FROM students');
const [laki] = await pool.query("SELECT COUNT(*) as count FROM students WHERE jenis_kelamin = 'L'");
const [perempuan] = await pool.query("SELECT COUNT(*) as count FROM students WHERE jenis_kelamin = 'P'");
const [byTahun] = await pool.query(
'SELECT tahun_ajaran, COUNT(*) as count FROM students GROUP BY tahun_ajaran ORDER BY tahun_ajaran DESC'
);
res.json({
total: total[0].count,
laki: laki[0].count,
perempuan: perempuan[0].count,
byTahunAjaran: byTahun
});
} catch (error) {
console.error('Error fetching stats:', error);
res.status(500).json({ success: false, message: 'Gagal mengambil statistik' });
}
});
// ==================== LEGER API ====================
// Get all legers for a student
app.get('/api/students/:id/legers', async (req, res) => {
try {
const studentId = req.params.id;
const [rows] = await pool.query(
'SELECT id, student_id, nis, nama, semester, file_name, file_size, uploaded_at, updated_at FROM student_legers WHERE student_id = ? ORDER BY semester ASC',
[studentId]
);
res.json(rows);
} catch (error) {
console.error('Error fetching legers:', error);
res.status(500).json({ success: false, message: 'Gagal mengambil data leger' });
}
});
// Get single leger with file data (for download/view)
app.get('/api/students/:id/legers/:semester', async (req, res) => {
try {
const { id, semester } = req.params;
const [rows] = await pool.query(
'SELECT * FROM student_legers WHERE student_id = ? AND semester = ?',
[id, semester]
);
if (rows.length === 0) {
return res.status(404).json({ success: false, message: 'Leger tidak ditemukan' });
}
res.json(rows[0]);
} catch (error) {
console.error('Error fetching leger:', error);
res.status(500).json({ success: false, message: 'Gagal mengambil data leger' });
}
});
// Upload/Update leger
app.post('/api/students/:id/legers', async (req, res) => {
try {
const studentId = req.params.id;
const { semester, fileName, fileData, fileSize, nis, nama } = req.body;
if (!semester || !fileName || !fileData) {
return res.status(400).json({ success: false, message: 'Data tidak lengkap' });
}
// Check if leger already exists for this student and semester
const [existing] = await pool.query(
'SELECT id FROM student_legers WHERE student_id = ? AND semester = ?',
[studentId, semester]
);
if (existing.length > 0) {
// Update existing
await pool.query(
'UPDATE student_legers SET file_name = ?, file_data = ?, file_size = ?, nis = ?, nama = ?, updated_at = NOW() WHERE student_id = ? AND semester = ?',
[fileName, fileData, fileSize, nis, nama, studentId, semester]
);
res.json({ success: true, message: `Leger semester ${semester} berhasil diperbarui` });
} else {
// Insert new
await pool.query(
'INSERT INTO student_legers (student_id, nis, nama, semester, file_name, file_data, file_size) VALUES (?, ?, ?, ?, ?, ?, ?)',
[studentId, nis, nama, semester, fileName, fileData, fileSize]
);
res.json({ success: true, message: `Leger semester ${semester} berhasil diupload` });
}
} catch (error) {
console.error('Error uploading leger:', error);
res.status(500).json({ success: false, message: 'Gagal mengupload leger: ' + error.message });
}
});
// Delete leger
app.delete('/api/students/:id/legers/:semester', async (req, res) => {
try {
const { id, semester } = req.params;
const [result] = await pool.query(
'DELETE FROM student_legers WHERE student_id = ? AND semester = ?',
[id, semester]
);
if (result.affectedRows === 0) {
return res.status(404).json({ success: false, message: 'Leger tidak ditemukan' });
}
res.json({ success: true, message: `Leger semester ${semester} berhasil dihapus` });
} catch (error) {
console.error('Error deleting leger:', error);
res.status(500).json({ success: false, message: 'Gagal menghapus leger' });
}
});
// Export all legers for a student as JSON (for backup)
app.get('/api/students/:id/legers/export/all', async (req, res) => {
try {
const studentId = req.params.id;
const [rows] = await pool.query(
'SELECT * FROM student_legers WHERE student_id = ? ORDER BY semester ASC',
[studentId]
);
res.json({
success: true,
studentId,
legers: rows
});
} catch (error) {
console.error('Error exporting legers:', error);
res.status(500).json({ success: false, message: 'Gagal mengekspor leger' });
}
});
// ==================== DOCUMENTS API ====================
// Get all documents for a student (optionally filter by type)
app.get('/api/students/:id/documents', async (req, res) => {
try {
const studentId = req.params.id;
const docType = req.query.type;
let query = 'SELECT id, student_id, nis, nama, doc_type, doc_name, file_name, file_size, uploaded_at, updated_at FROM student_documents WHERE student_id = ?';
const params = [studentId];
if (docType) {
query += ' AND doc_type = ?';
params.push(docType);
}
query += ' ORDER BY uploaded_at DESC';
const [rows] = await pool.query(query, params);
res.json(rows);
} catch (error) {
console.error('Error fetching documents:', error);
res.status(500).json({ success: false, message: 'Gagal mengambil data dokumen' });
}
});
// Get single document with file data
app.get('/api/students/:id/documents/:docId', async (req, res) => {
try {
const { id, docId } = req.params;
const [rows] = await pool.query(
'SELECT * FROM student_documents WHERE student_id = ? AND id = ?',
[id, docId]
);
if (rows.length === 0) {
return res.status(404).json({ success: false, message: 'Dokumen tidak ditemukan' });
}
res.json(rows[0]);
} catch (error) {
console.error('Error fetching document:', error);
res.status(500).json({ success: false, message: 'Gagal mengambil data dokumen' });
}
});
// Upload document
app.post('/api/students/:id/documents', async (req, res) => {
try {
const studentId = req.params.id;
const { docType, docName, fileName, fileData, fileSize, nis, nama } = req.body;
if (!docType || !fileName || !fileData) {
return res.status(400).json({ success: false, message: 'Data tidak lengkap' });
}
// For ijazah, check if one already exists (only allow 1 ijazah per student)
if (docType === 'ijazah') {
const [existing] = await pool.query(
'SELECT id FROM student_documents WHERE student_id = ? AND doc_type = ?',
[studentId, 'ijazah']
);
if (existing.length > 0) {
// Update existing ijazah
await pool.query(
'UPDATE student_documents SET doc_name = ?, file_name = ?, file_data = ?, file_size = ?, nis = ?, nama = ?, updated_at = NOW() WHERE student_id = ? AND doc_type = ?',
[docName || 'Ijazah', fileName, fileData, fileSize, nis, nama, studentId, 'ijazah']
);
return res.json({ success: true, message: 'Ijazah berhasil diperbarui' });
}
}
// Insert new document
await pool.query(
'INSERT INTO student_documents (student_id, nis, nama, doc_type, doc_name, file_name, file_data, file_size) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[studentId, nis, nama, docType, docName || (docType === 'ijazah' ? 'Ijazah' : 'Sertifikat'), fileName, fileData, fileSize]
);
const docLabel = docType === 'ijazah' ? 'Ijazah' : 'Sertifikat';
res.json({ success: true, message: `${docLabel} berhasil diupload` });
} catch (error) {
console.error('Error uploading document:', error);
res.status(500).json({ success: false, message: 'Gagal mengupload dokumen: ' + error.message });
}
});
// Delete document
app.delete('/api/students/:id/documents/:docId', async (req, res) => {
try {
const { id, docId } = req.params;
const [result] = await pool.query(
'DELETE FROM student_documents WHERE student_id = ? AND id = ?',
[id, docId]
);
if (result.affectedRows === 0) {
return res.status(404).json({ success: false, message: 'Dokumen tidak ditemukan' });
}
res.json({ success: true, message: 'Dokumen berhasil dihapus' });
} catch (error) {
console.error('Error deleting document:', error);
res.status(500).json({ success: false, message: 'Gagal menghapus dokumen' });
}
});
// ==================== PUBLIC STUDENT ACCESS API ====================
// Search student by name and birth date (Public)
app.post('/api/public/find-student', async (req, res) => {
try {
const { nama, tanggalLahir } = req.body;
if (!nama || !tanggalLahir) {
return res.status(400).json({ success: false, message: 'Nama dan Tanggal Lahir harus diisi' });
}
console.log(`Searching for: Name="${nama}", DOB="${tanggalLahir}"`);
// Helper to format date from YYYY-MM-DD to DD/MM/YYYY
const formatDateDMY = (dateStr) => {
if (!dateStr) return '';
const parts = dateStr.split('-');
if (parts.length === 3) {
return `${parts[2]}/${parts[1]}/${parts[0]}`;
}
return dateStr;
};
// Helper to format date from YYYY-MM-DD to DD-MM-YYYY
const formatDateDMYHyphen = (dateStr) => {
if (!dateStr) return '';
const parts = dateStr.split('-');
if (parts.length === 3) {
return `${parts[2]}-${parts[1]}-${parts[0]}`;
}
return dateStr;
};
const tanggalLahirDMY = formatDateDMY(tanggalLahir);
const tanggalLahirDMYHyphen = formatDateDMYHyphen(tanggalLahir);
// Try multiple date formats
// Robust search: Remove all spaces for name comparison to handle typo/spacing issues
// AND try multiple date formats
let query = `
SELECT id, nis, nama, tahun_ajaran, tanggal_lahir
FROM students
WHERE
LOWER(REPLACE(nama, ' ', '')) = LOWER(REPLACE(?, ' ', ''))
AND (
tanggal_lahir = ?
OR tanggal_lahir = ?
OR tanggal_lahir = ?
)
`;
let params = [nama, tanggalLahir, tanggalLahirDMY, tanggalLahirDMYHyphen];
const [rows] = await pool.query(query, params);
if (rows.length === 0) {
console.log('Search returned no results');
// Check for partial matches for debugging
// Name match (ignoring spaces)
const [nameCheck] = await pool.query(
"SELECT count(*) as count FROM students WHERE LOWER(REPLACE(nama, ' ', '')) = LOWER(REPLACE(?, ' ', ''))",
[nama]
);
// Date match (any format)
const [dateCheck] = await pool.query(
"SELECT count(*) as count FROM students WHERE tanggal_lahir = ? OR tanggal_lahir = ? OR tanggal_lahir = ?",
[tanggalLahir, tanggalLahirDMY, tanggalLahirDMYHyphen]
);
const debugInfo = {
nameMatches: nameCheck[0].count,
dateMatches: dateCheck[0].count,
searchedName: nama,
searchedDate: tanggalLahir,
searchedDateDMY: tanggalLahirDMY,
searchedDateDMYHyphen: tanggalLahirDMYHyphen
};
console.log('Debug info:', debugInfo);
let message = 'Data tidak ditemukan.';
if (nameCheck[0].count > 0 && dateCheck[0].count === 0) {
message = 'Nama siswa ditemukan, tetapi tanggal lahir tidak cocok. Pastikan tanggal lahir sesuai data sekolah.';
} else if (nameCheck[0].count === 0 && dateCheck[0].count > 0) {
message = 'Tanggal lahir valid, tetapi nama tidak ditemukan. Periksa penulisan nama (sesuai Ijazah/Akta).';
} else {
message = 'Data siswa tidak ditemukan. Periksa Nama dan Tanggal Lahir.';
}
return res.status(404).json({
success: false,
message,
debug: debugInfo
});
}
// Jika ditemukan, kembalikan data minimal (NIS & ID) untuk akses selanjutnya
res.json({
success: true,
student: convertKeysToCamel(rows[0])
});
} catch (error) {
console.error('Public search error:', error);
res.status(500).json({ success: false, message: 'Terjadi kesalahan pada server' });
}
});
// Health Check
app.get('/api/health', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
env: process.env.NODE_ENV,
db_name: process.env.DB_NAME
});
});
// Get student detail for public view (requires student ID)
app.get('/api/public/students/:id', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM students WHERE id = ?', [req.params.id]);
if (rows.length === 0) return res.status(404).json({ message: 'Siswa tidak ditemukan' });
res.json(convertKeysToCamel(rows[0]));
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// ==================== ACADEMIC YEARS API ====================
// Get all years
app.get('/api/years', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM academic_years ORDER BY year_name DESC');
res.json(rows.map(convertKeysToCamel));
} catch (error) {
console.error('Error fetching years:', error);
res.status(500).json({ success: false, message: 'Gagal mengambil data tahun ajaran' });
}
});
// Add year
app.post('/api/years', async (req, res) => {
try {
const { yearName } = req.body;
await pool.query('INSERT INTO academic_years (year_name) VALUES (?)', [yearName]);
res.json({ success: true, message: 'Tahun ajaran berhasil ditambah' });
} catch (error) {
console.error('Error adding year:', error);
res.status(500).json({ success: false, message: 'Gagal menambah tahun ajaran' });
}
});
// Set active year
app.post('/api/years/:id/active', async (req, res) => {
try {
const yearId = req.params.id;
// Set all to false
await pool.query('UPDATE academic_years SET is_active = FALSE');
// Set target to true
await pool.query('UPDATE academic_years SET is_active = TRUE WHERE id = ?', [yearId]);
// Also sync to global settings for backward compatibility/quick access
const [yearRows] = await pool.query('SELECT year_name FROM academic_years WHERE id = ?', [yearId]);
if (yearRows.length > 0) {
const newYear = yearRows[0].year_name;
const [settingsRows] = await pool.query('SELECT * FROM settings WHERE setting_key = ?', ['app_settings']);
if (settingsRows.length > 0) {
const currentSettings = JSON.parse(settingsRows[0].setting_value);
currentSettings.tahunAjaran = newYear;
await pool.query('UPDATE settings SET setting_value = ? WHERE setting_key = ?',
[JSON.stringify(currentSettings), 'app_settings']);
}
}
res.json({ success: true, message: 'Tahun ajaran aktif berhasil diubah' });
} catch (error) {
console.error('Error setting active year:', error);
res.status(500).json({ success: false, message: 'Gagal mengubah tahun ajaran aktif' });
}
});
// Delete year
app.delete('/api/years/:id', async (req, res) => {
try {
const yearId = req.params.id;
const [year] = await pool.query('SELECT is_active FROM academic_years WHERE id = ?', [yearId]);
if (year.length > 0 && year[0].is_active) {
return res.status(400).json({ success: false, message: 'Tidak dapat menghapus tahun ajaran yang sedang aktif' });
}
await pool.query('DELETE FROM academic_years WHERE id = ?', [yearId]);
res.json({ success: true, message: 'Tahun ajaran berhasil dihapus' });
} catch (error) {
console.error('Error deleting year:', error);
res.status(500).json({ success: false, message: 'Gagal menghapus tahun ajaran' });
}
});
// ==================== USER MANAGEMENT API ====================
// Get all users
app.get('/api/users', async (req, res) => {
try {
const [rows] = await pool.query('SELECT id, username, full_name, role, created_at FROM users ORDER BY full_name ASC');
res.json(rows.map(convertKeysToCamel));
} catch (error) {
console.error('Error fetching users:', error);
res.status(500).json({ success: false, message: 'Gagal mengambil data pengguna' });
}
});
// Add new user
app.post('/api/users', async (req, res) => {
try {
const { username, password, fullName, role } = req.body;
if (!username || !password || !fullName || !role) {
return res.status(400).json({ success: false, message: 'Semua data harus diisi' });
}
const hashedPassword = await bcrypt.hash(password, 10);
await pool.query(
'INSERT INTO users (username, password, full_name, role) VALUES (?, ?, ?, ?)',
[username, hashedPassword, fullName, role]
);
res.json({ success: true, message: 'Pengguna berhasil ditambahkan' });
} catch (error) {
console.error('Error adding user:', error);
if (error.code === 'ER_DUP_ENTRY') {
res.status(400).json({ success: false, message: 'Username sudah digunakan' });
} else {
res.status(500).json({ success: false, message: 'Gagal menambah pengguna' });
}
}
});
// Update user
app.put('/api/users/:id', async (req, res) => {
try {
const userId = req.params.id;
const { username, password, fullName, role } = req.body;
if (!username || !fullName || !role) {
return res.status(400).json({ success: false, message: 'Data nama, username dan role harus diisi' });
}
// Check if username exists for OTHER users
const [existing] = await pool.query('SELECT id FROM users WHERE username = ? AND id != ?', [username, userId]);
if (existing.length > 0) {
return res.status(400).json({ success: false, message: 'Username sudah digunakan oleh pengguna lain' });
}
if (password && password.trim() !== '') {
// Update with password
const hashedPassword = await bcrypt.hash(password, 10);
await pool.query(
'UPDATE users SET username = ?, password = ?, full_name = ?, role = ? WHERE id = ?',
[username, hashedPassword, fullName, role, userId]
);
} else {
// Update without password
await pool.query(
'UPDATE users SET username = ?, full_name = ?, role = ? WHERE id = ?',
[username, fullName, role, userId]
);
}
res.json({ success: true, message: 'Data pengguna berhasil diperbarui' });
} catch (error) {
console.error('Error updating user:', error);
res.status(500).json({ success: false, message: 'Gagal memperbarui pengguna' });
}
});
// Delete user
app.delete('/api/users/:id', async (req, res) => {
try {
const userId = req.params.id;
// Prevent deleting the last admin or the currently logged in user (would need a better check but for now simpler)
const [userRows] = await pool.query('SELECT username FROM users WHERE id = ?', [userId]);
if (userRows.length > 0 && userRows[0].username === 'Kesiswaan') {
return res.status(400).json({ success: false, message: 'Account utama tidak dapat dihapus' });
}
await pool.query('DELETE FROM users WHERE id = ?', [userId]);
res.json({ success: true, message: 'Pengguna berhasil dihapus' });
} catch (error) {
console.error('Error deleting user:', error);
res.status(500).json({ success: false, message: 'Gagal menghapus pengguna' });
}
});
// ==================== STATIC FILES ====================
// Serve static files from the dist directory
app.use(express.static(path.join(__dirname, 'dist')));
// Handle SPA routing
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});
// ==================== START SERVER ====================
const startServer = async () => {
try {
// Initialize database
await initDatabase();
app.listen(PORT, '0.0.0.0', () => {
console.log(`\n🚀 Server is running on http://localhost:${PORT}`);
console.log(`📦 Database: ${process.env.DB_NAME || 'db_bukuiduk'}`);
console.log(`🌐 Environment: Node.js ${process.version}`);
});
} catch (error) {
console.error('❌ Failed to start server:', error);
process.exit(1);
}
};
startServer();