Merge pull request #87 from PretendoNetwork/nnas-updates

This commit is contained in:
Jonathan Barrow
2024-04-16 13:05:47 -04:00
committed by GitHub
76 changed files with 745 additions and 802 deletions

View File

@@ -29,7 +29,7 @@
"no-extra-semi": "off",
"@typescript-eslint/no-extra-semi": "error",
"@typescript-eslint/no-empty-interface": "warn",
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/no-inferrable-types": "error",
"@typescript-eslint/explicit-function-return-type": "error",
"one-var": [
"error",

View File

@@ -9,7 +9,7 @@
"clean": "rimraf ./dist",
"copy-static": "npm run copy-assets && npm run copy-timezones",
"copy-assets": "cp -r ./src/assets ./dist/assets",
"copy-timezones": "cp ./src/services/nnid/timezones.json ./dist/services/nnid/timezones.json",
"copy-timezones": "cp ./src/services/nnas/timezones.json ./dist/services/nnas/timezones.json",
"start": "node .",
"start:dev": "NODE_ENV=development node ."
},

View File

@@ -4,9 +4,9 @@ import { config, disabledFeatures } from '@/config-manager';
let client: redis.RedisClientType;
const memoryCache: { [key: string]: Buffer } = {};
const memoryCache: Record<string, Buffer> = {};
const LOCAL_CDN_BASE: string = `${__dirname}/../cdn`;
const LOCAL_CDN_BASE = `${__dirname}/../cdn`;
export async function connect(): Promise<void> {
if (!disabledFeatures.redis) {
@@ -26,12 +26,12 @@ export async function setCachedFile(fileName: string, value: Buffer): Promise<vo
}
export async function getCachedFile(fileName: string, encoding?: BufferEncoding): Promise<Buffer> {
let cachedFile: Buffer = Buffer.alloc(0);
let cachedFile = Buffer.alloc(0);
if (disabledFeatures.redis) {
cachedFile = memoryCache[fileName] || null;
} else {
const redisValue: string | null = await client.get(fileName);
const redisValue = await client.get(fileName);
if (redisValue) {
cachedFile = Buffer.from(redisValue, encoding);
}
@@ -43,11 +43,11 @@ export async function getCachedFile(fileName: string, encoding?: BufferEncoding)
// * Local CDN cache functions
export async function getLocalCDNFile(name: string, encoding?: BufferEncoding): Promise<Buffer> {
let file: Buffer = await getCachedFile(`local_cdn:${name}`, encoding);
let file = await getCachedFile(`local_cdn:${name}`, encoding);
if (file === null) {
if (await fs.pathExists(`${LOCAL_CDN_BASE}/${name}`)) {
const fileBuffer: string | Buffer = await fs.readFile(`${LOCAL_CDN_BASE}/${name}`, { encoding });
const fileBuffer = await fs.readFile(`${LOCAL_CDN_BASE}/${name}`, { encoding });
file = Buffer.from(fileBuffer);
await setLocalCDNFile(name, file);
}

View File

@@ -2,11 +2,11 @@ import fs from 'fs-extra';
import mongoose from 'mongoose';
import dotenv from 'dotenv';
import { LOG_INFO, LOG_WARN, LOG_ERROR } from '@/logger';
import { Config, DisabledFeatures } from '@/types/common/config';
import { Config } from '@/types/common/config';
dotenv.config();
export const disabledFeatures: DisabledFeatures = {
export const disabledFeatures = {
redis: false,
email: false,
captcha: false,

View File

@@ -7,20 +7,18 @@ import { Server } from '@/models/server';
import { LOG_ERROR } from '@/logger';
import { config } from '@/config-manager';
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
import { IDevice } from '@/types/mongoose/device';
import { IDeviceAttribute } from '@/types/mongoose/device-attribute';
import { HydratedServerDocument } from '@/types/mongoose/server';
import { Token } from '@/types/common/token';
import { PNIDProfile } from '@/types/services/nnid/pnid-profile';
import { PNIDProfile } from '@/types/services/nnas/pnid-profile';
import { ConnectionData } from '@/types/services/api/connection-data';
import { ConnectionResponse } from '@/types/services/api/connection-response';
import { DiscordConnectionData } from '@/types/services/api/discord-connection-data';
const connection_string: string = config.mongoose.connection_string;
const options: mongoose.ConnectOptions = config.mongoose.options;
const connection_string = config.mongoose.connection_string;
const options = config.mongoose.options;
// TODO: Extend this later with more settings
const discordConnectionSchema: joi.ObjectSchema = joi.object({
// TODO - Extend this later with more settings
const discordConnectionSchema = joi.object({
id: joi.string()
});
@@ -79,19 +77,19 @@ export async function getPNIDByBasicAuth(token: string): Promise<HydratedPNIDDoc
// * Wii U sends Basic auth as `username password`, where the password may not have spaces
// * This is not to spec, but that is the consoles fault not ours
const decoded: string = Buffer.from(token, 'base64').toString();
const parts: string[] = decoded.split(' ');
const decoded = Buffer.from(token, 'base64').toString();
const parts = decoded.split(' ');
const username: string = parts[0];
const password: string = parts[1];
const username = parts[0];
const password = parts[1];
const pnid: HydratedPNIDDocument | null = await getPNIDByUsername(username);
const pnid = await getPNIDByUsername(username);
if (!pnid) {
return null;
}
const hashedPassword: string = nintendoPasswordHash(password, pnid.pid);
const hashedPassword = nintendoPasswordHash(password, pnid.pid);
if (!bcrypt.compareSync(hashedPassword, pnid.password)) {
return null;
@@ -104,13 +102,12 @@ export async function getPNIDByTokenAuth(token: string): Promise<HydratedPNIDDoc
verifyConnected();
try {
const decryptedToken: Buffer = decryptToken(Buffer.from(token, 'hex'));
const unpackedToken: Token = unpackToken(decryptedToken);
const pnid: HydratedPNIDDocument | null = await getPNIDByPID(unpackedToken.pid);
const decryptedToken = decryptToken(Buffer.from(token, 'hex'));
const unpackedToken = unpackToken(decryptedToken);
const pnid = await getPNIDByPID(unpackedToken.pid);
if (pnid) {
const expireTime: number = Math.floor((Number(unpackedToken.expire_time) / 1000));
const expireTime = Math.floor((Number(unpackedToken.expire_time) / 1000));
if (Math.floor(Date.now() / 1000) > expireTime) {
return null;
@@ -119,7 +116,7 @@ export async function getPNIDByTokenAuth(token: string): Promise<HydratedPNIDDoc
return pnid;
} catch (error: any) {
// TODO: Handle error
// TODO - Handle error
LOG_ERROR(error);
return null;
}
@@ -128,13 +125,13 @@ export async function getPNIDByTokenAuth(token: string): Promise<HydratedPNIDDoc
export async function getPNIDProfileJSONByPID(pid: number): Promise<PNIDProfile | null> {
verifyConnected();
const pnid: HydratedPNIDDocument | null = await getPNIDByPID(pid);
const pnid = await getPNIDByPID(pid);
if (!pnid) {
return null;
}
const device: IDevice = pnid.devices[0]; // * Just grab the first device
const device = pnid.devices[0]; // * Just grab the first device
let device_attributes: {
device_attribute: {
name: string;
@@ -145,9 +142,9 @@ export async function getPNIDProfileJSONByPID(pid: number): Promise<PNIDProfile
if (device) {
device_attributes = device.device_attributes.map((attribute: IDeviceAttribute) => {
const name: string = attribute.name;
const value: string = attribute.value;
const created_date: string | undefined = attribute.created_date;
const name = attribute.name;
const value = attribute.value;
const created_date = attribute.created_date;
return {
device_attribute: {
@@ -160,7 +157,7 @@ export async function getPNIDProfileJSONByPID(pid: number): Promise<PNIDProfile
}
return {
//accounts: {}, // * We need to figure this out, no idea what these values mean or what they do
// *accounts: {}, // * We need to figure this out, no idea what these values mean or what they do
active_flag: pnid.flags.active ? 'Y' : 'N',
birth_date: pnid.birthdate,
country: pnid.country,
@@ -237,7 +234,7 @@ export async function addPNIDConnection(pnid: HydratedPNIDDocument, data: Connec
}
export async function addPNIDConnectionDiscord(pnid: HydratedPNIDDocument, data: DiscordConnectionData): Promise<ConnectionResponse> {
const valid: joi.ValidationResult = discordConnectionSchema.validate(data);
const valid = discordConnectionSchema.validate(data);
if (valid.error) {
return {

View File

@@ -3,7 +3,7 @@ import colors from 'colors';
colors.enable();
const root: string = process.env.PN_ACT_LOGGER_PATH ? process.env.PN_ACT_LOGGER_PATH : `${__dirname}/..`;
const root = process.env.PN_ACT_LOGGER_PATH ? process.env.PN_ACT_LOGGER_PATH : `${__dirname}/..`;
fs.ensureDirSync(`${root}/logs`);
const streams = {
@@ -15,7 +15,7 @@ const streams = {
} as const;
export function LOG_SUCCESS(input: string): void {
const time: Date = new Date();
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [SUCCESS]: ${input}`;
streams.success.write(`${input}\n`);
@@ -23,7 +23,7 @@ export function LOG_SUCCESS(input: string): void {
}
export function LOG_ERROR(input: string): void {
const time: Date = new Date();
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [ERROR]: ${input}`;
streams.error.write(`${input}\n`);
@@ -31,7 +31,7 @@ export function LOG_ERROR(input: string): void {
}
export function LOG_WARN(input: string): void {
const time: Date = new Date();
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [WARN]: ${input}`;
streams.warn.write(`${input}\n`);
@@ -39,7 +39,7 @@ export function LOG_WARN(input: string): void {
}
export function LOG_INFO(input: string): void {
const time: Date = new Date();
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [INFO]: ${input}`;
streams.info.write(`${input}\n`);

View File

@@ -5,8 +5,8 @@ import * as aws from '@aws-sdk/client-ses';
import { config, disabledFeatures } from '@/config-manager';
import { MailerOptions } from '@/types/common/mailer-options';
const genericEmailTemplate: string = fs.readFileSync(path.join(__dirname, './assets/emails/genericTemplate.html'), 'utf8');
const confirmationEmailTemplate: string = fs.readFileSync(path.join(__dirname, './assets/emails/confirmationTemplate.html'), 'utf8');
const genericEmailTemplate = fs.readFileSync(path.join(__dirname, './assets/emails/genericTemplate.html'), 'utf8');
const confirmationEmailTemplate = fs.readFileSync(path.join(__dirname, './assets/emails/confirmationTemplate.html'), 'utf8');
let transporter: nodemailer.Transporter;
@@ -32,7 +32,7 @@ export async function sendMail(options: MailerOptions): Promise<void> {
if (!disabledFeatures.email) {
const { to, subject, username, paragraph, preview, text, link, confirmation } = options;
let html: string = confirmation ? confirmationEmailTemplate : genericEmailTemplate;
let html = confirmation ? confirmationEmailTemplate : genericEmailTemplate;
html = html.replace(/{{username}}/g, username);
html = html.replace(/{{paragraph}}/g, paragraph || '');
@@ -43,7 +43,7 @@ export async function sendMail(options: MailerOptions): Promise<void> {
if (link) {
const { href, text } = link;
const button: string = `<tr><td width="100%" height="16px" style="line-height: 16px;">&nbsp;</td></tr><tr><td class="confirm-link" bgcolor="#673db6" style="font-size: 14px; font-weight: 700; border-radius: 10px; padding: 12px" align="center"><a href="${href}" style="text-decoration: none; color: #ffffff; " width="100%">${text}</a></td></tr>`;
const button = `<tr><td width="100%" height="16px" style="line-height: 16px;">&nbsp;</td></tr><tr><td class="confirm-link" bgcolor="#673db6" style="font-size: 14px; font-weight: 700; border-radius: 10px; padding: 12px" align="center"><a href="${href}" style="text-decoration: none; color: #ffffff; " width="100%">${text}</a></td></tr>`;
html = html.replace(/<!--{{buttonPlaceholder}}-->/g, button);
}

View File

@@ -1,18 +1,17 @@
import express from 'express';
import { getValueFromHeaders } from '@/util';
import { getPNIDByTokenAuth } from '@/database';
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
async function APIMiddleware(request: express.Request, _response: express.Response, next: express.NextFunction): Promise<void> {
const authHeader: string | undefined = getValueFromHeaders(request.headers, 'authorization');
const authHeader = getValueFromHeaders(request.headers, 'authorization');
if (!authHeader || !(authHeader.startsWith('Bearer'))) {
return next();
}
try {
const token: string = authHeader.split(' ')[1];
const pnid: HydratedPNIDDocument | null = await getPNIDByTokenAuth(token);
const token = authHeader.split(' ')[1];
const pnid = await getPNIDByTokenAuth(token);
request.pnid = pnid;
} catch (error) {

View File

@@ -1,7 +1,7 @@
import express from 'express';
function CemuMiddleware(request: express.Request, _response: express.Response, next: express.NextFunction): void {
const subdomain: string = request.subdomains.reverse().join('.');
const subdomain = request.subdomains.reverse().join('.');
request.isCemu = subdomain === 'c.account';

View File

@@ -2,7 +2,7 @@ import express from 'express';
import xmlbuilder from 'xmlbuilder';
import { getValueFromHeaders } from '@/util';
const VALID_CLIENT_ID_SECRET_PAIRS: { [key: string]: string } = {
const VALID_CLIENT_ID_SECRET_PAIRS: Record<string, string> = {
// * 'Key' is the client ID, 'Value' is the client secret
'a2efa818a34fa16b8afbc8a74eba3eda': 'c91cdb5658bd4954ade78533a339cf9a', // * Possibly WiiU exclusive?
'daf6227853bcbdce3d75baee8332b': '3eff548eac636e2bf45bb7b375e7b6b0', // * Possibly 3DS exclusive?
@@ -14,14 +14,14 @@ function nintendoClientHeaderCheck(request: express.Request, response: express.R
response.set('Server', 'Nintendo 3DS (http)');
response.set('X-Nintendo-Date', new Date().getTime().toString());
const clientId: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-client-id');
const clientSecret: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-client-secret');
const clientID = getValueFromHeaders(request.headers, 'x-nintendo-client-id');
const clientSecret = getValueFromHeaders(request.headers, 'x-nintendo-client-secret');
if (
!clientId ||
!clientID ||
!clientSecret ||
!VALID_CLIENT_ID_SECRET_PAIRS[clientId] ||
clientSecret !== VALID_CLIENT_ID_SECRET_PAIRS[clientId]
!VALID_CLIENT_ID_SECRET_PAIRS[clientID] ||
clientSecret !== VALID_CLIENT_ID_SECRET_PAIRS[clientID]
) {
response.send(xmlbuilder.create({
errors: {

View File

@@ -3,7 +3,6 @@ import express from 'express';
import xmlbuilder from 'xmlbuilder';
import { Device } from '@/models/device';
import { getValueFromHeaders } from '@/util';
import { HydratedDeviceDocument } from '@/types/mongoose/device';
async function consoleStatusVerificationMiddleware(request: express.Request, response: express.Response, next: express.NextFunction): Promise<void> {
if (!request.certificate || !request.certificate.valid) {
@@ -17,7 +16,7 @@ async function consoleStatusVerificationMiddleware(request: express.Request, res
return;
}
const deviceIDHeader: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-device-id');
const deviceIDHeader = getValueFromHeaders(request.headers, 'x-nintendo-device-id');
if (!deviceIDHeader) {
response.status(400).send(xmlbuilder.create({
@@ -30,7 +29,7 @@ async function consoleStatusVerificationMiddleware(request: express.Request, res
return;
}
const deviceID: number = Number(deviceIDHeader);
const deviceID = Number(deviceIDHeader);
if (isNaN(deviceID)) {
response.status(400).send(xmlbuilder.create({
@@ -43,7 +42,7 @@ async function consoleStatusVerificationMiddleware(request: express.Request, res
return;
}
const serialNumber: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-serial-number');
const serialNumber = getValueFromHeaders(request.headers, 'x-nintendo-serial-number');
// TODO - Verify serial numbers somehow?
// * This is difficult to do safely because serial numbers are
@@ -69,70 +68,90 @@ async function consoleStatusVerificationMiddleware(request: express.Request, res
return;
}
// * This is kinda temp for now. Needs to be redone to handle linking this data to existing 3DS devices in the DB
// TODO - 3DS consoles are created in the NASC middleware. They need special handling to link them up with the data in the NNID API!
if (request.certificate.consoleType === 'wiiu') {
const certificateDeviceID: number = parseInt(request.certificate.certificateName.slice(2), 16);
let device = await Device.findOne({
serial: serialNumber,
});
if (deviceID !== certificateDeviceID) {
// TODO - Change this to a different error
response.status(400).send(xmlbuilder.create({
error: {
cause: 'Bad Request',
code: '1600',
message: 'Unable to process request'
}
}).end());
const certificateHash = crypto.createHash('sha256').update(request.certificate._certificate).digest('base64');
return;
}
if (!device && request.certificate.consoleType === '3ds') {
// * A 3DS console document will ALWAYS be created by NASC before
// * Hitting the NNAS server. NASC stores the serial number at
// * the time the device document was created. Therefore we can
// * know that serial tampering happened on the 3DS if this fails
// * to find a device document.
response.status(400).send(xmlbuilder.create({
error: {
code: '0002',
message: 'serialNumber format is invalid'
}
}).end());
// * Only store a hash of the certificate in case of a breach
const certificateHash: string = crypto.createHash('sha256').update(request.certificate._certificate).digest('base64');
return;
} else if (device && !device.certificate_hash && request.certificate.consoleType === '3ds') {
device.certificate_hash = certificateHash;
let device: HydratedDeviceDocument | null = await Device.findOne({
certificate_hash: certificateHash,
});
if (!device) {
device = await Device.create({
model: 'wup',
device_id: deviceID,
serial: serialNumber,
linked_pids: [],
certificate_hash: certificateHash
});
}
if (device.serial !== serialNumber) {
// TODO - Change this to a different error
response.status(400).send(xmlbuilder.create({
error: {
cause: 'Bad Request',
code: '1600',
message: 'Unable to process request'
}
}).end());
return;
}
if (device.access_level < 0) {
response.status(400).send(xmlbuilder.create({
errors: {
error: {
code: '0012',
message: 'Device has been banned by game server' // TODO - This is not the right error message
}
}
}).end());
return;
}
request.device = device;
await device.save();
}
device = await Device.findOne({
certificate_hash: certificateHash,
});
if (!device) {
// * Device must be a fresh Wii U
device = await Device.create({
model: 'wup',
device_id: deviceID,
serial: serialNumber,
linked_pids: [],
certificate_hash: certificateHash
});
}
if (device.serial !== serialNumber) {
// TODO - Change this to a different error
response.status(400).send(xmlbuilder.create({
error: {
cause: 'Bad Request',
code: '1600',
message: 'Unable to process request'
}
}).end());
return;
}
const certificateDeviceID = parseInt(request.certificate.certificateName.slice(2).split('-')[0], 16);
if (deviceID !== certificateDeviceID) {
// TODO - Change this to a different error
response.status(400).send(xmlbuilder.create({
error: {
cause: 'Bad Request',
code: '1600',
message: 'Unable to process request'
}
}).end());
return;
}
if (device.access_level < 0) {
response.status(400).send(xmlbuilder.create({
errors: {
error: {
code: '0012',
message: 'Device has been banned by game server' // TODO - This is not the right error message
}
}
}).end());
return;
}
request.device = device;
return next();
}

View File

@@ -3,7 +3,7 @@ import NintendoCertificate from '@/nintendo-certificate';
import { getValueFromHeaders } from '@/util';
function deviceCertificateMiddleware(request: express.Request, _response: express.Response, next: express.NextFunction): void {
const certificate: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-device-cert');
const certificate = getValueFromHeaders(request.headers, 'x-nintendo-device-cert');
if (!certificate) {
return next();

View File

@@ -1,6 +1,5 @@
import crypto from 'node:crypto';
import express from 'express';
import mongoose from 'mongoose';
import { Device } from '@/models/device';
import { NEXAccount } from '@/models/nex-account';
import { nascError, nintendoBase64Decode } from '@/util';
@@ -8,8 +7,6 @@ import { connection as databaseConnection } from '@/database';
import NintendoCertificate from '@/nintendo-certificate';
import { LOG_ERROR } from '@/logger';
import { NASCRequestParams } from '@/types/services/nasc/request-params';
import { HydratedNEXAccountDocument } from '@/types/mongoose/nex-account';
import { HydratedDeviceDocument } from '@/types/mongoose/device';
async function NASCMiddleware(request: express.Request, response: express.Response, next: express.NextFunction): Promise<void> {
const requestParams: NASCRequestParams = request.body;
@@ -21,23 +18,23 @@ async function NASCMiddleware(request: express.Request, response: express.Respon
!requestParams.titleid ||
!requestParams.servertype
) {
response.status(200).send(nascError('null').toString()); // This is what Nintendo sends
response.status(200).send(nascError('null').toString()); // * This is what Nintendo sends
return;
}
const action: string = nintendoBase64Decode(requestParams.action).toString();
const fcdcert: Buffer = nintendoBase64Decode(requestParams.fcdcert);
const serialNumber: string = nintendoBase64Decode(requestParams.csnum).toString();
const macAddress: string = nintendoBase64Decode(requestParams.macadr).toString();
const titleID: string = nintendoBase64Decode(requestParams.titleid).toString();
const environment: string = nintendoBase64Decode(requestParams.servertype).toString();
const action = nintendoBase64Decode(requestParams.action).toString();
const fcdcert = nintendoBase64Decode(requestParams.fcdcert);
const serialNumber = nintendoBase64Decode(requestParams.csnum).toString();
const macAddress = nintendoBase64Decode(requestParams.macadr).toString();
const titleID = nintendoBase64Decode(requestParams.titleid).toString();
const environment = nintendoBase64Decode(requestParams.servertype).toString();
const macAddressHash: string = crypto.createHash('sha256').update(macAddress).digest('base64');
const fcdcertHash: string = crypto.createHash('sha256').update(fcdcert).digest('base64');
const macAddressHash = crypto.createHash('sha256').update(macAddress).digest('base64');
const fcdcertHash = crypto.createHash('sha256').update(fcdcert).digest('base64');
let pid: number = 0; // * Real PIDs are always positive and non-zero
let pidHmac: string = '';
let password: string = '';
let pid = 0; // * Real PIDs are always positive and non-zero
let pidHmac = '';
let password = '';
if (requestParams.userid) {
pid = Number(nintendoBase64Decode(requestParams.userid).toString());
@@ -52,11 +49,11 @@ async function NASCMiddleware(request: express.Request, response: express.Respon
}
if (action !== 'LOGIN' && action !== 'SVCLOC') {
response.status(200).send(nascError('null').toString()); // This is what Nintendo sends
response.status(200).send(nascError('null').toString()); // * This is what Nintendo sends
return;
}
const cert: NintendoCertificate = new NintendoCertificate(fcdcert);
const cert = new NintendoCertificate(fcdcert);
if (!cert.valid) {
response.status(200).send(nascError('121').toString());
@@ -68,7 +65,7 @@ async function NASCMiddleware(request: express.Request, response: express.Respon
return;
}
let model: string = '';
let model = '';
switch (serialNumber[0]) {
case 'C':
model = 'ctr';
@@ -95,7 +92,7 @@ async function NASCMiddleware(request: express.Request, response: express.Respon
return;
}
let nexAccount: HydratedNEXAccountDocument | null = null;
let nexAccount = null;
if (pid) {
nexAccount = await NEXAccount.findOne({ pid });
@@ -106,7 +103,7 @@ async function NASCMiddleware(request: express.Request, response: express.Respon
}
let device: HydratedDeviceDocument | null = await Device.findOne({
let device = await Device.findOne({
fcdcert_hash: fcdcertHash,
});
@@ -117,7 +114,7 @@ async function NASCMiddleware(request: express.Request, response: express.Respon
}
if (pid) {
const linkedPIDs: number[] = device.linked_pids;
const linkedPIDs = device.linked_pids;
// * If a user performs a system transfer from
// * a console to another using a Nintendo account
@@ -138,11 +135,6 @@ async function NASCMiddleware(request: express.Request, response: express.Respon
response.status(200).send(nascError('102').toString());
return;
}
if (device.mac_hash !== macAddressHash) {
response.status(200).send(nascError('102').toString());
return;
}
}
// * Workaround for edge case on system transfers
@@ -167,13 +159,13 @@ async function NASCMiddleware(request: express.Request, response: express.Respon
if (titleID === '0004013000003202') {
if (password && !pid && !pidHmac) {
// Register new user
// * Register new user
const session: mongoose.ClientSession = await databaseConnection().startSession();
const session = await databaseConnection().startSession();
await session.startTransaction();
try {
// Create new NEX account
// * Create new NEX account
nexAccount = new NEXAccount({
device_type: '3ds',
password
@@ -185,21 +177,21 @@ async function NASCMiddleware(request: express.Request, response: express.Respon
pid = nexAccount.pid;
const pidBuffer: Buffer = Buffer.alloc(4);
const pidBuffer = Buffer.alloc(4);
pidBuffer.writeUInt32LE(pid);
const hash: crypto.Hash = crypto.createHash('sha1').update(pidBuffer);
const pidHash: Buffer = hash.digest();
const checksum: number = pidHash[0] >> 1;
const hex: string = checksum.toString(16) + pid.toString(16);
const int: number = parseInt(hex, 16);
const friendCode: string = int.toString().padStart(12, '0').match(/.{1,4}/g)!.join('-');
const hash = crypto.createHash('sha1').update(pidBuffer);
const pidHash = hash.digest();
const checksum = pidHash[0] >> 1;
const hex = checksum.toString(16) + pid.toString(16);
const int = parseInt(hex, 16);
const friendCode = int.toString().padStart(12, '0').match(/.{1,4}/g)!.join('-');
nexAccount.friend_code = friendCode;
await nexAccount.save({ session });
// Set password
// * Set password
if (!device) {
device = new Device({
@@ -222,7 +214,7 @@ async function NASCMiddleware(request: express.Request, response: express.Respon
await session.abortTransaction();
// 3DS expects 200 even on error
// * 3DS expects 200 even on error
response.status(200).send(nascError('102').toString());
return;
} finally {
@@ -238,9 +230,9 @@ async function NASCMiddleware(request: express.Request, response: express.Respon
return next();
}
// https://www.adminsub.net/mac-address-finder/nintendo
// Saves us from doing an OUI lookup each time
const NINTENDO_VENDER_OUIS: string[] = [
// * https://www.adminsub.net/mac-address-finder/nintendo
// * Saves us from doing an OUI lookup each time
const NINTENDO_VENDER_OUIS = [
'ECC40D', 'E84ECE', 'E0F6B5', 'E0E751', 'E00C7F', 'DC68EB',
'D86BF7', 'D4F057', 'CCFB65', 'CC9E00', 'B8AE6E', 'B88AEC',
'B87826', 'A4C0E1', 'A45C27', 'A438CC', '9CE635', '98E8FA',
@@ -265,10 +257,10 @@ const NINTENDO_VENDER_OUIS: string[] = [
'001AE9', '0019FD', '00191D', '0017AB', '001656', '0009BF'
];
// TODO: Make something better
const MAC_REGEX: RegExp = /^[0-9a-fA-F]{12}$/;
// TODO - Make something better
const MAC_REGEX = /^[0-9a-fA-F]{12}$/;
// Maybe should later parse more data out
// * Maybe should later parse more data out
function validNintendoMACAddress(macAddress: string): boolean {
if (!NINTENDO_VENDER_OUIS.includes(macAddress.substring(0, 6).toUpperCase())) {
return false;

View File

@@ -5,15 +5,15 @@ import { getPNIDByBasicAuth, getPNIDByTokenAuth } from '@/database';
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
async function PNIDMiddleware(request: express.Request, response: express.Response, next: express.NextFunction): Promise<void> {
const authHeader: string | undefined = getValueFromHeaders(request.headers, 'authorization');
const authHeader = getValueFromHeaders(request.headers, 'authorization');
if (!authHeader || !(authHeader.startsWith('Bearer') || authHeader.startsWith('Basic'))) {
return next();
}
const parts: string[] = authHeader.split(' ');
const type: string = parts[0];
let token: string = parts[1];
const parts = authHeader.split(' ');
const type = parts[0];
let token = parts[1];
let pnid: HydratedPNIDDocument | null;
if (request.isCemu) {

View File

@@ -7,7 +7,7 @@ export default ratelimit({
windowMs: 60 * 1000,
max: 1,
keyGenerator: (request: express.Request): string => {
let data: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-device-cert');
let data = getValueFromHeaders(request.headers, 'x-nintendo-device-cert');
if (!data) {
data = request.ip;

View File

@@ -5,9 +5,9 @@ import { getValueFromHeaders, mapToObject } from '@/util';
function XMLMiddleware(request: express.Request, response: express.Response, next: express.NextFunction): void {
if (request.method == 'POST' || request.method == 'PUT') {
const contentType: string | undefined = getValueFromHeaders(request.headers, 'content-type');
const contentLength: string | undefined = getValueFromHeaders(request.headers, 'content-length');
let body: string = '';
const contentType = getValueFromHeaders(request.headers, 'content-type');
const contentLength = getValueFromHeaders(request.headers, 'content-length');
let body = '';
if (
!contentType ||
@@ -34,7 +34,7 @@ function XMLMiddleware(request: express.Request, response: express.Response, nex
request.body = request.body.toObject();
request.body = mapToObject(request.body);
} catch (error) {
// TODO: This is not a real error code, check to see if better one exists
// TODO - This is not a real error code, check to see if better one exists
return response.status(401).send(xmlbuilder.create({
errors: {
error: {

View File

@@ -35,13 +35,13 @@ export const DeviceSchema = new Schema<IDevice, DeviceModel, IDeviceMethods>({
linked_pids: [Number],
access_level: {
type: Number,
default: 0 // 0: standard, 1: tester, 2: mod?, 3: dev
default: 0 // * 0: standard, 1: tester, 2: mod?, 3: dev
},
server_access_level: {
type: String,
default: 'prod' // everyone is in production by default
default: 'prod' // * everyone is in production by default
},
certificate_hash: String
});
export const Device: DeviceModel = model<IDevice, DeviceModel>('Device', DeviceSchema);
export const Device = model<IDevice, DeviceModel>('Device', DeviceSchema);

View File

@@ -1,12 +1,12 @@
import { Schema, model } from 'mongoose';
import uniqueValidator from 'mongoose-unique-validator';
import { HydratedNEXAccountDocument, INEXAccount, INEXAccountMethods, NEXAccountModel } from '@/types/mongoose/nex-account';
import { INEXAccount, INEXAccountMethods, NEXAccountModel } from '@/types/mongoose/nex-account';
const NEXAccountSchema = new Schema<INEXAccount, NEXAccountModel, INEXAccountMethods>({
device_type: {
type: String,
enum: [
// Only track the family here not the model
// * Only track the family here not the model
'wiiu',
'3ds',
]
@@ -19,11 +19,11 @@ const NEXAccountSchema = new Schema<INEXAccount, NEXAccountModel, INEXAccountMet
owning_pid: Number,
access_level: {
type: Number,
default: 0 // 0: standard, 1: tester, 2: mod?, 3: dev
default: 0 // * 0: standard, 1: tester, 2: mod?, 3: dev
},
server_access_level: {
type: String,
default: 'prod' // everyone is in production by default
default: 'prod' // * everyone is in production by default
},
friend_code: String
});
@@ -39,12 +39,12 @@ NEXAccountSchema.plugin(uniqueValidator, { message: '{PATH} already in use.' });
and the next few accounts counting down seem to be admin, service and internal test accounts
*/
NEXAccountSchema.method('generatePID', async function generatePID(): Promise<void> {
const min: number = 1000000000; // The console (WiiU) seems to not accept PIDs smaller than this
const max: number = 1799999999;
const min = 1000000000; // * The console (WiiU) seems to not accept PIDs smaller than this
const max = 1799999999;
const pid: number = Math.floor(Math.random() * (max - min + 1) + min);
const pid = Math.floor(Math.random() * (max - min + 1) + min);
const inuse: HydratedNEXAccountDocument | null = await NEXAccount.findOne({ pid });
const inuse = await NEXAccount.findOne({ pid });
if (inuse) {
await this.generatePID();
@@ -55,13 +55,13 @@ NEXAccountSchema.method('generatePID', async function generatePID(): Promise<voi
NEXAccountSchema.method('generatePassword', function generatePassword(): void {
function character(): string | number {
const offset: number = Math.floor(Math.random() * 62);
const offset = Math.floor(Math.random() * 62);
if (offset < 10) return offset;
if (offset < 36) return String.fromCharCode(offset + 55);
return String.fromCharCode(offset + 61);
}
const output: string[] = [];
const output = [];
while (output.length < 16) {
output.push(String(character()));
@@ -70,4 +70,4 @@ NEXAccountSchema.method('generatePassword', function generatePassword(): void {
this.password = output.join('');
});
export const NEXAccount: NEXAccountModel = model<INEXAccount, NEXAccountModel>('NEXAccount', NEXAccountSchema);
export const NEXAccount = model<INEXAccount, NEXAccountModel>('NEXAccount', NEXAccountSchema);

View File

@@ -9,7 +9,7 @@ import Stripe from 'stripe';
import { DeviceSchema } from '@/models/device';
import { uploadCDNAsset } from '@/util';
import { LOG_ERROR, LOG_WARN } from '@/logger';
import { HydratedPNIDDocument, IPNID, IPNIDMethods, PNIDModel } from '@/types/mongoose/pnid';
import { IPNID, IPNIDMethods, PNIDModel } from '@/types/mongoose/pnid';
import { PNIDPermissionFlag } from '@/types/common/permission-flags';
import { config } from '@/config-manager';
@@ -33,11 +33,11 @@ const PNIDSchema = new Schema<IPNID, PNIDModel, IPNIDMethods>({
},
access_level: {
type: Number,
default: 0 // 0: standard, 1: tester, 2: mod?, 3: dev
default: 0 // * 0: standard, 1: tester, 2: mod?, 3: dev
},
server_access_level: {
type: String,
default: 'prod' // everyone is in production by default
default: 'prod' // * everyone is in production by default
},
pid: {
type: Number,
@@ -89,7 +89,7 @@ const PNIDSchema = new Schema<IPNID, PNIDModel, IPNIDMethods>({
off_device: Boolean
},
devices: [DeviceSchema],
identification: { // user identification tokens
identification: { // * user identification tokens
email_code: {
type: String,
unique: true
@@ -133,12 +133,12 @@ PNIDSchema.plugin(uniqueValidator, {message: '{PATH} already in use.'});
and the next few accounts counting down seem to be admin, service and internal test accounts
*/
PNIDSchema.method('generatePID', async function generatePID(): Promise<void> {
const min: number = 1000000000; // The console (WiiU) seems to not accept PIDs smaller than this
const max: number = 1799999999;
const min = 1000000000; // * The console (WiiU) seems to not accept PIDs smaller than this
const max = 1799999999;
const pid: number = Math.floor(Math.random() * (max - min + 1) + min);
const pid = Math.floor(Math.random() * (max - min + 1) + min);
const inuse: HydratedPNIDDocument | null = await PNID.findOne({
const inuse = await PNID.findOne({
pid
});
@@ -150,17 +150,17 @@ PNIDSchema.method('generatePID', async function generatePID(): Promise<void> {
});
PNIDSchema.method('generateEmailValidationCode', async function generateEmailValidationCode(): Promise<void> {
// WiiU passes the PID along with the email code
// Does not actually need to be unique to all users
const code: string = Math.random().toFixed(6).split('.')[1]; // Dirty one-liner to generate numbers of 6 length and padded 0
// * WiiU passes the PID along with the email code
// * Does not actually need to be unique to all users
const code = Math.random().toFixed(6).split('.')[1]; // * Dirty one-liner to generate numbers of 6 length and padded 0
this.identification.email_code = code;
});
PNIDSchema.method('generateEmailValidationToken', async function generateEmailValidationToken(): Promise<void> {
const token: string = crypto.randomBytes(32).toString('hex');
const token = crypto.randomBytes(32).toString('hex');
const inuse: HydratedPNIDDocument | null = await PNID.findOne({
const inuse = await PNID.findOne({
'identification.email_token': token
});
@@ -185,40 +185,40 @@ PNIDSchema.method('updateMii', async function updateMii({ name, primary, data }:
});
PNIDSchema.method('generateMiiImages', async function generateMiiImages(): Promise<void> {
const miiData: string = this.mii.data;
const mii: Mii = new Mii(Buffer.from(miiData, 'base64'));
const miiStudioUrl: string = mii.studioUrl({
const miiData = this.mii.data;
const mii = new Mii(Buffer.from(miiData, 'base64'));
const miiStudioUrl = mii.studioUrl({
type: 'face',
width: 128,
instanceCount: 1,
});
const miiStudioNormalFaceImageData: Buffer = await got(miiStudioUrl).buffer();
const pngData: ImageData = await imagePixels(miiStudioNormalFaceImageData);
const tga: Buffer = TGA.createTgaBuffer(pngData.width, pngData.height, Uint8Array.from(pngData.data), false);
const miiStudioNormalFaceImageData = await got(miiStudioUrl).buffer();
const pngData = await imagePixels(miiStudioNormalFaceImageData);
const tga = TGA.createTgaBuffer(pngData.width, pngData.height, Uint8Array.from(pngData.data), false);
const userMiiKey: string = `mii/${this.pid}`;
const userMiiKey = `mii/${this.pid}`;
await uploadCDNAsset('pn-cdn', `${userMiiKey}/standard.tga`, tga, 'public-read');
await uploadCDNAsset('pn-cdn', `${userMiiKey}/normal_face.png`, miiStudioNormalFaceImageData, 'public-read');
const expressions: string[] = ['frustrated', 'smile_open_mouth', 'wink_left', 'sorrow', 'surprise_open_mouth'];
const expressions = ['frustrated', 'smile_open_mouth', 'wink_left', 'sorrow', 'surprise_open_mouth'];
for (const expression of expressions) {
const miiStudioExpressionUrl: string = mii.studioUrl({
const miiStudioExpressionUrl = mii.studioUrl({
type: 'face',
expression: expression,
width: 128,
instanceCount: 1,
});
const miiStudioExpressionImageData: Buffer = await got(miiStudioExpressionUrl).buffer();
const miiStudioExpressionImageData = await got(miiStudioExpressionUrl).buffer();
await uploadCDNAsset('pn-cdn', `${userMiiKey}/${expression}.png`, miiStudioExpressionImageData, 'public-read');
}
const miiStudioBodyUrl: string = mii.studioUrl({
const miiStudioBodyUrl = mii.studioUrl({
type: 'all_body',
width: 270,
instanceCount: 1,
});
const miiStudioBodyImageData: Buffer = await got(miiStudioBodyUrl).buffer();
const miiStudioBodyImageData = await got(miiStudioBodyUrl).buffer();
await uploadCDNAsset('pn-cdn', `${userMiiKey}/body.png`, miiStudioBodyImageData, 'public-read');
});
@@ -290,4 +290,4 @@ PNIDSchema.method('clearPermission', function clearPermission(flag: PNIDPermissi
this.permissions &= ~flag;
});
export const PNID: PNIDModel = model<IPNID, PNIDModel>('PNID', PNIDSchema);
export const PNID = model<IPNID, PNIDModel>('PNID', PNIDSchema);

View File

@@ -18,4 +18,4 @@ const ServerSchema = new Schema<IServer, ServerModel, IServerMethods>({
ServerSchema.plugin(uniqueValidator, { message: '{PATH} already in use.' });
export const Server: ServerModel = model<IServer, ServerModel>('Server', ServerSchema);
export const Server = model<IServer, ServerModel>('Server', ServerSchema);

View File

@@ -41,7 +41,7 @@ const CTR_LFCS_B_PUB = Buffer.from([
0xAF, 0x07, 0xEB, 0x9C, 0xBF, 0xA9, 0xC9
]);
// Signature options
// * Signature options
const SIGNATURE_SIZES = {
RSA_4096_SHA1: <SignatureSize>{
SIZE: 0x200,
@@ -77,7 +77,7 @@ class NintendoCertificate {
issuer: string;
keyType: number;
certificateName: string;
ngKeyId: number;
ngKeyID: number;
publicKey: Buffer;
valid: boolean;
publicKeyData: Buffer;
@@ -91,7 +91,7 @@ class NintendoCertificate {
this.issuer = '';
this.keyType = 0;
this.certificateName = '';
this.ngKeyId = 0;
this.ngKeyID = 0;
this.publicKey = Buffer.alloc(0);
this.valid = false;
this.publicKeyData = Buffer.alloc(0);
@@ -120,7 +120,7 @@ class NintendoCertificate {
// * Assume regular certificate
this.signatureType = this._certificate.readUInt32BE(0x00);
const signatureTypeSizes: SignatureSize = this._signatureTypeSizes(this.signatureType);
const signatureTypeSizes = this._signatureTypeSizes(this.signatureType);
this._certificateBody = this._certificate.subarray(0x4 + signatureTypeSizes.SIZE + signatureTypeSizes.PADDING_SIZE);
@@ -128,7 +128,7 @@ class NintendoCertificate {
this.issuer = this._certificate.subarray(0x80, 0xC0).toString().split('\0')[0];
this.keyType = this._certificate.readUInt32BE(0xC0);
this.certificateName = this._certificate.subarray(0xC4, 0x104).toString().split('\0')[0];
this.ngKeyId = this._certificate.readUInt32BE(0x104);
this.ngKeyID = this._certificate.readUInt32BE(0x104);
this.publicKeyData = this._certificate.subarray(0x108);
if (this.issuer === 'Root-CA00000003-MS00000012') {
@@ -180,7 +180,7 @@ class NintendoCertificate {
}
_verifySignatureRSA4096(): void {
const publicKey: NodeRSA = new NodeRSA();
const publicKey = new NodeRSA();
publicKey.importKey({
n: this.publicKeyData.subarray(0x0, 0x200),
@@ -191,7 +191,7 @@ class NintendoCertificate {
}
_verifySignatureRSA2048(): void {
const publicKey: NodeRSA = new NodeRSA();
const publicKey = new NodeRSA();
publicKey.importKey({
n: this.publicKeyData.subarray(0x0, 0x100),
@@ -201,13 +201,13 @@ class NintendoCertificate {
this.valid = publicKey.verify(this._certificateBody, this.signature);
}
// Huge thanks to Myria for helping get ECDSA working
// with Nodes native crypto module and getting the keys
// from bytes to PEM!
// https://github.com/Myriachan
// * Huge thanks to Myria for helping get ECDSA working
// * with Nodes native crypto module and getting the keys
// * from bytes to PEM!
// * https://github.com/Myriachan
_verifySignatureECDSA(): void {
const pem: string = this.consoleType === 'wiiu' ? WIIU_DEVICE_PUB_PEM : CTR_DEVICE_PUB_PEM;
const key: crypto.VerifyPublicKeyInput = {
const pem = this.consoleType === 'wiiu' ? WIIU_DEVICE_PUB_PEM : CTR_DEVICE_PUB_PEM;
const key = {
key: pem,
dsaEncoding: 'ieee-p1363' as crypto.DSAEncoding
};
@@ -216,7 +216,7 @@ class NintendoCertificate {
}
_verifySignatureLFCS(): void {
const publicKey: NodeRSA = new NodeRSA();
const publicKey = new NodeRSA();
publicKey.importKey({
n: CTR_LFCS_B_PUB,

View File

@@ -16,7 +16,7 @@ import { LOG_INFO, LOG_SUCCESS, LOG_WARN } from '@/logger';
import conntest from '@/services/conntest';
import cbvc from '@/services/cbvc';
import nnid from '@/services/nnid';
import nnas from '@/services/nnas';
import nasc from '@/services/nasc';
import datastore from '@/services/datastore';
import api from '@/services/api';
@@ -25,11 +25,11 @@ import assets from '@/services/assets';
import { config } from '@/config-manager';
const app: express.Express = express();
const app = express();
// START APPLICATION
// * START APPLICATION
// Create router
// * Create router
LOG_INFO('Setting up Middleware');
app.use(morgan('dev'));
app.use(express.json());
@@ -38,27 +38,27 @@ app.use(express.urlencoded({
}));
app.use(xmlparser);
// import the servers into one
// * import the servers into one
app.use(conntest);
app.use(cbvc);
app.use(nnid);
app.use(nnas);
app.use(nasc);
app.use(datastore);
app.use(api);
app.use(localcdn);
app.use(assets);
// 404 handler
// * 404 handler
LOG_INFO('Creating 404 status handler');
app.use((request: express.Request, response: express.Response): void => {
const url: string = fullUrl(request);
let deviceId: string | undefined = getValueFromHeaders(request.headers, 'X-Nintendo-Device-ID');
const url = fullUrl(request);
let deviceID = getValueFromHeaders(request.headers, 'X-Nintendo-Device-ID');
if (!deviceId) {
deviceId = 'Unknown';
if (!deviceID) {
deviceID = 'Unknown';
}
LOG_WARN(`HTTP 404 at ${url} from ${deviceId}`);
LOG_WARN(`HTTP 404 at ${url} from ${deviceID}`);
response.set('Content-Type', 'text/xml');
response.set('Server', 'Nintendo 3DS (http)');
@@ -75,18 +75,18 @@ app.use((request: express.Request, response: express.Response): void => {
}).end());
});
// non-404 error handler
// * non-404 error handler
LOG_INFO('Creating non-404 status handler');
app.use((error: any, request: express.Request, response: express.Response, _next: express.NextFunction): void => {
const status: number = error.status || 500;
const url: string = fullUrl(request);
let deviceId: string | undefined = getValueFromHeaders(request.headers, 'X-Nintendo-Device-ID');
const status = error.status || 500;
const url = fullUrl(request);
let deviceID = getValueFromHeaders(request.headers, 'X-Nintendo-Device-ID');
if (!deviceId) {
deviceId = 'Unknown';
if (!deviceID) {
deviceID = 'Unknown';
}
LOG_WARN(`HTTP ${status} at ${url} from ${deviceId}: ${error.message}`);
LOG_WARN(`HTTP ${status} at ${url} from ${deviceID}: ${error.message}`);
response.status(status).json({
app: 'api',
@@ -96,7 +96,7 @@ app.use((error: any, request: express.Request, response: express.Response, _next
});
async function main(): Promise<void> {
// Starts the server
// * Starts the server
LOG_INFO('Starting server');
await connectDatabase();

View File

@@ -6,15 +6,15 @@ import { LOG_INFO } from '@/logger';
import { V1 } from '@/services/api/routes';
// Router to handle the subdomain restriction
const api: express.Router = express.Router();
// * Router to handle the subdomain restriction
const api = express.Router();
LOG_INFO('[USER API] Importing middleware');
api.use(APIMiddleware);
api.use(cors());
api.options('*', cors());
// Setup routes
// * Setup routes
LOG_INFO('[USER API] Applying imported routes');
api.use('/v1/connections', V1.CONNECTIONS);
api.use('/v1/email', V1.EMAIL);
@@ -25,10 +25,10 @@ api.use('/v1/reset-password', V1.RESET_PASSWORD);
api.use('/v1/user', V1.USER);
// Main router for endpoints
const router: express.Router = express.Router();
// * Main router for endpoints
const router = express.Router();
// Create subdomains
// * Create subdomains
LOG_INFO('[USER API] Creating \'api\' subdomain');
router.use(subdomain('api', api));

View File

@@ -1,12 +1,9 @@
import express from 'express';
import { addPNIDConnection, removePNIDConnection } from '@/database';
import { ConnectionData } from '@/types/services/api/connection-data';
import { ConnectionResponse } from '@/types/services/api/connection-response';
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
const router: express.Router = express.Router();
const router = express.Router();
const VALID_CONNECTION_TYPES: string[] = [
const VALID_CONNECTION_TYPES = [
'discord'
];
@@ -16,9 +13,9 @@ const VALID_CONNECTION_TYPES: string[] = [
* Description: Adds an account connection to the users PNID
*/
router.post('/add/:type', async (request: express.Request, response: express.Response): Promise<void> => {
const data: ConnectionData = request.body?.data;
const pnid: HydratedPNIDDocument | null = request.pnid;
const type: string = request.params.type;
const data = request.body?.data;
const pnid = request.pnid;
const type = request.params.type;
if (!pnid) {
response.status(400).json({
@@ -50,7 +47,7 @@ router.post('/add/:type', async (request: express.Request, response: express.Res
return;
}
let result: ConnectionResponse | undefined = await addPNIDConnection(pnid, data, type);
let result = await addPNIDConnection(pnid, data, type);
if (!result) {
result = {
@@ -69,8 +66,8 @@ router.post('/add/:type', async (request: express.Request, response: express.Res
* Description: Removes an account connection from the users PNID
*/
router.delete('/remove/:type', async (request: express.Request, response: express.Response): Promise<void> => {
const pnid: HydratedPNIDDocument | null = request.pnid;
const type: string = request.params.type;
const pnid = request.pnid;
const type = request.params.type;
if (!pnid) {
response.status(400).json({
@@ -92,7 +89,7 @@ router.delete('/remove/:type', async (request: express.Request, response: expres
return;
}
let result: ConnectionResponse | undefined = await removePNIDConnection(pnid, type);
let result = await removePNIDConnection(pnid, type);
if (!result) {
result = {

View File

@@ -2,12 +2,11 @@ import express from 'express';
import moment from 'moment';
import { PNID } from '@/models/pnid';
import { getValueFromQueryString, sendEmailConfirmedEmail } from '@/util';
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
const router: express.Router = express.Router();
const router = express.Router();
router.get('/verify', async (request: express.Request, response: express.Response): Promise<void> => {
const token: string | undefined = getValueFromQueryString(request.query, 'token');
const token = getValueFromQueryString(request.query, 'token');
if (!token || token.trim() == '') {
response.status(400).json({
@@ -19,7 +18,7 @@ router.get('/verify', async (request: express.Request, response: express.Respons
return;
}
const pnid: HydratedPNIDDocument | null = await PNID.findOne({
const pnid = await PNID.findOne({
'identification.email_token': token
});
@@ -33,7 +32,7 @@ router.get('/verify', async (request: express.Request, response: express.Respons
return;
}
const validatedDate: string = moment().format('YYYY-MM-DDTHH:MM:SS');
const validatedDate = moment().format('YYYY-MM-DDTHH:MM:SS');
pnid.email.reachable = true;
pnid.email.validated = true;

View File

@@ -4,10 +4,10 @@ import { getPNIDByEmailAddress, getPNIDByUsername } from '@/database';
import { sendForgotPasswordEmail } from '@/util';
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
const router: express.Router = express.Router();
const router = express.Router();
router.post('/', async (request: express.Request, response: express.Response): Promise<void> => {
const input: string = request.body?.input;
const input = request.body?.input;
if (!input || input.trim() === '') {
response.status(400).json({

View File

@@ -3,10 +3,9 @@ import bcrypt from 'bcrypt';
import { getPNIDByUsername, getPNIDByTokenAuth } from '@/database';
import { nintendoPasswordHash, generateToken} from '@/util';
import { config } from '@/config-manager';
import { TokenOptions } from '@/types/common/token-options';
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
const router: express.Router = express.Router();
const router = express.Router();
/**
* [POST]
@@ -15,10 +14,10 @@ const router: express.Router = express.Router();
* TODO: Replace this with a more robust OAuth2 implementation
*/
router.post('/', async (request: express.Request, response: express.Response): Promise<void> => {
const grantType: string = request.body?.grant_type;
const username: string = request.body?.username;
const password: string = request.body?.password;
const refreshToken: string = request.body?.refresh_token;
const grantType = request.body?.grant_type;
const username = request.body?.username;
const password = request.body?.password;
const refreshToken = request.body?.refresh_token;
if (!['password', 'refresh_token'].includes(grantType)) {
response.status(400).json({
@@ -75,7 +74,7 @@ router.post('/', async (request: express.Request, response: express.Response): P
return;
}
const hashedPassword: string = nintendoPasswordHash(password, pnid.pid);
const hashedPassword = nintendoPasswordHash(password, pnid.pid);
if (!pnid || !bcrypt.compareSync(hashedPassword, pnid.password)) {
response.status(400).json({
@@ -100,7 +99,7 @@ router.post('/', async (request: express.Request, response: express.Response): P
}
}
const accessTokenOptions: TokenOptions = {
const accessTokenOptions = {
system_type: 0x3, // * API
token_type: 0x1, // * OAuth Access
pid: pnid.pid,
@@ -109,7 +108,7 @@ router.post('/', async (request: express.Request, response: express.Response): P
expire_time: BigInt(Date.now() + (3600 * 1000))
};
const refreshTokenOptions: TokenOptions = {
const refreshTokenOptions = {
system_type: 0x3, // * API
token_type: 0x2, // * OAuth Refresh
pid: pnid.pid,
@@ -118,11 +117,11 @@ router.post('/', async (request: express.Request, response: express.Response): P
expire_time: BigInt(Date.now() + (3600 * 1000))
};
const accessTokenBuffer: Buffer | null = await generateToken(config.aes_key, accessTokenOptions);
const refreshTokenBuffer: Buffer | null = await generateToken(config.aes_key, refreshTokenOptions);
const accessTokenBuffer = await generateToken(config.aes_key, accessTokenOptions);
const refreshTokenBuffer = await generateToken(config.aes_key, refreshTokenOptions);
const accessToken: string = accessTokenBuffer ? accessTokenBuffer.toString('hex') : '';
const newRefreshToken: string = refreshTokenBuffer ? refreshTokenBuffer.toString('hex') : '';
const accessToken = accessTokenBuffer ? accessTokenBuffer.toString('hex') : '';
const newRefreshToken = refreshTokenBuffer ? refreshTokenBuffer.toString('hex') : '';
// TODO - Handle null tokens

View File

@@ -6,31 +6,29 @@ import bcrypt from 'bcrypt';
import moment from 'moment';
import hcaptcha from 'hcaptcha';
import Mii from 'mii-js';
import mongoose from 'mongoose';
import { doesPNIDExist, connection as databaseConnection } from '@/database';
import { nintendoPasswordHash, sendConfirmationEmail, generateToken } from '@/util';
import { LOG_ERROR } from '@/logger';
import { PNID } from '@/models/pnid';
import { NEXAccount } from '@/models/nex-account';
import { config, disabledFeatures } from '@/config-manager';
import { TokenOptions } from '@/types/common/token-options';
import { HydratedNEXAccountDocument } from '@/types/mongoose/nex-account';
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
const router: express.Router = express.Router();
const router = express.Router();
const PNID_VALID_CHARACTERS_REGEX: RegExp = /^[\w\-.]*$/;
const PNID_PUNCTUATION_START_REGEX: RegExp = /^[_\-.]/;
const PNID_PUNCTUATION_END_REGEX: RegExp = /[_\-.]$/;
const PNID_PUNCTUATION_DUPLICATE_REGEX: RegExp = /[_\-.]{2,}/;
const PNID_VALID_CHARACTERS_REGEX = /^[\w\-.]*$/;
const PNID_PUNCTUATION_START_REGEX = /^[_\-.]/;
const PNID_PUNCTUATION_END_REGEX = /[_\-.]$/;
const PNID_PUNCTUATION_DUPLICATE_REGEX = /[_\-.]{2,}/;
// This sucks
const PASSWORD_WORD_OR_NUMBER_REGEX: RegExp = /(?=.*[a-zA-Z])(?=.*\d).*/;
const PASSWORD_WORD_OR_PUNCTUATION_REGEX: RegExp = /(?=.*[a-zA-Z])(?=.*[_\-.]).*/;
const PASSWORD_NUMBER_OR_PUNCTUATION_REGEX: RegExp = /(?=.*\d)(?=.*[_\-.]).*/;
const PASSWORD_REPEATED_CHARACTER_REGEX: RegExp = /(.)\1\1/;
// * This sucks
const PASSWORD_WORD_OR_NUMBER_REGEX = /(?=.*[a-zA-Z])(?=.*\d).*/;
const PASSWORD_WORD_OR_PUNCTUATION_REGEX = /(?=.*[a-zA-Z])(?=.*[_\-.]).*/;
const PASSWORD_NUMBER_OR_PUNCTUATION_REGEX = /(?=.*\d)(?=.*[_\-.]).*/;
const PASSWORD_REPEATED_CHARACTER_REGEX = /(.)\1\1/;
const DEFAULT_MII_DATA: Buffer = Buffer.from('AwAAQOlVognnx0GC2/uogAOzuI0n2QAAAEBEAGUAZgBhAHUAbAB0AAAAAAAAAEBAAAAhAQJoRBgmNEYUgRIXaA0AACkAUkhQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGm9', 'base64');
const DEFAULT_MII_DATA = Buffer.from('AwAAQOlVognnx0GC2/uogAOzuI0n2QAAAEBEAGUAZgBhAHUAbAB0AAAAAAAAAEBAAAAhAQJoRBgmNEYUgRIXaA0AACkAUkhQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGm9', 'base64');
/**
* [POST]
@@ -38,12 +36,12 @@ const DEFAULT_MII_DATA: Buffer = Buffer.from('AwAAQOlVognnx0GC2/uogAOzuI0n2QAAAE
* Description: Creates a new user PNID
*/
router.post('/', async (request: express.Request, response: express.Response): Promise<void> => {
const email: string = request.body.email?.trim();
const username: string = request.body.username?.trim();
const miiName: string = request.body.mii_name?.trim();
const password: string = request.body.password?.trim();
const passwordConfirm: string = request.body.password_confirm?.trim();
const hCaptchaResponse: string = request.body.hCaptchaResponse?.trim();
const email = request.body.email?.trim();
const username = request.body.username?.trim();
const miiName = request.body.mii_name?.trim();
const password = request.body.password?.trim();
const passwordConfirm = request.body.password_confirm?.trim();
const hCaptchaResponse = request.body.hCaptchaResponse?.trim();
if (!disabledFeatures.captcha) {
if (!hCaptchaResponse || hCaptchaResponse === '') {
@@ -56,7 +54,7 @@ router.post('/', async (request: express.Request, response: express.Response): P
return;
}
const captchaVerify: VerifyResponse = await hcaptcha.verify(config.hcaptcha.secret, hCaptchaResponse);
const captchaVerify = await hcaptcha.verify(config.hcaptcha.secret, hCaptchaResponse);
if (!captchaVerify.success) {
response.status(400).json({
@@ -160,7 +158,7 @@ router.post('/', async (request: express.Request, response: express.Response): P
return;
}
const userExists: boolean = await doesPNIDExist(username);
const userExists = await doesPNIDExist(username);
if (userExists) {
response.status(400).json({
@@ -252,7 +250,7 @@ router.post('/', async (request: express.Request, response: express.Response): P
return;
}
const miiNameBuffer: Buffer = Buffer.from(miiName, 'utf16le'); // UTF8 to UTF16
const miiNameBuffer = Buffer.from(miiName, 'utf16le'); // * UTF8 to UTF16
if (miiNameBuffer.length > 0x14) {
response.status(400).json({
@@ -264,14 +262,14 @@ router.post('/', async (request: express.Request, response: express.Response): P
return;
}
const mii: Mii = new Mii(DEFAULT_MII_DATA);
const mii = new Mii(DEFAULT_MII_DATA);
mii.miiName = miiName;
const creationDate: string = moment().format('YYYY-MM-DDTHH:MM:SS');
const creationDate = moment().format('YYYY-MM-DDTHH:MM:SS');
let pnid: HydratedPNIDDocument;
let nexAccount: HydratedNEXAccountDocument;
const session: mongoose.ClientSession = await databaseConnection().startSession();
const session = await databaseConnection().startSession();
await session.startTransaction();
try {
@@ -284,17 +282,17 @@ router.post('/', async (request: express.Request, response: express.Response): P
await nexAccount.generatePID();
await nexAccount.generatePassword();
// Quick hack to get the PIDs to match
// TODO: Change this maybe?
// NN with a NNID will always use the NNID PID
// even if the provided NEX PID is different
// To fix this we make them the same PID
// * Quick hack to get the PIDs to match
// TODO - Change this maybe?
// * NN with a NNID will always use the NNID PID
// * even if the provided NEX PID is different
// * To fix this we make them the same PID
nexAccount.owning_pid = nexAccount.pid;
await nexAccount.save({ session });
const primaryPasswordHash: string = nintendoPasswordHash(password, nexAccount.pid);
const passwordHash: string = await bcrypt.hash(primaryPasswordHash, 10);
const primaryPasswordHash = nintendoPasswordHash(password, nexAccount.pid);
const passwordHash = await bcrypt.hash(primaryPasswordHash, 10);
pnid = new PNID({
pid: nexAccount.pid,
@@ -303,40 +301,40 @@ router.post('/', async (request: express.Request, response: express.Response): P
username: username,
usernameLower: username.toLowerCase(),
password: passwordHash,
birthdate: '1990-01-01', // TODO: Change this
gender: 'M', // TODO: Change this
country: 'US', // TODO: Change this
language: 'en', // TODO: Change this
birthdate: '1990-01-01', // TODO - Change this
gender: 'M', // TODO - Change this
country: 'US', // TODO - Change this
language: 'en', // TODO - Change this
email: {
address: email.toLowerCase(),
primary: true, // TODO: Change this
parent: true, // TODO: Change this
reachable: false, // TODO: Change this
validated: false, // TODO: Change this
primary: true, // TODO - Change this
parent: true, // TODO - Change this
reachable: false, // TODO - Change this
validated: false, // TODO - Change this
id: crypto.randomBytes(4).readUInt32LE()
},
region: 0x310B0000, // TODO: Change this
region: 0x310B0000, // TODO - Change this
timezone: {
name: 'America/New_York', // TODO: Change this
offset: -14400 // TODO: Change this
name: 'America/New_York', // TODO - Change this
offset: -14400 // TODO - Change this
},
mii: {
name: miiName,
primary: true, // TODO: Change this
primary: true, // TODO - Change this
data: mii.encode().toString('base64'),
id: crypto.randomBytes(4).readUInt32LE(),
hash: crypto.randomBytes(7).toString('hex'),
image_url: '', // deprecated, will be removed in the future
image_url: '', // * deprecated, will be removed in the future
image_id: crypto.randomBytes(4).readUInt32LE()
},
flags: {
active: true, // TODO: Change this
marketing: true, // TODO: Change this
off_device: true // TODO: Change this
active: true, // TODO - Change this
marketing: true, // TODO - Change this
off_device: true // TODO - Change this
},
identification: {
email_code: 1, // will be overwritten before saving
email_token: '' // will be overwritten before saving
email_code: 1, // * will be overwritten before saving
email_token: '' // * will be overwritten before saving
}
});
@@ -367,7 +365,7 @@ router.post('/', async (request: express.Request, response: express.Response): P
await sendConfirmationEmail(pnid);
const accessTokenOptions: TokenOptions = {
const accessTokenOptions = {
system_type: 0x3, // * API
token_type: 0x1, // * OAuth Access
pid: pnid.pid,
@@ -376,7 +374,7 @@ router.post('/', async (request: express.Request, response: express.Response): P
expire_time: BigInt(Date.now() + (3600 * 1000))
};
const refreshTokenOptions: TokenOptions = {
const refreshTokenOptions = {
system_type: 0x3, // * API
token_type: 0x2, // * OAuth Refresh
pid: pnid.pid,
@@ -385,11 +383,11 @@ router.post('/', async (request: express.Request, response: express.Response): P
expire_time: BigInt(Date.now() + (3600 * 1000))
};
const accessTokenBuffer: Buffer | null = await generateToken(config.aes_key, accessTokenOptions);
const refreshTokenBuffer: Buffer | null = await generateToken(config.aes_key, refreshTokenOptions);
const accessTokenBuffer = await generateToken(config.aes_key, accessTokenOptions);
const refreshTokenBuffer = await generateToken(config.aes_key, refreshTokenOptions);
const accessToken: string = accessTokenBuffer ? accessTokenBuffer.toString('hex') : '';
const refreshToken: string = refreshTokenBuffer ? refreshTokenBuffer.toString('hex') : '';
const accessToken = accessTokenBuffer ? accessTokenBuffer.toString('hex') : '';
const refreshToken = refreshTokenBuffer ? refreshTokenBuffer.toString('hex') : '';
// TODO - Handle null tokens

View File

@@ -3,20 +3,19 @@ import bcrypt from 'bcrypt';
import { PNID } from '@/models/pnid';
import { decryptToken, unpackToken, nintendoPasswordHash } from '@/util';
import { Token } from '@/types/common/token';
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
const router: express.Router = express.Router();
const router = express.Router();
// This sucks
const PASSWORD_WORD_OR_NUMBER_REGEX: RegExp = /(?=.*[a-zA-Z])(?=.*\d).*/;
const PASSWORD_WORD_OR_PUNCTUATION_REGEX: RegExp = /(?=.*[a-zA-Z])(?=.*[_\-.]).*/;
const PASSWORD_NUMBER_OR_PUNCTUATION_REGEX: RegExp = /(?=.*\d)(?=.*[_\-.]).*/;
const PASSWORD_REPEATED_CHARACTER_REGEX: RegExp = /(.)\1\1/;
// * This sucks
const PASSWORD_WORD_OR_NUMBER_REGEX = /(?=.*[a-zA-Z])(?=.*\d).*/;
const PASSWORD_WORD_OR_PUNCTUATION_REGEX = /(?=.*[a-zA-Z])(?=.*[_\-.]).*/;
const PASSWORD_NUMBER_OR_PUNCTUATION_REGEX = /(?=.*\d)(?=.*[_\-.]).*/;
const PASSWORD_REPEATED_CHARACTER_REGEX = /(.)\1\1/;
router.post('/', async (request: express.Request, response: express.Response): Promise<void> => {
const password: string = request.body.password?.trim();
const passwordConfirm: string = request.body.password_confirm?.trim();
const token: string = request.body.token?.trim();
const password = request.body.password?.trim();
const passwordConfirm = request.body.password_confirm?.trim();
const token = request.body.token?.trim();
if (!token || token === '') {
response.status(400).json({
@@ -30,7 +29,7 @@ router.post('/', async (request: express.Request, response: express.Response): P
let unpackedToken: Token;
try {
const decryptedToken: Buffer = await decryptToken(Buffer.from(token, 'hex'));
const decryptedToken = await decryptToken(Buffer.from(token, 'hex'));
unpackedToken = unpackToken(decryptedToken);
} catch (error) {
response.status(400).json({
@@ -52,7 +51,7 @@ router.post('/', async (request: express.Request, response: express.Response): P
return;
}
const pnid: HydratedPNIDDocument | null = await PNID.findOne({ pid: unpackedToken.pid });
const pnid = await PNID.findOne({ pid: unpackedToken.pid });
if (!pnid) {
response.status(400).json({
@@ -135,8 +134,8 @@ router.post('/', async (request: express.Request, response: express.Response): P
return;
}
const primaryPasswordHash: string = nintendoPasswordHash(password, pnid.pid);
const passwordHash: string = await bcrypt.hash(primaryPasswordHash, 10);
const primaryPasswordHash = nintendoPasswordHash(password, pnid.pid);
const passwordHash = await bcrypt.hash(primaryPasswordHash, 10);
pnid.password = passwordHash;

View File

@@ -3,10 +3,9 @@ import { z } from 'zod';
import Mii from 'mii-js';
import { config } from '@/config-manager';
import { PNID } from '@/models/pnid';
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
import { UpdateUserRequest } from '@/types/services/api/update-user-request';
const router: express.Router = express.Router();
const router = express.Router();
// TODO - Extend this later with more settings
const userSchema = z.object({
@@ -24,7 +23,7 @@ const userSchema = z.object({
* Description: Gets PNID details about the current user
*/
router.get('/', async (request: express.Request, response: express.Response): Promise<void> => {
const pnid: HydratedPNIDDocument | null = request.pnid;
const pnid = request.pnid;
if (!pnid) {
response.status(400).json({
@@ -74,7 +73,7 @@ router.get('/', async (request: express.Request, response: express.Response): Pr
* Description: Updates PNID certain details about the current user
*/
router.post('/', async (request: express.Request, response: express.Response): Promise<void> => {
const pnid: HydratedPNIDDocument | null = request.pnid;
const pnid = request.pnid;
const updateUserRequest: UpdateUserRequest = request.body;
if (!pnid) {
@@ -100,7 +99,7 @@ router.post('/', async (request: express.Request, response: express.Response): P
}
if (result.data.mii) {
const miiNameBuffer: Buffer = Buffer.from(result.data.mii.name, 'utf16le'); // * UTF8 to UTF16
const miiNameBuffer = Buffer.from(result.data.mii.name, 'utf16le'); // * UTF8 to UTF16
if (miiNameBuffer.length < 1) {
response.status(400).json({
@@ -123,7 +122,7 @@ router.post('/', async (request: express.Request, response: express.Response): P
}
try {
const miiDataBuffer: Buffer = Buffer.from(result.data.mii.data, 'base64');
const miiDataBuffer = Buffer.from(result.data.mii.data, 'base64');
if (miiDataBuffer.length < 0x60) {
response.status(400).json({
@@ -145,7 +144,7 @@ router.post('/', async (request: express.Request, response: express.Response): P
return;
}
const mii: Mii = new Mii(miiDataBuffer);
const mii = new Mii(miiDataBuffer);
mii.validate();
} catch (_) {
response.status(400).json({
@@ -167,7 +166,7 @@ router.post('/', async (request: express.Request, response: express.Response): P
const updateData: Record<string, any> = {};
if (result.data.environment) {
const environment: string = result.data.environment;
const environment = result.data.environment;
if (environment === 'test' && pnid.access_level < 1) {
response.status(400).json({

View File

@@ -1,21 +1,21 @@
// handles serving assets
// * handles serving assets
import path from 'node:path';
import express from 'express';
import subdomain from 'express-subdomain';
import { LOG_INFO } from '@/logger';
// Router to handle the subdomain restriction
const assets: express.Router = express.Router();
// * Router to handle the subdomain restriction
const assets = express.Router();
// Setup public folder
// * Setup public folder
LOG_INFO('[assets] Setting up public folder');
assets.use(express.static(path.join(__dirname, '../../assets')));
// Main router for endpoints
const router: express.Router = express.Router();
// * Main router for endpoints
const router = express.Router();
// Create subdomains
// * Create subdomains
LOG_INFO('[conntest] Creating \'assets\' subdomain');
router.use(subdomain('assets', assets));

View File

@@ -1,13 +1,13 @@
// handles CBVC (CTR Browser Version Check?) endpoints
// * handles CBVC (CTR Browser Version Check?) endpoints
import express from 'express';
import subdomain from 'express-subdomain';
import { LOG_INFO } from '@/logger';
// Router to handle the subdomain restriction
const cbvc: express.Router = express.Router();
// * Router to handle the subdomain restriction
const cbvc = express.Router();
// Setup route
// * Setup route
LOG_INFO('[cbvc] Applying imported routes');
cbvc.get('/:consoleType/:unknown/:region', (request: express.Request, response: express.Response): void => {
response.set('Content-Type', 'text/plain');
@@ -21,10 +21,10 @@ cbvc.get('/:consoleType/:unknown/:region', (request: express.Request, response:
response.send('0');
});
// Main router for endpoints
const router: express.Router = express.Router();
// * Main router for endpoints
const router = express.Router();
// Create subdomains
// * Create subdomains
LOG_INFO('[cbvc] Creating \'cbvc\' subdomain');
router.use(subdomain('cbvc.cdn', cbvc));

View File

@@ -1,20 +1,20 @@
// handles conntest endpoints
// * handles conntest endpoints
import express from 'express';
import subdomain from 'express-subdomain';
import { LOG_INFO } from '@/logger';
// Router to handle the subdomain restriction
const conntest: express.Router = express.Router();
// * Router to handle the subdomain restriction
const conntest = express.Router();
// Setup route
// * Setup route
LOG_INFO('[conntest] Applying imported routes');
conntest.get('/', (request: express.Request, response: express.Response): void => {
response.set('Content-Type', 'text/html');
response.set('X-Organization', 'Nintendo');
response.send(`
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<!DOCTYPE html PUBLIC "-// *W3C// *DTD XHTML 1.0 Transitional// *EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>HTML Page</title>
@@ -26,10 +26,10 @@ This is test.html page
`);
});
// Main router for endpoints
const router: express.Router = express.Router();
// * Main router for endpoints
const router = express.Router();
// Create subdomains
// * Create subdomains
LOG_INFO('[conntest] Creating \'conntest\' subdomain');
router.use(subdomain('conntest', conntest));

View File

@@ -4,17 +4,17 @@ import { LOG_INFO } from '@/logger';
import upload from '@/services/datastore/routes/upload';
// Router to handle the subdomain
const datastore: express.Router = express.Router();
// * Router to handle the subdomain
const datastore = express.Router();
// Setup routes
// * Setup routes
LOG_INFO('[DATASTORE] Applying imported routes');
datastore.use(upload);
// Main router for endpoints
const router: express.Router = express.Router();
// * Main router for endpoints
const router = express.Router();
// Create subdomains
// * Create subdomains
LOG_INFO('[DATASTORE] Creating \'datastore\' subdomain');
router.use(subdomain('datastore', datastore));

View File

@@ -4,32 +4,32 @@ import express from 'express';
import Dicer from 'dicer';
import { uploadCDNAsset } from '@/util';
const router: express.Router = express.Router();
const router = express.Router();
const signatureSecret: Buffer = fs.readFileSync(`${__dirname}/../../../../certs/nex/datastore/secret.key`);
const signatureSecret = fs.readFileSync(`${__dirname}/../../../../certs/nex/datastore/secret.key`);
function multipartParser(request: express.Request, response: express.Response, next: express.NextFunction): void {
const RE_BOUNDARY: RegExp = /^multipart\/.+?(?:; boundary=(?:(?:"(.+)")|(?:([^\s]+))))$/i;
const RE_FILE_NAME: RegExp = /name="(.*)"/;
const RE_BOUNDARY = /^multipart\/.+?(?:; boundary=(?:(?:"(.+)")|(?:([^\s]+))))$/i;
const RE_FILE_NAME = /name="(.*)"/;
const contentType: string | undefined = request.header('content-type');
const contentType = request.header('content-type');
if (!contentType) {
return next();
}
const boundary: RegExpExecArray | null = RE_BOUNDARY.exec(contentType);
const boundary = RE_BOUNDARY.exec(contentType);
if (!boundary) {
return next();
}
const dicer: Dicer = new Dicer({ boundary: boundary[1] || boundary[2] });
const files: { [key: string]: Buffer } = {};
const dicer = new Dicer({ boundary: boundary[1] || boundary[2] });
const files: Record<string, Buffer> = {};
dicer.on('part', (part: Dicer.PartStream) => {
let fileBuffer: Buffer = Buffer.alloc(0);
let fileName: string = '';
let fileBuffer = Buffer.alloc(0);
let fileName = '';
part.on('header', header => {
const contentDisposition = header['content-disposition' as keyof object];
@@ -67,26 +67,26 @@ router.post('/upload', multipartParser, async (request: express.Request, respons
return;
}
const bucket: string = request.files.bucket.toString();
const key: string = request.files.key.toString();
const file: Buffer = request.files.file;
const acl: string = request.files.acl.toString();
const pid: string = request.files.pid.toString();
const date: string = request.files.date.toString();
const signature: string = request.files.signature.toString();
const bucket = request.files.bucket.toString();
const key = request.files.key.toString();
const file = request.files.file;
const acl = request.files.acl.toString();
const pid = request.files.pid.toString();
const date = request.files.date.toString();
const signature = request.files.signature.toString();
// Signatures only good for 1 minute
const minute: number = 1000 * 60;
const minuteAgo: number = Date.now() - minute;
// * Signatures only good for 1 minute
const minute = 1000 * 60;
const minuteAgo = Date.now() - minute;
if (Number(date) < Math.floor(minuteAgo / 1000)) {
response.sendStatus(400);
return;
}
const data: string = `${pid}${bucket}${key}${date}`;
const data = `${pid}${bucket}${key}${date}`;
const hmac: string = crypto.createHmac('sha256', signatureSecret).update(data).digest('hex');
const hmac = crypto.createHmac('sha256', signatureSecret).update(data).digest('hex');
console.log(hmac, signature);

View File

@@ -5,7 +5,7 @@ export async function* apiKeyMiddleware<Request, Response>(
call: ServerMiddlewareCall<Request, Response>,
context: CallContext,
): AsyncGenerator<Response, Response | void, undefined> {
const apiKey: string | undefined = context.metadata.get('X-API-Key');
const apiKey = context.metadata.get('X-API-Key');
if (!apiKey || apiKey !== config.grpc.master_api_keys.account) {
throw new ServerError(Status.UNAUTHENTICATED, 'Missing or invalid API key');

View File

@@ -2,7 +2,6 @@ import { Status, ServerError } from 'nice-grpc';
import { ExchangeTokenForUserDataRequest } from '@pretendonetwork/grpc/account/exchange_token_for_user_data';
import { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc';
import { getPNIDByTokenAuth } from '@/database';
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
import { PNID_PERMISSION_FLAGS } from '@/types/common/permission-flags';
import { config } from '@/config-manager';
@@ -11,7 +10,7 @@ export async function exchangeTokenForUserData(request: ExchangeTokenForUserData
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token');
}
const pnid: HydratedPNIDDocument | null = await getPNIDByTokenAuth(request.token);
const pnid = await getPNIDByTokenAuth(request.token);
if (!pnid) {
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token');

View File

@@ -1,10 +1,9 @@
import { Status, ServerError } from 'nice-grpc';
import {GetNEXDataRequest,GetNEXDataResponse, DeepPartial } from '@pretendonetwork/grpc/account/get_nex_data_rpc';
import { NEXAccount } from '@/models/nex-account';
import { HydratedNEXAccountDocument } from '@/types/mongoose/nex-account';
export async function getNEXData(request: GetNEXDataRequest): Promise<DeepPartial<GetNEXDataResponse>> {
const nexAccount: HydratedNEXAccountDocument | null = await NEXAccount.findOne({ pid: request.pid });
const nexAccount = await NEXAccount.findOne({ pid: request.pid });
if (!nexAccount) {
throw new ServerError(

View File

@@ -1,10 +1,9 @@
import { Status, ServerError } from 'nice-grpc';
import {GetNEXPasswordRequest,GetNEXPasswordResponse, DeepPartial } from '@pretendonetwork/grpc/account/get_nex_password_rpc';
import { NEXAccount } from '@/models/nex-account';
import { HydratedNEXAccountDocument } from '@/types/mongoose/nex-account';
export async function getNEXPassword(request: GetNEXPasswordRequest): Promise<DeepPartial<GetNEXPasswordResponse>> {
const nexAccount: HydratedNEXAccountDocument | null = await NEXAccount.findOne({ pid: request.pid });
const nexAccount = await NEXAccount.findOne({ pid: request.pid });
if (!nexAccount) {
throw new ServerError(

View File

@@ -1,12 +1,11 @@
import { Status, ServerError } from 'nice-grpc';
import { GetUserDataRequest, GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc';
import { getPNIDByPID } from '@/database';
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
import { PNID_PERMISSION_FLAGS } from '@/types/common/permission-flags';
import { config } from '@/config-manager';
export async function getUserData(request: GetUserDataRequest): Promise<GetUserDataResponse> {
const pnid: HydratedPNIDDocument | null = await getPNIDByPID(request.pid);
const pnid = await getPNIDByPID(request.pid);
if (!pnid) {
throw new ServerError(

View File

@@ -1,11 +1,10 @@
import { AccountServiceImplementation } from '@pretendonetwork/grpc/account/account_service';
import { getUserData } from '@/services/grpc/account/get-user-data';
import { getNEXPassword } from '@/services/grpc/account/get-nex-password';
import { getNEXData } from '@/services/grpc/account/get-nex-data';
import { updatePNIDPermissions } from '@/services/grpc/account/update-pnid-permissions';
import { exchangeTokenForUserData } from '@/services/grpc/account/exchange-token-for-user-data';
export const accountServiceImplementation: AccountServiceImplementation = {
export const accountServiceImplementation = {
getUserData,
getNEXPassword,
getNEXData,

View File

@@ -3,10 +3,9 @@ import { UpdatePNIDPermissionsRequest } from '@pretendonetwork/grpc/account/upda
import { getPNIDByPID } from '@/database';
import { PNID_PERMISSION_FLAGS } from '@/types/common/permission-flags';
import type { Empty } from '@pretendonetwork/grpc/api/google/protobuf/empty';
import type { HydratedPNIDDocument } from '@/types/mongoose/pnid';
export async function updatePNIDPermissions(request: UpdatePNIDPermissionsRequest): Promise<Empty> {
const pnid: HydratedPNIDDocument | null = await getPNIDByPID(request.pid);
const pnid = await getPNIDByPID(request.pid);
if (!pnid) {
throw new ServerError(

View File

@@ -5,7 +5,7 @@ export async function* apiKeyMiddleware<Request, Response>(
call: ServerMiddlewareCall<Request, Response>,
context: CallContext,
): AsyncGenerator<Response, Response | void, undefined> {
const apiKey: string | undefined = context.metadata.get('X-API-Key');
const apiKey = context.metadata.get('X-API-Key');
if (!apiKey || apiKey !== config.grpc.master_api_keys.api) {
throw new ServerError(Status.UNAUTHENTICATED, 'Missing or invalid API key');

View File

@@ -3,7 +3,7 @@ import { getPNIDByTokenAuth } from '@/database';
import type { HydratedPNIDDocument } from '@/types/mongoose/pnid';
// * These paths require that a token be present
const TOKEN_REQUIRED_PATHS: string[] = [
const TOKEN_REQUIRED_PATHS = [
'/api.API/GetUserData',
'/api.API/UpdateUserData',
'/api.API/ResetPassword', // * This paths token is not an authentication token, it is a password reset token
@@ -20,14 +20,14 @@ export async function* authenticationMiddleware<Request, Response>(
call: ServerMiddlewareCall<Request, Response, AuthenticationCallContextExt>,
context: CallContext,
): AsyncGenerator<Response, Response | void, undefined> {
const token: string | undefined = context.metadata.get('X-Token')?.trim();
const token = context.metadata.get('X-Token')?.trim();
if (!token && TOKEN_REQUIRED_PATHS.includes(call.method.path)) {
throw new ServerError(Status.UNAUTHENTICATED, 'Missing or invalid authentication token');
}
try {
let pnid: HydratedPNIDDocument | null = null;
let pnid = null;
if (token) {
pnid = await getPNIDByTokenAuth(token);
@@ -42,7 +42,7 @@ export async function* authenticationMiddleware<Request, Response>(
pnid
});
} catch (error) {
let message: string = 'Unknown server error';
let message = 'Unknown server error';
console.log(error);

View File

@@ -7,7 +7,7 @@ import type { Empty } from '@pretendonetwork/grpc/api/google/protobuf/empty';
import type { HydratedPNIDDocument } from '@/types/mongoose/pnid';
export async function forgotPassword(request: ForgotPasswordRequest): Promise<Empty> {
const input: string = request.emailAddressOrUsername.trim();
const input = request.emailAddressOrUsername.trim();
if (!input) {
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid or missing input');

View File

@@ -3,11 +3,10 @@ import { GetUserDataResponse, DeepPartial } from '@pretendonetwork/grpc/api/get_
import { config } from '@/config-manager';
import type { Empty } from '@pretendonetwork/grpc/api/google/protobuf/empty';
import type { AuthenticationCallContextExt } from '@/services/grpc/api/authentication-middleware';
import type { HydratedPNIDDocument } from '@/types/mongoose/pnid';
export async function getUserData(_request: Empty, context: CallContext & AuthenticationCallContextExt): Promise<DeepPartial<GetUserDataResponse>> {
// * This is asserted in authentication-middleware, we know this is never null
const pnid: HydratedPNIDDocument = context.pnid!;
const pnid = context.pnid!;
return {
deleted: pnid.deleted,

View File

@@ -1,4 +1,3 @@
import { APIServiceImplementation } from '@pretendonetwork/grpc/api/api_service';
import { register } from '@/services/grpc/api/register';
import { login } from '@/services/grpc/api/login';
import { getUserData } from '@/services/grpc/api/get-user-data';
@@ -8,7 +7,7 @@ import { resetPassword } from '@/services/grpc/api/reset-password';
import { setDiscordConnectionData } from '@/services/grpc/api/set-discord-connection-data';
import { setStripeConnectionData } from '@/services/grpc/api/set-stripe-connection-data';
export const apiServiceImplementation: APIServiceImplementation = {
export const apiServiceImplementation = {
register,
login,
getUserData,

View File

@@ -4,14 +4,13 @@ import bcrypt from 'bcrypt';
import { getPNIDByUsername, getPNIDByTokenAuth } from '@/database';
import { nintendoPasswordHash, generateToken} from '@/util';
import { config } from '@/config-manager';
import type { TokenOptions } from '@/types/common/token-options';
import type { HydratedPNIDDocument } from '@/types/mongoose/pnid';
export async function login(request: LoginRequest): Promise<DeepPartial<LoginResponse>> {
const grantType: string = request.grantType?.trim();
const username: string | undefined = request.username?.trim();
const password: string | undefined = request.password?.trim();
const refreshToken: string | undefined = request.refreshToken?.trim();
const grantType = request.grantType?.trim();
const username = request.username?.trim();
const password = request.password?.trim();
const refreshToken = request.refreshToken?.trim();
if (!['password', 'refresh_token'].includes(grantType)) {
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid grant type');
@@ -38,7 +37,7 @@ export async function login(request: LoginRequest): Promise<DeepPartial<LoginRes
throw new ServerError(Status.INVALID_ARGUMENT, 'User not found');
}
const hashedPassword: string = nintendoPasswordHash(password!, pnid.pid); // * We know password will never be null here
const hashedPassword = nintendoPasswordHash(password!, pnid.pid); // * We know password will never be null here
if (!bcrypt.compareSync(hashedPassword, pnid.password)) {
throw new ServerError(Status.INVALID_ARGUMENT, 'Password is incorrect');
@@ -55,7 +54,7 @@ export async function login(request: LoginRequest): Promise<DeepPartial<LoginRes
throw new ServerError(Status.UNAUTHENTICATED, 'Account has been deleted');
}
const accessTokenOptions: TokenOptions = {
const accessTokenOptions = {
system_type: 0x3, // * API
token_type: 0x1, // * OAuth Access
pid: pnid.pid,
@@ -64,7 +63,7 @@ export async function login(request: LoginRequest): Promise<DeepPartial<LoginRes
expire_time: BigInt(Date.now() + (3600 * 1000))
};
const refreshTokenOptions: TokenOptions = {
const refreshTokenOptions = {
system_type: 0x3, // * API
token_type: 0x2, // * OAuth Refresh
pid: pnid.pid,
@@ -73,11 +72,11 @@ export async function login(request: LoginRequest): Promise<DeepPartial<LoginRes
expire_time: BigInt(Date.now() + (3600 * 1000))
};
const accessTokenBuffer: Buffer | null = await generateToken(config.aes_key, accessTokenOptions);
const refreshTokenBuffer: Buffer | null = await generateToken(config.aes_key, refreshTokenOptions);
const accessTokenBuffer = await generateToken(config.aes_key, accessTokenOptions);
const refreshTokenBuffer = await generateToken(config.aes_key, refreshTokenOptions);
const accessToken: string = accessTokenBuffer ? accessTokenBuffer.toString('hex') : '';
const newRefreshToken: string = refreshTokenBuffer ? refreshTokenBuffer.toString('hex') : '';
const accessToken = accessTokenBuffer ? accessTokenBuffer.toString('hex') : '';
const newRefreshToken = refreshTokenBuffer ? refreshTokenBuffer.toString('hex') : '';
// TODO - Handle null tokens

View File

@@ -7,37 +7,35 @@ import bcrypt from 'bcrypt';
import moment from 'moment';
import hcaptcha from 'hcaptcha';
import Mii from 'mii-js';
import mongoose from 'mongoose';
import { doesPNIDExist, connection as databaseConnection } from '@/database';
import { nintendoPasswordHash, sendConfirmationEmail, generateToken } from '@/util';
import { LOG_ERROR } from '@/logger';
import { PNID } from '@/models/pnid';
import { NEXAccount } from '@/models/nex-account';
import { config, disabledFeatures } from '@/config-manager';
import type { TokenOptions } from '@/types/common/token-options';
import type { HydratedNEXAccountDocument } from '@/types/mongoose/nex-account';
import type { HydratedPNIDDocument } from '@/types/mongoose/pnid';
const PNID_VALID_CHARACTERS_REGEX: RegExp = /^[\w\-.]*$/;
const PNID_PUNCTUATION_START_REGEX: RegExp = /^[_\-.]/;
const PNID_PUNCTUATION_END_REGEX: RegExp = /[_\-.]$/;
const PNID_PUNCTUATION_DUPLICATE_REGEX: RegExp = /[_\-.]{2,}/;
const PNID_VALID_CHARACTERS_REGEX = /^[\w\-.]*$/;
const PNID_PUNCTUATION_START_REGEX = /^[_\-.]/;
const PNID_PUNCTUATION_END_REGEX = /[_\-.]$/;
const PNID_PUNCTUATION_DUPLICATE_REGEX = /[_\-.]{2,}/;
// This sucks
const PASSWORD_WORD_OR_NUMBER_REGEX: RegExp = /(?=.*[a-zA-Z])(?=.*\d).*/;
const PASSWORD_WORD_OR_PUNCTUATION_REGEX: RegExp = /(?=.*[a-zA-Z])(?=.*[_\-.]).*/;
const PASSWORD_NUMBER_OR_PUNCTUATION_REGEX: RegExp = /(?=.*\d)(?=.*[_\-.]).*/;
const PASSWORD_REPEATED_CHARACTER_REGEX: RegExp = /(.)\1\1/;
// * This sucks
const PASSWORD_WORD_OR_NUMBER_REGEX = /(?=.*[a-zA-Z])(?=.*\d).*/;
const PASSWORD_WORD_OR_PUNCTUATION_REGEX = /(?=.*[a-zA-Z])(?=.*[_\-.]).*/;
const PASSWORD_NUMBER_OR_PUNCTUATION_REGEX = /(?=.*\d)(?=.*[_\-.]).*/;
const PASSWORD_REPEATED_CHARACTER_REGEX = /(.)\1\1/;
const DEFAULT_MII_DATA: Buffer = Buffer.from('AwAAQOlVognnx0GC2/uogAOzuI0n2QAAAEBEAGUAZgBhAHUAbAB0AAAAAAAAAEBAAAAhAQJoRBgmNEYUgRIXaA0AACkAUkhQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGm9', 'base64');
const DEFAULT_MII_DATA = Buffer.from('AwAAQOlVognnx0GC2/uogAOzuI0n2QAAAEBEAGUAZgBhAHUAbAB0AAAAAAAAAEBAAAAhAQJoRBgmNEYUgRIXaA0AACkAUkhQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGm9', 'base64');
export async function register(request: RegisterRequest): Promise<DeepPartial<LoginResponse>> {
const email: string = request.email?.trim();
const username: string = request.username?.trim();
const miiName: string = request.miiName?.trim();
const password: string = request.password?.trim();
const passwordConfirm: string = request.passwordConfirm?.trim();
const captchaResponse: string | undefined = request.captchaResponse?.trim();
const email = request.email?.trim();
const username = request.username?.trim();
const miiName = request.miiName?.trim();
const password = request.password?.trim();
const passwordConfirm = request.passwordConfirm?.trim();
const captchaResponse = request.captchaResponse?.trim();
// * Only validate the captcha if that's enabled
if (!disabledFeatures.captcha) {
@@ -45,7 +43,7 @@ export async function register(request: RegisterRequest): Promise<DeepPartial<Lo
throw new ServerError(Status.INVALID_ARGUMENT, 'Must fill in captcha');
}
const captchaVerify: VerifyResponse = await hcaptcha.verify(config.hcaptcha.secret, captchaResponse);
const captchaVerify = await hcaptcha.verify(config.hcaptcha.secret, captchaResponse);
if (!captchaVerify.success) {
throw new ServerError(Status.INVALID_ARGUMENT, 'Captcha verification failed');
@@ -88,7 +86,7 @@ export async function register(request: RegisterRequest): Promise<DeepPartial<Lo
throw new ServerError(Status.INVALID_ARGUMENT, 'Two or more punctuation characters cannot be used in a row');
}
const userExists: boolean = await doesPNIDExist(username);
const userExists = await doesPNIDExist(username);
if (userExists) {
throw new ServerError(Status.INVALID_ARGUMENT, 'PNID already in use');
@@ -98,7 +96,7 @@ export async function register(request: RegisterRequest): Promise<DeepPartial<Lo
throw new ServerError(Status.INVALID_ARGUMENT, 'Must enter a Mii name');
}
const miiNameBuffer: Buffer = Buffer.from(miiName, 'utf16le'); // * UTF8 to UTF16
const miiNameBuffer = Buffer.from(miiName, 'utf16le'); // * UTF8 to UTF16
if (miiNameBuffer.length > 0x14) {
throw new ServerError(Status.INVALID_ARGUMENT, 'Mii name too long');
@@ -128,14 +126,14 @@ export async function register(request: RegisterRequest): Promise<DeepPartial<Lo
throw new ServerError(Status.INVALID_ARGUMENT, 'Passwords do not match');
}
const mii: Mii = new Mii(DEFAULT_MII_DATA);
const mii = new Mii(DEFAULT_MII_DATA);
mii.miiName = miiName;
const creationDate: string = moment().format('YYYY-MM-DDTHH:MM:SS');
const creationDate = moment().format('YYYY-MM-DDTHH:MM:SS');
let pnid: HydratedPNIDDocument;
let nexAccount: HydratedNEXAccountDocument;
const session: mongoose.ClientSession = await databaseConnection().startSession();
const session = await databaseConnection().startSession();
await session.startTransaction();
try {
@@ -148,17 +146,17 @@ export async function register(request: RegisterRequest): Promise<DeepPartial<Lo
await nexAccount.generatePID();
await nexAccount.generatePassword();
// Quick hack to get the PIDs to match
// TODO: Change this maybe?
// NN with a NNID will always use the NNID PID
// even if the provided NEX PID is different
// To fix this we make them the same PID
// * Quick hack to get the PIDs to match
// TODO - Change this maybe?
// * NN with a NNID will always use the NNID PID
// * even if the provided NEX PID is different
// * To fix this we make them the same PID
nexAccount.owning_pid = nexAccount.pid;
await nexAccount.save({ session });
const primaryPasswordHash: string = nintendoPasswordHash(password, nexAccount.pid);
const passwordHash: string = await bcrypt.hash(primaryPasswordHash, 10);
const primaryPasswordHash = nintendoPasswordHash(password, nexAccount.pid);
const passwordHash = await bcrypt.hash(primaryPasswordHash, 10);
pnid = new PNID({
pid: nexAccount.pid,
@@ -167,40 +165,40 @@ export async function register(request: RegisterRequest): Promise<DeepPartial<Lo
username: username,
usernameLower: username.toLowerCase(),
password: passwordHash,
birthdate: '1990-01-01', // TODO: Change this
gender: 'M', // TODO: Change this
country: 'US', // TODO: Change this
language: 'en', // TODO: Change this
birthdate: '1990-01-01', // TODO - Change this
gender: 'M', // TODO - Change this
country: 'US', // TODO - Change this
language: 'en', // TODO - Change this
email: {
address: email.toLowerCase(),
primary: true, // TODO: Change this
parent: true, // TODO: Change this
reachable: false, // TODO: Change this
validated: false, // TODO: Change this
primary: true, // TODO - Change this
parent: true, // TODO - Change this
reachable: false, // TODO - Change this
validated: false, // TODO - Change this
id: crypto.randomBytes(4).readUInt32LE()
},
region: 0x310B0000, // TODO: Change this
region: 0x310B0000, // TODO - Change this
timezone: {
name: 'America/New_York', // TODO: Change this
offset: -14400 // TODO: Change this
name: 'America/New_York', // TODO - Change this
offset: -14400 // TODO - Change this
},
mii: {
name: miiName,
primary: true, // TODO: Change this
primary: true, // TODO - Change this
data: mii.encode().toString('base64'),
id: crypto.randomBytes(4).readUInt32LE(),
hash: crypto.randomBytes(7).toString('hex'),
image_url: '', // deprecated, will be removed in the future
image_url: '', // * deprecated, will be removed in the future
image_id: crypto.randomBytes(4).readUInt32LE()
},
flags: {
active: true, // TODO: Change this
marketing: true, // TODO: Change this
off_device: true // TODO: Change this
active: true, // TODO - Change this
marketing: true, // TODO - Change this
off_device: true // TODO - Change this
},
identification: {
email_code: 1, // will be overwritten before saving
email_token: '' // will be overwritten before saving
email_code: 1, // * will be overwritten before saving
email_token: '' // * will be overwritten before saving
}
});
@@ -212,7 +210,7 @@ export async function register(request: RegisterRequest): Promise<DeepPartial<Lo
await session.commitTransaction();
} catch (error) {
let message: string = 'Unknown Mongo error';
let message = 'Unknown Mongo error';
if (error instanceof Error) {
message = error.message;
@@ -231,7 +229,7 @@ export async function register(request: RegisterRequest): Promise<DeepPartial<Lo
await sendConfirmationEmail(pnid);
const accessTokenOptions: TokenOptions = {
const accessTokenOptions = {
system_type: 0x3, // * API
token_type: 0x1, // * OAuth Access
pid: pnid.pid,
@@ -240,7 +238,7 @@ export async function register(request: RegisterRequest): Promise<DeepPartial<Lo
expire_time: BigInt(Date.now() + (3600 * 1000))
};
const refreshTokenOptions: TokenOptions = {
const refreshTokenOptions = {
system_type: 0x3, // * API
token_type: 0x2, // * OAuth Refresh
pid: pnid.pid,
@@ -249,11 +247,11 @@ export async function register(request: RegisterRequest): Promise<DeepPartial<Lo
expire_time: BigInt(Date.now() + (3600 * 1000))
};
const accessTokenBuffer: Buffer | null = await generateToken(config.aes_key, accessTokenOptions);
const refreshTokenBuffer: Buffer | null = await generateToken(config.aes_key, refreshTokenOptions);
const accessTokenBuffer = await generateToken(config.aes_key, accessTokenOptions);
const refreshTokenBuffer = await generateToken(config.aes_key, refreshTokenOptions);
const accessToken: string = accessTokenBuffer ? accessTokenBuffer.toString('hex') : '';
const refreshToken: string = refreshTokenBuffer ? refreshTokenBuffer.toString('hex') : '';
const accessToken = accessTokenBuffer ? accessTokenBuffer.toString('hex') : '';
const refreshToken = refreshTokenBuffer ? refreshTokenBuffer.toString('hex') : '';
// TODO - Handle null tokens

View File

@@ -5,18 +5,17 @@ import { decryptToken, unpackToken, nintendoPasswordHash } from '@/util';
import { getPNIDByPID } from '@/database';
import type { Empty } from '@pretendonetwork/grpc/api/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: RegExp = /(?=.*[a-zA-Z])(?=.*\d).*/;
const PASSWORD_WORD_OR_PUNCTUATION_REGEX: RegExp = /(?=.*[a-zA-Z])(?=.*[_\-.]).*/;
const PASSWORD_NUMBER_OR_PUNCTUATION_REGEX: RegExp = /(?=.*\d)(?=.*[_\-.]).*/;
const PASSWORD_REPEATED_CHARACTER_REGEX: RegExp = /(.)\1\1/;
// * This sucks
const PASSWORD_WORD_OR_NUMBER_REGEX = /(?=.*[a-zA-Z])(?=.*\d).*/;
const PASSWORD_WORD_OR_PUNCTUATION_REGEX = /(?=.*[a-zA-Z])(?=.*[_\-.]).*/;
const PASSWORD_NUMBER_OR_PUNCTUATION_REGEX = /(?=.*\d)(?=.*[_\-.]).*/;
const PASSWORD_REPEATED_CHARACTER_REGEX = /(.)\1\1/;
export async function resetPassword(request: ResetPasswordRequest): Promise<Empty> {
const password: string = request.password.trim();
const passwordConfirm: string = request.passwordConfirm.trim();
const token: string = request.token.trim();
const password = request.password.trim();
const passwordConfirm = request.passwordConfirm.trim();
const token = request.token.trim();
if (!token) {
throw new ServerError(Status.INVALID_ARGUMENT, 'Missing token');
@@ -24,7 +23,7 @@ export async function resetPassword(request: ResetPasswordRequest): Promise<Empt
let unpackedToken: Token;
try {
const decryptedToken: Buffer = await decryptToken(Buffer.from(token, 'base64'));
const decryptedToken = await decryptToken(Buffer.from(token, 'base64'));
unpackedToken = unpackToken(decryptedToken);
} catch (error) {
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token');
@@ -34,7 +33,7 @@ export async function resetPassword(request: ResetPasswordRequest): Promise<Empt
throw new ServerError(Status.INVALID_ARGUMENT, 'Token expired');
}
const pnid: HydratedPNIDDocument | null = await getPNIDByPID(unpackedToken.pid);
const pnid = await getPNIDByPID(unpackedToken.pid);
if (!pnid) {
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid token. No user found');
@@ -68,8 +67,8 @@ export async function resetPassword(request: ResetPasswordRequest): Promise<Empt
throw new ServerError(Status.INVALID_ARGUMENT, 'Passwords do not match');
}
const primaryPasswordHash: string = nintendoPasswordHash(password, pnid.pid);
const passwordHash: string = await bcrypt.hash(primaryPasswordHash, 10);
const primaryPasswordHash = nintendoPasswordHash(password, pnid.pid);
const passwordHash = await bcrypt.hash(primaryPasswordHash, 10);
pnid.password = passwordHash;

View File

@@ -2,18 +2,17 @@ import { Status, ServerError, CallContext } from 'nice-grpc';
import { SetDiscordConnectionDataRequest } from '@pretendonetwork/grpc/api/set_discord_connection_data_rpc';
import type { Empty } from '@pretendonetwork/grpc/api/google/protobuf/empty';
import type { AuthenticationCallContextExt } from '@/services/grpc/api/authentication-middleware';
import type { HydratedPNIDDocument } from '@/types/mongoose/pnid';
export async function setDiscordConnectionData(request: SetDiscordConnectionDataRequest, context: CallContext & AuthenticationCallContextExt): Promise<Empty>{
// * This is asserted in authentication-middleware, we know this is never null
const pnid: HydratedPNIDDocument = context.pnid!;
const pnid = context.pnid!;
try {
pnid.connections.discord.id = request.id;
await pnid.save();
} catch (error) {
let message: string = 'Unknown Mongo error';
let message = 'Unknown Mongo error';
if (error instanceof Error) {
message = error.message;

View File

@@ -3,7 +3,6 @@ import { SetStripeConnectionDataRequest } from '@pretendonetwork/grpc/api/set_st
import { PNID } from '@/models/pnid';
import type { Empty } from '@pretendonetwork/grpc/api/google/protobuf/empty';
import type { AuthenticationCallContextExt } from '@/services/grpc/api/authentication-middleware';
import type { HydratedPNIDDocument } from '@/types/mongoose/pnid';
type StripeMongoUpdateScheme = {
access_level?: number;
@@ -18,7 +17,7 @@ type StripeMongoUpdateScheme = {
export async function setStripeConnectionData(request: SetStripeConnectionDataRequest, context: CallContext & AuthenticationCallContextExt): Promise<Empty>{
// * This is asserted in authentication-middleware, we know this is never null
const pnid: HydratedPNIDDocument = context.pnid!;
const pnid = context.pnid!;
const updateData: StripeMongoUpdateScheme = {
'connections.stripe.latest_webhook_timestamp': Number(request.timestamp)
@@ -78,7 +77,7 @@ export async function setStripeConnectionData(request: SetStripeConnectionDataRe
}, { upsert: true }).exec();
}
} catch (error) {
let message: string = 'Unknown Mongo error';
let message = 'Unknown Mongo error';
if (error instanceof Error) {
message = error.message;

View File

@@ -2,12 +2,11 @@ import { CallContext } from 'nice-grpc';
import { UpdateUserDataRequest, DeepPartial } from '@pretendonetwork/grpc/api/update_user_data_rpc';
import { GetUserDataResponse } from '@pretendonetwork/grpc/api/get_user_data_rpc';
import { config } from '@/config-manager';
import type { HydratedPNIDDocument } from '@/types/mongoose/pnid';
import type { AuthenticationCallContextExt } from '@/services/grpc/api/authentication-middleware';
export async function updateUserData(_request: UpdateUserDataRequest, context: CallContext & AuthenticationCallContextExt): Promise<DeepPartial<GetUserDataResponse>> {
// * This is asserted in authentication-middleware, we know this is never null
const pnid: HydratedPNIDDocument = context.pnid!;
const pnid = context.pnid!;
// TODO - STUBBED, DO SOMETHING HERE

View File

@@ -1,4 +1,4 @@
import { createServer, Server } from 'nice-grpc';
import { createServer } from 'nice-grpc';
import { AccountDefinition } from '@pretendonetwork/grpc/account/account_service';
import { APIDefinition } from '@pretendonetwork/grpc/api/api_service';
@@ -12,7 +12,7 @@ import { apiServiceImplementation } from '@/services/grpc/api/implementation';
import { config } from '@/config-manager';
export async function startGRPCServer(): Promise<void> {
const server: Server = createServer();
const server = createServer();
server.with(accountApiKeyMiddleware).add(AccountDefinition, accountServiceImplementation);
server.with(apiApiKeyMiddleware).with(apiAuthenticationMiddleware).add(APIDefinition, apiServiceImplementation);

View File

@@ -5,13 +5,13 @@ import { LOG_INFO } from '@/logger';
import get from '@/services/local-cdn/routes/get';
const router: express.Router = express.Router();
const router = express.Router();
if (disabledFeatures.s3) {
// * s3 disabled, setup local CDN
// * Router to handle the subdomain
const localcdn: express.Router = express.Router();
const localcdn = express.Router();
// * Setup routes
LOG_INFO('[LOCAL-CDN] Applying imported routes');

View File

@@ -1,12 +1,12 @@
import express from 'express';
import { getLocalCDNFile } from '@/cache';
const router: express.Router = express.Router();
const router = express.Router();
router.get('/*', async (request: express.Request, response: express.Response): Promise<void> => {
const filePath: string = request.params[0];
const filePath = request.params[0];
const file: Buffer = await getLocalCDNFile(filePath);
const file = await getLocalCDNFile(filePath);
if (file) {
response.send(file);

View File

@@ -1,4 +1,4 @@
// handles NASC endpoints
// * handles NASC endpoints
import express from 'express';
import subdomain from 'express-subdomain';
@@ -7,20 +7,20 @@ import { LOG_INFO } from '@/logger';
import ac from '@/services/nasc/routes/ac';
// Router to handle the subdomain restriction
const nasc: express.Router = express.Router();
// * Router to handle the subdomain restriction
const nasc = express.Router();
LOG_INFO('[NASC] Importing middleware');
nasc.use(NASCMiddleware);
// Setup routes
// * Setup routes
LOG_INFO('[NASC] Applying imported routes');
nasc.use('/ac', ac);
// Main router for endpoints
const router: express.Router = express.Router();
// * Main router for endpoints
const router = express.Router();
// Create subdomains
// * Create subdomains
LOG_INFO('[NASC] Creating \'nasc\' subdomain');
router.use(subdomain('nasc', nasc));

View File

@@ -1,12 +1,10 @@
import express from 'express';
import { nintendoBase64Encode, nintendoBase64Decode, nascError, generateToken } from '@/util';
import { getServerByTitleID } from '@/database';
import { TokenOptions } from '@/types/common/token-options';
import { NASCRequestParams } from '@/types/services/nasc/request-params';
import { HydratedNEXAccountDocument } from '@/types/mongoose/nex-account';
import { HydratedServerDocument } from '@/types/mongoose/server';
const router: express.Router = express.Router();
const router = express.Router();
/**
* [POST]
@@ -15,25 +13,25 @@ const router: express.Router = express.Router();
*/
router.post('/', async (request: express.Request, response: express.Response): Promise<void> => {
const requestParams: NASCRequestParams = request.body;
const action: string = nintendoBase64Decode(requestParams.action).toString();
const titleID: string = nintendoBase64Decode(requestParams.titleid).toString();
const nexAccount: HydratedNEXAccountDocument | null = request.nexAccount;
let responseData: URLSearchParams = nascError('null');
const action = nintendoBase64Decode(requestParams.action).toString();
const titleID = nintendoBase64Decode(requestParams.titleid).toString();
const nexAccount = request.nexAccount;
let responseData = nascError('null');
if (!nexAccount) {
response.status(200).send(responseData.toString());
return;
}
// TODO: REMOVE AFTER PUBLIC LAUNCH
// TODO - REMOVE AFTER PUBLIC LAUNCH
// * LET EVERYONE IN THE `test` FRIENDS SERVER
// * THAT WAY EVERYONE CAN GET AN ASSIGNED PID
let serverAccessLevel: string = 'test';
let serverAccessLevel = 'test';
if (titleID !== '0004013000003202') {
serverAccessLevel = nexAccount.server_access_level;
}
const server: HydratedServerDocument | null = await getServerByTitleID(titleID, serverAccessLevel);
const server = await getServerByTitleID(titleID, serverAccessLevel);
if (!server || !server.aes_key) {
response.status(200).send(nascError('110').toString());
@@ -66,7 +64,7 @@ router.post('/', async (request: express.Request, response: express.Response): P
});
async function processLoginRequest(server: HydratedServerDocument, pid: number, titleID: string): Promise<URLSearchParams> {
const tokenOptions: TokenOptions = {
const tokenOptions = {
system_type: 0x2, // * 3DS
token_type: 0x3, // * NEX token
pid: pid,
@@ -77,8 +75,8 @@ async function processLoginRequest(server: HydratedServerDocument, pid: number,
// TODO - Handle null tokens
const nexTokenBuffer: Buffer | null = await generateToken(server.aes_key, tokenOptions);
const nexToken: string = nintendoBase64Encode(nexTokenBuffer || '');
const nexTokenBuffer = await generateToken(server.aes_key, tokenOptions);
const nexToken = nintendoBase64Encode(nexTokenBuffer || '');
return new URLSearchParams({
locator: nintendoBase64Encode(`${server.ip}:${server.port}`),
@@ -90,7 +88,7 @@ async function processLoginRequest(server: HydratedServerDocument, pid: number,
}
async function processServiceTokenRequest(server: HydratedServerDocument, pid: number, titleID: string): Promise<URLSearchParams> {
const tokenOptions: TokenOptions = {
const tokenOptions = {
system_type: 0x2, // * 3DS
token_type: 0x4, // * Service token
pid: pid,
@@ -101,8 +99,8 @@ async function processServiceTokenRequest(server: HydratedServerDocument, pid: n
// TODO - Handle null tokens
const serviceTokenBuffer: Buffer | null = await generateToken(server.aes_key, tokenOptions);
const serviceToken: string = nintendoBase64Encode(serviceTokenBuffer || '');
const serviceTokenBuffer = await generateToken(server.aes_key, tokenOptions);
const serviceToken = nintendoBase64Encode(serviceTokenBuffer || '');
return new URLSearchParams({
retry: nintendoBase64Encode('0'),

View File

@@ -0,0 +1,48 @@
// * handles "account.nintendo.net" endpoints
import express from 'express';
import subdomain from 'express-subdomain';
import clientHeaderCheck from '@/middleware/client-header';
import cemuMiddleware from '@/middleware/cemu';
import pnidMiddleware from '@/middleware/pnid';
import { LOG_INFO } from '@/logger';
import admin from '@/services/nnas/routes/admin';
import content from '@/services/nnas/routes/content';
import devices from '@/services/nnas/routes/devices';
import miis from '@/services/nnas/routes/miis';
import oauth from '@/services/nnas/routes/oauth';
import people from '@/services/nnas/routes/people';
import provider from '@/services/nnas/routes/provider';
import support from '@/services/nnas/routes/support';
// * Router to handle the subdomain restriction
const nnas = express.Router();
LOG_INFO('[NNAS] Importing middleware');
nnas.use(clientHeaderCheck);
nnas.use(cemuMiddleware);
nnas.use(pnidMiddleware);
// * Setup routes
LOG_INFO('[NNAS] Applying imported routes');
nnas.use('/v1/api/admin', admin);
nnas.use('/v1/api/content', content);
nnas.use('/v1/api/devices', devices);
nnas.use('/v1/api/miis', miis);
nnas.use('/v1/api/oauth20', oauth);
nnas.use('/v1/api/people', people);
nnas.use('/v1/api/provider', provider);
nnas.use('/v1/api/support', support);
// * Main router for endpoints
const router = express.Router();
// * Create subdomains
LOG_INFO('[NNAS] Creating \'account\' subdomain');
router.use(subdomain('account', nnas));
LOG_INFO('[NNAS] Creating \'c.account\' subdomain');
router.use(subdomain('c.account', nnas));
export default router;

View File

@@ -2,9 +2,8 @@ import express from 'express';
import xmlbuilder from 'xmlbuilder';
import { getValueFromQueryString } from '@/util';
import { PNID } from '@/models/pnid';
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
const router: express.Router = express.Router();
const router = express.Router();
/**
* [GET]
@@ -12,9 +11,9 @@ const router: express.Router = express.Router();
* Description: Maps between NNID usernames and PIDs
*/
router.get('/mapped_ids', async (request: express.Request, response: express.Response): Promise<void> => {
const inputType: string | undefined = getValueFromQueryString(request.query, 'input_type');
const outputType: string | undefined = getValueFromQueryString(request.query, 'output_type');
const input: string | undefined = getValueFromQueryString(request.query, 'input');
const inputType = getValueFromQueryString(request.query, 'input_type');
const outputType = getValueFromQueryString(request.query, 'output_type');
const input = getValueFromQueryString(request.query, 'input');
if (!inputType || !outputType || !input) {
response.status(400).send(xmlbuilder.create({
@@ -30,7 +29,7 @@ router.get('/mapped_ids', async (request: express.Request, response: express.Res
return;
}
let inputList: string[] = input.split(',');
let inputList = input.split(',');
let queryInput: string;
let queryOutput: string;
@@ -57,7 +56,7 @@ router.get('/mapped_ids', async (request: express.Request, response: express.Res
in_id: string;
out_id: string;
}[] = [];
const allowedTypes: string[] = ['pid', 'user_id'];
const allowedTypes = ['pid', 'user_id'];
for (const input of inputList) {
const result: {
@@ -88,7 +87,7 @@ router.get('/mapped_ids', async (request: express.Request, response: express.Res
}
}
const searchResult: HydratedPNIDDocument | null = await PNID.findOne(query);
const searchResult = await PNID.findOne(query);
if (searchResult) {
result.out_id = searchResult.get(queryOutput);

View File

@@ -1,10 +1,8 @@
import express from 'express';
import xmlbuilder from 'xmlbuilder';
import timezones from '@/services/nnid/timezones.json';
import { RegionLanguages } from '@/types/services/nnid/region-languages';
import { RegionTimezones } from '@/types/services/nnid/region-timezones';
import timezones from '@/services/nnas/timezones.json';
const router: express.Router = express.Router();
const router = express.Router();
/**
* [GET]
@@ -134,8 +132,8 @@ router.get('/time_zones/:countryCode/:language', (request: express.Request, resp
response.set('X-Nintendo-Date', new Date().getTime().toString());
/*
// Old method. Crashes WiiU when sending a list with over 32 entries, but otherwise works
// countryTimezones is "countries-and-timezones" module
// * Old method. Crashes WiiU when sending a list with over 32 entries, but otherwise works
// * countryTimezones is "countries-and-timezones" module
const country = countryTimezones.getCountry(countryCode);
const timezones = country.timezones.map((timezone, index) => {
@@ -151,11 +149,11 @@ router.get('/time_zones/:countryCode/:language', (request: express.Request, resp
});
*/
const countryCode: string = request.params.countryCode;
const language: string = request.params.language;
const countryCode = request.params.countryCode;
const language = request.params.language;
const regionLanguages: RegionLanguages = timezones[countryCode as keyof typeof timezones];
const regionTimezones: RegionTimezones = regionLanguages[language] ? regionLanguages[language] : Object.values(regionLanguages)[0];
const regionLanguages = timezones[countryCode as keyof typeof timezones];
const regionTimezones = regionLanguages[language as keyof typeof regionLanguages] ? regionLanguages[language as keyof typeof regionLanguages] : Object.values(regionLanguages)[0];
response.send(xmlbuilder.create({
timezones: {

View File

@@ -1,7 +1,7 @@
import express from 'express';
import xmlbuilder from 'xmlbuilder';
const router: express.Router = express.Router();
const router = express.Router();
/**
* [GET]

View File

@@ -3,10 +3,9 @@ import xmlbuilder from 'xmlbuilder';
import { getValueFromQueryString } from '@/util';
import { PNID } from '@/models/pnid';
import { config } from '@/config-manager';
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
import { YesNoBoolString } from '@/types/common/yes-no-bool-string';
const router: express.Router = express.Router();
const router = express.Router();
/**
* [GET]
@@ -14,7 +13,7 @@ const router: express.Router = express.Router();
* Description: Returns a list of NNID miis
*/
router.get('/', async (request: express.Request, response: express.Response): Promise<void> => {
const input: string | undefined = getValueFromQueryString(request.query, 'pids');
const input = getValueFromQueryString(request.query, 'pids');
if (!input) {
response.status(400).send(xmlbuilder.create({
@@ -30,7 +29,7 @@ router.get('/', async (request: express.Request, response: express.Response): Pr
return;
}
const pids: number[] = input.split(',').map(pid => Number(pid)).filter(pid => !isNaN(pid));
const pids = input.split(',').map(pid => Number(pid)).filter(pid => !isNaN(pid));
const miis: {
data: string;
@@ -51,7 +50,7 @@ router.get('/', async (request: express.Request, response: express.Response): Pr
for (const pid of pids) {
// TODO - Replace this with a single query again somehow? Maybe aggregation?
const pnid: HydratedPNIDDocument | null = await PNID.findOne({ pid });
const pnid = await PNID.findOne({ pid });
if (pnid) {
miis.push({

View File

@@ -6,11 +6,9 @@ import consoleStatusVerificationMiddleware from '@/middleware/console-status-ver
import { getPNIDByTokenAuth, getPNIDByUsername } from '@/database';
import { generateToken } from '@/util';
import { config } from '@/config-manager';
import { TokenOptions } from '@/types/common/token-options';
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
import { Device } from '@/models/device';
const router: express.Router = express.Router();
const router = express.Router();
/**
* [POST]
@@ -18,10 +16,10 @@ const router: express.Router = express.Router();
* Description: Generates an access token for a user
*/
router.post('/access_token/generate', deviceCertificateMiddleware, consoleStatusVerificationMiddleware, async (request: express.Request, response: express.Response): Promise<void> => {
const grantType: string = request.body.grant_type;
const username: string | undefined = request.body.user_id;
const password: string | undefined = request.body.password;
const refreshToken: string | undefined = request.body.refresh_token;
const grantType = request.body.grant_type;
const username = request.body.user_id;
const password = request.body.password;
const refreshToken = request.body.refresh_token;
if (!['password', 'refresh_token'].includes(grantType)) {
response.status(400).send(xmlbuilder.create({
@@ -35,7 +33,7 @@ router.post('/access_token/generate', deviceCertificateMiddleware, consoleStatus
return;
}
let pnid: HydratedPNIDDocument | null = null;
let pnid = null;
if (grantType === 'password') {
if (!username || username.trim() === '') {
@@ -141,25 +139,25 @@ router.post('/access_token/generate', deviceCertificateMiddleware, consoleStatus
return;
}
const accessTokenOptions: TokenOptions = {
const accessTokenOptions = {
system_type: 0x1, // * WiiU
token_type: 0x1, // * OAuth Access
pid: pnid.pid,
expire_time: BigInt(Date.now() + (3600 * 1000))
};
const refreshTokenOptions: TokenOptions = {
const refreshTokenOptions = {
system_type: 0x1, // * WiiU
token_type: 0x2, // * OAuth Refresh
pid: pnid.pid,
expire_time: BigInt(Date.now() + (3600 * 1000))
};
const accessTokenBuffer: Buffer | null = await generateToken(config.aes_key, accessTokenOptions);
const refreshTokenBuffer: Buffer | null = await generateToken(config.aes_key, refreshTokenOptions);
const accessTokenBuffer = await generateToken(config.aes_key, accessTokenOptions);
const refreshTokenBuffer = await generateToken(config.aes_key, refreshTokenOptions);
const accessToken: string = accessTokenBuffer ? accessTokenBuffer.toString('hex') : '';
const newRefreshToken: string = refreshTokenBuffer ? refreshTokenBuffer.toString('hex') : '';
const accessToken = accessTokenBuffer ? accessTokenBuffer.toString('hex') : '';
const newRefreshToken = refreshTokenBuffer ? refreshTokenBuffer.toString('hex') : '';
// TODO - Handle null tokens

View File

@@ -3,7 +3,6 @@ import express from 'express';
import xmlbuilder from 'xmlbuilder';
import bcrypt from 'bcrypt';
import moment from 'moment';
import mongoose from 'mongoose';
import deviceCertificateMiddleware from '@/middleware/device-certificate';
import ratelimit from '@/middleware/ratelimit';
import { connection as databaseConnection, doesPNIDExist, getPNIDProfileJSONByPID } from '@/database';
@@ -11,17 +10,13 @@ import { getValueFromHeaders, nintendoPasswordHash, sendConfirmationEmail, sendP
import { PNID } from '@/models/pnid';
import { NEXAccount } from '@/models/nex-account';
import { LOG_ERROR } from '@/logger';
import timezones from '@/services/nnid/timezones.json';
import timezones from '@/services/nnas/timezones.json';
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
import { HydratedNEXAccountDocument } from '@/types/mongoose/nex-account';
import { RegionLanguages } from '@/types/services/nnid/region-languages';
import { RegionTimezone, RegionTimezones } from '@/types/services/nnid/region-timezones';
import { Person } from '@/types/services/nnid/person';
import { PNIDProfile } from '@/types/services/nnid/pnid-profile';
import { Person } from '@/types/services/nnas/person';
const router: express.Router = express.Router();
const router = express.Router();
/**
* [GET]
@@ -29,9 +24,9 @@ const router: express.Router = express.Router();
* Description: Checks if a username is in use
*/
router.get('/:username', async (request: express.Request, response: express.Response): Promise<void> => {
const username: string = request.params.username;
const username = request.params.username;
const userExists: boolean = await doesPNIDExist(username);
const userExists = await doesPNIDExist(username);
if (userExists) {
response.status(400).send(xmlbuilder.create({
@@ -56,7 +51,7 @@ router.get('/:username', async (request: express.Request, response: express.Resp
*/
router.post('/', ratelimit, deviceCertificateMiddleware, async (request: express.Request, response: express.Response): Promise<void> => {
if (!request.certificate || !request.certificate.valid) {
// TODO: Change this to a different error
// TODO - Change this to a different error
response.status(400).send(xmlbuilder.create({
error: {
cause: 'Bad Request',
@@ -70,7 +65,7 @@ router.post('/', ratelimit, deviceCertificateMiddleware, async (request: express
const person: Person = request.body.person;
const userExists: boolean = await doesPNIDExist(person.user_id);
const userExists = await doesPNIDExist(person.user_id);
if (userExists) {
response.status(400).send(xmlbuilder.create({
@@ -85,11 +80,11 @@ router.post('/', ratelimit, deviceCertificateMiddleware, async (request: express
return;
}
const creationDate: string = moment().format('YYYY-MM-DDTHH:MM:SS');
const creationDate = moment().format('YYYY-MM-DDTHH:MM:SS');
let pnid: HydratedPNIDDocument;
let nexAccount: HydratedNEXAccountDocument;
const session: mongoose.ClientSession = await databaseConnection().startSession();
const session = await databaseConnection().startSession();
await session.startTransaction();
try {
@@ -100,25 +95,25 @@ router.post('/', ratelimit, deviceCertificateMiddleware, async (request: express
await nexAccount.generatePID();
await nexAccount.generatePassword();
// Quick hack to get the PIDs to match
// TODO: Change this maybe?
// NN with a NNID will always use the NNID PID
// even if the provided NEX PID is different
// To fix this we make them the same PID
// * Quick hack to get the PIDs to match
// TODO - Change this maybe?
// * NN with a NNID will always use the NNID PID
// * even if the provided NEX PID is different
// * To fix this we make them the same PID
nexAccount.owning_pid = nexAccount.pid;
await nexAccount.save({ session });
const primaryPasswordHash: string = nintendoPasswordHash(person.password, nexAccount.pid);
const passwordHash: string = await bcrypt.hash(primaryPasswordHash, 10);
const primaryPasswordHash = nintendoPasswordHash(person.password, nexAccount.pid);
const passwordHash = await bcrypt.hash(primaryPasswordHash, 10);
const countryCode: string = person.country;
const language: string = person.language;
const timezoneName: string = person.tz_name;
const countryCode = person.country;
const language = person.language;
const timezoneName = person.tz_name;
const regionLanguages: RegionLanguages = timezones[countryCode as keyof typeof timezones];
const regionTimezones: RegionTimezones = regionLanguages[language] ? regionLanguages[language] : Object.values(regionLanguages)[0];
let timezone: RegionTimezone | undefined = regionTimezones.find(tz => tz.area === timezoneName);
const regionLanguages = timezones[countryCode as keyof typeof timezones];
const regionTimezones = regionLanguages[language as keyof typeof regionLanguages] ? regionLanguages[language as keyof typeof regionLanguages] : Object.values(regionLanguages)[0];
let timezone = regionTimezones.find(tz => tz.area === timezoneName);
if (!timezone) {
// TODO - Change this, handle the error
@@ -161,7 +156,7 @@ router.post('/', ratelimit, deviceCertificateMiddleware, async (request: express
data: person.mii.data,
id: crypto.randomBytes(4).readUInt32LE(),
hash: crypto.randomBytes(7).toString('hex'),
image_url: '', // deprecated, will be removed in the future
image_url: '', // * deprecated, will be removed in the future
image_id: crypto.randomBytes(4).readUInt32LE()
},
flags: {
@@ -170,8 +165,8 @@ router.post('/', ratelimit, deviceCertificateMiddleware, async (request: express
off_device: person.off_device_flag === 'Y'
},
identification: {
email_code: 1, // will be overwritten before saving
email_token: '' // will be overwritten before saving
email_code: 1, // * will be overwritten before saving
email_token: '' // * will be overwritten before saving
}
});
@@ -221,7 +216,7 @@ router.get('/@me/profile', async (request: express.Request, response: express.Re
response.set('Server', 'Nintendo 3DS (http)');
response.set('X-Nintendo-Date', new Date().getTime().toString());
const pnid: HydratedPNIDDocument | null = request.pnid;
const pnid = request.pnid;
if (!pnid) {
// TODO - Research this error more
@@ -238,7 +233,7 @@ router.get('/@me/profile', async (request: express.Request, response: express.Re
return;
}
const person: PNIDProfile | null = await getPNIDProfileJSONByPID(pnid.pid);
const person = await getPNIDProfileJSONByPID(pnid.pid);
if (!person) {
// TODO - Research this error more
@@ -270,13 +265,13 @@ router.post('/@me/devices', async (request: express.Request, response: express.R
response.set('Server', 'Nintendo 3DS (http)');
response.set('X-Nintendo-Date', new Date().getTime().toString());
// We don't care about the device attributes
// The console ignores them and PNIDs are not tied to consoles anyway
// So the server also ignores them and does not save the ones posted here
// * We don't care about the device attributes
// * The console ignores them and PNIDs are not tied to consoles anyway
// * So the server also ignores them and does not save the ones posted here
// TODO - CHANGE THIS. WE NEED TO SAVE CONSOLE DETAILS !!!
const pnid: HydratedPNIDDocument | null = request.pnid;
const pnid = request.pnid;
if (!pnid) {
// TODO - Research this error more
@@ -293,7 +288,7 @@ router.post('/@me/devices', async (request: express.Request, response: express.R
return;
}
const person: PNIDProfile | null = await getPNIDProfileJSONByPID(pnid.pid);
const person = await getPNIDProfileJSONByPID(pnid.pid);
if (!person) {
// TODO - Research this error more
@@ -325,15 +320,15 @@ router.get('/@me/devices', async (request: express.Request, response: express.Re
response.set('Server', 'Nintendo 3DS (http)');
response.set('X-Nintendo-Date', new Date().getTime().toString());
const pnid: HydratedPNIDDocument | null = request.pnid;
const deviceId: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-device-id');
const acceptLanguage: string | undefined = getValueFromHeaders(request.headers, 'accept-language');
const platformId: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-platform-id');
const region: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-region');
const serialNumber: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-serial-number');
const systemVersion: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-system-version');
const pnid = request.pnid;
const deviceID = getValueFromHeaders(request.headers, 'x-nintendo-device-id');
const acceptLanguage = getValueFromHeaders(request.headers, 'accept-language');
const platformID = getValueFromHeaders(request.headers, 'x-nintendo-platform-id');
const region = getValueFromHeaders(request.headers, 'x-nintendo-region');
const serialNumber = getValueFromHeaders(request.headers, 'x-nintendo-serial-number');
const systemVersion = getValueFromHeaders(request.headers, 'x-nintendo-system-version');
if (!deviceId || !acceptLanguage || !platformId || !region || !serialNumber || !systemVersion) {
if (!deviceID || !acceptLanguage || !platformID || !region || !serialNumber || !systemVersion) {
// TODO - Research these error more
response.status(400).send(xmlbuilder.create({
errors: {
@@ -367,11 +362,11 @@ router.get('/@me/devices', async (request: express.Request, response: express.Re
devices: [
{
device: {
device_id: deviceId,
device_id: deviceID,
language: acceptLanguage,
updated: moment().format('YYYY-MM-DDTHH:MM:SS'),
pid: pnid.pid,
platform_id: platformId,
platform_id: platformID,
region: region,
serial_number: serialNumber,
status: 'ACTIVE',
@@ -394,7 +389,7 @@ router.get('/@me/devices/owner', async (request: express.Request, response: expr
response.set('Server', 'Nintendo 3DS (http)');
response.set('X-Nintendo-Date', moment().add(5, 'h').toString());
const pnid: HydratedPNIDDocument | null = request.pnid;
const pnid = request.pnid;
if (!pnid) {
// TODO - Research this error more
@@ -411,7 +406,7 @@ router.get('/@me/devices/owner', async (request: express.Request, response: expr
return;
}
const person: PNIDProfile | null = await getPNIDProfileJSONByPID(pnid.pid);
const person = await getPNIDProfileJSONByPID(pnid.pid);
if (!person) {
// TODO - Research this error more
@@ -455,7 +450,7 @@ router.get('/@me/devices/status', async (_request: express.Request, response: ex
* Description: Updates a users Mii
*/
router.put('/@me/miis/@primary', async (request: express.Request, response: express.Response): Promise<void> => {
const pnid: HydratedPNIDDocument | null = request.pnid;
const pnid = request.pnid;
if (!pnid) {
// TODO - Research this error more
@@ -480,9 +475,9 @@ router.put('/@me/miis/@primary', async (request: express.Request, response: expr
// TODO - Better checks
const name: string = mii.name;
const primary: string = mii.primary;
const data: string = mii.data;
const name = mii.name;
const primary = mii.primary;
const data = mii.data;
await pnid.updateMii({ name, primary, data });
@@ -498,7 +493,7 @@ router.put('/@me/devices/@current/inactivate', async (request: express.Request,
response.set('Server', 'Nintendo 3DS (http)');
response.set('X-Nintendo-Date', new Date().getTime().toString());
const pnid: HydratedPNIDDocument | null = request.pnid;
const pnid = request.pnid;
if (!pnid) {
response.status(400).send(xmlbuilder.create({
@@ -523,7 +518,7 @@ router.put('/@me/devices/@current/inactivate', async (request: express.Request,
* Description: Deletes a NNID
*/
router.post('/@me/deletion', async (request: express.Request, response: express.Response): Promise<void> => {
const pnid: HydratedPNIDDocument | null = request.pnid;
const pnid = request.pnid;
if (!pnid) {
response.status(400).send(xmlbuilder.create({
@@ -539,7 +534,7 @@ router.post('/@me/deletion', async (request: express.Request, response: express.
return;
}
const email: string = pnid.email.address;
const email = pnid.email.address;
await pnid.scrub();
await pnid.save();
@@ -559,7 +554,7 @@ router.post('/@me/deletion', async (request: express.Request, response: express.
* Description: Updates a PNIDs account details
*/
router.put('/@me', async (request: express.Request, response: express.Response): Promise<void> => {
const pnid: HydratedPNIDDocument | null = request.pnid;
const pnid = request.pnid;
const person: Person = request.body.person;
if (!pnid) {
@@ -576,11 +571,11 @@ router.put('/@me', async (request: express.Request, response: express.Response):
return;
}
const gender: string = person.gender ? person.gender : pnid.gender;
const region: number = person.region ? person.region : pnid.region;
const countryCode: string = person.country ? person.country : pnid.country;
const language: string = person.language ? person.language : pnid.language;
let timezoneName: string = person.tz_name ? person.tz_name : pnid.timezone.name;
const gender = person.gender ? person.gender : pnid.gender;
const region = person.region ? person.region : pnid.region;
const countryCode = person.country ? person.country : pnid.country;
const language = person.language ? person.language : pnid.language;
let timezoneName = person.tz_name ? person.tz_name : pnid.timezone.name;
// * Fix for 3DS sending empty person.tz_name, which is interpreted as an empty object
// TODO - See if there's a cleaner way to do this?
@@ -589,12 +584,12 @@ router.put('/@me', async (request: express.Request, response: express.Response):
timezoneName = pnid.timezone.name;
}
const marketingFlag: boolean = person.marketing_flag ? person.marketing_flag === 'Y' : pnid.flags.marketing;
const offDeviceFlag: boolean = person.off_device_flag ? person.off_device_flag === 'Y' : pnid.flags.off_device;
const marketingFlag = person.marketing_flag ? person.marketing_flag === 'Y' : pnid.flags.marketing;
const offDeviceFlag = person.off_device_flag ? person.off_device_flag === 'Y' : pnid.flags.off_device;
const regionLanguages: RegionLanguages = timezones[countryCode as keyof typeof timezones];
const regionTimezones: RegionTimezones = regionLanguages[language] ? regionLanguages[language] : Object.values(regionLanguages)[0];
let timezone: RegionTimezone | undefined = regionTimezones.find(tz => tz.area === timezoneName);
const regionLanguages = timezones[countryCode as keyof typeof timezones];
const regionTimezones = regionLanguages[language as keyof typeof regionLanguages] ? regionLanguages[language as keyof typeof regionLanguages] : Object.values(regionLanguages)[0];
let timezone = regionTimezones.find(tz => tz.area === timezoneName);
if (!timezone) {
// TODO - Change this, handle the error
@@ -608,8 +603,8 @@ router.put('/@me', async (request: express.Request, response: express.Response):
}
if (person.password) {
const primaryPasswordHash: string = nintendoPasswordHash(person.password, pnid.pid);
const passwordHash: string = await bcrypt.hash(primaryPasswordHash, 10);
const primaryPasswordHash = nintendoPasswordHash(person.password, pnid.pid);
const passwordHash = await bcrypt.hash(primaryPasswordHash, 10);
pnid.password = passwordHash;
}
@@ -632,7 +627,7 @@ router.put('/@me', async (request: express.Request, response: express.Response):
* Description: Gets a list (why?) of PNID emails
*/
router.get('/@me/emails', async (request: express.Request, response: express.Response): Promise<void> => {
const pnid: HydratedPNIDDocument | null = request.pnid;
const pnid = request.pnid;
if (!pnid) {
response.status(400).send(xmlbuilder.create({
@@ -657,8 +652,8 @@ router.get('/@me/emails', async (request: express.Request, response: express.Res
parent: pnid.email.parent ? 'Y' : 'N',
primary: pnid.email.primary ? 'Y' : 'N',
reachable: pnid.email.reachable ? 'Y' : 'N',
type: 'DEFAULT', // what is this?
updated_by: 'USER', // need to actually update this
type: 'DEFAULT', // * what is this?
updated_by: 'USER', // * need to actually update this
validated: pnid.email.validated ? 'Y' : 'N',
validated_date: pnid.email.validated_date,
}
@@ -673,7 +668,7 @@ router.get('/@me/emails', async (request: express.Request, response: express.Res
* Description: Updates a users email address
*/
router.put('/@me/emails/@primary', async (request: express.Request, response: express.Response): Promise<void> => {
const pnid: HydratedPNIDDocument | null = request.pnid;
const pnid = request.pnid;
const email: {
address: string;

View File

@@ -3,12 +3,8 @@ import xmlbuilder from 'xmlbuilder';
import { getServerByClientID, getServerByGameServerID } from '@/database';
import { generateToken, getValueFromHeaders, getValueFromQueryString } from '@/util';
import { NEXAccount } from '@/models/nex-account';
import { TokenOptions } from '@/types/common/token-options';
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
import { HydratedServerDocument } from '@/types/mongoose/server';
import { HydratedNEXAccountDocument } from '@/types/mongoose/nex-account';
const router: express.Router = express.Router();
const router = express.Router();
/**
* [GET]
@@ -16,7 +12,7 @@ const router: express.Router = express.Router();
* Description: Gets a service token
*/
router.get('/service_token/@me', async (request: express.Request, response: express.Response): Promise<void> => {
const pnid: HydratedPNIDDocument | null = request.pnid;
const pnid = request.pnid;
if (!pnid) {
response.status(400).send(xmlbuilder.create({
@@ -32,7 +28,7 @@ router.get('/service_token/@me', async (request: express.Request, response: expr
return;
}
const clientID: string | undefined = getValueFromQueryString(request.query, 'client_id');
const clientID = getValueFromQueryString(request.query, 'client_id');
if (!clientID) {
// TODO - Research this error more
@@ -48,7 +44,7 @@ router.get('/service_token/@me', async (request: express.Request, response: expr
return;
}
const titleID: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-title-id');
const titleID = getValueFromHeaders(request.headers, 'x-nintendo-title-id');
if (!titleID) {
// TODO - Research this error more
@@ -64,8 +60,8 @@ router.get('/service_token/@me', async (request: express.Request, response: expr
return;
}
const serverAccessLevel: string = pnid.server_access_level;
const server: HydratedServerDocument | null = await getServerByClientID(clientID, serverAccessLevel);
const serverAccessLevel = pnid.server_access_level;
const server = await getServerByClientID(clientID, serverAccessLevel);
if (!server || !server.aes_key) {
response.send(xmlbuilder.create({
@@ -93,7 +89,7 @@ router.get('/service_token/@me', async (request: express.Request, response: expr
return;
}
const tokenOptions: TokenOptions = {
const tokenOptions = {
system_type: server.device,
token_type: 0x4, // * Service token
pid: pnid.pid,
@@ -102,8 +98,8 @@ router.get('/service_token/@me', async (request: express.Request, response: expr
expire_time: BigInt(Date.now() + (3600 * 1000))
};
const serviceTokenBuffer: Buffer | null = await generateToken(server.aes_key, tokenOptions);
let serviceToken: string = serviceTokenBuffer ? serviceTokenBuffer.toString('base64') : '';
const serviceTokenBuffer = await generateToken(server.aes_key, tokenOptions);
let serviceToken = serviceTokenBuffer ? serviceTokenBuffer.toString('base64') : '';
if (request.isCemu) {
serviceToken = Buffer.from(serviceToken, 'base64').toString('hex');
@@ -122,7 +118,7 @@ router.get('/service_token/@me', async (request: express.Request, response: expr
* Description: Gets a NEX server address and token
*/
router.get('/nex_token/@me', async (request: express.Request, response: express.Response): Promise<void> => {
const pnid: HydratedPNIDDocument | null = request.pnid;
const pnid = request.pnid;
if (!pnid) {
response.status(400).send(xmlbuilder.create({
@@ -138,7 +134,7 @@ router.get('/nex_token/@me', async (request: express.Request, response: express.
return;
}
const nexAccount: HydratedNEXAccountDocument | null = await NEXAccount.findOne({
const nexAccount = await NEXAccount.findOne({
owning_pid: pnid.pid
});
@@ -156,7 +152,7 @@ router.get('/nex_token/@me', async (request: express.Request, response: express.
return;
}
const gameServerID: string | undefined = getValueFromQueryString(request.query, 'game_server_id');
const gameServerID = getValueFromQueryString(request.query, 'game_server_id');
if (!gameServerID) {
response.send(xmlbuilder.create({
@@ -171,8 +167,8 @@ router.get('/nex_token/@me', async (request: express.Request, response: express.
return;
}
const serverAccessLevel: string = pnid.server_access_level;
const server: HydratedServerDocument | null = await getServerByGameServerID(gameServerID, serverAccessLevel);
const serverAccessLevel = pnid.server_access_level;
const server = await getServerByGameServerID(gameServerID, serverAccessLevel);
if (!server || !server.aes_key) {
response.send(xmlbuilder.create({
@@ -200,7 +196,7 @@ router.get('/nex_token/@me', async (request: express.Request, response: express.
return;
}
const titleID: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-title-id');
const titleID = getValueFromHeaders(request.headers, 'x-nintendo-title-id');
if (!titleID) {
// TODO - Research this error more
@@ -216,17 +212,17 @@ router.get('/nex_token/@me', async (request: express.Request, response: express.
return;
}
const tokenOptions: TokenOptions = {
const tokenOptions = {
system_type: server.device,
token_type: 0x3, // nex token,
token_type: 0x3, // * nex token,
pid: pnid.pid,
access_level: pnid.access_level,
title_id: BigInt(parseInt(titleID, 16)),
expire_time: BigInt(Date.now() + (3600 * 1000))
};
const nexTokenBuffer: Buffer | null = await generateToken(server.aes_key, tokenOptions);
let nexToken: string = nexTokenBuffer ? nexTokenBuffer.toString('base64') : '';
const nexTokenBuffer = await generateToken(server.aes_key, tokenOptions);
let nexToken = nexTokenBuffer ? nexTokenBuffer.toString('base64') : '';
if (request.isCemu) {
nexToken = Buffer.from(nexToken || '', 'base64').toString('hex');

View File

@@ -4,9 +4,8 @@ import xmlbuilder from 'xmlbuilder';
import moment from 'moment';
import { getPNIDByPID } from '@/database';
import { sendEmailConfirmedEmail, sendConfirmationEmail, sendForgotPasswordEmail } from '@/util';
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
const router: express.Router = express.Router();
const router = express.Router();
/**
* [POST]
@@ -14,7 +13,7 @@ const router: express.Router = express.Router();
* Description: Verifies a provided email address is valid
*/
router.post('/validate/email', async (request: express.Request, response: express.Response): Promise<void> => {
const email: string = request.body.email;
const email = request.body.email;
if (!email) {
response.send(xmlbuilder.create({
@@ -30,7 +29,7 @@ router.post('/validate/email', async (request: express.Request, response: expres
return;
}
const domain: string = email.split('@')[1];
const domain = email.split('@')[1];
dns.resolveMx(domain, (error: NodeJS.ErrnoException | null) => {
if (error) {
@@ -54,10 +53,10 @@ router.post('/validate/email', async (request: express.Request, response: expres
* Description: Verifies a users email via 6 digit code
*/
router.put('/email_confirmation/:pid/:code', async (request: express.Request, response: express.Response): Promise<void> => {
const code: string = request.params.code;
const pid: number = Number(request.params.pid);
const code = request.params.code;
const pid = Number(request.params.pid);
const pnid: HydratedPNIDDocument | null = await getPNIDByPID(pid);
const pnid = await getPNIDByPID(pid);
if (!pnid) {
response.status(400).send(xmlbuilder.create({
@@ -84,7 +83,7 @@ router.put('/email_confirmation/:pid/:code', async (request: express.Request, re
return;
}
const validatedDate: string = moment().format('YYYY-MM-DDTHH:MM:SS');
const validatedDate = moment().format('YYYY-MM-DDTHH:MM:SS');
pnid.email.reachable = true;
pnid.email.validated = true;
@@ -103,9 +102,9 @@ router.put('/email_confirmation/:pid/:code', async (request: express.Request, re
* Description: Resends a users confirmation email
*/
router.get('/resend_confirmation', async (request: express.Request, response: express.Response): Promise<void> => {
const pid: number = Number(request.headers['x-nintendo-pid']);
const pid = Number(request.headers['x-nintendo-pid']);
const pnid: HydratedPNIDDocument | null = await getPNIDByPID(pid);
const pnid = await getPNIDByPID(pid);
if (!pnid) {
// TODO - Unsure if this is the right error
@@ -133,9 +132,9 @@ router.get('/resend_confirmation', async (request: express.Request, response: ex
* NOTE: On NN this was a temp password that expired after 24 hours. We do not do that
*/
router.get('/forgotten_password/:pid', async (request: express.Request, response: express.Response): Promise<void> => {
const pid: number = Number(request.params.pid);
const pid = Number(request.params.pid);
const pnid: HydratedPNIDDocument | null = await getPNIDByPID(pid);
const pnid = await getPNIDByPID(pid);
if (!pnid) {
// TODO - Better errors

View File

@@ -1,48 +0,0 @@
// handles "account.nintendo.net" endpoints
import express from 'express';
import subdomain from 'express-subdomain';
import clientHeaderCheck from '@/middleware/client-header';
import cemuMiddleware from '@/middleware/cemu';
import pnidMiddleware from '@/middleware/pnid';
import { LOG_INFO } from '@/logger';
import admin from '@/services/nnid/routes/admin';
import content from '@/services/nnid/routes/content';
import devices from '@/services/nnid/routes/devices';
import miis from '@/services/nnid/routes/miis';
import oauth from '@/services/nnid/routes/oauth';
import people from '@/services/nnid/routes/people';
import provider from '@/services/nnid/routes/provider';
import support from '@/services/nnid/routes/support';
// Router to handle the subdomain restriction
const nnid: express.Router = express.Router();
LOG_INFO('[NNID] Importing middleware');
nnid.use(clientHeaderCheck);
nnid.use(cemuMiddleware);
nnid.use(pnidMiddleware);
// Setup routes
LOG_INFO('[NNID] Applying imported routes');
nnid.use('/v1/api/admin', admin);
nnid.use('/v1/api/content', content);
nnid.use('/v1/api/devices', devices);
nnid.use('/v1/api/miis', miis);
nnid.use('/v1/api/oauth20', oauth);
nnid.use('/v1/api/people', people);
nnid.use('/v1/api/provider', provider);
nnid.use('/v1/api/support', support);
// Main router for endpoints
const router = express.Router();
// Create subdomains
LOG_INFO('[NNID] Creating \'account\' subdomain');
router.use(subdomain('account', nnid));
LOG_INFO('[NNID] Creating \'c.account\' subdomain');
router.use(subdomain('c.account', nnid));
export default router;

View File

@@ -46,11 +46,4 @@ export interface Config {
stripe?: {
secret_key: string;
};
}
export interface DisabledFeatures {
redis: boolean;
email: boolean;
captcha: boolean;
s3: boolean
}

View File

@@ -48,7 +48,7 @@ export interface IPNID {
off_device: boolean;
};
devices: Types.DocumentArray<IDevice>;
identification: { // user identification tokens
identification: { // * user identification tokens
email_code: string;
email_token: string;
access_token: {

View File

@@ -1,7 +1,7 @@
import { YesNoBoolString } from '@/types/common/yes-no-bool-string';
export interface PNIDProfile {
//accounts: {}; // * We need to figure this out; no idea what these values mean or what they do
// *accounts: {}; // * We need to figure this out; no idea what these values mean or what they do
active_flag: YesNoBoolString;
birth_date: string;
country: string;

View File

@@ -1,5 +0,0 @@
import { RegionTimezones } from '@/types/services/nnid/region-timezones';
export interface RegionLanguages {
[myKey: string]: RegionTimezones
}

View File

@@ -1,9 +0,0 @@
export interface RegionTimezone {
area: string;
language: string;
name: string;
utc_offset: string;
order: string;
}
export type RegionTimezones = RegionTimezone[];

View File

@@ -13,7 +13,6 @@ import { config, disabledFeatures } from '@/config-manager';
import { TokenOptions } from '@/types/common/token-options';
import { Token } from '@/types/common/token';
import { IPNID, IPNIDMethods } from '@/types/mongoose/pnid';
import { MailerOptions } from '@/types/common/mailer-options';
import { SafeQs } from '@/types/common/safe-qs';
let s3: aws.S3;
@@ -27,10 +26,10 @@ if (!disabledFeatures.s3) {
}
export function nintendoPasswordHash(password: string, pid: number): string {
const pidBuffer: Buffer = Buffer.alloc(4);
const pidBuffer = Buffer.alloc(4);
pidBuffer.writeUInt32LE(pid);
const unpacked: Buffer = Buffer.concat([
const unpacked = Buffer.concat([
pidBuffer,
Buffer.from('\x02\x65\x43\x46'),
Buffer.from(password)
@@ -45,12 +44,12 @@ export function nintendoBase64Decode(encoded: string): Buffer {
}
export function nintendoBase64Encode(decoded: string | Buffer): string {
const encoded: string = Buffer.from(decoded).toString('base64');
const encoded = Buffer.from(decoded).toString('base64');
return encoded.replaceAll('+', '.').replaceAll('/', '-').replaceAll('=', '*');
}
export function generateToken(key: string, options: TokenOptions): Buffer | null {
let dataBuffer: Buffer = Buffer.alloc(1 + 1 + 4 + 8);
let dataBuffer = Buffer.alloc(1 + 1 + 4 + 8);
dataBuffer.writeUInt8(options.system_type, 0x0);
dataBuffer.writeUInt8(options.token_type, 0x1);
@@ -73,19 +72,19 @@ export function generateToken(key: string, options: TokenOptions): Buffer | null
dataBuffer.writeInt8(options.access_level, 0x16);
}
const iv: Buffer = Buffer.alloc(16);
const cipher: crypto.Cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key, 'hex'), iv);
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()
]);
let final: Buffer = encrypted;
let final = encrypted;
if ((options.token_type !== 0x1 && options.token_type !== 0x2) || options.system_type === 0x3) {
// * Access and refresh tokens don't get a checksum due to size constraints
const checksum: Buffer = crc32(dataBuffer);
const checksum = crc32(dataBuffer);
final = Buffer.concat([
checksum,
@@ -98,7 +97,7 @@ export function generateToken(key: string, options: TokenOptions): Buffer | null
export function decryptToken(token: Buffer): Buffer {
let encryptedBody: Buffer;
let expectedChecksum: number = 0;
let expectedChecksum = 0;
if (token.length === 16) {
// * Token is an access/refresh token, no checksum
@@ -108,10 +107,10 @@ export function decryptToken(token: Buffer): Buffer {
encryptedBody = token.subarray(4);
}
const iv: Buffer = Buffer.alloc(16);
const decipher: crypto.Decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(config.aes_key, 'hex'), iv);
const iv = Buffer.alloc(16);
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(config.aes_key, 'hex'), iv);
const decrypted: Buffer = Buffer.concat([
const decrypted = Buffer.concat([
decipher.update(encryptedBody),
decipher.final()
]);
@@ -140,9 +139,9 @@ export function unpackToken(token: Buffer): Token {
}
export function fullUrl(request: express.Request): string {
const protocol: string = request.protocol;
const host: string = request.host;
const opath: string = request.originalUrl;
const protocol = request.protocol;
const host = request.host;
const opath = request.originalUrl;
return `${protocol}://${host}${opath}`;
}
@@ -161,8 +160,8 @@ export async function uploadCDNAsset(bucket: string, key: string, data: Buffer,
}
export async function writeLocalCDNFile(key: string, data: Buffer): Promise<void> {
const filePath: string = config.cdn.disk_path;
const folder: string = path.dirname(filePath);
const filePath = config.cdn.disk_path;
const folder = path.dirname(filePath);
await fs.ensureDir(folder);
await fs.writeFile(filePath, data);
@@ -177,7 +176,7 @@ export function nascError(errorCode: string): URLSearchParams {
}
export async function sendConfirmationEmail(pnid: mongoose.HydratedDocument<IPNID, IPNIDMethods>): Promise<void> {
const options: MailerOptions = {
const options = {
to: pnid.email.address,
subject: '[Pretendo Network] Please confirm your email address',
username: pnid.username,
@@ -192,7 +191,7 @@ export async function sendConfirmationEmail(pnid: mongoose.HydratedDocument<IPNI
}
export async function sendEmailConfirmedEmail(pnid: mongoose.HydratedDocument<IPNID, IPNIDMethods>): Promise<void> {
const options: MailerOptions = {
const options = {
to: pnid.email.address,
subject: '[Pretendo Network] Email address confirmed',
username: pnid.username,
@@ -204,21 +203,21 @@ export async function sendEmailConfirmedEmail(pnid: mongoose.HydratedDocument<IP
}
export async function sendForgotPasswordEmail(pnid: mongoose.HydratedDocument<IPNID, IPNIDMethods>): Promise<void> {
const tokenOptions: TokenOptions = {
const tokenOptions = {
system_type: 0xF, // * API
token_type: 0x5, // * Password reset
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
expire_time: BigInt(Date.now() + (24 * 60 * 60 * 1000)) // * Only valid for 24 hours
};
const tokenBuffer: Buffer | null = await generateToken(config.aes_key, tokenOptions);
const passwordResetToken: string = tokenBuffer ? tokenBuffer.toString('hex') : '';
const tokenBuffer = await generateToken(config.aes_key, tokenOptions);
const passwordResetToken = tokenBuffer ? tokenBuffer.toString('hex') : '';
// TODO - Handle null token
const mailerOptions: MailerOptions = {
const mailerOptions = {
to: pnid.email.address,
subject: '[Pretendo Network] Forgot Password',
username: pnid.username,
@@ -234,7 +233,7 @@ export async function sendForgotPasswordEmail(pnid: mongoose.HydratedDocument<IP
}
export async function sendPNIDDeletedEmail(email: string, username: string): Promise<void> {
const options: MailerOptions = {
const options = {
to: email,
subject: '[Pretendo Network] PNID Deleted',
username: username,
@@ -265,8 +264,8 @@ export function makeSafeQs(query: ParsedQs): SafeQs {
}
export function getValueFromQueryString(qs: ParsedQs, key: string): string | undefined {
let property: string | ParsedQs | string[] | ParsedQs[] | SafeQs | undefined = qs[key];
let value: string | undefined;
let property = qs[key];
let value;
if (property) {
if (Array.isArray(property)) {
@@ -285,8 +284,8 @@ export function getValueFromQueryString(qs: ParsedQs, key: string): string | und
}
export function getValueFromHeaders(headers: IncomingHttpHeaders, key: string): string | undefined {
let header: string | string[] | undefined = headers[key];
let value: string | undefined;
let header = headers[key];
let value;
if (header) {
if (!Array.isArray(header)) {