Some checks failed
docker-images / resolve-build-targets (push) Successful in 7s
ui-regression / playwright-regression (push) Failing after 13m4s
docker-images / build-and-push (admin) (push) Successful in 1m17s
docker-images / build-and-push (backend) (push) Successful in 28m13s
docker-images / build-and-push (frontend) (push) Successful in 47s
docker-images / submit-indexnow (push) Successful in 13s
style: enhance global CSS for better responsiveness of terminal chips and navigation pills test: remove inline subscription test and add maintenance mode access code test feat: implement media library picker dialog for selecting images from the media library feat: add media URL controls for uploading and managing media assets feat: add migration for music_enabled and maintenance_mode settings in site settings feat: implement maintenance mode functionality with access control feat: create maintenance page with access code input and error handling chore: add TypeScript declaration for QR code module
717 lines
20 KiB
Rust
717 lines
20 KiB
Rust
use axum::http::HeaderMap;
|
|
use loco_rs::prelude::*;
|
|
use sea_orm::{
|
|
ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, Order, QueryFilter, QueryOrder,
|
|
QuerySelect, Set,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::{
|
|
controllers::admin::check_auth,
|
|
models::_entities::{admin_audit_logs, notification_deliveries, post_revisions, subscriptions},
|
|
services::{
|
|
admin_audit, backups, post_revisions as revision_service,
|
|
subscriptions as subscription_service, worker_jobs,
|
|
},
|
|
};
|
|
|
|
#[derive(Clone, Debug, Default, Deserialize)]
|
|
pub struct AuditLogQuery {
|
|
pub action: Option<String>,
|
|
pub target_type: Option<String>,
|
|
pub limit: Option<u64>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, Deserialize)]
|
|
pub struct RevisionQuery {
|
|
pub slug: Option<String>,
|
|
pub limit: Option<u64>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, Deserialize)]
|
|
pub struct DeliveriesQuery {
|
|
pub limit: Option<u64>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, Deserialize)]
|
|
pub struct WorkerJobsQuery {
|
|
pub status: Option<String>,
|
|
pub job_kind: Option<String>,
|
|
pub worker_name: Option<String>,
|
|
pub search: Option<String>,
|
|
pub limit: Option<u64>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
pub struct SubscriptionPayload {
|
|
#[serde(alias = "channelType")]
|
|
pub channel_type: String,
|
|
pub target: String,
|
|
#[serde(default, alias = "displayName")]
|
|
pub display_name: Option<String>,
|
|
#[serde(default)]
|
|
pub status: Option<String>,
|
|
#[serde(default)]
|
|
pub filters: Option<serde_json::Value>,
|
|
#[serde(default)]
|
|
pub metadata: Option<serde_json::Value>,
|
|
#[serde(default)]
|
|
pub secret: Option<String>,
|
|
#[serde(default)]
|
|
pub notes: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
pub struct SubscriptionUpdatePayload {
|
|
#[serde(default, alias = "channelType")]
|
|
pub channel_type: Option<String>,
|
|
#[serde(default)]
|
|
pub target: Option<String>,
|
|
#[serde(default, alias = "displayName")]
|
|
pub display_name: Option<String>,
|
|
#[serde(default)]
|
|
pub status: Option<String>,
|
|
#[serde(default)]
|
|
pub filters: Option<serde_json::Value>,
|
|
#[serde(default)]
|
|
pub metadata: Option<serde_json::Value>,
|
|
#[serde(default)]
|
|
pub secret: Option<String>,
|
|
#[serde(default)]
|
|
pub notes: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
pub struct RestoreRevisionRequest {
|
|
#[serde(default)]
|
|
pub mode: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
pub struct DigestDispatchRequest {
|
|
pub period: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, Deserialize)]
|
|
pub struct RetryDeliveriesRequest {
|
|
pub limit: Option<u64>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
pub struct SiteBackupImportRequest {
|
|
pub backup: backups::SiteBackupDocument,
|
|
#[serde(default)]
|
|
pub mode: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize)]
|
|
pub struct PostRevisionListItem {
|
|
pub id: i32,
|
|
pub post_slug: String,
|
|
pub post_title: Option<String>,
|
|
pub operation: String,
|
|
pub revision_reason: Option<String>,
|
|
pub actor_username: Option<String>,
|
|
pub actor_email: Option<String>,
|
|
pub actor_source: Option<String>,
|
|
pub created_at: String,
|
|
pub has_markdown: bool,
|
|
pub metadata: Option<serde_json::Value>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize)]
|
|
pub struct PostRevisionDetailResponse {
|
|
#[serde(flatten)]
|
|
pub item: PostRevisionListItem,
|
|
pub markdown: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize)]
|
|
pub struct RestoreRevisionResponse {
|
|
pub restored: bool,
|
|
pub revision_id: i32,
|
|
pub post_slug: String,
|
|
pub mode: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize)]
|
|
pub struct SubscriptionListResponse {
|
|
pub subscriptions: Vec<subscriptions::Model>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize)]
|
|
pub struct DeliveryListResponse {
|
|
pub deliveries: Vec<notification_deliveries::Model>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize)]
|
|
pub struct WorkerTaskActionResponse {
|
|
pub queued: bool,
|
|
pub job: worker_jobs::WorkerJobRecord,
|
|
}
|
|
|
|
fn trim_to_option(value: Option<String>) -> Option<String> {
|
|
value.and_then(|item| {
|
|
let trimmed = item.trim().to_string();
|
|
if trimmed.is_empty() {
|
|
None
|
|
} else {
|
|
Some(trimmed)
|
|
}
|
|
})
|
|
}
|
|
|
|
fn format_revision(item: post_revisions::Model) -> PostRevisionListItem {
|
|
PostRevisionListItem {
|
|
id: item.id,
|
|
post_slug: item.post_slug,
|
|
post_title: item.post_title,
|
|
operation: item.operation,
|
|
revision_reason: item.revision_reason,
|
|
actor_username: item.actor_username,
|
|
actor_email: item.actor_email,
|
|
actor_source: item.actor_source,
|
|
created_at: item.created_at.format("%Y-%m-%d %H:%M:%S").to_string(),
|
|
has_markdown: item
|
|
.markdown
|
|
.as_deref()
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.is_some(),
|
|
metadata: item.metadata,
|
|
}
|
|
}
|
|
|
|
#[debug_handler]
|
|
pub async fn list_audit_logs(
|
|
headers: HeaderMap,
|
|
Query(query): Query<AuditLogQuery>,
|
|
State(ctx): State<AppContext>,
|
|
) -> Result<Response> {
|
|
check_auth(&headers)?;
|
|
|
|
let mut db_query =
|
|
admin_audit_logs::Entity::find().order_by(admin_audit_logs::Column::CreatedAt, Order::Desc);
|
|
|
|
if let Some(action) = query
|
|
.action
|
|
.map(|value| value.trim().to_string())
|
|
.filter(|value| !value.is_empty())
|
|
{
|
|
db_query = db_query.filter(admin_audit_logs::Column::Action.eq(action));
|
|
}
|
|
|
|
if let Some(target_type) = query
|
|
.target_type
|
|
.map(|value| value.trim().to_string())
|
|
.filter(|value| !value.is_empty())
|
|
{
|
|
db_query = db_query.filter(admin_audit_logs::Column::TargetType.eq(target_type));
|
|
}
|
|
|
|
format::json(
|
|
db_query
|
|
.limit(query.limit.unwrap_or(80))
|
|
.all(&ctx.db)
|
|
.await?,
|
|
)
|
|
}
|
|
|
|
#[debug_handler]
|
|
pub async fn list_post_revisions(
|
|
headers: HeaderMap,
|
|
Query(query): Query<RevisionQuery>,
|
|
State(ctx): State<AppContext>,
|
|
) -> Result<Response> {
|
|
check_auth(&headers)?;
|
|
let items =
|
|
revision_service::list_revisions(&ctx, query.slug.as_deref(), query.limit.unwrap_or(120))
|
|
.await?;
|
|
format::json(items.into_iter().map(format_revision).collect::<Vec<_>>())
|
|
}
|
|
|
|
#[debug_handler]
|
|
pub async fn get_post_revision(
|
|
headers: HeaderMap,
|
|
Path(id): Path<i32>,
|
|
State(ctx): State<AppContext>,
|
|
) -> Result<Response> {
|
|
check_auth(&headers)?;
|
|
let item = revision_service::get_revision(&ctx, id).await?;
|
|
format::json(PostRevisionDetailResponse {
|
|
item: format_revision(item.clone()),
|
|
markdown: item.markdown,
|
|
})
|
|
}
|
|
|
|
#[debug_handler]
|
|
pub async fn restore_post_revision(
|
|
headers: HeaderMap,
|
|
Path(id): Path<i32>,
|
|
State(ctx): State<AppContext>,
|
|
Json(payload): Json<RestoreRevisionRequest>,
|
|
) -> Result<Response> {
|
|
let actor = check_auth(&headers)?;
|
|
let mode = payload.mode.unwrap_or_else(|| "full".to_string());
|
|
let restored = revision_service::restore_revision(&ctx, Some(&actor), id, &mode).await?;
|
|
admin_audit::log_event(
|
|
&ctx,
|
|
Some(&actor),
|
|
"post.revision.restore",
|
|
"post_revision",
|
|
Some(restored.id.to_string()),
|
|
Some(restored.post_slug.clone()),
|
|
Some(serde_json::json!({
|
|
"post_slug": restored.post_slug,
|
|
"source_revision_id": id,
|
|
"mode": mode,
|
|
})),
|
|
)
|
|
.await?;
|
|
|
|
format::json(RestoreRevisionResponse {
|
|
restored: true,
|
|
revision_id: id,
|
|
post_slug: restored.post_slug,
|
|
mode,
|
|
})
|
|
}
|
|
|
|
#[debug_handler]
|
|
pub async fn list_subscriptions(
|
|
headers: HeaderMap,
|
|
State(ctx): State<AppContext>,
|
|
) -> Result<Response> {
|
|
check_auth(&headers)?;
|
|
format::json(SubscriptionListResponse {
|
|
subscriptions: subscription_service::list_subscriptions(&ctx, None, None).await?,
|
|
})
|
|
}
|
|
|
|
#[debug_handler]
|
|
pub async fn list_subscription_deliveries(
|
|
headers: HeaderMap,
|
|
Query(query): Query<DeliveriesQuery>,
|
|
State(ctx): State<AppContext>,
|
|
) -> Result<Response> {
|
|
check_auth(&headers)?;
|
|
format::json(DeliveryListResponse {
|
|
deliveries: subscription_service::list_recent_deliveries(&ctx, query.limit.unwrap_or(80))
|
|
.await?,
|
|
})
|
|
}
|
|
|
|
#[debug_handler]
|
|
pub async fn create_subscription(
|
|
headers: HeaderMap,
|
|
State(ctx): State<AppContext>,
|
|
Json(payload): Json<SubscriptionPayload>,
|
|
) -> Result<Response> {
|
|
let actor = check_auth(&headers)?;
|
|
|
|
let channel_type = subscription_service::normalize_channel_type(&payload.channel_type);
|
|
let target = payload.target.trim().to_string();
|
|
if target.is_empty() {
|
|
return Err(Error::BadRequest("target 不能为空".to_string()));
|
|
}
|
|
|
|
let created = subscriptions::ActiveModel {
|
|
channel_type: Set(channel_type.clone()),
|
|
target: Set(target.clone()),
|
|
display_name: Set(trim_to_option(payload.display_name)),
|
|
status: Set(subscription_service::normalize_status(
|
|
payload.status.as_deref().unwrap_or("active"),
|
|
)),
|
|
filters: Set(subscription_service::normalize_filters(payload.filters)),
|
|
metadata: Set(payload.metadata),
|
|
secret: Set(trim_to_option(payload.secret)),
|
|
notes: Set(trim_to_option(payload.notes)),
|
|
confirm_token: Set(None),
|
|
manage_token: Set(Some(subscription_service::generate_subscription_token())),
|
|
verified_at: Set(Some(chrono::Utc::now().to_rfc3339())),
|
|
failure_count: Set(Some(0)),
|
|
..Default::default()
|
|
}
|
|
.insert(&ctx.db)
|
|
.await?;
|
|
|
|
admin_audit::log_event(
|
|
&ctx,
|
|
Some(&actor),
|
|
"subscription.create",
|
|
"subscription",
|
|
Some(created.id.to_string()),
|
|
Some(format!("{}:{}", created.channel_type, created.target)),
|
|
Some(serde_json::json!({ "channel_type": created.channel_type, "target": created.target })),
|
|
)
|
|
.await?;
|
|
|
|
format::json(created)
|
|
}
|
|
|
|
#[debug_handler]
|
|
pub async fn update_subscription(
|
|
headers: HeaderMap,
|
|
Path(id): Path<i32>,
|
|
State(ctx): State<AppContext>,
|
|
Json(payload): Json<SubscriptionUpdatePayload>,
|
|
) -> Result<Response> {
|
|
let actor = check_auth(&headers)?;
|
|
|
|
let item = subscriptions::Entity::find_by_id(id)
|
|
.one(&ctx.db)
|
|
.await?
|
|
.ok_or(Error::NotFound)?;
|
|
let mut active = item.clone().into_active_model();
|
|
|
|
if let Some(channel_type) = payload.channel_type {
|
|
active.channel_type = Set(subscription_service::normalize_channel_type(&channel_type));
|
|
}
|
|
if let Some(target) = payload.target {
|
|
let normalized_target = target.trim().to_string();
|
|
if normalized_target.is_empty() {
|
|
return Err(Error::BadRequest("target 不能为空".to_string()));
|
|
}
|
|
active.target = Set(normalized_target);
|
|
}
|
|
if payload.display_name.is_some() {
|
|
active.display_name = Set(trim_to_option(payload.display_name));
|
|
}
|
|
if let Some(status) = payload.status {
|
|
active.status = Set(subscription_service::normalize_status(&status));
|
|
}
|
|
if payload.filters.is_some() {
|
|
active.filters = Set(subscription_service::normalize_filters(payload.filters));
|
|
}
|
|
if payload.metadata.is_some() {
|
|
active.metadata = Set(payload.metadata);
|
|
}
|
|
if payload.secret.is_some() {
|
|
active.secret = Set(trim_to_option(payload.secret));
|
|
}
|
|
if payload.notes.is_some() {
|
|
active.notes = Set(trim_to_option(payload.notes));
|
|
}
|
|
|
|
let updated = active.update(&ctx.db).await?;
|
|
admin_audit::log_event(
|
|
&ctx,
|
|
Some(&actor),
|
|
"subscription.update",
|
|
"subscription",
|
|
Some(updated.id.to_string()),
|
|
Some(format!("{}:{}", updated.channel_type, updated.target)),
|
|
None,
|
|
)
|
|
.await?;
|
|
|
|
format::json(updated)
|
|
}
|
|
|
|
#[debug_handler]
|
|
pub async fn delete_subscription(
|
|
headers: HeaderMap,
|
|
Path(id): Path<i32>,
|
|
State(ctx): State<AppContext>,
|
|
) -> Result<Response> {
|
|
let actor = check_auth(&headers)?;
|
|
let item = subscriptions::Entity::find_by_id(id)
|
|
.one(&ctx.db)
|
|
.await?
|
|
.ok_or(Error::NotFound)?;
|
|
let label = format!("{}:{}", item.channel_type, item.target);
|
|
item.delete(&ctx.db).await?;
|
|
|
|
admin_audit::log_event(
|
|
&ctx,
|
|
Some(&actor),
|
|
"subscription.delete",
|
|
"subscription",
|
|
Some(id.to_string()),
|
|
Some(label),
|
|
None,
|
|
)
|
|
.await?;
|
|
|
|
format::empty()
|
|
}
|
|
|
|
#[debug_handler]
|
|
pub async fn test_subscription(
|
|
headers: HeaderMap,
|
|
Path(id): Path<i32>,
|
|
State(ctx): State<AppContext>,
|
|
) -> Result<Response> {
|
|
let actor = check_auth(&headers)?;
|
|
let item = subscriptions::Entity::find_by_id(id)
|
|
.one(&ctx.db)
|
|
.await?
|
|
.ok_or(Error::NotFound)?;
|
|
|
|
let delivery = subscription_service::send_test_notification(&ctx, &item).await?;
|
|
let job = worker_jobs::find_latest_job_by_related_entity(
|
|
&ctx,
|
|
"notification_delivery",
|
|
&delivery.id.to_string(),
|
|
Some(worker_jobs::WORKER_NOTIFICATION_DELIVERY),
|
|
)
|
|
.await?;
|
|
admin_audit::log_event(
|
|
&ctx,
|
|
Some(&actor),
|
|
"subscription.test",
|
|
"subscription",
|
|
Some(item.id.to_string()),
|
|
Some(format!("{}:{}", item.channel_type, item.target)),
|
|
None,
|
|
)
|
|
.await?;
|
|
|
|
format::json(serde_json::json!({
|
|
"queued": true,
|
|
"id": item.id,
|
|
"delivery_id": delivery.id,
|
|
"job_id": job.as_ref().map(|value| value.id),
|
|
}))
|
|
}
|
|
|
|
#[debug_handler]
|
|
pub async fn send_subscription_digest(
|
|
headers: HeaderMap,
|
|
State(ctx): State<AppContext>,
|
|
Json(payload): Json<DigestDispatchRequest>,
|
|
) -> Result<Response> {
|
|
let actor = check_auth(&headers)?;
|
|
let summary =
|
|
subscription_service::send_digest(&ctx, payload.period.as_deref().unwrap_or("weekly"))
|
|
.await?;
|
|
|
|
admin_audit::log_event(
|
|
&ctx,
|
|
Some(&actor),
|
|
"subscription.digest.send",
|
|
"subscription_digest",
|
|
None,
|
|
Some(summary.period.clone()),
|
|
Some(serde_json::json!({
|
|
"period": summary.period,
|
|
"post_count": summary.post_count,
|
|
"queued": summary.queued,
|
|
"skipped": summary.skipped,
|
|
})),
|
|
)
|
|
.await?;
|
|
|
|
format::json(summary)
|
|
}
|
|
|
|
#[debug_handler]
|
|
pub async fn workers_overview(
|
|
headers: HeaderMap,
|
|
State(ctx): State<AppContext>,
|
|
) -> Result<Response> {
|
|
check_auth(&headers)?;
|
|
format::json(worker_jobs::get_overview(&ctx).await?)
|
|
}
|
|
|
|
#[debug_handler]
|
|
pub async fn list_worker_jobs(
|
|
headers: HeaderMap,
|
|
Query(query): Query<WorkerJobsQuery>,
|
|
State(ctx): State<AppContext>,
|
|
) -> Result<Response> {
|
|
check_auth(&headers)?;
|
|
format::json(
|
|
worker_jobs::list_jobs(
|
|
&ctx,
|
|
worker_jobs::WorkerJobListQuery {
|
|
status: query.status,
|
|
job_kind: query.job_kind,
|
|
worker_name: query.worker_name,
|
|
search: query.search,
|
|
limit: query.limit,
|
|
},
|
|
)
|
|
.await?,
|
|
)
|
|
}
|
|
|
|
#[debug_handler]
|
|
pub async fn get_worker_job(
|
|
headers: HeaderMap,
|
|
Path(id): Path<i32>,
|
|
State(ctx): State<AppContext>,
|
|
) -> Result<Response> {
|
|
check_auth(&headers)?;
|
|
format::json(worker_jobs::get_job_record(&ctx, id).await?)
|
|
}
|
|
|
|
#[debug_handler]
|
|
pub async fn cancel_worker_job(
|
|
headers: HeaderMap,
|
|
Path(id): Path<i32>,
|
|
State(ctx): State<AppContext>,
|
|
) -> Result<Response> {
|
|
let actor = check_auth(&headers)?;
|
|
let updated = worker_jobs::request_cancel(&ctx, id).await?;
|
|
|
|
admin_audit::log_event(
|
|
&ctx,
|
|
Some(&actor),
|
|
"worker.cancel",
|
|
"worker_job",
|
|
Some(id.to_string()),
|
|
Some(updated.worker_name.clone()),
|
|
Some(serde_json::json!({ "status": updated.status })),
|
|
)
|
|
.await?;
|
|
|
|
format::json(updated)
|
|
}
|
|
|
|
#[debug_handler]
|
|
pub async fn retry_worker_job(
|
|
headers: HeaderMap,
|
|
Path(id): Path<i32>,
|
|
State(ctx): State<AppContext>,
|
|
) -> Result<Response> {
|
|
let actor = check_auth(&headers)?;
|
|
let job = worker_jobs::retry_job(
|
|
&ctx,
|
|
id,
|
|
Some(actor.username.clone()),
|
|
Some(actor.source.clone()),
|
|
)
|
|
.await?;
|
|
|
|
admin_audit::log_event(
|
|
&ctx,
|
|
Some(&actor),
|
|
"worker.retry",
|
|
"worker_job",
|
|
Some(job.id.to_string()),
|
|
Some(job.worker_name.clone()),
|
|
Some(serde_json::json!({ "source_job_id": id })),
|
|
)
|
|
.await?;
|
|
|
|
format::json(WorkerTaskActionResponse { queued: true, job })
|
|
}
|
|
|
|
#[debug_handler]
|
|
pub async fn run_retry_deliveries_job(
|
|
headers: HeaderMap,
|
|
State(ctx): State<AppContext>,
|
|
Json(payload): Json<RetryDeliveriesRequest>,
|
|
) -> Result<Response> {
|
|
let actor = check_auth(&headers)?;
|
|
let job = worker_jobs::spawn_retry_deliveries_task(
|
|
&ctx,
|
|
payload.limit,
|
|
Some(actor.username.clone()),
|
|
Some(actor.source.clone()),
|
|
None,
|
|
Some("manual".to_string()),
|
|
)
|
|
.await?;
|
|
|
|
admin_audit::log_event(
|
|
&ctx,
|
|
Some(&actor),
|
|
"worker.task.retry_deliveries",
|
|
"worker_job",
|
|
Some(job.id.to_string()),
|
|
Some(job.worker_name.clone()),
|
|
Some(serde_json::json!({ "limit": payload.limit })),
|
|
)
|
|
.await?;
|
|
|
|
format::json(WorkerTaskActionResponse { queued: true, job })
|
|
}
|
|
|
|
#[debug_handler]
|
|
pub async fn run_digest_worker_job(
|
|
headers: HeaderMap,
|
|
State(ctx): State<AppContext>,
|
|
Json(payload): Json<DigestDispatchRequest>,
|
|
) -> Result<Response> {
|
|
let actor = check_auth(&headers)?;
|
|
let period = payload.period.unwrap_or_else(|| "weekly".to_string());
|
|
let job = worker_jobs::spawn_digest_task(
|
|
&ctx,
|
|
&period,
|
|
Some(actor.username.clone()),
|
|
Some(actor.source.clone()),
|
|
None,
|
|
Some("manual".to_string()),
|
|
)
|
|
.await?;
|
|
|
|
admin_audit::log_event(
|
|
&ctx,
|
|
Some(&actor),
|
|
"worker.task.digest",
|
|
"worker_job",
|
|
Some(job.id.to_string()),
|
|
Some(job.worker_name.clone()),
|
|
Some(serde_json::json!({ "period": period })),
|
|
)
|
|
.await?;
|
|
|
|
format::json(WorkerTaskActionResponse { queued: true, job })
|
|
}
|
|
|
|
#[debug_handler]
|
|
pub async fn export_site_backup(
|
|
headers: HeaderMap,
|
|
State(ctx): State<AppContext>,
|
|
) -> Result<Response> {
|
|
check_auth(&headers)?;
|
|
format::json(backups::export_site_backup(&ctx).await?)
|
|
}
|
|
|
|
#[debug_handler]
|
|
pub async fn import_site_backup(
|
|
headers: HeaderMap,
|
|
State(ctx): State<AppContext>,
|
|
Json(payload): Json<SiteBackupImportRequest>,
|
|
) -> Result<Response> {
|
|
check_auth(&headers)?;
|
|
format::json(backups::import_site_backup(&ctx, payload.backup, payload.mode.as_deref()).await?)
|
|
}
|
|
|
|
pub fn routes() -> Routes {
|
|
Routes::new()
|
|
.prefix("/api/admin")
|
|
.add("/audit-logs", get(list_audit_logs))
|
|
.add("/post-revisions", get(list_post_revisions))
|
|
.add("/post-revisions/{id}", get(get_post_revision))
|
|
.add("/post-revisions/{id}/restore", post(restore_post_revision))
|
|
.add(
|
|
"/subscriptions",
|
|
get(list_subscriptions).post(create_subscription),
|
|
)
|
|
.add(
|
|
"/subscriptions/deliveries",
|
|
get(list_subscription_deliveries),
|
|
)
|
|
.add("/subscriptions/digest", post(send_subscription_digest))
|
|
.add(
|
|
"/subscriptions/{id}",
|
|
patch(update_subscription).delete(delete_subscription),
|
|
)
|
|
.add("/subscriptions/{id}/test", post(test_subscription))
|
|
.add("/workers/overview", get(workers_overview))
|
|
.add("/workers/jobs", get(list_worker_jobs))
|
|
.add("/workers/jobs/{id}", get(get_worker_job))
|
|
.add("/workers/jobs/{id}/cancel", post(cancel_worker_job))
|
|
.add("/workers/jobs/{id}/retry", post(retry_worker_job))
|
|
.add(
|
|
"/workers/tasks/retry-deliveries",
|
|
post(run_retry_deliveries_job),
|
|
)
|
|
.add("/workers/tasks/digest", post(run_digest_worker_job))
|
|
.add("/site-backup/export", get(export_site_backup))
|
|
.add("/site-backup/import", post(import_site_backup))
|
|
}
|