From 388aa908de910c2a625cdd76c0ba2ed827c529f8 Mon Sep 17 00:00:00 2001 From: magdo Date: Wed, 4 Mar 2026 20:02:39 +0100 Subject: [PATCH] negyedik gyakorlat + megoldasok --- Backend/elso gyakorlat_minta/.gitignore | 3 + Backend/elso gyakorlat_minta/README.md | 119 ++ Backend/elso gyakorlat_minta/package.json | 17 + .../src/API/controllers/postController.js | 92 ++ .../src/API/controllers/userController.js | 79 ++ .../src/API/routers/postRouter.js | 14 + .../src/API/routers/userRouter.js | 13 + .../elso gyakorlat_minta/src/API/server.js | 36 + .../blogs/command/createPostCommand.js | 7 + .../blogs/command/createPostCommandHandler.js | 19 + .../blogs/command/deletePostCommand.js | 5 + .../blogs/command/deletePostCommandHandler.js | 12 + .../blogs/command/updatePostCommand.js | 8 + .../blogs/command/updatePostCommandHandler.js | 20 + .../blogs/query/getAllPostsQuery.js | 3 + .../blogs/query/getAllPostsQueryHandler.js | 9 + .../blogs/query/getPostByIdQuery.js | 5 + .../blogs/query/getPostByIdQueryHandler.js | 13 + .../blogs/query/getPostsByUserIdQuery.js | 5 + .../query/getPostsByUserIdQueryHandler.js | 9 + .../users/command/createUserCommand.js | 7 + .../users/command/createUserCommandHandler.js | 19 + .../users/command/deleteUserCommand.js | 5 + .../users/command/deleteUserCommandHandler.js | 12 + .../users/command/updateUserCommand.js | 8 + .../users/command/updateUserCommandHandler.js | 20 + .../users/query/getAllUsersQuery.js | 3 + .../users/query/getAllUsersQueryHandler.js | 9 + .../users/query/getUserByIdQuery.js | 5 + .../users/query/getUserByIdQueryHandler.js | 13 + .../src/Domain/IPostRepository.js | 25 + .../src/Domain/IUserRepository.js | 21 + .../src/Infrastructure/post.json | 1 + .../src/Infrastructure/postRepository.js | 65 + .../src/Infrastructure/user.json | 1 + .../src/Infrastructure/userRepository.js | 60 + Backend/harmadik gyakorlat_minta/.env.example | 19 + Backend/harmadik gyakorlat_minta/.gitignore | 8 + Backend/harmadik gyakorlat_minta/README.md | 89 ++ .../docker-compose.yml | 38 + Backend/harmadik gyakorlat_minta/package.json | 32 + .../prisma/schema.prisma | 44 + .../src/api/controllers/AuthController.js | 271 ++++ .../src/api/controllers/BlogController.js | 114 ++ .../src/api/middlewares/authMiddleware.js | 143 ++ .../src/api/middlewares/errorHandler.js | 19 + .../src/api/routes/authRoutes.js | 17 + .../src/api/routes/blogRoutes.js | 38 + .../src/domain/models/Blog.js | 29 + .../src/domain/models/User.js | 20 + .../domain/repositories/IBlogRepository.js | 25 + .../domain/repositories/IUserRepository.js | 29 + Backend/harmadik gyakorlat_minta/src/index.js | 62 + .../src/infrastructure/auth/authUtils.js | 231 ++++ .../src/infrastructure/database/prisma.js | 22 + .../src/infrastructure/database/redis.js | 28 + .../repositories/BlogRepository.js | 54 + .../repositories/UserRepository.js | 55 + Backend/második gyakorlat_minta/.env.example | 2 + Backend/második gyakorlat_minta/.gitignore | 5 + Backend/második gyakorlat_minta/README.md | 80 ++ Backend/második gyakorlat_minta/nodemon.json | 6 + Backend/második gyakorlat_minta/package.json | 25 + .../prisma/schema.prisma | 21 + .../src/api/controllers/UserController.js | 116 ++ .../src/api/routes/user.routes.js | 16 + .../második gyakorlat_minta/src/api/server.js | 51 + .../user/command/CreateUserCommand.js | 10 + .../user/command/CreateUserHandler.js | 41 + .../user/command/DeleteUserCommand.js | 7 + .../user/command/DeleteUserHandler.js | 22 + .../user/command/UpdateUserCommand.js | 10 + .../user/command/UpdateUserHandler.js | 34 + .../user/query/GetAllUsersHandler.js | 13 + .../application/user/query/GetAllUsersQuery.js | 5 + .../application/user/query/GetUserHandler.js | 17 + .../src/application/user/query/GetUserQuery.js | 7 + .../src/domain/interfaces/IUserRepository.js | 33 + .../src/domain/models/User.js | 56 + .../src/infrastructure/database/prisma.js | 7 + .../repositories/UserRepository.js | 79 ++ Backend/negyedik gyakorlat/.env.example | 17 + Backend/negyedik gyakorlat/.gitignore | 20 + Backend/negyedik gyakorlat/FELADAT.md | 1215 +++++++++++++++++ Backend/negyedik gyakorlat/SEGÍTSÉG.md | 829 +++++++++++ .../negyedik gyakorlat/coverage/clover.xml | 418 ++++++ .../coverage/coverage-final.json | 27 + .../api/controllers/AuthController.js.html | 394 ++++++ .../api/controllers/UserController.js.html | 397 ++++++ .../lcov-report/api/controllers/index.html | 131 ++ .../api/middlewares/authMiddleware.js.html | 217 +++ .../api/middlewares/corsMiddleware.js.html | 151 ++ .../lcov-report/api/middlewares/index.html | 146 ++ .../api/middlewares/scopeMiddleware.js.html | 139 ++ .../api/routers/authRoutes.js.html | 214 +++ .../lcov-report/api/routers/index.html | 131 ++ .../api/routers/userRoutes.js.html | 232 ++++ .../auth/commands/LoginUserCommand.js.html | 121 ++ .../commands/LoginUserCommandHandler.js.html | 265 ++++ .../auth/commands/RegisterUserCommand.js.html | 124 ++ .../RegisterUserCommandHandler.js.html | 334 +++++ .../application/auth/commands/index.html | 161 +++ .../application/services/Container.js.html | 256 ++++ .../application/services/EmailService.js.html | 178 +++ .../application/services/JwtService.js.html | 427 ++++++ .../application/services/index.html | 146 ++ .../commands/UpdateUserProfileCommand.js.html | 121 ++ .../UpdateUserProfileCommandHandler.js.html | 181 +++ .../application/user/commands/index.html | 131 ++ .../user/queries/GetAllUsersQuery.js.html | 118 ++ .../queries/GetAllUsersQueryHandler.js.html | 160 +++ .../user/queries/GetMeQuery.js.html | 118 ++ .../user/queries/GetMeQueryHandler.js.html | 178 +++ .../user/queries/GetUserByIdQuery.js.html | 118 ++ .../queries/GetUserByIdQueryHandler.js.html | 190 +++ .../application/user/queries/index.html | 191 +++ .../coverage/lcov-report/base.css | 224 +++ .../coverage/lcov-report/block-navigation.js | 87 ++ .../irepositories/IUserRepository.js.html | 265 ++++ .../domain/irepositories/index.html | 116 ++ .../lcov-report/domain/models/User.js.html | 313 +++++ .../lcov-report/domain/models/index.html | 116 ++ .../coverage/lcov-report/favicon.png | Bin 0 -> 445 bytes .../coverage/lcov-report/index.html | 266 ++++ .../db/DatabaseConnection.js.html | 307 +++++ .../lcov-report/infrastructure/db/index.html | 116 ++ .../repositories/UserRepository.js.html | 475 +++++++ .../infrastructure/repositories/index.html | 116 ++ .../coverage/lcov-report/prettify.css | 1 + .../coverage/lcov-report/prettify.js | 2 + .../lcov-report/sort-arrow-sprite.png | Bin 0 -> 138 bytes .../coverage/lcov-report/sorter.js | 210 +++ Backend/negyedik gyakorlat/coverage/lcov.info | 804 +++++++++++ Backend/negyedik gyakorlat/docker-compose.yml | 43 + Backend/negyedik gyakorlat/jest.config.js | 16 + Backend/negyedik gyakorlat/package.json | 39 + .../negyedik gyakorlat/prisma/schema.prisma | 21 + .../src/api/controllers/AuthController.js | 103 ++ .../src/api/controllers/UserController.js | 104 ++ .../src/api/middlewares/authMiddleware.js | 44 + .../src/api/middlewares/corsMiddleware.js | 22 + .../src/api/middlewares/scopeMiddleware.js | 18 + .../src/api/routers/authRoutes.js | 43 + .../src/api/routers/userRoutes.js | 49 + Backend/negyedik gyakorlat/src/api/server.js | 215 +++ .../auth/commands/LoginUserCommand.js | 12 + .../auth/commands/LoginUserCommandHandler.js | 60 + .../auth/commands/RegisterUserCommand.js | 13 + .../commands/RegisterUserCommandHandler.js | 83 ++ .../src/application/services/Container.js | 57 + .../src/application/services/EmailService.js | 31 + .../src/application/services/JwtService.js | 114 ++ .../services/templates/welcome.hbs | 145 ++ .../user/commands/UpdateUserProfileCommand.js | 12 + .../UpdateUserProfileCommandHandler.js | 32 + .../user/queries/GetAllUsersQuery.js | 11 + .../user/queries/GetAllUsersQueryHandler.js | 25 + .../application/user/queries/GetMeQuery.js | 11 + .../user/queries/GetMeQueryHandler.js | 31 + .../user/queries/GetUserByIdQuery.js | 11 + .../user/queries/GetUserByIdQueryHandler.js | 35 + .../domain/irepositories/IUserRepository.js | 60 + .../src/domain/models/User.js | 76 ++ .../infrastructure/db/DatabaseConnection.js | 74 + .../repositories/UserRepository.js | 130 ++ .../tests/container.test.js | 39 + .../tests/unit/application/Container.test.js | 311 +++++ .../commands/LoginUserCommandHandler.test.js | 117 ++ .../RegisterUserCommandHandler.test.js | 194 +++ .../UpdateUserProfileCommandHandler.test.js | 80 ++ .../unit/controllers/AuthController.test.js | 194 +++ .../unit/controllers/UserController.test.js | 223 +++ .../unit/middlewares/authMiddleware.test.js | 123 ++ .../unit/middlewares/corsMiddleware.test.js | 156 +++ .../queries/GetAllUsersQueryHandler.test.js | 86 ++ .../unit/queries/GetMeQueryHandler.test.js | 65 + .../queries/GetUserByIdQueryHandler.test.js | 91 ++ .../tests/unit/services/EmailService.test.js | 240 ++++ .../tests/unit/services/JwtService.test.js | 218 +++ Backend/negyedik gyakorlat/tests/user.test.js | 36 + Backend/negyedik gyakorlat_minta/.env.example | 15 + Backend/negyedik gyakorlat_minta/.gitignore | 20 + Backend/negyedik gyakorlat_minta/README.md | 98 ++ .../docker-compose.yml | 25 + .../negyedik gyakorlat_minta/jest.config.js | 16 + Backend/negyedik gyakorlat_minta/package.json | 40 + .../prisma/schema.prisma | 21 + .../src/api/controllers/AuthController.js | 83 ++ .../src/api/controllers/UserController.js | 62 + .../src/api/middlewares/authMiddleware.js | 23 + .../src/api/middlewares/corsMiddleware.js | 58 + .../src/api/middlewares/errorHandler.js | 12 + .../src/api/middlewares/scopedMiddleware.js | 14 + .../src/api/routers/authRoutes.js | 12 + .../src/api/routers/userRoutes.js | 16 + .../src/api/server.js | 145 ++ .../auth/commands/LoginUserCommand.js | 6 + .../auth/commands/LoginUserCommandHandler.js | 31 + .../auth/commands/RegisterUserCommand.js | 7 + .../commands/RegisterUserCommandHandler.js | 51 + .../src/application/services/Container.js | 126 ++ .../src/application/services/EmailService.js | 158 +++ .../src/application/services/JwtService.js | 33 + .../user/commands/UpdateUserProfileCommand.js | 7 + .../UpdateUserProfileCommandHandler.js | 26 + .../user/queries/GetAllUsersQuery.js | 2 + .../user/queries/GetAllUsersQueryHandler.js | 9 + .../application/user/queries/GetMeQuery.js | 5 + .../user/queries/GetMeQueryHandler.js | 13 + .../user/queries/GetUserByIdQuery.js | 5 + .../user/queries/GetUserByIdQueryHandler.js | 13 + .../domain/irepositories/IUserRepository.js | 25 + .../src/domain/models/User.js | 40 + .../src/email-templates/password-reset.hbs | 83 ++ .../src/email-templates/welcome.hbs | 69 + .../infrastructure/db/DatabaseConnection.js | 30 + .../repositories/UserRepository.js | 53 + 217 files changed, 19791 insertions(+) create mode 100644 Backend/elso gyakorlat_minta/.gitignore create mode 100644 Backend/elso gyakorlat_minta/README.md create mode 100644 Backend/elso gyakorlat_minta/package.json create mode 100644 Backend/elso gyakorlat_minta/src/API/controllers/postController.js create mode 100644 Backend/elso gyakorlat_minta/src/API/controllers/userController.js create mode 100644 Backend/elso gyakorlat_minta/src/API/routers/postRouter.js create mode 100644 Backend/elso gyakorlat_minta/src/API/routers/userRouter.js create mode 100644 Backend/elso gyakorlat_minta/src/API/server.js create mode 100644 Backend/elso gyakorlat_minta/src/Application/blogs/command/createPostCommand.js create mode 100644 Backend/elso gyakorlat_minta/src/Application/blogs/command/createPostCommandHandler.js create mode 100644 Backend/elso gyakorlat_minta/src/Application/blogs/command/deletePostCommand.js create mode 100644 Backend/elso gyakorlat_minta/src/Application/blogs/command/deletePostCommandHandler.js create mode 100644 Backend/elso gyakorlat_minta/src/Application/blogs/command/updatePostCommand.js create mode 100644 Backend/elso gyakorlat_minta/src/Application/blogs/command/updatePostCommandHandler.js create mode 100644 Backend/elso gyakorlat_minta/src/Application/blogs/query/getAllPostsQuery.js create mode 100644 Backend/elso gyakorlat_minta/src/Application/blogs/query/getAllPostsQueryHandler.js create mode 100644 Backend/elso gyakorlat_minta/src/Application/blogs/query/getPostByIdQuery.js create mode 100644 Backend/elso gyakorlat_minta/src/Application/blogs/query/getPostByIdQueryHandler.js create mode 100644 Backend/elso gyakorlat_minta/src/Application/blogs/query/getPostsByUserIdQuery.js create mode 100644 Backend/elso gyakorlat_minta/src/Application/blogs/query/getPostsByUserIdQueryHandler.js create mode 100644 Backend/elso gyakorlat_minta/src/Application/users/command/createUserCommand.js create mode 100644 Backend/elso gyakorlat_minta/src/Application/users/command/createUserCommandHandler.js create mode 100644 Backend/elso gyakorlat_minta/src/Application/users/command/deleteUserCommand.js create mode 100644 Backend/elso gyakorlat_minta/src/Application/users/command/deleteUserCommandHandler.js create mode 100644 Backend/elso gyakorlat_minta/src/Application/users/command/updateUserCommand.js create mode 100644 Backend/elso gyakorlat_minta/src/Application/users/command/updateUserCommandHandler.js create mode 100644 Backend/elso gyakorlat_minta/src/Application/users/query/getAllUsersQuery.js create mode 100644 Backend/elso gyakorlat_minta/src/Application/users/query/getAllUsersQueryHandler.js create mode 100644 Backend/elso gyakorlat_minta/src/Application/users/query/getUserByIdQuery.js create mode 100644 Backend/elso gyakorlat_minta/src/Application/users/query/getUserByIdQueryHandler.js create mode 100644 Backend/elso gyakorlat_minta/src/Domain/IPostRepository.js create mode 100644 Backend/elso gyakorlat_minta/src/Domain/IUserRepository.js create mode 100644 Backend/elso gyakorlat_minta/src/Infrastructure/post.json create mode 100644 Backend/elso gyakorlat_minta/src/Infrastructure/postRepository.js create mode 100644 Backend/elso gyakorlat_minta/src/Infrastructure/user.json create mode 100644 Backend/elso gyakorlat_minta/src/Infrastructure/userRepository.js create mode 100644 Backend/harmadik gyakorlat_minta/.env.example create mode 100644 Backend/harmadik gyakorlat_minta/.gitignore create mode 100644 Backend/harmadik gyakorlat_minta/README.md create mode 100644 Backend/harmadik gyakorlat_minta/docker-compose.yml create mode 100644 Backend/harmadik gyakorlat_minta/package.json create mode 100644 Backend/harmadik gyakorlat_minta/prisma/schema.prisma create mode 100644 Backend/harmadik gyakorlat_minta/src/api/controllers/AuthController.js create mode 100644 Backend/harmadik gyakorlat_minta/src/api/controllers/BlogController.js create mode 100644 Backend/harmadik gyakorlat_minta/src/api/middlewares/authMiddleware.js create mode 100644 Backend/harmadik gyakorlat_minta/src/api/middlewares/errorHandler.js create mode 100644 Backend/harmadik gyakorlat_minta/src/api/routes/authRoutes.js create mode 100644 Backend/harmadik gyakorlat_minta/src/api/routes/blogRoutes.js create mode 100644 Backend/harmadik gyakorlat_minta/src/domain/models/Blog.js create mode 100644 Backend/harmadik gyakorlat_minta/src/domain/models/User.js create mode 100644 Backend/harmadik gyakorlat_minta/src/domain/repositories/IBlogRepository.js create mode 100644 Backend/harmadik gyakorlat_minta/src/domain/repositories/IUserRepository.js create mode 100644 Backend/harmadik gyakorlat_minta/src/index.js create mode 100644 Backend/harmadik gyakorlat_minta/src/infrastructure/auth/authUtils.js create mode 100644 Backend/harmadik gyakorlat_minta/src/infrastructure/database/prisma.js create mode 100644 Backend/harmadik gyakorlat_minta/src/infrastructure/database/redis.js create mode 100644 Backend/harmadik gyakorlat_minta/src/infrastructure/repositories/BlogRepository.js create mode 100644 Backend/harmadik gyakorlat_minta/src/infrastructure/repositories/UserRepository.js create mode 100644 Backend/második gyakorlat_minta/.env.example create mode 100644 Backend/második gyakorlat_minta/.gitignore create mode 100644 Backend/második gyakorlat_minta/README.md create mode 100644 Backend/második gyakorlat_minta/nodemon.json create mode 100644 Backend/második gyakorlat_minta/package.json create mode 100644 Backend/második gyakorlat_minta/prisma/schema.prisma create mode 100644 Backend/második gyakorlat_minta/src/api/controllers/UserController.js create mode 100644 Backend/második gyakorlat_minta/src/api/routes/user.routes.js create mode 100644 Backend/második gyakorlat_minta/src/api/server.js create mode 100644 Backend/második gyakorlat_minta/src/application/user/command/CreateUserCommand.js create mode 100644 Backend/második gyakorlat_minta/src/application/user/command/CreateUserHandler.js create mode 100644 Backend/második gyakorlat_minta/src/application/user/command/DeleteUserCommand.js create mode 100644 Backend/második gyakorlat_minta/src/application/user/command/DeleteUserHandler.js create mode 100644 Backend/második gyakorlat_minta/src/application/user/command/UpdateUserCommand.js create mode 100644 Backend/második gyakorlat_minta/src/application/user/command/UpdateUserHandler.js create mode 100644 Backend/második gyakorlat_minta/src/application/user/query/GetAllUsersHandler.js create mode 100644 Backend/második gyakorlat_minta/src/application/user/query/GetAllUsersQuery.js create mode 100644 Backend/második gyakorlat_minta/src/application/user/query/GetUserHandler.js create mode 100644 Backend/második gyakorlat_minta/src/application/user/query/GetUserQuery.js create mode 100644 Backend/második gyakorlat_minta/src/domain/interfaces/IUserRepository.js create mode 100644 Backend/második gyakorlat_minta/src/domain/models/User.js create mode 100644 Backend/második gyakorlat_minta/src/infrastructure/database/prisma.js create mode 100644 Backend/második gyakorlat_minta/src/infrastructure/repositories/UserRepository.js create mode 100644 Backend/negyedik gyakorlat/.env.example create mode 100644 Backend/negyedik gyakorlat/.gitignore create mode 100644 Backend/negyedik gyakorlat/FELADAT.md create mode 100644 Backend/negyedik gyakorlat/SEGÍTSÉG.md create mode 100644 Backend/negyedik gyakorlat/coverage/clover.xml create mode 100644 Backend/negyedik gyakorlat/coverage/coverage-final.json create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/api/controllers/AuthController.js.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/api/controllers/UserController.js.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/api/controllers/index.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/api/middlewares/authMiddleware.js.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/api/middlewares/corsMiddleware.js.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/api/middlewares/index.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/api/middlewares/scopeMiddleware.js.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/api/routers/authRoutes.js.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/api/routers/index.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/api/routers/userRoutes.js.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/application/auth/commands/LoginUserCommand.js.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/application/auth/commands/LoginUserCommandHandler.js.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/application/auth/commands/RegisterUserCommand.js.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/application/auth/commands/RegisterUserCommandHandler.js.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/application/auth/commands/index.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/application/services/Container.js.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/application/services/EmailService.js.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/application/services/JwtService.js.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/application/services/index.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/application/user/commands/UpdateUserProfileCommand.js.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/application/user/commands/UpdateUserProfileCommandHandler.js.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/application/user/commands/index.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/application/user/queries/GetAllUsersQuery.js.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/application/user/queries/GetAllUsersQueryHandler.js.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/application/user/queries/GetMeQuery.js.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/application/user/queries/GetMeQueryHandler.js.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/application/user/queries/GetUserByIdQuery.js.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/application/user/queries/GetUserByIdQueryHandler.js.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/application/user/queries/index.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/base.css create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/block-navigation.js create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/domain/irepositories/IUserRepository.js.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/domain/irepositories/index.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/domain/models/User.js.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/domain/models/index.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/favicon.png create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/index.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/infrastructure/db/DatabaseConnection.js.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/infrastructure/db/index.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/infrastructure/repositories/UserRepository.js.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/infrastructure/repositories/index.html create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/prettify.css create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/prettify.js create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/sort-arrow-sprite.png create mode 100644 Backend/negyedik gyakorlat/coverage/lcov-report/sorter.js create mode 100644 Backend/negyedik gyakorlat/coverage/lcov.info create mode 100644 Backend/negyedik gyakorlat/docker-compose.yml create mode 100644 Backend/negyedik gyakorlat/jest.config.js create mode 100644 Backend/negyedik gyakorlat/package.json create mode 100644 Backend/negyedik gyakorlat/prisma/schema.prisma create mode 100644 Backend/negyedik gyakorlat/src/api/controllers/AuthController.js create mode 100644 Backend/negyedik gyakorlat/src/api/controllers/UserController.js create mode 100644 Backend/negyedik gyakorlat/src/api/middlewares/authMiddleware.js create mode 100644 Backend/negyedik gyakorlat/src/api/middlewares/corsMiddleware.js create mode 100644 Backend/negyedik gyakorlat/src/api/middlewares/scopeMiddleware.js create mode 100644 Backend/negyedik gyakorlat/src/api/routers/authRoutes.js create mode 100644 Backend/negyedik gyakorlat/src/api/routers/userRoutes.js create mode 100644 Backend/negyedik gyakorlat/src/api/server.js create mode 100644 Backend/negyedik gyakorlat/src/application/auth/commands/LoginUserCommand.js create mode 100644 Backend/negyedik gyakorlat/src/application/auth/commands/LoginUserCommandHandler.js create mode 100644 Backend/negyedik gyakorlat/src/application/auth/commands/RegisterUserCommand.js create mode 100644 Backend/negyedik gyakorlat/src/application/auth/commands/RegisterUserCommandHandler.js create mode 100644 Backend/negyedik gyakorlat/src/application/services/Container.js create mode 100644 Backend/negyedik gyakorlat/src/application/services/EmailService.js create mode 100644 Backend/negyedik gyakorlat/src/application/services/JwtService.js create mode 100644 Backend/negyedik gyakorlat/src/application/services/templates/welcome.hbs create mode 100644 Backend/negyedik gyakorlat/src/application/user/commands/UpdateUserProfileCommand.js create mode 100644 Backend/negyedik gyakorlat/src/application/user/commands/UpdateUserProfileCommandHandler.js create mode 100644 Backend/negyedik gyakorlat/src/application/user/queries/GetAllUsersQuery.js create mode 100644 Backend/negyedik gyakorlat/src/application/user/queries/GetAllUsersQueryHandler.js create mode 100644 Backend/negyedik gyakorlat/src/application/user/queries/GetMeQuery.js create mode 100644 Backend/negyedik gyakorlat/src/application/user/queries/GetMeQueryHandler.js create mode 100644 Backend/negyedik gyakorlat/src/application/user/queries/GetUserByIdQuery.js create mode 100644 Backend/negyedik gyakorlat/src/application/user/queries/GetUserByIdQueryHandler.js create mode 100644 Backend/negyedik gyakorlat/src/domain/irepositories/IUserRepository.js create mode 100644 Backend/negyedik gyakorlat/src/domain/models/User.js create mode 100644 Backend/negyedik gyakorlat/src/infrastructure/db/DatabaseConnection.js create mode 100644 Backend/negyedik gyakorlat/src/infrastructure/repositories/UserRepository.js create mode 100644 Backend/negyedik gyakorlat/tests/container.test.js create mode 100644 Backend/negyedik gyakorlat/tests/unit/application/Container.test.js create mode 100644 Backend/negyedik gyakorlat/tests/unit/commands/LoginUserCommandHandler.test.js create mode 100644 Backend/negyedik gyakorlat/tests/unit/commands/RegisterUserCommandHandler.test.js create mode 100644 Backend/negyedik gyakorlat/tests/unit/commands/UpdateUserProfileCommandHandler.test.js create mode 100644 Backend/negyedik gyakorlat/tests/unit/controllers/AuthController.test.js create mode 100644 Backend/negyedik gyakorlat/tests/unit/controllers/UserController.test.js create mode 100644 Backend/negyedik gyakorlat/tests/unit/middlewares/authMiddleware.test.js create mode 100644 Backend/negyedik gyakorlat/tests/unit/middlewares/corsMiddleware.test.js create mode 100644 Backend/negyedik gyakorlat/tests/unit/queries/GetAllUsersQueryHandler.test.js create mode 100644 Backend/negyedik gyakorlat/tests/unit/queries/GetMeQueryHandler.test.js create mode 100644 Backend/negyedik gyakorlat/tests/unit/queries/GetUserByIdQueryHandler.test.js create mode 100644 Backend/negyedik gyakorlat/tests/unit/services/EmailService.test.js create mode 100644 Backend/negyedik gyakorlat/tests/unit/services/JwtService.test.js create mode 100644 Backend/negyedik gyakorlat/tests/user.test.js create mode 100644 Backend/negyedik gyakorlat_minta/.env.example create mode 100644 Backend/negyedik gyakorlat_minta/.gitignore create mode 100644 Backend/negyedik gyakorlat_minta/README.md create mode 100644 Backend/negyedik gyakorlat_minta/docker-compose.yml create mode 100644 Backend/negyedik gyakorlat_minta/jest.config.js create mode 100644 Backend/negyedik gyakorlat_minta/package.json create mode 100644 Backend/negyedik gyakorlat_minta/prisma/schema.prisma create mode 100644 Backend/negyedik gyakorlat_minta/src/api/controllers/AuthController.js create mode 100644 Backend/negyedik gyakorlat_minta/src/api/controllers/UserController.js create mode 100644 Backend/negyedik gyakorlat_minta/src/api/middlewares/authMiddleware.js create mode 100644 Backend/negyedik gyakorlat_minta/src/api/middlewares/corsMiddleware.js create mode 100644 Backend/negyedik gyakorlat_minta/src/api/middlewares/errorHandler.js create mode 100644 Backend/negyedik gyakorlat_minta/src/api/middlewares/scopedMiddleware.js create mode 100644 Backend/negyedik gyakorlat_minta/src/api/routers/authRoutes.js create mode 100644 Backend/negyedik gyakorlat_minta/src/api/routers/userRoutes.js create mode 100644 Backend/negyedik gyakorlat_minta/src/api/server.js create mode 100644 Backend/negyedik gyakorlat_minta/src/application/auth/commands/LoginUserCommand.js create mode 100644 Backend/negyedik gyakorlat_minta/src/application/auth/commands/LoginUserCommandHandler.js create mode 100644 Backend/negyedik gyakorlat_minta/src/application/auth/commands/RegisterUserCommand.js create mode 100644 Backend/negyedik gyakorlat_minta/src/application/auth/commands/RegisterUserCommandHandler.js create mode 100644 Backend/negyedik gyakorlat_minta/src/application/services/Container.js create mode 100644 Backend/negyedik gyakorlat_minta/src/application/services/EmailService.js create mode 100644 Backend/negyedik gyakorlat_minta/src/application/services/JwtService.js create mode 100644 Backend/negyedik gyakorlat_minta/src/application/user/commands/UpdateUserProfileCommand.js create mode 100644 Backend/negyedik gyakorlat_minta/src/application/user/commands/UpdateUserProfileCommandHandler.js create mode 100644 Backend/negyedik gyakorlat_minta/src/application/user/queries/GetAllUsersQuery.js create mode 100644 Backend/negyedik gyakorlat_minta/src/application/user/queries/GetAllUsersQueryHandler.js create mode 100644 Backend/negyedik gyakorlat_minta/src/application/user/queries/GetMeQuery.js create mode 100644 Backend/negyedik gyakorlat_minta/src/application/user/queries/GetMeQueryHandler.js create mode 100644 Backend/negyedik gyakorlat_minta/src/application/user/queries/GetUserByIdQuery.js create mode 100644 Backend/negyedik gyakorlat_minta/src/application/user/queries/GetUserByIdQueryHandler.js create mode 100644 Backend/negyedik gyakorlat_minta/src/domain/irepositories/IUserRepository.js create mode 100644 Backend/negyedik gyakorlat_minta/src/domain/models/User.js create mode 100644 Backend/negyedik gyakorlat_minta/src/email-templates/password-reset.hbs create mode 100644 Backend/negyedik gyakorlat_minta/src/email-templates/welcome.hbs create mode 100644 Backend/negyedik gyakorlat_minta/src/infrastructure/db/DatabaseConnection.js create mode 100644 Backend/negyedik gyakorlat_minta/src/infrastructure/repositories/UserRepository.js diff --git a/Backend/elso gyakorlat_minta/.gitignore b/Backend/elso gyakorlat_minta/.gitignore new file mode 100644 index 0000000..2e8157a --- /dev/null +++ b/Backend/elso gyakorlat_minta/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.env +*.log diff --git a/Backend/elso gyakorlat_minta/README.md b/Backend/elso gyakorlat_minta/README.md new file mode 100644 index 0000000..310ab44 --- /dev/null +++ b/Backend/elso gyakorlat_minta/README.md @@ -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 diff --git a/Backend/elso gyakorlat_minta/package.json b/Backend/elso gyakorlat_minta/package.json new file mode 100644 index 0000000..21d5be7 --- /dev/null +++ b/Backend/elso gyakorlat_minta/package.json @@ -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" + } +} diff --git a/Backend/elso gyakorlat_minta/src/API/controllers/postController.js b/Backend/elso gyakorlat_minta/src/API/controllers/postController.js new file mode 100644 index 0000000..d776d54 --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/API/controllers/postController.js @@ -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 }); + } + } +} diff --git a/Backend/elso gyakorlat_minta/src/API/controllers/userController.js b/Backend/elso gyakorlat_minta/src/API/controllers/userController.js new file mode 100644 index 0000000..8095fd4 --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/API/controllers/userController.js @@ -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 }); + } + } +} diff --git a/Backend/elso gyakorlat_minta/src/API/routers/postRouter.js b/Backend/elso gyakorlat_minta/src/API/routers/postRouter.js new file mode 100644 index 0000000..bb20677 --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/API/routers/postRouter.js @@ -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; diff --git a/Backend/elso gyakorlat_minta/src/API/routers/userRouter.js b/Backend/elso gyakorlat_minta/src/API/routers/userRouter.js new file mode 100644 index 0000000..adea2e2 --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/API/routers/userRouter.js @@ -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; diff --git a/Backend/elso gyakorlat_minta/src/API/server.js b/Backend/elso gyakorlat_minta/src/API/server.js new file mode 100644 index 0000000..bc1193b --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/API/server.js @@ -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}`); +}); diff --git a/Backend/elso gyakorlat_minta/src/Application/blogs/command/createPostCommand.js b/Backend/elso gyakorlat_minta/src/Application/blogs/command/createPostCommand.js new file mode 100644 index 0000000..1408800 --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/Application/blogs/command/createPostCommand.js @@ -0,0 +1,7 @@ +export class CreatePostCommand { + constructor(title, content, author) { + this.title = title; + this.content = content; + this.author = author; + } +} diff --git a/Backend/elso gyakorlat_minta/src/Application/blogs/command/createPostCommandHandler.js b/Backend/elso gyakorlat_minta/src/Application/blogs/command/createPostCommandHandler.js new file mode 100644 index 0000000..6dae9a6 --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/Application/blogs/command/createPostCommandHandler.js @@ -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); + } +} diff --git a/Backend/elso gyakorlat_minta/src/Application/blogs/command/deletePostCommand.js b/Backend/elso gyakorlat_minta/src/Application/blogs/command/deletePostCommand.js new file mode 100644 index 0000000..1be79c2 --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/Application/blogs/command/deletePostCommand.js @@ -0,0 +1,5 @@ +export class DeletePostCommand { + constructor(id) { + this.id = id; + } +} diff --git a/Backend/elso gyakorlat_minta/src/Application/blogs/command/deletePostCommandHandler.js b/Backend/elso gyakorlat_minta/src/Application/blogs/command/deletePostCommandHandler.js new file mode 100644 index 0000000..a9ae443 --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/Application/blogs/command/deletePostCommandHandler.js @@ -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'); + } + } +} diff --git a/Backend/elso gyakorlat_minta/src/Application/blogs/command/updatePostCommand.js b/Backend/elso gyakorlat_minta/src/Application/blogs/command/updatePostCommand.js new file mode 100644 index 0000000..0b62bf8 --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/Application/blogs/command/updatePostCommand.js @@ -0,0 +1,8 @@ +export class UpdatePostCommand { + constructor(id, title, content, author) { + this.id = id; + this.title = title; + this.content = content; + this.author = author; + } +} diff --git a/Backend/elso gyakorlat_minta/src/Application/blogs/command/updatePostCommandHandler.js b/Backend/elso gyakorlat_minta/src/Application/blogs/command/updatePostCommandHandler.js new file mode 100644 index 0000000..2fb8c81 --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/Application/blogs/command/updatePostCommandHandler.js @@ -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; + } +} diff --git a/Backend/elso gyakorlat_minta/src/Application/blogs/query/getAllPostsQuery.js b/Backend/elso gyakorlat_minta/src/Application/blogs/query/getAllPostsQuery.js new file mode 100644 index 0000000..c3ba907 --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/Application/blogs/query/getAllPostsQuery.js @@ -0,0 +1,3 @@ +export class GetAllPostsQuery { + constructor() {} +} diff --git a/Backend/elso gyakorlat_minta/src/Application/blogs/query/getAllPostsQueryHandler.js b/Backend/elso gyakorlat_minta/src/Application/blogs/query/getAllPostsQueryHandler.js new file mode 100644 index 0000000..c8cc15f --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/Application/blogs/query/getAllPostsQueryHandler.js @@ -0,0 +1,9 @@ +export class GetAllPostsQueryHandler { + constructor(postRepository) { + this.postRepository = postRepository; + } + + async handle(query) { + return await this.postRepository.getAll(); + } +} diff --git a/Backend/elso gyakorlat_minta/src/Application/blogs/query/getPostByIdQuery.js b/Backend/elso gyakorlat_minta/src/Application/blogs/query/getPostByIdQuery.js new file mode 100644 index 0000000..0d63871 --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/Application/blogs/query/getPostByIdQuery.js @@ -0,0 +1,5 @@ +export class GetPostByIdQuery { + constructor(id) { + this.id = id; + } +} diff --git a/Backend/elso gyakorlat_minta/src/Application/blogs/query/getPostByIdQueryHandler.js b/Backend/elso gyakorlat_minta/src/Application/blogs/query/getPostByIdQueryHandler.js new file mode 100644 index 0000000..4626456 --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/Application/blogs/query/getPostByIdQueryHandler.js @@ -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; + } +} diff --git a/Backend/elso gyakorlat_minta/src/Application/blogs/query/getPostsByUserIdQuery.js b/Backend/elso gyakorlat_minta/src/Application/blogs/query/getPostsByUserIdQuery.js new file mode 100644 index 0000000..a021b0a --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/Application/blogs/query/getPostsByUserIdQuery.js @@ -0,0 +1,5 @@ +export class GetPostsByUserIdQuery { + constructor(userId) { + this.userId = userId; + } +} diff --git a/Backend/elso gyakorlat_minta/src/Application/blogs/query/getPostsByUserIdQueryHandler.js b/Backend/elso gyakorlat_minta/src/Application/blogs/query/getPostsByUserIdQueryHandler.js new file mode 100644 index 0000000..9a33f1f --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/Application/blogs/query/getPostsByUserIdQueryHandler.js @@ -0,0 +1,9 @@ +export class GetPostsByUserIdQueryHandler { + constructor(postRepository) { + this.postRepository = postRepository; + } + + async handle(query) { + return await this.postRepository.getByUserId(query.userId); + } +} diff --git a/Backend/elso gyakorlat_minta/src/Application/users/command/createUserCommand.js b/Backend/elso gyakorlat_minta/src/Application/users/command/createUserCommand.js new file mode 100644 index 0000000..f2e9185 --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/Application/users/command/createUserCommand.js @@ -0,0 +1,7 @@ +export class CreateUserCommand { + constructor(name, email, age) { + this.name = name; + this.email = email; + this.age = age; + } +} diff --git a/Backend/elso gyakorlat_minta/src/Application/users/command/createUserCommandHandler.js b/Backend/elso gyakorlat_minta/src/Application/users/command/createUserCommandHandler.js new file mode 100644 index 0000000..5353e63 --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/Application/users/command/createUserCommandHandler.js @@ -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); + } +} diff --git a/Backend/elso gyakorlat_minta/src/Application/users/command/deleteUserCommand.js b/Backend/elso gyakorlat_minta/src/Application/users/command/deleteUserCommand.js new file mode 100644 index 0000000..ccde30b --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/Application/users/command/deleteUserCommand.js @@ -0,0 +1,5 @@ +export class DeleteUserCommand { + constructor(id) { + this.id = id; + } +} diff --git a/Backend/elso gyakorlat_minta/src/Application/users/command/deleteUserCommandHandler.js b/Backend/elso gyakorlat_minta/src/Application/users/command/deleteUserCommandHandler.js new file mode 100644 index 0000000..a67c831 --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/Application/users/command/deleteUserCommandHandler.js @@ -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'); + } + } +} diff --git a/Backend/elso gyakorlat_minta/src/Application/users/command/updateUserCommand.js b/Backend/elso gyakorlat_minta/src/Application/users/command/updateUserCommand.js new file mode 100644 index 0000000..0c61d26 --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/Application/users/command/updateUserCommand.js @@ -0,0 +1,8 @@ +export class UpdateUserCommand { + constructor(id, name, email, age) { + this.id = id; + this.name = name; + this.email = email; + this.age = age; + } +} diff --git a/Backend/elso gyakorlat_minta/src/Application/users/command/updateUserCommandHandler.js b/Backend/elso gyakorlat_minta/src/Application/users/command/updateUserCommandHandler.js new file mode 100644 index 0000000..f268f48 --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/Application/users/command/updateUserCommandHandler.js @@ -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; + } +} diff --git a/Backend/elso gyakorlat_minta/src/Application/users/query/getAllUsersQuery.js b/Backend/elso gyakorlat_minta/src/Application/users/query/getAllUsersQuery.js new file mode 100644 index 0000000..f1d8735 --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/Application/users/query/getAllUsersQuery.js @@ -0,0 +1,3 @@ +export class GetAllUsersQuery { + constructor() {} +} diff --git a/Backend/elso gyakorlat_minta/src/Application/users/query/getAllUsersQueryHandler.js b/Backend/elso gyakorlat_minta/src/Application/users/query/getAllUsersQueryHandler.js new file mode 100644 index 0000000..25fdeef --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/Application/users/query/getAllUsersQueryHandler.js @@ -0,0 +1,9 @@ +export class GetAllUsersQueryHandler { + constructor(userRepository) { + this.userRepository = userRepository; + } + + async handle(query) { + return await this.userRepository.getAll(); + } +} diff --git a/Backend/elso gyakorlat_minta/src/Application/users/query/getUserByIdQuery.js b/Backend/elso gyakorlat_minta/src/Application/users/query/getUserByIdQuery.js new file mode 100644 index 0000000..c0a0caa --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/Application/users/query/getUserByIdQuery.js @@ -0,0 +1,5 @@ +export class GetUserByIdQuery { + constructor(id) { + this.id = id; + } +} diff --git a/Backend/elso gyakorlat_minta/src/Application/users/query/getUserByIdQueryHandler.js b/Backend/elso gyakorlat_minta/src/Application/users/query/getUserByIdQueryHandler.js new file mode 100644 index 0000000..29ab6e9 --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/Application/users/query/getUserByIdQueryHandler.js @@ -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; + } +} diff --git a/Backend/elso gyakorlat_minta/src/Domain/IPostRepository.js b/Backend/elso gyakorlat_minta/src/Domain/IPostRepository.js new file mode 100644 index 0000000..6252765 --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/Domain/IPostRepository.js @@ -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'); + } +} diff --git a/Backend/elso gyakorlat_minta/src/Domain/IUserRepository.js b/Backend/elso gyakorlat_minta/src/Domain/IUserRepository.js new file mode 100644 index 0000000..be0a50c --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/Domain/IUserRepository.js @@ -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'); + } +} diff --git a/Backend/elso gyakorlat_minta/src/Infrastructure/post.json b/Backend/elso gyakorlat_minta/src/Infrastructure/post.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/Infrastructure/post.json @@ -0,0 +1 @@ +[] diff --git a/Backend/elso gyakorlat_minta/src/Infrastructure/postRepository.js b/Backend/elso gyakorlat_minta/src/Infrastructure/postRepository.js new file mode 100644 index 0000000..e1e1456 --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/Infrastructure/postRepository.js @@ -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; + } +} diff --git a/Backend/elso gyakorlat_minta/src/Infrastructure/user.json b/Backend/elso gyakorlat_minta/src/Infrastructure/user.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/Infrastructure/user.json @@ -0,0 +1 @@ +[] diff --git a/Backend/elso gyakorlat_minta/src/Infrastructure/userRepository.js b/Backend/elso gyakorlat_minta/src/Infrastructure/userRepository.js new file mode 100644 index 0000000..809e563 --- /dev/null +++ b/Backend/elso gyakorlat_minta/src/Infrastructure/userRepository.js @@ -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; + } +} diff --git a/Backend/harmadik gyakorlat_minta/.env.example b/Backend/harmadik gyakorlat_minta/.env.example new file mode 100644 index 0000000..2f8865a --- /dev/null +++ b/Backend/harmadik gyakorlat_minta/.env.example @@ -0,0 +1,19 @@ +# Database +DATABASE_URL="postgresql://postgres:postgres@localhost:5433/blog_db?schema=public" + +# Redis +REDIS_HOST=localhost +REDIS_PORT=6380 + +# JWT +JWT_SECRET=your-secret-key-change-this-in-production +JWT_EXPIRES_IN=7d +JWT_REFRESH_SECRET=your-refresh-secret-key-change-this +JWT_REFRESH_EXPIRES_IN=30d + +# Server +PORT=3001 +NODE_ENV=development + +# Cookie +COOKIE_SECRET=your-cookie-secret-change-this diff --git a/Backend/harmadik gyakorlat_minta/.gitignore b/Backend/harmadik gyakorlat_minta/.gitignore new file mode 100644 index 0000000..61e3cce --- /dev/null +++ b/Backend/harmadik gyakorlat_minta/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +.env +dist/ +.DS_Store +*.log +coverage/ +.vscode/ +.idea/ diff --git a/Backend/harmadik gyakorlat_minta/README.md b/Backend/harmadik gyakorlat_minta/README.md new file mode 100644 index 0000000..fc9d5e2 --- /dev/null +++ b/Backend/harmadik gyakorlat_minta/README.md @@ -0,0 +1,89 @@ +# Harmadik Gyakorlat - MINTA Megoldás + +Ez a mappa tartalmazza a harmadik gyakorlat **teljes, működő megoldását**. + +## Amit implementáltunk: + +### 1. **Authentication (Hitelesítés)** +- User regisztráció jelszó hash-eléssel (bcrypt) +- User bejelentkezés JWT tokenekkel +- Cookie-based session kezelés +- Token frissítés (refresh token) +- Kijelentkezés + +### 2. **Authorization (Jogosultságkezelés)** +- JWT token validálás middleware +- Role-based access control (RBAC) +- Resource ownership ellenőrzés +- Védett endpoint-ok + +### 3. **Security Best Practices** +- HttpOnly cookie-k +- Secure cookie-k (production) +- JWT token expiry +- Password hashing (bcrypt) +- Input validáció + +## Indítás + +```bash +npm install +npm run docker:up +npx prisma generate +npx prisma migrate dev +npm run dev +``` + +## API Endpoints + +### Auth Endpoints +- `POST /api/auth/register` - Regisztráció +- `POST /api/auth/login` - Bejelentkezés +- `POST /api/auth/logout` - Kijelentkezés (védett) +- `POST /api/auth/refresh` - Token frissítés +- `GET /api/auth/me` - Aktuális user (védett) + +### Blog Endpoints +- `GET /api/blogs` - Összes blog (publikus) +- `GET /api/blogs/:id` - Egy blog (publikus) +- `POST /api/blogs` - Blog létrehozás (védett) +- `PUT /api/blogs/:id` - Blog módosítás (védett + ownership) +- `DELETE /api/blogs/:id` - Blog törlés (védett + ownership) + +## Példa Használat + +```bash +# Regisztráció +curl -X POST http://localhost:3000/api/auth/register \ + -H "Content-Type: application/json" \ + -d '{ + "email": "test@test.com", + "username": "testuser", + "password": "Test1234" + }' + +# Login +curl -X POST http://localhost:3000/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{ + "email": "test@test.com", + "password": "Test1234" + }' + +# Blog létrehozás (védett) +curl -X POST http://localhost:3000/api/blogs \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "title": "My Blog", + "content": "Blog content..." + }' +``` + +## Tanulási Pontok + +1. **JWT Tokens**: Access és Refresh tokenek használata +2. **Bcrypt**: Jelszó hash-elés és validálás +3. **Cookie-based Auth**: HttpOnly, Secure cookie-k +4. **Authorization Middleware**: Token validálás, role check, ownership +5. **Security**: Best practices implementálása diff --git a/Backend/harmadik gyakorlat_minta/docker-compose.yml b/Backend/harmadik gyakorlat_minta/docker-compose.yml new file mode 100644 index 0000000..edf246c --- /dev/null +++ b/Backend/harmadik gyakorlat_minta/docker-compose.yml @@ -0,0 +1,38 @@ +version: '3.8' + +services: + postgres: + image: postgres:15-alpine + container_name: blog_postgres_minta + restart: unless-stopped + ports: + - "5433:5432" + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: blog_db + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + container_name: blog_redis_minta + restart: unless-stopped + ports: + - "6380:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + postgres_data: + redis_data: diff --git a/Backend/harmadik gyakorlat_minta/package.json b/Backend/harmadik gyakorlat_minta/package.json new file mode 100644 index 0000000..a40e551 --- /dev/null +++ b/Backend/harmadik gyakorlat_minta/package.json @@ -0,0 +1,32 @@ +{ + "name": "blog-auth-practice-minta", + "version": "1.0.0", + "description": "MINTA - Backend gyakorló feladat - Authentication & Authorization", + "main": "src/index.js", + "type": "module", + "scripts": { + "dev": "node --watch src/index.js", + "start": "node src/index.js", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev", + "prisma:studio": "prisma studio", + "docker:up": "docker-compose up -d", + "docker:down": "docker-compose down", + "setup": "npm install && npm run docker:up && npm run prisma:migrate && npm run prisma:generate" + }, + "keywords": ["backend", "authentication", "authorization", "jwt", "prisma", "cqrs"], + "author": "", + "license": "ISC", + "dependencies": { + "@prisma/client": "^5.9.1", + "bcrypt": "^5.1.1", + "cookie-parser": "^1.4.6", + "dotenv": "^16.4.1", + "express": "^4.18.2", + "ioredis": "^5.3.2", + "jsonwebtoken": "^9.0.2" + }, + "devDependencies": { + "prisma": "^5.9.1" + } +} diff --git a/Backend/harmadik gyakorlat_minta/prisma/schema.prisma b/Backend/harmadik gyakorlat_minta/prisma/schema.prisma new file mode 100644 index 0000000..42abd54 --- /dev/null +++ b/Backend/harmadik gyakorlat_minta/prisma/schema.prisma @@ -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 +} diff --git a/Backend/harmadik gyakorlat_minta/src/api/controllers/AuthController.js b/Backend/harmadik gyakorlat_minta/src/api/controllers/AuthController.js new file mode 100644 index 0000000..8d7a6a2 --- /dev/null +++ b/Backend/harmadik gyakorlat_minta/src/api/controllers/AuthController.js @@ -0,0 +1,271 @@ +import { UserRepository } from '../../infrastructure/repositories/UserRepository.js'; +import { + hashPassword, + verifyPassword, + generateAccessToken, + generateRefreshToken, + setAuthCookies, + clearAuthCookies, + validateRegisterInput, + validateLoginInput, + verifyRefreshToken, + createSession, + deleteSession +} from '../../infrastructure/auth/authUtils.js'; + +export class AuthController { + constructor(userRepository) { + this.userRepository = userRepository || new UserRepository(); + } + + /** + * POST /api/auth/register + * Új felhasználó regisztrációja + */ + async register(req, res) { + try { + const { email, username, password } = req.body; + + // 1. Input validáció + const validation = validateRegisterInput(req.body); + if (!validation.isValid) { + return res.status(400).json({ + success: false, + errors: validation.errors + }); + } + + // 2. Email uniqueness ellenőrzés + const existingUserByEmail = await this.userRepository.findByEmail(email); + if (existingUserByEmail) { + return res.status(400).json({ + success: false, + error: 'Email already in use' + }); + } + + // 3. Username uniqueness ellenőrzés + const existingUserByUsername = await this.userRepository.findByUsername(username); + if (existingUserByUsername) { + return res.status(400).json({ + success: false, + error: 'Username already taken' + }); + } + + // 4. Jelszó hash-elés + const hashedPassword = await hashPassword(password); + + // 5. User létrehozása + const user = await this.userRepository.create({ + email, + username, + password: hashedPassword, + role: 'USER' + }); + + // 6. JWT tokenek generálása + const accessToken = generateAccessToken(user); + const refreshToken = generateRefreshToken(user); + + // 7. Cookie-k beállítása + setAuthCookies(res, accessToken, refreshToken); + + // 8. Redis session (opcionális) + await createSession(user.id, { email: user.email, username: user.username }); + + // 9. Válasz (jelszó nélkül!) + const { password: _, ...userWithoutPassword } = user; + res.status(201).json({ + success: true, + message: 'User registered successfully', + data: { + user: userWithoutPassword, + accessToken, + refreshToken + } + }); + } catch (error) { + console.error('Register error:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } + } + + /** + * POST /api/auth/login + * Bejelentkezés + */ + async login(req, res) { + try { + const { email, username, password } = req.body; + + // 1. Input validáció + const validation = validateLoginInput(req.body); + if (!validation.isValid) { + return res.status(400).json({ + success: false, + errors: validation.errors + }); + } + + // 2. User keresése (email VAGY username alapján) + let user; + if (email) { + user = await this.userRepository.findByEmail(email); + } else if (username) { + user = await this.userRepository.findByUsername(username); + } + + if (!user) { + return res.status(401).json({ + success: false, + error: 'Invalid credentials' + }); + } + + // 3. Jelszó ellenőrzés + const isPasswordValid = await verifyPassword(password, user.password); + if (!isPasswordValid) { + return res.status(401).json({ + success: false, + error: 'Invalid credentials' + }); + } + + // 4. JWT tokenek generálása + const accessToken = generateAccessToken(user); + const refreshToken = generateRefreshToken(user); + + // 5. Cookie-k beállítása + setAuthCookies(res, accessToken, refreshToken); + + // 6. Redis session + await createSession(user.id, { email: user.email, username: user.username }); + + // 7. Válasz (jelszó nélkül!) + const { password: _, ...userWithoutPassword } = user; + res.json({ + success: true, + message: 'Login successful', + data: { + user: userWithoutPassword, + accessToken, + refreshToken + } + }); + } catch (error) { + console.error('Login error:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } + } + + /** + * POST /api/auth/logout + * Kijelentkezés + */ + async logout(req, res) { + try { + // 1. Cookie-k törlése + clearAuthCookies(res); + + // 2. Redis session törlése + if (req.user && req.user.id) { + await deleteSession(req.user.id); + } + + res.json({ + success: true, + message: 'Logged out successfully' + }); + } catch (error) { + console.error('Logout error:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } + } + + /** + * POST /api/auth/refresh + * Token frissítés + */ + async refreshToken(req, res) { + try { + // 1. Refresh token kiolvasása + const refreshToken = req.cookies.refreshToken; + if (!refreshToken) { + return res.status(401).json({ + success: false, + error: 'Refresh token not found' + }); + } + + // 2. Refresh token validálás + const decoded = verifyRefreshToken(refreshToken); + + // 3. User lekérése + const user = await this.userRepository.findById(decoded.userId); + if (!user) { + return res.status(401).json({ + success: false, + error: 'User not found' + }); + } + + // 4. Új access token generálás + const newAccessToken = generateAccessToken(user); + + // 5. Cookie frissítése + res.cookie('accessToken', newAccessToken, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + maxAge: 7 * 24 * 60 * 60 * 1000 + }); + + res.json({ + success: true, + data: { + accessToken: newAccessToken + } + }); + } catch (error) { + console.error('Refresh token error:', error); + res.status(401).json({ + success: false, + error: 'Invalid refresh token' + }); + } + } + + /** + * GET /api/auth/me + * Bejelentkezett user adatai + */ + async getCurrentUser(req, res) { + try { + // req.user-t az authenticateToken middleware állította be + const { password: _, ...userWithoutPassword } = req.user; + + res.json({ + success: true, + data: { + user: userWithoutPassword + } + }); + } catch (error) { + console.error('Get current user error:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } + } +} diff --git a/Backend/harmadik gyakorlat_minta/src/api/controllers/BlogController.js b/Backend/harmadik gyakorlat_minta/src/api/controllers/BlogController.js new file mode 100644 index 0000000..2ddc619 --- /dev/null +++ b/Backend/harmadik gyakorlat_minta/src/api/controllers/BlogController.js @@ -0,0 +1,114 @@ +import { BlogRepository } from '../../infrastructure/repositories/BlogRepository.js'; + +export class BlogController { + constructor() { + this.blogRepository = new BlogRepository(); + } + + async getAll(req, res) { + try { + const blogs = await this.blogRepository.findAll(); + res.json({ + success: true, + data: blogs + }); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } + } + + async getById(req, res) { + try { + const blog = await this.blogRepository.findById(req.params.id); + if (!blog) { + return res.status(404).json({ + success: false, + error: 'Blog not found' + }); + } + res.json({ + success: true, + data: blog + }); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } + } + + async create(req, res) { + try { + const { title, content, published } = req.body; + + if (!title || !content) { + return res.status(400).json({ + success: false, + error: 'Title and content are required' + }); + } + + const blog = await this.blogRepository.create({ + title, + content, + published: published || false, + authorId: req.user.id + }); + + res.status(201).json({ + success: true, + data: blog, + message: 'Blog created successfully' + }); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } + } + + async update(req, res) { + try { + const { title, content, published } = req.body; + + const updateData = {}; + if (title) updateData.title = title; + if (content) updateData.content = content; + if (typeof published !== 'undefined') updateData.published = published; + + const blog = await this.blogRepository.update(req.params.id, updateData); + + res.json({ + success: true, + data: blog, + message: 'Blog updated successfully' + }); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } + } + + async delete(req, res) { + try { + await this.blogRepository.delete(req.params.id); + + res.json({ + success: true, + message: 'Blog deleted successfully' + }); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } + } +} diff --git a/Backend/harmadik gyakorlat_minta/src/api/middlewares/authMiddleware.js b/Backend/harmadik gyakorlat_minta/src/api/middlewares/authMiddleware.js new file mode 100644 index 0000000..26971c8 --- /dev/null +++ b/Backend/harmadik gyakorlat_minta/src/api/middlewares/authMiddleware.js @@ -0,0 +1,143 @@ +import { + extractTokenFromRequest, + verifyAccessToken +} from '../../infrastructure/auth/authUtils.js'; +import { UserRepository } from '../../infrastructure/repositories/UserRepository.js'; + +const userRepository = new UserRepository(); + +/** + * Authentication Middleware + * + * Ellenőrzi a JWT tokent és beállítja req.user-t + */ +export async function authenticateToken(req, res, next) { + try { + // 1. Token kiolvasása (cookie vagy Authorization header) + const token = extractTokenFromRequest(req); + + if (!token) { + return res.status(401).json({ + success: false, + error: 'Access token required' + }); + } + + // 2. Token validálás + const decoded = verifyAccessToken(token); + + // 3. User lekérése az adatbázisból + const user = await userRepository.findById(decoded.userId); + + if (!user) { + return res.status(401).json({ + success: false, + error: 'User not found' + }); + } + + // 4. User hozzáadása a request objektumhoz + req.user = user; + + // 5. Továbblépés a következő middleware-re + next(); + } catch (error) { + console.error('Authentication error:', error); + return res.status(401).json({ + success: false, + error: 'Invalid or expired token' + }); + } +} + +/** + * Authorization Middleware - Role Check + * + * Ellenőrzi, hogy a usernek megfelel ő role-ja van-e + * + * @param {string[]} allowedRoles - Engedélyezett role-ok listája + * @returns {Function} Express middleware + */ +export function requireRole(allowedRoles) { + return (req, res, next) => { + try { + // 1. Ellenőrzés: req.user létezik? (authenticateToken után fut) + if (!req.user) { + return res.status(401).json({ + success: false, + error: 'Authentication required' + }); + } + + // 2. Role check: van-e a usernek megfelelő role-ja? + if (!allowedRoles.includes(req.user.role)) { + return res.status(403).json({ + success: false, + error: 'Insufficient permissions' + }); + } + + // 3. OK, továbblépés + next(); + } catch (error) { + console.error('Authorization error:', error); + return res.status(403).json({ + success: false, + error: 'Authorization failed' + }); + } + }; +} + +/** + * Resource Ownership Check + * + * Ellenőrzi, hogy a user tulajdonosa-e az adott resource-nak + * VAGY admin role-ja van + * + * @param {Function} getResourceOwnerId - Async függvény ami visszaadja a resource owner ID-t + * @returns {Function} Express middleware + */ +export function checkOwnership(getResourceOwnerId) { + return async (req, res, next) => { + try { + // 1. User ellenőrzés + if (!req.user) { + return res.status(401).json({ + success: false, + error: 'Authentication required' + }); + } + + // 2. Resource owner ID lekérése + const ownerId = await getResourceOwnerId(req); + + if (!ownerId) { + return res.status(404).json({ + success: false, + error: 'Resource not found' + }); + } + + // 3. Ownership ellenőrzés: user tulajdonos VAGY admin + const isOwner = req.user.id === ownerId; + const isAdmin = req.user.role === 'ADMIN'; + + if (!isOwner && !isAdmin) { + return res.status(403).json({ + success: false, + error: 'You do not have permission to access this resource' + }); + } + + // 4. OK, továbblépés + next(); + } catch (error) { + console.error('Ownership check error:', error); + return res.status(500).json({ + success: false, + error: 'Ownership verification failed' + }); + } + }; +} diff --git a/Backend/harmadik gyakorlat_minta/src/api/middlewares/errorHandler.js b/Backend/harmadik gyakorlat_minta/src/api/middlewares/errorHandler.js new file mode 100644 index 0000000..7d59472 --- /dev/null +++ b/Backend/harmadik gyakorlat_minta/src/api/middlewares/errorHandler.js @@ -0,0 +1,19 @@ +export function errorHandler(err, req, res, next) { + console.error('Error:', err); + + const statusCode = err.statusCode || 500; + const message = err.message || 'Internal Server Error'; + + res.status(statusCode).json({ + success: false, + error: message, + ...(process.env.NODE_ENV === 'development' && { stack: err.stack }) + }); +} + +export function notFoundHandler(req, res) { + res.status(404).json({ + success: false, + error: 'Endpoint not found' + }); +} diff --git a/Backend/harmadik gyakorlat_minta/src/api/routes/authRoutes.js b/Backend/harmadik gyakorlat_minta/src/api/routes/authRoutes.js new file mode 100644 index 0000000..ce92402 --- /dev/null +++ b/Backend/harmadik gyakorlat_minta/src/api/routes/authRoutes.js @@ -0,0 +1,17 @@ +import express from 'express'; +import { AuthController } from '../controllers/AuthController.js'; +import { authenticateToken } from '../middlewares/authMiddleware.js'; + +const router = express.Router(); +const authController = new AuthController(); + +// Publikus route-ok +router.post('/register', (req, res) => authController.register(req, res)); +router.post('/login', (req, res) => authController.login(req, res)); +router.post('/refresh', (req, res) => authController.refreshToken(req, res)); + +// Védett route-ok (authenticateToken middleware) +router.post('/logout', authenticateToken, (req, res) => authController.logout(req, res)); +router.get('/me', authenticateToken, (req, res) => authController.getCurrentUser(req, res)); + +export default router; diff --git a/Backend/harmadik gyakorlat_minta/src/api/routes/blogRoutes.js b/Backend/harmadik gyakorlat_minta/src/api/routes/blogRoutes.js new file mode 100644 index 0000000..62ac6a6 --- /dev/null +++ b/Backend/harmadik gyakorlat_minta/src/api/routes/blogRoutes.js @@ -0,0 +1,38 @@ +import express from 'express'; +import { BlogController } from '../controllers/BlogController.js'; +import { authenticateToken, checkOwnership } from '../middlewares/authMiddleware.js'; +import { BlogRepository } from '../../infrastructure/repositories/BlogRepository.js'; + +const router = express.Router(); +const blogController = new BlogController(); +const blogRepository = new BlogRepository(); + +// Publikus route-ok +router.get('/', (req, res) => blogController.getAll(req, res)); +router.get('/:id', (req, res) => blogController.getById(req, res)); + +// Védett route-ok +router.post('/', + authenticateToken, + (req, res) => blogController.create(req, res) +); + +router.put('/:id', + authenticateToken, + checkOwnership(async (req) => { + const blog = await blogRepository.findById(parseInt(req.params.id)); + return blog?.authorId; + }), + (req, res) => blogController.update(req, res) +); + +router.delete('/:id', + authenticateToken, + checkOwnership(async (req) => { + const blog = await blogRepository.findById(parseInt(req.params.id)); + return blog?.authorId; + }), + (req, res) => blogController.delete(req, res) +); + +export default router; diff --git a/Backend/harmadik gyakorlat_minta/src/domain/models/Blog.js b/Backend/harmadik gyakorlat_minta/src/domain/models/Blog.js new file mode 100644 index 0000000..65ee27d --- /dev/null +++ b/Backend/harmadik gyakorlat_minta/src/domain/models/Blog.js @@ -0,0 +1,29 @@ +export class Blog { + constructor(data) { + this.id = data.id; + this.title = data.title; + this.content = data.content; + this.published = data.published; + this.authorId = data.authorId; + this.createdAt = data.createdAt; + this.updatedAt = data.updatedAt; + this.author = data.author; + } + + toJSON() { + return { + id: this.id, + title: this.title, + content: this.content, + published: this.published, + authorId: this.authorId, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + author: this.author ? { + id: this.author.id, + username: this.author.username, + email: this.author.email + } : null + }; + } +} diff --git a/Backend/harmadik gyakorlat_minta/src/domain/models/User.js b/Backend/harmadik gyakorlat_minta/src/domain/models/User.js new file mode 100644 index 0000000..6ff5ff6 --- /dev/null +++ b/Backend/harmadik gyakorlat_minta/src/domain/models/User.js @@ -0,0 +1,20 @@ +export class User { + constructor(data) { + this.id = data.id; + this.email = data.email; + this.username = data.username; + this.password = data.password; + this.role = data.role; + this.createdAt = data.createdAt; + this.updatedAt = data.updatedAt; + } + + isAdmin() { + return this.role === 'ADMIN'; + } + + toJSON() { + const { password, ...userWithoutPassword } = this; + return userWithoutPassword; + } +} diff --git a/Backend/harmadik gyakorlat_minta/src/domain/repositories/IBlogRepository.js b/Backend/harmadik gyakorlat_minta/src/domain/repositories/IBlogRepository.js new file mode 100644 index 0000000..3ca608a --- /dev/null +++ b/Backend/harmadik gyakorlat_minta/src/domain/repositories/IBlogRepository.js @@ -0,0 +1,25 @@ +export class IBlogRepository { + async findById(id) { + throw new Error('findById() must be implemented'); + } + + async findAll() { + throw new Error('findAll() must be implemented'); + } + + async findByAuthorId(authorId) { + throw new Error('findByAuthorId() must be implemented'); + } + + async create(blogData) { + throw new Error('create() must be implemented'); + } + + async update(id, blogData) { + throw new Error('update() must be implemented'); + } + + async delete(id) { + throw new Error('delete() must be implemented'); + } +} diff --git a/Backend/harmadik gyakorlat_minta/src/domain/repositories/IUserRepository.js b/Backend/harmadik gyakorlat_minta/src/domain/repositories/IUserRepository.js new file mode 100644 index 0000000..0c5ad70 --- /dev/null +++ b/Backend/harmadik gyakorlat_minta/src/domain/repositories/IUserRepository.js @@ -0,0 +1,29 @@ +export class IUserRepository { + async findById(id) { + throw new Error('findById() must be implemented'); + } + + async findByEmail(email) { + throw new Error('findByEmail() must be implemented'); + } + + async findByUsername(username) { + throw new Error('findByUsername() must be implemented'); + } + + async create(userData) { + throw new Error('create() must be implemented'); + } + + async update(id,userData) { + throw new Error('update() must be implemented'); + } + + async delete(id) { + throw new Error('delete() must be implemented'); + } + + async findAll() { + throw new Error('findAll() must be implemented'); + } +} diff --git a/Backend/harmadik gyakorlat_minta/src/index.js b/Backend/harmadik gyakorlat_minta/src/index.js new file mode 100644 index 0000000..5c9dc4f --- /dev/null +++ b/Backend/harmadik gyakorlat_minta/src/index.js @@ -0,0 +1,62 @@ +import express from 'express'; +import cookieParser from 'cookie-parser'; +import dotenv from 'dotenv'; + +dotenv.config(); + +import databaseClient from './infrastructure/database/prisma.js'; +import redisClient from './infrastructure/database/redis.js'; + +import authRoutes from './api/routes/authRoutes.js'; +import blogRoutes from './api/routes/blogRoutes.js'; + +import { errorHandler, notFoundHandler } from './api/middlewares/errorHandler.js'; + +const app = express(); +const PORT = process.env.PORT || 3001; + +// Middleware +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +app.use(cookieParser(process.env.COOKIE_SECRET)); + +// Database connection +await databaseClient.connect(); + +// Health check +app.get('/health', (req, res) => { + res.json({ + status: 'OK', + timestamp: new Date().toISOString(), + environment: process.env.NODE_ENV + }); +}); + +// Routes +app.use('/api/auth', authRoutes); +app.use('/api/blogs', blogRoutes); + +// Error handling +app.use(notFoundHandler); +app.use(errorHandler); + +// Graceful shutdown +process.on('SIGINT', async () => { + console.log('\n🛑 Shutting down gracefully...'); + await databaseClient.disconnect(); + await redisClient.disconnect(); + process.exit(0); +}); + +process.on('SIGTERM', async () => { + console.log('\n🛑 Shutting down gracefully...'); + await databaseClient.disconnect(); + await redisClient.disconnect(); + process.exit(0); +}); + +// Start server +app.listen(PORT, () => { + console.log(`✅ Server running on http://localhost:${PORT}`); + console.log(`📊 Environment: ${process.env.NODE_ENV}`); +}); diff --git a/Backend/harmadik gyakorlat_minta/src/infrastructure/auth/authUtils.js b/Backend/harmadik gyakorlat_minta/src/infrastructure/auth/authUtils.js new file mode 100644 index 0000000..980dd04 --- /dev/null +++ b/Backend/harmadik gyakorlat_minta/src/infrastructure/auth/authUtils.js @@ -0,0 +1,231 @@ +import jwt from 'jsonwebtoken'; +import bcrypt from 'bcrypt'; +import { redis } from '../database/redis.js'; + +/** + * JWT Token Generálás + */ + +export function generateAccessToken(user) { + const payload = { + userId: user.id, + email: user.email, + role: user.role + }; + + return jwt.sign( + payload, + process.env.JWT_SECRET, + { expiresIn: process.env.JWT_EXPIRES_IN || '7d' } + ); +} + +export function generateRefreshToken(user) { + const payload = { + userId: user.id + }; + + return jwt.sign( + payload, + process.env.JWT_REFRESH_SECRET, + { expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '30d' } + ); +} + +/** + * JWT Token Validálás + */ + +export function verifyAccessToken(token) { + try { + return jwt.verify(token, process.env.JWT_SECRET); + } catch (error) { + if (error.name === 'TokenExpiredError') { + throw new Error('Token expired'); + } + if (error.name === 'JsonWebTokenError') { + throw new Error('Invalid token'); + } + throw error; + } +} + +export function verifyRefreshToken(token) { + try { + return jwt.verify(token, process.env.JWT_REFRESH_SECRET); + } catch (error) { + throw new Error('Invalid refresh token'); + } +} + +/** + * Token Kiolvasás Request-ből + */ + +export function extractTokenFromRequest(req) { + // 1. Cookie-ból + if (req.cookies && req.cookies.accessToken) { + return req.cookies.accessToken; + } + + // 2. Authorization header-ből + const authHeader = req.headers.authorization; + if (authHeader && authHeader.startsWith('Bearer ')) { + return authHeader.substring(7); + } + + return null; +} + +/** + * Jelszó Hash-elés és Validálás + */ + +export async function hashPassword(plainPassword) { + const saltRounds = 10; + return await bcrypt.hash(plainPassword, saltRounds); +} + +export async function verifyPassword(plainPassword, hashedPassword) { + return await bcrypt.compare(plainPassword, hashedPassword); +} + +/** + * Cookie Beállítás + */ + +export function setAuthCookies(res, accessToken, refreshToken) { + const cookieOptions = { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict' + }; + + // Access token cookie - 7 nap + res.cookie('accessToken', accessToken, { + ...cookieOptions, + maxAge: 7 * 24 * 60 * 60 * 1000 + }); + + // Refresh token cookie - 30 nap + res.cookie('refreshToken', refreshToken, { + ...cookieOptions, + maxAge: 30 * 24 * 60 * 60 * 1000 + }); +} + +export function clearAuthCookies(res) { + res.clearCookie('accessToken'); + res.clearCookie('refreshToken'); +} + +/** + * Input Validáció + */ + +export function isValidEmail(email) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); +} + +export function isValidPassword(password) { + return password && password.length >= 8; +} + +export function validateRegisterInput(data) { + const errors = []; + + if (!data.email || !isValidEmail(data.email)) { + errors.push('Valid email is required'); + } + + if (!data.username || data.username.length < 3) { + errors.push('Username must be at least 3 characters'); + } + + if (!data.password || !isValidPassword(data.password)) { + errors.push('Password must be at least 8 characters'); + } + + return { + isValid: errors.length === 0, + errors + }; +} + +export function validateLoginInput(data) { + const errors = []; + + if (!data.password) { + errors.push('Password is required'); + } + + if (!data.email && !data.username) { + errors.push('Email or username is required'); + } + + return { + isValid: errors.length === 0, + errors + }; +} + +/** + * Redis Session Management (Opcionális) + */ + +export async function createSession(userId, sessionData) { + try { + const sessionKey = `session:${userId}`; + const ttl = 7 * 24 * 60 * 60; // 7 nap másodpercben + + await redis.set( + sessionKey, + JSON.stringify({ + userId, + loginTime: new Date().toISOString(), + ...sessionData + }), + 'EX', + ttl + ); + } catch (error) { + console.error('Redis session create error:', error); + } +} + +export async function getSession(userId) { + try { + const sessionKey = `session:${userId}`; + const session = await redis.get(sessionKey); + + if (!session) { + return null; + } + + return JSON.parse(session); + } catch (error) { + console.error('Redis session get error:', error); + return null; + } +} + +export async function deleteSession(userId) { + try { + const sessionKey = `session:${userId}`; + await redis.del(sessionKey); + } catch (error) { + console.error('Redis session delete error:', error); + } +} + +export async function sessionExists(userId) { + try { + const sessionKey = `session:${userId}`; + const exists = await redis.exists(sessionKey); + return exists === 1; + } catch (error) { + console.error('Redis session exists error:', error); + return false; + } +} diff --git a/Backend/harmadik gyakorlat_minta/src/infrastructure/database/prisma.js b/Backend/harmadik gyakorlat_minta/src/infrastructure/database/prisma.js new file mode 100644 index 0000000..5398b4d --- /dev/null +++ b/Backend/harmadik gyakorlat_minta/src/infrastructure/database/prisma.js @@ -0,0 +1,22 @@ +import { PrismaClient } from '@prisma/client'; + +export const prisma = new PrismaClient({ + log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], +}); + +export default { + client: prisma, + connect: async () => { + try { + await prisma.$connect(); + console.log('✅ Database connected'); + } catch (error) { + console.error('❌ Database connection failed:', error); + process.exit(1); + } + }, + disconnect: async () => { + await prisma.$disconnect(); + console.log('Database disconnected'); + } +}; diff --git a/Backend/harmadik gyakorlat_minta/src/infrastructure/database/redis.js b/Backend/harmadik gyakorlat_minta/src/infrastructure/database/redis.js new file mode 100644 index 0000000..363b9e4 --- /dev/null +++ b/Backend/harmadik gyakorlat_minta/src/infrastructure/database/redis.js @@ -0,0 +1,28 @@ +import Redis from 'ioredis'; + +const redis = new Redis({ + host: process.env.REDIS_HOST || 'localhost', + port: parseInt(process.env.REDIS_PORT) || 6380, + retryStrategy: (times) => { + const delay = Math.min(times * 50, 2000); + return delay; + } +}); + +redis.on('connect', () => { + console.log('✅ Redis connected'); +}); + +redis.on('error', (error) => { + console.error('❌ Redis connection error:', error); +}); + +export { redis }; + +export default { + client: redis, + disconnect: async () => { + await redis.quit(); + console.log('Redis disconnected'); + } +}; diff --git a/Backend/harmadik gyakorlat_minta/src/infrastructure/repositories/BlogRepository.js b/Backend/harmadik gyakorlat_minta/src/infrastructure/repositories/BlogRepository.js new file mode 100644 index 0000000..1ce5896 --- /dev/null +++ b/Backend/harmadik gyakorlat_minta/src/infrastructure/repositories/BlogRepository.js @@ -0,0 +1,54 @@ +import { IBlogRepository } from '../../domain/repositories/IBlogRepository.js'; +import { Blog } from '../../domain/models/Blog.js'; +import { prisma } from '../database/prisma.js'; + +export class BlogRepository extends IBlogRepository { + async findById(id) { + const blog = await prisma.blog.findUnique({ + where: { id }, + include: { author: true } + }); + return blog ? new Blog(blog) : null; + } + + async findAll() { + const blogs = await prisma.blog.findMany({ + include: { author: true }, + orderBy: { createdAt: 'desc' } + }); + return blogs.map(blog => new Blog(blog)); + } + + async findByAuthorId(authorId) { + const blogs = await prisma.blog.findMany({ + where: { authorId }, + include: { author: true }, + orderBy: { createdAt: 'desc' } + }); + return blogs.map(blog => new Blog(blog)); + } + + async create(blogData) { + const blog = await prisma.blog.create({ + data: blogData, + include: { author: true } + }); + return new Blog(blog); + } + + async update(id, blogData) { + const blog = await prisma.blog.update({ + where: { id }, + data: blogData, + include: { author: true } + }); + return new Blog(blog); + } + + async delete(id) { + await prisma.blog.delete({ + where: { id } + }); + return true; + } +} diff --git a/Backend/harmadik gyakorlat_minta/src/infrastructure/repositories/UserRepository.js b/Backend/harmadik gyakorlat_minta/src/infrastructure/repositories/UserRepository.js new file mode 100644 index 0000000..b79c112 --- /dev/null +++ b/Backend/harmadik gyakorlat_minta/src/infrastructure/repositories/UserRepository.js @@ -0,0 +1,55 @@ +import { IUserRepository } from '../../domain/repositories/IUserRepository.js'; +import { User } from '../../domain/models/User.js'; +import { prisma } from '../database/prisma.js'; + +export class UserRepository extends IUserRepository { + async findById(id) { + const user = await prisma.user.findUnique({ + where: { id } + }); + return user ? new User(user) : null; + } + + async findByEmail(email) { + const user = await prisma.user.findUnique({ + where: { email } + }); + return user ? new User(user) : null; + } + + async findByUsername(username) { + const user = await prisma.user.findUnique({ + where: { username } + }); + return user ? new User(user) : null; + } + + async create(userData) { + const user = await prisma.user.create({ + data: userData + }); + return new User(user); + } + + async update(id, userData) { + const user = await prisma.user.update({ + where: { id }, + data: userData + }); + return new User(user); + } + + async delete(id) { + await prisma.user.delete({ + where: { id } + }); + return true; + } + + async findAll() { + const users = await prisma.user.findMany({ + orderBy: { createdAt: 'desc' } + }); + return users.map(user => new User(user)); + } +} diff --git a/Backend/második gyakorlat_minta/.env.example b/Backend/második gyakorlat_minta/.env.example new file mode 100644 index 0000000..c2069f7 --- /dev/null +++ b/Backend/második gyakorlat_minta/.env.example @@ -0,0 +1,2 @@ +PORT=3000 +DATABASE_URL="file:./dev.db" diff --git a/Backend/második gyakorlat_minta/.gitignore b/Backend/második gyakorlat_minta/.gitignore new file mode 100644 index 0000000..0af9248 --- /dev/null +++ b/Backend/második gyakorlat_minta/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +.env +dev.db +dev.db-journal +prisma/migrations/ diff --git a/Backend/második gyakorlat_minta/README.md b/Backend/második gyakorlat_minta/README.md new file mode 100644 index 0000000..a10f98b --- /dev/null +++ b/Backend/második gyakorlat_minta/README.md @@ -0,0 +1,80 @@ +# Második Gyakorlat - MINTA Megoldás + +Ez a mappa tartalmazza a második gyakorlat **teljes, működő megoldását**. + +## Amit implementáltunk: + +### 1. **Prisma ORM + SQLite** +- Prisma schema definiálása +- User model adatbázis szinten +- Migrációk kezelése + +### 2. **CQRS Pattern + Layered Architecture** +- **API Layer**: Controllers, Routes +- **Application Layer**: Commands, Queries, Handlers +- **Domain Layer**: Models, Interfaces +- **Infrastructure Layer**: Prisma, Repositories + +### 3. **Repository Pattern** +- `IUserRepository` interface +- `UserRepository` Prisma implementáció +- Minden adatbázis művelet a repository-n keresztül + +### 4. **Domain Model** +- User osztály üzleti logikával +- Validációk +- Helper metódusok (isAdmin, canEdit, validate) + +## Indítás + +```bash +npm install +npx prisma generate +npx prisma migrate dev --name init +npm run dev +``` + +A szerver elindul a `http://localhost:3000` címen. + +## API Endpoints + +- `GET /api/users` - Összes user +- `GET /api/users/:id` - Egy user +- `POST /api/users` - User létrehozása +- `PUT /api/users/:id` - User frissítése +- `DELETE /api/users/:id` - User törlése + +## Példa Használat + +```bash +# User létrehozása +curl -X POST http://localhost:3000/api/users \ + -H "Content-Type: application/json" \ + -d '{ + "email": "test@test.com", + "name": "Test User", + "password": "password123" + }' + +# Összes user lekérése +curl http://localhost:3000/api/users + +# User frissítése +curl -X PUT http://localhost:3000/api/users/1 \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Updated Name" + }' + +# User törlése +curl -X DELETE http://localhost:3000/api/users/1 +``` + +## Tanulási Pontok + +1. **Prisma ORM**: Modern ORM használata Node.js-ben +2. **SQLite**: Egyszerű file-based adatbázis +3. **CQRS**: Command/Query szétválasztás +4. **Layered Architecture**: Rétegezett architektúra +5. **Domain Model**: Üzleti logika a domain rétegben +6. **Repository Pattern**: Adatelérés absztrakciója diff --git a/Backend/második gyakorlat_minta/nodemon.json b/Backend/második gyakorlat_minta/nodemon.json new file mode 100644 index 0000000..5339720 --- /dev/null +++ b/Backend/második gyakorlat_minta/nodemon.json @@ -0,0 +1,6 @@ +{ + "watch": ["src"], + "ext": "js,json", + "ignore": ["node_modules"], + "exec": "node ./src/api/server.js" +} diff --git a/Backend/második gyakorlat_minta/package.json b/Backend/második gyakorlat_minta/package.json new file mode 100644 index 0000000..f011515 --- /dev/null +++ b/Backend/második gyakorlat_minta/package.json @@ -0,0 +1,25 @@ +{ + "name": "user-management-backend-minta", + "version": "1.0.0", + "description": "MINTA - User management backend with CQRS and layered architecture", + "main": "./src/api/server.js", + "scripts": { + "start": "node ./src/api/server.js", + "dev": "nodemon ./src/api/server.js", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev", + "prisma:studio": "prisma studio" + }, + "keywords": ["express", "prisma", "sqlite", "cqrs"], + "author": "", + "license": "ISC", + "dependencies": { + "express": "^4.18.2", + "@prisma/client": "^5.8.0", + "dotenv": "^16.3.1" + }, + "devDependencies": { + "nodemon": "^3.0.2", + "prisma": "^5.8.0" + } +} diff --git a/Backend/második gyakorlat_minta/prisma/schema.prisma b/Backend/második gyakorlat_minta/prisma/schema.prisma new file mode 100644 index 0000000..561d041 --- /dev/null +++ b/Backend/második gyakorlat_minta/prisma/schema.prisma @@ -0,0 +1,21 @@ +// 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 = "sqlite" + url = env("DATABASE_URL") +} + +model User { + id Int @id @default(autoincrement()) + email String @unique + name String + password String + role String @default("user") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/Backend/második gyakorlat_minta/src/api/controllers/UserController.js b/Backend/második gyakorlat_minta/src/api/controllers/UserController.js new file mode 100644 index 0000000..b260c56 --- /dev/null +++ b/Backend/második gyakorlat_minta/src/api/controllers/UserController.js @@ -0,0 +1,116 @@ +const CreateUserCommand = require('../../application/user/command/CreateUserCommand'); +const CreateUserHandler = require('../../application/user/command/CreateUserHandler'); +const UpdateUserCommand = require('../../application/user/command/UpdateUserCommand'); +const UpdateUserHandler = require('../../application/user/command/UpdateUserHandler'); +const DeleteUserCommand = require('../../application/user/command/DeleteUserCommand'); +const DeleteUserHandler = require('../../application/user/command/DeleteUserHandler'); +const GetUserQuery = require('../../application/user/query/GetUserQuery'); +const GetUserHandler = require('../../application/user/query/GetUserHandler'); +const GetAllUsersQuery = require('../../application/user/query/GetAllUsersQuery'); +const GetAllUsersHandler = require('../../application/user/query/GetAllUsersHandler'); + +class UserController { + constructor() { + this.createUserHandler = new CreateUserHandler(); + this.updateUserHandler = new UpdateUserHandler(); + this.deleteUserHandler = new DeleteUserHandler(); + this.getUserHandler = new GetUserHandler(); + this.getAllUsersHandler = new GetAllUsersHandler(); + } + + async getAllUsers(req, res) { + try { + const query = new GetAllUsersQuery(); + const users = await this.getAllUsersHandler.handle(query); + + res.json({ + success: true, + data: users + }); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } + } + + async getUserById(req, res) { + try { + const query = new GetUserQuery(parseInt(req.params.id)); + const user = await this.getUserHandler.handle(query); + + res.json({ + success: true, + data: user + }); + } catch (error) { + res.status(404).json({ + success: false, + error: error.message + }); + } + } + + async createUser(req, res) { + try { + const { email, name, password, role } = req.body; + const command = new CreateUserCommand(email, name, password, role); + const user = await this.createUserHandler.handle(command); + + res.status(201).json({ + success: true, + data: user, + message: 'User sikeresen létrehozva' + }); + } catch (error) { + res.status(400).json({ + success: false, + error: error.message + }); + } + } + + async updateUser(req, res) { + try { + const { email, name, role } = req.body; + const command = new UpdateUserCommand( + parseInt(req.params.id), + email, + name, + role + ); + const user = await this.updateUserHandler.handle(command); + + res.json({ + success: true, + data: user, + message: 'User sikeresen frissítve' + }); + } catch (error) { + res.status(400).json({ + success: false, + error: error.message + }); + } + } + + async deleteUser(req, res) { + try { + const command = new DeleteUserCommand(parseInt(req.params.id)); + const result = await this.deleteUserHandler.handle(command); + + res.json({ + success: true, + message: result.message + }); + } catch (error) { + res.status(404).json({ + success: false, + error: error.message + }); + } + } +} + +module.exports = UserController; diff --git a/Backend/második gyakorlat_minta/src/api/routes/user.routes.js b/Backend/második gyakorlat_minta/src/api/routes/user.routes.js new file mode 100644 index 0000000..104395e --- /dev/null +++ b/Backend/második gyakorlat_minta/src/api/routes/user.routes.js @@ -0,0 +1,16 @@ +const express = require('express'); +const UserController = require('../controllers/UserController'); + +const router = express.Router(); +const userController = new UserController(); + +// Query routes (olvasás) +router.get('/', (req, res) => userController.getAllUsers(req, res)); +router.get('/:id', (req, res) => userController.getUserById(req, res)); + +// Command routes (írás) +router.post('/', (req, res) => userController.createUser(req, res)); +router.put('/:id', (req, res) => userController.updateUser(req, res)); +router.delete('/:id', (req, res) => userController.deleteUser(req, res)); + +module.exports = router; diff --git a/Backend/második gyakorlat_minta/src/api/server.js b/Backend/második gyakorlat_minta/src/api/server.js new file mode 100644 index 0000000..13d2003 --- /dev/null +++ b/Backend/második gyakorlat_minta/src/api/server.js @@ -0,0 +1,51 @@ +require('dotenv').config(); +const express = require('express'); +const userRoutes = require('./routes/user.routes'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Middleware +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + +// Routes +app.use('/api/users', userRoutes); + +// Alapértelmezett route +app.get('/', (req, res) => { + res.json({ + message: 'User Management API - MINTA Megoldás', + endpoints: { + users: { + getAll: 'GET /api/users', + getById: 'GET /api/users/:id', + create: 'POST /api/users', + update: 'PUT /api/users/:id', + delete: 'DELETE /api/users/:id' + } + } + }); +}); + +// 404 handler +app.use((req, res) => { + res.status(404).json({ + success: false, + message: 'Endpoint nem található!' + }); +}); + +// Error handler +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).json({ + success: false, + message: 'Szerver hiba történt!' + }); +}); + +app.listen(PORT, () => { + console.log(`✅ Szerver fut a http://localhost:${PORT} címen`); + console.log(`📊 Prisma Studio: npx prisma studio`); +}); diff --git a/Backend/második gyakorlat_minta/src/application/user/command/CreateUserCommand.js b/Backend/második gyakorlat_minta/src/application/user/command/CreateUserCommand.js new file mode 100644 index 0000000..bec5c5b --- /dev/null +++ b/Backend/második gyakorlat_minta/src/application/user/command/CreateUserCommand.js @@ -0,0 +1,10 @@ +class CreateUserCommand { + constructor(email, name, password, role) { + this.email = email; + this.name = name; + this.password = password; + this.role = role; + } +} + +module.exports = CreateUserCommand; diff --git a/Backend/második gyakorlat_minta/src/application/user/command/CreateUserHandler.js b/Backend/második gyakorlat_minta/src/application/user/command/CreateUserHandler.js new file mode 100644 index 0000000..9cc5d0c --- /dev/null +++ b/Backend/második gyakorlat_minta/src/application/user/command/CreateUserHandler.js @@ -0,0 +1,41 @@ +const UserRepository = require('../../../infrastructure/repositories/UserRepository'); + +class CreateUserHandler { + constructor() { + this.userRepository = new UserRepository(); + } + + async handle(command) { + // 1. Validáció + if (!command.email || !command.name || !command.password) { + throw new Error('Email, name és password kötelező'); + } + + // 2. Email formátum ellenőrzés + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(command.email)) { + throw new Error('Hibás email formátum'); + } + + // 3. Jelszó hossz ellenőrzés + if (command.password.length < 6) { + throw new Error('Jelszó minimum 6 karakter kell legyen'); + } + + // 4. Duplikáció ellenőrzés + const existing = await this.userRepository.findByEmail(command.email); + if (existing) { + throw new Error('Email már használatban van'); + } + + // 5. User létrehozása + return await this.userRepository.create({ + email: command.email, + name: command.name, + password: command.password, // Production környezetben hash-elni kell! + role: command.role || 'user' + }); + } +} + +module.exports = CreateUserHandler; diff --git a/Backend/második gyakorlat_minta/src/application/user/command/DeleteUserCommand.js b/Backend/második gyakorlat_minta/src/application/user/command/DeleteUserCommand.js new file mode 100644 index 0000000..43807e7 --- /dev/null +++ b/Backend/második gyakorlat_minta/src/application/user/command/DeleteUserCommand.js @@ -0,0 +1,7 @@ +class DeleteUserCommand { + constructor(id) { + this.id = id; + } +} + +module.exports = DeleteUserCommand; diff --git a/Backend/második gyakorlat_minta/src/application/user/command/DeleteUserHandler.js b/Backend/második gyakorlat_minta/src/application/user/command/DeleteUserHandler.js new file mode 100644 index 0000000..7745d7a --- /dev/null +++ b/Backend/második gyakorlat_minta/src/application/user/command/DeleteUserHandler.js @@ -0,0 +1,22 @@ +const UserRepository = require('../../../infrastructure/repositories/UserRepository'); + +class DeleteUserHandler { + constructor() { + this.userRepository = new UserRepository(); + } + + async handle(command) { + const user = await this.userRepository.findById(command.id); + if (!user) { + throw new Error('User nem található'); + } + + await this.userRepository.delete(command.id); + return { + message: 'User sikeresen törölve', + id: command.id + }; + } +} + +module.exports = DeleteUserHandler; diff --git a/Backend/második gyakorlat_minta/src/application/user/command/UpdateUserCommand.js b/Backend/második gyakorlat_minta/src/application/user/command/UpdateUserCommand.js new file mode 100644 index 0000000..58740c0 --- /dev/null +++ b/Backend/második gyakorlat_minta/src/application/user/command/UpdateUserCommand.js @@ -0,0 +1,10 @@ +class UpdateUserCommand { + constructor(id, email, name, role) { + this.id = id; + this.email = email; + this.name = name; + this.role = role; + } +} + +module.exports = UpdateUserCommand; diff --git a/Backend/második gyakorlat_minta/src/application/user/command/UpdateUserHandler.js b/Backend/második gyakorlat_minta/src/application/user/command/UpdateUserHandler.js new file mode 100644 index 0000000..8d956fe --- /dev/null +++ b/Backend/második gyakorlat_minta/src/application/user/command/UpdateUserHandler.js @@ -0,0 +1,34 @@ +const UserRepository = require('../../../infrastructure/repositories/UserRepository'); + +class UpdateUserHandler { + constructor() { + this.userRepository = new UserRepository(); + } + + async handle(command) { + // 1. User létezésének ellenőrzése + const user = await this.userRepository.findById(command.id); + if (!user) { + throw new Error('User nem található'); + } + + // 2. Email egyediség ellenőrzése (ha változott) + if (command.email && command.email !== user.email) { + const existing = await this.userRepository.findByEmail(command.email); + if (existing) { + throw new Error('Email már használatban van'); + } + } + + // 3. Frissítendő adatok összeállítása + const updateData = {}; + if (command.email) updateData.email = command.email; + if (command.name) updateData.name = command.name; + if (command.role) updateData.role = command.role; + + // 4. Frissítés végrehajtása + return await this.userRepository.update(command.id, updateData); + } +} + +module.exports = UpdateUserHandler; diff --git a/Backend/második gyakorlat_minta/src/application/user/query/GetAllUsersHandler.js b/Backend/második gyakorlat_minta/src/application/user/query/GetAllUsersHandler.js new file mode 100644 index 0000000..00b87d6 --- /dev/null +++ b/Backend/második gyakorlat_minta/src/application/user/query/GetAllUsersHandler.js @@ -0,0 +1,13 @@ +const UserRepository = require('../../../infrastructure/repositories/UserRepository'); + +class GetAllUsersHandler { + constructor() { + this.userRepository = new UserRepository(); + } + + async handle(query) { + return await this.userRepository.findAll(); + } +} + +module.exports = GetAllUsersHandler; diff --git a/Backend/második gyakorlat_minta/src/application/user/query/GetAllUsersQuery.js b/Backend/második gyakorlat_minta/src/application/user/query/GetAllUsersQuery.js new file mode 100644 index 0000000..5d6a2b5 --- /dev/null +++ b/Backend/második gyakorlat_minta/src/application/user/query/GetAllUsersQuery.js @@ -0,0 +1,5 @@ +class GetAllUsersQuery { + constructor() {} +} + +module.exports = GetAllUsersQuery; diff --git a/Backend/második gyakorlat_minta/src/application/user/query/GetUserHandler.js b/Backend/második gyakorlat_minta/src/application/user/query/GetUserHandler.js new file mode 100644 index 0000000..af235d9 --- /dev/null +++ b/Backend/második gyakorlat_minta/src/application/user/query/GetUserHandler.js @@ -0,0 +1,17 @@ +const UserRepository = require('../../../infrastructure/repositories/UserRepository'); + +class GetUserHandler { + constructor() { + this.userRepository = new UserRepository(); + } + + async handle(query) { + const user = await this.userRepository.findById(query.id); + if (!user) { + throw new Error('User nem található'); + } + return user; + } +} + +module.exports = GetUserHandler; diff --git a/Backend/második gyakorlat_minta/src/application/user/query/GetUserQuery.js b/Backend/második gyakorlat_minta/src/application/user/query/GetUserQuery.js new file mode 100644 index 0000000..f5f9beb --- /dev/null +++ b/Backend/második gyakorlat_minta/src/application/user/query/GetUserQuery.js @@ -0,0 +1,7 @@ +class GetUserQuery { + constructor(id) { + this.id = id; + } +} + +module.exports = GetUserQuery; diff --git a/Backend/második gyakorlat_minta/src/domain/interfaces/IUserRepository.js b/Backend/második gyakorlat_minta/src/domain/interfaces/IUserRepository.js new file mode 100644 index 0000000..08da129 --- /dev/null +++ b/Backend/második gyakorlat_minta/src/domain/interfaces/IUserRepository.js @@ -0,0 +1,33 @@ +/** + * IUserRepository Interface + * + * Ez definiálja a UserRepository-nak kötelező metódusokat. + * A Repository Pattern szerint az Infrastructure réteg implementálja ezt. + */ +class IUserRepository { + async create(userData) { + throw new Error('create() metódus nincs implementálva'); + } + + async findById(id) { + throw new Error('findById() metódus nincs implementálva'); + } + + async findByEmail(email) { + throw new Error('findByEmail() metódus nincs implementálva'); + } + + async findAll() { + throw new Error('findAll() metódus nincs implementálva'); + } + + async update(id, userData) { + throw new Error('update() metódus nincs implementálva'); + } + + async delete(id) { + throw new Error('delete() metódus nincs implementálva'); + } +} + +module.exports = IUserRepository; diff --git a/Backend/második gyakorlat_minta/src/domain/models/User.js b/Backend/második gyakorlat_minta/src/domain/models/User.js new file mode 100644 index 0000000..aa9929f --- /dev/null +++ b/Backend/második gyakorlat_minta/src/domain/models/User.js @@ -0,0 +1,56 @@ +class User { + constructor(data) { + this.id = data.id; + this.email = data.email; + this.name = data.name; + this.password = data.password; + this.role = data.role || 'user'; + this.createdAt = data.createdAt; + this.updatedAt = data.updatedAt; + } + + /** + * Ellenőrzi, hogy a user admin-e + */ + isAdmin() { + return this.role === 'admin'; + } + + /** + * Ellenőrzi, hogy a user szerkeszthet-e egy másik usert + * @param {User} targetUser - A szerkeszteni kívánt user + */ + canEdit(targetUser) { + return this.isAdmin() || this.id === targetUser.id; + } + + /** + * User adatok validálása + */ + validate() { + if (!this.email || !this.name || !this.password) { + throw new Error('Email, name és password kötelező'); + } + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(this.email)) { + throw new Error('Hibás email formátum'); + } + + if (this.password.length < 6) { + throw new Error('Jelszó min. 6 karakter'); + } + + return true; + } + + /** + * User objektum JSON-ként (jelszó nélkül!) + */ + toJSON() { + const { password, ...userWithoutPassword } = this; + return userWithoutPassword; + } +} + +module.exports = User; diff --git a/Backend/második gyakorlat_minta/src/infrastructure/database/prisma.js b/Backend/második gyakorlat_minta/src/infrastructure/database/prisma.js new file mode 100644 index 0000000..52b3a93 --- /dev/null +++ b/Backend/második gyakorlat_minta/src/infrastructure/database/prisma.js @@ -0,0 +1,7 @@ +const { PrismaClient } = require('@prisma/client'); + +const prisma = new PrismaClient({ + log: ['query', 'error', 'warn'], +}); + +module.exports = prisma; diff --git a/Backend/második gyakorlat_minta/src/infrastructure/repositories/UserRepository.js b/Backend/második gyakorlat_minta/src/infrastructure/repositories/UserRepository.js new file mode 100644 index 0000000..9de2037 --- /dev/null +++ b/Backend/második gyakorlat_minta/src/infrastructure/repositories/UserRepository.js @@ -0,0 +1,79 @@ +const prisma = require('../database/prisma'); +const IUserRepository = require('../../domain/interfaces/IUserRepository'); + +/** + * UserRepository - Prisma implementáció + * + * Ez az osztály implementálja az IUserRepository interfészt + * és Prisma ORM-et használ az adatbázis műveletekhez. + */ +class UserRepository extends IUserRepository { + /** + * User létrehozása + * @param {Object} userData - { email, name, password, role } + * @returns {Promise} + */ + async create(userData) { + return await prisma.user.create({ + data: userData + }); + } + + /** + * User keresése ID alapján + * @param {number} id + * @returns {Promise} + */ + async findById(id) { + return await prisma.user.findUnique({ + where: { id: parseInt(id) } + }); + } + + /** + * User keresése email alapján + * @param {string} email + * @returns {Promise} + */ + async findByEmail(email) { + return await prisma.user.findUnique({ + where: { email } + }); + } + + /** + * Összes user lekérése + * @returns {Promise} + */ + async findAll() { + return await prisma.user.findMany({ + orderBy: { createdAt: 'desc' } + }); + } + + /** + * User frissítése + * @param {number} id + * @param {Object} userData - Frissítendő mezők + * @returns {Promise} + */ + async update(id, userData) { + return await prisma.user.update({ + where: { id: parseInt(id) }, + data: userData + }); + } + + /** + * User törlése + * @param {number} id + * @returns {Promise} + */ + async delete(id) { + return await prisma.user.delete({ + where: { id: parseInt(id) } + }); + } +} + +module.exports = UserRepository; diff --git a/Backend/negyedik gyakorlat/.env.example b/Backend/negyedik gyakorlat/.env.example new file mode 100644 index 0000000..db51657 --- /dev/null +++ b/Backend/negyedik gyakorlat/.env.example @@ -0,0 +1,17 @@ +# Server Configuration +PORT=3000 +NODE_ENV=development + +# Database Configuration (PostgreSQL + Prisma) +DATABASE_URL="postgresql://postgres:postgres@localhost:5432/cors_di_app?schema=public" + +# JWT Configuration +JWT_SECRET=your-super-secret-jwt-key-change-this-in-production +JWT_EXPIRES_IN=7d + +# Ethereal Email Configuration (https://ethereal.email/create) +ETHEREAL_USER=your-ethereal-user@ethereal.email +ETHEREAL_PASS=your-ethereal-password + +# CORS Configuration (comma-separated origins) +ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001 diff --git a/Backend/negyedik gyakorlat/.gitignore b/Backend/negyedik gyakorlat/.gitignore new file mode 100644 index 0000000..dd2b21a --- /dev/null +++ b/Backend/negyedik gyakorlat/.gitignore @@ -0,0 +1,20 @@ +# Dependencies +node_modules/ + +# Environment +.env + +# Logs +*.log +npm-debug.log* + +# OS +.DS_Store +Thumbs.db + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ diff --git a/Backend/negyedik gyakorlat/FELADAT.md b/Backend/negyedik gyakorlat/FELADAT.md new file mode 100644 index 0000000..27ff6fa --- /dev/null +++ b/Backend/negyedik gyakorlat/FELADAT.md @@ -0,0 +1,1215 @@ +# FELADAT: CORS + Manuális DI Container + Email Notification + +## 🎯 Cél + +Fejlett backend fejlesztési technikák elsajátítása Node.js-ben **Clean Architecture** és **CQRS** mintával: +- **CORS biztonságos konfiguráció** whitelist alapú origin ellenőrzéssel +- **Saját Dependency Injection (DI) container** különböző lifecycle-okkal (singleton, transient, scoped) +- **Email küldés** Nodemailer + Handlebars template alapú értesítésekkel + +**Amit már kész kaptál:** +- ✅ **Clean Architecture** struktúra (api, application, domain, infrastructure) +- ✅ **CQRS Pattern** (Commands/Handlers, Queries/Handlers) +- ✅ **Prisma ORM** PostgreSQL adatbáziskezeléssel +- ✅ **Cookie-based JWT autentikáció** (regisztráció, login, logout, védett endpointok) +- ✅ **bcrypt** jelszó hashelés +- ✅ **Repository Pattern** (Domain interfaces + Infrastructure implementations) +- ✅ **JwtService** (cookie-based token management) +- ✅ **Jest** testing framework + +--- + +## 📐 Architektúra + +Ez a projekt **Clean Architecture** mintát követi **CQRS** (Command Query Responsibility Segregation) pattern-nel: + +### 🏛️ Layer Struktúra + +``` +src/ +├── api/ # Presentation Layer +│ ├── controllers/ # HTTP request handlers +│ ├── middlewares/ # Express middlewares (auth, cors, scope) +│ ├── routers/ # Route definitions +│ └── server.js # Express app setup + DI configuration +│ +├── application/ # Application Layer +│ ├── auth/commands/ # Authentication commands & handlers +│ ├── user/ # User commands & queries +│ │ ├── commands/ # Write operations +│ │ └── queries/ # Read operations +│ ├── services/ # Application services +│ │ ├── JwtService.js ✅ KÉSZ (Cookie-based JWT) +│ │ └── EmailService.js ❌ TODO (FELADAT 3) +│ └── Container.js ❌ TODO (FELADAT 1) +│ +├── domain/ # Domain Layer +│ ├── models/ # Business entities +│ │ └── User.js ✅ KÉSZ (Domain model) +│ └── irepositories/ # Repository interfaces +│ └── IUserRepository.js ✅ KÉSZ +│ +└── infrastructure/ # Infrastructure Layer + ├── db/ # Database connection + │ └── DatabaseConnection.js ✅ KÉSZ (Prisma wrapper) + └── repositories/ # Repository implementations + └── UserRepository.js ✅ KÉSZ (Prisma-based) +``` + +**Key Concepts:** +- **Domain**: Üzleti entitások és szabályok (User model, IUserRepository interface) +- **Application**: Business logic (Commands, Queries, Services, DI Container) +- **Infrastructure**: Külső rendszerekhez kapcsolódás (Prisma, Database) +- **API**: HTTP layer (controllers, routes, middlewares) + +--- + +## 🚀 Projekt Indítása + +### 1. Függőségek telepítése +```bash +npm install +``` + +### 2. Környezeti változók konfigurálása +Másold le a `.env.example` fájlt `.env` néven: +```bash +cp .env.example .env +``` + +Állítsd be az `.env` fájlban: +```env +DATABASE_URL="postgresql://postgres:postgres@localhost:5432/cors_di_app?schema=public" +JWT_SECRET="titkos-kulcs-jwt-hez-változtasd-meg" +JWT_EXPIRES_IN="7d" +PORT=3000 +NODE_ENV=development + +# Ethereal Email (test SMTP) +ETHEREAL_USER=your-ethereal-user@ethereal.email +ETHEREAL_PASS=your-ethereal-password + +# CORS (comma-separated origins) +ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173 +``` + +### 3. Docker indítása (PostgreSQL) +```bash +npm run docker:up +``` + +### 4. Prisma migrációk futtatása +```bash +npm run prisma:migrate +npm run prisma:generate +``` + +### 5. Szerver indítása +```bash +npm run dev +``` + +A szerver elindul: `http://localhost:3000` + +--- + +## 🔐 Kész API Endpoints (Teszteléshez) + +### ⚠️ FONTOS: Cookie-Based Authentication + +Ez a projekt **cookie-based JWT autentikációt** használ (nem localStorage!): +- JWT token **httpOnly cookie-ban** tárolódik +- Authorization header **nem szükséges** +- Böngésző **automatikusan** küldi a cookie-t + +### Publikus endpointok (JWT nem kell) + +#### 1. Regisztráció (Cookie-t ad vissza) +```bash +POST http://localhost:3000/api/auth/register +Content-Type: application/json + +{ + "name": "John Doe", + "email": "john@example.com", + "password": "securePassword123" +} +``` + +**Válasz:** +```http +HTTP/1.1 201 Created +Set-Cookie: auth_token=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=604800 + +{ + "message": "User registered successfully", + "data": { + "user": { + "id": 1, + "name": "John Doe", + "email": "john@example.com", + "createdAt": "2026-03-04T10:00:00.000Z", + "updatedAt": "2026-03-04T10:00:00.000Z" + } + } +} +``` + +**Figyelem:** Token **nincs** a response body-ban, csak cookie-ban! + +#### 2. Bejelentkezés (Cookie-t ad vissza) +```bash +POST http://localhost:3000/api/auth/login +Content-Type: application/json + +{ + "email": "john@example.com", + "password": "securePassword123" +} +``` + +**Válasz:** +```http +HTTP/1.1 200 OK +Set-Cookie: auth_token=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=604800 + +{ + "message": "Login successful", + "data": { + "user": { + "id": 1, + "name": "John Doe", + "email": "john@example.com" + } + } +} +``` + +#### 3. Kijelentkezés (Cookie törlése) +```bash +POST http://localhost:3000/api/auth/logout +Cookie: auth_token= +``` + +**Válasz:** +```http +HTTP/1.1 200 OK +Set-Cookie: auth_token=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0 + +{ + "message": "Logout successful" +} +``` + +### Védett endpointok (JWT cookie szükséges!) + +A böngésző **automatikusan** küldi a cookie-t. cURL-nél használd a `-b` és `-c` flageket. + +#### 4. Aktuális felhasználó lekérdezése +```bash +# cURL +curl -b cookies.txt http://localhost:3000/api/users/me + +# JavaScript (fetch) +fetch('http://localhost:3000/api/users/me', { + credentials: 'include' // Cookie automatikusan küldődik +}) +``` + +#### 5. Aktuális felhasználó frissítése +```bash +PUT http://localhost:3000/api/users/me +Cookie: auth_token= +Content-Type: application/json + +{ + "name": "John Updated" +} +``` + +#### 6. Összes felhasználó lekérdezése +```bash +GET http://localhost:3000/api/users +Cookie: auth_token= +``` + +#### 7. Egy felhasználó lekérdezése ID alapján +```bash +GET http://localhost:3000/api/users/2 +Cookie: auth_token= +``` + +--- + +## 📋 MIT KELL IMPLEMENTÁLNI - 3 FELADAT + +### ⚙️ FELADAT 1: DI Container (src/application/Container.js) +### ⚙️ FELADAT 1: DI Container (src/application/Container.js) + +**Cél:** Dependency Injection konténer implementálása különböző lifecycle-okkal + +**⚠️ FONTOS:** A Container.js fájl **src/application/** mappában van (nem core-ban)! + +A Container három lifecycle típust támogat: +- **Singleton**: Egyszer jön létre, osztva van minden request között +- **Transient**: Minden resolve()-nál új példány jön létre +- **Scoped**: Request szinten megosztott a példány (req.scope-on keresztül) + +**Implementálandó metódusok:** + +```javascript +/** + * Dependency Injection Container + * src/application/Container.js + */ +class Container { + constructor() { + // TODO 1: Inicializáld a services Map-et (singleton instance-ok tárolása) + // TODO 2: Inicializáld a factories Map-et (factory függvények tárolása) + // TODO 3: Inicializáld a lifetimes Map-et (lifecycle típusok tárolása) + + // Példa inicializálás: + // this.services = new Map(); + // this.factories = new Map(); + // this.lifetimes = new Map(); + } + + register(name, factory, lifetime = 'singleton') { + // TODO 4: Tárold el a factory függvényt (this.factories.set(name, factory)) + // TODO 5: Tárold el a lifetime típust (this.lifetimes.set(name, lifetime)) + // TODO 6: Ha a lifetime === 'singleton', azonnal példányosítsd: + // - Hívd meg a factory-t: const instance = factory(); + // - Tárold el: this.services.set(name, instance); + } + + resolve(name, scope = null) { + // TODO 7: Ha a service regisztrálva van mint 'scoped' ÉS van scope paraméter: + // - Ellenőrizd: if (scope && scope.has(name)) return scope.get(name); + // - Ha nincs még a scope-ban, példányosítsd és tárold: + // const instance = this.factories.get(name)(); + // scope.set(name, instance); + // return instance; + + // TODO 8: Ha singleton, add vissza a services-ből: + // if (this.lifetimes.get(name) === 'singleton') { + // return this.services.get(name); + // } + + // TODO 9: Ha transient, minden alkalommal hívj egy új factory-t: + // if (this.lifetimes.get(name) === 'transient') { + // return this.factories.get(name)(); + // } + + // TODO 10: Ha nem regisztrált a service, dobj hibát: + // throw new Error(`Service '${name}' is not registered`); + } + + createScope() { + // TODO 11: Hozz létre egy új Map-et az scoped instance-oknak + // TODO 12: Térj vissza egy objektummal ami tartalmaz egy resolve metódust: + // const scopeMap = new Map(); + // return { + // resolve: (name) => this.resolve(name, scopeMap) + // }; + } +} + +module.exports = Container; +``` + +**Tesztelés:** +A `src/api/server.js` már használja a Container-t: + +```javascript +const container = new Container(); + +// Singleton regisztráció +container.register('PrismaClient', () => { + return databaseConnection.getClient(); +}, 'singleton'); + +// Használat +const prisma = container.resolve('PrismaClient'); +``` + +Indítsd el a szervert és nézd meg, hogy működik-e a DI: +```bash +npm run dev +# ✅ DI Container configured (CQRS pattern) +``` + +**Részletes segítség:** Nézd meg a `SEGÍTSÉG.md` fájlt teljes megoldással! + +--- + +### 🌐 FELADAT 2: CORS Middleware (src/api/middlewares/corsMiddleware.js) + +**Cél:** Whitelist alapú CORS konfiguráció implementálása + +A CORS middleware csak bizonyos origin-ekről engedélyezi a hozzáférést. + +```javascript +const cors = require('cors'); + +// Engedélyezett origin-ek whitelist-je (környezeti változóból vagy default) +const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [ + 'http://localhost:3000', + 'http://localhost:5173' +]; + +const corsOptions = { + origin: function (origin, callback) { + // TODO 1: Ha nincs origin (pl. backend-to-backend, Postman, curl): + // if (!origin) { + // return callback(null, true); + // } + + // TODO 2: Ha az origin benne van az allowedOrigins listában: + // if (allowedOrigins.includes(origin)) { + // return callback(null, true); + // } + + // TODO 3: Egyébként tiltsd le CORS hibával: + // callback(new Error('Not allowed by CORS')); + }, + credentials: true, // Cookie és Authorization header engedélyezése + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization'] +}; + +module.exports = cors(corsOptions); +``` + +**.env beállítás:** +```env +ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173,https://myfrontend.com +``` + +**Aktiválás a server.js-ben:** + +Uncomment this line in `src/api/server.js`: +```javascript +// A middleware chain-ben (körülbelül 130. sor) +app.use(cookieParser()); +app.use(corsMiddleware); // <-- UNCOMMENT THIS LINE! +``` + +**Tesztelés cURL-lel:** +```bash +# Engedélyezett origin - SIKERES +curl -H "Origin: http://localhost:3000" http://localhost:3000/api/users +# Access-Control-Allow-Origin: http://localhost:3000 + +# Tiltott origin - HIBA +curl -H "Origin: http://malicious-site.com" http://localhost:3000/api/users +# Error: Not allowed by CORS +``` + +**Frontend tesztelés (React/Vue):** +```javascript +fetch('http://localhost:3000/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', // FONTOS: Cookie küldés/fogadás engedélyezése + body: JSON.stringify({ email, password }) +}) +.then(res => res.json()) +.then(data => console.log('Login sikeres:', data)) +.catch(err => console.error('CORS hiba:', err)); +``` + +**Részletes segítség:** Nézd meg a `SEGÍTSÉG.md` fájlt teljes megoldással! + +--- + +### 📧 FELADAT 3: Email Service (src/application/services/EmailService.js) + +**Cél:** Nodemailer + Handlebars template alapú email küldés implementálása + +Az EmailService welcome emailt küld regisztráció után. + +**⚠️ FONTOS:** Az EmailService **src/application/services/** mappában van! + +```javascript +const nodemailer = require('nodemailer'); +const handlebars = require('handlebars'); +const fs = require('fs'); +const path = require('path'); + +class EmailService { + constructor() { + // TODO 1: Hozz létre Nodemailer transportot Ethereal tesztelő SMTP-vel + // this.transporter = nodemailer.createTransport({ + // host: 'smtp.ethereal.email', + // port: 587, + // secure: false, // TLS + // auth: { + // user: process.env.ETHEREAL_USER || 'your-test-email@ethereal.email', + // pass: process.env.ETHEREAL_PASS || 'your-test-password' + // } + // }); + + console.log('📧 EmailService initialized'); + } + + async sendWelcomeEmail(userEmail, userName) { + try { + // TODO 2: Olvasd be a welcome.hbs template fájlt + // const templatePath = path.join(__dirname, 'templates', 'welcome.hbs'); + // const templateSource = fs.readFileSync(templatePath, 'utf-8'); + + // TODO 3: Compile-old a template-et Handlebars-szal + // const template = handlebars.compile(templateSource); + + // TODO 4: Generáld a HTML-t a felhasználói adatokkal + // const html = template({ + // name: userName, + // date: new Date().toLocaleDateString('hu-HU') + // }); + + // TODO 5: Küld el az emailt + // const info = await this.transporter.sendMail({ + // from: '"Clean Architecture App" ', + // to: userEmail, + // subject: 'Üdvözlünk az alkalmazásban!', + // html: html + // }); + + // TODO 6: Logold ki az Ethereal preview URL-t + // const previewUrl = nodemailer.getTestMessageUrl(info); + // console.log('📧 Email elküldve:', previewUrl); + + return true; + } catch (error) { + console.error('❌ Email küldési hiba:', error.message); + return false; + } + } +} + +module.exports = EmailService; +``` + +**Email Template létrehozása:** + +Hozd létre: `src/application/services/templates/welcome.hbs` + +```html + + + + + + + +
+

🎉 Üdvözlünk, {{name}}!

+

Sikeres regisztráció az alkalmazásunkban!

+

Regisztráció dátuma: {{date}}

+

Mostantól hozzáférsz az összes funkciónkhoz.

+ +
+ + +``` + +**.env konfigurálás Ethereal SMTP-vel:** + +1. Menj a https://ethereal.email oldalra +2. Kattints a "Create Ethereal Account" gombra +3. Másold ki a kapott user és password értékeket +4. Állítsd be a `.env` fájlban: + +```env +ETHEREAL_USER=your-generated-user@ethereal.email +ETHEREAL_PASS=your-generated-password +``` + +**Tesztelés:** + +Regisztrálj egy új felhasználót: +```bash +curl -X POST http://localhost:3000/api/auth/register \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Test User", + "email": "test@example.com", + "password": "password123" + }' +``` + +**Console output:** +``` +📧 Email elküldve: https://ethereal.email/message/XXXXXX +``` + +Nyisd meg a böngészőben a linket és látni fogod az elküldött emailt! + +**Részletes segítség:** Nézd meg a `SEGÍTSÉG.md` fájlt teljes megoldással és példákkal! + +--- + +## 📁 Projekt Struktúra (Frissített) + +``` +src/ +├── api/ # Presentation Layer +│ ├── controllers/ +│ │ ├── AuthController.js ✅ KÉSZ (Cookie-based JWT) +│ │ └── UserController.js ✅ KÉSZ (CQRS queries/commands) +│ ├── middlewares/ +│ │ ├── authMiddleware.js ✅ KÉSZ (Cookie-based auth) +│ │ ├── corsMiddleware.js ❌ TODO (FELADAT 2) +│ │ └── scopeMiddleware.js 📚 Opcionális +│ ├── routers/ +│ │ ├── authRoutes.js ✅ KÉSZ (register, login, logout) +│ │ └── userRoutes.js ✅ KÉSZ (protected endpoints) +│ └── server.js ✅ KÉSZ (Express + DI setup) +│ +├── application/ # Application Layer +│ ├── auth/commands/ ✅ KÉSZ (CQRS) +│ │ ├── RegisterUserCommand.js +│ │ ├── RegisterUserCommandHandler.js +│ │ ├── LoginUserCommand.js +│ │ └── LoginUserCommandHandler.js +│ ├── user/ ✅ KÉSZ (CQRS) +│ │ ├── commands/ # Write operations +│ │ └── queries/ # Read operations +│ ├── services/ +│ │ ├── JwtService.js ✅ KÉSZ (Cookie-based JWT) +│ │ ├── EmailService.js ❌ TODO (FELADAT 3) +│ │ └── templates/ +│ │ └── welcome.hbs ❌ TODO (Email template) +│ └── Container.js ❌ TODO (FELADAT 1) +│ +├── domain/ # Domain Layer +│ ├── models/ +│ │ └── User.js ✅ KÉSZ (Domain entity) +│ └── irepositories/ +│ └── IUserRepository.js ✅ KÉSZ (Repository interface) +│ +├── infrastructure/ # Infrastructure Layer +│ ├── db/ +│ │ └── DatabaseConnection.js ✅ KÉSZ (Prisma wrapper) +│ └── repositories/ +│ └── UserRepository.js ✅ KÉSZ (Prisma implementation) +│ +├── prisma/ +│ ├── schema.prisma ✅ KÉSZ (User model) +│ └── migrations/ ✅ KÉSZ +│ +└── tests/ # Testing + └── unit/ + ├── commands/ ✅ KÉSZ (Command handler tests) + ├── queries/ ✅ KÉSZ (Query handler tests) + ├── controllers/ ✅ KÉSZ (Controller tests) + ├── middlewares/ ✅ KÉSZ (Middleware tests) + └── services/ ✅ KÉSZ (JwtService tests) +``` + +--- + +## 🎯 Tanulási Célok + +### CORS (Cross-Origin Resource Sharing) +- ✅ Origin alapú hozzáférés-vezérlés implementálása +- ✅ Whitelist konfiguráció production környezetben +- ✅ Preflight request-ek kezelése (OPTIONS) +- ✅ Credential (cookie, auth header) kezelés + +### DI Container (Dependency Injection) +- ✅ Lifecycle management (Singleton, Transient, Scoped) +- ✅ Loose coupling az alkalmazásban +- ✅ Tesztelhetőség javítása (dependency injection) +- ✅ Service registry pattern + +### Email Notification +- ✅ SMTP konfiguráció (Ethereal Email) +- ✅ Template-alapú email generálás (Handlebars) +- ✅ Async email küldés (nem blokkolja a request-et) +- ✅ Test email preview Ethereal-lel + +### Clean Architecture + CQRS +- ✅ Layer separation (Domain, Application, Infrastructure, API) +- ✅ CQRS Pattern (Commands for writes, Queries for reads) +- ✅ Repository Pattern (interface + implementation) +- ✅ Cookie-based JWT authentication (httpOnly, secure, sameSite) + +--- + +## 🧪 Teljes Tesztelési Flow + +### 1. Szerver indítása +```bash +npm run dev +``` + +**Expected output:** +``` +✅ Prisma connected to PostgreSQL +✅ DI Container configured (CQRS pattern) +🚀 Server running on http://localhost:3000 +📧 Email Service configured (implement EmailService!) +🔐 JWT Authentication enabled (cookie-based) +🍪 Cookies: httpOnly, secure (production), sameSite=strict + +📍 Endpoints: + POST /api/auth/register + POST /api/auth/login + POST /api/auth/logout + GET /api/users/me (protected) + PUT /api/users/me (protected) + GET /api/users (protected) + GET /api/users/:id (protected) +``` + +### 2. Regisztráció (cURL cookie példa) +```bash +curl -c cookies.txt -X POST http://localhost:3000/api/auth/register \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Test User", + "email": "test@example.com", + "password": "password123" + }' +``` + +**Kimenet:** +- ✅ JWT token cookie-ban (Set-Cookie header) +- ✅ bcrypt-tel hashelt jelszó az adatbázisban +- ✅ Email elküldve (ha implementáltad a FELADAT 3-at) +- ✅ Cookie mentve a `cookies.txt` fájlba + +### 3. Bejelentkezés +```bash +curl -c cookies.txt -X POST http://localhost:3000/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{ + "email": "test@example.com", + "password": "password123" + }' +``` + +**Kimenet:** +- ✅ JWT token cookie-ban +- ✅ bcrypt.compare jelszó ellenőrzés + +### 4. Védett endpoint hívása (Cookie-val) +```bash +curl -b cookies.txt http://localhost:3000/api/users/me +``` + +**Kimenet:** +- ✅ authMiddleware ellenőrzi a JWT token-t cookie-ból +- ✅ req.user.userId beállítva +- ✅ GetMeQueryHandler lekéri az aktuális user-t Prisma-val + +### 5. Kijelentkezés +```bash +curl -b cookies.txt -c cookies.txt -X POST http://localhost:3000/api/auth/logout +``` + +**Kimenet:** +- ✅ Cookie törlése (Max-Age=0) +- ✅ Következő request-nél 401 Unauthorized + +--- + +## 🧪 Unit Testing + +**Futtatás:** +```bash +npm test # Összes teszt + coverage +npm run test:watch # Watch mode +npm run test:unit # Csak unit tesztek +``` + +**Expected output:** +``` +PASS tests/unit/commands/RegisterUserCommandHandler.test.js +PASS tests/unit/commands/LoginUserCommandHandler.test.js +PASS tests/unit/queries/GetMeQueryHandler.test.js +PASS tests/unit/controllers/AuthController.test.js +PASS tests/unit/middlewares/authMiddleware.test.js +PASS tests/unit/services/JwtService.test.js + +Test Suites: 10 passed, 10 total +Tests: 75 passed, 75 total +``` + +--- + +## ❌ Hibaelhárítás + +### Docker PostgreSQL nem indul +```bash +docker-compose down +docker-compose up -d +docker-compose logs postgres +``` + +### Prisma migration hiba +```bash +npm run prisma:migrate:reset +npm run prisma:migrate +npm run prisma:generate +``` + +### JWT token invalid hiba (Cookie-based) +- ✅ Ellenőrizd, hogy a `.env` fájlban van-e `JWT_SECRET` +- ✅ cURL-nél használd a `-b cookies.txt` flag-et +- ✅ Frontend-en használd a `credentials: 'include'` opciót + +### Email nem megy ki +- ✅ Ellenőrizd az Ethereal SMTP credentials-t (`.env`) +- ✅ Nézd meg a console-t az Ethereal preview URL-ért +- ✅ Hozd létre a `templates/welcome.hbs` fájlt +- ✅ Ellenőrizd, hogy implementáltad-e az EmailService-t + +### CORS hiba frontend-ből +- ✅ Add meg a frontend origin-t az `.env` fájlban: `ALLOWED_ORIGINS=http://localhost:5173` +- ✅ Uncomment a `app.use(corsMiddleware);` sort a `server.js`-ben +- ✅ Implementáld a CORS middleware-t (FELADAT 2) + +--- + +## 📊 Értékelési Szempontok + +### Funkcionális követelmények: +- ✅ DI Container működik (register, resolve, createScope) +- ✅ Singleton, Transient, Scoped lifecycle-ok implementálva +- ✅ CORS whitelist működik (engedélyezett és tiltott origin-ek) +- ✅ Email küldés funkcionális (Handlebars template + Ethereal SMTP) +- ✅ Template fájl létrehozva és használva + +### Kód minőség: +- ✅ Kód tisztaság és SOLID elvek követése +- ✅ Hibakezelés (try-catch, validation) +- ✅ Environment változók használata (.env) +- ✅ Kommentek és dokumentáció +- ✅ Clean Architecture layering betartása + +### Testing: +- ✅ Unit tesztek írása (Container, CORS, EmailService) +- ✅ Tesztek lefutnak és zöldek +- ✅ Megfelelő coverage (>70%) + +--- + +## 📚 Hasznos Források + +- 📖 [SEGÍTSÉG.md](SEGÍTSÉG.md) - Részletes példakódok és megoldások +- 📖 [ARCHITECTURE.md](ARCHITECTURE.md) - Clean Architecture dokumentáció +- 📖 [COOKIE_AUTH_MIGRATION.md](COOKIE_AUTH_MIGRATION.md) - Cookie-based JWT migration guide +- 🌐 [Ethereal Email](https://ethereal.email) - Test SMTP service +- 🌐 [Handlebars](https://handlebarsjs.com/) - Template engine +- 🌐 [Prisma Docs](https://www.prisma.io/docs) - ORM documentation + +--- + +**Sok sikert a feladatokhoz! 🚀** + +Ha elakadtál, nézd meg a `SEGÍTSÉG.md` fájlt teljes megoldásokkal és magyarázatokkal! + +**Cél:** Dependency Injection konténer implementálása különböző lifecycle-okkal + +A Container három lifecycle típust támogat: +- **Singleton**: Egyszer jön létre, osztva van minden request között +- **Transient**: Minden resolve()-nál új példány jön létre +- **Scoped**: Request szinten megosztott a példány (req.scope-on keresztül) + +```javascript +class Container { + constructor() { + // TODO 1: Inicializáld a services Map-et (singleton instance-ok tárolása) + // TODO 2: Inicializáld a factories Map-et (factory függvények tárolása) + // TODO 3: Inicializáld a lifetimes Map-et (lifecycle típusok tárolása) + + // Példa inicializálás: + // this.services = new Map(); + // this.factories = new Map(); + // this.lifetimes = new Map(); + } + + register(name, factory, lifetime = 'singleton') { + // TODO 4: Tárold el a factory függvényt (this.factories.set(name, factory)) + // TODO 5: Tárold el a lifetime típust (this.lifetimes.set(name, lifetime)) + // TODO 6: Ha a lifetime === 'singleton', azonnal példányosítsd: + // - Hívd meg a factory-t: const instance = factory(); + // - Tárold el: this.services.set(name, instance); + } + + resolve(name, scope = null) { + // TODO 7: Ha a service regisztrálva van mint 'scoped' ÉS van scope paraméter: + // - Ellenőrizd: if (scope && scope.has(name)) return scope.get(name); + // - Ha nincs még a scope-ban, példányosítsd és tárold: + // const instance = this.factories.get(name)(); + // scope.set(name, instance); + // return instance; + + // TODO 8: Ha singleton, add vissza a services-ből: + // if (this.lifetimes.get(name) === 'singleton') { + // return this.services.get(name); + // } + + // TODO 9: Ha transient, minden alkalommal hívj egy új factory-t: + // if (this.lifetimes.get(name) === 'transient') { + // return this.factories.get(name)(); + // } + + // TODO 10: Ha nem regisztrált a service, dobj hibát: + // throw new Error(`Service ''${name}'' is not registered`); + } + + createScope() { + // TODO 11: Hozz létre egy új Map-et az scoped instance-oknak + // TODO 12: Térj vissza egy objektummal ami tartalmaz egy resolve metódust: + // const scopeMap = new Map(); + // return { + // resolve: (name) => this.resolve(name, scopeMap) + // }; + } +} + +module.exports = Container; +``` + +**Tesztelés:** +A `src/api/server.js` már használja a Container-t. Indítsd el a szervert és nézd meg, hogy működik-e a DI. + +--- + +### FELADAT 2: CORS Middleware (src/api/middlewares/corsMiddleware.js) + +**Cél:** Whitelist alapú CORS konfiguráció implementálása + +A CORS middleware csak bizonyos origin-ekről engedélyezi a hozzáférést. + +```javascript +const cors = require(''cors''); + +// Engedélyezett origin-ek whitelist-je (környezeti változóból vagy default) +const allowedOrigins = process.env.ALLOWED_ORIGINS?.split('','') || [ + ''http://localhost:3000'', + ''http://localhost:5173'' +]; + +const corsOptions = { + origin: function (origin, callback) { + // TODO 1: Ha nincs origin (pl. backend-to-backend, Postman, curl): + // if (!origin) { + // return callback(null, true); + // } + + // TODO 2: Ha az origin benne van az allowedOrigins listában: + // if (allowedOrigins.includes(origin)) { + // return callback(null, true); + // } + + // TODO 3: Egyébként tiltsd le CORS hibával: + // callback(new Error(''Not allowed by CORS'')); + }, + credentials: true, // Cookie és Authorization header engedélyezése + methods: [''GET'', ''POST'', ''PUT'', ''DELETE'', ''OPTIONS''], + allowedHeaders: [''Content-Type'', ''Authorization''] +}; + +module.exports = cors(corsOptions); +``` + +**.env beállítás:** +```env +ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173,https://myfrontend.com +``` + +**Tesztelés:** +```bash +curl -H "Origin: http://localhost:3000" http://localhost:3000/api/users +# Sikeres - engedélyezett origin + +curl -H "Origin: http://malicious-site.com" http://localhost:3000/api/users +# Hiba - tiltott origin +``` + +**Aktiválás a server.js-ben:** +```javascript +// src/api/server.js +const corsMiddleware = require(''./middlewares/corsMiddleware''); + +app.use(corsMiddleware); // <-- Uncomment this line! +``` + +--- + +### FELADAT 3: Email Service (src/core/EmailService.js) + +**Cél:** Nodemailer + Handlebars template alapú email küldés implementálása + +Az EmailService welcome emailt küld regisztráció után. + +```javascript +const nodemailer = require(''nodemailer''); +const handlebars = require(''handlebars''); +const fs = require(''fs''); +const path = require(''path''); + +class EmailService { + constructor() { + // TODO 1: Hozz létre Nodemailer transportot Ethereal tesztelő SMTP-vel + // this.transporter = nodemailer.createTransport({ + // host: ''smtp.ethereal.email'', + // port: 587, + // secure: false, // TLS + // auth: { + // user: process.env.ETHEREAL_USER || ''your-test-email@ethereal.email'', + // pass: process.env.ETHEREAL_PASS || ''your-test-password'' + // } + // }); + + console.log('' EmailService initialized''); + } + + async sendWelcomeEmail(userEmail, userName) { + try { + // TODO 2: Olvasd be a welcome.hbs template fájlt + // const templatePath = path.join(__dirname, ''templates'', ''welcome.hbs''); + // const templateSource = fs.readFileSync(templatePath, ''utf-8''); + + // TODO 3: Compile-old a template-et Handlebars-szal + // const template = handlebars.compile(templateSource); + + // TODO 4: Generáld a HTML-t a felhasználói adatokkal + // const html = template({ + // name: userName, + // date: new Date().toLocaleDateString(''hu-HU'') + // }); + + // TODO 5: Küld el az emailt + // const info = await this.transporter.sendMail({ + // from: ''"Clean Architecture App" '', + // to: userEmail, + // subject: ''Üdvözlünk az alkalmazásban!'', + // html: html + // }); + + // TODO 6: Logold ki az Ethereal preview URL-t + // const previewUrl = nodemailer.getTestMessageUrl(info); + // console.log('' Email elküldve:'', previewUrl); + + } catch (error) { + console.error('' Email küldési hiba:'', error.message); + } + } +} + +module.exports = EmailService; +``` + +**Email Template (src/core/templates/welcome.hbs):** +```html + + + + + + + +
+

Üdvözlünk, {{name}}!

+

Sikeres regisztráció az alkalmazásunkban!

+

Regisztráció dátuma: {{date}}

+

Mostantól hozzáférsz az összes funkciónkhoz.

+ +
+ + +``` + +**.env konfigurálás Ethereal SMTP-vel:** +1. Menj a https://ethereal.email oldalra +2. Kattints a "Create Ethereal Account" gombra +3. Másold ki a kapott user és password értékeket +4. Állítsd be a `.env` fájlban: +```env +ETHEREAL_USER=your-generated-user@ethereal.email +ETHEREAL_PASS=your-generated-password +``` + +**Tesztelés:** +Regisztrálj egy új felhasználót: +```bash +POST http://localhost:3000/api/auth/register +{ + "name": "Test User", + "email": "test@example.com", + "password": "password123" +} +``` + +Nézd meg a console-ban az Ethereal preview URL-t, ami így níz ki: +``` + Email elküldve: https://ethereal.email/message/XXXXXX +``` + +Nyísd meg a böngészőben és látni fogod az elküldött emailt! + +--- + +## Projekt Struktúra + +``` +src/ + api/ + controllers/ + AuthController.js Kész (regisztráció, login) + UserController.js Kész (védett user endpointok) + middlewares/ + authMiddleware.js Kész (JWT token validálás) + corsMiddleware.js TODO - FELADAT 2 + scopeMiddleware.js Opcionális + routers/ + authRoutes.js Kész (publikus auth endpointok) + userRoutes.js Kész (JWT védett user endpointok) + server.js Kész (Express + DI setup) + application/ + auth/ + AuthService.js Kész (bcrypt + JWT) + user/ + UserService.js Kész (Prisma CRUD) + core/ + Container.js TODO - FELADAT 1 + EmailService.js TODO - FELADAT 3 + JwtUtil.js Kész (JWT utility) + templates/ + welcome.hbs Welcome email template + domain/ + (Prisma modellekkel helyettesítve) + infrastructure/ + (Prisma ORM kezeli) + prisma/ + schema.prisma Kész (User model) +``` + +--- + +## Tanulási Célok + +### CORS (Cross-Origin Resource Sharing) +- Origin alapú hozzáférés-vezérlés implementálása +- Whitelist konfiguráció production környezetben +- Preflight request-ek kezelése (OPTIONS) +- Credential (cookie, auth header) kezelés + +### DI Container (Dependency Injection) +- Lifecycle management (Singleton, Transient, Scoped) +- Loose coupling az alkalmazásban +- Tesztelhetőség javítása (dependency injection) +- Service registry pattern + +### Email Notification +- SMTP konfiguráció (Ethereal Email) +- Template-alapú email generálás (Handlebars) +- Async email küldés (nem blokkolja a request-et) +- Test email preview Ethereal-lel + +--- + +## Teljes Tesztelési Flow + +### 1. Szerver indítása +```bash +npm run dev +``` + +### 2. Regisztráció +```bash +POST http://localhost:3000/api/auth/register +{ + "name": "Test User", + "email": "test@example.com", + "password": "password123" +} +``` + +**Kimenet:** +- JWT token válaszban +- bcrypt-tel hashelt jelszó az adatbázisban +- Email elküldve (ha implementáltad a FELADAT 3-at) + +### 3. Bejelentkezés +```bash +POST http://localhost:3000/api/auth/login +{ + "email": "test@example.com", + "password": "password123" +} +``` + +**Kimenet:** +- JWT token válaszban +- bcrypt.compare jelszó ellenőrzés + +### 4. Védett endpoint hívása +```bash +GET http://localhost:3000/api/users/me +Authorization: Bearer +``` + +**Kimenet:** +- authMiddleware ellenőrzi a JWT token-t +- req.user.userId beállítva +- UserService lekéri az aktuális user-t Prisma-val + +--- + +## Hibaelhárítás + +### Docker PostgreSQL nem indul +```bash +docker-compose down +docker-compose up -d +docker-compose logs postgres +``` + +### Prisma migration hiba +```bash +npm run prisma:migrate:reset +npm run prisma:migrate +npm run prisma:generate +``` + +### JWT token invalid hiba +- Ellenőrizd, hogy a `.env` fájlban van-e `JWT_SECRET` +- Ellenőrizd, hogy a token `Authorization: Bearer ` formátumban van-e + +### Email nem megy ki +- Ellenőrizd az Ethereal SMTP credentials-t (`.env`) +- Nézd meg a console-t az Ethereal preview URL-ért +- Ellenőrizd, hogy implementáltad-e az EmailService-t + +--- + +## Értékelési Szempontok + +- DI Container működik (register, resolve, createScope) +- CORS whitelist működik (engedélyezett és tiltott origin-ek) +- Email külső funkcionális (Handlebars template + Ethereal SMTP) +- Kód tisztaság és SOLID elvek követése +- Hibakezelés (try-catch, validation) +- Environment változók használata (.env) + +Sok sikert! \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/SEGÍTSÉG.md b/Backend/negyedik gyakorlat/SEGÍTSÉG.md new file mode 100644 index 0000000..0de8263 --- /dev/null +++ b/Backend/negyedik gyakorlat/SEGÍTSÉG.md @@ -0,0 +1,829 @@ +# 📚 SEGÍTSÉG - Példakódok és Magyarázatok + +## Tartalomjegyzék + +1. [DI Container Implementálása](#1-di-container-implementálása) +2. [CORS Middleware Implementálása](#2-cors-middleware-implementálása) +3. [Email Service Implementálása](#3-email-service-implementálása) +4. [Cookie-Based JWT Használata](#4-cookie-based-jwt-használata) +5. [Tesztelési Példák](#5-tesztelési-példák) +6. [Gyakori Hibák és Megoldások](#6-gyakori-hibák-és-megoldások) + +--- + +## 1. DI Container Implementálása + +### 📁 Fájl: `src/application/Container.js` + +### Teljes megoldás magyarázattal: + +```javascript +/** + * Dependency Injection Container + * Supports singleton, transient, and scoped lifetimes + */ +class Container { + constructor() { + // 1. Singleton instance-ok tárolása (egyszer létrehozott objektumok) + this.services = new Map(); + + // 2. Factory függvények tárolása (objektumokat létrehozó függvények) + this.factories = new Map(); + + // 3. Lifecycle típusok tárolása (singleton/transient/scoped) + this.lifetimes = new Map(); + } + + /** + * Service regisztrálása + * @param {string} name - Service neve + * @param {Function} factory - Factory függvény ami a service-t létrehozza + * @param {string} lifetime - 'singleton' | 'transient' | 'scoped' + */ + register(name, factory, lifetime = 'singleton') { + // 4. Factory függvény eltárolása + this.factories.set(name, factory); + + // 5. Lifetime típus eltárolása + this.lifetimes.set(name, lifetime); + + // 6. Ha singleton, azonnal példányosítjuk és eltároljuk + if (lifetime === 'singleton') { + const instance = factory(); + this.services.set(name, instance); + } + } + + /** + * Service lekérése + * @param {string} name - Service neve + * @param {Map} scope - Opcionális scope Map scoped lifetime-hoz + * @returns {any} Service instance + */ + resolve(name, scope = null) { + // 7. Scoped lifecycle kezelése + if (this.lifetimes.get(name) === 'scoped' && scope) { + // Ha már van a scope-ban, azt adjuk vissza + if (scope.has(name)) { + return scope.get(name); + } + + // Ha nincs még, létrehozzuk és eltároljuk a scope-ban + const instance = this.factories.get(name)(); + scope.set(name, instance); + return instance; + } + + // 8. Singleton lifecycle - mindig ugyanazt az instance-t adjuk vissza + if (this.lifetimes.get(name) === 'singleton') { + return this.services.get(name); + } + + // 9. Transient lifecycle - mindig új instance-t hozunk létre + if (this.lifetimes.get(name) === 'transient') { + return this.factories.get(name)(); + } + + // 10. Ha nincs regisztrálva a service, hibát dobunk + throw new Error(`Service '${name}' is not registered`); + } + + /** + * Új scope létrehozása scoped lifecycle-hoz (pl. request-enkénti instance-ok) + * @returns {Object} Scope objektum resolve metódussal + */ + createScope() { + // 11. Új Map létrehozása a scoped instance-oknak + const scopeMap = new Map(); + + // 12. Visszaadunk egy objektumot ami tartalmaz egy resolve metódust + return { + resolve: (name) => this.resolve(name, scopeMap) + }; + } +} + +module.exports = Container; +``` + +### 💡 Használati példák: + +#### Singleton (egy instance az egész alkalmazásban) + +```javascript +const container = new Container(); + +// Singleton PrismaClient (egy kapcsolat az egész app-ban) +container.register('PrismaClient', () => { + return new PrismaClient(); +}, 'singleton'); + +// Minden resolve ugyanazt az instance-t adja vissza +const prisma1 = container.resolve('PrismaClient'); +const prisma2 = container.resolve('PrismaClient'); +console.log(prisma1 === prisma2); // true - ugyanaz az objektum! +``` + +#### Transient (minden resolve új instance) + +```javascript +// Transient logger (minden híváshoz új) +container.register('Logger', () => { + return { + id: Math.random(), + log: (msg) => console.log(`[${new Date().toISOString()}] ${msg}`) + }; +}, 'transient'); + +const logger1 = container.resolve('Logger'); +const logger2 = container.resolve('Logger'); +console.log(logger1 === logger2); // false - különböző objektumok! +console.log(logger1.id !== logger2.id); // true - különböző ID-k +``` + +#### Scoped (request szinten megosztott) + +```javascript +// Scoped RequestContext +container.register('RequestContext', () => { + return { + id: Math.random(), + user: null, + timestamp: Date.now() + }; +}, 'scoped'); + +// Első request scope +const scope1 = container.createScope(); +const ctx1a = scope1.resolve('RequestContext'); +const ctx1b = scope1.resolve('RequestContext'); +console.log(ctx1a === ctx1b); // true - ugyanaz a scope-on belül! + +// Második request scope +const scope2 = container.createScope(); +const ctx2 = scope2.resolve('RequestContext'); +console.log(ctx1a === ctx2); // false - különböző scope-ok! +``` + +### 🧪 Tesztelés: + +```javascript +// tests/unit/application/Container.test.js +const Container = require('../../src/application/Container'); + +describe('Container', () => { + let container; + + beforeEach(() => { + container = new Container(); + }); + + test('singleton - should return same instance', () => { + container.register('TestService', () => ({ id: Math.random() }), 'singleton'); + + const instance1 = container.resolve('TestService'); + const instance2 = container.resolve('TestService'); + + expect(instance1).toBe(instance2); + expect(instance1.id).toBe(instance2.id); + }); + + test('transient - should return different instances', () => { + container.register('TestService', () => ({ id: Math.random() }), 'transient'); + + const instance1 = container.resolve('TestService'); + const instance2 = container.resolve('TestService'); + + expect(instance1).not.toBe(instance2); + expect(instance1.id).not.toBe(instance2.id); + }); + + test('scoped - should return same instance within scope', () => { + container.register('TestService', () => ({ id: Math.random() }), 'scoped'); + + const scope = container.createScope(); + const instance1 = scope.resolve('TestService'); + const instance2 = scope.resolve('TestService'); + + expect(instance1).toBe(instance2); + expect(instance1.id).toBe(instance2.id); + }); + + test('scoped - different scopes should have different instances', () => { + container.register('TestService', () => ({ id: Math.random() }), 'scoped'); + + const scope1 = container.createScope(); + const scope2 = container.createScope(); + + const instance1 = scope1.resolve('TestService'); + const instance2 = scope2.resolve('TestService'); + + expect(instance1).not.toBe(instance2); + }); + + test('should throw error for unregistered service', () => { + expect(() => container.resolve('NonExistent')).toThrow( + "Service 'NonExistent' is not registered" + ); + }); +}); +``` + +--- + +## 2. CORS Middleware Implementálása + +### 📁 Fájl: `src/api/middlewares/corsMiddleware.js` + +### Teljes megoldás: + +```javascript +const cors = require('cors'); + +// Engedélyezett origin-ek whitelist-je (környezeti változóból) +const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [ + 'http://localhost:3000', + 'http://localhost:5173', // Vite default port + 'http://localhost:5174', + 'http://localhost:4200' // Angular default port +]; + +/** + * CORS Configuration + * Whitelist-based origin validation + */ +const corsOptions = { + /** + * Origin ellenőrzés + * @param {string} origin - Request origin + * @param {Function} callback - Callback(error, allowed) + */ + origin: function (origin, callback) { + // 1. Ha nincs origin (backend-to-backend, Postman, curl) + // Ezeket általában engedélyezzük development-ben + if (!origin) { + return callback(null, true); + } + + // 2. Ha az origin benne van az allowedOrigins listában + if (allowedOrigins.includes(origin)) { + return callback(null, true); + } + + // 3. Egyébként tiltjuk CORS hibával + callback(new Error('Not allowed by CORS')); + }, + + // Cookie és Authorization header engedélyezése + credentials: true, + + // Engedélyezett HTTP metódusok + methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], + + // Engedélyezett header-ek + allowedHeaders: ['Content-Type', 'Authorization'], + + // Response header-ek amiket a frontend láthat + exposedHeaders: ['Set-Cookie'], + + // Preflight cache idő (másodpercben) + maxAge: 600 // 10 perc +}; + +module.exports = cors(corsOptions); +``` + +### 💡 Használat: + +#### `.env` konfiguráció: + +```env +# Development +ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173 + +# Production +ALLOWED_ORIGINS=https://myapp.com,https://www.myapp.com,https://admin.myapp.com +``` + +#### Aktiválás `server.js`-ben: + +```javascript +// src/api/server.js +const corsMiddleware = require('./middlewares/corsMiddleware'); + +// Middleware chain +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +app.use(cookieParser()); +app.use(corsMiddleware); // <-- CORS middleware hozzáadása +``` + +### 🧪 Tesztelés cURL-lel: + +```bash +# Engedélyezett origin +curl -H "Origin: http://localhost:3000" \ + -H "Access-Control-Request-Method: POST" \ + -H "Access-Control-Request-Headers: Content-Type" \ + -X OPTIONS \ + http://localhost:3000/api/users + +# Sikeres válasz: +# Access-Control-Allow-Origin: http://localhost:3000 +# Access-Control-Allow-Credentials: true + +# Tiltott origin +curl -H "Origin: http://malicious-site.com" \ + -X GET \ + http://localhost:3000/api/users + +# Hiba válasz: "Not allowed by CORS" +``` + +### 🧪 Frontend tesztelés: + +```javascript +// React/Vue/Angular frontend +fetch('http://localhost:3000/api/auth/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'include', // Cookie küldés/fogadás engedélyezése + body: JSON.stringify({ email, password }) +}) +.then(res => res.json()) +.then(data => console.log('Login sikeres:', data)) +.catch(err => console.error('CORS hiba:', err)); +``` + +--- + +## 3. Email Service Implementálása + +### 📁 Fájl: `src/application/services/EmailService.js` + +### Teljes megoldás: + +```javascript +const nodemailer = require('nodemailer'); +const handlebars = require('handlebars'); +const fs = require('fs'); +const path = require('path'); + +/** + * Email Service + * Nodemailer + Handlebars template based email sending + */ +class EmailService { + constructor() { + // 1. Nodemailer transport létrehozása Ethereal SMTP-vel + this.transporter = nodemailer.createTransport({ + host: 'smtp.ethereal.email', + port: 587, + secure: false, // TLS + auth: { + user: process.env.ETHEREAL_USER || 'your-test-email@ethereal.email', + pass: process.env.ETHEREAL_PASS || 'your-test-password' + } + }); + + console.log('📧 EmailService initialized'); + } + + /** + * Welcome email küldése új regisztrált felhasználónak + * @param {string} userEmail - Felhasználó email címe + * @param {string} userName - Felhasználó neve + * @returns {Promise} + */ + async sendWelcomeEmail(userEmail, userName) { + try { + // 2. Template fájl beolvasása + const templatePath = path.join(__dirname, 'templates', 'welcome.hbs'); + const templateSource = fs.readFileSync(templatePath, 'utf-8'); + + // 3. Handlebars template compile-olása + const template = handlebars.compile(templateSource); + + // 4. HTML generálása az adatokkal + const html = template({ + name: userName, + email: userEmail, + date: new Date().toLocaleDateString('hu-HU'), + year: new Date().getFullYear() + }); + + // 5. Email küldése + const info = await this.transporter.sendMail({ + from: '"Clean Architecture App" ', + to: userEmail, + subject: '🎉 Üdvözlünk az alkalmazásban!', + html: html + }); + + // 6. Ethereal preview URL kiírása + const previewUrl = nodemailer.getTestMessageUrl(info); + console.log('📧 Email elküldve:', previewUrl); + + return true; + } catch (error) { + console.error('❌ Email küldési hiba:', error.message); + return false; + } + } + + /** + * Password reset email (bővítési lehetőség) + */ + async sendPasswordResetEmail(userEmail, resetToken) { + try { + const templatePath = path.join(__dirname, 'templates', 'password-reset.hbs'); + const templateSource = fs.readFileSync(templatePath, 'utf-8'); + const template = handlebars.compile(templateSource); + + const resetLink = `${process.env.FRONTEND_URL}/reset-password?token=${resetToken}`; + + const html = template({ + resetLink, + expiresIn: '1 óra' + }); + + const info = await this.transporter.sendMail({ + from: '"Clean Architecture App" ', + to: userEmail, + subject: '🔐 Jelszó visszaállítás', + html: html + }); + + console.log('📧 Password reset email:', nodemailer.getTestMessageUrl(info)); + return true; + } catch (error) { + console.error('❌ Password reset email hiba:', error.message); + return false; + } + } +} + +module.exports = EmailService; +``` + +### 📁 Email Template: `src/application/services/templates/welcome.hbs` + +```html + + + + + + Üdvözlünk! + + + +
+
🎉
+ +

Üdvözlünk, {{name}}!

+ +

Gratulálunk! Sikeres regisztráció az alkalmazásunkban.

+ +
+ Regisztráció részletei:
+ 📧 Email: {{email}}
+ 📅 Dátum: {{date}} +
+ +

Mostantól hozzáférsz az összes funkciónkhoz:

+
    +
  • ✅ Profil kezelés
  • +
  • ✅ Biztonságos autentikáció
  • +
  • ✅ API hozzáférés
  • +
+ +
+ + Profilom megtekintése + +
+ + +
+ + +``` + +### 💡 Ethereal Email Beállítása: + +1. **Menj a https://ethereal.email oldalra** +2. **Kattints "Create Ethereal Account" gombra** +3. **Másold ki a credentials-t:** + +``` +Username: your-random-name@ethereal.email +Password: your-random-password +``` + +4. **Állítsd be a `.env` fájlban:** + +```env +ETHEREAL_USER=your-random-name@ethereal.email +ETHEREAL_PASS=your-random-password +``` + +### 🧪 Tesztelés: + +```bash +# Regisztráció (automatikusan küld welcome emailt) +POST http://localhost:3000/api/auth/register +Content-Type: application/json + +{ + "name": "Test User", + "email": "test@example.com", + "password": "password123" +} + +# Console output: +# 📧 Email elküldve: https://ethereal.email/message/XXXXXX +``` + +**Nyisd meg a böngészőben a linket és látni fogod az emailt!** + +--- + +## 4. Cookie-Based JWT Használata + +### 🍪 Frontend Integration (React példa) + +```javascript +// authService.js +const API_URL = 'http://localhost:3000/api'; + +export const authService = { + async register(name, email, password) { + const response = await fetch(`${API_URL}/auth/register`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', // FONTOS: Cookie küldés/fogadás + body: JSON.stringify({ name, email, password }) + }); + + if (!response.ok) { + throw new Error('Registration failed'); + } + + return response.json(); + // JWT automatikusan cookie-ban tárolódik! + }, + + async login(email, password) { + const response = await fetch(`${API_URL}/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ email, password }) + }); + + if (!response.ok) { + throw new Error('Login failed'); + } + + return response.json(); + }, + + async logout() { + const response = await fetch(`${API_URL}/auth/logout`, { + method: 'POST', + credentials: 'include' + }); + + return response.json(); + }, + + async getCurrentUser() { + const response = await fetch(`${API_URL}/users/me`, { + credentials: 'include' // Cookie automatikusan küldődik + }); + + if (!response.ok) { + throw new Error('Unauthorized'); + } + + return response.json(); + } +}; +``` + +### 🧪 cURL Tesztelés Cookie-val: + +```bash +# 1. Login + Cookie mentése +curl -c cookies.txt -X POST http://localhost:3000/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{"email":"test@example.com","password":"password123"}' + +# 2. Protected endpoint hívása a cookie-val +curl -b cookies.txt http://localhost:3000/api/users/me + +# 3. Logout +curl -b cookies.txt -c cookies.txt -X POST http://localhost:3000/api/auth/logout +``` + +--- + +## 5. Tesztelési Példák + +### Controller Teszt Cookie-val: + +```javascript +// tests/unit/controllers/AuthController.test.js +const AuthController = require('../../src/api/controllers/AuthController'); + +describe('AuthController - Cookie-based JWT', () => { + let authController; + let mockRegisterHandler; + let mockLoginHandler; + let mockJwtService; + + beforeEach(() => { + mockRegisterHandler = { handle: jest.fn() }; + mockLoginHandler = { handle: jest.fn() }; + mockJwtService = { + getCookieName: jest.fn().mockReturnValue('auth_token'), + getCookieOptions: jest.fn().mockReturnValue({ + httpOnly: true, + secure: false, + sameSite: 'strict' + }) + }; + + authController = new AuthController( + mockRegisterHandler, + mockLoginHandler, + mockJwtService + ); + }); + + test('login should set JWT in cookie', async () => { + const mockReq = { + body: { email: 'test@example.com', password: 'password123' } + }; + const mockRes = { + cookie: jest.fn(), + status: jest.fn().mockReturnThis(), + json: jest.fn() + }; + + mockLoginHandler.handle.mockResolvedValue({ + user: { id: 1, email: 'test@example.com' }, + token: 'mock_jwt_token' + }); + + await authController.login(mockReq, mockRes); + + // Cookie beállítás ellenőrzése + expect(mockRes.cookie).toHaveBeenCalledWith( + 'auth_token', + 'mock_jwt_token', + expect.objectContaining({ + httpOnly: true, + sameSite: 'strict' + }) + ); + + // Response csak user-t tartalmaz, token nincs a body-ban + expect(mockRes.json).toHaveBeenCalledWith({ + message: 'Login successful', + data: { + user: { id: 1, email: 'test@example.com' } + } + }); + }); +}); +``` + +--- + +## 6. Gyakori Hibák és Megoldások + +### ❌ Hiba: "Service 'PrismaClient' is not registered" + +**Megoldás:** +```javascript +// Ellenőrizd a server.js-ben: +container.register('PrismaClient', () => { + return databaseConnection.getClient(); +}, 'singleton'); +``` + +### ❌ Hiba: "Not allowed by CORS" + +**Megoldás:** +```env +# .env fájlban add meg a frontend origin-t: +ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173 +``` + +### ❌ Hiba: "No token provided in cookies" + +**Megoldás:** +```javascript +// Frontend-en használd a credentials: 'include'-ot: +fetch('http://localhost:3000/api/users/me', { + credentials: 'include' +}); +``` + +### ❌ Hiba: Email nem megy ki + +**Megoldás:** +1. Ellenőrizd az Ethereal credentials-t (`.env`) +2. Hozz létre új Ethereal account-ot: https://ethereal.email +3. Ellenőrizd a template fájl elérési útját: + ```javascript + const templatePath = path.join(__dirname, 'templates', 'welcome.hbs'); + ``` + +### ❌ Hiba: Container circular dependency + +**Megoldás:** +```javascript +// Használj lazy loading-ot: +container.register('ServiceA', () => { + const ServiceB = container.resolve('ServiceB'); + return new ServiceA(ServiceB); +}, 'singleton'); +``` + +--- + +## 📚 További Olvasnivalók + +- [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) +- [Dependency Injection Pattern](https://martinfowler.com/articles/injection.html) +- [CORS in Express](https://expressjs.com/en/resources/middleware/cors.html) +- [Nodemailer Documentation](https://nodemailer.com/about/) +- [Handlebars Templates](https://handlebarsjs.com/) +- [Cookie Security Best Practices](https://owasp.org/www-community/controls/SecureCookieAttribute) + +--- + +**Sok sikert a feladatokhoz! 🚀** diff --git a/Backend/negyedik gyakorlat/coverage/clover.xml b/Backend/negyedik gyakorlat/coverage/clover.xml new file mode 100644 index 0000000..c50d4ab --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/clover.xml @@ -0,0 +1,418 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Backend/negyedik gyakorlat/coverage/coverage-final.json b/Backend/negyedik gyakorlat/coverage/coverage-final.json new file mode 100644 index 0000000..9c590f5 --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/coverage-final.json @@ -0,0 +1,27 @@ +{"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\api\\controllers\\AuthController.js": {"path":"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\api\\controllers\\AuthController.js","statementMap":{"0":{"start":{"line":1,"column":28},"end":{"line":1,"column":90}},"1":{"start":{"line":2,"column":25},"end":{"line":2,"column":84}},"2":{"start":{"line":10,"column":4},"end":{"line":10,"column":65}},"3":{"start":{"line":11,"column":4},"end":{"line":11,"column":59}},"4":{"start":{"line":12,"column":4},"end":{"line":12,"column":33}},"5":{"start":{"line":19,"column":4},"end":{"line":46,"column":5}},"6":{"start":{"line":20,"column":40},"end":{"line":20,"column":48}},"7":{"start":{"line":22,"column":22},"end":{"line":22,"column":68}},"8":{"start":{"line":23,"column":21},"end":{"line":23,"column":74}},"9":{"start":{"line":26,"column":6},"end":{"line":30,"column":8}},"10":{"start":{"line":32,"column":6},"end":{"line":37,"column":9}},"11":{"start":{"line":40,"column":21},"end":{"line":43,"column":66}},"12":{"start":{"line":45,"column":6},"end":{"line":45,"column":56}},"13":{"start":{"line":53,"column":4},"end":{"line":78,"column":5}},"14":{"start":{"line":54,"column":34},"end":{"line":54,"column":42}},"15":{"start":{"line":56,"column":22},"end":{"line":56,"column":59}},"16":{"start":{"line":57,"column":21},"end":{"line":57,"column":71}},"17":{"start":{"line":60,"column":6},"end":{"line":64,"column":8}},"18":{"start":{"line":66,"column":6},"end":{"line":71,"column":9}},"19":{"start":{"line":74,"column":21},"end":{"line":75,"column":67}},"20":{"start":{"line":77,"column":6},"end":{"line":77,"column":56}},"21":{"start":{"line":85,"column":4},"end":{"line":99,"column":5}},"22":{"start":{"line":87,"column":6},"end":{"line":92,"column":9}},"23":{"start":{"line":94,"column":6},"end":{"line":96,"column":9}},"24":{"start":{"line":98,"column":6},"end":{"line":98,"column":53}},"25":{"start":{"line":103,"column":0},"end":{"line":103,"column":32}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":9,"column":2},"end":{"line":9,"column":3}},"loc":{"start":{"line":9,"column":79},"end":{"line":13,"column":3}},"line":9},"1":{"name":"(anonymous_1)","decl":{"start":{"line":18,"column":2},"end":{"line":18,"column":3}},"loc":{"start":{"line":18,"column":27},"end":{"line":47,"column":3}},"line":18},"2":{"name":"(anonymous_2)","decl":{"start":{"line":52,"column":2},"end":{"line":52,"column":3}},"loc":{"start":{"line":52,"column":24},"end":{"line":79,"column":3}},"line":52},"3":{"name":"(anonymous_3)","decl":{"start":{"line":84,"column":2},"end":{"line":84,"column":3}},"loc":{"start":{"line":84,"column":25},"end":{"line":100,"column":3}},"line":84}},"branchMap":{"0":{"loc":{"start":{"line":40,"column":21},"end":{"line":43,"column":66}},"type":"cond-expr","locations":[{"start":{"line":43,"column":57},"end":{"line":43,"column":60}},{"start":{"line":43,"column":63},"end":{"line":43,"column":66}}],"line":40},"1":{"loc":{"start":{"line":40,"column":21},"end":{"line":43,"column":54}},"type":"binary-expr","locations":[{"start":{"line":40,"column":21},"end":{"line":40,"column":55}},{"start":{"line":41,"column":21},"end":{"line":41,"column":61}},{"start":{"line":42,"column":21},"end":{"line":42,"column":54}},{"start":{"line":43,"column":21},"end":{"line":43,"column":54}}],"line":40},"2":{"loc":{"start":{"line":74,"column":21},"end":{"line":75,"column":67}},"type":"cond-expr","locations":[{"start":{"line":75,"column":58},"end":{"line":75,"column":61}},{"start":{"line":75,"column":64},"end":{"line":75,"column":67}}],"line":74},"3":{"loc":{"start":{"line":74,"column":21},"end":{"line":75,"column":55}},"type":"binary-expr","locations":[{"start":{"line":74,"column":21},"end":{"line":74,"column":54}},{"start":{"line":75,"column":21},"end":{"line":75,"column":55}}],"line":74}},"s":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0,"20":0,"21":0,"22":0,"23":0,"24":0,"25":0},"f":{"0":0,"1":0,"2":0,"3":0},"b":{"0":[0,0],"1":[0,0,0,0],"2":[0,0],"3":[0,0]}} +,"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\api\\controllers\\UserController.js": {"path":"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\api\\controllers\\UserController.js","statementMap":{"0":{"start":{"line":1,"column":19},"end":{"line":1,"column":71}},"1":{"start":{"line":2,"column":25},"end":{"line":2,"column":83}},"2":{"start":{"line":3,"column":25},"end":{"line":3,"column":83}},"3":{"start":{"line":4,"column":33},"end":{"line":4,"column":100}},"4":{"start":{"line":12,"column":4},"end":{"line":12,"column":47}},"5":{"start":{"line":13,"column":4},"end":{"line":13,"column":59}},"6":{"start":{"line":14,"column":4},"end":{"line":14,"column":59}},"7":{"start":{"line":15,"column":4},"end":{"line":15,"column":75}},"8":{"start":{"line":22,"column":4},"end":{"line":36,"column":5}},"9":{"start":{"line":24,"column":21},"end":{"line":24,"column":36}},"10":{"start":{"line":26,"column":20},"end":{"line":26,"column":42}},"11":{"start":{"line":27,"column":19},"end":{"line":27,"column":61}},"12":{"start":{"line":29,"column":6},"end":{"line":32,"column":9}},"13":{"start":{"line":34,"column":21},"end":{"line":34,"column":68}},"14":{"start":{"line":35,"column":6},"end":{"line":35,"column":56}},"15":{"start":{"line":43,"column":4},"end":{"line":54,"column":5}},"16":{"start":{"line":44,"column":20},"end":{"line":44,"column":42}},"17":{"start":{"line":45,"column":20},"end":{"line":45,"column":68}},"18":{"start":{"line":47,"column":6},"end":{"line":51,"column":9}},"19":{"start":{"line":53,"column":6},"end":{"line":53,"column":53}},"20":{"start":{"line":61,"column":4},"end":{"line":79,"column":5}},"21":{"start":{"line":62,"column":21},"end":{"line":62,"column":31}},"22":{"start":{"line":63,"column":21},"end":{"line":63,"column":33}},"23":{"start":{"line":65,"column":6},"end":{"line":67,"column":7}},"24":{"start":{"line":66,"column":8},"end":{"line":66,"column":66}},"25":{"start":{"line":69,"column":20},"end":{"line":69,"column":48}},"26":{"start":{"line":70,"column":19},"end":{"line":70,"column":67}},"27":{"start":{"line":72,"column":6},"end":{"line":75,"column":9}},"28":{"start":{"line":77,"column":21},"end":{"line":77,"column":68}},"29":{"start":{"line":78,"column":6},"end":{"line":78,"column":56}},"30":{"start":{"line":86,"column":4},"end":{"line":100,"column":5}},"31":{"start":{"line":87,"column":21},"end":{"line":87,"column":36}},"32":{"start":{"line":88,"column":23},"end":{"line":88,"column":31}},"33":{"start":{"line":90,"column":22},"end":{"line":90,"column":64}},"34":{"start":{"line":91,"column":19},"end":{"line":91,"column":77}},"35":{"start":{"line":93,"column":6},"end":{"line":96,"column":9}},"36":{"start":{"line":98,"column":21},"end":{"line":98,"column":67}},"37":{"start":{"line":99,"column":6},"end":{"line":99,"column":56}},"38":{"start":{"line":104,"column":0},"end":{"line":104,"column":32}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":11,"column":2},"end":{"line":11,"column":3}},"loc":{"start":{"line":11,"column":116},"end":{"line":16,"column":3}},"line":11},"1":{"name":"(anonymous_1)","decl":{"start":{"line":21,"column":2},"end":{"line":21,"column":3}},"loc":{"start":{"line":21,"column":24},"end":{"line":37,"column":3}},"line":21},"2":{"name":"(anonymous_2)","decl":{"start":{"line":42,"column":2},"end":{"line":42,"column":3}},"loc":{"start":{"line":42,"column":25},"end":{"line":55,"column":3}},"line":42},"3":{"name":"(anonymous_3)","decl":{"start":{"line":60,"column":2},"end":{"line":60,"column":3}},"loc":{"start":{"line":60,"column":26},"end":{"line":80,"column":3}},"line":60},"4":{"name":"(anonymous_4)","decl":{"start":{"line":85,"column":2},"end":{"line":85,"column":3}},"loc":{"start":{"line":85,"column":27},"end":{"line":101,"column":3}},"line":85}},"branchMap":{"0":{"loc":{"start":{"line":34,"column":21},"end":{"line":34,"column":68}},"type":"cond-expr","locations":[{"start":{"line":34,"column":59},"end":{"line":34,"column":62}},{"start":{"line":34,"column":65},"end":{"line":34,"column":68}}],"line":34},"1":{"loc":{"start":{"line":65,"column":6},"end":{"line":67,"column":7}},"type":"if","locations":[{"start":{"line":65,"column":6},"end":{"line":67,"column":7}},{"start":{},"end":{}}],"line":65},"2":{"loc":{"start":{"line":77,"column":21},"end":{"line":77,"column":68}},"type":"cond-expr","locations":[{"start":{"line":77,"column":59},"end":{"line":77,"column":62}},{"start":{"line":77,"column":65},"end":{"line":77,"column":68}}],"line":77},"3":{"loc":{"start":{"line":98,"column":21},"end":{"line":98,"column":67}},"type":"cond-expr","locations":[{"start":{"line":98,"column":58},"end":{"line":98,"column":61}},{"start":{"line":98,"column":64},"end":{"line":98,"column":67}}],"line":98}},"s":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0,"20":0,"21":0,"22":0,"23":0,"24":0,"25":0,"26":0,"27":0,"28":0,"29":0,"30":0,"31":0,"32":0,"33":0,"34":0,"35":0,"36":0,"37":0,"38":0},"f":{"0":0,"1":0,"2":0,"3":0,"4":0},"b":{"0":[0,0],"1":[0,0],"2":[0,0],"3":[0,0]}} +,"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\api\\middlewares\\authMiddleware.js": {"path":"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\api\\middlewares\\authMiddleware.js","statementMap":{"0":{"start":{"line":1,"column":19},"end":{"line":1,"column":67}},"1":{"start":{"line":3,"column":19},"end":{"line":3,"column":35}},"2":{"start":{"line":14,"column":2},"end":{"line":41,"column":3}},"3":{"start":{"line":16,"column":18},"end":{"line":16,"column":65}},"4":{"start":{"line":18,"column":4},"end":{"line":23,"column":5}},"5":{"start":{"line":19,"column":6},"end":{"line":22,"column":9}},"6":{"start":{"line":26,"column":20},"end":{"line":26,"column":49}},"7":{"start":{"line":29,"column":4},"end":{"line":32,"column":6}},"8":{"start":{"line":35,"column":4},"end":{"line":35,"column":11}},"9":{"start":{"line":37,"column":4},"end":{"line":40,"column":7}},"10":{"start":{"line":44,"column":0},"end":{"line":44,"column":32}}},"fnMap":{"0":{"name":"authMiddleware","decl":{"start":{"line":13,"column":9},"end":{"line":13,"column":23}},"loc":{"start":{"line":13,"column":40},"end":{"line":42,"column":1}},"line":13}},"branchMap":{"0":{"loc":{"start":{"line":18,"column":4},"end":{"line":23,"column":5}},"type":"if","locations":[{"start":{"line":18,"column":4},"end":{"line":23,"column":5}},{"start":{},"end":{}}],"line":18}},"s":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0},"f":{"0":0},"b":{"0":[0,0]}} +,"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\api\\middlewares\\corsMiddleware.js": {"path":"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\api\\middlewares\\corsMiddleware.js","statementMap":{"0":{"start":{"line":1,"column":13},"end":{"line":1,"column":28}},"1":{"start":{"line":4,"column":23},"end":{"line":4,"column":91}},"2":{"start":{"line":6,"column":20},"end":{"line":20,"column":1}},"3":{"start":{"line":22,"column":0},"end":{"line":22,"column":35}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":7,"column":10},"end":{"line":7,"column":11}},"loc":{"start":{"line":7,"column":38},"end":{"line":16,"column":3}},"line":7}},"branchMap":{"0":{"loc":{"start":{"line":4,"column":23},"end":{"line":4,"column":91}},"type":"binary-expr","locations":[{"start":{"line":4,"column":23},"end":{"line":4,"column":62}},{"start":{"line":4,"column":66},"end":{"line":4,"column":91}}],"line":4}},"s":{"0":0,"1":0,"2":0,"3":0},"f":{"0":0},"b":{"0":[0,0]}} +,"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\api\\middlewares\\scopeMiddleware.js": {"path":"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\api\\middlewares\\scopeMiddleware.js","statementMap":{"0":{"start":{"line":6,"column":2},"end":{"line":15,"column":4}},"1":{"start":{"line":18,"column":0},"end":{"line":18,"column":33}}},"fnMap":{"0":{"name":"scopeMiddleware","decl":{"start":{"line":5,"column":9},"end":{"line":5,"column":24}},"loc":{"start":{"line":5,"column":36},"end":{"line":16,"column":1}},"line":5},"1":{"name":"(anonymous_1)","decl":{"start":{"line":6,"column":9},"end":{"line":6,"column":10}},"loc":{"start":{"line":6,"column":29},"end":{"line":15,"column":3}},"line":6}},"branchMap":{},"s":{"0":0,"1":0},"f":{"0":0,"1":0},"b":{}} +,"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\api\\routers\\authRoutes.js": {"path":"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\api\\routers\\authRoutes.js","statementMap":{"0":{"start":{"line":1,"column":16},"end":{"line":1,"column":34}},"1":{"start":{"line":10,"column":17},"end":{"line":10,"column":33}},"2":{"start":{"line":13,"column":25},"end":{"line":13,"column":60}},"3":{"start":{"line":20,"column":2},"end":{"line":20,"column":76}},"4":{"start":{"line":20,"column":41},"end":{"line":20,"column":74}},"5":{"start":{"line":27,"column":2},"end":{"line":27,"column":70}},"6":{"start":{"line":27,"column":38},"end":{"line":27,"column":68}},"7":{"start":{"line":33,"column":2},"end":{"line":33,"column":72}},"8":{"start":{"line":33,"column":39},"end":{"line":33,"column":70}},"9":{"start":{"line":38,"column":2},"end":{"line":38,"column":57}},"10":{"start":{"line":38,"column":36},"end":{"line":38,"column":55}},"11":{"start":{"line":40,"column":2},"end":{"line":40,"column":16}},"12":{"start":{"line":43,"column":0},"end":{"line":43,"column":34}}},"fnMap":{"0":{"name":"createAuthRoutes","decl":{"start":{"line":9,"column":9},"end":{"line":9,"column":25}},"loc":{"start":{"line":9,"column":37},"end":{"line":41,"column":1}},"line":9},"1":{"name":"(anonymous_1)","decl":{"start":{"line":20,"column":27},"end":{"line":20,"column":28}},"loc":{"start":{"line":20,"column":41},"end":{"line":20,"column":74}},"line":20},"2":{"name":"(anonymous_2)","decl":{"start":{"line":27,"column":24},"end":{"line":27,"column":25}},"loc":{"start":{"line":27,"column":38},"end":{"line":27,"column":68}},"line":27},"3":{"name":"(anonymous_3)","decl":{"start":{"line":33,"column":25},"end":{"line":33,"column":26}},"loc":{"start":{"line":33,"column":39},"end":{"line":33,"column":70}},"line":33},"4":{"name":"(anonymous_4)","decl":{"start":{"line":38,"column":22},"end":{"line":38,"column":23}},"loc":{"start":{"line":38,"column":36},"end":{"line":38,"column":55}},"line":38}},"branchMap":{},"s":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0},"f":{"0":0,"1":0,"2":0,"3":0,"4":0},"b":{}} +,"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\api\\routers\\userRoutes.js": {"path":"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\api\\routers\\userRoutes.js","statementMap":{"0":{"start":{"line":1,"column":16},"end":{"line":1,"column":34}},"1":{"start":{"line":2,"column":23},"end":{"line":2,"column":63}},"2":{"start":{"line":11,"column":17},"end":{"line":11,"column":33}},"3":{"start":{"line":14,"column":25},"end":{"line":14,"column":60}},"4":{"start":{"line":20,"column":2},"end":{"line":20,"column":82}},"5":{"start":{"line":20,"column":50},"end":{"line":20,"column":80}},"6":{"start":{"line":27,"column":2},"end":{"line":27,"column":85}},"7":{"start":{"line":27,"column":50},"end":{"line":27,"column":83}},"8":{"start":{"line":33,"column":2},"end":{"line":33,"column":81}},"9":{"start":{"line":33,"column":48},"end":{"line":33,"column":79}},"10":{"start":{"line":39,"column":2},"end":{"line":39,"column":85}},"11":{"start":{"line":39,"column":51},"end":{"line":39,"column":83}},"12":{"start":{"line":44,"column":2},"end":{"line":44,"column":57}},"13":{"start":{"line":44,"column":36},"end":{"line":44,"column":55}},"14":{"start":{"line":46,"column":2},"end":{"line":46,"column":16}},"15":{"start":{"line":49,"column":0},"end":{"line":49,"column":34}}},"fnMap":{"0":{"name":"createUserRoutes","decl":{"start":{"line":10,"column":9},"end":{"line":10,"column":25}},"loc":{"start":{"line":10,"column":37},"end":{"line":47,"column":1}},"line":10},"1":{"name":"(anonymous_1)","decl":{"start":{"line":20,"column":36},"end":{"line":20,"column":37}},"loc":{"start":{"line":20,"column":50},"end":{"line":20,"column":80}},"line":20},"2":{"name":"(anonymous_2)","decl":{"start":{"line":27,"column":36},"end":{"line":27,"column":37}},"loc":{"start":{"line":27,"column":50},"end":{"line":27,"column":83}},"line":27},"3":{"name":"(anonymous_3)","decl":{"start":{"line":33,"column":34},"end":{"line":33,"column":35}},"loc":{"start":{"line":33,"column":48},"end":{"line":33,"column":79}},"line":33},"4":{"name":"(anonymous_4)","decl":{"start":{"line":39,"column":37},"end":{"line":39,"column":38}},"loc":{"start":{"line":39,"column":51},"end":{"line":39,"column":83}},"line":39},"5":{"name":"(anonymous_5)","decl":{"start":{"line":44,"column":22},"end":{"line":44,"column":23}},"loc":{"start":{"line":44,"column":36},"end":{"line":44,"column":55}},"line":44}},"branchMap":{},"s":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0},"f":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0},"b":{}} +,"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\application\\auth\\commands\\LoginUserCommand.js": {"path":"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\application\\auth\\commands\\LoginUserCommand.js","statementMap":{"0":{"start":{"line":7,"column":4},"end":{"line":7,"column":23}},"1":{"start":{"line":8,"column":4},"end":{"line":8,"column":29}},"2":{"start":{"line":12,"column":0},"end":{"line":12,"column":34}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":6,"column":2},"end":{"line":6,"column":3}},"loc":{"start":{"line":6,"column":31},"end":{"line":9,"column":3}},"line":6}},"branchMap":{},"s":{"0":0,"1":0,"2":0},"f":{"0":0},"b":{}} +,"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\application\\auth\\commands\\LoginUserCommandHandler.js": {"path":"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\application\\auth\\commands\\LoginUserCommandHandler.js","statementMap":{"0":{"start":{"line":1,"column":15},"end":{"line":1,"column":34}},"1":{"start":{"line":2,"column":19},"end":{"line":2,"column":55}},"2":{"start":{"line":4,"column":19},"end":{"line":4,"column":35}},"3":{"start":{"line":12,"column":4},"end":{"line":12,"column":25}},"4":{"start":{"line":21,"column":32},"end":{"line":21,"column":39}},"5":{"start":{"line":24,"column":4},"end":{"line":26,"column":5}},"6":{"start":{"line":25,"column":6},"end":{"line":25,"column":57}},"7":{"start":{"line":29,"column":17},"end":{"line":31,"column":6}},"8":{"start":{"line":33,"column":4},"end":{"line":35,"column":5}},"9":{"start":{"line":34,"column":6},"end":{"line":34,"column":51}},"10":{"start":{"line":38,"column":28},"end":{"line":38,"column":73}},"11":{"start":{"line":40,"column":4},"end":{"line":42,"column":5}},"12":{"start":{"line":41,"column":6},"end":{"line":41,"column":51}},"13":{"start":{"line":45,"column":18},"end":{"line":48,"column":6}},"14":{"start":{"line":51,"column":52},"end":{"line":51,"column":56}},"15":{"start":{"line":53,"column":4},"end":{"line":56,"column":6}},"16":{"start":{"line":60,"column":0},"end":{"line":60,"column":41}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":11,"column":2},"end":{"line":11,"column":3}},"loc":{"start":{"line":11,"column":22},"end":{"line":13,"column":3}},"line":11},"1":{"name":"(anonymous_1)","decl":{"start":{"line":20,"column":2},"end":{"line":20,"column":3}},"loc":{"start":{"line":20,"column":24},"end":{"line":57,"column":3}},"line":20}},"branchMap":{"0":{"loc":{"start":{"line":24,"column":4},"end":{"line":26,"column":5}},"type":"if","locations":[{"start":{"line":24,"column":4},"end":{"line":26,"column":5}},{"start":{},"end":{}}],"line":24},"1":{"loc":{"start":{"line":24,"column":8},"end":{"line":24,"column":27}},"type":"binary-expr","locations":[{"start":{"line":24,"column":8},"end":{"line":24,"column":14}},{"start":{"line":24,"column":18},"end":{"line":24,"column":27}}],"line":24},"2":{"loc":{"start":{"line":33,"column":4},"end":{"line":35,"column":5}},"type":"if","locations":[{"start":{"line":33,"column":4},"end":{"line":35,"column":5}},{"start":{},"end":{}}],"line":33},"3":{"loc":{"start":{"line":40,"column":4},"end":{"line":42,"column":5}},"type":"if","locations":[{"start":{"line":40,"column":4},"end":{"line":42,"column":5}},{"start":{},"end":{}}],"line":40}},"s":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0},"f":{"0":0,"1":0},"b":{"0":[0,0],"1":[0,0],"2":[0,0],"3":[0,0]}} +,"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\application\\auth\\commands\\RegisterUserCommand.js": {"path":"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\application\\auth\\commands\\RegisterUserCommand.js","statementMap":{"0":{"start":{"line":7,"column":4},"end":{"line":7,"column":21}},"1":{"start":{"line":8,"column":4},"end":{"line":8,"column":23}},"2":{"start":{"line":9,"column":4},"end":{"line":9,"column":29}},"3":{"start":{"line":13,"column":0},"end":{"line":13,"column":37}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":6,"column":2},"end":{"line":6,"column":3}},"loc":{"start":{"line":6,"column":37},"end":{"line":10,"column":3}},"line":6}},"branchMap":{},"s":{"0":0,"1":0,"2":0,"3":0},"f":{"0":0},"b":{}} +,"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\application\\auth\\commands\\RegisterUserCommandHandler.js": {"path":"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\application\\auth\\commands\\RegisterUserCommandHandler.js","statementMap":{"0":{"start":{"line":1,"column":15},"end":{"line":1,"column":34}},"1":{"start":{"line":2,"column":19},"end":{"line":2,"column":55}},"2":{"start":{"line":4,"column":19},"end":{"line":4,"column":35}},"3":{"start":{"line":12,"column":4},"end":{"line":12,"column":25}},"4":{"start":{"line":13,"column":4},"end":{"line":13,"column":37}},"5":{"start":{"line":22,"column":38},"end":{"line":22,"column":45}},"6":{"start":{"line":25,"column":4},"end":{"line":27,"column":5}},"7":{"start":{"line":26,"column":6},"end":{"line":26,"column":63}},"8":{"start":{"line":29,"column":4},"end":{"line":31,"column":5}},"9":{"start":{"line":30,"column":6},"end":{"line":30,"column":69}},"10":{"start":{"line":34,"column":23},"end":{"line":34,"column":51}},"11":{"start":{"line":35,"column":4},"end":{"line":37,"column":5}},"12":{"start":{"line":36,"column":6},"end":{"line":36,"column":46}},"13":{"start":{"line":40,"column":25},"end":{"line":42,"column":6}},"14":{"start":{"line":44,"column":4},"end":{"line":46,"column":5}},"15":{"start":{"line":45,"column":6},"end":{"line":45,"column":61}},"16":{"start":{"line":49,"column":27},"end":{"line":49,"column":58}},"17":{"start":{"line":52,"column":17},"end":{"line":58,"column":6}},"18":{"start":{"line":61,"column":4},"end":{"line":65,"column":5}},"19":{"start":{"line":62,"column":6},"end":{"line":64,"column":9}},"20":{"start":{"line":63,"column":8},"end":{"line":63,"column":70}},"21":{"start":{"line":68,"column":18},"end":{"line":71,"column":6}},"22":{"start":{"line":74,"column":52},"end":{"line":74,"column":56}},"23":{"start":{"line":76,"column":4},"end":{"line":79,"column":6}},"24":{"start":{"line":83,"column":0},"end":{"line":83,"column":44}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":11,"column":2},"end":{"line":11,"column":3}},"loc":{"start":{"line":11,"column":36},"end":{"line":14,"column":3}},"line":11},"1":{"name":"(anonymous_1)","decl":{"start":{"line":21,"column":2},"end":{"line":21,"column":3}},"loc":{"start":{"line":21,"column":24},"end":{"line":80,"column":3}},"line":21},"2":{"name":"(anonymous_2)","decl":{"start":{"line":62,"column":60},"end":{"line":62,"column":61}},"loc":{"start":{"line":62,"column":67},"end":{"line":64,"column":7}},"line":62}},"branchMap":{"0":{"loc":{"start":{"line":25,"column":4},"end":{"line":27,"column":5}},"type":"if","locations":[{"start":{"line":25,"column":4},"end":{"line":27,"column":5}},{"start":{},"end":{}}],"line":25},"1":{"loc":{"start":{"line":25,"column":8},"end":{"line":25,"column":36}},"type":"binary-expr","locations":[{"start":{"line":25,"column":8},"end":{"line":25,"column":13}},{"start":{"line":25,"column":17},"end":{"line":25,"column":23}},{"start":{"line":25,"column":27},"end":{"line":25,"column":36}}],"line":25},"2":{"loc":{"start":{"line":29,"column":4},"end":{"line":31,"column":5}},"type":"if","locations":[{"start":{"line":29,"column":4},"end":{"line":31,"column":5}},{"start":{},"end":{}}],"line":29},"3":{"loc":{"start":{"line":35,"column":4},"end":{"line":37,"column":5}},"type":"if","locations":[{"start":{"line":35,"column":4},"end":{"line":37,"column":5}},{"start":{},"end":{}}],"line":35},"4":{"loc":{"start":{"line":44,"column":4},"end":{"line":46,"column":5}},"type":"if","locations":[{"start":{"line":44,"column":4},"end":{"line":46,"column":5}},{"start":{},"end":{}}],"line":44},"5":{"loc":{"start":{"line":61,"column":4},"end":{"line":65,"column":5}},"type":"if","locations":[{"start":{"line":61,"column":4},"end":{"line":65,"column":5}},{"start":{},"end":{}}],"line":61}},"s":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0,"20":0,"21":0,"22":0,"23":0,"24":0},"f":{"0":0,"1":0,"2":0},"b":{"0":[0,0],"1":[0,0,0],"2":[0,0],"3":[0,0],"4":[0,0],"5":[0,0]}} +,"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\application\\services\\Container.js": {"path":"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\application\\services\\Container.js","statementMap":{"0":{"start":{"line":57,"column":0},"end":{"line":57,"column":27}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":6,"column":2},"end":{"line":6,"column":3}},"loc":{"start":{"line":6,"column":16},"end":{"line":15,"column":3}},"line":6},"1":{"name":"(anonymous_1)","decl":{"start":{"line":17,"column":2},"end":{"line":17,"column":3}},"loc":{"start":{"line":17,"column":50},"end":{"line":23,"column":3}},"line":17},"2":{"name":"(anonymous_2)","decl":{"start":{"line":25,"column":2},"end":{"line":25,"column":3}},"loc":{"start":{"line":25,"column":30},"end":{"line":45,"column":3}},"line":25},"3":{"name":"(anonymous_3)","decl":{"start":{"line":47,"column":2},"end":{"line":47,"column":3}},"loc":{"start":{"line":47,"column":16},"end":{"line":54,"column":3}},"line":47}},"branchMap":{"0":{"loc":{"start":{"line":17,"column":26},"end":{"line":17,"column":48}},"type":"default-arg","locations":[{"start":{"line":17,"column":37},"end":{"line":17,"column":48}}],"line":17},"1":{"loc":{"start":{"line":25,"column":16},"end":{"line":25,"column":28}},"type":"default-arg","locations":[{"start":{"line":25,"column":24},"end":{"line":25,"column":28}}],"line":25}},"s":{"0":1},"f":{"0":22,"1":22,"2":19,"3":9},"b":{"0":[1],"1":[17]},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"6d069147677c6d63ea84f3a595b401d772672ea8"} +,"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\application\\services\\EmailService.js": {"path":"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\application\\services\\EmailService.js","statementMap":{"0":{"start":{"line":20,"column":4},"end":{"line":20,"column":47}},"1":{"start":{"line":26,"column":4},"end":{"line":26,"column":77}},"2":{"start":{"line":27,"column":4},"end":{"line":27,"column":16}},"3":{"start":{"line":31,"column":0},"end":{"line":31,"column":30}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":8,"column":2},"end":{"line":8,"column":3}},"loc":{"start":{"line":8,"column":16},"end":{"line":21,"column":3}},"line":8},"1":{"name":"(anonymous_1)","decl":{"start":{"line":23,"column":2},"end":{"line":23,"column":3}},"loc":{"start":{"line":23,"column":46},"end":{"line":28,"column":3}},"line":23}},"branchMap":{},"s":{"0":0,"1":0,"2":0,"3":0},"f":{"0":0,"1":0},"b":{}} +,"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\application\\services\\JwtService.js": {"path":"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\application\\services\\JwtService.js","statementMap":{"0":{"start":{"line":1,"column":12},"end":{"line":1,"column":35}},"1":{"start":{"line":9,"column":4},"end":{"line":9,"column":71}},"2":{"start":{"line":10,"column":4},"end":{"line":10,"column":56}},"3":{"start":{"line":11,"column":4},"end":{"line":11,"column":35}},"4":{"start":{"line":20,"column":4},"end":{"line":20,"column":73}},"5":{"start":{"line":29,"column":4},"end":{"line":33,"column":5}},"6":{"start":{"line":30,"column":6},"end":{"line":30,"column":44}},"7":{"start":{"line":32,"column":6},"end":{"line":32,"column":50}},"8":{"start":{"line":42,"column":4},"end":{"line":44,"column":5}},"9":{"start":{"line":43,"column":6},"end":{"line":43,"column":18}},"10":{"start":{"line":46,"column":18},"end":{"line":46,"column":39}},"11":{"start":{"line":48,"column":4},"end":{"line":50,"column":5}},"12":{"start":{"line":49,"column":6},"end":{"line":49,"column":18}},"13":{"start":{"line":52,"column":4},"end":{"line":52,"column":20}},"14":{"start":{"line":61,"column":4},"end":{"line":63,"column":5}},"15":{"start":{"line":62,"column":6},"end":{"line":62,"column":18}},"16":{"start":{"line":65,"column":4},"end":{"line":65,"column":36}},"17":{"start":{"line":73,"column":25},"end":{"line":73,"column":62}},"18":{"start":{"line":75,"column":4},"end":{"line":81,"column":6}},"19":{"start":{"line":91,"column":22},"end":{"line":91,"column":36}},"20":{"start":{"line":93,"column":4},"end":{"line":99,"column":5}},"21":{"start":{"line":94,"column":6},"end":{"line":94,"column":50}},"22":{"start":{"line":95,"column":11},"end":{"line":99,"column":5}},"23":{"start":{"line":96,"column":6},"end":{"line":96,"column":55}},"24":{"start":{"line":97,"column":11},"end":{"line":99,"column":5}},"25":{"start":{"line":98,"column":6},"end":{"line":98,"column":45}},"26":{"start":{"line":102,"column":4},"end":{"line":102,"column":26}},"27":{"start":{"line":110,"column":4},"end":{"line":110,"column":27}},"28":{"start":{"line":114,"column":0},"end":{"line":114,"column":28}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":8,"column":2},"end":{"line":8,"column":3}},"loc":{"start":{"line":8,"column":16},"end":{"line":12,"column":3}},"line":8},"1":{"name":"(anonymous_1)","decl":{"start":{"line":19,"column":2},"end":{"line":19,"column":3}},"loc":{"start":{"line":19,"column":25},"end":{"line":21,"column":3}},"line":19},"2":{"name":"(anonymous_2)","decl":{"start":{"line":28,"column":2},"end":{"line":28,"column":3}},"loc":{"start":{"line":28,"column":21},"end":{"line":34,"column":3}},"line":28},"3":{"name":"(anonymous_3)","decl":{"start":{"line":41,"column":2},"end":{"line":41,"column":3}},"loc":{"start":{"line":41,"column":37},"end":{"line":53,"column":3}},"line":41},"4":{"name":"(anonymous_4)","decl":{"start":{"line":60,"column":2},"end":{"line":60,"column":3}},"loc":{"start":{"line":60,"column":35},"end":{"line":66,"column":3}},"line":60},"5":{"name":"(anonymous_5)","decl":{"start":{"line":72,"column":2},"end":{"line":72,"column":3}},"loc":{"start":{"line":72,"column":21},"end":{"line":82,"column":3}},"line":72},"6":{"name":"(anonymous_6)","decl":{"start":{"line":89,"column":2},"end":{"line":89,"column":3}},"loc":{"start":{"line":89,"column":15},"end":{"line":103,"column":3}},"line":89},"7":{"name":"(anonymous_7)","decl":{"start":{"line":109,"column":2},"end":{"line":109,"column":3}},"loc":{"start":{"line":109,"column":18},"end":{"line":111,"column":3}},"line":109}},"branchMap":{"0":{"loc":{"start":{"line":9,"column":18},"end":{"line":9,"column":70}},"type":"binary-expr","locations":[{"start":{"line":9,"column":18},"end":{"line":9,"column":40}},{"start":{"line":9,"column":44},"end":{"line":9,"column":70}}],"line":9},"1":{"loc":{"start":{"line":10,"column":21},"end":{"line":10,"column":55}},"type":"binary-expr","locations":[{"start":{"line":10,"column":21},"end":{"line":10,"column":47}},{"start":{"line":10,"column":51},"end":{"line":10,"column":55}}],"line":10},"2":{"loc":{"start":{"line":42,"column":4},"end":{"line":44,"column":5}},"type":"if","locations":[{"start":{"line":42,"column":4},"end":{"line":44,"column":5}},{"start":{},"end":{}}],"line":42},"3":{"loc":{"start":{"line":48,"column":4},"end":{"line":50,"column":5}},"type":"if","locations":[{"start":{"line":48,"column":4},"end":{"line":50,"column":5}},{"start":{},"end":{}}],"line":48},"4":{"loc":{"start":{"line":48,"column":8},"end":{"line":48,"column":51}},"type":"binary-expr","locations":[{"start":{"line":48,"column":8},"end":{"line":48,"column":26}},{"start":{"line":48,"column":30},"end":{"line":48,"column":51}}],"line":48},"5":{"loc":{"start":{"line":61,"column":4},"end":{"line":63,"column":5}},"type":"if","locations":[{"start":{"line":61,"column":4},"end":{"line":63,"column":5}},{"start":{},"end":{}}],"line":61},"6":{"loc":{"start":{"line":61,"column":8},"end":{"line":61,"column":45}},"type":"binary-expr","locations":[{"start":{"line":61,"column":8},"end":{"line":61,"column":16}},{"start":{"line":61,"column":20},"end":{"line":61,"column":45}}],"line":61},"7":{"loc":{"start":{"line":93,"column":4},"end":{"line":99,"column":5}},"type":"if","locations":[{"start":{"line":93,"column":4},"end":{"line":99,"column":5}},{"start":{"line":95,"column":11},"end":{"line":99,"column":5}}],"line":93},"8":{"loc":{"start":{"line":95,"column":11},"end":{"line":99,"column":5}},"type":"if","locations":[{"start":{"line":95,"column":11},"end":{"line":99,"column":5}},{"start":{"line":97,"column":11},"end":{"line":99,"column":5}}],"line":95},"9":{"loc":{"start":{"line":97,"column":11},"end":{"line":99,"column":5}},"type":"if","locations":[{"start":{"line":97,"column":11},"end":{"line":99,"column":5}},{"start":{},"end":{}}],"line":97}},"s":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0,"20":0,"21":0,"22":0,"23":0,"24":0,"25":0,"26":0,"27":0,"28":0},"f":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0},"b":{"0":[0,0],"1":[0,0],"2":[0,0],"3":[0,0],"4":[0,0],"5":[0,0],"6":[0,0],"7":[0,0],"8":[0,0],"9":[0,0]}} +,"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\application\\user\\commands\\UpdateUserProfileCommand.js": {"path":"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\application\\user\\commands\\UpdateUserProfileCommand.js","statementMap":{"0":{"start":{"line":7,"column":4},"end":{"line":7,"column":25}},"1":{"start":{"line":8,"column":4},"end":{"line":8,"column":21}},"2":{"start":{"line":12,"column":0},"end":{"line":12,"column":42}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":6,"column":2},"end":{"line":6,"column":3}},"loc":{"start":{"line":6,"column":28},"end":{"line":9,"column":3}},"line":6}},"branchMap":{},"s":{"0":0,"1":0,"2":0},"f":{"0":0},"b":{}} +,"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\application\\user\\commands\\UpdateUserProfileCommandHandler.js": {"path":"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\application\\user\\commands\\UpdateUserProfileCommandHandler.js","statementMap":{"0":{"start":{"line":7,"column":4},"end":{"line":7,"column":25}},"1":{"start":{"line":16,"column":29},"end":{"line":16,"column":36}},"2":{"start":{"line":18,"column":4},"end":{"line":20,"column":5}},"3":{"start":{"line":19,"column":6},"end":{"line":19,"column":42}},"4":{"start":{"line":22,"column":17},"end":{"line":25,"column":6}},"5":{"start":{"line":27,"column":49},"end":{"line":27,"column":53}},"6":{"start":{"line":28,"column":4},"end":{"line":28,"column":31}},"7":{"start":{"line":32,"column":0},"end":{"line":32,"column":49}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":6,"column":2},"end":{"line":6,"column":3}},"loc":{"start":{"line":6,"column":22},"end":{"line":8,"column":3}},"line":6},"1":{"name":"(anonymous_1)","decl":{"start":{"line":15,"column":2},"end":{"line":15,"column":3}},"loc":{"start":{"line":15,"column":24},"end":{"line":29,"column":3}},"line":15}},"branchMap":{"0":{"loc":{"start":{"line":18,"column":4},"end":{"line":20,"column":5}},"type":"if","locations":[{"start":{"line":18,"column":4},"end":{"line":20,"column":5}},{"start":{},"end":{}}],"line":18}},"s":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0},"f":{"0":0,"1":0},"b":{"0":[0,0]}} +,"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\application\\user\\queries\\GetAllUsersQuery.js": {"path":"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\application\\user\\queries\\GetAllUsersQuery.js","statementMap":{"0":{"start":{"line":11,"column":0},"end":{"line":11,"column":34}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":6,"column":2},"end":{"line":6,"column":3}},"loc":{"start":{"line":6,"column":16},"end":{"line":8,"column":3}},"line":6}},"branchMap":{},"s":{"0":0},"f":{"0":0},"b":{}} +,"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\application\\user\\queries\\GetAllUsersQueryHandler.js": {"path":"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\application\\user\\queries\\GetAllUsersQueryHandler.js","statementMap":{"0":{"start":{"line":7,"column":4},"end":{"line":7,"column":25}},"1":{"start":{"line":16,"column":18},"end":{"line":18,"column":6}},"2":{"start":{"line":21,"column":4},"end":{"line":21,"column":54}},"3":{"start":{"line":21,"column":48},"end":{"line":21,"column":52}},"4":{"start":{"line":25,"column":0},"end":{"line":25,"column":41}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":6,"column":2},"end":{"line":6,"column":3}},"loc":{"start":{"line":6,"column":22},"end":{"line":8,"column":3}},"line":6},"1":{"name":"(anonymous_1)","decl":{"start":{"line":15,"column":2},"end":{"line":15,"column":3}},"loc":{"start":{"line":15,"column":22},"end":{"line":22,"column":3}},"line":15},"2":{"name":"(anonymous_2)","decl":{"start":{"line":21,"column":21},"end":{"line":21,"column":22}},"loc":{"start":{"line":21,"column":48},"end":{"line":21,"column":52}},"line":21}},"branchMap":{},"s":{"0":0,"1":0,"2":0,"3":0,"4":0},"f":{"0":0,"1":0,"2":0},"b":{}} +,"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\application\\user\\queries\\GetMeQuery.js": {"path":"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\application\\user\\queries\\GetMeQuery.js","statementMap":{"0":{"start":{"line":7,"column":4},"end":{"line":7,"column":25}},"1":{"start":{"line":11,"column":0},"end":{"line":11,"column":28}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":6,"column":2},"end":{"line":6,"column":3}},"loc":{"start":{"line":6,"column":22},"end":{"line":8,"column":3}},"line":6}},"branchMap":{},"s":{"0":0,"1":0},"f":{"0":0},"b":{}} +,"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\application\\user\\queries\\GetMeQueryHandler.js": {"path":"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\application\\user\\queries\\GetMeQueryHandler.js","statementMap":{"0":{"start":{"line":7,"column":4},"end":{"line":7,"column":25}},"1":{"start":{"line":16,"column":23},"end":{"line":16,"column":28}},"2":{"start":{"line":18,"column":17},"end":{"line":20,"column":6}},"3":{"start":{"line":22,"column":4},"end":{"line":24,"column":5}},"4":{"start":{"line":23,"column":6},"end":{"line":23,"column":40}},"5":{"start":{"line":26,"column":49},"end":{"line":26,"column":53}},"6":{"start":{"line":27,"column":4},"end":{"line":27,"column":31}},"7":{"start":{"line":31,"column":0},"end":{"line":31,"column":35}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":6,"column":2},"end":{"line":6,"column":3}},"loc":{"start":{"line":6,"column":22},"end":{"line":8,"column":3}},"line":6},"1":{"name":"(anonymous_1)","decl":{"start":{"line":15,"column":2},"end":{"line":15,"column":3}},"loc":{"start":{"line":15,"column":22},"end":{"line":28,"column":3}},"line":15}},"branchMap":{"0":{"loc":{"start":{"line":22,"column":4},"end":{"line":24,"column":5}},"type":"if","locations":[{"start":{"line":22,"column":4},"end":{"line":24,"column":5}},{"start":{},"end":{}}],"line":22}},"s":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0},"f":{"0":0,"1":0},"b":{"0":[0,0]}} +,"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\application\\user\\queries\\GetUserByIdQuery.js": {"path":"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\application\\user\\queries\\GetUserByIdQuery.js","statementMap":{"0":{"start":{"line":7,"column":4},"end":{"line":7,"column":25}},"1":{"start":{"line":11,"column":0},"end":{"line":11,"column":34}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":6,"column":2},"end":{"line":6,"column":3}},"loc":{"start":{"line":6,"column":22},"end":{"line":8,"column":3}},"line":6}},"branchMap":{},"s":{"0":0,"1":0},"f":{"0":0},"b":{}} +,"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\application\\user\\queries\\GetUserByIdQueryHandler.js": {"path":"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\application\\user\\queries\\GetUserByIdQueryHandler.js","statementMap":{"0":{"start":{"line":7,"column":4},"end":{"line":7,"column":25}},"1":{"start":{"line":16,"column":23},"end":{"line":16,"column":28}},"2":{"start":{"line":18,"column":4},"end":{"line":20,"column":5}},"3":{"start":{"line":19,"column":6},"end":{"line":19,"column":51}},"4":{"start":{"line":22,"column":17},"end":{"line":24,"column":6}},"5":{"start":{"line":26,"column":4},"end":{"line":28,"column":5}},"6":{"start":{"line":27,"column":6},"end":{"line":27,"column":40}},"7":{"start":{"line":30,"column":49},"end":{"line":30,"column":53}},"8":{"start":{"line":31,"column":4},"end":{"line":31,"column":31}},"9":{"start":{"line":35,"column":0},"end":{"line":35,"column":41}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":6,"column":2},"end":{"line":6,"column":3}},"loc":{"start":{"line":6,"column":22},"end":{"line":8,"column":3}},"line":6},"1":{"name":"(anonymous_1)","decl":{"start":{"line":15,"column":2},"end":{"line":15,"column":3}},"loc":{"start":{"line":15,"column":22},"end":{"line":32,"column":3}},"line":15}},"branchMap":{"0":{"loc":{"start":{"line":18,"column":4},"end":{"line":20,"column":5}},"type":"if","locations":[{"start":{"line":18,"column":4},"end":{"line":20,"column":5}},{"start":{},"end":{}}],"line":18},"1":{"loc":{"start":{"line":18,"column":8},"end":{"line":18,"column":32}},"type":"binary-expr","locations":[{"start":{"line":18,"column":8},"end":{"line":18,"column":15}},{"start":{"line":18,"column":19},"end":{"line":18,"column":32}}],"line":18},"2":{"loc":{"start":{"line":26,"column":4},"end":{"line":28,"column":5}},"type":"if","locations":[{"start":{"line":26,"column":4},"end":{"line":28,"column":5}},{"start":{},"end":{}}],"line":26}},"s":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0},"f":{"0":0,"1":0},"b":{"0":[0,0],"1":[0,0],"2":[0,0]}} +,"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\domain\\irepositories\\IUserRepository.js": {"path":"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\domain\\irepositories\\IUserRepository.js","statementMap":{"0":{"start":{"line":12,"column":4},"end":{"line":12,"column":61}},"1":{"start":{"line":21,"column":4},"end":{"line":21,"column":64}},"2":{"start":{"line":29,"column":4},"end":{"line":29,"column":60}},"3":{"start":{"line":38,"column":4},"end":{"line":38,"column":59}},"4":{"start":{"line":47,"column":4},"end":{"line":47,"column":59}},"5":{"start":{"line":56,"column":4},"end":{"line":56,"column":59}},"6":{"start":{"line":60,"column":0},"end":{"line":60,"column":33}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":11,"column":2},"end":{"line":11,"column":3}},"loc":{"start":{"line":11,"column":21},"end":{"line":13,"column":3}},"line":11},"1":{"name":"(anonymous_1)","decl":{"start":{"line":20,"column":2},"end":{"line":20,"column":3}},"loc":{"start":{"line":20,"column":27},"end":{"line":22,"column":3}},"line":20},"2":{"name":"(anonymous_2)","decl":{"start":{"line":28,"column":2},"end":{"line":28,"column":3}},"loc":{"start":{"line":28,"column":18},"end":{"line":30,"column":3}},"line":28},"3":{"name":"(anonymous_3)","decl":{"start":{"line":37,"column":2},"end":{"line":37,"column":3}},"loc":{"start":{"line":37,"column":21},"end":{"line":39,"column":3}},"line":37},"4":{"name":"(anonymous_4)","decl":{"start":{"line":46,"column":2},"end":{"line":46,"column":3}},"loc":{"start":{"line":46,"column":21},"end":{"line":48,"column":3}},"line":46},"5":{"name":"(anonymous_5)","decl":{"start":{"line":55,"column":2},"end":{"line":55,"column":3}},"loc":{"start":{"line":55,"column":19},"end":{"line":57,"column":3}},"line":55}},"branchMap":{},"s":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0},"f":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0},"b":{}} +,"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\domain\\models\\User.js": {"path":"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\domain\\models\\User.js","statementMap":{"0":{"start":{"line":7,"column":4},"end":{"line":7,"column":17}},"1":{"start":{"line":8,"column":4},"end":{"line":8,"column":21}},"2":{"start":{"line":9,"column":4},"end":{"line":9,"column":23}},"3":{"start":{"line":10,"column":4},"end":{"line":10,"column":29}},"4":{"start":{"line":11,"column":4},"end":{"line":11,"column":31}},"5":{"start":{"line":12,"column":4},"end":{"line":12,"column":31}},"6":{"start":{"line":21,"column":38},"end":{"line":21,"column":42}},"7":{"start":{"line":24,"column":4},"end":{"line":26,"column":5}},"8":{"start":{"line":25,"column":6},"end":{"line":25,"column":47}},"9":{"start":{"line":28,"column":4},"end":{"line":30,"column":5}},"10":{"start":{"line":29,"column":6},"end":{"line":29,"column":49}},"11":{"start":{"line":32,"column":4},"end":{"line":34,"column":5}},"12":{"start":{"line":33,"column":6},"end":{"line":33,"column":69}},"13":{"start":{"line":36,"column":4},"end":{"line":36,"column":94}},"14":{"start":{"line":45,"column":23},"end":{"line":45,"column":51}},"15":{"start":{"line":46,"column":4},"end":{"line":46,"column":34}},"16":{"start":{"line":54,"column":4},"end":{"line":56,"column":5}},"17":{"start":{"line":55,"column":6},"end":{"line":55,"column":47}},"18":{"start":{"line":57,"column":4},"end":{"line":57,"column":31}},"19":{"start":{"line":58,"column":4},"end":{"line":58,"column":32}},"20":{"start":{"line":66,"column":4},"end":{"line":72,"column":6}},"21":{"start":{"line":76,"column":0},"end":{"line":76,"column":22}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":6,"column":2},"end":{"line":6,"column":3}},"loc":{"start":{"line":6,"column":63},"end":{"line":13,"column":3}},"line":6},"1":{"name":"(anonymous_1)","decl":{"start":{"line":20,"column":2},"end":{"line":20,"column":3}},"loc":{"start":{"line":20,"column":22},"end":{"line":37,"column":3}},"line":20},"2":{"name":"(anonymous_2)","decl":{"start":{"line":44,"column":2},"end":{"line":44,"column":3}},"loc":{"start":{"line":44,"column":29},"end":{"line":47,"column":3}},"line":44},"3":{"name":"(anonymous_3)","decl":{"start":{"line":53,"column":2},"end":{"line":53,"column":3}},"loc":{"start":{"line":53,"column":22},"end":{"line":59,"column":3}},"line":53},"4":{"name":"(anonymous_4)","decl":{"start":{"line":65,"column":2},"end":{"line":65,"column":3}},"loc":{"start":{"line":65,"column":17},"end":{"line":73,"column":3}},"line":65}},"branchMap":{"0":{"loc":{"start":{"line":24,"column":4},"end":{"line":26,"column":5}},"type":"if","locations":[{"start":{"line":24,"column":4},"end":{"line":26,"column":5}},{"start":{},"end":{}}],"line":24},"1":{"loc":{"start":{"line":24,"column":8},"end":{"line":24,"column":41}},"type":"binary-expr","locations":[{"start":{"line":24,"column":8},"end":{"line":24,"column":13}},{"start":{"line":24,"column":17},"end":{"line":24,"column":41}}],"line":24},"2":{"loc":{"start":{"line":28,"column":4},"end":{"line":30,"column":5}},"type":"if","locations":[{"start":{"line":28,"column":4},"end":{"line":30,"column":5}},{"start":{},"end":{}}],"line":28},"3":{"loc":{"start":{"line":28,"column":8},"end":{"line":28,"column":43}},"type":"binary-expr","locations":[{"start":{"line":28,"column":8},"end":{"line":28,"column":14}},{"start":{"line":28,"column":18},"end":{"line":28,"column":43}}],"line":28},"4":{"loc":{"start":{"line":32,"column":4},"end":{"line":34,"column":5}},"type":"if","locations":[{"start":{"line":32,"column":4},"end":{"line":34,"column":5}},{"start":{},"end":{}}],"line":32},"5":{"loc":{"start":{"line":32,"column":8},"end":{"line":32,"column":40}},"type":"binary-expr","locations":[{"start":{"line":32,"column":8},"end":{"line":32,"column":17}},{"start":{"line":32,"column":21},"end":{"line":32,"column":40}}],"line":32},"6":{"loc":{"start":{"line":54,"column":4},"end":{"line":56,"column":5}},"type":"if","locations":[{"start":{"line":54,"column":4},"end":{"line":56,"column":5}},{"start":{},"end":{}}],"line":54},"7":{"loc":{"start":{"line":54,"column":8},"end":{"line":54,"column":47}},"type":"binary-expr","locations":[{"start":{"line":54,"column":8},"end":{"line":54,"column":16}},{"start":{"line":54,"column":20},"end":{"line":54,"column":47}}],"line":54}},"s":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0,"20":0,"21":0},"f":{"0":0,"1":0,"2":0,"3":0,"4":0},"b":{"0":[0,0],"1":[0,0],"2":[0,0],"3":[0,0],"4":[0,0],"5":[0,0],"6":[0,0],"7":[0,0]}} +,"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\infrastructure\\db\\DatabaseConnection.js": {"path":"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\infrastructure\\db\\DatabaseConnection.js","statementMap":{"0":{"start":{"line":1,"column":25},"end":{"line":1,"column":50}},"1":{"start":{"line":9,"column":4},"end":{"line":9,"column":23}},"2":{"start":{"line":16,"column":4},"end":{"line":19,"column":5}},"3":{"start":{"line":17,"column":6},"end":{"line":17,"column":57}},"4":{"start":{"line":18,"column":6},"end":{"line":18,"column":13}},"5":{"start":{"line":21,"column":4},"end":{"line":31,"column":5}},"6":{"start":{"line":22,"column":6},"end":{"line":24,"column":9}},"7":{"start":{"line":26,"column":6},"end":{"line":26,"column":35}},"8":{"start":{"line":27,"column":6},"end":{"line":27,"column":54}},"9":{"start":{"line":29,"column":6},"end":{"line":29,"column":63}},"10":{"start":{"line":30,"column":6},"end":{"line":30,"column":18}},"11":{"start":{"line":39,"column":4},"end":{"line":41,"column":5}},"12":{"start":{"line":40,"column":6},"end":{"line":40,"column":71}},"13":{"start":{"line":42,"column":4},"end":{"line":42,"column":23}},"14":{"start":{"line":49,"column":4},"end":{"line":53,"column":5}},"15":{"start":{"line":50,"column":6},"end":{"line":50,"column":38}},"16":{"start":{"line":51,"column":6},"end":{"line":51,"column":44}},"17":{"start":{"line":52,"column":6},"end":{"line":52,"column":25}},"18":{"start":{"line":61,"column":4},"end":{"line":67,"column":5}},"19":{"start":{"line":62,"column":6},"end":{"line":62,"column":44}},"20":{"start":{"line":63,"column":6},"end":{"line":63,"column":18}},"21":{"start":{"line":65,"column":6},"end":{"line":65,"column":62}},"22":{"start":{"line":66,"column":6},"end":{"line":66,"column":19}},"23":{"start":{"line":72,"column":27},"end":{"line":72,"column":51}},"24":{"start":{"line":74,"column":0},"end":{"line":74,"column":36}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":8,"column":2},"end":{"line":8,"column":3}},"loc":{"start":{"line":8,"column":16},"end":{"line":10,"column":3}},"line":8},"1":{"name":"(anonymous_1)","decl":{"start":{"line":15,"column":2},"end":{"line":15,"column":3}},"loc":{"start":{"line":15,"column":18},"end":{"line":32,"column":3}},"line":15},"2":{"name":"(anonymous_2)","decl":{"start":{"line":38,"column":2},"end":{"line":38,"column":3}},"loc":{"start":{"line":38,"column":14},"end":{"line":43,"column":3}},"line":38},"3":{"name":"(anonymous_3)","decl":{"start":{"line":48,"column":2},"end":{"line":48,"column":3}},"loc":{"start":{"line":48,"column":21},"end":{"line":54,"column":3}},"line":48},"4":{"name":"(anonymous_4)","decl":{"start":{"line":60,"column":2},"end":{"line":60,"column":3}},"loc":{"start":{"line":60,"column":22},"end":{"line":68,"column":3}},"line":60}},"branchMap":{"0":{"loc":{"start":{"line":16,"column":4},"end":{"line":19,"column":5}},"type":"if","locations":[{"start":{"line":16,"column":4},"end":{"line":19,"column":5}},{"start":{},"end":{}}],"line":16},"1":{"loc":{"start":{"line":23,"column":13},"end":{"line":23,"column":92}},"type":"cond-expr","locations":[{"start":{"line":23,"column":54},"end":{"line":23,"column":80}},{"start":{"line":23,"column":83},"end":{"line":23,"column":92}}],"line":23},"2":{"loc":{"start":{"line":39,"column":4},"end":{"line":41,"column":5}},"type":"if","locations":[{"start":{"line":39,"column":4},"end":{"line":41,"column":5}},{"start":{},"end":{}}],"line":39},"3":{"loc":{"start":{"line":49,"column":4},"end":{"line":53,"column":5}},"type":"if","locations":[{"start":{"line":49,"column":4},"end":{"line":53,"column":5}},{"start":{},"end":{}}],"line":49}},"s":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0,"20":0,"21":0,"22":0,"23":0,"24":0},"f":{"0":0,"1":0,"2":0,"3":0,"4":0},"b":{"0":[0,0],"1":[0,0],"2":[0,0],"3":[0,0]}} +,"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\infrastructure\\repositories\\UserRepository.js": {"path":"D:\\munka\\Egyetem\\25_26_II\\GKNB_MSTM071\\Backend\\negyedik gyakorlat\\src\\infrastructure\\repositories\\UserRepository.js","statementMap":{"0":{"start":{"line":1,"column":24},"end":{"line":1,"column":77}},"1":{"start":{"line":2,"column":13},"end":{"line":2,"column":48}},"2":{"start":{"line":10,"column":4},"end":{"line":10,"column":12}},"3":{"start":{"line":11,"column":4},"end":{"line":11,"column":25}},"4":{"start":{"line":20,"column":21},"end":{"line":22,"column":6}},"5":{"start":{"line":24,"column":4},"end":{"line":26,"column":5}},"6":{"start":{"line":25,"column":6},"end":{"line":25,"column":18}},"7":{"start":{"line":28,"column":4},"end":{"line":28,"column":36}},"8":{"start":{"line":37,"column":21},"end":{"line":39,"column":6}},"9":{"start":{"line":41,"column":4},"end":{"line":43,"column":5}},"10":{"start":{"line":42,"column":6},"end":{"line":42,"column":18}},"11":{"start":{"line":45,"column":4},"end":{"line":45,"column":36}},"12":{"start":{"line":53,"column":22},"end":{"line":55,"column":6}},"13":{"start":{"line":57,"column":4},"end":{"line":57,"column":63}},"14":{"start":{"line":57,"column":37},"end":{"line":57,"column":61}},"15":{"start":{"line":66,"column":21},"end":{"line":72,"column":6}},"16":{"start":{"line":74,"column":4},"end":{"line":74,"column":36}},"17":{"start":{"line":83,"column":21},"end":{"line":91,"column":6}},"18":{"start":{"line":93,"column":4},"end":{"line":93,"column":36}},"19":{"start":{"line":102,"column":4},"end":{"line":109,"column":5}},"20":{"start":{"line":103,"column":6},"end":{"line":105,"column":9}},"21":{"start":{"line":106,"column":6},"end":{"line":106,"column":18}},"22":{"start":{"line":108,"column":6},"end":{"line":108,"column":19}},"23":{"start":{"line":119,"column":4},"end":{"line":126,"column":6}},"24":{"start":{"line":130,"column":0},"end":{"line":130,"column":32}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":9,"column":2},"end":{"line":9,"column":3}},"loc":{"start":{"line":9,"column":22},"end":{"line":12,"column":3}},"line":9},"1":{"name":"(anonymous_1)","decl":{"start":{"line":19,"column":2},"end":{"line":19,"column":3}},"loc":{"start":{"line":19,"column":21},"end":{"line":29,"column":3}},"line":19},"2":{"name":"(anonymous_2)","decl":{"start":{"line":36,"column":2},"end":{"line":36,"column":3}},"loc":{"start":{"line":36,"column":27},"end":{"line":46,"column":3}},"line":36},"3":{"name":"(anonymous_3)","decl":{"start":{"line":52,"column":2},"end":{"line":52,"column":3}},"loc":{"start":{"line":52,"column":18},"end":{"line":58,"column":3}},"line":52},"4":{"name":"(anonymous_4)","decl":{"start":{"line":57,"column":25},"end":{"line":57,"column":26}},"loc":{"start":{"line":57,"column":37},"end":{"line":57,"column":61}},"line":57},"5":{"name":"(anonymous_5)","decl":{"start":{"line":65,"column":2},"end":{"line":65,"column":3}},"loc":{"start":{"line":65,"column":21},"end":{"line":75,"column":3}},"line":65},"6":{"name":"(anonymous_6)","decl":{"start":{"line":82,"column":2},"end":{"line":82,"column":3}},"loc":{"start":{"line":82,"column":21},"end":{"line":94,"column":3}},"line":82},"7":{"name":"(anonymous_7)","decl":{"start":{"line":101,"column":2},"end":{"line":101,"column":3}},"loc":{"start":{"line":101,"column":19},"end":{"line":110,"column":3}},"line":101},"8":{"name":"(anonymous_8)","decl":{"start":{"line":118,"column":2},"end":{"line":118,"column":3}},"loc":{"start":{"line":118,"column":22},"end":{"line":127,"column":3}},"line":118}},"branchMap":{"0":{"loc":{"start":{"line":24,"column":4},"end":{"line":26,"column":5}},"type":"if","locations":[{"start":{"line":24,"column":4},"end":{"line":26,"column":5}},{"start":{},"end":{}}],"line":24},"1":{"loc":{"start":{"line":41,"column":4},"end":{"line":43,"column":5}},"type":"if","locations":[{"start":{"line":41,"column":4},"end":{"line":43,"column":5}},{"start":{},"end":{}}],"line":41}},"s":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0,"20":0,"21":0,"22":0,"23":0,"24":0},"f":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0},"b":{"0":[0,0],"1":[0,0]}} +} diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/api/controllers/AuthController.js.html b/Backend/negyedik gyakorlat/coverage/lcov-report/api/controllers/AuthController.js.html new file mode 100644 index 0000000..83ccbfa --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/api/controllers/AuthController.js.html @@ -0,0 +1,394 @@ + + + + + + Code coverage report for api/controllers/AuthController.js + + + + + + + + + +
+
+

All files / api/controllers AuthController.js

+
+ +
+ 0% + Statements + 0/26 +
+ + +
+ 0% + Branches + 0/10 +
+ + +
+ 0% + Functions + 0/4 +
+ + +
+ 0% + Lines + 0/26 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
const RegisterUserCommand = require('../../application/auth/commands/RegisterUserCommand');
+const LoginUserCommand = require('../../application/auth/commands/LoginUserCommand');
+ 
+/**
+ * Auth Controller
+ * Authentication endpoints using CQRS Commands with cookie-based JWT
+ */
+class AuthController {
+  constructor(registerUserCommandHandler, loginUserCommandHandler, jwtService) {
+    this.registerUserCommandHandler = registerUserCommandHandler;
+    this.loginUserCommandHandler = loginUserCommandHandler;
+    this.jwtService = jwtService;
+  }
+ 
+  /**
+   * POST /api/auth/register - User regisztráció
+   */
+  async register(req, res) {
+    try {
+      const { name, email, password } = req.body;
+ 
+      const command = new RegisterUserCommand(name, email, password);
+      const result = await this.registerUserCommandHandler.handle(command);
+ 
+      // Set JWT token in httpOnly cookie
+      res.cookie(
+        this.jwtService.getCookieName(),
+        result.token,
+        this.jwtService.getCookieOptions()
+      );
+ 
+      res.status(201).json({
+        message: 'User registered successfully',
+        data: {
+          user: result.user
+        }
+      });
+    } catch (error) {
+      // Validációs hibák -> 400
+      const status = error.message.includes('required') || 
+                     error.message.includes('already exists') ||
+                     error.message.includes('Invalid') ||
+                     error.message.includes('must be') ? 400 : 500;
+ 
+      res.status(status).json({ error: error.message });
+    }
+  }
+ 
+  /**
+   * POST /api/auth/login - User bejelentkezés
+   */
+  async login(req, res) {
+    try {
+      const { email, password } = req.body;
+ 
+      const command = new LoginUserCommand(email, password);
+      const result = await this.loginUserCommandHandler.handle(command);
+ 
+      // Set JWT token in httpOnly cookie
+      res.cookie(
+        this.jwtService.getCookieName(),
+        result.token,
+        this.jwtService.getCookieOptions()
+      );
+ 
+      res.status(200).json({
+        message: 'Login successful',
+        data: {
+          user: result.user
+        }
+      });
+    } catch (error) {
+      // Validációs vagy auth hibák -> 401
+      const status = error.message.includes('Invalid') || 
+                     error.message.includes('required') ? 401 : 500;
+ 
+      res.status(status).json({ error: error.message });
+    }
+  }
+ 
+  /**
+   * POST /api/auth/logout - User kijelentkezés
+   */
+  async logout(req, res) {
+    try {
+      // Clear the auth cookie
+      res.clearCookie(this.jwtService.getCookieName(), {
+        httpOnly: true,
+        secure: process.env.NODE_ENV === 'production',
+        sameSite: 'strict',
+        path: '/'
+      });
+ 
+      res.status(200).json({
+        message: 'Logout successful'
+      });
+    } catch (error) {
+      res.status(500).json({ error: error.message });
+    }
+  }
+}
+ 
+module.exports = AuthController;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/api/controllers/UserController.js.html b/Backend/negyedik gyakorlat/coverage/lcov-report/api/controllers/UserController.js.html new file mode 100644 index 0000000..45dbf13 --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/api/controllers/UserController.js.html @@ -0,0 +1,397 @@ + + + + + + Code coverage report for api/controllers/UserController.js + + + + + + + + + +
+
+

All files / api/controllers UserController.js

+
+ +
+ 0% + Statements + 0/39 +
+ + +
+ 0% + Branches + 0/8 +
+ + +
+ 0% + Functions + 0/5 +
+ + +
+ 0% + Lines + 0/39 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
const GetMeQuery = require('../../application/user/queries/GetMeQuery');
+const GetAllUsersQuery = require('../../application/user/queries/GetAllUsersQuery');
+const GetUserByIdQuery = require('../../application/user/queries/GetUserByIdQuery');
+const UpdateUserProfileCommand = require('../../application/user/commands/UpdateUserProfileCommand');
+ 
+/**
+ * User Controller
+ * User-related endpoints using CQRS pattern (protected by JWT)
+ */
+class UserController {
+  constructor(getMeQueryHandler, getAllUsersQueryHandler, getUserByIdQueryHandler, updateUserProfileCommandHandler) {
+    this.getMeQueryHandler = getMeQueryHandler;
+    this.getAllUsersQueryHandler = getAllUsersQueryHandler;
+    this.getUserByIdQueryHandler = getUserByIdQueryHandler;
+    this.updateUserProfileCommandHandler = updateUserProfileCommandHandler;
+  }
+ 
+  /**
+   * GET /api/users/me - Bejelentkezett user adatai (protected)
+   */
+  async getMe(req, res) {
+    try {
+      // req.user-t az authMiddleware tölti ki a JWT-ből
+      const userId = req.user.userId;
+ 
+      const query = new GetMeQuery(userId);
+      const user = await this.getMeQueryHandler.handle(query);
+ 
+      res.status(200).json({
+        message: 'User retrieved successfully',
+        data: user
+      });
+    } catch (error) {
+      const status = error.message.includes('not found') ? 404 : 500;
+      res.status(status).json({ error: error.message });
+    }
+  }
+ 
+  /**
+   * GET /api/users - Összes user lekérése (protected)
+   */
+  async getAll(req, res) {
+    try {
+      const query = new GetAllUsersQuery();
+      const users = await this.getAllUsersQueryHandler.handle(query);
+ 
+      res.status(200).json({
+        message: 'Users retrieved successfully',
+        data: users,
+        count: users.length
+      });
+    } catch (error) {
+      res.status(500).json({ error: error.message });
+    }
+  }
+ 
+  /**
+   * GET /api/users/:id - User lekérése ID alapján (protected)
+   */
+  async getById(req, res) {
+    try {
+      const { id } = req.params;
+      const userId = parseInt(id);
+ 
+      if (isNaN(userId)) {
+        return res.status(400).json({ error: 'Invalid user ID' });
+      }
+ 
+      const query = new GetUserByIdQuery(userId);
+      const user = await this.getUserByIdQueryHandler.handle(query);
+ 
+      res.status(200).json({
+        message: 'User retrieved successfully',
+        data: user
+      });
+    } catch (error) {
+      const status = error.message.includes('not found') ? 404 : 500;
+      res.status(status).json({ error: error.message });
+    }
+  }
+ 
+  /**
+   * PUT /api/users/me - User profil frissítése (protected)
+   */
+  async updateMe(req, res) {
+    try {
+      const userId = req.user.userId;
+      const { name } = req.body;
+ 
+      const command = new UpdateUserProfileCommand(userId, name);
+      const user = await this.updateUserProfileCommandHandler.handle(command);
+ 
+      res.status(200).json({
+        message: 'Profile updated successfully',
+        data: user
+      });
+    } catch (error) {
+      const status = error.message.includes('required') ? 400 : 500;
+      res.status(status).json({ error: error.message });
+    }
+  }
+}
+ 
+module.exports = UserController;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/api/controllers/index.html b/Backend/negyedik gyakorlat/coverage/lcov-report/api/controllers/index.html new file mode 100644 index 0000000..24b2429 --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/api/controllers/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for api/controllers + + + + + + + + + +
+
+

All files api/controllers

+
+ +
+ 0% + Statements + 0/65 +
+ + +
+ 0% + Branches + 0/18 +
+ + +
+ 0% + Functions + 0/9 +
+ + +
+ 0% + Lines + 0/65 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
AuthController.js +
+
0%0/260%0/100%0/40%0/26
UserController.js +
+
0%0/390%0/80%0/50%0/39
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/api/middlewares/authMiddleware.js.html b/Backend/negyedik gyakorlat/coverage/lcov-report/api/middlewares/authMiddleware.js.html new file mode 100644 index 0000000..942bdea --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/api/middlewares/authMiddleware.js.html @@ -0,0 +1,217 @@ + + + + + + Code coverage report for api/middlewares/authMiddleware.js + + + + + + + + + +
+
+

All files / api/middlewares authMiddleware.js

+
+ +
+ 0% + Statements + 0/11 +
+ + +
+ 0% + Branches + 0/2 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 0% + Lines + 0/11 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
const JwtService = require('../../application/services/JwtService');
+ 
+const jwtService = new JwtService();
+ 
+/**
+ * Authentication Middleware - JWT token ellenőrzés (Cookie-based)
+ * 
+ * Ezt a middleware-t használd protected route-okon!
+ * 
+ * Példa használat:
+ * router.get('/me', authMiddleware, userController.getMe);
+ */
+function authMiddleware(req, res, next) {
+  try {
+    // 1. Token kinyerése cookieból
+    const token = jwtService.extractTokenFromCookies(req.cookies);
+    
+    if (!token) {
+      return res.status(401).json({ 
+        error: 'Authentication required',
+        message: 'No token provided in cookies'
+      });
+    }
+ 
+    // 2. Token verifikálása
+    const decoded = jwtService.verifyToken(token);
+ 
+    // 3. User adatok elhelyezése req.user-ben (controller-ek használhatják)
+    req.user = {
+      userId: decoded.userId,
+      email: decoded.email
+    };
+ 
+    // 4. Folytatás
+    next();
+  } catch (error) {
+    return res.status(401).json({ 
+      error: 'Authentication failed',
+      message: error.message
+    });
+  }
+}
+ 
+module.exports = authMiddleware;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/api/middlewares/corsMiddleware.js.html b/Backend/negyedik gyakorlat/coverage/lcov-report/api/middlewares/corsMiddleware.js.html new file mode 100644 index 0000000..fc33282 --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/api/middlewares/corsMiddleware.js.html @@ -0,0 +1,151 @@ + + + + + + Code coverage report for api/middlewares/corsMiddleware.js + + + + + + + + + +
+
+

All files / api/middlewares corsMiddleware.js

+
+ +
+ 0% + Statements + 0/4 +
+ + +
+ 0% + Branches + 0/2 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 0% + Lines + 0/4 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
const cors = require('cors');
+ 
+// Engedélyezett origin-ek whitelist (környezeti változóból)
+const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'];
+ 
+const corsOptions = {
+  origin: function (origin, callback) {
+    // TODO 1: Ha nincs origin (pl. Postman, curl, backend-backend hívás), engedélyezd
+    // Tipp: if (!origin) return callback(null, true);
+    
+    // TODO 2: Ha az origin benne van az allowedOrigins-ban, engedélyezd
+    // Tipp: if (allowedOrigins.includes(origin)) return callback(null, true);
+    
+    // TODO 3: Egyébként tiltsd le CORS hibával
+    // Tipp: callback(new Error('Not allowed by CORS'));
+  },
+  credentials: true, // Cookie/Auth header engedélyezése
+  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
+  allowedHeaders: ['Content-Type', 'Authorization']
+};
+ 
+module.exports = cors(corsOptions);
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/api/middlewares/index.html b/Backend/negyedik gyakorlat/coverage/lcov-report/api/middlewares/index.html new file mode 100644 index 0000000..9e1da4d --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/api/middlewares/index.html @@ -0,0 +1,146 @@ + + + + + + Code coverage report for api/middlewares + + + + + + + + + +
+
+

All files api/middlewares

+
+ +
+ 0% + Statements + 0/17 +
+ + +
+ 0% + Branches + 0/4 +
+ + +
+ 0% + Functions + 0/4 +
+ + +
+ 0% + Lines + 0/17 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
authMiddleware.js +
+
0%0/110%0/20%0/10%0/11
corsMiddleware.js +
+
0%0/40%0/20%0/10%0/4
scopeMiddleware.js +
+
0%0/2100%0/00%0/20%0/2
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/api/middlewares/scopeMiddleware.js.html b/Backend/negyedik gyakorlat/coverage/lcov-report/api/middlewares/scopeMiddleware.js.html new file mode 100644 index 0000000..81d6f35 --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/api/middlewares/scopeMiddleware.js.html @@ -0,0 +1,139 @@ + + + + + + Code coverage report for api/middlewares/scopeMiddleware.js + + + + + + + + + +
+
+

All files / api/middlewares scopeMiddleware.js

+
+ +
+ 0% + Statements + 0/2 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/2 +
+ + +
+ 0% + Lines + 0/2 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
/**
+ * Request szintű DI scope létrehozása
+ * Middleware ami minden kéréshez új DI scope-ot készít
+ */
+function scopeMiddleware(container) {
+  return (req, res, next) => {
+    // TODO 1: Hozz létre request-specifikus scope-ot
+    // Tipp: const scope = container.createScope();
+    
+    // TODO 2: Tárold el a scope-ot req.scope alatt
+    // Tipp: req.scope = scope;
+    
+    // TODO 3: Hívd meg a next()-et
+    // Tipp: next();
+  };
+}
+ 
+module.exports = scopeMiddleware;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/api/routers/authRoutes.js.html b/Backend/negyedik gyakorlat/coverage/lcov-report/api/routers/authRoutes.js.html new file mode 100644 index 0000000..5771f7c --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/api/routers/authRoutes.js.html @@ -0,0 +1,214 @@ + + + + + + Code coverage report for api/routers/authRoutes.js + + + + + + + + + +
+
+

All files / api/routers authRoutes.js

+
+ +
+ 0% + Statements + 0/13 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/5 +
+ + +
+ 0% + Lines + 0/9 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
const express = require('express');
+ 
+/**
+ * Auth Routes
+ * Public endpoints (nincs JWT védelem)
+ * 
+ * @param {Container} container - DI Container
+ */
+function createAuthRoutes(container) {
+  const router = express.Router();
+ 
+  // AuthController lekérése a DI Container-ből
+  const authController = container.resolve('AuthController');
+ 
+  /**
+   * POST /api/auth/register - User regisztráció
+   * Body: { name, email, password }
+   * Response: { user, token }
+   */
+  router.post('/register', (req, res) => authController.register(req, res));
+ 
+  /**
+   * POST /api/auth/login - User bejelentkezés
+   * Body: { email, password }
+   * Response: { user, token }
+   */
+  router.post('/login', (req, res) => authController.login(req, res));
+ 
+  /**
+   * POST /api/auth/logout - User kijelentkezés
+   * Clears the authentication cookie
+   */
+  router.post('/logout', (req, res) => authController.logout(req, res));
+ 
+  /**
+   * OPTIONS /api/auth/* - CORS preflight
+   */
+  router.options('*', (req, res) => res.sendStatus(204));
+ 
+  return router;
+}
+ 
+module.exports = createAuthRoutes;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/api/routers/index.html b/Backend/negyedik gyakorlat/coverage/lcov-report/api/routers/index.html new file mode 100644 index 0000000..6349d0b --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/api/routers/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for api/routers + + + + + + + + + +
+
+

All files api/routers

+
+ +
+ 0% + Statements + 0/29 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/11 +
+ + +
+ 0% + Lines + 0/20 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
authRoutes.js +
+
0%0/13100%0/00%0/50%0/9
userRoutes.js +
+
0%0/16100%0/00%0/60%0/11
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/api/routers/userRoutes.js.html b/Backend/negyedik gyakorlat/coverage/lcov-report/api/routers/userRoutes.js.html new file mode 100644 index 0000000..8565149 --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/api/routers/userRoutes.js.html @@ -0,0 +1,232 @@ + + + + + + Code coverage report for api/routers/userRoutes.js + + + + + + + + + +
+
+

All files / api/routers userRoutes.js

+
+ +
+ 0% + Statements + 0/16 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/6 +
+ + +
+ 0% + Lines + 0/11 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
const express = require('express');
+const authMiddleware = require('../middlewares/authMiddleware');
+ 
+/**
+ * User Routes
+ * Protected endpoints (JWT authentication required)
+ * 
+ * @param {Container} container - DI Container
+ */
+function createUserRoutes(container) {
+  const router = express.Router();
+ 
+  // UserController lekérése a DI Container-ből
+  const userController = container.resolve('UserController');
+ 
+  /**
+   * GET /api/users/me - Bejelentkezett user adatai
+   * Headers: Authorization: Bearer <token>
+   */
+  router.get('/me', authMiddleware, (req, res) => userController.getMe(req, res));
+ 
+  /**
+   * PUT /api/users/me - User profil frissítése
+   * Headers: Authorization: Bearer <token>
+   * Body: { name }
+   */
+  router.put('/me', authMiddleware, (req, res) => userController.updateMe(req, res));
+ 
+  /**
+   * GET /api/users - Összes user lekérése (protected)
+   * Headers: Authorization: Bearer <token>
+   */
+  router.get('/', authMiddleware, (req, res) => userController.getAll(req, res));
+ 
+  /**
+   * GET /api/users/:id - User lekérése ID alapján (protected)
+   * Headers: Authorization: Bearer <token>
+   */
+  router.get('/:id', authMiddleware, (req, res) => userController.getById(req, res));
+ 
+  /**
+   * OPTIONS /api/users/* - CORS preflight
+   */
+  router.options('*', (req, res) => res.sendStatus(204));
+ 
+  return router;
+}
+ 
+module.exports = createUserRoutes;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/application/auth/commands/LoginUserCommand.js.html b/Backend/negyedik gyakorlat/coverage/lcov-report/application/auth/commands/LoginUserCommand.js.html new file mode 100644 index 0000000..d124121 --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/application/auth/commands/LoginUserCommand.js.html @@ -0,0 +1,121 @@ + + + + + + Code coverage report for application/auth/commands/LoginUserCommand.js + + + + + + + + + +
+
+

All files / application/auth/commands LoginUserCommand.js

+
+ +
+ 0% + Statements + 0/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 0% + Lines + 0/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13  +  +  +  +  +  +  +  +  +  +  +  + 
/**
+ * Login User Command
+ * Command object for user login
+ */
+class LoginUserCommand {
+  constructor(email, password) {
+    this.email = email;
+    this.password = password;
+  }
+}
+ 
+module.exports = LoginUserCommand;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/application/auth/commands/LoginUserCommandHandler.js.html b/Backend/negyedik gyakorlat/coverage/lcov-report/application/auth/commands/LoginUserCommandHandler.js.html new file mode 100644 index 0000000..991b616 --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/application/auth/commands/LoginUserCommandHandler.js.html @@ -0,0 +1,265 @@ + + + + + + Code coverage report for application/auth/commands/LoginUserCommandHandler.js + + + + + + + + + +
+
+

All files / application/auth/commands LoginUserCommandHandler.js

+
+ +
+ 0% + Statements + 0/17 +
+ + +
+ 0% + Branches + 0/8 +
+ + +
+ 0% + Functions + 0/2 +
+ + +
+ 0% + Lines + 0/17 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
const bcrypt = require('bcryptjs');
+const JwtService = require('../../services/JwtService');
+ 
+const jwtService = new JwtService();
+ 
+/**
+ * Login User Command Handler
+ * Handles user login authentication
+ */
+class LoginUserCommandHandler {
+  constructor(prisma) {
+    this.prisma = prisma;
+  }
+ 
+  /**
+   * Execute login command
+   * @param {LoginUserCommand} command 
+   * @returns {Promise<Object>} { user, token }
+   */
+  async handle(command) {
+    const { email, password } = command;
+ 
+    // Validáció
+    if (!email || !password) {
+      throw new Error('Email and password are required');
+    }
+ 
+    // User keresése email alapján
+    const user = await this.prisma.user.findUnique({
+      where: { email }
+    });
+ 
+    if (!user) {
+      throw new Error('Invalid email or password');
+    }
+ 
+    // Jelszó ellenőrzése
+    const isPasswordValid = await bcrypt.compare(password, user.password);
+ 
+    if (!isPasswordValid) {
+      throw new Error('Invalid email or password');
+    }
+ 
+    // JWT token generálása
+    const token = jwtService.generateToken({
+      userId: user.id,
+      email: user.email
+    });
+ 
+    // Jelszót ne adjuk vissza
+    const { password: _, ...userWithoutPassword } = user;
+ 
+    return {
+      user: userWithoutPassword,
+      token
+    };
+  }
+}
+ 
+module.exports = LoginUserCommandHandler;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/application/auth/commands/RegisterUserCommand.js.html b/Backend/negyedik gyakorlat/coverage/lcov-report/application/auth/commands/RegisterUserCommand.js.html new file mode 100644 index 0000000..432bbae --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/application/auth/commands/RegisterUserCommand.js.html @@ -0,0 +1,124 @@ + + + + + + Code coverage report for application/auth/commands/RegisterUserCommand.js + + + + + + + + + +
+
+

All files / application/auth/commands RegisterUserCommand.js

+
+ +
+ 0% + Statements + 0/4 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 0% + Lines + 0/4 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14  +  +  +  +  +  +  +  +  +  +  +  +  + 
/**
+ * Register User Command
+ * Command object for user registration
+ */
+class RegisterUserCommand {
+  constructor(name, email, password) {
+    this.name = name;
+    this.email = email;
+    this.password = password;
+  }
+}
+ 
+module.exports = RegisterUserCommand;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/application/auth/commands/RegisterUserCommandHandler.js.html b/Backend/negyedik gyakorlat/coverage/lcov-report/application/auth/commands/RegisterUserCommandHandler.js.html new file mode 100644 index 0000000..ec2b543 --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/application/auth/commands/RegisterUserCommandHandler.js.html @@ -0,0 +1,334 @@ + + + + + + Code coverage report for application/auth/commands/RegisterUserCommandHandler.js + + + + + + + + + +
+
+

All files / application/auth/commands RegisterUserCommandHandler.js

+
+ +
+ 0% + Statements + 0/25 +
+ + +
+ 0% + Branches + 0/13 +
+ + +
+ 0% + Functions + 0/3 +
+ + +
+ 0% + Lines + 0/25 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
const bcrypt = require('bcryptjs');
+const JwtService = require('../../services/JwtService');
+ 
+const jwtService = new JwtService();
+ 
+/**
+ * Register User Command Handler
+ * Handles user registration business logic
+ */
+class RegisterUserCommandHandler {
+  constructor(prisma, emailService) {
+    this.prisma = prisma;
+    this.emailService = emailService;
+  }
+ 
+  /**
+   * Execute user registration command
+   * @param {RegisterUserCommand} command 
+   * @returns {Promise<Object>} { user, token }
+   */
+  async handle(command) {
+    const { name, email, password } = command;
+ 
+    // Validáció
+    if (!name || !email || !password) {
+      throw new Error('Name, email and password are required');
+    }
+ 
+    if (password.length < 6) {
+      throw new Error('Password must be at least 6 characters long');
+    }
+ 
+    // Email formátum ellenőrzés
+    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+    if (!emailRegex.test(email)) {
+      throw new Error('Invalid email format');
+    }
+ 
+    // Ellenőrizzük, hogy létezik-e már a user
+    const existingUser = await this.prisma.user.findUnique({
+      where: { email }
+    });
+ 
+    if (existingUser) {
+      throw new Error('User with this email already exists');
+    }
+ 
+    // Jelszó hashelése (bcrypt)
+    const hashedPassword = await bcrypt.hash(password, 10);
+ 
+    // User létrehozása
+    const user = await this.prisma.user.create({
+      data: {
+        name,
+        email,
+        password: hashedPassword
+      }
+    });
+ 
+    // Welcome email küldése (async, nem várunk rá)
+    if (this.emailService) {
+      this.emailService.sendWelcomeEmail(email, name).catch(err => {
+        console.error('❌ Failed to send welcome email:', err.message);
+      });
+    }
+ 
+    // JWT token generálása
+    const token = jwtService.generateToken({
+      userId: user.id,
+      email: user.email
+    });
+ 
+    // Jelszót ne adjuk vissza a response-ban
+    const { password: _, ...userWithoutPassword } = user;
+ 
+    return {
+      user: userWithoutPassword,
+      token
+    };
+  }
+}
+ 
+module.exports = RegisterUserCommandHandler;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/application/auth/commands/index.html b/Backend/negyedik gyakorlat/coverage/lcov-report/application/auth/commands/index.html new file mode 100644 index 0000000..7ac7bd5 --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/application/auth/commands/index.html @@ -0,0 +1,161 @@ + + + + + + Code coverage report for application/auth/commands + + + + + + + + + +
+
+

All files application/auth/commands

+
+ +
+ 0% + Statements + 0/49 +
+ + +
+ 0% + Branches + 0/21 +
+ + +
+ 0% + Functions + 0/7 +
+ + +
+ 0% + Lines + 0/49 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
LoginUserCommand.js +
+
0%0/3100%0/00%0/10%0/3
LoginUserCommandHandler.js +
+
0%0/170%0/80%0/20%0/17
RegisterUserCommand.js +
+
0%0/4100%0/00%0/10%0/4
RegisterUserCommandHandler.js +
+
0%0/250%0/130%0/30%0/25
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/application/services/Container.js.html b/Backend/negyedik gyakorlat/coverage/lcov-report/application/services/Container.js.html new file mode 100644 index 0000000..c5c82ab --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/application/services/Container.js.html @@ -0,0 +1,256 @@ + + + + + + Code coverage report for application/services/Container.js + + + + + + + + + +
+
+

All files / application/services Container.js

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 2/2 +
+ + +
+ 100% + Functions + 4/4 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x + 
/**
+ * Dependency Injection Container
+ * Supports singleton, transient, and scoped lifetimes
+ */
+class Container {
+  constructor() {
+    // TODO 1: Inicializáld a services Map-et (singleton instance-ok tárolása)
+    // TODO 2: Inicializáld a factories Map-et (factory függvények tárolása)
+    // TODO 3: Inicializáld a lifetimes Map-et (lifecycle típusok tárolása)
+    
+    // Példa inicializálás:
+    // this.services = new Map();
+    // this.factories = new Map();
+    // this.lifetimes = new Map();
+  }
+ 
+  register(name, factory, lifetime = 'singleton') {
+    // TODO 4: Tárold el a factory függvényt (this.factories.set(name, factory))
+    // TODO 5: Tárold el a lifetime típust (this.lifetimes.set(name, lifetime))
+    // TODO 6: Ha a lifetime === 'singleton', azonnal példányosítsd:
+    //         - Hívd meg a factory-t: const instance = factory();
+    //         - Tárold el: this.services.set(name, instance);
+  }
+ 
+  resolve(name, scope = null) {
+    // TODO 7: Ha a service regisztrálva van mint 'scoped' ÉS van scope paraméter:
+    //         - Ellenőrizd: if (scope && scope.has(name)) return scope.get(name);
+    //         - Ha nincs még a scope-ban, példányosítsd és tárold: 
+    //           const instance = this.factories.get(name)();
+    //           scope.set(name, instance);
+    //           return instance;
+    
+    // TODO 8: Ha singleton, add vissza a services-ből:
+    //         if (this.lifetimes.get(name) === 'singleton') {
+    //           return this.services.get(name);
+    //         }
+    
+    // TODO 9: Ha transient, minden alkalommal hívj egy új factory-t:
+    //         if (this.lifetimes.get(name) === 'transient') {
+    //           return this.factories.get(name)();
+    //         }
+    
+    // TODO 10: Ha nem regisztrált a service, dobj hibát:
+    //          throw new Error(`Service '${name}' is not registered`);
+  }
+ 
+  createScope() {
+    // TODO 11: Hozz létre egy új Map-et az scoped instance-oknak
+    // TODO 12: Térj vissza egy objektummal ami tartalmaz egy resolve metódust:
+    //          const scopeMap = new Map();
+    //          return {
+    //            resolve: (name) => this.resolve(name, scopeMap)
+    //          };
+  }
+}
+ 
+module.exports = Container;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/application/services/EmailService.js.html b/Backend/negyedik gyakorlat/coverage/lcov-report/application/services/EmailService.js.html new file mode 100644 index 0000000..3e70f84 --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/application/services/EmailService.js.html @@ -0,0 +1,178 @@ + + + + + + Code coverage report for application/services/EmailService.js + + + + + + + + + +
+
+

All files / application/services EmailService.js

+
+ +
+ 0% + Statements + 0/4 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/2 +
+ + +
+ 0% + Lines + 0/4 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
/**
+ * Email Service
+ * Nodemailer + Handlebars template based email sending
+ * 
+ * TODO: Implement by students
+ */
+class EmailService {
+  constructor() {
+    // TODO 1: Hozz létre Nodemailer transportot Ethereal tesztelő SMTP-vel
+    //         this.transporter = nodemailer.createTransport({
+    //           host: 'smtp.ethereal.email',
+    //           port: 587,
+    //           secure: false, // TLS
+    //           auth: {
+    //             user: process.env.ETHEREAL_USER || 'your-test-email@ethereal.email',
+    //             pass: process.env.ETHEREAL_PASS || 'your-test-password'
+    //           }
+    //         });
+    
+    console.log('📧 EmailService initialized');
+  }
+ 
+  async sendWelcomeEmail(userEmail, userName) {
+    // TODO 2-6: Implement email sending with Handlebars template
+    // For now, just log to console
+    console.log(`📧 Would send welcome email to ${userEmail} (${userName})`);
+    return true;
+  }
+}
+ 
+module.exports = EmailService;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/application/services/JwtService.js.html b/Backend/negyedik gyakorlat/coverage/lcov-report/application/services/JwtService.js.html new file mode 100644 index 0000000..fde995a --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/application/services/JwtService.js.html @@ -0,0 +1,427 @@ + + + + + + Code coverage report for application/services/JwtService.js + + + + + + + + + +
+
+

All files / application/services JwtService.js

+
+ +
+ 0% + Statements + 0/29 +
+ + +
+ 0% + Branches + 0/20 +
+ + +
+ 0% + Functions + 0/8 +
+ + +
+ 0% + Lines + 0/29 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
const jwt = require('jsonwebtoken');
+ 
+/**
+ * JWT Service
+ * Handles JWT token generation, verification, and cookie management
+ */
+class JwtService {
+  constructor() {
+    this.secret = process.env.JWT_SECRET || 'default-secret-change-me';
+    this.expiresIn = process.env.JWT_EXPIRES_IN || '1h';
+    this.cookieName = 'auth_token';
+  }
+ 
+  /**
+   * Generate JWT token
+   * @param {Object} payload - { userId, email }
+   * @returns {string} JWT token
+   */
+  generateToken(payload) {
+    return jwt.sign(payload, this.secret, { expiresIn: this.expiresIn });
+  }
+ 
+  /**
+   * Verify JWT token
+   * @param {string} token - JWT token
+   * @returns {Object} Decoded payload
+   */
+  verifyToken(token) {
+    try {
+      return jwt.verify(token, this.secret);
+    } catch (error) {
+      throw new Error('Invalid or expired token');
+    }
+  }
+ 
+  /**
+   * Extract token from Authorization header (Bearer token)
+   * @param {string} authHeader - Authorization header value
+   * @returns {string|null} Token or null
+   */
+  extractTokenFromHeader(authHeader) {
+    if (!authHeader) {
+      return null;
+    }
+ 
+    const parts = authHeader.split(' ');
+    
+    if (parts.length !== 2 || parts[0] !== 'Bearer') {
+      return null;
+    }
+ 
+    return parts[1];
+  }
+ 
+  /**
+   * Extract token from cookies
+   * @param {Object} cookies - Request cookies object
+   * @returns {string|null} Token or null
+   */
+  extractTokenFromCookies(cookies) {
+    if (!cookies || !cookies[this.cookieName]) {
+      return null;
+    }
+ 
+    return cookies[this.cookieName];
+  }
+ 
+  /**
+   * Get cookie options for setting JWT cookie
+   * @returns {Object} Cookie options
+   */
+  getCookieOptions() {
+    const isProduction = process.env.NODE_ENV === 'production';
+    
+    return {
+      httpOnly: true,        // Prevents XSS attacks
+      secure: isProduction,  // HTTPS only in production
+      sameSite: 'strict',    // CSRF protection
+      maxAge: this._getMaxAge(),
+      path: '/'
+    };
+  }
+ 
+  /**
+   * Get cookie max age in milliseconds
+   * @private
+   * @returns {number}
+   */
+  _getMaxAge() {
+    // Parse JWT_EXPIRES_IN (e.g., "1h", "7d")
+    const expiresIn = this.expiresIn;
+    
+    if (expiresIn.endsWith('h')) {
+      return parseInt(expiresIn) * 60 * 60 * 1000;
+    } else if (expiresIn.endsWith('d')) {
+      return parseInt(expiresIn) * 24 * 60 * 60 * 1000;
+    } else if (expiresIn.endsWith('m')) {
+      return parseInt(expiresIn) * 60 * 1000;
+    }
+    
+    // Default: 1 hour
+    return 60 * 60 * 1000;
+  }
+ 
+  /**
+   * Get cookie name
+   * @returns {string}
+   */
+  getCookieName() {
+    return this.cookieName;
+  }
+}
+ 
+module.exports = JwtService;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/application/services/index.html b/Backend/negyedik gyakorlat/coverage/lcov-report/application/services/index.html new file mode 100644 index 0000000..9eac7ae --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/application/services/index.html @@ -0,0 +1,146 @@ + + + + + + Code coverage report for application/services + + + + + + + + + +
+
+

All files application/services

+
+ +
+ 2.94% + Statements + 1/34 +
+ + +
+ 9.09% + Branches + 2/22 +
+ + +
+ 28.57% + Functions + 4/14 +
+ + +
+ 2.94% + Lines + 1/34 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
Container.js +
+
100%1/1100%2/2100%4/4100%1/1
EmailService.js +
+
0%0/4100%0/00%0/20%0/4
JwtService.js +
+
0%0/290%0/200%0/80%0/29
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/application/user/commands/UpdateUserProfileCommand.js.html b/Backend/negyedik gyakorlat/coverage/lcov-report/application/user/commands/UpdateUserProfileCommand.js.html new file mode 100644 index 0000000..07c0aeb --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/application/user/commands/UpdateUserProfileCommand.js.html @@ -0,0 +1,121 @@ + + + + + + Code coverage report for application/user/commands/UpdateUserProfileCommand.js + + + + + + + + + +
+
+

All files / application/user/commands UpdateUserProfileCommand.js

+
+ +
+ 0% + Statements + 0/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 0% + Lines + 0/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13  +  +  +  +  +  +  +  +  +  +  +  + 
/**
+ * Update User Profile Command
+ * Command object for updating user profile
+ */
+class UpdateUserProfileCommand {
+  constructor(userId, name) {
+    this.userId = userId;
+    this.name = name;
+  }
+}
+ 
+module.exports = UpdateUserProfileCommand;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/application/user/commands/UpdateUserProfileCommandHandler.js.html b/Backend/negyedik gyakorlat/coverage/lcov-report/application/user/commands/UpdateUserProfileCommandHandler.js.html new file mode 100644 index 0000000..78e7717 --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/application/user/commands/UpdateUserProfileCommandHandler.js.html @@ -0,0 +1,181 @@ + + + + + + Code coverage report for application/user/commands/UpdateUserProfileCommandHandler.js + + + + + + + + + +
+
+

All files / application/user/commands UpdateUserProfileCommandHandler.js

+
+ +
+ 0% + Statements + 0/8 +
+ + +
+ 0% + Branches + 0/2 +
+ + +
+ 0% + Functions + 0/2 +
+ + +
+ 0% + Lines + 0/8 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
/**
+ * Update User Profile Command Handler
+ * Handles user profile update logic
+ */
+class UpdateUserProfileCommandHandler {
+  constructor(prisma) {
+    this.prisma = prisma;
+  }
+ 
+  /**
+   * Execute update profile command
+   * @param {UpdateUserProfileCommand} command 
+   * @returns {Promise<Object>} Updated user data
+   */
+  async handle(command) {
+    const { userId, name } = command;
+ 
+    if (!name) {
+      throw new Error('Name is required');
+    }
+ 
+    const user = await this.prisma.user.update({
+      where: { id: userId },
+      data: { name }
+    });
+ 
+    const { password, ...userWithoutPassword } = user;
+    return userWithoutPassword;
+  }
+}
+ 
+module.exports = UpdateUserProfileCommandHandler;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/application/user/commands/index.html b/Backend/negyedik gyakorlat/coverage/lcov-report/application/user/commands/index.html new file mode 100644 index 0000000..702c253 --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/application/user/commands/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for application/user/commands + + + + + + + + + +
+
+

All files application/user/commands

+
+ +
+ 0% + Statements + 0/11 +
+ + +
+ 0% + Branches + 0/2 +
+ + +
+ 0% + Functions + 0/3 +
+ + +
+ 0% + Lines + 0/11 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
UpdateUserProfileCommand.js +
+
0%0/3100%0/00%0/10%0/3
UpdateUserProfileCommandHandler.js +
+
0%0/80%0/20%0/20%0/8
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/application/user/queries/GetAllUsersQuery.js.html b/Backend/negyedik gyakorlat/coverage/lcov-report/application/user/queries/GetAllUsersQuery.js.html new file mode 100644 index 0000000..9a80ec9 --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/application/user/queries/GetAllUsersQuery.js.html @@ -0,0 +1,118 @@ + + + + + + Code coverage report for application/user/queries/GetAllUsersQuery.js + + + + + + + + + +
+
+

All files / application/user/queries GetAllUsersQuery.js

+
+ +
+ 0% + Statements + 0/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 0% + Lines + 0/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12  +  +  +  +  +  +  +  +  +  +  + 
/**
+ * Get All Users Query
+ * Query object for retrieving all users
+ */
+class GetAllUsersQuery {
+  constructor() {
+    // No parameters needed for getting all users
+  }
+}
+ 
+module.exports = GetAllUsersQuery;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/application/user/queries/GetAllUsersQueryHandler.js.html b/Backend/negyedik gyakorlat/coverage/lcov-report/application/user/queries/GetAllUsersQueryHandler.js.html new file mode 100644 index 0000000..7080510 --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/application/user/queries/GetAllUsersQueryHandler.js.html @@ -0,0 +1,160 @@ + + + + + + Code coverage report for application/user/queries/GetAllUsersQueryHandler.js + + + + + + + + + +
+
+

All files / application/user/queries GetAllUsersQueryHandler.js

+
+ +
+ 0% + Statements + 0/5 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/3 +
+ + +
+ 0% + Lines + 0/4 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
/**
+ * Get All Users Query Handler
+ * Handles retrieval of all users
+ */
+class GetAllUsersQueryHandler {
+  constructor(prisma) {
+    this.prisma = prisma;
+  }
+ 
+  /**
+   * Execute get all users query
+   * @param {GetAllUsersQuery} query 
+   * @returns {Promise<Array>} List of users without passwords
+   */
+  async handle(query) {
+    const users = await this.prisma.user.findMany({
+      orderBy: { createdAt: 'desc' }
+    });
+ 
+    // Remove passwords from all users
+    return users.map(({ password, ...user }) => user);
+  }
+}
+ 
+module.exports = GetAllUsersQueryHandler;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/application/user/queries/GetMeQuery.js.html b/Backend/negyedik gyakorlat/coverage/lcov-report/application/user/queries/GetMeQuery.js.html new file mode 100644 index 0000000..ba8146e --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/application/user/queries/GetMeQuery.js.html @@ -0,0 +1,118 @@ + + + + + + Code coverage report for application/user/queries/GetMeQuery.js + + + + + + + + + +
+
+

All files / application/user/queries GetMeQuery.js

+
+ +
+ 0% + Statements + 0/2 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 0% + Lines + 0/2 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12  +  +  +  +  +  +  +  +  +  +  + 
/**
+ * Get Me Query
+ * Query object for getting current authenticated user
+ */
+class GetMeQuery {
+  constructor(userId) {
+    this.userId = userId;
+  }
+}
+ 
+module.exports = GetMeQuery;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/application/user/queries/GetMeQueryHandler.js.html b/Backend/negyedik gyakorlat/coverage/lcov-report/application/user/queries/GetMeQueryHandler.js.html new file mode 100644 index 0000000..953c0fc --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/application/user/queries/GetMeQueryHandler.js.html @@ -0,0 +1,178 @@ + + + + + + Code coverage report for application/user/queries/GetMeQueryHandler.js + + + + + + + + + +
+
+

All files / application/user/queries GetMeQueryHandler.js

+
+ +
+ 0% + Statements + 0/8 +
+ + +
+ 0% + Branches + 0/2 +
+ + +
+ 0% + Functions + 0/2 +
+ + +
+ 0% + Lines + 0/8 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
/**
+ * Get Me Query Handler
+ * Handles retrieval of current authenticated user
+ */
+class GetMeQueryHandler {
+  constructor(prisma) {
+    this.prisma = prisma;
+  }
+ 
+  /**
+   * Execute get me query
+   * @param {GetMeQuery} query 
+   * @returns {Promise<Object>} User data without password
+   */
+  async handle(query) {
+    const { userId } = query;
+ 
+    const user = await this.prisma.user.findUnique({
+      where: { id: userId }
+    });
+ 
+    if (!user) {
+      throw new Error('User not found');
+    }
+ 
+    const { password, ...userWithoutPassword } = user;
+    return userWithoutPassword;
+  }
+}
+ 
+module.exports = GetMeQueryHandler;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/application/user/queries/GetUserByIdQuery.js.html b/Backend/negyedik gyakorlat/coverage/lcov-report/application/user/queries/GetUserByIdQuery.js.html new file mode 100644 index 0000000..a3bb8ff --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/application/user/queries/GetUserByIdQuery.js.html @@ -0,0 +1,118 @@ + + + + + + Code coverage report for application/user/queries/GetUserByIdQuery.js + + + + + + + + + +
+
+

All files / application/user/queries GetUserByIdQuery.js

+
+ +
+ 0% + Statements + 0/2 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 0% + Lines + 0/2 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12  +  +  +  +  +  +  +  +  +  +  + 
/**
+ * Get User By ID Query
+ * Query object for retrieving a user by ID
+ */
+class GetUserByIdQuery {
+  constructor(userId) {
+    this.userId = userId;
+  }
+}
+ 
+module.exports = GetUserByIdQuery;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/application/user/queries/GetUserByIdQueryHandler.js.html b/Backend/negyedik gyakorlat/coverage/lcov-report/application/user/queries/GetUserByIdQueryHandler.js.html new file mode 100644 index 0000000..3cb8cfb --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/application/user/queries/GetUserByIdQueryHandler.js.html @@ -0,0 +1,190 @@ + + + + + + Code coverage report for application/user/queries/GetUserByIdQueryHandler.js + + + + + + + + + +
+
+

All files / application/user/queries GetUserByIdQueryHandler.js

+
+ +
+ 0% + Statements + 0/10 +
+ + +
+ 0% + Branches + 0/6 +
+ + +
+ 0% + Functions + 0/2 +
+ + +
+ 0% + Lines + 0/10 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
/**
+ * Get User By ID Query Handler
+ * Handles retrieval of a specific user by ID
+ */
+class GetUserByIdQueryHandler {
+  constructor(prisma) {
+    this.prisma = prisma;
+  }
+ 
+  /**
+   * Execute get user by ID query
+   * @param {GetUserByIdQuery} query 
+   * @returns {Promise<Object>} User data without password
+   */
+  async handle(query) {
+    const { userId } = query;
+ 
+    if (!userId || isNaN(userId)) {
+      throw new Error('Valid user ID is required');
+    }
+ 
+    const user = await this.prisma.user.findUnique({
+      where: { id: parseInt(userId) }
+    });
+ 
+    if (!user) {
+      throw new Error('User not found');
+    }
+ 
+    const { password, ...userWithoutPassword } = user;
+    return userWithoutPassword;
+  }
+}
+ 
+module.exports = GetUserByIdQueryHandler;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/application/user/queries/index.html b/Backend/negyedik gyakorlat/coverage/lcov-report/application/user/queries/index.html new file mode 100644 index 0000000..ec25582 --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/application/user/queries/index.html @@ -0,0 +1,191 @@ + + + + + + Code coverage report for application/user/queries + + + + + + + + + +
+
+

All files application/user/queries

+
+ +
+ 0% + Statements + 0/28 +
+ + +
+ 0% + Branches + 0/8 +
+ + +
+ 0% + Functions + 0/10 +
+ + +
+ 0% + Lines + 0/27 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
GetAllUsersQuery.js +
+
0%0/1100%0/00%0/10%0/1
GetAllUsersQueryHandler.js +
+
0%0/5100%0/00%0/30%0/4
GetMeQuery.js +
+
0%0/2100%0/00%0/10%0/2
GetMeQueryHandler.js +
+
0%0/80%0/20%0/20%0/8
GetUserByIdQuery.js +
+
0%0/2100%0/00%0/10%0/2
GetUserByIdQueryHandler.js +
+
0%0/100%0/60%0/20%0/10
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/base.css b/Backend/negyedik gyakorlat/coverage/lcov-report/base.css new file mode 100644 index 0000000..f418035 --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/base.css @@ -0,0 +1,224 @@ +body, html { + margin:0; padding: 0; + height: 100%; +} +body { + font-family: Helvetica Neue, Helvetica, Arial; + font-size: 14px; + color:#333; +} +.small { font-size: 12px; } +*, *:after, *:before { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + } +h1 { font-size: 20px; margin: 0;} +h2 { font-size: 14px; } +pre { + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { color:#0074D9; text-decoration:none; } +a:hover { text-decoration:underline; } +.strong { font-weight: bold; } +.space-top1 { padding: 10px 0 0 0; } +.pad2y { padding: 20px 0; } +.pad1y { padding: 10px 0; } +.pad2x { padding: 0 20px; } +.pad2 { padding: 20px; } +.pad1 { padding: 10px; } +.space-left2 { padding-left:55px; } +.space-right2 { padding-right:20px; } +.center { text-align:center; } +.clearfix { display:block; } +.clearfix:after { + content:''; + display:block; + height:0; + clear:both; + visibility:hidden; + } +.fl { float: left; } +@media only screen and (max-width:640px) { + .col3 { width:100%; max-width:100%; } + .hide-mobile { display:none!important; } +} + +.quiet { + color: #7f7f7f; + color: rgba(0,0,0,0.5); +} +.quiet a { opacity: 0.7; } + +.fraction { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #E8E8E8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, div.path a:visited { color: #333; } +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width:20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, .skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { border-bottom: 1px solid #bbb; } +.keyline-all { border: 1px solid #ddd; } +.coverage-summary td, .coverage-summary th { padding: 10px; } +.coverage-summary tbody { border: 1px solid #bbb; } +.coverage-summary td { border-right: 1px solid #bbb; } +.coverage-summary td:last-child { border-right: none; } +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { border-right: none !important; } +.coverage-summary th.pct { } +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { text-align: right; } +.coverage-summary td.file { white-space: nowrap; } +.coverage-summary td.pic { min-width: 120px !important; } +.coverage-summary tfoot td { } + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { height: 10px; } +/* yellow */ +.cbranch-no { background: yellow !important; color: #111; } +/* dark red */ +.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } +.low .chart { border:1px solid #C21F39 } +.highlighted, +.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ + background: #C21F39 !important; +} +/* medium red */ +.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +/* light red */ +.low, .cline-no { background:#FCE1E5 } +/* light green */ +.high, .cline-yes { background:rgb(230,245,208) } +/* medium green */ +.cstat-yes { background:rgb(161,215,106) } +/* dark green */ +.status-line.high, .high .cover-fill { background:rgb(77,146,33) } +.high .chart { border:1px solid rgb(77,146,33) } +/* dark yellow (gold) */ +.status-line.medium, .medium .cover-fill { background: #f9cd0b; } +.medium .chart { border:1px solid #f9cd0b; } +/* light yellow */ +.medium { background: #fff4c2; } + +.cstat-skip { background: #ddd; color: #111; } +.fstat-skip { background: #ddd; color: #111 !important; } +.cbranch-skip { background: #ddd !important; color: #111; } + +span.cline-neutral { background: #eaeaea; } + +.coverage-summary td.empty { + opacity: .5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, .cover-empty { + display:inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { color: #999 !important; } +.ignore-none { color: #999; font-weight: normal; } + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, .push { + height: 48px; +} diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/block-navigation.js b/Backend/negyedik gyakorlat/coverage/lcov-report/block-navigation.js new file mode 100644 index 0000000..530d1ed --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/block-navigation.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +var jumpToCode = (function init() { + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; + + // Elements to highlight in the file listing view + var fileListingElements = ['td.pct.low']; + + // We don't want to select elements that are direct descendants of another match + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` + + // Selector that finds elements on the page to which we can jump + var selector = + fileListingElements.join(', ') + + ', ' + + notSelector + + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); + + var currentIndex; + + function toggleClass(index) { + missingCoverageElements + .item(currentIndex) + .classList.remove('highlighted'); + missingCoverageElements.item(index).classList.add('highlighted'); + } + + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + } + + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== 'number' || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; + } + + makeCurrent(nextIndex); + } + + function goToNext() { + var nextIndex = 0; + + if ( + typeof currentIndex === 'number' && + currentIndex < missingCoverageElements.length - 1 + ) { + nextIndex = currentIndex + 1; + } + + makeCurrent(nextIndex); + } + + return function jump(event) { + if ( + document.getElementById('fileSearch') === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; +})(); +window.addEventListener('keydown', jumpToCode); diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/domain/irepositories/IUserRepository.js.html b/Backend/negyedik gyakorlat/coverage/lcov-report/domain/irepositories/IUserRepository.js.html new file mode 100644 index 0000000..6e2aff6 --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/domain/irepositories/IUserRepository.js.html @@ -0,0 +1,265 @@ + + + + + + Code coverage report for domain/irepositories/IUserRepository.js + + + + + + + + + +
+
+

All files / domain/irepositories IUserRepository.js

+
+ +
+ 0% + Statements + 0/7 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/6 +
+ + +
+ 0% + Lines + 0/7 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
/**
+ * User Repository Interface
+ * Defines contract for user data access
+ */
+class IUserRepository {
+  /**
+   * Find user by ID
+   * @param {number} id 
+   * @returns {Promise<User|null>}
+   */
+  async findById(id) {
+    throw new Error('Method findById() must be implemented');
+  }
+ 
+  /**
+   * Find user by email
+   * @param {string} email 
+   * @returns {Promise<User|null>}
+   */
+  async findByEmail(email) {
+    throw new Error('Method findByEmail() must be implemented');
+  }
+ 
+  /**
+   * Find all users
+   * @returns {Promise<User[]>}
+   */
+  async findAll() {
+    throw new Error('Method findAll() must be implemented');
+  }
+ 
+  /**
+   * Create new user
+   * @param {User} user 
+   * @returns {Promise<User>}
+   */
+  async create(user) {
+    throw new Error('Method create() must be implemented');
+  }
+ 
+  /**
+   * Update existing user
+   * @param {User} user 
+   * @returns {Promise<User>}
+   */
+  async update(user) {
+    throw new Error('Method update() must be implemented');
+  }
+ 
+  /**
+   * Delete user by ID
+   * @param {number} id 
+   * @returns {Promise<boolean>}
+   */
+  async delete(id) {
+    throw new Error('Method delete() must be implemented');
+  }
+}
+ 
+module.exports = IUserRepository;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/domain/irepositories/index.html b/Backend/negyedik gyakorlat/coverage/lcov-report/domain/irepositories/index.html new file mode 100644 index 0000000..c9c56e6 --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/domain/irepositories/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for domain/irepositories + + + + + + + + + +
+
+

All files domain/irepositories

+
+ +
+ 0% + Statements + 0/7 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/6 +
+ + +
+ 0% + Lines + 0/7 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
IUserRepository.js +
+
0%0/7100%0/00%0/60%0/7
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/domain/models/User.js.html b/Backend/negyedik gyakorlat/coverage/lcov-report/domain/models/User.js.html new file mode 100644 index 0000000..1b5f5f9 --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/domain/models/User.js.html @@ -0,0 +1,313 @@ + + + + + + Code coverage report for domain/models/User.js + + + + + + + + + +
+
+

All files / domain/models User.js

+
+ +
+ 0% + Statements + 0/22 +
+ + +
+ 0% + Branches + 0/16 +
+ + +
+ 0% + Functions + 0/5 +
+ + +
+ 0% + Lines + 0/22 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
/**
+ * User Domain Model
+ * Pure domain entity with business logic validation
+ */
+class User {
+  constructor(id, name, email, password, createdAt, updatedAt) {
+    this.id = id;
+    this.name = name;
+    this.email = email;
+    this.password = password;
+    this.createdAt = createdAt;
+    this.updatedAt = updatedAt;
+  }
+ 
+  /**
+   * Factory method to create a new User
+   * @param {Object} data - { name, email, password }
+   * @returns {User}
+   */
+  static create(data) {
+    const { name, email, password } = data;
+ 
+    // Validation
+    if (!name || name.trim().length === 0) {
+      throw new Error('User name is required');
+    }
+ 
+    if (!email || !User.isValidEmail(email)) {
+      throw new Error('Valid email is required');
+    }
+ 
+    if (!password || password.length < 6) {
+      throw new Error('Password must be at least 6 characters long');
+    }
+ 
+    return new User(null, name.trim(), email.toLowerCase(), password, new Date(), new Date());
+  }
+ 
+  /**
+   * Email validation
+   * @param {string} email 
+   * @returns {boolean}
+   */
+  static isValidEmail(email) {
+    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+    return emailRegex.test(email);
+  }
+ 
+  /**
+   * Update user name
+   * @param {string} newName 
+   */
+  updateName(newName) {
+    if (!newName || newName.trim().length === 0) {
+      throw new Error('User name is required');
+    }
+    this.name = newName.trim();
+    this.updatedAt = new Date();
+  }
+ 
+  /**
+   * Get user without sensitive data
+   * @returns {Object}
+   */
+  toPublicJSON() {
+    return {
+      id: this.id,
+      name: this.name,
+      email: this.email,
+      createdAt: this.createdAt,
+      updatedAt: this.updatedAt
+    };
+  }
+}
+ 
+module.exports = User;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/domain/models/index.html b/Backend/negyedik gyakorlat/coverage/lcov-report/domain/models/index.html new file mode 100644 index 0000000..8d53e51 --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/domain/models/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for domain/models + + + + + + + + + +
+
+

All files domain/models

+
+ +
+ 0% + Statements + 0/22 +
+ + +
+ 0% + Branches + 0/16 +
+ + +
+ 0% + Functions + 0/5 +
+ + +
+ 0% + Lines + 0/22 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
User.js +
+
0%0/220%0/160%0/50%0/22
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/favicon.png b/Backend/negyedik gyakorlat/coverage/lcov-report/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..c1525b811a167671e9de1fa78aab9f5c0b61cef7 GIT binary patch literal 445 zcmV;u0Yd(XP))rP{nL}Ln%S7`m{0DjX9TLF* zFCb$4Oi7vyLOydb!7n&^ItCzb-%BoB`=x@N2jll2Nj`kauio%aw_@fe&*}LqlFT43 z8doAAe))z_%=P%v^@JHp3Hjhj^6*Kr_h|g_Gr?ZAa&y>wxHE99Gk>A)2MplWz2xdG zy8VD2J|Uf#EAw*bo5O*PO_}X2Tob{%bUoO2G~T`@%S6qPyc}VkhV}UifBuRk>%5v( z)x7B{I~z*k<7dv#5tC+m{km(D087J4O%+<<;K|qwefb6@GSX45wCK}Sn*> + + + + Code coverage report for All files + + + + + + + + + +
+
+

All files

+
+ +
+ 0.32% + Statements + 1/312 +
+ + +
+ 1.94% + Branches + 2/103 +
+ + +
+ 4.81% + Functions + 4/83 +
+ + +
+ 0.33% + Lines + 1/301 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
api/controllers +
+
0%0/650%0/180%0/90%0/65
api/middlewares +
+
0%0/170%0/40%0/40%0/17
api/routers +
+
0%0/29100%0/00%0/110%0/20
application/auth/commands +
+
0%0/490%0/210%0/70%0/49
application/services +
+
2.94%1/349.09%2/2228.57%4/142.94%1/34
application/user/commands +
+
0%0/110%0/20%0/30%0/11
application/user/queries +
+
0%0/280%0/80%0/100%0/27
domain/irepositories +
+
0%0/7100%0/00%0/60%0/7
domain/models +
+
0%0/220%0/160%0/50%0/22
infrastructure/db +
+
0%0/250%0/80%0/50%0/25
infrastructure/repositories +
+
0%0/250%0/40%0/90%0/24
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/infrastructure/db/DatabaseConnection.js.html b/Backend/negyedik gyakorlat/coverage/lcov-report/infrastructure/db/DatabaseConnection.js.html new file mode 100644 index 0000000..e4127c9 --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/infrastructure/db/DatabaseConnection.js.html @@ -0,0 +1,307 @@ + + + + + + Code coverage report for infrastructure/db/DatabaseConnection.js + + + + + + + + + +
+
+

All files / infrastructure/db DatabaseConnection.js

+
+ +
+ 0% + Statements + 0/25 +
+ + +
+ 0% + Branches + 0/8 +
+ + +
+ 0% + Functions + 0/5 +
+ + +
+ 0% + Lines + 0/25 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
const { PrismaClient } = require('@prisma/client');
+ 
+/**
+ * Database Connection Wrapper
+ * Manages Prisma Client lifecycle
+ */
+class DatabaseConnection {
+  constructor() {
+    this.prisma = null;
+  }
+ 
+  /**
+   * Initialize Prisma Client
+   */
+  async connect() {
+    if (this.prisma) {
+      console.log('⚠️  Prisma Client already connected');
+      return;
+    }
+ 
+    try {
+      this.prisma = new PrismaClient({
+        log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
+      });
+ 
+      await this.prisma.$connect();
+      console.log('✅ Prisma connected to PostgreSQL');
+    } catch (error) {
+      console.error('❌ Failed to connect to database:', error);
+      throw error;
+    }
+  }
+ 
+  /**
+   * Get Prisma Client instance
+   * @returns {PrismaClient}
+   */
+  getClient() {
+    if (!this.prisma) {
+      throw new Error('Database not connected. Call connect() first.');
+    }
+    return this.prisma;
+  }
+ 
+  /**
+   * Close database connection
+   */
+  async disconnect() {
+    if (this.prisma) {
+      await this.prisma.$disconnect();
+      console.log('🛑 Prisma disconnected');
+      this.prisma = null;
+    }
+  }
+ 
+  /**
+   * Health check
+   * @returns {Promise<boolean>}
+   */
+  async healthCheck() {
+    try {
+      await this.prisma.$queryRaw`SELECT 1`;
+      return true;
+    } catch (error) {
+      console.error('❌ Database health check failed:', error);
+      return false;
+    }
+  }
+}
+ 
+// Singleton instance
+const databaseConnection = new DatabaseConnection();
+ 
+module.exports = databaseConnection;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/infrastructure/db/index.html b/Backend/negyedik gyakorlat/coverage/lcov-report/infrastructure/db/index.html new file mode 100644 index 0000000..b0f53bb --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/infrastructure/db/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for infrastructure/db + + + + + + + + + +
+
+

All files infrastructure/db

+
+ +
+ 0% + Statements + 0/25 +
+ + +
+ 0% + Branches + 0/8 +
+ + +
+ 0% + Functions + 0/5 +
+ + +
+ 0% + Lines + 0/25 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
DatabaseConnection.js +
+
0%0/250%0/80%0/50%0/25
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/infrastructure/repositories/UserRepository.js.html b/Backend/negyedik gyakorlat/coverage/lcov-report/infrastructure/repositories/UserRepository.js.html new file mode 100644 index 0000000..1cbf0b7 --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/infrastructure/repositories/UserRepository.js.html @@ -0,0 +1,475 @@ + + + + + + Code coverage report for infrastructure/repositories/UserRepository.js + + + + + + + + + +
+
+

All files / infrastructure/repositories UserRepository.js

+
+ +
+ 0% + Statements + 0/25 +
+ + +
+ 0% + Branches + 0/4 +
+ + +
+ 0% + Functions + 0/9 +
+ + +
+ 0% + Lines + 0/24 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
const IUserRepository = require('../../domain/irepositories/IUserRepository');
+const User = require('../../domain/models/User');
+ 
+/**
+ * User Repository Implementation
+ * Prisma-based data access for User entity
+ */
+class UserRepository extends IUserRepository {
+  constructor(prisma) {
+    super();
+    this.prisma = prisma;
+  }
+ 
+  /**
+   * Find user by ID
+   * @param {number} id 
+   * @returns {Promise<User|null>}
+   */
+  async findById(id) {
+    const userData = await this.prisma.user.findUnique({
+      where: { id: parseInt(id) }
+    });
+ 
+    if (!userData) {
+      return null;
+    }
+ 
+    return this._toDomain(userData);
+  }
+ 
+  /**
+   * Find user by email
+   * @param {string} email 
+   * @returns {Promise<User|null>}
+   */
+  async findByEmail(email) {
+    const userData = await this.prisma.user.findUnique({
+      where: { email: email.toLowerCase() }
+    });
+ 
+    if (!userData) {
+      return null;
+    }
+ 
+    return this._toDomain(userData);
+  }
+ 
+  /**
+   * Find all users
+   * @returns {Promise<User[]>}
+   */
+  async findAll() {
+    const usersData = await this.prisma.user.findMany({
+      orderBy: { createdAt: 'desc' }
+    });
+ 
+    return usersData.map(userData => this._toDomain(userData));
+  }
+ 
+  /**
+   * Create new user
+   * @param {User} user 
+   * @returns {Promise<User>}
+   */
+  async create(user) {
+    const userData = await this.prisma.user.create({
+      data: {
+        name: user.name,
+        email: user.email,
+        password: user.password
+      }
+    });
+ 
+    return this._toDomain(userData);
+  }
+ 
+  /**
+   * Update existing user
+   * @param {User} user 
+   * @returns {Promise<User>}
+   */
+  async update(user) {
+    const userData = await this.prisma.user.update({
+      where: { id: user.id },
+      data: {
+        name: user.name,
+        email: user.email,
+        password: user.password,
+        updatedAt: new Date()
+      }
+    });
+ 
+    return this._toDomain(userData);
+  }
+ 
+  /**
+   * Delete user by ID
+   * @param {number} id 
+   * @returns {Promise<boolean>}
+   */
+  async delete(id) {
+    try {
+      await this.prisma.user.delete({
+        where: { id: parseInt(id) }
+      });
+      return true;
+    } catch (error) {
+      return false;
+    }
+  }
+ 
+  /**
+   * Convert Prisma data to Domain model
+   * @private
+   * @param {Object} userData - Prisma user data
+   * @returns {User}
+   */
+  _toDomain(userData) {
+    return new User(
+      userData.id,
+      userData.name,
+      userData.email,
+      userData.password,
+      userData.createdAt,
+      userData.updatedAt
+    );
+  }
+}
+ 
+module.exports = UserRepository;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/infrastructure/repositories/index.html b/Backend/negyedik gyakorlat/coverage/lcov-report/infrastructure/repositories/index.html new file mode 100644 index 0000000..596286e --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/infrastructure/repositories/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for infrastructure/repositories + + + + + + + + + +
+
+

All files infrastructure/repositories

+
+ +
+ 0% + Statements + 0/25 +
+ + +
+ 0% + Branches + 0/4 +
+ + +
+ 0% + Functions + 0/9 +
+ + +
+ 0% + Lines + 0/24 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
UserRepository.js +
+
0%0/250%0/40%0/90%0/24
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/prettify.css b/Backend/negyedik gyakorlat/coverage/lcov-report/prettify.css new file mode 100644 index 0000000..b317a7c --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/prettify.css @@ -0,0 +1 @@ +.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/prettify.js b/Backend/negyedik gyakorlat/coverage/lcov-report/prettify.js new file mode 100644 index 0000000..b322523 --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/prettify.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/sort-arrow-sprite.png b/Backend/negyedik gyakorlat/coverage/lcov-report/sort-arrow-sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed68316eb3f65dec9063332d2f69bf3093bbfab GIT binary patch literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^>_9Bd!3HEZxJ@+%Qh}Z>jv*C{$p!i!8j}?a+@3A= zIAGwzjijN=FBi!|L1t?LM;Q;gkwn>2cAy-KV{dn nf0J1DIvEHQu*n~6U}x}qyky7vi4|9XhBJ7&`njxgN@xNA8m%nc literal 0 HcmV?d00001 diff --git a/Backend/negyedik gyakorlat/coverage/lcov-report/sorter.js b/Backend/negyedik gyakorlat/coverage/lcov-report/sorter.js new file mode 100644 index 0000000..4ed70ae --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov-report/sorter.js @@ -0,0 +1,210 @@ +/* eslint-disable */ +var addSorting = (function() { + 'use strict'; + var cols, + currentSort = { + index: 0, + desc: false + }; + + // returns the summary table element + function getTable() { + return document.querySelector('.coverage-summary'); + } + // returns the thead element of the summary table + function getTableHeader() { + return getTable().querySelector('thead tr'); + } + // returns the tbody element of the summary table + function getTableBody() { + return getTable().querySelector('tbody'); + } + // returns the th element for nth column + function getNthColumn(n) { + return getTableHeader().querySelectorAll('th')[n]; + } + + function onFilterInput() { + const searchValue = document.getElementById('fileSearch').value; + const rows = document.getElementsByTagName('tbody')[0].children; + + // Try to create a RegExp from the searchValue. If it fails (invalid regex), + // it will be treated as a plain text search + let searchRegex; + try { + searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive + } catch (error) { + searchRegex = null; + } + + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + let isMatch = false; + + if (searchRegex) { + // If a valid regex was created, use it for matching + isMatch = searchRegex.test(row.textContent); + } else { + // Otherwise, fall back to the original plain text search + isMatch = row.textContent + .toLowerCase() + .includes(searchValue.toLowerCase()); + } + + row.style.display = isMatch ? '' : 'none'; + } + } + + // loads the search box + function addSearchBox() { + var template = document.getElementById('filterTemplate'); + var templateClone = template.content.cloneNode(true); + templateClone.getElementById('fileSearch').oninput = onFilterInput; + template.parentElement.appendChild(templateClone); + } + + // loads all columns + function loadColumns() { + var colNodes = getTableHeader().querySelectorAll('th'), + colNode, + cols = [], + col, + i; + + for (i = 0; i < colNodes.length; i += 1) { + colNode = colNodes[i]; + col = { + key: colNode.getAttribute('data-col'), + sortable: !colNode.getAttribute('data-nosort'), + type: colNode.getAttribute('data-type') || 'string' + }; + cols.push(col); + if (col.sortable) { + col.defaultDescSort = col.type === 'number'; + colNode.innerHTML = + colNode.innerHTML + ''; + } + } + return cols; + } + // attaches a data attribute to every tr element with an object + // of data values keyed by column name + function loadRowData(tableRow) { + var tableCols = tableRow.querySelectorAll('td'), + colNode, + col, + data = {}, + i, + val; + for (i = 0; i < tableCols.length; i += 1) { + colNode = tableCols[i]; + col = cols[i]; + val = colNode.getAttribute('data-value'); + if (col.type === 'number') { + val = Number(val); + } + data[col.key] = val; + } + return data; + } + // loads all row data + function loadData() { + var rows = getTableBody().querySelectorAll('tr'), + i; + + for (i = 0; i < rows.length; i += 1) { + rows[i].data = loadRowData(rows[i]); + } + } + // sorts the table using the data for the ith column + function sortByIndex(index, desc) { + var key = cols[index].key, + sorter = function(a, b) { + a = a.data[key]; + b = b.data[key]; + return a < b ? -1 : a > b ? 1 : 0; + }, + finalSorter = sorter, + tableBody = document.querySelector('.coverage-summary tbody'), + rowNodes = tableBody.querySelectorAll('tr'), + rows = [], + i; + + if (desc) { + finalSorter = function(a, b) { + return -1 * sorter(a, b); + }; + } + + for (i = 0; i < rowNodes.length; i += 1) { + rows.push(rowNodes[i]); + tableBody.removeChild(rowNodes[i]); + } + + rows.sort(finalSorter); + + for (i = 0; i < rows.length; i += 1) { + tableBody.appendChild(rows[i]); + } + } + // removes sort indicators for current column being sorted + function removeSortIndicators() { + var col = getNthColumn(currentSort.index), + cls = col.className; + + cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); + col.className = cls; + } + // adds sort indicators for current column being sorted + function addSortIndicators() { + getNthColumn(currentSort.index).className += currentSort.desc + ? ' sorted-desc' + : ' sorted'; + } + // adds event listeners for all sorter widgets + function enableUI() { + var i, + el, + ithSorter = function ithSorter(i) { + var col = cols[i]; + + return function() { + var desc = col.defaultDescSort; + + if (currentSort.index === i) { + desc = !currentSort.desc; + } + sortByIndex(i, desc); + removeSortIndicators(); + currentSort.index = i; + currentSort.desc = desc; + addSortIndicators(); + }; + }; + for (i = 0; i < cols.length; i += 1) { + if (cols[i].sortable) { + // add the click event handler on the th so users + // dont have to click on those tiny arrows + el = getNthColumn(i).querySelector('.sorter').parentElement; + if (el.addEventListener) { + el.addEventListener('click', ithSorter(i)); + } else { + el.attachEvent('onclick', ithSorter(i)); + } + } + } + } + // adds sorting functionality to the UI + return function() { + if (!getTable()) { + return; + } + cols = loadColumns(); + loadData(); + addSearchBox(); + addSortIndicators(); + enableUI(); + }; +})(); + +window.addEventListener('load', addSorting); diff --git a/Backend/negyedik gyakorlat/coverage/lcov.info b/Backend/negyedik gyakorlat/coverage/lcov.info new file mode 100644 index 0000000..f11d276 --- /dev/null +++ b/Backend/negyedik gyakorlat/coverage/lcov.info @@ -0,0 +1,804 @@ +TN: +SF:src\api\controllers\AuthController.js +FN:9,(anonymous_0) +FN:18,(anonymous_1) +FN:52,(anonymous_2) +FN:84,(anonymous_3) +FNF:4 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +DA:1,0 +DA:2,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:19,0 +DA:20,0 +DA:22,0 +DA:23,0 +DA:26,0 +DA:32,0 +DA:40,0 +DA:45,0 +DA:53,0 +DA:54,0 +DA:56,0 +DA:57,0 +DA:60,0 +DA:66,0 +DA:74,0 +DA:77,0 +DA:85,0 +DA:87,0 +DA:94,0 +DA:98,0 +DA:103,0 +LF:26 +LH:0 +BRDA:40,0,0,0 +BRDA:40,0,1,0 +BRDA:40,1,0,0 +BRDA:40,1,1,0 +BRDA:40,1,2,0 +BRDA:40,1,3,0 +BRDA:74,2,0,0 +BRDA:74,2,1,0 +BRDA:74,3,0,0 +BRDA:74,3,1,0 +BRF:10 +BRH:0 +end_of_record +TN: +SF:src\api\controllers\UserController.js +FN:11,(anonymous_0) +FN:21,(anonymous_1) +FN:42,(anonymous_2) +FN:60,(anonymous_3) +FN:85,(anonymous_4) +FNF:5 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +DA:1,0 +DA:2,0 +DA:3,0 +DA:4,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:22,0 +DA:24,0 +DA:26,0 +DA:27,0 +DA:29,0 +DA:34,0 +DA:35,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:47,0 +DA:53,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:65,0 +DA:66,0 +DA:69,0 +DA:70,0 +DA:72,0 +DA:77,0 +DA:78,0 +DA:86,0 +DA:87,0 +DA:88,0 +DA:90,0 +DA:91,0 +DA:93,0 +DA:98,0 +DA:99,0 +DA:104,0 +LF:39 +LH:0 +BRDA:34,0,0,0 +BRDA:34,0,1,0 +BRDA:65,1,0,0 +BRDA:65,1,1,0 +BRDA:77,2,0,0 +BRDA:77,2,1,0 +BRDA:98,3,0,0 +BRDA:98,3,1,0 +BRF:8 +BRH:0 +end_of_record +TN: +SF:src\api\middlewares\authMiddleware.js +FN:13,authMiddleware +FNF:1 +FNH:0 +FNDA:0,authMiddleware +DA:1,0 +DA:3,0 +DA:14,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:26,0 +DA:29,0 +DA:35,0 +DA:37,0 +DA:44,0 +LF:11 +LH:0 +BRDA:18,0,0,0 +BRDA:18,0,1,0 +BRF:2 +BRH:0 +end_of_record +TN: +SF:src\api\middlewares\corsMiddleware.js +FN:7,(anonymous_0) +FNF:1 +FNH:0 +FNDA:0,(anonymous_0) +DA:1,0 +DA:4,0 +DA:6,0 +DA:22,0 +LF:4 +LH:0 +BRDA:4,0,0,0 +BRDA:4,0,1,0 +BRF:2 +BRH:0 +end_of_record +TN: +SF:src\api\middlewares\scopeMiddleware.js +FN:5,scopeMiddleware +FN:6,(anonymous_1) +FNF:2 +FNH:0 +FNDA:0,scopeMiddleware +FNDA:0,(anonymous_1) +DA:6,0 +DA:18,0 +LF:2 +LH:0 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src\api\routers\authRoutes.js +FN:9,createAuthRoutes +FN:20,(anonymous_1) +FN:27,(anonymous_2) +FN:33,(anonymous_3) +FN:38,(anonymous_4) +FNF:5 +FNH:0 +FNDA:0,createAuthRoutes +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +DA:1,0 +DA:10,0 +DA:13,0 +DA:20,0 +DA:27,0 +DA:33,0 +DA:38,0 +DA:40,0 +DA:43,0 +LF:9 +LH:0 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src\api\routers\userRoutes.js +FN:10,createUserRoutes +FN:20,(anonymous_1) +FN:27,(anonymous_2) +FN:33,(anonymous_3) +FN:39,(anonymous_4) +FN:44,(anonymous_5) +FNF:6 +FNH:0 +FNDA:0,createUserRoutes +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:0,(anonymous_5) +DA:1,0 +DA:2,0 +DA:11,0 +DA:14,0 +DA:20,0 +DA:27,0 +DA:33,0 +DA:39,0 +DA:44,0 +DA:46,0 +DA:49,0 +LF:11 +LH:0 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src\application\auth\commands\LoginUserCommand.js +FN:6,(anonymous_0) +FNF:1 +FNH:0 +FNDA:0,(anonymous_0) +DA:7,0 +DA:8,0 +DA:12,0 +LF:3 +LH:0 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src\application\auth\commands\LoginUserCommandHandler.js +FN:11,(anonymous_0) +FN:20,(anonymous_1) +FNF:2 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +DA:1,0 +DA:2,0 +DA:4,0 +DA:12,0 +DA:21,0 +DA:24,0 +DA:25,0 +DA:29,0 +DA:33,0 +DA:34,0 +DA:38,0 +DA:40,0 +DA:41,0 +DA:45,0 +DA:51,0 +DA:53,0 +DA:60,0 +LF:17 +LH:0 +BRDA:24,0,0,0 +BRDA:24,0,1,0 +BRDA:24,1,0,0 +BRDA:24,1,1,0 +BRDA:33,2,0,0 +BRDA:33,2,1,0 +BRDA:40,3,0,0 +BRDA:40,3,1,0 +BRF:8 +BRH:0 +end_of_record +TN: +SF:src\application\auth\commands\RegisterUserCommand.js +FN:6,(anonymous_0) +FNF:1 +FNH:0 +FNDA:0,(anonymous_0) +DA:7,0 +DA:8,0 +DA:9,0 +DA:13,0 +LF:4 +LH:0 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src\application\auth\commands\RegisterUserCommandHandler.js +FN:11,(anonymous_0) +FN:21,(anonymous_1) +FN:62,(anonymous_2) +FNF:3 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +DA:1,0 +DA:2,0 +DA:4,0 +DA:12,0 +DA:13,0 +DA:22,0 +DA:25,0 +DA:26,0 +DA:29,0 +DA:30,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:40,0 +DA:44,0 +DA:45,0 +DA:49,0 +DA:52,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:68,0 +DA:74,0 +DA:76,0 +DA:83,0 +LF:25 +LH:0 +BRDA:25,0,0,0 +BRDA:25,0,1,0 +BRDA:25,1,0,0 +BRDA:25,1,1,0 +BRDA:25,1,2,0 +BRDA:29,2,0,0 +BRDA:29,2,1,0 +BRDA:35,3,0,0 +BRDA:35,3,1,0 +BRDA:44,4,0,0 +BRDA:44,4,1,0 +BRDA:61,5,0,0 +BRDA:61,5,1,0 +BRF:13 +BRH:0 +end_of_record +TN: +SF:src\application\services\Container.js +FN:6,(anonymous_0) +FN:17,(anonymous_1) +FN:25,(anonymous_2) +FN:47,(anonymous_3) +FNF:4 +FNH:4 +FNDA:22,(anonymous_0) +FNDA:22,(anonymous_1) +FNDA:19,(anonymous_2) +FNDA:9,(anonymous_3) +DA:57,1 +LF:1 +LH:1 +BRDA:17,0,0,1 +BRDA:25,1,0,17 +BRF:2 +BRH:2 +end_of_record +TN: +SF:src\application\services\EmailService.js +FN:8,(anonymous_0) +FN:23,(anonymous_1) +FNF:2 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +DA:20,0 +DA:26,0 +DA:27,0 +DA:31,0 +LF:4 +LH:0 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src\application\services\JwtService.js +FN:8,(anonymous_0) +FN:19,(anonymous_1) +FN:28,(anonymous_2) +FN:41,(anonymous_3) +FN:60,(anonymous_4) +FN:72,(anonymous_5) +FN:89,(anonymous_6) +FN:109,(anonymous_7) +FNF:8 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:0,(anonymous_5) +FNDA:0,(anonymous_6) +FNDA:0,(anonymous_7) +DA:1,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:20,0 +DA:29,0 +DA:30,0 +DA:32,0 +DA:42,0 +DA:43,0 +DA:46,0 +DA:48,0 +DA:49,0 +DA:52,0 +DA:61,0 +DA:62,0 +DA:65,0 +DA:73,0 +DA:75,0 +DA:91,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:102,0 +DA:110,0 +DA:114,0 +LF:29 +LH:0 +BRDA:9,0,0,0 +BRDA:9,0,1,0 +BRDA:10,1,0,0 +BRDA:10,1,1,0 +BRDA:42,2,0,0 +BRDA:42,2,1,0 +BRDA:48,3,0,0 +BRDA:48,3,1,0 +BRDA:48,4,0,0 +BRDA:48,4,1,0 +BRDA:61,5,0,0 +BRDA:61,5,1,0 +BRDA:61,6,0,0 +BRDA:61,6,1,0 +BRDA:93,7,0,0 +BRDA:93,7,1,0 +BRDA:95,8,0,0 +BRDA:95,8,1,0 +BRDA:97,9,0,0 +BRDA:97,9,1,0 +BRF:20 +BRH:0 +end_of_record +TN: +SF:src\application\user\commands\UpdateUserProfileCommand.js +FN:6,(anonymous_0) +FNF:1 +FNH:0 +FNDA:0,(anonymous_0) +DA:7,0 +DA:8,0 +DA:12,0 +LF:3 +LH:0 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src\application\user\commands\UpdateUserProfileCommandHandler.js +FN:6,(anonymous_0) +FN:15,(anonymous_1) +FNF:2 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +DA:7,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:22,0 +DA:27,0 +DA:28,0 +DA:32,0 +LF:8 +LH:0 +BRDA:18,0,0,0 +BRDA:18,0,1,0 +BRF:2 +BRH:0 +end_of_record +TN: +SF:src\application\user\queries\GetAllUsersQuery.js +FN:6,(anonymous_0) +FNF:1 +FNH:0 +FNDA:0,(anonymous_0) +DA:11,0 +LF:1 +LH:0 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src\application\user\queries\GetAllUsersQueryHandler.js +FN:6,(anonymous_0) +FN:15,(anonymous_1) +FN:21,(anonymous_2) +FNF:3 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +DA:7,0 +DA:16,0 +DA:21,0 +DA:25,0 +LF:4 +LH:0 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src\application\user\queries\GetMeQuery.js +FN:6,(anonymous_0) +FNF:1 +FNH:0 +FNDA:0,(anonymous_0) +DA:7,0 +DA:11,0 +LF:2 +LH:0 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src\application\user\queries\GetMeQueryHandler.js +FN:6,(anonymous_0) +FN:15,(anonymous_1) +FNF:2 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +DA:7,0 +DA:16,0 +DA:18,0 +DA:22,0 +DA:23,0 +DA:26,0 +DA:27,0 +DA:31,0 +LF:8 +LH:0 +BRDA:22,0,0,0 +BRDA:22,0,1,0 +BRF:2 +BRH:0 +end_of_record +TN: +SF:src\application\user\queries\GetUserByIdQuery.js +FN:6,(anonymous_0) +FNF:1 +FNH:0 +FNDA:0,(anonymous_0) +DA:7,0 +DA:11,0 +LF:2 +LH:0 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src\application\user\queries\GetUserByIdQueryHandler.js +FN:6,(anonymous_0) +FN:15,(anonymous_1) +FNF:2 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +DA:7,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:22,0 +DA:26,0 +DA:27,0 +DA:30,0 +DA:31,0 +DA:35,0 +LF:10 +LH:0 +BRDA:18,0,0,0 +BRDA:18,0,1,0 +BRDA:18,1,0,0 +BRDA:18,1,1,0 +BRDA:26,2,0,0 +BRDA:26,2,1,0 +BRF:6 +BRH:0 +end_of_record +TN: +SF:src\domain\irepositories\IUserRepository.js +FN:11,(anonymous_0) +FN:20,(anonymous_1) +FN:28,(anonymous_2) +FN:37,(anonymous_3) +FN:46,(anonymous_4) +FN:55,(anonymous_5) +FNF:6 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:0,(anonymous_5) +DA:12,0 +DA:21,0 +DA:29,0 +DA:38,0 +DA:47,0 +DA:56,0 +DA:60,0 +LF:7 +LH:0 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src\domain\models\User.js +FN:6,(anonymous_0) +FN:20,(anonymous_1) +FN:44,(anonymous_2) +FN:53,(anonymous_3) +FN:65,(anonymous_4) +FNF:5 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +DA:7,0 +DA:8,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:21,0 +DA:24,0 +DA:25,0 +DA:28,0 +DA:29,0 +DA:32,0 +DA:33,0 +DA:36,0 +DA:45,0 +DA:46,0 +DA:54,0 +DA:55,0 +DA:57,0 +DA:58,0 +DA:66,0 +DA:76,0 +LF:22 +LH:0 +BRDA:24,0,0,0 +BRDA:24,0,1,0 +BRDA:24,1,0,0 +BRDA:24,1,1,0 +BRDA:28,2,0,0 +BRDA:28,2,1,0 +BRDA:28,3,0,0 +BRDA:28,3,1,0 +BRDA:32,4,0,0 +BRDA:32,4,1,0 +BRDA:32,5,0,0 +BRDA:32,5,1,0 +BRDA:54,6,0,0 +BRDA:54,6,1,0 +BRDA:54,7,0,0 +BRDA:54,7,1,0 +BRF:16 +BRH:0 +end_of_record +TN: +SF:src\infrastructure\db\DatabaseConnection.js +FN:8,(anonymous_0) +FN:15,(anonymous_1) +FN:38,(anonymous_2) +FN:48,(anonymous_3) +FN:60,(anonymous_4) +FNF:5 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +DA:1,0 +DA:9,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:21,0 +DA:22,0 +DA:26,0 +DA:27,0 +DA:29,0 +DA:30,0 +DA:39,0 +DA:40,0 +DA:42,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:65,0 +DA:66,0 +DA:72,0 +DA:74,0 +LF:25 +LH:0 +BRDA:16,0,0,0 +BRDA:16,0,1,0 +BRDA:23,1,0,0 +BRDA:23,1,1,0 +BRDA:39,2,0,0 +BRDA:39,2,1,0 +BRDA:49,3,0,0 +BRDA:49,3,1,0 +BRF:8 +BRH:0 +end_of_record +TN: +SF:src\infrastructure\repositories\UserRepository.js +FN:9,(anonymous_0) +FN:19,(anonymous_1) +FN:36,(anonymous_2) +FN:52,(anonymous_3) +FN:57,(anonymous_4) +FN:65,(anonymous_5) +FN:82,(anonymous_6) +FN:101,(anonymous_7) +FN:118,(anonymous_8) +FNF:9 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:0,(anonymous_5) +FNDA:0,(anonymous_6) +FNDA:0,(anonymous_7) +FNDA:0,(anonymous_8) +DA:1,0 +DA:2,0 +DA:10,0 +DA:11,0 +DA:20,0 +DA:24,0 +DA:25,0 +DA:28,0 +DA:37,0 +DA:41,0 +DA:42,0 +DA:45,0 +DA:53,0 +DA:57,0 +DA:66,0 +DA:74,0 +DA:83,0 +DA:93,0 +DA:102,0 +DA:103,0 +DA:106,0 +DA:108,0 +DA:119,0 +DA:130,0 +LF:24 +LH:0 +BRDA:24,0,0,0 +BRDA:24,0,1,0 +BRDA:41,1,0,0 +BRDA:41,1,1,0 +BRF:4 +BRH:0 +end_of_record diff --git a/Backend/negyedik gyakorlat/docker-compose.yml b/Backend/negyedik gyakorlat/docker-compose.yml new file mode 100644 index 0000000..0fdac75 --- /dev/null +++ b/Backend/negyedik gyakorlat/docker-compose.yml @@ -0,0 +1,43 @@ +version: '3.8' + +services: + # PostgreSQL Database + postgres: + image: postgres:15-alpine + container_name: cors-di-email-postgres + restart: unless-stopped + environment: + POSTGRES_USER: ${DB_USER:-postgres} + POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres} + POSTGRES_DB: ${DB_NAME:-cors_di_app} + ports: + - "${DB_PORT:-5432}:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./infrastructure/db/init.sql:/docker-entrypoint-initdb.d/init.sql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 + + # Node.js Application (opcionális - ha konténerben is futtatod) + # app: + # build: . + # container_name: cors-di-email-app + # restart: unless-stopped + # ports: + # - "${PORT:-3000}:3000" + # environment: + # - NODE_ENV=development + # - DB_HOST=postgres + # depends_on: + # postgres: + # condition: service_healthy + # volumes: + # - ./src:/app/src + # command: npm run dev + +volumes: + postgres_data: + driver: local diff --git a/Backend/negyedik gyakorlat/jest.config.js b/Backend/negyedik gyakorlat/jest.config.js new file mode 100644 index 0000000..e877d9e --- /dev/null +++ b/Backend/negyedik gyakorlat/jest.config.js @@ -0,0 +1,16 @@ +module.exports = { + testEnvironment: 'node', + coverageDirectory: 'coverage', + collectCoverageFrom: [ + 'src/**/*.js', + '!src/api/server.js', + '!src/**/index.js' + ], + testMatch: [ + '**/tests/**/*.test.js' + ], + verbose: true, + clearMocks: true, + resetMocks: true, + restoreMocks: true +}; diff --git a/Backend/negyedik gyakorlat/package.json b/Backend/negyedik gyakorlat/package.json new file mode 100644 index 0000000..8463a95 --- /dev/null +++ b/Backend/negyedik gyakorlat/package.json @@ -0,0 +1,39 @@ +{ + "name": "cors-di-email-practice", + "version": "1.0.0", + "description": "CORS + Dependency Injection + Email Notification Practice Project", + "main": "src/api/server.js", + "scripts": { + "dev": "nodemon src/api/server.js", + "start": "node src/api/server.js", + "docker:up": "docker-compose up -d", + "docker:down": "docker-compose down", + "docker:logs": "docker-compose logs -f postgres", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev", + "prisma:studio": "prisma studio", + "test": "jest --coverage", + "test:watch": "jest --watch", + "test:unit": "jest tests/unit" + }, + "keywords": ["express", "cors", "di", "dependency-injection", "email", "nodemailer", "prisma", "jwt", "authentication"], + "author": "", + "license": "ISC", + "dependencies": { + "express": "^4.18.2", + "cors": "^2.8.5", + "cookie-parser": "^1.4.6", + "@prisma/client": "^5.7.1", + "bcryptjs": "^2.4.3", + "jsonwebtoken": "^9.0.2", + "nodemailer": "^6.9.7", + "handlebars": "^4.7.8", + "dotenv": "^16.3.1", + "prisma": "^5.7.1" + }, + "devDependencies": { + "nodemon": "^3.0.2", + "jest": "^29.7.0", + "@types/jest": "^29.5.8" + } +} diff --git a/Backend/negyedik gyakorlat/prisma/schema.prisma b/Backend/negyedik gyakorlat/prisma/schema.prisma new file mode 100644 index 0000000..7fe56ff --- /dev/null +++ b/Backend/negyedik gyakorlat/prisma/schema.prisma @@ -0,0 +1,21 @@ +// Prisma Schema - PostgreSQL Database +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +// User Model +model User { + id Int @id @default(autoincrement()) + name String + email String @unique + password String // bcrypt hashed password + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("users") +} diff --git a/Backend/negyedik gyakorlat/src/api/controllers/AuthController.js b/Backend/negyedik gyakorlat/src/api/controllers/AuthController.js new file mode 100644 index 0000000..4398e60 --- /dev/null +++ b/Backend/negyedik gyakorlat/src/api/controllers/AuthController.js @@ -0,0 +1,103 @@ +const RegisterUserCommand = require('../../application/auth/commands/RegisterUserCommand'); +const LoginUserCommand = require('../../application/auth/commands/LoginUserCommand'); + +/** + * Auth Controller + * Authentication endpoints using CQRS Commands with cookie-based JWT + */ +class AuthController { + constructor(registerUserCommandHandler, loginUserCommandHandler, jwtService) { + this.registerUserCommandHandler = registerUserCommandHandler; + this.loginUserCommandHandler = loginUserCommandHandler; + this.jwtService = jwtService; + } + + /** + * POST /api/auth/register - User regisztráció + */ + async register(req, res) { + try { + const { name, email, password } = req.body; + + const command = new RegisterUserCommand(name, email, password); + const result = await this.registerUserCommandHandler.handle(command); + + // Set JWT token in httpOnly cookie + res.cookie( + this.jwtService.getCookieName(), + result.token, + this.jwtService.getCookieOptions() + ); + + res.status(201).json({ + message: 'User registered successfully', + data: { + user: result.user + } + }); + } catch (error) { + // Validációs hibák -> 400 + const status = error.message.includes('required') || + error.message.includes('already exists') || + error.message.includes('Invalid') || + error.message.includes('must be') ? 400 : 500; + + res.status(status).json({ error: error.message }); + } + } + + /** + * POST /api/auth/login - User bejelentkezés + */ + async login(req, res) { + try { + const { email, password } = req.body; + + const command = new LoginUserCommand(email, password); + const result = await this.loginUserCommandHandler.handle(command); + + // Set JWT token in httpOnly cookie + res.cookie( + this.jwtService.getCookieName(), + result.token, + this.jwtService.getCookieOptions() + ); + + res.status(200).json({ + message: 'Login successful', + data: { + user: result.user + } + }); + } catch (error) { + // Validációs vagy auth hibák -> 401 + const status = error.message.includes('Invalid') || + error.message.includes('required') ? 401 : 500; + + res.status(status).json({ error: error.message }); + } + } + + /** + * POST /api/auth/logout - User kijelentkezés + */ + async logout(req, res) { + try { + // Clear the auth cookie + res.clearCookie(this.jwtService.getCookieName(), { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + path: '/' + }); + + res.status(200).json({ + message: 'Logout successful' + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } + } +} + +module.exports = AuthController; diff --git a/Backend/negyedik gyakorlat/src/api/controllers/UserController.js b/Backend/negyedik gyakorlat/src/api/controllers/UserController.js new file mode 100644 index 0000000..29fc32b --- /dev/null +++ b/Backend/negyedik gyakorlat/src/api/controllers/UserController.js @@ -0,0 +1,104 @@ +const GetMeQuery = require('../../application/user/queries/GetMeQuery'); +const GetAllUsersQuery = require('../../application/user/queries/GetAllUsersQuery'); +const GetUserByIdQuery = require('../../application/user/queries/GetUserByIdQuery'); +const UpdateUserProfileCommand = require('../../application/user/commands/UpdateUserProfileCommand'); + +/** + * User Controller + * User-related endpoints using CQRS pattern (protected by JWT) + */ +class UserController { + constructor(getMeQueryHandler, getAllUsersQueryHandler, getUserByIdQueryHandler, updateUserProfileCommandHandler) { + this.getMeQueryHandler = getMeQueryHandler; + this.getAllUsersQueryHandler = getAllUsersQueryHandler; + this.getUserByIdQueryHandler = getUserByIdQueryHandler; + this.updateUserProfileCommandHandler = updateUserProfileCommandHandler; + } + + /** + * GET /api/users/me - Bejelentkezett user adatai (protected) + */ + async getMe(req, res) { + try { + // req.user-t az authMiddleware tölti ki a JWT-ből + const userId = req.user.userId; + + const query = new GetMeQuery(userId); + const user = await this.getMeQueryHandler.handle(query); + + res.status(200).json({ + message: 'User retrieved successfully', + data: user + }); + } catch (error) { + const status = error.message.includes('not found') ? 404 : 500; + res.status(status).json({ error: error.message }); + } + } + + /** + * GET /api/users - Összes user lekérése (protected) + */ + async getAll(req, res) { + try { + const query = new GetAllUsersQuery(); + const users = await this.getAllUsersQueryHandler.handle(query); + + res.status(200).json({ + message: 'Users retrieved successfully', + data: users, + count: users.length + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } + } + + /** + * GET /api/users/:id - User lekérése ID alapján (protected) + */ + async getById(req, res) { + try { + const { id } = req.params; + const userId = parseInt(id); + + if (isNaN(userId)) { + return res.status(400).json({ error: 'Invalid user ID' }); + } + + const query = new GetUserByIdQuery(userId); + const user = await this.getUserByIdQueryHandler.handle(query); + + res.status(200).json({ + message: 'User retrieved successfully', + data: user + }); + } catch (error) { + const status = error.message.includes('not found') ? 404 : 500; + res.status(status).json({ error: error.message }); + } + } + + /** + * PUT /api/users/me - User profil frissítése (protected) + */ + async updateMe(req, res) { + try { + const userId = req.user.userId; + const { name } = req.body; + + const command = new UpdateUserProfileCommand(userId, name); + const user = await this.updateUserProfileCommandHandler.handle(command); + + res.status(200).json({ + message: 'Profile updated successfully', + data: user + }); + } catch (error) { + const status = error.message.includes('required') ? 400 : 500; + res.status(status).json({ error: error.message }); + } + } +} + +module.exports = UserController; diff --git a/Backend/negyedik gyakorlat/src/api/middlewares/authMiddleware.js b/Backend/negyedik gyakorlat/src/api/middlewares/authMiddleware.js new file mode 100644 index 0000000..f2f8480 --- /dev/null +++ b/Backend/negyedik gyakorlat/src/api/middlewares/authMiddleware.js @@ -0,0 +1,44 @@ +const JwtService = require('../../application/services/JwtService'); + +const jwtService = new JwtService(); + +/** + * Authentication Middleware - JWT token ellenőrzés (Cookie-based) + * + * Ezt a middleware-t használd protected route-okon! + * + * Példa használat: + * router.get('/me', authMiddleware, userController.getMe); + */ +function authMiddleware(req, res, next) { + try { + // 1. Token kinyerése cookieból + const token = jwtService.extractTokenFromCookies(req.cookies); + + if (!token) { + return res.status(401).json({ + error: 'Authentication required', + message: 'No token provided in cookies' + }); + } + + // 2. Token verifikálása + const decoded = jwtService.verifyToken(token); + + // 3. User adatok elhelyezése req.user-ben (controller-ek használhatják) + req.user = { + userId: decoded.userId, + email: decoded.email + }; + + // 4. Folytatás + next(); + } catch (error) { + return res.status(401).json({ + error: 'Authentication failed', + message: error.message + }); + } +} + +module.exports = authMiddleware; diff --git a/Backend/negyedik gyakorlat/src/api/middlewares/corsMiddleware.js b/Backend/negyedik gyakorlat/src/api/middlewares/corsMiddleware.js new file mode 100644 index 0000000..e9d2402 --- /dev/null +++ b/Backend/negyedik gyakorlat/src/api/middlewares/corsMiddleware.js @@ -0,0 +1,22 @@ +const cors = require('cors'); + +// Engedélyezett origin-ek whitelist (környezeti változóból) +const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000']; + +const corsOptions = { + origin: function (origin, callback) { + // TODO 1: Ha nincs origin (pl. Postman, curl, backend-backend hívás), engedélyezd + // Tipp: if (!origin) return callback(null, true); + + // TODO 2: Ha az origin benne van az allowedOrigins-ban, engedélyezd + // Tipp: if (allowedOrigins.includes(origin)) return callback(null, true); + + // TODO 3: Egyébként tiltsd le CORS hibával + // Tipp: callback(new Error('Not allowed by CORS')); + }, + credentials: true, // Cookie/Auth header engedélyezése + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization'] +}; + +module.exports = cors(corsOptions); diff --git a/Backend/negyedik gyakorlat/src/api/middlewares/scopeMiddleware.js b/Backend/negyedik gyakorlat/src/api/middlewares/scopeMiddleware.js new file mode 100644 index 0000000..66c6f89 --- /dev/null +++ b/Backend/negyedik gyakorlat/src/api/middlewares/scopeMiddleware.js @@ -0,0 +1,18 @@ +/** + * Request szintű DI scope létrehozása + * Middleware ami minden kéréshez új DI scope-ot készít + */ +function scopeMiddleware(container) { + return (req, res, next) => { + // TODO 1: Hozz létre request-specifikus scope-ot + // Tipp: const scope = container.createScope(); + + // TODO 2: Tárold el a scope-ot req.scope alatt + // Tipp: req.scope = scope; + + // TODO 3: Hívd meg a next()-et + // Tipp: next(); + }; +} + +module.exports = scopeMiddleware; diff --git a/Backend/negyedik gyakorlat/src/api/routers/authRoutes.js b/Backend/negyedik gyakorlat/src/api/routers/authRoutes.js new file mode 100644 index 0000000..d4afa52 --- /dev/null +++ b/Backend/negyedik gyakorlat/src/api/routers/authRoutes.js @@ -0,0 +1,43 @@ +const express = require('express'); + +/** + * Auth Routes + * Public endpoints (nincs JWT védelem) + * + * @param {Container} container - DI Container + */ +function createAuthRoutes(container) { + const router = express.Router(); + + // AuthController lekérése a DI Container-ből + const authController = container.resolve('AuthController'); + + /** + * POST /api/auth/register - User regisztráció + * Body: { name, email, password } + * Response: { user, token } + */ + router.post('/register', (req, res) => authController.register(req, res)); + + /** + * POST /api/auth/login - User bejelentkezés + * Body: { email, password } + * Response: { user, token } + */ + router.post('/login', (req, res) => authController.login(req, res)); + + /** + * POST /api/auth/logout - User kijelentkezés + * Clears the authentication cookie + */ + router.post('/logout', (req, res) => authController.logout(req, res)); + + /** + * OPTIONS /api/auth/* - CORS preflight + */ + router.options('*', (req, res) => res.sendStatus(204)); + + return router; +} + +module.exports = createAuthRoutes; diff --git a/Backend/negyedik gyakorlat/src/api/routers/userRoutes.js b/Backend/negyedik gyakorlat/src/api/routers/userRoutes.js new file mode 100644 index 0000000..7792f33 --- /dev/null +++ b/Backend/negyedik gyakorlat/src/api/routers/userRoutes.js @@ -0,0 +1,49 @@ +const express = require('express'); +const authMiddleware = require('../middlewares/authMiddleware'); + +/** + * User Routes + * Protected endpoints (JWT authentication required) + * + * @param {Container} container - DI Container + */ +function createUserRoutes(container) { + const router = express.Router(); + + // UserController lekérése a DI Container-ből + const userController = container.resolve('UserController'); + + /** + * GET /api/users/me - Bejelentkezett user adatai + * Headers: Authorization: Bearer + */ + router.get('/me', authMiddleware, (req, res) => userController.getMe(req, res)); + + /** + * PUT /api/users/me - User profil frissítése + * Headers: Authorization: Bearer + * Body: { name } + */ + router.put('/me', authMiddleware, (req, res) => userController.updateMe(req, res)); + + /** + * GET /api/users - Összes user lekérése (protected) + * Headers: Authorization: Bearer + */ + router.get('/', authMiddleware, (req, res) => userController.getAll(req, res)); + + /** + * GET /api/users/:id - User lekérése ID alapján (protected) + * Headers: Authorization: Bearer + */ + router.get('/:id', authMiddleware, (req, res) => userController.getById(req, res)); + + /** + * OPTIONS /api/users/* - CORS preflight + */ + router.options('*', (req, res) => res.sendStatus(204)); + + return router; +} + +module.exports = createUserRoutes; diff --git a/Backend/negyedik gyakorlat/src/api/server.js b/Backend/negyedik gyakorlat/src/api/server.js new file mode 100644 index 0000000..55a1dfe --- /dev/null +++ b/Backend/negyedik gyakorlat/src/api/server.js @@ -0,0 +1,215 @@ +require('dotenv').config(); +const express = require('express'); +const cookieParser = require('cookie-parser'); + +// Infrastructure +const databaseConnection = require('../infrastructure/db/DatabaseConnection'); +const UserRepository = require('../infrastructure/repositories/UserRepository'); + +// Application Services +const Container = require('../application/services/Container'); +const EmailService = require('../application/services/EmailService'); +const JwtService = require('../application/services/JwtService'); + +// Command Handlers +const RegisterUserCommandHandler = require('../application/auth/commands/RegisterUserCommandHandler'); +const LoginUserCommandHandler = require('../application/auth/commands/LoginUserCommandHandler'); +const UpdateUserProfileCommandHandler = require('../application/user/commands/UpdateUserProfileCommandHandler'); + +// Query Handlers +const GetMeQueryHandler = require('../application/user/queries/GetMeQueryHandler'); +const GetAllUsersQueryHandler = require('../application/user/queries/GetAllUsersQueryHandler'); +const GetUserByIdQueryHandler = require('../application/user/queries/GetUserByIdQueryHandler'); + +// Controllers +const AuthController = require('./controllers/AuthController'); +const UserController = require('./controllers/UserController'); + +// Middlewares +const corsMiddleware = require('./middlewares/corsMiddleware'); +const scopeMiddleware = require('./middlewares/scopeMiddleware'); + +// Routes +const createAuthRoutes = require('./routers/authRoutes'); +const createUserRoutes = require('./routers/userRoutes'); + +const app = express(); +const container = new Container(); + +/** + * Dependency Injection Setup (CQRS Pattern) + */ +function setupDependencies() { + // Database Connection (singleton) + container.register('DatabaseConnection', () => databaseConnection, 'singleton'); + + // PrismaClient (singleton) + container.register('PrismaClient', () => { + return databaseConnection.getClient(); + }, 'singleton'); + + // UserRepository (singleton) + container.register('UserRepository', () => { + return new UserRepository(container.resolve('PrismaClient')); + }, 'singleton'); + + // EmailService (singleton) - A DIÁKOK IMPLEMENTÁLJÁK! + container.register('EmailService', () => new EmailService(), 'singleton'); + + // JwtService (singleton) + container.register('JwtService', () => new JwtService(), 'singleton'); + + // === Command Handlers === + + // RegisterUserCommandHandler (singleton) + container.register('RegisterUserCommandHandler', () => { + return new RegisterUserCommandHandler( + container.resolve('PrismaClient'), + container.resolve('EmailService') + ); + }, 'singleton'); + + // LoginUserCommandHandler (singleton) + container.register('LoginUserCommandHandler', () => { + return new LoginUserCommandHandler(container.resolve('PrismaClient')); + }, 'singleton'); + + // UpdateUserProfileCommandHandler (singleton) + container.register('UpdateUserProfileCommandHandler', () => { + return new UpdateUserProfileCommandHandler(container.resolve('PrismaClient')); + }, 'singleton'); + + // === Query Handlers === + + // GetMeQueryHandler (singleton) + container.register('GetMeQueryHandler', () => { + return new GetMeQueryHandler(container.resolve('PrismaClient')); + }, 'singleton'); + + // GetAllUsersQueryHandler (singleton) + container.register('GetAllUsersQueryHandler', () => { + return new GetAllUsersQueryHandler(container.resolve('PrismaClient')); + }, 'singleton'); + + // GetUserByIdQueryHandler (singleton) + container.register('GetUserByIdQueryHandler', () => { + return new GetUserByIdQueryHandler(container.resolve('PrismaClient')); + }, 'singleton'); + + // === Controllers === + + // AuthController (singleton) + container.register('AuthController', () => { + return new AuthController( + container.resolve('RegisterUserCommandHandler'), + container.resolve('LoginUserCommandHandler'), + container.resolve('JwtService') + ); + }, 'singleton'); + + // UserController (singleton) + container.register('UserController', () => { + return new UserController( + container.resolve('GetMeQueryHandler'), + container.resolve('GetAllUsersQueryHandler'), + container.resolve('GetUserByIdQueryHandler'), + container.resolve('UpdateUserProfileCommandHandler') + ); + }, 'singleton'); + + console.log('✅ DI Container configured (CQRS pattern)'); +} + +/** + * Express Middleware Chain + */ +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +app.use(cookieParser()); + +// TODO (DIÁKOK): Alkalmazd a CORS middleware-t +// Tipp: app.use(corsMiddleware); + +// TODO (DIÁKOK): Alkalmazd a Scope middleware-t (opcionális) +// Tipp: app.use(scopeMiddleware(container)); + +/** + * Routes + */ +app.use('/api/auth', createAuthRoutes(container)); +app.use('/api/users', createUserRoutes(container)); + +/** + * Health Check Endpoint + */ +app.get('/health', (req, res) => { + res.status(200).json({ + status: 'OK', + message: 'Server is running', + timestamp: new Date().toISOString() + }); +}); + +/** + * Error Handling Middleware + */ +app.use((err, req, res, next) => { + console.error('❌ Error:', err.message); + res.status(err.status || 500).json({ + error: err.message || 'Internal Server Error' + }); +}); + +/** + * 404 Handler + */ +app.use((req, res) => { + res.status(404).json({ error: 'Route not found' }); +}); + +/** + * Server Startup + */ +const PORT = process.env.PORT || 3000; + +async function startServer() { + try { + // Database connection + await databaseConnection.connect(); + + // DI Container inicializálása + setupDependencies(); + + // Server indítása + app.listen(PORT, () => { + console.log('🚀 Server running on http://localhost:' + PORT); + console.log('📧 Email Service configured (implement EmailService!)'); + console.log('🔐 JWT Authentication enabled (cookie-based)'); + console.log('🍪 Cookies: httpOnly, secure (production), sameSite=strict'); + console.log(''); + console.log('📍 Endpoints:'); + console.log(' POST /api/auth/register'); + console.log(' POST /api/auth/login'); + console.log(' POST /api/auth/logout'); + console.log(' GET /api/users/me (protected)'); + console.log(' PUT /api/users/me (protected)'); + console.log(' GET /api/users (protected)'); + console.log(' GET /api/users/:id (protected)'); + }); + } catch (error) { + console.error('❌ Failed to start server:', error); + await databaseConnection.disconnect(); + process.exit(1); + } +} + +// Graceful shutdown +process.on('SIGINT', async () => { + console.log('\n🛑 Shutting down gracefully...'); + await databaseConnection.disconnect(); + process.exit(0); +}); + +startServer(); + +module.exports = app; diff --git a/Backend/negyedik gyakorlat/src/application/auth/commands/LoginUserCommand.js b/Backend/negyedik gyakorlat/src/application/auth/commands/LoginUserCommand.js new file mode 100644 index 0000000..db04245 --- /dev/null +++ b/Backend/negyedik gyakorlat/src/application/auth/commands/LoginUserCommand.js @@ -0,0 +1,12 @@ +/** + * Login User Command + * Command object for user login + */ +class LoginUserCommand { + constructor(email, password) { + this.email = email; + this.password = password; + } +} + +module.exports = LoginUserCommand; diff --git a/Backend/negyedik gyakorlat/src/application/auth/commands/LoginUserCommandHandler.js b/Backend/negyedik gyakorlat/src/application/auth/commands/LoginUserCommandHandler.js new file mode 100644 index 0000000..ea52e96 --- /dev/null +++ b/Backend/negyedik gyakorlat/src/application/auth/commands/LoginUserCommandHandler.js @@ -0,0 +1,60 @@ +const bcrypt = require('bcryptjs'); +const JwtService = require('../../services/JwtService'); + +const jwtService = new JwtService(); + +/** + * Login User Command Handler + * Handles user login authentication + */ +class LoginUserCommandHandler { + constructor(prisma) { + this.prisma = prisma; + } + + /** + * Execute login command + * @param {LoginUserCommand} command + * @returns {Promise} { user, token } + */ + async handle(command) { + const { email, password } = command; + + // Validáció + if (!email || !password) { + throw new Error('Email and password are required'); + } + + // User keresése email alapján + const user = await this.prisma.user.findUnique({ + where: { email } + }); + + if (!user) { + throw new Error('Invalid email or password'); + } + + // Jelszó ellenőrzése + const isPasswordValid = await bcrypt.compare(password, user.password); + + if (!isPasswordValid) { + throw new Error('Invalid email or password'); + } + + // JWT token generálása + const token = jwtService.generateToken({ + userId: user.id, + email: user.email + }); + + // Jelszót ne adjuk vissza + const { password: _, ...userWithoutPassword } = user; + + return { + user: userWithoutPassword, + token + }; + } +} + +module.exports = LoginUserCommandHandler; diff --git a/Backend/negyedik gyakorlat/src/application/auth/commands/RegisterUserCommand.js b/Backend/negyedik gyakorlat/src/application/auth/commands/RegisterUserCommand.js new file mode 100644 index 0000000..4060b2b --- /dev/null +++ b/Backend/negyedik gyakorlat/src/application/auth/commands/RegisterUserCommand.js @@ -0,0 +1,13 @@ +/** + * Register User Command + * Command object for user registration + */ +class RegisterUserCommand { + constructor(name, email, password) { + this.name = name; + this.email = email; + this.password = password; + } +} + +module.exports = RegisterUserCommand; diff --git a/Backend/negyedik gyakorlat/src/application/auth/commands/RegisterUserCommandHandler.js b/Backend/negyedik gyakorlat/src/application/auth/commands/RegisterUserCommandHandler.js new file mode 100644 index 0000000..7485ba3 --- /dev/null +++ b/Backend/negyedik gyakorlat/src/application/auth/commands/RegisterUserCommandHandler.js @@ -0,0 +1,83 @@ +const bcrypt = require('bcryptjs'); +const JwtService = require('../../services/JwtService'); + +const jwtService = new JwtService(); + +/** + * Register User Command Handler + * Handles user registration business logic + */ +class RegisterUserCommandHandler { + constructor(prisma, emailService) { + this.prisma = prisma; + this.emailService = emailService; + } + + /** + * Execute user registration command + * @param {RegisterUserCommand} command + * @returns {Promise} { user, token } + */ + async handle(command) { + const { name, email, password } = command; + + // Validáció + if (!name || !email || !password) { + throw new Error('Name, email and password are required'); + } + + if (password.length < 6) { + throw new Error('Password must be at least 6 characters long'); + } + + // Email formátum ellenőrzés + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + throw new Error('Invalid email format'); + } + + // Ellenőrizzük, hogy létezik-e már a user + const existingUser = await this.prisma.user.findUnique({ + where: { email } + }); + + if (existingUser) { + throw new Error('User with this email already exists'); + } + + // Jelszó hashelése (bcrypt) + const hashedPassword = await bcrypt.hash(password, 10); + + // User létrehozása + const user = await this.prisma.user.create({ + data: { + name, + email, + password: hashedPassword + } + }); + + // Welcome email küldése (async, nem várunk rá) + if (this.emailService) { + this.emailService.sendWelcomeEmail(email, name).catch(err => { + console.error('❌ Failed to send welcome email:', err.message); + }); + } + + // JWT token generálása + const token = jwtService.generateToken({ + userId: user.id, + email: user.email + }); + + // Jelszót ne adjuk vissza a response-ban + const { password: _, ...userWithoutPassword } = user; + + return { + user: userWithoutPassword, + token + }; + } +} + +module.exports = RegisterUserCommandHandler; diff --git a/Backend/negyedik gyakorlat/src/application/services/Container.js b/Backend/negyedik gyakorlat/src/application/services/Container.js new file mode 100644 index 0000000..d451a17 --- /dev/null +++ b/Backend/negyedik gyakorlat/src/application/services/Container.js @@ -0,0 +1,57 @@ +/** + * Dependency Injection Container + * Supports singleton, transient, and scoped lifetimes + */ +class Container { + constructor() { + // TODO 1: Inicializáld a services Map-et (singleton instance-ok tárolása) + // TODO 2: Inicializáld a factories Map-et (factory függvények tárolása) + // TODO 3: Inicializáld a lifetimes Map-et (lifecycle típusok tárolása) + + // Példa inicializálás: + // this.services = new Map(); + // this.factories = new Map(); + // this.lifetimes = new Map(); + } + + register(name, factory, lifetime = 'singleton') { + // TODO 4: Tárold el a factory függvényt (this.factories.set(name, factory)) + // TODO 5: Tárold el a lifetime típust (this.lifetimes.set(name, lifetime)) + // TODO 6: Ha a lifetime === 'singleton', azonnal példányosítsd: + // - Hívd meg a factory-t: const instance = factory(); + // - Tárold el: this.services.set(name, instance); + } + + resolve(name, scope = null) { + // TODO 7: Ha a service regisztrálva van mint 'scoped' ÉS van scope paraméter: + // - Ellenőrizd: if (scope && scope.has(name)) return scope.get(name); + // - Ha nincs még a scope-ban, példányosítsd és tárold: + // const instance = this.factories.get(name)(); + // scope.set(name, instance); + // return instance; + + // TODO 8: Ha singleton, add vissza a services-ből: + // if (this.lifetimes.get(name) === 'singleton') { + // return this.services.get(name); + // } + + // TODO 9: Ha transient, minden alkalommal hívj egy új factory-t: + // if (this.lifetimes.get(name) === 'transient') { + // return this.factories.get(name)(); + // } + + // TODO 10: Ha nem regisztrált a service, dobj hibát: + // throw new Error(`Service '${name}' is not registered`); + } + + createScope() { + // TODO 11: Hozz létre egy új Map-et az scoped instance-oknak + // TODO 12: Térj vissza egy objektummal ami tartalmaz egy resolve metódust: + // const scopeMap = new Map(); + // return { + // resolve: (name) => this.resolve(name, scopeMap) + // }; + } +} + +module.exports = Container; diff --git a/Backend/negyedik gyakorlat/src/application/services/EmailService.js b/Backend/negyedik gyakorlat/src/application/services/EmailService.js new file mode 100644 index 0000000..a7fcebf --- /dev/null +++ b/Backend/negyedik gyakorlat/src/application/services/EmailService.js @@ -0,0 +1,31 @@ +/** + * Email Service + * Nodemailer + Handlebars template based email sending + * + * TODO: Implement by students + */ +class EmailService { + constructor() { + // TODO 1: Hozz létre Nodemailer transportot Ethereal tesztelő SMTP-vel + // this.transporter = nodemailer.createTransport({ + // host: 'smtp.ethereal.email', + // port: 587, + // secure: false, // TLS + // auth: { + // user: process.env.ETHEREAL_USER || 'your-test-email@ethereal.email', + // pass: process.env.ETHEREAL_PASS || 'your-test-password' + // } + // }); + + console.log('📧 EmailService initialized'); + } + + async sendWelcomeEmail(userEmail, userName) { + // TODO 2-6: Implement email sending with Handlebars template + // For now, just log to console + console.log(`📧 Would send welcome email to ${userEmail} (${userName})`); + return true; + } +} + +module.exports = EmailService; diff --git a/Backend/negyedik gyakorlat/src/application/services/JwtService.js b/Backend/negyedik gyakorlat/src/application/services/JwtService.js new file mode 100644 index 0000000..2ef935a --- /dev/null +++ b/Backend/negyedik gyakorlat/src/application/services/JwtService.js @@ -0,0 +1,114 @@ +const jwt = require('jsonwebtoken'); + +/** + * JWT Service + * Handles JWT token generation, verification, and cookie management + */ +class JwtService { + constructor() { + this.secret = process.env.JWT_SECRET || 'default-secret-change-me'; + this.expiresIn = process.env.JWT_EXPIRES_IN || '1h'; + this.cookieName = 'auth_token'; + } + + /** + * Generate JWT token + * @param {Object} payload - { userId, email } + * @returns {string} JWT token + */ + generateToken(payload) { + return jwt.sign(payload, this.secret, { expiresIn: this.expiresIn }); + } + + /** + * Verify JWT token + * @param {string} token - JWT token + * @returns {Object} Decoded payload + */ + verifyToken(token) { + try { + return jwt.verify(token, this.secret); + } catch (error) { + throw new Error('Invalid or expired token'); + } + } + + /** + * Extract token from Authorization header (Bearer token) + * @param {string} authHeader - Authorization header value + * @returns {string|null} Token or null + */ + extractTokenFromHeader(authHeader) { + if (!authHeader) { + return null; + } + + const parts = authHeader.split(' '); + + if (parts.length !== 2 || parts[0] !== 'Bearer') { + return null; + } + + return parts[1]; + } + + /** + * Extract token from cookies + * @param {Object} cookies - Request cookies object + * @returns {string|null} Token or null + */ + extractTokenFromCookies(cookies) { + if (!cookies || !cookies[this.cookieName]) { + return null; + } + + return cookies[this.cookieName]; + } + + /** + * Get cookie options for setting JWT cookie + * @returns {Object} Cookie options + */ + getCookieOptions() { + const isProduction = process.env.NODE_ENV === 'production'; + + return { + httpOnly: true, // Prevents XSS attacks + secure: isProduction, // HTTPS only in production + sameSite: 'strict', // CSRF protection + maxAge: this._getMaxAge(), + path: '/' + }; + } + + /** + * Get cookie max age in milliseconds + * @private + * @returns {number} + */ + _getMaxAge() { + // Parse JWT_EXPIRES_IN (e.g., "1h", "7d") + const expiresIn = this.expiresIn; + + if (expiresIn.endsWith('h')) { + return parseInt(expiresIn) * 60 * 60 * 1000; + } else if (expiresIn.endsWith('d')) { + return parseInt(expiresIn) * 24 * 60 * 60 * 1000; + } else if (expiresIn.endsWith('m')) { + return parseInt(expiresIn) * 60 * 1000; + } + + // Default: 1 hour + return 60 * 60 * 1000; + } + + /** + * Get cookie name + * @returns {string} + */ + getCookieName() { + return this.cookieName; + } +} + +module.exports = JwtService; diff --git a/Backend/negyedik gyakorlat/src/application/services/templates/welcome.hbs b/Backend/negyedik gyakorlat/src/application/services/templates/welcome.hbs new file mode 100644 index 0000000..1ced07e --- /dev/null +++ b/Backend/negyedik gyakorlat/src/application/services/templates/welcome.hbs @@ -0,0 +1,145 @@ + + + + + + Üdvözlünk! + + + + + + diff --git a/Backend/negyedik gyakorlat/src/application/user/commands/UpdateUserProfileCommand.js b/Backend/negyedik gyakorlat/src/application/user/commands/UpdateUserProfileCommand.js new file mode 100644 index 0000000..99dc8af --- /dev/null +++ b/Backend/negyedik gyakorlat/src/application/user/commands/UpdateUserProfileCommand.js @@ -0,0 +1,12 @@ +/** + * Update User Profile Command + * Command object for updating user profile + */ +class UpdateUserProfileCommand { + constructor(userId, name) { + this.userId = userId; + this.name = name; + } +} + +module.exports = UpdateUserProfileCommand; diff --git a/Backend/negyedik gyakorlat/src/application/user/commands/UpdateUserProfileCommandHandler.js b/Backend/negyedik gyakorlat/src/application/user/commands/UpdateUserProfileCommandHandler.js new file mode 100644 index 0000000..2b67450 --- /dev/null +++ b/Backend/negyedik gyakorlat/src/application/user/commands/UpdateUserProfileCommandHandler.js @@ -0,0 +1,32 @@ +/** + * Update User Profile Command Handler + * Handles user profile update logic + */ +class UpdateUserProfileCommandHandler { + constructor(prisma) { + this.prisma = prisma; + } + + /** + * Execute update profile command + * @param {UpdateUserProfileCommand} command + * @returns {Promise} Updated user data + */ + async handle(command) { + const { userId, name } = command; + + if (!name) { + throw new Error('Name is required'); + } + + const user = await this.prisma.user.update({ + where: { id: userId }, + data: { name } + }); + + const { password, ...userWithoutPassword } = user; + return userWithoutPassword; + } +} + +module.exports = UpdateUserProfileCommandHandler; diff --git a/Backend/negyedik gyakorlat/src/application/user/queries/GetAllUsersQuery.js b/Backend/negyedik gyakorlat/src/application/user/queries/GetAllUsersQuery.js new file mode 100644 index 0000000..ad4a86d --- /dev/null +++ b/Backend/negyedik gyakorlat/src/application/user/queries/GetAllUsersQuery.js @@ -0,0 +1,11 @@ +/** + * Get All Users Query + * Query object for retrieving all users + */ +class GetAllUsersQuery { + constructor() { + // No parameters needed for getting all users + } +} + +module.exports = GetAllUsersQuery; diff --git a/Backend/negyedik gyakorlat/src/application/user/queries/GetAllUsersQueryHandler.js b/Backend/negyedik gyakorlat/src/application/user/queries/GetAllUsersQueryHandler.js new file mode 100644 index 0000000..0cf0b9f --- /dev/null +++ b/Backend/negyedik gyakorlat/src/application/user/queries/GetAllUsersQueryHandler.js @@ -0,0 +1,25 @@ +/** + * Get All Users Query Handler + * Handles retrieval of all users + */ +class GetAllUsersQueryHandler { + constructor(prisma) { + this.prisma = prisma; + } + + /** + * Execute get all users query + * @param {GetAllUsersQuery} query + * @returns {Promise} List of users without passwords + */ + async handle(query) { + const users = await this.prisma.user.findMany({ + orderBy: { createdAt: 'desc' } + }); + + // Remove passwords from all users + return users.map(({ password, ...user }) => user); + } +} + +module.exports = GetAllUsersQueryHandler; diff --git a/Backend/negyedik gyakorlat/src/application/user/queries/GetMeQuery.js b/Backend/negyedik gyakorlat/src/application/user/queries/GetMeQuery.js new file mode 100644 index 0000000..2b2e306 --- /dev/null +++ b/Backend/negyedik gyakorlat/src/application/user/queries/GetMeQuery.js @@ -0,0 +1,11 @@ +/** + * Get Me Query + * Query object for getting current authenticated user + */ +class GetMeQuery { + constructor(userId) { + this.userId = userId; + } +} + +module.exports = GetMeQuery; diff --git a/Backend/negyedik gyakorlat/src/application/user/queries/GetMeQueryHandler.js b/Backend/negyedik gyakorlat/src/application/user/queries/GetMeQueryHandler.js new file mode 100644 index 0000000..d6d1e9e --- /dev/null +++ b/Backend/negyedik gyakorlat/src/application/user/queries/GetMeQueryHandler.js @@ -0,0 +1,31 @@ +/** + * Get Me Query Handler + * Handles retrieval of current authenticated user + */ +class GetMeQueryHandler { + constructor(prisma) { + this.prisma = prisma; + } + + /** + * Execute get me query + * @param {GetMeQuery} query + * @returns {Promise} User data without password + */ + async handle(query) { + const { userId } = query; + + const user = await this.prisma.user.findUnique({ + where: { id: userId } + }); + + if (!user) { + throw new Error('User not found'); + } + + const { password, ...userWithoutPassword } = user; + return userWithoutPassword; + } +} + +module.exports = GetMeQueryHandler; diff --git a/Backend/negyedik gyakorlat/src/application/user/queries/GetUserByIdQuery.js b/Backend/negyedik gyakorlat/src/application/user/queries/GetUserByIdQuery.js new file mode 100644 index 0000000..e15ca3a --- /dev/null +++ b/Backend/negyedik gyakorlat/src/application/user/queries/GetUserByIdQuery.js @@ -0,0 +1,11 @@ +/** + * Get User By ID Query + * Query object for retrieving a user by ID + */ +class GetUserByIdQuery { + constructor(userId) { + this.userId = userId; + } +} + +module.exports = GetUserByIdQuery; diff --git a/Backend/negyedik gyakorlat/src/application/user/queries/GetUserByIdQueryHandler.js b/Backend/negyedik gyakorlat/src/application/user/queries/GetUserByIdQueryHandler.js new file mode 100644 index 0000000..4e80c6d --- /dev/null +++ b/Backend/negyedik gyakorlat/src/application/user/queries/GetUserByIdQueryHandler.js @@ -0,0 +1,35 @@ +/** + * Get User By ID Query Handler + * Handles retrieval of a specific user by ID + */ +class GetUserByIdQueryHandler { + constructor(prisma) { + this.prisma = prisma; + } + + /** + * Execute get user by ID query + * @param {GetUserByIdQuery} query + * @returns {Promise} User data without password + */ + async handle(query) { + const { userId } = query; + + if (!userId || isNaN(userId)) { + throw new Error('Valid user ID is required'); + } + + const user = await this.prisma.user.findUnique({ + where: { id: parseInt(userId) } + }); + + if (!user) { + throw new Error('User not found'); + } + + const { password, ...userWithoutPassword } = user; + return userWithoutPassword; + } +} + +module.exports = GetUserByIdQueryHandler; diff --git a/Backend/negyedik gyakorlat/src/domain/irepositories/IUserRepository.js b/Backend/negyedik gyakorlat/src/domain/irepositories/IUserRepository.js new file mode 100644 index 0000000..51250f9 --- /dev/null +++ b/Backend/negyedik gyakorlat/src/domain/irepositories/IUserRepository.js @@ -0,0 +1,60 @@ +/** + * User Repository Interface + * Defines contract for user data access + */ +class IUserRepository { + /** + * Find user by ID + * @param {number} id + * @returns {Promise} + */ + async findById(id) { + throw new Error('Method findById() must be implemented'); + } + + /** + * Find user by email + * @param {string} email + * @returns {Promise} + */ + async findByEmail(email) { + throw new Error('Method findByEmail() must be implemented'); + } + + /** + * Find all users + * @returns {Promise} + */ + async findAll() { + throw new Error('Method findAll() must be implemented'); + } + + /** + * Create new user + * @param {User} user + * @returns {Promise} + */ + async create(user) { + throw new Error('Method create() must be implemented'); + } + + /** + * Update existing user + * @param {User} user + * @returns {Promise} + */ + async update(user) { + throw new Error('Method update() must be implemented'); + } + + /** + * Delete user by ID + * @param {number} id + * @returns {Promise} + */ + async delete(id) { + throw new Error('Method delete() must be implemented'); + } +} + +module.exports = IUserRepository; diff --git a/Backend/negyedik gyakorlat/src/domain/models/User.js b/Backend/negyedik gyakorlat/src/domain/models/User.js new file mode 100644 index 0000000..ebe5695 --- /dev/null +++ b/Backend/negyedik gyakorlat/src/domain/models/User.js @@ -0,0 +1,76 @@ +/** + * User Domain Model + * Pure domain entity with business logic validation + */ +class User { + constructor(id, name, email, password, createdAt, updatedAt) { + this.id = id; + this.name = name; + this.email = email; + this.password = password; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + /** + * Factory method to create a new User + * @param {Object} data - { name, email, password } + * @returns {User} + */ + static create(data) { + const { name, email, password } = data; + + // Validation + if (!name || name.trim().length === 0) { + throw new Error('User name is required'); + } + + if (!email || !User.isValidEmail(email)) { + throw new Error('Valid email is required'); + } + + if (!password || password.length < 6) { + throw new Error('Password must be at least 6 characters long'); + } + + return new User(null, name.trim(), email.toLowerCase(), password, new Date(), new Date()); + } + + /** + * Email validation + * @param {string} email + * @returns {boolean} + */ + static isValidEmail(email) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + } + + /** + * Update user name + * @param {string} newName + */ + updateName(newName) { + if (!newName || newName.trim().length === 0) { + throw new Error('User name is required'); + } + this.name = newName.trim(); + this.updatedAt = new Date(); + } + + /** + * Get user without sensitive data + * @returns {Object} + */ + toPublicJSON() { + return { + id: this.id, + name: this.name, + email: this.email, + createdAt: this.createdAt, + updatedAt: this.updatedAt + }; + } +} + +module.exports = User; diff --git a/Backend/negyedik gyakorlat/src/infrastructure/db/DatabaseConnection.js b/Backend/negyedik gyakorlat/src/infrastructure/db/DatabaseConnection.js new file mode 100644 index 0000000..0c360d9 --- /dev/null +++ b/Backend/negyedik gyakorlat/src/infrastructure/db/DatabaseConnection.js @@ -0,0 +1,74 @@ +const { PrismaClient } = require('@prisma/client'); + +/** + * Database Connection Wrapper + * Manages Prisma Client lifecycle + */ +class DatabaseConnection { + constructor() { + this.prisma = null; + } + + /** + * Initialize Prisma Client + */ + async connect() { + if (this.prisma) { + console.log('⚠️ Prisma Client already connected'); + return; + } + + try { + this.prisma = new PrismaClient({ + log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], + }); + + await this.prisma.$connect(); + console.log('✅ Prisma connected to PostgreSQL'); + } catch (error) { + console.error('❌ Failed to connect to database:', error); + throw error; + } + } + + /** + * Get Prisma Client instance + * @returns {PrismaClient} + */ + getClient() { + if (!this.prisma) { + throw new Error('Database not connected. Call connect() first.'); + } + return this.prisma; + } + + /** + * Close database connection + */ + async disconnect() { + if (this.prisma) { + await this.prisma.$disconnect(); + console.log('🛑 Prisma disconnected'); + this.prisma = null; + } + } + + /** + * Health check + * @returns {Promise} + */ + async healthCheck() { + try { + await this.prisma.$queryRaw`SELECT 1`; + return true; + } catch (error) { + console.error('❌ Database health check failed:', error); + return false; + } + } +} + +// Singleton instance +const databaseConnection = new DatabaseConnection(); + +module.exports = databaseConnection; diff --git a/Backend/negyedik gyakorlat/src/infrastructure/repositories/UserRepository.js b/Backend/negyedik gyakorlat/src/infrastructure/repositories/UserRepository.js new file mode 100644 index 0000000..efeab86 --- /dev/null +++ b/Backend/negyedik gyakorlat/src/infrastructure/repositories/UserRepository.js @@ -0,0 +1,130 @@ +const IUserRepository = require('../../domain/irepositories/IUserRepository'); +const User = require('../../domain/models/User'); + +/** + * User Repository Implementation + * Prisma-based data access for User entity + */ +class UserRepository extends IUserRepository { + constructor(prisma) { + super(); + this.prisma = prisma; + } + + /** + * Find user by ID + * @param {number} id + * @returns {Promise} + */ + async findById(id) { + const userData = await this.prisma.user.findUnique({ + where: { id: parseInt(id) } + }); + + if (!userData) { + return null; + } + + return this._toDomain(userData); + } + + /** + * Find user by email + * @param {string} email + * @returns {Promise} + */ + async findByEmail(email) { + const userData = await this.prisma.user.findUnique({ + where: { email: email.toLowerCase() } + }); + + if (!userData) { + return null; + } + + return this._toDomain(userData); + } + + /** + * Find all users + * @returns {Promise} + */ + async findAll() { + const usersData = await this.prisma.user.findMany({ + orderBy: { createdAt: 'desc' } + }); + + return usersData.map(userData => this._toDomain(userData)); + } + + /** + * Create new user + * @param {User} user + * @returns {Promise} + */ + async create(user) { + const userData = await this.prisma.user.create({ + data: { + name: user.name, + email: user.email, + password: user.password + } + }); + + return this._toDomain(userData); + } + + /** + * Update existing user + * @param {User} user + * @returns {Promise} + */ + async update(user) { + const userData = await this.prisma.user.update({ + where: { id: user.id }, + data: { + name: user.name, + email: user.email, + password: user.password, + updatedAt: new Date() + } + }); + + return this._toDomain(userData); + } + + /** + * Delete user by ID + * @param {number} id + * @returns {Promise} + */ + async delete(id) { + try { + await this.prisma.user.delete({ + where: { id: parseInt(id) } + }); + return true; + } catch (error) { + return false; + } + } + + /** + * Convert Prisma data to Domain model + * @private + * @param {Object} userData - Prisma user data + * @returns {User} + */ + _toDomain(userData) { + return new User( + userData.id, + userData.name, + userData.email, + userData.password, + userData.createdAt, + userData.updatedAt + ); + } +} + +module.exports = UserRepository; diff --git a/Backend/negyedik gyakorlat/tests/container.test.js b/Backend/negyedik gyakorlat/tests/container.test.js new file mode 100644 index 0000000..cd47f91 --- /dev/null +++ b/Backend/negyedik gyakorlat/tests/container.test.js @@ -0,0 +1,39 @@ +const { expect } = require('chai'); +const Container = require('../src/core/Container'); + +describe('DI Container', () => { + let container; + + beforeEach(() => { + container = new Container(); + }); + + it('should register and resolve a singleton service', () => { + // TODO 1: Teszt: singleton service mindig ugyanazt az instance-t adja vissza + // 1. Regisztrálj egy service-t: container.register('TestService', () => ({ id: Math.random() }), 'singleton'); + // 2. Resolve-old kétszer: const instance1 = container.resolve('TestService'); + // 3. Ellenőrizd, hogy ugyanaz az instance: expect(instance1).to.equal(instance2); + }); + + it('should create new instance for transient service', () => { + // TODO 2: Teszt: transient service mindig új instance-t ad vissza + // 1. Regisztrálj egy transient service-t: container.register('TransientService', () => ({ id: Math.random() }), 'transient'); + // 2. Resolve-old kétszer + // 3. Ellenőrizd, hogy különböző instance-ok: expect(instance1).to.not.equal(instance2); + }); + + it('should resolve scoped service within scope', () => { + // TODO 3: Teszt: scoped service scope-on belül ugyanaz, kívül más instance + // 1. Regisztrálj egy scoped service-t: container.register('ScopedService', () => ({ id: Math.random() }), 'scoped'); + // 2. Hozz létre egy scope-ot: const scope1 = container.createScope(); + // 3. Resolve-old kétszer ugyanabban a scope-ban + // 4. Ellenőrizd, hogy ugyanaz az instance + // 5. Hozz létre új scope-ot és resolve-old ott is + // 6. Ellenőrizd, hogy különböző instance-ok a két scope között + }); + + it('should throw error for unregistered service', () => { + // TODO 4: Teszt: nem regisztrált service resolve hiba dobása + // Tipp: expect(() => container.resolve('NonExistentService')).to.throw(); + }); +}); diff --git a/Backend/negyedik gyakorlat/tests/unit/application/Container.test.js b/Backend/negyedik gyakorlat/tests/unit/application/Container.test.js new file mode 100644 index 0000000..4dbec8c --- /dev/null +++ b/Backend/negyedik gyakorlat/tests/unit/application/Container.test.js @@ -0,0 +1,311 @@ +const Container = require('../../../src/application/services/Container'); + +describe('Container - Dependency Injection', () => { + let container; + + beforeEach(() => { + container = new Container(); + }); + + describe('constructor', () => { + test('should initialize with empty Maps', () => { + expect(container.services).toBeInstanceOf(Map); + expect(container.factories).toBeInstanceOf(Map); + expect(container.lifetimes).toBeInstanceOf(Map); + expect(container.services.size).toBe(0); + expect(container.factories.size).toBe(0); + expect(container.lifetimes.size).toBe(0); + }); + }); + + describe('register', () => { + test('should register a singleton service', () => { + const factory = jest.fn(() => ({ id: 1, name: 'Test Service' })); + + container.register('TestService', factory, 'singleton'); + + expect(container.factories.has('TestService')).toBe(true); + expect(container.lifetimes.get('TestService')).toBe('singleton'); + expect(factory).toHaveBeenCalled(); // Singleton is created immediately + expect(container.services.has('TestService')).toBe(true); + }); + + test('should register a transient service', () => { + const factory = jest.fn(() => ({ id: 2, name: 'Transient Service' })); + + container.register('TransientService', factory, 'transient'); + + expect(container.factories.has('TransientService')).toBe(true); + expect(container.lifetimes.get('TransientService')).toBe('transient'); + expect(factory).not.toHaveBeenCalled(); // Transient is NOT created immediately + expect(container.services.has('TransientService')).toBe(false); + }); + + test('should register a scoped service', () => { + const factory = jest.fn(() => ({ id: 3, name: 'Scoped Service' })); + + container.register('ScopedService', factory, 'scoped'); + + expect(container.factories.has('ScopedService')).toBe(true); + expect(container.lifetimes.get('ScopedService')).toBe('scoped'); + expect(factory).not.toHaveBeenCalled(); // Scoped is NOT created immediately + }); + + test('should default to singleton if lifetime not specified', () => { + const factory = () => ({ id: 4 }); + + container.register('DefaultService', factory); + + expect(container.lifetimes.get('DefaultService')).toBe('singleton'); + expect(container.services.has('DefaultService')).toBe(true); + }); + }); + + describe('resolve - singleton', () => { + test('should return the same instance for singleton', () => { + container.register('SingletonService', () => ({ id: Math.random() }), 'singleton'); + + const instance1 = container.resolve('SingletonService'); + const instance2 = container.resolve('SingletonService'); + + expect(instance1).toBe(instance2); + expect(instance1.id).toBe(instance2.id); + }); + + test('should return the pre-created singleton instance', () => { + const mockInstance = { id: 123, name: 'Mock' }; + container.register('PreCreatedService', () => mockInstance, 'singleton'); + + const resolved = container.resolve('PreCreatedService'); + + expect(resolved).toBe(mockInstance); + }); + }); + + describe('resolve - transient', () => { + test('should return different instances for transient', () => { + container.register('TransientService', () => ({ id: Math.random() }), 'transient'); + + const instance1 = container.resolve('TransientService'); + const instance2 = container.resolve('TransientService'); + + expect(instance1).not.toBe(instance2); + expect(instance1.id).not.toBe(instance2.id); + }); + + test('should call factory every time for transient', () => { + const factory = jest.fn(() => ({ id: Math.random() })); + container.register('TransientService', factory, 'transient'); + + container.resolve('TransientService'); + container.resolve('TransientService'); + container.resolve('TransientService'); + + expect(factory).toHaveBeenCalledTimes(3); + }); + }); + + describe('resolve - scoped', () => { + test('should return same instance within the same scope', () => { + container.register('ScopedService', () => ({ id: Math.random() }), 'scoped'); + + const scope = container.createScope(); + const instance1 = scope.resolve('ScopedService'); + const instance2 = scope.resolve('ScopedService'); + + expect(instance1).toBe(instance2); + expect(instance1.id).toBe(instance2.id); + }); + + test('should return different instances for different scopes', () => { + container.register('ScopedService', () => ({ id: Math.random() }), 'scoped'); + + const scope1 = container.createScope(); + const scope2 = container.createScope(); + + const instance1 = scope1.resolve('ScopedService'); + const instance2 = scope2.resolve('ScopedService'); + + expect(instance1).not.toBe(instance2); + expect(instance1.id).not.toBe(instance2.id); + }); + + test('should resolve scoped service with scope parameter', () => { + const scopeMap = new Map(); + container.register('ScopedService', () => ({ id: Math.random() }), 'scoped'); + + const instance1 = container.resolve('ScopedService', scopeMap); + const instance2 = container.resolve('ScopedService', scopeMap); + + expect(instance1).toBe(instance2); + expect(scopeMap.has('ScopedService')).toBe(true); + }); + }); + + describe('resolve - error handling', () => { + test('should throw error for unregistered service', () => { + expect(() => container.resolve('NonExistentService')).toThrow( + "Service 'NonExistentService' is not registered" + ); + }); + + test('should provide clear error message', () => { + try { + container.resolve('MissingService'); + fail('Should have thrown error'); + } catch (error) { + expect(error.message).toContain('MissingService'); + expect(error.message).toContain('not registered'); + } + }); + }); + + describe('createScope', () => { + test('should create a scope with resolve method', () => { + const scope = container.createScope(); + + expect(scope).toHaveProperty('resolve'); + expect(typeof scope.resolve).toBe('function'); + }); + + test('should create independent scopes', () => { + container.register('ScopedService', () => ({ id: Math.random() }), 'scoped'); + + const scope1 = container.createScope(); + const scope2 = container.createScope(); + + const instance1 = scope1.resolve('ScopedService'); + const instance2 = scope2.resolve('ScopedService'); + + expect(instance1.id).not.toBe(instance2.id); + }); + }); + + describe('real-world scenarios', () => { + test('should handle database connection as singleton', () => { + class DatabaseConnection { + constructor() { + this.id = Math.random(); + this.connected = true; + } + } + + container.register('Database', () => new DatabaseConnection(), 'singleton'); + + const db1 = container.resolve('Database'); + const db2 = container.resolve('Database'); + + expect(db1).toBe(db2); + expect(db1.id).toBe(db2.id); + expect(db1.connected).toBe(true); + }); + + test('should handle logger as transient', () => { + class Logger { + constructor() { + this.id = Math.random(); + } + log(msg) { + return `[${this.id}] ${msg}`; + } + } + + container.register('Logger', () => new Logger(), 'transient'); + + const logger1 = container.resolve('Logger'); + const logger2 = container.resolve('Logger'); + + expect(logger1).not.toBe(logger2); + expect(logger1.id).not.toBe(logger2.id); + }); + + test('should handle request context as scoped', () => { + class RequestContext { + constructor() { + this.requestId = Math.random(); + this.user = null; + this.timestamp = Date.now(); + } + } + + container.register('RequestContext', () => new RequestContext(), 'scoped'); + + // Request 1 + const request1Scope = container.createScope(); + const ctx1a = request1Scope.resolve('RequestContext'); + const ctx1b = request1Scope.resolve('RequestContext'); + expect(ctx1a).toBe(ctx1b); + + // Request 2 + const request2Scope = container.createScope(); + const ctx2 = request2Scope.resolve('RequestContext'); + expect(ctx1a).not.toBe(ctx2); + expect(ctx1a.requestId).not.toBe(ctx2.requestId); + }); + + test('should handle dependency chain', () => { + class Repository { + constructor(db) { + this.db = db; + } + } + + class Service { + constructor(repo) { + this.repo = repo; + } + } + + const mockDb = { id: 1, connected: true }; + container.register('Database', () => mockDb, 'singleton'); + container.register('Repository', () => { + return new Repository(container.resolve('Database')); + }, 'singleton'); + container.register('Service', () => { + return new Service(container.resolve('Repository')); + }, 'singleton'); + + const service = container.resolve('Service'); + + expect(service.repo).toBeDefined(); + expect(service.repo.db).toBe(mockDb); + }); + }); + + describe('mixed lifecycle scenarios', () => { + test('should handle mixed singleton and transient', () => { + container.register('Config', () => ({ port: 3000 }), 'singleton'); + container.register('Handler', () => ({ + id: Math.random(), + config: container.resolve('Config') + }), 'transient'); + + const handler1 = container.resolve('Handler'); + const handler2 = container.resolve('Handler'); + + // Different handlers + expect(handler1).not.toBe(handler2); + expect(handler1.id).not.toBe(handler2.id); + + // But same config + expect(handler1.config).toBe(handler2.config); + }); + + test('should handle mixed singleton and scoped', () => { + container.register('Database', () => ({ id: 'db' }), 'singleton'); + container.register('RequestData', () => ({ + id: Math.random() + }), 'scoped'); + + const scope1 = container.createScope(); + const scope2 = container.createScope(); + + const data1a = scope1.resolve('RequestData'); + const data1b = scope1.resolve('RequestData'); + const data2 = scope2.resolve('RequestData'); + + expect(data1a).toBe(data1b); // Same within scope + expect(data1a).not.toBe(data2); // Different across scopes + }); + }); +}); diff --git a/Backend/negyedik gyakorlat/tests/unit/commands/LoginUserCommandHandler.test.js b/Backend/negyedik gyakorlat/tests/unit/commands/LoginUserCommandHandler.test.js new file mode 100644 index 0000000..ed9982b --- /dev/null +++ b/Backend/negyedik gyakorlat/tests/unit/commands/LoginUserCommandHandler.test.js @@ -0,0 +1,117 @@ +const LoginUserCommandHandler = require('../../../src/application/auth/commands/LoginUserCommandHandler'); +const LoginUserCommand = require('../../../src/application/auth/commands/LoginUserCommand'); +const bcrypt = require('bcryptjs'); +const JwtService = require('../../../src/application/services/JwtService'); + +// Mock dependencies +jest.mock('bcryptjs'); +jest.mock('../../../src/application/services/JwtService'); + +describe('LoginUserCommandHandler', () => { + let handler; + let mockPrisma; + let mockJwtService; + + beforeEach(() => { + // Mock JwtService instance + mockJwtService = { + generateToken: jest.fn() + }; + JwtService.mockImplementation(() => mockJwtService); + + // Mock Prisma + mockPrisma = { + user: { + findUnique: jest.fn() + } + }; + + handler = new LoginUserCommandHandler(mockPrisma); + + // Reset all mocks + jest.clearAllMocks(); + }); + + describe('handle - success cases', () => { + it('should login user successfully with valid credentials', async () => { + // Arrange + const command = new LoginUserCommand('john@example.com', 'password123'); + + mockPrisma.user.findUnique.mockResolvedValue({ + id: 1, + name: 'John Doe', + email: 'john@example.com', + password: 'hashed_password', + createdAt: new Date(), + updatedAt: new Date() + }); + bcrypt.compare.mockResolvedValue(true); // Password is valid + mockJwtService.generateToken.mockReturnValue('mock_jwt_token'); + + // Act + const result = await handler.handle(command); + + // Assert + expect(mockPrisma.user.findUnique).toHaveBeenCalledWith({ + where: { email: 'john@example.com' } + }); + expect(bcrypt.compare).toHaveBeenCalledWith('password123', 'hashed_password'); + expect(mockJwtService.generateToken).toHaveBeenCalledWith({ + userId: 1, + email: 'john@example.com' + }); + expect(result.user).toEqual({ + id: 1, + name: 'John Doe', + email: 'john@example.com', + createdAt: expect.any(Date), + updatedAt: expect.any(Date) + }); + expect(result.token).toBe('mock_jwt_token'); + expect(result.user.password).toBeUndefined(); // Password should not be returned + }); + }); + + describe('handle - validation errors', () => { + it('should throw error if email is missing', async () => { + // Arrange + const command = new LoginUserCommand('', 'password123'); + + // Act & Assert + await expect(handler.handle(command)).rejects.toThrow('Email and password are required'); + }); + + it('should throw error if password is missing', async () => { + // Arrange + const command = new LoginUserCommand('john@example.com', ''); + + // Act & Assert + await expect(handler.handle(command)).rejects.toThrow('Email and password are required'); + }); + + it('should throw error if user does not exist', async () => { + // Arrange + const command = new LoginUserCommand('nonexistent@example.com', 'password123'); + + mockPrisma.user.findUnique.mockResolvedValue(null); + + // Act & Assert + await expect(handler.handle(command)).rejects.toThrow('Invalid email or password'); + }); + + it('should throw error if password is incorrect', async () => { + // Arrange + const command = new LoginUserCommand('john@example.com', 'wrongpassword'); + + mockPrisma.user.findUnique.mockResolvedValue({ + id: 1, + email: 'john@example.com', + password: 'hashed_password' + }); + bcrypt.compare.mockResolvedValue(false); // Password is invalid + + // Act & Assert + await expect(handler.handle(command)).rejects.toThrow('Invalid email or password'); + }); + }); +}); diff --git a/Backend/negyedik gyakorlat/tests/unit/commands/RegisterUserCommandHandler.test.js b/Backend/negyedik gyakorlat/tests/unit/commands/RegisterUserCommandHandler.test.js new file mode 100644 index 0000000..8fa3998 --- /dev/null +++ b/Backend/negyedik gyakorlat/tests/unit/commands/RegisterUserCommandHandler.test.js @@ -0,0 +1,194 @@ +const RegisterUserCommandHandler = require('../../../src/application/auth/commands/RegisterUserCommandHandler'); +const RegisterUserCommand = require('../../../src/application/auth/commands/RegisterUserCommand'); +const bcrypt = require('bcryptjs'); +const JwtService = require('../../../src/application/services/JwtService'); + +// Mock dependencies +jest.mock('bcryptjs'); +jest.mock('../../../src/application/services/JwtService'); + +describe('RegisterUserCommandHandler', () => { + let handler; + let mockPrisma; + let mockEmailService; + let mockJwtService; + + beforeEach(() => { + // Mock JwtService instance + mockJwtService = { + generateToken: jest.fn() + }; + JwtService.mockImplementation(() => mockJwtService); + + // Mock Prisma + mockPrisma = { + user: { + findUnique: jest.fn(), + create: jest.fn() + } + }; + + // Mock EmailService + mockEmailService = { + sendWelcomeEmail: jest.fn().mockResolvedValue(true) + }; + + handler = new RegisterUserCommandHandler(mockPrisma, mockEmailService); + + // Reset all mocks + jest.clearAllMocks(); + }); + + describe('handle - success cases', () => { + it('should register a new user successfully', async () => { + // Arrange + const command = new RegisterUserCommand('John Doe', 'john@example.com', 'password123'); + + mockPrisma.user.findUnique.mockResolvedValue(null); // No existing user + bcrypt.hash.mockResolvedValue('hashed_password'); + mockPrisma.user.create.mockResolvedValue({ + id: 1, + name: 'John Doe', + email: 'john@example.com', + password: 'hashed_password', + createdAt: new Date(), + updatedAt: new Date() + }); + mockJwtService.generateToken.mockReturnValue('mock_jwt_token'); + + // Act + const result = await handler.handle(command); + + // Assert + expect(mockPrisma.user.findUnique).toHaveBeenCalledWith({ + where: { email: 'john@example.com' } + }); + expect(bcrypt.hash).toHaveBeenCalledWith('password123', 10); + expect(mockPrisma.user.create).toHaveBeenCalledWith({ + data: { + name: 'John Doe', + email: 'john@example.com', + password: 'hashed_password' + } + }); + expect(mockJwtService.generateToken).toHaveBeenCalledWith({ + userId: 1, + email: 'john@example.com' + }); + expect(result.user).toEqual({ + id: 1, + name: 'John Doe', + email: 'john@example.com', + createdAt: expect.any(Date), + updatedAt: expect.any(Date) + }); + expect(result.token).toBe('mock_jwt_token'); + expect(result.user.password).toBeUndefined(); // Password should not be returned + }); + + it('should send welcome email after registration', async () => { + // Arrange + const command = new RegisterUserCommand('Jane Doe', 'jane@example.com', 'password123'); + + mockPrisma.user.findUnique.mockResolvedValue(null); + bcrypt.hash.mockResolvedValue('hashed_password'); + mockPrisma.user.create.mockResolvedValue({ + id: 2, + name: 'Jane Doe', + email: 'jane@example.com', + password: 'hashed_password', + createdAt: new Date(), + updatedAt: new Date() + }); + mockJwtService.generateToken.mockReturnValue('mock_jwt_token'); + + // Act + await handler.handle(command); + + // Assert + expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledWith('jane@example.com', 'Jane Doe'); + }); + }); + + describe('handle - validation errors', () => { + it('should throw error if name is missing', async () => { + // Arrange + const command = new RegisterUserCommand('', 'john@example.com', 'password123'); + + // Act & Assert + await expect(handler.handle(command)).rejects.toThrow('Name, email and password are required'); + }); + + it('should throw error if email is missing', async () => { + // Arrange + const command = new RegisterUserCommand('John Doe', '', 'password123'); + + // Act & Assert + await expect(handler.handle(command)).rejects.toThrow('Name, email and password are required'); + }); + + it('should throw error if password is missing', async () => { + // Arrange + const command = new RegisterUserCommand('John Doe', 'john@example.com', ''); + + // Act & Assert + await expect(handler.handle(command)).rejects.toThrow('Name, email and password are required'); + }); + + it('should throw error if password is too short', async () => { + // Arrange + const command = new RegisterUserCommand('John Doe', 'john@example.com', '12345'); + + // Act & Assert + await expect(handler.handle(command)).rejects.toThrow('Password must be at least 6 characters long'); + }); + + it('should throw error if email format is invalid', async () => { + // Arrange + const command = new RegisterUserCommand('John Doe', 'invalid-email', 'password123'); + + // Act & Assert + await expect(handler.handle(command)).rejects.toThrow('Invalid email format'); + }); + + it('should throw error if user already exists', async () => { + // Arrange + const command = new RegisterUserCommand('John Doe', 'john@example.com', 'password123'); + + mockPrisma.user.findUnique.mockResolvedValue({ + id: 1, + email: 'john@example.com' + }); + + // Act & Assert + await expect(handler.handle(command)).rejects.toThrow('User with this email already exists'); + }); + }); + + describe('handle - error handling', () => { + it('should not fail if email service throws error', async () => { + // Arrange + const command = new RegisterUserCommand('John Doe', 'john@example.com', 'password123'); + + mockPrisma.user.findUnique.mockResolvedValue(null); + bcrypt.hash.mockResolvedValue('hashed_password'); + mockPrisma.user.create.mockResolvedValue({ + id: 1, + name: 'John Doe', + email: 'john@example.com', + password: 'hashed_password', + createdAt: new Date(), + updatedAt: new Date() + }); + mockJwtService.generateToken.mockReturnValue('mock_jwt_token'); + mockEmailService.sendWelcomeEmail.mockRejectedValue(new Error('Email service error')); + + // Act + const result = await handler.handle(command); + + // Assert - should still return user and token even if email fails + expect(result.user.email).toBe('john@example.com'); + expect(result.token).toBe('mock_jwt_token'); + }); + }); +}); diff --git a/Backend/negyedik gyakorlat/tests/unit/commands/UpdateUserProfileCommandHandler.test.js b/Backend/negyedik gyakorlat/tests/unit/commands/UpdateUserProfileCommandHandler.test.js new file mode 100644 index 0000000..7fca179 --- /dev/null +++ b/Backend/negyedik gyakorlat/tests/unit/commands/UpdateUserProfileCommandHandler.test.js @@ -0,0 +1,80 @@ +const UpdateUserProfileCommandHandler = require('../../../src/application/user/commands/UpdateUserProfileCommandHandler'); +const UpdateUserProfileCommand = require('../../../src/application/user/commands/UpdateUserProfileCommand'); + +describe('UpdateUserProfileCommandHandler', () => { + let handler; + let mockPrisma; + + beforeEach(() => { + // Mock Prisma + mockPrisma = { + user: { + update: jest.fn() + } + }; + + handler = new UpdateUserProfileCommandHandler(mockPrisma); + + // Reset all mocks + jest.clearAllMocks(); + }); + + describe('handle - success cases', () => { + it('should update user profile successfully', async () => { + // Arrange + const command = new UpdateUserProfileCommand(1, 'Jane Updated'); + + mockPrisma.user.update.mockResolvedValue({ + id: 1, + name: 'Jane Updated', + email: 'jane@example.com', + password: 'hashed_password', + createdAt: new Date(), + updatedAt: new Date() + }); + + // Act + const result = await handler.handle(command); + + // Assert + expect(mockPrisma.user.update).toHaveBeenCalledWith({ + where: { id: 1 }, + data: { name: 'Jane Updated' } + }); + expect(result).toEqual({ + id: 1, + name: 'Jane Updated', + email: 'jane@example.com', + createdAt: expect.any(Date), + updatedAt: expect.any(Date) + }); + expect(result.password).toBeUndefined(); // Password should not be returned + }); + }); + + describe('handle - validation errors', () => { + it('should throw error if name is missing', async () => { + // Arrange + const command = new UpdateUserProfileCommand(1, ''); + + // Act & Assert + await expect(handler.handle(command)).rejects.toThrow('Name is required'); + }); + + it('should throw error if name is null', async () => { + // Arrange + const command = new UpdateUserProfileCommand(1, null); + + // Act & Assert + await expect(handler.handle(command)).rejects.toThrow('Name is required'); + }); + + it('should throw error if name is undefined', async () => { + // Arrange + const command = new UpdateUserProfileCommand(1, undefined); + + // Act & Assert + await expect(handler.handle(command)).rejects.toThrow('Name is required'); + }); + }); +}); diff --git a/Backend/negyedik gyakorlat/tests/unit/controllers/AuthController.test.js b/Backend/negyedik gyakorlat/tests/unit/controllers/AuthController.test.js new file mode 100644 index 0000000..247ed55 --- /dev/null +++ b/Backend/negyedik gyakorlat/tests/unit/controllers/AuthController.test.js @@ -0,0 +1,194 @@ +const AuthController = require('../../../src/api/controllers/AuthController'); +const RegisterUserCommand = require('../../../src/application/auth/commands/RegisterUserCommand'); +const LoginUserCommand = require('../../../src/application/auth/commands/LoginUserCommand'); + +describe('AuthController', () => { + let controller; + let mockRegisterHandler; + let mockLoginHandler; + let mockReq; + let mockRes; + + beforeEach(() => { + // Mock handlers + mockRegisterHandler = { + handle: jest.fn() + }; + mockLoginHandler = { + handle: jest.fn() + }; + + controller = new AuthController(mockRegisterHandler, mockLoginHandler); + + // Mock Express req/res + mockReq = { + body: {} + }; + mockRes = { + status: jest.fn().mockReturnThis(), + json: jest.fn() + }; + + // Reset all mocks + jest.clearAllMocks(); + }); + + describe('register', () => { + it('should register user successfully and return 201', async () => { + // Arrange + mockReq.body = { + name: 'John Doe', + email: 'john@example.com', + password: 'password123' + }; + + const mockResult = { + user: { id: 1, name: 'John Doe', email: 'john@example.com' }, + token: 'mock_jwt_token' + }; + + mockRegisterHandler.handle.mockResolvedValue(mockResult); + + // Act + await controller.register(mockReq, mockRes); + + // Assert + expect(mockRegisterHandler.handle).toHaveBeenCalledWith( + expect.any(RegisterUserCommand) + ); + expect(mockRes.status).toHaveBeenCalledWith(201); + expect(mockRes.json).toHaveBeenCalledWith({ + message: 'User registered successfully', + data: mockResult + }); + }); + + it('should return 400 for validation errors', async () => { + // Arrange + mockReq.body = { + name: '', + email: 'john@example.com', + password: 'password123' + }; + + mockRegisterHandler.handle.mockRejectedValue(new Error('Name, email and password are required')); + + // Act + await controller.register(mockReq, mockRes); + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(400); + expect(mockRes.json).toHaveBeenCalledWith({ + error: 'Name, email and password are required' + }); + }); + + it('should return 400 if user already exists', async () => { + // Arrange + mockReq.body = { + name: 'John Doe', + email: 'john@example.com', + password: 'password123' + }; + + mockRegisterHandler.handle.mockRejectedValue(new Error('User with this email already exists')); + + // Act + await controller.register(mockReq, mockRes); + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(400); + expect(mockRes.json).toHaveBeenCalledWith({ + error: 'User with this email already exists' + }); + }); + + it('should return 500 for unexpected errors', async () => { + // Arrange + mockReq.body = { + name: 'John Doe', + email: 'john@example.com', + password: 'password123' + }; + + mockRegisterHandler.handle.mockRejectedValue(new Error('Database connection failed')); + + // Act + await controller.register(mockReq, mockRes); + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(500); + expect(mockRes.json).toHaveBeenCalledWith({ + error: 'Database connection failed' + }); + }); + }); + + describe('login', () => { + it('should login user successfully and return 200', async () => { + // Arrange + mockReq.body = { + email: 'john@example.com', + password: 'password123' + }; + + const mockResult = { + user: { id: 1, name: 'John Doe', email: 'john@example.com' }, + token: 'mock_jwt_token' + }; + + mockLoginHandler.handle.mockResolvedValue(mockResult); + + // Act + await controller.login(mockReq, mockRes); + + // Assert + expect(mockLoginHandler.handle).toHaveBeenCalledWith( + expect.any(LoginUserCommand) + ); + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockRes.json).toHaveBeenCalledWith({ + message: 'Login successful', + data: mockResult + }); + }); + + it('should return 401 for invalid credentials', async () => { + // Arrange + mockReq.body = { + email: 'john@example.com', + password: 'wrongpassword' + }; + + mockLoginHandler.handle.mockRejectedValue(new Error('Invalid email or password')); + + // Act + await controller.login(mockReq, mockRes); + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(401); + expect(mockRes.json).toHaveBeenCalledWith({ + error: 'Invalid email or password' + }); + }); + + it('should return 401 for missing credentials', async () => { + // Arrange + mockReq.body = { + email: '', + password: '' + }; + + mockLoginHandler.handle.mockRejectedValue(new Error('Email and password are required')); + + // Act + await controller.login(mockReq, mockRes); + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(401); + expect(mockRes.json).toHaveBeenCalledWith({ + error: 'Email and password are required' + }); + }); + }); +}); diff --git a/Backend/negyedik gyakorlat/tests/unit/controllers/UserController.test.js b/Backend/negyedik gyakorlat/tests/unit/controllers/UserController.test.js new file mode 100644 index 0000000..dd71da2 --- /dev/null +++ b/Backend/negyedik gyakorlat/tests/unit/controllers/UserController.test.js @@ -0,0 +1,223 @@ +const UserController = require('../../../src/api/controllers/UserController'); +const GetMeQuery = require('../../../src/application/user/queries/GetMeQuery'); +const GetAllUsersQuery = require('../../../src/application/user/queries/GetAllUsersQuery'); +const GetUserByIdQuery = require('../../../src/application/user/queries/GetUserByIdQuery'); +const UpdateUserProfileCommand = require('../../../src/application/user/commands/UpdateUserProfileCommand'); + +describe('UserController', () => { + let controller; + let mockGetMeHandler; + let mockGetAllUsersHandler; + let mockGetUserByIdHandler; + let mockUpdateProfileHandler; + let mockReq; + let mockRes; + + beforeEach(() => { + // Mock handlers + mockGetMeHandler = { handle: jest.fn() }; + mockGetAllUsersHandler = { handle: jest.fn() }; + mockGetUserByIdHandler = { handle: jest.fn() }; + mockUpdateProfileHandler = { handle: jest.fn() }; + + controller = new UserController( + mockGetMeHandler, + mockGetAllUsersHandler, + mockGetUserByIdHandler, + mockUpdateProfileHandler + ); + + // Mock Express req/res + mockReq = { + user: { userId: 1 }, // Set by authMiddleware + body: {}, + params: {} + }; + mockRes = { + status: jest.fn().mockReturnThis(), + json: jest.fn() + }; + + // Reset all mocks + jest.clearAllMocks(); + }); + + describe('getMe', () => { + it('should return current user successfully', async () => { + // Arrange + const mockUser = { + id: 1, + name: 'John Doe', + email: 'john@example.com' + }; + + mockGetMeHandler.handle.mockResolvedValue(mockUser); + + // Act + await controller.getMe(mockReq, mockRes); + + // Assert + expect(mockGetMeHandler.handle).toHaveBeenCalledWith( + expect.any(GetMeQuery) + ); + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockRes.json).toHaveBeenCalledWith({ + message: 'User retrieved successfully', + data: mockUser + }); + }); + + it('should return 404 if user not found', async () => { + // Arrange + mockGetMeHandler.handle.mockRejectedValue(new Error('User not found')); + + // Act + await controller.getMe(mockReq, mockRes); + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(404); + expect(mockRes.json).toHaveBeenCalledWith({ + error: 'User not found' + }); + }); + }); + + describe('getAll', () => { + it('should return all users successfully', async () => { + // Arrange + const mockUsers = [ + { id: 1, name: 'John Doe', email: 'john@example.com' }, + { id: 2, name: 'Jane Doe', email: 'jane@example.com' } + ]; + + mockGetAllUsersHandler.handle.mockResolvedValue(mockUsers); + + // Act + await controller.getAll(mockReq, mockRes); + + // Assert + expect(mockGetAllUsersHandler.handle).toHaveBeenCalledWith( + expect.any(GetAllUsersQuery) + ); + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockRes.json).toHaveBeenCalledWith({ + message: 'Users retrieved successfully', + data: mockUsers, + count: 2 + }); + }); + + it('should return empty array if no users exist', async () => { + // Arrange + mockGetAllUsersHandler.handle.mockResolvedValue([]); + + // Act + await controller.getAll(mockReq, mockRes); + + // Assert + expect(mockRes.json).toHaveBeenCalledWith({ + message: 'Users retrieved successfully', + data: [], + count: 0 + }); + }); + }); + + describe('getById', () => { + it('should return user by ID successfully', async () => { + // Arrange + mockReq.params = { id: '2' }; + const mockUser = { + id: 2, + name: 'Jane Doe', + email: 'jane@example.com' + }; + + mockGetUserByIdHandler.handle.mockResolvedValue(mockUser); + + // Act + await controller.getById(mockReq, mockRes); + + // Assert + expect(mockGetUserByIdHandler.handle).toHaveBeenCalledWith( + expect.any(GetUserByIdQuery) + ); + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockRes.json).toHaveBeenCalledWith({ + message: 'User retrieved successfully', + data: mockUser + }); + }); + + it('should return 400 for invalid user ID', async () => { + // Arrange + mockReq.params = { id: 'invalid' }; + + // Act + await controller.getById(mockReq, mockRes); + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(400); + expect(mockRes.json).toHaveBeenCalledWith({ + error: 'Invalid user ID' + }); + }); + + it('should return 404 if user not found', async () => { + // Arrange + mockReq.params = { id: '999' }; + mockGetUserByIdHandler.handle.mockRejectedValue(new Error('User not found')); + + // Act + await controller.getById(mockReq, mockRes); + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(404); + expect(mockRes.json).toHaveBeenCalledWith({ + error: 'User not found' + }); + }); + }); + + describe('updateMe', () => { + it('should update user profile successfully', async () => { + // Arrange + mockReq.body = { name: 'John Updated' }; + const mockUpdatedUser = { + id: 1, + name: 'John Updated', + email: 'john@example.com' + }; + + mockUpdateProfileHandler.handle.mockResolvedValue(mockUpdatedUser); + + // Act + await controller.updateMe(mockReq, mockRes); + + // Assert + expect(mockUpdateProfileHandler.handle).toHaveBeenCalledWith( + expect.any(UpdateUserProfileCommand) + ); + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockRes.json).toHaveBeenCalledWith({ + message: 'Profile updated successfully', + data: mockUpdatedUser + }); + }); + + it('should return 400 if name is missing', async () => { + // Arrange + mockReq.body = { name: '' }; + mockUpdateProfileHandler.handle.mockRejectedValue(new Error('Name is required')); + + // Act + await controller.updateMe(mockReq, mockRes); + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(400); + expect(mockRes.json).toHaveBeenCalledWith({ + error: 'Name is required' + }); + }); + }); +}); diff --git a/Backend/negyedik gyakorlat/tests/unit/middlewares/authMiddleware.test.js b/Backend/negyedik gyakorlat/tests/unit/middlewares/authMiddleware.test.js new file mode 100644 index 0000000..3959272 --- /dev/null +++ b/Backend/negyedik gyakorlat/tests/unit/middlewares/authMiddleware.test.js @@ -0,0 +1,123 @@ +const authMiddleware = require('../../../src/api/middlewares/authMiddleware'); +const JwtService = require('../../../src/application/services/JwtService'); + +// Mock JwtService +jest.mock('../../../src/application/services/JwtService'); + +describe('authMiddleware (Cookie-based)', () => { + let mockReq; + let mockRes; + let mockNext; + let mockJwtService; + + beforeEach(() => { + // Mock Express req/res/next + mockReq = { + cookies: {} + }; + mockRes = { + status: jest.fn().mockReturnThis(), + json: jest.fn() + }; + mockNext = jest.fn(); + + // Mock JwtService instance + mockJwtService = { + extractTokenFromCookies: jest.fn(), + verifyToken: jest.fn() + }; + + JwtService.mockImplementation(() => mockJwtService); + + // Reset all mocks + jest.clearAllMocks(); + }); + + describe('successful authentication', () => { + it('should authenticate valid JWT token from cookie and call next()', () => { + // Arrange + mockReq.cookies = { auth_token: 'valid_token_123' }; + + const mockDecoded = { + userId: 1, + email: 'john@example.com' + }; + + mockJwtService.extractTokenFromCookies.mockReturnValue('valid_token_123'); + mockJwtService.verifyToken.mockReturnValue(mockDecoded); + + // Act + authMiddleware(mockReq, mockRes, mockNext); + + // Assert + expect(mockReq.user).toEqual({ + userId: 1, + email: 'john@example.com' + }); + expect(mockNext).toHaveBeenCalled(); + expect(mockRes.status).not.toHaveBeenCalled(); + }); + }); + + describe('authentication failures', () => { + it('should return 401 if no cookie is present', () => { + // Arrange + mockReq.cookies = {}; + + mockJwtService.extractTokenFromCookies.mockReturnValue(null); + + // Act + authMiddleware(mockReq, mockRes, mockNext); + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(401); + expect(mockRes.json).toHaveBeenCalledWith({ + error: 'Authentication required', + message: 'No token provided in cookies' + }); + expect(mockNext).not.toHaveBeenCalled(); + }); + + it('should return 401 if cookie token is invalid', () => { + // Arrange + mockReq.cookies = { auth_token: 'invalid_token' }; + + mockJwtService.extractTokenFromCookies.mockReturnValue('invalid_token'); + mockJwtService.verifyToken.mockImplementation(() => { + throw new Error('Invalid or expired token'); + }); + + // Act + authMiddleware(mockReq, mockRes, mockNext); + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(401); + expect(mockRes.json).toHaveBeenCalledWith({ + error: 'Authentication failed', + message: 'Invalid or expired token' + }); + expect(mockNext).not.toHaveBeenCalled(); + }); + + it('should return 401 if token is expired', () => { + // Arrange + mockReq.cookies = { auth_token: 'expired_token' }; + + mockJwtService.extractTokenFromCookies.mockReturnValue('expired_token'); + mockJwtService.verifyToken.mockImplementation(() => { + throw new Error('Token has expired'); + }); + + // Act + authMiddleware(mockReq, mockRes, mockNext); + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(401); + expect(mockRes.json).toHaveBeenCalledWith({ + error: 'Authentication failed', + message: 'Token has expired' + }); + expect(mockNext).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/Backend/negyedik gyakorlat/tests/unit/middlewares/corsMiddleware.test.js b/Backend/negyedik gyakorlat/tests/unit/middlewares/corsMiddleware.test.js new file mode 100644 index 0000000..bea3a29 --- /dev/null +++ b/Backend/negyedik gyakorlat/tests/unit/middlewares/corsMiddleware.test.js @@ -0,0 +1,156 @@ +const corsMiddleware = require('../../../src/api/middlewares/corsMiddleware'); + +describe('CORS Middleware', () => { + let req, res, next; + + beforeEach(() => { + req = { + headers: {}, + method: 'GET', + }; + res = { + setHeader: jest.fn(), + status: jest.fn().mockReturnThis(), + end: jest.fn(), + }; + next = jest.fn(); + }); + + describe('Allowed origins', () => { + test('should allow requests from http://localhost:3001', () => { + req.headers.origin = 'http://localhost:3001'; + + corsMiddleware(req, res, next); + + expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Origin', 'http://localhost:3001'); + expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Credentials', 'true'); + expect(next).toHaveBeenCalled(); + }); + + test('should allow requests from http://localhost:3000', () => { + req.headers.origin = 'http://localhost:3000'; + + corsMiddleware(req, res, next); + + expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Origin', 'http://localhost:3000'); + expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Credentials', 'true'); + expect(next).toHaveBeenCalled(); + }); + + test('should allow requests from https://myapp.com', () => { + req.headers.origin = 'https://myapp.com'; + + corsMiddleware(req, res, next); + + expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Origin', 'https://myapp.com'); + expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Credentials', 'true'); + expect(next).toHaveBeenCalled(); + }); + }); + + describe('Disallowed origins', () => { + test('should reject requests from unknown origins', () => { + req.headers.origin = 'http://malicious-site.com'; + + corsMiddleware(req, res, next); + + expect(res.status).toHaveBeenCalledWith(403); + expect(res.end).toHaveBeenCalledWith('CORS policy: Origin not allowed'); + expect(next).not.toHaveBeenCalled(); + }); + + test('should reject requests from http://evil.com', () => { + req.headers.origin = 'http://evil.com'; + + corsMiddleware(req, res, next); + + expect(res.status).toHaveBeenCalledWith(403); + expect(res.end).toHaveBeenCalledWith('CORS policy: Origin not allowed'); + expect(next).not.toHaveBeenCalled(); + }); + }); + + describe('Preflight requests (OPTIONS)', () => { + beforeEach(() => { + req.method = 'OPTIONS'; + }); + + test('should handle OPTIONS request from allowed origin', () => { + req.headers.origin = 'http://localhost:3001'; + + corsMiddleware(req, res, next); + + expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Origin', 'http://localhost:3001'); + expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Credentials', 'true'); + expect(res.status).toHaveBeenCalledWith(204); + expect(res.end).toHaveBeenCalled(); + expect(next).not.toHaveBeenCalled(); + }); + + test('should reject OPTIONS request from disallowed origin', () => { + req.headers.origin = 'http://hackersite.com'; + + corsMiddleware(req, res, next); + + expect(res.status).toHaveBeenCalledWith(403); + expect(res.end).toHaveBeenCalledWith('CORS policy: Origin not allowed'); + expect(next).not.toHaveBeenCalled(); + }); + }); + + describe('No origin header', () => { + test('should allow requests without origin header (same-origin)', () => { + delete req.headers.origin; + + corsMiddleware(req, res, next); + + expect(next).toHaveBeenCalled(); + }); + }); + + describe('Headers configuration', () => { + test('should set correct CORS headers for allowed origin', () => { + req.headers.origin = 'http://localhost:3000'; + + corsMiddleware(req, res, next); + + expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Origin', 'http://localhost:3000'); + expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Credentials', 'true'); + }); + + test('should set method headers for OPTIONS request', () => { + req.headers.origin = 'http://localhost:3000'; + req.method = 'OPTIONS'; + + corsMiddleware(req, res, next); + + expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + }); + }); + + describe('Real-world scenarios', () => { + test('should handle POST request from allowed frontend', () => { + req.headers.origin = 'http://localhost:3001'; + req.method = 'POST'; + + corsMiddleware(req, res, next); + + expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Origin', 'http://localhost:3001'); + expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Credentials', 'true'); + expect(next).toHaveBeenCalled(); + }); + + test('should reject DELETE request from disallowed origin', () => { + req.headers.origin = 'http://unauthorized.com'; + req.method = 'DELETE'; + + corsMiddleware(req, res, next); + + expect(res.status).toHaveBeenCalledWith(403); + expect(next).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/Backend/negyedik gyakorlat/tests/unit/queries/GetAllUsersQueryHandler.test.js b/Backend/negyedik gyakorlat/tests/unit/queries/GetAllUsersQueryHandler.test.js new file mode 100644 index 0000000..b6eb1b5 --- /dev/null +++ b/Backend/negyedik gyakorlat/tests/unit/queries/GetAllUsersQueryHandler.test.js @@ -0,0 +1,86 @@ +const GetAllUsersQueryHandler = require('../../../src/application/user/queries/GetAllUsersQueryHandler'); +const GetAllUsersQuery = require('../../../src/application/user/queries/GetAllUsersQuery'); + +describe('GetAllUsersQueryHandler', () => { + let handler; + let mockPrisma; + + beforeEach(() => { + // Mock Prisma + mockPrisma = { + user: { + findMany: jest.fn() + } + }; + + handler = new GetAllUsersQueryHandler(mockPrisma); + + // Reset all mocks + jest.clearAllMocks(); + }); + + describe('handle - success cases', () => { + it('should return all users successfully', async () => { + // Arrange + const query = new GetAllUsersQuery(); + + mockPrisma.user.findMany.mockResolvedValue([ + { + id: 1, + name: 'John Doe', + email: 'john@example.com', + password: 'hashed_password_1', + createdAt: new Date(), + updatedAt: new Date() + }, + { + id: 2, + name: 'Jane Doe', + email: 'jane@example.com', + password: 'hashed_password_2', + createdAt: new Date(), + updatedAt: new Date() + } + ]); + + // Act + const result = await handler.handle(query); + + // Assert + expect(mockPrisma.user.findMany).toHaveBeenCalledWith({ + orderBy: { createdAt: 'desc' } + }); + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ + id: 1, + name: 'John Doe', + email: 'john@example.com', + createdAt: expect.any(Date), + updatedAt: expect.any(Date) + }); + expect(result[1]).toEqual({ + id: 2, + name: 'Jane Doe', + email: 'jane@example.com', + createdAt: expect.any(Date), + updatedAt: expect.any(Date) + }); + // Passwords should not be returned + expect(result[0].password).toBeUndefined(); + expect(result[1].password).toBeUndefined(); + }); + + it('should return empty array if no users exist', async () => { + // Arrange + const query = new GetAllUsersQuery(); + + mockPrisma.user.findMany.mockResolvedValue([]); + + // Act + const result = await handler.handle(query); + + // Assert + expect(result).toEqual([]); + }); + }); +}); diff --git a/Backend/negyedik gyakorlat/tests/unit/queries/GetMeQueryHandler.test.js b/Backend/negyedik gyakorlat/tests/unit/queries/GetMeQueryHandler.test.js new file mode 100644 index 0000000..b5481e7 --- /dev/null +++ b/Backend/negyedik gyakorlat/tests/unit/queries/GetMeQueryHandler.test.js @@ -0,0 +1,65 @@ +const GetMeQueryHandler = require('../../../src/application/user/queries/GetMeQueryHandler'); +const GetMeQuery = require('../../../src/application/user/queries/GetMeQuery'); + +describe('GetMeQueryHandler', () => { + let handler; + let mockPrisma; + + beforeEach(() => { + // Mock Prisma + mockPrisma = { + user: { + findUnique: jest.fn() + } + }; + + handler = new GetMeQueryHandler(mockPrisma); + + // Reset all mocks + jest.clearAllMocks(); + }); + + describe('handle - success cases', () => { + it('should return current user successfully', async () => { + // Arrange + const query = new GetMeQuery(1); + + mockPrisma.user.findUnique.mockResolvedValue({ + id: 1, + name: 'John Doe', + email: 'john@example.com', + password: 'hashed_password', + createdAt: new Date(), + updatedAt: new Date() + }); + + // Act + const result = await handler.handle(query); + + // Assert + expect(mockPrisma.user.findUnique).toHaveBeenCalledWith({ + where: { id: 1 } + }); + expect(result).toEqual({ + id: 1, + name: 'John Doe', + email: 'john@example.com', + createdAt: expect.any(Date), + updatedAt: expect.any(Date) + }); + expect(result.password).toBeUndefined(); // Password should not be returned + }); + }); + + describe('handle - error cases', () => { + it('should throw error if user not found', async () => { + // Arrange + const query = new GetMeQuery(999); + + mockPrisma.user.findUnique.mockResolvedValue(null); + + // Act & Assert + await expect(handler.handle(query)).rejects.toThrow('User not found'); + }); + }); +}); diff --git a/Backend/negyedik gyakorlat/tests/unit/queries/GetUserByIdQueryHandler.test.js b/Backend/negyedik gyakorlat/tests/unit/queries/GetUserByIdQueryHandler.test.js new file mode 100644 index 0000000..53a349f --- /dev/null +++ b/Backend/negyedik gyakorlat/tests/unit/queries/GetUserByIdQueryHandler.test.js @@ -0,0 +1,91 @@ +const GetUserByIdQueryHandler = require('../../../src/application/user/queries/GetUserByIdQueryHandler'); +const GetUserByIdQuery = require('../../../src/application/user/queries/GetUserByIdQuery'); + +describe('GetUserByIdQueryHandler', () => { + let handler; + let mockPrisma; + + beforeEach(() => { + // Mock Prisma + mockPrisma = { + user: { + findUnique: jest.fn() + } + }; + + handler = new GetUserByIdQueryHandler(mockPrisma); + + // Reset all mocks + jest.clearAllMocks(); + }); + + describe('handle - success cases', () => { + it('should return user by ID successfully', async () => { + // Arrange + const query = new GetUserByIdQuery(1); + + mockPrisma.user.findUnique.mockResolvedValue({ + id: 1, + name: 'John Doe', + email: 'john@example.com', + password: 'hashed_password', + createdAt: new Date(), + updatedAt: new Date() + }); + + // Act + const result = await handler.handle(query); + + // Assert + expect(mockPrisma.user.findUnique).toHaveBeenCalledWith({ + where: { id: 1 } + }); + expect(result).toEqual({ + id: 1, + name: 'John Doe', + email: 'john@example.com', + createdAt: expect.any(Date), + updatedAt: expect.any(Date) + }); + expect(result.password).toBeUndefined(); // Password should not be returned + }); + }); + + describe('handle - validation errors', () => { + it('should throw error if userId is invalid (NaN)', async () => { + // Arrange + const query = new GetUserByIdQuery('invalid'); + + // Act & Assert + await expect(handler.handle(query)).rejects.toThrow('Valid user ID is required'); + }); + + it('should throw error if userId is null', async () => { + // Arrange + const query = new GetUserByIdQuery(null); + + // Act & Assert + await expect(handler.handle(query)).rejects.toThrow('Valid user ID is required'); + }); + + it('should throw error if userId is undefined', async () => { + // Arrange + const query = new GetUserByIdQuery(undefined); + + // Act & Assert + await expect(handler.handle(query)).rejects.toThrow('Valid user ID is required'); + }); + }); + + describe('handle - error cases', () => { + it('should throw error if user not found', async () => { + // Arrange + const query = new GetUserByIdQuery(999); + + mockPrisma.user.findUnique.mockResolvedValue(null); + + // Act & Assert + await expect(handler.handle(query)).rejects.toThrow('User not found'); + }); + }); +}); diff --git a/Backend/negyedik gyakorlat/tests/unit/services/EmailService.test.js b/Backend/negyedik gyakorlat/tests/unit/services/EmailService.test.js new file mode 100644 index 0000000..1e2c6f7 --- /dev/null +++ b/Backend/negyedik gyakorlat/tests/unit/services/EmailService.test.js @@ -0,0 +1,240 @@ +const EmailService = require('../../../src/application/services/EmailService'); +const nodemailer = require('nodemailer'); + +// Mock nodemailer +jest.mock('nodemailer'); + +describe('EmailService', () => { + let emailService; + let mockTransporter; + + beforeEach(() => { + // Reset mocks + jest.clearAllMocks(); + + // Mock transporter + mockTransporter = { + sendMail: jest.fn().mockResolvedValue({ + messageId: 'test-message-id', + response: '250 OK', + }), + }; + + // Mock nodemailer.createTransport + nodemailer.createTransport.mockReturnValue(mockTransporter); + + // Create EmailService instance + emailService = new EmailService(); + }); + + describe('constructor', () => { + test('should create transporter with correct configuration', () => { + expect(nodemailer.createTransport).toHaveBeenCalled(); + + const config = nodemailer.createTransport.mock.calls[0][0]; + expect(config).toHaveProperty('host'); + expect(config).toHaveProperty('port'); + expect(config).toHaveProperty('auth'); + expect(config.auth).toHaveProperty('user'); + expect(config.auth).toHaveProperty('pass'); + }); + + test('should initialize transporter instance', () => { + expect(emailService.transporter).toBeDefined(); + expect(emailService.transporter).toBe(mockTransporter); + }); + }); + + describe('sendWelcomeEmail', () => { + test('should send welcome email with correct parameters', async () => { + const userEmail = 'test@example.com'; + const userName = 'John Doe'; + + await emailService.sendWelcomeEmail(userEmail, userName); + + expect(mockTransporter.sendMail).toHaveBeenCalledTimes(1); + + const emailOptions = mockTransporter.sendMail.mock.calls[0][0]; + expect(emailOptions.to).toBe(userEmail); + expect(emailOptions.subject).toContain('Üdvözlünk'); + expect(emailOptions).toHaveProperty('html'); + }); + + test('should include user name in email content', async () => { + const userEmail = 'jane@example.com'; + const userName = 'Jane Smith'; + + await emailService.sendWelcomeEmail(userEmail, userName); + + const emailOptions = mockTransporter.sendMail.mock.calls[0][0]; + expect(emailOptions.html).toContain(userName); + }); + + test('should include user email in email content', async () => { + const userEmail = 'contact@test.com'; + const userName = 'Test User'; + + await emailService.sendWelcomeEmail(userEmail, userName); + + const emailOptions = mockTransporter.sendMail.mock.calls[0][0]; + expect(emailOptions.html).toContain(userEmail); + }); + + test('should return messageId on successful send', async () => { + const result = await emailService.sendWelcomeEmail('user@test.com', 'User'); + + expect(result).toBeDefined(); + expect(result.messageId).toBe('test-message-id'); + }); + + test('should use correct from address', async () => { + await emailService.sendWelcomeEmail('recipient@test.com', 'Recipient'); + + const emailOptions = mockTransporter.sendMail.mock.calls[0][0]; + expect(emailOptions).toHaveProperty('from'); + expect(emailOptions.from).toBeTruthy(); + }); + + test('should handle multiple recipients', async () => { + await emailService.sendWelcomeEmail('user1@test.com', 'User 1'); + await emailService.sendWelcomeEmail('user2@test.com', 'User 2'); + await emailService.sendWelcomeEmail('user3@test.com', 'User 3'); + + expect(mockTransporter.sendMail).toHaveBeenCalledTimes(3); + }); + }); + + describe('error handling', () => { + test('should throw error when email sending fails', async () => { + mockTransporter.sendMail.mockRejectedValue(new Error('SMTP connection failed')); + + await expect( + emailService.sendWelcomeEmail('user@test.com', 'User') + ).rejects.toThrow('SMTP connection failed'); + }); + + test('should throw error for invalid email address', async () => { + mockTransporter.sendMail.mockRejectedValue(new Error('Invalid recipient')); + + await expect( + emailService.sendWelcomeEmail('invalid-email', 'User') + ).rejects.toThrow(); + }); + + test('should throw error when transporter is not configured', async () => { + mockTransporter.sendMail.mockRejectedValue(new Error('Transporter not configured')); + + await expect( + emailService.sendWelcomeEmail('user@test.com', 'User') + ).rejects.toThrow('Transporter not configured'); + }); + }); + + describe('template rendering', () => { + test('should render HTML template with Handlebars', async () => { + await emailService.sendWelcomeEmail('template@test.com', 'Template User'); + + const emailOptions = mockTransporter.sendMail.mock.calls[0][0]; + + // Check that HTML is rendered (not raw Handlebars template) + expect(emailOptions.html).not.toContain('{{userName}}'); + expect(emailOptions.html).not.toContain('{{userEmail}}'); + expect(emailOptions.html).toContain('Template User'); + expect(emailOptions.html).toContain('template@test.com'); + }); + + test('should handle special characters in user name', async () => { + const specialName = "O'Reilly & Sons "; + + await emailService.sendWelcomeEmail('user@test.com', specialName); + + const emailOptions = mockTransporter.sendMail.mock.calls[0][0]; + + // Handlebars should escape HTML by default + expect(emailOptions.html).toBeDefined(); + }); + + test('should use correct template file path', async () => { + await emailService.sendWelcomeEmail('user@test.com', 'User'); + + // Verify that email was sent (which means template was loaded successfully) + expect(mockTransporter.sendMail).toHaveBeenCalled(); + }); + }); + + describe('real-world scenarios', () => { + test('should send welcome email after user registration', async () => { + const newUser = { + email: 'newuser@example.com', + name: 'New User', + }; + + const result = await emailService.sendWelcomeEmail(newUser.email, newUser.name); + + expect(result).toBeDefined(); + expect(mockTransporter.sendMail).toHaveBeenCalledWith( + expect.objectContaining({ + to: newUser.email, + html: expect.stringContaining(newUser.name), + }) + ); + }); + + test('should handle concurrent email sends', async () => { + const users = [ + { email: 'user1@test.com', name: 'User 1' }, + { email: 'user2@test.com', name: 'User 2' }, + { email: 'user3@test.com', name: 'User 3' }, + ]; + + await Promise.all( + users.map(user => emailService.sendWelcomeEmail(user.email, user.name)) + ); + + expect(mockTransporter.sendMail).toHaveBeenCalledTimes(3); + }); + + test('should work with Ethereal Email test account', async () => { + // Simulate Ethereal Email configuration + const etherealTransporter = { + sendMail: jest.fn().mockResolvedValue({ + messageId: '', + response: '250 Accepted', + }), + }; + + nodemailer.createTransport.mockReturnValue(etherealTransporter); + const etherealEmailService = new EmailService(); + + const result = await etherealEmailService.sendWelcomeEmail('test@ethereal.email', 'Test User'); + + expect(result.messageId).toContain('ethereal'); + expect(etherealTransporter.sendMail).toHaveBeenCalled(); + }); + }); + + describe('email content validation', () => { + test('should include welcome message', async () => { + await emailService.sendWelcomeEmail('user@test.com', 'User'); + + const emailOptions = mockTransporter.sendMail.mock.calls[0][0]; + expect(emailOptions.html.toLowerCase()).toMatch(/üdvözl|welcome/i); + }); + + test('should be HTML formatted', async () => { + await emailService.sendWelcomeEmail('user@test.com', 'User'); + + const emailOptions = mockTransporter.sendMail.mock.calls[0][0]; + expect(emailOptions.html).toContain(''); + }); + + test('should have valid subject line', async () => { + await emailService.sendWelcomeEmail('user@test.com', 'User'); + + const emailOptions = mockTransporter.sendMail.mock.calls[0][0]; + expect(emailOptions.subject).toBeTruthy(); + expect(emailOptions.subject.length).toBeGreaterThan(0); + }); + }); +}); diff --git a/Backend/negyedik gyakorlat/tests/unit/services/JwtService.test.js b/Backend/negyedik gyakorlat/tests/unit/services/JwtService.test.js new file mode 100644 index 0000000..9ddd5c3 --- /dev/null +++ b/Backend/negyedik gyakorlat/tests/unit/services/JwtService.test.js @@ -0,0 +1,218 @@ +const JwtService = require('../../../src/application/services/JwtService'); +const jwt = require('jsonwebtoken'); + +// Mock jsonwebtoken +jest.mock('jsonwebtoken'); + +describe('JwtService', () => { + let jwtService; + const mockSecret = 'test-secret'; + const mockExpiresIn = '1h'; + + beforeEach(() => { + // Setup environment variables + process.env.JWT_SECRET = mockSecret; + process.env.JWT_EXPIRES_IN = mockExpiresIn; + + jwtService = new JwtService(); + + // Reset all mocks + jest.clearAllMocks(); + }); + + describe('generateToken', () => { + it('should generate a JWT token with payload', () => { + // Arrange + const payload = { userId: 1, email: 'john@example.com' }; + const mockToken = 'mock_jwt_token_abc123'; + + jwt.sign.mockReturnValue(mockToken); + + // Act + const result = jwtService.generateToken(payload); + + // Assert + expect(jwt.sign).toHaveBeenCalledWith(payload, mockSecret, { expiresIn: mockExpiresIn }); + expect(result).toBe(mockToken); + }); + + it('should use default secret if JWT_SECRET not set', () => { + // Arrange + delete process.env.JWT_SECRET; + jwtService = new JwtService(); + const payload = { userId: 1, email: 'john@example.com' }; + + jwt.sign.mockReturnValue('token'); + + // Act + jwtService.generateToken(payload); + + // Assert + expect(jwt.sign).toHaveBeenCalledWith(payload, 'default-secret-change-me', expect.any(Object)); + }); + }); + + describe('verifyToken', () => { + it('should verify and return decoded token', () => { + // Arrange + const token = 'valid_token'; + const mockDecoded = { userId: 1, email: 'john@example.com' }; + + jwt.verify.mockReturnValue(mockDecoded); + + // Act + const result = jwtService.verifyToken(token); + + // Assert + expect(jwt.verify).toHaveBeenCalledWith(token, mockSecret); + expect(result).toEqual(mockDecoded); + }); + + it('should throw error for invalid token', () => { + // Arrange + const token = 'invalid_token'; + + jwt.verify.mockImplementation(() => { + throw new Error('jwt malformed'); + }); + + // Act & Assert + expect(() => jwtService.verifyToken(token)).toThrow('Invalid or expired token'); + }); + + it('should throw error for expired token', () => { + // Arrange + const token = 'expired_token'; + + jwt.verify.mockImplementation(() => { + throw new Error('jwt expired'); + }); + + // Act & Assert + expect(() => jwtService.verifyToken(token)).toThrow('Invalid or expired token'); + }); + }); + + describe('extractTokenFromCookies', () => { + it('should extract token from cookies object', () => { + // Arrange + const cookies = { auth_token: 'abc123xyz' }; + + // Act + const result = jwtService.extractTokenFromCookies(cookies); + + // Assert + expect(result).toBe('abc123xyz'); + }); + + it('should return null if cookies object is empty', () => { + // Arrange + const cookies = {}; + + // Act + const result = jwtService.extractTokenFromCookies(cookies); + + // Assert + expect(result).toBeNull(); + }); + + it('should return null if cookies is null', () => { + // Arrange + const cookies = null; + + // Act + const result = jwtService.extractTokenFromCookies(cookies); + + // Assert + expect(result).toBeNull(); + }); + + it('should return null if cookies is undefined', () => { + // Arrange + const cookies = undefined; + + // Act + const result = jwtService.extractTokenFromCookies(cookies); + + // Assert + expect(result).toBeNull(); + }); + }); + + describe('getCookieOptions', () => { + it('should return secure cookie options in production', () => { + // Arrange + process.env.NODE_ENV = 'production'; + jwtService = new JwtService(); + + // Act + const options = jwtService.getCookieOptions(); + + // Assert + expect(options.httpOnly).toBe(true); + expect(options.secure).toBe(true); + expect(options.sameSite).toBe('strict'); + expect(options.path).toBe('/'); + expect(options.maxAge).toBeGreaterThan(0); + }); + + it('should return non-secure cookie options in development', () => { + // Arrange + process.env.NODE_ENV = 'development'; + jwtService = new JwtService(); + + // Act + const options = jwtService.getCookieOptions(); + + // Assert + expect(options.httpOnly).toBe(true); + expect(options.secure).toBe(false); + expect(options.sameSite).toBe('strict'); + }); + }); + + describe('getCookieName', () => { + it('should return the cookie name', () => { + // Act + const name = jwtService.getCookieName(); + + // Assert + expect(name).toBe('auth_token'); + }); + }); + + describe('extractTokenFromHeader (legacy)', () => { + it('should extract token from valid Authorization header', () => { + // Arrange + const authHeader = 'Bearer abc123xyz'; + + // Act + const result = jwtService.extractTokenFromHeader(authHeader); + + // Assert + expect(result).toBe('abc123xyz'); + }); + + it('should return null if Authorization header is missing', () => { + // Arrange + const authHeader = null; + + // Act + const result = jwtService.extractTokenFromHeader(authHeader); + + // Assert + expect(result).toBeNull(); + }); + + it('should return null if Authorization header does not start with Bearer', () => { + // Arrange + const authHeader = 'Basic abc123xyz'; + + // Act + const result = jwtService.extractTokenFromHeader(authHeader); + + // Assert + expect(result).toBeNull(); + }); + }); +}); diff --git a/Backend/negyedik gyakorlat/tests/user.test.js b/Backend/negyedik gyakorlat/tests/user.test.js new file mode 100644 index 0000000..e659c65 --- /dev/null +++ b/Backend/negyedik gyakorlat/tests/user.test.js @@ -0,0 +1,36 @@ +const { expect } = require('chai'); +const UserService = require('../src/services/UserService'); + +describe('UserService', () => { + let emailService; + let userService; + + beforeEach(() => { + // TODO 1: Hozz létre egy mock EmailService-t (sendWelcomeEmail = async () => {}) + // Tipp: emailService = { sendWelcomeEmail: async (email, name) => { /* mock */ } }; + + // TODO 2: Példányosítsd a UserService-t a mock-kal + // Tipp: userService = new UserService(emailService); + }); + + it('should create a user and send welcome email', async () => { + // TODO 3: Teszt: createUser létrehoz egy usert és email-t küld + // 1. Hívd meg a createUser-t: const user = await userService.createUser('Test User', 'test@example.com'); + // 2. Ellenőrizd az user property-ket: expect(user).to.have.property('id'); + // 3. Ellenőrizd a name-et: expect(user.name).to.equal('Test User'); + // 4. Ellenőrizd, hogy a user a listában van: const users = userService.getAllUsers(); + }); + + it('should throw error for duplicate email', async () => { + // TODO 4: Teszt: duplikált email hibát dob + // 1. Hozz létre egy usert: await userService.createUser('User 1', 'test@example.com'); + // 2. Próbáld létrehozni ugyanazzal az emaillel: expect(...).to.throw() vagy try-catch + // Tipp async esetén: try { await userService.createUser(...); } catch(e) { expect(e.message).to.include('already exists'); } + }); + + it('should validate required fields', async () => { + // TODO 5: Teszt: hiányzó mezők validációs hibát dobnak + // 1. Próbáld meg név nélkül: expect(() => userService.createUser('', 'test@example.com')).to.throw(); + // 2. Próbáld meg email nélkül: expect(() => userService.createUser('Test', '')).to.throw(); + }); +}); diff --git a/Backend/negyedik gyakorlat_minta/.env.example b/Backend/negyedik gyakorlat_minta/.env.example new file mode 100644 index 0000000..41e390b --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/.env.example @@ -0,0 +1,15 @@ +DATABASE_URL="postgresql://postgres:postgres@localhost:5432/cors_di_app?schema=public" +JWT_SECRET="titkos-kulcs-jwt-hez-változtasd-meg" +JWT_EXPIRES_IN="7d" +PORT=3000 +NODE_ENV=development + +# Ethereal Email (Regisztrálj itt: https://ethereal.email/) +ETHEREAL_USER=your-ethereal-user@ethereal.email +ETHEREAL_PASS=your-ethereal-password + +# CORS (comma-separated origins) +ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173 + +# App URL (email linkekhez) +APP_URL=http://localhost:3000 diff --git a/Backend/negyedik gyakorlat_minta/.gitignore b/Backend/negyedik gyakorlat_minta/.gitignore new file mode 100644 index 0000000..dd2b21a --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/.gitignore @@ -0,0 +1,20 @@ +# Dependencies +node_modules/ + +# Environment +.env + +# Logs +*.log +npm-debug.log* + +# OS +.DS_Store +Thumbs.db + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ diff --git a/Backend/negyedik gyakorlat_minta/README.md b/Backend/negyedik gyakorlat_minta/README.md new file mode 100644 index 0000000..93066bb --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/README.md @@ -0,0 +1,98 @@ +# Negyedik Gyakorlat - MINTA Megoldás + +Ez a mappa tartalmazza a negyedik gyakorlat **teljes, működő megoldását**. + +## Amit implementáltunk: + +### 1. **Dependency Injection Container** +- Singleton lifecycle - egy példány az egész alkalmazásban +- Transient lifecycle - minden híváskor új példány +- Scoped lifecycle - request szintű scope +- Service regisztráció és feloldás +- Circular dependency kezelés + +### 2. **CORS Konfigurás** +- Whitelist-based origin ellenőrzés +- Credentials support (cookie-k) +- Preflight request kezelés +- Custom CORS middleware + +### 3. **Email Service (Nodemailer + Handlebars)** +- Ethereal test SMTP +- HTML email templatek (Handlebars) +- Welcome email új usereknek +- Password reset email +- Template rendering + +### 4. **Cookie-based JWT Auth** +- Tokenek csak cookie-ban (nem response body-ban!) +- HttpOnly, Secure, SameSite cookie-k +- Automatikus token ellenőrzés middleware-ből + +## Indítás + +```bash +npm install +npm run docker:up +npx prisma generate +npx prisma migrate dev +npm run dev +``` + +## Környezeti Változók + +```env +DATABASE_URL="postgresql://postgres:postgres@localhost:5432/cors_di_app?schema=public" +JWT_SECRET="your-secret-key" +JWT_EXPIRES_IN="7d" +PORT=3000 +NODE_ENV=development + +# Ethereal Email +ETHEREAL_USER=your-ethereal-user@ethereal.email +ETHEREAL_PASS=your-ethereal-password + +# CORS +ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173 +``` + +## DI Container Példa + +```javascript +// Service regisztráció +container.register('database', () => new DatabaseConnection(), 'singleton'); +container.register('userRepo', () => new UserRepository(), 'singleton'); +container.register('emailService', () => new EmailService(), 'singleton'); +container.register('requestId', () => crypto.randomUUID(), 'transient'); +container.register('jwtService', () => new JwtService(), 'scoped'); + +// Service feloldás +const userRepo = container.resolve('userRepo'); +const emailService = container.resolve('emailService'); + +// Scoped resolution (request szintű) +const scope = container.createScope(); +const jwtService = scope.resolve('jwtService'); +``` + +## Email Service Példa + +```javascript +// Welcome email küldése +await emailService.sendWelcomeEmail(user.email, user.name); + +// Password reset email +await emailService.sendPasswordResetEmail(user.email, resetToken); +``` + +## CORS Engedélyezett Eredethelyekről + +Csak az `ALLOWED_ORIGINS` környezeti változóban megadott origin-ek férhetnek hozzá az API-hoz. + +## Tanulási Pontok + +1. **Dependency Injection**: Service lifecycle management +2. **CORS**: Biztonságos cross-origin konfigurás +3. **Email Templates**: Handlebars template rendering +4. **Nodemailer**: Email küldés SMTP-vel +5. **Cookie-based Auth**: Token kezelés csak cookie-kban diff --git a/Backend/negyedik gyakorlat_minta/docker-compose.yml b/Backend/negyedik gyakorlat_minta/docker-compose.yml new file mode 100644 index 0000000..484a934 --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/docker-compose.yml @@ -0,0 +1,25 @@ +version: '3.8' + +services: + # PostgreSQL Database + postgres: + image: postgres:15-alpine + container_name: cors-di-email-postgres-minta + restart: unless-stopped + environment: + POSTGRES_USER: ${DB_USER:-postgres} + POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres} + POSTGRES_DB: ${DB_NAME:-cors_di_app_minta} + ports: + - "${DB_PORT:-5433}:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + postgres_data: + driver: local diff --git a/Backend/negyedik gyakorlat_minta/jest.config.js b/Backend/negyedik gyakorlat_minta/jest.config.js new file mode 100644 index 0000000..e877d9e --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/jest.config.js @@ -0,0 +1,16 @@ +module.exports = { + testEnvironment: 'node', + coverageDirectory: 'coverage', + collectCoverageFrom: [ + 'src/**/*.js', + '!src/api/server.js', + '!src/**/index.js' + ], + testMatch: [ + '**/tests/**/*.test.js' + ], + verbose: true, + clearMocks: true, + resetMocks: true, + restoreMocks: true +}; diff --git a/Backend/negyedik gyakorlat_minta/package.json b/Backend/negyedik gyakorlat_minta/package.json new file mode 100644 index 0000000..cfae78a --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/package.json @@ -0,0 +1,40 @@ +{ + "name": "cors-di-email-minta", + "version": "1.0.0", + "description": "MINTA - CORS + DI Container + Email Notifications", + "main": "src/api/server.js", + "type": "module", + "scripts": { + "dev": "nodemon src/api/server.js", + "start": "node src/api/server.js", + "test": "jest --coverage", + "test:watch": "jest --watch", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev", + "prisma:studio": "prisma studio", + "prisma:seed": "node prisma/seed.js", + "docker:up": "docker-compose up -d", + "docker:down": "docker-compose down", + "docker:logs": "docker-compose logs -f" + }, + "keywords": ["cors", "di", "dependency-injection", "email", "nodemailer"], + "author": "", + "license": "ISC", + "dependencies": { + "@prisma/client": "^5.9.1", + "bcrypt": "^5.1.1", + "cookie-parser": "^1.4.6", + "dotenv": "^16.4.1", + "express": "^4.18.2", + "handlebars": "^4.7.8", + "jsonwebtoken": "^9.0.2", + "nodemailer": "^6.9.8" + }, + "devDependencies": { + "@types/jest": "^29.5.11", + "jest": "^29.7.0", + "nodemon": "^3.0.3", + "prisma": "^5.9.1", + "supertest": "^6.3.4" + } +} diff --git a/Backend/negyedik gyakorlat_minta/prisma/schema.prisma b/Backend/negyedik gyakorlat_minta/prisma/schema.prisma new file mode 100644 index 0000000..7fe56ff --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/prisma/schema.prisma @@ -0,0 +1,21 @@ +// Prisma Schema - PostgreSQL Database +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +// User Model +model User { + id Int @id @default(autoincrement()) + name String + email String @unique + password String // bcrypt hashed password + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("users") +} diff --git a/Backend/negyedik gyakorlat_minta/src/api/controllers/AuthController.js b/Backend/negyedik gyakorlat_minta/src/api/controllers/AuthController.js new file mode 100644 index 0000000..e710f1e --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/src/api/controllers/AuthController.js @@ -0,0 +1,83 @@ +export class AuthController { + constructor(registerHandler, loginHandler) { + this.registerHandler = registerHandler; + this.loginHandler = loginHandler; + } + + async register(req, res, next) { + try { + const { name, email, password } = req.body; + + if (!name || !email || !password) { + return res.status(400).json({ + success: false, + error: 'Name, email and password are required' + }); + } + + const { user, token } = await this.registerHandler.handle({ + name, + email, + password + }); + + // Set cookie + res.cookie('token', token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days + }); + + res.status(201).json({ + success: true, + message: 'User registered successfully', + data: user.toJSON() + }); + } catch (error) { + next(error); + } + } + + async login(req, res, next) { + try { + const { email, password } = req.body; + + if (!email || !password) { + return res.status(400).json({ + success: false, + error: 'Email and password are required' + }); + } + + const { user, token } = await this.loginHandler.handle({ + email, + password + }); + + // Set cookie + res.cookie('token', token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days + }); + + res.json({ + success: true, + message: 'Login successful', + data: user.toJSON() + }); + } catch (error) { + next(error); + } + } + + async logout(req, res) { + res.clearCookie('token'); + res.json({ + success: true, + message: 'Logged out successfully' + }); + } +} diff --git a/Backend/negyedik gyakorlat_minta/src/api/controllers/UserController.js b/Backend/negyedik gyakorlat_minta/src/api/controllers/UserController.js new file mode 100644 index 0000000..926e458 --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/src/api/controllers/UserController.js @@ -0,0 +1,62 @@ +export class UserController { + constructor(getMeHandler, getAllUsersHandler, getUserByIdHandler, updateProfileHandler) { + this.getMeHandler = getMeHandler; + this.getAllUsersHandler = getAllUsersHandler; + this.getUserByIdHandler = getUserByIdHandler; + this.updateProfileHandler = updateProfileHandler; + } + + async getMe(req, res, next) { + try { + const user = await this.getMeHandler.handle({ userId: req.user.id }); + res.json({ + success: true, + data: user.toJSON() + }); + } catch (error) { + next(error); + } + } + + async getAllUsers(req, res, next) { + try { + const users = await this.getAllUsersHandler.handle({}); + res.json({ + success: true, + data: users.map(u => u.toJSON()) + }); + } catch (error) { + next(error); + } + } + + async getUserById(req, res, next) { + try { + const user = await this.getUserByIdHandler.handle({ userId: req.params.id }); + res.json({ + success: true, + data: user.toJSON() + }); + } catch (error) { + next(error); + } + } + + async updateProfile(req, res, next) { + try { + const { name, email } = req.body; + const user = await this.updateProfileHandler.handle({ + userId: req.user.id, + name, + email + }); + res.json({ + success: true, + message: 'Profile updated successfully', + data: user.toJSON() + }); + } catch (error) { + next(error); + } + } +} diff --git a/Backend/negyedik gyakorlat_minta/src/api/middlewares/authMiddleware.js b/Backend/negyedik gyakorlat_minta/src/api/middlewares/authMiddleware.js new file mode 100644 index 0000000..0815ade --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/src/api/middlewares/authMiddleware.js @@ -0,0 +1,23 @@ +export function authMiddleware(jwtService) { + return async (req, res, next) => { + try { + const token = req.cookies.token; + + if (!token) { + return res.status(401).json({ + success: false, + error: 'Authentication required' + }); + } + + const decoded = jwtService.verifyToken(token); + req.user = decoded; + next(); + } catch (error) { + return res.status(401).json({ + success: false, + error: 'Invalid or expired token' + }); + } + }; +} diff --git a/Backend/negyedik gyakorlat_minta/src/api/middlewares/corsMiddleware.js b/Backend/negyedik gyakorlat_minta/src/api/middlewares/corsMiddleware.js new file mode 100644 index 0000000..c9ab09b --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/src/api/middlewares/corsMiddleware.js @@ -0,0 +1,58 @@ +/** + * CORS Middleware - Whitelist alapú origin ellenőrzéssel + * + * Csak az engedélyezett origin-ekről érkező kéréseket fogadjuk el. + */ + +const allowedOrigins = (process.env.ALLOWED_ORIGINS || 'http://localhost:3000') + .split(',') + .map(origin => origin.trim()); + +console.log('🔒 CORS allowed origins:', allowedOrigins); + +export function corsMiddleware(req, res, next) { + const origin = req.headers.origin; + + // Ellenőrizzük hogy az origin engedélyezett-e + if (origin && allowedOrigins.includes(origin)) { + // Engedélyezett origin + res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Access-Control-Allow-Credentials', 'true'); + res.setHeader( + 'Access-Control-Allow-Methods', + 'GET, POST, PUT, DELETE, PATCH, OPTIONS' + ); + res.setHeader( + 'Access-Control-Allow-Headers', + 'Content-Type, Authorization, Cookie' + ); + res.setHeader( + 'Access-Control-Expose-Headers', + 'Set-Cookie' + ); + } + + // Preflight request kezelése (OPTIONS) + if (req.method === 'OPTIONS') { + return res.status(204).end(); + } + + next(); +} + +/** + * Strict CORS check - ha az origin nem engedélyezett, visszautasítjuk + */ +export function strictCorsCheck(req, res, next) { + const origin = req.headers.origin; + + // Ha van origin header de nincs az engedélyezett listában + if (origin && !allowedOrigins.includes(origin)) { + return res.status(403).json({ + success: false, + error: 'CORS policy: Origin not allowed' + }); + } + + next(); +} diff --git a/Backend/negyedik gyakorlat_minta/src/api/middlewares/errorHandler.js b/Backend/negyedik gyakorlat_minta/src/api/middlewares/errorHandler.js new file mode 100644 index 0000000..a514691 --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/src/api/middlewares/errorHandler.js @@ -0,0 +1,12 @@ +export function errorHandler(err, req, res, next) { + console.error('Error:', err); + + const statusCode = err.statusCode || 500; + const message = err.message || 'Internal Server Error'; + + res.status(statusCode).json({ + success: false, + error: message, + ...(process.env.NODE_ENV === 'development' && { stack: err.stack }) + }); +} diff --git a/Backend/negyedik gyakorlat_minta/src/api/middlewares/scopedMiddleware.js b/Backend/negyedik gyakorlat_minta/src/api/middlewares/scopedMiddleware.js new file mode 100644 index 0000000..2c7226a --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/src/api/middlewares/scopedMiddleware.js @@ -0,0 +1,14 @@ +// Scoped middleware - creates a new DI scope for each request +export function scopedMiddleware(container) { + return (req, res, next) => { + // Create request-scoped container + req.scope = container.createScope(); + + // Clean up scope after response + res.on('finish', () => { + req.scope.dispose(); + }); + + next(); + }; +} diff --git a/Backend/negyedik gyakorlat_minta/src/api/routers/authRoutes.js b/Backend/negyedik gyakorlat_minta/src/api/routers/authRoutes.js new file mode 100644 index 0000000..9a08e26 --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/src/api/routers/authRoutes.js @@ -0,0 +1,12 @@ +import express from 'express'; + +export function createAuthRoutes(container) { + const router = express.Router(); + const authController = container.resolve('AuthController'); + + router.post('/register', (req, res, next) => authController.register(req, res, next)); + router.post('/login', (req, res, next) => authController.login(req, res, next)); + router.post('/logout', (req, res, next) => authController.logout(req, res, next)); + + return router; +} diff --git a/Backend/negyedik gyakorlat_minta/src/api/routers/userRoutes.js b/Backend/negyedik gyakorlat_minta/src/api/routers/userRoutes.js new file mode 100644 index 0000000..2f0d3bd --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/src/api/routers/userRoutes.js @@ -0,0 +1,16 @@ +import express from 'express'; +import { authMiddleware } from '../middlewares/authMiddleware.js'; + +export function createUserRoutes(container) { + const router = express.Router(); + const userController = container.resolve('UserController'); + const jwtService = container.resolve('JwtService'); + + // Protected routes + router.get('/me', authMiddleware(jwtService), (req, res, next) => userController.getMe(req, res, next)); + router.get('/', authMiddleware(jwtService), (req, res, next) => userController.getAllUsers(req, res, next)); + router.get('/:id', authMiddleware(jwtService), (req, res, next) => userController.getUserById(req, res, next)); + router.put('/profile', authMiddleware(jwtService), (req, res, next) => userController.updateProfile(req, res, next)); + + return router; +} diff --git a/Backend/negyedik gyakorlat_minta/src/api/server.js b/Backend/negyedik gyakorlat_minta/src/api/server.js new file mode 100644 index 0000000..f072be0 --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/src/api/server.js @@ -0,0 +1,145 @@ +import express from 'express'; +import cookieParser from 'cookie-parser'; +import dotenv from 'dotenv'; +import { Container } from '../application/services/Container.js'; +import DatabaseConnection from '../infrastructure/db/DatabaseConnection.js'; +import { corsMiddleware } from './middlewares/corsMiddleware.js'; +import { errorHandler } from './middlewares/errorHandler.js'; +import { scopedMiddleware } from './middlewares/scopedMiddleware.js'; +import { createAuthRoutes } from './routers/authRoutes.js'; +import { createUserRoutes } from './routers/userRoutes.js'; + +// Services +import { JwtService } from '../application/services/JwtService.js'; +import { EmailService } from '../application/services/EmailService.js'; + +// Repositories +import { UserRepository } from '../infrastructure/repositories/UserRepository.js'; + +// Auth Handlers +import { RegisterUserCommandHandler } from '../application/auth/commands/RegisterUserCommandHandler.js'; +import { LoginUserCommandHandler } from '../application/auth/commands/LoginUserCommandHandler.js'; + +// User Handlers +import { GetMeQueryHandler } from '../application/user/queries/GetMeQueryHandler.js'; +import { GetAllUsersQueryHandler } from '../application/user/queries/GetAllUsersQueryHandler.js'; +import { GetUserByIdQueryHandler } from '../application/user/queries/GetUserByIdQueryHandler.js'; +import { UpdateUserProfileCommandHandler } from '../application/user/commands/UpdateUserProfileCommandHandler.js'; + +// Controllers +import { AuthController } from './controllers/AuthController.js'; +import { UserController } from './controllers/UserController.js'; + +dotenv.config(); + +const app = express(); +const PORT = process.env.PORT || 3002; + +// ===== DI Container Setup ===== +const container = new Container(); + +// Register Singleton Services (shared across all requests) +container.registerSingleton('JwtService', () => new JwtService()); +container.registerSingleton('EmailService', () => new EmailService()); +container.registerSingleton('UserRepository', () => new UserRepository()); + +// Register Transient Handlers (new instance per resolve) +container.registerTransient('RegisterUserCommandHandler', (c) => + new RegisterUserCommandHandler( + c.resolve('UserRepository'), + c.resolve('JwtService'), + c.resolve('EmailService') + ) +); + +container.registerTransient('LoginUserCommandHandler', (c) => + new LoginUserCommandHandler( + c.resolve('UserRepository'), + c.resolve('JwtService') + ) +); + +container.registerTransient('GetMeQueryHandler', (c) => + new GetMeQueryHandler(c.resolve('UserRepository')) +); + +container.registerTransient('GetAllUsersQueryHandler', (c) => + new GetAllUsersQueryHandler(c.resolve('UserRepository')) +); + +container.registerTransient('GetUserByIdQueryHandler', (c) => + new GetUserByIdQueryHandler(c.resolve('UserRepository')) +); + +container.registerTransient('UpdateUserProfileCommandHandler', (c) => + new UpdateUserProfileCommandHandler(c.resolve('UserRepository')) +); + +// Register Controllers +container.registerTransient('AuthController', (c) => + new AuthController( + c.resolve('RegisterUserCommandHandler'), + c.resolve('LoginUserCommandHandler') + ) +); + +container.registerTransient('UserController', (c) => + new UserController( + c.resolve('GetMeQueryHandler'), + c.resolve('GetAllUsersQueryHandler'), + c.resolve('GetUserByIdQueryHandler'), + c.resolve('UpdateUserProfileCommandHandler') + ) +); + +// ===== Middleware ===== +app.use(corsMiddleware()); // CORS with whitelist +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +app.use(cookieParser()); +app.use(scopedMiddleware(container)); // Request-scoped DI + +// ===== Database Connection ===== +await DatabaseConnection.connect(); + +// ===== Health Check ===== +app.get('/health', (req, res) => { + res.json({ + status: 'OK', + timestamp: new Date().toISOString() + }); +}); + +// ===== Routes ===== +app.use('/api/auth', createAuthRoutes(container)); +app.use('/api/users', createUserRoutes(container)); + +// ===== Error Handling ===== +app.use((req, res) => { + res.status(404).json({ + success: false, + error: 'Endpoint not found' + }); +}); + +app.use(errorHandler); + +// ===== Graceful Shutdown ===== +process.on('SIGINT', async () => { + console.log('\n🛑 Shutting down gracefully...'); + await DatabaseConnection.disconnect(); + process.exit(0); +}); + +process.on('SIGTERM', async () => { + console.log('\n🛑 Shutting down gracefully...'); + await DatabaseConnection.disconnect(); + process.exit(0); +}); + +// ===== Start Server ===== +app.listen(PORT, () => { + console.log(`✅ Server running on http://localhost:${PORT}`); + console.log(`📊 Environment: ${process.env.NODE_ENV}`); + console.log(`🔒 CORS enabled for: ${process.env.ALLOWED_ORIGINS}`); +}); diff --git a/Backend/negyedik gyakorlat_minta/src/application/auth/commands/LoginUserCommand.js b/Backend/negyedik gyakorlat_minta/src/application/auth/commands/LoginUserCommand.js new file mode 100644 index 0000000..c932636 --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/src/application/auth/commands/LoginUserCommand.js @@ -0,0 +1,6 @@ +export class LoginUserCommand { + constructor(email, password) { + this.email = email; + this.password = password; + } +} diff --git a/Backend/negyedik gyakorlat_minta/src/application/auth/commands/LoginUserCommandHandler.js b/Backend/negyedik gyakorlat_minta/src/application/auth/commands/LoginUserCommandHandler.js new file mode 100644 index 0000000..2c378ff --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/src/application/auth/commands/LoginUserCommandHandler.js @@ -0,0 +1,31 @@ +import bcrypt from 'bcryptjs'; + +export class LoginUserCommandHandler { + constructor(userRepository, jwtService) { + this.userRepository = userRepository; + this.jwtService = jwtService; + } + + async handle(command) { + // Find user by email + const user = await this.userRepository.findByEmail(command.email); + if (!user) { + throw new Error('Invalid credentials'); + } + + // Verify password + const isValidPassword = await bcrypt.compare(command.password, user.password); + if (!isValidPassword) { + throw new Error('Invalid credentials'); + } + + // Generate token + const token = this.jwtService.generateToken({ + id: user.id, + email: user.email, + name: user.name + }); + + return { user, token }; + } +} diff --git a/Backend/negyedik gyakorlat_minta/src/application/auth/commands/RegisterUserCommand.js b/Backend/negyedik gyakorlat_minta/src/application/auth/commands/RegisterUserCommand.js new file mode 100644 index 0000000..a574fd5 --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/src/application/auth/commands/RegisterUserCommand.js @@ -0,0 +1,7 @@ +export class RegisterUserCommand { + constructor(name, email, password) { + this.name = name; + this.email = email; + this.password = password; + } +} diff --git a/Backend/negyedik gyakorlat_minta/src/application/auth/commands/RegisterUserCommandHandler.js b/Backend/negyedik gyakorlat_minta/src/application/auth/commands/RegisterUserCommandHandler.js new file mode 100644 index 0000000..e6dd8e2 --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/src/application/auth/commands/RegisterUserCommandHandler.js @@ -0,0 +1,51 @@ +import bcrypt from 'bcryptjs'; +import { User } from '../../../domain/models/User.js'; + +export class RegisterUserCommandHandler { + constructor(userRepository, jwtService, emailService) { + this.userRepository = userRepository; + this.jwtService = jwtService; + this.emailService = emailService; + } + + async handle(command) { + // Validasi data + const errors = User.validate({ + name: command.name, + email: command.email, + password: command.password + }); + + if (errors.length > 0) { + throw new Error(errors.join(', ')); + } + + // Verify user does not exist + const existingUser = await this.userRepository.findByEmail(command.email); + if (existingUser) { + throw new Error('User with this email already exists'); + } + + // Hash password + const hashedPassword = await bcrypt.hash(command.password, 10); + + // Create user + const user = await this.userRepository.create({ + name: command.name, + email: command.email, + password: hashedPassword + }); + + // Send welcome email + await this.emailService.sendWelcomeEmail(user.email, user.name); + + // Generate token + const token = this.jwtService.generateToken({ + id: user.id, + email: user.email, + name: user.name + }); + + return { user, token }; + } +} diff --git a/Backend/negyedik gyakorlat_minta/src/application/services/Container.js b/Backend/negyedik gyakorlat_minta/src/application/services/Container.js new file mode 100644 index 0000000..bc54a91 --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/src/application/services/Container.js @@ -0,0 +1,126 @@ +/** + * Dependency Injection Container + * + * Supports three lifecycle types: + * - singleton: One instance for the entire application + * - transient: New instance every time + * - scoped: One instance per scope (e.g., per request) + */ +class Container { + constructor() { + this.services = new Map(); // Singleton instances tárolása + this.factories = new Map(); // Factory függvények tárolása + this.lifetimes = new Map(); // Lifecycle típusok tárolása + } + + /** + * Service regisztráció + * + * @param {string} name - Service neve + * @param {Function} factory - Factory függvény ami az instance-t létrehozza + * @param {string} lifetime - 'singleton' | 'transient' | 'scoped' + */ + register(name, factory, lifetime = 'singleton') { + // Factory és lifetime tárolása + this.factories.set(name, factory); + this.lifetimes.set(name, lifetime); + + // Ha singleton, azonnal példányosítjuk + if (lifetime === 'singleton') { + const instance = factory(); + this.services.set(name, instance); + } + } + + /** + * Service feloldás + * + * @param {string} name - Service neve + * @param {Map} scope - Scoped instance-ok tárolója (opcionális) + * @returns {*} Service instance + */ + resolve(name, scope = null) { + // Ellenőrizzük hogy regisztrálva van-e + if (!this.factories.has(name)) { + throw new Error(`Service '${name}' is not registered`); + } + + const lifetime = this.lifetimes.get(name); + + // SCOPED: Scope-ból vagy új instance + if (lifetime === 'scoped' && scope) { + if (scope.has(name)) { + return scope.get(name); + } + const instance = this.factories.get(name)(); + scope.set(name, instance); + return instance; + } + + // SINGLETON: Visszaadjuk a tárolt instance-t + if (lifetime === 'singleton') { + return this.services.get(name); + } + + // TRANSIENT: Mindig új instance + if (lifetime === 'transient') { + return this.factories.get(name)(); + } + + // Default: singleton behavior + return this.services.get(name); + } + + /** + * Új scope létrehozása + * + * @returns {Object} Scope objektum resolve metódussal + */ + createScope() { + const scopeMap = new Map(); + + return { + resolve: (name) => this.resolve(name, scopeMap), + dispose: () => { + // Scope-beli instance-ok felszabadítása + scopeMap.clear(); + } + }; + } + + /** + * Ellenőrzi hogy egy service regisztrálva van-e + * + * @param {string} name - Service neve + * @returns {boolean} + */ + has(name) { + return this.factories.has(name); + } + + /** + * Service törlése a containerből + * + * @param {string} name - Service neve + */ + unregister(name) { + this.services.delete(name); + this.factories.delete(name); + this.lifetimes.delete(name); + } + + /** + * Az összes service listázása + * + * @returns {Array<{name: string, lifetime: string}>} + */ + list() { + const services = []; + for (const [name, lifetime] of this.lifetimes.entries()) { + services.push({ name, lifetime }); + } + return services; + } +} + +export default Container; diff --git a/Backend/negyedik gyakorlat_minta/src/application/services/EmailService.js b/Backend/negyedik gyakorlat_minta/src/application/services/EmailService.js new file mode 100644 index 0000000..3b5c293 --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/src/application/services/EmailService.js @@ -0,0 +1,158 @@ +import nodemailer from 'nodemailer'; +import handlebars from 'handlebars'; +import fs from 'fs/promises'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +/** + * Email Service + * Nodemailer + Handlebars template alapú email küldés + */ +class EmailService { + constructor() { + // Nodemailer transporter létrehozása (Ethereal test SMTP) + this.transporter = nodemailer.createTransport({ + host: 'smtp.ethereal.email', + port: 587, + secure: false, // TLS + auth: { + user: process.env.ETHEREAL_USER || 'your-test-email@ethereal.email', + pass: process.env.ETHEREAL_PASS || 'your-test-password' + } + }); + + console.log('📧 EmailService initialized with Ethereal SMTP'); + } + + /** + * Template betöltése és renderelése + * + * @param {string} templateName - Template fájl neve (.hbs nélkül) + * @param {Object} data - Template adatok + * @returns {Promise} Renderelt HTML + */ + async renderTemplate(templateName, data) { + try { + // Template fájl beolvasása + const templatePath = path.join(__dirname, '..', '..', 'email-templates', `${templateName}.hbs`); + const templateContent = await fs.readFile(templatePath, 'utf-8'); + + // Handlebars compile és render + const template = handlebars.compile(templateContent); + return template(data); + } catch (error) { + console.error(`Template render error (${templateName}):`, error); + throw new Error(`Failed to render email template: ${templateName}`); + } + } + + /** + * Email küldés + * + * @param {Object} options - { to, subject, html, text } + * @returns {Promise} Nodemailer info objektum + */ + async sendEmail(options) { + try { + const mailOptions = { + from: '"Your App" ', + to: options.to, + subject: options.subject, + html: options.html, + text: options.text || '' + }; + + const info = await this.transporter.sendMail(mailOptions); + + console.log('📧 Email sent:', { + messageId: info.messageId, + to: options.to, + subject: options.subject, + previewURL: nodemailer.getTestMessageUrl(info) + }); + + return info; + } catch (error) { + console.error('Email send error:', error); + throw new Error('Failed to send email'); + } + } + + /** + * Welcome email küldése új user számára + * + * @param {string} userEmail - User email címe + * @param {string} userName - User neve + * @returns {Promise} + */ + async sendWelcomeEmail(userEmail, userName) { + try { + const html = await this.renderTemplate('welcome', { + userName, + appName: 'Your App', + loginUrl: `${process.env.APP_URL || 'http://localhost:3000'}/login` + }); + + return await this.sendEmail({ + to: userEmail, + subject: 'Welcome to Your App! 🎉', + html, + text: `Welcome ${userName}! Thank you for registering.` + }); + } catch (error) { + console.error('Welcome email error:', error); + // Ne dobjunk hibát, csak loggoljuk + // Az email küldés hibája ne akadályozza meg a regisztrációt + return null; + } + } + + /** + * Password reset email + * + * @param {string} userEmail - User email címe + * @param {string} resetToken - Password reset token + * @returns {Promise} + */ + async sendPasswordResetEmail(userEmail, resetToken) { + try { + const resetUrl = `${process.env.APP_URL || 'http://localhost:3000'}/reset-password?token=${resetToken}`; + + const html = await this.renderTemplate('password-reset', { + resetUrl, + expiryHours: 1 + }); + + return await this.sendEmail({ + to: userEmail, + subject: 'Password Reset Request 🔒', + html, + text: `Click the following link to reset your password: ${resetUrl}` + }); + } catch (error) { + console.error('Password reset email error:', error); + return null; + } + } + + /** + * Tesztelés: Email szolgáltatás ellenőrzése + * + * @returns {Promise} + */ + async verify() { + try { + await this.transporter.verify(); + console.log('✅ Email service is ready'); + return true; + } catch (error) { + console.error('❌ Email service verification failed:', error); + return false; + } + } +} + +export default EmailService; diff --git a/Backend/negyedik gyakorlat_minta/src/application/services/JwtService.js b/Backend/negyedik gyakorlat_minta/src/application/services/JwtService.js new file mode 100644 index 0000000..15f6bbf --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/src/application/services/JwtService.js @@ -0,0 +1,33 @@ +import jwt from 'jsonwebtoken'; + +export class JwtService { + constructor() { + this.secret = process.env.JWT_SECRET || 'default-secret-key'; + this.expiresIn = process.env.JWT_EXPIRES_IN || '7d'; + } + + generateToken(payload) { + return jwt.sign(payload, this.secret, { expiresIn: this.expiresIn }); + } + + verifyToken(token) { + try { + return jwt.verify(token, this.secret); + } catch (error) { + throw new Error('Invalid or expired token'); + } + } + + setTokenCookie(res, token) { + res.cookie('token', token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days + }); + } + + clearTokenCookie(res) { + res.clearCookie('token'); + } +} diff --git a/Backend/negyedik gyakorlat_minta/src/application/user/commands/UpdateUserProfileCommand.js b/Backend/negyedik gyakorlat_minta/src/application/user/commands/UpdateUserProfileCommand.js new file mode 100644 index 0000000..ea14e11 --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/src/application/user/commands/UpdateUserProfileCommand.js @@ -0,0 +1,7 @@ +export class UpdateUserProfileCommand { + constructor(userId, name, email) { + this.userId = userId; + this.name = name; + this.email = email; + } +} diff --git a/Backend/negyedik gyakorlat_minta/src/application/user/commands/UpdateUserProfileCommandHandler.js b/Backend/negyedik gyakorlat_minta/src/application/user/commands/UpdateUserProfileCommandHandler.js new file mode 100644 index 0000000..a0be083 --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/src/application/user/commands/UpdateUserProfileCommandHandler.js @@ -0,0 +1,26 @@ +export class UpdateUserProfileCommandHandler { + constructor(userRepository) { + this.userRepository = userRepository; + } + + async handle(command) { + const user = await this.userRepository.findById(command.userId); + if (!user) { + throw new Error('User not found'); + } + + const updateData = {}; + if (command.name && command.name.trim()) updateData.name = command.name.trim(); + if (command.email && command.email.trim()) updateData.email = command.email.trim(); + + // Check if email is already taken by another user + if (updateData.email && updateData.email !== user.email) { + const existingUser = await this.userRepository.findByEmail(updateData.email); + if (existingUser) { + throw new Error('Email is already taken'); + } + } + + return await this.userRepository.update(command.userId, updateData); + } +} diff --git a/Backend/negyedik gyakorlat_minta/src/application/user/queries/GetAllUsersQuery.js b/Backend/negyedik gyakorlat_minta/src/application/user/queries/GetAllUsersQuery.js new file mode 100644 index 0000000..0368956 --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/src/application/user/queries/GetAllUsersQuery.js @@ -0,0 +1,2 @@ +export class GetAllUsersQuery { +} diff --git a/Backend/negyedik gyakorlat_minta/src/application/user/queries/GetAllUsersQueryHandler.js b/Backend/negyedik gyakorlat_minta/src/application/user/queries/GetAllUsersQueryHandler.js new file mode 100644 index 0000000..31465f9 --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/src/application/user/queries/GetAllUsersQueryHandler.js @@ -0,0 +1,9 @@ +export class GetAllUsersQueryHandler { + constructor(userRepository) { + this.userRepository = userRepository; + } + + async handle(query) { + return await this.userRepository.findAll(); + } +} diff --git a/Backend/negyedik gyakorlat_minta/src/application/user/queries/GetMeQuery.js b/Backend/negyedik gyakorlat_minta/src/application/user/queries/GetMeQuery.js new file mode 100644 index 0000000..b1f0208 --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/src/application/user/queries/GetMeQuery.js @@ -0,0 +1,5 @@ +export class GetMeQuery { + constructor(userId) { + this.userId = userId; + } +} diff --git a/Backend/negyedik gyakorlat_minta/src/application/user/queries/GetMeQueryHandler.js b/Backend/negyedik gyakorlat_minta/src/application/user/queries/GetMeQueryHandler.js new file mode 100644 index 0000000..f86e51a --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/src/application/user/queries/GetMeQueryHandler.js @@ -0,0 +1,13 @@ +export class GetMeQueryHandler { + constructor(userRepository) { + this.userRepository = userRepository; + } + + async handle(query) { + const user = await this.userRepository.findById(query.userId); + if (!user) { + throw new Error('User not found'); + } + return user; + } +} diff --git a/Backend/negyedik gyakorlat_minta/src/application/user/queries/GetUserByIdQuery.js b/Backend/negyedik gyakorlat_minta/src/application/user/queries/GetUserByIdQuery.js new file mode 100644 index 0000000..c24f2ea --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/src/application/user/queries/GetUserByIdQuery.js @@ -0,0 +1,5 @@ +export class GetUserByIdQuery { + constructor(userId) { + this.userId = userId; + } +} diff --git a/Backend/negyedik gyakorlat_minta/src/application/user/queries/GetUserByIdQueryHandler.js b/Backend/negyedik gyakorlat_minta/src/application/user/queries/GetUserByIdQueryHandler.js new file mode 100644 index 0000000..8f01b57 --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/src/application/user/queries/GetUserByIdQueryHandler.js @@ -0,0 +1,13 @@ +export class GetUserByIdQueryHandler { + constructor(userRepository) { + this.userRepository = userRepository; + } + + async handle(query) { + const user = await this.userRepository.findById(query.userId); + if (!user) { + throw new Error('User not found'); + } + return user; + } +} diff --git a/Backend/negyedik gyakorlat_minta/src/domain/irepositories/IUserRepository.js b/Backend/negyedik gyakorlat_minta/src/domain/irepositories/IUserRepository.js new file mode 100644 index 0000000..939451a --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/src/domain/irepositories/IUserRepository.js @@ -0,0 +1,25 @@ +export class IUserRepository { + async findById(id) { + throw new Error('findById() must be implemented'); + } + + async findByEmail(email) { + throw new Error('findByEmail() must be implemented'); + } + + async findAll() { + throw new Error('findAll() must be implemented'); + } + + async create(userData) { + throw new Error('create() must be implemented'); + } + + async update(id, userData) { + throw new Error('update() must be implemented'); + } + + async delete(id) { + throw new Error('delete() must be implemented'); + } +} diff --git a/Backend/negyedik gyakorlat_minta/src/domain/models/User.js b/Backend/negyedik gyakorlat_minta/src/domain/models/User.js new file mode 100644 index 0000000..6b4d46a --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/src/domain/models/User.js @@ -0,0 +1,40 @@ +export class User { + constructor(data) { + this.id = data.id; + this.name = data.name; + this.email = data.email; + this.password = data.password; + this.createdAt = data.createdAt; + this.updatedAt = data.updatedAt; + } + + // Remove password from JSON serialization + toJSON() { + const { password, ...userWithoutPassword } = this; + return userWithoutPassword; + } + + // Validate user data + static validate(data) { + const errors = []; + + if (!data.name || data.name.trim().length < 2) { + errors.push('Name must be at least 2 characters'); + } + + if (!data.email || !this.isValidEmail(data.email)) { + errors.push('Valid email is required'); + } + + if (!data.password || data.password.length < 6) { + errors.push('Password must be at least 6 characters'); + } + + return errors; + } + + static isValidEmail(email) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + } +} diff --git a/Backend/negyedik gyakorlat_minta/src/email-templates/password-reset.hbs b/Backend/negyedik gyakorlat_minta/src/email-templates/password-reset.hbs new file mode 100644 index 0000000..3a455f0 --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/src/email-templates/password-reset.hbs @@ -0,0 +1,83 @@ + + + + + + + + +
+

Password Reset Request 🔒

+
+ +
+

Reset Your Password

+ +

We received a request to reset your password. Click the button below to create a new password:

+ +

+ Reset Password +

+ +
+

Important:

+
    +
  • This link will expire in {{expiryHours}} hour(s)
  • +
  • If you didn't request this, please ignore this email
  • +
  • Never share this link with anyone
  • +
+
+ +

If the button doesn't work, copy and paste this link into your browser:

+

{{resetUrl}}

+ +

Best regards,
The Security Team

+
+ + + + diff --git a/Backend/negyedik gyakorlat_minta/src/email-templates/welcome.hbs b/Backend/negyedik gyakorlat_minta/src/email-templates/welcome.hbs new file mode 100644 index 0000000..e721718 --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/src/email-templates/welcome.hbs @@ -0,0 +1,69 @@ + + + + + + + + +
+

Welcome to {{appName}}! 🎉

+
+ +
+

Hello {{userName}}!

+ +

Thank you for registering with {{appName}}. We're excited to have you on board!

+ +

Your account has been successfully created. You can now log in and start using our services.

+ +

+ Go to Login +

+ +

If you have any questions or need assistance, feel free to reach out to our support team.

+ +

Best regards,
The {{appName}} Team

+
+ + + + diff --git a/Backend/negyedik gyakorlat_minta/src/infrastructure/db/DatabaseConnection.js b/Backend/negyedik gyakorlat_minta/src/infrastructure/db/DatabaseConnection.js new file mode 100644 index 0000000..f393569 --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/src/infrastructure/db/DatabaseConnection.js @@ -0,0 +1,30 @@ +import { PrismaClient } from '@prisma/client'; + +class DatabaseConnection { + constructor() { + this.prisma = new PrismaClient({ + log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], + }); + } + + async connect() { + try { + await this.prisma.$connect(); + console.log('✅ Database connected successfully'); + } catch (error) { + console.error('❌ Database connection failed:', error); + throw error; + } + } + + async disconnect() { + await this.prisma.$disconnect(); + console.log('🔌 Database disconnected'); + } + + getClient() { + return this.prisma; + } +} + +export default new DatabaseConnection(); diff --git a/Backend/negyedik gyakorlat_minta/src/infrastructure/repositories/UserRepository.js b/Backend/negyedik gyakorlat_minta/src/infrastructure/repositories/UserRepository.js new file mode 100644 index 0000000..29ba37b --- /dev/null +++ b/Backend/negyedik gyakorlat_minta/src/infrastructure/repositories/UserRepository.js @@ -0,0 +1,53 @@ +import { IUserRepository } from '../../domain/irepositories/IUserRepository.js'; +import { User } from '../../domain/models/User.js'; +import DatabaseConnection from '../db/DatabaseConnection.js'; + +export class UserRepository extends IUserRepository { + constructor() { + super(); + this.prisma = DatabaseConnection.getClient(); + } + + async findById(id) { + const user = await this.prisma.user.findUnique({ + where: { id: parseInt(id) } + }); + return user ? new User(user) : null; + } + + async findByEmail(email) { + const user = await this.prisma.user.findUnique({ + where: { email } + }); + return user ? new User(user) : null; + } + + async findAll() { + const users = await this.prisma.user.findMany({ + orderBy: { createdAt: 'desc' } + }); + return users.map(user => new User(user)); + } + + async create(userData) { + const user = await this.prisma.user.create({ + data: userData + }); + return new User(user); + } + + async update(id, userData) { + const user = await this.prisma.user.update({ + where: { id: parseInt(id) }, + data: userData + }); + return new User(user); + } + + async delete(id) { + await this.prisma.user.delete({ + where: { id: parseInt(id) } + }); + return true; + } +}