diff --git a/example.config.json b/example.config.json index 5159291..0aa30dd 100644 --- a/example.config.json +++ b/example.config.json @@ -1,10 +1,15 @@ { "http": { - "port": 80 + "port": 80, + "base_url": "http://localhost:80" }, "trello": { "api_key": "key", "api_token": "token", "board_name": "name" + }, + "discord": { + "client_id": "client_id", + "client_secret": "client_secret" } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 702fb21..7fcdb66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -378,6 +378,11 @@ "which": "^2.0.1" } }, + "data-uri-to-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", + "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==" + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -718,6 +723,14 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fetch-blob": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.2.tgz", + "integrity": "sha512-hunJbvy/6OLjCD0uuhLdp0mMPzP/yd2ssd1t2FCJsaA7wkWhpbp9xfuNVpv7Ll4jFhzp6T4LAupSiV9uOeg0VQ==", + "requires": { + "web-streams-polyfill": "^3.0.3" + } + }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -1168,6 +1181,15 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==" }, + "node-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.0.0.tgz", + "integrity": "sha512-bKMI+C7/T/SPU1lKnbQbwxptpCrG9ashG+VkytmXCPZyuM9jB6VU+hY0oi4lC8LxTtAeWdckNCTa3nrGsAdA3Q==", + "requires": { + "data-uri-to-buffer": "^3.0.1", + "fetch-blob": "^3.1.2" + } + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1668,6 +1690,11 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, + "web-streams-polyfill": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.1.1.tgz", + "integrity": "sha512-Czi3fG883e96T4DLEPRvufrF2ydhOOW1+1a6c3gNjH2aIh50DNFBdfwh2AKoOf1rXvpvavAoA11Qdq9+BKjE0Q==" + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index b4e066f..c2ac0ab 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "ioredis": "^4.26.0", "marked": "^3.0.4", "morgan": "^1.10.0", + "node-fetch": "^3.0.0", "redis-json": "^5.0.0", "trello": "^0.10.0" }, diff --git a/public/assets/css/account.css b/public/assets/css/account.css new file mode 100644 index 0000000..1f559d4 --- /dev/null +++ b/public/assets/css/account.css @@ -0,0 +1,226 @@ +.account-wrapper { + display: grid; + column-gap: 60px; + margin-top: 80px; + color: var(--text-secondary); +} + +/* Account settings navbar */ +.account-sidebar .user { + text-align: center; + margin: 55px auto; + width: fit-content; +} +.account-sidebar .user .miiname { + font-size: 1.3rem; + color: var(--text); + margin: 8px 0 4px; +} +.account-sidebar .user .username { + margin: 0px; +} +.account-sidebar .user .mii { + width: 300px; + height: 300px; + border-radius: 100%; + background: var(--btn-secondary); +} + +/* Settings */ +.settings-wrapper { + display: grid; + grid-column-start: 2; + grid-template-columns: 50%; + column-gap: 20px; +} +.settings-wrapper a { + color: #9d6ff3; + text-decoration: none; + font-weight: bold; +} +.settings-wrapper a:hover { + text-decoration: underline; +} +.settings-wrapper h2.section-header { + margin-top: 40px; + grid-column: 1 / 3; + color: var(--text); +} + +.setting-card { + display: grid; + grid-template-rows: 35px repeat(2, auto); + row-gap: 24px; + position: relative; + border-radius: 10px; + background: #2a2f50; + padding: 48px 60px; +} +.setting-card * { + margin: 0; +} +.setting-card .edit { + color: var(--text-secondary); + background: #383f6b; + border-radius: 100%; + position: absolute; + top: 42px; + right: 48px; + width: 24px; + height: 24px; + padding: 12px; +} +.setting-card .edit:hover { + background: #383f6b; + color: var(--text); +} + +.setting-card .header { + color: var(--text); +} + +.setting-card .setting-list { + display: grid; + grid-template-columns: repeat(2, auto); + gap: 24px; + list-style: none; + padding: 0; +} +.setting-card .setting-list p.label { + color: var(--text); + margin-bottom: 4px; +} + +fieldset { + height: min-content; + padding: 0; + border: none; +} + +.setting-card .server-selection { + display: flex; + border-radius: 5px; + overflow: hidden; + background: #383f6b; +} +.setting-card .server-selection input { + display: none; +} +.server-selection input + label { + display: flex; + flex-flow: column; + align-items: center; + flex: 50%; + color: var(--text-secondary); + padding: 40px; + justify-content: space-between; + cursor: pointer; +} +.server-selection input + label h2 { + margin-top: 12px; + color: var(--text-secondary); +} +.server-selection input:checked + label, +.server-selection input:checked + label h2 { + background: var(--theme); + color: var(--text); +} + +.setting-card.sign-in-history a button { + width: 100%; + padding: 12px 48px; + cursor: pointer; + background: #383f6b; +} +.setting-card input[type="checkbox"] { + appearance: none; + -webkit-appearance: none; + background: #383f6b; + padding: 12px; + margin: 4px; + margin-left: 0; + border-radius: 4px; + vertical-align: -65%; +} +.setting-card input[type="checkbox"]:checked { + background: no-repeat center/contain url(../images/check.svg), var(--theme); +} + +.setting-card.span-both-columns { + grid-column: 1 / span 2; +} + +.account-link-notice { + display: flex; + justify-content: center; + position: absolute; + top: -150px; + width: 100%; + animation: account-link-notice 5s; +} + +@keyframes account-link-notice { + 0% {top: -150px} + 20% {top: 35px} + 80% {top: 35px} + 100% {top: -150px} +} + +.account-link-notice div { + background: #37A985; + padding: 4px 36px; + border-radius: 5px; + z-index: 3; +} + +footer { + margin-top: 80px; +} + +@media screen and (max-width: 1500px) { + .account-wrapper { + margin: 20px 0; + } + + .settings-wrapper { + grid-column-start: 1; + } + + .account-sidebar { + margin: 0; + } + + .account-sidebar .user .mii { + width: 250px; + height: 250px; + } +} + +@media screen and (max-width: 1050px) { + .settings-wrapper { + display: block; + width: 100%; + } + + .setting-card { + margin-bottom: 24px; + } +} + +@media screen and (max-width: 550px) { + .setting-card { + padding: 24px; + } + .setting-card .edit { + top: 24px; + right: 24px; + } + + .setting-card .setting-list { + grid-template-columns: auto; + } + + .setting-card .server-selection { + flex-flow: column; + } +} \ No newline at end of file diff --git a/src/routers/account.js b/src/routers/account.js new file mode 100644 index 0000000..a695435 --- /dev/null +++ b/src/routers/account.js @@ -0,0 +1,185 @@ +const { Router } = require('express'); +const util = require('../util'); +const router = new Router(); +const fetch = (...args) => + import('node-fetch').then(({ default: fetch }) => fetch(...args)); +const config = require('../../config.json'); + +// A test PNID +const account = { + access_level: 0, // 0: standard, 1: tester, 2: mod?, 3. dev?, 4. god + server_access_level: 'prod', // prod, test, dev + username: 'PN_Monty', + password: 'thelegend27', + birthdate: '13/04/2004', // what format + gender: 'what even is this', // what format + country: 'Italy', // what format + language: 'JS smh', // what format + email: { + address: 'notascam@freecreditreport.com', + primary: true, // do we need to let the user change anything? + parent: true, + reachable: true, + validated: true, + }, + region: 'don\'t know the numbers', + timezone: { + name: 'Europe/Rome', + offset: 3600, // haven't checked if this is correct, just an assumption + }, + mii: { + name: 'Monty', + image_url: + 'https://studio.mii.nintendo.com/miis/image.png?data=00080f50595a606a6268696f757883969b9aa1a8b1b8b7bebdc5cccbd1d8620a121a181119111916222d3444484c4b&type=face&expression=normal&width=512', + }, + flags: { + marketing: true, + off_device: true, // Forgot what this does + }, + devices: [ + { + is_emulator: { + type: false, + }, + console_type: { + type: 'wup', + }, + device_attributes: { + created_date: '23/07/2021', // what format + name: 'Wii U', // ? + value: 'what', // what format + }, + }, + { + is_emulator: { + type: false, + }, + console_type: { + type: 'wup', + }, + device_attributes: { + created_date: '24/05/2020', + name: 'Windows', // ? + value: 'what', + }, + }, + ], + connections: { // This needs to be added to the schema + discord: { + id: '406125028065804289', + access_token: 'GiJ2Osi6LpYS7uLgZNyvCbRtpRopv1', // This only has the identify scope so eh, who cares if it gets leaked + expires_on: 604800, + refresh_token: 'VeQMa6zp2Rx77PjhiNFJbpKrpz2gX2', + scope: 'identify', + token_type: 'Bearer', + }, + }, +}; + +router.get('/', async (request, response) => { + const justLinked = request.cookies.justLinked; + const reqLocale = request.locale; + const locale = util.getLocale(reqLocale.region, reqLocale.language); + + let discordUser; + + if (account.connections.discord.access_token) { + // TODO: check if the token is valid, if not refresh it. + const discord = account.connections.discord; + const fetchDiscordUser = await fetch('https://discord.com/api/users/@me', { + method: 'get', + headers: { + 'Authorization': `${discord.token_type} ${discord.access_token}` + }, + }); + discordUser = await fetchDiscordUser.json(); + } + + // test user + discordUser = { + id: '406125028065804289', + username: 'montylion', + avatar: '5184b3dc388bdad1a93eb58694a58398', + discriminator: '3581', + public_flags: 64, + flags: 64, + banner: null, + banner_color: '#ff7081', + accent_color: 16740481, + locale: 'en-US', + mfa_enabled: true, + }; + + const isTester = () => { + return account.access_level > 0; + }; + + response.clearCookie('justLinked'); + + // TODO: make the sign in history only show the first 2/4 devices + + response.render('account', { + layout: 'main', + locale, + localeString: reqLocale.toString(), + account, + isTester, + discordUser, + justLinked + }); +}); + +router.get('/connect/discord', async (request, response) => { + //const reqLocale = request.locale; + //const locale = util.getLocale(reqLocale.region, reqLocale.language); + + const codeExchange = await fetch('https://discord.com/api/oauth2/token', { + method: 'post', + body: new URLSearchParams({ + client_id: config.discord.client_id, + client_secret: config.discord.client_secret, + grant_type: 'authorization_code', + code: request.query.code, + redirect_uri: `${config.http.base_url}/account/connect/discord`, + }), + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + }); + const token = await codeExchange.json(); + + /* + // Send the oauth tokens to the account server + fetch('account server endpoint', { + method: 'post', + body: { + access_token: token.access_token, + token_type: token.token_type, + expires_on: new Date().getTime() + token.expires_in, + refresh_token: token.refresh_token, + scope: token.scope, + }, + headers: { + 'Content-Type': 'application/json', + } + }); + */ + + /* Token refresh function (should be moved to util.js) + const refreshTokens = await fetch('https://discord.com/api/oauth2/token', { + method: 'post', + body: new URLSearchParams({ + client_id: config.discord.client_id, + client_secret: config.discord.client_secret, + grant_type: 'refresh_token', + refresh_token: account.connections.discord.refresh_token + }), + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + }); + const newTokens = await refreshTokens.json(); + console.log(newTokens); + */ + + // This sets a cookie to tell the account page to show the "Account linked successfully" notice, and redirects. + response.cookie('justLinked', 'discord').redirect('/account'); +}); + +module.exports = router; diff --git a/src/server.js b/src/server.js index 0105091..32f0362 100644 --- a/src/server.js +++ b/src/server.js @@ -23,6 +23,7 @@ const routers = { home: require('./routers/home'), faq: require('./routers/faq'), progress: require('./routers/progress'), + account: require('./routers/account'), blog: require('./routers/blog'), localization: require('./routers/localization') }; @@ -70,6 +71,7 @@ app.use(expressLocale({ app.use('/', routers.home); app.use('/faq', routers.faq); app.use('/progress', routers.progress); +app.use('/account', routers.account); app.use('/localization', routers.localization); app.use('/blog', routers.blog); diff --git a/views/account.handlebars b/views/account.handlebars new file mode 100644 index 0000000..c32f1a4 --- /dev/null +++ b/views/account.handlebars @@ -0,0 +1,152 @@ + + +
Nickname
+{{account.mii.name}}
+Birth date
+{{account.birthdate}}
+Gender
+{{account.gender}}
+Country/region
+{{account.country}}
+Timezone
+{{account.timezone.name}}
+Connected as {{ discordUser.username }}#{{ discordUser.discriminator }}.
+ {{else}} +{{ discordUser.username }}#{{ discordUser.discriminator }} doesn't appear to be a @Tester account. Check that you have the role on Discord. [should probably fix the wording]
+ {{/if}} + {{else}} +Beta servers are exclusive to testers. If you're a tester, connect your Discord account here.
+ {{/if}} + + +{{account.email.address}}
+Password
+●●●●●●●●
+After changing your password, you will be signed out from all devices.
+{{this.device_attributes.name}}
+{{this.device_attributes.created_date}}
+Account linked successfully.
+