Skip to main content

auth_service/
admin.rs

1//! The auth service's admin API (see [`edda_server::admin`]): creating a
2//! campaign with its DM's setup code, a new setup code, what a campaign
3//! may keep and do, exporting and importing a campaign with its devices,
4//! and deleting one.
5
6use axum::{
7    Json, Router,
8    extract::{Path, State},
9    http::StatusCode,
10    routing::{delete, get, post, put},
11};
12use chrono::Duration;
13use edda_auth::protocol::{AuthExport, CampaignSettings, CreatedCampaign, NewCampaign, SetupCode};
14use edda_types::admin::{Deleted, Imported};
15use sqlx::PgPool;
16use uuid::Uuid;
17
18use crate::{Store, error::ApiError};
19
20/// How long a DM setup code works.
21pub const SETUP_CODE_LIFETIME: Duration = Duration::hours(24);
22
23/// The longest campaign name.
24const MAX_NAME_LENGTH: usize = 100;
25
26/// The admin API's routes.
27pub fn router(pool: PgPool) -> Router {
28    Router::new()
29        .route("/admin/campaigns", post(create))
30        .route("/admin/campaigns/{id}", delete(remove))
31        .route("/admin/campaigns/{id}/setup-code", post(setup_code))
32        .route(
33            "/admin/campaigns/{id}/settings",
34            get(settings).put(set_settings),
35        )
36        .route("/admin/campaigns/{id}/export", get(export))
37        .route("/admin/campaigns/{id}/import", put(import))
38        .with_state(Store::new(pool))
39}
40
41/// Creates a campaign, with a code for its DM's first device.
42async fn create(
43    State(store): State<Store>,
44    Json(new): Json<NewCampaign>,
45) -> Result<(StatusCode, Json<CreatedCampaign>), ApiError> {
46    let name = new.name.trim();
47    if name.is_empty() || name.chars().count() > MAX_NAME_LENGTH {
48        return Err(ApiError::BadRequest(format!(
49            "campaign names must be 1 to {MAX_NAME_LENGTH} characters"
50        )));
51    }
52    let campaign = store.create_campaign(name, new.edition).await?;
53    let setup_code = store
54        .new_setup_code(campaign.id, SETUP_CODE_LIFETIME)
55        .await?;
56    Ok((
57        StatusCode::CREATED,
58        Json(CreatedCampaign {
59            campaign,
60            setup_code,
61        }),
62    ))
63}
64
65/// A new code for a DM to set up a device with, as when every DM device
66/// is lost.
67async fn setup_code(
68    State(store): State<Store>,
69    Path(campaign): Path<Uuid>,
70) -> Result<Json<SetupCode>, ApiError> {
71    let code = store.new_setup_code(campaign, SETUP_CODE_LIFETIME).await?;
72    Ok(Json(SetupCode { code }))
73}
74
75/// Deletes a campaign with its devices; nothing, if it's gone already.
76async fn remove(
77    State(store): State<Store>,
78    Path(campaign): Path<Uuid>,
79) -> Result<Json<Deleted>, ApiError> {
80    let deleted = store
81        .delete_campaign(campaign)
82        .await?
83        .map_or(0, |(_, devices)| devices);
84    Ok(Json(Deleted { deleted }))
85}
86
87/// The campaign and its devices, to keep or to move to another server.
88async fn export(
89    State(store): State<Store>,
90    Path(campaign): Path<Uuid>,
91) -> Result<Json<AuthExport>, ApiError> {
92    store
93        .export_campaign(campaign)
94        .await?
95        .map(Json)
96        .ok_or(ApiError::NotFound)
97}
98
99/// Takes in a campaign exported from another server; `409` if it's here.
100async fn import(
101    State(store): State<Store>,
102    Path(campaign): Path<Uuid>,
103    Json(export): Json<AuthExport>,
104) -> Result<(StatusCode, Json<Imported>), ApiError> {
105    if export.campaign.id != campaign {
106        return Err(ApiError::BadRequest(
107            "that's another campaign's export".into(),
108        ));
109    }
110    let imported = store.import_campaign(&export).await?;
111    Ok((StatusCode::CREATED, Json(Imported { imported })))
112}
113
114/// What the campaign may keep and do.
115async fn settings(
116    State(store): State<Store>,
117    Path(campaign): Path<Uuid>,
118) -> Result<Json<CampaignSettings>, ApiError> {
119    store.campaign(campaign).await?;
120    Ok(Json(store.settings(campaign).await?))
121}
122
123/// Sets what the campaign may keep and do (read-only, or limits of its
124/// own); its devices' tokens carry it from their next login, within 15
125/// minutes.
126async fn set_settings(
127    State(store): State<Store>,
128    Path(campaign): Path<Uuid>,
129    Json(settings): Json<CampaignSettings>,
130) -> Result<Json<CampaignSettings>, ApiError> {
131    store.set_settings(campaign, &settings).await?;
132    Ok(Json(settings))
133}