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
121 lines
4.3 KiB
Rust
121 lines
4.3 KiB
Rust
use loco_rs::prelude::*;
|
|
use serde_json::Value;
|
|
use web_push::{
|
|
ContentEncoding, HyperWebPushClient, SubscriptionInfo, Urgency, VapidSignatureBuilder,
|
|
WebPushClient, WebPushMessageBuilder,
|
|
};
|
|
|
|
use crate::models::_entities::site_settings;
|
|
|
|
const ENV_PUBLIC_WEB_PUSH_VAPID_PUBLIC_KEY: &str = "PUBLIC_WEB_PUSH_VAPID_PUBLIC_KEY";
|
|
const ENV_LEGACY_WEB_PUSH_VAPID_PUBLIC_KEY: &str = "TERMI_WEB_PUSH_VAPID_PUBLIC_KEY";
|
|
const ENV_WEB_PUSH_VAPID_PRIVATE_KEY: &str = "TERMI_WEB_PUSH_VAPID_PRIVATE_KEY";
|
|
const ENV_WEB_PUSH_VAPID_SUBJECT: &str = "TERMI_WEB_PUSH_VAPID_SUBJECT";
|
|
|
|
fn env_value(name: &str) -> Option<String> {
|
|
std::env::var(name)
|
|
.ok()
|
|
.map(|value| value.trim().to_string())
|
|
.filter(|value| !value.is_empty())
|
|
}
|
|
|
|
fn configured_value(value: Option<&String>) -> Option<String> {
|
|
value.and_then(|item| {
|
|
let trimmed = item.trim();
|
|
if trimmed.is_empty() {
|
|
None
|
|
} else {
|
|
Some(trimmed.to_string())
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn public_key(settings: &site_settings::Model) -> Option<String> {
|
|
configured_value(settings.web_push_vapid_public_key.as_ref())
|
|
.or_else(|| env_value(ENV_PUBLIC_WEB_PUSH_VAPID_PUBLIC_KEY))
|
|
.or_else(|| env_value(ENV_LEGACY_WEB_PUSH_VAPID_PUBLIC_KEY))
|
|
}
|
|
|
|
pub fn private_key(settings: &site_settings::Model) -> Option<String> {
|
|
configured_value(settings.web_push_vapid_private_key.as_ref())
|
|
.or_else(|| env_value(ENV_WEB_PUSH_VAPID_PRIVATE_KEY))
|
|
}
|
|
|
|
pub fn vapid_subject(settings: &site_settings::Model) -> Option<String> {
|
|
configured_value(settings.web_push_vapid_subject.as_ref())
|
|
.or_else(|| env_value(ENV_WEB_PUSH_VAPID_SUBJECT))
|
|
}
|
|
|
|
fn effective_vapid_subject(settings: &site_settings::Model, site_url: Option<&str>) -> String {
|
|
vapid_subject(settings)
|
|
.or_else(|| {
|
|
site_url
|
|
.map(str::trim)
|
|
.filter(|value| value.starts_with("http://") || value.starts_with("https://"))
|
|
.map(ToString::to_string)
|
|
})
|
|
.unwrap_or_else(|| "mailto:noreply@example.com".to_string())
|
|
}
|
|
|
|
pub fn public_key_configured(settings: &site_settings::Model) -> bool {
|
|
public_key(settings).is_some()
|
|
}
|
|
|
|
pub fn private_key_configured(settings: &site_settings::Model) -> bool {
|
|
private_key(settings).is_some()
|
|
}
|
|
|
|
pub fn is_enabled(settings: &site_settings::Model) -> bool {
|
|
public_key_configured(settings) && private_key_configured(settings)
|
|
}
|
|
|
|
pub fn subscription_info_from_metadata(metadata: Option<&Value>) -> Result<SubscriptionInfo> {
|
|
let subscription = metadata
|
|
.and_then(Value::as_object)
|
|
.and_then(|object| object.get("subscription"))
|
|
.cloned()
|
|
.ok_or_else(|| Error::BadRequest("browser push metadata 缺少 subscription".to_string()))?;
|
|
|
|
serde_json::from_value::<SubscriptionInfo>(subscription)
|
|
.map_err(|_| Error::BadRequest("browser push metadata 非法".to_string()))
|
|
}
|
|
|
|
pub async fn send_payload(
|
|
settings: &site_settings::Model,
|
|
subscription_info: &SubscriptionInfo,
|
|
payload: &[u8],
|
|
urgency: Option<Urgency>,
|
|
ttl: u32,
|
|
site_url: Option<&str>,
|
|
) -> Result<()> {
|
|
let private_key = private_key(settings)
|
|
.ok_or_else(|| Error::BadRequest("web push VAPID private key 未配置".to_string()))?;
|
|
|
|
let mut signature_builder = VapidSignatureBuilder::from_base64(&private_key, subscription_info)
|
|
.map_err(|error| Error::BadRequest(format!("web push vapid build failed: {error}")))?;
|
|
signature_builder.add_claim("sub", effective_vapid_subject(settings, site_url));
|
|
let signature = signature_builder
|
|
.build()
|
|
.map_err(|error| Error::BadRequest(format!("web push vapid sign failed: {error}")))?;
|
|
|
|
let mut builder = WebPushMessageBuilder::new(subscription_info);
|
|
builder.set_ttl(ttl);
|
|
if let Some(urgency) = urgency {
|
|
builder.set_urgency(urgency);
|
|
}
|
|
builder.set_payload(ContentEncoding::Aes128Gcm, payload);
|
|
builder.set_vapid_signature(signature);
|
|
|
|
let client = HyperWebPushClient::new();
|
|
let message = builder
|
|
.build()
|
|
.map_err(|error| Error::BadRequest(format!("web push message build failed: {error}")))?;
|
|
|
|
client
|
|
.send(message)
|
|
.await
|
|
.map_err(|error| Error::BadRequest(format!("web push send failed: {error}")))?;
|
|
|
|
Ok(())
|
|
}
|