Gift / server.js
Mubspro54's picture
Upload server.js
9ac3c47 verified
Raw
History Blame Contribute Delete
57.4 kB
/**
* ============================================================
* Kawanda Secondary School β€” Backend Server
* Platform : Hugging Face Spaces (port 7860)
* Database : Supabase
* ============================================================
*
* REQUIRED ENVIRONMENT VARIABLES (set in HF Space Settings β†’ Variables):
* SUPABASE_URL β€” e.g. https://xxxx.supabase.co
* SUPABASE_KEY β€” your Supabase service-role key (starts with eyJ...)
* JWT_SECRET β€” any long random string, e.g. KawandaSS2026Secret
*
* ============================================================
*/
'use strict';
const express = require('express');
const cors = require('cors');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const { createClient } = require('@supabase/supabase-js');
/* ─────────────────────────────────────────────
ENV β€” crash early with clear message if missing
───────────────────────────────────────────── */
const SUPABASE_URL = process.env.SUPABASE_URL;
const SUPABASE_KEY = process.env.SUPABASE_KEY;
const JWT_SECRET = process.env.JWT_SECRET;
const PORT = process.env.PORT || 7860;
/* Warn clearly if DB env vars are missing β€” but do NOT crash.
The server must stay alive so HF Space doesn't show an HTML error page. */
if (!SUPABASE_URL || !SUPABASE_KEY) {
console.error('\n⚠️ WARNING: SUPABASE_URL and SUPABASE_KEY are not set.');
console.error(' Go to HF Space β†’ Settings β†’ Variables and secrets β†’ add them there.');
console.error(' API calls will fail until they are set.\n');
}
/* ─────────────────────────────────────────────
SUPABASE CLIENT
───────────────────────────────────────────── */
const db = createClient(SUPABASE_URL, SUPABASE_KEY);
/* ─────────────────────────────────────────────
EXPRESS SETUP
───────────────────────────────────────────── */
const app = express();
/* CORS β€” allow all origins (Netlify frontend can call this) */
app.use(cors({
origin: '*',
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));
app.options('*', cors()); /* handle preflight for all routes */
app.use(express.json({ limit: '1mb' }));
/* ─────────────────────────────────────────────
HELPERS
───────────────────────────────────────────── */
/* Strip HTML tags to prevent XSS */
function sanitize(s) {
if (typeof s !== 'string') return s;
return s.replace(/<[^>]*>/g, '').trim().slice(0, 500);
}
/* Sign a JWT token */
function makeToken(payload) {
return jwt.sign(payload, JWT_SECRET, { expiresIn: '7d' });
}
/* Middleware: require a valid JWT */
function requireAuth(req, res, next) {
try {
const header = req.headers.authorization || '';
const token = header.startsWith('Bearer ') ? header.slice(7) : header;
req.user = jwt.verify(token, JWT_SECRET);
next();
} catch {
res.status(401).json({ error: 'Not logged in. Please sign in again.' });
}
}
/* Middleware: require admin role */
function requireAdmin(req, res, next) {
if (req.user && req.user.role === 'admin') return next();
res.status(403).json({ error: 'Admin access required.' });
}
/* Middleware: require admin or teacher */
function requireStaff(req, res, next) {
if (req.user && (req.user.role === 'admin' || req.user.role === 'teacher')) return next();
res.status(403).json({ error: 'Staff access required.' });
}
/* Generate teacher login ID: KDSS{nnn}
e.g. KDSS001, KDSS002, KDSS003 */
async function makeTeacherLoginId() {
/* count existing teachers to get next number */
const { count } = await db
.from('users')
.select('*', { count: 'exact', head: true })
.eq('role', 'teacher');
const next = (count || 0) + 1;
/* try up to 20 slots to avoid collision */
for (let i = next; i < next + 20; i++) {
const id = 'KDSS' + String(i).padStart(3, '0');
const { data } = await db.from('users').select('login_id').eq('login_id', id).single();
if (!data) return id; /* slot is free */
}
/* fallback β€” extremely unlikely */
return 'KDSS' + Date.now().toString().slice(-5);
}
/* Generate student login ID: KDSS{nn}{year}
e.g. KDSS012026, KDSS022026, KDSS032026 */
async function makeStudentLoginId(cls) {
const year = new Date().getFullYear();
const prefix = 'KDSS';
const suffix = String(year);
/* Find highest existing student ID for this year */
const { data } = await db
.from('users')
.select('login_id')
.eq('role', 'student')
.like('login_id', prefix + '%' + suffix)
.order('login_id', { ascending: false })
.limit(1);
let next = 1;
if (data && data.length > 0) {
const mid = data[0].login_id.replace(prefix, '').replace(suffix, '');
const n = parseInt(mid, 10);
if (!isNaN(n)) next = n + 1;
}
/* Retry up to 20 slots to avoid collision */
for (let attempt = 0; attempt < 20; attempt++) {
const id = prefix + String(next + attempt).padStart(2, '0') + suffix;
const { data: existing } = await db.from('users').select('id').eq('login_id', id).single();
if (!existing) return id;
}
/* Fallback */
return prefix + Date.now().toString().slice(-4) + suffix;
}
/* ─────────────────────────────────────────────
SEED ADMIN ON STARTUP
Creates admin account "Kawanda795 / Kawanda795#"
in Supabase if it does not already exist.
───────────────────────────────────────────── */
async function seedAdmin() {
console.log('πŸ” Checking admin account...');
try {
const { data: existing } = await db
.from('users')
.select('id, login_id')
.eq('login_id', 'Kawanda795')
.single();
if (existing) {
console.log('βœ… Admin account already exists: Kawanda795');
return;
}
/* Admin not found β€” create it */
const hash = await bcrypt.hash('Kawanda795#', 12);
const { error } = await db.from('users').insert({
login_id: 'Kawanda795',
password_hash: hash,
role: 'admin',
full_name: 'Administrator',
status: 'active',
});
if (error) {
console.error('❌ Failed to create admin:', error.message);
console.error(' Make sure your SUPABASE_KEY has INSERT permission on the users table.');
} else {
console.log('βœ… Admin account created in Supabase: Kawanda795 / Kawanda795#');
}
} catch (err) {
console.error('❌ seedAdmin error:', err.message);
console.error(' This usually means SUPABASE_URL or SUPABASE_KEY is wrong.');
}
}
/* ─────────────────────────────────────────────
ROUTES
───────────────────────────────────────────── */
/* ── Health check (used by frontend to detect if server is awake) ── */
app.get('/api/health', (req, res) => {
res.json({
ok: true,
school: 'Kawanda Secondary School',
version: '3.0',
time: new Date().toISOString(),
});
});
/* ── AUTH: Login ── */
app.post('/api/auth/login', async (req, res) => {
const loginId = sanitize(req.body.loginId || '');
const password = String(req.body.password || '');
if (!loginId || !password) {
return res.status(400).json({ error: 'Login ID and password are required.' });
}
if (password.length > 200) {
return res.status(400).json({ error: 'Invalid input.' });
}
/* Fetch user from DB */
const { data: user, error: dbErr } = await db
.from('users')
.select('*')
.eq('login_id', loginId)
.single();
if (dbErr || !user) {
/* Always hash to prevent timing attacks */
await bcrypt.hash('timing_protection', 10);
return res.status(401).json({ error: 'Incorrect login ID or password.' });
}
if (user.status === 'suspended') {
return res.status(401).json({ error: 'Account suspended. Contact the school office.' });
}
const passwordMatch = await bcrypt.compare(password, user.password_hash);
if (!passwordMatch) {
return res.status(401).json({ error: 'Incorrect login ID or password.' });
}
/* Build token payload */
const classes = user.classes || (user.class ? [user.class] : []);
const payload = {
id: user.id,
loginId: user.login_id,
role: user.role,
subRole: user.sub_role || null,
permissions: user.permissions || null,
classTchrOf: user.class_teacher_of || null,
name: user.full_name,
subject: user.subject || null,
cls: user.class || null,
classes: classes,
childName: user.child_name || null,
childCls: user.child_class || null,
childLogin: user.child_login || null,
};
res.json({
token: makeToken(payload),
role: user.role,
subRole: user.sub_role || null,
name: user.full_name,
subject: user.subject || null,
cls: user.class || null,
classes: classes,
childName: user.child_name || null,
childCls: user.child_class || null,
});
});
/* ── AUTH: Teacher self-registration (goes to pending approval) ── */
app.post('/api/auth/register/teacher', async (req, res) => {
const name = sanitize(req.body.name || '');
const subject = sanitize(req.body.subject || '');
const stream = sanitize(req.body.stream || '');
const classes = Array.isArray(req.body.classes) ? req.body.classes : (req.body.classes ? [req.body.classes] : []);
const password = String(req.body.password || '');
if (!name || !subject || !classes.length || !password) {
return res.status(400).json({ error: 'Name, subject, at least one class, and password are required.' });
}
if (password.length < 6) {
return res.status(400).json({ error: 'Password must be at least 6 characters.' });
}
const proposedLogin = await makeTeacherLoginId();
const hash = await bcrypt.hash(password, 12);
const { error } = await db.from('teacher_requests').insert({
full_name: name,
subject: subject,
class: classes[0],
classes: classes,
stream: stream || null,
proposed_login: proposedLogin,
password_hash: hash,
status: 'pending',
});
if (error) return res.status(500).json({ error: 'Could not submit request: ' + error.message });
res.json({
success: true,
proposedLogin: proposedLogin,
message: 'Request submitted! Your login ID will be ' + proposedLogin + '. An admin will approve it shortly.',
});
});
/* ── AUTH: Parent self-registration ── */
app.post('/api/auth/register/parent', async (req, res) => {
const email = sanitize(req.body.email || '');
const fullName = sanitize(req.body.fullName || '');
const phone = sanitize(req.body.phone || '');
const password = String(req.body.password || '');
if (!email || !fullName || !password) {
return res.status(400).json({ error: 'Email, name and password are required.' });
}
if (password.length < 6) {
return res.status(400).json({ error: 'Password must be at least 6 characters.' });
}
const { data: existing } = await db.from('users').select('login_id').eq('login_id', email).single();
if (existing) {
return res.status(409).json({ error: 'An account with this email already exists. Try signing in.' });
}
const hash = await bcrypt.hash(password, 12);
const { error } = await db.from('users').insert({
login_id: email,
password_hash: hash,
role: 'parent',
full_name: fullName,
parent_phone: phone || null,
status: 'active',
});
if (error) return res.status(500).json({ error: error.message });
res.json({ success: true, message: 'Account created. You can now sign in with your email and password.' });
});
/* ── STUDENTS: list ── */
app.get('/api/students', requireAuth, requireAdmin, async (req, res) => {
const { data, error } = await db
.from('users')
.select('id, login_id, full_name, class, parent_name, parent_phone, parent_email, boarding_type, status, created_at')
.eq('role', 'student')
.order('class').order('full_name');
if (error) return res.status(500).json({ error: error.message });
res.json(data || []);
});
/* ── STUDENTS: create ── */
app.post('/api/students', requireAuth, requireAdmin, async (req, res) => {
const { fullName, cls, parentName, parentPhone, parentEmail, boardingType, password } = req.body;
if (!fullName || !cls || !password) {
return res.status(400).json({ error: 'Name, class and password are required.' });
}
const loginId = await makeStudentLoginId(cls);
const hash = await bcrypt.hash(password, 12);
const { error } = await db.from('users').insert({
login_id: loginId,
password_hash: hash,
role: 'student',
full_name: sanitize(fullName),
class: cls,
parent_name: sanitize(parentName || ''),
parent_phone: parentPhone || null,
parent_email: parentEmail || null,
boarding_type: boardingType || null,
status: 'active',
});
if (error) return res.status(500).json({ error: error.message });
res.json({ success: true, loginId, message: 'Student created. Login ID: ' + loginId });
});
/* ── STUDENTS: update ── */
app.patch('/api/students/:loginId', requireAuth, requireAdmin, async (req, res) => {
const updates = {};
if (req.body.password) updates.password_hash = await bcrypt.hash(req.body.password, 12);
if (req.body.status) updates.status = req.body.status;
if (req.body.cls) updates.class = req.body.cls;
if (!Object.keys(updates).length) return res.status(400).json({ error: 'Nothing to update.' });
await db.from('users').update(updates).eq('login_id', req.params.loginId).eq('role', 'student');
res.json({ success: true });
});
/* ── STUDENTS: delete (suspend) ── */
app.delete('/api/students/:loginId', requireAuth, requireAdmin, async (req, res) => {
await db.from('users').delete().eq('login_id', req.params.loginId).eq('role', 'student');
res.json({ success: true });
});
/* ── TEACHERS: list ── */
app.get('/api/teachers', requireAuth, requireAdmin, async (req, res) => {
const { data, error } = await db
.from('users')
.select('id, login_id, full_name, subject, class, classes, class_teacher_of, stream, status, created_at')
.eq('role', 'teacher')
.order('login_id');
if (error) return res.status(500).json({ error: error.message });
res.json((data || []).map(t => ({
...t,
classes: t.classes || (t.class ? [t.class] : []),
})));
});
/* ── TEACHERS: create (by admin) ── */
app.post('/api/teachers', requireAuth, requireAdmin, async (req, res) => {
const { name, subject, cls, classes, password } = req.body;
const classList = Array.isArray(classes) && classes.length ? classes : (cls ? [cls] : []);
if (!name || !subject || !classList.length || !password) {
return res.status(400).json({ error: 'Name, subject, at least one class, and password are required.' });
}
const loginId = await makeTeacherLoginId();
const hash = await bcrypt.hash(password, 12);
const { error } = await db.from('users').insert({
login_id: loginId,
password_hash: hash,
role: 'teacher',
full_name: sanitize(name),
subject: subject,
class: classList[0],
classes: classList,
status: 'active',
});
if (error) return res.status(500).json({ error: error.message });
res.json({ success: true, loginId, classes: classList });
});
/* ── TEACHERS: update ── */
app.patch('/api/teachers/:loginId', requireAuth, requireAdmin, async (req, res) => {
const updates = {};
if (req.body.password) updates.password_hash = await bcrypt.hash(req.body.password, 12);
if (req.body.status) updates.status = req.body.status;
if (req.body.classes && req.body.classes.length) {
updates.classes = req.body.classes;
updates.class = req.body.classes[0];
}
if (req.body.classTchrOf !== undefined) updates.class_teacher_of = req.body.classTchrOf || null;
if (!Object.keys(updates).length) return res.status(400).json({ error: 'Nothing to update.' });
await db.from('users').update(updates).eq('login_id', req.params.loginId).eq('role', 'teacher');
res.json({ success: true });
});
/* ── TEACHERS: delete ── */
app.delete('/api/teachers/:loginId', requireAuth, requireAdmin, async (req, res) => {
await db.from('users').delete().eq('login_id', req.params.loginId).eq('role', 'teacher');
res.json({ success: true });
});
/* ── TEACHER REQUESTS: list pending ── */
app.get('/api/teacher-requests', requireAuth, requireAdmin, async (req, res) => {
const { data } = await db
.from('teacher_requests')
.select('*')
.eq('status', 'pending')
.order('submitted_at', { ascending: false });
res.json(data || []);
});
/* ── TEACHER REQUESTS: approve or reject ── */
app.patch('/api/teacher-requests/:id', requireAuth, requireAdmin, async (req, res) => {
const { status } = req.body;
const { data: reqRow } = await db.from('teacher_requests').select('*').eq('id', req.params.id).single();
if (!reqRow) return res.status(404).json({ error: 'Request not found.' });
if (status === 'approved') {
const classList = reqRow.classes || [reqRow.class];
const { data: existing } = await db.from('users').select('login_id').eq('login_id', reqRow.proposed_login).single();
if (!existing) {
await db.from('users').insert({
login_id: reqRow.proposed_login,
password_hash: reqRow.password_hash,
role: 'teacher',
full_name: reqRow.full_name,
subject: reqRow.subject,
class: classList[0],
classes: classList,
stream: reqRow.stream || null,
status: 'active',
});
}
}
await db.from('teacher_requests').update({
status,
reviewed_at: new Date().toISOString(),
reviewed_by: req.user.id,
}).eq('id', req.params.id);
res.json({ success: true, loginId: reqRow.proposed_login });
});
/* ── FEES: list ── */
app.get('/api/fees', requireAuth, async (req, res) => {
let q = db.from('fee_payments').select('*').order('created_at', { ascending: false });
if (req.query.student) q = q.ilike('student_name', '%' + req.query.student + '%');
if (req.query.class) q = q.eq('class', req.query.class);
if (req.query.term) q = q.eq('term', req.query.term);
if (req.query.status) q = q.eq('status', req.query.status);
if (req.query.student) q = q.ilike('student_name', '%' + req.query.student + '%');
const { data, error } = await q;
if (error) return res.status(500).json({ error: error.message });
res.json(data || []);
});
/* ── FEES: record ── */
app.post('/api/fees', requireAuth, async (req, res) => {
const { studentName, parentName, cls, term, year, amount, method, status, date } = req.body;
if (!studentName || !cls || !amount) {
return res.status(400).json({ error: 'Student name, class and amount are required.' });
}
const { data, error } = await db.from('fee_payments').insert({
id: 'FEE-' + Date.now(),
student_name: sanitize(studentName),
parent_name: sanitize(parentName || ''),
class: cls,
term: term || null,
year: year || String(new Date().getFullYear()),
amount: parseInt(amount),
method: method || 'Cash',
status: status || 'paid',
payment_date: date || new Date().toISOString().split('T')[0],
}).select().single();
if (error) return res.status(500).json({ error: error.message });
res.json(data);
});
/* ── FEES: delete ── */
app.delete('/api/fees/:id', requireAuth, requireAdmin, async (req, res) => {
await db.from('fee_payments').delete().eq('id', req.params.id);
res.json({ success: true });
});
/* ── PROGRAMME: list ── */
app.get('/api/programme', async (req, res) => {
const { data } = await db.from('programme').select('*').order('date');
res.json(data || []);
});
/* ── PROGRAMME: create ── */
app.post('/api/programme', requireAuth, requireAdmin, async (req, res) => {
const { data, error } = await db.from('programme').insert({
date: req.body.date,
time: req.body.time || null,
title: sanitize(req.body.title || ''),
type: req.body.type || 'academic',
classes: req.body.classes || 'All',
description: sanitize(req.body.description || ''),
}).select().single();
if (error) return res.status(500).json({ error: error.message });
res.json(data);
});
/* ── PROGRAMME: update ── */
app.put('/api/programme/:id', requireAuth, requireAdmin, async (req, res) => {
const { data, error } = await db.from('programme').update({
date: req.body.date,
time: req.body.time || null,
title: sanitize(req.body.title || ''),
type: req.body.type || 'academic',
classes: req.body.classes || 'All',
description: sanitize(req.body.description || ''),
}).eq('id', req.params.id).select().single();
if (error) return res.status(500).json({ error: error.message });
res.json(data);
});
/* ── PROGRAMME: delete ── */
app.delete('/api/programme/:id', requireAuth, requireAdmin, async (req, res) => {
await db.from('programme').delete().eq('id', req.params.id);
res.json({ success: true });
});
/* ── ANNOUNCEMENTS: list ── */
app.get('/api/announcements', requireAuth, async (req, res) => {
let q = db.from('announcements').select('*').order('created_at', { ascending: false });
if (req.query.audience && req.query.audience !== 'All') {
q = q.in('audience', ['All', req.query.audience]);
}
const { data } = await q;
res.json(data || []);
});
/* ── ANNOUNCEMENTS: create ── */
app.post('/api/announcements', requireAuth, requireStaff, async (req, res) => {
if (!req.body.title || !req.body.message) {
return res.status(400).json({ error: 'Title and message are required.' });
}
const { data, error } = await db.from('announcements').insert({
title: sanitize(req.body.title),
message: sanitize(req.body.message),
audience: req.body.audience || 'All',
urgent: !!req.body.urgent,
sender_name: req.user.name,
sender_id: req.user.id,
}).select().single();
if (error) return res.status(500).json({ error: error.message });
res.json(data);
});
/* ── ANNOUNCEMENTS: delete ── */
app.delete('/api/announcements/:id', requireAuth, requireAdmin, async (req, res) => {
await db.from('announcements').delete().eq('id', req.params.id);
res.json({ success: true });
});
/* ── TIMETABLE: get by class ── */
app.get('/api/timetable/:cls', requireAuth, async (req, res) => {
const { data } = await db.from('timetables').select('*').eq('class', req.params.cls);
res.json(data || []);
});
/* ── TIMETABLE: set cell ── */
app.put('/api/timetable', requireAuth, requireStaff, async (req, res) => {
const { cls, dayIndex, periodIndex, subject } = req.body;
const { data, error } = await db.from('timetables').upsert({
class: cls,
day_index: dayIndex,
period_index: periodIndex,
subject: sanitize(subject || ''),
}, { onConflict: 'class,day_index,period_index' }).select().single();
if (error) return res.status(500).json({ error: error.message });
res.json(data);
});
/* ── RESOURCES: list ── */
app.get('/api/resources', requireAuth, async (req, res) => {
let q = db.from('resources').select('*').order('created_at', { ascending: false });
if (req.user.role === 'student') {
q = q.eq('class', req.user.cls);
} else {
if (req.query.class) q = q.eq('class', req.query.class);
if (req.query.subject) q = q.eq('subject', req.query.subject);
if (req.query.teacher) q = q.eq('teacher_name', req.query.teacher);
}
const { data, error } = await q;
if (error) return res.status(500).json({ error: error.message });
res.json(data || []);
});
/* ── RESOURCES: upload ── */
app.post('/api/resources', requireAuth, requireStaff, async (req, res) => {
const { title, subject, cls, description, fileUrl, fileType } = req.body;
if (!title || !subject || !cls || !fileUrl) {
return res.status(400).json({ error: 'Title, subject, class and file link are required.' });
}
const { data, error } = await db.from('resources').insert({
id: 'RES-' + Date.now(),
title: sanitize(title),
subject: subject,
class: cls,
description: sanitize(description || ''),
file_url: fileUrl,
file_type: fileType || 'other',
teacher_name: req.user.name,
teacher_id: req.user.id,
}).select().single();
if (error) return res.status(500).json({ error: error.message });
res.json(data);
});
/* ── RESOURCES: delete ── */
app.delete('/api/resources/:id', requireAuth, requireStaff, async (req, res) => {
let q = db.from('resources').delete().eq('id', req.params.id);
if (req.user.role === 'teacher') q = q.eq('teacher_id', req.user.id);
await q;
res.json({ success: true });
});
/* ── ADMISSIONS: submit application ── */
app.post('/api/admissions', async (req, res) => {
const { firstName, lastName, programme } = req.body;
if (!firstName || !lastName || !programme) {
return res.status(400).json({ error: 'First name, last name and programme are required.' });
}
const reference = 'KSS-' + new Date().getFullYear() + '-' + Math.random().toString(36).slice(2, 8).toUpperCase();
const { error } = await db.from('admission_applications').insert({
id: 'APP-' + Date.now(),
reference: reference,
first_name: sanitize(firstName),
last_name: sanitize(lastName),
date_of_birth: req.body.dob || null,
gender: req.body.gender || null,
programme: programme,
boarding_type: req.body.boardingType || null,
guardian_name: sanitize(req.body.guardianName || ''),
guardian_phone: req.body.guardianPhone || null,
guardian_email: req.body.guardianEmail || null,
previous_school: sanitize(req.body.prevSchool || ''),
subjects_data: req.body.subjects || {},
});
if (error) return res.status(500).json({ error: error.message });
res.json({ success: true, reference, message: 'Application submitted! Reference: ' + reference });
});
/* ── ADMISSIONS: list (admin only) ── */
app.get('/api/admissions', requireAuth, requireAdmin, async (req, res) => {
let q = db.from('admission_applications').select('*').order('submitted_at', { ascending: false });
if (req.query.status) q = q.eq('status', req.query.status);
const { data } = await q;
res.json(data || []);
});
/* ── ADMISSIONS: view one ── */
app.get('/api/admissions/:id', requireAuth, requireAdmin, async (req, res) => {
const { data } = await db.from('admission_applications').select('*').eq('id', req.params.id).single();
if (!data) return res.status(404).json({ error: 'Application not found.' });
res.json(data);
});
/* ── ADMISSIONS: update status (accept/reject) ── */
app.patch('/api/admissions/:id', requireAuth, requireAdmin, async (req, res) => {
const { status, adminNotes } = req.body;
const { data: application } = await db.from('admission_applications').select('*').eq('id', req.params.id).single();
if (!application) return res.status(404).json({ error: 'Application not found.' });
let generatedLogin = null;
if (status === 'accepted') {
/* Determine class from programme */
const classMap = { s1: 'S.1', s2: 'S.2', s3: 'S.3', s4: 'S.4', s5: 'S.5', s6: 'S.6', transfer: 'S.1' };
const cls = classMap[application.programme] || 'S.1';
generatedLogin = await makeStudentLoginId(cls);
/* Generate a simple password */
const pw = 'Kss' + Math.floor(1000 + Math.random() * 9000);
const hash = await bcrypt.hash(pw, 12);
await db.from('users').insert({
login_id: generatedLogin,
password_hash: hash,
role: 'student',
full_name: application.first_name + ' ' + application.last_name,
class: cls,
parent_name: application.guardian_name || null,
parent_phone: application.guardian_phone || null,
parent_email: application.guardian_email || null,
status: 'active',
});
await db.from('admission_applications').update({
status,
admin_notes: adminNotes || null,
generated_login: generatedLogin,
reviewed_at: new Date().toISOString(),
}).eq('id', req.params.id);
} else {
await db.from('admission_applications').update({
status,
admin_notes: adminNotes || null,
reviewed_at: new Date().toISOString(),
}).eq('id', req.params.id);
}
res.json({ success: true, generatedLogin });
});
app.delete('/api/admissions/:id', requireAuth, requireAdmin, async (req, res) => {
const { error } = await db.from('admission_applications').delete().eq('id', req.params.id);
if (error) return res.status(500).json({ error: error.message });
res.json({ success: true });
});
/* ── ADMISSION CONFIG ── */
app.get('/api/admission-config', async (req, res) => {
const { data } = await db.from('admission_config').select('*');
const config = {};
(data || []).forEach(row => { config[row.key] = row.value; });
res.json(config);
});
app.put('/api/admission-config', requireAuth, requireAdmin, async (req, res) => {
const rows = Object.entries(req.body).map(([key, value]) => ({ key, value: String(value) }));
const { error } = await db.from('admission_config').upsert(rows, { onConflict: 'key' });
if (error) return res.status(500).json({ error: error.message });
res.json({ success: true });
});
/* ── HOMEPAGE IMAGES ── */
app.get('/api/homepage-images', async (req, res) => {
const { data } = await db.from('homepage_images').select('*').eq('active', true).order('sort_order').order('created_at');
res.json(data || []);
});
app.post('/api/homepage-images', requireAuth, requireAdmin, async (req, res) => {
const { title, imageUrl, sortOrder } = req.body;
if (!imageUrl) return res.status(400).json({ error: 'Image URL is required.' });
const { data, error } = await db.from('homepage_images').insert({
title: sanitize(title || ''),
image_url: imageUrl,
sort_order: sortOrder || 0,
active: true,
uploaded_by: req.user.id,
}).select().single();
if (error) return res.status(500).json({ error: error.message });
res.json(data);
});
app.delete('/api/homepage-images/:id', requireAuth, requireAdmin, async (req, res) => {
await db.from('homepage_images').delete().eq('id', req.params.id);
res.json({ success: true });
});
/* ── NOTIFICATIONS: list ── */
app.get('/api/notifications', requireAuth, async (req, res) => {
const { data } = await db
.from('notifications')
.select('*')
.eq('recipient_id', req.user.id)
.order('created_at', { ascending: false });
res.json(data || []);
});
/* ── NOTIFICATIONS: mark read ── */
app.patch('/api/notifications/read-all', requireAuth, async (req, res) => {
await db.from('notifications').update({ read: true }).eq('recipient_id', req.user.id);
res.json({ success: true });
});
app.patch('/api/notifications/:id/read', requireAuth, async (req, res) => {
await db.from('notifications').update({ read: true }).eq('id', req.params.id).eq('recipient_id', req.user.id);
res.json({ success: true });
});
/* ── RC BATCHES: class teacher submits a batch of report cards to admin ── */
app.post('/api/rc-batches', requireAuth, async (req, res) => {
const { cls, stream, term, year, note } = req.body;
if (!cls || !term || !year) return res.status(400).json({ error: 'cls, term and year required.' });
const { data, error } = await db.from('rc_batches').insert({
teacher_login: req.user.loginId, teacher_name: req.user.name,
class: cls, stream: stream || null, term, year, note: note || '', status: 'draft',
}).select().single();
if (error) return res.status(500).json({ error: error.message });
res.json(data);
});
app.get('/api/rc-batches', requireAuth, async (req, res) => {
let q = db.from('rc_batches').select('*').order('created_at', { ascending: false });
if (req.user.role === 'teacher') q = q.eq('teacher_login', req.user.loginId);
if (req.query.status) q = q.eq('status', req.query.status);
const { data, error } = await q;
if (error) return res.status(500).json({ error: error.message });
res.json(data || []);
});
app.patch('/api/rc-batches/:id', requireAuth, async (req, res) => {
const updates = {};
if (req.body.status) updates.status = req.body.status;
if (req.body.note !== undefined) updates.note = req.body.note;
const { error } = await db.from('rc_batches').update(updates).eq('id', req.params.id);
if (error) return res.status(500).json({ error: error.message });
if (req.body.status === 'approved') {
await db.from('report_cards').update({ status: 'published' }).eq('batch_id', req.params.id);
}
res.json({ success: true });
});
app.delete('/api/rc-batches/:id', requireAuth, requireAdmin, async (req, res) => {
await db.from('rc_batches').delete().eq('id', req.params.id);
res.json({ success: true });
});
/* ── RC ASSIGNMENTS: admin sends template request to class teacher ── */
app.post('/api/rc-assignments', requireAuth, requireAdmin, async (req, res) => {
const { teacherLoginId, cls, stream, term, year, note } = req.body;
if (!teacherLoginId || !cls || !term || !year)
return res.status(400).json({ error: 'teacherLoginId, cls, term and year required.' });
const { data, error } = await db.from('rc_assignments').insert({
teacher_login: teacherLoginId, class: cls, stream: stream || null,
term, year, note: note || '', status: 'pending', created_by: req.user.id,
}).select().single();
if (error) return res.status(500).json({ error: error.message });
/* Send notification to the assigned teacher */
try {
const { data: teacher } = await db.from('users').select('id,full_name').eq('login_id', teacherLoginId).single();
if (teacher) {
const classLabel = cls + (stream ? ' ' + stream : '');
await db.from('notifications').insert({
recipient_id: teacher.id,
title: 'Report Card Assignment',
message: 'You have been assigned to fill report cards for ' + classLabel + ' β€” ' + term + ' ' + year + (note ? '.\nNote: ' + note : '.') + '\nGo to the Reports section to start filling.',
type: 'rc_assignment',
read: false,
});
}
} catch(notifErr) { /* non-fatal */ }
res.json(data);
});
app.get('/api/rc-assignments', requireAuth, async (req, res) => {
let q = db.from('rc_assignments').select('*').order('created_at', { ascending: false });
if (req.user.role === 'teacher') q = q.eq('teacher_login', req.user.loginId);
const { data, error } = await q;
if (error) return res.status(500).json({ error: error.message });
res.json(data || []);
});
app.patch('/api/rc-assignments/:id', requireAuth, async (req, res) => {
const { error } = await db.from('rc_assignments').update(req.body).eq('id', req.params.id);
if (error) return res.status(500).json({ error: error.message });
res.json({ success: true });
});
app.delete('/api/rc-assignments/:id', requireAuth, requireAdmin, async (req, res) => {
await db.from('rc_assignments').delete().eq('id', req.params.id);
res.json({ success: true });
});
/* ── RC BULK CSV ── */
app.post('/api/report-cards/bulk-csv', requireAuth, requireStaff, async (req, res) => {
const { rows, batchId } = req.body;
if (!rows || !rows.length) return res.status(400).json({ error: 'No rows provided.' });
const results = [];
for (const row of rows) {
const { studentName, studentLogin, cls, stream, term, year, regNo, subjects,
classTchrName, classTchrContact, principalComment } = row;
if (!studentName || !cls) continue;
let studentId = null;
if (studentLogin) {
const { data: su } = await db.from('users').select('id').eq('login_id', studentLogin).single();
if (su) studentId = su.id;
}
const avg = subjects && subjects.length
? (subjects.reduce((s, r) => s + (r.total || 0), 0) / subjects.length).toFixed(2) : 0;
const { data: rc, error } = await db.from('report_cards').insert({
student_id: studentId, student_login: studentLogin || null,
student_name: sanitize(studentName), class: cls, stream: stream || null,
term: term || 'Term 1', year: year || '2026', reg_no: regNo || null,
average: avg, batch_id: batchId || null,
class_teacher_name: sanitize(classTchrName || ''),
class_teacher_contact: classTchrContact || '',
principal_comment: sanitize(principalComment || ''),
status: 'draft', created_by: req.user.id,
}).select().single();
if (error || !rc) { results.push({ studentName, error: error?.message }); continue; }
if (subjects && subjects.length) {
await db.from('report_card_subjects').insert(subjects.map(s => ({
report_card_id: rc.id, subject: s.subject, chapter: s.chapter || '',
score_aoi: s.aoi != null ? s.aoi : null, score_eot: s.eot != null ? s.eot : null,
marks: s.total != null ? s.total : (s.marks || 0), out_of: 100,
grade: s.grade || (s.total >= 80 ? 'A' : s.total >= 70 ? 'B' : s.total >= 60 ? 'C' : s.total >= 50 ? 'D' : 'E'),
teacher_name: req.user.name,
})));
}
results.push({ studentName, id: rc.id, ok: true });
}
res.json({ created: results.filter(r => r.ok).length, results });
});
/* ── REPORT CARDS: list ── */
app.get('/api/report-cards', requireAuth, async (req, res) => {
let q = db.from('report_cards').select('*, report_card_subjects(*)').order('created_at', { ascending: false });
if (req.user.role === 'student') {
q = q.or('student_login.eq.' + req.user.loginId + ',student_id.eq.' + req.user.id).eq('status', 'published');
} else if (req.user.role === 'parent') {
/* support both ?child_login= and ?studentLogin= */
const cLogin = req.query.child_login || req.query.studentLogin || req.user.childLogin;
if (cLogin) q = q.eq('student_login', cLogin).eq('status', 'published');
else { res.json([]); return; }
} else if (req.user.role === 'teacher') {
if (req.query.batch_id) q = q.eq('batch_id', req.query.batch_id);
else if (req.user.classTchrOf) q = q.eq('class', req.user.classTchrOf);
else q = q.eq('class', req.user.cls);
}
if (req.query.class) q = q.eq('class', req.query.class);
if (req.query.term) q = q.eq('term', req.query.term);
if (req.query.year) q = q.eq('year', req.query.year);
if (req.query.status) q = q.eq('status', req.query.status);
if (req.query.batch_id) q = q.eq('batch_id', req.query.batch_id);
if (req.query.studentLogin && req.user.role !== 'parent') q = q.eq('student_login', req.query.studentLogin);
const { data, error } = await q;
if (error) return res.status(500).json({ error: error.message });
res.json((data || []).map(rc => {
const subs = rc.report_card_subjects || [];
delete rc.report_card_subjects;
rc.subjects = subs;
return rc;
}));
});
/* ── REPORT CARDS: get single by ID ── */
app.get('/api/report-cards/:id', requireAuth, async (req, res) => {
const { data: rc, error } = await db.from('report_cards')
.select('*, report_card_subjects(*)')
.eq('id', req.params.id)
.single();
if (error || !rc) return res.status(404).json({ error: 'Report card not found.' });
/* Role-based access check */
if (req.user.role === 'student') {
if (rc.student_login !== req.user.loginId && rc.student_id !== req.user.id)
return res.status(403).json({ error: 'Access denied.' });
if (rc.status !== 'published') return res.status(403).json({ error: 'Not yet published.' });
} else if (req.user.role === 'parent') {
/* parent must have linked this student */
const { data: link } = await db.from('parent_children')
.select('id').eq('parent_id', req.user.id).eq('student_login', rc.student_login).single();
if (!link) return res.status(403).json({ error: 'Access denied.' });
if (rc.status !== 'published') return res.status(403).json({ error: 'Not yet published.' });
}
const subs = rc.report_card_subjects || [];
delete rc.report_card_subjects;
rc.subjects = subs;
res.json(rc);
});
/* ── REPORT CARDS: create ── */
app.post('/api/report-cards', requireAuth, requireStaff, async (req, res) => {
const {
studentId, studentLogin, studentName, cls, stream, term, year, regNo,
subjects, conduct, classTchrComment, classTchrName, classTchrContact,
principalComment, wardenComment, position, outOf, nextTermStart, nextTermBoard,
batchId, status
} = req.body;
if (!studentName || !cls || !term || !year)
return res.status(400).json({ error: 'Student name, class, term and year are required.' });
// Resolve student_id from login if not provided
let resolvedId = studentId || null;
let resolvedLogin = studentLogin || null;
if (!resolvedId && studentLogin) {
const { data: su } = await db.from('users').select('id').eq('login_id', studentLogin).single();
if (su) resolvedId = su.id;
}
const avg = subjects && subjects.length
? (subjects.reduce((s, r) => s + (r.total != null ? r.total : (r.marks || 0)), 0) / subjects.length).toFixed(2) : 0;
const { data: rc, error } = await db.from('report_cards').insert({
student_id: resolvedId, student_login: resolvedLogin,
student_name: sanitize(studentName), class: cls, stream: stream || null,
term, year, reg_no: regNo || null, average: avg,
position: position || null, out_of: outOf || null, conduct: conduct || null,
class_teacher_comment: sanitize(classTchrComment || ''),
class_teacher_name: sanitize(classTchrName || ''),
class_teacher_contact: classTchrContact || '',
principal_comment: sanitize(principalComment || ''),
warden_comment: sanitize(wardenComment || ''),
next_term_start: nextTermStart || null, next_term_board: nextTermBoard || null,
batch_id: batchId || null,
class_teacher_login: req.user.classTchrOf ? req.user.loginId : null,
status: status || 'draft', created_by: req.user.id,
}).select().single();
if (error) return res.status(500).json({ error: error.message });
if (subjects && subjects.length) {
await db.from('report_card_subjects').insert(subjects.map(s => ({
report_card_id: rc.id, subject: s.subject, chapter: s.chapter || '',
score_aoi: s.aoi != null ? s.aoi : null, score_eot: s.eot != null ? s.eot : null,
marks: s.total != null ? s.total : (s.marks || 0), out_of: s.outOf || 100,
grade: s.grade || '', teacher_comment: sanitize(s.comment || ''),
teacher_ref: s.teacherRef || '', teacher_name: s.teacherName || req.user.name,
})));
}
res.json(rc);
});
/* ── REPORT CARDS: publish / update ── */
app.patch('/api/report-cards/:id', requireAuth, requireStaff, async (req, res) => {
const updates = {};
const b = req.body;
if (b.status !== undefined) updates.status = b.status;
if (b.principalComment !== undefined) updates.principal_comment = sanitize(b.principalComment || '');
if (b.conduct !== undefined) updates.conduct = b.conduct;
if (b.classTchrComment !== undefined) updates.class_teacher_comment = sanitize(b.classTchrComment || '');
if (b.classTchrName !== undefined) updates.class_teacher_name = sanitize(b.classTchrName || '');
if (b.classTchrContact !== undefined) updates.class_teacher_contact = b.classTchrContact || '';
if (b.wardenComment !== undefined) updates.warden_comment = sanitize(b.wardenComment || '');
if (b.nextTermStart !== undefined) updates.next_term_start = b.nextTermStart || null;
if (b.nextTermBoard !== undefined) updates.next_term_board = b.nextTermBoard || null;
if (b.position !== undefined) updates.position = b.position || null;
if (b.outOf !== undefined) updates.out_of = b.outOf || null;
if (b.regNo !== undefined) updates.reg_no = b.regNo || null;
if (b.studentName !== undefined) updates.student_name = sanitize(b.studentName || '');
if (b.cls !== undefined) updates.class = b.cls;
if (b.stream !== undefined) updates.stream = b.stream || null;
if (b.term !== undefined) updates.term = b.term;
if (b.year !== undefined) updates.year = b.year;
if (!Object.keys(updates).length) return res.status(400).json({ error: 'Nothing to update.' });
const { error } = await db.from('report_cards').update(updates).eq('id', req.params.id);
if (error) return res.status(500).json({ error: error.message });
/* If subjects array is provided, replace them */
if (b.subjects && Array.isArray(b.subjects)) {
await db.from('report_card_subjects').delete().eq('report_card_id', req.params.id);
if (b.subjects.length) {
await db.from('report_card_subjects').insert(b.subjects.map(s => ({
report_card_id: req.params.id,
subject: s.subject, chapter: s.chapter || '',
score_aoi: s.aoi != null ? s.aoi : null,
score_eot: s.eot != null ? s.eot : null,
marks: s.total != null ? s.total : (s.marks || 0), out_of: s.outOf || 100,
grade: s.grade || '', teacher_comment: sanitize(s.comment || ''),
teacher_ref: s.teacherRef || '', teacher_name: s.teacherName || req.user.name,
})));
}
/* Recalculate average */
const avg = b.subjects.length
? (b.subjects.reduce((a, s) => a + (s.total != null ? s.total : (s.marks || 0)), 0) / b.subjects.length).toFixed(2) : 0;
await db.from('report_cards').update({ average: avg }).eq('id', req.params.id);
}
res.json({ success: true });
});
app.delete('/api/report-cards/:id', requireAuth, requireAdmin, async (req, res) => {
const { error } = await db.from('report_cards').delete().eq('id', req.params.id);
if (error) return res.status(500).json({ error: error.message });
res.json({ success: true });
});
/* ── PARENT: link a child ── */
app.post('/api/parent/link-child', requireAuth, async (req, res) => {
if (req.user.role !== 'parent') return res.status(403).json({ error: 'Parents only.' });
const { childLoginId } = req.body;
if (!childLoginId) return res.status(400).json({ error: 'Child login ID is required.' });
const { data: student } = await db
.from('users')
.select('id, login_id, full_name, class')
.eq('login_id', childLoginId.toUpperCase())
.eq('role', 'student')
.single();
if (!student) return res.status(404).json({ error: 'Student not found. Please check the login ID.' });
const { data: existing } = await db
.from('parent_children')
.select('id')
.eq('parent_id', req.user.id)
.eq('student_id', student.id)
.single();
if (existing) return res.status(409).json({ error: 'This child is already linked to your account.' });
await db.from('parent_children').insert({
parent_id: req.user.id,
student_id: student.id,
student_login: student.login_id,
student_name: student.full_name,
student_class: student.class,
});
res.json({ success: true, message: student.full_name + ' (' + student.class + ') linked successfully.' });
});
/* ── PARENT: list children ── */
app.delete('/api/parent/link-child/:childLoginId', requireAuth, async (req, res) => {
if (req.user.role !== 'parent') return res.status(403).json({ error: 'Parents only.' });
await db.from('parent_children').delete()
.eq('parent_id', req.user.id)
.eq('student_login', req.params.childLoginId);
res.json({ success: true });
});
app.get('/api/parent/children', requireAuth, async (req, res) => {
if (req.user.role !== 'parent') return res.status(403).json({ error: 'Parents only.' });
const { data } = await db.from('parent_children').select('*').eq('parent_id', req.user.id).order('created_at');
res.json(data || []);
});
/* ── SUB-ADMINS: create ── */
app.post('/api/admin/sub-admins', requireAuth, requireAdmin, async (req, res) => {
if (req.user.subRole) return res.status(403).json({ error: 'Only the super admin can create sub-admins.' });
const { name, subRole, password, customLoginId } = req.body;
const validRoles = ['headteacher', 'academic', 'nurse', 'bursar'];
if (!name || !subRole || !password || !validRoles.includes(subRole)) {
return res.status(400).json({ error: 'Name, valid role and password are required.' });
}
const loginId = customLoginId ? customLoginId.toUpperCase() : ('ADMIN' + subRole.toUpperCase().slice(0, 4) + Math.floor(100 + Math.random() * 900));
const { data: existing } = await db.from('users').select('login_id').eq('login_id', loginId).single();
if (existing) return res.status(409).json({ error: 'Login ID "' + loginId + '" is already taken.' });
const hash = await bcrypt.hash(password, 12);
const { error } = await db.from('users').insert({
login_id: loginId,
password_hash: hash,
role: 'admin',
sub_role: subRole,
full_name: sanitize(name),
status: 'active',
});
if (error) return res.status(500).json({ error: error.message });
res.json({ success: true, loginId });
});
/* ── SUB-ADMINS: list ── */
app.get('/api/admin/sub-admins', requireAuth, requireAdmin, async (req, res) => {
const { data } = await db
.from('users')
.select('id, login_id, full_name, sub_role, status, created_at')
.eq('role', 'admin')
.not('sub_role', 'is', null)
.order('sub_role');
res.json(data || []);
});
/* ── SUB-ADMINS: delete ── */
app.delete('/api/admin/sub-admins/:loginId', requireAuth, requireAdmin, async (req, res) => {
if (req.user.subRole) return res.status(403).json({ error: 'Only the super admin can remove sub-admins.' });
await db.from('users').delete().eq('login_id', req.params.loginId).eq('role', 'admin').not('sub_role', 'is', null);
res.json({ success: true });
});
/* ── REPORT TEMPLATES: get students for a class ── */
app.get('/api/report-template/students/:cls', requireAuth, requireAdmin, async (req, res) => {
const { data, error } = await db.from('users').select('id, login_id, full_name, class, stream').eq('role','student').eq('class',req.params.cls).eq('status','active').order('full_name');
if (error) return res.status(500).json({ error: error.message });
res.json(data || []);
});
/* ── REPORT TEMPLATES: admin assigns template to a teacher ── */
app.post('/api/report-templates', requireAuth, requireAdmin, async (req, res) => {
const { teacherLoginId, cls, term, year, subject } = req.body;
if (!teacherLoginId || !cls || !term || !year) return res.status(400).json({ error: 'Teacher login, class, term and year required.' });
const { data: teacher } = await db.from('users').select('id,full_name,subject').eq('login_id',teacherLoginId).eq('role','teacher').single();
if (!teacher) return res.status(404).json({ error: 'Teacher not found: ' + teacherLoginId });
const { data: students } = await db.from('users').select('id,login_id,full_name,stream').eq('role','student').eq('class',cls).eq('status','active').order('full_name');
const subj = subject || teacher.subject || '';
const { data: existing } = await db.from('report_templates').select('id').eq('teacher_login',teacherLoginId).eq('class',cls).eq('term',term).eq('year',year).eq('subject',subj).single();
if (existing) {
await db.from('report_templates').update({ student_count:(students||[]).length, updated_at:new Date().toISOString() }).eq('id',existing.id);
return res.json({ success:true, id:existing.id, teacherName:teacher.full_name, studentCount:(students||[]).length, updated:true });
}
const { data: tmpl, error } = await db.from('report_templates').insert({ teacher_login:teacherLoginId, teacher_name:teacher.full_name, teacher_id:teacher.id, class:cls, term, year, subject:subj, student_count:(students||[]).length, status:'assigned', assigned_by:req.user.id, assigned_at:new Date().toISOString() }).select().single();
if (error) return res.status(500).json({ error: error.message });
res.json({ success:true, id:tmpl.id, teacherName:teacher.full_name, studentCount:(students||[]).length });
});
/* ── REPORT TEMPLATES: list ── */
app.get('/api/report-templates', requireAuth, async (req, res) => {
let q = req.user.role === 'admin'
? db.from('report_templates').select('*').order('assigned_at',{ascending:false})
: db.from('report_templates').select('*').eq('teacher_login',req.user.loginId).order('assigned_at',{ascending:false});
const { data, error } = await q;
if (error) return res.status(500).json({ error: error.message });
res.json(data || []);
});
/* ── REPORT TEMPLATES: get students for a specific template (teacher downloads) ── */
app.get('/api/report-templates/:id/students', requireAuth, async (req, res) => {
const { data: tmpl } = await db.from('report_templates').select('*').eq('id',req.params.id).single();
if (!tmpl) return res.status(404).json({ error: 'Template not found.' });
if (req.user.role === 'teacher' && tmpl.teacher_login !== req.user.loginId) return res.status(403).json({ error: 'Not assigned to you.' });
const { data: students } = await db.from('users').select('id,login_id,full_name,stream').eq('role','student').eq('class',tmpl.class).eq('status','active').order('full_name');
res.json({ template: tmpl, students: students || [] });
});
/* ── REPORT TEMPLATES: delete ── */
app.delete('/api/report-templates/:id', requireAuth, requireAdmin, async (req, res) => {
await db.from('report_templates').delete().eq('id',req.params.id);
res.json({ success:true });
});
/* ─────────────────────────────────────────────
CATCH-ALL: unknown routes
───────────────────────────────────────────── */
app.use((req, res) => {
res.status(404).json({ error: 'Not found: ' + req.method + ' ' + req.path });
});
/* ─────────────────────────────────────────────
GLOBAL ERROR HANDLER
───────────────────────────────────────────── */
app.use((err, req, res, next) => {
console.error('Unhandled error:', err);
res.status(500).json({ error: 'Internal server error.' });
});
/* ─────────────────────────────────────────────
START SERVER
───────────────────────────────────────────── */
app.listen(PORT, '0.0.0.0', () => {
console.log('\n══════════════════════════════════════════');
console.log(' Kawanda Secondary School β€” Backend v3.0');
console.log('══════════════════════════════════════════');
console.log(' Port :', PORT);
console.log(' Supabase :', SUPABASE_URL);
console.log(' JWT secret :', JWT_SECRET === 'KawandaSS_default_secret_change_me' ? '⚠️ using default β€” set JWT_SECRET env var' : 'βœ… custom');
console.log('══════════════════════════════════════════\n');
/* Seed admin 4 seconds after startup to let DB connection stabilise */
setTimeout(seedAdmin, 4000);
});