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}