negyedik gyakorlat + megoldasok

This commit is contained in:
magdo
2026-03-04 20:02:39 +01:00
parent afc3777ac9
commit 388aa908de
217 changed files with 19791 additions and 0 deletions
@@ -0,0 +1,15 @@
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/cors_di_app?schema=public"
JWT_SECRET="titkos-kulcs-jwt-hez-változtasd-meg"
JWT_EXPIRES_IN="7d"
PORT=3000
NODE_ENV=development
# Ethereal Email (Regisztrálj itt: https://ethereal.email/)
ETHEREAL_USER=your-ethereal-user@ethereal.email
ETHEREAL_PASS=your-ethereal-password
# CORS (comma-separated origins)
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173
# App URL (email linkekhez)
APP_URL=http://localhost:3000
@@ -0,0 +1,20 @@
# Dependencies
node_modules/
# Environment
.env
# Logs
*.log
npm-debug.log*
# OS
.DS_Store
Thumbs.db
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
@@ -0,0 +1,98 @@
# Negyedik Gyakorlat - MINTA Megoldás
Ez a mappa tartalmazza a negyedik gyakorlat **teljes, működő megoldását**.
## Amit implementáltunk:
### 1. **Dependency Injection Container**
- Singleton lifecycle - egy példány az egész alkalmazásban
- Transient lifecycle - minden híváskor új példány
- Scoped lifecycle - request szintű scope
- Service regisztráció és feloldás
- Circular dependency kezelés
### 2. **CORS Konfigurás**
- Whitelist-based origin ellenőrzés
- Credentials support (cookie-k)
- Preflight request kezelés
- Custom CORS middleware
### 3. **Email Service (Nodemailer + Handlebars)**
- Ethereal test SMTP
- HTML email templatek (Handlebars)
- Welcome email új usereknek
- Password reset email
- Template rendering
### 4. **Cookie-based JWT Auth**
- Tokenek csak cookie-ban (nem response body-ban!)
- HttpOnly, Secure, SameSite cookie-k
- Automatikus token ellenőrzés middleware-ből
## Indítás
```bash
npm install
npm run docker:up
npx prisma generate
npx prisma migrate dev
npm run dev
```
## Környezeti Változók
```env
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/cors_di_app?schema=public"
JWT_SECRET="your-secret-key"
JWT_EXPIRES_IN="7d"
PORT=3000
NODE_ENV=development
# Ethereal Email
ETHEREAL_USER=your-ethereal-user@ethereal.email
ETHEREAL_PASS=your-ethereal-password
# CORS
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173
```
## DI Container Példa
```javascript
// Service regisztráció
container.register('database', () => new DatabaseConnection(), 'singleton');
container.register('userRepo', () => new UserRepository(), 'singleton');
container.register('emailService', () => new EmailService(), 'singleton');
container.register('requestId', () => crypto.randomUUID(), 'transient');
container.register('jwtService', () => new JwtService(), 'scoped');
// Service feloldás
const userRepo = container.resolve('userRepo');
const emailService = container.resolve('emailService');
// Scoped resolution (request szintű)
const scope = container.createScope();
const jwtService = scope.resolve('jwtService');
```
## Email Service Példa
```javascript
// Welcome email küldése
await emailService.sendWelcomeEmail(user.email, user.name);
// Password reset email
await emailService.sendPasswordResetEmail(user.email, resetToken);
```
## CORS Engedélyezett Eredethelyekről
Csak az `ALLOWED_ORIGINS` környezeti változóban megadott origin-ek férhetnek hozzá az API-hoz.
## Tanulási Pontok
1. **Dependency Injection**: Service lifecycle management
2. **CORS**: Biztonságos cross-origin konfigurás
3. **Email Templates**: Handlebars template rendering
4. **Nodemailer**: Email küldés SMTP-vel
5. **Cookie-based Auth**: Token kezelés csak cookie-kban
@@ -0,0 +1,25 @@
version: '3.8'
services:
# PostgreSQL Database
postgres:
image: postgres:15-alpine
container_name: cors-di-email-postgres-minta
restart: unless-stopped
environment:
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
POSTGRES_DB: ${DB_NAME:-cors_di_app_minta}
ports:
- "${DB_PORT:-5433}:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data:
driver: local
@@ -0,0 +1,16 @@
module.exports = {
testEnvironment: 'node',
coverageDirectory: 'coverage',
collectCoverageFrom: [
'src/**/*.js',
'!src/api/server.js',
'!src/**/index.js'
],
testMatch: [
'**/tests/**/*.test.js'
],
verbose: true,
clearMocks: true,
resetMocks: true,
restoreMocks: true
};
@@ -0,0 +1,40 @@
{
"name": "cors-di-email-minta",
"version": "1.0.0",
"description": "MINTA - CORS + DI Container + Email Notifications",
"main": "src/api/server.js",
"type": "module",
"scripts": {
"dev": "nodemon src/api/server.js",
"start": "node src/api/server.js",
"test": "jest --coverage",
"test:watch": "jest --watch",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev",
"prisma:studio": "prisma studio",
"prisma:seed": "node prisma/seed.js",
"docker:up": "docker-compose up -d",
"docker:down": "docker-compose down",
"docker:logs": "docker-compose logs -f"
},
"keywords": ["cors", "di", "dependency-injection", "email", "nodemailer"],
"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",
"handlebars": "^4.7.8",
"jsonwebtoken": "^9.0.2",
"nodemailer": "^6.9.8"
},
"devDependencies": {
"@types/jest": "^29.5.11",
"jest": "^29.7.0",
"nodemon": "^3.0.3",
"prisma": "^5.9.1",
"supertest": "^6.3.4"
}
}
@@ -0,0 +1,21 @@
// Prisma Schema - PostgreSQL Database
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// User Model
model User {
id Int @id @default(autoincrement())
name String
email String @unique
password String // bcrypt hashed password
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("users")
}
@@ -0,0 +1,83 @@
export class AuthController {
constructor(registerHandler, loginHandler) {
this.registerHandler = registerHandler;
this.loginHandler = loginHandler;
}
async register(req, res, next) {
try {
const { name, email, password } = req.body;
if (!name || !email || !password) {
return res.status(400).json({
success: false,
error: 'Name, email and password are required'
});
}
const { user, token } = await this.registerHandler.handle({
name,
email,
password
});
// Set cookie
res.cookie('token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
});
res.status(201).json({
success: true,
message: 'User registered successfully',
data: user.toJSON()
});
} catch (error) {
next(error);
}
}
async login(req, res, next) {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({
success: false,
error: 'Email and password are required'
});
}
const { user, token } = await this.loginHandler.handle({
email,
password
});
// Set cookie
res.cookie('token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
});
res.json({
success: true,
message: 'Login successful',
data: user.toJSON()
});
} catch (error) {
next(error);
}
}
async logout(req, res) {
res.clearCookie('token');
res.json({
success: true,
message: 'Logged out successfully'
});
}
}
@@ -0,0 +1,62 @@
export class UserController {
constructor(getMeHandler, getAllUsersHandler, getUserByIdHandler, updateProfileHandler) {
this.getMeHandler = getMeHandler;
this.getAllUsersHandler = getAllUsersHandler;
this.getUserByIdHandler = getUserByIdHandler;
this.updateProfileHandler = updateProfileHandler;
}
async getMe(req, res, next) {
try {
const user = await this.getMeHandler.handle({ userId: req.user.id });
res.json({
success: true,
data: user.toJSON()
});
} catch (error) {
next(error);
}
}
async getAllUsers(req, res, next) {
try {
const users = await this.getAllUsersHandler.handle({});
res.json({
success: true,
data: users.map(u => u.toJSON())
});
} catch (error) {
next(error);
}
}
async getUserById(req, res, next) {
try {
const user = await this.getUserByIdHandler.handle({ userId: req.params.id });
res.json({
success: true,
data: user.toJSON()
});
} catch (error) {
next(error);
}
}
async updateProfile(req, res, next) {
try {
const { name, email } = req.body;
const user = await this.updateProfileHandler.handle({
userId: req.user.id,
name,
email
});
res.json({
success: true,
message: 'Profile updated successfully',
data: user.toJSON()
});
} catch (error) {
next(error);
}
}
}
@@ -0,0 +1,23 @@
export function authMiddleware(jwtService) {
return async (req, res, next) => {
try {
const token = req.cookies.token;
if (!token) {
return res.status(401).json({
success: false,
error: 'Authentication required'
});
}
const decoded = jwtService.verifyToken(token);
req.user = decoded;
next();
} catch (error) {
return res.status(401).json({
success: false,
error: 'Invalid or expired token'
});
}
};
}
@@ -0,0 +1,58 @@
/**
* CORS Middleware - Whitelist alapú origin ellenőrzéssel
*
* Csak az engedélyezett origin-ekről érkező kéréseket fogadjuk el.
*/
const allowedOrigins = (process.env.ALLOWED_ORIGINS || 'http://localhost:3000')
.split(',')
.map(origin => origin.trim());
console.log('🔒 CORS allowed origins:', allowedOrigins);
export function corsMiddleware(req, res, next) {
const origin = req.headers.origin;
// Ellenőrizzük hogy az origin engedélyezett-e
if (origin && allowedOrigins.includes(origin)) {
// Engedélyezett origin
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader(
'Access-Control-Allow-Methods',
'GET, POST, PUT, DELETE, PATCH, OPTIONS'
);
res.setHeader(
'Access-Control-Allow-Headers',
'Content-Type, Authorization, Cookie'
);
res.setHeader(
'Access-Control-Expose-Headers',
'Set-Cookie'
);
}
// Preflight request kezelése (OPTIONS)
if (req.method === 'OPTIONS') {
return res.status(204).end();
}
next();
}
/**
* Strict CORS check - ha az origin nem engedélyezett, visszautasítjuk
*/
export function strictCorsCheck(req, res, next) {
const origin = req.headers.origin;
// Ha van origin header de nincs az engedélyezett listában
if (origin && !allowedOrigins.includes(origin)) {
return res.status(403).json({
success: false,
error: 'CORS policy: Origin not allowed'
});
}
next();
}
@@ -0,0 +1,12 @@
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 })
});
}
@@ -0,0 +1,14 @@
// Scoped middleware - creates a new DI scope for each request
export function scopedMiddleware(container) {
return (req, res, next) => {
// Create request-scoped container
req.scope = container.createScope();
// Clean up scope after response
res.on('finish', () => {
req.scope.dispose();
});
next();
};
}
@@ -0,0 +1,12 @@
import express from 'express';
export function createAuthRoutes(container) {
const router = express.Router();
const authController = container.resolve('AuthController');
router.post('/register', (req, res, next) => authController.register(req, res, next));
router.post('/login', (req, res, next) => authController.login(req, res, next));
router.post('/logout', (req, res, next) => authController.logout(req, res, next));
return router;
}
@@ -0,0 +1,16 @@
import express from 'express';
import { authMiddleware } from '../middlewares/authMiddleware.js';
export function createUserRoutes(container) {
const router = express.Router();
const userController = container.resolve('UserController');
const jwtService = container.resolve('JwtService');
// Protected routes
router.get('/me', authMiddleware(jwtService), (req, res, next) => userController.getMe(req, res, next));
router.get('/', authMiddleware(jwtService), (req, res, next) => userController.getAllUsers(req, res, next));
router.get('/:id', authMiddleware(jwtService), (req, res, next) => userController.getUserById(req, res, next));
router.put('/profile', authMiddleware(jwtService), (req, res, next) => userController.updateProfile(req, res, next));
return router;
}
@@ -0,0 +1,145 @@
import express from 'express';
import cookieParser from 'cookie-parser';
import dotenv from 'dotenv';
import { Container } from '../application/services/Container.js';
import DatabaseConnection from '../infrastructure/db/DatabaseConnection.js';
import { corsMiddleware } from './middlewares/corsMiddleware.js';
import { errorHandler } from './middlewares/errorHandler.js';
import { scopedMiddleware } from './middlewares/scopedMiddleware.js';
import { createAuthRoutes } from './routers/authRoutes.js';
import { createUserRoutes } from './routers/userRoutes.js';
// Services
import { JwtService } from '../application/services/JwtService.js';
import { EmailService } from '../application/services/EmailService.js';
// Repositories
import { UserRepository } from '../infrastructure/repositories/UserRepository.js';
// Auth Handlers
import { RegisterUserCommandHandler } from '../application/auth/commands/RegisterUserCommandHandler.js';
import { LoginUserCommandHandler } from '../application/auth/commands/LoginUserCommandHandler.js';
// User Handlers
import { GetMeQueryHandler } from '../application/user/queries/GetMeQueryHandler.js';
import { GetAllUsersQueryHandler } from '../application/user/queries/GetAllUsersQueryHandler.js';
import { GetUserByIdQueryHandler } from '../application/user/queries/GetUserByIdQueryHandler.js';
import { UpdateUserProfileCommandHandler } from '../application/user/commands/UpdateUserProfileCommandHandler.js';
// Controllers
import { AuthController } from './controllers/AuthController.js';
import { UserController } from './controllers/UserController.js';
dotenv.config();
const app = express();
const PORT = process.env.PORT || 3002;
// ===== DI Container Setup =====
const container = new Container();
// Register Singleton Services (shared across all requests)
container.registerSingleton('JwtService', () => new JwtService());
container.registerSingleton('EmailService', () => new EmailService());
container.registerSingleton('UserRepository', () => new UserRepository());
// Register Transient Handlers (new instance per resolve)
container.registerTransient('RegisterUserCommandHandler', (c) =>
new RegisterUserCommandHandler(
c.resolve('UserRepository'),
c.resolve('JwtService'),
c.resolve('EmailService')
)
);
container.registerTransient('LoginUserCommandHandler', (c) =>
new LoginUserCommandHandler(
c.resolve('UserRepository'),
c.resolve('JwtService')
)
);
container.registerTransient('GetMeQueryHandler', (c) =>
new GetMeQueryHandler(c.resolve('UserRepository'))
);
container.registerTransient('GetAllUsersQueryHandler', (c) =>
new GetAllUsersQueryHandler(c.resolve('UserRepository'))
);
container.registerTransient('GetUserByIdQueryHandler', (c) =>
new GetUserByIdQueryHandler(c.resolve('UserRepository'))
);
container.registerTransient('UpdateUserProfileCommandHandler', (c) =>
new UpdateUserProfileCommandHandler(c.resolve('UserRepository'))
);
// Register Controllers
container.registerTransient('AuthController', (c) =>
new AuthController(
c.resolve('RegisterUserCommandHandler'),
c.resolve('LoginUserCommandHandler')
)
);
container.registerTransient('UserController', (c) =>
new UserController(
c.resolve('GetMeQueryHandler'),
c.resolve('GetAllUsersQueryHandler'),
c.resolve('GetUserByIdQueryHandler'),
c.resolve('UpdateUserProfileCommandHandler')
)
);
// ===== Middleware =====
app.use(corsMiddleware()); // CORS with whitelist
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
app.use(scopedMiddleware(container)); // Request-scoped DI
// ===== Database Connection =====
await DatabaseConnection.connect();
// ===== Health Check =====
app.get('/health', (req, res) => {
res.json({
status: 'OK',
timestamp: new Date().toISOString()
});
});
// ===== Routes =====
app.use('/api/auth', createAuthRoutes(container));
app.use('/api/users', createUserRoutes(container));
// ===== Error Handling =====
app.use((req, res) => {
res.status(404).json({
success: false,
error: 'Endpoint not found'
});
});
app.use(errorHandler);
// ===== Graceful Shutdown =====
process.on('SIGINT', async () => {
console.log('\n🛑 Shutting down gracefully...');
await DatabaseConnection.disconnect();
process.exit(0);
});
process.on('SIGTERM', async () => {
console.log('\n🛑 Shutting down gracefully...');
await DatabaseConnection.disconnect();
process.exit(0);
});
// ===== Start Server =====
app.listen(PORT, () => {
console.log(`✅ Server running on http://localhost:${PORT}`);
console.log(`📊 Environment: ${process.env.NODE_ENV}`);
console.log(`🔒 CORS enabled for: ${process.env.ALLOWED_ORIGINS}`);
});
@@ -0,0 +1,6 @@
export class LoginUserCommand {
constructor(email, password) {
this.email = email;
this.password = password;
}
}
@@ -0,0 +1,31 @@
import bcrypt from 'bcryptjs';
export class LoginUserCommandHandler {
constructor(userRepository, jwtService) {
this.userRepository = userRepository;
this.jwtService = jwtService;
}
async handle(command) {
// Find user by email
const user = await this.userRepository.findByEmail(command.email);
if (!user) {
throw new Error('Invalid credentials');
}
// Verify password
const isValidPassword = await bcrypt.compare(command.password, user.password);
if (!isValidPassword) {
throw new Error('Invalid credentials');
}
// Generate token
const token = this.jwtService.generateToken({
id: user.id,
email: user.email,
name: user.name
});
return { user, token };
}
}
@@ -0,0 +1,7 @@
export class RegisterUserCommand {
constructor(name, email, password) {
this.name = name;
this.email = email;
this.password = password;
}
}
@@ -0,0 +1,51 @@
import bcrypt from 'bcryptjs';
import { User } from '../../../domain/models/User.js';
export class RegisterUserCommandHandler {
constructor(userRepository, jwtService, emailService) {
this.userRepository = userRepository;
this.jwtService = jwtService;
this.emailService = emailService;
}
async handle(command) {
// Validasi data
const errors = User.validate({
name: command.name,
email: command.email,
password: command.password
});
if (errors.length > 0) {
throw new Error(errors.join(', '));
}
// Verify user does not exist
const existingUser = await this.userRepository.findByEmail(command.email);
if (existingUser) {
throw new Error('User with this email already exists');
}
// Hash password
const hashedPassword = await bcrypt.hash(command.password, 10);
// Create user
const user = await this.userRepository.create({
name: command.name,
email: command.email,
password: hashedPassword
});
// Send welcome email
await this.emailService.sendWelcomeEmail(user.email, user.name);
// Generate token
const token = this.jwtService.generateToken({
id: user.id,
email: user.email,
name: user.name
});
return { user, token };
}
}
@@ -0,0 +1,126 @@
/**
* Dependency Injection Container
*
* Supports three lifecycle types:
* - singleton: One instance for the entire application
* - transient: New instance every time
* - scoped: One instance per scope (e.g., per request)
*/
class Container {
constructor() {
this.services = new Map(); // Singleton instances tárolása
this.factories = new Map(); // Factory függvények tárolása
this.lifetimes = new Map(); // Lifecycle típusok tárolása
}
/**
* Service regisztráció
*
* @param {string} name - Service neve
* @param {Function} factory - Factory függvény ami az instance-t létrehozza
* @param {string} lifetime - 'singleton' | 'transient' | 'scoped'
*/
register(name, factory, lifetime = 'singleton') {
// Factory és lifetime tárolása
this.factories.set(name, factory);
this.lifetimes.set(name, lifetime);
// Ha singleton, azonnal példányosítjuk
if (lifetime === 'singleton') {
const instance = factory();
this.services.set(name, instance);
}
}
/**
* Service feloldás
*
* @param {string} name - Service neve
* @param {Map} scope - Scoped instance-ok tárolója (opcionális)
* @returns {*} Service instance
*/
resolve(name, scope = null) {
// Ellenőrizzük hogy regisztrálva van-e
if (!this.factories.has(name)) {
throw new Error(`Service '${name}' is not registered`);
}
const lifetime = this.lifetimes.get(name);
// SCOPED: Scope-ból vagy új instance
if (lifetime === 'scoped' && scope) {
if (scope.has(name)) {
return scope.get(name);
}
const instance = this.factories.get(name)();
scope.set(name, instance);
return instance;
}
// SINGLETON: Visszaadjuk a tárolt instance-t
if (lifetime === 'singleton') {
return this.services.get(name);
}
// TRANSIENT: Mindig új instance
if (lifetime === 'transient') {
return this.factories.get(name)();
}
// Default: singleton behavior
return this.services.get(name);
}
/**
* Új scope létrehozása
*
* @returns {Object} Scope objektum resolve metódussal
*/
createScope() {
const scopeMap = new Map();
return {
resolve: (name) => this.resolve(name, scopeMap),
dispose: () => {
// Scope-beli instance-ok felszabadítása
scopeMap.clear();
}
};
}
/**
* Ellenőrzi hogy egy service regisztrálva van-e
*
* @param {string} name - Service neve
* @returns {boolean}
*/
has(name) {
return this.factories.has(name);
}
/**
* Service törlése a containerből
*
* @param {string} name - Service neve
*/
unregister(name) {
this.services.delete(name);
this.factories.delete(name);
this.lifetimes.delete(name);
}
/**
* Az összes service listázása
*
* @returns {Array<{name: string, lifetime: string}>}
*/
list() {
const services = [];
for (const [name, lifetime] of this.lifetimes.entries()) {
services.push({ name, lifetime });
}
return services;
}
}
export default Container;
@@ -0,0 +1,158 @@
import nodemailer from 'nodemailer';
import handlebars from 'handlebars';
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/**
* Email Service
* Nodemailer + Handlebars template alapú email küldés
*/
class EmailService {
constructor() {
// Nodemailer transporter létrehozása (Ethereal test SMTP)
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 with Ethereal SMTP');
}
/**
* Template betöltése és renderelése
*
* @param {string} templateName - Template fájl neve (.hbs nélkül)
* @param {Object} data - Template adatok
* @returns {Promise<string>} Renderelt HTML
*/
async renderTemplate(templateName, data) {
try {
// Template fájl beolvasása
const templatePath = path.join(__dirname, '..', '..', 'email-templates', `${templateName}.hbs`);
const templateContent = await fs.readFile(templatePath, 'utf-8');
// Handlebars compile és render
const template = handlebars.compile(templateContent);
return template(data);
} catch (error) {
console.error(`Template render error (${templateName}):`, error);
throw new Error(`Failed to render email template: ${templateName}`);
}
}
/**
* Email küldés
*
* @param {Object} options - { to, subject, html, text }
* @returns {Promise<Object>} Nodemailer info objektum
*/
async sendEmail(options) {
try {
const mailOptions = {
from: '"Your App" <noreply@yourapp.com>',
to: options.to,
subject: options.subject,
html: options.html,
text: options.text || ''
};
const info = await this.transporter.sendMail(mailOptions);
console.log('📧 Email sent:', {
messageId: info.messageId,
to: options.to,
subject: options.subject,
previewURL: nodemailer.getTestMessageUrl(info)
});
return info;
} catch (error) {
console.error('Email send error:', error);
throw new Error('Failed to send email');
}
}
/**
* Welcome email küldése új user számára
*
* @param {string} userEmail - User email címe
* @param {string} userName - User neve
* @returns {Promise<Object>}
*/
async sendWelcomeEmail(userEmail, userName) {
try {
const html = await this.renderTemplate('welcome', {
userName,
appName: 'Your App',
loginUrl: `${process.env.APP_URL || 'http://localhost:3000'}/login`
});
return await this.sendEmail({
to: userEmail,
subject: 'Welcome to Your App! 🎉',
html,
text: `Welcome ${userName}! Thank you for registering.`
});
} catch (error) {
console.error('Welcome email error:', error);
// Ne dobjunk hibát, csak loggoljuk
// Az email küldés hibája ne akadályozza meg a regisztrációt
return null;
}
}
/**
* Password reset email
*
* @param {string} userEmail - User email címe
* @param {string} resetToken - Password reset token
* @returns {Promise<Object>}
*/
async sendPasswordResetEmail(userEmail, resetToken) {
try {
const resetUrl = `${process.env.APP_URL || 'http://localhost:3000'}/reset-password?token=${resetToken}`;
const html = await this.renderTemplate('password-reset', {
resetUrl,
expiryHours: 1
});
return await this.sendEmail({
to: userEmail,
subject: 'Password Reset Request 🔒',
html,
text: `Click the following link to reset your password: ${resetUrl}`
});
} catch (error) {
console.error('Password reset email error:', error);
return null;
}
}
/**
* Tesztelés: Email szolgáltatás ellenőrzése
*
* @returns {Promise<boolean>}
*/
async verify() {
try {
await this.transporter.verify();
console.log('✅ Email service is ready');
return true;
} catch (error) {
console.error('❌ Email service verification failed:', error);
return false;
}
}
}
export default EmailService;
@@ -0,0 +1,33 @@
import jwt from 'jsonwebtoken';
export class JwtService {
constructor() {
this.secret = process.env.JWT_SECRET || 'default-secret-key';
this.expiresIn = process.env.JWT_EXPIRES_IN || '7d';
}
generateToken(payload) {
return jwt.sign(payload, this.secret, { expiresIn: this.expiresIn });
}
verifyToken(token) {
try {
return jwt.verify(token, this.secret);
} catch (error) {
throw new Error('Invalid or expired token');
}
}
setTokenCookie(res, token) {
res.cookie('token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
});
}
clearTokenCookie(res) {
res.clearCookie('token');
}
}
@@ -0,0 +1,7 @@
export class UpdateUserProfileCommand {
constructor(userId, name, email) {
this.userId = userId;
this.name = name;
this.email = email;
}
}
@@ -0,0 +1,26 @@
export class UpdateUserProfileCommandHandler {
constructor(userRepository) {
this.userRepository = userRepository;
}
async handle(command) {
const user = await this.userRepository.findById(command.userId);
if (!user) {
throw new Error('User not found');
}
const updateData = {};
if (command.name && command.name.trim()) updateData.name = command.name.trim();
if (command.email && command.email.trim()) updateData.email = command.email.trim();
// Check if email is already taken by another user
if (updateData.email && updateData.email !== user.email) {
const existingUser = await this.userRepository.findByEmail(updateData.email);
if (existingUser) {
throw new Error('Email is already taken');
}
}
return await this.userRepository.update(command.userId, updateData);
}
}
@@ -0,0 +1,2 @@
export class GetAllUsersQuery {
}
@@ -0,0 +1,9 @@
export class GetAllUsersQueryHandler {
constructor(userRepository) {
this.userRepository = userRepository;
}
async handle(query) {
return await this.userRepository.findAll();
}
}
@@ -0,0 +1,5 @@
export class GetMeQuery {
constructor(userId) {
this.userId = userId;
}
}
@@ -0,0 +1,13 @@
export class GetMeQueryHandler {
constructor(userRepository) {
this.userRepository = userRepository;
}
async handle(query) {
const user = await this.userRepository.findById(query.userId);
if (!user) {
throw new Error('User not found');
}
return user;
}
}
@@ -0,0 +1,5 @@
export class GetUserByIdQuery {
constructor(userId) {
this.userId = userId;
}
}
@@ -0,0 +1,13 @@
export class GetUserByIdQueryHandler {
constructor(userRepository) {
this.userRepository = userRepository;
}
async handle(query) {
const user = await this.userRepository.findById(query.userId);
if (!user) {
throw new Error('User not found');
}
return user;
}
}
@@ -0,0 +1,25 @@
export class IUserRepository {
async findById(id) {
throw new Error('findById() must be implemented');
}
async findByEmail(email) {
throw new Error('findByEmail() must be implemented');
}
async findAll() {
throw new Error('findAll() must be implemented');
}
async create(userData) {
throw new Error('create() must be implemented');
}
async update(id, userData) {
throw new Error('update() must be implemented');
}
async delete(id) {
throw new Error('delete() must be implemented');
}
}
@@ -0,0 +1,40 @@
export class User {
constructor(data) {
this.id = data.id;
this.name = data.name;
this.email = data.email;
this.password = data.password;
this.createdAt = data.createdAt;
this.updatedAt = data.updatedAt;
}
// Remove password from JSON serialization
toJSON() {
const { password, ...userWithoutPassword } = this;
return userWithoutPassword;
}
// Validate user data
static validate(data) {
const errors = [];
if (!data.name || data.name.trim().length < 2) {
errors.push('Name must be at least 2 characters');
}
if (!data.email || !this.isValidEmail(data.email)) {
errors.push('Valid email is required');
}
if (!data.password || data.password.length < 6) {
errors.push('Password must be at least 6 characters');
}
return errors;
}
static isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
}
@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
padding: 30px;
text-align: center;
border-radius: 10px 10px 0 0;
}
.content {
background: #f9f9f9;
padding: 30px;
border-radius: 0 0 10px 10px;
}
.button {
display: inline-block;
padding: 12px 30px;
background: #f5576c;
color: white;
text-decoration: none;
border-radius: 5px;
margin: 20px 0;
}
.warning {
background: #fff3cd;
border-left: 4px solid #ff9800;
padding: 15px;
margin: 20px 0;
}
.footer {
text-align: center;
margin-top: 30px;
font-size: 12px;
color: #999;
}
</style>
</head>
<body>
<div class="header">
<h1>Password Reset Request 🔒</h1>
</div>
<div class="content">
<h2>Reset Your Password</h2>
<p>We received a request to reset your password. Click the button below to create a new password:</p>
<p style="text-align: center;">
<a href="{{resetUrl}}" class="button">Reset Password</a>
</p>
<div class="warning">
<p><strong>Important:</strong></p>
<ul>
<li>This link will expire in <strong>{{expiryHours}} hour(s)</strong></li>
<li>If you didn't request this, please ignore this email</li>
<li>Never share this link with anyone</li>
</ul>
</div>
<p>If the button doesn't work, copy and paste this link into your browser:</p>
<p style="word-break: break-all; color: #667eea;">{{resetUrl}}</p>
<p>Best regards,<br>The Security Team</p>
</div>
<div class="footer">
<p>This is an automated security message, please do not reply.</p>
</div>
</body>
</html>
@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
border-radius: 10px 10px 0 0;
}
.content {
background: #f9f9f9;
padding: 30px;
border-radius: 0 0 10px 10px;
}
.button {
display: inline-block;
padding: 12px 30px;
background: #667eea;
color: white;
text-decoration: none;
border-radius: 5px;
margin: 20px 0;
}
.footer {
text-align: center;
margin-top: 30px;
font-size: 12px;
color: #999;
}
</style>
</head>
<body>
<div class="header">
<h1>Welcome to {{appName}}! 🎉</h1>
</div>
<div class="content">
<h2>Hello {{userName}}!</h2>
<p>Thank you for registering with {{appName}}. We're excited to have you on board!</p>
<p>Your account has been successfully created. You can now log in and start using our services.</p>
<p style="text-align: center;">
<a href="{{loginUrl}}" class="button">Go to Login</a>
</p>
<p>If you have any questions or need assistance, feel free to reach out to our support team.</p>
<p>Best regards,<br>The {{appName}} Team</p>
</div>
<div class="footer">
<p>This is an automated message, please do not reply.</p>
</div>
</body>
</html>
@@ -0,0 +1,30 @@
import { PrismaClient } from '@prisma/client';
class DatabaseConnection {
constructor() {
this.prisma = new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
});
}
async connect() {
try {
await this.prisma.$connect();
console.log('✅ Database connected successfully');
} catch (error) {
console.error('❌ Database connection failed:', error);
throw error;
}
}
async disconnect() {
await this.prisma.$disconnect();
console.log('🔌 Database disconnected');
}
getClient() {
return this.prisma;
}
}
export default new DatabaseConnection();
@@ -0,0 +1,53 @@
import { IUserRepository } from '../../domain/irepositories/IUserRepository.js';
import { User } from '../../domain/models/User.js';
import DatabaseConnection from '../db/DatabaseConnection.js';
export class UserRepository extends IUserRepository {
constructor() {
super();
this.prisma = DatabaseConnection.getClient();
}
async findById(id) {
const user = await this.prisma.user.findUnique({
where: { id: parseInt(id) }
});
return user ? new User(user) : null;
}
async findByEmail(email) {
const user = await this.prisma.user.findUnique({
where: { email }
});
return user ? new User(user) : null;
}
async findAll() {
const users = await this.prisma.user.findMany({
orderBy: { createdAt: 'desc' }
});
return users.map(user => new User(user));
}
async create(userData) {
const user = await this.prisma.user.create({
data: userData
});
return new User(user);
}
async update(id, userData) {
const user = await this.prisma.user.update({
where: { id: parseInt(id) },
data: userData
});
return new User(user);
}
async delete(id) {
await this.prisma.user.delete({
where: { id: parseInt(id) }
});
return true;
}
}