forge
ExploreLog in

Tahinli/forge

public
0 watching
★ 0
5.0 KiB
1//! Page: create an organization (GET/POST `/org/new`). Any signed-in person may create one, unless
2//! the config sets `[orgs] creation = "admin"`; then only instance admins may. The creator becomes
3//! the first owner.
4
5use forge_core::config::OrgCreation;
6use forge_core::{AuditAction, User};
7use serde::Deserialize;
8use topcoat::Result;
9use topcoat::context::{Cx, app_context};
10use topcoat::router::content::Form;
11use topcoat::router::error::{forbidden, see_other};
12use topcoat::router::page;
13use topcoat::view::{View, component, view};
14
15use crate::audit::{self, Actor, Target};
16use crate::auth::{require_csrf, require_user};
17use crate::i18n::orgs::{self as g, K};
18use crate::i18n::{self, Lang};
19use crate::state::AppState;
20use crate::views::{alert, csrf_field, shell, user_facing_message};
21
22const MAX_DESCRIPTION_CHARS: usize = 2000;
23
24#[derive(Deserialize)]
25struct NewOrgForm {
26 #[serde(default)]
27 csrf: String,
28 #[serde(default)]
29 name: String,
30 #[serde(default)]
31 description: String,
32}
33
34/// Whether `user` may create an organization under the instance's policy.
35fn may_create_org(state: &AppState, user: &User) -> bool {
36 match state.config.orgs.creation {
37 OrgCreation::All => true,
38 OrgCreation::Admin => user.is_admin,
39 }
40}
41
42#[component]
43async fn new_org_form(
44 csrf: String,
45 name: String,
46 description: String,
47 error: Option<String>,
48 lang: Lang,
49) -> Result<impl View> {
50 Ok(view! {
51 <section class="card narrow">
52 <h1>(g::t(lang, K::NewOrganization))</h1>
53 <p class="muted small">(g::t(lang, K::NewOrgIntro))</p>
54 if let Some(message) = error {
55 alert(message: message.as_str())
56 }
57 <form method="post" action="/org/new" class="stack">
58 csrf_field(token: csrf.as_str())
59 <label>
60 <span>(g::t(lang, K::OrgName))</span>
61 <input type="text" name="name" value=(name.as_str()) required=(true) maxlength="64" autocomplete="off">
62 </label>
63 <label>
64 <span>(g::t(lang, K::DescriptionOptional))</span>
65 <textarea name="description" rows="3" maxlength="2000">(description.as_str())</textarea>
66 </label>
67 <button type="submit" class="primary">(g::t(lang, K::CreateOrg))</button>
68 </form>
69 </section>
70 })
71}
72
73#[page("/org/new")]
74async fn new_org_page(cx: &Cx) -> Result<impl View> {
75 let viewer = require_user(cx).await?;
76 if !may_create_org(app_context::<AppState>(cx), &viewer.0) {
77 return Err(forbidden().into());
78 }
79 let lang = i18n::lang(cx, Some(&viewer.0));
80 let title = g::t(lang, K::NewOrganization);
81 let csrf = viewer.1.csrf_token.clone();
82 Ok(view! { cx =>
83 shell(title: title, viewer: Some(&viewer),
84 new_org_form(csrf: csrf, name: String::new(), description: String::new(), error: None, lang: lang))
85 })
86}
87
88#[page(POST "/org/new")]
89async fn new_org_submit(cx: &Cx, Form(form): Form<NewOrgForm>) -> Result<impl View> {
90 let viewer = require_csrf(cx, &form.csrf).await?;
91 let state = app_context::<AppState>(cx);
92 if !may_create_org(state, &viewer.0) {
93 return Err(forbidden().into());
94 }
95 let lang = i18n::lang(cx, Some(&viewer.0));
96 match create_org(state, cx, &viewer.0, &form, lang).await? {
97 Ok(org) => Err(see_other(format!("/{}", org.username)).into()),
98 Err(message) => {
99 let title = g::t(lang, K::NewOrganization);
100 let csrf = viewer.1.csrf_token.clone();
101 let name = form.name.clone();
102 let description = form.description.clone();
103 Ok(view! { cx =>
104 shell(title: title, viewer: Some(&viewer),
105 new_org_form(csrf: csrf, name: name, description: description, error: Some(message), lang: lang))
106 })
107 }
108 }
109}
110
111/// Validates the form and creates the org. `Ok(Err(message))` is a user-facing rejection; the
112/// outer `Err` is an internal failure.
113async fn create_org(
114 state: &AppState,
115 cx: &Cx,
116 creator: &User,
117 form: &NewOrgForm,
118 lang: Lang,
119) -> Result<std::result::Result<User, String>> {
120 let description = form.description.trim();
121 if description.chars().count() > MAX_DESCRIPTION_CHARS {
122 return Ok(Err(g::description_too_long(lang, MAX_DESCRIPTION_CHARS)));
123 }
124 let org = match state.store.create_org(form.name.trim(), &creator.id).await {
125 Ok(org) => org,
126 Err(e) => match user_facing_message(&e) {
127 Some(message) => return Ok(Err(message)),
128 None => return Err(e.into()),
129 },
130 };
131 if !description.is_empty() {
132 state
133 .store
134 .set_profile(&org.id, description, &org.timezone)
135 .await?;
136 }
137 audit::record(
138 &state.store,
139 Some(cx),
140 Actor::User(creator),
141 AuditAction::OrgCreated,
142 Target {
143 kind: "org",
144 id: &org.id,
145 name: &org.username,
146 },
147 serde_json::Value::Null,
148 )
149 .await;
150 Ok(Ok(org))
151}