Compare commits

...

3 Commits

Author SHA1 Message Date
cpu
058441b6e6 default button name 2025-03-28 17:51:25 +01:00
cpu
5a3b4974c4 changed the route name 2025-03-28 17:28:24 +01:00
cpu
fda9178264 fixed cors 2025-03-28 15:45:20 +01:00
5 changed files with 213 additions and 102 deletions

View File

@@ -1,43 +1,40 @@
# --- Application Configuration --- # Flic to PWA WebPush Configuration
# --- VAPID Keys (REQUIRED for Web Push) --- # --- VAPID Keys (Required) ---
# Generate these once using npx web-push generate-vapid-keys (or other tools) # Generate using: npx web-push generate-vapid-keys
# Keep the private key SECRET!
VAPID_PUBLIC_KEY= VAPID_PUBLIC_KEY=
VAPID_PRIVATE_KEY= VAPID_PRIVATE_KEY=
VAPID_SUBJECT=mailto:mailto:admin@virtonline.eu # Contact email/URL for push service
# Subject claim for VAPID. Use a 'mailto:' URI or an 'https:' URL identifying your application. # --- Server Configuration ---
# Example: mailto:admin@yourdomain.com or https://yourdomain.com/contact PORT=3000 # Internal port for the Node.js app
VAPID_SUBJECT=mailto:admin@virtonline.eu SUBSCRIPTIONS_FILE=subscriptions.json # Path inside the container
DEFAULT_BUTTON_NAME=game-button # Default button name to use when not specified
# Subscription Storage # --- Authentication (Optional) ---
SUBSCRIPTIONS_FILE=subscriptions.json # 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=user12345
BASIC_AUTH_PASSWORD=password
# CORS # --- 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_ORIGINS=https://game-timer.virtonline.eu
ALLOWED_METHODS=POST,OPTIONS,GET ALLOWED_METHODS=POST,GET,OPTIONS
ALLOWED_HEADERS=Content-Type,Authorization ALLOWED_HEADERS=Content-Type,Authorization
# Logging Configuration # --- Web Push Retry Configuration (Optional) ---
LOG_LEVEL=INFO MAX_NOTIFICATION_RETRIES=3 # Number of retries on failure (e.g., DNS issues)
INITIAL_RETRY_DELAY_MS=1000 # Initial delay in milliseconds
# --- Security (Optional) --- # --- Network Configuration (Optional) ---
# If you want to add a simple security layer between Flic and this app. DNS_TIMEOUT_MS=5000 # Timeout for DNS lookups (ms)
# If set, configure Flic's HTTP request to include an "Authorization: Bearer YOUR_SECRET_VALUE" header. HTTP_TIMEOUT_MS=10000 # Timeout for outgoing HTTP requests (ms)
# use e.g.: openssl rand -hex 32
FLIC_SECRET=
# --- DNS and Network Configuration --- # --- Logging ---
# These settings help with Docker DNS resolution issues (EAI_AGAIN errors) # Controls log verbosity: error, warn, info, debug
LOG_LEVEL=info
# Maximum number of retry attempts for failed DNS resolutions
MAX_NOTIFICATION_RETRIES=3
# Initial delay in milliseconds before first retry (will increase with backoff)
INITIAL_RETRY_DELAY_MS=1000
# DNS resolution timeout in milliseconds
DNS_TIMEOUT_MS=5000
# HTTP request timeout in milliseconds
HTTP_TIMEOUT_MS=10000

111
README.md
View File

