Skip to main content

cloudillo_push/
handler.rs

1//! Push notification HTTP handlers
2
3use axum::{extract::State, http::StatusCode, Json};
4use serde::{Deserialize, Serialize};
5
6use crate::prelude::*;
7use cloudillo_core::extract::Auth;
8use cloudillo_types::meta_adapter::{PushSubscriptionData, PushSubscriptionKeys};
9
10/// Request body for creating a push subscription
11#[derive(Debug, Deserialize)]
12pub struct CreateSubscriptionRequest {
13	/// The push subscription from the browser's Push API
14	pub subscription: BrowserSubscription,
15}
16
17/// Browser's PushSubscription format
18#[derive(Debug, Deserialize)]
19pub struct BrowserSubscription {
20	/// Push endpoint URL
21	pub endpoint: String,
22	/// Expiration time (Unix timestamp in ms, from browser)
23	#[serde(rename = "expirationTime")]
24	pub expiration_time: Option<i64>,
25	/// Subscription keys
26	pub keys: BrowserSubscriptionKeys,
27}
28
29/// Browser subscription keys format
30#[derive(Debug, Deserialize)]
31pub struct BrowserSubscriptionKeys {
32	/// P-256 public key (base64url encoded)
33	pub p256dh: String,
34	/// Auth secret (base64url encoded)
35	pub auth: String,
36}
37
38/// Response for successful subscription
39#[derive(Debug, Serialize)]
40pub struct SubscriptionResponse {
41	/// The created subscription ID
42	pub id: u64,
43}
44
45/// POST /api/notification/subscription
46///
47/// Registers a push notification subscription for the authenticated user.
48/// The subscription will be stored and used to send push notifications when
49/// the user is offline.
50pub async fn post_subscription(
51	State(app): State<App>,
52	Auth(auth): Auth,
53	Json(body): Json<CreateSubscriptionRequest>,
54) -> Result<Json<SubscriptionResponse>, (StatusCode, String)> {
55	tracing::info!(
56		tn_id = %auth.tn_id.0,
57		endpoint = %body.subscription.endpoint,
58		"Registering push subscription"
59	);
60
61	// Convert browser subscription format to our storage format
62	let subscription_data = PushSubscriptionData {
63		endpoint: body.subscription.endpoint,
64		// Browser sends expiration in milliseconds, convert to seconds if present
65		expiration_time: body.subscription.expiration_time.map(|ms| ms / 1000),
66		keys: PushSubscriptionKeys {
67			p256dh: body.subscription.keys.p256dh,
68			auth: body.subscription.keys.auth,
69		},
70	};
71
72	// Store the subscription
73	let id = app
74		.meta_adapter
75		.create_push_subscription(auth.tn_id, &subscription_data)
76		.await
77		.map_err(|e| {
78			tracing::error!(error = %e, "Failed to create push subscription");
79			(StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to save subscription: {}", e))
80		})?;
81
82	tracing::debug!(
83		tn_id = %auth.tn_id.0,
84		subscription_id = %id,
85		"Push subscription created"
86	);
87
88	Ok(Json(SubscriptionResponse { id }))
89}
90
91/// DELETE /api/notification/subscription/{id}
92///
93/// Removes a push notification subscription.
94pub async fn delete_subscription(
95	State(app): State<App>,
96	Auth(auth): Auth,
97	axum::extract::Path(subscription_id): axum::extract::Path<u64>,
98) -> Result<StatusCode, (StatusCode, String)> {
99	tracing::info!(
100		tn_id = %auth.tn_id.0,
101		subscription_id = %subscription_id,
102		"Deleting push subscription"
103	);
104
105	app.meta_adapter
106		.delete_push_subscription(auth.tn_id, subscription_id)
107		.await
108		.map_err(|e| {
109			tracing::error!(error = %e, "Failed to delete push subscription");
110			(StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to delete subscription: {}", e))
111		})?;
112
113	Ok(StatusCode::NO_CONTENT)
114}
115
116/// GET /api/notification/vapid-public-key
117///
118/// Returns the VAPID public key for this tenant.
119/// Clients need this to subscribe to push notifications.
120/// If VAPID keys don't exist yet, they will be auto-generated.
121pub async fn get_vapid_public_key(
122	State(app): State<App>,
123	Auth(auth): Auth,
124) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
125	// Try to read existing VAPID public key
126	let public_key = match app.auth_adapter.read_vapid_public_key(auth.tn_id).await {
127		Ok(key) => key,
128		Err(Error::NotFound) => {
129			// VAPID key doesn't exist, create one
130			tracing::info!(tn_id = %auth.tn_id.0, "Creating VAPID key on demand");
131			let keypair = app.auth_adapter.create_vapid_key(auth.tn_id).await.map_err(|e| {
132				tracing::error!(error = %e, "Failed to create VAPID key");
133				(StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to create VAPID key: {}", e))
134			})?;
135			keypair.public_key
136		}
137		Err(e) => {
138			tracing::error!(error = %e, "Failed to read VAPID public key");
139			return Err((
140				StatusCode::INTERNAL_SERVER_ERROR,
141				format!("Failed to get VAPID key: {}", e),
142			));
143		}
144	};
145
146	Ok(Json(serde_json::json!({
147		"vapidPublicKey": public_key
148	})))
149}
150
151// vim: ts=4