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
14pub 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#[derive(Clone, Debug, Serialize, TS)]
63#[serde(tag = "tag")]
64enum RefDoc {
65 Readonly {
67 content: Value,
68 permissions: Permissions,
69 },
70
71 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#[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
185impl 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}