tablo_core/auth/tenant_registration.rs
1//! The panel's tenant registration page: the [`TenantRegistrar`] trait that creates a tenant for
2//! the signed-in user, the [`TenantRegistration`] a panel installs it with, and the
3//! `{prefix}/register-tenant` routes.
4
5use std::{any::Any, future::Future, marker::PhantomData};
6
7use topcoat::{
8 context::Cx,
9 icon::icon,
10 router::{
11 Body, RouteFuture,
12 error::{forbidden, not_found, see_other},
13 response::{IntoResponse, Response},
14 },
15 view::{ViewExt, attributes, view},
16};
17
18use super::{
19 PanelUser, SignedIn, infrastructure_failure, input,
20 login::logout_url,
21 page::{AuthPage, render, switch_link},
22 panel_root,
23 session::select_tenant,
24 signed, unauthenticated_error,
25};
26use crate::{
27 BoxFuture,
28 auth::realm::{Realm, current_realm, hosted, panel_prefix},
29 flash::set_notification,
30 form::{FieldErrors, FormInput, FormValues},
31 outcome::Notification,
32 schema::{Schema, Source},
33 tenancy::Membership,
34};
35
36/// How a panel lets the signed-in user create a tenant to act for, installed with a
37/// [`TenantRegistration`].
38///
39/// It mirrors a [`Registrar`](super::Registrar): [`Input`](Self::Input) is the form the page asks
40/// for, [`validate`](Self::validate) refuses a submission field by field, and
41/// [`register`](Self::register) writes the tenant and the user's membership of it. Both run in
42/// one transaction the framework opens. The next request reloads the user, whose
43/// [`tenants`](PanelUser::tenants) must then list the new one.
44///
45/// ```rust
46/// # use tablo_core::{Membership, PanelUser, auth::TenantRegistrar};
47/// # use topcoat::context::Cx;
48/// # #[derive(Debug, Clone, toasty::Model)]
49/// # struct Team { #[key] #[auto] id: uuid::Uuid, name: String }
50/// # #[derive(Debug, Clone, toasty::Model)]
51/// # struct Seat { #[key] #[auto] id: uuid::Uuid, staff_id: uuid::Uuid, team_id: uuid::Uuid }
52/// # struct Staff { id: uuid::Uuid, teams: Vec<Membership> }
53/// # impl PanelUser for Staff {
54/// # fn user_id(&self) -> String { self.id.to_string() }
55/// # fn display_name(&self) -> &str { "" }
56/// # fn can_access_panel(&self, _panel: &str) -> bool { true }
57/// # fn tenants(&self) -> &[Membership] { &self.teams }
58/// # }
59/// #[derive(tablo_core::FormInput)]
60/// struct NewTeam {
61/// name: String,
62/// }
63///
64/// struct Teams;
65///
66/// impl TenantRegistrar for Teams {
67/// type Input = NewTeam;
68/// type User = Staff;
69///
70/// async fn register(
71/// &self,
72/// _cx: &Cx,
73/// staff: &Staff,
74/// team: NewTeam,
75/// ex: &mut dyn toasty::Executor,
76/// ) -> topcoat::Result<Membership> {
77/// let team = toasty::create!(Team { name: team.name })
78/// .exec(&mut *ex)
79/// .await?;
80/// toasty::create!(Seat {
81/// staff_id: staff.id,
82/// team_id: team.id,
83/// })
84/// .exec(ex)
85/// .await?;
86/// Ok(Membership::new(team.id.into(), team.name))
87/// }
88/// }
89/// ```
90pub trait TenantRegistrar: Send + Sync + 'static {
91 /// What the page asks for: the app's own
92 /// [`#[derive(FormInput)]`](derive@crate::FormInput) struct.
93 type Input: FormInput;
94
95 /// The user who registers the tenant: the type the panel's
96 /// [`Authenticator`](super::Authenticator) loads, which mounting checks.
97 type User: PanelUser;
98
99 /// Refuse a submission [`register`](Self::register) should not receive, such as a name
100 /// another tenant holds: each error names an input field's key and renders under its
101 /// control. Defaults to none. A key no control renders is a declaration error: the page
102 /// would show no message.
103 ///
104 /// It runs inside the registration's transaction `ex`, after the input parses and its
105 /// controls' own rules pass.
106 fn validate(
107 &self,
108 _cx: &Cx,
109 _user: &Self::User,
110 _input: &Self::Input,
111 _ex: &mut dyn toasty::Executor,
112 ) -> impl Future<Output = topcoat::Result<FieldErrors>> + Send {
113 async { Ok(FieldErrors::new()) }
114 }
115
116 /// Create the tenant from a validated `input`, with `user` a member of it, through the
117 /// transaction `ex`, which commits when it returns `Ok`; answer the membership the request's
118 /// session then acts under.
119 fn register(
120 &self,
121 cx: &Cx,
122 user: &Self::User,
123 input: Self::Input,
124 ex: &mut dyn toasty::Executor,
125 ) -> impl Future<Output = topcoat::Result<Membership>> + Send;
126}
127
128/// A panel's tenant registration page, installed with
129/// [`Auth::tenant_registration`](super::Auth::tenant_registration): the [`TenantRegistrar`] that
130/// creates tenants, and whether a user who acts for none must create one first.
131///
132/// ```rust
133/// # use tablo_core::{Auth, Authenticator, Membership, PanelUser, TenantRegistration, auth::TenantRegistrar};
134/// # use topcoat::context::Cx;
135/// # struct Staff;
136/// # impl PanelUser for Staff {
137/// # fn user_id(&self) -> String { String::new() }
138/// # fn display_name(&self) -> &str { "" }
139/// # fn can_access_panel(&self, _panel: &str) -> bool { true }
140/// # }
141/// # struct StaffAuth;
142/// # impl Authenticator for StaffAuth {
143/// # type User = Staff;
144/// # async fn verify(&self, _: &Cx, _: &str, _: &str) -> topcoat::Result<Option<Staff>> { Ok(None) }
145/// # async fn find_by_id(&self, _: &Cx, _: &str) -> topcoat::Result<Option<Staff>> { Ok(None) }
146/// # }
147/// # #[derive(tablo_core::FormInput)]
148/// # struct NewTeam { name: String }
149/// # impl TenantRegistrar for StaffAuth {
150/// # type Input = NewTeam;
151/// # type User = Staff;
152/// # async fn register(&self, _: &Cx, _: &Staff, _: NewTeam, _: &mut dyn toasty::Executor)
153/// # -> topcoat::Result<Membership> { unimplemented!() }
154/// # }
155/// let auth = Auth::custom(StaffAuth).tenant_registration(TenantRegistration::new(StaffAuth).required());
156/// # let _ = auth;
157/// ```
158///
159/// `U` is the user type the registrar loads; [`Auth`](crate::Auth) takes only one over the type its
160/// authenticator loads.
161pub struct TenantRegistration<U> {
162 registrar: Box<dyn DynTenantRegistrar>,
163 required: bool,
164 user: PhantomData<fn() -> U>,
165}
166
167impl<U: PanelUser> TenantRegistration<U> {
168 /// Registers tenants through `registrar`. A user who acts for no tenant still reaches the
169 /// panel, and its resources that need a tenant answer 403; see [`required`](Self::required).
170 #[must_use]
171 pub fn new(registrar: impl TenantRegistrar<User = U>) -> Self {
172 Self {
173 registrar: Box::new(registrar),
174 required: false,
175 user: PhantomData,
176 }
177 }
178
179 /// Sends a signed-in user who acts for no tenant to the registration page before anything
180 /// else: a page answers with a redirect to it and any other request with 403, as for an
181 /// unverified address, which comes first. Leave it off where some users, such as operators,
182 /// act for no tenant on purpose.
183 #[must_use]
184 pub fn required(mut self) -> Self {
185 self.required = true;
186 self
187 }
188
189 /// The same declaration, its user type checked by [`Auth`](crate::Auth).
190 pub(crate) fn erase(self) -> TenantRegistration<()> {
191 TenantRegistration {
192 registrar: self.registrar,
193 required: self.required,
194 user: PhantomData,
195 }
196 }
197}
198
199impl TenantRegistration<()> {
200 pub(crate) fn registrar(&self) -> &dyn DynTenantRegistrar {
201 &*self.registrar
202 }
203}
204
205impl<U> std::fmt::Debug for TenantRegistration<U> {
206 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207 f.debug_struct("TenantRegistration")
208 .field("required", &self.required)
209 .finish_non_exhaustive()
210 }
211}
212
213/// What a tenant registration came to.
214pub(crate) enum Outcome {
215 /// The committed tenant, as the user's membership.
216 Registered(Membership),
217 /// The refused submission, its values, and why.
218 Refused {
219 values: FormValues,
220 errors: FieldErrors,
221 },
222}
223
224/// [`TenantRegistrar`] with its input and user types erased, as the panel keeps it.
225pub(crate) trait DynTenantRegistrar: Send + Sync {
226 /// The registration form.
227 fn schema(&self) -> Schema;
228 /// Parses, checks, validates and registers one submission for `signed`'s user.
229 fn register<'a>(
230 &'a self,
231 cx: &'a Cx,
232 signed: &'a SignedIn,
233 values: FormValues,
234 ) -> BoxFuture<'a, topcoat::Result<Outcome>>;
235}
236
237impl<R: TenantRegistrar> DynTenantRegistrar for R {
238 fn schema(&self) -> Schema {
239 R::Input::schema()
240 }
241
242 fn register<'a>(
243 &'a self,
244 cx: &'a Cx,
245 signed: &'a SignedIn,
246 values: FormValues,
247 ) -> BoxFuture<'a, topcoat::Result<Outcome>> {
248 Box::pin(async move {
249 let any: &dyn Any = &*signed.user;
250 let user = any
251 .downcast_ref::<R::User>()
252 .ok_or_else(|| topcoat::Error::from(forbidden()))?;
253 let input =
254 match input::parse::<R::Input>(cx, &values, &FormValues::new(), "tenant").await? {
255 Ok(input) => input,
256 Err(errors) => return Ok(Outcome::Refused { values, errors }),
257 };
258 let mut db = crate::db::db(cx)?;
259 let mut tx = db.transaction().await.map_err(infrastructure_failure)?;
260 let errors = self
261 .validate(cx, user, &input, &mut tx)
262 .await
263 .map_err(infrastructure_failure)?;
264 input::placed(
265 &R::Input::schema(),
266 &errors,
267 "the tenant registrar",
268 "tenant",
269 )?;
270 if !errors.is_empty() {
271 return Ok(Outcome::Refused { values, errors });
272 }
273 let membership = TenantRegistrar::register(self, cx, user, input, &mut tx)
274 .await
275 .map_err(infrastructure_failure)?;
276 tx.commit().await.map_err(infrastructure_failure)?;
277 Ok(Outcome::Registered(membership))
278 })
279 }
280}
281
282/// The keys a tenant registration POST carries besides its input, which no input field may post.
283pub(crate) const RESERVED_KEYS: [&str; 1] = [crate::csrf::FIELD_NAME];
284
285/// Where the panel's tenant registration page lives: `{prefix}/register-tenant`.
286pub(crate) fn register_tenant_url(cx: &Cx) -> String {
287 format!("{}/register-tenant", panel_prefix(cx))
288}
289
290/// Whether the request's panel installs a [`TenantRegistration`].
291pub(crate) fn offered(cx: &Cx) -> bool {
292 current_realm(cx).is_some_and(|panel| panel.auth.tenant_registration_page().is_some())
293}
294
295/// Whether `user` must register a tenant before `panel` serves them anything else: the panel
296/// requires one, and the user acts for none.
297pub(super) fn pending(panel: &Realm, user: &dyn PanelUser) -> bool {
298 panel
299 .auth
300 .tenant_registration_page()
301 .is_some_and(|registration| registration.required)
302 && user.tenants().is_empty()
303}
304
305/// The signed-in user and the panel's registrar, or the answer to a request without them.
306fn registering(cx: &Cx) -> topcoat::Result<(&SignedIn, &dyn DynTenantRegistrar)> {
307 let signed = signed(cx).ok_or_else(|| unauthenticated_error(cx))?;
308 let registration = signed
309 .realm
310 .auth
311 .tenant_registration_page()
312 .ok_or_else(not_found)?;
313 Ok((signed, registration.registrar()))
314}
315
316/// `GET {prefix}/register-tenant` — the standalone tenant registration page.
317pub(super) fn register_tenant_page(cx: &Cx, _body: Body) -> RouteFuture<'_> {
318 Box::pin(async move {
319 let (signed, registrar) = registering(cx)?;
320 // The page starts from each field's default, as the POST reads a disabled one.
321 let values = registrar.schema().defaults();
322 page_response(cx, signed, registrar, &values, &FieldErrors::new()).await
323 })
324}
325
326/// `POST {prefix}/register-tenant` — create the tenant, act for it, then land on the panel root.
327pub(super) fn register_tenant_post(cx: &Cx, body: Body) -> RouteFuture<'_> {
328 Box::pin(async move {
329 let (signed, registrar) = registering(cx)?;
330 let form = crate::auth::input::read_form(cx, body).await?;
331 crate::csrf::verify(cx, form.last(crate::csrf::FIELD_NAME))?;
332 let input_schema = registrar.schema();
333 let values = input_schema.read_input(cx, &form.pairs, &RESERVED_KEYS)?;
334 let membership = match registrar.register(cx, signed, values).await? {
335 Outcome::Registered(membership) => membership,
336 Outcome::Refused { values, errors } => {
337 return page_response(cx, signed, registrar, &values, &errors).await;
338 }
339 };
340 select_tenant(cx, membership.tenant).await?;
341 set_notification(
342 cx,
343 Notification::success(crate::t!(cx, "tablo-tenant-created")).description(crate::t!(
344 cx,
345 "tablo-tenant-acting",
346 tenant = membership.name.clone()
347 )),
348 );
349 see_other(panel_root(cx)).into_response(cx)
350 })
351}
352
353/// The registration page: 422 for a refused submission, else 200. A user who acts for a tenant
354/// already may go back to the panel; one who acts for none may only sign out.
355async fn page_response(
356 cx: &Cx,
357 signed: &SignedIn,
358 registrar: &dyn DynTenantRegistrar,
359 values: &FormValues,
360 errors: &FieldErrors,
361) -> topcoat::Result<Response> {
362 let csrf = crate::csrf::ensure_token(cx);
363 let action = register_tenant_url(cx);
364 let schema = registrar.schema();
365 let fields = schema.render(cx, Source::form(values, errors)).await?;
366 let first = signed.user.tenants().is_empty();
367 let logout = logout_url(cx);
368 let form = view! {
369 <div class="flex flex-col gap-3">
370 <form method="post" action=(action) class="flex flex-col gap-5">
371 (crate::csrf::field(cx, &csrf))
372 (fields)
373 tablo_ui::button(
374 variant: tablo_ui::ButtonVariant::Primary,
375 attrs: attributes! { type="submit" class="w-full" },
376 (crate::t!(cx, "tablo-shell-create-tenant"))
377 )
378 </form>
379 if first {
380 <form method="post" action=(logout)>
381 (crate::csrf::field(cx, &csrf))
382 tablo_ui::button(
383 variant: tablo_ui::ButtonVariant::Outline,
384 attrs: attributes! { type="submit" class="w-full" },
385 icon(data: tablo_ui::icons::LOG_OUT)
386 (crate::t!(cx, "tablo-shell-sign-out"))
387 )
388 </form>
389 }
390 </div>
391 }
392 .boxed();
393 let root = panel_root(cx);
394 let brand = hosted(cx)?.brand_name(cx);
395 let page = render(
396 cx,
397 AuthPage {
398 title: crate::t!(cx, "tablo-tenant-create-title"),
399 description: if first {
400 crate::t!(cx, "tablo-tenant-create-first", brand = brand)
401 } else {
402 crate::t!(cx, "tablo-tenant-create-another", brand = brand)
403 },
404 status: (!errors.is_empty()).then_some(http::StatusCode::UNPROCESSABLE_ENTITY),
405 form,
406 switch: (!first).then(|| {
407 switch_link(
408 cx,
409 "tablo-tenant-changed-mind",
410 &root,
411 "tablo-tenant-go-back",
412 )
413 }),
414 note: None,
415 },
416 )
417 .await?;
418 page.single().await?.into_response(cx)
419}
420
421/// The mistakes in a panel's [`Auth::tenant_registration`](super::Auth::tenant_registration): a
422/// registration without auth, a registrar whose user is not the authenticator's, and a form field
423/// the page cannot serve.
424pub(super) fn declaration_errors(auth: &super::AuthConfig) -> Vec<crate::DeclarationErrorKind> {
425 use super::input::FieldFault;
426 use crate::{DeclarationErrorKind, declaration::TenantRegistrationFault as Fault};
427
428 let Some(registrar) = auth
429 .tenant_registration
430 .as_ref()
431 .map(TenantRegistration::registrar)
432 else {
433 return Vec::new();
434 };
435 if auth.authenticator().is_none() {
436 return vec![DeclarationErrorKind::TenantRegistration(
437 Fault::AuthDisabled,
438 )];
439 }
440 let mut kinds = Vec::new();
441 let schema = registrar.schema();
442 kinds.extend(schema.declaration_errors());
443 kinds.extend(schema.empty_choices());
444 kinds.extend(
445 input::field_faults(&schema, &RESERVED_KEYS).map(|(fault, name)| {
446 DeclarationErrorKind::TenantRegistration(match fault {
447 FieldFault::Reserved => Fault::ReservedField(name),
448 FieldFault::File => Fault::FileField(name),
449 FieldFault::Relationship => Fault::RelationshipField(name),
450 })
451 }),
452 );
453 kinds
454}