#include "AuthService.h" #include "DatabaseService.h" #include "RedisHelper.h" #include #include #include using namespace drogon; using namespace drogon::orm; bool AuthService::validatePassword(const std::string& password, std::string& error) { if (password.length() < 8) { error = "Password must be at least 8 characters long"; return false; } if (!std::regex_search(password, std::regex("[0-9]"))) { error = "Password must contain at least one number"; return false; } if (!std::regex_search(password, std::regex("[!@#$%^&*(),.?\":{}|<>]"))) { error = "Password must contain at least one symbol"; return false; } return true; } void AuthService::registerUser(const std::string& username, const std::string& password, const std::string& publicKey, const std::string& fingerprint, std::function callback) { // Validate username if (username.length() < 3 || username.length() > 30) { callback(false, "Username must be between 3 and 30 characters", 0); return; } if (!std::regex_match(username, std::regex("^[a-zA-Z0-9_]+$"))) { callback(false, "Username can only contain letters, numbers, and underscores", 0); return; } // Validate password std::string error; if (!validatePassword(password, error)) { callback(false, error, 0); return; } auto dbClient = app().getDbClient(); // Check if username exists *dbClient << "SELECT id FROM users WHERE username = $1" << username >> [dbClient, username, password, publicKey, fingerprint, callback](const Result& r) { if (!r.empty()) { callback(false, "Username already exists", 0); return; } // Check if fingerprint exists *dbClient << "SELECT id FROM pgp_keys WHERE fingerprint = $1" << fingerprint >> [dbClient, username, password, publicKey, fingerprint, callback](const Result& r2) { if (!r2.empty()) { callback(false, "This PGP key is already registered", 0); return; } // Hash password std::string hash = BCrypt::generateHash(password); // Begin transaction auto trans = dbClient->newTransaction(); // Insert user with explicit false values for booleans *trans << "INSERT INTO users (username, password_hash, is_admin, is_streamer, is_pgp_only) VALUES ($1, $2, false, false, false) RETURNING id" << username << hash >> [trans, publicKey, fingerprint, callback](const Result& r3) { if (r3.empty()) { callback(false, "Failed to create user", 0); return; } int64_t userId = r3[0]["id"].as(); // Insert PGP key *trans << "INSERT INTO pgp_keys (user_id, public_key, fingerprint) VALUES ($1, $2, $3)" << userId << publicKey << fingerprint >> [trans, callback, userId](const Result&) { // Transaction commits automatically callback(true, "", userId); } >> [trans, callback](const DrogonDbException& e) { LOG_ERROR << "Failed to insert PGP key: " << e.base().what(); callback(false, "Failed to save PGP key", 0); }; } >> [trans, callback](const DrogonDbException& e) { LOG_ERROR << "Failed to insert user: " << e.base().what(); callback(false, "Registration failed", 0); }; } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(false, "Database error", 0); }; } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(false, "Database error", 0); }; } void AuthService::loginUser(const std::string& username, const std::string& password, std::function callback) { auto dbClient = app().getDbClient(); *dbClient << "SELECT id, username, password_hash, is_admin, is_streamer, is_pgp_only, bio, avatar_url, pgp_only_enabled_at " "FROM users WHERE username = $1" << username >> [password, callback, this](const Result& r) { if (r.empty()) { callback(false, "", UserInfo{}); return; } // Check if PGP-only is enabled BEFORE password validation bool isPgpOnly = r[0]["is_pgp_only"].isNull() ? false : r[0]["is_pgp_only"].as(); if (isPgpOnly) { // Return a specific error for PGP-only accounts callback(false, "PGP-only login enabled for this account", UserInfo{}); return; } std::string hash = r[0]["password_hash"].as(); if (!BCrypt::validatePassword(password, hash)) { callback(false, "", UserInfo{}); return; } UserInfo user; user.id = r[0]["id"].as(); user.username = r[0]["username"].as(); user.isAdmin = r[0]["is_admin"].isNull() ? false : r[0]["is_admin"].as(); user.isStreamer = r[0]["is_streamer"].isNull() ? false : r[0]["is_streamer"].as(); user.isPgpOnly = isPgpOnly; user.bio = r[0]["bio"].isNull() ? "" : r[0]["bio"].as(); user.avatarUrl = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as(); user.pgpOnlyEnabledAt = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as(); std::string token = generateToken(user); callback(true, token, user); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(false, "", UserInfo{}); }; } void AuthService::initiatePgpLogin(const std::string& username, std::function callback) { auto dbClient = app().getDbClient(); // Generate random challenge auto bytes = drogon::utils::genRandomString(32); std::string challenge = drogon::utils::base64Encode( reinterpret_cast(bytes.data()), bytes.length() ); // Store challenge in Redis with 5 minute TTL RedisHelper::storeKeyAsync("pgp_challenge:" + username, challenge, 300, [dbClient, username, challenge, callback](bool stored) { if (!stored) { callback(false, "", ""); return; } // Get user's latest public key *dbClient << "SELECT pk.public_key FROM pgp_keys pk " "JOIN users u ON pk.user_id = u.id " "WHERE u.username = $1 " "ORDER BY pk.created_at DESC LIMIT 1" << username >> [callback, challenge](const Result& r) { if (r.empty()) { callback(false, "", ""); return; } std::string publicKey = r[0]["public_key"].as(); callback(true, challenge, publicKey); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(false, "", ""); }; } ); } void AuthService::verifyPgpLogin(const std::string& username, const std::string& signature, const std::string& challenge, std::function callback) { // Get stored challenge from Redis RedisHelper::getKeyAsync("pgp_challenge:" + username, [username, signature, challenge, callback, this](const std::string& storedChallenge) { if (storedChallenge.empty() || storedChallenge != challenge) { callback(false, "", UserInfo{}); return; } // Delete challenge after use RedisHelper::deleteKeyAsync("pgp_challenge:" + username, [](bool) {}); // In a real implementation, you would verify the signature here // For now, we'll trust the client-side verification auto dbClient = app().getDbClient(); *dbClient << "SELECT id, username, is_admin, is_streamer, is_pgp_only, bio, avatar_url, pgp_only_enabled_at " "FROM users WHERE username = $1" << username >> [callback, this](const Result& r) { if (r.empty()) { callback(false, "", UserInfo{}); return; } UserInfo user; user.id = r[0]["id"].as(); user.username = r[0]["username"].as(); user.isAdmin = r[0]["is_admin"].isNull() ? false : r[0]["is_admin"].as(); user.isStreamer = r[0]["is_streamer"].isNull() ? false : r[0]["is_streamer"].as(); user.isPgpOnly = r[0]["is_pgp_only"].isNull() ? false : r[0]["is_pgp_only"].as(); user.bio = r[0]["bio"].isNull() ? "" : r[0]["bio"].as(); user.avatarUrl = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as(); user.pgpOnlyEnabledAt = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as(); std::string token = generateToken(user); callback(true, token, user); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(false, "", UserInfo{}); }; } ); } std::string AuthService::generateToken(const UserInfo& user) { if (jwtSecret_.empty()) { const char* envSecret = std::getenv("JWT_SECRET"); jwtSecret_ = envSecret ? std::string(envSecret) : "your-jwt-secret"; } auto token = jwt::create() .set_issuer("streaming-app") .set_type("JWS") .set_issued_at(std::chrono::system_clock::now()) .set_expires_at(std::chrono::system_clock::now() + std::chrono::hours(24)) .set_payload_claim("user_id", jwt::claim(std::to_string(user.id))) .set_payload_claim("username", jwt::claim(user.username)) .set_payload_claim("is_admin", jwt::claim(std::to_string(user.isAdmin))) .set_payload_claim("is_streamer", jwt::claim(std::to_string(user.isStreamer))) .sign(jwt::algorithm::hs256{jwtSecret_}); return token; } bool AuthService::validateToken(const std::string& token, UserInfo& userInfo) { if (jwtSecret_.empty()) { const char* envSecret = std::getenv("JWT_SECRET"); jwtSecret_ = envSecret ? std::string(envSecret) : "your-jwt-secret"; } try { auto decoded = jwt::decode(token); auto verifier = jwt::verify() .allow_algorithm(jwt::algorithm::hs256{jwtSecret_}) .with_issuer("streaming-app"); verifier.verify(decoded); userInfo.id = std::stoll(decoded.get_payload_claim("user_id").as_string()); userInfo.username = decoded.get_payload_claim("username").as_string(); userInfo.isAdmin = decoded.get_payload_claim("is_admin").as_string() == "1"; userInfo.isStreamer = decoded.has_payload_claim("is_streamer") ? decoded.get_payload_claim("is_streamer").as_string() == "1" : false; return true; } catch (const std::exception& e) { LOG_DEBUG << "Token validation failed: " << e.what(); return false; } } void AuthService::updatePassword(int64_t userId, const std::string& oldPassword, const std::string& newPassword, std::function callback) { // Validate new password std::string error; if (!validatePassword(newPassword, error)) { callback(false, error); return; } auto dbClient = app().getDbClient(); // Verify old password *dbClient << "SELECT password_hash FROM users WHERE id = $1" << userId >> [oldPassword, newPassword, userId, dbClient, callback](const Result& r) { if (r.empty()) { callback(false, "User not found"); return; } std::string hash = r[0]["password_hash"].as(); if (!BCrypt::validatePassword(oldPassword, hash)) { callback(false, "Incorrect password"); return; } // Update password std::string newHash = BCrypt::generateHash(newPassword); *dbClient << "UPDATE users SET password_hash = $1 WHERE id = $2" << newHash << userId >> [callback](const Result&) { callback(true, ""); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to update password: " << e.base().what(); callback(false, "Failed to update password"); }; } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(false, "Database error"); }; }