const express = require('express'); const crypto = require('crypto'); const fetch = require('node-fetch'); const Config = require('./config.js'); const Database = require('./db_interface.js'); const Mail = require('./mail.js'); let router = express.Router(); router.use(express.json()); let session_entropy = {}; user_cache = {}; email_cache = {}; 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; } async function get_user_details(id) { if (!id || id === 'undefined') { return undefined; } console.log(`search for user with id ${id}`); if (!user_cache[id]) { return await fetch_user({ id: id }) } // console.log(`returning ${JSON.stringify(user_cache[id])}`); return user_cache[id]; } async function get_user_details_by_email(email) { if (!email || email === 'undefined') { return undefined; } console.log(`search for user with email ${email}}`); if (!email_cache[email] || !user_cache[email_cache[email]]) { return await fetch_user({ email: email }) } // console.log(`returning ${JSON.stringify(user_cache[email_cache[email]])}`); return user_cache[email_cache[email]]; } async function get_user_details_by_discord_id(id) { if (!id || id === 'undefined') { return undefined; } if (!discord_cache[id] || !user_cache[discord_cache[id]]) { return await fetch_user({ discord_id: id }) } return user_cache[discord_cache[id]]; } function hash(secret, password, base64 = true) { let pw_hash = crypto.pbkdf2Sync( password, secret, Config.config.key?.iterations || 1000, Config.config.key?.length || 64, 'sha512' ); return pw_hash.toString(base64 ? 'base64' : 'hex'); } function verify(secret, password, hash) { let pw_hash = crypto.pbkdf2Sync( password, secret, Config.config.key?.iterations || 1000, Config.config.key?.length || 64, 'sha512' ); return hash === pw_hash.toString('base64'); } function hash_password(password) { return hash(Config.config.secret, password); } function verify_password(password, hash) { return verify(Config.config.secret, password, hash); } function get_session_token(id, password_hash, base64 = true) { session_entropy[id] = crypto.randomBytes(Config.config.session_entropy || 32); return hash(session_entropy[id], password_hash, base64); } 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) { let userid = req.get('id'); let session_token = req.get('authorization'); console.log('a', userid, session_token); if (!userid || !session_token) { return res.sendStatus(401); } let user = await get_user_details(userid); if (!user) { return res.sendStatus(401); } let verified_session = verify_session_token(userid, user.password_hash, session_token); if (!verified_session) { return res.sendStatus(401); } return next(); } router.post('/signup', async (req, res) => { if (!req.body?.email || !req.body?.password) { return res.status(400).json({ error: 'must have email and password fields' }); } let user = await get_user_details_by_email(req.body?.email); if (user !== undefined && user !== {}) { console.warn(`user already found: ${JSON.stringify(user)}`); return res.status(403).json({ error: `email ${req.body.email} is already in use.` }); } 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); let user = await Database.schemas.unverifiedUser.create({ email: String(req.body.email), password_hash: password_hash, verificationToken: get_session_token(randomString, password_hash, false) }); const link = `${Config.config.https ? 'https://' : 'http://'}${req.headers.host}/api/user/verify?verification=${user.verificationToken }`; const content = `Click here to verify your sign-up: ${link}`; const contentHtml = `

Click here to verify your sign-up:

${link}

`; await Mail.sendMail([String(req.body.email)], 'Verify Your Account', contentHtml, content); return res.sendStatus(204); } }); router.get('/verify', async (req, res) => { if (!req.query?.verification) { return res.status(400).send( `

No Verification Link

` ); } 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( `

Unknown Verification Link

` ); } let newUser = await Database.schemas.user.create({ email: user.email, password_hash: user.password_hash }); return res.send(`

Sign up complete.

`); } else { return res.status(404).send(`

Unknown Verification Link

`); } }); 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) =>{}); router.post('/login', async (req, res) => { if (!req.body?.email || !req.body?.password) { return res.status(400).json({ error: 'must have email and password fields' }); } let user = await get_user_details_by_email(req.body.email); if (!user) { return res.status(401).json({ error: 'incorrect email or password' }); } let verified = verify_password(req.body.password, user.password_hash); if (!verified) { return res.status(401).json({ error: 'incorrect email or password' }); } return res.json({ userid: user.id, session_token: get_session_token(user.id, user.password_hash) }); }); router.use('/logout', enforce_session_login) router.post('/logout', async (req, res) => { let userid = req.get('id'); let session_token = req.get('authorization'); let user = await get_user_details(userid); if (!user) { return res.status(401).json({ error: 'invalid user data' }); } let verified = verify_session_token(user.id, user.password_hash, session_token); if (!verified) { return res.status(401).json({ error: 'invalid user data' }); } delete session_entropy[user.id]; return res.sendStatus(204); }); 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) router.get('/:id([a-f0-9-]+)', async (req, res) => { console.log(req.params); if (!req.params?.id) { return res.status(400).json({ error: 'must have id parameter' }); } let id = req.params?.id; console.log(id); let user = await get_user_details(id); console.log(user); if (user !== undefined && user !== {}) { return res.json({ id: user.id, email: user.email }); } else { return res.sendStatus(404); } }); router.use('/authorized', enforce_session_login); router.get('/authorized', async (req, res) => { let userid = req.get('id'); let user = await get_user_details(userid); return res.json({ authorized: true, user: { id: user.id, email: user.email } }); }); module.exports = { router: router, enforce_session_login: enforce_session_login, get_user_details: get_user_details, get_user_details_by_email: get_user_details_by_email };