use actix_session::Session;
use actix_web::{web, HttpResponse, Responder};
use tera::Context;
use tracing::{error, info, warn};
use crate::helpers::template_helper::render_template;
use crate::models::adminx_model::get_admin_by_email;
use crate::registry::get_registered_menus;
use crate::utils::jwt::create_jwt_token;
use crate::utils::structs::LoginForm;
use crate::configs::initializer::AdminxConfig;
use crate::utils::auth::{is_rate_limited, reset_rate_limit, extract_claims_from_session};
use std::time::Duration;
use crate::helpers::auth_helper::{
create_base_template_context_with_auth,
};
pub async fn login_form(
session: Session,
config: web::Data<AdminxConfig>,
) -> impl Responder {
if let Ok(_claims) = extract_claims_from_session(&session, &config).await {
return HttpResponse::Found()
.append_header(("Location", "/adminx"))
.finish();
}
let mut ctx = Context::new();
ctx.insert("is_authenticated", &false);
ctx.insert("page_title", "Login");
render_template("login.html.tera", ctx).await
}
pub async fn login_action(
form: web::Form<LoginForm>,
session: Session,
config: web::Data<AdminxConfig>,
) -> impl Responder {
let email = form.email.trim();
let password = form.password.trim();
info!("Attempting login for: {}", email);
if email.is_empty() || password.is_empty() {
warn!("Empty email or password for login attempt");
let mut ctx = Context::new();
ctx.insert("is_authenticated", &false);
ctx.insert("error", "Email and password are required");
return render_template("login.html.tera", ctx).await;
}
if !email.contains('@') {
warn!("Invalid email format: {}", email);
let mut ctx = Context::new();
ctx.insert("is_authenticated", &false);
ctx.insert("error", "Invalid email format");
return render_template("login.html.tera", ctx).await;
}
if is_rate_limited(email, 5, Duration::from_secs(900)) {
warn!("Rate limit exceeded for: {}", email);
let mut ctx = Context::new();
ctx.insert("is_authenticated", &false);
ctx.insert("error", "Too many login attempts. Please try again later.");
return render_template("login.html.tera", ctx).await;
}
let dummy_hash = "$2b$12$dummy.hash.to.prevent.timing.attacks.abcdefghijklmnopqrstuvwxy";
match get_admin_by_email(email).await {
Some(admin) => {
if admin.verify_password(password) {
let admin_id = match &admin.id {
Some(id) => id.to_string(),
None => {
error!("Admin has no ID: {}", email);
let mut ctx = Context::new();
ctx.insert("is_authenticated", &false);
ctx.insert("error", "Authentication failed - missing admin ID");
return render_template("login.html.tera", ctx).await;
}
};
match create_jwt_token(&admin_id, email, "admin", &config) {
Ok(token) => {
info!("Login successful for: {}", email);
reset_rate_limit(email);
if let Err(err) = session.insert("admintoken", &token) {
error!("Session insertion failed: {}", err);
let mut ctx = Context::new();
ctx.insert("is_authenticated", &false);
ctx.insert("error", "Session creation failed");
return render_template("login.html.tera", ctx).await;
}
HttpResponse::Found()
.append_header(("Location", "/adminx"))
.finish()
}
Err(err) => {
error!("JWT generation failed for {}: {}", email, err);
let mut ctx = Context::new();
ctx.insert("is_authenticated", &false);
ctx.insert("error", "Authentication failed - token generation error");
render_template("login.html.tera", ctx).await
}
}
} else {
bcrypt::verify(password, dummy_hash).ok();
warn!("Invalid password for: {}", email);
let mut ctx = Context::new();
ctx.insert("is_authenticated", &false);
ctx.insert("error", "Invalid email or password");
render_template("login.html.tera", ctx).await
}
}
None => {
bcrypt::verify(password, dummy_hash).ok();
warn!("Admin not found: {}", email);
let mut ctx = Context::new();
ctx.insert("is_authenticated", &false);
ctx.insert("error", "Invalid email or password");
render_template("login.html.tera", ctx).await
}
}
}
pub async fn logout_action(session: Session) -> impl Responder {
let user_info = session.get::<String>("admintoken")
.unwrap_or_default()
.unwrap_or_else(|| "unknown".to_string());
session.clear();
info!("User logged out successfully: {}", if user_info == "unknown" { "session_token_unavailable" } else { "user_had_valid_session" });
HttpResponse::Found()
.append_header(("Location", "/adminx/login"))
.finish()
}
pub async fn dashboard_view(
session: Session,
config: web::Data<AdminxConfig>,
) -> impl Responder {
match create_base_template_context_with_auth("Dashboard", "", &session, &config).await {
Ok(mut ctx) => {
ctx.insert("page_title", "Dashboard");
render_template("stats.html.tera", ctx).await
}
Err(redirect_response) => redirect_response,
}
}
pub async fn profile_view(
session: Session,
config: web::Data<AdminxConfig>,
) -> impl Responder {
match extract_claims_from_session(&session, &config).await {
Ok(claims) => {
let mut ctx = Context::new();
ctx.insert("is_authenticated", &true);
ctx.insert("user_email", &claims.email);
ctx.insert("user_role", &claims.role);
ctx.insert("user_roles", &claims.roles);
ctx.insert("current_user", &claims);
ctx.insert("menus", &get_registered_menus());
ctx.insert("page_title", "Profile");
render_template("profile.html.tera", ctx).await
}
Err(_) => {
HttpResponse::Found()
.append_header(("Location", "/adminx/login"))
.finish()
}
}
}
fn auth_error_response(message: &str, status: actix_web::http::StatusCode) -> HttpResponse {
HttpResponse::build(status)
.content_type("application/json")
.json(serde_json::json!({
"error": message,
"timestamp": chrono::Utc::now().to_rfc3339()
}))
}
pub async fn api_login_action(
form: web::Json<LoginForm>,
session: Session,
config: web::Data<AdminxConfig>,
req: actix_web::HttpRequest,
) -> impl Responder {
let email = form.email.trim();
let password = form.password.trim();
let connection_info = req.connection_info();
let ip = connection_info.peer_addr().unwrap_or("unknown");
let user_agent = req.headers().get("user-agent")
.and_then(|h| h.to_str().ok())
.unwrap_or("unknown");
info!(
email = %email,
ip = %ip,
user_agent = %user_agent,
"API login attempt"
);
if email.is_empty() || password.is_empty() {
return auth_error_response("Email and password are required",
actix_web::http::StatusCode::BAD_REQUEST);
}
if !email.contains('@') {
return auth_error_response("Invalid email format",
actix_web::http::StatusCode::BAD_REQUEST);
}
if is_rate_limited(email, 5, Duration::from_secs(900)) {
warn!(
email = %email,
ip = %ip,
"Rate limit exceeded"
);
return auth_error_response("Too many login attempts",
actix_web::http::StatusCode::TOO_MANY_REQUESTS);
}
let dummy_hash = "$2b$12$dummy.hash.to.prevent.timing.attacks.abcdefghijklmnopqrstuvwxy";
match get_admin_by_email(email).await {
Some(admin) => {
if admin.verify_password(password) {
let admin_id = match &admin.id {
Some(id) => id.to_string(),
None => {
error!("Admin has no ID: {}", email);
return auth_error_response("Authentication failed",
actix_web::http::StatusCode::INTERNAL_SERVER_ERROR);
}
};
match create_jwt_token(&admin_id, email, "admin", &config) {
Ok(token) => {
info!(
email = %email,
ip = %ip,
"API login successful"
);
reset_rate_limit(email);
if let Err(err) = session.insert("admintoken", &token) {
error!("Session insertion failed: {}", err);
return auth_error_response("Session creation failed",
actix_web::http::StatusCode::INTERNAL_SERVER_ERROR);
}
HttpResponse::Ok().json(serde_json::json!({
"success": true,
"redirect": "/adminx",
"message": "Login successful",
"user": {
"email": email,
"role": "admin"
}
}))
}
Err(err) => {
error!("JWT generation failed for {}: {}", email, err);
auth_error_response("Authentication failed",
actix_web::http::StatusCode::INTERNAL_SERVER_ERROR)
}
}
} else {
bcrypt::verify(password, dummy_hash).ok();
warn!(
email = %email,
ip = %ip,
"Invalid password"
);
auth_error_response("Invalid credentials",
actix_web::http::StatusCode::UNAUTHORIZED)
}
}
None => {
bcrypt::verify(password, dummy_hash).ok();
warn!(
email = %email,
ip = %ip,
"Admin not found"
);
auth_error_response("Invalid credentials",
actix_web::http::StatusCode::UNAUTHORIZED)
}
}
}
pub async fn check_auth_status(
session: Session,
config: web::Data<AdminxConfig>,
) -> impl Responder {
match extract_claims_from_session(&session, &config).await {
Ok(claims) => {
HttpResponse::Ok().json(serde_json::json!({
"authenticated": true,
"user": {
"email": claims.email,
"role": claims.role,
"roles": claims.roles
}
}))
}
Err(_) => {
HttpResponse::Ok().json(serde_json::json!({
"authenticated": false
}))
}
}
}