238 lines
8.0 KiB
JavaScript
238 lines
8.0 KiB
JavaScript
import express from 'express';
|
|
import cors from 'cors';
|
|
import path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
import dotenv from 'dotenv';
|
|
import multer from 'multer';
|
|
import fs from 'fs';
|
|
|
|
// Load environment variables
|
|
dotenv.config();
|
|
|
|
// Import database and routes
|
|
import pool, { initDatabase } from './database.js';
|
|
import studentsRouter from './routes/students.js';
|
|
import violationsRouter from './routes/violations.js';
|
|
import achievementsRouter from './routes/achievements.js';
|
|
import settingsRouter from './routes/settings.js';
|
|
import usersRouter from './routes/users.js';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 3007;
|
|
|
|
// Middleware
|
|
app.use(cors());
|
|
app.use(express.json({ limit: '10mb' }));
|
|
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
|
|
|
// Static files - serve from dist folder (built frontend)
|
|
app.use(express.static(path.join(__dirname, '../dist')));
|
|
|
|
// Also serve public folder for images
|
|
app.use('/images', express.static(path.join(__dirname, '../public/images')));
|
|
|
|
// File upload configuration
|
|
const uploadDir = path.join(__dirname, '../uploads');
|
|
if (!fs.existsSync(uploadDir)) {
|
|
fs.mkdirSync(uploadDir, { recursive: true });
|
|
}
|
|
|
|
const storage = multer.diskStorage({
|
|
destination: (req, file, cb) => {
|
|
// Create monthly folder
|
|
const date = new Date();
|
|
const monthNames = ['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni',
|
|
'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'];
|
|
const monthFolder = `${monthNames[date.getMonth()]} ${date.getFullYear()}`;
|
|
const targetDir = path.join(uploadDir, monthFolder);
|
|
|
|
if (!fs.existsSync(targetDir)) {
|
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
}
|
|
cb(null, targetDir);
|
|
},
|
|
filename: (req, file, cb) => {
|
|
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
|
const ext = path.extname(file.originalname);
|
|
cb(null, `violation-${uniqueSuffix}${ext}`);
|
|
}
|
|
});
|
|
|
|
const upload = multer({
|
|
storage,
|
|
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB limit
|
|
fileFilter: (req, file, cb) => {
|
|
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
|
if (allowedTypes.includes(file.mimetype)) {
|
|
cb(null, true);
|
|
} else {
|
|
cb(new Error('Invalid file type. Only JPEG, PNG, GIF, and WebP are allowed.'));
|
|
}
|
|
}
|
|
});
|
|
|
|
// Serve uploaded files
|
|
app.use('/uploads', express.static(uploadDir));
|
|
|
|
// API Routes
|
|
app.use('/api/students', studentsRouter);
|
|
app.use('/api/violations', violationsRouter);
|
|
app.use('/api/achievements', achievementsRouter);
|
|
app.use('/api/settings', settingsRouter);
|
|
app.use('/api/users', usersRouter);
|
|
|
|
// File upload endpoint
|
|
app.post('/api/upload', upload.single('photo'), (req, res) => {
|
|
try {
|
|
if (!req.file) {
|
|
return res.status(400).json({ error: 'No file uploaded' });
|
|
}
|
|
|
|
// Build the relative path for the uploaded file
|
|
const relativePath = req.file.path.replace(uploadDir, '').replace(/\\/g, '/');
|
|
const photoUrl = `/uploads${relativePath}`;
|
|
|
|
res.json({
|
|
status: 'success',
|
|
photoUrl,
|
|
filename: req.file.filename
|
|
});
|
|
} catch (error) {
|
|
console.error('Upload error:', error);
|
|
res.status(500).json({ error: 'Failed to upload file' });
|
|
}
|
|
});
|
|
|
|
// Base64 image upload (for compatibility with existing frontend)
|
|
app.post('/api/upload-base64', async (req, res) => {
|
|
try {
|
|
const { photoBase64, violationId } = req.body;
|
|
|
|
if (!photoBase64) {
|
|
return res.status(400).json({ error: 'No image data provided' });
|
|
}
|
|
|
|
// Extract base64 data
|
|
const matches = photoBase64.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);
|
|
if (!matches || matches.length !== 3) {
|
|
return res.status(400).json({ error: 'Invalid base64 image format' });
|
|
}
|
|
|
|
const type = matches[1];
|
|
const data = matches[2];
|
|
const buffer = Buffer.from(data, 'base64');
|
|
|
|
// Determine file extension
|
|
let ext = '.jpg';
|
|
if (type.includes('png')) ext = '.png';
|
|
else if (type.includes('gif')) ext = '.gif';
|
|
else if (type.includes('webp')) ext = '.webp';
|
|
|
|
// Create monthly folder
|
|
const date = new Date();
|
|
const monthNames = ['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni',
|
|
'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'];
|
|
const monthFolder = `${monthNames[date.getMonth()]} ${date.getFullYear()}`;
|
|
const targetDir = path.join(uploadDir, monthFolder);
|
|
|
|
if (!fs.existsSync(targetDir)) {
|
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
}
|
|
|
|
// Generate filename
|
|
const filename = `violation-${violationId || Date.now()}${ext}`;
|
|
const filePath = path.join(targetDir, filename);
|
|
|
|
// Write file
|
|
fs.writeFileSync(filePath, buffer);
|
|
|
|
// Build URL
|
|
const photoUrl = `/uploads/${monthFolder}/${filename}`;
|
|
|
|
res.json({
|
|
status: 'success',
|
|
photoUrl,
|
|
filename
|
|
});
|
|
} catch (error) {
|
|
console.error('Base64 upload error:', error);
|
|
res.status(500).json({ error: 'Failed to save image' });
|
|
}
|
|
});
|
|
|
|
// Health check endpoint with DB verification
|
|
app.get('/api/health', async (req, res) => {
|
|
try {
|
|
// Test database connection
|
|
const [rows] = await pool.execute('SELECT 1 as connection_test');
|
|
|
|
res.json({
|
|
status: 'ok',
|
|
server: 'SIPASI API Server',
|
|
version: '2.0.0',
|
|
database: {
|
|
status: 'connected',
|
|
type: 'MySQL',
|
|
ping: 'ok'
|
|
},
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
status: 'error',
|
|
server: 'SIPASI API Server',
|
|
database: {
|
|
status: 'disconnected',
|
|
error: error.message
|
|
},
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
}
|
|
});
|
|
|
|
// SPA fallback - serve index.html for all non-API routes
|
|
app.use((req, res, next) => {
|
|
// Skip API routes
|
|
if (req.path.startsWith('/api')) {
|
|
return next();
|
|
}
|
|
// Serve index.html for client-side routing
|
|
res.sendFile(path.join(__dirname, '../dist/index.html'));
|
|
});
|
|
|
|
// Error handling middleware
|
|
app.use((err, req, res, next) => {
|
|
console.error('Server error:', err);
|
|
res.status(500).json({ error: err.message || 'Internal server error' });
|
|
});
|
|
|
|
// Initialize database and start server
|
|
async function startServer() {
|
|
try {
|
|
await initDatabase();
|
|
|
|
app.listen(PORT, () => {
|
|
console.log(`
|
|
╔════════════════════════════════════════════════════╗
|
|
║ ║
|
|
║ 🏫 SIPASI - Sistem Informasi Pelanggaran Siswa ║
|
|
║ SMA Negeri 1 Abiansemal ║
|
|
║ ║
|
|
║ ✅ Server running on port ${PORT} ║
|
|
║ 📡 API: http://localhost:${PORT}/api ║
|
|
║ 🌐 Web: http://localhost:${PORT} ║
|
|
║ ║
|
|
╚════════════════════════════════════════════════════╝
|
|
`);
|
|
});
|
|
} catch (error) {
|
|
console.error('❌ Failed to start server:', error);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
startServer();
|