mirror of
https://github.com/PretendoNetwork/account.git
synced 2026-03-21 17:44:49 -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
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -62,3 +62,4 @@ config.json
|
|||
certs
|
||||
cdn
|
||||
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-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": {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 { 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<Empty> {
|
||||
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 { 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<GetUserDataResponse> {
|
||||
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
|
||||
};
|
||||
}
|
||||
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 { 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<GetUserDataResponse> {
|
||||
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),
|
||||
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 { 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<Empty> {
|
||||
|
|
@ -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<DeepPartial<GetUserDataResponse>> {
|
||||
// * 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 { 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
|
||||
|
|
@ -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<Empty> {
|
||||
// * 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 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;
|
||||
}
|
||||
|
|
@ -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<DeepPartial<GetUserDataResponse>> {
|
||||
// * 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 { 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<void> {
|
||||
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}`);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user