use crate::server::app::AppState;
use axum::extract::State;
use axum::response::{IntoResponse, Response};
use axum::routing::{get, post};
use axum::{Json, Router};
use serde_json::json;
pub(crate) fn routes() -> Router<AppState> {
Router::new()
.route("/api/remote/status", get(status))
.route("/api/remote/connect", post(connect))
.route("/api/remote/pair", post(start_pairing).delete(unpair))
.route("/api/remote/pair/poll", post(poll_pairing))
}
#[derive(Clone, Default)]
pub(crate) struct PendingPairing(
std::sync::Arc<std::sync::Mutex<Option<nomoreide_core::remote::pairing::PairingTicket>>>,
);
impl PendingPairing {
fn put(&self, ticket: nomoreide_core::remote::pairing::PairingTicket) {
*self.0.lock().unwrap() = Some(ticket);
}
fn get(&self) -> Option<nomoreide_core::remote::pairing::PairingTicket> {
self.0.lock().unwrap().clone()
}
fn clear(&self) {
*self.0.lock().unwrap() = None;
}
}
async fn start_pairing(State(state): State<AppState>) -> Response {
use nomoreide_core::remote::pairing::{DeviceProposal, PairingFlow};
let flow = PairingFlow::discover();
let proposal = DeviceProposal::for_this_machine(None);
match flow.start(&proposal).await {
Ok(ticket) => {
state.pending_pairing.put(ticket.clone());
Json(json!({
"ok": true,
"userCode": ticket.user_code,
"verificationUrl": ticket.verification_url,
"verificationQr": nomoreide_core::remote::qr::encode(ticket.qr_payload()),
"expiresAt": ticket.expires_at,
"deviceName": proposal.name,
}))
.into_response()
}
Err(error) => pairing_failure(error),
}
}
async fn poll_pairing(State(state): State<AppState>) -> Response {
use nomoreide_core::remote::pairing::{PairingFlow, PairingStatus};
let Some(ticket) = state.pending_pairing.get() else {
return Json(json!({ "ok": false, "status": "noPairingInProgress" })).into_response();
};
let flow = PairingFlow::discover();
let progress = match flow.poll(&ticket).await {
Ok(progress) => progress,
Err(error) => return pairing_failure(error),
};
match progress.status {
PairingStatus::Pending => Json(json!({ "ok": true, "status": "pending" })).into_response(),
PairingStatus::Expired => {
state.pending_pairing.clear();
Json(json!({ "ok": false, "status": "expired" })).into_response()
}
PairingStatus::Claimed | PairingStatus::Exchanged => match flow.complete(&ticket).await {
Ok(stored) => {
state.pending_pairing.clear();
state.relay.ensure_started();
Json(json!({
"ok": true,
"status": "paired",
"deviceName": stored.device_name,
"deviceId": stored.device_id,
}))
.into_response()
}
Err(error) => pairing_failure(error),
},
}
}
async fn unpair(State(state): State<AppState>) -> Response {
state.pending_pairing.clear();
let retired = state.relay.retire();
match nomoreide_core::remote::credentials::RemoteCredentials::discover().clear() {
Ok(had) => {
Json(json!({ "ok": true, "wasPaired": had, "retired": retired })).into_response()
}
Err(error) => Json(json!({ "ok": false, "error": error.to_string() })).into_response(),
}
}
fn pairing_failure(error: nomoreide_core::remote::pairing::PairingError) -> Response {
Json(json!({ "ok": false, "status": "failed", "error": error.to_string() })).into_response()
}
async fn status(State(state): State<AppState>) -> Response {
let paired = nomoreide_core::remote::credentials::RemoteCredentials::discover().load();
let relay = state.relay.snapshot();
Json(json!({
"ok": true,
"paired": paired.is_some(),
"deviceName": paired.as_ref().map(|stored| stored.device_name.clone()),
"deviceId": paired.as_ref().map(|stored| stored.device_id.clone()),
"platformBaseUrl": paired.as_ref().map(|stored| stored.platform_base_url.clone()),
"webBaseUrl": paired.as_ref().map(|stored| stored.web_base_url().to_string()),
"relay": relay,
}))
.into_response()
}
async fn connect(State(state): State<AppState>) -> Response {
use crate::remote::supervisor::StartOutcome;
let outcome = state.relay.ensure_started();
let (ok, status) = match outcome {
StartOutcome::Started => (true, "started"),
StartOutcome::AlreadyRunning => (true, "alreadyRunning"),
StartOutcome::NotPaired => (false, "notPaired"),
StartOutcome::Disabled => (false, "disabled"),
};
Json(json!({ "ok": ok, "status": status })).into_response()
}