From ffca701b8426433b404701e1836d25a7560a34ba Mon Sep 17 00:00:00 2001 From: magdo Date: Wed, 25 Feb 2026 20:16:03 +0100 Subject: [PATCH] harmadik gyakorlat --- Backend/harmadik gyakorlat/.env.example | 19 + Backend/harmadik gyakorlat/.gitignore | 8 + Backend/harmadik gyakorlat/FELADAT.md | 300 +++++++ Backend/harmadik gyakorlat/HINTS.md | 752 ++++++++++++++++++ Backend/harmadik gyakorlat/README.md | 658 +++++++++++++++ Backend/harmadik gyakorlat/docker-compose.yml | 38 + Backend/harmadik gyakorlat/package.json | 32 + .../harmadik gyakorlat/prisma/schema.prisma | 44 + .../src/api/controllers/AuthController.js | 160 ++++ .../src/api/controllers/BlogController.js | 179 +++++ .../src/api/middlewares/authMiddleware.js | 112 +++ .../src/api/middlewares/errorHandler.js | 26 + .../src/api/routes/authRoutes.js | 21 + .../src/api/routes/blogRoutes.js | 20 + .../src/api/routes/userRoutes.js | 13 + .../application/commands/CreateBlogCommand.js | 33 + .../application/commands/DeleteBlogCommand.js | 27 + .../application/commands/UpdateBlogCommand.js | 33 + .../application/handlers/BlogQueryHandler.js | 31 + .../application/handlers/CreateBlogHandler.js | 26 + .../application/handlers/DeleteBlogHandler.js | 29 + .../application/handlers/UpdateBlogHandler.js | 30 + .../src/application/queries/BlogQueries.js | 31 + .../src/domain/models/Blog.js | 32 + .../src/domain/models/User.js | 29 + .../domain/repositories/IBlogRepository.js | 33 + .../domain/repositories/IUserRepository.js | 33 + Backend/harmadik gyakorlat/src/index.js | 107 +++ .../src/infrastructure/auth/authUtils.js | 222 ++++++ .../src/infrastructure/config/index.js | 59 ++ .../src/infrastructure/database/prisma.js | 31 + .../src/infrastructure/database/redis.js | 48 ++ .../repositories/BlogRepository.js | 122 +++ .../repositories/UserRepository.js | 59 ++ 34 files changed, 3397 insertions(+) create mode 100644 Backend/harmadik gyakorlat/.env.example create mode 100644 Backend/harmadik gyakorlat/.gitignore create mode 100644 Backend/harmadik gyakorlat/FELADAT.md create mode 100644 Backend/harmadik gyakorlat/HINTS.md create mode 100644 Backend/harmadik gyakorlat/README.md create mode 100644 Backend/harmadik gyakorlat/docker-compose.yml create mode 100644 Backend/harmadik gyakorlat/package.json create mode 100644 Backend/harmadik gyakorlat/prisma/schema.prisma create mode 100644 Backend/harmadik gyakorlat/src/api/controllers/AuthController.js create mode 100644 Backend/harmadik gyakorlat/src/api/controllers/BlogController.js create mode 100644 Backend/harmadik gyakorlat/src/api/middlewares/authMiddleware.js create mode 100644 Backend/harmadik gyakorlat/src/api/middlewares/errorHandler.js create mode 100644 Backend/harmadik gyakorlat/src/api/routes/authRoutes.js create mode 100644 Backend/harmadik gyakorlat/src/api/routes/blogRoutes.js create mode 100644 Backend/harmadik gyakorlat/src/api/routes/userRoutes.js create mode 100644 Backend/harmadik gyakorlat/src/application/commands/CreateBlogCommand.js create mode 100644 Backend/harmadik gyakorlat/src/application/commands/DeleteBlogCommand.js create mode 100644 Backend/harmadik gyakorlat/src/application/commands/UpdateBlogCommand.js create mode 100644 Backend/harmadik gyakorlat/src/application/handlers/BlogQueryHandler.js create mode 100644 Backend/harmadik gyakorlat/src/application/handlers/CreateBlogHandler.js create mode 100644 Backend/harmadik gyakorlat/src/application/handlers/DeleteBlogHandler.js create mode 100644 Backend/harmadik gyakorlat/src/application/handlers/UpdateBlogHandler.js create mode 100644 Backend/harmadik gyakorlat/src/application/queries/BlogQueries.js create mode 100644 Backend/harmadik gyakorlat/src/domain/models/Blog.js create mode 100644 Backend/harmadik gyakorlat/src/domain/models/User.js create mode 100644 Backend/harmadik gyakorlat/src/domain/repositories/IBlogRepository.js create mode 100644 Backend/harmadik gyakorlat/src/domain/repositories/IUserRepository.js create mode 100644 Backend/harmadik gyakorlat/src/index.js create mode 100644 Backend/harmadik gyakorlat/src/infrastructure/auth/authUtils.js create mode 100644 Backend/harmadik gyakorlat/src/infrastructure/config/index.js create mode 100644 Backend/harmadik gyakorlat/src/infrastructure/database/prisma.js create mode 100644 Backend/harmadik gyakorlat/src/infrastructure/database/redis.js create mode 100644 Backend/harmadik gyakorlat/src/infrastructure/repositories/BlogRepository.js create mode 100644 Backend/harmadik gyakorlat/src/infrastructure/repositories/UserRepository.js diff --git a/Backend/harmadik gyakorlat/.env.example b/Backend/harmadik gyakorlat/.env.example new file mode 100644 index 0000000..9ad0669 --- /dev/null +++ b/Backend/harmadik gyakorlat/.env.example @@ -0,0 +1,19 @@ +# Database +DATABASE_URL="postgresql://postgres:postgres@localhost:5432/blog_db?schema=public" + +# Redis +REDIS_HOST=localhost +REDIS_PORT=6379 + +# JWT +JWT_SECRET=your-secret-key-change-this-in-production +JWT_EXPIRES_IN=7d +JWT_REFRESH_SECRET=your-refresh-secret-key-change-this +JWT_REFRESH_EXPIRES_IN=30d + +# Server +PORT=3000 +NODE_ENV=development + +# Cookie +COOKIE_SECRET=your-cookie-secret-change-this diff --git a/Backend/harmadik gyakorlat/.gitignore b/Backend/harmadik gyakorlat/.gitignore new file mode 100644 index 0000000..61e3cce --- /dev/null +++ b/Backend/harmadik gyakorlat/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +.env +dist/ +.DS_Store +*.log +coverage/ +.vscode/ +.idea/ diff --git a/Backend/harmadik gyakorlat/FELADAT.md b/Backend/harmadik gyakorlat/FELADAT.md new file mode 100644 index 0000000..57d0765 --- /dev/null +++ b/Backend/harmadik gyakorlat/FELADAT.md @@ -0,0 +1,300 @@ +# Feladat Leírás + +## 🎯 Cél + +Implementálj egy teljes authentication és authorization rendszert egy blog platformhoz JWT token használatával, cookie-alapú session kezeléssel. + +--- + +## ⚠️ MIT KELL IMPLEMENTÁLNI + +### 1. AuthController (`src/api/controllers/AuthController.js`) + +#### `register(req, res)` ✏️ +```javascript +// Input: { email, username, password } +// 1. Validálás (kötelező mezők, formátum) +// 2. Ellenőrizd: email és username unique? +// 3. Hash-eld a jelszót: bcrypt.hash(password, 10) +// 4. Hozd létre a user-t: userRepository.create() +// 5. Generálj JWT tokent: jwt.sign() +// 6. Állíts be HTTP-only cookie-t: res.cookie() +// 7. Válasz: { user (jelszó nélkül!), accessToken, refreshToken } +``` + +#### `login(req, res)` ✏️ +```javascript +// Input: { email vagy username, password } +// 1. Keresd meg a user-t: userRepository.findByEmail() vagy findByUsername() +// 2. Ha nincs user → 401 error +// 3. Ellenőrizd a jelszót: bcrypt.compare(password, user.password) +// 4. Ha nem egyezik → 401 error +// 5. Generálj access és refresh tokent +// 6. Állítsd be a cookie-kat +// 7. Redis session (opcionális): redis.set(`session:${userId}`, ...) +// 8. Válasz: { user, accessToken, refreshToken } +``` + +#### `logout(req, res)` ✏️ +```javascript +// 1. Töröld a cookie-kat: res.clearCookie('accessToken') +// 2. Redis session törlés: redis.del(`session:${userId}`) +// 3. Válasz: { success: true, message: 'Logged out' } +``` + +#### `refreshToken(req, res)` ✏️ +```javascript +// 1. Olvasd ki a refresh tokent: req.cookies.refreshToken +// 2. Validáld: jwt.verify(refreshToken, JWT_REFRESH_SECRET) +// 3. Generálj új access tokent +// 4. Állítsd be az új cookie-t +// 5. Válasz: { accessToken } +``` + +#### `getCurrentUser(req, res)` ✏️ +```javascript +// 1. req.user-ből olvasd ki a user adatokat +// 2. Válasz: { user } +``` + +--- + +### 2. AuthMiddleware (`src/api/middlewares/authMiddleware.js`) + +#### `authenticateToken(req, res, next)` ✏️ +```javascript +// 1. Token kiolvasás: +// - Cookie-ból: req.cookies.accessToken +// - VAGY Header-ből: req.headers.authorization (Bearer token) +// 2. Ha nincs token → 401 Unauthorized +// 3. Validálás: jwt.verify(token, JWT_SECRET) +// 4. User lekérés: userRepository.findById(decoded.userId) +// 5. req.user = user +// 6. next() +``` + +#### `requireRole(allowedRoles)` ✏️ +```javascript +// Higher-order function - visszaad egy middleware-t +return (req, res, next) => { + // 1. Ellenőrizd: req.user létezik? + // 2. Ellenőrizd: allowedRoles.includes(req.user.role)? + // 3. Ha nem → 403 Forbidden + // 4. Ha yes → next() +} + +// Használat: +// router.delete('/admin', authenticateToken, requireRole(['ADMIN']), ...) +``` + +#### `checkOwnership(getResourceOwnerId)` ✏️ +```javascript +// Higher-order function +return async (req, res, next) => { + // 1. Szerezd meg a resource owner ID-t: + // const ownerId = await getResourceOwnerId(req) + // 2. Ellenőrizd: req.user.id === ownerId VAGY req.user.role === 'ADMIN' + // 3. Ha nem → 403 Forbidden + // 4. Ha yes → next() +} + +// Használat: +// router.put('/blogs/:id', authenticateToken, +// checkOwnership(async (req) => { +// const blog = await blogRepository.findById(req.params.id); +// return blog.authorId; +// }), +// updateController +// ); +``` + +--- + +### 3. Route-ok védelme + +#### `src/api/routes/authRoutes.js` ✏️ +```javascript +import { authenticateToken } from '../middlewares/authMiddleware.js'; + +// Publikus +router.post('/register', ...); +router.post('/login', ...); +router.post('/refresh', ...); + +// Védett - add hozzá az authenticateToken middleware-t! +router.post('/logout', authenticateToken, ...); +router.get('/me', authenticateToken, ...); +``` + +#### `src/api/routes/blogRoutes.js` ✏️ +```javascript +import { authenticateToken, checkOwnership } from '../middlewares/authMiddleware.js'; + +// Publikus +router.get('/', ...); +router.get('/:id', ...); + +// Védett - add hozzá a middleware-eket! +router.post('/', authenticateToken, ...); +router.put('/:id', authenticateToken, checkOwnership(...), ...); +router.delete('/:id', authenticateToken, checkOwnership(...), ...); +``` + +--- + +## 📋 Ellenőrző Lista + +Implementációd kész, ha az alábbiak működnek: + +```bash +# 1. Regisztráció +curl -X POST http://localhost:3000/api/auth/register \ + -H "Content-Type: application/json" \ + -d '{ + "email": "test@test.com", + "username": "testuser", + "password": "Test1234" + }' +# → 201 Created, user adatok, cookie beállítva + +# 2. Bejelentkezés +curl -X POST http://localhost:3000/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{ + "email": "test@test.com", + "password": "Test1234" + }' +# → 200 OK, user adatok, cookie beállítva + +# 3. Current user +curl -X GET http://localhost:3000/api/auth/me \ + -H "Authorization: Bearer " +# → 200 OK, user adatok + +# 4. Blog létrehozás (védett) +curl -X POST http://localhost:3000/api/blogs \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"title": "Test", "content": "Content"}' +# → 201 Created + +# 5. Másik user blogja - módosítás TILOS +curl -X PUT http://localhost:3000/api/blogs/ \ + -H "Authorization: Bearer " \ + -d '{"title": "Hacked"}' +# → 403 Forbidden + +# 6. Saját blog - módosítás OK +curl -X PUT http://localhost:3000/api/blogs/ \ + -H "Authorization: Bearer " \ + -d '{"title": "Updated"}' +# → 200 OK + +# 7. Kijelentkezés +curl -X POST http://localhost:3000/api/auth/logout \ + -H "Authorization: Bearer " +# → 200 OK, cookie törölve +``` + +--- + +## 🔑 Kulcs Fontosságú Részek + +### JWT Token Struktúra +```javascript +{ + userId: "uuid...", + email: "user@example.com", + role: "USER", + iat: 1234567890, // issued at + exp: 1234999999 // expires +} +``` + +### Cookie Options +```javascript +{ + httpOnly: true, // JavaScript nem fér hozzá + secure: true, // Csak HTTPS (production) + sameSite: 'strict', // CSRF védelem + maxAge: 7 * 24 * 60 * 60 * 1000 // millisec +} +``` + +### Környezeti Változók +```env +JWT_SECRET=minimum-32-karakter-hosszu-random-string +JWT_REFRESH_SECRET=egy-masik-32-karakter-hosszu-string +JWT_EXPIRES_IN=7d +JWT_REFRESH_EXPIRES_IN=30d +COOKIE_SECRET=meg-egy-secret +``` + +--- + +## ⚡ Gyors Start + +```bash +# 1. Telepítés +npm install + +# 2. Env fájl +cp .env.example .env +# Szerkeszd a .env fájlt! + +# 3. Docker indítás +npm run docker:up + +# 4. Migráció +npm run prisma:migrate +npm run prisma:generate + +# 5. Szerver indítás +npm run dev + +# 6. Implementáld az auth részeket +# - AuthController: register, login, logout, refresh, getCurrentUser +# - authMiddleware: authenticateToken, requireRole, checkOwnership +# - Routes: add hozzá a middleware-eket + +# 7. Tesztelés +# Használd a fenti curl parancsokat +``` + +--- + +## 🚨 Gyakori Hibák + +❌ **Jelszó plain text-ben van tárolva** +✅ Használd: `bcrypt.hash(password, 10)` + +❌ **Token nincs validálva** +✅ Használd: `jwt.verify(token, secret)` + +❌ **Cookie nem httpOnly** +✅ Állítsd be: `httpOnly: true` a cookie options-ben + +❌ **Rossz paraméter sorrend bcrypt.compare()-nál** +✅ Helyes: `bcrypt.compare(plainPassword, hashedPassword)` + +❌ **req.user nincs beállítva** +✅ Az authenticateToken middleware-nek kell beállítania! + +--- + +## 🎯 Mit Tanulsz + +- ✅ JWT-based authentication +- ✅ Cookie management (httpOnly, secure, sameSite) +- ✅ Password hashing (bcrypt) +- ✅ Middleware pattern +- ✅ Role-based access control (RBAC) +- ✅ Resource ownership validation +- ✅ Clean architecture +- ✅ CQRS pattern +- ✅ Repository pattern +- ✅ Dependency injection + +--- + +**Kezdj neki és sok sikert! 💪** diff --git a/Backend/harmadik gyakorlat/HINTS.md b/Backend/harmadik gyakorlat/HINTS.md new file mode 100644 index 0000000..0f633ae --- /dev/null +++ b/Backend/harmadik gyakorlat/HINTS.md @@ -0,0 +1,752 @@ +# Implementation Hints & Code Snippets + +Ez a fájl segítséget nyújt az implementációhoz konkrét kód példákkal. + +--- + +## 🔐 JWT Token Kezelés + +### Token Generálás + +```javascript +import jwt from 'jsonwebtoken'; + +function generateAccessToken(user) { + return jwt.sign( + { + userId: user.id, + email: user.email, + role: user.role + }, + process.env.JWT_SECRET, + { expiresIn: process.env.JWT_EXPIRES_IN || '7d' } + ); +} + +function generateRefreshToken(user) { + return jwt.sign( + { + userId: user.id + }, + process.env.JWT_REFRESH_SECRET, + { expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '30d' } + ); +} +``` + +### Token Validálás + +```javascript +import jwt from 'jsonwebtoken'; + +function verifyAccessToken(token) { + try { + return jwt.verify(token, process.env.JWT_SECRET); + } catch (error) { + if (error.name === 'TokenExpiredError') { + throw new Error('Token expired'); + } + throw new Error('Invalid token'); + } +} + +function verifyRefreshToken(token) { + try { + return jwt.verify(token, process.env.JWT_REFRESH_SECRET); + } catch (error) { + throw new Error('Invalid refresh token'); + } +} +``` + +### Token Kiolvasása Request-ből + +```javascript +function extractToken(req) { + // 1. Először cookie-ból próbálkozunk + if (req.cookies && req.cookies.accessToken) { + return req.cookies.accessToken; + } + + // 2. Ha nincs cookie, akkor Authorization header + const authHeader = req.headers.authorization; + if (authHeader && authHeader.startsWith('Bearer ')) { + return authHeader.substring(7); // "Bearer " eltávolítása + } + + return null; +} +``` + +--- + +## 🔒 Jelszó Hash-elés + +### Regisztrációnál + +```javascript +import bcrypt from 'bcrypt'; + +async function hashPassword(plainPassword) { + const saltRounds = 10; + return await bcrypt.hash(plainPassword, saltRounds); +} + +// Használat: +const hashedPassword = await hashPassword(req.body.password); +const user = await userRepository.create({ + email: req.body.email, + username: req.body.username, + password: hashedPassword, // Hash-elt jelszó! + role: 'USER' +}); +``` + +### Bejelentkezésnél + +```javascript +import bcrypt from 'bcrypt'; + +async function verifyPassword(plainPassword, hashedPassword) { + return await bcrypt.compare(plainPassword, hashedPassword); +} + +// Használat: +const user = await userRepository.findByEmail(req.body.email); +if (!user) { + return res.status(401).json({ error: 'Invalid credentials' }); +} + +const isPasswordValid = await verifyPassword(req.body.password, user.password); +if (!isPasswordValid) { + return res.status(401).json({ error: 'Invalid credentials' }); +} +``` + +--- + +## 🍪 Cookie Kezelés + +### Cookie Beállítás + +```javascript +function setAuthCookies(res, accessToken, refreshToken) { + const cookieOptions = { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict' + }; + + // Access token - 7 nap + res.cookie('accessToken', accessToken, { + ...cookieOptions, + maxAge: 7 * 24 * 60 * 60 * 1000 + }); + + // Refresh token - 30 nap + res.cookie('refreshToken', refreshToken, { + ...cookieOptions, + maxAge: 30 * 24 * 60 * 60 * 1000 + }); +} + +// Használat a login()-ban: +const accessToken = generateAccessToken(user); +const refreshToken = generateRefreshToken(user); +setAuthCookies(res, accessToken, refreshToken); +``` + +### Cookie Törlés + +```javascript +function clearAuthCookies(res) { + res.clearCookie('accessToken'); + res.clearCookie('refreshToken'); +} + +// Használat a logout()-ban: +clearAuthCookies(res); +res.json({ success: true, message: 'Logged out successfully' }); +``` + +--- + +## 📝 Input Validáció + +### Email Validáció + +```javascript +function isValidEmail(email) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); +} +``` + +### Password Validáció + +```javascript +function isValidPassword(password) { + // Minimum 8 karakter, legalább egy szám és egy betű + return password && password.length >= 8; +} +``` + +### Register Input Validáció + +```javascript +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 + }; +} + +// Használat: +const validation = validateRegisterInput(req.body); +if (!validation.isValid) { + return res.status(400).json({ errors: validation.errors }); +} +``` + +--- + +## 🔴 Redis Session Management + +### Session Tárolás + +```javascript +import { redis } from '../../infrastructure/database/redis.js'; + +async function createSession(userId, sessionData) { + 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 + ); +} + +// Használat a login()-ban: +await createSession(user.id, { + email: user.email, + role: user.role +}); +``` + +### Session Lekérés + +```javascript +async function getSession(userId) { + const sessionKey = `session:${userId}`; + const session = await redis.get(sessionKey); + + if (!session) { + return null; + } + + return JSON.parse(session); +} +``` + +### Session Törlés + +```javascript +async function deleteSession(userId) { + const sessionKey = `session:${userId}`; + await redis.del(sessionKey); +} + +// Használat a logout()-ban: +await deleteSession(req.user.id); +``` + +--- + +## 🛡️ AuthController Implementáció + +### Register + +```javascript +async register(req, res) { + try { + // 1. Input validáció + const validation = validateRegisterInput(req.body); + if (!validation.isValid) { + return res.status(400).json({ errors: validation.errors }); + } + + const { email, username, password } = req.body; + + // 2. Uniqueness ellenőrzés + const existingUser = await this.userRepository.findByEmail(email); + if (existingUser) { + return res.status(409).json({ error: 'Email already exists' }); + } + + const existingUsername = await this.userRepository.findByUsername(username); + if (existingUsername) { + return res.status(409).json({ error: 'Username already exists' }); + } + + // 3. Jelszó hash-elés + const hashedPassword = await bcrypt.hash(password, 10); + + // 4. User létrehozás + const user = await this.userRepository.create({ + email, + username, + password: hashedPassword, + role: 'USER' + }); + + // 5. Token generálás + const accessToken = generateAccessToken(user); + const refreshToken = generateRefreshToken(user); + + // 6. Cookie beállítás + setAuthCookies(res, accessToken, refreshToken); + + // 7. Válasz + res.status(201).json({ + success: true, + data: { + user: user.toPublicObject(), + accessToken, + refreshToken + } + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +} +``` + +### Login + +```javascript +async login(req, res) { + try { + const { email, username, password } = req.body; + + // Input validáció + if (!password || (!email && !username)) { + return res.status(400).json({ + error: 'Email/username and password required' + }); + } + + // User keresés + let user; + if (email) { + user = await this.userRepository.findByEmail(email); + } else { + user = await this.userRepository.findByUsername(username); + } + + if (!user) { + return res.status(401).json({ error: 'Invalid credentials' }); + } + + // Jelszó ellenőrzés + const isPasswordValid = await bcrypt.compare(password, user.password); + if (!isPasswordValid) { + return res.status(401).json({ error: 'Invalid credentials' }); + } + + // Token generálás + const accessToken = generateAccessToken(user); + const refreshToken = generateRefreshToken(user); + + // Cookie beállítás + setAuthCookies(res, accessToken, refreshToken); + + // Redis session (opcionális) + await createSession(user.id, { + email: user.email, + role: user.role + }); + + res.json({ + success: true, + data: { + user: user.toPublicObject(), + accessToken, + refreshToken + } + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +} +``` + +### Logout + +```javascript +async logout(req, res) { + try { + // Redis session törlés + if (req.user && req.user.id) { + await deleteSession(req.user.id); + } + + // Cookie törlés + clearAuthCookies(res); + + res.json({ + success: true, + message: 'Logged out successfully' + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +} +``` + +### Refresh Token + +```javascript +async refreshToken(req, res) { + try { + // Refresh token kiolvasása + const refreshToken = req.cookies.refreshToken; + + if (!refreshToken) { + return res.status(401).json({ error: 'Refresh token required' }); + } + + // Token validálás + const decoded = verifyRefreshToken(refreshToken); + + // User lekérés + const user = await this.userRepository.findById(decoded.userId); + if (!user) { + return res.status(401).json({ error: 'User not found' }); + } + + // Új access token generálás + const newAccessToken = generateAccessToken(user); + + // Cookie frissítés + 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) { + res.status(401).json({ error: 'Invalid refresh token' }); + } +} +``` + +### Get Current User + +```javascript +async getCurrentUser(req, res) { + try { + // req.user az authenticateToken middleware állítja be + if (!req.user) { + return res.status(401).json({ error: 'Not authenticated' }); + } + + res.json({ + success: true, + data: { + user: req.user.toPublicObject() + } + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +} +``` + +--- + +## 🛡️ AuthMiddleware Implementáció + +### authenticateToken + +```javascript +export async function authenticateToken(req, res, next) { + try { + // 1. Token kiolvasása + const token = extractToken(req); + + if (!token) { + return res.status(401).json({ + error: 'Access token required' + }); + } + + // 2. Token validálás + const decoded = verifyAccessToken(token); + + // 3. User betöltése (FONTOS: ezt implementálni kell!) + // Ehhez kell egy userRepository instance + // Lehetőség 1: req.app.locals.userRepository + // Lehetőség 2: Singleton pattern + // Lehetőség 3: Middleware factory function + + const userRepository = req.app.locals.userRepository; + const user = await userRepository.findById(decoded.userId); + + if (!user) { + return res.status(401).json({ error: 'User not found' }); + } + + // 4. req.user beállítása + req.user = user; + + // 5. Következő middleware + next(); + } catch (error) { + return res.status(401).json({ + error: 'Invalid or expired token' + }); + } +} +``` + +**FONTOS:** A userRepository-t be kell injektálni a middleware-be! + +Megoldás az index.js-ben: + +```javascript +// index.js-ben +app.locals.userRepository = userRepository; +``` + +### requireRole + +```javascript +export function requireRole(allowedRoles) { + return (req, res, next) => { + // Az authenticateToken után fut, tehát req.user létezik + if (!req.user) { + return res.status(401).json({ error: 'Authentication required' }); + } + + if (!allowedRoles.includes(req.user.role)) { + return res.status(403).json({ + error: 'Insufficient permissions', + required: allowedRoles, + current: req.user.role + }); + } + + next(); + }; +} + +// Használat: +// router.delete('/users/:id', authenticateToken, requireRole(['ADMIN']), ...) +``` + +### checkOwnership + +```javascript +export function checkOwnership(getResourceOwnerId) { + return async (req, res, next) => { + try { + if (!req.user) { + return res.status(401).json({ error: 'Authentication required' }); + } + + // Resource owner ID megszerzése + const ownerId = await getResourceOwnerId(req); + + // Admin mindent módosíthat + if (req.user.role === 'ADMIN') { + return next(); + } + + // Ownership ellenőrzés + if (req.user.id !== ownerId) { + return res.status(403).json({ + error: 'You can only modify your own resources' + }); + } + + next(); + } catch (error) { + return res.status(403).json({ error: 'Access denied' }); + } + }; +} + +// Használat blogRoutes.js-ben: +router.put('/:id', + authenticateToken, + checkOwnership(async (req) => { + const blogRepository = req.app.locals.blogRepository; + const blog = await blogRepository.findById(req.params.id); + if (!blog) { + throw new Error('Blog not found'); + } + return blog.authorId; + }), + (req, res) => blogController.updateBlog(req, res) +); +``` + +--- + +## 🔗 Route Protection Példák + +### authRoutes.js + +```javascript +import express from 'express'; +import { authenticateToken } from '../middlewares/authMiddleware.js'; + +export function createAuthRoutes(authController) { + const router = express.Router(); + + // 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 + router.post('/logout', authenticateToken, (req, res) => + authController.logout(req, res) + ); + router.get('/me', authenticateToken, (req, res) => + authController.getCurrentUser(req, res) + ); + + return router; +} +``` + +### blogRoutes.js + +```javascript +import express from 'express'; +import { authenticateToken, checkOwnership } from '../middlewares/authMiddleware.js'; + +export function createBlogRoutes(blogController) { + const router = express.Router(); + + // Publikus route-ok + router.get('/', (req, res) => blogController.getAllBlogs(req, res)); + router.get('/:id', (req, res) => blogController.getBlog(req, res)); + + // Védett route-ok + router.post('/', + authenticateToken, + (req, res) => blogController.createBlog(req, res) + ); + + router.put('/:id', + authenticateToken, + checkOwnership(async (req) => { + const blogRepository = req.app.locals.blogRepository; + const blog = await blogRepository.findById(req.params.id); + return blog?.authorId; + }), + (req, res) => blogController.updateBlog(req, res) + ); + + router.delete('/:id', + authenticateToken, + checkOwnership(async (req) => { + const blogRepository = req.app.locals.blogRepository; + const blog = await blogRepository.findById(req.params.id); + return blog?.authorId; + }), + (req, res) => blogController.deleteBlog(req, res) + ); + + return router; +} +``` + +--- + +## 🔧 index.js Módosítások + +```javascript +// Repository-k injektálása app.locals-ba +app.locals.userRepository = userRepository; +app.locals.blogRepository = blogRepository; + +// Így a middleware-ek hozzáférhetnek: +// const userRepository = req.app.locals.userRepository; +``` + +--- + +## 🧪 Tesztelési Példák + +### Regisztráció + +```bash +curl -X POST http://localhost:3000/api/auth/register \ + -H "Content-Type: application/json" \ + -d '{ + "email": "alice@example.com", + "username": "alice", + "password": "Alice1234" + }' +``` + +### Bejelentkezés + +```bash +curl -X POST http://localhost:3000/api/auth/login \ + -H "Content-Type: application/json" \ + -c cookies.txt \ + -d '{ + "email": "alice@example.com", + "password": "Alice1234" + }' +``` + +### Védett endpoint (cookie-val) + +```bash +curl -X GET http://localhost:3000/api/auth/me \ + -b cookies.txt +``` + +### Védett endpoint (Bearer token-nel) + +```bash +curl -X GET http://localhost:3000/api/auth/me \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +``` + +--- + +**Ez a hints fájl minden szükséges kódrészletet tartalmaz a sikeres implementációhoz! 🚀** diff --git a/Backend/harmadik gyakorlat/README.md b/Backend/harmadik gyakorlat/README.md new file mode 100644 index 0000000..be53a9f --- /dev/null +++ b/Backend/harmadik gyakorlat/README.md @@ -0,0 +1,658 @@ +# Backend Fejlesztés Gyakorló Feladat +## Authentication & Authorization Implementáció + +--- + +## 📋 Tartalom + +1. [Projekt Áttekintés](#projekt-áttekintés) +2. [Architektúra](#architektúra) +3. [Előkészületek](#előkészületek) +4. [A Feladat](#a-feladat) +5. [Implementációs Útmutató](#implementációs-útmutató) +6. [Tesztelés](#tesztelés) +7. [Hasznos Források](#hasznos-források) + +--- + +## 🎯 Projekt Áttekintés + +Ez egy gyakorló projekt backend fejlesztők számára, amely az **Authentication** és **Authorization** implementálására fókuszál. A projekt egy blog platformot szimulál, ahol a felhasználók regisztrálhatnak, bejelentkezhetnek, és blogokat írhatnak. + +### Technológiai Stack + +- **Node.js** + **Express.js** - Backend framework +- **Prisma ORM** - Database ORM +- **PostgreSQL** - Relációs adatbázis +- **Redis** - Cache és session management +- **JWT** - Token-based authentication +- **bcrypt** - Jelszó hash-elés +- **Cookie-parser** - Cookie kezelés +- **Docker** - Konténerizáció + +### Amit MEG KELL implementálni (FELADAT): + +✅ Authentication (Hitelesítés) +- Regisztráció +- Bejelentkezés +- Kijelentkezés +- Token refresh +- Current user lekérés + +✅ Authorization (Jogosultság kezelés) +- Authentication middleware +- Role-based access control (RBAC) +- Resource ownership ellenőrzés + +### Amit NEM kell implementálni (már kész): + +✅ Prisma schema és migrációk +✅ Repository pattern implementáció +✅ CQRS command/query handlers +✅ Blog CRUD műveletek +✅ Docker konfiguráció +✅ Projekt struktúra + +--- + +## 🏗️ Architektúra + +A projekt **Clean Architecture** és **CQRS** mintákat követ: + +``` +src/ +├── domain/ # Domain réteg (business logic) +│ ├── models/ # Domain modellek +│ │ ├── User.js +│ │ └── Blog.js +│ └── repositories/ # Repository interfészek +│ ├── IUserRepository.js +│ └── IBlogRepository.js +│ +├── application/ # Application réteg (use cases) +│ ├── commands/ # Command objektumok +│ │ ├── CreateBlogCommand.js +│ │ ├── UpdateBlogCommand.js +│ │ └── DeleteBlogCommand.js +│ ├── queries/ # Query objektumok +│ │ └── BlogQueries.js +│ └── handlers/ # Command/Query handlerek +│ ├── CreateBlogHandler.js +│ ├── UpdateBlogHandler.js +│ ├── DeleteBlogHandler.js +│ └── BlogQueryHandler.js +│ +├── infrastructure/ # Infrastructure réteg (külső függőségek) +│ ├── database/ # Database kapcsolatok +│ │ ├── prisma.js +│ │ └── redis.js +│ ├── repositories/ # Repository implementációk +│ │ ├── UserRepository.js +│ │ └── BlogRepository.js +│ └── config/ # Konfigurációk +│ └── index.js +│ +├── api/ # API réteg (HTTP) +│ ├── controllers/ # Controller-ek +│ │ ├── AuthController.js # ⚠️ IMPLEMENTÁLANDÓ +│ │ └── BlogController.js +│ ├── routes/ # Route definíciók +│ │ ├── authRoutes.js +│ │ ├── blogRoutes.js +│ │ └── userRoutes.js +│ └── middlewares/ # Middleware-ek +│ ├── authMiddleware.js # ⚠️ IMPLEMENTÁLANDÓ +│ └── errorHandler.js +│ +└── index.js # Alkalmazás belépési pont +``` + +### Rétegek Felelősségei + +**Domain réteg:** +- Domain modellek és üzleti logika +- Repository interfészek (dependency inversion) + +**Application réteg:** +- Use case-ek implementációja +- Command/Query objektumok és handlerek +- Validációs logika + +**Infrastructure réteg:** +- Külső rendszerek integrációja (DB, Redis) +- Repository implementációk +- Konfigurációk + +**API réteg:** +- HTTP kérések kezelése +- Routing +- Middleware-ek +- Authentication & Authorization ⚠️ + +--- + +## 🚀 Előkészületek + +### 1. Függőségek telepítése + +```bash +npm install +``` + +### 2. Környezeti változók beállítása + +Másold le a `.env.example` fájlt `.env` néven: + +```bash +cp .env.example .env +``` + +Állítsd be a környezeti változókat `.env` fájlban: + +```env +DATABASE_URL="postgresql://postgres:postgres@localhost:5432/blog_db?schema=public" +REDIS_HOST=localhost +REDIS_PORT=6379 + +JWT_SECRET=valami-nagyon-titkos-kulcs-ide +JWT_EXPIRES_IN=7d +JWT_REFRESH_SECRET=masik-nagyon-titkos-kulcs-ide +JWT_REFRESH_EXPIRES_IN=30d + +PORT=3000 +NODE_ENV=development + +COOKIE_SECRET=cookie-titkos-kulcs-ide +``` + +⚠️ **FONTOS:** Production környezetben használj erős, random generált kulcsokat! + +### 3. Docker konténerek indítása + +```bash +npm run docker:up +``` + +Ez elindítja a PostgreSQL és Redis konténereket. + +### 4. Adatbázis migráció + +```bash +npm run prisma:migrate +npm run prisma:generate +``` + +### 5. Projekt indítása (development) + +```bash +npm run dev +``` + +A szerver elindul a `http://localhost:3000` címen. + +Ellenőrizd a health endpoint-ot: +```bash +curl http://localhost:3000/health +``` + +--- + +## 📝 A Feladat + +A feladatod **három fő komponens** implementálása: + +### 1️⃣ AuthController implementálása + +**Fájl:** `src/api/controllers/AuthController.js` + +Implementálandó metódusok: + +#### `register(req, res)` +- Új felhasználó regisztrációja +- Input validáció (email, username, password) +- Uniqueness ellenőrzés (email és username) +- Jelszó hash-elés `bcrypt`-tel +- User létrehozása repository-n keresztül +- JWT token generálás +- HTTP-only cookie beállítás +- User publikus adatainak visszaadása (jelszó nélkül!) + +#### `login(req, res)` +- Bejelentkezés email vagy username alapján +- User keresése repository-val +- Jelszó ellenőrzés `bcrypt.compare()`-val +- Access és refresh token generálás +- Cookie-k beállítása (httpOnly, secure, sameSite) +- Opcionális: Session tárolás Redis-ben +- Sikeres bejelentkezés esetén user adatok és tokenek visszaadása + +#### `logout(req, res)` +- Cookie-k törlése +- Redis session törlése (ha van) +- Refresh token invalidálás + +#### `refreshToken(req, res)` +- Refresh token kiolvasása cookie-ból +- Token validálás +- Új access token generálás +- Új cookie beállítás + +#### `getCurrentUser(req, res)` +- Bejelentkezett user adatainak visszaadása +- `req.user` alapján (amit az auth middleware állít be) + +--- + +### 2️⃣ AuthMiddleware-ek implementálása + +**Fájl:** `src/api/middlewares/authMiddleware.js` + +#### `authenticateToken(req, res, next)` + +Ez a middleware **minden védett route előtt** fut. + +**Feladatai:** +1. JWT token kiolvasása: + - Cookie-ból: `req.cookies.accessToken` + - VAGY Authorization header-ből: `Bearer ` +2. Token validálás `jwt.verify()`-val +3. User ID kinyerése a token payload-ból +4. User betöltése repository-val +5. `req.user` beállítása a user objektummal +6. `next()` hívása + +**Hibakezelés:** +- Ha nincs token → 401 Unauthorized +- Ha érvénytelen token → 401 Unauthorized +- Ha user nem létezik → 401 Unauthorized + +#### `requireRole(allowedRoles)` + +Ez egy **higher-order middleware**, ami role-based access control-t implementál. + +**Feladatai:** +1. Visszaad egy middleware függvényt +2. Ellenőrzi, hogy `req.user` létezik (az `authenticateToken` után fut!) +3. Ellenőrzi, hogy `req.user.role` benne van-e az `allowedRoles` tömbben +4. Ha nincs jogosultság → 403 Forbidden +5. Ha van jogosultság → `next()` + +**Példa használat:** +```javascript +router.delete('/admin/users/:id', + authenticateToken, + requireRole(['ADMIN']), + deleteUserController +); +``` + +#### `checkOwnership(getResourceOwnerId)` + +Ez a middleware **resource ownership** ellenőrzést implementál. + +**Feladatai:** +1. Visszaad egy middleware függvényt +2. Meghívja a `getResourceOwnerId` függvényt, hogy megszerezze a resource tulajdonos ID-ját +3. Ellenőrzi, hogy: + - `req.user.id === ownerId` (a user a tulajdonos) + - VAGY `req.user.role === 'ADMIN'` (ADMIN mindent módosíthat) +4. Ha egyik sem teljesül → 403 Forbidden +5. Ha OK → `next()` + +**Példa használat:** +```javascript +router.put('/blogs/:id', + authenticateToken, + checkOwnership(async (req) => { + const blog = await blogRepository.findById(req.params.id); + return blog.authorId; + }), + (req, res) => blogController.updateBlog(req, res) +); +``` + +--- + +### 3️⃣ Middleware-ek integrálása a route-okba + +**Fájlok:** +- `src/api/routes/authRoutes.js` +- `src/api/routes/blogRoutes.js` + +Add hozzá az `authenticateToken` middleware-t a védett endpoint-okhoz: + +**authRoutes.js:** +```javascript +router.post('/logout', authenticateToken, (req, res) => authController.logout(req, res)); +router.get('/me', authenticateToken, (req, res) => authController.getCurrentUser(req, res)); +``` + +**blogRoutes.js:** +```javascript +router.post('/', authenticateToken, (req, res) => blogController.createBlog(req, res)); +router.put('/:id', authenticateToken, checkOwnership(...), (req, res) => blogController.updateBlog(req, res)); +router.delete('/:id', authenticateToken, checkOwnership(...), (req, res) => blogController.deleteBlog(req, res)); +``` + +--- + +## 🛠️ Implementációs Útmutató + +### JWT Token Generálás + +```javascript +import jwt from 'jsonwebtoken'; + +// Access token generálás +const accessToken = jwt.sign( + { + userId: user.id, + email: user.email, + role: user.role + }, + process.env.JWT_SECRET, + { expiresIn: process.env.JWT_EXPIRES_IN } +); + +// Refresh token generálás +const refreshToken = jwt.sign( + { userId: user.id }, + process.env.JWT_REFRESH_SECRET, + { expiresIn: process.env.JWT_REFRESH_EXPIRES_IN } +); +``` + +### JWT Token Validálás + +```javascript +import jwt from 'jsonwebtoken'; + +try { + const decoded = jwt.verify(token, process.env.JWT_SECRET); + // decoded.userId, decoded.email, decoded.role +} catch (error) { + // Token érvénytelen vagy lejárt + throw new Error('Invalid token'); +} +``` + +### Jelszó Hash-elés + +```javascript +import bcrypt from 'bcrypt'; + +// Hash-elés (regisztrációnál) +const saltRounds = 10; +const hashedPassword = await bcrypt.hash(password, saltRounds); + +// Ellenőrzés (bejelentkezésnél) +const isValid = await bcrypt.compare(password, user.password); +``` + +### Cookie Beállítás + +```javascript +// Access token cookie +res.cookie('accessToken', accessToken, { + httpOnly: true, // JavaScript nem férhet hozzá + secure: process.env.NODE_ENV === 'production', // Csak HTTPS-en + sameSite: 'strict', // CSRF védelem + maxAge: 7 * 24 * 60 * 60 * 1000 // 7 nap +}); + +// Refresh token cookie +res.cookie('refreshToken', refreshToken, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + maxAge: 30 * 24 * 60 * 60 * 1000 // 30 nap +}); +``` + +### Cookie Törlés + +```javascript +res.clearCookie('accessToken'); +res.clearCookie('refreshToken'); +``` + +### Redis Session Tárolás (Opcionális) + +```javascript +import { redis } from '../infrastructure/database/redis.js'; + +// Session mentése +await redis.set( + `session:${userId}`, + JSON.stringify({ userId, email, loginTime: new Date() }), + 'EX', + 60 * 60 * 24 * 7 // 7 nap TTL +); + +// Session lekérése +const session = await redis.get(`session:${userId}`); +const sessionData = JSON.parse(session); + +// Session törlése +await redis.del(`session:${userId}`); +``` + +### Token Kiolvasás Cookie-ból vagy Header-ből + +```javascript +function getTokenFromRequest(req) { + // Cookie-ból + if (req.cookies && req.cookies.accessToken) { + return req.cookies.accessToken; + } + + // Authorization header-ből + const authHeader = req.headers.authorization; + if (authHeader && authHeader.startsWith('Bearer ')) { + return authHeader.substring(7); + } + + return null; +} +``` + +--- + +## 🧪 Tesztelés + +### 1. Regisztráció + +```bash +curl -X POST http://localhost:3000/api/auth/register \ + -H "Content-Type: application/json" \ + -d '{ + "email": "test@example.com", + "username": "testuser", + "password": "SecurePassword123" + }' +``` + +**Elvárt válasz:** +```json +{ + "success": true, + "data": { + "user": { + "id": "uuid...", + "email": "test@example.com", + "username": "testuser", + "role": "USER" + }, + "accessToken": "eyJhbGc...", + "refreshToken": "eyJhbGc..." + } +} +``` + +### 2. Bejelentkezés + +```bash +curl -X POST http://localhost:3000/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{ + "email": "test@example.com", + "password": "SecurePassword123" + }' +``` + +### 3. Current User + +```bash +curl -X GET http://localhost:3000/api/auth/me \ + -H "Authorization: Bearer " +``` + +VAGY cookie-val: + +```bash +curl -X GET http://localhost:3000/api/auth/me \ + --cookie "accessToken=" +``` + +### 4. Blog Létrehozás (Védett endpoint) + +```bash +curl -X POST http://localhost:3000/api/blogs \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer " \ + -d '{ + "title": "My First Blog", + "content": "This is awesome!", + "published": true + }' +``` + +### 5. Blog Módosítás (Ownership ellenőrzéssel) + +```bash +curl -X PUT http://localhost:3000/api/blogs/ \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer " \ + -d '{ + "title": "Updated Title" + }' +``` + +### 6. Kijelentkezés + +```bash +curl -X POST http://localhost:3000/api/auth/logout \ + -H "Authorization: Bearer " +``` + +--- + +## ✅ Ellenőrző Lista + +Implementációd akkor teljes, ha: + +- [ ] ✅ Sikeres regisztráció email + username + password-dal +- [ ] ✅ Email és username uniqueness ellenőrzés működik +- [ ] ✅ Jelszó hash-elve van tárolva (bcrypt) +- [ ] ✅ Sikeres bejelentkezés +- [ ] ✅ Rossz jelszóval nem lehet bejelentkezni +- [ ] ✅ JWT token generálódik és cookie-ban tárolódik +- [ ] ✅ AuthMiddleware validálja a tokent +- [ ] ✅ Védett endpoint-ok csak token-nal elérhetők +- [ ] ✅ `/api/auth/me` visszaadja a bejelentkezett user adatait +- [ ] ✅ Kijelentkezés törli a cookie-kat +- [ ] ✅ Token refresh működik +- [ ] ✅ Role-based access control működik (ADMIN vs USER) +- [ ] ✅ Ownership check működik (csak saját blog módosítható) +- [ ] ✅ Hibakezelés 401/403 válaszokkal + +--- + +## 📚 Hasznos Források + +### Dokumentációk + +- [JWT](https://jwt.io/) - JSON Web Token +- [bcrypt](https://www.npmjs.com/package/bcrypt) - Password hashing +- [Prisma](https://www.prisma.io/docs) - ORM dokumentáció +- [Express](https://expressjs.com/) - Web framework +- [cookie-parser](https://www.npmjs.com/package/cookie-parser) - Cookie middleware + +### Tananyagok + +- [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html) +- [JWT Best Practices](https://blog.logrocket.com/jwt-authentication-best-practices/) +- [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) +- [CQRS Pattern](https://martinfowler.com/bliki/CQRS.html) + +--- + +## 🐛 Gyakori Hibák és Megoldások + +### "JWT must be provided" +- Ellenőrizd, hogy a token szerepel-e a cookie-ban vagy Authorization header-ben +- Ellenőrizd a cookie nevét (`accessToken`) + +### "Invalid token" +- Ellenőrizd a JWT_SECRET változót +- Lehet, hogy a token lejárt - próbálj újra bejelentkezni + +### "User already exists" +- Email vagy username már foglalt +- Használj egyedi értékeket + +### "Incorrect password" +- Ellenőrizd a bcrypt.compare() használatát +- A paraméterek sorrendje: `bcrypt.compare(plainPassword, hashedPassword)` + +### "403 Forbidden" +- Nincs jogosultságod az adott művelethez +- Ellenőrizd a role-t vagy az ownership-et + +--- + +## 🎓 Értékelési Szempontok + +A feladat sikeres megoldása: + +1. **Működőképesség (40%)** + - Regisztráció működik + - Bejelentkezés/kijelentkezés működik + - Védett endpoint-ok hozzáférés-védelme + +2. **Biztonság (30%)** + - Jelszavak hash-elve vannak + - HTTP-only cookie-k használata + - JWT token biztonságos kezelése + - Input validáció + +3. **Kódminőség (20%)** + - Letisztult, olvasható kód + - Megfelelő error handling + - Architektúra követése + +4. **Extra funkciók (10%)** + - Redis session management + - Token refresh flow + - Role-based access control + - Ownership validation + +--- + +## 💡 Tippek + +1. **Kezdd a legegyszerűbbel:** Először implementáld a regisztrációt, aztán a logint. + +2. **Tesztelj folyamatosan:** Minden metódus után tesztelj curl-lel vagy Postman-nel. + +3. **Nézd meg a meglévő kódot:** A BlogController egy jó példa, hogyan kell használni a repository-kat és handler-eket. + +4. **Debug logging:** Használj `console.log()`-ot fejlesztés közben, hogy lásd mi történik. + +5. **Hibakezelés:** Minden async művelet legyen try-catch blokkban. + +6. **Token lejárat:** Fejlesztés közben használj rövid lejárati időt (pl. 15 perc), hogy tesztelhesd a refresh flow-t. + +--- + +**Jó munkát és kellemes kódolást! 🚀** diff --git a/Backend/harmadik gyakorlat/docker-compose.yml b/Backend/harmadik gyakorlat/docker-compose.yml new file mode 100644 index 0000000..1d65442 --- /dev/null +++ b/Backend/harmadik gyakorlat/docker-compose.yml @@ -0,0 +1,38 @@ +version: '3.8' + +services: + postgres: + image: postgres:15-alpine + container_name: blog_postgres + restart: unless-stopped + ports: + - "5432:5432" + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: blog_db + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + container_name: blog_redis + restart: unless-stopped + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + postgres_data: + redis_data: diff --git a/Backend/harmadik gyakorlat/package.json b/Backend/harmadik gyakorlat/package.json new file mode 100644 index 0000000..27fb98c --- /dev/null +++ b/Backend/harmadik gyakorlat/package.json @@ -0,0 +1,32 @@ +{ + "name": "blog-auth-practice", + "version": "1.0.0", + "description": "Backend gyakorló feladat - Authentication & Authorization", + "main": "src/index.js", + "type": "module", + "scripts": { + "dev": "node --watch src/index.js", + "start": "node src/index.js", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev", + "prisma:studio": "prisma studio", + "docker:up": "docker-compose up -d", + "docker:down": "docker-compose down", + "setup": "npm install && npm run docker:up && npm run prisma:migrate && npm run prisma:generate" + }, + "keywords": ["backend", "authentication", "authorization", "jwt", "prisma", "cqrs"], + "author": "", + "license": "ISC", + "dependencies": { + "@prisma/client": "^5.9.1", + "bcrypt": "^5.1.1", + "cookie-parser": "^1.4.6", + "dotenv": "^16.4.1", + "express": "^4.18.2", + "ioredis": "^5.3.2", + "jsonwebtoken": "^9.0.2" + }, + "devDependencies": { + "prisma": "^5.9.1" + } +} diff --git a/Backend/harmadik gyakorlat/prisma/schema.prisma b/Backend/harmadik gyakorlat/prisma/schema.prisma new file mode 100644 index 0000000..42abd54 --- /dev/null +++ b/Backend/harmadik gyakorlat/prisma/schema.prisma @@ -0,0 +1,44 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id String @id @default(uuid()) + email String @unique + username String @unique + password String + role Role @default(USER) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + blogs Blog[] + + @@map("users") +} + +model Blog { + id String @id @default(uuid()) + title String + content String + published Boolean @default(false) + authorId String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) + + @@map("blogs") +} + +enum Role { + USER + ADMIN +} diff --git a/Backend/harmadik gyakorlat/src/api/controllers/AuthController.js b/Backend/harmadik gyakorlat/src/api/controllers/AuthController.js new file mode 100644 index 0000000..d3df7c9 --- /dev/null +++ b/Backend/harmadik gyakorlat/src/api/controllers/AuthController.js @@ -0,0 +1,160 @@ +/** + * AuthController + * + * FELADAT: Implementáld az alábbi metódusokat! + * + * Ez a controller felelős az authentication és authorization kezeléséért. + * A következő funkciók implementálása szükséges: + * + * 1. register() - Felhasználó regisztráció + * - Email és username uniqueness ellenőrzés + * - Jelszó hash-elés bcrypt-tel + * - User létrehozása a repository-n keresztül + * - JWT token generálás + * - Cookie beállítás + * + * 2. login() - Bejelentkezés + * - User keresése email vagy username alapján + * - Jelszó ellenőrzés bcrypt.compare()-val + * - JWT token generálás (access és refresh token) + * - Cookie-k beállítása + * - Redis-ben session tárolás (opcionális) + * + * 3. logout() - Kijelentkezés + * - Cookie-k törlése + * - Redis session törlése + * + * 4. refreshToken() - Token frissítés + * - Refresh token validálás + * - Új access token generálás + * + * 5. getCurrentUser() - Bejelentkezett user adatai + * - req.user alapján user visszaadása + */ +export class AuthController { + constructor(userRepository) { + this.userRepository = userRepository; + } + + /** + * POST /api/auth/register + * Új felhasználó regisztrációja + */ + async register(req, res) { + try { + // TODO: Implementáld a regisztrációt + // 1. Validáld az input adatokat (email, username, password) + // 2. Ellenőrizd, hogy létezik-e már a user (email vagy username) + // 3. Hash-eld a jelszót bcrypt-tel + // 4. Hozd létre a user-t a repository-n keresztül + // 5. Generálj JWT tokent + // 6. Állítsd be a cookie-kat + // 7. Küldd vissza a user adatokat (jelszó nélkül!) + + res.status(501).json({ + success: false, + error: 'Register not implemented yet - this is your task!' + }); + } catch (error) { + res.status(400).json({ + success: false, + error: error.message + }); + } + } + + /** + * POST /api/auth/login + * Bejelentkezés + */ + async login(req, res) { + try { + // TODO: Implementáld a login-t + // 1. Keresd meg a user-t email vagy username alapján + // 2. Ellenőrizd a jelszót bcrypt.compare()-val + // 3. Generálj JWT access és refresh tokent + // 4. Állítsd be a cookie-kat (httpOnly, secure) + // 5. Opcionálisan tárolj session-t Redis-ben + // 6. Küldd vissza a user adatokat és tokeneket + + res.status(501).json({ + success: false, + error: 'Login not implemented yet - this is your task!' + }); + } catch (error) { + res.status(401).json({ + success: false, + error: error.message + }); + } + } + + /** + * POST /api/auth/logout + * Kijelentkezés + */ + async logout(req, res) { + try { + // TODO: Implementáld a logout-ot + // 1. Töröld a cookie-kat + // 2. Töröld a Redis session-t (ha van) + // 3. Invalidáld a refresh tokent + + res.status(501).json({ + success: false, + error: 'Logout not implemented yet - this is your task!' + }); + } catch (error) { + res.status(400).json({ + success: false, + error: error.message + }); + } + } + + /** + * POST /api/auth/refresh + * Access token frissítése refresh token alapján + */ + async refreshToken(req, res) { + try { + // TODO: Implementáld a token refresh-t + // 1. Olvasd ki a refresh tokent a cookie-ból + // 2. Validáld a refresh tokent + // 3. Generálj új access tokent + // 4. Állítsd be az új cookie-t + + res.status(501).json({ + success: false, + error: 'Refresh token not implemented yet - this is your task!' + }); + } catch (error) { + res.status(401).json({ + success: false, + error: error.message + }); + } + } + + /** + * GET /api/auth/me + * Bejelentkezett felhasználó adatai + */ + async getCurrentUser(req, res) { + try { + // TODO: Implementáld a current user lekérést + // 1. Olvasd ki a user-t req.user-ből (amit az auth middleware állít be) + // 2. Küldd vissza a user publikus adatait + + res.status(501).json({ + success: false, + error: 'Get current user not implemented yet - this is your task!' + }); + } catch (error) { + res.status(401).json({ + success: false, + error: error.message + }); + } + } +} diff --git a/Backend/harmadik gyakorlat/src/api/controllers/BlogController.js b/Backend/harmadik gyakorlat/src/api/controllers/BlogController.js new file mode 100644 index 0000000..a89302b --- /dev/null +++ b/Backend/harmadik gyakorlat/src/api/controllers/BlogController.js @@ -0,0 +1,179 @@ +import { CreateBlogCommand } from '../../application/commands/CreateBlogCommand.js'; +import { UpdateBlogCommand } from '../../application/commands/UpdateBlogCommand.js'; +import { DeleteBlogCommand } from '../../application/commands/DeleteBlogCommand.js'; +import { GetBlogQuery, GetAllBlogsQuery, GetUserBlogsQuery } from '../../application/queries/BlogQueries.js'; + +/** + * BlogController + * Blog műveletek kezelése + */ +export class BlogController { + constructor(createHandler, updateHandler, deleteHandler, queryHandler) { + this.createHandler = createHandler; + this.updateHandler = updateHandler; + this.deleteHandler = deleteHandler; + this.queryHandler = queryHandler; + } + + /** + * Blog létrehozása + * POST /api/blogs + */ + async createBlog(req, res) { + try { + // FIGYELEM: req.user-t az auth middleware-nek kell beállítania + // Ez a feladat része - implementálandó! + const authorId = req.user?.id; + + if (!authorId) { + return res.status(401).json({ error: 'Authentication required' }); + } + + const command = new CreateBlogCommand({ + ...req.body, + authorId + }); + + const blog = await this.createHandler.handle(command); + + res.status(201).json({ + success: true, + data: blog.toObject() + }); + } catch (error) { + res.status(400).json({ + success: false, + error: error.message + }); + } + } + + /** + * Blog módosítása + * PUT /api/blogs/:id + */ + async updateBlog(req, res) { + try { + const userId = req.user?.id; + + if (!userId) { + return res.status(401).json({ error: 'Authentication required' }); + } + + const command = new UpdateBlogCommand(req.params.id, req.body); + + // FIGYELEM: Authorization ellenőrzés szükséges! + // Csak a szerző vagy admin módosíthatja + // Ez a feladat része - implementálandó! + + const blog = await this.updateHandler.handle(command); + + res.json({ + success: true, + data: blog.toObject() + }); + } catch (error) { + res.status(400).json({ + success: false, + error: error.message + }); + } + } + + /** + * Blog törlése + * DELETE /api/blogs/:id + */ + async deleteBlog(req, res) { + try { + const userId = req.user?.id; + + if (!userId) { + return res.status(401).json({ error: 'Authentication required' }); + } + + const command = new DeleteBlogCommand(req.params.id, userId); + const result = await this.deleteHandler.handle(command); + + res.json({ + success: true, + message: result.message + }); + } catch (error) { + res.status(400).json({ + success: false, + error: error.message + }); + } + } + + /** + * Egy blog lekérése + * GET /api/blogs/:id + */ + async getBlog(req, res) { + try { + const query = new GetBlogQuery(req.params.id); + const blog = await this.queryHandler.handleGetBlog(query); + + res.json({ + success: true, + data: blog.toObject() + }); + } catch (error) { + res.status(404).json({ + success: false, + error: error.message + }); + } + } + + /** + * Összes blog lekérése + * GET /api/blogs + */ + async getAllBlogs(req, res) { + try { + const query = new GetAllBlogsQuery({ + publishedOnly: req.query.published === 'true', + limit: req.query.limit ? parseInt(req.query.limit) : undefined, + offset: req.query.offset ? parseInt(req.query.offset) : 0 + }); + + const blogs = await this.queryHandler.handleGetAllBlogs(query); + + res.json({ + success: true, + data: blogs.map(blog => blog.toObject()), + count: blogs.length + }); + } catch (error) { + res.status(400).json({ + success: false, + error: error.message + }); + } + } + + /** + * User blogjai + * GET /api/users/:userId/blogs + */ + async getUserBlogs(req, res) { + try { + const query = new GetUserBlogsQuery(req.params.userId); + const blogs = await this.queryHandler.handleGetUserBlogs(query); + + res.json({ + success: true, + data: blogs.map(blog => blog.toObject()), + count: blogs.length + }); + } catch (error) { + res.status(400).json({ + success: false, + error: error.message + }); + } + } +} diff --git a/Backend/harmadik gyakorlat/src/api/middlewares/authMiddleware.js b/Backend/harmadik gyakorlat/src/api/middlewares/authMiddleware.js new file mode 100644 index 0000000..3c9da23 --- /dev/null +++ b/Backend/harmadik gyakorlat/src/api/middlewares/authMiddleware.js @@ -0,0 +1,112 @@ +/** + * Authentication Middleware + * + * FELADAT: Implementáld az authentication middleware-t! + * + * Ez a middleware felelős azért, hogy: + * 1. Ellenőrizze a JWT tokent (cookie-ból vagy Authorization header-ből) + * 2. Validálja a tokent + * 3. Beállítsa a req.user objektumot a dekódolt token alapján + * 4. Hiba esetén 401 Unauthorized választ küldjön + * + * Példa használat: + * router.get('/protected', authenticateToken, (req, res) => { + * res.json({ user: req.user }); + * }); + */ +export function authenticateToken(req, res, next) { + try { + // TODO: Implementáld az authentication-t + // 1. Olvasd ki a tokent a cookie-ból vagy Authorization header-ből + // 2. Validáld a tokent jwt.verify()-val + // 3. Állítsd be req.user-t a dekódolt token payload-jából + // 4. Hívd meg a next()-et, hogy tovább menjen a request + + // Ideiglenes: mindig hiba, amíg nincs implementálva + return res.status(401).json({ + success: false, + error: 'Authentication middleware not implemented yet - this is your task!' + }); + } catch (error) { + return res.status(401).json({ + success: false, + error: 'Invalid or expired token' + }); + } +} + +/** + * Authorization Middleware - Role Check + * + * FELADAT: Implementáld a role-based authorization middleware-t! + * + * Ez a middleware ellenőrzi, hogy a bejelentkezett usernek van-e megfelelő role-ja. + * + * Példa használat: + * router.delete('/admin/users/:id', authenticateToken, requireRole(['ADMIN']), (req, res) => { + * // Csak ADMIN role-lal lehet törölni + * }); + */ +export function requireRole(allowedRoles) { + return (req, res, next) => { + try { + // TODO: Implementáld a role check-et + // 1. Ellenőrizd, hogy req.user létezik-e (authenticateToken után fut) + // 2. Ellenőrizd, hogy req.user.role benne van-e az allowedRoles tömbben + // 3. Ha nincs jogosultság, küldj 403 Forbidden választ + // 4. Ha van jogosultság, hívd meg a next()-et + + return res.status(403).json({ + success: false, + error: 'Authorization middleware not implemented yet - this is your task!' + }); + } catch (error) { + return res.status(403).json({ + success: false, + error: 'Insufficient permissions' + }); + } + }; +} + +/** + * Resource Owner Check + * + * FELADAT: Implementáld a resource ownership ellenőrzést! + * + * Ez a middleware ellenőrzi, hogy a user tulajdonosa-e az adott resource-nak. + * Például: csak a blog szerzője módosíthatja/törölheti a blogot. + * + * @param {Function} getResourceOwnerId - Függvény, ami visszaadja a resource owner ID-t + * + * Példa használat: + * router.put('/blogs/:id', + * authenticateToken, + * checkOwnership(async (req) => { + * const blog = await blogRepository.findById(req.params.id); + * return blog.authorId; + * }), + * (req, res) => { ... } + * ); + */ +export function checkOwnership(getResourceOwnerId) { + return async (req, res, next) => { + try { + // TODO: Implementáld az ownership check-et + // 1. Szerezd meg a resource owner ID-t a getResourceOwnerId függvénnyel + // 2. Ellenőrizd, hogy req.user.id === ownerId VAGY req.user.role === 'ADMIN' + // 3. Ha nem egyezik és nem admin, küldj 403 Forbidden választ + // 4. Ha OK, hívd meg a next()-et + + return res.status(403).json({ + success: false, + error: 'Ownership check middleware not implemented yet - this is your task!' + }); + } catch (error) { + return res.status(403).json({ + success: false, + error: 'Access denied' + }); + } + }; +} diff --git a/Backend/harmadik gyakorlat/src/api/middlewares/errorHandler.js b/Backend/harmadik gyakorlat/src/api/middlewares/errorHandler.js new file mode 100644 index 0000000..859dc0a --- /dev/null +++ b/Backend/harmadik gyakorlat/src/api/middlewares/errorHandler.js @@ -0,0 +1,26 @@ +/** + * Error Handler Middleware + * Globális error handling + */ +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 }) + }); +} + +/** + * Not Found Handler + */ +export function notFoundHandler(req, res) { + res.status(404).json({ + success: false, + error: 'Route not found' + }); +} diff --git a/Backend/harmadik gyakorlat/src/api/routes/authRoutes.js b/Backend/harmadik gyakorlat/src/api/routes/authRoutes.js new file mode 100644 index 0000000..e313f61 --- /dev/null +++ b/Backend/harmadik gyakorlat/src/api/routes/authRoutes.js @@ -0,0 +1,21 @@ +import express from 'express'; + +/** + * Auth Routes + * + * FELADAT: Implementáld az auth middleware-t és védett route-okat! + */ +export function createAuthRoutes(authController) { + const router = express.Router(); + + // 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 - FELADAT: add hozzá az auth middleware-t! + router.post('/logout', (req, res) => authController.logout(req, res)); + router.get('/me', (req, res) => authController.getCurrentUser(req, res)); + + return router; +} diff --git a/Backend/harmadik gyakorlat/src/api/routes/blogRoutes.js b/Backend/harmadik gyakorlat/src/api/routes/blogRoutes.js new file mode 100644 index 0000000..c5d32d2 --- /dev/null +++ b/Backend/harmadik gyakorlat/src/api/routes/blogRoutes.js @@ -0,0 +1,20 @@ +import express from 'express'; + +/** + * Blog Routes + */ +export function createBlogRoutes(blogController) { + const router = express.Router(); + + // Publikus route-ok + router.get('/', (req, res) => blogController.getAllBlogs(req, res)); + router.get('/:id', (req, res) => blogController.getBlog(req, res)); + + // Védett route-ok - FELADAT: add hozzá az auth middleware-t! + // Példa: router.post('/', authMiddleware, (req, res) => ...) + router.post('/', (req, res) => blogController.createBlog(req, res)); + router.put('/:id', (req, res) => blogController.updateBlog(req, res)); + router.delete('/:id', (req, res) => blogController.deleteBlog(req, res)); + + return router; +} diff --git a/Backend/harmadik gyakorlat/src/api/routes/userRoutes.js b/Backend/harmadik gyakorlat/src/api/routes/userRoutes.js new file mode 100644 index 0000000..a40fc3b --- /dev/null +++ b/Backend/harmadik gyakorlat/src/api/routes/userRoutes.js @@ -0,0 +1,13 @@ +import express from 'express'; + +/** + * User Routes + */ +export function createUserRoutes(blogController) { + const router = express.Router(); + + // User blogjai + router.get('/:userId/blogs', (req, res) => blogController.getUserBlogs(req, res)); + + return router; +} diff --git a/Backend/harmadik gyakorlat/src/application/commands/CreateBlogCommand.js b/Backend/harmadik gyakorlat/src/application/commands/CreateBlogCommand.js new file mode 100644 index 0000000..e5b512a --- /dev/null +++ b/Backend/harmadik gyakorlat/src/application/commands/CreateBlogCommand.js @@ -0,0 +1,33 @@ +/** + * CreateBlogCommand + * Blog létrehozási command + */ +export class CreateBlogCommand { + constructor(data) { + this.title = data.title; + this.content = data.content; + this.authorId = data.authorId; + this.published = data.published || false; + } + + validate() { + const errors = []; + + if (!this.title || this.title.trim().length === 0) { + errors.push('Title is required'); + } + + if (!this.content || this.content.trim().length === 0) { + errors.push('Content is required'); + } + + if (!this.authorId) { + errors.push('Author ID is required'); + } + + return { + isValid: errors.length === 0, + errors + }; + } +} diff --git a/Backend/harmadik gyakorlat/src/application/commands/DeleteBlogCommand.js b/Backend/harmadik gyakorlat/src/application/commands/DeleteBlogCommand.js new file mode 100644 index 0000000..10ec680 --- /dev/null +++ b/Backend/harmadik gyakorlat/src/application/commands/DeleteBlogCommand.js @@ -0,0 +1,27 @@ +/** + * DeleteBlogCommand + * Blog törlési command + */ +export class DeleteBlogCommand { + constructor(id, requesterId) { + this.id = id; + this.requesterId = requesterId; // Ki kéri a törlést (authorization-höz kell) + } + + validate() { + const errors = []; + + if (!this.id) { + errors.push('Blog ID is required'); + } + + if (!this.requesterId) { + errors.push('Requester ID is required'); + } + + return { + isValid: errors.length === 0, + errors + }; + } +} diff --git a/Backend/harmadik gyakorlat/src/application/commands/UpdateBlogCommand.js b/Backend/harmadik gyakorlat/src/application/commands/UpdateBlogCommand.js new file mode 100644 index 0000000..7ea74d0 --- /dev/null +++ b/Backend/harmadik gyakorlat/src/application/commands/UpdateBlogCommand.js @@ -0,0 +1,33 @@ +/** + * UpdateBlogCommand + * Blog módosítási command + */ +export class UpdateBlogCommand { + constructor(id, data) { + this.id = id; + this.title = data.title; + this.content = data.content; + this.published = data.published; + } + + validate() { + const errors = []; + + if (!this.id) { + errors.push('Blog ID is required'); + } + + if (this.title !== undefined && this.title.trim().length === 0) { + errors.push('Title cannot be empty'); + } + + if (this.content !== undefined && this.content.trim().length === 0) { + errors.push('Content cannot be empty'); + } + + return { + isValid: errors.length === 0, + errors + }; + } +} diff --git a/Backend/harmadik gyakorlat/src/application/handlers/BlogQueryHandler.js b/Backend/harmadik gyakorlat/src/application/handlers/BlogQueryHandler.js new file mode 100644 index 0000000..27d0d6b --- /dev/null +++ b/Backend/harmadik gyakorlat/src/application/handlers/BlogQueryHandler.js @@ -0,0 +1,31 @@ +/** + * BlogQueryHandler + * Blog query-k kezelése + */ +export class BlogQueryHandler { + constructor(blogRepository) { + this.blogRepository = blogRepository; + } + + async handleGetBlog(query) { + const blog = await this.blogRepository.findById(query.id); + if (!blog) { + throw new Error('Blog not found'); + } + return blog; + } + + async handleGetAllBlogs(query) { + const blogs = await this.blogRepository.findAll({ + publishedOnly: query.publishedOnly, + limit: query.limit, + offset: query.offset + }); + return blogs; + } + + async handleGetUserBlogs(query) { + const blogs = await this.blogRepository.findByAuthorId(query.authorId); + return blogs; + } +} diff --git a/Backend/harmadik gyakorlat/src/application/handlers/CreateBlogHandler.js b/Backend/harmadik gyakorlat/src/application/handlers/CreateBlogHandler.js new file mode 100644 index 0000000..24b884b --- /dev/null +++ b/Backend/harmadik gyakorlat/src/application/handlers/CreateBlogHandler.js @@ -0,0 +1,26 @@ +/** + * CreateBlogHandler + * CreateBlogCommand handler + */ +export class CreateBlogHandler { + constructor(blogRepository) { + this.blogRepository = blogRepository; + } + + async handle(command) { + const validation = command.validate(); + if (!validation.isValid) { + throw new Error(`Validation failed: ${validation.errors.join(', ')}`); + } + + const blogData = { + title: command.title, + content: command.content, + authorId: command.authorId, + published: command.published + }; + + const blog = await this.blogRepository.create(blogData); + return blog; + } +} diff --git a/Backend/harmadik gyakorlat/src/application/handlers/DeleteBlogHandler.js b/Backend/harmadik gyakorlat/src/application/handlers/DeleteBlogHandler.js new file mode 100644 index 0000000..d763f05 --- /dev/null +++ b/Backend/harmadik gyakorlat/src/application/handlers/DeleteBlogHandler.js @@ -0,0 +1,29 @@ +/** + * DeleteBlogHandler + * DeleteBlogCommand handler + */ +export class DeleteBlogHandler { + constructor(blogRepository) { + this.blogRepository = blogRepository; + } + + async handle(command) { + const validation = command.validate(); + if (!validation.isValid) { + throw new Error(`Validation failed: ${validation.errors.join(', ')}`); + } + + // Ellenőrizzük, hogy létezik-e a blog + const existingBlog = await this.blogRepository.findById(command.id); + if (!existingBlog) { + throw new Error('Blog not found'); + } + + // FIGYELEM: Itt kell authorization ellenőrzés! + // Csak a szerző vagy admin törölheti + // Ez a feladat része - implementálandó! + + await this.blogRepository.delete(command.id); + return { success: true, message: 'Blog deleted successfully' }; + } +} diff --git a/Backend/harmadik gyakorlat/src/application/handlers/UpdateBlogHandler.js b/Backend/harmadik gyakorlat/src/application/handlers/UpdateBlogHandler.js new file mode 100644 index 0000000..695a0dc --- /dev/null +++ b/Backend/harmadik gyakorlat/src/application/handlers/UpdateBlogHandler.js @@ -0,0 +1,30 @@ +/** + * UpdateBlogHandler + * UpdateBlogCommand handler + */ +export class UpdateBlogHandler { + constructor(blogRepository) { + this.blogRepository = blogRepository; + } + + async handle(command) { + const validation = command.validate(); + if (!validation.isValid) { + throw new Error(`Validation failed: ${validation.errors.join(', ')}`); + } + + // Ellenőrizzük, hogy létezik-e a blog + const existingBlog = await this.blogRepository.findById(command.id); + if (!existingBlog) { + throw new Error('Blog not found'); + } + + const updateData = {}; + if (command.title !== undefined) updateData.title = command.title; + if (command.content !== undefined) updateData.content = command.content; + if (command.published !== undefined) updateData.published = command.published; + + const blog = await this.blogRepository.update(command.id, updateData); + return blog; + } +} diff --git a/Backend/harmadik gyakorlat/src/application/queries/BlogQueries.js b/Backend/harmadik gyakorlat/src/application/queries/BlogQueries.js new file mode 100644 index 0000000..fdc3c52 --- /dev/null +++ b/Backend/harmadik gyakorlat/src/application/queries/BlogQueries.js @@ -0,0 +1,31 @@ +/** + * GetBlogQuery + * Blog lekérdezési query + */ +export class GetBlogQuery { + constructor(id) { + this.id = id; + } +} + +/** + * GetAllBlogsQuery + * Összes blog lekérdezési query + */ +export class GetAllBlogsQuery { + constructor(options = {}) { + this.publishedOnly = options.publishedOnly || false; + this.limit = options.limit; + this.offset = options.offset || 0; + } +} + +/** + * GetUserBlogsQuery + * User blogjai lekérdezési query + */ +export class GetUserBlogsQuery { + constructor(authorId) { + this.authorId = authorId; + } +} diff --git a/Backend/harmadik gyakorlat/src/domain/models/Blog.js b/Backend/harmadik gyakorlat/src/domain/models/Blog.js new file mode 100644 index 0000000..7624aa9 --- /dev/null +++ b/Backend/harmadik gyakorlat/src/domain/models/Blog.js @@ -0,0 +1,32 @@ +/** + * Domain model - Blog + * Ez a Blog entitás domain reprezentációja + */ +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; + } + + /** + * Blog DTO + */ + toObject() { + 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 + }; + } +} diff --git a/Backend/harmadik gyakorlat/src/domain/models/User.js b/Backend/harmadik gyakorlat/src/domain/models/User.js new file mode 100644 index 0000000..a245c06 --- /dev/null +++ b/Backend/harmadik gyakorlat/src/domain/models/User.js @@ -0,0 +1,29 @@ +/** + * Domain model - User + * Ez a User entitás domain reprezentációja + */ +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; + } + + /** + * User DTO publikus adatokkal (jelszó nélkül) + */ + toPublicObject() { + return { + id: this.id, + email: this.email, + username: this.username, + role: this.role, + createdAt: this.createdAt, + updatedAt: this.updatedAt + }; + } +} diff --git a/Backend/harmadik gyakorlat/src/domain/repositories/IBlogRepository.js b/Backend/harmadik gyakorlat/src/domain/repositories/IBlogRepository.js new file mode 100644 index 0000000..e7ca109 --- /dev/null +++ b/Backend/harmadik gyakorlat/src/domain/repositories/IBlogRepository.js @@ -0,0 +1,33 @@ +/** + * IBlogRepository Interface + * Blog repository interface a dependency inversion principle-nek megfelelően + */ +export class IBlogRepository { + async findById(id) { + throw new Error('Method not implemented'); + } + + async findByAuthorId(authorId) { + throw new Error('Method not implemented'); + } + + async findAll(options = {}) { + throw new Error('Method not implemented'); + } + + async create(blogData) { + throw new Error('Method not implemented'); + } + + async update(id, blogData) { + throw new Error('Method not implemented'); + } + + async delete(id) { + throw new Error('Method not implemented'); + } + + async publish(id) { + throw new Error('Method not implemented'); + } +} diff --git a/Backend/harmadik gyakorlat/src/domain/repositories/IUserRepository.js b/Backend/harmadik gyakorlat/src/domain/repositories/IUserRepository.js new file mode 100644 index 0000000..6d305a0 --- /dev/null +++ b/Backend/harmadik gyakorlat/src/domain/repositories/IUserRepository.js @@ -0,0 +1,33 @@ +/** + * IUserRepository Interface + * User repository interface a dependency inversion principle-nek megfelelően + */ +export class IUserRepository { + async findById(id) { + throw new Error('Method not implemented'); + } + + async findByEmail(email) { + throw new Error('Method not implemented'); + } + + async findByUsername(username) { + throw new Error('Method not implemented'); + } + + async create(userData) { + throw new Error('Method not implemented'); + } + + async update(id, userData) { + throw new Error('Method not implemented'); + } + + async delete(id) { + throw new Error('Method not implemented'); + } + + async findAll() { + throw new Error('Method not implemented'); + } +} diff --git a/Backend/harmadik gyakorlat/src/index.js b/Backend/harmadik gyakorlat/src/index.js new file mode 100644 index 0000000..647fb8b --- /dev/null +++ b/Backend/harmadik gyakorlat/src/index.js @@ -0,0 +1,107 @@ +import express from 'express'; +import cookieParser from 'cookie-parser'; +import dotenv from 'dotenv'; + +// Config +dotenv.config(); + +// Database +import databaseClient from './infrastructure/database/prisma.js'; +import redisClient from './infrastructure/database/redis.js'; + +// Repositories +import { UserRepository } from './infrastructure/repositories/UserRepository.js'; +import { BlogRepository } from './infrastructure/repositories/BlogRepository.js'; + +// Handlers +import { CreateBlogHandler } from './application/handlers/CreateBlogHandler.js'; +import { UpdateBlogHandler } from './application/handlers/UpdateBlogHandler.js'; +import { DeleteBlogHandler } from './application/handlers/DeleteBlogHandler.js'; +import { BlogQueryHandler } from './application/handlers/BlogQueryHandler.js'; + +// Controllers +import { BlogController } from './api/controllers/BlogController.js'; +import { AuthController } from './api/controllers/AuthController.js'; + +// Routes +import { createBlogRoutes } from './api/routes/blogRoutes.js'; +import { createAuthRoutes } from './api/routes/authRoutes.js'; +import { createUserRoutes } from './api/routes/userRoutes.js'; + +// Middlewares +import { errorHandler, notFoundHandler } from './api/middlewares/errorHandler.js'; + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Middleware +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +app.use(cookieParser(process.env.COOKIE_SECRET)); + +// Dependency Injection - Repository létrehozás +const userRepository = new UserRepository(); +const blogRepository = new BlogRepository(); + +// Repository-k injektálása app.locals-ba (middleware-ek számára) +app.locals.userRepository = userRepository; +app.locals.blogRepository = blogRepository; + +// Dependency Injection - Handler létrehozás +const createBlogHandler = new CreateBlogHandler(blogRepository); +const updateBlogHandler = new UpdateBlogHandler(blogRepository); +const deleteBlogHandler = new DeleteBlogHandler(blogRepository); +const blogQueryHandler = new BlogQueryHandler(blogRepository); + +// Dependency Injection - Controller létrehozás +const blogController = new BlogController( + createBlogHandler, + updateBlogHandler, + deleteBlogHandler, + blogQueryHandler +); + +const authController = new AuthController(userRepository); + +// 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', createAuthRoutes(authController)); +app.use('/api/blogs', createBlogRoutes(blogController)); +app.use('/api/users', createUserRoutes(blogController)); + +// 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 port ${PORT}`); + console.log(`📝 Environment: ${process.env.NODE_ENV}`); + console.log(`🔗 API: http://localhost:${PORT}/api`); + console.log(`💚 Health: http://localhost:${PORT}/health`); +}); + +export default app; diff --git a/Backend/harmadik gyakorlat/src/infrastructure/auth/authUtils.js b/Backend/harmadik gyakorlat/src/infrastructure/auth/authUtils.js new file mode 100644 index 0000000..e29e34a --- /dev/null +++ b/Backend/harmadik gyakorlat/src/infrastructure/auth/authUtils.js @@ -0,0 +1,222 @@ +/** + * Authentication Utilities + * + * Ez a fájl példa helper függvényeket tartalmaz az auth implementációhoz. + * Használhatod ezeket a függvényeket az AuthController-ben és middleware-ekben. + */ + +import jwt from 'jsonwebtoken'; +import bcrypt from 'bcrypt'; + +/** + * 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) { + // Minimum 8 karakter + 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) + */ + +import { redis } from '../database/redis.js'; + +export async function createSession(userId, sessionData) { + 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 + ); +} + +export async function getSession(userId) { + const sessionKey = `session:${userId}`; + const session = await redis.get(sessionKey); + + if (!session) { + return null; + } + + return JSON.parse(session); +} + +export async function deleteSession(userId) { + const sessionKey = `session:${userId}`; + await redis.del(sessionKey); +} + +export async function sessionExists(userId) { + const sessionKey = `session:${userId}`; + const exists = await redis.exists(sessionKey); + return exists === 1; +} diff --git a/Backend/harmadik gyakorlat/src/infrastructure/config/index.js b/Backend/harmadik gyakorlat/src/infrastructure/config/index.js new file mode 100644 index 0000000..0f61f0d --- /dev/null +++ b/Backend/harmadik gyakorlat/src/infrastructure/config/index.js @@ -0,0 +1,59 @@ +/** + * Config Helper + * Környezeti változók kezelése és validálása + */ +import dotenv from 'dotenv'; + +dotenv.config(); + +export const config = { + // Server + port: process.env.PORT || 3000, + nodeEnv: process.env.NODE_ENV || 'development', + + // Database + databaseUrl: process.env.DATABASE_URL, + + // Redis + redis: { + host: process.env.REDIS_HOST || 'localhost', + port: parseInt(process.env.REDIS_PORT) || 6379 + }, + + // JWT + jwt: { + secret: process.env.JWT_SECRET, + expiresIn: process.env.JWT_EXPIRES_IN || '7d', + refreshSecret: process.env.JWT_REFRESH_SECRET, + refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '30d' + }, + + // Cookie + cookie: { + secret: process.env.COOKIE_SECRET, + options: { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days + } + } +}; + +/** + * Validálja a kötelező környezeti változókat + */ +export function validateConfig() { + const required = [ + 'DATABASE_URL', + 'JWT_SECRET', + 'JWT_REFRESH_SECRET', + 'COOKIE_SECRET' + ]; + + const missing = required.filter(key => !process.env[key]); + + if (missing.length > 0) { + throw new Error(`Missing required environment variables: ${missing.join(', ')}`); + } +} diff --git a/Backend/harmadik gyakorlat/src/infrastructure/database/prisma.js b/Backend/harmadik gyakorlat/src/infrastructure/database/prisma.js new file mode 100644 index 0000000..bd93aec --- /dev/null +++ b/Backend/harmadik gyakorlat/src/infrastructure/database/prisma.js @@ -0,0 +1,31 @@ +import { PrismaClient } from '@prisma/client'; + +/** + * Prisma Client Singleton + * Az egész alkalmazásban ugyanazt a Prisma Client példányt használjuk + */ +class DatabaseClient { + constructor() { + if (!DatabaseClient.instance) { + this.prisma = new PrismaClient({ + log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], + }); + DatabaseClient.instance = this; + } + return DatabaseClient.instance; + } + + getClient() { + return this.prisma; + } + + async disconnect() { + await this.prisma.$disconnect(); + } +} + +const databaseClient = new DatabaseClient(); +Object.freeze(databaseClient); + +export default databaseClient; +export const prisma = databaseClient.getClient(); diff --git a/Backend/harmadik gyakorlat/src/infrastructure/database/redis.js b/Backend/harmadik gyakorlat/src/infrastructure/database/redis.js new file mode 100644 index 0000000..07397bb --- /dev/null +++ b/Backend/harmadik gyakorlat/src/infrastructure/database/redis.js @@ -0,0 +1,48 @@ +import Redis from 'ioredis'; +import dotenv from 'dotenv'; + +dotenv.config(); + +/** + * Redis Client Singleton + * Cache és session management-hez + */ +class RedisClient { + constructor() { + if (!RedisClient.instance) { + this.client = new Redis({ + host: process.env.REDIS_HOST || 'localhost', + port: process.env.REDIS_PORT || 6379, + retryStrategy: (times) => { + const delay = Math.min(times * 50, 2000); + return delay; + }, + }); + + this.client.on('connect', () => { + console.log('✅ Redis connected'); + }); + + this.client.on('error', (err) => { + console.error('❌ Redis error:', err); + }); + + RedisClient.instance = this; + } + return RedisClient.instance; + } + + getClient() { + return this.client; + } + + async disconnect() { + await this.client.quit(); + } +} + +const redisClient = new RedisClient(); +Object.freeze(redisClient); + +export default redisClient; +export const redis = redisClient.getClient(); diff --git a/Backend/harmadik gyakorlat/src/infrastructure/repositories/BlogRepository.js b/Backend/harmadik gyakorlat/src/infrastructure/repositories/BlogRepository.js new file mode 100644 index 0000000..f251fb6 --- /dev/null +++ b/Backend/harmadik gyakorlat/src/infrastructure/repositories/BlogRepository.js @@ -0,0 +1,122 @@ +import { IBlogRepository } from '../../domain/repositories/IBlogRepository.js'; +import { Blog } from '../../domain/models/Blog.js'; +import { prisma } from '../database/prisma.js'; + +/** + * BlogRepository implementáció Prisma-val + * Implementálja az IBlogRepository interface-t + */ +export class BlogRepository extends IBlogRepository { + async findById(id) { + const blog = await prisma.blog.findUnique({ + where: { id }, + include: { + author: { + select: { + id: true, + username: true, + email: true + } + } + } + }); + return blog ? new Blog(blog) : null; + } + + async findByAuthorId(authorId) { + const blogs = await prisma.blog.findMany({ + where: { authorId }, + include: { + author: { + select: { + id: true, + username: true, + email: true + } + } + }, + orderBy: { createdAt: 'desc' } + }); + return blogs.map(blog => new Blog(blog)); + } + + async findAll(options = {}) { + const { publishedOnly = false, limit, offset } = options; + + const where = publishedOnly ? { published: true } : {}; + + const blogs = await prisma.blog.findMany({ + where, + include: { + author: { + select: { + id: true, + username: true, + email: true + } + } + }, + orderBy: { createdAt: 'desc' }, + take: limit, + skip: offset + }); + return blogs.map(blog => new Blog(blog)); + } + + async create(blogData) { + const blog = await prisma.blog.create({ + data: blogData, + include: { + author: { + select: { + id: true, + username: true, + email: true + } + } + } + }); + return new Blog(blog); + } + + async update(id, blogData) { + const blog = await prisma.blog.update({ + where: { id }, + data: blogData, + include: { + author: { + select: { + id: true, + username: true, + email: true + } + } + } + }); + return new Blog(blog); + } + + async delete(id) { + await prisma.blog.delete({ + where: { id } + }); + return true; + } + + async publish(id) { + const blog = await prisma.blog.update({ + where: { id }, + data: { published: true }, + include: { + author: { + select: { + id: true, + username: true, + email: true + } + } + } + }); + return new Blog(blog); + } +} diff --git a/Backend/harmadik gyakorlat/src/infrastructure/repositories/UserRepository.js b/Backend/harmadik gyakorlat/src/infrastructure/repositories/UserRepository.js new file mode 100644 index 0000000..5b81952 --- /dev/null +++ b/Backend/harmadik gyakorlat/src/infrastructure/repositories/UserRepository.js @@ -0,0 +1,59 @@ +import { IUserRepository } from '../../domain/repositories/IUserRepository.js'; +import { User } from '../../domain/models/User.js'; +import { prisma } from '../database/prisma.js'; + +/** + * UserRepository implementáció Prisma-val + * Implementálja az IUserRepository interface-t + */ +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)); + } +}