- Remove duplicate departments/designations/employees handlers from users service (already in employees service) - Fix all 9 profession admin handlers to use correct DB schema (display_name, bio, location, custom_data) - Fix companies admin handler to match CompanyProfile DB model with all fields - Fix customers admin handler to match Requirement model with preferred_date - Fix missing serde_json imports and type annotations in admin handlers - Add #[allow(dead_code)] for intentionally unused structs/fields - Add test infrastructure: auth crypto tests (2 passing), test directory structure - Zero compilation warnings across all services
128 lines
4.1 KiB
Rust
128 lines
4.1 KiB
Rust
use auth::{
|
|
crypto::{verify_password},
|
|
jwt::generate_tokens,
|
|
};
|
|
use axum::{
|
|
extract::State,
|
|
http::{header::SET_COOKIE, StatusCode},
|
|
response::IntoResponse,
|
|
routing::{get, post},
|
|
Json, Router,
|
|
};
|
|
use db::models::employee::EmployeeRepository;
|
|
use serde::{Deserialize, Serialize};
|
|
use contracts::auth_middleware::AuthUser;
|
|
use crate::AppState;
|
|
|
|
pub fn router() -> Router<AppState> {
|
|
Router::new()
|
|
.route("/login", post(login))
|
|
.route("/logout", post(logout))
|
|
.route("/session", get(session))
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct LoginPayload {
|
|
pub email: String,
|
|
pub password: String,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
#[allow(dead_code)]
|
|
pub struct SessionEmployee {
|
|
pub id: String,
|
|
pub email: String,
|
|
pub full_name: String,
|
|
pub role_code: String,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub struct ErrorResponse {
|
|
pub error: String,
|
|
pub code: String,
|
|
}
|
|
|
|
fn err(status: StatusCode, msg: &str, code: &str) -> (StatusCode, Json<ErrorResponse>) {
|
|
(status, Json(ErrorResponse {
|
|
error: msg.to_string(),
|
|
code: code.to_string(),
|
|
}))
|
|
}
|
|
|
|
async fn login(
|
|
State(state): State<AppState>,
|
|
Json(payload): Json<LoginPayload>,
|
|
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
|
|
let email = payload.email.to_lowercase();
|
|
|
|
let employee = EmployeeRepository::get_by_email(&state.pool, &email)
|
|
.await
|
|
.map_err(|_| err(StatusCode::INTERNAL_SERVER_ERROR, "DB error", "DB_ERROR"))?
|
|
.ok_or_else(|| err(StatusCode::UNAUTHORIZED, "Invalid credentials", "INVALID_CREDENTIALS"))?;
|
|
|
|
if employee.status != "ACTIVE" {
|
|
return Err(err(StatusCode::FORBIDDEN, "Account not active", "ACCOUNT_INACTIVE"));
|
|
}
|
|
|
|
let is_valid = verify_password(&payload.password, &employee.password_hash)
|
|
.map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string(), "INTERNAL_ERROR"))?;
|
|
|
|
if !is_valid {
|
|
return Err(err(StatusCode::UNAUTHORIZED, "Invalid credentials", "INVALID_CREDENTIALS"));
|
|
}
|
|
|
|
let jwt_secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set");
|
|
|
|
// Internal Staff Roles
|
|
let roles = vec![employee.role_code.clone()];
|
|
|
|
let tokens = generate_tokens(
|
|
employee.id.to_string(),
|
|
employee.email.clone(),
|
|
roles.clone(),
|
|
roles.first().cloned(),
|
|
&jwt_secret,
|
|
)
|
|
.map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string(), "TOKEN_ERROR"))?;
|
|
|
|
// Store session
|
|
EmployeeRepository::store_session(&state.pool, employee.id, &tokens.refresh_token, chrono::Utc::now() + chrono::Duration::days(30))
|
|
.await
|
|
.map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string(), "DB_ERROR"))?;
|
|
|
|
let cookie = format!(
|
|
"nxtgauge_admin_token={}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=2592000",
|
|
tokens.refresh_token
|
|
);
|
|
|
|
Ok((StatusCode::OK, [(SET_COOKIE, cookie)], Json(serde_json::json!({
|
|
"access_token": tokens.access_token,
|
|
"token_type": "Bearer",
|
|
"user": {
|
|
"id": employee.id.to_string(),
|
|
"email": employee.email,
|
|
"full_name": format!("{} {}", employee.first_name, employee.last_name),
|
|
"role_code": employee.role_code,
|
|
}
|
|
}))))
|
|
}
|
|
|
|
async fn logout(
|
|
_req: axum::http::Request<axum::body::Body>,
|
|
) -> impl IntoResponse {
|
|
let clear = "nxtgauge_admin_token=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0";
|
|
(StatusCode::OK, [(SET_COOKIE, clear)], Json(serde_json::json!({ "message": "Logged out" })))
|
|
}
|
|
|
|
async fn session(
|
|
auth: AuthUser,
|
|
_state: State<AppState>,
|
|
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
|
|
// For now, allow simple return from AuthUser if it matches internal structure
|
|
// In future, fetch from employee repository for fresh data
|
|
Ok(Json(serde_json::json!({
|
|
"id": auth.user_id.to_string(),
|
|
"email": auth.email,
|
|
"role_code": auth.claims.active_role,
|
|
})))
|
|
}
|