first commit: Complete phishing test management panel with Node.js backend and React frontend

This commit is contained in:
salvacybersec
2025-11-10 17:00:40 +03:00
commit 19e551f33b
77 changed files with 6677 additions and 0 deletions

View File

@@ -0,0 +1,119 @@
const bcrypt = require('bcrypt');
const { AdminUser } = require('../models');
const logger = require('../config/logger');
// Login
exports.login = async (req, res, next) => {
try {
const { username, password } = req.body;
// Find admin user
const admin = await AdminUser.findOne({ where: { username } });
if (!admin) {
logger.warn(`Login attempt with invalid username: ${username}`);
return res.status(401).json({
success: false,
error: 'Invalid username or password',
});
}
// Verify password
const isValidPassword = await bcrypt.compare(password, admin.password_hash);
if (!isValidPassword) {
logger.warn(`Failed login attempt for user: ${username}`);
return res.status(401).json({
success: false,
error: 'Invalid username or password',
});
}
// Update last login
await admin.update({ last_login: new Date() });
// Create session
req.session.userId = admin.id;
req.session.username = admin.username;
req.session.isAdmin = true;
logger.info(`User logged in successfully: ${username}`);
res.json({
success: true,
message: 'Login successful',
user: {
id: admin.id,
username: admin.username,
},
});
} catch (error) {
next(error);
}
};
// Logout
exports.logout = async (req, res, next) => {
try {
const username = req.session.username;
req.session.destroy((err) => {
if (err) {
logger.error('Logout error:', err);
return next(err);
}
logger.info(`User logged out: ${username}`);
res.json({
success: true,
message: 'Logout successful',
});
});
} catch (error) {
next(error);
}
};
// Check authentication status
exports.checkAuth = async (req, res) => {
if (req.session && req.session.userId) {
res.json({
success: true,
authenticated: true,
user: {
id: req.session.userId,
username: req.session.username,
},
});
} else {
res.json({
success: true,
authenticated: false,
});
}
};
// Get current user info
exports.me = async (req, res, next) => {
try {
const admin = await AdminUser.findByPk(req.session.userId, {
attributes: ['id', 'username', 'last_login', 'created_at'],
});
if (!admin) {
return res.status(404).json({
success: false,
error: 'User not found',
});
}
res.json({
success: true,
data: admin,
});
} catch (error) {
next(error);
}
};

View File

