feat(auth): add DB-based token authentication for dashboard-created clients
Add client_tokens table with auto-generated sk-{hex} tokens so clients
created in the dashboard get working API keys. Auth flow: DB token lookup
first, then env token fallback, then permissive mode. Includes token
management CRUD endpoints and copy-once reveal modal in the frontend.
This commit is contained in:
@@ -304,6 +304,7 @@ pub mod middleware {
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
};
|
||||
use sqlx;
|
||||
|
||||
/// Rate limiting middleware
|
||||
pub async fn rate_limit_middleware(
|
||||
@@ -311,8 +312,11 @@ pub mod middleware {
|
||||
request: Request,
|
||||
next: Next,
|
||||
) -> Result<Response, AppError> {
|
||||
// Extract client ID from authentication header
|
||||
let client_id = extract_client_id_from_request(&request);
|
||||
// Extract token synchronously from headers (avoids holding &Request across await)
|
||||
let token = extract_bearer_token(&request);
|
||||
|
||||
// Resolve client_id: DB token lookup, then prefix fallback
|
||||
let client_id = resolve_client_id(token, &state).await;
|
||||
|
||||
// Check rate limits
|
||||
if !state.rate_limit_manager.check_client_request(&client_id).await? {
|
||||
@@ -322,18 +326,33 @@ pub mod middleware {
|
||||
Ok(next.run(request).await)
|
||||
}
|
||||
|
||||
/// Extract client ID from request (helper function)
|
||||
fn extract_client_id_from_request(request: &Request) -> String {
|
||||
// Try to extract from Authorization header
|
||||
if let Some(auth_header) = request.headers().get("Authorization")
|
||||
&& let Ok(auth_str) = auth_header.to_str()
|
||||
&& let Some(token) = auth_str.strip_prefix("Bearer ")
|
||||
{
|
||||
// Use token hash as client ID (same logic as auth module)
|
||||
/// Synchronously extract bearer token from request headers
|
||||
fn extract_bearer_token(request: &Request) -> Option<String> {
|
||||
request.headers().get("Authorization")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|s| s.strip_prefix("Bearer "))
|
||||
.map(|t| t.to_string())
|
||||
}
|
||||
|
||||
/// Resolve client ID: try DB token first, then fall back to token-prefix derivation
|
||||
async fn resolve_client_id(token: Option<String>, state: &AppState) -> String {
|
||||
if let Some(token) = token {
|
||||
// Try DB token lookup first
|
||||
if let Ok(Some(cid)) = sqlx::query_scalar::<_, String>(
|
||||
"SELECT client_id FROM client_tokens WHERE token = ? AND is_active = TRUE",
|
||||
)
|
||||
.bind(&token)
|
||||
.fetch_optional(&state.db_pool)
|
||||
.await
|
||||
{
|
||||
return cid;
|
||||
}
|
||||
|
||||
// Fallback to token-prefix derivation (env tokens / permissive mode)
|
||||
return format!("client_{}", &token[..8.min(token.len())]);
|
||||
}
|
||||
|
||||
// Fallback to anonymous
|
||||
// No token — anonymous
|
||||
"anonymous".to_string()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user