├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── client ├── app-aot.ts ├── app.ts ├── index.html ├── loader.scss ├── modules │ ├── 404 │ │ ├── 404-routing.module.ts │ │ ├── 404.component.html │ │ ├── 404.component.scss │ │ ├── 404.component.ts │ │ └── 404.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.ts │ ├── app.module.ts │ ├── browser-app.module.ts │ ├── core │ │ ├── components │ │ │ ├── footer │ │ │ │ ├── footer.component.html │ │ │ │ ├── footer.component.scss │ │ │ │ └── footer.component.ts │ │ │ └── header │ │ │ │ ├── header.component.html │ │ │ │ ├── header.component.scss │ │ │ │ └── header.component.ts │ │ ├── core-routing.module.ts │ │ ├── core.component.html │ │ ├── core.component.scss │ │ ├── core.component.ts │ │ ├── core.module.ts │ │ └── services │ │ │ ├── auth │ │ │ ├── auth.service.ts │ │ │ └── token-interceptor.service.ts │ │ │ └── socketio │ │ │ └── socketio.service.ts │ ├── home │ │ ├── home-routing.module.ts │ │ ├── home.component.html │ │ ├── home.component.scss │ │ ├── home.component.ts │ │ └── home.module.ts │ ├── server-app.module.ts │ ├── shared │ │ └── shared.module.ts │ ├── transfer-state │ │ ├── browser-transfer-state.module.ts │ │ ├── server-transfer-state.module.ts │ │ ├── server-transfer-state.ts │ │ └── transfer-state.ts │ └── user-profile │ │ ├── user-profile-routing.module.ts │ │ ├── user-profile.component.html │ │ ├── user-profile.component.scss │ │ ├── user-profile.component.ts │ │ └── user-profile.module.ts ├── polyfills.ts ├── redux │ ├── actions │ │ ├── error │ │ │ ├── errorHandler.actions.spec.ts │ │ │ └── errorHandler.actions.ts │ │ ├── seo │ │ │ └── seo.actions.ts │ │ ├── user │ │ │ ├── user.actions.spec.ts │ │ │ └── user.actions.ts │ │ └── userForm │ │ │ ├── userForm.actions.spec.ts │ │ │ └── userForm.actions.ts │ ├── redux.module.ts │ └── store │ │ ├── errorHandler │ │ ├── errorHandler.initial-state.ts │ │ ├── errorHandler.reducer.spec.ts │ │ ├── errorHandler.reducer.ts │ │ ├── errorHandler.transformers.ts │ │ ├── errorHandler.types.ts │ │ └── index.ts │ │ ├── index.ts │ │ ├── user │ │ ├── index.ts │ │ ├── user.initial-state.ts │ │ ├── user.reducer.spec.ts │ │ ├── user.reducer.ts │ │ ├── user.transformers.ts │ │ └── user.types.ts │ │ └── userForm │ │ ├── index.ts │ │ ├── userForm.initial-state.ts │ │ ├── userForm.reducer.spec.ts │ │ ├── userForm.reducer.ts │ │ ├── userForm.transformers.ts │ │ └── userForm.types.ts ├── styles.scss └── vendor.ts ├── config ├── env │ ├── default.ts │ ├── development.ts │ ├── production.ts │ └── test.ts ├── helpers.js ├── index.ts ├── other │ ├── .sass-lint.yml │ ├── generate-ssl-certs.sh │ └── tslint.json ├── scripts.js ├── test-libs │ ├── karma-test-shim.js │ ├── karma.config.js │ ├── protractor.config.js │ └── server.test.js └── webpack │ ├── webpack.client.js │ ├── webpack.common.js │ └── webpack.server.js ├── e2e ├── app.e2e-spec.js └── header.e2e-spec.js ├── package-lock.json ├── package.json ├── public ├── assets │ ├── favicon.png │ ├── footer.jpg │ ├── goatlogo.svg │ ├── loader │ │ ├── fire-1.png │ │ ├── fire-2.png │ │ ├── space-goat.png │ │ ├── star.png │ │ └── star.svg │ └── octocat.png └── fonts │ └── Fredoka_One.woff2 ├── server ├── cassandra-db │ ├── api │ │ └── user │ │ │ ├── prepared.statements.ts │ │ │ ├── user.controller.ts │ │ │ ├── user.integration.ts │ │ │ ├── user.model.ts │ │ │ ├── user.router.ts │ │ │ └── user.spec.ts │ ├── auth │ │ ├── auth.router.ts │ │ ├── auth.service.ts │ │ └── local │ │ │ ├── local.passport.ts │ │ │ └── local.router.ts │ ├── db.model.ts │ ├── index.ts │ ├── prepared.statements.ts │ └── seed.ts ├── db-connect.ts ├── express.ts ├── mongo-db │ ├── api │ │ └── user │ │ │ ├── user.controller.ts │ │ │ ├── user.integration.ts │ │ │ ├── user.model.ts │ │ │ ├── user.router.ts │ │ │ └── user.spec.ts │ ├── auth │ │ ├── auth.router.ts │ │ ├── auth.service.ts │ │ └── local │ │ │ ├── local.passport.ts │ │ │ └── local.router.ts │ ├── index.ts │ └── seed.ts ├── routes.ts ├── server.ts ├── socketio.ts └── sql-db │ ├── api │ └── user │ │ ├── user.controller.ts │ │ ├── user.integration.ts │ │ ├── user.model.ts │ │ ├── user.router.ts │ │ └── user.spec.ts │ ├── auth │ ├── auth.router.ts │ ├── auth.service.ts │ └── local │ │ ├── local.passport.ts │ │ └── local.router.ts │ ├── index.ts │ └── seed.ts ├── tsconfig-aot.json ├── tsconfig.browser.json ├── tsconfig.json ├── tsconfig.server.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Node build artifacts 2 | node_modules 3 | npm-debug* 4 | 5 | # Local development 6 | *.env 7 | *.dev 8 | .DS_Store 9 | dist 10 | tmp 11 | 12 | ngc-aot 13 | *.ngfactory* 14 | *.shim.ts 15 | *.js.map 16 | 17 | # Docker 18 | Dockerfile 19 | docker-compose.yml 20 | 21 | # certificates 22 | server/sslcerts 23 | 24 | # TypeScript transpiled files 25 | client/**/**/**/*.js 26 | 27 | # CSS files 28 | **/*.css 29 | client/**/**/**/*.css 30 | 31 | # Typings folder 32 | typings 33 | 34 | # e2e test output 35 | config/sys/_test-output 36 | 37 | visualProject.njsproj 38 | .vs 39 | 40 | debug.log 41 | .com* 42 | .org* 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 JT 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node node_modules/gulp/bin/gulp build 2 | 3 | -------------------------------------------------------------------------------- /client/app-aot.ts: -------------------------------------------------------------------------------- 1 | //The browser platform with a compiler, used for Just in Time loading. 2 | //JIT means Angular compiles the application in the browser and then launches the app 3 | import { platformBrowser } from '@angular/platform-browser'; 4 | 5 | //imports the AppModule which is the root module that bootstraps app.component.ts 6 | import { AppModuleNgFactory } from '../ngc-aot/client/modules/app.module.ngfactory'; 7 | import { enableProdMode } from '@angular/core'; 8 | enableProdMode(); 9 | 10 | // Compile and launch the module 11 | platformBrowser().bootstrapModuleFactory(AppModuleNgFactory); 12 | -------------------------------------------------------------------------------- /client/app.ts: -------------------------------------------------------------------------------- 1 | //The browser platform with a compiler, used for Just in Time loading. 2 | //JIT means Angular compiles the application in the browser and then launches the app 3 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 4 | import { enableProdMode } from '@angular/core'; 5 | 6 | //imports the AppModule which is the root module that bootstraps app.component.ts 7 | import { AppModule } from './modules/app.module'; 8 | 9 | if (process.env.ENV === 'production') { 10 | enableProdMode(); 11 | } 12 | 13 | // Compile and launch the module 14 | platformBrowserDynamic().bootstrapModule(AppModule); 15 | -------------------------------------------------------------------------------- /client/modules/404/404-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | 4 | import { Four0FourComponent } from './404.component'; 5 | 6 | @NgModule({ 7 | imports: [RouterModule.forChild([ 8 | { path: 'PageNotFound', component: Four0FourComponent } 9 | ])], 10 | exports: [RouterModule] 11 | }) 12 | export class Four0FourRoutingModule {} -------------------------------------------------------------------------------- /client/modules/404/404.component.html: -------------------------------------------------------------------------------- 1 |

Page not found

-------------------------------------------------------------------------------- /client/modules/404/404.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | position: relative; 3 | display: block; 4 | width: 100%; 5 | text-align: center; 6 | } 7 | 8 | h1 { 9 | position: fixed; 10 | top: 40%; 11 | left: 0; 12 | right:0; 13 | font-size: 50px; 14 | } -------------------------------------------------------------------------------- /client/modules/404/404.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectionStrategy } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'four0four-section', 5 | templateUrl: './404.component.html', 6 | styleUrls: ['./404.component.css'], 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | 10 | export class Four0FourComponent { } 11 | -------------------------------------------------------------------------------- /client/modules/404/404.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { Four0FourRoutingModule } from './404-routing.module'; 4 | 5 | import { Four0FourComponent } from './404.component'; 6 | 7 | @NgModule({ 8 | imports: [CommonModule, Four0FourRoutingModule], 9 | declarations: [Four0FourComponent] 10 | }) 11 | 12 | export class Four0FourModule { 13 | 14 | } -------------------------------------------------------------------------------- /client/modules/app.component.html: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 |
15 | 16 |
17 | 18 | 19 | 20 |
21 | 22 | -------------------------------------------------------------------------------- /client/modules/app.component.scss: -------------------------------------------------------------------------------- 1 | :host{ 2 | position: relative; 3 | display: block; 4 | overflow-y: hidden; 5 | overflow-x: hidden; 6 | 7 | } 8 | 9 | header { 10 | position: fixed; 11 | top: 0; 12 | left: 0; 13 | right: 0; 14 | z-index: 10000; 15 | } 16 | 17 | .error-card { 18 | display: none; 19 | position: fixed; 20 | bottom: 10%; 21 | left: 5%; 22 | width: 350px; 23 | transform: translateY(400px); 24 | } 25 | 26 | .error-content { 27 | padding: 15px; 28 | font-size: medium; 29 | font-family: Roboto, "Helvetica Neue", sans-serif; 30 | } 31 | 32 | .user-sign button:hover { 33 | cursor: pointer; 34 | } 35 | 36 | .reg-card { 37 | will-change: transform; 38 | display: none; 39 | position: fixed; 40 | width: 400px; 41 | margin-left: -200px; 42 | top: 20%; 43 | left: 50%; 44 | z-index: 10000; 45 | padding: 0; 46 | } 47 | 48 | .reg-card#reg { 49 | top: 15%; 50 | } 51 | 52 | .reg-content { 53 | padding: 10px; 54 | } 55 | 56 | .reg-content button { 57 | margin-right: 2px; 58 | } 59 | .reg-content #login-btn, 60 | .reg-content #reg-btn { 61 | float: right; 62 | } 63 | 64 | 65 | 66 | @media (max-width: 800px) { 67 | .error-card { 68 | bottom: 0; 69 | left: 0; 70 | right: 0; 71 | width: auto; 72 | transform: translateY(-400px); 73 | } 74 | 75 | .reg-card { 76 | width: auto; 77 | margin: 0; 78 | top: 0; 79 | left: 0; 80 | right: 0; 81 | } 82 | .reg-card#reg { 83 | top: 0; 84 | } 85 | } -------------------------------------------------------------------------------- /client/modules/app.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | ================================================================================================================================= 3 | -- Bootstrapping component ------------------------------------------------------------------------------------------------------ 4 | ================================================================================================================================= 5 | ** According to Angular best practices the App component should be used for bootstrapping the application. ** 6 | ** This component gets bootstrapped through app.module.ts, the magic occurs in the @NgModule decorater's bootstrap property, ** 7 | ** we set that value to the AppComponent class defined in this component ** 8 | ** then the app.module.ts gets invoked in the main.ts bootstrap method. ** 9 | ================================================================================================================================= 10 | */ 11 | 12 | 13 | //main imports 14 | import { Component, ChangeDetectionStrategy } from '@angular/core'; 15 | 16 | import { select } from '@angular-redux/store'; 17 | import { Observable } from 'rxjs/Observable'; 18 | 19 | //decorator 20 | @Component({ 21 | selector: 'my-app', 22 | templateUrl: 'app.component.html', 23 | styleUrls: ['app.component.css'], 24 | changeDetection: ChangeDetectionStrategy.OnPush 25 | }) 26 | 27 | //the main app component which will act as the parent component to all other components in the app. 28 | export class AppComponent { 29 | //the @select() decorator is from NgRedux. 30 | //GOATstack embraces the immutible paradigm, and has a redux store which contains the applications state which can be found in root/client/redux 31 | //you can read more about Redux here: https://github.com/angular-redux/ng2-redux 32 | 33 | } 34 | -------------------------------------------------------------------------------- /client/modules/app.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | ================================================================================== 3 | -- Root Module ------------------------------------------------------------------- 4 | ================================================================================== 5 | ** Any assets included in this file will be attached ** 6 | ** to the global scope of the application. ** 7 | ** ** 8 | ** The Root Module has two main purposes ** 9 | ** 1) It tells Angular about all the apps dependencies ** 10 | ** so Angular can build the application tree ** 11 | ** 2) It tells Angular how to bootstrap the app ** 12 | ** ** 13 | ** Find out more here: https://angular.io/docs/ts/latest/guide/appmodule.html ** 14 | ---------------------------------------------------------------------------------- 15 | */ 16 | 17 | /* 18 | ------------------------------------------------------------------- 19 | Main component which gets bootstrapped 20 | ------------------------------------------------------------------- 21 | ** Named AppComponent in compliance with Angular best practices ** 22 | */ 23 | import { AppComponent } from './app.component'; 24 | 25 | /* 26 | -------------------------------------------------- 27 | Modules 28 | -------------------------------------------------- 29 | ** other necessary modules for this app 30 | */ 31 | import { NgModule } from '@angular/core'; 32 | import { BrowserModule } from '@angular/platform-browser'; 33 | import { ReduxModule } from '../redux/redux.module'; 34 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 35 | 36 | import { CoreModule } from './core/core.module'; 37 | 38 | /* 39 | -------------------------------------------------- 40 | NgModule 41 | -------------------------------------------------- 42 | ** decorator which packages all resources imported above for the app 43 | ** without this decorator Angular cannot use any of those above assets 44 | ** read more here: https://angular.io/docs/ts/latest/guide/ngmodule.html 45 | */ 46 | @NgModule({ 47 | //imports: this object imports helper modules which are children in the module tree 48 | imports: [ 49 | BrowserModule, 50 | ReduxModule, 51 | CoreModule, 52 | BrowserAnimationsModule 53 | ], 54 | //declarations: this object imports all child components which are used in this module 55 | declarations: [ AppComponent ], 56 | //bootstrap: identifies which component is supposed to be bootstrapped 57 | bootstrap: [ AppComponent ] 58 | }) 59 | 60 | //by convention the root module is called AppModule as stated in the Angular2 docs 61 | //we call AppModule in app.ts to bootstrap the application which points to the AppComponent defined in @NgModule 62 | export class AppModule { 63 | 64 | } -------------------------------------------------------------------------------- /client/modules/browser-app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { AppComponent } from './app.component'; 4 | import { AppModule } from './app.module'; 5 | import { BrowserTransferStateModule } from './transfer-state/browser-transfer-state.module'; 6 | 7 | @NgModule({ 8 | bootstrap: [ AppComponent ], 9 | imports: [ 10 | BrowserModule.withServerTransition({ 11 | appId: 'my-app' 12 | }), 13 | BrowserTransferStateModule, 14 | AppModule 15 | ] 16 | }) 17 | export class BrowserAppModule {} -------------------------------------------------------------------------------- /client/modules/core/components/footer/footer.component.html: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /client/modules/core/components/footer/footer.component.scss: -------------------------------------------------------------------------------- 1 | footer { 2 | position: fixed; 3 | bottom: 0; 4 | right: 0; 5 | left: 0; 6 | height: 25px; 7 | background-image: url("/public/assets/footer.jpg"); 8 | } 9 | 10 | p { 11 | color: white; 12 | text-decoration: none; 13 | display: inline-block; 14 | line-height: 25px; 15 | } 16 | 17 | .octocat { 18 | display: inline-block; 19 | float: left; 20 | height: 25px; 21 | width: auto; 22 | } 23 | 24 | .github p { 25 | margin-left: 3px; 26 | } 27 | 28 | .devs:hover p, 29 | .github:hover p { 30 | text-decoration: underline; 31 | } 32 | 33 | .devs { 34 | float: right; 35 | margin-right: 5px; 36 | } 37 | -------------------------------------------------------------------------------- /client/modules/core/components/footer/footer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectionStrategy } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'footer-section', 5 | templateUrl: './footer.component.html', 6 | styleUrls: ['./footer.component.css'], 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | 10 | export class FooterComponent { } 11 | -------------------------------------------------------------------------------- /client/modules/core/components/header/header.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

GOATstack

