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};
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 let pid = state.db.principal_of(me.id).await?;
37 state.db.pim_ensure_defaults(pid).await?;
38 let mut out = Vec::new();
39 for kind in [PimKind::Calendar, PimKind::AddressBook] {
40 for c in state.db.pim_collections(pid, kind).await? {
41 if kind == PimKind::Calendar && c.slug == INBOX {
42 continue;
43 }
44 out.push(PimCollectionInfo {
45 id: c.id,
46 kind: wire_kind(kind),
47 name: name_of(&c),
48 url: collection_href(&me.name, kind, &c.slug, None),
49 owner: me.name.clone(),
50 mode: None,
51 });
52 }
53 for (c, owner, mode) in state.db.pim_shared_collections(me.id, kind).await? {
54 out.push(PimCollectionInfo {
55 id: c.id,
56 kind: wire_kind(kind),
57 name: name_of(&c),
58 url: collection_href(&me.name, kind, &c.slug, Some(c.id)),
59 owner,
60 mode: Some(mode),
61 });
62 }
63 }
64 Ok(Json(out))
65}
66
67/// The id of a collection the signed-in user owns, or 404.
68async fn own(state: &AppState, auth: &SessionUser, id: i64) -> Result<i64, ApiError> {
69 let pid = state.db.principal_of(auth.user.id).await?;
70 match state.db.pim_collection_by_id(id).await? {
71 // The inbox is not lent: it holds messages, not events.
72 Some((owner, _, c)) if owner == pid && c.slug != INBOX => Ok(id),
73 _ => Err(ApiError::new(StatusCode::NOT_FOUND, "collection not found")),
74 }
75}
76
77/// GET {PIM_COLLECTIONS}/{id}{SHARES_SUFFIX}
78pub async fn shares(
79 State(state): State<Arc<AppState>>,
80 auth: SessionUser,
81 AxumPath(id): AxumPath<i64>,
82) -> Result<Json<Vec<PimShareInfo>>, ApiError> {
83 let id = own(&state, &auth, id).await?;
84 let out = state
85 .db
86 .pim_shares(id)
87 .await?
88 .into_iter()
89 .map(|(user_id, user_name, mode)| PimShareInfo {
90 user_id,
91 user_name,
92 mode,
93 })
94 .collect();
95 Ok(Json(out))
96}
97
98/// POST {PIM_COLLECTIONS}/{id}{SHARES_SUFFIX}
99pub async fn share(
100 State(state): State<Arc<AppState>>,
101 auth: SessionUser,
102 AxumPath(id): AxumPath<i64>,
103 Json(body): Json<CreatePimShare>,
104) -> Result<Json<PimShareInfo>, ApiError> {
105 let id = own(&state, &auth, id).await?;
106 let user = state
107 .db
108 .pim_principal(body.user.trim())
109 .await?
110 .ok_or_else(|| ApiError::new(StatusCode::NOT_FOUND, "user not found"))?;
111 let Some(user_id) = user.user_id else {
112 return Err(ApiError::new(StatusCode::NOT_FOUND, "user not found"));
113 };
114 if user_id == auth.user.id {
115 return Err(ApiError::new(
116 StatusCode::BAD_REQUEST,
117 "a collection cannot be shared with its owner",
118 ));
119 }
120 state.db.pim_set_share(id, user_id, body.mode).await?;
121 Ok(Json(PimShareInfo {
122 user_id,
123 user_name: user.name,
124 mode: body.mode,
125 }))
126}
127
128/// DELETE {PIM_COLLECTIONS}/{id}{SHARES_SUFFIX}/{user_id}
129pub async fn unshare(
130 State(state): State<Arc<AppState>>,
131 auth: SessionUser,
132 AxumPath((id, user_id)): AxumPath<(i64, i64)>,
133) -> Result<Json<OkResp>, ApiError> {
134 let id = own(&state, &auth, id).await?;
135 if !state.db.pim_remove_share(id, user_id).await? {
136 return Err(ApiError::new(StatusCode::NOT_FOUND, "share not found"));
137 }
138 Ok(Json(OkResp {}))
139}
140