init commit

This commit is contained in:
2025-10-19 18:27:00 +09:00
commit 150891b29d
219 changed files with 70016 additions and 0 deletions

154
middleware/auth.js Normal file
View File

@@ -0,0 +1,154 @@
const jwt = require('jsonwebtoken');
const User = require('../models/User');
/**
* Authentication middleware
* Verifies JWT token and attaches user to request
*/
const authenticateToken = async (req, res, next) => {
try {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({
success: false,
message: 'Access token required'
});
}
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findById(decoded.userId).select('-password');
if (!user || !user.isActive) {
return res.status(401).json({
success: false,
message: 'Invalid or inactive user'
});
}
req.user = user;
next();
} catch (error) {
console.error('Token verification error:', error);
return res.status(403).json({
success: false,
message: 'Invalid token'
});
}
};
/**
* Session-based authentication middleware
* For web pages using sessions
*/
const authenticateSession = async (req, res, next) => {
try {
if (!req.session.userId) {
req.flash('error', '로그인이 필요합니다.');
return res.redirect('/auth/login');
}
const user = await User.findById(req.session.userId).select('-password');
if (!user || !user.isActive) {
req.session.destroy();
req.flash('error', '유효하지 않은 사용자입니다.');
return res.redirect('/auth/login');
}
req.user = user;
res.locals.user = user;
next();
} catch (error) {
console.error('Session authentication error:', error);
req.session.destroy();
req.flash('error', '인증 오류가 발생했습니다.');
return res.redirect('/auth/login');
}
};
/**
* Admin role middleware
* Requires user to be authenticated and have admin role
*/
const requireAdmin = (req, res, next) => {
if (!req.user) {
return res.status(401).json({
success: false,
message: 'Authentication required'
});
}
if (req.user.role !== 'admin') {
return res.status(403).json({
success: false,
message: 'Admin access required'
});
}
next();
};
/**
* Admin session middleware for web pages
*/
const requireAdminSession = (req, res, next) => {
if (!req.user) {
req.flash('error', '로그인이 필요합니다.');
return res.redirect('/auth/login');
}
if (req.user.role !== 'admin') {
req.flash('error', '관리자 권한이 필요합니다.');
return res.redirect('/');
}
next();
};
/**
* Optional authentication middleware
* Attaches user if token exists but doesn't require it
*/
const optionalAuth = async (req, res, next) => {
try {
// Check session first
if (req.session.userId) {
const user = await User.findById(req.session.userId).select('-password');
if (user && user.isActive) {
req.user = user;
res.locals.user = user;
}
}
// Check JWT token if no session
if (!req.user) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (token) {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findById(decoded.userId).select('-password');
if (user && user.isActive) {
req.user = user;
res.locals.user = user;
}
}
}
next();
} catch (error) {
// Continue without authentication if token is invalid
next();
}
};
module.exports = {
authenticateToken,
authenticateSession,
requireAdmin,
requireAdminSession,
optionalAuth
};

310
middleware/validation.js Normal file
View File

