Files
guru-connect/server/src/api/auth.rs
Mike Swanson 1c5c1e78e7
Some checks failed
Build and Test / Build Server (Linux) (push) Failing after 2m59s
Build and Test / Build Agent (Windows) (push) Has started running
Build and Test / Security Audit (push) Has been cancelled
Build and Test / Build Summary (push) Has been cancelled
Run Tests / Test Server (push) Has been cancelled
Run Tests / Test Agent (push) Has been cancelled
Run Tests / Code Coverage (push) Has been cancelled
Run Tests / Lint and Format Check (push) Has been cancelled
style: cargo fmt --all — make codebase rustfmt-clean
First run of the build-and-test CI gate (cargo fmt --all -- --check) surfaced
pre-existing formatting drift across the agent and server crates. Apply rustfmt
across the workspace so the codebase meets its own CI gate. Pure formatting; no
logic changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 15:02:12 +00:00

311 lines
8.3 KiB
Rust

//! Authentication API endpoints
use axum::{
extract::{Request, State},
http::StatusCode,
Json,
};
use serde::{Deserialize, Serialize};
use crate::auth::{verify_password, AuthenticatedUser, JwtConfig};
use crate::db;
use crate::AppState;
/// Login request
#[derive(Debug, Deserialize)]
pub struct LoginRequest {
pub username: String,
pub password: String,
}
/// Login response
#[derive(Debug, Serialize)]
pub struct LoginResponse {
pub token: String,
pub user: UserResponse,
}
/// User info in response
#[derive(Debug, Serialize)]
pub struct UserResponse {
pub id: String,
pub username: String,
pub email: Option<String>,
pub role: String,
pub permissions: Vec<String>,
}
/// Error response
#[derive(Debug, Serialize)]
pub struct ErrorResponse {
pub error: String,
}
/// POST /api/auth/login
pub async fn login(
State(state): State<AppState>,
Json(request): Json<LoginRequest>,
) -> Result<Json<LoginResponse>, (StatusCode, Json<ErrorResponse>)> {
let db = state.db.as_ref().ok_or_else(|| {
(
StatusCode::SERVICE_UNAVAILABLE,
Json(ErrorResponse {
error: "Database not available".to_string(),
}),
)
})?;
// Get user by username
let user = db::get_user_by_username(db.pool(), &request.username)
.await
.map_err(|e| {
tracing::error!("Database error during login: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "Internal server error".to_string(),
}),
)
})?
.ok_or_else(|| {
(
StatusCode::UNAUTHORIZED,
Json(ErrorResponse {
error: "Invalid username or password".to_string(),
}),
)
})?;
// Check if user is enabled
if !user.enabled {
return Err((
StatusCode::UNAUTHORIZED,
Json(ErrorResponse {
error: "Account is disabled".to_string(),
}),
));
}
// Verify password
let password_valid = verify_password(&request.password, &user.password_hash).map_err(|e| {
tracing::error!("Password verification error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "Internal server error".to_string(),
}),
)
})?;
if !password_valid {
return Err((
StatusCode::UNAUTHORIZED,
Json(ErrorResponse {
error: "Invalid username or password".to_string(),
}),
));
}
// Get user permissions
let permissions = db::get_user_permissions(db.pool(), user.id)
.await
.unwrap_or_default();
// Update last login
let _ = db::update_last_login(db.pool(), user.id).await;
// Create JWT token
let token = state
.jwt_config
.create_token(user.id, &user.username, &user.role, permissions.clone())
.map_err(|e| {
tracing::error!("Token creation error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "Failed to create token".to_string(),
}),
)
})?;
tracing::info!("User {} logged in successfully", user.username);
Ok(Json(LoginResponse {
token,
user: UserResponse {
id: user.id.to_string(),
username: user.username,
email: user.email,
role: user.role,
permissions,
},
}))
}
/// GET /api/auth/me - Get current user info
pub async fn get_me(
State(state): State<AppState>,
user: AuthenticatedUser,
) -> Result<Json<UserResponse>, (StatusCode, Json<ErrorResponse>)> {
let db = state.db.as_ref().ok_or_else(|| {
(
StatusCode::SERVICE_UNAVAILABLE,
Json(ErrorResponse {
error: "Database not available".to_string(),
}),
)
})?;
let user_id = uuid::Uuid::parse_str(&user.user_id).map_err(|_| {
(
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: "Invalid user ID".to_string(),
}),
)
})?;
let db_user = db::get_user_by_id(db.pool(), user_id)
.await
.map_err(|e| {
tracing::error!("Database error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "Internal server error".to_string(),
}),
)
})?
.ok_or_else(|| {
(
StatusCode::NOT_FOUND,
Json(ErrorResponse {
error: "User not found".to_string(),
}),
)
})?;
let permissions = db::get_user_permissions(db.pool(), db_user.id)
.await
.unwrap_or_default();
Ok(Json(UserResponse {
id: db_user.id.to_string(),
username: db_user.username,
email: db_user.email,
role: db_user.role,
permissions,
}))
}
/// Change password request
#[derive(Debug, Deserialize)]
pub struct ChangePasswordRequest {
pub current_password: String,
pub new_password: String,
}
/// POST /api/auth/change-password
pub async fn change_password(
State(state): State<AppState>,
user: AuthenticatedUser,
Json(request): Json<ChangePasswordRequest>,
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
let db = state.db.as_ref().ok_or_else(|| {
(
StatusCode::SERVICE_UNAVAILABLE,
Json(ErrorResponse {
error: "Database not available".to_string(),
}),
)
})?;
let user_id = uuid::Uuid::parse_str(&user.user_id).map_err(|_| {
(
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: "Invalid user ID".to_string(),
}),
)
})?;
// Get current user
let db_user = db::get_user_by_id(db.pool(), user_id)
.await
.map_err(|e| {
tracing::error!("Database error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "Internal server error".to_string(),
}),
)
})?
.ok_or_else(|| {
(
StatusCode::NOT_FOUND,
Json(ErrorResponse {
error: "User not found".to_string(),
}),
)
})?;
// Verify current password
let password_valid = verify_password(&request.current_password, &db_user.password_hash)
.map_err(|e| {
tracing::error!("Password verification error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "Internal server error".to_string(),
}),
)
})?;
if !password_valid {
return Err((
StatusCode::UNAUTHORIZED,
Json(ErrorResponse {
error: "Current password is incorrect".to_string(),
}),
));
}
// Validate new password
if request.new_password.len() < 8 {
return Err((
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: "Password must be at least 8 characters".to_string(),
}),
));
}
// Hash new password
let new_hash = crate::auth::hash_password(&request.new_password).map_err(|e| {
tracing::error!("Password hashing error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "Failed to hash password".to_string(),
}),
)
})?;
// Update password
db::update_user_password(db.pool(), user_id, &new_hash)
.await
.map_err(|e| {
tracing::error!("Database error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "Failed to update password".to_string(),
}),
)
})?;
tracing::info!("User {} changed their password", user.username);
Ok(StatusCode::OK)
}