refactor: remove GATEWAY_USER_TOKENS, fix review feedback

GATEWAY_USER_TOKENS never went to production — replaced entirely by
DB-backed user management via /api/admin/users and /api/tokens.

Removed:
- UserTokenConfig struct and GATEWAY_USER_TOKENS env var parsing
- user_tokens field from GatewayConfig
- GatewayChannel::new_multi_auth() constructor
- Env-var user migration block in main.rs (~90 lines)
- multi_tenant auto-detection from GATEWAY_USER_TOKENS (now runtime
  via db.has_any_users() in app.rs)

Review fixes (zmanian):
- User ID generation: UUID instead of display-name derivation (#1)
- Invitation accept moved to public router (no auth needed) (#3)
- libSQL get_invitation_by_hash aligned with postgres: filters
  status='pending' AND expires_at > now (#4)
- UUID parse: returns DatabaseError::Serialization instead of
  unwrap_or_default (#7)
- PostgreSQL SELECT * replaced with explicit column lists (#8)
- Sort order aligned (both backends use DESC) (#6)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
2026-03-24 22:34:05 -07:00
co-authored by Claude Opus 4.6
parent 240cee82e1
commit aa35c2e1ce
13 changed files with 74 additions and 255 deletions
+6 -1
View File
@@ -69,6 +69,11 @@ impl MultiAuthState {
}
/// Create a multi-user auth state from a map of tokens to identities.
///
/// **Test-only** — production multi-user auth is DB-backed via
/// `DbAuthenticator`. This constructor is kept public (not `#[cfg(test)]`)
/// because integration tests in `tests/` compile the crate as a library
/// where `cfg(test)` is not set.
pub fn multi(tokens: HashMap<String, UserIdentity>) -> Self {
let hashed_tokens: Vec<([u8; 32], UserIdentity)> = tokens
.into_iter()
@@ -188,7 +193,7 @@ impl DbAuthenticator {
/// Combined auth state: tries env-var tokens first, then DB-backed tokens.
#[derive(Clone)]
pub struct CombinedAuthState {
/// In-memory tokens from GATEWAY_USER_TOKENS or GATEWAY_AUTH_TOKEN.
/// In-memory tokens from GATEWAY_AUTH_TOKEN.
pub env_auth: MultiAuthState,
/// DB-backed token authenticator (optional — only when a database is available).
pub db_auth: Option<DbAuthenticator>,
+1 -6
View File
@@ -162,12 +162,7 @@ pub async fn invitations_accept_handler(
return Err((StatusCode::GONE, "Invitation has expired".to_string()));
}
// Generate a user id from the display name.
let new_user_id = display_name
.to_ascii_lowercase()
.split_whitespace()
.collect::<Vec<_>>()
.join("-");
let new_user_id = Uuid::new_v4().to_string();
let now = chrono::Utc::now();
let user_record = UserRecord {
+2 -10
View File
@@ -7,6 +7,7 @@ use axum::{
extract::{Path, State},
http::StatusCode,
};
use uuid::Uuid;
use crate::channels::web::auth::AuthenticatedUser;
use crate::channels::web::server::GatewayState;
@@ -34,16 +35,7 @@ pub async fn users_create_handler(
let email = body.get("email").and_then(|v| v.as_str()).map(String::from);
// Generate user id: prefer email if provided, otherwise derive from display_name.
let user_id = if let Some(ref e) = email {
e.clone()
} else {
display_name
.to_ascii_lowercase()
.split_whitespace()
.collect::<Vec<_>>()
.join("-")
};
let user_id = Uuid::new_v4().to_string();
let now = chrono::Utc::now();
let user_record = UserRecord {
-43
View File
@@ -124,49 +124,6 @@ impl GatewayChannel {
}
}
/// Create a gateway channel with a pre-built multi-user auth state.
pub fn new_multi_auth(config: GatewayConfig, auth: MultiAuthState) -> Self {
let auth = CombinedAuthState {
env_auth: auth,
db_auth: None,
};
let state = Arc::new(GatewayState {
msg_tx: tokio::sync::RwLock::new(None),
sse: Arc::new(SseManager::new()),
workspace: None,
workspace_pool: None,
session_manager: None,
log_broadcaster: None,
log_level_handle: None,
extension_manager: None,
tool_registry: None,
store: None,
job_manager: None,
prompt_queue: None,
scheduler: None,
default_user_id: config.user_id.clone(),
shutdown_tx: tokio::sync::RwLock::new(None),
ws_tracker: Some(Arc::new(ws::WsConnectionTracker::new())),
llm_provider: None,
skill_registry: None,
skill_catalog: None,
chat_rate_limiter: server::PerUserRateLimiter::new(30, 60),
oauth_rate_limiter: server::RateLimiter::new(10, 60),
registry_entries: Vec::new(),
cost_guard: None,
routine_engine: Arc::new(tokio::sync::RwLock::new(None)),
startup_time: std::time::Instant::now(),
webhook_rate_limiter: server::RateLimiter::new(10, 60),
active_config: server::ActiveConfigSnapshot::default(),
});
Self {
config,
state,
auth,
}
}
/// Helper to rebuild state, copying existing fields and applying a mutation.
fn rebuild_state(&mut self, mutate: impl FnOnce(&mut GatewayState)) {
let mut new_state = GatewayState {
+5 -4
View File
@@ -417,6 +417,11 @@ pub async fn start_server(
.route(
"/api/webhooks/u/{user_id}/{path}",
post(crate::channels::web::handlers::webhooks::webhook_trigger_user_scoped_handler),
)
// Invitation accept (public — validated by invite token, not user auth)
.route(
"/api/invitations/accept",
post(super::handlers::invitations::invitations_accept_handler),
);
// Protected routes (require auth)
@@ -545,10 +550,6 @@ pub async fn start_server(
get(super::handlers::invitations::invitations_list_handler)
.post(super::handlers::invitations::invitations_create_handler),
)
.route(
"/api/invitations/accept",
post(super::handlers::invitations::invitations_accept_handler),
)
// Gateway control plane
.route("/api/gateway/status", get(gateway_status_handler))
// OpenAI-compatible API