mirror of
https://github.com/MrUnknownDE/internetx-ddns-updater.git
synced 2026-04-25 17:43:44 +02:00
feat: Implement the core InterNetX DDNS updater application with API, services, middleware, Docker support, and a detailed README.
This commit is contained in:
47
src/middleware/auth.middleware.js
Normal file
47
src/middleware/auth.middleware.js
Normal file
@@ -0,0 +1,47 @@
|
||||
const config = require('../config/config');
|
||||
const { logger, redactToken } = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* Authentication middleware - validates token
|
||||
*/
|
||||
const authenticate = (req, res, next) => {
|
||||
const token = req.query.token || req.body.token;
|
||||
|
||||
if (!token) {
|
||||
logger.warn('Authentication failed: No token provided', {
|
||||
ip: req.ip,
|
||||
path: req.path
|
||||
});
|
||||
return res.status(401).json({
|
||||
error: 'Authentication required',
|
||||
message: 'Missing token parameter'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if token is valid
|
||||
const isValid = config.security.authTokens.includes(token);
|
||||
|
||||
if (!isValid) {
|
||||
logger.warn('Authentication failed: Invalid token', {
|
||||
ip: req.ip,
|
||||
token: redactToken(token),
|
||||
path: req.path
|
||||
});
|
||||
return res.status(401).json({
|
||||
error: 'Authentication failed',
|
||||
message: 'Invalid token'
|
||||
});
|
||||
}
|
||||
|
||||
// Token is valid, attach redacted version for logging
|
||||
req.tokenPrefix = redactToken(token);
|
||||
|
||||
logger.debug('Authentication successful', {
|
||||
ip: req.ip,
|
||||
token: req.tokenPrefix
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
module.exports = authenticate;
|
||||
50
src/middleware/rateLimiter.middleware.js
Normal file
50
src/middleware/rateLimiter.middleware.js
Normal file
@@ -0,0 +1,50 @@
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const config = require('../config/config');
|
||||
const { logger } = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* Rate limiter for update endpoint
|
||||
*/
|
||||
const updateLimiter = rateLimit({
|
||||
windowMs: config.rateLimit.windowMs,
|
||||
max: config.rateLimit.maxRequests,
|
||||
|
||||
message: {
|
||||
error: 'Too many requests',
|
||||
message: 'Rate limit exceeded. Please try again later.',
|
||||
},
|
||||
|
||||
standardHeaders: true, // Return rate limit info in `RateLimit-*` headers
|
||||
legacyHeaders: false, // Disable `X-RateLimit-*` headers
|
||||
|
||||
// Custom handler for rate limit exceeded
|
||||
handler: (req, res) => {
|
||||
logger.warn('Rate limit exceeded', {
|
||||
ip: req.ip,
|
||||
path: req.path,
|
||||
limit: config.rateLimit.maxRequests,
|
||||
window: `${config.rateLimit.windowMs / 1000}s`,
|
||||
});
|
||||
|
||||
res.status(429).json({
|
||||
error: 'Too many requests',
|
||||
message: 'You have exceeded the rate limit. Please try again later.',
|
||||
retryAfter: Math.ceil(config.rateLimit.windowMs / 1000),
|
||||
});
|
||||
},
|
||||
|
||||
// Skip rate limiting for successful requests (optional)
|
||||
skip: (req) => {
|
||||
// Could skip based on certain conditions, e.g., whitelisted IPs
|
||||
return false;
|
||||
},
|
||||
|
||||
// Key generator - rate limit per IP
|
||||
keyGenerator: (req) => {
|
||||
return req.ip;
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
updateLimiter,
|
||||
};
|
||||
87
src/middleware/validation.middleware.js
Normal file
87
src/middleware/validation.middleware.js
Normal file
@@ -0,0 +1,87 @@
|
||||
const Joi = require('joi');
|
||||
const config = require('../config/config');
|
||||
const { logger } = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* Validation schema for update endpoint
|
||||
*/
|
||||
const updateSchema = Joi.object({
|
||||
hostname: Joi.string()
|
||||
.hostname()
|
||||
.pattern(new RegExp(`^[a-z0-9\\-]+\\.${config.ddns.defaultZone.replace('.', '\\.')}$`, 'i'))
|
||||
.required()
|
||||
.messages({
|
||||
'string.pattern.base': `Hostname must be a subdomain of ${config.ddns.defaultZone}`,
|
||||
'any.required': 'Hostname is required',
|
||||
}),
|
||||
|
||||
myip: Joi.string()
|
||||
.ip({ version: ['ipv4'], cidr: 'forbidden' })
|
||||
.optional()
|
||||
.messages({
|
||||
'string.ip': 'Invalid IPv4 address format',
|
||||
}),
|
||||
|
||||
myipv6: Joi.string()
|
||||
.ip({ version: ['ipv6'], cidr: 'forbidden' })
|
||||
.optional()
|
||||
.messages({
|
||||
'string.ip': 'Invalid IPv6 address format',
|
||||
}),
|
||||
|
||||
token: Joi.string()
|
||||
.min(8)
|
||||
.max(256)
|
||||
.required()
|
||||
.messages({
|
||||
'string.min': 'Token is too short',
|
||||
'any.required': 'Token is required',
|
||||
}),
|
||||
}).or('myip', 'myipv6')
|
||||
.messages({
|
||||
'object.missing': 'At least one IP address (myip or myipv6) is required',
|
||||
});
|
||||
|
||||
/**
|
||||
* Validation middleware factory
|
||||
* @param {Object} schema - Joi validation schema
|
||||
* @returns {Function} - Express middleware
|
||||
*/
|
||||
const validate = (schema) => {
|
||||
return (req, res, next) => {
|
||||
// Merge query and body for validation
|
||||
const data = { ...req.query, ...req.body };
|
||||
|
||||
const { error, value } = schema.validate(data, {
|
||||
abortEarly: false, // Return all errors, not just the first
|
||||
stripUnknown: true, // Remove unknown fields
|
||||
});
|
||||
|
||||
if (error) {
|
||||
const errors = error.details.map(detail => ({
|
||||
field: detail.path.join('.'),
|
||||
message: detail.message,
|
||||
}));
|
||||
|
||||
logger.warn('Validation failed', {
|
||||
ip: req.ip,
|
||||
errors,
|
||||
data: { ...data, token: '***' } // Redact token in logs
|
||||
});
|
||||
|
||||
return res.status(400).json({
|
||||
error: 'Validation failed',
|
||||
details: errors,
|
||||
});
|
||||
}
|
||||
|
||||
// Attach validated data to request
|
||||
req.validatedData = value;
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
validate,
|
||||
updateSchema,
|
||||
};
|
||||
Reference in New Issue
Block a user