this repo has no description
1use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
2use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
3use serde::Serialize;
4
5use crate::errors::AppError;
6
7#[derive(Debug, Serialize)]
8struct VapidClaims {
9 aud: String,
10 exp: usize,
11 sub: String,
12}
13
14/// Send a push notification to a subscriber.
15pub async fn send_push(
16 private_key_pem: &str,
17 public_key_b64url: &str,
18 endpoint: &str,
19 p256dh_b64url: &str,
20 auth_b64url: &str,
21 payload: &[u8],
22) -> Result<(), AppError> {
23 // Decode subscriber keys
24 let p256dh = URL_SAFE_NO_PAD
25 .decode(p256dh_b64url)
26 .map_err(|e| AppError::Internal(format!("Invalid p256dh: {e}")))?;
27 let auth = URL_SAFE_NO_PAD
28 .decode(auth_b64url)
29 .map_err(|e| AppError::Internal(format!("Invalid auth: {e}")))?;
30
31 // Encrypt payload using RFC 8291 (aes128gcm)
32 let encrypted = ece::encrypt(&p256dh, &auth, payload)
33 .map_err(|e| AppError::Internal(format!("Encryption failed: {e}")))?;
34
35 // Build VAPID JWT
36 let origin = get_origin(endpoint);
37 let exp = (chrono::Utc::now().timestamp() + 86400) as usize;
38 let claims = VapidClaims {
39 aud: origin,
40 exp,
41 sub: "mailto:admin@ayos.app".to_string(),
42 };
43 let header = Header::new(Algorithm::ES256);
44 let key = EncodingKey::from_ec_pem(private_key_pem.as_bytes())
45 .map_err(|e| AppError::Internal(format!("Invalid VAPID key: {e}")))?;
46 let jwt = encode(&header, &claims, &key)
47 .map_err(|e| AppError::Internal(format!("JWT signing failed: {e}")))?;
48
49 // Send HTTP request to push service
50 let client = reqwest::Client::new();
51 let response = client
52 .post(endpoint)
53 .header("TTL", "86400")
54 .header("Content-Encoding", "aes128gcm")
55 .header(
56 "Authorization",
57 format!("vapid t={jwt}, k={public_key_b64url}"),
58 )
59 .header("Content-Type", "application/octet-stream")
60 .body(encrypted)
61 .send()
62 .await
63 .map_err(|e| AppError::Internal(format!("Push send failed: {e}")))?;
64
65 if response.status().as_u16() == 410 || response.status().as_u16() == 404 {
66 return Err(AppError::NotFound("Subscription expired".to_string()));
67 }
68
69 if !response.status().is_success() {
70 let status = response.status();
71 let body = response.text().await.unwrap_or_default();
72 return Err(AppError::Internal(format!(
73 "Push service returned {status}: {body}"
74 )));
75 }
76
77 Ok(())
78}
79
80fn get_origin(url: &str) -> String {
81 if let Some(idx) = url.find("://") {
82 let rest = &url[idx + 3..];
83 if let Some(path_idx) = rest.find('/') {
84 url[..idx + 3 + path_idx].to_string()
85 } else {
86 url.to_string()
87 }
88 } else {
89 url.to_string()
90 }
91}
92
93/// Generate a new VAPID key pair.
94/// Returns (private_key_pem, public_key_base64url).
95pub fn generate_vapid_keys() -> Result<(String, String), AppError> {
96 use p256::elliptic_curve::sec1::ToEncodedPoint;
97 use p256::pkcs8::EncodePrivateKey;
98 use p256::SecretKey;
99
100 let secret_key = SecretKey::random(&mut rand::rngs::OsRng);
101
102 let pem = secret_key
103 .to_pkcs8_pem(p256::pkcs8::LineEnding::LF)
104 .map_err(|e| AppError::Internal(format!("Key generation failed: {e}")))?;
105
106 let public_key = secret_key.public_key();
107 let point = public_key.to_encoded_point(false);
108 let public_key_b64url = URL_SAFE_NO_PAD.encode(point.as_bytes());
109
110 Ok((pem.to_string(), public_key_b64url))
111}
112
113/// Load or generate VAPID keys from the database.
114pub async fn init_vapid_keys(pool: &sqlx::SqlitePool) -> (String, String) {
115 let existing = sqlx::query_as::<_, (String, String)>(
116 "SELECT private_key_pem, public_key_base64url FROM vapid_keys WHERE id = 1",
117 )
118 .fetch_optional(pool)
119 .await
120 .expect("Failed to query vapid_keys");
121
122 if let Some((pem, pub_key)) = existing {
123 return (pem, pub_key);
124 }
125
126 let (pem, pub_key) = generate_vapid_keys().expect("VAPID key generation failed");
127
128 sqlx::query(
129 "INSERT INTO vapid_keys (id, private_key_pem, public_key_base64url) VALUES (1, ?, ?)",
130 )
131 .bind(&pem)
132 .bind(&pub_key)
133 .execute(pool)
134 .await
135 .expect("Failed to store VAPID keys");
136
137 println!("Generated new VAPID keys");
138 (pem, pub_key)
139}