use axum::Json;
use axum::extract::{Path, Query, State};
use crate::AppState;
use crate::dto::{
BatchRequest, BatchResponse, CreateCronRequest, LogQueryParams, ToggleBody, UpdateCronRequest,
};
use crate::errors::app_error::{AppError, AppResult};
use crate::errors::response::ApiResponse;
use crate::middleware::auth::AuthUser;
use crate::types::snowflake_id::SnowflakeId;
use crate::utils::pagination::PaginationParams;
use crate::worker::{
CronSchedule, cleanup_execution_logs, create_schedule, delete_schedule, find_by_id,
list_execution_logs, list_schedules, recent_execution_logs, toggle_schedule, update_schedule,
};
pub fn routes(
registry: &mut crate::server::RouteRegistry,
config: &crate::config::app::AppConfig,
) -> axum::Router<crate::AppState> {
let restful = config.api_restful;
let r = axum::Router::new();
let r = reg_route!(
r,
registry,
restful,
"/admin/crons",
get,
self::list,
"system admin",
"admin/crons"
);
let r = reg_route!(
r,
registry,
restful,
"/admin/crons",
create,
self::create,
"system admin",
"admin/crons"
);
let r = reg_route!(
r,
registry,
restful,
"/admin/crons/{id}",
get,
self::get,
"system admin",
"admin/crons"
);
let r = reg_route!(
r,
registry,
restful,
"/admin/crons/{id}",
put,
update,
"system admin",
"admin/crons"
);
let r = reg_route!(
r,
registry,
restful,
"/admin/crons/{id}",
delete,
self::delete,
"system admin",
"admin/crons"
);
let r = reg_route!(
r,
registry,
restful,
"/admin/crons/{id}/toggle",
post,
toggle,
"system admin",
"admin/crons"
);
let r = reg_route!(
r,
registry,
restful,
"/admin/crons/logs",
get,
logs,
"system admin",
"admin/crons"
);
let r = reg_route!(
r,
registry,
restful,
"/admin/crons/logs/cleanup",
post,
cleanup_logs,
"system admin",
"admin/crons"
);
reg_route!(
r,
registry,
restful,
"/admin/crons/batch",
post,
admin_batch,
"system admin",
"admin/crons"
)
}
#[utoipa::path(get, path = "/admin/crons", tag = "cron",
security(("bearer_auth" = [])),
responses((status = 200, description = "List cron schedules"))
)]
pub async fn list(
auth: AuthUser,
State(state): State<AppState>,
Query(mut params): Query<PaginationParams>,
) -> AppResult<ApiResponse<crate::errors::response::PaginatedData<CronSchedule>>> {
auth.ensure_admin()?;
params.sanitize();
let all = list_schedules(&state.pool).await?;
let total = all.len() as i64;
let offset = params.offset() as usize;
let items: Vec<_> = all
.into_iter()
.skip(offset)
.take(params.page_size as usize)
.collect();
Ok(params.paginate(items, total))
}
#[utoipa::path(get, path = "/admin/crons/{id}", tag = "cron",
security(("bearer_auth" = [])),
params(("id" = String, Path, description = "Schedule ID")),
responses((status = 200, description = "Schedule detail"))
)]
pub async fn get(
auth: AuthUser,
State(state): State<AppState>,
Path(id): Path<String>,
) -> AppResult<ApiResponse<CronSchedule>> {
auth.ensure_admin()?;
let id = crate::types::snowflake_id::parse_id(&id)?;
let schedule = find_by_id(&state.pool, id)
.await?
.ok_or_else(|| AppError::not_found("cron_schedule"))?;
Ok(ApiResponse::success(schedule))
}
#[utoipa::path(post, path = "/admin/crons", tag = "cron",
security(("bearer_auth" = [])),
request_body = serde_json::Value,
responses((status = 200, description = "Schedule created"))
)]
pub async fn create(
auth: AuthUser,
State(state): State<AppState>,
Json(req): Json<CreateCronRequest>,
) -> AppResult<ApiResponse<CronSchedule>> {
auth.ensure_admin()?;
crate::errors::validation::validate(&req)?;
let schedule = create_schedule(
&state.pool,
&req.label,
&req.job_type,
req.payload.as_deref(),
&req.cron_expr,
req.enabled,
)
.await?;
Ok(ApiResponse::success(schedule))
}
#[utoipa::path(put, path = "/admin/crons/{id}", tag = "cron",
security(("bearer_auth" = [])),
params(("id" = String, Path, description = "Schedule ID")),
request_body = serde_json::Value,
responses((status = 200, description = "Schedule updated"))
)]
pub async fn update(
auth: AuthUser,
State(state): State<AppState>,
Path(id): Path<String>,
Json(req): Json<UpdateCronRequest>,
) -> AppResult<ApiResponse<CronSchedule>> {
auth.ensure_admin()?;
crate::errors::validation::validate(&req)?;
let id = crate::types::snowflake_id::parse_id(&id)?;
let updated = update_schedule(
&state.pool,
id,
req.label,
req.job_type,
req.payload,
req.cron_expr,
req.enabled,
)
.await?;
Ok(ApiResponse::success(updated))
}
#[utoipa::path(post, path = "/admin/crons/{id}/toggle", tag = "cron",
security(("bearer_auth" = [])),
params(("id" = String, Path, description = "Schedule ID")),
request_body = serde_json::Value,
responses((status = 200, description = "Schedule toggled"))
)]
pub async fn toggle(
auth: AuthUser,
State(state): State<AppState>,
Path(id): Path<String>,
Json(body): Json<ToggleBody>,
) -> AppResult<ApiResponse<()>> {
auth.ensure_admin()?;
let id = crate::types::snowflake_id::parse_id(&id)?;
toggle_schedule(&state.pool, id, body.enabled).await?;
Ok(ApiResponse::success(()))
}
#[utoipa::path(delete, path = "/admin/crons/{id}", tag = "cron",
security(("bearer_auth" = [])),
params(("id" = String, Path, description = "Schedule ID")),
responses((status = 200, description = "Schedule deleted"))
)]
pub async fn delete(
auth: AuthUser,
State(state): State<AppState>,
Path(id): Path<String>,
) -> AppResult<ApiResponse<()>> {
auth.ensure_admin()?;
let id = crate::types::snowflake_id::parse_id(&id)?;
delete_schedule(&state.pool, id).await?;
Ok(ApiResponse::success(()))
}
#[utoipa::path(get, path = "/admin/crons/logs", tag = "cron",
security(("bearer_auth" = [])),
responses((status = 200, description = "Execution logs"))
)]
pub async fn logs(
auth: AuthUser,
State(state): State<AppState>,
Query(params): Query<LogQueryParams>,
) -> AppResult<ApiResponse<Vec<crate::worker::CronExecutionLog>>> {
auth.ensure_admin()?;
let limit = params.limit.clamp(1, 100);
let logs = if let Some(ref schedule_id) = params.schedule_id {
let sid = crate::types::snowflake_id::parse_id(schedule_id)?;
list_execution_logs(&state.pool, sid, limit).await?
} else {
recent_execution_logs(&state.pool, limit).await?
};
Ok(ApiResponse::success(logs))
}
#[utoipa::path(post, path = "/admin/crons/logs/cleanup", tag = "cron",
security(("bearer_auth" = [])),
responses((status = 200, description = "Expired logs cleaned up"))
)]
pub async fn cleanup_logs(
auth: AuthUser,
State(state): State<AppState>,
) -> AppResult<ApiResponse<u64>> {
auth.ensure_admin()?;
let days = state.config.cron_log_retention_days;
let count = cleanup_execution_logs(&state.pool, days).await?;
Ok(ApiResponse::success(count))
}
#[utoipa::path(post, path = "/admin/crons/batch", tag = "cron",
security(("bearer_auth" = [])),
request_body = BatchRequest,
responses((status = 200, description = "Batch operation completed"))
)]
pub async fn admin_batch(
auth: AuthUser,
State(state): State<AppState>,
Json(req): Json<BatchRequest>,
) -> AppResult<ApiResponse<BatchResponse>> {
auth.ensure_admin()?;
crate::errors::validation::validate(&req)?;
let mut affected = 0usize;
for id_str in &req.ids {
let id = match id_str.parse::<i64>() {
Ok(v) => SnowflakeId(v),
Err(_) => continue,
};
match req.action.as_str() {
"delete" if delete_schedule(&state.pool, id).await.is_ok() => {
affected += 1;
}
"enable" if toggle_schedule(&state.pool, id, true).await.is_ok() => {
affected += 1;
}
"disable" if toggle_schedule(&state.pool, id, false).await.is_ok() => {
affected += 1;
}
_ => {}
}
}
Ok(ApiResponse::success(BatchResponse::new(
&req.action,
affected,
)))
}