diff --git a/backend/config.json.example b/backend/config.json.example index d3b00c5..763b2b4 100644 --- a/backend/config.json.example +++ b/backend/config.json.example @@ -3,11 +3,14 @@ "https": true, "alter_db": true, "port": 8080, + "frontend_url": "localhost:3000", "db_url": "postgres://postgres:@127.0.0.1/todo", "cert": "", "cert_key": "", "mail_host": "", "mail_port": 465, "mail_username": "", - "mail_password": "" + "mail_password": "", + "discord_id": "", + "discord_secret": "" } \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index 2415daa..21a2f92 100644 --- a/backend/package.json +++ b/backend/package.json @@ -5,7 +5,8 @@ "main": "src/index.js", "scripts": { "who": "pwd", - "start": "node src/index.js" + "start": "node src/index.js", + "test": "mocha" }, "repository": { "type": "git", @@ -18,11 +19,18 @@ "cors": "^2.8.5", "express": "^4.17.1", "express-paginate": "^1.0.2", - "nodemailer": "^6.6.1", + "http-proxy": "^1.18.1", + "node-fetch": "^2.6.1", + "nodemailer": "^6.6.2", "pg": "^8.6.0", - "sequelize": "^6.6.2" + "sequelize": "^6.6.5" }, "devDependencies": { - "sequelize-cli": "^6.2.0" + "chai": "^4.3.4", + "mocha": "^9.0.2", + "proxyrequire": "^1.0.21", + "sequelize-cli": "^6.2.0", + "sequelize-test-helpers": "^1.3.3", + "sinon": "^11.1.1" } } \ No newline at end of file diff --git a/backend/src/db_interface.js b/backend/src/db_interface.js index 84e9668..605c5cb 100644 --- a/backend/src/db_interface.js +++ b/backend/src/db_interface.js @@ -1,5 +1,6 @@ const Sequelize = require('sequelize'); const Config = require('./config.js'); +const Models = require('./models'); if (!Config.config.db_url) { console.error('No database url found. please set `db_url` in config.json'); @@ -8,118 +9,6 @@ if (!Config.config.db_url) { const db = new Sequelize(Config.config.db_url); -const UnverifiedUser = db.define('UnverifiedUser', { - id: { - type: Sequelize.DataTypes.UUID, - defaultValue: Sequelize.UUIDV4, - allowNull: false, - primaryKey: true, - unique: true - }, - verificationToken: { - type: Sequelize.DataTypes.STRING, - allowNull: false - }, - email: { - type: Sequelize.DataTypes.STRING, - allowNull: false, - unique: true - }, - password_hash: { - type: Sequelize.DataTypes.STRING, - allowNull: true - } -}); - -const User = db.define('User', { - id: { - type: Sequelize.DataTypes.UUID, - defaultValue: Sequelize.UUIDV4, - allowNull: false, - primaryKey: true, - unique: true - }, - email: { - type: Sequelize.DataTypes.STRING, - allowNull: false, - unique: true - }, - password_hash: { - type: Sequelize.DataTypes.STRING, - allowNull: true - } -}); - -const Todo = db.define('Todo', { - id: { - type: Sequelize.DataTypes.UUID, - defaultValue: Sequelize.UUIDV4, - allowNull: false, - primaryKey: true, - unique: true - }, - user: { - type: Sequelize.DataTypes.UUID, - allowNull: false - }, - content: { - type: Sequelize.DataTypes.TEXT, - allowNull: false - }, - tags: { - type: Sequelize.DataTypes.ARRAY(Sequelize.DataTypes.STRING), - allowNull: true - }, - complete: { - type: Sequelize.DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false - }, - deadline: { - type: Sequelize.DataTypes.DATE, - allowNull: true - } -}); - -const Grouping = db.define('Grouping', { - id: { - type: Sequelize.DataTypes.UUID, - defaultValue: Sequelize.UUIDV4, - allowNull: false, - primaryKey: true, - unique: true - }, - complete: { - type: Sequelize.DataTypes.BOOLEAN, - allowNull: true - }, - manually_added: { - type: Sequelize.DataTypes.ARRAY(Sequelize.DataTypes.UUID), - allowNull: true - }, - required: { - type: Sequelize.DataTypes.ARRAY(Sequelize.DataTypes.STRING), - allowNull: true - }, - exclusions: { - type: Sequelize.DataTypes.ARRAY(Sequelize.DataTypes.STRING), - allowNull: true - } -}); - -let options = { - alter: false -}; -if (Config.config.alter_db) { - options.alter = true; -} -let start = async () => { - await UnverifiedUser.sync(options); - await User.sync(options); - await Todo.sync(options); - await Grouping.sync(options); -}; -start(); module.exports = { db: db, constructors: { @@ -127,10 +16,5 @@ module.exports = { return User.build(); } }, - schemas: { - user: User, - unverifiedUser: UnverifiedUser, - todo: Todo, - grouping: Grouping - } + schemas: Models(db, Sequelize) }; diff --git a/backend/src/index.js b/backend/src/index.js index ed6b230..d3b4995 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -1,8 +1,10 @@ const http = require('http'); const https = require('https'); +const httpProxy = require('http-proxy'); const cors = require('cors'); const express = require('express'); const cookieParser = require('cookie-parser'); +const fs = require('fs'); const Config = require('./config.js'); const UserInterface = require('./user.js'); @@ -15,6 +17,10 @@ if (Config.config.https) { credentials.key = fs.readFileSync(Config.config.cert_key); credentials.cert = fs.readFileSync(Config.config.cert); } + else { + console.error('could not load certs') + process.exit() + } } let app = express(); @@ -43,10 +49,14 @@ if (!Config.config.secret) { app.use('/api/user', UserInterface.router); app.use('/api/todo', TodoInterface.router); -// serve static files last -// app.use(express.static('./static')); -// DISABLED: no longer needs to serve static files -// due to frontend being employed in elm +if (Config.config.frontend_url) { + const proxy = httpProxy.createProxyServer({}) + app.use('/', (req, res) => { + return proxy.web(req, res, { + target: Config.config.frontend_url + }) + }); +} if (Config.config.https) { var server = https.createServer(credentials, app); diff --git a/backend/src/models.js b/backend/src/models.js new file mode 100644 index 0000000..4b219e2 --- /dev/null +++ b/backend/src/models.js @@ -0,0 +1,141 @@ +const Sequelize = require('sequelize'); +const Config = require('./config.js'); + +const models = (db) => { + const UnverifiedUser = db.define('UnverifiedUser', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.UUIDV4, + allowNull: false, + primaryKey: true, + unique: true + }, + verificationToken: { + type: Sequelize.DataTypes.STRING, + allowNull: false + }, + email: { + type: Sequelize.DataTypes.STRING, + allowNull: false, + unique: true + }, + discord_only_account: { + type: Sequelize.DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false + }, + discord_id: { + type: Sequelize.DataTypes.STRING, + allowNull: true + }, + password_hash: { + type: Sequelize.DataTypes.STRING, + allowNull: true + } + }); + + const User = db.define('User', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.UUIDV4, + allowNull: false, + primaryKey: true, + unique: true + }, + email: { + type: Sequelize.DataTypes.STRING, + allowNull: false, + unique: true + }, + discord_only_account: { + type: Sequelize.DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false + }, + discord_id: { + type: Sequelize.DataTypes.STRING, + allowNull: true + }, + password_hash: { + type: Sequelize.DataTypes.STRING, + allowNull: true + } + }); + + const Todo = db.define('Todo', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.UUIDV4, + allowNull: false, + primaryKey: true, + unique: true + }, + user: { + type: Sequelize.DataTypes.UUID, + allowNull: false + }, + content: { + type: Sequelize.DataTypes.TEXT, + allowNull: false + }, + tags: { + type: Sequelize.DataTypes.ARRAY(Sequelize.DataTypes.STRING), + allowNull: true + }, + complete: { + type: Sequelize.DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false + }, + deadline: { + type: Sequelize.DataTypes.DATE, + allowNull: true + } + }); + + const Grouping = db.define('Grouping', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.UUIDV4, + allowNull: false, + primaryKey: true, + unique: true + }, + complete: { + type: Sequelize.DataTypes.BOOLEAN, + allowNull: true + }, + manually_added: { + type: Sequelize.DataTypes.ARRAY(Sequelize.DataTypes.UUID), + allowNull: true + }, + required: { + type: Sequelize.DataTypes.ARRAY(Sequelize.DataTypes.STRING), + allowNull: true + }, + exclusions: { + type: Sequelize.DataTypes.ARRAY(Sequelize.DataTypes.STRING), + allowNull: true + } + }); + + let options = { + alter: false + }; + if (Config.config.alter_db) { + options.alter = true; + } + UnverifiedUser.sync(options); + User.sync(options); + Todo.sync(options); + Grouping.sync(options); + + return { + user: User, + unverifiedUser: UnverifiedUser, + todo: Todo, + grouping: Grouping + }; +}; + +module.exports = models; diff --git a/backend/src/user.js b/backend/src/user.js index 2968380..151d1c3 100644 --- a/backend/src/user.js +++ b/backend/src/user.js @@ -1,5 +1,6 @@ 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'); @@ -12,6 +13,86 @@ 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') { @@ -19,18 +100,9 @@ async function get_user_details(id) { } console.log(`search for user with id ${id}`); if (!user_cache[id]) { - let user = await Database.schemas.user.findOne({ where: { id: id } }); - if (user === null) { - return undefined; - } - user_cache[user.id] = { - id: user.id, - email: user.email, - password_hash: user.password_hash - }; - email_cache[user.email] = user.id; + return await fetch_user({ id: id }) } - console.log(`returning ${JSON.stringify(user_cache[id])}`); + // console.log(`returning ${JSON.stringify(user_cache[id])}`); return user_cache[id]; } @@ -40,38 +112,21 @@ async function get_user_details_by_email(email) { } console.log(`search for user with email ${email}}`); if (!email_cache[email] || !user_cache[email_cache[email]]) { - let user = await Database.schemas.user.findOne({ where: { email: email } }); - if (user === null) { - return undefined; - } - user_cache[user.id] = { - id: user.id, - email: user.email, - password_hash: user.password_hash - }; - email_cache[user.email] = user.id; + return await fetch_user({ email: email }) } - console.log(`returning ${JSON.stringify(user_cache[email_cache[email]])}`); + // console.log(`returning ${JSON.stringify(user_cache[email_cache[email]])}`); return user_cache[email_cache[email]]; } -router.get('/byEmail/:email', async (req, res) => { - if (!req.params?.email) { - res.status(400).json({ - error: 'email is a required parameter' - }); +async function get_user_details_by_discord_id(id) { + if (!id || id === 'undefined') { + return undefined; } - 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); + 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( @@ -164,9 +219,8 @@ router.post('/signup', async (req, res) => { 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 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 = `