Mail mich in den Tod

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
DeathKaioken 2026-05-04 16:44:19 +02:00
parent 0bb230cf66
commit 9f53ffbbdd
8 changed files with 440 additions and 0 deletions

View File

@ -0,0 +1,87 @@
const MailTemplateService = require('../../services/template/MailTemplateService');
class MailTemplatesController {
static _currentUserId(req) {
return req.user?.userId ?? req.user?.id ?? null;
}
static async list(req, res) {
try {
const data = await MailTemplateService.list(req.query || {});
return res.json({ success: true, data });
} catch (error) {
return res.status(error?.status || 500).json({ success: false, message: error.message || 'Failed to list mail templates' });
}
}
static async getById(req, res) {
try {
const data = await MailTemplateService.getById(req.params.id);
if (!data) {
return res.status(404).json({ success: false, message: 'Mail template not found' });
}
return res.json({ success: true, data });
} catch (error) {
return res.status(error?.status || 500).json({ success: false, message: error.message || 'Failed to load mail template' });
}
}
static async create(req, res) {
try {
const data = await MailTemplateService.create(req.body || {}, MailTemplatesController._currentUserId(req));
return res.status(201).json({ success: true, data });
} catch (error) {
return res.status(error?.status || 500).json({ success: false, message: error.message || 'Failed to create mail template' });
}
}
static async update(req, res) {
try {
const data = await MailTemplateService.update(req.params.id, req.body || {}, MailTemplatesController._currentUserId(req));
if (!data) {
return res.status(404).json({ success: false, message: 'Mail template not found' });
}
return res.json({ success: true, data });
} catch (error) {
return res.status(error?.status || 500).json({ success: false, message: error.message || 'Failed to update mail template' });
}
}
static async activate(req, res) {
try {
const data = await MailTemplateService.activate(req.params.id, MailTemplatesController._currentUserId(req));
if (!data) {
return res.status(404).json({ success: false, message: 'Mail template not found' });
}
return res.json({ success: true, data });
} catch (error) {
return res.status(error?.status || 500).json({ success: false, message: error.message || 'Failed to activate mail template' });
}
}
static async archive(req, res) {
try {
const data = await MailTemplateService.archive(req.params.id, MailTemplatesController._currentUserId(req));
if (!data) {
return res.status(404).json({ success: false, message: 'Mail template not found' });
}
return res.json({ success: true, data });
} catch (error) {
return res.status(error?.status || 500).json({ success: false, message: error.message || 'Failed to archive mail template' });
}
}
static async remove(req, res) {
try {
const deleted = await MailTemplateService.remove(req.params.id);
if (!deleted) {
return res.status(404).json({ success: false, message: 'Mail template not found' });
}
return res.json({ success: true });
} catch (error) {
return res.status(error?.status || 500).json({ success: false, message: error.message || 'Failed to delete mail template' });
}
}
}
module.exports = MailTemplatesController;

View File

