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

View File

@ -4,6 +4,16 @@ const { logger } = require('../../middleware/logger');
const repo = new I18nPreferencesRepository();
class I18nPreferencesController {
static _normalizeLanguageCode(value) {
const raw = String(value == null ? '' : value).trim();
if (!raw) return '';
const normalized = raw.replace('_', '-');
const pattern = /^[a-z]{2,3}(-[a-z0-9]{2,8})?$/i;
if (!pattern.test(normalized)) return null;
return normalized.toLowerCase();
}
static _normalizeStringArray(values) {
if (!Array.isArray(values)) return [];
const normalized = values
@ -34,6 +44,132 @@ class I18nPreferencesController {
};
}
static _normalizeLanguagePayload(body) {
const source = (body?.language && typeof body.language === 'object') ? body.language : body;
const rawLanguageCode = source?.languageCode ?? source?.code ?? source?.lang;
const hasAnyLanguageField = rawLanguageCode !== undefined
|| source?.label !== undefined
|| source?.name !== undefined
|| source?.isEnabled !== undefined
|| source?.isCustom !== undefined;
if (!hasAnyLanguageField) return null;
const languageCode = I18nPreferencesController._normalizeLanguageCode(rawLanguageCode);
if (languageCode === null || !languageCode) {
throw new Error('Invalid language payload: languageCode is required');
}
const label = String(source?.label ?? source?.name ?? languageCode.toUpperCase()).trim();
const toBool = (v, fallback) => {
if (v === undefined || v === null) return fallback;
if (typeof v === 'boolean') return v;
const n = String(v).trim().toLowerCase();
if (['1', 'true', 'yes', 'on'].includes(n)) return true;
if (['0', 'false', 'no', 'off'].includes(n)) return false;
return fallback;
};
return {
languageCode,
label,
isEnabled: toBool(source?.isEnabled, true),
isCustom: toBool(source?.isCustom, true),
};
}
static _normalizeTranslationsPayload(body, fallbackLanguageCode) {
const raw = body?.translations ?? body?.translationOverrides ?? body?.entries;
if (raw == null) return [];
const normalized = [];
if (Array.isArray(raw)) {
for (const item of raw) {
const languageCode = I18nPreferencesController._normalizeLanguageCode(
item?.languageCode ?? item?.lang ?? fallbackLanguageCode
);
const namespace = String(item?.namespace ?? '').trim();
const key = String(item?.key ?? item?.t_key ?? '').trim();
const value = item?.value ?? item?.t_value;
if (!languageCode || !namespace || !key || value === undefined) continue;
normalized.push({
languageCode,
namespace,
key,
value: String(value),
isCustom: item?.isCustom,
});
}
return normalized;
}
if (typeof raw === 'object') {
for (const [namespace, kv] of Object.entries(raw)) {
if (!kv || typeof kv !== 'object') continue;
for (const [key, value] of Object.entries(kv)) {
const languageCode = I18nPreferencesController._normalizeLanguageCode(fallbackLanguageCode);
if (!languageCode || !namespace || !key || value === undefined) continue;
normalized.push({
languageCode,
namespace: String(namespace).trim(),
key: String(key).trim(),
value: String(value),
isCustom: true,
});
}
}
}
return normalized;
}
static _extractPreferencesPayload(body) {
const source = (body?.preferences && typeof body.preferences === 'object')
? body.preferences
: body;
const hasCategories = Object.prototype.hasOwnProperty.call(source || {}, 'categories');
const hasGlobalKeys = Object.prototype.hasOwnProperty.call(source || {}, 'globalKeys');
return {
categories: hasCategories ? I18nPreferencesController._normalizeCategories(source?.categories) : undefined,
globalKeys: hasGlobalKeys ? I18nPreferencesController._normalizeStringArray(source?.globalKeys) : undefined,
};
}
static async _upsertBundle(req, res) {
try {
const userId = req.user?.userId ?? req.user?.id ?? null;
const language = I18nPreferencesController._normalizeLanguagePayload(req.body || {});
const fallbackLanguageCode = language?.languageCode
|| I18nPreferencesController._normalizeLanguageCode(req.body?.languageCode ?? req.body?.lang);
const translations = I18nPreferencesController._normalizeTranslationsPayload(req.body || {}, fallbackLanguageCode);
const extracted = I18nPreferencesController._extractPreferencesPayload(req.body || {});
const result = await repo.upsertBundle({
categories: extracted.categories,
globalKeys: extracted.globalKeys,
language,
translations,
updatedByUserId: userId,
});
return res.status(200).json({
...I18nPreferencesController._buildResponse(result.preferences),
language: result.language || null,
translationsUpserted: result.translationsUpserted || 0,
});
} catch (error) {
if (String(error?.message || '').includes('Invalid language payload')) {
return res.status(400).json({ ok: false, message: error.message });
}
logger.error('i18nPreferences:upsertBundle:failed', { error: error?.message });
return res.status(500).json({ ok: false, message: 'Failed to upsert i18n language data' });
}
}
static async get(req, res) {
try {
const preferences = await repo.get();
@ -45,43 +181,34 @@ class I18nPreferencesController {
}
static async post(req, res) {
try {
const categories = I18nPreferencesController._normalizeCategories(req.body?.categories);
const globalKeys = I18nPreferencesController._normalizeStringArray(req.body?.globalKeys);
const preferences = await repo.upsert({
categories,
globalKeys,
updatedByUserId: req.user?.userId ?? req.user?.id ?? null,
});
return res.status(200).json(I18nPreferencesController._buildResponse(preferences));
} catch (error) {
logger.error('i18nPreferences:post:failed', { error: error?.message });
return res.status(500).json({ ok: false, message: 'Failed to save i18n preferences' });
}
return I18nPreferencesController._upsertBundle(req, res);
}
static async put(req, res) {
try {
const categories = I18nPreferencesController._normalizeCategories(req.body?.categories);
const globalKeys = I18nPreferencesController._normalizeStringArray(req.body?.globalKeys);
const preferences = await repo.upsert({
categories,
globalKeys,
updatedByUserId: req.user?.userId ?? req.user?.id ?? null,
});
return res.status(200).json(I18nPreferencesController._buildResponse(preferences));
} catch (error) {
logger.error('i18nPreferences:put:failed', { error: error?.message });
return res.status(500).json({ ok: false, message: 'Failed to update i18n preferences' });
}
return I18nPreferencesController._upsertBundle(req, res);
}
static async delete(req, res) {
try {
const requestedLanguageCode = I18nPreferencesController._normalizeLanguageCode(req.query?.languageCode);
if (requestedLanguageCode === null) {
return res.status(400).json({ ok: false, message: 'Invalid languageCode query parameter' });
}
if (requestedLanguageCode) {
const result = await repo.deleteLanguageEntries(
requestedLanguageCode,
req.user?.userId ?? req.user?.id ?? null
);
return res.status(200).json({
ok: true,
languageCode: requestedLanguageCode,
deletedRows: result.deletedRows,
touchedTables: result.touchedTables,
});
}
const hasBodyReplacement = req.body && (
Object.prototype.hasOwnProperty.call(req.body, 'categories') ||
Object.prototype.hasOwnProperty.call(req.body, 'globalKeys')
@ -101,6 +228,81 @@ class I18nPreferencesController {
return res.status(500).json({ ok: false, message: 'Failed to delete i18n preferences' });
}
}
static async getTranslations(req, res) {
try {
const languageCodeRaw = req.query?.languageCode ?? req.query?.lang;
const normalizedLanguageCode = languageCodeRaw
? I18nPreferencesController._normalizeLanguageCode(languageCodeRaw)
: '';
if (normalizedLanguageCode === null) {
return res.status(400).json({ ok: false, message: 'Invalid languageCode query parameter' });
}
const namespace = String(req.query?.namespace ?? '').trim() || undefined;
const translations = await repo.listTranslations({
languageCode: normalizedLanguageCode || undefined,
namespace,
});
return res.status(200).json({ ok: true, translations });
} catch (error) {
logger.error('i18nTranslations:get:failed', { error: error?.message });
return res.status(500).json({ ok: false, message: 'Failed to load translations' });
}
}
static async upsertTranslations(req, res) {
try {
const language = I18nPreferencesController._normalizeLanguagePayload(req.body || {});
const fallbackLanguageCode = language?.languageCode
|| I18nPreferencesController._normalizeLanguageCode(req.body?.languageCode ?? req.body?.lang);
const translations = I18nPreferencesController._normalizeTranslationsPayload(req.body || {}, fallbackLanguageCode);
if (!translations.length) {
return res.status(400).json({ ok: false, message: 'No valid translations provided' });
}
const userId = req.user?.userId ?? req.user?.id ?? null;
const result = await repo.upsertTranslations({ translations, updatedByUserId: userId });
if (language) {
await repo.upsertBundle({ language, updatedByUserId: userId });
}
return res.status(200).json({
ok: true,
translationsUpserted: result.upsertedCount || 0,
});
} catch (error) {
if (String(error?.message || '').includes('Invalid language payload')) {
return res.status(400).json({ ok: false, message: error.message });
}
logger.error('i18nTranslations:upsert:failed', { error: error?.message });
return res.status(500).json({ ok: false, message: 'Failed to upsert translations' });
}
}
static async scan(req, res) {
try {
const languageCodeRaw = req.query?.languageCode ?? req.query?.lang ?? req.body?.languageCode ?? req.body?.lang;
const normalizedLanguageCode = languageCodeRaw
? I18nPreferencesController._normalizeLanguageCode(languageCodeRaw)
: '';
if (normalizedLanguageCode === null) {
return res.status(400).json({ ok: false, message: 'Invalid languageCode parameter' });
}
const summary = await repo.getScanSummary({ languageCode: normalizedLanguageCode || undefined });
return res.status(200).json({
ok: true,
...summary,
});
} catch (error) {
logger.error('i18nScan:failed', { error: error?.message });
return res.status(500).json({ ok: false, message: 'Failed to scan i18n data' });
}
}
}
module.exports = I18nPreferencesController;

View File

@ -861,6 +861,50 @@ const createDatabase = async () => {
ADD CONSTRAINT \`i18n_preferences_ibfk_1\` FOREIGN KEY (\`updated_by_user_id\`) REFERENCES \`users\`(\`id\`) ON DELETE SET NULL ON UPDATE CASCADE`
);
// Language metadata used by language-management UI
await connection.query(`
CREATE TABLE IF NOT EXISTS i18n_languages (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
language_code VARCHAR(16) NOT NULL,
label VARCHAR(100) NULL,
is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
is_custom BOOLEAN NOT NULL DEFAULT FALSE,
created_by_user_id INT NULL,
updated_by_user_id INT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT uq_i18n_languages_code UNIQUE (language_code),
FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE,
FOREIGN KEY (updated_by_user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
);
`);
console.log('✅ i18n languages table created/verified');
// Translation overrides/custom values per language
await connection.query(`
CREATE TABLE IF NOT EXISTS i18n_translation_overrides (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
language_code VARCHAR(16) NOT NULL,
namespace VARCHAR(128) NOT NULL,
t_key VARCHAR(255) NOT NULL,
t_value LONGTEXT NOT NULL,
is_custom BOOLEAN NOT NULL DEFAULT TRUE,
created_by_user_id INT NULL,
updated_by_user_id INT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT uq_i18n_translation_overrides UNIQUE (language_code, namespace, t_key),
FOREIGN KEY (language_code) REFERENCES i18n_languages(language_code) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE,
FOREIGN KEY (updated_by_user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
);
`);
console.log('✅ i18n translation overrides table created/verified');
await ensureIndex(connection, 'i18n_translation_overrides', 'idx_i18n_overrides_lang', '`language_code`');
await ensureIndex(connection, 'i18n_translation_overrides', 'idx_i18n_overrides_namespace', '`namespace`');
await ensureIndex(connection, 'i18n_translation_overrides', 'idx_i18n_overrides_lang_namespace', '`language_code`, `namespace`');
// --- Dashboard Platforms (admin managed dashboard cards) ---
await connection.query(`
CREATE TABLE IF NOT EXISTS dashboard_plattforms (

View File

@ -22,6 +22,15 @@ class I18nPreferencesRepository {
};
}
_safeBoolean(value, fallback) {
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 get() {
const [rows] = await db.query('SELECT * FROM i18n_preferences WHERE id = 1 LIMIT 1');
if (!rows.length) {
@ -62,6 +71,351 @@ class I18nPreferencesRepository {
return this.get();
}
async _tableExists(conn, tableName) {
const [rows] = await conn.query(
`SELECT 1
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = ?
LIMIT 1`,
[tableName]
);
return rows.length > 0;
}
async _columnExists(conn, tableName, columnName) {
const [rows] = await conn.query(
`SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = ?
AND COLUMN_NAME = ?
LIMIT 1`,
[tableName, columnName]
);
return rows.length > 0;
}
async deleteLanguageEntries(languageCode, updatedByUserId) {
const conn = await db.getConnection();
const safeLanguageCode = String(languageCode || '').trim();
const targets = [
// Language metadata
{ table: 'i18n_languages', possibleColumns: ['language_code', 'lang', 'code'] },
{ table: 'i18n_language_metadata', possibleColumns: ['language_code', 'lang', 'code'] },
// Translation/custom-value stores
{ table: 'i18n_translation_overrides', possibleColumns: ['language_code', 'lang'] },
{ table: 'i18n_translations', possibleColumns: ['language_code', 'lang'] },
// Potential language-scoped preference/link tables
{ table: 'i18n_preferences_languages', possibleColumns: ['language_code', 'lang'] },
{ table: 'i18n_preference_categories', possibleColumns: ['language_code', 'lang'] },
{ table: 'i18n_preference_global_keys', possibleColumns: ['language_code', 'lang'] },
];
let deletedRows = 0;
const touchedTables = [];
try {
await conn.beginTransaction();
for (const target of targets) {
const exists = await this._tableExists(conn, target.table);
if (!exists) continue;
let deleteColumn = null;
for (const col of target.possibleColumns) {
if (await this._columnExists(conn, target.table, col)) {
deleteColumn = col;
break;
}
}
if (!deleteColumn) continue;
const [result] = await conn.query(
`DELETE FROM \`${target.table}\` WHERE \`${deleteColumn}\` = ?`,
[safeLanguageCode]
);
const affected = Number(result?.affectedRows || 0);
if (affected > 0) {
deletedRows += affected;
touchedTables.push(target.table);
}
}
if (await this._tableExists(conn, 'i18n_preferences')) {
await conn.query(
`UPDATE i18n_preferences
SET updated_by_user_id = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = 1`,
[updatedByUserId || null]
);
}
await conn.commit();
return { deletedRows, touchedTables };
} catch (error) {
await conn.rollback();
throw error;
} finally {
conn.release();
}
}
async upsertLanguage(conn, language, updatedByUserId) {
if (!language || !language.languageCode) return null;
const languageCode = String(language.languageCode).trim().toLowerCase();
const label = String(language.label || languageCode).trim();
const isEnabled = this._safeBoolean(language.isEnabled, true);
const isCustom = this._safeBoolean(language.isCustom, true);
await conn.query(
`INSERT INTO i18n_languages (language_code, label, is_enabled, is_custom, created_by_user_id, updated_by_user_id)
VALUES (?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
label = VALUES(label),
is_enabled = VALUES(is_enabled),
is_custom = VALUES(is_custom),
updated_by_user_id = VALUES(updated_by_user_id)`,
[languageCode, label, isEnabled ? 1 : 0, isCustom ? 1 : 0, updatedByUserId || null, updatedByUserId || null]
);
const [rows] = await conn.query(
`SELECT language_code AS languageCode,
label,
is_enabled AS isEnabled,
is_custom AS isCustom,
created_at AS createdAt,
updated_at AS updatedAt
FROM i18n_languages
WHERE language_code = ?
LIMIT 1`,
[languageCode]
);
return rows[0] || null;
}
async upsertTranslationOverrides(conn, translationEntries, updatedByUserId) {
if (!Array.isArray(translationEntries) || !translationEntries.length) {
return { upsertedCount: 0 };
}
let upsertedCount = 0;
for (const entry of translationEntries) {
const languageCode = String(entry.languageCode || '').trim().toLowerCase();
const namespace = String(entry.namespace || '').trim();
const key = String(entry.key || '').trim();
const value = entry.value == null ? '' : String(entry.value);
const isCustom = this._safeBoolean(entry.isCustom, true);
if (!languageCode || !namespace || !key) continue;
await conn.query(
`INSERT INTO i18n_translation_overrides
(language_code, namespace, t_key, t_value, is_custom, created_by_user_id, updated_by_user_id)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
t_value = VALUES(t_value),
is_custom = VALUES(is_custom),
updated_by_user_id = VALUES(updated_by_user_id)`,
[
languageCode,
namespace,
key,
value,
isCustom ? 1 : 0,
updatedByUserId || null,
updatedByUserId || null,
]
);
upsertedCount += 1;
}
return { upsertedCount };
}
async upsertBundle({ categories, globalKeys, language, translations, updatedByUserId } = {}) {
const conn = await db.getConnection();
try {
await conn.beginTransaction();
const [prefRows] = await conn.query('SELECT * FROM i18n_preferences WHERE id = 1 LIMIT 1');
const current = prefRows.length
? this._normalizeRow(prefRows[0])
: { categories: [], globalKeys: [] };
const nextCategories = categories !== undefined ? categories : current.categories;
const nextGlobalKeys = globalKeys !== undefined ? globalKeys : current.globalKeys;
await conn.query(
`INSERT INTO i18n_preferences (id, categories_json, global_keys_json, updated_by_user_id)
VALUES (1, ?, ?, ?)
ON DUPLICATE KEY UPDATE
categories_json = VALUES(categories_json),
global_keys_json = VALUES(global_keys_json),
updated_by_user_id = VALUES(updated_by_user_id)`,
[JSON.stringify(nextCategories || []), JSON.stringify(nextGlobalKeys || []), updatedByUserId || null]
);
const languagesToEnsure = new Map();
if (language && language.languageCode) {
languagesToEnsure.set(String(language.languageCode).toLowerCase(), language);
}
for (const t of (Array.isArray(translations) ? translations : [])) {
const code = String(t.languageCode || '').trim().toLowerCase();
if (!code) continue;
if (!languagesToEnsure.has(code)) {
languagesToEnsure.set(code, {
languageCode: code,
label: code.toUpperCase(),
isEnabled: true,
isCustom: true,
});
}
}
let upsertedLanguage = null;
for (const [, langPayload] of languagesToEnsure) {
const row = await this.upsertLanguage(conn, langPayload, updatedByUserId);
if (language && row && row.languageCode === String(language.languageCode).toLowerCase()) {
upsertedLanguage = row;
}
}
const translationResult = await this.upsertTranslationOverrides(conn, translations, updatedByUserId);
const [savedRows] = await conn.query('SELECT * FROM i18n_preferences WHERE id = 1 LIMIT 1');
const preferences = savedRows.length
? this._normalizeRow(savedRows[0])
: { categories: [], globalKeys: [] };
await conn.commit();
return {
preferences,
language: upsertedLanguage,
translationsUpserted: translationResult.upsertedCount,
};
} catch (error) {
await conn.rollback();
throw error;
} finally {
conn.release();
}
}
async listTranslations({ languageCode, namespace } = {}) {
const filters = [];
const params = [];
if (languageCode) {
filters.push('language_code = ?');
params.push(String(languageCode).trim().toLowerCase());
}
if (namespace) {
filters.push('namespace = ?');
params.push(String(namespace).trim());
}
const whereClause = filters.length ? `WHERE ${filters.join(' AND ')}` : '';
const [rows] = await db.query(
`SELECT language_code AS languageCode,
namespace,
t_key AS \`key\`,
t_value AS value,
is_custom AS isCustom,
updated_at AS updatedAt
FROM i18n_translation_overrides
${whereClause}
ORDER BY language_code, namespace, t_key`,
params
);
return rows || [];
}
async upsertTranslations({ translations, updatedByUserId } = {}) {
const conn = await db.getConnection();
try {
await conn.beginTransaction();
// Make sure every translation language exists in metadata table.
const ensured = new Set();
for (const t of (translations || [])) {
const code = String(t.languageCode || '').trim().toLowerCase();
if (!code || ensured.has(code)) continue;
await this.upsertLanguage(
conn,
{
languageCode: code,
label: code.toUpperCase(),
isEnabled: true,
isCustom: true,
},
updatedByUserId
);
ensured.add(code);
}
const result = await this.upsertTranslationOverrides(conn, translations || [], updatedByUserId);
await conn.commit();
return result;
} catch (error) {
await conn.rollback();
throw error;
} finally {
conn.release();
}
}
async getScanSummary({ languageCode } = {}) {
const code = languageCode ? String(languageCode).trim().toLowerCase() : null;
const [languages] = await db.query(
`SELECT language_code AS languageCode,
label,
is_enabled AS isEnabled,
is_custom AS isCustom
FROM i18n_languages
ORDER BY language_code`
);
const [namespaces] = await db.query(
`SELECT DISTINCT namespace
FROM i18n_translation_overrides
${code ? 'WHERE language_code = ?' : ''}
ORDER BY namespace`,
code ? [code] : []
);
const [countsByLanguage] = await db.query(
`SELECT language_code AS languageCode, COUNT(*) AS entryCount
FROM i18n_translation_overrides
${code ? 'WHERE language_code = ?' : ''}
GROUP BY language_code
ORDER BY language_code`,
code ? [code] : []
);
const prefs = await this.get();
const categoryNamespaces = Array.isArray(prefs.categories)
? [...new Set(prefs.categories.flatMap((c) => (Array.isArray(c?.namespaces) ? c.namespaces : [])))]
: [];
return {
languages: languages || [],
namespaces: (namespaces || []).map((r) => r.namespace),
countsByLanguage: countsByLanguage || [],
categories: prefs.categories || [],
globalKeys: prefs.globalKeys || [],
categoryNamespaces,
};
}
}
module.exports = I18nPreferencesRepository;

