Extract magic numbers to centralized constants module
Created backend/src/config/constants.js to centralize all magic numbers and configuration values used throughout the backend codebase. Changes: - Created constants.js with organized sections for: - SECURITY: JWT, rate limiting, password reset - AUTH: OTP configuration and rate limiting - SERVER: Port, body limits, startup delays - MQTT: Connection settings, cache limits - NOTIFICATIONS: Push settings, quiet hours, batching - SERIAL: Validation patterns and constraints - EMAIL: Template settings and defaults - CRON: Schedule configurations - STORAGE: Avatar storage settings - Updated files to use constants: - index.js: JWT validation, rate limits, startup delays - routes/auth.js: OTP generation, rate limits, JWT expiry - services/mqtt.js: Connection timeouts, cache size - services/notifications.js: Batch size, TTL, quiet hours - utils/serialValidation.js: Serial number constraints - Added comprehensive test suite (30 tests) for constants module - All tests passing (93 total including existing tests) - Validates reasonable values and consistency between related constants Benefits: - Single source of truth for configuration values - Easier to maintain and update settings - Better documentation of what each value represents - Improved code readability by removing hardcoded numbers - Testable configuration values 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
8456e85cfe
commit
bbb60a9e3f
206
backend/src/config/__tests__/constants.test.js
Normal file
206
backend/src/config/__tests__/constants.test.js
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
/**
|
||||||
|
* Tests for constants module
|
||||||
|
*/
|
||||||
|
|
||||||
|
const {
|
||||||
|
SECURITY,
|
||||||
|
AUTH,
|
||||||
|
SERVER,
|
||||||
|
MQTT,
|
||||||
|
NOTIFICATIONS,
|
||||||
|
SERIAL,
|
||||||
|
EMAIL,
|
||||||
|
CRON,
|
||||||
|
STORAGE,
|
||||||
|
} = require('../constants');
|
||||||
|
|
||||||
|
describe('Constants Module', () => {
|
||||||
|
describe('SECURITY', () => {
|
||||||
|
it('should have valid JWT configuration', () => {
|
||||||
|
expect(SECURITY.JWT_MIN_SECRET_LENGTH).toBe(32);
|
||||||
|
expect(SECURITY.JWT_DEFAULT_EXPIRES_IN).toBe('7d');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have valid rate limiting configuration', () => {
|
||||||
|
expect(SECURITY.RATE_LIMIT.WINDOW_MS).toBe(15 * 60 * 1000);
|
||||||
|
expect(SECURITY.RATE_LIMIT.MAX_REQUESTS).toBe(100);
|
||||||
|
expect(SECURITY.RATE_LIMIT.AUTH_MAX_REQUESTS).toBe(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have password reset configuration', () => {
|
||||||
|
expect(SECURITY.PASSWORD_RESET_TOKEN_EXPIRY_HOURS).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('AUTH', () => {
|
||||||
|
it('should have valid OTP configuration', () => {
|
||||||
|
expect(AUTH.OTP_CODE_LENGTH).toBe(6);
|
||||||
|
expect(AUTH.OTP_CODE_MIN).toBe(100000);
|
||||||
|
expect(AUTH.OTP_CODE_MAX).toBe(999999);
|
||||||
|
expect(AUTH.OTP_EXPIRY_MINUTES).toBe(10);
|
||||||
|
expect(AUTH.OTP_EXPIRY_MS).toBe(10 * 60 * 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have OTP rate limiting configuration', () => {
|
||||||
|
expect(AUTH.OTP_VERIFY_MAX_ATTEMPTS).toBe(5);
|
||||||
|
expect(AUTH.OTP_VERIFY_WINDOW_MS).toBe(15 * 60 * 1000);
|
||||||
|
expect(AUTH.OTP_REQUEST_MAX_ATTEMPTS).toBe(3);
|
||||||
|
expect(AUTH.OTP_REQUEST_WINDOW_MS).toBe(15 * 60 * 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ensure OTP code range is 6 digits', () => {
|
||||||
|
expect(AUTH.OTP_CODE_MIN.toString().length).toBe(6);
|
||||||
|
expect(AUTH.OTP_CODE_MAX.toString().length).toBe(6);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('SERVER', () => {
|
||||||
|
it('should have valid server configuration', () => {
|
||||||
|
expect(SERVER.DEFAULT_PORT).toBe(3000);
|
||||||
|
expect(SERVER.JSON_BODY_LIMIT).toBe('10mb');
|
||||||
|
expect(SERVER.TRUST_PROXY_LEVEL).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have valid startup delays', () => {
|
||||||
|
expect(SERVER.INITIAL_SYNC_DELAY_MS).toBe(10000);
|
||||||
|
expect(SERVER.MQTT_SUBSCRIBE_DELAY_MS).toBe(3000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('MQTT', () => {
|
||||||
|
it('should have valid connection settings', () => {
|
||||||
|
expect(MQTT.RECONNECT_PERIOD_MS).toBe(5000);
|
||||||
|
expect(MQTT.KEEPALIVE_SECONDS).toBe(60);
|
||||||
|
expect(MQTT.DEFAULT_PORT).toBe(1883);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have valid cache settings', () => {
|
||||||
|
expect(MQTT.MAX_ALERTS_CACHE).toBe(100);
|
||||||
|
expect(MQTT.DEFAULT_ALERTS_LIMIT).toBe(50);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('NOTIFICATIONS', () => {
|
||||||
|
it('should have valid push notification settings', () => {
|
||||||
|
expect(NOTIFICATIONS.BATCH_SIZE).toBe(100);
|
||||||
|
expect(NOTIFICATIONS.DEFAULT_TTL_SECONDS).toBe(86400);
|
||||||
|
expect(NOTIFICATIONS.DEFAULT_PRIORITY).toBe('high');
|
||||||
|
expect(NOTIFICATIONS.DEFAULT_SOUND).toBe('default');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have valid channel IDs', () => {
|
||||||
|
expect(NOTIFICATIONS.DEFAULT_CHANNEL_ID).toBe('default');
|
||||||
|
expect(NOTIFICATIONS.EMERGENCY_CHANNEL_ID).toBe('emergency');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have valid quiet hours defaults', () => {
|
||||||
|
expect(NOTIFICATIONS.DEFAULT_QUIET_START).toBe('22:00');
|
||||||
|
expect(NOTIFICATIONS.DEFAULT_QUIET_END).toBe('07:00');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have valid history settings', () => {
|
||||||
|
expect(NOTIFICATIONS.HISTORY_DEFAULT_LIMIT).toBe(50);
|
||||||
|
expect(NOTIFICATIONS.HISTORY_DEFAULT_OFFSET).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have time conversion constant', () => {
|
||||||
|
expect(NOTIFICATIONS.MINUTES_PER_HOUR).toBe(60);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('SERIAL', () => {
|
||||||
|
it('should have valid serial number constraints', () => {
|
||||||
|
expect(SERIAL.MIN_LENGTH).toBe(8);
|
||||||
|
expect(SERIAL.PRODUCTION_LENGTH).toBe(16);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have valid regex patterns', () => {
|
||||||
|
expect(SERIAL.PRODUCTION_PATTERN).toBeInstanceOf(RegExp);
|
||||||
|
expect(SERIAL.LEGACY_PATTERN).toBeInstanceOf(RegExp);
|
||||||
|
expect(SERIAL.VALID_CHARS_PATTERN).toBeInstanceOf(RegExp);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate production format correctly', () => {
|
||||||
|
expect(SERIAL.PRODUCTION_PATTERN.test('WELLNUO-1234-5678')).toBe(true);
|
||||||
|
expect(SERIAL.PRODUCTION_PATTERN.test('WELLNUO-ABCD-EFGH')).toBe(true);
|
||||||
|
expect(SERIAL.PRODUCTION_PATTERN.test('WELLNUO-1234-567')).toBe(false);
|
||||||
|
expect(SERIAL.PRODUCTION_PATTERN.test('INVALID-1234-5678')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate legacy format correctly', () => {
|
||||||
|
expect(SERIAL.LEGACY_PATTERN.test('12345678')).toBe(true);
|
||||||
|
expect(SERIAL.LEGACY_PATTERN.test('ABC-12345')).toBe(true);
|
||||||
|
expect(SERIAL.LEGACY_PATTERN.test('1234567')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate character set correctly', () => {
|
||||||
|
expect(SERIAL.VALID_CHARS_PATTERN.test('ABC123-XYZ')).toBe(true);
|
||||||
|
expect(SERIAL.VALID_CHARS_PATTERN.test('ABC@123')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('EMAIL', () => {
|
||||||
|
it('should have valid template settings', () => {
|
||||||
|
expect(EMAIL.MAX_CONTAINER_WIDTH).toBe(600);
|
||||||
|
expect(EMAIL.LOGO_FONT_SIZE).toBe(24);
|
||||||
|
expect(EMAIL.TITLE_FONT_SIZE).toBe(20);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have valid OTP email settings', () => {
|
||||||
|
expect(EMAIL.OTP_CODE_FONT_SIZE).toBe(32);
|
||||||
|
expect(EMAIL.OTP_CODE_LETTER_SPACING).toBe(6);
|
||||||
|
expect(EMAIL.OTP_EXPIRY_TEXT_MINUTES).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have valid Brevo defaults', () => {
|
||||||
|
expect(EMAIL.DEFAULT_SENDER_NAME).toBe('WellNuo');
|
||||||
|
expect(EMAIL.DEFAULT_SENDER_EMAIL).toBe('noreply@wellnuo.com');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('CRON', () => {
|
||||||
|
it('should have valid cron schedule', () => {
|
||||||
|
expect(CRON.SYNC_CRON_SCHEDULE).toBe('0 * * * *');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('STORAGE', () => {
|
||||||
|
it('should have valid avatar settings', () => {
|
||||||
|
expect(STORAGE.AVATAR_FILENAME_PREFIX).toBe('user-');
|
||||||
|
expect(STORAGE.AVATAR_FOLDER).toBe('avatars/users');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Time consistency', () => {
|
||||||
|
it('should have consistent OTP expiry times', () => {
|
||||||
|
expect(AUTH.OTP_EXPIRY_MS).toBe(AUTH.OTP_EXPIRY_MINUTES * 60 * 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have consistent rate limit windows', () => {
|
||||||
|
expect(AUTH.OTP_VERIFY_WINDOW_MS).toBe(SECURITY.RATE_LIMIT.WINDOW_MS);
|
||||||
|
expect(AUTH.OTP_REQUEST_WINDOW_MS).toBe(SECURITY.RATE_LIMIT.WINDOW_MS);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Reasonable values', () => {
|
||||||
|
it('should have reasonable timeout values', () => {
|
||||||
|
// Timeouts should be positive and not too large
|
||||||
|
expect(MQTT.RECONNECT_PERIOD_MS).toBeGreaterThan(0);
|
||||||
|
expect(MQTT.RECONNECT_PERIOD_MS).toBeLessThan(60000);
|
||||||
|
expect(SERVER.INITIAL_SYNC_DELAY_MS).toBeGreaterThan(0);
|
||||||
|
expect(SERVER.INITIAL_SYNC_DELAY_MS).toBeLessThan(60000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have reasonable cache limits', () => {
|
||||||
|
expect(MQTT.MAX_ALERTS_CACHE).toBeGreaterThan(0);
|
||||||
|
expect(MQTT.MAX_ALERTS_CACHE).toBeLessThan(1000);
|
||||||
|
expect(MQTT.DEFAULT_ALERTS_LIMIT).toBeGreaterThan(0);
|
||||||
|
expect(MQTT.DEFAULT_ALERTS_LIMIT).toBeLessThanOrEqual(MQTT.MAX_ALERTS_CACHE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have reasonable batch sizes', () => {
|
||||||
|
expect(NOTIFICATIONS.BATCH_SIZE).toBeGreaterThan(0);
|
||||||
|
expect(NOTIFICATIONS.BATCH_SIZE).toBeLessThanOrEqual(1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
152
backend/src/config/constants.js
Normal file
152
backend/src/config/constants.js
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
/**
|
||||||
|
* Application Constants
|
||||||
|
*
|
||||||
|
* Centralized configuration for magic numbers used throughout the backend.
|
||||||
|
* All time values are in milliseconds unless otherwise specified.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============ SECURITY ============
|
||||||
|
|
||||||
|
const SECURITY = {
|
||||||
|
// JWT Configuration
|
||||||
|
JWT_MIN_SECRET_LENGTH: 32,
|
||||||
|
JWT_DEFAULT_EXPIRES_IN: '7d',
|
||||||
|
|
||||||
|
// Password Reset
|
||||||
|
PASSWORD_RESET_TOKEN_EXPIRY_HOURS: 1,
|
||||||
|
|
||||||
|
// Rate Limiting
|
||||||
|
RATE_LIMIT: {
|
||||||
|
WINDOW_MS: 15 * 60 * 1000, // 15 minutes
|
||||||
|
MAX_REQUESTS: 100,
|
||||||
|
AUTH_MAX_REQUESTS: 30,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============ AUTHENTICATION ============
|
||||||
|
|
||||||
|
const AUTH = {
|
||||||
|
// OTP Configuration
|
||||||
|
OTP_CODE_LENGTH: 6,
|
||||||
|
OTP_CODE_MIN: 100000,
|
||||||
|
OTP_CODE_MAX: 999999,
|
||||||
|
OTP_EXPIRY_MINUTES: 10,
|
||||||
|
OTP_EXPIRY_MS: 10 * 60 * 1000,
|
||||||
|
|
||||||
|
// Rate Limiting for OTP
|
||||||
|
OTP_VERIFY_MAX_ATTEMPTS: 5,
|
||||||
|
OTP_VERIFY_WINDOW_MS: 15 * 60 * 1000, // 15 minutes
|
||||||
|
OTP_REQUEST_MAX_ATTEMPTS: 3,
|
||||||
|
OTP_REQUEST_WINDOW_MS: 15 * 60 * 1000, // 15 minutes
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============ SERVER ============
|
||||||
|
|
||||||
|
const SERVER = {
|
||||||
|
// Default Port
|
||||||
|
DEFAULT_PORT: 3000,
|
||||||
|
|
||||||
|
// Body Parser Limits
|
||||||
|
JSON_BODY_LIMIT: '10mb',
|
||||||
|
|
||||||
|
// Startup Delays
|
||||||
|
INITIAL_SYNC_DELAY_MS: 10000, // 10 seconds
|
||||||
|
MQTT_SUBSCRIBE_DELAY_MS: 3000, // 3 seconds
|
||||||
|
|
||||||
|
// Trust Proxy
|
||||||
|
TRUST_PROXY_LEVEL: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============ MQTT ============
|
||||||
|
|
||||||
|
const MQTT = {
|
||||||
|
// Connection Settings
|
||||||
|
RECONNECT_PERIOD_MS: 5000, // 5 seconds
|
||||||
|
KEEPALIVE_SECONDS: 60,
|
||||||
|
|
||||||
|
// Alert Cache
|
||||||
|
MAX_ALERTS_CACHE: 100,
|
||||||
|
DEFAULT_ALERTS_LIMIT: 50,
|
||||||
|
|
||||||
|
// Default Broker Settings
|
||||||
|
DEFAULT_PORT: 1883,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============ NOTIFICATIONS ============
|
||||||
|
|
||||||
|
const NOTIFICATIONS = {
|
||||||
|
// Push Notification Settings
|
||||||
|
BATCH_SIZE: 100, // Expo recommends batches of 100
|
||||||
|
DEFAULT_TTL_SECONDS: 86400, // 24 hours
|
||||||
|
DEFAULT_PRIORITY: 'high',
|
||||||
|
DEFAULT_SOUND: 'default',
|
||||||
|
DEFAULT_CHANNEL_ID: 'default',
|
||||||
|
EMERGENCY_CHANNEL_ID: 'emergency',
|
||||||
|
|
||||||
|
// Quiet Hours Defaults
|
||||||
|
DEFAULT_QUIET_START: '22:00',
|
||||||
|
DEFAULT_QUIET_END: '07:00',
|
||||||
|
|
||||||
|
// History
|
||||||
|
HISTORY_DEFAULT_LIMIT: 50,
|
||||||
|
HISTORY_DEFAULT_OFFSET: 0,
|
||||||
|
|
||||||
|
// Time Conversion
|
||||||
|
MINUTES_PER_HOUR: 60,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============ SERIAL VALIDATION ============
|
||||||
|
|
||||||
|
const SERIAL = {
|
||||||
|
// Serial Number Formats
|
||||||
|
MIN_LENGTH: 8,
|
||||||
|
PRODUCTION_LENGTH: 16,
|
||||||
|
PRODUCTION_PATTERN: /^WELLNUO-[A-Z0-9]{4}-[A-Z0-9]{4}$/,
|
||||||
|
LEGACY_PATTERN: /^[A-Z0-9\-]{8,}$/,
|
||||||
|
VALID_CHARS_PATTERN: /^[A-Z0-9\-]+$/,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============ EMAIL ============
|
||||||
|
|
||||||
|
const EMAIL = {
|
||||||
|
// Email Template Settings
|
||||||
|
MAX_CONTAINER_WIDTH: 600,
|
||||||
|
LOGO_FONT_SIZE: 24,
|
||||||
|
TITLE_FONT_SIZE: 20,
|
||||||
|
|
||||||
|
// OTP Email
|
||||||
|
OTP_CODE_FONT_SIZE: 32,
|
||||||
|
OTP_CODE_LETTER_SPACING: 6,
|
||||||
|
OTP_EXPIRY_TEXT_MINUTES: 10,
|
||||||
|
|
||||||
|
// Brevo Defaults
|
||||||
|
DEFAULT_SENDER_NAME: 'WellNuo',
|
||||||
|
DEFAULT_SENDER_EMAIL: 'noreply@wellnuo.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============ CRON JOBS ============
|
||||||
|
|
||||||
|
const CRON = {
|
||||||
|
// Subscription Sync
|
||||||
|
SYNC_CRON_SCHEDULE: '0 * * * *', // Every hour
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============ STORAGE ============
|
||||||
|
|
||||||
|
const STORAGE = {
|
||||||
|
// Avatar Settings
|
||||||
|
AVATAR_FILENAME_PREFIX: 'user-',
|
||||||
|
AVATAR_FOLDER: 'avatars/users',
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
SECURITY,
|
||||||
|
AUTH,
|
||||||
|
SERVER,
|
||||||
|
MQTT,
|
||||||
|
NOTIFICATIONS,
|
||||||
|
SERIAL,
|
||||||
|
EMAIL,
|
||||||
|
CRON,
|
||||||
|
STORAGE,
|
||||||
|
};
|
||||||
@ -1,9 +1,11 @@
|
|||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const { SECURITY, SERVER, CRON } = require('./config/constants');
|
||||||
|
|
||||||
// ============ SECURITY VALIDATION ============
|
// ============ SECURITY VALIDATION ============
|
||||||
// Validate JWT_SECRET at startup
|
// Validate JWT_SECRET at startup
|
||||||
if (!process.env.JWT_SECRET || process.env.JWT_SECRET.length < 32) {
|
if (!process.env.JWT_SECRET || process.env.JWT_SECRET.length < SECURITY.JWT_MIN_SECRET_LENGTH) {
|
||||||
console.error('JWT_SECRET must be at least 32 characters!');
|
console.error(`JWT_SECRET must be at least ${SECURITY.JWT_MIN_SECRET_LENGTH} characters!`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,10 +31,10 @@ const { syncAllSubscriptions } = require('./services/subscription-sync');
|
|||||||
const mqttService = require('./services/mqtt');
|
const mqttService = require('./services/mqtt');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || SERVER.DEFAULT_PORT;
|
||||||
|
|
||||||
// Trust proxy for correct IP when behind nginx
|
// Trust proxy for correct IP when behind nginx
|
||||||
app.set('trust proxy', 1);
|
app.set('trust proxy', SERVER.TRUST_PROXY_LEVEL);
|
||||||
|
|
||||||
// ============ SECURITY ============
|
// ============ SECURITY ============
|
||||||
|
|
||||||
@ -73,8 +75,8 @@ app.use(cors({
|
|||||||
|
|
||||||
// Rate Limiting - защита от DDoS
|
// Rate Limiting - защита от DDoS
|
||||||
const limiter = rateLimit({
|
const limiter = rateLimit({
|
||||||
windowMs: 15 * 60 * 1000, // 15 минут
|
windowMs: SECURITY.RATE_LIMIT.WINDOW_MS,
|
||||||
max: 100, // максимум 100 запросов за 15 минут
|
max: SECURITY.RATE_LIMIT.MAX_REQUESTS,
|
||||||
message: { error: 'Too many requests, please try again later' },
|
message: { error: 'Too many requests, please try again later' },
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false
|
legacyHeaders: false
|
||||||
@ -82,8 +84,8 @@ const limiter = rateLimit({
|
|||||||
|
|
||||||
// Лимит для auth endpoints
|
// Лимит для auth endpoints
|
||||||
const authLimiter = rateLimit({
|
const authLimiter = rateLimit({
|
||||||
windowMs: 15 * 60 * 1000,
|
windowMs: SECURITY.RATE_LIMIT.WINDOW_MS,
|
||||||
max: 30, // 30 попыток логина за 15 минут
|
max: SECURITY.RATE_LIMIT.AUTH_MAX_REQUESTS,
|
||||||
message: { error: 'Too many login attempts, please try again later' },
|
message: { error: 'Too many login attempts, please try again later' },
|
||||||
// Пропускаем админов без лимита
|
// Пропускаем админов без лимита
|
||||||
skip: (req) => {
|
skip: (req) => {
|
||||||
@ -103,8 +105,8 @@ app.use('/api/webhook/stripe', express.raw({ type: 'application/json' }));
|
|||||||
|
|
||||||
// JSON body parser for other routes
|
// JSON body parser for other routes
|
||||||
// Increased limit for base64 avatar uploads
|
// Increased limit for base64 avatar uploads
|
||||||
app.use(express.json({ limit: '10mb' }));
|
app.use(express.json({ limit: SERVER.JSON_BODY_LIMIT }));
|
||||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
app.use(express.urlencoded({ extended: true, limit: SERVER.JSON_BODY_LIMIT }));
|
||||||
|
|
||||||
// ============ ROUTES ============
|
// ============ ROUTES ============
|
||||||
|
|
||||||
@ -164,18 +166,18 @@ app.get('/api', (req, res) => {
|
|||||||
// ============ CRON JOBS ============
|
// ============ CRON JOBS ============
|
||||||
|
|
||||||
// Sync subscriptions from Stripe every hour
|
// Sync subscriptions from Stripe every hour
|
||||||
cron.schedule('0 * * * *', async () => {
|
cron.schedule(CRON.SYNC_CRON_SCHEDULE, async () => {
|
||||||
console.log('[CRON] Running subscription sync...');
|
console.log('[CRON] Running subscription sync...');
|
||||||
const result = await syncAllSubscriptions();
|
const result = await syncAllSubscriptions();
|
||||||
console.log('[CRON] Subscription sync result:', result);
|
console.log('[CRON] Subscription sync result:', result);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Run sync on startup (after 10 seconds to let everything initialize)
|
// Run sync on startup (after delay to let everything initialize)
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
console.log('[STARTUP] Running initial subscription sync...');
|
console.log('[STARTUP] Running initial subscription sync...');
|
||||||
const result = await syncAllSubscriptions();
|
const result = await syncAllSubscriptions();
|
||||||
console.log('[STARTUP] Initial sync result:', result);
|
console.log('[STARTUP] Initial sync result:', result);
|
||||||
}, 10000);
|
}, SERVER.INITIAL_SYNC_DELAY_MS);
|
||||||
|
|
||||||
// Manual sync endpoint (for admin)
|
// Manual sync endpoint (for admin)
|
||||||
app.post('/api/admin/sync-subscriptions', async (req, res) => {
|
app.post('/api/admin/sync-subscriptions', async (req, res) => {
|
||||||
@ -195,7 +197,7 @@ app.listen(PORT, () => {
|
|||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const deployments = await mqttService.subscribeToAllDeployments();
|
const deployments = await mqttService.subscribeToAllDeployments();
|
||||||
console.log(`[MQTT] Subscribed to ${deployments.length} deployments:`, deployments);
|
console.log(`[MQTT] Subscribed to ${deployments.length} deployments:`, deployments);
|
||||||
}, 3000);
|
}, SERVER.MQTT_SUBSCRIBE_DELAY_MS);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Graceful shutdown
|
// Graceful shutdown
|
||||||
|
|||||||
@ -6,11 +6,12 @@ const rateLimit = require('express-rate-limit');
|
|||||||
const { supabase } = require('../config/supabase');
|
const { supabase } = require('../config/supabase');
|
||||||
const { sendOTPEmail } = require('../services/email');
|
const { sendOTPEmail } = require('../services/email');
|
||||||
const storage = require('../services/storage');
|
const storage = require('../services/storage');
|
||||||
|
const { AUTH, SECURITY, STORAGE } = require('../config/constants');
|
||||||
|
|
||||||
// Rate limiter for OTP verification: 5 attempts per 15 minutes per email
|
// Rate limiter for OTP verification
|
||||||
const verifyOtpLimiter = rateLimit({
|
const verifyOtpLimiter = rateLimit({
|
||||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
windowMs: AUTH.OTP_VERIFY_WINDOW_MS,
|
||||||
max: 5,
|
max: AUTH.OTP_VERIFY_MAX_ATTEMPTS,
|
||||||
keyGenerator: (req) => {
|
keyGenerator: (req) => {
|
||||||
// Use email only - avoid IP-based limiting issues
|
// Use email only - avoid IP-based limiting issues
|
||||||
const email = req.body?.email?.toLowerCase()?.trim();
|
const email = req.body?.email?.toLowerCase()?.trim();
|
||||||
@ -21,10 +22,10 @@ const verifyOtpLimiter = rateLimit({
|
|||||||
legacyHeaders: false,
|
legacyHeaders: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Rate limiter for OTP request: 3 attempts per 15 minutes per email
|
// Rate limiter for OTP request
|
||||||
const requestOtpLimiter = rateLimit({
|
const requestOtpLimiter = rateLimit({
|
||||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
windowMs: AUTH.OTP_REQUEST_WINDOW_MS,
|
||||||
max: 3,
|
max: AUTH.OTP_REQUEST_MAX_ATTEMPTS,
|
||||||
keyGenerator: (req) => {
|
keyGenerator: (req) => {
|
||||||
// Use email only - avoid IP-based limiting issues
|
// Use email only - avoid IP-based limiting issues
|
||||||
const email = req.body?.email?.toLowerCase()?.trim();
|
const email = req.body?.email?.toLowerCase()?.trim();
|
||||||
@ -119,8 +120,8 @@ router.post('/request-otp', requestOtpLimiter, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Генерируем 6-значный OTP код
|
// Генерируем 6-значный OTP код
|
||||||
const otpCode = crypto.randomInt(100000, 999999).toString();
|
const otpCode = crypto.randomInt(AUTH.OTP_CODE_MIN, AUTH.OTP_CODE_MAX).toString();
|
||||||
const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 минут
|
const expiresAt = new Date(Date.now() + AUTH.OTP_EXPIRY_MS);
|
||||||
|
|
||||||
// Удаляем старые неиспользованные коды для этого email
|
// Удаляем старые неиспользованные коды для этого email
|
||||||
await supabase
|
await supabase
|
||||||
@ -266,7 +267,7 @@ router.post('/verify-otp', verifyOtpLimiter, async (req, res) => {
|
|||||||
email: user.email
|
email: user.email
|
||||||
},
|
},
|
||||||
process.env.JWT_SECRET,
|
process.env.JWT_SECRET,
|
||||||
{ expiresIn: process.env.JWT_EXPIRES_IN || '7d' }
|
{ expiresIn: process.env.JWT_EXPIRES_IN || SECURITY.JWT_DEFAULT_EXPIRES_IN }
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@ -503,8 +504,8 @@ router.patch('/avatar', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Upload new avatar to MinIO
|
// Upload new avatar to MinIO
|
||||||
const filename = `user-${userId}-${Date.now()}`;
|
const filename = `${STORAGE.AVATAR_FILENAME_PREFIX}${userId}-${Date.now()}`;
|
||||||
const result = await storage.uploadBase64Image(avatar, 'avatars/users', filename);
|
const result = await storage.uploadBase64Image(avatar, STORAGE.AVATAR_FOLDER, filename);
|
||||||
avatarUrl = result.url;
|
avatarUrl = result.url;
|
||||||
|
|
||||||
console.log('[AUTH] Avatar uploaded to MinIO:', avatarUrl);
|
console.log('[AUTH] Avatar uploaded to MinIO:', avatarUrl);
|
||||||
|
|||||||
@ -14,15 +14,16 @@
|
|||||||
const mqtt = require('mqtt');
|
const mqtt = require('mqtt');
|
||||||
const { pool } = require('../config/database');
|
const { pool } = require('../config/database');
|
||||||
const { sendPushNotifications: sendNotificationsWithSettings, NotificationType } = require('./notifications');
|
const { sendPushNotifications: sendNotificationsWithSettings, NotificationType } = require('./notifications');
|
||||||
|
const { MQTT } = require('../config/constants');
|
||||||
|
|
||||||
// MQTT Configuration
|
// MQTT Configuration
|
||||||
const MQTT_BROKER = process.env.MQTT_BROKER || 'mqtt://mqtt.eluxnetworks.net:1883';
|
const MQTT_BROKER = process.env.MQTT_BROKER || `mqtt://mqtt.eluxnetworks.net:${MQTT.DEFAULT_PORT}`;
|
||||||
const MQTT_USER = process.env.MQTT_USER || process.env.LEGACY_API_USERNAME || 'robster';
|
const MQTT_USER = process.env.MQTT_USER || process.env.LEGACY_API_USERNAME || 'robster';
|
||||||
const MQTT_PASSWORD = process.env.MQTT_PASSWORD || process.env.LEGACY_API_PASSWORD || 'rob2';
|
const MQTT_PASSWORD = process.env.MQTT_PASSWORD || process.env.LEGACY_API_PASSWORD || 'rob2';
|
||||||
|
|
||||||
// Store for received alerts (in-memory, last 100)
|
// Store for received alerts (in-memory)
|
||||||
const alertsCache = [];
|
const alertsCache = [];
|
||||||
const MAX_ALERTS_CACHE = 100;
|
const MAX_ALERTS_CACHE = MQTT.MAX_ALERTS_CACHE;
|
||||||
|
|
||||||
// MQTT Client
|
// MQTT Client
|
||||||
let client = null;
|
let client = null;
|
||||||
@ -44,8 +45,8 @@ function init() {
|
|||||||
username: MQTT_USER,
|
username: MQTT_USER,
|
||||||
password: MQTT_PASSWORD,
|
password: MQTT_PASSWORD,
|
||||||
clientId: `wellnuo-backend-${Date.now()}`,
|
clientId: `wellnuo-backend-${Date.now()}`,
|
||||||
reconnectPeriod: 5000, // Reconnect every 5 seconds
|
reconnectPeriod: MQTT.RECONNECT_PERIOD_MS,
|
||||||
keepalive: 60,
|
keepalive: MQTT.KEEPALIVE_SECONDS,
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('connect', () => {
|
client.on('connect', () => {
|
||||||
@ -335,7 +336,7 @@ function subscribeToDeployments(deploymentIds) {
|
|||||||
/**
|
/**
|
||||||
* Get recent alerts from cache
|
* Get recent alerts from cache
|
||||||
*/
|
*/
|
||||||
function getRecentAlerts(limit = 50, deploymentId = null) {
|
function getRecentAlerts(limit = MQTT.DEFAULT_ALERTS_LIMIT, deploymentId = null) {
|
||||||
let alerts = alertsCache;
|
let alerts = alertsCache;
|
||||||
|
|
||||||
if (deploymentId) {
|
if (deploymentId) {
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const { supabase } = require('../config/supabase');
|
const { supabase } = require('../config/supabase');
|
||||||
|
const { NOTIFICATIONS } = require('../config/constants');
|
||||||
|
|
||||||
// Expo Push API endpoint
|
// Expo Push API endpoint
|
||||||
const EXPO_PUSH_URL = 'https://exp.host/--/api/v2/push/send';
|
const EXPO_PUSH_URL = 'https://exp.host/--/api/v2/push/send';
|
||||||
@ -51,9 +52,9 @@ function isInQuietHours(quietStart, quietEnd, timezone = 'UTC') {
|
|||||||
const [currentHour, currentMin] = timeStr.split(':').map(Number);
|
const [currentHour, currentMin] = timeStr.split(':').map(Number);
|
||||||
|
|
||||||
// Convert to minutes since midnight for easier comparison
|
// Convert to minutes since midnight for easier comparison
|
||||||
const currentMinutes = currentHour * 60 + currentMin;
|
const currentMinutes = currentHour * NOTIFICATIONS.MINUTES_PER_HOUR + currentMin;
|
||||||
const startMinutes = startHour * 60 + startMin;
|
const startMinutes = startHour * NOTIFICATIONS.MINUTES_PER_HOUR + startMin;
|
||||||
const endMinutes = endHour * 60 + endMin;
|
const endMinutes = endHour * NOTIFICATIONS.MINUTES_PER_HOUR + endMin;
|
||||||
|
|
||||||
// Handle overnight quiet hours (e.g., 22:00 - 07:00)
|
// Handle overnight quiet hours (e.g., 22:00 - 07:00)
|
||||||
if (startMinutes > endMinutes) {
|
if (startMinutes > endMinutes) {
|
||||||
@ -128,8 +129,8 @@ function shouldSendNotification(settings, notificationType) {
|
|||||||
|
|
||||||
// Check quiet hours (except for emergency which returns early)
|
// Check quiet hours (except for emergency which returns early)
|
||||||
if (settings.quiet_hours_enabled) {
|
if (settings.quiet_hours_enabled) {
|
||||||
const quietStart = settings.quiet_hours_start || '22:00';
|
const quietStart = settings.quiet_hours_start || NOTIFICATIONS.DEFAULT_QUIET_START;
|
||||||
const quietEnd = settings.quiet_hours_end || '07:00';
|
const quietEnd = settings.quiet_hours_end || NOTIFICATIONS.DEFAULT_QUIET_END;
|
||||||
|
|
||||||
if (isInQuietHours(quietStart, quietEnd)) {
|
if (isInQuietHours(quietStart, quietEnd)) {
|
||||||
return { allowed: false, reason: 'quiet_hours' };
|
return { allowed: false, reason: 'quiet_hours' };
|
||||||
@ -297,11 +298,11 @@ async function sendPushNotifications({
|
|||||||
body,
|
body,
|
||||||
type = NotificationType.SYSTEM,
|
type = NotificationType.SYSTEM,
|
||||||
data = {},
|
data = {},
|
||||||
sound = 'default',
|
sound = NOTIFICATIONS.DEFAULT_SOUND,
|
||||||
channelId = 'default',
|
channelId = NOTIFICATIONS.DEFAULT_CHANNEL_ID,
|
||||||
badge,
|
badge,
|
||||||
ttl = 86400, // 24 hours default
|
ttl = NOTIFICATIONS.DEFAULT_TTL_SECONDS,
|
||||||
priority = 'high',
|
priority = NOTIFICATIONS.DEFAULT_PRIORITY,
|
||||||
beneficiaryId = null
|
beneficiaryId = null
|
||||||
}) {
|
}) {
|
||||||
// Normalize userIds to array
|
// Normalize userIds to array
|
||||||
@ -427,7 +428,7 @@ async function sendPushNotifications({
|
|||||||
// Send all messages in batch
|
// Send all messages in batch
|
||||||
if (messagesToSend.length > 0) {
|
if (messagesToSend.length > 0) {
|
||||||
// Expo recommends batches of 100
|
// Expo recommends batches of 100
|
||||||
const batchSize = 100;
|
const batchSize = NOTIFICATIONS.BATCH_SIZE;
|
||||||
const batches = [];
|
const batches = [];
|
||||||
|
|
||||||
for (let i = 0; i < messagesToSend.length; i += batchSize) {
|
for (let i = 0; i < messagesToSend.length; i += batchSize) {
|
||||||
@ -547,8 +548,8 @@ async function notifyCaretakers(beneficiaryId, notification) {
|
|||||||
*/
|
*/
|
||||||
async function getNotificationHistory(userId, options = {}) {
|
async function getNotificationHistory(userId, options = {}) {
|
||||||
const {
|
const {
|
||||||
limit = 50,
|
limit = NOTIFICATIONS.HISTORY_DEFAULT_LIMIT,
|
||||||
offset = 0,
|
offset = NOTIFICATIONS.HISTORY_DEFAULT_OFFSET,
|
||||||
type,
|
type,
|
||||||
status
|
status
|
||||||
} = options;
|
} = options;
|
||||||
|
|||||||
@ -7,6 +7,8 @@
|
|||||||
* - Legacy: 8+ alphanumeric characters
|
* - Legacy: 8+ alphanumeric characters
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const { SERIAL } = require('../config/constants');
|
||||||
|
|
||||||
// Demo serial numbers
|
// Demo serial numbers
|
||||||
const DEMO_SERIALS = new Set([
|
const DEMO_SERIALS = new Set([
|
||||||
'DEMO-00000',
|
'DEMO-00000',
|
||||||
@ -52,7 +54,7 @@ function validateSerial(serial) {
|
|||||||
|
|
||||||
// Production format: WELLNUO-XXXX-XXXX
|
// Production format: WELLNUO-XXXX-XXXX
|
||||||
// Must be exactly 16 characters with hyphens at positions 7 and 12
|
// Must be exactly 16 characters with hyphens at positions 7 and 12
|
||||||
if (/^WELLNUO-[A-Z0-9]{4}-[A-Z0-9]{4}$/.test(normalized)) {
|
if (SERIAL.PRODUCTION_PATTERN.test(normalized)) {
|
||||||
return {
|
return {
|
||||||
isValid: true,
|
isValid: true,
|
||||||
format: 'production',
|
format: 'production',
|
||||||
@ -61,7 +63,7 @@ function validateSerial(serial) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Legacy format: minimum 8 alphanumeric characters (no special characters except hyphens)
|
// Legacy format: minimum 8 alphanumeric characters (no special characters except hyphens)
|
||||||
if (/^[A-Z0-9\-]{8,}$/.test(normalized)) {
|
if (SERIAL.LEGACY_PATTERN.test(normalized)) {
|
||||||
return {
|
return {
|
||||||
isValid: true,
|
isValid: true,
|
||||||
format: 'legacy',
|
format: 'legacy',
|
||||||
@ -72,9 +74,9 @@ function validateSerial(serial) {
|
|||||||
// Invalid format
|
// Invalid format
|
||||||
let error = 'Invalid serial number format';
|
let error = 'Invalid serial number format';
|
||||||
|
|
||||||
if (normalized.length < 8) {
|
if (normalized.length < SERIAL.MIN_LENGTH) {
|
||||||
error = 'Serial number must be at least 8 characters';
|
error = `Serial number must be at least ${SERIAL.MIN_LENGTH} characters`;
|
||||||
} else if (!/^[A-Z0-9\-]+$/.test(normalized)) {
|
} else if (!SERIAL.VALID_CHARS_PATTERN.test(normalized)) {
|
||||||
error = 'Serial number can only contain letters, numbers, and hyphens';
|
error = 'Serial number can only contain letters, numbers, and hyphens';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user