add frame express app

This commit is contained in:
2020-06-13 01:31:48 -06:00
parent bbe78747f3
commit d3eb93666e
29 changed files with 1670 additions and 0 deletions

57
frame/src/app.ts Normal file
View File

@ -0,0 +1,57 @@
import * as createError from 'http-errors';
import * as express from 'express';
import * as path from 'path';
import * as cookieParser from 'cookie-parser';
import * as logger from 'morgan';
import * as passport from 'passport';
import * as helmet from 'helmet';
require('./components/authentication/passportSetup'); // runs some passport initialization
import { sessionsSetup } from './components/sessions/sessions';
var indexRouter = require('./routes/index');
var usersRouter = require('./components/users/usersRoutes');
// SYSTEM-BUILDER-router.require
const debug = require('debug')('frame:app-root');
var app = express();
sessionsSetup(app);
app.use(helmet());
app.use(passport.initialize());
app.use(passport.session());
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
app.use('/api/v1/users', usersRouter);
// SYSTEM-BUILDER-app.use
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;

95
frame/src/bin/www.ts Normal file
View File

@ -0,0 +1,95 @@
#!/usr/bin/env node
import '../env';
import { appReady } from '../initializeServices';
/**
* Module dependencies.
*/
var app = require('../app');
var debug = require('debug')('frame:server');
var http = require('http');
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
appReady.then(() => {
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
});
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}

View File

@ -0,0 +1,17 @@
import BergxSDK, { TryResponse } from 'bergx-sdk';
import { userService } from '../users/userService';
import { DBUser } from '../users/usersMapper';
const bergxService = new BergxSDK({
clientId: process.env.BX_CLIENT_ID,
clientSecret: process.env.BX_CLIENT_SECRET,
updateAccessTokenCallback: (userSub: string, newAccessToken: string) => {
// finds the user in the database and updates the accessToken
userService.find(userSub).then((user: DBUser) => {
user.accessToken = newAccessToken;
userService.update(user);
});
}
});
export default bergxService;

View File

@ -0,0 +1,20 @@
import * as express from 'express';
import * as passport from 'passport';
var router = express.Router();
const debug = require('debug')('frame:passportSetup');
router.get('/callback', [
passport.authenticate('oauth2', { failureRedirect: '/login' }),
(req, res, next) => {
res.redirect('/');
}
]);
router.get('/logout', (req, res, next) => {
req.session.destroy((err) => {
res.redirect('/');
});
});
module.exports = router;

View File

@ -0,0 +1,43 @@
import * as passport from 'passport';
import * as refresh from 'passport-oauth2-refresh';
import * as OAuth2Strategy from 'passport-oauth2';
import { userService } from '../users/userService';
passport.serializeUser((user, done) => {
done(null, user.sub);
});
passport.deserializeUser((sub, done) => {
userService.find(sub).then((user) => {
if (typeof user !== 'undefined') {
done(null, user);
} else {
done('User not found');
}
});
});
var strategy = new OAuth2Strategy({
authorizationURL: process.env.AUTHORIZATION_URL,
tokenURL: process.env.TOKEN_URL,
clientID: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
callbackURL: process.env.CALLBACK_URL,
state: true,
pkce: true
},
function(accessToken, refreshToken, params, _, cb) {
console.log(params);
userService.findOrCreate(params.profile).then((user) => {
user.refreshToken = refreshToken;
user.accessToken = accessToken;
return cb(null, user);
}).catch((err) => {
return cb(err);
});
}
);
passport.use(strategy);
refresh.use(strategy);

View File

@ -0,0 +1,42 @@
import * as session from 'express-session';
import * as mySQLSession from 'express-mysql-session';
const debug = require('debug')('frame:session');
function sessionsSetup(app) {
// setup sessions
let MySQLStore = mySQLSession(session);
const DOMAIN = process.env.DOMAIN || 'localhost';
debug(`Domain: ${DOMAIN}`);
let mysqlSessionOptions = {
host: process.env.DB_HOST,
port: process.env.DB_PORT || 3306,
user: process.env.SESSION_DB_USER,
password: process.env.SESSION_DB_PASSWORD,
database: process.env.SESSION_DB_NAME
};
let sessionStore = new MySQLStore(mysqlSessionOptions);
let sess = {
secret: process.env.SESSION_SECRET, // can be an array. Use an array when rolling this key
cookie: {
secure: false,
domain: DOMAIN
},
resave: false,
store: sessionStore,
saveUninitialized: false
};
debug(`Environment: ${app.get('env')}`);
if (app.get('env') === 'production') {
app.set('trust proxy', 1) // trust first proxy (usually nginx or loadbalancer)
// sess.cookie.secure = true // serve secure cookies
sess.cookie.secure = false // serve insecure cookies
}
app.use(session(sess));
}
export {
sessionsSetup
}

