From eac5ec974223906272955badd8e734b45b4559a7 Mon Sep 17 00:00:00 2001 From: Jonathan Barrow Date: Tue, 17 Jun 2025 16:55:38 -0400 Subject: [PATCH] feat: implement v2 grpc servers adds support for the new v2 gRPC servers while maintaining the old ones for legacy clients --- .gitignore | 3 +- package-lock.json | 32 ++- package.json | 2 +- src/services/grpc/account/implementation.ts | 13 - .../account/{ => v1}/api-key-middleware.ts | 0 .../v1/exchange-token-for-user-data.ts | 62 ++++ .../grpc/account/{ => v1}/get-nex-data.ts | 0 .../grpc/account/{ => v1}/get-nex-password.ts | 0 src/services/grpc/account/v1/get-user-data.ts | 60 ++++ .../grpc/account/v1/implementation.ts | 13 + .../{ => v1}/update-pnid-permissions.ts | 2 +- .../grpc/account/v2/api-key-middleware.ts | 16 ++ .../{ => v2}/exchange-token-for-user-data.ts | 6 +- src/services/grpc/account/v2/get-nex-data.ts | 23 ++ .../grpc/account/v2/get-nex-password.ts | 18 ++ .../grpc/account/{ => v2}/get-user-data.ts | 4 +- .../grpc/account/v2/implementation.ts | 13 + .../account/v2/update-pnid-permissions.ts | 159 +++++++++++ src/services/grpc/api/implementation.ts | 19 -- .../grpc/api/{ => v1}/api-key-middleware.ts | 0 .../api/{ => v1}/authentication-middleware.ts | 0 .../grpc/api/{ => v1}/forgot-password.ts | 2 +- .../grpc/api/{ => v1}/get-user-data.ts | 4 +- src/services/grpc/api/v1/implementation.ts | 19 ++ src/services/grpc/api/{ => v1}/login.ts | 0 src/services/grpc/api/{ => v1}/register.ts | 0 .../grpc/api/{ => v1}/reset-password.ts | 2 +- .../{ => v1}/set-discord-connection-data.ts | 4 +- .../{ => v1}/set-stripe-connection-data.ts | 14 +- .../grpc/api/{ => v1}/update-user-data.ts | 2 +- .../grpc/api/v2/api-key-middleware.ts | 16 ++ .../grpc/api/v2/authentication-middleware.ts | 56 ++++ src/services/grpc/api/v2/forgot-password.ts | 29 ++ src/services/grpc/api/v2/get-user-data.ts | 45 +++ src/services/grpc/api/v2/implementation.ts | 19 ++ src/services/grpc/api/v2/login.ts | 89 ++++++ src/services/grpc/api/v2/register.ts | 264 ++++++++++++++++++ src/services/grpc/api/v2/reset-password.ts | 77 +++++ .../api/v2/set-discord-connection-data.ts | 25 ++ .../grpc/api/v2/set-stripe-connection-data.ts | 80 ++++++ src/services/grpc/api/v2/update-user-data.ts | 47 ++++ src/services/grpc/server.ts | 28 +- 42 files changed, 1194 insertions(+), 73 deletions(-) delete mode 100644 src/services/grpc/account/implementation.ts rename src/services/grpc/account/{ => v1}/api-key-middleware.ts (100%) create mode 100644 src/services/grpc/account/v1/exchange-token-for-user-data.ts rename src/services/grpc/account/{ => v1}/get-nex-data.ts (100%) rename src/services/grpc/account/{ => v1}/get-nex-password.ts (100%) create mode 100644 src/services/grpc/account/v1/get-user-data.ts create mode 100644 src/services/grpc/account/v1/implementation.ts rename src/services/grpc/account/{ => v1}/update-pnid-permissions.ts (98%) create mode 100644 src/services/grpc/account/v2/api-key-middleware.ts rename src/services/grpc/account/{ => v2}/exchange-token-for-user-data.ts (96%) create mode 100644 src/services/grpc/account/v2/get-nex-data.ts create mode 100644 src/services/grpc/account/v2/get-nex-password.ts rename src/services/grpc/account/{ => v2}/get-user-data.ts (97%) create mode 100644 src/services/grpc/account/v2/implementation.ts create mode 100644 src/services/grpc/account/v2/update-pnid-permissions.ts delete mode 100644 src/services/grpc/api/implementation.ts rename src/services/grpc/api/{ => v1}/api-key-middleware.ts (100%) rename src/services/grpc/api/{ => v1}/authentication-middleware.ts (100%) rename src/services/grpc/api/{ => v1}/forgot-password.ts (91%) rename src/services/grpc/api/{ => v1}/get-user-data.ts (93%) create mode 100644 src/services/grpc/api/v1/implementation.ts rename src/services/grpc/api/{ => v1}/login.ts (100%) rename src/services/grpc/api/{ => v1}/register.ts (100%) rename src/services/grpc/api/{ => v1}/reset-password.ts (97%) rename src/services/grpc/api/{ => v1}/set-discord-connection-data.ts (87%) rename src/services/grpc/api/{ => v1}/set-stripe-connection-data.ts (86%) rename src/services/grpc/api/{ => v1}/update-user-data.ts (97%) create mode 100644 src/services/grpc/api/v2/api-key-middleware.ts create mode 100644 src/services/grpc/api/v2/authentication-middleware.ts create mode 100644 src/services/grpc/api/v2/forgot-password.ts create mode 100644 src/services/grpc/api/v2/get-user-data.ts create mode 100644 src/services/grpc/api/v2/implementation.ts create mode 100644 src/services/grpc/api/v2/login.ts create mode 100644 src/services/grpc/api/v2/register.ts create mode 100644 src/services/grpc/api/v2/reset-password.ts create mode 100644 src/services/grpc/api/v2/set-discord-connection-data.ts create mode 100644 src/services/grpc/api/v2/set-stripe-connection-data.ts create mode 100644 src/services/grpc/api/v2/update-user-data.ts diff --git a/.gitignore b/.gitignore index bd3afc3..396101a 100644 --- a/.gitignore +++ b/.gitignore @@ -61,4 +61,5 @@ typings/ config.json certs cdn -dist \ No newline at end of file +dist +.DS_Store \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e75c9dc..d7955b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": "^1.0.5", + "@pretendonetwork/grpc": "2.1.1", "bcrypt": "^5.0.0", "buffer-crc32": "^0.2.13", "colors": "^1.4.0", @@ -2915,6 +2915,12 @@ "version": "2.7.0", "license": "0BSD" }, + "node_modules/@bufbuild/protobuf": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.5.2.tgz", + "integrity": "sha512-foZ7qr0IsUBjzWIq+SuBLfdQCpJ1j8cTuNNT4owngTHoN5KsJb8L9t65fzz7SCeSWzescoOil/0ldqiL041ABg==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, "node_modules/@eslint-community/eslint-plugin-eslint-comments": { "version": "4.4.1", "dev": true, @@ -3565,11 +3571,27 @@ } }, "node_modules/@pretendonetwork/grpc": { - "version": "1.0.5", - "license": "ISC", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@pretendonetwork/grpc/-/grpc-2.1.1.tgz", + "integrity": "sha512-SEfeG9zqRh8Iaij7cPSpnC3s5bQ5oJhCXO0tKIMpirO6aQIGit8sNrABDfGihSXMVK9q+R8gHzODapIQQV4V6Q==", + "license": "AGPL-3.0-only", "dependencies": { - "long": "^5.2.1", - "protobufjs": "^7.2.3" + "@bufbuild/protobuf": "^2.2.2", + "nice-grpc-common": "^2.0.2", + "typescript": "^5.7.2" + } + }, + "node_modules/@pretendonetwork/grpc/node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" } }, "node_modules/@protobufjs/aspromise": { diff --git a/package.json b/package.json index 6dd155d..4054b90 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "@aws-sdk/client-s3": "^3.657.0", "@aws-sdk/client-ses": "^3.515.0", "@inquirer/prompts": "^7.2.0", - "@pretendonetwork/grpc": "^1.0.5", + "@pretendonetwork/grpc": "2.1.1", "bcrypt": "^5.0.0", "buffer-crc32": "^0.2.13", "colors": "^1.4.0", diff --git a/src/services/grpc/account/implementation.ts b/src/services/grpc/account/implementation.ts deleted file mode 100644 index 37a9dd4..0000000 --- a/src/services/grpc/account/implementation.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { getUserData } from '@/services/grpc/account/get-user-data'; -import { getNEXPassword } from '@/services/grpc/account/get-nex-password'; -import { getNEXData } from '@/services/grpc/account/get-nex-data'; -import { updatePNIDPermissions } from '@/services/grpc/account/update-pnid-permissions'; -import { exchangeTokenForUserData } from '@/services/grpc/account/exchange-token-for-user-data'; - -export const accountServiceImplementation = { - getUserData, - getNEXPassword, - getNEXData, - updatePNIDPermissions, - exchangeTokenForUserData -}; diff --git a/src/services/grpc/account/api-key-middleware.ts b/src/services/grpc/account/v1/api-key-middleware.ts similarity index 100% rename from src/services/grpc/account/api-key-middleware.ts rename to src/services/grpc/account/v1/api-key-middleware.ts diff --git a/src/services/grpc/account/v1/exchange-token-for-user-data.ts b/src/services/grpc/account/v1/exchange-token-for-user-data.ts new file mode 100644 index 0000000..167b1ba --- /dev/null +++ b/src/services/grpc/account/v1/exchange-token-for-user-data.ts @@ -0,0 +1,62 @@ +import { Status, ServerError } from 'nice-grpc'; +import { getPNIDByTokenAuth } from '@/database'; +import { PNID_PERMISSION_FLAGS } from '@/types/common/permission-flags'; +import { config } from '@/config-manager'; +import type { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc'; +import type { ExchangeTokenForUserDataRequest } from '@pretendonetwork/grpc/account/exchange_token_for_user_data'; + +export async function exchangeTokenForUserData(request: ExchangeTokenForUserDataRequest): Promise { + if (!request.token.trim()) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token'); + } + + const pnid = await getPNIDByTokenAuth(request.token); + + if (!pnid) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token'); + } + + return { + deleted: pnid.deleted, + 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` + }, + creationDate: pnid.creation_date, + birthdate: pnid.birthdate, + gender: pnid.gender, + country: pnid.country, + language: pnid.language, + emailAddress: pnid.email.address, + tierName: pnid.connections.stripe.tier_name, + permissions: { + bannedAllPermanently: pnid.hasPermission(PNID_PERMISSION_FLAGS.BANNED_ALL_PERMANENTLY), + bannedAllTemporarily: pnid.hasPermission(PNID_PERMISSION_FLAGS.BANNED_ALL_TEMPORARILY), + betaAccess: pnid.hasPermission(PNID_PERMISSION_FLAGS.BETA_ACCESS), + accessAdminPanel: pnid.hasPermission(PNID_PERMISSION_FLAGS.ACCESS_ADMIN_PANEL), + createServerConfigs: pnid.hasPermission(PNID_PERMISSION_FLAGS.CREATE_SERVER_CONFIGS), + modifyServerConfigs: pnid.hasPermission(PNID_PERMISSION_FLAGS.MODIFY_SERVER_CONFIGS), + deployServer: pnid.hasPermission(PNID_PERMISSION_FLAGS.DEPLOY_SERVER), + modifyPnids: pnid.hasPermission(PNID_PERMISSION_FLAGS.MODIFY_PNIDS), + modifyNexAccounts: pnid.hasPermission(PNID_PERMISSION_FLAGS.MODIFY_NEX_ACCOUNTS), + modifyConsoles: pnid.hasPermission(PNID_PERMISSION_FLAGS.MODIFY_CONSOLES), + banPnids: pnid.hasPermission(PNID_PERMISSION_FLAGS.BAN_PNIDS), + banNexAccounts: pnid.hasPermission(PNID_PERMISSION_FLAGS.BAN_NEX_ACCOUNTS), + banConsoles: pnid.hasPermission(PNID_PERMISSION_FLAGS.BAN_CONSOLES), + moderateMiiverse: pnid.hasPermission(PNID_PERMISSION_FLAGS.MODERATE_MIIVERSE), + createApiKeys: pnid.hasPermission(PNID_PERMISSION_FLAGS.CREATE_API_KEYS), + createBossTasks: pnid.hasPermission(PNID_PERMISSION_FLAGS.CREATE_BOSS_TASKS), + updateBossTasks: pnid.hasPermission(PNID_PERMISSION_FLAGS.UPDATE_BOSS_TASKS), + deleteBossTasks: pnid.hasPermission(PNID_PERMISSION_FLAGS.DELETE_BOSS_TASKS), + uploadBossFiles: pnid.hasPermission(PNID_PERMISSION_FLAGS.UPLOAD_BOSS_FILES), + updateBossFiles: pnid.hasPermission(PNID_PERMISSION_FLAGS.UPDATE_BOSS_FILES), + deleteBossFiles: pnid.hasPermission(PNID_PERMISSION_FLAGS.DELETE_BOSS_FILES), + updatePnidPermissions: pnid.hasPermission(PNID_PERMISSION_FLAGS.UPDATE_PNID_PERMISSIONS) + } + }; +} diff --git a/src/services/grpc/account/get-nex-data.ts b/src/services/grpc/account/v1/get-nex-data.ts similarity index 100% rename from src/services/grpc/account/get-nex-data.ts rename to src/services/grpc/account/v1/get-nex-data.ts diff --git a/src/services/grpc/account/get-nex-password.ts b/src/services/grpc/account/v1/get-nex-password.ts similarity index 100% rename from src/services/grpc/account/get-nex-password.ts rename to src/services/grpc/account/v1/get-nex-password.ts diff --git a/src/services/grpc/account/v1/get-user-data.ts b/src/services/grpc/account/v1/get-user-data.ts new file mode 100644 index 0000000..bce0c65 --- /dev/null +++ b/src/services/grpc/account/v1/get-user-data.ts @@ -0,0 +1,60 @@ +import { Status, ServerError } from 'nice-grpc'; +import { getPNIDByPID } from '@/database'; +import { PNID_PERMISSION_FLAGS } from '@/types/common/permission-flags'; +import { config } from '@/config-manager'; +import type { GetUserDataRequest, GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc'; + +export async function getUserData(request: GetUserDataRequest): Promise { + const pnid = await getPNIDByPID(request.pid); + + if (!pnid) { + throw new ServerError( + Status.INVALID_ARGUMENT, + 'No PNID found' + ); + } + + return { + deleted: pnid.deleted, + 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` + }, + creationDate: pnid.creation_date, + birthdate: pnid.birthdate, + gender: pnid.gender, + country: pnid.country, + language: pnid.language, + emailAddress: pnid.email.address, + tierName: pnid.connections.stripe.tier_name, + permissions: { + bannedAllPermanently: pnid.hasPermission(PNID_PERMISSION_FLAGS.BANNED_ALL_PERMANENTLY), + bannedAllTemporarily: pnid.hasPermission(PNID_PERMISSION_FLAGS.BANNED_ALL_TEMPORARILY), + betaAccess: pnid.hasPermission(PNID_PERMISSION_FLAGS.BETA_ACCESS), + accessAdminPanel: pnid.hasPermission(PNID_PERMISSION_FLAGS.ACCESS_ADMIN_PANEL), + createServerConfigs: pnid.hasPermission(PNID_PERMISSION_FLAGS.CREATE_SERVER_CONFIGS), + modifyServerConfigs: pnid.hasPermission(PNID_PERMISSION_FLAGS.MODIFY_SERVER_CONFIGS), + deployServer: pnid.hasPermission(PNID_PERMISSION_FLAGS.DEPLOY_SERVER), + modifyPnids: pnid.hasPermission(PNID_PERMISSION_FLAGS.MODIFY_PNIDS), + modifyNexAccounts: pnid.hasPermission(PNID_PERMISSION_FLAGS.MODIFY_NEX_ACCOUNTS), + modifyConsoles: pnid.hasPermission(PNID_PERMISSION_FLAGS.MODIFY_CONSOLES), + banPnids: pnid.hasPermission(PNID_PERMISSION_FLAGS.BAN_PNIDS), + banNexAccounts: pnid.hasPermission(PNID_PERMISSION_FLAGS.BAN_NEX_ACCOUNTS), + banConsoles: pnid.hasPermission(PNID_PERMISSION_FLAGS.BAN_CONSOLES), + moderateMiiverse: pnid.hasPermission(PNID_PERMISSION_FLAGS.MODERATE_MIIVERSE), + createApiKeys: pnid.hasPermission(PNID_PERMISSION_FLAGS.CREATE_API_KEYS), + createBossTasks: pnid.hasPermission(PNID_PERMISSION_FLAGS.CREATE_BOSS_TASKS), + updateBossTasks: pnid.hasPermission(PNID_PERMISSION_FLAGS.UPDATE_BOSS_TASKS), + deleteBossTasks: pnid.hasPermission(PNID_PERMISSION_FLAGS.DELETE_BOSS_TASKS), + uploadBossFiles: pnid.hasPermission(PNID_PERMISSION_FLAGS.UPLOAD_BOSS_FILES), + updateBossFiles: pnid.hasPermission(PNID_PERMISSION_FLAGS.UPDATE_BOSS_FILES), + deleteBossFiles: pnid.hasPermission(PNID_PERMISSION_FLAGS.DELETE_BOSS_FILES), + updatePnidPermissions: pnid.hasPermission(PNID_PERMISSION_FLAGS.UPDATE_PNID_PERMISSIONS) + } + }; +} diff --git a/src/services/grpc/account/v1/implementation.ts b/src/services/grpc/account/v1/implementation.ts new file mode 100644 index 0000000..e4d2d72 --- /dev/null +++ b/src/services/grpc/account/v1/implementation.ts @@ -0,0 +1,13 @@ +import { getUserData } from '@/services/grpc/account/v1/get-user-data'; +import { getNEXPassword } from '@/services/grpc/account/v1/get-nex-password'; +import { getNEXData } from '@/services/grpc/account/v1/get-nex-data'; +import { updatePNIDPermissions } from '@/services/grpc/account/v1/update-pnid-permissions'; +import { exchangeTokenForUserData } from '@/services/grpc/account/v1/exchange-token-for-user-data'; + +export const accountServiceImplementationV1 = { + getUserData, + getNEXPassword, + getNEXData, + updatePNIDPermissions, + exchangeTokenForUserData +}; diff --git a/src/services/grpc/account/update-pnid-permissions.ts b/src/services/grpc/account/v1/update-pnid-permissions.ts similarity index 98% rename from src/services/grpc/account/update-pnid-permissions.ts rename to src/services/grpc/account/v1/update-pnid-permissions.ts index 4ba78a4..fbc52ae 100644 --- a/src/services/grpc/account/update-pnid-permissions.ts +++ b/src/services/grpc/account/v1/update-pnid-permissions.ts @@ -2,7 +2,7 @@ import { Status, ServerError } from 'nice-grpc'; import { getPNIDByPID } from '@/database'; import { PNID_PERMISSION_FLAGS } from '@/types/common/permission-flags'; import type { UpdatePNIDPermissionsRequest } from '@pretendonetwork/grpc/account/update_pnid_permissions'; -import type { Empty } from '@pretendonetwork/grpc/api/google/protobuf/empty'; +import type { Empty } from '@pretendonetwork/grpc/google/protobuf/empty'; export async function updatePNIDPermissions(request: UpdatePNIDPermissionsRequest): Promise { const pnid = await getPNIDByPID(request.pid); diff --git a/src/services/grpc/account/v2/api-key-middleware.ts b/src/services/grpc/account/v2/api-key-middleware.ts new file mode 100644 index 0000000..c79e0fb --- /dev/null +++ b/src/services/grpc/account/v2/api-key-middleware.ts @@ -0,0 +1,16 @@ +import { Status, ServerError } from 'nice-grpc'; +import { config } from '@/config-manager'; +import type { ServerMiddlewareCall, CallContext } from 'nice-grpc'; + +export async function* apiKeyMiddleware( + call: ServerMiddlewareCall, + context: CallContext +): AsyncGenerator { + const apiKey = 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); +} diff --git a/src/services/grpc/account/exchange-token-for-user-data.ts b/src/services/grpc/account/v2/exchange-token-for-user-data.ts similarity index 96% rename from src/services/grpc/account/exchange-token-for-user-data.ts rename to src/services/grpc/account/v2/exchange-token-for-user-data.ts index f8ef169..ace66af 100644 --- a/src/services/grpc/account/exchange-token-for-user-data.ts +++ b/src/services/grpc/account/v2/exchange-token-for-user-data.ts @@ -3,8 +3,8 @@ import { getPNIDByTokenAuth } from '@/database'; import { PNID_PERMISSION_FLAGS } from '@/types/common/permission-flags'; import { config } from '@/config-manager'; import { Device } from '@/models/device'; -import type { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc'; -import type { ExchangeTokenForUserDataRequest } from '@pretendonetwork/grpc/account/exchange_token_for_user_data'; +import type { GetUserDataResponse } from '@pretendonetwork/grpc/account/v2/get_user_data_rpc'; +import type { ExchangeTokenForUserDataRequest } from '@pretendonetwork/grpc/account/v2/exchange_token_for_user_data'; export async function exchangeTokenForUserData(request: ExchangeTokenForUserDataRequest): Promise { if (!request.token.trim()) { @@ -71,6 +71,6 @@ export async function exchangeTokenForUserData(request: ExchangeTokenForUserData deleteBossFiles: pnid.hasPermission(PNID_PERMISSION_FLAGS.DELETE_BOSS_FILES), updatePnidPermissions: pnid.hasPermission(PNID_PERMISSION_FLAGS.UPDATE_PNID_PERMISSIONS) }, - devices + linkedDevices: devices }; } diff --git a/src/services/grpc/account/v2/get-nex-data.ts b/src/services/grpc/account/v2/get-nex-data.ts new file mode 100644 index 0000000..debd177 --- /dev/null +++ b/src/services/grpc/account/v2/get-nex-data.ts @@ -0,0 +1,23 @@ +import { Status, ServerError } from 'nice-grpc'; +import { NEXAccount } from '@/models/nex-account'; +import type { GetNEXDataRequest, GetNEXDataResponse, DeepPartial } from '@pretendonetwork/grpc/account/v2/get_nex_data_rpc'; + +export async function getNEXData(request: GetNEXDataRequest): Promise> { + const nexAccount = await NEXAccount.findOne({ pid: request.pid }); + + if (!nexAccount) { + throw new ServerError( + Status.INVALID_ARGUMENT, + 'No NEX account found' + ); + } + + return { + pid: nexAccount.pid, + password: nexAccount.password, + owningPid: nexAccount.owning_pid, + accessLevel: nexAccount.access_level, + serverAccessLevel: nexAccount.server_access_level, + friendCode: nexAccount.friend_code + }; +} diff --git a/src/services/grpc/account/v2/get-nex-password.ts b/src/services/grpc/account/v2/get-nex-password.ts new file mode 100644 index 0000000..3a29942 --- /dev/null +++ b/src/services/grpc/account/v2/get-nex-password.ts @@ -0,0 +1,18 @@ +import { Status, ServerError } from 'nice-grpc'; +import { NEXAccount } from '@/models/nex-account'; +import type { GetNEXPasswordRequest, GetNEXPasswordResponse, DeepPartial } from '@pretendonetwork/grpc/account/v2/get_nex_password_rpc'; + +export async function getNEXPassword(request: GetNEXPasswordRequest): Promise> { + const nexAccount = await NEXAccount.findOne({ pid: request.pid }); + + if (!nexAccount) { + throw new ServerError( + Status.INVALID_ARGUMENT, + 'No NEX account found' + ); + } + + return { + password: nexAccount.password + }; +} diff --git a/src/services/grpc/account/get-user-data.ts b/src/services/grpc/account/v2/get-user-data.ts similarity index 97% rename from src/services/grpc/account/get-user-data.ts rename to src/services/grpc/account/v2/get-user-data.ts index 9dffe28..0cb8f4e 100644 --- a/src/services/grpc/account/get-user-data.ts +++ b/src/services/grpc/account/v2/get-user-data.ts @@ -3,7 +3,7 @@ import { getPNIDByPID } from '@/database'; import { PNID_PERMISSION_FLAGS } from '@/types/common/permission-flags'; import { config } from '@/config-manager'; import { Device } from '@/models/device'; -import type { GetUserDataRequest, GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc'; +import type { GetUserDataRequest, GetUserDataResponse } from '@pretendonetwork/grpc/account/v2/get_user_data_rpc'; export async function getUserData(request: GetUserDataRequest): Promise { const pnid = await getPNIDByPID(request.pid); @@ -69,6 +69,6 @@ export async function getUserData(request: GetUserDataRequest): Promise { + const pnid = await getPNIDByPID(request.pid); + + if (!pnid) { + throw new ServerError( + Status.INVALID_ARGUMENT, + 'No PNID found' + ); + } + + if (!request.permissions) { + throw new ServerError( + Status.INVALID_ARGUMENT, + 'Permissions flags not found' + ); + } + + if (request.permissions.bannedAllPermanently === true) { + await pnid.addPermission(PNID_PERMISSION_FLAGS.BANNED_ALL_PERMANENTLY); + } else if (request.permissions.bannedAllPermanently === false) { + await pnid.clearPermission(PNID_PERMISSION_FLAGS.BANNED_ALL_PERMANENTLY); + } + + if (request.permissions.bannedAllTemporarily === true) { + await pnid.addPermission(PNID_PERMISSION_FLAGS.BANNED_ALL_TEMPORARILY); + } else if (request.permissions.bannedAllTemporarily === false) { + await pnid.clearPermission(PNID_PERMISSION_FLAGS.BANNED_ALL_TEMPORARILY); + } + + if (request.permissions.betaAccess === true) { + await pnid.addPermission(PNID_PERMISSION_FLAGS.BETA_ACCESS); + } else if (request.permissions.betaAccess === false) { + await pnid.clearPermission(PNID_PERMISSION_FLAGS.BETA_ACCESS); + } + + if (request.permissions.accessAdminPanel === true) { + await pnid.addPermission(PNID_PERMISSION_FLAGS.ACCESS_ADMIN_PANEL); + } else if (request.permissions.accessAdminPanel === false) { + await pnid.clearPermission(PNID_PERMISSION_FLAGS.ACCESS_ADMIN_PANEL); + } + + if (request.permissions.createServerConfigs === true) { + await pnid.addPermission(PNID_PERMISSION_FLAGS.CREATE_SERVER_CONFIGS); + } else if (request.permissions.createServerConfigs === false) { + await pnid.clearPermission(PNID_PERMISSION_FLAGS.CREATE_SERVER_CONFIGS); + } + + if (request.permissions.modifyServerConfigs === true) { + await pnid.addPermission(PNID_PERMISSION_FLAGS.MODIFY_SERVER_CONFIGS); + } else if (request.permissions.modifyServerConfigs === false) { + await pnid.clearPermission(PNID_PERMISSION_FLAGS.MODIFY_SERVER_CONFIGS); + } + + if (request.permissions.deployServer === true) { + await pnid.addPermission(PNID_PERMISSION_FLAGS.DEPLOY_SERVER); + } else if (request.permissions.deployServer === false) { + await pnid.clearPermission(PNID_PERMISSION_FLAGS.DEPLOY_SERVER); + } + + if (request.permissions.modifyPnids === true) { + await pnid.addPermission(PNID_PERMISSION_FLAGS.MODIFY_PNIDS); + } else if (request.permissions.modifyPnids === false) { + await pnid.clearPermission(PNID_PERMISSION_FLAGS.MODIFY_PNIDS); + } + + if (request.permissions.modifyNexAccounts === true) { + await pnid.addPermission(PNID_PERMISSION_FLAGS.MODIFY_NEX_ACCOUNTS); + } else if (request.permissions.modifyNexAccounts === false) { + await pnid.clearPermission(PNID_PERMISSION_FLAGS.MODIFY_NEX_ACCOUNTS); + } + + if (request.permissions.modifyConsoles === true) { + await pnid.addPermission(PNID_PERMISSION_FLAGS.MODIFY_CONSOLES); + } else if (request.permissions.modifyConsoles === false) { + await pnid.clearPermission(PNID_PERMISSION_FLAGS.MODIFY_CONSOLES); + } + + if (request.permissions.banPnids === true) { + await pnid.addPermission(PNID_PERMISSION_FLAGS.BAN_PNIDS); + } else if (request.permissions.banPnids === false) { + await pnid.clearPermission(PNID_PERMISSION_FLAGS.BAN_PNIDS); + } + + if (request.permissions.banNexAccounts === true) { + await pnid.addPermission(PNID_PERMISSION_FLAGS.BAN_NEX_ACCOUNTS); + } else if (request.permissions.banNexAccounts === false) { + await pnid.clearPermission(PNID_PERMISSION_FLAGS.BAN_NEX_ACCOUNTS); + } + + if (request.permissions.banConsoles === true) { + await pnid.addPermission(PNID_PERMISSION_FLAGS.BAN_CONSOLES); + } else if (request.permissions.banConsoles === false) { + await pnid.clearPermission(PNID_PERMISSION_FLAGS.BAN_CONSOLES); + } + + if (request.permissions.moderateMiiverse === true) { + await pnid.addPermission(PNID_PERMISSION_FLAGS.MODERATE_MIIVERSE); + } else if (request.permissions.moderateMiiverse === false) { + await pnid.clearPermission(PNID_PERMISSION_FLAGS.MODERATE_MIIVERSE); + } + + if (request.permissions.createApiKeys === true) { + await pnid.addPermission(PNID_PERMISSION_FLAGS.CREATE_API_KEYS); + } else if (request.permissions.createApiKeys === false) { + await pnid.clearPermission(PNID_PERMISSION_FLAGS.CREATE_API_KEYS); + } + + if (request.permissions.createBossTasks === true) { + await pnid.addPermission(PNID_PERMISSION_FLAGS.CREATE_BOSS_TASKS); + } else if (request.permissions.createBossTasks === false) { + await pnid.clearPermission(PNID_PERMISSION_FLAGS.CREATE_BOSS_TASKS); + } + + if (request.permissions.updateBossTasks === true) { + await pnid.addPermission(PNID_PERMISSION_FLAGS.UPDATE_BOSS_TASKS); + } else if (request.permissions.updateBossTasks === false) { + await pnid.clearPermission(PNID_PERMISSION_FLAGS.UPDATE_BOSS_TASKS); + } + + if (request.permissions.deleteBossTasks === true) { + await pnid.addPermission(PNID_PERMISSION_FLAGS.DELETE_BOSS_TASKS); + } else if (request.permissions.deleteBossTasks === false) { + await pnid.clearPermission(PNID_PERMISSION_FLAGS.DELETE_BOSS_TASKS); + } + + if (request.permissions.uploadBossFiles === true) { + await pnid.addPermission(PNID_PERMISSION_FLAGS.UPLOAD_BOSS_FILES); + } else if (request.permissions.uploadBossFiles === false) { + await pnid.clearPermission(PNID_PERMISSION_FLAGS.UPLOAD_BOSS_FILES); + } + + if (request.permissions.updateBossFiles === true) { + await pnid.addPermission(PNID_PERMISSION_FLAGS.UPDATE_BOSS_FILES); + } else if (request.permissions.updateBossFiles === false) { + await pnid.clearPermission(PNID_PERMISSION_FLAGS.UPDATE_BOSS_FILES); + } + + if (request.permissions.deleteBossFiles === true) { + await pnid.addPermission(PNID_PERMISSION_FLAGS.DELETE_BOSS_FILES); + } else if (request.permissions.deleteBossFiles === false) { + await pnid.clearPermission(PNID_PERMISSION_FLAGS.DELETE_BOSS_FILES); + } + + if (request.permissions.updatePnidPermissions === true) { + await pnid.addPermission(PNID_PERMISSION_FLAGS.UPDATE_PNID_PERMISSIONS); + } else if (request.permissions.updatePnidPermissions === false) { + await pnid.clearPermission(PNID_PERMISSION_FLAGS.UPDATE_PNID_PERMISSIONS); + } + + await pnid.save(); + + return {}; +} diff --git a/src/services/grpc/api/implementation.ts b/src/services/grpc/api/implementation.ts deleted file mode 100644 index 6e79ab2..0000000 --- a/src/services/grpc/api/implementation.ts +++ /dev/null @@ -1,19 +0,0 @@ -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 = { - register, - login, - getUserData, - updateUserData, - forgotPassword, - resetPassword, - setDiscordConnectionData, - setStripeConnectionData -}; diff --git a/src/services/grpc/api/api-key-middleware.ts b/src/services/grpc/api/v1/api-key-middleware.ts similarity index 100% rename from src/services/grpc/api/api-key-middleware.ts rename to src/services/grpc/api/v1/api-key-middleware.ts diff --git a/src/services/grpc/api/authentication-middleware.ts b/src/services/grpc/api/v1/authentication-middleware.ts similarity index 100% rename from src/services/grpc/api/authentication-middleware.ts rename to src/services/grpc/api/v1/authentication-middleware.ts diff --git a/src/services/grpc/api/forgot-password.ts b/src/services/grpc/api/v1/forgot-password.ts similarity index 91% rename from src/services/grpc/api/forgot-password.ts rename to src/services/grpc/api/v1/forgot-password.ts index 30733cd..c815015 100644 --- a/src/services/grpc/api/forgot-password.ts +++ b/src/services/grpc/api/v1/forgot-password.ts @@ -3,7 +3,7 @@ import validator from 'validator'; import { getPNIDByEmailAddress, getPNIDByUsername } from '@/database'; import { sendForgotPasswordEmail } from '@/util'; import type { ForgotPasswordRequest } from '@pretendonetwork/grpc/api/forgot_password_rpc'; -import type { Empty } from '@pretendonetwork/grpc/api/google/protobuf/empty'; +import type { Empty } from '@pretendonetwork/grpc/google/protobuf/empty'; import type { HydratedPNIDDocument } from '@/types/mongoose/pnid'; export async function forgotPassword(request: ForgotPasswordRequest): Promise { diff --git a/src/services/grpc/api/get-user-data.ts b/src/services/grpc/api/v1/get-user-data.ts similarity index 93% rename from src/services/grpc/api/get-user-data.ts rename to src/services/grpc/api/v1/get-user-data.ts index 4d6ffa7..29a78bc 100644 --- a/src/services/grpc/api/get-user-data.ts +++ b/src/services/grpc/api/v1/get-user-data.ts @@ -1,8 +1,8 @@ import { config } from '@/config-manager'; import type { CallContext } from 'nice-grpc'; import type { GetUserDataResponse, DeepPartial } from '@pretendonetwork/grpc/api/get_user_data_rpc'; -import type { Empty } from '@pretendonetwork/grpc/api/google/protobuf/empty'; -import type { AuthenticationCallContextExt } from '@/services/grpc/api/authentication-middleware'; +import type { Empty } from '@pretendonetwork/grpc/google/protobuf/empty'; +import type { AuthenticationCallContextExt } from '@/services/grpc/api/v1/authentication-middleware'; export async function getUserData(_request: Empty, context: CallContext & AuthenticationCallContextExt): Promise> { // * This is asserted in authentication-middleware, we know this is never null diff --git a/src/services/grpc/api/v1/implementation.ts b/src/services/grpc/api/v1/implementation.ts new file mode 100644 index 0000000..30daa10 --- /dev/null +++ b/src/services/grpc/api/v1/implementation.ts @@ -0,0 +1,19 @@ +import { register } from '@/services/grpc/api/v1/register'; +import { login } from '@/services/grpc/api/v1/login'; +import { getUserData } from '@/services/grpc/api/v1/get-user-data'; +import { updateUserData } from '@/services/grpc/api/v1/update-user-data'; +import { forgotPassword } from '@/services/grpc/api/v1/forgot-password'; +import { resetPassword } from '@/services/grpc/api/v1/reset-password'; +import { setDiscordConnectionData } from '@/services/grpc/api/v1/set-discord-connection-data'; +import { setStripeConnectionData } from '@/services/grpc/api/v1/set-stripe-connection-data'; + +export const apiServiceImplementationV1 = { + register, + login, + getUserData, + updateUserData, + forgotPassword, + resetPassword, + setDiscordConnectionData, + setStripeConnectionData +}; diff --git a/src/services/grpc/api/login.ts b/src/services/grpc/api/v1/login.ts similarity index 100% rename from src/services/grpc/api/login.ts rename to src/services/grpc/api/v1/login.ts diff --git a/src/services/grpc/api/register.ts b/src/services/grpc/api/v1/register.ts similarity index 100% rename from src/services/grpc/api/register.ts rename to src/services/grpc/api/v1/register.ts diff --git a/src/services/grpc/api/reset-password.ts b/src/services/grpc/api/v1/reset-password.ts similarity index 97% rename from src/services/grpc/api/reset-password.ts rename to src/services/grpc/api/v1/reset-password.ts index 0fb99ca..5be6135 100644 --- a/src/services/grpc/api/reset-password.ts +++ b/src/services/grpc/api/v1/reset-password.ts @@ -3,7 +3,7 @@ import { Status, ServerError } from 'nice-grpc'; import { decryptToken, unpackToken, nintendoPasswordHash } from '@/util'; import { getPNIDByPID } from '@/database'; import type { ResetPasswordRequest } from '@pretendonetwork/grpc/api/reset_password_rpc'; -import type { Empty } from '@pretendonetwork/grpc/api/google/protobuf/empty'; +import type { Empty } from '@pretendonetwork/grpc/google/protobuf/empty'; import type { Token } from '@/types/common/token'; // * This sucks diff --git a/src/services/grpc/api/set-discord-connection-data.ts b/src/services/grpc/api/v1/set-discord-connection-data.ts similarity index 87% rename from src/services/grpc/api/set-discord-connection-data.ts rename to src/services/grpc/api/v1/set-discord-connection-data.ts index bf28820..cc2341d 100644 --- a/src/services/grpc/api/set-discord-connection-data.ts +++ b/src/services/grpc/api/v1/set-discord-connection-data.ts @@ -1,8 +1,8 @@ import { Status, ServerError } from 'nice-grpc'; import type { CallContext } from 'nice-grpc'; import type { SetDiscordConnectionDataRequest } from '@pretendonetwork/grpc/api/set_discord_connection_data_rpc'; -import type { Empty } from '@pretendonetwork/grpc/api/google/protobuf/empty'; -import type { AuthenticationCallContextExt } from '@/services/grpc/api/authentication-middleware'; +import type { Empty } from '@pretendonetwork/grpc/google/protobuf/empty'; +import type { AuthenticationCallContextExt } from '@/services/grpc/api/v1/authentication-middleware'; export async function setDiscordConnectionData(request: SetDiscordConnectionDataRequest, context: CallContext & AuthenticationCallContextExt): Promise { // * This is asserted in authentication-middleware, we know this is never null diff --git a/src/services/grpc/api/set-stripe-connection-data.ts b/src/services/grpc/api/v1/set-stripe-connection-data.ts similarity index 86% rename from src/services/grpc/api/set-stripe-connection-data.ts rename to src/services/grpc/api/v1/set-stripe-connection-data.ts index 307a6a1..e73c365 100644 --- a/src/services/grpc/api/set-stripe-connection-data.ts +++ b/src/services/grpc/api/v1/set-stripe-connection-data.ts @@ -2,8 +2,8 @@ import { Status, ServerError } from 'nice-grpc'; import { PNID } from '@/models/pnid'; import type { CallContext } from 'nice-grpc'; import type { SetStripeConnectionDataRequest } from '@pretendonetwork/grpc/api/set_stripe_connection_data_rpc'; -import type { Empty } from '@pretendonetwork/grpc/api/google/protobuf/empty'; -import type { AuthenticationCallContextExt } from '@/services/grpc/api/authentication-middleware'; +import type { Empty } from '@pretendonetwork/grpc/google/protobuf/empty'; +import type { AuthenticationCallContextExt } from '@/services/grpc/api/v1/authentication-middleware'; type StripeMongoUpdateScheme = { 'access_level'?: number; @@ -28,16 +28,6 @@ export async function setStripeConnectionData(request: SetStripeConnectionDataRe 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; } diff --git a/src/services/grpc/api/update-user-data.ts b/src/services/grpc/api/v1/update-user-data.ts similarity index 97% rename from src/services/grpc/api/update-user-data.ts rename to src/services/grpc/api/v1/update-user-data.ts index 1fc21d2..6375d0f 100644 --- a/src/services/grpc/api/update-user-data.ts +++ b/src/services/grpc/api/v1/update-user-data.ts @@ -2,7 +2,7 @@ import { config } from '@/config-manager'; import type { CallContext } from 'nice-grpc'; import type { UpdateUserDataRequest, DeepPartial } from '@pretendonetwork/grpc/api/update_user_data_rpc'; import type { GetUserDataResponse } from '@pretendonetwork/grpc/api/get_user_data_rpc'; -import type { AuthenticationCallContextExt } from '@/services/grpc/api/authentication-middleware'; +import type { AuthenticationCallContextExt } from '@/services/grpc/api/v1/authentication-middleware'; export async function updateUserData(_request: UpdateUserDataRequest, context: CallContext & AuthenticationCallContextExt): Promise> { // * This is asserted in authentication-middleware, we know this is never null diff --git a/src/services/grpc/api/v2/api-key-middleware.ts b/src/services/grpc/api/v2/api-key-middleware.ts new file mode 100644 index 0000000..c88f5b1 --- /dev/null +++ b/src/services/grpc/api/v2/api-key-middleware.ts @@ -0,0 +1,16 @@ +import { Status, ServerError } from 'nice-grpc'; +import { config } from '@/config-manager'; +import type { ServerMiddlewareCall, CallContext } from 'nice-grpc'; + +export async function* apiKeyMiddleware( + call: ServerMiddlewareCall, + context: CallContext +): AsyncGenerator { + const apiKey = context.metadata.get('X-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/v2/authentication-middleware.ts b/src/services/grpc/api/v2/authentication-middleware.ts new file mode 100644 index 0000000..ff15fa2 --- /dev/null +++ b/src/services/grpc/api/v2/authentication-middleware.ts @@ -0,0 +1,56 @@ +import { Status, ServerError } from 'nice-grpc'; +import { getPNIDByTokenAuth } from '@/database'; +import type { ServerMiddlewareCall, CallContext } from 'nice-grpc'; +import type { HydratedPNIDDocument } from '@/types/mongoose/pnid'; + +// * These paths require that a token be present +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/SetDiscordConnectionData', + '/api.v2.ApiService/SetStripeConnectionData', + '/api.v2.ApiService/RemoveConnection' +]; + +export type AuthenticationCallContextExt = { + pnid: HydratedPNIDDocument | null; +}; + +export async function* authenticationMiddleware( + call: ServerMiddlewareCall, + context: CallContext +): AsyncGenerator { + const token = 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 = null; + + if (token) { + pnid = await getPNIDByTokenAuth(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 = 'Unknown server error'; + + console.log(error); + + if (error instanceof Error) { + message = error.message; + } + + throw new ServerError(Status.INVALID_ARGUMENT, message); + } +} diff --git a/src/services/grpc/api/v2/forgot-password.ts b/src/services/grpc/api/v2/forgot-password.ts new file mode 100644 index 0000000..a20dea8 --- /dev/null +++ b/src/services/grpc/api/v2/forgot-password.ts @@ -0,0 +1,29 @@ +import { Status, ServerError } from 'nice-grpc'; +import validator from 'validator'; +import { getPNIDByEmailAddress, getPNIDByUsername } from '@/database'; +import { sendForgotPasswordEmail } from '@/util'; +import type { ForgotPasswordRequest } from '@pretendonetwork/grpc/api/v2/forgot_password_rpc'; +import type { Empty } from '@pretendonetwork/grpc/google/protobuf/empty'; +import type { HydratedPNIDDocument } from '@/types/mongoose/pnid'; + +export async function forgotPassword(request: ForgotPasswordRequest): Promise { + const input = 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 {}; +} diff --git a/src/services/grpc/api/v2/get-user-data.ts b/src/services/grpc/api/v2/get-user-data.ts new file mode 100644 index 0000000..3a3b151 --- /dev/null +++ b/src/services/grpc/api/v2/get-user-data.ts @@ -0,0 +1,45 @@ +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'; + +export async function getUserData(_request: Empty, context: CallContext & AuthenticationCallContextExt): Promise> { + // * This is asserted in authentication-middleware, we know this is never null + const pnid = 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: BigInt(pnid.connections.stripe.latest_webhook_timestamp ?? 0) + } + }, + marketingFlag: pnid.flags.marketing + }; +} diff --git a/src/services/grpc/api/v2/implementation.ts b/src/services/grpc/api/v2/implementation.ts new file mode 100644 index 0000000..39ee31f --- /dev/null +++ b/src/services/grpc/api/v2/implementation.ts @@ -0,0 +1,19 @@ +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 { forgotPassword } from '@/services/grpc/api/v2/forgot-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'; + +export const apiServiceImplementationV2 = { + register, + login, + getUserData, + updateUserData, + forgotPassword, + resetPassword, + setDiscordConnectionData, + setStripeConnectionData +}; diff --git a/src/services/grpc/api/v2/login.ts b/src/services/grpc/api/v2/login.ts new file mode 100644 index 0000000..6cfdb63 --- /dev/null +++ b/src/services/grpc/api/v2/login.ts @@ -0,0 +1,89 @@ +import { Status, ServerError } from 'nice-grpc'; +import bcrypt from 'bcrypt'; +import { getPNIDByUsername, getPNIDByTokenAuth } from '@/database'; +import { nintendoPasswordHash, generateToken } from '@/util'; +import { config } from '@/config-manager'; +import type { LoginRequest, LoginResponse, DeepPartial } from '@pretendonetwork/grpc/api/v2/login_rpc'; +import type { HydratedPNIDDocument } from '@/types/mongoose/pnid'; + +export async function login(request: LoginRequest): Promise> { + const grantType = request.grantType?.trim(); + const username = request.username?.trim(); + const password = request.password?.trim(); + const refreshToken = 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 = 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 getPNIDByTokenAuth(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 = { + 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 = { + 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 = await generateToken(config.aes_key, accessTokenOptions); + const refreshTokenBuffer = await generateToken(config.aes_key, refreshTokenOptions); + + const accessToken = accessTokenBuffer ? accessTokenBuffer.toString('hex') : ''; + const newRefreshToken = refreshTokenBuffer ? refreshTokenBuffer.toString('hex') : ''; + + // TODO - Handle null tokens + + return { + accessToken: accessToken, + tokenType: 'Bearer', + expiresIn: 3600, + refreshToken: newRefreshToken + }; +} diff --git a/src/services/grpc/api/v2/register.ts b/src/services/grpc/api/v2/register.ts new file mode 100644 index 0000000..e2e6e18 --- /dev/null +++ b/src/services/grpc/api/v2/register.ts @@ -0,0 +1,264 @@ +import crypto from 'node:crypto'; +import { Status, ServerError } from 'nice-grpc'; +import emailvalidator from 'email-validator'; +import bcrypt from 'bcrypt'; +import moment from 'moment'; +import hcaptcha from 'hcaptcha'; +import Mii from 'mii-js'; +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 { LoginResponse } from '@pretendonetwork/grpc/api/v2/login_rpc'; +import type { RegisterRequest, DeepPartial } from '@pretendonetwork/grpc/api/v2/register_rpc'; +import type { HydratedNEXAccountDocument } from '@/types/mongoose/nex-account'; +import type { HydratedPNIDDocument } from '@/types/mongoose/pnid'; + +const PNID_VALID_CHARACTERS_REGEX = /^[\w\-.]*$/; +const PNID_PUNCTUATION_START_REGEX = /^[_\-.]/; +const PNID_PUNCTUATION_END_REGEX = /[_\-.]$/; +const PNID_PUNCTUATION_DUPLICATE_REGEX = /[_\-.]{2,}/; + +// * 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/; + +const DEFAULT_MII_DATA = Buffer.from('AwAAQOlVognnx0GC2/uogAOzuI0n2QAAAEBEAGUAZgBhAHUAbAB0AAAAAAAAAEBAAAAhAQJoRBgmNEYUgRIXaA0AACkAUkhQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGm9', 'base64'); + +export async function register(request: RegisterRequest): Promise> { + const email = request.email?.trim(); + const username = request.username?.trim(); + const miiName = request.miiName?.trim(); + const password = request.password?.trim(); + const passwordConfirm = request.passwordConfirm?.trim(); + const captchaResponse = 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 = 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 = 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.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 = new Mii(DEFAULT_MII_DATA); + mii.miiName = miiName; + + const creationDate = moment().format('YYYY-MM-DDTHH:MM:SS'); + let pnid: HydratedPNIDDocument; + let nexAccount: HydratedNEXAccountDocument; + + const session = 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 = nintendoPasswordHash(password, nexAccount.pid); + const passwordHash = 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 = '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 = { + 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 = { + 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 = await generateToken(config.aes_key, accessTokenOptions); + const refreshTokenBuffer = await generateToken(config.aes_key, refreshTokenOptions); + + const accessToken = accessTokenBuffer ? accessTokenBuffer.toString('hex') : ''; + const refreshToken = refreshTokenBuffer ? refreshTokenBuffer.toString('hex') : ''; + + // TODO - Handle null tokens + + return { + accessToken: accessToken, + tokenType: 'Bearer', + expiresIn: 3600, + refreshToken: refreshToken + }; +} diff --git a/src/services/grpc/api/v2/reset-password.ts b/src/services/grpc/api/v2/reset-password.ts new file mode 100644 index 0000000..dbe833c --- /dev/null +++ b/src/services/grpc/api/v2/reset-password.ts @@ -0,0 +1,77 @@ +import bcrypt from 'bcrypt'; +import { Status, ServerError } from 'nice-grpc'; +import { decryptToken, unpackToken, nintendoPasswordHash } from '@/util'; +import { getPNIDByPID } from '@/database'; +import type { ResetPasswordRequest, ResetPasswordResponse } from '@pretendonetwork/grpc/api/v2/reset_password_rpc'; +import type { Token } from '@/types/common/token'; + +// * 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 resetPassword(request: ResetPasswordRequest): Promise { + const password = request.password.trim(); + const passwordConfirm = request.passwordConfirm.trim(); + const token = request.token.trim(); + + if (!token) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Missing token'); + } + + let unpackedToken: Token; + try { + const decryptedToken = await decryptToken(Buffer.from(token, 'base64')); + unpackedToken = unpackToken(decryptedToken); + } catch { + throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token'); + } + + if (unpackedToken.expire_time < Date.now()) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Token expired'); + } + + const pnid = 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 = nintendoPasswordHash(password, pnid.pid); + const passwordHash = await bcrypt.hash(primaryPasswordHash, 10); + + pnid.password = passwordHash; + + await pnid.save(); + + return {}; +} diff --git a/src/services/grpc/api/v2/set-discord-connection-data.ts b/src/services/grpc/api/v2/set-discord-connection-data.ts new file mode 100644 index 0000000..a1bda07 --- /dev/null +++ b/src/services/grpc/api/v2/set-discord-connection-data.ts @@ -0,0 +1,25 @@ +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'; + +export async function setDiscordConnectionData(request: SetDiscordConnectionDataRequest, context: CallContext & AuthenticationCallContextExt): Promise { + // * This is asserted in authentication-middleware, we know this is never null + const pnid = context.pnid!; + + try { + pnid.connections.discord.id = request.id; + + await pnid.save(); + } catch (error) { + let message = 'Unknown Mongo error'; + + if (error instanceof Error) { + message = error.message; + } + + throw new ServerError(Status.INVALID_ARGUMENT, message); + } + + return {}; +} diff --git a/src/services/grpc/api/v2/set-stripe-connection-data.ts b/src/services/grpc/api/v2/set-stripe-connection-data.ts new file mode 100644 index 0000000..04e0163 --- /dev/null +++ b/src/services/grpc/api/v2/set-stripe-connection-data.ts @@ -0,0 +1,80 @@ +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'; + +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 = context.pnid!; + + const updateData: StripeMongoUpdateScheme = { + 'connections.stripe.latest_webhook_timestamp': Number(request.timestamp) + }; + + if (request.customerId && !pnid.connections.stripe.customer_id) { + updateData['connections.stripe.customer_id'] = request.customerId; + } + + 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 = 'Unknown Mongo error'; + + if (error instanceof Error) { + message = error.message; + } + + throw new ServerError(Status.INVALID_ARGUMENT, message); + } + + return {}; +} diff --git a/src/services/grpc/api/v2/update-user-data.ts b/src/services/grpc/api/v2/update-user-data.ts new file mode 100644 index 0000000..d67e9dc --- /dev/null +++ b/src/services/grpc/api/v2/update-user-data.ts @@ -0,0 +1,47 @@ +import { config } from '@/config-manager'; +import type { CallContext } from 'nice-grpc'; +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'; + +export async function updateUserData(_request: UpdateUserDataRequest, context: CallContext & AuthenticationCallContextExt): Promise> { + // * This is asserted in authentication-middleware, we know this is never null + const pnid = 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: BigInt(pnid.connections.stripe.latest_webhook_timestamp ?? 0) + } + }, + marketingFlag: pnid.flags.marketing + }; +} diff --git a/src/services/grpc/server.ts b/src/services/grpc/server.ts index b4dee0d..c2ee117 100644 --- a/src/services/grpc/server.ts +++ b/src/services/grpc/server.ts @@ -1,18 +1,28 @@ import { createServer } from 'nice-grpc'; -import { AccountDefinition } from '@pretendonetwork/grpc/account/account_service'; -import { APIDefinition } from '@pretendonetwork/grpc/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 { AccountDefinition as AccountServiceDefinitionV1 } from '@pretendonetwork/grpc/account/account_service'; +import { APIDefinition as ApiServiceDefinitionV1 } from '@pretendonetwork/grpc/api/api_service'; +import { apiKeyMiddleware as accountApiKeyMiddlewareV1 } from '@/services/grpc/account/v1/api-key-middleware'; +import { apiKeyMiddleware as apiApiKeyMiddlewareV1 } from '@/services/grpc/api/v1/api-key-middleware'; +import { authenticationMiddleware as apiAuthenticationMiddlewareV1 } from '@/services/grpc/api/v1/authentication-middleware'; +import { accountServiceImplementationV1 } from '@/services/grpc/account/v1/implementation'; +import { apiServiceImplementationV1 } from '@/services/grpc/api/v1/implementation'; +import { AccountServiceDefinition as AccountServiceDefinitionV2 } from '@pretendonetwork/grpc/account/v2/account_service'; +import { ApiServiceDefinition as ApiServiceDefinitionV2 } from '@pretendonetwork/grpc/api/v2/api_service'; +import { apiKeyMiddleware as accountApiKeyMiddlewareV2 } from '@/services/grpc/account/v2/api-key-middleware'; +import { apiKeyMiddleware as apiApiKeyMiddlewareV2 } from '@/services/grpc/api/v2/api-key-middleware'; +import { authenticationMiddleware as apiAuthenticationMiddlewareV2 } from '@/services/grpc/api/v2/authentication-middleware'; +import { accountServiceImplementationV2 } from '@/services/grpc/account/v2/implementation'; +import { apiServiceImplementationV2 } from '@/services/grpc/api/v2/implementation'; import { config } from '@/config-manager'; export async function startGRPCServer(): Promise { const server = createServer(); - server.with(accountApiKeyMiddleware).add(AccountDefinition, accountServiceImplementation); - server.with(apiApiKeyMiddleware).with(apiAuthenticationMiddleware).add(APIDefinition, apiServiceImplementation); + server.with(accountApiKeyMiddlewareV1).add(AccountServiceDefinitionV1, accountServiceImplementationV1); + server.with(apiApiKeyMiddlewareV1).with(apiAuthenticationMiddlewareV1).add(ApiServiceDefinitionV1, apiServiceImplementationV1); + + server.with(accountApiKeyMiddlewareV2).add(AccountServiceDefinitionV2, accountServiceImplementationV2); + server.with(apiApiKeyMiddlewareV2).with(apiAuthenticationMiddlewareV2).add(ApiServiceDefinitionV2, apiServiceImplementationV2); await server.listen(`0.0.0.0:${config.grpc.port}`); }