Compare commits

..

19 Commits

Author SHA1 Message Date
magdo 3ee9c03b85 updated frontend 2026-04-01 22:01:36 +02:00
magdo 91e48d2178 Frontend feladat 1 2026-03-24 20:12:08 +01:00
magdo ad1e783472 For Frontend practice 2026-03-21 20:39:18 +01:00
magdo 8b8c08be1b react_minta 2026-03-18 00:01:04 +01:00
magdo 28bd7661f5 frontend elmélet 2026-03-17 23:53:10 +01:00
magdo 8da3a1eb32 negyedik gyakorlat megoldás javítva 2026-03-06 23:26:33 +01:00
magdo 388aa908de negyedik gyakorlat + megoldasok 2026-03-04 20:02:39 +01:00
magdo afc3777ac9 email és dic 2026-03-03 18:37:08 +01:00
magdo ffca701b84 harmadik gyakorlat 2026-02-25 20:16:03 +01:00
magdo a837f5ecba authn_z 2026-02-24 01:30:22 +01:00
magdo 31d4479c63 authn_z 2026-02-24 01:29:59 +01:00
magdo 77f7eb2664 masodik_gyak 2026-02-18 21:02:39 +01:00
magdo af57733506 docker 2026-02-17 21:17:42 +01:00
magdo d90a6ed735 backend feladat 2026-02-13 20:50:01 +01:00
magdo 0f0e463062 backend feladat 2026-02-13 19:31:01 +01:00
magdo 87f07bb22f backend feladat 2026-02-13 19:16:45 +01:00
magdo 86109cf48b backend feladat 2026-02-13 19:15:16 +01:00
magdo 622d4a3321 backend feladat 2026-02-13 19:14:10 +01:00
magdo 6afdef6a30 database added 2026-02-08 00:51:17 +01:00
436 changed files with 35729 additions and 1 deletions
+12
View File
@@ -14,4 +14,16 @@
*.vrb
*.synctex.gz
#nodejs
*/*/node_modules
*/*/node_modules
*/*/package-lock.json
*/*/yarn.lock
Thumbs.*
# tömörített fájlok
*.zip
*.tar.gz
*.rar
*.7z
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
{
"name": "elso-gyakorlat",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"start": "nodemon src/API/server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"express": "^5.2.1",
"nodemon": "^3.1.11"
}
}
@@ -0,0 +1,92 @@
import { PostRepository } from '../../Infrastructure/postRepository.js';
import { GetAllPostsQuery } from '../../Application/blogs/query/getAllPostsQuery.js';
import { GetAllPostsQueryHandler } from '../../Application/blogs/query/getAllPostsQueryHandler.js';
import { GetPostByIdQuery } from '../../Application/blogs/query/getPostByIdQuery.js';
import { GetPostByIdQueryHandler } from '../../Application/blogs/query/getPostByIdQueryHandler.js';
import { GetPostsByUserIdQuery } from '../../Application/blogs/query/getPostsByUserIdQuery.js';
import { GetPostsByUserIdQueryHandler } from '../../Application/blogs/query/getPostsByUserIdQueryHandler.js';
import { CreatePostCommand } from '../../Application/blogs/command/createPostCommand.js';
import { CreatePostCommandHandler } from '../../Application/blogs/command/createPostCommandHandler.js';
import { UpdatePostCommand } from '../../Application/blogs/command/updatePostCommand.js';
import { UpdatePostCommandHandler } from '../../Application/blogs/command/updatePostCommandHandler.js';
import { DeletePostCommand } from '../../Application/blogs/command/deletePostCommand.js';
import { DeletePostCommandHandler } from '../../Application/blogs/command/deletePostCommandHandler.js';
const postRepository = new PostRepository();
const getAllPostsQueryHandler = new GetAllPostsQueryHandler(postRepository);
const getPostByIdQueryHandler = new GetPostByIdQueryHandler(postRepository);
const getPostsByUserIdQueryHandler = new GetPostsByUserIdQueryHandler(postRepository);
const createPostCommandHandler = new CreatePostCommandHandler(postRepository);
const updatePostCommandHandler = new UpdatePostCommandHandler(postRepository);
const deletePostCommandHandler = new DeletePostCommandHandler(postRepository);
export class PostController {
async getAll(req, res) {
try {
const query = new GetAllPostsQuery();
const posts = await getAllPostsQueryHandler.handle(query);
res.json(posts);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async getById(req, res) {
try {
const query = new GetPostByIdQuery(req.params.id);
const post = await getPostByIdQueryHandler.handle(query);
res.json(post);
} catch (error) {
res.status(404).json({ error: error.message });
}
}
async getByUserId(req, res) {
try {
const query = new GetPostsByUserIdQuery(req.params.userId);
const posts = await getPostsByUserIdQueryHandler.handle(query);
res.json(posts);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async create(req, res) {
try {
const command = new CreatePostCommand(
req.body.title,
req.body.content,
req.body.author
);
const post = await createPostCommandHandler.handle(command);
res.status(201).json(post);
} catch (error) {
res.status(400).json({ error: error.message });
}
}
async update(req, res) {
try {
const command = new UpdatePostCommand(
req.params.id,
req.body.title,
req.body.content,
req.body.author
);
const post = await updatePostCommandHandler.handle(command);
res.json(post);
} catch (error) {
res.status(404).json({ error: error.message });
}
}
async delete(req, res) {
try {
const command = new DeletePostCommand(req.params.id);
await deletePostCommandHandler.handle(command);
res.status(204).send();
} catch (error) {
res.status(404).json({ error: error.message });
}
}
}
@@ -0,0 +1,79 @@
import { UserRepository } from '../../Infrastructure/userRepository.js';
import { GetAllUsersQuery } from '../../Application/users/query/getAllUsersQuery.js';
import { GetAllUsersQueryHandler } from '../../Application/users/query/getAllUsersQueryHandler.js';
import { GetUserByIdQuery } from '../../Application/users/query/getUserByIdQuery.js';
import { GetUserByIdQueryHandler } from '../../Application/users/query/getUserByIdQueryHandler.js';
import { CreateUserCommand } from '../../Application/users/command/createUserCommand.js';
import { CreateUserCommandHandler } from '../../Application/users/command/createUserCommandHandler.js';
import { UpdateUserCommand } from '../../Application/users/command/updateUserCommand.js';
import { UpdateUserCommandHandler } from '../../Application/users/command/updateUserCommandHandler.js';
import { DeleteUserCommand } from '../../Application/users/command/deleteUserCommand.js';
import { DeleteUserCommandHandler } from '../../Application/users/command/deleteUserCommandHandler.js';
const userRepository = new UserRepository();
const getAllUsersQueryHandler = new GetAllUsersQueryHandler(userRepository);
const getUserByIdQueryHandler = new GetUserByIdQueryHandler(userRepository);
const createUserCommandHandler = new CreateUserCommandHandler(userRepository);
const updateUserCommandHandler = new UpdateUserCommandHandler(userRepository);
const deleteUserCommandHandler = new DeleteUserCommandHandler(userRepository);
export class UserController {
async getAll(req, res) {
try {
const query = new GetAllUsersQuery();
const users = await getAllUsersQueryHandler.handle(query);
res.json(users);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async getById(req, res) {
try {
const query = new GetUserByIdQuery(req.params.id);
const user = await getUserByIdQueryHandler.handle(query);
res.json(user);
} catch (error) {
res.status(404).json({ error: error.message });
}
}
async create(req, res) {
try {
const command = new CreateUserCommand(
req.body.name,
req.body.email,
req.body.age
);
const user = await createUserCommandHandler.handle(command);
res.status(201).json(user);
} catch (error) {
res.status(400).json({ error: error.message });
}
}
async update(req, res) {
try {
const command = new UpdateUserCommand(
req.params.id,
req.body.name,
req.body.email,
req.body.age
);
const user = await updateUserCommandHandler.handle(command);
res.json(user);
} catch (error) {
res.status(404).json({ error: error.message });
}
}
async delete(req, res) {
try {
const command = new DeleteUserCommand(req.params.id);
await deleteUserCommandHandler.handle(command);
res.status(204).send();
} catch (error) {
res.status(404).json({ error: error.message });
}
}
}
@@ -0,0 +1,14 @@
import express from 'express';
import { PostController } from '../controllers/postController.js';
const router = express.Router();
const postController = new PostController();
router.get('/', (req, res) => postController.getAll(req, res));
router.get('/user/:userId', (req, res) => postController.getByUserId(req, res));
router.get('/:id', (req, res) => postController.getById(req, res));
router.post('/', (req, res) => postController.create(req, res));
router.put('/:id', (req, res) => postController.update(req, res));
router.delete('/:id', (req, res) => postController.delete(req, res));
export default router;
@@ -0,0 +1,13 @@
import express from 'express';
import { UserController } from '../controllers/userController.js';
const router = express.Router();
const userController = new UserController();
router.get('/', (req, res) => userController.getAll(req, res));
router.get('/:id', (req, res) => userController.getById(req, res));
router.post('/', (req, res) => userController.create(req, res));
router.put('/:id', (req, res) => userController.update(req, res));
router.delete('/:id', (req, res) => userController.delete(req, res));
export default router;
+36
View File
@@ -0,0 +1,36 @@
import express from 'express';
import postRouter from './routers/postRouter.js';
import userRouter from './routers/userRouter.js';
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Routes
app.use('/api/posts', postRouter);
app.use('/api/users', userRouter);
// Root endpoint
app.get('/', (req, res) => {
res.json({
message: 'Welcome to the Blog API',
endpoints: {
posts: '/api/posts',
users: '/api/users'
}
});
});
// Error handling middleware
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Something went wrong!' });
});
// Start server
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
@@ -0,0 +1,7 @@
export class CreatePostCommand {
constructor(title, content, author) {
this.title = title;
this.content = content;
this.author = author;
}
}
@@ -0,0 +1,19 @@
export class CreatePostCommandHandler {
constructor(postRepository) {
this.postRepository = postRepository;
}
async handle(command) {
if (!command.title || !command.content) {
throw new Error('Title and content are required');
}
const postData = {
title: command.title,
content: command.content,
author: command.author
};
return await this.postRepository.create(postData);
}
}
@@ -0,0 +1,5 @@
export class DeletePostCommand {
constructor(id) {
this.id = id;
}
}
@@ -0,0 +1,13 @@
export class DeletePostCommandHandler {
constructor(postRepository) {
this.postRepository = postRepository;
}
async handle(command) {
const result = await this.postRepository.delete(command.id);
if (!result) {
throw new Error('Post not found');
}
return result;
}
}
@@ -0,0 +1,8 @@
export class UpdatePostCommand {
constructor(id, title, content, author) {
this.id = id;
this.title = title;
this.content = content;
this.author = author;
}
}
@@ -0,0 +1,18 @@
export class UpdatePostCommandHandler {
constructor(postRepository) {
this.postRepository = postRepository;
}
async handle(command) {
const postData = {};
if (command.title) postData.title = command.title;
if (command.content) postData.content = command.content;
if (command.author) postData.author = command.author;
const result = await this.postRepository.update(command.id, postData);
if (!result) {
throw new Error('Post not found');
}
return result;
}
}
@@ -0,0 +1,3 @@
export class GetAllPostsQuery {
constructor() {}
}
@@ -0,0 +1,9 @@
export class GetAllPostsQueryHandler {
constructor(postRepository) {
this.postRepository = postRepository;
}
async handle(query) {
return await this.postRepository.getAll();
}
}
@@ -0,0 +1,5 @@
export class GetPostByIdQuery {
constructor(id) {
this.id = id;
}
}
@@ -0,0 +1,13 @@
export class GetPostByIdQueryHandler {
constructor(postRepository) {
this.postRepository = postRepository;
}
async handle(query) {
const post = await this.postRepository.getById(query.id);
if (!post) {
throw new Error('Post not found');
}
return post;
}
}
@@ -0,0 +1,5 @@
export class GetPostsByUserIdQuery {
constructor(userId) {
this.userId = userId;
}
}
@@ -0,0 +1,9 @@
export class GetPostsByUserIdQueryHandler {
constructor(postRepository) {
this.postRepository = postRepository;
}
async handle(query) {
return await this.postRepository.getByUserId(query.userId);
}
}
@@ -0,0 +1,7 @@
export class CreateUserCommand {
constructor(name, email, age) {
this.name = name;
this.email = email;
this.age = age;
}
}
@@ -0,0 +1,19 @@
export class CreateUserCommandHandler {
constructor(userRepository) {
this.userRepository = userRepository;
}
async handle(command) {
if (!command.name || !command.email) {
throw new Error('Name and email are required');
}
const userData = {
name: command.name,
email: command.email,
age: command.age
};
return await this.userRepository.create(userData);
}
}
@@ -0,0 +1,5 @@
export class DeleteUserCommand {
constructor(id) {
this.id = id;
}
}
@@ -0,0 +1,13 @@
export class DeleteUserCommandHandler {
constructor(userRepository) {
this.userRepository = userRepository;
}
async handle(command) {
const result = await this.userRepository.delete(command.id);
if (!result) {
throw new Error('User not found');
}
return result;
}
}
@@ -0,0 +1,8 @@
export class UpdateUserCommand {
constructor(id, name, email, age) {
this.id = id;
this.name = name;
this.email = email;
this.age = age;
}
}
@@ -0,0 +1,18 @@
export class UpdateUserCommandHandler {
constructor(userRepository) {
this.userRepository = userRepository;
}
async handle(command) {
const userData = {};
if (command.name) userData.name = command.name;
if (command.email) userData.email = command.email;
if (command.age !== undefined) userData.age = command.age;
const result = await this.userRepository.update(command.id, userData);
if (!result) {
throw new Error('User not found');
}
return result;
}
}
@@ -0,0 +1,3 @@
export class GetAllUsersQuery {
constructor() {}
}
@@ -0,0 +1,9 @@
export class GetAllUsersQueryHandler {
constructor(userRepository) {
this.userRepository = userRepository;
}
async handle(query) {
return await this.userRepository.getAll();
}
}
@@ -0,0 +1,5 @@
export class GetUserByIdQuery {
constructor(id) {
this.id = id;
}
}
@@ -0,0 +1,13 @@
export class GetUserByIdQueryHandler {
constructor(userRepository) {
this.userRepository = userRepository;
}
async handle(query) {
const user = await this.userRepository.getById(query.id);
if (!user) {
throw new Error('User not found');
}
return user;
}
}
@@ -0,0 +1,25 @@
export class IPostRepository {
async getAll() {
throw new Error('Method not implemented');
}
async getById(id) {
throw new Error('Method not implemented');
}
async getByUserId(userId) {
throw new Error('Method not implemented');
}
async create(post) {
throw new Error('Method not implemented');
}
async update(id, postData) {
throw new Error('Method not implemented');
}
async delete(id) {
throw new Error('Method not implemented');
}
}
@@ -0,0 +1,21 @@
export class IUserRepository {
async getAll() {
throw new Error('Method not implemented');
}
async getById(id) {
throw new Error('Method not implemented');
}
async create(user) {
throw new Error('Method not implemented');
}
async update(id, userData) {
throw new Error('Method not implemented');
}
async delete(id) {
throw new Error('Method not implemented');
}
}
@@ -0,0 +1 @@
[]
@@ -0,0 +1,65 @@
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import { IPostRepository } from '../Domain/IPostRepository.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const POST_FILE = path.join(__dirname, 'post.json');
export class PostRepository extends IPostRepository {
async getAll() {
try {
const data = await fs.readFile(POST_FILE, 'utf-8');
return JSON.parse(data);
} catch (error) {
return [];
}
}
async getById(id) {
const posts = await this.getAll();
return posts.find(post => post.id === id);
}
async getByUserId(userId) {
const posts = await this.getAll();
return posts.filter(post => post.userId === userId);
}
async create(post) {
const posts = await this.getAll();
const newPost = {
id: Date.now().toString(),
...post,
createdAt: new Date().toISOString()
};
posts.push(newPost);
await fs.writeFile(POST_FILE, JSON.stringify(posts, null, 2));
return newPost;
}
async update(id, postData) {
const posts = await this.getAll();
const index = posts.findIndex(post => post.id === id);
if (index === -1) return null;
posts[index] = {
...posts[index],
...postData,
id: posts[index].id,
updatedAt: new Date().toISOString()
};
await fs.writeFile(POST_FILE, JSON.stringify(posts, null, 2));
return posts[index];
}
async delete(id) {
const posts = await this.getAll();
const filteredPosts = posts.filter(post => post.id !== id);
if (posts.length === filteredPosts.length) return false;
await fs.writeFile(POST_FILE, JSON.stringify(filteredPosts, null, 2));
return true;
}
}
@@ -0,0 +1 @@
[]
@@ -0,0 +1,60 @@
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import { IUserRepository } from '../Domain/IUserRepository.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const USER_FILE = path.join(__dirname, 'user.json');
export class UserRepository extends IUserRepository {
async getAll() {
try {
const data = await fs.readFile(USER_FILE, 'utf-8');
return JSON.parse(data);
} catch (error) {
return [];
}
}
async getById(id) {
const users = await this.getAll();
return users.find(user => user.id === id);
}
async create(user) {
const users = await this.getAll();
const newUser = {
id: Date.now().toString(),
...user,
createdAt: new Date().toISOString()
};
users.push(newUser);
await fs.writeFile(USER_FILE, JSON.stringify(users, null, 2));
return newUser;
}
async update(id, userData) {
const users = await this.getAll();
const index = users.findIndex(user => user.id === id);
if (index === -1) return null;
users[index] = {
...users[index],
...userData,
id: users[index].id,
updatedAt: new Date().toISOString()
};
await fs.writeFile(USER_FILE, JSON.stringify(users, null, 2));
return users[index];
}
async delete(id) {
const users = await this.getAll();
const filteredUsers = users.filter(user => user.id !== id);
if (users.length === filteredUsers.length) return false;
await fs.writeFile(USER_FILE, JSON.stringify(filteredUsers, null, 2));
return true;
}
}
+3
View File
@@ -0,0 +1,3 @@
node_modules/
.env
*.log
+119
View File
@@ -0,0 +1,119 @@
# Első Gyakorlat - MINTA Megoldás
Ez a mappa tartalmazza az első gyakorlat **teljes, működő megoldását**.
## Amit implementáltunk:
### 1. **Layered Architecture** (Rétegezett Architektúra)
- **API Layer**: Controllers, Routers - HTTP kérések kezelése
- **Application Layer**: Commands, Queries, Handlers - Üzleti logika
- **Domain Layer**: Interfaces (Repository pattern)
- **Infrastructure Layer**: Repositories - Adatkezelés (JSON file-ok)
### 2. **CQRS Pattern** (Command Query Responsibility Segregation)
- **Commands**: Írási műveletek (Create, Update, Delete)
- **Queries**: Olvasási műveletek (GetAll, GetById)
- **Handlers**: Command és Query végrehajtók
### 3. **Repository Pattern**
- `IUserRepository` és `IPostRepository` interfészek
- `UserRepository` és `PostRepository` implementációk
- Adatok tárolása JSON file-okban
## Struktúra
```
src/
├── API/
│ ├── server.js # Express szerver
│ ├── controllers/
│ │ ├── userController.js # User endpoint logika
│ │ └── postController.js # Post endpoint logika
│ └── routers/
│ ├── userRouter.js # User route-ok
│ └── postRouter.js # Post route-ok
├── Application/
│ ├── users/
│ │ ├── command/ # User írási műveletek
│ │ │ ├── createUserCommand.js
│ │ │ ├── createUserCommandHandler.js
│ │ │ ├── updateUserCommand.js
│ │ │ ├── updateUserCommandHandler.js
│ │ │ ├── deleteUserCommand.js
│ │ │ └── deleteUserCommandHandler.js
│ │ └── query/ # User olvasási műveletek
│ │ ├── getAllUsersQuery.js
│ │ ├── getAllUsersQueryHandler.js
│ │ ├── getUserByIdQuery.js
│ │ └── getUserByIdQueryHandler.js
│ └── blogs/
│ ├── command/ # Post írási műveletek
│ └── query/ # Post olvasási műveletek
├── Domain/
│ ├── IUserRepository.js # User repository interface
│ └── IPostRepository.js # Post repository interface
└── Infrastructure/
├── userRepository.js # User repository implementáció
├── postRepository.js # Post repository implementáció
├── user.json # User adatok
└── post.json # Post adatok
```
## API Endpoints
### Users
- `GET /api/users` - Összes user lekérése
- `GET /api/users/:id` - Egy user lekérése
- `POST /api/users` - Új user létrehozása
- `PUT /api/users/:id` - User módosítása
- `DELETE /api/users/:id` - User törlése
### Posts
- `GET /api/posts` - Összes post lekérése
- `GET /api/posts/:id` - Egy post lekérése
- `GET /api/posts/user/:userId` - User összes postja
- `POST /api/posts` - Új post létrehozása
- `PUT /api/posts/:id` - Post módosítása
- `DELETE /api/posts/:id` - Post törlése
## Indítás
```bash
npm install
npm start
```
A szerver elindul a `http://localhost:3000` címen.
## Példa Használat
### User létrehozása
```bash
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{
"name": "John Doe",
"email": "john@example.com",
"age": 25
}'
```
### Post létrehozása
```bash
curl -X POST http://localhost:3000/api/posts \
-H "Content-Type: application/json" \
-d '{
"title": "My First Post",
"content": "Hello World!",
"author": "John Doe"
}'
```
## Tanulási Pontok
1. **Separation of Concerns**: Minden réteg saját felelősséggel rendelkezik
2. **CQRS**: Írási és olvasási műveletek szétválasztása
3. **Repository Pattern**: Adatkezelés absztrakciója
4. **Dependency Injection**: Handler-ek megkapják a repository-t
5. **Express.js**: RESTful API építése
6. **File-based Storage**: JSON file-ok használata adatbázis helyett
+17
View File
@@ -0,0 +1,17 @@
{
"name": "elso-gyakorlat-minta",
"version": "1.0.0",
"description": "Complete reference implementation - First Exercise",
"main": "index.js",
"type": "module",
"scripts": {
"start": "nodemon src/API/server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"express": "^5.2.1",
"nodemon": "^3.1.11"
}
}
@@ -0,0 +1,92 @@
import { PostRepository } from '../../Infrastructure/postRepository.js';
import { GetAllPostsQuery } from '../../Application/blogs/query/getAllPostsQuery.js';
import { GetAllPostsQueryHandler } from '../../Application/blogs/query/getAllPostsQueryHandler.js';
import { GetPostByIdQuery } from '../../Application/blogs/query/getPostByIdQuery.js';
import { GetPostByIdQueryHandler } from '../../Application/blogs/query/getPostByIdQueryHandler.js';
import { GetPostsByUserIdQuery } from '../../Application/blogs/query/getPostsByUserIdQuery.js';
import { GetPostsByUserIdQueryHandler } from '../../Application/blogs/query/getPostsByUserIdQueryHandler.js';
import { CreatePostCommand } from '../../Application/blogs/command/createPostCommand.js';
import { CreatePostCommandHandler } from '../../Application/blogs/command/createPostCommandHandler.js';
import { UpdatePostCommand } from '../../Application/blogs/command/updatePostCommand.js';
import { UpdatePostCommandHandler } from '../../Application/blogs/command/updatePostCommandHandler.js';
import { DeletePostCommand } from '../../Application/blogs/command/deletePostCommand.js';
import { DeletePostCommandHandler } from '../../Application/blogs/command/deletePostCommandHandler.js';
const postRepository = new PostRepository();
const getAllPostsQueryHandler = new GetAllPostsQueryHandler(postRepository);
const getPostByIdQueryHandler = new GetPostByIdQueryHandler(postRepository);
const getPostsByUserIdQueryHandler = new GetPostsByUserIdQueryHandler(postRepository);
const createPostCommandHandler = new CreatePostCommandHandler(postRepository);
const updatePostCommandHandler = new UpdatePostCommandHandler(postRepository);
const deletePostCommandHandler = new DeletePostCommandHandler(postRepository);
export class PostController {
async getAll(req, res) {
try {
const query = new GetAllPostsQuery();
const posts = await getAllPostsQueryHandler.handle(query);
res.json(posts);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async getById(req, res) {
try {
const query = new GetPostByIdQuery(req.params.id);
const post = await getPostByIdQueryHandler.handle(query);
res.json(post);
} catch (error) {
res.status(404).json({ error: error.message });
}
}
async getByUserId(req, res) {
try {
const query = new GetPostsByUserIdQuery(req.params.userId);
const posts = await getPostsByUserIdQueryHandler.handle(query);
res.json(posts);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async create(req, res) {
try {
const command = new CreatePostCommand(
req.body.title,
req.body.content,
req.body.author
);
const post = await createPostCommandHandler.handle(command);
res.status(201).json(post);
} catch (error) {
res.status(400).json({ error: error.message });
}
}
async update(req, res) {
try {
const command = new UpdatePostCommand(
req.params.id,
req.body.title,
req.body.content,
req.body.author
);
const post = await updatePostCommandHandler.handle(command);
res.json(post);
} catch (error) {
res.status(404).json({ error: error.message });
}
}
async delete(req, res) {
try {
const command = new DeletePostCommand(req.params.id);
await deletePostCommandHandler.handle(command);
res.status(204).send();
} catch (error) {
res.status(404).json({ error: error.message });
}
}
}
@@ -0,0 +1,79 @@
import { UserRepository } from '../../Infrastructure/userRepository.js';
import { GetAllUsersQuery } from '../../Application/users/query/getAllUsersQuery.js';
import { GetAllUsersQueryHandler } from '../../Application/users/query/getAllUsersQueryHandler.js';
import { GetUserByIdQuery } from '../../Application/users/query/getUserByIdQuery.js';
import { GetUserByIdQueryHandler } from '../../Application/users/query/getUserByIdQueryHandler.js';
import { CreateUserCommand } from '../../Application/users/command/createUserCommand.js';
import { CreateUserCommandHandler } from '../../Application/users/command/createUserCommandHandler.js';
import { UpdateUserCommand } from '../../Application/users/command/updateUserCommand.js';
import { UpdateUserCommandHandler } from '../../Application/users/command/updateUserCommandHandler.js';
import { DeleteUserCommand } from '../../Application/users/command/deleteUserCommand.js';
import { DeleteUserCommandHandler } from '../../Application/users/command/deleteUserCommandHandler.js';
const userRepository = new UserRepository();
const getAllUsersQueryHandler = new GetAllUsersQueryHandler(userRepository);
const getUserByIdQueryHandler = new GetUserByIdQueryHandler(userRepository);
const createUserCommandHandler = new CreateUserCommandHandler(userRepository);
const updateUserCommandHandler = new UpdateUserCommandHandler(userRepository);
const deleteUserCommandHandler = new DeleteUserCommandHandler(userRepository);
export class UserController {
async getAll(req, res) {
try {
const query = new GetAllUsersQuery();
const users = await getAllUsersQueryHandler.handle(query);
res.json(users);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async getById(req, res) {
try {
const query = new GetUserByIdQuery(req.params.id);
const user = await getUserByIdQueryHandler.handle(query);
res.json(user);
} catch (error) {
res.status(404).json({ error: error.message });
}
}
async create(req, res) {
try {
const command = new CreateUserCommand(
req.body.name,
req.body.email,
req.body.age
);
const user = await createUserCommandHandler.handle(command);
res.status(201).json(user);
} catch (error) {
res.status(400).json({ error: error.message });
}
}
async update(req, res) {
try {
const command = new UpdateUserCommand(
req.params.id,
req.body.name,
req.body.email,
req.body.age
);
const user = await updateUserCommandHandler.handle(command);
res.json(user);
} catch (error) {
res.status(404).json({ error: error.message });
}
}
async delete(req, res) {
try {
const command = new DeleteUserCommand(req.params.id);
await deleteUserCommandHandler.handle(command);
res.status(204).send();
} catch (error) {
res.status(404).json({ error: error.message });
}
}
}
@@ -0,0 +1,14 @@
import express from 'express';
import { PostController } from '../controllers/postController.js';
const router = express.Router();
const postController = new PostController();
router.get('/', (req, res) => postController.getAll(req, res));
router.get('/:id', (req, res) => postController.getById(req, res));
router.get('/user/:userId', (req, res) => postController.getByUserId(req, res));
router.post('/', (req, res) => postController.create(req, res));
router.put('/:id', (req, res) => postController.update(req, res));
router.delete('/:id', (req, res) => postController.delete(req, res));
export default router;
@@ -0,0 +1,13 @@
import express from 'express';
import { UserController } from '../controllers/userController.js';
const router = express.Router();
const userController = new UserController();
router.get('/', (req, res) => userController.getAll(req, res));
router.get('/:id', (req, res) => userController.getById(req, res));
router.post('/', (req, res) => userController.create(req, res));
router.put('/:id', (req, res) => userController.update(req, res));
router.delete('/:id', (req, res) => userController.delete(req, res));
export default router;
@@ -0,0 +1,36 @@
import express from 'express';
import postRouter from './routers/postRouter.js';
import userRouter from './routers/userRouter.js';
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Routes
app.use('/api/posts', postRouter);
app.use('/api/users', userRouter);
// Root endpoint
app.get('/', (req, res) => {
res.json({
message: 'Welcome to the Blog API - MINTA Megoldás',
endpoints: {
posts: '/api/posts',
users: '/api/users'
}
});
});
// Error handling middleware
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Something went wrong!' });
});
// Start server
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
@@ -0,0 +1,7 @@
export class CreatePostCommand {
constructor(title, content, author) {
this.title = title;
this.content = content;
this.author = author;
}
}
@@ -0,0 +1,19 @@
export class CreatePostCommandHandler {
constructor(postRepository) {
this.postRepository = postRepository;
}
async handle(command) {
if (!command.title || !command.content) {
throw new Error('Title and content are required');
}
const postData = {
title: command.title,
content: command.content,
author: command.author
};
return await this.postRepository.create(postData);
}
}
@@ -0,0 +1,5 @@
export class DeletePostCommand {
constructor(id) {
this.id = id;
}
}
@@ -0,0 +1,12 @@
export class DeletePostCommandHandler {
constructor(postRepository) {
this.postRepository = postRepository;
}
async handle(command) {
const result = await this.postRepository.delete(command.id);
if (!result) {
throw new Error('Post not found');
}
}
}
@@ -0,0 +1,8 @@
export class UpdatePostCommand {
constructor(id, title, content, author) {
this.id = id;
this.title = title;
this.content = content;
this.author = author;
}
}
@@ -0,0 +1,20 @@
export class UpdatePostCommandHandler {
constructor(postRepository) {
this.postRepository = postRepository;
}
async handle(command) {
const postData = {
title: command.title,
content: command.content,
author: command.author
};
const post = await this.postRepository.update(command.id, postData);
if (!post) {
throw new Error('Post not found');
}
return post;
}
}
@@ -0,0 +1,3 @@
export class GetAllPostsQuery {
constructor() {}
}
@@ -0,0 +1,9 @@
export class GetAllPostsQueryHandler {
constructor(postRepository) {
this.postRepository = postRepository;
}
async handle(query) {
return await this.postRepository.getAll();
}
}
@@ -0,0 +1,5 @@
export class GetPostByIdQuery {
constructor(id) {
this.id = id;
}
}
@@ -0,0 +1,13 @@
export class GetPostByIdQueryHandler {
constructor(postRepository) {
this.postRepository = postRepository;
}
async handle(query) {
const post = await this.postRepository.getById(query.id);
if (!post) {
throw new Error('Post not found');
}
return post;
}
}
@@ -0,0 +1,5 @@
export class GetPostsByUserIdQuery {
constructor(userId) {
this.userId = userId;
}
}
@@ -0,0 +1,9 @@
export class GetPostsByUserIdQueryHandler {
constructor(postRepository) {
this.postRepository = postRepository;
}
async handle(query) {
return await this.postRepository.getByUserId(query.userId);
}
}
@@ -0,0 +1,7 @@
export class CreateUserCommand {
constructor(name, email, age) {
this.name = name;
this.email = email;
this.age = age;
}
}
@@ -0,0 +1,19 @@
export class CreateUserCommandHandler {
constructor(userRepository) {
this.userRepository = userRepository;
}
async handle(command) {
if (!command.name || !command.email) {
throw new Error('Name and email are required');
}
const userData = {
name: command.name,
email: command.email,
age: command.age
};
return await this.userRepository.create(userData);
}
}
@@ -0,0 +1,5 @@
export class DeleteUserCommand {
constructor(id) {
this.id = id;
}
}
@@ -0,0 +1,12 @@
export class DeleteUserCommandHandler {
constructor(userRepository) {
this.userRepository = userRepository;
}
async handle(command) {
const result = await this.userRepository.delete(command.id);
if (!result) {
throw new Error('User not found');
}
}
}
@@ -0,0 +1,8 @@
export class UpdateUserCommand {
constructor(id, name, email, age) {
this.id = id;
this.name = name;
this.email = email;
this.age = age;
}
}
@@ -0,0 +1,20 @@
export class UpdateUserCommandHandler {
constructor(userRepository) {
this.userRepository = userRepository;
}
async handle(command) {
const userData = {
name: command.name,
email: command.email,
age: command.age
};
const user = await this.userRepository.update(command.id, userData);
if (!user) {
throw new Error('User not found');
}
return user;
}
}
@@ -0,0 +1,3 @@
export class GetAllUsersQuery {
constructor() {}
}
@@ -0,0 +1,9 @@
export class GetAllUsersQueryHandler {
constructor(userRepository) {
this.userRepository = userRepository;
}
async handle(query) {
return await this.userRepository.getAll();
}
}
@@ -0,0 +1,5 @@
export class GetUserByIdQuery {
constructor(id) {
this.id = id;
}
}
@@ -0,0 +1,13 @@
export class GetUserByIdQueryHandler {
constructor(userRepository) {
this.userRepository = userRepository;
}
async handle(query) {
const user = await this.userRepository.getById(query.id);
if (!user) {
throw new Error('User not found');
}
return user;
}
}
@@ -0,0 +1,25 @@
export class IPostRepository {
async getAll() {
throw new Error('Method not implemented');
}
async getById(id) {
throw new Error('Method not implemented');
}
async getByUserId(userId) {
throw new Error('Method not implemented');
}
async create(post) {
throw new Error('Method not implemented');
}
async update(id, postData) {
throw new Error('Method not implemented');
}
async delete(id) {
throw new Error('Method not implemented');
}
}
@@ -0,0 +1,21 @@
export class IUserRepository {
async getAll() {
throw new Error('Method not implemented');
}
async getById(id) {
throw new Error('Method not implemented');
}
async create(user) {
throw new Error('Method not implemented');
}
async update(id, userData) {
throw new Error('Method not implemented');
}
async delete(id) {
throw new Error('Method not implemented');
}
}
@@ -0,0 +1 @@
[]
@@ -0,0 +1,65 @@
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import { IPostRepository } from '../Domain/IPostRepository.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const POST_FILE = path.join(__dirname, 'post.json');
export class PostRepository extends IPostRepository {
async getAll() {
try {
const data = await fs.readFile(POST_FILE, 'utf-8');
return JSON.parse(data);
} catch (error) {
return [];
}
}
async getById(id) {
const posts = await this.getAll();
return posts.find(post => post.id === id);
}
async getByUserId(userId) {
const posts = await this.getAll();
return posts.filter(post => post.userId === userId);
}
async create(post) {
const posts = await this.getAll();
const newPost = {
id: Date.now().toString(),
...post,
createdAt: new Date().toISOString()
};
posts.push(newPost);
await fs.writeFile(POST_FILE, JSON.stringify(posts, null, 2));
return newPost;
}
async update(id, postData) {
const posts = await this.getAll();
const index = posts.findIndex(post => post.id === id);
if (index === -1) return null;
posts[index] = {
...posts[index],
...postData,
id: posts[index].id,
updatedAt: new Date().toISOString()
};
await fs.writeFile(POST_FILE, JSON.stringify(posts, null, 2));
return posts[index];
}
async delete(id) {
const posts = await this.getAll();
const filteredPosts = posts.filter(post => post.id !== id);
if (posts.length === filteredPosts.length) return false;
await fs.writeFile(POST_FILE, JSON.stringify(filteredPosts, null, 2));
return true;
}
}
@@ -0,0 +1 @@
[]
@@ -0,0 +1,60 @@
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import { IUserRepository } from '../Domain/IUserRepository.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const USER_FILE = path.join(__dirname, 'user.json');
export class UserRepository extends IUserRepository {
async getAll() {
try {
const data = await fs.readFile(USER_FILE, 'utf-8');
return JSON.parse(data);
} catch (error) {
return [];
}
}
async getById(id) {
const users = await this.getAll();
return users.find(user => user.id === id);
}
async create(user) {
const users = await this.getAll();
const newUser = {
id: Date.now().toString(),
...user,
createdAt: new Date().toISOString()
};
users.push(newUser);
await fs.writeFile(USER_FILE, JSON.stringify(users, null, 2));
return newUser;
}
async update(id, userData) {
const users = await this.getAll();
const index = users.findIndex(user => user.id === id);
if (index === -1) return null;
users[index] = {
...users[index],
...userData,
id: users[index].id,
updatedAt: new Date().toISOString()
};
await fs.writeFile(USER_FILE, JSON.stringify(users, null, 2));
return users[index];
}
async delete(id) {
const users = await this.getAll();
const filteredUsers = users.filter(user => user.id !== id);
if (users.length === filteredUsers.length) return false;
await fs.writeFile(USER_FILE, JSON.stringify(filteredUsers, null, 2));
return true;
}
}
+19
View File
@@ -0,0 +1,19 @@
# Database
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/blog_db?schema=public"
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
# 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=3000
NODE_ENV=development
# Cookie
COOKIE_SECRET=your-cookie-secret-change-this
+8
View File
@@ -0,0 +1,8 @@
node_modules/
.env
dist/
.DS_Store
*.log
coverage/
.vscode/
.idea/
+300
View File
@@ -0,0 +1,300 @@
# Feladat Leírás
## 🎯 Cél
Implementálj egy teljes authentication és authorization rendszert egy blog platformhoz JWT token használatával, cookie-alapú session kezeléssel.
---
## ⚠️ MIT KELL IMPLEMENTÁLNI
### 1. AuthController (`src/api/controllers/AuthController.js`)
#### `register(req, res)` ✏️
```javascript
// Input: { email, username, password }
// 1. Validálás (kötelező mezők, formátum)
// 2. Ellenőrizd: email és username unique?
// 3. Hash-eld a jelszót: bcrypt.hash(password, 10)
// 4. Hozd létre a user-t: userRepository.create()
// 5. Generálj JWT tokent: jwt.sign()
// 6. Állíts be HTTP-only cookie-t: res.cookie()
// 7. Válasz: { user (jelszó nélkül!), accessToken, refreshToken }
```
#### `login(req, res)` ✏️
```javascript
// Input: { email vagy username, password }
// 1. Keresd meg a user-t: userRepository.findByEmail() vagy findByUsername()
// 2. Ha nincs user → 401 error
// 3. Ellenőrizd a jelszót: bcrypt.compare(password, user.password)
// 4. Ha nem egyezik → 401 error
// 5. Generálj access és refresh tokent
// 6. Állítsd be a cookie-kat
// 7. Redis session (opcionális): redis.set(`session:${userId}`, ...)
// 8. Válasz: { user, accessToken, refreshToken }
```
#### `logout(req, res)` ✏️
```javascript
// 1. Töröld a cookie-kat: res.clearCookie('accessToken')
// 2. Redis session törlés: redis.del(`session:${userId}`)
// 3. Válasz: { success: true, message: 'Logged out' }
```
#### `refreshToken(req, res)` ✏️
```javascript
// 1. Olvasd ki a refresh tokent: req.cookies.refreshToken
// 2. Validáld: jwt.verify(refreshToken, JWT_REFRESH_SECRET)
// 3. Generálj új access tokent
// 4. Állítsd be az új cookie-t
// 5. Válasz: { accessToken }
```
#### `getCurrentUser(req, res)` ✏️
```javascript
// 1. req.user-ből olvasd ki a user adatokat
// 2. Válasz: { user }
```
---
### 2. AuthMiddleware (`src/api/middlewares/authMiddleware.js`)
#### `authenticateToken(req, res, next)` ✏️
```javascript
// 1. Token kiolvasás:
// - Cookie-ból: req.cookies.accessToken
// - VAGY Header-ből: req.headers.authorization (Bearer token)
// 2. Ha nincs token → 401 Unauthorized
// 3. Validálás: jwt.verify(token, JWT_SECRET)
// 4. User lekérés: userRepository.findById(decoded.userId)
// 5. req.user = user
// 6. next()
```
#### `requireRole(allowedRoles)` ✏️
```javascript
// Higher-order function - visszaad egy middleware-t
return (req, res, next) => {
// 1. Ellenőrizd: req.user létezik?
// 2. Ellenőrizd: allowedRoles.includes(req.user.role)?
// 3. Ha nem → 403 Forbidden
// 4. Ha yes → next()
}
// Használat:
// router.delete('/admin', authenticateToken, requireRole(['ADMIN']), ...)
```
#### `checkOwnership(getResourceOwnerId)` ✏️
```javascript
// Higher-order function
return async (req, res, next) => {
// 1. Szerezd meg a resource owner ID-t:
// const ownerId = await getResourceOwnerId(req)
// 2. Ellenőrizd: req.user.id === ownerId VAGY req.user.role === 'ADMIN'
// 3. Ha nem → 403 Forbidden
// 4. Ha yes → next()
}
// Használat:
// router.put('/blogs/:id', authenticateToken,
// checkOwnership(async (req) => {
// const blog = await blogRepository.findById(req.params.id);
// return blog.authorId;
// }),
// updateController
// );
```
---
### 3. Route-ok védelme
#### `src/api/routes/authRoutes.js` ✏️
```javascript
import { authenticateToken } from '../middlewares/authMiddleware.js';
// Publikus
router.post('/register', ...);
router.post('/login', ...);
router.post('/refresh', ...);
// Védett - add hozzá az authenticateToken middleware-t!
router.post('/logout', authenticateToken, ...);
router.get('/me', authenticateToken, ...);
```
#### `src/api/routes/blogRoutes.js` ✏️
```javascript
import { authenticateToken, checkOwnership } from '../middlewares/authMiddleware.js';
// Publikus
router.get('/', ...);
router.get('/:id', ...);
// Védett - add hozzá a middleware-eket!
router.post('/', authenticateToken, ...);
router.put('/:id', authenticateToken, checkOwnership(...), ...);
router.delete('/:id', authenticateToken, checkOwnership(...), ...);
```
---
## 📋 Ellenőrző Lista
Implementációd kész, ha az alábbiak működnek:
```bash
# 1. 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"
}'
# → 201 Created, user adatok, cookie beállítva
# 2. Bejelentkezés
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "test@test.com",
"password": "Test1234"
}'
# → 200 OK, user adatok, cookie beállítva
# 3. Current user
curl -X GET http://localhost:3000/api/auth/me \
-H "Authorization: Bearer <token>"
# → 200 OK, user adatok
# 4. Blog létrehozás (védett)
curl -X POST http://localhost:3000/api/blogs \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"title": "Test", "content": "Content"}'
# → 201 Created
# 5. Másik user blogja - módosítás TILOS
curl -X PUT http://localhost:3000/api/blogs/<mas_user_blog_id> \
-H "Authorization: Bearer <token>" \
-d '{"title": "Hacked"}'
# → 403 Forbidden
# 6. Saját blog - módosítás OK
curl -X PUT http://localhost:3000/api/blogs/<sajat_blog_id> \
-H "Authorization: Bearer <token>" \
-d '{"title": "Updated"}'
# → 200 OK
# 7. Kijelentkezés
curl -X POST http://localhost:3000/api/auth/logout \
-H "Authorization: Bearer <token>"
# → 200 OK, cookie törölve
```
---
## 🔑 Kulcs Fontosságú Részek
### JWT Token Struktúra
```javascript
{
userId: "uuid...",
email: "user@example.com",
role: "USER",
iat: 1234567890, // issued at
exp: 1234999999 // expires
}
```
### Cookie Options
```javascript
{
httpOnly: true, // JavaScript nem fér hozzá
secure: true, // Csak HTTPS (production)
sameSite: 'strict', // CSRF védelem
maxAge: 7 * 24 * 60 * 60 * 1000 // millisec
}
```
### Környezeti Változók
```env
JWT_SECRET=minimum-32-karakter-hosszu-random-string
JWT_REFRESH_SECRET=egy-masik-32-karakter-hosszu-string
JWT_EXPIRES_IN=7d
JWT_REFRESH_EXPIRES_IN=30d
COOKIE_SECRET=meg-egy-secret
```
---
## ⚡ Gyors Start
```bash
# 1. Telepítés
npm install
# 2. Env fájl
cp .env.example .env
# Szerkeszd a .env fájlt!
# 3. Docker indítás
npm run docker:up
# 4. Migráció
npm run prisma:migrate
npm run prisma:generate
# 5. Szerver indítás
npm run dev
# 6. Implementáld az auth részeket
# - AuthController: register, login, logout, refresh, getCurrentUser
# - authMiddleware: authenticateToken, requireRole, checkOwnership
# - Routes: add hozzá a middleware-eket
# 7. Tesztelés
# Használd a fenti curl parancsokat
```
---
## 🚨 Gyakori Hibák
❌ **Jelszó plain text-ben van tárolva**
✅ Használd: `bcrypt.hash(password, 10)`
❌ **Token nincs validálva**
✅ Használd: `jwt.verify(token, secret)`
❌ **Cookie nem httpOnly**
✅ Állítsd be: `httpOnly: true` a cookie options-ben
❌ **Rossz paraméter sorrend bcrypt.compare()-nál**
✅ Helyes: `bcrypt.compare(plainPassword, hashedPassword)`
❌ **req.user nincs beállítva**
✅ Az authenticateToken middleware-nek kell beállítania!
---
## 🎯 Mit Tanulsz
- ✅ JWT-based authentication
- ✅ Cookie management (httpOnly, secure, sameSite)
- ✅ Password hashing (bcrypt)
- ✅ Middleware pattern
- ✅ Role-based access control (RBAC)
- ✅ Resource ownership validation
- ✅ Clean architecture
- ✅ CQRS pattern
- ✅ Repository pattern
- ✅ Dependency injection
---
**Kezdj neki és sok sikert! 💪**
+752
View File
@@ -0,0 +1,752 @@
# Implementation Hints & Code Snippets
Ez a fájl segítséget nyújt az implementációhoz konkrét kód példákkal.
---
## 🔐 JWT Token Kezelés
### Token Generálás
```javascript
import jwt from 'jsonwebtoken';
function generateAccessToken(user) {
return jwt.sign(
{
userId: user.id,
email: user.email,
role: user.role
},
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN || '7d' }
);
}
function generateRefreshToken(user) {
return jwt.sign(
{
userId: user.id
},
process.env.JWT_REFRESH_SECRET,
{ expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '30d' }
);
}
```
### Token Validálás
```javascript
import jwt from 'jsonwebtoken';
function verifyAccessToken(token) {
try {
return jwt.verify(token, process.env.JWT_SECRET);
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new Error('Token expired');
}
throw new Error('Invalid token');
}
}
function verifyRefreshToken(token) {
try {
return jwt.verify(token, process.env.JWT_REFRESH_SECRET);
} catch (error) {
throw new Error('Invalid refresh token');
}
}
```
### Token Kiolvasása Request-ből
```javascript
function extractToken(req) {
// 1. Először cookie-ból próbálkozunk
if (req.cookies && req.cookies.accessToken) {
return req.cookies.accessToken;
}
// 2. Ha nincs cookie, akkor Authorization header
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
return authHeader.substring(7); // "Bearer " eltávolítása
}
return null;
}
```
---
## 🔒 Jelszó Hash-elés
### Regisztrációnál
```javascript
import bcrypt from 'bcrypt';
async function hashPassword(plainPassword) {
const saltRounds = 10;
return await bcrypt.hash(plainPassword, saltRounds);
}
// Használat:
const hashedPassword = await hashPassword(req.body.password);
const user = await userRepository.create({
email: req.body.email,
username: req.body.username,
password: hashedPassword, // Hash-elt jelszó!
role: 'USER'
});
```
### Bejelentkezésnél
```javascript
import bcrypt from 'bcrypt';
async function verifyPassword(plainPassword, hashedPassword) {
return await bcrypt.compare(plainPassword, hashedPassword);
}
// Használat:
const user = await userRepository.findByEmail(req.body.email);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const isPasswordValid = await verifyPassword(req.body.password, user.password);
if (!isPasswordValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
```
---
## 🍪 Cookie Kezelés
### Cookie Beállítás
```javascript
function setAuthCookies(res, accessToken, refreshToken) {
const cookieOptions = {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict'
};
// Access token - 7 nap
res.cookie('accessToken', accessToken, {
...cookieOptions,
maxAge: 7 * 24 * 60 * 60 * 1000
});
// Refresh token - 30 nap
res.cookie('refreshToken', refreshToken, {
...cookieOptions,
maxAge: 30 * 24 * 60 * 60 * 1000
});
}
// Használat a login()-ban:
const accessToken = generateAccessToken(user);
const refreshToken = generateRefreshToken(user);
setAuthCookies(res, accessToken, refreshToken);
```
### Cookie Törlés
```javascript
function clearAuthCookies(res) {
res.clearCookie('accessToken');
res.clearCookie('refreshToken');
}
// Használat a logout()-ban:
clearAuthCookies(res);
res.json({ success: true, message: 'Logged out successfully' });
```
---
## 📝 Input Validáció
### Email Validáció
```javascript
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
```
### Password Validáció
```javascript
function isValidPassword(password) {
// Minimum 8 karakter, legalább egy szám és egy betű
return password && password.length >= 8;
}
```
### Register Input Validáció
```javascript
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
};
}
// Használat:
const validation = validateRegisterInput(req.body);
if (!validation.isValid) {
return res.status(400).json({ errors: validation.errors });
}
```
---
## 🔴 Redis Session Management
### Session Tárolás
```javascript
import { redis } from '../../infrastructure/database/redis.js';
async function createSession(userId, sessionData) {
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
);
}
// Használat a login()-ban:
await createSession(user.id, {
email: user.email,
role: user.role
});
```
### Session Lekérés
```javascript
async function getSession(userId) {
const sessionKey = `session:${userId}`;
const session = await redis.get(sessionKey);
if (!session) {
return null;
}
return JSON.parse(session);
}
```
### Session Törlés
```javascript
async function deleteSession(userId) {
const sessionKey = `session:${userId}`;
await redis.del(sessionKey);
}
// Használat a logout()-ban:
await deleteSession(req.user.id);
```
---
## 🛡️ AuthController Implementáció
### Register
```javascript
async register(req, res) {
try {
// 1. Input validáció
const validation = validateRegisterInput(req.body);
if (!validation.isValid) {
return res.status(400).json({ errors: validation.errors });
}
const { email, username, password } = req.body;
// 2. Uniqueness ellenőrzés
const existingUser = await this.userRepository.findByEmail(email);
if (existingUser) {
return res.status(409).json({ error: 'Email already exists' });
}
const existingUsername = await this.userRepository.findByUsername(username);
if (existingUsername) {
return res.status(409).json({ error: 'Username already exists' });
}
// 3. Jelszó hash-elés
const hashedPassword = await bcrypt.hash(password, 10);
// 4. User létrehozás
const user = await this.userRepository.create({
email,
username,
password: hashedPassword,
role: 'USER'
});
// 5. Token generálás
const accessToken = generateAccessToken(user);
const refreshToken = generateRefreshToken(user);
// 6. Cookie beállítás
setAuthCookies(res, accessToken, refreshToken);
// 7. Válasz
res.status(201).json({
success: true,
data: {
user: user.toPublicObject(),
accessToken,
refreshToken
}
});
} catch (error) {
res.status(500).json({ error: error.message });
}
}
```
### Login
```javascript
async login(req, res) {
try {
const { email, username, password } = req.body;
// Input validáció
if (!password || (!email && !username)) {
return res.status(400).json({
error: 'Email/username and password required'
});
}
// User keresés
let user;
if (email) {
user = await this.userRepository.findByEmail(email);
} else {
user = await this.userRepository.findByUsername(username);
}
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Jelszó ellenőrzés
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Token generálás
const accessToken = generateAccessToken(user);
const refreshToken = generateRefreshToken(user);
// Cookie beállítás
setAuthCookies(res, accessToken, refreshToken);
// Redis session (opcionális)
await createSession(user.id, {
email: user.email,
role: user.role
});
res.json({
success: true,
data: {
user: user.toPublicObject(),
accessToken,
refreshToken
}
});
} catch (error) {
res.status(500).json({ error: error.message });
}
}
```
### Logout
```javascript
async logout(req, res) {
try {
// Redis session törlés
if (req.user && req.user.id) {
await deleteSession(req.user.id);
}
// Cookie törlés
clearAuthCookies(res);
res.json({
success: true,
message: 'Logged out successfully'
});
} catch (error) {
res.status(500).json({ error: error.message });
}
}
```
### Refresh Token
```javascript
async refreshToken(req, res) {
try {
// Refresh token kiolvasása
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh token required' });
}
// Token validálás
const decoded = verifyRefreshToken(refreshToken);
// User lekérés
const user = await this.userRepository.findById(decoded.userId);
if (!user) {
return res.status(401).json({ error: 'User not found' });
}
// Új access token generálás
const newAccessToken = generateAccessToken(user);
// Cookie frissítés
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) {
res.status(401).json({ error: 'Invalid refresh token' });
}
}
```
### Get Current User
```javascript
async getCurrentUser(req, res) {
try {
// req.user az authenticateToken middleware állítja be
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
res.json({
success: true,
data: {
user: req.user.toPublicObject()
}
});
} catch (error) {
res.status(500).json({ error: error.message });
}
}
```
---
## 🛡️ AuthMiddleware Implementáció
### authenticateToken
```javascript
export async function authenticateToken(req, res, next) {
try {
// 1. Token kiolvasása
const token = extractToken(req);
if (!token) {
return res.status(401).json({
error: 'Access token required'
});
}
// 2. Token validálás
const decoded = verifyAccessToken(token);
// 3. User betöltése (FONTOS: ezt implementálni kell!)
// Ehhez kell egy userRepository instance
// Lehetőség 1: req.app.locals.userRepository
// Lehetőség 2: Singleton pattern
// Lehetőség 3: Middleware factory function
const userRepository = req.app.locals.userRepository;
const user = await userRepository.findById(decoded.userId);
if (!user) {
return res.status(401).json({ error: 'User not found' });
}
// 4. req.user beállítása
req.user = user;
// 5. Következő middleware
next();
} catch (error) {
return res.status(401).json({
error: 'Invalid or expired token'
});
}
}
```
**FONTOS:** A userRepository-t be kell injektálni a middleware-be!
Megoldás az index.js-ben:
```javascript
// index.js-ben
app.locals.userRepository = userRepository;
```
### requireRole
```javascript
export function requireRole(allowedRoles) {
return (req, res, next) => {
// Az authenticateToken után fut, tehát req.user létezik
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (!allowedRoles.includes(req.user.role)) {
return res.status(403).json({
error: 'Insufficient permissions',
required: allowedRoles,
current: req.user.role
});
}
next();
};
}
// Használat:
// router.delete('/users/:id', authenticateToken, requireRole(['ADMIN']), ...)
```
### checkOwnership
```javascript
export function checkOwnership(getResourceOwnerId) {
return async (req, res, next) => {
try {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
// Resource owner ID megszerzése
const ownerId = await getResourceOwnerId(req);
// Admin mindent módosíthat
if (req.user.role === 'ADMIN') {
return next();
}
// Ownership ellenőrzés
if (req.user.id !== ownerId) {
return res.status(403).json({
error: 'You can only modify your own resources'
});
}
next();
} catch (error) {
return res.status(403).json({ error: 'Access denied' });
}
};
}
// Használat blogRoutes.js-ben:
router.put('/:id',
authenticateToken,
checkOwnership(async (req) => {
const blogRepository = req.app.locals.blogRepository;
const blog = await blogRepository.findById(req.params.id);
if (!blog) {
throw new Error('Blog not found');
}
return blog.authorId;
}),
(req, res) => blogController.updateBlog(req, res)
);
```
---
## 🔗 Route Protection Példák
### authRoutes.js
```javascript
import express from 'express';
import { authenticateToken } from '../middlewares/authMiddleware.js';
export function createAuthRoutes(authController) {
const router = express.Router();
// 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
router.post('/logout', authenticateToken, (req, res) =>
authController.logout(req, res)
);
router.get('/me', authenticateToken, (req, res) =>
authController.getCurrentUser(req, res)
);
return router;
}
```
### blogRoutes.js
```javascript
import express from 'express';
import { authenticateToken, checkOwnership } from '../middlewares/authMiddleware.js';
export function createBlogRoutes(blogController) {
const router = express.Router();
// Publikus route-ok
router.get('/', (req, res) => blogController.getAllBlogs(req, res));
router.get('/:id', (req, res) => blogController.getBlog(req, res));
// Védett route-ok
router.post('/',
authenticateToken,
(req, res) => blogController.createBlog(req, res)
);
router.put('/:id',
authenticateToken,
checkOwnership(async (req) => {
const blogRepository = req.app.locals.blogRepository;
const blog = await blogRepository.findById(req.params.id);
return blog?.authorId;
}),
(req, res) => blogController.updateBlog(req, res)
);
router.delete('/:id',
authenticateToken,
checkOwnership(async (req) => {
const blogRepository = req.app.locals.blogRepository;
const blog = await blogRepository.findById(req.params.id);
return blog?.authorId;
}),
(req, res) => blogController.deleteBlog(req, res)
);
return router;
}
```
---
## 🔧 index.js Módosítások
```javascript
// Repository-k injektálása app.locals-ba
app.locals.userRepository = userRepository;
app.locals.blogRepository = blogRepository;
// Így a middleware-ek hozzáférhetnek:
// const userRepository = req.app.locals.userRepository;
```
---
## 🧪 Tesztelési Példák
### Regisztráció
```bash
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "alice@example.com",
"username": "alice",
"password": "Alice1234"
}'
```
### Bejelentkezés
```bash
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-c cookies.txt \
-d '{
"email": "alice@example.com",
"password": "Alice1234"
}'
```
### Védett endpoint (cookie-val)
```bash
curl -X GET http://localhost:3000/api/auth/me \
-b cookies.txt
```
### Védett endpoint (Bearer token-nel)
```bash
curl -X GET http://localhost:3000/api/auth/me \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
```
---
**Ez a hints fájl minden szükséges kódrészletet tartalmaz a sikeres implementációhoz! 🚀**
+658
View File
@@ -0,0 +1,658 @@
# Backend Fejlesztés Gyakorló Feladat
## Authentication & Authorization Implementáció
---
## 📋 Tartalom
1. [Projekt Áttekintés](#projekt-áttekintés)
2. [Architektúra](#architektúra)
3. [Előkészületek](#előkészületek)
4. [A Feladat](#a-feladat)
5. [Implementációs Útmutató](#implementációs-útmutató)
6. [Tesztelés](#tesztelés)
7. [Hasznos Források](#hasznos-források)
---
## 🎯 Projekt Áttekintés
Ez egy gyakorló projekt backend fejlesztők számára, amely az **Authentication** és **Authorization** implementálására fókuszál. A projekt egy blog platformot szimulál, ahol a felhasználók regisztrálhatnak, bejelentkezhetnek, és blogokat írhatnak.
### Technológiai Stack
- **Node.js** + **Express.js** - Backend framework
- **Prisma ORM** - Database ORM
- **PostgreSQL** - Relációs adatbázis
- **Redis** - Cache és session management
- **JWT** - Token-based authentication
- **bcrypt** - Jelszó hash-elés
- **Cookie-parser** - Cookie kezelés
- **Docker** - Konténerizáció
### Amit MEG KELL implementálni (FELADAT):
✅ Authentication (Hitelesítés)
- Regisztráció
- Bejelentkezés
- Kijelentkezés
- Token refresh
- Current user lekérés
✅ Authorization (Jogosultság kezelés)
- Authentication middleware
- Role-based access control (RBAC)
- Resource ownership ellenőrzés
### Amit NEM kell implementálni (már kész):
✅ Prisma schema és migrációk
✅ Repository pattern implementáció
✅ CQRS command/query handlers
✅ Blog CRUD műveletek
✅ Docker konfiguráció
✅ Projekt struktúra
---
## 🏗️ Architektúra
A projekt **Clean Architecture** és **CQRS** mintákat követ:
```
src/
├── domain/ # Domain réteg (business logic)
│ ├── models/ # Domain modellek
│ │ ├── User.js
│ │ └── Blog.js
│ └── repositories/ # Repository interfészek
│ ├── IUserRepository.js
│ └── IBlogRepository.js
├── application/ # Application réteg (use cases)
│ ├── commands/ # Command objektumok
│ │ ├── CreateBlogCommand.js
│ │ ├── UpdateBlogCommand.js
│ │ └── DeleteBlogCommand.js
│ ├── queries/ # Query objektumok
│ │ └── BlogQueries.js
│ └── handlers/ # Command/Query handlerek
│ ├── CreateBlogHandler.js
│ ├── UpdateBlogHandler.js
│ ├── DeleteBlogHandler.js
│ └── BlogQueryHandler.js
├── infrastructure/ # Infrastructure réteg (külső függőségek)
│ ├── database/ # Database kapcsolatok
│ │ ├── prisma.js
│ │ └── redis.js
│ ├── repositories/ # Repository implementációk
│ │ ├── UserRepository.js
│ │ └── BlogRepository.js
│ └── config/ # Konfigurációk
│ └── index.js
├── api/ # API réteg (HTTP)
│ ├── controllers/ # Controller-ek
│ │ ├── AuthController.js # ⚠️ IMPLEMENTÁLANDÓ
│ │ └── BlogController.js
│ ├── routes/ # Route definíciók
│ │ ├── authRoutes.js
│ │ ├── blogRoutes.js
│ │ └── userRoutes.js
│ └── middlewares/ # Middleware-ek
│ ├── authMiddleware.js # ⚠️ IMPLEMENTÁLANDÓ
│ └── errorHandler.js
└── index.js # Alkalmazás belépési pont
```
### Rétegek Felelősségei
**Domain réteg:**
- Domain modellek és üzleti logika
- Repository interfészek (dependency inversion)
**Application réteg:**
- Use case-ek implementációja
- Command/Query objektumok és handlerek
- Validációs logika
**Infrastructure réteg:**
- Külső rendszerek integrációja (DB, Redis)
- Repository implementációk
- Konfigurációk
**API réteg:**
- HTTP kérések kezelése
- Routing
- Middleware-ek
- Authentication & Authorization ⚠️
---
## 🚀 Előkészületek
### 1. Függőségek telepítése
```bash
npm install
```
### 2. Környezeti változók beállítása
Másold le a `.env.example` fájlt `.env` néven:
```bash
cp .env.example .env
```
Állítsd be a környezeti változókat `.env` fájlban:
```env
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/blog_db?schema=public"
REDIS_HOST=localhost
REDIS_PORT=6379
JWT_SECRET=valami-nagyon-titkos-kulcs-ide
JWT_EXPIRES_IN=7d
JWT_REFRESH_SECRET=masik-nagyon-titkos-kulcs-ide
JWT_REFRESH_EXPIRES_IN=30d
PORT=3000
NODE_ENV=development
COOKIE_SECRET=cookie-titkos-kulcs-ide
```
⚠️ **FONTOS:** Production környezetben használj erős, random generált kulcsokat!
### 3. Docker konténerek indítása
```bash
npm run docker:up
```
Ez elindítja a PostgreSQL és Redis konténereket.
### 4. Adatbázis migráció
```bash
npm run prisma:migrate
npm run prisma:generate
```
### 5. Projekt indítása (development)
```bash
npm run dev
```
A szerver elindul a `http://localhost:3000` címen.
Ellenőrizd a health endpoint-ot:
```bash
curl http://localhost:3000/health
```
---
## 📝 A Feladat
A feladatod **három fő komponens** implementálása:
### 1️⃣ AuthController implementálása
**Fájl:** `src/api/controllers/AuthController.js`
Implementálandó metódusok:
#### `register(req, res)`
- Új felhasználó regisztrációja
- Input validáció (email, username, password)
- Uniqueness ellenőrzés (email és username)
- Jelszó hash-elés `bcrypt`-tel
- User létrehozása repository-n keresztül
- JWT token generálás
- HTTP-only cookie beállítás
- User publikus adatainak visszaadása (jelszó nélkül!)
#### `login(req, res)`
- Bejelentkezés email vagy username alapján
- User keresése repository-val
- Jelszó ellenőrzés `bcrypt.compare()`-val
- Access és refresh token generálás
- Cookie-k beállítása (httpOnly, secure, sameSite)
- Opcionális: Session tárolás Redis-ben
- Sikeres bejelentkezés esetén user adatok és tokenek visszaadása
#### `logout(req, res)`
- Cookie-k törlése
- Redis session törlése (ha van)
- Refresh token invalidálás
#### `refreshToken(req, res)`
- Refresh token kiolvasása cookie-ból
- Token validálás
- Új access token generálás
- Új cookie beállítás
#### `getCurrentUser(req, res)`
- Bejelentkezett user adatainak visszaadása
- `req.user` alapján (amit az auth middleware állít be)
---
### 2️⃣ AuthMiddleware-ek implementálása
**Fájl:** `src/api/middlewares/authMiddleware.js`
#### `authenticateToken(req, res, next)`
Ez a middleware **minden védett route előtt** fut.
**Feladatai:**
1. JWT token kiolvasása:
- Cookie-ból: `req.cookies.accessToken`
- VAGY Authorization header-ből: `Bearer <token>`
2. Token validálás `jwt.verify()`-val
3. User ID kinyerése a token payload-ból
4. User betöltése repository-val
5. `req.user` beállítása a user objektummal
6. `next()` hívása
**Hibakezelés:**
- Ha nincs token → 401 Unauthorized
- Ha érvénytelen token → 401 Unauthorized
- Ha user nem létezik → 401 Unauthorized
#### `requireRole(allowedRoles)`
Ez egy **higher-order middleware**, ami role-based access control-t implementál.
**Feladatai:**
1. Visszaad egy middleware függvényt
2. Ellenőrzi, hogy `req.user` létezik (az `authenticateToken` után fut!)
3. Ellenőrzi, hogy `req.user.role` benne van-e az `allowedRoles` tömbben
4. Ha nincs jogosultság → 403 Forbidden
5. Ha van jogosultság → `next()`
**Példa használat:**
```javascript
router.delete('/admin/users/:id',
authenticateToken,
requireRole(['ADMIN']),
deleteUserController
);
```
#### `checkOwnership(getResourceOwnerId)`
Ez a middleware **resource ownership** ellenőrzést implementál.
**Feladatai:**
1. Visszaad egy middleware függvényt
2. Meghívja a `getResourceOwnerId` függvényt, hogy megszerezze a resource tulajdonos ID-ját
3. Ellenőrzi, hogy:
- `req.user.id === ownerId` (a user a tulajdonos)
- VAGY `req.user.role === 'ADMIN'` (ADMIN mindent módosíthat)
4. Ha egyik sem teljesül → 403 Forbidden
5. Ha OK → `next()`
**Példa használat:**
```javascript
router.put('/blogs/:id',
authenticateToken,
checkOwnership(async (req) => {
const blog = await blogRepository.findById(req.params.id);
return blog.authorId;
}),
(req, res) => blogController.updateBlog(req, res)
);
```
---
### 3️⃣ Middleware-ek integrálása a route-okba
**Fájlok:**
- `src/api/routes/authRoutes.js`
- `src/api/routes/blogRoutes.js`
Add hozzá az `authenticateToken` middleware-t a védett endpoint-okhoz:
**authRoutes.js:**
```javascript
router.post('/logout', authenticateToken, (req, res) => authController.logout(req, res));
router.get('/me', authenticateToken, (req, res) => authController.getCurrentUser(req, res));
```
**blogRoutes.js:**
```javascript
router.post('/', authenticateToken, (req, res) => blogController.createBlog(req, res));
router.put('/:id', authenticateToken, checkOwnership(...), (req, res) => blogController.updateBlog(req, res));
router.delete('/:id', authenticateToken, checkOwnership(...), (req, res) => blogController.deleteBlog(req, res));
```
---
## 🛠️ Implementációs Útmutató
### JWT Token Generálás
```javascript
import jwt from 'jsonwebtoken';
// Access token generálás
const accessToken = jwt.sign(
{
userId: user.id,
email: user.email,
role: user.role
},
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN }
);
// Refresh token generálás
const refreshToken = jwt.sign(
{ userId: user.id },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: process.env.JWT_REFRESH_EXPIRES_IN }
);
```
### JWT Token Validálás
```javascript
import jwt from 'jsonwebtoken';
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// decoded.userId, decoded.email, decoded.role
} catch (error) {
// Token érvénytelen vagy lejárt
throw new Error('Invalid token');
}
```
### Jelszó Hash-elés
```javascript
import bcrypt from 'bcrypt';
// Hash-elés (regisztrációnál)
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds);
// Ellenőrzés (bejelentkezésnél)
const isValid = await bcrypt.compare(password, user.password);
```
### Cookie Beállítás
```javascript
// Access token cookie
res.cookie('accessToken', accessToken, {
httpOnly: true, // JavaScript nem férhet hozzá
secure: process.env.NODE_ENV === 'production', // Csak HTTPS-en
sameSite: 'strict', // CSRF védelem
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 nap
});
// Refresh token cookie
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 nap
});
```
### Cookie Törlés
```javascript
res.clearCookie('accessToken');
res.clearCookie('refreshToken');
```
### Redis Session Tárolás (Opcionális)
```javascript
import { redis } from '../infrastructure/database/redis.js';
// Session mentése
await redis.set(
`session:${userId}`,
JSON.stringify({ userId, email, loginTime: new Date() }),
'EX',
60 * 60 * 24 * 7 // 7 nap TTL
);
// Session lekérése
const session = await redis.get(`session:${userId}`);
const sessionData = JSON.parse(session);
// Session törlése
await redis.del(`session:${userId}`);
```
### Token Kiolvasás Cookie-ból vagy Header-ből
```javascript
function getTokenFromRequest(req) {
// Cookie-ból
if (req.cookies && req.cookies.accessToken) {
return req.cookies.accessToken;
}
// Authorization header-ből
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
return authHeader.substring(7);
}
return null;
}
```
---
## 🧪 Tesztelés
### 1. Regisztráció
```bash
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"username": "testuser",
"password": "SecurePassword123"
}'
```
**Elvárt válasz:**
```json
{
"success": true,
"data": {
"user": {
"id": "uuid...",
"email": "test@example.com",
"username": "testuser",
"role": "USER"
},
"accessToken": "eyJhbGc...",
"refreshToken": "eyJhbGc..."
}
}
```
### 2. Bejelentkezés
```bash
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "SecurePassword123"
}'
```
### 3. Current User
```bash
curl -X GET http://localhost:3000/api/auth/me \
-H "Authorization: Bearer <access_token>"
```
VAGY cookie-val:
```bash
curl -X GET http://localhost:3000/api/auth/me \
--cookie "accessToken=<token>"
```
### 4. Blog Létrehozás (Védett endpoint)
```bash
curl -X POST http://localhost:3000/api/blogs \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <access_token>" \
-d '{
"title": "My First Blog",
"content": "This is awesome!",
"published": true
}'
```
### 5. Blog Módosítás (Ownership ellenőrzéssel)
```bash
curl -X PUT http://localhost:3000/api/blogs/<blog_id> \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <access_token>" \
-d '{
"title": "Updated Title"
}'
```
### 6. Kijelentkezés
```bash
curl -X POST http://localhost:3000/api/auth/logout \
-H "Authorization: Bearer <access_token>"
```
---
## ✅ Ellenőrző Lista
Implementációd akkor teljes, ha:
- [ ] ✅ Sikeres regisztráció email + username + password-dal
- [ ] ✅ Email és username uniqueness ellenőrzés működik
- [ ] ✅ Jelszó hash-elve van tárolva (bcrypt)
- [ ] ✅ Sikeres bejelentkezés
- [ ] ✅ Rossz jelszóval nem lehet bejelentkezni
- [ ] ✅ JWT token generálódik és cookie-ban tárolódik
- [ ] ✅ AuthMiddleware validálja a tokent
- [ ] ✅ Védett endpoint-ok csak token-nal elérhetők
- [ ] ✅ `/api/auth/me` visszaadja a bejelentkezett user adatait
- [ ] ✅ Kijelentkezés törli a cookie-kat
- [ ] ✅ Token refresh működik
- [ ] ✅ Role-based access control működik (ADMIN vs USER)
- [ ] ✅ Ownership check működik (csak saját blog módosítható)
- [ ] ✅ Hibakezelés 401/403 válaszokkal
---
## 📚 Hasznos Források
### Dokumentációk
- [JWT](https://jwt.io/) - JSON Web Token
- [bcrypt](https://www.npmjs.com/package/bcrypt) - Password hashing
- [Prisma](https://www.prisma.io/docs) - ORM dokumentáció
- [Express](https://expressjs.com/) - Web framework
- [cookie-parser](https://www.npmjs.com/package/cookie-parser) - Cookie middleware
### Tananyagok
- [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html)
- [JWT Best Practices](https://blog.logrocket.com/jwt-authentication-best-practices/)
- [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
- [CQRS Pattern](https://martinfowler.com/bliki/CQRS.html)
---
## 🐛 Gyakori Hibák és Megoldások
### "JWT must be provided"
- Ellenőrizd, hogy a token szerepel-e a cookie-ban vagy Authorization header-ben
- Ellenőrizd a cookie nevét (`accessToken`)
### "Invalid token"
- Ellenőrizd a JWT_SECRET változót
- Lehet, hogy a token lejárt - próbálj újra bejelentkezni
### "User already exists"
- Email vagy username már foglalt
- Használj egyedi értékeket
### "Incorrect password"
- Ellenőrizd a bcrypt.compare() használatát
- A paraméterek sorrendje: `bcrypt.compare(plainPassword, hashedPassword)`
### "403 Forbidden"
- Nincs jogosultságod az adott művelethez
- Ellenőrizd a role-t vagy az ownership-et
---
## 🎓 Értékelési Szempontok
A feladat sikeres megoldása:
1. **Működőképesség (40%)**
- Regisztráció működik
- Bejelentkezés/kijelentkezés működik
- Védett endpoint-ok hozzáférés-védelme
2. **Biztonság (30%)**
- Jelszavak hash-elve vannak
- HTTP-only cookie-k használata
- JWT token biztonságos kezelése
- Input validáció
3. **Kódminőség (20%)**
- Letisztult, olvasható kód
- Megfelelő error handling
- Architektúra követése
4. **Extra funkciók (10%)**
- Redis session management
- Token refresh flow
- Role-based access control
- Ownership validation
---
## 💡 Tippek
1. **Kezdd a legegyszerűbbel:** Először implementáld a regisztrációt, aztán a logint.
2. **Tesztelj folyamatosan:** Minden metódus után tesztelj curl-lel vagy Postman-nel.
3. **Nézd meg a meglévő kódot:** A BlogController egy jó példa, hogyan kell használni a repository-kat és handler-eket.
4. **Debug logging:** Használj `console.log()`-ot fejlesztés közben, hogy lásd mi történik.
5. **Hibakezelés:** Minden async művelet legyen try-catch blokkban.
6. **Token lejárat:** Fejlesztés közben használj rövid lejárati időt (pl. 15 perc), hogy tesztelhesd a refresh flow-t.
---
**Jó munkát és kellemes kódolást! 🚀**
@@ -0,0 +1,38 @@
version: '3.8'
services:
postgres:
image: postgres:15-alpine
container_name: blog_postgres
restart: unless-stopped
ports:
- "5432: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
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data:
redis_data:
+32
View File
@@ -0,0 +1,32 @@
{
"name": "blog-auth-practice",
"version": "1.0.0",
"description": "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,160 @@
/**
* AuthController
*
* FELADAT: Implementáld az alábbi metódusokat!
*
* Ez a controller felelős az authentication és authorization kezeléséért.
* A következő funkciók implementálása szükséges:
*
* 1. register() - Felhasználó regisztráció
* - Email és username uniqueness ellenőrzés
* - Jelszó hash-elés bcrypt-tel
* - User létrehozása a repository-n keresztül
* - JWT token generálás
* - Cookie beállítás
*
* 2. login() - Bejelentkezés
* - User keresése email vagy username alapján
* - Jelszó ellenőrzés bcrypt.compare()-val
* - JWT token generálás (access és refresh token)
* - Cookie-k beállítása
* - Redis-ben session tárolás (opcionális)
*
* 3. logout() - Kijelentkezés
* - Cookie-k törlése
* - Redis session törlése
*
* 4. refreshToken() - Token frissítés
* - Refresh token validálás
* - Új access token generálás
*
* 5. getCurrentUser() - Bejelentkezett user adatai
* - req.user alapján user visszaadása
*/
export class AuthController {
constructor(userRepository) {
this.userRepository = userRepository;
}
/**
* POST /api/auth/register
* Új felhasználó regisztrációja
*/
async register(req, res) {
try {
// TODO: Implementáld a regisztrációt
// 1. Validáld az input adatokat (email, username, password)
// 2. Ellenőrizd, hogy létezik-e már a user (email vagy username)
// 3. Hash-eld a jelszót bcrypt-tel
// 4. Hozd létre a user-t a repository-n keresztül
// 5. Generálj JWT tokent
// 6. Állítsd be a cookie-kat
// 7. Küldd vissza a user adatokat (jelszó nélkül!)
res.status(501).json({
success: false,
error: 'Register not implemented yet - this is your task!'
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message
});
}
}
/**
* POST /api/auth/login
* Bejelentkezés
*/
async login(req, res) {
try {
// TODO: Implementáld a login-t
// 1. Keresd meg a user-t email vagy username alapján
// 2. Ellenőrizd a jelszót bcrypt.compare()-val
// 3. Generálj JWT access és refresh tokent
// 4. Állítsd be a cookie-kat (httpOnly, secure)
// 5. Opcionálisan tárolj session-t Redis-ben
// 6. Küldd vissza a user adatokat és tokeneket
res.status(501).json({
success: false,
error: 'Login not implemented yet - this is your task!'
});
} catch (error) {
res.status(401).json({
success: false,
error: error.message
});
}
}
/**
* POST /api/auth/logout
* Kijelentkezés
*/
async logout(req, res) {
try {
// TODO: Implementáld a logout-ot
// 1. Töröld a cookie-kat
// 2. Töröld a Redis session-t (ha van)
// 3. Invalidáld a refresh tokent
res.status(501).json({
success: false,
error: 'Logout not implemented yet - this is your task!'
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message
});
}
}
/**
* POST /api/auth/refresh
* Access token frissítése refresh token alapján
*/
async refreshToken(req, res) {
try {
// TODO: Implementáld a token refresh-t
// 1. Olvasd ki a refresh tokent a cookie-ból
// 2. Validáld a refresh tokent
// 3. Generálj új access tokent
// 4. Állítsd be az új cookie-t
res.status(501).json({
success: false,
error: 'Refresh token not implemented yet - this is your task!'
});
} catch (error) {
res.status(401).json({
success: false,
error: error.message
});
}
}
/**
* GET /api/auth/me
* Bejelentkezett felhasználó adatai
*/
async getCurrentUser(req, res) {
try {
// TODO: Implementáld a current user lekérést
// 1. Olvasd ki a user-t req.user-ből (amit az auth middleware állít be)
// 2. Küldd vissza a user publikus adatait
res.status(501).json({
success: false,
error: 'Get current user not implemented yet - this is your task!'
});
} catch (error) {
res.status(401).json({
success: false,
error: error.message
});
}
}
}
@@ -0,0 +1,179 @@
import { CreateBlogCommand } from '../../application/commands/CreateBlogCommand.js';
import { UpdateBlogCommand } from '../../application/commands/UpdateBlogCommand.js';
import { DeleteBlogCommand } from '../../application/commands/DeleteBlogCommand.js';
import { GetBlogQuery, GetAllBlogsQuery, GetUserBlogsQuery } from '../../application/queries/BlogQueries.js';
/**
* BlogController
* Blog műveletek kezelése
*/
export class BlogController {
constructor(createHandler, updateHandler, deleteHandler, queryHandler) {
this.createHandler = createHandler;
this.updateHandler = updateHandler;
this.deleteHandler = deleteHandler;
this.queryHandler = queryHandler;
}
/**
* Blog létrehozása
* POST /api/blogs
*/
async createBlog(req, res) {
try {
// FIGYELEM: req.user-t az auth middleware-nek kell beállítania
// Ez a feladat része - implementálandó!
const authorId = req.user?.id;
if (!authorId) {
return res.status(401).json({ error: 'Authentication required' });
}
const command = new CreateBlogCommand({
...req.body,
authorId
});
const blog = await this.createHandler.handle(command);
res.status(201).json({
success: true,
data: blog.toObject()
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message
});
}
}
/**
* Blog módosítása
* PUT /api/blogs/:id
*/
async updateBlog(req, res) {
try {
const userId = req.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Authentication required' });
}
const command = new UpdateBlogCommand(req.params.id, req.body);
// FIGYELEM: Authorization ellenőrzés szükséges!
// Csak a szerző vagy admin módosíthatja
// Ez a feladat része - implementálandó!
const blog = await this.updateHandler.handle(command);
res.json({
success: true,
data: blog.toObject()
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message
});
}
}
/**
* Blog törlése
* DELETE /api/blogs/:id
*/
async deleteBlog(req, res) {
try {
const userId = req.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Authentication required' });
}
const command = new DeleteBlogCommand(req.params.id, userId);
const result = await this.deleteHandler.handle(command);
res.json({
success: true,
message: result.message
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message
});
}
}
/**
* Egy blog lekérése
* GET /api/blogs/:id
*/
async getBlog(req, res) {
try {
const query = new GetBlogQuery(req.params.id);
const blog = await this.queryHandler.handleGetBlog(query);
res.json({
success: true,
data: blog.toObject()
});
} catch (error) {
res.status(404).json({
success: false,
error: error.message
});
}
}
/**
* Összes blog lekérése
* GET /api/blogs
*/
async getAllBlogs(req, res) {
try {
const query = new GetAllBlogsQuery({
publishedOnly: req.query.published === 'true',
limit: req.query.limit ? parseInt(req.query.limit) : undefined,
offset: req.query.offset ? parseInt(req.query.offset) : 0
});
const blogs = await this.queryHandler.handleGetAllBlogs(query);
res.json({
success: true,
data: blogs.map(blog => blog.toObject()),
count: blogs.length
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message
});
}
}
/**
* User blogjai
* GET /api/users/:userId/blogs
*/
async getUserBlogs(req, res) {
try {
const query = new GetUserBlogsQuery(req.params.userId);
const blogs = await this.queryHandler.handleGetUserBlogs(query);
res.json({
success: true,
data: blogs.map(blog => blog.toObject()),
count: blogs.length
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message
});
}
}
}
@@ -0,0 +1,112 @@
/**
* Authentication Middleware
*
* FELADAT: Implementáld az authentication middleware-t!
*
* Ez a middleware felelős azért, hogy:
* 1. Ellenőrizze a JWT tokent (cookie-ból vagy Authorization header-ből)
* 2. Validálja a tokent
* 3. Beállítsa a req.user objektumot a dekódolt token alapján
* 4. Hiba esetén 401 Unauthorized választ küldjön
*
* Példa használat:
* router.get('/protected', authenticateToken, (req, res) => {
* res.json({ user: req.user });
* });
*/
export function authenticateToken(req, res, next) {
try {
// TODO: Implementáld az authentication-t
// 1. Olvasd ki a tokent a cookie-ból vagy Authorization header-ből
// 2. Validáld a tokent jwt.verify()-val
// 3. Állítsd be req.user-t a dekódolt token payload-jából
// 4. Hívd meg a next()-et, hogy tovább menjen a request
// Ideiglenes: mindig hiba, amíg nincs implementálva
return res.status(401).json({
success: false,
error: 'Authentication middleware not implemented yet - this is your task!'
});
} catch (error) {
return res.status(401).json({
success: false,
error: 'Invalid or expired token'
});
}
}
/**
* Authorization Middleware - Role Check
*
* FELADAT: Implementáld a role-based authorization middleware-t!
*
* Ez a middleware ellenőrzi, hogy a bejelentkezett usernek van-e megfelelő role-ja.
*
* Példa használat:
* router.delete('/admin/users/:id', authenticateToken, requireRole(['ADMIN']), (req, res) => {
* // Csak ADMIN role-lal lehet törölni
* });
*/
export function requireRole(allowedRoles) {
return (req, res, next) => {
try {
// TODO: Implementáld a role check-et
// 1. Ellenőrizd, hogy req.user létezik-e (authenticateToken után fut)
// 2. Ellenőrizd, hogy req.user.role benne van-e az allowedRoles tömbben
// 3. Ha nincs jogosultság, küldj 403 Forbidden választ
// 4. Ha van jogosultság, hívd meg a next()-et
return res.status(403).json({
success: false,
error: 'Authorization middleware not implemented yet - this is your task!'
});
} catch (error) {
return res.status(403).json({
success: false,
error: 'Insufficient permissions'
});
}
};
}
/**
* Resource Owner Check
*
* FELADAT: Implementáld a resource ownership ellenőrzést!
*
* Ez a middleware ellenőrzi, hogy a user tulajdonosa-e az adott resource-nak.
* Például: csak a blog szerzője módosíthatja/törölheti a blogot.
*
* @param {Function} getResourceOwnerId - Függvény, ami visszaadja a resource owner ID-t
*
* Példa használat:
* router.put('/blogs/:id',
* authenticateToken,
* checkOwnership(async (req) => {
* const blog = await blogRepository.findById(req.params.id);
* return blog.authorId;
* }),
* (req, res) => { ... }
* );
*/
export function checkOwnership(getResourceOwnerId) {
return async (req, res, next) => {
try {
// TODO: Implementáld az ownership check-et
// 1. Szerezd meg a resource owner ID-t a getResourceOwnerId függvénnyel
// 2. Ellenőrizd, hogy req.user.id === ownerId VAGY req.user.role === 'ADMIN'
// 3. Ha nem egyezik és nem admin, küldj 403 Forbidden választ
// 4. Ha OK, hívd meg a next()-et
return res.status(403).json({
success: false,
error: 'Ownership check middleware not implemented yet - this is your task!'
});
} catch (error) {
return res.status(403).json({
success: false,
error: 'Access denied'
});
}
};
}
@@ -0,0 +1,26 @@
/**
* Error Handler Middleware
* Globális error handling
*/
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 })
});
}
/**
* Not Found Handler
*/
export function notFoundHandler(req, res) {
res.status(404).json({
success: false,
error: 'Route not found'
});
}
@@ -0,0 +1,21 @@
import express from 'express';
/**
* Auth Routes
*
* FELADAT: Implementáld az auth middleware-t és védett route-okat!
*/
export function createAuthRoutes(authController) {
const router = express.Router();
// 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 - FELADAT: add hozzá az auth middleware-t!
router.post('/logout', (req, res) => authController.logout(req, res));
router.get('/me', (req, res) => authController.getCurrentUser(req, res));
return router;
}
@@ -0,0 +1,20 @@
import express from 'express';
/**
* Blog Routes
*/
export function createBlogRoutes(blogController) {
const router = express.Router();
// Publikus route-ok
router.get('/', (req, res) => blogController.getAllBlogs(req, res));
router.get('/:id', (req, res) => blogController.getBlog(req, res));
// Védett route-ok - FELADAT: add hozzá az auth middleware-t!
// Példa: router.post('/', authMiddleware, (req, res) => ...)
router.post('/', (req, res) => blogController.createBlog(req, res));
router.put('/:id', (req, res) => blogController.updateBlog(req, res));
router.delete('/:id', (req, res) => blogController.deleteBlog(req, res));
return router;
}
@@ -0,0 +1,13 @@
import express from 'express';
/**
* User Routes
*/
export function createUserRoutes(blogController) {
const router = express.Router();
// User blogjai
router.get('/:userId/blogs', (req, res) => blogController.getUserBlogs(req, res));
return router;
}
@@ -0,0 +1,33 @@
/**
* CreateBlogCommand
* Blog létrehozási command
*/
export class CreateBlogCommand {
constructor(data) {
this.title = data.title;
this.content = data.content;
this.authorId = data.authorId;
this.published = data.published || false;
}
validate() {
const errors = [];
if (!this.title || this.title.trim().length === 0) {
errors.push('Title is required');
}
if (!this.content || this.content.trim().length === 0) {
errors.push('Content is required');
}
if (!this.authorId) {
errors.push('Author ID is required');
}
return {
isValid: errors.length === 0,
errors
};
}
}
@@ -0,0 +1,27 @@
/**
* DeleteBlogCommand
* Blog törlési command
*/
export class DeleteBlogCommand {
constructor(id, requesterId) {
this.id = id;
this.requesterId = requesterId; // Ki kéri a törlést (authorization-höz kell)
}
validate() {
const errors = [];
if (!this.id) {
errors.push('Blog ID is required');
}
if (!this.requesterId) {
errors.push('Requester ID is required');
}
return {
isValid: errors.length === 0,
errors
};
}
}
@@ -0,0 +1,33 @@
/**
* UpdateBlogCommand
* Blog módosítási command
*/
export class UpdateBlogCommand {
constructor(id, data) {
this.id = id;
this.title = data.title;
this.content = data.content;
this.published = data.published;
}
validate() {
const errors = [];
if (!this.id) {
errors.push('Blog ID is required');
}
if (this.title !== undefined && this.title.trim().length === 0) {
errors.push('Title cannot be empty');
}
if (this.content !== undefined && this.content.trim().length === 0) {
errors.push('Content cannot be empty');
}
return {
isValid: errors.length === 0,
errors
};
}
}
@@ -0,0 +1,31 @@
/**
* BlogQueryHandler
* Blog query-k kezelése
*/
export class BlogQueryHandler {
constructor(blogRepository) {
this.blogRepository = blogRepository;
}
async handleGetBlog(query) {
const blog = await this.blogRepository.findById(query.id);
if (!blog) {
throw new Error('Blog not found');
}
return blog;
}
async handleGetAllBlogs(query) {
const blogs = await this.blogRepository.findAll({
publishedOnly: query.publishedOnly,
limit: query.limit,
offset: query.offset
});
return blogs;
}
async handleGetUserBlogs(query) {
const blogs = await this.blogRepository.findByAuthorId(query.authorId);
return blogs;
}
}
@@ -0,0 +1,26 @@
/**
* CreateBlogHandler
* CreateBlogCommand handler
*/
export class CreateBlogHandler {
constructor(blogRepository) {
this.blogRepository = blogRepository;
}
async handle(command) {
const validation = command.validate();
if (!validation.isValid) {
throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
}
const blogData = {
title: command.title,
content: command.content,
authorId: command.authorId,
published: command.published
};
const blog = await this.blogRepository.create(blogData);
return blog;
}
}
@@ -0,0 +1,29 @@
/**
* DeleteBlogHandler
* DeleteBlogCommand handler
*/
export class DeleteBlogHandler {
constructor(blogRepository) {
this.blogRepository = blogRepository;
}
async handle(command) {
const validation = command.validate();
if (!validation.isValid) {
throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
}
// Ellenőrizzük, hogy létezik-e a blog
const existingBlog = await this.blogRepository.findById(command.id);
if (!existingBlog) {
throw new Error('Blog not found');
}
// FIGYELEM: Itt kell authorization ellenőrzés!
// Csak a szerző vagy admin törölheti
// Ez a feladat része - implementálandó!
await this.blogRepository.delete(command.id);
return { success: true, message: 'Blog deleted successfully' };
}
}
@@ -0,0 +1,30 @@
/**
* UpdateBlogHandler
* UpdateBlogCommand handler
*/
export class UpdateBlogHandler {
constructor(blogRepository) {
this.blogRepository = blogRepository;
}
async handle(command) {
const validation = command.validate();
if (!validation.isValid) {
throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
}
// Ellenőrizzük, hogy létezik-e a blog
const existingBlog = await this.blogRepository.findById(command.id);
if (!existingBlog) {
throw new Error('Blog not found');
}
const updateData = {};
if (command.title !== undefined) updateData.title = command.title;
if (command.content !== undefined) updateData.content = command.content;
if (command.published !== undefined) updateData.published = command.published;
const blog = await this.blogRepository.update(command.id, updateData);
return blog;
}
}
@@ -0,0 +1,31 @@
/**
* GetBlogQuery
* Blog lekérdezési query
*/
export class GetBlogQuery {
constructor(id) {
this.id = id;
}
}
/**
* GetAllBlogsQuery
* Összes blog lekérdezési query
*/
export class GetAllBlogsQuery {
constructor(options = {}) {
this.publishedOnly = options.publishedOnly || false;
this.limit = options.limit;
this.offset = options.offset || 0;
}
}
/**
* GetUserBlogsQuery
* User blogjai lekérdezési query
*/
export class GetUserBlogsQuery {
constructor(authorId) {
this.authorId = authorId;
}
}
@@ -0,0 +1,32 @@
/**
* Domain model - Blog
* Ez a Blog entitás domain reprezentációja
*/
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;
}
/**
* Blog DTO
*/
toObject() {
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
};
}
}
@@ -0,0 +1,29 @@
/**
* Domain model - User
* Ez a User entitás domain reprezentációja
*/
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;
}
/**
* User DTO publikus adatokkal (jelszó nélkül)
*/
toPublicObject() {
return {
id: this.id,
email: this.email,
username: this.username,
role: this.role,
createdAt: this.createdAt,
updatedAt: this.updatedAt
};
}
}
@@ -0,0 +1,33 @@
/**
* IBlogRepository Interface
* Blog repository interface a dependency inversion principle-nek megfelelően
*/
export class IBlogRepository {
async findById(id) {
throw new Error('Method not implemented');
}
async findByAuthorId(authorId) {
throw new Error('Method not implemented');
}
async findAll(options = {}) {
throw new Error('Method not implemented');
}
async create(blogData) {
throw new Error('Method not implemented');
}
async update(id, blogData) {
throw new Error('Method not implemented');
}
async delete(id) {
throw new Error('Method not implemented');
}
async publish(id) {
throw new Error('Method not implemented');
}
}

Some files were not shown because too many files have changed in this diff Show More