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