Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions PUSH_NOTIFICATIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Push Notifications for Heartbeat

## Overview

Server-initiated heartbeat checks allow the heartbeat server to actively monitor device status by sending push notifications that wake up PWA service workers and trigger immediate heartbeat responses. This works even when the PWA is completely closed.

## Setup Guide

### 1. Generate VAPID Keys

VAPID (Voluntary Application Server Identification) keys are required for web push notifications:

```bash
# Install web-push CLI globally
npm install -g web-push

# Generate VAPID key pair
web-push generate-vapid-keys

# Output example:
# Public Key: BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U
# Private Key: tUkzMcPbtl2-xZ4Z5A1OqiELPo2Pc9-SFNw6hU8_Hh0
```

- The private key should only be on your heartbeat server
- The public key needs to be in both server config AND PWA client code

### 2. Configure Heartbeat Server

Add the following to your `config/.env` file:

```bash
# VAPID Configuration for Push Notifications
VAPID_PUBLIC_KEY=BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U
VAPID_PRIVATE_KEY=tUkzMcPbtl2-xZ4Z5A1OqiELPo2Pc9-SFNw6hU8_Hh0
VAPID_SUBJECT=mailto:your-email@example.com

# Push Monitoring (Optional)
PUSH_CHECK_INTERVAL=5m # How often to check for inactive devices
PUSH_INACTIVITY_THRESHOLD=1h # Trigger push if no heartbeat for this long
```

### 3. Configure PWA Client

The PWA now supports configuring the VAPID public key directly through the user interface:

1. **Open the PWA** in your browser or installed app
2. **Enter the VAPID Public Key** in the configuration section
3. **Save Configuration** to persist your settings
4. **Enable Server Checks** to subscribe to push notifications

The VAPID public key field accepts your generated public key (e.g., `BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U`)
36 changes: 35 additions & 1 deletion api.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,26 @@ var (
apiInfoPath = "/api/info"
apiStatsPath = "/api/stats"
apiDevicesPath = "/api/devices"
apiPushSubscribePath = "/api/push-subscribe"
apiPushUnsubscribePath = "/api/push-unsubscribe"
apiPushTestPath = "/api/push-test"
jsonMime = "application/json"
)

func ApiHandler(ctx *fasthttp.RequestCtx, path string) {
// Add CORS headers to all responses
ctx.Response.Header.Set("Access-Control-Allow-Origin", "*")
ctx.Response.Header.Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
ctx.Response.Header.Set("Access-Control-Allow-Headers", "Content-Type, Auth, Device")

// Handle CORS preflight requests first, before any other checks
if ctx.IsOptions() {
ctx.SetStatusCode(200)
return
}

switch path {
case apiBeatPath, apiUpdateStatsPath, apiUpdateDevicesPath:
case apiBeatPath, apiUpdateStatsPath, apiUpdateDevicesPath, apiPushSubscribePath, apiPushUnsubscribePath:
if !ctx.IsPost() {
ErrorBadRequest(ctx, true)
return
Expand All @@ -32,6 +46,20 @@ func ApiHandler(ctx *fasthttp.RequestCtx, path string) {
// The authentication key provided with said Auth header
header := ctx.Request.Header.Peek("Auth")

// Make sure Auth key is correct
if string(header) != authToken {
ErrorForbidden(ctx, true)
return
}
case apiPushTestPath:
if !ctx.IsGet() {
ErrorBadRequest(ctx, true)
return
}

// The authentication key provided with said Auth header
header := ctx.Request.Header.Peek("Auth")

// Make sure Auth key is correct
if string(header) != authToken {
ErrorForbidden(ctx, true)
Expand All @@ -55,6 +83,12 @@ func ApiHandler(ctx *fasthttp.RequestCtx, path string) {
handleUpdateStats(ctx)
case apiUpdateDevicesPath:
handleUpdateDevices(ctx)
case apiPushSubscribePath:
HandlePushSubscribe(ctx)
case apiPushUnsubscribePath:
HandlePushUnsubscribe(ctx)
case apiPushTestPath:
HandlePushTest(ctx)
case apiInfoPath:
handleJsonObject(ctx, FormattedInfo())
case apiStatsPath:
Expand Down
10 changes: 5 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,23 @@ module github.com/5HT2B/heartbeat
go 1.20

require (
github.com/SherClockHolmes/webpush-go v1.4.0
github.com/ferluci/fast-realip v1.0.1
github.com/go-redis/redis/v8 v8.11.5
github.com/joho/godotenv v1.5.1
github.com/nitishm/go-rejson/v4 v4.2.0
github.com/redis/go-redis/v9 v9.6.0
github.com/valyala/fasthttp v1.55.0
github.com/valyala/quicktemplate v1.8.0
golang.org/x/text v0.16.0
golang.org/x/text v0.21.0
)

require (
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/gomodule/redigo v1.8.8 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/redis/go-redis/v9 v9.6.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
)
Loading