1use 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
36pub trait TenantRegistrar: Send + Sync + 'static {
90 type Input: FormInput;
93
94 type User: PanelUser;
97
98 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 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
127pub struct TenantRegistration<U> {
160 registrar: Box<dyn DynTenantRegistrar>,
161 required: bool,
162 user: PhantomData<fn() -> U>,
163}
164
165impl<U: PanelUser> TenantRegistration<U> {
166 #[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 #[must_use]
182 pub fn required(mut self) -> Self {
183 self.required = true;
184 self
185 }
186
187 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
211pub(crate) enum Outcome {
213 Registered(Membership),
215 Refused {
217 values: FormValues,
218 errors: FieldErrors,
219 },
220}
221
222pub(crate) trait DynTenantRegistrar: Send + Sync {
224 fn schema(&self) -> Schema;
226 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
280pub(crate) const RESERVED_KEYS: [&str; 1] = [crate::csrf::FIELD_NAME];
282
283pub(crate) fn register_tenant_url(cx: &Cx) -> String {
285 format!("{}/register-tenant", panel_prefix(cx))
286}
287
288pub(crate) fn offered(cx: &Cx) -> bool {
290 current_realm(cx).is_some_and(|panel| panel.auth.tenant_registration_page().is_some())
291}
292
293pub(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
303fn 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
314pub(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
329pub(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
356async 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
425pub(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}