botserver/src/web/mod.rs

726 lines
20 KiB
Rust
Raw Normal View History

2025-11-29 16:29:28 -03:00
//! Web module with Askama templates for HTMX and authentication
use askama::Template;
use askama_axum::IntoResponse;
use axum::{
extract::{Path, Query, State, WebSocketUpgrade},
http::StatusCode,
middleware,
response::{Html, Response},
routing::{get, post},
Json, Router,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use tower_cookies::CookieManagerLayer;
use uuid::Uuid;
use crate::shared::state::AppState;
// Authentication modules
pub mod auth;
pub mod auth_handlers;
pub mod chat_handlers;
2025-11-30 22:33:54 -03:00
pub mod compliance_handlers;
pub mod filters;
pub mod stream_handlers;
2025-11-29 16:29:28 -03:00
// Module stubs - to be implemented with full HTMX
pub mod drive {
use super::*;
use crate::web::auth::AuthenticatedUser;
pub fn routes() -> Router<AppState> {
Router::new()
.route("/api/files/list", get(list_files))
.route("/api/files/read", post(read_file))
.route("/api/files/write", post(write_file))
.route("/api/files/delete", post(delete_file))
.route("/api/files/create-folder", post(create_folder))
.route("/api/files/download", get(download_file))
.route("/api/files/share", get(share_file))
}
pub async fn drive_page(AuthenticatedUser { claims }: AuthenticatedUser) -> impl IntoResponse {
DriveTemplate {
user_name: claims.name,
user_email: claims.email,
}
}
#[derive(Template)]
#[template(path = "suite/drive.html")]
2025-11-29 16:29:28 -03:00
struct DriveTemplate {
user_name: String,
user_email: String,
}
async fn list_files(
Query(params): Query<HashMap<String, String>>,
AuthenticatedUser { .. }: AuthenticatedUser,
) -> impl IntoResponse {
// Implementation will connect to actual S3/MinIO backend
Json(serde_json::json!([]))
}
async fn read_file(
Json(payload): Json<FileRequest>,
AuthenticatedUser { .. }: AuthenticatedUser,
) -> impl IntoResponse {
Json(serde_json::json!({
"content": ""
}))
}
async fn write_file(
Json(payload): Json<WriteFileRequest>,
AuthenticatedUser { .. }: AuthenticatedUser,
) -> impl IntoResponse {
Json(serde_json::json!({
"success": true
}))
}
async fn delete_file(
Json(payload): Json<FileRequest>,
AuthenticatedUser { .. }: AuthenticatedUser,
) -> impl IntoResponse {
Json(serde_json::json!({
"success": true
}))
}
async fn create_folder(
Json(payload): Json<CreateFolderRequest>,
AuthenticatedUser { .. }: AuthenticatedUser,
) -> impl IntoResponse {
Json(serde_json::json!({
"success": true
}))
}
async fn download_file(
Query(params): Query<HashMap<String, String>>,
AuthenticatedUser { .. }: AuthenticatedUser,
) -> impl IntoResponse {
StatusCode::NOT_IMPLEMENTED
}
async fn share_file(
Query(params): Query<HashMap<String, String>>,
AuthenticatedUser { .. }: AuthenticatedUser,
) -> impl IntoResponse {
Json(serde_json::json!({
"share_url": ""
}))
}
#[derive(Deserialize)]
struct FileRequest {
bucket: Option<String>,
path: String,
}
#[derive(Deserialize)]
struct WriteFileRequest {
bucket: Option<String>,
path: String,
content: String,
}
#[derive(Deserialize)]
struct CreateFolderRequest {
bucket: Option<String>,
path: String,
name: String,
}
}
pub mod mail {
use super::*;
use crate::web::auth::AuthenticatedUser;
pub fn routes() -> Router<AppState> {
Router::new()
.route("/api/email/accounts", get(get_accounts))
.route("/api/email/list", post(list_emails))
.route("/api/email/send", post(send_email))
.route("/api/email/delete", post(delete_email))
.route("/api/email/mark", post(mark_email))
.route("/api/email/draft", post(save_draft))
}
pub async fn mail_page(AuthenticatedUser { claims }: AuthenticatedUser) -> impl IntoResponse {
MailTemplate {
user_name: claims.name,
user_email: claims.email,
}
}
#[derive(Template)]
#[template(path = "suite/mail.html")]
2025-11-29 16:29:28 -03:00
struct MailTemplate {
user_name: String,
user_email: String,
}
async fn get_accounts(AuthenticatedUser { claims }: AuthenticatedUser) -> impl IntoResponse {
// Will integrate with actual email service
Json(serde_json::json!({
"success": true,
"data": [{
"id": "1",
"email": claims.email,
"display_name": claims.name,
"is_primary": true
}]
}))
}
async fn list_emails(
Json(payload): Json<ListEmailsRequest>,
AuthenticatedUser { .. }: AuthenticatedUser,
) -> impl IntoResponse {
Json(serde_json::json!({
"success": true,
"data": []
}))
}
async fn send_email(
Json(payload): Json<SendEmailRequest>,
AuthenticatedUser { .. }: AuthenticatedUser,
) -> impl IntoResponse {
Json(serde_json::json!({
"success": true,
"message_id": Uuid::new_v4().to_string()
}))
}
async fn delete_email(
Json(payload): Json<EmailActionRequest>,
AuthenticatedUser { .. }: AuthenticatedUser,
) -> impl IntoResponse {
Json(serde_json::json!({
"success": true
}))
}
async fn mark_email(
Json(payload): Json<MarkEmailRequest>,
AuthenticatedUser { .. }: AuthenticatedUser,
) -> impl IntoResponse {
Json(serde_json::json!({
"success": true
}))
}
async fn save_draft(
Json(payload): Json<SendEmailRequest>,
AuthenticatedUser { .. }: AuthenticatedUser,
) -> impl IntoResponse {
Json(serde_json::json!({
"success": true,
"draft_id": Uuid::new_v4().to_string()
}))
}
#[derive(Deserialize)]
struct ListEmailsRequest {
account_id: String,
folder: String,
limit: usize,
offset: usize,
}
#[derive(Deserialize)]
struct SendEmailRequest {
account_id: String,
to: String,
cc: Option<String>,
bcc: Option<String>,
subject: String,
body: String,
is_html: bool,
}
#[derive(Deserialize)]
struct EmailActionRequest {
account_id: String,
email_id: String,
}
#[derive(Deserialize)]
struct MarkEmailRequest {
account_id: String,
email_id: String,
read: bool,
}
}
pub mod meet {
use super::*;
use crate::web::auth::AuthenticatedUser;
pub fn routes() -> Router<AppState> {
Router::new()
.route("/api/meet/create", post(create_meeting))
.route("/api/meet/token", post(get_meeting_token))
.route("/api/meet/invite", post(send_invites))
}
pub async fn meet_page(AuthenticatedUser { claims }: AuthenticatedUser) -> impl IntoResponse {
MeetTemplate {
user_name: claims.name,
user_email: claims.email,
}
}
#[derive(Template)]
#[template(path = "suite/meet.html")]
2025-11-29 16:29:28 -03:00
struct MeetTemplate {
user_name: String,
user_email: String,
}
pub async fn websocket_handler(
ws: WebSocketUpgrade,
State(state): State<AppState>,
AuthenticatedUser { .. }: AuthenticatedUser,
) -> impl IntoResponse {
ws.on_upgrade(move |socket| handle_meet_socket(socket, state))
}
async fn handle_meet_socket(socket: axum::extract::ws::WebSocket, _state: AppState) {
// WebRTC signaling implementation
}
async fn create_meeting(
Json(payload): Json<CreateMeetingRequest>,
AuthenticatedUser { claims }: AuthenticatedUser,
) -> impl IntoResponse {
Json(serde_json::json!({
"id": Uuid::new_v4().to_string(),
"name": payload.name,
"host": claims.email
}))
}
async fn get_meeting_token(
Json(payload): Json<TokenRequest>,
AuthenticatedUser { claims }: AuthenticatedUser,
) -> impl IntoResponse {
// Will integrate with LiveKit for actual tokens
Json(serde_json::json!({
"token": base64::encode(format!("{}:{}", payload.room_id, claims.sub))
}))
}
async fn send_invites(
Json(payload): Json<InviteRequest>,
AuthenticatedUser { .. }: AuthenticatedUser,
) -> impl IntoResponse {
Json(serde_json::json!({
"success": true,
"sent": payload.emails.len()
}))
}
#[derive(Deserialize)]
struct CreateMeetingRequest {
name: String,
description: Option<String>,
settings: Option<MeetingSettings>,
}
#[derive(Deserialize)]
struct MeetingSettings {
enable_transcription: bool,
enable_recording: bool,
enable_bot: bool,
waiting_room: bool,
}
#[derive(Deserialize)]
struct TokenRequest {
room_id: String,
user_name: String,
}
#[derive(Deserialize)]
struct InviteRequest {
meeting_id: String,
emails: Vec<String>,
}
}
pub mod tasks {
use super::*;
use crate::web::auth::AuthenticatedUser;
pub fn routes() -> Router<AppState> {
Router::new()
}
pub async fn tasks_page(AuthenticatedUser { claims }: AuthenticatedUser) -> impl IntoResponse {
TasksTemplate {
user_name: claims.name,
user_email: claims.email,
}
}
#[derive(Template)]
#[template(path = "suite/tasks.html")]
2025-11-29 16:29:28 -03:00
struct TasksTemplate {
user_name: String,
user_email: String,
}
}
/// Base template data
#[derive(Default)]
pub struct BaseContext {
pub user_name: String,
pub user_email: String,
pub user_initial: String,
}
/// Home page template
#[derive(Template)]
#[template(path = "suite/home.html")]
2025-11-29 16:29:28 -03:00
struct HomeTemplate {
base: BaseContext,
apps: Vec<AppCard>,
}
/// App card for home page
#[derive(Serialize)]
struct AppCard {
name: String,
icon: String,
description: String,
url: String,
}
/// Apps menu template
#[derive(Template)]
#[template(path = "suite/partials/apps_menu.html")]
2025-11-29 16:29:28 -03:00
struct AppsMenuTemplate {
apps: Vec<AppMenuItem>,
}
/// App menu item
#[derive(Serialize)]
struct AppMenuItem {
name: String,
icon: String,
url: String,
active: bool,
}
/// User menu template
#[derive(Template)]
#[template(path = "suite/partials/user_menu.html")]
2025-11-29 16:29:28 -03:00
struct UserMenuTemplate {
user_name: String,
user_email: String,
user_initial: String,
}
/// Create the main web router
pub fn create_router(app_state: AppState) -> Router {
// Initialize authentication
let auth_config = auth::AuthConfig::from_env();
// Create session storage
let sessions: Arc<RwLock<HashMap<String, auth::UserSession>>> =
Arc::new(RwLock::new(HashMap::new()));
// Add to app state extensions
let mut app_state = app_state;
app_state.extensions.insert(auth_config.clone());
app_state.extensions.insert(sessions);
// Public routes (no auth required)
let public_routes = Router::new()
.route("/login", get(auth_handlers::login_page))
.route("/auth/login", post(auth_handlers::login_submit))
.route("/auth/callback", get(auth_handlers::oauth_callback))
.route("/api/auth/mode", get(get_auth_mode))
.route("/health", get(health_check));
// Protected routes (auth required)
let protected_routes = Router::new()
// Pages
.route("/", get(home_handler))
.route("/chat", get(chat_handlers::chat_page))
.route("/drive", get(drive::drive_page))
.route("/mail", get(mail::mail_page))
.route("/meet", get(meet::meet_page))
.route("/tasks", get(tasks::tasks_page))
// Auth endpoints
.route("/logout", post(auth_handlers::logout))
.route("/api/auth/user", get(auth_handlers::get_user_info))
.route("/api/auth/refresh", post(auth_handlers::refresh_token))
.route("/api/auth/check", get(auth_handlers::check_session))
// API endpoints
.merge(chat_handlers::routes())
.merge(stream_handlers::routes())
2025-11-29 16:29:28 -03:00
.merge(drive::routes())
.merge(mail::routes())
.merge(meet::routes())
.merge(tasks::routes())
2025-11-30 22:33:54 -03:00
.merge(compliance_handlers::routes())
2025-11-29 16:29:28 -03:00
// Partials
.route("/api/apps/menu", get(apps_menu_handler))
.route("/api/user/menu", get(user_menu_handler))
.route("/api/theme/toggle", post(toggle_theme_handler))
// WebSocket endpoints
.route("/ws", get(websocket_handler))
.route("/ws/chat", get(chat_handlers::websocket_handler))
.route("/ws/meet", get(meet::websocket_handler))
.layer(middleware::from_fn_with_state(
app_state.clone(),
auth::auth_middleware,
));
Router::new()
.merge(public_routes)
.merge(protected_routes)
.layer(CookieManagerLayer::new())
.with_state(app_state)
}
/// Home page handler
async fn home_handler(
State(_state): State<AppState>,
auth::AuthenticatedUser { claims }: auth::AuthenticatedUser,
) -> impl IntoResponse {
let template = HomeTemplate {
base: BaseContext {
user_name: claims.name.clone(),
user_email: claims.email.clone(),
user_initial: claims
.name
.chars()
.next()
.unwrap_or('U')
.to_uppercase()
.to_string(),
},
apps: vec![
AppCard {
name: "Chat".to_string(),
icon: "💬".to_string(),
description: "AI-powered conversations".to_string(),
url: "/chat".to_string(),
},
AppCard {
name: "Drive".to_string(),
icon: "📁".to_string(),
description: "Secure file storage".to_string(),
url: "/drive".to_string(),
},
AppCard {
name: "Mail".to_string(),
icon: "✉️".to_string(),
description: "Email management".to_string(),
url: "/mail".to_string(),
},
AppCard {
name: "Meet".to_string(),
icon: "🎥".to_string(),
description: "Video conferencing".to_string(),
url: "/meet".to_string(),
},
AppCard {
name: "Tasks".to_string(),
icon: "".to_string(),
description: "Task management".to_string(),
url: "/tasks".to_string(),
},
],
};
template
}
/// Apps menu handler
async fn apps_menu_handler(
State(_state): State<AppState>,
auth::AuthenticatedUser { .. }: auth::AuthenticatedUser,
) -> impl IntoResponse {
let template = AppsMenuTemplate {
apps: vec![
AppMenuItem {
name: "Chat".to_string(),
icon: "💬".to_string(),
url: "/chat".to_string(),
active: false,
},
AppMenuItem {
name: "Drive".to_string(),
icon: "📁".to_string(),
url: "/drive".to_string(),
active: false,
},
AppMenuItem {
name: "Mail".to_string(),
icon: "✉️".to_string(),
url: "/mail".to_string(),
active: false,
},
AppMenuItem {
name: "Meet".to_string(),
icon: "🎥".to_string(),
url: "/meet".to_string(),
active: false,
},
AppMenuItem {
name: "Tasks".to_string(),
icon: "".to_string(),
url: "/tasks".to_string(),
active: false,
},
],
};
template
}
/// User menu handler
async fn user_menu_handler(
State(_state): State<AppState>,
auth::AuthenticatedUser { claims }: auth::AuthenticatedUser,
) -> impl IntoResponse {
let template = UserMenuTemplate {
user_name: claims.name.clone(),
user_email: claims.email.clone(),
user_initial: claims
.name
.chars()
.next()
.unwrap_or('U')
.to_uppercase()
.to_string(),
};
template
}
/// Theme toggle handler
async fn toggle_theme_handler(
State(_state): State<AppState>,
auth::AuthenticatedUser { .. }: auth::AuthenticatedUser,
) -> impl IntoResponse {
Response::builder()
.header("HX-Trigger", "theme-changed")
.body("".to_string())
.unwrap()
}
/// Main WebSocket handler
async fn websocket_handler(
ws: WebSocketUpgrade,
State(state): State<AppState>,
auth::AuthenticatedUser { claims }: auth::AuthenticatedUser,
) -> impl IntoResponse {
ws.on_upgrade(move |socket| handle_socket(socket, state, claims))
}
async fn handle_socket(
socket: axum::extract::ws::WebSocket,
_state: AppState,
claims: auth::Claims,
) {
use futures_util::{SinkExt, StreamExt};
let (mut sender, mut receiver) = socket.split();
// Send welcome message
let welcome = serde_json::json!({
"type": "connected",
"user": claims.name,
"session": claims.session_id
});
let _ = sender
.send(axum::extract::ws::Message::Text(welcome.to_string()))
.await;
// Handle incoming messages
while let Some(msg) = receiver.next().await {
if let Ok(msg) = msg {
match msg {
axum::extract::ws::Message::Text(text) => {
// Echo back for now with user info
let response = serde_json::json!({
"type": "message",
"from": claims.name,
"content": text,
"timestamp": chrono::Utc::now().to_rfc3339()
});
let _ = sender
.send(axum::extract::ws::Message::Text(response.to_string()))
.await;
}
axum::extract::ws::Message::Close(_) => break,
_ => {}
}
}
}
}
/// Health check endpoint
async fn health_check() -> impl IntoResponse {
Json(serde_json::json!({
"status": "healthy",
"timestamp": chrono::Utc::now().to_rfc3339()
}))
}
/// Get authentication mode (for login page)
async fn get_auth_mode(State(state): State<AppState>) -> impl IntoResponse {
let auth_config = state.extensions.get::<auth::AuthConfig>();
let mode = if auth_config.is_some() && !auth_config.unwrap().zitadel_client_secret.is_empty() {
"production"
} else {
"development"
};
Json(serde_json::json!({
"mode": mode
}))
}
/// Common types for HTMX responses
#[derive(Serialize)]
pub struct HtmxResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub swap: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trigger: Option<String>,
}
/// Notification for HTMX
#[derive(Serialize, Template)]
#[template(path = "suite/partials/notification.html")]
2025-11-29 16:29:28 -03:00
pub struct NotificationTemplate {
pub message: String,
pub severity: String, // info, success, warning, error
}
/// Message template for chat/notifications
#[derive(Serialize, Template)]
#[template(path = "suite/partials/message.html")]
2025-11-29 16:29:28 -03:00
pub struct MessageTemplate {
pub id: String,
pub sender: String,
pub content: String,
pub timestamp: String,
pub is_user: bool,
}