Enable strict mode in TS and fix most issues with it

This commit is contained in:
Jonathan Barrow 2023-03-17 17:39:46 -04:00
parent e508dd95d4
commit dd952954fd
No known key found for this signature in database
GPG Key ID: E86E9FE9049C741F
30 changed files with 3358 additions and 141 deletions

View File

@ -1 +1,2 @@
dist
dist
*.js

3083
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -33,7 +33,7 @@
"dotenv": "^16.0.3",
"email-validator": "^2.0.4",
"express": "^4.17.1",
"express-rate-limit": "^5.3.0",
"express-rate-limit": "^6.7.0",
"express-subdomain": "^1.0.5",
"fs-extra": "^8.1.0",
"got": "^11.8.2",
@ -55,13 +55,17 @@
},
"devDependencies": {
"@hcaptcha/types": "^1.0.3",
"@types/bcrypt": "^5.0.0",
"@types/cors": "^2.8.13",
"@types/dicer": "^0.2.2",
"@types/express": "^4.17.17",
"@types/fs-extra": "^11.0.1",
"@types/mongoose-unique-validator": "^1.0.7",
"@types/morgan": "^1.9.4",
"@types/node": "^18.14.4",
"@types/node-rsa": "^1.1.1",
"@types/nodemailer": "^6.4.7",
"@types/validator": "^13.7.14",
"@typescript-eslint/eslint-plugin": "^5.54.1",
"@typescript-eslint/parser": "^5.54.1",
"eslint": "^8.35.0",

View File

@ -28,12 +28,12 @@ export async function setCachedFile(fileName: string, value: Buffer): Promise<vo
}
export async function getCachedFile(fileName: string, encoding?: BufferEncoding): Promise<Buffer> {
let cachedFile: Buffer;
let cachedFile: Buffer = Buffer.alloc(0);
if (disabledFeatures.redis) {
cachedFile = memoryCache[fileName] || null;
} else {
const redisValue: string = await client.get(fileName);
const redisValue: string | null = await client.get(fileName);
if (redisValue) {
cachedFile = Buffer.from(redisValue, encoding);
}
@ -177,7 +177,7 @@ export async function getLocalCDNFile(name: string, encoding?: BufferEncoding):
if (file === null) {
if (await fs.pathExists(`${LOCAL_CDN_BASE}/${name}`)) {
const fileBuffer: string = await fs.readFile(`${LOCAL_CDN_BASE}/${name}`, { encoding });
const fileBuffer: string | Buffer = await fs.readFile(`${LOCAL_CDN_BASE}/${name}`, { encoding });
file = Buffer.from(fileBuffer);
await setLocalCDNFile(name, file);
}

View File

@ -15,7 +15,7 @@ export const disabledFeatures: DisabledFeatures = {
LOG_INFO('Loading config');
let mongooseConnectOptions: mongoose.ConnectOptions;
let mongooseConnectOptions: mongoose.ConnectOptions = {};
if (process.env.PN_ACT_CONFIG_MONGOOSE_CONNECT_OPTIONS_PATH) {
mongooseConnectOptions = fs.readJSONSync(process.env.PN_ACT_CONFIG_MONGOOSE_CONNECT_OPTIONS_PATH);
@ -23,41 +23,41 @@ if (process.env.PN_ACT_CONFIG_MONGOOSE_CONNECT_OPTIONS_PATH) {
export const config: Config = {
http: {
port: Number(process.env.PN_ACT_CONFIG_HTTP_PORT)
port: Number(process.env.PN_ACT_CONFIG_HTTP_PORT || '')
},
mongoose: {
connection_string: process.env.PN_ACT_CONFIG_MONGO_CONNECTION_STRING,
connection_string: process.env.PN_ACT_CONFIG_MONGO_CONNECTION_STRING || '',
options: mongooseConnectOptions
},
redis: {
client: {
url: process.env.PN_ACT_CONFIG_REDIS_URL
url: process.env.PN_ACT_CONFIG_REDIS_URL || ''
}
},
email: {
host: process.env.PN_ACT_CONFIG_EMAIL_HOST,
port: Number(process.env.PN_ACT_CONFIG_EMAIL_PORT),
secure: Boolean(process.env.PN_ACT_CONFIG_EMAIL_SECURE),
host: process.env.PN_ACT_CONFIG_EMAIL_HOST || '',
port: Number(process.env.PN_ACT_CONFIG_EMAIL_PORT || ''),
secure: Boolean(process.env.PN_ACT_CONFIG_EMAIL_SECURE || ''),
auth: {
user: process.env.PN_ACT_CONFIG_EMAIL_USERNAME,
pass: process.env.PN_ACT_CONFIG_EMAIL_PASSWORD
user: process.env.PN_ACT_CONFIG_EMAIL_USERNAME || '',
pass: process.env.PN_ACT_CONFIG_EMAIL_PASSWORD || ''
},
from: process.env.PN_ACT_CONFIG_EMAIL_FROM
from: process.env.PN_ACT_CONFIG_EMAIL_FROM || ''
},
s3: {
endpoint: process.env.PN_ACT_CONFIG_S3_ENDPOINT,
key: process.env.PN_ACT_CONFIG_S3_ACCESS_KEY,
secret: process.env.PN_ACT_CONFIG_S3_ACCESS_SECRET
endpoint: process.env.PN_ACT_CONFIG_S3_ENDPOINT || '',
key: process.env.PN_ACT_CONFIG_S3_ACCESS_KEY || '',
secret: process.env.PN_ACT_CONFIG_S3_ACCESS_SECRET || ''
},
hcaptcha: {
secret: process.env.PN_ACT_CONFIG_HCAPTCHA_SECRET
secret: process.env.PN_ACT_CONFIG_HCAPTCHA_SECRET || ''
},
cdn: {
subdomain: process.env.PN_ACT_CONFIG_CDN_SUBDOMAIN,
disk_path: process.env.PN_ACT_CONFIG_CDN_DISK_PATH,
base_url: process.env.PN_ACT_CONFIG_CDN_BASE_URL
subdomain: process.env.PN_ACT_CONFIG_CDN_SUBDOMAIN || '',
disk_path: process.env.PN_ACT_CONFIG_CDN_DISK_PATH || '',
base_url: process.env.PN_ACT_CONFIG_CDN_BASE_URL || ''
},
website_base: process.env.PN_ACT_CONFIG_WEBSITE_BASE
website_base: process.env.PN_ACT_CONFIG_WEBSITE_BASE || ''
};
LOG_INFO('Config loaded, checking integrity');

View File

@ -42,7 +42,7 @@ export function verifyConnected(): void {
}
}
export async function getUserByUsername(username: string): Promise<HydratedPNIDDocument> {
export async function getUserByUsername(username: string): Promise<HydratedPNIDDocument | null> {
verifyConnected();
return await PNID.findOne<HydratedPNIDDocument>({
@ -50,7 +50,7 @@ export async function getUserByUsername(username: string): Promise<HydratedPNIDD
});
}
export async function getUserByPID(pid: number): Promise<HydratedPNIDDocument> {
export async function getUserByPID(pid: number): Promise<HydratedPNIDDocument | null> {
verifyConnected();
return await PNID.findOne<HydratedPNIDDocument>({
@ -58,7 +58,7 @@ export async function getUserByPID(pid: number): Promise<HydratedPNIDDocument> {
});
}
export async function getUserByEmailAddress(email: string): Promise<HydratedPNIDDocument> {
export async function getUserByEmailAddress(email: string): Promise<HydratedPNIDDocument | null> {
verifyConnected();
// TODO - Update documents to store email normalized
@ -73,7 +73,7 @@ export async function doesUserExist(username: string): Promise<boolean> {
return !!await getUserByUsername(username);
}
export async function getUserBasic(token: string): Promise<HydratedPNIDDocument> {
export async function getUserBasic(token: string): Promise<HydratedPNIDDocument | null> {
verifyConnected();
// * Wii U sends Basic auth as `username password`, where the password may not have spaces
@ -84,7 +84,7 @@ export async function getUserBasic(token: string): Promise<HydratedPNIDDocument>
const username: string = parts[0];
const password: string = parts[1];
const user: HydratedPNIDDocument = await getUserByUsername(username);
const user: HydratedPNIDDocument | null = await getUserByUsername(username);
if (!user) {
return null;
@ -99,14 +99,14 @@ export async function getUserBasic(token: string): Promise<HydratedPNIDDocument>
return user;
}
export async function getUserBearer(token: string): Promise<HydratedPNIDDocument> {
export async function getUserBearer(token: string): Promise<HydratedPNIDDocument | null> {
verifyConnected();
try {
const decryptedToken: Buffer = await decryptToken(Buffer.from(token, 'base64'));
const unpackedToken: Token = unpackToken(decryptedToken);
const user: HydratedPNIDDocument = await getUserByPID(unpackedToken.pid);
const user: HydratedPNIDDocument | null = await getUserByPID(unpackedToken.pid);
if (user) {
const expireTime: number = Math.floor((Number(unpackedToken.expire_time) / 1000));
@ -124,27 +124,38 @@ export async function getUserBearer(token: string): Promise<HydratedPNIDDocument
}
}
export async function getUserProfileJSONByPID(pid: number): Promise<PNIDProfile> {
export async function getUserProfileJSONByPID(pid: number): Promise<PNIDProfile | null> {
verifyConnected();
const user: HydratedPNIDDocument = await getUserByPID(pid);
const user: HydratedPNIDDocument | null = await getUserByPID(pid);
if (!user) {
return null;
}
const device: HydratedDeviceDocument = user.get('devices')[0]; // * Just grab the first device
let device_attributes: [{
let device_attributes: {
device_attribute: {
name: string;
value: string;
created_date: string;
};
}];
}[] = [];
if (device) {
device_attributes = device.get('device_attributes').map(({name, value, created_date}) => ({
device_attribute: {
name,
value,
created_date: created_date ? created_date : ''
}
}));
device_attributes = device.get('device_attributes').map((attribute: { name: string; value: string; created_date: string; }) => {
const name: string = attribute.name;
const value: string = attribute.value;
const created_date: string = attribute.created_date;
return {
device_attribute: {
name,
value,
created_date: created_date ? created_date : ''
}
};
});
}
return {
@ -196,21 +207,21 @@ export async function getUserProfileJSONByPID(pid: number): Promise<PNIDProfile>
};
}
export async function getServer(gameServerId: string, accessMode: string): Promise<HydratedServerDocument> {
export async function getServer(gameServerId: string, accessMode: string): Promise<HydratedServerDocument | null> {
return await Server.findOne({
game_server_id: gameServerId,
access_mode: accessMode
});
}
export async function getServerByTitleId(titleId: string, accessMode: string): Promise<HydratedServerDocument> {
export async function getServerByTitleId(titleId: string, accessMode: string): Promise<HydratedServerDocument | null> {
return await Server.findOne({
title_ids: titleId,
access_mode: accessMode
});
}
export async function addUserConnection(pnid: HydratedPNIDDocument, data: ConnectionData, type: string): Promise<ConnectionResponse> {
export async function addUserConnection(pnid: HydratedPNIDDocument, data: ConnectionData, type: string): Promise<ConnectionResponse | undefined> {
if (type === 'discord') {
return await addUserConnectionDiscord(pnid, data);
}
@ -239,7 +250,7 @@ export async function addUserConnectionDiscord(pnid: HydratedPNIDDocument, data:
};
}
export async function removeUserConnection(pnid: HydratedPNIDDocument, type: string): Promise<ConnectionResponse> {
export async function removeUserConnection(pnid: HydratedPNIDDocument, type: string): Promise<ConnectionResponse | undefined> {
// * Add more connections later?
if (type === 'discord') {
return await removeUserConnectionDiscord(pnid);

View File

@ -20,10 +20,10 @@ export async function sendMail(options: MailerOptions): Promise<void> {
let html: string = confirmation ? confirmationEmailTemplate : genericEmailTemplate;
html = html.replace(/{{username}}/g, username);
html = html.replace(/{{paragraph}}/g, paragraph);
html = html.replace(/{{preview}}/g, (preview || ''));
html = html.replace(/{{confirmation-href}}/g, (confirmation?.href || ''));
html = html.replace(/{{confirmation-code}}/g, (confirmation?.code || ''));
html = html.replace(/{{paragraph}}/g, paragraph || '');
html = html.replace(/{{preview}}/g, preview || '');
html = html.replace(/{{confirmation-href}}/g, confirmation?.href || '');
html = html.replace(/{{confirmation-code}}/g, confirmation?.code || '');
if (link) {
const { href, text } = link;

View File

@ -3,14 +3,14 @@ import { getUserBearer } 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 = request.headers.authorization;
const authHeader: string | undefined = request.headers.authorization;
if (!authHeader || !(authHeader.startsWith('Bearer'))) {
return next();
}
const token: string = authHeader.split(' ')[1];
const user: HydratedPNIDDocument = await getUserBearer(token);
const user: HydratedPNIDDocument | null = await getUserBearer(token);
request.pnid = user;

View File

@ -35,9 +35,9 @@ async function NASCMiddleware(request: express.Request, response: express.Respon
const macAddressHash: string = crypto.createHash('sha256').update(macAddress).digest('base64');
const fcdcertHash: string = crypto.createHash('sha256').update(fcdcert).digest('base64');
let pid: number;
let pidHmac: string;
let password: string;
let pid: number = 0; // * Real PIDs are always positive and non-zero
let pidHmac: string = '';
let password: string = '';
if (requestParams.userid) {
pid = Number(nintendoBase64Decode(requestParams.userid).toString());
@ -68,7 +68,7 @@ async function NASCMiddleware(request: express.Request, response: express.Respon
return;
}
let model: string;
let model: string = '';
switch (serialNumber[0]) {
case 'C':
model = 'ctr';
@ -95,7 +95,7 @@ async function NASCMiddleware(request: express.Request, response: express.Respon
return;
}
let device: HydratedDeviceDocument = await Device.findOne({
let device: HydratedDeviceDocument | null = await Device.findOne({
model,
serial: serialNumber,
environment,
@ -174,7 +174,7 @@ async function NASCMiddleware(request: express.Request, response: express.Respon
}
}
const nexUser: HydratedNEXAccountDocument = await NEXAccount.findOne({ pid });
const nexUser: HydratedNEXAccountDocument | null = await NEXAccount.findOne({ pid });
if (!nexUser || nexUser.get('access_level') < 0) {
response.status(200).send(nascError('102'));

View File

@ -4,7 +4,7 @@ import { getUserBasic, getUserBearer } 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 = request.headers.authorization;
const authHeader: string | undefined = request.headers.authorization;
if (!authHeader || !(authHeader.startsWith('Bearer') || authHeader.startsWith('Basic'))) {
return next();
@ -13,7 +13,7 @@ async function PNIDMiddleware(request: express.Request, response: express.Respon
const parts: string[] = authHeader.split(' ');
const type: string = parts[0];
let token: string = parts[1];
let user: HydratedPNIDDocument;
let user: HydratedPNIDDocument | null;
if (request.isCemu) {
token = Buffer.from(token, 'hex').toString('base64');

View File

@ -4,8 +4,8 @@ import { document as xmlParser } from 'xmlbuilder2';
function XMLMiddleware(request: express.Request, response: express.Response, next: express.NextFunction): void {
if (request.method == 'POST' || request.method == 'PUT') {
const contentType: string = request.headers['content-type'];
const contentLength: string = request.headers['content-length'];
const contentType: string | undefined = request.headers['content-type'];
const contentLength: string | undefined = request.headers['content-length'];
let body: string = '';
if (

View File

@ -121,7 +121,7 @@ PNIDSchema.method('generatePID', async function generatePID(): Promise<void> {
const pid: number = Math.floor(Math.random() * (max - min + 1) + min);
const inuse: HydratedPNIDDocument = await PNID.findOne({
const inuse: HydratedPNIDDocument | null = await PNID.findOne({
pid
});
@ -143,7 +143,7 @@ PNIDSchema.method('generateEmailValidationCode', async function generateEmailVal
PNIDSchema.method('generateEmailValidationToken', async function generateEmailValidationToken(): Promise<void> {
const token: string = crypto.randomBytes(32).toString('hex');
const inuse: HydratedPNIDDocument = await PNID.findOne({
const inuse: HydratedPNIDDocument | null = await PNID.findOne({
'identification.email_token': token
});

View File

@ -17,7 +17,7 @@ const VALID_CONNECTION_TYPES: string[] = [
*/
router.post('/add/:type', async (request: express.Request, response: express.Response) => {
const data: ConnectionData = request.body?.data;
const pnid: HydratedPNIDDocument = request.pnid;
const pnid: HydratedPNIDDocument | null = request.pnid;
const type: string = request.params.type;
if (!pnid) {
@ -44,9 +44,17 @@ router.post('/add/:type', async (request: express.Request, response: express.Res
});
}
const result: ConnectionResponse = await addUserConnection(pnid, data, type);
let result: ConnectionResponse | undefined = await addUserConnection(pnid, data, type);
response.status(result.status).json(result);
if (!result) {
result = {
app: 'api',
status: 500,
error: 'Unknown server error'
};
}
response.status(result.status || 500).json(result);
});
/**
@ -55,7 +63,7 @@ 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) => {
const pnid: HydratedPNIDDocument = request.pnid;
const pnid: HydratedPNIDDocument | null = request.pnid;
const type: string = request.params.type;
if (!pnid) {
@ -74,7 +82,15 @@ router.delete('/remove/:type', async (request: express.Request, response: expres
});
}
const result: ConnectionResponse = await removeUserConnection(pnid, type);
let result: ConnectionResponse | undefined = await removeUserConnection(pnid, type);
if (!result) {
result = {
app: 'api',
status: 500,
error: 'Unknown server error'
};
}
response.status(result.status).json(result);
});

View File

@ -7,7 +7,7 @@ import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
const router: express.Router = express.Router();
router.get('/verify', async (request: express.Request, response: express.Response) => {
let token: string = request.query.token as string;
const token: string = request.query.token as string;
if (!token || token.trim() == '') {
return response.status(400).json({
@ -17,7 +17,7 @@ router.get('/verify', async (request: express.Request, response: express.Respons
});
}
const pnid: HydratedPNIDDocument = await PNID.findOne({
const pnid: HydratedPNIDDocument | null = await PNID.findOne({
'identification.email_token': token
});

View File

@ -17,7 +17,7 @@ router.post('/', async (request: express.Request, response: express.Response) =>
});
}
let pnid: HydratedPNIDDocument;
let pnid: HydratedPNIDDocument | null;
if (validator.isEmail(input)) {
pnid = await getUserByEmailAddress(input);

View File

@ -54,7 +54,7 @@ router.post('/', async (request: express.Request, response: express.Response) =>
});
}
let pnid: HydratedPNIDDocument;
let pnid: HydratedPNIDDocument | null;
if (grantType === 'password') {
pnid = await getUserByUsername(username);
@ -125,8 +125,10 @@ router.post('/', async (request: express.Request, response: express.Response) =>
expire_time: BigInt(Date.now() + (3600 * 1000))
};
const accessToken: string = await generateToken(cryptoOptions, accessTokenOptions);
const newRefreshToken: string = await generateToken(cryptoOptions, refreshTokenOptions);
const accessToken: string | null = await generateToken(cryptoOptions, accessTokenOptions);
const newRefreshToken: string | null = await generateToken(cryptoOptions, refreshTokenOptions);
// TODO - Handle null tokens
response.json({
access_token: accessToken,

View File

@ -363,8 +363,10 @@ router.post('/', async (request: express.Request, response: express.Response) =>
expire_time: BigInt(Date.now() + (3600 * 1000))
};
const accessToken: string = await generateToken(cryptoOptions, accessTokenOptions);
const refreshToken: string = await generateToken(cryptoOptions, refreshTokenOptions);
const accessToken: string | null = await generateToken(cryptoOptions, accessTokenOptions);
const refreshToken: string | null = await generateToken(cryptoOptions, refreshTokenOptions);
// TODO - Handle null tokens
response.json({
access_token: accessToken,

View File

@ -47,7 +47,7 @@ router.post('/', async (request: express.Request, response: express.Response) =>
});
}
const pnid: HydratedPNIDDocument = await PNID.findOne({ pid: unpackedToken.pid });
const pnid: HydratedPNIDDocument | null = await PNID.findOne({ pid: unpackedToken.pid });
if (!pnid) {
return response.status(400).json({

View File

@ -22,7 +22,7 @@ const userSchema: joi.ObjectSchema = joi.object({
* Description: Gets PNID details about the current user
*/
router.get('/', async (request: express.Request, response: express.Response) => {
const pnid: HydratedPNIDDocument = request.pnid;
const pnid: HydratedPNIDDocument | null = request.pnid;
if (!pnid) {
return response.status(400).json({
@ -70,7 +70,7 @@ router.get('/', async (request: express.Request, response: express.Response) =>
* Description: Updates PNID certain details about the current user
*/
router.post('/', async (request: express.Request, response: express.Response) => {
const pnid: HydratedPNIDDocument = request.pnid;
const pnid: HydratedPNIDDocument | null = request.pnid;
const updateUserRequest: UpdateUserRequest = request.body;
if (!pnid) {

View File

@ -8,11 +8,22 @@ const router: express.Router = express.Router();
const signatureSecret: Buffer = fs.readFileSync(`${__dirname}/../../../../certs/nex/datastore/secret.key`);
function multipartParser(request: express.Request, response: express.Response, next: express.NextFunction) {
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 boundary: RegExpExecArray = RE_BOUNDARY.exec(request.header('content-type'));
const contentType: string | undefined = request.header('content-type');
if (!contentType) {
return next();
}
const boundary: RegExpExecArray | null = RE_BOUNDARY.exec(contentType);
if (!boundary) {
return next();
}
const dicer: Dicer = new Dicer({ boundary: boundary[1] || boundary[2] });
const files: { [key: string]: Buffer } = {};
@ -21,6 +32,7 @@ function multipartParser(request: express.Request, response: express.Response, n
let fileName: string = '';
part.on('header', header => {
// TODO - strict mode yells here
fileName = RE_FILE_NAME.exec(header['content-disposition'][0])[1];
});
@ -46,6 +58,10 @@ function multipartParser(request: express.Request, response: express.Response, n
}
router.post('/upload', multipartParser, async (request: express.Request, response: express.Response) => {
if (!request.files) {
return response.sendStatus(500);
}
const bucket: string = request.files.bucket.toString();
const key: string = request.files.key.toString();
const file: Buffer = request.files.file;

View File

@ -18,7 +18,7 @@ const router: express.Router = express.Router();
router.post('/', async (request: express.Request, response: express.Response) => {
const requestParams: NASCRequestParams = request.body;
const action: string = nintendoBase64Decode(requestParams.action).toString();
let responseData: URLSearchParams;
let responseData: URLSearchParams = nascError('null');
switch (action) {
case 'LOGIN':
@ -35,7 +35,12 @@ router.post('/', async (request: express.Request, response: express.Response) =>
async function processLoginRequest(request: express.Request): Promise<URLSearchParams> {
const requestParams: NASCRequestParams = request.body;
const titleID: string = nintendoBase64Decode(requestParams.titleid).toString();
const nexUser: HydratedNEXAccountDocument = request.nexUser;
const nexUser: HydratedNEXAccountDocument | null = request.nexUser;
if (!nexUser) {
// TODO - Research this error more
return nascError('null');
}
// TODO: REMOVE AFTER PUBLIC LAUNCH
// LET EVERYONE IN THE `test` FRIENDS SERVER
@ -45,7 +50,7 @@ async function processLoginRequest(request: express.Request): Promise<URLSearchP
serverAccessLevel = nexUser.get('server_access_level');
}
const server: HydratedServerDocument = await getServerByTitleId(titleID, serverAccessLevel);
const server: HydratedServerDocument | null = await getServerByTitleId(titleID, serverAccessLevel);
if (!server || !server.service_name || !server.ip || !server.port) {
return nascError('110');
@ -72,8 +77,10 @@ async function processLoginRequest(request: express.Request): Promise<URLSearchP
expire_time: BigInt(Date.now() + (3600 * 1000))
};
let nexToken: string = await generateToken(cryptoOptions, tokenOptions);
nexToken = nintendoBase64Encode(Buffer.from(nexToken, 'base64'));
// TODO - Handle null tokens
let nexToken: string | null = await generateToken(cryptoOptions, tokenOptions);
nexToken = nintendoBase64Encode(Buffer.from(nexToken || '', 'base64'));
return new URLSearchParams({
locator: nintendoBase64Encode(`${ip}:${port}`),

View File

@ -64,9 +64,10 @@ router.get('/mapped_ids', async (request: express.Request, response: express.Res
pid?: number;
} = {};
// TODO - TS strict mode...what?
query[queryInput] = input;
const searchResult: HydratedPNIDDocument = await PNID.findOne(query);
const searchResult: HydratedPNIDDocument | null = await PNID.findOne(query);
if (searchResult) {
result.out_id = searchResult.get(queryOutput);

View File

@ -52,7 +52,7 @@ router.post('/access_token/generate', async (request: express.Request, response:
}).end());
}
const pnid: HydratedPNIDDocument = await getUserByUsername(username);
const pnid: HydratedPNIDDocument | null = await getUserByUsername(username);
if (!pnid || !await bcrypt.compare(password, pnid.password)) {
response.status(400);
@ -103,12 +103,14 @@ router.post('/access_token/generate', async (request: express.Request, response:
expire_time: BigInt(Date.now() + (3600 * 1000))
};
let accessToken: string = await generateToken(null, accessTokenOptions);
let refreshToken: string = await generateToken(null, refreshTokenOptions);
let accessToken: string | null = await generateToken(null, accessTokenOptions);
let refreshToken: string | null = await generateToken(null, refreshTokenOptions);
// TODO - Handle null tokens
if (request.isCemu) {
accessToken = Buffer.from(accessToken, 'base64').toString('hex');
refreshToken = Buffer.from(refreshToken, 'base64').toString('hex');
accessToken = Buffer.from(accessToken || '', 'base64').toString('hex');
refreshToken = Buffer.from(refreshToken || '', 'base64').toString('hex');
}
response.send(xmlbuilder.create({

View File

@ -212,9 +212,21 @@ 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 = request.pnid;
const pnid: HydratedPNIDDocument | null = request.pnid;
const person: PNIDProfile = await getUserProfileJSONByPID(pnid.get('pid'));
if (!pnid) {
// TODO - Research this error more
response.status(404);
return response.send('<errors><error><cause/><code>0008</code><message>Not Found</message></error></errors>');
}
const person: PNIDProfile | null = await getUserProfileJSONByPID(pnid.get('pid'));
if (!person) {
// TODO - Research this error more
response.status(404);
return response.send('<errors><error><cause/><code>0008</code><message>Not Found</message></error></errors>');
}
response.send(xmlbuilder.create({
person
@ -237,9 +249,21 @@ router.post('/@me/devices', async (request: express.Request, response: express.R
// TODO - CHANGE THIS. WE NEED TO SAVE CONSOLE DETAILS !!!
const pnid: HydratedPNIDDocument = request.pnid;
const pnid: HydratedPNIDDocument | null = request.pnid;
const person: PNIDProfile = await getUserProfileJSONByPID(pnid.pid);
if (!pnid) {
// TODO - Research this error more
response.status(404);
return response.send('<errors><error><cause/><code>0008</code><message>Not Found</message></error></errors>');
}
const person: PNIDProfile | null = await getUserProfileJSONByPID(pnid.get('pid'));
if (!person) {
// TODO - Research this error more
response.status(404);
return response.send('<errors><error><cause/><code>0008</code><message>Not Found</message></error></errors>');
}
response.send(xmlbuilder.create({
person
@ -256,7 +280,7 @@ 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 = request.pnid;
const pnid: HydratedPNIDDocument | null = request.pnid;
const deviceId: string = request.headers['x-nintendo-device-id'] as string;
const acceptLanguage: string = request.headers['accept-language'] as string;
const platformId: string = request.headers['x-nintendo-platform-id'] as string;
@ -264,6 +288,12 @@ router.get('/@me/devices', async (request: express.Request, response: express.Re
const serialNumber: string = request.headers['x-nintendo-serial-number'] as string;
const systemVersion: string = request.headers['x-nintendo-system-version'] as string;
if (!pnid) {
// TODO - Research this error more
response.status(404);
return response.send('<errors><error><cause/><code>0008</code><message>Not Found</message></error></errors>');
}
response.send(xmlbuilder.create({
devices: [
{
@ -295,9 +325,21 @@ 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 = request.pnid;
const pnid: HydratedPNIDDocument | null = request.pnid;
const person: PNIDProfile = await getUserProfileJSONByPID(pnid.get('pid'));
if (!pnid) {
// TODO - Research this error more
response.status(404);
return response.send('<errors><error><cause/><code>0008</code><message>Not Found</message></error></errors>');
}
const person: PNIDProfile | null = await getUserProfileJSONByPID(pnid.get('pid'));
if (!person) {
// TODO - Research this error more
response.status(404);
return response.send('<errors><error><cause/><code>0008</code><message>Not Found</message></error></errors>');
}
response.send(xmlbuilder.create({
person
@ -326,14 +368,22 @@ 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) => {
const pnid: HydratedPNIDDocument = request.pnid;
const pnid: HydratedPNIDDocument | null = request.pnid;
if (!pnid) {
// TODO - Research this error more
response.status(404);
return response.send('<errors><error><cause/><code>0008</code><message>Not Found</message></error></errors>');
}
// TODO - Make this more strictly typed?
const mii: Map<string, string> = request.body.get('mii');
const name: string = mii.get('name');
const primary: string = mii.get('primary');
const data: string = mii.get('data');
// TODO - Better checks
const name: string | undefined = mii.get('name') || '';
const primary: string | undefined = mii.get('primary') || '';
const data: string | undefined = mii.get('data') || '';
await pnid.updateMii({ name, primary, data });
@ -349,7 +399,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 = request.pnid;
const pnid: HydratedPNIDDocument | null = request.pnid;
if (!pnid) {
response.status(400);
@ -375,7 +425,7 @@ router.put('/@me/devices/@current/inactivate', async (request: express.Request,
* Description: Deletes a NNID
*/
router.put('/@me/deletion', async (request: express.Request, response: express.Response) => {
const pnid: HydratedPNIDDocument = request.pnid;
const pnid: HydratedPNIDDocument | null = request.pnid;
if (!pnid) {
response.status(400);
@ -400,7 +450,7 @@ router.put('/@me/deletion', async (request: express.Request, response: express.R
* Description: Updates a PNIDs account details
*/
router.put('/@me', async (request: express.Request, response: express.Response) => {
const pnid: HydratedPNIDDocument = request.pnid;
const pnid: HydratedPNIDDocument | null = request.pnid;
const person: Person = request.body.get('person');
if (!pnid) {
@ -454,7 +504,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) => {
const pnid: HydratedPNIDDocument = request.pnid;
const pnid: HydratedPNIDDocument | null = request.pnid;
if (!pnid) {
response.status(400);
@ -495,12 +545,12 @@ 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) => {
const pnid: HydratedPNIDDocument = request.pnid;
const pnid: HydratedPNIDDocument | null = request.pnid;
// TODO - Make this more strictly typed?
const email: Map<string, string> = request.body.get('email');
if (!pnid) {
if (!pnid || !email) {
response.status(400);
return response.end(xmlbuilder.create({
@ -514,7 +564,8 @@ router.put('/@me/emails/@primary', async (request: express.Request, response: ex
}).end());
}
pnid.set('email.address', email.get('address').toLowerCase());
// TODO - Better email check
pnid.set('email.address', (email.get('address') || '').toLowerCase());
pnid.set('email.reachable', false);
pnid.set('email.validated', false);
pnid.set('email.validated_date', '');

View File

@ -19,11 +19,25 @@ const router: express.Router = express.Router();
* Description: Gets a service token
*/
router.get('/service_token/@me', async (request: express.Request, response: express.Response) => {
const pnid: HydratedPNIDDocument = request.pnid;
const pnid: HydratedPNIDDocument | null = request.pnid;
if (!pnid) {
response.status(400);
return response.end(xmlbuilder.create({
errors: {
error: {
cause: 'access_token',
code: '0002',
message: 'Invalid access token'
}
}
}).end());
}
const titleId: string = request.headers['x-nintendo-title-id'] as string;
const serverAccessLevel: string = pnid.get('server_access_level');
const server: HydratedServerDocument = await getServerByTitleId(titleId, serverAccessLevel);
const server: HydratedServerDocument | null = await getServerByTitleId(titleId, serverAccessLevel);
if (!server) {
return response.send(xmlbuilder.create({
@ -70,10 +84,12 @@ router.get('/service_token/@me', async (request: express.Request, response: expr
expire_time: BigInt(Date.now() + (3600 * 1000))
};
let serviceToken: string = await generateToken(cryptoOptions, tokenOptions);
let serviceToken: string | null = await generateToken(cryptoOptions, tokenOptions);
// TODO - Handle null tokens
if (request.isCemu) {
serviceToken = Buffer.from(serviceToken, 'base64').toString('hex');
serviceToken = Buffer.from(serviceToken || '', 'base64').toString('hex');
}
response.send(xmlbuilder.create({
@ -89,7 +105,22 @@ 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) => {
const pnid: HydratedPNIDDocument = request.pnid;
const pnid: HydratedPNIDDocument | null = request.pnid;
if (!pnid) {
response.status(400);
return response.end(xmlbuilder.create({
errors: {
error: {
cause: 'access_token',
code: '0002',
message: 'Invalid access token'
}
}
}).end());
}
const gameServerID: string = request.query.game_server_id as string;
if (!gameServerID) {
@ -104,7 +135,7 @@ router.get('/nex_token/@me', async (request: express.Request, response: express.
}
const serverAccessLevel: string = pnid.get('server_access_level');
const server: HydratedServerDocument = await getServer(gameServerID, serverAccessLevel);
const server: HydratedServerDocument | null = await getServer(gameServerID, serverAccessLevel);
if (!server) {
return response.send(xmlbuilder.create({
@ -154,7 +185,7 @@ router.get('/nex_token/@me', async (request: express.Request, response: express.
expire_time: BigInt(Date.now() + (3600 * 1000))
};
const nexUser: HydratedNEXAccountDocument = await NEXAccount.findOne({
const nexUser: HydratedNEXAccountDocument | null = await NEXAccount.findOne({
owning_pid: pnid.get('pid')
});
@ -163,10 +194,12 @@ router.get('/nex_token/@me', async (request: express.Request, response: express.
return response.send('<errors><error><cause/><code>0008</code><message>Not Found</message></error></errors>');
}
let nexToken: string = await generateToken(cryptoOptions, tokenOptions);
let nexToken: string | null = await generateToken(cryptoOptions, tokenOptions);
// TODO = Handle null tokens
if (request.isCemu) {
nexToken = Buffer.from(nexToken, 'base64').toString('hex');
nexToken = Buffer.from(nexToken || '', 'base64').toString('hex');
}
response.send(xmlbuilder.create({

View File

@ -30,7 +30,7 @@ router.post('/validate/email', async (request: express.Request, response: expres
const domain: string = email.split('@')[1];
dns.resolveMx(domain, (error: NodeJS.ErrnoException) => {
dns.resolveMx(domain, (error: NodeJS.ErrnoException | null) => {
if (error) {
return response.send(xmlbuilder.create({
errors: {
@ -56,7 +56,7 @@ router.put('/email_confirmation/:pid/:code', async (request: express.Request, re
const code: string = request.params.code;
const pid: number = Number(request.params.pid);
const pnid: HydratedPNIDDocument = await getUserByPID(pid);
const pnid: HydratedPNIDDocument | null = await getUserByPID(pid);
if (!pnid) {
return response.status(400).send(xmlbuilder.create({
@ -101,7 +101,7 @@ router.put('/email_confirmation/:pid/:code', async (request: express.Request, re
router.get('/resend_confirmation', async (request: express.Request, response: express.Response) => {
const pid: number = Number(request.headers['x-nintendo-pid']);
const pnid: HydratedPNIDDocument = await getUserByPID(pid);
const pnid: HydratedPNIDDocument | null = await getUserByPID(pid);
if (!pnid) {
// TODO - Unsure if this is the right error
@ -129,7 +129,7 @@ router.get('/resend_confirmation', async (request: express.Request, response: ex
router.get('/forgotten_password/:pid', async (request: express.Request, response: express.Response) => {
const pid: number = Number(request.params.pid);
const pnid: HydratedPNIDDocument = await getUserByPID(pid);
const pnid: HydratedPNIDDocument | null = await getUserByPID(pid);
if (!pnid) {
// TODO - Better errors

View File

@ -5,8 +5,8 @@ import { HydratedNEXAccountDocument } from '@/types/mongoose/nex-account';
declare global {
namespace Express {
interface Request {
pnid?: HydratedPNIDDocument;
nexUser?: HydratedNEXAccountDocument;
pnid: HydratedPNIDDocument | null;
nexUser: HydratedNEXAccountDocument | null;
isCemu?: boolean;
files?: Record<string, any>;
certificate?: NintendoCertificate;

View File

@ -6,13 +6,13 @@ export interface PNIDProfile {
birth_date: string;
country: string;
create_date: string;
device_attributes: [{
device_attributes: {
device_attribute: {
name: string;
value: string;
created_date: string;
};
}];
}[];
gender: string;
language: string;
updated: string;

View File

@ -47,7 +47,7 @@ export function nintendoBase64Encode(decoded: string | Buffer): string {
return encoded.replaceAll('+', '.').replaceAll('/', '-').replaceAll('=', '*');
}
export async function generateToken(cryptoOptions: CryptoOptions | null, tokenOptions: TokenOptions): Promise<string> {
export async function generateToken(cryptoOptions: CryptoOptions | null, tokenOptions: TokenOptions): Promise<string | null> {
// Access and refresh tokens use a different format since they must be much smaller
// They take no extra crypto options
if (!cryptoOptions) {
@ -67,6 +67,8 @@ export async function generateToken(cryptoOptions: CryptoOptions | null, tokenOp
encryptedBody = Buffer.concat([encryptedBody, cipher.final()]);
return encryptedBody.toString('base64');
} else if (!tokenOptions.access_level || !tokenOptions.title_id) {
return null;
}
const publicKey: NodeRSA = new NodeRSA(cryptoOptions.public_key, 'pkcs8-public-pem', {
@ -191,8 +193,9 @@ export async function decryptToken(token: Buffer): Promise<Buffer> {
const calculatedSignature: Buffer = hmac.digest();
if (!signature.equals(calculatedSignature)) {
// TODO - FIX THIS. ONLY DONE SO STRICT MODE DOESN'T YELL
console.log('Token signature did not match');
return null;
return Buffer.alloc(0);
}
return decryptedBody;
@ -300,7 +303,9 @@ export async function sendForgotPasswordEmail(pnid: mongoose.HydratedDocument<IP
expire_time: BigInt(Date.now() + (24 * 60 * 60 * 1000)) // Only valid for 24 hours
};
const passwordResetToken: string = await generateToken(cryptoOptions, tokenOptions);
const passwordResetToken: string | null = await generateToken(cryptoOptions, tokenOptions);
// TODO - Handle null token
const mailerOptions: MailerOptions = {
to: pnid.get('email.address'),
@ -309,9 +314,9 @@ export async function sendForgotPasswordEmail(pnid: mongoose.HydratedDocument<IP
paragraph: 'a password reset has been requested from this account. If you did not request the password reset, please ignore this email. If you did request this password reset, please click the link below to reset your password.',
link: {
text: 'Reset password',
href: `${config.website_base}/account/reset-password?token=${encodeURIComponent(passwordResetToken)}`
href: `${config.website_base}/account/reset-password?token=${encodeURIComponent(passwordResetToken || '')}`
},
text: `Dear ${pnid.get('username')}, a password reset has been requested from this account. \r\n\r\nIf you did not request the password reset, please ignore this email. \r\nIf you did request this password reset, please click the link to reset your password: ${config.website_base}/account/reset-password?token=${encodeURIComponent(passwordResetToken)}`
text: `Dear ${pnid.get('username')}, a password reset has been requested from this account. \r\n\r\nIf you did not request the password reset, please ignore this email. \r\nIf you did request this password reset, please click the link to reset your password: ${config.website_base}/account/reset-password?token=${encodeURIComponent(passwordResetToken || '')}`
};
await sendMail(mailerOptions);

View File

@ -1,5 +1,6 @@
{
"compilerOptions": {
"strict": true,
"resolveJsonModule": true,
"module": "commonjs",
"esModuleInterop": true,