@@ -0,0 +1,225 @@
const { Company, TrackingToken, sequelize } = require('../models');
const logger = require('../config/logger');
// Get all companies
exports.getAllCompanies = async (req, res, next) => {
try {
const companies = await Company.findAll({
order: [['created_at', 'DESC']],
});
res.json({
success: true,
data: companies,
count: companies.length,
});
} catch (error) {
next(error);
}
};
// Get company by ID
exports.getCompanyById = async (req, res, next) => {
try {
const { id } = req.params;
const company = await Company.findByPk(id);
if (!company) {
return res.status(404).json({
success: false,
error: 'Company not found',
});
}
res.json({
success: true,
data: company,
});
} catch (error) {
next(error);
}
};
// Create new company
exports.createCompany = async (req, res, next) => {
try {
const { name, description, logo_url, industry } = req.body;
const company = await Company.create({
name,
description,
logo_url,
industry,
});
logger.info(`Company created: ${name} (ID: ${company.id})`);
res.status(201).json({
success: true,
message: 'Company created successfully',
data: company,
});
} catch (error) {
next(error);
}
};
// Update company
exports.updateCompany = async (req, res, next) => {
try {
const { id } = req.params;
const { name, description, logo_url, industry, active } = req.body;
const company = await Company.findByPk(id);
if (!company) {
return res.status(404).json({
success: false,
error: 'Company not found',
});
}
await company.update({
name: name || company.name,
description: description !== undefined ? description : company.description,
logo_url: logo_url !== undefined ? logo_url : company.logo_url,
industry: industry || company.industry,
active: active !== undefined ? active : company.active,
});
logger.info(`Company updated: ${company.name} (ID: ${id})`);
res.json({
success: true,
message: 'Company updated successfully',
data: company,
});
} catch (error) {
next(error);
}
};
// Delete company
exports.deleteCompany = async (req, res, next) => {
try {
const { id } = req.params;
const company = await Company.findByPk(id);
if (!company) {
return res.status(404).json({
success: false,
error: 'Company not found',
});
}
const companyName = company.name;
await company.destroy();
logger.info(`Company deleted: ${companyName} (ID: ${id})`);
res.json({
success: true,
message: 'Company deleted successfully',
});
} catch (error) {
next(error);
}
};
// Get company tokens
exports.getCompanyTokens = async (req, res, next) => {
try {
const { id } = req.params;
const { limit = 50, offset = 0 } = req.query;
const company = await Company.findByPk(id);
if (!company) {
return res.status(404).json({
success: false,
error: 'Company not found',
});
}
const tokens = await TrackingToken.findAll({
where: { company_id: id },
order: [['created_at', 'DESC']],
limit: parseInt(limit),
offset: parseInt(offset),
});
const total = await TrackingToken.count({ where: { company_id: id } });
res.json({
success: true,
data: tokens,
pagination: {
total,
limit: parseInt(limit),
offset: parseInt(offset),
hasMore: parseInt(offset) + parseInt(limit) < total,
},
});
} catch (error) {
next(error);
}
};
// Get company stats
exports.getCompanyStats = async (req, res, next) => {
try {
const { id } = req.params;
const company = await Company.findByPk(id);
if (!company) {
return res.status(404).json({
success: false,
error: 'Company not found',
});
}
// Get detailed stats
const stats = await sequelize.query(
`
SELECT
COUNT(*) as total_tokens,
SUM(CASE WHEN mail_sent = 1 THEN 1 ELSE 0 END) as mails_sent,
SUM(CASE WHEN clicked = 1 THEN 1 ELSE 0 END) as tokens_clicked,
SUM(click_count) as total_clicks,
MAX(last_click_at) as last_activity
FROM tracking_tokens
WHERE company_id = ?
`,
{
replacements: [id],
type: sequelize.QueryTypes.SELECT,
}
);
const result = stats[0];
const clickRate = result.total_tokens > 0
? ((result.tokens_clicked / result.total_tokens) * 100).toFixed(2)
: 0;
res.json({
success: true,
data: {
company,
stats: {
total_tokens: parseInt(result.total_tokens) || 0,
mails_sent: parseInt(result.mails_sent) || 0,
tokens_clicked: parseInt(result.tokens_clicked) || 0,
total_clicks: parseInt(result.total_clicks) || 0,
click_rate: parseFloat(clickRate),
last_activity: result.last_activity,
},
},
});
} catch (error) {
next(error);
}
};

View File

@@ -0,0 +1,127 @@
const { Settings } = require('../models');
const mailService = require('../services/mail.service');
const telegramService = require('../services/telegram.service');
// Get all settings
exports.getAllSettings = async (req, res, next) => {
try {
const settings = await Settings.findAll();
// Hide sensitive values
const sanitized = settings.map(s => ({
...s.toJSON(),
value: s.is_encrypted ? '********' : s.value,
}));
res.json({
success: true,
data: sanitized,
});
} catch (error) {
next(error);
}
};
// Update Gmail settings
exports.updateGmailSettings = async (req, res, next) => {
try {
const { gmail_user, gmail_password, gmail_from_name } = req.body;
if (gmail_user) {
await Settings.upsert({
key: 'gmail_user',
value: gmail_user,
is_encrypted: false,
description: 'Gmail email address',
});
}
if (gmail_password) {
await Settings.upsert({
key: 'gmail_password',
value: gmail_password,
is_encrypted: true,
description: 'Gmail App Password',
});
}
if (gmail_from_name) {
await Settings.upsert({
key: 'gmail_from_name',
value: gmail_from_name,
is_encrypted: false,
description: 'Sender name for emails',
});
}
res.json({
success: true,
message: 'Gmail settings updated successfully',
});
} catch (error) {
next(error);
}
};
// Update Telegram settings
exports.updateTelegramSettings = async (req, res, next) => {
try {
const { telegram_bot_token, telegram_chat_id } = req.body;
if (telegram_bot_token) {
await Settings.upsert({
key: 'telegram_bot_token',
value: telegram_bot_token,
is_encrypted: true,
description: 'Telegram Bot Token',
});
}
if (telegram_chat_id) {
await Settings.upsert({
key: 'telegram_chat_id',
value: telegram_chat_id,
is_encrypted: false,
description: 'Telegram Chat ID',
});
}
res.json({
success: true,
message: 'Telegram settings updated successfully',
});
} catch (error) {
next(error);
}
};
// Test Gmail connection
exports.testGmail = async (req, res, next) => {
try {
const result = await mailService.testConnection();
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
error: error.message,
});
}
};
// Test Telegram connection
exports.testTelegram = async (req, res, next) => {
try {
const result = await telegramService.sendTestMessage();
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
error: error.message,
});
}
};
module.exports = exports;

