Menu
Public repository / master
AkurAI-Notes / crates/notes-api/src/router_setup.rs
Latest change f3a035b12c8f9e52dab037eccf159be9d932d6dc - AKURAI-NOTES-7 fix cargo fmt formatting by Ólafur Búi Ólafsson
use crate::auth;
use crate::auth::idp_client::IdpConfig;
use crate::handlers;
use akurai_http::{Request, Response};
use akurai_json::Value;
use akurai_storage::BTree;
use notes_core::metadata::ObjectMetadata;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
/// A registered route: HTTP method, URL pattern, and handler closure.
pub struct Route {
pub method: &'static str,
pub pattern: &'static str,
pub handler: Box<dyn Fn(&Request) -> Response + Send + Sync>,
}
/// Shared Notes application state
pub struct NotesState {
pub db: Arc<Mutex<BTree>>,
pub objects: Vec<ObjectMetadata>,
pub frontend_dir: PathBuf,
pub embed_url: Option<String>,
pub embed_model: Option<String>,
pub embed_api_key: Option<String>,
pub idp_cfg: IdpConfig,
pub telemetry: Option<crate::telemetry::NotesTelemetry>,
#[allow(dead_code)]
_delivery: Option<crate::telemetry::TelemetryDelivery>,
}
impl NotesState {
pub fn new(db_path: &str, frontend_dir: PathBuf, idp_cfg: IdpConfig) -> Result<Self, String> {
let btree = BTree::open(db_path).map_err(|e| format!("failed to open storage: {e}"))?;
let objects = ObjectMetadata::standard_objects();
let (delivery, telemetry) = crate::telemetry::init_from_env();
Ok(Self {
db: Arc::new(Mutex::new(btree)),
objects,
frontend_dir,
embed_url: std::env::var("AKURAI_NOTES_EMBED_URL")
.ok()
.filter(|s| !s.trim().is_empty()),
embed_model: std::env::var("AKURAI_NOTES_EMBED_MODEL")
.ok()
.filter(|s| !s.trim().is_empty()),
embed_api_key: std::env::var("AKURAI_NOTES_EMBED_API_KEY")
.ok()
.filter(|s| !s.trim().is_empty()),
idp_cfg,
telemetry,
_delivery: delivery,
})
}
pub fn now() -> i64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64
}
}
pub fn r(
state: &'static str,
p: &'static str,
h: Box<dyn Fn(&Request) -> Response + Send + Sync>,
) -> Route {
Route {
method: state,
pattern: p,
handler: h,
}
}
/// Build the route table for the Notes API
pub fn build_router(state: Arc<Mutex<NotesState>>) -> Vec<Route> {
vec![
r("OPTIONS", "/*", options_handler()),
r(
"GET",
"/",
handlers::ssr::ssr_page_route(Arc::clone(&state), "index", None),
),
r(
"GET",
"/app",
handlers::ssr::ssr_page_route(Arc::clone(&state), "app", None),
),
r(
"GET",
"/app/search",
handlers::ssr::ssr_page_route(Arc::clone(&state), "search", None),
),
r(
"GET",
"/app/settings",
handlers::ssr::ssr_page_route(Arc::clone(&state), "settings", None),
),
// Auth routes (before catch-all /*)
r("GET", "/auth/login", auth::login_route(Arc::clone(&state))),
r(
"GET",
"/auth/callback",
auth::callback_route(Arc::clone(&state)),
),
r(
"GET",
"/auth/logout",
auth::logout_route(Arc::clone(&state)),
),
r("GET", "/api/me", auth::me_route(Arc::clone(&state))),
r(
"POST",
"/api/client-logs",
handlers::client_logs::client_logs_route(),
),
// Meta / health
r(
"GET",
"/api/_meta",
handlers::meta::meta_route(Arc::clone(&state)),
),
r("GET", "/api/health", health_route()),
// Notes CRUD
r(
"GET",
"/api/notes",
handlers::crud::list_route(Arc::clone(&state), "notes"),
),
r(
"GET",
"/api/notes/:id",
handlers::crud::get_route(Arc::clone(&state), "notes"),
),
r(
"POST",
"/api/notes",
handlers::crud::create_route(Arc::clone(&state), "notes"),
),
r(
"PUT",
"/api/notes/:id",
handlers::crud::update_route(Arc::clone(&state), "notes"),
),
r(
"DELETE",
"/api/notes/:id",
handlers::crud::delete_route(Arc::clone(&state), "notes"),
),
// Notebooks CRUD
r(
"GET",
"/api/notebooks",
handlers::crud::list_route(Arc::clone(&state), "notebooks"),
),
r(
"GET",
"/api/notebooks/:id",
handlers::crud::get_route(Arc::clone(&state), "notebooks"),
),
r(
"POST",
"/api/notebooks",
handlers::crud::create_route(Arc::clone(&state), "notebooks"),
),
r(
"PUT",
"/api/notebooks/:id",
handlers::crud::update_route(Arc::clone(&state), "notebooks"),
),
r(
"DELETE",
"/api/notebooks/:id",
handlers::crud::delete_route(Arc::clone(&state), "notebooks"),
),
// Tags CRUD
r(
"GET",
"/api/tags",
handlers::crud::list_route(Arc::clone(&state), "tags"),
),
r(
"GET",
"/api/tags/:id",
handlers::crud::get_route(Arc::clone(&state), "tags"),
),
r(
"POST",
"/api/tags",
handlers::crud::create_route(Arc::clone(&state), "tags"),
),
r(
"PUT",
"/api/tags/:id",
handlers::crud::update_route(Arc::clone(&state), "tags"),
),
r(
"DELETE",
"/api/tags/:id",
handlers::crud::delete_route(Arc::clone(&state), "tags"),
),
// Search / Timeline
r(
"GET",
"/api/search",
handlers::search::search_route(Arc::clone(&state)),
),
// Catch-all static files (must be LAST)
r(
"GET",
"/*",
handlers::static_files::static_file_route(Arc::clone(&state)),
),
]
}
/// Upper bound helper for BTree range queries: appends a 0xFF byte so the bound
/// is strictly greater than every key with the given prefix.
pub fn upper_bound(prefix: &[u8]) -> Vec<u8> {
let mut bound = prefix.to_vec();
bound.push(0xff);
bound
}
pub fn json_response(data: Value) -> Response {
let body = data.to_json();
Response::ok()
.with_header("Content-Type", "application/json")
.with_header("Access-Control-Allow-Origin", "*")
.with_body("application/json", body.into_bytes())
}
pub fn error_response(status: u16, msg: &str) -> Response {
let data = Value::Object(vec![("error".into(), Value::Str(msg.into()))]);
let mut resp = json_response(data);
resp.status = status;
resp.reason = match status {
400 => "Bad Request",
401 => "Unauthorized",
404 => "Not Found",
500 => "Internal Server Error",
_ => "Error",
}
.into();
resp
}
pub fn unauthorized() -> Response {
error_response(401, "unauthorized")
}
fn health_route() -> Box<dyn Fn(&Request) -> Response + Send + Sync> {
Box::new(|_req: &Request| {
json_response(Value::Object(vec![
("app".into(), Value::Str("akurai-notes".into())),
(
"version".into(),
Value::Str(env!("CARGO_PKG_VERSION").into()),
),
("status".into(), Value::Str("ok".into())),
("framework".into(), Value::Str("AkurAI-Framework".into())),
("framework_version".into(), Value::Str("0.8.2".into())),
]))
})
}
pub fn options_handler() -> Box<dyn Fn(&Request) -> Response + Send + Sync> {
Box::new(|_req: &Request| {
Response::new(204)
.with_header("Access-Control-Allow-Origin", "*")
.with_header(
"Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS",
)
.with_header(
"Access-Control-Allow-Headers",
"Content-Type, Authorization",
)
.with_header("Access-Control-Max-Age", "86400")
})
}
pub fn not_found(msg: &str) -> Response {
error_response(404, msg)
}
pub fn bad_request(msg: &str) -> Response {
error_response(400, msg)
}
pub fn internal_error(msg: &str) -> Response {
error_response(500, msg)
}