negyedik gyakorlat + megoldasok
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user