Phase 3.4: backend self-managed auth cutover
All backend auth now goes through our own token service — Firebase Auth
dependency is fully removed from auth paths. FCM (firebase-admin messaging)
is still used for push.
Schema:
- auth_sessions (multi-device refresh tokens, bcrypt-hashed)
- otp_requests (Fazpass reference + rate-limit history)
- customers.email + google_sub + apple_sub (social identity)
- control_center_users.password_hash + failed_login_count + lockout_until
- firebase_uid columns made nullable (drop in later cleanup migration)
- 6 new app_config keys for OTP + CC lockout tuning
Services:
- password.service.js — bcrypt cost 12 + complexity (min 8, digit + upper +
lower)
- token.service.js — JWT HS256 access (1h) + opaque refresh (30d, bcrypt-
hashed, rotated on use); session_id claim pre-wires future Valkey-based
instant revocation; revokeSession + revokeAllSessionsForUser helpers
- social-identity.service.js — Google via google-auth-library, Apple via
jwks-rsa + jsonwebtoken
- otp.service.js — Fazpass stub (generates locally, logs the code) clearly
marked for replacement once real API docs arrive; rate-limit + resend
cooldown + verify-attempts all configurable via app_config
- auth.service.js — orchestrator: signInAnonymous, completeCustomer/Mitra-
PhoneSignIn, signInWithGoogle, signInWithApple, signInCcUser, refresh,
logout; reject-on-existing for identity conflicts
- cc-user.service.js — email+password helpers + lockout counters
Routes & middleware:
- authenticate middleware now verifies our JWT and attaches
request.auth = { userType, userId, sessionId }
- WebSocket handshake verifies our JWT (no more Firebase lookup)
- All existing routes updated to use request.auth.userId instead of
request.firebaseUser.uid
- New public routes:
/api/shared/auth/anonymous /refresh /logout
/api/client/auth/otp/request /otp/verify /google /apple /me /profile
/api/mitra/auth/otp/request /otp/verify /me
- New internal routes:
/internal/auth/login /refresh /logout /me (httpOnly cookie refresh)
/internal/control-center-users (accepts plain password, bcrypt-hashed)
/internal/control-center-users/me/password (self-service change)
/internal/control-center-users/:id/password (admin forced reset)
- Deleted legacy customer.routes.js (anonymous + link handled by auth now)
- app.internal.js: @fastify/cookie + CORS credentials for CC httpOnly cookie
Config:
- AUTH_JWT_SECRET + ACCESS_TOKEN_TTL_SECONDS + REFRESH_TOKEN_TTL_DAYS env
- FAZPASS_* env vars (TBD until real API docs)
- GOOGLE_OAUTH_CLIENT_IDS, APPLE_SERVICES_ID/TEAM_ID/KEY_ID/PRIVATE_KEY
- ADMIN_EMAIL + ADMIN_PASSWORD for seed
- CC_ORIGIN for internal-app CORS origin allowlist
Dependencies:
- Added: bcrypt, jsonwebtoken, jwks-rsa, google-auth-library, @fastify/cookie
- Kept: firebase-admin (messaging only)
Still outstanding: Fazpass API integration (stub in place), Apple Developer
prereqs for end-to-end iOS testing, client_app/mitra_app/control_center auth
flow rewrites.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,34 @@ DATABASE_URL=postgresql://user:password@localhost:5432/halobestie
|
|||||||
# Valkey / Redis
|
# Valkey / Redis
|
||||||
VALKEY_URL=redis://localhost:6379
|
VALKEY_URL=redis://localhost:6379
|
||||||
|
|
||||||
# Firebase
|
# Control center origin (for CORS + refresh-cookie). Comma-separated list allowed.
|
||||||
FIREBASE_PROJECT_ID=your-firebase-project-id
|
CC_ORIGIN=http://localhost:5173
|
||||||
FIREBASE_CLIENT_EMAIL=your-service-account@project.iam.gserviceaccount.com
|
|
||||||
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
|
# --- Auth (Phase 3.4) ---
|
||||||
|
|
||||||
|
# JWT access token signing key (HS256). Must be >= 32 chars.
|
||||||
|
AUTH_JWT_SECRET=replace-with-strong-random-32+char-secret
|
||||||
|
ACCESS_TOKEN_TTL_SECONDS=3600
|
||||||
|
REFRESH_TOKEN_TTL_DAYS=30
|
||||||
|
|
||||||
|
# Fazpass (OTP provider — TBD real values once docs are available)
|
||||||
|
FAZPASS_API_KEY=
|
||||||
|
FAZPASS_BASE_URL=
|
||||||
|
FAZPASS_WEBHOOK_SECRET=
|
||||||
|
|
||||||
|
# Google OAuth — comma-separated list of valid audience client IDs (Android, iOS).
|
||||||
|
GOOGLE_OAUTH_CLIENT_IDS=
|
||||||
|
|
||||||
|
# Apple Sign In
|
||||||
|
APPLE_SERVICES_ID=
|
||||||
|
APPLE_TEAM_ID=
|
||||||
|
APPLE_KEY_ID=
|
||||||
|
APPLE_PRIVATE_KEY=
|
||||||
|
|
||||||
|
# First super-admin (used by seed script)
|
||||||
|
ADMIN_EMAIL=admin@halobestie.com
|
||||||
|
ADMIN_PASSWORD=ChangeMe123!
|
||||||
|
|
||||||
|
# --- FCM (kept — only Messaging is used; Auth is self-managed) ---
|
||||||
|
# Path to Firebase service-account JSON (falls back to backend/firebase-service-account.json)
|
||||||
|
FIREBASE_SERVICE_ACCOUNT_PATH=
|
||||||
|
|||||||
479
backend/package-lock.json
generated
479
backend/package-lock.json
generated
@@ -8,13 +8,18 @@
|
|||||||
"name": "halo-bestie-backend",
|
"name": "halo-bestie-backend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fastify/cookie": "^11.0.2",
|
||||||
"@fastify/cors": "^11.0.0",
|
"@fastify/cors": "^11.0.0",
|
||||||
"@fastify/sensible": "^6.0.0",
|
"@fastify/sensible": "^6.0.0",
|
||||||
"@fastify/websocket": "^11.0.0",
|
"@fastify/websocket": "^11.0.0",
|
||||||
|
"bcrypt": "^5.1.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"fastify": "^5.0.0",
|
"fastify": "^5.0.0",
|
||||||
"firebase-admin": "^12.2.0",
|
"firebase-admin": "^12.2.0",
|
||||||
|
"google-auth-library": "^9.15.1",
|
||||||
"ioredis": "^5.4.1",
|
"ioredis": "^5.4.1",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
|
"jwks-rsa": "^3.2.2",
|
||||||
"pg": "^8.12.0",
|
"pg": "^8.12.0",
|
||||||
"postgres": "^3.4.4",
|
"postgres": "^3.4.4",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
@@ -50,6 +55,26 @@
|
|||||||
"integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==",
|
"integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@fastify/cookie": {
|
||||||
|
"version": "11.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-11.0.2.tgz",
|
||||||
|
"integrity": "sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fastify"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/fastify"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cookie": "^1.0.0",
|
||||||
|
"fastify-plugin": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@fastify/cors": {
|
"node_modules/@fastify/cors": {
|
||||||
"version": "11.2.0",
|
"version": "11.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-11.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-11.2.0.tgz",
|
||||||
@@ -457,6 +482,51 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@mapbox/node-pre-gyp": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"detect-libc": "^2.0.0",
|
||||||
|
"https-proxy-agent": "^5.0.0",
|
||||||
|
"make-dir": "^3.1.0",
|
||||||
|
"node-fetch": "^2.6.7",
|
||||||
|
"nopt": "^5.0.0",
|
||||||
|
"npmlog": "^5.0.1",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"semver": "^7.3.5",
|
||||||
|
"tar": "^6.1.11"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"node-pre-gyp": "bin/node-pre-gyp"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@mapbox/node-pre-gyp/node_modules/agent-base": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "6",
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@opentelemetry/api": {
|
"node_modules/@opentelemetry/api": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz",
|
||||||
@@ -628,6 +698,12 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/abbrev": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/abort-controller": {
|
"node_modules/abort-controller": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||||
@@ -652,7 +728,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||||
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
@@ -695,7 +770,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@@ -716,6 +790,26 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/aproba": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/are-we-there-yet": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
|
||||||
|
"deprecated": "This package is no longer supported.",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"delegates": "^1.0.0",
|
||||||
|
"readable-stream": "^3.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/arrify": {
|
"node_modules/arrify": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
|
||||||
@@ -772,6 +866,12 @@
|
|||||||
"fastq": "^1.17.1"
|
"fastq": "^1.17.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/balanced-match": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/base64-js": {
|
"node_modules/base64-js": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
@@ -790,19 +890,41 @@
|
|||||||
"url": "https://feross.org/support"
|
"url": "https://feross.org/support"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/bcrypt": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==",
|
||||||
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"dependencies": {
|
||||||
|
"@mapbox/node-pre-gyp": "^1.0.11",
|
||||||
|
"node-addon-api": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bignumber.js": {
|
"node_modules/bignumber.js": {
|
||||||
"version": "9.3.1",
|
"version": "9.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
|
||||||
"integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
|
"integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/brace-expansion": {
|
||||||
|
"version": "1.1.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||||
|
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0",
|
||||||
|
"concat-map": "0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/buffer-equal-constant-time": {
|
"node_modules/buffer-equal-constant-time": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||||
@@ -823,6 +945,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chownr": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cliui": {
|
"node_modules/cliui": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||||
@@ -867,6 +998,15 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/color-support": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"color-support": "bin.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/combined-stream": {
|
"node_modules/combined-stream": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
@@ -880,6 +1020,18 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/concat-map": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/console-control-strings": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/content-type": {
|
"node_modules/content-type": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||||
@@ -929,6 +1081,12 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/delegates": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/denque": {
|
"node_modules/denque": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||||
@@ -956,6 +1114,15 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/detect-libc": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.6.1",
|
"version": "16.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||||
@@ -1008,8 +1175,7 @@
|
|||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/end-of-stream": {
|
"node_modules/end-of-stream": {
|
||||||
"version": "1.4.5",
|
"version": "1.4.5",
|
||||||
@@ -1093,8 +1259,7 @@
|
|||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/farmhash-modern": {
|
"node_modules/farmhash-modern": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
@@ -1353,6 +1518,36 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fs-minipass": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"minipass": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fs-minipass/node_modules/minipass": {
|
||||||
|
"version": "3.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||||
|
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"yallist": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fs.realpath": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/function-bind": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
@@ -1370,12 +1565,32 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/gauge": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
|
||||||
|
"deprecated": "This package is no longer supported.",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"aproba": "^1.0.3 || ^2.0.0",
|
||||||
|
"color-support": "^1.1.2",
|
||||||
|
"console-control-strings": "^1.0.0",
|
||||||
|
"has-unicode": "^2.0.1",
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"signal-exit": "^3.0.0",
|
||||||
|
"string-width": "^4.2.3",
|
||||||
|
"strip-ansi": "^6.0.1",
|
||||||
|
"wide-align": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/gaxios": {
|
"node_modules/gaxios": {
|
||||||
"version": "6.7.1",
|
"version": "6.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
|
||||||
"integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
|
"integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"extend": "^3.0.2",
|
"extend": "^3.0.2",
|
||||||
"https-proxy-agent": "^7.0.1",
|
"https-proxy-agent": "^7.0.1",
|
||||||
@@ -1396,7 +1611,6 @@
|
|||||||
"https://github.com/sponsors/ctavan"
|
"https://github.com/sponsors/ctavan"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"uuid": "dist/bin/uuid"
|
"uuid": "dist/bin/uuid"
|
||||||
}
|
}
|
||||||
@@ -1406,7 +1620,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
|
||||||
"integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
|
"integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"gaxios": "^6.1.1",
|
"gaxios": "^6.1.1",
|
||||||
"google-logging-utils": "^0.0.2",
|
"google-logging-utils": "^0.0.2",
|
||||||
@@ -1465,12 +1678,32 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/glob": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
|
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.1.1",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/google-auth-library": {
|
"node_modules/google-auth-library": {
|
||||||
"version": "9.15.1",
|
"version": "9.15.1",
|
||||||
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
|
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
|
||||||
"integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
|
"integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"base64-js": "^1.3.0",
|
"base64-js": "^1.3.0",
|
||||||
"ecdsa-sig-formatter": "^1.0.11",
|
"ecdsa-sig-formatter": "^1.0.11",
|
||||||
@@ -1526,7 +1759,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
|
||||||
"integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==",
|
"integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
@@ -1549,7 +1781,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
|
||||||
"integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
|
"integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"gaxios": "^6.0.0",
|
"gaxios": "^6.0.0",
|
||||||
"jws": "^4.0.0"
|
"jws": "^4.0.0"
|
||||||
@@ -1587,6 +1818,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/has-unicode": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/hasown": {
|
"node_modules/hasown": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
@@ -1676,7 +1913,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"agent-base": "^7.1.2",
|
"agent-base": "^7.1.2",
|
||||||
"debug": "4"
|
"debug": "4"
|
||||||
@@ -1685,6 +1921,17 @@
|
|||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/inflight": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||||
|
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/inherits": {
|
"node_modules/inherits": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
@@ -1729,7 +1976,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@@ -1739,7 +1985,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
},
|
},
|
||||||
@@ -1761,7 +2006,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
|
||||||
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
|
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bignumber.js": "^9.0.0"
|
"bignumber.js": "^9.0.0"
|
||||||
}
|
}
|
||||||
@@ -1988,6 +2232,30 @@
|
|||||||
"lru-cache": "6.0.0"
|
"lru-cache": "6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/make-dir": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"semver": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/make-dir/node_modules/semver": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@@ -2043,18 +2311,81 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/minimatch": {
|
||||||
|
"version": "3.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||||
|
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^1.1.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/minipass": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/minizlib": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"minipass": "^3.0.0",
|
||||||
|
"yallist": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/minizlib/node_modules/minipass": {
|
||||||
|
"version": "3.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||||
|
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"yallist": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mkdirp": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"mkdirp": "bin/cmd.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/node-addon-api": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/node-fetch": {
|
"node_modules/node-fetch": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"whatwg-url": "^5.0.0"
|
"whatwg-url": "^5.0.0"
|
||||||
},
|
},
|
||||||
@@ -2079,6 +2410,43 @@
|
|||||||
"node": ">= 6.13.0"
|
"node": ">= 6.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nopt": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"abbrev": "1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"nopt": "bin/nopt.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/npmlog": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
|
||||||
|
"deprecated": "This package is no longer supported.",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"are-we-there-yet": "^2.0.0",
|
||||||
|
"console-control-strings": "^1.1.0",
|
||||||
|
"gauge": "^3.0.0",
|
||||||
|
"set-blocking": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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-hash": {
|
"node_modules/object-hash": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
||||||
@@ -2139,6 +2507,15 @@
|
|||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/path-is-absolute": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pg": {
|
"node_modules/pg": {
|
||||||
"version": "8.20.0",
|
"version": "8.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
|
||||||
@@ -2491,6 +2868,22 @@
|
|||||||
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/rimraf": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
||||||
|
"deprecated": "Rimraf versions prior to v4 are no longer supported",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"glob": "^7.1.3"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"rimraf": "bin.js"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/safe-buffer": {
|
"node_modules/safe-buffer": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
@@ -2570,6 +2963,12 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-blocking": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/set-cookie-parser": {
|
"node_modules/set-cookie-parser": {
|
||||||
"version": "2.7.2",
|
"version": "2.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||||
@@ -2582,6 +2981,12 @@
|
|||||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/signal-exit": {
|
||||||
|
"version": "3.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||||
|
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/sonic-boom": {
|
"node_modules/sonic-boom": {
|
||||||
"version": "4.2.1",
|
"version": "4.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",
|
||||||
@@ -2645,7 +3050,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"emoji-regex": "^8.0.0",
|
"emoji-regex": "^8.0.0",
|
||||||
"is-fullwidth-code-point": "^3.0.0",
|
"is-fullwidth-code-point": "^3.0.0",
|
||||||
@@ -2660,7 +3064,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-regex": "^5.0.1"
|
"ansi-regex": "^5.0.1"
|
||||||
},
|
},
|
||||||
@@ -2688,6 +3091,24 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/tar": {
|
||||||
|
"version": "6.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
||||||
|
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
|
||||||
|
"deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"chownr": "^2.0.0",
|
||||||
|
"fs-minipass": "^2.0.0",
|
||||||
|
"minipass": "^5.0.0",
|
||||||
|
"minizlib": "^2.1.1",
|
||||||
|
"mkdirp": "^1.0.3",
|
||||||
|
"yallist": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/teeny-request": {
|
"node_modules/teeny-request": {
|
||||||
"version": "9.0.0",
|
"version": "9.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz",
|
||||||
@@ -2777,8 +3198,7 @@
|
|||||||
"version": "0.0.3",
|
"version": "0.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
@@ -2863,8 +3283,7 @@
|
|||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause"
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/websocket-driver": {
|
"node_modules/websocket-driver": {
|
||||||
"version": "0.7.4",
|
"version": "0.7.4",
|
||||||
@@ -2894,12 +3313,20 @@
|
|||||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tr46": "~0.0.3",
|
"tr46": "~0.0.3",
|
||||||
"webidl-conversions": "^3.0.0"
|
"webidl-conversions": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wide-align": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^1.0.2 || 2 || 3 || 4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/wrap-ansi": {
|
"node_modules/wrap-ansi": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
|
|||||||
@@ -11,13 +11,18 @@
|
|||||||
"db:seed": "node src/db/seed.js"
|
"db:seed": "node src/db/seed.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fastify/cookie": "^11.0.2",
|
||||||
"@fastify/cors": "^11.0.0",
|
"@fastify/cors": "^11.0.0",
|
||||||
"@fastify/sensible": "^6.0.0",
|
"@fastify/sensible": "^6.0.0",
|
||||||
"@fastify/websocket": "^11.0.0",
|
"@fastify/websocket": "^11.0.0",
|
||||||
|
"bcrypt": "^5.1.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"fastify": "^5.0.0",
|
"fastify": "^5.0.0",
|
||||||
"firebase-admin": "^12.2.0",
|
"firebase-admin": "^12.2.0",
|
||||||
|
"google-auth-library": "^9.15.1",
|
||||||
"ioredis": "^5.4.1",
|
"ioredis": "^5.4.1",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
|
"jwks-rsa": "^3.2.2",
|
||||||
"pg": "^8.12.0",
|
"pg": "^8.12.0",
|
||||||
"postgres": "^3.4.4",
|
"postgres": "^3.4.4",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Fastify from 'fastify'
|
import Fastify from 'fastify'
|
||||||
import cors from '@fastify/cors'
|
import cors from '@fastify/cors'
|
||||||
|
import cookie from '@fastify/cookie'
|
||||||
import sensible from '@fastify/sensible'
|
import sensible from '@fastify/sensible'
|
||||||
import { mitraManagementRoutes } from './routes/internal/mitra.routes.js'
|
import { mitraManagementRoutes } from './routes/internal/mitra.routes.js'
|
||||||
import { ccUserRoutes } from './routes/internal/cc-user.routes.js'
|
import { ccUserRoutes } from './routes/internal/cc-user.routes.js'
|
||||||
@@ -13,7 +14,13 @@ import { errorHandler } from './plugins/error-handler.js'
|
|||||||
export const buildInternalApp = async () => {
|
export const buildInternalApp = async () => {
|
||||||
const app = Fastify({ logger: true })
|
const app = Fastify({ logger: true })
|
||||||
|
|
||||||
await app.register(cors, { origin: true })
|
// CORS: control center origin must be allowed with credentials for httpOnly refresh cookie
|
||||||
|
const ccOrigin = process.env.CC_ORIGIN
|
||||||
|
await app.register(cors, {
|
||||||
|
origin: ccOrigin ? ccOrigin.split(',').map((s) => s.trim()) : true,
|
||||||
|
credentials: true,
|
||||||
|
})
|
||||||
|
await app.register(cookie)
|
||||||
await app.register(sensible)
|
await app.register(sensible)
|
||||||
app.setErrorHandler(errorHandler)
|
app.setErrorHandler(errorHandler)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Fastify from 'fastify'
|
import Fastify from 'fastify'
|
||||||
import cors from '@fastify/cors'
|
import cors from '@fastify/cors'
|
||||||
import sensible from '@fastify/sensible'
|
import sensible from '@fastify/sensible'
|
||||||
import { customerRoutes } from './routes/public/customer.routes.js'
|
import { sharedAuthRoutes } from './routes/public/shared.auth.routes.js'
|
||||||
import { clientAuthRoutes } from './routes/public/client.auth.routes.js'
|
import { clientAuthRoutes } from './routes/public/client.auth.routes.js'
|
||||||
import { mitraAuthRoutes } from './routes/public/mitra.auth.routes.js'
|
import { mitraAuthRoutes } from './routes/public/mitra.auth.routes.js'
|
||||||
import { sharedConfigRoutes } from './routes/public/shared.config.routes.js'
|
import { sharedConfigRoutes } from './routes/public/shared.config.routes.js'
|
||||||
@@ -20,7 +20,7 @@ export const buildPublicApp = async () => {
|
|||||||
await registerWebSocketPlugin(app)
|
await registerWebSocketPlugin(app)
|
||||||
app.setErrorHandler(errorHandler)
|
app.setErrorHandler(errorHandler)
|
||||||
|
|
||||||
app.register(customerRoutes, { prefix: '/api/shared/customer' })
|
app.register(sharedAuthRoutes, { prefix: '/api/shared/auth' })
|
||||||
app.register(sharedConfigRoutes, { prefix: '/api/shared/config' })
|
app.register(sharedConfigRoutes, { prefix: '/api/shared/config' })
|
||||||
app.register(sharedChatRoutes, { prefix: '/api/shared' })
|
app.register(sharedChatRoutes, { prefix: '/api/shared' })
|
||||||
app.register(clientAuthRoutes, { prefix: '/api/client/auth' })
|
app.register(clientAuthRoutes, { prefix: '/api/client/auth' })
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
export const UserType = Object.freeze({
|
export const UserType = Object.freeze({
|
||||||
CUSTOMER: 'customer',
|
CUSTOMER: 'customer',
|
||||||
MITRA: 'mitra',
|
MITRA: 'mitra',
|
||||||
|
CC_USER: 'cc_user',
|
||||||
})
|
})
|
||||||
|
|
||||||
// Chat session statuses
|
// Chat session statuses
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import 'dotenv/config'
|
import 'dotenv/config'
|
||||||
import admin from 'firebase-admin'
|
|
||||||
import { getDb } from './client.js'
|
import { getDb } from './client.js'
|
||||||
import { initFirebase } from '../plugins/firebase.js'
|
import { hashPassword } from '../services/password.service.js'
|
||||||
|
|
||||||
const sql = getDb()
|
const sql = getDb()
|
||||||
|
|
||||||
const seed = async () => {
|
const seed = async () => {
|
||||||
initFirebase()
|
|
||||||
|
|
||||||
// Create super_admin role
|
// Create super_admin role
|
||||||
const [role] = await sql`
|
const [role] = await sql`
|
||||||
INSERT INTO roles (name, permissions)
|
INSERT INTO roles (name, permissions)
|
||||||
@@ -24,21 +21,14 @@ const seed = async () => {
|
|||||||
RETURNING id
|
RETURNING id
|
||||||
`
|
`
|
||||||
|
|
||||||
// Create first super admin user in Firebase
|
const email = process.env.ADMIN_EMAIL || process.env.SEED_ADMIN_EMAIL || 'admin@halobestie.com'
|
||||||
const email = process.env.SEED_ADMIN_EMAIL || 'admin@halobestie.com'
|
const password = process.env.ADMIN_PASSWORD || process.env.SEED_ADMIN_PASSWORD || 'ChangeMe123!'
|
||||||
const password = process.env.SEED_ADMIN_PASSWORD || 'ChangeMe123!'
|
const passwordHash = await hashPassword(password)
|
||||||
|
|
||||||
let firebaseUser
|
|
||||||
try {
|
|
||||||
firebaseUser = await admin.auth().getUserByEmail(email)
|
|
||||||
} catch {
|
|
||||||
firebaseUser = await admin.auth().createUser({ email, password, displayName: 'Super Admin' })
|
|
||||||
}
|
|
||||||
|
|
||||||
await sql`
|
await sql`
|
||||||
INSERT INTO control_center_users (firebase_uid, email, display_name, role_id)
|
INSERT INTO control_center_users (email, display_name, role_id, password_hash)
|
||||||
VALUES (${firebaseUser.uid}, ${email}, 'Super Admin', ${role.id})
|
VALUES (${email}, 'Super Admin', ${role.id}, ${passwordHash})
|
||||||
ON CONFLICT (email) DO NOTHING
|
ON CONFLICT (email) DO UPDATE SET password_hash = EXCLUDED.password_hash
|
||||||
`
|
`
|
||||||
|
|
||||||
console.log(`Seed complete. Admin: ${email}`)
|
console.log(`Seed complete. Admin: ${email}`)
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import { verifyFirebaseToken } from './firebase.js'
|
import { verifyAccessToken } from '../services/token.service.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fastify preHandler — verifies Firebase JWT and attaches decoded token to request.
|
* Fastify preHandler — verifies our JWT access token and attaches claims to request.auth.
|
||||||
* Usage: add as preHandler on any route that requires authentication.
|
*
|
||||||
|
* On success: request.auth = { userType, userId, sessionId }
|
||||||
|
* On failure: returns 401 UNAUTHORIZED and short-circuits the handler.
|
||||||
|
*
|
||||||
|
* Future hook: if Valkey-based instant revocation is enabled, add a
|
||||||
|
* SISMEMBER revoked_sessions <session_id> check here before accepting.
|
||||||
*/
|
*/
|
||||||
export const authenticate = async (request, reply) => {
|
export const authenticate = async (request, reply) => {
|
||||||
const authHeader = request.headers.authorization
|
const authHeader = request.headers.authorization
|
||||||
@@ -15,18 +20,25 @@ export const authenticate = async (request, reply) => {
|
|||||||
|
|
||||||
const token = authHeader.slice(7)
|
const token = authHeader.slice(7)
|
||||||
try {
|
try {
|
||||||
request.firebaseUser = await verifyFirebaseToken(token)
|
const claims = verifyAccessToken(token)
|
||||||
} catch (err) {
|
if (!claims.userId || !claims.userType || !claims.sessionId) {
|
||||||
console.error('Auth failed:', err.code || err.message, '| token preview:', token.substring(0, 20) + '...')
|
|
||||||
return reply.code(401).send({
|
return reply.code(401).send({
|
||||||
success: false,
|
success: false,
|
||||||
error: { code: 'UNAUTHORIZED', message: 'Invalid or expired token' },
|
error: { code: 'UNAUTHORIZED', message: 'Invalid token claims' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
request.auth = claims
|
||||||
|
} catch (err) {
|
||||||
|
return reply.code(err.statusCode || 401).send({
|
||||||
|
success: false,
|
||||||
|
error: { code: err.code || 'UNAUTHORIZED', message: err.message || 'Invalid or expired token' },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a preHandler that checks if the CC user has the required permission.
|
* Returns a preHandler that checks if the authenticated CC user has the required permission.
|
||||||
|
* Requires `attachCcUser` (or equivalent) to have run earlier and set request.ccUser.
|
||||||
* Usage: requirePermission('mitra', 'create')
|
* Usage: requirePermission('mitra', 'create')
|
||||||
*/
|
*/
|
||||||
export const requirePermission = (resource, action) => {
|
export const requirePermission = (resource, action) => {
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
|
|||||||
|
|
||||||
let initialized = false
|
let initialized = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes Firebase Admin SDK. Phase 3.4+ only uses this for FCM (messaging);
|
||||||
|
* authentication is handled by our own token service.
|
||||||
|
*/
|
||||||
export const initFirebase = () => {
|
export const initFirebase = () => {
|
||||||
if (initialized) return
|
if (initialized) return
|
||||||
|
|
||||||
@@ -19,7 +23,3 @@ export const initFirebase = () => {
|
|||||||
})
|
})
|
||||||
initialized = true
|
initialized = true
|
||||||
}
|
}
|
||||||
|
|
||||||
export const verifyFirebaseToken = async (token) => {
|
|
||||||
return admin.auth().verifyIdToken(token)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import websocket from '@fastify/websocket'
|
import websocket from '@fastify/websocket'
|
||||||
import { verifyFirebaseToken } from './firebase.js'
|
import { verifyAccessToken } from '../services/token.service.js'
|
||||||
import { getCustomerByFirebaseUid } from '../services/customer.service.js'
|
|
||||||
import { getMitraByFirebaseUid } from '../services/mitra.service.js'
|
|
||||||
import { subscribe, publish } from './valkey.js'
|
import { subscribe, publish } from './valkey.js'
|
||||||
import { UserType, WsMessage } from '../constants.js'
|
import { UserType, WsMessage } from '../constants.js'
|
||||||
|
|
||||||
@@ -64,18 +62,15 @@ export const registerWebSocketRoute = (app) => {
|
|||||||
// Handle auth message
|
// Handle auth message
|
||||||
if (msg.type === WsMessage.AUTH) {
|
if (msg.type === WsMessage.AUTH) {
|
||||||
try {
|
try {
|
||||||
const decoded = await verifyFirebaseToken(msg.token)
|
const claims = verifyAccessToken(msg.token)
|
||||||
const customer = await getCustomerByFirebaseUid(decoded.uid)
|
if (claims.userType !== UserType.CUSTOMER && claims.userType !== UserType.MITRA) {
|
||||||
const mitra = customer ? null : await getMitraByFirebaseUid(decoded.uid)
|
send({ type: WsMessage.ERROR, message: 'Unsupported user type for websocket' })
|
||||||
|
|
||||||
if (!customer && !mitra) {
|
|
||||||
send({ type: WsMessage.ERROR, message: 'Account not found' })
|
|
||||||
socket.close()
|
socket.close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const userType = customer ? UserType.CUSTOMER : UserType.MITRA
|
const userType = claims.userType
|
||||||
const userId = customer ? customer.id : mitra.id
|
const userId = claims.userId
|
||||||
const sessionId = msg.session_id
|
const sessionId = msg.session_id
|
||||||
|
|
||||||
authenticatedUser = { type: userType, id: userId, sessionId }
|
authenticatedUser = { type: userType, id: userId, sessionId }
|
||||||
|
|||||||
@@ -1,17 +1,127 @@
|
|||||||
import { authenticate } from '../../plugins/auth.js'
|
import { authenticate } from '../../plugins/auth.js'
|
||||||
import { getCcUserByFirebaseUid } from '../../services/cc-user.service.js'
|
import { getCcUserById } from '../../services/cc-user.service.js'
|
||||||
|
import {
|
||||||
|
signInCcUser,
|
||||||
|
refreshTokens,
|
||||||
|
logout,
|
||||||
|
} from '../../services/auth.service.js'
|
||||||
|
import { UserType } from '../../constants.js'
|
||||||
|
|
||||||
|
const REFRESH_COOKIE_NAME = 'cc_refresh_token'
|
||||||
|
|
||||||
|
const extractDeviceInfo = (request) => ({
|
||||||
|
user_agent: request.headers['user-agent'] || null,
|
||||||
|
ip: request.ip || null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const sendAuthError = (reply, err) => reply.code(err.statusCode || 500).send({
|
||||||
|
success: false,
|
||||||
|
error: { code: err.code || 'INTERNAL', message: err.message },
|
||||||
|
})
|
||||||
|
|
||||||
|
const cookieOpts = () => ({
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
sameSite: process.env.NODE_ENV === 'production' ? 'none' : 'lax',
|
||||||
|
path: '/',
|
||||||
|
})
|
||||||
|
|
||||||
|
const setRefreshCookie = (reply, refreshToken, expiresAt) => {
|
||||||
|
reply.setCookie(REFRESH_COOKIE_NAME, refreshToken, {
|
||||||
|
...cookieOpts(),
|
||||||
|
expires: new Date(expiresAt),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearRefreshCookie = (reply) => {
|
||||||
|
reply.clearCookie(REFRESH_COOKIE_NAME, cookieOpts())
|
||||||
|
}
|
||||||
|
|
||||||
export const internalAuthRoutes = async (app) => {
|
export const internalAuthRoutes = async (app) => {
|
||||||
app.post('/verify', { preHandler: authenticate }, async (request, reply) => {
|
app.post('/login', async (request, reply) => {
|
||||||
const user = await getCcUserByFirebaseUid(request.firebaseUser.uid)
|
const { email, password } = request.body || {}
|
||||||
if (!user) {
|
if (!email || !password) {
|
||||||
|
return reply.code(422).send({
|
||||||
|
success: false,
|
||||||
|
error: { code: 'VALIDATION_ERROR', message: 'email and password are required' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { tokens, profile } = await signInCcUser({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
deviceInfo: extractDeviceInfo(request),
|
||||||
|
})
|
||||||
|
setRefreshCookie(reply, tokens.refresh_token, tokens.refresh_token_expires_at)
|
||||||
|
return reply.send({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
access_token: tokens.access_token,
|
||||||
|
access_token_expires_in: tokens.access_token_expires_in,
|
||||||
|
profile,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
return sendAuthError(reply, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/refresh', async (request, reply) => {
|
||||||
|
const refreshToken = request.cookies?.[REFRESH_COOKIE_NAME]
|
||||||
|
if (!refreshToken) {
|
||||||
|
return reply.code(401).send({
|
||||||
|
success: false,
|
||||||
|
error: { code: 'REFRESH_MISSING', message: 'Refresh token missing' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { tokens, profile } = await refreshTokens({
|
||||||
|
refreshToken,
|
||||||
|
deviceInfo: extractDeviceInfo(request),
|
||||||
|
})
|
||||||
|
setRefreshCookie(reply, tokens.refresh_token, tokens.refresh_token_expires_at)
|
||||||
|
return reply.send({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
access_token: tokens.access_token,
|
||||||
|
access_token_expires_in: tokens.access_token_expires_in,
|
||||||
|
profile,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
clearRefreshCookie(reply)
|
||||||
|
return sendAuthError(reply, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/logout', { preHandler: authenticate }, async (request, reply) => {
|
||||||
|
await logout({ sessionId: request.auth.sessionId })
|
||||||
|
clearRefreshCookie(reply)
|
||||||
|
return reply.send({ success: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/me', { preHandler: authenticate }, async (request, reply) => {
|
||||||
|
if (request.auth.userType !== UserType.CC_USER) {
|
||||||
return reply.code(403).send({
|
return reply.code(403).send({
|
||||||
success: false,
|
success: false,
|
||||||
error: { code: 'FORBIDDEN', message: 'Not a control center user' },
|
error: { code: 'FORBIDDEN', message: 'Control center account required' },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Attach to request for downstream permission checks
|
const user = await getCcUserById(request.auth.userId)
|
||||||
request.ccUser = user
|
if (!user) {
|
||||||
return reply.send({ success: true, data: user })
|
return reply.code(404).send({
|
||||||
|
success: false,
|
||||||
|
error: { code: 'NOT_FOUND', message: 'Control center account not found' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return reply.send({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
display_name: user.display_name,
|
||||||
|
role: user.role,
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,54 @@
|
|||||||
import { authenticate, requirePermission } from '../../plugins/auth.js'
|
import { authenticate, requirePermission } from '../../plugins/auth.js'
|
||||||
import { getCcUserByFirebaseUid, createCcUser, listCcUsers } from '../../services/cc-user.service.js'
|
import {
|
||||||
|
getCcUserById,
|
||||||
|
createCcUserWithPassword,
|
||||||
|
listCcUsers,
|
||||||
|
updateCcUserPasswordHash,
|
||||||
|
} from '../../services/cc-user.service.js'
|
||||||
|
import {
|
||||||
|
hashPassword,
|
||||||
|
verifyPassword,
|
||||||
|
validatePasswordComplexity,
|
||||||
|
} from '../../services/password.service.js'
|
||||||
|
import { UserType } from '../../constants.js'
|
||||||
|
|
||||||
const attachCcUser = async (request, reply) => {
|
const attachCcUser = async (request, reply) => {
|
||||||
const user = await getCcUserByFirebaseUid(request.firebaseUser.uid)
|
if (request.auth?.userType !== UserType.CC_USER) {
|
||||||
|
return reply.code(403).send({ success: false, error: { code: 'FORBIDDEN', message: 'Not a control center user' } })
|
||||||
|
}
|
||||||
|
const user = await getCcUserById(request.auth.userId)
|
||||||
if (!user) return reply.code(403).send({ success: false, error: { code: 'FORBIDDEN', message: 'Not a control center user' } })
|
if (!user) return reply.code(403).send({ success: false, error: { code: 'FORBIDDEN', message: 'Not a control center user' } })
|
||||||
request.ccUser = user
|
request.ccUser = user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sendValidation = (reply, err) => reply.code(err.statusCode || 422).send({
|
||||||
|
success: false,
|
||||||
|
error: { code: err.code || 'VALIDATION_ERROR', message: err.message },
|
||||||
|
})
|
||||||
|
|
||||||
export const ccUserRoutes = async (app) => {
|
export const ccUserRoutes = async (app) => {
|
||||||
|
// Create CC user (with initial password)
|
||||||
app.post('/', {
|
app.post('/', {
|
||||||
preHandler: [authenticate, attachCcUser, requirePermission('control_center_users', 'create')],
|
preHandler: [authenticate, attachCcUser, requirePermission('control_center_users', 'create')],
|
||||||
}, async (request, reply) => {
|
}, async (request, reply) => {
|
||||||
const { email, display_name, role_id } = request.body ?? {}
|
const { email, display_name, role_id, password } = request.body ?? {}
|
||||||
if (!email || !display_name || !role_id) {
|
if (!email || !display_name || !role_id || !password) {
|
||||||
return reply.code(422).send({ success: false, error: { code: 'VALIDATION_ERROR', message: 'email, display_name, and role_id are required' } })
|
return reply.code(422).send({
|
||||||
|
success: false,
|
||||||
|
error: { code: 'VALIDATION_ERROR', message: 'email, display_name, role_id, and password are required' },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
// Create Firebase user with temporary password — admin will share credentials verbally
|
validatePasswordComplexity(password)
|
||||||
const { initFirebase } = await import('../../plugins/firebase.js')
|
} catch (err) {
|
||||||
const admin = (await import('firebase-admin')).default
|
return sendValidation(reply, err)
|
||||||
initFirebase()
|
}
|
||||||
|
const passwordHash = await hashPassword(password)
|
||||||
const tempPassword = Math.random().toString(36).slice(-10) + 'A1!'
|
const user = await createCcUserWithPassword({ email, display_name, role_id, password_hash: passwordHash })
|
||||||
const firebaseUser = await admin.auth().createUser({ email, password: tempPassword })
|
|
||||||
|
|
||||||
const user = await createCcUser({ firebase_uid: firebaseUser.uid, email, display_name, role_id })
|
|
||||||
return reply.code(201).send({ success: true, data: user })
|
return reply.code(201).send({ success: true, data: user })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// List CC users
|
||||||
app.get('/', {
|
app.get('/', {
|
||||||
preHandler: [authenticate, attachCcUser, requirePermission('control_center_users', 'read')],
|
preHandler: [authenticate, attachCcUser, requirePermission('control_center_users', 'read')],
|
||||||
}, async (request, reply) => {
|
}, async (request, reply) => {
|
||||||
@@ -35,4 +56,53 @@ export const ccUserRoutes = async (app) => {
|
|||||||
const result = await listCcUsers({ page: Number(page), limit: Number(limit) })
|
const result = await listCcUsers({ page: Number(page), limit: Number(limit) })
|
||||||
return reply.send({ success: true, data: result })
|
return reply.send({ success: true, data: result })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Self-service password change
|
||||||
|
app.patch('/me/password', {
|
||||||
|
preHandler: [authenticate, attachCcUser],
|
||||||
|
}, async (request, reply) => {
|
||||||
|
const { current_password, new_password } = request.body || {}
|
||||||
|
if (!current_password || !new_password) {
|
||||||
|
return reply.code(422).send({
|
||||||
|
success: false,
|
||||||
|
error: { code: 'VALIDATION_ERROR', message: 'current_password and new_password are required' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const ok = await verifyPassword(current_password, request.ccUser.password_hash)
|
||||||
|
if (!ok) {
|
||||||
|
return reply.code(401).send({
|
||||||
|
success: false,
|
||||||
|
error: { code: 'INVALID_CREDENTIALS', message: 'Current password is incorrect' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
validatePasswordComplexity(new_password)
|
||||||
|
} catch (err) {
|
||||||
|
return sendValidation(reply, err)
|
||||||
|
}
|
||||||
|
const hash = await hashPassword(new_password)
|
||||||
|
await updateCcUserPasswordHash(request.ccUser.id, hash)
|
||||||
|
return reply.send({ success: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
// Admin-forced password reset
|
||||||
|
app.patch('/:id/password', {
|
||||||
|
preHandler: [authenticate, attachCcUser, requirePermission('control_center_users', 'update')],
|
||||||
|
}, async (request, reply) => {
|
||||||
|
const { new_password } = request.body || {}
|
||||||
|
if (!new_password) {
|
||||||
|
return reply.code(422).send({
|
||||||
|
success: false,
|
||||||
|
error: { code: 'VALIDATION_ERROR', message: 'new_password is required' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
validatePasswordComplexity(new_password)
|
||||||
|
} catch (err) {
|
||||||
|
return sendValidation(reply, err)
|
||||||
|
}
|
||||||
|
const hash = await hashPassword(new_password)
|
||||||
|
await updateCcUserPasswordHash(request.params.id, hash)
|
||||||
|
return reply.send({ success: true })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { authenticate, requirePermission } from '../../plugins/auth.js'
|
import { authenticate, requirePermission } from '../../plugins/auth.js'
|
||||||
import { getCcUserByFirebaseUid } from '../../services/cc-user.service.js'
|
import { getCcUserById } from '../../services/cc-user.service.js'
|
||||||
|
import { UserType } from '../../constants.js'
|
||||||
import {
|
import {
|
||||||
getAnonymityConfig, setAnonymityConfig,
|
getAnonymityConfig, setAnonymityConfig,
|
||||||
getMaxCustomersPerMitra, setMaxCustomersPerMitra,
|
getMaxCustomersPerMitra, setMaxCustomersPerMitra,
|
||||||
@@ -11,7 +12,10 @@ import {
|
|||||||
} from '../../services/config.service.js'
|
} from '../../services/config.service.js'
|
||||||
|
|
||||||
const attachCcUser = async (request, reply) => {
|
const attachCcUser = async (request, reply) => {
|
||||||
const user = await getCcUserByFirebaseUid(request.firebaseUser.uid)
|
if (request.auth?.userType !== UserType.CC_USER) {
|
||||||
|
return reply.code(403).send({ success: false, error: { code: 'FORBIDDEN', message: 'Not a control center user' } })
|
||||||
|
}
|
||||||
|
const user = await getCcUserById(request.auth.userId)
|
||||||
if (!user) return reply.code(403).send({ success: false, error: { code: 'FORBIDDEN', message: 'Not a control center user' } })
|
if (!user) return reply.code(403).send({ success: false, error: { code: 'FORBIDDEN', message: 'Not a control center user' } })
|
||||||
request.ccUser = user
|
request.ccUser = user
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { authenticate, requirePermission } from '../../plugins/auth.js'
|
import { authenticate, requirePermission } from '../../plugins/auth.js'
|
||||||
import { getCcUserByFirebaseUid } from '../../services/cc-user.service.js'
|
import { getCcUserById } from '../../services/cc-user.service.js'
|
||||||
import { getMitraActivityLog, getMitraActivitySummary } from '../../services/mitra-activity.service.js'
|
import { getMitraActivityLog, getMitraActivitySummary } from '../../services/mitra-activity.service.js'
|
||||||
|
import { UserType } from '../../constants.js'
|
||||||
|
|
||||||
const attachCcUser = async (request, reply) => {
|
const attachCcUser = async (request, reply) => {
|
||||||
const user = await getCcUserByFirebaseUid(request.firebaseUser.uid)
|
if (request.auth?.userType !== UserType.CC_USER) {
|
||||||
if (!user) return reply.code(403).send({
|
return reply.code(403).send({ success: false, error: { code: 'FORBIDDEN', message: 'Not a control center user' } })
|
||||||
success: false,
|
}
|
||||||
error: { code: 'FORBIDDEN', message: 'Not a control center user' },
|
const user = await getCcUserById(request.auth.userId)
|
||||||
})
|
if (!user) return reply.code(403).send({ success: false, error: { code: 'FORBIDDEN', message: 'Not a control center user' } })
|
||||||
request.ccUser = user
|
request.ccUser = user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { authenticate, requirePermission } from '../../plugins/auth.js'
|
import { authenticate, requirePermission } from '../../plugins/auth.js'
|
||||||
import { getCcUserByFirebaseUid } from '../../services/cc-user.service.js'
|
import { getCcUserById } from '../../services/cc-user.service.js'
|
||||||
import { createMitra, listMitras, updateMitraStatus } from '../../services/mitra.service.js'
|
import { createMitra, listMitras, updateMitraStatus } from '../../services/mitra.service.js'
|
||||||
import { getOnlineMitras, getOnlineLogs } from '../../services/mitra-status.service.js'
|
import { getOnlineMitras, getOnlineLogs } from '../../services/mitra-status.service.js'
|
||||||
|
import { UserType } from '../../constants.js'
|
||||||
|
|
||||||
const attachCcUser = async (request, reply) => {
|
const attachCcUser = async (request, reply) => {
|
||||||
const user = await getCcUserByFirebaseUid(request.firebaseUser.uid)
|
if (request.auth?.userType !== UserType.CC_USER) {
|
||||||
|
return reply.code(403).send({ success: false, error: { code: 'FORBIDDEN', message: 'Not a control center user' } })
|
||||||
|
}
|
||||||
|
const user = await getCcUserById(request.auth.userId)
|
||||||
if (!user) return reply.code(403).send({ success: false, error: { code: 'FORBIDDEN', message: 'Not a control center user' } })
|
if (!user) return reply.code(403).send({ success: false, error: { code: 'FORBIDDEN', message: 'Not a control center user' } })
|
||||||
request.ccUser = user
|
request.ccUser = user
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { authenticate, requirePermission } from '../../plugins/auth.js'
|
import { authenticate, requirePermission } from '../../plugins/auth.js'
|
||||||
import { getCcUserByFirebaseUid } from '../../services/cc-user.service.js'
|
import { getCcUserById } from '../../services/cc-user.service.js'
|
||||||
import { listRoles } from '../../services/roles.service.js'
|
import { listRoles } from '../../services/roles.service.js'
|
||||||
|
import { UserType } from '../../constants.js'
|
||||||
|
|
||||||
const attachCcUser = async (request, reply) => {
|
const attachCcUser = async (request, reply) => {
|
||||||
const user = await getCcUserByFirebaseUid(request.firebaseUser.uid)
|
if (request.auth?.userType !== UserType.CC_USER) {
|
||||||
|
return reply.code(403).send({ success: false, error: { code: 'FORBIDDEN', message: 'Not a control center user' } })
|
||||||
|
}
|
||||||
|
const user = await getCcUserById(request.auth.userId)
|
||||||
if (!user) return reply.code(403).send({ success: false, error: { code: 'FORBIDDEN', message: 'Not a control center user' } })
|
if (!user) return reply.code(403).send({ success: false, error: { code: 'FORBIDDEN', message: 'Not a control center user' } })
|
||||||
request.ccUser = user
|
request.ccUser = user
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { authenticate, requirePermission } from '../../plugins/auth.js'
|
import { authenticate, requirePermission } from '../../plugins/auth.js'
|
||||||
import { getCcUserByFirebaseUid } from '../../services/cc-user.service.js'
|
import { getCcUserById } from '../../services/cc-user.service.js'
|
||||||
import { listSessions, getSessionById, rerouteSession } from '../../services/session.service.js'
|
import { listSessions, getSessionById, rerouteSession } from '../../services/session.service.js'
|
||||||
import { getSessionSensitivityLog } from '../../services/sensitivity.service.js'
|
import { getSessionSensitivityLog } from '../../services/sensitivity.service.js'
|
||||||
import { getDashboardStats } from '../../services/dashboard.service.js'
|
import { getDashboardStats } from '../../services/dashboard.service.js'
|
||||||
|
import { UserType } from '../../constants.js'
|
||||||
|
|
||||||
const attachCcUser = async (request, reply) => {
|
const attachCcUser = async (request, reply) => {
|
||||||
const user = await getCcUserByFirebaseUid(request.firebaseUser.uid)
|
if (request.auth?.userType !== UserType.CC_USER) {
|
||||||
|
return reply.code(403).send({ success: false, error: { code: 'FORBIDDEN', message: 'Not a control center user' } })
|
||||||
|
}
|
||||||
|
const user = await getCcUserById(request.auth.userId)
|
||||||
if (!user) return reply.code(403).send({ success: false, error: { code: 'FORBIDDEN', message: 'Not a control center user' } })
|
if (!user) return reply.code(403).send({ success: false, error: { code: 'FORBIDDEN', message: 'Not a control center user' } })
|
||||||
request.ccUser = user
|
request.ccUser = user
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,134 @@
|
|||||||
import { authenticate } from '../../plugins/auth.js'
|
import { authenticate } from '../../plugins/auth.js'
|
||||||
import { getOrCreateCustomer, getCustomerByFirebaseUid, updateCustomerDisplayName } from '../../services/customer.service.js'
|
import { getCustomerById, updateCustomerDisplayName } from '../../services/customer.service.js'
|
||||||
|
import {
|
||||||
|
completeCustomerPhoneSignIn,
|
||||||
|
signInWithGoogle,
|
||||||
|
signInWithApple,
|
||||||
|
} from '../../services/auth.service.js'
|
||||||
|
import { requestOtp, verifyOtp } from '../../services/otp.service.js'
|
||||||
|
import { UserType } from '../../constants.js'
|
||||||
|
|
||||||
|
const extractDeviceInfo = (request) => ({
|
||||||
|
user_agent: request.headers['user-agent'] || null,
|
||||||
|
ip: request.ip || null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const sendAuthError = (reply, err) => reply.code(err.statusCode || 500).send({
|
||||||
|
success: false,
|
||||||
|
error: { code: err.code || 'INTERNAL', message: err.message },
|
||||||
|
})
|
||||||
|
|
||||||
export const clientAuthRoutes = async (app) => {
|
export const clientAuthRoutes = async (app) => {
|
||||||
app.post('/verify', { preHandler: authenticate }, async (request, reply) => {
|
// --- Phone OTP ---
|
||||||
const { uid, phone_number, name } = request.firebaseUser
|
|
||||||
const customer = await getOrCreateCustomer({
|
app.post('/otp/request', async (request, reply) => {
|
||||||
firebase_uid: uid,
|
const { phone, channel } = request.body || {}
|
||||||
phone: phone_number || null,
|
try {
|
||||||
display_name: name || null,
|
const result = await requestOtp({
|
||||||
|
phone,
|
||||||
|
userType: UserType.CUSTOMER,
|
||||||
|
ipAddress: request.ip,
|
||||||
|
channel,
|
||||||
})
|
})
|
||||||
return reply.send({ success: true, data: customer })
|
return reply.send({ success: true, data: result })
|
||||||
|
} catch (err) {
|
||||||
|
return sendAuthError(reply, err)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.patch('/profile', { preHandler: authenticate }, async (request, reply) => {
|
app.post('/otp/verify', async (request, reply) => {
|
||||||
const customer = await getCustomerByFirebaseUid(request.firebaseUser.uid)
|
const { otp_request_id, code, anonymous_customer_id } = request.body || {}
|
||||||
|
try {
|
||||||
|
const { phone, user_type } = await verifyOtp({ otpRequestId: otp_request_id, code })
|
||||||
|
if (user_type !== UserType.CUSTOMER) {
|
||||||
|
return reply.code(400).send({
|
||||||
|
success: false,
|
||||||
|
error: { code: 'WRONG_FLOW', message: 'This OTP was issued for a different user type' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const { tokens, profile } = await completeCustomerPhoneSignIn({
|
||||||
|
phone,
|
||||||
|
anonymousCustomerId: anonymous_customer_id || null,
|
||||||
|
deviceInfo: extractDeviceInfo(request),
|
||||||
|
})
|
||||||
|
return reply.send({ success: true, data: { ...tokens, profile } })
|
||||||
|
} catch (err) {
|
||||||
|
return sendAuthError(reply, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- Google ---
|
||||||
|
|
||||||
|
app.post('/google', async (request, reply) => {
|
||||||
|
const { id_token, anonymous_customer_id } = request.body || {}
|
||||||
|
if (!id_token) {
|
||||||
|
return reply.code(422).send({
|
||||||
|
success: false,
|
||||||
|
error: { code: 'VALIDATION_ERROR', message: 'id_token is required' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { tokens, profile } = await signInWithGoogle({
|
||||||
|
idToken: id_token,
|
||||||
|
anonymousCustomerId: anonymous_customer_id || null,
|
||||||
|
deviceInfo: extractDeviceInfo(request),
|
||||||
|
})
|
||||||
|
return reply.send({ success: true, data: { ...tokens, profile } })
|
||||||
|
} catch (err) {
|
||||||
|
return sendAuthError(reply, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- Apple ---
|
||||||
|
|
||||||
|
app.post('/apple', async (request, reply) => {
|
||||||
|
const { id_token, anonymous_customer_id } = request.body || {}
|
||||||
|
if (!id_token) {
|
||||||
|
return reply.code(422).send({
|
||||||
|
success: false,
|
||||||
|
error: { code: 'VALIDATION_ERROR', message: 'id_token is required' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { tokens, profile } = await signInWithApple({
|
||||||
|
idToken: id_token,
|
||||||
|
anonymousCustomerId: anonymous_customer_id || null,
|
||||||
|
deviceInfo: extractDeviceInfo(request),
|
||||||
|
})
|
||||||
|
return reply.send({ success: true, data: { ...tokens, profile } })
|
||||||
|
} catch (err) {
|
||||||
|
return sendAuthError(reply, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- Current user profile ---
|
||||||
|
|
||||||
|
app.get('/me', { preHandler: authenticate }, async (request, reply) => {
|
||||||
|
if (request.auth.userType !== UserType.CUSTOMER) {
|
||||||
|
return reply.code(403).send({
|
||||||
|
success: false,
|
||||||
|
error: { code: 'FORBIDDEN', message: 'Customer account required' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const customer = await getCustomerById(request.auth.userId)
|
||||||
if (!customer) {
|
if (!customer) {
|
||||||
return reply.code(404).send({
|
return reply.code(404).send({
|
||||||
success: false,
|
success: false,
|
||||||
error: { code: 'NOT_FOUND', message: 'Customer account not found' },
|
error: { code: 'NOT_FOUND', message: 'Customer account not found' },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
return reply.send({ success: true, data: customer })
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- Update display name ---
|
||||||
|
|
||||||
|
app.patch('/profile', { preHandler: authenticate }, async (request, reply) => {
|
||||||
|
if (request.auth.userType !== UserType.CUSTOMER) {
|
||||||
|
return reply.code(403).send({
|
||||||
|
success: false,
|
||||||
|
error: { code: 'FORBIDDEN', message: 'Customer account required' },
|
||||||
|
})
|
||||||
|
}
|
||||||
const { display_name } = request.body || {}
|
const { display_name } = request.body || {}
|
||||||
if (!display_name || typeof display_name !== 'string' || display_name.trim().length === 0) {
|
if (!display_name || typeof display_name !== 'string' || display_name.trim().length === 0) {
|
||||||
return reply.code(422).send({
|
return reply.code(422).send({
|
||||||
@@ -27,7 +136,7 @@ export const clientAuthRoutes = async (app) => {
|
|||||||
error: { code: 'VALIDATION_ERROR', message: 'display_name is required' },
|
error: { code: 'VALIDATION_ERROR', message: 'display_name is required' },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const updated = await updateCustomerDisplayName(customer.id, display_name.trim())
|
const updated = await updateCustomerDisplayName(request.auth.userId, display_name.trim())
|
||||||
return reply.send({ success: true, data: updated })
|
return reply.send({ success: true, data: updated })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
import { authenticate } from '../../plugins/auth.js'
|
import { authenticate } from '../../plugins/auth.js'
|
||||||
import { getCustomerByFirebaseUid } from '../../services/customer.service.js'
|
import { getCustomerById } from '../../services/customer.service.js'
|
||||||
import { createPairingRequest, cancelPairingRequest } from '../../services/pairing.service.js'
|
import { createPairingRequest, cancelPairingRequest } from '../../services/pairing.service.js'
|
||||||
import { getActiveSessionByCustomer, getActiveSessionByCustomerWithUnread, endSession, getCustomerHistory } from '../../services/session.service.js'
|
import { getActiveSessionByCustomer, getActiveSessionByCustomerWithUnread, endSession, getCustomerHistory } from '../../services/session.service.js'
|
||||||
import { getPricingForCustomer, isValidTier, isCustomerEligibleForFreeTrial, getFreeTrial } from '../../services/pricing.service.js'
|
import { getPricingForCustomer, isValidTier, isCustomerEligibleForFreeTrial, getFreeTrial } from '../../services/pricing.service.js'
|
||||||
import { requestExtension } from '../../services/extension.service.js'
|
import { requestExtension } from '../../services/extension.service.js'
|
||||||
import { EndedBy, TopicSensitivity } from '../../constants.js'
|
import { EndedBy, TopicSensitivity, UserType } from '../../constants.js'
|
||||||
|
|
||||||
const resolveCustomer = async (request, reply) => {
|
const resolveCustomer = async (request, reply) => {
|
||||||
const customer = await getCustomerByFirebaseUid(request.firebaseUser.uid)
|
if (request.auth?.userType !== UserType.CUSTOMER) {
|
||||||
|
return reply.code(403).send({
|
||||||
|
success: false,
|
||||||
|
error: { code: 'FORBIDDEN', message: 'Customer account required' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const customer = await getCustomerById(request.auth.userId)
|
||||||
if (!customer) {
|
if (!customer) {
|
||||||
return reply.code(404).send({
|
return reply.code(404).send({
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { authenticate } from '../../plugins/auth.js'
|
|
||||||
import { createAnonymousCustomer, linkCustomerAccount } from '../../services/customer.service.js'
|
|
||||||
|
|
||||||
export const customerRoutes = async (app) => {
|
|
||||||
app.post('/anonymous', { preHandler: authenticate }, async (request, reply) => {
|
|
||||||
const { display_name } = request.body ?? {}
|
|
||||||
if (!display_name?.trim()) {
|
|
||||||
return reply.code(422).send({
|
|
||||||
success: false,
|
|
||||||
error: { code: 'DISPLAY_NAME_REQUIRED', message: 'Display name is required' },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const firebase_uid = request.firebaseUser.uid
|
|
||||||
const customer = await createAnonymousCustomer({ display_name: display_name.trim(), firebase_uid })
|
|
||||||
return reply.code(201).send({ success: true, data: customer })
|
|
||||||
})
|
|
||||||
|
|
||||||
app.post('/link', { preHandler: authenticate }, async (request, reply) => {
|
|
||||||
const { customer_id } = request.body ?? {}
|
|
||||||
const firebase_uid = request.firebaseUser.uid
|
|
||||||
|
|
||||||
if (!customer_id) {
|
|
||||||
return reply.code(422).send({
|
|
||||||
success: false,
|
|
||||||
error: { code: 'VALIDATION_ERROR', message: 'customer_id is required' },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const customer = await linkCustomerAccount({ customer_id, firebase_uid })
|
|
||||||
return reply.send({ success: true, data: customer })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,44 +1,75 @@
|
|||||||
import { authenticate } from '../../plugins/auth.js'
|
import { authenticate } from '../../plugins/auth.js'
|
||||||
import { getMitraByFirebaseUid, getMitraByPhone, setMitraFirebaseUid } from '../../services/mitra.service.js'
|
import { getMitraById } from '../../services/mitra.service.js'
|
||||||
|
import { completeMitraPhoneSignIn } from '../../services/auth.service.js'
|
||||||
|
import { requestOtp, verifyOtp } from '../../services/otp.service.js'
|
||||||
|
import { UserType } from '../../constants.js'
|
||||||
|
|
||||||
|
const extractDeviceInfo = (request) => ({
|
||||||
|
user_agent: request.headers['user-agent'] || null,
|
||||||
|
ip: request.ip || null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const sendAuthError = (reply, err) => reply.code(err.statusCode || 500).send({
|
||||||
|
success: false,
|
||||||
|
error: { code: err.code || 'INTERNAL', message: err.message },
|
||||||
|
})
|
||||||
|
|
||||||
export const mitraAuthRoutes = async (app) => {
|
export const mitraAuthRoutes = async (app) => {
|
||||||
app.post('/verify', { preHandler: authenticate }, async (request, reply) => {
|
app.post('/otp/request', async (request, reply) => {
|
||||||
const { uid, phone_number } = request.firebaseUser
|
const { phone, channel } = request.body || {}
|
||||||
|
try {
|
||||||
// First try lookup by firebase_uid (returning user)
|
const result = await requestOtp({
|
||||||
let mitra = await getMitraByFirebaseUid(uid)
|
phone,
|
||||||
|
userType: UserType.MITRA,
|
||||||
// First-time login: link firebase_uid to mitra record via phone number
|
ipAddress: request.ip,
|
||||||
if (!mitra && phone_number) {
|
channel,
|
||||||
mitra = await getMitraByPhone(phone_number)
|
})
|
||||||
if (mitra) {
|
return reply.send({ success: true, data: result })
|
||||||
await setMitraFirebaseUid(mitra.id, uid)
|
} catch (err) {
|
||||||
}
|
return sendAuthError(reply, err)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if (!mitra) {
|
app.post('/otp/verify', async (request, reply) => {
|
||||||
return reply.code(404).send({
|
const { otp_request_id, code } = request.body || {}
|
||||||
|
try {
|
||||||
|
const { phone, user_type } = await verifyOtp({ otpRequestId: otp_request_id, code })
|
||||||
|
if (user_type !== UserType.MITRA) {
|
||||||
|
return reply.code(400).send({
|
||||||
success: false,
|
success: false,
|
||||||
error: { code: 'ACCOUNT_NOT_FOUND', message: 'Account not found. Contact your administrator.' },
|
error: { code: 'WRONG_FLOW', message: 'This OTP was issued for a different user type' },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
const { tokens, profile } = await completeMitraPhoneSignIn({
|
||||||
if (!mitra.is_active) {
|
phone,
|
||||||
|
deviceInfo: extractDeviceInfo(request),
|
||||||
|
})
|
||||||
|
if (!profile.is_active) {
|
||||||
return reply.code(403).send({
|
return reply.code(403).send({
|
||||||
success: false,
|
success: false,
|
||||||
error: { code: 'ACCOUNT_INACTIVE', message: 'Account is inactive. Contact your administrator.' },
|
error: { code: 'ACCOUNT_INACTIVE', message: 'Account is inactive. Contact your administrator.' },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
return reply.send({ success: true, data: { ...tokens, profile } })
|
||||||
return reply.send({
|
} catch (err) {
|
||||||
success: true,
|
return sendAuthError(reply, err)
|
||||||
data: {
|
}
|
||||||
id: mitra.id,
|
|
||||||
display_name: mitra.display_name,
|
|
||||||
phone: mitra.phone,
|
|
||||||
is_active: mitra.is_active,
|
|
||||||
created_at: mitra.created_at,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.get('/me', { preHandler: authenticate }, async (request, reply) => {
|
||||||
|
if (request.auth.userType !== UserType.MITRA) {
|
||||||
|
return reply.code(403).send({
|
||||||
|
success: false,
|
||||||
|
error: { code: 'FORBIDDEN', message: 'Mitra account required' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const mitra = await getMitraById(request.auth.userId)
|
||||||
|
if (!mitra) {
|
||||||
|
return reply.code(404).send({
|
||||||
|
success: false,
|
||||||
|
error: { code: 'NOT_FOUND', message: 'Mitra account not found' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return reply.send({ success: true, data: mitra })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import { authenticate } from '../../plugins/auth.js'
|
import { authenticate } from '../../plugins/auth.js'
|
||||||
import { getMitraByFirebaseUid } from '../../services/mitra.service.js'
|
import { getMitraById } from '../../services/mitra.service.js'
|
||||||
import { acceptPairingRequest, declinePairingRequest, getSessionStatus, getPendingRequestsForMitra } from '../../services/pairing.service.js'
|
import { acceptPairingRequest, declinePairingRequest, getSessionStatus, getPendingRequestsForMitra } from '../../services/pairing.service.js'
|
||||||
import { getActiveSessionsByMitra, getActiveSessionsByMitraWithUnread, endSession, getMitraHistory } from '../../services/session.service.js'
|
import { getActiveSessionsByMitra, getActiveSessionsByMitraWithUnread, endSession, getMitraHistory } from '../../services/session.service.js'
|
||||||
import { respondToExtension } from '../../services/extension.service.js'
|
import { respondToExtension } from '../../services/extension.service.js'
|
||||||
import { EndedBy } from '../../constants.js'
|
import { EndedBy, UserType } from '../../constants.js'
|
||||||
|
|
||||||
const resolveMitra = async (request, reply) => {
|
const resolveMitra = async (request, reply) => {
|
||||||
const mitra = await getMitraByFirebaseUid(request.firebaseUser.uid)
|
if (request.auth?.userType !== UserType.MITRA) {
|
||||||
|
return reply.code(403).send({
|
||||||
|
success: false,
|
||||||
|
error: { code: 'FORBIDDEN', message: 'Mitra account required' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const mitra = await getMitraById(request.auth.userId)
|
||||||
if (!mitra) {
|
if (!mitra) {
|
||||||
return reply.code(404).send({
|
return reply.code(404).send({
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import { authenticate } from '../../plugins/auth.js'
|
import { authenticate } from '../../plugins/auth.js'
|
||||||
import { getMitraByFirebaseUid } from '../../services/mitra.service.js'
|
import { getMitraById } from '../../services/mitra.service.js'
|
||||||
import * as mitraStatusService from '../../services/mitra-status.service.js'
|
import * as mitraStatusService from '../../services/mitra-status.service.js'
|
||||||
|
import { UserType } from '../../constants.js'
|
||||||
|
|
||||||
export const mitraStatusRoutes = async (app) => {
|
export const mitraStatusRoutes = async (app) => {
|
||||||
// Resolve mitra from Firebase token
|
|
||||||
const resolveMitra = async (request, reply) => {
|
const resolveMitra = async (request, reply) => {
|
||||||
const mitra = await getMitraByFirebaseUid(request.firebaseUser.uid)
|
if (request.auth?.userType !== UserType.MITRA) {
|
||||||
|
return reply.code(403).send({
|
||||||
|
success: false,
|
||||||
|
error: { code: 'FORBIDDEN', message: 'Mitra account required' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const mitra = await getMitraById(request.auth.userId)
|
||||||
if (!mitra) {
|
if (!mitra) {
|
||||||
return reply.code(404).send({
|
return reply.code(404).send({
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
54
backend/src/routes/public/shared.auth.routes.js
Normal file
54
backend/src/routes/public/shared.auth.routes.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { authenticate } from '../../plugins/auth.js'
|
||||||
|
import {
|
||||||
|
signInAnonymous,
|
||||||
|
refreshTokens,
|
||||||
|
logout,
|
||||||
|
} from '../../services/auth.service.js'
|
||||||
|
|
||||||
|
const extractDeviceInfo = (request) => ({
|
||||||
|
user_agent: request.headers['user-agent'] || null,
|
||||||
|
ip: request.ip || null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const sendAuthError = (reply, err) => reply.code(err.statusCode || 500).send({
|
||||||
|
success: false,
|
||||||
|
error: { code: err.code || 'INTERNAL', message: err.message },
|
||||||
|
})
|
||||||
|
|
||||||
|
export const sharedAuthRoutes = async (app) => {
|
||||||
|
// Issue an anonymous customer session
|
||||||
|
app.post('/anonymous', async (request, reply) => {
|
||||||
|
try {
|
||||||
|
const { tokens, profile } = await signInAnonymous({ deviceInfo: extractDeviceInfo(request) })
|
||||||
|
return reply.code(201).send({ success: true, data: { ...tokens, profile } })
|
||||||
|
} catch (err) {
|
||||||
|
return sendAuthError(reply, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Rotate refresh token
|
||||||
|
app.post('/refresh', async (request, reply) => {
|
||||||
|
const { refresh_token } = request.body || {}
|
||||||
|
if (!refresh_token) {
|
||||||
|
return reply.code(422).send({
|
||||||
|
success: false,
|
||||||
|
error: { code: 'VALIDATION_ERROR', message: 'refresh_token is required' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { tokens, profile } = await refreshTokens({
|
||||||
|
refreshToken: refresh_token,
|
||||||
|
deviceInfo: extractDeviceInfo(request),
|
||||||
|
})
|
||||||
|
return reply.send({ success: true, data: { ...tokens, profile } })
|
||||||
|
} catch (err) {
|
||||||
|
return sendAuthError(reply, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Logout — revoke current session
|
||||||
|
app.post('/logout', { preHandler: authenticate }, async (request, reply) => {
|
||||||
|
await logout({ sessionId: request.auth.sessionId })
|
||||||
|
return reply.send({ success: true })
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
import { authenticate } from '../../plugins/auth.js'
|
import { authenticate } from '../../plugins/auth.js'
|
||||||
import { getCustomerByFirebaseUid } from '../../services/customer.service.js'
|
|
||||||
import { getMitraByFirebaseUid } from '../../services/mitra.service.js'
|
|
||||||
import { getMessages } from '../../services/chat.service.js'
|
import { getMessages } from '../../services/chat.service.js'
|
||||||
import { getSessionClosures } from '../../services/closure.service.js'
|
import { getSessionClosures } from '../../services/closure.service.js'
|
||||||
import { registerDeviceToken } from '../../services/notification.service.js'
|
import { registerDeviceToken } from '../../services/notification.service.js'
|
||||||
@@ -11,23 +9,15 @@ import { TopicSensitivity, UserType } from '../../constants.js'
|
|||||||
const sql = getDb()
|
const sql = getDb()
|
||||||
|
|
||||||
const resolveUser = async (request, reply) => {
|
const resolveUser = async (request, reply) => {
|
||||||
const customer = await getCustomerByFirebaseUid(request.firebaseUser.uid)
|
if (request.auth?.userType !== UserType.CUSTOMER && request.auth?.userType !== UserType.MITRA) {
|
||||||
if (customer) {
|
return reply.code(403).send({
|
||||||
request.userType = UserType.CUSTOMER
|
|
||||||
request.userId = customer.id
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const mitra = await getMitraByFirebaseUid(request.firebaseUser.uid)
|
|
||||||
if (mitra) {
|
|
||||||
request.userType = UserType.MITRA
|
|
||||||
request.userId = mitra.id
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return reply.code(404).send({
|
|
||||||
success: false,
|
success: false,
|
||||||
error: { code: 'ACCOUNT_NOT_FOUND', message: 'Account not found' },
|
error: { code: 'FORBIDDEN', message: 'Customer or mitra account required' },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
request.userType = request.auth.userType
|
||||||
|
request.userId = request.auth.userId
|
||||||
|
}
|
||||||
|
|
||||||
// Verify session belongs to the authenticated user
|
// Verify session belongs to the authenticated user
|
||||||
const verifySessionOwnership = async (request, reply) => {
|
const verifySessionOwnership = async (request, reply) => {
|
||||||
|
|||||||
235
backend/src/services/auth.service.js
Normal file
235
backend/src/services/auth.service.js
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
import crypto from 'node:crypto'
|
||||||
|
import {
|
||||||
|
getCustomerById,
|
||||||
|
getCustomerByPhone,
|
||||||
|
getCustomerByGoogleSub,
|
||||||
|
getCustomerByAppleSub,
|
||||||
|
createAnonymousCustomerV2,
|
||||||
|
createCustomerWithIdentity,
|
||||||
|
upgradeCustomerIdentity,
|
||||||
|
} from './customer.service.js'
|
||||||
|
import {
|
||||||
|
getMitraByPhone,
|
||||||
|
getMitraById,
|
||||||
|
createMitra,
|
||||||
|
} from './mitra.service.js'
|
||||||
|
import {
|
||||||
|
getCcUserByEmail,
|
||||||
|
getCcUserById,
|
||||||
|
incrementCcUserFailedLogin,
|
||||||
|
resetCcUserFailedLogin,
|
||||||
|
} from './cc-user.service.js'
|
||||||
|
import { verifyGoogleIdToken, verifyAppleIdToken } from './social-identity.service.js'
|
||||||
|
import { verifyPassword } from './password.service.js'
|
||||||
|
import { issueTokens, refreshTokens as rotateRefresh, revokeSession } from './token.service.js'
|
||||||
|
import { getCcLoginLockoutConfig } from './config.service.js'
|
||||||
|
import { UserType } from '../constants.js'
|
||||||
|
|
||||||
|
const generateAnonymousDisplayName = () => {
|
||||||
|
const n = crypto.randomInt(1000, 10000)
|
||||||
|
return `Teman Anonim #${n}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AuthError extends Error {
|
||||||
|
constructor(message, code, statusCode) {
|
||||||
|
super(message)
|
||||||
|
this.code = code
|
||||||
|
this.statusCode = statusCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeIdentityConflict = async ({ existing, anonymousCustomerId }) => {
|
||||||
|
// If an authenticated identity is already linked to a DIFFERENT customer,
|
||||||
|
// reject. Merge is deferred per PRD.
|
||||||
|
if (existing && anonymousCustomerId && existing.id !== anonymousCustomerId) {
|
||||||
|
throw new AuthError(
|
||||||
|
'This account is already linked to another session. Please log out first.',
|
||||||
|
'IDENTITY_ALREADY_LINKED', 409,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Anonymous ---
|
||||||
|
|
||||||
|
export const signInAnonymous = async ({ deviceInfo } = {}) => {
|
||||||
|
const customer = await createAnonymousCustomerV2({
|
||||||
|
display_name: generateAnonymousDisplayName(),
|
||||||
|
})
|
||||||
|
const tokens = await issueTokens({
|
||||||
|
userType: UserType.CUSTOMER,
|
||||||
|
userId: customer.id,
|
||||||
|
deviceInfo,
|
||||||
|
})
|
||||||
|
return { tokens, profile: customer }
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Phone OTP — Customer ---
|
||||||
|
|
||||||
|
export const completeCustomerPhoneSignIn = async ({ phone, anonymousCustomerId, deviceInfo }) => {
|
||||||
|
const existing = await getCustomerByPhone(phone)
|
||||||
|
await normalizeIdentityConflict({ existing, anonymousCustomerId })
|
||||||
|
|
||||||
|
let customer
|
||||||
|
if (existing) {
|
||||||
|
customer = existing
|
||||||
|
} else if (anonymousCustomerId) {
|
||||||
|
customer = await upgradeCustomerIdentity(anonymousCustomerId, { phone })
|
||||||
|
} else {
|
||||||
|
customer = await createCustomerWithIdentity({ phone, display_name: null })
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = await issueTokens({
|
||||||
|
userType: UserType.CUSTOMER,
|
||||||
|
userId: customer.id,
|
||||||
|
deviceInfo,
|
||||||
|
})
|
||||||
|
return { tokens, profile: customer }
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Phone OTP — Mitra ---
|
||||||
|
|
||||||
|
export const completeMitraPhoneSignIn = async ({ phone, deviceInfo }) => {
|
||||||
|
let mitra = await getMitraByPhone(phone)
|
||||||
|
if (!mitra) {
|
||||||
|
mitra = await createMitra({ phone, display_name: phone })
|
||||||
|
}
|
||||||
|
const tokens = await issueTokens({
|
||||||
|
userType: UserType.MITRA,
|
||||||
|
userId: mitra.id,
|
||||||
|
deviceInfo,
|
||||||
|
})
|
||||||
|
return { tokens, profile: mitra }
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Google (customer only) ---
|
||||||
|
|
||||||
|
export const signInWithGoogle = async ({ idToken, anonymousCustomerId, deviceInfo }) => {
|
||||||
|
const google = await verifyGoogleIdToken(idToken)
|
||||||
|
const existing = await getCustomerByGoogleSub(google.sub)
|
||||||
|
await normalizeIdentityConflict({ existing, anonymousCustomerId })
|
||||||
|
|
||||||
|
let customer
|
||||||
|
if (existing) {
|
||||||
|
customer = existing
|
||||||
|
} else if (anonymousCustomerId) {
|
||||||
|
customer = await upgradeCustomerIdentity(anonymousCustomerId, {
|
||||||
|
google_sub: google.sub,
|
||||||
|
email: google.email,
|
||||||
|
display_name: google.name,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
customer = await createCustomerWithIdentity({
|
||||||
|
google_sub: google.sub,
|
||||||
|
email: google.email,
|
||||||
|
display_name: google.name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = await issueTokens({
|
||||||
|
userType: UserType.CUSTOMER,
|
||||||
|
userId: customer.id,
|
||||||
|
deviceInfo,
|
||||||
|
})
|
||||||
|
return { tokens, profile: customer }
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Apple (customer only) ---
|
||||||
|
|
||||||
|
export const signInWithApple = async ({ idToken, anonymousCustomerId, deviceInfo }) => {
|
||||||
|
const apple = await verifyAppleIdToken(idToken)
|
||||||
|
const existing = await getCustomerByAppleSub(apple.sub)
|
||||||
|
await normalizeIdentityConflict({ existing, anonymousCustomerId })
|
||||||
|
|
||||||
|
let customer
|
||||||
|
if (existing) {
|
||||||
|
customer = existing
|
||||||
|
} else if (anonymousCustomerId) {
|
||||||
|
customer = await upgradeCustomerIdentity(anonymousCustomerId, {
|
||||||
|
apple_sub: apple.sub,
|
||||||
|
email: apple.email,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
customer = await createCustomerWithIdentity({
|
||||||
|
apple_sub: apple.sub,
|
||||||
|
email: apple.email,
|
||||||
|
display_name: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = await issueTokens({
|
||||||
|
userType: UserType.CUSTOMER,
|
||||||
|
userId: customer.id,
|
||||||
|
deviceInfo,
|
||||||
|
})
|
||||||
|
return { tokens, profile: customer }
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Control center email/password ---
|
||||||
|
|
||||||
|
export const signInCcUser = async ({ email, password, deviceInfo }) => {
|
||||||
|
const user = await getCcUserByEmail(email)
|
||||||
|
// Constant-time response — same branch for "not found" vs "wrong password" OR lockout
|
||||||
|
if (!user) {
|
||||||
|
throw new AuthError('Invalid credentials', 'INVALID_CREDENTIALS', 401)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lockout check
|
||||||
|
if (user.lockout_until && new Date(user.lockout_until) > new Date()) {
|
||||||
|
throw new AuthError(
|
||||||
|
`Account locked until ${user.lockout_until}. Try again later.`,
|
||||||
|
'ACCOUNT_LOCKED', 423,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ok = await verifyPassword(password, user.password_hash)
|
||||||
|
if (!ok) {
|
||||||
|
const { max_attempts, lockout_minutes } = await getCcLoginLockoutConfig()
|
||||||
|
await incrementCcUserFailedLogin(user.id, lockout_minutes, max_attempts)
|
||||||
|
throw new AuthError('Invalid credentials', 'INVALID_CREDENTIALS', 401)
|
||||||
|
}
|
||||||
|
|
||||||
|
await resetCcUserFailedLogin(user.id)
|
||||||
|
|
||||||
|
const tokens = await issueTokens({
|
||||||
|
userType: UserType.CC_USER,
|
||||||
|
userId: user.id,
|
||||||
|
deviceInfo,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
tokens,
|
||||||
|
profile: {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
display_name: user.display_name,
|
||||||
|
role: user.role,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Refresh ---
|
||||||
|
|
||||||
|
export const refreshTokens = async ({ refreshToken, deviceInfo }) => {
|
||||||
|
const result = await rotateRefresh({ refreshToken, deviceInfo })
|
||||||
|
let profile = null
|
||||||
|
if (result.user_type === UserType.CUSTOMER) {
|
||||||
|
profile = await getCustomerById(result.user_id)
|
||||||
|
} else if (result.user_type === UserType.MITRA) {
|
||||||
|
profile = await getMitraById(result.user_id)
|
||||||
|
} else if (result.user_type === UserType.CC_USER) {
|
||||||
|
const ccUser = await getCcUserById(result.user_id)
|
||||||
|
profile = ccUser ? {
|
||||||
|
id: ccUser.id,
|
||||||
|
email: ccUser.email,
|
||||||
|
display_name: ccUser.display_name,
|
||||||
|
role: ccUser.role,
|
||||||
|
} : null
|
||||||
|
}
|
||||||
|
return { tokens: result, profile }
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Logout ---
|
||||||
|
|
||||||
|
export const logout = async ({ sessionId }) => {
|
||||||
|
if (!sessionId) return
|
||||||
|
await revokeSession(sessionId)
|
||||||
|
}
|
||||||
@@ -2,14 +2,16 @@ import { getDb } from '../db/client.js'
|
|||||||
|
|
||||||
const sql = getDb()
|
const sql = getDb()
|
||||||
|
|
||||||
export const getCcUserByFirebaseUid = async (firebase_uid) => {
|
|
||||||
|
export const getCcUserById = async (id) => {
|
||||||
const [user] = await sql`
|
const [user] = await sql`
|
||||||
SELECT
|
SELECT
|
||||||
u.id, u.email, u.display_name, u.created_at,
|
u.id, u.email, u.display_name, u.created_at,
|
||||||
|
u.password_hash, u.failed_login_count, u.lockout_until,
|
||||||
r.id as role_id, r.name as role_name, r.permissions
|
r.id as role_id, r.name as role_name, r.permissions
|
||||||
FROM control_center_users u
|
FROM control_center_users u
|
||||||
JOIN roles r ON r.id = u.role_id
|
JOIN roles r ON r.id = u.role_id
|
||||||
WHERE u.firebase_uid = ${firebase_uid}
|
WHERE u.id = ${id}
|
||||||
`
|
`
|
||||||
if (!user) return null
|
if (!user) return null
|
||||||
return {
|
return {
|
||||||
@@ -17,20 +19,77 @@ export const getCcUserByFirebaseUid = async (firebase_uid) => {
|
|||||||
email: user.email,
|
email: user.email,
|
||||||
display_name: user.display_name,
|
display_name: user.display_name,
|
||||||
created_at: user.created_at,
|
created_at: user.created_at,
|
||||||
|
password_hash: user.password_hash,
|
||||||
|
failed_login_count: user.failed_login_count,
|
||||||
|
lockout_until: user.lockout_until,
|
||||||
role: { id: user.role_id, name: user.role_name, permissions: user.permissions },
|
role: { id: user.role_id, name: user.role_name, permissions: user.permissions },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createCcUser = async ({ firebase_uid, email, display_name, role_id }) => {
|
export const getCcUserByEmail = async (email) => {
|
||||||
const [user] = await sql`
|
const [user] = await sql`
|
||||||
INSERT INTO control_center_users (firebase_uid, email, display_name, role_id)
|
SELECT
|
||||||
VALUES (${firebase_uid}, ${email}, ${display_name}, ${role_id})
|
u.id, u.email, u.display_name, u.created_at,
|
||||||
|
u.password_hash, u.failed_login_count, u.lockout_until,
|
||||||
|
r.id as role_id, r.name as role_name, r.permissions
|
||||||
|
FROM control_center_users u
|
||||||
|
JOIN roles r ON r.id = u.role_id
|
||||||
|
WHERE u.email = ${email}
|
||||||
|
`
|
||||||
|
if (!user) return null
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
display_name: user.display_name,
|
||||||
|
created_at: user.created_at,
|
||||||
|
password_hash: user.password_hash,
|
||||||
|
failed_login_count: user.failed_login_count,
|
||||||
|
lockout_until: user.lockout_until,
|
||||||
|
role: { id: user.role_id, name: user.role_name, permissions: user.permissions },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createCcUserWithPassword = async ({ email, display_name, role_id, password_hash }) => {
|
||||||
|
const [user] = await sql`
|
||||||
|
INSERT INTO control_center_users (email, display_name, role_id, password_hash)
|
||||||
|
VALUES (${email}, ${display_name}, ${role_id}, ${password_hash})
|
||||||
RETURNING id, email, display_name, role_id, created_at
|
RETURNING id, email, display_name, role_id, created_at
|
||||||
`
|
`
|
||||||
const [role] = await sql`SELECT id, name FROM roles WHERE id = ${role_id}`
|
const [role] = await sql`SELECT id, name FROM roles WHERE id = ${role_id}`
|
||||||
return { ...user, role }
|
return { ...user, role }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const updateCcUserPasswordHash = async (id, password_hash) => {
|
||||||
|
await sql`
|
||||||
|
UPDATE control_center_users SET password_hash = ${password_hash}
|
||||||
|
WHERE id = ${id}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const incrementCcUserFailedLogin = async (id, lockoutMinutes, maxAttempts) => {
|
||||||
|
// Atomic: increment counter; if reaches maxAttempts set lockout_until
|
||||||
|
const [row] = await sql`
|
||||||
|
UPDATE control_center_users
|
||||||
|
SET failed_login_count = failed_login_count + 1,
|
||||||
|
lockout_until = CASE
|
||||||
|
WHEN failed_login_count + 1 >= ${maxAttempts}
|
||||||
|
THEN NOW() + (${lockoutMinutes} || ' minutes')::interval
|
||||||
|
ELSE lockout_until
|
||||||
|
END
|
||||||
|
WHERE id = ${id}
|
||||||
|
RETURNING failed_login_count, lockout_until
|
||||||
|
`
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
export const resetCcUserFailedLogin = async (id) => {
|
||||||
|
await sql`
|
||||||
|
UPDATE control_center_users
|
||||||
|
SET failed_login_count = 0, lockout_until = NULL
|
||||||
|
WHERE id = ${id}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
export const listCcUsers = async ({ page = 1, limit = 20 }) => {
|
export const listCcUsers = async ({ page = 1, limit = 20 }) => {
|
||||||
const offset = (page - 1) * limit
|
const offset = (page - 1) * limit
|
||||||
const items = await sql`
|
const items = await sql`
|
||||||
|
|||||||
@@ -2,81 +2,6 @@ import { getDb } from '../db/client.js'
|
|||||||
|
|
||||||
const sql = getDb()
|
const sql = getDb()
|
||||||
|
|
||||||
export const createAnonymousCustomer = async ({ display_name, firebase_uid }) => {
|
|
||||||
// Return existing customer if already linked to this Firebase UID
|
|
||||||
const [existing] = await sql`
|
|
||||||
SELECT id, display_name, is_anonymous, created_at
|
|
||||||
FROM customers WHERE firebase_uid = ${firebase_uid}
|
|
||||||
`
|
|
||||||
if (existing) return existing
|
|
||||||
|
|
||||||
const [customer] = await sql`
|
|
||||||
INSERT INTO customers (display_name, is_anonymous, firebase_uid)
|
|
||||||
VALUES (${display_name}, true, ${firebase_uid})
|
|
||||||
RETURNING id, display_name, is_anonymous, created_at
|
|
||||||
`
|
|
||||||
return customer
|
|
||||||
}
|
|
||||||
|
|
||||||
export const linkCustomerAccount = async ({ customer_id, firebase_uid }) => {
|
|
||||||
const [existing] = await sql`
|
|
||||||
SELECT id, firebase_uid FROM customers WHERE id = ${customer_id}
|
|
||||||
`
|
|
||||||
if (!existing) throw Object.assign(new Error('Customer not found'), { code: 'NOT_FOUND', statusCode: 404 })
|
|
||||||
if (existing.firebase_uid) throw Object.assign(new Error('Account already linked'), { code: 'ALREADY_REGISTERED', statusCode: 409 })
|
|
||||||
|
|
||||||
// Also fetch phone from firebase_uid if exists in another customer record for uniqueness
|
|
||||||
const [firebaseLinked] = await sql`
|
|
||||||
SELECT id FROM customers WHERE firebase_uid = ${firebase_uid}
|
|
||||||
`
|
|
||||||
if (firebaseLinked) throw Object.assign(new Error('Account already linked'), { code: 'ALREADY_REGISTERED', statusCode: 409 })
|
|
||||||
|
|
||||||
const [updated] = await sql`
|
|
||||||
UPDATE customers
|
|
||||||
SET firebase_uid = ${firebase_uid}, is_anonymous = false
|
|
||||||
WHERE id = ${customer_id}
|
|
||||||
RETURNING id, display_name, is_anonymous, phone, created_at
|
|
||||||
`
|
|
||||||
return updated
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getCustomerByFirebaseUid = async (firebase_uid) => {
|
|
||||||
const [customer] = await sql`
|
|
||||||
SELECT id, display_name, is_anonymous, phone, created_at
|
|
||||||
FROM customers
|
|
||||||
WHERE firebase_uid = ${firebase_uid}
|
|
||||||
`
|
|
||||||
return customer
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getOrCreateCustomer = async ({ firebase_uid, phone, display_name }) => {
|
|
||||||
// Return existing customer if already linked to this Firebase UID
|
|
||||||
const existing = await getCustomerByFirebaseUid(firebase_uid)
|
|
||||||
if (existing) return existing
|
|
||||||
|
|
||||||
// Check if a customer with this phone already exists (re-login with new Firebase UID)
|
|
||||||
if (phone) {
|
|
||||||
const [byPhone] = await sql`
|
|
||||||
SELECT id, display_name, is_anonymous, phone, created_at
|
|
||||||
FROM customers WHERE phone = ${phone}
|
|
||||||
`
|
|
||||||
if (byPhone) {
|
|
||||||
// Link the new Firebase UID to the existing phone-based customer
|
|
||||||
await sql`UPDATE customers SET firebase_uid = ${firebase_uid} WHERE id = ${byPhone.id}`
|
|
||||||
return { ...byPhone, firebase_uid }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-create a registered (non-anonymous) customer for phone/social login
|
|
||||||
// display_name is null — user must set it on first login
|
|
||||||
const [customer] = await sql`
|
|
||||||
INSERT INTO customers (firebase_uid, phone, display_name, is_anonymous)
|
|
||||||
VALUES (${firebase_uid}, ${phone || null}, ${display_name || null}, false)
|
|
||||||
RETURNING id, display_name, is_anonymous, phone, created_at
|
|
||||||
`
|
|
||||||
return customer
|
|
||||||
}
|
|
||||||
|
|
||||||
export const updateCustomerDisplayName = async (customerId, displayName) => {
|
export const updateCustomerDisplayName = async (customerId, displayName) => {
|
||||||
const [customer] = await sql`
|
const [customer] = await sql`
|
||||||
UPDATE customers SET display_name = ${displayName}
|
UPDATE customers SET display_name = ${displayName}
|
||||||
@@ -85,3 +10,72 @@ export const updateCustomerDisplayName = async (customerId, displayName) => {
|
|||||||
`
|
`
|
||||||
return customer
|
return customer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Phase 3.4: Self-Managed Auth ---
|
||||||
|
|
||||||
|
const CUSTOMER_SELECT = sql`id, display_name, is_anonymous, phone, email, google_sub, apple_sub, created_at`
|
||||||
|
|
||||||
|
export const getCustomerById = async (id) => {
|
||||||
|
const [row] = await sql`SELECT ${CUSTOMER_SELECT} FROM customers WHERE id = ${id}`
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCustomerByPhone = async (phone) => {
|
||||||
|
const [row] = await sql`SELECT ${CUSTOMER_SELECT} FROM customers WHERE phone = ${phone}`
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCustomerByGoogleSub = async (googleSub) => {
|
||||||
|
const [row] = await sql`SELECT ${CUSTOMER_SELECT} FROM customers WHERE google_sub = ${googleSub}`
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCustomerByAppleSub = async (appleSub) => {
|
||||||
|
const [row] = await sql`SELECT ${CUSTOMER_SELECT} FROM customers WHERE apple_sub = ${appleSub}`
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createAnonymousCustomerV2 = async ({ display_name }) => {
|
||||||
|
const [row] = await sql`
|
||||||
|
INSERT INTO customers (display_name, is_anonymous)
|
||||||
|
VALUES (${display_name}, true)
|
||||||
|
RETURNING ${CUSTOMER_SELECT}
|
||||||
|
`
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createCustomerWithIdentity = async ({
|
||||||
|
display_name,
|
||||||
|
phone = null,
|
||||||
|
email = null,
|
||||||
|
google_sub = null,
|
||||||
|
apple_sub = null,
|
||||||
|
}) => {
|
||||||
|
const [row] = await sql`
|
||||||
|
INSERT INTO customers (display_name, is_anonymous, phone, email, google_sub, apple_sub)
|
||||||
|
VALUES (${display_name}, false, ${phone}, ${email}, ${google_sub}, ${apple_sub})
|
||||||
|
RETURNING ${CUSTOMER_SELECT}
|
||||||
|
`
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
export const upgradeCustomerIdentity = async (customerId, {
|
||||||
|
phone,
|
||||||
|
email,
|
||||||
|
google_sub,
|
||||||
|
apple_sub,
|
||||||
|
display_name,
|
||||||
|
}) => {
|
||||||
|
const [row] = await sql`
|
||||||
|
UPDATE customers SET
|
||||||
|
is_anonymous = false,
|
||||||
|
phone = COALESCE(${phone ?? null}, phone),
|
||||||
|
email = COALESCE(${email ?? null}, email),
|
||||||
|
google_sub = COALESCE(${google_sub ?? null}, google_sub),
|
||||||
|
apple_sub = COALESCE(${apple_sub ?? null}, apple_sub),
|
||||||
|
display_name = COALESCE(${display_name ?? null}, display_name)
|
||||||
|
WHERE id = ${customerId}
|
||||||
|
RETURNING ${CUSTOMER_SELECT}
|
||||||
|
`
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,28 +2,22 @@ import { getDb } from '../db/client.js'
|
|||||||
|
|
||||||
const sql = getDb()
|
const sql = getDb()
|
||||||
|
|
||||||
export const getMitraByFirebaseUid = async (firebase_uid) => {
|
|
||||||
const [mitra] = await sql`
|
|
||||||
SELECT id, display_name, phone, is_active, created_at
|
|
||||||
FROM mitras
|
|
||||||
WHERE firebase_uid = ${firebase_uid}
|
|
||||||
`
|
|
||||||
return mitra
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getMitraByPhone = async (phone) => {
|
export const getMitraByPhone = async (phone) => {
|
||||||
const [mitra] = await sql`
|
const [mitra] = await sql`
|
||||||
SELECT id, display_name, phone, is_active, firebase_uid, created_at
|
SELECT id, display_name, phone, is_active, created_at
|
||||||
FROM mitras
|
FROM mitras
|
||||||
WHERE phone = ${phone}
|
WHERE phone = ${phone}
|
||||||
`
|
`
|
||||||
return mitra
|
return mitra
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setMitraFirebaseUid = async (id, firebase_uid) => {
|
export const getMitraById = async (id) => {
|
||||||
await sql`
|
const [mitra] = await sql`
|
||||||
UPDATE mitras SET firebase_uid = ${firebase_uid} WHERE id = ${id}
|
SELECT id, display_name, phone, is_active, created_at
|
||||||
|
FROM mitras
|
||||||
|
WHERE id = ${id}
|
||||||
`
|
`
|
||||||
|
return mitra
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createMitra = async ({ phone, display_name }) => {
|
export const createMitra = async ({ phone, display_name }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user