mirror of
https://github.com/PretendoNetwork/website.git
synced 2026-04-24 15:37:12 -05:00
commit
0e78b7f922
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"commonjs": true,
|
||||
"es6": true
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: "Test"
|
||||
author: "Monty"
|
||||
author: "monty"
|
||||
author_image: "https://www.github.com/ashmonty.png"
|
||||
date: "January 20, 2038"
|
||||
caption: "A post to test the styling of the various elements we might use (rename to _test.md before deploying the blog section)"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@
|
|||
},
|
||||
"discord": {
|
||||
"client_id": "client_id",
|
||||
"client_secret": "client_secret"
|
||||
"client_secret": "client_secret",
|
||||
"guild_id": "Guild ID",
|
||||
"bot_token": "token",
|
||||
"tester_roles": [
|
||||
"role id"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -107,7 +107,7 @@
|
|||
"github": "https://github.com/jipfr"
|
||||
},
|
||||
{
|
||||
"name": "Monty",
|
||||
"name": "monty",
|
||||
"caption": "مطور الويب",
|
||||
"picture": "https://github.com/ashmonty.png",
|
||||
"github": "https://github.com/ashmonty"
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@
|
|||
"github": "https://github.com/jipfr"
|
||||
},
|
||||
{
|
||||
"name": "Monty",
|
||||
"name": "monty",
|
||||
"caption": "Desenvolvimento web",
|
||||
"picture": "https://github.com/ashmonty.png",
|
||||
"github": "https://github.com/ashmonty"
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@
|
|||
"github": "https://github.com/jipfr"
|
||||
},
|
||||
{
|
||||
"name": "Monty",
|
||||
"name": "monty",
|
||||
"caption": "Webentwicklung",
|
||||
"picture": "https://github.com/ashmonty.png",
|
||||
"github": "https://github.com/ashmonty"
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@
|
|||
"github": "https://github.com/jipfr"
|
||||
},
|
||||
{
|
||||
"name": "Monty",
|
||||
"name": "monty",
|
||||
"caption": "Desarrollo web",
|
||||
"picture": "https://github.com/ashmonty.png",
|
||||
"github": "https://github.com/ashmonty"
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@
|
|||
"github": "https://github.com/jipfr"
|
||||
},
|
||||
{
|
||||
"name": "Monty",
|
||||
"name": "monty",
|
||||
"caption": "Développement Web",
|
||||
"picture": "https://github.com/ashmonty.png",
|
||||
"github": "https://github.com/ashmonty"
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@
|
|||
"github": "https://github.com/jipfr"
|
||||
},
|
||||
{
|
||||
"name": "Monty",
|
||||
"name": "monty",
|
||||
"caption": "Sviluppo web",
|
||||
"picture": "https://github.com/ashmonty.png",
|
||||
"github": "https://github.com/ashmonty"
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@
|
|||
"github": "https://github.com/jipfr"
|
||||
},
|
||||
{
|
||||
"name": "Monty",
|
||||
"name": "monty",
|
||||
"caption": "웹 개발",
|
||||
"picture": "https://github.com/ashmonty.png",
|
||||
"github": "https://github.com/ashmonty"
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@
|
|||
"github": "https://github.com/jipfr"
|
||||
},
|
||||
{
|
||||
"name": "Monty",
|
||||
"name": "monty",
|
||||
"caption": "Dezvoltare web",
|
||||
"picture": "https://github.com/ashmonty.png",
|
||||
"github": "https://github.com/ashmonty"
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@
|
|||
"github": "https://github.com/jipfr"
|
||||
},
|
||||
{
|
||||
"name": "Monty",
|
||||
"name": "monty",
|
||||
"caption": "Web-разработка",
|
||||
"picture": "https://github.com/ashmonty.png",
|
||||
"github": "https://github.com/ashmonty"
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@
|
|||
"github": "https://github.com/jipfr"
|
||||
},
|
||||
{
|
||||
"name": "Monty",
|
||||
"name": "monty",
|
||||
"caption": "Web sayfası geliştirme",
|
||||
"picture": "https://github.com/ashmonty.png",
|
||||
"github": "https://github.com/ashmonty"
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@
|
|||
"github": "https://github.com/jipfr"
|
||||
},
|
||||
{
|
||||
"name": "Monty",
|
||||
"name": "monty",
|
||||
"caption": "Web development",
|
||||
"picture": "https://github.com/ashmonty.png",
|
||||
"github": "https://github.com/ashmonty"
|
||||
|
|
|
|||
2602
package-lock.json
generated
2602
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -19,18 +19,16 @@
|
|||
"dependencies": {
|
||||
"colors": "^1.4.0",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"discord-oauth2": "github:ryanblenis/discord-oauth2",
|
||||
"express": "^4.17.1",
|
||||
"express-handlebars": "^4.0.4",
|
||||
"express-locale": "^2.0.0",
|
||||
"fs-extra": "^9.1.0",
|
||||
"got": "^11.8.2",
|
||||
"gray-matter": "^4.0.3",
|
||||
"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"
|
||||
"trello": "^0.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^7.32.0"
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@
|
|||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Account settings navbar */
|
||||
/* Account settings sidebar */
|
||||
.account-sidebar .user {
|
||||
text-align: center;
|
||||
margin: 55px auto;
|
||||
width: fit-content;
|
||||
}
|
||||
.account-sidebar .user .miiname {
|
||||
font-size: 1.3rem;
|
||||
font-size: 1.2rem;
|
||||
color: var(--text);
|
||||
margin: 8px 0 4px;
|
||||
}
|
||||
|
|
@ -20,8 +20,8 @@
|
|||
margin: 0px;
|
||||
}
|
||||
.account-sidebar .user .mii {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
border-radius: 100%;
|
||||
background: var(--btn-secondary);
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
.settings-wrapper {
|
||||
display: grid;
|
||||
grid-column-start: 2;
|
||||
grid-template-columns: 50%;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
column-gap: 20px;
|
||||
}
|
||||
.settings-wrapper a {
|
||||
|
|
@ -126,6 +126,13 @@ fieldset {
|
|||
color: var(--text);
|
||||
}
|
||||
|
||||
.setting-card #remove-discord-connection {
|
||||
width: 100%;
|
||||
padding: 12px 48px;
|
||||
cursor: pointer;
|
||||
background: #383f6b;
|
||||
}
|
||||
|
||||
.setting-card.sign-in-history a button {
|
||||
width: 100%;
|
||||
padding: 12px 48px;
|
||||
|
|
@ -150,34 +157,37 @@ fieldset {
|
|||
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 {
|
||||
@keyframes banner-notice {
|
||||
0% {top: -150px}
|
||||
20% {top: 35px}
|
||||
80% {top: 35px}
|
||||
100% {top: -150px}
|
||||
}
|
||||
|
||||
.account-link-notice div {
|
||||
background: #37A985;
|
||||
.banner-notice {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
top: -150px;
|
||||
width: 100%;
|
||||
animation: banner-notice 5s;
|
||||
}
|
||||
.banner-notice div {
|
||||
padding: 4px 36px;
|
||||
border-radius: 5px;
|
||||
z-index: 3;
|
||||
}
|
||||
.banner-notice.success div {
|
||||
background: #37A985;
|
||||
}
|
||||
.banner-notice.error div {
|
||||
background: #A9375B;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 80px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1500px) {
|
||||
@media screen and (max-width: 1300px) {
|
||||
.account-wrapper {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
|
@ -191,12 +201,12 @@ footer {
|
|||
}
|
||||
|
||||
.account-sidebar .user .mii {
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1050px) {
|
||||
@media screen and (max-width: 1000px) {
|
||||
.settings-wrapper {
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
|
@ -209,18 +219,28 @@ footer {
|
|||
|
||||
@media screen and (max-width: 550px) {
|
||||
.setting-card {
|
||||
padding: 24px;
|
||||
padding: 24px;
|
||||
width: calc(100vw - 48px);
|
||||
margin-left: -5vw;
|
||||
margin-right: -2.5vw;
|
||||
border-radius: 0;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.setting-card .edit {
|
||||
top: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
|
||||
.setting-card .setting-list {
|
||||
grid-template-columns: auto;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
transform: scale(0.85);
|
||||
}
|
||||
|
||||
.setting-card .server-selection {
|
||||
flex-flow: column;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 350px) {
|
||||
.setting-card .setting-list {
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
}
|
||||
108
public/assets/css/login.css
Normal file
108
public/assets/css/login.css
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
.wrapper {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.login-wrapper {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
form.login {
|
||||
padding: 48px;
|
||||
background-color: #151934;
|
||||
border-radius: 12px;
|
||||
max-width: calc(90vw - 92px);
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
form.login input {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
display: block;
|
||||
border: 0;
|
||||
font-family: Poppins, Arial, Helvetica, sans-serif;
|
||||
font-size: 1rem;
|
||||
background-color: var(--btn-secondary);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
color: var(--text);
|
||||
width: calc(100% - 24px);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
form.login input::placeholder {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
form.login input:focus {
|
||||
background-color: #fff;
|
||||
color: var(--btn-secondary);
|
||||
transition: 200ms;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
form.login button {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
display: block;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
font-family: Poppins, Arial, Helvetica, sans-serif;
|
||||
font-size: 1rem;
|
||||
color: var(--text);
|
||||
padding: 12px;
|
||||
background: var(--btn);
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
form.login a.pwdreset {
|
||||
display: block;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
text-align: right;
|
||||
margin: 6px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
form.login a.pwdreset:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
form.login .register {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
form.login .register button {
|
||||
background: var(--btn-secondary);
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 23px;
|
||||
}
|
||||
|
||||
@keyframes banner-notice {
|
||||
0% {top: -150px}
|
||||
20% {top: 35px}
|
||||
80% {top: 35px}
|
||||
100% {top: -150px}
|
||||
}
|
||||
.banner-notice {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
top: -150px;
|
||||
width: 100%;
|
||||
animation: banner-notice 5s;
|
||||
}
|
||||
.banner-notice div {
|
||||
padding: 4px 36px;
|
||||
border-radius: 5px;
|
||||
z-index: 3;
|
||||
}
|
||||
.banner-notice.error div {
|
||||
background: #A9375B;
|
||||
}
|
||||
22
public/assets/js/account.js
Normal file
22
public/assets/js/account.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
|
||||
document.getElementById('remove-discord-connection').addEventListener('click', () => {
|
||||
// TODO: Refresh access token if expired (move this to the backend maybe?)
|
||||
|
||||
const tokenType = document.cookie.split('; ').find(row => row.startsWith('token_type=')).split('=')[1];
|
||||
const accessToken = document.cookie.split('; ').find(row => row.startsWith('access_token=')).split('=')[1];
|
||||
|
||||
fetch('https://api.pretendo.cc/v1/connections/remove/discord', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `${tokenType} ${decodeURIComponent(accessToken)}`
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(({ status }) => {
|
||||
if (status === 200) {
|
||||
location.reload();
|
||||
}
|
||||
})
|
||||
.catch(console.log);
|
||||
});
|
||||
|
|
@ -1,185 +1,284 @@
|
|||
const { Router } = require('express');
|
||||
const crypto = require('crypto');
|
||||
const DiscordOauth2 = require('discord-oauth2');
|
||||
const util = require('../util');
|
||||
const router = new Router();
|
||||
const fetch = (...args) =>
|
||||
import('node-fetch').then(({ default: fetch }) => fetch(...args));
|
||||
const config = require('../../config.json');
|
||||
const router = new Router();
|
||||
|
||||
// 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',
|
||||
},
|
||||
},
|
||||
};
|
||||
// Create OAuth client
|
||||
const discordOAuth = new DiscordOauth2({
|
||||
clientId: config.discord.client_id,
|
||||
clientSecret: config.discord.client_secret,
|
||||
redirectUri: `${config.http.base_url}/account/connect/discord`,
|
||||
version: 'v9'
|
||||
});
|
||||
|
||||
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();
|
||||
// Verify the user is logged in
|
||||
if (!request.cookies.access_token || !request.cookies.refresh_token) {
|
||||
return response.redirect('/account/login');
|
||||
}
|
||||
|
||||
// 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', {
|
||||
// Setup the data to be sent to the handlebars renderer
|
||||
const renderData = {
|
||||
layout: 'main',
|
||||
locale,
|
||||
localeString: reqLocale.toString(),
|
||||
account,
|
||||
isTester,
|
||||
discordUser,
|
||||
justLinked
|
||||
locale: util.getLocale(request.locale.region, request.locale.language),
|
||||
localeString: request.locale.toString(),
|
||||
linked: request.cookies.linked,
|
||||
error: request.cookies.error
|
||||
};
|
||||
|
||||
// Reset message cookies
|
||||
response.clearCookie('linked');
|
||||
response.clearCookie('error');
|
||||
|
||||
// Attempt to get user data
|
||||
let apiResponse = await util.apiGetRequest('/v1/user', {
|
||||
'Authorization': `${request.cookies.token_type} ${request.cookies.access_token}`
|
||||
});
|
||||
|
||||
if (apiResponse.statusCode !== 200) {
|
||||
// Assume expired, refresh and retry request
|
||||
apiResponse = await util.apiPostGetRequest('/v1/login', {}, {
|
||||
refresh_token: request.cookies.refresh_token,
|
||||
grant_type: 'refresh_token'
|
||||
});
|
||||
|
||||
if (apiResponse.statusCode !== 200) {
|
||||
// TODO: Error message
|
||||
return response.status(apiResponse.statusCode).json({
|
||||
error: 'Bad'
|
||||
});
|
||||
}
|
||||
|
||||
const tokens = apiResponse.body;
|
||||
|
||||
response.cookie('refresh_token', tokens.refresh_token, { domain : '.pretendo.network' });
|
||||
response.cookie('access_token', tokens.access_token, { domain : '.pretendo.network' });
|
||||
response.cookie('token_type', tokens.token_type, { domain : '.pretendo.network' });
|
||||
|
||||
apiResponse = await util.apiGetRequest('/v1/user', {
|
||||
'Authorization': `${tokens.token_type} ${tokens.access_token}`
|
||||
});
|
||||
}
|
||||
|
||||
// If still failed, something went horribly wrong
|
||||
if (apiResponse.statusCode !== 200) {
|
||||
// TODO: Error message
|
||||
return response.status(apiResponse.statusCode).json({
|
||||
error: 'Bad'
|
||||
});
|
||||
}
|
||||
|
||||
// Set user account info to render data
|
||||
const account = apiResponse.body;
|
||||
|
||||
renderData.account = account;
|
||||
renderData.isTester = account.access_level > 0;
|
||||
|
||||
// Check if a Discord account is linked to the PNID
|
||||
if (account.connections.discord.id && account.connections.discord.id.trim() !== '') {
|
||||
// If Discord account is linked, then get user info
|
||||
try {
|
||||
renderData.discordUser = await discordOAuth.getUser(account.connections.discord.access_token);
|
||||
} catch (error) {
|
||||
// Assume expired, refresh and retry Discord request
|
||||
let tokens;
|
||||
try {
|
||||
tokens = await discordOAuth.tokenRequest({
|
||||
scope: 'identify guilds',
|
||||
grantType: 'refresh_token',
|
||||
refreshToken: account.connections.discord.refresh_token,
|
||||
});
|
||||
} catch (error) {
|
||||
renderData.error = 'Invalid Discord refresh token. Remove account and relink';
|
||||
response.render('account', renderData);
|
||||
}
|
||||
|
||||
// TODO: Add a dedicated endpoint for updating connections?
|
||||
apiResponse = await util.apiPostGetRequest('/v1/connections/add/discord', {
|
||||
'Authorization': `${request.cookies.token_type} ${request.cookies.access_token}`
|
||||
}, {
|
||||
data: {
|
||||
id: account.connections.discord.id,
|
||||
access_token: tokens.access_token,
|
||||
refresh_token: tokens.refresh_token,
|
||||
}
|
||||
});
|
||||
|
||||
if (apiResponse.statusCode !== 200) {
|
||||
// Assume expired, refresh and retry request
|
||||
apiResponse = await util.apiPostGetRequest('/v1/login', {}, {
|
||||
refresh_token: request.cookies.refresh_token,
|
||||
grant_type: 'refresh_token'
|
||||
});
|
||||
|
||||
if (apiResponse.statusCode !== 200) {
|
||||
// TODO: Error message
|
||||
return response.status(apiResponse.statusCode).json({
|
||||
error: 'Bad'
|
||||
});
|
||||
}
|
||||
|
||||
const tokens = apiResponse.body;
|
||||
|
||||
response.cookie('refresh_token', tokens.refresh_token, { domain : '.pretendo.network' });
|
||||
response.cookie('access_token', tokens.access_token, { domain : '.pretendo.network' });
|
||||
response.cookie('token_type', tokens.token_type, { domain : '.pretendo.network' });
|
||||
|
||||
apiResponse = await util.apiPostGetRequest('/v1/connections/add/discord', {
|
||||
'Authorization': `${tokens.token_type} ${tokens.access_token}`
|
||||
}, {
|
||||
data: {
|
||||
id: account.connections.discord.id,
|
||||
access_token: tokens.access_token,
|
||||
refresh_token: tokens.refresh_token,
|
||||
}
|
||||
});
|
||||
|
||||
// If still failed, something went horribly wrong
|
||||
if (apiResponse.statusCode !== 200) {
|
||||
// TODO: Error message
|
||||
return response.status(apiResponse.statusCode).json({
|
||||
error: 'Bad'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
account.connections.discord.access_token = tokens.access_token;
|
||||
account.connections.discord.refresh_token = tokens.refresh_token;
|
||||
}
|
||||
|
||||
// Get the users Discord roles to check if they are a tester
|
||||
const { roles } = await discordOAuth.getMemberRolesForGuild({
|
||||
userId: account.connections.discord.id,
|
||||
guildId: config.discord.guild_id,
|
||||
botToken: config.discord.bot_token
|
||||
});
|
||||
|
||||
// Only run this check if not already a tester (edge case)
|
||||
if (!renderData.isTester) {
|
||||
// 409116477212459008 = Developer
|
||||
// 882247322933801030 = Super Mario (Patreon tier)
|
||||
renderData.isTester = roles.some(role => config.discord.tester_roles.includes(role));
|
||||
}
|
||||
} else {
|
||||
// If no Discord account linked, generate an auth URL
|
||||
const discordAuthURL = discordOAuth.generateAuthUrl({
|
||||
scope: ['identify', 'guilds'],
|
||||
state: crypto.randomBytes(16).toString('hex'),
|
||||
});
|
||||
|
||||
renderData.discordAuthURL = discordAuthURL;
|
||||
}
|
||||
|
||||
response.render('account', renderData);
|
||||
});
|
||||
|
||||
router.get('/login', async (request, response) => {
|
||||
const renderData = {
|
||||
layout: 'main',
|
||||
locale: util.getLocale(request.locale.region, request.locale.language),
|
||||
localeString: request.locale.toString(),
|
||||
error: request.cookies.error
|
||||
};
|
||||
|
||||
response.clearCookie('error');
|
||||
|
||||
response.render('account_login', renderData);
|
||||
});
|
||||
|
||||
router.post('/login', async (request, response) => {
|
||||
const { username, password } = request.body;
|
||||
|
||||
const apiResponse = await util.apiPostGetRequest('/v1/login', {}, {
|
||||
username,
|
||||
password,
|
||||
grant_type: 'password'
|
||||
});
|
||||
|
||||
if (apiResponse.statusCode !== 200) {
|
||||
response.cookie('error', apiResponse.body.error, { domain: '.pretendo.network' });
|
||||
return response.redirect('/account/login');
|
||||
}
|
||||
|
||||
const tokens = apiResponse.body;
|
||||
|
||||
response.cookie('refresh_token', tokens.refresh_token, { domain : '.pretendo.network' });
|
||||
response.cookie('access_token', tokens.access_token, { domain : '.pretendo.network' });
|
||||
response.cookie('token_type', tokens.token_type, { domain : '.pretendo.network' });
|
||||
|
||||
response.redirect('/account');
|
||||
});
|
||||
|
||||
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',
|
||||
let tokens;
|
||||
try {
|
||||
// Attempt to get OAuth2 tokens
|
||||
tokens = await discordOAuth.tokenRequest({
|
||||
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();
|
||||
scope: 'identify guilds',
|
||||
grantType: 'authorization_code',
|
||||
});
|
||||
} catch (error) {
|
||||
response.cookie('error', 'Invalid Discord authorization code. Please try again', { domain: '.pretendo.network' });
|
||||
return response.redirect('/account');
|
||||
}
|
||||
|
||||
/*
|
||||
// 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',
|
||||
// Get Discord user data
|
||||
const user = await discordOAuth.getUser(tokens.access_token);
|
||||
|
||||
// Link the Discord account to the PNID
|
||||
let apiResponse = await util.apiPostGetRequest('/v1/connections/add/discord', {
|
||||
'Authorization': `${request.cookies.token_type} ${request.cookies.access_token}`
|
||||
}, {
|
||||
data: {
|
||||
id: user.id,
|
||||
access_token: tokens.access_token,
|
||||
refresh_token: tokens.refresh_token,
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
/* 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);
|
||||
*/
|
||||
if (apiResponse.statusCode !== 200) {
|
||||
// Assume expired, refresh and retry request
|
||||
apiResponse = await util.apiPostGetRequest('/v1/login', {}, {
|
||||
refresh_token: request.cookies.refresh_token,
|
||||
grant_type: 'refresh_token'
|
||||
});
|
||||
|
||||
// This sets a cookie to tell the account page to show the "Account linked successfully" notice, and redirects.
|
||||
response.cookie('justLinked', 'discord').redirect('/account');
|
||||
if (apiResponse.statusCode !== 200) {
|
||||
// TODO: Error message
|
||||
return response.status(apiResponse.statusCode).json({
|
||||
error: 'Bad'
|
||||
});
|
||||
}
|
||||
|
||||
const tokens = apiResponse.body;
|
||||
|
||||
response.cookie('refresh_token', tokens.refresh_token, { domain : '.pretendo.network' });
|
||||
response.cookie('access_token', tokens.access_token, { domain : '.pretendo.network' });
|
||||
response.cookie('token_type', tokens.token_type, { domain : '.pretendo.network' });
|
||||
|
||||
apiResponse = await util.apiPostGetRequest('/v1/connections/add/discord', {
|
||||
'Authorization': `${tokens.token_type} ${tokens.access_token}`
|
||||
}, {
|
||||
data: {
|
||||
id: user.id,
|
||||
access_token: tokens.access_token,
|
||||
refresh_token: tokens.refresh_token,
|
||||
}
|
||||
});
|
||||
|
||||
// If still failed, something went horribly wrong
|
||||
if (apiResponse.statusCode !== 200) {
|
||||
// TODO: Error message
|
||||
return response.status(apiResponse.statusCode).json({
|
||||
error: 'Bad'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
response.cookie('linked', 'Discord', { domain: '.pretendo.network' }).redirect('/account');
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router;
|
||||
|
|
@ -14,6 +14,7 @@ const app = express();
|
|||
|
||||
logger.info('Setting up Middleware');
|
||||
app.use(morgan('dev'));
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
logger.info('Setting up static public folder');
|
||||
app.use(express.static('public'));
|
||||
|
|
@ -65,9 +66,6 @@ app.use(expressLocale({
|
|||
'default': 'en-US'
|
||||
}));
|
||||
|
||||
|
||||
|
||||
|
||||
app.use('/', routers.home);
|
||||
app.use('/faq', routers.faq);
|
||||
app.use('/progress', routers.progress);
|
||||
|
|
@ -108,6 +106,12 @@ app.engine('handlebars', handlebars({
|
|||
${htmlRight}
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
eq(value1, value2) {
|
||||
return value1 === value2;
|
||||
},
|
||||
neq(value1, value2) {
|
||||
return value1 !== value2;
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -6,15 +6,20 @@ const trello = new Trello(config.trello.api_key, config.trello.api_token);
|
|||
let cache;
|
||||
|
||||
async function getTrelloCache() {
|
||||
const available = await trelloAPIAvailable();
|
||||
if (!available) {
|
||||
return {
|
||||
update_time: Date.now(),
|
||||
sections: []
|
||||
};
|
||||
}
|
||||
|
||||
if (!cache) {
|
||||
cache = await updateTrelloCache();
|
||||
}
|
||||
|
||||
if (cache.update_time < Date.now() - (1000 * 60 * 60)) {
|
||||
const available = await trelloAPIAvailable();
|
||||
if (available) {
|
||||
cache = await updateTrelloCache();
|
||||
}
|
||||
cache = await updateTrelloCache();
|
||||
}
|
||||
|
||||
return cache;
|
||||
|
|
|
|||
40
src/util.js
40
src/util.js
|
|
@ -1,4 +1,5 @@
|
|||
const fs = require('fs-extra');
|
||||
const got = require('got');
|
||||
const logger = require('./logger');
|
||||
|
||||
function fullUrl(request) {
|
||||
|
|
@ -17,7 +18,44 @@ function getLocale(region, language) {
|
|||
return require(`${__dirname}/../locales/US_en.json`);
|
||||
}
|
||||
|
||||
function apiGetRequest(path, headers) {
|
||||
return got.get(`https://api.pretendo.cc${path}`, {
|
||||
responseType: 'json',
|
||||
throwHttpErrors: false,
|
||||
https: {
|
||||
rejectUnauthorized: false, // Needed for self-signed certificates on localhost testing
|
||||
},
|
||||
headers
|
||||
});
|
||||
}
|
||||
|
||||
function apiPostGetRequest(path, headers, json) {
|
||||
return got.post(`https://api.pretendo.cc${path}`, {
|
||||
responseType: 'json',
|
||||
throwHttpErrors: false,
|
||||
https: {
|
||||
rejectUnauthorized: false, // Needed for self-signed certificates on localhost testing
|
||||
},
|
||||
headers,
|
||||
json
|
||||
});
|
||||
}
|
||||
|
||||
function apiDeleteGetRequest(path, headers, json) {
|
||||
return got.delete(`https://api.pretendo.cc${path}`, {
|
||||
throwHttpErrors: false,
|
||||
https: {
|
||||
rejectUnauthorized: false, // Needed for self-signed certificates on localhost testing
|
||||
},
|
||||
headers,
|
||||
json
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fullUrl,
|
||||
getLocale
|
||||
getLocale,
|
||||
apiGetRequest,
|
||||
apiPostGetRequest,
|
||||
apiDeleteGetRequest
|
||||
};
|
||||
|
|
@ -6,11 +6,11 @@
|
|||
|
||||
<div class="account-wrapper">
|
||||
<div class="account-sidebar">
|
||||
<div class="user">
|
||||
<img src="{{account.mii.image_url}}&bgColor=3E446600" class="mii" />
|
||||
<p class="miiname">{{account.mii.name}}</p>
|
||||
<p class="username">PNID: {{account.username}}</p>
|
||||
</div>
|
||||
<div class="user">
|
||||
<img src="{{account.mii.image_url}}" class="mii" />
|
||||
<p class="miiname">{{account.mii.name}}</p>
|
||||
<p class="username">PNID: {{account.username}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-wrapper">
|
||||
|
|
@ -50,8 +50,8 @@
|
|||
<h2 class="header">Servers</h2>
|
||||
<fieldset {{#if isTester}}{{else}}disabled{{/if}}>
|
||||
<form class="server-selection" id="server">
|
||||
<input type="radio" id="prod" name="server_selection" value="prod">
|
||||
<label for="prod">
|
||||
<input type="radio" id="prod" name="server_selection" value="prod" checked="{{ eq account.server_access_level 'prod'}}">
|
||||
<label for="prod">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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>
|
||||
|
|
@ -62,8 +62,8 @@
|
|||
</svg>
|
||||
<h2>Production</h2>
|
||||
</label>
|
||||
<input type="radio" id="beta" name="server_selection" value="beta">
|
||||
<label for="beta">
|
||||
<input type="radio" id="beta" name="server_selection" value="beta" checked="{{ neq account.server_access_level 'prod'}}">
|
||||
<label for="beta">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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>
|
||||
|
|
@ -73,15 +73,14 @@
|
|||
</fieldset>
|
||||
{{#if discordUser}}
|
||||
{{#if isTester }}
|
||||
<p>Connected as {{ discordUser.username }}#{{ discordUser.discriminator }}.</p>
|
||||
<p>Connected as {{ discordUser.username }}#{{ discordUser.discriminator }}</p>
|
||||
<button class="button secondary" id="remove-discord-connection">Remove Discord account</button>
|
||||
{{else}}
|
||||
<p>{{ 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]</p>
|
||||
<p>Beta servers are exclusive to testers. To become a tester, check us out on <a href="https://www.patreon.com/PretendoNetwork" target="_blank">Patreon</a> and link your Discord account to your Patreon and PNID accounts</p>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<p>Beta servers are exclusive to testers. If you're a tester, connect your Discord account <a href="https://discord.com/api/oauth2/authorize?client_id=896508671742345266&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Faccount%2Fconnect%2Fdiscord&response_type=code&scope=identify">here</a>.</p>
|
||||
<p>Beta servers are exclusive to testers. If you're already a tester, connect your Discord account <a href="{{ discordAuthURL }}">here</a>.</p>
|
||||
{{/if}}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<h2 class="section-header" id="security">Sign in and security</h2>
|
||||
|
|
@ -109,11 +108,11 @@
|
|||
<h2 class="header">Sign in history</h2>
|
||||
<ul class="setting-list">
|
||||
{{#each account.devices }}
|
||||
<li>
|
||||
<li>
|
||||
<p class="label">{{this.device_attributes.name}}</p>
|
||||
<p class="value">{{this.device_attributes.created_date}}</p>
|
||||
</li>
|
||||
{{/each}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
<a href="/account/sign-in-history">
|
||||
<button class="button secondary">View full sign in history</button>
|
||||
|
|
@ -124,29 +123,30 @@
|
|||
<div class="setting-card span-both-columns">
|
||||
<h2 class="header">Newsletter</h2>
|
||||
<form id="other">
|
||||
<input type="checkbox" id="marketing" name="marketing" {{#if account.flags.marketing}}checked{{/if}}>
|
||||
<label for="marketing">Receive project updates via email (you can opt-out at any time)</label>
|
||||
|
||||
<input type="checkbox" id="marketing" name="marketing" {{#if account.flags.marketing}}checked{{/if}}>
|
||||
<label for="marketing">Receive project updates via email (you can opt-out at any time)</label>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{{> footer }}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{{#if justLinked}}
|
||||
<div class="account-link-notice">
|
||||
<div>
|
||||
<p>Account linked successfully.</p>
|
||||
</div>
|
||||
{{#if linked}}
|
||||
<div class="banner-notice success">
|
||||
<div>
|
||||
<p>{{ linked }} account linked successfully</p>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<script>
|
||||
// Selectes the server level from the user's PNID
|
||||
{{ account.server_access_level }}.setAttribute('checked', true);
|
||||
</script>
|
||||
{{#if error}}
|
||||
<div class="banner-notice error">
|
||||
<div>
|
||||
<p>{{ error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<script src="/assets/js/account.js"></script>
|
||||
42
views/account_login.handlebars
Normal file
42
views/account_login.handlebars
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<link rel="stylesheet" href="/assets/css/login.css" />
|
||||
|
||||
<div class="wrapper">
|
||||
|
||||
{{> header}}
|
||||
|
||||
|
||||
<div class="login-wrapper">
|
||||
<h1 class="title dot">Login</h1>
|
||||
<form action="/account/login" method="post" class="login">
|
||||
<div>
|
||||
<input name="username" id="username" placeholder="Username" required>
|
||||
</div>
|
||||
<div>
|
||||
<input name="password" id="password" type="password" placeholder="Password" required>
|
||||
</div>
|
||||
<input name="grant_type" id="grant_type" type="hidden" value="password">
|
||||
<div>
|
||||
<button type="submit">Login</button>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/account/passwordreset" class="pwdreset">Forgot your password?</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/account/register" class="register">
|
||||
<button type="button">Don't have an account?</button>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{> footer }}
|
||||
|
||||
</div>
|
||||
|
||||
{{#if error}}
|
||||
<div class="banner-notice error">
|
||||
<div>
|
||||
<p>{{ error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
|
@ -43,5 +43,4 @@
|
|||
</div>
|
||||
|
||||
{{> footer }}
|
||||
|
||||
</div>
|
||||
|
|
@ -1 +1 @@
|
|||
<footer>Copyright 2021 - Design by mrjvs, development by Jip Fr & Monty</footer>
|
||||
<footer>Copyright 2021 - Design by mrjvs, development by Jip Fr & monty</footer>
|
||||
Loading…
Reference in New Issue
Block a user