feat: use "enum" for token system type

This commit is contained in:
William Oldham 2025-01-30 11:59:58 +00:00
parent cad2d0e68e
commit 169e686944
10 changed files with 72 additions and 30 deletions

View File

@ -1,6 +1,7 @@
import { Schema, model } from 'mongoose';
import uniqueValidator from 'mongoose-unique-validator';
import { IServer, IServerMethods, ServerModel } from '@/types/mongoose/server';
import type { SystemType } from '@/types/common/token';
const ServerSchema = new Schema<IServer, ServerModel, IServerMethods>({
client_id: String,
@ -18,4 +19,9 @@ const ServerSchema = new Schema<IServer, ServerModel, IServerMethods>({
ServerSchema.plugin(uniqueValidator, { message: '{PATH} already in use.' });
export const Server = model<IServer, ServerModel>('Server', ServerSchema);
export const Server = model<IServer, ServerModel>('Server', ServerSchema);
export const serverDeviceToSystemType: Record<number, SystemType> = {
1: 'WIIU',
2: '3DS'
};

View File

@ -109,8 +109,7 @@ router.post('/', async (request: express.Request, response: express.Response): P
}
try {
const systemType = 0x3; // * API
const tokenGeneration = generateOAuthTokens(systemType, pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days
const tokenGeneration = generateOAuthTokens('API', pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days
response.json({
access_token: tokenGeneration.accessToken,

View File

@ -365,10 +365,9 @@ router.post('/', async (request: express.Request, response: express.Response): P
}
await sendConfirmationEmail(pnid);
try {
const systemType = 0x3; // * API
const tokenGeneration = generateOAuthTokens(systemType, pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days
const tokenGeneration = generateOAuthTokens('API', pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days
response.json({
access_token: tokenGeneration.accessToken,

View File

@ -54,8 +54,7 @@ export async function login(request: LoginRequest): Promise<DeepPartial<LoginRes
}
try {
const systemType = 0x3; // * API
const tokenGeneration = generateOAuthTokens(systemType, pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days
const tokenGeneration = generateOAuthTokens('API', pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days
return {
accessToken: tokenGeneration.accessToken,

View File

@ -230,8 +230,7 @@ export async function register(request: RegisterRequest): Promise<DeepPartial<Lo
await sendConfirmationEmail(pnid);
try {
const systemType = 0x3; // * API
const tokenGeneration = generateOAuthTokens(systemType, pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days
const tokenGeneration = generateOAuthTokens('API', pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days
return {
accessToken: tokenGeneration.accessToken,

View File

@ -66,7 +66,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 = {
system_type: 0x2, // * 3DS
system_type: '3DS',
token_type: 'NEX',
pid: pid,
access_level: 0,
@ -90,7 +90,7 @@ async function processLoginRequest(server: HydratedServerDocument, pid: number,
async function processServiceTokenRequest(server: HydratedServerDocument, pid: number, titleID: string): Promise<URLSearchParams> {
const tokenOptions: TokenOptions = {
system_type: 0x2, // * 3DS
system_type: '3DS',
token_type: 'SERVICE',
pid: pid,
access_level: 0,

View File

@ -153,8 +153,7 @@ router.post('/access_token/generate', deviceCertificateMiddleware, consoleStatus
}
try {
const systemType = 0x1; // * WiiU
const tokenGeneration = generateOAuthTokens(systemType, pnid);
const tokenGeneration = generateOAuthTokens('WIIU', pnid);
response.send(xmlbuilder.create({
OAuth20: {

View File

@ -4,6 +4,7 @@ import { getServerByClientID, getServerByGameServerID } from '@/database';
import { generateToken, getValueFromHeaders, getValueFromQueryString } from '@/util';
import { NEXAccount } from '@/models/nex-account';
import { TokenOptions } from '@/types/common/token';
import { serverDeviceToSystemType } from '@/models/server';
const router = express.Router();
@ -90,8 +91,20 @@ router.get('/service_token/@me', async (request: express.Request, response: expr
return;
}
const systemType = serverDeviceToSystemType[server.device];
if (!systemType) {
response.send(xmlbuilder.create({
errors: {
error: {
code: '1021',
message: 'The requested game server was not found'
}
}
}).end());
}
const tokenOptions: TokenOptions = {
system_type: server.device,
system_type: systemType,
token_type: 'SERVICE',
pid: pnid.pid,
access_level: pnid.access_level,
@ -213,8 +226,20 @@ router.get('/nex_token/@me', async (request: express.Request, response: express.
return;
}
const systemType = serverDeviceToSystemType[server.device];
if (!systemType) {
response.send(xmlbuilder.create({
errors: {
error: {
code: '1021',
message: 'The requested game server was not found'
}
}
}).end());
}
const tokenOptions: TokenOptions = {
system_type: server.device,
system_type: systemType,
token_type: 'NEX',
pid: pnid.pid,
access_level: pnid.access_level,

View File

@ -5,15 +5,28 @@ export const TokenTypes = {
SERVICE: 4,
PASSWORD_RESET: 5
} as const;
export type TokenType = keyof typeof TokenTypes;
export function getTokenTypeFromValue(type: number): keyof typeof TokenTypes | undefined {
const keys = Object.keys(TokenTypes) as (keyof typeof TokenTypes)[];
export function getTokenTypeFromValue(type: number): TokenType | undefined {
const keys = Object.keys(TokenTypes) as TokenType[];
return keys.find((key) => TokenTypes[key] === type);
}
export const SystemTypes = {
'WIIU': 1,
'3DS': 2,
'API': 3
} as const;
export type SystemType = keyof typeof SystemTypes;
export function getSystemTypeFromValue(type: number): SystemType | undefined {
const keys = Object.keys(SystemTypes) as SystemType[];
return keys.find((key) => SystemTypes[key] === type);
}
export interface Token {
system_type: number;
token_type: keyof typeof TokenTypes;
system_type: SystemType;
token_type: TokenType;
pid: number;
access_level?: number;
title_id?: bigint;

View File

@ -10,7 +10,7 @@ import crc32 from 'buffer-crc32';
import crc from 'crc';
import { sendMail } from '@/mailer';
import { config, disabledFeatures } from '@/config-manager';
import { getTokenTypeFromValue, OAuthTokenGenerationResponse, OAuthTokenOptions, Token, TokenOptions, TokenTypes } from '@/types/common/token';
import { getSystemTypeFromValue, getTokenTypeFromValue, OAuthTokenGenerationResponse, OAuthTokenOptions, SystemType, SystemTypes, Token, TokenOptions, TokenTypes } from '@/types/common/token';
import { HydratedPNIDDocument, IPNID, IPNIDMethods } from '@/types/mongoose/pnid';
import { SafeQs } from '@/types/common/safe-qs';
@ -52,7 +52,7 @@ export function nintendoBase64Encode(decoded: string | Buffer): string {
return encoded.replaceAll('+', '.').replaceAll('/', '-').replaceAll('=', '*');
}
export function generateOAuthTokens(systemType: number, pnid: HydratedPNIDDocument, options?: OAuthTokenOptions): OAuthTokenGenerationResponse {
export function generateOAuthTokens(systemType: SystemType, pnid: HydratedPNIDDocument, options?: OAuthTokenOptions): OAuthTokenGenerationResponse {
const accessTokenExpiresInSecs = options?.accessExpiresIn ?? 60 * 60; // * 1 hour
const refreshTokenExpiresInSecs = options?.refreshExpiresIn ?? 24 * 60 * 60; // * 24 hours
@ -92,14 +92,15 @@ export function generateOAuthTokens(systemType: number, pnid: HydratedPNIDDocume
export function generateToken(key: string, options: TokenOptions): Buffer | null {
let dataBuffer = Buffer.alloc(1 + 1 + 4 + 8);
const systemType = SystemTypes[options.system_type];
const tokenType = TokenTypes[options.token_type];
dataBuffer.writeUInt8(options.system_type, 0x0);
dataBuffer.writeUInt8(systemType, 0x0);
dataBuffer.writeUInt8(tokenType, 0x1);
dataBuffer.writeUInt32LE(options.pid, 0x2);
dataBuffer.writeBigUInt64LE(options.expire_time, 0x6);
if ((options.token_type !== 'OAUTH_ACCESS' && options.token_type !== 'OAUTH_REFRESH') || options.system_type === 0x3) {
if ((options.token_type !== 'OAUTH_ACCESS' && options.token_type !== 'OAUTH_REFRESH') || options.system_type === 'API') {
// * Access and refresh tokens have smaller bodies due to size constraints
// * The API does not have this restraint, however
if (options.title_id === undefined || options.access_level === undefined) {
@ -125,7 +126,7 @@ export function generateToken(key: string, options: TokenOptions): Buffer | null
let final = encrypted;
if ((options.token_type !== 'OAUTH_ACCESS' && options.token_type !== 'OAUTH_REFRESH') || options.system_type === 0x3) {
if ((options.token_type !== 'OAUTH_ACCESS' && options.token_type !== 'OAUTH_REFRESH') || options.system_type === 'API') {
// * Access and refresh tokens don't get a checksum due to size constraints
const checksum = crc32(dataBuffer);
@ -170,15 +171,17 @@ export function decryptToken(token: Buffer, key?: string): Buffer {
}
export function unpackToken(token: Buffer): Token {
const systemTypeNum = token.readUInt8(0x0);
const tokenTypeNum = token.readUInt8(0x1);
const systemType = getSystemTypeFromValue(systemTypeNum);
const tokenType = getTokenTypeFromValue(tokenTypeNum);
if (!tokenType) {
throw new Error('Invalid token type');
}
if (!systemType) throw new Error('Invalid system type');
if (!tokenType) throw new Error('Invalid token type');
const unpacked: Token = {
system_type: token.readUInt8(0x0),
system_type: systemType,
token_type: tokenType,
pid: token.readUInt32LE(0x2),
expire_time: token.readBigUInt64LE(0x6)
@ -271,7 +274,7 @@ export async function sendEmailConfirmedEmail(pnid: mongoose.HydratedDocument<IP
export async function sendForgotPasswordEmail(pnid: mongoose.HydratedDocument<IPNID, IPNIDMethods>): Promise<void> {
const tokenOptions: TokenOptions = {
system_type: 0xF, // * API
system_type: 'API',
token_type: 'PASSWORD_RESET',
pid: pnid.pid,
access_level: pnid.access_level,