View File

@ -0,0 +1,45 @@
import { db } from '../../databases';
import { UsersMapper, DBUser } from './usersMapper';
class UserService {
private userMapper = new UsersMapper(db);
public initialize() {
return Promise.resolve();
}
public create(profile: DBUser) {
return this.userMapper.createUser(profile.sub, profile.email, profile.accessToken, profile.refreshToken);
}
public find(sub: string): Promise<DBUser> {
return this.userMapper.findUserBySub(sub).then((matchingUsers) => {
if (matchingUsers.length) {
return matchingUsers[0];
} else {
throw new Error('User Not Found');
}
});
}
public update(profile: DBUser) {
return this.userMapper.updateUser(profile.sub, profile.email, profile.accessToken, profile.refreshToken);
}
public findOrCreate(profile: DBUser) {
return this.find(profile.sub).then((matchingUser: DBUser) => {
return this.update(profile);
}).catch((err) => {
return this.create(profile);
});
}
// SYSTEM-BUILDER-userService
}
const userService = new UserService();
export {
userService
}

View File

@ -0,0 +1,29 @@
import { Mapper } from '../../db/Mapper';
class UsersMapper extends Mapper {
public findUserBySub(sub: string): Promise<DBUser[]> {
return super.runQuery('SELECT * FROM Users WHERE sub = ?', sub);
}
public createUser(sub: string, email: string, accessToken: string, refreshToken: string) {
}
public updateUser(sub: string, email: string, accessToken: string, refreshToken: string) {
}
}
interface DBUser {
sub: string;
email: string;
accessToken: string;
refreshToken: string;
}
export {
UsersMapper,
DBUser
}

View File

@ -0,0 +1,25 @@
import * as express from 'express';
import * as passport from 'passport';
import bergxService from '../Bergx/bergxService';
var router = express.Router();
const debug = require('debug')('frame:usersRoutes');
router.get('/profile/me', [
passport.authenticate('oauth2'),
(req, res, next) => {
bergxService.getProfile(req.user).then((info) => {
res.status(200);
res.json(info);
}).catch((err) => {
res.status(500);
res.json({
status: 'error',
message: err.message
});
});
}
]);
module.exports = router;

View File

@ -0,0 +1,9 @@
import { Mapper } from '../../db/Mapper';
class {{Component}}Mapper extends Mapper {
// SYSTEM-BUILDER-{{component}}-mapper
}
export {
{{Component}}Mapper
}

View File

@ -0,0 +1,10 @@
import * as express from 'express';
import * as passport from 'passport';
var router = express.Router();
const debug = require('debug')('frame:{{component}}Routes');
// SYSTEM-BUILDER-{{component}}-routes
module.exports = router;

View File

@ -0,0 +1,18 @@
import { db } from '../../databases';
import { {{Component}}Mapper } from './componentMapper';
class {{Component}}Service {
private {{component}}Mapper = new {{Component}}Mapper(db);
public initialize() {
return Promise.resolve();
}
// SYSTEM-BUILDER-{{component}}-service
}
const {{component}}Service = new {{Component}}Service();
export {
{{component}}Service
}

24
frame/src/databases.ts Normal file
View File

@ -0,0 +1,24 @@
import { Database } from './db/db';
import { Mapper } from './db/Mapper';
import { Altitude, Job } from './db/migrations/Altitude';
import jobs from './migrationJobs';
let db = new Database({
host: process.env.DB_HOST,
port: process.env.DB_PORT || '3306',
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
});
let mapper = new Mapper(db);
let altitude = new Altitude(mapper);
jobs.map((job) => {
altitude.addMigration(job);
});
export {
altitude,
db,
mapper
};

25
frame/src/db/Mapper.ts Normal file
View File

@ -0,0 +1,25 @@
import { Database } from '../db/db';
class Mapper {
constructor(public database: Database) {}
public runQuery(query: string, values: any): Promise<any[]> {
let defer = new Promise<any[]>((resolve, reject) => {
let connection = this.database.getConnection();
connection.connect();
connection.query(query, values, (error, results, fields) => {
if (error) {
reject(error);
} else {
resolve(results);
}
});
connection.end();
});
return defer;
}
}
export {
Mapper
};

23
frame/src/db/db.ts Normal file
View File

@ -0,0 +1,23 @@
import * as mysql from 'mysql';
interface DbInfo {
host: string;
port: string;
user: string;
password: string;
database: string;
}
class Database {
constructor(public connectionOptions: DbInfo) {}
public getConnection() {
return mysql.createConnection(this.connectionOptions);
}
}
export {
DbInfo,
Database
};

