+ + + + ); + } +} diff --git a/pages/_error.tsx b/pages/_error.tsx new file mode 100644 index 0000000..8460224 --- /dev/null +++ b/pages/_error.tsx @@ -0,0 +1,75 @@ +import c from 'classnames'; +import HTTPStatus from 'http-status'; +import React, {PureComponent} from 'react'; + +const bulma = require('../styles/global.scss'); + +export default class Page extends PureComponent { + public static getInitialProps({res, err, query}) { + const statusCode = res ? res.statusCode : err ? err.statusCode : null; + + return {statusCode, query}; + } + + public render() { + const {statusCode} = this.props; + const title = statusCode === 404 + ? 'This page could not be found' + : HTTPStatus[statusCode] || 'An unexpected error has occurred'; + + return ( +
+
+
+ {statusCode ?

{statusCode}

: null} +
+

{title}.

+
+
+
+
+ ); + } +} + +const styles = { + error: { + color: '#000', + background: '#fff', + height: '100vh', + textAlign: 'center', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + }, + + desc: { + display: 'inline-block', + textAlign: 'left', + lineHeight: '49px', + height: '49px', + verticalAlign: 'middle', + }, + + h1: { + display: 'inline-block', + borderRight: '1px solid rgba(0, 0, 0,.3)', + margin: 0, + marginRight: '20px', + padding: '10px 23px 10px 0', + fontSize: '24px', + fontWeight: 500, + verticalAlign: 'top', + color: 'black', + }, + + h2: { + fontSize: '14px', + fontWeight: 'normal', + lineHeight: 'inherit', + margin: 0, + padding: 0, + color: 'black', + }, +}; diff --git a/pages/index.tsx b/pages/index.tsx new file mode 100644 index 0000000..ed60884 --- /dev/null +++ b/pages/index.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; + +export default class extends React.Component { + public render() { + return ( +
+ Hello World! +
+ ); + } +} diff --git a/server/Controller/IndexController.ts b/server/Controller/IndexController.ts new file mode 100644 index 0000000..84b74b9 --- /dev/null +++ b/server/Controller/IndexController.ts @@ -0,0 +1,9 @@ +import {BaseHttpController, controller, httpGet, results} from 'inversify-express-utils'; + +@controller('/') +export class IndexController extends BaseHttpController { + @httpGet('/status') + private index(): results.JsonResult { + return this.json({status: 'ok'}); + } +} diff --git a/server/Controller/index.ts b/server/Controller/index.ts new file mode 100644 index 0000000..5150456 --- /dev/null +++ b/server/Controller/index.ts @@ -0,0 +1 @@ +export {default as IndexController} from './IndexController'; diff --git a/server/Kernel.ts b/server/Kernel.ts new file mode 100644 index 0000000..35c0a2a --- /dev/null +++ b/server/Kernel.ts @@ -0,0 +1,177 @@ +import * as bodyParser from 'body-parser'; +import * as ConnectDynamo from 'connect-dynamodb'; +import * as session from 'express-session'; +import {Container} from 'inversify'; +import {InversifyExpressServer} from 'inversify-express-utils'; +import * as morgan from 'morgan'; +import {Server} from 'next'; +import * as passport from 'passport'; +import {Strategy as OAuth2Strategy} from 'passport-oauth2'; +import * as request from 'request'; +import {Connection, createConnection} from 'typeorm'; +import {createLogger, format, Logger, transports} from 'winston'; + +import Types from './types'; +import {Config, Vault} from './Vault'; + +export default async (app: Server) => { + const container: Container = new Container({defaultScope: 'Singleton'}); + const server = new InversifyExpressServer(container); + const DynamoDBStore = ConnectDynamo({session}); + const store = new DynamoDBStore({table: process.env.SESSION_TABLE}); + const url = process.env.IS_OFFLINE ? 'http://localhost:3000' : 'https://apply.hotline.gg'; + server.setConfig((_app) => { + _app.use(bodyParser.urlencoded({extended: true})) + .use(bodyParser.json()) + .use(morgan('dev')) + .use((req, res, nextReq) => { + if (process.env.NODE_ENV === 'production') { + if (req.protocol === 'https' || req.headers['x-forwarded-proto'] === 'https') { + return nextReq(); + } + + return res.redirect('https://' + req.hostname + req.originalUrl); + } + }); + + const cookie: { maxAge: number, domain?: string, secure?: boolean } = { + maxAge: 1000 * 60 * 60 * 24 * 7, + secure: undefined, + }; + if (process.env.NODE_ENV === 'production') { + cookie.domain = 'www.hotline.gg'; + cookie.secure = true; + _app.set('trust proxy', 1); + } + + _app.use(session({ + secret: '9a7sd79asrh99a9', + saveUninitialized: true, + resave: false, + rolling: true, + cookie, + store, + })); + + _app.use(passport.initialize()); + _app.use(passport.session()); + }); + + // Logger + container.bind(Types.logger).toDynamicValue(() => createLogger({ + level: process.env.DEBUG || false ? 'debug' : 'info', + format: format.combine( + format.splat(), + format.colorize(), + format.timestamp(), + format.align(), + format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`), + ), + transports: [ + new transports.Console(), + ], + })); + + // Vault + container.bind(Types.vault.config).toConstantValue({ + vaultFile: process.env.VAULT_FILE, + address: process.env.VAULT_ADDR, + rootToken: process.env.VAULT_TOKEN, + roleId: process.env.VAULT_ROLE_ID, + secretId: process.env.VAULT_SECRET_ID, + }); + container.bind(Types.vault.client).to(Vault); + const vault = container.get(Types.vault.client); + await vault.initialize(); + + container.bind(Types.next.app).toConstantValue(app); + container.bind(Types.next.handler).toFunction(app.getRequestHandler); + await import('./Controller'); + + // Database/TypeORM + const connection = await createConnection({ + synchronize: true, + host: await vault.getSecret('database', 'host'), + database: await vault.getSecret('api/database', 'name'), + port: 3306, + username: await vault.getSecret('api/database', 'user'), + password: await vault.getSecret('api/database', 'password'), + type: 'mysql', + supportBigNumbers: true, + logger: this.logger, + bigNumberStrings: true, + entities: [], + }); + container.bind(Types.database).toConstantValue(connection); + + const expressApp = server.build(); + + const baseUrl = `https://discordapp.com/api`; + const authType = `discord`; + // Passport Initialization + passport.use(authType, new OAuth2Strategy( + { + authorizationURL: `${baseUrl}/oauth2/authorize`, + tokenURL: `${baseUrl}/oauth2/token`, + clientID: await vault.getSecret('discord', 'client_id'), + clientSecret: await vault.getSecret('discord', 'secret'), + callbackURL: url + '/connect/callback', + }, + function(accessToken, refreshToken, empty, cb) { + request.get( + `${baseUrl}/users/@me`, { + headers: { + Authorization: 'Bearer ' + accessToken, + }, + }, + function(err, response, body) { + if (err) { + console.error(err); + + return cb(err); + } + + const profile = JSON.parse(body); + profile.accessToken = accessToken; + profile.refreshToken = refreshToken; + + cb(undefined, profile); + }, + ); + }, + )); + + expressApp.get(`/connect`, (req, res, next) => { + const scope = ['identify', ...req.query.scopes ? req.query.scopes.split(',') : []]; + + return passport.authenticate(authType, {scope})(req, res, next); + }); + expressApp.get( + `/connect/callback`, + (req: any, res: any) => passport.authenticate(authType, (err, user) => { + if (err) { + console.error(err); + + return res.statusCode(500).send(err.message); + } + + req.logIn(user, (error) => { + if (error) { + console.error(error); + + return res.statusCode(500).send(error.message); + } + const redirect = req.session.lastUrl || url; + delete req.session.lastUrl; + + res.redirect(redirect); + }); + })(req, res), + ); + + passport.serializeUser((user, done) => done(null, user)); + passport.deserializeUser((user, done) => done(null, user)); + expressApp.get('*', app.getRequestHandler() as any); + + return expressApp; +}; diff --git a/server/Vault/Config.ts b/server/Vault/Config.ts new file mode 100644 index 0000000..45111da --- /dev/null +++ b/server/Vault/Config.ts @@ -0,0 +1,11 @@ +export default class Config { + public vaultFile?: string = undefined; + + public address?: string = undefined; + + public rootToken?: string = undefined; + + public roleId?: string = undefined; + + public secretId?: string = undefined; +} diff --git a/server/Vault/Vault.ts b/server/Vault/Vault.ts new file mode 100644 index 0000000..752d128 --- /dev/null +++ b/server/Vault/Vault.ts @@ -0,0 +1,96 @@ +import {readFileSync} from 'fs'; +import {inject, injectable} from 'inversify'; +import * as NodeVault from 'node-vault'; + +import Types from '../types'; +import Config from './Config'; + +interface Path { + insert: number; + data: { + [key: string]: string, + }; +} + +@injectable() +export default class Vault { + private vault: NodeVault.client; + + private paths: { [key: string]: Path } = {}; + + constructor( + @inject(Types.vault.config) private config: Config, + ) { + if (config.address) { + this.vault = NodeVault({endpoint: config.address, token: config.rootToken}); + } + } + + public async initialize() { + if (this.config.address && this.config.roleId && this.config.secretId) { + await this.vault.approleLogin({role_id: this.config.roleId, secret_id: this.config.secretId}); + } + } + + public async getSecret( + path: string, + field: string, + cache: boolean = true, + withPrefix: boolean = true, + ttl: number = 60 * 5, + ): Promise { + try { + return (await this.getSecrets(path, cache, withPrefix, ttl))[field]; + } catch (e) { + console.error( + 'Failed fetching secret %s from path %s. Original Error: %s\n%s', + field, + withPrefix ? 'secret/hotline/' + path : path, + e.message, + e.stack, + ); + + throw e; + } + } + + public async getSecrets( + path: string, + cache: boolean = true, + withPrefix: boolean = true, + ttl: number = 60 * 5, + ): Promise<{ [key: string]: string }> { + try { + return await this.getPath(withPrefix ? 'secret/hotline/' + path : path, cache, ttl); + } catch (e) { + console.error( + 'Failed fetching secret path %s. Original Error: %s\n%s', + withPrefix ? 'secret/hotline/' + path : path, + e.message, + e.stack, + ); + + throw e; + } + } + + private async getPath(path: string, cache: boolean = false, ttl: number = 60 * 5) { + if (cache && this.paths[path] && this.paths[path].insert + (ttl * 1000) < Date.now()) { + return this.paths[path]; + } + + let value; + if (this.config.vaultFile) { + const fileContents = JSON.parse(readFileSync(this.config.vaultFile, 'utf8')); + value = fileContents[path]; + } else { + value = (await this.vault.read(path)).data; + } + + if (cache) { + this.paths[path] = {insert: Date.now(), data: value}; + } + + return value; + } +} diff --git a/server/Vault/index.ts b/server/Vault/index.ts new file mode 100644 index 0000000..a09a1e2 --- /dev/null +++ b/server/Vault/index.ts @@ -0,0 +1,2 @@ +export {default as Vault} from './Vault'; +export {default as Config} from './Config'; diff --git a/server/index.ts b/server/index.ts new file mode 100644 index 0000000..78ae4a1 --- /dev/null +++ b/server/index.ts @@ -0,0 +1,42 @@ +import {Application} from 'express'; +import * as next from 'next'; +import * as nextServerless from 'next-serverless/handler'; +import 'reflect-metadata'; +import 'source-map-support/register'; + +import Kernel from './Kernel'; + +const dev = process.env.NODE_ENV !== 'production'; +if (process.env.IS_OFFLINE) { + process.env.LAMBDA_TASK_ROOT = 'true'; + process.env.AWS_EXECUTION_ENV = 'true'; +} + +declare global { + namespace NodeJS { + interface Global { + app: next.Server; + kernel: Application; + handler: any; + } + } +} + +global.app = next({dev}); +module.exports.handler = async (event, context, callback) => { + if (!global.kernel) { + global.kernel = await Kernel(global.app); + } + global.handler = nextServerless(global.app, global.kernel); + + try { + console.log('Getting Response'); + debugger; + const response = await global.handler(event, context); + console.log('Response: ', {response}); + + return response; + } catch (e) { + console.error({e}); + } +}; diff --git a/server/types.ts b/server/types.ts new file mode 100644 index 0000000..175f572 --- /dev/null +++ b/server/types.ts @@ -0,0 +1,19 @@ +const Types = { + database: Symbol('database'), + discord: { + token: Symbol('discord.token'), + options: Symbol('discord.options'), + client: Symbol('discord.client'), + }, + logger: Symbol('logger'), + next: { + app: Symbol('next.app'), + handler: Symbol('next.handler'), + }, + vault: { + client: Symbol('vault.client'), + config: Symbol('vault.config'), + }, +}; + +export default Types; diff --git a/static/hotline.png b/static/hotline.png new file mode 100644 index 0000000000000000000000000000000000000000..963212cadfb0fec2c958f9d4dd6ae5fb090c9ab1 GIT binary patch literal 13904 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 NqkCZIGo6!{{tr52|1|&r literal 0 HcmV?d00001 diff --git a/static/hotline.svg b/static/hotline.svg new file mode 100644 index 0000000..a50417a --- /dev/null +++ b/static/hotline.svg @@ -0,0 +1,58 @@ + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/static/logo.png b/static/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..220a596d334696920139aa1dd26d6dd96c7897a0 GIT binary patch literal 19969 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 literal 0 HcmV?d00001 diff --git a/styles/Layout.scss b/styles/Layout.scss new file mode 100644 index 0000000..44c7ce2 --- /dev/null +++ b/styles/Layout.scss @@ -0,0 +1,28 @@ +@import "variables"; + +.content { + padding-top: 2rem; +} + +.footer { + composes: footer from "./global.scss"; + flex: 1; + background: transparent; + color: $white; + + a { + color: $primary; + } +} + +.notice { + color: $white; + text-shadow: 1px 1px 3px rgba($primary, .3); + + a { + color: $primary; + &:hover { + color: darken($primary, 20%); + } + } +} diff --git a/styles/global.scss b/styles/global.scss new file mode 100644 index 0000000..e481ccd --- /dev/null +++ b/styles/global.scss @@ -0,0 +1,44 @@ +@import "variables"; +@import "~bulma"; +@import "overrides"; +@import "./mixins"; + +a.title { + color: $light; + + &:hover { + color: $primary; + border-bottom: 2px solid $primary; + + } +} + +body::-webkit-scrollbar-track { + display: none; +} + +body::-webkit-scrollbar { + display: none; +} + +body::-webkit-scrollbar-thumb { + display: none; +} + +.is-transparent { + background: transparent; + border: none; + cursor: default; + + &:active, &:focus, &:hover { + border: none; + color: initial; + outline: none; + } +} + +.button { + > svg { + margin-right: 5px; + } +} diff --git a/styles/mixins.scss b/styles/mixins.scss new file mode 100644 index 0000000..ee4fe0d --- /dev/null +++ b/styles/mixins.scss @@ -0,0 +1,90 @@ +/// Convert angle +/// @author Chris Eppstein +/// @param {Number} $value - Value to convert +/// @param {String} $unit - Unit to convert to +/// @return {Number} Converted angle +@function convert-angle($value, $unit) { + $convertable-units: deg grad turn rad; + $conversion-factors: 1 (10grad/9deg) (1turn/360deg) (3.1415926rad/180deg); + @if index($convertable-units, unit($value)) and index($convertable-units, $unit) { + @return $value + / nth($conversion-factors, index($convertable-units, unit($value))) + * nth($conversion-factors, index($convertable-units, $unit)); + } + + @warn "Cannot convert `#{unit($value)}` to `#{$unit}`."; +} +/// Test if `$value` is an angle +/// @param {*} $value - Value to test +/// @return {Bool} +@function is-direction($value) { + $is-direction: index((to top, to top right, to right top, to right, to bottom right, to right bottom, to bottom, to bottom left, to left bottom, to left, to left top, to top left), $value); + $is-angle: type-of($value) == 'number' and index('deg' 'grad' 'turn' 'rad', unit($value)); + @return $is-direction or $is-angle; +} +/// Convert a direction to legacy syntax +/// @param {Keyword | Angle} $value - Value to convert +/// @require {function} is-direction +/// @require {function} convert-angle +@function legacy-direction($value) { + @if is-direction($value) == false { + @warn "Cannot convert `#{$value}` to legacy syntax because it doesn't seem to be an angle or a direction"; + } + + $conversion-map: ( + to top : bottom, + to top right : bottom left, + to right top : left bottom, + to right : left, + to bottom right : top left, + to right bottom : left top, + to bottom : top, + to bottom left : top right, + to left bottom : right top, + to left : right, + to left top : right bottom, + to top left : bottom right + ); + + @if map-has-key($conversion-map, $value) { + @return map-get($conversion-map, $value); + } + + @return 90deg - convert-angle($value, 'deg'); +} +/// Mixin printing a linear-gradient +/// as well as a plain color fallback +/// and the `-webkit-` prefixed declaration +/// @access public +/// @param {String | List | Angle} $direction - Linear gradient direction +/// @param {Arglist} $color-stops - List of color-stops composing the gradient +@mixin linear-gradient($direction, $color-stops...) { + @if is-direction($direction) == false { + $color-stops: ($direction, $color-stops); + $direction: 180deg; + } + + background: nth(nth($color-stops, 1), 1); + background: -webkit-linear-gradient(legacy-direction($direction), $color-stops); + background: linear-gradient($direction, $color-stops); +} +@mixin box-shadow($color, $extra: 0rem) { + box-shadow: 0 0 (2rem + $extra) $color; +} +@mixin inset-box-shadow($color) { + box-shadow: inset 0 0 -28px $color; +} +@mixin radial-gradient($from, $to) { + background: -moz-radial-gradient(center, circle cover, $from 0%, $to 100%); + background: -webkit-gradient(radial, center center, 0px, center center, 100%, color-stop(0%, $from), color-stop(100%, $to)); + background: -webkit-radial-gradient(center, circle cover, $from 0%, $to 100%); + background: -o-radial-gradient(center, circle cover, $from 0%, $to 100%); + background: -ms-radial-gradient(center, circle cover, $from 0%, $to 100%); + background: radial-gradient(center, circle cover, $from 0%, $to 100%); + background-color: $from; +} + +@mixin animated($time) { + -webkit-animation-duration: $time; + animation-duration: $time +} diff --git a/styles/overrides.scss b/styles/overrides.scss new file mode 100644 index 0000000..af559f5 --- /dev/null +++ b/styles/overrides.scss @@ -0,0 +1 @@ +@import "~bulmaswatch/darkly/overrides"; diff --git a/styles/variables.scss b/styles/variables.scss new file mode 100644 index 0000000..8e42119 --- /dev/null +++ b/styles/variables.scss @@ -0,0 +1,3 @@ +@import "~bulmaswatch/darkly/variables"; +$white: $white-ter; +$navbar-height: 5rem; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..995b8b0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "target": "es2017", + "jsx": "react", + "module": "commonjs", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "removeComments": false, + "preserveConstEnums": true, + "sourceMap": true, + "skipLibCheck": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "noImplicitAny": false, + "strictNullChecks": true, + "lib": [ + "esnext", + "es2017", + "es2015", + "es6", + "es5", + "dom" + ], + "typeRoots": [ + "./node_modules/@types" + ], + "types": [ + "node", + "reflect-metadata" + ] + } +} diff --git a/tsconfig.server.json b/tsconfig.server.json new file mode 100644 index 0000000..96a78dc --- /dev/null +++ b/tsconfig.server.json @@ -0,0 +1,37 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "jsx": "preserve", + "allowJs": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "removeComments": false, + "preserveConstEnums": true, + "sourceMap": true, + "skipLibCheck": true, + "baseUrl": ".", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "noImplicitAny": false, + "outDir": ".next/", + "pretty": true, + "lib": [ + "es2017.object", + "es2015", + "es2017", + "esnext", + "dom" + ], + "typeRoots": [ + "./node_modules/@types" + ], + "types": [ + "node", + "reflect-metadata" + ] + } +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..c949373 --- /dev/null +++ b/tslint.json @@ -0,0 +1,43 @@ +{ + "extends": "tslint:recommended", + "rules": { + "semicolon": [ + true + ], + "curly": true, + "variable-name": [ + true, + "ban-keywords", + "check-format", + "allow-leading-underscore", + "allow-pascal-case" + ], + "align": [ + true, + "parameters", + "statements", + "arguments", + "members", + "elements" + ], + "quotemark": [ + true, + "single", + "jsx-double", + "avoid-escape" + ], + "only-arrow-functions": false, + "no-console": false, + "no-empty": false, + "no-eval": false, + "no-string-literal": false, + "object-literal-sort-keys": false, + "no-var-requires": false, + "interface-name": false, + "newline-before-return": true, + "no-bitwise": false + }, + "jsRules": { + "curly": true + } +}