spacetimedb_client_api/routes/
identity.rs

1use std::time::Duration;
2
3use axum::extract::{Path, State};
4use axum::response::IntoResponse;
5use http::header::CONTENT_TYPE;
6use http::StatusCode;
7use serde::{Deserialize, Serialize};
8
9use spacetimedb_lib::de::serde::DeserializeWrapper;
10use spacetimedb_lib::Identity;
11
12use crate::auth::{JwtAuthProvider, SpacetimeAuth, SpacetimeAuthRequired};
13use crate::{log_and_500, ControlStateDelegate, NodeDelegate};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct CreateIdentityResponse {
17    identity: Identity,
18    token: String,
19}
20
21pub async fn create_identity<S: ControlStateDelegate + NodeDelegate>(
22    State(ctx): State<S>,
23) -> axum::response::Result<impl IntoResponse> {
24    let auth = SpacetimeAuth::alloc(&ctx).await?;
25
26    let identity_response = CreateIdentityResponse {
27        identity: auth.identity,
28        token: auth.creds.token().to_owned(),
29    };
30    Ok(axum::Json(identity_response))
31}
32
33/// A version of `Identity` appropriate for URL de/encoding.
34///
35/// Because `Identity` is represented in SATS as a `ProductValue`,
36/// its serialized format is somewhat gnarly.
37/// When URL-encoding identities, we want to use only the hex string,
38/// without wrapping it in a `ProductValue`.
39/// This keeps our routes pretty, like `/identity/<64 hex chars>/set-email`.
40///
41/// This newtype around `Identity` implements `Deserialize`
42/// directly from the inner identity bytes,
43/// without the enclosing `ProductValue` wrapper.
44#[derive(derive_more::Into, Clone, Debug, Copy)]
45pub struct IdentityForUrl(Identity);
46
47impl From<Identity> for IdentityForUrl {
48    fn from(i: Identity) -> Self {
49        IdentityForUrl(i)
50    }
51}
52
53impl IdentityForUrl {
54    pub fn into_inner(&self) -> Identity {
55        self.0
56    }
57}
58
59impl<'de> serde::Deserialize<'de> for IdentityForUrl {
60    fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
61        <_>::deserialize(de).map(|DeserializeWrapper(b)| IdentityForUrl(Identity::from_be_byte_array(b)))
62    }
63}
64
65#[derive(Deserialize)]
66pub struct GetDatabasesParams {
67    identity: IdentityForUrl,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct GetDatabasesResponse {
72    identities: Vec<Identity>,
73}
74
75pub async fn get_databases<S: ControlStateDelegate>(
76    State(ctx): State<S>,
77    Path(GetDatabasesParams { identity }): Path<GetDatabasesParams>,
78) -> axum::response::Result<impl IntoResponse> {
79    let identity = identity.into();
80    // Linear scan for all databases that have this owner, and return their identities
81    let all_dbs = ctx.get_databases().map_err(|e| {
82        log::error!("Failure when retrieving databases for search: {e}");
83        StatusCode::INTERNAL_SERVER_ERROR
84    })?;
85    let identities = all_dbs
86        .iter()
87        .filter(|db| db.owner_identity == identity)
88        .map(|db| db.database_identity)
89        .collect();
90    Ok(axum::Json(GetDatabasesResponse { identities }))
91}
92
93#[derive(Debug, Serialize)]
94pub struct WebsocketTokenResponse {
95    pub token: String,
96}
97
98// This endpoint takes a token from a client and sends a newly signed token with a 60s expiry.
99// Note that even if the token has a different issuer, we will sign it with our key.
100// This is ok because `FullTokenValidator` checks if we signed the token before worrying about the issuer.
101pub async fn create_websocket_token<S: NodeDelegate>(
102    State(ctx): State<S>,
103    SpacetimeAuthRequired(auth): SpacetimeAuthRequired,
104) -> axum::response::Result<impl IntoResponse> {
105    let expiry = Duration::from_secs(60);
106    let token = auth
107        .re_sign_with_expiry(ctx.jwt_auth_provider(), expiry)
108        .map_err(log_and_500)?;
109    // let token = encode_token_with_expiry(ctx.private_key(), auth.identity, Some(expiry)).map_err(log_and_500)?;
110    Ok(axum::Json(WebsocketTokenResponse { token }))
111}
112
113#[derive(Deserialize)]
114pub struct ValidateTokenParams {
115    identity: IdentityForUrl,
116}
117
118pub async fn validate_token(
119    Path(ValidateTokenParams { identity }): Path<ValidateTokenParams>,
120    SpacetimeAuthRequired(auth): SpacetimeAuthRequired,
121) -> axum::response::Result<impl IntoResponse> {
122    let identity = Identity::from(identity);
123
124    if auth.identity != identity {
125        return Err(StatusCode::BAD_REQUEST.into());
126    }
127
128    Ok(StatusCode::NO_CONTENT)
129}
130
131pub async fn get_public_key<S: NodeDelegate>(State(ctx): State<S>) -> axum::response::Result<impl IntoResponse> {
132    Ok((
133        [(CONTENT_TYPE, "application/pem-certificate-chain")],
134        ctx.jwt_auth_provider().public_key_bytes().to_owned(),
135    ))
136}
137
138pub fn router<S>() -> axum::Router<S>
139where
140    S: NodeDelegate + ControlStateDelegate + Clone + 'static,
141{
142    use axum::routing::{get, post};
143    axum::Router::new()
144        .route("/", post(create_identity::<S>))
145        .route("/public-key", get(get_public_key::<S>))
146        .route("/websocket-token", post(create_websocket_token::<S>))
147        .route("/:identity/verify", get(validate_token))
148        .route("/:identity/databases", get(get_databases::<S>))
149}