Compare commits
1 Commits
675c0a2d87
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 52b19701cb |
39
.env
Normal file
39
.env
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Flic to PWA WebPush Configuration
|
||||||
|
|
||||||
|
# --- VAPID Keys (Required) ---
|
||||||
|
# Generate using: npx web-push generate-vapid-keys
|
||||||
|
VAPID_PUBLIC_KEY=BKfRJXjSQmAJ452gLwlK_8scGrW6qMU1mBRp39ONtcQHkSsQgmLAaODIyGbgHyRpnDEv3HfXV1oGh3SC0fHxY0E
|
||||||
|
VAPID_PRIVATE_KEY=ErEgsDKYQi5j2KPERC_gCtrEALAD0k-dWSwrrcD0-JU
|
||||||
|
VAPID_SUBJECT=mailto:admin@virtonline.eu
|
||||||
|
|
||||||
|
# --- Server Configuration ---
|
||||||
|
# Internal port for the Node.js app
|
||||||
|
PORT=3000
|
||||||
|
SUBSCRIPTIONS_FILE=subscriptions.json
|
||||||
|
DEFAULT_BUTTON_NAME=game-button
|
||||||
|
|
||||||
|
# --- Authentication (Optional) ---
|
||||||
|
# If both USERNAME and PASSWORD are set, Basic Auth will be enabled for:
|
||||||
|
# - POST /subscribe
|
||||||
|
# - GET /webhook
|
||||||
|
# Leave blank to disable authentication.
|
||||||
|
BASIC_AUTH_USERNAME=player
|
||||||
|
BASIC_AUTH_PASSWORD=SevenOfNine
|
||||||
|
|
||||||
|
# --- Web Push Retry Configuration (Optional) ---
|
||||||
|
# Number of retries on failure (e.g., DNS issues)
|
||||||
|
NOTIFICATION_MAX_RETRIES=3
|
||||||
|
# First retry delay in milliseconds (minimal delay for immediate retry)
|
||||||
|
NOTIFICATION_FIRST_RETRY_DELAY_MS=10
|
||||||
|
# Base delay in milliseconds for subsequent retries (used for exponential backoff)
|
||||||
|
NOTIFICATION_SUBSEQUENT_RETRY_DELAY_MS=1000
|
||||||
|
|
||||||
|
# --- Network Configuration (Optional) ---
|
||||||
|
# Timeout for DNS lookups (ms)
|
||||||
|
DNS_TIMEOUT_MS=5000
|
||||||
|
# Timeout for outgoing HTTP requests (ms)
|
||||||
|
HTTP_TIMEOUT_MS=10000
|
||||||
|
|
||||||
|
# --- Logging ---
|
||||||
|
# Controls log verbosity: error, warn, info, debug
|
||||||
|
LOG_LEVEL=info
|
||||||
10
.env.example
10
.env.example
@@ -9,9 +9,7 @@ VAPID_SUBJECT=mailto:mailto:user@example.org
|
|||||||
# --- Server Configuration ---
|
# --- Server Configuration ---
|
||||||
# Internal port for the Node.js app
|
# Internal port for the Node.js app
|
||||||
PORT=3000
|
PORT=3000
|
||||||
# Path inside the container
|
|
||||||
SUBSCRIPTIONS_FILE=/app/subscriptions.json
|
SUBSCRIPTIONS_FILE=/app/subscriptions.json
|
||||||
# Default button name to use when not specified
|
|
||||||
DEFAULT_BUTTON_NAME=game-button
|
DEFAULT_BUTTON_NAME=game-button
|
||||||
|
|
||||||
# --- Authentication (Optional) ---
|
# --- Authentication (Optional) ---
|
||||||
@@ -22,14 +20,6 @@ DEFAULT_BUTTON_NAME=game-button
|
|||||||
BASIC_AUTH_USERNAME=user12345
|
BASIC_AUTH_USERNAME=user12345
|
||||||
BASIC_AUTH_PASSWORD=password
|
BASIC_AUTH_PASSWORD=password
|
||||||
|
|
||||||
# --- CORS Configuration (Optional but Recommended) ---
|
|
||||||
# Comma-separated list of allowed origins for requests (e.g., your PWA frontend URL)
|
|
||||||
# If blank or not set, CORS might block browser requests (like from a setup page).
|
|
||||||
# Use '*' carefully, preferably list specific domains.
|
|
||||||
ALLOWED_ORIGINS=https://game-timer.virtonline.eu
|
|
||||||
ALLOWED_METHODS=POST,GET,OPTIONS
|
|
||||||
ALLOWED_HEADERS=Content-Type,Authorization
|
|
||||||
|
|
||||||
# --- Web Push Retry Configuration (Optional) ---
|
# --- Web Push Retry Configuration (Optional) ---
|
||||||
# Number of retries on failure (e.g., DNS issues)
|
# Number of retries on failure (e.g., DNS issues)
|
||||||
NOTIFICATION_MAX_RETRIES=3
|
NOTIFICATION_MAX_RETRIES=3
|
||||||
|
|||||||
115
README.md
115
README.md
@@ -15,7 +15,6 @@ It's designed to be run as a Docker container and integrated with Traefik v3 for
|
|||||||
* Integrates with Traefik v3 via Docker labels.
|
* Integrates with Traefik v3 via Docker labels.
|
||||||
* Configurable via environment variables (`.env` file).
|
* Configurable via environment variables (`.env` file).
|
||||||
* Optional Basic Authentication for securing the `/webhook` and `/subscribe` endpoints.
|
* Optional Basic Authentication for securing the `/webhook` and `/subscribe` endpoints.
|
||||||
* CORS configuration for allowing requests (needed if your PWA management interface interacts with the `/subscribe` endpoint).
|
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
@@ -52,55 +51,32 @@ It's designed to be run as a Docker container and integrated with Traefik v3 for
|
|||||||
This will output a Public Key and a Private Key.
|
This will output a Public Key and a Private Key.
|
||||||
|
|
||||||
3. **Configure Environment Variables:**
|
3. **Configure Environment Variables:**
|
||||||
* Copy the example `.env` file:
|
* Copy the example `.env` file:
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
```
|
```
|
||||||
* Edit the `.env` file with your specific values:
|
* Edit the `.env` file with your specific values:
|
||||||
* `VAPID_PUBLIC_KEY`: The public key generated in step 2. **Your PWA will also need this key** when it subscribes to push notifications.
|
* `VAPID_PUBLIC_KEY`: The public key generated in step 2. **Your PWA will also need this key** when it subscribes to push notifications.
|
||||||
* `VAPID_PRIVATE_KEY`: The private key generated in step 2. **Keep this secret!**
|
* `VAPID_PRIVATE_KEY`: The private key generated in step 2. **Keep this secret!**
|
||||||
* `VAPID_SUBJECT`: A `mailto:` or `https:` URL identifying you or your application (e.g., `mailto:admin@yourdomain.com`). Used by push services to contact you.
|
* `VAPID_SUBJECT`: A `mailto:` or `https:` URL identifying you or your application (e.g., `mailto:admin@yourdomain.com`). Used by push services to contact you.
|
||||||
* `PORT`: (Default: `3000`) The internal port the Node.js app listens on. Traefik will map to this.
|
* `PORT`: (Default: `3000`) The internal port the Node.js app listens on. Traefik will map to this.
|
||||||
* `SUBSCRIPTIONS_FILE`: (Default: `/app/subscriptions.json`) The path *inside the container* where the button-to-subscription mapping is stored.
|
* `SUBSCRIPTIONS_FILE`: (Default: `subscriptions.json`) The path *inside the container* where the button-to-subscription mapping is stored.
|
||||||
* `DEFAULT_BUTTON_NAME`: (Default: `game-button`) The default button name to use when the `Button-Name` header is not provided in the webhook request.
|
* `DEFAULT_BUTTON_NAME`: (Default: `game-button`) The default button name to use when the `Button-Name` header is not provided in the webhook request.
|
||||||
* `BASIC_AUTH_USERNAME`: (Optional) Username for Basic Authentication. If set along with `BASIC_AUTH_PASSWORD`, authentication will be enabled for `/webhook` and `/subscribe`.
|
* `BASIC_AUTH_USERNAME`: (Optional) Username for Basic Authentication. If set along with `BASIC_AUTH_PASSWORD`, authentication will be enabled for `/webhook` and `/subscribe`.
|
||||||
* `BASIC_AUTH_PASSWORD`: (Optional) Password for Basic Authentication. If set along with `BASIC_AUTH_USERNAME`, authentication will be enabled.
|
* `BASIC_AUTH_PASSWORD`: (Optional) Password for Basic Authentication. If set along with `BASIC_AUTH_USERNAME`, authentication will be enabled.
|
||||||
* `ALLOWED_ORIGINS`: Comma-separated list of domains allowed by CORS. Include your PWA's domain if it needs to interact directly (e.g., for setup). Example: `https://my-pwa.com`.
|
|
||||||
* `ALLOWED_METHODS`: (Default: `POST,OPTIONS`) Standard methods needed.
|
|
||||||
* `ALLOWED_HEADERS`: (Default: `Content-Type,Authorization`) Standard headers needed.
|
|
||||||
* `NOTIFICATION_MAX_RETRIES`: (Default: `3`) Number of retry attempts for failed push notifications. Must be a number.
|
* `NOTIFICATION_MAX_RETRIES`: (Default: `3`) Number of retry attempts for failed push notifications. Must be a number.
|
||||||
* `NOTIFICATION_FIRST_RETRY_DELAY_MS`: (Default: `10`) Delay in milliseconds for the first retry attempt. Setting to 0-10ms provides near-immediate first retry for transient DNS issues. Must be a number.
|
* `NOTIFICATION_FIRST_RETRY_DELAY_MS`: (Default: `10`) Delay in milliseconds for the first retry attempt. Setting to 0-10ms provides near-immediate first retry for transient DNS issues. Must be a number.
|
||||||
* `NOTIFICATION_SUBSEQUENT_RETRY_DELAY_MS`: (Default: `1000`) Base delay in milliseconds for subsequent retries. Each additional retry uses this value with exponential backoff and jitter. Must be a number.
|
* `NOTIFICATION_SUBSEQUENT_RETRY_DELAY_MS`: (Default: `1000`) Base delay in milliseconds for subsequent retries. Each additional retry uses this value with exponential backoff and jitter. Must be a number.
|
||||||
* `DNS_TIMEOUT_MS`: (Default: `5000`) DNS resolution timeout in milliseconds. Must be a number.
|
* `DNS_TIMEOUT_MS`: (Default: `5000`) DNS resolution timeout in milliseconds. Must be a number.
|
||||||
* `HTTP_TIMEOUT_MS`: (Default: `10000`) HTTP request timeout in milliseconds. Must be a number.
|
* `HTTP_TIMEOUT_MS`: (Default: `10000`) HTTP request timeout in milliseconds. Must be a number.
|
||||||
* `LOG_LEVEL`: (Default: `info`) Controls verbosity of logs. Valid values are `error`, `warn`, `info`, or `debug`. Use `debug` to see detailed header information and other diagnostic messages.
|
* `LOG_LEVEL`: (Default: `info`) Controls verbosity of logs. Valid values are `error`, `warn`, `info`, or `debug`. Use `debug` to see detailed header information and other diagnostic messages.
|
||||||
* `TRAEFIK_SERVICE_HOST`: Your public domain for this service (e.g., `webpush.virtonline.eu`).
|
|
||||||
* `TRAEFIK_CERT_RESOLVER`: The name of your TLS certificate resolver configured in Traefik (e.g., `le`, `myresolver`).
|
|
||||||
|
|
||||||
4. **Configure Traefik Labels:**
|
4. **Configure Traefik Labels:**
|
||||||
* Copy the example `labels` file:
|
* Copy the example `labels` file:
|
||||||
```bash
|
```bash
|
||||||
cp labels.example labels
|
cp labels.example labels
|
||||||
```
|
```
|
||||||
|
|
||||||
5. **Prepare Subscription Mapping File:**
|
|
||||||
* Edit the `subscriptions.json` file
|
|
||||||
* Add entries mapping your Flic button's serial number (as a lowercase string key) to the PWA `PushSubscription` object.
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"game": { // <-- Replace with your actual Flic Button name (lowercase recommended)
|
|
||||||
"endpoint": "https://your_pwa_push_endpoint...",
|
|
||||||
"expirationTime": null,
|
|
||||||
"keys": {
|
|
||||||
"p256dh": "YOUR_PWA_SUBSCRIPTION_P256DH_KEY",
|
|
||||||
"auth": "YOUR_PWA_SUBSCRIPTION_AUTH_KEY"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Add more entries for other buttons if needed
|
|
||||||
}
|
|
||||||
```
|
|
||||||
* Ensure this file contains valid JSON.
|
|
||||||
|
|
||||||
## Running the Service
|
## Running the Service
|
||||||
|
|
||||||
1. **Build the Docker Image:**
|
1. **Build the Docker Image:**
|
||||||
@@ -113,7 +89,7 @@ It's designed to be run as a Docker container and integrated with Traefik v3 for
|
|||||||
This command runs the container in detached mode (`-d`), names it, connects it to the `traefik` network, passes environment variables from the `.env` file, applies the Traefik labels from the `labels` file, and mounts the `subscriptions.json` file into the container.
|
This command runs the container in detached mode (`-d`), names it, connects it to the `traefik` network, passes environment variables from the `.env` file, applies the Traefik labels from the `labels` file, and mounts the `subscriptions.json` file into the container.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d --name flic-webhook-webpush \
|
docker run --rm -d --name flic-webhook-webpush \
|
||||||
--network traefik \
|
--network traefik \
|
||||||
--env-file .env \
|
--env-file .env \
|
||||||
--label-file labels \
|
--label-file labels \
|
||||||
@@ -137,14 +113,42 @@ In your Flic app or Flic Hub SDK interface:
|
|||||||
1. Select your Flic button.
|
1. Select your Flic button.
|
||||||
2. Add an "Internet Request" action.
|
2. Add an "Internet Request" action.
|
||||||
3. Fill in the following details:
|
3. Fill in the following details:
|
||||||
* Set `GET` method.
|
* Select the `GET` method.
|
||||||
* Set URL with query parameter: `https://<your_domain>/webhook/SingleClick` (Replace `<your_domain>` with your actual service domain, e.g., `webpush.virtonline.eu`).
|
* Set URL with query parameter: `https://webpush.virtonline.eu/webhook/SingleClick`
|
||||||
* **If Basic Authentication is enabled:**
|
* **If Basic Authentication is enabled:**
|
||||||
* Set the `Username` and `Password` fields to the values from your `BASIC_AUTH_USERNAME` and `BASIC_AUTH_PASSWORD` environment variables.
|
* Set the Headers:
|
||||||
* **If Basic Authentication is disabled:**
|
* Set the `Key` fields to `Authorization`.
|
||||||
* Leave the `Username` and `Password` fields empty.
|
* Set the `Value` fields to `Basic <base64 encoded username:password>` (e.g., `Basic dXNlcm5hbWU6cGFzc3dvcmQ=`). Use `$(echo -n 'user:password' | base64)` to generate the base64 encoded string.
|
||||||
* Tap on `Save action`.
|
* Click `ADD`.
|
||||||
4. Repeat for Double Click and/or Hold events, changing the `click_type` parameter accordingly (e.g., `/DoubleClick`).
|
* Tap on `SAVE ACTION`.
|
||||||
|
4. Repeat for Double Click (i.e., `/DoubleClick`) and Hold (i.e., `/Hold`) events.
|
||||||
|
The request for the Hold event should look like this:
|
||||||
|
|
||||||
|
<img src="images/flic-button-request.png" width="300" alt="Flic Button Request">
|
||||||
|
|
||||||
|
## App Example: "HTTP Shortcuts" by waboodoo
|
||||||
|
|
||||||
|
Search the Play Store - there might be others with similar names.
|
||||||
|
|
||||||
|
1. Install the App: Download and install "HTTP Shortcuts" or a similar app from the Google Play Store.
|
||||||
|
2. Create a New Shortcut within the App:
|
||||||
|
* Open the app and usually tap a '+' or 'Add' button.
|
||||||
|
* Give your shortcut a Name (e.g., "Turn on Office Light", "Log Water Intake").
|
||||||
|
* Choose an Icon.
|
||||||
|
* Enter the URL you want the request sent to (your webhook URL, IFTTT URL, Home Assistant webhook trigger, etc.).
|
||||||
|
* Select the HTTP Method (GET, POST, PUT, DELETE, etc. - often GET or POST for simple triggers).
|
||||||
|
* For POST/PUT: You'll likely need to configure the Request Body (e.g., JSON data) and Content Type (e.g., application/json).
|
||||||
|
* Authentication: Configure Basic Auth, Bearer Tokens, or custom Headers if your endpoint requires authentication.
|
||||||
|
* Other Options: Explore settings for response handling (show message on success/failure), timeouts, etc.
|
||||||
|
* Save the shortcut configuration within the app.
|
||||||
|
3. Add the Widget/Shortcut to your Home Screen:
|
||||||
|
* Go to your Android Home Screen.
|
||||||
|
* Long-press on an empty space.
|
||||||
|
* Select "Widgets".
|
||||||
|
* Scroll through the list to find the "HTTP Shortcuts" app (or the app you installed).
|
||||||
|
* Drag the app's widget or shortcut option onto your home screen.
|
||||||
|
* The app will likely ask you to choose which specific shortcut (the one you just created) this widget should trigger. Select it.
|
||||||
|
4. Test: Tap the newly created button on your home screen. It should trigger the internet request you configured.
|
||||||
|
|
||||||
## API Endpoints
|
## API Endpoints
|
||||||
|
|
||||||
@@ -156,7 +160,7 @@ In your Flic app or Flic Hub SDK interface:
|
|||||||
* `subscription` (object, required): The [PushSubscription object](https://developer.mozilla.org/en-US/docs/Web/API/PushSubscription) obtained from the browser's Push API.
|
* `subscription` (object, required): The [PushSubscription object](https://developer.mozilla.org/en-US/docs/Web/API/PushSubscription) obtained from the browser's Push API.
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"button_id": "game-button", // Optional, defaults to DEFAULT_BUTTON_NAME environment variable
|
"button_id": "game-button",
|
||||||
"subscription": {
|
"subscription": {
|
||||||
"endpoint": "https://your_pwa_push_endpoint...",
|
"endpoint": "https://your_pwa_push_endpoint...",
|
||||||
"expirationTime": null,
|
"expirationTime": null,
|
||||||
@@ -178,20 +182,10 @@ In your Flic app or Flic Hub SDK interface:
|
|||||||
* **Authentication:** Optional Basic Authentication via `Authorization` header if `BASIC_AUTH_USERNAME` and `BASIC_AUTH_PASSWORD` are configured.
|
* **Authentication:** Optional Basic Authentication via `Authorization` header if `BASIC_AUTH_USERNAME` and `BASIC_AUTH_PASSWORD` are configured.
|
||||||
* **URL Parameters:**
|
* **URL Parameters:**
|
||||||
* `click_type` (required): The type of button press (e.g., `SingleClick`, `DoubleClick`, or `Hold`).
|
* `click_type` (required): The type of button press (e.g., `SingleClick`, `DoubleClick`, or `Hold`).
|
||||||
* **Required Headers:**
|
|
||||||
* `Button-Name`: The identifier of the Flic button (sent by the Flic system). If not provided, the value of `DEFAULT_BUTTON_NAME` environment variable will be used as a fallback.
|
|
||||||
* **Optional Headers:**
|
* **Optional Headers:**
|
||||||
|
* `Button-Name`: The identifier of the Flic button (sent by the Flic system). If not provided, the value of `DEFAULT_BUTTON_NAME` environment variable will be used as a fallback.
|
||||||
* `Timestamp`: Timestamp of the button event (sent by the Flic system).
|
* `Timestamp`: Timestamp of the button event (sent by the Flic system).
|
||||||
* `Button-Battery-Level`: The battery level percentage of the button (sent by the Flic system).
|
* `Button-Battery-Level`: The battery level percentage of the button (sent by the Flic system).
|
||||||
* **Push Notification Payload (`data` field):** The service sends a JSON payload within the push notification. The client-side Service Worker can access this data via `event.data.json()`. The structure is:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"action": "SingleClick", // or DoubleClick, Hold
|
|
||||||
"button": "game-button", // Normalized button name (lowercase)
|
|
||||||
"timestamp": "2024-03-28T15:00:00.000Z", // ISO 8601 timestamp or current server time
|
|
||||||
"batteryLevel": 100 // Integer percentage (0-100) or 'N/A' if not provided
|
|
||||||
}
|
|
||||||
```
|
|
||||||
* **Responses:**
|
* **Responses:**
|
||||||
* `200 OK`: Webhook received, push notification sent successfully.
|
* `200 OK`: Webhook received, push notification sent successfully.
|
||||||
* `400 Bad Request`: Missing `Button-Name` header or `click_type` URL parameter.
|
* `400 Bad Request`: Missing `Button-Name` header or `click_type` URL parameter.
|
||||||
@@ -204,25 +198,14 @@ In your Flic app or Flic Hub SDK interface:
|
|||||||
|
|
||||||
Once your service is up and running, you can test the webhook endpoint using curl or any API testing tool. This example assumes Basic Authentication is enabled.
|
Once your service is up and running, you can test the webhook endpoint using curl or any API testing tool. This example assumes Basic Authentication is enabled.
|
||||||
|
|
||||||
**Note:** Replace `<username>`, `<password>`, `<your_domain>`, and `<button_name>` with your actual values. The `Button-Name` header is optional and will default to the value of `DEFAULT_BUTTON_NAME` if not provided.
|
**Note:** Replace `<username>`, `<password>` with your actual values. The `Button-Name` header is optional and will default to the value of `DEFAULT_BUTTON_NAME` if not provided.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Generate Base64 credentials (run this once)
|
curl -X GET "https://webpush.virtonline.eu/webhook/SingleClick" \
|
||||||
# echo -n '<username>:<password>' | base64
|
-H "Authorization: Basic $(echo -n 'user:password' | base64)" \
|
||||||
|
-H "Button-Name: game-button" \
|
||||||
# Example using generated Base64 string (replace YOUR_BASE64_CREDS)
|
|
||||||
curl -X GET "https://<your_domain>/webhook/SingleClick" \
|
|
||||||
-H "Authorization: Basic YOUR_BASE64_CREDS" \
|
|
||||||
-H "Button-Name: <button_name>" \
|
|
||||||
-H "Timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
-H "Timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||||
-H "Button-Battery-Level: 100" \
|
-H "Button-Battery-Level: 100"
|
||||||
|
|
||||||
# Example using curl's built-in Basic Auth (-u)
|
|
||||||
curl -X GET "https://<your_domain>/webhook/SingleClick" \
|
|
||||||
-u "<username>:<password>" \
|
|
||||||
-H "Button-Name: <button_name>" \
|
|
||||||
-H "Timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
||||||
-H "Button-Battery-Level: 100" \
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The expected response should be:
|
The expected response should be:
|
||||||
@@ -232,7 +215,7 @@ The expected response should be:
|
|||||||
|
|
||||||
If successful, the above response indicates that:
|
If successful, the above response indicates that:
|
||||||
1. Your webhook endpoint is properly configured
|
1. Your webhook endpoint is properly configured
|
||||||
2. The button ID was found in your subscriptions.json file
|
2. The button name was found in your subscriptions.json file
|
||||||
3. The web push notification was successfully sent to the registered PUSH API endpoint (e.g. https://jmt17.google.com/fcm/send/cf907M...)
|
3. The web push notification was successfully sent to the registered PUSH API endpoint (e.g. https://jmt17.google.com/fcm/send/cf907M...)
|
||||||
|
|
||||||
If you receive a different response, refer to the Troubleshooting section below.
|
If you receive a different response, refer to the Troubleshooting section below.
|
||||||
@@ -240,7 +223,7 @@ If you receive a different response, refer to the Troubleshooting section below.
|
|||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
* **Check Backend Logs:** `docker logs flic-webhook-webpush`. Look for errors related to configuration, file access, JSON parsing, authentication, or sending push notifications.
|
* **Check Backend Logs:** `docker logs flic-webhook-webpush`. Look for errors related to configuration, file access, JSON parsing, authentication, or sending push notifications.
|
||||||
* To see detailed debug information including all headers received from the Flic button, set `LOG_LEVEL=debug` in your .env file.
|
* To see detailed debug information including all headers received from the Flic button, set `LOG_LEVEL=debug` in your .env file.
|
||||||
* **Check Traefik Logs:** `docker logs traefik`. Look for routing errors or certificate issues.
|
* **Check Traefik Logs:** `docker logs traefik`. Look for routing errors or certificate issues.
|
||||||
* **Verify `.env`:** Ensure all required variables are set correctly, especially VAPID keys and Traefik settings.
|
* **Verify `.env`:** Ensure all required variables are set correctly, especially VAPID keys and Traefik settings.
|
||||||
* **Verify `labels`:** Double-check that variables were correctly substituted manually and match your `.env` and Traefik setup.
|
* **Verify `labels`:** Double-check that variables were correctly substituted manually and match your `.env` and Traefik setup.
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 378 KiB |
@@ -17,9 +17,9 @@ traefik.http.routers.flic-webhook-webpush.service=flic-webhook-webpush
|
|||||||
traefik.http.services.flic-webhook-webpush.loadbalancer.server.port=3000
|
traefik.http.services.flic-webhook-webpush.loadbalancer.server.port=3000
|
||||||
|
|
||||||
# Middleware CORS
|
# Middleware CORS
|
||||||
traefik.http.middlewares.cors-headers.headers.accesscontrolallowmethods=POST,GET,OPTIONS
|
traefik.http.middlewares.cors-headers.headers.accesscontrolallowmethods=POST,GET
|
||||||
traefik.http.middlewares.cors-headers.headers.accesscontrolalloworiginlist=https://game-timer.virtonline.eu
|
traefik.http.middlewares.cors-headers.headers.accesscontrolalloworiginlist=https://game-timer.virtonline.eu
|
||||||
traefik.http.middlewares.cors-headers.headers.accesscontrolallowheaders=Content-Type,Authorization
|
traefik.http.middlewares.cors-headers.headers.accesscontrolallowheaders=Content-Type,Authorization,button-name,button-battery-level,timestamp
|
||||||
traefik.http.middlewares.cors-headers.headers.accesscontrolallowcredentials=true
|
traefik.http.middlewares.cors-headers.headers.accesscontrolallowcredentials=true
|
||||||
traefik.http.middlewares.cors-headers.headers.accesscontrolmaxage=600
|
traefik.http.middlewares.cors-headers.headers.accesscontrolmaxage=600
|
||||||
traefik.http.middlewares.cors-headers.headers.addvaryheader=true
|
traefik.http.middlewares.cors-headers.headers.addvaryheader=true
|
||||||
|
|||||||
21
package-lock.json
generated
21
package-lock.json
generated
@@ -9,7 +9,6 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "^2.8.5",
|
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"web-push": "^3.6.7"
|
"web-push": "^3.6.7"
|
||||||
@@ -151,18 +150,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
|
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
|
||||||
},
|
},
|
||||||
"node_modules/cors": {
|
|
||||||
"version": "2.8.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
|
||||||
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
|
|
||||||
"dependencies": {
|
|
||||||
"object-assign": "^4",
|
|
||||||
"vary": "^1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "2.6.9",
|
"version": "2.6.9",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
@@ -614,14 +601,6 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/object-assign": {
|
|
||||||
"version": "4.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
|
||||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/object-inspect": {
|
"node_modules/object-inspect": {
|
||||||
"version": "1.13.4",
|
"version": "1.13.4",
|
||||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
"author": "Your Name",
|
"author": "Your Name",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "^2.8.5",
|
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"web-push": "^3.6.7"
|
"web-push": "^3.6.7"
|
||||||
|
|||||||
41
server.js
41
server.js
@@ -1,6 +1,5 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const webpush = require('web-push');
|
const webpush = require('web-push');
|
||||||
const cors = require('cors');
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const dns = require('dns'); // Add DNS module
|
const dns = require('dns'); // Add DNS module
|
||||||
@@ -20,9 +19,6 @@ const basicAuthUsername = process.env.BASIC_AUTH_USERNAME;
|
|||||||
const basicAuthPassword = process.env.BASIC_AUTH_PASSWORD;
|
const basicAuthPassword = process.env.BASIC_AUTH_PASSWORD;
|
||||||
// Note: We are NOT adding specific authentication for the /subscribe endpoint in this version.
|
// Note: We are NOT adding specific authentication for the /subscribe endpoint in this version.
|
||||||
// Consider adding API key or other auth if exposing this publicly.
|
// Consider adding API key or other auth if exposing this publicly.
|
||||||
const allowedOrigins = (process.env.ALLOWED_ORIGINS || "").split(',').map(origin => origin.trim()).filter(origin => origin);
|
|
||||||
const allowedMethods = (process.env.ALLOWED_METHODS || "POST,OPTIONS,GET").split(',').map(method => method.trim()).filter(method => method);
|
|
||||||
const allowedHeaders = (process.env.ALLOWED_HEADERS || "Content-Type,Authorization").split(',').map(header => header.trim()).filter(header => header);
|
|
||||||
// Retry configuration for DNS resolution issues
|
// Retry configuration for DNS resolution issues
|
||||||
const maxRetries = parseInt(process.env.NOTIFICATION_MAX_RETRIES || 3, 10);
|
const maxRetries = parseInt(process.env.NOTIFICATION_MAX_RETRIES || 3, 10);
|
||||||
const subsequentRetryDelay = parseInt(process.env.NOTIFICATION_SUBSEQUENT_RETRY_DELAY_MS || 1000, 10); // 1 second base delay for subsequent retries
|
const subsequentRetryDelay = parseInt(process.env.NOTIFICATION_SUBSEQUENT_RETRY_DELAY_MS || 1000, 10); // 1 second base delay for subsequent retries
|
||||||
@@ -182,34 +178,18 @@ loadSubscriptions();
|
|||||||
// --- Express App Setup ---
|
// --- Express App Setup ---
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
// --- CORS Middleware ---
|
|
||||||
const corsOptions = {
|
|
||||||
origin: (origin, callback) => {
|
|
||||||
if (!origin || allowedOrigins.length === 0 || allowedOrigins.includes(origin)) {
|
|
||||||
callback(null, true);
|
|
||||||
} else {
|
|
||||||
console.warn(`CORS: Blocked origin: ${origin}`);
|
|
||||||
callback(new Error('Not allowed by CORS'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: allowedMethods,
|
|
||||||
allowedHeaders: allowedHeaders,
|
|
||||||
optionsSuccessStatus: 204 // For pre-flight requests
|
|
||||||
};
|
|
||||||
app.use(cors(corsOptions));
|
|
||||||
// Enable pre-flight requests for all relevant routes
|
|
||||||
app.options('/webhook', cors(corsOptions));
|
|
||||||
app.options('/subscribe', cors(corsOptions));
|
|
||||||
|
|
||||||
|
|
||||||
// --- Body Parsing Middleware ---
|
// --- Body Parsing Middleware ---
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
// --- Basic Authentication Middleware ---
|
// --- Basic Authentication Middleware ---
|
||||||
const authenticateBasic = (req, res, next) => {
|
const authenticateBasic = (req, res, next) => {
|
||||||
// Skip authentication for OPTIONS requests (CORS preflight)
|
// Skip authentication for OPTIONS requests (CORS preflight - Traefik might still forward them or handle them)
|
||||||
|
// It's safe to keep this check.
|
||||||
if (req.method === 'OPTIONS') {
|
if (req.method === 'OPTIONS') {
|
||||||
return next();
|
logger.debug('Auth: Skipping auth for OPTIONS request.');
|
||||||
|
// Traefik should ideally respond to OPTIONS, but if it forwards, we just proceed without auth check.
|
||||||
|
// We don't need to send CORS headers here anymore.
|
||||||
|
return res.sendStatus(204); // Standard practice for preflight response if it reaches the backend
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip authentication if username or password are not set in environment
|
// Skip authentication if username or password are not set in environment
|
||||||
@@ -249,7 +229,7 @@ const authenticateBasic = (req, res, next) => {
|
|||||||
app.post('/subscribe', authenticateBasic, async (req, res) => {
|
app.post('/subscribe', authenticateBasic, async (req, res) => {
|
||||||
let { button_id, subscription } = req.body;
|
let { button_id, subscription } = req.body;
|
||||||
|
|
||||||
logger.debug('All headers received:');
|
logger.debug('All headers received on /subscribe:');
|
||||||
Object.keys(req.headers).forEach(headerName => {
|
Object.keys(req.headers).forEach(headerName => {
|
||||||
logger.debug(` ${headerName}: ${req.headers[headerName]}`);
|
logger.debug(` ${headerName}: ${req.headers[headerName]}`);
|
||||||
});
|
});
|
||||||
@@ -299,7 +279,7 @@ app.get('/webhook/:click_type', authenticateBasic, async (req, res) => {
|
|||||||
const batteryLevel = batteryLevelHeader ? parseInt(batteryLevelHeader, 10) || batteryLevelHeader : 'N/A';
|
const batteryLevel = batteryLevelHeader ? parseInt(batteryLevelHeader, 10) || batteryLevelHeader : 'N/A';
|
||||||
|
|
||||||
// Log all headers received from Flic
|
// Log all headers received from Flic
|
||||||
logger.debug('All headers received:');
|
logger.debug('All headers received on /webhook:');
|
||||||
Object.keys(req.headers).forEach(headerName => {
|
Object.keys(req.headers).forEach(headerName => {
|
||||||
logger.debug(` ${headerName}: ${req.headers[headerName]}`);
|
logger.debug(` ${headerName}: ${req.headers[headerName]}`);
|
||||||
});
|
});
|
||||||
@@ -362,9 +342,7 @@ const server = http.createServer(app);
|
|||||||
|
|
||||||
server.listen(port, () => {
|
server.listen(port, () => {
|
||||||
logger.info(`Flic Webhook to WebPush server listening on port ${port}`);
|
logger.info(`Flic Webhook to WebPush server listening on port ${port}`);
|
||||||
logger.info(`Allowed Origins: ${allowedOrigins.length > 0 ? allowedOrigins.join(', ') : '(Any)'}`);
|
logger.info('CORS: Handled by Traefik');
|
||||||
logger.info(`Allowed Methods: ${allowedMethods.join(', ')}`);
|
|
||||||
logger.info(`Allowed Headers: ${allowedHeaders.join(', ')}`);
|
|
||||||
// Log Basic Auth status instead of Flic Secret
|
// Log Basic Auth status instead of Flic Secret
|
||||||
if (basicAuthUsername && basicAuthPassword) {
|
if (basicAuthUsername && basicAuthPassword) {
|
||||||
logger.info('Authentication: Basic Auth Enabled');
|
logger.info('Authentication: Basic Auth Enabled');
|
||||||
@@ -372,6 +350,7 @@ server.listen(port, () => {
|
|||||||
logger.info('Authentication: Basic Auth Disabled (username/password not set)');
|
logger.info('Authentication: Basic Auth Disabled (username/password not set)');
|
||||||
}
|
}
|
||||||
logger.info(`Subscription Endpoint Auth: ${basicAuthUsername && basicAuthPassword ? 'Enabled (Basic)' : 'Disabled'}`);
|
logger.info(`Subscription Endpoint Auth: ${basicAuthUsername && basicAuthPassword ? 'Enabled (Basic)' : 'Disabled'}`);
|
||||||
|
logger.info(`Webhook Endpoint Auth: ${basicAuthUsername && basicAuthPassword ? 'Enabled (Basic)' : 'Disabled'}`);
|
||||||
logger.info(`Subscriptions File: ${subscriptionsFilePath}`);
|
logger.info(`Subscriptions File: ${subscriptionsFilePath}`);
|
||||||
logger.info(`Push Notification Retry Config: ${maxRetries} retries, first retry: ${firstRetryDelay}ms, subsequent retries: ${subsequentRetryDelay}ms base delay`);
|
logger.info(`Push Notification Retry Config: ${maxRetries} retries, first retry: ${firstRetryDelay}ms, subsequent retries: ${subsequentRetryDelay}ms base delay`);
|
||||||
logger.info(`DNS Config: IPv4 first, timeout ${dnsTimeout}ms`);
|
logger.info(`DNS Config: IPv4 first, timeout ${dnsTimeout}ms`);
|
||||||
|
|||||||
Reference in New Issue
Block a user