pim_api.rs
| 1 | //! JSON management of calendars and address books (session-authenticated): |
| 2 | //! - `GET {PIM_COLLECTIONS}` — own and lent collections |
| 3 | //! - `GET {PIM_COLLECTIONS}/{id}{SHARES_SUFFIX}` — loans of an own collection |
| 4 | //! - `POST {PIM_COLLECTIONS}/{id}{SHARES_SUFFIX}` — lend it, or change a loan |
| 5 | //! - `DELETE {PIM_COLLECTIONS}/{id}{SHARES_SUFFIX}/{user_id}` — end a loan |
| 6 | |
| 7 | use std::sync::Arc; |
| 8 | |
| 9 | use api_types::{CreatePimShare, OkResp, PimCollectionInfo, PimCollectionKind, PimShareInfo}; |
| 10 | use axum::Json; |
| 11 | use axum::extract::{Path as AxumPath, State}; |
| 12 | use axum::http::StatusCode; |
| 13 | |
| 14 | use crate::api::common::SessionUser; |
| 15 | use crate::api::pim::{INBOX, collection_href}; |
| 16 | use crate::db::{PimCollection, PimKind, UserType}; |
| 17 | use crate::error::{ApiError, AppState}; |
| 18 | |
| 19 | fn wire_kind(kind: PimKind) -> PimCollectionKind { |
| 20 | match kind { |
| 21 | PimKind::Calendar => PimCollectionKind::Calendar, |
| 22 | PimKind::AddressBook => PimCollectionKind::Addressbook, |
| 23 | } |
| 24 | } |
| 25 | |
| 26 | fn name_of(c: &PimCollection) -> String { |
| 27 | c.displayname.clone().unwrap_or_else(|| c.slug.clone()) |
| 28 | } |
| 29 | |
| 30 | /// GET {PIM_COLLECTIONS} |
| 31 | pub async fn list( |
| 32 | State(state): State<Arc<AppState>>, |
| 33 | auth: SessionUser, |
| 34 | ) -> Result<Json<Vec<PimCollectionInfo>>, ApiError> { |
| 35 | let me = &auth.user; |
| 36 | state.db.pim_ensure_defaults(me.id).await?; |
| 37 | let mut out = Vec::new(); |
| 38 | for kind in [PimKind::Calendar, PimKind::AddressBook] { |
| 39 | for c in state.db.pim_collections(me.id, kind).await? { |
| 40 | if kind == PimKind::Calendar && c.slug == INBOX { |
| 41 | continue; |
| 42 | } |
| 43 | out.push(PimCollectionInfo { |
| 44 | id: c.id, |
| 45 | kind: wire_kind(kind), |
| 46 | name: name_of(&c), |
| 47 | url: collection_href(&me.name, kind, &c.slug, None), |
| 48 | owner: me.name.clone(), |
| 49 | mode: None, |
| 50 | }); |
| 51 | } |
| 52 | for (c, owner, mode) in state.db.pim_shared_collections(me.id, kind).await? { |
| 53 | out.push(PimCollectionInfo { |
| 54 | id: c.id, |
| 55 | kind: wire_kind(kind), |
| 56 | name: name_of(&c), |
| 57 | url: collection_href(&me.name, kind, &c.slug, Some(c.id)), |
| 58 | owner, |
| 59 | mode: Some(mode), |
| 60 | }); |
| 61 | } |
| 62 | } |
| 63 | Ok(Json(out)) |
| 64 | } |
| 65 | |
| 66 | /// The id of a collection the signed-in user owns, or 404. |
| 67 | async fn own(state: &AppState, auth: &SessionUser, id: i64) -> Result<i64, ApiError> { |
| 68 | match state.db.pim_collection_by_id(id).await? { |
| 69 | // The inbox is not lent: it holds messages, not events. |
| 70 | Some((owner, _, c)) if owner == auth.user.id && c.slug != INBOX => Ok(id), |
| 71 | _ => Err(ApiError::new(StatusCode::NOT_FOUND, "collection not found")), |
| 72 | } |
| 73 | } |
| 74 | |
| 75 | /// GET {PIM_COLLECTIONS}/{id}{SHARES_SUFFIX} |
| 76 | pub async fn shares( |
| 77 | State(state): State<Arc<AppState>>, |
| 78 | auth: SessionUser, |
| 79 | AxumPath(id): AxumPath<i64>, |
| 80 | ) -> Result<Json<Vec<PimShareInfo>>, ApiError> { |
| 81 | let id = own(&state, &auth, id).await?; |
| 82 | let out = state |
| 83 | .db |
| 84 | .pim_shares(id) |
| 85 | .await? |
| 86 | .into_iter() |
| 87 | .map(|(user_id, user_name, mode)| PimShareInfo { |
| 88 | user_id, |
| 89 | user_name, |
| 90 | mode, |
| 91 | }) |
| 92 | .collect(); |
| 93 | Ok(Json(out)) |
| 94 | } |
| 95 | |
| 96 | /// POST {PIM_COLLECTIONS}/{id}{SHARES_SUFFIX} |
| 97 | pub async fn share( |
| 98 | State(state): State<Arc<AppState>>, |
| 99 | auth: SessionUser, |
| 100 | AxumPath(id): AxumPath<i64>, |
| 101 | Json(body): Json<CreatePimShare>, |
| 102 | ) -> Result<Json<PimShareInfo>, ApiError> { |
| 103 | let id = own(&state, &auth, id).await?; |
| 104 | let user = state |
| 105 | .db |
| 106 | .pim_principal(body.user.trim()) |
| 107 | .await? |
| 108 | .filter(|p| p.kind == UserType::Individual) |
| 109 | .ok_or_else(|| ApiError::new(StatusCode::NOT_FOUND, "user not found"))?; |
| 110 | if user.id == auth.user.id { |
| 111 | return Err(ApiError::new( |
| 112 | StatusCode::BAD_REQUEST, |
| 113 | "a collection cannot be shared with its owner", |
| 114 | )); |
| 115 | } |
| 116 | state.db.pim_set_share(id, user.id, body.mode).await?; |
| 117 | Ok(Json(PimShareInfo { |
| 118 | user_id: user.id, |
| 119 | user_name: user.name, |
| 120 | mode: body.mode, |
| 121 | })) |
| 122 | } |
| 123 | |
| 124 | /// DELETE {PIM_COLLECTIONS}/{id}{SHARES_SUFFIX}/{user_id} |
| 125 | pub async fn unshare( |
| 126 | State(state): State<Arc<AppState>>, |
| 127 | auth: SessionUser, |
| 128 | AxumPath((id, user_id)): AxumPath<(i64, i64)>, |
| 129 | ) -> Result<Json<OkResp>, ApiError> { |
| 130 | let id = own(&state, &auth, id).await?; |
| 131 | if !state.db.pim_remove_share(id, user_id).await? { |
| 132 | return Err(ApiError::new(StatusCode::NOT_FOUND, "share not found")); |
| 133 | } |
| 134 | Ok(Json(OkResp {})) |
| 135 | } |
| 136 |