mirror of
https://github.com/PretendoNetwork/account.git
synced 2026-08-20 09:44:06 -05:00
Merge pull request #313 from PretendoNetwork/feat/grpc-tokens
Opaque tokens
This commit is contained in:
3439
package-lock.json
generated
3439
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,8 @@
|
||||
"description": "",
|
||||
"main": "./dist/server.js",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint --fix .",
|
||||
"lint": "eslint ./src",
|
||||
"lint:fix": "eslint --fix ./src",
|
||||
"download-geoip-db": "node ./scripts/download-ip2location-databases.js",
|
||||
"build": "npm run lint && npm run clean && npx tsc && npx tsc-alias && npm run copy-static && npm run download-geoip-db",
|
||||
"clean": "rimraf ./dist",
|
||||
@@ -90,5 +90,8 @@
|
||||
"globals": "^15.14.0",
|
||||
"ndarray": "^1.0.19",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"overrides": {
|
||||
"@bufbuild/protobuf": "2.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import crypto from 'node:crypto';
|
||||
import mongoose from 'mongoose';
|
||||
import bcrypt from 'bcrypt';
|
||||
import joi from 'joi';
|
||||
import { nintendoPasswordHash, decryptToken, unpackToken } from '@/util';
|
||||
import { nintendoPasswordHash } from '@/util';
|
||||
import { OAuthToken } from '@/models/oauth-token';
|
||||
import { PNID } from '@/models/pnid';
|
||||
import { Server } from '@/models/server';
|
||||
import { LOG_ERROR } from '@/logger';
|
||||
@@ -110,26 +112,37 @@ async function getPNIDByOAuthToken(token: string, expectedSystemType: SystemType
|
||||
verifyConnected();
|
||||
|
||||
try {
|
||||
const decryptedToken = decryptToken(Buffer.from(token, 'hex'));
|
||||
const unpackedToken = unpackToken(decryptedToken);
|
||||
const oauthToken = await OAuthToken.findOne({
|
||||
token: crypto.createHash('sha256').update(token).digest('hex')
|
||||
});
|
||||
|
||||
if (unpackedToken.system_type !== expectedSystemType) {
|
||||
return null;
|
||||
}
|
||||
if (unpackedToken.token_type !== expectedTokenType) {
|
||||
if (!oauthToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pnid = await getPNIDByPID(unpackedToken.pid);
|
||||
if (oauthToken.info.system_type !== expectedSystemType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (oauthToken.info.token_type !== expectedTokenType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pnid = await getPNIDByPID(oauthToken.pid);
|
||||
|
||||
if (pnid) {
|
||||
const expireTime = Math.floor((Number(unpackedToken.expire_time) / 1000));
|
||||
const expireTime = Math.floor((Number(oauthToken.info.expires) / 1000));
|
||||
|
||||
if (Math.floor(Date.now() / 1000) > expireTime) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// * Refresh tokens are single use
|
||||
if (expectedTokenType === TokenType.OAuthRefresh) {
|
||||
await oauthToken.deleteOne();
|
||||
}
|
||||
|
||||
return pnid;
|
||||
} catch (error: any) {
|
||||
// TODO - Handle error
|
||||
|
||||
@@ -2,6 +2,7 @@ import { getValueFromHeaders } from '@/util';
|
||||
import { getPNIDByAPIAccessToken } from '@/database';
|
||||
import { LOG_ERROR } from '@/logger';
|
||||
import type express from 'express';
|
||||
|
||||
async function APIMiddleware(request: express.Request, _response: express.Response, next: express.NextFunction): Promise<void> {
|
||||
const authHeader = getValueFromHeaders(request.headers, 'authorization');
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@ import { connection as databaseConnection } from '@/database';
|
||||
import NintendoCertificate from '@/nintendo-certificate';
|
||||
import { LOG_ERROR } from '@/logger';
|
||||
import type express from 'express';
|
||||
import type { NASCRequestParams } from '@/types/services/nasc/request-params';
|
||||
import type { NASCACRequestParams } from '@/types/services/nasc/ac-request-params';
|
||||
|
||||
async function NASCMiddleware(request: express.Request, response: express.Response, next: express.NextFunction): Promise<void> {
|
||||
const requestParams: NASCRequestParams = request.body;
|
||||
const requestParams: NASCACRequestParams = request.body;
|
||||
|
||||
if (!requestParams.action ||
|
||||
!requestParams.fcdcert ||
|
||||
@@ -37,15 +37,15 @@ async function NASCMiddleware(request: express.Request, response: express.Respon
|
||||
let pidHmac = '';
|
||||
let password = '';
|
||||
|
||||
if (requestParams.userid) {
|
||||
if ('userid' in requestParams) {
|
||||
pid = Number(nintendoBase64Decode(requestParams.userid).toString());
|
||||
}
|
||||
|
||||
if (requestParams.uidhmac) {
|
||||
if ('uidhmac' in requestParams) {
|
||||
pidHmac = nintendoBase64Decode(requestParams.uidhmac).toString();
|
||||
}
|
||||
|
||||
if (requestParams.passwd) {
|
||||
if ('passwd' in requestParams) {
|
||||
password = nintendoBase64Decode(requestParams.passwd).toString();
|
||||
}
|
||||
|
||||
|
||||
45
src/models/independent-service-token.ts
Normal file
45
src/models/independent-service-token.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Schema, model } from 'mongoose';
|
||||
import type { Model, HydratedDocument } from 'mongoose';
|
||||
|
||||
// TODO - Move these to the types folder, just putting them here to push this out the door
|
||||
export interface IIndependentServiceToken {
|
||||
token: string;
|
||||
client_id: string;
|
||||
title_id: string;
|
||||
pid: number;
|
||||
info: {
|
||||
system_type: number;
|
||||
token_type: number;
|
||||
title_id: bigint;
|
||||
issued: Date;
|
||||
expires: Date;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IIndependentServiceTokenMethods {}
|
||||
|
||||
interface IIndependentServiceTokenQueryHelpers {}
|
||||
|
||||
export interface IndependentServiceTokenModel extends Model<IIndependentServiceToken, IIndependentServiceTokenQueryHelpers, IIndependentServiceTokenMethods> {}
|
||||
|
||||
export type HydratedIndependentServiceTokenDocument = HydratedDocument<IIndependentServiceToken, IIndependentServiceTokenMethods>;
|
||||
|
||||
const IndependentServiceTokenSchema = new Schema<IIndependentServiceToken, IndependentServiceTokenModel, IIndependentServiceTokenMethods>({
|
||||
token: String,
|
||||
client_id: String,
|
||||
title_id: String,
|
||||
pid: Number,
|
||||
info: {
|
||||
system_type: Number,
|
||||
token_type: Number,
|
||||
title_id: BigInt,
|
||||
issued: Date,
|
||||
expires: Date
|
||||
}
|
||||
});
|
||||
|
||||
IndependentServiceTokenSchema.index({ 'info.expires': 1 }, { expireAfterSeconds: 0 });
|
||||
IndependentServiceTokenSchema.index({ token: 1 });
|
||||
IndependentServiceTokenSchema.index({ pid: 1 });
|
||||
|
||||
export const IndependentServiceToken = model<IIndependentServiceToken, IndependentServiceTokenModel>('IndependentServiceToken', IndependentServiceTokenSchema);
|
||||
43
src/models/nex-token.ts
Normal file
43
src/models/nex-token.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Schema, model } from 'mongoose';
|
||||
import type { Model, HydratedDocument } from 'mongoose';
|
||||
|
||||
// TODO - Move these to the types folder, just putting them here to push this out the door
|
||||
export interface INEXToken {
|
||||
token: string;
|
||||
game_server_id: string;
|
||||
pid: number;
|
||||
info: {
|
||||
system_type: number;
|
||||
token_type: number;
|
||||
title_id: bigint;
|
||||
issued: Date;
|
||||
expires: Date;
|
||||
};
|
||||
}
|
||||
|
||||
export interface INEXTokenMethods {}
|
||||
|
||||
interface INEXTokenQueryHelpers {}
|
||||
|
||||
export interface NEXTokenModel extends Model<INEXToken, INEXTokenQueryHelpers, INEXTokenMethods> {}
|
||||
|
||||
export type HydratedNEXTokenDocument = HydratedDocument<INEXToken, INEXTokenMethods>;
|
||||
|
||||
const NEXTokenSchema = new Schema<INEXToken, NEXTokenModel, INEXTokenMethods>({
|
||||
token: String,
|
||||
game_server_id: String,
|
||||
pid: Number,
|
||||
info: {
|
||||
system_type: Number,
|
||||
token_type: Number,
|
||||
title_id: BigInt,
|
||||
issued: Date,
|
||||
expires: Date
|
||||
}
|
||||
});
|
||||
|
||||
NEXTokenSchema.index({ 'info.expires': 1 }, { expireAfterSeconds: 0 });
|
||||
NEXTokenSchema.index({ token: 1 });
|
||||
NEXTokenSchema.index({ pid: 1 });
|
||||
|
||||
export const NEXToken = model<INEXToken, NEXTokenModel>('NEXToken', NEXTokenSchema);
|
||||
45
src/models/oauth-token.ts
Normal file
45
src/models/oauth-token.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Schema, model } from 'mongoose';
|
||||
import type { Model, HydratedDocument } from 'mongoose';
|
||||
|
||||
// TODO - Move these to the types folder, just putting them here to push this out the door
|
||||
export interface IOAuthToken {
|
||||
token: string;
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
pid: number;
|
||||
info: {
|
||||
system_type: number;
|
||||
token_type: number;
|
||||
title_id: bigint;
|
||||
issued: Date;
|
||||
expires: Date;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IOAuthTokenMethods {}
|
||||
|
||||
interface IOAuthTokenQueryHelpers {}
|
||||
|
||||
export interface OAuthTokenModel extends Model<IOAuthToken, IOAuthTokenQueryHelpers, IOAuthTokenMethods> {}
|
||||
|
||||
export type HydratedOAuthTokenDocument = HydratedDocument<IOAuthToken, IOAuthTokenMethods>;
|
||||
|
||||
const OAuthTokenSchema = new Schema<IOAuthToken, OAuthTokenModel, IOAuthTokenMethods>({
|
||||
token: String,
|
||||
client_id: String,
|
||||
client_secret: String,
|
||||
pid: Number,
|
||||
info: {
|
||||
system_type: Number,
|
||||
token_type: Number,
|
||||
title_id: BigInt,
|
||||
issued: Date,
|
||||
expires: Date
|
||||
}
|
||||
});
|
||||
|
||||
OAuthTokenSchema.index({ 'info.expires': 1 }, { expireAfterSeconds: 0 });
|
||||
OAuthTokenSchema.index({ token: 1 });
|
||||
OAuthTokenSchema.index({ pid: 1 });
|
||||
|
||||
export const OAuthToken = model<IOAuthToken, OAuthTokenModel>('OAuthToken', OAuthTokenSchema);
|
||||
41
src/models/password-reset-token.ts
Normal file
41
src/models/password-reset-token.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Schema, model } from 'mongoose';
|
||||
import type { Model, HydratedDocument } from 'mongoose';
|
||||
|
||||
// TODO - Move these to the types folder, just putting them here to push this out the door
|
||||
export interface IPasswordResetToken {
|
||||
token: string;
|
||||
pid: number;
|
||||
info: {
|
||||
system_type: number;
|
||||
token_type: number;
|
||||
title_id: bigint;
|
||||
issued: Date;
|
||||
expires: Date;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IPasswordResetTokenMethods {}
|
||||
|
||||
interface IPasswordResetTokenQueryHelpers {}
|
||||
|
||||
export interface PasswordResetTokenModel extends Model<IPasswordResetToken, IPasswordResetTokenQueryHelpers, IPasswordResetTokenMethods> {}
|
||||
|
||||
export type HydratedPasswordResetTokenDocument = HydratedDocument<IPasswordResetToken, IPasswordResetTokenMethods>;
|
||||
|
||||
const PasswordResetTokenSchema = new Schema<IPasswordResetToken, PasswordResetTokenModel, IPasswordResetTokenMethods>({
|
||||
token: String,
|
||||
pid: Number,
|
||||
info: {
|
||||
system_type: Number,
|
||||
token_type: Number,
|
||||
title_id: BigInt,
|
||||
issued: Date,
|
||||
expires: Date
|
||||
}
|
||||
});
|
||||
|
||||
PasswordResetTokenSchema.index({ 'info.expires': 1 }, { expireAfterSeconds: 0 });
|
||||
PasswordResetTokenSchema.index({ token: 1 });
|
||||
PasswordResetTokenSchema.index({ pid: 1 });
|
||||
|
||||
export const PasswordResetToken = model<IPasswordResetToken, PasswordResetTokenModel>('PasswordResetToken', PasswordResetTokenSchema);
|
||||
@@ -12,6 +12,10 @@ import { DeviceSchema } from '@/models/device';
|
||||
import { uploadCDNAsset } from '@/util';
|
||||
import { LOG_ERROR, LOG_WARN } from '@/logger';
|
||||
import { config } from '@/config-manager';
|
||||
import { IndependentServiceToken } from '@/models/independent-service-token';
|
||||
import { NEXToken } from '@/models/nex-token';
|
||||
import { OAuthToken } from '@/models/oauth-token';
|
||||
import { PasswordResetToken } from '@/models/password-reset-token';
|
||||
import type { IPNID, IPNIDMethods, PNIDModel } from '@/types/mongoose/pnid';
|
||||
import type { PNIDPermissionFlag } from '@/types/common/permission-flags';
|
||||
|
||||
@@ -368,6 +372,15 @@ PNIDSchema.method('scrub', async function scrub() {
|
||||
await this.save();
|
||||
});
|
||||
|
||||
PNIDSchema.method('removeAllTokens', async function removeAllTokens() {
|
||||
await Promise.all([
|
||||
IndependentServiceToken.deleteMany({ pid: this.pid }),
|
||||
NEXToken.deleteMany({ pid: this.pid }),
|
||||
OAuthToken.deleteMany({ pid: this.pid }),
|
||||
PasswordResetToken.deleteMany({ pid: this.pid })
|
||||
]);
|
||||
});
|
||||
|
||||
PNIDSchema.method('hasPermission', function hasPermission(flag: PNIDPermissionFlag): boolean {
|
||||
return (this.permissions & flag) === flag;
|
||||
});
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import crypto from 'node:crypto';
|
||||
import express from 'express';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { getPNIDByUsername, getPNIDByAPIRefreshToken } from '@/database';
|
||||
import { nintendoPasswordHash, generateToken } from '@/util';
|
||||
import { nintendoPasswordHash } from '@/util';
|
||||
import { OAuthToken } from '@/models/oauth-token';
|
||||
import { SystemType } from '@/types/common/system-types';
|
||||
import { TokenType } from '@/types/common/token-types';
|
||||
import { config } from '@/config-manager';
|
||||
import { LOG_ERROR } from '@/logger';
|
||||
import type { HydratedPNIDDocument } from '@/types/mongoose/pnid';
|
||||
|
||||
@@ -112,30 +113,37 @@ router.post('/', async (request: express.Request, response: express.Response): P
|
||||
return;
|
||||
}
|
||||
|
||||
const accessTokenOptions = {
|
||||
system_type: SystemType.API,
|
||||
token_type: TokenType.OAuthAccess,
|
||||
pid: pnid.pid,
|
||||
access_level: pnid.access_level,
|
||||
title_id: BigInt(0),
|
||||
expire_time: BigInt(Date.now() + (3600 * 1000))
|
||||
};
|
||||
|
||||
const refreshTokenOptions = {
|
||||
system_type: SystemType.API,
|
||||
token_type: TokenType.OAuthRefresh,
|
||||
pid: pnid.pid,
|
||||
access_level: pnid.access_level,
|
||||
title_id: BigInt(0),
|
||||
expire_time: BigInt(Date.now() + 12 * 3600 * 1000)
|
||||
};
|
||||
|
||||
try {
|
||||
const accessTokenBuffer = await generateToken(config.aes_key, accessTokenOptions);
|
||||
const refreshTokenBuffer = await generateToken(config.aes_key, refreshTokenOptions);
|
||||
const accessToken = crypto.randomBytes(16).toString('hex');
|
||||
const newRefreshToken = crypto.randomBytes(20).toString('hex');
|
||||
|
||||
const accessToken = accessTokenBuffer ? accessTokenBuffer.toString('hex') : '';
|
||||
const newRefreshToken = refreshTokenBuffer ? refreshTokenBuffer.toString('hex') : '';
|
||||
await OAuthToken.create({
|
||||
token: crypto.createHash('sha256').update(accessToken).digest('hex'),
|
||||
client_id: 'a2efa818a34fa16b8afbc8a74eba3eda', // TODO - This is the Wii U config, change this?
|
||||
client_secret: 'c91cdb5658bd4954ade78533a339cf9a', // TODO - This is the Wii U config, change this?
|
||||
pid: pnid.pid,
|
||||
info: {
|
||||
system_type: SystemType.API,
|
||||
token_type: TokenType.OAuthAccess,
|
||||
title_id: BigInt(0),
|
||||
issued: new Date(),
|
||||
expires: new Date(Date.now() + (3600 * 1000))
|
||||
}
|
||||
});
|
||||
|
||||
await OAuthToken.create({
|
||||
token: crypto.createHash('sha256').update(newRefreshToken).digest('hex'),
|
||||
client_id: 'a2efa818a34fa16b8afbc8a74eba3eda', // TODO - This is the Wii U config, change this?
|
||||
client_secret: 'c91cdb5658bd4954ade78533a339cf9a', // TODO - This is the Wii U config, change this?
|
||||
pid: pnid.pid,
|
||||
info: {
|
||||
system_type: SystemType.API,
|
||||
token_type: TokenType.OAuthRefresh,
|
||||
title_id: BigInt(0),
|
||||
issued: new Date(),
|
||||
expires: new Date(Date.now() + 12 * 3600 * 1000)
|
||||
}
|
||||
});
|
||||
|
||||
// TODO - Handle null tokens
|
||||
|
||||
|
||||
@@ -6,12 +6,13 @@ import moment from 'moment';
|
||||
import hcaptcha from 'hcaptcha';
|
||||
import Mii from 'mii-js';
|
||||
import { doesPNIDExist, connection as databaseConnection } from '@/database';
|
||||
import { isValidBirthday, getAgeFromDate, nintendoPasswordHash, sendConfirmationEmail, generateToken } from '@/util';
|
||||
import { isValidBirthday, getAgeFromDate, nintendoPasswordHash, sendConfirmationEmail } from '@/util';
|
||||
import IP2LocationManager from '@/ip2location';
|
||||
import { SystemType } from '@/types/common/system-types';
|
||||
import { TokenType } from '@/types/common/token-types';
|
||||
import { LOG_ERROR } from '@/logger';
|
||||
import { PNID } from '@/models/pnid';
|
||||
import { OAuthToken } from '@/models/oauth-token';
|
||||
import { NEXAccount } from '@/models/nex-account';
|
||||
import { config, disabledFeatures } from '@/config-manager';
|
||||
import type { HydratedNEXAccountDocument } from '@/types/mongoose/nex-account';
|
||||
@@ -435,32 +436,37 @@ router.post('/', async (request: express.Request, response: express.Response): P
|
||||
|
||||
await sendConfirmationEmail(pnid);
|
||||
|
||||
const accessTokenOptions = {
|
||||
system_type: SystemType.API,
|
||||
token_type: TokenType.OAuthAccess,
|
||||
pid: pnid.pid,
|
||||
access_level: pnid.access_level,
|
||||
title_id: BigInt(0),
|
||||
expire_time: BigInt(Date.now() + (3600 * 1000))
|
||||
};
|
||||
|
||||
const refreshTokenOptions = {
|
||||
system_type: SystemType.API,
|
||||
token_type: TokenType.OAuthRefresh,
|
||||
pid: pnid.pid,
|
||||
access_level: pnid.access_level,
|
||||
title_id: BigInt(0),
|
||||
expire_time: BigInt(Date.now() + 12 * 3600 * 1000)
|
||||
};
|
||||
|
||||
try {
|
||||
const accessTokenBuffer = await generateToken(config.aes_key, accessTokenOptions);
|
||||
const refreshTokenBuffer = await generateToken(config.aes_key, refreshTokenOptions);
|
||||
const accessToken = crypto.randomBytes(16).toString('hex');
|
||||
const refreshToken = crypto.randomBytes(20).toString('hex');
|
||||
|
||||
const accessToken = accessTokenBuffer ? accessTokenBuffer.toString('hex') : '';
|
||||
const refreshToken = refreshTokenBuffer ? refreshTokenBuffer.toString('hex') : '';
|
||||
await OAuthToken.create({
|
||||
token: crypto.createHash('sha256').update(accessToken).digest('hex'),
|
||||
client_id: 'a2efa818a34fa16b8afbc8a74eba3eda', // TODO - This is the Wii U config, change this?
|
||||
client_secret: 'c91cdb5658bd4954ade78533a339cf9a', // TODO - This is the Wii U config, change this?
|
||||
pid: pnid.pid,
|
||||
info: {
|
||||
system_type: SystemType.API,
|
||||
token_type: TokenType.OAuthAccess,
|
||||
title_id: BigInt(0),
|
||||
issued: new Date(),
|
||||
expires: new Date(Date.now() + (3600 * 1000))
|
||||
}
|
||||
});
|
||||
|
||||
// TODO - Handle null tokens
|
||||
await OAuthToken.create({
|
||||
token: crypto.createHash('sha256').update(refreshToken).digest('hex'),
|
||||
client_id: 'a2efa818a34fa16b8afbc8a74eba3eda', // TODO - This is the Wii U config, change this?
|
||||
client_secret: 'c91cdb5658bd4954ade78533a339cf9a', // TODO - This is the Wii U config, change this?
|
||||
pid: pnid.pid,
|
||||
info: {
|
||||
system_type: SystemType.API,
|
||||
token_type: TokenType.OAuthRefresh,
|
||||
title_id: BigInt(0),
|
||||
issued: new Date(),
|
||||
expires: new Date(Date.now() + 12 * 3600 * 1000)
|
||||
}
|
||||
});
|
||||
|
||||
response.json({
|
||||
access_token: accessToken,
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import crypto from 'node:crypto';
|
||||
import express from 'express';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { PNID } from '@/models/pnid';
|
||||
import { decryptToken, unpackToken, nintendoPasswordHash } from '@/util';
|
||||
import type { Token } from '@/types/common/token';
|
||||
import { PasswordResetToken } from '@/models/password-reset-token';
|
||||
import { nintendoPasswordHash } from '@/util';
|
||||
import { SystemType } from '@/types/common/system-types';
|
||||
import { TokenType } from '@/types/common/token-types';
|
||||
import { getPNIDByPID } from '@/database';
|
||||
import type { HydratedPNIDDocument } from '@/types/mongoose/pnid';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -27,10 +31,53 @@ router.post('/', async (request: express.Request, response: express.Response): P
|
||||
return;
|
||||
}
|
||||
|
||||
let unpackedToken: Token;
|
||||
let pnid: HydratedPNIDDocument | null = null;
|
||||
try {
|
||||
const decryptedToken = await decryptToken(Buffer.from(token, 'hex'));
|
||||
unpackedToken = unpackToken(decryptedToken);
|
||||
const passwordResetToken = await PasswordResetToken.findOne({
|
||||
token: crypto.createHash('sha256').update(token).digest('hex')
|
||||
});
|
||||
|
||||
if (!passwordResetToken) {
|
||||
response.status(400).json({
|
||||
app: 'api',
|
||||
status: 400,
|
||||
error: 'Invalid token'
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (passwordResetToken.info.system_type !== SystemType.PasswordReset) {
|
||||
response.status(400).json({
|
||||
app: 'api',
|
||||
status: 400,
|
||||
error: 'Invalid token'
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (passwordResetToken.info.token_type !== TokenType.PasswordReset) {
|
||||
response.status(400).json({
|
||||
app: 'api',
|
||||
status: 400,
|
||||
error: 'Invalid token'
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (passwordResetToken.info.expires < new Date()) {
|
||||
response.status(400).json({
|
||||
app: 'api',
|
||||
status: 400,
|
||||
error: 'Token expired'
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
pnid = await getPNIDByPID(passwordResetToken.pid);
|
||||
} catch {
|
||||
response.status(400).json({
|
||||
app: 'api',
|
||||
@@ -41,18 +88,6 @@ router.post('/', async (request: express.Request, response: express.Response): P
|
||||
return;
|
||||
}
|
||||
|
||||
if (unpackedToken.expire_time < Date.now()) {
|
||||
response.status(400).json({
|
||||
app: 'api',
|
||||
status: 400,
|
||||
error: 'Token expired'
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const pnid = await PNID.findOne({ pid: unpackedToken.pid });
|
||||
|
||||
if (!pnid) {
|
||||
response.status(400).json({
|
||||
app: 'api',
|
||||
@@ -138,6 +173,7 @@ router.post('/', async (request: express.Request, response: express.Response): P
|
||||
|
||||
pnid.password = passwordHash;
|
||||
|
||||
await pnid.removeAllTokens();
|
||||
await pnid.save();
|
||||
|
||||
response.json({
|
||||
|
||||
9
src/services/grpc/account/v2/create-audit-log-comment.ts
Normal file
9
src/services/grpc/account/v2/create-audit-log-comment.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import type { CreateAuditLogCommentRequest, CreateAuditLogCommentResponse } from '@pretendonetwork/grpc/account/v2/create_audit_log_comment_rpc';
|
||||
|
||||
export async function createAuditLogComment(_request: CreateAuditLogCommentRequest): Promise<CreateAuditLogCommentResponse> {
|
||||
throw new ServerError(
|
||||
Status.UNIMPLEMENTED,
|
||||
'Stubbed'
|
||||
);
|
||||
}
|
||||
9
src/services/grpc/account/v2/create-ban-comment.ts
Normal file
9
src/services/grpc/account/v2/create-ban-comment.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import type { CreateBanCommentRequest, CreateBanCommentResponse } from '@pretendonetwork/grpc/account/v2/create_ban_comment_rpc';
|
||||
|
||||
export async function createBanComment(_request: CreateBanCommentRequest): Promise<CreateBanCommentResponse> {
|
||||
throw new ServerError(
|
||||
Status.UNIMPLEMENTED,
|
||||
'Stubbed'
|
||||
);
|
||||
}
|
||||
9
src/services/grpc/account/v2/create-server.ts
Normal file
9
src/services/grpc/account/v2/create-server.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import type { CreateServerRequest, CreateServerResponse } from '@pretendonetwork/grpc/account/v2/create_server_rpc';
|
||||
|
||||
export async function createServer(_request: CreateServerRequest): Promise<CreateServerResponse> {
|
||||
throw new ServerError(
|
||||
Status.UNIMPLEMENTED,
|
||||
'Stubbed'
|
||||
);
|
||||
}
|
||||
9
src/services/grpc/account/v2/delete-pnid.ts
Normal file
9
src/services/grpc/account/v2/delete-pnid.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import type { DeletePNIDRequest, DeletePNIDResponse } from '@pretendonetwork/grpc/account/v2/delete_pnid_rpc';
|
||||
|
||||
export async function deletePNID(_request: DeletePNIDRequest): Promise<DeletePNIDResponse> {
|
||||
throw new ServerError(
|
||||
Status.UNIMPLEMENTED,
|
||||
'Stubbed'
|
||||
);
|
||||
}
|
||||
9
src/services/grpc/account/v2/delete-server.ts
Normal file
9
src/services/grpc/account/v2/delete-server.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import type { DeleteServerRequest, DeleteServerResponse } from '@pretendonetwork/grpc/account/v2/delete_server_rpc';
|
||||
|
||||
export async function deleteServer(_request: DeleteServerRequest): Promise<DeleteServerResponse> {
|
||||
throw new ServerError(
|
||||
Status.UNIMPLEMENTED,
|
||||
'Stubbed'
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import { IndependentServiceToken } from '@/models/independent-service-token';
|
||||
import { Device } from '@/models/device';
|
||||
import { NEXAccount } from '@/models/nex-account';
|
||||
import { getPNIDByPID } from '@/database';
|
||||
import { PNID_PERMISSION_FLAGS } from '@/types/common/permission-flags';
|
||||
import { config } from '@/config-manager';
|
||||
import type { ExchangeIndependentServiceTokenForUserDataRequest, ExchangeIndependentServiceTokenForUserDataResponse } from '@pretendonetwork/grpc/account/v2/exchange_independent_service_token_for_user_data_rpc';
|
||||
import type { HydratedPNIDDocument } from '@/types/mongoose/pnid';
|
||||
import type { HydratedNEXAccountDocument } from '@/types/mongoose/nex-account';
|
||||
import type { HydratedIndependentServiceTokenDocument } from '@/models/independent-service-token';
|
||||
|
||||
export async function exchangeIndependentServiceTokenForUserData(request: ExchangeIndependentServiceTokenForUserDataRequest): Promise<ExchangeIndependentServiceTokenForUserDataResponse> {
|
||||
let pnid: HydratedPNIDDocument | null = null;
|
||||
let nexAccount: HydratedNEXAccountDocument | null = null;
|
||||
let serviceToken: HydratedIndependentServiceTokenDocument | null = null;
|
||||
try {
|
||||
serviceToken = await IndependentServiceToken.findOne({
|
||||
token: crypto.createHash('sha256').update(request.token).digest('hex')
|
||||
});
|
||||
|
||||
if (!serviceToken) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token');
|
||||
}
|
||||
|
||||
// TODO - Add checks for the client IDs and matching system/token types here
|
||||
|
||||
if (serviceToken.info.expires < new Date()) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token');
|
||||
}
|
||||
|
||||
pnid = await getPNIDByPID(serviceToken.pid);
|
||||
nexAccount = await NEXAccount.findOne({ pid: serviceToken.pid });
|
||||
} catch {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token');
|
||||
}
|
||||
|
||||
if (!nexAccount) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token. No user found');
|
||||
}
|
||||
|
||||
// TODO - Change this. This duplication sucks, but I wanna push this out the door
|
||||
if (pnid) {
|
||||
const devices = (await Device.find({
|
||||
linked_pids: pnid.pid
|
||||
})).map((device) => {
|
||||
return {
|
||||
model: device.get('model'), // * ".model" gives the Mongoose model
|
||||
serial: device.serial,
|
||||
linkedPids: device.linked_pids,
|
||||
accessLevel: device.access_level,
|
||||
serverAccessLevel: device.server_access_level,
|
||||
deviceId: device.device_id
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
pnid: {
|
||||
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)
|
||||
},
|
||||
linkedDevices: devices
|
||||
},
|
||||
nexAccount: {
|
||||
pid: nexAccount.pid,
|
||||
owningPid: nexAccount.owning_pid,
|
||||
accessLevel: nexAccount.access_level,
|
||||
serverAccessLevel: nexAccount.server_access_level,
|
||||
friendCode: nexAccount.friend_code,
|
||||
deviceType: nexAccount.device_type
|
||||
},
|
||||
tokenInfo: {
|
||||
systemType: serviceToken.info.system_type as any, // TODO - Stop the any usage
|
||||
tokenType: serviceToken.info.token_type as any, // TODO - Stop the any usage
|
||||
pid: BigInt(pnid.pid),
|
||||
accessLevel: pnid.access_level,
|
||||
titleId: serviceToken.info.title_id,
|
||||
issueTime: serviceToken.info.issued,
|
||||
expireTime: serviceToken.info.expires
|
||||
},
|
||||
basicUserInfo: {
|
||||
// TODO - ban: {}
|
||||
accessBetaServers: pnid.access_level === 1 || pnid.access_level === 2 || pnid.access_level === 3, // TODO - Remove with a better permission check later
|
||||
accessDeveloperServers: pnid.access_level === 3 // TODO - Remove with a better permission check later
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
pnid: undefined, // * Throws an error if this is not explictly set?
|
||||
nexAccount: {
|
||||
pid: nexAccount.pid,
|
||||
owningPid: nexAccount.owning_pid,
|
||||
accessLevel: nexAccount.access_level,
|
||||
serverAccessLevel: nexAccount.server_access_level,
|
||||
friendCode: nexAccount.friend_code,
|
||||
deviceType: nexAccount.device_type
|
||||
},
|
||||
tokenInfo: {
|
||||
systemType: serviceToken.info.system_type as any, // TODO - Stop the any usage
|
||||
tokenType: serviceToken.info.token_type as any, // TODO - Stop the any usage
|
||||
pid: BigInt(nexAccount.pid),
|
||||
accessLevel: nexAccount.access_level,
|
||||
titleId: serviceToken.info.title_id,
|
||||
issueTime: serviceToken.info.issued,
|
||||
expireTime: serviceToken.info.expires
|
||||
},
|
||||
basicUserInfo: {
|
||||
// TODO - ban: {}
|
||||
accessBetaServers: nexAccount.access_level === 1 || nexAccount.access_level === 2 || nexAccount.access_level === 3, // TODO - Remove with a better permission check later
|
||||
accessDeveloperServers: nexAccount.access_level === 3 // TODO - Remove with a better permission check later
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import { NEXToken } from '@/models/nex-token';
|
||||
import { NEXAccount } from '@/models/nex-account';
|
||||
import { getPNIDByPID } from '@/database';
|
||||
import type { ExchangeNEXTokenForUserDataRequest, ExchangeNEXTokenForUserDataResponse } from '@pretendonetwork/grpc/account/v2/exchange_nex_token_for_user_data_rpc';
|
||||
import type { HydratedNEXAccountDocument } from '@/types/mongoose/nex-account';
|
||||
import type { HydratedNEXTokenDocument } from '@/models/nex-token';
|
||||
|
||||
export async function exchangeNEXTokenForUserData(request: ExchangeNEXTokenForUserDataRequest): Promise<ExchangeNEXTokenForUserDataResponse> {
|
||||
let nexAccount: HydratedNEXAccountDocument | null = null;
|
||||
let nexToken: HydratedNEXTokenDocument | null = null;
|
||||
try {
|
||||
nexToken = await NEXToken.findOne({
|
||||
token: crypto.createHash('sha256').update(request.token).digest('hex')
|
||||
});
|
||||
|
||||
if (!nexToken) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token');
|
||||
}
|
||||
|
||||
// TODO - Add checks for the game server ID and matching system/token types here
|
||||
|
||||
if (nexToken.info.expires < new Date()) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token');
|
||||
}
|
||||
|
||||
nexAccount = await NEXAccount.findOne({ pid: nexToken.pid });
|
||||
} catch {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token');
|
||||
}
|
||||
|
||||
if (!nexAccount) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token. No user found');
|
||||
}
|
||||
|
||||
const pnid = await getPNIDByPID(nexAccount.owning_pid || nexAccount.pid);
|
||||
|
||||
if (pnid) {
|
||||
return {
|
||||
nexAccount: {
|
||||
pid: nexAccount.pid,
|
||||
owningPid: nexAccount.owning_pid,
|
||||
accessLevel: nexAccount.access_level,
|
||||
serverAccessLevel: nexAccount.server_access_level,
|
||||
friendCode: nexAccount.friend_code,
|
||||
deviceType: nexAccount.device_type
|
||||
},
|
||||
tokenInfo: {
|
||||
systemType: nexToken.info.system_type as any, // TODO - Stop the any usage
|
||||
tokenType: nexToken.info.token_type as any, // TODO - Stop the any usage
|
||||
pid: BigInt(nexAccount.pid),
|
||||
accessLevel: nexAccount.access_level,
|
||||
titleId: nexToken.info.title_id,
|
||||
issueTime: nexToken.info.issued,
|
||||
expireTime: nexToken.info.expires
|
||||
},
|
||||
basicUserInfo: {
|
||||
// TODO - ban: {}
|
||||
accessBetaServers: pnid.access_level === 1 || pnid.access_level === 2 || pnid.access_level === 3, // TODO - Remove with a better permission check later
|
||||
accessDeveloperServers: pnid.access_level === 3 // TODO - Remove with a better permission check later
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
nexAccount: {
|
||||
pid: nexAccount.pid,
|
||||
owningPid: nexAccount.owning_pid,
|
||||
accessLevel: nexAccount.access_level,
|
||||
serverAccessLevel: nexAccount.server_access_level,
|
||||
friendCode: nexAccount.friend_code,
|
||||
deviceType: nexAccount.device_type
|
||||
},
|
||||
tokenInfo: {
|
||||
systemType: nexToken.info.system_type as any, // TODO - Stop the any usage
|
||||
tokenType: nexToken.info.token_type as any, // TODO - Stop the any usage
|
||||
pid: BigInt(nexAccount.pid),
|
||||
accessLevel: nexAccount.access_level,
|
||||
titleId: nexToken.info.title_id,
|
||||
issueTime: nexToken.info.issued,
|
||||
expireTime: nexToken.info.expires
|
||||
},
|
||||
basicUserInfo: {
|
||||
// TODO - ban: {}
|
||||
accessBetaServers: nexAccount.access_level === 1 || nexAccount.access_level === 2 || nexAccount.access_level === 3, // TODO - Remove with a better permission check later
|
||||
accessDeveloperServers: nexAccount.access_level === 3 // TODO - Remove with a better permission check later
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import { OAuthToken } from '@/models/oauth-token';
|
||||
import { Device } from '@/models/device';
|
||||
import { NEXAccount } from '@/models/nex-account';
|
||||
import { getPNIDByPID } from '@/database';
|
||||
import { PNID_PERMISSION_FLAGS } from '@/types/common/permission-flags';
|
||||
import { config } from '@/config-manager';
|
||||
import type { ExchangeOAuthTokenForUserDataRequest, ExchangeOAuthTokenForUserDataResponse } from '@pretendonetwork/grpc/account/v2/exchange_oauth_token_for_user_data_rpc';
|
||||
import type { HydratedPNIDDocument } from '@/types/mongoose/pnid';
|
||||
import type { HydratedNEXAccountDocument } from '@/types/mongoose/nex-account';
|
||||
import type { HydratedOAuthTokenDocument } from '@/models/oauth-token';
|
||||
|
||||
export async function exchangeOAuthTokenForUserData(request: ExchangeOAuthTokenForUserDataRequest): Promise<ExchangeOAuthTokenForUserDataResponse> {
|
||||
let pnid: HydratedPNIDDocument | null = null;
|
||||
let nexAccount: HydratedNEXAccountDocument | null = null;
|
||||
let oAuthToken: HydratedOAuthTokenDocument | null = null;
|
||||
try {
|
||||
oAuthToken = await OAuthToken.findOne({
|
||||
token: crypto.createHash('sha256').update(request.token).digest('hex')
|
||||
});
|
||||
|
||||
if (!oAuthToken) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token');
|
||||
}
|
||||
|
||||
// TODO - Add checks for the client ID/title ID and matching system/token types here
|
||||
|
||||
if (oAuthToken.info.expires < new Date()) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token');
|
||||
}
|
||||
|
||||
pnid = await getPNIDByPID(oAuthToken.pid);
|
||||
nexAccount = await NEXAccount.findOne({ pid: oAuthToken.pid });
|
||||
} catch {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token');
|
||||
}
|
||||
|
||||
if (!nexAccount || !pnid) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token. No user found');
|
||||
}
|
||||
|
||||
const devices = (await Device.find({
|
||||
linked_pids: pnid.pid
|
||||
})).map((device) => {
|
||||
return {
|
||||
model: device.get('model'), // * ".model" gives the Mongoose model
|
||||
serial: device.serial,
|
||||
linkedPids: device.linked_pids,
|
||||
accessLevel: device.access_level,
|
||||
serverAccessLevel: device.server_access_level,
|
||||
deviceId: device.device_id
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
pnid: {
|
||||
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)
|
||||
},
|
||||
linkedDevices: devices
|
||||
},
|
||||
nexAccount: {
|
||||
pid: nexAccount.pid,
|
||||
owningPid: nexAccount.owning_pid,
|
||||
accessLevel: nexAccount.access_level,
|
||||
serverAccessLevel: nexAccount.server_access_level,
|
||||
friendCode: nexAccount.friend_code,
|
||||
deviceType: nexAccount.device_type
|
||||
},
|
||||
tokenInfo: {
|
||||
systemType: oAuthToken.info.system_type as any, // TODO - Stop the any usage
|
||||
tokenType: oAuthToken.info.token_type as any, // TODO - Stop the any usage
|
||||
pid: BigInt(pnid.pid),
|
||||
accessLevel: pnid.access_level,
|
||||
titleId: oAuthToken.info.title_id,
|
||||
issueTime: oAuthToken.info.issued,
|
||||
expireTime: oAuthToken.info.expires
|
||||
},
|
||||
basicUserInfo: {
|
||||
// TODO - ban: {}
|
||||
accessBetaServers: pnid.access_level === 1 || pnid.access_level === 2 || pnid.access_level === 3, // TODO - Remove with a better permission check later
|
||||
accessDeveloperServers: pnid.access_level === 3 // TODO - Remove with a better permission check later
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import { PasswordResetToken } from '@/models/password-reset-token';
|
||||
import { Device } from '@/models/device';
|
||||
import { SystemType } from '@/types/common/system-types';
|
||||
import { TokenType } from '@/types/common/token-types';
|
||||
import { getPNIDByPID } from '@/database';
|
||||
import { PNID_PERMISSION_FLAGS } from '@/types/common/permission-flags';
|
||||
import { config } from '@/config-manager';
|
||||
import type { ExchangePasswordResetTokenForUserDataRequest, ExchangePasswordResetTokenForUserDataResponse } from '@pretendonetwork/grpc/account/v2/exchange_password_reset_token_for_user_data_rpc';
|
||||
import type { HydratedPNIDDocument } from '@/types/mongoose/pnid';
|
||||
import type { HydratedPasswordResetTokenDocument } from '@/models/password-reset-token';
|
||||
|
||||
export async function exchangePasswordResetTokenForUserData(request: ExchangePasswordResetTokenForUserDataRequest): Promise<ExchangePasswordResetTokenForUserDataResponse> {
|
||||
let pnid: HydratedPNIDDocument | null = null;
|
||||
let passwordResetToken: HydratedPasswordResetTokenDocument | null = null;
|
||||
try {
|
||||
passwordResetToken = await PasswordResetToken.findOne({
|
||||
token: crypto.createHash('sha256').update(request.token).digest('hex')
|
||||
});
|
||||
|
||||
if (!passwordResetToken) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token');
|
||||
}
|
||||
|
||||
if (passwordResetToken.info.system_type !== SystemType.PasswordReset) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token');
|
||||
}
|
||||
|
||||
if (passwordResetToken.info.token_type !== TokenType.PasswordReset) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token');
|
||||
}
|
||||
|
||||
if (passwordResetToken.info.expires < new Date()) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token');
|
||||
}
|
||||
|
||||
pnid = await getPNIDByPID(passwordResetToken.pid);
|
||||
} catch {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token');
|
||||
}
|
||||
|
||||
if (!pnid) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token. No user found');
|
||||
}
|
||||
|
||||
const devices = (await Device.find({
|
||||
linked_pids: pnid.pid
|
||||
})).map((device) => {
|
||||
return {
|
||||
model: device.get('model'), // * ".model" gives the Mongoose model
|
||||
serial: device.serial,
|
||||
linkedPids: device.linked_pids,
|
||||
accessLevel: device.access_level,
|
||||
serverAccessLevel: device.server_access_level,
|
||||
deviceId: device.device_id
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
pnid: {
|
||||
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)
|
||||
},
|
||||
linkedDevices: devices
|
||||
},
|
||||
tokenInfo: {
|
||||
systemType: passwordResetToken.info.system_type as any, // TODO - Stop the any usage
|
||||
tokenType: passwordResetToken.info.token_type as any, // TODO - Stop the any usage
|
||||
pid: BigInt(pnid.pid),
|
||||
accessLevel: pnid.access_level,
|
||||
titleId: passwordResetToken.info.title_id,
|
||||
issueTime: passwordResetToken.info.issued,
|
||||
expireTime: passwordResetToken.info.expires
|
||||
},
|
||||
basicUserInfo: {
|
||||
// TODO - ban: {}
|
||||
accessBetaServers: pnid.access_level === 1 || pnid.access_level === 2 || pnid.access_level === 3, // TODO - Remove with a better permission check later
|
||||
accessDeveloperServers: pnid.access_level === 3 // TODO - Remove with a better permission check later
|
||||
}
|
||||
};
|
||||
}
|
||||
9
src/services/grpc/account/v2/get-ban.ts
Normal file
9
src/services/grpc/account/v2/get-ban.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import type { GetBanRequest, GetBanResponse } from '@pretendonetwork/grpc/account/v2/get_ban_rpc';
|
||||
|
||||
export async function getBan(_request: GetBanRequest): Promise<GetBanResponse> {
|
||||
throw new ServerError(
|
||||
Status.UNIMPLEMENTED,
|
||||
'Stubbed'
|
||||
);
|
||||
}
|
||||
9
src/services/grpc/account/v2/get-device.ts
Normal file
9
src/services/grpc/account/v2/get-device.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import type { GetDeviceRequest, GetDeviceResponse } from '@pretendonetwork/grpc/account/v2/get_device_rpc';
|
||||
|
||||
export async function getDevice(_request: GetDeviceRequest): Promise<GetDeviceResponse> {
|
||||
throw new ServerError(
|
||||
Status.UNIMPLEMENTED,
|
||||
'Stubbed'
|
||||
);
|
||||
}
|
||||
9
src/services/grpc/account/v2/get-nex-account.ts
Normal file
9
src/services/grpc/account/v2/get-nex-account.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import type { GetNEXAccountRequest, GetNEXAccountResponse } from '@pretendonetwork/grpc/account/v2/get_nex_account_rpc';
|
||||
|
||||
export async function getNEXAccount(_request: GetNEXAccountRequest): Promise<GetNEXAccountResponse> {
|
||||
throw new ServerError(
|
||||
Status.UNIMPLEMENTED,
|
||||
'Stubbed'
|
||||
);
|
||||
}
|
||||
9
src/services/grpc/account/v2/get-pnid.ts
Normal file
9
src/services/grpc/account/v2/get-pnid.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import type { GetPNIDRequest, GetPNIDResponse } from '@pretendonetwork/grpc/account/v2/get_pnid_rpc';
|
||||
|
||||
export async function getPNID(_request: GetPNIDRequest): Promise<GetPNIDResponse> {
|
||||
throw new ServerError(
|
||||
Status.UNIMPLEMENTED,
|
||||
'Stubbed'
|
||||
);
|
||||
}
|
||||
9
src/services/grpc/account/v2/get-server.ts
Normal file
9
src/services/grpc/account/v2/get-server.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import type { GetServerRequest, GetServerResponse } from '@pretendonetwork/grpc/account/v2/get_server_rpc';
|
||||
|
||||
export async function getServer(_request: GetServerRequest): Promise<GetServerResponse> {
|
||||
throw new ServerError(
|
||||
Status.UNIMPLEMENTED,
|
||||
'Stubbed'
|
||||
);
|
||||
}
|
||||
@@ -1,56 +1,78 @@
|
||||
import { ServerError, Status } from 'nice-grpc';
|
||||
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';
|
||||
import { exchangeOAuthTokenForUserData } from '@/services/grpc/account/v2/exchange-oauth-token-for-user-data';
|
||||
import { exchangeNEXTokenForUserData } from '@/services/grpc/account/v2/exchange-nex-token-for-user-data';
|
||||
import { exchangeIndependentServiceTokenForUserData } from '@/services/grpc/account/v2/exchange-independent-service-token-for-user-data';
|
||||
import { exchangePasswordResetTokenForUserData } from '@/services/grpc/account/v2/exchange-password-reset-token-for-user-data';
|
||||
import { validateIndependentServiceToken } from '@/services/grpc/account/v2/validate-independent-service-token';
|
||||
import { deleteAccount } from '@/services/grpc/account/v2/delete-account';
|
||||
import { listDevices } from '@/services/grpc/account/v2/list-devices';
|
||||
import { getDevice } from '@/services/grpc/account/v2/get-device';
|
||||
import { updateDevice } from '@/services/grpc/account/v2/update-device';
|
||||
import { listNEXAccounts } from '@/services/grpc/account/v2/list-nex-accounts';
|
||||
import { getNEXAccount } from '@/services/grpc/account/v2/get-nex-account';
|
||||
import { updateNEXAccount } from '@/services/grpc/account/v2/update-nex-account';
|
||||
import { listServers } from '@/services/grpc/account/v2/list-servers';
|
||||
import { createServer } from '@/services/grpc/account/v2/create-server';
|
||||
import { getServer } from '@/services/grpc/account/v2/get-server';
|
||||
import { updateServer } from '@/services/grpc/account/v2/update-server';
|
||||
import { deleteServer } from '@/services/grpc/account/v2/delete-server';
|
||||
import { listPNIDs } from '@/services/grpc/account/v2/list-pnids';
|
||||
import { getPNID } from '@/services/grpc/account/v2/get-pnid';
|
||||
import { getPNIDs } from '@/services/grpc/account/v2/get-pnids';
|
||||
import type { ServiceImplementation } from 'nice-grpc';
|
||||
import type { AccountServiceDefinition } from '@pretendonetwork/grpc/account/v2/account_service';
|
||||
import { updatePNID } from '@/services/grpc/account/v2/update-pnid';
|
||||
import { deletePNID } from '@/services/grpc/account/v2/delete-pnid';
|
||||
import { listAuditLogs } from '@/services/grpc/account/v2/list-audit-logs';
|
||||
import { listAuditLogComments } from '@/services/grpc/account/v2/list-audit-log-comments';
|
||||
import { createAuditLogComment } from '@/services/grpc/account/v2/create-audit-log-comment';
|
||||
import { listBans } from '@/services/grpc/account/v2/list-bans';
|
||||
import { issueBan } from '@/services/grpc/account/v2/issue-ban';
|
||||
import { getBan } from '@/services/grpc/account/v2/get-ban';
|
||||
import { updateBan } from '@/services/grpc/account/v2/update-ban';
|
||||
import { pardonBan } from '@/services/grpc/account/v2/pardon-ban';
|
||||
import { listBanComments } from '@/services/grpc/account/v2/list-ban-comments';
|
||||
import { createBanComment } from '@/services/grpc/account/v2/create-ban-comment';
|
||||
import type { AccountServiceImplementation } from '@pretendonetwork/grpc/account/v2/account_service';
|
||||
|
||||
const notImplemented = (): never => {
|
||||
throw new ServerError(Status.UNIMPLEMENTED, 'Not implemented');
|
||||
};
|
||||
|
||||
export const accountServiceImplementationV2: ServiceImplementation<AccountServiceDefinition> = {
|
||||
export const accountServiceImplementationV2: AccountServiceImplementation = {
|
||||
getUserData,
|
||||
getNEXPassword,
|
||||
getNEXData,
|
||||
updatePNIDPermissions,
|
||||
exchangeTokenForUserData,
|
||||
exchangeOAuthTokenForUserData,
|
||||
exchangeNEXTokenForUserData,
|
||||
exchangeIndependentServiceTokenForUserData,
|
||||
exchangePasswordResetTokenForUserData,
|
||||
validateIndependentServiceToken,
|
||||
deleteAccount,
|
||||
listDevices,
|
||||
getDevice,
|
||||
updateDevice,
|
||||
listNEXAccounts,
|
||||
getNEXAccount,
|
||||
updateNEXAccount,
|
||||
listServers,
|
||||
createServer,
|
||||
getServer,
|
||||
updateServer,
|
||||
deleteServer,
|
||||
listPNIDs,
|
||||
getPNID,
|
||||
getPNIDs,
|
||||
|
||||
// The following methods are not yet implemented
|
||||
createAuditLogComment: notImplemented,
|
||||
createBanComment: notImplemented,
|
||||
createServer: notImplemented,
|
||||
deleteServer: notImplemented,
|
||||
deletePNID: notImplemented,
|
||||
exchangeIndependentServiceTokenForUserData: notImplemented,
|
||||
exchangeNEXTokenForUserData: notImplemented,
|
||||
exchangeOAuthTokenForUserData: notImplemented,
|
||||
exchangePasswordResetTokenForUserData: notImplemented,
|
||||
getBan: notImplemented,
|
||||
getDevice: notImplemented,
|
||||
getNEXAccount: notImplemented,
|
||||
getPNID: notImplemented,
|
||||
getServer: notImplemented,
|
||||
issueBan: notImplemented,
|
||||
listAuditLogComments: notImplemented,
|
||||
listBans: notImplemented,
|
||||
listDevices: notImplemented,
|
||||
listNEXAccounts: notImplemented,
|
||||
listPNIDs: notImplemented,
|
||||
listServers: notImplemented,
|
||||
listAuditLogs: notImplemented,
|
||||
listBanComments: notImplemented,
|
||||
pardonBan: notImplemented,
|
||||
updateBan: notImplemented,
|
||||
updateDevice: notImplemented,
|
||||
updateNEXAccount: notImplemented,
|
||||
updateServer: notImplemented,
|
||||
updatePNID: notImplemented,
|
||||
validateIndependentServiceToken: notImplemented
|
||||
updatePNID,
|
||||
deletePNID,
|
||||
listAuditLogs,
|
||||
listAuditLogComments,
|
||||
createAuditLogComment,
|
||||
listBans,
|
||||
issueBan,
|
||||
getBan,
|
||||
updateBan,
|
||||
pardonBan,
|
||||
listBanComments,
|
||||
createBanComment
|
||||
};
|
||||
|
||||
9
src/services/grpc/account/v2/issue-ban.ts
Normal file
9
src/services/grpc/account/v2/issue-ban.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import type { IssueBanRequest, IssueBanResponse } from '@pretendonetwork/grpc/account/v2/issue_ban_rpc';
|
||||
|
||||
export async function issueBan(_request: IssueBanRequest): Promise<IssueBanResponse> {
|
||||
throw new ServerError(
|
||||
Status.UNIMPLEMENTED,
|
||||
'Stubbed'
|
||||
);
|
||||
}
|
||||
9
src/services/grpc/account/v2/list-audit-log-comments.ts
Normal file
9
src/services/grpc/account/v2/list-audit-log-comments.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import type { ListAuditLogCommentsRequest, ListAuditLogCommentsResponse } from '@pretendonetwork/grpc/account/v2/list_audit_log_comments_rpc';
|
||||
|
||||
export async function listAuditLogComments(_request: ListAuditLogCommentsRequest): Promise<ListAuditLogCommentsResponse> {
|
||||
throw new ServerError(
|
||||
Status.UNIMPLEMENTED,
|
||||
'Stubbed'
|
||||
);
|
||||
}
|
||||
9
src/services/grpc/account/v2/list-audit-logs.ts
Normal file
9
src/services/grpc/account/v2/list-audit-logs.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import type { ListAuditLogsRequest, ListAuditLogsResponse } from '@pretendonetwork/grpc/account/v2/list_audit_logs_rpc';
|
||||
|
||||
export async function listAuditLogs(_request: ListAuditLogsRequest): Promise<ListAuditLogsResponse> {
|
||||
throw new ServerError(
|
||||
Status.UNIMPLEMENTED,
|
||||
'Stubbed'
|
||||
);
|
||||
}
|
||||
9
src/services/grpc/account/v2/list-ban-comments.ts
Normal file
9
src/services/grpc/account/v2/list-ban-comments.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import type { ListBanCommentsRequest, ListBanCommentsResponse } from '@pretendonetwork/grpc/account/v2/list_ban_comments_rpc';
|
||||
|
||||
export async function listBanComments(_request: ListBanCommentsRequest): Promise<ListBanCommentsResponse> {
|
||||
throw new ServerError(
|
||||
Status.UNIMPLEMENTED,
|
||||
'Stubbed'
|
||||
);
|
||||
}
|
||||
9
src/services/grpc/account/v2/list-bans.ts
Normal file
9
src/services/grpc/account/v2/list-bans.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import type { ListBansRequest, ListBansResponse } from '@pretendonetwork/grpc/account/v2/list_bans_rpc';
|
||||
|
||||
export async function listBans(_request: ListBansRequest): Promise<ListBansResponse> {
|
||||
throw new ServerError(
|
||||
Status.UNIMPLEMENTED,
|
||||
'Stubbed'
|
||||
);
|
||||
}
|
||||
9
src/services/grpc/account/v2/list-devices.ts
Normal file
9
src/services/grpc/account/v2/list-devices.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import type { ListDevicesRequest, ListDevicesResponse } from '@pretendonetwork/grpc/account/v2/list_devices_rpc';
|
||||
|
||||
export async function listDevices(_request: ListDevicesRequest): Promise<ListDevicesResponse> {
|
||||
throw new ServerError(
|
||||
Status.UNIMPLEMENTED,
|
||||
'Stubbed'
|
||||
);
|
||||
}
|
||||
9
src/services/grpc/account/v2/list-nex-accounts.ts
Normal file
9
src/services/grpc/account/v2/list-nex-accounts.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import type { ListNEXAccountsRequest, ListNEXAccountsResponse } from '@pretendonetwork/grpc/account/v2/list_nex_accounts_rpc';
|
||||
|
||||
export async function listNEXAccounts(_request: ListNEXAccountsRequest): Promise<ListNEXAccountsResponse> {
|
||||
throw new ServerError(
|
||||
Status.UNIMPLEMENTED,
|
||||
'Stubbed'
|
||||
);
|
||||
}
|
||||
9
src/services/grpc/account/v2/list-pnids.ts
Normal file
9
src/services/grpc/account/v2/list-pnids.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import type { ListPNIDsRequest, ListPNIDsResponse } from '@pretendonetwork/grpc/account/v2/list_pnids_rpc';
|
||||
|
||||
export async function listPNIDs(_request: ListPNIDsRequest): Promise<ListPNIDsResponse> {
|
||||
throw new ServerError(
|
||||
Status.UNIMPLEMENTED,
|
||||
'Stubbed'
|
||||
);
|
||||
}
|
||||
9
src/services/grpc/account/v2/list-servers.ts
Normal file
9
src/services/grpc/account/v2/list-servers.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import type { ListServersRequest, ListServersResponse } from '@pretendonetwork/grpc/account/v2/list_servers_rpc';
|
||||
|
||||
export async function listServers(_request: ListServersRequest): Promise<ListServersResponse> {
|
||||
throw new ServerError(
|
||||
Status.UNIMPLEMENTED,
|
||||
'Stubbed'
|
||||
);
|
||||
}
|
||||
9
src/services/grpc/account/v2/pardon-ban.ts
Normal file
9
src/services/grpc/account/v2/pardon-ban.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import type { PardonBanRequest, PardonBanResponse } from '@pretendonetwork/grpc/account/v2/pardon_ban_rpc';
|
||||
|
||||
export async function pardonBan(_request: PardonBanRequest): Promise<PardonBanResponse> {
|
||||
throw new ServerError(
|
||||
Status.UNIMPLEMENTED,
|
||||
'Stubbed'
|
||||
);
|
||||
}
|
||||
9
src/services/grpc/account/v2/update-ban.ts
Normal file
9
src/services/grpc/account/v2/update-ban.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import type { UpdateBanRequest, UpdateBanResponse } from '@pretendonetwork/grpc/account/v2/update_ban_rpc';
|
||||
|
||||
export async function updateBan(_request: UpdateBanRequest): Promise<UpdateBanResponse> {
|
||||
throw new ServerError(
|
||||
Status.UNIMPLEMENTED,
|
||||
'Stubbed'
|
||||
);
|
||||
}
|
||||
9
src/services/grpc/account/v2/update-device.ts
Normal file
9
src/services/grpc/account/v2/update-device.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import type { UpdateDeviceRequest, UpdateDeviceResponse } from '@pretendonetwork/grpc/account/v2/update_device_rpc';
|
||||
|
||||
export async function updateDevice(_request: UpdateDeviceRequest): Promise<UpdateDeviceResponse> {
|
||||
throw new ServerError(
|
||||
Status.UNIMPLEMENTED,
|
||||
'Stubbed'
|
||||
);
|
||||
}
|
||||
9
src/services/grpc/account/v2/update-nex-account.ts
Normal file
9
src/services/grpc/account/v2/update-nex-account.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import type { UpdateNEXAccountRequest, UpdateNEXAccountResponse } from '@pretendonetwork/grpc/account/v2/update_nex_account_rpc';
|
||||
|
||||
export async function updateNEXAccount(_request: UpdateNEXAccountRequest): Promise<UpdateNEXAccountResponse> {
|
||||
throw new ServerError(
|
||||
Status.UNIMPLEMENTED,
|
||||
'Stubbed'
|
||||
);
|
||||
}
|
||||
9
src/services/grpc/account/v2/update-pnid.ts
Normal file
9
src/services/grpc/account/v2/update-pnid.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import type { UpdatePNIDRequest, UpdatePNIDResponse } from '@pretendonetwork/grpc/account/v2/update_pnid_rpc';
|
||||
|
||||
export async function updatePNID(_request: UpdatePNIDRequest): Promise<UpdatePNIDResponse> {
|
||||
throw new ServerError(
|
||||
Status.UNIMPLEMENTED,
|
||||
'Stubbed'
|
||||
);
|
||||
}
|
||||
9
src/services/grpc/account/v2/update-server.ts
Normal file
9
src/services/grpc/account/v2/update-server.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import type { UpdateServerRequest, UpdateServerResponse } from '@pretendonetwork/grpc/account/v2/update_server_rpc';
|
||||
|
||||
export async function updateServer(_request: UpdateServerRequest): Promise<UpdateServerResponse> {
|
||||
throw new ServerError(
|
||||
Status.UNIMPLEMENTED,
|
||||
'Stubbed'
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import { Server } from '@/models/server';
|
||||
import { PNID } from '@/models/pnid';
|
||||
import { NEXAccount } from '@/models/nex-account';
|
||||
import type { ValidateIndependentServiceTokenRequest, ValidateIndependentServiceTokenResponse } from '@pretendonetwork/grpc/account/v2/validate_independent_service_token_rpc';
|
||||
import type { HydratedServerDocument } from '@/types/mongoose/server';
|
||||
|
||||
export async function validateIndependentServiceToken(request: ValidateIndependentServiceTokenRequest): Promise<ValidateIndependentServiceTokenResponse> {
|
||||
if (request.clientIds.length === 0) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'No server identification data sent');
|
||||
}
|
||||
|
||||
let server: HydratedServerDocument | null = null;
|
||||
try {
|
||||
server = await Server.findOne({
|
||||
client_id: {
|
||||
$in: request.clientIds
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
return {
|
||||
isValid: false
|
||||
};
|
||||
}
|
||||
|
||||
if (!server) {
|
||||
return {
|
||||
isValid: false
|
||||
};
|
||||
}
|
||||
|
||||
const token = Buffer.from(request.token, 'hex');
|
||||
|
||||
if (token.length !== 60) {
|
||||
return {
|
||||
isValid: false
|
||||
};
|
||||
}
|
||||
|
||||
const tokenBody = token.subarray(0, 28);
|
||||
const expectedHMAC = token.subarray(28);
|
||||
const calculatedHMAC = crypto.createHmac('sha256', server.aes_key).update(tokenBody).digest();
|
||||
|
||||
if (!crypto.timingSafeEqual(expectedHMAC, calculatedHMAC)) {
|
||||
return {
|
||||
isValid: false
|
||||
};
|
||||
}
|
||||
|
||||
// TODO - Add ban lookups once bans are no longer stubbed
|
||||
const pid = tokenBody.readUInt32BE(0);
|
||||
|
||||
const nexAccount = await NEXAccount.findOne({
|
||||
pid: pid
|
||||
});
|
||||
|
||||
if (!nexAccount) {
|
||||
return {
|
||||
isValid: false
|
||||
};
|
||||
}
|
||||
|
||||
const pnid = await PNID.findOne({
|
||||
pid: pid
|
||||
});
|
||||
|
||||
if (pnid) {
|
||||
return {
|
||||
isValid: true,
|
||||
basicUserInfo: {
|
||||
// TODO - ban: {}
|
||||
accessBetaServers: pnid.access_level === 1 || pnid.access_level === 2 || pnid.access_level === 3, // TODO - Remove with a better permission check later
|
||||
accessDeveloperServers: pnid.access_level === 3 // TODO - Remove with a better permission check later
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
isValid: true,
|
||||
basicUserInfo: {
|
||||
accessBetaServers: nexAccount.access_level === 1 || nexAccount.access_level === 2 || nexAccount.access_level === 3, // TODO - Remove with a better permission check later
|
||||
accessDeveloperServers: nexAccount.access_level === 3 // TODO - Remove with a better permission check later
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { getPNIDByUsername, getPNIDByAPIRefreshToken } from '@/database';
|
||||
import { nintendoPasswordHash, generateToken } from '@/util';
|
||||
import { nintendoPasswordHash } from '@/util';
|
||||
import { OAuthToken } from '@/models/oauth-token';
|
||||
import { SystemType } from '@/types/common/system-types';
|
||||
import { TokenType } from '@/types/common/token-types';
|
||||
import { config } from '@/config-manager';
|
||||
import type { LoginRequest, LoginResponse, DeepPartial } from '@pretendonetwork/grpc/api/login_rpc';
|
||||
import type { HydratedPNIDDocument } from '@/types/mongoose/pnid';
|
||||
|
||||
@@ -56,31 +57,36 @@ export async function login(request: LoginRequest): Promise<DeepPartial<LoginRes
|
||||
throw new ServerError(Status.UNAUTHENTICATED, 'Account has been deleted');
|
||||
}
|
||||
|
||||
const accessTokenOptions = {
|
||||
system_type: SystemType.API,
|
||||
token_type: TokenType.OAuthAccess,
|
||||
const accessToken = crypto.randomBytes(16).toString('hex');
|
||||
const newRefreshToken = crypto.randomBytes(20).toString('hex');
|
||||
|
||||
await OAuthToken.create({
|
||||
token: crypto.createHash('sha256').update(accessToken).digest('hex'),
|
||||
client_id: 'a2efa818a34fa16b8afbc8a74eba3eda', // TODO - This is the Wii U config, change this?
|
||||
client_secret: 'c91cdb5658bd4954ade78533a339cf9a', // TODO - This is the Wii U config, change this?
|
||||
pid: pnid.pid,
|
||||
access_level: pnid.access_level,
|
||||
title_id: BigInt(0),
|
||||
expire_time: BigInt(Date.now() + (3600 * 1000))
|
||||
};
|
||||
info: {
|
||||
system_type: SystemType.API,
|
||||
token_type: TokenType.OAuthAccess,
|
||||
title_id: BigInt(0),
|
||||
issued: new Date(),
|
||||
expires: new Date(Date.now() + (3600 * 1000))
|
||||
}
|
||||
});
|
||||
|
||||
const refreshTokenOptions = {
|
||||
system_type: SystemType.API,
|
||||
token_type: TokenType.OAuthRefresh,
|
||||
await OAuthToken.create({
|
||||
token: crypto.createHash('sha256').update(newRefreshToken).digest('hex'),
|
||||
client_id: 'a2efa818a34fa16b8afbc8a74eba3eda', // TODO - This is the Wii U config, change this?
|
||||
client_secret: 'c91cdb5658bd4954ade78533a339cf9a', // TODO - This is the Wii U config, change this?
|
||||
pid: pnid.pid,
|
||||
access_level: pnid.access_level,
|
||||
title_id: BigInt(0),
|
||||
expire_time: BigInt(Date.now() + 12 * 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
|
||||
info: {
|
||||
system_type: SystemType.API,
|
||||
token_type: TokenType.OAuthRefresh,
|
||||
title_id: BigInt(0),
|
||||
issued: new Date(),
|
||||
expires: new Date(Date.now() + 12 * 3600 * 1000)
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken: accessToken,
|
||||
|
||||
@@ -6,10 +6,11 @@ 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 { nintendoPasswordHash, sendConfirmationEmail } from '@/util';
|
||||
import { LOG_ERROR } from '@/logger';
|
||||
import { PNID } from '@/models/pnid';
|
||||
import { NEXAccount } from '@/models/nex-account';
|
||||
import { OAuthToken } from '@/models/oauth-token';
|
||||
import { SystemType } from '@/types/common/system-types';
|
||||
import { TokenType } from '@/types/common/token-types';
|
||||
import { config, disabledFeatures } from '@/config-manager';
|
||||
@@ -231,31 +232,36 @@ export async function register(request: RegisterRequest): Promise<DeepPartial<Lo
|
||||
|
||||
await sendConfirmationEmail(pnid);
|
||||
|
||||
const accessTokenOptions = {
|
||||
system_type: SystemType.API,
|
||||
token_type: TokenType.OAuthAccess,
|
||||
const accessToken = crypto.randomBytes(16).toString('hex');
|
||||
const refreshToken = crypto.randomBytes(20).toString('hex');
|
||||
|
||||
await OAuthToken.create({
|
||||
token: crypto.createHash('sha256').update(accessToken).digest('hex'),
|
||||
client_id: 'a2efa818a34fa16b8afbc8a74eba3eda', // TODO - This is the Wii U config, change this?
|
||||
client_secret: 'c91cdb5658bd4954ade78533a339cf9a', // TODO - This is the Wii U config, change this?
|
||||
pid: pnid.pid,
|
||||
access_level: pnid.access_level,
|
||||
title_id: BigInt(0),
|
||||
expire_time: BigInt(Date.now() + (3600 * 1000))
|
||||
};
|
||||
info: {
|
||||
system_type: SystemType.API,
|
||||
token_type: TokenType.OAuthAccess,
|
||||
title_id: BigInt(0),
|
||||
issued: new Date(),
|
||||
expires: new Date(Date.now() + (3600 * 1000))
|
||||
}
|
||||
});
|
||||
|
||||
const refreshTokenOptions = {
|
||||
system_type: SystemType.API,
|
||||
token_type: TokenType.OAuthRefresh,
|
||||
await OAuthToken.create({
|
||||
token: crypto.createHash('sha256').update(refreshToken).digest('hex'),
|
||||
client_id: 'a2efa818a34fa16b8afbc8a74eba3eda', // TODO - This is the Wii U config, change this?
|
||||
client_secret: 'c91cdb5658bd4954ade78533a339cf9a', // TODO - This is the Wii U config, change this?
|
||||
pid: pnid.pid,
|
||||
access_level: pnid.access_level,
|
||||
title_id: BigInt(0),
|
||||
expire_time: BigInt(Date.now() + 12 * 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
|
||||
info: {
|
||||
system_type: SystemType.API,
|
||||
token_type: TokenType.OAuthRefresh,
|
||||
title_id: BigInt(0),
|
||||
issued: new Date(),
|
||||
expires: new Date(Date.now() + 12 * 3600 * 1000)
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken: accessToken,
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import crypto from 'node:crypto';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import { decryptToken, unpackToken, nintendoPasswordHash } from '@/util';
|
||||
import { PasswordResetToken } from '@/models/password-reset-token';
|
||||
import { nintendoPasswordHash } from '@/util';
|
||||
import { getPNIDByPID } from '@/database';
|
||||
import { SystemType } from '@/types/common/system-types';
|
||||
import { TokenType } from '@/types/common/token-types';
|
||||
import type { ResetPasswordRequest } from '@pretendonetwork/grpc/api/reset_password_rpc';
|
||||
import type { Empty } from '@pretendonetwork/grpc/google/protobuf/empty';
|
||||
import type { Token } from '@/types/common/token';
|
||||
import type { HydratedPNIDDocument } from '@/types/mongoose/pnid';
|
||||
|
||||
// * This sucks
|
||||
const PASSWORD_WORD_OR_NUMBER_REGEX = /(?=.*[a-zA-Z])(?=.*\d).*/;
|
||||
@@ -21,20 +25,33 @@ export async function resetPassword(request: ResetPasswordRequest): Promise<Empt
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Missing token');
|
||||
}
|
||||
|
||||
let unpackedToken: Token;
|
||||
let pnid: HydratedPNIDDocument | null = null;
|
||||
try {
|
||||
const decryptedToken = await decryptToken(Buffer.from(token, 'base64'));
|
||||
unpackedToken = unpackToken(decryptedToken);
|
||||
const passwordResetToken = await PasswordResetToken.findOne({
|
||||
token: crypto.createHash('sha256').update(token).digest('hex')
|
||||
});
|
||||
|
||||
if (!passwordResetToken) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token');
|
||||
}
|
||||
|
||||
if (passwordResetToken.info.system_type !== SystemType.PasswordReset) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token');
|
||||
}
|
||||
|
||||
if (passwordResetToken.info.token_type !== TokenType.PasswordReset) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token');
|
||||
}
|
||||
|
||||
if (passwordResetToken.info.expires < new Date()) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token');
|
||||
}
|
||||
|
||||
pnid = await getPNIDByPID(passwordResetToken.pid);
|
||||
} 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');
|
||||
}
|
||||
@@ -72,6 +89,7 @@ export async function resetPassword(request: ResetPasswordRequest): Promise<Empt
|
||||
|
||||
pnid.password = passwordHash;
|
||||
|
||||
await pnid.removeAllTokens();
|
||||
await pnid.save();
|
||||
|
||||
return {};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { getPNIDByUsername, getPNIDByAPIRefreshToken } from '@/database';
|
||||
import { nintendoPasswordHash, generateToken } from '@/util';
|
||||
import { config } from '@/config-manager';
|
||||
import { nintendoPasswordHash } from '@/util';
|
||||
import { OAuthToken } from '@/models/oauth-token';
|
||||
import { SystemType } from '@/types/common/system-types';
|
||||
import { TokenType } from '@/types/common/token-types';
|
||||
import type { LoginRequest, LoginResponse, DeepPartial } from '@pretendonetwork/grpc/api/v2/login_rpc';
|
||||
@@ -56,39 +57,41 @@ export async function login(request: LoginRequest): Promise<DeepPartial<LoginRes
|
||||
throw new ServerError(Status.UNAUTHENTICATED, 'Account has been deleted');
|
||||
}
|
||||
|
||||
const accessTokenOptions = {
|
||||
system_type: SystemType.API,
|
||||
token_type: TokenType.OAuthAccess,
|
||||
const accessToken = crypto.randomBytes(16).toString('hex');
|
||||
const newRefreshToken = crypto.randomBytes(20).toString('hex');
|
||||
|
||||
await OAuthToken.create({
|
||||
token: crypto.createHash('sha256').update(accessToken).digest('hex'),
|
||||
client_id: 'a2efa818a34fa16b8afbc8a74eba3eda', // TODO - This is the Wii U config, change this?
|
||||
client_secret: 'c91cdb5658bd4954ade78533a339cf9a', // TODO - This is the Wii U config, change this?
|
||||
pid: pnid.pid,
|
||||
access_level: pnid.access_level,
|
||||
title_id: BigInt(0),
|
||||
expire_time: BigInt(Date.now() + (3600 * 1000))
|
||||
};
|
||||
info: {
|
||||
system_type: SystemType.API,
|
||||
token_type: TokenType.OAuthAccess,
|
||||
title_id: BigInt(0),
|
||||
issued: new Date(),
|
||||
expires: new Date(Date.now() + (3600 * 1000))
|
||||
}
|
||||
});
|
||||
|
||||
const refreshTokenOptions = {
|
||||
system_type: SystemType.API,
|
||||
token_type: TokenType.OAuthAccess,
|
||||
await OAuthToken.create({
|
||||
token: crypto.createHash('sha256').update(newRefreshToken).digest('hex'),
|
||||
client_id: 'a2efa818a34fa16b8afbc8a74eba3eda', // TODO - This is the Wii U config, change this?
|
||||
client_secret: 'c91cdb5658bd4954ade78533a339cf9a', // TODO - This is the Wii U config, change this?
|
||||
pid: pnid.pid,
|
||||
access_level: pnid.access_level,
|
||||
title_id: BigInt(0),
|
||||
expire_time: BigInt(Date.now() + 12 * 3600 * 1000)
|
||||
};
|
||||
|
||||
const accessTokenBuffer = await generateToken(config.aes_key, accessTokenOptions);
|
||||
const refreshTokenBuffer = await generateToken(config.aes_key, refreshTokenOptions);
|
||||
|
||||
if (!accessTokenBuffer) {
|
||||
throw new ServerError(Status.INTERNAL, 'Failed to generate access token');
|
||||
}
|
||||
|
||||
if (!refreshTokenBuffer) {
|
||||
throw new ServerError(Status.INTERNAL, 'Failed to generate refresh token');
|
||||
}
|
||||
info: {
|
||||
system_type: SystemType.API,
|
||||
token_type: TokenType.OAuthRefresh,
|
||||
title_id: BigInt(0),
|
||||
issued: new Date(),
|
||||
expires: new Date(Date.now() + 12 * 3600 * 1000)
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken: accessTokenBuffer.toString('hex'),
|
||||
accessToken: accessToken,
|
||||
tokenType: 'Bearer',
|
||||
expiresIn: 3600,
|
||||
refreshToken: refreshTokenBuffer.toString('hex')
|
||||
refreshToken: newRefreshToken
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,10 +6,11 @@ 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 { nintendoPasswordHash, sendConfirmationEmail } from '@/util';
|
||||
import { LOG_ERROR } from '@/logger';
|
||||
import { PNID } from '@/models/pnid';
|
||||
import { NEXAccount } from '@/models/nex-account';
|
||||
import { OAuthToken } from '@/models/oauth-token';
|
||||
import { config, disabledFeatures } from '@/config-manager';
|
||||
import { SystemType } from '@/types/common/system-types';
|
||||
import { TokenType } from '@/types/common/token-types';
|
||||
@@ -231,39 +232,41 @@ export async function register(request: RegisterRequest): Promise<DeepPartial<Lo
|
||||
|
||||
await sendConfirmationEmail(pnid);
|
||||
|
||||
const accessTokenOptions = {
|
||||
system_type: SystemType.API,
|
||||
token_type: TokenType.OAuthAccess,
|
||||
const accessToken = crypto.randomBytes(16).toString('hex');
|
||||
const refreshToken = crypto.randomBytes(20).toString('hex');
|
||||
|
||||
await OAuthToken.create({
|
||||
token: crypto.createHash('sha256').update(accessToken).digest('hex'),
|
||||
client_id: 'a2efa818a34fa16b8afbc8a74eba3eda', // TODO - This is the Wii U config, change this?
|
||||
client_secret: 'c91cdb5658bd4954ade78533a339cf9a', // TODO - This is the Wii U config, change this?
|
||||
pid: pnid.pid,
|
||||
access_level: pnid.access_level,
|
||||
title_id: BigInt(0),
|
||||
expire_time: BigInt(Date.now() + (3600 * 1000))
|
||||
};
|
||||
info: {
|
||||
system_type: SystemType.API,
|
||||
token_type: TokenType.OAuthAccess,
|
||||
title_id: BigInt(0),
|
||||
issued: new Date(),
|
||||
expires: new Date(Date.now() + (3600 * 1000))
|
||||
}
|
||||
});
|
||||
|
||||
const refreshTokenOptions = {
|
||||
system_type: SystemType.API,
|
||||
token_type: TokenType.OAuthRefresh,
|
||||
await OAuthToken.create({
|
||||
token: crypto.createHash('sha256').update(refreshToken).digest('hex'),
|
||||
client_id: 'a2efa818a34fa16b8afbc8a74eba3eda', // TODO - This is the Wii U config, change this?
|
||||
client_secret: 'c91cdb5658bd4954ade78533a339cf9a', // TODO - This is the Wii U config, change this?
|
||||
pid: pnid.pid,
|
||||
access_level: pnid.access_level,
|
||||
title_id: BigInt(0),
|
||||
expire_time: BigInt(Date.now() + 12 * 3600 * 1000)
|
||||
};
|
||||
|
||||
const accessTokenBuffer = await generateToken(config.aes_key, accessTokenOptions);
|
||||
const refreshTokenBuffer = await generateToken(config.aes_key, refreshTokenOptions);
|
||||
|
||||
if (!accessTokenBuffer) {
|
||||
throw new ServerError(Status.INTERNAL, 'Failed to generate access token');
|
||||
}
|
||||
|
||||
if (!refreshTokenBuffer) {
|
||||
throw new ServerError(Status.INTERNAL, 'Failed to generate refresh token');
|
||||
}
|
||||
info: {
|
||||
system_type: SystemType.API,
|
||||
token_type: TokenType.OAuthRefresh,
|
||||
title_id: BigInt(0),
|
||||
issued: new Date(),
|
||||
expires: new Date(Date.now() + 12 * 3600 * 1000)
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken: accessTokenBuffer.toString('hex'),
|
||||
accessToken: accessToken,
|
||||
tokenType: 'Bearer',
|
||||
expiresIn: 3600,
|
||||
refreshToken: refreshTokenBuffer.toString('hex')
|
||||
refreshToken: refreshToken
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import crypto from 'node:crypto';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import { decryptToken, unpackToken, nintendoPasswordHash } from '@/util';
|
||||
import { PasswordResetToken } from '@/models/password-reset-token';
|
||||
import { nintendoPasswordHash } from '@/util';
|
||||
import { getPNIDByPID } from '@/database';
|
||||
import { SystemType } from '@/types/common/system-types';
|
||||
import { TokenType } from '@/types/common/token-types';
|
||||
import type { ResetPasswordRequest, ResetPasswordResponse } from '@pretendonetwork/grpc/api/v2/reset_password_rpc';
|
||||
import type { Token } from '@/types/common/token';
|
||||
import type { HydratedPNIDDocument } from '@/types/mongoose/pnid';
|
||||
|
||||
// * This sucks
|
||||
const PASSWORD_WORD_OR_NUMBER_REGEX = /(?=.*[a-zA-Z])(?=.*\d).*/;
|
||||
@@ -20,20 +24,33 @@ export async function resetPassword(request: ResetPasswordRequest): Promise<Rese
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Missing token');
|
||||
}
|
||||
|
||||
let unpackedToken: Token;
|
||||
let pnid: HydratedPNIDDocument | null = null;
|
||||
try {
|
||||
const decryptedToken = await decryptToken(Buffer.from(token, 'base64'));
|
||||
unpackedToken = unpackToken(decryptedToken);
|
||||
const passwordResetToken = await PasswordResetToken.findOne({
|
||||
token: crypto.createHash('sha256').update(token).digest('hex')
|
||||
});
|
||||
|
||||
if (!passwordResetToken) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token');
|
||||
}
|
||||
|
||||
if (passwordResetToken.info.system_type !== SystemType.PasswordReset) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token');
|
||||
}
|
||||
|
||||
if (passwordResetToken.info.token_type !== TokenType.PasswordReset) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token');
|
||||
}
|
||||
|
||||
if (passwordResetToken.info.expires < new Date()) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token');
|
||||
}
|
||||
|
||||
pnid = await getPNIDByPID(passwordResetToken.pid);
|
||||
} 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');
|
||||
}
|
||||
@@ -71,6 +88,7 @@ export async function resetPassword(request: ResetPasswordRequest): Promise<Rese
|
||||
|
||||
pnid.password = passwordHash;
|
||||
|
||||
await pnid.removeAllTokens();
|
||||
await pnid.save();
|
||||
|
||||
return {};
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import crypto from 'node:crypto';
|
||||
import express from 'express';
|
||||
import { SystemType } from '@/types/common/system-types';
|
||||
import { TokenType } from '@/types/common/token-types';
|
||||
import { nintendoBase64Encode, nintendoBase64Decode, nascDateTime, nascError, generateToken } from '@/util';
|
||||
import { nintendoBase64Encode, nintendoBase64Decode, nascDateTime, nascError, createServiceToken } from '@/util';
|
||||
import { getServerByTitleID } from '@/database';
|
||||
import type { NASCRequestParams } from '@/types/services/nasc/request-params';
|
||||
import { IndependentServiceToken } from '@/models/independent-service-token';
|
||||
import { NEXToken } from '@/models/nex-token';
|
||||
import type { NASCACRequestParams, NASCLoginACRequestParams, NASCServiceTokenACRequestParams } from '@/types/services/nasc/ac-request-params';
|
||||
import type { HydratedServerDocument } from '@/types/mongoose/server';
|
||||
|
||||
const router = express.Router();
|
||||
@@ -14,7 +17,7 @@ const router = express.Router();
|
||||
* Description: Gets a NEX server address and token
|
||||
*/
|
||||
router.post('/', async (request: express.Request, response: express.Response): Promise<void> => {
|
||||
const requestParams: NASCRequestParams = request.body;
|
||||
const requestParams: NASCACRequestParams = request.body;
|
||||
const action = nintendoBase64Decode(requestParams.action).toString();
|
||||
const titleID = nintendoBase64Decode(requestParams.titleid).toString();
|
||||
const gameServerID = nintendoBase64Decode(requestParams.gameid).toString();
|
||||
@@ -61,60 +64,72 @@ router.post('/', async (request: express.Request, response: express.Response): P
|
||||
|
||||
switch (action) {
|
||||
case 'LOGIN':
|
||||
responseData = await processLoginRequest(server, nexAccount.pid, titleID);
|
||||
responseData = await processLoginRequest(server, nexAccount.pid, requestParams as NASCLoginACRequestParams); // TODO - Remove this "as" with field checking
|
||||
break;
|
||||
case 'SVCLOC':
|
||||
responseData = await processServiceTokenRequest(server, nexAccount.pid, titleID);
|
||||
responseData = await processServiceTokenRequest(server, nexAccount.pid, requestParams as NASCServiceTokenACRequestParams); // TODO - Remove this "as" with field checking
|
||||
break;
|
||||
}
|
||||
|
||||
response.status(200).send(responseData.toString());
|
||||
});
|
||||
|
||||
async function processLoginRequest(server: HydratedServerDocument, pid: number, titleID: string): Promise<URLSearchParams> {
|
||||
const tokenOptions = {
|
||||
system_type: SystemType.CTR,
|
||||
token_type: TokenType.NEX,
|
||||
async function processLoginRequest(server: HydratedServerDocument, pid: number, requestParams: NASCLoginACRequestParams): Promise<URLSearchParams> {
|
||||
const titleID = nintendoBase64Decode(requestParams.titleid).toString();
|
||||
const token = nintendoBase64Encode(crypto.randomBytes(112));
|
||||
|
||||
await NEXToken.create({
|
||||
token: crypto.createHash('sha256').update(token).digest('hex'),
|
||||
game_server_id: server.game_server_id,
|
||||
pid: pid,
|
||||
access_level: 0,
|
||||
title_id: BigInt(parseInt(titleID, 16)),
|
||||
expire_time: BigInt(Date.now() + (3600 * 1000))
|
||||
};
|
||||
|
||||
// TODO - Handle null tokens
|
||||
|
||||
const nexTokenBuffer = await generateToken(server.aes_key, tokenOptions);
|
||||
const nexToken = nintendoBase64Encode(nexTokenBuffer || '');
|
||||
info: {
|
||||
system_type: SystemType.CTR,
|
||||
token_type: TokenType.NEX,
|
||||
title_id: BigInt(parseInt(titleID, 16)),
|
||||
issued: new Date(),
|
||||
expires: new Date(Date.now() + (3600 * 1000))
|
||||
}
|
||||
});
|
||||
|
||||
const connectInfo = await server.getServerConnectInfo();
|
||||
return new URLSearchParams({
|
||||
locator: nintendoBase64Encode(`${connectInfo.ip}:${connectInfo.port}`),
|
||||
retry: nintendoBase64Encode('0'),
|
||||
returncd: nintendoBase64Encode('001'),
|
||||
token: nexToken,
|
||||
token: token,
|
||||
datetime: nintendoBase64Encode(nascDateTime())
|
||||
});
|
||||
}
|
||||
|
||||
async function processServiceTokenRequest(server: HydratedServerDocument, pid: number, titleID: string): Promise<URLSearchParams> {
|
||||
const tokenOptions = {
|
||||
system_type: SystemType.CTR,
|
||||
token_type: TokenType.IndependentService,
|
||||
async function processServiceTokenRequest(server: HydratedServerDocument, pid: number, requestParams: NASCServiceTokenACRequestParams): Promise<URLSearchParams> {
|
||||
const titleID = nintendoBase64Decode(requestParams.titleid).toString();
|
||||
const serviceTokenOptions = {
|
||||
pid: pid,
|
||||
access_level: 0,
|
||||
title_id: BigInt(parseInt(titleID, 16)),
|
||||
expire_time: BigInt(Date.now()) // TODO - Hack. Independent services expire their own tokens, so we give them the ISSUED time, not an EXPIRE time
|
||||
title_id: titleID,
|
||||
issued: new Date(),
|
||||
expires: new Date(Date.now() + 24 * 3600 * 1000)
|
||||
};
|
||||
|
||||
// TODO - Handle null tokens
|
||||
const token = nintendoBase64Encode(createServiceToken(server, serviceTokenOptions));
|
||||
|
||||
const serviceTokenBuffer = await generateToken(server.aes_key, tokenOptions);
|
||||
const serviceToken = nintendoBase64Encode(serviceTokenBuffer || '');
|
||||
await IndependentServiceToken.create({
|
||||
token: crypto.createHash('sha256').update(token).digest('hex'),
|
||||
client_id: nintendoBase64Decode(requestParams.keyhash).toString(),
|
||||
title_id: serviceTokenOptions.title_id,
|
||||
pid: serviceTokenOptions.pid,
|
||||
info: {
|
||||
system_type: SystemType.CTR,
|
||||
token_type: TokenType.IndependentService,
|
||||
title_id: BigInt(parseInt(titleID, 16)),
|
||||
issued: serviceTokenOptions.issued,
|
||||
expires: serviceTokenOptions.expires
|
||||
}
|
||||
});
|
||||
|
||||
return new URLSearchParams({
|
||||
retry: nintendoBase64Encode('0'),
|
||||
returncd: nintendoBase64Encode('007'),
|
||||
servicetoken: serviceToken,
|
||||
servicetoken: token,
|
||||
statusdata: nintendoBase64Encode('Y'),
|
||||
svchost: nintendoBase64Encode('n/a'),
|
||||
datetime: nintendoBase64Encode(nascDateTime())
|
||||
|
||||
@@ -4,14 +4,14 @@ import got from 'got';
|
||||
import { z } from 'zod';
|
||||
import { getServerByClientID, getPNIDByPID } from '@/database';
|
||||
import { LOG_ERROR } from '@/logger';
|
||||
import { decryptToken, unpackToken, getValueFromHeaders, sendConfirmationEmail } from '@/util';
|
||||
import { getValueFromHeaders, sendConfirmationEmail } from '@/util';
|
||||
import { config } from '@/config-manager';
|
||||
import { IndependentServiceToken } from '@/models/independent-service-token';
|
||||
import timezones from '@/services/nnas/timezones.json';
|
||||
import regionsList from '@/services/nnas/regions.json';
|
||||
import type { HydratedServerDocument } from '@/types/mongoose/server';
|
||||
import type { HydratedPNIDDocument } from '@/types/mongoose/pnid';
|
||||
import type { AccountSettings } from '@/types/services/nnas/account-settings';
|
||||
import type { Token } from '@/types/common/token';
|
||||
import type { RegionLanguages } from '@/types/services/nnas/region-languages';
|
||||
import type { RegionTimezone, RegionTimezones } from '@/types/services/nnas/region-timezones';
|
||||
import type { Country, Region } from '@/types/services/nnas/regions';
|
||||
@@ -44,13 +44,22 @@ router.get('/ui/profile', async function (request: express.Request, response: ex
|
||||
return;
|
||||
}
|
||||
|
||||
const aes_key: string = server?.aes_key;
|
||||
const decryptedToken = decryptToken(Buffer.from(token, 'base64'), aes_key);
|
||||
const serviceToken = await IndependentServiceToken.findOne({
|
||||
token: crypto.createHash('sha256').update(token).digest('hex')
|
||||
});
|
||||
|
||||
const tokenContents: Token = unpackToken(decryptedToken);
|
||||
if (!serviceToken) {
|
||||
response.sendStatus(504);
|
||||
return;
|
||||
}
|
||||
|
||||
if (serviceToken.client_id !== '3f3928cc6f780638d360f0485cef973f') {
|
||||
response.sendStatus(504);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const PNID: HydratedPNIDDocument | null = await getPNIDByPID(tokenContents.pid);
|
||||
const PNID: HydratedPNIDDocument | null = await getPNIDByPID(serviceToken.pid);
|
||||
|
||||
if (!PNID) {
|
||||
response.sendStatus(504);
|
||||
@@ -127,12 +136,22 @@ router.post('/update', async function (request: express.Request, response: expre
|
||||
return;
|
||||
}
|
||||
|
||||
const aesKey = server?.aes_key;
|
||||
const decryptedToken = decryptToken(Buffer.from(token, 'base64'), aesKey);
|
||||
const tokenContents: Token = unpackToken(decryptedToken);
|
||||
const serviceToken = await IndependentServiceToken.findOne({
|
||||
token: crypto.createHash('sha256').update(token).digest('hex')
|
||||
});
|
||||
|
||||
if (!serviceToken) {
|
||||
response.sendStatus(504);
|
||||
return;
|
||||
}
|
||||
|
||||
if (serviceToken.client_id !== '3f3928cc6f780638d360f0485cef973f') {
|
||||
response.sendStatus(504);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const pnid: HydratedPNIDDocument | null = await getPNIDByPID(tokenContents.pid);
|
||||
const pnid: HydratedPNIDDocument | null = await getPNIDByPID(serviceToken.pid);
|
||||
const personBody: AccountSettings = request.body;
|
||||
|
||||
if (!pnid) {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import crypto from 'node:crypto';
|
||||
import express from 'express';
|
||||
import xmlbuilder from 'xmlbuilder';
|
||||
import bcrypt from 'bcrypt';
|
||||
import deviceCertificateMiddleware from '@/middleware/device-certificate';
|
||||
import consoleStatusVerificationMiddleware from '@/middleware/console-status-verification';
|
||||
import { getPNIDByNNASRefreshToken, getPNIDByUsername } from '@/database';
|
||||
import { generateToken } from '@/util';
|
||||
import { SystemType } from '@/types/common/system-types';
|
||||
import { TokenType } from '@/types/common/token-types';
|
||||
import { config } from '@/config-manager';
|
||||
import { Device } from '@/models/device';
|
||||
import { OAuthToken } from '@/models/oauth-token';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -167,25 +167,39 @@ router.post('/access_token/generate', deviceCertificateMiddleware, consoleStatus
|
||||
return;
|
||||
}
|
||||
|
||||
const accessTokenOptions = {
|
||||
system_type: SystemType.WUP,
|
||||
token_type: TokenType.OAuthAccess,
|
||||
const clientID = request.header('x-nintendo-client-id');
|
||||
const clientSecret = request.header('x-nintendo-client-secret');
|
||||
|
||||
const accessToken = crypto.randomBytes(16).toString('hex');
|
||||
const newRefreshToken = crypto.randomBytes(20).toString('hex');
|
||||
|
||||
await OAuthToken.create({
|
||||
token: crypto.createHash('sha256').update(accessToken).digest('hex'),
|
||||
client_id: clientID,
|
||||
client_secret: clientSecret,
|
||||
pid: pnid.pid,
|
||||
expire_time: BigInt(Date.now() + (3600 * 1000))
|
||||
};
|
||||
info: {
|
||||
system_type: SystemType.WUP,
|
||||
token_type: TokenType.OAuthAccess,
|
||||
title_id: BigInt(0), // TODO - Add this?
|
||||
issued: new Date(),
|
||||
expires: new Date(Date.now() + (3600 * 1000))
|
||||
}
|
||||
});
|
||||
|
||||
const refreshTokenOptions = {
|
||||
system_type: SystemType.WUP,
|
||||
token_type: TokenType.OAuthRefresh,
|
||||
await OAuthToken.create({
|
||||
token: crypto.createHash('sha256').update(newRefreshToken).digest('hex'),
|
||||
client_id: clientID,
|
||||
client_secret: clientSecret,
|
||||
pid: pnid.pid,
|
||||
expire_time: BigInt(Date.now() + 12 * 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') : '';
|
||||
info: {
|
||||
system_type: SystemType.WUP,
|
||||
token_type: TokenType.OAuthRefresh,
|
||||
title_id: BigInt(0), // TODO - Add this?
|
||||
issued: new Date(),
|
||||
expires: new Date(Date.now() + 12 * 3600 * 1000)
|
||||
}
|
||||
});
|
||||
|
||||
// TODO - Handle null tokens
|
||||
|
||||
|
||||
@@ -650,6 +650,8 @@ router.put('/@me', async (request: express.Request, response: express.Response):
|
||||
const passwordHash = await bcrypt.hash(primaryPasswordHash, 10);
|
||||
|
||||
pnid.password = passwordHash;
|
||||
|
||||
await pnid.removeAllTokens();
|
||||
}
|
||||
|
||||
pnid.gender = gender;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import crypto from 'node:crypto';
|
||||
import express from 'express';
|
||||
import xmlbuilder from 'xmlbuilder';
|
||||
import { getServerByClientID, getServerByGameServerID } from '@/database';
|
||||
import { generateToken, getValueFromHeaders, getValueFromQueryString } from '@/util';
|
||||
import { createServiceToken, getValueFromHeaders, getValueFromQueryString } from '@/util';
|
||||
import { TokenType } from '@/types/common/token-types';
|
||||
import { IndependentServiceToken } from '@/models/independent-service-token';
|
||||
import { NEXToken } from '@/models/nex-token';
|
||||
import { NEXAccount } from '@/models/nex-account';
|
||||
|
||||
const router = express.Router();
|
||||
@@ -90,25 +93,32 @@ router.get('/service_token/@me', async (request: express.Request, response: expr
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenOptions = {
|
||||
system_type: server.device,
|
||||
token_type: TokenType.IndependentService,
|
||||
const serviceTokenOptions = {
|
||||
pid: pnid.pid,
|
||||
access_level: pnid.access_level,
|
||||
title_id: BigInt(parseInt(titleID, 16)),
|
||||
expire_time: BigInt(Date.now()) // TODO - Hack. Independent services expire their own tokens, so we give them the ISSUED time, not an EXPIRE time
|
||||
title_id: titleID,
|
||||
issued: new Date(),
|
||||
expires: new Date(Date.now() + 24 * 3600 * 1000)
|
||||
};
|
||||
|
||||
const serviceTokenBuffer = await generateToken(server.aes_key, tokenOptions);
|
||||
let serviceToken = serviceTokenBuffer ? serviceTokenBuffer.toString('base64') : '';
|
||||
const token = createServiceToken(server, serviceTokenOptions).toString('base64');
|
||||
|
||||
if (request.isCemu) {
|
||||
serviceToken = Buffer.from(serviceToken, 'base64').toString('hex');
|
||||
}
|
||||
await IndependentServiceToken.create({
|
||||
token: crypto.createHash('sha256').update(token).digest('hex'),
|
||||
client_id: clientID,
|
||||
title_id: serviceTokenOptions.title_id,
|
||||
pid: serviceTokenOptions.pid,
|
||||
info: {
|
||||
system_type: server.device,
|
||||
token_type: TokenType.IndependentService,
|
||||
title_id: BigInt(parseInt(titleID, 16)),
|
||||
issued: serviceTokenOptions.issued,
|
||||
expires: serviceTokenOptions.expires
|
||||
}
|
||||
});
|
||||
|
||||
response.send(xmlbuilder.create({
|
||||
service_token: {
|
||||
token: serviceToken
|
||||
token: token
|
||||
}
|
||||
}).end());
|
||||
});
|
||||
@@ -213,21 +223,20 @@ router.get('/nex_token/@me', async (request: express.Request, response: express.
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenOptions = {
|
||||
system_type: server.device,
|
||||
token_type: TokenType.NEX,
|
||||
const token = crypto.randomBytes(36).toString('base64');
|
||||
|
||||
await NEXToken.create({
|
||||
token: crypto.createHash('sha256').update(token).digest('hex'),
|
||||
game_server_id: gameServerID,
|
||||
pid: pnid.pid,
|
||||
access_level: pnid.access_level,
|
||||
title_id: BigInt(parseInt(titleID, 16)),
|
||||
expire_time: BigInt(Date.now() + (3600 * 1000))
|
||||
};
|
||||
|
||||
const nexTokenBuffer = await generateToken(server.aes_key, tokenOptions);
|
||||
let nexToken = nexTokenBuffer ? nexTokenBuffer.toString('base64') : '';
|
||||
|
||||
if (request.isCemu) {
|
||||
nexToken = Buffer.from(nexToken || '', 'base64').toString('hex');
|
||||
}
|
||||
info: {
|
||||
system_type: server.device,
|
||||
token_type: TokenType.NEX,
|
||||
title_id: BigInt(parseInt(titleID, 16)),
|
||||
issued: new Date(),
|
||||
expires: new Date(Date.now() + (3600 * 1000))
|
||||
}
|
||||
});
|
||||
|
||||
const connectInfo = await server.getServerConnectInfo();
|
||||
response.send(xmlbuilder.create({
|
||||
@@ -236,7 +245,7 @@ router.get('/nex_token/@me', async (request: express.Request, response: express.
|
||||
nex_password: nexAccount.password,
|
||||
pid: nexAccount.pid,
|
||||
port: connectInfo.port,
|
||||
token: nexToken
|
||||
token: token
|
||||
}
|
||||
}).end());
|
||||
});
|
||||
|
||||
6
src/types/common/service-token-options.ts
Normal file
6
src/types/common/service-token-options.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface ServiceTokenOptions {
|
||||
pid: number;
|
||||
title_id: string;
|
||||
issued: Date;
|
||||
expires: Date;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { SystemType } from '@/types/common/system-types';
|
||||
import type { TokenType } from '@/types/common/token-types';
|
||||
|
||||
export interface TokenOptions {
|
||||
system_type: SystemType;
|
||||
token_type: TokenType;
|
||||
pid: number;
|
||||
access_level?: number;
|
||||
title_id?: bigint;
|
||||
expire_time: bigint;
|
||||
}
|
||||
@@ -85,6 +85,7 @@ export interface IPNIDMethods {
|
||||
generateMiiImages(): Promise<void>;
|
||||
markForDeletion(): void;
|
||||
scrub(): Promise<void>;
|
||||
removeAllTokens(): Promise<void>;
|
||||
hasPermission(flag: PNIDPermissionFlag): boolean;
|
||||
addPermission(flag: PNIDPermissionFlag): void;
|
||||
clearPermission(flag: PNIDPermissionFlag): void;
|
||||
|
||||
216
src/types/services/nasc/ac-request-params.ts
Normal file
216
src/types/services/nasc/ac-request-params.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* Çommon request parameters found on all NASC `ac` requests.
|
||||
* All fields are base64 encoded using Nintendo's custom alphabet:
|
||||
* '+' -> '.', '/' -> '-', '=' -> '*'
|
||||
*/
|
||||
export interface NASCCommonACRequestParams {
|
||||
/**
|
||||
* Game server ID (`%08X`). This is the same as the `X-GameId` header.
|
||||
* Derived from the games default title ID (usually the Japanese title ID)
|
||||
*/
|
||||
gameid: string;
|
||||
|
||||
/**
|
||||
* Major and minor SDK version (`%03d%03d`). Always `000000`
|
||||
*/
|
||||
sdkver: string;
|
||||
|
||||
/**
|
||||
* Title ID (`%016X`)
|
||||
*/
|
||||
titleid: string;
|
||||
|
||||
/**
|
||||
* Product code. See https://3dsdb.com/
|
||||
*/
|
||||
gamecd: string;
|
||||
|
||||
/**
|
||||
* Title version (`%04X`)
|
||||
*/
|
||||
gamever: string;
|
||||
|
||||
/**
|
||||
* Game type
|
||||
*
|
||||
* - 0 = System
|
||||
* - 1 = Digital
|
||||
* - 2 = Cartridge
|
||||
*/
|
||||
mediatype: string;
|
||||
|
||||
/**
|
||||
* Unique ROM (game) ID.
|
||||
* Only present if the media type is 2 (cartridge)
|
||||
*/
|
||||
romid?: string;
|
||||
|
||||
/**
|
||||
* Product maker (company code)
|
||||
*/
|
||||
makercd: string;
|
||||
|
||||
/**
|
||||
* Unit code
|
||||
*
|
||||
* - 0 = NDS
|
||||
* - 1 = Wii
|
||||
* - 2 = 3DS
|
||||
*/
|
||||
unitcd: string;
|
||||
|
||||
/**
|
||||
* Device MAC address
|
||||
*/
|
||||
macadr: string;
|
||||
|
||||
/**
|
||||
* BSSID of active wifi network
|
||||
*/
|
||||
bssid: string;
|
||||
|
||||
/**
|
||||
* Information about the used Wi-Fi access point in the format `AA:BBBBBBBBBB`.
|
||||
* Example `01:0000000000`. `AA` is the AP slot. `BBBBBBBBBB` comes from either `ACU_GetNZoneApNumService` or `ACU_GetConnectingHotspotSubset` based on the result from `ACU_GetWifiStatus`
|
||||
*/
|
||||
apinfo: string;
|
||||
|
||||
/**
|
||||
* LocalFriendCodeSeed_B
|
||||
*/
|
||||
fcdcert: string;
|
||||
|
||||
/**
|
||||
* Device name (UTF-16-LE)
|
||||
*/
|
||||
devname: string;
|
||||
|
||||
/**
|
||||
* Environment (`L1` for production)
|
||||
*/
|
||||
servertype: string;
|
||||
|
||||
/**
|
||||
* FPD version (`%04X`). This is also included in the user agent.
|
||||
*/
|
||||
fpdver: string;
|
||||
|
||||
/**
|
||||
* Current device time (`%y%m%d%H%M%S`)
|
||||
*/
|
||||
devtime: string;
|
||||
|
||||
/**
|
||||
* Language code (`%02X`)
|
||||
*/
|
||||
lang: string;
|
||||
|
||||
/**
|
||||
* Region code (`%02X`)
|
||||
*/
|
||||
region: string;
|
||||
|
||||
/**
|
||||
* Serial number
|
||||
*/
|
||||
csnum: string;
|
||||
|
||||
/**
|
||||
* The type of action the console wishes to perform:
|
||||
*
|
||||
* - LOGIN = Register new game server account or login to existing on
|
||||
* - SVCLOC = Request service token
|
||||
* - nzchk = Unknown, but seems related to Nintendo Zone
|
||||
* - parse = Unknown
|
||||
* - message = Unknown
|
||||
*/
|
||||
action: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for when a console wants to register a new game server account.
|
||||
* See also NASCCommonRequestParams.
|
||||
* All fields are base64 encoded using Nintendo's custom alphabet:
|
||||
* '+' -> '.', '/' -> '-', '=' -> '*'
|
||||
*/
|
||||
export interface NASCRegistrationACRequestParams extends NASCCommonACRequestParams {
|
||||
/**
|
||||
* Game server account password the console wishes to use for the new account.
|
||||
* Can be any character between `\x21-\x5B` and `\x5D-\x7D`. Always 16 characters long
|
||||
*/
|
||||
passwd: string;
|
||||
|
||||
/**
|
||||
* Nickname provided by game.
|
||||
* Usually unused, leftover from the Wii
|
||||
*/
|
||||
ingamesn: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for when a console wants to login to an existing game server account.
|
||||
* See also NASCCommonRequestParams.
|
||||
* All fields are base64 encoded using Nintendo's custom alphabet:
|
||||
* '+' -> '.', '/' -> '-', '=' -> '*'
|
||||
*/
|
||||
export interface NASCLoginACRequestParams extends NASCCommonACRequestParams {
|
||||
/**
|
||||
* Hash of the user ID
|
||||
*/
|
||||
uidhmac: string;
|
||||
|
||||
/**
|
||||
* Game server account user ID/username.
|
||||
* Always the NEX account PID on 3DS
|
||||
*/
|
||||
userid: string;
|
||||
|
||||
/**
|
||||
* Nickname provided by game.
|
||||
* Usually unused, leftover from the Wii
|
||||
*/
|
||||
ingamesn: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for when a console wants to request a service token.
|
||||
* See also NASCCommonRequestParams.
|
||||
* All fields are base64 encoded using Nintendo's custom alphabet:
|
||||
* '+' -> '.', '/' -> '-', '=' -> '*'
|
||||
*/
|
||||
export interface NASCServiceTokenACRequestParams extends NASCCommonACRequestParams {
|
||||
/**
|
||||
* Hash of the user ID
|
||||
*/
|
||||
uidhmac: string;
|
||||
|
||||
/**
|
||||
* Game server account user ID/username.
|
||||
* Always the NEX account PID on 3DS
|
||||
*/
|
||||
userid: string;
|
||||
|
||||
/**
|
||||
* Unique hash assigned to each game, regardless of title ID.
|
||||
* Analogous to the NNAS client ID
|
||||
*/
|
||||
keyhash: string;
|
||||
|
||||
/**
|
||||
* Service request type. Changes the `svchost` response field.
|
||||
* This is likely a remnant from the original NAS/NASWII servers.
|
||||
*
|
||||
* - 0000 = "n/a"
|
||||
* - 9001 = "dls1.nintendowifi.net"
|
||||
*/
|
||||
svc: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Union type representing all possible NASC request parameter types.
|
||||
* The specific type used depends on the `action` field value:
|
||||
*
|
||||
* - `action: 'LOGIN'` = `NASCRegistrationRequestParams` or `NASCLoginRequestParams`
|
||||
* - `action: 'SVCLOC'` = `NASCServiceTokenRequestParams`
|
||||
*/
|
||||
export type NASCACRequestParams = NASCRegistrationACRequestParams | NASCLoginACRequestParams | NASCServiceTokenACRequestParams;
|
||||
@@ -1,12 +0,0 @@
|
||||
export interface NASCRequestParams {
|
||||
action: string;
|
||||
fcdcert: string;
|
||||
csnum: string;
|
||||
macadr: string;
|
||||
titleid: string;
|
||||
servertype: string;
|
||||
gameid: string;
|
||||
userid?: string;
|
||||
uidhmac?: string;
|
||||
passwd?: string;
|
||||
}
|
||||
133
src/util.ts
133
src/util.ts
@@ -2,24 +2,23 @@ import crypto from 'node:crypto';
|
||||
import path from 'node:path';
|
||||
import { S3 } from '@aws-sdk/client-s3';
|
||||
import fs from 'fs-extra';
|
||||
import bufferCrc32 from 'buffer-crc32';
|
||||
import { crc32 } from 'crc';
|
||||
import { CronJob } from 'cron';
|
||||
import { checkMarkedDeletions } from '@/database';
|
||||
import { sendMail, CreateEmail } from '@/mailer';
|
||||
import { SystemType } from '@/types/common/system-types';
|
||||
import { TokenType } from '@/types/common/token-types';
|
||||
import { config, disabledFeatures } from '@/config-manager';
|
||||
import { PasswordResetToken } from '@/models/password-reset-token';
|
||||
import { LOG_ERROR } from '@/logger';
|
||||
import type { IncomingHttpHeaders } from 'node:http';
|
||||
import type { ParsedQs } from 'qs';
|
||||
import type mongoose from 'mongoose';
|
||||
import type express from 'express';
|
||||
import type { ObjectCannedACL } from '@aws-sdk/client-s3';
|
||||
import type { TokenOptions } from '@/types/common/token-options';
|
||||
import type { Token } from '@/types/common/token';
|
||||
import type { IPNID, IPNIDMethods } from '@/types/mongoose/pnid';
|
||||
import type { SafeQs } from '@/types/common/safe-qs';
|
||||
import type { HydratedServerDocument } from '@/types/mongoose/server';
|
||||
import type { ServiceTokenOptions } from '@/types/common/service-token-options';
|
||||
|
||||
let s3: S3;
|
||||
|
||||
@@ -59,98 +58,22 @@ export function nintendoBase64Encode(decoded: string | Buffer): string {
|
||||
return encoded.replaceAll('+', '.').replaceAll('/', '-').replaceAll('=', '*');
|
||||
}
|
||||
|
||||
export function generateToken(key: string, options: TokenOptions): Buffer | null {
|
||||
let dataBuffer = Buffer.alloc(1 + 1 + 4 + 8);
|
||||
export function createServiceToken(server: HydratedServerDocument, options: ServiceTokenOptions): Buffer {
|
||||
const dataBuffer = Buffer.alloc(28);
|
||||
|
||||
dataBuffer.writeUInt8(options.system_type, 0x0);
|
||||
dataBuffer.writeUInt8(options.token_type, 0x1);
|
||||
dataBuffer.writeUInt32LE(options.pid, 0x2);
|
||||
dataBuffer.writeBigUInt64LE(options.expire_time, 0x6);
|
||||
dataBuffer.writeUInt32BE(options.pid, 0);
|
||||
dataBuffer.writeBigUInt64BE(BigInt(parseInt(options.title_id, 16)), 4);
|
||||
dataBuffer.writeBigUInt64BE(BigInt(options.issued.getTime()), 12);
|
||||
dataBuffer.writeBigUInt64BE(BigInt(options.expires.getTime()), 20);
|
||||
|
||||
if ((options.token_type !== TokenType.OAuthAccess && options.token_type !== TokenType.OAuthRefresh) || options.system_type === SystemType.API) {
|
||||
// * Access and refresh tokens have smaller bodies due to size constraints
|
||||
// * The API does not have this restraint, however
|
||||
if (options.title_id === undefined || options.access_level === undefined) {
|
||||
return null;
|
||||
}
|
||||
// * Not using AES anymore but fuck it, it's here already
|
||||
// TODO - rename the AES field
|
||||
const hmac = crypto.createHmac('sha256', server.aes_key).update(dataBuffer).digest();
|
||||
|
||||
dataBuffer = Buffer.concat([
|
||||
dataBuffer,
|
||||
Buffer.alloc(8 + 1)
|
||||
]);
|
||||
|
||||
dataBuffer.writeBigUInt64LE(options.title_id, 0xE);
|
||||
dataBuffer.writeInt8(options.access_level, 0x16);
|
||||
}
|
||||
|
||||
const iv = Buffer.alloc(16);
|
||||
const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key, 'hex'), iv);
|
||||
|
||||
const encrypted = Buffer.concat([
|
||||
cipher.update(dataBuffer),
|
||||
cipher.final()
|
||||
return Buffer.concat([
|
||||
dataBuffer,
|
||||
hmac
|
||||
]);
|
||||
|
||||
let final = encrypted;
|
||||
|
||||
if ((options.token_type !== TokenType.OAuthAccess && options.token_type !== TokenType.OAuthRefresh) || options.system_type === SystemType.API) {
|
||||
// * Access and refresh tokens don't get a checksum due to size constraints
|
||||
const checksum = bufferCrc32(dataBuffer);
|
||||
|
||||
final = Buffer.concat([
|
||||
checksum,
|
||||
final
|
||||
]);
|
||||
}
|
||||
|
||||
return final;
|
||||
}
|
||||
|
||||
export function decryptToken(token: Buffer, key?: string): Buffer {
|
||||
let encryptedBody: Buffer;
|
||||
let expectedChecksum = 0;
|
||||
|
||||
if (token.length === 16) {
|
||||
// * Token is an access/refresh token, no checksum
|
||||
encryptedBody = token;
|
||||
} else {
|
||||
expectedChecksum = token.readUint32BE();
|
||||
encryptedBody = token.subarray(4);
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
key = config.aes_key;
|
||||
}
|
||||
|
||||
const iv = Buffer.alloc(16);
|
||||
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key, 'hex'), iv);
|
||||
|
||||
const decrypted = Buffer.concat([
|
||||
decipher.update(encryptedBody),
|
||||
decipher.final()
|
||||
]);
|
||||
|
||||
if (expectedChecksum && (expectedChecksum !== crc32(decrypted))) {
|
||||
throw new Error('Checksum did not match. Failed decrypt. Are you using the right key?');
|
||||
}
|
||||
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
export function unpackToken(token: Buffer): Token {
|
||||
const unpacked: Token = {
|
||||
system_type: token.readUInt8(0x0),
|
||||
token_type: token.readUInt8(0x1),
|
||||
pid: token.readUInt32LE(0x2),
|
||||
expire_time: token.readBigUInt64LE(0x6)
|
||||
};
|
||||
|
||||
if (unpacked.token_type !== TokenType.OAuthAccess && unpacked.token_type !== TokenType.OAuthRefresh) {
|
||||
unpacked.title_id = token.readBigUInt64LE(0xE);
|
||||
unpacked.access_level = token.readInt8(0x16);
|
||||
}
|
||||
|
||||
return unpacked;
|
||||
}
|
||||
|
||||
export function fullUrl(request: express.Request): string {
|
||||
@@ -251,25 +174,25 @@ export async function sendEmailConfirmedParentalControlsEmail(pnid: mongoose.Hyd
|
||||
}
|
||||
|
||||
export async function sendForgotPasswordEmail(pnid: mongoose.HydratedDocument<IPNID, IPNIDMethods>): Promise<void> {
|
||||
const tokenOptions = {
|
||||
system_type: SystemType.PasswordReset,
|
||||
token_type: TokenType.PasswordReset,
|
||||
const token = crypto.randomBytes(36).toString('hex');
|
||||
|
||||
await PasswordResetToken.create({
|
||||
token: crypto.createHash('sha256').update(token).digest('hex'),
|
||||
pid: pnid.pid,
|
||||
access_level: pnid.access_level,
|
||||
title_id: BigInt(0),
|
||||
expire_time: BigInt(Date.now() + (24 * 60 * 60 * 1000)) // * Only valid for 24 hours
|
||||
};
|
||||
|
||||
const tokenBuffer = await generateToken(config.aes_key, tokenOptions);
|
||||
const passwordResetToken = tokenBuffer ? tokenBuffer.toString('hex') : '';
|
||||
|
||||
// TODO - Handle null token
|
||||
info: {
|
||||
system_type: SystemType.PasswordReset,
|
||||
token_type: TokenType.PasswordReset,
|
||||
title_id: BigInt(0),
|
||||
issued: new Date(),
|
||||
expires: new Date(Date.now() + (24 * 60 * 60 * 1000))
|
||||
}
|
||||
});
|
||||
|
||||
const email = new CreateEmail()
|
||||
.addHeader('Dear {{pnid}},', { pnid: pnid.username })
|
||||
.addParagraph('a password reset has been requested from this account.')
|
||||
.addParagraph('If you did not request the password reset, please ignore this email. If you did request this password reset, please click the link below to reset your password.')
|
||||
.addButton('Reset password', `${config.website_base}/account/reset-password?token=${encodeURIComponent(passwordResetToken)}`);
|
||||
.addButton('Reset password', `${config.website_base}/account/reset-password?token=${encodeURIComponent(token)}`);
|
||||
|
||||
const mailerOptions = {
|
||||
to: pnid.email.address,
|
||||
|
||||
Reference in New Issue
Block a user