feat: Implement full auth pipeline — login, register, JWT, bootstrap

Backend auth flow is now functional:
- services/auth.rs: Argon2id password hashing, JWT access/refresh tokens
- services/encryption.rs: AES-256-GCM encrypt/decrypt, hex token generation
- api/auth.rs: Login, register, refresh, logout, /me endpoints
- middleware/auth.rs: JWT Bearer token extractor (FromRequestParts)
- db/users.rs + licenses.rs: Full CRUD with runtime queries (no compile-time DB)
- main.rs: Bootstrap admin user on first run via ADMIN_EMAIL/ADMIN_PASSWORD env vars
- NATS connection now optional for dev (graceful fallback)
- Added hex and http crates to Cargo.toml

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell
2026-02-14 21:49:37 -05:00
parent 9217f77998
commit 5668675b6a
10 changed files with 671 additions and 101 deletions

View File

@@ -1,7 +1,12 @@
use std::sync::Arc;
use axum::extract::FromRequestParts;
use http::request::Parts;
use uuid::Uuid;
use crate::services::auth as auth_service;
use crate::AppState;
/// Extractor that validates the JWT from the Authorization header
/// and provides the authenticated user's identity to handlers.
#[derive(Debug, Clone)]
@@ -12,21 +17,39 @@ pub struct AuthUser {
pub role: Option<String>,
}
// TODO: Implement JWT validation logic
// - Extract Bearer token from Authorization header
// - Decode and verify JWT signature using the app's secret
// - Check token expiration
// - Build AuthUser from claims
// - Return 401 Unauthorized on any failure
#[axum::async_trait]
impl<S> FromRequestParts<S> for AuthUser
where
S: Send + Sync,
{
impl FromRequestParts<Arc<AppState>> for AuthUser {
type Rejection = http::StatusCode;
async fn from_request_parts(_parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
todo!()
async fn from_request_parts(
parts: &mut Parts,
state: &Arc<AppState>,
) -> Result<Self, Self::Rejection> {
// Extract Bearer token from Authorization header
let auth_header = parts
.headers
.get(http::header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.ok_or(http::StatusCode::UNAUTHORIZED)?;
let token = auth_header
.strip_prefix("Bearer ")
.ok_or(http::StatusCode::UNAUTHORIZED)?;
// Validate JWT
let claims = auth_service::validate_token(&state.config, token)
.map_err(|_| http::StatusCode::UNAUTHORIZED)?;
// Ensure it's an access token, not a refresh token
if claims.token_type != "access" {
return Err(http::StatusCode::UNAUTHORIZED);
}
Ok(AuthUser {
user_id: claims.sub,
email: claims.email,
license_id: claims.license_id,
role: claims.role,
})
}
}