feat: add Bir Nur (Well of Light) waqf dashboard + reframe Support with bank trustee giving model
- New BirNur.svelte: waqf configuration based on companions' model - Reframed Support.svelte with dual-track: bank trustee giving + app support - Updated App.svelte to add Waqf tab (tab 14) between WorshipTracker and Support - Model inspired by Uthman's Well of Rumah and Abdur Rahman bin Auf's productive endowment
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
# Nūr — Muslim Companion · Deployment Guide
|
||||
|
||||
**Target:** `moslem02.falahos.my`
|
||||
**CDN:** Cloudflare (proxied / orange-cloud)
|
||||
**Server:** Linux with Nginx
|
||||
**Stack:** Svelte 5 + Vite PWA (static SPA)
|
||||
|
||||
---
|
||||
|
||||
## 1. Build
|
||||
|
||||
```bash
|
||||
# Install dependencies (one-time)
|
||||
npm ci
|
||||
|
||||
# Build for production
|
||||
npm run build
|
||||
```
|
||||
|
||||
Output goes to `dist/`. This includes:
|
||||
|
||||
- `index.html` — SPA entry (must not be cached long)
|
||||
- `assets/index-<hash>.js` / `.css` — hashed, cacheable forever
|
||||
- `sw.js` — Workbox service worker (must not be cached)
|
||||
- `manifest.webmanifest` — PWA manifest
|
||||
- Icons (`icon-192.png`, `icon-512.png`)
|
||||
- Optional: `workbox-<hash>.js`
|
||||
|
||||
---
|
||||
|
||||
## 2. Upload to Server
|
||||
|
||||
Choose one method.
|
||||
|
||||
### SCP (manual)
|
||||
|
||||
```bash
|
||||
scp -r dist/* user@moslem02.falahos.my:/var/www/nur-muslim-companion/
|
||||
```
|
||||
|
||||
### Rsync (incremental, recommended)
|
||||
|
||||
```bash
|
||||
rsync -avz --delete dist/ user@moslem02.falahos.my:/var/www/nur-muslim-companion/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Nginx
|
||||
|
||||
### Copy the config
|
||||
|
||||
```bash
|
||||
scp deploy/nginx.conf user@moslem02.falahos.my:/etc/nginx/sites-available/nur-muslim-companion
|
||||
```
|
||||
|
||||
### Enable and test
|
||||
|
||||
```bash
|
||||
sudo ln -sf /etc/nginx/sites-available/nur-muslim-companion /etc/nginx/sites-enabled/
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
### Verify
|
||||
|
||||
```bash
|
||||
curl -sI https://moslem02.falahos.my/ | grep -i "cache-control"
|
||||
# Should show: cache-control: no-cache, must-revalidate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Cloudflare Configuration
|
||||
|
||||
These settings are in your Cloudflare dashboard for `moslem02.falahos.my`:
|
||||
|
||||
| Setting | Value | Reason |
|
||||
|---------|-------|--------|
|
||||
| **Proxy status** | Proxied (orange cloud) | CDN caching, DDoS protection, SSL |
|
||||
| **SSL/TLS** | Full (strict) | End-to-end encryption; requires a valid origin cert |
|
||||
| **Always Use HTTPS** | On | Redirect HTTP → HTTPS |
|
||||
| **Auto Minify** | Off | Service worker integrity; Vite already minifies |
|
||||
| **Brotli** | On (default) | Better compression than gzip |
|
||||
| **Cache Level** | Standard | Respects origin `Cache-Control` |
|
||||
| **Edge Cache TTL** | Respect Existing Headers | Our nginx config sets correct policies |
|
||||
| **Security Level** | Medium | Default; raise if under attack |
|
||||
|
||||
### Origin Certificate
|
||||
|
||||
Since SSL/TLS is set to **Full (strict)**, the origin server (your VPS) needs a valid certificate. Generate one in Cloudflare Dashboard → SSL/TLS → Origin Server → Create Certificate. Install it on the VPS and point nginx to it.
|
||||
|
||||
**If you use Cloudflare's edge certificates only (Flexible SSL), the nginx config can stay HTTP-only on port 80.** The `deploy/nginx.conf` in this repo listens on port 80 — this is safe because Cloudflare proxies all traffic; your VPS never speaks cleartext to the internet.
|
||||
|
||||
---
|
||||
|
||||
## 5. Service Worker & Cache Invalidation
|
||||
|
||||
The PWA uses `registerType: 'autoUpdate'`:
|
||||
|
||||
1. **Always re-deploy `sw.js` with `no-cache`** — `deploy/nginx.conf` already does this.
|
||||
2. When `sw.js` changes, Workbox detects the update, installs the new version, and the PWA updates automatically on next page load or tab switch.
|
||||
3. Static assets (`/assets/*`) use content-hashed filenames — old cache entries are harmless and evicted naturally.
|
||||
|
||||
### Force-refresh after deploy
|
||||
|
||||
```bash
|
||||
# Clear Cloudflare cache for the whole zone
|
||||
curl -X POST "https://api.cloudflare.com/client/v4/zones/<ZONE_ID>/purge_cache" \
|
||||
-H "Authorization: Bearer <API_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data '{"purge_everything":true}'
|
||||
```
|
||||
|
||||
Or use Cloudflare Dashboard → Caching → Purge Everything.
|
||||
|
||||
---
|
||||
|
||||
## 6. Verification Checklist
|
||||
|
||||
- [ ] `curl -I https://moslem02.falahos.my/` returns 200
|
||||
- [ ] `curl -I https://moslem02.falahos.my/assets/index-*.js` has `cache-control: public, immutable, max-age=31536000`
|
||||
- [ ] `curl -I https://moslem02.falahos.my/sw.js` has `cache-control: no-cache, no-store, must-revalidate` and `service-worker-allowed: /`
|
||||
- [ ] Open https://moslem02.falahos.my/ in Chrome → DevTools → Application → Service Workers shows "activated"
|
||||
- [ ] App installs as PWA (install prompt or Add to Home Screen)
|
||||
- [ ] Install prompt appears on mobile (Chrome Android / Safari iOS)
|
||||
|
||||
---
|
||||
|
||||
## 7. Rollback
|
||||
|
||||
```bash
|
||||
# Deploy previous build
|
||||
rsync -avz --delete path/to/previous-build/ user@moslem02.falahos.my:/var/www/nur-muslim-companion/
|
||||
# Purge Cloudflare cache
|
||||
```
|
||||
@@ -0,0 +1,264 @@
|
||||
{
|
||||
"name": "Polar.sh → CRM Sync",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "webhook-trigger",
|
||||
"name": "Polar Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"position": [250, 300],
|
||||
"webhookId": "polar-crm-sync",
|
||||
"authentication": "none",
|
||||
"properties": {
|
||||
"httpMethod": "POST",
|
||||
"path": "polar-webhook",
|
||||
"responseMode": "onReceived",
|
||||
"responseData": "",
|
||||
"options": {}
|
||||
},
|
||||
"notes": "SETUP: 1) Deploy this workflow. 2) Copy the webhook URL. 3) Go to Polar.sh Dashboard → Settings → Webhooks → Add Endpoint. 4) Paste URL. 5) Select events: checkout.created, checkout.succeeded"
|
||||
},
|
||||
{
|
||||
"id": "switch-crm",
|
||||
"name": "Route to CRM",
|
||||
"type": "n8n-nodes-base.switch",
|
||||
"typeVersion": 2,
|
||||
"position": [450, 300],
|
||||
"properties": {
|
||||
"dataType": "string",
|
||||
"value1": "",
|
||||
"value2": "",
|
||||
"outputType": "route",
|
||||
"rules": [
|
||||
{
|
||||
"value": "",
|
||||
"outputKey": "Mautic"
|
||||
},
|
||||
{
|
||||
"value": "",
|
||||
"outputKey": "Twenty CRM"
|
||||
}
|
||||
]
|
||||
},
|
||||
"notes": "SET ME: Change routing based on which CRM you're using. Currently routes to Mautic (output 1). Switch to Twenty by changing routing logic."
|
||||
},
|
||||
{
|
||||
"id": "code-parse",
|
||||
"name": "Parse Polar Payload",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [650, 150],
|
||||
"properties": {
|
||||
"language": "javaScript",
|
||||
"code": "const payload = $input.first().json;\nconst eventType = payload.type || '';\nconst checkout = payload.data || {};\n\nconst email = checkout.customer?.email || checkout.customerEmail || '';\nconst name = checkout.customer?.name || checkout.customerName || '';\nconst amount = checkout.amount || 0;\nconst currency = checkout.currency || 'usd';\nconst productName = checkout.product?.name || checkout.productName || '';\nconst metadata = checkout.product?.metadata || checkout.metadata || {};\nconst donationType = metadata.donationType || metadata.type || '';\nconst timestamp = checkout.createdAt || new Date().toISOString();\n\nlet donationLabel = '';\nif (donationType === 'sadaqah' || productName.toLowerCase().includes('sadaqah')) {\n donationLabel = 'Sadaqah Jariyah';\n} else if (donationType === 'zakat' || productName.toLowerCase().includes('zakat')) {\n donationLabel = 'Zakat (Fi Sabilillah)';\n} else if (donationType === 'hibah' || productName.toLowerCase().includes('hibah')) {\n donationLabel = 'Hibah (Gift)';\n} else {\n donationLabel = 'Donation';\n}\n\nconst amountDisplay = currency === 'myr'\n ? `RM${(amount / 100).toFixed(2)}`\n : `${(amount / 100).toFixed(2)} ${currency.toUpperCase()}`;\n\nreturn {\n email,\n name,\n amount,\n amountDisplay,\n currency,\n donationLabel,\n donationType,\n productName,\n timestamp\n};"
|
||||
},
|
||||
"notes": "Extracts fields from Polar.sh webhook payload."
|
||||
},
|
||||
{
|
||||
"id": "mautic-upsert",
|
||||
"name": "Mautic - Upsert Contact",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [850, 100],
|
||||
"credentials": {
|
||||
"httpRequest": {
|
||||
"id": "SET_ME_MauticCredId",
|
||||
"name": "Mautic API"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"method": "POST",
|
||||
"url": "https://mautic.falahos.my/api/contacts/new",
|
||||
"authentication": "genericCredentialType",
|
||||
"genericAuthType": "httpHeaderAuth",
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "email",
|
||||
"value": "={{ $json.email }}"
|
||||
},
|
||||
{
|
||||
"name": "firstname",
|
||||
"value": "={{ $json.name.split(' ')[0] }}"
|
||||
},
|
||||
{
|
||||
"name": "lastname",
|
||||
"value": "={{ $json.name.split(' ').slice(1).join(' ') }}"
|
||||
},
|
||||
{
|
||||
"name": "tags",
|
||||
"value": "={{ 'polar-donor,' + $json.donationLabel.toLowerCase().replace(' ', '-') }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"notes": "Creates/updates contact in Mautic CRM. Mautic is running at mautic.falahos.my. API credentials: admin / FalahMautic2026!"
|
||||
},
|
||||
{
|
||||
"id": "mautic-note",
|
||||
"name": "Mautic - Add Donation Note",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [850, 300],
|
||||
"credentials": {
|
||||
"httpRequest": {
|
||||
"id": "SET_ME_MauticCredId",
|
||||
"name": "Mautic API"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"method": "POST",
|
||||
"url": "https://mautic.falahos.my/api/notes/new",
|
||||
"authentication": "genericCredentialType",
|
||||
"genericAuthType": "httpHeaderAuth",
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "text",
|
||||
"value": "={{ '💚 ' + $json.donationLabel + ': ' + $json.amountDisplay + ' via Polar.sh' }}"
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"value": "donation"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"notes": "Adds a timeline note in Mautic recording the donation. The contact ID from the upsert response is used as the parent."
|
||||
},
|
||||
{
|
||||
"id": "twenty-upsert",
|
||||
"name": "Twenty CRM - Upsert Contact",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [850, 500],
|
||||
"credentials": {
|
||||
"httpRequest": {
|
||||
"id": "SET_ME_TwentyCredId",
|
||||
"name": "Twenty CRM API"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"method": "POST",
|
||||
"url": "SET_ME_TwentyURL/rest/contacts",
|
||||
"authentication": "genericCredentialType",
|
||||
"genericAuthType": "httpHeaderAuth",
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{ "name": "email", "value": "={{ $json.email }}" },
|
||||
{ "name": "name", "value": "={{ $json.name }}" },
|
||||
{
|
||||
"name": "position",
|
||||
"value": "={{ $json.donationLabel + ' Donor' }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"notes": "SET ME: Configure Twenty CRM API URL and API key. Currently disabled. Activate when Twenty is set up."
|
||||
},
|
||||
{
|
||||
"id": "twenty-note",
|
||||
"name": "Twenty CRM - Log Donation Note",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [850, 700],
|
||||
"credentials": {
|
||||
"httpRequest": {
|
||||
"id": "SET_ME_TwentyCredId",
|
||||
"name": "Twenty CRM API"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"method": "POST",
|
||||
"url": "SET_ME_TwentyURL/rest/activities",
|
||||
"authentication": "genericCredentialType",
|
||||
"genericAuthType": "httpHeaderAuth",
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "title",
|
||||
"value": "={{ $json.donationLabel }}"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"value": "={{ 'Donation of ' + $json.amountDisplay + ' via Polar.sh' }}"
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"value": "Note"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"notes": "SET ME: Configure when Twenty CRM is set up. Logs donation as an activity note."
|
||||
},
|
||||
{
|
||||
"id": "done",
|
||||
"name": "Done",
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [1050, 300],
|
||||
"properties": {}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Polar Webhook": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "Parse Polar Payload", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
},
|
||||
"Parse Polar Payload": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "Mautic - Upsert Contact", "type": "main", "index": 0 },
|
||||
{ "node": "Mautic - Add Donation Note", "type": "main", "index": 0 },
|
||||
{ "node": "Twenty CRM - Upsert Contact", "type": "main", "index": 0 },
|
||||
{ "node": "Twenty CRM - Log Donation Note", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
},
|
||||
"Mautic - Upsert Contact": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "Done", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
},
|
||||
"Mautic - Add Donation Note": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "Done", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
},
|
||||
"Twenty CRM - Upsert Contact": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "Done", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
},
|
||||
"Twenty CRM - Log Donation Note": {
|
||||
"main": [
|
||||
[
|
||||
{ "node": "Done", "type": "main", "index": 0 }
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"timezone": "Asia/Kuala_Lumpur"
|
||||
},
|
||||
"staticData": null,
|
||||
"tags": ["polar", "crm", "donations", "nur-falah"]
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
{
|
||||
"name": "Polar.sh → Twenty CRM Sync",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "webhook-trigger",
|
||||
"name": "Polar Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"position": [250, 300],
|
||||
"webhookId": "polar-twenty-sync",
|
||||
"authentication": "none",
|
||||
"properties": {
|
||||
"httpMethod": "POST",
|
||||
"path": "polar-webhook",
|
||||
"responseMode": "onReceived",
|
||||
"responseData": "",
|
||||
"options": {}
|
||||
},
|
||||
"notes": "SET ME: Configure this webhook URL in Polar.sh dashboard under Webhooks → Add endpoint. Polar sends checkout.created, checkout.updated, checkout.succeeded events."
|
||||
},
|
||||
{
|
||||
"id": "code-parse",
|
||||
"name": "Parse Polar Payload",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [450, 300],
|
||||
"properties": {
|
||||
"language": "javaScript",
|
||||
"code": "// Parse Polar.sh webhook payload\n// Polar sends: type, data (containing checkout object)\n// Checkout object has: customer (email, name, billingAddress), product (name, metadata), amount, currency\n\nconst payload = $input.first().json;\nconst eventType = payload.type || '';\nconst checkout = payload.data || {};\n\n// Extract fields from Polar payload\nconst email = checkout.customer?.email || checkout.customerEmail || '';\nconst name = checkout.customer?.name || checkout.customerName || '';\nconst amount = checkout.amount || 0;\nconst currency = checkout.currency || 'usd';\nconst productName = checkout.product?.name || checkout.productName || '';\nconst metadata = checkout.product?.metadata || checkout.metadata || {};\nconst donationType = metadata.donationType || metadata.type || '';\nconst timestamp = checkout.createdAt || new Date().toISOString();\nconst polarCheckoutId = checkout.id || '';\n\n// Map donation type\nlet donationLabel = '';\nif (donationType === 'sadaqah' || productName.toLowerCase().includes('sadaqah')) {\n donationLabel = 'Sadaqah';\n} else if (donationType === 'zakat' || productName.toLowerCase().includes('zakat')) {\n donationLabel = 'Zakat';\n} else if (donationType === 'hibah' || productName.toLowerCase().includes('hibah')) {\n donationLabel = 'Hibah';\n} else {\n donationLabel = 'Donation';\n}\n\n// Format amount to display value (cents to main unit)\nconst amountDisplay = currency === 'myr' ?\n `RM${(amount / 100).toFixed(2)}` :\n `${(amount / 100).toFixed(2)} ${currency.toUpperCase()}`;\n\nreturn {\n email,\n name,\n amount,\n amountDisplay,\n currency,\n donationLabel,\n donationType,\n productName,\n polarCheckoutId,\n timestamp\n};"
|
||||
},
|
||||
"notes": "Extracts and normalises Polar.sh webhook fields. Metadata fields are set in Polar product configuration."
|
||||
},
|
||||
{
|
||||
"id": "http-twenty",
|
||||
"name": "Twenty CRM - Upsert Contact",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [650, 300],
|
||||
"credentials": {
|
||||
"httpRequest": {
|
||||
"id": "SET_ME_TwentyCRMApiKeyId",
|
||||
"name": "Twenty CRM API Key"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"method": "POST",
|
||||
"url": "SET_ME_TwentyCRMAPIUrl/rest/contacts",
|
||||
"authentication": "genericCredentialType",
|
||||
"genericAuthType": "httpHeaderAuth",
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "email",
|
||||
"value": "={{ $json.email }}"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"value": "={{ $json.name }}"
|
||||
},
|
||||
{
|
||||
"name": "city",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"name": "phone",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"name": "position",
|
||||
"value": "={{ $json.donationLabel }}"
|
||||
},
|
||||
{
|
||||
"name": "linkedinLink",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"name": "xLink",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"name": "introduction",
|
||||
"value": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"timeout": 10000,
|
||||
"allowUnauthorizedCerts": false,
|
||||
"redirect": {},
|
||||
"response": {
|
||||
"response": {
|
||||
"responseFormat": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"headers": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"notes": "SET ME: Replace the URL placeholder with your Twenty CRM instance URL (e.g., https://your-workspace.twenty.com). Configure the header auth credential with your Twenty CRM API key."
|
||||
},
|
||||
{
|
||||
"id": "set-activity",
|
||||
"name": "Twenty CRM - Log Note Activity",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [650, 480],
|
||||
"credentials": {
|
||||
"httpRequest": {
|
||||
"id": "SET_ME_TwentyCRMApiKeyId",
|
||||
"name": "Twenty CRM API Key"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"method": "POST",
|
||||
"url": "SET_ME_TwentyCRMAPIUrl/rest/activityTargets",
|
||||
"authentication": "genericCredentialType",
|
||||
"genericAuthType": "httpHeaderAuth",
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "title",
|
||||
"value": "={{ $json.donationLabel + ' Donation - ' + $json.amountDisplay }}"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"value": "={{ 'Donation from ' + $json.name + ' (' + $json.email + ')\\n' + 'Type: ' + $json.donationLabel + '\\n' + 'Amount: ' + $json.amountDisplay + '\\n' + 'Checkout ID: ' + $json.polarCheckoutId + '\\n' + 'Date: ' + $json.timestamp }}"
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"value": "Note"
|
||||
},
|
||||
{
|
||||
"name": "visibility",
|
||||
"value": "workspace"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"timeout": 10000,
|
||||
"allowUnauthorizedCerts": false,
|
||||
"redirect": {},
|
||||
"response": {
|
||||
"responseFormat": "json"
|
||||
}
|
||||
},
|
||||
"headers": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"notes": "SET ME: Same credential and URL base as above. This node logs a note activity for the contact."
|
||||
},
|
||||
{
|
||||
"id": "noop-complete",
|
||||
"name": "Done",
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [850, 300],
|
||||
"properties": {}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Polar Webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Parse Polar Payload",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Parse Polar Payload": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Twenty CRM - Upsert Contact",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Twenty CRM - Log Note Activity",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Twenty CRM - Upsert Contact": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Done",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Twenty CRM - Log Note Activity": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Done",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"staticData": null,
|
||||
"tags": [],
|
||||
"versionId": "1.0.0"
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
# ──────────────────────────────────────────────
|
||||
# Nūr — Muslim Companion · Nginx SPA Config
|
||||
# Target: moslem02.falahos.my (Cloudflare proxied)
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
upstream backend_prayer {
|
||||
# Placeholder for future API upstream (e.g. aladhan.com proxy)
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name moslem02.falahos.my;
|
||||
|
||||
# ── Cloudflare real-ip ─────────────────────
|
||||
# Cloudflare sends visitor IP via CF-Connecting-IP header.
|
||||
# Uncomment and populate with current CF IP ranges:
|
||||
# https://www.cloudflare.com/ips-v4 / ips-v6
|
||||
# real_ip_header CF-Connecting-IP;
|
||||
# real_ip_recursive on;
|
||||
# set_real_ip_from 173.245.48.0/20;
|
||||
# set_real_ip_from 103.21.244.0/22;
|
||||
# … (keep current by syncing from cloudflare.com/ips-v4)
|
||||
|
||||
# ── Static root ────────────────────────────
|
||||
root /var/www/nur-muslim-companion;
|
||||
index index.html;
|
||||
error_page 404 =200 /index.html;
|
||||
|
||||
# ── Gzip ───────────────────────────────────
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 5;
|
||||
gzip_min_length 512;
|
||||
gzip_types
|
||||
text/html
|
||||
text/plain
|
||||
text/css
|
||||
text/javascript
|
||||
application/javascript
|
||||
application/json
|
||||
application/manifest+json
|
||||
image/svg+xml
|
||||
image/x-icon
|
||||
font/woff2;
|
||||
|
||||
# ── Security headers ───────────────────────
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "0" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
# HSTS — only enable once TLS is confirmed working via Cloudflare
|
||||
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
|
||||
|
||||
# ── SPA fallback ───────────────────────────
|
||||
# All routes serve index.html; actual 404s are impossible.
|
||||
# index.html is NOT cached so updates reach clients immediately.
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
# index.html: no-cache (must always fetch fresh version)
|
||||
add_header Cache-Control "no-cache, must-revalidate" always;
|
||||
}
|
||||
|
||||
# ── Hashed static assets (immutable) ───────
|
||||
# Vite emits content-hashed filenames: assets/index-abc123.js
|
||||
# These can be cached forever on CDN and browsers alike.
|
||||
location /assets/ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable, max-age=31536000" always;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# ── Favicon ────────────────────────────────
|
||||
location = /favicon.svg {
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, max-age=604800" always;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# ── PWA manifest ───────────────────────────
|
||||
location = /manifest.webmanifest {
|
||||
expires 1d;
|
||||
add_header Cache-Control "public, max-age=86400" always;
|
||||
add_header Content-Type "application/manifest+json";
|
||||
}
|
||||
|
||||
# ── Service Worker ─────────────────────────
|
||||
# Service-worker script must NOT be cached and must be served
|
||||
# from its own scope (root). Cloudflare bypasses cache for sw.js.
|
||||
location /sw.js {
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
|
||||
add_header Service-Worker-Allowed "/";
|
||||
expires off;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# ── PWA app shell (workbox precached) ──────
|
||||
# workbox-*.js, worker-*.js are versioned hashed files:
|
||||
location ~* \.(js|css|woff2)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable, max-age=31536000" always;
|
||||
}
|
||||
|
||||
# ── Icon files ─────────────────────────────
|
||||
location ~* \.(png|ico)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable, max-age=31536000" always;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# ── Deny hidden files ──────────────────────
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
|
||||
# ── Deny node_modules & src ────────────────
|
||||
location ~ ^/(node_modules|src)/ {
|
||||
deny all;
|
||||
access_log off;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user