Menu
Public repository / 54570403655120661aa5aeb6120646216896cc76
AkurAI-CRM / crates/crm-api/src/auth/mod.rs
Latest change c7bf22ef78ad08f1f59b04d08e88d86e4ca91815 - feat: add tenant-safe account graph by Ólafur Búi Ólafsson
pub mod idp_client;
pub mod session;
use crate::router_setup::{internal_error, json_response, CrmState};
use akurai_http::{Request, Response};
use akurai_json::Value;
use std::sync::{Arc, Mutex};
/// GET /auth/login — redirect to IDP /authorize
pub fn login_route(state: Arc<Mutex<CrmState>>) -> Box<dyn Fn(&Request) -> Response + Send + Sync> {
let _ = state; // unused, kept for signature consistency
Box::new(move |_req: &Request| {
let config = match idp_client::Config::from_env() {
Ok(config) => config,
Err(error) => return internal_error(&error),
};
let state_token = format!(
"crm_{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
);
let location = config.authorization_url(&state_token);
let cookie =
format!("crm_oauth_state={state_token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=600");
let mut resp = Response::ok();
resp.status = 302;
resp.reason = "Found".to_string();
resp = resp.with_header("Location", &location);
resp = resp.with_header("Set-Cookie", &cookie);
resp
})
}
/// GET /auth/callback — exchange code, create session, redirect to dashboard
pub fn callback_route(
state: Arc<Mutex<CrmState>>,
) -> Box<dyn Fn(&Request) -> Response + Send + Sync> {
Box::new(move |req: &Request| {
let query = req.query.as_deref().unwrap_or("");
let mut code = String::new();
let mut returned_state = String::new();
for pair in query.split('&') {
if let Some((key, value)) = pair.split_once('=') {
match key {
"code" => code = value.to_string(),
"state" => returned_state = value.to_string(),
_ => {}
}
}
}
if code.is_empty() || returned_state.is_empty() {
return auth_redirect_error();
}
if let Err(error) = idp_client::Config::from_env() {
return internal_error(&error);
}
match idp_client::exchange_code(&code, &returned_state) {
Ok(userinfo) => {
let session_id = {
let state_lock = match state.lock() {
Ok(s) => s,
Err(_) => return internal_error("lock poisoned"),
};
let mut db = match state_lock.db.lock() {
Ok(db) => db,
Err(_) => return internal_error("db lock poisoned"),
};
match session::create(
&mut db,
&userinfo.sub,
&userinfo.email,
&userinfo.tenant_id,
) {
Ok(session_id) => session_id,
Err(error) => return internal_error(&error),
}
};
let cookie = format!(
"crm_session={session_id}; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400"
);
let mut resp = Response::ok();
resp.status = 302;
resp.reason = "Found".to_string();
resp = resp.with_header("Location", "/dashboard.html");
resp = resp.with_header("Set-Cookie", &cookie);
resp
}
Err(_) => auth_redirect_error(),
}
})
}
/// GET /api/me — return current user info from session
pub fn me_route(state: Arc<Mutex<CrmState>>) -> Box<dyn Fn(&Request) -> Response + Send + Sync> {
Box::new(move |req: &Request| {
let session_id = session::extract_session_id(req);
let user = match session_id {
Some(ref sid) => {
let state_lock = match state.lock() {
Ok(s) => s,
Err(_) => return internal_error("lock poisoned"),
};
let mut db = match state_lock.db.lock() {
Ok(db) => db,
Err(_) => return internal_error("db lock poisoned"),
};
session::get_user(&mut db, sid)
}
None => None,
};
match user {
Some(u) => json_response(Value::Object(vec![
("authenticated".into(), Value::Bool(true)),
("sub".into(), Value::Str(u.sub)),
("email".into(), Value::Str(u.email)),
("tenantId".into(), Value::Str(u.tenant_id)),
])),
None => json_response(Value::Object(vec![(
"authenticated".into(),
Value::Bool(false),
)])),
}
})
}
/// GET /auth/logout — delete session and clear cookie
pub fn logout_route(
state: Arc<Mutex<CrmState>>,
) -> Box<dyn Fn(&Request) -> Response + Send + Sync> {
Box::new(move |req: &Request| {
if let Some(sid) = session::extract_session_id(req) {
let state_lock = match state.lock() {
Ok(lock) => lock,
Err(_) => return internal_error("lock poisoned"),
};
let mut db = match state_lock.db.lock() {
Ok(db) => db,
Err(_) => return internal_error("db lock poisoned"),
};
let key = format!("_session:{sid}");
if let Err(error) = db.delete(key.as_bytes()) {
return internal_error(&format!("session delete: {error}"));
}
if let Err(error) = db.commit() {
return internal_error(&format!("session commit: {error}"));
}
}
let cookie = "crm_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0".to_string();
let mut resp = Response::ok();
resp.status = 302;
resp.reason = "Found".to_string();
resp = resp.with_header("Location", "/");
resp = resp.with_header("Set-Cookie", &cookie);
resp
})
}
fn auth_redirect_error() -> Response {
let mut resp = Response::ok();
resp.status = 302;
resp.reason = "Found".to_string();
resp = resp.with_header("Location", "/?auth_error=1");
resp
}