use std::collections::BTreeMap;
use std::sync::Arc;
use axum::Router;
use axum::body::Bytes;
use axum::extract::{DefaultBodyLimit, Path, Query, State};
use axum::http::{HeaderMap, StatusCode, header};
use axum::response::{Html, IntoResponse, Response};
use axum::routing::{get, post};
use crate::bridge::Bridge;
use crate::flows;
pub fn router(bridge: Arc<Bridge>) -> Router {
let limit = bridge.cfg.max_body_bytes;
let r = Router::new().route("/healthz", get(healthz));
#[cfg(feature = "forge-github")]
let r = r
.route("/github/{host}/{owner}/register", get(github_register))
.route("/github/{host}/{owner}/registered", get(github_registered))
.route("/github/{host}/{owner}/setup", get(github_setup))
.route("/github/{host}/{owner}/webhook", post(github_webhook));
#[cfg(feature = "forge-forgejo")]
let r = r
.route("/forgejo/{host}/bind", get(forgejo_bind))
.route("/forgejo/{host}/link", get(forgejo_link))
.route("/forgejo/{host}/webhook", post(webhook));
r.layer(DefaultBodyLimit::max(limit)).with_state(bridge)
}
async fn healthz(State(bridge): State<Arc<Bridge>>) -> Response {
if let Some(Err(why)) = bridge.store.mirror().map(|m| m.health()) {
return (StatusCode::SERVICE_UNAVAILABLE, why).into_response();
}
"ok".into_response()
}
fn page(status: StatusCode, text: &str) -> Response {
(
status,
[(header::CACHE_CONTROL, "no-store")],
Html(format!(
"<!doctype html><html><head><meta charset=\"utf-8\"><title>VGI bridge</title></head>\
<body><p>{}</p></body></html>",
flows::html_escape(text)
)),
)
.into_response()
}
#[cfg(feature = "forge-github")]
fn known_app(bridge: &Bridge, host: &str, owner: &str) -> bool {
bridge
.cfg
.github_on(host)
.any(|g| g.app_owner.eq_ignore_ascii_case(owner))
}
fn known_host(bridge: &Bridge, host: &str) -> bool {
bridge.adapters.get(host).is_some()
|| bridge.cfg.github.iter().any(|g| g.host == host)
|| bridge
.cfg
.forgejo
.iter()
.any(|f| f.host().is_ok_and(|h| h == host))
}
async fn webhook(
State(bridge): State<Arc<Bridge>>,
Path(host): Path<String>,
headers: HeaderMap,
body: Bytes,
) -> StatusCode {
if !known_host(&bridge, &host) {
return StatusCode::NOT_FOUND;
}
crate::events::on_webhook(&bridge, &host, None, &headers, &body).await
}
#[cfg(feature = "forge-github")]
async fn github_webhook(
State(bridge): State<Arc<Bridge>>,
Path((host, owner)): Path<(String, String)>,
headers: HeaderMap,
body: Bytes,
) -> StatusCode {
if !known_app(&bridge, &host, &owner) {
return StatusCode::NOT_FOUND;
}
crate::events::on_webhook(&bridge, &host, Some(&owner), &headers, &body).await
}
#[cfg(feature = "forge-github")]
async fn github_register(
State(bridge): State<Arc<Bridge>>,
Path((host, owner)): Path<(String, String)>,
Query(q): Query<BTreeMap<String, String>>,
) -> Response {
if !known_app(&bridge, &host, &owner) {
return StatusCode::NOT_FOUND.into_response();
}
register(bridge, host, owner, q)
}
#[cfg(feature = "forge-github")]
fn register(
bridge: Arc<Bridge>,
host: String,
owner: String,
q: BTreeMap<String, String>,
) -> Response {
let state = q.get("state").map(String::as_str).unwrap_or("");
match flows::manifest_page(&bridge, &host, &owner, state) {
Ok(html) => (
[
(header::CACHE_CONTROL, "no-store"),
(header::REFERRER_POLICY, "no-referrer"),
],
Html(html),
)
.into_response(),
Err(e) => {
tracing::warn!(%host, error = %e, "refused a registration page");
page(
StatusCode::NOT_FOUND,
"This registration link is unknown or expired.",
)
}
}
}
#[cfg(feature = "forge-github")]
async fn github_registered(
State(bridge): State<Arc<Bridge>>,
Path((host, owner)): Path<(String, String)>,
Query(q): Query<BTreeMap<String, String>>,
) -> Response {
if !known_app(&bridge, &host, &owner) {
return StatusCode::NOT_FOUND.into_response();
}
registered(bridge, host, owner, q).await
}
#[cfg(feature = "forge-github")]
async fn registered(
bridge: Arc<Bridge>,
host: String,
owner: String,
q: BTreeMap<String, String>,
) -> Response {
let (Some(code), Some(state)) = (q.get("code"), q.get("state")) else {
return page(
StatusCode::BAD_REQUEST,
"The redirect is missing its code or state.",
);
};
match flows::manifest_callback(&bridge, &host, &owner, code, state).await {
Ok(msg) => page(StatusCode::OK, &msg),
Err(e) => {
tracing::warn!(%host, error = %e, "App registration failed");
page(
StatusCode::BAD_REQUEST,
"The App could not be registered; the bridge's log says why.",
)
}
}
}
async fn bind(
bridge: Arc<Bridge>,
host: String,
owner: Option<String>,
q: BTreeMap<String, String>,
) -> Response {
if !known_host(&bridge, &host) {
return StatusCode::NOT_FOUND.into_response();
}
match flows::bind_callback(&bridge, &host, owner.as_deref(), q).await {
Ok(msg) => page(StatusCode::OK, &msg),
Err(e) => {
tracing::warn!(%host, error = %e, "bind callback refused");
page(
StatusCode::BAD_REQUEST,
"The namespace could not be bound; ask the VTC admin to start again.",
)
}
}
}
#[cfg(feature = "forge-github")]
async fn github_setup(
State(bridge): State<Arc<Bridge>>,
Path((host, owner)): Path<(String, String)>,
Query(q): Query<BTreeMap<String, String>>,
) -> Response {
if !known_app(&bridge, &host, &owner) {
return StatusCode::NOT_FOUND.into_response();
}
bind(bridge, host, Some(owner), q).await
}
#[cfg(feature = "forge-forgejo")]
async fn forgejo_bind(
State(bridge): State<Arc<Bridge>>,
Path(host): Path<String>,
Query(q): Query<BTreeMap<String, String>>,
) -> Response {
bind(bridge, host, None, q).await
}
#[cfg(feature = "forge-forgejo")]
async fn forgejo_link(
State(bridge): State<Arc<Bridge>>,
Path(host): Path<String>,
Query(q): Query<BTreeMap<String, String>>,
) -> Response {
if !known_host(&bridge, &host) {
return StatusCode::NOT_FOUND.into_response();
}
match flows::link_callback(&bridge, &host, q).await {
Ok(msg) => page(StatusCode::OK, &msg),
Err(e) => {
tracing::warn!(%host, error = %e, "link callback refused");
page(
StatusCode::BAD_REQUEST,
"Your account could not be linked; start again from your community app.",
)
}
}
}