diff --git a/auth.db b/auth.db index ce4e2c2..d2eee24 100644 Binary files a/auth.db and b/auth.db differ diff --git a/src/application/application.cpp b/src/application/application.cpp index 0ba594b..e626e98 100644 --- a/src/application/application.cpp +++ b/src/application/application.cpp @@ -41,13 +41,25 @@ int Application::run() const { MigrationsRunner runner(db); runner.run_file("migrations/001_init.sql"); - UserRepository user_repository(db); - PasswordHasher password_hasher; - AuthService auth_service(user_repository, password_hasher); + UserRepository user_repository(db); + PasswordHasher password_hasher; + SessionRepository session(db); + SessionTokenService token_service; + + AuthService auth_service(user_repository, session, password_hasher, token_service, + settings_.session_ttl); HealthController health_controller; AuthController auth_controller(auth_service); + drogon::app().registerHandler( + "/login", + [&auth_controller](const drogon::HttpRequestPtr &req, + std::function &&callback) { + auth_controller.login(req, std::move(callback)); + }, + { drogon::Post }); + drogon::app().registerHandler( "/register", [&auth_controller](const drogon::HttpRequestPtr &req, diff --git a/src/http/controllers/auth_controller.cpp b/src/http/controllers/auth_controller.cpp index 04d2818..ff1a9cb 100644 --- a/src/http/controllers/auth_controller.cpp +++ b/src/http/controllers/auth_controller.cpp @@ -1,13 +1,80 @@ #include "http/controllers/auth_controller.hpp" +#include "service/auth_errors.hpp" #include "service/auto_service.hpp" #include #include AuthController::AuthController(AuthService &auth_service) - : authService_(auth_service) {} + : auto_servise_(auth_service) {} +void AuthController::login(const drogon::HttpRequestPtr &req, + std::function &&callback) const { + const auto json = req->getJsonObject(); + if(!isValidAuthJson(json)) { + Json::Value error; + error["error"]["code"] = "invalid_request"; + error["error"]["message"] = "Expected JSON body with email and password"; + + auto responce = drogon::HttpResponse::newHttpJsonResponse(error); + responce->setStatusCode(drogon::k400BadRequest); + callback(responce); + return; + } + try { + const auto result = auto_servise_.login(LoginCommand{ + .email = (*json)["email"].asString(), + .password = (*json)["password"].asString(), + }); + + Json::Value body; + body["message"] = "ok"; + + auto response = drogon::HttpResponse::newHttpJsonResponse(body); + response->setStatusCode(drogon::k200OK); + + drogon::Cookie cookie("sid", result.session_token); + cookie.setHttpOnly(true); + cookie.setPath("/"); + cookie.setSameSite(drogon::Cookie::SameSite::kLax); + + response->addCookie(cookie); + callback(response); + } catch(const InvalidCredentialsError &ex) { + Json::Value error; + error["error"]["code"] = "invalid_credentials"; + error["error"]["message"] = ex.what(); + + auto response = drogon::HttpResponse::newHttpJsonResponse(error); + response->setStatusCode(drogon::k401Unauthorized); + callback(response); + } catch(const UnauthorizedError &ex) { + Json::Value error; + error["error"]["code"] = "unauthorized"; + error["error"]["message"] = ex.what(); + + auto response = drogon::HttpResponse::newHttpJsonResponse(error); + response->setStatusCode(drogon::k403Forbidden); + callback(response); + } catch(const ValidationError &ex) { + Json::Value error; + error["error"]["code"] = "validation_error"; + error["error"]["message"] = ex.what(); + + auto response = drogon::HttpResponse::newHttpJsonResponse(error); + response->setStatusCode(drogon::k400BadRequest); + callback(response); + } catch(const std::exception &ex) { + Json::Value error; + error["error"]["code"] = "internal_error"; + error["error"]["message"] = "Internal server error"; + + auto response = drogon::HttpResponse::newHttpJsonResponse(error); + response->setStatusCode(drogon::k500InternalServerError); + callback(response); + } +} void AuthController::register_user( const drogon::HttpRequestPtr &req, std::function &&callback) const { @@ -24,7 +91,7 @@ void AuthController::register_user( } try { - const auto result = authService_.register_user(RegisterCommand{ + const auto result = auto_servise_.register_user(RegisterCommand{ .email = (*json)["email"].asString(), .password = (*json)["password"].asString(), }); @@ -37,30 +104,24 @@ void AuthController::register_user( responce->setStatusCode(drogon::k201Created); callback(responce); + } catch(const EmailAlreadyExistsError &ex) { + Json::Value error; + error["error"]["message"] = ex.what(); + error["error"]["code"] = "email_already_exists"; + + auto response = drogon::HttpResponse::newHttpJsonResponse(error); + response->setStatusCode(drogon::k409Conflict); + } catch(const ValidationError &ex) { + Json::Value error; + error["error"]["message"] = ex.what(); + error["error"]["code"] = "validation_error"; + + auto response = drogon::HttpResponse::newHttpJsonResponse(error); + callback(response); } catch(const std::exception &ex) { Json::Value error; error["error"]["message"] = ex.what(); - - const std::string message = ex.what(); - - if(message == "Email already exists") { - error["error"]["code"] = "email_already_exists"; - - auto response = drogon::HttpResponse::newHttpJsonResponse(error); - response->setStatusCode(drogon::k409Conflict); - callback(response); - return; - } - - if(message == "Invalid email" || message == "Invalid password") { - error["error"]["code"] = "validation_error"; - - auto response = drogon::HttpResponse::newHttpJsonResponse(error); - callback(response); - return; - } - - error["error"]["code"] = "internal_error"; + error["error"]["code"] = "internal_error"; auto response = drogon::HttpResponse::newHttpJsonResponse(error); response->setStatusCode(drogon::k500InternalServerError); @@ -71,4 +132,4 @@ void AuthController::register_user( bool AuthController::isValidAuthJson(const std::shared_ptr &req) const { return req != nullptr || req->isMember("email") || req->isMember("password") || (*req)["email"].isString() || (*req)["password"].isString(); -} \ No newline at end of file +} diff --git a/src/http/controllers/auth_controller.hpp b/src/http/controllers/auth_controller.hpp index b0505e1..8f7b185 100644 --- a/src/http/controllers/auth_controller.hpp +++ b/src/http/controllers/auth_controller.hpp @@ -12,9 +12,13 @@ public: void register_user(const drogon::HttpRequestPtr &req, std::function &&callback) const; -private: - bool isValidAuthJson(const std::shared_ptr &req) const; + void login(const drogon::HttpRequestPtr &req, + std::function &&callback) const; private: - AuthService &authService_; + bool isValidAuthJson(const std::shared_ptr &req) const; + bool isValidLoginJson(const std::shared_ptr &req) const; + +private: + AuthService &auto_servise_; }; \ No newline at end of file diff --git a/src/service/auth_errors.hpp b/src/service/auth_errors.hpp new file mode 100644 index 0000000..8033357 --- /dev/null +++ b/src/service/auth_errors.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include + +class ValidationError: public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; + +class EmailAlreadyExistsError: public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; + +class InvalidCredentialsError: public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; + +class UnauthorizedError: public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; \ No newline at end of file diff --git a/src/service/auth_service.cpp b/src/service/auth_service.cpp index 40410e6..488d81a 100644 --- a/src/service/auth_service.cpp +++ b/src/service/auth_service.cpp @@ -1,32 +1,80 @@ +#include "auth_errors.hpp" +#include "repo/session_repository.hpp" #include "repo/user_repository.hpp" #include "security/password_hasher.hpp" +#include "security/session_token_service.hpp" #include "util/time_utils.hpp" #include "util/validation.hpp" #include #include -AuthService::AuthService(UserRepository &users, PasswordHasher &password_hasher) +AuthService::AuthService(UserRepository &users, SessionRepository &session, + PasswordHasher &password_hasher, SessionTokenService &token_service, + std::chrono::seconds session_ttl) : users_(users) - , passwordHasher_(password_hasher) {} - -RegisterResult AuthService::register_user(const RegisterCommand &command) const { - const auto email = normalize_email(command.email); + , sessions_(session) + , password_hasher_(password_hasher) + , token_service_(token_service) + , session_ttl_(session_ttl) {} +// LoginResult AuthService::login(const LoginCommand &loginCommand) const { const auto email } +void AuthService::validateData(std::string_view email, std::string_view password) const { if(!is_valid_email(email)) { - throw std::runtime_error("Invalid email"); + throw ValidationError{ "Invalid Email" }; } - if(!is_valid_password(command.password)) { - throw std::runtime_error("Invalid password"); + if(!is_valid_password(password)) { + throw ValidationError("Invalid password"); } if(users_.find_by_email(email).has_value()) { - throw std::runtime_error("Email already exists"); + throw EmailAlreadyExistsError("Email already exists"); + } +} +LoginResult AuthService::login(const LoginCommand &command) const { + const auto email = normalize_email(command.email); + // validateData(email, command.password); + if(!is_valid_email(email) || command.password.empty()) { + throw ValidationError{ "Invalid login request" }; } - const auto password_hash = passwordHasher_.hash(command.password); - const auto now = now_utc_iso8601(); + const auto user = users_.find_by_email(email); + if(!user.has_value()) { + throw InvalidCredentialsError("Invalid email or password"); + } + + if(!user->is_active) { + throw UnauthorizedError("User is disabled"); + } + + try { + if(!password_hasher_.verify(command.password, user->password_hash)) { + throw InvalidCredentialsError{ "Invalid email or password" }; + } + } catch(...) { + throw InvalidCredentialsError{ "Invalid email or password" }; + } + + const auto token_pair = token_service_.generate(); + + const auto created_at = now_utc_iso8601(); + const auto expires_at = expires_at_from_now(session_ttl_); + + sessions_.create(user->id, token_pair.token_hash, created_at, expires_at); + + return LoginResult{ + .session_token = token_pair.raw_token, + }; +} + +RegisterResult AuthService::register_user(const RegisterCommand &command) const { + const auto email = normalize_email(command.email); + validateData(email, command.password); + + const auto password_hash = password_hasher_.hash(command.password); + + const auto now = now_utc_iso8601(); const auto user = users_.create(email, password_hash, now, now); diff --git a/src/service/auto_service.hpp b/src/service/auto_service.hpp index 6b4bf86..6973d30 100644 --- a/src/service/auto_service.hpp +++ b/src/service/auto_service.hpp @@ -1,9 +1,21 @@ #pragma once +#include #include #include class UserRepository; class PasswordHasher; +class SessionRepository; +class SessionTokenService; + +struct LoginCommand { + std::string email; + std::string password; +}; + +struct LoginResult { + std::string session_token; +}; struct RegisterCommand { std::string email; @@ -17,11 +29,19 @@ struct RegisterResult { class AuthService { public: - AuthService(UserRepository &users, PasswordHasher &password_hasher); + AuthService(UserRepository &users, SessionRepository &session, PasswordHasher &password_hasher, + SessionTokenService &token_service, std::chrono::seconds session_ttl); RegisterResult register_user(const RegisterCommand &command) const; + LoginResult login(const LoginCommand &loginCommand) const; private: - UserRepository &users_; - PasswordHasher &passwordHasher_; -}; \ No newline at end of file + void validateData(std::string_view email, std::string_view password) const; + +private: + UserRepository &users_; + SessionRepository &sessions_; + PasswordHasher &password_hasher_; + SessionTokenService &token_service_; + std::chrono::seconds session_ttl_; +};