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