//! Page: organization settings (`/org/{org}/settings`): the public description, the default role //! members get on the org's repos, whether members may create repos, and deletion. Owners only: //! non-members get 404 and members who are not owners get 403. use forge_core::AuditAction; use forge_core::store::orgs::{DefaultRepoRole, OrgSettings}; use serde::Deserialize; use topcoat::Result; use topcoat::context::{Cx, app_context}; use topcoat::router::content::Form; use topcoat::router::error::{SeeOther, see_other}; use topcoat::router::{page, path_param, route}; use topcoat::view::{View, component, view}; use crate::audit::{self, Actor, Target}; use crate::i18n::orgs::{self as g, K}; use crate::i18n::{self, Lang}; use crate::pages::org_ctx::{ DEFAULT_ROLES, load_org, org_head, org_notice, require_form_csrf, require_owner, set_notice, take_notice, }; use crate::state::AppState; use crate::views::shell; path_param!(org); const MAX_DESCRIPTION_CHARS: usize = 2000; #[derive(Deserialize)] struct SettingsForm { #[serde(default)] csrf: String, #[serde(default)] description: String, #[serde(default)] default_repo_role: String, /// The checkbox sends `on` when checked and nothing when unchecked. #[serde(default)] members_can_create: String, } #[derive(Deserialize)] struct DeleteForm { #[serde(default)] csrf: String, /// The org's name, typed again to confirm the deletion. #[serde(default)] confirm: String, } #[component] async fn settings_body( name: String, description: String, base: String, csrf: String, default_role: String, members_can_create: bool, notice: Option<(&'static str, bool)>, lang: Lang, ) -> Result { let save_action = format!("{base}/settings"); let delete_action = format!("{base}/settings/delete"); let confirm_label = g::type_to_confirm(lang, &name); Ok(view! { org_head(name: name, description: description.clone(), active: "settings", is_owner: true, lang: lang) org_notice(notice: notice)

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

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

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

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

}) } /// Gets the settings page. Owners only. #[page("/org/{org}/settings")] async fn org_settings_page(cx: &Cx) -> Result { let ctx = load_org(cx, path_param::(cx)).await?; require_owner(&ctx)?; let lang = i18n::lang(cx, Some(&ctx.viewer.0)); let state = app_context::(cx); let settings = state.store.org_settings(&ctx.org.id).await?; let default_role = settings.default_repo_role.as_str().to_owned(); let members_can_create = settings.members_can_create_repos; let notice = take_notice(cx, lang); let title = g::page_title(lang, &ctx.org.username, K::Settings); let name = ctx.org.username.clone(); let description = ctx.org.about.clone(); let base = ctx.href(""); let csrf = ctx.viewer.1.csrf_token.clone(); Ok(view! { cx => shell(title: title.as_str(), viewer: Some(&ctx.viewer), settings_body(name: name, description: description, base: base, csrf: csrf, default_role: default_role, members_can_create: members_can_create, notice: notice, lang: lang)) }) } /// Saves the description and the org settings. Owners only. #[route(POST "/org/{org}/settings")] async fn org_settings_save(cx: &Cx, Form(form): Form) -> Result { let ctx = load_org(cx, path_param::(cx)).await?; require_form_csrf(&ctx, &form.csrf)?; require_owner(&ctx)?; let state = app_context::(cx); let back = ctx.href("/settings"); let description = form.description.trim(); if description.chars().count() > MAX_DESCRIPTION_CHARS { set_notice(cx, "description_length"); return Ok(see_other(back)); } let Ok(default_repo_role) = DefaultRepoRole::parse(form.default_repo_role.trim()) else { set_notice(cx, "invalid_role"); return Ok(see_other(back)); }; let members_can_create_repos = !form.members_can_create.is_empty(); state .store .set_org_settings( &ctx.org.id, OrgSettings { default_repo_role, members_can_create_repos, }, ) .await?; state .store .set_profile(&ctx.org.id, description, &ctx.org.timezone) .await?; audit::record( &state.store, Some(cx), Actor::User(&ctx.viewer.0), AuditAction::OrgSettingsUpdated, Target { kind: "org", id: &ctx.org.id, name: &ctx.org.username, }, serde_json::json!({ "default_repo_role": default_repo_role.as_str(), "members_can_create_repos": members_can_create_repos, }), ) .await; set_notice(cx, "settings_saved"); Ok(see_other(back)) } /// Deletes the org. Owners only, and the name must be typed again. Refused while the org owns /// repositories; then the settings page says so. #[route(POST "/org/{org}/settings/delete")] async fn org_delete(cx: &Cx, Form(form): Form) -> Result { let ctx = load_org(cx, path_param::(cx)).await?; require_form_csrf(&ctx, &form.csrf)?; require_owner(&ctx)?; let state = app_context::(cx); let back = ctx.href("/settings"); if form.confirm.trim() != ctx.org.username { set_notice(cx, "confirm_name"); return Ok(see_other(back)); } match state.store.delete_org(&ctx.org.id).await { Ok(()) => {} Err(forge_core::Error::Conflict(_)) => { set_notice(cx, "has_repos"); return Ok(see_other(back)); } Err(e) => return Err(e.into()), } audit::record( &state.store, Some(cx), Actor::User(&ctx.viewer.0), AuditAction::OrgDeleted, Target { kind: "org", id: &ctx.org.id, name: &ctx.org.username, }, serde_json::Value::Null, ) .await; Ok(see_other("/")) }