mirror of
https://github.com/PretendoNetwork/account.git
synced 2025-04-02 11:02:15 -04:00
309 lines
No EOL
8.7 KiB
TypeScript
309 lines
No EOL
8.7 KiB
TypeScript
import mongoose from 'mongoose';
|
|
import bcrypt from 'bcrypt';
|
|
import joi from 'joi';
|
|
import { nintendoPasswordHash, decryptToken, unpackToken } from '@/util';
|
|
import { PNID } from '@/models/pnid';
|
|
import { Server } from '@/models/server';
|
|
import { LOG_ERROR } from '@/logger';
|
|
import { config } from '@/config-manager';
|
|
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
|
|
import { IDeviceAttribute } from '@/types/mongoose/device-attribute';
|
|
import { HydratedServerDocument } from '@/types/mongoose/server';
|
|
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 = config.mongoose.connection_string;
|
|
const options = config.mongoose.options;
|
|
|
|
// TODO - Extend this later with more settings
|
|
const discordConnectionSchema = joi.object({
|
|
id: joi.string()
|
|
});
|
|
|
|
const accessModeOrder: Record<string, string[]> = {
|
|
prod: ['prod'],
|
|
test: ['test', 'prod'],
|
|
dev: ['dev', 'test', 'prod']
|
|
};
|
|
|
|
let _connection: mongoose.Connection;
|
|
|
|
export async function connect(): Promise<void> {
|
|
await mongoose.connect(connection_string, options);
|
|
|
|
_connection = mongoose.connection;
|
|
_connection.on('error', console.error.bind(console, 'connection error:'));
|
|
}
|
|
|
|
export function connection(): mongoose.Connection {
|
|
return _connection;
|
|
}
|
|
|
|
export function verifyConnected(): void {
|
|
if (!connection()) {
|
|
throw new Error('Cannot make database requets without being connected');
|
|
}
|
|
}
|
|
|
|
export async function getPNIDByUsername(username: string): Promise<HydratedPNIDDocument | null> {
|
|
verifyConnected();
|
|
|
|
return await PNID.findOne<HydratedPNIDDocument>({
|
|
usernameLower: username.toLowerCase()
|
|
});
|
|
}
|
|
|
|
export async function getPNIDByPID(pid: number): Promise<HydratedPNIDDocument | null> {
|
|
verifyConnected();
|
|
|
|
return await PNID.findOne<HydratedPNIDDocument>({
|
|
pid
|
|
});
|
|
}
|
|
|
|
export async function getPNIDByEmailAddress(email: string): Promise<HydratedPNIDDocument | null> {
|
|
verifyConnected();
|
|
|
|
// TODO - Update documents to store email normalized
|
|
return await PNID.findOne<HydratedPNIDDocument>({
|
|
'email.address': email
|
|
});
|
|
}
|
|
|
|
export async function doesPNIDExist(username: string): Promise<boolean> {
|
|
verifyConnected();
|
|
|
|
return !!await getPNIDByUsername(username);
|
|
}
|
|
|
|
export async function getPNIDByBasicAuth(token: string): Promise<HydratedPNIDDocument | null> {
|
|
verifyConnected();
|
|
|
|
// * 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 = Buffer.from(token, 'base64').toString();
|
|
const parts = decoded.split(' ');
|
|
|
|
const username = parts[0];
|
|
const password = parts[1];
|
|
|
|
const pnid = await getPNIDByUsername(username);
|
|
|
|
if (!pnid) {
|
|
return null;
|
|
}
|
|
|
|
const hashedPassword = nintendoPasswordHash(password, pnid.pid);
|
|
|
|
if (!bcrypt.compareSync(hashedPassword, pnid.password)) {
|
|
return null;
|
|
}
|
|
|
|
return pnid;
|
|
}
|
|
|
|
export async function getPNIDByTokenAuth(token: string): Promise<HydratedPNIDDocument | null> {
|
|
verifyConnected();
|
|
|
|
try {
|
|
const decryptedToken = decryptToken(Buffer.from(token, 'hex'));
|
|
const unpackedToken = unpackToken(decryptedToken);
|
|
const pnid = await getPNIDByPID(unpackedToken.pid);
|
|
|
|
if (pnid) {
|
|
const expireTime = Math.floor((Number(unpackedToken.expire_time) / 1000));
|
|
|
|
if (Math.floor(Date.now() / 1000) > expireTime) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return pnid;
|
|
} catch (error: any) {
|
|
// TODO - Handle error
|
|
LOG_ERROR(error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function getPNIDProfileJSONByPID(pid: number): Promise<PNIDProfile | null> {
|
|
verifyConnected();
|
|
|
|
const pnid = await getPNIDByPID(pid);
|
|
|
|
if (!pnid) {
|
|
return null;
|
|
}
|
|
|
|
const device = pnid.devices[0]; // * Just grab the first device
|
|
let device_attributes: {
|
|
device_attribute: {
|
|
name: string;
|
|
value: string;
|
|
created_date: string;
|
|
};
|
|
}[] = [];
|
|
|
|
if (device) {
|
|
device_attributes = device.device_attributes.map((attribute: IDeviceAttribute) => {
|
|
const name = attribute.name;
|
|
const value = attribute.value;
|
|
const created_date = attribute.created_date;
|
|
|
|
return {
|
|
device_attribute: {
|
|
name,
|
|
value,
|
|
created_date: created_date ? created_date : ''
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
return {
|
|
// *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,
|
|
create_date: pnid.creation_date,
|
|
device_attributes: device_attributes,
|
|
gender: pnid.gender,
|
|
language: pnid.language,
|
|
updated: pnid.updated,
|
|
marketing_flag: pnid.flags.marketing ? 'Y' : 'N',
|
|
off_device_flag: pnid.flags.off_device ? 'Y' : 'N',
|
|
pid: pnid.pid,
|
|
email: {
|
|
address: pnid.email.address,
|
|
id: pnid.email.id,
|
|
parent: pnid.email.parent ? 'Y' : 'N',
|
|
primary: pnid.email.primary ? 'Y' : 'N',
|
|
reachable: pnid.email.reachable ? 'Y' : 'N',
|
|
type: 'DEFAULT',
|
|
updated_by: 'USER', // * Can also be INTERNAL WS, don't know the difference
|
|
validated: pnid.email.validated ? 'Y' : 'N',
|
|
validated_date: pnid.email.validated ? pnid.email.validated_date : ''
|
|
},
|
|
mii: {
|
|
status: 'COMPLETED',
|
|
data: pnid.mii.data.replace(/(\r\n|\n|\r)/gm, ''),
|
|
id: pnid.mii.id,
|
|
mii_hash: pnid.mii.hash,
|
|
mii_images: {
|
|
mii_image: {
|
|
// * Images MUST be loaded over HTTPS or console ignores them
|
|
// * Bunny CDN is the only CDN which seems to support TLS 1.0/1.1 (required)
|
|
cached_url: `${config.cdn.base_url}/mii/${pnid.pid}/standard.tga`,
|
|
id: pnid.mii.image_id,
|
|
url: `${config.cdn.base_url}/mii/${pnid.pid}/standard.tga`,
|
|
type: 'standard'
|
|
}
|
|
},
|
|
name: pnid.mii.name,
|
|
primary: pnid.mii.primary ? 'Y' : 'N',
|
|
},
|
|
region: pnid.region,
|
|
tz_name: pnid.timezone.name,
|
|
user_id: pnid.username,
|
|
utc_offset: pnid.timezone.offset
|
|
};
|
|
}
|
|
|
|
export async function getServerByGameServerID(gameServerID: string, accessMode: string): Promise<HydratedServerDocument | null> {
|
|
const searchModes = accessModeOrder[accessMode] ?? accessModeOrder.prod; // Default to prod if invalid mode
|
|
|
|
const servers = await Server.find({
|
|
game_server_id: gameServerID,
|
|
access_mode: { $in: searchModes }
|
|
});
|
|
|
|
for (const mode of searchModes) {
|
|
const server = servers.find(s => s.access_mode === mode);
|
|
if (server) return server;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export async function getServerByTitleID(titleID: string, accessMode: string): Promise<HydratedServerDocument | null> {
|
|
const searchModes = accessModeOrder[accessMode] ?? accessModeOrder.prod;
|
|
|
|
const servers = await Server.find({
|
|
title_ids: titleID,
|
|
access_mode: { $in: searchModes }
|
|
});
|
|
|
|
for (const mode of searchModes) {
|
|
const server = servers.find(s => s.access_mode === mode);
|
|
if (server) return server;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export async function getServerByClientID(clientID: string, accessMode: string): Promise<HydratedServerDocument | null> {
|
|
const searchModes = accessModeOrder[accessMode] ?? accessModeOrder.prod;
|
|
|
|
const servers = await Server.find({
|
|
client_id: clientID,
|
|
access_mode: { $in: searchModes }
|
|
});
|
|
|
|
for (const mode of searchModes) {
|
|
const server = servers.find(s => s.access_mode === mode);
|
|
if (server) return server;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export async function addPNIDConnection(pnid: HydratedPNIDDocument, data: ConnectionData, type: string): Promise<ConnectionResponse | undefined> {
|
|
if (type === 'discord') {
|
|
return await addPNIDConnectionDiscord(pnid, data);
|
|
}
|
|
}
|
|
|
|
export async function addPNIDConnectionDiscord(pnid: HydratedPNIDDocument, data: DiscordConnectionData): Promise<ConnectionResponse> {
|
|
const valid = discordConnectionSchema.validate(data);
|
|
|
|
if (valid.error) {
|
|
return {
|
|
app: 'api',
|
|
status: 400,
|
|
error: 'Invalid or missing connection data'
|
|
};
|
|
}
|
|
|
|
await PNID.updateOne({ pid: pnid.pid }, {
|
|
$set: {
|
|
'connections.discord.id': data.id
|
|
}
|
|
});
|
|
|
|
return {
|
|
app: 'api',
|
|
status: 200
|
|
};
|
|
}
|
|
|
|
export async function removePNIDConnection(pnid: HydratedPNIDDocument, type: string): Promise<ConnectionResponse | undefined> {
|
|
// * Add more connections later?
|
|
if (type === 'discord') {
|
|
return await removePNIDConnectionDiscord(pnid);
|
|
}
|
|
}
|
|
|
|
export async function removePNIDConnectionDiscord(pnid: HydratedPNIDDocument): Promise<ConnectionResponse> {
|
|
await PNID.updateOne({ pid: pnid.pid }, {
|
|
$set: {
|
|
'connections.discord.id': ''
|
|
}
|
|
});
|
|
|
|
return {
|
|
app: 'api',
|
|
status: 200
|
|
};
|
|
} |