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,19 @@
# Database
DATABASE_URL="postgresql://postgres:postgres@localhost:5433/blog_db?schema=public"
# Redis
REDIS_HOST=localhost
REDIS_PORT=6380
# JWT
JWT_SECRET=your-secret-key-change-this-in-production
JWT_EXPIRES_IN=7d
JWT_REFRESH_SECRET=your-refresh-secret-key-change-this
JWT_REFRESH_EXPIRES_IN=30d
# Server
PORT=3001
NODE_ENV=development
# Cookie
COOKIE_SECRET=your-cookie-secret-change-this
@@ -0,0 +1,8 @@
node_modules/
.env
dist/
.DS_Store
*.log
coverage/
.vscode/
.idea/
@@ -0,0 +1,89 @@
# Harmadik Gyakorlat - MINTA Megoldás
Ez a mappa tartalmazza a harmadik gyakorlat **teljes, működő megoldását**.
## Amit implementáltunk:
### 1. **Authentication (Hitelesítés)**
- User regisztráció jelszó hash-eléssel (bcrypt)
- User bejelentkezés JWT tokenekkel
- Cookie-based session kezelés
- Token frissítés (refresh token)
- Kijelentkezés
### 2. **Authorization (Jogosultságkezelés)**
- JWT token validálás middleware
- Role-based access control (RBAC)
- Resource ownership ellenőrzés
- Védett endpoint-ok
### 3. **Security Best Practices**
- HttpOnly cookie-k
- Secure cookie-k (production)
- JWT token expiry
- Password hashing (bcrypt)
- Input validáció
## Indítás
```bash
npm install
npm run docker:up
npx prisma generate
npx prisma migrate dev
npm run dev
```
## API Endpoints
### Auth Endpoints
- `POST /api/auth/register` - Regisztráció
- `POST /api/auth/login` - Bejelentkezés
- `POST /api/auth/logout` - Kijelentkezés (védett)
- `POST /api/auth/refresh` - Token frissítés
- `GET /api/auth/me` - Aktuális user (védett)
### Blog Endpoints
- `GET /api/blogs` - Összes blog (publikus)
- `GET /api/blogs/:id` - Egy blog (publikus)
- `POST /api/blogs` - Blog létrehozás (védett)
- `PUT /api/blogs/:id` - Blog módosítás (védett + ownership)
- `DELETE /api/blogs/:id` - Blog törlés (védett + ownership)
## Példa Használat
```bash
# Regisztráció
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "test@test.com",
"username": "testuser",
"password": "Test1234"
}'
# Login
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "test@test.com",
"password": "Test1234"
}'
# Blog létrehozás (védett)
curl -X POST http://localhost:3000/api/blogs \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "My Blog",
"content": "Blog content..."
}'
```
## Tanulási Pontok
1. **JWT Tokens**: Access és Refresh tokenek használata
2. **Bcrypt**: Jelszó hash-elés és validálás
3. **Cookie-based Auth**: HttpOnly, Secure cookie-k
4. **Authorization Middleware**: Token validálás, role check, ownership
5. **Security**: Best practices implementálása
@@ -0,0 +1,38 @@
version: '3.8'
services:
postgres:
image: postgres:15-alpine
container_name: blog_postgres_minta
restart: unless-stopped
ports:
- "5433:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: blog_db
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: blog_redis_minta
restart: unless-stopped
ports:
- "6380:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data:
redis_data:
@@ -0,0 +1,32 @@
{
"name": "blog-auth-practice-minta",
"version": "1.0.0",
"description": "MINTA - Backend gyakorló feladat - Authentication & Authorization",
"main": "src/index.js",
"type": "module",
"scripts": {
"dev": "node --watch src/index.js",
"start": "node src/index.js",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev",
"prisma:studio": "prisma studio",
"docker:up": "docker-compose up -d",
"docker:down": "docker-compose down",
"setup": "npm install && npm run docker:up && npm run prisma:migrate && npm run prisma:generate"
},
"keywords": ["backend", "authentication", "authorization", "jwt", "prisma", "cqrs"],
"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",
"ioredis": "^5.3.2",
"jsonwebtoken": "^9.0.2"
},
"devDependencies": {
"prisma": "^5.9.1"
}
}
@@ -0,0 +1,44 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid())
email String @unique
username String @unique
password String
role Role @default(USER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
blogs Blog[]
@@map("users")
}
model Blog {
id String @id @default(uuid())
title String
content String
published Boolean @default(false)
authorId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
@@map("blogs")
}
enum Role {
USER
ADMIN
}
@@ -0,0 +1,271 @@
import { UserRepository } from '../../infrastructure/repositories/UserRepository.js';
import {
hashPassword,
verifyPassword,
generateAccessToken,
generateRefreshToken,
setAuthCookies,
clearAuthCookies,
validateRegisterInput,
validateLoginInput,
verifyRefreshToken,
createSession,
deleteSession
} from '../../infrastructure/auth/authUtils.js';
export class AuthController {
constructor(userRepository) {
this.userRepository = userRepository || new UserRepository();
}
/**
* POST /api/auth/register
* Új felhasználó regisztrációja
*/
async register(req, res) {
try {
const { email, username, password } = req.body;
// 1. Input validáció
const validation = validateRegisterInput(req.body);
if (!validation.isValid) {
return res.status(400).json({
success: false,
errors: validation.errors
});
}
// 2. Email uniqueness ellenőrzés
const existingUserByEmail = await this.userRepository.findByEmail(email);
if (existingUserByEmail) {
return res.status(400).json({
success: false,
error: 'Email already in use'
});
}
// 3. Username uniqueness ellenőrzés
const existingUserByUsername = await this.userRepository.findByUsername(username);
if (existingUserByUsername) {
return res.status(400).json({
success: false,
error: 'Username already taken'
});
}
// 4. Jelszó hash-elés
const hashedPassword = await hashPassword(password);
// 5. User létrehozása
const user = await this.userRepository.create({
email,
username,
password: hashedPassword,
role: 'USER'
});
// 6. JWT tokenek generálása
const accessToken = generateAccessToken(user);
const refreshToken = generateRefreshToken(user);
// 7. Cookie-k beállítása
setAuthCookies(res, accessToken, refreshToken);
// 8. Redis session (opcionális)
await createSession(user.id, { email: user.email, username: user.username });
// 9. Válasz (jelszó nélkül!)
const { password: _, ...userWithoutPassword } = user;
res.status(201).json({
success: true,
message: 'User registered successfully',
data: {
user: userWithoutPassword,
accessToken,
refreshToken
}
});
} catch (error) {
console.error('Register error:', error);
res.status(500).json({
success: false,
error: error.message
});
}
}
/**
* POST /api/auth/login
* Bejelentkezés
*/
async login(req, res) {
try {
const { email, username, password } = req.body;
// 1. Input validáció
const validation = validateLoginInput(req.body);
if (!validation.isValid) {
return res.status(400).json({
success: false,
errors: validation.errors
});
}
// 2. User keresése (email VAGY username alapján)
let user;
if (email) {
user = await this.userRepository.findByEmail(email);
} else if (username) {
user = await this.userRepository.findByUsername(username);
}
if (!user) {
return res.status(401).json({
success: false,
error: 'Invalid credentials'
});
}
// 3. Jelszó ellenőrzés
const isPasswordValid = await verifyPassword(password, user.password);
if (!isPasswordValid) {
return res.status(401).json({
success: false,
error: 'Invalid credentials'
});
}
// 4. JWT tokenek generálása
const accessToken = generateAccessToken(user);
const refreshToken = generateRefreshToken(user);
// 5. Cookie-k beállítása
setAuthCookies(res, accessToken, refreshToken);
// 6. Redis session
await createSession(user.id, { email: user.email, username: user.username });
// 7. Válasz (jelszó nélkül!)
const { password: _, ...userWithoutPassword } = user;
res.json({
success: true,
message: 'Login successful',
data: {
user: userWithoutPassword,
accessToken,
refreshToken
}
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({
success: false,
error: error.message
});
}
}
/**
* POST /api/auth/logout
* Kijelentkezés
*/
async logout(req, res) {
try {
// 1. Cookie-k törlése
clearAuthCookies(res);
// 2. Redis session törlése
if (req.user && req.user.id) {
await deleteSession(req.user.id);
}
res.json({
success: true,
message: 'Logged out successfully'
});
} catch (error) {
console.error('Logout error:', error);
res.status(500).json({
success: false,
error: error.message
});
}
}
/**
* POST /api/auth/refresh
* Token frissítés
*/
async refreshToken(req, res) {
try {
// 1. Refresh token kiolvasása
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(401).json({
success: false,
error: 'Refresh token not found'
});
}
// 2. Refresh token validálás
const decoded = verifyRefreshToken(refreshToken);
// 3. User lekérése
const user = await this.userRepository.findById(decoded.userId);
if (!user) {
return res.status(401).json({
success: false,
error: 'User not found'
});
}
// 4. Új access token generálás
const newAccessToken = generateAccessToken(user);
// 5. Cookie frissítése
res.cookie('accessToken', newAccessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000
});
res.json({
success: true,
data: {
accessToken: newAccessToken
}
});
} catch (error) {
console.error('Refresh token error:', error);
res.status(401).json({
success: false,
error: 'Invalid refresh token'
});
}
}
/**
* GET /api/auth/me
* Bejelentkezett user adatai
*/
async getCurrentUser(req, res) {
try {
// req.user-t az authenticateToken middleware állította be
const { password: _, ...userWithoutPassword } = req.user;
res.json({
success: true,
data: {
user: userWithoutPassword
}
});
} catch (error) {
console.error('Get current user error:', error);
res.status(500).json({
success: false,
error: error.message
});
}
}
}
@@ -0,0 +1,114 @@
import { BlogRepository } from '../../infrastructure/repositories/BlogRepository.js';
export class BlogController {
constructor() {
this.blogRepository = new BlogRepository();
}
async getAll(req, res) {
try {
const blogs = await this.blogRepository.findAll();
res.json({
success: true,
data: blogs
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
}
async getById(req, res) {
try {
const blog = await this.blogRepository.findById(req.params.id);
if (!blog) {
return res.status(404).json({
success: false,
error: 'Blog not found'
});
}
res.json({
success: true,
data: blog
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
}
async create(req, res) {
try {
const { title, content, published } = req.body;
if (!title || !content) {
return res.status(400).json({
success: false,
error: 'Title and content are required'
});
}
const blog = await this.blogRepository.create({
title,
content,
published: published || false,
authorId: req.user.id
});
res.status(201).json({
success: true,
data: blog,
message: 'Blog created successfully'
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
}
async update(req, res) {
try {
const { title, content, published } = req.body;
const updateData = {};
if (title) updateData.title = title;
if (content) updateData.content = content;
if (typeof published !== 'undefined') updateData.published = published;
const blog = await this.blogRepository.update(req.params.id, updateData);
res.json({
success: true,
data: blog,
message: 'Blog updated successfully'
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
}
async delete(req, res) {
try {
await this.blogRepository.delete(req.params.id);
res.json({
success: true,
message: 'Blog deleted successfully'
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
}
}
@@ -0,0 +1,143 @@
import {
extractTokenFromRequest,
verifyAccessToken
} from '../../infrastructure/auth/authUtils.js';
import { UserRepository } from '../../infrastructure/repositories/UserRepository.js';
const userRepository = new UserRepository();
/**
* Authentication Middleware
*
* Ellenőrzi a JWT tokent és beállítja req.user-t
*/
export async function authenticateToken(req, res, next) {
try {
// 1. Token kiolvasása (cookie vagy Authorization header)
const token = extractTokenFromRequest(req);
if (!token) {
return res.status(401).json({
success: false,
error: 'Access token required'
});
}
// 2. Token validálás
const decoded = verifyAccessToken(token);
// 3. User lekérése az adatbázisból
const user = await userRepository.findById(decoded.userId);
if (!user) {
return res.status(401).json({
success: false,
error: 'User not found'
});
}
// 4. User hozzáadása a request objektumhoz
req.user = user;
// 5. Továbblépés a következő middleware-re
next();
} catch (error) {
console.error('Authentication error:', error);
return res.status(401).json({
success: false,
error: 'Invalid or expired token'
});
}
}
/**
* Authorization Middleware - Role Check
*
* Ellenőrzi, hogy a usernek megfelel ő role-ja van-e
*
* @param {string[]} allowedRoles - Engedélyezett role-ok listája
* @returns {Function} Express middleware
*/
export function requireRole(allowedRoles) {
return (req, res, next) => {
try {
// 1. Ellenőrzés: req.user létezik? (authenticateToken után fut)
if (!req.user) {
return res.status(401).json({
success: false,
error: 'Authentication required'
});
}
// 2. Role check: van-e a usernek megfelelő role-ja?
if (!allowedRoles.includes(req.user.role)) {
return res.status(403).json({
success: false,
error: 'Insufficient permissions'
});
}
// 3. OK, továbblépés
next();
} catch (error) {
console.error('Authorization error:', error);
return res.status(403).json({
success: false,
error: 'Authorization failed'
});
}
};
}
/**
* Resource Ownership Check
*
* Ellenőrzi, hogy a user tulajdonosa-e az adott resource-nak
* VAGY admin role-ja van
*
* @param {Function} getResourceOwnerId - Async függvény ami visszaadja a resource owner ID-t
* @returns {Function} Express middleware
*/
export function checkOwnership(getResourceOwnerId) {
return async (req, res, next) => {
try {
// 1. User ellenőrzés
if (!req.user) {
return res.status(401).json({
success: false,
error: 'Authentication required'
});
}
// 2. Resource owner ID lekérése
const ownerId = await getResourceOwnerId(req);
if (!ownerId) {
return res.status(404).json({
success: false,
error: 'Resource not found'
});
}
// 3. Ownership ellenőrzés: user tulajdonos VAGY admin
const isOwner = req.user.id === ownerId;
const isAdmin = req.user.role === 'ADMIN';
if (!isOwner && !isAdmin) {
return res.status(403).json({
success: false,
error: 'You do not have permission to access this resource'
});
}
// 4. OK, továbblépés
next();
} catch (error) {
console.error('Ownership check error:', error);
return res.status(500).json({
success: false,
error: 'Ownership verification failed'
});
}
};
}
@@ -0,0 +1,19 @@
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 })
});
}
export function notFoundHandler(req, res) {
res.status(404).json({
success: false,
error: 'Endpoint not found'
});
}
@@ -0,0 +1,17 @@
import express from 'express';
import { AuthController } from '../controllers/AuthController.js';
import { authenticateToken } from '../middlewares/authMiddleware.js';
const router = express.Router();
const authController = new AuthController();
// Publikus route-ok
router.post('/register', (req, res) => authController.register(req, res));
router.post('/login', (req, res) => authController.login(req, res));
router.post('/refresh', (req, res) => authController.refreshToken(req, res));
// Védett route-ok (authenticateToken middleware)
router.post('/logout', authenticateToken, (req, res) => authController.logout(req, res));
router.get('/me', authenticateToken, (req, res) => authController.getCurrentUser(req, res));
export default router;
@@ -0,0 +1,38 @@
import express from 'express';
import { BlogController } from '../controllers/BlogController.js';
import { authenticateToken, checkOwnership } from '../middlewares/authMiddleware.js';
import { BlogRepository } from '../../infrastructure/repositories/BlogRepository.js';
const router = express.Router();
const blogController = new BlogController();
const blogRepository = new BlogRepository();
// Publikus route-ok
router.get('/', (req, res) => blogController.getAll(req, res));
router.get('/:id', (req, res) => blogController.getById(req, res));
// Védett route-ok
router.post('/',
authenticateToken,
(req, res) => blogController.create(req, res)
);
router.put('/:id',
authenticateToken,
checkOwnership(async (req) => {
const blog = await blogRepository.findById(parseInt(req.params.id));
return blog?.authorId;
}),
(req, res) => blogController.update(req, res)
);
router.delete('/:id',
authenticateToken,
checkOwnership(async (req) => {
const blog = await blogRepository.findById(parseInt(req.params.id));
return blog?.authorId;
}),
(req, res) => blogController.delete(req, res)
);
export default router;
@@ -0,0 +1,29 @@
export class Blog {
constructor(data) {
this.id = data.id;
this.title = data.title;
this.content = data.content;
this.published = data.published;
this.authorId = data.authorId;
this.createdAt = data.createdAt;
this.updatedAt = data.updatedAt;
this.author = data.author;
}
toJSON() {
return {
id: this.id,
title: this.title,
content: this.content,
published: this.published,
authorId: this.authorId,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
author: this.author ? {
id: this.author.id,
username: this.author.username,
email: this.author.email
} : null
};
}
}
@@ -0,0 +1,20 @@
export class User {
constructor(data) {
this.id = data.id;
this.email = data.email;
this.username = data.username;
this.password = data.password;
this.role = data.role;
this.createdAt = data.createdAt;
this.updatedAt = data.updatedAt;
}
isAdmin() {
return this.role === 'ADMIN';
}
toJSON() {
const { password, ...userWithoutPassword } = this;
return userWithoutPassword;
}
}
@@ -0,0 +1,25 @@
export class IBlogRepository {
async findById(id) {
throw new Error('findById() must be implemented');
}
async findAll() {
throw new Error('findAll() must be implemented');
}
async findByAuthorId(authorId) {
throw new Error('findByAuthorId() must be implemented');
}
async create(blogData) {
throw new Error('create() must be implemented');
}
async update(id, blogData) {
throw new Error('update() must be implemented');
}
async delete(id) {
throw new Error('delete() must be implemented');
}
}
@@ -0,0 +1,29 @@
export class IUserRepository {
async findById(id) {
throw new Error('findById() must be implemented');
}
async findByEmail(email) {
throw new Error('findByEmail() must be implemented');
}
async findByUsername(username) {
throw new Error('findByUsername() 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');
}
async findAll() {
throw new Error('findAll() must be implemented');
}
}
@@ -0,0 +1,62 @@
import express from 'express';
import cookieParser from 'cookie-parser';
import dotenv from 'dotenv';
dotenv.config();
import databaseClient from './infrastructure/database/prisma.js';
import redisClient from './infrastructure/database/redis.js';
import authRoutes from './api/routes/authRoutes.js';
import blogRoutes from './api/routes/blogRoutes.js';
import { errorHandler, notFoundHandler } from './api/middlewares/errorHandler.js';
const app = express();
const PORT = process.env.PORT || 3001;
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser(process.env.COOKIE_SECRET));
// Database connection
await databaseClient.connect();
// Health check
app.get('/health', (req, res) => {
res.json({
status: 'OK',
timestamp: new Date().toISOString(),
environment: process.env.NODE_ENV
});
});
// Routes
app.use('/api/auth', authRoutes);
app.use('/api/blogs', blogRoutes);
// Error handling
app.use(notFoundHandler);
app.use(errorHandler);
// Graceful shutdown
process.on('SIGINT', async () => {
console.log('\n🛑 Shutting down gracefully...');
await databaseClient.disconnect();
await redisClient.disconnect();
process.exit(0);
});
process.on('SIGTERM', async () => {
console.log('\n🛑 Shutting down gracefully...');
await databaseClient.disconnect();
await redisClient.disconnect();
process.exit(0);
});
// Start server
app.listen(PORT, () => {
console.log(`✅ Server running on http://localhost:${PORT}`);
console.log(`📊 Environment: ${process.env.NODE_ENV}`);
});
@@ -0,0 +1,231 @@
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
import { redis } from '../database/redis.js';
/**
* JWT Token Generálás
*/
export function generateAccessToken(user) {
const payload = {
userId: user.id,
email: user.email,
role: user.role
};
return jwt.sign(
payload,
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN || '7d' }
);
}
export function generateRefreshToken(user) {
const payload = {
userId: user.id
};
return jwt.sign(
payload,
process.env.JWT_REFRESH_SECRET,
{ expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '30d' }
);
}
/**
* JWT Token Validálás
*/
export function verifyAccessToken(token) {
try {
return jwt.verify(token, process.env.JWT_SECRET);
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new Error('Token expired');
}
if (error.name === 'JsonWebTokenError') {
throw new Error('Invalid token');
}
throw error;
}
}
export function verifyRefreshToken(token) {
try {
return jwt.verify(token, process.env.JWT_REFRESH_SECRET);
} catch (error) {
throw new Error('Invalid refresh token');
}
}
/**
* Token Kiolvasás Request-ből
*/
export function extractTokenFromRequest(req) {
// 1. Cookie-ból
if (req.cookies && req.cookies.accessToken) {
return req.cookies.accessToken;
}
// 2. Authorization header-ből
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
return authHeader.substring(7);
}
return null;
}
/**
* Jelszó Hash-elés és Validálás
*/
export async function hashPassword(plainPassword) {
const saltRounds = 10;
return await bcrypt.hash(plainPassword, saltRounds);
}
export async function verifyPassword(plainPassword, hashedPassword) {
return await bcrypt.compare(plainPassword, hashedPassword);
}
/**
* Cookie Beállítás
*/
export function setAuthCookies(res, accessToken, refreshToken) {
const cookieOptions = {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict'
};
// Access token cookie - 7 nap
res.cookie('accessToken', accessToken, {
...cookieOptions,
maxAge: 7 * 24 * 60 * 60 * 1000
});
// Refresh token cookie - 30 nap
res.cookie('refreshToken', refreshToken, {
...cookieOptions,
maxAge: 30 * 24 * 60 * 60 * 1000
});
}
export function clearAuthCookies(res) {
res.clearCookie('accessToken');
res.clearCookie('refreshToken');
}
/**
* Input Validáció
*/
export function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
export function isValidPassword(password) {
return password && password.length >= 8;
}
export function validateRegisterInput(data) {
const errors = [];
if (!data.email || !isValidEmail(data.email)) {
errors.push('Valid email is required');
}
if (!data.username || data.username.length < 3) {
errors.push('Username must be at least 3 characters');
}
if (!data.password || !isValidPassword(data.password)) {
errors.push('Password must be at least 8 characters');
}
return {
isValid: errors.length === 0,
errors
};
}
export function validateLoginInput(data) {
const errors = [];
if (!data.password) {
errors.push('Password is required');
}
if (!data.email && !data.username) {
errors.push('Email or username is required');
}
return {
isValid: errors.length === 0,
errors
};
}
/**
* Redis Session Management (Opcionális)
*/
export async function createSession(userId, sessionData) {
try {
const sessionKey = `session:${userId}`;
const ttl = 7 * 24 * 60 * 60; // 7 nap másodpercben
await redis.set(
sessionKey,
JSON.stringify({
userId,
loginTime: new Date().toISOString(),
...sessionData
}),
'EX',
ttl
);
} catch (error) {
console.error('Redis session create error:', error);
}
}
export async function getSession(userId) {
try {
const sessionKey = `session:${userId}`;
const session = await redis.get(sessionKey);
if (!session) {
return null;
}
return JSON.parse(session);
} catch (error) {
console.error('Redis session get error:', error);
return null;
}
}
export async function deleteSession(userId) {
try {
const sessionKey = `session:${userId}`;
await redis.del(sessionKey);
} catch (error) {
console.error('Redis session delete error:', error);
}
}
export async function sessionExists(userId) {
try {
const sessionKey = `session:${userId}`;
const exists = await redis.exists(sessionKey);
return exists === 1;
} catch (error) {
console.error('Redis session exists error:', error);
return false;
}
}
@@ -0,0 +1,22 @@
import { PrismaClient } from '@prisma/client';
export const prisma = new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
});
export default {
client: prisma,
connect: async () => {
try {
await prisma.$connect();
console.log('✅ Database connected');
} catch (error) {
console.error('❌ Database connection failed:', error);
process.exit(1);
}
},
disconnect: async () => {
await prisma.$disconnect();
console.log('Database disconnected');
}
};
@@ -0,0 +1,28 @@
import Redis from 'ioredis';
const redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT) || 6380,
retryStrategy: (times) => {
const delay = Math.min(times * 50, 2000);
return delay;
}
});
redis.on('connect', () => {
console.log('✅ Redis connected');
});
redis.on('error', (error) => {
console.error('❌ Redis connection error:', error);
});
export { redis };
export default {
client: redis,
disconnect: async () => {
await redis.quit();
console.log('Redis disconnected');
}
};
@@ -0,0 +1,54 @@
import { IBlogRepository } from '../../domain/repositories/IBlogRepository.js';
import { Blog } from '../../domain/models/Blog.js';
import { prisma } from '../database/prisma.js';
export class BlogRepository extends IBlogRepository {
async findById(id) {
const blog = await prisma.blog.findUnique({
where: { id },
include: { author: true }
});
return blog ? new Blog(blog) : null;
}
async findAll() {
const blogs = await prisma.blog.findMany({
include: { author: true },
orderBy: { createdAt: 'desc' }
});
return blogs.map(blog => new Blog(blog));
}
async findByAuthorId(authorId) {
const blogs = await prisma.blog.findMany({
where: { authorId },
include: { author: true },
orderBy: { createdAt: 'desc' }
});
return blogs.map(blog => new Blog(blog));
}
async create(blogData) {
const blog = await prisma.blog.create({
data: blogData,
include: { author: true }
});
return new Blog(blog);
}
async update(id, blogData) {
const blog = await prisma.blog.update({
where: { id },
data: blogData,
include: { author: true }
});
return new Blog(blog);
}
async delete(id) {
await prisma.blog.delete({
where: { id }
});
return true;
}
}
@@ -0,0 +1,55 @@
import { IUserRepository } from '../../domain/repositories/IUserRepository.js';
import { User } from '../../domain/models/User.js';
import { prisma } from '../database/prisma.js';
export class UserRepository extends IUserRepository {
async findById(id) {
const user = await prisma.user.findUnique({
where: { id }
});
return user ? new User(user) : null;
}
async findByEmail(email) {
const user = await prisma.user.findUnique({
where: { email }
});
return user ? new User(user) : null;
}
async findByUsername(username) {
const user = await prisma.user.findUnique({
where: { username }
});
return user ? new User(user) : null;
}
async create(userData) {
const user = await prisma.user.create({
data: userData
});
return new User(user);
}
async update(id, userData) {
const user = await prisma.user.update({
where: { id },
data: userData
});
return new User(user);
}
async delete(id) {
await prisma.user.delete({
where: { id }
});
return true;
}
async findAll() {
const users = await prisma.user.findMany({
orderBy: { createdAt: 'desc' }
});
return users.map(user => new User(user));
}
}