518 lines
21 KiB
JavaScript
518 lines
21 KiB
JavaScript
|
|
require('dotenv').config({ quiet: true });
|
||
|
|
const path = require('path');
|
||
|
|
const express = require('express');
|
||
|
|
const session = require('express-session');
|
||
|
|
const methodOverride = require('method-override');
|
||
|
|
const morgan = require('morgan');
|
||
|
|
const passport = require('passport');
|
||
|
|
const LocalStrategy = require('passport-local').Strategy;
|
||
|
|
const GoogleStrategy = require('passport-google-oauth20').Strategy;
|
||
|
|
const bcrypt = require('bcryptjs');
|
||
|
|
const crypto = require('crypto');
|
||
|
|
const { db, initDb } = require('./server/db');
|
||
|
|
const expressLayouts = require('express-ejs-layouts');
|
||
|
|
const { sendVerificationEmail } = require('./server/mailer');
|
||
|
|
|
||
|
|
const app = express();
|
||
|
|
|
||
|
|
const PORT = process.env.PORT || 3000;
|
||
|
|
const SESSION_SECRET = process.env.SESSION_SECRET || 'dev_secret_change_me';
|
||
|
|
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || '';
|
||
|
|
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET || '';
|
||
|
|
const GOOGLE_CALLBACK_URL = process.env.GOOGLE_CALLBACK_URL || `http://localhost:${PORT}/auth/google/callback`;
|
||
|
|
|
||
|
|
app.set('view engine', 'ejs');
|
||
|
|
app.set('views', path.join(__dirname, 'views'));
|
||
|
|
app.use('/public', express.static(path.join(__dirname, 'public')));
|
||
|
|
app.use(expressLayouts);
|
||
|
|
app.set('layout', 'layout');
|
||
|
|
|
||
|
|
app.use(express.urlencoded({ extended: true }));
|
||
|
|
app.use(express.json());
|
||
|
|
app.use(methodOverride('_method'));
|
||
|
|
app.use(morgan('dev'));
|
||
|
|
|
||
|
|
app.use(
|
||
|
|
session({
|
||
|
|
secret: SESSION_SECRET,
|
||
|
|
resave: false,
|
||
|
|
saveUninitialized: false,
|
||
|
|
})
|
||
|
|
);
|
||
|
|
|
||
|
|
app.use(passport.initialize());
|
||
|
|
app.use(passport.session());
|
||
|
|
|
||
|
|
passport.serializeUser((user, done) => {
|
||
|
|
done(null, { id: user.id, email: user.email, role: user.role, name: user.name });
|
||
|
|
});
|
||
|
|
passport.deserializeUser((obj, done) => done(null, obj));
|
||
|
|
|
||
|
|
passport.use(
|
||
|
|
new LocalStrategy(
|
||
|
|
{ usernameField: 'email', passwordField: 'password' },
|
||
|
|
(email, password, done) => {
|
||
|
|
const normEmail = email.toLowerCase();
|
||
|
|
db.get('SELECT * FROM users WHERE email = ?', [normEmail], async (err, user) => {
|
||
|
|
if (err) return done(err);
|
||
|
|
if (!user || !user.password_hash) return done(null, false, { message: 'Invalid credentials' });
|
||
|
|
if (user.banned) return done(null, false, { message: 'Account is banned' });
|
||
|
|
const ok = await bcrypt.compare(password, user.password_hash);
|
||
|
|
if (!ok) return done(null, false, { message: 'Invalid credentials' });
|
||
|
|
if (!user.email_verified) return done(null, false, { reason: 'unverified', email: normEmail });
|
||
|
|
return done(null, user);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
)
|
||
|
|
);
|
||
|
|
|
||
|
|
if (GOOGLE_CLIENT_ID && GOOGLE_CLIENT_SECRET) {
|
||
|
|
passport.use(
|
||
|
|
new GoogleStrategy(
|
||
|
|
{
|
||
|
|
clientID: GOOGLE_CLIENT_ID,
|
||
|
|
clientSecret: GOOGLE_CLIENT_SECRET,
|
||
|
|
callbackURL: GOOGLE_CALLBACK_URL,
|
||
|
|
},
|
||
|
|
(accessToken, refreshToken, profile, done) => {
|
||
|
|
try {
|
||
|
|
const googleId = profile.id;
|
||
|
|
const rawEmail = (profile.emails && profile.emails[0] && profile.emails[0].value) || null;
|
||
|
|
const email = rawEmail ? rawEmail.toLowerCase() : null;
|
||
|
|
const name = profile.displayName || (profile.name ? `${profile.name.givenName || ''} ${profile.name.familyName || ''}`.trim() : (email || 'Google User'));
|
||
|
|
db.get('SELECT * FROM users WHERE google_id = ? OR (email IS NOT NULL AND email = ?)', [googleId, email], (err, user) => {
|
||
|
|
if (err) return done(err);
|
||
|
|
if (user) {
|
||
|
|
if (user.banned) return done(null, false, { message: 'Account is banned' });
|
||
|
|
if (!user.google_id) {
|
||
|
|
db.run('UPDATE users SET google_id = ?, email_verified = 1 WHERE id = ?', [googleId, user.id], (uErr) => done(uErr, { ...user, google_id: googleId, email_verified: 1 }));
|
||
|
|
} else {
|
||
|
|
if (!user.email_verified) {
|
||
|
|
db.run('UPDATE users SET email_verified = 1 WHERE id = ?', [user.id], (uErr) => done(uErr, { ...user, email_verified: 1 }));
|
||
|
|
} else {
|
||
|
|
return done(null, user);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
db.run(
|
||
|
|
'INSERT INTO users (email, name, google_id, role, email_verified, created_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)',
|
||
|
|
[email, name, googleId, 'user', 1],
|
||
|
|
function (insErr) {
|
||
|
|
if (insErr) return done(insErr);
|
||
|
|
db.get('SELECT * FROM users WHERE id = ?', [this.lastID], (selErr, newUser) => done(selErr, newUser));
|
||
|
|
}
|
||
|
|
);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
} catch (e) {
|
||
|
|
return done(e);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function ensureAuth(req, res, next) {
|
||
|
|
if (req.isAuthenticated()) {
|
||
|
|
db.get('SELECT banned FROM users WHERE id = ?', [req.user.id], (e, row) => {
|
||
|
|
if (e || !row) return res.redirect('/logout');
|
||
|
|
if (row.banned) {
|
||
|
|
req.logout(() => {
|
||
|
|
req.session.message = 'Your account has been banned.';
|
||
|
|
return res.redirect('/login');
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
return next();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
res.redirect('/login');
|
||
|
|
}
|
||
|
|
function ensureAdmin(req, res, next) {
|
||
|
|
if (!req.isAuthenticated()) return res.status(403).send('Forbidden');
|
||
|
|
db.get('SELECT role, banned FROM users WHERE id = ?', [req.user.id], (e, row) => {
|
||
|
|
if (e || !row) return res.status(403).send('Forbidden');
|
||
|
|
if (row.banned) {
|
||
|
|
req.logout(() => {
|
||
|
|
req.session.message = 'Your account has been banned.';
|
||
|
|
return res.redirect('/login');
|
||
|
|
});
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (row.role === 'admin') return next();
|
||
|
|
return res.status(403).send('Forbidden');
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
app.use((req, res, next) => {
|
||
|
|
res.locals.currentUser = req.user || null;
|
||
|
|
res.locals.googleEnabled = Boolean(GOOGLE_CLIENT_ID && GOOGLE_CLIENT_SECRET);
|
||
|
|
res.locals.message = req.session.message || null;
|
||
|
|
delete req.session.message;
|
||
|
|
next();
|
||
|
|
});
|
||
|
|
|
||
|
|
app.get('/', (req, res) => {
|
||
|
|
if (req.isAuthenticated()) return res.redirect('/dashboard');
|
||
|
|
res.redirect('/login');
|
||
|
|
});
|
||
|
|
|
||
|
|
app.get('/login', (req, res) => res.render('login', { query: req.query }));
|
||
|
|
app.post('/login', (req, res, next) => {
|
||
|
|
passport.authenticate('local', (err, user, info) => {
|
||
|
|
if (err) return next(err);
|
||
|
|
if (!user) {
|
||
|
|
if (info && info.reason === 'unverified' && info.email) {
|
||
|
|
return res.redirect(`/login?unverified=1&email=${encodeURIComponent(info.email)}`);
|
||
|
|
}
|
||
|
|
return res.redirect('/login?error=1');
|
||
|
|
}
|
||
|
|
req.logIn(user, (e) => {
|
||
|
|
if (e) return next(e);
|
||
|
|
return res.redirect('/dashboard');
|
||
|
|
});
|
||
|
|
})(req, res, next);
|
||
|
|
});
|
||
|
|
|
||
|
|
app.get('/register', (req, res) => res.render('register'));
|
||
|
|
app.post('/register', async (req, res) => {
|
||
|
|
const { name, email, password } = req.body;
|
||
|
|
if (!email || !password) {
|
||
|
|
req.session.message = 'Email and password are required';
|
||
|
|
return res.redirect('/register');
|
||
|
|
}
|
||
|
|
const hash = await bcrypt.hash(password, 10);
|
||
|
|
const normEmail = email.toLowerCase();
|
||
|
|
const token = crypto.randomBytes(32).toString('hex');
|
||
|
|
const expires = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
|
||
|
|
db.run(
|
||
|
|
'INSERT INTO users (name, email, password_hash, role, email_verified, verify_token, verify_token_expires, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)',
|
||
|
|
[name || '', normEmail, hash, 'user', 0, token, expires],
|
||
|
|
async function (err) {
|
||
|
|
if (err) {
|
||
|
|
req.session.message = 'Registration failed. Email may be already in use.';
|
||
|
|
return res.redirect('/register');
|
||
|
|
}
|
||
|
|
try { await sendVerificationEmail(normEmail, token, name || ''); } catch (e) { console.error('Failed to send verification email:', e.message); }
|
||
|
|
req.session.message = 'Registration successful. Please check your email to verify your account.';
|
||
|
|
res.redirect('/login');
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
app.post('/logout', (req, res, next) => {
|
||
|
|
req.logout(err => {
|
||
|
|
if (err) return next(err);
|
||
|
|
res.redirect('/login');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
if (GOOGLE_CLIENT_ID && GOOGLE_CLIENT_SECRET) {
|
||
|
|
app.get('/auth/google', passport.authenticate('google', { scope: ['profile', 'email'] }));
|
||
|
|
app.get('/auth/google/callback', passport.authenticate('google', { failureRedirect: '/login?oauth_error=1' }), (req, res) => {
|
||
|
|
res.redirect('/dashboard');
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
app.get('/dashboard', ensureAuth, (req, res) => {
|
||
|
|
db.all(
|
||
|
|
'SELECT * FROM tickets WHERE user_id = ? ORDER BY updated_at DESC, created_at DESC',
|
||
|
|
[req.user.id],
|
||
|
|
(err, rows) => {
|
||
|
|
if (err) rows = [];
|
||
|
|
res.render('dashboard', { tickets: rows });
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
app.get('/tickets/new', ensureAuth, (req, res) => res.render('ticket_new'));
|
||
|
|
app.post('/tickets', ensureAuth, (req, res) => {
|
||
|
|
const { subject, description } = req.body;
|
||
|
|
if (!subject || !description) {
|
||
|
|
req.session.message = 'Subject and description are required.';
|
||
|
|
return res.redirect('/tickets/new');
|
||
|
|
}
|
||
|
|
db.run(
|
||
|
|
'INSERT INTO tickets (user_id, subject, description, status, created_at, updated_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)',
|
||
|
|
[req.user.id, subject, description, 'open'],
|
||
|
|
function (err) {
|
||
|
|
if (err) {
|
||
|
|
req.session.message = 'Failed to create ticket.';
|
||
|
|
return res.redirect('/tickets/new');
|
||
|
|
}
|
||
|
|
res.redirect(`/tickets/${this.lastID}`);
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
app.get('/tickets/:id', ensureAuth, (req, res) => {
|
||
|
|
const id = req.params.id;
|
||
|
|
db.get('SELECT t.*, u.name as user_name, u.email as user_email FROM tickets t JOIN users u ON u.id = t.user_id WHERE t.id = ?', [id], (err, ticket) => {
|
||
|
|
if (err || !ticket) return res.status(404).send('Not found');
|
||
|
|
if (req.user.role !== 'admin' && ticket.user_id !== req.user.id) return res.status(403).send('Forbidden');
|
||
|
|
db.all('SELECT r.*, u.name, u.email FROM ticket_responses r JOIN users u ON u.id = r.user_id WHERE r.ticket_id = ? ORDER BY r.created_at ASC', [id], (rErr, responses) => {
|
||
|
|
if (rErr) responses = [];
|
||
|
|
res.render('ticket_view', { ticket, responses });
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
app.post('/tickets/:id/respond', ensureAuth, (req, res) => {
|
||
|
|
const id = req.params.id;
|
||
|
|
const { message } = req.body;
|
||
|
|
if (!message) return res.redirect(`/tickets/${id}`);
|
||
|
|
db.get('SELECT * FROM tickets WHERE id = ?', [id], (err, ticket) => {
|
||
|
|
if (err || !ticket) return res.status(404).send('Not found');
|
||
|
|
if (req.user.role !== 'admin' && ticket.user_id !== req.user.id) return res.status(403).send('Forbidden');
|
||
|
|
db.run(
|
||
|
|
'INSERT INTO ticket_responses (ticket_id, user_id, message, is_admin_response, created_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)',
|
||
|
|
[id, req.user.id, message, req.user.role === 'admin' ? 1 : 0],
|
||
|
|
function (rErr) {
|
||
|
|
if (rErr) return res.status(500).send('Failed to add response');
|
||
|
|
db.run('UPDATE tickets SET updated_at = CURRENT_TIMESTAMP, status = ? WHERE id = ?', [req.user.role === 'admin' ? 'answered' : 'open', id], () => {
|
||
|
|
res.redirect(`/tickets/${id}`);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
app.get('/admin', ensureAdmin, (req, res) => {
|
||
|
|
const status = req.query.status;
|
||
|
|
const params = [];
|
||
|
|
let sql = 'SELECT t.*, u.email as user_email, u.name as user_name FROM tickets t JOIN users u ON u.id = t.user_id';
|
||
|
|
if (status) {
|
||
|
|
sql += ' WHERE t.status = ?';
|
||
|
|
params.push(status);
|
||
|
|
}
|
||
|
|
sql += ' ORDER BY updated_at DESC, created_at DESC';
|
||
|
|
db.all(sql, params, (err, rows) => {
|
||
|
|
if (err) rows = [];
|
||
|
|
res.render('admin', { tickets: rows, status: status || '' });
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
app.post('/admin/tickets/:id/status', ensureAdmin, (req, res) => {
|
||
|
|
const { status } = req.body;
|
||
|
|
const id = req.params.id;
|
||
|
|
db.run('UPDATE tickets SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [status, id], (err) => {
|
||
|
|
if (err) req.session.message = 'Failed to update status';
|
||
|
|
res.redirect('/admin');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
app.post('/admin/tickets/:id/delete', ensureAdmin, (req, res) => {
|
||
|
|
const id = req.params.id;
|
||
|
|
db.run('DELETE FROM tickets WHERE id = ?', [id], (err) => {
|
||
|
|
req.session.message = err ? 'Failed to delete ticket.' : 'Ticket deleted.';
|
||
|
|
res.redirect('/admin');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
app.get('/admin/users', ensureAdmin, (req, res) => {
|
||
|
|
db.all(
|
||
|
|
`SELECT
|
||
|
|
id, name, email, role, created_at, banned, email_verified,
|
||
|
|
CASE WHEN google_id IS NOT NULL AND TRIM(google_id) <> '' THEN 1 ELSE 0 END AS has_google,
|
||
|
|
CASE WHEN password_hash IS NOT NULL AND TRIM(password_hash) <> '' THEN 1 ELSE 0 END AS has_password
|
||
|
|
FROM users
|
||
|
|
ORDER BY created_at DESC, id DESC`,
|
||
|
|
[],
|
||
|
|
(err, users) => {
|
||
|
|
if (err) users = [];
|
||
|
|
res.render('admin_users', { users });
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
app.post('/admin/users/:id/make-admin', ensureAdmin, (req, res) => {
|
||
|
|
const id = req.params.id;
|
||
|
|
db.run('UPDATE users SET role = ? WHERE id = ?', ['admin', id], (err) => {
|
||
|
|
req.session.message = err ? 'Failed to grant admin rights.' : 'Admin rights granted.';
|
||
|
|
res.redirect('/admin/users');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
app.post('/admin/users/:id/unadmin', ensureAdmin, (req, res) => {
|
||
|
|
const id = parseInt(req.params.id, 10);
|
||
|
|
if (Number.isNaN(id)) {
|
||
|
|
req.session.message = 'Invalid user id.';
|
||
|
|
return res.redirect('/admin/users');
|
||
|
|
}
|
||
|
|
if (req.user && req.user.id === id) {
|
||
|
|
req.session.message = 'You cannot remove your own admin role.';
|
||
|
|
return res.redirect('/admin/users');
|
||
|
|
}
|
||
|
|
db.get('SELECT COUNT(*) AS cnt FROM users WHERE role = ?', ['admin'], (err, row) => {
|
||
|
|
if (err) {
|
||
|
|
req.session.message = 'Database error.';
|
||
|
|
return res.redirect('/admin/users');
|
||
|
|
}
|
||
|
|
const adminCount = row ? row.cnt : 0;
|
||
|
|
db.get('SELECT id, email, role FROM users WHERE id = ?', [id], (e2, user) => {
|
||
|
|
if (e2 || !user) {
|
||
|
|
req.session.message = 'User not found.';
|
||
|
|
return res.redirect('/admin/users');
|
||
|
|
}
|
||
|
|
if (user.role !== 'admin') {
|
||
|
|
req.session.message = 'User is not an admin.';
|
||
|
|
return res.redirect('/admin/users');
|
||
|
|
}
|
||
|
|
if (adminCount <= 1) {
|
||
|
|
req.session.message = 'Cannot remove the last admin.';
|
||
|
|
return res.redirect('/admin/users');
|
||
|
|
}
|
||
|
|
db.run('UPDATE users SET role = ? WHERE id = ?', ['user', id], (uErr) => {
|
||
|
|
req.session.message = uErr ? 'Failed to remove admin rights.' : `Admin rights removed from ${user.email || 'user'}.`;
|
||
|
|
return res.redirect('/admin/users');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
app.post('/admin/users/grant-admin', ensureAdmin, (req, res) => {
|
||
|
|
let { email, name } = req.body;
|
||
|
|
email = (email || '').trim().toLowerCase();
|
||
|
|
name = (name || '').trim();
|
||
|
|
if (!email) {
|
||
|
|
req.session.message = 'Email is required.';
|
||
|
|
return res.redirect('/admin/users');
|
||
|
|
}
|
||
|
|
db.get('SELECT * FROM users WHERE email = ?', [email], (err, user) => {
|
||
|
|
if (err) {
|
||
|
|
req.session.message = 'Database error.';
|
||
|
|
return res.redirect('/admin/users');
|
||
|
|
}
|
||
|
|
if (user) {
|
||
|
|
db.run('UPDATE users SET role = ? WHERE id = ?', ['admin', user.id], (uErr) => {
|
||
|
|
req.session.message = uErr ? 'Failed to grant admin rights.' : `Admin rights granted to ${email}.`;
|
||
|
|
return res.redirect('/admin/users');
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
db.run(
|
||
|
|
'INSERT INTO users (name, email, role, created_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)',
|
||
|
|
[name, email, 'admin'],
|
||
|
|
(iErr) => {
|
||
|
|
req.session.message = iErr ? 'Failed to create admin user.' : `Admin user created for ${email}.`;
|
||
|
|
return res.redirect('/admin/users');
|
||
|
|
}
|
||
|
|
);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
app.post('/admin/users/:id/ban', ensureAdmin, (req, res) => {
|
||
|
|
const id = parseInt(req.params.id, 10);
|
||
|
|
if (Number.isNaN(id)) { req.session.message = 'Invalid user id.'; return res.redirect('/admin/users'); }
|
||
|
|
if (req.user && req.user.id === id) { req.session.message = 'You cannot ban yourself.'; return res.redirect('/admin/users'); }
|
||
|
|
db.get('SELECT id, email, role FROM users WHERE id = ?', [id], (e, user) => {
|
||
|
|
if (e || !user) { req.session.message = 'User not found.'; return res.redirect('/admin/users'); }
|
||
|
|
if (user.role === 'admin') {
|
||
|
|
db.get('SELECT COUNT(*) AS cnt FROM users WHERE role = ?', ['admin'], (err, row) => {
|
||
|
|
const adminCount = row ? row.cnt : 0;
|
||
|
|
if (adminCount <= 1) { req.session.message = 'Cannot ban the last admin.'; return res.redirect('/admin/users'); }
|
||
|
|
db.run('UPDATE users SET banned = 1 WHERE id = ?', [id], (uErr) => {
|
||
|
|
req.session.message = uErr ? 'Failed to ban user.' : `User ${user.email || ''} banned.`;
|
||
|
|
return res.redirect('/admin/users');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
db.run('UPDATE users SET banned = 1 WHERE id = ?', [id], (uErr) => {
|
||
|
|
req.session.message = uErr ? 'Failed to ban user.' : `User ${user.email || ''} banned.`;
|
||
|
|
return res.redirect('/admin/users');
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
app.post('/admin/users/:id/unban', ensureAdmin, (req, res) => {
|
||
|
|
const id = parseInt(req.params.id, 10);
|
||
|
|
if (Number.isNaN(id)) { req.session.message = 'Invalid user id.'; return res.redirect('/admin/users'); }
|
||
|
|
db.run('UPDATE users SET banned = 0 WHERE id = ?', [id], (uErr) => {
|
||
|
|
req.session.message = uErr ? 'Failed to unban user.' : 'User unbanned.';
|
||
|
|
return res.redirect('/admin/users');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
app.post('/admin/users/:id/delete', ensureAdmin, (req, res) => {
|
||
|
|
const id = parseInt(req.params.id, 10);
|
||
|
|
if (Number.isNaN(id)) { req.session.message = 'Invalid user id.'; return res.redirect('/admin/users'); }
|
||
|
|
if (req.user && req.user.id === id) { req.session.message = 'You cannot delete yourself.'; return res.redirect('/admin/users'); }
|
||
|
|
db.get('SELECT id, email, role FROM users WHERE id = ?', [id], (e, user) => {
|
||
|
|
if (e || !user) { req.session.message = 'User not found.'; return res.redirect('/admin/users'); }
|
||
|
|
if (user.role === 'admin') {
|
||
|
|
db.get('SELECT COUNT(*) AS cnt FROM users WHERE role = ?', ['admin'], (err, row) => {
|
||
|
|
const adminCount = row ? row.cnt : 0;
|
||
|
|
if (adminCount <= 1) { req.session.message = 'Cannot delete the last admin.'; return res.redirect('/admin/users'); }
|
||
|
|
db.run('DELETE FROM users WHERE id = ?', [id], (dErr) => {
|
||
|
|
req.session.message = dErr ? 'Failed to delete user.' : `User ${user.email || ''} deleted.`;
|
||
|
|
return res.redirect('/admin/users');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
db.run('DELETE FROM users WHERE id = ?', [id], (dErr) => {
|
||
|
|
req.session.message = dErr ? 'Failed to delete user.' : `User ${user.email || ''} deleted.`;
|
||
|
|
return res.redirect('/admin/users');
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
initDb(() => {
|
||
|
|
const adminEmail = process.env.ADMIN_EMAIL || 'admin@example.com';
|
||
|
|
const adminPass = process.env.ADMIN_PASSWORD || 'admin123';
|
||
|
|
db.get('SELECT * FROM users WHERE email = ?', [adminEmail], async (err, user) => {
|
||
|
|
if (!user) {
|
||
|
|
const hash = await bcrypt.hash(adminPass, 10);
|
||
|
|
db.run(
|
||
|
|
'INSERT INTO users (name, email, password_hash, role, email_verified, created_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)',
|
||
|
|
['Administrator', adminEmail, hash, 'admin', 1]
|
||
|
|
);
|
||
|
|
console.log(`Seeded admin user: ${adminEmail}`);
|
||
|
|
} else if (!user.email_verified) {
|
||
|
|
db.run('UPDATE users SET email_verified = 1 WHERE id = ?', [user.id]);
|
||
|
|
}
|
||
|
|
app.listen(PORT, () => console.log(`TicketSupport server running on http://localhost:${PORT}`));
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
app.get('/verify/:token', (req, res) => {
|
||
|
|
const { token } = req.params;
|
||
|
|
if (!token) { req.session.message = 'Invalid verification link.'; return res.redirect('/login'); }
|
||
|
|
db.get('SELECT * FROM users WHERE verify_token = ?', [token], (err, user) => {
|
||
|
|
if (err || !user) { req.session.message = 'Invalid or expired verification link.'; return res.redirect('/login'); }
|
||
|
|
if (user.email_verified) { req.session.message = 'Your email is already verified. Please log in.'; return res.redirect('/login'); }
|
||
|
|
const now = new Date();
|
||
|
|
const exp = user.verify_token_expires ? new Date(user.verify_token_expires) : null;
|
||
|
|
if (!exp || exp < now) { req.session.message = 'Verification link has expired. Please request a new one.'; return res.redirect(`/verify/resend?email=${encodeURIComponent(user.email || '')}`); }
|
||
|
|
db.run('UPDATE users SET email_verified = 1, verify_token = NULL, verify_token_expires = NULL WHERE id = ?', [user.id], (uErr) => {
|
||
|
|
req.session.message = uErr ? 'Failed to verify email. Try again.' : 'Your email has been verified. You can now log in.';
|
||
|
|
return res.redirect('/login');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
app.get('/verify/resend', (req, res) => {
|
||
|
|
const email = (req.query && req.query.email) ? String(req.query.email) : '';
|
||
|
|
res.render('verify_resend', { email });
|
||
|
|
});
|
||
|
|
|
||
|
|
app.post('/verify/resend', (req, res) => {
|
||
|
|
let { email } = req.body;
|
||
|
|
email = (email || '').trim().toLowerCase();
|
||
|
|
if (!email) { req.session.message = 'Please enter your email.'; return res.redirect('/verify/resend'); }
|
||
|
|
db.get('SELECT * FROM users WHERE email = ?', [email], (err, user) => {
|
||
|
|
if (err) { req.session.message = 'Something went wrong.'; return res.redirect(`/verify/resend?email=${encodeURIComponent(email)}`); }
|
||
|
|
if (!user) { req.session.message = 'If an account exists, a verification email has been sent.'; return res.redirect('/login'); }
|
||
|
|
if (user.email_verified) { req.session.message = 'Your email is already verified. Please log in.'; return res.redirect('/login'); }
|
||
|
|
const token = crypto.randomBytes(32).toString('hex');
|
||
|
|
const expires = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
|
||
|
|
db.run('UPDATE users SET verify_token = ?, verify_token_expires = ? WHERE id = ?', [token, expires, user.id], async (uErr) => {
|
||
|
|
if (uErr) { req.session.message = 'Could not generate verification link. Try again later.'; return res.redirect(`/verify/resend?email=${encodeURIComponent(email)}`); }
|
||
|
|
try { await sendVerificationEmail(email, token, user.name || ''); } catch (e) { console.error('Failed to send verification email:', e.message); }
|
||
|
|
req.session.message = 'Verification email sent. Please check your inbox.'; return res.redirect('/login');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|