negyedik gyakorlat + megoldasok
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
+31
@@ -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;
|
||||
}
|
||||
}
|
||||
+51
@@ -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');
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
export class UpdateUserProfileCommand {
|
||||
constructor(userId, name, email) {
|
||||
this.userId = userId;
|
||||
this.name = name;
|
||||
this.email = email;
|
||||
}
|
||||
}
|
||||
+26
@@ -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 {
|
||||
}
|
||||
+9
@@ -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;
|
||||
}
|
||||
}
|
||||
+13
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user