Skip to main content

pubhubs/api/
phc.rs

1//! Additional endpoints provided by PubHubs Central
2use crate::api::*;
3
4use std::collections::{HashMap, HashSet};
5
6use actix_web::http::{self, header};
7use serde::{Deserialize, Serialize};
8
9use crate::attr;
10use crate::handle;
11use crate::id::Id;
12use crate::misc::serde_ext::bytes_wrapper::B64UU;
13use crate::servers::Constellation;
14
15/// `.ph/hub/...` endpoints, used by hubs
16pub mod hub {
17    use super::*;
18
19    /// Used by a hub to request a ticket (see [`TicketContent`]) from PubHubs Central.
20    /// The request must be signed for the `verifying_key` advertised by the hub info endoint
21    /// (see [`crate::api::hub::InfoEP`]).
22    ///
23    /// If the signature cannot be verified, [`ErrorCode::BadRequest`] is returned.
24    pub struct TicketEP {}
25    impl EndpointDetails for TicketEP {
26        type RequestType = Signed<TicketReq>;
27        type ResponseType = Result<TicketResp>;
28
29        const METHOD: http::Method = http::Method::POST;
30        const PATH: &'static str = ".ph/hub/ticket";
31    }
32
33    having_message_code!(TicketReq, PhcHubTicketReq);
34
35    #[derive(Serialize, Deserialize, Debug, Clone)]
36    #[serde(deny_unknown_fields)]
37    pub struct TicketReq {
38        pub handle: crate::handle::Handle,
39    }
40
41    /// What [`TicketEP`] returns
42    #[derive(Serialize, Deserialize, Debug, Clone)]
43    #[serde(deny_unknown_fields)]
44    #[must_use]
45    pub enum TicketResp {
46        Success(Ticket),
47
48        /// No hub known with this handle
49        UnknownHub,
50
51        /// Hub has no verifying key set
52        NoVerifyingKey,
53    }
54
55    pub type Ticket = Signed<TicketContent>;
56
57    /// A ticket, a [`Signed`] [`TicketContent`], certifies that the hub uses the given
58    /// `verifying_key`.
59    #[derive(Serialize, Deserialize, Debug, Clone)]
60    #[serde(deny_unknown_fields)]
61    pub struct TicketContent {
62        pub handle: crate::handle::Handle,
63        pub verifying_key: VerifyingKeyBytes,
64    }
65
66    having_message_code!(TicketContent, PhcHubTicket);
67
68    /// A [`Signed`] message together with a [`Ticket`].
69    #[derive(Serialize, Deserialize, Debug, Clone)]
70    #[serde(deny_unknown_fields)]
71    pub struct TicketSigned<T> {
72        pub ticket: Ticket,
73        signed: Signed<T>,
74    }
75
76    impl<T> TicketSigned<T> {
77        /// Opens this [`TicketSigned`], checking the signature on `signed` using the verifying key in
78        /// the provided `ticket`, and checking the `ticket` using `key`.
79        pub fn open(
80            self,
81            key: &VerifyingKey,
82        ) -> std::result::Result<(T, crate::handle::Handle), TicketOpenError>
83        where
84            T: Signable,
85        {
86            let ticket_content: TicketContent = self
87                .ticket
88                .open(key, None)
89                .map_err(TicketOpenError::Ticket)?;
90
91            // PHC validates the hub's verifying key when it mints the ticket, so a malformed key in a
92            // valid ticket is PHC's fault — report it as a Ticket error, not a Signed one.
93            let hub_verifying_key = ticket_content
94                .verifying_key
95                .decode()
96                .map_err(|_| TicketOpenError::Ticket(OpenError::OtherwiseInvalid))?;
97
98            let msg: T = self
99                .signed
100                .open(&hub_verifying_key, None)
101                .map_err(TicketOpenError::Signed)?;
102
103            Ok((msg, ticket_content.handle))
104        }
105
106        pub fn new(ticket: Ticket, signed: Signed<T>) -> Self {
107            Self { ticket, signed }
108        }
109    }
110
111    /// Error returned by [`TicketSigned::open`].
112    #[derive(thiserror::Error, Debug)]
113    pub enum TicketOpenError {
114        #[error("ticket is invalid")]
115        Ticket(#[source] OpenError),
116
117        #[error("ticket is valid, but not the signature made using it")]
118        Signed(#[source] OpenError),
119    }
120
121    impl TicketOpenError {
122        /// Standard verdict for this error: respond with `retry_response` (when the hub can
123        /// recover by obtaining a new ticket), or fail the handler with an [`ErrorCode`].
124        ///
125        /// `retry_response` is typically the handler's `RetryWithNewTicket` response variant.
126        pub fn default_verdict<R>(self, retry_response: R) -> Result<R> {
127            match self {
128                // Ticket failed PHC's signature check or has aged past its TTL: requesting a fresh
129                // ticket from PHC fixes both cases.
130                Self::Ticket(OpenError::InvalidSignature) | Self::Ticket(OpenError::Expired) => {
131                    Ok(retry_response)
132                }
133                // Internal crypto failure.  `Ticket(OtherConstellation)` is defensive: the ticket
134                // is not constellation-bound (see [`Signable::CONSTELLATION_BOUND`]), so this
135                // arm would only fire on a code bug.
136                Self::Ticket(OpenError::InternalError)
137                | Self::Ticket(OpenError::OtherConstellation(..))
138                | Self::Signed(OpenError::InternalError) => Err(ErrorCode::InternalError),
139                // The hub just created the inner `Signed` for this request, so a bad/expired
140                // signature, a malformed JWT, or a constellation mismatch all point to a
141                // hub-side bug; surface as `BadRequest` so the hub investigates rather than
142                // loops on a fresh ticket.  (`Signed(OtherConstellation)` is currently
143                // unreachable: [`TicketSigned`] does not support constellation-bound inner
144                // messages.  If support is added, that arm should get its own
145                // `RetryWithNewConstellation` response variant instead.)
146                Self::Ticket(OpenError::OtherwiseInvalid)
147                | Self::Signed(OpenError::OtherwiseInvalid)
148                | Self::Signed(OpenError::InvalidSignature)
149                | Self::Signed(OpenError::Expired)
150                | Self::Signed(OpenError::OtherConstellation(..)) => Err(ErrorCode::BadRequest),
151            }
152        }
153    }
154}
155
156/// `.ph/user/...` endpoints, used by the ('global') web client
157pub mod user {
158    use super::*;
159
160    /// Provides the global client with basic details about the current PubHubs setup.
161    pub struct WelcomeEP {}
162    impl EndpointDetails for WelcomeEP {
163        type RequestType = NoPayload;
164        type ResponseType = Result<WelcomeResp>;
165
166        const METHOD: http::Method = http::Method::GET;
167        const PATH: &'static str = ".ph/user/welcome";
168    }
169
170    /// Returned by [`WelcomeEP`].
171    #[derive(Serialize, Deserialize, Debug, Clone)]
172    #[serde(deny_unknown_fields)]
173    pub struct WelcomeResp {
174        pub constellation: Constellation,
175        pub hubs: HashMap<handle::Handle, crate::hub::BasicInfo>,
176    }
177
178    /// Provides the global client with cached details about the hubs.
179    ///
180    /// Hub information is retrieved by default every minute, but this can be configured,
181    /// see [`crate::servers::config::phc::ExtraConfig::hub_cache`].
182    ///
183    /// If a hub becomes unreachable, the last seen hub info will remain in the cache.
184    ///
185    /// To see whether a hub is offline, you can compare the
186    /// [`crate::api::hub::DynamicHubInfo::last_reload`] value of the [`crate::api::hub::InfoResp::dynamic`] field
187    /// against the current time.  If the difference is more than, say, three minutes, the hub is
188    /// likely offline.  (The hub currently updates every minute, PHC fetches these updates every
189    /// minute, and pushes this after at most 5 seconds to the `App`s.)
190    #[derive(Debug)]
191    pub struct CachedHubInfoEP {}
192    impl EndpointDetails for CachedHubInfoEP {
193        type RequestType = NoPayload;
194        type ResponseType = Result<CachedHubInfoResp>;
195
196        const METHOD: http::Method = http::Method::GET;
197        const PATH: &'static str = ".ph/user/cached-hub-info";
198    }
199
200    /// Returned by [`CachedHubInfoEP`].
201    #[derive(Serialize, Deserialize, Debug, Clone)]
202    #[serde(deny_unknown_fields)]
203    pub struct CachedHubInfoResp {
204        pub hubs: HashMap<handle::Handle, Option<crate::api::hub::InfoResp>>,
205    }
206
207    /// Login (and register if needed)
208    pub struct EnterEP {}
209    impl EndpointDetails for EnterEP {
210        type RequestType = EnterReq;
211        type ResponseType = Result<EnterResp>;
212
213        const METHOD: http::Method = http::Method::POST;
214        const PATH: &'static str = ".ph/user/enter";
215    }
216
217    /// Request to log in to an existing account, or register a new one.
218    ///
219    /// Also used to add attributes to the new or existing user account.
220    ///
221    /// May fail with [`ErrorCode::BadRequest`] when:
222    ///  - [`identifying_attr`] is not identifying
223    ///  - The same attribute appears twice among [`add_attrs`] and [`identifying_attr`].
224    ///  - A non-addable attribute is in `add_attrs` (such as a pubhubs card attribute not obtained
225    ///    via the [`auths::CardEP`] endpoint.
226    ///  - Neither an identifying nor a auth token (via the `Authorization` header) is provided.
227    ///  - When the auth token is used, but the mode is not login.
228    ///
229    /// [`identifying_attr`]: Self::identifying_attr
230    /// [`add_attrs`]: Self::add_attrs
231    #[derive(Serialize, Deserialize, Debug, Clone, Default)]
232    #[serde(deny_unknown_fields)]
233    pub struct EnterReq {
234        /// [`Attr`]ibute identifying the user.
235        ///
236        /// If omitted, an `AuthToken` must be passed via the `Authorization` header instead.
237        ///
238        /// [`Attr`]: attr::Attr
239        #[serde(default)]
240        #[serde(skip_serializing_if = "Option::is_none")]
241        pub identifying_attr: Option<Signed<attr::Attr>>,
242
243        /// The mode determines whether we want to create an account if none exists,
244        /// and whether we expect an account to exist.
245        #[serde(default)]
246        pub mode: EnterMode,
247
248        /// Add these attributes to your account, required, for example, when registering a new
249        /// account, or when no bannable attribute is registered for this account.
250        #[serde(default)]
251        #[serde(skip_serializing_if = "Vec::is_empty")]
252        pub add_attrs: Vec<Signed<attr::Attr>>,
253
254        /// When the registration of a new user account is needed for this request, check that none
255        /// of the provided attributes already bans another user.  If one of the supplied
256        /// attributes does ban another user, [`EnterResp::AttributeAlreadyTaken`] is returned.
257        ///
258        /// Checking for this condition is useful when an end-user already has an account, supplied
259        /// one attribute that bans it, but not an identifying attribute tied to their original
260        /// account.  If this check is not performed, a second account is created, which is not
261        /// what the user might want.  With this check, the frontend can prompt the user to confirm
262        /// that they really do want to create a (potential) second account.
263        #[serde(default)]
264        #[serde(skip_serializing_if = "std::ops::Not::not")]
265        pub register_only_with_unique_attrs: bool,
266    }
267
268    /// Returned by [`EnterEP`].
269    #[derive(Serialize, Deserialize, Debug, Clone)]
270    #[serde(deny_unknown_fields)]
271    #[serde(rename = "snake_case")]
272    #[must_use]
273    pub enum EnterResp {
274        /// Happens only in [`EnterMode::Login`]
275        AccountDoesNotExist,
276
277        /// This attribute is banned and therefore cannot be used.
278        AttributeBanned(attr::Attr),
279
280        /// Cannot login, because this account is banned.
281        Banned,
282
283        /// The given identifying attribute (in [`EnterReq::add_attrs`] or [`EnterReq::identifying_attr`])
284        /// is already tied to another account.
285        ///
286        /// If [`EnterReq::register_only_with_unique_attrs`] is set, this variant will also be returned if
287        /// a registration is attempted, but one of the supplied attributes already bans another
288        /// user.
289        ///
290        /// May occasionally happen under the [`EnterMode::LoginOrRegister`] mode if the account
291        /// was created by some parallel invocation of [`EnterEP`] at about the same time.
292        AttributeAlreadyTaken {
293            #[serde(flatten)]
294            attr: attr::Attr,
295
296            /// Set if this attribute was taken in the sense that it already bans another user.
297            #[serde(default)]
298            #[serde(skip_serializing_if = "std::ops::Not::not")]
299            bans_other_user: bool,
300        },
301
302        /// Cannot register an account with these attributes:  no bannable attribute provided.
303        NoBannableAttribute,
304
305        /// Signature on identifying attribute is invalid or expired; please reobtain the
306        /// identifying attribute and retry.  If this fails even with a fresh attribute something
307        /// is wrong with the server.
308        RetryWithNewIdentifyingAttr,
309
310        /// An authtoken was passed via the Authorization header that is expired or otherwise invalid.  
311        /// Obtain a new one and retry.
312        RetryWithNewAuthToken,
313
314        /// Signature on [`EnterReq::add_attrs`]  attribute is invalid or expired; please reobtain the
315        /// attribute and retry.  If this fails even with a fresh attribute something
316        /// is wrong with the server.
317        RetryWithNewAddAttr {
318            /// `add_attrs[index]` is the offending attribute
319            index: usize,
320        },
321
322        /// The given identifying attribute (now) grants access to a pubhubs account.
323        Entered {
324            /// Whether we created a new account
325            new_account: bool,
326
327            /// An access token identifying the user towards pubhubs central.
328            ///
329            /// May not be provided, for example, when the user is banned, or if no bannable
330            /// attribute is currently associated to the user's account.
331            auth_token_package: std::result::Result<AuthTokenPackage, AuthTokenDeniedReason>,
332
333            attr_status: Vec<(attr::Attr, AttrAddStatus)>,
334        },
335    }
336
337    /// Why no auth token was granted
338    #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
339    #[serde(deny_unknown_fields)]
340    pub enum AuthTokenDeniedReason {
341        /// No bannable attribute associated to account.
342        ///
343        /// May happen when a bannable attribute was provided in the [`EnterReq`], but adding this
344        /// attribute failed for some reason.  Just try to add the bannable attribute again.
345        NoBannableAttribute,
346
347        /// This account is banned.  Only returned in [`RefreshResp`] (since [`EnterResp`] has
348        /// [`EnterResp::Banned`]).
349        Banned,
350    }
351
352    /// Whether to login, register, or both.
353    #[derive(Default, Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
354    #[serde(deny_unknown_fields)]
355    pub enum EnterMode {
356        /// Log in to an existing account
357        #[default]
358        Login,
359
360        /// Register a new account
361        Register,
362
363        /// Log in to an existing account, or register one first if needed
364        LoginOrRegister,
365    }
366
367    /// Result of trying to add an attribute via [`EnterEP`].
368    #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
369    #[serde(deny_unknown_fields)]
370    #[serde(rename = "snake_case")]
371    pub enum AttrAddStatus {
372        /// Did nothing - the attribute was already there
373        AlreadyThere,
374
375        /// The attribute was added
376        Added,
377
378        /// Adding this attribute (partially) failed.
379        PleaseTryAgain,
380    }
381
382    /// Refresh authentication token.  Requires authentication, but the access token used to
383    /// authenticate may be expired.
384    pub struct RefreshEP {}
385    impl EndpointDetails for RefreshEP {
386        type RequestType = NoPayload;
387        type ResponseType = Result<RefreshResp>;
388
389        const METHOD: http::Method = http::Method::GET;
390        const PATH: &'static str = ".ph/user/refresh";
391    }
392
393    /// Returned by [`RefreshEP`].
394    #[derive(Serialize, Deserialize, Debug, Clone)]
395    #[serde(deny_unknown_fields)]
396    #[serde(rename = "snake_case")]
397    #[must_use]
398    pub enum RefreshResp {
399        /// Something is wrong with the provided auth token.  Please obtain a new one via the
400        /// [`EnterEP`].
401        ReobtainAuthToken,
402
403        /// Cannot issue authentication token for the given [`AuthTokenDeniedReason`]
404        Denied(AuthTokenDeniedReason),
405
406        /// The refreshed authentication token
407        Success(AuthTokenPackage),
408    }
409
410    /// An [`AuthToken`] with some additional information.
411    #[derive(Serialize, Deserialize, Debug, Clone)]
412    #[serde(deny_unknown_fields)]
413    pub struct AuthTokenPackage {
414        /// The actual authentication token
415        pub auth_token: AuthToken,
416
417        /// When [`Self::auth_token`] expires
418        pub expires: NumericDate,
419    }
420
421    /// An opaque token used to identify the user towards pubhubs central via the
422    /// `Authorization` header.  The token can be obtained via the [`EnterEP`],
423    /// and be refreshed via the [`RefreshEP`].
424    #[derive(Serialize, Deserialize, Debug, Clone)]
425    #[serde(transparent)]
426    pub struct AuthToken {
427        pub(crate) inner: B64UU,
428    }
429
430    impl std::fmt::Display for AuthToken {
431        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
432            write!(f, "{}", self.inner)
433        }
434    }
435
436    /// So [`AuthToken`] can be used as the value of a [`clap`] flag.
437    impl std::str::FromStr for AuthToken {
438        type Err = <B64UU as std::str::FromStr>::Err;
439
440        fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
441            Ok(Self {
442                inner: B64UU::from_str(s)?,
443            })
444        }
445    }
446
447    impl header::TryIntoHeaderValue for AuthToken {
448        type Error = std::convert::Infallible;
449
450        fn try_into_value(self) -> std::result::Result<header::HeaderValue, Self::Error> {
451            let vec: Vec<u8> = self.inner.to_string().into_bytes();
452
453            Ok(header::HeaderValue::try_from(vec).unwrap())
454        }
455    }
456
457    impl header::Header for AuthToken {
458        fn name() -> header::HeaderName {
459            header::AUTHORIZATION
460        }
461
462        fn parse<M: actix_web::HttpMessage>(
463            msg: &M,
464        ) -> std::result::Result<Self, actix_web::error::ParseError> {
465            Ok(AuthToken {
466                inner: header::from_one_raw_str(msg.headers().get(Self::name()))?,
467            })
468        }
469    }
470
471    /// Get state of the current user
472    pub struct StateEP {}
473    impl EndpointDetails for StateEP {
474        type RequestType = NoPayload;
475        type ResponseType = Result<StateResp>;
476
477        const METHOD: http::Method = http::Method::GET;
478        const PATH: &'static str = ".ph/user/state";
479    }
480
481    /// Result of retrieving a user's state
482    #[derive(Serialize, Deserialize, Debug, Clone)]
483    #[serde(deny_unknown_fields)]
484    #[serde(rename = "snake_case")]
485    #[must_use]
486    pub enum StateResp {
487        /// The auth provided is expired or otherwise invalid.  Obtain a new one and retry.
488        RetryWithNewAuthToken,
489
490        /// Retrieval of [`UserState`] was successful
491        State(UserState),
492    }
493
494    /// State of a user's account at pubhubs as shown to the user.
495    #[derive(Serialize, Deserialize, Debug, Clone)]
496    #[serde(deny_unknown_fields)]
497    pub struct UserState {
498        /// Attributes that may be used to log in as this user.
499        pub allow_login_by: HashSet<Id>,
500
501        /// Attributes that when banned ban this user.
502        pub could_be_banned_by: HashSet<Id>,
503
504        /// Objects stored for this user
505        pub stored_objects: HashMap<handle::Handle, UserObjectDetails>,
506        // TODO: add information on Quota
507    }
508
509    /// Details on an object stored at pubhubs central for a user.
510    #[derive(Serialize, Deserialize, Debug, Clone)]
511    #[serde(deny_unknown_fields)]
512    pub struct UserObjectDetails {
513        /// Identifier for this object - does not change
514        pub hash: Id,
515
516        /// Needs to be provided to the [`GetObjectEP`] when retrieving this object.  May change.
517        pub hmac: Id,
518
519        /// Size of the object in bytes
520        pub size: u32,
521    }
522
523    /// Retrieves a user object with the given `hash` from PubHubs central
524    ///
525    /// Authorization happens not via an access token, but using the [`UserObjectDetails::hmac`].
526    /// This allows HTTP caching without leaking the access token to the cache.
527    pub struct GetObjectEP {}
528    impl EndpointDetails for GetObjectEP {
529        type RequestType = NoPayload;
530
531        /// Generally the API endpoints return `application/json` encoding `Result<ResponseType>`,
532        /// but this endpoint is different.  It returns either an `application/json` encoding an
533        /// `Result<GetObjectResp>` (when there's a problem) or an `application/octet-stream` containing just `bytes::Bytes`.
534        type ResponseType = Payload<Result<GetObjectResp>>;
535
536        const METHOD: http::Method = http::Method::GET;
537        const PATH: &'static str = ".ph/user/obj/by-hash/{hash}/{hmac}";
538
539        /// Responses should be cached indefinitely
540        fn immutable_response() -> bool {
541            true
542        }
543    }
544
545    /// Returned by [`GetObjectEP`] when there's a problem.  When there's no problem an octet
546    /// stream is returned instead.
547    #[derive(Serialize, Deserialize, Debug, Clone)]
548    #[serde(deny_unknown_fields)]
549    #[serde(rename = "snake_case")]
550    #[must_use]
551    pub enum GetObjectResp {
552        /// The `hmac` you sent is invalid, probably because it is outdated.
553        ///
554        /// Please retry after obtaining the current `hmac` from [`StateEP`].
555        RetryWithNewHmac,
556
557        /// The `hmac` was correct, so the object you requested probably did exist at one point,
558        /// but it does not longer.  Please reload the list of stored objects via [`StateEP`].
559        NotFound,
560    }
561
562    /// Stores a new object at pubhubs central, under the given `handle`.
563    pub struct NewObjectEP {}
564    impl EndpointDetails for NewObjectEP {
565        type RequestType = BytesPayload;
566        type ResponseType = Result<StoreObjectResp>;
567
568        const METHOD: http::Method = http::Method::POST;
569        const PATH: &'static str = ".ph/user/obj/by-handle/{handle}";
570    }
571
572    /// Stores an object at pubhubs central under the given `handle`, overwriting the previous
573    /// object stored there.
574    pub struct OverwriteObjectEP {}
575    impl EndpointDetails for OverwriteObjectEP {
576        type RequestType = BytesPayload;
577        type ResponseType = Result<StoreObjectResp>;
578
579        const METHOD: http::Method = http::Method::POST;
580        const PATH: &'static str = ".ph/user/obj/by-hash/{handle}/{overwrite_hash}";
581    }
582
583    /// Returned by [`NewObjectEP`] and [`OverwriteObjectEP`].
584    #[derive(Serialize, Deserialize, Debug, Clone)]
585    #[serde(deny_unknown_fields)]
586    #[serde(rename = "snake_case")]
587    #[must_use]
588    pub enum StoreObjectResp {
589        /// Please retry the same request again.  This may happen when another call changed the
590        /// user's state. The purpose of letting the client make the same call again (instead of
591        /// letting the server retry) is that the client gets feedback about this.
592        PleaseRetry,
593
594        /// The auth provided is expired or otherwise invalid.  Obtain a new one and retry.
595        RetryWithNewAuthToken,
596
597        /// Returned when using [`NewObjectEP`], but there is already an object stored under that handle.  
598        /// To make sure that you're not overriding recent changes made by another global client,
599        /// you must pass the hash of the object you want to overwrite by using the
600        /// [`OverwriteObjectEP`] instead.
601        MissingHash,
602
603        /// Returned when [`OverwriteObjectEP`] is used, but there is no (longer) an object
604        /// stored under that handle.  Use [`NewObjectEP`] to create a new one.
605        NotFound,
606
607        /// Returned when using [`OverwriteObjectEP`] but the object stored at that handle
608        /// has a different hash, presumably because it has been changed in the meantime by another
609        /// global client.
610        HashDidNotMatch,
611
612        /// The object that you sent did not differ from the object already stored.  Doing this
613        /// should be avoided.
614        NoChanges,
615
616        /// Cannot perform this request, because the user has (or would have) reached the named
617        /// quotum.
618        ///
619        /// This should only happen when the user is trying to abuse PubHubs central as object
620        /// store, or when the global client is storing more than it should.
621        QuotumReached(QuotumName),
622
623        /// The object was stored succesfully. The user objects that are currently stored for this user are returned.
624        Stored {
625            stored_objects: HashMap<handle::Handle, UserObjectDetails>,
626        },
627    }
628
629    /// Quota for a user
630    #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
631    #[serde(deny_unknown_fields)]
632    pub struct Quota {
633        /// Total number of objects allowed for a user
634        pub object_count: u16,
635
636        /// The sum total of all bytes of all objects of a user cannot exceed this
637        pub object_bytes_total: u32,
638    }
639
640    impl Default for Quota {
641        fn default() -> Self {
642            Self {
643                object_count: 5,
644                object_bytes_total: 1024 * 1024, // 1 mb
645            }
646        }
647    }
648
649    /// The different quota used in [`Quota`].
650    #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
651    #[serde(deny_unknown_fields)]
652    #[serde(rename_all = "snake_case")]
653    pub enum QuotumName {
654        ObjectCount,
655        ObjectBytesTotal,
656    }
657
658    impl std::fmt::Display for QuotumName {
659        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
660            self.serialize(f)
661        }
662    }
663
664    /// Requests an [`sso::PolymorphicPseudonymPackage`].  Requires authentication.
665    pub struct PppEP {}
666    impl EndpointDetails for PppEP {
667        type RequestType = NoPayload;
668        type ResponseType = Result<PppResp>;
669
670        const METHOD: http::Method = http::Method::POST;
671        const PATH: &'static str = ".ph/user/ppp";
672    }
673
674    /// Returned by [`PppEP`].
675    #[derive(Serialize, Deserialize, Debug, Clone)]
676    #[serde(deny_unknown_fields)]
677    #[serde(rename = "snake_case")]
678    #[must_use]
679    pub enum PppResp {
680        /// The auth provided is expired or otherwise invalid.  Obtain a new one and retry.
681        RetryWithNewAuthToken,
682
683        /// The requested polymorphic pseudonym package (PPP).  Must be used only once lest the
684        /// transcryptor can track the user by the PPP used.
685        Success(Sealed<sso::PolymorphicPseudonymPackage>),
686    }
687
688    /// Type of [`sso::PolymorphicPseudonymPackage::nonce`]
689    #[derive(Serialize, Deserialize, Debug, Clone)]
690    #[serde(transparent)]
691    pub struct PpNonce {
692        pub(crate) inner: B64UU,
693    }
694
695    /// Requests an [`sso::HashedHubPseudonymPackage`].  Requires authentication.
696    pub struct HhppEP {}
697    impl EndpointDetails for HhppEP {
698        type RequestType = HhppReq;
699        type ResponseType = Result<HhppResp>;
700
701        const METHOD: http::Method = http::Method::POST;
702        const PATH: &'static str = ".ph/user/hhpp";
703    }
704
705    /// Request type for [`HhppEP`]
706    #[derive(Serialize, Deserialize, Debug, Clone)]
707    #[serde(deny_unknown_fields)]
708    #[serde(rename = "snake_case")]
709    pub struct HhppReq {
710        /// The encrypted pseudonym to hash. Can be obtained from [`tr::EhppEP`].
711        pub ehpp: Sealed<sso::EncryptedHubPseudonymPackage>,
712
713        /// The scheme to sign the resulting HHPP with, relayed by the global client from
714        /// [`EnterStartResp::hhpp_signature_scheme`](crate::api::hub::EnterStartResp::hhpp_signature_scheme).
715        /// Absent ⇒ [`Ed25519`](sso::HhppSignatureScheme::Ed25519).
716        #[serde(default, skip_serializing_if = "sso::HhppSignatureScheme::is_default")]
717        pub hhpp_signature_scheme: sso::HhppSignatureScheme,
718    }
719
720    /// Returned by [`HhppEP`].
721    #[derive(Serialize, Deserialize, Debug, Clone)]
722    #[serde(deny_unknown_fields)]
723    #[serde(rename = "snake_case")]
724    #[must_use]
725    pub enum HhppResp {
726        /// There's something wrong with the [`sso::EncryptedHubPseudonymPackage`].
727        /// You probably want to start at [`PppEP`] again.
728        RetryWithNewPpp,
729
730        /// The auth provided is expired or otherwise invalid.  Obtain a new one and retry.
731        RetryWithNewAuthToken,
732
733        /// The requested hashed hub pseudonym package (HHPP).  
734        Success(Signed<sso::HashedHubPseudonymPackage>),
735    }
736
737    /// A registration pseudonym used on pubhubs cards
738    #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
739    #[serde(transparent)]
740    pub struct CardPseud(pub Id);
741
742    impl std::fmt::Display for CardPseud {
743        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
744            self.0.fmt(f)
745        }
746    }
747
748    /// A registration pseudonym coupled with the registration date.
749    #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
750    pub struct CardPseudPackage {
751        pub card_pseud: CardPseud,
752
753        /// Registration date for this user.  Can be `None` for users that registered under v3.0.0.
754        pub registration_date: Option<NumericDate>,
755    }
756
757    having_message_code!(CardPseudPackage, CardPseudPackage);
758
759    /// Requests the 'registration pseudonym' used on PubHubs cards issued for this account.
760    /// Requires authentication.
761    pub struct CardPseudEP {}
762    impl EndpointDetails for CardPseudEP {
763        type RequestType = NoPayload;
764        type ResponseType = Result<CardPseudResp>;
765
766        const METHOD: http::Method = http::Method::POST;
767        const PATH: &'static str = ".ph/user/card-pseud";
768    }
769
770    /// Returned by [`CardPseudEP`].
771    #[derive(Serialize, Deserialize, Debug, Clone)]
772    #[serde(deny_unknown_fields)]
773    #[serde(rename = "snake_case")]
774    #[must_use]
775    pub enum CardPseudResp {
776        /// The auth provided is expired or otherwise invalid.  Obtain a new one and retry.
777        RetryWithNewAuthToken,
778
779        /// The requested registration pseudonym, signed by PHC's jwt key.
780        Success(Signed<CardPseudPackage>),
781    }
782
783    #[cfg(test)]
784    mod test {
785        use super::*;
786
787        #[test]
788        fn backwards_compat() {
789            let _: EnterResp = serde_json::from_value(serde_json::json!({
790                "AttributeAlreadyTaken": {
791                    "attr_type": "Fr7Gsfh73AU9k9N4eR9vDBINhMOImXm-Qqfkz0RxjwI",
792                    "value": "blurp"
793                }
794            }))
795            .unwrap();
796        }
797    }
798}