Files
GKNB_MSTM071/Backend/negyedik gyakorlat/SEGÍTSÉG.md
T
2026-03-04 20:02:39 +01:00

830 lines
21 KiB
Markdown

# 📚 SEGÍTSÉG - Példakódok és Magyarázatok
## Tartalomjegyzék
1. [DI Container Implementálása](#1-di-container-implementálása)
2. [CORS Middleware Implementálása](#2-cors-middleware-implementálása)
3. [Email Service Implementálása](#3-email-service-implementálása)
4. [Cookie-Based JWT Használata](#4-cookie-based-jwt-használata)
5. [Tesztelési Példák](#5-tesztelési-példák)
6. [Gyakori Hibák és Megoldások](#6-gyakori-hibák-és-megoldások)
---
## 1. DI Container Implementálása
### 📁 Fájl: `src/application/Container.js`
### Teljes megoldás magyarázattal:
```javascript
/**
* Dependency Injection Container
* Supports singleton, transient, and scoped lifetimes
*/
class Container {
constructor() {
// 1. Singleton instance-ok tárolása (egyszer létrehozott objektumok)
this.services = new Map();
// 2. Factory függvények tárolása (objektumokat létrehozó függvények)
this.factories = new Map();
// 3. Lifecycle típusok tárolása (singleton/transient/scoped)
this.lifetimes = new Map();
}
/**
* Service regisztrálása
* @param {string} name - Service neve
* @param {Function} factory - Factory függvény ami a service-t létrehozza
* @param {string} lifetime - 'singleton' | 'transient' | 'scoped'
*/
register(name, factory, lifetime = 'singleton') {
// 4. Factory függvény eltárolása
this.factories.set(name, factory);
// 5. Lifetime típus eltárolása
this.lifetimes.set(name, lifetime);
// 6. Ha singleton, azonnal példányosítjuk és eltároljuk
if (lifetime === 'singleton') {
const instance = factory();
this.services.set(name, instance);
}
}
/**
* Service lekérése
* @param {string} name - Service neve
* @param {Map} scope - Opcionális scope Map scoped lifetime-hoz
* @returns {any} Service instance
*/
resolve(name, scope = null) {
// 7. Scoped lifecycle kezelése
if (this.lifetimes.get(name) === 'scoped' && scope) {
// Ha már van a scope-ban, azt adjuk vissza
if (scope.has(name)) {
return scope.get(name);
}
// Ha nincs még, létrehozzuk és eltároljuk a scope-ban
const instance = this.factories.get(name)();
scope.set(name, instance);
return instance;
}
// 8. Singleton lifecycle - mindig ugyanazt az instance-t adjuk vissza
if (this.lifetimes.get(name) === 'singleton') {
return this.services.get(name);
}
// 9. Transient lifecycle - mindig új instance-t hozunk létre
if (this.lifetimes.get(name) === 'transient') {
return this.factories.get(name)();
}
// 10. Ha nincs regisztrálva a service, hibát dobunk
throw new Error(`Service '${name}' is not registered`);
}
/**
* Új scope létrehozása scoped lifecycle-hoz (pl. request-enkénti instance-ok)
* @returns {Object} Scope objektum resolve metódussal
*/
createScope() {
// 11. Új Map létrehozása a scoped instance-oknak
const scopeMap = new Map();
// 12. Visszaadunk egy objektumot ami tartalmaz egy resolve metódust
return {
resolve: (name) => this.resolve(name, scopeMap)
};
}
}
module.exports = Container;
```
### 💡 Használati példák:
#### Singleton (egy instance az egész alkalmazásban)
```javascript
const container = new Container();
// Singleton PrismaClient (egy kapcsolat az egész app-ban)
container.register('PrismaClient', () => {
return new PrismaClient();
}, 'singleton');
// Minden resolve ugyanazt az instance-t adja vissza
const prisma1 = container.resolve('PrismaClient');
const prisma2 = container.resolve('PrismaClient');
console.log(prisma1 === prisma2); // true - ugyanaz az objektum!
```
#### Transient (minden resolve új instance)
```javascript
// Transient logger (minden híváshoz új)
container.register('Logger', () => {
return {
id: Math.random(),
log: (msg) => console.log(`[${new Date().toISOString()}] ${msg}`)
};
}, 'transient');
const logger1 = container.resolve('Logger');
const logger2 = container.resolve('Logger');
console.log(logger1 === logger2); // false - különböző objektumok!
console.log(logger1.id !== logger2.id); // true - különböző ID-k
```
#### Scoped (request szinten megosztott)
```javascript
// Scoped RequestContext
container.register('RequestContext', () => {
return {
id: Math.random(),
user: null,
timestamp: Date.now()
};
}, 'scoped');
// Első request scope
const scope1 = container.createScope();
const ctx1a = scope1.resolve('RequestContext');
const ctx1b = scope1.resolve('RequestContext');
console.log(ctx1a === ctx1b); // true - ugyanaz a scope-on belül!
// Második request scope
const scope2 = container.createScope();
const ctx2 = scope2.resolve('RequestContext');
console.log(ctx1a === ctx2); // false - különböző scope-ok!
```
### 🧪 Tesztelés:
```javascript
// tests/unit/application/Container.test.js
const Container = require('../../src/application/Container');
describe('Container', () => {
let container;
beforeEach(() => {
container = new Container();
});
test('singleton - should return same instance', () => {
container.register('TestService', () => ({ id: Math.random() }), 'singleton');
const instance1 = container.resolve('TestService');
const instance2 = container.resolve('TestService');
expect(instance1).toBe(instance2);
expect(instance1.id).toBe(instance2.id);
});
test('transient - should return different instances', () => {
container.register('TestService', () => ({ id: Math.random() }), 'transient');
const instance1 = container.resolve('TestService');
const instance2 = container.resolve('TestService');
expect(instance1).not.toBe(instance2);
expect(instance1.id).not.toBe(instance2.id);
});
test('scoped - should return same instance within scope', () => {
container.register('TestService', () => ({ id: Math.random() }), 'scoped');
const scope = container.createScope();
const instance1 = scope.resolve('TestService');
const instance2 = scope.resolve('TestService');
expect(instance1).toBe(instance2);
expect(instance1.id).toBe(instance2.id);
});
test('scoped - different scopes should have different instances', () => {
container.register('TestService', () => ({ id: Math.random() }), 'scoped');
const scope1 = container.createScope();
const scope2 = container.createScope();
const instance1 = scope1.resolve('TestService');
const instance2 = scope2.resolve('TestService');
expect(instance1).not.toBe(instance2);
});
test('should throw error for unregistered service', () => {
expect(() => container.resolve('NonExistent')).toThrow(
"Service 'NonExistent' is not registered"
);
});
});
```
---
## 2. CORS Middleware Implementálása
### 📁 Fájl: `src/api/middlewares/corsMiddleware.js`
### Teljes megoldás:
```javascript
const cors = require('cors');
// Engedélyezett origin-ek whitelist-je (környezeti változóból)
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [
'http://localhost:3000',
'http://localhost:5173', // Vite default port
'http://localhost:5174',
'http://localhost:4200' // Angular default port
];
/**
* CORS Configuration
* Whitelist-based origin validation
*/
const corsOptions = {
/**
* Origin ellenőrzés
* @param {string} origin - Request origin
* @param {Function} callback - Callback(error, allowed)
*/
origin: function (origin, callback) {
// 1. Ha nincs origin (backend-to-backend, Postman, curl)
// Ezeket általában engedélyezzük development-ben
if (!origin) {
return callback(null, true);
}
// 2. Ha az origin benne van az allowedOrigins listában
if (allowedOrigins.includes(origin)) {
return callback(null, true);
}
// 3. Egyébként tiltjuk CORS hibával
callback(new Error('Not allowed by CORS'));
},
// Cookie és Authorization header engedélyezése
credentials: true,
// Engedélyezett HTTP metódusok
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
// Engedélyezett header-ek
allowedHeaders: ['Content-Type', 'Authorization'],
// Response header-ek amiket a frontend láthat
exposedHeaders: ['Set-Cookie'],
// Preflight cache idő (másodpercben)
maxAge: 600 // 10 perc
};
module.exports = cors(corsOptions);
```
### 💡 Használat:
#### `.env` konfiguráció:
```env
# Development
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173
# Production
ALLOWED_ORIGINS=https://myapp.com,https://www.myapp.com,https://admin.myapp.com
```
#### Aktiválás `server.js`-ben:
```javascript
// src/api/server.js
const corsMiddleware = require('./middlewares/corsMiddleware');
// Middleware chain
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
app.use(corsMiddleware); // <-- CORS middleware hozzáadása
```
### 🧪 Tesztelés cURL-lel:
```bash
# Engedélyezett origin
curl -H "Origin: http://localhost:3000" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type" \
-X OPTIONS \
http://localhost:3000/api/users
# Sikeres válasz:
# Access-Control-Allow-Origin: http://localhost:3000
# Access-Control-Allow-Credentials: true
# Tiltott origin
curl -H "Origin: http://malicious-site.com" \
-X GET \
http://localhost:3000/api/users
# Hiba válasz: "Not allowed by CORS"
```
### 🧪 Frontend tesztelés:
```javascript
// React/Vue/Angular frontend
fetch('http://localhost:3000/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include', // Cookie küldés/fogadás engedélyezése
body: JSON.stringify({ email, password })
})
.then(res => res.json())
.then(data => console.log('Login sikeres:', data))
.catch(err => console.error('CORS hiba:', err));
```
---
## 3. Email Service Implementálása
### 📁 Fájl: `src/application/services/EmailService.js`
### Teljes megoldás:
```javascript
const nodemailer = require('nodemailer');
const handlebars = require('handlebars');
const fs = require('fs');
const path = require('path');
/**
* Email Service
* Nodemailer + Handlebars template based email sending
*/
class EmailService {
constructor() {
// 1. Nodemailer transport létrehozása Ethereal SMTP-vel
this.transporter = nodemailer.createTransport({
host: 'smtp.ethereal.email',
port: 587,
secure: false, // TLS
auth: {
user: process.env.ETHEREAL_USER || 'your-test-email@ethereal.email',
pass: process.env.ETHEREAL_PASS || 'your-test-password'
}
});
console.log('📧 EmailService initialized');
}
/**
* Welcome email küldése új regisztrált felhasználónak
* @param {string} userEmail - Felhasználó email címe
* @param {string} userName - Felhasználó neve
* @returns {Promise<boolean>}
*/
async sendWelcomeEmail(userEmail, userName) {
try {
// 2. Template fájl beolvasása
const templatePath = path.join(__dirname, 'templates', 'welcome.hbs');
const templateSource = fs.readFileSync(templatePath, 'utf-8');
// 3. Handlebars template compile-olása
const template = handlebars.compile(templateSource);
// 4. HTML generálása az adatokkal
const html = template({
name: userName,
email: userEmail,
date: new Date().toLocaleDateString('hu-HU'),
year: new Date().getFullYear()
});
// 5. Email küldése
const info = await this.transporter.sendMail({
from: '"Clean Architecture App" <noreply@cleanarch.com>',
to: userEmail,
subject: '🎉 Üdvözlünk az alkalmazásban!',
html: html
});
// 6. Ethereal preview URL kiírása
const previewUrl = nodemailer.getTestMessageUrl(info);
console.log('📧 Email elküldve:', previewUrl);
return true;
} catch (error) {
console.error('❌ Email küldési hiba:', error.message);
return false;
}
}
/**
* Password reset email (bővítési lehetőség)
*/
async sendPasswordResetEmail(userEmail, resetToken) {
try {
const templatePath = path.join(__dirname, 'templates', 'password-reset.hbs');
const templateSource = fs.readFileSync(templatePath, 'utf-8');
const template = handlebars.compile(templateSource);
const resetLink = `${process.env.FRONTEND_URL}/reset-password?token=${resetToken}`;
const html = template({
resetLink,
expiresIn: '1 óra'
});
const info = await this.transporter.sendMail({
from: '"Clean Architecture App" <noreply@cleanarch.com>',
to: userEmail,
subject: '🔐 Jelszó visszaállítás',
html: html
});
console.log('📧 Password reset email:', nodemailer.getTestMessageUrl(info));
return true;
} catch (error) {
console.error('❌ Password reset email hiba:', error.message);
return false;
}
}
}
module.exports = EmailService;
```
### 📁 Email Template: `src/application/services/templates/welcome.hbs`
```html
<!DOCTYPE html>
<html lang="hu">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Üdvözlünk!</title>
<style>
body {
font-family: 'Segoe UI', Arial, sans-serif;
background-color: #f4f7f9;
margin: 0;
padding: 20px;
}
.container {
background: white;
padding: 40px;
border-radius: 12px;
max-width: 600px;
margin: 0 auto;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
h1 {
color: #2c3e50;
margin-top: 0;
font-size: 28px;
}
.welcome-icon {
font-size: 48px;
text-align: center;
margin: 20px 0;
}
p {
color: #555;
line-height: 1.6;
font-size: 16px;
}
.highlight {
background-color: #e3f2fd;
padding: 15px;
border-left: 4px solid #2196f3;
margin: 20px 0;
border-radius: 4px;
}
.button {
display: inline-block;
background-color: #4caf50;
color: white;
padding: 12px 30px;
text-decoration: none;
border-radius: 6px;
margin: 20px 0;
font-weight: bold;
}
.footer {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #eee;
font-size: 12px;
color: #999;
text-align: center;
}
</style>
</head>
<body>
<div class="container">
<div class="welcome-icon">🎉</div>
<h1>Üdvözlünk, {{name}}!</h1>
<p>Gratulálunk! Sikeres regisztráció az alkalmazásunkban.</p>
<div class="highlight">
<strong>Regisztráció részletei:</strong><br>
📧 Email: {{email}}<br>
📅 Dátum: {{date}}
</div>
<p>Mostantól hozzáférsz az összes funkciónkhoz:</p>
<ul>
<li>✅ Profil kezelés</li>
<li>✅ Biztonságos autentikáció</li>
<li>✅ API hozzáférés</li>
</ul>
<center>
<a href="http://localhost:3000/api/users/me" class="button">
Profilom megtekintése
</a>
</center>
<div class="footer">
<p>Ez egy automatikus üzenet, kérjük ne válaszolj rá.</p>
<p>&copy; {{year}} Clean Architecture App. Minden jog fenntartva.</p>
</div>
</div>
</body>
</html>
```
### 💡 Ethereal Email Beállítása:
1. **Menj a https://ethereal.email oldalra**
2. **Kattints "Create Ethereal Account" gombra**
3. **Másold ki a credentials-t:**
```
Username: your-random-name@ethereal.email
Password: your-random-password
```
4. **Állítsd be a `.env` fájlban:**
```env
ETHEREAL_USER=your-random-name@ethereal.email
ETHEREAL_PASS=your-random-password
```
### 🧪 Tesztelés:
```bash
# Regisztráció (automatikusan küld welcome emailt)
POST http://localhost:3000/api/auth/register
Content-Type: application/json
{
"name": "Test User",
"email": "test@example.com",
"password": "password123"
}
# Console output:
# 📧 Email elküldve: https://ethereal.email/message/XXXXXX
```
**Nyisd meg a böngészőben a linket és látni fogod az emailt!**
---
## 4. Cookie-Based JWT Használata
### 🍪 Frontend Integration (React példa)
```javascript
// authService.js
const API_URL = 'http://localhost:3000/api';
export const authService = {
async register(name, email, password) {
const response = await fetch(`${API_URL}/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include', // FONTOS: Cookie küldés/fogadás
body: JSON.stringify({ name, email, password })
});
if (!response.ok) {
throw new Error('Registration failed');
}
return response.json();
// JWT automatikusan cookie-ban tárolódik!
},
async login(email, password) {
const response = await fetch(`${API_URL}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ email, password })
});
if (!response.ok) {
throw new Error('Login failed');
}
return response.json();
},
async logout() {
const response = await fetch(`${API_URL}/auth/logout`, {
method: 'POST',
credentials: 'include'
});
return response.json();
},
async getCurrentUser() {
const response = await fetch(`${API_URL}/users/me`, {
credentials: 'include' // Cookie automatikusan küldődik
});
if (!response.ok) {
throw new Error('Unauthorized');
}
return response.json();
}
};
```
### 🧪 cURL Tesztelés Cookie-val:
```bash
# 1. Login + Cookie mentése
curl -c cookies.txt -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"password123"}'
# 2. Protected endpoint hívása a cookie-val
curl -b cookies.txt http://localhost:3000/api/users/me
# 3. Logout
curl -b cookies.txt -c cookies.txt -X POST http://localhost:3000/api/auth/logout
```
---
## 5. Tesztelési Példák
### Controller Teszt Cookie-val:
```javascript
// tests/unit/controllers/AuthController.test.js
const AuthController = require('../../src/api/controllers/AuthController');
describe('AuthController - Cookie-based JWT', () => {
let authController;
let mockRegisterHandler;
let mockLoginHandler;
let mockJwtService;
beforeEach(() => {
mockRegisterHandler = { handle: jest.fn() };
mockLoginHandler = { handle: jest.fn() };
mockJwtService = {
getCookieName: jest.fn().mockReturnValue('auth_token'),
getCookieOptions: jest.fn().mockReturnValue({
httpOnly: true,
secure: false,
sameSite: 'strict'
})
};
authController = new AuthController(
mockRegisterHandler,
mockLoginHandler,
mockJwtService
);
});
test('login should set JWT in cookie', async () => {
const mockReq = {
body: { email: 'test@example.com', password: 'password123' }
};
const mockRes = {
cookie: jest.fn(),
status: jest.fn().mockReturnThis(),
json: jest.fn()
};
mockLoginHandler.handle.mockResolvedValue({
user: { id: 1, email: 'test@example.com' },
token: 'mock_jwt_token'
});
await authController.login(mockReq, mockRes);
// Cookie beállítás ellenőrzése
expect(mockRes.cookie).toHaveBeenCalledWith(
'auth_token',
'mock_jwt_token',
expect.objectContaining({
httpOnly: true,
sameSite: 'strict'
})
);
// Response csak user-t tartalmaz, token nincs a body-ban
expect(mockRes.json).toHaveBeenCalledWith({
message: 'Login successful',
data: {
user: { id: 1, email: 'test@example.com' }
}
});
});
});
```
---
## 6. Gyakori Hibák és Megoldások
### ❌ Hiba: "Service 'PrismaClient' is not registered"
**Megoldás:**
```javascript
// Ellenőrizd a server.js-ben:
container.register('PrismaClient', () => {
return databaseConnection.getClient();
}, 'singleton');
```
### ❌ Hiba: "Not allowed by CORS"
**Megoldás:**
```env
# .env fájlban add meg a frontend origin-t:
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173
```
### ❌ Hiba: "No token provided in cookies"
**Megoldás:**
```javascript
// Frontend-en használd a credentials: 'include'-ot:
fetch('http://localhost:3000/api/users/me', {
credentials: 'include'
});
```
### ❌ Hiba: Email nem megy ki
**Megoldás:**
1. Ellenőrizd az Ethereal credentials-t (`.env`)
2. Hozz létre új Ethereal account-ot: https://ethereal.email
3. Ellenőrizd a template fájl elérési útját:
```javascript
const templatePath = path.join(__dirname, 'templates', 'welcome.hbs');
```
### ❌ Hiba: Container circular dependency
**Megoldás:**
```javascript
// Használj lazy loading-ot:
container.register('ServiceA', () => {
const ServiceB = container.resolve('ServiceB');
return new ServiceA(ServiceB);
}, 'singleton');
```
---
## 📚 További Olvasnivalók
- [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
- [Dependency Injection Pattern](https://martinfowler.com/articles/injection.html)
- [CORS in Express](https://expressjs.com/en/resources/middleware/cors.html)
- [Nodemailer Documentation](https://nodemailer.com/about/)
- [Handlebars Templates](https://handlebarsjs.com/)
- [Cookie Security Best Practices](https://owasp.org/www-community/controls/SecureCookieAttribute)
---
**Sok sikert a feladatokhoz! 🚀**