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