fix: Multiple compilation fixes per PROMPT.md
Video module: - Fix state.db -> state.conn field name - Fix analytics.rs imports placement - Remove AppState dependency from websocket.rs (use global broadcaster) - Simplify render.rs broadcaster usage Other modules: - Add sha1 crate dependency - Fix AppState import paths (project, legal) - Fix db_pool -> conn throughout codebase - Add missing types: RefundResult, ExternalSyncError, TasksIntegrationError, RecordingError, FallbackAttemptTracker - Add stub implementations for GoogleContactsClient, MicrosoftPeopleClient - Fix social/mod.rs format string - Fix designer/canvas.rs SVG path - Remove doc comments per PROMPT.md - Add missing handler implementations in calendar_integration.rs
This commit is contained in:
parent
998e4c2806
commit
a4cbf145d2
26 changed files with 532 additions and 437 deletions
|
|
@ -137,6 +137,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
sha2 = "0.10.9"
|
sha2 = "0.10.9"
|
||||||
|
sha1 = "0.10.6"
|
||||||
tokio = { version = "1.41", features = ["full"] }
|
tokio = { version = "1.41", features = ["full"] }
|
||||||
tokio-stream = "0.1"
|
tokio-stream = "0.1"
|
||||||
tower = "0.4"
|
tower = "0.4"
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,16 @@ pub struct InvoiceDiscount {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct RefundResult {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub invoice_id: Uuid,
|
||||||
|
pub amount: i64,
|
||||||
|
pub currency: String,
|
||||||
|
pub reason: Option<String>,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct InvoiceTax {
|
pub struct InvoiceTax {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,3 @@
|
||||||
//! Contacts-Calendar Integration Module
|
|
||||||
//!
|
|
||||||
//! This module provides integration between the Contacts and Calendar apps,
|
|
||||||
//! allowing contacts to be linked to calendar events and providing contact
|
|
||||||
//! context for meetings.
|
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, Query, State},
|
extract::{Path, Query, State},
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
|
|
@ -19,7 +13,6 @@ use uuid::Uuid;
|
||||||
use crate::shared::state::AppState;
|
use crate::shared::state::AppState;
|
||||||
use crate::shared::utils::DbPool;
|
use crate::shared::utils::DbPool;
|
||||||
|
|
||||||
/// A contact linked to a calendar event
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct EventContact {
|
pub struct EventContact {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
|
|
@ -32,7 +25,6 @@ pub struct EventContact {
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Role of a contact in an event
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||||
pub enum EventContactRole {
|
pub enum EventContactRole {
|
||||||
#[default]
|
#[default]
|
||||||
|
|
@ -57,7 +49,6 @@ impl std::fmt::Display for EventContactRole {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response status for event invitation
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||||
pub enum ResponseStatus {
|
pub enum ResponseStatus {
|
||||||
#[default]
|
#[default]
|
||||||
|
|
@ -80,7 +71,6 @@ impl std::fmt::Display for ResponseStatus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request to link a contact to an event
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct LinkContactRequest {
|
pub struct LinkContactRequest {
|
||||||
pub contact_id: Uuid,
|
pub contact_id: Uuid,
|
||||||
|
|
@ -88,7 +78,6 @@ pub struct LinkContactRequest {
|
||||||
pub send_notification: Option<bool>,
|
pub send_notification: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request to link multiple contacts to an event
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct BulkLinkContactsRequest {
|
pub struct BulkLinkContactsRequest {
|
||||||
pub contact_ids: Vec<Uuid>,
|
pub contact_ids: Vec<Uuid>,
|
||||||
|
|
@ -96,14 +85,12 @@ pub struct BulkLinkContactsRequest {
|
||||||
pub send_notification: Option<bool>,
|
pub send_notification: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request to update a contact's role or status in an event
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct UpdateEventContactRequest {
|
pub struct UpdateEventContactRequest {
|
||||||
pub role: Option<EventContactRole>,
|
pub role: Option<EventContactRole>,
|
||||||
pub response_status: Option<ResponseStatus>,
|
pub response_status: Option<ResponseStatus>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Query parameters for listing event contacts
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct EventContactsQuery {
|
pub struct EventContactsQuery {
|
||||||
pub role: Option<EventContactRole>,
|
pub role: Option<EventContactRole>,
|
||||||
|
|
@ -111,7 +98,6 @@ pub struct EventContactsQuery {
|
||||||
pub include_contact_details: Option<bool>,
|
pub include_contact_details: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Query parameters for listing contact's events
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ContactEventsQuery {
|
pub struct ContactEventsQuery {
|
||||||
pub from_date: Option<DateTime<Utc>>,
|
pub from_date: Option<DateTime<Utc>>,
|
||||||
|
|
@ -122,14 +108,12 @@ pub struct ContactEventsQuery {
|
||||||
pub offset: Option<u32>,
|
pub offset: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Event contact with full contact details
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct EventContactWithDetails {
|
pub struct EventContactWithDetails {
|
||||||
pub event_contact: EventContact,
|
pub event_contact: EventContact,
|
||||||
pub contact: ContactSummary,
|
pub contact: ContactSummary,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Summary of contact information for display
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ContactSummary {
|
pub struct ContactSummary {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
|
|
@ -142,7 +126,6 @@ pub struct ContactSummary {
|
||||||
pub avatar_url: Option<String>,
|
pub avatar_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Event summary for contact view
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct EventSummary {
|
pub struct EventSummary {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
|
|
@ -155,14 +138,12 @@ pub struct EventSummary {
|
||||||
pub organizer_name: Option<String>,
|
pub organizer_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Contact's event with role information
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ContactEventWithDetails {
|
pub struct ContactEventWithDetails {
|
||||||
pub event_contact: EventContact,
|
pub event_contact: EventContact,
|
||||||
pub event: EventSummary,
|
pub event: EventSummary,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response for listing contact events
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ContactEventsResponse {
|
pub struct ContactEventsResponse {
|
||||||
pub events: Vec<ContactEventWithDetails>,
|
pub events: Vec<ContactEventWithDetails>,
|
||||||
|
|
@ -171,7 +152,6 @@ pub struct ContactEventsResponse {
|
||||||
pub past_count: u32,
|
pub past_count: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Suggested contacts based on event context
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct SuggestedContact {
|
pub struct SuggestedContact {
|
||||||
pub contact: ContactSummary,
|
pub contact: ContactSummary,
|
||||||
|
|
@ -179,7 +159,6 @@ pub struct SuggestedContact {
|
||||||
pub score: f32,
|
pub score: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reason for contact suggestion
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum SuggestionReason {
|
pub enum SuggestionReason {
|
||||||
FrequentCollaborator,
|
FrequentCollaborator,
|
||||||
|
|
@ -203,7 +182,6 @@ impl std::fmt::Display for SuggestionReason {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calendar integration service for contacts
|
|
||||||
pub struct CalendarIntegrationService {
|
pub struct CalendarIntegrationService {
|
||||||
pool: DbPool,
|
pool: DbPool,
|
||||||
}
|
}
|
||||||
|
|
@ -213,7 +191,6 @@ impl CalendarIntegrationService {
|
||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Link a contact to a calendar event
|
|
||||||
pub async fn link_contact_to_event(
|
pub async fn link_contact_to_event(
|
||||||
&self,
|
&self,
|
||||||
organization_id: Uuid,
|
organization_id: Uuid,
|
||||||
|
|
@ -267,7 +244,6 @@ impl CalendarIntegrationService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Link multiple contacts to an event
|
|
||||||
pub async fn bulk_link_contacts(
|
pub async fn bulk_link_contacts(
|
||||||
&self,
|
&self,
|
||||||
organization_id: Uuid,
|
organization_id: Uuid,
|
||||||
|
|
@ -296,7 +272,6 @@ impl CalendarIntegrationService {
|
||||||
Ok(results)
|
Ok(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Unlink a contact from an event
|
|
||||||
pub async fn unlink_contact_from_event(
|
pub async fn unlink_contact_from_event(
|
||||||
&self,
|
&self,
|
||||||
organization_id: Uuid,
|
organization_id: Uuid,
|
||||||
|
|
@ -322,7 +297,6 @@ impl CalendarIntegrationService {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update a contact's role or status in an event
|
|
||||||
pub async fn update_event_contact(
|
pub async fn update_event_contact(
|
||||||
&self,
|
&self,
|
||||||
organization_id: Uuid,
|
organization_id: Uuid,
|
||||||
|
|
@ -348,7 +322,6 @@ impl CalendarIntegrationService {
|
||||||
Ok(event_contact)
|
Ok(event_contact)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all contacts linked to an event
|
|
||||||
pub async fn get_event_contacts(
|
pub async fn get_event_contacts(
|
||||||
&self,
|
&self,
|
||||||
organization_id: Uuid,
|
organization_id: Uuid,
|
||||||
|
|
@ -390,7 +363,6 @@ impl CalendarIntegrationService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all events for a contact
|
|
||||||
pub async fn get_contact_events(
|
pub async fn get_contact_events(
|
||||||
&self,
|
&self,
|
||||||
organization_id: Uuid,
|
organization_id: Uuid,
|
||||||
|
|
@ -417,7 +389,6 @@ impl CalendarIntegrationService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get suggested contacts for an event
|
|
||||||
pub async fn get_suggested_contacts(
|
pub async fn get_suggested_contacts(
|
||||||
&self,
|
&self,
|
||||||
organization_id: Uuid,
|
organization_id: Uuid,
|
||||||
|
|
@ -480,7 +451,6 @@ impl CalendarIntegrationService {
|
||||||
Ok(suggestions)
|
Ok(suggestions)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find contacts by email for quick add to event
|
|
||||||
pub async fn find_contacts_for_event(
|
pub async fn find_contacts_for_event(
|
||||||
&self,
|
&self,
|
||||||
organization_id: Uuid,
|
organization_id: Uuid,
|
||||||
|
|
@ -496,7 +466,6 @@ impl CalendarIntegrationService {
|
||||||
Ok(results)
|
Ok(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create contacts from event attendees who don't exist
|
|
||||||
pub async fn create_contacts_from_attendees(
|
pub async fn create_contacts_from_attendees(
|
||||||
&self,
|
&self,
|
||||||
organization_id: Uuid,
|
organization_id: Uuid,
|
||||||
|
|
@ -763,7 +732,6 @@ impl CalendarIntegrationService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attendee information for creating contacts
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct AttendeeInfo {
|
pub struct AttendeeInfo {
|
||||||
pub email: String,
|
pub email: String,
|
||||||
|
|
@ -771,7 +739,6 @@ pub struct AttendeeInfo {
|
||||||
pub company: Option<String>,
|
pub company: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Error types for calendar integration
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum CalendarIntegrationError {
|
pub enum CalendarIntegrationError {
|
||||||
DatabaseError,
|
DatabaseError,
|
||||||
|
|
@ -825,7 +792,6 @@ impl IntoResponse for CalendarIntegrationError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create database tables migration
|
|
||||||
pub fn create_calendar_integration_tables_migration() -> String {
|
pub fn create_calendar_integration_tables_migration() -> String {
|
||||||
r#"
|
r#"
|
||||||
CREATE TABLE IF NOT EXISTS event_contacts (
|
CREATE TABLE IF NOT EXISTS event_contacts (
|
||||||
|
|
@ -849,7 +815,6 @@ pub fn create_calendar_integration_tables_migration() -> String {
|
||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// API routes for calendar integration
|
|
||||||
pub fn calendar_integration_routes() -> Router<Arc<AppState>> {
|
pub fn calendar_integration_routes() -> Router<Arc<AppState>> {
|
||||||
Router::new()
|
Router::new()
|
||||||
// Event contacts
|
// Event contacts
|
||||||
|
|
@ -888,8 +853,8 @@ async fn get_event_contacts_handler(
|
||||||
Path(event_id): Path<Uuid>,
|
Path(event_id): Path<Uuid>,
|
||||||
Query(query): Query<EventContactsQuery>,
|
Query(query): Query<EventContactsQuery>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let service = CalendarIntegrationService::new(state.db_pool.clone());
|
let service = CalendarIntegrationService::new(state.conn.clone());
|
||||||
let org_id = Uuid::new_v4(); // Get from auth context
|
let org_id = Uuid::new_v4();
|
||||||
|
|
||||||
match service.get_event_contacts(org_id, event_id, &query).await {
|
match service.get_event_contacts(org_id, event_id, &query).await {
|
||||||
Ok(contacts) => Json(contacts).into_response(),
|
Ok(contacts) => Json(contacts).into_response(),
|
||||||
|
|
@ -902,11 +867,106 @@ async fn link_contact_handler(
|
||||||
Path(event_id): Path<Uuid>,
|
Path(event_id): Path<Uuid>,
|
||||||
Json(request): Json<LinkContactRequest>,
|
Json(request): Json<LinkContactRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let service = CalendarIntegrationService::new(state.db_pool.clone());
|
let service = CalendarIntegrationService::new(state.conn.clone());
|
||||||
let org_id = Uuid::new_v4(); // Get from auth context
|
let org_id = Uuid::new_v4();
|
||||||
|
|
||||||
match service.link_contact_to_event(org_id, event_id, &request).await {
|
match service.link_contact_to_event(org_id, event_id, &request).await {
|
||||||
Ok(event_contact) => Json(event_contact).into_response(),
|
Ok(event_contact) => Json(event_contact).into_response(),
|
||||||
Err(e) => e.into_response(),
|
Err(e) => e.into_response(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn bulk_link_contacts_handler(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Path(event_id): Path<Uuid>,
|
||||||
|
Json(request): Json<BulkLinkContactsRequest>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let service = CalendarIntegrationService::new(state.conn.clone());
|
||||||
|
let org_id = Uuid::new_v4();
|
||||||
|
|
||||||
|
match service.bulk_link_contacts(org_id, event_id, &request).await {
|
||||||
|
Ok(contacts) => Json(contacts).into_response(),
|
||||||
|
Err(e) => e.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn unlink_contact_handler(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Path((event_id, contact_id)): Path<(Uuid, Uuid)>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let service = CalendarIntegrationService::new(state.conn.clone());
|
||||||
|
let org_id = Uuid::new_v4();
|
||||||
|
|
||||||
|
match service.unlink_contact_from_event(org_id, event_id, contact_id).await {
|
||||||
|
Ok(_) => Json(serde_json::json!({ "success": true })).into_response(),
|
||||||
|
Err(e) => e.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_event_contact_handler(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Path((event_id, contact_id)): Path<(Uuid, Uuid)>,
|
||||||
|
Json(request): Json<UpdateEventContactRequest>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let service = CalendarIntegrationService::new(state.conn.clone());
|
||||||
|
let org_id = Uuid::new_v4();
|
||||||
|
|
||||||
|
match service.update_event_contact(org_id, event_id, contact_id, &request).await {
|
||||||
|
Ok(contact) => Json(contact).into_response(),
|
||||||
|
Err(e) => e.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_suggestions_handler(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Path(event_id): Path<Uuid>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let service = CalendarIntegrationService::new(state.conn.clone());
|
||||||
|
let org_id = Uuid::new_v4();
|
||||||
|
|
||||||
|
match service.get_suggested_contacts(org_id, event_id).await {
|
||||||
|
Ok(suggestions) => Json(suggestions).into_response(),
|
||||||
|
Err(e) => e.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_contact_events_handler(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Path(contact_id): Path<Uuid>,
|
||||||
|
Query(query): Query<ContactEventsQuery>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let service = CalendarIntegrationService::new(state.conn.clone());
|
||||||
|
let org_id = Uuid::new_v4();
|
||||||
|
|
||||||
|
match service.get_contact_events(org_id, contact_id, &query).await {
|
||||||
|
Ok(events) => Json(events).into_response(),
|
||||||
|
Err(e) => e.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_contacts_handler(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Path(event_id): Path<Uuid>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let service = CalendarIntegrationService::new(state.conn.clone());
|
||||||
|
let org_id = Uuid::new_v4();
|
||||||
|
|
||||||
|
match service.find_contacts_for_event(org_id, event_id).await {
|
||||||
|
Ok(contacts) => Json(contacts).into_response(),
|
||||||
|
Err(e) => e.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_contacts_from_attendees_handler(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Path(event_id): Path<Uuid>,
|
||||||
|
Json(attendees): Json<Vec<AttendeeInfo>>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let service = CalendarIntegrationService::new(state.conn.clone());
|
||||||
|
let org_id = Uuid::new_v4();
|
||||||
|
|
||||||
|
match service.create_contacts_from_attendees(org_id, event_id, &attendees).await {
|
||||||
|
Ok(contacts) => Json(contacts).into_response(),
|
||||||
|
Err(e) => e.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,3 @@
|
||||||
//! External Address Book Synchronization Module
|
|
||||||
//!
|
|
||||||
//! This module provides synchronization between the internal Contacts app
|
|
||||||
//! and external address book providers like Google Contacts and Microsoft
|
|
||||||
//! People (Outlook/Office 365).
|
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, Query, State},
|
extract::{Path, Query, State},
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
|
|
@ -19,7 +13,151 @@ use uuid::Uuid;
|
||||||
use crate::shared::state::AppState;
|
use crate::shared::state::AppState;
|
||||||
use crate::shared::utils::DbPool;
|
use crate::shared::utils::DbPool;
|
||||||
|
|
||||||
/// Supported external providers
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct GoogleConfig {
|
||||||
|
pub client_id: String,
|
||||||
|
pub client_secret: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MicrosoftConfig {
|
||||||
|
pub client_id: String,
|
||||||
|
pub client_secret: String,
|
||||||
|
pub tenant_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GoogleContactsClient {
|
||||||
|
config: GoogleConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GoogleContactsClient {
|
||||||
|
pub fn new(config: GoogleConfig) -> Self {
|
||||||
|
Self { config }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_auth_url(&self, redirect_uri: &str, state: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"https://accounts.google.com/o/oauth2/v2/auth?client_id={}&redirect_uri={}&state={}&scope=https://www.googleapis.com/auth/contacts&response_type=code",
|
||||||
|
self.config.client_id, redirect_uri, state
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn exchange_code(&self, _code: &str, _redirect_uri: &str) -> Result<TokenResponse, ExternalSyncError> {
|
||||||
|
Ok(TokenResponse {
|
||||||
|
access_token: String::new(),
|
||||||
|
refresh_token: Some(String::new()),
|
||||||
|
expires_in: 3600,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_contacts(&self, _access_token: &str) -> Result<Vec<ExternalContact>, ExternalSyncError> {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_contact(&self, _access_token: &str, _contact: &ExternalContact) -> Result<String, ExternalSyncError> {
|
||||||
|
Ok(String::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_contact(&self, _access_token: &str, _external_id: &str, _contact: &ExternalContact) -> Result<(), ExternalSyncError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_contact(&self, _access_token: &str, _external_id: &str) -> Result<(), ExternalSyncError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MicrosoftPeopleClient {
|
||||||
|
config: MicrosoftConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MicrosoftPeopleClient {
|
||||||
|
pub fn new(config: MicrosoftConfig) -> Self {
|
||||||
|
Self { config }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_auth_url(&self, redirect_uri: &str, state: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"https://login.microsoftonline.com/{}/oauth2/v2.0/authorize?client_id={}&redirect_uri={}&state={}&scope=Contacts.ReadWrite&response_type=code",
|
||||||
|
self.config.tenant_id, self.config.client_id, redirect_uri, state
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn exchange_code(&self, _code: &str, _redirect_uri: &str) -> Result<TokenResponse, ExternalSyncError> {
|
||||||
|
Ok(TokenResponse {
|
||||||
|
access_token: String::new(),
|
||||||
|
refresh_token: Some(String::new()),
|
||||||
|
expires_in: 3600,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_contacts(&self, _access_token: &str) -> Result<Vec<ExternalContact>, ExternalSyncError> {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_contact(&self, _access_token: &str, _contact: &ExternalContact) -> Result<String, ExternalSyncError> {
|
||||||
|
Ok(String::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_contact(&self, _access_token: &str, _external_id: &str, _contact: &ExternalContact) -> Result<(), ExternalSyncError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_contact(&self, _access_token: &str, _external_id: &str) -> Result<(), ExternalSyncError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TokenResponse {
|
||||||
|
pub access_token: String,
|
||||||
|
pub refresh_token: Option<String>,
|
||||||
|
pub expires_in: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum ImportResult {
|
||||||
|
Created,
|
||||||
|
Updated,
|
||||||
|
Skipped,
|
||||||
|
Conflict,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum ExportResult {
|
||||||
|
Created,
|
||||||
|
Updated,
|
||||||
|
Deleted,
|
||||||
|
Skipped,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ExternalSyncError {
|
||||||
|
DatabaseError(String),
|
||||||
|
UnsupportedProvider(String),
|
||||||
|
Unauthorized,
|
||||||
|
SyncDisabled,
|
||||||
|
SyncInProgress,
|
||||||
|
ApiError(String),
|
||||||
|
InvalidData(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ExternalSyncError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::DatabaseError(e) => write!(f, "Database error: {e}"),
|
||||||
|
Self::UnsupportedProvider(p) => write!(f, "Unsupported provider: {p}"),
|
||||||
|
Self::Unauthorized => write!(f, "Unauthorized"),
|
||||||
|
Self::SyncDisabled => write!(f, "Sync is disabled"),
|
||||||
|
Self::SyncInProgress => write!(f, "Sync already in progress"),
|
||||||
|
Self::ApiError(e) => write!(f, "API error: {e}"),
|
||||||
|
Self::InvalidData(e) => write!(f, "Invalid data: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for ExternalSyncError {}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||||
pub enum ExternalProvider {
|
pub enum ExternalProvider {
|
||||||
Google,
|
Google,
|
||||||
|
|
@ -40,7 +178,7 @@ impl std::fmt::Display for ExternalProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::str::FromStr for ExternalProvider {
|
impl std::str::FromStr for ExternalProvider {
|
||||||
type Err = ExternalSyncError;
|
type Err = String;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
match s.to_lowercase().as_str() {
|
match s.to_lowercase().as_str() {
|
||||||
|
|
@ -48,12 +186,11 @@ impl std::str::FromStr for ExternalProvider {
|
||||||
"microsoft" => Ok(ExternalProvider::Microsoft),
|
"microsoft" => Ok(ExternalProvider::Microsoft),
|
||||||
"apple" => Ok(ExternalProvider::Apple),
|
"apple" => Ok(ExternalProvider::Apple),
|
||||||
"carddav" => Ok(ExternalProvider::CardDav),
|
"carddav" => Ok(ExternalProvider::CardDav),
|
||||||
_ => Err(ExternalSyncError::UnsupportedProvider(s.to_string())),
|
_ => Err(format!("Unsupported provider: {s}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// External account connection
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ExternalAccount {
|
pub struct ExternalAccount {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
|
|
@ -76,7 +213,6 @@ pub struct ExternalAccount {
|
||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sync direction configuration
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||||
pub enum SyncDirection {
|
pub enum SyncDirection {
|
||||||
#[default]
|
#[default]
|
||||||
|
|
@ -95,7 +231,6 @@ impl std::fmt::Display for SyncDirection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sync operation status
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
pub enum SyncStatus {
|
pub enum SyncStatus {
|
||||||
Success,
|
Success,
|
||||||
|
|
@ -117,7 +252,6 @@ impl std::fmt::Display for SyncStatus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mapping between internal and external contact
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ContactMapping {
|
pub struct ContactMapping {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
|
|
@ -131,7 +265,6 @@ pub struct ContactMapping {
|
||||||
pub conflict_data: Option<ConflictData>,
|
pub conflict_data: Option<ConflictData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sync status for individual contact mapping
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
pub enum MappingSyncStatus {
|
pub enum MappingSyncStatus {
|
||||||
Synced,
|
Synced,
|
||||||
|
|
@ -155,7 +288,6 @@ impl std::fmt::Display for MappingSyncStatus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Conflict information when sync encounters conflicting changes
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ConflictData {
|
pub struct ConflictData {
|
||||||
pub detected_at: DateTime<Utc>,
|
pub detected_at: DateTime<Utc>,
|
||||||
|
|
@ -165,7 +297,6 @@ pub struct ConflictData {
|
||||||
pub resolved_at: Option<DateTime<Utc>>,
|
pub resolved_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How to resolve a sync conflict
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
pub enum ConflictResolution {
|
pub enum ConflictResolution {
|
||||||
KeepInternal,
|
KeepInternal,
|
||||||
|
|
@ -174,7 +305,6 @@ pub enum ConflictResolution {
|
||||||
Skip,
|
Skip,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sync history record
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct SyncHistory {
|
pub struct SyncHistory {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
|
|
@ -192,7 +322,6 @@ pub struct SyncHistory {
|
||||||
pub triggered_by: SyncTrigger,
|
pub triggered_by: SyncTrigger,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What triggered the sync
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum SyncTrigger {
|
pub enum SyncTrigger {
|
||||||
Manual,
|
Manual,
|
||||||
|
|
@ -212,7 +341,6 @@ impl std::fmt::Display for SyncTrigger {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Individual sync error
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct SyncError {
|
pub struct SyncError {
|
||||||
pub contact_id: Option<Uuid>,
|
pub contact_id: Option<Uuid>,
|
||||||
|
|
@ -223,7 +351,6 @@ pub struct SyncError {
|
||||||
pub retryable: bool,
|
pub retryable: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request to connect an external account
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ConnectAccountRequest {
|
pub struct ConnectAccountRequest {
|
||||||
pub provider: ExternalProvider,
|
pub provider: ExternalProvider,
|
||||||
|
|
@ -232,21 +359,18 @@ pub struct ConnectAccountRequest {
|
||||||
pub sync_direction: Option<SyncDirection>,
|
pub sync_direction: Option<SyncDirection>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response with OAuth authorization URL
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct AuthorizationUrlResponse {
|
pub struct AuthorizationUrlResponse {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub state: String,
|
pub state: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request to start manual sync
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct StartSyncRequest {
|
pub struct StartSyncRequest {
|
||||||
pub full_sync: Option<bool>,
|
pub full_sync: Option<bool>,
|
||||||
pub direction: Option<SyncDirection>,
|
pub direction: Option<SyncDirection>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sync progress response
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct SyncProgressResponse {
|
pub struct SyncProgressResponse {
|
||||||
pub sync_id: Uuid,
|
pub sync_id: Uuid,
|
||||||
|
|
@ -259,14 +383,12 @@ pub struct SyncProgressResponse {
|
||||||
pub estimated_completion: Option<DateTime<Utc>>,
|
pub estimated_completion: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request to resolve a conflict
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ResolveConflictRequest {
|
pub struct ResolveConflictRequest {
|
||||||
pub resolution: ConflictResolution,
|
pub resolution: ConflictResolution,
|
||||||
pub merged_data: Option<MergedContactData>,
|
pub merged_data: Option<MergedContactData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Merged contact data for manual conflict resolution
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct MergedContactData {
|
pub struct MergedContactData {
|
||||||
pub first_name: Option<String>,
|
pub first_name: Option<String>,
|
||||||
|
|
@ -278,7 +400,6 @@ pub struct MergedContactData {
|
||||||
pub notes: Option<String>,
|
pub notes: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sync settings for an account
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct SyncSettings {
|
pub struct SyncSettings {
|
||||||
pub sync_enabled: bool,
|
pub sync_enabled: bool,
|
||||||
|
|
@ -308,7 +429,6 @@ impl Default for SyncSettings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Account status response
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct AccountStatusResponse {
|
pub struct AccountStatusResponse {
|
||||||
pub account: ExternalAccount,
|
pub account: ExternalAccount,
|
||||||
|
|
@ -318,7 +438,6 @@ pub struct AccountStatusResponse {
|
||||||
pub next_scheduled_sync: Option<DateTime<Utc>>,
|
pub next_scheduled_sync: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sync statistics
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct SyncStats {
|
pub struct SyncStats {
|
||||||
pub total_synced_contacts: u32,
|
pub total_synced_contacts: u32,
|
||||||
|
|
@ -329,7 +448,6 @@ pub struct SyncStats {
|
||||||
pub average_sync_duration_seconds: u32,
|
pub average_sync_duration_seconds: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// External contact representation (provider-agnostic)
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ExternalContact {
|
pub struct ExternalContact {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|
@ -377,7 +495,6 @@ pub struct ExternalAddress {
|
||||||
pub primary: bool,
|
pub primary: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// External sync service
|
|
||||||
pub struct ExternalSyncService {
|
pub struct ExternalSyncService {
|
||||||
pool: DbPool,
|
pool: DbPool,
|
||||||
google_client: GoogleContactsClient,
|
google_client: GoogleContactsClient,
|
||||||
|
|
@ -393,7 +510,6 @@ impl ExternalSyncService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get OAuth authorization URL for a provider
|
|
||||||
pub fn get_authorization_url(
|
pub fn get_authorization_url(
|
||||||
&self,
|
&self,
|
||||||
provider: &ExternalProvider,
|
provider: &ExternalProvider,
|
||||||
|
|
@ -419,7 +535,6 @@ impl ExternalSyncService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Connect an external account using OAuth authorization code
|
|
||||||
pub async fn connect_account(
|
pub async fn connect_account(
|
||||||
&self,
|
&self,
|
||||||
organization_id: Uuid,
|
organization_id: Uuid,
|
||||||
|
|
@ -499,7 +614,6 @@ impl ExternalSyncService {
|
||||||
Ok(account)
|
Ok(account)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Disconnect an external account
|
|
||||||
pub async fn disconnect_account(
|
pub async fn disconnect_account(
|
||||||
&self,
|
&self,
|
||||||
organization_id: Uuid,
|
organization_id: Uuid,
|
||||||
|
|
@ -531,7 +645,6 @@ impl ExternalSyncService {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start a sync operation
|
|
||||||
pub async fn start_sync(
|
pub async fn start_sync(
|
||||||
&self,
|
&self,
|
||||||
organization_id: Uuid,
|
organization_id: Uuid,
|
||||||
|
|
@ -617,7 +730,6 @@ impl ExternalSyncService {
|
||||||
Ok(history)
|
Ok(history)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform two-way sync
|
|
||||||
async fn perform_two_way_sync(
|
async fn perform_two_way_sync(
|
||||||
&self,
|
&self,
|
||||||
account: &ExternalAccount,
|
account: &ExternalAccount,
|
||||||
|
|
@ -633,7 +745,6 @@ impl ExternalSyncService {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Import contacts from external provider
|
|
||||||
async fn perform_import_sync(
|
async fn perform_import_sync(
|
||||||
&self,
|
&self,
|
||||||
account: &ExternalAccount,
|
account: &ExternalAccount,
|
||||||
|
|
@ -692,7 +803,6 @@ impl ExternalSyncService {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Export contacts to external provider
|
|
||||||
async fn perform_export_sync(
|
async fn perform_export_sync(
|
||||||
&self,
|
&self,
|
||||||
account: &ExternalAccount,
|
account: &ExternalAccount,
|
||||||
|
|
@ -723,7 +833,6 @@ impl ExternalSyncService {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Import a single contact
|
|
||||||
async fn import_contact(
|
async fn import_contact(
|
||||||
&self,
|
&self,
|
||||||
account: &ExternalAccount,
|
account: &ExternalAccount,
|
||||||
|
|
@ -778,7 +887,6 @@ impl ExternalSyncService {
|
||||||
Ok(ImportResult::Created)
|
Ok(ImportResult::Created)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Export a single contact
|
|
||||||
async fn export_contact(
|
async fn export_contact(
|
||||||
&self,
|
&self,
|
||||||
account: &ExternalAccount,
|
account: &ExternalAccount,
|
||||||
|
|
@ -841,7 +949,6 @@ impl ExternalSyncService {
|
||||||
Ok(ExportResult::Updated)
|
Ok(ExportResult::Updated)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get list of connected accounts
|
|
||||||
pub async fn list_accounts(
|
pub async fn list_accounts(
|
||||||
&self,
|
&self,
|
||||||
organization_id: Uuid,
|
organization_id: Uuid,
|
||||||
|
|
@ -868,7 +975,6 @@ impl ExternalSyncService {
|
||||||
Ok(results)
|
Ok(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get sync history for an account
|
|
||||||
pub async fn get_sync_history(
|
pub async fn get_sync_history(
|
||||||
&self,
|
&self,
|
||||||
organization_id: Uuid,
|
organization_id: Uuid,
|
||||||
|
|
@ -884,7 +990,6 @@ impl ExternalSyncService {
|
||||||
self.fetch_sync_history(account_id, limit.unwrap_or(20)).await
|
self.fetch_sync_history(account_id, limit.unwrap_or(20)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get pending conflicts for an account
|
|
||||||
pub async fn get_conflicts(
|
pub async fn get_conflicts(
|
||||||
&self,
|
&self,
|
||||||
organization_id: Uuid,
|
organization_id: Uuid,
|
||||||
|
|
@ -899,7 +1004,6 @@ impl ExternalSyncService {
|
||||||
self.fetch_conflicts(account_id).await
|
self.fetch_conflicts(account_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve a sync conflict
|
|
||||||
pub async fn resolve_conflict(
|
pub async fn resolve_conflict(
|
||||||
&self,
|
&self,
|
||||||
organization_id: Uuid,
|
organization_id: Uuid,
|
||||||
|
|
|
||||||
|
|
@ -1334,7 +1334,7 @@ async fn list_contacts_handler(
|
||||||
Query(query): Query<ContactListQuery>,
|
Query(query): Query<ContactListQuery>,
|
||||||
organization_id: Uuid,
|
organization_id: Uuid,
|
||||||
) -> Result<Json<ContactListResponse>, ContactsError> {
|
) -> Result<Json<ContactListResponse>, ContactsError> {
|
||||||
let service = ContactsService::new(state.db_pool.clone());
|
let service = ContactsService::new(state.conn.clone());
|
||||||
let response = service.list_contacts(organization_id, query).await?;
|
let response = service.list_contacts(organization_id, query).await?;
|
||||||
Ok(Json(response))
|
Ok(Json(response))
|
||||||
}
|
}
|
||||||
|
|
@ -1345,7 +1345,7 @@ async fn create_contact_handler(
|
||||||
user_id: Option<Uuid>,
|
user_id: Option<Uuid>,
|
||||||
Json(request): Json<CreateContactRequest>,
|
Json(request): Json<CreateContactRequest>,
|
||||||
) -> Result<Json<Contact>, ContactsError> {
|
) -> Result<Json<Contact>, ContactsError> {
|
||||||
let service = ContactsService::new(state.db_pool.clone());
|
let service = ContactsService::new(state.conn.clone());
|
||||||
let contact = service.create_contact(organization_id, user_id, request).await?;
|
let contact = service.create_contact(organization_id, user_id, request).await?;
|
||||||
Ok(Json(contact))
|
Ok(Json(contact))
|
||||||
}
|
}
|
||||||
|
|
@ -1355,7 +1355,7 @@ async fn get_contact_handler(
|
||||||
Path(contact_id): Path<Uuid>,
|
Path(contact_id): Path<Uuid>,
|
||||||
organization_id: Uuid,
|
organization_id: Uuid,
|
||||||
) -> Result<Json<Contact>, ContactsError> {
|
) -> Result<Json<Contact>, ContactsError> {
|
||||||
let service = ContactsService::new(state.db_pool.clone());
|
let service = ContactsService::new(state.conn.clone());
|
||||||
let contact = service.get_contact(organization_id, contact_id).await?;
|
let contact = service.get_contact(organization_id, contact_id).await?;
|
||||||
Ok(Json(contact))
|
Ok(Json(contact))
|
||||||
}
|
}
|
||||||
|
|
@ -1367,7 +1367,7 @@ async fn update_contact_handler(
|
||||||
user_id: Option<Uuid>,
|
user_id: Option<Uuid>,
|
||||||
Json(request): Json<UpdateContactRequest>,
|
Json(request): Json<UpdateContactRequest>,
|
||||||
) -> Result<Json<Contact>, ContactsError> {
|
) -> Result<Json<Contact>, ContactsError> {
|
||||||
let service = ContactsService::new(state.db_pool.clone());
|
let service = ContactsService::new(state.conn.clone());
|
||||||
let contact = service.update_contact(organization_id, contact_id, request, user_id).await?;
|
let contact = service.update_contact(organization_id, contact_id, request, user_id).await?;
|
||||||
Ok(Json(contact))
|
Ok(Json(contact))
|
||||||
}
|
}
|
||||||
|
|
@ -1377,7 +1377,7 @@ async fn delete_contact_handler(
|
||||||
Path(contact_id): Path<Uuid>,
|
Path(contact_id): Path<Uuid>,
|
||||||
organization_id: Uuid,
|
organization_id: Uuid,
|
||||||
) -> Result<StatusCode, ContactsError> {
|
) -> Result<StatusCode, ContactsError> {
|
||||||
let service = ContactsService::new(state.db_pool.clone());
|
let service = ContactsService::new(state.conn.clone());
|
||||||
service.delete_contact(organization_id, contact_id).await?;
|
service.delete_contact(organization_id, contact_id).await?;
|
||||||
Ok(StatusCode::NO_CONTENT)
|
Ok(StatusCode::NO_CONTENT)
|
||||||
}
|
}
|
||||||
|
|
@ -1388,7 +1388,7 @@ async fn import_contacts_handler(
|
||||||
user_id: Option<Uuid>,
|
user_id: Option<Uuid>,
|
||||||
Json(request): Json<ImportRequest>,
|
Json(request): Json<ImportRequest>,
|
||||||
) -> Result<Json<ImportResult>, ContactsError> {
|
) -> Result<Json<ImportResult>, ContactsError> {
|
||||||
let service = ContactsService::new(state.db_pool.clone());
|
let service = ContactsService::new(state.conn.clone());
|
||||||
let result = service.import_contacts(organization_id, user_id, request).await?;
|
let result = service.import_contacts(organization_id, user_id, request).await?;
|
||||||
Ok(Json(result))
|
Ok(Json(result))
|
||||||
}
|
}
|
||||||
|
|
@ -1398,7 +1398,7 @@ async fn export_contacts_handler(
|
||||||
organization_id: Uuid,
|
organization_id: Uuid,
|
||||||
Json(request): Json<ExportRequest>,
|
Json(request): Json<ExportRequest>,
|
||||||
) -> Result<Json<ExportResult>, ContactsError> {
|
) -> Result<Json<ExportResult>, ContactsError> {
|
||||||
let service = ContactsService::new(state.db_pool.clone());
|
let service = ContactsService::new(state.conn.clone());
|
||||||
let result = service.export_contacts(organization_id, request).await?;
|
let result = service.export_contacts(organization_id, request).await?;
|
||||||
Ok(Json(result))
|
Ok(Json(result))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,3 @@
|
||||||
//! Contacts-Tasks Integration Module
|
|
||||||
//!
|
|
||||||
//! This module provides integration between the Contacts and Tasks apps,
|
|
||||||
//! allowing contacts to be assigned to tasks and providing contact
|
|
||||||
//! context for task management.
|
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, Query, State},
|
extract::{Path, Query, State},
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
|
|
@ -19,7 +13,47 @@ use uuid::Uuid;
|
||||||
use crate::shared::state::AppState;
|
use crate::shared::state::AppState;
|
||||||
use crate::shared::utils::DbPool;
|
use crate::shared::utils::DbPool;
|
||||||
|
|
||||||
/// A contact assigned to a task
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum TasksIntegrationError {
|
||||||
|
DatabaseError(String),
|
||||||
|
ContactNotFound,
|
||||||
|
TaskNotFound,
|
||||||
|
AlreadyAssigned,
|
||||||
|
NotAssigned,
|
||||||
|
Unauthorized,
|
||||||
|
InvalidInput(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for TasksIntegrationError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::DatabaseError(e) => write!(f, "Database error: {e}"),
|
||||||
|
Self::ContactNotFound => write!(f, "Contact not found"),
|
||||||
|
Self::TaskNotFound => write!(f, "Task not found"),
|
||||||
|
Self::AlreadyAssigned => write!(f, "Contact already assigned"),
|
||||||
|
Self::NotAssigned => write!(f, "Contact not assigned"),
|
||||||
|
Self::Unauthorized => write!(f, "Unauthorized"),
|
||||||
|
Self::InvalidInput(e) => write!(f, "Invalid input: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for TasksIntegrationError {}
|
||||||
|
|
||||||
|
impl IntoResponse for TasksIntegrationError {
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
let status = match &self {
|
||||||
|
Self::ContactNotFound | Self::TaskNotFound => StatusCode::NOT_FOUND,
|
||||||
|
Self::AlreadyAssigned | Self::NotAssigned => StatusCode::CONFLICT,
|
||||||
|
Self::Unauthorized => StatusCode::UNAUTHORIZED,
|
||||||
|
Self::InvalidInput(_) => StatusCode::BAD_REQUEST,
|
||||||
|
Self::DatabaseError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
};
|
||||||
|
(status, Json(serde_json::json!({ "error": self.to_string() }))).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct TaskContact {
|
pub struct TaskContact {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
|
|
@ -33,8 +67,7 @@ pub struct TaskContact {
|
||||||
pub notes: Option<String>,
|
pub notes: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Role of a contact in a task
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
|
||||||
pub enum TaskContactRole {
|
pub enum TaskContactRole {
|
||||||
#[default]
|
#[default]
|
||||||
Assignee,
|
Assignee,
|
||||||
|
|
@ -62,7 +95,6 @@ impl std::fmt::Display for TaskContactRole {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request to assign a contact to a task
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct AssignContactRequest {
|
pub struct AssignContactRequest {
|
||||||
pub contact_id: Uuid,
|
pub contact_id: Uuid,
|
||||||
|
|
@ -71,14 +103,12 @@ pub struct AssignContactRequest {
|
||||||
pub notes: Option<String>,
|
pub notes: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request to assign multiple contacts to a task
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct BulkAssignContactsRequest {
|
pub struct BulkAssignContactsRequest {
|
||||||
pub assignments: Vec<ContactAssignment>,
|
pub assignments: Vec<ContactAssignment>,
|
||||||
pub send_notification: Option<bool>,
|
pub send_notification: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Individual contact assignment
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ContactAssignment {
|
pub struct ContactAssignment {
|
||||||
pub contact_id: Uuid,
|
pub contact_id: Uuid,
|
||||||
|
|
@ -86,21 +116,18 @@ pub struct ContactAssignment {
|
||||||
pub notes: Option<String>,
|
pub notes: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request to update a contact's assignment
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct UpdateTaskContactRequest {
|
pub struct UpdateTaskContactRequest {
|
||||||
pub role: Option<TaskContactRole>,
|
pub role: Option<TaskContactRole>,
|
||||||
pub notes: Option<String>,
|
pub notes: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Query parameters for listing task contacts
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct TaskContactsQuery {
|
pub struct TaskContactsQuery {
|
||||||
pub role: Option<TaskContactRole>,
|
pub role: Option<TaskContactRole>,
|
||||||
pub include_contact_details: Option<bool>,
|
pub include_contact_details: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Query parameters for listing contact's tasks
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ContactTasksQuery {
|
pub struct ContactTasksQuery {
|
||||||
pub status: Option<String>,
|
pub status: Option<String>,
|
||||||
|
|
@ -115,7 +142,6 @@ pub struct ContactTasksQuery {
|
||||||
pub sort_order: Option<SortOrder>,
|
pub sort_order: Option<SortOrder>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sort fields for tasks
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub enum TaskSortField {
|
pub enum TaskSortField {
|
||||||
#[default]
|
#[default]
|
||||||
|
|
@ -126,7 +152,6 @@ pub enum TaskSortField {
|
||||||
Title,
|
Title,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sort order
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub enum SortOrder {
|
pub enum SortOrder {
|
||||||
#[default]
|
#[default]
|
||||||
|
|
@ -134,14 +159,12 @@ pub enum SortOrder {
|
||||||
Desc,
|
Desc,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Task contact with full contact details
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct TaskContactWithDetails {
|
pub struct TaskContactWithDetails {
|
||||||
pub task_contact: TaskContact,
|
pub task_contact: TaskContact,
|
||||||
pub contact: ContactSummary,
|
pub contact: ContactSummary,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Summary of contact information for display
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ContactSummary {
|
pub struct ContactSummary {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
|
|
@ -160,7 +183,6 @@ impl ContactSummary {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Task summary for contact view
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct TaskSummary {
|
pub struct TaskSummary {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
|
|
@ -176,14 +198,12 @@ pub struct TaskSummary {
|
||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Contact's task with role information
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ContactTaskWithDetails {
|
pub struct ContactTaskWithDetails {
|
||||||
pub task_contact: TaskContact,
|
pub task_contact: TaskContact,
|
||||||
pub task: TaskSummary,
|
pub task: TaskSummary,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response for listing contact tasks
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ContactTasksResponse {
|
pub struct ContactTasksResponse {
|
||||||
pub tasks: Vec<ContactTaskWithDetails>,
|
pub tasks: Vec<ContactTaskWithDetails>,
|
||||||
|
|
@ -195,7 +215,6 @@ pub struct ContactTasksResponse {
|
||||||
pub due_this_week_count: u32,
|
pub due_this_week_count: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Task statistics for a contact
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ContactTaskStats {
|
pub struct ContactTaskStats {
|
||||||
pub contact_id: Uuid,
|
pub contact_id: Uuid,
|
||||||
|
|
@ -209,7 +228,6 @@ pub struct ContactTaskStats {
|
||||||
pub recent_activity: Vec<TaskActivity>,
|
pub recent_activity: Vec<TaskActivity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Task activity record
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct TaskActivity {
|
pub struct TaskActivity {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
|
|
@ -220,7 +238,6 @@ pub struct TaskActivity {
|
||||||
pub occurred_at: DateTime<Utc>,
|
pub occurred_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Types of task activities
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum TaskActivityType {
|
pub enum TaskActivityType {
|
||||||
Assigned,
|
Assigned,
|
||||||
|
|
@ -246,7 +263,6 @@ impl std::fmt::Display for TaskActivityType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Suggested contacts for task assignment
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct SuggestedTaskContact {
|
pub struct SuggestedTaskContact {
|
||||||
pub contact: ContactSummary,
|
pub contact: ContactSummary,
|
||||||
|
|
@ -255,7 +271,6 @@ pub struct SuggestedTaskContact {
|
||||||
pub workload: ContactWorkload,
|
pub workload: ContactWorkload,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reason for suggesting a contact for a task
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum TaskSuggestionReason {
|
pub enum TaskSuggestionReason {
|
||||||
PreviouslyAssigned,
|
PreviouslyAssigned,
|
||||||
|
|
@ -281,7 +296,6 @@ impl std::fmt::Display for TaskSuggestionReason {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Contact's current workload
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ContactWorkload {
|
pub struct ContactWorkload {
|
||||||
pub active_tasks: u32,
|
pub active_tasks: u32,
|
||||||
|
|
@ -291,7 +305,6 @@ pub struct ContactWorkload {
|
||||||
pub workload_level: WorkloadLevel,
|
pub workload_level: WorkloadLevel,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Workload level indicator
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
pub enum WorkloadLevel {
|
pub enum WorkloadLevel {
|
||||||
Low,
|
Low,
|
||||||
|
|
@ -311,7 +324,6 @@ impl std::fmt::Display for WorkloadLevel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request to create a task and assign to contact
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct CreateTaskForContactRequest {
|
pub struct CreateTaskForContactRequest {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
|
@ -324,7 +336,6 @@ pub struct CreateTaskForContactRequest {
|
||||||
pub send_notification: Option<bool>,
|
pub send_notification: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tasks integration service for contacts
|
|
||||||
pub struct TasksIntegrationService {
|
pub struct TasksIntegrationService {
|
||||||
pool: DbPool,
|
pool: DbPool,
|
||||||
}
|
}
|
||||||
|
|
@ -334,7 +345,6 @@ impl TasksIntegrationService {
|
||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Assign a contact to a task
|
|
||||||
pub async fn assign_contact_to_task(
|
pub async fn assign_contact_to_task(
|
||||||
&self,
|
&self,
|
||||||
organization_id: Uuid,
|
organization_id: Uuid,
|
||||||
|
|
@ -400,7 +410,6 @@ impl TasksIntegrationService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Assign multiple contacts to a task
|
|
||||||
pub async fn bulk_assign_contacts(
|
pub async fn bulk_assign_contacts(
|
||||||
&self,
|
&self,
|
||||||
organization_id: Uuid,
|
organization_id: Uuid,
|
||||||
|
|
@ -431,7 +440,6 @@ impl TasksIntegrationService {
|
||||||
Ok(results)
|
Ok(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Unassign a contact from a task
|
|
||||||
pub async fn unassign_contact_from_task(
|
pub async fn unassign_contact_from_task(
|
||||||
&self,
|
&self,
|
||||||
organization_id: Uuid,
|
organization_id: Uuid,
|
||||||
|
|
@ -454,7 +462,6 @@ impl TasksIntegrationService {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update a contact's assignment
|
|
||||||
pub async fn update_task_contact(
|
pub async fn update_task_contact(
|
||||||
&self,
|
&self,
|
||||||
organization_id: Uuid,
|
organization_id: Uuid,
|
||||||
|
|
@ -480,7 +487,6 @@ impl TasksIntegrationService {
|
||||||
Ok(task_contact)
|
Ok(task_contact)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all contacts assigned to a task
|
|
||||||
pub async fn get_task_contacts(
|
pub async fn get_task_contacts(
|
||||||
&self,
|
&self,
|
||||||
organization_id: Uuid,
|
organization_id: Uuid,
|
||||||
|
|
@ -522,7 +528,6 @@ impl TasksIntegrationService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all tasks for a contact
|
|
||||||
pub async fn get_contact_tasks(
|
pub async fn get_contact_tasks(
|
||||||
&self,
|
&self,
|
||||||
organization_id: Uuid,
|
organization_id: Uuid,
|
||||||
|
|
@ -569,7 +574,6 @@ impl TasksIntegrationService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get task statistics for a contact
|
|
||||||
pub async fn get_contact_task_stats(
|
pub async fn get_contact_task_stats(
|
||||||
&self,
|
&self,
|
||||||
organization_id: Uuid,
|
organization_id: Uuid,
|
||||||
|
|
@ -582,7 +586,6 @@ impl TasksIntegrationService {
|
||||||
Ok(stats)
|
Ok(stats)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get suggested contacts for a task
|
|
||||||
pub async fn get_suggested_contacts(
|
pub async fn get_suggested_contacts(
|
||||||
&self,
|
&self,
|
||||||
organization_id: Uuid,
|
organization_id: Uuid,
|
||||||
|
|
@ -650,7 +653,6 @@ impl TasksIntegrationService {
|
||||||
Ok(suggestions)
|
Ok(suggestions)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get contact's workload
|
|
||||||
pub async fn get_contact_workload(
|
pub async fn get_contact_workload(
|
||||||
&self,
|
&self,
|
||||||
organization_id: Uuid,
|
organization_id: Uuid,
|
||||||
|
|
@ -663,7 +665,6 @@ impl TasksIntegrationService {
|
||||||
Ok(workload)
|
Ok(workload)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a task and assign to contact in one operation
|
|
||||||
pub async fn create_task_for_contact(
|
pub async fn create_task_for_contact(
|
||||||
&self,
|
&self,
|
||||||
organization_id: Uuid,
|
organization_id: Uuid,
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ impl KbContextManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_active_kbs(&self, session_id: Uuid) -> Result<Vec<SessionKbAssociation>> {
|
pub fn get_active_kbs(&self, session_id: Uuid) -> Result<Vec<SessionKbAssociation>> {
|
||||||
let mut conn = self.db_pool.get()?;
|
let mut conn = self.conn.get()?;
|
||||||
|
|
||||||
let query = diesel::sql_query(
|
let query = diesel::sql_query(
|
||||||
"SELECT kb_name, qdrant_collection, kb_folder_path, is_active
|
"SELECT kb_name, qdrant_collection, kb_folder_path, is_active
|
||||||
|
|
@ -227,7 +227,7 @@ impl KbContextManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_active_tools(&self, session_id: Uuid) -> Result<Vec<String>> {
|
pub fn get_active_tools(&self, session_id: Uuid) -> Result<Vec<String>> {
|
||||||
let mut conn = self.db_pool.get()?;
|
let mut conn = self.conn.get()?;
|
||||||
|
|
||||||
let query = diesel::sql_query(
|
let query = diesel::sql_query(
|
||||||
"SELECT tool_name
|
"SELECT tool_name
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ impl UserProvisioningService {
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
let mut conn = self
|
let mut conn = self
|
||||||
.db_pool
|
.conn
|
||||||
.get()
|
.get()
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to get database connection: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("Failed to get database connection: {}", e))?;
|
||||||
diesel::insert_into(users::table)
|
diesel::insert_into(users::table)
|
||||||
|
|
@ -184,7 +184,7 @@ impl UserProvisioningService {
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
||||||
let mut conn = self
|
let mut conn = self
|
||||||
.db_pool
|
.conn
|
||||||
.get()
|
.get()
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to get database connection: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("Failed to get database connection: {}", e))?;
|
||||||
|
|
||||||
|
|
@ -219,7 +219,7 @@ impl UserProvisioningService {
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut conn = self
|
let mut conn = self
|
||||||
.db_pool
|
.conn
|
||||||
.get()
|
.get()
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to get database connection: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("Failed to get database connection: {}", e))?;
|
||||||
for (key, value) in services {
|
for (key, value) in services {
|
||||||
|
|
@ -259,7 +259,7 @@ impl UserProvisioningService {
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
||||||
let mut conn = self
|
let mut conn = self
|
||||||
.db_pool
|
.conn
|
||||||
.get()
|
.get()
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to get database connection: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("Failed to get database connection: {}", e))?;
|
||||||
diesel::delete(users::table.filter(users::username.eq(username))).execute(&mut conn)?;
|
diesel::delete(users::table.filter(users::username.eq(username))).execute(&mut conn)?;
|
||||||
|
|
@ -310,7 +310,7 @@ impl UserProvisioningService {
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
||||||
let mut conn = self
|
let mut conn = self
|
||||||
.db_pool
|
.conn
|
||||||
.get()
|
.get()
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to get database connection: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("Failed to get database connection: {}", e))?;
|
||||||
diesel::delete(
|
diesel::delete(
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ impl WebsiteCrawlerService {
|
||||||
fn check_and_crawl_websites(&self) -> Result<(), Box<dyn std::error::Error>> {
|
fn check_and_crawl_websites(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
info!("Checking for websites that need recrawling");
|
info!("Checking for websites that need recrawling");
|
||||||
|
|
||||||
let mut conn = self.db_pool.get()?;
|
let mut conn = self.conn.get()?;
|
||||||
|
|
||||||
let websites = diesel::sql_query(
|
let websites = diesel::sql_query(
|
||||||
"SELECT id, bot_id, url, expires_policy, max_depth, max_pages
|
"SELECT id, bot_id, url, expires_policy, max_depth, max_pages
|
||||||
|
|
@ -77,7 +77,7 @@ impl WebsiteCrawlerService {
|
||||||
.execute(&mut conn)?;
|
.execute(&mut conn)?;
|
||||||
|
|
||||||
let kb_manager = Arc::clone(&self.kb_manager);
|
let kb_manager = Arc::clone(&self.kb_manager);
|
||||||
let db_pool = self.db_pool.clone();
|
let db_pool = self.conn.clone();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(e) = Self::crawl_website(website, kb_manager, db_pool).await {
|
if let Err(e) = Self::crawl_website(website, kb_manager, db_pool).await {
|
||||||
|
|
|
||||||
|
|
@ -334,7 +334,7 @@ impl ContextMiddlewareState {
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(org_id)
|
.bind(org_id)
|
||||||
.fetch_optional(self.db_pool.as_ref())
|
.fetch_optional(self.conn.as_ref())
|
||||||
.await
|
.await
|
||||||
.ok()
|
.ok()
|
||||||
.flatten();
|
.flatten();
|
||||||
|
|
@ -395,7 +395,7 @@ impl ContextMiddlewareState {
|
||||||
)
|
)
|
||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.bind(org_id)
|
.bind(org_id)
|
||||||
.fetch_all(self.db_pool.as_ref())
|
.fetch_all(self.conn.as_ref())
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if let Ok(r) = role_result {
|
if let Ok(r) = role_result {
|
||||||
|
|
@ -413,7 +413,7 @@ impl ContextMiddlewareState {
|
||||||
)
|
)
|
||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.bind(org_id)
|
.bind(org_id)
|
||||||
.fetch_all(self.db_pool.as_ref())
|
.fetch_all(self.conn.as_ref())
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if let Ok(g) = group_result {
|
if let Ok(g) = group_result {
|
||||||
|
|
@ -459,18 +459,13 @@ impl ContextMiddlewareState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, sqlx::FromRow)]
|
#[derive(Debug)]
|
||||||
struct OrganizationRow {
|
struct OrganizationRow {
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
name: String,
|
name: String,
|
||||||
plan_id: Option<String>,
|
plan_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Middleware Functions
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Extract organization context from request and add to extensions
|
|
||||||
pub async fn organization_context_middleware(
|
pub async fn organization_context_middleware(
|
||||||
State(state): State<Arc<ContextMiddlewareState>>,
|
State(state): State<Arc<ContextMiddlewareState>>,
|
||||||
mut request: Request<Body>,
|
mut request: Request<Body>,
|
||||||
|
|
|
||||||
|
|
@ -1334,7 +1334,7 @@ impl CanvasService {
|
||||||
|
|
||||||
pub async fn get_asset_library(&self, asset_type: Option<AssetType>) -> Result<Vec<AssetLibraryItem>, CanvasError> {
|
pub async fn get_asset_library(&self, asset_type: Option<AssetType>) -> Result<Vec<AssetLibraryItem>, CanvasError> {
|
||||||
let icons = vec![
|
let icons = vec![
|
||||||
AssetLibraryItem { id: Uuid::new_v4(), name: "Bot".to_string(), asset_type: AssetType::Icon, url: None, svg_content: Some(include_str!("../../botui/ui/suite/assets/icons/gb-bot.svg").to_string()), category: "General Bots".to_string(), tags: vec!["bot".to_string(), "assistant".to_string()], is_system: true },
|
AssetLibraryItem { id: Uuid::new_v4(), name: "Bot".to_string(), asset_type: AssetType::Icon, url: None, svg_content: Some(include_str!("../../../botui/ui/suite/assets/icons/gb-bot.svg").to_string()), category: "General Bots".to_string(), tags: vec!["bot".to_string(), "assistant".to_string()], is_system: true },
|
||||||
AssetLibraryItem { id: Uuid::new_v4(), name: "Analytics".to_string(), asset_type: AssetType::Icon, url: None, svg_content: Some("<svg></svg>".to_string()), category: "General Bots".to_string(), tags: vec!["analytics".to_string(), "chart".to_string()], is_system: true },
|
AssetLibraryItem { id: Uuid::new_v4(), name: "Analytics".to_string(), asset_type: AssetType::Icon, url: None, svg_content: Some("<svg></svg>".to_string()), category: "General Bots".to_string(), tags: vec!["analytics".to_string(), "chart".to_string()], is_system: true },
|
||||||
AssetLibraryItem { id: Uuid::new_v4(), name: "Calendar".to_string(), asset_type: AssetType::Icon, url: None, svg_content: Some("<svg></svg>".to_string()), category: "General Bots".to_string(), tags: vec!["calendar".to_string(), "date".to_string()], is_system: true },
|
AssetLibraryItem { id: Uuid::new_v4(), name: "Calendar".to_string(), asset_type: AssetType::Icon, url: None, svg_content: Some("<svg></svg>".to_string()), category: "General Bots".to_string(), tags: vec!["calendar".to_string(), "date".to_string()], is_system: true },
|
||||||
AssetLibraryItem { id: Uuid::new_v4(), name: "Chat".to_string(), asset_type: AssetType::Icon, url: None, svg_content: Some("<svg></svg>".to_string()), category: "General Bots".to_string(), tags: vec!["chat".to_string(), "message".to_string()], is_system: true },
|
AssetLibraryItem { id: Uuid::new_v4(), name: "Chat".to_string(), asset_type: AssetType::Icon, url: None, svg_content: Some("<svg></svg>".to_string()), category: "General Bots".to_string(), tags: vec!["chat".to_string(), "message".to_string()], is_system: true },
|
||||||
|
|
@ -1455,7 +1455,7 @@ async fn create_canvas_handler(
|
||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
Json(request): Json<CreateCanvasRequest>,
|
Json(request): Json<CreateCanvasRequest>,
|
||||||
) -> Result<Json<Canvas>, CanvasError> {
|
) -> Result<Json<Canvas>, CanvasError> {
|
||||||
let service = CanvasService::new(state.db_pool.clone());
|
let service = CanvasService::new(state.conn.clone());
|
||||||
let canvas = service.create_canvas(organization_id, user_id, request).await?;
|
let canvas = service.create_canvas(organization_id, user_id, request).await?;
|
||||||
Ok(Json(canvas))
|
Ok(Json(canvas))
|
||||||
}
|
}
|
||||||
|
|
@ -1464,7 +1464,7 @@ async fn get_canvas_handler(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(canvas_id): Path<Uuid>,
|
Path(canvas_id): Path<Uuid>,
|
||||||
) -> Result<Json<Canvas>, CanvasError> {
|
) -> Result<Json<Canvas>, CanvasError> {
|
||||||
let service = CanvasService::new(state.db_pool.clone());
|
let service = CanvasService::new(state.conn.clone());
|
||||||
let canvas = service.get_canvas(canvas_id).await?;
|
let canvas = service.get_canvas(canvas_id).await?;
|
||||||
Ok(Json(canvas))
|
Ok(Json(canvas))
|
||||||
}
|
}
|
||||||
|
|
@ -1475,7 +1475,7 @@ async fn add_element_handler(
|
||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
Json(request): Json<AddElementRequest>,
|
Json(request): Json<AddElementRequest>,
|
||||||
) -> Result<Json<CanvasElement>, CanvasError> {
|
) -> Result<Json<CanvasElement>, CanvasError> {
|
||||||
let service = CanvasService::new(state.db_pool.clone());
|
let service = CanvasService::new(state.conn.clone());
|
||||||
let element = service.add_element(canvas_id, user_id, request).await?;
|
let element = service.add_element(canvas_id, user_id, request).await?;
|
||||||
Ok(Json(element))
|
Ok(Json(element))
|
||||||
}
|
}
|
||||||
|
|
@ -1486,7 +1486,7 @@ async fn update_element_handler(
|
||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
Json(request): Json<UpdateElementRequest>,
|
Json(request): Json<UpdateElementRequest>,
|
||||||
) -> Result<Json<CanvasElement>, CanvasError> {
|
) -> Result<Json<CanvasElement>, CanvasError> {
|
||||||
let service = CanvasService::new(state.db_pool.clone());
|
let service = CanvasService::new(state.conn.clone());
|
||||||
let element = service.update_element(canvas_id, element_id, user_id, request).await?;
|
let element = service.update_element(canvas_id, element_id, user_id, request).await?;
|
||||||
Ok(Json(element))
|
Ok(Json(element))
|
||||||
}
|
}
|
||||||
|
|
@ -1496,7 +1496,7 @@ async fn delete_element_handler(
|
||||||
Path((canvas_id, element_id)): Path<(Uuid, Uuid)>,
|
Path((canvas_id, element_id)): Path<(Uuid, Uuid)>,
|
||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
) -> Result<StatusCode, CanvasError> {
|
) -> Result<StatusCode, CanvasError> {
|
||||||
let service = CanvasService::new(state.db_pool.clone());
|
let service = CanvasService::new(state.conn.clone());
|
||||||
service.delete_element(canvas_id, element_id, user_id).await?;
|
service.delete_element(canvas_id, element_id, user_id).await?;
|
||||||
Ok(StatusCode::NO_CONTENT)
|
Ok(StatusCode::NO_CONTENT)
|
||||||
}
|
}
|
||||||
|
|
@ -1507,7 +1507,7 @@ async fn group_elements_handler(
|
||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
Json(request): Json<GroupElementsRequest>,
|
Json(request): Json<GroupElementsRequest>,
|
||||||
) -> Result<Json<CanvasElement>, CanvasError> {
|
) -> Result<Json<CanvasElement>, CanvasError> {
|
||||||
let service = CanvasService::new(state.db_pool.clone());
|
let service = CanvasService::new(state.conn.clone());
|
||||||
let group = service.group_elements(canvas_id, user_id, request).await?;
|
let group = service.group_elements(canvas_id, user_id, request).await?;
|
||||||
Ok(Json(group))
|
Ok(Json(group))
|
||||||
}
|
}
|
||||||
|
|
@ -1518,7 +1518,7 @@ async fn add_layer_handler(
|
||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
Json(request): Json<CreateLayerRequest>,
|
Json(request): Json<CreateLayerRequest>,
|
||||||
) -> Result<Json<Layer>, CanvasError> {
|
) -> Result<Json<Layer>, CanvasError> {
|
||||||
let service = CanvasService::new(state.db_pool.clone());
|
let service = CanvasService::new(state.conn.clone());
|
||||||
let layer = service.add_layer(canvas_id, user_id, request).await?;
|
let layer = service.add_layer(canvas_id, user_id, request).await?;
|
||||||
Ok(Json(layer))
|
Ok(Json(layer))
|
||||||
}
|
}
|
||||||
|
|
@ -1528,7 +1528,7 @@ async fn export_canvas_handler(
|
||||||
Path(canvas_id): Path<Uuid>,
|
Path(canvas_id): Path<Uuid>,
|
||||||
Json(request): Json<ExportRequest>,
|
Json(request): Json<ExportRequest>,
|
||||||
) -> Result<Json<ExportResult>, CanvasError> {
|
) -> Result<Json<ExportResult>, CanvasError> {
|
||||||
let service = CanvasService::new(state.db_pool.clone());
|
let service = CanvasService::new(state.conn.clone());
|
||||||
let result = service.export_canvas(canvas_id, request).await?;
|
let result = service.export_canvas(canvas_id, request).await?;
|
||||||
Ok(Json(result))
|
Ok(Json(result))
|
||||||
}
|
}
|
||||||
|
|
@ -1542,7 +1542,7 @@ async fn get_templates_handler(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Query(query): Query<TemplatesQuery>,
|
Query(query): Query<TemplatesQuery>,
|
||||||
) -> Result<Json<Vec<CanvasTemplate>>, CanvasError> {
|
) -> Result<Json<Vec<CanvasTemplate>>, CanvasError> {
|
||||||
let service = CanvasService::new(state.db_pool.clone());
|
let service = CanvasService::new(state.conn.clone());
|
||||||
let templates = service.get_templates(query.category).await?;
|
let templates = service.get_templates(query.category).await?;
|
||||||
Ok(Json(templates))
|
Ok(Json(templates))
|
||||||
}
|
}
|
||||||
|
|
@ -1565,7 +1565,7 @@ async fn get_assets_handler(
|
||||||
_ => None,
|
_ => None,
|
||||||
});
|
});
|
||||||
|
|
||||||
let service = CanvasService::new(state.db_pool.clone());
|
let service = CanvasService::new(state.conn.clone());
|
||||||
let assets = service.get_asset_library(asset_type).await?;
|
let assets = service.get_asset_library(asset_type).await?;
|
||||||
Ok(Json(assets))
|
Ok(Json(assets))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1606,7 +1606,7 @@ pub async fn list_courses(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Query(filters): Query<CourseFilters>,
|
Query(filters): Query<CourseFilters>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = LearnEngine::new(state.db_pool.clone());
|
let engine = LearnEngine::new(state.conn.clone());
|
||||||
|
|
||||||
match engine.list_courses(filters).await {
|
match engine.list_courses(filters).await {
|
||||||
Ok(courses) => Json(serde_json::json!({
|
Ok(courses) => Json(serde_json::json!({
|
||||||
|
|
@ -1630,7 +1630,7 @@ pub async fn create_course(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Json(req): Json<CreateCourseRequest>,
|
Json(req): Json<CreateCourseRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = LearnEngine::new(state.db_pool.clone());
|
let engine = LearnEngine::new(state.conn.clone());
|
||||||
|
|
||||||
match engine.create_course(req, None, None).await {
|
match engine.create_course(req, None, None).await {
|
||||||
Ok(course) => (
|
Ok(course) => (
|
||||||
|
|
@ -1657,7 +1657,7 @@ pub async fn get_course(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(course_id): Path<Uuid>,
|
Path(course_id): Path<Uuid>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = LearnEngine::new(state.db_pool.clone());
|
let engine = LearnEngine::new(state.conn.clone());
|
||||||
|
|
||||||
match engine.get_course(course_id).await {
|
match engine.get_course(course_id).await {
|
||||||
Ok(Some(course)) => {
|
Ok(Some(course)) => {
|
||||||
|
|
@ -1699,7 +1699,7 @@ pub async fn update_course(
|
||||||
Path(course_id): Path<Uuid>,
|
Path(course_id): Path<Uuid>,
|
||||||
Json(req): Json<UpdateCourseRequest>,
|
Json(req): Json<UpdateCourseRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = LearnEngine::new(state.db_pool.clone());
|
let engine = LearnEngine::new(state.conn.clone());
|
||||||
|
|
||||||
match engine.update_course(course_id, req).await {
|
match engine.update_course(course_id, req).await {
|
||||||
Ok(course) => Json(serde_json::json!({
|
Ok(course) => Json(serde_json::json!({
|
||||||
|
|
@ -1723,7 +1723,7 @@ pub async fn delete_course(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(course_id): Path<Uuid>,
|
Path(course_id): Path<Uuid>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = LearnEngine::new(state.db_pool.clone());
|
let engine = LearnEngine::new(state.conn.clone());
|
||||||
|
|
||||||
match engine.delete_course(course_id).await {
|
match engine.delete_course(course_id).await {
|
||||||
Ok(()) => Json(serde_json::json!({
|
Ok(()) => Json(serde_json::json!({
|
||||||
|
|
@ -1747,7 +1747,7 @@ pub async fn get_lessons(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(course_id): Path<Uuid>,
|
Path(course_id): Path<Uuid>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = LearnEngine::new(state.db_pool.clone());
|
let engine = LearnEngine::new(state.conn.clone());
|
||||||
|
|
||||||
match engine.get_lessons(course_id).await {
|
match engine.get_lessons(course_id).await {
|
||||||
Ok(lessons) => Json(serde_json::json!({
|
Ok(lessons) => Json(serde_json::json!({
|
||||||
|
|
@ -1772,7 +1772,7 @@ pub async fn create_lesson(
|
||||||
Path(course_id): Path<Uuid>,
|
Path(course_id): Path<Uuid>,
|
||||||
Json(req): Json<CreateLessonRequest>,
|
Json(req): Json<CreateLessonRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = LearnEngine::new(state.db_pool.clone());
|
let engine = LearnEngine::new(state.conn.clone());
|
||||||
|
|
||||||
match engine.create_lesson(course_id, req).await {
|
match engine.create_lesson(course_id, req).await {
|
||||||
Ok(lesson) => (
|
Ok(lesson) => (
|
||||||
|
|
@ -1800,7 +1800,7 @@ pub async fn update_lesson(
|
||||||
Path(lesson_id): Path<Uuid>,
|
Path(lesson_id): Path<Uuid>,
|
||||||
Json(req): Json<UpdateLessonRequest>,
|
Json(req): Json<UpdateLessonRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = LearnEngine::new(state.db_pool.clone());
|
let engine = LearnEngine::new(state.conn.clone());
|
||||||
|
|
||||||
match engine.update_lesson(lesson_id, req).await {
|
match engine.update_lesson(lesson_id, req).await {
|
||||||
Ok(lesson) => Json(serde_json::json!({
|
Ok(lesson) => Json(serde_json::json!({
|
||||||
|
|
@ -1824,7 +1824,7 @@ pub async fn delete_lesson(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(lesson_id): Path<Uuid>,
|
Path(lesson_id): Path<Uuid>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = LearnEngine::new(state.db_pool.clone());
|
let engine = LearnEngine::new(state.conn.clone());
|
||||||
|
|
||||||
match engine.delete_lesson(lesson_id).await {
|
match engine.delete_lesson(lesson_id).await {
|
||||||
Ok(()) => Json(serde_json::json!({
|
Ok(()) => Json(serde_json::json!({
|
||||||
|
|
@ -1848,7 +1848,7 @@ pub async fn get_quiz(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(course_id): Path<Uuid>,
|
Path(course_id): Path<Uuid>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = LearnEngine::new(state.db_pool.clone());
|
let engine = LearnEngine::new(state.conn.clone());
|
||||||
|
|
||||||
match engine.get_quiz(course_id).await {
|
match engine.get_quiz(course_id).await {
|
||||||
Ok(Some(quiz)) => Json(serde_json::json!({
|
Ok(Some(quiz)) => Json(serde_json::json!({
|
||||||
|
|
@ -1881,7 +1881,7 @@ pub async fn submit_quiz(
|
||||||
Path(course_id): Path<Uuid>,
|
Path(course_id): Path<Uuid>,
|
||||||
Json(submission): Json<QuizSubmission>,
|
Json(submission): Json<QuizSubmission>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = LearnEngine::new(state.db_pool.clone());
|
let engine = LearnEngine::new(state.conn.clone());
|
||||||
|
|
||||||
// Get quiz ID first
|
// Get quiz ID first
|
||||||
let quiz = match engine.get_quiz(course_id).await {
|
let quiz = match engine.get_quiz(course_id).await {
|
||||||
|
|
@ -1933,7 +1933,7 @@ pub async fn get_progress(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Query(filters): Query<ProgressFilters>,
|
Query(filters): Query<ProgressFilters>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = LearnEngine::new(state.db_pool.clone());
|
let engine = LearnEngine::new(state.conn.clone());
|
||||||
|
|
||||||
// TODO: Get user_id from session
|
// TODO: Get user_id from session
|
||||||
let user_id = Uuid::new_v4();
|
let user_id = Uuid::new_v4();
|
||||||
|
|
@ -1960,7 +1960,7 @@ pub async fn start_course(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(course_id): Path<Uuid>,
|
Path(course_id): Path<Uuid>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = LearnEngine::new(state.db_pool.clone());
|
let engine = LearnEngine::new(state.conn.clone());
|
||||||
|
|
||||||
// TODO: Get user_id from session
|
// TODO: Get user_id from session
|
||||||
let user_id = Uuid::new_v4();
|
let user_id = Uuid::new_v4();
|
||||||
|
|
@ -1987,7 +1987,7 @@ pub async fn complete_lesson_handler(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(lesson_id): Path<Uuid>,
|
Path(lesson_id): Path<Uuid>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = LearnEngine::new(state.db_pool.clone());
|
let engine = LearnEngine::new(state.conn.clone());
|
||||||
|
|
||||||
// TODO: Get user_id from session
|
// TODO: Get user_id from session
|
||||||
let user_id = Uuid::new_v4();
|
let user_id = Uuid::new_v4();
|
||||||
|
|
@ -2014,7 +2014,7 @@ pub async fn create_assignment(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Json(req): Json<CreateAssignmentRequest>,
|
Json(req): Json<CreateAssignmentRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = LearnEngine::new(state.db_pool.clone());
|
let engine = LearnEngine::new(state.conn.clone());
|
||||||
|
|
||||||
// TODO: Get assigner user_id from session
|
// TODO: Get assigner user_id from session
|
||||||
let assigned_by = None;
|
let assigned_by = None;
|
||||||
|
|
@ -2041,7 +2041,7 @@ pub async fn create_assignment(
|
||||||
|
|
||||||
/// Get pending assignments
|
/// Get pending assignments
|
||||||
pub async fn get_pending_assignments(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
pub async fn get_pending_assignments(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||||
let engine = LearnEngine::new(state.db_pool.clone());
|
let engine = LearnEngine::new(state.conn.clone());
|
||||||
|
|
||||||
// TODO: Get user_id from session
|
// TODO: Get user_id from session
|
||||||
let user_id = Uuid::new_v4();
|
let user_id = Uuid::new_v4();
|
||||||
|
|
@ -2068,7 +2068,7 @@ pub async fn delete_assignment(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(assignment_id): Path<Uuid>,
|
Path(assignment_id): Path<Uuid>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = LearnEngine::new(state.db_pool.clone());
|
let engine = LearnEngine::new(state.conn.clone());
|
||||||
|
|
||||||
match engine.delete_assignment(assignment_id).await {
|
match engine.delete_assignment(assignment_id).await {
|
||||||
Ok(()) => Json(serde_json::json!({
|
Ok(()) => Json(serde_json::json!({
|
||||||
|
|
@ -2089,7 +2089,7 @@ pub async fn delete_assignment(
|
||||||
|
|
||||||
/// Get user certificates
|
/// Get user certificates
|
||||||
pub async fn get_certificates(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
pub async fn get_certificates(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||||
let engine = LearnEngine::new(state.db_pool.clone());
|
let engine = LearnEngine::new(state.conn.clone());
|
||||||
|
|
||||||
// TODO: Get user_id from session
|
// TODO: Get user_id from session
|
||||||
let user_id = Uuid::new_v4();
|
let user_id = Uuid::new_v4();
|
||||||
|
|
@ -2126,7 +2126,7 @@ pub async fn verify_certificate(Path(code): Path<String>) -> impl IntoResponse {
|
||||||
|
|
||||||
/// Get categories
|
/// Get categories
|
||||||
pub async fn get_categories(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
pub async fn get_categories(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||||
let engine = LearnEngine::new(state.db_pool.clone());
|
let engine = LearnEngine::new(state.conn.clone());
|
||||||
|
|
||||||
match engine.get_categories().await {
|
match engine.get_categories().await {
|
||||||
Ok(categories) => Json(serde_json::json!({
|
Ok(categories) => Json(serde_json::json!({
|
||||||
|
|
@ -2147,7 +2147,7 @@ pub async fn get_categories(State(state): State<Arc<AppState>>) -> impl IntoResp
|
||||||
|
|
||||||
/// Get AI recommendations
|
/// Get AI recommendations
|
||||||
pub async fn get_recommendations(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
pub async fn get_recommendations(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||||
let engine = LearnEngine::new(state.db_pool.clone());
|
let engine = LearnEngine::new(state.conn.clone());
|
||||||
|
|
||||||
// TODO: Get user_id from session
|
// TODO: Get user_id from session
|
||||||
let user_id = Uuid::new_v4();
|
let user_id = Uuid::new_v4();
|
||||||
|
|
@ -2171,7 +2171,7 @@ pub async fn get_recommendations(State(state): State<Arc<AppState>>) -> impl Int
|
||||||
|
|
||||||
/// Get learn statistics
|
/// Get learn statistics
|
||||||
pub async fn get_statistics(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
pub async fn get_statistics(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||||
let engine = LearnEngine::new(state.db_pool.clone());
|
let engine = LearnEngine::new(state.conn.clone());
|
||||||
|
|
||||||
match engine.get_statistics().await {
|
match engine.get_statistics().await {
|
||||||
Ok(stats) => Json(serde_json::json!({
|
Ok(stats) => Json(serde_json::json!({
|
||||||
|
|
@ -2192,7 +2192,7 @@ pub async fn get_statistics(State(state): State<Arc<AppState>>) -> impl IntoResp
|
||||||
|
|
||||||
/// Get user stats
|
/// Get user stats
|
||||||
pub async fn get_user_stats(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
pub async fn get_user_stats(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||||
let engine = LearnEngine::new(state.db_pool.clone());
|
let engine = LearnEngine::new(state.conn.clone());
|
||||||
|
|
||||||
// TODO: Get user_id from session
|
// TODO: Get user_id from session
|
||||||
let user_id = Uuid::new_v4();
|
let user_id = Uuid::new_v4();
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ use std::sync::Arc;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::AppState;
|
use crate::shared::state::AppState;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ impl std::fmt::Debug for CachedLLMProvider {
|
||||||
.field("cache", &self.cache)
|
.field("cache", &self.cache)
|
||||||
.field("config", &self.config)
|
.field("config", &self.config)
|
||||||
.field("embedding_service", &self.embedding_service.is_some())
|
.field("embedding_service", &self.embedding_service.is_some())
|
||||||
.field("db_pool", &self.db_pool.is_some())
|
.field("db_pool", &self.conn.is_some())
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -145,7 +145,7 @@ impl CachedLLMProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn is_cache_enabled(&self, bot_id: &str) -> bool {
|
async fn is_cache_enabled(&self, bot_id: &str) -> bool {
|
||||||
if let Some(ref db_pool) = self.db_pool {
|
if let Some(ref db_pool) = self.conn {
|
||||||
let bot_uuid = match Uuid::parse_str(bot_id) {
|
let bot_uuid = match Uuid::parse_str(bot_id) {
|
||||||
Ok(uuid) => uuid,
|
Ok(uuid) => uuid,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
|
|
@ -181,7 +181,7 @@ impl CachedLLMProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_bot_cache_config(&self, bot_id: &str) -> CacheConfig {
|
fn get_bot_cache_config(&self, bot_id: &str) -> CacheConfig {
|
||||||
if let Some(ref db_pool) = self.db_pool {
|
if let Some(ref db_pool) = self.conn {
|
||||||
let bot_uuid = match Uuid::parse_str(bot_id) {
|
let bot_uuid = match Uuid::parse_str(bot_id) {
|
||||||
Ok(uuid) => uuid,
|
Ok(uuid) => uuid,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
|
|
|
||||||
|
|
@ -1012,7 +1012,7 @@ async fn preview_cleanup_handler(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Query(query): Query<PreviewQuery>,
|
Query(query): Query<PreviewQuery>,
|
||||||
) -> Result<Json<CleanupPreview>, CleanupError> {
|
) -> Result<Json<CleanupPreview>, CleanupError> {
|
||||||
let service = CleanupService::new(state.db_pool.clone());
|
let service = CleanupService::new(state.conn.clone());
|
||||||
|
|
||||||
let categories = query.categories.map(|s| {
|
let categories = query.categories.map(|s| {
|
||||||
s.split(',')
|
s.split(',')
|
||||||
|
|
@ -1041,7 +1041,7 @@ async fn execute_cleanup_handler(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Json(request): Json<ExecuteRequest>,
|
Json(request): Json<ExecuteRequest>,
|
||||||
) -> Result<Json<CleanupResult>, CleanupError> {
|
) -> Result<Json<CleanupResult>, CleanupError> {
|
||||||
let service = CleanupService::new(state.db_pool.clone());
|
let service = CleanupService::new(state.conn.clone());
|
||||||
|
|
||||||
let categories = request.categories.map(|cats| {
|
let categories = request.categories.map(|cats| {
|
||||||
cats.iter()
|
cats.iter()
|
||||||
|
|
@ -1076,7 +1076,7 @@ async fn storage_usage_handler(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Query(query): Query<StorageQuery>,
|
Query(query): Query<StorageQuery>,
|
||||||
) -> Result<Json<StorageUsage>, CleanupError> {
|
) -> Result<Json<StorageUsage>, CleanupError> {
|
||||||
let service = CleanupService::new(state.db_pool.clone());
|
let service = CleanupService::new(state.conn.clone());
|
||||||
let usage = service.get_storage_usage(query.organization_id).await?;
|
let usage = service.get_storage_usage(query.organization_id).await?;
|
||||||
Ok(Json(usage))
|
Ok(Json(usage))
|
||||||
}
|
}
|
||||||
|
|
@ -1085,7 +1085,7 @@ async fn cleanup_history_handler(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Query(query): Query<HistoryQuery>,
|
Query(query): Query<HistoryQuery>,
|
||||||
) -> Result<Json<Vec<CleanupHistory>>, CleanupError> {
|
) -> Result<Json<Vec<CleanupHistory>>, CleanupError> {
|
||||||
let service = CleanupService::new(state.db_pool.clone());
|
let service = CleanupService::new(state.conn.clone());
|
||||||
let history = service
|
let history = service
|
||||||
.get_cleanup_history(query.organization_id, query.limit)
|
.get_cleanup_history(query.organization_id, query.limit)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
@ -1096,7 +1096,7 @@ async fn get_config_handler(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Query(query): Query<StorageQuery>,
|
Query(query): Query<StorageQuery>,
|
||||||
) -> Result<Json<CleanupConfig>, CleanupError> {
|
) -> Result<Json<CleanupConfig>, CleanupError> {
|
||||||
let service = CleanupService::new(state.db_pool.clone());
|
let service = CleanupService::new(state.conn.clone());
|
||||||
let config = service.get_cleanup_config(query.organization_id).await?;
|
let config = service.get_cleanup_config(query.organization_id).await?;
|
||||||
Ok(Json(config))
|
Ok(Json(config))
|
||||||
}
|
}
|
||||||
|
|
@ -1105,7 +1105,7 @@ async fn save_config_handler(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Json(config): Json<CleanupConfig>,
|
Json(config): Json<CleanupConfig>,
|
||||||
) -> Result<StatusCode, CleanupError> {
|
) -> Result<StatusCode, CleanupError> {
|
||||||
let service = CleanupService::new(state.db_pool.clone());
|
let service = CleanupService::new(state.conn.clone());
|
||||||
service.save_cleanup_config(&config).await?;
|
service.save_cleanup_config(&config).await?;
|
||||||
Ok(StatusCode::OK)
|
Ok(StatusCode::OK)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,3 @@
|
||||||
//! Webinar Recording and Transcription Service
|
|
||||||
//!
|
|
||||||
//! This module provides recording and automatic transcription capabilities for webinars.
|
|
||||||
//! It integrates with various transcription providers and handles the full lifecycle
|
|
||||||
//! of recordings from capture to processing to storage.
|
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
|
|
@ -20,45 +14,59 @@ use uuid::Uuid;
|
||||||
use crate::shared::state::AppState;
|
use crate::shared::state::AppState;
|
||||||
use crate::shared::utils::DbPool;
|
use crate::shared::utils::DbPool;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum RecordingError {
|
||||||
|
DatabaseError(String),
|
||||||
|
NotFound,
|
||||||
|
AlreadyExists,
|
||||||
|
InvalidState(String),
|
||||||
|
StorageError(String),
|
||||||
|
TranscriptionError(String),
|
||||||
|
Unauthorized,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for RecordingError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::DatabaseError(e) => write!(f, "Database error: {e}"),
|
||||||
|
Self::NotFound => write!(f, "Recording not found"),
|
||||||
|
Self::AlreadyExists => write!(f, "Recording already exists"),
|
||||||
|
Self::InvalidState(s) => write!(f, "Invalid state: {s}"),
|
||||||
|
Self::StorageError(e) => write!(f, "Storage error: {e}"),
|
||||||
|
Self::TranscriptionError(e) => write!(f, "Transcription error: {e}"),
|
||||||
|
Self::Unauthorized => write!(f, "Unauthorized"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for RecordingError {}
|
||||||
|
|
||||||
use super::webinar::{
|
use super::webinar::{
|
||||||
RecordingQuality, RecordingStatus, TranscriptionFormat, TranscriptionSegment,
|
RecordingQuality, RecordingStatus, TranscriptionFormat, TranscriptionSegment,
|
||||||
TranscriptionStatus, TranscriptionWord, WebinarRecording, WebinarTranscription,
|
TranscriptionStatus, TranscriptionWord, WebinarRecording, WebinarTranscription,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Maximum recording duration in seconds (8 hours)
|
|
||||||
const MAX_RECORDING_DURATION_SECONDS: u64 = 28800;
|
const MAX_RECORDING_DURATION_SECONDS: u64 = 28800;
|
||||||
|
|
||||||
/// Default transcription language
|
|
||||||
const DEFAULT_TRANSCRIPTION_LANGUAGE: &str = "en-US";
|
const DEFAULT_TRANSCRIPTION_LANGUAGE: &str = "en-US";
|
||||||
|
|
||||||
/// Supported transcription languages
|
|
||||||
const SUPPORTED_LANGUAGES: &[&str] = &[
|
const SUPPORTED_LANGUAGES: &[&str] = &[
|
||||||
"en-US", "en-GB", "es-ES", "es-MX", "fr-FR", "de-DE", "it-IT", "pt-BR", "pt-PT", "nl-NL",
|
"en-US", "en-GB", "es-ES", "es-MX", "fr-FR", "de-DE", "it-IT", "pt-BR", "pt-PT", "nl-NL",
|
||||||
"pl-PL", "ru-RU", "ja-JP", "ko-KR", "zh-CN", "zh-TW", "ar-SA", "hi-IN", "tr-TR", "vi-VN",
|
"pl-PL", "ru-RU", "ja-JP", "ko-KR", "zh-CN", "zh-TW", "ar-SA", "hi-IN", "tr-TR", "vi-VN",
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Configuration for recording service
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct RecordingConfig {
|
pub struct RecordingConfig {
|
||||||
/// Maximum recording duration in seconds
|
|
||||||
pub max_duration_seconds: u64,
|
pub max_duration_seconds: u64,
|
||||||
/// Default recording quality
|
|
||||||
pub default_quality: RecordingQuality,
|
pub default_quality: RecordingQuality,
|
||||||
/// Storage backend (local, s3, azure, gcs)
|
|
||||||
pub storage_backend: StorageBackend,
|
pub storage_backend: StorageBackend,
|
||||||
/// Storage bucket/container name
|
|
||||||
pub storage_bucket: String,
|
pub storage_bucket: String,
|
||||||
/// Enable automatic transcription
|
|
||||||
pub auto_transcribe: bool,
|
pub auto_transcribe: bool,
|
||||||
/// Default transcription language
|
|
||||||
pub default_language: String,
|
pub default_language: String,
|
||||||
/// Transcription provider
|
|
||||||
pub transcription_provider: TranscriptionProvider,
|
pub transcription_provider: TranscriptionProvider,
|
||||||
/// Recording retention days (0 = indefinite)
|
|
||||||
pub retention_days: u32,
|
pub retention_days: u32,
|
||||||
/// Enable speaker diarization
|
|
||||||
pub speaker_diarization: bool,
|
pub speaker_diarization: bool,
|
||||||
/// Maximum speakers to identify
|
|
||||||
pub max_speakers: u8,
|
pub max_speakers: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,7 +87,6 @@ impl Default for RecordingConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Storage backend options
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
||||||
pub enum StorageBackend {
|
pub enum StorageBackend {
|
||||||
#[default]
|
#[default]
|
||||||
|
|
@ -100,7 +107,6 @@ impl std::fmt::Display for StorageBackend {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Transcription provider options
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
||||||
pub enum TranscriptionProvider {
|
pub enum TranscriptionProvider {
|
||||||
#[default]
|
#[default]
|
||||||
|
|
@ -125,7 +131,6 @@ impl std::fmt::Display for TranscriptionProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recording session state
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct RecordingSession {
|
pub struct RecordingSession {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
|
|
@ -143,7 +148,6 @@ pub struct RecordingSession {
|
||||||
pub bytes_written: u64,
|
pub bytes_written: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Transcription job state
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct TranscriptionJob {
|
pub struct TranscriptionJob {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
|
|
@ -161,7 +165,6 @@ pub struct TranscriptionJob {
|
||||||
pub retry_count: u8,
|
pub retry_count: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recording event types
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum RecordingEvent {
|
pub enum RecordingEvent {
|
||||||
Started {
|
Started {
|
||||||
|
|
@ -216,7 +219,6 @@ pub enum RecordingEvent {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request to start recording
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct StartRecordingRequest {
|
pub struct StartRecordingRequest {
|
||||||
pub webinar_id: Uuid,
|
pub webinar_id: Uuid,
|
||||||
|
|
@ -226,14 +228,12 @@ pub struct StartRecordingRequest {
|
||||||
pub speaker_diarization: Option<bool>,
|
pub speaker_diarization: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request to stop recording
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct StopRecordingRequest {
|
pub struct StopRecordingRequest {
|
||||||
pub recording_id: Uuid,
|
pub recording_id: Uuid,
|
||||||
pub start_transcription: Option<bool>,
|
pub start_transcription: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request to get transcription in specific format
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ExportTranscriptionRequest {
|
pub struct ExportTranscriptionRequest {
|
||||||
pub format: TranscriptionFormat,
|
pub format: TranscriptionFormat,
|
||||||
|
|
@ -242,7 +242,6 @@ pub struct ExportTranscriptionRequest {
|
||||||
pub max_line_length: Option<usize>,
|
pub max_line_length: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response for transcription export
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ExportTranscriptionResponse {
|
pub struct ExportTranscriptionResponse {
|
||||||
pub format: TranscriptionFormat,
|
pub format: TranscriptionFormat,
|
||||||
|
|
@ -251,7 +250,6 @@ pub struct ExportTranscriptionResponse {
|
||||||
pub filename: String,
|
pub filename: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recording service for managing webinar recordings and transcriptions
|
|
||||||
pub struct RecordingService {
|
pub struct RecordingService {
|
||||||
pool: DbPool,
|
pool: DbPool,
|
||||||
config: RecordingConfig,
|
config: RecordingConfig,
|
||||||
|
|
@ -272,12 +270,10 @@ impl RecordingService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Subscribe to recording events
|
|
||||||
pub fn subscribe(&self) -> broadcast::Receiver<RecordingEvent> {
|
pub fn subscribe(&self) -> broadcast::Receiver<RecordingEvent> {
|
||||||
self.event_sender.subscribe()
|
self.event_sender.subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start recording a webinar
|
|
||||||
pub async fn start_recording(
|
pub async fn start_recording(
|
||||||
&self,
|
&self,
|
||||||
request: StartRecordingRequest,
|
request: StartRecordingRequest,
|
||||||
|
|
@ -352,7 +348,6 @@ impl RecordingService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pause recording
|
|
||||||
pub async fn pause_recording(&self, recording_id: Uuid) -> Result<(), RecordingError> {
|
pub async fn pause_recording(&self, recording_id: Uuid) -> Result<(), RecordingError> {
|
||||||
let mut sessions = self.active_sessions.write().await;
|
let mut sessions = self.active_sessions.write().await;
|
||||||
let session = sessions
|
let session = sessions
|
||||||
|
|
@ -372,7 +367,6 @@ impl RecordingService {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resume recording
|
|
||||||
pub async fn resume_recording(&self, recording_id: Uuid) -> Result<(), RecordingError> {
|
pub async fn resume_recording(&self, recording_id: Uuid) -> Result<(), RecordingError> {
|
||||||
let mut sessions = self.active_sessions.write().await;
|
let mut sessions = self.active_sessions.write().await;
|
||||||
let session = sessions
|
let session = sessions
|
||||||
|
|
@ -390,7 +384,6 @@ impl RecordingService {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stop recording and optionally start transcription
|
|
||||||
pub async fn stop_recording(
|
pub async fn stop_recording(
|
||||||
&self,
|
&self,
|
||||||
request: StopRecordingRequest,
|
request: StopRecordingRequest,
|
||||||
|
|
@ -452,7 +445,6 @@ impl RecordingService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process recording (convert, compress, generate thumbnails)
|
|
||||||
async fn process_recording(&self, recording_id: Uuid) -> Result<(), RecordingError> {
|
async fn process_recording(&self, recording_id: Uuid) -> Result<(), RecordingError> {
|
||||||
let _ = self
|
let _ = self
|
||||||
.event_sender
|
.event_sender
|
||||||
|
|
@ -484,7 +476,6 @@ impl RecordingService {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start transcription for a recording
|
|
||||||
pub async fn start_transcription(
|
pub async fn start_transcription(
|
||||||
&self,
|
&self,
|
||||||
recording_id: Uuid,
|
recording_id: Uuid,
|
||||||
|
|
@ -560,7 +551,6 @@ impl RecordingService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the transcription process
|
|
||||||
async fn run_transcription(&self, transcription_id: Uuid, recording_id: Uuid) {
|
async fn run_transcription(&self, transcription_id: Uuid, recording_id: Uuid) {
|
||||||
// Update status to in progress
|
// Update status to in progress
|
||||||
{
|
{
|
||||||
|
|
@ -673,7 +663,6 @@ impl RecordingService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get recording by ID
|
|
||||||
pub async fn get_recording(&self, recording_id: Uuid) -> Result<WebinarRecording, RecordingError> {
|
pub async fn get_recording(&self, recording_id: Uuid) -> Result<WebinarRecording, RecordingError> {
|
||||||
// Check active sessions first
|
// Check active sessions first
|
||||||
let sessions = self.active_sessions.read().await;
|
let sessions = self.active_sessions.read().await;
|
||||||
|
|
@ -709,7 +698,6 @@ impl RecordingService {
|
||||||
self.get_recording_from_db(recording_id).await
|
self.get_recording_from_db(recording_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get transcription by ID
|
|
||||||
pub async fn get_transcription(
|
pub async fn get_transcription(
|
||||||
&self,
|
&self,
|
||||||
transcription_id: Uuid,
|
transcription_id: Uuid,
|
||||||
|
|
@ -742,7 +730,6 @@ impl RecordingService {
|
||||||
self.get_transcription_from_db(transcription_id).await
|
self.get_transcription_from_db(transcription_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Export transcription in specified format
|
|
||||||
pub async fn export_transcription(
|
pub async fn export_transcription(
|
||||||
&self,
|
&self,
|
||||||
transcription_id: Uuid,
|
transcription_id: Uuid,
|
||||||
|
|
@ -782,7 +769,6 @@ impl RecordingService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format transcription as plain text
|
|
||||||
fn format_as_plain_text(
|
fn format_as_plain_text(
|
||||||
&self,
|
&self,
|
||||||
transcription: &WebinarTranscription,
|
transcription: &WebinarTranscription,
|
||||||
|
|
@ -810,7 +796,6 @@ impl RecordingService {
|
||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format transcription as VTT (WebVTT)
|
|
||||||
fn format_as_vtt(
|
fn format_as_vtt(
|
||||||
&self,
|
&self,
|
||||||
transcription: &WebinarTranscription,
|
transcription: &WebinarTranscription,
|
||||||
|
|
@ -838,7 +823,6 @@ impl RecordingService {
|
||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format transcription as SRT
|
|
||||||
fn format_as_srt(
|
fn format_as_srt(
|
||||||
&self,
|
&self,
|
||||||
transcription: &WebinarTranscription,
|
transcription: &WebinarTranscription,
|
||||||
|
|
@ -866,7 +850,6 @@ impl RecordingService {
|
||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List recordings for a webinar
|
|
||||||
pub async fn list_recordings(
|
pub async fn list_recordings(
|
||||||
&self,
|
&self,
|
||||||
webinar_id: Uuid,
|
webinar_id: Uuid,
|
||||||
|
|
@ -874,7 +857,6 @@ impl RecordingService {
|
||||||
self.list_recordings_from_db(webinar_id).await
|
self.list_recordings_from_db(webinar_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete a recording
|
|
||||||
pub async fn delete_recording(&self, recording_id: Uuid) -> Result<(), RecordingError> {
|
pub async fn delete_recording(&self, recording_id: Uuid) -> Result<(), RecordingError> {
|
||||||
// Check if recording is active
|
// Check if recording is active
|
||||||
let sessions = self.active_sessions.read().await;
|
let sessions = self.active_sessions.read().await;
|
||||||
|
|
|
||||||
|
|
@ -1596,7 +1596,7 @@ async fn create_webinar_handler(
|
||||||
host_id: Uuid,
|
host_id: Uuid,
|
||||||
Json(request): Json<CreateWebinarRequest>,
|
Json(request): Json<CreateWebinarRequest>,
|
||||||
) -> Result<Json<Webinar>, WebinarError> {
|
) -> Result<Json<Webinar>, WebinarError> {
|
||||||
let service = WebinarService::new(state.db_pool.clone());
|
let service = WebinarService::new(state.conn.clone());
|
||||||
let webinar = service.create_webinar(organization_id, host_id, request).await?;
|
let webinar = service.create_webinar(organization_id, host_id, request).await?;
|
||||||
Ok(Json(webinar))
|
Ok(Json(webinar))
|
||||||
}
|
}
|
||||||
|
|
@ -1605,7 +1605,7 @@ async fn get_webinar_handler(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(webinar_id): Path<Uuid>,
|
Path(webinar_id): Path<Uuid>,
|
||||||
) -> Result<Json<Webinar>, WebinarError> {
|
) -> Result<Json<Webinar>, WebinarError> {
|
||||||
let service = WebinarService::new(state.db_pool.clone());
|
let service = WebinarService::new(state.conn.clone());
|
||||||
let webinar = service.get_webinar(webinar_id).await?;
|
let webinar = service.get_webinar(webinar_id).await?;
|
||||||
Ok(Json(webinar))
|
Ok(Json(webinar))
|
||||||
}
|
}
|
||||||
|
|
@ -1615,7 +1615,7 @@ async fn start_webinar_handler(
|
||||||
Path(webinar_id): Path<Uuid>,
|
Path(webinar_id): Path<Uuid>,
|
||||||
host_id: Uuid,
|
host_id: Uuid,
|
||||||
) -> Result<Json<Webinar>, WebinarError> {
|
) -> Result<Json<Webinar>, WebinarError> {
|
||||||
let service = WebinarService::new(state.db_pool.clone());
|
let service = WebinarService::new(state.conn.clone());
|
||||||
let webinar = service.start_webinar(webinar_id, host_id).await?;
|
let webinar = service.start_webinar(webinar_id, host_id).await?;
|
||||||
Ok(Json(webinar))
|
Ok(Json(webinar))
|
||||||
}
|
}
|
||||||
|
|
@ -1625,7 +1625,7 @@ async fn end_webinar_handler(
|
||||||
Path(webinar_id): Path<Uuid>,
|
Path(webinar_id): Path<Uuid>,
|
||||||
host_id: Uuid,
|
host_id: Uuid,
|
||||||
) -> Result<Json<Webinar>, WebinarError> {
|
) -> Result<Json<Webinar>, WebinarError> {
|
||||||
let service = WebinarService::new(state.db_pool.clone());
|
let service = WebinarService::new(state.conn.clone());
|
||||||
let webinar = service.end_webinar(webinar_id, host_id).await?;
|
let webinar = service.end_webinar(webinar_id, host_id).await?;
|
||||||
Ok(Json(webinar))
|
Ok(Json(webinar))
|
||||||
}
|
}
|
||||||
|
|
@ -1635,7 +1635,7 @@ async fn register_handler(
|
||||||
Path(webinar_id): Path<Uuid>,
|
Path(webinar_id): Path<Uuid>,
|
||||||
Json(request): Json<RegisterRequest>,
|
Json(request): Json<RegisterRequest>,
|
||||||
) -> Result<Json<WebinarRegistration>, WebinarError> {
|
) -> Result<Json<WebinarRegistration>, WebinarError> {
|
||||||
let service = WebinarService::new(state.db_pool.clone());
|
let service = WebinarService::new(state.conn.clone());
|
||||||
let registration = service.register_attendee(webinar_id, request).await?;
|
let registration = service.register_attendee(webinar_id, request).await?;
|
||||||
Ok(Json(registration))
|
Ok(Json(registration))
|
||||||
}
|
}
|
||||||
|
|
@ -1645,7 +1645,7 @@ async fn join_handler(
|
||||||
Path(webinar_id): Path<Uuid>,
|
Path(webinar_id): Path<Uuid>,
|
||||||
participant_id: Uuid,
|
participant_id: Uuid,
|
||||||
) -> Result<Json<WebinarParticipant>, WebinarError> {
|
) -> Result<Json<WebinarParticipant>, WebinarError> {
|
||||||
let service = WebinarService::new(state.db_pool.clone());
|
let service = WebinarService::new(state.conn.clone());
|
||||||
let participant = service.join_webinar(webinar_id, participant_id).await?;
|
let participant = service.join_webinar(webinar_id, participant_id).await?;
|
||||||
Ok(Json(participant))
|
Ok(Json(participant))
|
||||||
}
|
}
|
||||||
|
|
@ -1655,7 +1655,7 @@ async fn raise_hand_handler(
|
||||||
Path(webinar_id): Path<Uuid>,
|
Path(webinar_id): Path<Uuid>,
|
||||||
participant_id: Uuid,
|
participant_id: Uuid,
|
||||||
) -> Result<StatusCode, WebinarError> {
|
) -> Result<StatusCode, WebinarError> {
|
||||||
let service = WebinarService::new(state.db_pool.clone());
|
let service = WebinarService::new(state.conn.clone());
|
||||||
service.raise_hand(webinar_id, participant_id).await?;
|
service.raise_hand(webinar_id, participant_id).await?;
|
||||||
Ok(StatusCode::OK)
|
Ok(StatusCode::OK)
|
||||||
}
|
}
|
||||||
|
|
@ -1665,7 +1665,7 @@ async fn lower_hand_handler(
|
||||||
Path(webinar_id): Path<Uuid>,
|
Path(webinar_id): Path<Uuid>,
|
||||||
participant_id: Uuid,
|
participant_id: Uuid,
|
||||||
) -> Result<StatusCode, WebinarError> {
|
) -> Result<StatusCode, WebinarError> {
|
||||||
let service = WebinarService::new(state.db_pool.clone());
|
let service = WebinarService::new(state.conn.clone());
|
||||||
service.lower_hand(webinar_id, participant_id).await?;
|
service.lower_hand(webinar_id, participant_id).await?;
|
||||||
Ok(StatusCode::OK)
|
Ok(StatusCode::OK)
|
||||||
}
|
}
|
||||||
|
|
@ -1674,7 +1674,7 @@ async fn get_raised_hands_handler(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(webinar_id): Path<Uuid>,
|
Path(webinar_id): Path<Uuid>,
|
||||||
) -> Result<Json<Vec<WebinarParticipant>>, WebinarError> {
|
) -> Result<Json<Vec<WebinarParticipant>>, WebinarError> {
|
||||||
let service = WebinarService::new(state.db_pool.clone());
|
let service = WebinarService::new(state.conn.clone());
|
||||||
let hands = service.get_raised_hands(webinar_id).await?;
|
let hands = service.get_raised_hands(webinar_id).await?;
|
||||||
Ok(Json(hands))
|
Ok(Json(hands))
|
||||||
}
|
}
|
||||||
|
|
@ -1683,7 +1683,7 @@ async fn get_questions_handler(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(webinar_id): Path<Uuid>,
|
Path(webinar_id): Path<Uuid>,
|
||||||
) -> Result<Json<Vec<QAQuestion>>, WebinarError> {
|
) -> Result<Json<Vec<QAQuestion>>, WebinarError> {
|
||||||
let service = WebinarService::new(state.db_pool.clone());
|
let service = WebinarService::new(state.conn.clone());
|
||||||
let questions = service.get_questions(webinar_id, false).await?;
|
let questions = service.get_questions(webinar_id, false).await?;
|
||||||
Ok(Json(questions))
|
Ok(Json(questions))
|
||||||
}
|
}
|
||||||
|
|
@ -1694,7 +1694,7 @@ async fn submit_question_handler(
|
||||||
asker_id: Option<Uuid>,
|
asker_id: Option<Uuid>,
|
||||||
Json(request): Json<SubmitQuestionRequest>,
|
Json(request): Json<SubmitQuestionRequest>,
|
||||||
) -> Result<Json<QAQuestion>, WebinarError> {
|
) -> Result<Json<QAQuestion>, WebinarError> {
|
||||||
let service = WebinarService::new(state.db_pool.clone());
|
let service = WebinarService::new(state.conn.clone());
|
||||||
let question = service.submit_question(webinar_id, asker_id, "Anonymous".to_string(), request).await?;
|
let question = service.submit_question(webinar_id, asker_id, "Anonymous".to_string(), request).await?;
|
||||||
Ok(Json(question))
|
Ok(Json(question))
|
||||||
}
|
}
|
||||||
|
|
@ -1705,7 +1705,7 @@ async fn answer_question_handler(
|
||||||
answerer_id: Uuid,
|
answerer_id: Uuid,
|
||||||
Json(request): Json<AnswerQuestionRequest>,
|
Json(request): Json<AnswerQuestionRequest>,
|
||||||
) -> Result<Json<QAQuestion>, WebinarError> {
|
) -> Result<Json<QAQuestion>, WebinarError> {
|
||||||
let service = WebinarService::new(state.db_pool.clone());
|
let service = WebinarService::new(state.conn.clone());
|
||||||
let question = service.answer_question(question_id, answerer_id, request).await?;
|
let question = service.answer_question(question_id, answerer_id, request).await?;
|
||||||
Ok(Json(question))
|
Ok(Json(question))
|
||||||
}
|
}
|
||||||
|
|
@ -1715,7 +1715,7 @@ async fn upvote_question_handler(
|
||||||
Path((webinar_id, question_id)): Path<(Uuid, Uuid)>,
|
Path((webinar_id, question_id)): Path<(Uuid, Uuid)>,
|
||||||
voter_id: Uuid,
|
voter_id: Uuid,
|
||||||
) -> Result<Json<QAQuestion>, WebinarError> {
|
) -> Result<Json<QAQuestion>, WebinarError> {
|
||||||
let service = WebinarService::new(state.db_pool.clone());
|
let service = WebinarService::new(state.conn.clone());
|
||||||
let question = service.upvote_question(question_id, voter_id).await?;
|
let question = service.upvote_question(question_id, voter_id).await?;
|
||||||
Ok(Json(question))
|
Ok(Json(question))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ use std::sync::Arc;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::AppState;
|
use crate::shared::state::AppState;
|
||||||
|
|
||||||
pub mod import;
|
pub mod import;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,19 @@ use std::sync::{Arc, RwLock};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::shared::state::AppState;
|
use crate::shared::state::AppState;
|
||||||
|
use crate::shared::utils::DbPool;
|
||||||
|
|
||||||
const CHALLENGE_TIMEOUT_SECONDS: i64 = 300;
|
const CHALLENGE_TIMEOUT_SECONDS: i64 = 300;
|
||||||
const PASSKEY_NAME_MAX_LENGTH: usize = 64;
|
const PASSKEY_NAME_MAX_LENGTH: usize = 64;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct FallbackAttemptTracker {
|
||||||
|
attempts: u32,
|
||||||
|
first_attempt_at: DateTime<Utc>,
|
||||||
|
locked_until: Option<DateTime<Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct PasskeyCredential {
|
pub struct PasskeyCredential {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub user_id: Uuid,
|
pub user_id: Uuid,
|
||||||
|
|
@ -195,14 +203,12 @@ pub struct RegistrationResult {
|
||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request for password fallback authentication when passkey is unavailable
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct PasswordFallbackRequest {
|
pub struct PasswordFallbackRequest {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response for password fallback authentication
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct PasswordFallbackResponse {
|
pub struct PasswordFallbackResponse {
|
||||||
pub success: bool,
|
pub success: bool,
|
||||||
|
|
@ -212,18 +218,12 @@ pub struct PasswordFallbackResponse {
|
||||||
pub passkey_available: bool,
|
pub passkey_available: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configuration for fallback authentication behavior
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct FallbackConfig {
|
pub struct FallbackConfig {
|
||||||
/// Whether password fallback is enabled
|
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
/// Require additional verification after password fallback
|
|
||||||
pub require_additional_verification: bool,
|
pub require_additional_verification: bool,
|
||||||
/// Maximum password fallback attempts before lockout
|
|
||||||
pub max_fallback_attempts: u32,
|
pub max_fallback_attempts: u32,
|
||||||
/// Lockout duration in seconds after max attempts
|
|
||||||
pub lockout_duration_seconds: u64,
|
pub lockout_duration_seconds: u64,
|
||||||
/// Prompt user to set up passkey after password login
|
|
||||||
pub prompt_passkey_setup: bool,
|
pub prompt_passkey_setup: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -291,7 +291,6 @@ impl PasskeyService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new PasskeyService with custom fallback configuration
|
|
||||||
pub fn with_fallback_config(
|
pub fn with_fallback_config(
|
||||||
pool: DbPool,
|
pool: DbPool,
|
||||||
rp_id: String,
|
rp_id: String,
|
||||||
|
|
@ -311,17 +310,11 @@ impl PasskeyService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if user has any registered passkeys
|
|
||||||
pub async fn user_has_passkeys(&self, username: &str) -> Result<bool, PasskeyError> {
|
pub async fn user_has_passkeys(&self, username: &str) -> Result<bool, PasskeyError> {
|
||||||
let passkeys = self.get_passkeys_by_username(username).await?;
|
let passkeys = self.get_passkeys_by_username(username).await?;
|
||||||
Ok(!passkeys.is_empty())
|
Ok(!passkeys.is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Authenticate using password fallback when passkey is unavailable
|
|
||||||
/// This is used when:
|
|
||||||
/// 1. User's device doesn't support passkeys
|
|
||||||
/// 2. User hasn't set up passkeys yet
|
|
||||||
/// 3. Passkey authentication failed and fallback is enabled
|
|
||||||
pub async fn authenticate_with_password_fallback(
|
pub async fn authenticate_with_password_fallback(
|
||||||
&self,
|
&self,
|
||||||
request: &PasswordFallbackRequest,
|
request: &PasswordFallbackRequest,
|
||||||
|
|
@ -384,7 +377,6 @@ impl PasskeyService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if user is locked out due to too many failed attempts
|
|
||||||
async fn is_user_locked_out(&self, username: &str) -> bool {
|
async fn is_user_locked_out(&self, username: &str) -> bool {
|
||||||
let attempts = self.fallback_attempts.read().await;
|
let attempts = self.fallback_attempts.read().await;
|
||||||
if let Some(tracker) = attempts.get(username) {
|
if let Some(tracker) = attempts.get(username) {
|
||||||
|
|
@ -395,7 +387,6 @@ impl PasskeyService {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Track a failed fallback attempt
|
|
||||||
async fn track_fallback_attempt(&self, username: &str) {
|
async fn track_fallback_attempt(&self, username: &str) {
|
||||||
let mut attempts = self.fallback_attempts.write().await;
|
let mut attempts = self.fallback_attempts.write().await;
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
|
|
@ -416,32 +407,25 @@ impl PasskeyService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear fallback attempts after successful login
|
|
||||||
async fn clear_fallback_attempts(&self, username: &str) {
|
async fn clear_fallback_attempts(&self, username: &str) {
|
||||||
let mut attempts = self.fallback_attempts.write().await;
|
let mut attempts = self.fallback_attempts.write().await;
|
||||||
attempts.remove(username);
|
attempts.remove(username);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verify password against database
|
|
||||||
async fn verify_password(&self, username: &str, password: &str) -> Result<Uuid, PasskeyError> {
|
async fn verify_password(&self, username: &str, password: &str) -> Result<Uuid, PasskeyError> {
|
||||||
let mut conn = self.pool.get().map_err(|_| PasskeyError::DatabaseError)?;
|
let mut conn = self.pool.get().map_err(|_| PasskeyError::DatabaseError)?;
|
||||||
|
|
||||||
// Query user by username
|
let result: Option<(Uuid, Option<String>)> = diesel::sql_query(
|
||||||
let result = sqlx::query!(
|
"SELECT id, password_hash FROM users WHERE username = $1 OR email = $1"
|
||||||
r#"
|
|
||||||
SELECT id, password_hash
|
|
||||||
FROM users
|
|
||||||
WHERE username = $1 OR email = $1
|
|
||||||
"#,
|
|
||||||
username
|
|
||||||
)
|
)
|
||||||
.fetch_optional(&mut *conn)
|
.bind::<Text, _>(username)
|
||||||
.await;
|
.get_result::<(Uuid, Option<String>)>(&mut conn)
|
||||||
|
.optional()
|
||||||
|
.map_err(|_| PasskeyError::DatabaseError)?;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(Some(user)) => {
|
Some((user_id, password_hash)) => {
|
||||||
// Verify password hash using argon2
|
if let Some(hash) = password_hash {
|
||||||
if let Some(hash) = user.password_hash {
|
|
||||||
let parsed_hash = argon2::PasswordHash::new(&hash)
|
let parsed_hash = argon2::PasswordHash::new(&hash)
|
||||||
.map_err(|_| PasskeyError::InvalidCredentialId)?;
|
.map_err(|_| PasskeyError::InvalidCredentialId)?;
|
||||||
|
|
||||||
|
|
@ -449,17 +433,15 @@ impl PasskeyService {
|
||||||
.verify_password(password.as_bytes(), &parsed_hash)
|
.verify_password(password.as_bytes(), &parsed_hash)
|
||||||
.is_ok()
|
.is_ok()
|
||||||
{
|
{
|
||||||
return Ok(user.id);
|
return Ok(user_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(PasskeyError::InvalidCredentialId)
|
Err(PasskeyError::InvalidCredentialId)
|
||||||
}
|
}
|
||||||
Ok(None) => Err(PasskeyError::InvalidCredentialId),
|
None => Err(PasskeyError::InvalidCredentialId),
|
||||||
Err(_) => Err(PasskeyError::DatabaseError),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a session token for authenticated user
|
|
||||||
fn generate_session_token(&self, user_id: &Uuid) -> String {
|
fn generate_session_token(&self, user_id: &Uuid) -> String {
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
|
|
@ -471,7 +453,6 @@ impl PasskeyService {
|
||||||
format!("{}:{}", user_id, token)
|
format!("{}:{}", user_id, token)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if password fallback should be offered based on passkey availability
|
|
||||||
pub async fn should_offer_password_fallback(&self, username: &str) -> Result<bool, PasskeyError> {
|
pub async fn should_offer_password_fallback(&self, username: &str) -> Result<bool, PasskeyError> {
|
||||||
if !self.fallback_config.enabled {
|
if !self.fallback_config.enabled {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
|
|
@ -482,12 +463,10 @@ impl PasskeyService {
|
||||||
Ok(!has_passkeys || self.fallback_config.enabled)
|
Ok(!has_passkeys || self.fallback_config.enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get fallback configuration
|
|
||||||
pub fn get_fallback_config(&self) -> &FallbackConfig {
|
pub fn get_fallback_config(&self) -> &FallbackConfig {
|
||||||
&self.fallback_config
|
&self.fallback_config
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update fallback configuration
|
|
||||||
pub fn set_fallback_config(&mut self, config: FallbackConfig) {
|
pub fn set_fallback_config(&mut self, config: FallbackConfig) {
|
||||||
self.fallback_config = config;
|
self.fallback_config = config;
|
||||||
}
|
}
|
||||||
|
|
@ -1303,7 +1282,6 @@ pub fn passkey_routes(_state: Arc<AppState>) -> Router<Arc<AppState>> {
|
||||||
.route("/fallback/config", get(get_fallback_config_handler))
|
.route("/fallback/config", get(get_fallback_config_handler))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handler for password fallback authentication
|
|
||||||
async fn password_fallback_handler(
|
async fn password_fallback_handler(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Json(request): Json<PasswordFallbackRequest>,
|
Json(request): Json<PasswordFallbackRequest>,
|
||||||
|
|
@ -1315,7 +1293,6 @@ pub fn passkey_routes(_state: Arc<AppState>) -> Router<Arc<AppState>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handler to check if password fallback is available for a user
|
|
||||||
async fn check_fallback_available_handler(
|
async fn check_fallback_available_handler(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(username): Path<String>,
|
Path(username): Path<String>,
|
||||||
|
|
@ -1346,7 +1323,6 @@ pub fn passkey_routes(_state: Arc<AppState>) -> Router<Arc<AppState>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handler to get fallback configuration (public settings only)
|
|
||||||
async fn get_fallback_config_handler(
|
async fn get_fallback_config_handler(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
|
|
@ -1438,7 +1414,7 @@ struct RegistrationVerifyRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_passkey_service(state: &AppState) -> Result<PasskeyService, PasskeyError> {
|
fn get_passkey_service(state: &AppState) -> Result<PasskeyService, PasskeyError> {
|
||||||
let pool = state.db_pool.clone();
|
let pool = state.conn.clone();
|
||||||
let rp_id = std::env::var("PASSKEY_RP_ID").unwrap_or_else(|_| "localhost".to_string());
|
let rp_id = std::env::var("PASSKEY_RP_ID").unwrap_or_else(|_| "localhost".to_string());
|
||||||
let rp_name = std::env::var("PASSKEY_RP_NAME").unwrap_or_else(|_| "General Bots".to_string());
|
let rp_name = std::env::var("PASSKEY_RP_NAME").unwrap_or_else(|_| "General Bots".to_string());
|
||||||
let rp_origin = std::env::var("PASSKEY_RP_ORIGIN").unwrap_or_else(|_| "http://localhost:8081".to_string());
|
let rp_origin = std::env::var("PASSKEY_RP_ORIGIN").unwrap_or_else(|_| "http://localhost:8081".to_string());
|
||||||
|
|
|
||||||
|
|
@ -1111,39 +1111,35 @@ fn render_post_card_html(post: &PostWithAuthor) -> String {
|
||||||
.post
|
.post
|
||||||
.reaction_counts
|
.reaction_counts
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(emoji, count)| format!(r#"<span class="reaction">{emoji} {count}</span>"#))
|
.map(|(emoji, count)| format!("<span class=\"reaction\">{emoji} {count}</span>"))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
let avatar_url = post.author.avatar_url.as_deref().unwrap_or("/assets/default-avatar.svg");
|
||||||
|
let post_time = post.post.created_at.format("%b %d, %Y");
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
concat!(
|
"<article class=\"post-card\" data-post-id=\"{id}\">\
|
||||||
r#"<article class="post-card" data-post-id="{}">"#,
|
<header class=\"post-header\">\
|
||||||
r#"<header class="post-header">"#,
|
<img class=\"avatar\" src=\"{avatar}\" alt=\"{name}\" />\
|
||||||
r#"<img class="avatar" src="{}" alt="{}" />"#,
|
<div class=\"post-meta\"><span class=\"author-name\">{name}</span><span class=\"post-time\">{time}</span></div>\
|
||||||
r#"<div class="post-meta"><span class="author-name">{}</span><span class="post-time">{}</span></div>"#,
|
</header>\
|
||||||
r#"</header>"#,
|
<div class=\"post-content\">{content}</div>\
|
||||||
r#"<div class="post-content">{}</div>"#,
|
<footer class=\"post-footer\">\
|
||||||
r#"<footer class="post-footer">"#,
|
<div class=\"reactions\">{reactions}</div>\
|
||||||
r#"<div class="reactions">{}</div>"#,
|
<div class=\"post-actions\">\
|
||||||
r#"<div class="post-actions">"#,
|
<button class=\"btn-react\" hx-post=\"/api/social/posts/{id}/react\" hx-swap=\"outerHTML\">Like</button>\
|
||||||
r#"<button class="btn-react" hx-post="/api/social/posts/{}/react" hx-swap="outerHTML">👍</button>"#,
|
<button class=\"btn-comment\" hx-get=\"/api/social/posts/{id}/comments\" hx-target=\"#comments-{id}\">Comment {comments}</button>\
|
||||||
r#"<button class="btn-comment" hx-get="/api/social/posts/{}/comments" hx-target="#comments-{}">💬 {}</button>"#,
|
</div>\
|
||||||
r#"</div>"#,
|
</footer>\
|
||||||
r#"</footer>"#,
|
<div id=\"comments-{id}\" class=\"comments-section\"></div>\
|
||||||
r##"<div id="comments-{}" class="comments-section"></div>"##,
|
</article>",
|
||||||
r#"</article>"#
|
id = post.post.id,
|
||||||
),
|
avatar = avatar_url,
|
||||||
post.post.id,
|
name = post.author.name,
|
||||||
post.author.avatar_url.as_deref().unwrap_or("/assets/default-avatar.svg"),
|
time = post_time,
|
||||||
post.author.name,
|
content = post.post.content,
|
||||||
post.author.name,
|
reactions = reactions_html,
|
||||||
post.post.created_at.format("%b %d, %Y"),
|
comments = post.post.comment_count,
|
||||||
post.post.content,
|
|
||||||
reactions_html,
|
|
||||||
post.post.id,
|
|
||||||
post.post.id,
|
|
||||||
post.post.id,
|
|
||||||
post.post.comment_count,
|
|
||||||
post.post.id
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,7 @@ impl VectorDBIndexer {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_active_users(&self) -> Result<Vec<(Uuid, Uuid)>> {
|
async fn get_active_users(&self) -> Result<Vec<(Uuid, Uuid)>> {
|
||||||
let conn = self.db_pool.clone();
|
let conn = self.conn.clone();
|
||||||
|
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
use crate::shared::models::schema::user_sessions::dsl::*;
|
use crate::shared::models::schema::user_sessions::dsl::*;
|
||||||
|
|
@ -395,7 +395,7 @@ impl VectorDBIndexer {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_user_email_accounts(&self, user_id: Uuid) -> Result<Vec<String>> {
|
async fn get_user_email_accounts(&self, user_id: Uuid) -> Result<Vec<String>> {
|
||||||
let conn = self.db_pool.clone();
|
let conn = self.conn.clone();
|
||||||
|
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
@ -427,7 +427,7 @@ impl VectorDBIndexer {
|
||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
account_id: &str,
|
account_id: &str,
|
||||||
) -> Result<Vec<EmailDocument>, Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<Vec<EmailDocument>, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let pool = self.db_pool.clone();
|
let pool = self.conn.clone();
|
||||||
let account_id = account_id.to_string();
|
let account_id = account_id.to_string();
|
||||||
|
|
||||||
let results = tokio::task::spawn_blocking(move || {
|
let results = tokio::task::spawn_blocking(move || {
|
||||||
|
|
@ -504,7 +504,7 @@ impl VectorDBIndexer {
|
||||||
&self,
|
&self,
|
||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<FileDocument>, Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<Vec<FileDocument>, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let pool = self.db_pool.clone();
|
let pool = self.conn.clone();
|
||||||
|
|
||||||
let results = tokio::task::spawn_blocking(move || {
|
let results = tokio::task::spawn_blocking(move || {
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,16 @@
|
||||||
|
use axum::{
|
||||||
|
extract::{Path, State},
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Json},
|
||||||
|
};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::security::error_sanitizer::SafeErrorResponse;
|
||||||
|
use crate::shared::state::AppState;
|
||||||
use crate::shared::utils::DbPool;
|
use crate::shared::utils::DbPool;
|
||||||
|
|
||||||
use super::models::*;
|
use super::models::*;
|
||||||
|
|
@ -296,20 +303,11 @@ fn parse_devices(json: &Option<serde_json::Value>) -> DeviceBreakdown {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use axum::{
|
|
||||||
extract::{Path, State},
|
|
||||||
http::StatusCode,
|
|
||||||
response::{IntoResponse, Json},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::security::error_sanitizer::SafeErrorResponse;
|
|
||||||
use crate::shared::state::AppState;
|
|
||||||
|
|
||||||
pub async fn get_analytics_handler(
|
pub async fn get_analytics_handler(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(project_id): Path<Uuid>,
|
Path(project_id): Path<Uuid>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = AnalyticsEngine::new(state.db.clone());
|
let engine = AnalyticsEngine::new(state.conn.clone());
|
||||||
|
|
||||||
let _ = engine.get_or_create_analytics(project_id, None).await;
|
let _ = engine.get_or_create_analytics(project_id, None).await;
|
||||||
|
|
||||||
|
|
@ -329,7 +327,7 @@ pub async fn record_view_handler(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Json(req): Json<RecordViewRequest>,
|
Json(req): Json<RecordViewRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = AnalyticsEngine::new(state.db.clone());
|
let engine = AnalyticsEngine::new(state.conn.clone());
|
||||||
|
|
||||||
match engine.record_view(req).await {
|
match engine.record_view(req).await {
|
||||||
Ok(_) => (
|
Ok(_) => (
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ pub async fn list_projects(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Query(filters): Query<ProjectFilters>,
|
Query(filters): Query<ProjectFilters>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
match engine.list_projects(None, filters).await {
|
match engine.list_projects(None, filters).await {
|
||||||
Ok(projects) => (
|
Ok(projects) => (
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
|
|
@ -37,7 +37,7 @@ pub async fn create_project(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Json(req): Json<CreateProjectRequest>,
|
Json(req): Json<CreateProjectRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
match engine.create_project(None, None, req).await {
|
match engine.create_project(None, None, req).await {
|
||||||
Ok(project) => (
|
Ok(project) => (
|
||||||
StatusCode::CREATED,
|
StatusCode::CREATED,
|
||||||
|
|
@ -57,7 +57,7 @@ pub async fn get_project(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(id): Path<Uuid>,
|
Path(id): Path<Uuid>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
match engine.get_project_detail(id).await {
|
match engine.get_project_detail(id).await {
|
||||||
Ok(detail) => (StatusCode::OK, Json(serde_json::json!(detail))),
|
Ok(detail) => (StatusCode::OK, Json(serde_json::json!(detail))),
|
||||||
Err(diesel::result::Error::NotFound) => (
|
Err(diesel::result::Error::NotFound) => (
|
||||||
|
|
@ -79,7 +79,7 @@ pub async fn update_project(
|
||||||
Path(id): Path<Uuid>,
|
Path(id): Path<Uuid>,
|
||||||
Json(req): Json<UpdateProjectRequest>,
|
Json(req): Json<UpdateProjectRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
match engine.update_project(id, req).await {
|
match engine.update_project(id, req).await {
|
||||||
Ok(project) => (
|
Ok(project) => (
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
|
|
@ -103,7 +103,7 @@ pub async fn delete_project(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(id): Path<Uuid>,
|
Path(id): Path<Uuid>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
match engine.delete_project(id).await {
|
match engine.delete_project(id).await {
|
||||||
Ok(()) => (StatusCode::NO_CONTENT, Json(serde_json::json!({}))),
|
Ok(()) => (StatusCode::NO_CONTENT, Json(serde_json::json!({}))),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -120,7 +120,7 @@ pub async fn get_clips(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(project_id): Path<Uuid>,
|
Path(project_id): Path<Uuid>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
match engine.get_clips(project_id).await {
|
match engine.get_clips(project_id).await {
|
||||||
Ok(clips) => (StatusCode::OK, Json(serde_json::json!({ "clips": clips }))),
|
Ok(clips) => (StatusCode::OK, Json(serde_json::json!({ "clips": clips }))),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -138,7 +138,7 @@ pub async fn add_clip(
|
||||||
Path(project_id): Path<Uuid>,
|
Path(project_id): Path<Uuid>,
|
||||||
Json(req): Json<AddClipRequest>,
|
Json(req): Json<AddClipRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
match engine.add_clip(project_id, req).await {
|
match engine.add_clip(project_id, req).await {
|
||||||
Ok(clip) => (StatusCode::CREATED, Json(serde_json::json!({ "clip": clip }))),
|
Ok(clip) => (StatusCode::CREATED, Json(serde_json::json!({ "clip": clip }))),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -156,7 +156,7 @@ pub async fn update_clip(
|
||||||
Path(clip_id): Path<Uuid>,
|
Path(clip_id): Path<Uuid>,
|
||||||
Json(req): Json<UpdateClipRequest>,
|
Json(req): Json<UpdateClipRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
match engine.update_clip(clip_id, req).await {
|
match engine.update_clip(clip_id, req).await {
|
||||||
Ok(clip) => (StatusCode::OK, Json(serde_json::json!({ "clip": clip }))),
|
Ok(clip) => (StatusCode::OK, Json(serde_json::json!({ "clip": clip }))),
|
||||||
Err(diesel::result::Error::NotFound) => (
|
Err(diesel::result::Error::NotFound) => (
|
||||||
|
|
@ -177,7 +177,7 @@ pub async fn delete_clip(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(clip_id): Path<Uuid>,
|
Path(clip_id): Path<Uuid>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
match engine.delete_clip(clip_id).await {
|
match engine.delete_clip(clip_id).await {
|
||||||
Ok(()) => (StatusCode::NO_CONTENT, Json(serde_json::json!({}))),
|
Ok(()) => (StatusCode::NO_CONTENT, Json(serde_json::json!({}))),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -195,7 +195,7 @@ pub async fn split_clip_handler(
|
||||||
Path(clip_id): Path<Uuid>,
|
Path(clip_id): Path<Uuid>,
|
||||||
Json(req): Json<SplitClipRequest>,
|
Json(req): Json<SplitClipRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
match engine.split_clip(clip_id, req.at_ms).await {
|
match engine.split_clip(clip_id, req.at_ms).await {
|
||||||
Ok((first, second)) => (
|
Ok((first, second)) => (
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
|
|
@ -222,7 +222,7 @@ pub async fn get_layers(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(project_id): Path<Uuid>,
|
Path(project_id): Path<Uuid>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
match engine.get_layers(project_id).await {
|
match engine.get_layers(project_id).await {
|
||||||
Ok(layers) => (StatusCode::OK, Json(serde_json::json!({ "layers": layers }))),
|
Ok(layers) => (StatusCode::OK, Json(serde_json::json!({ "layers": layers }))),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -240,7 +240,7 @@ pub async fn add_layer(
|
||||||
Path(project_id): Path<Uuid>,
|
Path(project_id): Path<Uuid>,
|
||||||
Json(req): Json<AddLayerRequest>,
|
Json(req): Json<AddLayerRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
match engine.add_layer(project_id, req).await {
|
match engine.add_layer(project_id, req).await {
|
||||||
Ok(layer) => (
|
Ok(layer) => (
|
||||||
StatusCode::CREATED,
|
StatusCode::CREATED,
|
||||||
|
|
@ -261,7 +261,7 @@ pub async fn update_layer(
|
||||||
Path(layer_id): Path<Uuid>,
|
Path(layer_id): Path<Uuid>,
|
||||||
Json(req): Json<UpdateLayerRequest>,
|
Json(req): Json<UpdateLayerRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
match engine.update_layer(layer_id, req).await {
|
match engine.update_layer(layer_id, req).await {
|
||||||
Ok(layer) => (StatusCode::OK, Json(serde_json::json!({ "layer": layer }))),
|
Ok(layer) => (StatusCode::OK, Json(serde_json::json!({ "layer": layer }))),
|
||||||
Err(diesel::result::Error::NotFound) => (
|
Err(diesel::result::Error::NotFound) => (
|
||||||
|
|
@ -282,7 +282,7 @@ pub async fn delete_layer(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(layer_id): Path<Uuid>,
|
Path(layer_id): Path<Uuid>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
match engine.delete_layer(layer_id).await {
|
match engine.delete_layer(layer_id).await {
|
||||||
Ok(()) => (StatusCode::NO_CONTENT, Json(serde_json::json!({}))),
|
Ok(()) => (StatusCode::NO_CONTENT, Json(serde_json::json!({}))),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -299,7 +299,7 @@ pub async fn get_audio_tracks(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(project_id): Path<Uuid>,
|
Path(project_id): Path<Uuid>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
match engine.get_audio_tracks(project_id).await {
|
match engine.get_audio_tracks(project_id).await {
|
||||||
Ok(tracks) => (
|
Ok(tracks) => (
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
|
|
@ -320,7 +320,7 @@ pub async fn add_audio_track(
|
||||||
Path(project_id): Path<Uuid>,
|
Path(project_id): Path<Uuid>,
|
||||||
Json(req): Json<AddAudioRequest>,
|
Json(req): Json<AddAudioRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
match engine.add_audio_track(project_id, req).await {
|
match engine.add_audio_track(project_id, req).await {
|
||||||
Ok(track) => (
|
Ok(track) => (
|
||||||
StatusCode::CREATED,
|
StatusCode::CREATED,
|
||||||
|
|
@ -340,7 +340,7 @@ pub async fn delete_audio_track(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(track_id): Path<Uuid>,
|
Path(track_id): Path<Uuid>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
match engine.delete_audio_track(track_id).await {
|
match engine.delete_audio_track(track_id).await {
|
||||||
Ok(()) => (StatusCode::NO_CONTENT, Json(serde_json::json!({}))),
|
Ok(()) => (StatusCode::NO_CONTENT, Json(serde_json::json!({}))),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -357,7 +357,7 @@ pub async fn get_keyframes(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(layer_id): Path<Uuid>,
|
Path(layer_id): Path<Uuid>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
match engine.get_keyframes(layer_id).await {
|
match engine.get_keyframes(layer_id).await {
|
||||||
Ok(keyframes) => (
|
Ok(keyframes) => (
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
|
|
@ -378,7 +378,7 @@ pub async fn add_keyframe(
|
||||||
Path(layer_id): Path<Uuid>,
|
Path(layer_id): Path<Uuid>,
|
||||||
Json(req): Json<AddKeyframeRequest>,
|
Json(req): Json<AddKeyframeRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
match engine.add_keyframe(layer_id, req).await {
|
match engine.add_keyframe(layer_id, req).await {
|
||||||
Ok(keyframe) => (
|
Ok(keyframe) => (
|
||||||
StatusCode::CREATED,
|
StatusCode::CREATED,
|
||||||
|
|
@ -398,7 +398,7 @@ pub async fn delete_keyframe(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(keyframe_id): Path<Uuid>,
|
Path(keyframe_id): Path<Uuid>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
match engine.delete_keyframe(keyframe_id).await {
|
match engine.delete_keyframe(keyframe_id).await {
|
||||||
Ok(()) => (StatusCode::NO_CONTENT, Json(serde_json::json!({}))),
|
Ok(()) => (StatusCode::NO_CONTENT, Json(serde_json::json!({}))),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -423,7 +423,7 @@ pub async fn upload_media(
|
||||||
error!("Failed to create upload directory: {e}");
|
error!("Failed to create upload directory: {e}");
|
||||||
return (
|
return (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json(serde_json::json!({ "error": sanitize_error("upload_media") })),
|
Json(serde_json::json!({ "error": SafeErrorResponse::internal_error() })),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -457,7 +457,7 @@ pub async fn upload_media(
|
||||||
error!("Failed to write uploaded file: {e}");
|
error!("Failed to write uploaded file: {e}");
|
||||||
return (
|
return (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json(serde_json::json!({ "error": sanitize_error("upload_media") })),
|
Json(serde_json::json!({ "error": SafeErrorResponse::internal_error() })),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -501,7 +501,7 @@ pub async fn get_preview_frame(
|
||||||
Path(project_id): Path<Uuid>,
|
Path(project_id): Path<Uuid>,
|
||||||
Query(params): Query<PreviewFrameRequest>,
|
Query(params): Query<PreviewFrameRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
let at_ms = params.at_ms.unwrap_or(0);
|
let at_ms = params.at_ms.unwrap_or(0);
|
||||||
let width = params.width.unwrap_or(640);
|
let width = params.width.unwrap_or(640);
|
||||||
let height = params.height.unwrap_or(360);
|
let height = params.height.unwrap_or(360);
|
||||||
|
|
@ -513,7 +513,7 @@ pub async fn get_preview_frame(
|
||||||
error!("Failed to create preview directory: {e}");
|
error!("Failed to create preview directory: {e}");
|
||||||
return (
|
return (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json(serde_json::json!({ "error": sanitize_error("get_preview_frame") })),
|
Json(serde_json::json!({ "error": SafeErrorResponse::internal_error() })),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -529,7 +529,7 @@ pub async fn get_preview_frame(
|
||||||
error!("Failed to generate preview: {e}");
|
error!("Failed to generate preview: {e}");
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json(serde_json::json!({ "error": sanitize_error("get_preview_frame") })),
|
Json(serde_json::json!({ "error": SafeErrorResponse::internal_error() })),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -540,7 +540,7 @@ pub async fn transcribe_handler(
|
||||||
Path(project_id): Path<Uuid>,
|
Path(project_id): Path<Uuid>,
|
||||||
Json(req): Json<TranscribeRequest>,
|
Json(req): Json<TranscribeRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
match engine
|
match engine
|
||||||
.transcribe_audio(project_id, req.clip_id, req.language)
|
.transcribe_audio(project_id, req.clip_id, req.language)
|
||||||
.await
|
.await
|
||||||
|
|
@ -561,7 +561,7 @@ pub async fn generate_captions_handler(
|
||||||
Path(project_id): Path<Uuid>,
|
Path(project_id): Path<Uuid>,
|
||||||
Json(req): Json<GenerateCaptionsRequest>,
|
Json(req): Json<GenerateCaptionsRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
|
|
||||||
let transcription = match engine.transcribe_audio(project_id, None, None).await {
|
let transcription = match engine.transcribe_audio(project_id, None, None).await {
|
||||||
Ok(t) => t,
|
Ok(t) => t,
|
||||||
|
|
@ -569,7 +569,7 @@ pub async fn generate_captions_handler(
|
||||||
error!("Transcription failed: {e}");
|
error!("Transcription failed: {e}");
|
||||||
return (
|
return (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json(serde_json::json!({ "error": sanitize_error("generate_captions") })),
|
Json(serde_json::json!({ "error": SafeErrorResponse::internal_error() })),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -614,7 +614,7 @@ pub async fn tts_handler(
|
||||||
Path(project_id): Path<Uuid>,
|
Path(project_id): Path<Uuid>,
|
||||||
Json(req): Json<TTSRequest>,
|
Json(req): Json<TTSRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
let output_dir =
|
let output_dir =
|
||||||
std::env::var("VIDEO_AUDIO_DIR").unwrap_or_else(|_| "./audio/video".to_string());
|
std::env::var("VIDEO_AUDIO_DIR").unwrap_or_else(|_| "./audio/video".to_string());
|
||||||
|
|
||||||
|
|
@ -622,7 +622,7 @@ pub async fn tts_handler(
|
||||||
error!("Failed to create audio directory: {e}");
|
error!("Failed to create audio directory: {e}");
|
||||||
return (
|
return (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json(serde_json::json!({ "error": sanitize_error("tts") })),
|
Json(serde_json::json!({ "error": SafeErrorResponse::internal_error() })),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -661,7 +661,7 @@ pub async fn tts_handler(
|
||||||
error!("Failed to add audio track: {e}");
|
error!("Failed to add audio track: {e}");
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json(serde_json::json!({ "error": sanitize_error("tts") })),
|
Json(serde_json::json!({ "error": SafeErrorResponse::internal_error() })),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -680,7 +680,7 @@ pub async fn detect_scenes_handler(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(project_id): Path<Uuid>,
|
Path(project_id): Path<Uuid>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
let output_dir =
|
let output_dir =
|
||||||
std::env::var("VIDEO_THUMBNAILS_DIR").unwrap_or_else(|_| "./thumbnails/video".to_string());
|
std::env::var("VIDEO_THUMBNAILS_DIR").unwrap_or_else(|_| "./thumbnails/video".to_string());
|
||||||
|
|
||||||
|
|
@ -688,7 +688,7 @@ pub async fn detect_scenes_handler(
|
||||||
error!("Failed to create thumbnails directory: {e}");
|
error!("Failed to create thumbnails directory: {e}");
|
||||||
return (
|
return (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json(serde_json::json!({ "error": sanitize_error("detect_scenes") })),
|
Json(serde_json::json!({ "error": SafeErrorResponse::internal_error() })),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -709,7 +709,7 @@ pub async fn auto_reframe_handler(
|
||||||
Path(project_id): Path<Uuid>,
|
Path(project_id): Path<Uuid>,
|
||||||
Json(req): Json<AutoReframeRequest>,
|
Json(req): Json<AutoReframeRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
let clips = match engine.get_clips(project_id).await {
|
let clips = match engine.get_clips(project_id).await {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
|
|
@ -736,7 +736,7 @@ pub async fn auto_reframe_handler(
|
||||||
error!("Failed to create reframe directory: {e}");
|
error!("Failed to create reframe directory: {e}");
|
||||||
return (
|
return (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json(serde_json::json!({ "error": sanitize_error("auto_reframe") })),
|
Json(serde_json::json!({ "error": SafeErrorResponse::internal_error() })),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -769,7 +769,7 @@ pub async fn remove_background_handler(
|
||||||
Path(project_id): Path<Uuid>,
|
Path(project_id): Path<Uuid>,
|
||||||
Json(req): Json<BackgroundRemovalRequest>,
|
Json(req): Json<BackgroundRemovalRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
|
|
||||||
match engine
|
match engine
|
||||||
.remove_background(project_id, req.clip_id, req.replacement)
|
.remove_background(project_id, req.clip_id, req.replacement)
|
||||||
|
|
@ -791,7 +791,7 @@ pub async fn enhance_video_handler(
|
||||||
Path(project_id): Path<Uuid>,
|
Path(project_id): Path<Uuid>,
|
||||||
Json(req): Json<VideoEnhanceRequest>,
|
Json(req): Json<VideoEnhanceRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
|
|
||||||
match engine.enhance_video(project_id, req).await {
|
match engine.enhance_video(project_id, req).await {
|
||||||
Ok(response) => (StatusCode::OK, Json(serde_json::json!(response))),
|
Ok(response) => (StatusCode::OK, Json(serde_json::json!(response))),
|
||||||
|
|
@ -810,7 +810,7 @@ pub async fn beat_sync_handler(
|
||||||
Path(project_id): Path<Uuid>,
|
Path(project_id): Path<Uuid>,
|
||||||
Json(req): Json<BeatSyncRequest>,
|
Json(req): Json<BeatSyncRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
|
|
||||||
match engine
|
match engine
|
||||||
.detect_beats(project_id, req.audio_track_id, req.sensitivity)
|
.detect_beats(project_id, req.audio_track_id, req.sensitivity)
|
||||||
|
|
@ -832,7 +832,7 @@ pub async fn generate_waveform_handler(
|
||||||
Path(project_id): Path<Uuid>,
|
Path(project_id): Path<Uuid>,
|
||||||
Json(req): Json<WaveformRequest>,
|
Json(req): Json<WaveformRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
|
|
||||||
match engine
|
match engine
|
||||||
.generate_waveform(project_id, req.audio_track_id, req.samples_per_second)
|
.generate_waveform(project_id, req.audio_track_id, req.samples_per_second)
|
||||||
|
|
@ -896,7 +896,7 @@ pub async fn apply_template_handler(
|
||||||
Path(project_id): Path<Uuid>,
|
Path(project_id): Path<Uuid>,
|
||||||
Json(req): Json<ApplyTemplateRequest>,
|
Json(req): Json<ApplyTemplateRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
|
|
||||||
match engine
|
match engine
|
||||||
.apply_template(project_id, &req.template_id, req.customizations)
|
.apply_template(project_id, &req.template_id, req.customizations)
|
||||||
|
|
@ -921,7 +921,7 @@ pub async fn add_transition_handler(
|
||||||
Path((from_id, to_id)): Path<(Uuid, Uuid)>,
|
Path((from_id, to_id)): Path<(Uuid, Uuid)>,
|
||||||
Json(req): Json<TransitionRequest>,
|
Json(req): Json<TransitionRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
|
|
||||||
match engine
|
match engine
|
||||||
.add_transition(from_id, to_id, &req.transition_type, req.duration_ms)
|
.add_transition(from_id, to_id, &req.transition_type, req.duration_ms)
|
||||||
|
|
@ -946,7 +946,7 @@ pub async fn chat_edit(
|
||||||
Path(project_id): Path<Uuid>,
|
Path(project_id): Path<Uuid>,
|
||||||
Json(req): Json<ChatEditRequest>,
|
Json(req): Json<ChatEditRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
|
|
||||||
match engine
|
match engine
|
||||||
.process_chat_command(project_id, &req.message, req.playhead_ms, req.selection)
|
.process_chat_command(project_id, &req.message, req.playhead_ms, req.selection)
|
||||||
|
|
@ -973,7 +973,7 @@ pub async fn start_export(
|
||||||
Path(project_id): Path<Uuid>,
|
Path(project_id): Path<Uuid>,
|
||||||
Json(req): Json<ExportRequest>,
|
Json(req): Json<ExportRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
|
|
||||||
match engine.start_export(project_id, req, state.cache.as_ref()).await {
|
match engine.start_export(project_id, req, state.cache.as_ref()).await {
|
||||||
Ok(export) => (
|
Ok(export) => (
|
||||||
|
|
@ -994,7 +994,7 @@ pub async fn get_export_status(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(export_id): Path<Uuid>,
|
Path(export_id): Path<Uuid>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let engine = VideoEngine::new(state.db.clone());
|
let engine = VideoEngine::new(state.conn.clone());
|
||||||
|
|
||||||
match engine.get_export_status(export_id).await {
|
match engine.get_export_status(export_id).await {
|
||||||
Ok(export) => (
|
Ok(export) => (
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ pub use analytics::{get_analytics_handler, record_view_handler, AnalyticsEngine}
|
||||||
pub use engine::VideoEngine;
|
pub use engine::VideoEngine;
|
||||||
pub use handlers::*;
|
pub use handlers::*;
|
||||||
pub use models::*;
|
pub use models::*;
|
||||||
pub use render::{start_render_worker, start_render_worker_with_broadcaster, VideoRenderWorker};
|
pub use render::{start_render_worker, VideoRenderWorker};
|
||||||
pub use schema::*;
|
pub use schema::*;
|
||||||
pub use websocket::{broadcast_export_progress, export_progress_websocket, ExportProgressBroadcaster};
|
pub use websocket::{broadcast_export_progress, export_progress_websocket, ExportProgressBroadcaster};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,12 @@ use crate::shared::utils::DbPool;
|
||||||
|
|
||||||
use super::models::*;
|
use super::models::*;
|
||||||
use super::schema::*;
|
use super::schema::*;
|
||||||
use super::websocket::{broadcast_export_progress, ExportProgressBroadcaster};
|
use super::websocket::broadcast_export_progress;
|
||||||
|
|
||||||
pub struct VideoRenderWorker {
|
pub struct VideoRenderWorker {
|
||||||
db: DbPool,
|
db: DbPool,
|
||||||
cache: Arc<redis::Client>,
|
cache: Arc<redis::Client>,
|
||||||
output_dir: String,
|
output_dir: String,
|
||||||
broadcaster: Option<Arc<ExportProgressBroadcaster>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VideoRenderWorker {
|
impl VideoRenderWorker {
|
||||||
|
|
@ -24,21 +23,6 @@ impl VideoRenderWorker {
|
||||||
db,
|
db,
|
||||||
cache,
|
cache,
|
||||||
output_dir,
|
output_dir,
|
||||||
broadcaster: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_broadcaster(
|
|
||||||
db: DbPool,
|
|
||||||
cache: Arc<redis::Client>,
|
|
||||||
output_dir: String,
|
|
||||||
broadcaster: Arc<ExportProgressBroadcaster>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
db,
|
|
||||||
cache,
|
|
||||||
output_dir,
|
|
||||||
broadcaster: Some(broadcaster),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,18 +147,15 @@ impl VideoRenderWorker {
|
||||||
.execute(&mut db_conn)?;
|
.execute(&mut db_conn)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(broadcaster) = &self.broadcaster {
|
broadcast_export_progress(
|
||||||
broadcast_export_progress(
|
export_id,
|
||||||
broadcaster,
|
project_id,
|
||||||
export_id,
|
status,
|
||||||
project_id,
|
progress,
|
||||||
status,
|
Some(format!("Export {progress}%")),
|
||||||
progress,
|
output_url,
|
||||||
Some(format!("Export {progress}%")),
|
gbdrive_path,
|
||||||
output_url,
|
);
|
||||||
gbdrive_path,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -451,15 +432,3 @@ pub fn start_render_worker(db: DbPool, cache: Arc<redis::Client>, output_dir: St
|
||||||
worker.run_worker_loop().await;
|
worker.run_worker_loop().await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start_render_worker_with_broadcaster(
|
|
||||||
db: DbPool,
|
|
||||||
cache: Arc<redis::Client>,
|
|
||||||
output_dir: String,
|
|
||||||
broadcaster: Arc<ExportProgressBroadcaster>,
|
|
||||||
) {
|
|
||||||
let worker = VideoRenderWorker::with_broadcaster(db, cache, output_dir, broadcaster);
|
|
||||||
tokio::spawn(async move {
|
|
||||||
worker.run_worker_loop().await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@ use crate::shared::state::AppState;
|
||||||
|
|
||||||
use super::models::ExportProgressEvent;
|
use super::models::ExportProgressEvent;
|
||||||
|
|
||||||
|
static GLOBAL_BROADCASTER: std::sync::OnceLock<Arc<ExportProgressBroadcaster>> =
|
||||||
|
std::sync::OnceLock::new();
|
||||||
|
|
||||||
pub struct ExportProgressBroadcaster {
|
pub struct ExportProgressBroadcaster {
|
||||||
tx: broadcast::Sender<ExportProgressEvent>,
|
tx: broadcast::Sender<ExportProgressEvent>,
|
||||||
}
|
}
|
||||||
|
|
@ -25,6 +28,12 @@ impl ExportProgressBroadcaster {
|
||||||
Self { tx }
|
Self { tx }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn global() -> Arc<Self> {
|
||||||
|
GLOBAL_BROADCASTER
|
||||||
|
.get_or_init(|| Arc::new(Self::new()))
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn sender(&self) -> broadcast::Sender<ExportProgressEvent> {
|
pub fn sender(&self) -> broadcast::Sender<ExportProgressEvent> {
|
||||||
self.tx.clone()
|
self.tx.clone()
|
||||||
}
|
}
|
||||||
|
|
@ -48,14 +57,14 @@ impl Default for ExportProgressBroadcaster {
|
||||||
|
|
||||||
pub async fn export_progress_websocket(
|
pub async fn export_progress_websocket(
|
||||||
ws: WebSocketUpgrade,
|
ws: WebSocketUpgrade,
|
||||||
State(state): State<Arc<AppState>>,
|
State(_state): State<Arc<AppState>>,
|
||||||
Path(export_id): Path<Uuid>,
|
Path(export_id): Path<Uuid>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
info!("WebSocket connection request for export: {export_id}");
|
info!("WebSocket connection request for export: {export_id}");
|
||||||
ws.on_upgrade(move |socket| handle_export_websocket(socket, state, export_id))
|
ws.on_upgrade(move |socket| handle_export_websocket(socket, export_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_export_websocket(socket: WebSocket, state: Arc<AppState>, export_id: Uuid) {
|
async fn handle_export_websocket(socket: WebSocket, export_id: Uuid) {
|
||||||
let (mut sender, mut receiver) = socket.split();
|
let (mut sender, mut receiver) = socket.split();
|
||||||
|
|
||||||
info!("WebSocket connected for export: {export_id}");
|
info!("WebSocket connected for export: {export_id}");
|
||||||
|
|
@ -75,13 +84,8 @@ async fn handle_export_websocket(socket: WebSocket, state: Arc<AppState>, export
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut progress_rx = if let Some(broadcaster) = state.video_progress_broadcaster.as_ref() {
|
let broadcaster = ExportProgressBroadcaster::global();
|
||||||
broadcaster.subscribe()
|
let mut progress_rx = broadcaster.subscribe();
|
||||||
} else {
|
|
||||||
let (tx, rx) = broadcast::channel(1);
|
|
||||||
drop(tx);
|
|
||||||
rx
|
|
||||||
};
|
|
||||||
|
|
||||||
let export_id_for_recv = export_id;
|
let export_id_for_recv = export_id;
|
||||||
|
|
||||||
|
|
@ -177,7 +181,6 @@ async fn handle_export_websocket(socket: WebSocket, state: Arc<AppState>, export
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn broadcast_export_progress(
|
pub fn broadcast_export_progress(
|
||||||
broadcaster: &ExportProgressBroadcaster,
|
|
||||||
export_id: Uuid,
|
export_id: Uuid,
|
||||||
project_id: Uuid,
|
project_id: Uuid,
|
||||||
status: &str,
|
status: &str,
|
||||||
|
|
@ -196,5 +199,5 @@ pub fn broadcast_export_progress(
|
||||||
gbdrive_path,
|
gbdrive_path,
|
||||||
};
|
};
|
||||||
|
|
||||||
broadcaster.send(event);
|
ExportProgressBroadcaster::global().send(event);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue