Skip to content
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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ ammonia = "4"
anyhow = "1.0"
bytes = "1.11"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4.5", features = ["derive"] }
clap = { version = "4.5", features = ["derive", "env"] }
dotenvy = "0.15"
futures = "0.3"
image = "0.25"
Expand Down
37 changes: 37 additions & 0 deletions src/cli/jobs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use crate::{config::AppData, database, jobs};

#[derive(Debug, clap::Subcommand)]
pub enum JobCommand {
/// Cleans up mod_downloads from more than 30 days ago
CleanupDownloads,
/// Cleans up auth and refresh tokens that are expired
CleanupTokens,
/// Emergency logout for a developer
LogoutDeveloper {
/// Username of the developer
username: String,
},
/// Runs migrations
Migrate,
}

pub async fn run(job: JobCommand, data: AppData) -> anyhow::Result<()> {
let mut conn = data.db().acquire().await?;

match job {
JobCommand::Migrate => {
database::migrate(&mut conn).await?;
}
JobCommand::CleanupDownloads => {
jobs::cleanup_downloads::cleanup_downloads(&mut conn).await?;
}
JobCommand::LogoutDeveloper { username } => {
jobs::logout_user::logout_user(&username, &mut conn).await?;
}
JobCommand::CleanupTokens => {
jobs::token_cleanup::token_cleanup(&mut conn).await?;
}
}

Ok(())
}
70 changes: 17 additions & 53 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
@@ -1,68 +1,32 @@
use crate::config::AppData;
use crate::jobs;
use crate::{cli::serve::ServeArgs, config::AppData};
use clap::{Parser, Subcommand};

mod jobs;
mod serve;