4 |
5 | 17 | 20 | -------------------------------------------------------------------------------- /client/modules/core/components/header/header.component.scss: -------------------------------------------------------------------------------- 1 | .router-links a.active { 2 | color: orange; 3 | } 4 | 5 | :host { 6 | position: absolute; 7 | top: 0; 8 | left: 0; 9 | right: 0; 10 | margin: 23px 2.5%; 11 | } 12 | 13 | a:hover { 14 | cursor: pointer; 15 | } 16 | 17 | .app-title { 18 | width: 275px; 19 | height: 60px; 20 | float: left; 21 | } 22 | 23 | .app-title h1 { 24 | padding: 10px; 25 | text-align: center; 26 | vertical-align: top; 27 | font-size: 32px; 28 | color: black; 29 | display: inline-block; 30 | } 31 | 32 | .menu-container { 33 | width: -webkit-calc(100% - 275px); 34 | width: -moz-calc(100% - 275px); 35 | width: calc(100% - 275px); 36 | float: left; 37 | margin-top: 11px; 38 | } 39 | .menu-container.menu-mobile { 40 | position: absolute; 41 | right: 0; 42 | top: 80%; 43 | width: auto; 44 | } 45 | 46 | .router-links a { 47 | background-color: #2196f3; 48 | display: inline-block; 49 | text-align: center; 50 | padding: 0 5px; 51 | min-height: 35px; 52 | min-width: 88px; 53 | line-height: 35px; 54 | border-radius: 2px; 55 | color: white; 56 | text-decoration: none; 57 | } 58 | 59 | .router-links button { 60 | float: right; 61 | margin-left: 5px; 62 | padding: 0 5px; 63 | background-color: #967ADC; 64 | border: none; 65 | min-width: 88px; 66 | min-height: 35px; 67 | border-radius: 2px; 68 | color: white; 69 | } 70 | 71 | .menu-container.menu-mobile > .router-links.show { 72 | display: block; 73 | } 74 | .menu-container.menu-mobile > .router-links { 75 | display: none; 76 | float: none; 77 | } 78 | .menu-container.menu-mobile > .router-links > .hidden { 79 | display: none; 80 | } 81 | .menu-container.menu-mobile > .router-links > a, 82 | .menu-container.menu-mobile > .router-links > button { 83 | float: none; 84 | display: block; 85 | margin-top: 5px; 86 | margin-left: 0; 87 | } 88 | 89 | #logo { 90 | display: inline-block; 91 | opacity: 0.8; 92 | width: 75px; 93 | height: 75px; 94 | margin-top: -10px; 95 | } 96 | .app-title h1.night-time { 97 | color: white; 98 | } 99 | 100 | #welcome-user { 101 | position: absolute; 102 | top: 11px; 103 | right: 103px; 104 | } 105 | 106 | .user-mobile#welcome-user { 107 | position: relative; 108 | float: right; 109 | margin-right: 2%; 110 | top: 11px; 111 | right: 0; 112 | } 113 | 114 | 115 | @media (max-width: 550px) { 116 | .app-title { 117 | width: auto; 118 | } 119 | .app-title h1 { 120 | display: none; 121 | } 122 | #logo { 123 | width: 60px; 124 | height: 60px; 125 | margin-top: 0; 126 | margin-left: 10px; 127 | } 128 | } 129 | 130 | 131 | // Menu button 132 | 133 | *, 134 | *:after, 135 | *:before { 136 | box-sizing: border-box; 137 | } 138 | 139 | html { 140 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 141 | } 142 | 143 | body { 144 | background: #434A54; 145 | text-align: center; 146 | padding: 50px 0; 147 | } 148 | 149 | .hidden { 150 | display: none; 151 | } 152 | 153 | /* Nav Trigger */ 154 | .nav-trigger { 155 | float: right; 156 | width: 50px; 157 | height: 50px; 158 | position: relative; 159 | background: transparent; 160 | border: none; 161 | vertical-align: middle; 162 | padding: 10px; 163 | margin: 0; 164 | margin-top: 4px; 165 | cursor: pointer; 166 | &:focus { 167 | outline: 0; 168 | } 169 | &:hover { 170 | span, 171 | span:before, 172 | span:after { 173 | background: #AC92EC; 174 | } 175 | } 176 | &:before { 177 | content: ''; 178 | opacity: 0; 179 | width: 0; 180 | height: 0; 181 | border-radius: 50%; 182 | position: absolute; 183 | top: 50%; 184 | left: 50%; 185 | background: transparent; 186 | transform: translate(-50%, -50%); 187 | transition: all 0.4s ease; 188 | } 189 | span { 190 | display: block; 191 | position: relative; 192 | &:before, 193 | &:after { 194 | content: ''; 195 | position: absolute; 196 | left: 0; 197 | } 198 | &:before { 199 | top: -8px; 200 | } 201 | &:after { 202 | bottom: -8px; 203 | } 204 | } 205 | span, 206 | span:before, 207 | span:after { 208 | width: 100%; 209 | height: 4px; 210 | background: #967ADC; 211 | transition: all 0.4s ease; 212 | } 213 | &.is-active { 214 | &:before { 215 | opacity: 1; 216 | width: 50px; 217 | height: 50px; 218 | background: #fff; 219 | } 220 | span { 221 | background: transparent; 222 | &:before { 223 | top: 0; 224 | transform: rotate(225deg); 225 | } 226 | &:after { 227 | bottom: 0; 228 | transform: rotate(-225deg); 229 | } 230 | } 231 | } 232 | } -------------------------------------------------------------------------------- /client/modules/core/components/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, AfterViewInit, ViewChild, ElementRef, HostListener, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; 2 | import { select } from '@angular-redux/store'; 3 | import { Observable } from 'rxjs/Observable'; 4 | 5 | import { UserActions } from '../../../../redux/actions/user/user.actions'; 6 | import { UserFormActions } from '../../../../redux/actions/userForm/userForm.actions'; 7 | 8 | declare let TweenMax: any; 9 | declare let TimelineMax: any; 10 | declare let Power0: any; 11 | 12 | @Component({ 13 | selector: 'header-section', 14 | templateUrl: './header.component.html', 15 | styleUrls: ['./header.component.css'], 16 | changeDetection: ChangeDetectionStrategy.OnPush 17 | }) 18 | 19 | export class HeaderComponent implements OnInit, AfterViewInit { 20 | 21 | @ViewChild('menu') m: ElementRef; 22 | 23 | @select('user') user$: Observable; 24 | @select('userForm') userForm$: Observable; 25 | menuHide: boolean = true; 26 | menuOpen: boolean = false; 27 | 28 | linkWidth: number; 29 | buttonWidth: number; 30 | bQuant: number; 31 | savedWidth: number; 32 | 33 | private timeline: any; 34 | 35 | constructor( 36 | private el: ElementRef, 37 | public userActions: UserActions, 38 | public userFormActions: UserFormActions, 39 | private ref: ChangeDetectorRef 40 | ) {} 41 | 42 | ngOnInit() { 43 | this.userActions.getMe(); 44 | } 45 | 46 | ngAfterViewInit() { 47 | this.linkWidth = this.m.nativeElement.clientWidth; 48 | this.buttonWidth = this.m.nativeElement.children[0].children[0].clientWidth; 49 | this.bQuant = this.m.nativeElement.children[0].children.length - 1; 50 | this.checkMenuWidth(); 51 | 52 | this.initMenuAnima(); 53 | } 54 | 55 | openMenu() { 56 | this.menuOpen = !this.menuOpen; 57 | if (this.menuOpen) { 58 | this.timeline.play(); 59 | } else { 60 | this.timeline.reverse(); 61 | } 62 | this.ref.markForCheck(); 63 | } 64 | 65 | checkMenuWidth(): void { 66 | this.linkWidth = this.m.nativeElement.clientWidth; 67 | 68 | if (this.linkWidth - ((this.buttonWidth * this.bQuant) + (4 * this.bQuant)) < 1 && this.menuHide) { 69 | this.savedWidth = window.innerWidth + 50; 70 | this.menuHide = false; 71 | 72 | this.ref.markForCheck(); 73 | } else if (window.innerWidth > this.savedWidth && !this.menuHide) { 74 | this.menuHide = true; 75 | 76 | this.ref.markForCheck(); 77 | } 78 | } 79 | 80 | initMenuAnima() { 81 | // initialize menu handling animation timeline 82 | this.timeline = new TimelineMax({ paused: true }); 83 | 84 | const links = this.m.nativeElement.children[0].children; 85 | 86 | this.timeline 87 | .to(this.m.nativeElement.children[0], 0, { ease: Power0.easeNone, css: { className:'+=show' } }) 88 | .to(links[0], 0, { x: 150 }) 89 | .to(links[1], 0, { x: 150 }) 90 | .to(links[4], 0, { x: 150 }) 91 | .to(links[2], 0, { x: 150 }) 92 | .to(links[3], 0, { x: 150 }) 93 | .to(links[0], 0.5, { x: 0 }) 94 | .to(links[1], 0.5, { x: 0 }, '-=0.3') 95 | .to(links[4], 0.5, { x: 0 }, '-=0.3') 96 | .to(links[2], 0.5, { x: 0 }, '-=0.5') 97 | .to(links[3], 0.5, { x: 0 }, '-=0.3'); 98 | } 99 | 100 | 101 | @HostListener('window:resize', ['$event']) 102 | resize(event) { 103 | this.checkMenuWidth(); 104 | } 105 | 106 | @HostListener('document:click', ['$event']) 107 | body(event) { 108 | let clicked = event.target; 109 | let inside = false; 110 | do { 111 | if (clicked === this.m.nativeElement || clicked === this.el.nativeElement.children[2]) { 112 | inside = true; 113 | } 114 | clicked = clicked.parentNode; 115 | } while (clicked); 116 | if(inside){ 117 | 118 | }else{ 119 | if (this.menuOpen && !this.menuHide) this.openMenu(); 120 | } 121 | } 122 | 123 | @HostListener('window:click', ['$event']) 124 | menu(event) { 125 | let clicked = event.target; 126 | let inside = false; 127 | do { 128 | if (clicked === this.m.nativeElement) { 129 | inside = true; 130 | } 131 | clicked = clicked.parentNode; 132 | } while (clicked); 133 | if(inside){ 134 | if (this.menuOpen && !this.menuHide) this.openMenu(); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /client/modules/core/core-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | export const routes: Routes = [ 5 | { path: '', redirectTo: '/', pathMatch: 'full' }, 6 | { path: 'profile', redirectTo: '/profile', pathMatch: 'full' }, 7 | { path: '**', redirectTo: '/PageNotFound', pathMatch: 'full' } 8 | ]; 9 | 10 | @NgModule({ 11 | imports: [RouterModule.forRoot(routes)], 12 | exports: [RouterModule] 13 | }) 14 | export class CoreRoutingModule {} -------------------------------------------------------------------------------- /client/modules/core/core.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
Sign In
5 |
6 |
7 |
8 |
9 |
10 | 11 |
12 |
13 | 14 |
15 |
16 |
17 | 18 | 19 | 20 |
21 |
22 |
23 |
24 |
25 |
26 |
Create New User
27 |
28 |
29 |
30 |
31 |
32 | 33 |
34 |
35 | 36 |
37 |
38 | 39 |
40 |
41 | 42 |
43 |
44 |
45 | 46 | 47 | 48 |
49 |
50 |
51 |
52 |
53 | 54 |
55 |
56 |
57 |
Error Message
58 |
59 |
60 |

{{ (error$ | async).get('message') }}

61 |
62 |
63 |
-------------------------------------------------------------------------------- /client/modules/core/core.component.scss: -------------------------------------------------------------------------------- 1 | .error-card { 2 | display: none; 3 | position: fixed; 4 | bottom: 10%; 5 | left: 5%; 6 | width: 350px; 7 | transform: translateY(400px); 8 | } 9 | 10 | .error-content { 11 | padding: 15px; 12 | font-size: medium; 13 | font-family: Roboto, "Helvetica Neue", sans-serif; 14 | } 15 | 16 | .user-sign button:hover { 17 | cursor: pointer; 18 | } 19 | 20 | .reg-card { 21 | will-change: transform; 22 | display: none; 23 | position: fixed; 24 | width: 400px; 25 | margin-left: -200px; 26 | top: 20%; 27 | left: 50%; 28 | z-index: 10000; 29 | padding: 0; 30 | background: white; 31 | border-radius: 2px; 32 | box-shadow: 0 3px 1px -2px rgba(0,0,0,.2), 0 2px 2px 0 rgba(0,0,0,.14), 0 1px 5px 0 rgba(0,0,0,.12); 33 | transition: box-shadow 280ms cubic-bezier(.4,0,.2,1); 34 | 35 | .title-color { 36 | background: #2196f3; 37 | color: white; 38 | min-height: 48px; 39 | font-size: 20px; 40 | padding: 0 16px; 41 | 42 | div { 43 | line-height: 48px; 44 | } 45 | 46 | } 47 | 48 | .input-wrapper { 49 | height: 40px; 50 | } 51 | 52 | input { 53 | font: inherit; 54 | background: 0 0; 55 | color: currentColor; 56 | border: none; 57 | outline: 0; 58 | padding: 0; 59 | width: 100%; 60 | border-bottom: 1px solid #cacaca; 61 | } 62 | 63 | input:focus { 64 | border-bottom: 2px solid #2196f3; 65 | } 66 | 67 | button { 68 | background-color: #2196f3; 69 | color: white; 70 | box-shadow: 0 3px 1px -2px rgba(0,0,0,.2), 0 2px 2px 0 rgba(0,0,0,.14), 0 1px 5px 0 rgba(0,0,0,.12); 71 | padding: 0 16px; 72 | line-height: 35px; 73 | border-radius: 2px; 74 | } 75 | 76 | button:last-of-type { 77 | background-color: #4caf50; 78 | } 79 | 80 | } 81 | 82 | .reg-card#reg { 83 | top: 15%; 84 | } 85 | 86 | 87 | 88 | .reg-content { 89 | padding: 10px; 90 | } 91 | 92 | .reg-content button { 93 | margin-right: 2px; 94 | } 95 | .reg-content #login-btn, 96 | .reg-content #reg-btn { 97 | float: right; 98 | } 99 | 100 | .error-card { 101 | .error-color { 102 | background: #4caf50; 103 | color: white; 104 | min-height: 48px; 105 | font-size: 20px; 106 | padding: 0 16px; 107 | 108 | div { 109 | line-height: 48px; 110 | } 111 | 112 | } 113 | } 114 | 115 | 116 | 117 | @media (max-width: 800px) { 118 | .error-card { 119 | bottom: 0; 120 | left: 0; 121 | right: 0; 122 | width: auto; 123 | transform: translateY(-400px); 124 | } 125 | 126 | .reg-card { 127 | width: auto; 128 | margin: 0; 129 | top: 0; 130 | left: 0; 131 | right: 0; 132 | } 133 | .reg-card#reg { 134 | top: 0; 135 | } 136 | } -------------------------------------------------------------------------------- /client/modules/core/core.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild, AfterViewInit, ElementRef, HostListener, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; 2 | import { NgRedux, select } from '@angular-redux/store'; 3 | import { ErrorHandlerActions } from '../../redux/actions/error/errorHandler.actions'; 4 | import { UserFormActions } from '../../redux/actions/userForm/userForm.actions'; 5 | import { UserActions } from '../../redux/actions/user/user.actions'; 6 | import { SEOActions } from '../../redux/actions/seo/seo.actions'; 7 | import { Observable } from 'rxjs/Observable'; 8 | 9 | declare let TweenMax: any; 10 | declare let TimelineMax: any; 11 | declare let Power0: any; 12 | 13 | @Component({ 14 | selector: 'core-section', 15 | templateUrl: 'core.component.html', 16 | styleUrls: ['core.component.css'], 17 | changeDetection: ChangeDetectionStrategy.OnPush 18 | }) 19 | export class CoreComponent implements AfterViewInit { 20 | //this decorator is for NgRedux. you can read more about Redux here: https://github.com/angular-redux/ng2-redux 21 | @select('error') error$: Observable; 22 | @select('userForm') userForm$: Observable; 23 | 24 | userSigning: boolean = false; 25 | userSignup: boolean = false; 26 | 27 | private errorTimeline: any; 28 | private formTimeline: any; 29 | private formTimeline2: any; 30 | 31 | private manuContainer: ElementRef; 32 | 33 | //this decorator gabs the object associated with the #errorToast template variable assigned in the app.componnent.html file, 34 | //-- and assigns this object to the class variable errorToast 35 | @ViewChild('errorToast') errorToast: ElementRef; 36 | @ViewChild('formToast') formToast: ElementRef; 37 | 38 | constructor( 39 | private errorHandler: ErrorHandlerActions, 40 | public userFormActions: UserFormActions, 41 | public userActions: UserActions, 42 | private el: ElementRef, 43 | private ref: ChangeDetectorRef 44 | ) {} 45 | 46 | ngAfterViewInit() { 47 | this.manuContainer = this.el.nativeElement.parentElement.children[0].children[0].children[1]; 48 | 49 | // Signin and Signup form timelines 50 | this.formTimeline = new TimelineMax({ paused: true }); 51 | this.formTimeline 52 | .to(this.formToast.nativeElement.children[0], 0, {ease: Power0.easeNone, display: 'block'}) 53 | .fromTo(this.formToast.nativeElement.children[0], 1, {y:-500}, {y: 0}); 54 | 55 | this.formTimeline2 = new TimelineMax({ paused: true }); 56 | this.formTimeline2 57 | .to(this.formToast.nativeElement.children[1], 0, {ease: Power0.easeNone, display: 'block'}) 58 | .fromTo(this.formToast.nativeElement.children[1], 1, {y:-500}, {y: 0}); 59 | 60 | this.userForm$.subscribe(uf => { 61 | this.userSigning = uf.get('userSigning'); 62 | this.userSignup = uf.get('userSignup'); 63 | uf.get('userSigning') ? this.formTimeline.play(): this.formTimeline.reverse(); 64 | uf.get('userSignup') ? this.formTimeline2.play(): this.formTimeline2.reverse(); 65 | }); 66 | 67 | 68 | // initialize error handling animation timeline 69 | this.errorTimeline = new TimelineMax({ paused: true }); 70 | this.errorTimeline 71 | .to(this.errorToast.nativeElement, 0, {display:'block',y:400}) 72 | .to(this.errorToast.nativeElement, 1, {y:0}) 73 | .to(this.errorToast.nativeElement, 1, {y:400, display:'none'}, "+=3") 74 | .add(() => this.errorHandler.hideError()); 75 | 76 | // Let the component be in charge of triggering the animation 77 | this.error$.subscribe((error) => error.get('message') ? this.errorTimeline.play(0) : null); 78 | } 79 | 80 | @HostListener('document:click', ['$event']) 81 | body(event) { 82 | let clicked = event.target; 83 | let inside = false; 84 | do { 85 | if (clicked === this.formToast.nativeElement || clicked === this.manuContainer) { 86 | inside = true; 87 | } 88 | clicked = clicked.parentNode; 89 | } while (clicked); 90 | if(inside){ 91 | 92 | }else{ 93 | if (this.userSigning) 94 | this.userFormActions.loginForm(false); 95 | if (this.userSignup) 96 | this.userFormActions.registerForm(false); 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /client/modules/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { SharedModule } from '../shared/shared.module'; 3 | import { CoreRoutingModule } from './core-routing.module'; 4 | 5 | import { HomeModule } from '../home/home.module'; 6 | import { UserProfileModule } from '../user-profile/user-profile.module'; 7 | import { Four0FourModule } from '../404/404.module'; 8 | 9 | import { HttpClientModule, HttpClient, HttpInterceptor, HTTP_INTERCEPTORS } from '@angular/common/http'; 10 | 11 | import { CoreComponent } from './core.component'; 12 | import { HeaderComponent } from './components/header/header.component'; 13 | import { FooterComponent } from './components/footer/footer.component'; 14 | 15 | import { ErrorHandlerActions } from '../../redux/actions/error/errorHandler.actions'; 16 | import { UserFormActions } from '../../redux/actions/userForm/userForm.actions'; 17 | import { UserActions } from '../../redux/actions/user/user.actions'; 18 | import { SEOActions } from '../../redux/actions/seo/seo.actions'; 19 | 20 | import { SocketService } from './services/socketio/socketio.service'; 21 | import { TokenInterceptor } from './services/auth/token-interceptor.service'; 22 | import { AuthService } from './services/auth/auth.service'; 23 | 24 | //Angular and 3rd party serices 25 | import { Cookie } from 'ng2-cookies/ng2-cookies'; 26 | 27 | @NgModule({ 28 | imports: [ 29 | SharedModule, 30 | CoreRoutingModule, 31 | HomeModule, 32 | UserProfileModule, 33 | Four0FourModule, ], 34 | declarations: [ CoreComponent, HeaderComponent, FooterComponent ], 35 | exports: [ CoreRoutingModule, HttpClientModule, CoreComponent, HeaderComponent, FooterComponent ], 36 | providers: [ 37 | { 38 | provide: HTTP_INTERCEPTORS, 39 | useClass: TokenInterceptor, 40 | multi: true 41 | }, 42 | HttpClient, 43 | ErrorHandlerActions, 44 | UserActions, 45 | UserFormActions, 46 | SEOActions, 47 | 48 | SocketService, 49 | AuthService, 50 | 51 | Cookie 52 | ] 53 | }) 54 | export class CoreModule { 55 | 56 | } -------------------------------------------------------------------------------- /client/modules/core/services/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient, HttpResponse, HttpErrorResponse } from '@angular/common/http'; 3 | 4 | import { Observable } from 'rxjs/Observable'; 5 | import { Cookie } from 'ng2-cookies/ng2-cookies'; 6 | 7 | import 'rxjs/Rx'; 8 | 9 | //instead of HttpResponse create custom response interface for extractToken 10 | interface tokenExtraction extends HttpResponse { 11 | token: string, 12 | user: { 13 | created: string, 14 | email: string, 15 | provider: string, 16 | role: string, 17 | username: string 18 | } 19 | } 20 | 21 | @Injectable() 22 | export class AuthService { 23 | constructor(private http: HttpClient) { } 24 | 25 | // Private variables that only this service can use 26 | private authUrl = 'auth/local'; 27 | private userUrl = 'api/users'; 28 | 29 | private extractToken(res: tokenExtraction) { 30 | Cookie.set('token', res.token); 31 | return res.user; 32 | } 33 | 34 | // This is called when there is a cookie OAuth token 35 | // present in the browser so the user will automatically 36 | // sign in 37 | autoLogin(): Observable { 38 | return this.http.get(this.userUrl + '/me'); 39 | } 40 | 41 | login(email: string, password: string): Observable { 42 | let body = { 43 | email: email, 44 | password: password 45 | }; 46 | 47 | return this.http.post(this.authUrl, body) 48 | .map(this.extractToken); 49 | } 50 | 51 | signup(username: string, email: string, password: string): Observable { 52 | let body = { 53 | username: username, 54 | email: email, 55 | password: password 56 | }; 57 | return this.http.post(this.userUrl, body) 58 | .map(this.extractToken); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /client/modules/core/services/auth/token-interceptor.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; 3 | import { Observable } from 'rxjs/Rx'; 4 | import { Cookie } from 'ng2-cookies/ng2-cookies'; 5 | import * as _ from 'lodash'; 6 | 7 | // Extending the Http class so connect a OAuth token if present in the cookies 8 | // When the request is recieved on the server authenticated endpoints will 9 | // have varification that give them the ability to execute 10 | @Injectable() 11 | export class TokenInterceptor implements HttpInterceptor { 12 | constructor() { } 13 | 14 | intercept(req: HttpRequest, next: HttpHandler): Observable> { 15 | 16 | // get the token from a service 17 | const authHeader = `Bearer ${Cookie.get('token')}`; 18 | const authReq = req.clone({setHeaders: {Authorization: authHeader}}); 19 | 20 | if(authHeader) 21 | return next.handle(authReq); 22 | else 23 | return next.handle(req); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /client/modules/core/services/socketio/socketio.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Observable } from 'rxjs/Observable'; 4 | 5 | import { NgRedux } from '@angular-redux/store'; 6 | import { IAppState } from '../../../../redux/store'; 7 | 8 | import * as _ from 'lodash'; 9 | import * as io from 'socket.io-client'; 10 | 11 | @Injectable() 12 | export class SocketService { 13 | socket; 14 | 15 | constructor(private ngRedux: NgRedux) { 16 | // socket.io now auto-configures its connection when we ommit a connection url 17 | this.socket = io.connect({ path: '/socket.io-client' }); 18 | } 19 | 20 | /** 21 | * Register listeners to sync an array with updates on a model 22 | * 23 | * Takes the array we want to sync, the model name that socket updates are sent from, 24 | * and an optional callback function after new items are updated. 25 | * 26 | * modelName: The server model with atached socket listener 27 | * array: model array from service subscription 28 | * stateArray: Redux states that this syncUpdates instance will invoke 29 | * index 0: update/add-state, index 1: remove-state 30 | * cb: callback function, will be invoked after redux dispatch 31 | * bcb: beforeCallback function, will be invoked before redux dispatch 32 | * dpDelay: dispatch delay, give dp time until dispatch is called 33 | * NOTE: bcb will be called immidiately, dispatch will wait dp to execute 34 | */ 35 | syncUpdates(modelName: string, array: any, stateArray: Array, cb?, bcb?, dpDelay?: number) { 36 | /** 37 | * Syncs item creation/updates on 'model:save' 38 | */ 39 | this.socket.on(modelName + ':save', (item) => { 40 | const oldItem = _.find(array, { _id: item._id }); 41 | const index = array.indexOf(oldItem); 42 | 43 | let event: string = 'created'; 44 | let isNew: boolean; 45 | 46 | // replace oldItem if it exists 47 | // otherwise just add item to the collection 48 | if (oldItem) { 49 | // Update store with new object 50 | isNew = false; 51 | event = 'updated'; 52 | } else { 53 | // Finds the model for the listener 54 | // and pushes a new object to store 55 | isNew = true; 56 | } 57 | 58 | // create beforCall observable and set the delay to specified time 59 | const bcbObs = Observable.of(true).map(() => bcb ? bcb(item, index, event) : null).delay(dpDelay ? dpDelay : 0); 60 | //create the normal socketio execution observable 61 | const nowObs = Observable.of(true).map(() => { 62 | this.ngRedux.dispatch({ type: stateArray[0], payload: { index: index, object: item, isNew: isNew } }); 63 | }); 64 | // create callback observable 65 | const cbObs = Observable.of(true).map(() => cb ? cb(item, index, event) : null); 66 | // concatonate all observables in proper order and subscribe to execute 67 | return Observable.concat(bcbObs, nowObs, cbObs).subscribe(); 68 | }); 69 | 70 | /** 71 | * Syncs removed items on 'model:remove' 72 | */ 73 | this.socket.on(modelName + ':remove', (item) => { 74 | const event = 'deconsted'; 75 | const oldItem = _.find(array, { _id: item._id }); 76 | const index = array.indexOf(oldItem); 77 | _.remove(array, { _id: item._id }); 78 | 79 | const nowObserv = Observable.of(true).map(() => { 80 | this.ngRedux.dispatch({ type: stateArray[1], payload: { index: index, object: item } }); 81 | }); 82 | const cbObserv = Observable.of(true).map(() => cb ? cb(item, index, event) : null); 83 | 84 | return Observable.concat(nowObserv, cbObserv).subscribe(); 85 | }); 86 | } 87 | 88 | /** 89 | * Removes listeners for a models updates on the socket 90 | */ 91 | unsyncUpdates(modelName: string) { 92 | this.socket.removeListener(modelName + ':save'); 93 | this.socket.removeListener(modelName + ':remove'); 94 | } 95 | 96 | 97 | } 98 | -------------------------------------------------------------------------------- /client/modules/home/home-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | 4 | import { HomeComponent } from './home.component'; 5 | 6 | @NgModule({ 7 | imports: [RouterModule.forChild([ 8 | { path: '', component: HomeComponent } 9 | ])], 10 | exports: [RouterModule] 11 | }) 12 | export class HomeRoutingModule {} -------------------------------------------------------------------------------- /client/modules/home/home.component.html: -------------------------------------------------------------------------------- 1 |

Home

-------------------------------------------------------------------------------- /client/modules/home/home.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | position: relative; 3 | display: block; 4 | width: 100%; 5 | text-align: center; 6 | } 7 | 8 | h1 { 9 | position: fixed; 10 | top: 40%; 11 | left: 0; 12 | right:0; 13 | font-size: 50px; 14 | } -------------------------------------------------------------------------------- /client/modules/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostListener, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; 2 | 3 | import { select } from '@angular-redux/store'; 4 | import { Observable } from 'rxjs/Observable'; 5 | 6 | @Component({ 7 | selector: 'home-section', 8 | templateUrl: './home.component.html', 9 | styleUrls: ['./home.component.css'], 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | 13 | export class HomeComponent { 14 | 15 | constructor() { } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /client/modules/home/home.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { SharedModule } from '../shared/shared.module'; 3 | import { HomeRoutingModule } from './home-routing.module'; 4 | 5 | import { HomeComponent } from './home.component'; 6 | 7 | @NgModule({ 8 | imports: [ SharedModule, HomeRoutingModule ], 9 | declarations: [ 10 | HomeComponent 11 | ] 12 | }) 13 | export class HomeModule { } -------------------------------------------------------------------------------- /client/modules/server-app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, APP_BOOTSTRAP_LISTENER, ApplicationRef } from '@angular/core'; 2 | import { ServerModule } from '@angular/platform-server'; 3 | import { ServerTransferStateModule } from './transfer-state/server-transfer-state.module'; 4 | import { AppComponent } from './app.component'; 5 | import { AppModule } from './app.module'; 6 | import { TransferState } from './transfer-state/transfer-state'; 7 | import { BrowserModule } from '@angular/platform-browser'; 8 | 9 | export function onBootstrap(appRef: ApplicationRef, transferState: TransferState) { 10 | return () => { 11 | appRef.isStable 12 | .filter(stable => stable) 13 | .first() 14 | .subscribe(() => { 15 | transferState.inject(); 16 | }); 17 | }; 18 | } 19 | 20 | @NgModule({ 21 | bootstrap: [AppComponent], 22 | imports: [ 23 | BrowserModule.withServerTransition({ 24 | appId: 'my-app' 25 | }), 26 | ServerModule, 27 | ServerTransferStateModule, 28 | AppModule 29 | ], 30 | providers: [ 31 | { 32 | provide: APP_BOOTSTRAP_LISTENER, 33 | useFactory: onBootstrap, 34 | multi: true, 35 | deps: [ 36 | ApplicationRef, 37 | TransferState 38 | ] 39 | } 40 | ] 41 | }) 42 | export class ServerAppModule { 43 | 44 | } -------------------------------------------------------------------------------- /client/modules/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | 5 | @NgModule({ 6 | imports: [ CommonModule ], 7 | exports: [ 8 | CommonModule, 9 | FormsModule 10 | ] 11 | }) 12 | export class SharedModule { } -------------------------------------------------------------------------------- /client/modules/transfer-state/browser-transfer-state.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { TransferState } from './transfer-state'; 3 | 4 | export function getTransferState(): TransferState { 5 | const transferState = new TransferState(); 6 | transferState.initialize(window['TRANSFER_STATE'] || {}); 7 | return transferState; 8 | } 9 | 10 | @NgModule({ 11 | providers: [ 12 | { 13 | provide: TransferState, 14 | useFactory: getTransferState 15 | } 16 | ] 17 | }) 18 | export class BrowserTransferStateModule { 19 | 20 | } -------------------------------------------------------------------------------- /client/modules/transfer-state/server-transfer-state.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { ServerTransferState } from './server-transfer-state'; 3 | import { TransferState } from './transfer-state'; 4 | 5 | @NgModule({ 6 | providers: [ 7 | { provide: TransferState, useClass: ServerTransferState } 8 | ] 9 | }) 10 | export class ServerTransferStateModule { 11 | 12 | } -------------------------------------------------------------------------------- /client/modules/transfer-state/server-transfer-state.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Optional, RendererFactory2, ViewEncapsulation } from '@angular/core'; 2 | import { TransferState } from './transfer-state'; 3 | import { PlatformState } from '@angular/platform-server'; 4 | import * as serialize from 'serialize-javascript'; 5 | 6 | @Injectable() 7 | export class ServerTransferState extends TransferState { 8 | constructor( private state: PlatformState, private rendererFactory: RendererFactory2) { 9 | super(); 10 | } 11 | 12 | /** 13 | * Inject the State into the bottom of the 14 | */ 15 | inject() { 16 | try { 17 | const document: any = this.state.getDocument(); 18 | const transferStateString = serialize(this.toJson()); 19 | const renderer = this.rendererFactory.createRenderer(document, { 20 | id: '-1', 21 | encapsulation: ViewEncapsulation.None, 22 | styles: [], 23 | data: {} 24 | }); 25 | 26 | const head = document.head; 27 | if (!head) { 28 | throw new Error('Please have as the first element in your document'); 29 | } 30 | 31 | const script = renderer.createElement('script'); 32 | renderer.setValue(script, `window['TRANSFER_STATE'] = ${transferStateString}`); 33 | renderer.appendChild(head, script); 34 | } catch (e) { 35 | console.error(e); 36 | } 37 | } 38 | 39 | 40 | } -------------------------------------------------------------------------------- /client/modules/transfer-state/transfer-state.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable() 4 | export class TransferState { 5 | private _map = new Map(); 6 | 7 | constructor() {} 8 | 9 | keys() { 10 | return this._map.keys(); 11 | } 12 | 13 | get(key: string): any { 14 | return this._map.get(key); 15 | } 16 | 17 | set(key: string, value: any): Map { 18 | return this._map.set(key, value); 19 | } 20 | 21 | toJson(): any { 22 | const obj = {}; 23 | Array.from(this.keys()) 24 | .forEach(key => { 25 | obj[key] = this.get(key); 26 | }); 27 | return obj; 28 | } 29 | 30 | initialize(obj: any): void { 31 | Object.keys(obj) 32 | .forEach(key => { 33 | this.set(key, obj[key]); 34 | }); 35 | } 36 | 37 | inject(): void {} 38 | } -------------------------------------------------------------------------------- /client/modules/user-profile/user-profile-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | 4 | import { UserProfileComponent } from './user-profile.component'; 5 | 6 | @NgModule({ 7 | imports: [RouterModule.forChild([ 8 | { path: 'profile', component: UserProfileComponent } 9 | ])], 10 | exports: [RouterModule] 11 | }) 12 | export class UserProfileRoutingModule {} -------------------------------------------------------------------------------- /client/modules/user-profile/user-profile.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Welcome, {{(user$ | async).getIn(['userItem', 'username'])}}

3 |
4 | -------------------------------------------------------------------------------- /client/modules/user-profile/user-profile.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | position: relative; 3 | display: block; 4 | width: 100%; 5 | text-align: center; 6 | } 7 | 8 | section { 9 | position: fixed; 10 | top: 40%; 11 | left: 0; 12 | right:0; 13 | font-size: 50px; 14 | } -------------------------------------------------------------------------------- /client/modules/user-profile/user-profile.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectionStrategy } from '@angular/core'; 2 | 3 | import { select } from '@angular-redux/store'; 4 | import { Observable } from 'rxjs/Observable'; 5 | 6 | @Component({ 7 | selector: 'user-profile', 8 | templateUrl: './user-profile.component.html', 9 | styleUrls: ['./user-profile.component.css'], 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | 13 | export class UserProfileComponent { 14 | 15 | @select('user') user$: Observable; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /client/modules/user-profile/user-profile.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { SharedModule } from '../shared/shared.module'; 3 | import { UserProfileRoutingModule } from './user-profile-routing.module' 4 | 5 | import { UserProfileComponent } from './user-profile.component'; 6 | 7 | @NgModule({ 8 | imports: [ SharedModule, UserProfileRoutingModule ], 9 | declarations: [ UserProfileComponent ] 10 | }) 11 | export class UserProfileModule { 12 | 13 | } -------------------------------------------------------------------------------- /client/polyfills.ts: -------------------------------------------------------------------------------- 1 | /* 2 | ================================================================================== 3 | -- Polyfills for webpack --------------------------------------------------------- 4 | ================================================================================== 5 | ** Some simple polyfills for the development environment ** 6 | ** This file is used by webpack to include the proper polyfills ** 7 | ** The webpack file is located here: GOATstack/config/webpack/webpack.common.js ** 8 | ================================================================================== 9 | */ 10 | 11 | import 'core-js/es6'; 12 | import 'core-js/es7/reflect'; 13 | require('zone.js/dist/zone'); 14 | 15 | if (process.env.ENV === 'production') { 16 | // Production 17 | } else { 18 | // Development 19 | Error.stackTraceLimit = Infinity; 20 | } -------------------------------------------------------------------------------- /client/redux/actions/error/errorHandler.actions.spec.ts: -------------------------------------------------------------------------------- 1 | import { NgRedux } from '@angular-redux/store'; 2 | import { MockNgRedux } from '@angular-redux/store/testing'; 3 | import { ErrorHandlerActions } from './errorHandler.actions'; 4 | 5 | describe('ErrorHandler Actions Creator', () => { 6 | let actions: ErrorHandlerActions; 7 | let mockRedux: NgRedux; 8 | 9 | beforeEach(() => { 10 | mockRedux = MockNgRedux.getInstance(); 11 | actions = new ErrorHandlerActions(mockRedux); 12 | }); 13 | 14 | it('should dispatch SHOW_ERROR action', () => { 15 | const expectedAction = { 16 | type: ErrorHandlerActions.SHOW_ERROR, 17 | payload: 'Testing Error Message' 18 | }; 19 | 20 | spyOn(mockRedux, 'dispatch'); 21 | actions.showError('Testing Error Message'); 22 | 23 | expect(mockRedux.dispatch).toHaveBeenCalled(); 24 | expect(mockRedux.dispatch).toHaveBeenCalledWith(expectedAction); 25 | }); 26 | 27 | it('should dispatch HIDE_ERROR action', () => { 28 | const expectedAction = { 29 | type: ErrorHandlerActions.HIDE_ERROR 30 | }; 31 | 32 | spyOn(mockRedux, 'dispatch'); 33 | actions.hideError(); 34 | 35 | expect(mockRedux.dispatch).toHaveBeenCalled(); 36 | expect(mockRedux.dispatch).toHaveBeenCalledWith(expectedAction); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /client/redux/actions/error/errorHandler.actions.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, ElementRef } from '@angular/core'; 2 | import { NgRedux } from '@angular-redux/store'; 3 | import { IAppState } from '../../store/index'; 4 | 5 | // declare global variables to hook onto gsap library 6 | declare let TweenMax: any; 7 | declare let TimelineMax: any; 8 | 9 | ///////////////////////////////////////////////////////// 10 | /* ErrorHandler Actions: Used to call dispatches to change 11 | error object in the store 12 | 13 | SHOW_ERROR -> updates the error message to display 14 | HIDE_ERROR -> removes error message string 15 | */ 16 | //////////////////////////////////////////////////////// 17 | @Injectable() 18 | export class ErrorHandlerActions { 19 | timeline: any; 20 | 21 | constructor(private ngRedux: NgRedux) { } 22 | 23 | static SHOW_ERROR: string = 'SHOW_ERROR'; 24 | static HIDE_ERROR: string = 'HIDE_ERROR'; 25 | 26 | showError(error: string): void { 27 | this.ngRedux.dispatch({ 28 | type: ErrorHandlerActions.SHOW_ERROR, 29 | payload: error 30 | }); 31 | } 32 | 33 | hideError(): void { 34 | this.ngRedux.dispatch({ type: ErrorHandlerActions.HIDE_ERROR }); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /client/redux/actions/seo/seo.actions.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Title } from '@angular/platform-browser'; 3 | 4 | //////////////////////////////////////////////////////////////////////// 5 | // SEO Actions: used to get or change the title, icon link, meta tags 6 | // in the heaa of the index.html 7 | //////////////////////////////////////////////////////////////////////// 8 | @Injectable() 9 | export class SEOActions { 10 | private headElement: any; 11 | private favicon: any; 12 | private metaDescription: any; 13 | private metaKeywords: any; 14 | 15 | constructor(private titleService: Title) { 16 | /** 17 | * get the Element 18 | * @type {any} 19 | */ 20 | this.headElement = document.getElementsByTagName('head'); 21 | this.favicon = document.head.querySelector('link[rel=icon]'); 22 | this.metaDescription = this.getOrCreateMetaElement('description'); 23 | this.metaKeywords = this.getOrCreateMetaElement('keywords'); 24 | } 25 | 26 | /** 27 | * get the HTML Element when it is in the markup, or create it. 28 | * @param name 29 | * @returns {HTMLElement} 30 | */ 31 | private getOrCreateMetaElement(name: string): Element { 32 | let el: Element; 33 | el = document.head.querySelector('meta[name=' + name + ']'); 34 | if (el === null) { 35 | el = document.createElement('meta'); 36 | el.setAttribute('name', name); 37 | this.headElement[0].appendChild(el); 38 | } 39 | return el; 40 | } 41 | 42 | // get the current site site 43 | getTitle(): string { 44 | return this.titleService.getTitle(); 45 | } 46 | 47 | // set the site title 48 | setTitle(newTitle: string): void { 49 | this.titleService.setTitle(newTitle); 50 | } 51 | 52 | // get the current link icon 53 | getLinkFavicon(): string { 54 | return this.favicon.getAttribute('href'); 55 | } 56 | 57 | // set the site link icon 58 | setLinkFavicon(href: string): void { 59 | this.favicon.setAttribute('href', href); 60 | } 61 | 62 | // get the current meta description 63 | getMetaDescription(): string { 64 | return this.metaDescription.getAttribute('content'); 65 | } 66 | 67 | // set the meta description 68 | setMetaDescription(description: string): void { 69 | this.metaDescription.setAttribute('content', description); 70 | } 71 | 72 | // get the current meta keywords 73 | getMetaKeywords(): Array { 74 | return this.metaKeywords.getAttribute('content').split(','); 75 | } 76 | 77 | // set the meta keywords 78 | setMetaKeywords(keywords: Array): void { 79 | this.metaKeywords.setAttribute('content', keywords.toString()); 80 | } 81 | 82 | setAll(object: any): void { 83 | if (object.title) 84 | this.setTitle(object.title); 85 | if (object.favicon) 86 | this.setLinkFavicon(object.favicon); 87 | if (object.description) 88 | this.setMetaDescription(object.description); 89 | if (object.keywords) 90 | this.setMetaKeywords(object.keywords); 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /client/redux/actions/user/user.actions.spec.ts: -------------------------------------------------------------------------------- 1 | import { FormGroup, FormControl } from '@angular/forms'; 2 | import { Observable } from 'rxjs/Observable'; 3 | 4 | import { NgRedux } from '@angular-redux/store'; 5 | import { MockNgRedux } from '@angular-redux/store/testing'; 6 | import { UserActions } from './user.actions'; 7 | import { AuthService } from '../../../modules/core/services/auth/auth.service'; 8 | import { ErrorHandlerActions } from '../error/errorHandler.actions'; 9 | import { Cookie } from 'ng2-cookies/ng2-cookies'; 10 | 11 | const testUser = { 12 | _id: '1234', 13 | created: 'today', 14 | userName: 'testUserName', 15 | firstName: 'testFirstName', 16 | lastName: 'testLastName', 17 | email: 'testEmail', 18 | role: 'testRole' 19 | }; 20 | 21 | const error = { 22 | status: 400, 23 | statusText: 'Bad Request', 24 | url: 'test:7001', 25 | message: 'this is a test error message' 26 | }; 27 | 28 | class MockAuthService extends AuthService { 29 | constructor() { 30 | super(null); 31 | } 32 | 33 | autoLogin(): Observable { 34 | return Observable.of(testUser); 35 | } 36 | login(email: string, password: string): Observable { 37 | return Observable.of(testUser); 38 | } 39 | signup(username: string, email: string, password: string): Observable { 40 | return Observable.of(testUser); 41 | } 42 | logout() { } 43 | } 44 | 45 | describe('User Actions Creator', () => { 46 | let actions: UserActions; 47 | let authService: AuthService; 48 | let errorActions: ErrorHandlerActions; 49 | let mockRedux: NgRedux; 50 | 51 | beforeEach(() => { 52 | Cookie.delete('token'); 53 | 54 | authService = new MockAuthService(); 55 | mockRedux = MockNgRedux.getInstance(); 56 | errorActions = new ErrorHandlerActions(mockRedux); 57 | actions = new UserActions(mockRedux, errorActions, authService); 58 | }); 59 | 60 | it('should dispatch LOGIN_USER action when autoLogin() called', () => { 61 | Cookie.set('token', 'testCookie'); 62 | 63 | const expectedActionPre = { 64 | type: UserActions.FETCH_USER 65 | }; 66 | const expectedAction = { 67 | type: UserActions.LOGIN_USER, 68 | payload: testUser 69 | }; 70 | 71 | spyOn(mockRedux, 'dispatch'); 72 | actions.getMe(); 73 | 74 | expect(mockRedux.dispatch).toHaveBeenCalled(); 75 | expect(mockRedux.dispatch).toHaveBeenCalledWith(expectedActionPre); 76 | expect(mockRedux.dispatch).toHaveBeenCalledWith(expectedAction); 77 | }); 78 | 79 | it('should dispatch LOGIN_USER action', () => { 80 | const expectedActionPre = { 81 | type: UserActions.FETCH_USER 82 | }; 83 | const expectedAction = { 84 | type: UserActions.LOGIN_USER, 85 | payload: testUser 86 | }; 87 | 88 | const form = new FormGroup({ 89 | login_email: new FormControl("test"), 90 | login_password: new FormControl("test") 91 | }); 92 | 93 | spyOn(mockRedux, 'dispatch'); 94 | actions.login(form); 95 | 96 | expect(mockRedux.dispatch).toHaveBeenCalled(); 97 | expect(mockRedux.dispatch).toHaveBeenCalledWith(expectedActionPre); 98 | expect(mockRedux.dispatch).toHaveBeenCalledWith(expectedAction); 99 | }); 100 | 101 | it('should dispatch REGISTER_USER action', () => { 102 | const expectedActionPre = { 103 | type: UserActions.FETCH_USER 104 | }; 105 | const expectedAction = { 106 | type: UserActions.REGISTER_USER, 107 | payload: testUser 108 | }; 109 | 110 | const form = new FormGroup({ 111 | signup_username: new FormControl("testUserName"), 112 | signup_email: new FormControl("testEmail"), 113 | signup_password: new FormControl("test"), 114 | signup_re_password: new FormControl("test") 115 | }); 116 | 117 | spyOn(mockRedux, 'dispatch'); 118 | actions.register(form); 119 | 120 | expect(mockRedux.dispatch).toHaveBeenCalled(); 121 | expect(mockRedux.dispatch).toHaveBeenCalledWith(expectedActionPre); 122 | expect(mockRedux.dispatch).toHaveBeenCalledWith(expectedAction); 123 | }); 124 | 125 | it('should dispatch LOGOUT_USER action', () => { 126 | const expectedAction = { type: UserActions.LOGOUT_USER }; 127 | 128 | spyOn(mockRedux, 'dispatch'); 129 | actions.logout(); 130 | 131 | expect(mockRedux.dispatch).toHaveBeenCalled(); 132 | expect(mockRedux.dispatch).toHaveBeenCalledWith(expectedAction); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /client/redux/actions/user/user.actions.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {FormGroup, NgForm} from '@angular/forms'; 3 | import {HttpErrorResponse} from '@angular/common/http'; 4 | 5 | import {NgRedux} from '@angular-redux/store'; 6 | import {IAppState} from '../../store/index'; 7 | 8 | import {AuthService} from '../../../modules/core/services/auth/auth.service'; 9 | import {ErrorHandlerActions} from '../error/errorHandler.actions'; 10 | import {Cookie} from 'ng2-cookies/ng2-cookies'; 11 | 12 | ////////////////////////////////////////////////////////////////////// 13 | /* User Actions: used to call dispatches to change the user object 14 | in the store 15 | 16 | LOGIN_USER -> updates the user object with user information 17 | LOGOUT_USER -> clears the user object from the store 18 | REGISTER_USER -> updates the user object with user information 19 | */ 20 | ////////////////////////////////////////////////////////////////////// 21 | @Injectable() 22 | export class UserActions { 23 | constructor( 24 | private ngRedux: NgRedux, 25 | private errorHandler: ErrorHandlerActions, 26 | private authService: AuthService) { } 27 | 28 | static FETCH_USER: string = 'FETCH_USER'; 29 | static INVALIDATE_USER: string = 'INVALIDATE_USER'; 30 | static LOGIN_USER: string = 'LOGIN_USER'; 31 | static LOGOUT_USER: string = 'LOGOUT_USER'; 32 | static REGISTER_USER: string = 'REGISTER_USER'; 33 | 34 | invalidateUser(error: Object): void { 35 | this.ngRedux.dispatch({ // if an error happens change state to reflect 36 | type: UserActions.INVALIDATE_USER, 37 | payload: error // pass in the json object made in userService.handleError 38 | }); 39 | } 40 | 41 | fetchUser(): void { 42 | this.ngRedux.dispatch({ type: UserActions.FETCH_USER }); 43 | } 44 | 45 | getMe(): void { 46 | // We will only execute if there's a token present 47 | if (Cookie.get('token')) { 48 | // First change the state to fetching 49 | this.fetchUser(); 50 | // subscribe to the service and wait for a response 51 | this.authService.autoLogin().subscribe(user => { 52 | // once a response comes change the state to reflect user info 53 | this.ngRedux.dispatch({ 54 | type: UserActions.LOGIN_USER, 55 | payload: user 56 | }); 57 | }, (err: HttpErrorResponse) => this.invalidateUser(err)); 58 | } 59 | } 60 | 61 | // Setting lf to type FormGroup causes issues 62 | login(lf: any): void { 63 | console.log('lf', lf.value.login_email.type); 64 | // only if the login form is filled 65 | if (lf.valid) { 66 | // First change the state to fetching 67 | this.fetchUser(); 68 | // subscribe to the service and wait for a response 69 | this.authService.login(lf.value.login_email, lf.value.login_password) 70 | .subscribe(user => { 71 | // once a response comes change the state to reflect user info 72 | this.ngRedux.dispatch({ 73 | type: UserActions.LOGIN_USER, 74 | payload: user 75 | }); 76 | }, (err: HttpErrorResponse) => { 77 | this.invalidateUser(err); 78 | this.errorHandler.showError(err.error.message); 79 | }); 80 | } else if(!lf.value.login_email || !lf.value.login_password) { 81 | if(!lf.value.login_email && !lf.value.login_password) { 82 | this.errorHandler.showError("Please enter an Email address and password."); 83 | } else if(!lf.value.login_email) { 84 | this.errorHandler.showError("Please enter an Email address."); 85 | } else if(!lf.value.login_password) { 86 | this.errorHandler.showError("Please enter a password."); 87 | } 88 | } 89 | } 90 | 91 | logout(): void { 92 | // simply delete the cached token 93 | Cookie.delete('token'); 94 | // and delete the user object in the state 95 | this.ngRedux.dispatch({ type: UserActions.LOGOUT_USER }); 96 | } 97 | 98 | // Setting lf to type FormGroup causes issues 99 | register(rf: any): void { 100 | // only if the form is filled and passwords equal the same 101 | if (rf.valid && (rf.value.signup_password === rf.value.signup_re_password)) { 102 | // First change the state to fetching 103 | this.fetchUser(); 104 | // subscribe to the service and wait for a response 105 | this.authService.signup(rf.value.signup_username, rf.value.signup_email, rf.value.signup_password) 106 | .subscribe(user => { 107 | // once a response comes change the state to reflect user info 108 | this.ngRedux.dispatch({ 109 | type: UserActions.REGISTER_USER, 110 | payload: user 111 | }); 112 | }, (err: HttpErrorResponse) => { 113 | this.invalidateUser(err); 114 | this.errorHandler.showError(err.error.message); 115 | }); 116 | } 117 | else if (rf.value.signup_password !== rf.value.signup_re_password) 118 | // if the passwords are not the same, simply display the message 119 | this.errorHandler.showError('Passwords do not match!'); 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /client/redux/actions/userForm/userForm.actions.spec.ts: -------------------------------------------------------------------------------- 1 | import { NgRedux } from '@angular-redux/store'; 2 | import { MockNgRedux } from '@angular-redux/store/testing'; 3 | import { UserFormActions } from './userForm.actions'; 4 | 5 | describe('UserForm Actions Creator', () => { 6 | let actions: UserFormActions; 7 | let mockRedux: NgRedux; 8 | 9 | beforeEach(() => { 10 | mockRedux = MockNgRedux.getInstance(); 11 | actions = new UserFormActions(mockRedux); 12 | }); 13 | 14 | it('should dispatch LOGIN_FORM_IN action', () => { 15 | const expectedAction = { 16 | type: UserFormActions.LOGIN_FORM_IN 17 | }; 18 | 19 | spyOn(mockRedux, 'dispatch'); 20 | actions.loginForm(true); 21 | 22 | expect(mockRedux.dispatch).toHaveBeenCalled(); 23 | expect(mockRedux.dispatch).toHaveBeenCalledWith(expectedAction); 24 | }); 25 | 26 | it('should dispatch LOGIN_FORM_OUT action', () => { 27 | const expectedAction = { 28 | type: UserFormActions.LOGIN_FORM_OUT 29 | }; 30 | 31 | spyOn(mockRedux, 'dispatch'); 32 | actions.loginForm(false); 33 | 34 | expect(mockRedux.dispatch).toHaveBeenCalled(); 35 | expect(mockRedux.dispatch).toHaveBeenCalledWith(expectedAction); 36 | }); 37 | 38 | it('should dispatch REGISTER_FORM_IN action', () => { 39 | const expectedAction = { 40 | type: UserFormActions.REGISTER_FORM_IN 41 | }; 42 | 43 | spyOn(mockRedux, 'dispatch'); 44 | actions.registerForm(true); 45 | 46 | expect(mockRedux.dispatch).toHaveBeenCalled(); 47 | expect(mockRedux.dispatch).toHaveBeenCalledWith(expectedAction); 48 | }); 49 | 50 | it('should dispatch REGISTER_FORM_OUT action', () => { 51 | const expectedAction = { 52 | type: UserFormActions.REGISTER_FORM_OUT 53 | }; 54 | 55 | spyOn(mockRedux, 'dispatch'); 56 | actions.registerForm(false); 57 | 58 | expect(mockRedux.dispatch).toHaveBeenCalled(); 59 | expect(mockRedux.dispatch).toHaveBeenCalledWith(expectedAction); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /client/redux/actions/userForm/userForm.actions.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { FormGroup, NgForm } from '@angular/forms'; 3 | 4 | import { NgRedux } from '@angular-redux/store'; 5 | import { IAppState } from '../../store/index'; 6 | 7 | ///////////////////////////////////////////////////////////////////////// 8 | /* UserForm Actions: used to call dispatches to change the userForm 9 | object in the store 10 | 11 | LOGIN_FORM_IN -> Opens the login form (closes reg form) 12 | LOGIN_FORM_OUT -> Closes the Login form 13 | REGISTER_FORM_IN -> Opens the registration form (closes login form) 14 | REGISTER_FORM_OUT -> Closes the registration form 15 | */ 16 | ///////////////////////////////////////////////////////////////////////// 17 | @Injectable() 18 | export class UserFormActions { 19 | private userSigning: boolean = false; 20 | private userSignup: boolean = false; 21 | 22 | constructor(private ngRedux: NgRedux) { } 23 | 24 | static LOGIN_FORM_IN: string = 'LOGIN_FORM_IN'; 25 | static LOGIN_FORM_OUT: string = 'LOGIN_FORM_OUT'; 26 | static REGISTER_FORM_IN: string = 'REGISTER_FORM_IN'; 27 | static REGISTER_FORM_OUT: string = 'REGISTER_FORM_OUT'; 28 | 29 | loginForm(action: boolean) { 30 | if (action) 31 | this.ngRedux.dispatch({ type: UserFormActions.LOGIN_FORM_IN }); 32 | else 33 | this.ngRedux.dispatch({ type: UserFormActions.LOGIN_FORM_OUT }); 34 | } 35 | 36 | registerForm(action: boolean) { 37 | if (action) 38 | this.ngRedux.dispatch({ type: UserFormActions.REGISTER_FORM_IN }); 39 | else 40 | this.ngRedux.dispatch({ type: UserFormActions.REGISTER_FORM_OUT }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /client/redux/redux.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, isDevMode } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { NgReduxModule, NgRedux, DevToolsExtension } from '@angular-redux/store'; 4 | 5 | import { IAppState, rootReducer, enhancers } from './store/index'; 6 | import { createLogger } from 'redux-logger'; 7 | 8 | @NgModule({ 9 | imports: [ CommonModule, NgReduxModule ], 10 | providers: [ 11 | { provide: DevToolsExtension, useClass: DevToolsExtension } 12 | ] 13 | }) 14 | export class ReduxModule { 15 | constructor( 16 | private ngRedux: NgRedux, 17 | private devTool: DevToolsExtension) { 18 | 19 | // configure the store here, this is where the enhancers are set 20 | this.ngRedux.configureStore(rootReducer, {}, 21 | isDevMode() ? [createLogger({ collapsed: true })] : [], 22 | isDevMode() && devTool.isEnabled() ? [...enhancers, devTool.enhancer()] : [...enhancers]); 23 | } 24 | } -------------------------------------------------------------------------------- /client/redux/store/errorHandler/errorHandler.initial-state.ts: -------------------------------------------------------------------------------- 1 | import { reimmutifyError } from './errorHandler.transformers'; 2 | 3 | // Define the INITIAL_STATE of the error attribute in the store 4 | export const INITIAL_STATE = reimmutifyError({ 5 | message: '', 6 | }); 7 | -------------------------------------------------------------------------------- /client/redux/store/errorHandler/errorHandler.reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | import { errorHandlerReducer } from './errorHandler.reducer'; 3 | import { INITIAL_STATE } from './errorHandler.initial-state'; 4 | import { ErrorHandlerActions } from '../../actions/error/errorHandler.actions'; 5 | 6 | // Testing for the errorHandler reducer 7 | describe('ErrorHandler Reducer', () => { 8 | let initialState = INITIAL_STATE; 9 | 10 | // before each test we will reset the state 11 | beforeEach(() => { 12 | initialState = errorHandlerReducer(undefined, { type: 'TEST_INIT' }); 13 | }); 14 | 15 | // First test is the state object in fact is immutable 16 | it('should have an immutable initial state', () => { 17 | expect(Map.isMap(initialState)).toBe(true); 18 | }); 19 | 20 | // Test to see if the object does contain the message 21 | it('should set the error message on SHOW_ERROR', () => { 22 | const previousState = initialState; 23 | const nextState = errorHandlerReducer(initialState, 24 | { type: ErrorHandlerActions.SHOW_ERROR, payload: 'Testing Error Message' }); 25 | 26 | expect(previousState.getIn(['message'])).toBe(''); 27 | expect(nextState.getIn(['message'])).toBe('Testing Error Message'); 28 | }); 29 | 30 | // Test to see if the object does not contain the message 31 | it('should remove error message on HIDE_ERROR', () => { 32 | // First SHOW_ERROR and check 33 | const nextState = errorHandlerReducer(initialState, 34 | { type: ErrorHandlerActions.SHOW_ERROR, payload: 'Testing Error Message' }); 35 | expect(nextState.getIn(['message'])).toBe('Testing Error Message'); 36 | // Then HIDE_ERROR and check 37 | const nextState2 = errorHandlerReducer(nextState, 38 | { type: ErrorHandlerActions.HIDE_ERROR }); 39 | expect(nextState2.getIn(['message'])).toBe(''); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /client/redux/store/errorHandler/errorHandler.reducer.ts: -------------------------------------------------------------------------------- 1 | import { ErrorHandlerActions } from '../../actions/error/errorHandler.actions'; 2 | import { reimmutifyError } from './errorHandler.transformers'; 3 | import { IError } from './errorHandler.types'; 4 | 5 | import { INITIAL_STATE } from './errorHandler.initial-state'; 6 | 7 | // define the reducer for error attribute in store 8 | export function errorHandlerReducer(state: IError = INITIAL_STATE, action: any) { 9 | // Depending on the incoming state 'type' execute corresponding state change 10 | switch(action.type) { 11 | case ErrorHandlerActions.SHOW_ERROR: 12 | return state.updateIn(['message'], val => action.payload); 13 | case ErrorHandlerActions.HIDE_ERROR: 14 | return state.updateIn(['message'], val => ''); 15 | default: 16 | return state; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/redux/store/errorHandler/errorHandler.transformers.ts: -------------------------------------------------------------------------------- 1 | import { Map, Record } from 'immutable'; 2 | import { IError, IErrorItem } from './errorHandler.types'; 3 | 4 | // functions to change the state of the data 5 | // either immutable -> mutable or mutable -> immutable 6 | export function deimmutifyError(state: IError): Object { 7 | return state.toJS(); 8 | } 9 | 10 | export function reimmutifyError(plain): IError { 11 | return Map(plain ? plain : ''); 12 | } 13 | -------------------------------------------------------------------------------- /client/redux/store/errorHandler/errorHandler.types.ts: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | 3 | // Interface describing the attributes 4 | // the corresponding reducer will need 5 | // to manipulate (immutably) 6 | export interface IErrorItem { 7 | message: string; 8 | } 9 | 10 | // Export type so reducer will understand 11 | export type IError = Map; 12 | -------------------------------------------------------------------------------- /client/redux/store/errorHandler/index.ts: -------------------------------------------------------------------------------- 1 | import { errorHandlerReducer } from './errorHandler.reducer'; 2 | import { IError } from './errorHandler.types'; 3 | import { deimmutifyError, reimmutifyError } from './errorHandler.transformers'; 4 | 5 | // This file is for convienience so only one import is required 6 | export { 7 | errorHandlerReducer, 8 | IError, 9 | deimmutifyError, 10 | reimmutifyError 11 | }; 12 | -------------------------------------------------------------------------------- /client/redux/store/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | // import persistState from 'redux-localStorage'; 3 | import * as error from './errorHandler/index'; 4 | import * as userForm from './userForm/index'; 5 | import * as user from './user/index'; 6 | // DO NOT REMOVE: template store imports 7 | 8 | // IAppState is the applications store where all persistant data 9 | // should be stored 10 | export class IAppState { 11 | error?: error.IError; 12 | user?: user.IUser; 13 | userForm?: userForm.IUserForm; 14 | // DO NOT REMOVE: template store attributes 15 | }; 16 | 17 | // Each reducer is connected to a coresponding store attribute 18 | // combineReducers() creates a root reducer while maintaining 19 | // this one-2-one relationship 20 | export const rootReducer = combineReducers({ 21 | error: error.errorHandlerReducer, 22 | user: user.userReducer, 23 | userForm: userForm.userFormReducer, 24 | // DO NOT REMOVE: template reducers 25 | }); 26 | 27 | // Redux plugins/enhancers go here 28 | export const enhancers = [ 29 | // persistState('GOAT-stack', { key: 'GOAT-stack' }) 30 | ]; 31 | -------------------------------------------------------------------------------- /client/redux/store/user/index.ts: -------------------------------------------------------------------------------- 1 | import { userReducer } from './user.reducer'; 2 | import { IUser } from './user.types'; 3 | import { deimmutifyUser, reimmutifyUser } from './user.transformers'; 4 | 5 | // This file is for convienience so only one import is required 6 | export { 7 | userReducer, 8 | IUser, 9 | deimmutifyUser, 10 | reimmutifyUser 11 | }; 12 | -------------------------------------------------------------------------------- /client/redux/store/user/user.initial-state.ts: -------------------------------------------------------------------------------- 1 | import { reimmutifyUser } from './user.transformers'; 2 | import { Map } from 'immutable'; 3 | 4 | // Define the INITIAL_STATE of the user object 5 | export const INITIAL_STATE = reimmutifyUser({ 6 | fetching: false 7 | }); 8 | -------------------------------------------------------------------------------- /client/redux/store/user/user.reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | import { userReducer } from './user.reducer'; 3 | import { INITIAL_STATE } from './user.initial-state'; 4 | import { UserActions } from '../../actions/user/user.actions'; 5 | 6 | const testUser = { 7 | _id: '1234', 8 | created: 'today', 9 | userName: 'testUserName', 10 | firstName: 'testFirstName', 11 | lastName: 'testLastName', 12 | email: 'testEmail', 13 | role: 'testRole' 14 | }; 15 | 16 | describe('User Reducer', () => { 17 | let initialState = INITIAL_STATE; 18 | 19 | beforeEach(() => { 20 | initialState = userReducer(undefined, { type: 'TEST_INIT' }); 21 | }); 22 | 23 | it('should have an immutable initial state', () => { 24 | expect(Map.isMap(initialState)).toBe(true); 25 | }); 26 | 27 | it('should indicate didInvalidate when INVALIDATE_USER state change', () => { 28 | const previousState = initialState; 29 | const nextState = userReducer(previousState, 30 | { type: UserActions.INVALIDATE_USER, payload: { 31 | status: 400, 32 | statusText: 'Bad Request', 33 | url: 'test:7001', 34 | message: 'this is a error message test' 35 | }}); 36 | 37 | expect(previousState.getIn(['fetching'])).toBe(false); 38 | expect(previousState.hasIn(['didInvalidate'])).toBe(false); 39 | expect(previousState.hasIn(['userItem'])).toBe(false); 40 | 41 | expect(nextState.getIn(['fetching'])).toBe(false); 42 | expect(nextState.hasIn(['userItem'])).toBe(false); 43 | expect(nextState.getIn(['didInvalidate', 'status'])).toBe(400); 44 | expect(nextState.getIn(['didInvalidate', 'statusText'])).toBe('Bad Request'); 45 | expect(nextState.getIn(['didInvalidate', 'url'])).toBe('test:7001'); 46 | expect(nextState.getIn(['didInvalidate', 'message'])).toBe('this is a error message test'); 47 | }); 48 | 49 | it('should indicated fetching when FETCH_USER state change', () => { 50 | const previousState = initialState; 51 | const nextState = userReducer(previousState, { type: UserActions.FETCH_USER }); 52 | 53 | expect(previousState.getIn(['fetching'])).toBe(false); 54 | expect(previousState.hasIn(['didInvalidate'])).toBe(false); 55 | expect(previousState.hasIn(['userItem'])).toBe(false); 56 | 57 | expect(nextState.getIn(['fetching'])).toBe(true); 58 | expect(previousState.hasIn(['didInvalidate'])).toBe(false); 59 | expect(previousState.hasIn(['userItem'])).toBe(false); 60 | 61 | }); 62 | 63 | it('should set user to user Object on LOGIN_USER', () => { 64 | const previousState = initialState; 65 | const nextState = userReducer(previousState, 66 | { type: UserActions.LOGIN_USER, payload: testUser }); 67 | 68 | expect(previousState.hasIn(['userItem'])).toBe(false); 69 | 70 | expect(nextState.getIn(['userItem', '_id'])).toBe('1234'); 71 | expect(nextState.getIn(['userItem', 'created'])).toBe('today'); 72 | expect(nextState.getIn(['userItem', 'userName'])).toBe('testUserName'); 73 | expect(nextState.getIn(['userItem', 'firstName'])).toBe('testFirstName'); 74 | expect(nextState.getIn(['userItem', 'lastName'])).toBe('testLastName'); 75 | expect(nextState.getIn(['userItem', 'email'])).toBe('testEmail'); 76 | expect(nextState.getIn(['userItem', 'role'])).toBe('testRole'); 77 | }); 78 | 79 | it('should set user to user Object on REGISTER_USER', () => { 80 | const previousState = initialState; 81 | const nextState = userReducer(previousState, 82 | { type: UserActions.REGISTER_USER, payload: testUser }); 83 | 84 | expect(previousState.hasIn(['userItem'])).toBe(false); 85 | 86 | expect(nextState.getIn(['userItem', '_id'])).toBe('1234'); 87 | expect(nextState.getIn(['userItem', 'created'])).toBe('today'); 88 | expect(nextState.getIn(['userItem', 'userName'])).toBe('testUserName'); 89 | expect(nextState.getIn(['userItem', 'firstName'])).toBe('testFirstName'); 90 | expect(nextState.getIn(['userItem', 'lastName'])).toBe('testLastName'); 91 | expect(nextState.getIn(['userItem', 'email'])).toBe('testEmail'); 92 | expect(nextState.getIn(['userItem', 'role'])).toBe('testRole'); 93 | }); 94 | 95 | it('should set user to empty Map on LOGOUT_USER', () => { 96 | const previousState = userReducer(initialState, 97 | { type: UserActions.LOGIN_USER, payload: testUser }); 98 | const nextState = userReducer(previousState, 99 | { type: UserActions.LOGOUT_USER }); 100 | 101 | expect(previousState.getIn(['userItem', '_id'])).toBe('1234'); 102 | expect(previousState.getIn(['userItem', 'created'])).toBe('today'); 103 | expect(previousState.getIn(['userItem', 'userName'])).toBe('testUserName'); 104 | expect(previousState.getIn(['userItem', 'firstName'])).toBe('testFirstName'); 105 | expect(previousState.getIn(['userItem', 'lastName'])).toBe('testLastName'); 106 | expect(previousState.getIn(['userItem', 'email'])).toBe('testEmail'); 107 | expect(previousState.getIn(['userItem', 'role'])).toBe('testRole'); 108 | 109 | expect(nextState.hasIn(['userItem'])).toBe(false); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /client/redux/store/user/user.reducer.ts: -------------------------------------------------------------------------------- 1 | import { UserActions } from '../../actions/user/user.actions'; 2 | import { IUser } from './user.types'; 3 | import { reimmutifyUser, } from './user.transformers'; 4 | import { INITIAL_STATE } from './user.initial-state'; 5 | 6 | // Define the reducer that will initiate state changes for user 7 | export function userReducer(state: IUser = INITIAL_STATE, action: any) { 8 | // will determine proper state change based off the type 9 | switch (action.type) { 10 | case UserActions.INVALIDATE_USER: 11 | // Indead of return a new Map, have immutable manage 12 | // what happens to the old object by merging 13 | return state.mergeWith((prev, next) => next, reimmutifyUser({ 14 | fetching: false, 15 | didInvalidate: action.payload 16 | })); 17 | case UserActions.FETCH_USER: 18 | return state 19 | .updateIn(['fetching'], val => true) 20 | .deleteIn(['didInvalidate']); 21 | case UserActions.LOGIN_USER: 22 | case UserActions.REGISTER_USER: 23 | // Indead of return a new Map, have immutable manage 24 | // what happens to the old object by merging 25 | return state.mergeWith((prev, next) => next, reimmutifyUser({ 26 | fetching: false, 27 | userItem: action.payload 28 | })); 29 | case UserActions.LOGOUT_USER: 30 | return state.clear() 31 | .updateIn(['fetching'], val => false) 32 | .deleteIn(['userItem']); 33 | default: 34 | return state; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /client/redux/store/user/user.transformers.ts: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | import { IUser, IUserBaseItem, IUserItem, IInvalidateItem } from './user.types'; 3 | 4 | // functions to change the state of the data 5 | // either immutable -> mutable or mutable -> immutable 6 | export function deimmutifyUser(state: IUser): Object { 7 | return state.toJS(); 8 | } 9 | 10 | export function reimmutifyUser(plain): IUser { 11 | if (plain.userItem) { 12 | plain.userItem = Map(plain.userItem); 13 | } 14 | if (plain.didInvalidate) { 15 | plain.didInvalidate = Map(plain.didInvalidate); 16 | } 17 | 18 | return Map(plain ? plain : {}); 19 | } 20 | -------------------------------------------------------------------------------- /client/redux/store/user/user.types.ts: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | 3 | // Define an interface of the object that will be saved 4 | export interface IUserItem { 5 | _id: string; 6 | created: string; 7 | userName: string; 8 | firstName: string; 9 | lastName: string; 10 | email: string; 11 | role: string; 12 | } 13 | export interface IInvalidateItem { 14 | status: number; 15 | statusText: string; 16 | url: string; 17 | message: string; 18 | } 19 | export interface IUserBaseItem { 20 | fetching: boolean; 21 | didInvalidate: Map; 22 | userItem: Map; 23 | } 24 | 25 | // Export the type so the reducer and store will understand 26 | export type IUser = Map; 27 | -------------------------------------------------------------------------------- /client/redux/store/userForm/index.ts: -------------------------------------------------------------------------------- 1 | import { userFormReducer } from './userForm.reducer'; 2 | import { IUserForm } from './userForm.types'; 3 | import { deimmutifyUserForm, reimmutifyUserForm } from './userForm.transformers'; 4 | 5 | // This file is for convienience so only one import is required 6 | export { 7 | userFormReducer, 8 | IUserForm, 9 | deimmutifyUserForm, 10 | reimmutifyUserForm 11 | }; 12 | -------------------------------------------------------------------------------- /client/redux/store/userForm/userForm.initial-state.ts: -------------------------------------------------------------------------------- 1 | import { reimmutifyUserForm } from './userForm.transformers'; 2 | 3 | // Define the initial state of userForm object 4 | export const INITIAL_STATE = reimmutifyUserForm({ 5 | userSigning: false, 6 | userSignup: false 7 | }); 8 | -------------------------------------------------------------------------------- /client/redux/store/userForm/userForm.reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | import { userFormReducer } from './userForm.reducer'; 3 | import { INITIAL_STATE } from './userForm.initial-state'; 4 | import { UserFormActions } from '../../actions/userForm/userForm.actions'; 5 | 6 | describe('UserForm Reducer', () => { 7 | let initialState = INITIAL_STATE; 8 | 9 | beforeEach(() => { 10 | initialState = userFormReducer(undefined, { type: 'TEST_INIT' }); 11 | }); 12 | 13 | it('should have an immutable initial state', () => { 14 | expect(Map.isMap(initialState)).toBe(true); 15 | }); 16 | 17 | it('should set userSigning to true on LOGIN_FORM_IN', () => { 18 | const previousState = initialState; 19 | const nextState = userFormReducer(previousState, 20 | { type: UserFormActions.LOGIN_FORM_IN }); 21 | 22 | expect(previousState.getIn(['userSigning'])).toBe(false); 23 | expect(previousState.getIn(['userSignup'])).toBe(false); 24 | 25 | expect(nextState.getIn(['userSigning'])).toBe(true); 26 | expect(nextState.getIn(['userSignup'])).toBe(false); 27 | }); 28 | 29 | it('should set userSigning to false on LOGIN_FORM_OUT', () => { 30 | const previousState = userFormReducer(initialState, 31 | { type: UserFormActions.LOGIN_FORM_IN }); 32 | const nextState = userFormReducer(previousState, 33 | { type: UserFormActions.LOGIN_FORM_OUT }); 34 | 35 | expect(previousState.getIn(['userSigning'])).toBe(true); 36 | expect(previousState.getIn(['userSignup'])).toBe(false); 37 | 38 | expect(nextState.getIn(['userSigning'])).toBe(false); 39 | expect(nextState.getIn(['userSignup'])).toBe(false); 40 | }); 41 | 42 | it('should set userSignup to true on REGISTER_FORM_IN', () => { 43 | const previousState = initialState; 44 | const nextState = userFormReducer(previousState, 45 | { type: UserFormActions.REGISTER_FORM_IN }); 46 | 47 | expect(previousState.getIn(['userSigning'])).toBe(false); 48 | expect(previousState.getIn(['userSignup'])).toBe(false); 49 | 50 | expect(nextState.getIn(['userSigning'])).toBe(false); 51 | expect(nextState.getIn(['userSignup'])).toBe(true); 52 | }); 53 | 54 | it('should set userSignup to false on REGISTER_FORM_OUT', () => { 55 | const previousState = userFormReducer(initialState, 56 | { type: UserFormActions.REGISTER_FORM_IN }); 57 | const nextState = userFormReducer(previousState, 58 | { type: UserFormActions.REGISTER_FORM_OUT }); 59 | 60 | expect(previousState.getIn(['userSigning'])).toBe(false); 61 | expect(previousState.getIn(['userSignup'])).toBe(true); 62 | 63 | expect(nextState.getIn(['userSigning'])).toBe(false); 64 | expect(nextState.getIn(['userSignup'])).toBe(false); 65 | }); 66 | 67 | it('should set swap userSigning and userSignup on REGISTER_FORM_IN', () => { 68 | const previousState = userFormReducer(initialState, 69 | { type: UserFormActions.LOGIN_FORM_IN }); 70 | const nextState = userFormReducer(previousState, 71 | { type: UserFormActions.REGISTER_FORM_IN }); 72 | 73 | expect(previousState.getIn(['userSigning'])).toBe(true); 74 | expect(previousState.getIn(['userSignup'])).toBe(false); 75 | 76 | expect(nextState.getIn(['userSigning'])).toBe(false); 77 | expect(nextState.getIn(['userSignup'])).toBe(true); 78 | }); 79 | 80 | it('should set swap userSignup and userSigning on LOGIN_FORM_IN', () => { 81 | const previousState = userFormReducer(initialState, 82 | { type: UserFormActions.REGISTER_FORM_IN }); 83 | const nextState = userFormReducer(previousState, 84 | { type: UserFormActions.LOGIN_FORM_IN }); 85 | 86 | expect(previousState.getIn(['userSigning'])).toBe(false); 87 | expect(previousState.getIn(['userSignup'])).toBe(true); 88 | 89 | expect(nextState.getIn(['userSigning'])).toBe(true); 90 | expect(nextState.getIn(['userSignup'])).toBe(false); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /client/redux/store/userForm/userForm.reducer.ts: -------------------------------------------------------------------------------- 1 | import { UserFormActions } from '../../actions/userForm/userForm.actions'; 2 | import { reimmutifyUserForm } from './userForm.transformers'; 3 | import { IUserForm } from './userForm.types'; 4 | import { INITIAL_STATE } from './userForm.initial-state'; 5 | 6 | // Define the reducer that will initiate state changes for userForm 7 | export function userFormReducer(state: IUserForm = INITIAL_STATE, action: any) { 8 | // will decide what state change is necessary based off the type 9 | switch (action.type) { 10 | case UserFormActions.LOGIN_FORM_IN: 11 | return state 12 | .updateIn(['userSigning'], val => true) 13 | .updateIn(['userSignup'], val => false); 14 | case UserFormActions.REGISTER_FORM_IN: 15 | return state 16 | .updateIn(['userSigning'], val => false) 17 | .updateIn(['userSignup'], val => true); 18 | case UserFormActions.LOGIN_FORM_OUT: 19 | case UserFormActions.REGISTER_FORM_OUT: 20 | return state 21 | .updateIn(['userSignup'], val => false) 22 | .updateIn(['userSigning'], val => false); 23 | default: 24 | return state; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /client/redux/store/userForm/userForm.transformers.ts: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | import { IUserForm, IUserFormItem } from './userForm.types'; 3 | 4 | // functions to change the state of the data 5 | // either immutable -> mutable or mutable -> immutable 6 | export function deimmutifyUserForm(state: IUserForm): Object { 7 | return state.toJS(); 8 | } 9 | 10 | export function reimmutifyUserForm(plain): IUserForm { 11 | return Map(plain ? plain : {}); 12 | } 13 | -------------------------------------------------------------------------------- /client/redux/store/userForm/userForm.types.ts: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | 3 | // Define an interface of the object that will be saved 4 | export interface IUserFormItem { 5 | userSigning: boolean; 6 | userSignup: boolean; 7 | } 8 | 9 | // Export the type so the reducer and store will understand 10 | export type IUserForm = Map; 11 | -------------------------------------------------------------------------------- /client/styles.scss: -------------------------------------------------------------------------------- 1 | /* latin */ 2 | @font-face { 3 | font-family: 'Fredoka One'; 4 | font-style: normal; 5 | font-weight: 400; 6 | src: local('Fredoka One'), local('FredokaOne-Regular'), url('/public/fonts/Fredoka_One.woff2') format('woff2'); 7 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215; 8 | } 9 | 10 | 11 | /* 12 | ========================================================================== 13 | * Reset browser default: 14 | * CSS so cross browser styling is more predictable 15 | ========================================================================== 16 | */ 17 | 18 | 19 | /* http://meyerweb.com/eric/tools/css/reset/ 20 | v2.0 | 20110126 21 | License: none (public domain) 22 | */ 23 | 24 | applet, 25 | html, 26 | body, 27 | div, 28 | span, 29 | object, 30 | iframe, 31 | h1, 32 | h2, 33 | h3, 34 | h4, 35 | h5, 36 | h6, 37 | p, 38 | blockquote, 39 | pre, 40 | a, 41 | abbr, 42 | acronym, 43 | address, 44 | big, 45 | cite, 46 | code, 47 | del, 48 | dfn, 49 | em, 50 | img, 51 | ins, 52 | kbd, 53 | q, 54 | s, 55 | samp, 56 | small, 57 | strike, 58 | strong, 59 | sub, 60 | sup, 61 | tt, 62 | var, 63 | b, 64 | u, 65 | i, 66 | center, 67 | dl, 68 | dt, 69 | dd, 70 | ol, 71 | ul, 72 | li, 73 | fieldset, 74 | form, 75 | label, 76 | legend, 77 | table, 78 | caption, 79 | tbody, 80 | tfoot, 81 | thead, 82 | tr, 83 | th, 84 | td, 85 | article, 86 | aside, 87 | canvas, 88 | details, 89 | embed, 90 | figure, 91 | figcaption, 92 | footer, 93 | header, 94 | hgroup, 95 | menu, 96 | nav, 97 | output, 98 | ruby, 99 | section, 100 | summary, 101 | time, 102 | mark, 103 | audio, 104 | video, 105 | button { 106 | margin: 0; 107 | padding: 0; 108 | border: 0; 109 | font-size: 100%; 110 | font: inherit; 111 | font-family: 'Fredoka One', Arial; 112 | vertical-align: baseline; 113 | } 114 | 115 | 116 | /* HTML5 display-role reset for older browsers */ 117 | 118 | article, 119 | aside, 120 | details, 121 | figcaption, 122 | figure, 123 | footer, 124 | header, 125 | hgroup, 126 | menu, 127 | nav, 128 | section { 129 | display: block; 130 | } 131 | 132 | ol, 133 | ul, 134 | li { 135 | list-style: none; 136 | } 137 | 138 | blockquote, 139 | q { 140 | quotes: none; 141 | } 142 | 143 | blockquote:before, 144 | blockquote:after, 145 | q:before, 146 | q:after { 147 | content: none; 148 | } 149 | 150 | table { 151 | border-collapse: collapse; 152 | border-spacing: 0; 153 | } 154 | 155 | label { 156 | height: 100%; 157 | } 158 | 159 | input:-webkit-autofill { 160 | -webkit-box-shadow: 0 0 0 1000px white inset; 161 | -moz-box-shadow: 0 0 0 1000px white inset; 162 | box-shadow: 0 0 0 1000px white inset; 163 | } 164 | 165 | html { 166 | position: relative; 167 | overflow-y: scroll; 168 | } 169 | 170 | 171 | /* 172 | -------------------------------------------------------------------------- 173 | * End Reset browser default 174 | -------------------------------------------------------------------------- 175 | */ -------------------------------------------------------------------------------- /client/vendor.ts: -------------------------------------------------------------------------------- 1 | /* 2 | ================================================================================== 3 | -- Vendor packages for webpack --------------------------------------------------- 4 | ================================================================================== 5 | ** This is where all vendor resources will be imported ** 6 | ** This file is used by webpack to include stable vendor packages ** 7 | ** The webpack file is located here: GOATstack/config/webpack/webpack.common.js ** 8 | ================================================================================== 9 | */ 10 | 11 | // Angular 12 | import '@angular/platform-browser'; 13 | import '@angular/platform-browser-dynamic'; 14 | import '@angular/core'; 15 | import '@angular/common'; 16 | import '@angular/common/http'; 17 | import '@angular/router'; 18 | 19 | import 'hammerjs/hammer'; 20 | 21 | // RxJS 22 | import 'rxjs'; 23 | 24 | import '@angular-redux/store'; 25 | import 'lodash'; 26 | import 'ng2-cookies/ng2-cookies'; 27 | 28 | // Other vendors for example jQuery, Lodash or Bootstrap 29 | // You can import js, ts, css, sass, ... 30 | 31 | require('./styles'); 32 | require('./loader'); -------------------------------------------------------------------------------- /config/env/default.ts: -------------------------------------------------------------------------------- 1 | /* 2 | ============================================================================================== 3 | These configuration settings get called no matter what Node's process.env.NODE_ENV is set to. 4 | ============================================================================================== 5 | */ 6 | 7 | export const defaultConfig = { 8 | // Change to use https 9 | https_secure: false, 10 | // You will need to generate a self signed ssl certificate 11 | // using the generator in ./scripts or use a trusted certificate 12 | cert_loc: './server/sslcerts/cert.pem', 13 | key_loc: './server/sslcerts/key.pem', 14 | 15 | port: process.env.PORT || 5000, 16 | host: process.env.HOST || '0.0.0.0', 17 | // Session Cookie settings 18 | sessionCookie: { 19 | // session expiration is set by default to 24 hours 20 | maxAge: 24 * (60 * 60 * 1000), 21 | // httpOnly flag makes sure the cookie is only accessed 22 | // through the HTTP protocol and not JS/browser 23 | httpOnly: true, 24 | // secure cookie should be turned to true to provide additional 25 | // layer of security so that the cookie is set only when working 26 | // in HTTPS mode. 27 | secure: false 28 | }, 29 | // sessionSecret should be changed for security measures and concerns 30 | sessionSecret: process.env.SESSION_SECRET || 'APP', 31 | // sessionKey is set to the generic sessionId key used by PHP applications 32 | // for obsecurity reasons 33 | sessionKey: 'sessionId', 34 | sessionCollection: 'sessions', 35 | userRoles: ['guest', 'user', 'admin'] 36 | }; 37 | -------------------------------------------------------------------------------- /config/env/development.ts: -------------------------------------------------------------------------------- 1 | /* 2 | =============================================== 3 | Used when process.env.NODE_ENV = 'development' 4 | =============================================== 5 | //This file adds config settings and overwrites config settings in the ./default.ts file 6 | //process.env.NODE_ENV is utilized in config/config.ts 7 | */ 8 | 9 | export const devEnv = { 10 | mongo: { 11 | uri: 'mongodb://localhost/dev', 12 | options: { 13 | useMongoClient: true 14 | }, 15 | // Enable mongoose debug mode 16 | debug: process.env.MONGODB_DEBUG || false 17 | }, 18 | cassandra: { 19 | contactPoints: ['127.0.0.1'], 20 | protocolOptions: { port: 9042 }, 21 | queryOptions: { consistency: 1 }, 22 | keyspace: 'dev' 23 | }, 24 | sql: { 25 | // uri: 'postgres://postgres:postgres@localhost:5432/GOATstack' 26 | database: 'dev', 27 | username: 'postgres', 28 | password: 'postgres', 29 | options: { 30 | host: 'localhost', 31 | dialect: 'postgres'||'mysql'||'mariadb'||'sqlite'||'mssql', 32 | logging: false, 33 | } 34 | }, 35 | seedDB: true 36 | }; 37 | -------------------------------------------------------------------------------- /config/env/production.ts: -------------------------------------------------------------------------------- 1 | /* 2 | =============================================== 3 | Used when process.env.NODE_ENV = 'production' 4 | =============================================== 5 | //This file adds config settings and overwrites config settings in the ./default.ts file 6 | //process.env.NODE_ENV is utilized in config/config.ts 7 | */ 8 | 9 | export const prodEnv = { 10 | port: process.env.PORT || 8443, 11 | // Binding to 127.0.0.1 is safer in production. 12 | host: process.env.HOST || '0.0.0.0', 13 | mongo: { 14 | uri: process.env.DB_URI || 'mongodb://localhost/prod', 15 | options: { 16 | useMongoClient: true, 17 | user: process.env.DB_USER || '', 18 | pass: process.env.DB_PW || '' 19 | }, 20 | // Enable mongoose debug mode 21 | debug: process.env.MONGODB_DEBUG || false 22 | }, 23 | cassandra: { 24 | contactPoints: ['127.0.0.1'], 25 | protocolOptions: { port: 9042 }, 26 | queryOptions: { consistency: 1 }, 27 | keyspace: 'prod' 28 | }, 29 | sql: { 30 | // uri: 'postgres://postgres:postgres@localhost:5432/GOATstack' 31 | database: 'prod', 32 | username: 'postgres', 33 | password: 'postgres', 34 | options: { 35 | host: 'localhost', 36 | dialect: 'postgres'||'mysql'||'mariadb'||'sqlite'||'mssql', 37 | logging: false, 38 | } 39 | }, 40 | seedDB: true 41 | }; 42 | -------------------------------------------------------------------------------- /config/env/test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | ====================================================================================== 3 | Used when process.env.NODE_ENV is equal to 'test' 4 | ====================================================================================== 5 | //This file adds config settings and overwrites config settings in the ./default.ts file 6 | //process.env.NODE_ENV is utilized in config/config.ts 7 | */ 8 | 9 | export const testEnv = { 10 | port: process.env.PORT || 7001, 11 | mongo: { 12 | uri: 'mongodb://localhost/test', 13 | options: { 14 | useMongoClient: true, 15 | user: '', 16 | pass: '' 17 | }, 18 | // Enable mongoose debug mode 19 | debug: process.env.MONGODB_DEBUG || false 20 | }, 21 | cassandra: { 22 | contactPoints: ['127.0.0.1'], 23 | protocolOptions: { port: 9042 }, 24 | queryOptions: { consistency: 1 }, 25 | keyspace: 'test' 26 | }, 27 | sql: { 28 | // uri: 'postgres://postgres:postgres@localhost:5432/GOATstack' 29 | database: 'test', 30 | username: 'postgres', 31 | password: 'postgres', 32 | options: { 33 | host: 'localhost', 34 | dialect: 'postgres'||'mysql'||'mariadb'||'sqlite'||'mssql', 35 | logging: false, 36 | } 37 | }, 38 | seedDB: true 39 | }; 40 | -------------------------------------------------------------------------------- /config/helpers.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var del = require('del'); 3 | 4 | var client = [ 5 | 'client/**/**/**/**/**/*.css*', 6 | 'client/**/**/**/**/**/*.js*', 7 | 'client/**/**/**/**/**/*.shim*', 8 | 'client/**/**/**/**/**/*.ngfactory.ts', 9 | 'client/**/**/**/**/**/*.ngstyle.ts', 10 | 'client/**/**/**/**/**/*.ngsummary.json', 11 | 'ngc-aot/**', 12 | '.com*/**', 13 | '.org*/**' 14 | ]; 15 | 16 | var all = client.concat([ 17 | 'dist/**', 18 | 'dist/.git/**' 19 | ]); 20 | 21 | var _root = path.resolve(process.cwd()); 22 | 23 | function root(args) { 24 | args = Array.prototype.slice.call(arguments, 0); 25 | return path.join.apply(path, [_root].concat(args)); 26 | } 27 | 28 | function cleanup(option) { 29 | switch (option) { 30 | case 'client': 31 | return del.sync(client); 32 | break; 33 | default: 34 | return del.sync(all); 35 | break; 36 | } 37 | } 38 | 39 | exports.root = root; 40 | exports.cleanup = cleanup; 41 | -------------------------------------------------------------------------------- /config/index.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | import {defaultConfig} from './env/default'; 4 | import {devEnv} from './env/development'; 5 | import {prodEnv} from './env/production'; 6 | import {testEnv} from './env/test'; 7 | 8 | function mergeConfig(): any { 9 | 10 | // Depending on the environment we will merge 11 | // the default assets and config to corresponding 12 | // environment files 13 | const environmentConfig = process.env.NODE_ENV === 'development' ? devEnv : 14 | process.env.NODE_ENV === 'test' ? testEnv : prodEnv; 15 | 16 | // Merge config files 17 | return _.merge(defaultConfig, environmentConfig); 18 | }; 19 | 20 | const config = mergeConfig(); 21 | export default config; 22 | -------------------------------------------------------------------------------- /config/other/.sass-lint.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | single-line-per-selector: 0 3 | space-after-colon: 0 4 | space-before-brace: 0 5 | property-sort-order: 0 6 | empty-args: 0 7 | indentation: 0 8 | empty-line-between-blocks: 0 9 | force-pseudo-nesting: 0 10 | pseudo-element: 0 11 | no-css-comments: 0 12 | no-empty-rulesets: 0 13 | no-important: 0 14 | no-vendor-prefixes: 0 15 | no-color-literals: 0 16 | no-color-keywords: 0 17 | no-qualifying-elements: 0 18 | no-trailing-whitespace: 0 19 | quotes: 0 20 | final-newline: 0 21 | force-element-nesting: 0 22 | no-ids: 0 23 | leading-zero: 0 24 | space-after-comma: 0 25 | space-around-operator: 0 26 | space-before-bang: 0 -------------------------------------------------------------------------------- /config/other/generate-ssl-certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ ! -e ../../server/server.ts ] 3 | then 4 | echo "Error: could not find main application server.js file" 5 | echo "You should run the generate-ssl-certs.sh script from the main MEAN application root directory" 6 | echo "i.e: bash scripts/generate-ssl-certs.sh" 7 | exit -1 8 | fi 9 | echo "Generating self-signed certificates..." 10 | mkdir -p ../config/sslcerts 11 | openssl genrsa -out ../config/sslcerts/key.pem 4096 12 | openssl req -new -key ../config/sslcerts/key.pem -out ../config/sslcerts/csr.pem 13 | openssl x509 -req -days 365 -in ../config/sslcerts/csr.pem -signkey ../config/sslcerts/key.pem -out ../config/sslcerts/cert.pem 14 | rm ../config/sslcerts/csr.pem 15 | chmod 600 ./config/sslcerts/key.pem ../config/sslcerts/cert.pem 16 | -------------------------------------------------------------------------------- /config/other/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "class-name": false, 7 | "eofline": false, 8 | "forin": false, 9 | "label-position": true, 10 | "member-access": false, 11 | "member-ordering": [ 12 | false, 13 | "static-before-instance", 14 | "variables-before-functions" 15 | ], 16 | "no-arg": true, 17 | "no-bitwise": true, 18 | "no-console": [ 19 | true, 20 | "debug", 21 | "info", 22 | "time", 23 | "timeEnd", 24 | "trace" 25 | ], 26 | "no-construct": true, 27 | "no-debugger": true, 28 | "no-duplicate-variable": true, 29 | "no-empty": false, 30 | "no-eval": true, 31 | "no-inferrable-types": false, 32 | "no-shadowed-variable": false, 33 | "no-string-literal": false, 34 | "no-switch-case-fall-through": true, 35 | "no-trailing-whitespace": false, 36 | "no-unused-expression": true, 37 | "no-use-before-declare": true, 38 | "no-var-keyword": true, 39 | "object-literal-sort-keys": false, 40 | "radix": true, 41 | "semicolon": [ 42 | "always" 43 | ], 44 | "triple-equals": [ 45 | true, 46 | "allow-null-check" 47 | ], 48 | "typedef-whitespace": [ 49 | true, 50 | { 51 | "call-signature": "nospace", 52 | "index-signature": "nospace", 53 | "parameter": "nospace", 54 | "property-declaration": "nospace", 55 | "variable-declaration": "nospace" 56 | } 57 | ], 58 | "variable-name": false, 59 | 60 | "use-input-property-decorator": true, 61 | "use-output-property-decorator": true, 62 | "use-host-property-decorator": true, 63 | "use-life-cycle-interface": false, 64 | "use-pipe-transform-interface": true 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /config/test-libs/karma-test-shim.js: -------------------------------------------------------------------------------- 1 | Error.stackTraceLimit = Infinity; 2 | 3 | require('core-js/es6'); 4 | require('core-js/es7/reflect'); 5 | 6 | require('zone.js/dist/zone'); 7 | require('zone.js/dist/long-stack-trace-zone'); 8 | require('zone.js/dist/proxy'); 9 | require('zone.js/dist/sync-test'); 10 | require('zone.js/dist/jasmine-patch'); 11 | require('zone.js/dist/async-test'); 12 | require('zone.js/dist/fake-async-test'); 13 | 14 | var appContext = require.context('../../client', true, /\.spec\.ts/); 15 | 16 | appContext.keys().forEach(appContext); 17 | 18 | var testing = require('@angular/core/testing'); 19 | var browser = require('@angular/platform-browser-dynamic/testing'); 20 | 21 | testing.TestBed.initTestEnvironment(browser.BrowserDynamicTestingModule, browser.platformBrowserDynamicTesting()); -------------------------------------------------------------------------------- /config/test-libs/karma.config.js: -------------------------------------------------------------------------------- 1 | var webpackConfig = require('../webpack/webpack.common')({ env: 'karma' }); 2 | 3 | module.exports = function (config) { 4 | 5 | var _config = { 6 | basePath: '../../', 7 | 8 | frameworks: ['jasmine'], 9 | 10 | plugins: [ 11 | require('karma-jasmine'), 12 | require('karma-webpack'), 13 | require('karma-sourcemap-loader'), 14 | require('karma-chrome-launcher'), 15 | require('karma-mocha-reporter'), 16 | require('karma-jasmine-html-reporter'), // click "Debug" in browser to see it 17 | require('karma-htmlfile-reporter') // crashing w/ strange socket error 18 | ], 19 | 20 | customLaunchers: { 21 | // From the CLI. Not used here but interesting 22 | // chrome setup for travis CI using chromium 23 | Chrome_travis_ci: { 24 | base: 'Chrome', 25 | flags: ['--no-sandbox'] 26 | } 27 | }, 28 | 29 | files: [ 30 | {pattern: './config/test-libs/karma-test-shim.js', watched: false} 31 | ], 32 | 33 | preprocessors: { 34 | './config/test-libs/karma-test-shim.js': ['webpack', 'sourcemap'] 35 | }, 36 | 37 | webpack: webpackConfig, 38 | 39 | webpackMiddleware: { 40 | stats: "none" 41 | }, 42 | 43 | webpackServer: { 44 | noInfo: true 45 | }, 46 | 47 | reporters: ['kjhtml', 'mocha'], 48 | port: 9876, 49 | colors: true, 50 | logLevel: config.LOG_INFO, 51 | autoWatch: false, 52 | browsers: ['Chrome'], 53 | singleRun: true 54 | }; 55 | 56 | config.set(_config); 57 | }; -------------------------------------------------------------------------------- /config/test-libs/protractor.config.js: -------------------------------------------------------------------------------- 1 | // FIRST TIME ONLY- run: 2 | // ./node_modules/.bin/webdriver-manager update 3 | // 4 | // Try: `npm run webdriver:update` 5 | // 6 | // AND THEN EVERYTIME ... 7 | // 1. Compile with `tsc` 8 | // 2. Make sure the test server (e.g., http-server: localhost:8080) is running. 9 | // 3. ./node_modules/.bin/protractor protractor.config.js 10 | // 11 | // To do all steps, try: `npm run e2e` 12 | 13 | var helpers = require('../helpers'); 14 | 15 | 16 | exports.config = { 17 | directConnect: true, 18 | 19 | // For angular tests 20 | useAllAngular2AppRoots: true, 21 | 22 | // Base URL for application server 23 | baseUrl: 'http://localhost:7001', 24 | 25 | // Spec patterns are relative to this config file 26 | specs: [ 27 | helpers.root('e2e/*.e2e-spec.js') 28 | ], 29 | 30 | // Capabilities to be passed to the webdriver instance. 31 | capabilities: { 32 | 'browserName': 'chrome' 33 | }, 34 | 35 | // Framework to use. Jasmine is recommended. 36 | framework: 'jasmine', 37 | 38 | allScriptsTimeout: 110000, 39 | 40 | onPrepare: function () { 41 | browser.ignoreSynchronization = true; 42 | browser.get(''); 43 | 44 | // SpecReporter 45 | var SpecReporter = require('jasmine-spec-reporter').SpecReporter; 46 | jasmine.getEnv().addReporter(new SpecReporter({displayStacktrace: 'all'})); 47 | }, 48 | 49 | jasmineNodeOpts: { 50 | showTiming: true, 51 | showColors: true, 52 | isVerbose: false, 53 | includeStackTrace: false, 54 | defaultTimeoutInterval: 40000, 55 | print: function() {} 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /config/test-libs/server.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var exec = require('child_process').exec; 4 | var glob = require('glob'); 5 | 6 | var Jasmine = require('jasmine'); 7 | var jasmine = new Jasmine(); 8 | var JasmineReporter = require('jasmine-spec-reporter').SpecReporter; 9 | 10 | process.env.NODE_ENV = 'test'; 11 | 12 | // Server SPEC tests 13 | glob('server/**/api/**', function(er, files) { 14 | exec('tsc ' + files.join(' ') + ' --outDir dist', () => { 15 | 16 | jasmine.loadConfig({ 17 | spec_dir: 'dist', 18 | spec_files: [ 19 | 'server/mongo-db/api/**/*.spec.js', 20 | 'server/mongo-db/api/user/user.integration.js', 21 | 'server/mongo-db/api/**/*.integration.js' 22 | ] 23 | }); 24 | 25 | jasmine.env.clearReporters(); 26 | jasmine.addReporter(new JasmineReporter()); 27 | 28 | jasmine.execute(); 29 | 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /config/webpack/webpack.client.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var webpackMerge = require('webpack-merge'); 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | var WebpackShellPlugin = require('webpack-shell-plugin'); 5 | var CopyWebpackPlugin = require('copy-webpack-plugin'); 6 | var commonConfig = require('./webpack.common.js'); 7 | var chalk = require('chalk'); 8 | 9 | const helpers = require('../helpers'); 10 | 11 | const cmd = require('../scripts').cmd; 12 | 13 | process.noDeprecation = true; 14 | 15 | const generalConfig = { 16 | 17 | // Specify descriptions for all webpack environments 18 | devtool: { 19 | dev: 'cheap-module-eval-source-map', 20 | prod: 'source-map', 21 | test: 'cheap-module-eval-source-map' 22 | }, 23 | 24 | output: { 25 | dev: { 26 | path: helpers.root('dist/client'), 27 | publicPath: 'http://localhost:1701/', 28 | filename: '[name].js', 29 | chunkFilename: '[id].chunk.js' 30 | }, 31 | prod: { 32 | path: helpers.root('dist/client'), 33 | filename: '[name].js', 34 | chunkFilename: '[id].chunk.js' 35 | }, 36 | test: { 37 | path: helpers.root('dist/client'), 38 | publicPath: 'http://localhost:7001/', 39 | filename: '[name].js', 40 | chunkFilename: '[id].chunk.js' 41 | } 42 | }, 43 | 44 | devServer: { 45 | dev: { 46 | port: 1701, 47 | historyApiFallback: { 48 | index: 'http://localhost:1701/index.html' 49 | }, 50 | proxy: [{ 51 | context: ['/api', '/auth', '/socket.io-client'], 52 | target: 'http://localhost:5000/', 53 | secure: false 54 | }], 55 | stats: { 56 | chunks: false 57 | } 58 | }, 59 | prod: {}, 60 | test: {} 61 | }, 62 | 63 | stats: { 64 | dev: {}, 65 | prod: {}, 66 | test: 'none' 67 | } 68 | }; 69 | 70 | module.exports = function(options) { 71 | 72 | 73 | return webpackMerge(commonConfig(options), { 74 | devtool: generalConfig.devtool[options.env], 75 | output: generalConfig.output[options.env], 76 | devServer: generalConfig.devServer[options.env], 77 | stats: generalConfig.stats[options.env], 78 | 79 | plugins: options.env === 'dev' ? [ 80 | new ExtractTextPlugin('styles.css'), 81 | new WebpackShellPlugin({ 82 | onBuildStart:[`${cmd.webpack} --hide-modules true --env server:dev --watch`], 83 | onBuildEnd:[`${cmd.nodemon} dist --watch dist`] 84 | }) 85 | ] : options.env === 'test' ? [ 86 | new ExtractTextPlugin('styles.css') 87 | ] : [ 88 | new webpack.NoEmitOnErrorsPlugin(), 89 | new webpack.optimize.UglifyJsPlugin({ 90 | mangle: { 91 | keep_fnames: true 92 | } 93 | }), 94 | new ExtractTextPlugin('styles.css'), 95 | new WebpackShellPlugin({ 96 | onBuildStart:[`${cmd.webpack} --hide-modules true --env server:prod${ options.e2e ? ':e2e' : '' }`] 97 | }), 98 | new CopyWebpackPlugin([ 99 | { 100 | from: helpers.root('package.json'), 101 | to: helpers.root('dist'), 102 | transform: (content, path) => { 103 | return content.toString().replace(/npm run dev/, 'node index'); 104 | } 105 | }, 106 | { 107 | from: helpers.root('public'), 108 | to: helpers.root('dist/public') 109 | } 110 | ]) 111 | ] 112 | }); 113 | } -------------------------------------------------------------------------------- /config/webpack/webpack.common.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | var nodeExternals = require('webpack-node-externals'); 5 | 6 | var helpers = require('../helpers'); 7 | 8 | module.exports = function(options) { 9 | const prod = options.env === 'prod'; 10 | 11 | var config = { 12 | entry: { 13 | 'polyfills': './client/polyfills.ts', 14 | 'vendor': './client/vendor.ts', 15 | 'app': './client/app.ts' 16 | }, 17 | 18 | module: { 19 | rules: [ 20 | { 21 | test: /component\.ts/, 22 | loader: 'string-replace-loader', 23 | query: { 24 | search: '.css', 25 | replace: '.scss' 26 | } 27 | }, 28 | { 29 | test: /\.ts$/, 30 | use: ['awesome-typescript-loader', 'angular2-template-loader'] 31 | }, 32 | { 33 | test: /\.html$/, 34 | loader: 'html-loader?-attrs' 35 | }, 36 | { 37 | test: /\.(png|svg|jpg)$/, 38 | loader: 'file-loader', 39 | query: { 40 | 'name': 'public/assets/[name].[ext]' 41 | } 42 | }, 43 | { 44 | test: /\.scss/, 45 | include: [helpers.root('client/styles.scss'), helpers.root('client/loader.scss')], 46 | loader: ExtractTextPlugin.extract({ 47 | fallback: 'style-loader', 48 | use: 'css-loader?sourceMap!sass-loader?sourceMap' 49 | }) 50 | }, 51 | { 52 | test: /\.scss/, 53 | exclude: [helpers.root('client/styles.scss'), helpers.root('client/loader.scss')], 54 | loader: 'to-string-loader!css-loader?sourceMap!sass-loader?sourceMap' 55 | }, 56 | ] 57 | }, 58 | 59 | resolve: { 60 | extensions: ['.ts', '.js', '.scss'] 61 | }, 62 | 63 | plugins: [ 64 | new webpack.optimize.CommonsChunkPlugin({ 65 | name: ['app', 'vendor', 'polyfills'] 66 | }), 67 | 68 | new HtmlWebpackPlugin({ 69 | template: 'client/index.html' 70 | }), 71 | 72 | new webpack.ContextReplacementPlugin( // fixes angular linker WARNING 73 | /angular(\\|\/)core(\\|\/)@angular/, 74 | helpers.root('src') 75 | ) 76 | ] 77 | }; 78 | 79 | if (prod) { 80 | config.entry.app = './client/app-aot.ts'; 81 | 82 | config.module.rules[5] = { 83 | test: /\.css$/, 84 | exclude: [helpers.root('client/styles.css'), helpers.root('client/loader.css')], 85 | loader: 'raw-loader' 86 | }; 87 | 88 | config.module.rules.splice(0,1); 89 | } 90 | 91 | if (options.env === 'karma') { 92 | delete config.entry; 93 | // delete config.entry.polyfills; 94 | delete config.plugins; 95 | config.devtool = 'inline-source-map'; 96 | config.stats = { warnings: false }; 97 | 98 | config.module.rules[3].loader = 'null-loader'; 99 | config.module.rules[4].loader = 'null-loader'; 100 | } 101 | 102 | return config; 103 | } -------------------------------------------------------------------------------- /config/webpack/webpack.server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var nodeExternals = require('webpack-node-externals'); 3 | var WebpackShellPlugin = require('webpack-shell-plugin'); 4 | 5 | const helpers = require('../helpers'); 6 | const cmd = require('../scripts').cmd; 7 | 8 | module.exports = function(options) { 9 | 10 | const ENV = process.env.ENV = process.env.NODE_ENV = options.env === 'dev' ? 'development' : 11 | options.env === 'prod' ? 'production' : 'test'; 12 | const METADATA = { 13 | ENV: ENV, 14 | }; 15 | 16 | return { 17 | entry: { 18 | 'server': './server/server.ts', 19 | }, 20 | 21 | output: { 22 | path: helpers.root('dist'), 23 | filename: 'index.js' 24 | }, 25 | 26 | stats: 'none', 27 | target: 'node', 28 | externals: [nodeExternals()], 29 | 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.ts$/, 34 | loader: 'awesome-typescript-loader' 35 | } 36 | ] 37 | }, 38 | 39 | resolve: { 40 | extensions: ['.ts', '.js'] 41 | }, 42 | 43 | plugins: options.env === 'dev' ? [ 44 | // Dev Plugins 45 | new webpack.DefinePlugin({ 46 | 'ENV': JSON.stringify(METADATA.ENV), 47 | 'process.env': { 48 | 'ENV': JSON.stringify(METADATA.ENV), 49 | 'NODE_ENV': JSON.stringify(METADATA.ENV) 50 | } 51 | }), 52 | new WebpackShellPlugin({ 53 | // onBuildEnd:[`${cmd.webpackDevServer} --inline --env dev`] 54 | }) 55 | ] : options.env === 'test' ? [ 56 | // Test Plugins 57 | new webpack.DefinePlugin({ 58 | 'ENV': JSON.stringify(METADATA.ENV), 59 | 'process.env': { 60 | 'ENV': JSON.stringify(METADATA.ENV), 61 | 'NODE_ENV': JSON.stringify(METADATA.ENV) 62 | } 63 | }) 64 | ] : options.env === 'prod' ? [ 65 | // Prod Plugins 66 | new webpack.optimize.UglifyJsPlugin({ 67 | mangle: { 68 | keep_fnames: true 69 | } 70 | }), 71 | ] : [ 72 | new webpack.optimize.UglifyJsPlugin({ 73 | mangle: { 74 | keep_fnames: true 75 | } 76 | }), 77 | new webpack.DefinePlugin({ 78 | 'ENV': JSON.stringify(METADATA.ENV), 79 | 'process.env': { 80 | 'ENV': JSON.stringify(METADATA.ENV), 81 | 'NODE_ENV': JSON.stringify(METADATA.ENV) 82 | } 83 | }) 84 | ] 85 | }; 86 | } -------------------------------------------------------------------------------- /e2e/app.e2e-spec.js: -------------------------------------------------------------------------------- 1 | describe('App E2E Tests', function () { 2 | 3 | var EC = protractor.ExpectedConditions; 4 | var defaultConfig = eval(require('typescript') 5 | .transpile(require('graceful-fs') 6 | .readFileSync('./config/env/default.ts') 7 | .toString())); 8 | 9 | it('should contain correct title tag', function () { 10 | expect(browser.getTitle()).toEqual("GOATstack"); 11 | }); 12 | 13 | it('should contain correct favicon', function () { 14 | expect(element(by.id('favicon')).getAttribute('href')) 15 | .toEqual('http://localhost:7001/public/assets/favicon.png'); 16 | }); 17 | 18 | it('should contain correct meta description', function () { 19 | expect(element(by.name('description')).getAttribute('content')).toEqual("The Greatest of All Time Stack!"); 20 | }); 21 | 22 | it('should contain correct meta keywords', function () { 23 | expect(element(by.name('keywords')).getAttribute('content')) 24 | .toEqual("redux, node, mongo, express, angular2, ng2, jasmine, karma, protractor"); 25 | }); 26 | 27 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "goat-stack", 3 | "version": "4.3.0", 4 | "description": "A MEAN stack boilerplate using Angular 2", 5 | "engines": { 6 | "node": "8.10.1" 7 | }, 8 | "scripts": { 9 | "start": "npm run dev", 10 | "test": "node -e \"require('./config/scripts').startTest()\"", 11 | "dev": "node -e \"require('./config/scripts').startDev()\"", 12 | "prod": "node -e \"require('./config/scripts').startProd()\"", 13 | "e2e": "node -e \"require('./config/scripts').startE2E()\"", 14 | "e2e:prod": "node -e \"require('./config/scripts').startProdE2E()\"", 15 | "deploy:heroku": "node -e \"require('./config/scripts').herokuPrompt()\"", 16 | "cleanup": "node -e \"require('./config/helpers').cleanup()\"" 17 | }, 18 | "dependencies": { 19 | "@angular-redux/store": "^6.5.7", 20 | "@angular/animations": "^5.0.1", 21 | "@angular/cdk": "^2.0.0-beta.12", 22 | "@angular/common": "^5.0.1", 23 | "@angular/compiler": "^5.0.1", 24 | "@angular/compiler-cli": "^5.0.1", 25 | "@angular/core": "^5.0.1", 26 | "@angular/http": "^5.0.1", 27 | "@angular/forms": "^5.0.1", 28 | "@angular/platform-browser": "^5.0.1", 29 | "@angular/platform-browser-dynamic": "^5.0.1", 30 | "@angular/platform-server": "^5.0.1", 31 | "@angular/router": "^5.0.1", 32 | "body-parser": "^1.18.2", 33 | "bootstrap": "3.3.7", 34 | "cassandra-driver": "^3.3.0", 35 | "chalk": "2.0.1", 36 | "composable-middleware": "0.3.0", 37 | "connect-mongo": "2.0.0", 38 | "cookie-parser": "1.4.3", 39 | "core-js": "2.5.1", 40 | "express": "^4.16.2", 41 | "express-jwt": "^5.3.0", 42 | "express-session": "^1.15.6", 43 | "graceful-fs": "4.1.11", 44 | "hammerjs": "2.0.8", 45 | "immutable": "3.8.2", 46 | "jsonwebtoken": "^8.1.0", 47 | "lodash": "4.17.4", 48 | "method-override": "^2.3.10", 49 | "mongodb": "^2.2.33", 50 | "mongoose": "^4.12.5", 51 | "morgan": "^1.9.0", 52 | "ng2-cookies": "^1.0.12", 53 | "passport": "0.4.0", 54 | "passport-facebook": "2.1.1", 55 | "passport-google-oauth20": "1.0.0", 56 | "passport-local": "1.0.0", 57 | "pg": "^7.3.0", 58 | "preboot": "^5.1.7", 59 | "redux": "^3.7.2", 60 | "redux-localstorage": "0.4.1", 61 | "reflect-metadata": "^0.1.10", 62 | "rxjs": "^5.5.2", 63 | "sequelize": "4.20.1", 64 | "serialize-javascript": "^1.4.0", 65 | "socket.io": "^2.0.4", 66 | "socket.io-client": "^2.0.4", 67 | "socketio-jwt": "4.5.0", 68 | "typescript": "2.6.1", 69 | "zone.js": "^0.8.18" 70 | }, 71 | "devDependencies": { 72 | "@types/body-parser": "^1.16.7", 73 | "@types/cassandra-driver": "^3.2.1", 74 | "@types/chalk": "2.2.0", 75 | "@types/cookie-parser": "^1.4.1", 76 | "@types/core-js": "^0.9.43", 77 | "@types/express": "^4.0.39", 78 | "@types/express-session": "^1.15.5", 79 | "@types/glob": "5.0.33", 80 | "@types/graceful-fs": "4.1.1", 81 | "@types/hammerjs": "2.0.35", 82 | "@types/jasmine": "^2.6.2", 83 | "@types/jsonwebtoken": "^7.2.3", 84 | "@types/lodash": "^4.14.80", 85 | "@types/method-override": "^0.0.31", 86 | "@types/mongodb": "^2.2.15", 87 | "@types/mongoose": "^4.7.24", 88 | "@types/morgan": "^1.7.35", 89 | "@types/node": "^8.0.47", 90 | "@types/passport": "^0.3.5", 91 | "@types/passport-local": "^1.0.32", 92 | "@types/proxyquire": "1.3.28", 93 | "@types/sequelize": "^4.0.78", 94 | "@types/sinon": "^2.3.7", 95 | "@types/socket.io": "^1.4.31", 96 | "@types/socket.io-client": "1.4.31", 97 | "@types/supertest": "^2.0.3", 98 | "angular2-template-loader": "^0.6.2", 99 | "awesome-typescript-loader": "^3.2.3", 100 | "canonical-path": "0.0.2", 101 | "concurrently": "^3.5.0", 102 | "copy-webpack-plugin": "4.2.0", 103 | "css-loader": "^0.28.7", 104 | "del": "^3.0.0", 105 | "extract-loader": "1.0.1", 106 | "extract-text-webpack-plugin": "^3.0.2", 107 | "file-loader": "^1.1.5", 108 | "glob": "^7.1.2", 109 | "html-loader": "^0.5.1", 110 | "html-webpack-plugin": "^2.30.1", 111 | "inquirer": "^3.3.0", 112 | "jasmine": "^2.8.0", 113 | "jasmine-core": "^2.8.0", 114 | "jasmine-sinon": "0.4.0", 115 | "jasmine-spec-reporter": "^4.2.1", 116 | "karma": "^1.7.1", 117 | "karma-chrome-launcher": "^2.2.0", 118 | "karma-cli": "1.0.1", 119 | "karma-htmlfile-reporter": "0.3.5", 120 | "karma-jasmine": "1.1.0", 121 | "karma-jasmine-html-reporter": "0.2.2", 122 | "karma-mocha-reporter": "^2.2.5", 123 | "karma-remap-istanbul": "^0.6.0", 124 | "karma-sourcemap-loader": "0.3.7", 125 | "karma-webpack": "^2.0.5", 126 | "node-sass": "^4.5.3", 127 | "nodemon": "1.12.1", 128 | "null-loader": "0.1.1", 129 | "protractor": "^5.2.0", 130 | "proxyquire": "^1.8.0", 131 | "raw-loader": "0.5.1", 132 | "redux-logger": "^3.0.6", 133 | "sass-lint": "1.12.1", 134 | "sass-loader": "^6.0.6", 135 | "sinon": "^4.0.2", 136 | "string-replace-loader": "^1.3.0", 137 | "style-loader": "^0.19.0", 138 | "supertest": "^3.0.0", 139 | "to-string-loader": "1.1.5", 140 | "webpack": "^3.8.1", 141 | "webpack-dev-server": "^2.9.3", 142 | "webpack-merge": "^4.1.0", 143 | "webpack-node-externals": "^1.6.0", 144 | "webpack-shell-plugin": "0.5.0" 145 | }, 146 | "repository": { 147 | "type": "git", 148 | "url": "https://github.com/projectSHAI/GOAT-stack" 149 | }, 150 | "keywords": [ 151 | "node", 152 | "heroku", 153 | "express", 154 | "mongodb", 155 | "OAuth", 156 | "GOAT-stack" 157 | ], 158 | "license": "MIT" 159 | } 160 | -------------------------------------------------------------------------------- /public/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectSHAI/GOATstack/d6bee4b8f6efb0c521a6a83a7ff10aace8a7643e/public/assets/favicon.png -------------------------------------------------------------------------------- /public/assets/footer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectSHAI/GOATstack/d6bee4b8f6efb0c521a6a83a7ff10aace8a7643e/public/assets/footer.jpg -------------------------------------------------------------------------------- /public/assets/loader/fire-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectSHAI/GOATstack/d6bee4b8f6efb0c521a6a83a7ff10aace8a7643e/public/assets/loader/fire-1.png -------------------------------------------------------------------------------- /public/assets/loader/fire-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectSHAI/GOATstack/d6bee4b8f6efb0c521a6a83a7ff10aace8a7643e/public/assets/loader/fire-2.png -------------------------------------------------------------------------------- /public/assets/loader/space-goat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectSHAI/GOATstack/d6bee4b8f6efb0c521a6a83a7ff10aace8a7643e/public/assets/loader/space-goat.png -------------------------------------------------------------------------------- /public/assets/loader/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectSHAI/GOATstack/d6bee4b8f6efb0c521a6a83a7ff10aace8a7643e/public/assets/loader/star.png -------------------------------------------------------------------------------- /public/assets/loader/star.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /public/assets/octocat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectSHAI/GOATstack/d6bee4b8f6efb0c521a6a83a7ff10aace8a7643e/public/assets/octocat.png -------------------------------------------------------------------------------- /public/fonts/Fredoka_One.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectSHAI/GOATstack/d6bee4b8f6efb0c521a6a83a7ff10aace8a7643e/public/fonts/Fredoka_One.woff2 -------------------------------------------------------------------------------- /server/cassandra-db/api/user/prepared.statements.ts: -------------------------------------------------------------------------------- 1 | //////////////////////// 2 | // Prepared statements// 3 | //////////////////////// 4 | const Uuid = require('cassandra-driver').types.Uuid; 5 | 6 | class UserStmts { 7 | 8 | // create tables 9 | public userTable: string = `CREATE TABLE IF NOT EXISTS users ( 10 | id uuid, 11 | email text, 12 | created timestamp, 13 | password text, 14 | salt text, 15 | facebook text, 16 | firstname text, 17 | github text, 18 | google text, 19 | lastname text, 20 | middlename text, 21 | role text, 22 | username text, 23 | PRIMARY KEY (email) 24 | );`; 25 | 26 | // delete tables 27 | public truncateUserTable: string = `TRUNCATE users`; 28 | 29 | /////////////////////// 30 | // Seeding //////////// 31 | /////////////////////// 32 | // seed statements 33 | public seedUserTable: Array<{ query: string, params: Array }> = [{ 34 | query: 'INSERT INTO users (id, email, created, password, salt, role, username ) VALUES (?, ?, ?, ?, ?, ?, ?)', 35 | params: [Uuid.random(), 'admin@admin.com', Date.now(), 'fUnz3sNJaiLSotLsOX0kqBuYD9MH9lotMyAdBtbCyPBnFToAABMPqxv4kZ/E16gk/zp6/rtBEOnQZsPSnS1LmQ==', 'Lv1oeSSHMut0kKRFFDyk5g==', 'admin', 'AdMiN'] 36 | }, 37 | { 38 | query: 'INSERT INTO users (id, email, created, password, salt, role, username ) VALUES (?, ?, ?, ?, ?, ?, ?)', 39 | params: [Uuid.random(), 'test@test.com', Date.now(), 'JOe+CGVaNXUK2wZuOLzhpiCfXO8K/18R5mhoE5ji5MGcxMF/otA3QaLeMa9ELw0W8zyr0VvQbW9NHpA350MGbg==', '61DynVS8QOWjMy7bRdkUtw==', 'test', 'test'] 40 | }]; 41 | 42 | //////////// 43 | // queries// 44 | //////////// 45 | 46 | // create 47 | public insertRow: string = `INSERT INTO users (id, email, created, password, salt, role, username ) VALUES (?, ?, ?, ?, ?, ?, ?)`; 48 | // read 49 | public findByEmail: string = 'SELECT email, firstname, lastname, middlename, role, username FROM users WHERE email = ?'; 50 | public allRows: string = 'SELECT email, firstname, lastname, middlename, role, username FROM users'; 51 | // update - NA 52 | // delete - NA 53 | 54 | } 55 | 56 | export default new UserStmts; -------------------------------------------------------------------------------- /server/cassandra-db/api/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import UserModel from './user.model'; 2 | import { client } from '../../../cassandra-db'; 3 | import config from '../../../../config'; 4 | import * as jwt from 'jsonwebtoken'; 5 | 6 | // Handles status codes and error message json 7 | // specificity: error 8 | function handleError(res, err) { 9 | if (err) { 10 | res.status(500).json(err); 11 | } 12 | } 13 | 14 | function validationError(res, err) { 15 | if (err) { 16 | res.status(422).json(err); 17 | } 18 | } 19 | 20 | export function index(req, res) { 21 | let users = []; 22 | return UserModel.allUsers() 23 | .then(result => { 24 | users = result.rows 25 | res.json(users); 26 | }) 27 | .catch(err => { 28 | handleError(res, err) 29 | }); 30 | } 31 | 32 | export function show(req, res, next) { 33 | const userEmail = req.params.email; 34 | 35 | return UserModel.userByEmail(userEmail) 36 | .then(result => { 37 | if (!result) { 38 | return res.status(404).end(); 39 | } 40 | res.json({ 41 | username: result.rows[0].username, 42 | firstname: result.rows[0].firstname, 43 | lastname: result.rows[0].lastname 44 | }); 45 | }) 46 | .catch(err => { 47 | handleError(res, err) 48 | });; 49 | } 50 | 51 | export function changePassword(req, res) { 52 | const userEmail = req.user.email; 53 | const oldPass = String(req.body.oldPassword); 54 | const newPass = String(req.body.newPassword); 55 | 56 | return UserModel.updatePassword(userEmail, oldPass, newPass, res) 57 | .then((result) => { 58 | res.status(204).end(); 59 | }) 60 | .catch(err => { 61 | validationError(res, err); 62 | }); 63 | } 64 | 65 | export function create(req, res, next) { 66 | const user = req.body; 67 | return UserModel.userByEmail(user.email).then(result => { 68 | if (result.rows[0] === undefined) { 69 | return UserModel.insertUser(user.email, user.username, user.password) 70 | .then(result => { 71 | const token = jwt.sign( 72 | { 73 | email: user.email, 74 | role: user.role 75 | }, 76 | config.sessionSecret, 77 | { expiresIn: 60 * 60 * 5 }); 78 | 79 | req.headers.token = token; 80 | req.user = user; 81 | next(); 82 | 83 | }) 84 | .catch(err => { 85 | validationError(res, err) 86 | }); 87 | } 88 | else { 89 | const duplicate: object = { message: 'Email is already in use!' }; 90 | 91 | validationError(res, duplicate); 92 | return res.status(403).json(duplicate); 93 | } 94 | }).catch(); 95 | 96 | } 97 | 98 | export function me(req, res, next) { 99 | const token = req.headers.token; 100 | const userEmail = req.user.email; 101 | 102 | return UserModel.userByEmail(userEmail) 103 | .then(result => { 104 | const user = result.rows[0]; 105 | if (!user) return res.status(401).json({ message: 'User does not exist' }); 106 | 107 | if (token) res.json({ token, user }); 108 | else res.json(user); 109 | }) 110 | .catch(err => next(err)); 111 | } -------------------------------------------------------------------------------- /server/cassandra-db/api/user/user.integration.ts: -------------------------------------------------------------------------------- 1 | import app from '../../../server'; 2 | import request = require('supertest'); 3 | import { client } from '../../../cassandra-db'; 4 | import DbModel from '../../db.model'; 5 | import UserModel from './user.model'; 6 | 7 | // User Endpoint testing 8 | describe('User API:', function () { 9 | let user; 10 | let token; 11 | 12 | // users are cleared from DB seeding 13 | // add a new testing user 14 | beforeAll(done => { 15 | UserModel.userByEmail('test@test.com') 16 | .then(result => { 17 | user = result.rows[0]; 18 | done(); 19 | }) 20 | .catch(err => { 21 | expect(err).not.toBeDefined(); 22 | done(); 23 | }); 24 | }); 25 | 26 | 27 | // Encapsolate GET me enpoint 28 | describe('GET /api/users/me cassandra', function () { 29 | 30 | // before every 'it' get new OAuth token representing the user 31 | beforeAll(function (done) { 32 | setTimeout(() => request(app) 33 | .post('/auth/local') 34 | .send({ 35 | email: 'test@test.com', 36 | password: 'test1' 37 | }) 38 | .expect(200) 39 | .end((err, res) => { 40 | if (err) { 41 | done.fail(err); 42 | } else { 43 | token = res.body.token; 44 | done(); 45 | } 46 | }), 2000); 47 | }); 48 | 49 | // If the token was properly set inside the header of the request 50 | // it should respond with a 200 status code with the user json 51 | it('should respond with a user profile when authenticated', function (done) { 52 | request(app) 53 | .get('/api/users/me') 54 | .set('authorization', 'Bearer ' + token) 55 | .expect(200) 56 | .expect('Content-Type', /json/) 57 | .end((err, res) => { 58 | if (err) { 59 | done.fail(err); 60 | } else { 61 | expect(res.body.username).toEqual(user.username); 62 | expect(res.body.email).toEqual(user.email); 63 | done(); 64 | } 65 | }); 66 | }); 67 | 68 | // If the token was improperly / not set to the header 69 | // status code 401 should be thrown 70 | it('should respond with a 401 when not authenticated', function (done) { 71 | request(app) 72 | .get('/api/users/me') 73 | .expect(401) 74 | .end((err, res) => { 75 | if (err) { 76 | done.fail(err); 77 | } else { 78 | done(); 79 | } 80 | }); 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /server/cassandra-db/api/user/user.model.ts: -------------------------------------------------------------------------------- 1 | import { client } from '../../../cassandra-db'; 2 | import UserStmts from './prepared.statements'; 3 | 4 | const Uuid = require('cassandra-driver').types.Uuid, 5 | crypto = require('crypto'); 6 | // Define Prepared Statments 7 | const allRows: string = UserStmts.allRows, 8 | findByEmail: string = UserStmts.findByEmail, 9 | insertRow: string = UserStmts.insertRow; 10 | 11 | 12 | class UserModel { 13 | 14 | private password: string; 15 | private queryOptions: object = { prepared: true }; 16 | private updatePw: string = 'UPDATE users SET password = ?, salt = ? WHERE email = ?'; 17 | private credentials: string = 'SELECT email, firstname, lastname, middlename, role, username, password, salt FROM users WHERE email = ?' 18 | 19 | /* 20 | Auth 21 | */ 22 | 23 | makeSalt(byteSize?: number): any { 24 | let defaultByteSize = 16; 25 | if (!byteSize) { 26 | byteSize = defaultByteSize; 27 | } 28 | return crypto.randomBytes(byteSize).toString('base64'); 29 | } 30 | 31 | encryptPassword(password: string, salt: string): any { 32 | const saltBuffer = new Buffer(salt, 'base64'); 33 | const defaultIterations = 10000; 34 | const defaultKeyLength = 64; 35 | 36 | return crypto.pbkdf2Sync(password, saltBuffer, defaultIterations, defaultKeyLength, 'sha512').toString('base64'); 37 | 38 | }; 39 | 40 | randNum(min, max) { 41 | min = Math.ceil(min); 42 | max = Math.floor(max); 43 | return Math.floor(Math.random() * (max - min)) + min; //The maximum is exclusive and the minimum is inclusive 44 | } 45 | 46 | authenticate(dbPassword: string, dbSalt: string, providedPassword: string) { 47 | return this.encryptPassword(providedPassword, dbSalt) === dbPassword; 48 | }; 49 | 50 | /* 51 | Queries 52 | */ 53 | allUsers(): Promise { 54 | return client.execute(allRows, undefined, this.queryOptions); 55 | } 56 | 57 | userByEmail(email: string): Promise { 58 | return client.execute(findByEmail, [email], this.queryOptions); 59 | } 60 | 61 | getCredentials(email: string): Promise { 62 | return client.execute(this.credentials, [email], this.queryOptions); 63 | } 64 | 65 | updatePassword(email: string, oldPW: string, newPass: string, res): Promise { 66 | 67 | return this.getCredentials(email).then(result => { 68 | const dbPW: string = result.rows[0].password; 69 | const dbSalt: string = result.rows[0].salt; 70 | if (this.authenticate(dbPW, dbSalt, oldPW)) { 71 | const byteSize: number = 16; 72 | const newSalt: string = this.makeSalt(byteSize); 73 | const newHashedPW = this.encryptPassword(newPass, newSalt); 74 | return client.execute(this.updatePw, [newHashedPW, newSalt, email], this.queryOptions); 75 | } 76 | else { 77 | res.status(403).end(); 78 | } 79 | }).catch(err => console.error(err)); 80 | 81 | } 82 | 83 | insertUser(email: string, username: string, password: string): Promise { 84 | 85 | const byteSize: number = 16; 86 | const id: string = String(Uuid.random()); 87 | const salt: string = this.makeSalt(byteSize); 88 | const newHashedPW = this.encryptPassword(password, salt); 89 | const queryOptions = { 90 | prepared: true 91 | }; 92 | return client.execute(insertRow, [id, email, Date.now(), newHashedPW, salt, 'user', username], queryOptions); 93 | } 94 | 95 | } 96 | 97 | export default new UserModel; -------------------------------------------------------------------------------- /server/cassandra-db/api/user/user.router.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * GET /api/user -> allUsers 3 | * POST /api/user -> create 4 | * GET /api/user/:email -> show 5 | * PUT /api/user/:email/password -> changePassword 6 | * DELETE /api/user/:email -> destroy 7 | */ 8 | 9 | let express = require('express'); 10 | import * as auth from '../../auth/auth.service'; 11 | import * as UserController from './user.controller'; 12 | 13 | let router = express.Router(); 14 | 15 | router.get('/', auth.hasRole('admin'), UserController.index); 16 | router.put('/:email/password', auth.isAuthenticated(), UserController.changePassword); 17 | 18 | router.post('/', UserController.create, UserController.me); 19 | router.get('/me', auth.isAuthenticated(), UserController.me); 20 | router.get('/:email', auth.isAuthenticated(), UserController.show); 21 | 22 | export { router as userRoutes }; 23 | -------------------------------------------------------------------------------- /server/cassandra-db/api/user/user.spec.ts: -------------------------------------------------------------------------------- 1 | import proxyquire = require('proxyquire'); 2 | let pq = proxyquire.noPreserveCache(); 3 | import sinon = require('sinon'); 4 | 5 | // userCtrlStub is used to mimic the router 6 | let userCtrlStub = { 7 | index: 'userCtrl.index', 8 | me: 'userCtrl.me', 9 | changePassword: 'userCtrl.changePassword', 10 | show: 'userCtrl.show', 11 | create: 'userCtrl.create' 12 | }; 13 | 14 | // mimic teh auth service 15 | let authServiceStub = { 16 | isAuthenticated() { 17 | return 'authService.isAuthenticated'; 18 | }, 19 | hasRole(role) { 20 | return 'authService.hasRole.' + role; 21 | } 22 | }; 23 | 24 | // routerStub spys on http RESTFUL requests 25 | let routerStub = { 26 | get: sinon.spy(), 27 | put: sinon.spy(), 28 | post: sinon.spy() 29 | }; 30 | 31 | // require the index with our stubbed out modules 32 | // proxyquire simulates the request 33 | // initialize proxyquire 34 | let userIndex = pq('./user.router.js', { 35 | 'express': { 36 | Router() { 37 | return routerStub; 38 | } 39 | }, 40 | './user.controller': userCtrlStub, 41 | '../../auth/auth.service': authServiceStub 42 | }); 43 | 44 | describe('User API Router:', function() { 45 | 46 | // expects the prozyquire routes to equal the routes it was assigned to 47 | it('should return an express router instance', function() { 48 | expect(userIndex.userRoutes).toEqual(routerStub); 49 | }); 50 | 51 | describe('GET /api/users', function() { 52 | 53 | // expect with each request the approapriate endpoint was called 54 | it('should verify admin role and route to user.controller.index', function() { 55 | expect(routerStub.get.withArgs('/', 'authService.hasRole.admin', 'userCtrl.index').calledOnce) 56 | .toBe(true); 57 | }); 58 | 59 | }); 60 | 61 | describe('GET /api/users/me', function() { 62 | 63 | it('should be authenticated and route to user.controller.me', function() { 64 | expect(routerStub.get.withArgs('/me', 'authService.isAuthenticated', 'userCtrl.me').calledOnce) 65 | .toBe(true); 66 | }); 67 | 68 | }); 69 | 70 | describe('PUT /api/users/:email/password', function() { 71 | 72 | it('should be authenticated and route to user.controller.changePassword', function() { 73 | expect(routerStub.put.withArgs('/:email/password', 'authService.isAuthenticated', 'userCtrl.changePassword').calledOnce) 74 | .toBe(true); 75 | }); 76 | 77 | }); 78 | 79 | describe('GET /api/users/:email', function() { 80 | 81 | it('should be authenticated and route to user.controller.show', function() { 82 | expect(routerStub.get.withArgs('/:email', 'authService.isAuthenticated', 'userCtrl.show').calledOnce) 83 | .toBe(true); 84 | }); 85 | 86 | }); 87 | 88 | describe('POST /api/users', function() { 89 | 90 | it('should route to user.controller.create', function() { 91 | expect(routerStub.post.withArgs('/', 'userCtrl.create').calledOnce) 92 | .toBe(true); 93 | }); 94 | 95 | }); 96 | 97 | }); 98 | -------------------------------------------------------------------------------- /server/cassandra-db/auth/auth.router.ts: -------------------------------------------------------------------------------- 1 | let express = require('express'); 2 | 3 | import UserModel from '../api/user/user.model'; 4 | 5 | import config from '../../../config'; 6 | import { localRoutes } from './local/local.router'; 7 | import { localSetup } from './local/local.passport'; 8 | 9 | // Passport configuration 10 | localSetup(UserModel, config); 11 | 12 | let router = express.Router(); 13 | 14 | // Import routes here 15 | // this will setup the passport configuration from the *.passport file 16 | router.use('/local', localRoutes); 17 | 18 | // export the es6 way 19 | export { router as authRoutes }; 20 | -------------------------------------------------------------------------------- /server/cassandra-db/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import UserModel from '../api/user/user.model'; 2 | 3 | import config from '../../../config'; 4 | 5 | import * as jwt from 'jsonwebtoken'; 6 | 7 | let expressJwt = require('express-jwt'); 8 | let compose = require('composable-middleware'); 9 | 10 | let validateJwt = expressJwt({ 11 | secret: config.sessionSecret 12 | }); 13 | 14 | /** 15 | * Attaches the user object to the request if authenticated 16 | * Otherwise returns 403 17 | */ 18 | export function isAuthenticated() { 19 | return compose() 20 | // Validate jwt 21 | .use((req, res, next) => { 22 | // allow access_token to be passed through query parameter as well 23 | if (req.query && req.query.hasOwnProperty('access_token')) { 24 | req.headers.authorization = 'Bearer ' + req.query.access_token; 25 | } 26 | return validateJwt(req, res, next); 27 | }) 28 | // Attach user to request 29 | .use((req, res, next) => { 30 | let user; 31 | return UserModel.userByEmail(req.user.email) 32 | .then(result => { 33 | user = result.rows[0]; 34 | if (!user || user > 1) 35 | res.status(401).json({ message: 'Invalid Token' }); 36 | 37 | req.user = user; 38 | next(); 39 | }) 40 | .catch(err => next(err)); 41 | }); 42 | } 43 | 44 | /** 45 | * Checks if the user role meets the minimum requirements of the route 46 | */ 47 | export function hasRole(roleRequired) { 48 | if (!roleRequired) { 49 | throw new Error('Required role needs to be set'); 50 | } 51 | 52 | return compose() 53 | .use(isAuthenticated()) 54 | .use(function meetsRequirements(req, res, next) { 55 | if (config.userRoles.indexOf(req.user.role) >= 56 | config.userRoles.indexOf(roleRequired)) { 57 | next(); 58 | } else { 59 | res.status(403).send('Forbidden'); 60 | } 61 | }); 62 | } 63 | 64 | /** 65 | * Returns a jwt token signed by the app secret 66 | */ 67 | export function signToken(id, email, role) { 68 | return jwt.sign({ 69 | id: id, 70 | email: email, 71 | role: role 72 | }, config.sessionSecret, { 73 | expiresIn: 60 * 60 * 5 74 | }); 75 | } 76 | 77 | /** 78 | * Set token cookie directly for oAuth strategies 79 | */ 80 | export function setTokenCookie(req, res) { 81 | if (!req.user) { 82 | return res.status(404).send('It looks like you aren\'t logged in, please try again.'); 83 | } 84 | let token = signToken(req.user.id, req.user.email, req.user.role); 85 | res.cookie('token', token); 86 | res.redirect('/'); 87 | } 88 | -------------------------------------------------------------------------------- /server/cassandra-db/auth/local/local.passport.ts: -------------------------------------------------------------------------------- 1 | import * as passport from 'passport'; 2 | import { Strategy as LocalStrategy } from 'passport-local'; 3 | 4 | // This is the authentication process that happens in passport before the 5 | // router callback function. 6 | // When done is called the items will be passed to the callback function in 7 | // local.router.ts 8 | function localAuthenticate(UserModel, email, password, done) { 9 | let user; 10 | 11 | UserModel.getCredentials(email).then(result => { 12 | 13 | if (Object.keys(result.rows).length > 1) { 14 | //TODO send an email to admin account notifying multiple users with the same credentials 15 | return done(null, false, { message: 'There was more than one user!' }); 16 | } 17 | else if (Object.keys(result.rows).length < 1) { 18 | return done(null, false, { message: 'Account does not exist!' }); 19 | } 20 | else { 21 | const dbPW: string = result.rows[0].password; 22 | const dbSalt: string = result.rows[0].salt; 23 | if (UserModel.authenticate(dbPW, dbSalt, password)) { 24 | user = result.rows[0]; 25 | delete user.password; 26 | delete user.salt; 27 | return done(null, user); 28 | } 29 | else { 30 | return done(null, false, { message: 'This password is not correct!' }); 31 | } 32 | } 33 | 34 | 35 | }) 36 | .catch(err => { 37 | console.error('This email is not registered!', email); 38 | done(null, false, { message: 'This email is not registered!' + email }); 39 | }); 40 | 41 | 42 | } 43 | 44 | function setup(UserModel, config) { 45 | passport.use(new LocalStrategy({ 46 | usernameField: 'email', 47 | passwordField: 'password' // this is the virtual field on the model 48 | }, function (email, password, done) { 49 | return localAuthenticate(UserModel, email, password, done); 50 | })); 51 | } 52 | 53 | export { setup as localSetup }; 54 | -------------------------------------------------------------------------------- /server/cassandra-db/auth/local/local.router.ts: -------------------------------------------------------------------------------- 1 | let express = require('express'); 2 | 3 | import { signToken, isAuthenticated } from '../auth.service'; 4 | import { me } from '../../api/user/user.controller'; 5 | import * as passport from 'passport'; 6 | 7 | let router = express.Router(); 8 | 9 | function pp(req, res, next) { 10 | passport.authenticate('local', function (err, user, info) { 11 | let error = err || info; 12 | if (error) { 13 | res.status(401).json(error); 14 | return null; 15 | } 16 | if (!user) { 17 | res.status(404).json({ message: 'Something went wrong, please try again' }); 18 | return null; 19 | } 20 | 21 | let token = signToken(user.id, user.email, user.role); 22 | req.headers.token = token; 23 | req.user = user; 24 | next(); 25 | 26 | })(req, res, next); 27 | } 28 | 29 | // Only one route is necessary 30 | // When local authentication is required the 'local' hook is known from setup 31 | // in .passport.ts file 32 | router.post('/', pp, me); 33 | 34 | export { router as localRoutes }; 35 | -------------------------------------------------------------------------------- /server/cassandra-db/db.model.ts: -------------------------------------------------------------------------------- 1 | import { client } from '../cassandra-db'; 2 | 3 | class DbModel { 4 | 5 | ///////////////// 6 | //seed function// 7 | ///////////////// 8 | public keyspace(keyspace: string): Promise { 9 | return client.execute(keyspace); 10 | } 11 | 12 | public seed(table: string, truncate: string, queries: Array<{ query: string, params: Array }>, queryOptions: object): Promise { 13 | return client.execute(table).then(result => { 14 | return client.execute(truncate).then(result => { 15 | return this.batch(queries, queryOptions).then(() => console.log('Batching succesful!')).catch(err => console.error(err)); 16 | }).catch(err => console.error(err)); 17 | }).catch(err => console.error(err)); 18 | } 19 | 20 | public batch(queries: Array<{ query: string, params: Array }>, queryOptions: object) { 21 | return client.batch(queries, queryOptions); 22 | } 23 | 24 | } 25 | 26 | 27 | 28 | export default new DbModel; -------------------------------------------------------------------------------- /server/cassandra-db/index.ts: -------------------------------------------------------------------------------- 1 | const cassandra = require('cassandra-driver'); 2 | import config from '../../config'; 3 | 4 | import * as Rx from 'rxjs'; 5 | 6 | export const client = new cassandra.Client(config.cassandra); 7 | 8 | // Initialize Express-Cassandra 9 | export function cassandraConnect(): Rx.Observable { 10 | return Rx.Observable.create(observer => { 11 | 12 | client.connect(function (err) { 13 | if (err) { 14 | observer.error(err); 15 | } 16 | observer.next(); 17 | observer.complete(); 18 | }); 19 | 20 | }); 21 | }; 22 | 23 | export function cassandraDisconnect() { 24 | client.shutdown(() => { 25 | console.log('Cassandra DB is now shutdown.'); 26 | }); 27 | }; 28 | 29 | -------------------------------------------------------------------------------- /server/cassandra-db/prepared.statements.ts: -------------------------------------------------------------------------------- 1 | class DbStmts { 2 | 3 | //keyspaces 4 | public devKeyspace: string = `CREATE KEYSPACE IF NOT EXISTS dev WITH REPLICATION = { 5 | 'class' : 'SimpleStrategy', 6 | 'replication_factor' : 1 7 | };`; 8 | public testKeyspace: string = `CREATE KEYSPACE IF NOT EXISTS dev WITH REPLICATION = { 9 | 'class' : 'SimpleStrategy', 10 | 'replication_factor' : 1 11 | };`; 12 | 13 | } 14 | 15 | export default new DbStmts; -------------------------------------------------------------------------------- /server/cassandra-db/seed.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Populate DB with sample data on server start 3 | * to disable, edit config/environment/index.js, and set `seedDB: false` 4 | */ 5 | import { client } from '../cassandra-db'; 6 | import DbModel from './db.model'; 7 | import DbStmts from './prepared.statements'; 8 | import UserStmts from './api/user/prepared.statements'; 9 | 10 | // Define Prepared Statments 11 | const devKeyspace: string = DbStmts.devKeyspace; 12 | const testKeyspace: string = DbStmts.testKeyspace; 13 | const userTable: string = UserStmts.userTable; 14 | const truncateUserTable: string = UserStmts.truncateUserTable; 15 | const seedUserTable: Array<{ query: string, params: Array }> = UserStmts.seedUserTable; 16 | const quertOptions: object = {prepared: true}; 17 | 18 | 19 | export default function cassandraSeed(env?: string): void { 20 | 21 | // Insert seeds below 22 | switch (env) { 23 | case "development": 24 | DbModel.keyspace(devKeyspace) 25 | .then(result => { 26 | console.log('Dev keyspace ready to seed!'); 27 | // list all your batch queries here by table 28 | DbModel.seed(userTable, truncateUserTable, seedUserTable, this.queryOptions) 29 | .then(result => console.log('User Table seeded succesfully!')) 30 | .catch(err => console.error(err)); 31 | 32 | }) 33 | .catch(err => console.error(err)); 34 | break; 35 | case "test": 36 | DbModel.keyspace(testKeyspace) 37 | .then(result => { 38 | console.log('Test keyspace ready to seed!'); 39 | 40 | // list all your batch queries here by table 41 | DbModel.seed(userTable, truncateUserTable, seedUserTable, this.queryOptions) 42 | .then(result => console.log('User Table seeded succesfully!')) 43 | .catch(err => console.error(err)); 44 | 45 | }) 46 | .catch(err => console.error(err)); 47 | break; 48 | default: 49 | // code... for production and others 50 | break; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /server/db-connect.ts: -------------------------------------------------------------------------------- 1 | import { mongoConnect, mongoDisconnect } from './mongo-db'; 2 | import { cassandraConnect, cassandraDisconnect } from './cassandra-db'; 3 | import { sequelizeConnect, sequelizeDisconnect } from './sql-db'; 4 | 5 | import * as Rx from 'rxjs'; 6 | 7 | export function connect(): Rx.Observable { 8 | let obs = []; 9 | obs.push(mongoConnect()); 10 | obs.push(cassandraConnect()); 11 | obs.push(sequelizeConnect()); 12 | 13 | return obs.length > 1 ? Rx.Observable.merge.apply(this, obs) : obs[0]; 14 | } 15 | 16 | export function disconnect() { 17 | mongoDisconnect(); 18 | cassandraDisconnect(); 19 | sequelizeDisconnect(); 20 | } 21 | -------------------------------------------------------------------------------- /server/express.ts: -------------------------------------------------------------------------------- 1 | // importing modules the es6 way 2 | import routes from './routes'; 3 | import config from '../config'; 4 | 5 | import * as mongoose from 'mongoose'; 6 | import * as path from 'path'; 7 | import * as passport from 'passport'; 8 | import * as express from 'express'; 9 | import * as fs from 'graceful-fs'; 10 | import * as chalk from 'chalk'; 11 | import * as morgan from 'morgan'; 12 | import * as bodyParser from 'body-parser'; 13 | import * as methodOverride from 'method-override'; 14 | import * as cookieParser from 'cookie-parser'; 15 | 16 | import * as session from 'express-session'; 17 | import * as connectMongo from 'connect-mongo'; 18 | 19 | let MongoStore = connectMongo(session); 20 | 21 | // function to initialize the express app 22 | function expressInit(app) { 23 | 24 | //aditional app Initializations 25 | app.use(bodyParser.urlencoded({ 26 | extended: false 27 | })); 28 | app.use(bodyParser.json()); 29 | app.use(methodOverride()); 30 | app.use(cookieParser()); 31 | // Initialize passport and passport session 32 | app.use(passport.initialize()); 33 | 34 | //initialize morgan express logger 35 | // NOTE: all node and custom module requests 36 | if (process.env.NODE_ENV !== 'test') { 37 | app.use(morgan('dev', { 38 | skip: function (req, res) { return res.statusCode < 400 } 39 | })); 40 | } 41 | 42 | // app.use(session({ 43 | // secret: config.sessionSecret, 44 | // saveUninitialized: true, 45 | // resave: false, 46 | // store: new MongoStore({ 47 | // mongooseConnection: mongoose.connection 48 | // }) 49 | // })); 50 | 51 | //sets the routes for all the API queries 52 | routes(app); 53 | 54 | const dist = fs.existsSync('dist'); 55 | 56 | //exposes the client and node_modules folders to the client for file serving when client queries "/" 57 | app.use('/node_modules', express.static('node_modules')); 58 | app.use('/custom_modules', express.static('custom_modules')); 59 | app.use(express.static(`${dist ? 'dist/client' : 'client'}`)); 60 | app.use('/public', express.static('public')); 61 | 62 | //exposes the client and node_modules folders to the client for file serving when client queries anything, * is a wildcard 63 | app.use('*', express.static('node_modules')); 64 | app.use('*', express.static('custom_modules')); 65 | app.use('*', express.static(`${dist ? 'dist/client' : 'client'}`)); 66 | app.use('*', express.static('public')); 67 | 68 | // starts a get function when any directory is queried (* is a wildcard) by the client, 69 | // sends back the index.html as a response. Angular then does the proper routing on client side 70 | if (process.env.NODE_ENV !== 'development') 71 | app.get('*', function (req, res) { 72 | res.sendFile(path.join(process.cwd(), `/${dist ? 'dist/client' : 'client'}/index.html`)); 73 | }); 74 | 75 | return app; 76 | 77 | }; 78 | 79 | export default expressInit; 80 | -------------------------------------------------------------------------------- /server/mongo-db/api/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import User from './user.model'; 2 | import config from '../../../../config'; 3 | 4 | import * as jwt from 'jsonwebtoken'; 5 | 6 | // Handles status codes and error message json 7 | // specificity: validation 8 | function validationError(res, statusCode = null) { 9 | console.log('duuude'); 10 | statusCode = statusCode || 422; 11 | return function(err) { 12 | res.status(statusCode).json(err); 13 | return null; 14 | }; 15 | } 16 | 17 | // Handles status codes and error message json 18 | // specificity: error 19 | function handleError(res, statusCode = null) { 20 | console.log('duuwowude'); 21 | statusCode = statusCode || 500; 22 | return function(err) { 23 | res.status(statusCode).send(err); 24 | return null; 25 | }; 26 | } 27 | 28 | /** 29 | * Change a users password endpoint 30 | */ 31 | export function changePassword(req, res, next) { 32 | let userId = req.user._id; 33 | let oldPass = String(req.body.oldPassword); 34 | let newPass = String(req.body.newPassword); 35 | 36 | return User.findById(userId).exec() 37 | .then(user => { 38 | if (user.authenticate(oldPass)) { 39 | user.password = newPass; 40 | return user.save() 41 | .then(() => { 42 | res.status(204).end(); 43 | }) 44 | .catch(validationError(res)); 45 | } else { 46 | return res.status(403).end(); 47 | } 48 | }); 49 | } 50 | 51 | /** 52 | * Get list of users 53 | * restriction: 'admin' 54 | */ 55 | export function index(req, res) { 56 | return User.find({}, '-salt -password').exec() 57 | .then(users => { 58 | res.status(200).json(users); 59 | }) 60 | .catch(handleError(res)); 61 | } 62 | 63 | /** 64 | * Creates a new user endpoint 65 | */ 66 | export function create(req, res, next) { 67 | console.log('duuudaaaaae'); 68 | let newUser = new User(req.body); 69 | newUser.provider = 'local'; 70 | newUser.role = 'user'; 71 | return newUser.save() 72 | .then(user => { 73 | let token = jwt.sign( 74 | { _id: user._id }, 75 | config.sessionSecret, 76 | { expiresIn: 60 * 60 * 5 } 77 | ); 78 | 79 | req.headers.token = token; 80 | req.user = user; 81 | next(); 82 | 83 | return null; 84 | }) 85 | .catch(validationError(res)); 86 | } 87 | 88 | /** 89 | * Deletes a user 90 | * restriction: 'admin' 91 | */ 92 | export function destroy(req, res) { 93 | return User.findByIdAndRemove(req.params.id).exec() 94 | .then(function() { 95 | res.status(204).end(); 96 | }) 97 | .catch(handleError(res)); 98 | } 99 | 100 | /** 101 | * Get a single user 102 | */ 103 | export function show(req, res, next) { 104 | let userId = req.params.id; 105 | 106 | return User.findById(userId).exec() 107 | .then(user => { 108 | if (!user) { 109 | return res.status(404).end(); 110 | } 111 | res.json(user.profile); 112 | }) 113 | .catch(err => next(err)); 114 | } 115 | 116 | /** 117 | * Get my info: all user information 118 | */ 119 | export function me(req, res, next) { 120 | let userId = req.user._id; 121 | let token = req.headers.token; 122 | 123 | return User.findOne({ 124 | _id: userId 125 | }, '-salt -password').exec() 126 | .then(user => { // don't ever give out the password or salt 127 | if (!user) { 128 | return res.status(401).json({ message: 'User does not exist' }); 129 | } 130 | 131 | if (token) res.json({ token, user }); 132 | else res.json(user); 133 | 134 | return null; 135 | }) 136 | .catch(err => next(err)); 137 | } 138 | -------------------------------------------------------------------------------- /server/mongo-db/api/user/user.integration.ts: -------------------------------------------------------------------------------- 1 | import app from '../../../server'; 2 | import request = require('supertest'); 3 | 4 | import User from './user.model'; 5 | 6 | // User Endpoint testing 7 | describe('User API:', function () { 8 | let user; 9 | let token; 10 | 11 | // users are cleared from DB seeding 12 | // add a new testing user 13 | beforeAll(done => { 14 | return User.remove({}).then(function () { 15 | user = new User(); 16 | user.username = 'test'; 17 | user.email = 'test@test.com'; 18 | user.password = 'test'; 19 | user.firstname = 'testFirst'; 20 | user.lastname = 'testLast'; 21 | 22 | return user.save().then(() => done()) 23 | .catch(err => { 24 | expect(err).not.toBeDefined(); 25 | done(); 26 | }); 27 | }).catch(err => { 28 | expect(err).not.toBeDefined(); 29 | done(); 30 | }); 31 | }); 32 | 33 | // Encapsolate GET me enpoint 34 | describe('GET /api/users/me', function () { 35 | 36 | // before every 'it' get new OAuth token representing the user 37 | beforeAll(done => { 38 | setTimeout(() => request(app) 39 | .post('/auth/local') 40 | .send({ 41 | email: 'test@test.com', 42 | password: 'test' 43 | }) 44 | .expect(200) 45 | .end((err, res) => { 46 | if (err) { 47 | done.fail(err); 48 | } else { 49 | token = res.body.token; 50 | done(); 51 | } 52 | }), 2000); 53 | }); 54 | 55 | // If the token was properly set inside the header of the request 56 | // it should respond with a 200 status code with the user json 57 | it('should respond with a user profile when authenticated', done => { 58 | request(app) 59 | .get('/api/users/me') 60 | .set('authorization', 'Bearer ' + token) 61 | .expect(200) 62 | .expect('Content-Type', /json/) 63 | .end((err, res) => { 64 | if (err) { 65 | done.fail(err); 66 | } else { 67 | expect(res.body.username).toEqual(user.username); 68 | expect(res.body.firstname).toEqual(user.firstname); 69 | expect(res.body.lastname).toEqual(user.lastname); 70 | expect(res.body.email).toEqual(user.email); 71 | done(); 72 | } 73 | }); 74 | }); 75 | 76 | // If the token was improperly / not set to the header 77 | // status code 401 should be thrown 78 | it('should respond with a 401 when not authenticated', done => { 79 | request(app) 80 | .get('/api/users/me') 81 | .expect(401) 82 | .end((err, res) => { 83 | if (err) { 84 | done.fail(err); 85 | } else { 86 | done(); 87 | } 88 | }); 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /server/mongo-db/api/user/user.router.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * GET /api/user -> allUsers 3 | * POST /api/user -> create 4 | * GET /api/user/:id -> show 5 | * PUT /api/user/:id/password -> changePassword 6 | * DELETE /api/user/:id -> destroy 7 | */ 8 | 9 | let express = require('express'); 10 | import * as auth from '../../auth/auth.service'; 11 | import * as UserController from './user.controller'; 12 | 13 | let router = express.Router(); 14 | 15 | router.get('/', auth.hasRole('admin'), UserController.index); 16 | router.delete('/:id', auth.hasRole('admin'), UserController.destroy); 17 | router.put('/:id/password', auth.isAuthenticated(), UserController.changePassword); 18 | 19 | router.post('/', UserController.create, UserController.me); 20 | router.get('/me', auth.isAuthenticated(), UserController.me); 21 | router.get('/:id', auth.isAuthenticated(), UserController.show); 22 | 23 | export {router as userRoutes}; 24 | -------------------------------------------------------------------------------- /server/mongo-db/api/user/user.spec.ts: -------------------------------------------------------------------------------- 1 | import proxyquire = require('proxyquire'); 2 | let pq = proxyquire.noPreserveCache(); 3 | import sinon = require('sinon'); 4 | 5 | // userCtrlStub is used to mimic the router 6 | let userCtrlStub = { 7 | index: 'userCtrl.index', 8 | destroy: 'userCtrl.destroy', 9 | me: 'userCtrl.me', 10 | changePassword: 'userCtrl.changePassword', 11 | show: 'userCtrl.show', 12 | create: 'userCtrl.create' 13 | }; 14 | 15 | // mimic teh auth service 16 | let authServiceStub = { 17 | isAuthenticated() { 18 | return 'authService.isAuthenticated'; 19 | }, 20 | hasRole(role) { 21 | return 'authService.hasRole.' + role; 22 | } 23 | }; 24 | 25 | // routerStub spys on http RESTFUL requests 26 | let routerStub = { 27 | get: sinon.spy(), 28 | put: sinon.spy(), 29 | post: sinon.spy(), 30 | delete: sinon.spy() 31 | }; 32 | 33 | // require the index with our stubbed out modules 34 | // proxyquire simulates the request 35 | // initialize proxyquire 36 | let userIndex = pq('./user.router.js', { 37 | 'express': { 38 | Router() { 39 | return routerStub; 40 | } 41 | }, 42 | './user.controller': userCtrlStub, 43 | '../../auth/auth.service': authServiceStub 44 | }); 45 | 46 | describe('User API Router:', function() { 47 | 48 | // expects the prozyquire routes to equal the routes it was assigned to 49 | it('should return an express router instance', function() { 50 | expect(userIndex.userRoutes).toEqual(routerStub); 51 | }); 52 | 53 | describe('GET /api/users', function() { 54 | 55 | // expect with each request the approapriate endpoint was called 56 | it('should verify admin role and route to user.controller.index', function() { 57 | expect(routerStub.get.withArgs('/', 'authService.hasRole.admin', 'userCtrl.index').calledOnce) 58 | .toBe(true); 59 | }); 60 | 61 | }); 62 | 63 | describe('DELETE /api/users/:id', function() { 64 | 65 | it('should verify admin role and route to user.controller.destroy', function() { 66 | expect(routerStub.delete.withArgs('/:id', 'authService.hasRole.admin', 'userCtrl.destroy').calledOnce) 67 | .toBe(true); 68 | }); 69 | 70 | }); 71 | 72 | describe('GET /api/users/me', function() { 73 | 74 | it('should be authenticated and route to user.controller.me', function() { 75 | expect(routerStub.get.withArgs('/me', 'authService.isAuthenticated', 'userCtrl.me').calledOnce) 76 | .toBe(true); 77 | }); 78 | 79 | }); 80 | 81 | describe('PUT /api/users/:id/password', function() { 82 | 83 | it('should be authenticated and route to user.controller.changePassword', function() { 84 | expect(routerStub.put.withArgs('/:id/password', 'authService.isAuthenticated', 'userCtrl.changePassword').calledOnce) 85 | .toBe(true); 86 | }); 87 | 88 | }); 89 | 90 | describe('GET /api/users/:id', function() { 91 | 92 | it('should be authenticated and route to user.controller.show', function() { 93 | expect(routerStub.get.withArgs('/:id', 'authService.isAuthenticated', 'userCtrl.show').calledOnce) 94 | .toBe(true); 95 | }); 96 | 97 | }); 98 | 99 | describe('POST /api/users', function() { 100 | 101 | it('should route to user.controller.create', function() { 102 | expect(routerStub.post.withArgs('/', 'userCtrl.create').calledOnce) 103 | .toBe(true); 104 | }); 105 | 106 | }); 107 | 108 | }); 109 | -------------------------------------------------------------------------------- /server/mongo-db/auth/auth.router.ts: -------------------------------------------------------------------------------- 1 | let express = require('express'); 2 | 3 | import User from '../api/user/user.model'; 4 | import config from '../../../config'; 5 | import {localRoutes} from './local/local.router'; 6 | import {localSetup} from './local/local.passport'; 7 | 8 | // Passport configuration 9 | localSetup(User, config); 10 | 11 | let router = express.Router(); 12 | 13 | // Import routes here 14 | // this will setup the passport configuration from the *.passport file 15 | router.use('/local', localRoutes); 16 | 17 | // export the es6 way 18 | export {router as authRoutes}; 19 | -------------------------------------------------------------------------------- /server/mongo-db/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import User from '../api/user/user.model'; 2 | 3 | import config from '../../../config'; 4 | 5 | import * as jwt from 'jsonwebtoken'; 6 | 7 | let expressJwt = require('express-jwt'); 8 | let compose = require('composable-middleware'); 9 | 10 | let validateJwt = expressJwt({ 11 | secret: config.sessionSecret 12 | }); 13 | 14 | /** 15 | * Attaches the user object to the request if authenticated 16 | * Otherwise returns 403 17 | */ 18 | export function isAuthenticated() { 19 | return compose() 20 | // Validate jwt 21 | .use((req, res, next) => { 22 | // allow access_token to be passed through query parameter as well 23 | if (req.query && req.query.hasOwnProperty('access_token')) { 24 | req.headers.authorization = 'Bearer ' + req.query.access_token; 25 | } 26 | return validateJwt(req, res, next); 27 | }) 28 | // Attach user to request 29 | .use((req, res, next) => { 30 | return User.findById(req.user._id) 31 | .then(user => { 32 | if (!user) { 33 | return res.status(401).json({ message: 'Invalid Token' }); 34 | } 35 | req.user = user; 36 | next(); 37 | // runnaway promise to remove node warning 38 | return null; 39 | }) 40 | .catch(err => next(err)); 41 | }); 42 | } 43 | 44 | /** 45 | * Checks if the user role meets the minimum requirements of the route 46 | */ 47 | export function hasRole(roleRequired) { 48 | if (!roleRequired) { 49 | throw new Error('Required role needs to be set'); 50 | } 51 | 52 | return compose() 53 | .use(isAuthenticated()) 54 | .use(function meetsRequirements(req, res, next) { 55 | if (config.userRoles.indexOf(req.user.role) >= 56 | config.userRoles.indexOf(roleRequired)) { 57 | next(); 58 | } else { 59 | res.status(403).send('Forbidden'); 60 | } 61 | }); 62 | } 63 | 64 | /** 65 | * Returns a jwt token signed by the app secret 66 | */ 67 | export function signToken(id, role) { 68 | return jwt.sign({ 69 | _id: id, 70 | role: role 71 | }, config.sessionSecret, { 72 | expiresIn: 60 * 60 * 5 73 | }); 74 | } 75 | 76 | /** 77 | * Set token cookie directly for oAuth strategies 78 | */ 79 | export function setTokenCookie(req, res) { 80 | if (!req.user) { 81 | return res.status(404).send('It looks like you aren\'t logged in, please try again.'); 82 | } 83 | let token = signToken(req.user._id, req.user.role); 84 | res.cookie('token', token); 85 | res.redirect('/'); 86 | } 87 | -------------------------------------------------------------------------------- /server/mongo-db/auth/local/local.passport.ts: -------------------------------------------------------------------------------- 1 | import * as passport from 'passport'; 2 | import {Strategy as LocalStrategy} from 'passport-local'; 3 | 4 | // This is the authentication process that happens in passport before the 5 | // router callback function. 6 | // When done is called the items will be passed to the callback function in 7 | // local.router.ts 8 | function localAuthenticate(User, email, password, done) { 9 | User.findOne({ 10 | email: email.toLowerCase() 11 | }).exec() 12 | .then(user => { 13 | if (!user) { 14 | return done(null, false, { 15 | message: 'This email is not registered!' 16 | }); 17 | } 18 | user.authenticate(password, (authError, authenticated) => { 19 | if (authError) { 20 | return done(authError); 21 | } 22 | if (!authenticated) { 23 | return done(null, false, { 24 | message: 'This password is not correct!' 25 | }); 26 | } else { 27 | return done(null, user); 28 | } 29 | }); 30 | }) 31 | .catch(err => done(err)); 32 | } 33 | 34 | function setup(User, config) { 35 | passport.use(new LocalStrategy({ 36 | usernameField: 'email', 37 | passwordField: 'password' // this is the virtual field on the model 38 | }, function (email, password, done) { 39 | return localAuthenticate(User, email, password, done); 40 | })); 41 | } 42 | 43 | export {setup as localSetup}; 44 | -------------------------------------------------------------------------------- /server/mongo-db/auth/local/local.router.ts: -------------------------------------------------------------------------------- 1 | let express = require('express'); 2 | 3 | import {signToken, isAuthenticated} from '../auth.service'; 4 | import {me} from '../../api/user/user.controller'; 5 | import * as passport from 'passport'; 6 | 7 | let router = express.Router(); 8 | 9 | // Only one route is necessary 10 | // When local authentication is required the 'local' hook is known from setup 11 | // in .passport.ts file 12 | router.post('/', function(req, res, next) { 13 | passport.authenticate('local', function(err, user, info) { 14 | let error = err || info; 15 | if (error) { 16 | res.status(401).json(error); 17 | return null; 18 | } 19 | if (!user) { 20 | res.status(404).json({ message: 'Something went wrong, please try again' }); 21 | return null; 22 | } 23 | 24 | let token = signToken(user._id, user.role); 25 | req.headers.token = token; 26 | req.user = user; 27 | next(); 28 | 29 | })(req, res, next); 30 | }, me); 31 | 32 | export {router as localRoutes}; 33 | -------------------------------------------------------------------------------- /server/mongo-db/index.ts: -------------------------------------------------------------------------------- 1 | let mongoose = require('mongoose'); 2 | mongoose.Promise = Promise; // promise library plugin 3 | 4 | import * as chalk from 'chalk'; 5 | import config from '../../config'; 6 | 7 | import * as Rx from 'rxjs'; 8 | 9 | // Initialize Mongoose 10 | export function mongoConnect(): Rx.Observable { 11 | return Rx.Observable.create(observer => { 12 | 13 | mongoose.connect(config.mongo.uri, config.mongo.options, function (err) { 14 | // Log Error 15 | if (err) { 16 | console.error(chalk.default.bold.red('Could not connect to MongoDB!')); 17 | observer.error(err); 18 | } else { 19 | // Enabling mongoose debug mode if required 20 | mongoose.set('debug', config.mongo.debug); 21 | 22 | observer.next(); 23 | observer.complete(); 24 | } 25 | }); 26 | 27 | }); 28 | }; 29 | 30 | export function mongoDisconnect() { 31 | mongoose.disconnect(function (err) { 32 | console.log(chalk.default.bold.yellow('Disconnected from MongoDB.')); 33 | }); 34 | }; -------------------------------------------------------------------------------- /server/mongo-db/seed.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Populate DB with sample data on server start 3 | * to disable, edit config/environment/index.js, and set `seedDB: false` 4 | */ 5 | import User from './api/user/user.model'; 6 | 7 | export default function mongoSeed(env?: string): void { 8 | 9 | // Insert seeds below 10 | switch (env) { 11 | case "development": 12 | User.find({}).remove().then(() => { 13 | User.create({ 14 | username: 'AdMiN', 15 | firstname: 'admin', 16 | lastname: 'admin', 17 | email: 'admin@admin.com', 18 | password: 'admin1' 19 | }, { 20 | username: 'test', 21 | firstname: 'testFirst', 22 | lastname: 'testLast', 23 | email: 'test@test.com', 24 | password: 'test' 25 | }); 26 | }).catch(error => console.log(error)); 27 | break; 28 | case "test": 29 | User.find({}).remove().then(() => { 30 | User.create({ 31 | username: 'test', 32 | firstname: 'testFirst', 33 | lastname: 'testLast', 34 | email: 'test@test.com', 35 | password: 'test' 36 | }); 37 | }).catch(error => console.log(error)); 38 | break; 39 | default: 40 | // code... 41 | break; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /server/routes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Main application routes 3 | */ 4 | import { userRoutes } from './mongo-db/api/user/user.router'; 5 | // DO NOT REMOVE: template route imports 6 | import { authRoutes } from './mongo-db/auth/auth.router'; 7 | 8 | export default function routes(app) { 9 | // Insert routes below 10 | app.use('/api/users', userRoutes); 11 | // DO NOT REMOVE: template routes 12 | app.use('/auth', authRoutes); 13 | }; 14 | -------------------------------------------------------------------------------- /server/server.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express' 2 | import * as chalk from 'chalk'; 3 | import * as fs from 'graceful-fs'; 4 | import * as http from 'http'; 5 | import * as https from 'https'; 6 | import config from '../config'; 7 | 8 | import socketInit from './socketio'; 9 | import expressInit from './express'; 10 | 11 | import cassandraSeed from './cassandra-db/seed'; 12 | import mongoSeed from './mongo-db/seed'; 13 | import sqlSeed from './sql-db/seed'; 14 | import {connect, disconnect} from './db-connect'; 15 | 16 | const isSecure = config.https_secure && (process.env.NODE_ENV === 'production' || !process.env.NODE_ENV); 17 | 18 | // Initialize express 19 | let app = express(); 20 | 21 | // Initialize http server 22 | let server: any = http.createServer(app); 23 | // If specified in the default assets, https will be used 24 | if (isSecure) { 25 | let credentials = { 26 | key: fs.readFileSync(config.key_loc, 'utf8'), 27 | cert: fs.readFileSync(config.cert_loc, 'utf8') 28 | }; 29 | 30 | server = https.createServer(credentials, app); 31 | } 32 | // Initialize the socketio with the respective server 33 | let socketio = require('socket.io')(server, { 34 | // serveClient: process.env.NODE_ENV !== 'production', 35 | path: '/socket.io-client' 36 | }); 37 | 38 | connect().subscribe( 39 | x => {}, 40 | err => console.log(err), 41 | () => { 42 | expressInit(app); 43 | socketInit(socketio); 44 | 45 | if (config.seedDB) { 46 | mongoSeed(process.env.NODE_ENV); 47 | cassandraSeed(process.env.NODE_ENV); 48 | // sqlSeed(process.env.NODE_ENV); 49 | } 50 | 51 | // Start the server on port / host 52 | server.listen(config.port, config.host, () => { 53 | let host = server.address().address; 54 | let port = server.address().port; 55 | 56 | if (process.env.NODE_ENV !== 'test') { 57 | console.log( 58 | chalk.default.bold.cyan(`\n\tEnvironment:\t\t\t ${ process.env.NODE_ENV || 'production' }\n`)); 59 | 60 | console.log( 61 | chalk.default.bold.cyan(`\tSQL:`) + 62 | chalk.default.bold.cyan(`\n\t - URI:\t\t\t\t sql://${config.sql.username}:${config.sql.password}@localhost:5432/${config.sql.database}\n`)); 63 | 64 | if (!process.env.NODE_ENV) 65 | console.log( 66 | chalk.default.bold.magenta(`\t${isSecure ? 'HTTPS': 'HTTP'} Server`) + 67 | chalk.default.bold.gray(`\n\tServer Address:\t\t\t ${isSecure ? 'https': 'http'}://localhost:${ port }\n`)); 68 | else 69 | console.log( 70 | chalk.default.bold.magenta(`\tWebPack DevServer:`) + 71 | chalk.default.bold.gray(`\n\tServer Address:\t\t\t ${isSecure ? 'https': 'http'}://localhost:1701\n`)); 72 | } 73 | }); 74 | }); 75 | 76 | // export express app for testing 77 | export default app; 78 | -------------------------------------------------------------------------------- /server/socketio.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Socket.io configuration 3 | */ 4 | import config from '../config'; 5 | 6 | // Socket imports go here 7 | // DO NOT REMOVE: template socket imports 8 | 9 | // When the user disconnects.. perform this 10 | function onDisconnect(socket) { 11 | } 12 | 13 | // When the user connects.. perform this 14 | function onConnect(socket) { 15 | // When the client emits 'info', this listens and executes 16 | socket.on('info', data => { 17 | socket.log(JSON.stringify(data, null, 2)); 18 | }); 19 | 20 | // Insert sockets below 21 | // DO NOT REMOVE: template sockets 22 | 23 | } 24 | 25 | function socketInit(socketio) { 26 | // socket.io (v1.x.x) is powered by debug. 27 | // We can authenticate socket.io users and access their token through socket.decoded_token 28 | // 29 | // 1. You will need to send the token in `app/services/socketio/socketio.service.ts` 30 | // 31 | // 2. Require authentication here: 32 | // socketio.use(require('socketio-jwt').authorize({ 33 | // secret: config.secrets.session, 34 | // handshake: true 35 | // })); 36 | 37 | socketio.on('connection', function (socket) { 38 | socket.address = socket.request.connection.remoteAddress + 39 | ':' + socket.request.connection.remotePort; 40 | 41 | socket.connectedAt = new Date(); 42 | 43 | socket.log = function (...data) { 44 | console.log(`SocketIO ${socket.nsp.name} [${socket.address}]`, ...data); 45 | }; 46 | 47 | // Call onDisconnect. 48 | socket.on('disconnect', () => { 49 | onDisconnect(socket); 50 | socket.log('DISCONNECTED'); 51 | }); 52 | 53 | // Call onConnect. 54 | onConnect(socket); 55 | socket.log('CONNECTED'); 56 | }); 57 | } 58 | 59 | export default socketInit; 60 | -------------------------------------------------------------------------------- /server/sql-db/api/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import User from './user.model'; 2 | import config from '../../../../config'; 3 | 4 | import * as jwt from 'jsonwebtoken'; 5 | 6 | // Handles status codes and error message json 7 | // specificity: validation 8 | function validationError(res, err, statusCode = null) { 9 | 10 | statusCode = statusCode || 422; 11 | 12 | res.status(statusCode).json(err.errors[0]); 13 | return null; 14 | 15 | } 16 | 17 | // Handles status codes and error message json 18 | // specificity: error 19 | function handleError(res, err, statusCode = null) { 20 | statusCode = statusCode || 500; 21 | 22 | res.status(statusCode).send(err.errors[0]); 23 | return null; 24 | 25 | } 26 | 27 | /** 28 | * Change a users password endpoint 29 | */ 30 | export function changePassword(req, res, next) { 31 | let userId = req.user.id; 32 | let oldPass = String(req.body.oldPassword); 33 | let newPass = String(req.body.newPassword); 34 | 35 | return User.findById(userId).then(user => { 36 | if (user['authenticate'](oldPass)) { 37 | user['password'] = newPass; 38 | return user['save']() 39 | .then(() => { 40 | res.status(204).end(); 41 | }) 42 | .catch(err => validationError(res, err)); 43 | } else { 44 | return res.status(403).end(); 45 | } 46 | }); 47 | } 48 | 49 | /** 50 | * Get list of users 51 | * restriction: 'admin' 52 | */ 53 | export function index(req, res) { 54 | return User.findAll({ attributes: {exclude: ['salt', 'password'] } }).then(users => { 55 | res.status(200).json(users); 56 | }) 57 | .catch(err => handleError(res, err)); 58 | } 59 | 60 | /** 61 | * Creates a new user endpoint 62 | */ 63 | export function create(req, res, next) { 64 | 65 | User.create({ 66 | username: req.body.username, 67 | email: req.body.email, 68 | password: req.body.password, 69 | role: 'user' 70 | }).then(user => { 71 | let token = jwt.sign( 72 | { id: user['id'] }, 73 | config.sessionSecret, 74 | { expiresIn: 60 * 60 * 5 } 75 | ); 76 | 77 | req.headers.token = token; 78 | req.user = user; 79 | next(); 80 | 81 | return null; 82 | }) 83 | .catch(err => { 84 | validationError(res, err); 85 | }); 86 | } 87 | 88 | /** 89 | * Deletes a user 90 | * restriction: 'admin' 91 | */ 92 | export function destroy(req, res) { 93 | return User.destroy({where: {id: req.params.id}}) 94 | .then(function() { 95 | res.status(204).end(); 96 | }) 97 | .catch(err => handleError(res, err)); 98 | } 99 | 100 | /** 101 | * Get a single user 102 | */ 103 | export function show(req, res, next) { 104 | let userId = req.params.id; 105 | 106 | return User.findById(userId).then(user => { 107 | if (!user) { 108 | return res.status(404).end(); 109 | } 110 | res.json(user['profile']); 111 | }) 112 | .catch(err => validationError(res, err)); 113 | } 114 | 115 | /** 116 | * Get my info: all user information 117 | */ 118 | export function me(req, res, next) { 119 | let userId = req.user.id; 120 | let token = req.headers.token; 121 | 122 | return User.findOne({ where: {id: userId}, attributes: {exclude: ['salt', 'password'] } }).then(user => { // don't ever give out the password or salt 123 | if (!user) { 124 | return res.status(401).json({ message: 'User does not exist' }); 125 | } 126 | 127 | if (token) res.json({ token, user }); 128 | else res.json(user); 129 | 130 | return null; 131 | }) 132 | .catch(err => validationError(res, err)); 133 | } 134 | -------------------------------------------------------------------------------- /server/sql-db/api/user/user.integration.ts: -------------------------------------------------------------------------------- 1 | import app from '../../../server'; 2 | import request = require('supertest'); 3 | 4 | import User from './user.model'; 5 | 6 | // User Endpoint testing 7 | describe('User API:', function () { 8 | let user; 9 | let token; 10 | 11 | // users are cleared from DB seeding 12 | // add a new testing user 13 | beforeAll(function (done) { 14 | return User.sync().then(() =>{ 15 | User.destroy({truncate: true, cascade: true}).then(() => { 16 | User.create({ 17 | username: 'test', 18 | firstname: 'testFirst', 19 | lastname: 'testLast', 20 | email: 'test@test.com', 21 | password: 'test' 22 | }).then(u => { 23 | user = u; 24 | done(); 25 | }).catch(err => { 26 | expect(err).not.toBeDefined(); 27 | done(); 28 | }); 29 | 30 | }); 31 | }).catch(err => { 32 | expect(err).not.toBeDefined(); 33 | done(); 34 | }); 35 | 36 | }); 37 | 38 | // Encapsolate GET me enpoint 39 | describe('GET /api/users/me', function () { 40 | 41 | // before every 'it' get new OAuth token representing the user 42 | beforeAll(function (done) { 43 | setTimeout(() => request(app) 44 | .post('/auth/local') 45 | .send({ 46 | email: 'test@test.com', 47 | password: 'test' 48 | }) 49 | .expect(200) 50 | .end((err, res) => { 51 | if (err) { 52 | done.fail(err); 53 | } else { 54 | token = res.body.token; 55 | done(); 56 | } 57 | }), 2000); 58 | 59 | }); 60 | 61 | // If the token was properly set inside the header of the request 62 | // it should respond with a 200 status code with the user json 63 | it('should respond with a user profile when authenticated', function (done) { 64 | request(app) 65 | .get('/api/users/me') 66 | .set('authorization', 'Bearer ' + token) 67 | .expect(200) 68 | .expect('Content-Type', /json/) 69 | .end((err, res) => { 70 | if (err) { 71 | done.fail(err); 72 | } else { 73 | expect(res.body.username).toEqual(user.username); 74 | expect(res.body.firstname).toEqual(user.firstname); 75 | expect(res.body.lastname).toEqual(user.lastname); 76 | expect(res.body.email).toEqual(user.email); 77 | done(); 78 | } 79 | }); 80 | }); 81 | 82 | // If the token was improperly / not set to the header 83 | // status code 401 should be thrown 84 | it('should respond with a 401 when not authenticated', function (done) { 85 | request(app) 86 | .get('/api/users/me') 87 | .expect(401) 88 | .end((err, res) => { 89 | if (err) { 90 | done.fail(err); 91 | } else { 92 | done(); 93 | } 94 | }); 95 | }); 96 | }); 97 | }); -------------------------------------------------------------------------------- /server/sql-db/api/user/user.model.ts: -------------------------------------------------------------------------------- 1 | import Sequelize from "sequelize"; 2 | import sequelize from "../../../sql-db"; 3 | 4 | const crypto = require('crypto'); 5 | 6 | let User = sequelize.define("user", { 7 | username: {type: Sequelize.STRING, 8 | unique: true, 9 | validate: { 10 | notEmpty: { 11 | args: true, 12 | msg: 'A user needs at least a username' 13 | } 14 | } 15 | }, 16 | firstname: {type: Sequelize.STRING}, 17 | lastname: {type: Sequelize.STRING}, 18 | email: {type: Sequelize.STRING, 19 | unique: true, 20 | validate: { 21 | notEmpty: { 22 | args: true, 23 | msg: 'Email cannot be blank' 24 | } 25 | } 26 | }, 27 | password: {type: Sequelize.STRING, 28 | validate: { 29 | notEmpty: { 30 | args: true, 31 | msg: 'Password cannot be blank' 32 | } 33 | }}, 34 | salt: {type: Sequelize.STRING} 35 | },{ 36 | hooks: { 37 | beforeCreate: function(user, options, next) { 38 | 39 | // Make salt with a callback 40 | user['makeSalt']((saltErr, salt) => { 41 | if (saltErr) { 42 | return next(saltErr); 43 | } 44 | user['salt'] = salt; 45 | user['encryptPassword'](user['password'], (encryptErr, hashedPassword) => { 46 | if (encryptErr) { 47 | return next(encryptErr); 48 | } 49 | user['password'] = hashedPassword; 50 | next(); 51 | }); 52 | }); 53 | } 54 | }, 55 | 56 | instanceMethods: { 57 | /** 58 | * Authenticate - check if the passwords are the same 59 | * 60 | * @param {String} password 61 | * @param {Function} callback 62 | * @return {Boolean} 63 | * @api public 64 | */ 65 | authenticate(password, callback) { 66 | if (!callback) { 67 | return this.password === this.encryptPassword(password); 68 | } 69 | 70 | this.encryptPassword(password, (err, pwdGen) => { 71 | if (err) { 72 | return callback(err); 73 | } 74 | 75 | if (this.password === pwdGen) { 76 | callback(null, true); 77 | } else { 78 | callback(null, false); 79 | } 80 | }); 81 | }, 82 | 83 | /** 84 | * Make salt 85 | * 86 | * @param {Number} byteSize Optional salt byte size, default to 16 87 | * @param {Function} callback 88 | * @return {String} 89 | * @api public 90 | */ 91 | makeSalt(byteSize, callback): any { 92 | let defaultByteSize = 16; 93 | 94 | if (typeof arguments[0] === 'function') { 95 | callback = arguments[0]; 96 | byteSize = defaultByteSize; 97 | } else if (typeof arguments[1] === 'function') { 98 | callback = arguments[1]; 99 | } 100 | 101 | if (!byteSize) { 102 | byteSize = defaultByteSize; 103 | } 104 | 105 | if (!callback) { 106 | return crypto.randomBytes(byteSize).toString('base64'); 107 | } 108 | 109 | return crypto.randomBytes(byteSize, (err, salt) => { 110 | if (err) { 111 | callback(err); 112 | } else { 113 | callback(null, salt.toString('base64')); 114 | } 115 | }); 116 | }, 117 | 118 | /** 119 | * Encrypt password 120 | * 121 | * @param {String} password 122 | * @param {Function} callback 123 | * @return {String} 124 | * @api public 125 | */ 126 | encryptPassword(password, callback): any { 127 | if (!password || !this.salt) { 128 | if (!callback) { 129 | return null; 130 | } else { 131 | return callback('Missing password or salt'); 132 | } 133 | } 134 | 135 | let defaultIterations = 10000; 136 | let defaultKeyLength = 64; 137 | let salt = new Buffer(this.salt, 'base64'); 138 | 139 | if (!callback) { 140 | return crypto.pbkdf2Sync(password, salt, defaultIterations, defaultKeyLength, 'sha512') 141 | .toString('base64'); 142 | } 143 | 144 | return crypto.pbkdf2(password, salt, defaultIterations, defaultKeyLength, 'sha512', (err, key) => { 145 | if (err) { 146 | callback(err); 147 | } else { 148 | callback(null, key.toString('base64')); 149 | } 150 | }); 151 | } 152 | } 153 | }); 154 | 155 | export default User; 156 | -------------------------------------------------------------------------------- /server/sql-db/api/user/user.router.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * GET /api/user -> allUsers 3 | * POST /api/user -> create 4 | * GET /api/user/:id -> show 5 | * PUT /api/user/:id/password -> changePassword 6 | * DELETE /api/user/:id -> destroy 7 | */ 8 | 9 | let express = require('express'); 10 | import * as auth from '../../auth/auth.service'; 11 | import * as UserController from './user.controller'; 12 | 13 | let router = express.Router(); 14 | 15 | router.get('/', auth.hasRole('admin'), UserController.index); 16 | router.delete('/:id', auth.hasRole('admin'), UserController.destroy); 17 | router.put('/:id/password', auth.isAuthenticated(), UserController.changePassword); 18 | 19 | router.post('/', UserController.create, UserController.me); 20 | router.get('/me', auth.isAuthenticated(), UserController.me); 21 | router.get('/:id', auth.isAuthenticated(), UserController.show); 22 | 23 | export {router as userRoutes}; 24 | -------------------------------------------------------------------------------- /server/sql-db/api/user/user.spec.ts: -------------------------------------------------------------------------------- 1 | import proxyquire = require('proxyquire'); 2 | let pq = proxyquire.noPreserveCache(); 3 | import sinon = require('sinon'); 4 | 5 | // userCtrlStub is used to mimic the router 6 | let userCtrlStub = { 7 | index: 'userCtrl.index', 8 | destroy: 'userCtrl.destroy', 9 | me: 'userCtrl.me', 10 | changePassword: 'userCtrl.changePassword', 11 | show: 'userCtrl.show', 12 | create: 'userCtrl.create' 13 | }; 14 | 15 | // mimic teh auth service 16 | let authServiceStub = { 17 | isAuthenticated() { 18 | return 'authService.isAuthenticated'; 19 | }, 20 | hasRole(role) { 21 | return 'authService.hasRole.' + role; 22 | } 23 | }; 24 | 25 | // routerStub spys on http RESTFUL requests 26 | let routerStub = { 27 | get: sinon.spy(), 28 | put: sinon.spy(), 29 | post: sinon.spy(), 30 | delete: sinon.spy() 31 | }; 32 | 33 | // require the index with our stubbed out modules 34 | // proxyquire simulates the request 35 | // initialize proxyquire 36 | let userIndex = pq('./user.router.js', { 37 | 'express': { 38 | Router() { 39 | return routerStub; 40 | } 41 | }, 42 | './user.controller': userCtrlStub, 43 | '../../auth/auth.service': authServiceStub 44 | }); 45 | 46 | describe('User API Router:', function() { 47 | 48 | // expects the prozyquire routes to equal the routes it was assigned to 49 | it('should return an express router instance', function() { 50 | expect(userIndex.userRoutes).toEqual(routerStub); 51 | }); 52 | 53 | describe('GET /api/users', function() { 54 | 55 | // expect with each request the approapriate endpoint was called 56 | it('should verify admin role and route to user.controller.index', function() { 57 | expect(routerStub.get.withArgs('/', 'authService.hasRole.admin', 'userCtrl.index').calledOnce) 58 | .toBe(true); 59 | }); 60 | 61 | }); 62 | 63 | describe('DELETE /api/users/:id', function() { 64 | 65 | it('should verify admin role and route to user.controller.destroy', function() { 66 | expect(routerStub.delete.withArgs('/:id', 'authService.hasRole.admin', 'userCtrl.destroy').calledOnce) 67 | .toBe(true); 68 | }); 69 | 70 | }); 71 | 72 | describe('GET /api/users/me', function() { 73 | 74 | it('should be authenticated and route to user.controller.me', function() { 75 | expect(routerStub.get.withArgs('/me', 'authService.isAuthenticated', 'userCtrl.me').calledOnce) 76 | .toBe(true); 77 | }); 78 | 79 | }); 80 | 81 | describe('PUT /api/users/:id/password', function() { 82 | 83 | it('should be authenticated and route to user.controller.changePassword', function() { 84 | expect(routerStub.put.withArgs('/:id/password', 'authService.isAuthenticated', 'userCtrl.changePassword').calledOnce) 85 | .toBe(true); 86 | }); 87 | 88 | }); 89 | 90 | describe('GET /api/users/:id', function() { 91 | 92 | it('should be authenticated and route to user.controller.show', function() { 93 | expect(routerStub.get.withArgs('/:id', 'authService.isAuthenticated', 'userCtrl.show').calledOnce) 94 | .toBe(true); 95 | }); 96 | 97 | }); 98 | 99 | describe('POST /api/users', function() { 100 | 101 | it('should route to user.controller.create', function() { 102 | expect(routerStub.post.withArgs('/', 'userCtrl.create').calledOnce) 103 | .toBe(true); 104 | }); 105 | 106 | }); 107 | 108 | }); -------------------------------------------------------------------------------- /server/sql-db/auth/auth.router.ts: -------------------------------------------------------------------------------- 1 | let express = require('express'); 2 | 3 | import User from '../api/user/user.model'; 4 | import config from '../../../config'; 5 | import {localRoutes} from './local/local.router'; 6 | import {localSetup} from './local/local.passport'; 7 | 8 | // Passport configuration 9 | localSetup(User, config); 10 | 11 | let router = express.Router(); 12 | 13 | // Import routes here 14 | // this will setup the passport configuration from the *.passport file 15 | router.use('/local', localRoutes); 16 | 17 | // export the es6 way 18 | export {router as authRoutes}; 19 | -------------------------------------------------------------------------------- /server/sql-db/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import User from '../api/user/user.model'; 2 | 3 | import config from '../../../config'; 4 | 5 | import * as jwt from 'jsonwebtoken'; 6 | 7 | let expressJwt = require('express-jwt'); 8 | let compose = require('composable-middleware'); 9 | 10 | let validateJwt = expressJwt({ 11 | secret: config.sessionSecret 12 | }); 13 | 14 | /** 15 | * Attaches the user object to the request if authenticated 16 | * Otherwise returns 403 17 | */ 18 | export function isAuthenticated() { 19 | return compose() 20 | // Validate jwt 21 | .use((req, res, next) => { 22 | // allow access_token to be passed through query parameter as well 23 | if (req.query && req.query.hasOwnProperty('access_token')) { 24 | req.headers.authorization = 'Bearer ' + req.query.access_token; 25 | } 26 | return validateJwt(req, res, next); 27 | }) 28 | // Attach user to request 29 | .use((req, res, next) => { 30 | return User.findById(req.user.id) 31 | .then(user => { 32 | if (!user) { 33 | return res.status(401).json({ message: 'Invalid Token' }); 34 | } 35 | req.user = user; 36 | next(); 37 | // runnaway promise to remove node warning 38 | return null; 39 | }) 40 | .catch(err => next(err)); 41 | }); 42 | } 43 | 44 | /** 45 | * Checks if the user role meets the minimum requirements of the route 46 | */ 47 | export function hasRole(roleRequired) { 48 | if (!roleRequired) { 49 | throw new Error('Required role needs to be set'); 50 | } 51 | 52 | return compose() 53 | .use(isAuthenticated()) 54 | .use(function meetsRequirements(req, res, next) { 55 | if (config.userRoles.indexOf(req.user.role) >= 56 | config.userRoles.indexOf(roleRequired)) { 57 | next(); 58 | } else { 59 | res.status(403).send('Forbidden'); 60 | } 61 | }); 62 | } 63 | 64 | /** 65 | * Returns a jwt token signed by the app secret 66 | */ 67 | export function signToken(id, role) { 68 | return jwt.sign({ 69 | id: id, 70 | role: role 71 | }, config.sessionSecret, { 72 | expiresIn: 60 * 60 * 5 73 | }); 74 | } 75 | 76 | /** 77 | * Set token cookie directly for oAuth strategies 78 | */ 79 | export function setTokenCookie(req, res) { 80 | if (!req.user) { 81 | return res.status(404).send('It looks like you aren\'t logged in, please try again.'); 82 | } 83 | let token = signToken(req.user._id, req.user.role); 84 | res.cookie('token', token); 85 | res.redirect('/'); 86 | } 87 | -------------------------------------------------------------------------------- /server/sql-db/auth/local/local.passport.ts: -------------------------------------------------------------------------------- 1 | import * as passport from 'passport'; 2 | import {Strategy as LocalStrategy} from 'passport-local'; 3 | 4 | // This is the authentication process that happens in passport before the 5 | // router callback function. 6 | // When done is called the items will be passed to the callback function in 7 | // local.router.ts 8 | function localAuthenticate(User, email, password, done) { 9 | 10 | User.findOne({ where: {email: email}}).then(user => { 11 | if (!user) { 12 | return done(null, false, { 13 | message: 'This email is not registered!' 14 | }); 15 | } 16 | 17 | user.authenticate(password, (authError, authenticated) => { 18 | if (authError) { 19 | return done(authError); 20 | } 21 | if (!authenticated) { 22 | return done(null, false, { 23 | message: 'This password is not correct!' 24 | }); 25 | } else { 26 | return done(null, user); 27 | } 28 | }); 29 | }) 30 | .catch(err => done(err)); 31 | } 32 | 33 | function setup(User, config) { 34 | passport.use(new LocalStrategy({ 35 | usernameField: 'email', 36 | passwordField: 'password' // this is the virtual field on the model 37 | }, function (email, password, done) { 38 | return localAuthenticate(User, email, password, done); 39 | })); 40 | } 41 | 42 | export {setup as localSetup}; 43 | -------------------------------------------------------------------------------- /server/sql-db/auth/local/local.router.ts: -------------------------------------------------------------------------------- 1 | let express = require('express'); 2 | 3 | import {signToken, isAuthenticated} from '../auth.service'; 4 | import {me} from '../../api/user/user.controller'; 5 | import * as passport from 'passport'; 6 | 7 | let router = express.Router(); 8 | 9 | // Only one route is necessary 10 | // When local authentication is required the 'local' hook is known from setup 11 | // in .passport.ts file 12 | router.post('/', function(req, res, next) { 13 | 14 | passport.authenticate('local', function(err, user, info) { 15 | 16 | let error = err || info; 17 | if (error) { 18 | res.status(401).json(error); 19 | return null; 20 | } 21 | if (!user) { 22 | res.status(404).json({ message: 'Something went wrong, please try again' }); 23 | return null; 24 | } 25 | 26 | let token = signToken(user.id, user.role); 27 | req.headers.token = token; 28 | req.user = user; 29 | next(); 30 | 31 | })(req, res, next); 32 | 33 | }, me); 34 | 35 | export {router as localRoutes}; 36 | -------------------------------------------------------------------------------- /server/sql-db/index.ts: -------------------------------------------------------------------------------- 1 | import config from "../../config"; 2 | import Sequelize from "sequelize"; 3 | 4 | import * as Rx from 'rxjs'; 5 | 6 | //initilize the database 7 | let sequelize = new Sequelize(config.sql.database, config.sql.username, config.sql.password, config.sql.options); 8 | 9 | export default sequelize; 10 | 11 | // Initialize sequelize 12 | export function sequelizeConnect() { 13 | return Rx.Observable.create(observer => { 14 | sequelize.sync().then(function() { 15 | 16 | observer.next(); 17 | observer.complete(); 18 | 19 | }).catch(err => observer.error(err)); 20 | }); 21 | }; 22 | 23 | export function sequelizeDisconnect() { 24 | 25 | }; 26 | -------------------------------------------------------------------------------- /server/sql-db/seed.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Populate DB with sample data on server start 3 | * to disable, edit config/environment/index.js, and set `seedDB: false` 4 | */ 5 | import User from './api/user/user.model'; 6 | 7 | export default function sqlSeed(env?: string): void { 8 | 9 | // Insert seeds below 10 | switch (env) { 11 | case "development": 12 | User.sync().then(() =>{ 13 | User.destroy({truncate: true, cascade: true}).then(() => { 14 | User.create({ 15 | username: 'AdMiN', 16 | firstname:'admin', 17 | lastname: 'admin', 18 | email: 'admin@admin.com', 19 | role: 'admin', 20 | password: 'admin1' 21 | }).then(() => { 22 | User.create({ 23 | username: 'test', 24 | firstname:'testFirst', 25 | lastname: 'testLast', 26 | email: 'test@test.com', 27 | role: 'user', 28 | password: 'test' 29 | }).catch(() => {}); 30 | }).catch(() => {}); 31 | }).catch(err => console.log(err.message)); 32 | }).catch(err => console.log(err.message)); 33 | 34 | break; 35 | case "test": 36 | User.sync().then(() =>{ 37 | User.destroy({truncate: true, cascade: true}).then(() => { 38 | User.create({ 39 | username: 'test', 40 | firstname:'testFirst', 41 | lastname: 'testLast', 42 | email: 'test@test.com', 43 | role: 'user', 44 | password: 'test' 45 | }).catch(() => {}); 46 | }).catch(err => console.log(err.message)); 47 | }).catch(err => console.log(err.message)); 48 | break; 49 | default: 50 | // code... 51 | break; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /tsconfig-aot.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": false, 4 | "target": "es5", 5 | "module": "es2015", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "allowSyntheticDefaultImports": true, 9 | "emitDecoratorMetadata": true, 10 | "noImplicitAny": false, 11 | "suppressImplicitAnyIndexErrors": true, 12 | "experimentalDecorators": true, 13 | "lib": ["es6", "dom"], 14 | "types": [ 15 | "graceful-fs", 16 | "immutable", 17 | "lodash", 18 | "core-js", 19 | "socket.io-client" 20 | ] 21 | }, 22 | "files": [ 23 | "client/modules/app.module.ts", 24 | "client/modules/app.server.module.ts", 25 | "client/app.ts", 26 | "client/app-aot.ts" 27 | ], 28 | "angularCompilerOptions": { 29 | "genDir": "ngc-aot", 30 | "skipMetadataEmit" : true, 31 | "entryModule": "client/app.module#MainModule" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.browser.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "angularCompilerOptions": { 4 | "entryModule": "./client/modules/browser-app.module#BrowserAppModule" 5 | }, 6 | "exclude": [ 7 | "./main.server.aot.ts" 8 | ] 9 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": false, 4 | "target": "es5", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "sourceMap": false, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "removeComments": false, 11 | "noImplicitAny": false, 12 | "allowSyntheticDefaultImports": true, 13 | "suppressImplicitAnyIndexErrors": true, 14 | "rootDir": "./", 15 | "outDir": "dist", 16 | "lib": ["es2016", "dom"], 17 | "typeRoots": [ 18 | "./node_modules/@types" 19 | ] 20 | }, 21 | "compileOnSave": false, 22 | "exclude": [ 23 | "./dist", 24 | "./node_modules", 25 | "./config/gulp", 26 | "./client/app-aot.ts" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "angularCompilerOptions": { 4 | "entryModule": "./client/modules/server-app.module#ServerAppModule" 5 | }, 6 | "exclude": [] 7 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // Look in ./config/webpack folder for webpack.client.x.js 2 | 3 | module.exports = function(env) { 4 | // use webpack --env to change environment 5 | switch (env) { 6 | case 'server:prod:e2e': 7 | return require('./config/webpack/webpack.server')({ env: 'prod:e2e' }); 8 | break; 9 | case 'prod:e2e': 10 | return require('./config/webpack/webpack.client')({ env: 'prod', e2e: true }); 11 | break; 12 | case 'server:prod': 13 | return require('./config/webpack/webpack.server')({ env: 'prod' }); 14 | break; 15 | case 'prod': 16 | case 'production': 17 | return require('./config/webpack/webpack.client')({ env: 'prod' }); 18 | break; 19 | case 'server:test': 20 | return require('./config/webpack/webpack.server')({ env: 'test' }); 21 | break; 22 | case 'test': 23 | case 'testing': 24 | return require('./config/webpack/webpack.client')({ env: 'test' }); 25 | break; 26 | case 'server:dev': 27 | return require('./config/webpack/webpack.server')({ env: 'dev' }); 28 | break; 29 | case 'dev': 30 | case 'development': 31 | default: 32 | return require('./config/webpack/webpack.client')({ env: 'dev' }); 33 | } 34 | } 35 | --------------------------------------------------------------------------------