Skip to main content

mz_frontegg_mock/handlers/
tenant.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Mock implementations of the Frontegg vendor tenant API, as consumed by the
11//! `frontegg` crate's `Client`.
12//!
13//! Tenants exist only once created through the API; configured users do not
14//! imply tenants.
15
16use std::sync::Arc;
17
18use axum::Json;
19use axum::extract::{Path, State};
20use axum::http::StatusCode;
21use chrono::Utc;
22use serde::Deserialize;
23use uuid::Uuid;
24
25use crate::models::{TenantConfig, TenantResponse};
26use crate::server::Context;
27
28/// Handle GET /tenants/resources/tenants/v1
29///
30/// Lists all tenants, sorted by ID.
31pub async fn handle_list_tenants(State(context): State<Arc<Context>>) -> Json<Vec<TenantResponse>> {
32    let tenants = context.tenants.lock().unwrap();
33    Json(tenants.values().map(TenantResponse::from).collect())
34}
35
36/// Handle GET /tenants/resources/tenants/v1/{id}
37///
38/// Like Frontegg, responds with a JSON array: one element if the tenant
39/// exists, and empty otherwise. The `frontegg` client pops the single element
40/// and maps an empty array to a 404 error.
41pub async fn handle_get_tenant(
42    State(context): State<Arc<Context>>,
43    Path(id): Path<Uuid>,
44) -> Json<Vec<TenantResponse>> {
45    let tenants = context.tenants.lock().unwrap();
46    Json(
47        tenants
48            .get(&id)
49            .map(TenantResponse::from)
50            .into_iter()
51            .collect(),
52    )
53}
54
55/// Request body for creating a tenant, matching `frontegg::TenantRequest`.
56#[derive(Debug, Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct CreateTenantRequest {
59    #[serde(default = "Uuid::new_v4")]
60    pub tenant_id: Uuid,
61    pub name: String,
62    #[serde(default)]
63    pub metadata: serde_json::Value,
64    pub creator_name: Option<String>,
65    pub creator_email: Option<String>,
66}
67
68/// Handle POST /tenants/resources/tenants/v1
69///
70/// Creates a tenant. Fails with a 409 if a tenant with the ID already exists.
71pub async fn handle_create_tenant(
72    State(context): State<Arc<Context>>,
73    Json(body): Json<CreateTenantRequest>,
74) -> Result<Json<TenantResponse>, StatusCode> {
75    let now = Utc::now();
76    let metadata = match body.metadata {
77        serde_json::Value::Null => serde_json::json!({}),
78        metadata => metadata,
79    };
80    let tenant = TenantConfig {
81        id: body.tenant_id,
82        name: body.name,
83        metadata,
84        creator_name: body.creator_name,
85        creator_email: body.creator_email,
86        created_at: now,
87        updated_at: now,
88        deleted_at: None,
89    };
90
91    let mut tenants = context.tenants.lock().unwrap();
92    if tenants.contains_key(&tenant.id) {
93        return Err(StatusCode::CONFLICT);
94    }
95    let response = TenantResponse::from(&tenant);
96    tenants.insert(tenant.id, tenant);
97    Ok(Json(response))
98}
99
100/// Handle DELETE /tenants/resources/tenants/v1/{id}
101///
102/// Responds with an empty body, which is what the `frontegg` client expects.
103pub async fn handle_delete_tenant(
104    State(context): State<Arc<Context>>,
105    Path(id): Path<Uuid>,
106) -> StatusCode {
107    let removed = context.tenants.lock().unwrap().remove(&id);
108    match removed {
109        Some(_) => StatusCode::OK,
110        None => StatusCode::NOT_FOUND,
111    }
112}
113
114/// Request body for setting tenant metadata.
115#[derive(Debug, Deserialize)]
116pub struct SetTenantMetadataRequest {
117    pub metadata: serde_json::Value,
118}
119
120/// Handle POST /tenants/resources/tenants/v1/{id}/metadata
121///
122/// Merges the given keys into the tenant's metadata. Keys that are not
123/// mentioned are left in place, as in Frontegg.
124pub async fn handle_set_tenant_metadata(
125    State(context): State<Arc<Context>>,
126    Path(id): Path<Uuid>,
127    Json(body): Json<SetTenantMetadataRequest>,
128) -> Result<Json<TenantResponse>, StatusCode> {
129    let mut tenants = context.tenants.lock().unwrap();
130    let tenant = tenants.get_mut(&id).ok_or(StatusCode::NOT_FOUND)?;
131
132    match (tenant.metadata.as_object_mut(), body.metadata) {
133        (Some(existing), serde_json::Value::Object(new)) => existing.extend(new),
134        (_, new) => tenant.metadata = new,
135    }
136    tenant.updated_at = Utc::now();
137
138    Ok(Json(TenantResponse::from(&*tenant)))
139}
140
141/// Handle DELETE /tenants/resources/tenants/v1/{id}/metadata/{key}
142///
143/// Removes a single key from the tenant's metadata.
144pub async fn handle_delete_tenant_metadata(
145    State(context): State<Arc<Context>>,
146    Path((id, key)): Path<(Uuid, String)>,
147) -> Result<Json<TenantResponse>, StatusCode> {
148    let mut tenants = context.tenants.lock().unwrap();
149    let tenant = tenants.get_mut(&id).ok_or(StatusCode::NOT_FOUND)?;
150
151    if let Some(existing) = tenant.metadata.as_object_mut() {
152        existing.remove(&key);
153    }
154    tenant.updated_at = Utc::now();
155
156    Ok(Json(TenantResponse::from(&*tenant)))
157}