@@ -6,15 +6,16 @@ It's designed to be run as a Docker container and integrated with Traefik v3 for
## Features ## Features
* Receives GET requests on `/flic-webhook`. * Receives GET requests on `/webhook`.
* Receives POST requests on `/subscribe` to manage button-PWA mappings.
* Uses HTTP headers `Button-Name` and `Timestamp` from the Flic request. * Uses HTTP headers `Button-Name` and `Timestamp` from the Flic request.
* Gets `click_type` from query parameter. * Gets `click_type` from URL path.
* Looks up the target PWA push subscription based on the `Button-Name` header in a JSON file. * Looks up the target PWA push subscription based on the `Button-Name` header in a JSON file.
* Sends a Web Push notification containing the click details (action, button, timestamp) to the corresponding PWA subscription. * Sends a Web Push notification containing the click details (action, button, timestamp) to the corresponding PWA subscription.
* 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 bearer token authentication for securing the Flic webhook endpoint. * Optional Basic Authentication for securing the `/webhook` and `/subscribe` endpoints.
* CORS configuration for allowing requests (needed if your PWA management interface interacts with this service, although not strictly necessary for the Flic->Backend->PWA push flow itself). * CORS configuration for allowing requests (needed if your PWA management interface interacts with the `/subscribe` endpoint).
## Prerequisites ## Prerequisites
@@ -61,7 +62,9 @@ It's designed to be run as a Docker container and integrated with Traefik v3 for
* `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: `/app/subscriptions.json`) The path *inside the container* where the button-to-subscription mapping is stored.
* `FLIC_SECRET`: (Optional) Set a strong, random secret string if you want to secure the webhook endpoint using Bearer token authentication. Generate with `openssl rand -hex 32` or a password manager. * `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_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_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_METHODS`: (Default: `POST,OPTIONS`) Standard methods needed.
* `ALLOWED_HEADERS`: (Default: `Content-Type,Authorization`) Standard headers needed. * `ALLOWED_HEADERS`: (Default: `Content-Type,Authorization`) Standard headers needed.
@@ -78,10 +81,6 @@ It's designed to be run as a Docker container and integrated with Traefik v3 for
```bash ```bash
cp labels.example labels cp labels.example labels
``` ```
* **Important:** Edit the `labels` file. Replace `${TRAEFIK_SERVICE_HOST}`, `${TRAEFIK_CERT_RESOLVER}`, and `${PORT}` with the *actual values* from your `.env` file, as `docker run` does not substitute variables in label files.
* Example replacement: `Host(\`${TRAEFIK_SERVICE_HOST}\`)` becomes `Host(`webpush.virtonline.eu`)`.
* `traefik.http.routers.flic-webhook.tls.certresolver=${TRAEFIK_CERT_RESOLVER}` becomes `traefik.http.routers.flic-webhook.tls.certresolver=myresolver`.
* `traefik.http.services.flic-webhook.loadbalancer.server.port=${PORT}` becomes `traefik.http.services.flic-webhook.loadbalancer.server.port=3000`.
5. **Prepare Subscription Mapping File:** 5. **Prepare Subscription Mapping File:**
* Edit the `subscriptions.json` file * Edit the `subscriptions.json` file
@@ -138,43 +137,91 @@ In your Flic app or Flic Hub SDK interface:
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. * Set `GET` method.
* Set URL with query parameter: `https://webpush.virtonline.eu/flic-webhook?click_type=SingleClick` * Set URL with query parameter: `https://<your_domain>/webhook/SingleClick` (Replace `<your_domain>` with your actual service domain, e.g., `webpush.virtonline.eu`).
* Add headers: * **If Basic Authentication is enabled:**
* Key: `Authorization` * Set the `Username` and `Password` fields to the values from your `BASIC_AUTH_USERNAME` and `BASIC_AUTH_PASSWORD` environment variables.
* Value: `Bearer <FLIC_SECRET>` (Replace `<FLIC_SECRET>` with the actual secret from your `.env` file). * **If Basic Authentication is disabled:**
* It should look like this: * Leave the `Username` and `Password` fields empty.
<!-- <img src="images/flic-button-request.png" width="300" alt="Flic Button Request Configuration"> // TODO: Update image -->
* Tap on `Save action`. * Tap on `Save action`.
4. Repeat for Double Click and/or Hold events. 4. Repeat for Double Click and/or Hold events, changing the `click_type` parameter accordingly (e.g., `/DoubleClick`).
## API Endpoint ## API Endpoints
* **`GET /flic-webhook`** * **`POST /subscribe`**
* **Description:** Adds or updates the Web Push subscription associated with a specific Flic button ID.
* **Authentication:** Optional Basic Authentication via `Authorization` header if `BASIC_AUTH_USERNAME` and `BASIC_AUTH_PASSWORD` are configured.
* **Request Body:** JSON object containing:
* `button_id` (string, optional): The unique identifier for the Flic button (lowercase recommended, e.g., "game-button", "lights-button"). If not provided, the value of `DEFAULT_BUTTON_NAME` environment variable will be used as a fallback.
* `subscription` (object, required): The [PushSubscription object](https://developer.mozilla.org/en-US/docs/Web/API/PushSubscription) obtained from the browser's Push API.
```json
{
"button_id": "game-button", // Optional, defaults to DEFAULT_BUTTON_NAME environment variable
"subscription": {
"endpoint": "https://your_pwa_push_endpoint...",
"expirationTime": null,
"keys": {
"p256dh": "YOUR_PWA_SUBSCRIPTION_P256DH_KEY",
"auth": "YOUR_PWA_SUBSCRIPTION_AUTH_KEY"
}
}
}
```
* **Responses:**
* `201 Created`: Subscription saved successfully.
* `400 Bad Request`: Missing or invalid `subscription` object in the request body.
* `401 Unauthorized`: Missing or invalid Basic Authentication credentials (if authentication is enabled).
* `500 Internal Server Error`: Failed to save the subscription to the file.
* **`GET /webhook/:click_type`**
* **Description:** Receives Flic button events. * **Description:** Receives Flic button events.
* **Authentication:** Optional Bearer token via `Authorization` header if `FLIC_SECRET` is configured. * **Authentication:** Optional Basic Authentication via `Authorization` header if `BASIC_AUTH_USERNAME` and `BASIC_AUTH_PASSWORD` are configured.
* **Query Parameters:** * **URL Parameters:**
* `click_type`: The type of button press (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:**
* `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).
* **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` query parameter. * `400 Bad Request`: Missing `Button-Name` header or `click_type` URL parameter.
* `401 Unauthorized`: Missing or invalid Bearer token (if `FLIC_SECRET` is enabled). * `401 Unauthorized`: Missing or invalid Basic Authentication credentials (if authentication is enabled).
* `404 Not Found`: No subscription found in `subscriptions.json` for the given `Button-Name`. * `404 Not Found`: No subscription found in `subscriptions.json` for the given `Button-Name`.
* `410 Gone`: The push subscription associated with the button was rejected by the push service (likely expired or revoked). * `410 Gone`: The push subscription associated with the button was rejected by the push service (likely expired or revoked).
* `500 Internal Server Error`: Failed to send the push notification for other reasons. * `500 Internal Server Error`: Failed to send the push notification for other reasons.
## Testing the Webhook ## Testing the Webhook
Once your service is up and running, you can test the webhook endpoint using curl or any API testing tool: 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:** In the command below, replace `a74181969a613c545d66c1e436e75c1e4a6` with your actual FLIC_SECRET value from your .env file. **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.
```bash ```bash
curl -X GET "https://webpush.virtonline.eu/flic-webhook?click_type=SingleClick" \ # Generate Base64 credentials (run this once)
-H "Authorization: Bearer a74181969a613c545d66c1e436e75c1e4a6" \ # echo -n '<username>:<password>' | base64
-H "Button-Name: Game" \
-H "Timestamp: 2025-03-26T01:10:20Z" # 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 "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:
@@ -197,5 +244,5 @@ If you receive a different response, refer to the Troubleshooting section below.
* **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.
* **Verify `subscriptions.json`:** Ensure it's valid JSON and the button serial number (key) matches exactly what Flic sends (check backend logs for "Received webhook: Button=..."). Check if the subscription details are correct. Case sensitivity matters for the JSON keys (button serials). * **Verify `subscriptions.json`:** Ensure it's valid JSON and the button serial number (key) matches exactly what Flic sends (check backend logs for "Received webhook: Button=..."). Check if the subscription details are correct. Case sensitivity matters for the JSON keys (button serials).
* **Check Flic Configuration:** Ensure the URL, Method, Body, and Headers (especially `Content-Type` and `Authorization` if used) are correct in the Flic action setup. Use `curl` or Postman to test the endpoint manually first. * **Check Flic Configuration:** Ensure the URL, Method, `click_type` parameter, and authentication details (Username/Password if enabled) are correct in the Flic action setup. Use `curl` or Postman to test the endpoint manually first.
* **PWA Service Worker:** Remember that the PWA needs a correctly registered Service Worker to receive and handle the incoming push messages. Ensure the PWA subscribes using the *same* `VAPID_PUBLIC_KEY` configured in the backend's `.env`. * **PWA Service Worker:** Remember that the PWA needs a correctly registered Service Worker to receive and handle the incoming push messages. Ensure the PWA subscribes using the *same* `VAPID_PUBLIC_KEY` configured in the backend's `.env`.

8
labels
View File

@@ -20,13 +20,13 @@ traefik.http.services.flic-webhook-webpush.loadbalancer.server.port=3000
traefik.http.middlewares.cors-headers.headers.accesscontrolallowmethods=POST,GET,OPTIONS traefik.http.middlewares.cors-headers.headers.accesscontrolallowmethods=POST,GET,OPTIONS
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
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
# Apply the middleware to the router
traefik.http.routers.flic-webhook-webpush.middlewares=cors-headers
# Middleware Rate Limiting # Middleware Rate Limiting
traefik.http.middlewares.flic-ratelimit.ratelimit.average=10 traefik.http.middlewares.flic-ratelimit.ratelimit.average=10
traefik.http.middlewares.flic-ratelimit.ratelimit.burst=20 traefik.http.middlewares.flic-ratelimit.ratelimit.burst=20
# Apply the middleware to the router
traefik.http.routers.flic-webhook-webpush.middlewares=flic-ratelimit # Apply both middlewares to the router (comma-separated list)
traefik.http.routers.flic-webhook-webpush.middlewares=cors-headers,flic-ratelimit

101
server.js
View File

@@ -14,7 +14,10 @@ const vapidPublicKey = process.env.VAPID_PUBLIC_KEY;
const vapidPrivateKey = process.env.VAPID_PRIVATE_KEY; const vapidPrivateKey = process.env.VAPID_PRIVATE_KEY;
const vapidSubject = process.env.VAPID_SUBJECT; // mailto: or https: const vapidSubject = process.env.VAPID_SUBJECT; // mailto: or https:
const subscriptionsFilePath = process.env.SUBSCRIPTIONS_FILE || path.join(__dirname, 'subscriptions.json'); const subscriptionsFilePath = process.env.SUBSCRIPTIONS_FILE || path.join(__dirname, 'subscriptions.json');
const flicSecret = process.env.FLIC_SECRET; // Optional Bearer token secret for Flic webhook const defaultButtonName = process.env.DEFAULT_BUTTON_NAME || 'game-button';
// Basic Authentication Credentials
const basicAuthUsername = process.env.BASIC_AUTH_USERNAME;
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 allowedOrigins = (process.env.ALLOWED_ORIGINS || "").split(',').map(origin => origin.trim()).filter(origin => origin);
@@ -182,51 +185,71 @@ const corsOptions = {
}; };
app.use(cors(corsOptions)); app.use(cors(corsOptions));
// Enable pre-flight requests for all relevant routes // Enable pre-flight requests for all relevant routes
app.options('/flic-webhook', cors(corsOptions)); app.options('/webhook', cors(corsOptions));
app.options('/subscribe', cors(corsOptions)); app.options('/subscribe', cors(corsOptions));
// --- Body Parsing Middleware --- // --- Body Parsing Middleware ---
app.use(express.json()); app.use(express.json());
// --- Authentication Middleware (For Flic Webhook Only) --- // --- Basic Authentication Middleware ---
const authenticateFlicRequest = (req, res, next) => { const authenticateBasic = (req, res, next) => {
// Only apply auth if flicSecret is configured // Skip authentication for OPTIONS requests (CORS preflight)
if (!flicSecret) { if (req.method === 'OPTIONS') {
return next();
}
// Skip authentication if username or password are not set in environment
if (!basicAuthUsername || !basicAuthPassword) {
logger.warn('Auth: Basic Auth username or password not configured. Skipping authentication.');
return next(); return next();
} }
const authHeader = req.headers.authorization; const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
logger.warn('Auth (Flic): Missing or malformed Authorization header'); if (!authHeader || !authHeader.startsWith('Basic ')) {
return res.status(401).json({ message: 'Unauthorized: Missing or malformed Bearer token' }); logger.warn('Auth: Missing or malformed Basic Authorization header');
res.setHeader('WWW-Authenticate', 'Basic realm="Restricted Area"');
return res.status(401).json({ message: 'Unauthorized: Basic Authentication required' });
} }
const token = authHeader.split(' ')[1]; const credentials = authHeader.split(' ')[1];
if (token !== flicSecret) { const decodedCredentials = Buffer.from(credentials, 'base64').toString('utf8');
logger.warn('Auth (Flic): Invalid Bearer token received'); const [username, password] = decodedCredentials.split(':');
return res.status(401).json({ message: 'Unauthorized: Invalid token' });
}
logger.debug('Auth (Flic): Request authenticated successfully.'); // Note: Use constant-time comparison for production environments if possible,
next(); // but for this scope, direct comparison is acceptable.
if (username === basicAuthUsername && password === basicAuthPassword) {
logger.debug('Auth: Basic Authentication successful.');
return next();
} else {
logger.warn('Auth: Invalid Basic Authentication credentials received.');
res.setHeader('WWW-Authenticate', 'Basic realm="Restricted Area"');
return res.status(401).json({ message: 'Unauthorized: Invalid credentials' });
}
}; };
app.post('/subscribe', async (req, res) => { // --- Routes ---
const { button_id, subscription } = req.body;
// Subscribe endpoint: Add a new button->subscription mapping
// Apply Basic Authentication
app.post('/subscribe', authenticateBasic, async (req, res) => {
let { button_id, subscription } = req.body;
logger.debug('All headers received:'); logger.debug('All headers received:');
Object.keys(req.headers).forEach(headerName => { Object.keys(req.headers).forEach(headerName => {
logger.debug(` ${headerName}: ${req.headers[headerName]}`); logger.debug(` ${headerName}: ${req.headers[headerName]}`);
}); });
// If button_id is not provided, use defaultButtonName from environment variable
if (!button_id || typeof button_id !== 'string' || button_id.trim() === '') {
button_id = defaultButtonName;
logger.info(`No button_id provided, using default button name: ${button_id}`);
}
logger.info(`Received subscription request for button: ${button_id}`); logger.info(`Received subscription request for button: ${button_id}`);
// Basic Validation // Basic Validation - now we only validate subscription since button_id will use default if not provided
if (!button_id || typeof button_id !== 'string' || button_id.trim() === '') {
logger.warn('Subscription Error: Missing or invalid button_id');
return res.status(400).json({ message: 'Bad Request: Missing or invalid button_id' });
}
if (!subscription || typeof subscription !== 'object' || !subscription.endpoint || !subscription.keys || !subscription.keys.p256dh || !subscription.keys.auth) { if (!subscription || typeof subscription !== 'object' || !subscription.endpoint || !subscription.keys || !subscription.keys.p256dh || !subscription.keys.auth) {
logger.warn('Subscription Error: Missing or invalid subscription object structure'); logger.warn('Subscription Error: Missing or invalid subscription object structure');
return res.status(400).json({ message: 'Bad Request: Missing or invalid subscription object' }); return res.status(400).json({ message: 'Bad Request: Missing or invalid subscription object' });
@@ -250,13 +273,17 @@ app.post('/subscribe', async (req, res) => {
}); });
// --- Flic Webhook Endpoint (GET only) --- // --- Flic Webhook Endpoint (GET only) ---
// Apply Flic-specific authentication to this route // Apply Basic Authentication
app.get('/flic-webhook', authenticateFlicRequest, async (req, res) => { app.get('/webhook/:click_type', authenticateBasic, async (req, res) => {
// Get buttonName from Header 'Button-Name' and timestamp from Header 'Timestamp' // Get buttonName from Header 'Button-Name' and timestamp from Header 'Timestamp'
const buttonName = req.headers['button-name']; const buttonName = req.headers['button-name'] || defaultButtonName;
const timestamp = req.headers['timestamp']; const timestamp = req.headers['timestamp'];
// Get click_type from query parameter instead of request body // Get click_type from URL path
const click_type = req.query.click_type; const click_type = req.params.click_type;
// Get battery level from Header 'Button-Battery-Level'
const batteryLevelHeader = req.headers['button-battery-level'];
// Use 'N/A' if header is missing or empty, otherwise parse as integer (or keep as string if parsing fails)
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:');
@@ -267,9 +294,9 @@ app.get('/flic-webhook', authenticateFlicRequest, async (req, res) => {
logger.info(`Received GET webhook: Button=${buttonName}, Type=${click_type}, Timestamp=${timestamp || 'N/A'}`); logger.info(`Received GET webhook: Button=${buttonName}, Type=${click_type}, Timestamp=${timestamp || 'N/A'}`);
// Basic validation // Basic validation
if (!buttonName || !click_type) { if (!click_type) {
logger.warn(`Webhook Error: Missing Button-Name header or click_type query parameter`); logger.warn(`Webhook Error: Missing click_type query parameter`);
return res.status(400).json({ message: 'Bad Request: Missing Button-Name header or click_type query parameter' }); return res.status(400).json({ message: 'Bad Request: Missing click_type query parameter' });
} }
const normalizedButtonName = buttonName.toLowerCase(); // Use lowercase for lookup consistency const normalizedButtonName = buttonName.toLowerCase(); // Use lowercase for lookup consistency
@@ -289,7 +316,8 @@ app.get('/flic-webhook', authenticateFlicRequest, async (req, res) => {
data: { data: {
action: click_type, action: click_type,
button: normalizedButtonName, // Send normalized button name button: normalizedButtonName, // Send normalized button name
timestamp: timestamp || new Date().toISOString() timestamp: timestamp || new Date().toISOString(),
batteryLevel: batteryLevel // Use the extracted value
} }
// icon: '/path/to/icon.png' // icon: '/path/to/icon.png'
}); });
@@ -324,8 +352,13 @@ server.listen(port, () => {
logger.info(`Allowed Origins: ${allowedOrigins.length > 0 ? allowedOrigins.join(', ') : '(Any)'}`); logger.info(`Allowed Origins: ${allowedOrigins.length > 0 ? allowedOrigins.join(', ') : '(Any)'}`);
logger.info(`Allowed Methods: ${allowedMethods.join(', ')}`); logger.info(`Allowed Methods: ${allowedMethods.join(', ')}`);
logger.info(`Allowed Headers: ${allowedHeaders.join(', ')}`); logger.info(`Allowed Headers: ${allowedHeaders.join(', ')}`);
logger.info(`Flic Webhook Auth: ${flicSecret ? 'Enabled (Bearer Token)' : 'Disabled'}`); // Log Basic Auth status instead of Flic Secret
logger.info(`Subscription Endpoint Auth: Disabled`); if (basicAuthUsername && basicAuthPassword) {
logger.info('Authentication: Basic Auth Enabled');
} else {
logger.info('Authentication: Basic Auth Disabled (username/password not set)');
}
logger.info(`Subscription 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, ${initialRetryDelay}ms initial delay`); logger.info(`Push Notification Retry Config: ${maxRetries} retries, ${initialRetryDelay}ms initial delay`);
logger.info(`DNS Config: IPv4 first, timeout ${dnsTimeout}ms`); logger.info(`DNS Config: IPv4 first, timeout ${dnsTimeout}ms`);

View File

@@ -0,0 +1,34 @@
[Unit]
Description=flic-webhook-webpush (virt-flic-webhook-webpush)
Requires=docker.service
After=docker.service
DefaultDependencies=no
[Service]
Type=simple
Environment="HOME=/root"
Environment="APP_PATH=/virt/flic-webhook-webpush"
ExecStartPre=-/usr/bin/env sh -c '/usr/bin/env docker kill virt-flic-webhook-webpush 2>/dev/null || true'
ExecStartPre=-/usr/bin/env sh -c '/usr/bin/env docker rm virt-flic-webhook-webpush 2>/dev/null || true'
ExecStartPre=/usr/bin/env sh -c 'touch ${APP_PATH}/subscriptions.json'
ExecStart=/usr/bin/env docker run \
--rm \
--name=virt-flic-webhook-webpush \
--log-driver=none \
--network=traefik \
--env-file=${APP_PATH}/.env \
--label-file=${APP_PATH}/labels \
--mount type=bind,src=${APP_PATH}/subscriptions.json,dst=/app/subscriptions.json \
flic-webhook-webpush
ExecStop=-/usr/bin/env sh -c '/usr/bin/env docker kill virt-flic-webhook-webpush 2>/dev/null || true'
ExecStop=-/usr/bin/env sh -c '/usr/bin/env docker rm virt-flic-webhook-webpush 2>/dev/null || true'
Restart=always
RestartSec=30
SyslogIdentifier=virt-flic-webhook-webpush
[Install]
WantedBy=multi-user.target