omega/src/server/api.rs
2026-07-27 20:36:23 +02:00

245 lines
9.1 KiB
Rust

use crate::api::response::{
ConnectionsResponse, IotaResponse, OmikronResponse, PublicKeyResponse, StatusResponse,
UserResponse, UsernameResponse, json,
};
use crate::db::{
iota_repo::get_iota_by_id,
omikron_repo::get_omikron_by_id,
user_repo::{get_by_user_id, get_by_username},
};
use crate::error::{OmegaError, Result};
use crate::load_keyring;
use crate::models::UserId;
use crate::server::{
middleware,
validation::{parse_positive_id, validate_non_empty},
};
use crate::sql::user_online_tracker::{get_all_connections, get_iota_primary_omikron_connection};
use crate::transport::omikron_manager::{get_connected_omikron, get_random_omikron};
use crate::util::file_util::get_directory;
use base64::Engine as _;
use bytes::Bytes;
use http::{Method, StatusCode};
use mtp::webserver::{HttpRequest, HttpResponse, RouteParams};
use std::collections::BTreeMap;
fn error_body(error: &OmegaError) -> String {
json(&StatusResponse {
status: match error {
OmegaError::Validation(_) => "error_bad_request",
OmegaError::NotFound => "error_not_found",
_ => "error",
},
})
}
fn user_response(user: crate::models::User) -> UserResponse {
UserResponse {
status: "success",
username: user.username,
public_key: user.public_key.to_base64(),
user_id: user.id.0,
iota_id: user.iota_id.0,
sub_level: user.sub_level,
sub_end: user.sub_end,
display: user.display,
status_message: user.status,
about: user.about,
avatar: user
.avatar
.map(|value| base64::engine::general_purpose::STANDARD.encode(value)),
}
}
async fn route(path_parts: &[&str]) -> Result<(StatusCode, String)> {
match path_parts {
["api", "get", "omikron"] => {
let connection = get_random_omikron()
.await
.map_err(|_| OmegaError::NotFound)?;
let id = connection
.get_omikron_id()
.await
.ok_or(OmegaError::NotFound)?;
let omikron = get_omikron_by_id(id.into()).await?;
Ok((
StatusCode::OK,
json(&OmikronResponse {
status: "success",
id,
public_key: omikron.public_key.to_base64(),
ip_address: omikron.ip_address,
port: omikron.port,
}),
))
}
["api", "get", "omikron", id] => {
let id = parse_positive_id(id)?;
let omikron_id = if get_connected_omikron(id).is_some() {
id
} else if let Some(omikron_id) = get_iota_primary_omikron_connection(id) {
omikron_id
} else {
let user = get_by_user_id(UserId::from(id)).await?;
get_iota_primary_omikron_connection(user.iota_id.0).ok_or(OmegaError::NotFound)?
};
// Database rows describe registered Omikrons. The public discovery
// API must expose only routes backed by a currently live transport.
get_connected_omikron(omikron_id).ok_or(OmegaError::NotFound)?;
let omikron = get_omikron_by_id(omikron_id.into()).await?;
Ok((
StatusCode::OK,
json(&OmikronResponse {
status: "success",
id: omikron.id.0,
public_key: omikron.public_key.to_base64(),
ip_address: omikron.ip_address,
port: omikron.port,
}),
))
}
["api", "get", "connections"] => {
let connections = get_all_connections()
.await
.map_err(|_| OmegaError::Transport("failed to load connections".to_string()))?;
let connections = connections
.into_iter()
.map(|(omikron_id, iotas)| {
let iotas = iotas
.into_iter()
.map(|(iota_id, users)| {
(
iota_id.to_string(),
users.into_iter().map(i64::from).collect(),
)
})
.collect();
(omikron_id.to_string(), iotas)
})
.collect::<BTreeMap<_, _>>();
Ok((
StatusCode::OK,
json(&ConnectionsResponse {
status: "success",
connections,
}),
))
}
["api", "get", "iota", id] => {
let id = parse_positive_id(id)?;
let iota = get_iota_by_id(id.into()).await?;
Ok((
StatusCode::OK,
json(&IotaResponse {
status: "success",
iota_id: iota.id.0,
public_key: iota.public_key.to_base64(),
}),
))
}
["api", "get", "id", username] => {
validate_non_empty(username, "username", 15)?;
let user = get_by_username(username).await?;
Ok((
StatusCode::OK,
json(&UsernameResponse {
status: "success",
username: user.username,
public_key: user.public_key.to_base64(),
user_id: user.id.0,
iota_id: user.iota_id.0,
sub_level: user.sub_level,
sub_end: user.sub_end,
}),
))
}
["api", "get", "public_key"] => {
let public_key = base64::engine::general_purpose::STANDARD
.encode(load_keyring().public_key_bundle().as_bytes());
Ok((
StatusCode::OK,
json(&PublicKeyResponse {
status: "success",
public_key,
}),
))
}
["api", "get", "user", id] => {
let id = parse_positive_id(id)?;
let user = get_by_user_id(UserId::from(id)).await?;
Ok((StatusCode::OK, json(&user_response(user))))
}
_ => Ok((
StatusCode::INTERNAL_SERVER_ERROR,
json(&StatusResponse { status: "error" }),
)),
}
}
pub async fn handle(request: HttpRequest, response: HttpResponse) -> HttpResponse {
let method = request.method;
let path = request.uri.path().to_string();
if method != Method::OPTIONS && !middleware::allow(request.remote_addr.ip(), &path) {
return response
.status(StatusCode::TOO_MANY_REQUESTS)
.header("access-control-allow-origin", &crate::config::cors_origin())
.body(json(&StatusResponse {
status: "error_rate_limited",
}));
}
if method == Method::OPTIONS {
return response
.status(StatusCode::OK)
.header("access-control-allow-origin", &crate::config::cors_origin())
.header("access-control-allow-methods", "GET, POST, OPTIONS")
.header("access-control-allow-headers", "*");
}
let path_parts: Vec<&str> = path.split('/').filter(|part| !part.is_empty()).collect();
if let ["api", "download", "iota_frontend"] = path_parts.as_slice() {
let file_path = format!("{}/downloads/iota_frontend.zip", get_directory());
return match std::fs::read(file_path) {
Ok(bytes) => response
.status(StatusCode::OK)
.header("access-control-allow-origin", &crate::config::cors_origin())
.header("content-type", "application/zip")
.header(
"content-disposition",
"attachment; filename=\"iota_frontend.zip\"",
)
.body(Bytes::from(bytes)),
Err(_) => response
.status(StatusCode::NOT_FOUND)
.header("access-control-allow-origin", &crate::config::cors_origin())
.body(json(&StatusResponse {
status: "error_not_found",
})),
};
}
if let ["direct", short @ ..] = path_parts.as_slice() {
let short = short.join("");
let location = crate::server::short_link::get_short_link(&short)
.await
.unwrap_or_else(|_| "https://tensamin.net".to_string());
return response
.status(StatusCode::TEMPORARY_REDIRECT)
.header("location", &location);
}
let (status, body) = route(&path_parts)
.await
.unwrap_or_else(|error| (error.status_code(), error_body(&error)));
response
.status(status)
.header("access-control-allow-origin", &crate::config::cors_origin())
.header("access-control-allow-headers", "*")
.header("access-control-allow-methods", "GET, POST, OPTIONS")
.body(body)
}
pub async fn handle_pattern(
request: HttpRequest,
response: HttpResponse,
_params: RouteParams,
) -> HttpResponse {
handle(request, response).await
}