From dee432b4a0e564ca4dede0044965a44985ff0773 Mon Sep 17 00:00:00 2001 From: Jonathan Barrow Date: Wed, 31 May 2023 21:29:41 -0400 Subject: [PATCH] Added gRPC service for the API (for future use) --- SETUP.md | 3 +- package-lock.json | 321 +++++++++++------- src/config-manager.ts | 14 +- src/server.ts | 2 +- .../grpc/account/api-key-middleware.ts | 16 + .../grpc/{ => account}/get-user-data.ts | 0 src/services/grpc/account/implementation.ts | 6 + .../grpc/{ => api}/api-key-middleware.ts | 7 +- .../grpc/api/authentication-middleware.ts | 55 +++ src/services/grpc/api/forgot-password.ts | 29 ++ src/services/grpc/api/get-user-data.ts | 46 +++ src/services/grpc/api/implementation.ts | 20 ++ src/services/grpc/api/login.ts | 90 +++++ src/services/grpc/api/register.ts | 266 +++++++++++++++ src/services/grpc/api/reset-password.ts | 79 +++++ .../grpc/api/set-discord-connection-data.ts | 26 ++ .../grpc/api/set-stripe-connection-data.ts | 91 +++++ src/services/grpc/api/update-user-data.ts | 48 +++ src/services/grpc/login.ts | 6 - src/services/grpc/register-pnid.ts | 7 - src/services/grpc/server.ts | 27 +- src/types/common/config.ts | 5 +- 22 files changed, 1000 insertions(+), 164 deletions(-) create mode 100644 src/services/grpc/account/api-key-middleware.ts rename src/services/grpc/{ => account}/get-user-data.ts (100%) create mode 100644 src/services/grpc/account/implementation.ts rename src/services/grpc/{ => api}/api-key-middleware.ts (75%) create mode 100644 src/services/grpc/api/authentication-middleware.ts create mode 100644 src/services/grpc/api/forgot-password.ts create mode 100644 src/services/grpc/api/get-user-data.ts create mode 100644 src/services/grpc/api/implementation.ts create mode 100644 src/services/grpc/api/login.ts create mode 100644 src/services/grpc/api/register.ts create mode 100644 src/services/grpc/api/reset-password.ts create mode 100644 src/services/grpc/api/set-discord-connection-data.ts create mode 100644 src/services/grpc/api/set-stripe-connection-data.ts create mode 100644 src/services/grpc/api/update-user-data.ts delete mode 100644 src/services/grpc/login.ts delete mode 100644 src/services/grpc/register-pnid.ts diff --git a/SETUP.md b/SETUP.md index 0ff4310..1aeaba2 100644 --- a/SETUP.md +++ b/SETUP.md @@ -75,6 +75,7 @@ Configurations are loaded through environment variables. `.env` files are suppor | `PN_ACT_CONFIG_CDN_BASE_URL` | URL for serving CDN contents (usually the same as s3 endpoint) | No | | `PN_ACT_CONFIG_WEBSITE_BASE` | Website URL | Yes | | `PN_ACT_CONFIG_AES_KEY` | AES-256 key used for encrypting tokens | No | -| `PN_ACT_CONFIG_GRPC_API_KEY` | gRPC API key that other clients use to interact with this server | No | +| `PN_ACT_CONFIG_GRPC_MASTER_API_KEY_ACCOUNT` | Master API key to interact with the account gRPC service | No | +| `PN_ACT_CONFIG_GRPC_MASTER_API_KEY_API` | Master API key to interact with the API gRPC service | No | | `PN_ACT_CONFIG_GRPC_PORT` | gRPC server port | No | | `PN_ACT_CONFIG_STRIPE_SECRET_KEY` | Stripe API key. Used to cancel subscriptions when scrubbing PNIDs | Yes | \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 80029e5..f1ab5ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -92,23 +92,23 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.0.tgz", - "integrity": "sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", + "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/eslintrc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz", - "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", + "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.5.1", + "espree": "^9.5.2", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -124,9 +124,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz", - "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==", + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.40.0.tgz", + "integrity": "sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -145,15 +145,15 @@ } }, "node_modules/@grpc/proto-loader": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.6.tgz", - "integrity": "sha512-QyAXR8Hyh7uMDmveWxDSUcJr9NAWaZ2I6IXgAYvQmfflwouTM+rArE2eEaCtLlRqO81j7pRLCt81IefUei6Zbw==", + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.7.tgz", + "integrity": "sha512-1TIeXOi8TuSCQprPItwoMymZXxWT0CPxUhkrkeCUH+D8U7QDwQ6b7SUz2MaLuWM2llT+J/TVFLmQI5KtML3BhQ==", "dependencies": { "@types/long": "^4.0.1", "lodash.camelcase": "^4.3.0", "long": "^4.0.0", "protobufjs": "^7.0.0", - "yargs": "^16.2.0" + "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" @@ -686,9 +686,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.16.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.3.tgz", - "integrity": "sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q==" + "version": "18.16.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.7.tgz", + "integrity": "sha512-MFg7ua/bRtnA1hYE3pVyWxGd/r7aMqjNOdHvlSsXV3n8iaeGKkOaPzpJh6/ovf4bEXWcojkeMJpTsq3mzXW4IQ==" }, "node_modules/@types/node-rsa": { "version": "1.1.1", @@ -729,9 +729,9 @@ } }, "node_modules/@types/semver": { - "version": "7.3.13", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", - "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", "dev": true }, "node_modules/@types/send": { @@ -755,9 +755,9 @@ } }, "node_modules/@types/validator": { - "version": "13.7.15", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.15.tgz", - "integrity": "sha512-yeinDVQunb03AEP8luErFcyf/7Lf7AzKCD0NXfgVoGCCQDNpZET8Jgq74oBgqKld3hafLbfzt/3inUdQvaFeXQ==", + "version": "13.7.17", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.17.tgz", + "integrity": "sha512-aqayTNmeWrZcvnG2MG9eGYI6b7S5fl+yKgPs6bAjOTwPS316R5SxBGKvtSExfyoJU7pIeHJfsHI0Ji41RVMkvQ==", "dev": true }, "node_modules/@types/webidl-conversions": { @@ -775,15 +775,15 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.1.tgz", - "integrity": "sha512-AVi0uazY5quFB9hlp2Xv+ogpfpk77xzsgsIEWyVS7uK/c7MZ5tw7ZPbapa0SbfkqE0fsAMkz5UwtgMLVk2BQAg==", + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.5.tgz", + "integrity": "sha512-feA9xbVRWJZor+AnLNAr7A8JRWeZqHUf4T9tlP+TN04b05pFVhO5eN7/O93Y/1OUlLMHKbnJisgDURs/qvtqdg==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.1", - "@typescript-eslint/type-utils": "5.59.1", - "@typescript-eslint/utils": "5.59.1", + "@typescript-eslint/scope-manager": "5.59.5", + "@typescript-eslint/type-utils": "5.59.5", + "@typescript-eslint/utils": "5.59.5", "debug": "^4.3.4", "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", @@ -809,14 +809,14 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.1.tgz", - "integrity": "sha512-nzjFAN8WEu6yPRDizIFyzAfgK7nybPodMNFGNH0M9tei2gYnYszRDqVA0xlnRjkl7Hkx2vYrEdb6fP2a21cG1g==", + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.5.tgz", + "integrity": "sha512-NJXQC4MRnF9N9yWqQE2/KLRSOLvrrlZb48NGVfBa+RuPMN6B7ZcK5jZOvhuygv4D64fRKnZI4L4p8+M+rfeQuw==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.59.1", - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/typescript-estree": "5.59.1", + "@typescript-eslint/scope-manager": "5.59.5", + "@typescript-eslint/types": "5.59.5", + "@typescript-eslint/typescript-estree": "5.59.5", "debug": "^4.3.4" }, "engines": { @@ -836,13 +836,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz", - "integrity": "sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==", + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.5.tgz", + "integrity": "sha512-jVecWwnkX6ZgutF+DovbBJirZcAxgxC0EOHYt/niMROf8p4PwxxG32Qdhj/iIQQIuOflLjNkxoXyArkcIP7C3A==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/visitor-keys": "5.59.1" + "@typescript-eslint/types": "5.59.5", + "@typescript-eslint/visitor-keys": "5.59.5" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -853,13 +853,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.1.tgz", - "integrity": "sha512-ZMWQ+Oh82jWqWzvM3xU+9y5U7MEMVv6GLioM3R5NJk6uvP47kZ7YvlgSHJ7ERD6bOY7Q4uxWm25c76HKEwIjZw==", + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.5.tgz", + "integrity": "sha512-4eyhS7oGym67/pSxA2mmNq7X164oqDYNnZCUayBwJZIRVvKpBCMBzFnFxjeoDeShjtO6RQBHBuwybuX3POnDqg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.59.1", - "@typescript-eslint/utils": "5.59.1", + "@typescript-eslint/typescript-estree": "5.59.5", + "@typescript-eslint/utils": "5.59.5", "debug": "^4.3.4", "tsutils": "^3.21.0" }, @@ -880,9 +880,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.1.tgz", - "integrity": "sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==", + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.5.tgz", + "integrity": "sha512-xkfRPHbqSH4Ggx4eHRIO/eGL8XL4Ysb4woL8c87YuAo8Md7AUjyWKa9YMwTL519SyDPrfEgKdewjkxNCVeJW7w==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -893,13 +893,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz", - "integrity": "sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==", + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.5.tgz", + "integrity": "sha512-+XXdLN2CZLZcD/mO7mQtJMvCkzRfmODbeSKuMY/yXbGkzvA9rJyDY5qDYNoiz2kP/dmyAxXquL2BvLQLJFPQIg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/visitor-keys": "5.59.1", + "@typescript-eslint/types": "5.59.5", + "@typescript-eslint/visitor-keys": "5.59.5", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -920,17 +920,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.1.tgz", - "integrity": "sha512-MkTe7FE+K1/GxZkP5gRj3rCztg45bEhsd8HYjczBuYm+qFHP5vtZmjx3B0yUCDotceQ4sHgTyz60Ycl225njmA==", + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.5.tgz", + "integrity": "sha512-sCEHOiw+RbyTii9c3/qN74hYDPNORb8yWCoPLmB7BIflhplJ65u2PBpdRla12e3SSTJ2erRkPjz7ngLHhUegxA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.1", - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/typescript-estree": "5.59.1", + "@typescript-eslint/scope-manager": "5.59.5", + "@typescript-eslint/types": "5.59.5", + "@typescript-eslint/typescript-estree": "5.59.5", "eslint-scope": "^5.1.1", "semver": "^7.3.7" }, @@ -946,12 +946,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz", - "integrity": "sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==", + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.5.tgz", + "integrity": "sha512-qL+Oz+dbeBRTeyJTIy0eniD3uvqU7x+y1QceBismZ41hd4aBSRh8UAw4pZP0+XzLuPZmx4raNMq/I+59W2lXKA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/types": "5.59.5", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -1202,9 +1202,9 @@ } }, "node_modules/aws-sdk": { - "version": "2.1368.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1368.0.tgz", - "integrity": "sha512-Yc3s8PqdcYG4wyCOpDj4TwXacGZGDgZBJ/XAtzMLKW2wN2c4uu7GwSosLxZ8ejzbAbcqjf080odPuD8P0819tw==", + "version": "2.1376.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1376.0.tgz", + "integrity": "sha512-ja/Xnft8BDcDEz786VJFPrWpuWpOgsA+QzBAwzsjYeIolQ/vEs/bbXkoS085fOoeAPEhYWQh9wog7cVvrQPJFQ==", "dependencies": { "buffer": "4.9.2", "events": "1.1.1", @@ -1231,6 +1231,11 @@ "isarray": "^1.0.0" } }, + "node_modules/aws-sdk/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, "node_modules/aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -1387,13 +1392,38 @@ } }, "node_modules/bson": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/bson/-/bson-5.2.0.tgz", - "integrity": "sha512-HevkSpDbpUfsrHWmWiAsNavANKYIErV2ePXllp1bwq5CDreAaFVj6RVlZpJnxK4WWDCJ/5jMUpaY6G526q3Hjg==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-5.3.0.tgz", + "integrity": "sha512-ukmCZMneMlaC5ebPHXIkP8YJzNl5DC41N5MAIvKDqLggdao342t4McltoJBQfQya/nHBWAcSsYRqlXPoQkTJag==", "engines": { "node": ">=14.20.1" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true, + "peer": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -1425,6 +1455,27 @@ "resolved": "https://registry.npmjs.org/buffer-to-uint8array/-/buffer-to-uint8array-1.1.0.tgz", "integrity": "sha512-JVTSbtA6YuOGdu5NL0ffizsBwuwbTXfV7OC91FhazMz9UKP/KlDS+Z7wuiSRClbnTQz52fJgVXI9YDXQRVl2sQ==" }, + "node_modules/buffer/node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true, + "peer": true + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1514,13 +1565,16 @@ "integrity": "sha512-nJ22fZvCwkJfMppkOEE7GciLX08rDnVzEJ+U46kBFZtwNzH2V4tNxMWa9Tc365WspCxy1c3NtGJ5EeT4SgjmCA==" }, "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dependencies": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" } }, "node_modules/clone-response": { @@ -1604,6 +1658,11 @@ "typedarray": "^0.0.6" } }, + "node_modules/concat-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, "node_modules/concat-stream/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -1906,6 +1965,11 @@ "readable-stream": "^2.0.2" } }, + "node_modules/duplexer2/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, "node_modules/duplexer2/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -2148,15 +2212,15 @@ } }, "node_modules/eslint": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz", - "integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==", + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.40.0.tgz", + "integrity": "sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.2", - "@eslint/js": "8.39.0", + "@eslint/eslintrc": "^2.0.3", + "@eslint/js": "8.40.0", "@humanwhocodes/config-array": "^0.11.8", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -2167,8 +2231,8 @@ "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.0", - "espree": "^9.5.1", + "eslint-visitor-keys": "^3.4.1", + "espree": "^9.5.2", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -2218,9 +2282,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", - "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2255,14 +2319,14 @@ } }, "node_modules/espree": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz", - "integrity": "sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==", + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", + "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", "dev": true, "dependencies": { "acorn": "^8.8.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.0" + "eslint-visitor-keys": "^3.4.1" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -3359,9 +3423,9 @@ "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==" }, "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" }, "node_modules/isexe": { "version": "2.0.0", @@ -3729,9 +3793,9 @@ } }, "node_modules/minipass": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", - "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", "engines": { "node": ">=8" } @@ -3820,9 +3884,9 @@ } }, "node_modules/mongoose": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-7.1.0.tgz", - "integrity": "sha512-shoo9z/7o96Ojx69wpJn65+EC+Mt3q1SWTducW+F2Y4ieCXo0lZwpCZedgC841MIvJ7V8o6gmzoN1NfcnOTOuw==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-7.1.1.tgz", + "integrity": "sha512-AIxaWwGY+td7QOMk4NgK6fbRuGovFyDzv65nU1uj1DsUh3lpjfP3iFYHSR+sUKrs7nbp19ksLlRXkmInBteSCA==", "dependencies": { "bson": "^5.2.0", "kareem": "2.5.1", @@ -3997,9 +4061,9 @@ "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" }, "node_modules/node-fetch": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", - "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -4043,9 +4107,9 @@ } }, "node_modules/nodemailer": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.1.tgz", - "integrity": "sha512-qHw7dOiU5UKNnQpXktdgQ1d3OFgRAekuvbJLcdG5dnEo/GtcTHRYM7+UfJARdOFU9WUQO8OiIamgWPmiSFHYAA==", + "version": "6.9.2", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.2.tgz", + "integrity": "sha512-4+TYaa/e1nIxQfyw/WzNPYTEZ5OvHIDEnmjs4LPmIfccPQN+2CYKmGHjWixn/chzD3bmUTu5FMfpltizMxqzdg==", "engines": { "node": ">=6.0.0" } @@ -4311,7 +4375,7 @@ }, "node_modules/pretendo-grpc-ts": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/PretendoNetwork/grpc-ts.git#028bc63602eb3cf33ea5e775face04b4c586399c", + "resolved": "git+ssh://git@github.com/PretendoNetwork/grpc-ts.git#b7bcb0d691cec3f88ff0627cb7431c14dd2d2d1d", "hasInstallScript": true, "license": "ISC", "dependencies": { @@ -4524,11 +4588,6 @@ "string_decoder": "~0.10.x" } }, - "node_modules/readable-stream/node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" - }, "node_modules/redis": { "version": "4.6.6", "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.6.tgz", @@ -5006,6 +5065,11 @@ "through2": "~2.0.3" } }, + "node_modules/static-module/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, "node_modules/static-module/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -5105,9 +5169,9 @@ } }, "node_modules/stripe": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/stripe/-/stripe-12.3.0.tgz", - "integrity": "sha512-B9Q1b0gbKY/Z4fQc1Y82VpHTFLh8A67D6kdcFtgpGfTovVkI7SamE66vmVaWNHgcUjPqI8x6wVvksdRf/ucTDw==", + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-12.4.0.tgz", + "integrity": "sha512-QjZRzKi3wf8TsuJf/fd6/ejfPgwNptDIzFogRWaRzP3oMJnSD73I2YxR0Eje5zfrU8FmddYWZYawoUejqN+o1w==", "dependencies": { "@types/node": ">=8.1.0", "qs": "^6.11.0" @@ -5140,13 +5204,13 @@ } }, "node_modules/tar": { - "version": "6.1.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz", - "integrity": "sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==", + "version": "6.1.14", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.14.tgz", + "integrity": "sha512-piERznXu0U7/pW7cdSn7hjqySIVTYT6F76icmFk7ptU7dDYlXTm5r9A6K04R2vU3olYgoKeo1Cg3eeu5nhftAw==", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", - "minipass": "^4.0.0", + "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" @@ -5195,6 +5259,11 @@ "xtend": "~4.0.1" } }, + "node_modules/through2/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, "node_modules/through2/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -5685,28 +5754,28 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dependencies": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.0", + "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "yargs-parser": "^21.1.1" }, "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/yesno": { diff --git a/src/config-manager.ts b/src/config-manager.ts index 3305eb7..7bad4ac 100644 --- a/src/config-manager.ts +++ b/src/config-manager.ts @@ -67,7 +67,10 @@ export const config: Config = { website_base: process.env.PN_ACT_CONFIG_WEBSITE_BASE || '', aes_key: process.env.PN_ACT_CONFIG_AES_KEY || '', grpc: { - api_key: process.env.PN_ACT_CONFIG_GRPC_API_KEY || '', + master_api_keys: { + account: process.env.PN_ACT_CONFIG_GRPC_MASTER_API_KEY_ACCOUNT || '', + api: process.env.PN_ACT_CONFIG_GRPC_MASTER_API_KEY_API || '', + }, port: Number(process.env.PN_ACT_CONFIG_GRPC_PORT || ''), } }; @@ -180,8 +183,13 @@ if (!config.aes_key) { process.exit(0); } -if (!config.grpc.api_key) { - LOG_ERROR('gRPC API key is not set. Set the PN_ACT_CONFIG_GRPC_API_KEY environment variable'); +if (!config.grpc.master_api_keys.account) { + LOG_ERROR('Master gRPC API key for the account service is not set. Set the PN_ACT_CONFIG_GRPC_MASTER_API_KEY_ACCOUNT environment variable'); + process.exit(0); +} + +if (!config.grpc.master_api_keys.api) { + LOG_ERROR('Master gRPC API key for the api service is not set. Set the PN_ACT_CONFIG_GRPC_MASTER_API_KEY_API environment variable'); process.exit(0); } diff --git a/src/server.ts b/src/server.ts index 80b67cd..d8fdb63 100644 --- a/src/server.ts +++ b/src/server.ts @@ -24,7 +24,7 @@ import assets from '@/services/assets'; import { config } from '@/config-manager'; -const app = express(); +const app: express.Express = express(); // START APPLICATION diff --git a/src/services/grpc/account/api-key-middleware.ts b/src/services/grpc/account/api-key-middleware.ts new file mode 100644 index 0000000..63e03c0 --- /dev/null +++ b/src/services/grpc/account/api-key-middleware.ts @@ -0,0 +1,16 @@ +import { Status, ServerMiddlewareCall, CallContext, ServerError } from 'nice-grpc'; +import { config } from '@/config-manager'; + +export async function* apiKeyMiddleware( + call: ServerMiddlewareCall, + context: CallContext, +): AsyncGenerator { + console.log(call.method); + const apiKey: string | undefined = context.metadata.get('X-API-Key'); + + if (!apiKey || apiKey !== config.grpc.master_api_keys.account) { + throw new ServerError(Status.UNAUTHENTICATED, 'Missing or invalid API key'); + } + + return yield* call.next(call.request, context); +} \ No newline at end of file diff --git a/src/services/grpc/get-user-data.ts b/src/services/grpc/account/get-user-data.ts similarity index 100% rename from src/services/grpc/get-user-data.ts rename to src/services/grpc/account/get-user-data.ts diff --git a/src/services/grpc/account/implementation.ts b/src/services/grpc/account/implementation.ts new file mode 100644 index 0000000..266e157 --- /dev/null +++ b/src/services/grpc/account/implementation.ts @@ -0,0 +1,6 @@ +import { AccountServiceImplementation } from 'pretendo-grpc-ts/dist/account/account_service'; +import { getUserData } from '@/services/grpc/account/get-user-data'; + +export const accountServiceImplementation: AccountServiceImplementation = { + getUserData, +}; \ No newline at end of file diff --git a/src/services/grpc/api-key-middleware.ts b/src/services/grpc/api/api-key-middleware.ts similarity index 75% rename from src/services/grpc/api-key-middleware.ts rename to src/services/grpc/api/api-key-middleware.ts index 4fffca3..19c4e3b 100644 --- a/src/services/grpc/api-key-middleware.ts +++ b/src/services/grpc/api/api-key-middleware.ts @@ -7,11 +7,8 @@ export async function* apiKeyMiddleware( ): AsyncGenerator { const apiKey: string | undefined = context.metadata.get('X-API-Key'); - if (!apiKey || apiKey !== config.grpc.api_key) { - throw new ServerError( - Status.UNAUTHENTICATED, - 'Missing or invalid API key', - ); + if (!apiKey || apiKey !== config.grpc.master_api_keys.api) { + throw new ServerError(Status.UNAUTHENTICATED, 'Missing or invalid API key'); } return yield* call.next(call.request, context); diff --git a/src/services/grpc/api/authentication-middleware.ts b/src/services/grpc/api/authentication-middleware.ts new file mode 100644 index 0000000..c650e6d --- /dev/null +++ b/src/services/grpc/api/authentication-middleware.ts @@ -0,0 +1,55 @@ +import { Status, ServerMiddlewareCall, CallContext, ServerError } from 'nice-grpc'; +import { getPNIDByBearerAuth } from '@/database'; +import type { HydratedPNIDDocument } from '@/types/mongoose/pnid'; + +// * These paths require that a token be present +const TOKEN_REQUIRED_PATHS: string[] = [ + '/api.API/GetUserData', + '/api.API/UpdateUserData', + '/api.API/ResetPassword', // * This paths token is not an authentication token, it is a password reset token + '/api.API/SetDiscordConnectionData', + '/api.API/SetStripeConnectionData', + '/api.API/RemoveConnection' +]; + +export type AuthenticationCallContextExt = { + pnid: HydratedPNIDDocument | null; +}; + +export async function* authenticationMiddleware( + call: ServerMiddlewareCall, + context: CallContext, +): AsyncGenerator { + const token: string | undefined = context.metadata.get('X-Token')?.trim(); + + if (!token && TOKEN_REQUIRED_PATHS.includes(call.method.path)) { + throw new ServerError(Status.UNAUTHENTICATED, 'Missing or invalid authentication token'); + } + + try { + let pnid: HydratedPNIDDocument | null = null; + + if (token) { + pnid = await getPNIDByBearerAuth(token); + } + + if (!pnid && TOKEN_REQUIRED_PATHS.includes(call.method.path)) { + throw new ServerError(Status.UNAUTHENTICATED, 'Missing or invalid authentication token'); + } + + return yield* call.next(call.request, { + ...context, + pnid + }); + } catch (error) { + let message: string = 'Unknown server error'; + + console.log(error); + + if (error instanceof Error) { + message = error.message; + } + + throw new ServerError(Status.INVALID_ARGUMENT, message); + } +} \ No newline at end of file diff --git a/src/services/grpc/api/forgot-password.ts b/src/services/grpc/api/forgot-password.ts new file mode 100644 index 0000000..d9a887f --- /dev/null +++ b/src/services/grpc/api/forgot-password.ts @@ -0,0 +1,29 @@ +import { Status, ServerError } from 'nice-grpc'; +import validator from 'validator'; +import { ForgotPasswordRequest } from 'pretendo-grpc-ts/dist/api/forgot_password_rpc'; +import { getPNIDByEmailAddress, getPNIDByUsername } from '@/database'; +import { sendForgotPasswordEmail } from '@/util'; +import type { Empty } from 'pretendo-grpc-ts/dist/api/google/protobuf/empty'; +import type { HydratedPNIDDocument } from '@/types/mongoose/pnid'; + +export async function forgotPassword(request: ForgotPasswordRequest): Promise { + const input: string = request.emailAddressOrUsername.trim(); + + if (!input) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid or missing input'); + } + + let pnid: HydratedPNIDDocument | null; + + if (validator.isEmail(input)) { + pnid = await getPNIDByEmailAddress(input); + } else { + pnid = await getPNIDByUsername(input); + } + + if (pnid) { + await sendForgotPasswordEmail(pnid); + } + + return {}; +} \ No newline at end of file diff --git a/src/services/grpc/api/get-user-data.ts b/src/services/grpc/api/get-user-data.ts new file mode 100644 index 0000000..beeb92e --- /dev/null +++ b/src/services/grpc/api/get-user-data.ts @@ -0,0 +1,46 @@ +import { CallContext } from 'nice-grpc'; +import { GetUserDataResponse, DeepPartial } from 'pretendo-grpc-ts/dist/api/get_user_data_rpc'; +import { config } from '@/config-manager'; +import type { Empty } from 'pretendo-grpc-ts/dist/api/google/protobuf/empty'; +import type { AuthenticationCallContextExt } from '@/services/grpc/api/authentication-middleware'; +import type { HydratedPNIDDocument } from '@/types/mongoose/pnid'; + +export async function getUserData(_request: Empty, context: CallContext & AuthenticationCallContextExt): Promise> { + // * This is asserted in authentication-middleware, we know this is never null + const pnid: HydratedPNIDDocument = context.pnid!; + + return { + deleted: pnid.deleted, + creationDate: pnid.creation_date, + updatedDate: pnid.updated, + pid: pnid.pid, + username: pnid.username, + accessLevel: pnid.access_level, + serverAccessLevel: pnid.server_access_level, + mii: { + name: pnid.mii.name, + data: pnid.mii.data, + url: `${config.cdn.base_url}/mii/${pnid.pid}/standard.tga`, + }, + birthday: pnid.birthdate, + gender: pnid.gender, + country: pnid.country, + timezone: pnid.timezone.name, + language: pnid.language, + emailAddress: pnid.email.address, + connections: { + discord: { + id: pnid.connections.discord.id + }, + stripe: { + customerId: pnid.connections.stripe.customer_id, + subscriptionId: pnid.connections.stripe.subscription_id, + priceId: pnid.connections.stripe.price_id, + tierLevel: pnid.connections.stripe.tier_level, + tierName: pnid.connections.stripe.tier_name, + latestWebhookTimestamp: pnid.connections.stripe.latest_webhook_timestamp + } + }, + marketingFlag: pnid.flags.marketing + }; +} \ No newline at end of file diff --git a/src/services/grpc/api/implementation.ts b/src/services/grpc/api/implementation.ts new file mode 100644 index 0000000..337c513 --- /dev/null +++ b/src/services/grpc/api/implementation.ts @@ -0,0 +1,20 @@ +import { APIServiceImplementation } from 'pretendo-grpc-ts/dist/api/api_service'; +import { register } from '@/services/grpc/api/register'; +import { login } from '@/services/grpc/api/login'; +import { getUserData } from '@/services/grpc/api/get-user-data'; +import { updateUserData } from '@/services/grpc/api/update-user-data'; +import { forgotPassword } from '@/services/grpc/api/forgot-password'; +import { resetPassword } from '@/services/grpc/api/reset-password'; +import { setDiscordConnectionData } from '@/services/grpc/api/set-discord-connection-data'; +import { setStripeConnectionData } from '@/services/grpc/api/set-stripe-connection-data'; + +export const apiServiceImplementation: APIServiceImplementation = { + register, + login, + getUserData, + updateUserData, + forgotPassword, + resetPassword, + setDiscordConnectionData, + setStripeConnectionData +}; \ No newline at end of file diff --git a/src/services/grpc/api/login.ts b/src/services/grpc/api/login.ts new file mode 100644 index 0000000..2069305 --- /dev/null +++ b/src/services/grpc/api/login.ts @@ -0,0 +1,90 @@ +import { Status, ServerError } from 'nice-grpc'; +import { LoginRequest, LoginResponse, DeepPartial } from 'pretendo-grpc-ts/dist/api/login_rpc'; +import bcrypt from 'bcrypt'; +import { getPNIDByUsername, getPNIDByBearerAuth } from '@/database'; +import { nintendoPasswordHash, generateToken} from '@/util'; +import { config } from '@/config-manager'; +import type { TokenOptions } from '@/types/common/token-options'; +import type { HydratedPNIDDocument } from '@/types/mongoose/pnid'; + +export async function login(request: LoginRequest): Promise> { + const grantType: string = request.grantType?.trim(); + const username: string | undefined = request.username?.trim(); + const password: string | undefined = request.password?.trim(); + const refreshToken: string | undefined = request.refreshToken?.trim(); + + if (!['password', 'refresh_token'].includes(grantType)) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid grant type'); + } + + if (grantType === 'password' && !username) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid or missing username'); + } + + if (grantType === 'password' && !password) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid or missing password'); + } + + if (grantType === 'refresh_token' && !refreshToken) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid or missing refresh token'); + } + + let pnid: HydratedPNIDDocument | null; + + if (grantType === 'password') { + pnid = await getPNIDByUsername(username!); // * We know username will never be null here + + if (!pnid) { + throw new ServerError(Status.INVALID_ARGUMENT, 'User not found'); + } + + const hashedPassword: string = nintendoPasswordHash(password!, pnid.pid); // * We know password will never be null here + + if (!bcrypt.compareSync(hashedPassword, pnid.password)) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Password is incorrect'); + } + } else { + pnid = await getPNIDByBearerAuth(refreshToken!); // * We know refreshToken will never be null here + + if (!pnid) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid or missing refresh token'); + } + } + + if (pnid.deleted) { + throw new ServerError(Status.UNAUTHENTICATED, 'Account has been deleted'); + } + + const accessTokenOptions: TokenOptions = { + system_type: 0x3, // * API + token_type: 0x1, // * OAuth Access + pid: pnid.pid, + access_level: pnid.access_level, + title_id: BigInt(0), + expire_time: BigInt(Date.now() + (3600 * 1000)) + }; + + const refreshTokenOptions: TokenOptions = { + system_type: 0x3, // * API + token_type: 0x2, // * OAuth Refresh + pid: pnid.pid, + access_level: pnid.access_level, + title_id: BigInt(0), + expire_time: BigInt(Date.now() + (3600 * 1000)) + }; + + const accessTokenBuffer: Buffer | null = await generateToken(config.aes_key, accessTokenOptions); + const refreshTokenBuffer: Buffer | null = await generateToken(config.aes_key, refreshTokenOptions); + + const accessToken: string = accessTokenBuffer ? accessTokenBuffer.toString('hex') : ''; + const newRefreshToken: string = refreshTokenBuffer ? refreshTokenBuffer.toString('hex') : ''; + + // TODO - Handle null tokens + + return { + accessToken: accessToken, + tokenType: 'Bearer', + expiresIn: 3600, + refreshToken: newRefreshToken + }; +} \ No newline at end of file diff --git a/src/services/grpc/api/register.ts b/src/services/grpc/api/register.ts new file mode 100644 index 0000000..8800d9c --- /dev/null +++ b/src/services/grpc/api/register.ts @@ -0,0 +1,266 @@ +import crypto from 'node:crypto'; +import { Status, ServerError } from 'nice-grpc'; +import { RegisterRequest, DeepPartial } from 'pretendo-grpc-ts/dist/api/register_rpc'; +import { LoginResponse } from 'pretendo-grpc-ts/dist/api/login_rpc'; +import emailvalidator from 'email-validator'; +import bcrypt from 'bcrypt'; +import moment from 'moment'; +import hcaptcha from 'hcaptcha'; +import Mii from 'mii-js'; +import mongoose from 'mongoose'; +import { doesPNIDExist, connection as databaseConnection } from '@/database'; +import { nintendoPasswordHash, sendConfirmationEmail, generateToken } from '@/util'; +import { LOG_ERROR } from '@/logger'; +import { PNID } from '@/models/pnid'; +import { NEXAccount } from '@/models/nex-account'; +import { config, disabledFeatures } from '@/config-manager'; +import type { TokenOptions } from '@/types/common/token-options'; +import type { HydratedNEXAccountDocument } from '@/types/mongoose/nex-account'; +import type { HydratedPNIDDocument } from '@/types/mongoose/pnid'; + +const PNID_VALID_CHARACTERS_REGEX: RegExp = /^[\w\-.]*$/; +const PNID_PUNCTUATION_START_REGEX: RegExp = /^[_\-.]/; +const PNID_PUNCTUATION_END_REGEX: RegExp = /[_\-.]$/; +const PNID_PUNCTUATION_DUPLICATE_REGEX: RegExp = /[_\-.]{2,}/; + +// This sucks +const PASSWORD_WORD_OR_NUMBER_REGEX: RegExp = /(?=.*[a-zA-Z])(?=.*\d).*/; +const PASSWORD_WORD_OR_PUNCTUATION_REGEX: RegExp = /(?=.*[a-zA-Z])(?=.*[_\-.]).*/; +const PASSWORD_NUMBER_OR_PUNCTUATION_REGEX: RegExp = /(?=.*\d)(?=.*[_\-.]).*/; +const PASSWORD_REPEATED_CHARACTER_REGEX: RegExp = /(.)\1\1/; + +const DEFAULT_MII_DATA: Buffer = Buffer.from('AwAAQOlVognnx0GC2/uogAOzuI0n2QAAAEBEAGUAZgBhAHUAbAB0AAAAAAAAAEBAAAAhAQJoRBgmNEYUgRIXaA0AACkAUkhQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGm9', 'base64'); + +export async function register(request: RegisterRequest): Promise> { + const email: string = request.email?.trim(); + const username: string = request.username?.trim(); + const miiName: string = request.miiName?.trim(); + const password: string = request.password?.trim(); + const passwordConfirm: string = request.passwordConfirm?.trim(); + const captchaResponse: string | undefined = request.captchaResponse?.trim(); + + // * Only validate the captcha if that's enabled + if (!disabledFeatures.captcha) { + if (!captchaResponse) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Must fill in captcha'); + } + + const captchaVerify: VerifyResponse = await hcaptcha.verify(config.hcaptcha.secret, captchaResponse); + + if (!captchaVerify.success) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Captcha verification failed'); + } + } + + if (!email) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Must enter an email address'); + } + + if (!emailvalidator.validate(email)) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid email address'); + } + + if (!username) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Must enter a username'); + } + + if (username.length < 6) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Username is too short'); + } + + if (username.length > 16) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Username is too long'); + } + + if (!PNID_VALID_CHARACTERS_REGEX.test(username)) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Username contains invalid characters'); + } + + if (PNID_PUNCTUATION_START_REGEX.test(username)) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Username cannot begin with punctuation characters'); + } + + if (PNID_PUNCTUATION_END_REGEX.test(username)) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Username cannot end with punctuation characters'); + } + + if (PNID_PUNCTUATION_DUPLICATE_REGEX.test(username)) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Two or more punctuation characters cannot be used in a row'); + } + + const userExists: boolean = await doesPNIDExist(username); + + if (userExists) { + throw new ServerError(Status.INVALID_ARGUMENT, 'PNID already in use'); + } + + if (!miiName) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Must enter a Mii name'); + } + + const miiNameBuffer: Buffer = Buffer.from(miiName, 'utf16le'); // * UTF8 to UTF16 + + if (miiNameBuffer.length > 0x14) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Mii name too long'); + } + + if (!password) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Must enter a password'); + } + + if (password.length < 6 || password.length > 16) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Password must be between 6 and 16 characters long'); + } + + if (password.toLowerCase() === username.toLowerCase()) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Password cannot be the same as username'); + } + + if (!PASSWORD_WORD_OR_NUMBER_REGEX.test(password) && !PASSWORD_WORD_OR_PUNCTUATION_REGEX.test(password) && !PASSWORD_NUMBER_OR_PUNCTUATION_REGEX.test(password)) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Password must have combination of letters, numbers, and/or punctuation characters'); + } + + if (PASSWORD_REPEATED_CHARACTER_REGEX.test(password)) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Password may not have 3 repeating characters'); + } + + if (password !== passwordConfirm) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Passwords do not match'); + } + + const mii: Mii = new Mii(DEFAULT_MII_DATA); + mii.miiName = miiName; + + const creationDate: string = moment().format('YYYY-MM-DDTHH:MM:SS'); + let pnid: HydratedPNIDDocument; + let nexAccount: HydratedNEXAccountDocument; + + const session: mongoose.ClientSession = await databaseConnection().startSession(); + await session.startTransaction(); + + try { + // * PNIDs can only be registered from a Wii U + // * So assume website users are WiiU NEX accounts + nexAccount = new NEXAccount({ + device_type: 'wiiu', + }); + + await nexAccount.generatePID(); + await nexAccount.generatePassword(); + + // Quick hack to get the PIDs to match + // TODO: Change this maybe? + // NN with a NNID will always use the NNID PID + // even if the provided NEX PID is different + // To fix this we make them the same PID + nexAccount.owning_pid = nexAccount.pid; + + await nexAccount.save({ session }); + + const primaryPasswordHash: string = nintendoPasswordHash(password, nexAccount.pid); + const passwordHash: string = await bcrypt.hash(primaryPasswordHash, 10); + + pnid = new PNID({ + pid: nexAccount.pid, + creation_date: creationDate, + updated: creationDate, + username: username, + usernameLower: username.toLowerCase(), + password: passwordHash, + birthdate: '1990-01-01', // TODO: Change this + gender: 'M', // TODO: Change this + country: 'US', // TODO: Change this + language: 'en', // TODO: Change this + email: { + address: email.toLowerCase(), + primary: true, // TODO: Change this + parent: true, // TODO: Change this + reachable: false, // TODO: Change this + validated: false, // TODO: Change this + id: crypto.randomBytes(4).readUInt32LE() + }, + region: 0x310B0000, // TODO: Change this + timezone: { + name: 'America/New_York', // TODO: Change this + offset: -14400 // TODO: Change this + }, + mii: { + name: miiName, + primary: true, // TODO: Change this + data: mii.encode().toString('base64'), + id: crypto.randomBytes(4).readUInt32LE(), + hash: crypto.randomBytes(7).toString('hex'), + image_url: '', // deprecated, will be removed in the future + image_id: crypto.randomBytes(4).readUInt32LE() + }, + flags: { + active: true, // TODO: Change this + marketing: true, // TODO: Change this + off_device: true // TODO: Change this + }, + identification: { + email_code: 1, // will be overwritten before saving + email_token: '' // will be overwritten before saving + } + }); + + await pnid.generateEmailValidationCode(); + await pnid.generateEmailValidationToken(); + await pnid.generateMiiImages(); + + await pnid.save({ session }); + + await session.commitTransaction(); + } catch (error) { + let message: string = 'Unknown Mongo error'; + + if (error instanceof Error) { + message = error.message; + } + + LOG_ERROR(`[gRPC] /api.API/Register: ${message}`); + + await session.abortTransaction(); + + throw new ServerError(Status.INVALID_ARGUMENT, message); + } finally { + // * This runs regardless of failure + // * Returning on catch will not prevent this from running + await session.endSession(); + } + + await sendConfirmationEmail(pnid); + + const accessTokenOptions: TokenOptions = { + system_type: 0x3, // * API + token_type: 0x1, // * OAuth Access + pid: pnid.pid, + access_level: pnid.access_level, + title_id: BigInt(0), + expire_time: BigInt(Date.now() + (3600 * 1000)) + }; + + const refreshTokenOptions: TokenOptions = { + system_type: 0x3, // * API + token_type: 0x2, // * OAuth Refresh + pid: pnid.pid, + access_level: pnid.access_level, + title_id: BigInt(0), + expire_time: BigInt(Date.now() + (3600 * 1000)) + }; + + const accessTokenBuffer: Buffer | null = await generateToken(config.aes_key, accessTokenOptions); + const refreshTokenBuffer: Buffer | null = await generateToken(config.aes_key, refreshTokenOptions); + + const accessToken: string = accessTokenBuffer ? accessTokenBuffer.toString('hex') : ''; + const refreshToken: string = refreshTokenBuffer ? refreshTokenBuffer.toString('hex') : ''; + + // TODO - Handle null tokens + + return { + accessToken: accessToken, + tokenType: 'Bearer', + expiresIn: 3600, + refreshToken: refreshToken + }; +} \ No newline at end of file diff --git a/src/services/grpc/api/reset-password.ts b/src/services/grpc/api/reset-password.ts new file mode 100644 index 0000000..d01111c --- /dev/null +++ b/src/services/grpc/api/reset-password.ts @@ -0,0 +1,79 @@ +import bcrypt from 'bcrypt'; +import { Status, ServerError } from 'nice-grpc'; +import { ResetPasswordRequest } from 'pretendo-grpc-ts/dist/api/reset_password_rpc'; +import { decryptToken, unpackToken, nintendoPasswordHash } from '@/util'; +import { getPNIDByPID } from '@/database'; +import type { Empty } from 'pretendo-grpc-ts/dist/api/google/protobuf/empty'; +import type { Token } from '@/types/common/token'; +import type { HydratedPNIDDocument } from '@/types/mongoose/pnid'; + +// This sucks +const PASSWORD_WORD_OR_NUMBER_REGEX: RegExp = /(?=.*[a-zA-Z])(?=.*\d).*/; +const PASSWORD_WORD_OR_PUNCTUATION_REGEX: RegExp = /(?=.*[a-zA-Z])(?=.*[_\-.]).*/; +const PASSWORD_NUMBER_OR_PUNCTUATION_REGEX: RegExp = /(?=.*\d)(?=.*[_\-.]).*/; +const PASSWORD_REPEATED_CHARACTER_REGEX: RegExp = /(.)\1\1/; + +export async function resetPassword(request: ResetPasswordRequest): Promise { + const password: string = request.password.trim(); + const passwordConfirm: string = request.passwordConfirm.trim(); + const token: string = request.token.trim(); + + if (!token) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Missing token'); + } + + let unpackedToken: Token; + try { + const decryptedToken: Buffer = await decryptToken(Buffer.from(token, 'base64')); + unpackedToken = unpackToken(decryptedToken); + } catch (error) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token'); + } + + if (unpackedToken.expire_time < Date.now()) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Token expired'); + } + + const pnid: HydratedPNIDDocument | null = await getPNIDByPID(unpackedToken.pid); + + if (!pnid) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token. No user found'); + } + + if (!password) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Must enter a password'); + } + + if (password.length < 6) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Password is too short'); + } + + if (password.length > 16) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Password is too long'); + } + + if (password.toLowerCase() === pnid.usernameLower) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Password cannot be the same as username'); + } + + if (!PASSWORD_WORD_OR_NUMBER_REGEX.test(password) && !PASSWORD_WORD_OR_PUNCTUATION_REGEX.test(password) && !PASSWORD_NUMBER_OR_PUNCTUATION_REGEX.test(password)) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Password must have combination of letters, numbers, and/or punctuation characters'); + } + + if (PASSWORD_REPEATED_CHARACTER_REGEX.test(password)) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Password may not have 3 repeating characters'); + } + + if (password !== passwordConfirm) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Passwords do not match'); + } + + const primaryPasswordHash: string = nintendoPasswordHash(password, pnid.pid); + const passwordHash: string = await bcrypt.hash(primaryPasswordHash, 10); + + pnid.password = passwordHash; + + await pnid.save(); + + return {}; +} \ No newline at end of file diff --git a/src/services/grpc/api/set-discord-connection-data.ts b/src/services/grpc/api/set-discord-connection-data.ts new file mode 100644 index 0000000..2f3dbed --- /dev/null +++ b/src/services/grpc/api/set-discord-connection-data.ts @@ -0,0 +1,26 @@ +import { Status, ServerError, CallContext } from 'nice-grpc'; +import { SetDiscordConnectionDataRequest } from 'pretendo-grpc-ts/dist/api/set_discord_connection_data_rpc'; +import type { Empty } from 'pretendo-grpc-ts/dist/api/google/protobuf/empty'; +import type { AuthenticationCallContextExt } from '@/services/grpc/api/authentication-middleware'; +import type { HydratedPNIDDocument } from '@/types/mongoose/pnid'; + +export async function setDiscordConnectionData(request: SetDiscordConnectionDataRequest, context: CallContext & AuthenticationCallContextExt): Promise{ + // * This is asserted in authentication-middleware, we know this is never null + const pnid: HydratedPNIDDocument = context.pnid!; + + try { + pnid.connections.discord.id = request.id; + + await pnid.save(); + } catch (error) { + let message: string = 'Unknown Mongo error'; + + if (error instanceof Error) { + message = error.message; + } + + throw new ServerError(Status.INVALID_ARGUMENT, message); + } + + return {}; +} \ No newline at end of file diff --git a/src/services/grpc/api/set-stripe-connection-data.ts b/src/services/grpc/api/set-stripe-connection-data.ts new file mode 100644 index 0000000..238bc08 --- /dev/null +++ b/src/services/grpc/api/set-stripe-connection-data.ts @@ -0,0 +1,91 @@ +import { Status, ServerError, CallContext } from 'nice-grpc'; +import { SetStripeConnectionDataRequest } from 'pretendo-grpc-ts/dist/api/set_stripe_connection_data_rpc'; +import { PNID } from '@/models/pnid'; +import type { Empty } from 'pretendo-grpc-ts/dist/api/google/protobuf/empty'; +import type { AuthenticationCallContextExt } from '@/services/grpc/api/authentication-middleware'; +import type { HydratedPNIDDocument } from '@/types/mongoose/pnid'; + +type StripeMongoUpdateScheme = { + access_level?: number; + server_access_level?: string; + 'connections.stripe.customer_id'?: string; + 'connections.stripe.subscription_id'?: string; + 'connections.stripe.price_id'?: string; + 'connections.stripe.tier_level'?: number; + 'connections.stripe.tier_name'?: string; + 'connections.stripe.latest_webhook_timestamp': number; +}; + +export async function setStripeConnectionData(request: SetStripeConnectionDataRequest, context: CallContext & AuthenticationCallContextExt): Promise{ + // * This is asserted in authentication-middleware, we know this is never null + const pnid: HydratedPNIDDocument = context.pnid!; + + const updateData: StripeMongoUpdateScheme = { + 'connections.stripe.latest_webhook_timestamp': request.timestamp + }; + + if (request.customerId && !pnid.connections.stripe.customer_id) { + updateData['connections.stripe.customer_id'] = request.customerId; + } + + // * These checks allow for null/0 values in order to reset data if needed + + if (request.accessLevel !== undefined) { + updateData.access_level = request.accessLevel; + } + + if (request.serverAccessLevel !== undefined) { + updateData.server_access_level = request.serverAccessLevel; + } + + if (request.subscriptionId !== undefined) { + updateData['connections.stripe.subscription_id'] = request.subscriptionId; + } + + if (request.subscriptionId !== undefined) { + updateData['connections.stripe.subscription_id'] = request.subscriptionId; + } + + if (request.priceId !== undefined) { + updateData['connections.stripe.price_id'] = request.priceId; + } + + if (request.tierLevel !== undefined) { + updateData['connections.stripe.tier_level'] = request.tierLevel; + } + + if (request.tierName !== undefined) { + updateData['connections.stripe.tier_name'] = request.tierName; + } + + try { + if (pnid.connections.stripe.latest_webhook_timestamp && pnid.connections.stripe.customer_id) { + // * Stripe customer data has already been initialized, update it + await PNID.updateOne({ + pid: pnid.pid, + 'connections.stripe.latest_webhook_timestamp': { + $lte: request.timestamp + } + }, { $set: updateData }).exec(); + } else { + // * Initialize a new Stripe user + if (!request.customerId) { + throw new ServerError(Status.INVALID_ARGUMENT, 'No Stripe user data found and no custom ID provided'); + } + + PNID.updateOne({ pid: pnid.pid }, { + $set: updateData + }, { upsert: true }).exec(); + } + } catch (error) { + let message: string = 'Unknown Mongo error'; + + if (error instanceof Error) { + message = error.message; + } + + throw new ServerError(Status.INVALID_ARGUMENT, message); + } + + return {}; +} \ No newline at end of file diff --git a/src/services/grpc/api/update-user-data.ts b/src/services/grpc/api/update-user-data.ts new file mode 100644 index 0000000..3747144 --- /dev/null +++ b/src/services/grpc/api/update-user-data.ts @@ -0,0 +1,48 @@ +import { CallContext } from 'nice-grpc'; +import { UpdateUserDataRequest, DeepPartial } from 'pretendo-grpc-ts/dist/api/update_user_data_rpc'; +import { GetUserDataResponse } from 'pretendo-grpc-ts/dist/api/get_user_data_rpc'; +import { config } from '@/config-manager'; +import type { HydratedPNIDDocument } from '@/types/mongoose/pnid'; +import type { AuthenticationCallContextExt } from '@/services/grpc/api/authentication-middleware'; + +export async function updateUserData(_request: UpdateUserDataRequest, context: CallContext & AuthenticationCallContextExt): Promise> { + // * This is asserted in authentication-middleware, we know this is never null + const pnid: HydratedPNIDDocument = context.pnid!; + + // TODO - STUBBED, DO SOMETHING HERE + + return { + deleted: pnid.deleted, + creationDate: pnid.creation_date, + updatedDate: pnid.updated, + pid: pnid.pid, + username: pnid.username, + accessLevel: pnid.access_level, + serverAccessLevel: pnid.server_access_level, + mii: { + name: pnid.mii.name, + data: pnid.mii.data, + url: `${config.cdn.base_url}/mii/${pnid.pid}/standard.tga`, + }, + birthday: pnid.birthdate, + gender: pnid.gender, + country: pnid.country, + timezone: pnid.timezone.name, + language: pnid.language, + emailAddress: pnid.email.address, + connections: { + discord: { + id: pnid.connections.discord.id + }, + stripe: { + customerId: pnid.connections.stripe.customer_id, + subscriptionId: pnid.connections.stripe.subscription_id, + priceId: pnid.connections.stripe.price_id, + tierLevel: pnid.connections.stripe.tier_level, + tierName: pnid.connections.stripe.tier_name, + latestWebhookTimestamp: pnid.connections.stripe.latest_webhook_timestamp + } + }, + marketingFlag: pnid.flags.marketing + }; +} \ No newline at end of file diff --git a/src/services/grpc/login.ts b/src/services/grpc/login.ts deleted file mode 100644 index 5cd2da3..0000000 --- a/src/services/grpc/login.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Status, ServerError } from 'nice-grpc'; -import { LoginRequest, LoginResponse, DeepPartial } from 'pretendo-grpc-ts/dist/account/login_rpc'; - -export async function login(_request: LoginRequest): Promise> { - throw new ServerError(Status.UNIMPLEMENTED, 'Login method not implemented'); -} \ No newline at end of file diff --git a/src/services/grpc/register-pnid.ts b/src/services/grpc/register-pnid.ts deleted file mode 100644 index 1da888e..0000000 --- a/src/services/grpc/register-pnid.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Status, ServerError } from 'nice-grpc'; -import { RegisterPNIDRequest, DeepPartial } from 'pretendo-grpc-ts/dist/account/register_pnid_rpc'; -import { LoginResponse } from 'pretendo-grpc-ts/dist/account/login_rpc'; - -export async function registerPNID(_request: RegisterPNIDRequest): Promise> { - throw new ServerError(Status.UNIMPLEMENTED, 'Register PNID method not implemented'); -} \ No newline at end of file diff --git a/src/services/grpc/server.ts b/src/services/grpc/server.ts index f568114..5e4bc8d 100644 --- a/src/services/grpc/server.ts +++ b/src/services/grpc/server.ts @@ -1,22 +1,21 @@ import { createServer, Server } from 'nice-grpc'; -import { AccountServiceImplementation, AccountDefinition } from 'pretendo-grpc-ts/dist/account/account_service'; +import { AccountDefinition } from 'pretendo-grpc-ts/dist/account/account_service'; +import { APIDefinition } from 'pretendo-grpc-ts/dist/api/api_service'; + +import { apiKeyMiddleware as accountApiKeyMiddleware } from '@/services/grpc/account/api-key-middleware'; +import { apiKeyMiddleware as apiApiKeyMiddleware } from '@/services/grpc/api/api-key-middleware'; +import { authenticationMiddleware as apiAuthenticationMiddleware } from '@/services/grpc/api/authentication-middleware'; + +import { accountServiceImplementation } from '@/services/grpc/account/implementation'; +import { apiServiceImplementation } from '@/services/grpc/api/implementation'; + import { config } from '@/config-manager'; -import { apiKeyMiddleware } from '@/services/grpc/api-key-middleware'; -import { getUserData } from '@/services/grpc/get-user-data'; -import { login } from '@/services/grpc/login'; -import { registerPNID } from '@/services/grpc/register-pnid'; - -const accountServiceImplementation: AccountServiceImplementation = { - getUserData, - login, - registerPNID, -}; - export async function startGRPCServer(): Promise { - const server: Server = createServer().use(apiKeyMiddleware); + const server: Server = createServer(); - server.add(AccountDefinition, accountServiceImplementation); + server.with(accountApiKeyMiddleware).add(AccountDefinition, accountServiceImplementation); + server.with(apiApiKeyMiddleware).with(apiAuthenticationMiddleware).add(APIDefinition, apiServiceImplementation); await server.listen(`0.0.0.0:${config.grpc.port}`); } \ No newline at end of file diff --git a/src/types/common/config.ts b/src/types/common/config.ts index 753322d..ce328d9 100644 --- a/src/types/common/config.ts +++ b/src/types/common/config.ts @@ -39,7 +39,10 @@ export interface Config { website_base: string; aes_key: string; grpc: { - api_key: string; + master_api_keys: { + account: string; + api: string; + }; port: number; }; stripe?: {