View File

@@ -0,0 +1,103 @@
const { Company, TrackingToken, ClickLog, sequelize } = require('../models');
// Dashboard stats
exports.getDashboardStats = async (req, res, next) => {
try {
// Get overall stats
const totalCompanies = await Company.count();
const totalTokens = await TrackingToken.count();
const clickedTokens = await TrackingToken.count({ where: { clicked: true } });
const totalClicks = await TrackingToken.sum('click_count') || 0;
const clickRate = totalTokens > 0 ? ((clickedTokens / totalTokens) * 100).toFixed(2) : 0;
// Get today's activity
const today = new Date();
today.setHours(0, 0, 0, 0);
const todayClicks = await ClickLog.count({
where: {
clicked_at: {
[sequelize.Sequelize.Op.gte]: today,
},
},
});
// Get company-based summary
const companyStats = await Company.findAll({
attributes: ['id', 'name', 'industry', 'total_tokens', 'total_clicks', 'click_rate'],
order: [['total_clicks', 'DESC']],
limit: 10,
});
res.json({
success: true,
data: {
overview: {
total_companies: totalCompanies,
total_tokens: totalTokens,
clicked_tokens: clickedTokens,
total_clicks: parseInt(totalClicks),
click_rate: parseFloat(clickRate),
today_clicks: todayClicks,
},
top_companies: companyStats,
},
});
} catch (error) {
next(error);
}
};
// Recent clicks
exports.getRecentClicks = async (req, res, next) => {
try {
const { limit = 20 } = req.query;
const clicks = await ClickLog.findAll({
include: [
{
model: TrackingToken,
as: 'token',
attributes: ['target_email', 'employee_name', 'company_id'],
include: [
{
model: Company,
as: 'company',
attributes: ['name', 'industry'],
},
],
},
],
order: [['clicked_at', 'DESC']],
limit: parseInt(limit),
});
res.json({
success: true,
data: clicks,
});
} catch (error) {
next(error);
}
};
// Company-based stats for charts
exports.getCompanyBasedStats = async (req, res, next) => {
try {
const companies = await Company.findAll({
attributes: ['id', 'name', 'total_tokens', 'total_clicks', 'click_rate'],
order: [['name', 'ASC']],
});
res.json({
success: true,
data: companies,
});
} catch (error) {
next(error);
}
};
module.exports = exports;

View File

