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

21 KiB

📚 SEGÍTSÉG - Példakódok és Magyarázatok

Tartalomjegyzék

  1. DI Container Implementálása
  2. CORS Middleware Implementálása
  3. Email Service Implementálása
  4. Cookie-Based JWT Használata
  5. Tesztelési Példák
  6. Gyakori Hibák és Megoldások

1. DI Container Implementálása

📁 Fájl: src/application/Container.js

Teljes megoldás magyarázattal:

/**
 * 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)

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)

// 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)

// 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:

// 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:

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ó:

# 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:

// 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:

# 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:

// 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:

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

<!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
  1. Állítsd be a .env fájlban:
ETHEREAL_USER=your-random-name@ethereal.email
ETHEREAL_PASS=your-random-password

🧪 Tesztelés:

# 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!


🍪 Frontend Integration (React példa)

// 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();
  }
};
# 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

// 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:

// Ellenőrizd a server.js-ben:
container.register('PrismaClient', () => {
  return databaseConnection.getClient();
}, 'singleton');

Hiba: "Not allowed by CORS"

Megoldás:

# .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:

// 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:
    const templatePath = path.join(__dirname, 'templates', 'welcome.hbs');
    

Hiba: Container circular dependency

Megoldás:

// Használj lazy loading-ot:
container.register('ServiceA', () => {
  const ServiceB = container.resolve('ServiceB');
  return new ServiceA(ServiceB);
}, 'singleton');

📚 További Olvasnivalók


Sok sikert a feladatokhoz! 🚀