Files
api/src/api.rs
2024-11-26 19:30:56 +00:00

247 lines
6.1 KiB
Rust

use crate::nip98::Nip98Auth;
use crate::provisioner::Provisioner;
use crate::status::{VmState, VmStateCache};
use lnvps_db::hydrate::Hydrate;
use lnvps_db::{LNVpsDb, UserSshKey, Vm, VmOsImage, VmPayment, VmTemplate};
use nostr::util::hex;
use rocket::serde::json::Json;
use rocket::{get, post, routes, Responder, Route, State};
use serde::{Deserialize, Serialize};
use ssh_key::PublicKey;
pub fn routes() -> Vec<Route> {
routes![
v1_list_vms,
v1_get_vm,
v1_list_vm_templates,
v1_list_vm_images,
v1_list_ssh_keys,
v1_add_ssh_key,
v1_create_vm_order,
v1_renew_vm,
v1_get_payment
]
}
type ApiResult<T> = Result<Json<ApiData<T>>, ApiError>;
#[derive(Serialize)]
struct ApiData<T: Serialize> {
pub data: T,
}
impl<T: Serialize> ApiData<T> {
pub fn ok(data: T) -> ApiResult<T> {
Ok(Json::from(ApiData { data }))
}
pub fn err(msg: &str) -> ApiResult<T> {
Err(msg.into())
}
}
#[derive(Responder)]
#[response(status = 500)]
struct ApiError {
pub error: String,
}
impl ApiError {
pub fn new(error: &str) -> Self {
Self {
error: error.to_owned(),
}
}
}
impl<T: ToString> From<T> for ApiError {
fn from(value: T) -> Self {
Self {
error: value.to_string(),
}
}
}
#[derive(Serialize)]
struct ApiVmStatus {
#[serde(flatten)]
pub vm: Vm,
pub status: VmState,
}
#[get("/api/v1/vm")]
async fn v1_list_vms(
auth: Nip98Auth,
db: &State<Box<dyn LNVpsDb>>,
vm_state: &State<VmStateCache>,
) -> ApiResult<Vec<ApiVmStatus>> {
let pubkey = auth.event.pubkey.to_bytes();
let uid = db.upsert_user(&pubkey).await?;
let mut vms = db.list_user_vms(uid).await?;
let mut ret = vec![];
for mut vm in vms {
vm.hydrate_up(db).await?;
vm.hydrate_down(db).await?;
if let Some(t) = &mut vm.template {
t.hydrate_up(db).await?;
}
let state = vm_state.get_state(vm.id).await;
ret.push(ApiVmStatus { vm, status: state });
}
ApiData::ok(ret)
}
#[get("/api/v1/vm/<id>")]
async fn v1_get_vm(
auth: Nip98Auth,
db: &State<Box<dyn LNVpsDb>>,
vm_state: &State<VmStateCache>,
id: u64,
) -> ApiResult<ApiVmStatus> {
let pubkey = auth.event.pubkey.to_bytes();
let uid = db.upsert_user(&pubkey).await?;
let mut vm = db.get_vm(id).await?;
if vm.user_id != uid {
return ApiData::err("VM doesnt belong to you");
}
vm.hydrate_up(db).await?;
vm.hydrate_down(db).await?;
if let Some(t) = &mut vm.template {
t.hydrate_up(db).await?;
}
let state = vm_state.get_state(vm.id).await;
ApiData::ok(ApiVmStatus { vm, status: state })
}
#[get("/api/v1/image")]
async fn v1_list_vm_images(db: &State<Box<dyn LNVpsDb>>) -> ApiResult<Vec<VmOsImage>> {
let vms = db.list_os_image().await?;
ApiData::ok(vms)
}
#[get("/api/v1/vm/templates")]
async fn v1_list_vm_templates(db: &State<Box<dyn LNVpsDb>>) -> ApiResult<Vec<VmTemplate>> {
let mut vms = db.list_vm_templates().await?;
for vm in &mut vms {
vm.hydrate_up(db).await?;
}
ApiData::ok(vms.iter().filter(|v| v.enabled).collect())
}
#[get("/api/v1/ssh-key")]
async fn v1_list_ssh_keys(
auth: Nip98Auth,
db: &State<Box<dyn LNVpsDb>>,
) -> ApiResult<Vec<UserSshKey>> {
let uid = db.upsert_user(&auth.event.pubkey.to_bytes()).await?;
let keys = db.list_user_ssh_key(uid).await?;
ApiData::ok(keys)
}
#[post("/api/v1/ssh-key", data = "<req>", format = "json")]
async fn v1_add_ssh_key(
auth: Nip98Auth,
db: &State<Box<dyn LNVpsDb>>,
req: Json<CreateSshKey>,
) -> ApiResult<UserSshKey> {
let uid = db.upsert_user(&auth.event.pubkey.to_bytes()).await?;
let pk: PublicKey = req.key_data.parse()?;
let key_name = if !req.name.is_empty() {
&req.name
} else {
pk.comment()
};
let mut new_key = UserSshKey {
name: key_name.to_string(),
user_id: uid,
key_data: pk.to_openssh()?,
..Default::default()
};
let key_id = db.insert_user_ssh_key(&new_key).await?;
new_key.id = key_id;
ApiData::ok(new_key)
}
#[post("/api/v1/vm", data = "<req>", format = "json")]
async fn v1_create_vm_order(
auth: Nip98Auth,
db: &State<Box<dyn LNVpsDb>>,
provisioner: &State<Box<dyn Provisioner>>,
req: Json<CreateVmRequest>,
) -> ApiResult<Vm> {
let pubkey = auth.event.pubkey.to_bytes();
let uid = db.upsert_user(&pubkey).await?;
let req = req.0;
let mut rsp = provisioner
.provision(uid, req.template_id, req.image_id, req.ssh_key_id)
.await?;
rsp.hydrate_up(db).await?;
ApiData::ok(rsp)
}
#[get("/api/v1/vm/<id>/renew")]
async fn v1_renew_vm(
auth: Nip98Auth,
db: &State<Box<dyn LNVpsDb>>,
provisioner: &State<Box<dyn Provisioner>>,
id: u64,
) -> ApiResult<VmPayment> {
let pubkey = auth.event.pubkey.to_bytes();
let uid = db.upsert_user(&pubkey).await?;
let vm = db.get_vm(id).await?;
if uid != vm.user_id {
return ApiData::err("VM does not belong to you");
}
let rsp = provisioner.renew(id).await?;
ApiData::ok(rsp)
}
#[get("/api/v1/payment/<id>")]
async fn v1_get_payment(
auth: Nip98Auth,
db: &State<Box<dyn LNVpsDb>>,
id: &str,
) -> ApiResult<VmPayment> {
let pubkey = auth.event.pubkey.to_bytes();
let uid = db.upsert_user(&pubkey).await?;
let id = if let Ok(i) = hex::decode(id) {
i
} else {
return ApiData::err("Invalid payment id");
};
let payment = db.get_vm_payment(&id).await?;
let vm = db.get_vm(payment.vm_id).await?;
if vm.user_id != uid {
return ApiData::err("VM does not belong to you");
}
ApiData::ok(payment)
}
#[derive(Deserialize)]
struct CreateVmRequest {
template_id: u64,
image_id: u64,
ssh_key_id: u64,
}
impl From<CreateVmRequest> for VmTemplate {
fn from(val: CreateVmRequest) -> Self {
VmTemplate {
id: val.template_id,
..Default::default()
}
}
}
#[derive(Deserialize)]
struct CreateSshKey {
name: String,
key_data: String,
}