Skip to main content

watchword_core/
request.rs

1//! [`AuthRequest`] and [`AuthResponse`]: every operation of watchword's
2//! service as one value, whichever transport carried it.
3
4use alloc::string::String;
5use alloc::vec::Vec;
6
7use crate::{
8    AuthConfig, CloudflareConfig, CreatedUser, Link, LoginRequest, NewUser, PasswordChange,
9    RegisterInfo, RegisterRequest, UserChange, UserEntry, UserId, UserInfo,
10};
11
12/// One operation of watchword's service, as the server's `Auth::dispatch`
13/// runs it and a frontend's transport carries it.
14///
15/// This is plumbing, not a wire format: it has no serde or musli
16/// derives. The HTTP routes keep their paths and JSON bodies, and the
17/// musli-web endpoints their own request types; each transport decodes its
18/// request into an `AuthRequest`, has it dispatched, and encodes the
19/// [`AuthResponse`] as that route or endpoint answers. Since every
20/// transport runs the same dispatch, an operation behaves the same over each.
21///
22/// The operations that set, replace or clear the session cookie say so
23/// with [`cookie`](AuthRequest::cookie). Only an HTTP response can carry the
24/// cookie, so those go over HTTP.
25///
26/// New operations are added as new variants, so a `match` on it needs a
27/// wildcard arm.
28#[derive(Debug, Clone, PartialEq, Eq)]
29#[non_exhaustive]
30pub enum AuthRequest {
31    /// The sign-in methods on offer, [`AuthConfig`]. Anyone. `GET
32    /// /api/auth/config`.
33    Config,
34    /// The signed-in user, with [`UserInfo::impersonated_by`] when an admin
35    /// is acting as them. Signed in. `GET /api/auth/me`, the musli-web
36    /// endpoint `Me`.
37    Me,
38    /// Signs in with a password. Anyone; throttled by the client's address.
39    /// Sets the cookie. `POST /api/auth/login`.
40    Login(LoginRequest),
41    /// Signs in the user the request's Cloudflare Access credentials vouch
42    /// for. Anyone. Sets the cookie. `POST /api/auth/cloudflare`.
43    CloudflareSignIn,
44    /// Ends the session. Anyone. Clears the cookie. `POST /api/auth/logout`.
45    Logout,
46    /// What a registration or reset link is for. Anyone. `GET
47    /// /api/auth/register/{token}`.
48    RegisterInfo {
49        /// The link's token.
50        token: String,
51    },
52    /// Registers, or resets the password, with a link. Anyone. Sets the
53    /// cookie. `POST /api/auth/register/{token}`.
54    Register {
55        /// The link's token.
56        token: String,
57        /// The credentials to register with.
58        request: RegisterRequest,
59    },
60    /// Sets or changes the signed-in user's password. Signed in. `POST
61    /// /api/auth/account/password`, the musli-web endpoint `ChangePassword`.
62    ChangePassword(PasswordChange),
63    /// Changes the signed-in user's own login or email, not their role.
64    /// Signed in. `PATCH /api/auth/account`, the musli-web endpoint
65    /// `UpdateAccount`.
66    UpdateAccount(UserChange),
67    /// Every user. Admins. `GET /api/auth/users`, the musli-web endpoint
68    /// `Users`.
69    Users,
70    /// Creates a user and their registration link. Admins. `POST
71    /// /api/auth/users`, the musli-web endpoint `CreateUser`.
72    CreateUser(NewUser),
73    /// Changes another user. Admins. `PATCH /api/auth/users/{id}`, the
74    /// musli-web endpoint `UpdateUser`.
75    UpdateUser {
76        /// The user to change.
77        id: UserId,
78        /// The change.
79        change: UserChange,
80    },
81    /// Deletes another user. Admins. `DELETE /api/auth/users/{id}`, the
82    /// musli-web endpoint `DeleteUser`.
83    DeleteUser {
84        /// The user.
85        id: UserId,
86    },
87    /// Issues a registration or reset link for a user. Admins. `POST
88    /// /api/auth/users/{id}/link`, the musli-web endpoint `IssueLink`.
89    IssueLink {
90        /// The user.
91        id: UserId,
92    },
93    /// Uses up a user's links. Admins. `DELETE /api/auth/users/{id}/link`,
94    /// the musli-web endpoint `RevokeLink`.
95    RevokeLink {
96        /// The user.
97        id: UserId,
98    },
99    /// Ends every session of a user. Admins. `POST
100    /// /api/auth/users/{id}/sign-out`, the musli-web endpoint `SignOutUser`.
101    SignOutUser {
102        /// The user.
103        id: UserId,
104    },
105    /// Revokes another user's access. Admins. `POST
106    /// /api/auth/users/{id}/revoke`, the musli-web endpoint `RevokeAccess`.
107    RevokeAccess {
108        /// The user.
109        id: UserId,
110    },
111    /// Signs in as another user, in a session marked with the admin. Admins,
112    /// if impersonation is enabled. Replaces the cookie. `POST
113    /// /api/auth/users/{id}/impersonate`.
114    Impersonate {
115        /// The user to act as.
116        id: UserId,
117    },
118    /// Ends an impersonated session, signing the admin back in. Signed in,
119    /// impersonated. Replaces the cookie. `POST /api/auth/impersonate/stop`.
120    StopImpersonating,
121    /// The Cloudflare Access settings. Admins. `GET /api/auth/cloudflare`,
122    /// the musli-web endpoint `Cloudflare`.
123    Cloudflare,
124    /// Stores and applies the Cloudflare Access settings. Admins. `PUT
125    /// /api/auth/cloudflare`, the musli-web endpoint `SetCloudflare`.
126    SetCloudflare(CloudflareConfig),
127}
128
129impl AuthRequest {
130    /// What the operation does to the session cookie, or `None` when it
131    /// leaves it alone.
132    ///
133    /// Only an HTTP response can set or clear the `HttpOnly` cookie, so an
134    /// operation with a [`CookieChange`] must go over HTTP; the server's
135    /// socket adapter has no endpoint for one. One that
136    /// [replaces](CookieChange::Replace) the cookie also leaves a socket
137    /// opened with the earlier session speaking for that session.
138    pub fn cookie(&self) -> Option<CookieChange> {
139        match self {
140            AuthRequest::Login(..)
141            | AuthRequest::CloudflareSignIn
142            | AuthRequest::Register { .. } => Some(CookieChange::Set),
143            AuthRequest::Impersonate { .. } | AuthRequest::StopImpersonating => {
144                Some(CookieChange::Replace)
145            }
146            AuthRequest::Logout => Some(CookieChange::Clear),
147            AuthRequest::Config
148            | AuthRequest::Me
149            | AuthRequest::RegisterInfo { .. }
150            | AuthRequest::ChangePassword(..)
151            | AuthRequest::UpdateAccount(..)
152            | AuthRequest::Users
153            | AuthRequest::CreateUser(..)
154            | AuthRequest::UpdateUser { .. }
155            | AuthRequest::DeleteUser { .. }
156            | AuthRequest::IssueLink { .. }
157            | AuthRequest::RevokeLink { .. }
158            | AuthRequest::SignOutUser { .. }
159            | AuthRequest::RevokeAccess { .. }
160            | AuthRequest::Cloudflare
161            | AuthRequest::SetCloudflare(..) => None,
162        }
163    }
164}
165
166/// What an [`AuthRequest`] does to the session cookie; see
167/// [`AuthRequest::cookie`].
168#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
169pub enum CookieChange {
170    /// Signs in, setting the cookie to a new session.
171    Set,
172    /// Replaces the signed-in session with a new one, as impersonating and
173    /// stopping do.
174    Replace,
175    /// Ends the session and clears the cookie.
176    Clear,
177}
178
179/// The answer to an [`AuthRequest`], one variant per type of answer.
180///
181/// Like the request, this is plumbing: each transport answers with the
182/// value a variant carries, as that route or endpoint has always answered.
183/// With the `axum08` feature it is an `IntoResponse`, and with `musliweb05`
184/// it encodes as the value it carries, the response of the musli-web
185/// endpoint for its request.
186#[derive(Debug, Clone, PartialEq, Eq)]
187#[non_exhaustive]
188pub enum AuthResponse {
189    /// The answer to [`AuthRequest::Config`].
190    Config(AuthConfig),
191    /// A user: the signed-in one for [`Me`](AuthRequest::Me), the account
192    /// operations and the sign-ins, or the one an admin operation changed.
193    User(UserInfo),
194    /// The answer to [`AuthRequest::Users`].
195    Users(Vec<UserEntry>),
196    /// The answer to [`AuthRequest::CreateUser`].
197    CreatedUser(CreatedUser),
198    /// The answer to [`AuthRequest::IssueLink`].
199    Link(Link),
200    /// The answer to [`AuthRequest::RegisterInfo`].
201    RegisterInfo(RegisterInfo),
202    /// The Cloudflare Access settings, the answer to
203    /// [`Cloudflare`](AuthRequest::Cloudflare) and
204    /// [`SetCloudflare`](AuthRequest::SetCloudflare).
205    Cloudflare(CloudflareConfig),
206    /// Done, with nothing to answer: HTTP's 204, musli-web's `()`.
207    Done,
208}
209
210#[cfg(feature = "musliweb05")]
211mod musli {
212    use alloc::vec::Vec;
213
214    use musli_core02::en::{Encode, Encoder};
215
216    use super::AuthResponse;
217    use crate::{
218        AuthConfig, CloudflareConfig, CreatedUser, Link, RegisterInfo, UserEntry, UserInfo,
219    };
220
221    /// Encodes as the value the variant carries, `()` for
222    /// [`Done`](AuthResponse::Done): the response of the musli-web endpoint
223    /// that answers the request. There is no `Decode`; a client decodes the
224    /// endpoint's own response type.
225    #[cfg_attr(watchword_docsrs, doc(cfg(feature = "musliweb05")))]
226    impl<M> Encode<M> for AuthResponse
227    where
228        M: 'static,
229        AuthConfig: Encode<M>,
230        UserInfo: Encode<M>,
231        Vec<UserEntry>: Encode<M>,
232        CreatedUser: Encode<M>,
233        Link: Encode<M>,
234        RegisterInfo: Encode<M>,
235        CloudflareConfig: Encode<M>,
236        (): Encode<M>,
237    {
238        type Encode = Self;
239
240        fn encode<E>(&self, encoder: E) -> Result<(), E::Error>
241        where
242            E: Encoder<Mode = M>,
243        {
244            match self {
245                AuthResponse::Config(value) => value.encode(encoder),
246                AuthResponse::User(value) => value.encode(encoder),
247                AuthResponse::Users(value) => value.encode(encoder),
248                AuthResponse::CreatedUser(value) => value.encode(encoder),
249                AuthResponse::Link(value) => value.encode(encoder),
250                AuthResponse::RegisterInfo(value) => value.encode(encoder),
251                AuthResponse::Cloudflare(value) => value.encode(encoder),
252                AuthResponse::Done => ().encode(encoder),
253            }
254        }
255
256        fn as_encode(&self) -> &Self::Encode {
257            self
258        }
259    }
260}