backend/
rpc.rs

1use firebase_auth::FirebaseUser;
2use http::StatusCode;
3use qubit::{Extensions, FromRequestExtensions, Router, RpcError, handler};
4use serde::Serialize;
5use serde_json::Value;
6use tracing::debug;
7use ts_rs::TS;
8use uuid::Uuid;
9
10use crate::app::Paginated;
11
12use super::app::{AppCtx, AppError, AppState};
13use super::auth::{NewPermissions, PermissionLevel, Permissions};
14use super::{auth, document as doc, user};
15
16/// Create router for RPC API.
17pub fn router() -> Router<AppState> {
18    Router::new()
19        .handler(new_ref)
20        .handler(get_doc)
21        .handler(head_snapshot)
22        .handler(create_snapshot)
23        .handler(get_permissions)
24        .handler(set_permissions)
25        .handler(validate_session)
26        .handler(sign_up_or_sign_in)
27        .handler(user_by_username)
28        .handler(username_status)
29        .handler(get_active_user_profile)
30        .handler(set_active_user_profile)
31        .handler(search_ref_stubs)
32}
33
34#[handler(mutation)]
35async fn new_ref(ctx: AppCtx, content: Value) -> RpcResult<Uuid> {
36    doc::new_ref(ctx, content).await.into()
37}
38
39#[handler(query)]
40async fn get_doc(ctx: AppCtx, ref_id: Uuid) -> RpcResult<RefDoc> {
41    _get_doc(ctx, ref_id).await.into()
42}
43async fn _get_doc(ctx: AppCtx, ref_id: Uuid) -> Result<RefDoc, AppError> {
44    let permissions = auth::permissions(&ctx, ref_id).await?;
45    let max_level = permissions.max_level();
46    if max_level >= Some(PermissionLevel::Write) {
47        let doc_id = doc::doc_id(ctx.state, ref_id).await?;
48        Ok(RefDoc::Live {
49            doc_id,
50            permissions,
51        })
52    } else if max_level >= Some(PermissionLevel::Read) {
53        let content = doc::head_snapshot(ctx.state, ref_id).await?;
54        Ok(RefDoc::Readonly {
55            content,
56            permissions,
57        })
58    } else {
59        Err(AppError::Forbidden(ref_id))
60    }
61}
62
63/// Document identified by a ref.
64#[derive(Clone, Debug, Serialize, TS)]
65#[serde(tag = "tag")]
66enum RefDoc {
67    /// Readonly document, containing content at the current head.
68    Readonly {
69        content: Value,
70        permissions: Permissions,
71    },
72
73    /// Live document, containing an Automerge document ID.
74    Live {
75        #[serde(rename = "docId")]
76        doc_id: String,
77        permissions: Permissions,
78    },
79}
80
81#[handler(query)]
82async fn search_ref_stubs(
83    ctx: AppCtx,
84    query_params: doc::RefQueryParams,
85) -> RpcResult<Paginated<doc::RefStub>> {
86    doc::search_ref_stubs(ctx, query_params).await.into()
87}
88
89#[handler(query)]
90async fn head_snapshot(ctx: AppCtx, ref_id: Uuid) -> RpcResult<Value> {
91    _head_snapshot(ctx, ref_id).await.into()
92}
93async fn _head_snapshot(ctx: AppCtx, ref_id: Uuid) -> Result<Value, AppError> {
94    auth::authorize(&ctx, ref_id, PermissionLevel::Read).await?;
95    doc::head_snapshot(ctx.state, ref_id).await
96}
97
98#[handler(mutation)]
99async fn create_snapshot(ctx: AppCtx, ref_id: Uuid) -> RpcResult<()> {
100    async {
101        auth::authorize(&ctx, ref_id, PermissionLevel::Write).await?;
102        doc::create_snapshot(ctx.state, ref_id).await
103    }
104    .await
105    .into()
106}
107
108#[handler(query)]
109async fn get_permissions(ctx: AppCtx, ref_id: Uuid) -> RpcResult<Permissions> {
110    auth::permissions(&ctx, ref_id).await.into()
111}
112
113#[handler(mutation)]
114async fn set_permissions(ctx: AppCtx, ref_id: Uuid, new: NewPermissions) -> RpcResult<()> {
115    _set_permissions(ctx, ref_id, new).await.into()
116}
117async fn _set_permissions(ctx: AppCtx, ref_id: Uuid, new: NewPermissions) -> Result<(), AppError> {
118    if ctx.user.is_none() {
119        return Err(AppError::Unauthorized);
120    }
121    auth::authorize(&ctx, ref_id, PermissionLevel::Own).await?;
122    auth::set_permissions(&ctx.state, ref_id, new).await
123}
124
125#[handler(query)]
126async fn validate_session(ctx: AppCtx) -> RpcResult<()> {
127    auth::validate_session(ctx).await.into()
128}
129
130#[handler(mutation)]
131async fn sign_up_or_sign_in(ctx: AppCtx) -> RpcResult<()> {
132    user::sign_up_or_sign_in(ctx).await.into()
133}
134
135#[handler(query)]
136async fn user_by_username(ctx: AppCtx, username: &str) -> RpcResult<Option<user::UserSummary>> {
137    user::user_by_username(ctx.state, username).await.into()
138}
139
140#[handler(query)]
141async fn username_status(ctx: AppCtx, username: &str) -> RpcResult<user::UsernameStatus> {
142    user::username_status(ctx.state, username).await.into()
143}
144
145#[handler(query)]
146async fn get_active_user_profile(ctx: AppCtx) -> RpcResult<user::UserProfile> {
147    user::get_active_user_profile(ctx).await.into()
148}
149
150#[handler(mutation)]
151async fn set_active_user_profile(ctx: AppCtx, user: user::UserProfile) -> RpcResult<()> {
152    user::set_active_user_profile(ctx, user).await.into()
153}
154
155/// Result returned by an RPC handler.
156#[derive(Debug, Clone, Serialize, TS)]
157#[serde(tag = "tag")]
158enum RpcResult<T> {
159    Ok { content: T },
160    Err { code: u16, message: String },
161}
162
163impl<T> From<AppError> for RpcResult<T> {
164    fn from(error: AppError) -> Self {
165        let code = match error {
166            AppError::Invalid(_) => StatusCode::BAD_REQUEST,
167            AppError::Unauthorized => StatusCode::UNAUTHORIZED,
168            AppError::Forbidden(_) => StatusCode::FORBIDDEN,
169            AppError::Db(sqlx::Error::RowNotFound) => StatusCode::NOT_FOUND,
170            _ => StatusCode::INTERNAL_SERVER_ERROR,
171        };
172        RpcResult::Err {
173            code: code.as_u16(),
174            message: error.to_string(),
175        }
176    }
177}
178
179impl<T> From<Result<T, AppError>> for RpcResult<T> {
180    fn from(result: Result<T, AppError>) -> Self {
181        match result {
182            Ok(content) => RpcResult::Ok { content },
183            Err(error) => error.into(),
184        }
185    }
186}
187
188/// Extract user from request extension, if present.
189impl FromRequestExtensions<AppState> for AppCtx {
190    async fn from_request_extensions(
191        state: AppState,
192        mut extensions: Extensions,
193    ) -> Result<Self, RpcError> {
194        let user: Option<FirebaseUser> = extensions.remove();
195        if let Some(some_user) = &user {
196            debug!("Handling request from user: {}", some_user.user_id);
197        }
198        Ok(AppCtx { state, user })
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use std::path::PathBuf;
205
206    #[test]
207    fn rspc_type_defs() {
208        let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("pkg").join("src");
209        super::router().write_bindings_to_dir(dir);
210    }
211}