//! Page: create an organization (GET/POST `/org/new`). Any signed-in person may create one, unless //! the config sets `[orgs] creation = "admin"`; then only instance admins may. The creator becomes //! the first owner. use forge_core::config::OrgCreation; use forge_core::{AuditAction, User}; use serde::Deserialize; use topcoat::Result; use topcoat::context::{Cx, app_context}; use topcoat::router::content::Form; use topcoat::router::error::{forbidden, see_other}; use topcoat::router::page; use topcoat::view::{View, component, view}; use crate::audit::{self, Actor, Target}; use crate::auth::{require_csrf, require_user}; use crate::i18n::orgs::{self as g, K}; use crate::i18n::{self, Lang}; use crate::state::AppState; use crate::views::{alert, csrf_field, shell, user_facing_message}; const MAX_DESCRIPTION_CHARS: usize = 2000; #[derive(Deserialize)] struct NewOrgForm { #[serde(default)] csrf: String, #[serde(default)] name: String, #[serde(default)] description: String, } /// Whether `user` may create an organization under the instance's policy. fn may_create_org(state: &AppState, user: &User) -> bool { match state.config.orgs.creation { OrgCreation::All => true, OrgCreation::Admin => user.is_admin, } } #[component] async fn new_org_form( csrf: String, name: String, description: String, error: Option, lang: Lang, ) -> Result { Ok(view! {

(g::t(lang, K::NewOrganization))

(g::t(lang, K::NewOrgIntro))

if let Some(message) = error { alert(message: message.as_str()) }
csrf_field(token: csrf.as_str())
}) } #[page("/org/new")] async fn new_org_page(cx: &Cx) -> Result { let viewer = require_user(cx).await?; if !may_create_org(app_context::(cx), &viewer.0) { return Err(forbidden().into()); } let lang = i18n::lang(cx, Some(&viewer.0)); let title = g::t(lang, K::NewOrganization); let csrf = viewer.1.csrf_token.clone(); Ok(view! { cx => shell(title: title, viewer: Some(&viewer), new_org_form(csrf: csrf, name: String::new(), description: String::new(), error: None, lang: lang)) }) } #[page(POST "/org/new")] async fn new_org_submit(cx: &Cx, Form(form): Form) -> Result { let viewer = require_csrf(cx, &form.csrf).await?; let state = app_context::(cx); if !may_create_org(state, &viewer.0) { return Err(forbidden().into()); } let lang = i18n::lang(cx, Some(&viewer.0)); match create_org(state, cx, &viewer.0, &form, lang).await? { Ok(org) => Err(see_other(format!("/{}", org.username)).into()), Err(message) => { let title = g::t(lang, K::NewOrganization); let csrf = viewer.1.csrf_token.clone(); let name = form.name.clone(); let description = form.description.clone(); Ok(view! { cx => shell(title: title, viewer: Some(&viewer), new_org_form(csrf: csrf, name: name, description: description, error: Some(message), lang: lang)) }) } } } /// Validates the form and creates the org. `Ok(Err(message))` is a user-facing rejection; the /// outer `Err` is an internal failure. async fn create_org( state: &AppState, cx: &Cx, creator: &User, form: &NewOrgForm, lang: Lang, ) -> Result> { let description = form.description.trim(); if description.chars().count() > MAX_DESCRIPTION_CHARS { return Ok(Err(g::description_too_long(lang, MAX_DESCRIPTION_CHARS))); } let org = match state.store.create_org(form.name.trim(), &creator.id).await { Ok(org) => org, Err(e) => match user_facing_message(&e) { Some(message) => return Ok(Err(message)), None => return Err(e.into()), }, }; if !description.is_empty() { state .store .set_profile(&org.id, description, &org.timezone) .await?; } audit::record( &state.store, Some(cx), Actor::User(creator), AuditAction::OrgCreated, Target { kind: "org", id: &org.id, name: &org.username, }, serde_json::Value::Null, ) .await; Ok(Ok(org)) }