negyedik gyakorlat + megoldasok

This commit is contained in:
magdo
2026-03-04 20:02:39 +01:00
parent afc3777ac9
commit 388aa908de
217 changed files with 19791 additions and 0 deletions
@@ -0,0 +1,271 @@
import { UserRepository } from '../../infrastructure/repositories/UserRepository.js';
import {
hashPassword,
verifyPassword,
generateAccessToken,
generateRefreshToken,
setAuthCookies,
clearAuthCookies,
validateRegisterInput,
validateLoginInput,
verifyRefreshToken,
createSession,
deleteSession
} from '../../infrastructure/auth/authUtils.js';
export class AuthController {
constructor(userRepository) {
this.userRepository = userRepository || new UserRepository();
}
/**
* POST /api/auth/register
* Új felhasználó regisztrációja
*/
async register(req, res) {
try {
const { email, username, password } = req.body;
// 1. Input validáció
const validation = validateRegisterInput(req.body);
if (!validation.isValid) {
return res.status(400).json({
success: false,
errors: validation.errors
});
}
// 2. Email uniqueness ellenőrzés
const existingUserByEmail = await this.userRepository.findByEmail(email);
if (existingUserByEmail) {
return res.status(400).json({
success: false,
error: 'Email already in use'
});
}
// 3. Username uniqueness ellenőrzés
const existingUserByUsername = await this.userRepository.findByUsername(username);
if (existingUserByUsername) {
return res.status(400).json({
success: false,
error: 'Username already taken'
});
}
// 4. Jelszó hash-elés
const hashedPassword = await hashPassword(password);
// 5. User létrehozása
const user = await this.userRepository.create({
email,
username,
password: hashedPassword,
role: 'USER'
});
// 6. JWT tokenek generálása
const accessToken = generateAccessToken(user);
const refreshToken = generateRefreshToken(user);
// 7. Cookie-k beállítása
setAuthCookies(res, accessToken, refreshToken);
// 8. Redis session (opcionális)
await createSession(user.id, { email: user.email, username: user.username });
// 9. Válasz (jelszó nélkül!)
const { password: _, ...userWithoutPassword } = user;
res.status(201).json({
success: true,
message: 'User registered successfully',
data: {
user: userWithoutPassword,
accessToken,
refreshToken
}
});
} catch (error) {
console.error('Register error:', error);
res.status(500).json({
success: false,
error: error.message
});
}
}
/**
* POST /api/auth/login
* Bejelentkezés
*/
async login(req, res) {
try {
const { email, username, password } = req.body;
// 1. Input validáció
const validation = validateLoginInput(req.body);
if (!validation.isValid) {
return res.status(400).json({
success: false,
errors: validation.errors
});
}
// 2. User keresése (email VAGY username alapján)
let user;
if (email) {
user = await this.userRepository.findByEmail(email);
} else if (username) {
user = await this.userRepository.findByUsername(username);
}
if (!user) {
return res.status(401).json({
success: false,
error: 'Invalid credentials'
});
}
// 3. Jelszó ellenőrzés
const isPasswordValid = await verifyPassword(password, user.password);
if (!isPasswordValid) {
return res.status(401).json({
success: false,
error: 'Invalid credentials'
});
}
// 4. JWT tokenek generálása
const accessToken = generateAccessToken(user);
const refreshToken = generateRefreshToken(user);
// 5. Cookie-k beállítása
setAuthCookies(res, accessToken, refreshToken);
// 6. Redis session
await createSession(user.id, { email: user.email, username: user.username });
// 7. Válasz (jelszó nélkül!)
const { password: _, ...userWithoutPassword } = user;
res.json({
success: true,
message: 'Login successful',
data: {
user: userWithoutPassword,
accessToken,
refreshToken
}
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({
success: false,
error: error.message
});
}
}
/**
* POST /api/auth/logout
* Kijelentkezés
*/
async logout(req, res) {
try {
// 1. Cookie-k törlése
clearAuthCookies(res);
// 2. Redis session törlése
if (req.user && req.user.id) {
await deleteSession(req.user.id);
}
res.json({
success: true,
message: 'Logged out successfully'
});
} catch (error) {
console.error('Logout error:', error);
res.status(500).json({
success: false,
error: error.message
});
}
}
/**
* POST /api/auth/refresh
* Token frissítés
*/
async refreshToken(req, res) {
try {
// 1. Refresh token kiolvasása
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(401).json({
success: false,
error: 'Refresh token not found'
});
}
// 2. Refresh token validálás
const decoded = verifyRefreshToken(refreshToken);
// 3. User lekérése
const user = await this.userRepository.findById(decoded.userId);
if (!user) {
return res.status(401).json({
success: false,
error: 'User not found'
});
}
// 4. Új access token generálás
const newAccessToken = generateAccessToken(user);
// 5. Cookie frissítése
res.cookie('accessToken', newAccessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000
});
res.json({
success: true,
data: {
accessToken: newAccessToken
}
});
} catch (error) {
console.error('Refresh token error:', error);
res.status(401).json({
success: false,
error: 'Invalid refresh token'
});
}
}
/**
* GET /api/auth/me
* Bejelentkezett user adatai
*/
async getCurrentUser(req, res) {
try {
// req.user-t az authenticateToken middleware állította be
const { password: _, ...userWithoutPassword } = req.user;
res.json({
success: true,
data: {
user: userWithoutPassword
}
});
} catch (error) {
console.error('Get current user error:', error);
res.status(500).json({
success: false,
error: error.message
});
}
}
}
@@ -0,0 +1,114 @@
import { BlogRepository } from '../../infrastructure/repositories/BlogRepository.js';
export class BlogController {
constructor() {
this.blogRepository = new BlogRepository();
}
async getAll(req, res) {
try {
const blogs = await this.blogRepository.findAll();
res.json({
success: true,
data: blogs
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
}
async getById(req, res) {
try {
const blog = await this.blogRepository.findById(req.params.id);
if (!blog) {
return res.status(404).json({
success: false,
error: 'Blog not found'
});
}
res.json({
success: true,
data: blog
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
}
async create(req, res) {
try {
const { title, content, published } = req.body;
if (!title || !content) {
return res.status(400).json({
success: false,
error: 'Title and content are required'
});
}
const blog = await this.blogRepository.create({
title,
content,
published: published || false,
authorId: req.user.id
});
res.status(201).json({
success: true,
data: blog,
message: 'Blog created successfully'
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
}
async update(req, res) {
try {
const { title, content, published } = req.body;
const updateData = {};
if (title) updateData.title = title;
if (content) updateData.content = content;
if (typeof published !== 'undefined') updateData.published = published;
const blog = await this.blogRepository.update(req.params.id, updateData);
res.json({
success: true,
data: blog,
message: 'Blog updated successfully'
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
}
async delete(req, res) {
try {
await this.blogRepository.delete(req.params.id);
res.json({
success: true,
message: 'Blog deleted successfully'
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
}
}
@@ -0,0 +1,143 @@
import {
extractTokenFromRequest,
verifyAccessToken
} from '../../infrastructure/auth/authUtils.js';
import { UserRepository } from '../../infrastructure/repositories/UserRepository.js';
const userRepository = new UserRepository();
/**
* Authentication Middleware
*
* Ellenőrzi a JWT tokent és beállítja req.user-t
*/
export async function authenticateToken(req, res, next) {
try {
// 1. Token kiolvasása (cookie vagy Authorization header)
const token = extractTokenFromRequest(req);
if (!token) {
return res.status(401).json({
success: false,
error: 'Access token required'
});
}
// 2. Token validálás
const decoded = verifyAccessToken(token);
// 3. User lekérése az adatbázisból
const user = await userRepository.findById(decoded.userId);
if (!user) {
return res.status(401).json({
success: false,
error: 'User not found'
});
}
// 4. User hozzáadása a request objektumhoz
req.user = user;
// 5. Továbblépés a következő middleware-re
next();
} catch (error) {
console.error('Authentication error:', error);
return res.status(401).json({
success: false,
error: 'Invalid or expired token'
});
}
}
/**
* Authorization Middleware - Role Check
*
* Ellenőrzi, hogy a usernek megfelel ő role-ja van-e
*
* @param {string[]} allowedRoles - Engedélyezett role-ok listája
* @returns {Function} Express middleware
*/
export function requireRole(allowedRoles) {
return (req, res, next) => {
try {
// 1. Ellenőrzés: req.user létezik? (authenticateToken után fut)
if (!req.user) {
return res.status(401).json({
success: false,
error: 'Authentication required'
});
}
// 2. Role check: van-e a usernek megfelelő role-ja?
if (!allowedRoles.includes(req.user.role)) {
return res.status(403).json({
success: false,
error: 'Insufficient permissions'
});
}
// 3. OK, továbblépés
next();
} catch (error) {
console.error('Authorization error:', error);
return res.status(403).json({
success: false,
error: 'Authorization failed'
});
}
};
}
/**
* Resource Ownership Check
*
* Ellenőrzi, hogy a user tulajdonosa-e az adott resource-nak
* VAGY admin role-ja van
*
* @param {Function} getResourceOwnerId - Async függvény ami visszaadja a resource owner ID-t
* @returns {Function} Express middleware
*/
export function checkOwnership(getResourceOwnerId) {
return async (req, res, next) => {
try {
// 1. User ellenőrzés
if (!req.user) {
return res.status(401).json({
success: false,
error: 'Authentication required'
});
}
// 2. Resource owner ID lekérése
const ownerId = await getResourceOwnerId(req);
if (!ownerId) {
return res.status(404).json({
success: false,
error: 'Resource not found'
});
}
// 3. Ownership ellenőrzés: user tulajdonos VAGY admin
const isOwner = req.user.id === ownerId;
const isAdmin = req.user.role === 'ADMIN';
if (!isOwner && !isAdmin) {
return res.status(403).json({
success: false,
error: 'You do not have permission to access this resource'
});
}
// 4. OK, továbblépés
next();
} catch (error) {
console.error('Ownership check error:', error);
return res.status(500).json({
success: false,
error: 'Ownership verification failed'
});
}
};
}
@@ -0,0 +1,19 @@
export function errorHandler(err, req, res, next) {
console.error('Error:', err);
const statusCode = err.statusCode || 500;
const message = err.message || 'Internal Server Error';
res.status(statusCode).json({
success: false,
error: message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
}
export function notFoundHandler(req, res) {
res.status(404).json({
success: false,
error: 'Endpoint not found'
});
}
@@ -0,0 +1,17 @@
import express from 'express';
import { AuthController } from '../controllers/AuthController.js';
import { authenticateToken } from '../middlewares/authMiddleware.js';
const router = express.Router();
const authController = new AuthController();
// Publikus route-ok
router.post('/register', (req, res) => authController.register(req, res));
router.post('/login', (req, res) => authController.login(req, res));
router.post('/refresh', (req, res) => authController.refreshToken(req, res));
// Védett route-ok (authenticateToken middleware)
router.post('/logout', authenticateToken, (req, res) => authController.logout(req, res));
router.get('/me', authenticateToken, (req, res) => authController.getCurrentUser(req, res));
export default router;
@@ -0,0 +1,38 @@
import express from 'express';
import { BlogController } from '../controllers/BlogController.js';
import { authenticateToken, checkOwnership } from '../middlewares/authMiddleware.js';
import { BlogRepository } from '../../infrastructure/repositories/BlogRepository.js';
const router = express.Router();
const blogController = new BlogController();
const blogRepository = new BlogRepository();
// Publikus route-ok
router.get('/', (req, res) => blogController.getAll(req, res));
router.get('/:id', (req, res) => blogController.getById(req, res));
// Védett route-ok
router.post('/',
authenticateToken,
(req, res) => blogController.create(req, res)
);
router.put('/:id',
authenticateToken,
checkOwnership(async (req) => {
const blog = await blogRepository.findById(parseInt(req.params.id));
return blog?.authorId;
}),
(req, res) => blogController.update(req, res)
);
router.delete('/:id',
authenticateToken,
checkOwnership(async (req) => {
const blog = await blogRepository.findById(parseInt(req.params.id));
return blog?.authorId;
}),
(req, res) => blogController.delete(req, res)
);
export default router;
@@ -0,0 +1,29 @@
export class Blog {
constructor(data) {
this.id = data.id;
this.title = data.title;
this.content = data.content;
this.published = data.published;
this.authorId = data.authorId;
this.createdAt = data.createdAt;
this.updatedAt = data.updatedAt;
this.author = data.author;
}
toJSON() {
return {
id: this.id,
title: this.title,
content: this.content,
published: this.published,
authorId: this.authorId,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
author: this.author ? {
id: this.author.id,
username: this.author.username,
email: this.author.email
} : null
};
}
}
@@ -0,0 +1,20 @@
export class User {
constructor(data) {
this.id = data.id;
this.email = data.email;
this.username = data.username;
this.password = data.password;
this.role = data.role;
this.createdAt = data.createdAt;
this.updatedAt = data.updatedAt;
}
isAdmin() {
return this.role === 'ADMIN';
}
toJSON() {
const { password, ...userWithoutPassword } = this;
return userWithoutPassword;
}
}
@@ -0,0 +1,25 @@
export class IBlogRepository {
async findById(id) {
throw new Error('findById() must be implemented');
}
async findAll() {
throw new Error('findAll() must be implemented');
}
async findByAuthorId(authorId) {
throw new Error('findByAuthorId() must be implemented');
}
async create(blogData) {
throw new Error('create() must be implemented');
}
async update(id, blogData) {
throw new Error('update() must be implemented');
}
async delete(id) {
throw new Error('delete() must be implemented');
}
}
@@ -0,0 +1,29 @@
export class IUserRepository {
async findById(id) {
throw new Error('findById() must be implemented');
}
async findByEmail(email) {
throw new Error('findByEmail() must be implemented');
}
async findByUsername(username) {
throw new Error('findByUsername() must be implemented');
}
async create(userData) {
throw new Error('create() must be implemented');
}
async update(id,userData) {
throw new Error('update() must be implemented');
}
async delete(id) {
throw new Error('delete() must be implemented');
}
async findAll() {
throw new Error('findAll() must be implemented');
}
}
@@ -0,0 +1,62 @@
import express from 'express';
import cookieParser from 'cookie-parser';
import dotenv from 'dotenv';
dotenv.config();
import databaseClient from './infrastructure/database/prisma.js';
import redisClient from './infrastructure/database/redis.js';
import authRoutes from './api/routes/authRoutes.js';
import blogRoutes from './api/routes/blogRoutes.js';
import { errorHandler, notFoundHandler } from './api/middlewares/errorHandler.js';
const app = express();
const PORT = process.env.PORT || 3001;
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser(process.env.COOKIE_SECRET));
// Database connection
await databaseClient.connect();
// Health check
app.get('/health', (req, res) => {
res.json({
status: 'OK',
timestamp: new Date().toISOString(),
environment: process.env.NODE_ENV
});
});
// Routes
app.use('/api/auth', authRoutes);
app.use('/api/blogs', blogRoutes);
// Error handling
app.use(notFoundHandler);
app.use(errorHandler);
// Graceful shutdown
process.on('SIGINT', async () => {
console.log('\n🛑 Shutting down gracefully...');
await databaseClient.disconnect();
await redisClient.disconnect();
process.exit(0);
});
process.on('SIGTERM', async () => {
console.log('\n🛑 Shutting down gracefully...');
await databaseClient.disconnect();
await redisClient.disconnect();
process.exit(0);
});
// Start server
app.listen(PORT, () => {
console.log(`✅ Server running on http://localhost:${PORT}`);
console.log(`📊 Environment: ${process.env.NODE_ENV}`);
});
@@ -0,0 +1,231 @@
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
import { redis } from '../database/redis.js';
/**
* JWT Token Generálás
*/
export function generateAccessToken(user) {
const payload = {
userId: user.id,
email: user.email,
role: user.role
};
return jwt.sign(
payload,
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN || '7d' }
);
}
export function generateRefreshToken(user) {
const payload = {
userId: user.id
};
return jwt.sign(
payload,
process.env.JWT_REFRESH_SECRET,
{ expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '30d' }
);
}
/**
* JWT Token Validálás
*/
export function verifyAccessToken(token) {
try {
return jwt.verify(token, process.env.JWT_SECRET);
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new Error('Token expired');
}
if (error.name === 'JsonWebTokenError') {
throw new Error('Invalid token');
}
throw error;
}
}
export function verifyRefreshToken(token) {
try {
return jwt.verify(token, process.env.JWT_REFRESH_SECRET);
} catch (error) {
throw new Error('Invalid refresh token');
}
}
/**
* Token Kiolvasás Request-ből
*/
export function extractTokenFromRequest(req) {
// 1. Cookie-ból
if (req.cookies && req.cookies.accessToken) {
return req.cookies.accessToken;
}
// 2. Authorization header-ből
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
return authHeader.substring(7);
}
return null;
}
/**
* Jelszó Hash-elés és Validálás
*/
export async function hashPassword(plainPassword) {
const saltRounds = 10;
return await bcrypt.hash(plainPassword, saltRounds);
}
export async function verifyPassword(plainPassword, hashedPassword) {
return await bcrypt.compare(plainPassword, hashedPassword);
}
/**
* Cookie Beállítás
*/
export function setAuthCookies(res, accessToken, refreshToken) {
const cookieOptions = {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict'
};
// Access token cookie - 7 nap
res.cookie('accessToken', accessToken, {
...cookieOptions,
maxAge: 7 * 24 * 60 * 60 * 1000
});
// Refresh token cookie - 30 nap
res.cookie('refreshToken', refreshToken, {
...cookieOptions,
maxAge: 30 * 24 * 60 * 60 * 1000
});
}
export function clearAuthCookies(res) {
res.clearCookie('accessToken');
res.clearCookie('refreshToken');
}
/**
* Input Validáció
*/
export function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
export function isValidPassword(password) {
return password && password.length >= 8;
}
export function validateRegisterInput(data) {
const errors = [];
if (!data.email || !isValidEmail(data.email)) {
errors.push('Valid email is required');
}
if (!data.username || data.username.length < 3) {
errors.push('Username must be at least 3 characters');
}
if (!data.password || !isValidPassword(data.password)) {
errors.push('Password must be at least 8 characters');
}
return {
isValid: errors.length === 0,
errors
};
}
export function validateLoginInput(data) {
const errors = [];
if (!data.password) {
errors.push('Password is required');
}
if (!data.email && !data.username) {
errors.push('Email or username is required');
}
return {
isValid: errors.length === 0,
errors
};
}
/**
* Redis Session Management (Opcionális)
*/
export async function createSession(userId, sessionData) {
try {
const sessionKey = `session:${userId}`;
const ttl = 7 * 24 * 60 * 60; // 7 nap másodpercben
await redis.set(
sessionKey,
JSON.stringify({
userId,
loginTime: new Date().toISOString(),
...sessionData
}),
'EX',
ttl
);
} catch (error) {
console.error('Redis session create error:', error);
}
}
export async function getSession(userId) {
try {
const sessionKey = `session:${userId}`;
const session = await redis.get(sessionKey);
if (!session) {
return null;
}
return JSON.parse(session);
} catch (error) {
console.error('Redis session get error:', error);
return null;
}
}
export async function deleteSession(userId) {
try {
const sessionKey = `session:${userId}`;
await redis.del(sessionKey);
} catch (error) {
console.error('Redis session delete error:', error);
}
}
export async function sessionExists(userId) {
try {
const sessionKey = `session:${userId}`;
const exists = await redis.exists(sessionKey);
return exists === 1;
} catch (error) {
console.error('Redis session exists error:', error);
return false;
}
}
@@ -0,0 +1,22 @@
import { PrismaClient } from '@prisma/client';
export const prisma = new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
});
export default {
client: prisma,
connect: async () => {
try {
await prisma.$connect();
console.log('✅ Database connected');
} catch (error) {
console.error('❌ Database connection failed:', error);
process.exit(1);
}
},
disconnect: async () => {
await prisma.$disconnect();
console.log('Database disconnected');
}
};
@@ -0,0 +1,28 @@
import Redis from 'ioredis';
const redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT) || 6380,
retryStrategy: (times) => {
const delay = Math.min(times * 50, 2000);
return delay;
}
});
redis.on('connect', () => {
console.log('✅ Redis connected');
});
redis.on('error', (error) => {
console.error('❌ Redis connection error:', error);
});
export { redis };
export default {
client: redis,
disconnect: async () => {
await redis.quit();
console.log('Redis disconnected');
}
};
@@ -0,0 +1,54 @@
import { IBlogRepository } from '../../domain/repositories/IBlogRepository.js';
import { Blog } from '../../domain/models/Blog.js';
import { prisma } from '../database/prisma.js';
export class BlogRepository extends IBlogRepository {
async findById(id) {
const blog = await prisma.blog.findUnique({
where: { id },
include: { author: true }
});
return blog ? new Blog(blog) : null;
}
async findAll() {
const blogs = await prisma.blog.findMany({
include: { author: true },
orderBy: { createdAt: 'desc' }
});
return blogs.map(blog => new Blog(blog));
}
async findByAuthorId(authorId) {
const blogs = await prisma.blog.findMany({
where: { authorId },
include: { author: true },
orderBy: { createdAt: 'desc' }
});
return blogs.map(blog => new Blog(blog));
}
async create(blogData) {
const blog = await prisma.blog.create({
data: blogData,
include: { author: true }
});
return new Blog(blog);
}
async update(id, blogData) {
const blog = await prisma.blog.update({
where: { id },
data: blogData,
include: { author: true }
});
return new Blog(blog);
}
async delete(id) {
await prisma.blog.delete({
where: { id }
});
return true;
}
}
@@ -0,0 +1,55 @@
import { IUserRepository } from '../../domain/repositories/IUserRepository.js';
import { User } from '../../domain/models/User.js';
import { prisma } from '../database/prisma.js';
export class UserRepository extends IUserRepository {
async findById(id) {
const user = await prisma.user.findUnique({
where: { id }
});
return user ? new User(user) : null;
}
async findByEmail(email) {
const user = await prisma.user.findUnique({
where: { email }
});
return user ? new User(user) : null;
}
async findByUsername(username) {
const user = await prisma.user.findUnique({
where: { username }
});
return user ? new User(user) : null;
}
async create(userData) {
const user = await prisma.user.create({
data: userData
});
return new User(user);
}
async update(id, userData) {
const user = await prisma.user.update({
where: { id },
data: userData
});
return new User(user);
}
async delete(id) {
await prisma.user.delete({
where: { id }
});
return true;
}
async findAll() {
const users = await prisma.user.findMany({
orderBy: { createdAt: 'desc' }
});
return users.map(user => new User(user));
}
}