mirror of
https://github.com/PretendoNetwork/account.git
synced 2026-04-26 16:17:31 -05:00
feat: implement v2 grpc servers
adds support for the new v2 gRPC servers while maintaining the old ones for legacy clients
This commit is contained in:
parent
84922a5c51
commit
eac5ec9742
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -61,4 +61,5 @@ typings/
|
||||||
config.json
|
config.json
|
||||||
certs
|
certs
|
||||||
cdn
|
cdn
|
||||||
dist
|
dist
|
||||||
|
.DS_Store
|
||||||
32
package-lock.json
generated
32
package-lock.json
generated
|
|
@ -12,7 +12,7 @@
|
||||||
"@aws-sdk/client-s3": "^3.657.0",
|
"@aws-sdk/client-s3": "^3.657.0",
|
||||||
"@aws-sdk/client-ses": "^3.515.0",
|
"@aws-sdk/client-ses": "^3.515.0",
|
||||||
"@inquirer/prompts": "^7.2.0",
|
"@inquirer/prompts": "^7.2.0",
|
||||||
"@pretendonetwork/grpc": "^1.0.5",
|
"@pretendonetwork/grpc": "2.1.1",
|
||||||
"bcrypt": "^5.0.0",
|
"bcrypt": "^5.0.0",
|
||||||
"buffer-crc32": "^0.2.13",
|
"buffer-crc32": "^0.2.13",
|
||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
|
|
@ -2915,6 +2915,12 @@
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"license": "0BSD"
|
"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": {
|
"node_modules/@eslint-community/eslint-plugin-eslint-comments": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|
@ -3565,11 +3571,27 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@pretendonetwork/grpc": {
|
"node_modules/@pretendonetwork/grpc": {
|
||||||
"version": "1.0.5",
|
"version": "2.1.1",
|
||||||
"license": "ISC",
|
"resolved": "https://registry.npmjs.org/@pretendonetwork/grpc/-/grpc-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-SEfeG9zqRh8Iaij7cPSpnC3s5bQ5oJhCXO0tKIMpirO6aQIGit8sNrABDfGihSXMVK9q+R8gHzODapIQQV4V6Q==",
|
||||||
|
"license": "AGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"long": "^5.2.1",
|
"@bufbuild/protobuf": "^2.2.2",
|
||||||
"protobufjs": "^7.2.3"
|
"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": {
|
"node_modules/@protobufjs/aspromise": {
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
"@aws-sdk/client-s3": "^3.657.0",
|
"@aws-sdk/client-s3": "^3.657.0",
|
||||||
"@aws-sdk/client-ses": "^3.515.0",
|
"@aws-sdk/client-ses": "^3.515.0",
|
||||||
"@inquirer/prompts": "^7.2.0",
|
"@inquirer/prompts": "^7.2.0",
|
||||||
"@pretendonetwork/grpc": "^1.0.5",
|
"@pretendonetwork/grpc": "2.1.1",
|
||||||
"bcrypt": "^5.0.0",
|
"bcrypt": "^5.0.0",
|
||||||
"buffer-crc32": "^0.2.13",
|
"buffer-crc32": "^0.2.13",
|
||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
};
|
|
||||||
62
src/services/grpc/account/v1/exchange-token-for-user-data.ts
Normal file
62
src/services/grpc/account/v1/exchange-token-for-user-data.ts
Normal file
|
|
@ -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<GetUserDataResponse> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
60
src/services/grpc/account/v1/get-user-data.ts
Normal file
60
src/services/grpc/account/v1/get-user-data.ts
Normal file
|
|
@ -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<GetUserDataResponse> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
13
src/services/grpc/account/v1/implementation.ts
Normal file
13
src/services/grpc/account/v1/implementation.ts
Normal file
|
|
@ -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
|
||||||
|
};
|
||||||
|
|
@ -2,7 +2,7 @@ import { Status, ServerError } from 'nice-grpc';
|
||||||
import { getPNIDByPID } from '@/database';
|
import { getPNIDByPID } from '@/database';
|
||||||
import { PNID_PERMISSION_FLAGS } from '@/types/common/permission-flags';
|
import { PNID_PERMISSION_FLAGS } from '@/types/common/permission-flags';
|
||||||
import type { UpdatePNIDPermissionsRequest } from '@pretendonetwork/grpc/account/update_pnid_permissions';
|
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<Empty> {
|
export async function updatePNIDPermissions(request: UpdatePNIDPermissionsRequest): Promise<Empty> {
|
||||||
const pnid = await getPNIDByPID(request.pid);
|
const pnid = await getPNIDByPID(request.pid);
|
||||||
16
src/services/grpc/account/v2/api-key-middleware.ts
Normal file
16
src/services/grpc/account/v2/api-key-middleware.ts
Normal file
|
|
@ -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<Request, Response>(
|
||||||
|
call: ServerMiddlewareCall<Request, Response>,
|
||||||
|
context: CallContext
|
||||||
|
): AsyncGenerator<Response, Response | void, undefined> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
@ -3,8 +3,8 @@ import { getPNIDByTokenAuth } from '@/database';
|
||||||
import { PNID_PERMISSION_FLAGS } from '@/types/common/permission-flags';
|
import { PNID_PERMISSION_FLAGS } from '@/types/common/permission-flags';
|
||||||
import { config } from '@/config-manager';
|
import { config } from '@/config-manager';
|
||||||
import { Device } from '@/models/device';
|
import { Device } from '@/models/device';
|
||||||
import type { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc';
|
import type { GetUserDataResponse } from '@pretendonetwork/grpc/account/v2/get_user_data_rpc';
|
||||||
import type { ExchangeTokenForUserDataRequest } from '@pretendonetwork/grpc/account/exchange_token_for_user_data';
|
import type { ExchangeTokenForUserDataRequest } from '@pretendonetwork/grpc/account/v2/exchange_token_for_user_data';
|
||||||
|
|
||||||
export async function exchangeTokenForUserData(request: ExchangeTokenForUserDataRequest): Promise<GetUserDataResponse> {
|
export async function exchangeTokenForUserData(request: ExchangeTokenForUserDataRequest): Promise<GetUserDataResponse> {
|
||||||
if (!request.token.trim()) {
|
if (!request.token.trim()) {
|
||||||
|
|
@ -71,6 +71,6 @@ export async function exchangeTokenForUserData(request: ExchangeTokenForUserData
|
||||||
deleteBossFiles: pnid.hasPermission(PNID_PERMISSION_FLAGS.DELETE_BOSS_FILES),
|
deleteBossFiles: pnid.hasPermission(PNID_PERMISSION_FLAGS.DELETE_BOSS_FILES),
|
||||||
updatePnidPermissions: pnid.hasPermission(PNID_PERMISSION_FLAGS.UPDATE_PNID_PERMISSIONS)
|
updatePnidPermissions: pnid.hasPermission(PNID_PERMISSION_FLAGS.UPDATE_PNID_PERMISSIONS)
|
||||||
},
|
},
|
||||||
devices
|
linkedDevices: devices
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
23
src/services/grpc/account/v2/get-nex-data.ts
Normal file
23
src/services/grpc/account/v2/get-nex-data.ts
Normal file
|
|
@ -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<DeepPartial<GetNEXDataResponse>> {
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
18
src/services/grpc/account/v2/get-nex-password.ts
Normal file
18
src/services/grpc/account/v2/get-nex-password.ts
Normal file
|
|
@ -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<DeepPartial<GetNEXPasswordResponse>> {
|
||||||
|
const nexAccount = await NEXAccount.findOne({ pid: request.pid });
|
||||||
|
|
||||||
|
if (!nexAccount) {
|
||||||
|
throw new ServerError(
|
||||||
|
Status.INVALID_ARGUMENT,
|
||||||
|
'No NEX account found'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
password: nexAccount.password
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,7 @@ import { getPNIDByPID } from '@/database';
|
||||||
import { PNID_PERMISSION_FLAGS } from '@/types/common/permission-flags';
|
import { PNID_PERMISSION_FLAGS } from '@/types/common/permission-flags';
|
||||||
import { config } from '@/config-manager';
|
import { config } from '@/config-manager';
|
||||||
import { Device } from '@/models/device';
|
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<GetUserDataResponse> {
|
export async function getUserData(request: GetUserDataRequest): Promise<GetUserDataResponse> {
|
||||||
const pnid = await getPNIDByPID(request.pid);
|
const pnid = await getPNIDByPID(request.pid);
|
||||||
|
|
@ -69,6 +69,6 @@ export async function getUserData(request: GetUserDataRequest): Promise<GetUserD
|
||||||
deleteBossFiles: pnid.hasPermission(PNID_PERMISSION_FLAGS.DELETE_BOSS_FILES),
|
deleteBossFiles: pnid.hasPermission(PNID_PERMISSION_FLAGS.DELETE_BOSS_FILES),
|
||||||
updatePnidPermissions: pnid.hasPermission(PNID_PERMISSION_FLAGS.UPDATE_PNID_PERMISSIONS)
|
updatePnidPermissions: pnid.hasPermission(PNID_PERMISSION_FLAGS.UPDATE_PNID_PERMISSIONS)
|
||||||
},
|
},
|
||||||
devices
|
linkedDevices: devices
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
13
src/services/grpc/account/v2/implementation.ts
Normal file
13
src/services/grpc/account/v2/implementation.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { getUserData } from '@/services/grpc/account/v2/get-user-data';
|
||||||
|
import { getNEXPassword } from '@/services/grpc/account/v2/get-nex-password';
|
||||||
|
import { getNEXData } from '@/services/grpc/account/v2/get-nex-data';
|
||||||
|
import { updatePNIDPermissions } from '@/services/grpc/account/v2/update-pnid-permissions';
|
||||||
|
import { exchangeTokenForUserData } from '@/services/grpc/account/v2/exchange-token-for-user-data';
|
||||||
|
|
||||||
|
export const accountServiceImplementationV2 = {
|
||||||
|
getUserData,
|
||||||
|
getNEXPassword,
|
||||||
|
getNEXData,
|
||||||
|
updatePNIDPermissions,
|
||||||
|
exchangeTokenForUserData
|
||||||
|
};
|
||||||
159
src/services/grpc/account/v2/update-pnid-permissions.ts
Normal file
159
src/services/grpc/account/v2/update-pnid-permissions.ts
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
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/v2/update_pnid_permissions';
|
||||||
|
import type { Empty } from '@pretendonetwork/grpc/google/protobuf/empty';
|
||||||
|
|
||||||
|
export async function updatePNIDPermissions(request: UpdatePNIDPermissionsRequest): Promise<Empty> {
|
||||||
|
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 {};
|
||||||
|
}
|
||||||
|
|
@ -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
|
|
||||||
};
|
|
||||||
|
|
@ -3,7 +3,7 @@ import validator from 'validator';
|
||||||
import { getPNIDByEmailAddress, getPNIDByUsername } from '@/database';
|
import { getPNIDByEmailAddress, getPNIDByUsername } from '@/database';
|
||||||
import { sendForgotPasswordEmail } from '@/util';
|
import { sendForgotPasswordEmail } from '@/util';
|
||||||
import type { ForgotPasswordRequest } from '@pretendonetwork/grpc/api/forgot_password_rpc';
|
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';
|
import type { HydratedPNIDDocument } from '@/types/mongoose/pnid';
|
||||||
|
|
||||||
export async function forgotPassword(request: ForgotPasswordRequest): Promise<Empty> {
|
export async function forgotPassword(request: ForgotPasswordRequest): Promise<Empty> {
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { config } from '@/config-manager';
|
import { config } from '@/config-manager';
|
||||||
import type { CallContext } from 'nice-grpc';
|
import type { CallContext } from 'nice-grpc';
|
||||||
import type { GetUserDataResponse, DeepPartial } from '@pretendonetwork/grpc/api/get_user_data_rpc';
|
import type { GetUserDataResponse, DeepPartial } from '@pretendonetwork/grpc/api/get_user_data_rpc';
|
||||||
import type { Empty } from '@pretendonetwork/grpc/api/google/protobuf/empty';
|
import type { Empty } from '@pretendonetwork/grpc/google/protobuf/empty';
|
||||||
import type { AuthenticationCallContextExt } from '@/services/grpc/api/authentication-middleware';
|
import type { AuthenticationCallContextExt } from '@/services/grpc/api/v1/authentication-middleware';
|
||||||
|
|
||||||
export async function getUserData(_request: Empty, context: CallContext & AuthenticationCallContextExt): Promise<DeepPartial<GetUserDataResponse>> {
|
export async function getUserData(_request: Empty, context: CallContext & AuthenticationCallContextExt): Promise<DeepPartial<GetUserDataResponse>> {
|
||||||
// * This is asserted in authentication-middleware, we know this is never null
|
// * This is asserted in authentication-middleware, we know this is never null
|
||||||
19
src/services/grpc/api/v1/implementation.ts
Normal file
19
src/services/grpc/api/v1/implementation.ts
Normal file
|
|
@ -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
|
||||||
|
};
|
||||||
|
|
@ -3,7 +3,7 @@ import { Status, ServerError } from 'nice-grpc';
|
||||||
import { decryptToken, unpackToken, nintendoPasswordHash } from '@/util';
|
import { decryptToken, unpackToken, nintendoPasswordHash } from '@/util';
|
||||||
import { getPNIDByPID } from '@/database';
|
import { getPNIDByPID } from '@/database';
|
||||||
import type { ResetPasswordRequest } from '@pretendonetwork/grpc/api/reset_password_rpc';
|
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';
|
import type { Token } from '@/types/common/token';
|
||||||
|
|
||||||
// * This sucks
|
// * This sucks
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Status, ServerError } from 'nice-grpc';
|
import { Status, ServerError } from 'nice-grpc';
|
||||||
import type { CallContext } from 'nice-grpc';
|
import type { CallContext } from 'nice-grpc';
|
||||||
import type { SetDiscordConnectionDataRequest } from '@pretendonetwork/grpc/api/set_discord_connection_data_rpc';
|
import type { SetDiscordConnectionDataRequest } from '@pretendonetwork/grpc/api/set_discord_connection_data_rpc';
|
||||||
import type { Empty } from '@pretendonetwork/grpc/api/google/protobuf/empty';
|
import type { Empty } from '@pretendonetwork/grpc/google/protobuf/empty';
|
||||||
import type { AuthenticationCallContextExt } from '@/services/grpc/api/authentication-middleware';
|
import type { AuthenticationCallContextExt } from '@/services/grpc/api/v1/authentication-middleware';
|
||||||
|
|
||||||
export async function setDiscordConnectionData(request: SetDiscordConnectionDataRequest, context: CallContext & AuthenticationCallContextExt): Promise<Empty> {
|
export async function setDiscordConnectionData(request: SetDiscordConnectionDataRequest, context: CallContext & AuthenticationCallContextExt): Promise<Empty> {
|
||||||
// * This is asserted in authentication-middleware, we know this is never null
|
// * This is asserted in authentication-middleware, we know this is never null
|
||||||
|
|
@ -2,8 +2,8 @@ import { Status, ServerError } from 'nice-grpc';
|
||||||
import { PNID } from '@/models/pnid';
|
import { PNID } from '@/models/pnid';
|
||||||
import type { CallContext } from 'nice-grpc';
|
import type { CallContext } from 'nice-grpc';
|
||||||
import type { SetStripeConnectionDataRequest } from '@pretendonetwork/grpc/api/set_stripe_connection_data_rpc';
|
import type { SetStripeConnectionDataRequest } from '@pretendonetwork/grpc/api/set_stripe_connection_data_rpc';
|
||||||
import type { Empty } from '@pretendonetwork/grpc/api/google/protobuf/empty';
|
import type { Empty } from '@pretendonetwork/grpc/google/protobuf/empty';
|
||||||
import type { AuthenticationCallContextExt } from '@/services/grpc/api/authentication-middleware';
|
import type { AuthenticationCallContextExt } from '@/services/grpc/api/v1/authentication-middleware';
|
||||||
|
|
||||||
type StripeMongoUpdateScheme = {
|
type StripeMongoUpdateScheme = {
|
||||||
'access_level'?: number;
|
'access_level'?: number;
|
||||||
|
|
@ -28,16 +28,6 @@ export async function setStripeConnectionData(request: SetStripeConnectionDataRe
|
||||||
updateData['connections.stripe.customer_id'] = request.customerId;
|
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) {
|
if (request.subscriptionId !== undefined) {
|
||||||
updateData['connections.stripe.subscription_id'] = request.subscriptionId;
|
updateData['connections.stripe.subscription_id'] = request.subscriptionId;
|
||||||
}
|
}
|
||||||
|
|
@ -2,7 +2,7 @@ import { config } from '@/config-manager';
|
||||||
import type { CallContext } from 'nice-grpc';
|
import type { CallContext } from 'nice-grpc';
|
||||||
import type { UpdateUserDataRequest, DeepPartial } from '@pretendonetwork/grpc/api/update_user_data_rpc';
|
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 { 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<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
|
// * This is asserted in authentication-middleware, we know this is never null
|
||||||
16
src/services/grpc/api/v2/api-key-middleware.ts
Normal file
16
src/services/grpc/api/v2/api-key-middleware.ts
Normal file
|
|
@ -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<Request, Response>(
|
||||||
|
call: ServerMiddlewareCall<Request, Response>,
|
||||||
|
context: CallContext
|
||||||
|
): AsyncGenerator<Response, Response | void, undefined> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
56
src/services/grpc/api/v2/authentication-middleware.ts
Normal file
56
src/services/grpc/api/v2/authentication-middleware.ts
Normal file
|
|
@ -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<Request, Response>(
|
||||||
|
call: ServerMiddlewareCall<Request, Response, AuthenticationCallContextExt>,
|
||||||
|
context: CallContext
|
||||||
|
): AsyncGenerator<Response, Response | void, undefined> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/services/grpc/api/v2/forgot-password.ts
Normal file
29
src/services/grpc/api/v2/forgot-password.ts
Normal file
|
|
@ -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<Empty> {
|
||||||
|
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 {};
|
||||||
|
}
|
||||||
45
src/services/grpc/api/v2/get-user-data.ts
Normal file
45
src/services/grpc/api/v2/get-user-data.ts
Normal file
|
|
@ -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<DeepPartial<GetUserDataResponse>> {
|
||||||
|
// * 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
|
||||||
|
};
|
||||||
|
}
|
||||||
19
src/services/grpc/api/v2/implementation.ts
Normal file
19
src/services/grpc/api/v2/implementation.ts
Normal file
|
|
@ -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
|
||||||
|
};
|
||||||
89
src/services/grpc/api/v2/login.ts
Normal file
89
src/services/grpc/api/v2/login.ts
Normal file
|
|
@ -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<DeepPartial<LoginResponse>> {
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
264
src/services/grpc/api/v2/register.ts
Normal file
264
src/services/grpc/api/v2/register.ts
Normal file
|
|
@ -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<DeepPartial<LoginResponse>> {
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
77
src/services/grpc/api/v2/reset-password.ts
Normal file
77
src/services/grpc/api/v2/reset-password.ts
Normal file
|
|
@ -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<ResetPasswordResponse> {
|
||||||
|
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 {};
|
||||||
|
}
|
||||||
25
src/services/grpc/api/v2/set-discord-connection-data.ts
Normal file
25
src/services/grpc/api/v2/set-discord-connection-data.ts
Normal file
|
|
@ -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<SetDiscordConnectionDataResponse> {
|
||||||
|
// * 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 {};
|
||||||
|
}
|
||||||
80
src/services/grpc/api/v2/set-stripe-connection-data.ts
Normal file
80
src/services/grpc/api/v2/set-stripe-connection-data.ts
Normal file
|
|
@ -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<SetStripeConnectionDataResponse> {
|
||||||
|
// * 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 {};
|
||||||
|
}
|
||||||
47
src/services/grpc/api/v2/update-user-data.ts
Normal file
47
src/services/grpc/api/v2/update-user-data.ts
Normal file
|
|
@ -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<DeepPartial<GetUserDataResponse>> {
|
||||||
|
// * 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,18 +1,28 @@
|
||||||
import { createServer } from 'nice-grpc';
|
import { createServer } from 'nice-grpc';
|
||||||
import { AccountDefinition } from '@pretendonetwork/grpc/account/account_service';
|
import { AccountDefinition as AccountServiceDefinitionV1 } from '@pretendonetwork/grpc/account/account_service';
|
||||||
import { APIDefinition } from '@pretendonetwork/grpc/api/api_service';
|
import { APIDefinition as ApiServiceDefinitionV1 } from '@pretendonetwork/grpc/api/api_service';
|
||||||
import { apiKeyMiddleware as accountApiKeyMiddleware } from '@/services/grpc/account/api-key-middleware';
|
import { apiKeyMiddleware as accountApiKeyMiddlewareV1 } from '@/services/grpc/account/v1/api-key-middleware';
|
||||||
import { apiKeyMiddleware as apiApiKeyMiddleware } from '@/services/grpc/api/api-key-middleware';
|
import { apiKeyMiddleware as apiApiKeyMiddlewareV1 } from '@/services/grpc/api/v1/api-key-middleware';
|
||||||
import { authenticationMiddleware as apiAuthenticationMiddleware } from '@/services/grpc/api/authentication-middleware';
|
import { authenticationMiddleware as apiAuthenticationMiddlewareV1 } from '@/services/grpc/api/v1/authentication-middleware';
|
||||||
import { accountServiceImplementation } from '@/services/grpc/account/implementation';
|
import { accountServiceImplementationV1 } from '@/services/grpc/account/v1/implementation';
|
||||||
import { apiServiceImplementation } from '@/services/grpc/api/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';
|
import { config } from '@/config-manager';
|
||||||
|
|
||||||
export async function startGRPCServer(): Promise<void> {
|
export async function startGRPCServer(): Promise<void> {
|
||||||
const server = createServer();
|
const server = createServer();
|
||||||
|
|
||||||
server.with(accountApiKeyMiddleware).add(AccountDefinition, accountServiceImplementation);
|
server.with(accountApiKeyMiddlewareV1).add(AccountServiceDefinitionV1, accountServiceImplementationV1);
|
||||||
server.with(apiApiKeyMiddleware).with(apiAuthenticationMiddleware).add(APIDefinition, apiServiceImplementation);
|
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}`);
|
await server.listen(`0.0.0.0:${config.grpc.port}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user