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