@@ -0,0 +1,310 @@
/**
* Validation middleware for various data types
*/
const { body, validationResult } = require('express-validator');
/**
* Validation error handler
*/
const handleValidationErrors = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
// For API requests
if (req.xhr || req.headers.accept?.includes('application/json')) {
return res.status(400).json({
success: false,
message: 'Validation failed',
errors: errors.array()
});
}
// For web requests
const errorMessages = errors.array().map(error => error.msg);
req.flash('error', errorMessages);
return res.redirect('back');
}
next();
};
/**
* Contact form validation
*/
const validateContactForm = [
body('name')
.trim()
.isLength({ min: 2, max: 50 })
.withMessage('이름은 2-50자 사이여야 합니다.')
.matches(/^[a-zA-Z가-힣\s]+$/)
.withMessage('이름에는 한글, 영문, 공백만 사용할 수 있습니다.'),
body('email')
.isEmail()
.withMessage('유효한 이메일 주소를 입력해주세요.')
.normalizeEmail(),
body('phone')
.optional()
.matches(/^[0-9\-\+\(\)\s]+$/)
.withMessage('유효한 전화번호를 입력해주세요.'),
body('company')
.optional()
.trim()
.isLength({ max: 100 })
.withMessage('회사명은 100자 이하여야 합니다.'),
body('service')
.isIn(['web-development', 'mobile-app', 'ui-ux-design', 'branding', 'digital-marketing', 'consulting', 'other'])
.withMessage('유효한 서비스를 선택해주세요.'),
body('budget')
.optional()
.isIn(['under-500', '500-1000', '1000-3000', '3000-5000', '5000-10000', 'over-10000', 'discuss'])
.withMessage('유효한 예산 범위를 선택해주세요.'),
body('message')
.trim()
.isLength({ min: 10, max: 2000 })
.withMessage('메시지는 10-2000자 사이여야 합니다.'),
handleValidationErrors
];
/**
* User registration validation
*/
const validateRegistration = [
body('name')
.trim()
.isLength({ min: 2, max: 50 })
.withMessage('이름은 2-50자 사이여야 합니다.')
.matches(/^[a-zA-Z가-힣\s]+$/)
.withMessage('이름에는 한글, 영문, 공백만 사용할 수 있습니다.'),
body('email')
.isEmail()
.withMessage('유효한 이메일 주소를 입력해주세요.')
.normalizeEmail(),
body('password')
.isLength({ min: 8 })
.withMessage('비밀번호는 최소 8자 이상이어야 합니다.')
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/)
.withMessage('비밀번호는 대소문자, 숫자, 특수문자를 포함해야 합니다.'),
body('confirmPassword')
.custom((value, { req }) => {
if (value !== req.body.password) {
throw new Error('비밀번호 확인이 일치하지 않습니다.');
}
return true;
}),
handleValidationErrors
];
/**
* User login validation
*/
const validateLogin = [
body('email')
.isEmail()
.withMessage('유효한 이메일 주소를 입력해주세요.')
.normalizeEmail(),
body('password')
.isLength({ min: 1 })
.withMessage('비밀번호를 입력해주세요.'),
handleValidationErrors
];
/**
* Portfolio validation
*/
const validatePortfolio = [
body('title')
.trim()
.isLength({ min: 2, max: 100 })
.withMessage('제목은 2-100자 사이여야 합니다.'),
body('description')
.trim()
.isLength({ min: 10, max: 5000 })
.withMessage('설명은 10-5000자 사이여야 합니다.'),
body('shortDescription')
.optional()
.trim()
.isLength({ max: 200 })
.withMessage('간단한 설명은 200자 이하여야 합니다.'),
body('category')
.isIn(['web-development', 'mobile-app', 'ui-ux-design', 'branding', 'marketing'])
.withMessage('유효한 카테고리를 선택해주세요.'),
body('technologies')
.optional()
.isArray()
.withMessage('기술 스택은 배열이어야 합니다.'),
body('clientName')
.optional()
.trim()
.isLength({ max: 100 })
.withMessage('클라이언트 이름은 100자 이하여야 합니다.'),
body('projectUrl')
.optional()
.isURL()
.withMessage('유효한 URL을 입력해주세요.'),
body('status')
.optional()
.isIn(['planning', 'in-progress', 'completed', 'on-hold'])
.withMessage('유효한 상태를 선택해주세요.'),
handleValidationErrors
];
/**
* Service validation
*/
const validateService = [
body('name')
.trim()
.isLength({ min: 2, max: 100 })
.withMessage('서비스명은 2-100자 사이여야 합니다.'),
body('description')
.trim()
.isLength({ min: 10, max: 5000 })
.withMessage('설명은 10-5000자 사이여야 합니다.'),
body('shortDescription')
.optional()
.trim()
.isLength({ max: 200 })
.withMessage('간단한 설명은 200자 이하여야 합니다.'),
body('category')
.isIn(['development', 'design', 'marketing', 'consulting'])
.withMessage('유효한 카테고리를 선택해주세요.'),
body('pricing.basePrice')
.optional()
.isNumeric()
.withMessage('기본 가격은 숫자여야 합니다.'),
body('pricing.priceType')
.optional()
.isIn(['project', 'hourly', 'monthly'])
.withMessage('유효한 가격 유형을 선택해주세요.'),
handleValidationErrors
];
/**
* Calculator validation
*/
const validateCalculator = [
body('service')
.isMongoId()
.withMessage('유효한 서비스를 선택해주세요.'),
body('projectType')
.optional()
.isIn(['simple', 'medium', 'complex', 'enterprise'])
.withMessage('유효한 프로젝트 유형을 선택해주세요.'),
body('timeline')
.optional()
.isIn(['urgent', 'normal', 'flexible'])
.withMessage('유효한 타임라인을 선택해주세요.'),
body('features')
.optional()
.isArray()
.withMessage('기능은 배열이어야 합니다.'),
body('contactInfo.name')
.optional()
.trim()
.isLength({ min: 2, max: 50 })
.withMessage('이름은 2-50자 사이여야 합니다.'),
body('contactInfo.email')
.optional()
.isEmail()
.withMessage('유효한 이메일 주소를 입력해주세요.'),
body('contactInfo.phone')
.optional()
.matches(/^[0-9\-\+\(\)\s]+$/)
.withMessage('유효한 전화번호를 입력해주세요.'),
handleValidationErrors
];
/**
* Settings validation
*/
const validateSettings = [
body('siteName')
.optional()
.trim()
.isLength({ min: 1, max: 100 })
.withMessage('사이트명은 1-100자 사이여야 합니다.'),
body('siteDescription')
.optional()
.trim()
.isLength({ max: 500 })
.withMessage('사이트 설명은 500자 이하여야 합니다.'),
body('contact.email')
.optional()
.isEmail()
.withMessage('유효한 이메일 주소를 입력해주세요.'),
body('contact.phone')
.optional()
.matches(/^[0-9\-\+\(\)\s]+$/)
.withMessage('유효한 전화번호를 입력해주세요.'),
body('social.facebook')
.optional()
.isURL()
.withMessage('유효한 Facebook URL을 입력해주세요.'),
body('social.twitter')
.optional()
.isURL()
.withMessage('유효한 Twitter URL을 입력해주세요.'),
body('social.linkedin')
.optional()
.isURL()
.withMessage('유효한 LinkedIn URL을 입력해주세요.'),
body('social.instagram')
.optional()
.isURL()
.withMessage('유효한 Instagram URL을 입력해주세요.'),
handleValidationErrors
];
module.exports = {
handleValidationErrors,
validateContactForm,
validateRegistration,
validateLogin,
validatePortfolio,
validateService,
validateCalculator,
validateSettings
};