1use std::sync::Arc;
15
16use axum::Json;
17use axum::extract::State;
18use axum::http::header::ORIGIN;
19use axum::http::{HeaderMap, StatusCode};
20use axum::response::{IntoResponse, Response};
21use kernel::{AxCode, AxError};
22use serde::{Deserialize, Serialize};
23
24use crate::answer::DeviceId;
25
26use super::config::{ShellState, refusal_text};
27
28#[derive(Debug, Deserialize)]
31pub struct PairBody {
32 #[serde(flatten)]
33 pub proof: PairProof,
34 pub public_key: String,
35 pub label: String,
36}
37
38#[derive(Debug, Deserialize)]
40#[serde(rename_all = "snake_case")]
41pub enum PairProof {
42 Open(String),
44 Code(String),
46}
47
48#[derive(Debug, Serialize)]
50pub struct PairAnswer {
51 pub device: DeviceId,
52}
53
54#[derive(Debug, Serialize)]
56pub struct ChallengeAnswer {
57 pub nonce: String,
58}
59
60#[derive(Debug, Deserialize)]
63pub struct SessionBody {
64 pub device: DeviceId,
65 pub nonce: String,
66 pub signature: String,
67}
68
69#[derive(Debug, Serialize)]
72pub struct SessionAnswer {
73 pub token: String,
74}
75
76pub(super) async fn accept_pairing(
77 State(state): State<Arc<ShellState>>,
78 Json(body): Json<PairBody>,
79) -> Response {
80 match state.door.pair(&body.proof, &body.public_key, &body.label) {
81 Ok(device) => (StatusCode::OK, Json(PairAnswer { device })).into_response(),
82 Err(err) => refusal(&err),
83 }
84}
85
86pub(super) async fn offer_challenge(State(state): State<Arc<ShellState>>) -> Response {
87 match state.door.challenge() {
88 Ok(nonce) => (StatusCode::OK, Json(ChallengeAnswer { nonce })).into_response(),
89 Err(err) => refusal(&err),
90 }
91}
92
93pub(super) async fn open_session(
94 State(state): State<Arc<ShellState>>,
95 headers: HeaderMap,
96 Json(body): Json<SessionBody>,
97) -> Response {
98 let Some(origin) = headers.get(ORIGIN).and_then(|value| value.to_str().ok()) else {
102 return refusal(
103 &AxError::failure(
104 AxCode::GateDenied,
105 "open a browser session",
106 "the request carried no Origin",
107 )
108 .with_recovery("open a session from the city's own page"),
109 );
110 };
111 match state.door.open_session(&body, origin) {
112 Ok(token) => (StatusCode::OK, Json(SessionAnswer { token })).into_response(),
113 Err(err) => refusal(&err),
114 }
115}
116
117fn refusal(err: &AxError) -> Response {
119 let code = err.code();
120 let status = if matches!(code, AxCode::PairingRefused | AxCode::GateDenied) {
121 StatusCode::FORBIDDEN
122 } else if matches!(code, AxCode::InvalidArgs) {
123 StatusCode::UNPROCESSABLE_ENTITY
124 } else {
125 StatusCode::INTERNAL_SERVER_ERROR
126 };
127 (status, refusal_text(err)).into_response()
128}