Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
a890511
chore: pin the core dependency to a tag instead of tracking main (#25)
fylorn Sep 23, 2026
a90e9f3
feat!: forward what can be forwarded, convert only what must be (#26)
fylorn Sep 23, 2026
ca7647c
refactor: take the circuit-breaker registry and metric labels back fr…
fylorn Sep 23, 2026
a6678cf
refactor: redact PII with core's guard engine, and restore tool argum…
fylorn Sep 23, 2026
437a671
feat: inspect the tool calls an upstream returns (#29)
fylorn Sep 23, 2026
038796b
build: keep only line tables in dev and test builds (#30)
fylorn Sep 24, 2026
05f52e8
feat: one circuit-breaker state machine, and hidden characters in req…
fylorn Sep 24, 2026
8a76395
fix: make the integration suite pass again (#32)
fylorn Sep 24, 2026
6482690
test: fix the two flaky integration tests (#33)
fylorn Sep 24, 2026
9f46116
fix: bill a Chat stream whose caller did not ask for usage (#34)
fylorn Sep 24, 2026
3c35ab8
fix: a request the upstream refuses no longer fails over or trips bre…
fylorn Sep 24, 2026
234fcce
Merge main (v1.1.0) into dev
fylorn Sep 24, 2026
82f8061
docs: cut releases from a release branch, and keep the tag's headings…
fylorn Sep 24, 2026
bd57b5d
ci: run checks on pull requests into dev, including the integration s…
fylorn Sep 24, 2026
c1b2085
fix: bill cached input at cache prices, estimate missing usage, strip…
fylorn Sep 24, 2026
ad5b75b
refactor: take back what only this side used from core (#41)
fylorn Sep 24, 2026
3a617f3
feat(gateway): take official hosts, output fallback and error shapes …
fylorn Sep 24, 2026
ed1c8b5
feat(gateway): accept Gemini clients, and the Responses API over a We…
fylorn Sep 24, 2026
28d4387
test: count the probes that miss before the streaming cache hit (#44)
fylorn Sep 24, 2026
d3f36fc
refactor(server): move the catalog's SQL into repositories (#45)
fylorn Sep 24, 2026
83e9bcc
refactor(server): move the dashboard, limits and log-forwarding handl…
fylorn Sep 24, 2026
9b5b326
refactor(server): move the MCP handlers' SQL into repositories (#50)
fylorn Sep 24, 2026
86beb82
refactor(gateway): run the request guards on core's tw-guard engines …
fylorn Sep 24, 2026
1c80b83
refactor(server): move the identity handlers' SQL into repositories (…
fylorn Sep 24, 2026
c25b44f
fix(mcp): revoking a default connection no longer fails with a 500 (#51)
fylorn Sep 24, 2026
dbce845
test: leave the cancelled stream after it has started, not on a timer…
fylorn Sep 24, 2026
c3d2d57
refactor(server): move the access handlers' SQL into repositories (#48)
fylorn Sep 24, 2026
d92c2d1
fix(settings): read security.totp_required as a boolean, and seed aut…
fylorn Sep 24, 2026
7e95183
feat(gateway): record a client that leaves before its response exists…
fylorn Sep 24, 2026
367df3c
feat(auth): enforce the TOTP requirement (#55)
fylorn Sep 24, 2026
443de8d
feat(gateway): keep the last response on a Responses WebSocket (#56)
fylorn Sep 24, 2026
6890c72
Merge main (v2.0.0) into dev
fylorn Sep 24, 2026
d4f6d5a
docs(release): CI runs the integration suite on the release PR (#58)
fylorn Sep 24, 2026
ef3de87
docs(contributing): point vulnerability reports at the organization's…
fylorn Sep 25, 2026
c4f8b1c
docs(readme): current description of ThinkWatch Lite and ThinkWatch C…
fylorn Sep 25, 2026
f259962
feat(providers): authenticate Bedrock with an API key (#61)
fylorn Sep 28, 2026
78f9ab5
test: hold the early-cancel client once its key is known, not on a ti…
fylorn Sep 28, 2026
1b752f2
feat(providers): list Bedrock's models, and let the route editor take…
fylorn Sep 28, 2026
1f08421
fix(bedrock): refuse keys that will not decrypt, and keep instance-ro…
fylorn Sep 28, 2026
e480bfd
refactor(bedrock): use core's shared tw-bedrock, and core v0.55.0 (#65)
fylorn Sep 29, 2026
e1e7999
docs(readme): rewrite both READMEs to be short and accurate (#66)
fylorn Sep 30, 2026
af9eb81
Merge main (v2.1.0) into dev
fylorn Sep 30, 2026
ed6dd39
docs(readme): 37 MCP templates, what the setup wizard does, body reda…
fylorn Sep 30, 2026
9afcc08
fix(rbac): make the seeded team_manager work at team scope (#69)
fylorn Sep 30, 2026
fa3a5b9
fix(rbac): require gateway use, and count only grants that give it (#70)
fylorn Sep 30, 2026
a1eed6f
Merge main (v2.2.0) into dev
fylorn Sep 30, 2026
da88b09
Merge main (sponsor line) into dev
fylorn Oct 2, 2026
1a399e7
Allow clippy::double_must_use on the async_trait BlobStore (#73)
fylorn Oct 2, 2026
8d8d96f
Unify the request guards with thinkwatch-core's rule model (#72)
fylorn Oct 3, 2026
2baa123
Fix what the pre-release review of the guard unification found (#75)
fylorn Oct 3, 2026
33c22dc
Merge main (v3.0.0) into dev
fylorn Oct 3, 2026
dcc8616
Merge main (README: ThinkWatch Lite for individual developers) into dev
fylorn Oct 4, 2026
6b73344
Merge main (v3.1.0) into dev
fylorn Oct 5, 2026
03c393c
Usage limits: token limits refuse, key limits count on the key, Retry…
fylorn Oct 5, 2026
4075855
fix(redis): connect over TLS for rediss:// URLs (#82)
fylorn Oct 5, 2026
33234ee
Concurrent startup, body retention on restart, and Helm network polic…
fylorn Oct 5, 2026
b7a34e1
Merge main (v3.2.0) into dev
fylorn Oct 5, 2026
2ce6471
chore(deps): update rust crate rand to v0.10.1 [security] (#77)
renovate[bot] Oct 6, 2026
0682864
chore(deps): update rust crate xxhash-rust to v0.8.16 [security] (#78)
renovate[bot] Oct 6, 2026
1fbe7bd
chore(deps): take the Renovate dashboard upgrades (#85)
fylorn Oct 6, 2026
2e23fd9
chore(deps): pnpm 12, and ClickHouse row validation on (#86)
fylorn Oct 6, 2026
977cab0
ci: save Rust caches only on pushes, and let releases read CI's image…
fylorn Oct 8, 2026
a19ed65
Merge main (v3.2.1) into dev
fylorn Oct 8, 2026
ea2a81f
Merge main (v3.3.0) into dev
fylorn Oct 9, 2026
e8ae1f1
Stream endings and in-stream errors logged like answers; core v0.68.0…
fylorn Oct 10, 2026
0e070a9
Merge main (v3.3.1) into dev
fylorn Oct 10, 2026
633ad90
Merge main (v3.4.0) into dev
fylorn Oct 10, 2026
235e38b
fix(limits): enforce route caps and close the places limits leaked (#94)
fylorn Oct 10, 2026
31cbfc8
feat: GET /v1/usage answers for the key or its owner; API key limits …
fylorn Oct 10, 2026
8151cee
Merge main (v3.5.0) into dev
fylorn Oct 10, 2026
721b5ee
Merge main (README zh) into dev
fylorn Oct 10, 2026
4f87839
feat(console): Add to ThinkWatch Lite; gateway address for every user…
fylorn Oct 11, 2026
042e605
Merge main (v3.6.0) into dev
fylorn Oct 11, 2026
ebdb9dc
chore: add a contributor license agreement signed by PR comment (#102)
fylorn Oct 11, 2026
133d610
ci: name the CLA job cla (#103)
fylorn Oct 12, 2026
eef9ff4
Merge main (3.7.0) back into dev
fylorn Oct 12, 2026
30c5f5b
Merge main (3.7.1) back into dev
fylorn Oct 12, 2026
12f4f82
Merge main (3.8.0) back into dev
fylorn Oct 13, 2026
7ef4ff0
Merge main (3.8.1) back into dev
fylorn Oct 13, 2026
72888a4
Bump rustls to 0.23.46 (RUSTSEC-2026-0346 to -0349) (#108)
fylorn Oct 15, 2026
98b6790
Merge main (3.8.2) back into dev
fylorn Oct 16, 2026
d86570c
Tool inspection: decide on settled input, record whole calls (core v0…
fylorn Oct 19, 2026
a9c5150
chore(deps): update node.js to v24.22.0 (#111)
renovate[bot] Oct 19, 2026
57369f6
chore(deps): update pnpm to v12.14.0 (#99)
renovate[bot] Oct 19, 2026
9549fd6
chore(deps): update ghcr.io/zitadel/zitadel docker tag to v4.19.5 (#110)
renovate[bot] Oct 19, 2026
57c05d9
Merge main (3.8.3) back into dev
fylorn Oct 20, 2026
ff8ccf4
feat(web): carry a key's request limits in the Add to ThinkWatch Lite…
fylorn Oct 20, 2026
17b7e9c
Fix the rotation comment: key limits follow the lineage (#116)
fylorn Oct 20, 2026
caa6751
fix(auth): answer database failures in auth middleware with a 503, no…
fylorn Oct 20, 2026
fd13601
Run Argon2 on the blocking pool in request handlers (#118)
fylorn Oct 20, 2026
3e769f3
fix(auth): run at most one Argon2 per CPU at a time
fylorn Oct 20, 2026
c6c59ce
chore(release): tag 3.9.0
fylorn Oct 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,54 @@ target.

## [Unreleased]

## [3.9.0] — 2026-10-20

The Add to ThinkWatch Lite link from a rotated key now carries the key's
request limits. A database failure during authentication answers 503 or
500 with the usual JSON error instead of an empty 500, and password
hashing no longer runs on the request workers. Monitoring that matches
only on 500 sees a database outage during authentication as a 503 from
this release on. The thinkwatch-core crates stay at v0.77.0.

No setting, schema or API route changes.

### Added

- **The rotated-key dialog's Add to ThinkWatch Lite link carries the key's
request limits.** The key's own enabled, unexpired AI-gateway rules that
count requests over a window of whole minutes up to 60, or whole hours
up to 24 (the console's 1 minute, 5 minutes, 1 hour, 5 hours and 24
hours among them), go into the link's `limits` parameter as Lite's
rolling windows of the same length; of two rules on one window the
smaller count goes, and at most 16 windows, those allowing the fewest
requests per second. Token rules stay out, since they count weighted
tokens and Lite counts plain ones; so do sliding 7-day rules (Lite's
week is a calendar week), budgets, MCP rules and the owner's limits.
A link with limits needs ThinkWatch Lite 2026.10.26 or later, which the
dialog states under the button. Reading the rules needs
`rate_limits:read` in a scope covering the key; without it, or when the
rules cannot be read, the link is built without limits as before. A new
key has no limits of its own yet, so its link carries none.

### Fixed

- **A database outage during authentication is a 503 with an error body,
not an empty 500.** When the session or API-key check could not reach
Postgres (most often a connection-pool timeout), the console and the
gateway answered `500 Internal Server Error` with no body and, for the
permission lookup, no log line. These lookups now answer like every
handler does: `503 Service Unavailable` with the usual JSON error when
the database is unavailable, `500` with it for any other database
error, and an error log naming the lookup that failed.
- **Password hashing no longer holds up other requests.** Logins,
password changes and resets, new users, turning off two-factor
authentication and first-run setup ran Argon2 (tens of milliseconds of
CPU by design) on the server's async workers, so every other request
scheduled on that worker waited for it. They now run it on the blocking
thread pool, at most one run per CPU at a time as before, so a burst of
logins holds no more memory than it did (19 MiB per run). The hashes
and the constant-time login are unchanged.

## [3.8.3] — 2026-10-20

Tool-call inspection in streamed answers now decides only on arguments
Expand Down Expand Up @@ -2489,7 +2537,8 @@ unreleased builds should: stop the gateway, run `db/schema.sql`
against PostgreSQL, restart against this tag. The schema is
idempotent end-to-end, so the apply is safe to repeat.

[Unreleased]: https://github.com/ThinkWatchProject/ThinkWatch/compare/v3.8.3...HEAD
[Unreleased]: https://github.com/ThinkWatchProject/ThinkWatch/compare/v3.9.0...HEAD
[3.9.0]: https://github.com/ThinkWatchProject/ThinkWatch/releases/tag/v3.9.0
[3.8.3]: https://github.com/ThinkWatchProject/ThinkWatch/releases/tag/v3.8.3
[3.8.2]: https://github.com/ThinkWatchProject/ThinkWatch/releases/tag/v3.8.2
[3.8.1]: https://github.com/ThinkWatchProject/ThinkWatch/releases/tag/v3.8.1
Expand Down
12 changes: 6 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ members = [
]

[workspace.package]
version = "3.8.3"
version = "3.9.0"
edition = "2024"
# The MSRV: the newest `rust-version` among the locked dependencies
# (sqlx 0.9 and the aws-smithy crates under aws-sigv4). Without it,
Expand Down
98 changes: 98 additions & 0 deletions crates/auth/src/password.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use std::sync::{Arc, LazyLock};

use argon2::{
Argon2,
password_hash::{PasswordHasher, PasswordVerifier, phc::PasswordHash},
};
use tokio::sync::Semaphore;

const RANDOM_PASSWORD_LEN: usize = 16;
const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%&*";
Expand Down Expand Up @@ -52,10 +55,105 @@ pub fn verify_password_or_dummy(password: &str, hash: Option<&str>) -> bool {
}
}

// Argon2 costs tens of milliseconds of CPU on purpose. Called from a
// request handler it would hold an async worker for that long, stalling
// every other request scheduled on that thread, so the handlers use
// these versions, which run it on the blocking pool.

/// How many Argon2 runs go at once: one per CPU, as many as the async
/// workers allowed when they ran it. Each run holds 19 MiB (`m=19456`)
/// while it lasts and the blocking pool grows to hundreds of threads, so
/// without this a burst of logins would hold gigabytes at once.
static ARGON2_PERMITS: LazyLock<Arc<Semaphore>> = LazyLock::new(|| {
Arc::new(Semaphore::new(
std::thread::available_parallelism().map_or(1, |n| n.get()),
))
});

/// `f` on the blocking pool once an [`ARGON2_PERMITS`] place is free. The
/// task keeps the place until it ends, even when the caller has gone.
async fn on_blocking_pool<T: Send + 'static>(
f: impl FnOnce() -> T + Send + 'static,
) -> Result<T, tokio::task::JoinError> {
// Never closed, so the acquire cannot fail; the permit inside the
// result is held all the same.
let permit = Arc::clone(&ARGON2_PERMITS).acquire_owned().await;
tokio::task::spawn_blocking(move || {
let _permit = permit;
f()
})
.await
}

/// [`hash_password`] on the blocking pool.
pub async fn hash_password_off_runtime(password: String) -> anyhow::Result<String> {
on_blocking_pool(move || hash_password(&password))
.await
.map_err(|e| anyhow::anyhow!("Password hashing task failed: {e}"))?
}

/// [`verify_password`] on the blocking pool.
pub async fn verify_password_off_runtime(password: String, hash: String) -> anyhow::Result<bool> {
on_blocking_pool(move || verify_password(&password, &hash))
.await
.map_err(|e| anyhow::anyhow!("Password verification task failed: {e}"))?
}

/// [`verify_password_or_dummy`] on the blocking pool. A task that cannot
/// run counts as a mismatch, like an unreadable hash.
pub async fn verify_password_or_dummy_off_runtime(password: String, hash: Option<String>) -> bool {
on_blocking_pool(move || verify_password_or_dummy(&password, hash.as_deref()))
.await
.unwrap_or(false)
}

#[cfg(test)]
mod tests {
use super::*;

#[tokio::test]
async fn the_off_runtime_versions_agree_with_the_blocking_ones() {
let hash = hash_password_off_runtime("pw".into()).await.unwrap();
assert!(
verify_password_off_runtime("pw".into(), hash.clone())
.await
.unwrap()
);
assert!(
!verify_password_off_runtime("nope".into(), hash.clone())
.await
.unwrap()
);
assert!(verify_password_or_dummy_off_runtime("pw".into(), Some(hash)).await);
assert!(!verify_password_or_dummy_off_runtime("pw".into(), None).await);
}

#[tokio::test(flavor = "multi_thread")]
async fn no_more_runs_at_once_than_there_are_permits() {
use std::sync::atomic::{AtomicUsize, Ordering};
static RUNNING: AtomicUsize = AtomicUsize::new(0);
static PEAK: AtomicUsize = AtomicUsize::new(0);
let permits = std::thread::available_parallelism().map_or(1, |n| n.get());
let tasks: Vec<_> = (0..permits * 4)
.map(|_| {
tokio::spawn(on_blocking_pool(|| {
let now = RUNNING.fetch_add(1, Ordering::SeqCst) + 1;
PEAK.fetch_max(now, Ordering::SeqCst);
std::thread::sleep(std::time::Duration::from_millis(20));
RUNNING.fetch_sub(1, Ordering::SeqCst);
}))
})
.collect();
for task in tasks {
task.await.unwrap().unwrap();
}
let peak = PEAK.load(Ordering::SeqCst);
assert!(
(1..=permits).contains(&peak),
"peak {peak}, permits {permits}"
);
}

/// A hash written by argon2 0.5.3 (`Argon2::default()`, fixed salt).
/// Stored hashes outlive crate upgrades: they must keep verifying.
const KAT_HASH: &str = "$argon2id$v=19$m=19456,t=2,p=1$dGhpbmt3YXRjaC1rYXQtc2FsdA$6xffcgfh1b9KJMWg4mBS4qOWsK5MNUM1asLNHvgV0v8";
Expand Down
4 changes: 2 additions & 2 deletions crates/server/src/handlers/admin/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@ pub async fn create_user(
return Err(AppError::Conflict("Email already registered".into()));
}

let password_hash = password::hash_password(&raw_password)?;
let password_hash = password::hash_password_off_runtime(raw_password.clone()).await?;

let mut tx = state.db.begin().await?;

Expand Down Expand Up @@ -920,7 +920,7 @@ pub async fn reset_user_password(
}

let new_password = password::generate_random_password();
let hash = password::hash_password(&new_password)?;
let hash = password::hash_password_off_runtime(new_password.clone()).await?;

user_repository::update_password_hash(&state.db, user_id, &hash, true).await?;

Expand Down
9 changes: 4 additions & 5 deletions crates/server/src/handlers/api_keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -927,11 +927,10 @@ pub async fn rotate_key(
.await;
let grace_period_ends_at = chrono::Utc::now() + chrono::Duration::hours(grace_hours);

// Generate new key. Rate-limit / budget rules attached to the
// OLD key id are NOT copied here — they live in `rate_limit_rules`
// / `budget_caps` keyed by api_key UUID. The follow-up phase
// ("limits attached to api_keys") will add an explicit copy step
// when rotation happens.
// Generate new key. Nothing to copy for limits: the key's
// rate-limit rules and budgets are attached to its `lineage_id`
// (`api_key_lineage` subjects, see `think_watch_common::limits`),
// and the new key keeps that lineage, so they carry over as is.
let generated = api_key::generate_api_key();

// The new key carries the old one's name verbatim — the chain is
Expand Down
19 changes: 11 additions & 8 deletions crates/server/src/handlers/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -497,10 +497,11 @@ pub async fn login(
// Constant-time login: always run Argon2, against a dummy hash when
// there is no account or it has no password, so the response time does
// not tell which addresses have accounts.
let password_valid = password::verify_password_or_dummy(
&req.password,
user.as_ref().and_then(|u| u.password_hash.as_deref()),
);
let password_valid = password::verify_password_or_dummy_off_runtime(
req.password.clone(),
user.as_ref().and_then(|u| u.password_hash.clone()),
)
.await;

if !password_valid || user.is_none() {
// Per-email cross-IP failure counter. The `count` above is keyed
Expand Down Expand Up @@ -1053,7 +1054,7 @@ pub async fn register(
let email = normalize_email(&req.email);
validate_email(&email)?;

let password_hash = password::hash_password(&req.password)?;
let password_hash = password::hash_password_off_runtime(req.password.clone()).await?;

// Use a transaction to ensure user creation + role assignment are atomic
let mut tx = state.db.begin().await?;
Expand Down Expand Up @@ -1467,7 +1468,9 @@ pub async fn change_password(
return Err(AppError::Unauthorized);
}

if !password::verify_password(&req.old_password, current_hash)? {
if !password::verify_password_off_runtime(req.old_password.clone(), current_hash.to_owned())
.await?
{
let fails =
crate::services::auth_lockout::record_failure(&state.redis, &pwchange_fail_key).await?;
if let Some(secs) = crate::services::auth_lockout::ladder_secs(fails) {
Expand All @@ -1481,7 +1484,7 @@ pub async fn change_password(
crate::services::auth_lockout::clear(&state.redis, &[&pwchange_fail_key, &pwchange_locked_key])
.await;

let new_hash = password::hash_password(&req.new_password)?;
let new_hash = password::hash_password_off_runtime(req.new_password.clone()).await?;
repo::set_own_password(&state.db, user.id, &new_hash).await?;

// Revoke all signing public keys for this user (invalidates sessions)
Expand Down Expand Up @@ -1832,7 +1835,7 @@ pub async fn totp_disable(
let hash = user.password_hash.as_ref().ok_or(AppError::BadRequest(
"SSO accounts cannot manage TOTP here".into(),
))?;
if !password::verify_password(&req.old_password, hash)? {
if !password::verify_password_off_runtime(req.old_password.clone(), hash.to_owned()).await? {
return Err(AppError::Unauthorized);
}

Expand Down
2 changes: 1 addition & 1 deletion crates/server/src/handlers/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ pub async fn setup_initialize(

// Create the super_admin user with the first API key, and mark
// setup done.
let password_hash = password::hash_password(&req.admin.password)?;
let password_hash = password::hash_password_off_runtime(req.admin.password.clone()).await?;
let generated = api_key::generate_api_key();
let site_name = req.site_name.as_deref().unwrap_or("ThinkWatch");
let admin_user = repo::create_first_admin(
Expand Down
25 changes: 19 additions & 6 deletions crates/server/src/middleware/api_key_auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,9 +365,16 @@ fn authenticate(
.bind(&key_hash)
.bind(global_inactivity_days)
.fetch_optional(&state.db)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.ok_or(StatusCode::UNAUTHORIZED)?;
.await;
let row = match row {
Ok(row) => row.ok_or(StatusCode::UNAUTHORIZED)?,
Err(e) => {
return Ok(super::auth_guard::db_failure_response(
"Gateway API key lookup failed",
e,
));
}
};

// Surface gate. Even a valid key gets rejected if its
// `surfaces` list doesn't include the gateway being called.
Expand Down Expand Up @@ -433,9 +440,15 @@ fn authenticate(
// the aggregated `surface_constraints` JSON so the gateway
// hot path has rate limits + budgets without further lookups.
let (role_limits, user_roles, loaded_limits) = if let Some(uid) = row.user_id {
let limits = rbac::compute_user_resource_limits(&state.db, uid)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let limits = match rbac::compute_user_resource_limits(&state.db, uid).await {
Ok(limits) => limits,
Err(e) => {
return Ok(super::auth_guard::db_failure_response(
"Loading the key owner's resource limits failed",
e,
));
}
};
let names = rbac::load_user_role_names(&state.db, uid)
.await
.unwrap_or_default();
Expand Down
Loading
Loading