stuff
All checks were successful
/ upload (release) Successful in 40s

This commit is contained in:
pavel 2026-02-17 19:07:06 +01:00
commit 9aca4cadf0
13 changed files with 483 additions and 21 deletions

View file

@ -4,7 +4,7 @@ export default defineConfig({
server: { server: {
proxy: { proxy: {
'/api': { '/api': {
target: 'http://localhost:3000', target: 'http://localhost:3001',
changeOrigin: true, changeOrigin: true,
ws: true, ws: true,
} }

View file

@ -15,6 +15,7 @@ pub struct Config {
pub agent_max_turns: u32, pub agent_max_turns: u32,
pub agent_max_duration_secs: u64, pub agent_max_duration_secs: u64,
pub vapid_private_key: String, pub vapid_private_key: String,
pub calendar_api_url: String,
} }
impl Config { impl Config {
@ -58,6 +59,9 @@ impl Config {
let vapid_private_key = env::var("VAPID_PRIVATE_KEY") let vapid_private_key = env::var("VAPID_PRIVATE_KEY")
.map_err(|_| AppError::Config("VAPID_PRIVATE_KEY must be set".into()))?; .map_err(|_| AppError::Config("VAPID_PRIVATE_KEY must be set".into()))?;
let calendar_api_url =
env::var("CALENDAR_API_URL").unwrap_or_else(|_| "http://localhost:8000".to_string());
Ok(Config { Ok(Config {
database_url, database_url,
port, port,
@ -71,6 +75,7 @@ impl Config {
agent_max_turns, agent_max_turns,
agent_max_duration_secs, agent_max_duration_secs,
vapid_private_key, vapid_private_key,
calendar_api_url,
}) })
} }
} }

View file

@ -3,6 +3,7 @@ pub mod tools;
use chrono::Utc; use chrono::Utc;
use sea_orm::DatabaseConnection; use sea_orm::DatabaseConnection;
use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use self::api::{ChatRequest, ChatResponse, Message, Tool}; use self::api::{ChatRequest, ChatResponse, Message, Tool};
@ -13,6 +14,8 @@ pub struct Agent {
url: String, url: String,
zen_api_key: Option<String>, zen_api_key: Option<String>,
tavily_api_key: Option<String>, tavily_api_key: Option<String>,
calendar_client: Arc<crate::domain::calendar::CalendarClient>,
pub user_sub: Option<String>,
pub messages: Vec<Message>, pub messages: Vec<Message>,
tools: Option<Vec<Tool>>, tools: Option<Vec<Tool>>,
logs: String, logs: String,
@ -26,6 +29,8 @@ impl Agent {
db: DatabaseConnection, db: DatabaseConnection,
zen_api_key: Option<String>, zen_api_key: Option<String>,
tavily_api_key: Option<String>, tavily_api_key: Option<String>,
calendar_client: Arc<crate::domain::calendar::CalendarClient>,
user_sub: Option<String>,
initial_message: String, initial_message: String,
) -> AppResult<Self> { ) -> AppResult<Self> {
let intro = format!( let intro = format!(
@ -50,13 +55,22 @@ impl Agent {
}, },
]; ];
Self::with_messages(db, zen_api_key, tavily_api_key, messages) Self::with_messages(
db,
zen_api_key,
tavily_api_key,
calendar_client,
user_sub,
messages,
)
} }
pub fn with_messages( pub fn with_messages(
db: DatabaseConnection, db: DatabaseConnection,
zen_api_key: Option<String>, zen_api_key: Option<String>,
tavily_api_key: Option<String>, tavily_api_key: Option<String>,
calendar_client: Arc<crate::domain::calendar::CalendarClient>,
user_sub: Option<String>,
messages: Vec<Message>, messages: Vec<Message>,
) -> AppResult<Self> { ) -> AppResult<Self> {
let tools = Some(tools::get_tools()); let tools = Some(tools::get_tools());
@ -72,6 +86,8 @@ impl Agent {
url: "https://opencode.ai/zen/v1/chat/completions".to_string(), url: "https://opencode.ai/zen/v1/chat/completions".to_string(),
zen_api_key, zen_api_key,
tavily_api_key, tavily_api_key,
calendar_client,
user_sub,
messages, messages,
tools, tools,
logs: String::new(), logs: String::new(),
@ -166,12 +182,15 @@ impl Agent {
for tool_call in tool_calls { for tool_call in tool_calls {
self.log(&format!("Calling tool: {}", tool_call.function.name)); self.log(&format!("Calling tool: {}", tool_call.function.name));
let (tool_message, is_final, tool_answer) = let (tool_message, is_final, tool_answer) = tools::handle_tool_call(
tools::handle_tool_call(tool_call, &self.tavily_api_key, &self.db) tool_call,
.await &self.tavily_api_key,
.map_err(|e| { &self.db,
AppError::Internal(format!("Tool execution failed: {}", e)) &self.calendar_client,
})?; self.user_sub.as_deref(),
)
.await
.map_err(|e| AppError::Internal(format!("Tool execution failed: {}", e)))?;
if let Some(ans) = tool_answer { if let Some(ans) = tool_answer {
self.answer = Some(ans.clone()); self.answer = Some(ans.clone());

View file

@ -3,6 +3,7 @@ use sea_orm::{
ColumnTrait, Condition, DatabaseConnection, EntityTrait, QueryFilter, QueryOrder, QuerySelect, ColumnTrait, Condition, DatabaseConnection, EntityTrait, QueryFilter, QueryOrder, QuerySelect,
}; };
use serde::Deserialize; use serde::Deserialize;
use std::sync::Arc;
use uuid::Uuid; use uuid::Uuid;
#[derive(Deserialize)] #[derive(Deserialize)]
@ -96,6 +97,47 @@ pub fn get_tools() -> Vec<Tool> {
}), }),
}, },
}, },
Tool {
tool_type: "function".to_string(),
function: FunctionDefinition {
name: "calendar_list_events".to_string(),
description: "List calendar events".to_string(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"upcoming": {
"type": "boolean",
"description": "If true, only upcoming events will be listed"
}
}
}),
},
},
Tool {
tool_type: "function".to_string(),
function: FunctionDefinition {
name: "calendar_create_event".to_string(),
description: "Create a new calendar event".to_string(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of the event"
},
"from": {
"type": "string",
"description": "Start time in ISO 8601 format (e.g., 2023-10-27T10:00:00Z)"
},
"to": {
"type": "string",
"description": "End time in ISO 8601 format (e.g., 2023-10-27T11:00:00Z)"
}
},
"required": ["name", "from", "to"]
}),
},
},
] ]
} }
@ -103,6 +145,8 @@ pub async fn handle_tool_call(
tool_call: &ToolCall, tool_call: &ToolCall,
tavily_api_key: &Option<String>, tavily_api_key: &Option<String>,
db: &DatabaseConnection, db: &DatabaseConnection,
calendar: &Arc<crate::domain::calendar::CalendarClient>,
user_sub: Option<&str>,
) -> Result<(Message, bool, Option<String>), Box<dyn std::error::Error>> { ) -> Result<(Message, bool, Option<String>), Box<dyn std::error::Error>> {
let mut answer = None; let mut answer = None;
let name = &tool_call.function.name; let name = &tool_call.function.name;
@ -198,6 +242,34 @@ pub async fn handle_tool_call(
)); ));
} }
(out, false) (out, false)
} else if name == "calendar_list_events" {
let args: serde_json::Value = serde_json::from_str(&tool_call.function.arguments)?;
let upcoming = args["upcoming"].as_bool();
match calendar
.list_events(user_sub.map(|s| s.to_string()), upcoming)
.await
{
Ok(events) => {
tracing::info!("{:#?}", events);
(serde_json::to_string(&events)?, false)
}
Err(e) => (format!("Error listing events: {}", e), false),
}
} else if name == "calendar_create_event" {
let args: serde_json::Value = serde_json::from_str(&tool_call.function.arguments)?;
let name_val = args["name"].as_str().unwrap_or_default();
let from_val = args["from"].as_str().unwrap_or_default();
let to_val = args["to"].as_str().unwrap_or_default();
match calendar
.create_event(user_sub.map(|s| s.to_string()), name_val, from_val, to_val)
.await
{
Ok(event) => (
format!("Event created: {}", serde_json::to_string(&event)?),
false,
),
Err(e) => (format!("Error creating event: {}", e), false),
}
} else { } else {
(format!("Error: Unknown tool {}", name), false) (format!("Error: Unknown tool {}", name), false)
}; };

