mirror of
https://github.com/PretendoNetwork/account.git
synced 2026-04-24 23:11:05 -05:00
Added support for user information app on Wii U (from #77)
This commit is contained in:
parent
d4b975cbf7
commit
37fa5abd83
65
package-lock.json
generated
65
package-lock.json
generated
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
298
src/assets/user-info-settings/index.css
Normal file
298
src/assets/user-info-settings/index.css
Normal 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;
|
||||
}
|
||||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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
42182
src/services/nnas/regions.json
Normal file
File diff suppressed because it is too large
Load Diff
212
src/services/nnas/routes/account-settings.ts
Normal file
212
src/services/nnas/routes/account-settings.ts
Normal 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;
|
||||
|
|
@ -46,4 +46,5 @@ export interface Config {
|
|||
stripe?: {
|
||||
secret_key: string;
|
||||
};
|
||||
server_environment: string;
|
||||
}
|
||||
1
src/types/common/gender-types.ts
Normal file
1
src/types/common/gender-types.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export type GenderTypes = 'M' | 'F';
|
||||
11
src/types/services/nnas/account-settings.ts
Normal file
11
src/types/services/nnas/account-settings.ts
Normal 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;
|
||||
}
|
||||
5
src/types/services/nnas/region-languages.ts
Normal file
5
src/types/services/nnas/region-languages.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { RegionTimezones } from '@/types/services/nnas/region-timezones';
|
||||
|
||||
export interface RegionLanguages {
|
||||
[myKey: string]: RegionTimezones
|
||||
}
|
||||
9
src/types/services/nnas/region-timezones.ts
Normal file
9
src/types/services/nnas/region-timezones.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export interface RegionTimezone {
|
||||
area: string;
|
||||
language: string;
|
||||
name: string;
|
||||
utc_offset: string;
|
||||
order: string;
|
||||
}
|
||||
|
||||
export type RegionTimezones = RegionTimezone[];
|
||||
36
src/types/services/nnas/regions.ts
Normal file
36
src/types/services/nnas/regions.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
|
|
@ -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
146
src/views/index.ejs
Normal 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>
|
||||
Loading…
Reference in New Issue
Block a user