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