View file

@ -190,4 +190,27 @@ impl Authenticator {
Ok(res) Ok(res)
} }
pub async fn client_credentials(
&self,
scope: &str,
) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
let params = [
("grant_type", "client_credentials"),
("client_id", &self.client_id),
("client_secret", &self.client_secret),
("scope", scope),
];
let res = self
.client
.post(&self.token_url)
.form(&params)
.send()
.await?
.json()
.await?;
Ok(res)
}
} }

292
src/domain/calendar/mod.rs Normal file
View file

@ -0,0 +1,292 @@
use crate::domain::auth::Authenticator;
use crate::error::{AppError, AppResult};
use chrono::{DateTime, Duration, Utc};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CalendarEvent {
pub id: Option<i64>,
pub name: String,
pub from: DateTime<Utc>,
pub to: DateTime<Utc>,
pub user_sub: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateEventRequest {
pub name: String,
pub from: String,
pub to: String,
pub user_sub: Option<String>,
}
struct TokenState {
access_token: String,
expires_at: DateTime<Utc>,
}
pub struct CalendarClient {
base_url: String,
client: Client,
authenticator: Arc<Authenticator>,
token_state: RwLock<Option<TokenState>>,
}
impl CalendarClient {
pub fn new(base_url: String, authenticator: Arc<Authenticator>) -> Self {
Self {
base_url: base_url.trim_end_matches('/').to_string(),
client: Client::new(),
authenticator,
token_state: RwLock::new(None),
}
}
async fn get_token(&self) -> AppResult<String> {
{
let state = self.token_state.read().await;
if let Some(token) = &*state {
if token.expires_at > Utc::now() + Duration::seconds(30) {
tracing::debug!("Using cached Calendar API token");
return Ok(token.access_token.clone());
}
}
}
let mut state = self.token_state.write().await;
// Double check after acquiring write lock
if let Some(token) = &*state {
if token.expires_at > Utc::now() + Duration::seconds(30) {
return Ok(token.access_token.clone());
}
}
tracing::info!("Refreshing Calendar API token via Client Credentials flow");
let token_data = self
.authenticator
.client_credentials("profile")
.await
.map_err(|e| AppError::Internal(format!("Failed to get client credentials: {}", e)))?;
let access_token = token_data["access_token"]
.as_str()
.ok_or_else(|| AppError::Internal("Missing access_token in response".into()))?
.to_string();
let expires_in = token_data["expires_in"].as_i64().unwrap_or(3600);
let expires_at = Utc::now() + Duration::seconds(expires_in);
*state = Some(TokenState {
access_token: access_token.clone(),
expires_at,
});
Ok(access_token)
}
pub async fn list_events(
&self,
user_sub: Option<String>,
upcoming: Option<bool>,
) -> AppResult<Vec<CalendarEvent>> {
let token = self.get_token().await?;
let mut url = format!("{}/service/v1/events", self.base_url);
let mut params = Vec::new();
if let Some(uid) = &user_sub {
params.push(format!("user_sub={}", uid));
}
if let Some(u) = upcoming {
params.push(format!("upcoming={}", u));
}
if !params.is_empty() {
url.push_str("?");
url.push_str(&params.join("&"));
}
tracing::info!(method = "GET", %url, "Sending Calendar API request");
let res = self
.client
.get(&url)
.bearer_auth(token)
.send()
.await
.map_err(AppError::Network)?;
let status = res.status();
tracing::info!(%status, %url, "Received Calendar API response");
if !status.is_success() {
let error_body = res.text().await.unwrap_or_default();
tracing::error!(%status, %url, body = %error_body, "Calendar API request failed");
return Err(AppError::Internal(format!(
"Failed to list events: {} - {}",
status, error_body
)));
}
let body = res
.json()
.await
.map_err(|e| AppError::Internal(e.to_string()));
tracing::info!(%status, %url, body = ?body, "Received Calendar API response");
body
}
pub async fn create_event(
&self,
user_sub: Option<String>,
name: &str,
from: &str,
to: &str,
) -> AppResult<CalendarEvent> {
let token = self.get_token().await?;
let url = format!("{}/service/v1/events", self.base_url);
let request = CreateEventRequest {
name: name.to_string(),
from: from.to_string(),
to: to.to_string(),
user_sub,
};
tracing::info!(method = "POST", %url, "Sending Calendar API request");
let res = self
.client
.post(&url)
.bearer_auth(token)
.json(&request)
.send()
.await
.map_err(AppError::Network)?;
let status = res.status();
tracing::info!(%status, %url, "Received Calendar API response");
if !status.is_success() {
let error_body = res.text().await.unwrap_or_default();
tracing::error!(%status, %url, body = %error_body, "Calendar API request failed");
return Err(AppError::Internal(format!(
"Failed to create event: {} - {}",
status, error_body
)));
}
res.json()
.await
.map_err(|e| AppError::Internal(e.to_string()))
}
#[allow(dead_code)]
pub async fn get_event(&self, id: i32) -> AppResult<CalendarEvent> {
let token = self.get_token().await?;
let url = format!("{}/service/v1/events/{}", self.base_url, id);
tracing::info!(method = "GET", %url, "Sending Calendar API request");
let res = self
.client
.get(&url)
.bearer_auth(token)
.send()
.await
.map_err(AppError::Network)?;
let status = res.status();
tracing::info!(%status, %url, "Received Calendar API response");
if !status.is_success() {
let error_body = res.text().await.unwrap_or_default();
tracing::error!(%status, %url, body = %error_body, "Calendar API request failed");
return Err(AppError::Internal(format!(
"Failed to get event: {} - {}",
status, error_body
)));
}
res.json()
.await
.map_err(|e| AppError::Internal(e.to_string()))
}
#[allow(dead_code)]
pub async fn update_event(
&self,
id: i32,
user_sub: Option<String>,
name: &str,
from: &str,
to: &str,
) -> AppResult<CalendarEvent> {
let token = self.get_token().await?;
let url = format!("{}/service/v1/events/{}", self.base_url, id);
let request = CreateEventRequest {
name: name.to_string(),
from: from.to_string(),
to: to.to_string(),
user_sub,
};
tracing::info!(method = "PUT", %url, "Sending Calendar API request");
let res = self
.client
.put(&url)
.bearer_auth(token)
.json(&request)
.send()
.await
.map_err(AppError::Network)?;
let status = res.status();
tracing::info!(%status, %url, "Received Calendar API response");
if !status.is_success() {
let error_body = res.text().await.unwrap_or_default();
tracing::error!(%status, %url, body = %error_body, "Calendar API request failed");
return Err(AppError::Internal(format!(
"Failed to update event: {} - {}",
status, error_body
)));
}
res.json()
.await
.map_err(|e| AppError::Internal(e.to_string()))
}
#[allow(dead_code)]
pub async fn delete_event(&self, id: i32) -> AppResult<()> {
let token = self.get_token().await?;
let url = format!("{}/service/v1/events/{}", self.base_url, id);
tracing::info!(method = "DELETE", %url, "Sending Calendar API request");
let res = self
.client
.delete(&url)
.bearer_auth(token)
.send()
.await
.map_err(AppError::Network)?;
let status = res.status();
tracing::info!(%status, %url, "Received Calendar API response");
if !status.is_success() {
let error_body = res.text().await.unwrap_or_default();
tracing::error!(%status, %url, body = %error_body, "Calendar API request failed");
return Err(AppError::Internal(format!(
"Failed to delete event: {} - {}",
status, error_body
)));
}
Ok(())
}
}

View file

@ -1,4 +1,5 @@
pub mod agent; pub mod agent;
pub mod auth; pub mod auth;
pub mod calendar;
pub mod notifications; pub mod notifications;
pub mod tasks; pub mod tasks;

View file

@ -56,6 +56,7 @@ pub async fn execute_agent_run(
db: &DatabaseConnection, db: &DatabaseConnection,
_scheduler: &Arc<Scheduler>, _scheduler: &Arc<Scheduler>,
config: &Arc<Config>, config: &Arc<Config>,
calendar_client: Arc<crate::domain::calendar::CalendarClient>,
task_id: Uuid, task_id: Uuid,
goal: String, goal: String,
) -> AppResult<TaskResponse> { ) -> AppResult<TaskResponse> {
@ -92,6 +93,8 @@ pub async fn execute_agent_run(
db.clone(), db.clone(),
config.zen_api_key.clone(), config.zen_api_key.clone(),
config.tavily_api_key.clone(), config.tavily_api_key.clone(),
calendar_client.clone(),
None,
goal.clone(), goal.clone(),
)?; )?;

View file

@ -32,16 +32,20 @@ pub enum AppError {
impl IntoResponse for AppError { impl IntoResponse for AppError {
fn into_response(self) -> Response { fn into_response(self) -> Response {
let (status, error_message) = match self { let (status, error_message) = match &self {
AppError::Database(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()), AppError::Database(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()),
AppError::Config(err) => (StatusCode::INTERNAL_SERVER_ERROR, err), AppError::Config(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.clone()),
AppError::NotFound(err) => (StatusCode::NOT_FOUND, err), AppError::NotFound(err) => (StatusCode::NOT_FOUND, err.clone()),
AppError::Unauthorized(err) => (StatusCode::UNAUTHORIZED, err), AppError::Unauthorized(err) => (StatusCode::UNAUTHORIZED, err.clone()),
AppError::Internal(err) => (StatusCode::INTERNAL_SERVER_ERROR, err), AppError::Internal(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.clone()),
AppError::Network(err) => (StatusCode::BAD_GATEWAY, err.to_string()), AppError::Network(err) => (StatusCode::BAD_GATEWAY, err.to_string()),
AppError::InvalidRequest(err) => (StatusCode::BAD_REQUEST, err), AppError::InvalidRequest(err) => (StatusCode::BAD_REQUEST, err.clone()),
}; };
if status.is_server_error() || status.is_client_error() {
tracing::error!(%status, error = %self, "AppError converted to response");
}
let body = Json(json!({ let body = Json(json!({
"error": error_message, "error": error_message,
})); }));

View file

@ -13,6 +13,7 @@ pub struct Scheduler {
db: DatabaseConnection, db: DatabaseConnection,
tasks_to_jobs: DashMap<Uuid, Uuid>, tasks_to_jobs: DashMap<Uuid, Uuid>,
config: Arc<crate::config::Config>, config: Arc<crate::config::Config>,
pub calendar_client: Arc<crate::domain::calendar::CalendarClient>,
pub tx: tokio::sync::broadcast::Sender<crate::server::notifications::WsEvent>, pub tx: tokio::sync::broadcast::Sender<crate::server::notifications::WsEvent>,
pub push_sender: Arc<crate::domain::notifications::push::PushSender>, pub push_sender: Arc<crate::domain::notifications::push::PushSender>,
} }
@ -21,6 +22,7 @@ impl Scheduler {
pub async fn new( pub async fn new(
db: DatabaseConnection, db: DatabaseConnection,
config: Arc<crate::config::Config>, config: Arc<crate::config::Config>,
calendar_client: Arc<crate::domain::calendar::CalendarClient>,
tx: tokio::sync::broadcast::Sender<crate::server::notifications::WsEvent>, tx: tokio::sync::broadcast::Sender<crate::server::notifications::WsEvent>,
push_sender: Arc<crate::domain::notifications::push::PushSender>, push_sender: Arc<crate::domain::notifications::push::PushSender>,
) -> AppResult<Self> { ) -> AppResult<Self> {
@ -36,6 +38,7 @@ impl Scheduler {
db, db,
tasks_to_jobs: DashMap::new(), tasks_to_jobs: DashMap::new(),
config, config,
calendar_client,
tx, tx,
push_sender, push_sender,
}) })
@ -52,13 +55,17 @@ impl Scheduler {
let tx = self.tx.clone(); let tx = self.tx.clone();
let push_sender = self.push_sender.clone(); let push_sender = self.push_sender.clone();
let calendar_client = self.calendar_client.clone();
let job = Job::new_async(cron_expr, move |_uuid, _l| { let job = Job::new_async(cron_expr, move |_uuid, _l| {
let db = db.clone(); let db = db.clone();
let config = config.clone(); let config = config.clone();
let tx = tx.clone(); let tx = tx.clone();
let push_sender = push_sender.clone(); let push_sender = push_sender.clone();
let calendar_client = calendar_client.clone();
Box::pin(async move { Box::pin(async move {
if let Err(e) = Self::run_task(db, config, tx, push_sender, task_id).await { if let Err(e) =
Self::run_task(db, config, calendar_client, tx, push_sender, task_id).await
{
tracing::error!("Error in scheduled task {}: {}", task_id, e); tracing::error!("Error in scheduled task {}: {}", task_id, e);
} }
}) })
@ -91,6 +98,7 @@ impl Scheduler {
async fn run_task( async fn run_task(
db: DatabaseConnection, db: DatabaseConnection,
config: Arc<crate::config::Config>, config: Arc<crate::config::Config>,
calendar_client: Arc<crate::domain::calendar::CalendarClient>,
tx: tokio::sync::broadcast::Sender<crate::server::notifications::WsEvent>, tx: tokio::sync::broadcast::Sender<crate::server::notifications::WsEvent>,
push_sender: Arc<crate::domain::notifications::push::PushSender>, push_sender: Arc<crate::domain::notifications::push::PushSender>,
task_id: Uuid, task_id: Uuid,
@ -132,6 +140,8 @@ impl Scheduler {
db.clone(), db.clone(),
config.zen_api_key.clone(), config.zen_api_key.clone(),
config.tavily_api_key.clone(), config.tavily_api_key.clone(),
calendar_client.clone(),
None,
task.goal.clone(), task.goal.clone(),
)?; )?;

View file

@ -17,6 +17,7 @@ pub struct ChatResult {
pub async fn chat_handler( pub async fn chat_handler(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
user: crate::server::auth::AuthenticatedUser,
Json(payload): Json<ChatPayload>, Json(payload): Json<ChatPayload>,
) -> Result<Json<ChatResult>, AppError> { ) -> Result<Json<ChatResult>, AppError> {
let msg_count = payload.messages.len(); let msg_count = payload.messages.len();
@ -45,6 +46,8 @@ pub async fn chat_handler(
state.db.clone(), state.db.clone(),
state.config.zen_api_key.clone(), state.config.zen_api_key.clone(),
state.config.tavily_api_key.clone(), state.config.tavily_api_key.clone(),
state.calendar_client.clone(),
Some(user.0.sub),
messages, messages,
)?; )?;

View file

@ -13,7 +13,6 @@ use sea_orm::{Database, DatabaseConnection, EntityTrait};
use std::sync::Arc; use std::sync::Arc;
use tower_http::cors::{AllowOrigin, CorsLayer}; use tower_http::cors::{AllowOrigin, CorsLayer};
use crate::entities::task::Entity as Task;
use crate::scheduler::Scheduler; use crate::scheduler::Scheduler;
use crate::error::AppResult; use crate::error::AppResult;
@ -25,6 +24,7 @@ pub struct AppState {
pub config: Arc<crate::config::Config>, pub config: Arc<crate::config::Config>,
pub verifier: Arc<crate::domain::auth::JwksVerifier>, pub verifier: Arc<crate::domain::auth::JwksVerifier>,
pub authenticator: Arc<crate::domain::auth::Authenticator>, pub authenticator: Arc<crate::domain::auth::Authenticator>,
pub calendar_client: Arc<crate::domain::calendar::CalendarClient>,
pub tx: tokio::sync::broadcast::Sender<crate::server::notifications::WsEvent>, pub tx: tokio::sync::broadcast::Sender<crate::server::notifications::WsEvent>,
} }
@ -39,13 +39,26 @@ pub async fn start(config: crate::config::Config) -> AppResult<()> {
&config.vapid_private_key.clone(), &config.vapid_private_key.clone(),
)?); )?);
let (verifier, authenticator) = setup_auth(&config).await?;
let calendar_client = Arc::new(crate::domain::calendar::CalendarClient::new(
config.calendar_api_url.clone(),
authenticator.clone(),
));
let scheduler = Arc::new( let scheduler = Arc::new(
Scheduler::new(db.clone(), config.clone(), tx.clone(), push_sender.clone()) Scheduler::new(
.await db.clone(),
.map_err(|e| crate::error::AppError::Internal(e.to_string()))?, config.clone(),
calendar_client.clone(),
tx.clone(),
push_sender.clone(),
)
.await
.map_err(|e| crate::error::AppError::Internal(e.to_string()))?,
); );
// Load existing scheduled tasks // Load existing scheduled tasks
use crate::entities::task::Entity as Task;
let existing_tasks = Task::find() let existing_tasks = Task::find()
.all(&db) .all(&db)
.await .await
@ -56,14 +69,13 @@ pub async fn start(config: crate::config::Config) -> AppResult<()> {
} }
} }
let (verifier, authenticator) = setup_auth(&config).await?;
let state = Arc::new(AppState { let state = Arc::new(AppState {
db, db,
scheduler, scheduler,
config: config.clone(), config: config.clone(),
verifier, verifier,
authenticator, authenticator,
calendar_client,
tx, tx,
}); });
@ -136,6 +148,7 @@ fn build_app(state: Arc<AppState>, config: &crate::config::Config) -> Router {
.route("/api/notifications/vapid-key", get(notifications::push_handlers::get_vapid_key)) .route("/api/notifications/vapid-key", get(notifications::push_handlers::get_vapid_key))
.route("/api/tasks/:id/subscription", get(notifications::push_handlers::get_subscription_status)) .route("/api/tasks/:id/subscription", get(notifications::push_handlers::get_subscription_status))
.route("/api/tasks/:id/subscribe", post(notifications::push_handlers::subscribe_task).delete(notifications::push_handlers::unsubscribe_task)) .route("/api/tasks/:id/subscribe", post(notifications::push_handlers::subscribe_task).delete(notifications::push_handlers::unsubscribe_task))
.layer(axum::middleware::from_fn(log_error_responses))
.layer(cors) .layer(cors)
.layer(tower_http::set_header::SetResponseHeaderLayer::overriding( .layer(tower_http::set_header::SetResponseHeaderLayer::overriding(
axum::http::header::CONTENT_SECURITY_POLICY, axum::http::header::CONTENT_SECURITY_POLICY,
@ -153,6 +166,22 @@ fn build_app(state: Arc<AppState>, config: &crate::config::Config) -> Router {
.with_state(state) .with_state(state)
} }
async fn log_error_responses(
req: axum::extract::Request,
next: axum::middleware::Next,
) -> axum::response::Response {
let method = req.method().clone();
let uri = req.uri().clone();
let res = next.run(req).await;
let status = res.status();
if status.is_client_error() || status.is_server_error() {
tracing::error!(%method, %uri, %status, "Response error");
}
res
}
fn build_cors_layer(config: &crate::config::Config) -> CorsLayer { fn build_cors_layer(config: &crate::config::Config) -> CorsLayer {
let allow_origin = if let Some(origins) = &config.cors_allowed_origins { let allow_origin = if let Some(origins) = &config.cors_allowed_origins {
let values: Vec<HeaderValue> = origins let values: Vec<HeaderValue> = origins

View file

@ -109,6 +109,7 @@ pub async fn rerun_task(
&state.db, &state.db,
&state.scheduler, &state.scheduler,
&state.config, &state.config,
state.calendar_client.clone(),
task.id, task.id,
task.goal, task.goal,
) )