zua
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
parent
413a30ddb0
commit
918deb2b69
106
controller/admin/I18nPreferencesController.js
Normal file
106
controller/admin/I18nPreferencesController.js
Normal file
@ -0,0 +1,106 @@
|
||||
const I18nPreferencesRepository = require('../../repositories/settings/I18nPreferencesRepository');
|
||||
const { logger } = require('../../middleware/logger');
|
||||
|
||||
const repo = new I18nPreferencesRepository();
|
||||
|
||||
class I18nPreferencesController {
|
||||
static _normalizeStringArray(values) {
|
||||
if (!Array.isArray(values)) return [];
|
||||
const normalized = values
|
||||
.map((v) => (v == null ? '' : String(v).trim()))
|
||||
.filter(Boolean);
|
||||
return [...new Set(normalized)];
|
||||
}
|
||||
|
||||
static _normalizeCategories(categories) {
|
||||
if (!Array.isArray(categories)) return [];
|
||||
|
||||
return categories
|
||||
.map((item, idx) => {
|
||||
const id = String(item?.id ?? '').trim() || `category_${idx + 1}`;
|
||||
const label = String(item?.label ?? id).trim() || id;
|
||||
const namespaces = I18nPreferencesController._normalizeStringArray(item?.namespaces);
|
||||
const isCustom = Boolean(item?.isCustom);
|
||||
return { id, label, namespaces, isCustom };
|
||||
});
|
||||
}
|
||||
|
||||
static _buildResponse(preferences) {
|
||||
return {
|
||||
ok: true,
|
||||
preferences,
|
||||
categories: preferences.categories,
|
||||
globalKeys: preferences.globalKeys,
|
||||
};
|
||||
}
|
||||
|
||||
static async get(req, res) {
|
||||
try {
|
||||
const preferences = await repo.get();
|
||||
return res.status(200).json(I18nPreferencesController._buildResponse(preferences));
|
||||
} catch (error) {
|
||||
logger.error('i18nPreferences:get:failed', { error: error?.message });
|
||||
return res.status(500).json({ ok: false, message: 'Failed to load i18n preferences' });
|
||||
}
|
||||
}
|
||||
|
||||
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' });
|
||||
}
|
||||
}
|
||||
|
||||
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' });
|
||||
}
|
||||
}
|
||||
|
||||
static async delete(req, res) {
|
||||
try {
|
||||
const hasBodyReplacement = req.body && (
|
||||
Object.prototype.hasOwnProperty.call(req.body, 'categories') ||
|
||||
Object.prototype.hasOwnProperty.call(req.body, 'globalKeys')
|
||||
);
|
||||
|
||||
const preferences = hasBodyReplacement
|
||||
? await repo.upsert({
|
||||
categories: I18nPreferencesController._normalizeCategories(req.body?.categories),
|
||||
globalKeys: I18nPreferencesController._normalizeStringArray(req.body?.globalKeys),
|
||||
updatedByUserId: req.user?.userId ?? req.user?.id ?? null,
|
||||
})
|
||||
: await repo.clear(req.user?.userId ?? req.user?.id ?? null);
|
||||
|
||||
return res.status(200).json(I18nPreferencesController._buildResponse(preferences));
|
||||
} catch (error) {
|
||||
logger.error('i18nPreferences:delete:failed', { error: error?.message });
|
||||
return res.status(500).json({ ok: false, message: 'Failed to delete i18n preferences' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = I18nPreferencesController;
|
||||
@ -261,6 +261,56 @@ class LoginController {
|
||||
return res.status(500).json({ success: false, message: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
|
||||
static async validate(req, res) {
|
||||
try {
|
||||
const refreshToken = req.cookies?.refreshToken;
|
||||
if (!refreshToken) {
|
||||
return res.status(401).json({ ok: false, message: 'No refresh token provided' });
|
||||
}
|
||||
|
||||
const result = await LoginService.validateByRefreshToken(refreshToken);
|
||||
const user = result?.user || {};
|
||||
|
||||
const role = String(user.role || '').toLowerCase();
|
||||
const userRoles = Array.isArray(user.roles)
|
||||
? user.roles.map((r) => String(r).toLowerCase())
|
||||
: [];
|
||||
const mergedRoles = [...new Set([role, ...userRoles].filter(Boolean))];
|
||||
|
||||
const isAdmin = Boolean(
|
||||
user.isAdmin ||
|
||||
role === 'admin' ||
|
||||
role === 'super_admin' ||
|
||||
role === 'superadmin' ||
|
||||
mergedRoles.includes('admin') ||
|
||||
mergedRoles.includes('super_admin') ||
|
||||
mergedRoles.includes('superadmin')
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
ok: true,
|
||||
user: {
|
||||
id: String(user.id ?? ''),
|
||||
role: user.role || null,
|
||||
isAdmin,
|
||||
roles: mergedRoles,
|
||||
},
|
||||
isAdmin,
|
||||
role: user.role || null,
|
||||
roles: mergedRoles,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.status) {
|
||||
return res.status(error.status).json({ ok: false, message: error.message });
|
||||
}
|
||||
logger.error('authValidate:error', {
|
||||
message: error?.message,
|
||||
stack: error?.stack,
|
||||
});
|
||||
return res.status(500).json({ ok: false, message: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LoginController;
|
||||
@ -829,6 +829,38 @@ const createDatabase = async () => {
|
||||
await addColumnIfMissing(connection, 'company_settings', 'qr_code_60_base64', 'LONGTEXT NULL');
|
||||
await addColumnIfMissing(connection, 'company_settings', 'qr_code_120_base64', 'LONGTEXT NULL');
|
||||
|
||||
// --- I18n Preferences (single-row, admin language-management settings) ---
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS i18n_preferences (
|
||||
id TINYINT PRIMARY KEY DEFAULT 1,
|
||||
categories_json JSON NULL,
|
||||
global_keys_json JSON NULL,
|
||||
updated_by_user_id INT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT chk_i18n_preferences_singleton CHECK (id = 1),
|
||||
FOREIGN KEY (updated_by_user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
`);
|
||||
await connection.query(`
|
||||
INSERT IGNORE INTO i18n_preferences (id, categories_json, global_keys_json, updated_by_user_id)
|
||||
VALUES (1, JSON_ARRAY(), JSON_ARRAY(), NULL);
|
||||
`);
|
||||
console.log('✅ i18n preferences table created/verified');
|
||||
|
||||
// Backward-compatible for older schemas
|
||||
await addColumnIfMissing(connection, 'i18n_preferences', 'categories_json', 'JSON NULL');
|
||||
await addColumnIfMissing(connection, 'i18n_preferences', 'global_keys_json', 'JSON NULL');
|
||||
await addColumnIfMissing(connection, 'i18n_preferences', 'updated_by_user_id', 'INT NULL');
|
||||
|
||||
await addForeignKeyIfMissing(
|
||||
connection,
|
||||
'i18n_preferences',
|
||||
'i18n_preferences_ibfk_1',
|
||||
`ALTER TABLE \`i18n_preferences\`
|
||||
ADD CONSTRAINT \`i18n_preferences_ibfk_1\` FOREIGN KEY (\`updated_by_user_id\`) REFERENCES \`users\`(\`id\`) ON DELETE SET NULL ON UPDATE CASCADE`
|
||||
);
|
||||
|
||||
// --- Dashboard Platforms (admin managed dashboard cards) ---
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS dashboard_plattforms (
|
||||
|
||||
67
repositories/settings/I18nPreferencesRepository.js
Normal file
67
repositories/settings/I18nPreferencesRepository.js
Normal file
@ -0,0 +1,67 @@
|
||||
const db = require('../../database/database');
|
||||
|
||||
class I18nPreferencesRepository {
|
||||
_safeJsonArray(value) {
|
||||
if (Array.isArray(value)) return value;
|
||||
if (value == null) return [];
|
||||
|
||||
try {
|
||||
const parsed = typeof value === 'string' ? JSON.parse(value) : value;
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
_normalizeRow(row) {
|
||||
const categories = this._safeJsonArray(row?.categories_json);
|
||||
const globalKeys = this._safeJsonArray(row?.global_keys_json);
|
||||
return {
|
||||
categories,
|
||||
globalKeys,
|
||||
};
|
||||
}
|
||||
|
||||
async get() {
|
||||
const [rows] = await db.query('SELECT * FROM i18n_preferences WHERE id = 1 LIMIT 1');
|
||||
if (!rows.length) {
|
||||
return { categories: [], globalKeys: [] };
|
||||
}
|
||||
return this._normalizeRow(rows[0]);
|
||||
}
|
||||
|
||||
async upsert({ categories, globalKeys, updatedByUserId } = {}) {
|
||||
const current = await this.get();
|
||||
|
||||
const nextCategories = categories !== undefined ? categories : current.categories;
|
||||
const nextGlobalKeys = globalKeys !== undefined ? globalKeys : current.globalKeys;
|
||||
|
||||
await db.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]
|
||||
);
|
||||
|
||||
return this.get();
|
||||
}
|
||||
|
||||
async clear(updatedByUserId) {
|
||||
await db.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([]), JSON.stringify([]), updatedByUserId || null]
|
||||
);
|
||||
|
||||
return this.get();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = I18nPreferencesRepository;
|
||||
@ -10,6 +10,10 @@ const CoffeeController = require('../controller/admin/CoffeeController');
|
||||
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) {
|
||||
@ -37,5 +41,6 @@ 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);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@ -30,6 +30,12 @@ const DevManagementController = require('../controller/dev/DevManagementControll
|
||||
const CompanySettingsController = require('../controller/admin/CompanySettingsController');
|
||||
const DashboardPlatformsController = require('../controller/admin/DashboardPlatformsController');
|
||||
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');
|
||||
|
||||
// small helpers copied from original files
|
||||
|
||||
@ -44,6 +50,7 @@ function forceCompanyForAdmin(req, res, next) {
|
||||
// === GET routes moved from other files ===
|
||||
|
||||
// auth.js GETs
|
||||
router.get(AUTH_VALIDATE_ROUTE_PATH, LoginController.validate);
|
||||
router.get('/me', authMiddleware, UserController.getMe);
|
||||
router.get('/user/status', authMiddleware, UserStatusController.getStatus);
|
||||
router.get('/user/status-progress', authMiddleware, UserStatusController.getStatusProgress);
|
||||
@ -53,6 +60,7 @@ 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('/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
|
||||
|
||||
|
||||
@ -32,6 +32,10 @@ const NewsController = require('../controller/news/NewsController');
|
||||
const InvoiceController = require('../controller/invoice/InvoiceController'); // NEW
|
||||
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() });
|
||||
@ -82,6 +86,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(I18N_PREFERENCES_ROUTE_PATH, authMiddleware, adminOnly, I18nPreferencesController.post);
|
||||
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
|
||||
|
||||
@ -9,6 +9,10 @@ const CoffeeController = require('../controller/admin/CoffeeController');
|
||||
const CompanySettingsController = require('../controller/admin/CompanySettingsController');
|
||||
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() });
|
||||
|
||||
@ -28,5 +32,6 @@ 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);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@ -221,6 +221,63 @@ class LoginService {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async validateByRefreshToken(refreshToken) {
|
||||
logger.info('LoginService.validateByRefreshToken:start');
|
||||
const unitOfWork = new UnitOfWork();
|
||||
await unitOfWork.start();
|
||||
unitOfWork.registerRepository('login', new LoginRepository(unitOfWork));
|
||||
unitOfWork.registerRepository('user', new UserRepository(unitOfWork));
|
||||
unitOfWork.registerRepository('status', new UserStatusRepository(unitOfWork));
|
||||
|
||||
try {
|
||||
const loginRepo = unitOfWork.getRepository('login');
|
||||
const tokenRecord = await loginRepo.findRefreshToken(refreshToken);
|
||||
if (!tokenRecord) {
|
||||
const error = new Error('Invalid or expired refresh token');
|
||||
error.status = 401;
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (new Date(tokenRecord.expires_at) < new Date()) {
|
||||
const error = new Error('Refresh token expired');
|
||||
error.status = 401;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const userRepo = unitOfWork.getRepository('user');
|
||||
const user = await userRepo.findUserByEmailOrId(tokenRecord.user_id);
|
||||
if (!user) {
|
||||
const error = new Error('User not found');
|
||||
error.status = 401;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const statusRepo = unitOfWork.getRepository('status');
|
||||
const userStatus = await statusRepo.getStatusByUserId(user.id);
|
||||
if (userStatus && userStatus.status === 'suspended') {
|
||||
const error = new Error('Account suspended');
|
||||
error.status = 403;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const role = await loginRepo.getUserRole(user.id);
|
||||
const permissions = await loginRepo.getUserPermissions(user.id);
|
||||
|
||||
await unitOfWork.commit();
|
||||
|
||||
return {
|
||||
user: {
|
||||
...user.getPublicData(),
|
||||
role,
|
||||
permissions,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
await unitOfWork.rollback(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper for finding user by id or email
|
||||
|
||||
22
utils/apiPath.js
Normal file
22
utils/apiPath.js
Normal file
@ -0,0 +1,22 @@
|
||||
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,
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user