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}