@@ -0,0 +1,72 @@
const { MailTemplate } = require('../models');
const mailService = require('../services/mail.service');
// Get all templates
exports.getAllTemplates = async (req, res, next) => {
try {
const templates = await MailTemplate.findAll({
order: [['created_at', 'DESC']],
});
res.json({
success: true,
data: templates,
});
} catch (error) {
next(error);
}
};
// Get template by type
exports.getTemplateByType = async (req, res, next) => {
try {
const { type } = req.params;
const template = await MailTemplate.findOne({
where: { template_type: type },
});
if (!template) {
return res.status(404).json({
success: false,
error: 'Template not found',
});
}
res.json({
success: true,
data: template,
});
} catch (error) {
next(error);
}
};
// Preview template
exports.previewTemplate = async (req, res, next) => {
try {
const { template_html, company_name, employee_name } = req.body;
const data = {
company_name: company_name || 'Örnek Şirket',
employee_name: employee_name || null,
tracking_url: 'https://example.com/t/preview-token',
current_date: new Date().toLocaleDateString('tr-TR'),
current_year: new Date().getFullYear(),
};
const rendered = mailService.renderTemplate(template_html, data);
res.json({
success: true,
data: {
rendered_html: rendered,
},
});
} catch (error) {
next(error);
}
};
module.exports = exports;

View File

@@ -0,0 +1,239 @@
const { TrackingToken, Company, ClickLog } = require('../models');
const tokenService = require('../services/token.service');
const logger = require('../config/logger');
// Get all tokens
exports.getAllTokens = async (req, res, next) => {
try {
const { company_id, limit = 50, offset = 0 } = req.query;
const where = {};
if (company_id) {
where.company_id = company_id;
}
const tokens = await TrackingToken.findAll({
where,
include: [{ model: Company, as: 'company', attributes: ['id', 'name', 'industry'] }],
order: [['created_at', 'DESC']],
limit: parseInt(limit),
offset: parseInt(offset),
});
const total = await TrackingToken.count({ where });
res.json({
success: true,
data: tokens,
pagination: {
total,
limit: parseInt(limit),
offset: parseInt(offset),
hasMore: parseInt(offset) + parseInt(limit) < total,
},
});
} catch (error) {
next(error);
}
};
// Get token by ID
exports.getTokenById = async (req, res, next) => {
try {
const { id } = req.params;
const token = await TrackingToken.findByPk(id, {
include: [{ model: Company, as: 'company' }],
});
if (!token) {
return res.status(404).json({
success: false,
error: 'Token not found',
});
}
res.json({
success: true,
data: token,
});
} catch (error) {
next(error);
}
};
// Create token (without sending mail)
exports.createToken = async (req, res, next) => {
try {
const { company_id, target_email, employee_name, template_type } = req.body;
const token = await tokenService.createToken({
company_id,
target_email,
employee_name,
template_type,
});
const trackingUrl = `${process.env.BASE_URL}/t/${token.token}`;
res.status(201).json({
success: true,
message: 'Token created successfully',
data: {
...token.toJSON(),
tracking_url: trackingUrl,
},
});
} catch (error) {
next(error);
}
};
// Create token and send mail
exports.createAndSendToken = async (req, res, next) => {
try {
const { company_id, target_email, employee_name, template_type } = req.body;
// Create token
const token = await tokenService.createToken({
company_id,
target_email,
employee_name,
template_type,
});
// Send mail
try {
await tokenService.sendMail(token.id);
} catch (mailError) {
logger.error('Failed to send mail:', mailError);
return res.status(500).json({
success: false,
error: 'Token created but failed to send mail',
details: mailError.message,
token_id: token.id,
});
}
const trackingUrl = `${process.env.BASE_URL}/t/${token.token}`;
res.status(201).json({
success: true,
message: 'Token created and mail sent successfully',
data: {
...token.toJSON(),
tracking_url: trackingUrl,
mail_sent: true,
},
});
} catch (error) {
next(error);
}
};
// Send mail for existing token
exports.sendTokenMail = async (req, res, next) => {
try {
const { id } = req.params;
await tokenService.sendMail(id);
res.json({
success: true,
message: 'Mail sent successfully',
});
} catch (error) {
next(error);
}
};
// Update token
exports.updateToken = async (req, res, next) => {
try {
const { id } = req.params;
const { notes } = req.body;
const token = await TrackingToken.findByPk(id);
if (!token) {
return res.status(404).json({
success: false,
error: 'Token not found',
});
}
await token.update({ notes });
logger.info(`Token updated: ${id}`);
res.json({
success: true,
message: 'Token updated successfully',
data: token,
});
} catch (error) {
next(error);
}
};
// Delete token
exports.deleteToken = async (req, res, next) => {
try {
const { id } = req.params;
const token = await TrackingToken.findByPk(id);
if (!token) {
return res.status(404).json({
success: false,
error: 'Token not found',
});
}
const companyId = token.company_id;
await token.destroy();
// Update company stats
await tokenService.updateCompanyStats(companyId);
logger.info(`Token deleted: ${id}`);
res.json({
success: true,
message: 'Token deleted successfully',
});
} catch (error) {
next(error);
}
};
// Get token click logs
exports.getTokenClicks = async (req, res, next) => {
try {
const { id } = req.params;
const token = await TrackingToken.findByPk(id);
if (!token) {
return res.status(404).json({
success: false,
error: 'Token not found',
});
}
const clicks = await ClickLog.findAll({
where: { token_id: id },
order: [['clicked_at', 'DESC']],
});
res.json({
success: true,
data: clicks,
count: clicks.length,
});
} catch (error) {
next(error);
}
};