#[derive(Debug, Parser)]
#[command(version, about, long_about = None)]
struct Args {
pub struct Args {
#[command(subcommand)]
command: Option<Commands>,
}

#[derive(Subcommand, Debug)]
enum Commands {
pub enum Commands {
/// Run the HTTP server (default)
Serve(serve::ServeArgs),
/// Run an internal job
#[command(subcommand)]
Job(JobCommand),
}

#[derive(Debug, Subcommand)]
enum JobCommand {
/// Cleans up mod_downloads from more than 30 days ago
CleanupDownloads,
/// Cleans up auth and refresh tokens that are expired
CleanupTokens,
/// Emergency logout for a developer
LogoutDeveloper {
/// Username of the developer
username: String,
},
/// Runs migrations
Migrate,
Job(jobs::JobCommand),
}

pub async fn maybe_cli(data: &AppData) -> anyhow::Result<bool> {
let cli = Args::parse();

if let Some(c) = cli.command {
return match c {
Commands::Job(job) => match job {
JobCommand::Migrate => {
let mut conn = data.db().acquire().await?;
jobs::migrate::migrate(&mut conn).await?;

Ok(true)
}
JobCommand::CleanupDownloads => {
let mut conn = data.db().acquire().await?;
jobs::cleanup_downloads::cleanup_downloads(&mut conn).await?;

Ok(true)
}
JobCommand::LogoutDeveloper { username } => {
let mut conn = data.db().acquire().await?;
jobs::logout_user::logout_user(&username, &mut conn).await?;

Ok(true)
}
JobCommand::CleanupTokens => {
let mut conn = data.db().acquire().await?;
jobs::token_cleanup::token_cleanup(&mut conn).await?;

Ok(true)
}
},
};
#[tracing::instrument(skip_all, fields(command = ?args.command))]
pub async fn run(args: Args, data: AppData) -> anyhow::Result<()> {
match args
.command
.unwrap_or_else(|| Commands::Serve(ServeArgs::default()))
{
Commands::Serve(serve_args) => serve::run(serve_args, data).await,
Commands::Job(job) => jobs::run(job, data).await,
}
Ok(false)
}
31 changes: 31 additions & 0 deletions src/cli/serve.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use std::sync::Arc;

use tracing::Instrument;

use crate::{config::AppData, database, http, s3_worker};

#[derive(clap::Args, Debug, Default)]
pub struct ServeArgs {
/// Don't run database migrations on startup
#[arg(long, env = "SKIP_MIGRATIONS", value_parser = clap::builder::FalseyValueParser::new())]
skip_migrations: bool,
}

pub async fn run(args: ServeArgs, data: AppData) -> anyhow::Result<()> {
data.static_storage().init().await?;
data.private_storage().init().await?;

if let Some(cdn) = data.cdn_storage() {
cdn.init().await?;
}

if !args.skip_migrations {
database::migrate(data.db()).await?;
}

let data = Arc::new(data);
tokio::spawn(
s3_worker::run_s3_worker(data.clone()).instrument(tracing::info_span!("s3_worker")),
);
http::serve_http(data).await
}
22 changes: 11 additions & 11 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ use tokio::sync::mpsc::Sender;
use crate::{
auth::github::GithubClient,
dns::ValidateDnsResolver,
endpoints::mods::IndexQueryParams,
s3_worker::S3WorkerTask,
http::{self, response::ApiResponse},
s3_worker::{QueuedS3Task, S3WorkerTask},
storage::{LocalBackend, PrivateDisk, PublicDisk, S3Backend, S3Configuration},
types::{
api::{ApiResponse, PaginatedData},
models::mod_entity::Mod,
api::PaginatedData,
models::mod_entity::{IndexQueryParams, Mod},
},
};

Expand All @@ -39,7 +39,7 @@ pub struct AppData {
http_client: reqwest::Client,
check_dns_http_client: reqwest::Client,

s3_sender: OnceLock<Sender<S3WorkerTask>>,
s3_sender: OnceLock<Sender<QueuedS3Task>>,
}

pub async fn build_config() -> anyhow::Result<AppData> {
Expand Down Expand Up @@ -198,17 +198,17 @@ impl AppData {
&self.check_dns_http_client
}

pub fn init_s3_sender(&self, sender: Sender<S3WorkerTask>) {
pub fn init_s3_sender(&self, sender: Sender<QueuedS3Task>) {
self.s3_sender
.set(sender)
.expect("init_s3_sender must be called only once");
}

pub fn send_s3_task(&self, task: S3WorkerTask) -> bool {
if let Some(sender) = self.s3_sender.get() {
sender.try_send(task).is_ok()
} else {
false
}
let Some(sender) = self.s3_sender.get() else {
return false;
};
let request_id = http::REQUEST_ID.try_with(|id| *id).ok();
sender.try_send(QueuedS3Task { task, request_id }).is_ok()
}
}
16 changes: 16 additions & 0 deletions src/database/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
use std::ops::Deref;

use sqlx::{Acquire, migrate::Migrate};

pub mod repository;

#[derive(thiserror::Error, Debug)]
Expand All @@ -7,3 +11,15 @@ pub enum DatabaseError {
#[error("Unknown database error")]
SqlxError(#[from] sqlx::Error),
}

pub async fn migrate<'a, A>(conn: A) -> anyhow::Result<()>
where
A: Acquire<'a>,
<A::Connection as Deref>::Target: Migrate,
{
sqlx::migrate!()
.run(conn)
.await
.inspect_err(|e| tracing::error!("Failed to run migrations: {e}"))?;
Ok(())
}
35 changes: 17 additions & 18 deletions src/endpoints/auth/github.rs → src/http/endpoints/auth/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ use crate::config::AppData;
use crate::database::repository::{
auth_tokens, developers, github_login_attempts, github_web_logins, refresh_tokens,
};
use crate::endpoints::ApiError;
use crate::endpoints::auth::TokensResponse;
use crate::types::api::ApiResponse;
use crate::http::error::ApiError;
use crate::http::endpoints::auth::TokensResponse;
use crate::http::response::ApiResponse;

#[derive(Deserialize, ToSchema)]
struct PollParams {
Expand All @@ -30,16 +30,23 @@ struct CallbackParams {
state: String,
}

pub fn router_config(cfg: &mut web::ServiceConfig) {
cfg
.service(start_github_web_login)
.service(github_web_callback)
.service(poll_github_login)
.service(github_token_login)
.service(start_github_login);
}

/// Start GitHub device flow authentication
#[utoipa::path(
post,
path = "/v1/login/github",
tag = "auth",
responses(
(status = 200, description = "Device flow started", body = inline(ApiResponse<String>))
)
)]
#[post("v1/login/github")]
#[post("/login/github")]
#[tracing::instrument(skip_all)]
pub async fn start_github_login(
data: web::Data<AppData>,
Expand Down Expand Up @@ -67,14 +74,12 @@ pub async fn start_github_login(

/// Start GitHub web OAuth flow
#[utoipa::path(
post,
path = "/v1/login/github/web",
tag = "auth",
responses(
(status = 200, description = "OAuth URL generated", body = inline(ApiResponse<String>))
)
)]
#[post("v1/login/github/web")]
#[post("/login/github/web")]
#[tracing::instrument(skip_all)]
pub async fn start_github_web_login(data: web::Data<AppData>) -> Result<impl Responder, ApiError> {
let mut pool = data.db().acquire().await?;
Expand All @@ -95,8 +100,6 @@ pub async fn start_github_web_login(data: web::Data<AppData>) -> Result<impl Res

/// Handle GitHub OAuth callback
#[utoipa::path(
post,
path = "/v1/login/github/callback",
tag = "auth",
request_body = CallbackParams,
responses(
Expand All @@ -105,7 +108,7 @@ pub async fn start_github_web_login(data: web::Data<AppData>) -> Result<impl Res
(status = 404, description = "Invalid secret")
)
)]
#[post("v1/login/github/callback")]
#[post("/login/github/callback")]
#[tracing::instrument(skip_all)]
pub async fn github_web_callback(
json: web::Json<CallbackParams>,
Expand Down Expand Up @@ -149,8 +152,6 @@ pub async fn github_web_callback(

/// Poll GitHub device flow for authentication
#[utoipa::path(
post,
path = "/v1/login/github/poll",
tag = "auth",
request_body = PollParams,
responses(
Expand All @@ -159,7 +160,7 @@ pub async fn github_web_callback(
(status = 404, description = "Login attempt not found")
)
)]
#[post("v1/login/github/poll")]
#[post("/login/github/poll")]
#[tracing::instrument(skip_all)]
pub async fn poll_github_login(
json: web::Json<PollParams>,
Expand Down Expand Up @@ -252,16 +253,14 @@ pub async fn poll_github_login(

/// Login using a GitHub personal access token
#[utoipa::path(
post,
path = "/v1/login/github/token",
tag = "auth",
request_body = TokenLoginParams,
responses(
(status = 200, description = "Login successful", body = inline(ApiResponse<String>)),
(status = 400, description = "Invalid access token")
)
)]
#[post("v1/login/github/token")]
#[post("/login/github/token")]
#[tracing::instrument(skip_all)]
pub async fn github_token_login(
json: web::Json<TokenLoginParams>,
Expand Down
16 changes: 10 additions & 6 deletions src/endpoints/auth/mod.rs → src/http/endpoints/auth/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use crate::config::AppData;
use crate::database::repository::{auth_tokens, developers, refresh_tokens};
use crate::endpoints::ApiError;
use crate::extractors::auth::Auth;
use crate::types::api::ApiResponse;
use crate::http::error::ApiError;
use crate::http::extractors::auth::Auth;
use crate::http::response::ApiResponse;
use actix_web::{Responder, post, web};
use serde::{Deserialize, Serialize};
use sqlx::Acquire;
Expand All @@ -22,18 +22,22 @@ struct RefreshBody {
refresh_token: String,
}

pub fn router_config(cfg: &mut web::ServiceConfig) {
cfg
.service(refresh_token)
.configure(github::router_config);
}

/// Refresh an access token using a refresh token
#[utoipa::path(
post,
path = "/v1/login/refresh",
tag = "auth",
request_body = RefreshBody,
responses(
(status = 200, description = "New tokens generated", body = inline(ApiResponse<TokensResponse>)),
(status = 400, description = "Invalid or expired refresh token")
)
)]
#[post("v1/login/refresh")]
#[post("/login/refresh")]
#[tracing::instrument(skip_all)]
pub async fn refresh_token(
json: web::Json<RefreshBody>,
Expand Down
Loading
Loading