Update VAPID key handling and improve push notification encoding
Some checks failed
CI / ci (push) Failing after 1m14s
Some checks failed
CI / ci (push) Failing after 1m14s
This commit is contained in:
@@ -8,3 +8,4 @@ data/
|
|||||||
config.toml
|
config.toml
|
||||||
spa/node_modules/
|
spa/node_modules/
|
||||||
spa/dist/
|
spa/dist/
|
||||||
|
spa/.env
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -74,11 +74,17 @@ Copy `config.example.toml` to `config.toml` and adjust as needed.
|
|||||||
|
|
||||||
K-Mood supports Web Push notifications (works on iOS 16.4+ when added to Home Screen, Android, and desktop browsers). No Firebase or third-party service required.
|
K-Mood supports Web Push notifications (works on iOS 16.4+ when added to Home Screen, Android, and desktop browsers). No Firebase or third-party service required.
|
||||||
|
|
||||||
**1. Generate VAPID keys:**
|
**1. Generate a VAPID private key** (base64url-encoded, 32 bytes):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openssl ecparam -genkey -name prime256v1 -noout -out vapid_private.pem
|
python3 -c "
|
||||||
openssl ec -in vapid_private.pem -outform PEM 2>/dev/null | base64
|
import subprocess, base64
|
||||||
|
key = subprocess.check_output(
|
||||||
|
'openssl ecparam -genkey -name prime256v1 -noout 2>/dev/null | openssl ec -outform DER 2>/dev/null',
|
||||||
|
shell=True
|
||||||
|
)
|
||||||
|
print(base64.urlsafe_b64encode(key[7:39]).rstrip(b'=').decode())
|
||||||
|
"
|
||||||
```
|
```
|
||||||
|
|
||||||
**2. Add to `config.toml`:**
|
**2. Add to `config.toml`:**
|
||||||
@@ -86,7 +92,7 @@ openssl ec -in vapid_private.pem -outform PEM 2>/dev/null | base64
|
|||||||
```toml
|
```toml
|
||||||
[push]
|
[push]
|
||||||
enabled = true
|
enabled = true
|
||||||
vapid_private_key = "<base64 output from step 1>"
|
vapid_private_key = "<output from step 1>"
|
||||||
vapid_subject = "mailto:you@example.com"
|
vapid_subject = "mailto:you@example.com"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -38,5 +38,5 @@ allow_registration = true
|
|||||||
|
|
||||||
# [push]
|
# [push]
|
||||||
# enabled = true
|
# enabled = true
|
||||||
# vapid_private_key = "<base64-encoded PEM private key>"
|
# vapid_private_key = "<base64url-encoded 32-byte EC private key>"
|
||||||
# vapid_subject = "mailto:you@example.com"
|
# vapid_subject = "mailto:you@example.com"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use domain::user::UserId;
|
|||||||
|
|
||||||
pub struct WebPushSender {
|
pub struct WebPushSender {
|
||||||
client: IsahcWebPushClient,
|
client: IsahcWebPushClient,
|
||||||
vapid_private_key: Vec<u8>,
|
vapid_private_key: String,
|
||||||
vapid_subject: String,
|
vapid_subject: String,
|
||||||
subscription_query: Arc<dyn PushSubscriptionQueryPort>,
|
subscription_query: Arc<dyn PushSubscriptionQueryPort>,
|
||||||
}
|
}
|
||||||
@@ -32,14 +32,15 @@ impl WebPushSender {
|
|||||||
.as_deref()
|
.as_deref()
|
||||||
.ok_or_else(|| DomainError::InvalidInput("vapid_subject is required".into()))?;
|
.ok_or_else(|| DomainError::InvalidInput("vapid_subject is required".into()))?;
|
||||||
|
|
||||||
let decoded = base64_decode(private_key)?;
|
VapidSignatureBuilder::from_base64_no_sub(private_key)
|
||||||
|
.map_err(|e| DomainError::InvalidInput(format!("invalid VAPID key: {e}")))?;
|
||||||
|
|
||||||
let client = IsahcWebPushClient::new()
|
let client = IsahcWebPushClient::new()
|
||||||
.map_err(|e| DomainError::InvalidInput(format!("failed to create push client: {e}")))?;
|
.map_err(|e| DomainError::InvalidInput(format!("failed to create push client: {e}")))?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
client,
|
client,
|
||||||
vapid_private_key: decoded,
|
vapid_private_key: private_key.to_string(),
|
||||||
vapid_subject: subject.to_string(),
|
vapid_subject: subject.to_string(),
|
||||||
subscription_query,
|
subscription_query,
|
||||||
})
|
})
|
||||||
@@ -51,9 +52,7 @@ impl WebPushSender {
|
|||||||
.as_deref()
|
.as_deref()
|
||||||
.ok_or_else(|| DomainError::InvalidInput("vapid_private_key is required".into()))?;
|
.ok_or_else(|| DomainError::InvalidInput("vapid_private_key is required".into()))?;
|
||||||
|
|
||||||
let decoded = base64_decode(private_key)?;
|
let sig_builder = VapidSignatureBuilder::from_base64_no_sub(private_key)
|
||||||
|
|
||||||
let sig_builder = VapidSignatureBuilder::from_pem_no_sub(std::io::Cursor::new(&decoded))
|
|
||||||
.map_err(|e| DomainError::InvalidInput(format!("invalid VAPID key: {e}")))?;
|
.map_err(|e| DomainError::InvalidInput(format!("invalid VAPID key: {e}")))?;
|
||||||
|
|
||||||
let public_key = sig_builder.get_public_key();
|
let public_key = sig_builder.get_public_key();
|
||||||
@@ -61,13 +60,6 @@ impl WebPushSender {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn base64_decode(input: &str) -> Result<Vec<u8>, DomainError> {
|
|
||||||
use base64::Engine;
|
|
||||||
base64::engine::general_purpose::STANDARD
|
|
||||||
.decode(input)
|
|
||||||
.map_err(|e| DomainError::InvalidInput(format!("invalid base64: {e}")))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn base64_url_encode(input: &[u8]) -> String {
|
fn base64_url_encode(input: &[u8]) -> String {
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(input)
|
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(input)
|
||||||
@@ -91,10 +83,11 @@ impl domain::ports::ReminderSenderPort for WebPushSender {
|
|||||||
let payload_str = payload.to_string();
|
let payload_str = payload.to_string();
|
||||||
|
|
||||||
for sub in &subscriptions {
|
for sub in &subscriptions {
|
||||||
let subscription_info = SubscriptionInfo::new(sub.endpoint(), sub.p256dh(), sub.auth());
|
let subscription_info =
|
||||||
|
SubscriptionInfo::new(sub.endpoint(), sub.p256dh(), sub.auth());
|
||||||
|
|
||||||
let mut sig_builder = VapidSignatureBuilder::from_pem(
|
let mut sig_builder = VapidSignatureBuilder::from_base64(
|
||||||
std::io::Cursor::new(&self.vapid_private_key),
|
&self.vapid_private_key,
|
||||||
&subscription_info,
|
&subscription_info,
|
||||||
)
|
)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
|
|||||||
@@ -13,6 +13,13 @@ function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
|||||||
return array
|
return array
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function arrayBufferToBase64Url(buffer: ArrayBuffer): string {
|
||||||
|
const bytes = new Uint8Array(buffer)
|
||||||
|
let binary = ""
|
||||||
|
for (const b of bytes) binary += String.fromCharCode(b)
|
||||||
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
|
||||||
|
}
|
||||||
|
|
||||||
export function usePushNotifications() {
|
export function usePushNotifications() {
|
||||||
const [isSubscribed, setIsSubscribed] = useState(false)
|
const [isSubscribed, setIsSubscribed] = useState(false)
|
||||||
const [isSupported, setIsSupported] = useState(false)
|
const [isSupported, setIsSupported] = useState(false)
|
||||||
@@ -59,8 +66,8 @@ export function usePushNotifications() {
|
|||||||
|
|
||||||
await push.subscribe({
|
await push.subscribe({
|
||||||
endpoint: sub.endpoint,
|
endpoint: sub.endpoint,
|
||||||
p256dh: btoa(String.fromCharCode(...new Uint8Array(key))),
|
p256dh: arrayBufferToBase64Url(key),
|
||||||
auth: btoa(String.fromCharCode(...new Uint8Array(auth))),
|
auth: arrayBufferToBase64Url(auth),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onSuccess: () => setIsSubscribed(true),
|
onSuccess: () => setIsSubscribed(true),
|
||||||
|
|||||||
Reference in New Issue
Block a user