pim_api.rs
⎇
Raw
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
7use std::sync::Arc;
8
9use api_types::{CreatePimShare, OkResp, PimCollectionInfo, PimCollectionKind, PimShareInfo};
10use axum::Json;
11use axum::extract::{Path as AxumPath, State};
12use axum::http::StatusCode;
13
14use crate::api::common::SessionUser;
15use crate::api::pim::{INBOX, collection_href};
16use crate::db::{PimCollection, PimKind, UserType};
17use crate::error::{ApiError, AppState};
18
19fn wire_kind(kind: PimKind) -> PimCollectionKind {
20 match kind {
21 PimKind::Calendar => PimCollectionKind::Calendar,
22 PimKind::AddressBook => PimCollectionKind::Addressbook,
23 }
24}
25
26fn name_of(c: &PimCollection) -> String {
27 c.displayname.clone().unwrap_or_else(|| c.slug.clone())
28}
29
30/// GET {PIM_COLLECTIONS}
31pub 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.
67async 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}
76pub 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}
97pub 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}
125pub 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