Add account admin mutation endpoints
This commit is contained in:
+286
-2
@@ -1,7 +1,9 @@
|
|||||||
#include "HTTPServer.hh"
|
#include "HTTPServer.hh"
|
||||||
|
|
||||||
|
#include <ctype.h>
|
||||||
#include <inttypes.h>
|
#include <inttypes.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
#include <phosg/Network.hh>
|
#include <phosg/Network.hh>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -30,6 +32,94 @@ HTTPServer::HTTPServer(std::shared_ptr<ServerState> state)
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
auto parse_u32_string = [](const std::string& value_str, int base, const char* field_name) -> uint32_t {
|
||||||
|
if (value_str.empty()) {
|
||||||
|
throw HTTPError(400, std::format("{} is required", field_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t conversion_end = 0;
|
||||||
|
uint64_t value = 0;
|
||||||
|
try {
|
||||||
|
value = std::stoull(value_str, &conversion_end, base);
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
throw HTTPError(400, std::format("Invalid {}", field_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conversion_end != value_str.size()) {
|
||||||
|
throw HTTPError(400, std::format("Invalid {}", field_name));
|
||||||
|
}
|
||||||
|
if (value > 0xFFFFFFFFULL) {
|
||||||
|
throw HTTPError(400, std::format("{} out of range", field_name));
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto parse_u32_decimal_or_0x = [parse_u32_string](const std::string& value_str, const char* field_name) -> uint32_t {
|
||||||
|
int base = 10;
|
||||||
|
if ((value_str.size() >= 2) &&
|
||||||
|
(value_str[0] == '0') &&
|
||||||
|
((value_str[1] == 'x') || (value_str[1] == 'X'))) {
|
||||||
|
base = 16;
|
||||||
|
}
|
||||||
|
return parse_u32_string(value_str, base, field_name);
|
||||||
|
};
|
||||||
|
|
||||||
|
auto parse_account_id = [parse_u32_decimal_or_0x](const std::string& account_id_str) -> uint32_t {
|
||||||
|
return parse_u32_decimal_or_0x(account_id_str, "account ID");
|
||||||
|
};
|
||||||
|
|
||||||
|
auto require_nonzero_u32 = [](uint32_t value, const char* field_name) -> uint32_t {
|
||||||
|
if (value == 0) {
|
||||||
|
throw HTTPError(400, std::format("{} is zero", field_name));
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto require_string_length = [](const std::string& value, const char* field_name, size_t min_size, size_t max_size) -> void {
|
||||||
|
if (value.size() < min_size) {
|
||||||
|
throw HTTPError(400, std::format("{} is too short", field_name));
|
||||||
|
}
|
||||||
|
if (value.size() > max_size) {
|
||||||
|
throw HTTPError(400, std::format("{} is too long", field_name));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
auto require_exact_string_length = [](const std::string& value, const char* field_name, size_t expected_size) -> void {
|
||||||
|
if (value.size() != expected_size) {
|
||||||
|
throw HTTPError(400, std::format("{} length is incorrect", field_name));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
auto normalize_license_type = [](std::string type_str) -> std::string {
|
||||||
|
for (char& ch : type_str) {
|
||||||
|
ch = static_cast<char>(toupper(static_cast<unsigned char>(ch)));
|
||||||
|
}
|
||||||
|
return type_str;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto require_account_admin_secret = [this](const HTTPRequest& req) -> void {
|
||||||
|
const auto& account_sync_config = this->state->config_json->get("AccountSync", phosg::JSON::dict());
|
||||||
|
std::string shared_secret = account_sync_config.get_string("SharedSecret", "");
|
||||||
|
if (shared_secret.empty()) {
|
||||||
|
throw HTTPError(403, "Account admin mutations are disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string* header_secret = req.get_header("x-psopeeps-admin-secret");
|
||||||
|
if (!header_secret || (*header_secret != shared_secret)) {
|
||||||
|
throw HTTPError(403, "Forbidden");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
auto account_mutation_response = [](const char* action, std::shared_ptr<Account> account) -> std::shared_ptr<phosg::JSON> {
|
||||||
|
return std::make_shared<phosg::JSON>(phosg::JSON::dict({
|
||||||
|
{"OK", true},
|
||||||
|
{"Action", action},
|
||||||
|
{"AccountID", account->account_id},
|
||||||
|
{"AccountIDDecimal", std::format("{:010}", account->account_id)},
|
||||||
|
{"AccountIDHex", std::format("{:08X}", account->account_id)},
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
this->router.add(HTTPRequest::Method::GET, "/", [generate_server_version_json](ArgsT&&) -> RetT {
|
this->router.add(HTTPRequest::Method::GET, "/", [generate_server_version_json](ArgsT&&) -> RetT {
|
||||||
co_return std::make_shared<phosg::JSON>(generate_server_version_json());
|
co_return std::make_shared<phosg::JSON>(generate_server_version_json());
|
||||||
});
|
});
|
||||||
@@ -661,8 +751,8 @@ HTTPServer::HTTPServer(std::shared_ptr<ServerState> state)
|
|||||||
co_return res;
|
co_return res;
|
||||||
});
|
});
|
||||||
|
|
||||||
this->router.add(HTTPRequest::Method::GET, "/y/account/:account_id", [this](ArgsT&& args) -> RetT {
|
this->router.add(HTTPRequest::Method::GET, "/y/account/:account_id", [this, parse_account_id](ArgsT&& args) -> RetT {
|
||||||
uint32_t account_id = args.get_param<uint32_t>("account_id");
|
uint32_t account_id = parse_account_id(args.params.at("account_id"));
|
||||||
try {
|
try {
|
||||||
co_return std::make_shared<phosg::JSON>(this->state->account_index->from_account_id(account_id)->json());
|
co_return std::make_shared<phosg::JSON>(this->state->account_index->from_account_id(account_id)->json());
|
||||||
} catch (const AccountIndex::missing_account&) {
|
} catch (const AccountIndex::missing_account&) {
|
||||||
@@ -670,6 +760,200 @@ HTTPServer::HTTPServer(std::shared_ptr<ServerState> state)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this->router.add(HTTPRequest::Method::POST, "/y/account/:account_id/ensure",
|
||||||
|
[this, parse_account_id, parse_u32_decimal_or_0x, require_string_length, require_account_admin_secret, account_mutation_response](ArgsT&& args) -> RetT {
|
||||||
|
require_account_admin_secret(args.req);
|
||||||
|
uint32_t account_id = parse_account_id(args.params.at("account_id"));
|
||||||
|
|
||||||
|
try {
|
||||||
|
std::shared_ptr<Account> account;
|
||||||
|
try {
|
||||||
|
account = this->state->account_index->from_account_id(account_id);
|
||||||
|
} catch (const AccountIndex::missing_account&) {
|
||||||
|
account = std::make_shared<Account>();
|
||||||
|
account->account_id = account_id;
|
||||||
|
account->flags = parse_u32_decimal_or_0x(args.post_data.get_string("flags", "0"), "flags");
|
||||||
|
account->user_flags = parse_u32_decimal_or_0x(args.post_data.get_string("user_flags", "0"), "user_flags");
|
||||||
|
this->state->account_index->add(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string bb_username = args.post_data.get_string("bb_username", "");
|
||||||
|
if (!bb_username.empty()) {
|
||||||
|
auto license = std::make_shared<BBLicense>();
|
||||||
|
license->username = std::move(bb_username);
|
||||||
|
license->password = args.post_data.get_string("bb_password", "");
|
||||||
|
require_string_length(license->username, "bb_username", 1, 16);
|
||||||
|
require_string_length(license->password, "bb_password", 1, 16);
|
||||||
|
|
||||||
|
auto existing_it = account->bb_licenses.find(license->username);
|
||||||
|
if (existing_it != account->bb_licenses.end()) {
|
||||||
|
if (existing_it->second->password != license->password) {
|
||||||
|
throw HTTPError(409, "BB license already exists on account with different password");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this->state->account_index->add_bb_license(account, license);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
account->save();
|
||||||
|
co_return account_mutation_response("ensure-account", account);
|
||||||
|
|
||||||
|
} catch (const HTTPError&) {
|
||||||
|
throw;
|
||||||
|
} catch (const std::runtime_error& e) {
|
||||||
|
if (!strcmp(e.what(), "username already registered")) {
|
||||||
|
throw HTTPError(409, e.what());
|
||||||
|
}
|
||||||
|
throw HTTPError(400, e.what());
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
throw HTTPError(400, e.what());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this->router.add(HTTPRequest::Method::POST, "/y/account/:account_id/add-license",
|
||||||
|
[this, parse_account_id, parse_u32_string, require_nonzero_u32, require_string_length, require_exact_string_length, normalize_license_type, require_account_admin_secret, account_mutation_response](ArgsT&& args) -> RetT {
|
||||||
|
require_account_admin_secret(args.req);
|
||||||
|
uint32_t account_id = parse_account_id(args.params.at("account_id"));
|
||||||
|
|
||||||
|
try {
|
||||||
|
auto account = this->state->account_index->from_account_id(account_id);
|
||||||
|
std::string type_str = normalize_license_type(args.post_data.get_string("type"));
|
||||||
|
|
||||||
|
if (type_str == "DC") {
|
||||||
|
auto license = std::make_shared<V1V2License>();
|
||||||
|
license->serial_number = require_nonzero_u32(parse_u32_string(args.post_data.get_string("serial_number"), 16, "serial_number"), "serial_number");
|
||||||
|
license->access_key = args.post_data.get_string("access_key");
|
||||||
|
require_exact_string_length(license->access_key, "access_key", 8);
|
||||||
|
auto existing_it = account->dc_licenses.find(license->serial_number);
|
||||||
|
if (existing_it != account->dc_licenses.end()) {
|
||||||
|
if (existing_it->second->access_key != license->access_key) {
|
||||||
|
throw HTTPError(409, "DC license already exists on account with different access key");
|
||||||
|
}
|
||||||
|
co_return account_mutation_response("add-license", account);
|
||||||
|
}
|
||||||
|
this->state->account_index->add_dc_license(account, license);
|
||||||
|
|
||||||
|
} else if (type_str == "PC") {
|
||||||
|
auto license = std::make_shared<V1V2License>();
|
||||||
|
license->serial_number = require_nonzero_u32(parse_u32_string(args.post_data.get_string("serial_number"), 16, "serial_number"), "serial_number");
|
||||||
|
license->access_key = args.post_data.get_string("access_key");
|
||||||
|
require_exact_string_length(license->access_key, "access_key", 8);
|
||||||
|
auto existing_it = account->pc_licenses.find(license->serial_number);
|
||||||
|
if (existing_it != account->pc_licenses.end()) {
|
||||||
|
if (existing_it->second->access_key != license->access_key) {
|
||||||
|
throw HTTPError(409, "PC license already exists on account with different access key");
|
||||||
|
}
|
||||||
|
co_return account_mutation_response("add-license", account);
|
||||||
|
}
|
||||||
|
this->state->account_index->add_pc_license(account, license);
|
||||||
|
|
||||||
|
} else if (type_str == "GC") {
|
||||||
|
auto license = std::make_shared<GCLicense>();
|
||||||
|
license->serial_number = require_nonzero_u32(parse_u32_string(args.post_data.get_string("serial_number"), 10, "serial_number"), "serial_number");
|
||||||
|
license->access_key = args.post_data.get_string("access_key");
|
||||||
|
license->password = args.post_data.get_string("password");
|
||||||
|
require_exact_string_length(license->access_key, "access_key", 12);
|
||||||
|
require_string_length(license->password, "password", 1, 8);
|
||||||
|
auto existing_it = account->gc_licenses.find(license->serial_number);
|
||||||
|
if (existing_it != account->gc_licenses.end()) {
|
||||||
|
if ((existing_it->second->access_key != license->access_key) ||
|
||||||
|
(existing_it->second->password != license->password)) {
|
||||||
|
throw HTTPError(409, "GC license already exists on account with different credentials");
|
||||||
|
}
|
||||||
|
co_return account_mutation_response("add-license", account);
|
||||||
|
}
|
||||||
|
this->state->account_index->add_gc_license(account, license);
|
||||||
|
|
||||||
|
} else if (type_str == "BB") {
|
||||||
|
auto license = std::make_shared<BBLicense>();
|
||||||
|
license->username = args.post_data.get_string("username");
|
||||||
|
license->password = args.post_data.get_string("password", "");
|
||||||
|
require_string_length(license->username, "username", 1, 16);
|
||||||
|
require_string_length(license->password, "password", 1, 16);
|
||||||
|
auto existing_it = account->bb_licenses.find(license->username);
|
||||||
|
if (existing_it != account->bb_licenses.end()) {
|
||||||
|
if (existing_it->second->password != license->password) {
|
||||||
|
throw HTTPError(409, "BB license already exists on account with different password");
|
||||||
|
}
|
||||||
|
co_return account_mutation_response("add-license", account);
|
||||||
|
}
|
||||||
|
this->state->account_index->add_bb_license(account, license);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw HTTPError(400, "Invalid license type");
|
||||||
|
}
|
||||||
|
|
||||||
|
account->save();
|
||||||
|
co_return account_mutation_response("add-license", account);
|
||||||
|
|
||||||
|
} catch (const AccountIndex::missing_account&) {
|
||||||
|
throw HTTPError(404, "Account does not exist");
|
||||||
|
} catch (const HTTPError&) {
|
||||||
|
throw;
|
||||||
|
} catch (const std::runtime_error& e) {
|
||||||
|
if (!strcmp(e.what(), "serial number already registered") ||
|
||||||
|
!strcmp(e.what(), "username already registered")) {
|
||||||
|
throw HTTPError(409, e.what());
|
||||||
|
}
|
||||||
|
throw HTTPError(400, e.what());
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
throw HTTPError(400, e.what());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this->router.add(HTTPRequest::Method::POST, "/y/account/:account_id/delete-license",
|
||||||
|
[this, parse_account_id, parse_u32_string, normalize_license_type, require_account_admin_secret, account_mutation_response](ArgsT&& args) -> RetT {
|
||||||
|
require_account_admin_secret(args.req);
|
||||||
|
uint32_t account_id = parse_account_id(args.params.at("account_id"));
|
||||||
|
|
||||||
|
try {
|
||||||
|
auto account = this->state->account_index->from_account_id(account_id);
|
||||||
|
std::string type_str = normalize_license_type(args.post_data.get_string("type"));
|
||||||
|
|
||||||
|
if (type_str == "DC") {
|
||||||
|
uint32_t serial_number = parse_u32_string(args.post_data.get_string("serial_number"), 16, "serial_number");
|
||||||
|
if (account->dc_licenses.find(serial_number) == account->dc_licenses.end()) {
|
||||||
|
co_return account_mutation_response("delete-license", account);
|
||||||
|
}
|
||||||
|
this->state->account_index->remove_dc_license(account, serial_number);
|
||||||
|
|
||||||
|
} else if (type_str == "PC") {
|
||||||
|
uint32_t serial_number = parse_u32_string(args.post_data.get_string("serial_number"), 16, "serial_number");
|
||||||
|
if (account->pc_licenses.find(serial_number) == account->pc_licenses.end()) {
|
||||||
|
co_return account_mutation_response("delete-license", account);
|
||||||
|
}
|
||||||
|
this->state->account_index->remove_pc_license(account, serial_number);
|
||||||
|
|
||||||
|
} else if (type_str == "GC") {
|
||||||
|
uint32_t serial_number = parse_u32_string(args.post_data.get_string("serial_number"), 10, "serial_number");
|
||||||
|
if (account->gc_licenses.find(serial_number) == account->gc_licenses.end()) {
|
||||||
|
co_return account_mutation_response("delete-license", account);
|
||||||
|
}
|
||||||
|
this->state->account_index->remove_gc_license(account, serial_number);
|
||||||
|
|
||||||
|
} else if (type_str == "BB") {
|
||||||
|
std::string username = args.post_data.get_string("username");
|
||||||
|
if (account->bb_licenses.find(username) == account->bb_licenses.end()) {
|
||||||
|
co_return account_mutation_response("delete-license", account);
|
||||||
|
}
|
||||||
|
this->state->account_index->remove_bb_license(account, username);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw HTTPError(400, "Invalid license type");
|
||||||
|
}
|
||||||
|
|
||||||
|
account->save();
|
||||||
|
co_return account_mutation_response("delete-license", account);
|
||||||
|
|
||||||
|
} catch (const AccountIndex::missing_account&) {
|
||||||
|
throw HTTPError(404, "Account does not exist");
|
||||||
|
} catch (const HTTPError&) {
|
||||||
|
throw;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
throw HTTPError(400, e.what());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this->router.add(HTTPRequest::Method::GET, "/y/teams", [this](ArgsT&&) -> RetT {
|
this->router.add(HTTPRequest::Method::GET, "/y/teams", [this](ArgsT&&) -> RetT {
|
||||||
auto res = std::make_shared<phosg::JSON>(phosg::JSON::dict());
|
auto res = std::make_shared<phosg::JSON>(phosg::JSON::dict());
|
||||||
for (const auto& it : this->state->team_index->all()) {
|
for (const auto& it : this->state->team_index->all()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user