Spaces:
Sleeping
Sleeping
Vu Minh Chien
commited on
Commit
Β·
24e8af1
1
Parent(s):
f6902b3
init
Browse files- .dockerignore +81 -0
- Dockerfile +39 -0
- devices.json +22 -0
- package-lock.json +0 -0
- package.json +23 -0
- server.js +685 -0
.dockerignore
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Node.js dependencies
|
| 2 |
+
node_modules/
|
| 3 |
+
npm-debug.log*
|
| 4 |
+
yarn-debug.log*
|
| 5 |
+
yarn-error.log*
|
| 6 |
+
|
| 7 |
+
# Runtime data
|
| 8 |
+
pids
|
| 9 |
+
*.pid
|
| 10 |
+
*.seed
|
| 11 |
+
*.pid.lock
|
| 12 |
+
|
| 13 |
+
# Coverage directory used by tools like istanbul
|
| 14 |
+
coverage/
|
| 15 |
+
|
| 16 |
+
# Logs
|
| 17 |
+
logs
|
| 18 |
+
*.log
|
| 19 |
+
|
| 20 |
+
# Dependency directories
|
| 21 |
+
.npm
|
| 22 |
+
.eslintcache
|
| 23 |
+
|
| 24 |
+
# Optional npm cache directory
|
| 25 |
+
.npm
|
| 26 |
+
|
| 27 |
+
# Optional eslint cache
|
| 28 |
+
.eslintcache
|
| 29 |
+
|
| 30 |
+
# Microbundle cache
|
| 31 |
+
.rpt2_cache/
|
| 32 |
+
.rts2_cache_cjs/
|
| 33 |
+
.rts2_cache_es/
|
| 34 |
+
.rts2_cache_umd/
|
| 35 |
+
|
| 36 |
+
# Optional REPL history
|
| 37 |
+
.node_repl_history
|
| 38 |
+
|
| 39 |
+
# Output of 'npm pack'
|
| 40 |
+
*.tgz
|
| 41 |
+
|
| 42 |
+
# Yarn Integrity file
|
| 43 |
+
.yarn-integrity
|
| 44 |
+
|
| 45 |
+
# dotenv environment variables file
|
| 46 |
+
.env
|
| 47 |
+
.env.local
|
| 48 |
+
.env.development.local
|
| 49 |
+
.env.test.local
|
| 50 |
+
.env.production.local
|
| 51 |
+
|
| 52 |
+
# parcel-bundler cache (https://parceljs.org/)
|
| 53 |
+
.cache
|
| 54 |
+
.parcel-cache
|
| 55 |
+
|
| 56 |
+
# OS generated files
|
| 57 |
+
.DS_Store
|
| 58 |
+
.DS_Store?
|
| 59 |
+
._*
|
| 60 |
+
.Spotlight-V100
|
| 61 |
+
.Trashes
|
| 62 |
+
ehthumbs.db
|
| 63 |
+
Thumbs.db
|
| 64 |
+
|
| 65 |
+
# Git
|
| 66 |
+
.git
|
| 67 |
+
.gitignore
|
| 68 |
+
|
| 69 |
+
# Development files
|
| 70 |
+
README.md
|
| 71 |
+
*.md
|
| 72 |
+
test/
|
| 73 |
+
tests/
|
| 74 |
+
docs/
|
| 75 |
+
|
| 76 |
+
# Firebase service account (should be mounted as secret)
|
| 77 |
+
firebase-service-account.json
|
| 78 |
+
|
| 79 |
+
# Development scripts
|
| 80 |
+
test-*.sh
|
| 81 |
+
test-*.js
|
Dockerfile
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use the official Node.js runtime as base image
|
| 2 |
+
FROM node:18-alpine
|
| 3 |
+
|
| 4 |
+
# Set the working directory
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Copy package.json and package-lock.json
|
| 8 |
+
COPY package*.json ./
|
| 9 |
+
|
| 10 |
+
# Install dependencies
|
| 11 |
+
RUN npm ci --only=production
|
| 12 |
+
|
| 13 |
+
# Copy the rest of the application code
|
| 14 |
+
COPY . .
|
| 15 |
+
|
| 16 |
+
# Create a non-root user
|
| 17 |
+
RUN addgroup -g 1001 -S nodejs
|
| 18 |
+
RUN adduser -S nodejs -u 1001
|
| 19 |
+
|
| 20 |
+
# Change ownership of the app directory to the nodejs user
|
| 21 |
+
RUN chown -R nodejs:nodejs /app
|
| 22 |
+
|
| 23 |
+
# Switch to the non-root user
|
| 24 |
+
USER nodejs
|
| 25 |
+
|
| 26 |
+
# Expose the port that the app runs on
|
| 27 |
+
# Hugging Face Spaces expects the app to run on port 7860
|
| 28 |
+
EXPOSE 7860
|
| 29 |
+
|
| 30 |
+
# Set environment variables
|
| 31 |
+
ENV NODE_ENV=production
|
| 32 |
+
ENV PORT=7860
|
| 33 |
+
|
| 34 |
+
# Health check
|
| 35 |
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
| 36 |
+
CMD node -e "require('http').get('http://localhost:7860', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"
|
| 37 |
+
|
| 38 |
+
# Start the application
|
| 39 |
+
CMD ["node", "server.js"]
|
devices.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
[
|
| 3 |
+
"4903319C-6B6F-45A5-B3E1-1B2B1A0DE705",
|
| 4 |
+
{
|
| 5 |
+
"token": "f4jB7HvitkbLitRnP8PezM:APA91bFpr0zdompJnWqBZwjG_1--WdCnXbUAleDpnpbs_bhdBWGZuJBircYGyiW13JrPJ1OuI5Q3ZzLjZflsWIvvx_XCrnpa5JP5lJOrCcHZCJUdqBXuSzM",
|
| 6 |
+
"lastUpdated": "2025-07-04T01:57:11.482Z",
|
| 7 |
+
"platform": "iOS",
|
| 8 |
+
"appVersion": "1.0.0",
|
| 9 |
+
"registered": true
|
| 10 |
+
}
|
| 11 |
+
],
|
| 12 |
+
[
|
| 13 |
+
"326514BA-AC8D-4F58-AFD8-AEE5708B9AE4",
|
| 14 |
+
{
|
| 15 |
+
"token": "dJz9z8vH0UMVhyRr0-M5Ul:APA91bHE1qqammp3lpv9KCN13yiRUk4EiQqZu0LvWqGtc__r7axaWgBjiTS5oIElfsZpyM6p3vYPv2he8XfbWRQDl1fITivKwvrjjjyeHdgzAcwZtCI55R4",
|
| 16 |
+
"lastUpdated": "2025-07-04T06:40:28.014Z",
|
| 17 |
+
"platform": "iOS",
|
| 18 |
+
"appVersion": "1.0.0",
|
| 19 |
+
"registered": true
|
| 20 |
+
}
|
| 21 |
+
]
|
| 22 |
+
]
|
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "houzou-notification-server",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "Notification server for Houzou Medical App",
|
| 5 |
+
"main": "server.js",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"start": "node server.js",
|
| 8 |
+
"dev": "nodemon server.js"
|
| 9 |
+
},
|
| 10 |
+
"dependencies": {
|
| 11 |
+
"express": "^4.18.2",
|
| 12 |
+
"firebase-admin": "^12.0.0",
|
| 13 |
+
"cors": "^2.8.5",
|
| 14 |
+
"body-parser": "^1.20.2",
|
| 15 |
+
"dotenv": "^16.3.1"
|
| 16 |
+
},
|
| 17 |
+
"devDependencies": {
|
| 18 |
+
"nodemon": "^3.0.2"
|
| 19 |
+
},
|
| 20 |
+
"keywords": ["firebase", "fcm", "notification", "flutter"],
|
| 21 |
+
"author": "Houzou Medical",
|
| 22 |
+
"license": "MIT"
|
| 23 |
+
}
|
server.js
ADDED
|
@@ -0,0 +1,685 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const express = require('express');
|
| 2 |
+
const admin = require('firebase-admin');
|
| 3 |
+
const cors = require('cors');
|
| 4 |
+
const bodyParser = require('body-parser');
|
| 5 |
+
const os = require('os');
|
| 6 |
+
require('dotenv').config();
|
| 7 |
+
|
| 8 |
+
const app = express();
|
| 9 |
+
const PORT = process.env.PORT || 3000;
|
| 10 |
+
|
| 11 |
+
// Middleware
|
| 12 |
+
app.use(cors());
|
| 13 |
+
app.use(bodyParser.json());
|
| 14 |
+
app.use(bodyParser.urlencoded({ extended: true }));
|
| 15 |
+
|
| 16 |
+
// Initialize Firebase Admin SDK
|
| 17 |
+
// Supports both environment variable (production) and file (development)
|
| 18 |
+
try {
|
| 19 |
+
let serviceAccount;
|
| 20 |
+
|
| 21 |
+
if (process.env.FIREBASE_SERVICE_ACCOUNT) {
|
| 22 |
+
// Production: Read from environment variable (Hugging Face Spaces secret)
|
| 23 |
+
console.log('π₯ Loading Firebase service account from environment variable');
|
| 24 |
+
serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT);
|
| 25 |
+
} else {
|
| 26 |
+
// Development: Read from file
|
| 27 |
+
console.log('π₯ Loading Firebase service account from file');
|
| 28 |
+
serviceAccount = require('./firebase-service-account.json');
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
console.log(`π₯ Firebase service account loaded for project: ${serviceAccount.project_id}`);
|
| 32 |
+
|
| 33 |
+
admin.initializeApp({
|
| 34 |
+
credential: admin.credential.cert(serviceAccount),
|
| 35 |
+
});
|
| 36 |
+
|
| 37 |
+
console.log('β
Firebase Admin SDK initialized successfully');
|
| 38 |
+
} catch (error) {
|
| 39 |
+
console.error('β Firebase Admin SDK initialization failed:', error);
|
| 40 |
+
if (!process.env.FIREBASE_SERVICE_ACCOUNT) {
|
| 41 |
+
console.error('π‘ Please check that firebase-service-account.json exists and has correct permissions');
|
| 42 |
+
console.error('π‘ Or set FIREBASE_SERVICE_ACCOUNT environment variable for production');
|
| 43 |
+
} else {
|
| 44 |
+
console.error('π‘ Please check that FIREBASE_SERVICE_ACCOUNT environment variable contains valid JSON');
|
| 45 |
+
}
|
| 46 |
+
process.exit(1);
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
// Sample products data (matching your Flutter app)
|
| 50 |
+
const sampleProducts = [
|
| 51 |
+
{ id: 1, title: "NMN 10000mg Ultra", price: "Β₯8,800", category: "Anti-Aging" },
|
| 52 |
+
{ id: 2, title: "Arginine & Citrulline", price: "Β₯5,200", category: "Sports Nutrition" },
|
| 53 |
+
{ id: 3, title: "Broccoli Sprout Extract", price: "Β₯3,600", category: "Detox & Cleanse" },
|
| 54 |
+
{ id: 4, title: "Sun Protection Plus", price: "Β₯4,200", category: "Skin Health" },
|
| 55 |
+
{ id: 5, title: "Alpha-GPC Cognitive", price: "Β₯6,500", category: "Brain Health" },
|
| 56 |
+
{ id: 6, title: "Multivitamin Complete", price: "Β₯2,800", category: "General Health" },
|
| 57 |
+
];
|
| 58 |
+
|
| 59 |
+
const fs = require('fs');
|
| 60 |
+
const path = require('path');
|
| 61 |
+
|
| 62 |
+
// File storage for FCM tokens
|
| 63 |
+
const DEVICES_FILE = path.join(__dirname, 'devices.json');
|
| 64 |
+
|
| 65 |
+
// Load devices from file or create empty storage
|
| 66 |
+
let deviceTokens = new Map();
|
| 67 |
+
|
| 68 |
+
function loadDevicesFromFile() {
|
| 69 |
+
try {
|
| 70 |
+
if (fs.existsSync(DEVICES_FILE)) {
|
| 71 |
+
const data = fs.readFileSync(DEVICES_FILE, 'utf8');
|
| 72 |
+
const devicesArray = JSON.parse(data);
|
| 73 |
+
deviceTokens = new Map(devicesArray);
|
| 74 |
+
console.log(`π Loaded ${deviceTokens.size} devices from file`);
|
| 75 |
+
} else {
|
| 76 |
+
console.log('π No devices file found, starting fresh');
|
| 77 |
+
}
|
| 78 |
+
} catch (error) {
|
| 79 |
+
console.error('β Error loading devices file:', error);
|
| 80 |
+
deviceTokens = new Map();
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
function saveDevicesToFile() {
|
| 85 |
+
try {
|
| 86 |
+
const devicesArray = Array.from(deviceTokens.entries());
|
| 87 |
+
fs.writeFileSync(DEVICES_FILE, JSON.stringify(devicesArray, null, 2));
|
| 88 |
+
console.log(`πΎ Saved ${deviceTokens.size} devices to file`);
|
| 89 |
+
} catch (error) {
|
| 90 |
+
console.error('β Error saving devices file:', error);
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// Load devices on startup
|
| 95 |
+
loadDevicesFromFile();
|
| 96 |
+
|
| 97 |
+
// Routes
|
| 98 |
+
|
| 99 |
+
// Health check
|
| 100 |
+
app.get('/', (req, res) => {
|
| 101 |
+
res.json({
|
| 102 |
+
message: 'Houzou Medical Notification Server',
|
| 103 |
+
status: 'running',
|
| 104 |
+
timestamp: new Date().toISOString()
|
| 105 |
+
});
|
| 106 |
+
});
|
| 107 |
+
|
| 108 |
+
// Test Firebase connectivity
|
| 109 |
+
app.get('/test-firebase', async (req, res) => {
|
| 110 |
+
try {
|
| 111 |
+
console.log('π₯ Testing Firebase connectivity...');
|
| 112 |
+
|
| 113 |
+
// Try to get Firebase project info
|
| 114 |
+
const projectId = admin.app().options.projectId;
|
| 115 |
+
console.log(`π± Project ID: ${projectId}`);
|
| 116 |
+
|
| 117 |
+
// Try a simple Firebase operation
|
| 118 |
+
const testMessage = {
|
| 119 |
+
notification: {
|
| 120 |
+
title: 'Test',
|
| 121 |
+
body: 'Firebase connectivity test'
|
| 122 |
+
},
|
| 123 |
+
token: 'test-token-that-will-fail' // This will fail but test auth
|
| 124 |
+
};
|
| 125 |
+
|
| 126 |
+
try {
|
| 127 |
+
await admin.messaging().send(testMessage);
|
| 128 |
+
} catch (testError) {
|
| 129 |
+
console.log(`π Test message error (expected):`, testError.code);
|
| 130 |
+
|
| 131 |
+
if (testError.code === 'messaging/invalid-registration-token') {
|
| 132 |
+
// This is expected - it means Firebase auth is working
|
| 133 |
+
res.json({
|
| 134 |
+
success: true,
|
| 135 |
+
message: 'Firebase connectivity OK',
|
| 136 |
+
projectId: projectId,
|
| 137 |
+
note: 'Authentication working (test token failed as expected)'
|
| 138 |
+
});
|
| 139 |
+
return;
|
| 140 |
+
} else if (testError.code && testError.code.includes('auth')) {
|
| 141 |
+
// Auth error - this is the real problem
|
| 142 |
+
throw testError;
|
| 143 |
+
} else {
|
| 144 |
+
// Other error but auth seems OK
|
| 145 |
+
res.json({
|
| 146 |
+
success: true,
|
| 147 |
+
message: 'Firebase connectivity OK',
|
| 148 |
+
projectId: projectId,
|
| 149 |
+
note: `Test completed with expected error: ${testError.code}`
|
| 150 |
+
});
|
| 151 |
+
return;
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
res.json({
|
| 156 |
+
success: true,
|
| 157 |
+
message: 'Firebase connectivity OK',
|
| 158 |
+
projectId: projectId
|
| 159 |
+
});
|
| 160 |
+
|
| 161 |
+
} catch (error) {
|
| 162 |
+
console.error('β Firebase test failed:', error);
|
| 163 |
+
res.status(500).json({
|
| 164 |
+
success: false,
|
| 165 |
+
error: error.message,
|
| 166 |
+
code: error.code
|
| 167 |
+
});
|
| 168 |
+
}
|
| 169 |
+
});
|
| 170 |
+
|
| 171 |
+
// Send notification to navigate to Home
|
| 172 |
+
app.post('/send-home-notification', async (req, res) => {
|
| 173 |
+
try {
|
| 174 |
+
const { token, title, body } = req.body;
|
| 175 |
+
|
| 176 |
+
if (!token) {
|
| 177 |
+
return res.status(400).json({ error: 'FCM token is required' });
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
const message = {
|
| 181 |
+
notification: {
|
| 182 |
+
title: title || 'Welcome Back!',
|
| 183 |
+
body: body || 'Check out our latest health supplements',
|
| 184 |
+
},
|
| 185 |
+
data: {
|
| 186 |
+
type: 'home',
|
| 187 |
+
click_action: 'FLUTTER_NOTIFICATION_CLICK',
|
| 188 |
+
},
|
| 189 |
+
token: token,
|
| 190 |
+
};
|
| 191 |
+
|
| 192 |
+
const response = await admin.messaging().send(message);
|
| 193 |
+
|
| 194 |
+
res.json({
|
| 195 |
+
success: true,
|
| 196 |
+
messageId: response,
|
| 197 |
+
message: 'Home notification sent successfully'
|
| 198 |
+
});
|
| 199 |
+
|
| 200 |
+
} catch (error) {
|
| 201 |
+
console.error('Error sending home notification:', error);
|
| 202 |
+
res.status(500).json({
|
| 203 |
+
success: false,
|
| 204 |
+
error: error.message
|
| 205 |
+
});
|
| 206 |
+
}
|
| 207 |
+
});
|
| 208 |
+
|
| 209 |
+
// Send notification to navigate to Product Detail
|
| 210 |
+
app.post('/send-product-notification', async (req, res) => {
|
| 211 |
+
try {
|
| 212 |
+
const { token, productId, title, body } = req.body;
|
| 213 |
+
|
| 214 |
+
if (!token) {
|
| 215 |
+
return res.status(400).json({ error: 'FCM token is required' });
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
if (!productId) {
|
| 219 |
+
return res.status(400).json({ error: 'Product ID is required' });
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
// Find product by ID
|
| 223 |
+
const product = sampleProducts.find(p => p.id.toString() === productId.toString());
|
| 224 |
+
|
| 225 |
+
if (!product) {
|
| 226 |
+
return res.status(404).json({ error: 'Product not found' });
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
const message = {
|
| 230 |
+
notification: {
|
| 231 |
+
title: title || `New Deal: ${product.title}`,
|
| 232 |
+
body: body || `Special offer on ${product.title} - ${product.price}. Tap to view details!`,
|
| 233 |
+
},
|
| 234 |
+
data: {
|
| 235 |
+
type: 'product_detail',
|
| 236 |
+
product_id: productId.toString(),
|
| 237 |
+
product_title: product.title,
|
| 238 |
+
product_price: product.price,
|
| 239 |
+
click_action: 'FLUTTER_NOTIFICATION_CLICK',
|
| 240 |
+
},
|
| 241 |
+
token: token,
|
| 242 |
+
};
|
| 243 |
+
|
| 244 |
+
const response = await admin.messaging().send(message);
|
| 245 |
+
|
| 246 |
+
res.json({
|
| 247 |
+
success: true,
|
| 248 |
+
messageId: response,
|
| 249 |
+
message: 'Product notification sent successfully',
|
| 250 |
+
product: product
|
| 251 |
+
});
|
| 252 |
+
|
| 253 |
+
} catch (error) {
|
| 254 |
+
console.error('Error sending product notification:', error);
|
| 255 |
+
res.status(500).json({
|
| 256 |
+
success: false,
|
| 257 |
+
error: error.message
|
| 258 |
+
});
|
| 259 |
+
}
|
| 260 |
+
});
|
| 261 |
+
|
| 262 |
+
// Send notification to multiple devices (topic)
|
| 263 |
+
app.post('/send-topic-notification', async (req, res) => {
|
| 264 |
+
try {
|
| 265 |
+
const { topic, title, body, type, productId } = req.body;
|
| 266 |
+
|
| 267 |
+
if (!topic) {
|
| 268 |
+
return res.status(400).json({ error: 'Topic is required' });
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
let data = {
|
| 272 |
+
type: type || 'home',
|
| 273 |
+
click_action: 'FLUTTER_NOTIFICATION_CLICK',
|
| 274 |
+
};
|
| 275 |
+
|
| 276 |
+
// Add product data if it's a product notification
|
| 277 |
+
if (type === 'product_detail' && productId) {
|
| 278 |
+
const product = sampleProducts.find(p => p.id.toString() === productId.toString());
|
| 279 |
+
if (product) {
|
| 280 |
+
data.product_id = productId.toString();
|
| 281 |
+
data.product_title = product.title;
|
| 282 |
+
data.product_price = product.price;
|
| 283 |
+
}
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
const message = {
|
| 287 |
+
notification: {
|
| 288 |
+
title: title || 'Houzou Medical',
|
| 289 |
+
body: body || 'New update available!',
|
| 290 |
+
},
|
| 291 |
+
data: data,
|
| 292 |
+
topic: topic,
|
| 293 |
+
};
|
| 294 |
+
|
| 295 |
+
const response = await admin.messaging().send(message);
|
| 296 |
+
|
| 297 |
+
res.json({
|
| 298 |
+
success: true,
|
| 299 |
+
messageId: response,
|
| 300 |
+
message: `Topic notification sent successfully to ${topic}`
|
| 301 |
+
});
|
| 302 |
+
|
| 303 |
+
} catch (error) {
|
| 304 |
+
console.error('Error sending topic notification:', error);
|
| 305 |
+
res.status(500).json({
|
| 306 |
+
success: false,
|
| 307 |
+
error: error.message
|
| 308 |
+
});
|
| 309 |
+
}
|
| 310 |
+
});
|
| 311 |
+
|
| 312 |
+
// Get sample products
|
| 313 |
+
app.get('/products', (req, res) => {
|
| 314 |
+
res.json({
|
| 315 |
+
success: true,
|
| 316 |
+
products: sampleProducts
|
| 317 |
+
});
|
| 318 |
+
});
|
| 319 |
+
|
| 320 |
+
// Test notification endpoint
|
| 321 |
+
app.post('/test-notification', async (req, res) => {
|
| 322 |
+
try {
|
| 323 |
+
const { token } = req.body;
|
| 324 |
+
|
| 325 |
+
if (!token) {
|
| 326 |
+
return res.status(400).json({ error: 'FCM token is required for testing' });
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
// Send a test notification to home
|
| 330 |
+
const homeMessage = {
|
| 331 |
+
notification: {
|
| 332 |
+
title: 'π Test Home Notification',
|
| 333 |
+
body: 'This will take you to the home screen!',
|
| 334 |
+
},
|
| 335 |
+
data: {
|
| 336 |
+
type: 'home',
|
| 337 |
+
click_action: 'FLUTTER_NOTIFICATION_CLICK',
|
| 338 |
+
},
|
| 339 |
+
token: token,
|
| 340 |
+
};
|
| 341 |
+
|
| 342 |
+
const homeResponse = await admin.messaging().send(homeMessage);
|
| 343 |
+
|
| 344 |
+
// Send a test notification for product detail (random product)
|
| 345 |
+
const randomProduct = sampleProducts[Math.floor(Math.random() * sampleProducts.length)];
|
| 346 |
+
|
| 347 |
+
const productMessage = {
|
| 348 |
+
notification: {
|
| 349 |
+
title: 'ποΈ Test Product Notification',
|
| 350 |
+
body: `Check out ${randomProduct.title} - ${randomProduct.price}`,
|
| 351 |
+
},
|
| 352 |
+
data: {
|
| 353 |
+
type: 'product_detail',
|
| 354 |
+
product_id: randomProduct.id.toString(),
|
| 355 |
+
product_title: randomProduct.title,
|
| 356 |
+
product_price: randomProduct.price,
|
| 357 |
+
click_action: 'FLUTTER_NOTIFICATION_CLICK',
|
| 358 |
+
},
|
| 359 |
+
token: token,
|
| 360 |
+
};
|
| 361 |
+
|
| 362 |
+
const productResponse = await admin.messaging().send(productMessage);
|
| 363 |
+
|
| 364 |
+
res.json({
|
| 365 |
+
success: true,
|
| 366 |
+
message: 'Test notifications sent successfully',
|
| 367 |
+
results: {
|
| 368 |
+
home: homeResponse,
|
| 369 |
+
product: productResponse,
|
| 370 |
+
testedProduct: randomProduct
|
| 371 |
+
}
|
| 372 |
+
});
|
| 373 |
+
|
| 374 |
+
} catch (error) {
|
| 375 |
+
console.error('Error sending test notifications:', error);
|
| 376 |
+
res.status(500).json({
|
| 377 |
+
success: false,
|
| 378 |
+
error: error.message
|
| 379 |
+
});
|
| 380 |
+
}
|
| 381 |
+
});
|
| 382 |
+
|
| 383 |
+
// Register/Update FCM Token
|
| 384 |
+
app.post('/register-token', async (req, res) => {
|
| 385 |
+
try {
|
| 386 |
+
const { token, deviceId, platform, appVersion } = req.body;
|
| 387 |
+
|
| 388 |
+
if (!token || !deviceId) {
|
| 389 |
+
return res.status(400).json({
|
| 390 |
+
success: false,
|
| 391 |
+
error: 'FCM token and device ID are required'
|
| 392 |
+
});
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
// Store token with metadata
|
| 396 |
+
deviceTokens.set(deviceId, {
|
| 397 |
+
token: token,
|
| 398 |
+
lastUpdated: new Date().toISOString(),
|
| 399 |
+
platform: platform || 'unknown',
|
| 400 |
+
appVersion: appVersion || '1.0.0',
|
| 401 |
+
registered: true
|
| 402 |
+
});
|
| 403 |
+
|
| 404 |
+
console.log(`π± Token registered for device ${deviceId} (${platform})`);
|
| 405 |
+
|
| 406 |
+
// Auto-subscribe to 'all_users' topic for broadcast notifications
|
| 407 |
+
try {
|
| 408 |
+
await admin.messaging().subscribeToTopic([token], 'all_users');
|
| 409 |
+
console.log(`β
Device ${deviceId} subscribed to 'all_users' topic`);
|
| 410 |
+
} catch (topicError) {
|
| 411 |
+
console.warn(`β οΈ Failed to subscribe device to topic: ${topicError.message}`);
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
// Save to file
|
| 415 |
+
saveDevicesToFile();
|
| 416 |
+
|
| 417 |
+
res.json({
|
| 418 |
+
success: true,
|
| 419 |
+
message: 'FCM token registered successfully',
|
| 420 |
+
deviceCount: deviceTokens.size
|
| 421 |
+
});
|
| 422 |
+
|
| 423 |
+
} catch (error) {
|
| 424 |
+
console.error('Error registering token:', error);
|
| 425 |
+
res.status(500).json({
|
| 426 |
+
success: false,
|
| 427 |
+
error: error.message
|
| 428 |
+
});
|
| 429 |
+
}
|
| 430 |
+
});
|
| 431 |
+
|
| 432 |
+
// Unregister FCM Token
|
| 433 |
+
app.post('/unregister-token', async (req, res) => {
|
| 434 |
+
try {
|
| 435 |
+
const { deviceId } = req.body;
|
| 436 |
+
|
| 437 |
+
if (!deviceId) {
|
| 438 |
+
return res.status(400).json({
|
| 439 |
+
success: false,
|
| 440 |
+
error: 'Device ID is required'
|
| 441 |
+
});
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
const deviceInfo = deviceTokens.get(deviceId);
|
| 445 |
+
if (deviceInfo) {
|
| 446 |
+
// Unsubscribe from topics
|
| 447 |
+
try {
|
| 448 |
+
await admin.messaging().unsubscribeFromTopic([deviceInfo.token], 'all_users');
|
| 449 |
+
} catch (topicError) {
|
| 450 |
+
console.warn(`β οΈ Failed to unsubscribe device from topic: ${topicError.message}`);
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
deviceTokens.delete(deviceId);
|
| 454 |
+
console.log(`π± Token unregistered for device ${deviceId}`);
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
// Save to file
|
| 458 |
+
saveDevicesToFile();
|
| 459 |
+
|
| 460 |
+
res.json({
|
| 461 |
+
success: true,
|
| 462 |
+
message: 'FCM token unregistered successfully',
|
| 463 |
+
deviceCount: deviceTokens.size
|
| 464 |
+
});
|
| 465 |
+
|
| 466 |
+
} catch (error) {
|
| 467 |
+
console.error('Error unregistering token:', error);
|
| 468 |
+
res.status(500).json({
|
| 469 |
+
success: false,
|
| 470 |
+
error: error.message
|
| 471 |
+
});
|
| 472 |
+
}
|
| 473 |
+
});
|
| 474 |
+
|
| 475 |
+
// Get registered devices info
|
| 476 |
+
app.get('/devices', (req, res) => {
|
| 477 |
+
const devices = Array.from(deviceTokens.entries()).map(([deviceId, info]) => ({
|
| 478 |
+
deviceId,
|
| 479 |
+
platform: info.platform,
|
| 480 |
+
appVersion: info.appVersion,
|
| 481 |
+
lastUpdated: info.lastUpdated,
|
| 482 |
+
registered: info.registered
|
| 483 |
+
}));
|
| 484 |
+
|
| 485 |
+
res.json({
|
| 486 |
+
success: true,
|
| 487 |
+
deviceCount: devices.length,
|
| 488 |
+
devices: devices
|
| 489 |
+
});
|
| 490 |
+
});
|
| 491 |
+
|
| 492 |
+
// Debug endpoint to manually add test device
|
| 493 |
+
app.post('/debug-add-device', (req, res) => {
|
| 494 |
+
try {
|
| 495 |
+
const { deviceId, token, platform } = req.body;
|
| 496 |
+
|
| 497 |
+
if (!deviceId || !token) {
|
| 498 |
+
return res.status(400).json({
|
| 499 |
+
success: false,
|
| 500 |
+
error: 'deviceId and token are required'
|
| 501 |
+
});
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
deviceTokens.set(deviceId, {
|
| 505 |
+
token: token,
|
| 506 |
+
lastUpdated: new Date().toISOString(),
|
| 507 |
+
platform: platform || 'debug',
|
| 508 |
+
appVersion: '1.0.0',
|
| 509 |
+
registered: true
|
| 510 |
+
});
|
| 511 |
+
|
| 512 |
+
saveDevicesToFile();
|
| 513 |
+
|
| 514 |
+
console.log(`π§ Debug: Added device ${deviceId} (${platform})`);
|
| 515 |
+
|
| 516 |
+
res.json({
|
| 517 |
+
success: true,
|
| 518 |
+
message: 'Device added for testing',
|
| 519 |
+
deviceCount: deviceTokens.size
|
| 520 |
+
});
|
| 521 |
+
|
| 522 |
+
} catch (error) {
|
| 523 |
+
console.error('Error adding debug device:', error);
|
| 524 |
+
res.status(500).json({
|
| 525 |
+
success: false,
|
| 526 |
+
error: error.message
|
| 527 |
+
});
|
| 528 |
+
}
|
| 529 |
+
});
|
| 530 |
+
|
| 531 |
+
// Send notification to ALL registered devices
|
| 532 |
+
app.post('/send-broadcast-notification', async (req, res) => {
|
| 533 |
+
try {
|
| 534 |
+
const { title, body, type, productId } = req.body;
|
| 535 |
+
|
| 536 |
+
if (deviceTokens.size === 0) {
|
| 537 |
+
return res.status(400).json({
|
| 538 |
+
success: false,
|
| 539 |
+
error: 'No devices registered'
|
| 540 |
+
});
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
// Prepare message data
|
| 544 |
+
let data = {
|
| 545 |
+
type: type || 'home',
|
| 546 |
+
click_action: 'FLUTTER_NOTIFICATION_CLICK',
|
| 547 |
+
};
|
| 548 |
+
|
| 549 |
+
// Add product data if it's a product notification
|
| 550 |
+
if (type === 'product_detail' && productId) {
|
| 551 |
+
const product = sampleProducts.find(p => p.id.toString() === productId.toString());
|
| 552 |
+
if (product) {
|
| 553 |
+
data.product_id = productId.toString();
|
| 554 |
+
data.product_title = product.title;
|
| 555 |
+
data.product_price = product.price;
|
| 556 |
+
}
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
// Get all active tokens
|
| 560 |
+
const tokens = Array.from(deviceTokens.values()).map(device => device.token);
|
| 561 |
+
|
| 562 |
+
// Send to all devices using multicast
|
| 563 |
+
const message = {
|
| 564 |
+
notification: {
|
| 565 |
+
title: title || 'Houzou Medical',
|
| 566 |
+
body: body || 'New update available!',
|
| 567 |
+
},
|
| 568 |
+
data: data,
|
| 569 |
+
tokens: tokens,
|
| 570 |
+
};
|
| 571 |
+
|
| 572 |
+
console.log(`π€ Attempting to send notification to ${tokens.length} devices`);
|
| 573 |
+
console.log(`π Message data:`, JSON.stringify(message, null, 2));
|
| 574 |
+
|
| 575 |
+
const response = await admin.messaging().sendEachForMulticast(message);
|
| 576 |
+
|
| 577 |
+
console.log(`π Send results: Success=${response.successCount}, Failed=${response.failureCount}`);
|
| 578 |
+
|
| 579 |
+
// Handle failed tokens with detailed logging
|
| 580 |
+
if (response.failureCount > 0) {
|
| 581 |
+
console.log(`β Detailed failure analysis:`);
|
| 582 |
+
const failedTokens = [];
|
| 583 |
+
|
| 584 |
+
response.responses.forEach((resp, idx) => {
|
| 585 |
+
const token = tokens[idx];
|
| 586 |
+
const deviceId = Array.from(deviceTokens.entries()).find(([id, info]) => info.token === token)?.[0];
|
| 587 |
+
|
| 588 |
+
if (!resp.success) {
|
| 589 |
+
console.log(`β Device ${deviceId} failed:`, resp.error);
|
| 590 |
+
console.log(` Error code: ${resp.error?.code}`);
|
| 591 |
+
console.log(` Error message: ${resp.error?.message}`);
|
| 592 |
+
|
| 593 |
+
// Only remove tokens for specific errors (not auth errors)
|
| 594 |
+
const errorCode = resp.error?.code;
|
| 595 |
+
const shouldRemoveToken = [
|
| 596 |
+
'messaging/invalid-registration-token',
|
| 597 |
+
'messaging/registration-token-not-registered'
|
| 598 |
+
].includes(errorCode);
|
| 599 |
+
|
| 600 |
+
if (shouldRemoveToken) {
|
| 601 |
+
failedTokens.push(token);
|
| 602 |
+
console.log(`ποΈ Marking token for removal: ${deviceId} (${errorCode})`);
|
| 603 |
+
} else {
|
| 604 |
+
console.log(`β οΈ Keeping token for ${deviceId} - temporary error: ${errorCode}`);
|
| 605 |
+
}
|
| 606 |
+
} else {
|
| 607 |
+
console.log(`β
Device ${deviceId} notification sent successfully`);
|
| 608 |
+
}
|
| 609 |
+
});
|
| 610 |
+
|
| 611 |
+
// Remove only truly invalid tokens
|
| 612 |
+
if (failedTokens.length > 0) {
|
| 613 |
+
console.log(`ποΈ Removing ${failedTokens.length} invalid tokens`);
|
| 614 |
+
failedTokens.forEach(failedToken => {
|
| 615 |
+
for (const [deviceId, info] of deviceTokens.entries()) {
|
| 616 |
+
if (info.token === failedToken) {
|
| 617 |
+
console.log(`ποΈ Removing invalid token for device ${deviceId}`);
|
| 618 |
+
deviceTokens.delete(deviceId);
|
| 619 |
+
break;
|
| 620 |
+
}
|
| 621 |
+
}
|
| 622 |
+
});
|
| 623 |
+
saveDevicesToFile();
|
| 624 |
+
} else {
|
| 625 |
+
console.log(`β οΈ No tokens removed - all failures appear to be temporary`);
|
| 626 |
+
}
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
res.json({
|
| 630 |
+
success: true,
|
| 631 |
+
message: `Broadcast notification sent to ${response.successCount} devices`,
|
| 632 |
+
results: {
|
| 633 |
+
successCount: response.successCount,
|
| 634 |
+
failureCount: response.failureCount,
|
| 635 |
+
totalDevices: deviceTokens.size
|
| 636 |
+
}
|
| 637 |
+
});
|
| 638 |
+
|
| 639 |
+
} catch (error) {
|
| 640 |
+
console.error('Error sending broadcast notification:', error);
|
| 641 |
+
res.status(500).json({
|
| 642 |
+
success: false,
|
| 643 |
+
error: error.message
|
| 644 |
+
});
|
| 645 |
+
}
|
| 646 |
+
});
|
| 647 |
+
|
| 648 |
+
// Error handling middleware
|
| 649 |
+
app.use((error, req, res, next) => {
|
| 650 |
+
console.error('Server error:', error);
|
| 651 |
+
res.status(500).json({
|
| 652 |
+
success: false,
|
| 653 |
+
error: 'Internal server error'
|
| 654 |
+
});
|
| 655 |
+
});
|
| 656 |
+
|
| 657 |
+
// Get local IP address
|
| 658 |
+
function getLocalIPAddress() {
|
| 659 |
+
const interfaces = os.networkInterfaces();
|
| 660 |
+
for (const devName in interfaces) {
|
| 661 |
+
const iface = interfaces[devName];
|
| 662 |
+
for (let i = 0; i < iface.length; i++) {
|
| 663 |
+
const alias = iface[i];
|
| 664 |
+
if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
|
| 665 |
+
return alias.address;
|
| 666 |
+
}
|
| 667 |
+
}
|
| 668 |
+
}
|
| 669 |
+
return 'localhost';
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
+
// Start server
|
| 673 |
+
app.listen(PORT, () => {
|
| 674 |
+
const localIP = getLocalIPAddress();
|
| 675 |
+
|
| 676 |
+
console.log(`π Houzou Medical Notification Server running on port ${PORT}`);
|
| 677 |
+
console.log(`π± Ready to send notifications to your Flutter app!`);
|
| 678 |
+
console.log(`\nπ Server URLs:`);
|
| 679 |
+
console.log(` Local: http://localhost:${PORT}`);
|
| 680 |
+
console.log(` Network: http://${localIP}:${PORT}`);
|
| 681 |
+
console.log(`\nπ§ For iPhone app, use: http://${localIP}:${PORT}`);
|
| 682 |
+
console.log(`π Devices saved to: ${DEVICES_FILE}`);
|
| 683 |
+
});
|
| 684 |
+
|
| 685 |
+
module.exports = app;
|