@sehirapp/core-microservice
Version:
Modern mikroservis core paketi - MongoDB 6.7, Express API, Mongoose, PM2 cluster desteği
525 lines (458 loc) • 16.2 kB
JavaScript
/**
* Authentication Middleware Suite - Core mikroservisler için kimlik doğrulama
* Multi-tenant, city-based, user/admin rol desteği
*/
import { logger } from '../logger.js';
/**
* Multi-tenant şehir anahtarı doğrulaması
* Header'dan city-key'i alır ve doğrular
*/
export const requireCity = (req, res, next) => {
try {
const cityKey = req.headers['x-city-key'] || req.headers['city-key'] || req.headers['x-city'];
if (!cityKey) {
logger.warn('City key missing in request', {
path: req.path,
method: req.method,
ip: req.ip,
userAgent: req.get('User-Agent')
});
return res.status(400).json({
success: false,
error: 'City key is required',
code: 'MISSING_CITY_KEY',
timestamp: Date.now()
});
}
// City key formatını doğrula
if (typeof cityKey !== 'string' || cityKey.length < 3) {
logger.warn('Invalid city key format', {
cityKey: cityKey.substring(0, 10) + '...', // Güvenlik için kısalt
path: req.path,
method: req.method,
ip: req.ip
});
return res.status(400).json({
success: false,
error: 'Invalid city key format',
code: 'INVALID_CITY_KEY',
timestamp: Date.now()
});
}
// City key'i request'e ekle
req.cityKey = cityKey;
req.city = {
key: cityKey,
id: cityKey, // Alias for compatibility
validated: true
};
logger.debug('City key validated', {
cityKey: cityKey.substring(0, 10) + '...',
path: req.path,
method: req.method
});
next();
} catch (error) {
logger.error('City key validation error', {
error: error.message,
path: req.path,
method: req.method,
stack: error.stack
});
return res.status(500).json({
success: false,
error: 'Internal server error during city validation',
code: 'CITY_VALIDATION_ERROR',
timestamp: Date.now()
});
}
};
/**
* City key'i request'e atar (zorunlu değil)
*/
export const setCityKey = (req, res, next) => {
const cityKey = req.headers['x-city-key'] || req.headers['city-key'];
if (cityKey) {
req.cityKey = cityKey;
req.city = {
key: cityKey,
id: cityKey,
validated: false // Optional olduğu için validated false
};
}
next();
};
/**
* Kullanıcı kimlik doğrulaması
* API Gateway'in token doğrulamış olduğunu varsayar
*/
export const requireUser = (req, res, next) => {
try {
// API Gateway'den gelen user bilgisi
const userId = req.headers['x-user-id'];
const userRole = req.headers['x-user-role'];
const userEmail = req.headers['x-user-email'];
const userToken = req.headers['authorization'];
if (!userId) {
logger.warn('User ID missing in request', {
path: req.path,
method: req.method,
ip: req.ip,
hasToken: !!userToken
});
return res.status(401).json({
success: false,
error: 'User authentication required',
code: 'USER_AUTH_REQUIRED',
timestamp: Date.now()
});
}
// User bilgilerini request'e ekle
req.user = {
id: userId,
role: userRole || 'user',
email: userEmail,
authenticated: true,
token: userToken
};
logger.debug('User authenticated', {
userId,
userRole,
path: req.path,
method: req.method
});
next();
} catch (error) {
logger.error('User authentication error', {
error: error.message,
path: req.path,
method: req.method,
stack: error.stack
});
return res.status(500).json({
success: false,
error: 'Internal server error during user authentication',
code: 'USER_AUTH_ERROR',
timestamp: Date.now()
});
}
};
/**
* Admin rol doğrulaması
*/
export const requireAdmin = (req, res, next) => {
try {
// Önce user doğrulamasını yap
requireUser(req, res, (err) => {
if (err) return next(err);
const userRole = req.user?.role;
const allowedRoles = ['admin', 'super_admin', 'system_admin'];
if (!allowedRoles.includes(userRole)) {
logger.warn('Admin access denied', {
userId: req.user?.id,
userRole,
requiredRoles: allowedRoles,
path: req.path,
method: req.method,
ip: req.ip
});
return res.status(403).json({
success: false,
error: 'Admin access required',
code: 'ADMIN_ACCESS_REQUIRED',
timestamp: Date.now()
});
}
// Admin bilgilerini ekle
req.admin = {
id: req.user.id,
role: req.user.role,
level: userRole === 'super_admin' ? 'super' : 'standard'
};
logger.debug('Admin access granted', {
adminId: req.admin.id,
adminRole: req.admin.role,
path: req.path,
method: req.method
});
next();
});
} catch (error) {
logger.error('Admin authentication error', {
error: error.message,
path: req.path,
method: req.method,
stack: error.stack
});
return res.status(500).json({
success: false,
error: 'Internal server error during admin authentication',
code: 'ADMIN_AUTH_ERROR',
timestamp: Date.now()
});
}
};
/**
* Resource ownership doğrulaması factory
* @param {CoreClass} Model - Model sınıfı
* @param {string} idField - ID alanının adı (default: 'id')
* @param {string} ownerField - Sahiplik alanının adı (default: 'userId')
*/
export const requireOwnership = (Model, idField = 'id', ownerField = 'userId') => {
return async (req, res, next) => {
try {
// User doğrulaması gerekli
if (!req.user?.id) {
return res.status(401).json({
success: false,
error: 'User authentication required for ownership check',
code: 'USER_AUTH_REQUIRED',
timestamp: Date.now()
});
}
const resourceId = req.params[idField];
if (!resourceId) {
return res.status(400).json({
success: false,
error: `Resource ${idField} parameter is required`,
code: 'MISSING_RESOURCE_ID',
timestamp: Date.now()
});
}
// Resource'u bul ve sahiplik kontrol et
const modelInstance = new Model();
const found = await modelInstance.find({ [idField]: resourceId });
if (!found) {
logger.warn('Resource not found for ownership check', {
resourceId,
userId: req.user.id,
model: Model.name,
path: req.path
});
return res.status(404).json({
success: false,
error: 'Resource not found',
code: 'RESOURCE_NOT_FOUND',
timestamp: Date.now()
});
}
// Sahiplik kontrolü - Admin bypass
if (req.admin || req.user.role === 'admin') {
logger.debug('Ownership check bypassed for admin', {
resourceId,
adminId: req.user.id,
model: Model.name
});
} else if (modelInstance[ownerField] !== req.user.id) {
logger.warn('Ownership validation failed', {
resourceId,
userId: req.user.id,
ownerId: modelInstance[ownerField],
model: Model.name,
path: req.path
});
return res.status(403).json({
success: false,
error: 'You do not have access to this resource',
code: 'OWNERSHIP_REQUIRED',
timestamp: Date.now()
});
}
// Resource'u request'e ekle
req.resource = modelInstance;
req.resourceId = resourceId;
logger.debug('Ownership validated', {
resourceId,
userId: req.user.id,
model: Model.name
});
next();
} catch (error) {
logger.error('Ownership validation error', {
error: error.message,
resourceId: req.params[idField],
userId: req.user?.id,
model: Model?.name,
stack: error.stack
});
return res.status(500).json({
success: false,
error: 'Internal server error during ownership validation',
code: 'OWNERSHIP_VALIDATION_ERROR',
timestamp: Date.now()
});
}
};
};
/**
* Request alanları doğrulama middleware'i
* @param {Array<string>} requiredFields - Zorunlu alanlar
* @param {string} source - Kaynak ('body', 'query', 'params')
*/
export const validateRequired = (requiredFields = [], source = 'body') => {
return (req, res, next) => {
try {
const data = req[source];
const missing = [];
if (!data || typeof data !== 'object') {
return res.status(400).json({
success: false,
error: `Request ${source} is required`,
code: 'MISSING_REQUEST_DATA',
timestamp: Date.now()
});
}
requiredFields.forEach(field => {
if (data[field] === undefined || data[field] === null || data[field] === '') {
missing.push(field);
}
});
if (missing.length > 0) {
logger.warn('Required fields missing', {
missing,
source,
path: req.path,
method: req.method,
userId: req.user?.id
});
return res.status(400).json({
success: false,
error: 'Required fields are missing',
code: 'MISSING_REQUIRED_FIELDS',
details: {
missing,
source
},
timestamp: Date.now()
});
}
logger.debug('Required fields validated', {
fields: requiredFields,
source,
path: req.path
});
next();
} catch (error) {
logger.error('Field validation error', {
error: error.message,
requiredFields,
source,
path: req.path,
stack: error.stack
});
return res.status(500).json({
success: false,
error: 'Internal server error during field validation',
code: 'FIELD_VALIDATION_ERROR',
timestamp: Date.now()
});
}
};
};
/**
* Request loglama middleware'i
*/
export const logRequest = (req, res, next) => {
const startTime = Date.now();
// Response'u intercept et
const originalSend = res.send;
res.send = function(body) {
const duration = Date.now() - startTime;
logger.info('Request completed', {
method: req.method,
path: req.path,
statusCode: res.statusCode,
duration: `${duration}ms`,
userId: req.user?.id,
cityKey: req.cityKey ? req.cityKey.substring(0, 10) + '...' : null,
ip: req.ip,
userAgent: req.get('User-Agent'),
contentLength: body ? Buffer.byteLength(body, 'utf8') : 0
});
return originalSend.call(this, body);
};
logger.debug('Request started', {
method: req.method,
path: req.path,
query: req.query,
userId: req.user?.id,
cityKey: req.cityKey ? req.cityKey.substring(0, 10) + '...' : null,
ip: req.ip
});
next();
};
/**
* Rate limiting ve güvenlik kontrolleri
* @param {Object} options - Rate limiting seçenekleri
*/
export const rateLimitByUser = (options = {}) => {
const {
windowMs = 15 * 60 * 1000, // 15 dakika
maxRequests = 100,
skipSuccessfulRequests = false,
skipFailedRequests = false
} = options;
const userLimits = new Map();
return (req, res, next) => {
const userId = req.user?.id || req.ip;
const now = Date.now();
const windowStart = now - windowMs;
// Kullanıcı verilerini al veya oluştur
if (!userLimits.has(userId)) {
userLimits.set(userId, {
requests: [],
firstRequest: now
});
}
const userLimit = userLimits.get(userId);
// Eski istekleri temizle
userLimit.requests = userLimit.requests.filter(time => time > windowStart);
// Limit kontrolü
if (userLimit.requests.length >= maxRequests) {
logger.warn('Rate limit exceeded', {
userId,
requestCount: userLimit.requests.length,
maxRequests,
windowMs,
path: req.path,
ip: req.ip
});
return res.status(429).json({
success: false,
error: 'Too many requests',
code: 'RATE_LIMIT_EXCEEDED',
details: {
maxRequests,
windowMs,
retryAfter: Math.ceil(windowMs / 1000)
},
timestamp: Date.now()
});
}
// İsteği kaydet
userLimit.requests.push(now);
next();
};
};
// Kombine middleware setleri
export const userAuth = [requireUser, requireCity, logRequest];
export const adminAuth = [requireAdmin, setCityKey, logRequest];
export const publicWithCity = [requireCity, logRequest];
export const publicOptional = [setCityKey, logRequest];
// Tüm auth middleware'leri dışa aktar
export const authMiddleware = {
requireCity,
setCityKey,
requireUser,
requireAdmin,
requireOwnership,
validateRequired,
logRequest,
rateLimitByUser,
// Kombine setler
userAuth,
adminAuth,
publicWithCity,
publicOptional
};
export default authMiddleware;