Menu
Commit 067df512
067df512e550e011fdb00cebb40754b0aa5429d9 by Ólafur Búi Ólafsson
- CHANGELOG.md +10 -0
- Cargo.toml +1 -1
- VERSION +1 -1
- assets/agent.js +0 -23
- src/db/mod.rs +3 -3
- src/lib/crypto.rs +1 -1
- src/lib/html.rs +4 -4
- src/lib/jwt.rs +6 -5
- src/lib/mod.rs +2 -3
- src/lib/password.rs +1 -1
- src/lib/totp.rs +4 -3
- src/main.rs +11 -16
- src/middleware/auth.rs +2 -5
- src/middleware/csrf.rs +5 -6
- src/middleware/rate_limit.rs +3 -8
- src/middleware/secure_headers.rs +4 -1
- src/routes/account.rs +12 -13
- src/routes/admin/audit.rs +14 -15
- src/routes/admin/clients.rs +74 -26
- src/routes/admin/groups.rs +68 -18
- src/routes/admin/tenants.rs +54 -16
- src/routes/admin/users.rs +107 -29
- src/routes/agent.rs +1 -44
- src/routes/agent_os.css +3 -87
- src/routes/agent_view.rs +20 -219
- src/routes/auth_pages.rs +8 -8
- src/routes/authorize.rs +37 -17
- src/routes/introspect.rs +10 -4
- src/routes/revoke.rs +10 -4
- src/routes/token.rs +206 -37
- src/routes/userinfo.rs +1 -1
- src/routes/well_known.rs +5 -4
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..28a14f2
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,10 @@
+# Changelog
+
+All notable changes to this project are documented here. Format: Keep a Changelog; SemVer.
+
+## [Unreleased]
+
+## [0.1.1] - 2026-06-29
+
+- Remove AI-slop 'Agent surfaces' from agent console: delete Tasks, Projects, AGY context, Notes, Passvault, Cron, Curator pages + the dead Tools launcher (all hardcoded placeholder content, no real data). Console nav is now Chat / Kanban / Run details. Also: repo-wide rustfmt + clippy cleanup.
+
diff --git a/Cargo.toml b/Cargo.toml
index 0bafacb..2fda114 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "akurai-idp"
-version = "0.1.0"
+version = "0.1.1"
edition = "2024"
description = "Lightweight Rust OIDC/OAuth2 identity provider for AkurAI — replaces 96MB Node process"
license = "MIT"
diff --git a/VERSION b/VERSION
index 6e8bf73..17e51c3 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.1.0
+0.1.1
diff --git a/assets/agent.js b/assets/agent.js
index 65c9cae..6e9e37d 100644
--- a/assets/agent.js
+++ b/assets/agent.js
@@ -9,7 +9,6 @@ for (const root of roots) {
const session = root.dataset.session || "default";
const page = root.dataset.agentPage || location.pathname;
const isChatPage = page === "/agent";
- const notesKey = `akurai-agent-notes:${session}`;
const chatKey = `akurai-agent-chat:${session}`;
const draftKey = `akurai-agent-draft:${session}`;
const pendingPromptKey = `akurai-agent-pending-prompt:${session}`;
@@ -277,27 +276,6 @@ for (const root of roots) {
}
};
- const initNotes = (container) => {
- const notes = container.querySelector("[data-notes-editor]");
- const notesStatus = container.querySelector("[data-notes-status]");
- if (notes) {
- notes.value = localStorage.getItem(notesKey) || "";
- container.querySelector("[data-save-notes]")?.addEventListener("click", () => {
- localStorage.setItem(notesKey, notes.value);
- if (notesStatus) notesStatus.textContent = "Saved locally";
- setStatus("Notes saved");
- });
- container.querySelector("[data-use-notes]")?.addEventListener("click", () => {
- const text = notes.value.trim();
- fillPrompt(
- text
- ? `Use these local notes as context and propose the next concrete action:\n\n${text}`
- : "No local notes are saved yet. Ask me what to capture before continuing.",
- );
- });
- }
- };
-
root.querySelectorAll("[data-agent-prompt]").forEach((button) => {
button.addEventListener("click", () => fillPrompt(button.dataset.agentPrompt));
});
@@ -309,7 +287,6 @@ for (const root of roots) {
form?.addEventListener("submit", submitStream);
restoreChat();
restorePrompt();
- initNotes(root);
root.querySelectorAll("[data-kanban-board]").forEach(initKanbanPanel);
async function kanbanFetch(path, options = {}) {
diff --git a/src/db/mod.rs b/src/db/mod.rs
index abdb694..ed08b41 100644
--- a/src/db/mod.rs
+++ b/src/db/mod.rs
@@ -1,9 +1,9 @@
pub mod schema;
-use rusqlite::Connection;
-use std::sync::Mutex;
-use std::sync::LazyLock;
use crate::config;
+use rusqlite::Connection;
+use std::sync::LazyLock;
+use std::sync::Mutex;
static DB: LazyLock<Mutex<Connection>> = LazyLock::new(|| {
let cfg = config::get();
diff --git a/src/lib/crypto.rs b/src/lib/crypto.rs
index d7651a2..6b4e9b6 100644
--- a/src/lib/crypto.rs
+++ b/src/lib/crypto.rs
@@ -20,8 +20,8 @@ pub fn sha256(input: &str) -> String {
/// Base64url-encode bytes (no padding, URL-safe alphabet).
pub fn base64url(input: &[u8]) -> String {
- use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
+ use base64::engine::general_purpose::URL_SAFE_NO_PAD;
URL_SAFE_NO_PAD.encode(input)
}
diff --git a/src/lib/html.rs b/src/lib/html.rs
index b25374f..7ed4161 100644
--- a/src/lib/html.rs
+++ b/src/lib/html.rs
@@ -31,10 +31,10 @@ impl Locale {
if let Some(cookies) = cookie_header {
for part in cookies.split(';') {
let part = part.trim();
- if let Some(val) = part.strip_prefix("akurai-lang=") {
- if val.trim() == "en" {
- return Locale::En;
- }
+ if let Some(val) = part.strip_prefix("akurai-lang=")
+ && val.trim() == "en"
+ {
+ return Locale::En;
}
}
}
diff --git a/src/lib/jwt.rs b/src/lib/jwt.rs
index 29a1c4f..896769f 100644
--- a/src/lib/jwt.rs
+++ b/src/lib/jwt.rs
@@ -1,6 +1,6 @@
use base64::Engine;
-use ed25519_dalek::{pkcs8::DecodePublicKey, SigningKey, VerifyingKey};
-use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
+use ed25519_dalek::{SigningKey, VerifyingKey, pkcs8::DecodePublicKey};
+use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode};
use serde_json::Value;
use super::crypto::generate_secure_token;
@@ -46,7 +46,8 @@ pub fn sign_token(
let mut payload = claims.clone();
if let Some(obj) = payload.as_object_mut() {
obj.entry("iat").or_insert(Value::from(now));
- obj.entry("exp").or_insert(Value::from(now + expires_in_secs));
+ obj.entry("exp")
+ .or_insert(Value::from(now + expires_in_secs));
}
let encoding_key = EncodingKey::from_ed_pem(private_key_pem.as_bytes())
@@ -73,8 +74,8 @@ pub fn verify_token(token: &str, public_key_pem: &str) -> Result<Value, String>
/// Export the public key as a JWK object for a JWKS endpoint.
/// The returned JSON object contains kty, crv, x, kid, alg, use.
pub fn export_public_key_jwk(public_key_pem: &str, kid: &str) -> Result<Value, String> {
- let verifying_key =
- VerifyingKey::from_public_key_pem(public_key_pem).map_err(|e| format!("invalid public key: {e}"))?;
+ let verifying_key = VerifyingKey::from_public_key_pem(public_key_pem)
+ .map_err(|e| format!("invalid public key: {e}"))?;
// Ed25519 JWK: kty=OKP, crv=Ed25519, x=base64url(raw 32-byte public key)
let x = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(verifying_key.to_bytes());
diff --git a/src/lib/mod.rs b/src/lib/mod.rs
index 8abffaf..100f384 100644
--- a/src/lib/mod.rs
+++ b/src/lib/mod.rs
@@ -8,7 +8,6 @@ pub mod totp;
/// Parse a DB column that may be a JSON array `["a","b"]` or space-separated `"a b"`.
pub fn parse_json_or_space_separated(s: &str) -> Vec<String> {
- serde_json::from_str::<Vec<String>>(s).unwrap_or_else(|_| {
- s.split_whitespace().map(String::from).collect()
- })
+ serde_json::from_str::<Vec<String>>(s)
+ .unwrap_or_else(|_| s.split_whitespace().map(String::from).collect())
}
diff --git a/src/lib/password.rs b/src/lib/password.rs
index 89b48ff..adadbe4 100644
--- a/src/lib/password.rs
+++ b/src/lib/password.rs
@@ -1,6 +1,6 @@
use argon2::{
- password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Algorithm, Argon2, Params, Version,
+ password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
};
/// Hash a plaintext password with Argon2id.
diff --git a/src/lib/totp.rs b/src/lib/totp.rs
index 62cfb21..99f5ebd 100644
--- a/src/lib/totp.rs
+++ b/src/lib/totp.rs
@@ -7,7 +7,9 @@ use super::crypto::generate_secure_token;
pub fn generate_totp_secret(email: &str, issuer: &str) -> (String, String) {
let secret = Secret::generate_secret();
let base32 = secret.to_encoded().to_string();
- let secret_bytes = secret.to_bytes().expect("failed to decode generated secret");
+ let secret_bytes = secret
+ .to_bytes()
+ .expect("failed to decode generated secret");
// Build the otpauth URI manually since we don't have the `otpauth` feature
// Format: otpauth://totp/ISSUER:EMAIL?secret=BASE32&issuer=ISSUER&algorithm=SHA1&digits=6&period=30
@@ -20,8 +22,7 @@ pub fn generate_totp_secret(email: &str, issuer: &str) -> (String, String) {
);
// Verify the secret works by constructing a TOTP (5-param, no otpauth feature)
- let _totp = TOTP::new(Algorithm::SHA1, 6, 1, 30, secret_bytes)
- .expect("failed to create TOTP");
+ let _totp = TOTP::new(Algorithm::SHA1, 6, 1, 30, secret_bytes).expect("failed to create TOTP");
(base32, uri)
}
diff --git a/src/main.rs b/src/main.rs
index 54885ac..8b42325 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,7 @@
+#![allow(clippy::result_large_err)]
+#![allow(clippy::too_many_arguments)]
+#![allow(special_module_name)]
+
pub mod config;
pub mod db;
pub mod lib;
@@ -5,13 +9,13 @@ pub mod middleware;
pub mod routes;
use axum::{
+ Extension, Json, Router,
body::Body,
extract::Request,
- http::{header, Response, StatusCode},
+ http::{Response, StatusCode, header},
middleware as axum_mw,
response::{IntoResponse, Redirect},
routing::get,
- Extension, Json, Router,
};
use serde_json::json;
use subtle::ConstantTimeEq;
@@ -63,20 +67,16 @@ async fn main() {
let app = Router::new()
// Auth pages (login/mfa/logout) with CSRF
.merge(
- routes::auth_pages::router()
- .layer(axum_mw::from_fn(middleware::csrf::csrf_protection))
+ routes::auth_pages::router().layer(axum_mw::from_fn(middleware::csrf::csrf_protection)),
)
// OIDC discovery
.merge(routes::well_known::router())
// OIDC endpoints
.merge(
- routes::authorize::router()
- .layer(axum_mw::from_fn(middleware::auth::session_optional))
+ routes::authorize::router().layer(axum_mw::from_fn(middleware::auth::session_optional)),
)
.merge(routes::token::router())
- .merge(
- routes::userinfo::router().layer(axum_mw::from_fn(middleware::auth::bearer_auth)),
- )
+ .merge(routes::userinfo::router().layer(axum_mw::from_fn(middleware::auth::bearer_auth)))
.merge(routes::introspect::router())
.merge(routes::revoke::router())
// Authenticated agent console
@@ -117,16 +117,11 @@ async fn main() {
.await
.expect("failed to bind listener");
- axum::serve(listener, app)
- .await
- .expect("server error");
+ axum::serve(listener, app).await.expect("server error");
}
/// Admin auth middleware: requires Bearer token matching IDP_ADMIN_TOKEN (constant-time compare).
-async fn admin_auth_middleware(
- req: Request,
- next: axum::middleware::Next,
-) -> impl IntoResponse {
+async fn admin_auth_middleware(req: Request, next: axum::middleware::Next) -> impl IntoResponse {
let cfg = config::get();
if cfg.admin_token.is_empty() {
diff --git a/src/middleware/auth.rs b/src/middleware/auth.rs
index d88889d..604b3dd 100644
--- a/src/middleware/auth.rs
+++ b/src/middleware/auth.rs
@@ -140,8 +140,7 @@ pub async fn bearer_auth(request: Request<Body>, next: Next) -> Response<Body> {
let cfg = config::get();
// Mode 1: admin token match
- if !cfg.admin_token.is_empty() && constant_time_equal(&token, &cfg.admin_token)
- {
+ if !cfg.admin_token.is_empty() && constant_time_equal(&token, &cfg.admin_token) {
return next.run(request).await;
}
@@ -151,9 +150,7 @@ pub async fn bearer_auth(request: Request<Body>, next: Next) -> Response<Body> {
.prepare("SELECT kid, public_key, alg FROM signing_keys WHERE active = 1")
.ok()?;
let keys: Vec<(String, String, String)> = stmt
- .query_map([], |row| {
- Ok((row.get(0)?, row.get(1)?, row.get(2)?))
- })
+ .query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))
.ok()?
.filter_map(|r| r.ok())
.collect();
diff --git a/src/middleware/csrf.rs b/src/middleware/csrf.rs
index 78ce6a0..13bb017 100644
--- a/src/middleware/csrf.rs
+++ b/src/middleware/csrf.rs
@@ -28,12 +28,11 @@ pub async fn csrf_protection(mut request: Request<Body>, next: Next) -> Response
}
// Skip CSRF for Bearer-authenticated requests (API clients)
- if let Some(auth) = request.headers().get(header::AUTHORIZATION) {
- if let Ok(val) = auth.to_str() {
- if val.starts_with("Bearer ") {
- return next.run(request).await;
- }
- }
+ if let Some(auth) = request.headers().get(header::AUTHORIZATION)
+ && let Ok(val) = auth.to_str()
+ && val.starts_with("Bearer ")
+ {
+ return next.run(request).await;
}
// For state-changing methods, verify double-submit cookie
diff --git a/src/middleware/rate_limit.rs b/src/middleware/rate_limit.rs
index a0a0040..a11286e 100644
--- a/src/middleware/rate_limit.rs
+++ b/src/middleware/rate_limit.rs
@@ -53,9 +53,7 @@ pub async fn rate_limit(request: Request<Body>, next: Next) -> Response<Body> {
let (count, reset_at, remaining) = {
let mut store = state.store.lock().unwrap();
- let entry = store
- .entry(ip)
- .or_insert((0, current + state.window_ms));
+ let entry = store.entry(ip).or_insert((0, current + state.window_ms));
if current >= entry.1 {
// Window expired — reset
@@ -69,7 +67,7 @@ pub async fn rate_limit(request: Request<Body>, next: Next) -> Response<Body> {
};
if count > state.max_requests {
- let retry_after = (reset_at.saturating_sub(current) + 999) / 1000;
+ let retry_after = reset_at.saturating_sub(current).div_ceil(1000);
let body = json!({
"error": "too_many_requests",
"error_description": "Rate limit exceeded. Try again later.",
@@ -86,10 +84,7 @@ pub async fn rate_limit(request: Request<Body>, next: Next) -> Response<Body> {
"x-ratelimit-reset",
(reset_at / 1000).to_string().parse().unwrap(),
);
- headers.insert(
- "retry-after",
- retry_after.to_string().parse().unwrap(),
- );
+ headers.insert("retry-after", retry_after.to_string().parse().unwrap());
return resp;
}
diff --git a/src/middleware/secure_headers.rs b/src/middleware/secure_headers.rs
index 44f072a..6738822 100644
--- a/src/middleware/secure_headers.rs
+++ b/src/middleware/secure_headers.rs
@@ -14,7 +14,10 @@ pub async fn secure_headers(request: Request<Body>, next: Next) -> Response<Body
"strict-transport-security",
"max-age=31536000; includeSubDomains".parse().unwrap(),
);
- headers.insert("referrer-policy", "strict-origin-when-cross-origin".parse().unwrap());
+ headers.insert(
+ "referrer-policy",
+ "strict-origin-when-cross-origin".parse().unwrap(),
+ );
headers.insert(
"permissions-policy",
"camera=(), microphone=(), geolocation=(), interest-cohort=()"
diff --git a/src/routes/account.rs b/src/routes/account.rs
index fe1e83d..157b2d8 100644
--- a/src/routes/account.rs
+++ b/src/routes/account.rs
@@ -673,20 +673,19 @@ async fn sessions_page(headers: HeaderMap) -> Response {
ORDER BY created_at DESC",
)
.unwrap();
- let rows = stmt
- .query_map(rusqlite::params![user.user_id, now], |row| {
- Ok(SessionRow {
- id: row.get(0)?,
- ip: row.get(1)?,
- user_agent: row.get(2)?,
- created_at: row.get(3)?,
- expires_at: row.get(4)?,
- })
+
+ stmt.query_map(rusqlite::params![user.user_id, now], |row| {
+ Ok(SessionRow {
+ id: row.get(0)?,
+ ip: row.get(1)?,
+ user_agent: row.get(2)?,
+ created_at: row.get(3)?,
+ expires_at: row.get(4)?,
})
- .unwrap()
- .filter_map(|r| r.ok())
- .collect::<Vec<_>>();
- rows
+ })
+ .unwrap()
+ .filter_map(|r| r.ok())
+ .collect::<Vec<_>>()
});
let lbl_current = t(locale, " (núverandi)", " (current)");
diff --git a/src/routes/admin/audit.rs b/src/routes/admin/audit.rs
index c41e911..f26e833 100644
--- a/src/routes/admin/audit.rs
+++ b/src/routes/admin/audit.rs
@@ -1,10 +1,4 @@
-use axum::{
- extract::Query,
- http::StatusCode,
- response::IntoResponse,
- routing::get,
- Json, Router,
-};
+use axum::{Json, Router, extract::Query, http::StatusCode, response::IntoResponse, routing::get};
use serde::{Deserialize, Serialize};
use serde_json::json;
@@ -72,12 +66,12 @@ async fn list_audit(Query(q): Query<AuditQuery>) -> impl IntoResponse {
params.push(Box::new(evt.clone()));
idx += 1;
}
- if let Some(ref since_str) = q.since {
- if let Some(ts) = parse_since(since_str) {
- conditions.push(format!("created_at >= ?{idx}"));
- params.push(Box::new(ts));
- idx += 1;
- }
+ if let Some(ref since_str) = q.since
+ && let Some(ts) = parse_since(since_str)
+ {
+ conditions.push(format!("created_at >= ?{idx}"));
+ params.push(Box::new(ts));
+ idx += 1;
}
let where_clause = if conditions.is_empty() {
@@ -92,7 +86,8 @@ async fn list_audit(Query(q): Query<AuditQuery>) -> impl IntoResponse {
params.push(Box::new(limit as i64));
let mut stmt = conn.prepare(&sql)?;
- let params_refs: Vec<&dyn rusqlite::types::ToSql> = params.iter().map(|p| p.as_ref()).collect();
+ let params_refs: Vec<&dyn rusqlite::types::ToSql> =
+ params.iter().map(|p| p.as_ref()).collect();
let rows = stmt.query_map(params_refs.as_slice(), |row| {
let metadata_raw: Option<String> = row.get(5)?;
let metadata = metadata_raw.and_then(|s| serde_json::from_str(&s).ok());
@@ -115,6 +110,10 @@ async fn list_audit(Query(q): Query<AuditQuery>) -> impl IntoResponse {
match result {
Ok(entries) => Json(json!(entries)).into_response(),
- Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response(),
+ Err(e) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(json!({"error": e.to_string()})),
+ )
+ .into_response(),
}
}
diff --git a/src/routes/admin/clients.rs b/src/routes/admin/clients.rs
index 3e13aa9..370d136 100644
--- a/src/routes/admin/clients.rs
+++ b/src/routes/admin/clients.rs
@@ -1,9 +1,9 @@
use axum::{
+ Json, Router,
extract::{Path, Query},
http::StatusCode,
response::IntoResponse,
routing::{delete, get, post},
- Json, Router,
};
use serde::{Deserialize, Serialize};
use serde_json::json;
@@ -63,7 +63,8 @@ async fn list_clients(Query(q): Query<ListQuery>) -> impl IntoResponse {
),
};
let mut stmt = conn.prepare(&sql)?;
- let params_refs: Vec<&dyn rusqlite::types::ToSql> = params.iter().map(|p| p.as_ref()).collect();
+ let params_refs: Vec<&dyn rusqlite::types::ToSql> =
+ params.iter().map(|p| p.as_ref()).collect();
let rows = stmt.query_map(params_refs.as_slice(), |row| {
let redirect_uris_raw: String = row.get(3)?;
let grant_types_raw: String = row.get(4)?;
@@ -88,7 +89,11 @@ async fn list_clients(Query(q): Query<ListQuery>) -> impl IntoResponse {
match result {
Ok(clients) => Json(json!(clients)).into_response(),
- Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response(),
+ Err(e) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(json!({"error": e.to_string()})),
+ )
+ .into_response(),
}
}
@@ -98,11 +103,20 @@ async fn create_client(Json(body): Json<CreateClient>) -> impl IntoResponse {
let secret_hash = sha256(&client_secret);
let now = chrono::Utc::now().timestamp();
- let grant_types = body.grant_types.unwrap_or_else(|| vec!["authorization_code".to_string()]);
- let scopes = body.scopes.unwrap_or_else(|| vec!["openid".to_string(), "profile".to_string(), "email".to_string()]);
+ let grant_types = body
+ .grant_types
+ .unwrap_or_else(|| vec!["authorization_code".to_string()]);
+ let scopes = body.scopes.unwrap_or_else(|| {
+ vec![
+ "openid".to_string(),
+ "profile".to_string(),
+ "email".to_string(),
+ ]
+ });
let first_party = body.first_party.unwrap_or(false);
- let redirect_uris_json = serde_json::to_string(&body.redirect_uris).unwrap_or_else(|_| "[]".to_string());
+ let redirect_uris_json =
+ serde_json::to_string(&body.redirect_uris).unwrap_or_else(|_| "[]".to_string());
let grant_types_json = serde_json::to_string(&grant_types).unwrap_or_else(|_| "[]".to_string());
let scopes_json = serde_json::to_string(&scopes).unwrap_or_else(|_| "[]".to_string());
@@ -122,33 +136,53 @@ async fn create_client(Json(body): Json<CreateClient>) -> impl IntoResponse {
None,
Some(&json!({"client_id": id, "name": body.name})),
);
- (StatusCode::CREATED, Json(json!({
- "id": id,
- "client_secret": client_secret,
- "name": body.name,
- "tenant_id": body.tenant_id,
- "redirect_uris": body.redirect_uris,
- "grant_types": grant_types,
- "scopes": scopes,
- "first_party": first_party
- }))).into_response()
+ (
+ StatusCode::CREATED,
+ Json(json!({
+ "id": id,
+ "client_secret": client_secret,
+ "name": body.name,
+ "tenant_id": body.tenant_id,
+ "redirect_uris": body.redirect_uris,
+ "grant_types": grant_types,
+ "scopes": scopes,
+ "first_party": first_party
+ })),
+ )
+ .into_response()
}
- Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response(),
+ Err(e) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(json!({"error": e.to_string()})),
+ )
+ .into_response(),
}
}
async fn delete_client(Path(id): Path<String>) -> impl IntoResponse {
- let result = with_db(|conn| {
- conn.execute("DELETE FROM clients WHERE id = ?1", [&id])
- });
+ let result = with_db(|conn| conn.execute("DELETE FROM clients WHERE id = ?1", [&id]));
match result {
- Ok(0) => (StatusCode::NOT_FOUND, Json(json!({"error": "client not found"}))).into_response(),
+ Ok(0) => (
+ StatusCode::NOT_FOUND,
+ Json(json!({"error": "client not found"})),
+ )
+ .into_response(),
Ok(_) => {
- audit::log_audit(None, None, audit::CLIENT_DELETED, None, Some(&json!({"client_id": id})));
+ audit::log_audit(
+ None,
+ None,
+ audit::CLIENT_DELETED,
+ None,
+ Some(&json!({"client_id": id})),
+ );
Json(json!({"ok": true})).into_response()
}
- Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response(),
+ Err(e) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(json!({"error": e.to_string()})),
+ )
+ .into_response(),
}
}
@@ -164,11 +198,25 @@ async fn rotate_secret(Path(id): Path<String>) -> impl IntoResponse {
});
match result {
- Ok(0) => (StatusCode::NOT_FOUND, Json(json!({"error": "client not found"}))).into_response(),
+ Ok(0) => (
+ StatusCode::NOT_FOUND,
+ Json(json!({"error": "client not found"})),
+ )
+ .into_response(),
Ok(_) => {
- audit::log_audit(None, None, audit::CLIENT_SECRET_ROTATED, None, Some(&json!({"client_id": id})));
+ audit::log_audit(
+ None,
+ None,
+ audit::CLIENT_SECRET_ROTATED,
+ None,
+ Some(&json!({"client_id": id})),
+ );
Json(json!({"id": id, "client_secret": new_secret})).into_response()
}
- Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response(),
+ Err(e) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(json!({"error": e.to_string()})),
+ )
+ .into_response(),
}
}
diff --git a/src/routes/admin/groups.rs b/src/routes/admin/groups.rs
index d86dc82..cf2ff01 100644
--- a/src/routes/admin/groups.rs
+++ b/src/routes/admin/groups.rs
@@ -1,9 +1,9 @@
use axum::{
+ Json, Router,
extract::{Path, Query},
http::StatusCode,
response::IntoResponse,
routing::{delete, get, post},
- Json, Router,
};
use serde::{Deserialize, Serialize};
use serde_json::json;
@@ -49,7 +49,8 @@ async fn list_groups(Query(q): Query<ListQuery>) -> impl IntoResponse {
let result = with_db(|conn| {
let (sql, params): (String, Vec<Box<dyn rusqlite::types::ToSql>>) = match &q.tenant_id {
Some(tid) => (
- "SELECT id, tenant_id, name, description FROM groups WHERE tenant_id = ?1".to_string(),
+ "SELECT id, tenant_id, name, description FROM groups WHERE tenant_id = ?1"
+ .to_string(),
vec![Box::new(tid.clone()) as Box<dyn rusqlite::types::ToSql>],
),
None => (
@@ -58,7 +59,8 @@ async fn list_groups(Query(q): Query<ListQuery>) -> impl IntoResponse {
),
};
let mut stmt = conn.prepare(&sql)?;
- let params_refs: Vec<&dyn rusqlite::types::ToSql> = params.iter().map(|p| p.as_ref()).collect();
+ let params_refs: Vec<&dyn rusqlite::types::ToSql> =
+ params.iter().map(|p| p.as_ref()).collect();
let rows = stmt.query_map(params_refs.as_slice(), |row| {
Ok(GroupRow {
id: row.get(0)?,
@@ -76,7 +78,11 @@ async fn list_groups(Query(q): Query<ListQuery>) -> impl IntoResponse {
match result {
Ok(groups) => Json(json!(groups)).into_response(),
- Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response(),
+ Err(e) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(json!({"error": e.to_string()})),
+ )
+ .into_response(),
}
}
@@ -99,31 +105,55 @@ async fn create_group(Json(body): Json<CreateGroup>) -> impl IntoResponse {
None,
Some(&json!({"name": body.name, "group_id": id})),
);
- (StatusCode::CREATED, Json(json!({"id": id, "name": body.name, "tenant_id": body.tenant_id}))).into_response()
+ (
+ StatusCode::CREATED,
+ Json(json!({"id": id, "name": body.name, "tenant_id": body.tenant_id})),
+ )
+ .into_response()
}
Err(e) => {
let msg = e.to_string();
if msg.contains("UNIQUE") {
- (StatusCode::CONFLICT, Json(json!({"error": "group with this name already exists in tenant"}))).into_response()
+ (
+ StatusCode::CONFLICT,
+ Json(json!({"error": "group with this name already exists in tenant"})),
+ )
+ .into_response()
} else {
- (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": msg}))).into_response()
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(json!({"error": msg})),
+ )
+ .into_response()
}
}
}
}
async fn delete_group(Path(id): Path<String>) -> impl IntoResponse {
- let result = with_db(|conn| {
- conn.execute("DELETE FROM groups WHERE id = ?1", [&id])
- });
+ let result = with_db(|conn| conn.execute("DELETE FROM groups WHERE id = ?1", [&id]));
match result {
- Ok(0) => (StatusCode::NOT_FOUND, Json(json!({"error": "group not found"}))).into_response(),
+ Ok(0) => (
+ StatusCode::NOT_FOUND,
+ Json(json!({"error": "group not found"})),
+ )
+ .into_response(),
Ok(_) => {
- audit::log_audit(None, None, audit::GROUP_DELETED, None, Some(&json!({"group_id": id})));
+ audit::log_audit(
+ None,
+ None,
+ audit::GROUP_DELETED,
+ None,
+ Some(&json!({"group_id": id})),
+ );
Json(json!({"ok": true})).into_response()
}
- Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response(),
+ Err(e) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(json!({"error": e.to_string()})),
+ )
+ .into_response(),
}
}
@@ -149,11 +179,23 @@ async fn add_member(Path(id): Path<String>, Json(body): Json<AddMember>) -> impl
Err(e) => {
let msg = e.to_string();
if msg.contains("UNIQUE") || msg.contains("PRIMARY") {
- (StatusCode::CONFLICT, Json(json!({"error": "user already in group"}))).into_response()
+ (
+ StatusCode::CONFLICT,
+ Json(json!({"error": "user already in group"})),
+ )
+ .into_response()
} else if msg.contains("FOREIGN") {
- (StatusCode::BAD_REQUEST, Json(json!({"error": "user or group does not exist"}))).into_response()
+ (
+ StatusCode::BAD_REQUEST,
+ Json(json!({"error": "user or group does not exist"})),
+ )
+ .into_response()
} else {
- (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": msg}))).into_response()
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(json!({"error": msg})),
+ )
+ .into_response()
}
}
}
@@ -168,7 +210,11 @@ async fn remove_member(Path((id, user_id)): Path<(String, String)>) -> impl Into
});
match result {
- Ok(0) => (StatusCode::NOT_FOUND, Json(json!({"error": "membership not found"}))).into_response(),
+ Ok(0) => (
+ StatusCode::NOT_FOUND,
+ Json(json!({"error": "membership not found"})),
+ )
+ .into_response(),
Ok(_) => {
audit::log_audit(
None,
@@ -179,6 +225,10 @@ async fn remove_member(Path((id, user_id)): Path<(String, String)>) -> impl Into
);
Json(json!({"ok": true})).into_response()
}
- Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response(),
+ Err(e) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(json!({"error": e.to_string()})),
+ )
+ .into_response(),
}
}
diff --git a/src/routes/admin/tenants.rs b/src/routes/admin/tenants.rs
index bc33396..6c0c6d6 100644
--- a/src/routes/admin/tenants.rs
+++ b/src/routes/admin/tenants.rs
@@ -1,9 +1,9 @@
use axum::{
+ Json, Router,
extract::Path,
http::StatusCode,
response::IntoResponse,
- routing::{get, patch, post},
- Json, Router,
+ routing::{get, patch},
};
use serde::{Deserialize, Serialize};
use serde_json::json;
@@ -62,7 +62,11 @@ async fn list_tenants() -> impl IntoResponse {
match result {
Ok(tenants) => Json(json!(tenants)).into_response(),
- Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response(),
+ Err(e) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(json!({"error": e.to_string()})),
+ )
+ .into_response(),
}
}
@@ -86,20 +90,35 @@ async fn create_tenant(Json(body): Json<CreateTenant>) -> impl IntoResponse {
None,
Some(&json!({"name": body.name, "slug": body.slug})),
);
- (StatusCode::CREATED, Json(json!({"id": id, "name": body.name, "slug": body.slug}))).into_response()
+ (
+ StatusCode::CREATED,
+ Json(json!({"id": id, "name": body.name, "slug": body.slug})),
+ )
+ .into_response()
}
Err(e) => {
let msg = e.to_string();
if msg.contains("UNIQUE") {
- (StatusCode::CONFLICT, Json(json!({"error": "tenant with this slug already exists"}))).into_response()
+ (
+ StatusCode::CONFLICT,
+ Json(json!({"error": "tenant with this slug already exists"})),
+ )
+ .into_response()
} else {
- (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": msg}))).into_response()
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(json!({"error": msg})),
+ )
+ .into_response()
}
}
}
}
-async fn update_tenant(Path(id): Path<String>, Json(body): Json<UpdateTenant>) -> impl IntoResponse {
+async fn update_tenant(
+ Path(id): Path<String>,
+ Json(body): Json<UpdateTenant>,
+) -> impl IntoResponse {
let result = with_db(|conn| {
let mut sets = Vec::new();
let mut params: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
@@ -128,12 +147,17 @@ async fn update_tenant(Path(id): Path<String>, Json(body): Json<UpdateTenant>) -
let sql = format!("UPDATE tenants SET {} WHERE id = ?{idx}", sets.join(", "));
params.push(Box::new(id.clone()));
- let params_refs: Vec<&dyn rusqlite::types::ToSql> = params.iter().map(|p| p.as_ref()).collect();
+ let params_refs: Vec<&dyn rusqlite::types::ToSql> =
+ params.iter().map(|p| p.as_ref()).collect();
conn.execute(&sql, params_refs.as_slice())
});
match result {
- Ok(0) => (StatusCode::NOT_FOUND, Json(json!({"error": "tenant not found or no changes"}))).into_response(),
+ Ok(0) => (
+ StatusCode::NOT_FOUND,
+ Json(json!({"error": "tenant not found or no changes"})),
+ )
+ .into_response(),
Ok(_) => {
audit::log_audit(Some(&id), None, audit::TENANT_UPDATED, None, None);
Json(json!({"ok": true})).into_response()
@@ -141,25 +165,39 @@ async fn update_tenant(Path(id): Path<String>, Json(body): Json<UpdateTenant>) -
Err(e) => {
let msg = e.to_string();
if msg.contains("UNIQUE") {
- (StatusCode::CONFLICT, Json(json!({"error": "slug already taken"}))).into_response()
+ (
+ StatusCode::CONFLICT,
+ Json(json!({"error": "slug already taken"})),
+ )
+ .into_response()
} else {
- (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": msg}))).into_response()
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(json!({"error": msg})),
+ )
+ .into_response()
}
}
}
}
async fn delete_tenant(Path(id): Path<String>) -> impl IntoResponse {
- let result = with_db(|conn| {
- conn.execute("DELETE FROM tenants WHERE id = ?1", [&id])
- });
+ let result = with_db(|conn| conn.execute("DELETE FROM tenants WHERE id = ?1", [&id]));
match result {
- Ok(0) => (StatusCode::NOT_FOUND, Json(json!({"error": "tenant not found"}))).into_response(),
+ Ok(0) => (
+ StatusCode::NOT_FOUND,
+ Json(json!({"error": "tenant not found"})),
+ )
+ .into_response(),
Ok(_) => {
audit::log_audit(Some(&id), None, audit::TENANT_DELETED, None, None);
Json(json!({"ok": true})).into_response()
}
- Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response(),
+ Err(e) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(json!({"error": e.to_string()})),
+ )
+ .into_response(),
}
}
diff --git a/src/routes/admin/users.rs b/src/routes/admin/users.rs
index 28414e6..fa11846 100644
--- a/src/routes/admin/users.rs
+++ b/src/routes/admin/users.rs
@@ -1,9 +1,9 @@
use axum::{
+ Json, Router,
extract::{Path, Query},
http::StatusCode,
response::IntoResponse,
routing::{get, post},
- Json, Router,
};
use serde::{Deserialize, Serialize};
use serde_json::json;
@@ -55,7 +55,10 @@ pub struct ResetPassword {
pub fn router() -> Router {
Router::new()
.route("/", get(list_users).post(create_user))
- .route("/{id}", get(get_user).patch(update_user).delete(delete_user))
+ .route(
+ "/{id}",
+ get(get_user).patch(update_user).delete(delete_user),
+ )
.route("/{id}/reset-password", post(reset_password))
.route("/{id}/lock", post(lock_user))
.route("/{id}/unlock", post(unlock_user))
@@ -74,7 +77,8 @@ async fn list_users(Query(q): Query<ListQuery>) -> impl IntoResponse {
),
};
let mut stmt = conn.prepare(&sql)?;
- let params_refs: Vec<&dyn rusqlite::types::ToSql> = params.iter().map(|p| p.as_ref()).collect();
+ let params_refs: Vec<&dyn rusqlite::types::ToSql> =
+ params.iter().map(|p| p.as_ref()).collect();
let rows = stmt.query_map(params_refs.as_slice(), |row| {
Ok(UserRow {
id: row.get(0)?,
@@ -98,7 +102,11 @@ async fn list_users(Query(q): Query<ListQuery>) -> impl IntoResponse {
match result {
Ok(users) => Json(json!(users)).into_response(),
- Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response(),
+ Err(e) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(json!({"error": e.to_string()})),
+ )
+ .into_response(),
}
}
@@ -126,17 +134,25 @@ async fn get_user(Path(id): Path<String>) -> impl IntoResponse {
match result {
Ok(user) => Json(json!(user)).into_response(),
- Err(rusqlite::Error::QueryReturnedNoRows) => {
- (StatusCode::NOT_FOUND, Json(json!({"error": "user not found"}))).into_response()
- }
- Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response(),
+ Err(rusqlite::Error::QueryReturnedNoRows) => (
+ StatusCode::NOT_FOUND,
+ Json(json!({"error": "user not found"})),
+ )
+ .into_response(),
+ Err(e) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(json!({"error": e.to_string()})),
+ )
+ .into_response(),
}
}
async fn create_user(Json(body): Json<CreateUser>) -> impl IntoResponse {
let pw_hash = match hash_password(&body.password) {
Ok(h) => h,
- Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e}))).into_response(),
+ Err(e) => {
+ return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e}))).into_response();
+ }
};
let id = generate_secure_token(16);
@@ -158,14 +174,26 @@ async fn create_user(Json(body): Json<CreateUser>) -> impl IntoResponse {
None,
Some(&json!({"email": body.email})),
);
- (StatusCode::CREATED, Json(json!({"id": id, "email": body.email, "tenant_id": body.tenant_id}))).into_response()
+ (
+ StatusCode::CREATED,
+ Json(json!({"id": id, "email": body.email, "tenant_id": body.tenant_id})),
+ )
+ .into_response()
}
Err(e) => {
let msg = e.to_string();
if msg.contains("UNIQUE") {
- (StatusCode::CONFLICT, Json(json!({"error": "user with this email already exists in tenant"}))).into_response()
+ (
+ StatusCode::CONFLICT,
+ Json(json!({"error": "user with this email already exists in tenant"})),
+ )
+ .into_response()
} else {
- (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": msg}))).into_response()
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(json!({"error": msg})),
+ )
+ .into_response()
}
}
}
@@ -207,39 +235,59 @@ async fn update_user(Path(id): Path<String>, Json(body): Json<UpdateUser>) -> im
let sql = format!("UPDATE users SET {} WHERE id = ?{idx}", sets.join(", "));
params.push(Box::new(id.clone()));
- let params_refs: Vec<&dyn rusqlite::types::ToSql> = params.iter().map(|p| p.as_ref()).collect();
+ let params_refs: Vec<&dyn rusqlite::types::ToSql> =
+ params.iter().map(|p| p.as_ref()).collect();
conn.execute(&sql, params_refs.as_slice())
});
match result {
- Ok(0) => (StatusCode::NOT_FOUND, Json(json!({"error": "user not found or no changes"}))).into_response(),
+ Ok(0) => (
+ StatusCode::NOT_FOUND,
+ Json(json!({"error": "user not found or no changes"})),
+ )
+ .into_response(),
Ok(_) => {
audit::log_audit(None, Some(&id), audit::USER_UPDATED, None, None);
Json(json!({"ok": true})).into_response()
}
- Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response(),
+ Err(e) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(json!({"error": e.to_string()})),
+ )
+ .into_response(),
}
}
async fn delete_user(Path(id): Path<String>) -> impl IntoResponse {
- let result = with_db(|conn| {
- conn.execute("DELETE FROM users WHERE id = ?1", [&id])
- });
+ let result = with_db(|conn| conn.execute("DELETE FROM users WHERE id = ?1", [&id]));
match result {
- Ok(0) => (StatusCode::NOT_FOUND, Json(json!({"error": "user not found"}))).into_response(),
+ Ok(0) => (
+ StatusCode::NOT_FOUND,
+ Json(json!({"error": "user not found"})),
+ )
+ .into_response(),
Ok(_) => {
audit::log_audit(None, Some(&id), audit::USER_DELETED, None, None);
Json(json!({"ok": true})).into_response()
}
- Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response(),
+ Err(e) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(json!({"error": e.to_string()})),
+ )
+ .into_response(),
}
}
-async fn reset_password(Path(id): Path<String>, Json(body): Json<ResetPassword>) -> impl IntoResponse {
+async fn reset_password(
+ Path(id): Path<String>,
+ Json(body): Json<ResetPassword>,
+) -> impl IntoResponse {
let pw_hash = match hash_password(&body.password) {
Ok(h) => h,
- Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e}))).into_response(),
+ Err(e) => {
+ return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e}))).into_response();
+ }
};
let now = chrono::Utc::now().timestamp();
@@ -252,12 +300,20 @@ async fn reset_password(Path(id): Path<String>, Json(body): Json<ResetPassword>)
});
match result {
- Ok(0) => (StatusCode::NOT_FOUND, Json(json!({"error": "user not found"}))).into_response(),
+ Ok(0) => (
+ StatusCode::NOT_FOUND,
+ Json(json!({"error": "user not found"})),
+ )
+ .into_response(),
Ok(_) => {
audit::log_audit(None, Some(&id), audit::USER_PASSWORD_CHANGED, None, None);
Json(json!({"ok": true})).into_response()
}
- Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response(),
+ Err(e) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(json!({"error": e.to_string()})),
+ )
+ .into_response(),
}
}
@@ -274,12 +330,20 @@ async fn lock_user(Path(id): Path<String>) -> impl IntoResponse {
});
match result {
- Ok(0) => (StatusCode::NOT_FOUND, Json(json!({"error": "user not found"}))).into_response(),
+ Ok(0) => (
+ StatusCode::NOT_FOUND,
+ Json(json!({"error": "user not found"})),
+ )
+ .into_response(),
Ok(_) => {
audit::log_audit(None, Some(&id), audit::USER_LOCKED, None, None);
Json(json!({"ok": true, "locked_until": locked_until})).into_response()
}
- Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response(),
+ Err(e) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(json!({"error": e.to_string()})),
+ )
+ .into_response(),
}
}
@@ -294,11 +358,25 @@ async fn unlock_user(Path(id): Path<String>) -> impl IntoResponse {
});
match result {
- Ok(0) => (StatusCode::NOT_FOUND, Json(json!({"error": "user not found"}))).into_response(),
+ Ok(0) => (
+ StatusCode::NOT_FOUND,
+ Json(json!({"error": "user not found"})),
+ )
+ .into_response(),
Ok(_) => {
- audit::log_audit(None, Some(&id), audit::USER_UPDATED, None, Some(&json!({"action": "unlock"})));
+ audit::log_audit(
+ None,
+ Some(&id),
+ audit::USER_UPDATED,
+ None,
+ Some(&json!({"action": "unlock"})),
+ );
Json(json!({"ok": true})).into_response()
}
- Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response(),
+ Err(e) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(json!({"error": e.to_string()})),
+ )
+ .into_response(),
}
}
diff --git a/src/routes/agent.rs b/src/routes/agent.rs
index 7702007..ad9111b 100644
--- a/src/routes/agent.rs
+++ b/src/routes/agent.rs
@@ -28,15 +28,7 @@ pub fn router() -> Router {
Router::new()
.route("/agent", get(agent_page).post(agent_submit))
.route("/agent/kanban", get(agent_kanban_page))
- .route("/agent/tools", get(agent_tools_page))
.route("/agent/run", get(agent_run_page))
- .route("/agent/tasks", get(agent_tasks_page))
- .route("/agent/projects", get(agent_projects_page))
- .route("/agent/agy", get(agent_agy_page))
- .route("/agent/notes", get(agent_notes_page))
- .route("/agent/passvault", get(agent_passvault_page))
- .route("/agent/cron", get(agent_cron_page))
- .route("/agent/curator", get(agent_curator_page))
.route("/agent/kanban/boards", get(kanban_boards))
.route("/agent/kanban/board/{board}", get(kanban_board))
.route("/agent/kanban/tasks", post(kanban_create_task))
@@ -130,45 +122,10 @@ async fn agent_kanban_page(headers: HeaderMap, Extension(user): Extension<AuthUs
agent_workspace_page(headers, user, AgentPage::Kanban).await
}
-async fn agent_tools_page(headers: HeaderMap, Extension(user): Extension<AuthUser>) -> Response {
- agent_workspace_page(headers, user, AgentPage::Tools).await
-}
-
async fn agent_run_page(headers: HeaderMap, Extension(user): Extension<AuthUser>) -> Response {
agent_workspace_page(headers, user, AgentPage::Run).await
}
-async fn agent_tasks_page(headers: HeaderMap, Extension(user): Extension<AuthUser>) -> Response {
- agent_workspace_page(headers, user, AgentPage::Tasks).await
-}
-
-async fn agent_projects_page(headers: HeaderMap, Extension(user): Extension<AuthUser>) -> Response {
- agent_workspace_page(headers, user, AgentPage::Projects).await
-}
-
-async fn agent_agy_page(headers: HeaderMap, Extension(user): Extension<AuthUser>) -> Response {
- agent_workspace_page(headers, user, AgentPage::Agy).await
-}
-
-async fn agent_notes_page(headers: HeaderMap, Extension(user): Extension<AuthUser>) -> Response {
- agent_workspace_page(headers, user, AgentPage::Notes).await
-}
-
-async fn agent_passvault_page(
- headers: HeaderMap,
- Extension(user): Extension<AuthUser>,
-) -> Response {
- agent_workspace_page(headers, user, AgentPage::Passvault).await
-}
-
-async fn agent_cron_page(headers: HeaderMap, Extension(user): Extension<AuthUser>) -> Response {
- agent_workspace_page(headers, user, AgentPage::Cron).await
-}
-
-async fn agent_curator_page(headers: HeaderMap, Extension(user): Extension<AuthUser>) -> Response {
- agent_workspace_page(headers, user, AgentPage::Curator).await
-}
-
async fn agent_workspace_page(headers: HeaderMap, user: AuthUser, page: AgentPage) -> Response {
if !agent_allowed(&user.email) {
return forbidden_page(&headers, &user);
@@ -545,7 +502,7 @@ async fn request_json(method: &str, url: &str, body: Option<&str>) -> Result<Str
method = method,
path = target.path,
host = target.host_header(),
- len = body.as_bytes().len(),
+ len = body.len(),
body = body,
);
stream
diff --git a/src/routes/agent_os.css b/src/routes/agent_os.css
index bdb336c..f6299ba 100644
--- a/src/routes/agent_os.css
+++ b/src/routes/agent_os.css
@@ -460,62 +460,13 @@ body {
border-color: color-mix(in srgb, var(--accent) 38%, var(--border));
}
-.agent-panel-grid,
-.agent-panel-list {
- display: grid;
- gap: 8px;
-}
-
-.agent-tool-launcher {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 8px;
-}
-
-.agent-tool-launcher button,
-.agent-tool-launcher a {
- appearance: none;
- min-width: 0;
- display: grid;
- gap: 4px;
- min-height: 78px;
- padding: 11px;
- border: 1px solid var(--soft-border);
- border-radius: 12px;
- background: color-mix(in srgb, var(--bg-2) 68%, transparent);
- color: var(--fg);
- font: inherit;
- text-align: left;
- text-decoration: none;
- cursor: pointer;
-}
-
-.agent-tool-launcher button:hover,
-.agent-tool-launcher a:hover {
- border-color: color-mix(in srgb, var(--accent) 38%, var(--border));
- background: color-mix(in srgb, var(--panel) 68%, var(--bg));
-}
-
-.agent-tool-launcher b {
- font-size: 13px;
-}
-
-.agent-tool-launcher span {
- color: var(--muted);
- font-size: 12px;
- line-height: 1.35;
-}
-
-.agent-route-launcher {
- grid-template-columns: repeat(4, minmax(0, 1fr));
-}
-
.agent-panel-grid {
+ display: grid;
+ gap: 8px;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
-.agent-panel-grid div,
-.agent-panel-list article {
+.agent-panel-grid div {
min-width: 0;
padding: 10px;
border: 1px solid var(--soft-border);
@@ -537,21 +488,6 @@ body {
overflow-wrap: anywhere;
}
-.agent-panel-list article {
- display: grid;
- gap: 3px;
-}
-
-.agent-panel-list b {
- color: var(--fg);
- font-weight: 650;
-}
-
-.agent-panel-list span {
- color: var(--muted);
- font-size: 13px;
-}
-
.agent-panel-actions {
display: flex;
flex-wrap: wrap;
@@ -559,18 +495,6 @@ body {
margin-top: 14px;
}
-.agent-notes-editor {
- min-height: 150px;
- width: 100%;
- padding: 10px;
- border: 1px solid var(--soft-border);
- border-radius: 12px;
- background: color-mix(in srgb, var(--bg-2) 68%, transparent);
- color: var(--fg);
- font: 14px/1.55 var(--font-sans);
- resize: vertical;
-}
-
.kanban-shell {
display: grid;
gap: 12px;
@@ -942,14 +866,6 @@ body {
grid-template-columns: 1fr;
}
- .agent-tool-launcher {
- grid-template-columns: 1fr;
- }
-
- .agent-route-launcher {
- grid-template-columns: 1fr;
- }
-
.agent-page-scroll {
padding: 16px;
}
diff --git a/src/routes/agent_view.rs b/src/routes/agent_view.rs
index 4dd3ccb..bec5762 100644
--- a/src/routes/agent_view.rs
+++ b/src/routes/agent_view.rs
@@ -9,15 +9,7 @@ pub const AGENT_OS_STYLES: &str = include_str!("agent_os.css");
pub(crate) enum AgentPage {
Chat,
Kanban,
- Tools,
Run,
- Tasks,
- Projects,
- Agy,
- Notes,
- Passvault,
- Cron,
- Curator,
}
impl AgentPage {
@@ -25,15 +17,7 @@ impl AgentPage {
match self {
Self::Chat => "Current conversation",
Self::Kanban => "Kanban",
- Self::Tools => "Tools",
Self::Run => "Run details",
- Self::Tasks => "Tasks",
- Self::Projects => "Projects",
- Self::Agy => "AGY context",
- Self::Notes => "Notes",
- Self::Passvault => "Passvault",
- Self::Cron => "Cron",
- Self::Curator => "Curator",
}
}
@@ -41,15 +25,7 @@ impl AgentPage {
match self {
Self::Chat => "/agent",
Self::Kanban => "/agent/kanban",
- Self::Tools => "/agent/tools",
Self::Run => "/agent/run",
- Self::Tasks => "/agent/tasks",
- Self::Projects => "/agent/projects",
- Self::Agy => "/agent/agy",
- Self::Notes => "/agent/notes",
- Self::Passvault => "/agent/passvault",
- Self::Cron => "/agent/cron",
- Self::Curator => "/agent/curator",
}
}
}
@@ -162,33 +138,12 @@ fn render_sidebar(active: AgentPage) -> String {
<nav class="agent-nav" aria-label="Workspace">
{chat}
{kanban}
- {tools}
{run}
</nav>
- </div>
- <div class="agent-section">
- <h2>Agent surfaces</h2>
- <nav class="agent-nav" aria-label="Agent surfaces">
- {tasks}
- {projects}
- {agy}
- {notes}
- {passvault}
- {cron}
- {curator}
- </nav>
</div>"#,
chat = nav_link(AgentPage::Chat, active, Some("now")),
kanban = nav_link(AgentPage::Kanban, active, Some("board")),
- tools = nav_link(AgentPage::Tools, active, None),
run = nav_link(AgentPage::Run, active, None),
- tasks = nav_link(AgentPage::Tasks, active, None),
- projects = nav_link(AgentPage::Projects, active, None),
- agy = nav_link(AgentPage::Agy, active, None),
- notes = nav_link(AgentPage::Notes, active, None),
- passvault = nav_link(AgentPage::Passvault, active, None),
- cron = nav_link(AgentPage::Cron, active, None),
- curator = nav_link(AgentPage::Curator, active, None),
)
}
@@ -206,7 +161,6 @@ fn render_chat_main(prompt: &str, outcome: Option<&AgentOutcome>, csrf: &str) ->
</div>
<div class="agent-meta" aria-label="Runtime">
<a href="/agent/kanban">Kanban</a>
- <a href="/agent/tools">Tools</a>
<a href="/agent/run">Run details</a>
</div>
</header>
@@ -242,7 +196,6 @@ fn render_workspace_main(user: &AuthUser, page: AgentPage, provider: &str, model
<div class="agent-meta" aria-label="Runtime">
<a href="/agent">Chat</a>
<a href="/agent/kanban">Kanban</a>
- <a href="/agent/tools">Tools</a>
</div>
</header>
<div class="agent-page-scroll">
@@ -264,7 +217,7 @@ fn render_ready_timeline() -> String {
<div class="agent-suggestions" aria-label="Suggestions">
<button type="button" data-agent-prompt="Inspect the current agent status and tell me the next concrete fixes.">Inspect agent</button>
<button type="button" data-agent-prompt="Plan the next agent deploy. Include risks, checks, and rollback.">Plan a deploy</button>
- <a href="/agent/tools">Open tools</a>
+ <a href="/agent/kanban">Open kanban</a>
</div>
</section>"#
.to_string()
@@ -341,70 +294,10 @@ fn render_page_content(user: &AuthUser, page: AgentPage, provider: &str, model:
match page {
AgentPage::Chat => String::new(),
AgentPage::Kanban => render_kanban_page(),
- AgentPage::Tools => render_tools_page(),
AgentPage::Run => render_run_page(user, provider, model),
- AgentPage::Tasks => render_tasks_page(),
- AgentPage::Projects => render_projects_page(),
- AgentPage::Agy => render_agy_page(),
- AgentPage::Notes => render_notes_page(),
- AgentPage::Passvault => render_passvault_page(),
- AgentPage::Cron => render_cron_page(),
- AgentPage::Curator => render_curator_page(),
}
}
-fn render_tools_page() -> String {
- let tools = [
- (
- AgentPage::Tasks,
- "Plan, create, and review work without covering the chat.",
- ),
- (
- AgentPage::Projects,
- "Open AkurAI workspaces connected to this agent.",
- ),
- (
- AgentPage::Agy,
- "Review durable personal context for the current tenant.",
- ),
- (
- AgentPage::Notes,
- "Save local working notes for this browser session.",
- ),
- (
- AgentPage::Passvault,
- "Prepare confirmation-gated credential requests.",
- ),
- (AgentPage::Cron, "Draft scheduled checks and follow-ups."),
- (
- AgentPage::Kanban,
- "Open the Rust Agent board, task, and claim workflow.",
- ),
- (
- AgentPage::Curator,
- "Find unfinished UI, noisy controls, and cleanup work.",
- ),
- ];
- let links = tools
- .into_iter()
- .map(|(page, description)| {
- format!(
- r#"<a href="{path}"><b>{label}</b><span>{description}</span></a>"#,
- path = page.path(),
- label = page.title(),
- description = esc_html(description),
- )
- })
- .collect::<Vec<_>>()
- .join("");
- format!(
- r#"<div class="agent-panel-head">
- <div><h2>Agent tools</h2><p>Use these pages when the conversation needs context, notes, credentials, or work queues.</p></div>
- </div>
- <div class="agent-tool-launcher agent-route-launcher">{links}</div>"#
- )
-}
-
fn render_run_page(user: &AuthUser, provider: &str, model: &str) -> String {
let session_id = agent_session_id(user);
let scope_id = agent_scope_id(user);
@@ -431,96 +324,6 @@ fn render_run_page(user: &AuthUser, provider: &str, model: &str) -> String {
)
}
-fn render_tasks_page() -> String {
- r#"<div class="agent-panel-head">
- <div><h2>Tasks</h2><p>Work queue for this tenant agent.</p></div>
- </div>
- <div class="agent-panel-list">
- <article><b>Review open work</b><span>Summarize active agent tasks and gaps.</span></article>
- <article><b>Create from chat</b><span>Turn the current conversation into a concrete task.</span></article>
- <article><b>Confirm before action</b><span>Collect approvals before destructive or credentialed steps.</span></article>
- </div>
- <div class="agent-panel-actions">
- <button type="button" data-agent-prompt="Review my open agent tasks and group them by priority.">Review tasks</button>
- <button type="button" data-agent-prompt="Create a concise task plan from this conversation and wait for confirmation.">Create task</button>
- </div>"#
- .to_string()
-}
-
-fn render_projects_page() -> String {
- r#"<div class="agent-panel-head">
- <div><h2>Projects</h2><p>AkurAI workspaces connected to this agent.</p></div>
- </div>
- <div class="agent-panel-list">
- <article><b>Agent Core</b><span>Gateway, CLI/TUI, and persistent AGY context.</span></article>
- <article><b>AkurAI IDP</b><span>Agent console, login, tenant routing, auth surface.</span></article>
- <article><b>AkurAI Framework</b><span>Shared theme registry and console components.</span></article>
- </div>
- <div class="agent-panel-actions">
- <button type="button" data-agent-prompt="Inspect the agent core project and report the highest-impact incomplete UI/runtime work.">Inspect project</button>
- <button type="button" data-agent-prompt="Compare AkurAI IDP and agent core UI responsibilities and suggest the clean boundary.">Check boundary</button>
- </div>"#
- .to_string()
-}
-
-fn render_agy_page() -> String {
- r#"<div class="agent-panel-head">
- <div><h2>AGY context</h2><p>Personal agent context, durable notes, and preferences.</p></div>
- </div>
- <div class="agent-panel-list">
- <article><b>Identity</b><span>Tenant agent for olibuijr@olibuijr.com.</span></article>
- <article><b>Unique features</b><span>Persistent AGY context, notes, and passvault-aware requests.</span></article>
- <article><b>Operating rule</b><span>Ask for confirmation when a tool action needs approval.</span></article>
- </div>
- <div class="agent-panel-actions">
- <button type="button" data-agent-prompt="Load AGY context for this tenant and summarize the durable preferences that matter now.">Summarize AGY</button>
- <button type="button" data-agent-prompt="Use AGY context, notes, and passvault boundaries before planning the next step.">Use context</button>
- </div>"#
- .to_string()
-}
-
-fn render_notes_page() -> String {
- r#"<div class="agent-panel-head">
- <div><h2>Notes</h2><p>Local working notes for this browser session.</p></div>
- </div>
- <textarea class="agent-notes-editor" data-notes-editor placeholder="Write working notes for this agent..."></textarea>
- <div class="agent-panel-actions">
- <button type="button" data-save-notes>Save notes</button>
- <button type="button" data-use-notes>Use notes</button>
- </div>
- <p class="agent-panel-status" data-notes-status></p>"#
- .to_string()
-}
-
-fn render_passvault_page() -> String {
- r#"<div class="agent-panel-head">
- <div><h2>Passvault</h2><p>Credential requests stay confirmation-gated.</p></div>
- </div>
- <div class="agent-panel-list">
- <article><b>Sealed by default</b><span>No credential is shown in the UI.</span></article>
- <article><b>Confirm first</b><span>The agent should ask before using a secret.</span></article>
- </div>
- <div class="agent-panel-actions">
- <button type="button" data-agent-prompt="Prepare a passvault-backed request. Ask me exactly what secret or confirmation you need before using it.">Request secret</button>
- <button type="button" data-agent-prompt="Audit whether the next task needs passvault access. If yes, ask for confirmation first.">Check access</button>
- </div>"#
- .to_string()
-}
-
-fn render_cron_page() -> String {
- r#"<div class="agent-panel-head">
- <div><h2>Cron</h2><p>Scheduled agent work.</p></div>
- </div>
- <div class="agent-panel-list">
- <article><b>Daily health</b><span>Check deploy state, gateway health, and parity drift.</span></article>
- <article><b>Follow-up sweep</b><span>Find stale tasks and unfinished UI surfaces.</span></article>
- </div>
- <div class="agent-panel-actions">
- <button type="button" data-agent-prompt="Draft a cron schedule for agent health checks and ask before enabling anything.">Draft schedule</button>
- </div>"#
- .to_string()
-}
-
fn render_kanban_page() -> String {
r#"<div class="agent-panel-head">
<div><h2>Kanban</h2><p>Rust Agent board, tasks, claims, and project delivery state.</p></div>
@@ -556,21 +359,6 @@ fn render_kanban_page() -> String {
.to_string()
}
-fn render_curator_page() -> String {
- r#"<div class="agent-panel-head">
- <div><h2>Curator</h2><p>Cleanup and quality pass.</p></div>
- </div>
- <div class="agent-panel-list">
- <article><b>Remove dead UI</b><span>Find controls that do not map to a real workflow.</span></article>
- <article><b>Tighten copy</b><span>Reduce status noise and internal implementation labels.</span></article>
- <article><b>Verify live</b><span>Use screenshots and smoke prompts after deploy.</span></article>
- </div>
- <div class="agent-panel-actions">
- <button type="button" data-agent-prompt="Act as curator: identify UI bloat, dead controls, and the smallest fixes to make this console feel finished.">Run curator</button>
- </div>"#
- .to_string()
-}
-
fn agent_scope_id(user: &AuthUser) -> String {
format!("idp:{}", user.tenant_id)
}
@@ -600,10 +388,9 @@ mod tests {
fn body_contains_agent_os_surfaces() {
let html = agent_body(&user(), "csrf", "", None);
assert!(html.contains("AkurAI"));
- assert!(html.contains("Passvault"));
assert!(html.contains("Kanban"));
assert!(html.contains(r#"href="/agent/kanban""#));
- assert!(html.contains(r#"href="/agent/notes""#));
+ assert!(html.contains(r#"href="/agent/run""#));
assert!(!html.contains("data-panel-trigger"));
assert!(!html.contains("data-agent-panel"));
assert!(!html.contains("data-panel-template"));
@@ -613,6 +400,19 @@ mod tests {
assert!(html.contains("tool_call"));
}
+ #[test]
+ fn sidebar_drops_agent_surfaces_slop() {
+ let html = agent_body(&user(), "csrf", "", None);
+ assert!(!html.contains("Agent surfaces"));
+ assert!(!html.contains(r#"href="/agent/tasks""#));
+ assert!(!html.contains(r#"href="/agent/projects""#));
+ assert!(!html.contains(r#"href="/agent/notes""#));
+ assert!(!html.contains(r#"href="/agent/passvault""#));
+ assert!(!html.contains(r#"href="/agent/cron""#));
+ assert!(!html.contains(r#"href="/agent/curator""#));
+ assert!(!html.contains(r#"href="/agent/tools""#));
+ }
+
#[test]
fn kanban_body_contains_board_on_own_page() {
let html = agent_static_page_body(&user(), "csrf", AgentPage::Kanban);
@@ -624,10 +424,11 @@ mod tests {
}
#[test]
- fn notes_body_uses_route_not_overlay_template() {
- let html = agent_static_page_body(&user(), "csrf", AgentPage::Notes);
- assert!(html.contains(r#"data-agent-page="/agent/notes""#));
- assert!(html.contains("data-use-notes"));
+ fn run_body_uses_route_and_shows_real_runtime() {
+ let html = agent_static_page_body(&user(), "csrf", AgentPage::Run);
+ assert!(html.contains(r#"data-agent-page="/agent/run""#));
+ assert!(html.contains("Current run"));
+ assert!(html.contains("olibuijr@olibuijr.com"));
assert!(!html.contains("data-panel-template"));
}
diff --git a/src/routes/auth_pages.rs b/src/routes/auth_pages.rs
index e63655e..6b726a9 100644
--- a/src/routes/auth_pages.rs
+++ b/src/routes/auth_pages.rs
@@ -136,10 +136,10 @@ async fn login_submit(headers: HeaderMap, Form(form): Form<LoginForm>) -> Respon
let now = now_epoch();
// Check lockout
- if let Some(locked_until) = user.locked_until {
- if locked_until > now {
- return login_redirect("Account temporarily locked. Try again later.", &return_to);
- }
+ if let Some(locked_until) = user.locked_until
+ && locked_until > now
+ {
+ return login_redirect("Account temporarily locked. Try again later.", &return_to);
}
// Verify password
@@ -483,10 +483,10 @@ fn safe_return_to(input: Option<&str>) -> String {
// Absolute URL: must match our base_url origin
let base = &config::get().base_url;
- if let Some(origin) = extract_origin(base) {
- if val.starts_with(&origin) {
- return val.to_string();
- }
+ if let Some(origin) = extract_origin(base)
+ && val.starts_with(&origin)
+ {
+ return val.to_string();
}
"/".to_string()
diff --git a/src/routes/authorize.rs b/src/routes/authorize.rs
index dcf8f8b..265dd0d 100644
--- a/src/routes/authorize.rs
+++ b/src/routes/authorize.rs
@@ -38,7 +38,14 @@ async fn authorize_endpoint(
// Validate required params
let client_id = match ¶ms.client_id {
Some(c) if !c.is_empty() => c.clone(),
- _ => return auth_error_redirect(None, params.state.as_deref(), "invalid_request", "Missing client_id"),
+ _ => {
+ return auth_error_redirect(
+ None,
+ params.state.as_deref(),
+ "invalid_request",
+ "Missing client_id",
+ );
+ }
};
let redirect_uri = match ¶ms.redirect_uri {
@@ -100,15 +107,15 @@ async fn authorize_endpoint(
}
// PKCE validation (require S256 if code_challenge provided)
- if let Some(ref method) = params.code_challenge_method {
- if method != "S256" {
- return auth_error_redirect(
- Some(&redirect_uri),
- params.state.as_deref(),
- "invalid_request",
- "Only S256 code_challenge_method is supported",
- );
- }
+ if let Some(ref method) = params.code_challenge_method
+ && method != "S256"
+ {
+ return auth_error_redirect(
+ Some(&redirect_uri),
+ params.state.as_deref(),
+ "invalid_request",
+ "Only S256 code_challenge_method is supported",
+ );
}
if params.code_challenge.is_some() && params.code_challenge_method.is_none() {
return auth_error_redirect(
@@ -136,13 +143,18 @@ async fn authorize_endpoint(
urlencoded(requested_scopes),
urlencoded(params.state.as_deref().unwrap_or("")),
urlencoded(params.nonce.as_deref().unwrap_or("")),
- params.code_challenge.as_ref().map(|c|
- format!("&code_challenge={}&code_challenge_method={}",
+ params
+ .code_challenge
+ .as_ref()
+ .map(|c| format!(
+ "&code_challenge={}&code_challenge_method={}",
urlencoded(c),
- urlencoded(params.code_challenge_method.as_deref().unwrap_or("S256")))
- ).unwrap_or_default(),
+ urlencoded(params.code_challenge_method.as_deref().unwrap_or("S256"))
+ ))
+ .unwrap_or_default(),
);
- return Redirect::to(&format!("/login?return_to={}", urlencoded(&return_url))).into_response();
+ return Redirect::to(&format!("/login?return_to={}", urlencoded(&return_url)))
+ .into_response();
}
};
@@ -252,7 +264,10 @@ fn auth_error_redirect(
) -> axum::response::Response {
match redirect_uri {
Some(uri) => {
- let mut location = format!("{uri}?error={error}&error_description={}", urlencoded(description));
+ let mut location = format!(
+ "{uri}?error={error}&error_description={}",
+ urlencoded(description)
+ );
if let Some(s) = state {
location.push_str(&format!("&state={}", urlencoded(s)));
}
@@ -268,7 +283,12 @@ fn auth_error_page(description: &str) -> axum::response::Response {
<html><head><title>Authorization Error</title></head>
<body><h1>Authorization Error</h1><p>{description}</p></body></html>"#,
);
- (StatusCode::BAD_REQUEST, [("content-type", "text/html")], html).into_response()
+ (
+ StatusCode::BAD_REQUEST,
+ [("content-type", "text/html")],
+ html,
+ )
+ .into_response()
}
fn urlencoded(s: &str) -> String {
diff --git a/src/routes/introspect.rs b/src/routes/introspect.rs
index 71c31d8..f33a428 100644
--- a/src/routes/introspect.rs
+++ b/src/routes/introspect.rs
@@ -1,4 +1,6 @@
-use axum::{Json, Router, extract::Request, http::StatusCode, response::IntoResponse, routing::post};
+use axum::{
+ Json, Router, extract::Request, http::StatusCode, response::IntoResponse, routing::post,
+};
use serde::Deserialize;
use serde_json::{Value, json};
@@ -27,8 +29,8 @@ async fn introspect_endpoint(request: Request) -> impl IntoResponse {
Err(_) => return (StatusCode::OK, Json(json!({"active": false}))).into_response(),
};
- let mut params: IntrospectRequest = serde_json::from_slice(&body_bytes)
- .unwrap_or_else(|_| parse_form_urlencoded(&body_bytes));
+ let mut params: IntrospectRequest =
+ serde_json::from_slice(&body_bytes).unwrap_or_else(|_| parse_form_urlencoded(&body_bytes));
// Basic auth overrides body params
if let Some(id) = basic_id {
@@ -39,7 +41,11 @@ async fn introspect_endpoint(request: Request) -> impl IntoResponse {
}
if params.client_id.is_none() {
- return (StatusCode::UNAUTHORIZED, Json(json!({"error": "invalid_client"}))).into_response();
+ return (
+ StatusCode::UNAUTHORIZED,
+ Json(json!({"error": "invalid_client"})),
+ )
+ .into_response();
}
let token = match params.token {
diff --git a/src/routes/revoke.rs b/src/routes/revoke.rs
index 107ac5c..21e5d86 100644
--- a/src/routes/revoke.rs
+++ b/src/routes/revoke.rs
@@ -1,4 +1,6 @@
-use axum::{Json, Router, extract::Request, http::StatusCode, response::IntoResponse, routing::post};
+use axum::{
+ Json, Router, extract::Request, http::StatusCode, response::IntoResponse, routing::post,
+};
use serde::Deserialize;
use serde_json::json;
@@ -26,8 +28,8 @@ async fn revoke_endpoint(request: Request) -> impl IntoResponse {
Err(_) => return StatusCode::OK.into_response(),
};
- let mut params: RevokeRequest = serde_json::from_slice(&body_bytes)
- .unwrap_or_else(|_| parse_form_urlencoded(&body_bytes));
+ let mut params: RevokeRequest =
+ serde_json::from_slice(&body_bytes).unwrap_or_else(|_| parse_form_urlencoded(&body_bytes));
if let Some(id) = basic_id {
params.client_id = Some(id);
@@ -38,7 +40,11 @@ async fn revoke_endpoint(request: Request) -> impl IntoResponse {
// Client auth required
if params.client_id.is_none() {
- return (StatusCode::UNAUTHORIZED, Json(json!({"error": "invalid_client"}))).into_response();
+ return (
+ StatusCode::UNAUTHORIZED,
+ Json(json!({"error": "invalid_client"})),
+ )
+ .into_response();
}
let token = match params.token {
diff --git a/src/routes/token.rs b/src/routes/token.rs
index 9dd8e34..1401042 100644
--- a/src/routes/token.rs
+++ b/src/routes/token.rs
@@ -12,7 +12,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
use crate::config;
use crate::db::with_db;
use crate::lib::audit::{self, log_audit_event};
-use crate::lib::crypto::{generate_secure_token, sha256, constant_time_equal};
+use crate::lib::crypto::{constant_time_equal, generate_secure_token, sha256};
use crate::lib::jwt::sign_token;
use crate::lib::pkce::verify_code_challenge;
@@ -41,11 +41,17 @@ async fn token_endpoint(request: Request) -> impl IntoResponse {
// Parse body as form or JSON
let body_bytes = match axum::body::to_bytes(request.into_body(), 1024 * 64).await {
Ok(b) => b,
- Err(_) => return token_error(StatusCode::BAD_REQUEST, "invalid_request", "Failed to read body"),
+ Err(_) => {
+ return token_error(
+ StatusCode::BAD_REQUEST,
+ "invalid_request",
+ "Failed to read body",
+ );
+ }
};
- let mut params: TokenRequest = serde_json::from_slice(&body_bytes)
- .unwrap_or_else(|_| parse_form_urlencoded(&body_bytes));
+ let mut params: TokenRequest =
+ serde_json::from_slice(&body_bytes).unwrap_or_else(|_| parse_form_urlencoded(&body_bytes));
// Basic auth overrides body params for client credentials
if let Some(id) = basic_client_id {
@@ -57,14 +63,24 @@ async fn token_endpoint(request: Request) -> impl IntoResponse {
let grant_type = match ¶ms.grant_type {
Some(gt) => gt.as_str(),
- None => return token_error(StatusCode::BAD_REQUEST, "invalid_request", "Missing grant_type"),
+ None => {
+ return token_error(
+ StatusCode::BAD_REQUEST,
+ "invalid_request",
+ "Missing grant_type",
+ );
+ }
};
match grant_type {
"authorization_code" => handle_authorization_code(params, &ip).await,
"refresh_token" => handle_refresh_token(params, &ip).await,
"client_credentials" => handle_client_credentials(params, &ip).await,
- _ => token_error(StatusCode::BAD_REQUEST, "unsupported_grant_type", "Unsupported grant_type"),
+ _ => token_error(
+ StatusCode::BAD_REQUEST,
+ "unsupported_grant_type",
+ "Unsupported grant_type",
+ ),
}
}
@@ -75,11 +91,23 @@ async fn handle_authorization_code(params: TokenRequest, ip: &str) -> axum::resp
};
let client_id = match ¶ms.client_id {
Some(c) => c.clone(),
- None => return token_error(StatusCode::BAD_REQUEST, "invalid_request", "Missing client_id"),
+ None => {
+ return token_error(
+ StatusCode::BAD_REQUEST,
+ "invalid_request",
+ "Missing client_id",
+ );
+ }
};
let redirect_uri = match ¶ms.redirect_uri {
Some(r) => r.clone(),
- None => return token_error(StatusCode::BAD_REQUEST, "invalid_request", "Missing redirect_uri"),
+ None => {
+ return token_error(
+ StatusCode::BAD_REQUEST,
+ "invalid_request",
+ "Missing redirect_uri",
+ );
+ }
};
let now = now_secs();
@@ -111,7 +139,13 @@ async fn handle_authorization_code(params: TokenRequest, ip: &str) -> axum::resp
let auth_code = match auth_code {
Some(ac) => ac,
- None => return token_error(StatusCode::BAD_REQUEST, "invalid_grant", "Invalid authorization code"),
+ None => {
+ return token_error(
+ StatusCode::BAD_REQUEST,
+ "invalid_grant",
+ "Invalid authorization code",
+ );
+ }
};
// Validate
@@ -124,21 +158,41 @@ async fn handle_authorization_code(params: TokenRequest, ip: &str) -> axum::resp
)
.ok();
});
- return token_error(StatusCode::BAD_REQUEST, "invalid_grant", "Authorization code already used");
+ return token_error(
+ StatusCode::BAD_REQUEST,
+ "invalid_grant",
+ "Authorization code already used",
+ );
}
if auth_code.expires_at < now {
- return token_error(StatusCode::BAD_REQUEST, "invalid_grant", "Authorization code expired");
+ return token_error(
+ StatusCode::BAD_REQUEST,
+ "invalid_grant",
+ "Authorization code expired",
+ );
}
if auth_code.client_id != client_id {
- return token_error(StatusCode::BAD_REQUEST, "invalid_grant", "Client ID mismatch");
+ return token_error(
+ StatusCode::BAD_REQUEST,
+ "invalid_grant",
+ "Client ID mismatch",
+ );
}
if auth_code.redirect_uri != redirect_uri {
- return token_error(StatusCode::BAD_REQUEST, "invalid_grant", "Redirect URI mismatch");
+ return token_error(
+ StatusCode::BAD_REQUEST,
+ "invalid_grant",
+ "Redirect URI mismatch",
+ );
}
// Verify client secret
if !verify_client_secret(&client_id, params.client_secret.as_deref()) {
- return token_error(StatusCode::UNAUTHORIZED, "invalid_client", "Invalid client credentials");
+ return token_error(
+ StatusCode::UNAUTHORIZED,
+ "invalid_client",
+ "Invalid client credentials",
+ );
}
// PKCE verification
@@ -146,17 +200,30 @@ async fn handle_authorization_code(params: TokenRequest, ip: &str) -> axum::resp
let method = auth_code.code_challenge_method.as_deref().unwrap_or("S256");
let verifier = match ¶ms.code_verifier {
Some(v) => v,
- None => return token_error(StatusCode::BAD_REQUEST, "invalid_request", "Missing code_verifier"),
+ None => {
+ return token_error(
+ StatusCode::BAD_REQUEST,
+ "invalid_request",
+ "Missing code_verifier",
+ );
+ }
};
if !verify_code_challenge(verifier, challenge, method) {
- return token_error(StatusCode::BAD_REQUEST, "invalid_grant", "PKCE verification failed");
+ return token_error(
+ StatusCode::BAD_REQUEST,
+ "invalid_grant",
+ "PKCE verification failed",
+ );
}
}
// Mark code as used
with_db(|conn| {
- conn.execute("UPDATE auth_codes SET used = 1 WHERE code = ?1", rusqlite::params![code])
- .ok();
+ conn.execute(
+ "UPDATE auth_codes SET used = 1 WHERE code = ?1",
+ rusqlite::params![code],
+ )
+ .ok();
});
// Load user for token claims
@@ -199,17 +266,35 @@ async fn handle_authorization_code(params: TokenRequest, ip: &str) -> axum::resp
let (kid, private_key) = match get_active_signing_key() {
Some(k) => k,
- None => return token_error(StatusCode::INTERNAL_SERVER_ERROR, "server_error", "No signing key available"),
+ None => {
+ return token_error(
+ StatusCode::INTERNAL_SERVER_ERROR,
+ "server_error",
+ "No signing key available",
+ );
+ }
};
let access_token = match sign_token(&access_token_claims, &private_key, &kid, 3600) {
Ok(t) => t,
- Err(_) => return token_error(StatusCode::INTERNAL_SERVER_ERROR, "server_error", "Failed to sign token"),
+ Err(_) => {
+ return token_error(
+ StatusCode::INTERNAL_SERVER_ERROR,
+ "server_error",
+ "Failed to sign token",
+ );
+ }
};
let id_token = match sign_token(&id_token_claims, &private_key, &kid, 3600) {
Ok(t) => t,
- Err(_) => return token_error(StatusCode::INTERNAL_SERVER_ERROR, "server_error", "Failed to sign token"),
+ Err(_) => {
+ return token_error(
+ StatusCode::INTERNAL_SERVER_ERROR,
+ "server_error",
+ "Failed to sign token",
+ );
+ }
};
// Issue refresh token if offline_access scope
@@ -260,15 +345,31 @@ async fn handle_authorization_code(params: TokenRequest, ip: &str) -> axum::resp
async fn handle_refresh_token(params: TokenRequest, ip: &str) -> axum::response::Response {
let rt = match ¶ms.refresh_token {
Some(t) => t.clone(),
- None => return token_error(StatusCode::BAD_REQUEST, "invalid_request", "Missing refresh_token"),
+ None => {
+ return token_error(
+ StatusCode::BAD_REQUEST,
+ "invalid_request",
+ "Missing refresh_token",
+ );
+ }
};
let client_id = match ¶ms.client_id {
Some(c) => c.clone(),
- None => return token_error(StatusCode::BAD_REQUEST, "invalid_request", "Missing client_id"),
+ None => {
+ return token_error(
+ StatusCode::BAD_REQUEST,
+ "invalid_request",
+ "Missing client_id",
+ );
+ }
};
if !verify_client_secret(&client_id, params.client_secret.as_deref()) {
- return token_error(StatusCode::UNAUTHORIZED, "invalid_client", "Invalid client credentials");
+ return token_error(
+ StatusCode::UNAUTHORIZED,
+ "invalid_client",
+ "Invalid client credentials",
+ );
}
let rt_hash = sha256(&rt);
@@ -296,17 +397,35 @@ async fn handle_refresh_token(params: TokenRequest, ip: &str) -> axum::response:
let stored = match stored {
Some(s) => s,
- None => return token_error(StatusCode::BAD_REQUEST, "invalid_grant", "Invalid refresh token"),
+ None => {
+ return token_error(
+ StatusCode::BAD_REQUEST,
+ "invalid_grant",
+ "Invalid refresh token",
+ );
+ }
};
if stored.revoked {
- return token_error(StatusCode::BAD_REQUEST, "invalid_grant", "Refresh token revoked");
+ return token_error(
+ StatusCode::BAD_REQUEST,
+ "invalid_grant",
+ "Refresh token revoked",
+ );
}
if stored.expires_at < now {
- return token_error(StatusCode::BAD_REQUEST, "invalid_grant", "Refresh token expired");
+ return token_error(
+ StatusCode::BAD_REQUEST,
+ "invalid_grant",
+ "Refresh token expired",
+ );
}
if stored.client_id != client_id {
- return token_error(StatusCode::BAD_REQUEST, "invalid_grant", "Client ID mismatch");
+ return token_error(
+ StatusCode::BAD_REQUEST,
+ "invalid_grant",
+ "Client ID mismatch",
+ );
}
// Rotate: revoke old, issue new
@@ -363,16 +482,34 @@ async fn handle_refresh_token(params: TokenRequest, ip: &str) -> axum::response:
let (kid, private_key) = match get_active_signing_key() {
Some(k) => k,
- None => return token_error(StatusCode::INTERNAL_SERVER_ERROR, "server_error", "No signing key"),
+ None => {
+ return token_error(
+ StatusCode::INTERNAL_SERVER_ERROR,
+ "server_error",
+ "No signing key",
+ );
+ }
};
let access_token = match sign_token(&access_token_claims, &private_key, &kid, 3600) {
Ok(t) => t,
- Err(_) => return token_error(StatusCode::INTERNAL_SERVER_ERROR, "server_error", "Sign failed"),
+ Err(_) => {
+ return token_error(
+ StatusCode::INTERNAL_SERVER_ERROR,
+ "server_error",
+ "Sign failed",
+ );
+ }
};
let id_token = match sign_token(&id_token_claims, &private_key, &kid, 3600) {
Ok(t) => t,
- Err(_) => return token_error(StatusCode::INTERNAL_SERVER_ERROR, "server_error", "Sign failed"),
+ Err(_) => {
+ return token_error(
+ StatusCode::INTERNAL_SERVER_ERROR,
+ "server_error",
+ "Sign failed",
+ );
+ }
};
let audit_meta = json!({"grant_type": "refresh_token", "client_id": client_id});
@@ -402,11 +539,21 @@ async fn handle_refresh_token(params: TokenRequest, ip: &str) -> axum::response:
async fn handle_client_credentials(params: TokenRequest, ip: &str) -> axum::response::Response {
let client_id = match ¶ms.client_id {
Some(c) => c.clone(),
- None => return token_error(StatusCode::BAD_REQUEST, "invalid_request", "Missing client_id"),
+ None => {
+ return token_error(
+ StatusCode::BAD_REQUEST,
+ "invalid_request",
+ "Missing client_id",
+ );
+ }
};
if !verify_client_secret(&client_id, params.client_secret.as_deref()) {
- return token_error(StatusCode::UNAUTHORIZED, "invalid_client", "Invalid client credentials");
+ return token_error(
+ StatusCode::UNAUTHORIZED,
+ "invalid_client",
+ "Invalid client credentials",
+ );
}
// Verify client supports client_credentials grant
@@ -428,12 +575,22 @@ async fn handle_client_credentials(params: TokenRequest, ip: &str) -> axum::resp
let client = match client {
Some(c) => c,
- None => return token_error(StatusCode::UNAUTHORIZED, "invalid_client", "Client not found"),
+ None => {
+ return token_error(
+ StatusCode::UNAUTHORIZED,
+ "invalid_client",
+ "Client not found",
+ );
+ }
};
let grant_types = crate::lib::parse_json_or_space_separated(&client.grant_types);
if !grant_types.iter().any(|g| g == "client_credentials") {
- return token_error(StatusCode::BAD_REQUEST, "unauthorized_client", "Grant type not allowed");
+ return token_error(
+ StatusCode::BAD_REQUEST,
+ "unauthorized_client",
+ "Grant type not allowed",
+ );
}
let requested_scope = params.scope.as_deref().unwrap_or(&client.scopes);
@@ -452,12 +609,24 @@ async fn handle_client_credentials(params: TokenRequest, ip: &str) -> axum::resp
let (kid, private_key) = match get_active_signing_key() {
Some(k) => k,
- None => return token_error(StatusCode::INTERNAL_SERVER_ERROR, "server_error", "No signing key"),
+ None => {
+ return token_error(
+ StatusCode::INTERNAL_SERVER_ERROR,
+ "server_error",
+ "No signing key",
+ );
+ }
};
let access_token = match sign_token(&access_token_claims, &private_key, &kid, 3600) {
Ok(t) => t,
- Err(_) => return token_error(StatusCode::INTERNAL_SERVER_ERROR, "server_error", "Sign failed"),
+ Err(_) => {
+ return token_error(
+ StatusCode::INTERNAL_SERVER_ERROR,
+ "server_error",
+ "Sign failed",
+ );
+ }
};
let audit_meta = json!({"grant_type": "client_credentials", "client_id": client_id});
diff --git a/src/routes/userinfo.rs b/src/routes/userinfo.rs
index 9f39d9c..cb781eb 100644
--- a/src/routes/userinfo.rs
+++ b/src/routes/userinfo.rs
@@ -1,7 +1,7 @@
-use axum::{Json, Router, routing::get};
use axum::extract::Request;
use axum::http::StatusCode;
use axum::response::IntoResponse;
+use axum::{Json, Router, routing::get};
use serde_json::{Value, json};
use crate::db::with_db;
diff --git a/src/routes/well_known.rs b/src/routes/well_known.rs
index ad040dd..73c52f7 100644
--- a/src/routes/well_known.rs
+++ b/src/routes/well_known.rs
@@ -7,7 +7,10 @@ use crate::lib::jwt::export_public_key_jwk;
pub fn router() -> Router {
Router::new()
- .route("/.well-known/openid-configuration", get(openid_configuration))
+ .route(
+ "/.well-known/openid-configuration",
+ get(openid_configuration),
+ )
.route("/jwks", get(jwks))
}
@@ -45,9 +48,7 @@ async fn jwks() -> Json<Value> {
})
.expect("failed to query signing_keys")
.filter_map(|r| r.ok())
- .filter_map(|(kid, public_key, _alg)| {
- export_public_key_jwk(&public_key, &kid).ok()
- })
+ .filter_map(|(kid, public_key, _alg)| export_public_key_jwk(&public_key, &kid).ok())
.collect()
});