Skip to main content

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}