OXYGENHUB
◆ main oxygenhub / crates / hub / src / web / orgs.rs
28.1 KiB · Rust · last changed New repository page in sections, with OxygenFlow, the wiki and a default bookmark 1 hour ago source blame history raw
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
20
pub async fn new_form(State(app): State<Shared>, viewer: Viewer) -> Result<Page<OrgNewPage>, Error> {
21
    let shell = Shell::new(&app, "/orgs/new", &viewer);
22
    require(&viewer, &shell)?;
23
    Ok(Page(OrgNewPage { shell, error: None }))
24
}
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
34
pub async fn create(State(app): State<Shared>, viewer: Viewer, Form(f): Form<OrgNewForm>) -> Result<Response, Error> {
35
    let shell = Shell::new(&app, "/orgs/new", &viewer);
36
    let me = require(&viewer, &shell)?;
37
    csrf(&viewer, &f.csrf_token, &shell)?;
38
    let name = f.name.trim().to_string();
39
    if !crate::repos::names::valid_owner(&name) {
40
        return Ok(Page(OrgNewPage {
41
            shell,
42
            error: Some("An organisation name is letters, digits, dots, dashes and underscores.".into()),
43
        })
44
        .into_response());
45
    }
46
    let taken = accounts::account_by_name(&app.db, &name).await.map_err(|e| Error::from_db(shell.clone(), e))?.is_some()
47
        || accounts::org_by_name(&app.db, &name).await.map_err(|e| Error::from_db(shell.clone(), e))?.is_some()
48
        || accounts::username_retired(&app.db, &name).await.map_err(|e| Error::from_db(shell.clone(), e))?;
49
    if taken {
50
        return Ok(Page(OrgNewPage { shell, error: Some("That name is taken.".into()) }).into_response());
51
    }
52
    let org =
53
        accounts::create_org(&app.db, &name, f.display_name.trim(), me.account.id).await.map_err(|e| Error::from_db(shell.clone(), e))?;
54
    accounts::audit(&app.db, Some(&me.account), "org.created", &org.name, "").await.ok();
55
    Ok(Redirect::to(&format!("/orgs/{}/settings", org.name)).into_response())
56
}
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
    /// Its runners' updates (oxygenhub#322): its setting, test groups and runners.
72
    updates: super::runner_updates::UpdateViews,
73
    /// A registration token just made, shown once.
74
    runner_token: Option<String>,
75
    /// The organisation's AI assistants and prompt (#97).
76
    assistants: super::assistants::Panel,
77
    /// The organisation's Flow variables (#102).
78
    variables: super::variables::Panel,
79
    /// The organisation's Flow secrets (#107).
80
    secrets: super::secrets::Panel,
81
    /// Where its new repositories may live (#96).
82
    policy: crate::storage::Policy,
83
    /// Its new repositories' default bookmark (#371), and the site's.
84
    default_bookmark: String,
85
    site_bookmark: String,
