mirror of
https://github.com/PretendoNetwork/account.git
synced 2026-09-10 20:36:23 -05:00
Merge pull request #87 from PretendoNetwork/nnas-updates
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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 ."
|
||||
},
|
||||
|
||||
12
src/cache.ts
12
src/cache.ts
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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`);
|
||||
|
||||
|
||||
@@ -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;"> </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;"> </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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
48
src/services/nnas/index.ts
Normal file
48
src/services/nnas/index.ts
Normal 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;
|
||||
@@ -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);
|
||||
@@ -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: {
|
||||
@@ -1,7 +1,7 @@
|
||||
import express from 'express';
|
||||
import xmlbuilder from 'xmlbuilder';
|
||||
|
||||
const router: express.Router = express.Router();
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* [GET]
|
||||
@@ -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({
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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');
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -46,11 +46,4 @@ export interface Config {
|
||||
stripe?: {
|
||||
secret_key: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DisabledFeatures {
|
||||
redis: boolean;
|
||||
email: boolean;
|
||||
captcha: boolean;
|
||||
s3: boolean
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
@@ -1,5 +0,0 @@
|
||||
import { RegionTimezones } from '@/types/services/nnid/region-timezones';
|
||||
|
||||
export interface RegionLanguages {
|
||||
[myKey: string]: RegionTimezones
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export interface RegionTimezone {
|
||||
area: string;
|
||||
language: string;
|
||||
name: string;
|
||||
utc_offset: string;
|
||||
order: string;
|
||||
}
|
||||
|
||||
export type RegionTimezones = RegionTimezone[];
|
||||
59
src/util.ts
59
src/util.ts
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user