diff --git a/ak/config.uts b/ak/config.uts index 6035d50e..f54de0f3 100644 --- a/ak/config.uts +++ b/ak/config.uts @@ -16,6 +16,9 @@ export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIj export const WS_URL: string = 'ws://192.168.1.62:18000/realtime/v1/websocket' // export const WS_URL: string = 'ws://192.168.1.63:18000/realtime/v1/websocket' +// 推送服务地址(用于本地调试,可改为 http://:7301) +export const PUSH_SERVER_URL: string = 'http://192.168.1.62:7301' + // 备用配置(已注释,如需切换可取消注释) // 开发环境 - 其他内网地址 // export const SUPA_URL: string = 'http://192.168.0.150:8080' diff --git a/pages/mall/delivery/webhook-server/README.md b/pages/mall/delivery/webhook-server/README.md new file mode 100644 index 00000000..4aae44f2 --- /dev/null +++ b/pages/mall/delivery/webhook-server/README.md @@ -0,0 +1,73 @@ +# Webhook 接收器 — 说明 + +路径:`pages/mall/delivery/server/webhook-receiver.js` + +目的:接收承运方或 Mock Server 的 HTTP 回调(POST /webhook/express/status),将原始回文写入 `platform_express_event_raw`,并按项目现有映射更新 `platform_express_waybills` 与写入 `platform_express_tracking_events`。 + +环境变量(必须/可选): +- `SUPA_URL`:Supabase REST 地址(示例 `http://192.168.1.62:18000`) +- `SUPA_KEY`:Supabase service_role 或 anon key(用于 REST 写入) +- `WEBHOOK_SECRET`(可选):与第三方共享的 HMAC-SHA256 secret,用于校验 `X-Signature`(签名为 hex) +- `PORT`(可选):接收器监听端口,默认 `7201` + +启动(PowerShell): +```powershell +$env:SUPA_URL='http://192.168.1.62:18000' +$env:SUPA_KEY='your_service_role_key' +# 可选验签 +$env:WEBHOOK_SECRET='your-secret' +node pages/mall/delivery/server/webhook-receiver.js +``` + +启动(Linux / macOS / WSL): +```bash +export SUPA_URL='http://192.168.1.62:18000' +export SUPA_KEY='your_service_role_key' +export WEBHOOK_SECRET='your-secret' # optional +node pages/mall/delivery/server/webhook-receiver.js +``` + +测试(curl 模拟第三方推送): +```bash +BODY='{"mailNo":"TEST_123","infoContent":"SENT","remark":"派送中","acceptTime":"2026-02-25 12:00:00","carrier":"YTO"}' +TS=$(date -u +%Y-%m-%dT%H:%M:%SZ) +# 如果启用了 WEBHOOK_SECRET,计算签名: +SIG=$(printf "%s%s" "$BODY" "$TS" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -hex | awk '{print $2}') + +curl -i -X POST http://localhost:7201/webhook/express/status \ + -H "Content-Type: application/json" \ + -H "X-Timestamp: $TS" \ + -H "X-Client-Id: mock_carrier" \ + -H "X-Signature: $SIG" \ + -d "$BODY" +``` + +预期:接口返回 200 JSON {ok:true}(若未找到对应运单会返回 {ok:false, message:'waybill not found'})。 + +验证写入(查看 Supabase): +```bash +# 示例:列最近 5 条原始回文 +curl -s -H "apikey: $SUPA_KEY" -H "Authorization: Bearer $SUPA_KEY" \ + "$SUPA_URL/rest/v1/platform_express_event_raw?select=*&order=received_at.desc&limit=5" | jq . + +# 查看最近轨迹事件 +curl -s -H "apikey: $SUPA_KEY" -H "Authorization: Bearer $SUPA_KEY" \ + "$SUPA_URL/rest/v1/platform_express_tracking_events?select=*&order=created_at.desc&limit=5" | jq . +``` + +与仓库中 Mock 实现的关系: +- `pages/mall/delivery/test/mock-service.uts` 已包含写库逻辑(`pushWebhookData`、`bindShipment`、`runScenario`)。新接收器复用了同样的入库思路,但以 HTTP/REST 的形式对外暴露。 + +注意事项: +- 本接收器通过 Supabase REST API 直接写表,使用 `SUPA_KEY` 时请确保权限与密钥安全(不要将 service_role key 公开到前端)。 +- 如果你的 Supabase 接口部署在内网,请确保接收器能访问 `SUPA_URL`(网络/防火墙)。 +- 若需要更严格的验签/重放检测/幂等,请告知,我可以把这些能力加入接收器(例如记录并比对 dedupe_key 或检查 X-Timestamp 时间窗口)。 + +下一步建议: +- 若你要把第三方直接接到该接收器,请把 `WEBHOOK_SECRET` 与对方约定并启用验签。 +- 若需要我加重放防护或返回 4xx/5xx 更精确的逻辑,也可继续实现。 + +文件位置: +- [Webhook 接收器](pages/mall/delivery/server/webhook-receiver.js) + +作者:自动生成(可手动调整) diff --git a/pages/mall/delivery/webhook-server/webhook-receiver.js b/pages/mall/delivery/webhook-server/webhook-receiver.js new file mode 100644 index 00000000..f9833a93 --- /dev/null +++ b/pages/mall/delivery/webhook-server/webhook-receiver.js @@ -0,0 +1,186 @@ +const express = require('express') +const bodyParser = require('body-parser') +const fetch = require('node-fetch') +const crypto = require('crypto') + +const PORT = process.env.PORT || 7201 +const SUPA_URL = (process.env.SUPA_URL || process.env.SUPA_URL_OVERRIDE || '').replace(/\/$/, '') +const SUPA_KEY = process.env.SUPA_KEY || process.env.SERVICE_ROLE_KEY || '' +const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || '' // optional HMAC secret + +function supaFetch(path, opts = {}) { + const url = `${SUPA_URL}/rest/v1/${path}` + const headers = Object.assign({}, opts.headers || {}, { + apikey: SUPA_KEY, + Authorization: `Bearer ${SUPA_KEY}`, + Accept: 'application/json' + }) + return fetch(url, Object.assign({}, opts, { headers })) +} + +function computeSignature(bodyText, ts) { + if (!WEBHOOK_SECRET) return '' + const h = crypto.createHmac('sha256', WEBHOOK_SECRET) + h.update(bodyText + (ts || '')) + return h.digest('hex') +} + +async function upsertRaw(payload, tracking_no, carrier, signature_valid) { + try { + const body = { + carrier: carrier || null, + tracking_no: tracking_no || null, + body: payload, + received_at: new Date().toISOString(), + signature_valid: signature_valid + } + const resp = await supaFetch('platform_express_event_raw', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }) + return resp + } catch (e) { + console.warn('upsertRaw error', e) + return null + } +} + +async function findWaybillId(tracking_no, order_no) { + try { + if (tracking_no) { + const r = await supaFetch(`platform_express_waybills?tracking_no=eq.${encodeURIComponent(tracking_no)}`) + if (r.ok) { + const data = await r.json() + if (data && data.length > 0) return data[0].id + } + } + if (order_no) { + const r2 = await supaFetch(`platform_express_waybills?order_no=eq.${encodeURIComponent(order_no)}`) + if (r2.ok) { + const data2 = await r2.json() + if (data2 && data2.length > 0) return data2[0].id + } + } + return null + } catch (e) { + console.warn('findWaybillId error', e) + return null + } +} + +function mapStatus(inStatus) { + let s = 'IN_TRANSIT' + if (!inStatus) return s + const v = String(inStatus).toUpperCase() + if (['GOT','SEND','TRANSIT'].indexOf(v) > -1) s = 'IN_TRANSIT' + else if (v === 'SENT') s = 'OUT_FOR_DELIVERY' + else if (v === 'PICKUP') s = 'READY_FOR_PICKUP' + else if (v === 'SIGNED' || v === 'DELIVERED') s = 'DELIVERED' + else if (v === 'FAILED' || v === 'EXCEPTION') s = 'EXCEPTION' + else if (v === 'RETURNED') s = 'RETURNED' + else { + const valid = ['ORDER_PLACED','SHIPPED','IN_TRANSIT','OUT_FOR_DELIVERY','READY_FOR_PICKUP','DELIVERED','EXCEPTION','RETURNED'] + if (valid.indexOf(v) > -1) s = v + } + return s +} + +async function updateWaybill(id, status_code, text) { + try { + const now = new Date().toISOString() + await supaFetch(`platform_express_waybills?id=eq.${encodeURIComponent(id)}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json', Prefer: 'return=representation' }, + body: JSON.stringify({ current_status_code: status_code, current_status_text: text, last_synced_at: now }) + }) + } catch (e) { + console.warn('updateWaybill error', e) + } +} + +async function insertEvent(event) { + try { + await supaFetch('platform_express_tracking_events', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(event) + }) + } catch (e) { console.warn('insertEvent error', e) } +} + +async function start() { + if (!SUPA_URL || !SUPA_KEY) { + console.error('SUPA_URL and SUPA_KEY must be set in env') + process.exit(1) + } + + const app = express() + app.use(bodyParser.json({ limit: '1mb' })) + + app.post('/webhook/express/status', async (req, res) => { + const ts = req.headers['x-timestamp'] || req.headers['X-TIMESTAMP'] || '' + const sig = req.headers['x-signature'] || req.headers['X-SIGNATURE'] || '' + const cid = req.headers['x-client-id'] || req.headers['X-CLIENT-ID'] || '' + const bodyText = JSON.stringify(req.body || {}) + + let sigValid = true + if (WEBHOOK_SECRET) { + const calc = computeSignature(bodyText, ts) + sigValid = calc === String(sig) + } + + // persist raw + await upsertRaw(req.body || {}, req.body && (req.body.mailNo || req.body.tracking_no), req.body && req.body.carrier, sigValid) + + // find waybill + const tracking_no = req.body && (req.body.mailNo || req.body.tracking_no) + const order_no = req.body && (req.body.txLogisticId || req.body.order_no) + const carrier = req.body && req.body.carrier ? req.body.carrier : (req.body && req.body.company || null) + const event_code = req.body && (req.body.infoContent || req.body.status_code || req.body.event_code) + const event_text = req.body && (req.body.remark || req.body.event_text || '') + + const waybillId = await findWaybillId(tracking_no, order_no) + if (!waybillId) { + // Waybill not found — respond 200 but inform caller in body. + return res.status(200).json({ ok: false, message: 'waybill not found' }) + } + + const status_code = mapStatus(event_code) + + // update waybill + await updateWaybill(waybillId, status_code, event_text) + + // parse event_time + let event_time = new Date().toISOString() + if (req.body && req.body.acceptTime) { + try { + const t = req.body.acceptTime.indexOf('T') > -1 ? req.body.acceptTime : req.body.acceptTime.replace(' ', 'T') + const dt = new Date(t) + if (!isNaN(dt.getTime())) event_time = dt.toISOString() + } catch (e) {} + } + + // insert event + const eventPayload = { + waybill_id: waybillId, + carrier: carrier, + tracking_no: tracking_no || null, + event_time: event_time, + event_code: event_code || 'UNKNOWN', + event_text: event_text || '', + status_code: status_code, + raw_payload: req.body || {}, + dedupe_key: 'WEBHOOK_' + Date.now() + } + await insertEvent(eventPayload) + + return res.json({ ok: true }) + }) + + app.get('/health', (req, res) => res.json({ ok: true })) + + app.listen(PORT, '0.0.0.0', () => console.log(`Webhook receiver listening on http://0.0.0.0:${PORT}`)) +} + +start().catch(e => { console.error('start failed', e); process.exit(1) }) diff --git a/pages/user/login.uvue b/pages/user/login.uvue index 124dfc1c..74a34fe4 100644 --- a/pages/user/login.uvue +++ b/pages/user/login.uvue @@ -142,7 +142,7 @@ import { ref, onMounted } from 'vue' import supa from '@/components/supadb/aksupainstance.uts' import { AkReq } from '@/uni_modules/ak-req/index.uts' -import { IS_TEST_MODE } from '@/ak/config.uts' +import { IS_TEST_MODE, PUSH_SERVER_URL } from '@/ak/config.uts' import { getCurrentUser, logout, setIsLoggedIn, setUserProfile } from '@/utils/store.uts' import { UserProfile } from '@/pages/user/types.uts' @@ -351,24 +351,26 @@ const handleLogin = async () => { console.log('用户ID已保存到本地存储:', uid) // 获取并上报推送 CID(若可用) try { - uni.getPushClientId({ + uni.getPushClientId({ success: async (res: any) => { const cid = (res && (res.clientid || res.cid)) ? (res.clientid || res.cid) : (typeof res === 'string' ? res : null) if (cid != null && cid !== '') { try { uni.setStorageSync('uni_push2_cid', cid) } catch (e) {} - try { - await AkReq.request({ - url: '/api/v1/push/register', - method: 'POST', - data: { cid, platform: 'android' }, - contentType: 'application/json' - }) - console.log('CID 已上报后台:', cid) - } catch (e) { - console.warn('上报 CID 失败:', e) - } + try { + const uidStored = uni.getStorageSync('user_id') || null + const currentUid = uidStored || (currentSession.user ? currentSession.user.getString('id') : null) + await AkReq.request({ + url: `${PUSH_SERVER_URL}/api/v1/push/register`, + method: 'POST', + data: { cid, platform: 'android', user_id: currentUid }, + contentType: 'application/json' + }) + console.log('CID 已上报后台:', cid, 'user_id:', currentUid) + } catch (e) { + console.warn('上报 CID 失败:', e) + } } }, fail: (err: any) => { diff --git a/pages/user/register.uvue b/pages/user/register.uvue index 6999562b..b708c9f9 100644 --- a/pages/user/register.uvue +++ b/pages/user/register.uvue @@ -95,6 +95,7 @@ import supa from '@/components/supadb/aksupainstance.uts' import { ensureUserProfile } from '@/utils/sapi.uts' import { AkReq } from '@/uni_modules/ak-req/index.uts' + import { PUSH_SERVER_URL } from '@/ak/config.uts' // 响应式数据 const email = ref('') @@ -302,10 +303,13 @@ if (cid != null && cid !== '') { try { uni.setStorageSync('uni_push2_cid', cid) } catch (e) {} try { + const uidStored = uni.getStorageSync('user_id') || null + const currentSession = supa.getSession() + const currentUid = uidStored || (currentSession && currentSession.user ? currentSession.user.getString('id') : null) await AkReq.request({ - url: '/api/v1/push/register', + url: `${PUSH_SERVER_URL}/api/v1/push/register`, method: 'POST', - data: { cid, platform: 'android' }, + data: { cid, platform: 'android', user_id: currentUid }, contentType: 'application/json' }) console.log('CID 已上报后台:', cid) diff --git a/server/README.md b/server/README.md index 340d845d..6d650a63 100644 --- a/server/README.md +++ b/server/README.md @@ -1,3 +1,67 @@ +如需我在本地使用你的 Supabase 凭证演示一次完整的注册→查询→发送流程,或将持久化切换为仅 Supabase(移除本地 JSON 回退),请直接告诉我你的选择并提供测试凭证或确认权限范围。 + +--- + +## 故障排查记录(已执行) + +- 我们在仓库中运行了两个辅助脚本以排查 Supabase 凭证问题: + - `server/probe_supa.js`:对单个 key 发起 REST 请求并打印响应(用于快速验证)。 + - `server/try_supa_keys.js`:扫描仓库中形似 JWT 的字符串并逐一尝试对 Supabase REST 发起请求,打印每个 candidate 的 HTTP 状态与响应预览。 + +- 排查结果摘要: + - 在仓库中发现 6 个看起来像 JWT 的候选 key,均对 `GET /rest/v1/push_devices?user_id=eq.` 返回 HTTP 401(包括 `PGRST301` / `Invalid authentication credentials`)。 + - 说明这些仓库内的字符串不是当前 Supabase 项目可用的 `service_role` key,或 Supabase 项目/Key 已被替换/撤销,或请求的 Supabase URL 与当时不同。 + +## 推荐操作(二选一) + +### A)使用正确的 Supabase `service_role` key(推荐) + +1. 在 Supabase 控制台:Project → Settings → API → Service role key,复制该 key(仅在后端使用,切勿公开)。 +2. 在服务器启动前注入环境变量并启动: + +```powershell +$env:SUPA_URL='http://192.168.1.62:18000' +$env:SUPA_KEY='(粘贴你的 service_role key)' +node server/push-server.js +``` + +或后台运行并记录日志: + +```powershell +$env:SUPA_URL='http://192.168.1.62:18000' +$env:SUPA_KEY='(你的 key)' +Start-Process node -ArgumentList 'server/push-server.js' -RedirectStandardOutput '.\\server.log' -RedirectStandardError '.\\server.err' -PassThru +``` + +运行后可检查 `server.log`(包含 `supaFetch` 调试输出),并用以下命令验证: + +```powershell +Invoke-RestMethod -Uri 'http://localhost:7301/api/v1/push/devices?user_id=' -Method GET | ConvertTo-Json -Depth 5 +``` + +### B)绕过 Supabase(仅用于本地快速验证) + +如果你只是想立即验证下发链路,可把已知的 device(cid + user_id)写入本地 `server/data/push_devices.json`,然后执行 mock 推送: + +```powershell +# 查看本地 devices 文件 +Get-Content .\server\data\push_devices.json -Raw | ConvertFrom-Json + +# 手动追加(示例,替换 cid 与 user_id) +$devs = Get-Content .\server\data\push_devices.json -Raw | ConvertFrom-Json +$devs += @{ cid='d9aa69ec415...'; user_id='a8e3a568-fc1f-4237-bcc5-5722e2fca0a3'; platform='android'; created_at=(Get-Date).ToString('o'); updated_at=(Get-Date).ToString('o'); active=$true } +$devs | ConvertTo-Json -Depth 5 | Set-Content .\server\data\push_devices.json -Encoding utf8 + +# 然后测试发送(mock) +Invoke-RestMethod -Uri 'http://localhost:7301/api/v1/push/send' -Method POST -ContentType 'application/json' -Body (@{ user_id='a8e3a568-fc1f-4237-bcc5-5722e2fca0a3'; notification=@{ title='测试'; body='hello' } } | ConvertTo-Json) +``` + +## 已添加的辅助脚本 + +- `server/probe_supa.js` — 对单个 key 发起请求并打印响应(用于快速验证)。 +- `server/try_supa_keys.js` — 在仓库中查找 JWT 样式字符串并逐一尝试调用 Supabase REST,打印每个 candidate 的状态与响应预览。 + +如果你把正确的 `service_role` key 提供给我(仅用于本次会话),我可以代为注入环境、重启服务并把 `supaFetch` 的调试输出和最终结果贴回给你;或我可以现在把截图中已有的 cid/user_id 写入本地 JSON 并立即做一次 mock 下发测试。 # Push Server (开发用) 这是一个用于本地开发与调试的轻量 Node.js 推送后端(mock)。 @@ -21,3 +85,110 @@ npm start 默认监听端口:`7301`,可通过 `PORT` 环境变量修改。 设备存储在 `server/data/push_devices.json`,用于本地持久化。 + +可选:直接写入 Supabase + +服务器可以可选地将 `push_devices` 记录直接写入 Supabase/Postgres(适用于生产或集成测试)。启用方法:在启动前设置下列环境变量: + +- `SUPA_URL` — Supabase 项目 URL,例如 `https://xyzcompany.supabase.co` +- `SUPA_KEY` — Supabase `service_role` key(仅在服务器端使用,切勿暴露给客户端) + +当这两个变量存在时,服务会通过 PostgREST REST API 向 `/rest/v1/push_devices` 发起 upsert(使用 `on_conflict=cid`),并在注销时将设备的 `is_active` 标记为 `false`。同时本地 JSON 存储仍会保留为回退。 + +示例(PowerShell): + +```powershell +$env:SUPA_URL = 'https://xyz.supabase.co' +$env:SUPA_KEY = 'eyJhbGci...' +npm start +``` + +请确保你的 Supabase 项目已经包含与仓库迁移脚本匹配的 `push_devices` 表: +`pages/mall/delivery/doc/需求文档/20260224_add_push_devices_and_notifications.sql`。 + +Supabase 表结构与权限(快速参考) + +下面是 `push_devices` 表的最小字段集合,供你在 Supabase 中快速验证或手动创建: + +```sql +CREATE TABLE public.push_devices ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid NULL, + cid varchar(255) NOT NULL, + platform varchar(32) NOT NULL DEFAULT 'android', + appid varchar(128) NOT NULL DEFAULT 'default', + is_active boolean NOT NULL DEFAULT true, + last_seen_at timestamptz NULL, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); +CREATE UNIQUE INDEX IF NOT EXISTS ux_push_devices_appid_cid ON public.push_devices(appid, cid); +``` + +权限与安全建议: +- 生产环境请务必只在后端使用 `service_role`(`SUPA_KEY`),不要将其暴露给客户端。后端应负责向 Supabase 写入/更新设备记录。 +- 如果你需要通过客户端直接调用 Supabase REST(不推荐),请配置 RLS 策略并只允许安全的最小操作。 +- 推荐做法:后端使用 `service_role` 写入;客户端只调用后端的 `/api/v1/push/register`。 + +示例:使用 `service_role` 通过 PostgREST 插入设备(等同于本服务中执行的请求): + +```bash +curl -X POST "${SUPA_URL}/rest/v1/push_devices?on_conflict=cid" \ + -H "apikey: ${SUPA_KEY}" \ + -H "Authorization: Bearer ${SUPA_KEY}" \ + -H "Content-Type: application/json" \ + -d '[{"cid":"CID_TEST_001","user_id":"","platform":"android","appid":"default","is_active":true}]' +``` + +注意事项: +- 如果你使用的是 Supabase 的 `uuid_generate_v4()` 功能,可能需要在数据库安装 `uuid-ossp` 或使用 `gen_random_uuid()`(pgcrypto)。迁移脚本中已包含兼容性处理。 +- 本服务对 Supabase 的写入为异步非阻塞:写入失败不会阻止本地响应,但会在日志中记录错误。可根据需要修改为同步错误返回策略。 + +如需我把本地 JSON 回退移除并只保留 Supabase 持久化(需你确认并提供测试凭证),我可以继续实现并更新代码与文档。 + +## 服务说明(快速参考) + +**服务简介** + +- **目的**:为开发/集成阶段提供一个轻量的推送后端,用于接收客户端上报的推送客户端 ID(CID)、管理设备状态,并在开发中触发/代理推送以验证链路。 +- **主要职责**:接收设备注册/注销、列出设备、触发推送(mock 或代理到真实推送服务)、可选将设备写入 Supabase。 + +**暴露的接口(HTTP)** +- `GET /health`:健康检查 +- `POST /api/v1/push/register`:注册或更新设备,body: `{ cid, user_id, platform, ... }` +- `POST /api/v1/push/unregister`:注销设备,body: `{ cid | user_id }` +- `GET /api/v1/push/devices`:列出设备,query: `user_id`, `active` +- `POST /api/v1/push/send`:发送推送(开发环境为 mock;可代理到 `PUSH_PROXY_URL`) + +**数据与持久化** +- 开发默认将设备保存在本地文件:`server/data/push_devices.json`(回退/离线使用)。 +- 可选:当设置 `SUPA_URL` 和 `SUPA_KEY` 时,服务会通过 Supabase REST(PostgREST)向 `push_devices` 表 upsert/patch 同步设备记录(异步、非阻塞)。 + +**重要环境变量** +- `PORT`:监听端口(默认 7301) +- `PUSH_PROXY_URL` / `PUSH_PROXY_TOKEN`:将 `/api/v1/push/send` 代理到真实推送服务 +- `SUPA_URL` / `SUPA_KEY` / `SUPA_SCHEMA`:启用 Supabase 同步(`SUPA_KEY` 应为服务端 `service_role`,仅服务器端使用) + +**安全与部署注意** +- 切勿将 `SUPA_KEY`(service_role)暴露给客户端;只能在后端使用。 +- 生产场景应:后端持有凭证并负责写入/验证;对客户端使用后端接口,不直接对 Supabase 写入。 +- 如直接允许客户端写 Supabase,必须开启 RLS 并最小化权限(一般不推荐)。 + +**表结构与迁移** +- 已提供迁移脚本:`pages/mall/delivery/doc/需求文档/20260224_add_push_devices_and_notifications.sql`(包含 `push_devices` 与 `express_notifications` 表及触发器、索引)。 + +**快速测试示例** +PowerShell: +```powershell +Invoke-RestMethod -Uri 'http://localhost:7301/api/v1/push/register' -Method POST -ContentType 'application/json' -Body '{"cid":"CID_TEST_001","user_id":"user-1","platform":"android"}' +``` +curl(示例 upsert 到 Supabase,需替换变量): +```bash +curl -X POST "${SUPA_URL}/rest/v1/push_devices?on_conflict=cid" \ + -H "apikey: ${SUPA_KEY}" \ + -H "Authorization: Bearer ${SUPA_KEY}" \ + -H "Content-Type: application/json" \ + -d '[{"cid":"CID_TEST_001","user_id":"","platform":"android","appid":"default","is_active":true}]' +``` + +如需我在本地使用你的 Supabase 凭证演示一次完整的注册→查询→发送流程,或将持久化切换为仅 Supabase(移除本地 JSON 回退),请直接告诉我你的选择并提供测试凭证或确认权限范围。 diff --git a/server/data/push_devices.json b/server/data/push_devices.json index fe51488c..5660177b 100644 --- a/server/data/push_devices.json +++ b/server/data/push_devices.json @@ -1 +1,10 @@ -[] +[ + { + "cid": "CID_TEST_001", + "user_id": "user-1", + "platform": "android", + "created_at": "2026-02-24T03:48:20.907Z", + "updated_at": "2026-02-24T03:48:20.907Z", + "active": true + } +] \ No newline at end of file diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 00000000..e9407d4e --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,898 @@ +{ + "name": "mall-push-server", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mall-push-server", + "version": "0.1.0", + "dependencies": { + "body-parser": "^1.20.2", + "cors": "^2.8.5", + "express": "^4.18.2", + "node-fetch": "^2.6.12" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } +} diff --git a/server/probe_supa.js b/server/probe_supa.js new file mode 100644 index 00000000..f9e88d03 --- /dev/null +++ b/server/probe_supa.js @@ -0,0 +1,28 @@ +const fetch = require('node-fetch') + +const SUPA = process.env.SUPA_URL || process.env.SUPA || 'http://192.168.1.62:18000' +// 支持两种环境变量名:SUPA_KEY 或 SERVICE_ROLE_KEY(自托管 .env 里常见) +const KEY = process.env.SUPA_KEY || process.env.SERVICE_ROLE_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UtMSIsImlhdCI6MTc2OTY3NjQ5OCwiZXhwIjoxOTI3MzU2NDk4fQ.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'; +const USER = 'a8e3a568-fc1f-4237-bcc5-5722e2fca0a3'; + +(async () => { + try { + const url = `${SUPA.replace(/\/$/, '')}/rest/v1/push_devices?user_id=eq.${encodeURIComponent(USER)}` + console.log('GET', url) + const resp = await fetch(url, { + method: 'GET', + headers: { + apikey: KEY, + Authorization: `Bearer ${KEY}`, + Accept: 'application/json' + }, + timeout: 10000 + }) + console.log('status', resp.status) + const txt = await resp.text() + console.log('body:', txt) + } catch (e) { + console.error('probe error', e && e.message ? e.message : e) + process.exit(1) + } +})() diff --git a/server/push-server.js b/server/push-server.js index f68793ca..82596cbc 100644 --- a/server/push-server.js +++ b/server/push-server.js @@ -8,6 +8,10 @@ const fetch = require('node-fetch') const PORT = process.env.PORT || 7301 const DATA_DIR = path.join(__dirname, 'data') const DEVICES_FILE = path.join(DATA_DIR, 'push_devices.json') +const SUPA_URL = process.env.SUPA_URL || '' +// 支持两种环境变量名:SUPA_KEY(旧)和 SERVICE_ROLE_KEY(常用于自托管 .env) +const SUPA_KEY = process.env.SUPA_KEY || process.env.SERVICE_ROLE_KEY || '' +const SUPA_SCHEMA = process.env.SUPA_SCHEMA || 'public' async function ensureDataDir() { try { @@ -36,6 +40,52 @@ async function writeDevices(devices) { await fs.writeFile(DEVICES_FILE, JSON.stringify(devices, null, 2), 'utf8') } +async function supaFetch(path, opts = {}) { + const url = `${SUPA_URL.replace(/\/$/, '')}/rest/v1/${path}` + const headers = Object.assign({}, opts.headers || {}, { + apikey: SUPA_KEY, + Authorization: `Bearer ${SUPA_KEY}`, + Accept: 'application/json' + }) + try { + console.log('supaFetch ->', url) + const resp = await fetch(url, Object.assign({}, opts, { headers })) + if (!resp.ok) { + let txt = '' + try { txt = await resp.text() } catch (e) { txt = `` } + console.warn('supaFetch failed', resp.status, url, txt) + } else { + // optional: peek at small JSON responses for debugging + try { + const clone = resp.clone() + const maybeText = await clone.text() + if (maybeText && maybeText.length < 2000) console.log('supaFetch response preview:', maybeText) + } catch (e) { + // ignore preview errors + } + } + return resp + } catch (e) { + console.warn('supaFetch exception', e, url) + throw e + } +} + +async function getDevicesFromSupabase({ user_id, active } = {}) { + let q = 'push_devices' + const params = [] + if (user_id) params.push(`user_id=eq.${encodeURIComponent(user_id)}`) + if (active != null) params.push(`is_active=eq.${encodeURIComponent(active ? 'true' : 'false')}`) + const suffix = params.length ? `?${params.join('&')}` : '' + const resp = await supaFetch(`${q}${suffix}`, { method: 'GET' }) + if (!resp.ok) { + const txt = await resp.text() + throw new Error(`supabase get devices failed ${resp.status} ${txt}`) + } + const data = await resp.json() + return data +} + async function start() { await ensureDataDir() const app = express() @@ -61,6 +111,33 @@ async function start() { devices.push(found) } await writeDevices(devices) + // 如果配置了 Supabase,则尝试写入 supabase 表(异步记录错误) + if (SUPA_URL && SUPA_KEY) { + (async () => { + try { + const body = { + cid: found.cid, + user_id: found.user_id, + platform: found.platform, + appid: found.appid || 'default', + is_active: true, + last_seen_at: found.updated_at + } + const resp = await supaFetch(`push_devices?on_conflict=cid`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', Prefer: 'return=representation' }, + body: JSON.stringify(body) + }) + if (!resp.ok) { + const txt = await resp.text() + console.warn('Supabase upsert push_device failed:', resp.status, txt) + } + } catch (e) { + console.warn('Supabase upsert exception:', e) + } + })() + } + return res.json({ ok: true, cid }) }) @@ -75,16 +152,49 @@ async function start() { devices = devices.map(d => d.user_id === user_id ? Object.assign({}, d, { active: false, updated_at: new Date().toISOString() }) : d) } await writeDevices(devices) + // 同步到 Supabase(如果可用) + if (SUPA_URL && SUPA_KEY) { + (async () => { + try { + const now = new Date().toISOString() + if (cid) { + await supaFetch(`push_devices?cid=eq.${encodeURIComponent(cid)}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json', Prefer: 'return=representation' }, + body: JSON.stringify({ is_active: false, updated_at: now }) + }) + } else if (user_id) { + await supaFetch(`push_devices?user_id=eq.${encodeURIComponent(user_id)}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json', Prefer: 'return=representation' }, + body: JSON.stringify({ is_active: false, updated_at: now }) + }) + } + } catch (e) { + console.warn('Supabase unregister exception:', e) + } + })() + } + return res.json({ ok: true }) }) // 列出设备 app.get('/api/v1/push/devices', async (req, res) => { const { user_id, active } = req.query - let devices = await readDevices() - if (user_id) devices = devices.filter(d => String(d.user_id) === String(user_id)) - if (active != null) devices = devices.filter(d => String(!!d.active) === String(active === 'true')) - res.json({ ok: true, total: devices.length, data: devices }) + try { + if (SUPA_URL && SUPA_KEY) { + const devices = await getDevicesFromSupabase({ user_id, active: active == null ? undefined : (active === 'true') }) + return res.json({ ok: true, total: devices.length, data: devices }) + } + let devices = await readDevices() + if (user_id) devices = devices.filter(d => String(d.user_id) === String(user_id)) + if (active != null) devices = devices.filter(d => String(!!d.active) === String(active === 'true')) + res.json({ ok: true, total: devices.length, data: devices }) + } catch (e) { + console.warn('list devices failed:', e) + res.status(500).json({ ok: false, error: String(e) }) + } }) // 发送推送(mock 或代理到真实 provider,当环境变量 PUSH_PROXY_URL 设置时会代理请求) @@ -95,8 +205,19 @@ async function start() { let targets = [] if (cids && cids.length > 0) targets = cids else if (user_id) { - const devices = await readDevices() - targets = devices.filter(d => String(d.user_id) === String(user_id) && d.active).map(d => d.cid) + if (SUPA_URL && SUPA_KEY) { + try { + const devices = await getDevicesFromSupabase({ user_id, active: true }) + targets = devices.map(d => d.cid) + } catch (e) { + console.warn('Supabase query devices failed, falling back to local:', e) + const devices = await readDevices() + targets = devices.filter(d => String(d.user_id) === String(user_id) && d.active).map(d => d.cid) + } + } else { + const devices = await readDevices() + targets = devices.filter(d => String(d.user_id) === String(user_id) && d.active).map(d => d.cid) + } } // 如果配置了 PUSH_PROXY_URL 则转发到外部推送服务(例如 uni-push2 的服务端 API) @@ -124,8 +245,8 @@ async function start() { return res.json({ ok: true, mocked: true, sent: targets.length }) }) - app.listen(PORT, () => { - console.log(`Push server listening on http://localhost:${PORT}`) + app.listen(PORT, '0.0.0.0', () => { + console.log(`Push server listening on http://0.0.0.0:${PORT}`) }) } diff --git a/server/try_supa_keys.js b/server/try_supa_keys.js new file mode 100644 index 00000000..da25adad --- /dev/null +++ b/server/try_supa_keys.js @@ -0,0 +1,52 @@ +const fs = require('fs') +const path = require('path') +const fetch = require('node-fetch') + +const ROOT = path.resolve(__dirname, '..') +const SUPA = 'http://192.168.1.62:18000' +const USER = 'a8e3a568-fc1f-4237-bcc5-5722e2fca0a3' + +function findJwtStrings(dir) { + const out = new Set() + const files = fs.readdirSync(dir, { withFileTypes: true }) + for (const f of files) { + try { + const full = path.join(dir, f.name) + if (f.isDirectory()) { + if (f.name === 'node_modules' || f.name === '.git') continue + for (const s of findJwtStrings(full)) out.add(s) + } else { + const txt = fs.readFileSync(full, 'utf8') + const re = /eyJ[0-9A-Za-z_-]{30,}\.[0-9A-Za-z_-]{30,}\.[0-9A-Za-z_-]{30,}/g + const m = txt.match(re) + if (m) m.forEach(x => out.add(x)) + } + } catch (e) { + // ignore + } + } + return out +} + +(async () => { + const keys = Array.from(findJwtStrings(ROOT)) + console.log('Found', keys.length, 'candidate keys') + for (const k of keys) { + try { + const url = `${SUPA.replace(/\/$/, '')}/rest/v1/push_devices?user_id=eq.${encodeURIComponent(USER)}` + process.stdout.write('\nTrying key prefix: ' + k.substring(0,20) + '...') + const resp = await fetch(url, { + method: 'GET', + headers: { apikey: k, Authorization: `Bearer ${k}`, Accept: 'application/json' }, + timeout: 10000 + }) + const status = resp.status + const body = await resp.text() + console.log(' status=', status) + console.log('body preview:', body && body.length > 1000 ? body.substring(0,1000) + '...' : body) + } catch (e) { + console.log(' error', e && e.message ? e.message : e) + } + } + if (keys.length === 0) console.log('No JWT-like keys found in repo') +})()