View File

@@ -0,0 +1,111 @@
const { TrackingToken, ClickLog, Company } = require('../models');
const { getGeoLocation } = require('../utils/geoip');
const { parseUserAgent } = require('../utils/userAgentParser');
const telegramService = require('../services/telegram.service');
const tokenService = require('../services/token.service');
const logger = require('../config/logger');
exports.trackClick = async (req, res, next) => {
try {
const { token } = req.params;
// Find token
const trackingToken = await TrackingToken.findOne({
where: { token },
include: [{ model: Company, as: 'company' }],
});
if (!trackingToken) {
logger.warn(`Invalid token accessed: ${token}`);
return res.redirect(process.env.BASE_URL || 'https://google.com');
}
// Get IP address
const ipAddress = req.headers['x-forwarded-for']?.split(',')[0].trim()
|| req.connection.remoteAddress
|| req.socket.remoteAddress
|| req.ip;
// Get user agent
const userAgent = req.headers['user-agent'] || '';
const referer = req.headers['referer'] || req.headers['referrer'] || null;
// Parse geo location
const geoData = getGeoLocation(ipAddress);
// Parse user agent
const uaData = parseUserAgent(userAgent);
// Create click log
const clickLog = await ClickLog.create({
token_id: trackingToken.id,
ip_address: ipAddress,
country: geoData.country,
city: geoData.city,
latitude: geoData.latitude,
longitude: geoData.longitude,
user_agent: userAgent,
browser: uaData.browser,
os: uaData.os,
device: uaData.device,
referer,
});
// Update token stats
const isFirstClick = !trackingToken.clicked;
await trackingToken.update({
clicked: true,
click_count: trackingToken.click_count + 1,
first_click_at: isFirstClick ? new Date() : trackingToken.first_click_at,
last_click_at: new Date(),
});
// Update company stats
await tokenService.updateCompanyStats(trackingToken.company_id);
// Get updated company for Telegram notification
const company = await Company.findByPk(trackingToken.company_id);
// Send Telegram notification
try {
const timestamp = new Date().toLocaleString('tr-TR', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
await telegramService.sendNotification({
companyName: company.name,
targetEmail: trackingToken.target_email,
employeeName: trackingToken.employee_name,
ipAddress,
country: geoData.country,
city: geoData.city,
browser: uaData.browser,
os: uaData.os,
timestamp,
clickCount: trackingToken.click_count + 1,
companyTotalClicks: company.total_clicks,
companyTotalTokens: company.total_tokens,
});
await clickLog.update({ telegram_sent: true });
} catch (telegramError) {
logger.error('Telegram notification failed:', telegramError);
// Don't fail the request if Telegram fails
}
logger.info(`Click tracked: ${token} from ${ipAddress} (${geoData.city}, ${geoData.country})`);
// Redirect to landing page
res.redirect('/landing.html');
} catch (error) {
logger.error('Tracking error:', error);
// Even on error, redirect to something
res.redirect(process.env.BASE_URL || 'https://google.com');
}
};