mirror of
https://github.com/PretendoNetwork/account.git
synced 2026-09-28 05:08:07 -05:00
Merge branch 'dev'
This commit is contained in:
4
SETUP.md
4
SETUP.md
@@ -96,4 +96,6 @@ Configurations are loaded through environment variables. `.env` files are suppor
|
||||
| `PN_ACT_CONFIG_GRPC_MIIVERSE_HOST` | Used to remove Miiverse user data during account deletion | No |
|
||||
| `PN_ACT_CONFIG_GRPC_MIIVERSE_PORT` | Used to remove Miiverse user data during account deletion | No |
|
||||
| `PN_ACT_CONFIG_GRPC_MIIVERSE_KEY_API` | Used to remove Miiverse user data during account deletion | No |
|
||||
| `PN_ACT_PROVISIONING_SERVER_CONFIG` | Specify a path to a JSON file containing a list of servers to provision automatically to the DB | Yes |
|
||||
| `PN_ACT_PROVISIONING_SERVER_CONFIG` | Specify a path to a JSON file containing a list of servers to provision automatically to the DB | Yes |
|
||||
| `PN_ACT_CONFIG_METRICS_ENABLED` | Set to `true` to enable the metrics server, uses the metrics port | Yes |
|
||||
| `PN_ACT_CONFIG_METRICS_PORT` | The HTTP port the metrics server listens on | Yes |
|
||||
|
||||
245
package-lock.json
generated
245
package-lock.json
generated
@@ -12,7 +12,7 @@
|
||||
"@aws-sdk/client-s3": "^3.657.0",
|
||||
"@aws-sdk/client-ses": "^3.515.0",
|
||||
"@inquirer/prompts": "^7.2.0",
|
||||
"@pretendonetwork/grpc": "^2.5.4",
|
||||
"@pretendonetwork/grpc": "^2.6.1",
|
||||
"bcrypt": "^5.0.0",
|
||||
"buffer-crc32": "^0.2.13",
|
||||
"colors": "^1.4.0",
|
||||
@@ -24,6 +24,7 @@
|
||||
"ejs": "^3.1.10",
|
||||
"email-validator": "^2.0.4",
|
||||
"express": "^4.17.1",
|
||||
"express-prom-bundle": "^7.0.2",
|
||||
"express-rate-limit": "^6.7.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"got": "^11.8.2",
|
||||
@@ -52,7 +53,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hcaptcha/types": "^1.0.3",
|
||||
"@pretendonetwork/eslint-config": "^0.1.4",
|
||||
"@pretendonetwork/eslint-config": "^0.2.0",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/buffer-crc32": "^0.2.2",
|
||||
"@types/cors": "^2.8.13",
|
||||
@@ -1636,29 +1637,39 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/api": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz",
|
||||
"integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@pretendonetwork/eslint-config": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@pretendonetwork/eslint-config/-/eslint-config-0.1.4.tgz",
|
||||
"integrity": "sha512-3Y6PvfAOLBLMstWUwbuAP/Qfiq0KitAvKpYFWCR6aSel60wxHEJpPAB92rGSXep6LRaZw2qlLbGcLrHn12onsA==",
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@pretendonetwork/eslint-config/-/eslint-config-0.2.0.tgz",
|
||||
"integrity": "sha512-1E3eDYxWVjICsfcVYB4St2pVBDcZTi7/NRYdM+smEWwZ+2SY2r0CuwlgenTejSBLyF5xvtZpR66RSUp/YUyJFw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-plugin-eslint-comments": "^4.6.0",
|
||||
"@stylistic/eslint-plugin": "^5.9.0",
|
||||
"eslint": "^9.39.3",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"@eslint-community/eslint-plugin-eslint-comments": "^4.7.2",
|
||||
"@stylistic/eslint-plugin": "^5.10.0",
|
||||
"eslint": "^9.39.5",
|
||||
"eslint-import-resolver-typescript": "^4.4.5",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^17.4.0",
|
||||
"typescript-eslint": "^8.56.1"
|
||||
"globals": "^17.11.0",
|
||||
"typescript-eslint": "^8.67.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@pretendonetwork/eslint-config/node_modules/globals": {
|
||||
"version": "17.7.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz",
|
||||
"integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==",
|
||||
"version": "17.11.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-17.11.0.tgz",
|
||||
"integrity": "sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1669,9 +1680,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@pretendonetwork/grpc": {
|
||||
"version": "2.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@pretendonetwork/grpc/-/grpc-2.5.4.tgz",
|
||||
"integrity": "sha512-spjg6sOSP8z+9T9vqR9RM5guJb7UHYQlFBfb6p6WxpaYHsXlmVIRPeQlR7NTkgpNzARV/4QVpqerBbtLDvN4oA==",
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@pretendonetwork/grpc/-/grpc-2.6.1.tgz",
|
||||
"integrity": "sha512-+HKJ8cTV4AV8PJgUqg2Tocz4EJTajqErY9r/Oul8WIZM2lO+0gRPSf3Mlxi9Ghvjq88ZS9pWWccAKtn77ePrew==",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.2.2",
|
||||
@@ -1965,7 +1976,6 @@
|
||||
"version": "1.19.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||
"integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/connect": "*",
|
||||
@@ -1998,7 +2008,6 @@
|
||||
"version": "3.4.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
@@ -2035,7 +2044,6 @@
|
||||
"version": "4.17.25",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz",
|
||||
"integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
@@ -2048,7 +2056,6 @@
|
||||
"version": "4.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.9.tgz",
|
||||
"integrity": "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
@@ -2085,7 +2092,6 @@
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
|
||||
"integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
@@ -2131,7 +2137,6 @@
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/morgan": {
|
||||
@@ -2184,14 +2189,12 @@
|
||||
"version": "6.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz",
|
||||
"integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/range-parser": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
|
||||
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/responselike": {
|
||||
@@ -2207,7 +2210,6 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
|
||||
"integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
@@ -2217,7 +2219,6 @@
|
||||
"version": "1.15.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz",
|
||||
"integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/http-errors": "*",
|
||||
@@ -2229,7 +2230,6 @@
|
||||
"version": "0.17.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz",
|
||||
"integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mime": "^1",
|
||||
@@ -2260,17 +2260,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz",
|
||||
"integrity": "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==",
|
||||
"version": "8.67.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.67.0.tgz",
|
||||
"integrity": "sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.64.0",
|
||||
"@typescript-eslint/type-utils": "8.64.0",
|
||||
"@typescript-eslint/utils": "8.64.0",
|
||||
"@typescript-eslint/visitor-keys": "8.64.0",
|
||||
"@typescript-eslint/scope-manager": "8.67.0",
|
||||
"@typescript-eslint/type-utils": "8.67.0",
|
||||
"@typescript-eslint/utils": "8.67.0",
|
||||
"@typescript-eslint/visitor-keys": "8.67.0",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
@@ -2283,22 +2283,22 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.64.0",
|
||||
"@typescript-eslint/parser": "^8.67.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.64.0.tgz",
|
||||
"integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==",
|
||||
"version": "8.67.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.67.0.tgz",
|
||||
"integrity": "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.64.0",
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/typescript-estree": "8.64.0",
|
||||
"@typescript-eslint/visitor-keys": "8.64.0",
|
||||
"@typescript-eslint/scope-manager": "8.67.0",
|
||||
"@typescript-eslint/types": "8.67.0",
|
||||
"@typescript-eslint/typescript-estree": "8.67.0",
|
||||
"@typescript-eslint/visitor-keys": "8.67.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2314,14 +2314,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz",
|
||||
"integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==",
|
||||
"version": "8.67.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.67.0.tgz",
|
||||
"integrity": "sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.64.0",
|
||||
"@typescript-eslint/types": "^8.64.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.67.0",
|
||||
"@typescript-eslint/types": "^8.67.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2336,14 +2336,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz",
|
||||
"integrity": "sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==",
|
||||
"version": "8.67.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.67.0.tgz",
|
||||
"integrity": "sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/visitor-keys": "8.64.0"
|
||||
"@typescript-eslint/types": "8.67.0",
|
||||
"@typescript-eslint/visitor-keys": "8.67.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -2354,9 +2354,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz",
|
||||
"integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==",
|
||||
"version": "8.67.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.67.0.tgz",
|
||||
"integrity": "sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2371,15 +2371,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz",
|
||||
"integrity": "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==",
|
||||
"version": "8.67.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.67.0.tgz",
|
||||
"integrity": "sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/typescript-estree": "8.64.0",
|
||||
"@typescript-eslint/utils": "8.64.0",
|
||||
"@typescript-eslint/types": "8.67.0",
|
||||
"@typescript-eslint/typescript-estree": "8.67.0",
|
||||
"@typescript-eslint/utils": "8.67.0",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
@@ -2396,9 +2396,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz",
|
||||
"integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==",
|
||||
"version": "8.67.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.67.0.tgz",
|
||||
"integrity": "sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2410,16 +2410,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz",
|
||||
"integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==",
|
||||
"version": "8.67.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.67.0.tgz",
|
||||
"integrity": "sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.64.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.64.0",
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/visitor-keys": "8.64.0",
|
||||
"@typescript-eslint/project-service": "8.67.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.67.0",
|
||||
"@typescript-eslint/types": "8.67.0",
|
||||
"@typescript-eslint/visitor-keys": "8.67.0",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
@@ -2438,16 +2438,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.64.0.tgz",
|
||||
"integrity": "sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==",
|
||||
"version": "8.67.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.67.0.tgz",
|
||||
"integrity": "sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.64.0",
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/typescript-estree": "8.64.0"
|
||||
"@typescript-eslint/scope-manager": "8.67.0",
|
||||
"@typescript-eslint/types": "8.67.0",
|
||||
"@typescript-eslint/typescript-estree": "8.67.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -2462,13 +2462,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz",
|
||||
"integrity": "sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==",
|
||||
"version": "8.67.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.67.0.tgz",
|
||||
"integrity": "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/types": "8.67.0",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -3302,6 +3302,13 @@
|
||||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/bintrees": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz",
|
||||
"integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/bit-buffer": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/bit-buffer/-/bit-buffer-0.2.5.tgz",
|
||||
@@ -5308,6 +5315,24 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express-prom-bundle": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/express-prom-bundle/-/express-prom-bundle-7.0.2.tgz",
|
||||
"integrity": "sha512-ffFV4HGHvCKnkNJFqm42sYztRJE5mLgOj8MpGey1HOatuFhtcwXoBD2m5gca7ZbcyjkIf7lOH5ZdrhlrBf0sGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"express": "^4.18.2",
|
||||
"on-finished": "^2.3.0",
|
||||
"url-value-parser": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prom-client": ">=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "6.11.2",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.11.2.tgz",
|
||||
@@ -7290,13 +7315,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"version": "10.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
|
||||
"integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.5"
|
||||
"brace-expansion": "^5.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
@@ -8154,6 +8179,21 @@
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prom-client": {
|
||||
"version": "15.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz",
|
||||
"integrity": "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==",
|
||||
"deprecated": "prom-client has been replaced by @prometheus-io/client",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.4.0",
|
||||
"tdigest": "^0.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16 || ^18 || >=20"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
@@ -9328,6 +9368,16 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/tdigest": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.3.tgz",
|
||||
"integrity": "sha512-zbRt+lT+/H4fRItHshczHErVCQnitJk8MfMT24MqFJf3YL7SJJPqGIGeuOdvxXxM/AHFzKBl7WoyaYwqO9s3Kw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"bintrees": "1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/tga": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/tga/-/tga-1.0.7.tgz",
|
||||
@@ -9671,16 +9721,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.64.0.tgz",
|
||||
"integrity": "sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ==",
|
||||
"version": "8.67.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.67.0.tgz",
|
||||
"integrity": "sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.64.0",
|
||||
"@typescript-eslint/parser": "8.64.0",
|
||||
"@typescript-eslint/typescript-estree": "8.64.0",
|
||||
"@typescript-eslint/utils": "8.64.0"
|
||||
"@typescript-eslint/eslint-plugin": "8.67.0",
|
||||
"@typescript-eslint/parser": "8.67.0",
|
||||
"@typescript-eslint/typescript-estree": "8.67.0",
|
||||
"@typescript-eslint/utils": "8.67.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -9813,6 +9863,15 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/url-value-parser": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/url-value-parser/-/url-value-parser-2.2.0.tgz",
|
||||
"integrity": "sha512-yIQdxJpgkPamPPAPuGdS7Q548rLhny42tg8d4vyTNzFqvOnwqrgHXvgehT09U7fwrzxi3RxCiXjoNUNnNOlQ8A==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"@aws-sdk/client-s3": "^3.657.0",
|
||||
"@aws-sdk/client-ses": "^3.515.0",
|
||||
"@inquirer/prompts": "^7.2.0",
|
||||
"@pretendonetwork/grpc": "^2.5.4",
|
||||
"@pretendonetwork/grpc": "^2.6.1",
|
||||
"bcrypt": "^5.0.0",
|
||||
"buffer-crc32": "^0.2.13",
|
||||
"colors": "^1.4.0",
|
||||
@@ -40,6 +40,7 @@
|
||||
"ejs": "^3.1.10",
|
||||
"email-validator": "^2.0.4",
|
||||
"express": "^4.17.1",
|
||||
"express-prom-bundle": "^7.0.2",
|
||||
"express-rate-limit": "^6.7.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"got": "^11.8.2",
|
||||
@@ -68,7 +69,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hcaptcha/types": "^1.0.3",
|
||||
"@pretendonetwork/eslint-config": "^0.1.4",
|
||||
"@pretendonetwork/eslint-config": "^0.2.0",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/buffer-crc32": "^0.2.2",
|
||||
"@types/cors": "^2.8.13",
|
||||
|
||||
@@ -38,6 +38,10 @@ export const config: Config = {
|
||||
http: {
|
||||
port: Number(process.env.PN_ACT_CONFIG_HTTP_PORT || '')
|
||||
},
|
||||
metrics: {
|
||||
enabled: process.env.PN_ACT_CONFIG_METRICS_ENABLED === 'true',
|
||||
port: Number(process.env.PN_ACT_CONFIG_METRICS_PORT || '')
|
||||
},
|
||||
mongoose: {
|
||||
connection_string: process.env.PN_ACT_CONFIG_MONGO_CONNECTION_STRING || '',
|
||||
options: mongooseConnectOptions
|
||||
|
||||
75
src/metrics.ts
Normal file
75
src/metrics.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { format } from 'node:util';
|
||||
import { Gauge } from 'prom-client';
|
||||
import expressMetrics from 'express-prom-bundle';
|
||||
import express from 'express';
|
||||
import { LOG_ERROR, LOG_INFO, LOG_SUCCESS } from '@/logger';
|
||||
import { config } from '@/config-manager';
|
||||
import { PNID } from '@/models/pnid';
|
||||
import { NEXToken } from '@/models/nex-token';
|
||||
import type { Express, NextFunction, Request, Response } from 'express';
|
||||
|
||||
export const pnidTotalGauge = new Gauge({
|
||||
name: 'pn_account_pnid_total',
|
||||
help: 'Total number of registered PNIDs',
|
||||
async collect(): Promise<void> {
|
||||
// * Aggregations are faster on large collections
|
||||
const [result] = await PNID.aggregate<{ n: number } | undefined>([
|
||||
{ $match: { deleted: false } },
|
||||
{ $count: 'n' }
|
||||
]);
|
||||
this.set(result?.n ?? 0);
|
||||
}
|
||||
});
|
||||
|
||||
export const nexTokenTotalGauge = new Gauge({
|
||||
name: 'pn_account_nex_token_total',
|
||||
help: 'Total number of NEX tokens',
|
||||
async collect(): Promise<void> {
|
||||
// * Aggregations are faster on large collections
|
||||
const [result] = await NEXToken.aggregate<{ n: number } | undefined>([
|
||||
{ $count: 'n' }
|
||||
]);
|
||||
this.set(result?.n ?? 0);
|
||||
}
|
||||
});
|
||||
|
||||
export function registerMetrics(app: Express): Express {
|
||||
const metrics = express();
|
||||
|
||||
if (config.metrics.enabled) {
|
||||
LOG_INFO('Setting up metrics');
|
||||
app.use(expressMetrics({
|
||||
// * Include full express and nodejs metrics
|
||||
includeMethod: true,
|
||||
includePath: true,
|
||||
urlValueParser: {
|
||||
minBase64Length: 15
|
||||
},
|
||||
promClient: {
|
||||
collectDefaultMetrics: {}
|
||||
},
|
||||
|
||||
// * Keep metrics on a different app (so they aren't exposed)
|
||||
autoregister: false,
|
||||
metricsApp: metrics
|
||||
}));
|
||||
}
|
||||
|
||||
metrics.use((error: Error, req: Request, res: Response, _next: NextFunction) => {
|
||||
LOG_ERROR(`Request failed (metrics): ${format(error)}`);
|
||||
res.sendStatus(500);
|
||||
});
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
export function listenMetrics(metricsApp: Express): void {
|
||||
if (!config.metrics.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const port = config.metrics.port;
|
||||
metricsApp.listen(port, () => {
|
||||
LOG_SUCCESS(`Metrics HTTP server started on port ${port}`);
|
||||
});
|
||||
}
|
||||
@@ -44,4 +44,6 @@ export const DeviceSchema = new Schema<IDevice, DeviceModel, IDeviceMethods>({
|
||||
certificate_hash: String
|
||||
});
|
||||
|
||||
export const Device = model<IDevice, DeviceModel>('Device', DeviceSchema);
|
||||
DeviceSchema.index({ linked_pids: 1 });
|
||||
|
||||
export const Device = model<IDevice, DeviceModel>('Device', DeviceSchema);
|
||||
|
||||
10
src/models/email-update-event.ts
Normal file
10
src/models/email-update-event.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Schema, model } from 'mongoose';
|
||||
import type { IEmailUpdateEvent, EmailUpdateEventModel, IEmailUpdateEventMethods } from '@/types/mongoose/email-update-event';
|
||||
|
||||
export const EmailUpdateEventSchema = new Schema<IEmailUpdateEvent, EmailUpdateEventModel, IEmailUpdateEventMethods>({
|
||||
old: String,
|
||||
new: String,
|
||||
on: Date
|
||||
});
|
||||
|
||||
export const EmailUpdateEvent = model<IEmailUpdateEvent, EmailUpdateEventModel>('EmailUpdateEvent', EmailUpdateEventSchema);
|
||||
@@ -1,5 +1,5 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { Schema, model } from 'mongoose';
|
||||
import { Schema, Types, model } from 'mongoose';
|
||||
import uniqueValidator from 'mongoose-unique-validator';
|
||||
import imagePixels from 'image-pixels';
|
||||
import TGA from 'tga';
|
||||
@@ -16,8 +16,10 @@ import { IndependentServiceToken } from '@/models/independent-service-token';
|
||||
import { NEXToken } from '@/models/nex-token';
|
||||
import { OAuthToken } from '@/models/oauth-token';
|
||||
import { PasswordResetToken } from '@/models/password-reset-token';
|
||||
import { EmailUpdateEventSchema } from '@/models/email-update-event';
|
||||
import type { IPNID, IPNIDMethods, PNIDModel } from '@/types/mongoose/pnid';
|
||||
import type { PNIDPermissionFlag } from '@/types/common/permission-flags';
|
||||
import type { IEmailUpdateEvent } from '@/types/mongoose/email-update-event';
|
||||
|
||||
let stripe: Stripe;
|
||||
|
||||
@@ -82,7 +84,8 @@ const PNIDSchema = new Schema<IPNID, PNIDModel, IPNIDMethods>({
|
||||
reachable: Boolean,
|
||||
validated: Boolean,
|
||||
validated_date: String,
|
||||
id: Number
|
||||
id: Number,
|
||||
history: [EmailUpdateEventSchema]
|
||||
},
|
||||
region: Number,
|
||||
timezone: {
|
||||
@@ -141,6 +144,9 @@ PNIDSchema.index({ 'pid': 1, 'username': 1, 'connections.discord.id': 1 });
|
||||
|
||||
PNIDSchema.plugin(uniqueValidator, { message: '{PATH} already in use.' });
|
||||
|
||||
// * Used by metrics
|
||||
PNIDSchema.index({ deleted: 1 });
|
||||
|
||||
/*
|
||||
According to http://pf2m.com/tools/rank.php Nintendo PID's start at 1,800,000,000 and count down with each account
|
||||
This means the max PID is 1799999999 and hard-limits the number of potential accounts to 1,800,000,000
|
||||
@@ -350,6 +356,7 @@ PNIDSchema.method('scrub', async function scrub() {
|
||||
this.email.reachable = false;
|
||||
this.email.validated = false;
|
||||
this.email.validated_date = '';
|
||||
this.email.history = new Types.DocumentArray<IEmailUpdateEvent>([]);
|
||||
this.email.id = 0;
|
||||
this.region = 0;
|
||||
this.timezone.name = '';
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { format } from 'node:util';
|
||||
import express from 'express';
|
||||
import morgan from 'morgan';
|
||||
import xmlbuilder from 'xmlbuilder';
|
||||
import xmlparser from '@/middleware/xml-parser';
|
||||
import { connect as connectCache } from '@/cache';
|
||||
import { checkMarkedDeletions, connect as connectDatabase } from '@/database';
|
||||
import { connect as connectDatabase } from '@/database';
|
||||
import { startGRPCServer } from '@/services/grpc/server';
|
||||
import { fullUrl, getValueFromHeaders, setupScheduledTasks } from '@/util';
|
||||
import { LOG_INFO, LOG_SUCCESS, LOG_WARN } from '@/logger';
|
||||
@@ -15,8 +16,10 @@ import datastore from '@/services/datastore';
|
||||
import api from '@/services/api';
|
||||
import localcdn from '@/services/local-cdn';
|
||||
import assets from '@/services/assets';
|
||||
import healthz from '@/services/healthz';
|
||||
import { config, disabledFeatures } from '@/config-manager';
|
||||
import { startProvisioner } from '@/provisioning';
|
||||
import { listenMetrics, registerMetrics } from '@/metrics';
|
||||
|
||||
process.title = 'Pretendo - Account';
|
||||
process.on('uncaughtException', (err, origin) => {
|
||||
@@ -29,6 +32,9 @@ process.on('SIGTERM', () => {
|
||||
|
||||
const app = express();
|
||||
|
||||
// * Metrics has to happen first so we can measure the other middleware
|
||||
const metricsApp = registerMetrics(app);
|
||||
|
||||
// * START APPLICATION
|
||||
app.set('view engine', 'ejs');
|
||||
app.set('views', __dirname + '/views');
|
||||
@@ -51,6 +57,7 @@ app.use(nasc);
|
||||
app.use(api);
|
||||
app.use(localcdn);
|
||||
app.use(assets);
|
||||
app.use(healthz);
|
||||
|
||||
if (!disabledFeatures.datastore) {
|
||||
app.use(datastore);
|
||||
@@ -94,7 +101,7 @@ app.use((error: any, request: express.Request, response: express.Response, _next
|
||||
deviceID = 'Unknown';
|
||||
}
|
||||
|
||||
LOG_WARN(`HTTP ${status} at ${url} from ${deviceID}: ${error.message}`);
|
||||
LOG_WARN(`HTTP ${status} at ${url} from ${deviceID}: ${format(error)}`);
|
||||
|
||||
response.status(status).json({
|
||||
app: 'api',
|
||||
@@ -116,13 +123,12 @@ async function main(): Promise<void> {
|
||||
|
||||
startProvisioner();
|
||||
|
||||
await checkMarkedDeletions();
|
||||
|
||||
setupScheduledTasks();
|
||||
|
||||
app.listen(config.http.port, () => {
|
||||
LOG_SUCCESS(`HTTP server started on port ${config.http.port}`);
|
||||
});
|
||||
listenMetrics(metricsApp);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
|
||||
@@ -142,7 +142,7 @@ router.post('/', loginRatelimit, async (request: express.Request, response: expr
|
||||
token_type: TokenType.OAuthRefresh,
|
||||
title_id: BigInt(0),
|
||||
issued: new Date(),
|
||||
expires: new Date(Date.now() + 12 * 3600 * 1000)
|
||||
expires: new Date(Date.now() + 7 * 24 * 3600 * 1000)
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -465,7 +465,7 @@ router.post('/', webRegisterRatelimit, async (request: express.Request, response
|
||||
token_type: TokenType.OAuthRefresh,
|
||||
title_id: BigInt(0),
|
||||
issued: new Date(),
|
||||
expires: new Date(Date.now() + 12 * 3600 * 1000)
|
||||
expires: new Date(Date.now() + 7 * 24 * 3600 * 1000)
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import crypto from 'node:crypto';
|
||||
import express from 'express';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { PasswordResetToken } from '@/models/password-reset-token';
|
||||
import { nintendoPasswordHash } from '@/util';
|
||||
import { nintendoPasswordHash, sendPasswordResetNoticeEmail } from '@/util';
|
||||
import { SystemType } from '@/types/common/system-types';
|
||||
import { TokenType } from '@/types/common/token-types';
|
||||
import { getPNIDByPID } from '@/database';
|
||||
@@ -177,6 +177,8 @@ router.post('/', passwordResetRatelimit, async (request: express.Request, respon
|
||||
await pnid.removeAllTokens();
|
||||
await pnid.save();
|
||||
|
||||
await sendPasswordResetNoticeEmail(pnid);
|
||||
|
||||
response.json({
|
||||
app: 'api',
|
||||
status: 200
|
||||
|
||||
@@ -84,7 +84,7 @@ export async function login(request: LoginRequest): Promise<DeepPartial<LoginRes
|
||||
token_type: TokenType.OAuthRefresh,
|
||||
title_id: BigInt(0),
|
||||
issued: new Date(),
|
||||
expires: new Date(Date.now() + 12 * 3600 * 1000)
|
||||
expires: new Date(Date.now() + 7 * 24 * 3600 * 1000)
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -259,7 +259,7 @@ export async function register(request: RegisterRequest): Promise<DeepPartial<Lo
|
||||
token_type: TokenType.OAuthRefresh,
|
||||
title_id: BigInt(0),
|
||||
issued: new Date(),
|
||||
expires: new Date(Date.now() + 12 * 3600 * 1000)
|
||||
expires: new Date(Date.now() + 7 * 24 * 3600 * 1000)
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import crypto from 'node:crypto';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import { PasswordResetToken } from '@/models/password-reset-token';
|
||||
import { nintendoPasswordHash } from '@/util';
|
||||
import { nintendoPasswordHash, sendPasswordResetNoticeEmail } from '@/util';
|
||||
import { getPNIDByPID } from '@/database';
|
||||
import { SystemType } from '@/types/common/system-types';
|
||||
import { TokenType } from '@/types/common/token-types';
|
||||
@@ -92,5 +92,7 @@ export async function resetPassword(request: ResetPasswordRequest): Promise<Empt
|
||||
await pnid.removeAllTokens();
|
||||
await pnid.save();
|
||||
|
||||
await sendPasswordResetNoticeEmail(pnid);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -8,9 +8,11 @@ const TOKEN_REQUIRED_PATHS = [
|
||||
'/api.v2.ApiService/GetUserData',
|
||||
'/api.v2.ApiService/UpdateUserData',
|
||||
'/api.v2.ApiService/ResetPassword', // * This paths token is not an authentication token, it is a password reset token
|
||||
'/api.v2.ApiService/UpdatePassword',
|
||||
'/api.v2.ApiService/SetDiscordConnectionData',
|
||||
'/api.v2.ApiService/SetStripeConnectionData',
|
||||
'/api.v2.ApiService/RemoveConnection'
|
||||
'/api.v2.ApiService/RemoveConnection',
|
||||
'/api.v2.ApiService/UpdateEmail'
|
||||
];
|
||||
|
||||
export type AuthenticationCallContextExt = {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { config } from '@/config-manager';
|
||||
import type { CallContext } from 'nice-grpc';
|
||||
import type { GetUserDataResponse, DeepPartial } from '@pretendonetwork/grpc/api/v2/get_user_data_rpc';
|
||||
import type { Empty } from '@pretendonetwork/grpc/google/protobuf/empty';
|
||||
import type { AuthenticationCallContextExt } from '@/services/grpc/api/v1/authentication-middleware';
|
||||
import type { AuthenticationCallContextExt } from '@/services/grpc/api/v2/authentication-middleware';
|
||||
|
||||
export async function getUserData(_request: Empty, context: CallContext & AuthenticationCallContextExt): Promise<DeepPartial<GetUserDataResponse>> {
|
||||
// * This is asserted in authentication-middleware, we know this is never null
|
||||
@@ -24,9 +24,11 @@ export async function getUserData(_request: Empty, context: CallContext & Authen
|
||||
birthday: pnid.birthdate,
|
||||
gender: pnid.gender,
|
||||
country: pnid.country,
|
||||
region: pnid.region,
|
||||
timezone: pnid.timezone.name,
|
||||
language: pnid.language,
|
||||
emailAddress: pnid.email.address,
|
||||
emailValidated: pnid.email.validated,
|
||||
connections: {
|
||||
discord: {
|
||||
id: pnid.connections.discord.id
|
||||
|
||||
@@ -2,7 +2,10 @@ import { register } from '@/services/grpc/api/v2/register';
|
||||
import { login } from '@/services/grpc/api/v2/login';
|
||||
import { getUserData } from '@/services/grpc/api/v2/get-user-data';
|
||||
import { updateUserData } from '@/services/grpc/api/v2/update-user-data';
|
||||
import { updateEmail } from '@/services/grpc/api/v2/update-email';
|
||||
import { verifyEmail } from '@/services/grpc/api/v2/verify-email';
|
||||
import { forgotPassword } from '@/services/grpc/api/v2/forgot-password';
|
||||
import { updatePassword } from '@/services/grpc/api/v2/update-password';
|
||||
import { resetPassword } from '@/services/grpc/api/v2/reset-password';
|
||||
import { setDiscordConnectionData } from '@/services/grpc/api/v2/set-discord-connection-data';
|
||||
import { setStripeConnectionData } from '@/services/grpc/api/v2/set-stripe-connection-data';
|
||||
@@ -13,7 +16,10 @@ export const apiServiceImplementationV2 = {
|
||||
login,
|
||||
getUserData,
|
||||
updateUserData,
|
||||
updateEmail,
|
||||
verifyEmail,
|
||||
forgotPassword,
|
||||
updatePassword,
|
||||
resetPassword,
|
||||
setDiscordConnectionData,
|
||||
setStripeConnectionData,
|
||||
|
||||
@@ -84,7 +84,7 @@ export async function login(request: LoginRequest): Promise<DeepPartial<LoginRes
|
||||
token_type: TokenType.OAuthRefresh,
|
||||
title_id: BigInt(0),
|
||||
issued: new Date(),
|
||||
expires: new Date(Date.now() + 12 * 3600 * 1000)
|
||||
expires: new Date(Date.now() + 7 * 24 * 3600 * 1000)
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -259,7 +259,7 @@ export async function register(request: RegisterRequest): Promise<DeepPartial<Lo
|
||||
token_type: TokenType.OAuthRefresh,
|
||||
title_id: BigInt(0),
|
||||
issued: new Date(),
|
||||
expires: new Date(Date.now() + 12 * 3600 * 1000)
|
||||
expires: new Date(Date.now() + 7 * 24 * 3600 * 1000)
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import crypto from 'node:crypto';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import { PasswordResetToken } from '@/models/password-reset-token';
|
||||
import { nintendoPasswordHash } from '@/util';
|
||||
import { nintendoPasswordHash, sendPasswordResetNoticeEmail } from '@/util';
|
||||
import { getPNIDByPID } from '@/database';
|
||||
import { SystemType } from '@/types/common/system-types';
|
||||
import { TokenType } from '@/types/common/token-types';
|
||||
@@ -91,5 +91,7 @@ export async function resetPassword(request: ResetPasswordRequest): Promise<Rese
|
||||
await pnid.removeAllTokens();
|
||||
await pnid.save();
|
||||
|
||||
await sendPasswordResetNoticeEmail(pnid);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import type { CallContext } from 'nice-grpc';
|
||||
import type { SetDiscordConnectionDataRequest, SetDiscordConnectionDataResponse } from '@pretendonetwork/grpc/api/v2/set_discord_connection_data_rpc';
|
||||
import type { AuthenticationCallContextExt } from '@/services/grpc/api/v1/authentication-middleware';
|
||||
import type { AuthenticationCallContextExt } from '@/services/grpc/api/v2/authentication-middleware';
|
||||
|
||||
export async function setDiscordConnectionData(request: SetDiscordConnectionDataRequest, context: CallContext & AuthenticationCallContextExt): Promise<SetDiscordConnectionDataResponse> {
|
||||
// * This is asserted in authentication-middleware, we know this is never null
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Status, ServerError } from 'nice-grpc';
|
||||
import { PNID } from '@/models/pnid';
|
||||
import type { CallContext } from 'nice-grpc';
|
||||
import type { SetStripeConnectionDataRequest, SetStripeConnectionDataResponse } from '@pretendonetwork/grpc/api/v2/set_stripe_connection_data_rpc';
|
||||
import type { AuthenticationCallContextExt } from '@/services/grpc/api/v1/authentication-middleware';
|
||||
import type { AuthenticationCallContextExt } from '@/services/grpc/api/v2/authentication-middleware';
|
||||
|
||||
type StripeMongoUpdateScheme = {
|
||||
'access_level'?: number;
|
||||
|
||||
52
src/services/grpc/api/v2/update-email.ts
Normal file
52
src/services/grpc/api/v2/update-email.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import crypto from 'node:crypto';
|
||||
import validator from 'validator';
|
||||
import { ServerError, Status } from 'nice-grpc';
|
||||
import { sendConfirmationEmail } from '@/util';
|
||||
import type { CallContext } from 'nice-grpc';
|
||||
import type {
|
||||
UpdateEmailRequest,
|
||||
UpdateEmailResponse
|
||||
} from '@pretendonetwork/grpc/api/v2/update_email_rpc';
|
||||
import type { AuthenticationCallContextExt } from '@/services/grpc/api/v2/authentication-middleware';
|
||||
|
||||
export async function updateEmail(
|
||||
request: UpdateEmailRequest,
|
||||
context: CallContext & AuthenticationCallContextExt
|
||||
): Promise<UpdateEmailResponse> {
|
||||
// * This is asserted in authentication-middleware, we know this is never null
|
||||
const pnid = context.pnid!;
|
||||
|
||||
const newEmail = request.email?.trim().toLowerCase();
|
||||
|
||||
if (!newEmail) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Must provide new email address');
|
||||
}
|
||||
|
||||
if (!validator.isEmail(newEmail)) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid email address');
|
||||
}
|
||||
|
||||
/* We allow the new email to equal the old email, and treat this as a verification email resend request
|
||||
|
||||
if (newEmail === pnid.email.address) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'New email address must differ from current');
|
||||
}
|
||||
*/
|
||||
|
||||
const emailUpdateEvent = { old: pnid.email.address, new: newEmail, on: new Date() };
|
||||
pnid.email.history.unshift(emailUpdateEvent);
|
||||
|
||||
pnid.email.address = newEmail;
|
||||
pnid.email.reachable = false;
|
||||
pnid.email.validated = false;
|
||||
pnid.email.validated_date = '';
|
||||
pnid.email.id = crypto.randomBytes(4).readUInt32LE();
|
||||
|
||||
await pnid.generateEmailValidationCode();
|
||||
await pnid.generateEmailValidationToken();
|
||||
await sendConfirmationEmail(pnid);
|
||||
|
||||
await pnid.save();
|
||||
|
||||
return {};
|
||||
}
|
||||
69
src/services/grpc/api/v2/update-password.ts
Normal file
69
src/services/grpc/api/v2/update-password.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import bcrypt from 'bcrypt';
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import { nintendoPasswordHash, sendPasswordResetNoticeEmail } from '@/util';
|
||||
import type { CallContext } from 'nice-grpc';
|
||||
import type { UpdatePasswordRequest, UpdatePasswordResponse } from '@pretendonetwork/grpc/api/v2/update_password_rpc';
|
||||
import type { AuthenticationCallContextExt } from '@/services/grpc/api/v2/authentication-middleware';
|
||||
|
||||
// * This sucks
|
||||
const PASSWORD_WORD_OR_NUMBER_REGEX = /(?=.*[a-zA-Z])(?=.*\d).*/;
|
||||
const PASSWORD_WORD_OR_PUNCTUATION_REGEX = /(?=.*[a-zA-Z])(?=.*[_\-.]).*/;
|
||||
const PASSWORD_NUMBER_OR_PUNCTUATION_REGEX = /(?=.*\d)(?=.*[_\-.]).*/;
|
||||
const PASSWORD_REPEATED_CHARACTER_REGEX = /(.)\1\1/;
|
||||
|
||||
export async function updatePassword(request: UpdatePasswordRequest,
|
||||
context: CallContext & AuthenticationCallContextExt
|
||||
): Promise<UpdatePasswordResponse> {
|
||||
// * This is asserted in authentication-middleware, we know this is never null
|
||||
const pnid = context.pnid!;
|
||||
|
||||
const oldPassword = request.oldPassword.trim();
|
||||
const newPassword = request.newPassword.trim();
|
||||
const newPasswordConfirm = request.newPasswordConfirm.trim();
|
||||
|
||||
const hashedOldPassword = nintendoPasswordHash(oldPassword, pnid.pid);
|
||||
|
||||
if (!bcrypt.compareSync(hashedOldPassword, pnid.password)) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Password is incorrect');
|
||||
}
|
||||
|
||||
if (!newPassword) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Must enter a new password');
|
||||
}
|
||||
|
||||
if (newPassword !== newPasswordConfirm) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Passwords do not match');
|
||||
}
|
||||
|
||||
if (newPassword === oldPassword) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'New password must not equal current password');
|
||||
}
|
||||
|
||||
if (newPassword.length < 6 || newPassword.length > 16) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Password must be between 6 and 16 characters long');
|
||||
}
|
||||
|
||||
if (newPassword.toLowerCase() === pnid.username.toLowerCase()) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Password cannot be the same as username');
|
||||
}
|
||||
|
||||
if (!PASSWORD_WORD_OR_NUMBER_REGEX.test(newPassword) && !PASSWORD_WORD_OR_PUNCTUATION_REGEX.test(newPassword) && !PASSWORD_NUMBER_OR_PUNCTUATION_REGEX.test(newPassword)) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Password must have combination of letters, numbers, and/or punctuation characters');
|
||||
}
|
||||
|
||||
if (PASSWORD_REPEATED_CHARACTER_REGEX.test(newPassword)) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Password may not have 3 repeating characters');
|
||||
}
|
||||
|
||||
const primaryPasswordHash = nintendoPasswordHash(newPassword, pnid.pid);
|
||||
const passwordHash = await bcrypt.hash(primaryPasswordHash, 10);
|
||||
|
||||
pnid.password = passwordHash;
|
||||
|
||||
await pnid.removeAllTokens();
|
||||
await pnid.save();
|
||||
|
||||
await sendPasswordResetNoticeEmail(pnid);
|
||||
|
||||
return {};
|
||||
}
|
||||
@@ -1,14 +1,144 @@
|
||||
import { ServerError, Status } from 'nice-grpc';
|
||||
import Mii from 'mii-js';
|
||||
import { isValidBirthday } from '@/util';
|
||||
import { config } from '@/config-manager';
|
||||
import timezones from '@/services/nnas/timezones.json';
|
||||
import regions from '@/services/nnas/regions.json';
|
||||
import type { CallContext } from 'nice-grpc';
|
||||
import type { UpdateUserDataRequest, DeepPartial } from '@pretendonetwork/grpc/api/v2/update_user_data_rpc';
|
||||
import type {
|
||||
UpdateUserDataRequest,
|
||||
DeepPartial
|
||||
} from '@pretendonetwork/grpc/api/v2/update_user_data_rpc';
|
||||
import type { GetUserDataResponse } from '@pretendonetwork/grpc/api/v2/get_user_data_rpc';
|
||||
import type { AuthenticationCallContextExt } from '@/services/grpc/api/v1/authentication-middleware';
|
||||
import type { AuthenticationCallContextExt } from '@/services/grpc/api/v2/authentication-middleware';
|
||||
|
||||
export async function updateUserData(_request: UpdateUserDataRequest, context: CallContext & AuthenticationCallContextExt): Promise<DeepPartial<GetUserDataResponse>> {
|
||||
export async function updateUserData(
|
||||
request: UpdateUserDataRequest,
|
||||
context: CallContext & AuthenticationCallContextExt
|
||||
): Promise<DeepPartial<GetUserDataResponse>> {
|
||||
// * This is asserted in authentication-middleware, we know this is never null
|
||||
const pnid = context.pnid!;
|
||||
|
||||
// TODO - STUBBED, DO SOMETHING HERE
|
||||
const serverAccessLevel = request.serverAccessLevel?.trim();
|
||||
const mii = request?.mii?.trim();
|
||||
const birthday = request.birthday?.trim();
|
||||
const gender = request.gender?.trim();
|
||||
const region = request.region;
|
||||
const timezone = request.timezone?.trim();
|
||||
|
||||
/* TODO: implement these if/when needed */
|
||||
// const language = request.language?.trim();
|
||||
// const marketingFlag = request.marketingFlag;
|
||||
|
||||
if (serverAccessLevel) {
|
||||
if (!['prod', 'test', 'dev'].includes(serverAccessLevel)) {
|
||||
throw new ServerError(
|
||||
Status.INVALID_ARGUMENT,
|
||||
'Must be one of: prod, test, dev'
|
||||
);
|
||||
}
|
||||
|
||||
if (serverAccessLevel === 'prod') {
|
||||
if (pnid.access_level < 0) {
|
||||
throw new ServerError(Status.PERMISSION_DENIED, 'Banned');
|
||||
}
|
||||
|
||||
pnid.server_access_level = serverAccessLevel;
|
||||
}
|
||||
|
||||
if (serverAccessLevel === 'test') {
|
||||
if (pnid.access_level < 1) {
|
||||
throw new ServerError(
|
||||
Status.INVALID_ARGUMENT,
|
||||
'Do not have permission to enter this environment'
|
||||
);
|
||||
}
|
||||
|
||||
pnid.server_access_level = serverAccessLevel;
|
||||
}
|
||||
|
||||
if (serverAccessLevel === 'dev') {
|
||||
if (pnid.access_level < 3) {
|
||||
throw new ServerError(
|
||||
Status.INVALID_ARGUMENT,
|
||||
'Do not have permission to enter this environment'
|
||||
);
|
||||
}
|
||||
|
||||
pnid.server_access_level = serverAccessLevel;
|
||||
}
|
||||
}
|
||||
|
||||
if (birthday) {
|
||||
if (!isValidBirthday(birthday)) {
|
||||
throw new ServerError(
|
||||
Status.INVALID_ARGUMENT,
|
||||
'Must be a valid date formatted as: YYYY-MM-DD'
|
||||
);
|
||||
}
|
||||
|
||||
pnid.birthdate = birthday;
|
||||
}
|
||||
|
||||
if (gender) {
|
||||
if (!['M', 'F'].includes(gender)) {
|
||||
throw new ServerError(
|
||||
Status.INVALID_ARGUMENT,
|
||||
'Must be one of: F, M'
|
||||
);
|
||||
}
|
||||
|
||||
pnid.gender = gender;
|
||||
}
|
||||
|
||||
if (region) {
|
||||
const countryObj = regions.find(c => c.id === ((region >>> 24) & 0xFF));
|
||||
const regionObj = countryObj?.regions.find(r => r.id === region);
|
||||
|
||||
if (!countryObj || !regionObj) {
|
||||
throw new ServerError(
|
||||
Status.INVALID_ARGUMENT,
|
||||
'Invalid region'
|
||||
);
|
||||
}
|
||||
|
||||
pnid.country = countryObj.iso_code;
|
||||
pnid.region = regionObj.id;
|
||||
}
|
||||
|
||||
if (timezone) {
|
||||
const pnidCountryTimezones =
|
||||
timezones[pnid.country as keyof typeof timezones];
|
||||
// using japanese because some timezones are only available in that locale
|
||||
const newTimezone = pnidCountryTimezones.ja.find(
|
||||
t => t.area === timezone
|
||||
);
|
||||
|
||||
if (!newTimezone) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid timezone');
|
||||
}
|
||||
|
||||
pnid.timezone.name = newTimezone.area;
|
||||
pnid.timezone.offset = Number(newTimezone.utc_offset);
|
||||
}
|
||||
|
||||
if (mii) {
|
||||
try {
|
||||
const parsedMii = new Mii(Buffer.from(mii, 'base64'));
|
||||
|
||||
parsedMii.validate();
|
||||
|
||||
await pnid.updateMii({
|
||||
name: parsedMii.miiName,
|
||||
primary: 'Y',
|
||||
data: parsedMii.encode().toString('base64')
|
||||
});
|
||||
} catch {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid mii data');
|
||||
}
|
||||
}
|
||||
|
||||
await pnid.save();
|
||||
|
||||
return {
|
||||
deleted: pnid.deleted || pnid.marked_for_deletion,
|
||||
@@ -26,9 +156,11 @@ export async function updateUserData(_request: UpdateUserDataRequest, context: C
|
||||
birthday: pnid.birthdate,
|
||||
gender: pnid.gender,
|
||||
country: pnid.country,
|
||||
region: pnid.region,
|
||||
timezone: pnid.timezone.name,
|
||||
language: pnid.language,
|
||||
emailAddress: pnid.email.address,
|
||||
emailValidated: pnid.email.validated,
|
||||
connections: {
|
||||
discord: {
|
||||
id: pnid.connections.discord.id
|
||||
@@ -39,7 +171,9 @@ export async function updateUserData(_request: UpdateUserDataRequest, context: C
|
||||
priceId: pnid.connections.stripe.price_id,
|
||||
tierLevel: pnid.connections.stripe.tier_level,
|
||||
tierName: pnid.connections.stripe.tier_name,
|
||||
latestWebhookTimestamp: BigInt(pnid.connections.stripe.latest_webhook_timestamp ?? 0)
|
||||
latestWebhookTimestamp: BigInt(
|
||||
pnid.connections.stripe.latest_webhook_timestamp ?? 0
|
||||
)
|
||||
}
|
||||
},
|
||||
marketingFlag: pnid.flags.marketing
|
||||
|
||||
39
src/services/grpc/api/v2/verify-email.ts
Normal file
39
src/services/grpc/api/v2/verify-email.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import moment from 'moment';
|
||||
import { ServerError, Status } from 'nice-grpc';
|
||||
import { sendEmailConfirmedEmail } from '@/util';
|
||||
import { PNID } from '@/models/pnid';
|
||||
import type {
|
||||
VerifyEmailRequest,
|
||||
VerifyEmailResponse
|
||||
} from '@pretendonetwork/grpc/api/v2/verify_email_rpc';
|
||||
|
||||
export async function verifyEmail(
|
||||
request: VerifyEmailRequest
|
||||
): Promise<VerifyEmailResponse> {
|
||||
const token = request?.token?.trim();
|
||||
|
||||
if (!token) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Missing email token');
|
||||
}
|
||||
|
||||
const pnid = await PNID.findOne({
|
||||
'identification.email_token': token
|
||||
});
|
||||
|
||||
if (!pnid) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid email token');
|
||||
}
|
||||
|
||||
if (!pnid.email.validated) {
|
||||
const validatedDate = moment().format('YYYY-MM-DDTHH:MM:SS');
|
||||
|
||||
pnid.email.reachable = true;
|
||||
pnid.email.validated = true;
|
||||
pnid.email.validated_date = validatedDate;
|
||||
|
||||
await pnid.save();
|
||||
await sendEmailConfirmedEmail(pnid);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
9
src/services/healthz.ts
Normal file
9
src/services/healthz.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import express from 'express';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/healthz', (req, res) => {
|
||||
res.status(200).send('OK');
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -220,9 +220,13 @@ router.post('/update', async function (request: express.Request, response: expre
|
||||
pnid.server_access_level = environment;
|
||||
}
|
||||
|
||||
if (person.data.email.trim().toLowerCase() !== pnid.email.address) {
|
||||
const newEmail = person.data.email.trim().toLowerCase();
|
||||
|
||||
if (newEmail !== pnid.email.address) {
|
||||
pnid.email.history.unshift({ old: pnid.email.address, new: newEmail, on: new Date() });
|
||||
|
||||
// TODO - Better email check
|
||||
pnid.email.address = person.data.email.trim().toLowerCase();
|
||||
pnid.email.address = newEmail;
|
||||
pnid.email.reachable = false;
|
||||
pnid.email.validated = false;
|
||||
pnid.email.validated_date = '';
|
||||
|
||||
@@ -6,7 +6,7 @@ import moment from 'moment';
|
||||
import deviceCertificateMiddleware from '@/middleware/device-certificate';
|
||||
import { deviceRatelimit } from '@/middleware/ratelimit';
|
||||
import { connection as databaseConnection, doesPNIDExist, getPNIDProfileJSONByPID } from '@/database';
|
||||
import { getAgeFromDate, getValueFromHeaders, nintendoPasswordHash, sendConfirmationEmail, sendPNIDDeletedEmail } from '@/util';
|
||||
import { getAgeFromDate, getValueFromHeaders, nintendoPasswordHash, sendConfirmationEmail, sendPNIDDeletedEmail, sendPasswordResetNoticeEmail } from '@/util';
|
||||
import IP2LocationManager from '@/ip2location';
|
||||
import { PNID } from '@/models/pnid';
|
||||
import { NEXAccount } from '@/models/nex-account';
|
||||
@@ -652,6 +652,7 @@ router.put('/@me', async (request: express.Request, response: express.Response):
|
||||
pnid.password = passwordHash;
|
||||
|
||||
await pnid.removeAllTokens();
|
||||
await sendPasswordResetNoticeEmail(pnid);
|
||||
}
|
||||
|
||||
pnid.gender = gender;
|
||||
@@ -735,6 +736,8 @@ router.put('/@me/emails/@primary', async (request: express.Request, response: ex
|
||||
return;
|
||||
}
|
||||
|
||||
pnid.email.history.unshift({ old: pnid.email.address, new: email.address.toLowerCase(), on: new Date() });
|
||||
|
||||
// TODO - Better email check
|
||||
pnid.email.address = email.address.toLowerCase();
|
||||
pnid.email.reachable = false;
|
||||
|
||||
@@ -9,6 +9,10 @@ export interface Config {
|
||||
http: {
|
||||
port: number;
|
||||
};
|
||||
metrics: {
|
||||
enabled: boolean;
|
||||
port: number;
|
||||
};
|
||||
mongoose: {
|
||||
connection_string: string;
|
||||
options: mongoose.ConnectOptions;
|
||||
|
||||
15
src/types/mongoose/email-update-event.ts
Normal file
15
src/types/mongoose/email-update-event.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { Model, HydratedDocument } from 'mongoose';
|
||||
|
||||
export interface IEmailUpdateEvent {
|
||||
new: string;
|
||||
old: string;
|
||||
on: Date;
|
||||
}
|
||||
|
||||
export interface IEmailUpdateEventMethods {}
|
||||
|
||||
interface IEmailUpdateEventQueryHelpers {}
|
||||
|
||||
export interface EmailUpdateEventModel extends Model<IEmailUpdateEvent, IEmailUpdateEventQueryHelpers, IEmailUpdateEventMethods> {}
|
||||
|
||||
export type HydratedEmailUpdateDocument = HydratedDocument<IEmailUpdateEvent, IEmailUpdateEventMethods>;
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Model, Types, HydratedDocument } from 'mongoose';
|
||||
import type { IDevice } from '@/types/mongoose/device';
|
||||
import type { IEmailUpdateEvent } from '@/types/mongoose/email-update-event';
|
||||
import type { PNIDPermissionFlag } from '@/types/common/permission-flags';
|
||||
|
||||
export interface IPNID {
|
||||
@@ -27,13 +28,12 @@ export interface IPNID {
|
||||
validated: boolean;
|
||||
validated_date: string;
|
||||
id: number;
|
||||
history: Types.DocumentArray<IEmailUpdateEvent>;
|
||||
};
|
||||
region: number;
|
||||
timezone: {
|
||||
name: string;
|
||||
offset: number;
|
||||
marketing: boolean;
|
||||
off_device: boolean;
|
||||
};
|
||||
mii: {
|
||||
name: string;
|
||||
|
||||
67
src/util.ts
67
src/util.ts
@@ -9,7 +9,7 @@ import { SystemType } from '@/types/common/system-types';
|
||||
import { TokenType } from '@/types/common/token-types';
|
||||
import { config, disabledFeatures } from '@/config-manager';
|
||||
import { PasswordResetToken } from '@/models/password-reset-token';
|
||||
import { LOG_ERROR } from '@/logger';
|
||||
import { LOG_ERROR, LOG_SUCCESS } from '@/logger';
|
||||
import type { IncomingHttpHeaders } from 'node:http';
|
||||
import type { ParsedQs } from 'qs';
|
||||
import type mongoose from 'mongoose';
|
||||
@@ -78,7 +78,7 @@ export function createServiceToken(server: HydratedServerDocument, options: Serv
|
||||
|
||||
export function fullUrl(request: express.Request): string {
|
||||
const protocol = request.protocol;
|
||||
const host = request.host;
|
||||
const host = request.hostname;
|
||||
const opath = request.originalUrl;
|
||||
|
||||
return `${protocol}://${host}${opath}`;
|
||||
@@ -129,8 +129,8 @@ export function nascError(errorCode: string): URLSearchParams {
|
||||
export async function sendConfirmationEmail(pnid: mongoose.HydratedDocument<IPNID, IPNIDMethods>): Promise<void> {
|
||||
const email = new CreateEmail()
|
||||
.addHeader('Hello {{pnid}}!', { pnid: pnid.username })
|
||||
.addParagraph('Your <b>Pretendo Network ID</b> activation is almost complete. Please click the link below to confirm your e-mail address and complete the activation process.')
|
||||
.addButton('Confirm email address', `https://api.pretendo.cc/v1/email/verify?token=${pnid.identification.email_token}`)
|
||||
.addParagraph('Please click the link below to confirm your e-mail address.')
|
||||
.addButton('Confirm email address', `${config.website_base}/account/verify-email?token=${pnid.identification.email_token}`)
|
||||
.addParagraph('You may also enter the following 6-digit code on your console:')
|
||||
.addButton(pnid.identification.email_code, '', false)
|
||||
.addParagraph('We hope you have fun using our services!');
|
||||
@@ -145,18 +145,51 @@ export async function sendConfirmationEmail(pnid: mongoose.HydratedDocument<IPNI
|
||||
}
|
||||
|
||||
export async function sendEmailConfirmedEmail(pnid: mongoose.HydratedDocument<IPNID, IPNIDMethods>): Promise<void> {
|
||||
const email = new CreateEmail()
|
||||
const noticeEmail = new CreateEmail()
|
||||
.addHeader('Dear {{pnid}}!', { pnid: pnid.username })
|
||||
.addParagraph('Your email address has been confirmed.')
|
||||
.addParagraph('We hope you have fun on Pretendo Network!');
|
||||
|
||||
const options = {
|
||||
const noticeOptions = {
|
||||
to: pnid.email.address,
|
||||
subject: '[Pretendo Network] Email address confirmed',
|
||||
email
|
||||
email: noticeEmail
|
||||
};
|
||||
|
||||
await sendMail(options);
|
||||
await sendMail(noticeOptions);
|
||||
|
||||
if (pnid.email.history.length > 0) {
|
||||
// we can just grab the latest email update event, since it's guaranteed to be the relevant one (or the tokens wouldn't be valid)
|
||||
const emailUpdateEvent = pnid.email.history[0];
|
||||
|
||||
const warningEmail = new CreateEmail()
|
||||
.addHeader('Dear {{pnid}},', { pnid: pnid.username })
|
||||
.addParagraph('your email address has been changed.')
|
||||
.addParagraph('If this wasn\'t you, contact [support@pretendo.network](mailto:support@pretendo.network).');
|
||||
|
||||
const warningOptions = {
|
||||
to: emailUpdateEvent.old,
|
||||
subject: '[Pretendo Network] Email address changed',
|
||||
email: warningEmail
|
||||
};
|
||||
|
||||
await sendMail(warningOptions);
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendPasswordResetNoticeEmail(pnid: mongoose.HydratedDocument<IPNID, IPNIDMethods>): Promise<void> {
|
||||
const noticeEmail = new CreateEmail()
|
||||
.addHeader('Dear {{pnid}},', { pnid: pnid.username })
|
||||
.addParagraph('your password has been changed.')
|
||||
.addParagraph('If this wasn\'t you, contact [support@pretendo.network](mailto:support@pretendo.network).');
|
||||
|
||||
const noticeOptions = {
|
||||
to: pnid.email.address,
|
||||
subject: '[Pretendo Network] Password changed',
|
||||
email: noticeEmail
|
||||
};
|
||||
|
||||
await sendMail(noticeOptions);
|
||||
}
|
||||
|
||||
export async function sendEmailConfirmedParentalControlsEmail(pnid: mongoose.HydratedDocument<IPNID, IPNIDMethods>): Promise<void> {
|
||||
@@ -191,7 +224,7 @@ export async function sendForgotPasswordEmail(pnid: mongoose.HydratedDocument<IP
|
||||
const email = new CreateEmail()
|
||||
.addHeader('Dear {{pnid}},', { pnid: pnid.username })
|
||||
.addParagraph('a password reset has been requested from this account.')
|
||||
.addParagraph('If you did not request the password reset, please ignore this email. If you did request this password reset, please click the link below to reset your password.')
|
||||
.addParagraph('If you did not request the password reset, please ignore this email. Otherwise, please click the link below to reset your password.')
|
||||
.addButton('Reset password', `${config.website_base}/account/reset-password?token=${encodeURIComponent(token)}`);
|
||||
|
||||
const mailerOptions = {
|
||||
@@ -299,6 +332,20 @@ export function isValidBirthday(dateString: string): boolean {
|
||||
const month = parseInt(parts[1], 10);
|
||||
const day = parseInt(parts[2], 10);
|
||||
|
||||
const today = new Date();
|
||||
const currentYear = today.getFullYear();
|
||||
const currentMonth = today.getMonth() + 1;
|
||||
const currentDay = today.getDate();
|
||||
|
||||
// Check that date isn't in the future
|
||||
if (currentYear < year && currentMonth < month && currentDay < day) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (year < 1900) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const date = new Date(year, month - 1, day);
|
||||
|
||||
return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day;
|
||||
@@ -347,5 +394,5 @@ function scheduledTask(schedule: string, name: string, fn: () => void | Promise<
|
||||
start: true
|
||||
});
|
||||
|
||||
LOG_ERROR(`Added schedule ${name} for ${schedule}`);
|
||||
LOG_SUCCESS(`Added schedule ${name} for ${schedule}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user