Added support for user information app on Wii U (from #77)

This commit is contained in:
Jemma 2024-04-21 18:28:18 -05:00
parent d4b975cbf7
commit 37fa5abd83
16 changed files with 43004 additions and 11 deletions

65
package-lock.json generated
View File

@ -19,6 +19,7 @@
"crc": "^4.3.2",
"dicer": "^0.2.5",
"dotenv": "^16.0.3",
"ejs": "^3.1.10",
"email-validator": "^2.0.4",
"express": "^4.17.1",
"express-rate-limit": "^6.7.0",
@ -2651,8 +2652,7 @@
"node_modules/async": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz",
"integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==",
"dev": true
"integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g=="
},
"node_modules/asynckit": {
"version": "0.4.0",
@ -3018,7 +3018,6 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@ -3493,6 +3492,20 @@
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
"node_modules/ejs": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
"dependencies": {
"jake": "^10.8.5"
},
"bin": {
"ejs": "bin/cli.js"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/email-validator": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/email-validator/-/email-validator-2.0.4.tgz",
@ -4112,6 +4125,33 @@
"node": ">=6"
}
},
"node_modules/filelist": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
"dependencies": {
"minimatch": "^5.0.1"
}
},
"node_modules/filelist/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/filelist/node_modules/minimatch": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@ -4516,7 +4556,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"engines": {
"node": ">=8"
}
@ -4946,6 +4985,23 @@
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
"integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g=="
},
"node_modules/jake": {
"version": "10.8.7",
"resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz",
"integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==",
"dependencies": {
"async": "^3.2.3",
"chalk": "^4.0.2",
"filelist": "^1.0.4",
"minimatch": "^3.1.2"
},
"bin": {
"jake": "bin/cli.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/jmespath": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz",
@ -6663,7 +6719,6 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},

View File

@ -7,8 +7,9 @@
"lint": "npx eslint .",
"build": "npm run lint && npm run clean && npx tsc && npx tsc-alias && npm run copy-static",
"clean": "rimraf ./dist",
"copy-static": "npm run copy-assets && npm run copy-timezones",
"copy-static": "npm run copy-assets && npm run copy-timezones && npm run copy-views",
"copy-assets": "cp -r ./src/assets ./dist/assets",
"copy-views": "cp -r ./src/views ./dist/views",
"copy-timezones": "cp ./src/services/nnas/timezones.json ./dist/services/nnas/timezones.json",
"start": "node .",
"start:dev": "NODE_ENV=development node ."
@ -35,6 +36,7 @@
"crc": "^4.3.2",
"dicer": "^0.2.5",
"dotenv": "^16.0.3",
"ejs": "^3.1.10",
"email-validator": "^2.0.4",
"express": "^4.17.1",
"express-rate-limit": "^6.7.0",

View File

@ -0,0 +1,298 @@
html {
background: #FF0000;
}
body {
font-family: Poppins, Arial, Helvetica, sans-serif;
font-size: 15px;
width: 1280px;
margin: 0;
overflow: auto;
box-sizing: border-box;
background: #EAEAEA;
}
button:active,
.server-selection input:checked + label {
box-shadow: inset 0 0px 10px 0 rgba(66, 45, 120, 0.75) !important;
color: #9D6FF3;
}
header {
position: relative;
background: #37A985;
color: #FFF;
height: 70px;
line-height: 70px;
border-bottom: 2px solid #4F2E8C;
background: -webkit-gradient(linear, left top, left bottom, from(#9D6FF3), to(#673DB6));
z-index: 19;
}
header h1 {
margin: 0;
font-size: 30px;
text-align: center;
font-weight: normal;
}
button,
.button,
input[type="submit"] {
font-family: Poppins, Arial, Helvetica, sans-serif;
color: #45297A;
min-width: 200px;
box-sizing: content-box;
-webkit-box-sizing: content-box;
-webkit-box-align: center;
-webkit-box-pack: center;
min-height: 60px;
text-align: center;
font-size: 28px;
background: -webkit-gradient(linear, left top, left bottom, from(#FFF), color-stop(0.5, #FFF), color-stop(0.8, #F6F6F6), color-stop(0.95, #F5F5F5), to(#BBB))0 0;
border-radius: 12px;
cursor: pointer;
border: 0;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.4);
margin: 5px;
}
.body-content {
margin: 30px 60px 0 200px;
}
div.group h2 {
font-weight: normal;
}
div.group {
width: 485px;
display: inline-block;
margin: 0 5px;
}
div.body-content > h1 {
margin: 15px 0 0 0;
}
div.group > * {
margin: 5px 0;
font-size: 30px;
}
div.group > p.content,
div.group > input[type="text"],
div.group > select {
background: #FFF;
color: #6A6C75;
padding: 10px;
border-radius: 8px;
font-size: 25px;
box-shadow: none;
border: none;
width: 100%;
height: 58px;
box-sizing: border-box;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
text-indent: 1px;
text-overflow: '';
line-height: 38px;
user-select: none;
}
div.group > input[type="text"] {
color: #000;
}
div.group > select {
content: ' ';
background-color: #FFF;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' fill='%23673DB6' viewBox='0 0 256 256'%3E%3Cpath d='M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,53.66,90.34L128,164.69l74.34-74.35a8,8,0,0,1,11.32,11.32Z'%3E%3C/path%3E%3C/svg%3E");
background-size: 40px;
background-position: 440px center;
background-repeat: no-repeat;
color: #9D6FF3;
cursor: pointer;
}
div.group > span {
color: #000;
}
div.group > svg {
fill: #9D6FF3;
border: none;
height: 40px;
width: 40px;
box-sizing: border-box;
user-select: none;
padding: 0px;
margin-bottom: -10px;
margin-left: 5px;
}
.account-info {
float: left;
width: 200px;
box-sizing: content-box;
-webkit-box-sizing: content-box;
-webkit-box-align: center;
-webkit-box-pack: center;
}
.account-info > img {
display: inline-block;
width: 128px;
height: 128px;
overflow: hidden;
border-radius: 100%;
background: #E4DBF2;
margin: 30px 30px 0 35px;
box-shadow: 0-1px 2px rgba(0, 0, 0, 0.4);
}
.account-info > .content {
text-align: center;
color: #673DB6;
margin: 0;
}
.account-info > h3.content {
font-weight: normal;
color: #9D6FF3;
font-size: 20px;
}
.account-info .access-level-banned {
background: rgba(255, 63, 0, 0.3);
color: #FF3F00;
border-color: #FF3F00;
}
.account-info .tier-level-1 {
background: #934D4D;
color: #FF8484;
border-color: #FF8484;
}
.account-info .tier-level-2 {
background: #316C59;
color: #59C9A5;
border-color: #59C9A5;
}
.account-info .tier-level-3 {
background: #6B5E84;
color: #CAB1FB;
border-color: #CAB1FB;
}
.account-info .access-level-1 {
background: #3B918C;
color: #64F7EF;
border-color: #64F7EF;
}
.account-info .access-level-2 {
background: #917235;
color: #FFC759;
border-color: #FFC759;
}
.account-info .access-level-3 {
background: #3A973C;
color: #5AFF15;
border-color: #5AFF15;
}
.account-info .tier-name {
margin: 12px auto;
line-height: 1.2em;
border-radius: 1.2em;
border-width: 2px;
border-style: solid;
padding: 4px 16px;
width: min-content;
display: inline-block;
}
div.group > p.content > button {
min-width: 100px;
min-height: 20px;
display: block;
width: 100px;
float: right;
}
div.radio {
display: inline-block;
}
.server-selection input {
opacity: 0;
position: absolute;
width: 200px;
height: 70px;
cursor: pointer;
}
div.radio label {
display: inline-block;
color: #6A6C75;
min-width: 200px;
box-sizing: content-box;
-webkit-box-sizing: content-box;
-webkit-box-align: center;
-webkit-box-pack: center;
text-align: center;
background: -webkit-gradient(linear, left top, left bottom, from(#FFF), color-stop(0.5, #FFF), color-stop(0.8, #F6F6F6), color-stop(0.95, #F5F5F5), to(#BBB))0 0;
border-radius: 12px;
cursor: pointer;
border: 0;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.4);
margin: 5px;
font-weight: normal;
font-size: 15px;
}
div.radio label h2 {
display: inline-block;
font-weight: normal;
}
div.radio label svg {
margin: -6px 0;
}
header .fixed-bottom-button.left {
padding: 0 60px 0 40px !important;
}
.fixed-bottom-button.left {
right: auto;
left: 0;
border-radius: 0 40px 0 0;
}
header .fixed-bottom-button {
min-width: 120px;
padding: 0 40px 0 60px;
}
.fixed-bottom-button,
input[type="submit"] {
position: fixed;
bottom: 0;
right: 0;
height: 85px;
line-height: 100px;
padding: 0 40px 0 60px;
background: -webkit-gradient(linear, left top, left bottom, from(#FFF), color-stop(0.5, #FFF), to(#E6E6E6))0 0;
font-size: 28px;
z-index: 20;
border-radius: 40px 0 0 0;
margin: 0;
border: none;
}

View File

@ -70,7 +70,8 @@ export const config: Config = {
api: process.env.PN_ACT_CONFIG_GRPC_MASTER_API_KEY_API || '',
},
port: Number(process.env.PN_ACT_CONFIG_GRPC_PORT || ''),
}
},
server_environment: process.env.PN_ACT_CONFIG_SERVER_ENVIRONMENT || ''
};
if (process.env.PN_ACT_CONFIG_STRIPE_SECRET_KEY) {
@ -148,6 +149,11 @@ if (!config.s3.secret) {
disabledFeatures.s3 = true;
}
if (!config.server_environment) {
LOG_WARN('Failed to find server environment. To change the environment, set the PN_ACT_CONFIG_SERVER_ENVIRONMENT environment variable');
config.server_environment = 'prod';
}
if (disabledFeatures.s3) {
if (!config.cdn.subdomain) {
LOG_ERROR('s3 file storage is disabled and no CDN subdomain was set. Set the PN_ACT_CONFIG_CDN_SUBDOMAIN environment variable');

View File

@ -28,6 +28,8 @@ import { config } from '@/config-manager';
const app = express();
// * START APPLICATION
app.set('view engine', 'ejs');
app.set('views', __dirname + '/views');
// * Create router
LOG_INFO('Setting up Middleware');

View File

@ -1,5 +1,6 @@
// * handles "account.nintendo.net" endpoints
import path from 'node:path';
import express from 'express';
import subdomain from 'express-subdomain';
import clientHeaderCheck from '@/middleware/client-header';
@ -15,17 +16,39 @@ 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';
import settings from '@/services/nnas/routes/account-settings';
// * Router to handle the subdomain restriction
const nnas = express.Router();
// Static routes for the user information app
async function setCSSHeader(request: express.Request, response: express.Response, next: express.NextFunction): Promise<void> {
response.set('Content-Type', 'text/css');
return next();
}
async function setJSHeader(request: express.Request, response: express.Response, next: express.NextFunction): Promise<void> {
response.set('Content-Type', 'text/javascript');
return next();
}
async function setIMGHeader(request: express.Request, response: express.Response, next: express.NextFunction): Promise<void> {
response.set('Content-Type', 'image/png');
return next();
}
// * Setup routes
LOG_INFO('[NNAS] Applying imported routes');
nnas.use('/v1/account-settings/', settings);
nnas.use('/v1/account-settings/css/', setCSSHeader, express.static(path.join(__dirname, '../../assets/user-info-settings')));
nnas.use('/v1/account-settings/js/', setJSHeader, express.static(path.join(__dirname, '../../assets/user-info-settings')));
nnas.use('/v1/account-settings/img/', setIMGHeader, express.static(path.join(__dirname, '../../assets/user-info-settings')));
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);

42182
src/services/nnas/regions.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,212 @@
import crypto from 'node:crypto';
import express from 'express';
import got from 'got';
import { z } from 'zod';
import { getServerByClientID, getPNIDByPID } from '@/database';
import { LOG_ERROR } from '@/logger';
import { decryptToken, unpackToken, getValueFromHeaders, sendConfirmationEmail } from '@/util';
import { config } from '@/config-manager';
import { HydratedServerDocument } from '@/types/mongoose/server';
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
import { AccountSettings } from '@/types/services/nnas/account-settings';
import { Token } from '@/types/common/token';
import { RegionLanguages } from '@/types/services/nnas/region-languages';
import { RegionTimezone, RegionTimezones } from '@/types/services/nnas/region-timezones';
import { Country, Region } from '@/types/services/nnas/regions';
import timezones from '@/services/nnas/timezones.json';
import regionsList from '@/services/nnas/regions.json';
const router: express.Router = express.Router();
const accountSettingsSchema = z.object({
gender: z.enum(['M', 'F']),
tz_name: z.string(),
region: z.coerce.number(),
email: z.string().email(),
server_selection: z.enum(['prod', 'test', 'dev']),
marketing_flag: z.enum(['true', 'false']).transform((value) => value === 'true'),
off_device_flag: z.enum(['true', 'false']).transform((value) => value === 'true'),
});
/**
* [GET]
* Replacement for: https://account.nintendo.net/v1/account-settings/ui/profile
* Description: Serves the Nintendo Network ID Settings page for the Wii U
*/
router.get('/ui/profile', async function (request: express.Request, response: express.Response): Promise<void> {
const server: HydratedServerDocument | null = await getServerByClientID('3f3928cc6f780638d360f0485cef973f', config.server_environment);
const token: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-service-token');
if (!server || !token) {
response.sendStatus(504);
return;
}
const aes_key: string = server?.aes_key;
const decryptedToken: Buffer = decryptToken(Buffer.from(token, 'base64'), aes_key);
const tokenContents: Token = unpackToken(decryptedToken);
try {
const PNID: HydratedPNIDDocument | null = await getPNIDByPID(tokenContents.pid);
if (!PNID) {
response.sendStatus(504);
return;
}
const countryCode: string = PNID.country;
const language: string = PNID.language;
const regionLanguages: RegionLanguages = timezones[countryCode as keyof typeof timezones];
const regionTimezones: RegionTimezones = regionLanguages[language] ? regionLanguages[language] : Object.values(regionLanguages)[0];
const region: Country | undefined = regionsList.find((region) => region.iso_code === countryCode);
const miiFaces = ['normal_face', 'smile_open_mouth', 'sorrow', 'surprise_open_mouth', 'wink_left', 'frustrated'];
const face = miiFaces[crypto.randomInt(5)];
const notice: string | undefined = request.query.notice ? request.query.notice.toString() : undefined;
const accountLevel: string[] = ['Standard', 'Tester', 'Moderator', 'Developer'];
response.render('index.ejs', {
PNID,
regionTimezones,
face,
notice,
accountLevel,
regions: region ? region.regions: []
});
}
catch (error: any) {
LOG_ERROR(error);
response.sendStatus(504);
return;
}
});
/**
* [GET]
* Description: Fetches the requested mii image from the CDN and send it to the client.
* This is required because of the strict domain whitelist in the account settings app
* on the Wii U.
*/
router.get('/mii/:pid/:face', async function (request: express.Request, response: express.Response): Promise<void> {
if (!config.cdn.base_url) {
response.sendStatus(404);
return;
}
const miiImage: Buffer = await got(`${config.cdn.base_url}/mii/${request.params.pid}/${request.params.face}.png`).buffer();
response.set('Content-Type', 'image/png');
response.send(miiImage);
});
/**
* [POST]
* Description: Endpoint to update the PNID from the account settings app on the Wii
*/
router.post('/update', async function (request: express.Request, response: express.Response): Promise<void> {
const server: HydratedServerDocument | null = await getServerByClientID('3f3928cc6f780638d360f0485cef973f', config.server_environment);
const token: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-service-token');
if (!server || !token) {
response.sendStatus(504);
return;
}
const aesKey: string = server?.aes_key;
const decryptedToken: Buffer = decryptToken(Buffer.from(token, 'base64'), aesKey);
const tokenContents: Token = unpackToken(decryptedToken);
try {
const pnid: HydratedPNIDDocument | null = await getPNIDByPID(tokenContents.pid);
const personBody: AccountSettings = request.body;
if (!pnid) {
response.status(404);
response.redirect('/v1/account-settings/ui/profile');
return;
}
const person = accountSettingsSchema.safeParse(personBody);
if (!person.success) {
response.status(404);
response.redirect('/v1/account-settings/ui/profile');
return;
}
const timezoneName: string = (person.data.tz_name && !!Object.keys(person.data.tz_name).length) ? person.data.tz_name : pnid.timezone.name;
const regionLanguages: RegionLanguages = timezones[pnid.country as keyof typeof timezones];
const regionTimezones: RegionTimezones = regionLanguages[pnid.language] ? regionLanguages[pnid.language] : Object.values(regionLanguages)[0];
const timezone: RegionTimezone | undefined = regionTimezones.find(tz => tz.area === timezoneName);
const country: Country | undefined = regionsList.find((region) => region.iso_code === pnid.country);
let notice = '';
if (!country) {
response.status(404);
response.redirect('/v1/account-settings/ui/profile');
return;
}
const regionObject: Region | undefined = country.regions.find((region) => region.id === person.data.region);
const region: number = regionObject ? regionObject.id : pnid.region;
if (!timezone) {
response.status(404);
response.redirect('/v1/account-settings/ui/profile');
return;
}
pnid.gender = person.data.gender;
pnid.region = region;
pnid.timezone.name = timezoneName;
pnid.timezone.offset = Number(timezone.utc_offset);
pnid.flags.marketing = person.data.marketing_flag;
pnid.flags.off_device = person.data.off_device_flag;
if (person.data.server_selection) {
const environment: string = person.data.server_selection;
if (environment === 'test' && pnid.access_level < 1) {
response.status(400);
notice = 'Do not have permission to enter this environment';
response.redirect(`/v1/account-settings/ui/profile?notice=${notice}`);
return;
}
if (environment === 'dev' && pnid.access_level < 3) {
response.status(400);
notice = 'Do not have permission to enter this environment';
response.redirect(`/v1/account-settings/ui/profile?notice=${notice}`);
return;
}
pnid.server_access_level = environment;
}
if (person.data.email.trim().toLowerCase() !== pnid.email.address) {
// TODO - Better email check
pnid.email.address = person.data.email.trim().toLowerCase();
pnid.email.reachable = false;
pnid.email.validated = false;
pnid.email.validated_date = '';
pnid.email.id = crypto.randomBytes(4).readUInt32LE();
await pnid.generateEmailValidationCode();
await pnid.generateEmailValidationToken();
await sendConfirmationEmail(pnid);
notice = 'A confirmation email has been sent to your inbox.';
}
await pnid.save();
response.redirect(`/v1/account-settings/ui/profile?notice=${notice}`);
} catch (error: any) {
LOG_ERROR(error);
response.sendStatus(504);
return;
}
});
export default router;

View File

@ -46,4 +46,5 @@ export interface Config {
stripe?: {
secret_key: string;
};
server_environment: string;
}

View File

@ -0,0 +1 @@
export type GenderTypes = 'M' | 'F';

View File

@ -0,0 +1,11 @@
import { GenderTypes } from '@/types/common/gender-types';
export interface AccountSettings {
gender: GenderTypes;
tz_name: string;
region: number;
email: string;
server_selection: string;
marketing_flag: boolean;
off_device_flag: boolean;
}

View File

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

View File

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

View File

@ -0,0 +1,36 @@
export interface Country {
id: number;
iso_code: string;
name: string;
translations: Translations;
regions: Region[];
}
export interface Translations {
japanese: string;
english: string;
french: string;
german: string;
italian: string;
spanish: string;
chinese_simple: string;
korean: string;
dutch: string;
portuguese: string;
russian: string;
chinese_traditional: string;
unknown1: string;
unknown2: string;
unknown3: string;
unknown4: string;
}
export interface Region {
id: number;
name: string;
translations: Translations;
coordinates: {
latitude: number;
longitude: number;
};
}

View File

@ -95,7 +95,7 @@ export function generateToken(key: string, options: TokenOptions): Buffer | null
return final;
}
export function decryptToken(token: Buffer): Buffer {
export function decryptToken(token: Buffer, key?: string): Buffer {
let encryptedBody: Buffer;
let expectedChecksum = 0;
@ -107,8 +107,12 @@ export function decryptToken(token: Buffer): Buffer {
encryptedBody = token.subarray(4);
}
if (!key) {
key = config.aes_key;
}
const iv = Buffer.alloc(16);
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(config.aes_key, 'hex'), iv);
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key, 'hex'), iv);
const decrypted = Buffer.concat([
decipher.update(encryptedBody),

146
src/views/index.ejs Normal file
View File

@ -0,0 +1,146 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Account Settings</title>
<link rel="stylesheet" type="text/css" href="/v1/account-settings/css/index.css">
</head>
<body>
<form action=" /v1/account-settings/update" method="post">
<header>
<h1>Pretendo Network ID Settings</h1>
<button id="close" class="fixed-bottom-button left" onclick="wiiuBrowser.jumpToBaristaAccount();">Close</button>
<input id="save" class="fixed-bottom-button" type="submit" value="Submit">
</header>
<div class="account-info">
<img src="/v1/account-settings/mii/<%= PNID.pid %>/<%= face %>">
<h2 class="content"><%= PNID.mii.name %></h2>
<h3 class="content"><%= PNID.username %></h3>
<h3 class="content">
<% if (PNID.access_level === -1) { %>
<p class="tier-name access-level-banned">Banned</p>
<% } else if (PNID.connections.stripe.tier_level) { %>
<p class="tier-name tier-level-<%= PNID.connections.stripe.tier_level %>"><%= PNID.connections.stripe.tier_name %></p>
<% } else { %>
<p class="tier-name access-level-<%= PNID.access_level %>"><%= accountLevel[PNID.access_level] %></p>
<% } %>
</h3>
</div>
<div class="body-content">
<h1>User Settings</h1>
<div class="group">
<h2>Birth Date</h2>
<p class="content"><%= PNID.birthdate %></p>
</div>
<div class="group">
<h2>Gender</h2>
<select name="gender" id="gender">
<option value="M" <% if (PNID.gender === 'M') { %>selected<% } %>>Male</option>
<option value="F" <% if (PNID.gender === 'F') { %>selected<% } %>>Female</option>
</select>
</div>
<div class="group">
<h2>Country</h2>
<p class="content" id="country"><%= PNID.country %></p>
</div>
<div class="group">
<h2>Region</h2>
<select name="region" id="region">
<% for (let region of regions) { %>
<option value="<%= region.id %>" <% if (PNID.region === region.id) { %>selected<% } %>><%= region.name %></option>
<% } %>
</select>
</div>
<div class="group">
<h2>Timezone</h2>
<select name="tz_name" id="gender">
<% for (let timezone of regionTimezones) { %>
<option value="<%= timezone.area %>" <% if (PNID.timezone.name === timezone.area) { %>selected<% } %>><%= timezone.area %></option>
<% } %>
</select>
</div>
<h1>Email</h1>
<div class="group">
<input name="email" type="text" class="content" value="<%= PNID.email.address %>"></input>
</div>
<div class="group">
<% if(PNID.email.validated) { %>
<span>Verified</span>
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 256 256"><path d="M173.66,98.34a8,8,0,0,1,0,11.32l-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35A8,8,0,0,1,173.66,98.34ZM232,128A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z"></path></svg>
<% } else { %>
<span>Email Not Verified</span>
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 256 256"><path d="M165.66,101.66,139.31,128l26.35,26.34a8,8,0,0,1-11.32,11.32L128,139.31l-26.34,26.35a8,8,0,0,1-11.32-11.32L116.69,128,90.34,101.66a8,8,0,0,1,11.32-11.32L128,116.69l26.34-26.35a8,8,0,0,1,11.32,11.32ZM232,128A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z"></path></svg>
<% } %>
</div>
<h1>Other Settings</h1>
<div class="group">
<h2>Server Environment</h2>
<div class="radio server-selection" id="server">
<input type="radio" id="prod" name="server_selection" value="prod" <% if (PNID.server_access_level === 'prod') { %>checked=""<% } %>>
<label for="prod">
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
<polyline points="7.5 4.21 12 6.81 16.5 4.21"></polyline>
<polyline points="7.5 19.79 7.5 14.6 3 12"></polyline>
<polyline points="21 12 16.5 14.6 16.5 19.79"></polyline>
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
<line x1="12" y1="22.08" x2="12" y2="12"></line>
</svg>
<h2>Production</h2>
</label>
<% if(PNID.access_level >= 1) { %>
<input type="radio" id="beta" name="server_selection" value="test" <% if (PNID.server_access_level === 'test') { %> checked="" <% } %> <% if(PNID.access_level < 1) { %> disabled <% } %> >
<label for="beta">
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="2,21 22,21 14,11.5 14,5 10,3 10,11.5"></polygon>
</svg>
<h2>Beta</h2>
</label>
<% } %>
<% if(PNID.access_level === 3) { %>
<input type="radio" id="dev" name="server_selection" value="dev" <% if (PNID.server_access_level === 'dev') { %> checked="" <% } %> <% if(PNID.access_level < 3) { %> disabled <% } %> >
<label for="dev">
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="2,21 22,21 14,11.5 14,5 10,3 10,11.5"></polygon>
</svg>
<h2>Dev</h2>
</label>
<% } %>
</div>
</div>
<div class="group">
<h2>Email Notifications</h2>
<div class="radio server-selection" id="server">
<input type="radio" id="true" name="marketing_flag" value="true" <% if (PNID.flags.marketing) { %>checked=""<% } %>>
<label for="marketing_flag">
<h2>Receive</h2>
</label>
<input type="radio" id="false" name="marketing_flag" value="false" <% if (!PNID.flags.marketing) { %>checked=""<% } %>>
<label for="marketing_flag">
<h2>Do Not Receive</h2>
</label>
</div>
</div>
<div class="group">
<h2>Non-Nintendo Device Setting</h2>
<div class="radio server-selection" id="server">
<input type="radio" id="true" name="off_device_flag" value="true" <% if (PNID.flags.off_device) { %>checked=""<% } %>>
<label for="off_device_flag">
<h2>Allow</h2>
</label>
<input type="radio" id="false" name="off_device_flag" value="false" <% if (!PNID.flags.off_device) { %>checked=""<% } %>>
<label for="off_device_flag">
<h2>Deny</h2>
</label>
</div>
</div>
</div>
</form>
<% if (notice) { %>
<script>
alert('<%= notice %>');
</script>
<% } %>
</body>
</html>