use axum::{
extract::{Path, State},
http::StatusCode,
middleware,
response::{IntoResponse, Redirect},
routing::{delete, get, post},
Form, Json, Router,
};
use serde::Deserialize;
use std::sync::Arc;
use crate::store::KeyEntry;
use crate::tunnel::TunnelManager;
use crate::web;
use gout_api::{
api_ok, ApiResponse, CreateKeyRequest, CreateKeyResponse, KeyInfo,
};
mod auth;
pub(crate) mod tunnels;
use tunnels::AppState;
pub fn build_router(
tunnel_mgr: Arc<TunnelManager>,
store: Arc<crate::store::KeyStore>,
) -> Router {
let state = AppState {
tunnel_mgr,
store: store.clone(),
};
let key_routes = Router::new()
.route("/", post(api_create_key).get(api_list_keys))
.route("/:key", delete(api_delete_key))
.layer(middleware::from_fn(auth::require_admin_key))
.layer(axum::Extension(store.clone()));
let tunnel_routes = Router::new()
.route("/", get(tunnels::list_tunnels).post(tunnels::create_tunnel))
.route("/:token", delete(tunnels::delete_tunnel))
.layer(middleware::from_fn(auth::require_tunnel_key))
.layer(axum::Extension(store.clone()));
let web_pub = Router::new()
.route("/login", get(web::login_page).post(web::login_post))
.route("/logout", get(web::logout))
.route("/", get(|| async { Redirect::to("/dashboard") }));
let web_auth = Router::new()
.route("/dashboard", get(web::dashboard))
.route("/keys", get(web::keys_page).post(web_key_create))
.route("/keys/delete/:key", post(web_key_delete))
.layer(middleware::from_fn(web::auth::require_web_auth))
.layer(axum::Extension(store.clone()));
let api_routes = Router::new()
.nest("/api/v1/keys", key_routes)
.nest("/api/v1/tunnels", tunnel_routes);
Router::new()
.merge(web_pub)
.merge(web_auth)
.merge(api_routes)
.with_state(state)
}
#[derive(Deserialize)]
struct WebKeyCreateForm {
name: String,
}
async fn web_key_create(
State(state): State<AppState>,
Form(form): Form<WebKeyCreateForm>,
) -> impl IntoResponse {
let api_key = gout_api::generate_api_key();
let now = chrono::Utc::now();
let entry = KeyEntry {
key: api_key.clone(),
name: form.name,
created_at: now.to_rfc3339(),
admin: false,
};
match state.store.add(entry).await {
Ok(()) => Redirect::to("/keys").into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
}
}
#[derive(Deserialize)]
struct WebKeyDeleteForm {
_method: Option<String>,
}
async fn web_key_delete(
State(state): State<AppState>,
Path(key): Path<String>,
Form(_form): Form<WebKeyDeleteForm>,
) -> impl IntoResponse {
match state.store.delete(&key).await {
Ok(_) => Redirect::to("/keys").into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
}
}
async fn api_create_key(
State(state): State<AppState>,
Json(req): Json<CreateKeyRequest>,
) -> impl IntoResponse {
let api_key = gout_api::generate_api_key();
let now = chrono::Utc::now();
let entry = KeyEntry {
key: api_key.clone(),
name: req.name.clone(),
created_at: now.to_rfc3339(),
admin: false,
};
match state.store.add(entry).await {
Ok(()) => (
StatusCode::CREATED,
Json(ApiResponse::ok(CreateKeyResponse {
key: api_key,
name: req.name,
})),
)
.into_response(),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiResponse::<CreateKeyResponse>::err(e.to_string())),
)
.into_response(),
}
}
async fn api_list_keys(
State(state): State<AppState>,
) -> impl IntoResponse {
match state.store.load().await {
Ok(keys) => {
let list: Vec<KeyInfo> = keys
.into_iter()
.filter(|k| !k.admin)
.map(|k| KeyInfo {
key: k.key.clone(),
name: k.name,
})
.collect();
(StatusCode::OK, Json(ApiResponse::ok(list))).into_response()
}
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiResponse::<Vec<KeyInfo>>::err(e.to_string())),
)
.into_response(),
}
}
async fn api_delete_key(
State(state): State<AppState>,
Path(key): Path<String>,
) -> impl IntoResponse {
if key.starts_with("sk-") {
if let Ok(true) = state.store.validate_admin(&key).await {
return (
StatusCode::FORBIDDEN,
Json(ApiResponse::<()>::err("cannot delete admin key")),
)
.into_response();
}
}
match state.store.delete(&key).await {
Ok(true) => (StatusCode::OK, Json(api_ok())).into_response(),
Ok(false) => (
StatusCode::NOT_FOUND,
Json(ApiResponse::<()>::err("key not found")),
)
.into_response(),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiResponse::<()>::err(e.to_string())),
)
.into_response(),
}
}