View File

@ -0,0 +1,243 @@
import { Mapper } from '../Mapper';
class Altitude {
private migrations: Job[];
private ready: Promise<any>;
constructor(private mapper: Mapper) {
let self = this;
this.migrations = [];
this.ready = new Promise((resolve, reject) => {
this.shouldInitDB().then((shouldInit: boolean) => {
if (shouldInit) {
console.log(`Initializing ${this.mapper.database.connectionOptions.database}`);
this.mapper.runQuery(`CREATE TABLE CHANGE_LOG (
id int NOT NULL AUTO_INCREMENT,
name VARCHAR(255),
creator VARCHAR(255),
UpdatedTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);`, []).then(() => {
console.log(`Created CHANGE_LOG table.`);
resolve(true);
});
} else {
resolve(true);
}
}).catch((err) => {
console.log(err)
});
});
}
public addMigration(newMigration: Job) {
if (this.validateJob(newMigration)) {
this.migrations.push(newMigration);
} else {
throw `The provided migration job (${newMigration.name}) is not valid.`;
}
}
public removeMigration(migrationName: string) {
this.migrations = this.migrations.filter((migration: Job) => {
return migration.name !== migrationName;
});
}
public runMigrationByName(jobName: string) {
let migrationsToRun: Job[] = this.migrations.filter((job: Job) => {
return job.name === jobName;
});
return this.runMigrations(migrationsToRun);
}
public runMigrations(migrationsToRun: Job[] = this.migrations) {
let self = this;
let defer = this.ready;
migrationsToRun.forEach((job: Job) => {
defer = defer.then((): Promise<any> => {
return self.runMigration(job);
});
});
return defer;
}
private validateJob(job: Job) {
let valid = true;
if (job.preCondition) {
valid = valid && typeof job.preCondition === 'string';
}
if (job.preConditionFieldName) {
valid = valid && typeof job.preConditionFieldName === 'string';
}
if (job.preConditionValidation) {
valid = valid && typeof job.preConditionValidation === 'function';
}
if (job.postCondition) {
valid = valid && typeof job.postCondition === 'string';
}
if (job.postConditionFieldName) {
valid = valid && typeof job.postConditionFieldName === 'string';
}
if (job.postConditionValidation) {
valid = valid && typeof job.postConditionValidation === 'function';
}
if (job.rollback) {
valid = valid && typeof job.rollback === 'string';
}
valid = valid && typeof job.name === 'string';
valid = valid && typeof job.query === 'string';
valid = valid && typeof job.creator === 'string';
return valid;
}
private shouldInitDB() {
return this.mapper.runQuery(`SELECT table_name FROM information_schema.tables WHERE table_schema = '${this.mapper.database.connectionOptions.database}' AND table_name = 'CHANGE_LOG';`, [])
.then((results: any[]) => {
return results.length === 0;
});
}
private alreadyRun(migration: Job) {
return this.mapper.runQuery(`SELECT name FROM CHANGE_LOG WHERE name = '${migration.name}';`, [])
.then((results: any[]) => {
return results.length !== 0;
});
}
private runMigration(migration: Job) {
let self = this;
return this.alreadyRun(migration)
.then((alreadyRun: boolean) => {
if (alreadyRun) {
return `Skipping migration ${migration.name} because it has already been run.`;
} else {
return doMigration();
}
});
function doMigration() {
return self.runPreCondition(migration)
.then((preConditionResult) => {
if (preConditionResult) {
let defer: Promise<any> = Promise.resolve();
let queries: string[] = migration.query.split(';');
queries = queries.filter((query) => query.length);
queries.forEach((query: string) => {
defer = defer.then((result) => {
if (result) console.log(result);
return self.mapper.runQuery(query, []);
});
});
return defer;
} else {
throw 'PreCondition was not met.';
}
})
.then((mainQueryResult) => {
console.log(mainQueryResult);
return self.runPostCondition(migration);
})
.then((postConditionResult) => {
if (!postConditionResult) {
return self.runRollback(migration);
} else {
return self.recordRun(migration)
.then(() => {
return 'Success';
});
}
})
.catch((error) => {
console.log(error);
});
}
}
private recordRun(migration: Job) {
return this.mapper.runQuery(`INSERT INTO CHANGE_LOG SET ?;`, [new ChangeLog(migration)]);
}
private runPreCondition(migration: Job) {
let defer = new Promise((resolve, reject) => {
if (migration.preCondition) {
this.mapper.runQuery(migration.preCondition, []).then((result) => {
console.log(result);
let isValid = typeof migration.preConditionValidation === 'function' ? migration.preConditionValidation(result) : result.length > 0 && result[0][migration.preConditionFieldName || 'precondition'] !== 0;
console.log(isValid);
resolve(isValid);
});
} else {
resolve(true);
}
});
return defer;
}
private runPostCondition(migration: Job) {
let defer = new Promise((resolve, reject) => {
if (migration.postCondition) {
this.mapper.runQuery(migration.postCondition, []).then((result) => {
console.log(result);
console.log(result[0][migration.postConditionFieldName || 'postcondition'] !== 0);
resolve(result[0][migration.postConditionFieldName || 'postcondition'] !== 0);
});
} else {
resolve(true);
}
});
return defer;
}
private runRollback(migration: Job) {
let defer = new Promise((resolve, reject) => {
if (migration.rollback) {
this.mapper.runQuery(migration.rollback, []).then((result) => {
resolve(result);
}).catch((error) => {
reject(error);
});
} else {
resolve(true);
}
});
return defer;
}
}
interface ValidationFunction {
(results: any[]): boolean;
}
interface Job {
preCondition?: string;
preConditionFieldName?: string;
preConditionValidation?: ValidationFunction;
postCondition?: string;
postConditionFieldName?: string;
postConditionValidation?: ValidationFunction;
rollback?: string;
name: string;
query: string;
creator: string;
}
class ChangeLog {
public name: string;
public creator: string;
constructor(migration: Job) {
this.name = migration.name;
this.creator = migration.creator;
}
}
export {
Altitude,
Job
}

