use std::{any::Any, future::Future};
use topcoat::{
context::Cx,
router::{
Body,
error::{forbidden, not_found, see_other},
},
view::BoxView,
};
use super::{PanelUser, SignedIn, input, membership, signed, unauthenticated_error};
use crate::{
BoxFuture,
auth::realm::{SettingsPage, current_realm, hosted, panel_prefix},
flash::set_notification,
form::{FieldErrors, FormInput, FormValues},
outcome::Notification,
schema::Schema,
tenancy::Membership,
topcoat_compat::async_page,
};
pub trait TenantEditor: Send + Sync + 'static {
type Input: FormInput;
type User: PanelUser;
fn may_edit(&self, _cx: &Cx, _user: &Self::User, _tenant: &Membership) -> bool {
true
}
fn fill(
&self,
cx: &Cx,
user: &Self::User,
tenant: &Membership,
) -> impl Future<Output = topcoat::Result<Self::Input>> + Send;
fn validate(
&self,
_cx: &Cx,
_user: &Self::User,
_tenant: &Membership,
_input: &Self::Input,
_ex: &mut dyn toasty::Executor,
) -> impl Future<Output = topcoat::Result<FieldErrors>> + Send {
async { Ok(FieldErrors::new()) }
}
fn save(
&self,
cx: &Cx,
user: &Self::User,
tenant: &Membership,
input: Self::Input,
ex: &mut dyn toasty::Executor,
) -> impl Future<Output = topcoat::Result<()>> + Send;
}
pub(crate) enum Outcome {
Saved,
Refused {
values: FormValues,
errors: FieldErrors,
},
}
pub(crate) trait DynTenantEditor: Send + Sync {
fn schema(&self) -> Schema;
fn may_edit(&self, cx: &Cx, user: &dyn PanelUser, tenant: &Membership) -> bool;
fn values<'a>(
&'a self,
cx: &'a Cx,
user: &'a dyn PanelUser,
tenant: &'a Membership,
) -> BoxFuture<'a, topcoat::Result<Option<FormValues>>>;
fn submit<'a>(
&'a self,
cx: &'a Cx,
signed: &'a SignedIn,
tenant: &'a Membership,
values: FormValues,
) -> BoxFuture<'a, topcoat::Result<Outcome>>;
}
impl<E: TenantEditor> DynTenantEditor for E {
fn schema(&self) -> Schema {
E::Input::schema()
}
fn may_edit(&self, cx: &Cx, user: &dyn PanelUser, tenant: &Membership) -> bool {
let user: &dyn Any = user;
user.downcast_ref::<E::User>()
.is_some_and(|user| TenantEditor::may_edit(self, cx, user, tenant))
}
fn values<'a>(
&'a self,
cx: &'a Cx,
user: &'a dyn PanelUser,
tenant: &'a Membership,
) -> BoxFuture<'a, topcoat::Result<Option<FormValues>>> {
Box::pin(async move {
let user: &dyn Any = user;
let Some(user) = user.downcast_ref::<E::User>() else {
return Ok(None);
};
let mut values = FormValues::new();
self.fill(cx, user, tenant)
.await
.map_err(write_failure)?
.write(&mut values);
Ok(Some(values))
})
}
fn submit<'a>(
&'a self,
cx: &'a Cx,
signed: &'a SignedIn,
tenant: &'a Membership,
mut values: FormValues,
) -> BoxFuture<'a, topcoat::Result<Outcome>> {
Box::pin(async move {
let any: &dyn Any = &*signed.user;
let user = any
.downcast_ref::<E::User>()
.ok_or_else(|| topcoat::Error::from(forbidden()))?;
let schema = E::Input::schema();
let mut stored = FormValues::new();
self.fill(cx, user, tenant)
.await
.map_err(write_failure)?
.write(&mut stored);
input::keep_disabled(&schema, &mut values, &stored);
schema.keep_unedited(&mut values, &stored, &crate::time_zone(cx));
let input =
match input::parse::<E::Input>(cx, &values, &stored, "tenant settings").await? {
Ok(input) => input,
Err(errors) => return Ok(Outcome::Refused { values, errors }),
};
let mut db = crate::db::db(cx)?;
let mut tx = db.transaction().await.map_err(crate::error::unavailable)?;
let errors = self
.validate(cx, user, tenant, &input, &mut tx)
.await
.map_err(write_failure)?;
input::placed(&schema, &errors, "the tenant editor", "tenant settings")?;
if !errors.is_empty() {
return Ok(Outcome::Refused { values, errors });
}
self.save(cx, user, tenant, input, &mut tx)
.await
.map_err(write_failure)?;
tx.commit().await.map_err(crate::error::unavailable)?;
Ok(Outcome::Saved)
})
}
}
fn write_failure(error: topcoat::Error) -> topcoat::Error {
crate::error::driver_failure(error, crate::error::DATABASE_UNAVAILABLE)
}
const TENANT_FIELD: &str = "editing_tenant";
pub(crate) const RESERVED_KEYS: [&str; 2] = [crate::csrf::FIELD_NAME, TENANT_FIELD];
pub(crate) fn tenant_profile_url(cx: &Cx) -> String {
current_realm(cx).map_or_else(
|| format!("{}/tenant/profile", panel_prefix(cx)),
|realm| realm.pages.tenant_profile.clone(),
)
}
pub(crate) fn offered(cx: &Cx) -> bool {
editing(cx).is_ok()
}
fn editing(cx: &Cx) -> topcoat::Result<(&SignedIn, &dyn DynTenantEditor, &Membership)> {
let signed = signed(cx).ok_or_else(|| unauthenticated_error(cx))?;
let editor = current_realm(cx)
.and_then(|panel| panel.auth.tenant_editor())
.ok_or_else(not_found)?;
let tenant = membership(cx)
.filter(|tenant| editor.may_edit(cx, signed.user.as_ref(), tenant))
.ok_or_else(forbidden)?;
Ok((signed, editor, tenant))
}
pub(super) fn tenant_profile_page(cx: &Cx, _body: Body) -> BoxView<'_> {
async_page(async move {
let (signed, editor, tenant) = editing(cx)?;
let values = editor
.values(cx, signed.user.as_ref(), tenant)
.await?
.ok_or_else(forbidden)?;
crate::csrf::ensure_token(cx);
render(cx, editor, tenant, &values, &FieldErrors::new()).await
})
}
pub(super) fn tenant_profile_post(cx: &Cx, body: Body) -> BoxView<'_> {
async_page(async move {
let (signed, editor, tenant) = editing(cx)?;
let form = crate::auth::input::read_form(cx, body).await?;
crate::csrf::verify(cx, form.last(crate::csrf::FIELD_NAME))?;
if form.last(TENANT_FIELD) != Some(tenant.tenant.to_string().as_str()) {
set_notification(
cx,
Notification::warning(crate::t!(cx, "tablo-tenant-not-saved")).description(
crate::t!(cx, "tablo-tenant-switched", tenant = tenant.name.clone()),
),
);
return Err(see_other(tenant_profile_url(cx)).into());
}
let input_schema = editor.schema();
let values = input_schema.read_input(cx, &form.pairs, &RESERVED_KEYS)?;
if let Outcome::Refused { values, errors } =
editor.submit(cx, signed, tenant, values).await?
{
return render(cx, editor, tenant, &values, &errors).await;
}
set_notification(
cx,
Notification::success(crate::t!(cx, "tablo-tenant-saved")),
);
Err(see_other(tenant_profile_url(cx)).into())
})
}
async fn render<'a>(
cx: &'a Cx,
editor: &dyn DynTenantEditor,
tenant: &Membership,
values: &FormValues,
errors: &FieldErrors,
) -> topcoat::Result<BoxView<'a>> {
let schema = editor.schema();
let page = SettingsPage {
title: crate::t!(cx, "tablo-shell-tenant-settings"),
schema: &schema,
hidden: Some((TENANT_FIELD, tenant.tenant.to_string())),
values,
errors,
};
hosted(cx)?.settings_page(cx, page).await
}
pub(super) fn declaration_errors(auth: &super::AuthConfig) -> Vec<crate::DeclarationErrorKind> {
use super::input::FieldFault;
use crate::{DeclarationErrorKind, declaration::TenantProfileFault as Fault};
let Some(editor) = auth.tenant_editor.as_deref() else {
return Vec::new();
};
if auth.authenticator().is_none() {
return vec![DeclarationErrorKind::TenantProfile(Fault::AuthDisabled)];
}
let mut kinds = Vec::new();
let schema = editor.schema();
kinds.extend(schema.declaration_errors());
kinds.extend(schema.empty_choices());
kinds.extend(
input::field_faults(&schema, &RESERVED_KEYS).map(|(fault, name)| {
DeclarationErrorKind::TenantProfile(match fault {
FieldFault::Reserved => Fault::ReservedField(name),
FieldFault::File => Fault::FileField(name),
FieldFault::Relationship => Fault::RelationshipField(name),
})
}),
);
kinds
}