Skip to content

Commit

Permalink
Support arbitrary Git repositories (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
Su5eD authored Feb 2, 2025
2 parents 593624f + 4ef480c commit 9009821
Show file tree
Hide file tree
Showing 17 changed files with 261 additions and 168 deletions.
17 changes: 9 additions & 8 deletions src/api/v1/auth.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,20 @@ namespace api::v1 {
explicit AuthController(const std::string &, const std::string &, const std::string &, const std::string &, const std::string &,
service::Auth &, service::GitHub &, service::MemoryCache &, service::Database &);

// clang-format off
METHOD_LIST_BEGIN
ADD_METHOD_TO(AuthController::initLogin, "/api/v1/auth/login", drogon::Get);
ADD_METHOD_TO(AuthController::callbackGithub, "/api/v1/auth/callback/github?code={1:code}", drogon::Get);
ADD_METHOD_TO(AuthController::logout, "/api/v1/auth/logout", drogon::Get);
ADD_METHOD_TO(AuthController::initLogin, "/api/v1/auth/login", drogon::Get);
ADD_METHOD_TO(AuthController::callbackGithub, "/api/v1/auth/callback/github?code={1:code}", drogon::Get);
ADD_METHOD_TO(AuthController::logout, "/api/v1/auth/logout", drogon::Get);

ADD_METHOD_TO(AuthController::linkModrinth, "/api/v1/auth/link/modrinth", drogon::Get, "AuthFilter");
ADD_METHOD_TO(AuthController::linkModrinth, "/api/v1/auth/link/modrinth", drogon::Get, "AuthFilter");
ADD_METHOD_TO(AuthController::callbackModrinth, "/api/v1/auth/callback/modrinth?code={1:code}&state={2:state}", drogon::Get);
ADD_METHOD_TO(AuthController::unlinkModrinth, "/api/v1/auth/unlink/modrinth", drogon::Post, "AuthFilter");
ADD_METHOD_TO(AuthController::unlinkModrinth, "/api/v1/auth/unlink/modrinth", drogon::Post, "AuthFilter");

ADD_METHOD_TO(AuthController::userProfile, "/api/v1/auth/user", drogon::Get, "AuthFilter");

ADD_METHOD_TO(AuthController::deleteAccount, "/api/v1/auth/user", drogon::Delete, "AuthFilter");
ADD_METHOD_TO(AuthController::userProfile, "/api/v1/auth/user", drogon::Get, "AuthFilter");
ADD_METHOD_TO(AuthController::deleteAccount, "/api/v1/auth/user", drogon::Delete, "AuthFilter");
METHOD_LIST_END
// clang-format on

drogon::Task<> initLogin(drogon::HttpRequestPtr req, std::function<void(const drogon::HttpResponsePtr &)> callback) const;
drogon::Task<> callbackGithub(drogon::HttpRequestPtr req, std::function<void(const drogon::HttpResponsePtr &)> callback,
Expand Down
11 changes: 4 additions & 7 deletions src/api/v1/docs.cc
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,7 @@ namespace api::v1 {
co_return;
}

Json::Value root;
root["project"] = resolved->toJson();

const auto resp = HttpResponse::newHttpJsonResponse(root);
const auto resp = HttpResponse::newHttpJsonResponse(resolved->toJson());
resp->setStatusCode(k200OK);
callback(resp);
} catch (const HttpException &err) {
Expand All @@ -66,7 +63,7 @@ namespace api::v1 {

Task<> DocsController::page(HttpRequestPtr req, std::function<void(const HttpResponsePtr &)> callback, std::string project) const {
try {
std::string prefix = std::format("/api/v1/project/{}/page/", project);
std::string prefix = std::format("/api/v1/docs/{}/page/", project);
std::string path = req->getPath().substr(prefix.size());

if (path.empty()) {
Expand All @@ -90,7 +87,7 @@ namespace api::v1 {
Json::Value root;
root["project"] = resolved->toJson();
root["content"] = page.content;
if (resolved->getProject().getValueOfIsPublic()) {
if (resolved->getProject().getValueOfIsPublic() && !page.editUrl.empty()) {
root["edit_url"] = page.editUrl;
}
if (!page.updatedAt.empty()) {
Expand Down Expand Up @@ -147,7 +144,7 @@ namespace api::v1 {
co_return;
}

std::string prefix = std::format("/api/v1/project/{}/asset/", project);
std::string prefix = std::format("/api/v1/docs/{}/asset", project);
std::string location = req->getPath().substr(prefix.size());

if (location.empty()) {
Expand Down
9 changes: 5 additions & 4 deletions src/api/v1/docs.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ namespace api::v1 {
explicit DocsController(Database &, Storage &);

METHOD_LIST_BEGIN
ADD_METHOD_TO(DocsController::project, "/api/v1/docs/{1:project}", drogon::Get, "AuthFilter");
ADD_METHOD_TO(DocsController::page, "/api/v1/project/{1:project}/page/.*", drogon::Get, "AuthFilter");
ADD_METHOD_TO(DocsController::tree, "/api/v1/project/{1:project}/tree", drogon::Get, "AuthFilter");
ADD_METHOD_TO(DocsController::asset, "/api/v1/project/{1:project}/asset/.*", drogon::Get); // Public
ADD_METHOD_TO(DocsController::project, "/api/v1/docs/{1:project}", drogon::Get, "AuthFilter");
ADD_METHOD_TO(DocsController::page, "/api/v1/docs/{1:project}/page/.*", drogon::Get, "AuthFilter");
ADD_METHOD_TO(DocsController::tree, "/api/v1/docs/{1:project}/tree", drogon::Get, "AuthFilter");
// Public
ADD_METHOD_TO(DocsController::asset, "/api/v1/docs/{1:project}/asset/.*", drogon::Get);
METHOD_LIST_END

drogon::Task<> project(drogon::HttpRequestPtr req, std::function<void(const drogon::HttpResponsePtr &)> callback,
Expand Down
14 changes: 12 additions & 2 deletions src/api/v1/projects.cc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "projects.h"

#include <include/uri.h>
#include <log/log.h>
#include <models/Project.h>
#include <models/UserProject.h>
Expand All @@ -24,10 +25,11 @@ namespace api::v1 {

Task<bool> isProjectPubliclyBrowseable(const std::string &repo) {
try {
const auto client = createHttpClient("https://github.com");
const uri repoUri(repo);
const auto client = createHttpClient(repo);
const auto httpReq = HttpRequest::newHttpRequest();
httpReq->setMethod(Get);
httpReq->setPath("/" + repo);
httpReq->setPath("/" + repoUri.get_path());
const auto response = co_await client->sendRequestCoro(httpReq);
const auto status = response->getStatusCode();
co_return status == k200OK;
Expand Down Expand Up @@ -128,6 +130,14 @@ namespace api::v1 {
const auto repo = json["repo"].asString();
const auto path = json["path"].asString();

try {
const uri repoUri(repo);
} catch (const std::exception &e) {
logger.error("Invalid repository URL provided: {} Error: {}", repo, e.what());
simpleError(Error::ErrBadRequest, "no_repository", callback);
co_return std::nullopt;
}

Project tempProject;
tempProject.setId("_temp-" + crypto::generateSecureRandomString(8));
tempProject.setSourceRepo(repo);
Expand Down
25 changes: 15 additions & 10 deletions src/api/v1/projects.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,23 @@ namespace api::v1 {
public:
explicit ProjectsController(Auth &, Platforms &, Database &, Storage &, CloudFlare &);

// clang-format off
METHOD_LIST_BEGIN
ADD_METHOD_TO(ProjectsController::greet, "/", drogon::Get);
ADD_METHOD_TO(ProjectsController::listIDs, "/api/v1/projects", drogon::Get, "AuthFilter");
ADD_METHOD_TO(ProjectsController::listUserProjects, "/api/v1/projects/dev", drogon::Get, "AuthFilter");
ADD_METHOD_TO(ProjectsController::getProjectLog, "/api/v1/project/{1:id}/log", drogon::Get, "AuthFilter");
ADD_METHOD_TO(ProjectsController::listPopularProjects, "/api/v1/projects/popular", drogon::Get, "AuthFilter");
ADD_METHOD_TO(ProjectsController::getProject, "/api/v1/project/{1:id}", drogon::Get, "AuthFilter");
ADD_METHOD_TO(ProjectsController::create, "/api/v1/project/create", drogon::Post, "AuthFilter");
ADD_METHOD_TO(ProjectsController::remove, "/api/v1/project/{1:id}/remove", drogon::Post, "AuthFilter");
ADD_METHOD_TO(ProjectsController::update, "/api/v1/project/update", drogon::Post, "AuthFilter");
ADD_METHOD_TO(ProjectsController::invalidate, "/api/v1/project/{1:id}/invalidate", drogon::Post, "AuthFilter");
// Public
ADD_METHOD_TO(ProjectsController::greet, "/", drogon::Get);
// Internal
ADD_METHOD_TO(ProjectsController::listIDs, "/api/v1/projects", drogon::Get, "AuthFilter");
ADD_METHOD_TO(ProjectsController::listPopularProjects, "/api/v1/projects/popular", drogon::Get, "AuthFilter");
// Private
ADD_METHOD_TO(ProjectsController::listUserProjects, "/api/v1/dev/projects", drogon::Get, "AuthFilter");
ADD_METHOD_TO(ProjectsController::getProject, "/api/v1/dev/projects/{1:id}", drogon::Get, "AuthFilter");
ADD_METHOD_TO(ProjectsController::getProjectLog, "/api/v1/dev/projects/{1:id}/log", drogon::Get, "AuthFilter");
ADD_METHOD_TO(ProjectsController::create, "/api/v1/dev/projects", drogon::Post, "AuthFilter");
ADD_METHOD_TO(ProjectsController::update, "/api/v1/dev/projects", drogon::Put, "AuthFilter");
ADD_METHOD_TO(ProjectsController::remove, "/api/v1/dev/projects/{1:id}", drogon::Delete, "AuthFilter");
ADD_METHOD_TO(ProjectsController::invalidate, "/api/v1/dev/projects/{1:id}/invalidate", drogon::Post, "AuthFilter");
METHOD_LIST_END
// clang-format on

drogon::Task<> greet(drogon::HttpRequestPtr req, std::function<void(const drogon::HttpResponsePtr &)> callback) const;

Expand Down
24 changes: 23 additions & 1 deletion src/service/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,27 @@ target_link_libraries(service PRIVATE
)

target_include_directories(service PUBLIC
${PROJECT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}
)

file(GLOB SCHEMA_FILES "${CMAKE_CURRENT_SOURCE_DIR}/*.json")
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/schemas.cc
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/schemas.cc ${CMAKE_CURRENT_BINARY_DIR}/schemas.cc
DEPENDS ${SCHEMA_FILES}
)
add_custom_target(update_schemas DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/schemas.cc)
add_dependencies(service update_schemas)

file(READ "${CMAKE_CURRENT_SOURCE_DIR}/schemas/sinytra-wiki.schema.json" PROJECT_META_SCHEMA)
string(CONFIGURE "${PROJECT_META_SCHEMA}" PROJECT_META_SCHEMA ESCAPE_QUOTES)
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/schemas/_meta.schema.json" FOLDER_META_SCHEMA)
string(CONFIGURE "${FOLDER_META_SCHEMA}" FOLDER_META_SCHEMA ESCAPE_QUOTES)
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/schemas/config.schema.json" SYSTEM_CONFIG_SCHEMA)
string(CONFIGURE "${SYSTEM_CONFIG_SCHEMA}" SYSTEM_CONFIG_SCHEMA ESCAPE_QUOTES)

configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/schemas.cc
${CMAKE_CURRENT_BINARY_DIR}/schemas.cc
@ONLY
)
4 changes: 2 additions & 2 deletions src/service/platforms.cc
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ namespace service {
throw std::runtime_error("Cannot stringify unknown project type");
}

Task<bool> DistributionPlatform::verifyProjectAccess(const PlatformProject project, const User user, const std::string repo) {
if (const auto expected = "https://github.com/" + repo; !project.sourceUrl.empty() && project.sourceUrl.starts_with(expected)) {
Task<bool> DistributionPlatform::verifyProjectAccess(const PlatformProject project, const User user, const std::string repoUrl) {
if (!project.sourceUrl.empty() && project.sourceUrl.starts_with(repoUrl)) {
co_return true;
}
co_return false;
Expand Down
2 changes: 1 addition & 1 deletion src/service/platforms.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ namespace service {
public:
virtual drogon::Task<std::optional<PlatformProject>> getProject(std::string slug) = 0;

virtual drogon::Task<bool> verifyProjectAccess(PlatformProject project, User user, std::string repo);
virtual drogon::Task<bool> verifyProjectAccess(PlatformProject project, User user, std::string repoUrl);
};

class ModrinthPlatform : public DistributionPlatform {
Expand Down
49 changes: 42 additions & 7 deletions src/service/resolved.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@

#include <filesystem>
#include <fstream>
#include <unordered_map>

#include <fmt/args.h>
#include <git2.h>
#include <include/uri.h>

#define DOCS_META_FILE "sinytra-wiki.json"
#define FOLDER_META_FILE "_meta.json"
Expand All @@ -17,6 +20,10 @@ using namespace logging;
using namespace drogon;
namespace fs = std::filesystem;

std::unordered_map<std::string, std::string> GIT_PROVIDERS = {
{ "github.com", "blob/{branch}/{base}/{path}" }
};

std::string formatISOTime(git_time_t time, const int offset) {
// time += offset * 60;

Expand Down Expand Up @@ -174,9 +181,9 @@ std::string getDocsTreeEntryName(std::string s) {
std::string getDocsTreeEntryPath(const std::string &s) { return s.ends_with(DOCS_FILE_EXT) ? s.substr(0, s.size() - 4) : s; }

fs::path getFolderMetaFilePath(const fs::path &rootDir, const fs::path &dir, const std::string &locale) {
const auto relativePath = relative(dir, rootDir);

if (!locale.empty()) {
const auto relativePath = relative(dir, rootDir);

if (const auto localeFile = rootDir / I18N_DIR_PATH / locale / relativePath / FOLDER_META_FILE; exists(localeFile)) {
return localeFile;
}
Expand Down Expand Up @@ -287,6 +294,8 @@ namespace service {
return "requires_auth";
case ProjectError::NO_REPOSITORY:
return "no_repository";
case ProjectError::REPO_TOO_LARGE:
return "repo_too_large";
case ProjectError::NO_BRANCH:
return "no_branch";
case ProjectError::NO_PATH:
Expand All @@ -298,6 +307,24 @@ namespace service {
}
}

std::string formatEditUrl(const Project &project, const std::string &filePath) {
const uri parsed{project.getValueOfSourceRepo()};
const auto domain = parsed.get_host();

const auto provider = GIT_PROVIDERS.find(domain);
if (provider == GIT_PROVIDERS.end()) {
return "";
}

fmt::dynamic_format_arg_store<fmt::format_context> store;
store.push_back(fmt::arg("branch", project.getValueOfSourceBranch()));
store.push_back(fmt::arg("base", removeLeadingSlash(project.getValueOfSourcePath())));
store.push_back(fmt::arg("path", removeTrailingSlash(filePath)));
const std::string result = fmt::vformat(provider->second, store);

return removeTrailingSlash(project.getValueOfSourceRepo()) + "/" + result;
}

ResolvedProject::ResolvedProject(const Project &p, const std::filesystem::path &r, const std::filesystem::path &d) :
project_(p), defaultVersion_(nullptr), rootDir_(r), docsDir_(d) {}

Expand Down Expand Up @@ -361,8 +388,19 @@ namespace service {
return versions;
}

fs::path getFilePath(const fs::path &rootDir, const std::string &path, const std::string &locale) {
if (!locale.empty()) {
const auto relativePath = relative(path, rootDir);

if (const auto localeFile = rootDir / I18N_DIR_PATH / locale / removeLeadingSlash(path); exists(localeFile)) {
return localeFile;
}
}
return rootDir / removeLeadingSlash(path);
}

std::tuple<ProjectPage, Error> ResolvedProject::readFile(std::string path) const {
const auto filePath = docsDir_ / removeLeadingSlash(path);
const auto filePath = getFilePath(docsDir_, removeLeadingSlash(path), locale_);

std::ifstream file(filePath);

Expand All @@ -376,10 +414,7 @@ namespace service {
file.close();

const auto updatedAt = getLastCommitDate(rootDir_, removeLeadingSlash(project_.getValueOfSourcePath()) + '/' + path).value_or("");

const auto editUrl =
std::format("https://github.com/{}/blob/{}/{}/{}", project_.getValueOfSourceRepo(), project_.getValueOfSourceBranch(),
removeLeadingSlash(project_.getValueOfSourcePath()), removeLeadingSlash(path));
const auto editUrl = formatEditUrl(project_, path);

return {{buffer.str(), editUrl, updatedAt}, Error::Ok};
}
Expand Down
1 change: 1 addition & 0 deletions src/service/resolved.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ namespace service {
OK,
REQUIRES_AUTH,
NO_REPOSITORY,
REPO_TOO_LARGE,
NO_BRANCH,
NO_PATH,
INVALID_META,
Expand Down
Loading

0 comments on commit 9009821

Please sign in to comment.