//! JSON management of calendars and address books (session-authenticated): //! - `GET {PIM_COLLECTIONS}` — own and lent collections //! - `GET {PIM_COLLECTIONS}/{id}{SHARES_SUFFIX}` — loans of an own collection //! - `POST {PIM_COLLECTIONS}/{id}{SHARES_SUFFIX}` — lend it, or change a loan //! - `DELETE {PIM_COLLECTIONS}/{id}{SHARES_SUFFIX}/{user_id}` — end a loan use std::sync::Arc; use api_types::{CreatePimShare, OkResp, PimCollectionInfo, PimCollectionKind, PimShareInfo}; use axum::Json; use axum::extract::{Path as AxumPath, State}; use axum::http::StatusCode; use crate::api::common::SessionUser; use crate::api::pim::collection_href; use crate::db::{PimCollection, PimKind, UserType}; use crate::error::{ApiError, AppState}; fn wire_kind(kind: PimKind) -> PimCollectionKind { match kind { PimKind::Calendar => PimCollectionKind::Calendar, PimKind::AddressBook => PimCollectionKind::Addressbook, } } fn name_of(c: &PimCollection) -> String { c.displayname.clone().unwrap_or_else(|| c.slug.clone()) } /// GET {PIM_COLLECTIONS} pub async fn list( State(state): State>, auth: SessionUser, ) -> Result>, ApiError> { let me = &auth.user; state.db.pim_ensure_defaults(me.id).await?; let mut out = Vec::new(); for kind in [PimKind::Calendar, PimKind::AddressBook] { for c in state.db.pim_collections(me.id, kind).await? { out.push(PimCollectionInfo { id: c.id, kind: wire_kind(kind), name: name_of(&c), url: collection_href(&me.name, kind, &c.slug, None), owner: me.name.clone(), mode: None, }); } for (c, owner, mode) in state.db.pim_shared_collections(me.id, kind).await? { out.push(PimCollectionInfo { id: c.id, kind: wire_kind(kind), name: name_of(&c), url: collection_href(&me.name, kind, &c.slug, Some(c.id)), owner, mode: Some(mode), }); } } Ok(Json(out)) } /// The id of a collection the signed-in user owns, or 404. async fn own(state: &AppState, auth: &SessionUser, id: i64) -> Result { match state.db.pim_collection_by_id(id).await? { Some((owner, _, _)) if owner == auth.user.id => Ok(id), _ => Err(ApiError::new(StatusCode::NOT_FOUND, "collection not found")), } } /// GET {PIM_COLLECTIONS}/{id}{SHARES_SUFFIX} pub async fn shares( State(state): State>, auth: SessionUser, AxumPath(id): AxumPath, ) -> Result>, ApiError> { let id = own(&state, &auth, id).await?; let out = state .db .pim_shares(id) .await? .into_iter() .map(|(user_id, user_name, mode)| PimShareInfo { user_id, user_name, mode, }) .collect(); Ok(Json(out)) } /// POST {PIM_COLLECTIONS}/{id}{SHARES_SUFFIX} pub async fn share( State(state): State>, auth: SessionUser, AxumPath(id): AxumPath, Json(body): Json, ) -> Result, ApiError> { let id = own(&state, &auth, id).await?; let user = state .db .pim_principal(body.user.trim()) .await? .filter(|p| p.kind == UserType::Individual) .ok_or_else(|| ApiError::new(StatusCode::NOT_FOUND, "user not found"))?; if user.id == auth.user.id { return Err(ApiError::new( StatusCode::BAD_REQUEST, "a collection cannot be shared with its owner", )); } state.db.pim_set_share(id, user.id, body.mode).await?; Ok(Json(PimShareInfo { user_id: user.id, user_name: user.name, mode: body.mode, })) } /// DELETE {PIM_COLLECTIONS}/{id}{SHARES_SUFFIX}/{user_id} pub async fn unshare( State(state): State>, auth: SessionUser, AxumPath((id, user_id)): AxumPath<(i64, i64)>, ) -> Result, ApiError> { let id = own(&state, &auth, id).await?; if !state.db.pim_remove_share(id, user_id).await? { return Err(ApiError::new(StatusCode::NOT_FOUND, "share not found")); } Ok(Json(OkResp {})) }