86
}
87
88
async fn org_page(
89
    app: &Shared,
90
    shell: Shell,
91
    org: accounts::Organisation,
92
    notice: Option<String>,
93
    error: Option<String>,
94
) -> Result<Page<OrgSettingsPage>, Error> {
95
    let members = accounts::org_members(&app.db, org.id).await.map_err(|e| Error::from_db(shell.clone(), e))?;
96
    let mut teams = Vec::new();
97
    for t in accounts::teams(&app.db, org.id).await.map_err(|e| Error::from_db(shell.clone(), e))? {
98
        let members = accounts::team_members(&app.db, t.id).await.map_err(|e| Error::from_db(shell.clone(), e))?;
99
        teams.push((t, members));
100
    }
101
    let hooks = crate::db::hooks::hooks_for_org(&app.db, org.id).await.map_err(|e| Error::from_db(shell.clone(), e))?;
102
    let hooks = super::webhooks::rows(app, hooks).await.map_err(|e| Error::from_db(shell.clone(), e))?;
103
    let updates = super::runner_updates::views(app, &super::runner_updates::Reach::Org(org.name.clone()))
104
        .await
105
        .map_err(|e| Error::from_db(shell.clone(), e))?;
106
    let mut runners = Vec::new();
107
    for r in crate::db::runners::serving_owner(&app.db, &org.name).await.map_err(|e| Error::from_db(shell.clone(), e))? {
108
        let own = r.scope_owner == org.name;
109
        let serves = match r.scope_repo_id {
110
            Some(id) => crate::db::repos::get_by_id(&app.db, id).await.ok().flatten().map(|p| p.slug()).unwrap_or_default(),
111
            None if own => "the organisation's repositories".into(),
112
            None => "the whole site".into(),
113
        };
114
        runners.push((r, serves, own));
115
    }
116
    let assistants = super::assistants::panel(app, &org_assistants(&org)).await.map_err(|e| Error::from_db(shell.clone(), e))?;
117
    let variables = super::variables::panel(app, &org_assistants(&org)).await.map_err(|e| Error::from_db(shell.clone(), e))?;
118
    let secrets = super::secrets::panel(app, &org_assistants(&org)).await.map_err(|e| Error::from_db(shell.clone(), e))?;
119
    let policy = crate::storage::policy(&app.db, org.id).await.map_err(|e| Error::from_db(shell.clone(), e))?;
120
    let default_bookmark = crate::repo_setup::bookmark_setting(&app.db, crate::repo_setup::BookmarkScope::Org(org.id))
121
        .await
122
        .ok()
123
        .flatten()
124
        .unwrap_or_default();
125
    let site_bookmark = crate::repo_setup::bookmark_setting(&app.db, crate::repo_setup::BookmarkScope::Site)
126
        .await
127
        .ok()
128
        .flatten()
129
        .unwrap_or_else(|| crate::repo_setup::MAIN.into());
130
    Ok(Page(OrgSettingsPage {
131
        default_bookmark,
132
        site_bookmark,
133
        policy,
134
        shell,
135
        org,
136
        members,
137
        teams,
138
        hooks,
139
        notice,
140
        error,
141
        runners,
142
        updates,
143
        runner_token: None,
144
        assistants,
145
        variables,
146
        secrets,
147
    }))
148
}
149
150
pub(crate) async fn owned_org(
151
    app: &Shared,
152
    shell: &Shell,
153
    viewer: &Viewer,
154
    name: &str,
155
) -> Result<(accounts::Account, accounts::Organisation), Error> {
156
    let me = require(viewer, shell)?;
157
    let org = accounts::org_by_name(&app.db, name)
158
        .await
159
        .map_err(|e| Error::from_db(shell.clone(), e))?
160
        .ok_or_else(|| Error::not_found(shell.clone()))?;
161
    if !roles::is_org_owner(&app.db, &me.account, org.id).await.map_err(|e| Error::from_db(shell.clone(), e))? {
162
        return Err(Error::not_found(shell.clone()));
163
    }
164
    Ok((me.account, org))
165
}
166
167
pub async fn settings(State(app): State<Shared>, viewer: Viewer, Path(name): Path<String>) -> Result<Page<OrgSettingsPage>, Error> {
168
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
169
    let (_, org) = owned_org(&app, &shell, &viewer, &name).await?;
170
    org_page(&app, shell, org, None, None).await
171
}
172
173
#[derive(Deserialize)]
174
pub struct OrgForm {
175
    #[serde(rename = "_csrf")]
176
    csrf_token: String,
177
    display_name: String,
178
    description: String,
179
    default_role: String,
180
    /// Its new repositories' default bookmark (#371); empty, the site's.
181
    #[serde(default)]
182
    default_bookmark: String,
183
}
184
185
pub async fn update(
186
    State(app): State<Shared>,
187
    viewer: Viewer,
188
    Path(name): Path<String>,
189
    Form(f): Form<OrgForm>,
190
) -> Result<Page<OrgSettingsPage>, Error> {
191
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
192
    let (me, org) = owned_org(&app, &shell, &viewer, &name).await?;
193
    csrf(&viewer, &f.csrf_token, &shell)?;
194
    let role = if f.default_role == "none" || roles::Role::parse(&f.default_role).is_some() { f.default_role.as_str() } else { "read" };
195
    if let Err(m) =
196
        crate::repo_setup::set_bookmark_setting(&app.db, crate::repo_setup::BookmarkScope::Org(org.id), &f.default_bookmark).await
197
    {
198
        return org_page(&app, shell, org, None, Some(m)).await;
199
    }
200
    accounts::set_org(&app.db, org.id, f.display_name.trim(), f.description.trim(), role)
201
        .await
202
        .map_err(|e| Error::from_db(shell.clone(), e))?;
203
    accounts::audit(&app.db, Some(&me), "org.updated", &org.name, "").await.ok();
204
    let org = accounts::org_by_name(&app.db, &name).await.map_err(|e| Error::from_db(shell.clone(), e))?.unwrap_or(org);
205
    org_page(&app, shell, org, Some("Saved.".into()), None).await
206
}
207
208
#[derive(Deserialize)]
209
pub struct PriorityForm {
210
    #[serde(rename = "_csrf")]
211
    csrf_token: String,
212
    priority: String,
213
}
214
215
/// `POST /orgs/{name}/settings/flow-priority` (oxygenhub#244): its
216
/// repositories' Flow level unless one sets its own. Owners choose low,
217
/// normal or the site's; only site administrators raise one to high.
218
pub async fn flow_priority(
219
    State(app): State<Shared>,
220
    viewer: Viewer,
221
    Path(name): Path<String>,
222
    Form(f): Form<PriorityForm>,
223
) -> Result<Page<OrgSettingsPage>, Error> {
224
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
225
    let (me, org) = owned_org(&app, &shell, &viewer, &name).await?;
226
    csrf(&viewer, &f.csrf_token, &shell)?;
227
    let level = match f.priority.as_str() {
228
        "low" | "normal" | "high" => f.priority.as_str(),
229
        "default" => "",
230
        _ => return org_page(&app, shell, org, None, Some("Pick low, normal, high or the site's.".into())).await,
231
    };
232
    if level == "high" && !viewer.is_admin() {
233
        return org_page(&app, shell, org, None, Some("Only site administrators raise an organisation to high.".into())).await;
234
    }
235
    accounts::set_org_flow_priority(&app.db, org.id, level).await.map_err(|e| Error::from_db(shell.clone(), e))?;
236
    accounts::audit(&app.db, Some(&me), "org.flow_priority", &org.name, if level.is_empty() { "default" } else { level }).await.ok();
237
    let org = accounts::org_by_name(&app.db, &name).await.map_err(|e| Error::from_db(shell.clone(), e))?.unwrap_or(org);
238
    org_page(&app, shell, org, Some("Saved.".into()), None).await
239
}
240
241
#[derive(Deserialize)]
242
pub struct MemberForm {
243
    #[serde(rename = "_csrf")]
244
    csrf_token: String,
245
    username: String,
246
    #[serde(default)]
247
    role: String,
248
    #[serde(default)]
249
    remove: Option<String>,
250
}
251
252
pub async fn member(
253
    State(app): State<Shared>,
254
    viewer: Viewer,
255
    Path(name): Path<String>,
256
    Form(f): Form<MemberForm>,
257
) -> Result<Page<OrgSettingsPage>, Error> {
258
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
259
    let (me, org) = owned_org(&app, &shell, &viewer, &name).await?;
260
    csrf(&viewer, &f.csrf_token, &shell)?;
261
    let Some(target) = accounts::account_by_name(&app.db, f.username.trim()).await.map_err(|e| Error::from_db(shell.clone(), e))? else {
262
        return org_page(&app, shell, org, None, Some("No such account.".into())).await;
263
    };
264
    if f.remove.is_some() {
265
        let owners = accounts::org_members(&app.db, org.id)
266
            .await
267
            .map_err(|e| Error::from_db(shell.clone(), e))?
268
            .iter()
269
            .filter(|(_, r)| r == "owner")
270
            .count();
271
        let is_owner =
272
            accounts::org_role(&app.db, org.id, target.id).await.map_err(|e| Error::from_db(shell.clone(), e))?.as_deref() == Some("owner");
273
        if is_owner && owners <= 1 {
274
            return org_page(&app, shell, org, None, Some("An organisation keeps at least one owner.".into())).await;
275
        }
276
        accounts::remove_org_member(&app.db, org.id, target.id).await.map_err(|e| Error::from_db(shell.clone(), e))?;
277
        accounts::audit(&app.db, Some(&me), "org.member.removed", &org.name, &target.username).await.ok();
278
        return org_page(&app, shell, org, Some(format!("Removed {}.", target.username)), None).await;
279
    }
280
    let role = if f.role == "owner" { "owner" } else { "member" };
281
    accounts::set_org_member(&app.db, org.id, target.id, role).await.map_err(|e| Error::from_db(shell.clone(), e))?;
282
    accounts::audit(&app.db, Some(&me), "org.member.set", &org.name, &format!("{} {role}", target.username)).await.ok();
283
    org_page(&app, shell, org, Some(format!("{} is now {role}.", target.username)), None).await
284
}
285
286
#[derive(Deserialize)]
287
pub struct TeamForm {
288
    #[serde(rename = "_csrf")]
289
    csrf_token: String,
290
    team: String,
291
    #[serde(default)]
292
    username: String,
293
    #[serde(default)]
294
    action: String,
295
}
296
297
pub async fn team(
298
    State(app): State<Shared>,
299
    viewer: Viewer,
300
    Path(name): Path<String>,
301
    Form(f): Form<TeamForm>,
302
) -> Result<Page<OrgSettingsPage>, Error> {
303
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
304
    let (me, org) = owned_org(&app, &shell, &viewer, &name).await?;
305
    csrf(&viewer, &f.csrf_token, &shell)?;
306
    let team_name = f.team.trim().to_string();
307
    if !crate::repos::names::valid_segment(&team_name) {
308
        return org_page(&app, shell, org, None, Some("A team name is letters, digits, dots, dashes and underscores.".into())).await;
309
    }
310
    let existing = accounts::team_by_name(&app.db, org.id, &team_name).await.map_err(|e| Error::from_db(shell.clone(), e))?;
311
    let notice = match f.action.as_str() {
312
        "create" => {
313
            if existing.is_some() {
314
                return org_page(&app, shell, org, None, Some("That team exists.".into())).await;
315
            }
316
            accounts::create_team(&app.db, org.id, &team_name).await.map_err(|e| Error::from_db(shell.clone(), e))?;
317
            format!("Team {team_name} created.")
318
        }
319
        "delete" => {
320
            if let Some(t) = existing {
321
                accounts::delete_team(&app.db, t.id).await.map_err(|e| Error::from_db(shell.clone(), e))?;
322
            }
323
            format!("Team {team_name} deleted.")
324
        }
325
        "add" | "remove" => {
326
            let Some(t) = existing else { return org_page(&app, shell, org, None, Some("No such team.".into())).await };
327
            let Some(target) = accounts::account_by_name(&app.db, f.username.trim()).await.map_err(|e| Error::from_db(shell.clone(), e))?
328
            else {
329
                return org_page(&app, shell, org, None, Some("No such account.".into())).await;
330
            };
331
            if f.action == "add" {
332
                accounts::set_org_member(&app.db, org.id, target.id, "member").await.ok();
333
                accounts::add_team_member(&app.db, t.id, target.id).await.map_err(|e| Error::from_db(shell.clone(), e))?;
334
                format!("{} added to {team_name}.", target.username)
335
            } else {
336
                accounts::remove_team_member(&app.db, t.id, target.id).await.map_err(|e| Error::from_db(shell.clone(), e))?;
337
                format!("{} removed from {team_name}.", target.username)
338
            }
339
        }
340
        _ => return Err(Error::bad(shell, "unknown team action")),
341
    };
342
    accounts::audit(&app.db, Some(&me), "org.team", &org.name, &format!("{} {team_name} {}", f.action, f.username.trim())).await.ok();
343
    org_page(&app, shell, org, Some(notice), None).await
344
}
345
346
/// `POST /orgs/{name}/settings/avatar`: upload or remove the organisation's logo.
347
pub async fn avatar(
348
    State(app): State<Shared>,
349
    viewer: Viewer,
350
    Path(name): Path<String>,
351
    multipart: axum::extract::Multipart,
352
) -> Result<Page<OrgSettingsPage>, Error> {
353
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
354
    let (me, org) = owned_org(&app, &shell, &viewer, &name).await?;
355
    let upload = super::avatar::read_upload(multipart).await;
356
    csrf(&viewer, &upload.csrf, &shell)?;
357
    let result = super::avatar::apply(&app, "org", org.id, upload).await;
358
    if result.is_ok() {
359
        accounts::audit(&app.db, Some(&me), "org.picture", &org.name, "").await.ok();
360
    }
361
    let org = accounts::org_by_name(&app.db, &name).await.map_err(|e| Error::from_db(shell.clone(), e))?.unwrap_or(org);
362
    match result {
363
        Ok(m) => org_page(&app, shell, org, Some(m), None).await,
364
        Err(e) => org_page(&app, shell, org, None, Some(e)).await,
365
    }
366
}
367
368
/// `POST /orgs/{name}/settings/hooks`: webhooks that hear every repository of the organisation.
369
pub async fn hooks(
370
    State(app): State<Shared>,
371
    viewer: Viewer,
372
    Path(name): Path<String>,
373
    Form(f): Form<super::webhooks::HookForm>,
374
) -> Result<Page<OrgSettingsPage>, Error> {
375
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
376
    let (_, org) = owned_org(&app, &shell, &viewer, &name).await?;
377
    csrf(&viewer, &f.csrf_token, &shell)?;
378
    let scope = super::webhooks::Scope::Org { id: org.id, name: org.name.clone() };
379
    match super::webhooks::apply(&app, &viewer, &scope, f).await.map_err(|e| Error::from_db(shell.clone(), e))? {
380
        Ok(notice) => org_page(&app, shell, org, Some(notice), None).await,
381
        Err(error) => org_page(&app, shell, org, None, Some(error)).await,
382
    }
383
}
384
385
pub async fn deliveries(
386
    State(app): State<Shared>,
387
    viewer: Viewer,
388
    Path((name, id)): Path<(String, i64)>,
389
) -> Result<Page<super::webhooks::DeliveriesPage>, Error> {
390
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings/hooks/{id}/deliveries"), &viewer);
391
    let (_, org) = owned_org(&app, &shell, &viewer, &name).await?;
392
    let scope = super::webhooks::Scope::Org { id: org.id, name: org.name };
393
    super::webhooks::deliveries_page(&app, shell, &scope, None, id).await
394
}
395
396
pub async fn redeliver(
397
    State(app): State<Shared>,
398
    viewer: Viewer,
399
    Path((name, id, delivery)): Path<(String, i64, i64)>,
400
    Form(f): Form<super::session::CsrfForm>,
401
) -> Result<Page<super::webhooks::DeliveriesPage>, Error> {
402
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings/hooks/{id}/deliveries"), &viewer);
403
    let (_, org) = owned_org(&app, &shell, &viewer, &name).await?;
404
    csrf(&viewer, &f.csrf, &shell)?;
405
    let scope = super::webhooks::Scope::Org { id: org.id, name: org.name };
406
    super::webhooks::redeliver(&app, &scope, id, delivery).await.map_err(|e| Error::from_db(shell.clone(), e))?;
407
    super::webhooks::deliveries_page(&app, shell, &scope, None, id).await
408
}
409
410
/// `POST /orgs/{name}/settings/runners/token`: a one-time registration
411
/// token for a runner that serves the organisation's repositories
412
/// (oxygenhub#41).
413
pub async fn runner_token(
414
    State(app): State<Shared>,
415
    viewer: Viewer,
416
    Path(name): Path<String>,
417
    Form(f): Form<super::session::CsrfForm>,
418
) -> Result<Page<OrgSettingsPage>, Error> {
419
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
420
    let (me, org) = owned_org(&app, &shell, &viewer, &name).await?;
421
    csrf(&viewer, &f.csrf, &shell)?;
422
    let token = format!("flowreg_{}", crate::auth::random_hex(24));
423
    let scope = crate::db::runners::Scope { owner: org.name.clone(), repo_id: None };
424
    crate::db::runners::add_registration(&app.db, &blake3::hash(token.as_bytes()).to_hex(), &me.username, &scope)
425
        .await
426
        .map_err(|e| Error::from_db(shell.clone(), e))?;
427
    accounts::audit(&app.db, Some(&me), "flow.runner.token", &org.name, "registration token for the organisation").await.ok();
428
    let mut page = org_page(&app, shell, org, Some("Copy the token now; it is not shown again.".into()), None).await?;
429
    page.0.runner_token = Some(token);
430
    Ok(page)
431
}
432
433
/// `POST /orgs/{name}/settings/runners/{id}/remove`: remove a runner scoped
434
/// to the organisation or one of its repositories, when it runs nothing.
435
pub async fn runner_remove(
436
    State(app): State<Shared>,
437
    viewer: Viewer,
438
    Path((name, id)): Path<(String, i64)>,
439
    Form(f): Form<super::session::CsrfForm>,
440
) -> Result<Page<OrgSettingsPage>, Error> {
441
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
442
    let (me, org) = owned_org(&app, &shell, &viewer, &name).await?;
443
    csrf(&viewer, &f.csrf, &shell)?;
444
    let e = |e| Error::from_db(shell.clone(), e);
445
    let Some(r) = crate::db::runners::get(&app.db, id).await.map_err(e)?.filter(|r| r.scope_owner == org.name) else {
446
        return Err(Error::not_found(shell));
447
    };
448
    if crate::db::runners::running(&app.db).await.map_err(e)?.iter().any(|(rid, n)| *rid == id && *n > 0) {
449
        return org_page(&app, shell, org, None, Some(format!("{} is running jobs: wait for them to finish.", r.name))).await;
450
    }
451
    crate::db::runners::remove(&app.db, id).await.map_err(e)?;
452
    accounts::audit(&app.db, Some(&me), "flow.runner.removed", &r.name, &org.name).await.ok();
453
    org_page(&app, shell, org, Some(format!("{} removed: its credential no longer works.", r.name)), None).await
454
}
455
456
fn org_assistants(org: &accounts::Organisation) -> crate::levels::At {
457
    crate::levels::At::org(org)
458
}
459
460
/// `POST /orgs/{name}/settings/assistants`: the organisation's assistants
461
/// and prompt (#97).
462
pub async fn assistants(
463
    State(app): State<Shared>,
464
    viewer: Viewer,
465
    Path(name): Path<String>,
466
    Form(f): Form<super::assistants::AssistantForm>,
467
) -> Result<Page<OrgSettingsPage>, Error> {
468
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
469
    let (_, org) = owned_org(&app, &shell, &viewer, &name).await?;
470
    csrf(&viewer, &f.csrf_token, &shell)?;
471
    match super::assistants::apply(&app, &viewer, &org_assistants(&org), f).await.map_err(|e| Error::from_db(shell.clone(), e))? {
472
        Ok(notice) => org_page(&app, shell, org, Some(notice), None).await,
473
        Err(error) => org_page(&app, shell, org, None, Some(error)).await,
474
    }
475
}
476
477
#[derive(Deserialize)]
478
pub struct PolicyForm {
479
    #[serde(rename = "_csrf")]
480
    csrf_token: String,
481
    #[serde(default)]
482
    require_bucket: Option<String>,
483
    #[serde(default)]
484
    allowed: String,
485
}
486
487
/// `POST /orgs/{name}/settings/storage`: where the organisation's new
488
/// repositories may live (#96).
489
pub async fn storage(
490
    State(app): State<Shared>,
491
    viewer: Viewer,
492
    Path(name): Path<String>,
493
    Form(f): Form<PolicyForm>,
494
) -> Result<Page<OrgSettingsPage>, Error> {
495
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
496
    let (me, org) = owned_org(&app, &shell, &viewer, &name).await?;
497
    csrf(&viewer, &f.csrf_token, &shell)?;
498
    match crate::storage::set_policy(&app.db, org.id, f.require_bucket.is_some(), &f.allowed).await {
499
        Ok(p) => {
500
            let what = match (p.require_bucket, p.allowed.is_empty()) {
501
                (true, true) => "every new repository in a bucket".to_string(),
502
                (true, false) => format!("every new repository in {}", p.allowed.join(", ")),
503
                (false, true) => "any storage".to_string(),
504
                (false, false) => format!("buckets only in {}", p.allowed.join(", ")),
505
            };
506
            accounts::audit(&app.db, Some(&me), "org.storage_policy", &org.name, &what).await.ok();
507
            org_page(&app, shell, org, Some(format!("Storage policy saved: {what}.")), None).await
508
        }
509
        Err(e) => org_page(&app, shell, org, None, Some(format!("Storage policy: {e}."))).await,
510
    }
511
}
512
513
/// `POST /orgs/{name}/settings/variables`: the organisation's Flow
514
/// variables (#102).
515
pub async fn variables(
516
    State(app): State<Shared>,
517
    viewer: Viewer,
518
    Path(name): Path<String>,
519
    Form(f): Form<super::variables::VariableForm>,
520
) -> Result<Page<OrgSettingsPage>, Error> {
521
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
522
    let (_, org) = owned_org(&app, &shell, &viewer, &name).await?;
523
    csrf(&viewer, &f.csrf_token, &shell)?;
524
    match super::variables::apply(&app, &viewer, &org_assistants(&org), f).await.map_err(|e| Error::from_db(shell.clone(), e))? {
525
        Ok(notice) => org_page(&app, shell, org, Some(notice), None).await,
526
        Err(error) => org_page(&app, shell, org, None, Some(error)).await,
527
    }
528
}
529
530
/// `POST /orgs/{name}/settings/secrets`: the organisation's Flow secrets (#107).
531
pub async fn secrets(
532
    State(app): State<Shared>,
533
    viewer: Viewer,
534
    Path(name): Path<String>,
535
    Form(f): Form<super::secrets::SecretForm>,
536
) -> Result<Page<OrgSettingsPage>, Error> {
537
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
538
    let (_, org) = owned_org(&app, &shell, &viewer, &name).await?;
539
    csrf(&viewer, &f.csrf_token, &shell)?;
540
    match super::secrets::apply(&app, &viewer, &org_assistants(&org), f).await.map_err(|e| Error::from_db(shell.clone(), e))? {
541
        Ok(notice) => org_page(&app, shell, org, Some(notice), None).await,
542
        Err(error) => org_page(&app, shell, org, None, Some(error)).await,
543
    }
544
}
545
546
/// `POST /orgs/{name}/settings/assistants/models`: what a provider offers (#109).
547
pub async fn assistant_models(
548
    State(app): State<Shared>,
549
    viewer: Viewer,
550
    Path(name): Path<String>,
551
    Form(f): Form<super::assistants::ModelsForm>,
552
) -> Result<axum::Json<serde_json::Value>, Error> {
553
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
554
    let (_, org) = owned_org(&app, &shell, &viewer, &name).await?;
555
    csrf(&viewer, &f.csrf_token, &shell)?;
556
    Ok(axum::Json(super::assistants::models(&app, &org_assistants(&org), f).await.map_err(|e| Error::from_db(shell.clone(), e))?))
557
}
558
559
/// `POST /orgs/{name}/settings/runners/updates/policy`: the organisation's,
560
/// one of its test groups' or one of its runners' update settings.
561
pub async fn runner_updates_policy(
562
    State(app): State<Shared>,
563
    viewer: Viewer,
564
    Path(name): Path<String>,
565
    Form(f): Form<super::runner_updates::PolicyForm>,
566
) -> Result<Page<OrgSettingsPage>, Error> {
567
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
568
    let (me, org) = owned_org(&app, &shell, &viewer, &name).await?;
569
    csrf(&viewer, &f.csrf_token, &shell)?;
570
    let reach = super::runner_updates::Reach::Org(org.name.clone());
571
    let outcome = super::runner_updates::apply_policy(&app, &me, &reach, &f).await.map_err(|e| Error::from_db(shell.clone(), e))?;
572
    org_page_after(&app, shell, org, outcome).await
573
}
574
575
/// `POST /orgs/{name}/settings/runners/updates/groups`: a test group of the organisation's runners.
576
pub async fn runner_updates_group_new(
577
    State(app): State<Shared>,
578
    viewer: Viewer,
579
    Path(name): Path<String>,
580
    axum_extra::extract::Form(f): axum_extra::extract::Form<super::runner_updates::GroupForm>,
581
) -> Result<Page<OrgSettingsPage>, Error> {
582
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
583
    let (me, org) = owned_org(&app, &shell, &viewer, &name).await?;
584
    csrf(&viewer, &f.csrf_token, &shell)?;
585
    let reach = super::runner_updates::Reach::Org(org.name.clone());
586
    let outcome = super::runner_updates::apply_create_group(&app, &me, &reach, &f).await.map_err(|e| Error::from_db(shell.clone(), e))?;
587
    org_page_after(&app, shell, org, outcome).await
588
}
589
590
/// `POST /orgs/{name}/settings/runners/updates/groups/{id}`: who is in one of its test groups.
591
pub async fn runner_updates_group(
592
    State(app): State<Shared>,
593
    viewer: Viewer,
594
    Path((name, id)): Path<(String, i64)>,
595
    axum_extra::extract::Form(f): axum_extra::extract::Form<super::runner_updates::GroupForm>,
596
) -> Result<Page<OrgSettingsPage>, Error> {
597
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
598
    let (me, org) = owned_org(&app, &shell, &viewer, &name).await?;
599
    csrf(&viewer, &f.csrf_token, &shell)?;
600
    let reach = super::runner_updates::Reach::Org(org.name.clone());
601
    let outcome = super::runner_updates::apply_save_group(&app, &me, &reach, id, &f).await.map_err(|e| Error::from_db(shell.clone(), e))?;
602
    org_page_after(&app, shell, org, outcome).await
603
}
604
605
/// `POST /orgs/{name}/settings/runners/updates/groups/{id}/delete`.
606
pub async fn runner_updates_group_delete(
607
    State(app): State<Shared>,
608
    viewer: Viewer,
609
    Path((name, id)): Path<(String, i64)>,
610
    Form(f): Form<super::runner_updates::CsrfOnly>,
611
) -> Result<Page<OrgSettingsPage>, Error> {
612
    let shell = Shell::new(&app, &format!("/orgs/{name}/settings"), &viewer);
613
    let (me, org) = owned_org(&app, &shell, &viewer, &name).await?;
614
    csrf(&viewer, &f.csrf_token, &shell)?;
615
    let reach = super::runner_updates::Reach::Org(org.name.clone());
616
    let outcome = super::runner_updates::apply_delete_group(&app, &me, &reach, id).await.map_err(|e| Error::from_db(shell.clone(), e))?;
617
    org_page_after(&app, shell, org, outcome).await
618
}
619
620
/// The settings page saying what a change did.
621
async fn org_page_after(
622
    app: &Shared,
623
    shell: Shell,
624
    org: accounts::Organisation,
625
    outcome: super::runner_updates::Outcome,
626
) -> Result<Page<OrgSettingsPage>, Error> {
627
    match outcome {
628
        Ok(n) => org_page(app, shell, org, Some(n), None).await,
629
        Err(why) => org_page(app, shell, org, None, Some(why)).await,
630
    }
631
}