Skip to content
This repository was archived by the owner on Jun 7, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions modules/Deps.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ IF (WIN32 AND USE_LOCAL_DEPS)
endif ()
ENDIF ()

CPMAddPackage(
NAME nlohmann_json
GITHUB_REPOSITORY nlohmann/json
GIT_TAG v3.12.0
OPTIONS
"JSON_BuildTests OFF"
"BUILD_SHARED_LIBS OFF"
)

CPMAddPackage(
NAME nlohmann_json_schema_validator
GITHUB_REPOSITORY pboettch/json-schema-validator
Expand Down
39 changes: 39 additions & 0 deletions sql/20251129211200_virtual_projects.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
ALTER TABLE project
ADD COLUMN is_virtual bool not null default false;

INSERT INTO project
VALUES ('minecraft',
'Minecraft',
'', '', '', false,
'mod',
'{}',
null,
default,
false,
'minecraft',
true);

INSERT INTO project_version
VALUES (default, 'minecraft', null, '');

UPDATE project_item
SET version_id = (SELECT id FROM project_version WHERE project_id = 'minecraft' AND name IS NULL)
WHERE version_id IS NULL;

UPDATE project_tag
SET version_id = (SELECT id FROM project_version WHERE project_id = 'minecraft' AND name IS NULL)
WHERE version_id IS NULL;

UPDATE recipe_type
SET version_id = (SELECT id FROM project_version WHERE project_id = 'minecraft' AND name IS NULL)
WHERE loc LIKE 'minecraft:%';

ALTER TABLE project_item ALTER COLUMN version_id SET NOT NULL;
ALTER TABLE project_tag ALTER COLUMN version_id SET NOT NULL;
ALTER TABLE recipe_type ALTER COLUMN version_id SET NOT NULL;

DROP INDEX unique_item_no_project;
DROP INDEX unique_tag_no_project;
DROP INDEX unique_recipe_type_no_project;
DROP INDEX unique_recipe_no_project;

14 changes: 7 additions & 7 deletions sql/provided/20250625211200_game_builtins.sql
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ WHERE r.loc IN (
'minecraft:crafting_shaped',
'minecraft:crafting_shapeless'
)
AND r.version_id IS NULL;
AND r.version_id = (SELECT id FROM project_version WHERE project_id = 'minecraft' AND name IS NULL);

INSERT INTO recipe_workbench (type_id, item_id)
SELECT r.id, pitem.id
Expand All @@ -33,7 +33,7 @@ FROM recipe_type r
WHERE r.loc IN (
'minecraft:smelting'
)
AND r.version_id IS NULL;
AND r.version_id = (SELECT id FROM project_version WHERE project_id = 'minecraft' AND name IS NULL);

INSERT INTO recipe_workbench (type_id, item_id)
SELECT r.id, pitem.id
Expand All @@ -42,7 +42,7 @@ FROM recipe_type r
WHERE r.loc IN (
'minecraft:blasting'
)
AND r.version_id IS NULL;
AND r.version_id = (SELECT id FROM project_version WHERE project_id = 'minecraft' AND name IS NULL);

INSERT INTO recipe_workbench (type_id, item_id)
SELECT r.id, pitem.id
Expand All @@ -58,7 +58,7 @@ FROM recipe_type r
WHERE r.loc IN (
'minecraft:campfire_cooking'
)
AND r.version_id IS NULL;
AND r.version_id = (SELECT id FROM project_version WHERE project_id = 'minecraft' AND name IS NULL);

INSERT INTO recipe_workbench (type_id, item_id)
SELECT r.id, pitem.id
Expand All @@ -67,7 +67,7 @@ FROM recipe_type r
WHERE r.loc IN (
'minecraft:smoking'
)
AND r.version_id IS NULL;
AND r.version_id = (SELECT id FROM project_version WHERE project_id = 'minecraft' AND name IS NULL);

INSERT INTO recipe_workbench (type_id, item_id)
SELECT r.id, pitem.id
Expand All @@ -76,7 +76,7 @@ FROM recipe_type r
WHERE r.loc IN (
'minecraft:stonecutting'
)
AND r.version_id IS NULL;
AND r.version_id = (SELECT id FROM project_version WHERE project_id = 'minecraft' AND name IS NULL);

INSERT INTO recipe_workbench (type_id, item_id)
SELECT r.id, pitem.id
Expand All @@ -85,4 +85,4 @@ FROM recipe_type r
WHERE r.loc IN (
'minecraft:smithing_transform'
)
AND r.version_id IS NULL;
AND r.version_id = (SELECT id FROM project_version WHERE project_id = 'minecraft' AND name IS NULL);
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ target_link_libraries(${PROJECT_NAME} PRIVATE
api
Drogon::Drogon
spdlog::spdlog
nlohmann_json::nlohmann_json
nlohmann_json_schema_validator
libgit2
libgit2package
Expand Down
20 changes: 10 additions & 10 deletions src/api/v1/auth.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#include "error.h"

#include <service/external/github.h>
#include <database/database.h>
#include <service/database/database.h>
#include <include/uri.h>
#include <service/util/crypto.h>

Expand Down Expand Up @@ -30,7 +30,7 @@ namespace api::v1 {
}

if (const auto token = co_await global::auth->requestUserAccessToken(code)) {
const auto [profile, err] = co_await global::github->getAuthenticatedUser(*token);
const auto profile = co_await global::github->getAuthenticatedUser(*token);
if (!profile) {
callback(statusResponse(k400BadRequest));
}
Expand Down Expand Up @@ -107,8 +107,8 @@ namespace api::v1 {
}

if (const auto token = co_await global::auth->requestModrinthUserAccessToken(code)) {
if (const auto result = co_await global::auth->linkModrinthAccount(session->username, *token); result != Error::Ok) {
throw ApiException(result, "Error linking Modrinth account");
if (const auto result = co_await global::auth->linkModrinthAccount(session->username, *token); !result) {
throw ApiException(result.error(), "Error linking Modrinth account");
}

const auto resp = HttpResponse::newRedirectionResponse(config_.settingsCallbackUrl);
Expand All @@ -122,8 +122,8 @@ namespace api::v1 {
Task<> AuthController::unlinkModrinth(const HttpRequestPtr req, const std::function<void(const HttpResponsePtr &)> callback) const {
const auto session{co_await global::auth->getSession(req)};

if (const auto result = co_await global::database->unlinkUserModrinthAccount(session.username); result != Error::Ok) {
throw ApiException(result, "Failed to unlink Modrinth account");
if (const auto result = co_await global::database->unlinkUserModrinthAccount(session.username); !result) {
throw ApiException(result.error(), "Failed to unlink Modrinth account");
}

callback(statusResponse(k200OK));
Expand All @@ -140,12 +140,12 @@ namespace api::v1 {
Task<> AuthController::deleteAccount(const HttpRequestPtr req, const std::function<void(const HttpResponsePtr &)> callback) const {
const auto session{co_await global::auth->getSession(req)};

if (const auto result = co_await global::database->deleteUserProjects(session.username); result != Error::Ok) {
throw ApiException(result, "Error deleting account");
if (const auto result = co_await global::database->deleteUserProjects(session.username); !result) {
throw ApiException(result.error(), "Error deleting account");
}

if (const auto result = co_await global::database->deleteUser(session.username); result != Error::Ok) {
throw ApiException(result, "Error deleting account");
if (const auto result = co_await global::database->deleteUser(session.username); !result) {
throw ApiException(result.error(), "Error deleting account");
}

co_await global::auth->expireSession(session.sessionId);
Expand Down
33 changes: 23 additions & 10 deletions src/api/v1/base.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,44 @@
#include <auth.h>
#include <service/system/lang.h>

#include "project/cached.h"

using namespace drogon;
using namespace service;

namespace api::v1 {
Task<ResolvedProject> BaseProjectController::getProjectWithParams(const HttpRequestPtr req, const std::string project) {
void requireNonVirtual(const ProjectBasePtr &project) {
if (project && project->getProject().getValueOfIsVirtual()) {
throw ApiException(Error::ErrNotFound, "Project not found");
}
}

Task<ProjectBasePtr> BaseProjectController::getProjectWithParamsCached(const HttpRequestPtr req, const std::string project) {
const auto resolved(co_await getProjectWithParams(req, project));
co_return std::make_shared<CachedProject>(resolved);
}

Task<ProjectBasePtr> BaseProjectController::getProjectWithParams(const HttpRequestPtr req, const std::string project) {
const auto version = req->getOptionalParameter<std::string>("version");
const auto locale = req->getOptionalParameter<std::string>("locale");
const auto validatedLocale = co_await validateLocale(locale);
co_return co_await getProject(project, version, validatedLocale);
}

Task<ResolvedProject> BaseProjectController::getVersionedProject(const HttpRequestPtr req, const std::string project) {
Task<ProjectBasePtr> BaseProjectController::getVersionedProject(const HttpRequestPtr req, const std::string project) {
const auto version = req->getOptionalParameter<std::string>("version");
co_return co_await getProject(project, version, std::nullopt);
}

Task<ResolvedProject> BaseProjectController::getProject(const std::string &project, const std::optional<std::string> &version,
const std::optional<std::string> &locale) {
Task<ProjectBasePtr> BaseProjectController::getProject(const std::string &project, const std::optional<std::string> &version,
const std::optional<std::string> &locale) {
if (project.empty()) {
throw ApiException(Error::ErrBadRequest, "Missing project parameter");
}

const auto [resolved, resErr](co_await global::storage->getProject(project, version, locale));
const auto resolved = co_await global::storage->getProject(project, version, locale);
if (!resolved) {
throw ApiException(resErr, "Resolution failure");
throw ApiException(resolved.error(), "Resolution failure");
}

co_return *resolved;
Expand All @@ -42,16 +55,16 @@ namespace api::v1 {
co_return *project;
}

Task<ResolvedProject> BaseProjectController::getUserProject(const HttpRequestPtr req, const std::string &id,
const std::optional<std::string> &version,
const std::optional<std::string> &locale) {
Task<ProjectBasePtr> BaseProjectController::getUserProject(const HttpRequestPtr req, const std::string &id,
const std::optional<std::string> &version,
const std::optional<std::string> &locale) {
const auto session{co_await global::auth->getSession(req)};
const auto project{co_await global::database->getUserProject(session.username, id)};
if (!project) {
throw ApiException(Error::ErrNotFound, "not_found");
}

const auto [resolved, resErr](co_await global::storage->getProject(*project, version, locale));
const auto resolved(co_await global::storage->getProject(*project, version, locale));
if (!resolved) {
throw ApiException(Error::ErrNotFound, "not_found");
}
Expand Down
20 changes: 11 additions & 9 deletions src/api/v1/base.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,24 @@
#include <service/storage/storage.h>

namespace api::v1 {
void requireNonVirtual(const service::ProjectBasePtr &project);

class BaseProjectController {
public:
static drogon::Task<service::ResolvedProject>
getProjectWithParams(drogon::HttpRequestPtr req, std::string project);
static drogon::Task<service::ProjectBasePtr> getProjectWithParams(drogon::HttpRequestPtr req, std::string project);

static drogon::Task<service::ProjectBasePtr> getProjectWithParamsCached(drogon::HttpRequestPtr req, std::string project);

static drogon::Task<service::ResolvedProject>
getVersionedProject(drogon::HttpRequestPtr req, std::string project);
static drogon::Task<service::ProjectBasePtr> getVersionedProject(drogon::HttpRequestPtr req, std::string project);

static drogon::Task<service::ResolvedProject> getProject(const std::string &project, const std::optional<std::string> &version,
const std::optional<std::string> &locale);
static drogon::Task<service::ProjectBasePtr> getProject(const std::string &project, const std::optional<std::string> &version,
const std::optional<std::string> &locale);

static drogon::Task<Project> getUserProject(drogon::HttpRequestPtr req, std::string id);

static drogon::Task<service::ResolvedProject> getUserProject(drogon::HttpRequestPtr req, const std::string &project,
const std::optional<std::string> &version,
const std::optional<std::string> &locale);
static drogon::Task<service::ProjectBasePtr> getUserProject(drogon::HttpRequestPtr req, const std::string &project,
const std::optional<std::string> &version,
const std::optional<std::string> &locale);

static nlohmann::json jsonBody(const drogon::HttpRequestPtr &req);
static nlohmann::json validatedBody(const drogon::HttpRequestPtr &req, const nlohmann::json &schema);
Expand Down
8 changes: 4 additions & 4 deletions src/api/v1/browse.cc
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ using namespace drogon_model::postgres;
namespace api::v1 {
Task<> BrowseController::browse(const HttpRequestPtr req, const std::function<void(const HttpResponsePtr &)> callback,
const std::string query, const std::string types, const std::string sort, const int page) const {
const auto [searchResults, searchError] = co_await global::database->findProjects(query, types, sort, page);
const auto searchResults = co_await global::database->findProjects(query, types, sort, page);

Json::Value root;
Json::Value data(Json::arrayValue);
for (const auto &item: searchResults.data) {
for (const auto &item: searchResults->data) {
Json::Value itemJson;
itemJson["id"] = item.getValueOfId();
itemJson["name"] = item.getValueOfName();
Expand All @@ -31,8 +31,8 @@ namespace api::v1 {
itemJson["created_at"] = item.getValueOfCreatedAt().toDbStringLocal();
data.append(itemJson);
}
root["pages"] = searchResults.pages;
root["total"] = searchResults.total;
root["pages"] = searchResults->pages;
root["total"] = searchResults->total;
root["data"] = data;

callback(HttpResponse::newHttpJsonResponse(root));
Expand Down
34 changes: 19 additions & 15 deletions src/api/v1/docs.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ namespace api::v1 {
const std::string project) const {
const auto version = req->getOptionalParameter<std::string>("version");
const auto resolved = co_await BaseProjectController::getProject(project, version, std::nullopt);
requireNonVirtual(resolved);

if (version && !co_await resolved.hasVersion(*version)) {
if (version && !co_await resolved->hasVersion(*version)) {
throw ApiException(Error::ErrNotFound, "Version not found");
}

const auto json = co_await resolved.toJsonVerbose();
const auto json = co_await resolved->toJsonVerbose();
callback(HttpResponse::newHttpJsonResponse(json));
}

Expand All @@ -38,20 +39,21 @@ namespace api::v1 {
}

const auto resolved = co_await BaseProjectController::getProjectWithParams(req, project);
requireNonVirtual(resolved);

const auto [page, pageError](resolved.readPageFile(path + DOCS_FILE_EXT));
if (pageError != Error::Ok) {
const auto page(resolved->readPageFile(path + DOCS_FILE_EXT));
if (!page) {
const auto optionalParam = req->getOptionalParameter<std::string>("optional");
const auto optional = optionalParam.has_value() && optionalParam == "true";

throw ApiException(optional ? Error::Ok : pageError, "File not found");
throw ApiException(optional ? Error::Ok : page.error(), "File not found");
}

Json::Value root;
root["project"] = co_await resolved.toJson();
root["content"] = page.content;
if (resolved.getProject().getValueOfIsPublic() && !page.editUrl.empty()) {
root["edit_url"] = page.editUrl;
root["project"] = co_await resolved->toJson();
root["content"] = page->content;
if (resolved->getProject().getValueOfIsPublic() && !page->editUrl.empty()) {
root["edit_url"] = page->editUrl;
}

callback(HttpResponse::newHttpJsonResponse(root));
Expand All @@ -67,22 +69,24 @@ namespace api::v1 {
Task<> DocsController::tree(const HttpRequestPtr req, const std::function<void(const HttpResponsePtr &)> callback,
const std::string project) const {
const auto resolved = co_await BaseProjectController::getProjectWithParams(req, project);
requireNonVirtual(resolved);

const auto [tree, treeError](resolved.getDirectoryTree());
if (treeError != Error::Ok) {
throw ApiException(treeError, "Error getting directory tree");
const auto tree(co_await resolved->getDirectoryTree());
if (!tree) {
throw ApiException(tree.error(), "Error getting directory tree");
}

nlohmann::json root;
root["project"] = parkourJson(co_await resolved.toJson());
root["tree"] = tree;
root["project"] = parkourJson(co_await resolved->toJson());
root["tree"] = *tree;

callback(jsonResponse(root));
}

Task<> DocsController::asset(HttpRequestPtr req, std::function<void(const HttpResponsePtr &)> callback, std::string project) const {
try {
const auto resolved = co_await BaseProjectController::getProject(project, req->getOptionalParameter<std::string>("version"), std::nullopt);
requireNonVirtual(resolved);

std::string prefix = std::format("/api/v1/docs/{}/asset/", project);
std::string location = req->getPath().substr(prefix.size());
Expand All @@ -96,7 +100,7 @@ namespace api::v1 {
throw ApiException(Error::ErrBadRequest, "Invalid location specified");
}

const auto asset = resolved.getAsset(*resourceLocation);
const auto asset = resolved->getAsset(*resourceLocation);
if (!asset) {
const auto optionalParam = req->getOptionalParameter<std::string>("optional");
const auto optional = optionalParam.has_value() && optionalParam == "true";
Expand Down
Loading