Fixed notifications VAPID
Build and Publish / Build and Publish Docker Image (push) Successful in 5m11s
Build and Publish / Build and Publish Docker Image (push) Successful in 5m11s
This commit is contained in:
@@ -438,6 +438,8 @@ struct MediaTemplate<'a> {
|
||||
items: Vec<MediaItem>,
|
||||
clients: Vec<Client>,
|
||||
filter_client_id: i64,
|
||||
page: usize,
|
||||
total_pages: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Template)]
|
||||
@@ -2088,6 +2090,7 @@ async fn visit_set_cancel(
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async fn media_page(request: Request, session: Session, db: Database) -> cot::Result<Response> {
|
||||
const MEDIA_PER_PAGE: usize = 24;
|
||||
let lang = detect_lang(&request);
|
||||
let admin_name = match require_auth(&session, lang).await {
|
||||
Ok(name) => name,
|
||||
@@ -2104,6 +2107,17 @@ async fn media_page(request: Request, session: Session, db: Database) -> cot::Re
|
||||
.and_then(|v| v.parse().ok())
|
||||
})
|
||||
.unwrap_or(0);
|
||||
let requested_page = request
|
||||
.uri()
|
||||
.query()
|
||||
.and_then(|query| {
|
||||
query
|
||||
.split('&')
|
||||
.find_map(|part| part.strip_prefix("page="))
|
||||
.and_then(|value| value.parse::<usize>().ok())
|
||||
})
|
||||
.unwrap_or(1)
|
||||
.max(1);
|
||||
|
||||
let clients_all = Client::objects().all(&db).await?;
|
||||
let visits_all = Visit::objects().all(&db).await?;
|
||||
@@ -2139,8 +2153,14 @@ async fn media_page(request: Request, session: Session, db: Database) -> cot::Re
|
||||
}
|
||||
media_list.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
||||
|
||||
let total_pages = media_list.len().div_ceil(MEDIA_PER_PAGE).max(1);
|
||||
let page = requested_page.min(total_pages);
|
||||
let page_start = (page - 1) * MEDIA_PER_PAGE;
|
||||
|
||||
let items: Vec<MediaItem> = media_list
|
||||
.into_iter()
|
||||
.skip(page_start)
|
||||
.take(MEDIA_PER_PAGE)
|
||||
.map(|m| {
|
||||
let cid: i64 = m.client_id.primary_key().unwrap();
|
||||
let client = clients_all.iter().find(|c| c.id.unwrap() == cid);
|
||||
@@ -2172,6 +2192,8 @@ async fn media_page(request: Request, session: Session, db: Database) -> cot::Re
|
||||
items,
|
||||
clients: active_clients,
|
||||
filter_client_id,
|
||||
page,
|
||||
total_pages,
|
||||
}
|
||||
.render()?;
|
||||
html_response(body, lang)
|
||||
|
||||
+33
-10
@@ -460,12 +460,14 @@ async fn client_portal(
|
||||
.await
|
||||
.map(|config| config.public_key)
|
||||
.unwrap_or_default();
|
||||
let notifications_enabled = !vapid_public_key.is_empty()
|
||||
&& query!(Setting, $key == notification_key)
|
||||
.get(&db)
|
||||
.await?
|
||||
.map(|setting| setting.value == "true")
|
||||
.unwrap_or(false);
|
||||
// The administrator setting controls whether the client can see notification
|
||||
// controls. Keep this independent from VAPID validation so a configuration
|
||||
// error is visible in the modal instead of silently removing the button.
|
||||
let notifications_enabled = query!(Setting, $key == notification_key)
|
||||
.get(&db)
|
||||
.await?
|
||||
.map(|setting| setting.value == "true")
|
||||
.unwrap_or(false);
|
||||
|
||||
let turnstile_site_key = crate::turnstile::get_site_key(&db).await?;
|
||||
let body = ClientPortalTemplate {
|
||||
@@ -624,6 +626,12 @@ async fn web_manifest(_request: Request, Path(token): Path<String>) -> cot::Resu
|
||||
|
||||
async fn service_worker(_request: Request) -> cot::Result<Response> {
|
||||
let script = r#"
|
||||
self.addEventListener('install', function(event) {
|
||||
self.skipWaiting();
|
||||
});
|
||||
self.addEventListener('activate', function(event) {
|
||||
event.waitUntil(self.clients.claim());
|
||||
});
|
||||
self.addEventListener('push', function(event) {
|
||||
var data = event.data ? event.data.json() : {};
|
||||
event.waitUntil(self.registration.showNotification(data.title || 'Pet Visits', {
|
||||
@@ -634,12 +642,23 @@ self.addEventListener('push', function(event) {
|
||||
self.addEventListener('notificationclick', function(event) {
|
||||
event.notification.close();
|
||||
var target = new URL(event.notification.data.url || '/', self.location.origin).href;
|
||||
event.waitUntil(clients.matchAll({ type: 'window', includeUncontrolled: true }).then(function(list) {
|
||||
event.waitUntil((async function() {
|
||||
var list = await clients.matchAll({ type: 'window', includeUncontrolled: true });
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
if ('focus' in list[i]) { list[i].navigate(target); return list[i].focus(); }
|
||||
if (list[i].url === target && 'focus' in list[i]) {
|
||||
return list[i].focus();
|
||||
}
|
||||
}
|
||||
return clients.openWindow ? clients.openWindow(target) : undefined;
|
||||
}));
|
||||
for (var j = 0; j < list.length; j++) {
|
||||
if ('navigate' in list[j] && 'focus' in list[j]) {
|
||||
try {
|
||||
var navigated = await list[j].navigate(target);
|
||||
return navigated ? navigated.focus() : list[j].focus();
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
if (clients.openWindow) return clients.openWindow(target);
|
||||
})());
|
||||
});
|
||||
"#;
|
||||
let mut response = Response::new(cot::Body::fixed(script));
|
||||
@@ -649,6 +668,10 @@ self.addEventListener('notificationclick', function(event) {
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("service-worker-allowed", "/".parse().unwrap());
|
||||
response.headers_mut().insert(
|
||||
"cache-control",
|
||||
"no-cache, no-store, must-revalidate".parse().unwrap(),
|
||||
);
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
|
||||
+35
-23
@@ -14,6 +14,26 @@ pub struct VapidConfig {
|
||||
subject: String,
|
||||
}
|
||||
|
||||
fn normalize_key_pair(public_key: &str, private_key: &str) -> Option<(String, String)> {
|
||||
let strip_assignment = |value: &str, name: &str| {
|
||||
value
|
||||
.trim()
|
||||
.strip_prefix(&format!("{name}="))
|
||||
.unwrap_or(value.trim())
|
||||
.trim()
|
||||
.to_string()
|
||||
};
|
||||
let public_key = strip_assignment(public_key, "WEB_PETTING_VAPID_PUBLIC_KEY");
|
||||
let private_key = strip_assignment(private_key, "WEB_PETTING_VAPID_PRIVATE_KEY");
|
||||
let public_bytes = URL_SAFE_NO_PAD.decode(&public_key).ok()?;
|
||||
let private_bytes = URL_SAFE_NO_PAD.decode(&private_key).ok()?;
|
||||
|
||||
if public_bytes.len() == 65 && public_bytes.first() == Some(&4) && private_bytes.len() == 32 {
|
||||
return Some((public_key, private_key));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn load_config(db: &Database) -> Option<VapidConfig> {
|
||||
let settings = Setting::objects().all(db).await.ok()?;
|
||||
let value = |key: &str| {
|
||||
@@ -23,30 +43,22 @@ pub async fn load_config(db: &Database) -> Option<VapidConfig> {
|
||||
.map(|setting| setting.value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
};
|
||||
let strip_assignment = |value: String, name: &str| {
|
||||
value
|
||||
.strip_prefix(&format!("{name}="))
|
||||
.unwrap_or(&value)
|
||||
.trim()
|
||||
.to_string()
|
||||
let raw_public_key = value("vapid_public_key")?;
|
||||
let raw_private_key = value("vapid_private_key")?;
|
||||
let (public_key, private_key) = match normalize_key_pair(&raw_public_key, &raw_private_key) {
|
||||
Some(keys) => keys,
|
||||
None => {
|
||||
tracing::warn!("invalid VAPID configuration in database");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let public_key = strip_assignment(value("vapid_public_key")?, "WEB_PETTING_VAPID_PUBLIC_KEY");
|
||||
let private_key =
|
||||
strip_assignment(value("vapid_private_key")?, "WEB_PETTING_VAPID_PRIVATE_KEY");
|
||||
let subject = strip_assignment(
|
||||
value("vapid_subject").unwrap_or_else(|| "mailto:admin@localhost".to_string()),
|
||||
"WEB_PETTING_VAPID_SUBJECT",
|
||||
);
|
||||
let public_bytes = URL_SAFE_NO_PAD.decode(&public_key).ok()?;
|
||||
let private_bytes = URL_SAFE_NO_PAD.decode(&private_key).ok()?;
|
||||
if public_bytes.len() != 65 || public_bytes.first() != Some(&4) || private_bytes.len() != 32 {
|
||||
tracing::warn!(
|
||||
public_key_bytes = public_bytes.len(),
|
||||
private_key_bytes = private_bytes.len(),
|
||||
"invalid VAPID configuration in database"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
let subject_value =
|
||||
value("vapid_subject").unwrap_or_else(|| "mailto:admin@localhost".to_string());
|
||||
let subject = subject_value
|
||||
.strip_prefix("WEB_PETTING_VAPID_SUBJECT=")
|
||||
.unwrap_or(&subject_value)
|
||||
.trim()
|
||||
.to_string();
|
||||
Some(VapidConfig {
|
||||
public_key,
|
||||
private_key,
|
||||
|
||||
@@ -55,6 +55,19 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if total_pages > 1 %}
|
||||
<nav class="media-pagination" aria-label="Pagination">
|
||||
{% if page > 1 %}
|
||||
<a class="button is-small" href="/admin/media?lang={{ lang.code() }}&page={{ page - 1 }}{% if filter_client_id > 0 %}&client_id={{ filter_client_id }}{% endif %}">←</a>
|
||||
{% endif %}
|
||||
{% for p in 1..=total_pages %}
|
||||
<a class="button is-small{% if p == page %} is-link{% endif %}" href="/admin/media?lang={{ lang.code() }}&page={{ p }}{% if filter_client_id > 0 %}&client_id={{ filter_client_id }}{% endif %}">{{ p }}</a>
|
||||
{% endfor %}
|
||||
{% if page < total_pages %}
|
||||
<a class="button is-small" href="/admin/media?lang={{ lang.code() }}&page={{ page + 1 }}{% if filter_client_id > 0 %}&client_id={{ filter_client_id }}{% endif %}">→</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<style>
|
||||
@@ -101,6 +114,13 @@
|
||||
.media-info {
|
||||
padding: 0.6rem 0.75rem;
|
||||
}
|
||||
.media-pagination {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.35rem;
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
.media-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -359,7 +359,13 @@ renderCalendar();
|
||||
showStatus('{{ t.portal_notifications_unsupported }}', true);
|
||||
return;
|
||||
}
|
||||
if (!'{{ vapid_public_key }}') {
|
||||
toggle.disabled = true;
|
||||
showStatus('{{ t.portal_notifications_error }}', true);
|
||||
return;
|
||||
}
|
||||
registration = await navigator.serviceWorker.register('/service-worker.js');
|
||||
await registration.update();
|
||||
await navigator.serviceWorker.ready;
|
||||
subscription = await registration.pushManager.getSubscription();
|
||||
var expectedKey = decodeKey('{{ vapid_public_key }}');
|
||||
|
||||
Reference in New Issue
Block a user