socket pager
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
# Pager Socket.IO Testing Guide
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Start your NestJS server:**
|
||||
```bash
|
||||
npm run start:dev
|
||||
```
|
||||
|
||||
2. **Open the test page:**
|
||||
- Open `test-pager-socket.html` in your web browser
|
||||
- Or serve it via a simple HTTP server:
|
||||
```bash
|
||||
# Using Python
|
||||
python3 -m http.server 8080
|
||||
# Then open http://localhost:8080/test-pager-socket.html
|
||||
|
||||
# Using Node.js (if you have http-server installed)
|
||||
npx http-server -p 8080
|
||||
```
|
||||
|
||||
## Testing Scenarios
|
||||
|
||||
### Scenario 1: Test as Restaurant Admin/Staff
|
||||
|
||||
1. **Get your admin JWT token:**
|
||||
- Login as admin via your API to get the JWT token
|
||||
- Example:
|
||||
```bash
|
||||
curl -X POST http://localhost:4000/admin/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "admin@example.com",
|
||||
"password": "password"
|
||||
}'
|
||||
```
|
||||
- Copy the `accessToken` from the response
|
||||
|
||||
2. **Connect to the server:**
|
||||
- Enter server URL: `http://localhost:4000`
|
||||
- Enter your admin JWT token in the "Admin Token" field
|
||||
- Click "Connect"
|
||||
|
||||
3. **Join restaurant room:**
|
||||
- Select "Restaurant (Admin/Staff)" from User Type
|
||||
- Click "Join Room"
|
||||
- The restaurant ID will be automatically extracted from your token
|
||||
|
||||
3. **Create a pager request (via API):**
|
||||
```bash
|
||||
curl -X POST http://localhost:4000/public/pager \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Slug: your-restaurant-slug" \
|
||||
-d '{
|
||||
"tableNumber": "5",
|
||||
"message": "Need assistance"
|
||||
}'
|
||||
```
|
||||
|
||||
4. **Observe the event:**
|
||||
- You should see `pager:created` event in the Events Log
|
||||
|
||||
### Scenario 2: Test as Public User
|
||||
|
||||
1. **Create a pager request first (to get cookie ID):**
|
||||
```bash
|
||||
curl -X POST http://localhost:4000/public/pager \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Slug: your-restaurant-slug" \
|
||||
-d '{
|
||||
"tableNumber": "3",
|
||||
"message": "Need water"
|
||||
}' \
|
||||
-c cookies.txt
|
||||
```
|
||||
|
||||
2. **Get the cookie ID:**
|
||||
- Check the response or cookies.txt file for `pagerId` cookie value
|
||||
|
||||
3. **Connect and join session:**
|
||||
- Connect to the server
|
||||
- Select "Session (Public User)" from User Type
|
||||
- Enter the cookie ID from step 2
|
||||
- Click "Join Room"
|
||||
|
||||
4. **Update pager status (via API as admin):**
|
||||
```bash
|
||||
curl -X PATCH http://localhost:4000/admin/pager/{pagerId}/status \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
|
||||
-d '{
|
||||
"status": "acknowledged"
|
||||
}'
|
||||
```
|
||||
|
||||
5. **Observe the event:**
|
||||
- You should see `pager:status:updated` event in the Events Log
|
||||
|
||||
## Socket Events Reference
|
||||
|
||||
### Client → Server Events
|
||||
|
||||
| Event | Payload | Description |
|
||||
|-------|---------|-------------|
|
||||
| `join:restaurant` | `{}` (empty, restId extracted from token) | Join restaurant room for admin/staff. **Requires authentication via JWT token in connection auth** |
|
||||
| `join:session` | `{ cookieId: string }` | Join session room for public user |
|
||||
| `leave:room` | `{ room: string }` | Leave a specific room |
|
||||
|
||||
**Note:** For `join:restaurant`, the restaurant ID is automatically extracted from the authenticated admin's JWT token. You must provide the token when connecting:
|
||||
|
||||
```javascript
|
||||
const socket = io('http://localhost:4000/pager', {
|
||||
auth: {
|
||||
token: 'your-jwt-token-here' // or 'Bearer your-jwt-token-here'
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Server → Client Events
|
||||
|
||||
| Event | Payload | Description |
|
||||
|-------|---------|-------------|
|
||||
| `pager:created` | `{ pager: {...} }` | New pager request created |
|
||||
| `pager:status:updated` | `{ pager: {...} }` | Pager status updated |
|
||||
| `joined` | `{ room: string, message: string }` | Successfully joined a room |
|
||||
| `left` | `{ room: string, message: string }` | Successfully left a room |
|
||||
| `error` | `{ message: string }` | Error occurred |
|
||||
|
||||
## Room Names
|
||||
|
||||
- **Restaurant Room:** `restaurant:{restId}`
|
||||
- **Session Room:** `cookie:{cookieId}`
|
||||
|
||||
## Testing Tips
|
||||
|
||||
1. **Open multiple browser tabs/windows** to simulate multiple clients
|
||||
2. **Use browser DevTools** to inspect WebSocket connections
|
||||
3. **Check server logs** to see gateway activity
|
||||
4. **Test reconnection** by disconnecting and reconnecting
|
||||
5. **Test room isolation** by joining different rooms in different tabs
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Connection fails:** Check if server is running on the correct port
|
||||
- **No events received:** Verify you've joined the correct room
|
||||
- **CORS errors:** Ensure CORS is enabled in your NestJS app (already configured in gateway)
|
||||
- **Events not showing:** Check browser console for errors
|
||||
|
||||
Generated
+325
-1
@@ -32,9 +32,11 @@
|
||||
"@nestjs/mapped-types": "*",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/platform-fastify": "^11.1.8",
|
||||
"@nestjs/platform-socket.io": "^11.1.9",
|
||||
"@nestjs/schedule": "^6.0.1",
|
||||
"@nestjs/swagger": "^11.2.1",
|
||||
"@nestjs/throttler": "^6.4.0",
|
||||
"@types/socket.io": "^3.0.1",
|
||||
"axios": "^1.13.1",
|
||||
"bullmq": "^5.65.1",
|
||||
"cache-manager": "^7.2.4",
|
||||
@@ -45,6 +47,7 @@
|
||||
"firebase-admin": "^13.6.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"socket.io": "^4.8.1",
|
||||
"ulid": "^3.0.1",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
@@ -4871,6 +4874,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/platform-socket.io": {
|
||||
"version": "11.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.9.tgz",
|
||||
"integrity": "sha512-OaAW+voXo5BXbFKd9Ot3SL05tEucRMhZRdw5wdWZf/RpIl9hB6G6OHr8DDxNbUGvuQWzNnZHCDHx3EQJzjcIyA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"socket.io": "4.8.1",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/nest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^11.0.0",
|
||||
"@nestjs/websockets": "^11.0.0",
|
||||
"rxjs": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/schedule": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-6.0.1.tgz",
|
||||
@@ -5054,6 +5077,30 @@
|
||||
"reflect-metadata": "^0.1.13 || ^0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/websockets": {
|
||||
"version": "11.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.1.9.tgz",
|
||||
"integrity": "sha512-kkkdeTVcc3X7ZzvVqUVpOAJoh49kTRUjWNUXo5jmG+27OvZoHfs/vuSiqxidrrbIgydSqN15HUsf1wZwQUrxCQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"iterare": "1.2.1",
|
||||
"object-hash": "3.0.0",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^11.0.0",
|
||||
"@nestjs/core": "^11.0.0",
|
||||
"@nestjs/platform-socket.io": "^11.0.0",
|
||||
"reflect-metadata": "^0.1.12 || ^0.2.0",
|
||||
"rxjs": "^7.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@nestjs/platform-socket.io": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||
@@ -6010,6 +6057,12 @@
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tokenizer/inflate": {
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz",
|
||||
@@ -6161,6 +6214,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/cors": {
|
||||
"version": "2.8.19",
|
||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
|
||||
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/eslint": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
||||
@@ -6414,6 +6476,15 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/socket.io": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.1.tgz",
|
||||
"integrity": "sha512-XSma2FhVD78ymvoxYV4xGXrIH/0EKQ93rR+YR0Y+Kw1xbPzLDCip/UWSejZ08FpxYeYNci/PZPQS9anrvJRqMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"socket.io": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/stack-utils": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
|
||||
@@ -7691,6 +7762,15 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64id": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
||||
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^4.5.0 || >= 5.9"
|
||||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.8.19",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.19.tgz",
|
||||
@@ -8855,6 +8935,95 @@
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io": {
|
||||
"version": "6.6.4",
|
||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
|
||||
"integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/node": ">=10.0.0",
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "2.0.0",
|
||||
"cookie": "~0.7.2",
|
||||
"cors": "~2.8.5",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.17.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io/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/engine.io/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io/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/engine.io/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/engine.io/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/enhanced-resolve": {
|
||||
"version": "5.18.3",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
||||
@@ -12937,7 +13106,6 @@
|
||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
||||
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
@@ -14393,6 +14561,141 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io": {
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
|
||||
"integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "~2.0.0",
|
||||
"cors": "~2.8.5",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io": "~6.6.0",
|
||||
"socket.io-adapter": "~2.5.2",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-adapter": {
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
|
||||
"integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "~4.3.4",
|
||||
"ws": "~8.17.1"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-adapter/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io/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/socket.io/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io/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/socket.io/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/socket.io/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/sonic-boom": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz",
|
||||
@@ -16165,6 +16468,27 @@
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
||||
@@ -47,9 +47,11 @@
|
||||
"@nestjs/mapped-types": "*",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/platform-fastify": "^11.1.8",
|
||||
"@nestjs/platform-socket.io": "^11.1.9",
|
||||
"@nestjs/schedule": "^6.0.1",
|
||||
"@nestjs/swagger": "^11.2.1",
|
||||
"@nestjs/throttler": "^6.4.0",
|
||||
"@types/socket.io": "^3.0.1",
|
||||
"axios": "^1.13.1",
|
||||
"bullmq": "^5.65.1",
|
||||
"cache-manager": "^7.2.4",
|
||||
@@ -60,6 +62,7 @@
|
||||
"firebase-admin": "^13.6.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"socket.io": "^4.8.1",
|
||||
"ulid": "^3.0.1",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
import { AuthenticatedSocket } from '../guards/ws-admin-auth.guard';
|
||||
|
||||
/**
|
||||
* Extract admin ID from authenticated WebSocket client
|
||||
* Use this decorator in WebSocket handlers to get the adminId from the authenticated admin
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @SubscribeMessage('some:event')
|
||||
* handleEvent(@WsAdminId() adminId: string) {
|
||||
* // adminId is automatically extracted from authenticated admin
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const WsAdminId = createParamDecorator((data: unknown, ctx: ExecutionContext): string => {
|
||||
const client = ctx.switchToWs().getClient<AuthenticatedSocket>();
|
||||
const adminId = client.adminId;
|
||||
|
||||
if (!adminId) {
|
||||
throw new Error('Admin ID not found. Ensure WsAdminAuthGuard is applied.');
|
||||
}
|
||||
|
||||
return adminId;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
import { AuthenticatedSocket } from '../guards/ws-admin-auth.guard';
|
||||
|
||||
/**
|
||||
* Extract restaurant ID from authenticated WebSocket client
|
||||
* Use this decorator in WebSocket handlers to get the restId from the authenticated admin
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @SubscribeMessage('join:restaurant')
|
||||
* handleJoinRestaurant(@WsRestId() restId: string) {
|
||||
* // restId is automatically extracted from authenticated admin
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const WsRestId = createParamDecorator((data: unknown, ctx: ExecutionContext): string => {
|
||||
const client = ctx.switchToWs().getClient<AuthenticatedSocket>();
|
||||
const restId = client.restId;
|
||||
|
||||
if (!restId) {
|
||||
throw new Error('Restaurant ID not found. Ensure WsAdminAuthGuard is applied.');
|
||||
}
|
||||
|
||||
return restId;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { CanActivate, ExecutionContext, Injectable, Logger, Inject } from '@nestjs/common';
|
||||
import { WsException } from '@nestjs/websockets';
|
||||
import { Socket } from 'socket.io';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { IAdminTokenPayload } from '../../auth/interfaces/IToken-payload';
|
||||
|
||||
export interface AuthenticatedSocket extends Socket {
|
||||
adminId?: string;
|
||||
restId?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class WsAdminAuthGuard implements CanActivate {
|
||||
private readonly logger = new Logger(WsAdminAuthGuard.name);
|
||||
|
||||
constructor(
|
||||
@Inject(JwtService)
|
||||
private readonly jwtService: JwtService,
|
||||
@Inject(ConfigService)
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const client: Socket = context.switchToWs().getClient<Socket>();
|
||||
|
||||
try {
|
||||
const token = this.extractToken(client);
|
||||
|
||||
if (!token) {
|
||||
this.logger.warn('No token provided in WebSocket connection', {
|
||||
socketId: client.id,
|
||||
auth: client.handshake.auth,
|
||||
query: client.handshake.query,
|
||||
});
|
||||
throw new WsException('Authentication required');
|
||||
}
|
||||
|
||||
const secret = this.configService.getOrThrow<string>('JWT_SECRET');
|
||||
const payload = await this.jwtService.verifyAsync<IAdminTokenPayload>(token, {
|
||||
secret,
|
||||
});
|
||||
|
||||
if (!payload.adminId || !payload.restId) {
|
||||
this.logger.error('Invalid token payload structure', payload);
|
||||
throw new WsException('Invalid token payload');
|
||||
}
|
||||
|
||||
// Attach admin info to socket
|
||||
(client as AuthenticatedSocket).adminId = payload.adminId;
|
||||
(client as AuthenticatedSocket).restId = payload.restId;
|
||||
|
||||
this.logger.log(`Admin authenticated via WebSocket: ${payload.adminId}, restId: ${payload.restId}`);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof WsException) {
|
||||
throw error;
|
||||
}
|
||||
this.logger.error('WebSocket authentication error', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
socketId: client.id,
|
||||
});
|
||||
throw new WsException('Authentication failed');
|
||||
}
|
||||
}
|
||||
|
||||
private extractToken(client: Socket): string | undefined {
|
||||
// Try to get token from handshake auth (recommended for socket.io)
|
||||
const auth = client.handshake.auth;
|
||||
const authToken = (auth?.token as string | undefined) || (auth?.authorization as string | undefined);
|
||||
|
||||
if (authToken) {
|
||||
// If it's "Bearer <token>", extract just the token
|
||||
if (typeof authToken === 'string' && authToken.startsWith('Bearer ')) {
|
||||
return authToken.substring(7);
|
||||
}
|
||||
return authToken;
|
||||
}
|
||||
|
||||
// Try to get from query parameters (fallback)
|
||||
const queryToken = client.handshake.query?.token || client.handshake.query?.authorization;
|
||||
|
||||
if (queryToken) {
|
||||
if (typeof queryToken === 'string' && queryToken.startsWith('Bearer ')) {
|
||||
return queryToken.substring(7);
|
||||
}
|
||||
return queryToken as string;
|
||||
}
|
||||
|
||||
// Try to get from headers (if available)
|
||||
const headers = client.handshake.headers;
|
||||
const authHeader = headers.authorization || headers['authorization'];
|
||||
|
||||
if (authHeader && typeof authHeader === 'string') {
|
||||
const [type, token] = authHeader.split(' ');
|
||||
if (type?.toLowerCase() === 'bearer' && token) {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import {
|
||||
WebSocketGateway,
|
||||
WebSocketServer,
|
||||
SubscribeMessage,
|
||||
OnGatewayConnection,
|
||||
OnGatewayDisconnect,
|
||||
MessageBody,
|
||||
ConnectedSocket,
|
||||
} from '@nestjs/websockets';
|
||||
import { Server, Socket } from 'socket.io';
|
||||
import { Logger, UseGuards } from '@nestjs/common';
|
||||
import { Pager } from './entities/pager.entity';
|
||||
import { WsAdminAuthGuard, type AuthenticatedSocket } from './guards/ws-admin-auth.guard';
|
||||
import { WsRestId } from './decorators/ws-rest-id.decorator';
|
||||
|
||||
@WebSocketGateway({
|
||||
cors: {
|
||||
origin: true,
|
||||
credentials: true,
|
||||
},
|
||||
namespace: '/pager',
|
||||
})
|
||||
export class PagerGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
||||
@WebSocketServer()
|
||||
server!: Server;
|
||||
|
||||
private readonly logger = new Logger(PagerGateway.name);
|
||||
|
||||
handleConnection(client: Socket) {
|
||||
this.logger.log(`Client connected: ${client.id}`);
|
||||
}
|
||||
|
||||
handleDisconnect(client: Socket) {
|
||||
this.logger.log(`Client disconnected: ${client.id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Join a room for a specific restaurant (admin/staff)
|
||||
* Requires admin authentication via JWT token
|
||||
* Restaurant ID is extracted from the authenticated admin's token
|
||||
* Room format: `restaurant:${restId}`
|
||||
*/
|
||||
@UseGuards(WsAdminAuthGuard)
|
||||
@SubscribeMessage('join:restaurant')
|
||||
handleJoinRestaurant(@ConnectedSocket() client: AuthenticatedSocket, @WsRestId() restId: string) {
|
||||
const room = `restaurant:${restId}`;
|
||||
void client.join(room);
|
||||
this.logger.log(`Admin ${client.adminId} (Client ${client.id}) joined room: ${room}`);
|
||||
void client.emit('joined', { room, message: 'Successfully joined restaurant room' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Join a room for a specific cookie/session (public user)
|
||||
* Room format: `cookie:${cookieId}`
|
||||
*/
|
||||
@SubscribeMessage('join:session')
|
||||
handleJoinSession(@ConnectedSocket() client: Socket, @MessageBody() data: { cookieId: string }) {
|
||||
if (!data.cookieId) {
|
||||
void client.emit('error', { message: 'Cookie ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const room = `cookie:${data.cookieId}`;
|
||||
void client.join(room);
|
||||
this.logger.log(`Client ${client.id} joined room: ${room}`);
|
||||
void client.emit('joined', { room, message: 'Successfully joined session room' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Leave a room
|
||||
*/
|
||||
@SubscribeMessage('leave:room')
|
||||
handleLeaveRoom(@ConnectedSocket() client: Socket, @MessageBody() data: { room: string }) {
|
||||
if (data.room) {
|
||||
void client.leave(data.room);
|
||||
this.logger.log(`Client ${client.id} left room: ${data.room}`);
|
||||
void client.emit('left', { room: data.room, message: 'Successfully left room' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit pager created event to restaurant room
|
||||
*/
|
||||
emitPagerCreated(pager: Pager) {
|
||||
const room = `restaurant:${pager.restaurant.id}`;
|
||||
this.server.to(room).emit('pager:created', {
|
||||
pager: {
|
||||
id: pager.id,
|
||||
tableNumber: pager.tableNumber,
|
||||
message: pager.message,
|
||||
status: pager.status,
|
||||
createdAt: pager.createdAt,
|
||||
user: pager.user
|
||||
? {
|
||||
id: pager.user.id,
|
||||
firstName: pager.user.firstName,
|
||||
lastName: pager.user.lastName,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
});
|
||||
this.logger.log(`Emitted pager:created to room: ${room}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit pager status updated event to both restaurant and cookie rooms
|
||||
*/
|
||||
emitPagerStatusUpdated(pager: Pager) {
|
||||
const restaurantRoom = `restaurant:${pager.restaurant.id}`;
|
||||
const cookieRoom = `cookie:${pager.cookieId}`;
|
||||
|
||||
const pagerData = {
|
||||
id: pager.id,
|
||||
tableNumber: pager.tableNumber,
|
||||
message: pager.message,
|
||||
status: pager.status,
|
||||
updatedAt: pager.updatedAt,
|
||||
staff: pager.staff
|
||||
? {
|
||||
id: pager.staff.id,
|
||||
firstName: pager.staff.firstName,
|
||||
lastName: pager.staff.lastName,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
|
||||
// Emit to restaurant room (admin/staff)
|
||||
this.server.to(restaurantRoom).emit('pager:status:updated', {
|
||||
pager: pagerData,
|
||||
});
|
||||
this.logger.log(`Emitted pager:status:updated to room: ${restaurantRoom}`);
|
||||
|
||||
// Emit to cookie room (public user)
|
||||
this.server.to(cookieRoom).emit('pager:status:updated', {
|
||||
pager: pagerData,
|
||||
});
|
||||
this.logger.log(`Emitted pager:status:updated to room: ${cookieRoom}`);
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,12 @@ import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { Pager } from './entities/pager.entity';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { RolesModule } from '../roles/roles.module';
|
||||
import { PagerGateway } from './pager.gateway';
|
||||
|
||||
@Module({
|
||||
imports: [MikroOrmModule.forFeature([Pager]), JwtModule, RolesModule],
|
||||
controllers: [PagerController],
|
||||
providers: [PagerService],
|
||||
providers: [PagerService, PagerGateway],
|
||||
exports: [PagerGateway],
|
||||
})
|
||||
export class PagerModule {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable, NotFoundException, Inject, forwardRef } from '@nestjs/common';
|
||||
import { CreatePagerDto } from './dto/create-pager.dto';
|
||||
import { User } from '../users/entities/user.entity';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
@@ -7,10 +7,15 @@ import { Pager } from './entities/pager.entity';
|
||||
import { PagerStatus } from './interface/pager';
|
||||
import { ulid } from 'ulid';
|
||||
import { Admin } from '../admin/entities/admin.entity';
|
||||
import { PagerGateway } from './pager.gateway';
|
||||
|
||||
@Injectable()
|
||||
export class PagerService {
|
||||
constructor(private readonly em: EntityManager) {}
|
||||
constructor(
|
||||
private readonly em: EntityManager,
|
||||
@Inject(forwardRef(() => PagerGateway))
|
||||
private readonly pagerGateway: PagerGateway,
|
||||
) {}
|
||||
|
||||
async create(createPagerDto: CreatePagerDto, params: { restId?: string; userId?: string; slug: string }) {
|
||||
const { restId, userId, slug } = params;
|
||||
@@ -47,6 +52,13 @@ export class PagerService {
|
||||
});
|
||||
|
||||
await this.em.persistAndFlush(pager);
|
||||
|
||||
// Populate relations before emitting
|
||||
await this.em.populate(pager, ['restaurant', 'user']);
|
||||
|
||||
// Emit socket event for new pager
|
||||
this.pagerGateway.emitPagerCreated(pager);
|
||||
|
||||
return pager;
|
||||
}
|
||||
|
||||
@@ -61,7 +73,11 @@ export class PagerService {
|
||||
}
|
||||
|
||||
async updateStatus(pagerId: string, restId: string, staffId: string, status: PagerStatus) {
|
||||
const pager = await this.em.findOne(Pager, { id: pagerId, restaurant: { id: restId } });
|
||||
const pager = await this.em.findOne(
|
||||
Pager,
|
||||
{ id: pagerId, restaurant: { id: restId } },
|
||||
{ populate: ['restaurant'] },
|
||||
);
|
||||
if (!pager) {
|
||||
throw new NotFoundException('Pager not found *');
|
||||
}
|
||||
@@ -74,6 +90,13 @@ export class PagerService {
|
||||
pager.staff = staff;
|
||||
}
|
||||
await this.em.persistAndFlush(pager);
|
||||
|
||||
// Populate relations before emitting
|
||||
await this.em.populate(pager, ['restaurant', 'staff', 'user']);
|
||||
|
||||
// Emit socket event for status update
|
||||
this.pagerGateway.emitPagerStatusUpdated(pager);
|
||||
|
||||
return pager;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,494 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Pager Socket.IO Test</title>
|
||||
<script src="https://cdn.socket.io/4.8.1/socket.io.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 28px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: #555;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
input, select {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
input:focus, select:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5568d3;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #218838;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.4);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #c82333;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status.connected {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.status.disconnected {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.status.connecting {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
border: 1px solid #ffeaa7;
|
||||
}
|
||||
|
||||
.events {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.event-item {
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
background: #2d2d2d;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid #667eea;
|
||||
}
|
||||
|
||||
.event-time {
|
||||
color: #858585;
|
||||
font-size: 11px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.event-type {
|
||||
color: #4ec9b0;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.event-data {
|
||||
color: #ce9178;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.room-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🔔 Pager Socket.IO Test Client</h1>
|
||||
<p>Test real-time pager notifications</p>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<!-- Connection Section -->
|
||||
<div class="section">
|
||||
<h2>Connection</h2>
|
||||
<div class="form-group">
|
||||
<label for="serverUrl">Server URL:</label>
|
||||
<input type="text" id="serverUrl" value="http://localhost:4000" placeholder="http://localhost:4000">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="authToken">Admin Token (for Restaurant rooms):</label>
|
||||
<input type="text" id="authToken" placeholder="Bearer token (optional, required for restaurant rooms)">
|
||||
<small style="color: #666; display: block; margin-top: 5px;">Enter JWT token for admin authentication. Required when joining restaurant rooms.</small>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button class="btn-primary" onclick="connect()">Connect</button>
|
||||
<button class="btn-danger" onclick="disconnect()">Disconnect</button>
|
||||
</div>
|
||||
<div id="connectionStatus" class="status disconnected">
|
||||
Status: Disconnected
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Room Management Section -->
|
||||
<div class="section">
|
||||
<h2>Room Management</h2>
|
||||
<div class="form-group">
|
||||
<label for="userType">User Type:</label>
|
||||
<select id="userType" onchange="updateRoomInputs()">
|
||||
<option value="restaurant">Restaurant (Admin/Staff)</option>
|
||||
<option value="session">Session (Public User)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="restaurantGroup">
|
||||
<label for="restId">Restaurant ID:</label>
|
||||
<input type="text" id="restId" placeholder="Auto-extracted from token" disabled>
|
||||
<small style="color: #666; display: block; margin-top: 5px;">Restaurant ID is automatically extracted from your admin token. No need to enter manually.</small>
|
||||
</div>
|
||||
<div class="form-group" id="sessionGroup" style="display: none;">
|
||||
<label for="cookieId">Cookie ID:</label>
|
||||
<input type="text" id="cookieId" placeholder="Enter cookie ID (from pager cookie)">
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button class="btn-success" onclick="joinRoom()">Join Room</button>
|
||||
<button class="btn-secondary" onclick="leaveRoom()">Leave Room</button>
|
||||
</div>
|
||||
<div id="roomStatus" style="margin-top: 15px;">
|
||||
<strong>Current Room:</strong> <span id="currentRoom">None</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Events Section -->
|
||||
<div class="section">
|
||||
<h2>Events Log <button class="btn-secondary clear-btn" onclick="clearEvents()">Clear</button></h2>
|
||||
<div id="events" class="events">
|
||||
<div class="event-item">
|
||||
<div class="event-time">Ready to connect...</div>
|
||||
<div class="event-type">INFO</div>
|
||||
<div class="event-data">Enter server URL and click Connect to start</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let socket = null;
|
||||
let currentRoom = null;
|
||||
|
||||
function updateRoomInputs() {
|
||||
const userType = document.getElementById('userType').value;
|
||||
const restaurantGroup = document.getElementById('restaurantGroup');
|
||||
const sessionGroup = document.getElementById('sessionGroup');
|
||||
|
||||
if (userType === 'restaurant') {
|
||||
restaurantGroup.style.display = 'block';
|
||||
sessionGroup.style.display = 'none';
|
||||
} else {
|
||||
restaurantGroup.style.display = 'none';
|
||||
sessionGroup.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
function connect() {
|
||||
const serverUrl = document.getElementById('serverUrl').value || 'http://localhost:4000';
|
||||
const token = document.getElementById('authToken').value.trim();
|
||||
|
||||
if (socket && socket.connected) {
|
||||
addEvent('WARNING', 'Already connected!');
|
||||
return;
|
||||
}
|
||||
|
||||
updateStatus('connecting', 'Connecting...');
|
||||
addEvent('INFO', `Connecting to ${serverUrl}/pager...`);
|
||||
|
||||
const authOptions = {};
|
||||
if (token) {
|
||||
// Remove "Bearer " prefix if present
|
||||
const cleanToken = token.startsWith('Bearer ') ? token.substring(7) : token;
|
||||
authOptions.token = cleanToken;
|
||||
addEvent('INFO', 'Using authentication token');
|
||||
}
|
||||
|
||||
socket = io(`${serverUrl}/pager`, {
|
||||
transports: ['websocket', 'polling'],
|
||||
reconnection: true,
|
||||
reconnectionDelay: 1000,
|
||||
reconnectionAttempts: 5,
|
||||
auth: authOptions
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
updateStatus('connected', 'Connected');
|
||||
addEvent('SUCCESS', `Connected! Socket ID: ${socket.id}`);
|
||||
});
|
||||
|
||||
socket.on('disconnect', (reason) => {
|
||||
updateStatus('disconnected', 'Disconnected');
|
||||
addEvent('WARNING', `Disconnected: ${reason}`);
|
||||
currentRoom = null;
|
||||
document.getElementById('currentRoom').textContent = 'None';
|
||||
});
|
||||
|
||||
socket.on('connect_error', (error) => {
|
||||
updateStatus('disconnected', 'Connection Failed');
|
||||
addEvent('ERROR', `Connection error: ${error.message}`);
|
||||
});
|
||||
|
||||
socket.on('joined', (data) => {
|
||||
addEvent('SUCCESS', `Joined room: ${data.room}`, data);
|
||||
currentRoom = data.room;
|
||||
document.getElementById('currentRoom').textContent = data.room;
|
||||
|
||||
// If it's a restaurant room, extract and display the restId
|
||||
if (data.room && data.room.startsWith('restaurant:')) {
|
||||
const restId = data.room.replace('restaurant:', '');
|
||||
document.getElementById('restId').value = restId;
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('left', (data) => {
|
||||
addEvent('INFO', `Left room: ${data.room}`, data);
|
||||
if (currentRoom === data.room) {
|
||||
currentRoom = null;
|
||||
document.getElementById('currentRoom').textContent = 'None';
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (data) => {
|
||||
addEvent('ERROR', data.message || 'Error occurred', data);
|
||||
});
|
||||
|
||||
// Pager events
|
||||
socket.on('pager:created', (data) => {
|
||||
addEvent('PAGER_CREATED', 'New pager request created!', data);
|
||||
});
|
||||
|
||||
socket.on('pager:status:updated', (data) => {
|
||||
addEvent('PAGER_STATUS_UPDATED', 'Pager status updated!', data);
|
||||
});
|
||||
|
||||
// Handle authentication errors
|
||||
socket.on('exception', (data) => {
|
||||
addEvent('ERROR', `Server error: ${data.message || JSON.stringify(data)}`, data);
|
||||
});
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
if (socket) {
|
||||
socket.disconnect();
|
||||
socket = null;
|
||||
updateStatus('disconnected', 'Disconnected');
|
||||
addEvent('INFO', 'Manually disconnected');
|
||||
currentRoom = null;
|
||||
document.getElementById('currentRoom').textContent = 'None';
|
||||
}
|
||||
}
|
||||
|
||||
function joinRoom() {
|
||||
if (!socket || !socket.connected) {
|
||||
addEvent('ERROR', 'Please connect first!');
|
||||
return;
|
||||
}
|
||||
|
||||
const userType = document.getElementById('userType').value;
|
||||
|
||||
if (userType === 'restaurant') {
|
||||
const token = document.getElementById('authToken').value.trim();
|
||||
if (!token) {
|
||||
addEvent('ERROR', 'Admin token is required for restaurant rooms. Please enter your JWT token in the Connection section.');
|
||||
return;
|
||||
}
|
||||
// Restaurant ID is extracted from token automatically, no need to send it
|
||||
socket.emit('join:restaurant', {});
|
||||
addEvent('INFO', 'Joining restaurant room (restId will be extracted from token)...');
|
||||
} else {
|
||||
const cookieId = document.getElementById('cookieId').value;
|
||||
if (!cookieId) {
|
||||
addEvent('ERROR', 'Please enter Cookie ID');
|
||||
return;
|
||||
}
|
||||
socket.emit('join:session', { cookieId });
|
||||
addEvent('INFO', `Joining session room: cookie:${cookieId}`);
|
||||
}
|
||||
}
|
||||
|
||||
function leaveRoom() {
|
||||
if (!socket || !socket.connected) {
|
||||
addEvent('ERROR', 'Please connect first!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentRoom) {
|
||||
addEvent('WARNING', 'Not in any room');
|
||||
return;
|
||||
}
|
||||
|
||||
socket.emit('leave:room', { room: currentRoom });
|
||||
}
|
||||
|
||||
function updateStatus(status, message) {
|
||||
const statusEl = document.getElementById('connectionStatus');
|
||||
statusEl.className = `status ${status}`;
|
||||
statusEl.textContent = `Status: ${message}`;
|
||||
}
|
||||
|
||||
function addEvent(type, message, data = null) {
|
||||
const eventsDiv = document.getElementById('events');
|
||||
const eventItem = document.createElement('div');
|
||||
eventItem.className = 'event-item';
|
||||
|
||||
const time = new Date().toLocaleTimeString();
|
||||
const typeColors = {
|
||||
'INFO': '#4ec9b0',
|
||||
'SUCCESS': '#4ec9b0',
|
||||
'WARNING': '#dcdcaa',
|
||||
'ERROR': '#f48771',
|
||||
'PAGER_CREATED': '#4fc1ff',
|
||||
'PAGER_STATUS_UPDATED': '#4fc1ff'
|
||||
};
|
||||
|
||||
eventItem.innerHTML = `
|
||||
<div class="event-time">${time}</div>
|
||||
<div class="event-type" style="color: ${typeColors[type] || '#4ec9b0'}">[${type}]</div>
|
||||
<div class="event-data">${message}</div>
|
||||
${data ? `<div class="event-data" style="margin-top: 8px; color: #9cdcfe;">${JSON.stringify(data, null, 2)}</div>` : ''}
|
||||
`;
|
||||
|
||||
eventsDiv.insertBefore(eventItem, eventsDiv.firstChild);
|
||||
}
|
||||
|
||||
function clearEvents() {
|
||||
document.getElementById('events').innerHTML = '';
|
||||
addEvent('INFO', 'Events log cleared');
|
||||
}
|
||||
|
||||
// Initialize
|
||||
updateRoomInputs();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user