Compare commits
19 Commits
863f76934c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ee9c03b85 | |||
| 91e48d2178 | |||
| ad1e783472 | |||
| 8b8c08be1b | |||
| 28bd7661f5 | |||
| 8da3a1eb32 | |||
| 388aa908de | |||
| afc3777ac9 | |||
| ffca701b84 | |||
| a837f5ecba | |||
| 31d4479c63 | |||
| 77f7eb2664 | |||
| af57733506 | |||
| d90a6ed735 | |||
| 0f0e463062 | |||
| 87f07bb22f | |||
| 86109cf48b | |||
| 622d4a3321 | |||
| 6afdef6a30 |
+13
-1
@@ -14,4 +14,16 @@
|
||||
*.vrb
|
||||
*.synctex.gz
|
||||
|
||||
Thumbs.*
|
||||
#nodejs
|
||||
*/*/node_modules
|
||||
*/*/node_modules
|
||||
*/*/package-lock.json
|
||||
*/*/yarn.lock
|
||||
|
||||
Thumbs.*
|
||||
|
||||
# tömörített fájlok
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
*.7z
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Generated
+1148
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
.env
|
||||
*.log
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+19
@@ -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;
|
||||
}
|
||||
}
|
||||
+12
@@ -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;
|
||||
}
|
||||
}
|
||||
+20
@@ -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;
|
||||
}
|
||||
}
|
||||
+9
@@ -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;
|
||||
}
|
||||
}
|
||||
+19
@@ -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;
|
||||
}
|
||||
}
|
||||
+12
@@ -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;
|
||||
}
|
||||
}
|
||||
+20
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
.env
|
||||
dist/
|
||||
.DS_Store
|
||||
*.log
|
||||
coverage/
|
||||
.vscode/
|
||||
.idea/
|
||||
@@ -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! 💪**
|
||||
@@ -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! 🚀**
|
||||
@@ -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:
|
||||
@@ -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
Reference in New Issue
Block a user