From 3f1b50871ad265d3f8e373e4c4cba6e2ccd6729c Mon Sep 17 00:00:00 2001 From: Alexander Borg Date: Fri, 19 Sep 2025 08:14:41 +0200 Subject: [PATCH] Fix jwt-token --- server/middleware/auth.js | 78 ++--- server/routes/management.js | 72 ++--- .../tests/integration/auth-security.test.js | 14 +- server/tests/middleware/auth.test.js | 20 +- server/utils/i18n.js | 272 ++++++++++++++++++ 5 files changed, 343 insertions(+), 113 deletions(-) create mode 100644 server/utils/i18n.js diff --git a/server/middleware/auth.js b/server/middleware/auth.js index 32aaac2..b48c1c8 100644 --- a/server/middleware/auth.js +++ b/server/middleware/auth.js @@ -1,4 +1,5 @@ const jwt = require('jsonwebtoken'); +const { createErrorResponse, getLanguageFromRequest } = require('../utils/i18n'); // Allow models to be injected for testing let models = null; @@ -17,27 +18,24 @@ async function authenticateToken(req, res, next) { const authHeader = req.headers['authorization']; if (!authHeader) { - return res.status(401).json({ - success: false, - message: 'Access token required' - }); + const errorResponse = createErrorResponse(req, 401, 'NO_TOKEN'); + errorResponse.json.redirectToLogin = true; + return res.status(errorResponse.status).json(errorResponse.json); } // Check for proper Bearer token format if (!authHeader.startsWith('Bearer ')) { - return res.status(401).json({ - success: false, - message: 'Invalid token format' - }); + const errorResponse = createErrorResponse(req, 401, 'INVALID_TOKEN'); + errorResponse.json.redirectToLogin = true; + return res.status(errorResponse.status).json(errorResponse.json); } const token = authHeader.split(' ')[1]; if (!token) { - return res.status(401).json({ - success: false, - message: 'Access token required' - }); + const errorResponse = createErrorResponse(req, 401, 'NO_TOKEN'); + errorResponse.json.redirectToLogin = true; + return res.status(errorResponse.status).json(errorResponse.json); } try { @@ -65,23 +63,15 @@ async function authenticateToken(req, res, next) { }); if (!user) { - return res.status(401).json({ - success: false, - message: 'User account not found. Please contact support.', - error: 'USER_NOT_FOUND', - errorCode: 'USER_NOT_FOUND', - redirectToLogin: true - }); + const errorResponse = createErrorResponse(req, 401, 'USER_NOT_FOUND'); + errorResponse.json.redirectToLogin = true; + return res.status(errorResponse.status).json(errorResponse.json); } if (!user.is_active) { - return res.status(401).json({ - success: false, - message: 'Your account has been deactivated. Please contact support.', - error: 'ACCOUNT_DEACTIVATED', - errorCode: 'ACCOUNT_INACTIVE', - redirectToLogin: true - }); + const errorResponse = createErrorResponse(req, 401, 'ACCOUNT_DEACTIVATED'); + errorResponse.json.redirectToLogin = true; + return res.status(errorResponse.status).json(errorResponse.json); } // Set user context with expected properties for compatibility @@ -123,39 +113,27 @@ async function authenticateToken(req, res, next) { // Handle specific JWT errors with detailed responses if (error.name === 'TokenExpiredError') { - return res.status(401).json({ - success: false, - error: 'TOKEN_EXPIRED', - message: 'Token expired', - redirectToLogin: true - }); + const errorResponse = createErrorResponse(req, 401, 'TOKEN_EXPIRED'); + errorResponse.json.redirectToLogin = true; + return res.status(errorResponse.status).json(errorResponse.json); } if (error.name === 'JsonWebTokenError') { - return res.status(401).json({ - success: false, - error: 'INVALID_TOKEN', - message: 'Invalid token', - redirectToLogin: true - }); + const errorResponse = createErrorResponse(req, 401, 'INVALID_TOKEN'); + errorResponse.json.redirectToLogin = true; + return res.status(errorResponse.status).json(errorResponse.json); } if (error.name === 'NotBeforeError') { - return res.status(401).json({ - success: false, - error: 'TOKEN_NOT_ACTIVE', - message: 'Token not active', - redirectToLogin: true - }); + const errorResponse = createErrorResponse(req, 401, 'TOKEN_NOT_ACTIVE'); + errorResponse.json.redirectToLogin = true; + return res.status(errorResponse.status).json(errorResponse.json); } // Generic authentication error - return res.status(401).json({ - success: false, - error: 'AUTHENTICATION_FAILED', - message: 'Authentication failed', - redirectToLogin: true - }); + const errorResponse = createErrorResponse(req, 401, 'AUTHENTICATION_FAILED'); + errorResponse.json.redirectToLogin = true; + return res.status(errorResponse.status).json(errorResponse.json); } } diff --git a/server/routes/management.js b/server/routes/management.js index d481737..f4f30a6 100644 --- a/server/routes/management.js +++ b/server/routes/management.js @@ -9,16 +9,15 @@ const jwt = require('jsonwebtoken'); const bcrypt = require('bcryptjs'); // Fixed: use bcryptjs instead of bcrypt const { Op } = require('sequelize'); // Add Sequelize operators const { Tenant, User, ManagementUser } = require('../models'); +const { createErrorResponse, getSystemMessage, getLanguageFromRequest } = require('../utils/i18n'); // Management-specific authentication middleware - NO shared auth with tenants const requireManagementAuth = (req, res, next) => { const token = req.headers.authorization?.replace('Bearer ', ''); if (!token) { - return res.status(401).json({ - success: false, - message: 'Management authentication required' - }); + const errorResponse = createErrorResponse(req, 401, 'AUTHENTICATION_REQUIRED'); + return res.status(errorResponse.status).json(errorResponse.json); } try { @@ -28,10 +27,8 @@ const requireManagementAuth = (req, res, next) => { // Verify this is a management token with proper role if (!decoded.isManagement || !['super_admin', 'platform_admin'].includes(decoded.role)) { - return res.status(403).json({ - success: false, - message: 'Insufficient management privileges' - }); + const errorResponse = createErrorResponse(req, 403, 'INSUFFICIENT_PRIVILEGES'); + return res.status(errorResponse.status).json(errorResponse.json); } req.managementUser = { @@ -43,11 +40,8 @@ const requireManagementAuth = (req, res, next) => { next(); } catch (error) { - return res.status(403).json({ - success: false, - message: 'Invalid management token', - error: error.message - }); + const errorResponse = createErrorResponse(req, 403, 'INVALID_TOKEN'); + return res.status(errorResponse.status).json(errorResponse.json); } }; @@ -60,10 +54,8 @@ router.post('/auth/login', async (req, res) => { const managementUser = await ManagementUser.findByCredentials(username, password); if (!managementUser) { - return res.status(401).json({ - success: false, - message: 'Invalid management credentials' - }); + const errorResponse = createErrorResponse(req, 401, 'INVALID_MANAGEMENT_CREDENTIALS'); + return res.status(errorResponse.status).json(errorResponse.json); } const MANAGEMENT_SECRET = process.env.MANAGEMENT_JWT_SECRET || 'mgmt-super-secret-change-in-production'; @@ -87,11 +79,9 @@ router.post('/auth/login', async (req, res) => { } }); } catch (error) { - res.status(500).json({ - success: false, - message: 'Management authentication error', - error: error.message - }); + const errorResponse = createErrorResponse(req, 500, 'MANAGEMENT_AUTH_ERROR'); + errorResponse.json.error = error.message; + return res.status(errorResponse.status).json(errorResponse.json); } }); @@ -718,10 +708,8 @@ router.post('/tenants', async (req, res) => { // Enhanced validation for management portal if (!tenantData.name || !tenantData.slug) { - return res.status(400).json({ - success: false, - message: 'Name and slug are required' - }); + const errorResponse = createErrorResponse(req, 400, 'NAME_SLUG_REQUIRED'); + return res.status(errorResponse.status).json(errorResponse.json); } // Convert empty domain string to null to avoid unique constraint issues @@ -732,20 +720,16 @@ router.post('/tenants', async (req, res) => { // Check for unique slug const existingTenant = await Tenant.findOne({ where: { slug: tenantData.slug } }); if (existingTenant) { - return res.status(409).json({ - success: false, - message: 'Tenant slug already exists' - }); + const errorResponse = createErrorResponse(req, 409, 'TENANT_SLUG_EXISTS'); + return res.status(errorResponse.status).json(errorResponse.json); } // Check for unique domain if provided if (tenantData.domain) { const existingDomain = await Tenant.findOne({ where: { domain: tenantData.domain } }); if (existingDomain) { - return res.status(409).json({ - success: false, - message: 'Domain already exists for another tenant' - }); + const errorResponse = createErrorResponse(req, 409, 'DOMAIN_EXISTS'); + return res.status(errorResponse.status).json(errorResponse.json); } } @@ -757,15 +741,13 @@ router.post('/tenants', async (req, res) => { res.status(201).json({ success: true, data: tenant, - message: 'Tenant created successfully' + message: getSystemMessage(getLanguageFromRequest(req), 'TENANT_CREATED') }); } catch (error) { console.error('Management: Error creating tenant:', error); - res.status(500).json({ - success: false, - message: 'Failed to create tenant' - }); + const errorResponse = createErrorResponse(req, 500, 'TENANT_CREATION_FAILED'); + return res.status(errorResponse.status).json(errorResponse.json); } }); @@ -783,10 +765,8 @@ router.get('/tenants/:id', async (req, res) => { }); if (!tenant) { - return res.status(404).json({ - success: false, - message: 'Tenant not found' - }); + const errorResponse = createErrorResponse(req, 404, 'TENANT_NOT_FOUND'); + return res.status(errorResponse.status).json(errorResponse.json); } res.json({ @@ -796,10 +776,8 @@ router.get('/tenants/:id', async (req, res) => { } catch (error) { console.error('Management: Error fetching tenant:', error); - res.status(500).json({ - success: false, - message: 'Failed to fetch tenant' - }); + const errorResponse = createErrorResponse(req, 500, 'FETCH_TENANT_FAILED'); + return res.status(errorResponse.status).json(errorResponse.json); } }); diff --git a/server/tests/integration/auth-security.test.js b/server/tests/integration/auth-security.test.js index 3b7b3ae..1b6498d 100644 --- a/server/tests/integration/auth-security.test.js +++ b/server/tests/integration/auth-security.test.js @@ -37,7 +37,7 @@ describe('Authentication Integration Tests', () => { expect(response.body).to.deep.equal({ success: false, - message: 'Access token required' + message: 'No authentication token provided.' }); }); @@ -49,7 +49,7 @@ describe('Authentication Integration Tests', () => { expect(response.body).to.deep.equal({ success: false, - message: 'Invalid token format' + message: 'Invalid authentication token. Please log in again.' }); }); @@ -68,7 +68,7 @@ describe('Authentication Integration Tests', () => { expect(response.body).to.deep.equal({ success: false, error: 'TOKEN_EXPIRED', - message: 'Token expired', + message: 'Your session has expired. Please log in again.', redirectToLogin: true }); }); @@ -88,7 +88,7 @@ describe('Authentication Integration Tests', () => { expect(response.body).to.deep.equal({ success: false, error: 'INVALID_TOKEN', - message: 'Invalid token', + message: 'Invalid authentication token. Please log in again.', redirectToLogin: true }); }); @@ -107,7 +107,7 @@ describe('Authentication Integration Tests', () => { expect(response.body).to.deep.equal({ success: false, - message: 'User not found' + message: 'User account not found. Please contact support.' }); }); @@ -128,7 +128,7 @@ describe('Authentication Integration Tests', () => { expect(response.body).to.deep.equal({ success: false, - message: 'User account is inactive' + message: 'Your account has been deactivated. Please contact support.' }); }); @@ -194,7 +194,7 @@ describe('Authentication Integration Tests', () => { expect(response.body).to.deep.equal({ success: false, - message: 'Insufficient permissions' + message: 'You do not have permission to perform this action.' }); }); }); diff --git a/server/tests/middleware/auth.test.js b/server/tests/middleware/auth.test.js index 31210fa..85ccd8a 100644 --- a/server/tests/middleware/auth.test.js +++ b/server/tests/middleware/auth.test.js @@ -36,7 +36,7 @@ describe('Authentication Middleware', () => { expect(res.statusCode).to.equal(401); expect(res.data).to.deep.equal({ success: false, - message: 'Access token required' + message: 'No authentication token provided.' }); expect(next.errors).to.have.length(0); }); @@ -53,7 +53,7 @@ describe('Authentication Middleware', () => { expect(res.statusCode).to.equal(401); expect(res.data).to.deep.equal({ success: false, - message: 'Invalid token format' + message: 'Invalid authentication token. Please log in again.' }); }); @@ -68,7 +68,7 @@ describe('Authentication Middleware', () => { expect(res.statusCode).to.equal(401); expect(res.data.success).to.be.false; - expect(res.data.message).to.equal('Invalid token'); + expect(res.data.message).to.equal('Invalid authentication token. Please log in again.'); }); it('should reject request with expired JWT token', async () => { @@ -88,7 +88,7 @@ describe('Authentication Middleware', () => { expect(res.statusCode).to.equal(401); expect(res.data.success).to.be.false; - expect(res.data.message).to.equal('Token expired'); + expect(res.data.message).to.equal('Your session has expired. Please log in again.'); }); it('should accept valid JWT token and set user data', async () => { @@ -162,7 +162,7 @@ describe('Authentication Middleware', () => { expect(res.statusCode).to.equal(401); expect(res.data.success).to.be.false; - expect(res.data.message).to.equal('User not found'); + expect(res.data.message).to.equal('User account not found. Please contact support.'); }); it('should reject inactive user', async () => { @@ -186,7 +186,7 @@ describe('Authentication Middleware', () => { expect(res.statusCode).to.equal(401); expect(res.data.success).to.be.false; - expect(res.data.message).to.equal('User account is inactive'); + expect(res.data.message).to.equal('Your account has been deactivated. Please contact support.'); }); it('should handle malformed JWT token', async () => { @@ -326,7 +326,7 @@ describe('Authentication Middleware', () => { expect(res.data).to.deep.equal({ success: false, error: 'TOKEN_EXPIRED', - message: 'Token expired', + message: 'Your session has expired. Please log in again.', redirectToLogin: true }); }); @@ -344,7 +344,7 @@ describe('Authentication Middleware', () => { expect(res.data).to.deep.equal({ success: false, error: 'INVALID_TOKEN', - message: 'Invalid token', + message: 'Invalid authentication token. Please log in again.', redirectToLogin: true }); }); @@ -359,7 +359,9 @@ describe('Authentication Middleware', () => { expect(res.statusCode).to.equal(401); expect(res.data).to.deep.equal({ success: false, - message: 'Access token required' + error: 'NO_TOKEN', + message: 'No authentication token provided.', + redirectToLogin: true }); }); }); diff --git a/server/utils/i18n.js b/server/utils/i18n.js new file mode 100644 index 0000000..e4aa024 --- /dev/null +++ b/server/utils/i18n.js @@ -0,0 +1,272 @@ +/** + * Internationalization utility for backend error messages and system messages + */ + +const SUPPORTED_LANGUAGES = ['en', 'sv']; +const DEFAULT_LANGUAGE = 'en'; + +// Error message translations +const ERROR_MESSAGES = { + en: { + // Authentication errors + TOKEN_EXPIRED: 'Your session has expired. Please log in again.', + INVALID_TOKEN: 'Invalid authentication token. Please log in again.', + TOKEN_NOT_ACTIVE: 'Authentication token is not yet active.', + USER_NOT_FOUND: 'User account not found. Please contact support.', + ACCOUNT_DEACTIVATED: 'Your account has been deactivated. Please contact support.', + AUTH_REQUIRED: 'Authentication is required to access this resource.', + NO_TOKEN: 'No authentication token provided.', + + // Authorization errors + INSUFFICIENT_PERMISSIONS: 'Access denied. This action requires {requiredRoles} permissions, but you have {userRole} permissions.', + PERMISSION_DENIED: 'You do not have permission to perform this action.', + INSUFFICIENT_MANAGEMENT_PRIVILEGES: 'Insufficient management privileges to access this resource.', + + // Management errors + AUTHENTICATION_REQUIRED: 'Management authentication required.', + INSUFFICIENT_PRIVILEGES: 'Insufficient management privileges.', + INVALID_MANAGEMENT_CREDENTIALS: 'Invalid management credentials.', + MANAGEMENT_AUTH_ERROR: 'Management authentication error.', + + // Tenant management + TENANT_NOT_FOUND: 'Tenant not found.', + TENANT_CREATION_FAILED: 'Failed to create tenant.', + TENANT_UPDATE_FAILED: 'Failed to update tenant.', + TENANT_DELETE_FAILED: 'Failed to delete tenant.', + TENANT_SLUG_EXISTS: 'Tenant slug already exists.', + DOMAIN_EXISTS: 'Domain already exists for another tenant.', + CANNOT_DELETE_DEFAULT: 'Cannot delete default tenant.', + NAME_SLUG_REQUIRED: 'Name and slug are required.', + TENANT_CREATED: 'Tenant created successfully.', + TENANT_UPDATED: 'Tenant updated successfully.', + TENANT_DELETED: 'Tenant deleted successfully.', + FETCH_TENANTS_FAILED: 'Failed to fetch tenants.', + FETCH_TENANT_FAILED: 'Failed to fetch tenant.', + + // System errors + SYSTEM_INFO_FAILED: 'Failed to fetch system information.', + DOCKER_ACCESS_ERROR: 'This could mean Docker is not running, no containers are active, or the monitoring system needs Docker access.', + MANAGEMENT_AUTH_REQUIRED: 'Management authentication is required to access this resource.', + INVALID_MANAGEMENT_TOKEN: 'Invalid management authentication token.', + + // General errors + AUTHENTICATION_FAILED: 'Authentication failed. Please try again.', + ACCESS_DENIED: 'Access denied.', + VALIDATION_ERROR: 'Validation error occurred.', + INTERNAL_ERROR: 'An internal error occurred. Please try again later.' + }, + sv: { + // Authentication errors + TOKEN_EXPIRED: 'Din session har löpt ut. Vänligen logga in igen.', + INVALID_TOKEN: 'Ogiltig autentiseringstoken. Vänligen logga in igen.', + TOKEN_NOT_ACTIVE: 'Autentiseringstoken är inte aktiv ännu.', + USER_NOT_FOUND: 'Användarkonto hittades inte. Vänligen kontakta support.', + ACCOUNT_DEACTIVATED: 'Ditt konto har inaktiverats. Vänligen kontakta support.', + AUTH_REQUIRED: 'Autentisering krävs för att komma åt denna resurs.', + NO_TOKEN: 'Ingen autentiseringstoken tillhandahållen.', + + // Authorization errors + INSUFFICIENT_PERMISSIONS: 'Åtkomst nekad. Denna åtgärd kräver {requiredRoles} behörigheter, men du har {userRole} behörigheter.', + PERMISSION_DENIED: 'Du har inte behörighet att utföra denna åtgärd.', + INSUFFICIENT_MANAGEMENT_PRIVILEGES: 'Otillräckliga förvaltningsprivilegier för att komma åt denna resurs.', + + // Management errors + MANAGEMENT_AUTH_REQUIRED: 'Förvaltningsautentisering krävs för att komma åt denna resurs.', + INVALID_MANAGEMENT_TOKEN: 'Ogiltig förvaltningsautentiseringstoken.', + AUTHENTICATION_REQUIRED: 'Förvaltningsautentisering krävs.', + INSUFFICIENT_PRIVILEGES: 'Otillräckliga förvaltningsprivilegier.', + INVALID_MANAGEMENT_CREDENTIALS: 'Ogiltiga förvaltningsuppgifter.', + MANAGEMENT_AUTH_ERROR: 'Förvaltningsautentiseringsfel.', + + // Tenant management + TENANT_NOT_FOUND: 'Hyresgäst hittades inte.', + TENANT_CREATION_FAILED: 'Misslyckades att skapa hyresgäst.', + TENANT_UPDATE_FAILED: 'Misslyckades att uppdatera hyresgäst.', + TENANT_DELETE_FAILED: 'Misslyckades att ta bort hyresgäst.', + TENANT_SLUG_EXISTS: 'Hyresgästslug finns redan.', + DOMAIN_EXISTS: 'Domän finns redan för en annan hyresgäst.', + CANNOT_DELETE_DEFAULT: 'Kan inte ta bort standardhyresgäst.', + NAME_SLUG_REQUIRED: 'Namn och slug krävs.', + TENANT_CREATED: 'Hyresgäst skapad framgångsrikt.', + TENANT_UPDATED: 'Hyresgäst uppdaterad framgångsrikt.', + TENANT_DELETED: 'Hyresgäst borttagen framgångsrikt.', + FETCH_TENANTS_FAILED: 'Misslyckades att hämta hyresgäster.', + FETCH_TENANT_FAILED: 'Misslyckades att hämta hyresgäst.', + + // System errors + SYSTEM_INFO_FAILED: 'Misslyckades att hämta systeminformation.', + DOCKER_ACCESS_ERROR: 'Detta kan betyda att Docker inte körs, inga behållare är aktiva eller att övervakningssystemet behöver Docker-åtkomst.', + + // General errors + AUTHENTICATION_FAILED: 'Autentisering misslyckades. Vänligen försök igen.', + ACCESS_DENIED: 'Åtkomst nekad.', + VALIDATION_ERROR: 'Valideringsfel inträffade.', + INTERNAL_ERROR: 'Ett internt fel inträffade. Vänligen försök igen senare.' + } +}; + +// System message translations (for alerts, notifications, etc.) +const SYSTEM_MESSAGES = { + en: { + // Alert messages + CRITICAL_THREAT_DETECTED: 'CRITICAL THREAT: {droneType} DETECTED - IMMEDIATE RESPONSE REQUIRED', + HIGH_THREAT_DETECTED: 'HIGH THREAT: {droneType} detected at {distance}m', + MEDIUM_THREAT_DETECTED: 'MEDIUM THREAT: {droneType} detected in vicinity', + LOW_THREAT_DETECTED: 'LOW THREAT: {droneType} detected at distance', + MONITORING_DETECTED: 'MONITORING: {droneType} detected at long range', + MILITARY_THREAT_ENHANCED: 'MILITARY THREAT: {droneType} DETECTED - ENHANCED RESPONSE REQUIRED', + + // Device messages + DEVICE_OFFLINE: 'Device {deviceName} is offline', + DEVICE_ONLINE: 'Device {deviceName} is back online', + HEARTBEAT_RECEIVED: 'Heartbeat received from {deviceName}', + + // General system messages + OPERATION_SUCCESSFUL: 'Operation completed successfully', + OPERATION_FAILED: 'Operation failed', + DATA_SAVED: 'Data saved successfully', + DATA_DELETED: 'Data deleted successfully' + }, + sv: { + // Alert messages + CRITICAL_THREAT_DETECTED: 'KRITISKT HOT: {droneType} UPPTÄCKT - OMEDELBAR RESPONS KRÄVS', + HIGH_THREAT_DETECTED: 'HÖGT HOT: {droneType} upptäckt på {distance}m', + MEDIUM_THREAT_DETECTED: 'MEDELHÖGT HOT: {droneType} upptäckt i närheten', + LOW_THREAT_DETECTED: 'LÅGT HOT: {droneType} upptäckt på avstånd', + MONITORING_DETECTED: 'ÖVERVAKNING: {droneType} upptäckt på långt avstånd', + MILITARY_THREAT_ENHANCED: 'MILITÄRT HOT: {droneType} UPPTÄCKT - FÖRSTÄRKT RESPONS KRÄVS', + + // Device messages + DEVICE_OFFLINE: 'Enhet {deviceName} är offline', + DEVICE_ONLINE: 'Enhet {deviceName} är online igen', + HEARTBEAT_RECEIVED: 'Heartbeat mottagen från {deviceName}', + + // General system messages + OPERATION_SUCCESSFUL: 'Operationen slutfördes framgångsrikt', + OPERATION_FAILED: 'Operationen misslyckades', + DATA_SAVED: 'Data sparad framgångsrikt', + DATA_DELETED: 'Data raderad framgångsrikt' + } +}; + +// Drone type translations +const DRONE_TYPES = { + en: { + 0: 'Unknown Drone', + 1: 'DJI Mavic', + 2: 'Orlan', + 3: 'Racing Drone', + 4: 'DJI Phantom', + 5: 'Zala Lancet', + 6: 'Eleron', + 7: 'Commercial Drone', + 8: 'Professional Drone' + }, + sv: { + 0: 'Okänd Drönare', + 1: 'DJI Mavic', + 2: 'Orlan', + 3: 'Racing Drönare', + 4: 'DJI Phantom', + 5: 'Zala Lancet', + 6: 'Eleron', + 7: 'Kommersiell Drönare', + 8: 'Professionell Drönare' + } +}; + +/** + * Get user's preferred language from request headers + */ +function getLanguageFromRequest(req) { + // Check for explicit language header + const explicitLang = req.headers['accept-language-override'] || req.headers['x-language']; + if (explicitLang && SUPPORTED_LANGUAGES.includes(explicitLang)) { + return explicitLang; + } + + // Parse Accept-Language header + const acceptLanguage = req.headers['accept-language']; + if (acceptLanguage) { + const languages = acceptLanguage + .split(',') + .map(lang => lang.split(';')[0].trim().toLowerCase()) + .map(lang => lang.split('-')[0]); // Get just the language part (sv from sv-SE) + + for (const lang of languages) { + if (SUPPORTED_LANGUAGES.includes(lang)) { + return lang; + } + } + } + + return DEFAULT_LANGUAGE; +} + +/** + * Get localized error message + */ +function getErrorMessage(messageKey, language = DEFAULT_LANGUAGE, params = {}) { + const lang = SUPPORTED_LANGUAGES.includes(language) ? language : DEFAULT_LANGUAGE; + let message = ERROR_MESSAGES[lang]?.[messageKey] || ERROR_MESSAGES[DEFAULT_LANGUAGE]?.[messageKey] || messageKey; + + // Replace parameters in message + Object.keys(params).forEach(key => { + message = message.replace(new RegExp(`{${key}}`, 'g'), params[key]); + }); + + return message; +} + +/** + * Get localized system message + */ +function getSystemMessage(messageKey, language = DEFAULT_LANGUAGE, params = {}) { + const lang = SUPPORTED_LANGUAGES.includes(language) ? language : DEFAULT_LANGUAGE; + let message = SYSTEM_MESSAGES[lang]?.[messageKey] || SYSTEM_MESSAGES[DEFAULT_LANGUAGE]?.[messageKey] || messageKey; + + // Replace parameters in message + Object.keys(params).forEach(key => { + message = message.replace(new RegExp(`{${key}}`, 'g'), params[key]); + }); + + return message; +} + +/** + * Get localized drone type name + */ +function getDroneTypeName(droneType, language = DEFAULT_LANGUAGE) { + const lang = SUPPORTED_LANGUAGES.includes(language) ? language : DEFAULT_LANGUAGE; + return DRONE_TYPES[lang]?.[droneType] || DRONE_TYPES[DEFAULT_LANGUAGE]?.[droneType] || 'Unknown'; +} + +/** + * Create localized error response + */ +function createErrorResponse(req, status, errorCode, params = {}) { + const language = getLanguageFromRequest(req); + const message = getErrorMessage(errorCode, language, params); + + return { + status, + json: { + success: false, + message, + error: errorCode, + errorCode, + language, + ...params + } + }; +} + +module.exports = { + getLanguageFromRequest, + getErrorMessage, + getSystemMessage, + getDroneTypeName, + createErrorResponse, + SUPPORTED_LANGUAGES, + DEFAULT_LANGUAGE +}; \ No newline at end of file