feat: ship blog platform admin and deploy stack
This commit is contained in:
202
backend/src/controllers/subscription.rs
Normal file
202
backend/src/controllers/subscription.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
use loco_rs::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::services::{abuse_guard, admin_audit, subscriptions};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct PublicSubscriptionPayload {
|
||||
pub email: String,
|
||||
#[serde(default, alias = "displayName")]
|
||||
pub display_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub source: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct SubscriptionTokenPayload {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct SubscriptionManageQuery {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct SubscriptionManageUpdatePayload {
|
||||
pub token: String,
|
||||
#[serde(default, alias = "displayName")]
|
||||
pub display_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub status: Option<String>,
|
||||
#[serde(default)]
|
||||
pub filters: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct PublicSubscriptionResponse {
|
||||
pub ok: bool,
|
||||
pub subscription_id: i32,
|
||||
pub status: String,
|
||||
pub requires_confirmation: bool,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct SubscriptionManageResponse {
|
||||
pub ok: bool,
|
||||
pub subscription: subscriptions::PublicSubscriptionView,
|
||||
}
|
||||
|
||||
fn public_subscription_metadata(source: Option<String>) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"source": source,
|
||||
"kind": "public-form",
|
||||
})
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn subscribe(
|
||||
State(ctx): State<AppContext>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Json(payload): Json<PublicSubscriptionPayload>,
|
||||
) -> Result<Response> {
|
||||
let email = payload.email.trim().to_ascii_lowercase();
|
||||
abuse_guard::enforce_public_scope(
|
||||
"subscription",
|
||||
abuse_guard::detect_client_ip(&headers).as_deref(),
|
||||
Some(&email),
|
||||
)?;
|
||||
|
||||
let result = subscriptions::create_public_email_subscription(
|
||||
&ctx,
|
||||
&email,
|
||||
payload.display_name,
|
||||
Some(public_subscription_metadata(payload.source)),
|
||||
)
|
||||
.await?;
|
||||
|
||||
admin_audit::log_event(
|
||||
&ctx,
|
||||
None,
|
||||
if result.requires_confirmation {
|
||||
"subscription.public.pending"
|
||||
} else {
|
||||
"subscription.public.active"
|
||||
},
|
||||
"subscription",
|
||||
Some(result.subscription.id.to_string()),
|
||||
Some(result.subscription.target.clone()),
|
||||
Some(serde_json::json!({
|
||||
"channel_type": result.subscription.channel_type,
|
||||
"status": result.subscription.status,
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
|
||||
format::json(PublicSubscriptionResponse {
|
||||
ok: true,
|
||||
subscription_id: result.subscription.id,
|
||||
status: result.subscription.status,
|
||||
requires_confirmation: result.requires_confirmation,
|
||||
message: result.message,
|
||||
})
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn confirm(
|
||||
State(ctx): State<AppContext>,
|
||||
Json(payload): Json<SubscriptionTokenPayload>,
|
||||
) -> Result<Response> {
|
||||
let item = subscriptions::confirm_subscription(&ctx, &payload.token).await?;
|
||||
admin_audit::log_event(
|
||||
&ctx,
|
||||
None,
|
||||
"subscription.public.confirm",
|
||||
"subscription",
|
||||
Some(item.id.to_string()),
|
||||
Some(item.target.clone()),
|
||||
Some(serde_json::json!({ "channel_type": item.channel_type })),
|
||||
)
|
||||
.await?;
|
||||
|
||||
format::json(SubscriptionManageResponse {
|
||||
ok: true,
|
||||
subscription: subscriptions::to_public_subscription_view(&item),
|
||||
})
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn manage(
|
||||
State(ctx): State<AppContext>,
|
||||
Query(query): Query<SubscriptionManageQuery>,
|
||||
) -> Result<Response> {
|
||||
let item = subscriptions::get_subscription_by_manage_token(&ctx, &query.token).await?;
|
||||
format::json(SubscriptionManageResponse {
|
||||
ok: true,
|
||||
subscription: subscriptions::to_public_subscription_view(&item),
|
||||
})
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn update_manage(
|
||||
State(ctx): State<AppContext>,
|
||||
Json(payload): Json<SubscriptionManageUpdatePayload>,
|
||||
) -> Result<Response> {
|
||||
let item = subscriptions::update_subscription_preferences(
|
||||
&ctx,
|
||||
&payload.token,
|
||||
payload.display_name,
|
||||
payload.status,
|
||||
payload.filters,
|
||||
)
|
||||
.await?;
|
||||
|
||||
admin_audit::log_event(
|
||||
&ctx,
|
||||
None,
|
||||
"subscription.public.update",
|
||||
"subscription",
|
||||
Some(item.id.to_string()),
|
||||
Some(item.target.clone()),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
format::json(SubscriptionManageResponse {
|
||||
ok: true,
|
||||
subscription: subscriptions::to_public_subscription_view(&item),
|
||||
})
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn unsubscribe(
|
||||
State(ctx): State<AppContext>,
|
||||
Json(payload): Json<SubscriptionTokenPayload>,
|
||||
) -> Result<Response> {
|
||||
let item = subscriptions::unsubscribe_subscription(&ctx, &payload.token).await?;
|
||||
admin_audit::log_event(
|
||||
&ctx,
|
||||
None,
|
||||
"subscription.public.unsubscribe",
|
||||
"subscription",
|
||||
Some(item.id.to_string()),
|
||||
Some(item.target.clone()),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
format::json(SubscriptionManageResponse {
|
||||
ok: true,
|
||||
subscription: subscriptions::to_public_subscription_view(&item),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn routes() -> Routes {
|
||||
Routes::new()
|
||||
.prefix("/api/subscriptions")
|
||||
.add("/", post(subscribe))
|
||||
.add("/confirm", post(confirm))
|
||||
.add("/manage", get(manage).patch(update_manage))
|
||||
.add("/unsubscribe", post(unsubscribe))
|
||||
}
|
||||
Reference in New Issue
Block a user