2021-06-27 18:44:09 +00:00
|
|
|
const express = require('express');
|
|
|
|
const crypto = require('crypto');
|
2021-07-10 03:01:52 +00:00
|
|
|
const fetch = require('node-fetch');
|
2021-06-27 18:44:09 +00:00
|
|
|
const Config = require('./config.js');
|
|
|
|
const Database = require('./db_interface.js');
|
2021-06-17 21:00:56 +00:00
|
|
|
const Mail = require('./mail.js');
|
2021-06-14 22:04:46 +00:00
|
|
|
|
|
|
|
let router = express.Router();
|
|
|
|
|
|
|
|
router.use(express.json());
|
|
|
|
|
|
|
|
let session_entropy = {};
|
|
|
|
|
|
|
|
user_cache = {};
|
|
|
|
email_cache = {};
|
2021-07-10 03:01:52 +00:00
|
|
|
discord_cache = {};
|
|
|
|
discord_user_cache = {};
|
|
|
|
|
|
|
|
async function fetch_user(where) {
|
|
|
|
let user = await Database.schemas.user.findOne({ where: where });
|
|
|
|
if (user === null) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
user_cache[user.id] = {
|
|
|
|
id: user.id,
|
|
|
|
email: user.email,
|
|
|
|
discord_id: user.discord_id,
|
|
|
|
password_hash: user.password_hash
|
|
|
|
};
|
|
|
|
email_cache[user.email] = user.id;
|
|
|
|
if (user.discord_id) {
|
|
|
|
discord_cache[user.discord_id] = user.id
|
|
|
|
}
|
|
|
|
return user_cache[user.id]
|
|
|
|
}
|
|
|
|
|
|
|
|
async function fetch_discord_user(auth) {
|
|
|
|
const result = await fetch(`https://discord.com/api/v8/users/@me`, {
|
|
|
|
headers: {
|
|
|
|
'Authorization': auth.token_type + ' ' + auth.access_token
|
|
|
|
}
|
|
|
|
});
|
|
|
|
const json = result.json();
|
|
|
|
discord_user_cache[json.id] = {
|
|
|
|
user: json,
|
|
|
|
auth: auth,
|
|
|
|
expires: (new Date().getTime()) + (json.expires_in * 1000)
|
|
|
|
}
|
|
|
|
return discord_user_cache[id];
|
|
|
|
}
|
|
|
|
|
|
|
|
async function acquire_discord_token(code, redirect) {
|
|
|
|
let data = {
|
|
|
|
client_id: Config.config.discord_id,
|
|
|
|
client_secret: Config.config.discord_secret,
|
|
|
|
grant_type: 'authorization_code',
|
|
|
|
code: code,
|
|
|
|
redirect_uri: redirect
|
|
|
|
}
|
|
|
|
const result = await fetch(`https://discord.com/api/oauth2/token`, {
|
|
|
|
method: 'POST',
|
|
|
|
body: data,
|
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
|
|
}
|
|
|
|
}).catch(err => console.error(err));
|
|
|
|
if (!result.ok) {
|
|
|
|
return res.status(500).json({error: "could not fetch user details"})
|
|
|
|
}
|
|
|
|
const json = result.json();
|
|
|
|
return fetch_discord_user(json);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function refresh_discord_token(id) {
|
|
|
|
let data = {
|
|
|
|
client_id: Config.config.discord_id,
|
|
|
|
client_secret: Config.config.discord_secret,
|
|
|
|
grant_type: 'refresh_token',
|
|
|
|
refresh_token: discord_user_cache[id].auth.refresh_token
|
|
|
|
}
|
|
|
|
const result = await fetch(`https://discord.com/api/oauth2/token`, {
|
|
|
|
method: 'POST',
|
|
|
|
body: data,
|
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
|
|
}
|
|
|
|
}).catch(err => console.error(err));
|
|
|
|
if (!result.ok) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const json = result.json();
|
|
|
|
discord_user_cache[id].auth.access_token = json.access_token;
|
|
|
|
discord_user_cache[id].expires = (new Date().getTime()) + (json.expires_in * 1000);
|
|
|
|
return true;
|
|
|
|
}
|
2021-06-14 22:04:46 +00:00
|
|
|
|
|
|
|
async function get_user_details(id) {
|
2021-06-27 18:44:09 +00:00
|
|
|
if (!id || id === 'undefined') {
|
2021-06-14 22:04:46 +00:00
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
console.log(`search for user with id ${id}`);
|
|
|
|
if (!user_cache[id]) {
|
2021-07-10 03:01:52 +00:00
|
|
|
return await fetch_user({ id: id })
|
2021-06-14 22:04:46 +00:00
|
|
|
}
|
2021-07-10 03:01:52 +00:00
|
|
|
// console.log(`returning ${JSON.stringify(user_cache[id])}`);
|
2021-06-14 22:04:46 +00:00
|
|
|
return user_cache[id];
|
|
|
|
}
|
|
|
|
|
|
|
|
async function get_user_details_by_email(email) {
|
2021-06-27 18:44:09 +00:00
|
|
|
if (!email || email === 'undefined') {
|
2021-06-14 22:04:46 +00:00
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
console.log(`search for user with email ${email}}`);
|
|
|
|
if (!email_cache[email] || !user_cache[email_cache[email]]) {
|
2021-07-10 03:01:52 +00:00
|
|
|
return await fetch_user({ email: email })
|
2021-06-14 22:04:46 +00:00
|
|
|
}
|
2021-07-10 03:01:52 +00:00
|
|
|
// console.log(`returning ${JSON.stringify(user_cache[email_cache[email]])}`);
|
2021-06-14 22:04:46 +00:00
|
|
|
return user_cache[email_cache[email]];
|
|
|
|
}
|
|
|
|
|
2021-07-10 03:01:52 +00:00
|
|
|
async function get_user_details_by_discord_id(id) {
|
|
|
|
if (!id || id === 'undefined') {
|
|
|
|
return undefined;
|
2021-06-14 22:04:46 +00:00
|
|
|
}
|
2021-07-10 03:01:52 +00:00
|
|
|
if (!discord_cache[id] || !user_cache[discord_cache[id]]) {
|
|
|
|
return await fetch_user({ discord_id: id })
|
2021-06-14 22:04:46 +00:00
|
|
|
}
|
2021-07-10 03:01:52 +00:00
|
|
|
return user_cache[discord_cache[id]];
|
|
|
|
}
|
2021-06-14 22:04:46 +00:00
|
|
|
|
2021-06-27 18:44:09 +00:00
|
|
|
function hash(secret, password, base64 = true) {
|
2021-06-14 22:04:46 +00:00
|
|
|
let pw_hash = crypto.pbkdf2Sync(
|
|
|
|
password,
|
|
|
|
secret,
|
|
|
|
Config.config.key?.iterations || 1000,
|
|
|
|
Config.config.key?.length || 64,
|
2021-06-27 18:44:09 +00:00
|
|
|
'sha512'
|
2021-06-14 22:04:46 +00:00
|
|
|
);
|
|
|
|
|
2021-06-27 18:44:09 +00:00
|
|
|
return pw_hash.toString(base64 ? 'base64' : 'hex');
|
2021-06-14 22:04:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function verify(secret, password, hash) {
|
|
|
|
let pw_hash = crypto.pbkdf2Sync(
|
|
|
|
password,
|
|
|
|
secret,
|
|
|
|
Config.config.key?.iterations || 1000,
|
|
|
|
Config.config.key?.length || 64,
|
2021-06-27 18:44:09 +00:00
|
|
|
'sha512'
|
2021-06-14 22:04:46 +00:00
|
|
|
);
|
|
|
|
|
2021-06-27 18:44:09 +00:00
|
|
|
return hash === pw_hash.toString('base64');
|
2021-06-14 22:04:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function hash_password(password) {
|
|
|
|
return hash(Config.config.secret, password);
|
|
|
|
}
|
|
|
|
|
|
|
|
function verify_password(password, hash) {
|
|
|
|
return verify(Config.config.secret, password, hash);
|
|
|
|
}
|
|
|
|
|
2021-06-27 18:44:09 +00:00
|
|
|
function get_session_token(id, password_hash, base64 = true) {
|
2021-06-14 22:04:46 +00:00
|
|
|
session_entropy[id] = crypto.randomBytes(Config.config.session_entropy || 32);
|
2021-06-27 18:44:09 +00:00
|
|
|
return hash(session_entropy[id], password_hash, base64);
|
2021-06-14 22:04:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function verify_session_token(id, hash, token) {
|
|
|
|
if (session_entropy[id]) {
|
|
|
|
return verify(session_entropy[id], hash, token);
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function enforce_session_login(req, res, next) {
|
2021-06-27 18:44:09 +00:00
|
|
|
let userid = req.get('id');
|
|
|
|
let session_token = req.get('authorization');
|
|
|
|
console.log('a', userid, session_token);
|
2021-06-14 22:04:46 +00:00
|
|
|
if (!userid || !session_token) {
|
|
|
|
return res.sendStatus(401);
|
|
|
|
}
|
|
|
|
let user = await get_user_details(userid);
|
|
|
|
if (!user) {
|
|
|
|
return res.sendStatus(401);
|
|
|
|
}
|
2021-06-27 18:44:09 +00:00
|
|
|
let verified_session = verify_session_token(userid, user.password_hash, session_token);
|
2021-06-14 22:04:46 +00:00
|
|
|
if (!verified_session) {
|
|
|
|
return res.sendStatus(401);
|
|
|
|
}
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
2021-06-27 18:44:09 +00:00
|
|
|
router.post('/signup', async (req, res) => {
|
2021-06-14 22:04:46 +00:00
|
|
|
if (!req.body?.email || !req.body?.password) {
|
|
|
|
return res.status(400).json({
|
2021-06-27 18:44:09 +00:00
|
|
|
error: 'must have email and password fields'
|
2021-06-14 22:04:46 +00:00
|
|
|
});
|
|
|
|
}
|
2021-06-27 18:44:09 +00:00
|
|
|
let user = await get_user_details_by_email(req.body?.email);
|
2021-06-17 21:00:56 +00:00
|
|
|
|
2021-06-27 18:44:09 +00:00
|
|
|
if (user !== undefined && user !== {}) {
|
|
|
|
console.warn(`user already found: ${JSON.stringify(user)}`);
|
2021-06-14 22:04:46 +00:00
|
|
|
return res.status(403).json({
|
2021-06-27 18:44:09 +00:00
|
|
|
error: `email ${req.body.email} is already in use.`
|
2021-06-14 22:04:46 +00:00
|
|
|
});
|
2021-06-27 18:44:09 +00:00
|
|
|
} else {
|
|
|
|
let match = await Database.schemas.unverifiedUser.findOne({ where: { email: req.body.email } });
|
|
|
|
if (!!match) {
|
|
|
|
await Database.schemas.unverifiedUser.destroy({ where: { email: match.email } });
|
|
|
|
}
|
|
|
|
let randomString = 'Signup';
|
|
|
|
for (let i = 0; i < 16; i++) {
|
|
|
|
randomString += Math.floor(Math.random() * 10);
|
|
|
|
}
|
|
|
|
let password_hash = hash_password(req.body.password);
|
2021-06-17 21:00:56 +00:00
|
|
|
let user = await Database.schemas.unverifiedUser.create({
|
2021-06-14 22:04:46 +00:00
|
|
|
email: String(req.body.email),
|
2021-06-27 18:44:09 +00:00
|
|
|
password_hash: password_hash,
|
|
|
|
verificationToken: get_session_token(randomString, password_hash, false)
|
2021-06-17 21:00:56 +00:00
|
|
|
});
|
2021-07-10 03:01:52 +00:00
|
|
|
const link = `${Config.config.https ? 'https://' : 'http://'}${req.headers.host}/api/user/verify?verification=${user.verificationToken
|
|
|
|
}`;
|
2021-06-27 18:44:09 +00:00
|
|
|
const content = `Click here to verify your sign-up:
|
|
|
|
${link}`;
|
|
|
|
const contentHtml = `<h1>Click here to verify your sign-up:</h1>
|
|
|
|
<p><a href=${link}>${link}</a></p>`;
|
|
|
|
await Mail.sendMail([String(req.body.email)], 'Verify Your Account', contentHtml, content);
|
2021-06-17 21:00:56 +00:00
|
|
|
return res.sendStatus(204);
|
|
|
|
}
|
2021-06-27 18:44:09 +00:00
|
|
|
});
|
2021-06-17 21:00:56 +00:00
|
|
|
|
2021-06-27 18:44:09 +00:00
|
|
|
router.get('/verify', async (req, res) => {
|
|
|
|
if (!req.query?.verification) {
|
2021-06-17 21:00:56 +00:00
|
|
|
return res.status(400).send(
|
|
|
|
`<html>
|
|
|
|
<body>
|
2021-06-27 18:44:09 +00:00
|
|
|
<h1>No Verification Link</h1>
|
2021-06-17 21:00:56 +00:00
|
|
|
</body>
|
|
|
|
</html>`
|
2021-06-27 18:44:09 +00:00
|
|
|
);
|
2021-06-17 21:00:56 +00:00
|
|
|
}
|
2021-06-27 18:44:09 +00:00
|
|
|
let verification = req.query?.verification;
|
|
|
|
let user = await Database.schemas.unverifiedUser.findOne({ where: { verificationToken: verification } });
|
|
|
|
|
|
|
|
if (user !== undefined && user !== {}) {
|
|
|
|
if (user.verificationToken != verification) {
|
|
|
|
return res.status(404).send(
|
|
|
|
`<html>
|
|
|
|
<body>
|
|
|
|
<h1>Unknown Verification Link</h1>
|
|
|
|
</body>
|
|
|
|
</html>`
|
|
|
|
);
|
|
|
|
}
|
2021-06-17 21:00:56 +00:00
|
|
|
let newUser = await Database.schemas.user.create({
|
|
|
|
email: user.email,
|
2021-06-27 18:44:09 +00:00
|
|
|
password_hash: user.password_hash
|
2021-06-14 22:04:46 +00:00
|
|
|
});
|
|
|
|
|
2021-06-27 18:44:09 +00:00
|
|
|
return res.send(`<html>
|
|
|
|
<body>
|
|
|
|
<h1>Sign up complete.</h1>
|
|
|
|
</body>
|
|
|
|
</html>`);
|
2021-06-17 21:00:56 +00:00
|
|
|
} else {
|
2021-06-27 18:44:09 +00:00
|
|
|
return res.status(404).send(`<html>
|
|
|
|
<body>
|
|
|
|
<h1>Unknown Verification Link</h1>
|
|
|
|
</body>
|
|
|
|
</html>`);
|
2021-06-14 22:04:46 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2021-07-10 03:01:52 +00:00
|
|
|
router.get('/login/discord', async (req, res) => {
|
|
|
|
if (!Config.config.discord_id || !Config.config.discord_secret) {
|
|
|
|
return res.status(403).send("discord login is not enabled.");
|
|
|
|
}
|
|
|
|
const url = encodeURIComponent(`${req.headers.host}discord`);
|
|
|
|
return res.send(`https://discord.com/api/oauth2/authorize?client_id=${Config.config.discord_id}&redirect_uri=${url}&response_type=code&scope=identify%20email%20guilds`);
|
|
|
|
});
|
|
|
|
|
|
|
|
router.post('/login/discord', async (req, res) => {
|
|
|
|
if (!Config.config.discord_id || !Config.config.discord_secret) {
|
|
|
|
return res.status(403).json({ error: "discord login is not enabled." });
|
|
|
|
}
|
|
|
|
if (!req.params.code || !req.headers.host) {
|
|
|
|
return res.status(400).json({error: "invalid oauth request"});
|
|
|
|
}
|
|
|
|
const result = await acquire_discord_token(req.params.code, req.headers.host);
|
|
|
|
const matching_account = await get_user_details_by_discord_id(result.user.id);
|
|
|
|
if (!matching_account) {
|
|
|
|
let user = await Database.schemas.unverifiedUser.create({
|
|
|
|
email: String(result.user.email),
|
|
|
|
discord_id: user.id,
|
|
|
|
verificationToken: get_session_token(randomString, result.auth.access_token, false)
|
|
|
|
});
|
|
|
|
return res.json({
|
|
|
|
type: 'unverified',
|
|
|
|
verificationToken: user.verificationToken
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return res.json({
|
|
|
|
type: 'verified',
|
|
|
|
userid: matching_account.id,
|
|
|
|
session_token: get_session_token(matching_account.id, result.auth.access_token)
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
//TODO
|
|
|
|
router.post('/discord/create', async (req, res) =>{});
|
|
|
|
router.post('/discord/link', async (req, res) =>{});
|
|
|
|
|
2021-06-27 18:44:09 +00:00
|
|
|
router.post('/login', async (req, res) => {
|
2021-06-14 22:04:46 +00:00
|
|
|
if (!req.body?.email || !req.body?.password) {
|
|
|
|
return res.status(400).json({
|
2021-06-27 18:44:09 +00:00
|
|
|
error: 'must have email and password fields'
|
2021-06-14 22:04:46 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
let user = await get_user_details_by_email(req.body.email);
|
|
|
|
if (!user) {
|
|
|
|
return res.status(401).json({
|
2021-06-27 18:44:09 +00:00
|
|
|
error: 'incorrect email or password'
|
2021-06-14 22:04:46 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
let verified = verify_password(req.body.password, user.password_hash);
|
|
|
|
|
|
|
|
if (!verified) {
|
|
|
|
return res.status(401).json({
|
2021-06-27 18:44:09 +00:00
|
|
|
error: 'incorrect email or password'
|
2021-06-14 22:04:46 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-06-17 21:00:56 +00:00
|
|
|
return res.json({
|
|
|
|
userid: user.id,
|
2021-06-27 18:44:09 +00:00
|
|
|
session_token: get_session_token(user.id, user.password_hash)
|
2021-06-14 22:04:46 +00:00
|
|
|
});
|
2021-06-17 21:00:56 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
2021-07-10 03:01:52 +00:00
|
|
|
router.use('/logout', enforce_session_login)
|
|
|
|
router.post('/logout', async (req, res) => {
|
|
|
|
let userid = req.get('id');
|
|
|
|
let session_token = req.get('authorization');
|
2021-06-17 21:00:56 +00:00
|
|
|
|
|
|
|
let user = await get_user_details(userid);
|
|
|
|
if (!user) {
|
|
|
|
return res.status(401).json({
|
2021-06-27 18:44:09 +00:00
|
|
|
error: 'invalid user data'
|
2021-06-17 21:00:56 +00:00
|
|
|
});
|
|
|
|
}
|
2021-06-27 18:44:09 +00:00
|
|
|
let verified = verify_session_token(user.id, user.password_hash, session_token);
|
2021-06-17 21:00:56 +00:00
|
|
|
|
|
|
|
if (!verified) {
|
|
|
|
return res.status(401).json({
|
2021-06-27 18:44:09 +00:00
|
|
|
error: 'invalid user data'
|
2021-06-17 21:00:56 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
delete session_entropy[user.id];
|
2021-06-14 22:04:46 +00:00
|
|
|
return res.sendStatus(204);
|
|
|
|
});
|
|
|
|
|
2021-07-10 03:01:52 +00:00
|
|
|
router.use('/byEmail', enforce_session_login)
|
|
|
|
router.get('/byEmail/:email', async (req, res) => {
|
|
|
|
if (!req.params?.email) {
|
|
|
|
res.status(400).json({
|
|
|
|
error: 'email is a required parameter'
|
|
|
|
});
|
|
|
|
}
|
|
|
|
let user = get_user_details_by_email(req.params.email);
|
|
|
|
console.log(user);
|
|
|
|
if (user !== undefined && user !== {}) {
|
|
|
|
res.json({
|
|
|
|
id: user.id,
|
|
|
|
email: user.email
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
res.sendStatus(404);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
router.use('/', enforce_session_login)
|
2021-06-27 18:44:09 +00:00
|
|
|
router.get('/:id([a-f0-9-]+)', async (req, res) => {
|
2021-06-14 22:04:46 +00:00
|
|
|
console.log(req.params);
|
|
|
|
if (!req.params?.id) {
|
|
|
|
return res.status(400).json({
|
2021-06-27 18:44:09 +00:00
|
|
|
error: 'must have id parameter'
|
2021-06-14 22:04:46 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
let id = req.params?.id;
|
|
|
|
console.log(id);
|
|
|
|
let user = await get_user_details(id);
|
|
|
|
console.log(user);
|
2021-06-27 18:44:09 +00:00
|
|
|
if (user !== undefined && user !== {}) {
|
2021-06-14 22:04:46 +00:00
|
|
|
return res.json({
|
|
|
|
id: user.id,
|
2021-06-27 18:44:09 +00:00
|
|
|
email: user.email
|
2021-06-14 22:04:46 +00:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
return res.sendStatus(404);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2021-06-27 18:44:09 +00:00
|
|
|
router.use('/authorized', enforce_session_login);
|
|
|
|
router.get('/authorized', async (req, res) => {
|
|
|
|
let userid = req.get('id');
|
2021-06-14 22:04:46 +00:00
|
|
|
let user = await get_user_details(userid);
|
|
|
|
return res.json({
|
|
|
|
authorized: true,
|
|
|
|
user: {
|
|
|
|
id: user.id,
|
2021-06-27 18:44:09 +00:00
|
|
|
email: user.email
|
|
|
|
}
|
2021-06-14 22:04:46 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
router: router,
|
|
|
|
enforce_session_login: enforce_session_login,
|
2021-06-27 18:44:09 +00:00
|
|
|
get_user_details: get_user_details,
|
|
|
|
get_user_details_by_email: get_user_details_by_email
|
2021-06-14 22:04:46 +00:00
|
|
|
};
|