botserver/src/auth/mod.rs

296 lines
9.9 KiB
Rust
Raw Normal View History

use actix_web::{HttpRequest, HttpResponse, Result, web};
2025-10-06 10:30:17 -03:00
use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Argon2,
};
2025-10-11 12:29:03 -03:00
use diesel::pg::PgConnection;
2025-10-12 13:27:48 -03:00
use diesel::prelude::*;
use log::{error};
2025-10-06 10:30:17 -03:00
use redis::Client;
2025-10-18 12:01:39 -03:00
use std::collections::HashMap;
2025-10-06 10:30:17 -03:00
use std::sync::Arc;
use uuid::Uuid;
2025-10-12 13:27:48 -03:00
use crate::shared;
2025-10-18 12:01:39 -03:00
use crate::shared::state::AppState;
2025-10-12 13:27:48 -03:00
2025-10-06 10:30:17 -03:00
pub struct AuthService {
2025-10-11 12:29:03 -03:00
pub conn: PgConnection,
2025-10-06 10:30:17 -03:00
pub redis: Option<Arc<Client>>,
}
impl AuthService {
2025-10-11 12:29:03 -03:00
pub fn new(conn: PgConnection, redis: Option<Arc<Client>>) -> Self {
Self { conn, redis }
2025-10-06 10:30:17 -03:00
}
2025-10-11 12:29:03 -03:00
pub fn verify_user(
&mut self,
2025-10-06 10:30:17 -03:00
username: &str,
password: &str,
2025-10-06 19:12:13 -03:00
) -> Result<Option<Uuid>, Box<dyn std::error::Error + Send + Sync>> {
2025-10-11 12:29:03 -03:00
use crate::shared::models::users;
2025-10-12 13:27:48 -03:00
2025-10-11 12:29:03 -03:00
let user = users::table
.filter(users::username.eq(username))
.filter(users::is_active.eq(true))
.select((users::id, users::password_hash))
.first::<(Uuid, String)>(&mut self.conn)
.optional()?;
if let Some((user_id, password_hash)) = user {
2025-10-06 10:30:17 -03:00
if let Ok(parsed_hash) = PasswordHash::new(&password_hash) {
if Argon2::default()
.verify_password(password.as_bytes(), &parsed_hash)
.is_ok()
{
return Ok(Some(user_id));
}
}
}
Ok(None)
}
2025-10-11 12:29:03 -03:00
pub fn create_user(
&mut self,
2025-10-06 10:30:17 -03:00
username: &str,
email: &str,
password: &str,
2025-10-06 19:12:13 -03:00
) -> Result<Uuid, Box<dyn std::error::Error + Send + Sync>> {
2025-10-11 12:29:03 -03:00
use crate::shared::models::users;
use diesel::insert_into;
2025-10-12 13:27:48 -03:00
2025-10-06 10:30:17 -03:00
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
2025-10-12 13:27:48 -03:00
let password_hash = argon2
.hash_password(password.as_bytes(), &salt)
2025-10-11 12:29:03 -03:00
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?
.to_string();
let user_id = Uuid::new_v4();
2025-10-12 13:27:48 -03:00
2025-10-11 12:29:03 -03:00
insert_into(users::table)
.values((
users::id.eq(user_id),
users::username.eq(username),
users::email.eq(email),
users::password_hash.eq(password_hash),
))
.execute(&mut self.conn)?;
Ok(user_id)
2025-10-06 10:30:17 -03:00
}
pub async fn delete_user_cache(
&self,
username: &str,
2025-10-06 19:12:13 -03:00
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
2025-10-06 10:30:17 -03:00
if let Some(redis_client) = &self.redis {
let mut conn = redis_client.get_multiplexed_async_connection().await?;
let cache_key = format!("auth:user:{}", username);
let _: () = redis::Cmd::del(&cache_key).query_async(&mut conn).await?;
}
Ok(())
}
2025-10-11 12:29:03 -03:00
pub fn update_user_password(
&mut self,
2025-10-06 10:30:17 -03:00
user_id: Uuid,
new_password: &str,
2025-10-06 19:12:13 -03:00
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
2025-10-11 12:29:03 -03:00
use crate::shared::models::users;
use diesel::update;
2025-10-12 13:27:48 -03:00
2025-10-06 10:30:17 -03:00
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
2025-10-12 13:27:48 -03:00
let password_hash = argon2
.hash_password(new_password.as_bytes(), &salt)
2025-10-11 12:29:03 -03:00
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?
.to_string();
update(users::table.filter(users::id.eq(user_id)))
.set((
users::password_hash.eq(&password_hash),
users::updated_at.eq(diesel::dsl::now),
))
.execute(&mut self.conn)?;
if let Some(username) = users::table
.filter(users::id.eq(user_id))
.select(users::username)
.first::<String>(&mut self.conn)
.optional()?
2025-10-06 10:30:17 -03:00
{
2025-10-11 12:29:03 -03:00
// Note: This would need to be handled differently in async context
// For now, we'll just log it
log::info!("Would delete cache for user: {}", username);
2025-10-06 10:30:17 -03:00
}
Ok(())
}
2025-10-12 13:27:48 -03:00
pub(crate) fn get_user_by_id(
&mut self,
_uid: Uuid,
2025-10-12 13:27:48 -03:00
) -> Result<Option<shared::models::User>, Box<dyn std::error::Error + Send + Sync>> {
use crate::shared::models::users;
let user = users::table
// TODO: .filter(users::id.eq(uid))
2025-10-12 13:27:48 -03:00
.filter(users::is_active.eq(true))
.first::<shared::models::User>(&mut self.conn)
.optional()?;
Ok(user)
}
pub fn bot_from_name(
&mut self,
bot_name: &str,
) -> Result<Option<Uuid>, Box<dyn std::error::Error + Send + Sync>> {
use crate::shared::models::bots;
let bot = bots::table
.filter(bots::name.eq(bot_name))
.filter(bots::is_active.eq(true))
.select(bots::id)
.first::<Uuid>(&mut self.conn)
.optional()?;
Ok(bot)
}
2025-10-06 10:30:17 -03:00
}
2025-10-18 12:01:39 -03:00
#[actix_web::get("/api/auth")]
async fn auth_handler(
_req: HttpRequest,
2025-10-18 12:01:39 -03:00
data: web::Data<AppState>,
web::Query(params): web::Query<HashMap<String, String>>,
) -> Result<HttpResponse> {
let bot_name = params.get("bot_name").cloned().unwrap_or_default();
2025-10-18 12:01:39 -03:00
let _token = params.get("token").cloned().unwrap_or_default();
// Create or get anonymous user with proper UUID
let user_id = {
let mut sm = data.session_manager.lock().await;
match sm.get_or_create_anonymous_user(None) {
Ok(uid) => uid,
Err(e) => {
error!("Failed to create anonymous user: {}", e);
return Ok(HttpResponse::InternalServerError()
.json(serde_json::json!({"error": "Failed to create user"})));
}
}
};
let mut db_conn = data.conn.lock().unwrap();
// Use bot_name query parameter if provided, otherwise fallback to path-based lookup
let bot_name_param = bot_name.clone();
let (bot_id, bot_name) = {
use crate::shared::models::schema::bots::dsl::*;
use diesel::prelude::*;
use actix_web::error::ErrorInternalServerError;
// Try to find bot by the provided name
match bots
.filter(name.eq(&bot_name_param))
.filter(is_active.eq(true))
.select((id, name))
.first::<(Uuid, String)>(&mut *db_conn)
.optional()
.map_err(|e| ErrorInternalServerError(e))?
{
Some((id_val, name_val)) => (id_val, name_val),
None => {
// Fallback to first active bot if not found
match bots
.filter(is_active.eq(true))
.select((id, name))
.first::<(Uuid, String)>(&mut *db_conn)
.optional()
.map_err(|e| ErrorInternalServerError(e))?
{
Some((id_val, name_val)) => (id_val, name_val),
None => {
error!("No active bots found");
return Ok(HttpResponse::ServiceUnavailable()
.json(serde_json::json!({"error": "No bots available"})));
}
}
}
}
2025-10-18 12:01:39 -03:00
};
let session = {
let mut sm = data.session_manager.lock().await;
match sm.get_or_create_user_session(user_id, bot_id, "Auth Session") {
Ok(Some(s)) => s,
Ok(None) => {
error!("Failed to create session");
return Ok(HttpResponse::InternalServerError()
.json(serde_json::json!({"error": "Failed to create session"})));
}
Err(e) => {
error!("Failed to create session: {}", e);
return Ok(HttpResponse::InternalServerError()
.json(serde_json::json!({"error": e.to_string()})));
}
}
};
let auth_script_path = format!("./work/{}.gbai/{}.gbdialog/auth.ast", bot_name, bot_name);
if std::path::Path::new(&auth_script_path).exists() {
let auth_script = match std::fs::read_to_string(&auth_script_path) {
Ok(content) => content,
Err(e) => {
error!("Failed to read auth script: {}", e);
return Ok(HttpResponse::InternalServerError()
.json(serde_json::json!({"error": "Failed to read auth script"})));
}
};
2025-10-18 12:01:39 -03:00
let script_service = crate::basic::ScriptService::new(Arc::clone(&data), session.clone());
match script_service
.compile(&auth_script)
.and_then(|ast| script_service.run(&ast))
{
Ok(result) => {
if result.to_string() == "false" {
error!("Auth script returned false, authentication failed");
return Ok(HttpResponse::Unauthorized()
.json(serde_json::json!({"error": "Authentication failed"})));
}
}
Err(e) => {
error!("Failed to run auth script: {}", e);
return Ok(HttpResponse::InternalServerError()
.json(serde_json::json!({"error": "Auth failed"})));
2025-10-18 12:01:39 -03:00
}
}
}
let session = {
let mut sm = data.session_manager.lock().await;
match sm.get_session_by_id(session.id) {
2025-10-18 12:01:39 -03:00
Ok(Some(s)) => s,
Ok(None) => {
error!("Failed to retrieve session");
return Ok(HttpResponse::InternalServerError()
.json(serde_json::json!({"error": "Failed to retrieve session"})));
}
Err(e) => {
error!("Failed to retrieve session: {}", e);
return Ok(HttpResponse::InternalServerError()
.json(serde_json::json!({"error": e.to_string()})));
}
}
};
Ok(HttpResponse::Ok().json(serde_json::json!({
"user_id": session.user_id,
"session_id": session.id,
"status": "authenticated"
})))
}