View File

@ -11,9 +11,6 @@ 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 { getRouterPathFromApiEnv } = require('../utils/apiPath');
const I18N_PREFERENCES_ROUTE_PATH = getRouterPathFromApiEnv('BACKEND_I18N_PREFERENCES_PATH', '/api/admin/i18n/preferences');
// Helper middlewares for company-stamp
function forceCompanyForAdmin(req, res, next) {
@ -41,6 +38,7 @@ router.delete('/admin/news/:id', authMiddleware, adminOnly, NewsController.delet
// Admin: remove pool members
router.delete('/admin/pools/:id/members', authMiddleware, adminOnly, PoolController.removeMembers);
router.delete(I18N_PREFERENCES_ROUTE_PATH, authMiddleware, adminOnly, I18nPreferencesController.delete);
router.delete('/admin/i18n/preferences', authMiddleware, adminOnly, I18nPreferencesController.delete);
router.delete('/i18n/preferences', authMiddleware, adminOnly, I18nPreferencesController.delete);
module.exports = router;

View File

@ -32,10 +32,8 @@ 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 { getRouterPathFromApiEnv } = require('../utils/apiPath');
const AUTH_VALIDATE_ROUTE_PATH = getRouterPathFromApiEnv('BACKEND_AUTH_VALIDATE_PATH', '/api/auth/validate');
const I18N_PREFERENCES_ROUTE_PATH = getRouterPathFromApiEnv('BACKEND_I18N_PREFERENCES_PATH', '/api/admin/i18n/preferences');
const AUTH_VALIDATE_ROUTE_PATH = '/auth/validate';
// small helpers copied from original files
@ -60,7 +58,10 @@ router.get('/users/:id/permissions', authMiddleware, PermissionController.getUse
router.get('/admin/users/:id/full', authMiddleware, adminOnly, AdminUserController.getFullUserAccountDetails);
router.get('/admin/users/:id/detailed', authMiddleware, adminOnly, AdminUserController.getDetailedUserInfo);
router.get('/admin/company-settings', authMiddleware, adminOnly, CompanySettingsController.get);
router.get(I18N_PREFERENCES_ROUTE_PATH, authMiddleware, adminOnly, I18nPreferencesController.get);
router.get('/admin/i18n/preferences', authMiddleware, adminOnly, I18nPreferencesController.get);
router.get('/i18n/preferences', authMiddleware, adminOnly, I18nPreferencesController.get);
router.get('/i18n/translations', authMiddleware, adminOnly, I18nPreferencesController.getTranslations);
router.get('/i18n/scan', authMiddleware, adminOnly, I18nPreferencesController.scan);
router.get('/users/:id/documents', authMiddleware, UserController.getUserDocumentsAndContracts);
router.get('/verify-password-reset', (req, res) => { /* Note: was moved from PasswordResetController.verifyPasswordResetToken */ res.status(204).end(); }); // keep placeholder if controller already registered via other verb

View File

@ -33,9 +33,6 @@ 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 { getRouterPathFromApiEnv } = require('../utils/apiPath');
const I18N_PREFERENCES_ROUTE_PATH = getRouterPathFromApiEnv('BACKEND_I18N_PREFERENCES_PATH', '/api/admin/i18n/preferences');
const multer = require('multer');
const upload = multer({ storage: multer.memoryStorage() });
@ -86,7 +83,10 @@ 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(I18N_PREFERENCES_ROUTE_PATH, authMiddleware, adminOnly, I18nPreferencesController.post);
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);
router.post('/i18n/scan', authMiddleware, adminOnly, I18nPreferencesController.scan);
router.post('/admin/send-password-reset/:userId', authMiddleware, adminOnly, async (req, res) => {
const userId = req.params.userId;
// require here to avoid circular/top-level ordering issues

View File

@ -10,9 +10,6 @@ const CompanySettingsController = require('../controller/admin/CompanySettingsCo
const DashboardPlatformsController = require('../controller/admin/DashboardPlatformsController');
const ShippingFeesController = require('../controller/admin/ShippingFeesController');
const I18nPreferencesController = require('../controller/admin/I18nPreferencesController');
const { getRouterPathFromApiEnv } = require('../utils/apiPath');
const I18N_PREFERENCES_ROUTE_PATH = getRouterPathFromApiEnv('BACKEND_I18N_PREFERENCES_PATH', '/api/admin/i18n/preferences');
const multer = require('multer');
const upload = multer({ storage: multer.memoryStorage() });
@ -32,6 +29,8 @@ router.put('/admin/dashboard-platforms/:id', authMiddleware, adminOnly, Dashboar
// Admin: update shipping fee for a piece count (60/120)
router.put('/admin/shipping-fees/:pieceCount', authMiddleware, adminOnly, ShippingFeesController.updatePrice);
router.put(I18N_PREFERENCES_ROUTE_PATH, authMiddleware, adminOnly, I18nPreferencesController.put);
router.put('/admin/i18n/preferences', authMiddleware, adminOnly, I18nPreferencesController.put);
router.put('/i18n/preferences', authMiddleware, adminOnly, I18nPreferencesController.put);
router.put('/i18n/translations', authMiddleware, adminOnly, I18nPreferencesController.upsertTranslations);
module.exports = router;

View File

@ -3,7 +3,6 @@ const UnitOfWork = require('../database/UnitOfWork');
const argon2 = require('argon2');
async function createAdminUser() {
return;
// const adminEmail = process.env.ADMIN_EMAIL || 'office@profit-planet.com';
const adminEmail = process.env.ADMIN_EMAIL || 'alexander.ibrahim.ai@gmail.com';
// const adminEmail = process.env.ADMIN_EMAIL || 'loki.aahi@gmail.com';

View File

@ -1,22 +0,0 @@
function toRouterPath(configuredPath, defaultApiPath) {
const fallback = String(defaultApiPath || '/').trim() || '/';
const raw = String(configuredPath || fallback).trim() || fallback;
let path = raw.startsWith('/') ? raw : `/${raw}`;
// Routers are mounted under /api in server.js, so strip an optional /api prefix.
if (path === '/api') path = '/';
if (path.startsWith('/api/')) path = path.slice(4);
if (path.length > 1) path = path.replace(/\/+$/, '');
return path || '/';
}
function getRouterPathFromApiEnv(envKey, defaultApiPath) {
return toRouterPath(process.env[envKey], defaultApiPath);
}
module.exports = {
toRouterPath,
getRouterPathFromApiEnv,
};