# 📚 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} */ 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" ', 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" ', 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 Üdvözlünk!
🎉

Üdvözlünk, {{name}}!

Gratulálunk! Sikeres regisztráció az alkalmazásunkban.

Regisztráció részletei:
📧 Email: {{email}}
📅 Dátum: {{date}}

Mostantól hozzáférsz az összes funkciónkhoz:

  • ✅ Profil kezelés
  • ✅ Biztonságos autentikáció
  • ✅ API hozzáférés
Profilom megtekintése
``` ### 💡 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! 🚀**