Replace master branch with local files
This commit is contained in:
commit
875a53f499
60 changed files with 21637 additions and 0 deletions
347
backend/src/services/AuthService.cpp
Normal file
347
backend/src/services/AuthService.cpp
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
#include "AuthService.h"
|
||||
#include "DatabaseService.h"
|
||||
#include "RedisHelper.h"
|
||||
#include <drogon/utils/Utilities.h>
|
||||
#include <regex>
|
||||
#include <random>
|
||||
|
||||
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<void(bool, const std::string&, int64_t)> 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<int64_t>();
|
||||
|
||||
// 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<void(bool, const std::string&, const UserInfo&)> 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<bool>();
|
||||
|
||||
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<std::string>();
|
||||
|
||||
if (!BCrypt::validatePassword(password, hash)) {
|
||||
callback(false, "", UserInfo{});
|
||||
return;
|
||||
}
|
||||
|
||||
UserInfo user;
|
||||
user.id = r[0]["id"].as<int64_t>();
|
||||
user.username = r[0]["username"].as<std::string>();
|
||||
user.isAdmin = r[0]["is_admin"].isNull() ? false : r[0]["is_admin"].as<bool>();
|
||||
user.isStreamer = r[0]["is_streamer"].isNull() ? false : r[0]["is_streamer"].as<bool>();
|
||||
user.isPgpOnly = isPgpOnly;
|
||||
user.bio = r[0]["bio"].isNull() ? "" : r[0]["bio"].as<std::string>();
|
||||
user.avatarUrl = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as<std::string>();
|
||||
user.pgpOnlyEnabledAt = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as<std::string>();
|
||||
|
||||
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<void(bool, const std::string&, const std::string&)> callback) {
|
||||
auto dbClient = app().getDbClient();
|
||||
|
||||
// Generate random challenge
|
||||
auto bytes = drogon::utils::genRandomString(32);
|
||||
std::string challenge = drogon::utils::base64Encode(
|
||||
reinterpret_cast<const unsigned char*>(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<std::string>();
|
||||
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<void(bool, const std::string&, const UserInfo&)> 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<int64_t>();
|
||||
user.username = r[0]["username"].as<std::string>();
|
||||
user.isAdmin = r[0]["is_admin"].isNull() ? false : r[0]["is_admin"].as<bool>();
|
||||
user.isStreamer = r[0]["is_streamer"].isNull() ? false : r[0]["is_streamer"].as<bool>();
|
||||
user.isPgpOnly = r[0]["is_pgp_only"].isNull() ? false : r[0]["is_pgp_only"].as<bool>();
|
||||
user.bio = r[0]["bio"].isNull() ? "" : r[0]["bio"].as<std::string>();
|
||||
user.avatarUrl = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as<std::string>();
|
||||
user.pgpOnlyEnabledAt = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as<std::string>();
|
||||
|
||||
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<void(bool, const std::string&)> 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<std::string>();
|
||||
|
||||
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");
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue