Fix
ci / frontend (push) Successful in 9m11s
ci / backend (push) Failing after 16m6s

This commit is contained in:
khodowany
2026-10-06 09:07:41 -05:00
parent 09d976e988
commit fa99c6624a
2 changed files with 42 additions and 20 deletions
+41 -19
View File
@@ -10,6 +10,9 @@
namespace {
// GCC 12 (PR101367) can destroy lambda captures incorrectly when the lambda is
// created inside a co_await expression. Construct worker tasks before awaiting.
auto execute(std::function<boost::json::value()> operation) -> boost::asio::awaitable<boost::json::value> {
co_return operation();
}
@@ -99,7 +102,8 @@ auto fpol::application::now() const -> std::int64_t {
auto fpol::application::work(std::function<boost::json::value(fpol::database &)> operation) -> boost::asio::awaitable<boost::json::value> {
auto path = config_.database;
co_return co_await boost::asio::co_spawn(database_worker_, execute([path, operation = std::move(operation)]() {auto database=fpol::database{path};database.begin();auto value=operation(database);database.commit();return value; }), boost::asio::use_awaitable);
auto transaction = execute([path, operation = std::move(operation)]() {auto database=fpol::database{path};database.begin();auto value=operation(database);database.commit();return value; });
co_return co_await boost::asio::co_spawn(database_worker_, std::move(transaction), boost::asio::use_awaitable);
}
auto fpol::application::hash_work(std::function<boost::json::value()> operation) -> boost::asio::awaitable<boost::json::value> {
@@ -119,7 +123,8 @@ auto fpol::application::hash_work(std::function<boost::json::value()> operation)
auto fpol::application::tick(bool restart) -> boost::asio::awaitable<void> {
auto time = now();
co_await work([time, restart](fpol::database &database) {fpol::advance_drafts(database,time,restart);return boost::json::value{}; });
auto advance = work([time, restart](fpol::database &database) {fpol::advance_drafts(database,time,restart);return boost::json::value{}; });
co_await std::move(advance);
}
auto fpol::application::handle(std::string action, boost::json::value input, std::string token, std::string csrf, std::string peer) -> boost::asio::awaitable<boost::json::value> {
@@ -130,7 +135,7 @@ auto fpol::application::handle(std::string action, boost::json::value input, std
auto name = username(input);
auto password = fpol::text(input, "password");
ensure(password.size() <= 256, "Invalid credentials", 401);
auto user = co_await work([name, peer, time](fpol::database &database) {
auto lookup = work([name, peer, time](fpol::database &database) {
for (auto const &key : {"user:" + name, "peer:" + peer}) {
auto limit = database.one("SELECT * FROM login_limits WHERE key=?", {key});
auto maximum = key.starts_with("user:") ? 10 : 100;
@@ -141,35 +146,47 @@ auto fpol::application::handle(std::string action, boost::json::value input, std
}
return boost::json::value{database.one("SELECT * FROM users WHERE username=?", {name})};
});
auto user = co_await std::move(lookup);
auto hash = user.as_object().empty() ? "scrypt$131072$8$1$" + std::string(32, '0') + "$" + std::string(128, '0') : fpol::text(user, "password_hash");
auto valid = co_await hash_work([password, hash]() { return boost::json::value{fpol::password_matches(password, hash)}; });
auto verification = hash_work([password, hash]() { return boost::json::value{fpol::password_matches(password, hash)}; });
auto valid = co_await std::move(verification);
ensure(valid.as_bool() && !user.as_object().empty(), "Invalid username or password", 401);
co_return co_await work([user, hash, name, time](fpol::database &database) {auto current=database.one("SELECT password_hash FROM users WHERE id=?",{fpol::text(user,"id")});ensure(!current.empty()&&fpol::text(current,"password_hash")==hash,"Password changed; sign in again",401);database.query("DELETE FROM login_limits WHERE key=?",{"user:"+name});return session(database,fpol::text(user,"id"),time); });
auto login = work([user, hash, name, time](fpol::database &database) {auto current=database.one("SELECT password_hash FROM users WHERE id=?",{fpol::text(user,"id")});ensure(!current.empty()&&fpol::text(current,"password_hash")==hash,"Password changed; sign in again",401);database.query("DELETE FROM login_limits WHERE key=?",{"user:"+name});return session(database,fpol::text(user,"id"),time); });
co_return co_await std::move(login);
}
if (action == "auth/register") {
auto name = username(input);
auto invite = fpol::text(input, "invite");
auto password = fpol::text(input, "password");
auto team_name = fpol::text(input, "team_name");
co_await work([name, invite, time](fpol::database &database) {ensure(database.one("SELECT id FROM users WHERE username=?",{name}).empty(),"Username is unavailable",409);ensure(!database.one("SELECT token_hash FROM invites WHERE token_hash=? AND expires_at>?",{fpol::digest(invite),time}).empty(),"Invitation is invalid or expired",404);return boost::json::value{}; });
auto hash = co_await hash_work([password]() { return boost::json::value{fpol::password_hash(password)}; });
co_return co_await work([=, this](fpol::database &database) {ensure(database.one("SELECT id FROM users WHERE username=?",{name}).empty(),"Username is unavailable",409);auto id=fpol::random_token(12);database.query("INSERT INTO users(id,username,password_hash,created_at) VALUES(?,?,?,?)",{id,name,std::string{hash.as_string()},time});auto user=database.one("SELECT id,username,admin FROM users WHERE id=?",{id});game_action(database,"league/join",boost::json::object{{"invite",invite},{"team_name",team_name}},user,time,config_.allow_demo);return session(database,id,time); });
auto validation = work([name, invite, time](fpol::database &database) {ensure(database.one("SELECT id FROM users WHERE username=?",{name}).empty(),"Username is unavailable",409);ensure(!database.one("SELECT token_hash FROM invites WHERE token_hash=? AND expires_at>?",{fpol::digest(invite),time}).empty(),"Invitation is invalid or expired",404);return boost::json::value{}; });
co_await std::move(validation);
auto hashing = hash_work([password]() { return boost::json::value{fpol::password_hash(password)}; });
auto hash = co_await std::move(hashing);
auto registration = work([=, this](fpol::database &database) {ensure(database.one("SELECT id FROM users WHERE username=?",{name}).empty(),"Username is unavailable",409);auto id=fpol::random_token(12);database.query("INSERT INTO users(id,username,password_hash,created_at) VALUES(?,?,?,?)",{id,name,std::string{hash.as_string()},time});auto user=database.one("SELECT id,username,admin FROM users WHERE id=?",{id});game_action(database,"league/join",boost::json::object{{"invite",invite},{"team_name",team_name}},user,time,config_.allow_demo);return session(database,id,time); });
co_return co_await std::move(registration);
}
if (action == "auth/reset") {
auto reset = fpol::text(input, "reset_token");
auto password = fpol::text(input, "password");
auto previous = co_await work([reset, time](fpol::database &database) {auto value=database.one("SELECT user_id FROM password_resets WHERE token_hash=? AND expires_at>?",{fpol::digest(reset),time});ensure(!value.empty(),"Reset link is invalid or expired",400);return boost::json::value{value}; });
auto hash = co_await hash_work([password]() { return boost::json::value{fpol::password_hash(password)}; });
co_return co_await work([reset, hash, time, previous](fpol::database &database) {auto value=database.one("SELECT user_id FROM password_resets WHERE token_hash=? AND expires_at>?",{fpol::digest(reset),time});ensure(!value.empty(),"Reset link is invalid or expired",400);auto id=fpol::text(previous,"user_id");database.query("UPDATE users SET password_hash=? WHERE id=?",{std::string{hash.as_string()},id});database.query("DELETE FROM sessions WHERE user_id=?",{id});database.query("DELETE FROM password_resets WHERE user_id=?",{id});return success(boost::json::object{{"message","Password reset. Sign in with your new password."}}); });
auto lookup = work([reset, time](fpol::database &database) {auto value=database.one("SELECT user_id FROM password_resets WHERE token_hash=? AND expires_at>?",{fpol::digest(reset),time});ensure(!value.empty(),"Reset link is invalid or expired",400);return boost::json::value{value}; });
auto previous = co_await std::move(lookup);
auto hashing = hash_work([password]() { return boost::json::value{fpol::password_hash(password)}; });
auto hash = co_await std::move(hashing);
auto reset_password = work([reset, hash, time, previous](fpol::database &database) {auto value=database.one("SELECT user_id FROM password_resets WHERE token_hash=? AND expires_at>?",{fpol::digest(reset),time});ensure(!value.empty(),"Reset link is invalid or expired",400);auto id=fpol::text(previous,"user_id");database.query("UPDATE users SET password_hash=? WHERE id=?",{std::string{hash.as_string()},id});database.query("DELETE FROM sessions WHERE user_id=?",{id});database.query("DELETE FROM password_resets WHERE user_id=?",{id});return success(boost::json::object{{"message","Password reset. Sign in with your new password."}}); });
co_return co_await std::move(reset_password);
}
if (action == "auth/password") {
auto previous = co_await work([=](fpol::database &database) {auto user=authenticated(database,token,csrf,time,true);return boost::json::value{database.one("SELECT id,password_hash FROM users WHERE id=?",{fpol::text(user,"id")})}; });
auto lookup = work([=](fpol::database &database) {auto user=authenticated(database,token,csrf,time,true);return boost::json::value{database.one("SELECT id,password_hash FROM users WHERE id=?",{fpol::text(user,"id")})}; });
auto previous = co_await std::move(lookup);
auto old_password = fpol::text(input, "current_password");
auto password = fpol::text(input, "password");
auto hash = co_await hash_work([previous, old_password, password]() {ensure(fpol::password_matches(old_password,fpol::text(previous,"password_hash")),"Current password is incorrect",401);return boost::json::value{fpol::password_hash(password)}; });
co_return co_await work([=](fpol::database &database) {auto user=authenticated(database,token,csrf,time,true);auto current=database.one("SELECT password_hash FROM users WHERE id=?",{fpol::text(user,"id")});ensure(fpol::text(current,"password_hash")==fpol::text(previous,"password_hash"),"Password changed; sign in again",409);database.query("UPDATE users SET password_hash=? WHERE id=?",{std::string{hash.as_string()},fpol::text(user,"id")});database.query("DELETE FROM sessions WHERE user_id=?",{fpol::text(user,"id")});return session(database,fpol::text(user,"id"),time); });
auto hashing = hash_work([previous, old_password, password]() {ensure(fpol::password_matches(old_password,fpol::text(previous,"password_hash")),"Current password is incorrect",401);return boost::json::value{fpol::password_hash(password)}; });
auto hash = co_await std::move(hashing);
auto change_password = work([=](fpol::database &database) {auto user=authenticated(database,token,csrf,time,true);auto current=database.one("SELECT password_hash FROM users WHERE id=?",{fpol::text(user,"id")});ensure(fpol::text(current,"password_hash")==fpol::text(previous,"password_hash"),"Password changed; sign in again",409);database.query("UPDATE users SET password_hash=? WHERE id=?",{std::string{hash.as_string()},fpol::text(user,"id")});database.query("DELETE FROM sessions WHERE user_id=?",{fpol::text(user,"id")});return session(database,fpol::text(user,"id"),time); });
co_return co_await std::move(change_password);
}
co_return co_await work([=, this](fpol::database &database) {
auto dispatch = work([=, this](fpol::database &database) {
auto user = authenticated(database, token, csrf, time, mutation(action));
if (action == "auth/me") {
return success(user);
@@ -272,6 +289,7 @@ auto fpol::application::handle(std::string action, boost::json::value input, std
}
return success(fpol::game_action(database, action, input, user, time, config_.allow_demo));
});
co_return co_await std::move(dispatch);
} catch (fpol::api_error const &error) {
co_return boost::json::object{{"ok", false}, {"status", error.status}, {"message", error.what()}, {"code", std::to_string(error.status)}};
} catch (std::invalid_argument const &error) {
@@ -293,15 +311,19 @@ auto fpol::application::admin(std::string action, boost::json::value input) -> b
if (action == "create-user") {
auto name = username(input);
auto password = fpol::text(input, "password");
auto hash = co_await hash_work([password]() { return boost::json::value{fpol::password_hash(password)}; });
co_return co_await work([name, hash, time](fpol::database &database) {ensure(database.one("SELECT id FROM users WHERE username=?",{name}).empty(),"Username already exists",409);auto id=fpol::random_token(12);database.query("INSERT INTO users(id,username,password_hash,admin,created_at) VALUES(?,?,?,1,?)",{id,name,std::string{hash.as_string()},time});return boost::json::value{boost::json::object{{"id",id},{"username",name}}}; });
auto hashing = hash_work([password]() { return boost::json::value{fpol::password_hash(password)}; });
auto hash = co_await std::move(hashing);
auto creation = work([name, hash, time](fpol::database &database) {ensure(database.one("SELECT id FROM users WHERE username=?",{name}).empty(),"Username already exists",409);auto id=fpol::random_token(12);database.query("INSERT INTO users(id,username,password_hash,admin,created_at) VALUES(?,?,?,1,?)",{id,name,std::string{hash.as_string()},time});return boost::json::value{boost::json::object{{"id",id},{"username",name}}}; });
co_return co_await std::move(creation);
}
if (action == "reset-link") {
auto name = username(input);
co_return co_await work([name, time](fpol::database &database) {auto user=database.one("SELECT id FROM users WHERE username=?",{name});ensure(!user.empty(),"Unknown account",404);auto token=fpol::random_token();auto id=fpol::text(user,"id");database.query("DELETE FROM password_resets WHERE user_id=?",{id});database.query("INSERT INTO password_resets VALUES(?,?,?)",{fpol::digest(token),id,time+1800});return boost::json::value{boost::json::object{{"reset_token",token},{"expires_at",time+1800}}}; });
auto reset = work([name, time](fpol::database &database) {auto user=database.one("SELECT id FROM users WHERE username=?",{name});ensure(!user.empty(),"Unknown account",404);auto token=fpol::random_token();auto id=fpol::text(user,"id");database.query("DELETE FROM password_resets WHERE user_id=?",{id});database.query("INSERT INTO password_resets VALUES(?,?,?)",{fpol::digest(token),id,time+1800});return boost::json::value{boost::json::object{{"reset_token",token},{"expires_at",time+1800}}}; });
co_return co_await std::move(reset);
}
if (action == "sources") {
co_return co_await work([](fpol::database &database) { return boost::json::value{database.query("SELECT * FROM sources ORDER BY id")}; });
auto listing = work([](fpol::database &database) { return boost::json::value{database.query("SELECT * FROM sources ORDER BY id")}; });
co_return co_await std::move(listing);
}
throw fpol::api_error{400, "Unknown administrative action"};
}
+1 -1
View File
@@ -24,7 +24,7 @@ class application_tests(unittest.TestCase):
def command(*args, stdin=None):
run = subprocess.run([str(ADMIN), "--database", str(cls.base_database), "--allow-demo", *args], input=stdin, text=True, capture_output=True, timeout=30)
if run.returncode:
raise RuntimeError(run.stderr)
raise RuntimeError(f"{args[0]} exited with code {run.returncode}:\n{run.stderr}")
return json.loads(run.stdout.splitlines()[-1])
command("import", str(ROOT / "data/demo/catalog.json"), str(ROOT / "data/demo/ratings.json"), str(ROOT / "data/real/us-2026-10-04-partial.json"))
for username in ("alice", "bob", "carol"):