Files
xevion.dev/src/cli/api/auth.rs
Xevion aa56d31067 feat: add comprehensive CLI with API client, session management, and data seeding
- Binary renamed from 'api' to 'xevion' with full CLI command structure
- Authentication: login/logout with session persistence to .xevion-session
- API commands: projects, tags, settings CRUD operations with JSON/table output
- Serve command: run production server with configurable listen addresses
- Seed command: moved from bin/ to CLI subcommand for database initialization
- HTTP client abstraction supporting both TCP and Unix socket connections
2026-01-13 19:55:45 -06:00

129 lines
3.6 KiB
Rust

use serde::{Deserialize, Serialize};
use time::{Duration, OffsetDateTime};
use crate::cli::client::{ApiClient, Session, check_response};
use crate::cli::output;
#[derive(Serialize)]
struct LoginRequest {
username: String,
password: String,
}
#[derive(Deserialize)]
struct LoginResponse {
success: bool,
username: String,
}
#[derive(Deserialize)]
struct SessionResponse {
authenticated: bool,
username: String,
}
/// Login and save session
pub async fn login(
mut client: ApiClient,
username: &str,
password: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let request = LoginRequest {
username: username.to_string(),
password: password.to_string(),
};
let response = client.post("/api/login", &request).await?;
// Extract session cookie from response headers
let session_token = response
.headers()
.get_all("set-cookie")
.iter()
.find_map(|v| {
let s = v.to_str().ok()?;
if s.starts_with("admin_session=") {
// Extract just the token value
let token = s.strip_prefix("admin_session=")?.split(';').next()?;
Some(token.to_string())
} else {
None
}
});
let response = check_response(response).await?;
let login_response: LoginResponse = response.json().await?;
if !login_response.success {
output::error("Login failed");
return Ok(());
}
let session_token = session_token.ok_or("No session cookie received")?;
let session = Session {
api_url: client.api_url.clone(),
session_token,
username: login_response.username.clone(),
created_at: OffsetDateTime::now_utc(),
expires_at: OffsetDateTime::now_utc() + Duration::days(7),
};
client.save_session(session)?;
output::success(&format!("Logged in as {}", login_response.username));
Ok(())
}
/// Clear saved session
pub async fn logout(mut client: ApiClient) -> Result<(), Box<dyn std::error::Error>> {
// Try to call logout endpoint if we have a session
if client.is_authenticated() {
let _ = client.post("/api/logout", &()).await;
}
client.clear_session()?;
output::success("Logged out");
Ok(())
}
/// Check current session status
pub async fn session(client: ApiClient, json: bool) -> Result<(), Box<dyn std::error::Error>> {
// First check local session
if let Some(session) = client.session() {
// Verify with server
let response = client.get("/api/session").await?;
let response = check_response(response).await?;
let session_response: SessionResponse = response.json().await?;
if json {
println!(
"{}",
serde_json::json!({
"authenticated": session_response.authenticated,
"username": session_response.username,
"api_url": session.api_url,
"expires_at": session.expires_at.format(&time::format_description::well_known::Rfc3339).unwrap(),
})
);
} else if session_response.authenticated {
output::print_session(&session_response.username, &session.api_url);
} else {
output::error("Session expired or invalid");
}
} else {
if json {
println!(
"{}",
serde_json::json!({
"authenticated": false,
})
);
} else {
output::info("Not logged in");
}
}
Ok(())
}