diff --git a/migrations/.gitkeep b/migrations/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/migrations/20170527035704-add-tokens-table.js b/migrations/20170527035704-add-tokens-table.js new file mode 100644 index 0000000..34ad73e --- /dev/null +++ b/migrations/20170527035704-add-tokens-table.js @@ -0,0 +1,34 @@ +'use strict'; + +var dbm; +var type; +var seed; + +/** + * We receive the dbmigrate dependency from dbmigrate initially. + * This enables us to not have to rely on NODE_PATH. + */ +exports.setup = function (options, seedLink) { + dbm = options.dbmigrate; + type = dbm.dataType; + seed = seedLink; +}; + +exports.up = function (db) { + return db.createTable("tokens", { + id: {type: 'int', primaryKey: true, autoIncrement: true, notNull: true}, + matrixUserId: {type: 'string', notNull: true}, + matrixServerName: {type: 'string', notNull: true}, + matrixAccessToken: {type: 'string', notNull: true}, + scalarToken: {type: 'string', notNull: true}, + expires: {type: 'timestamp', notNull: true} + }); +}; + +exports.down = function (db) { + return db.dropTable("tokens"); +}; + +exports._meta = { + "version": 1 +}; diff --git a/package.json b/package.json index 5f8f8d0..0aed0f4 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "app.js", "license": "GPL-3.0", "scripts": { - "dev": "webpack-dev-server --inline --progress --port 8080", + "dev": "webpack-dev-server --inline --progress --port 8080 --host 0.0.0.0", "build": "rimraf web-dist && webpack --progress --profile --bail" }, "repository": { @@ -14,6 +14,7 @@ }, "author": "Travis Ralston", "dependencies": { + "body-parser": "^1.17.2", "chalk": "^1.1.3", "config": "^1.25.1", "db-migrate": "^0.10.0-beta.20", @@ -21,6 +22,8 @@ "express": "^4.15.2", "js-yaml": "^3.8.2", "moment": "^2.18.1", + "random-string": "^0.2.0", + "request": "^2.81.0", "sequelize": "^3.30.4", "sqlite3": "^3.1.8", "winston": "^2.3.1" diff --git a/src/Dimension.js b/src/Dimension.js index 4415402..ac89b0e 100644 --- a/src/Dimension.js +++ b/src/Dimension.js @@ -2,6 +2,10 @@ var express = require("express"); var config = require("config"); var log = require("./util/LogService"); var DimensionStore = require("./storage/DimensionStore"); +var bodyParser = require('body-parser'); +var path = require("path"); +var MatrixLiteClient = require("./matrix/MatrixLiteClient"); +var randomString = require("random-string"); /** * Primary entry point for Dimension @@ -16,6 +20,27 @@ class Dimension { this._db = db; this._app = express(); this._app.use(express.static('web-dist')); + this._app.use(bodyParser.json()); + + // Register routes for angular app + this._app.get(['/riot/*', '/app/*', '/splash/*'], (req, res) => { + res.sendFile(path.join(__dirname, "..", "web-dist", "index.html")); + }); + + // Allow CORS + this._app.use((req, res, next) => { + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); + next(); + }); + + // Logging incoming requests + this._app.use((req, res, next) => { + log.verbose("Dimension", "Incoming: " + req.method + " " + req.url); + next(); + }); + + this._app.post("/api/v1/scalar/register", this._scalarRegister.bind(this)); } start() { @@ -23,6 +48,25 @@ class Dimension { log.info("Dimension", "API and UI listening on " + config.get("web.address") + ":" + config.get("web.port")); } + _scalarRegister(req, res) { + var tokenInfo = req.body; + if (!tokenInfo || !tokenInfo['access_token'] || !tokenInfo['token_type'] || !tokenInfo['matrix_server_name'] || !tokenInfo['expires_in']) { + res.status(400).send('Missing OpenID'); + return; + } + + var client = new MatrixLiteClient(tokenInfo); + var scalarToken = randomString({length: 25}); + client.getSelfMxid().then(mxid => { + return this._db.createToken(mxid, tokenInfo, scalarToken); + }).then(() => { + res.setHeader("Content-Type", "application/json"); + res.send(JSON.stringify({scalar_token: scalarToken})); + }, err => { + throw err; + //res.status(500).send(err.message); + }); + } } module.exports = Dimension; \ No newline at end of file diff --git a/src/OpenID.js b/src/OpenID.js new file mode 100644 index 0000000..75e9d12 --- /dev/null +++ b/src/OpenID.js @@ -0,0 +1,11 @@ +/** + * Serves the purpose of being a documentation endpoint + */ +class OpenID { + access_token = ""; + token_type = ""; + matrix_server_name = ""; + expires_in = 0; +} + +module.exports = OpenID; \ No newline at end of file diff --git a/src/matrix/MatrixLiteClient.js b/src/matrix/MatrixLiteClient.js new file mode 100644 index 0000000..78d4c9d --- /dev/null +++ b/src/matrix/MatrixLiteClient.js @@ -0,0 +1,53 @@ +var request = require('request'); +var log = require("../util/LogService"); + +/** + * Represents a lightweight matrix client with minimal functionality + */ +class MatrixLiteClient { + + /** + * Creates a new matrix client + * @param {OpenID} openId the open ID to use + */ + constructor(openId) { + this._openId = openId; + } + + /** + * Gets the Matrix User ID that owns this open ID + * @return {Promise} resolves to the mxid + */ + getSelfMxid() { + return this._do("GET", "/_matrix/federation/v1/openid/userinfo").then((response, body) => { + var json = JSON.parse(response.body); + return json['sub']; + }); + } + + _do(method, endpoint, qs = null, body = null) { + var url = "http://" + this._openId.matrix_server_name + endpoint; + + log.verbose("MatrixLiteClient", "Performing request: " + url); + + if (!qs) qs = {}; + qs['access_token'] = this._openId.access_token; + + var params = { + url: url, + method: method, + form: body, + qs: qs + }; + + return new Promise((resolve, reject) => { + request(params, (err, response, body) => { + if (err) { + reject(err); + } else resolve(response, body); + }); + }); + } +} + +module.exports = MatrixLiteClient; \ No newline at end of file diff --git a/src/storage/DimensionStore.js b/src/storage/DimensionStore.js index 5287297..8c76fa9 100644 --- a/src/storage/DimensionStore.js +++ b/src/storage/DimensionStore.js @@ -2,6 +2,7 @@ var DBMigrate = require("db-migrate"); var log = require("./../util/LogService"); var Sequelize = require('sequelize'); var dbConfig = require("../../config/database.json"); +var moment = require("moment"); /** * Primary storage for Dimension. @@ -54,9 +55,40 @@ class DimensionStore { _bindModels() { // Models + this.__Tokens = this._orm.import(__dirname + "/models/tokens"); // Relationships } + + /** + * Creates a new Scalar token + * @param {string} mxid the matrix user id + * @param {OpenID} openId the open ID + * @param {string} scalarToken the token associated with the user + * @returns {Promise<>} resolves when complete + */ + createToken(mxid, openId, scalarToken) { + return this.__Tokens.create({ + matrixUserId: mxid, + matrixServerName: openId.matrix_server_name, + matrixAccessToken: openId.access_token, + scalarToken: scalarToken, + expires: moment().add(openId.expires_in, 'seconds').toDate() + }); + } + + /** + * Checks to determine if a token is valid or not + * @param {string} scalarToken the scalar token to check + * @returns {Promise<>} resolves if valid, rejected otherwise + */ + checkToken(scalarToken) { + return this.__Tokens.find({where: {scalarToken: scalarToken}}).then(token => { + if (!token) return Promise.reject(); + //if (moment().isAfter(moment(token.expires))) return this.__Tokens.destroy({where: {id: token.id}}).then(() => Promise.reject()); + return Promise.resolve(); + }); + } } module.exports = DimensionStore; \ No newline at end of file diff --git a/src/storage/models/.gitkeep b/src/storage/models/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/storage/models/tokens.js b/src/storage/models/tokens.js new file mode 100644 index 0000000..b616583 --- /dev/null +++ b/src/storage/models/tokens.js @@ -0,0 +1,40 @@ +module.exports = function (sequelize, DataTypes) { + return sequelize.define('tokens', { + id: { + type: DataTypes.INTEGER, + allowNull: false, + autoIncrement: true, + primaryKey: true, + field: 'id' + }, + matrixUserId: { + type: DataTypes.STRING, + allowNull: false, + field: 'matrixUserId' + }, + matrixServerName: { + type: DataTypes.STRING, + allowNull: false, + field: 'matrixServerName' + }, + matrixAccessToken: { + type: DataTypes.STRING, + allowNull: false, + field: 'matrixAccessToken' + }, + scalarToken: { + type: DataTypes.STRING, + allowNull: false, + field: 'scalarToken' + }, + expires: { + type: DataTypes.TIME, + allowNull: false, + field: 'expires' + } + }, { + tableName: 'tokens', + underscored: false, + timestamps: false + }); +}; diff --git a/src/util/LogService.js b/src/util/LogService.js index 53cd3a9..e6c040f 100644 --- a/src/util/LogService.js +++ b/src/util/LogService.js @@ -69,7 +69,7 @@ var log = new winston.Logger({ }); function doLog(level, module, messageOrObject) { - if (typeof(messageOrObject) === 'object') + if (typeof(messageOrObject) === 'object' && !(messageOrObject instanceof Error)) messageOrObject = JSON.stringify(messageOrObject); var message = "[" + module + "] " + messageOrObject; log.log(level, message); diff --git a/webpack.config.js b/webpack.config.js index 10382f8..9138a61 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -23,7 +23,7 @@ module.exports = function () { config.output = { path: root('web-dist'), - publicPath: isProd ? '/' : 'http://localhost:8080', + publicPath: isProd ? '/' : '/', //http://0.0.0.0:8080', filename: isProd ? 'js/[name].[hash].js' : 'js/[name].js', chunkFilename: isProd ? '[id].[hash].chunk.js' : '[id].chunk.js' };