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 { 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 = Result>, ApiError>; #[derive(Serialize)] struct ApiData { pub data: T, } impl ApiData { pub fn ok(data: T) -> ApiResult { Ok(Json::from(ApiData { data })) } pub fn err(msg: &str) -> ApiResult { 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 From 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>, vm_state: &State, ) -> ApiResult> { 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/")] async fn v1_get_vm( auth: Nip98Auth, db: &State>, vm_state: &State, id: u64, ) -> ApiResult { 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>) -> ApiResult> { let vms = db.list_os_image().await?; ApiData::ok(vms) } #[get("/api/v1/vm/templates")] async fn v1_list_vm_templates(db: &State>) -> ApiResult> { 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>, ) -> ApiResult> { 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 = "", format = "json")] async fn v1_add_ssh_key( auth: Nip98Auth, db: &State>, req: Json, ) -> ApiResult { 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 = "", format = "json")] async fn v1_create_vm_order( auth: Nip98Auth, db: &State>, provisioner: &State>, req: Json, ) -> ApiResult { 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//renew")] async fn v1_renew_vm( auth: Nip98Auth, db: &State>, provisioner: &State>, id: u64, ) -> ApiResult { 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/")] async fn v1_get_payment( auth: Nip98Auth, db: &State>, id: &str, ) -> ApiResult { 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 for VmTemplate { fn from(val: CreateVmRequest) -> Self { VmTemplate { id: val.template_id, ..Default::default() } } } #[derive(Deserialize)] struct CreateSshKey { name: String, key_data: String, }