mod account_admin;
mod account_cloud;
mod auth;
mod billing_edge;
mod buddy;
mod cep;
mod commands;
mod config;
mod contribute;
mod db;
mod devices;
mod digest;
mod feedback;
mod gain;
mod global_stats;
mod gotchas;
mod helpers;
mod index_sync;
mod knowledge;
mod models;
mod oauth;
mod site_theme;
mod sso;
mod stats;
mod team_join;
mod wrapped;
use axum::Router;
use axum::routing::{delete, get, patch, post, put};
use tower_http::cors::{AllowOrigin, CorsLayer};
pub async fn run() -> anyhow::Result<()> {
let cfg = config::Config::from_env()?;
let pool = db::pool_from_database_url(&cfg.database_url)?;
db::init_schema(&pool).await?;
let mailer = if cfg.smtp_enabled() {
Some(auth::Mailer::new(&cfg)?)
} else {
None
};
let state = auth::AppState::new(pool, cfg.clone(), mailer);
digest::spawn_digest_job(state.clone());
let cors = CorsLayer::new()
.allow_origin(AllowOrigin::list([
"https://leanctx.com"
.parse()
.expect("BUG: invalid hardcoded URL"),
"https://www.leanctx.com"
.parse()
.expect("BUG: invalid hardcoded URL"),
"http://localhost:4321"
.parse()
.expect("BUG: invalid hardcoded URL"),
]))
.allow_methods([
axum::http::Method::GET,
axum::http::Method::POST,
axum::http::Method::PUT,
axum::http::Method::PATCH,
axum::http::Method::DELETE,
axum::http::Method::OPTIONS,
])
.allow_headers([
axum::http::header::CONTENT_TYPE,
axum::http::header::AUTHORIZATION,
axum::http::header::ACCEPT,
])
.allow_credentials(true);
let app = Router::new()
.route("/health", get(auth::health))
.route("/oauth/register", post(oauth::register_client))
.route("/oauth/token", post(oauth::token))
.route("/api/auth/register", post(auth::register))
.route("/api/auth/login", post(auth::login))
.route("/api/auth/sso/start", post(sso::sso_start))
.route("/api/auth/sso/callback", get(sso::sso_callback))
.route("/api/auth/sso/handoff", post(sso::sso_handoff))
.route("/api/auth/forgot-password", post(auth::forgot_password))
.route("/api/auth/reset-password", post(auth::reset_password))
.route("/api/auth/verify-email", get(auth::verify_email))
.route(
"/api/auth/resend-verification",
post(auth::resend_verification),
)
.route("/api/auth/me", get(auth::me))
.route("/api/stats", get(stats::get_stats).post(stats::post_stats))
.route("/api/contribute", post(contribute::post_contribute))
.route(
"/api/sync/knowledge",
get(knowledge::get_knowledge).post(knowledge::post_knowledge),
)
.route(
"/api/sync/commands",
get(commands::get_commands).post(commands::post_commands),
)
.route("/api/sync/cep", get(cep::get_cep).post(cep::post_cep))
.route(
"/api/sync/gotchas",
get(gotchas::get_gotchas).post(gotchas::post_gotchas),
)
.route(
"/api/sync/buddy",
get(buddy::get_buddy).post(buddy::post_buddy),
)
.route(
"/api/sync/feedback",
get(feedback::get_feedback).post(feedback::post_feedback),
)
.route("/api/sync/gain", get(gain::get_gain).post(gain::post_gain))
.route("/api/sync/index", get(index_sync::list_bundles))
.route(
"/api/sync/index/{project_hash}",
put(index_sync::put_bundle)
.get(index_sync::get_bundle)
.delete(index_sync::delete_bundle)
.layer(axum::extract::DefaultBodyLimit::max(
index_sync::MAX_BUNDLE_BYTES,
)),
)
.route(
"/api/wrapped",
post(wrapped::publish).layer(axum::extract::DefaultBodyLimit::max(64 * 1024)),
)
.route(
"/api/wrapped/{id}",
get(wrapped::get_card).delete(wrapped::delete_card),
)
.route("/api/wrapped/{id}/card.svg", get(wrapped::get_card_svg))
.route("/api/wrapped/{id}/card.png", get(wrapped::get_card_png))
.route("/api/wrapped/{id}/claim", post(wrapped::claim_card))
.route("/api/wrapped/{id}/link/start", post(wrapped::link_start))
.route(
"/api/wrapped/{id}/link/complete",
post(wrapped::link_complete),
)
.route("/w/{id}", get(wrapped::get_permalink_page))
.route("/api/leaderboard", get(wrapped::leaderboard))
.route("/leaderboard", get(wrapped::get_leaderboard_page))
.route("/api/global-stats", get(global_stats::get_global_stats))
.route("/api/cloud/models", get(models::get_models))
.route("/api/supporters", get(billing_edge::get_supporters))
.route(
"/api/supporters/checkout",
post(billing_edge::post_supporter_checkout),
)
.route(
"/api/account/entitlements",
get(billing_edge::get_account_entitlements),
)
.route(
"/api/account/checkout",
post(billing_edge::post_account_checkout),
)
.route(
"/api/account/portal",
post(billing_edge::post_account_portal),
)
.route("/api/account/cloud", get(account_cloud::get_account_cloud))
.route("/api/account/export", get(account_admin::export_account))
.route("/api/account", delete(account_admin::delete_account))
.route("/api/account/devices", get(devices::list_devices))
.route(
"/api/account/devices/{label}",
delete(devices::forget_device),
)
.route("/api/digest/opt-out", get(digest::opt_out))
.route(
"/api/account/digest",
get(digest::get_digest_pref).put(digest::put_digest_pref),
)
.route("/api/account/team", get(billing_edge::get_account_team))
.route(
"/api/account/team/savings",
get(billing_edge::get_account_team_savings),
)
.route(
"/api/account/team/savings/member/{signer}",
get(billing_edge::get_account_team_savings_member),
)
.route(
"/api/account/team/settings",
axum::routing::put(billing_edge::put_account_team_settings),
)
.route(
"/api/account/team/owner-token",
post(billing_edge::post_account_team_owner_token),
)
.route(
"/api/account/team/members",
post(billing_edge::post_account_team_member),
)
.route(
"/api/account/team/members/{token_id}",
delete(billing_edge::delete_account_team_member),
)
.route(
"/api/account/team/invites",
get(billing_edge::get_account_team_invites)
.post(billing_edge::post_account_team_invite),
)
.route(
"/api/account/team/invites/{invite_id}",
delete(billing_edge::delete_account_team_invite),
)
.route("/api/team/join", post(team_join::post_team_join))
.route(
"/api/account/org/sso",
get(billing_edge::get_account_org_sso)
.put(billing_edge::put_account_org_sso)
.delete(billing_edge::delete_account_org_sso),
)
.route(
"/api/account/org/sso/verify",
post(billing_edge::post_account_org_sso_verify),
)
.route(
"/api/account/org/sso/required",
put(billing_edge::put_account_org_sso_required),
)
.route(
"/api/account/org/audit",
get(billing_edge::get_account_org_audit),
)
.route(
"/api/account/org/audit/export.csv",
get(billing_edge::get_account_org_audit_export),
)
.route(
"/api/account/registry",
get(billing_edge::get_account_registry),
)
.route(
"/api/account/registry/namespace",
put(billing_edge::put_account_registry_namespace),
)
.route(
"/api/account/registry/tokens",
post(billing_edge::post_account_registry_token),
)
.route(
"/api/account/registry/tokens/{token_id}",
delete(billing_edge::delete_account_registry_token),
)
.route(
"/api/account/registry/domains",
post(billing_edge::post_account_registry_domain),
)
.route(
"/api/account/registry/domains/{domain_id}/verify",
post(billing_edge::post_account_registry_domain_verify),
)
.route(
"/api/account/registry/domains/{domain_id}",
delete(billing_edge::delete_account_registry_domain),
)
.route(
"/api/account/registry/price",
put(billing_edge::put_account_registry_price),
)
.route(
"/api/account/registry/buy",
post(billing_edge::post_account_registry_buy),
)
.route(
"/api/account/team/seats",
post(billing_edge::post_account_team_seats),
)
.route(
"/api/account/team/storage",
get(billing_edge::get_account_team_storage),
)
.route(
"/api/account/team/connectors",
get(billing_edge::get_account_team_connectors)
.post(billing_edge::post_account_team_connector),
)
.route(
"/api/account/team/connectors/{connector_id}",
patch(billing_edge::patch_account_team_connector)
.delete(billing_edge::delete_account_team_connector),
)
.with_state(state)
.layer(cors)
.layer(axum::extract::DefaultBodyLimit::max(1024 * 1024));
let listener = tokio::net::TcpListener::bind(cfg.bind_addr()).await?;
axum::serve(listener, app).await?;
Ok(())
}