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