@ -451,6 +451,30 @@ const createDatabase = async () => {
`);
console.log('✅ Document templates table created/verified');
await connection.query(`
CREATE TABLE IF NOT EXISTS mail_templates (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
template_type VARCHAR(100) NOT NULL,
name VARCHAR(255) NOT NULL,
subject VARCHAR(255) NULL,
html_content LONGTEXT NOT NULL,
is_active TINYINT(1) NOT NULL DEFAULT 0,
is_archived TINYINT(1) NOT NULL DEFAULT 0,
archived_at TIMESTAMP NULL,
created_by INT NULL,
updated_by INT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_mail_templates_created_by FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT fk_mail_templates_updated_by FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE,
INDEX idx_mail_templates_type (template_type),
INDEX idx_mail_templates_active (is_active),
INDEX idx_mail_templates_archived (is_archived),
INDEX idx_mail_templates_type_active (template_type, is_active)
);
`);
console.log('✅ Mail templates table created/verified');
// Ensure enum includes 'abo' on existing schemas
try {
await connection.query(`
@ -1806,6 +1830,10 @@ const createDatabase = async () => {
await ensureIndex(connection, 'rate_limit', 'idx_rate_limit_rate_key', 'rate_key');
await ensureIndex(connection, 'document_templates', 'idx_document_templates_user_type', 'user_type');
await ensureIndex(connection, 'document_templates', 'idx_document_templates_state_user_type', 'state, user_type');
await ensureIndex(connection, 'mail_templates', 'idx_mail_templates_type', 'template_type');
await ensureIndex(connection, 'mail_templates', 'idx_mail_templates_active', 'is_active');
await ensureIndex(connection, 'mail_templates', 'idx_mail_templates_archived', 'is_archived');
await ensureIndex(connection, 'mail_templates', 'idx_mail_templates_type_active', 'template_type, is_active');
await ensureIndex(connection, 'company_stamps', 'idx_company_stamps_company', 'company_id');
await ensureIndex(connection, 'company_stamps', 'idx_company_stamps_active', 'is_active');
console.log('🚀 Performance indexes created/verified');

View File

@ -0,0 +1,192 @@
const db = require('../../database/database');
class MailTemplateRepository {
_mapRow(row) {
if (!row) return null;
return {
id: Number(row.id),
template_type: row.template_type,
name: row.name,
subject: row.subject,
html_content: row.html_content,
is_active: row.is_active === 1 || row.is_active === true,
is_archived: row.is_archived === 1 || row.is_archived === true,
archived_at: row.archived_at,
created_by: row.created_by,
updated_by: row.updated_by,
created_at: row.created_at,
updated_at: row.updated_at,
};
}
async list({ includeArchived = false, templateType } = {}) {
const where = [];
const params = [];
if (!includeArchived) {
where.push('is_archived = 0');
}
if (templateType) {
where.push('template_type = ?');
params.push(templateType);
}
const whereClause = where.length ? `WHERE ${where.join(' AND ')}` : '';
const [rows] = await db.query(
`SELECT *
FROM mail_templates
${whereClause}
ORDER BY template_type ASC, is_active DESC, updated_at DESC`,
params
);
return (rows || []).map((row) => this._mapRow(row));
}
async getById(id) {
const [rows] = await db.query(
`SELECT *
FROM mail_templates
WHERE id = ?
LIMIT 1`,
[id]
);
return this._mapRow(rows?.[0]);
}
async create(payload) {
const [result] = await db.query(
`INSERT INTO mail_templates
(template_type, name, subject, html_content, is_active, is_archived, created_by, updated_by)
VALUES (?, ?, ?, ?, 0, 0, ?, ?)`,
[
payload.template_type,
payload.name,
payload.subject || null,
payload.html_content,
payload.userId || null,
payload.userId || null,
]
);
return this.getById(result.insertId);
}
async update(id, payload) {
const fields = [];
const values = [];
if (payload.template_type !== undefined) {
fields.push('template_type = ?');
values.push(payload.template_type);
}
if (payload.name !== undefined) {
fields.push('name = ?');
values.push(payload.name);
}
if (payload.subject !== undefined) {
fields.push('subject = ?');
values.push(payload.subject || null);
}
if (payload.html_content !== undefined) {
fields.push('html_content = ?');
values.push(payload.html_content);
}
fields.push('updated_by = ?');
values.push(payload.userId || null);
if (!fields.length) return this.getById(id);
values.push(id);
const [result] = await db.query(
`UPDATE mail_templates
SET ${fields.join(', ')}, updated_at = NOW()
WHERE id = ?`,
values
);
if (!result?.affectedRows) return null;
return this.getById(id);
}
async activate(id, userId) {
const conn = await db.getConnection();
try {
await conn.beginTransaction();
const [targetRows] = await conn.query(
`SELECT id, template_type, is_archived
FROM mail_templates
WHERE id = ?
LIMIT 1
FOR UPDATE`,
[id]
);
const target = targetRows?.[0];
if (!target) {
await conn.rollback();
return null;
}
if (target.is_archived === 1 || target.is_archived === true) {
const error = new Error('Archived templates cannot be activated');
error.status = 400;
throw error;
}
await conn.query(
`UPDATE mail_templates
SET is_active = 0, updated_by = ?, updated_at = NOW()
WHERE template_type = ?`,
[userId || null, target.template_type]
);
await conn.query(
`UPDATE mail_templates
SET is_active = 1,
is_archived = 0,
archived_at = NULL,
updated_by = ?,
updated_at = NOW()
WHERE id = ?`,
[userId || null, id]
);
await conn.commit();
} catch (error) {
await conn.rollback();
throw error;
} finally {
conn.release();
}
return this.getById(id);
}
async archive(id, userId) {
const [result] = await db.query(
`UPDATE mail_templates
SET is_archived = 1,
is_active = 0,
archived_at = NOW(),
updated_by = ?,
updated_at = NOW()
WHERE id = ?`,
[userId || null, id]
);
if (!result?.affectedRows) return null;
return this.getById(id);
}
async delete(id) {
const [result] = await db.query('DELETE FROM mail_templates WHERE id = ?', [id]);
return Number(result?.affectedRows || 0) > 0;
}
}
module.exports = new MailTemplateRepository();

View File

@ -11,6 +11,7 @@ const AffiliateController = require('../controller/affiliate/AffiliateController
const NewsController = require('../controller/news/NewsController');
const PoolController = require('../controller/pool/PoolController');
const I18nPreferencesController = require('../controller/admin/I18nPreferencesController');
const MailTemplatesController = require('../controller/admin/MailTemplatesController');
// Helper middlewares for company-stamp
function forceCompanyForAdmin(req, res, next) {
@ -22,6 +23,7 @@ function forceCompanyForAdmin(req, res, next) {
// DELETE /admin/user/:id (moved from routes/admin.js)
router.delete('/admin/user/:id', authMiddleware, adminOnly, AdminUserController.deleteUser);
router.delete('/admin/mail-templates/:id', authMiddleware, adminOnly, MailTemplatesController.remove);
// DELETE /document-templates/:id (moved from routes/documentTemplates.js)
router.delete('/document-templates/:id', authMiddleware, DocumentTemplateController.deleteTemplate);

View File

@ -32,6 +32,7 @@ const DashboardPlatformsController = require('../controller/admin/DashboardPlatf
const ShippingFeesController = require('../controller/admin/ShippingFeesController');
const LoginController = require('../controller/login/LoginController');
const I18nPreferencesController = require('../controller/admin/I18nPreferencesController');
const MailTemplatesController = require('../controller/admin/MailTemplatesController');
const AUTH_VALIDATE_ROUTE_PATH = '/auth/validate';
@ -68,6 +69,8 @@ router.get('/verify-password-reset', (req, res) => { /* Note: was moved from Pas
// admin.js GETs
router.get('/admin/user-stats', authMiddleware, adminOnly, AdminUserController.getUserStats);
router.get('/admin/user-list', authMiddleware, adminOnly, AdminUserController.getUserList);
router.get('/admin/mail-templates', authMiddleware, adminOnly, MailTemplatesController.list);
router.get('/admin/mail-templates/:id', authMiddleware, adminOnly, MailTemplatesController.getById);
router.get('/admin/verification-pending-users', authMiddleware, adminOnly, AdminUserController.getVerificationPendingUsers);
router.get('/admin/unverified-users', authMiddleware, adminOnly, AdminUserController.getUnverifiedUsers);
router.get('/admin/user/:id/documents', authMiddleware, adminOnly, UserDocumentController.getAllDocumentsForUser);

View File

@ -15,6 +15,7 @@ const NewsController = require('../controller/news/NewsController');
const AbonemmentController = require('../controller/abonemments/AbonemmentController');
const InvoiceController = require('../controller/invoice/InvoiceController');
const DashboardPlatformsController = require('../controller/admin/DashboardPlatformsController');
const MailTemplatesController = require('../controller/admin/MailTemplatesController');
const multer = require('multer');
const upload = multer({ storage: multer.memoryStorage() });
@ -36,6 +37,9 @@ router.patch('/company-stamps/:id/activate', authMiddleware, adminOnly, forceCom
// Admin user management PATCH routes
router.patch('/admin/archive-user/:id', authMiddleware, adminOnly, AdminUserController.archiveUser);
router.patch('/admin/unarchive-user/:id', authMiddleware, adminOnly, AdminUserController.unarchiveUser);
router.patch('/admin/mail-templates/:id', authMiddleware, adminOnly, MailTemplatesController.update);
router.patch('/admin/mail-templates/:id/activate', authMiddleware, adminOnly, MailTemplatesController.activate);
router.patch('/admin/mail-templates/:id/archive', authMiddleware, adminOnly, MailTemplatesController.archive);
router.patch('/admin/update-verification/:id', authMiddleware, adminOnly, AdminUserController.updateUserVerification);
router.patch('/admin/update-user-profile/:id', authMiddleware, adminOnly, AdminUserController.updateUserProfile);
router.patch('/admin/update-user-status/:id', authMiddleware, adminOnly, AdminUserController.updateUserStatus);

View File

@ -33,6 +33,7 @@ const InvoiceController = require('../controller/invoice/InvoiceController'); //
const DevManagementController = require('../controller/dev/DevManagementController');
const DashboardPlatformsController = require('../controller/admin/DashboardPlatformsController');
const I18nPreferencesController = require('../controller/admin/I18nPreferencesController');
const MailTemplatesController = require('../controller/admin/MailTemplatesController');
const multer = require('multer');
const upload = multer({ storage: multer.memoryStorage() });
@ -83,6 +84,7 @@ router.post('/profile/company/complete', authMiddleware, CompanyProfileControlle
// Admin POSTs (moved from routes/admin.js)
router.post('/admin/verify-user/:id', authMiddleware, adminOnly, AdminUserController.verifyUser);
router.post('/admin/mail-templates', authMiddleware, adminOnly, MailTemplatesController.create);
router.post('/admin/i18n/preferences', authMiddleware, adminOnly, I18nPreferencesController.post);
router.post('/i18n/preferences', authMiddleware, adminOnly, I18nPreferencesController.post);
router.post('/i18n/translations', authMiddleware, adminOnly, I18nPreferencesController.upsertTranslations);

View File

@ -0,0 +1,122 @@
const MailTemplateRepository = require('../../repositories/template/MailTemplateRepository');
class MailTemplateService {
_requiredString(value, fieldName) {
const parsed = String(value == null ? '' : value).trim();
if (!parsed) {
const error = new Error(`${fieldName} is required`);
error.status = 400;
throw error;
}
return parsed;
}
_optionalString(value) {
if (value === undefined || value === null) return null;
const parsed = String(value).trim();
return parsed || null;
}
_toBool(value, fallback = false) {
if (value === undefined || value === null) return fallback;
if (typeof value === 'boolean') return value;
const normalized = String(value).trim().toLowerCase();
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
return fallback;
}
async list(query = {}) {
return MailTemplateRepository.list({
includeArchived: this._toBool(query.includeArchived, false),
templateType: this._optionalString(query.templateType),
});
}
async getById(id) {
const numericId = Number(id);
if (!Number.isFinite(numericId) || numericId <= 0) {
const error = new Error('Invalid id');
error.status = 400;
throw error;
}
return MailTemplateRepository.getById(numericId);
}
async create(payload = {}, userId = null) {
const template_type = this._requiredString(payload.template_type, 'template_type');
const name = this._requiredString(payload.name, 'name');
const html_content = this._requiredString(payload.html_content, 'html_content');
return MailTemplateRepository.create({
template_type,
name,
subject: this._optionalString(payload.subject),
html_content,
userId,
});
}
async update(id, payload = {}, userId = null) {
const numericId = Number(id);
if (!Number.isFinite(numericId) || numericId <= 0) {
const error = new Error('Invalid id');
error.status = 400;
throw error;
}
const updatePayload = {};
if (Object.prototype.hasOwnProperty.call(payload, 'template_type')) {
updatePayload.template_type = this._requiredString(payload.template_type, 'template_type');
}
if (Object.prototype.hasOwnProperty.call(payload, 'name')) {
updatePayload.name = this._requiredString(payload.name, 'name');
}
if (Object.prototype.hasOwnProperty.call(payload, 'subject')) {
updatePayload.subject = this._optionalString(payload.subject);
}
if (Object.prototype.hasOwnProperty.call(payload, 'html_content')) {
updatePayload.html_content = this._requiredString(payload.html_content, 'html_content');
}
updatePayload.userId = userId;
return MailTemplateRepository.update(numericId, updatePayload);
}
async activate(id, userId = null) {
const numericId = Number(id);
if (!Number.isFinite(numericId) || numericId <= 0) {
const error = new Error('Invalid id');
error.status = 400;
throw error;
}
return MailTemplateRepository.activate(numericId, userId);
}
async archive(id, userId = null) {
const numericId = Number(id);
if (!Number.isFinite(numericId) || numericId <= 0) {
const error = new Error('Invalid id');
error.status = 400;
throw error;
}
return MailTemplateRepository.archive(numericId, userId);
}
async remove(id) {
const numericId = Number(id);
if (!Number.isFinite(numericId) || numericId <= 0) {
const error = new Error('Invalid id');
error.status = 400;
throw error;
}
return MailTemplateRepository.delete(numericId);
}
}
module.exports = new MailTemplateService();