This commit is contained in:
doomtube 2025-08-10 07:55:39 -04:00
parent 4c23ab840a
commit e8864cc853
15 changed files with 4004 additions and 1593 deletions

View file

@ -4,6 +4,8 @@
#include <drogon/utils/Utilities.h>
#include <regex>
#include <random>
#include <functional>
#include <memory>
using namespace drogon;
using namespace drogon::orm;
@ -27,205 +29,306 @@ bool AuthService::validatePassword(const std::string& password, std::string& err
return true;
}
void AuthService::generateUniqueColor(std::function<void(const std::string& color)> callback) {
try {
auto dbClient = app().getDbClient();
// Create a structure to hold the state for recursive attempts
struct ColorGenerator : public std::enable_shared_from_this<ColorGenerator> {
std::mt19937 gen;
std::uniform_int_distribution<> dis;
std::function<void(const std::string&)> callback;
DbClientPtr dbClient;
int attempts;
ColorGenerator(std::function<void(const std::string&)> cb, DbClientPtr db)
: gen(std::random_device{}()),
dis(0, 0xFFFFFF),
callback(cb),
dbClient(db),
attempts(0) {}
void tryGenerate() {
auto self = shared_from_this();
// Limit attempts to prevent infinite recursion
if (++attempts > 100) {
LOG_ERROR << "Failed to generate unique color after 100 attempts";
callback("#561D5E"); // Fallback to default
return;
}
// Generate random color
int colorValue = dis(gen);
char colorHex[8];
snprintf(colorHex, sizeof(colorHex), "#%06X", colorValue);
std::string color(colorHex);
// Check if color exists
*dbClient << "SELECT id FROM users WHERE user_color = $1 LIMIT 1"
<< color
>> [self, color](const Result& r) {
if (r.empty()) {
// Color is unique, use it
self->callback(color);
} else {
// Color exists, try again
self->tryGenerate();
}
}
>> [self](const DrogonDbException& e) {
LOG_ERROR << "Database error checking color: " << e.base().what();
// Fallback to a default color
self->callback("#561D5E");
};
}
};
auto generator = std::make_shared<ColorGenerator>(callback, dbClient);
generator->tryGenerate();
} catch (const std::exception& e) {
LOG_ERROR << "Exception in generateUniqueColor: " << e.what();
callback("#561D5E"); // Fallback to default
}
}
void AuthService::updateUserColor(int64_t userId, const std::string& newColor,
std::function<void(bool, const std::string&, const std::string&)> callback) {
try {
// Validate color format
if (newColor.length() != 7 || newColor[0] != '#') {
callback(false, "Invalid color format. Use #RRGGBB", "");
return;
}
// Check if color is valid hex
for (size_t i = 1; i < 7; i++) {
if (!std::isxdigit(newColor[i])) {
callback(false, "Invalid color format. Use #RRGGBB", "");
return;
}
}
auto dbClient = app().getDbClient();
// Check if color is already taken
*dbClient << "SELECT id FROM users WHERE user_color = $1 AND id != $2 LIMIT 1"
<< newColor << userId
>> [dbClient, userId, newColor, callback](const Result& r) {
if (!r.empty()) {
callback(false, "This color is already taken", "");
return;
}
// Update the color
*dbClient << "UPDATE users SET user_color = $1 WHERE id = $2 RETURNING user_color"
<< newColor << userId
>> [callback, newColor](const Result& r2) {
if (!r2.empty()) {
callback(true, "", newColor);
} else {
callback(false, "Failed to update color", "");
}
}
>> [callback](const DrogonDbException& e) {
LOG_ERROR << "Failed to update color: " << e.base().what();
callback(false, "Database error", "");
};
}
>> [callback](const DrogonDbException& e) {
LOG_ERROR << "Database error: " << e.base().what();
callback(false, "Database error", "");
};
} catch (const std::exception& e) {
LOG_ERROR << "Exception in updateUserColor: " << e.what();
callback(false, "Internal server error", "");
}
}
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;
try {
LOG_DEBUG << "Starting user registration for: " << username;
// 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;
}
LOG_DEBUG << "Validation passed, generating unique color";
// Generate unique color first
generateUniqueColor([this, username, password, publicKey, fingerprint, callback](const std::string& color) {
try {
LOG_DEBUG << "Got unique color: " << color << ", checking username availability";
auto dbClient = app().getDbClient();
if (!dbClient) {
LOG_ERROR << "Database client is null";
callback(false, "Database connection error", 0);
return;
}
// Check if username exists
*dbClient << "SELECT id FROM users WHERE username = $1 LIMIT 1"
<< username
>> [dbClient, username, password, publicKey, fingerprint, color, callback](const Result& r) {
try {
if (!r.empty()) {
LOG_WARN << "Username already exists: " << username;
callback(false, "Username already exists", 0);
return;
}
LOG_DEBUG << "Username available, checking fingerprint";
// Check if fingerprint exists
*dbClient << "SELECT id FROM pgp_keys WHERE fingerprint = $1 LIMIT 1"
<< fingerprint
>> [dbClient, username, password, publicKey, fingerprint, color, callback](const Result& r2) {
try {
if (!r2.empty()) {
LOG_WARN << "Fingerprint already exists";
callback(false, "This PGP key is already registered", 0);
return;
}
LOG_DEBUG << "Fingerprint available, hashing password";
// Hash password
std::string hash;
try {
hash = BCrypt::generateHash(password);
} catch (const std::exception& e) {
LOG_ERROR << "Failed to hash password: " << e.what();
callback(false, "Failed to process password", 0);
return;
}
LOG_DEBUG << "Password hashed, inserting user";
// Insert user first (without transaction for simplicity)
*dbClient << "INSERT INTO users (username, password_hash, is_admin, is_streamer, is_pgp_only, user_color) "
"VALUES ($1, $2, false, false, false, $3) RETURNING id"
<< username << hash << color
>> [dbClient, publicKey, fingerprint, callback, username](const Result& r3) {
try {
if (r3.empty()) {
LOG_ERROR << "Failed to insert user";
callback(false, "Failed to create user", 0);
return;
}
int64_t userId = r3[0]["id"].as<int64_t>();
LOG_INFO << "User created with ID: " << userId;
// Insert PGP key
*dbClient << "INSERT INTO pgp_keys (user_id, public_key, fingerprint) VALUES ($1, $2, $3)"
<< userId << publicKey << fingerprint
>> [callback, userId, username](const Result&) {
LOG_INFO << "User registration complete for: " << username << " (ID: " << userId << ")";
callback(true, "", userId);
}
>> [dbClient, userId, callback](const DrogonDbException& e) {
LOG_ERROR << "Failed to insert PGP key: " << e.base().what();
// Try to clean up the user
*dbClient << "DELETE FROM users WHERE id = $1" << userId >> [](const Result&) {} >> [](const DrogonDbException&) {};
callback(false, "Failed to save PGP key", 0);
};
} catch (const std::exception& e) {
LOG_ERROR << "Exception processing user insert result: " << e.what();
callback(false, "Registration failed", 0);
}
}
>> [callback](const DrogonDbException& e) {
LOG_ERROR << "Failed to insert user: " << e.base().what();
callback(false, "Registration failed", 0);
};
} catch (const std::exception& e) {
LOG_ERROR << "Exception in fingerprint check callback: " << e.what();
callback(false, "Registration failed", 0);
}
}
>> [callback](const DrogonDbException& e) {
LOG_ERROR << "Database error checking fingerprint: " << e.base().what();
callback(false, "Database error", 0);
};
} catch (const std::exception& e) {
LOG_ERROR << "Exception in username check callback: " << e.what();
callback(false, "Registration failed", 0);
}
}
>> [callback](const DrogonDbException& e) {
LOG_ERROR << "Database error checking username: " << e.base().what();
callback(false, "Database error", 0);
};
} catch (const std::exception& e) {
LOG_ERROR << "Exception in color generation callback: " << e.what();
callback(false, "Registration failed", 0);
}
});
} catch (const std::exception& e) {
LOG_ERROR << "Exception in registerUser: " << e.what();
callback(false, "Registration failed", 0);
}
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) {
try {
auto dbClient = app().getDbClient();
if (!dbClient) {
LOG_ERROR << "Database client is null";
callback(false, "", UserInfo{});
return;
}
*dbClient << "SELECT id, username, password_hash, is_admin, is_streamer, is_pgp_only, bio, avatar_url, pgp_only_enabled_at, user_color "
"FROM users WHERE username = $1 LIMIT 1"
<< username
>> [password, callback, this](const Result& r) {
try {
if (r.empty()) {
callback(false, "", "");
callback(false, "", UserInfo{});
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()) {
// 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>();
bool valid = false;
try {
valid = BCrypt::validatePassword(password, hash);
} catch (const std::exception& e) {
LOG_ERROR << "Failed to validate password: " << e.what();
callback(false, "", UserInfo{});
return;
}
if (!valid) {
callback(false, "", UserInfo{});
return;
}
@ -235,49 +338,185 @@ void AuthService::verifyPgpLogin(const std::string& username, const std::string&
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.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>();
user.colorCode = r[0]["user_color"].isNull() ? "#561D5E" : r[0]["user_color"].as<std::string>();
std::string token = generateToken(user);
callback(true, token, user);
}
>> [callback](const DrogonDbException& e) {
LOG_ERROR << "Database error: " << e.base().what();
} catch (const std::exception& e) {
LOG_ERROR << "Exception in login callback: " << e.what();
callback(false, "", UserInfo{});
};
}
}
>> [callback](const DrogonDbException& e) {
LOG_ERROR << "Database error: " << e.base().what();
callback(false, "", UserInfo{});
};
} catch (const std::exception& e) {
LOG_ERROR << "Exception in loginUser: " << e.what();
callback(false, "", UserInfo{});
}
}
void AuthService::initiatePgpLogin(const std::string& username,
std::function<void(bool, const std::string&, const std::string&)> callback) {
try {
auto dbClient = app().getDbClient();
if (!dbClient) {
LOG_ERROR << "Database client is null";
callback(false, "", "");
return;
}
);
// 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, "", "");
};
}
);
} catch (const std::exception& e) {
LOG_ERROR << "Exception in initiatePgpLogin: " << e.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) {
try {
// Get stored challenge from Redis
RedisHelper::getKeyAsync("pgp_challenge:" + username,
[username, signature, challenge, callback, this](const std::string& storedChallenge) {
try {
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();
if (!dbClient) {
LOG_ERROR << "Database client is null";
callback(false, "", UserInfo{});
return;
}
*dbClient << "SELECT id, username, is_admin, is_streamer, is_pgp_only, bio, avatar_url, pgp_only_enabled_at, user_color "
"FROM users WHERE username = $1 LIMIT 1"
<< username
>> [callback, this](const Result& r) {
try {
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>();
user.colorCode = r[0]["user_color"].isNull() ? "#561D5E" : r[0]["user_color"].as<std::string>();
std::string token = generateToken(user);
callback(true, token, user);
} catch (const std::exception& e) {
LOG_ERROR << "Exception processing user data: " << e.what();
callback(false, "", UserInfo{});
}
}
>> [callback](const DrogonDbException& e) {
LOG_ERROR << "Database error: " << e.base().what();
callback(false, "", UserInfo{});
};
} catch (const std::exception& e) {
LOG_ERROR << "Exception in Redis callback: " << e.what();
callback(false, "", UserInfo{});
}
}
);
} catch (const std::exception& e) {
LOG_ERROR << "Exception in verifyPgpLogin: " << e.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";
try {
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)))
.set_payload_claim("color_code", jwt::claim(
user.colorCode.empty() ? "#561D5E" : user.colorCode
)) // Ensure color is never empty
.sign(jwt::algorithm::hs256{jwtSecret_});
return token;
} catch (const std::exception& e) {
LOG_ERROR << "Failed to generate token: " << e.what();
return "";
}
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 {
if (jwtSecret_.empty()) {
const char* envSecret = std::getenv("JWT_SECRET");
jwtSecret_ = envSecret ? std::string(envSecret) : "your-jwt-secret";
}
auto decoded = jwt::decode(token);
auto verifier = jwt::verify()
@ -292,6 +531,14 @@ bool AuthService::validateToken(const std::string& token, UserInfo& userInfo) {
userInfo.isStreamer = decoded.has_payload_claim("is_streamer") ?
decoded.get_payload_claim("is_streamer").as_string() == "1" : false;
// Get color from token if available, otherwise will need to fetch from DB
if (decoded.has_payload_claim("color_code")) {
userInfo.colorCode = decoded.get_payload_claim("color_code").as_string();
} else {
// For older tokens without color, default value
userInfo.colorCode = "#561D5E";
}
return true;
} catch (const std::exception& e) {
LOG_DEBUG << "Token validation failed: " << e.what();
@ -302,46 +549,123 @@ bool AuthService::validateToken(const std::string& token, UserInfo& userInfo) {
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;
try {
// Validate new password
std::string error;
if (!validatePassword(newPassword, error)) {
callback(false, error);
return;
}
auto dbClient = app().getDbClient();
if (!dbClient) {
LOG_ERROR << "Database client is null";
callback(false, "Database connection error");
return;
}
// Verify old password
*dbClient << "SELECT password_hash FROM users WHERE id = $1 LIMIT 1"
<< userId
>> [oldPassword, newPassword, userId, dbClient, callback](const Result& r) {
try {
if (r.empty()) {
callback(false, "User not found");
return;
}
std::string hash = r[0]["password_hash"].as<std::string>();
bool valid = false;
try {
valid = BCrypt::validatePassword(oldPassword, hash);
} catch (const std::exception& e) {
LOG_ERROR << "Failed to validate password: " << e.what();
callback(false, "Password validation error");
return;
}
if (!valid) {
callback(false, "Incorrect password");
return;
}
// Update password
std::string newHash;
try {
newHash = BCrypt::generateHash(newPassword);
} catch (const std::exception& e) {
LOG_ERROR << "Failed to hash new password: " << e.what();
callback(false, "Failed to process new password");
return;
}
*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");
};
} catch (const std::exception& e) {
LOG_ERROR << "Exception in password update callback: " << e.what();
callback(false, "Failed to update password");
}
}
>> [callback](const DrogonDbException& e) {
LOG_ERROR << "Database error: " << e.base().what();
callback(false, "Database error");
};
} catch (const std::exception& e) {
LOG_ERROR << "Exception in updatePassword: " << e.what();
callback(false, "Failed to update password");
}
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;
}
void AuthService::fetchUserInfo(int64_t userId, std::function<void(bool, const UserInfo&)> callback) {
try {
auto dbClient = app().getDbClient();
if (!dbClient) {
LOG_ERROR << "Database client is null";
callback(false, UserInfo{});
return;
}
*dbClient << "SELECT id, username, is_admin, is_streamer, is_pgp_only, bio, avatar_url, pgp_only_enabled_at, user_color "
"FROM users WHERE id = $1 LIMIT 1"
<< userId
>> [callback](const Result& r) {
try {
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>();
user.colorCode = r[0]["user_color"].isNull() ? "#561D5E" : r[0]["user_color"].as<std::string>();
callback(true, user);
} catch (const std::exception& e) {
LOG_ERROR << "Exception processing user data: " << e.what();
callback(false, UserInfo{});
}
}
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");
};
>> [callback](const DrogonDbException& e) {
LOG_ERROR << "Database error: " << e.base().what();
callback(false, UserInfo{});
};
} catch (const std::exception& e) {
LOG_ERROR << "Exception in fetchUserInfo: " << e.what();
callback(false, UserInfo{});
}
}