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