OXYGENHUB

Coverage / crates/hub/src/web/orgs.rs 51.3%

222 of 433 lines run, 28 of 172 functions · run #470 on main · run missed

1
//! Organisations and teams (hub spec §5.2).
2
3
use super::settings::{csrf, require};
4
use super::{filters, Error, Page, Shell};
5
use crate::auth::{roles, Viewer};
6
use crate::db::accounts;
7
use crate::Shared;
8
use askama::Template;
9
use axum::extract::{Form, Path, State};
10
use axum::response::{IntoResponse, Redirect, Response};
11
use serde::Deserialize;
12
13
#[derive(Template)]
14
#[template(path = "org_new.html")]
15
pub struct OrgNewPage {
16
    shell: Shell,
17
    error: Option<String>,
18
}
19
203×
pub async fn new_form(State(app): State<Shared>, viewer: Viewer) -> Result<Page<OrgNewPage>, Error> {
213×
    let shell = Shell::new(&app, "/orgs/new", &viewer);
223×
    require(&viewer, &shell)?;
233×
    Ok(Page(OrgNewPage { shell, error: None }))
243×
}
25
26
#[derive(Deserialize)]
27
pub struct OrgNewForm {
28
    #[serde(rename = "_csrf")]
29
    csrf_token: String,
30
    name: String,
31
    display_name: String,
32
}
33
344×
pub async fn create(State(app): State<Shared>, viewer: Viewer, Form(f): Form<OrgNewForm>) -> Result<Response, Error> {
354×
    let shell = Shell::new(&app, "/orgs/new", &viewer);
364×
    let me = require(&viewer, &shell)?;
374×
    csrf(&viewer, &f.csrf_token, &shell)?;
384×
    let name = f.name.trim().to_string();
394×
    if !crate::repos::names::valid_owner(&name) {
400×
        return Ok(Page(OrgNewPage {
410×
            shell,
420×
            error: Some("An organisation name is letters, digits, dots, dashes and underscores.".into()),
430×
        })
440×
        .into_response());
454×
    }
464×
    let taken = accounts::account_by_name(&app.db, &name).await.map_err(|e| Error::from_db(shell.clone(), e))?.is_some()
474×
        || accounts::org_by_name(&app.db, &name).await.map_err(|e| Error::from_db(shell.clone(), e))?.is_some()
484×
        || accounts::username_retired(&app.db, &name).await.map_err(|e| Error::from_db(shell.clone(), e))?;
494×
    if taken {
500×
        return Ok(Page(OrgNewPage { shell, error: Some("That name is taken.".into()) }).into_response());
514×
    }
524×
    let org =
534×
        accounts::create_org(&app.db, &name, f.display_name.trim(), me.account.id).await.map_err(|e| Error::from_db(shell.clone(), e))?;
544×
    accounts::audit(&app.db, Some(&me.account), "org.created", &org.name, "").await.ok();
554×
    Ok(Redirect::to(&format!("/orgs/{}/settings", org.name)).into_response())
564×
}
57
58
#[derive(Template)]
59
#[template(path = "org_settings.html")]
60
pub struct OrgSettingsPage {
61
    shell: Shell,
62
    org: accounts::Organisation,
63
    members: Vec<(accounts::Account, String)>,
64
    teams: Vec<(accounts::Team, Vec<accounts::Account>)>,
65
    hooks: Vec<super::webhooks::HookRow>,
66
    notice: Option<String>,
67
    error: Option<String>,
68
    /// Runners serving the organisation: (runner, what it serves, whether
69
    /// it is the organisation's own).
70
    runners: Vec<(crate::db::runners::Runner, String, bool)>,
71
    /// A registration token just made, shown once.
72
    runner_token: Option<String>,
73
    /// The organisation's AI assistants and prompt (#97).
74
    assistants: super::assistants::Panel,
75
    /// The organisation's Flow variables (#102).
76
    variables: super::variables::Panel,
77
    /// The organisation's Flow secrets (#107).
78
    secrets: super::secrets::Panel,
79
    /// Where its new repositories may live (#96).
80
    policy: crate::storage::Policy,
81
}
82
8320×
async fn org_page(
8420×
    app: &Shared,
8520×
    shell: Shell,
8620×
    org: accounts::Organisation,
8720×
    notice: Option<String>,
8820×
    error: Option<String>,
8920×
) -> Result<Page<OrgSettingsPage>, Error> {
9020×
    let members = accounts::org_members(&app.db, org.id).await.map_err(|e| Error::from_db(shell.clone(), e))?;
9120×
    let mut teams = Vec::new();
9220×
    for t in accounts::teams(&app.db, org.id).await.map_err(|e| Error::from_db(shell.clone(), e))? {
930×
        let members = accounts::team_members(&app.db, t.id).await.map_err(|e| Error::from_db(shell.clone(), e))?;
940×
        teams.push((t, members));
95
    }
9620×
    let hooks = crate::db::hooks::hooks_for_org(&app.db, org.id).await.map_err(|e| Error::from_db(shell.clone(), e))?;
9720×
    let hooks = super::webhooks::rows(app, hooks).await.map_err(|e| Error::from_db(shell.clone(), e))?;
9820×
    let mut runners = Vec::new();
9920×
    for r in crate::db::runners::serving_owner(&app.db, &org.name).await.map_err(|e| Error::from_db(shell.clone(), e))? {
1005×
        let own = r.scope_owner == org.name;
1015×
        let serves = match r.scope_repo_id {
1020×
            Some(id) => crate::db::repos::get_by_id(&app.db, id).await.ok().flatten().map(|p| p.slug()).unwrap_or_default(),
1031×
            None if own => "the organisation's repositories".into(),
1044×
            None => "the whole site".into(),
105
        };
1065×
        runners.push((r, serves, own));
107
    }
10820×
    let assistants = super::assistants::panel(app, &org_assistants(&org)).await.map_err(|e| Error::from_db(shell.clone(), e))?;
10920×
    let variables = super::variables::panel(app, &org_assistants(&org)).await.map_err(|e| Error::from_db(shell.clone(), e))?;
11020×
    let secrets = super::secrets::panel(app, &org_assistants(&org)).await.map_err(|e| Error::from_db(shell.clone(), e))?;
11120×
    let policy = crate::storage::policy(&app.db, org.id).await.map_err(|e| Error::from_db(shell.clone(), e))?;
11220×
    Ok(Page(OrgSettingsPage {
11320×
        policy,
11420×
        shell,
11520×
        org,
11620×
        members,
11720×
        teams,
11820×
        hooks,
11920×
        notice,
12020×
        error,
12120×
        runners,
12220×
        runner_token: None,
12320×
        assistants,
12420×
        variables,
12520×
        secrets,
12620×
    }))
12720×
}
128
12927×
pub(crate) async fn owned_org(
13027×
    app: &Shared,
13127×
    shell: &Shell,
13227×
    viewer: &Viewer,
13327×
    name: &str,
13427×
) -> Result<(accounts::Account, accounts::Organisation), Error> {
13527×
    let me = require(viewer, shell)?;
13627×
    let org = accounts::org_by_name(&app.db, name)
13727×
        .await
13827×
        .map_err(|e| Error::from_db(shell.clone(), e))?
13927×
        .ok_or_else(|| Error::not_found(shell.clone()))?;
14027×
    if !roles::is_org_owner(&app.db, &me.account, org.id).await.map_err(|e| Error::from_db(shell.clone(), e))? {
1414×
        return Err(Error::not_found(shell.clone()));
14223×
    }
14323×
    Ok((me.account, org))
14427×
}
145
1469×
pub async fn settings(State(app): State<Shared>, viewer: Viewer, Path(name): Path<String>) -> Result<Page<OrgSettingsPage>, Error> {
1479×
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
1489×
    let (_, org) = owned_org(&app, &shell, &viewer, &name).await?;
1498×
    org_page(&app, shell, org, None, None).await
1509×
}
151
152
#[derive(Deserialize)]
153
pub struct OrgForm {
154
    #[serde(rename = "_csrf")]
155
    csrf_token: String,
156
    display_name: String,
157
    description: String,
158
    default_role: String,
159
}
160
1610×
pub async fn update(
1620×
    State(app): State<Shared>,
1630×
    viewer: Viewer,
1640×
    Path(name): Path<String>,
1650×
    Form(f): Form<OrgForm>,
1660×
) -> Result<Page<OrgSettingsPage>, Error> {
1670×
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
1680×
    let (me, org) = owned_org(&app, &shell, &viewer, &name).await?;
1690×
    csrf(&viewer, &f.csrf_token, &shell)?;
1700×
    let role = if f.default_role == "none" || roles::Role::parse(&f.default_role).is_some() { f.default_role.as_str() } else { "read" };
1710×
    accounts::set_org(&app.db, org.id, f.display_name.trim(), f.description.trim(), role)
1720×
        .await
1730×
        .map_err(|e| Error::from_db(shell.clone(), e))?;
1740×
    accounts::audit(&app.db, Some(&me), "org.updated", &org.name, "").await.ok();
1750×
    let org = accounts::org_by_name(&app.db, &name).await.map_err(|e| Error::from_db(shell.clone(), e))?.unwrap_or(org);
1760×
    org_page(&app, shell, org, Some("Saved.".into()), None).await
1770×
}
178
179
#[derive(Deserialize)]
180
pub struct PriorityForm {
181
    #[serde(rename = "_csrf")]
182
    csrf_token: String,
183
    priority: String,
184
}
185
186
/// `POST /orgs/{name}/settings/flow-priority` (oxygenhub#244): its
187
/// repositories' Flow level unless one sets its own. Owners choose low,
188
/// normal or the site's; only site administrators raise one to high.
1890×
pub async fn flow_priority(
1900×
    State(app): State<Shared>,
1910×
    viewer: Viewer,
1920×
    Path(name): Path<String>,
1930×
    Form(f): Form<PriorityForm>,
1940×
) -> Result<Page<OrgSettingsPage>, Error> {
1950×
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
1960×
    let (me, org) = owned_org(&app, &shell, &viewer, &name).await?;
1970×
    csrf(&viewer, &f.csrf_token, &shell)?;
1980×
    let level = match f.priority.as_str() {
1990×
        "low" | "normal" | "high" => f.priority.as_str(),
2000×
        "default" => "",
2010×
        _ => return org_page(&app, shell, org, None, Some("Pick low, normal, high or the site's.".into())).await,
202
    };
2030×
    if level == "high" && !viewer.is_admin() {
2040×
        return org_page(&app, shell, org, None, Some("Only site administrators raise an organisation to high.".into())).await;
2050×
    }
2060×
    accounts::set_org_flow_priority(&app.db, org.id, level).await.map_err(|e| Error::from_db(shell.clone(), e))?;
2070×
    accounts::audit(&app.db, Some(&me), "org.flow_priority", &org.name, if level.is_empty() { "default" } else { level }).await.ok();
2080×
    let org = accounts::org_by_name(&app.db, &name).await.map_err(|e| Error::from_db(shell.clone(), e))?.unwrap_or(org);
2090×
    org_page(&app, shell, org, Some("Saved.".into()), None).await
2100×
}
211
212
#[derive(Deserialize)]
213
pub struct MemberForm {
214
    #[serde(rename = "_csrf")]
215
    csrf_token: String,
216
    username: String,
217
    #[serde(default)]
218
    role: String,
219
    #[serde(default)]
220
    remove: Option<String>,
221
}
222
2231×
pub async fn member(
2241×
    State(app): State<Shared>,
2251×
    viewer: Viewer,
2261×
    Path(name): Path<String>,
2271×
    Form(f): Form<MemberForm>,
2281×
) -> Result<Page<OrgSettingsPage>, Error> {
2291×
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
2301×
    let (me, org) = owned_org(&app, &shell, &viewer, &name).await?;
2311×
    csrf(&viewer, &f.csrf_token, &shell)?;
2321×
    let Some(target) = accounts::account_by_name(&app.db, f.username.trim()).await.map_err(|e| Error::from_db(shell.clone(), e))? else {
2330×
        return org_page(&app, shell, org, None, Some("No such account.".into())).await;
234
    };
2351×
    if f.remove.is_some() {
2360×
        let owners = accounts::org_members(&app.db, org.id)
2370×
            .await
2380×
            .map_err(|e| Error::from_db(shell.clone(), e))?
2390×
            .iter()
2400×
            .filter(|(_, r)| r == "owner")
2410×
            .count();
2420×
        let is_owner =
2430×
            accounts::org_role(&app.db, org.id, target.id).await.map_err(|e| Error::from_db(shell.clone(), e))?.as_deref() == Some("owner");
2440×
        if is_owner && owners <= 1 {
2450×
            return org_page(&app, shell, org, None, Some("An organisation keeps at least one owner.".into())).await;
2460×
        }
2470×
        accounts::remove_org_member(&app.db, org.id, target.id).await.map_err(|e| Error::from_db(shell.clone(), e))?;
2480×
        accounts::audit(&app.db, Some(&me), "org.member.removed", &org.name, &target.username).await.ok();
2490×
        return org_page(&app, shell, org, Some(format!("Removed {}.", target.username)), None).await;
2501×
    }
2511×
    let role = if f.role == "owner" { "owner" } else { "member" };
2521×
    accounts::set_org_member(&app.db, org.id, target.id, role).await.map_err(|e| Error::from_db(shell.clone(), e))?;
2531×
    accounts::audit(&app.db, Some(&me), "org.member.set", &org.name, &format!("{} {role}", target.username)).await.ok();
2541×
    org_page(&app, shell, org, Some(format!("{} is now {role}.", target.username)), None).await
2551×
}
256
257
#[derive(Deserialize)]
258
pub struct TeamForm {
259
    #[serde(rename = "_csrf")]
260
    csrf_token: String,
261
    team: String,
262
    #[serde(default)]
263
    username: String,
264
    #[serde(default)]
265
    action: String,
266
}
267
2680×
pub async fn team(
2690×
    State(app): State<Shared>,
2700×
    viewer: Viewer,
2710×
    Path(name): Path<String>,
2720×
    Form(f): Form<TeamForm>,
2730×
) -> Result<Page<OrgSettingsPage>, Error> {
2740×
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
2750×
    let (me, org) = owned_org(&app, &shell, &viewer, &name).await?;
2760×
    csrf(&viewer, &f.csrf_token, &shell)?;
2770×
    let team_name = f.team.trim().to_string();
2780×
    if !crate::repos::names::valid_segment(&team_name) {
2790×
        return org_page(&app, shell, org, None, Some("A team name is letters, digits, dots, dashes and underscores.".into())).await;
2800×
    }
2810×
    let existing = accounts::team_by_name(&app.db, org.id, &team_name).await.map_err(|e| Error::from_db(shell.clone(), e))?;
2820×
    let notice = match f.action.as_str() {
2830×
        "create" => {
2840×
            if existing.is_some() {
2850×
                return org_page(&app, shell, org, None, Some("That team exists.".into())).await;
2860×
            }
2870×
            accounts::create_team(&app.db, org.id, &team_name).await.map_err(|e| Error::from_db(shell.clone(), e))?;
2880×
            format!("Team {team_name} created.")
289
        }
2900×
        "delete" => {
2910×
            if let Some(t) = existing {
2920×
                accounts::delete_team(&app.db, t.id).await.map_err(|e| Error::from_db(shell.clone(), e))?;
2930×
            }
2940×
            format!("Team {team_name} deleted.")
295
        }
2960×
        "add" | "remove" => {
2970×
            let Some(t) = existing else { return org_page(&app, shell, org, None, Some("No such team.".into())).await };
2980×
            let Some(target) = accounts::account_by_name(&app.db, f.username.trim()).await.map_err(|e| Error::from_db(shell.clone(), e))?
299
            else {
3000×
                return org_page(&app, shell, org, None, Some("No such account.".into())).await;
301
            };
3020×
            if f.action == "add" {
3030×
                accounts::set_org_member(&app.db, org.id, target.id, "member").await.ok();
3040×
                accounts::add_team_member(&app.db, t.id, target.id).await.map_err(|e| Error::from_db(shell.clone(), e))?;
3050×
                format!("{} added to {team_name}.", target.username)
306
            } else {
3070×
                accounts::remove_team_member(&app.db, t.id, target.id).await.map_err(|e| Error::from_db(shell.clone(), e))?;
3080×
                format!("{} removed from {team_name}.", target.username)
309
            }
310
        }
3110×
        _ => return Err(Error::bad(shell, "unknown team action")),
312
    };
3130×
    accounts::audit(&app.db, Some(&me), "org.team", &org.name, &format!("{} {team_name} {}", f.action, f.username.trim())).await.ok();
3140×
    org_page(&app, shell, org, Some(notice), None).await
3150×
}
316
317
/// `POST /orgs/{name}/settings/avatar`: upload or remove the organisation's logo.
3182×
pub async fn avatar(
3192×
    State(app): State<Shared>,
3202×
    viewer: Viewer,
3212×
    Path(name): Path<String>,
3222×
    multipart: axum::extract::Multipart,
3232×
) -> Result<Page<OrgSettingsPage>, Error> {
3242×
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
3252×
    let (me, org) = owned_org(&app, &shell, &viewer, &name).await?;
3261×
    let upload = super::avatar::read_upload(multipart).await;
3271×
    csrf(&viewer, &upload.csrf, &shell)?;
3281×
    let result = super::avatar::apply(&app, "org", org.id, upload).await;
3291×
    if result.is_ok() {
3301×
        accounts::audit(&app.db, Some(&me), "org.picture", &org.name, "").await.ok();
3310×
    }
3321×
    let org = accounts::org_by_name(&app.db, &name).await.map_err(|e| Error::from_db(shell.clone(), e))?.unwrap_or(org);
3331×
    match result {
3341×
        Ok(m) => org_page(&app, shell, org, Some(m), None).await,
3350×
        Err(e) => org_page(&app, shell, org, None, Some(e)).await,
336
    }
3372×
}
338
339
/// `POST /orgs/{name}/settings/hooks`: webhooks that hear every repository of the organisation.
3404×
pub async fn hooks(
3414×
    State(app): State<Shared>,
3424×
    viewer: Viewer,
3434×
    Path(name): Path<String>,
3444×
    Form(f): Form<super::webhooks::HookForm>,
3454×
) -> Result<Page<OrgSettingsPage>, Error> {
3464×
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
3474×
    let (_, org) = owned_org(&app, &shell, &viewer, &name).await?;
3484×
    csrf(&viewer, &f.csrf_token, &shell)?;
3494×
    let scope = super::webhooks::Scope::Org { id: org.id, name: org.name.clone() };
3504×
    match super::webhooks::apply(&app, &viewer, &scope, f).await.map_err(|e| Error::from_db(shell.clone(), e))? {
3513×
        Ok(notice) => org_page(&app, shell, org, Some(notice), None).await,
3521×
        Err(error) => org_page(&app, shell, org, None, Some(error)).await,
353
    }
3544×
}
355
3562×
pub async fn deliveries(
3572×
    State(app): State<Shared>,
3582×
    viewer: Viewer,
3592×
    Path((name, id)): Path<(String, i64)>,
3602×
) -> Result<Page<super::webhooks::DeliveriesPage>, Error> {
3612×
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings/hooks/{id}/deliveries"), &viewer);
3622×
    let (_, org) = owned_org(&app, &shell, &viewer, &name).await?;
3632×
    let scope = super::webhooks::Scope::Org { id: org.id, name: org.name };
3642×
    super::webhooks::deliveries_page(&app, shell, &scope, None, id).await
3652×
}
366
3670×
pub async fn redeliver(
3680×
    State(app): State<Shared>,
3690×
    viewer: Viewer,
3700×
    Path((name, id, delivery)): Path<(String, i64, i64)>,
3710×
    Form(f): Form<super::session::CsrfForm>,
3720×
) -> Result<Page<super::webhooks::DeliveriesPage>, Error> {
3730×
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings/hooks/{id}/deliveries"), &viewer);
3740×
    let (_, org) = owned_org(&app, &shell, &viewer, &name).await?;
3750×
    csrf(&viewer, &f.csrf, &shell)?;
3760×
    let scope = super::webhooks::Scope::Org { id: org.id, name: org.name };
3770×
    super::webhooks::redeliver(&app, &scope, id, delivery).await.map_err(|e| Error::from_db(shell.clone(), e))?;
3780×
    super::webhooks::deliveries_page(&app, shell, &scope, None, id).await
3790×
}
380
381
/// `POST /orgs/{name}/settings/runners/token`: a one-time registration
382
/// token for a runner that serves the organisation's repositories
383
/// (oxygenhub#41).
3841×
pub async fn runner_token(
3851×
    State(app): State<Shared>,
3861×
    viewer: Viewer,
3871×
    Path(name): Path<String>,
3881×
    Form(f): Form<super::session::CsrfForm>,
3891×
) -> Result<Page<OrgSettingsPage>, Error> {
3901×
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
3911×
    let (me, org) = owned_org(&app, &shell, &viewer, &name).await?;
3921×
    csrf(&viewer, &f.csrf, &shell)?;
3931×
    let token = format!("flowreg_{}", crate::auth::random_hex(24));
3941×
    let scope = crate::db::runners::Scope { owner: org.name.clone(), repo_id: None };
3951×
    crate::db::runners::add_registration(&app.db, &blake3::hash(token.as_bytes()).to_hex(), &me.username, &scope)
3961×
        .await
3971×
        .map_err(|e| Error::from_db(shell.clone(), e))?;
3981×
    accounts::audit(&app.db, Some(&me), "flow.runner.token", &org.name, "registration token for the organisation").await.ok();
3991×
    let mut page = org_page(&app, shell, org, Some("Copy the token now; it is not shown again.".into()), None).await?;
4001×
    page.0.runner_token = Some(token);
4011×
    Ok(page)
4021×
}
403
404
/// `POST /orgs/{name}/settings/runners/{id}/remove`: remove a runner scoped
405
/// to the organisation or one of its repositories, when it runs nothing.
4061×
pub async fn runner_remove(
4071×
    State(app): State<Shared>,
4081×
    viewer: Viewer,
4091×
    Path((name, id)): Path<(String, i64)>,
4101×
    Form(f): Form<super::session::CsrfForm>,
4111×
) -> Result<Page<OrgSettingsPage>, Error> {
4121×
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
4131×
    let (me, org) = owned_org(&app, &shell, &viewer, &name).await?;
4141×
    csrf(&viewer, &f.csrf, &shell)?;
4151×
    let e = |e| Error::from_db(shell.clone(), e);
4161×
    let Some(r) = crate::db::runners::get(&app.db, id).await.map_err(e)?.filter(|r| r.scope_owner == org.name) else {
4170×
        return Err(Error::not_found(shell));
418
    };
4191×
    if crate::db::runners::running(&app.db).await.map_err(e)?.iter().any(|(rid, n)| *rid == id && *n > 0) {
4200×
        return org_page(&app, shell, org, None, Some(format!("{} is running jobs: wait for them to finish.", r.name))).await;
4211×
    }
4221×
    crate::db::runners::remove(&app.db, id).await.map_err(e)?;
4231×
    accounts::audit(&app.db, Some(&me), "flow.runner.removed", &r.name, &org.name).await.ok();
4241×
    org_page(&app, shell, org, Some(format!("{} removed: its credential no longer works.", r.name)), None).await
4251×
}
426
42762×
fn org_assistants(org: &accounts::Organisation) -> crate::levels::At {
42862×
    crate::levels::At::org(org)
42962×
}
430
431
/// `POST /orgs/{name}/settings/assistants`: the organisation's assistants
432
/// and prompt (#97).
4333×
pub async fn assistants(
4343×
    State(app): State<Shared>,
4353×
    viewer: Viewer,
4363×
    Path(name): Path<String>,
4373×
    Form(f): Form<super::assistants::AssistantForm>,
4383×
) -> Result<Page<OrgSettingsPage>, Error> {
4393×
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
4403×
    let (_, org) = owned_org(&app, &shell, &viewer, &name).await?;
4412×
    csrf(&viewer, &f.csrf_token, &shell)?;
4422×
    match super::assistants::apply(&app, &viewer, &org_assistants(&org), f).await.map_err(|e| Error::from_db(shell.clone(), e))? {
4432×
        Ok(notice) => org_page(&app, shell, org, Some(notice), None).await,
4440×
        Err(error) => org_page(&app, shell, org, None, Some(error)).await,
445
    }
4463×
}
447
448
#[derive(Deserialize)]
449
pub struct PolicyForm {
450
    #[serde(rename = "_csrf")]
451
    csrf_token: String,
452
    #[serde(default)]
453
    require_bucket: Option<String>,
454
    #[serde(default)]
455
    allowed: String,
456
}
457
458
/// `POST /orgs/{name}/settings/storage`: where the organisation's new
459
/// repositories may live (#96).
4602×
pub async fn storage(
4612×
    State(app): State<Shared>,
4622×
    viewer: Viewer,
4632×
    Path(name): Path<String>,
4642×
    Form(f): Form<PolicyForm>,
4652×
) -> Result<Page<OrgSettingsPage>, Error> {
4662×
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
4672×
    let (me, org) = owned_org(&app, &shell, &viewer, &name).await?;
4682×
    csrf(&viewer, &f.csrf_token, &shell)?;
4692×
    match crate::storage::set_policy(&app.db, org.id, f.require_bucket.is_some(), &f.allowed).await {
4701×
        Ok(p) => {
4711×
            let what = match (p.require_bucket, p.allowed.is_empty()) {
4720×
                (true, true) => "every new repository in a bucket".to_string(),
4731×
                (true, false) => format!("every new repository in {}", p.allowed.join(", ")),
4740×
                (false, true) => "any storage".to_string(),
4750×
                (false, false) => format!("buckets only in {}", p.allowed.join(", ")),
476
            };
4771×
            accounts::audit(&app.db, Some(&me), "org.storage_policy", &org.name, &what).await.ok();
4781×
            org_page(&app, shell, org, Some(format!("Storage policy saved: {what}.")), None).await
479
        }
4801×
        Err(e) => org_page(&app, shell, org, None, Some(format!("Storage policy: {e}."))).await,
481
    }
4822×
}
483
484
/// `POST /orgs/{name}/settings/variables`: the organisation's Flow
485
/// variables (#102).
4860×
pub async fn variables(
4870×
    State(app): State<Shared>,
4880×
    viewer: Viewer,
4890×
    Path(name): Path<String>,
4900×
    Form(f): Form<super::variables::VariableForm>,
4910×
) -> Result<Page<OrgSettingsPage>, Error> {
4920×
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
4930×
    let (_, org) = owned_org(&app, &shell, &viewer, &name).await?;
4940×
    csrf(&viewer, &f.csrf_token, &shell)?;
4950×
    match super::variables::apply(&app, &viewer, &org_assistants(&org), f).await.map_err(|e| Error::from_db(shell.clone(), e))? {
4960×
        Ok(notice) => org_page(&app, shell, org, Some(notice), None).await,
4970×
        Err(error) => org_page(&app, shell, org, None, Some(error)).await,
498
    }
4990×
}
500
501
/// `POST /orgs/{name}/settings/secrets`: the organisation's Flow secrets (#107).
5020×
pub async fn secrets(
5030×
    State(app): State<Shared>,
5040×
    viewer: Viewer,
5050×
    Path(name): Path<String>,
5060×
    Form(f): Form<super::secrets::SecretForm>,
5070×
) -> Result<Page<OrgSettingsPage>, Error> {
5080×
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
5090×
    let (_, org) = owned_org(&app, &shell, &viewer, &name).await?;
5100×
    csrf(&viewer, &f.csrf_token, &shell)?;
5110×
    match super::secrets::apply(&app, &viewer, &org_assistants(&org), f).await.map_err(|e| Error::from_db(shell.clone(), e))? {
5120×
        Ok(notice) => org_page(&app, shell, org, Some(notice), None).await,
5130×
        Err(error) => org_page(&app, shell, org, None, Some(error)).await,
514
    }
5150×
}
516
517
/// `POST /orgs/{name}/settings/assistants/models`: what a provider offers (#109).
5180×
pub async fn assistant_models(
5190×
    State(app): State<Shared>,
5200×
    viewer: Viewer,
5210×
    Path(name): Path<String>,
5220×
    Form(f): Form<super::assistants::ModelsForm>,
5230×
) -> Result<axum::Json<serde_json::Value>, Error> {
5240×
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
5250×
    let (_, org) = owned_org(&app, &shell, &viewer, &name).await?;
5260×
    csrf(&viewer, &f.csrf_token, &shell)?;
5270×
    Ok(axum::Json(super::assistants::models(&app, &org_assistants(&org), f).await.map_err(|e| Error::from_db(shell.clone(), e))?))
5280×
}