View File

@ -0,0 +1,42 @@
# Altitude
Altitude is a custom migration tool for updating a database with migrations. The only dependency is the MySQL library. Migrations are simple. Just create jobs for Altitude to do.
```Typescript
interface Job {
preCondition?: string;
preConditionFieldName?: stirng;
postCondition?: string;
postConditionFieldName?: stirng;
rollback?: string;
name: string;
query: string;
}
```
Then add the job to Altitude and tell it to run the migration.
```Typescript
let altitude = new Altitude();
let job: Job = {
name: 'test',
query: 'CREATE TABLE test_table (test VARCHAR(20));'
};
altitude.addMigration(job);
altitude.runMigrationByName('test');
```
All options are optional except for `name` and `query`. `query` can have multiple queries in the string as long as each are separated with a semicolon `;`. `name` must be unique otherwise only the first migration with that name will ever be run.
```Typescript
// optional properties
preCondition: `SELECT @@version NOT LIKE '5.0%' AND @@version NOT LIKE '5.1%' AS precondition;`, // query ran before the migration to check the current state of the database.
preConditionFieldName: 'precondition', // if the field in the precondition that needs to be checked isn't named precondition you can specify the correct field name here.
postCondition: `SELECT COUNT(*) as postcondition FROM StormTest.Users;` // same thing as precondition except run after the migration
postConditionFieldName: string; // same thing as the preConditionFieldName except for the postCondition
rollback: `DROP TABLE test_table;` // query that is run if the post condition fails.
```

2
frame/src/env.ts Normal file
View File

@ -0,0 +1,2 @@
import { config } from 'dotenv';
config({ silent: true })

View File

@ -0,0 +1,18 @@
import { userService } from './components/users/userService';
// SYSTEM-BUILDER-initialize-import
import { altitude } from './databases';
const appReady = altitude.runMigrations().then(() => {
let servicesPromise = [
userService.initialize(),
// SYSTEM-BUILDER-initialize-service
];
return Promise.all(servicesPromise);
});
export {
appReady
}

View File

@ -0,0 +1,12 @@
import { Job } from '../db/migrations/Altitude';
let jobs: Job[] = [];
jobs.push({
creator: 'Mason Payne',
name: 'initializeDB',
query: `CREATE TABLE Users (sub VARCHAR(36) NOT NULL UNIQUE, email VARCHAR(255) NOT NULL UNIQUE, accessToken VARCHAR(255), refreshToken VARCHAR(255), CreationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (sub));`
});
// SYSTEM-BUILDER-jobs.push
export default jobs;

View File

@ -0,0 +1,8 @@
body {
padding: 50px;
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}
a {
color: #00B7FF;
}

View File

@ -0,0 +1,9 @@
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
module.exports = router;

View File

@ -0,0 +1,3 @@
<h1><%= message %></h1>
<h2><%= error.status %></h2>
<pre><%= error.stack %></pre>

11
frame/src/views/index.ejs Normal file
View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<h1><%= title %></h1>
<p>Welcome to <%= title %></p>
</body>
</html>