├── .babelrc ├── .editorconfig ├── .env.development ├── .flowconfig ├── .gitignore ├── .sequelizerc ├── LICENSE ├── README.md ├── flow-typed ├── npm │ ├── bcrypt_v1.x.x.js │ ├── body-parser_v1.x.x.js │ ├── express_v4.16.x.js │ ├── flow-bin_v0.x.x.js │ ├── history_v4.x.x.js │ ├── pg_v7.x.x.js │ ├── react-redux_v5.x.x.js │ ├── react-router-dom_v4.x.x.js │ ├── react-router_v4.x.x.js │ ├── redux_v4.x.x.js │ ├── sequelize_v4.x.x.js │ └── uuid_v3.x.x.js └── webpack.js ├── package.json ├── scripts ├── console.js ├── sequelize-cli.config.js ├── start-debug.js └── start-hot.js ├── src ├── .babelrc ├── actions │ ├── index.js │ └── user.js ├── api │ ├── index.js │ ├── routes.js │ └── types.js ├── app │ ├── components │ │ ├── Dashboard │ │ │ ├── Home │ │ │ │ └── index.js │ │ │ └── index.js │ │ └── Login │ │ │ ├── EnsureLoggedInContainer.js │ │ │ ├── LoginForm.js │ │ │ └── index.js │ ├── index.css │ └── index.js ├── browser │ └── index.js ├── reducers │ ├── index.js │ └── user.js ├── routes │ └── index.js ├── server │ ├── api │ │ └── index.js │ ├── authentication │ │ ├── index.js │ │ └── passport.js │ ├── db │ │ ├── index.js │ │ ├── migrations │ │ │ └── 20180312145544-create-user.js │ │ ├── models │ │ │ ├── index.js │ │ │ └── user │ │ │ │ └── index.js │ │ └── seeds │ │ │ └── 20180312165657-admin-user.js │ ├── env.js │ ├── hmr.js │ ├── index.js │ └── routes │ │ └── index.js └── store │ └── index.js ├── webpack.config.babel.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ], 11 | "@babel/flow" 12 | ], 13 | "plugins": [ 14 | "@babel/plugin-syntax-dynamic-import", 15 | "@babel/plugin-syntax-import-meta", 16 | "@babel/plugin-proposal-class-properties", 17 | "@babel/plugin-proposal-json-strings" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = tab 8 | trim_trailing_whitespace = true 9 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | HOSTNAME=localhost 2 | PORT=8080 3 | COOKIE_SESSION_SECRET=myn0tsosecretcookies3creeet 4 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/config-chain/test/broken.json 3 | 4 | [include] 5 | 6 | [libs] 7 | ./flow-typed 8 | 9 | [options] 10 | emoji=true 11 | 12 | munge_underscores=true 13 | 14 | suppress_type=$FlowIssue 15 | suppress_type=$FlowFixMe 16 | 17 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue 18 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | .DS_Store 3 | 4 | # IntelliJ 5 | .idea/ 6 | 7 | # node.js 8 | node_modules/ 9 | 10 | # dotenv (private) 11 | /.env 12 | 13 | # build 14 | /dist 15 | -------------------------------------------------------------------------------- /.sequelizerc: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | 'config': path.resolve('scripts', 'sequelize-cli.config.js'), 5 | 'models-path': path.resolve('src', 'server', 'db', 'models'), 6 | 'seeders-path': path.resolve('src', 'server', 'db', 'seeds'), 7 | 'migrations-path': path.resolve('src', 'server', 'db', 'migrations') 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Glass Echidna Pty Ltd 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Practical React SSR Template 2 | 3 | A React, React SSR, Redux, React Router, Express (API) and Sequelize (PostgreSQL) template for real-world applications. 4 | 5 | ## What's included? 6 | 7 | We're attempting to avoid being _too_ opinionated, however we're providing a fairly feature-complete starting point 8 | compared to some other React templates, so we've definitely taken some liberty to do things in a particular fashion. 9 | 10 | ### Core functionality 11 | 12 | * React v16 13 | * React server-side rendering 14 | * Redux (state management) - server and client 15 | * React Router v4 16 | * Express API (i.e. REST-like web services) 17 | * Webpack v4 18 | * Hot module reloading (browser _and server_) 19 | * Babel v7 (next) i.e. ES7 support 20 | * Flow (static type checking) 21 | * CSS module & JSS support 22 | * PostgreSQL integration (Sequelize) 23 | * Support for database migrations and seeds 24 | * Authentication (Passport, Bcrypt and cookie-session) 25 | * Console/REPL 26 | 27 | ### Example (quick start) features 28 | 29 | * Simple `User` ORM model 30 | * `/login` end-point 31 | * Redux actions, reducers for logging in 32 | * Bare bones React components necessary to login 33 | 34 | ### What's not included? 35 | 36 | * Much documentation. 37 | 38 | This isn't intended as a "learning template", it's designed to get a practical React SSR project up and running quickly. 39 | 40 | There is a reasonable amount of _by example_ code (surrounding the login flow), however you should refer to third-party 41 | documentation whenever you want to do something that hasn't already been demonstrated. 42 | 43 | ## Setup 44 | 45 | ### External dependencies 46 | 47 | You must have PostgreSQL installed on your system. 48 | 49 | In the case of a macOS development machine, PostgreSQL can be installed trivially from Homebrew with: 50 | 51 | ``` 52 | brew install postgres 53 | ``` 54 | 55 | 56 | ### Install node dependencies 57 | 58 | ``` 59 | yarn 60 | ``` 61 | 62 | ### Database initialization 63 | 64 | Create a `.env` file and add `DATABASE_URL` in the form of: 65 | 66 | ``` 67 | DATABASE_URL=postgres://:@localhost:5432/ 68 | ``` 69 | 70 | Then create and seed the database: 71 | 72 | ``` 73 | yarn run sequelize db:create 74 | yarn run sequelize db:migrate 75 | ``` 76 | 77 | ### Seeding the database 78 | 79 | ``` 80 | yarn run sequelize db:seed:all 81 | ``` 82 | 83 | This will create a demo user for you with the credentials: 84 | 85 | Username: `admin@localhost` 86 | Password: `practicalSSR` 87 | 88 | The seed files can be found at `src/server/db/seeds`. You'll obviously want to at the very least change (or delete) 89 | the seed user's credentials before deploying anything to production. 90 | 91 | ## Build / Execution 92 | 93 | ### Run/Debug 94 | 95 | ``` 96 | yarn debug 97 | ``` 98 | 99 | You may now open your browser to `http://localhost:8080` and proceed to login, assuming you've seeded the database or 100 | created a user from the console. 101 | 102 | ### Build 103 | 104 | To build a production bundle: 105 | 106 | ``` 107 | yarn build 108 | ``` 109 | 110 | ### Execution 111 | 112 | To run in production: 113 | 114 | ``` 115 | yarn start 116 | ``` 117 | 118 | Keep in mind that we've provided a `.env.development`, but not a `.env.production`. You'll need to configure environment variables yourself using `.env.development` as reference. 119 | 120 | ### Analyze browser bundle size 121 | 122 | Thanks to [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) you can view a treemap 123 | of which modules (and source code) are contributing most to the generated bundle's file size. 124 | 125 | ``` 126 | yarn analyze 127 | ``` 128 | 129 | ## Debug 130 | 131 | Hot module reloading is always enabled in the browser when debugging, however you can optionally debug with or without 132 | server-side debugging. 133 | 134 | Server-side hot module reloading results in the server being restarted when changes are made, as such it isn't always 135 | desirable as the server restarts can slow-down development and tend to mess with debug break-points. 136 | 137 | ### Browser hot module reloading 138 | 139 | ``` 140 | yarn debug 141 | ``` 142 | 143 | ### Server (and browser) hot module reloading 144 | 145 | ``` 146 | yarn debug-hot 147 | ``` 148 | 149 | ### Production server with browser hot module reloading 150 | 151 | ``` 152 | yarn start-hot 153 | ``` 154 | 155 | ## Console / REPL 156 | 157 | A console is provided where ORM models are automatically loaded. 158 | 159 | ``` 160 | yarn console 161 | ``` 162 | -------------------------------------------------------------------------------- /flow-typed/npm/bcrypt_v1.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 96d9e6596558a201899e45822d93e38d 2 | // flow-typed version: da30fe6876/bcrypt_v1.x.x/flow_>=v0.25.x 3 | 4 | declare module bcrypt { 5 | declare function genSaltSync(rounds?: number): string; 6 | declare function genSalt(rounds: number): Promise; 7 | declare function genSalt(): Promise; 8 | declare function genSalt(callback: (err: Error, salt: string) => void): void; 9 | declare function genSalt( 10 | rounds: number, 11 | callback: (err: Error, salt: string) => void 12 | ): void; 13 | declare function hashSync(data: string, salt: string): string; 14 | declare function hashSync(data: string, rounds: number): string; 15 | declare function hash( 16 | data: string, 17 | saltOrRounds: string | number 18 | ): Promise; 19 | declare function hash( 20 | data: string, 21 | rounds: number, 22 | callback: (err: Error, encrypted: string) => void 23 | ): void; 24 | declare function hash( 25 | data: string, 26 | salt: string, 27 | callback: (err: Error, encrypted: string) => void 28 | ): void; 29 | declare function compareSync(data: string, encrypted: string): boolean; 30 | declare function compare(data: string, encrypted: string): Promise; 31 | declare function compare( 32 | data: string, 33 | encrypted: string, 34 | callback: (err: Error, same: boolean) => void 35 | ): void; 36 | declare function getRounds(encrypted: string): number; 37 | } 38 | -------------------------------------------------------------------------------- /flow-typed/npm/body-parser_v1.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: bac0ee66e0653772d037dc47b51a5e1f 2 | // flow-typed version: da30fe6876/body-parser_v1.x.x/flow_>=v0.25.x 3 | 4 | import type { Middleware, $Request, $Response } from "express"; 5 | 6 | declare type bodyParser$Options = { 7 | inflate?: boolean, 8 | limit?: number | string, 9 | type?: string | string[] | ((req: $Request) => any), 10 | verify?: ( 11 | req: $Request, 12 | res: $Response, 13 | buf: Buffer, 14 | encoding: string 15 | ) => void 16 | }; 17 | 18 | declare type bodyParser$OptionsText = bodyParser$Options & { 19 | reviver?: (key: string, value: any) => any, 20 | strict?: boolean 21 | }; 22 | 23 | declare type bodyParser$OptionsJson = bodyParser$Options & { 24 | reviver?: (key: string, value: any) => any, 25 | strict?: boolean 26 | }; 27 | 28 | declare type bodyParser$OptionsUrlencoded = bodyParser$Options & { 29 | extended?: boolean, 30 | parameterLimit?: number 31 | }; 32 | 33 | declare module "body-parser" { 34 | declare type Options = bodyParser$Options; 35 | declare type OptionsText = bodyParser$OptionsText; 36 | declare type OptionsJson = bodyParser$OptionsJson; 37 | declare type OptionsUrlencoded = bodyParser$OptionsUrlencoded; 38 | 39 | declare function json(options?: OptionsJson): Middleware; 40 | 41 | declare function raw(options?: Options): Middleware; 42 | 43 | declare function text(options?: OptionsText): Middleware; 44 | 45 | declare function urlencoded(options?: OptionsUrlencoded): Middleware; 46 | } 47 | -------------------------------------------------------------------------------- /flow-typed/npm/express_v4.16.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: cc24a4e737d9dfb8e1381c3bd4ebaa65 2 | // flow-typed version: d11eab7bb5/express_v4.16.x/flow_>=v0.32.x 3 | 4 | import type { Server } from "http"; 5 | import type { Socket } from "net"; 6 | 7 | declare type express$RouterOptions = { 8 | caseSensitive?: boolean, 9 | mergeParams?: boolean, 10 | strict?: boolean 11 | }; 12 | 13 | declare class express$RequestResponseBase { 14 | app: express$Application; 15 | get(field: string): string | void; 16 | } 17 | 18 | declare type express$RequestParams = { 19 | [param: string]: string 20 | }; 21 | 22 | declare class express$Request extends http$IncomingMessage mixins express$RequestResponseBase { 23 | baseUrl: string; 24 | body: mixed; 25 | cookies: { [cookie: string]: string }; 26 | connection: Socket; 27 | fresh: boolean; 28 | hostname: string; 29 | ip: string; 30 | ips: Array; 31 | method: string; 32 | originalUrl: string; 33 | params: express$RequestParams; 34 | path: string; 35 | protocol: "https" | "http"; 36 | query: { [name: string]: string | Array }; 37 | route: string; 38 | secure: boolean; 39 | signedCookies: { [signedCookie: string]: string }; 40 | stale: boolean; 41 | subdomains: Array; 42 | xhr: boolean; 43 | accepts(types: string): string | false; 44 | accepts(types: Array): string | false; 45 | acceptsCharsets(...charsets: Array): string | false; 46 | acceptsEncodings(...encoding: Array): string | false; 47 | acceptsLanguages(...lang: Array): string | false; 48 | header(field: string): string | void; 49 | is(type: string): boolean; 50 | param(name: string, defaultValue?: string): string | void; 51 | } 52 | 53 | declare type express$CookieOptions = { 54 | domain?: string, 55 | encode?: (value: string) => string, 56 | expires?: Date, 57 | httpOnly?: boolean, 58 | maxAge?: number, 59 | path?: string, 60 | secure?: boolean, 61 | signed?: boolean 62 | }; 63 | 64 | declare type express$Path = string | RegExp; 65 | 66 | declare type express$RenderCallback = ( 67 | err: Error | null, 68 | html?: string 69 | ) => mixed; 70 | 71 | declare type express$SendFileOptions = { 72 | maxAge?: number, 73 | root?: string, 74 | lastModified?: boolean, 75 | headers?: { [name: string]: string }, 76 | dotfiles?: "allow" | "deny" | "ignore" 77 | }; 78 | 79 | declare class express$Response extends http$ServerResponse mixins express$RequestResponseBase { 80 | headersSent: boolean; 81 | locals: { [name: string]: mixed }; 82 | append(field: string, value?: string): this; 83 | attachment(filename?: string): this; 84 | cookie(name: string, value: string, options?: express$CookieOptions): this; 85 | clearCookie(name: string, options?: express$CookieOptions): this; 86 | download( 87 | path: string, 88 | filename?: string, 89 | callback?: (err?: ?Error) => void 90 | ): this; 91 | format(typesObject: { [type: string]: Function }): this; 92 | json(body?: mixed): this; 93 | jsonp(body?: mixed): this; 94 | links(links: { [name: string]: string }): this; 95 | location(path: string): this; 96 | redirect(url: string, ...args: Array): this; 97 | redirect(status: number, url: string, ...args: Array): this; 98 | render( 99 | view: string, 100 | locals?: { [name: string]: mixed }, 101 | callback?: express$RenderCallback 102 | ): this; 103 | send(body?: mixed): this; 104 | sendFile( 105 | path: string, 106 | options?: express$SendFileOptions, 107 | callback?: (err?: ?Error) => mixed 108 | ): this; 109 | sendStatus(statusCode: number): this; 110 | header(field: string, value?: string): this; 111 | header(headers: { [name: string]: string }): this; 112 | set(field: string, value?: string | string[]): this; 113 | set(headers: { [name: string]: string }): this; 114 | status(statusCode: number): this; 115 | type(type: string): this; 116 | vary(field: string): this; 117 | req: express$Request; 118 | } 119 | 120 | declare type express$NextFunction = (err?: ?Error | "route") => mixed; 121 | declare type express$Middleware = 122 | | (( 123 | req: $Subtype, 124 | res: express$Response, 125 | next: express$NextFunction 126 | ) => mixed) 127 | | (( 128 | error: Error, 129 | req: $Subtype, 130 | res: express$Response, 131 | next: express$NextFunction 132 | ) => mixed); 133 | declare interface express$RouteMethodType { 134 | (middleware: express$Middleware): T; 135 | (...middleware: Array): T; 136 | ( 137 | path: express$Path | express$Path[], 138 | ...middleware: Array 139 | ): T; 140 | } 141 | declare class express$Route { 142 | all: express$RouteMethodType; 143 | get: express$RouteMethodType; 144 | post: express$RouteMethodType; 145 | put: express$RouteMethodType; 146 | head: express$RouteMethodType; 147 | delete: express$RouteMethodType; 148 | options: express$RouteMethodType; 149 | trace: express$RouteMethodType; 150 | copy: express$RouteMethodType; 151 | lock: express$RouteMethodType; 152 | mkcol: express$RouteMethodType; 153 | move: express$RouteMethodType; 154 | purge: express$RouteMethodType; 155 | propfind: express$RouteMethodType; 156 | proppatch: express$RouteMethodType; 157 | unlock: express$RouteMethodType; 158 | report: express$RouteMethodType; 159 | mkactivity: express$RouteMethodType; 160 | checkout: express$RouteMethodType; 161 | merge: express$RouteMethodType; 162 | 163 | // @TODO Missing 'm-search' but get flow illegal name error. 164 | 165 | notify: express$RouteMethodType; 166 | subscribe: express$RouteMethodType; 167 | unsubscribe: express$RouteMethodType; 168 | patch: express$RouteMethodType; 169 | search: express$RouteMethodType; 170 | connect: express$RouteMethodType; 171 | } 172 | 173 | declare class express$Router extends express$Route { 174 | constructor(options?: express$RouterOptions): void; 175 | route(path: string): express$Route; 176 | static (options?: express$RouterOptions): express$Router; 177 | use(middleware: express$Middleware): this; 178 | use(...middleware: Array): this; 179 | use( 180 | path: express$Path | express$Path[], 181 | ...middleware: Array 182 | ): this; 183 | use(path: string, router: express$Router): this; 184 | handle( 185 | req: http$IncomingMessage, 186 | res: http$ServerResponse, 187 | next: express$NextFunction 188 | ): void; 189 | param( 190 | param: string, 191 | callback: ( 192 | req: $Subtype, 193 | res: express$Response, 194 | next: express$NextFunction, 195 | id: string 196 | ) => mixed 197 | ): void; 198 | ( 199 | req: http$IncomingMessage, 200 | res: http$ServerResponse, 201 | next?: ?express$NextFunction 202 | ): void; 203 | } 204 | 205 | /* 206 | With flow-bin ^0.59, express app.listen() is deemed to return any and fails flow type coverage. 207 | Which is ironic because https://github.com/facebook/flow/blob/master/Changelog.md#misc-2 (release notes for 0.59) 208 | says "Improves typings for Node.js HTTP server listen() function." See that? IMPROVES! 209 | To work around this issue, we changed Server to ?Server here, so that our invocations of express.listen() will 210 | not be deemed to lack type coverage. 211 | */ 212 | 213 | declare class express$Application extends express$Router mixins events$EventEmitter { 214 | constructor(): void; 215 | locals: { [name: string]: mixed }; 216 | mountpath: string; 217 | listen( 218 | port: number, 219 | hostname?: string, 220 | backlog?: number, 221 | callback?: (err?: ?Error) => mixed 222 | ): ?Server; 223 | listen( 224 | port: number, 225 | hostname?: string, 226 | callback?: (err?: ?Error) => mixed 227 | ): ?Server; 228 | listen(port: number, callback?: (err?: ?Error) => mixed): ?Server; 229 | listen(path: string, callback?: (err?: ?Error) => mixed): ?Server; 230 | listen(handle: Object, callback?: (err?: ?Error) => mixed): ?Server; 231 | disable(name: string): void; 232 | disabled(name: string): boolean; 233 | enable(name: string): express$Application; 234 | enabled(name: string): boolean; 235 | engine(name: string, callback: Function): void; 236 | /** 237 | * Mixed will not be taken as a value option. Issue around using the GET http method name and the get for settings. 238 | */ 239 | // get(name: string): mixed; 240 | set(name: string, value: mixed): mixed; 241 | render( 242 | name: string, 243 | optionsOrFunction: { [name: string]: mixed }, 244 | callback: express$RenderCallback 245 | ): void; 246 | handle( 247 | req: http$IncomingMessage, 248 | res: http$ServerResponse, 249 | next?: ?express$NextFunction 250 | ): void; 251 | // callable signature is not inherited 252 | ( 253 | req: http$IncomingMessage, 254 | res: http$ServerResponse, 255 | next?: ?express$NextFunction 256 | ): void; 257 | } 258 | 259 | declare type JsonOptions = { 260 | inflate?: boolean, 261 | limit?: string | number, 262 | reviver?: (key: string, value: mixed) => mixed, 263 | strict?: boolean, 264 | type?: string | Array | ((req: express$Request) => boolean), 265 | verify?: ( 266 | req: express$Request, 267 | res: express$Response, 268 | buf: Buffer, 269 | encoding: string 270 | ) => mixed 271 | }; 272 | 273 | declare type express$UrlEncodedOptions = { 274 | extended?: boolean, 275 | inflate?: boolean, 276 | limit?: string | number, 277 | parameterLimit?: number, 278 | type?: string | Array | ((req: express$Request) => boolean), 279 | verify?: ( 280 | req: express$Request, 281 | res: express$Response, 282 | buf: Buffer, 283 | encoding: string 284 | ) => mixed, 285 | } 286 | 287 | declare module "express" { 288 | declare export type RouterOptions = express$RouterOptions; 289 | declare export type CookieOptions = express$CookieOptions; 290 | declare export type Middleware = express$Middleware; 291 | declare export type NextFunction = express$NextFunction; 292 | declare export type RequestParams = express$RequestParams; 293 | declare export type $Response = express$Response; 294 | declare export type $Request = express$Request; 295 | declare export type $Application = express$Application; 296 | 297 | declare module.exports: { 298 | (): express$Application, // If you try to call like a function, it will use this signature 299 | json: (opts: ?JsonOptions) => express$Middleware, 300 | static: (root: string, options?: Object) => express$Middleware, // `static` property on the function 301 | Router: typeof express$Router, // `Router` property on the function 302 | urlencoded: (opts: ?express$UrlEncodedOptions) => express$Middleware, 303 | }; 304 | } 305 | -------------------------------------------------------------------------------- /flow-typed/npm/flow-bin_v0.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 6a5610678d4b01e13bbfbbc62bdaf583 2 | // flow-typed version: 3817bc6980/flow-bin_v0.x.x/flow_>=v0.25.x 3 | 4 | declare module "flow-bin" { 5 | declare module.exports: string; 6 | } 7 | -------------------------------------------------------------------------------- /flow-typed/npm/history_v4.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: eb8bd974b677b08dfca89de9ac05b60b 2 | // flow-typed version: 43b30482ac/history_v4.x.x/flow_>=v0.25.x 3 | 4 | declare module "history/createBrowserHistory" { 5 | declare function Unblock(): void; 6 | 7 | declare export type Action = "PUSH" | "REPLACE" | "POP"; 8 | 9 | declare export type BrowserLocation = { 10 | pathname: string, 11 | search: string, 12 | hash: string, 13 | // Browser and Memory specific 14 | state: string, 15 | key: string, 16 | }; 17 | 18 | declare export type BrowserHistory = { 19 | length: number, 20 | location: BrowserLocation, 21 | action: Action, 22 | push: (path: string, Array) => void, 23 | replace: (path: string, Array) => void, 24 | go: (n: number) => void, 25 | goBack: () => void, 26 | goForward: () => void, 27 | listen: Function, 28 | block: (message: string) => Unblock, 29 | block: ((location: BrowserLocation, action: Action) => string) => Unblock, 30 | push: (path: string) => void, 31 | replace: (path: string) => void, 32 | }; 33 | 34 | declare type HistoryOpts = { 35 | basename?: string, 36 | forceRefresh?: boolean, 37 | getUserConfirmation?: ( 38 | message: string, 39 | callback: (willContinue: boolean) => void, 40 | ) => void, 41 | }; 42 | 43 | declare export default (opts?: HistoryOpts) => BrowserHistory; 44 | } 45 | 46 | declare module "history/createMemoryHistory" { 47 | declare function Unblock(): void; 48 | 49 | declare export type Action = "PUSH" | "REPLACE" | "POP"; 50 | 51 | declare export type MemoryLocation = { 52 | pathname: string, 53 | search: string, 54 | hash: string, 55 | // Browser and Memory specific 56 | state: string, 57 | key: string, 58 | }; 59 | 60 | declare export type MemoryHistory = { 61 | length: number, 62 | location: MemoryLocation, 63 | action: Action, 64 | index: number, 65 | entries: Array, 66 | push: (path: string, Array) => void, 67 | replace: (path: string, Array) => void, 68 | go: (n: number) => void, 69 | goBack: () => void, 70 | goForward: () => void, 71 | // Memory only 72 | canGo: (n: number) => boolean, 73 | listen: Function, 74 | block: (message: string) => Unblock, 75 | block: ((location: MemoryLocation, action: Action) => string) => Unblock, 76 | push: (path: string) => void, 77 | }; 78 | 79 | declare type HistoryOpts = { 80 | initialEntries?: Array, 81 | initialIndex?: number, 82 | keyLength?: number, 83 | getUserConfirmation?: ( 84 | message: string, 85 | callback: (willContinue: boolean) => void, 86 | ) => void, 87 | }; 88 | 89 | declare export default (opts?: HistoryOpts) => MemoryHistory; 90 | } 91 | 92 | declare module "history/createHashHistory" { 93 | declare function Unblock(): void; 94 | 95 | declare export type Action = "PUSH" | "REPLACE" | "POP"; 96 | 97 | declare export type HashLocation = { 98 | pathname: string, 99 | search: string, 100 | hash: string, 101 | }; 102 | 103 | declare export type HashHistory = { 104 | length: number, 105 | location: HashLocation, 106 | action: Action, 107 | push: (path: string, Array) => void, 108 | replace: (path: string, Array) => void, 109 | go: (n: number) => void, 110 | goBack: () => void, 111 | goForward: () => void, 112 | listen: Function, 113 | block: (message: string) => Unblock, 114 | block: ((location: HashLocation, action: Action) => string) => Unblock, 115 | push: (path: string) => void, 116 | }; 117 | 118 | declare type HistoryOpts = { 119 | basename?: string, 120 | hashType: "slash" | "noslash" | "hashbang", 121 | getUserConfirmation?: ( 122 | message: string, 123 | callback: (willContinue: boolean) => void, 124 | ) => void, 125 | }; 126 | 127 | declare export default (opts?: HistoryOpts) => HashHistory; 128 | } 129 | -------------------------------------------------------------------------------- /flow-typed/npm/pg_v7.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: bce25cf9995831e2ac1ebae0b4169cfc 2 | // flow-typed version: a175a2307f/pg_v7.x.x/flow_>=v0.28.x 3 | 4 | declare module pg { 5 | // Note: Currently There are some issues in Function overloading. 6 | // https://github.com/facebook/flow/issues/2423 7 | // So i temporarily remove the 8 | // `((event: string, listener: Function) => EventEmitter );` 9 | // from all overloading for EventEmitter.on(). 10 | 11 | // `any` types exised in this file, cause of currently `mixed` did not work well 12 | // in Function Overloading. 13 | 14 | // `Function` types exised in this file, cause of they come from another 15 | // untyped npm lib. 16 | 17 | /* Cause of > 31 | /* 32 | * PgPoolConfig's properties are passed unchanged to both 33 | * the node-postgres Client constructor and the node-pool constructor 34 | * allowing you to fully configure the behavior of both 35 | * node-pool (https://github.com/coopernurse/node-pool) 36 | */ 37 | declare type PgPoolConfig = { 38 | // node-pool ---------------- 39 | name: string, 40 | create: Function, 41 | destroy: Function, 42 | max: number, 43 | min: number, 44 | refreshIdle: boolean, 45 | idleTimeoutMillis: number, 46 | connectionTimeoutMillis: number, 47 | reapIntervalMillis: number, 48 | returnToHead: boolean, 49 | priorityRange: number, 50 | validate: Function, 51 | validateAsync: Function, 52 | log: Function, 53 | 54 | // node-postgres Client ------ 55 | //database connection string to define some other config parameters 56 | connectionString: string, 57 | //database user's name 58 | user: string, 59 | //name of database to connect 60 | database: string, 61 | //database user's password 62 | password: string, 63 | //database port 64 | port: number, 65 | // database host. defaults to localhost 66 | host?: string, 67 | // whether to try SSL/TLS to connect to server. default value: false 68 | ssl?: boolean, 69 | // name displayed in the pg_stat_activity view and included in CSV log entries 70 | // default value: process.env.PGAPPNAME 71 | application_name?: string, 72 | // fallback value for the application_name configuration parameter 73 | // default value: false 74 | fallback_application_name?: string, 75 | // max milliseconds any query using this connection will execute for before timing out in error. false=unlimited 76 | // default value: false 77 | statement_timeout?: boolean | number, 78 | // pg-pool 79 | Client: mixed, 80 | Promise: mixed, 81 | onCreate: Function, 82 | }; 83 | 84 | /* 85 | * Not extends from Client, cause some of Client's functions(ex: connect and end) 86 | * should not be used by PoolClient (which returned from Pool.connect). 87 | */ 88 | declare type PoolClient = { 89 | release(error?: mixed): void, 90 | 91 | query: 92 | ( (query: QueryConfig|string, callback?: QueryCallback) => Query ) & 93 | ( (text: string, values: Array, callback?: QueryCallback) => Query ), 94 | 95 | on: 96 | ((event: 'drain', listener: () => void) => events$EventEmitter )& 97 | ((event: 'error', listener: (err: PG_ERROR) => void) => events$EventEmitter )& 98 | ((event: 'notification', listener: (message: any) => void) => events$EventEmitter )& 99 | ((event: 'notice', listener: (message: any) => void) => events$EventEmitter )& 100 | ((event: 'end', listener: () => void) => events$EventEmitter ), 101 | } 102 | 103 | declare type PoolConnectCallback = (error: PG_ERROR|null, 104 | client: PoolClient|null, done: DoneCallback) => void; 105 | declare type DoneCallback = (error?: mixed) => void; 106 | // https://github.com/facebook/flow/blob/master/lib/node.js#L581 107 | // on() returns a events$EventEmitter 108 | declare class Pool extends events$EventEmitter { 109 | constructor(options: $Shape, Client?: Class): void; 110 | connect(cb?: PoolConnectCallback): Promise; 111 | take(cb?: PoolConnectCallback): Promise; 112 | end(cb?: DoneCallback): Promise; 113 | 114 | // Note: not like the pg's Client, the Pool.query return a Promise, 115 | // not a Thenable Query which Client returned. 116 | // And there is a flow(<0.34) issue here, when Array, 117 | // the overloading will not work 118 | query: 119 | ( (query: QueryConfig|string, callback?: QueryCallback) => Promise ) & 120 | ( (text: string, values: Array, callback?: QueryCallback) => Promise); 121 | 122 | /* flow issue: https://github.com/facebook/flow/issues/2423 123 | * When this fixed, this overloading can be used. 124 | */ 125 | /* 126 | on: 127 | ((event: 'connect', listener: (client: PoolClient) => void) => events$EventEmitter )& 128 | ((event: 'acquire', listener: (client: PoolClient) => void) => events$EventEmitter )& 129 | ((event: "error", listener: (err: PG_ERROR) => void) => events$EventEmitter )& 130 | ((event: string, listener: Function) => events$EventEmitter); 131 | */ 132 | } 133 | 134 | // <<------------- copy from 'pg-pool' ------------------------------ 135 | 136 | 137 | // error 138 | declare type PG_ERROR = { 139 | name: string, 140 | length: number, 141 | severity: string, 142 | code: string, 143 | detail: string|void, 144 | hint: string|void, 145 | position: string|void, 146 | internalPosition: string|void, 147 | internalQuery: string|void, 148 | where: string|void, 149 | schema: string|void, 150 | table: string|void, 151 | column: string|void, 152 | dataType: string|void, 153 | constraint: string|void, 154 | file: string|void, 155 | line: string|void, 156 | routine: string|void 157 | }; 158 | 159 | declare type ClientConfig = { 160 | //database user's name 161 | user?: string, 162 | //name of database to connect 163 | database?: string, 164 | //database user's password 165 | password?: string, 166 | //database port 167 | port?: number, 168 | // database host. defaults to localhost 169 | host?: string, 170 | // whether to try SSL/TLS to connect to server. default value: false 171 | ssl?: boolean, 172 | // name displayed in the pg_stat_activity view and included in CSV log entries 173 | // default value: process.env.PGAPPNAME 174 | application_name?: string, 175 | // fallback value for the application_name configuration parameter 176 | // default value: false 177 | fallback_application_name?: string, 178 | } 179 | 180 | declare type Row = { 181 | [key: string]: mixed, 182 | }; 183 | declare type ResultSet = { 184 | command: string, 185 | rowCount: number, 186 | oid: number, 187 | rows: Array, 188 | }; 189 | declare type ResultBuilder = { 190 | command: string, 191 | rowCount: number, 192 | oid: number, 193 | rows: Array, 194 | addRow: (row: Row) => void, 195 | }; 196 | declare type QueryConfig = { 197 | name?: string, 198 | text: string, 199 | values?: any[], 200 | }; 201 | 202 | declare type QueryCallback = (err: PG_ERROR|null, result: ResultSet|void) => void; 203 | declare type ClientConnectCallback = (err: PG_ERROR|null, client: Client|void) => void; 204 | 205 | /* 206 | * lib/query.js 207 | * Query extends from EventEmitter in source code. 208 | * but in Flow there is no multiple extends. 209 | * And in Flow await is a `declare function $await(p: Promise | T): T;` 210 | * seems can not resolve a Thenable's value type directly 211 | * so `Query extends Promise` to make thing temporarily work. 212 | * like this: 213 | * const q = client.query('select * from some'); 214 | * q.on('row',cb); // Event 215 | * const result = await q; // or await 216 | * 217 | * ToDo: should find a better way. 218 | */ 219 | declare class Query extends Promise { 220 | then( 221 | onFulfill?: ?((value: ResultSet) => Promise | U), 222 | onReject?: ?((error: PG_ERROR) => Promise | U) 223 | ): Promise; 224 | // Because then and catch return a Promise, 225 | // .then.catch will lose catch's type information PG_ERROR. 226 | catch( 227 | onReject?: ?((error: PG_ERROR) => Promise | U) 228 | ): Promise; 229 | 230 | on : 231 | ((event: 'row', listener: (row: Row, result: ResultBuilder) => void) => events$EventEmitter )& 232 | ((event: 'end', listener: (result: ResultBuilder) => void) => events$EventEmitter )& 233 | ((event: 'error', listener: (err: PG_ERROR) => void) => events$EventEmitter ); 234 | } 235 | 236 | /* 237 | * lib/client.js 238 | * Note: not extends from EventEmitter, for This Type returned by on(). 239 | * Flow's EventEmitter force return a EventEmitter in on(). 240 | * ToDo: Not sure in on() if return events$EventEmitter or this will be more suitable 241 | * return this will restrict event to given literial when chain on().on().on(). 242 | * return a events$EventEmitter will fallback to raw EventEmitter, when chains 243 | */ 244 | declare class Client { 245 | constructor(config?: string | ClientConfig): void; 246 | connect(callback?: ClientConnectCallback):void; 247 | end(): void; 248 | 249 | escapeLiteral(str: string): string; 250 | escapeIdentifier(str: string): string; 251 | 252 | query: 253 | ( (query: QueryConfig|string, callback?: QueryCallback) => Query ) & 254 | ( (text: string, values: Array, callback?: QueryCallback) => Query ); 255 | 256 | on: 257 | ((event: 'drain', listener: () => void) => this )& 258 | ((event: 'error', listener: (err: PG_ERROR) => void) => this )& 259 | ((event: 'notification', listener: (message: any) => void) => this )& 260 | ((event: 'notice', listener: (message: any) => void) => this )& 261 | ((event: 'end', listener: () => void) => this ); 262 | } 263 | 264 | /* 265 | * require('pg-types') 266 | */ 267 | declare type TypeParserText = (value: string) => any; 268 | declare type TypeParserBinary = (value: Buffer) => any; 269 | declare type Types = { 270 | getTypeParser: 271 | ((oid: number, format?: 'text') => TypeParserText )& 272 | ((oid: number, format: 'binary') => TypeParserBinary ); 273 | 274 | setTypeParser: 275 | ((oid: number, format?: 'text', parseFn: TypeParserText) => void )& 276 | ((oid: number, format: 'binary', parseFn: TypeParserBinary) => void)& 277 | ((oid: number, parseFn: TypeParserText) => void), 278 | } 279 | 280 | /* 281 | * lib/index.js ( class PG) 282 | */ 283 | declare class PG extends events$EventEmitter { 284 | types: Types; 285 | Client: Class; 286 | Pool: Class; 287 | Connection: mixed; //Connection is used internally by the Client. 288 | constructor(client: Client): void; 289 | native: { // native binding, have the same capability like PG 290 | types: Types; 291 | Client: Class; 292 | Pool: Class; 293 | Connection: mixed; 294 | }; 295 | // The end(),connect(),cancel() in PG is abandoned ? 296 | } 297 | 298 | // These class are not exposed by pg. 299 | declare type PoolType = Pool; 300 | declare type PGType = PG; 301 | declare type QueryType = Query; 302 | // module export, keep same structure with index.js 303 | declare module.exports: PG; 304 | } 305 | -------------------------------------------------------------------------------- /flow-typed/npm/react-redux_v5.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 3d2adf9e3c8823252a60ff4631b486a3 2 | // flow-typed version: 844b6ca3d3/react-redux_v5.x.x/flow_>=v0.63.0 3 | 4 | import type { Dispatch, Store } from "redux"; 5 | 6 | declare module "react-redux" { 7 | import type { ComponentType, ElementConfig } from 'react'; 8 | 9 | declare export class Provider extends React$Component<{ 10 | store: Store, 11 | children?: any 12 | }> {} 13 | 14 | declare export function createProvider( 15 | storeKey?: string, 16 | subKey?: string 17 | ): Provider<*, *>; 18 | 19 | /* 20 | 21 | S = State 22 | A = Action 23 | OP = OwnProps 24 | SP = StateProps 25 | DP = DispatchProps 26 | MP = Merge props 27 | MDP = Map dispatch to props object 28 | RSP = Returned state props 29 | RDP = Returned dispatch props 30 | RMP = Returned merge props 31 | CP = Props for returned component 32 | Com = React Component 33 | ST = Static properties of Com 34 | */ 35 | 36 | declare type MapStateToProps = (state: S, props: SP) => RSP; 37 | 38 | declare type MapDispatchToProps = (dispatch: Dispatch, ownProps: OP) => RDP; 39 | 40 | declare type MergeProps = ( 41 | stateProps: SP, 42 | dispatchProps: DP, 43 | ownProps: MP 44 | ) => RMP; 45 | 46 | declare type ConnectOptions = {| 47 | pure?: boolean, 48 | withRef?: boolean, 49 | areStatesEqual?: (next: S, prev: S) => boolean, 50 | areOwnPropsEqual?: (next: OP, prev: OP) => boolean, 51 | areStatePropsEqual?: (next: RSP, prev: RSP) => boolean, 52 | areMergedPropsEqual?: (next: RMP, prev: RMP) => boolean, 53 | storeKey?: string 54 | |}; 55 | 56 | declare type OmitDispatch = $Diff}>; 57 | 58 | declare export function connect< 59 | Com: ComponentType<*>, 60 | S: Object, 61 | SP: Object, 62 | RSP: Object, 63 | CP: $Diff>, RSP>, 64 | ST: {[_: $Keys]: any} 65 | >( 66 | mapStateToProps: MapStateToProps, 67 | mapDispatchToProps?: null 68 | ): (component: Com) => ComponentType & $Shape; 69 | 70 | declare export function connect< 71 | Com: ComponentType<*>, 72 | ST: {[_: $Keys]: any} 73 | >( 74 | mapStateToProps?: null, 75 | mapDispatchToProps?: null 76 | ): (component: Com) => ComponentType>> & $Shape; 77 | 78 | declare export function connect< 79 | Com: ComponentType<*>, 80 | A, 81 | S: Object, 82 | DP: Object, 83 | SP: Object, 84 | RSP: Object, 85 | RDP: Object, 86 | CP: $Diff<$Diff, RSP>, RDP>, 87 | ST: $Subtype<{[_: $Keys]: any}> 88 | >( 89 | mapStateToProps: MapStateToProps, 90 | mapDispatchToProps: MapDispatchToProps 91 | ): (component: Com) => ComponentType & $Shape; 92 | 93 | declare export function connect< 94 | Com: ComponentType<*>, 95 | A, 96 | OP: Object, 97 | DP: Object, 98 | PR: Object, 99 | CP: $Diff, DP>, 100 | ST: $Subtype<{[_: $Keys]: any}> 101 | >( 102 | mapStateToProps?: null, 103 | mapDispatchToProps: MapDispatchToProps 104 | ): (Com) => ComponentType; 105 | 106 | declare export function connect< 107 | Com: ComponentType<*>, 108 | MDP: Object, 109 | ST: $Subtype<{[_: $Keys]: any}> 110 | >( 111 | mapStateToProps?: null, 112 | mapDispatchToProps: MDP 113 | ): (component: Com) => ComponentType<$Diff, MDP>> & $Shape; 114 | 115 | declare export function connect< 116 | Com: ComponentType<*>, 117 | S: Object, 118 | SP: Object, 119 | RSP: Object, 120 | MDP: Object, 121 | CP: $Diff, RSP>, 122 | ST: $Subtype<{[_: $Keys]: any}> 123 | >( 124 | mapStateToProps: MapStateToProps, 125 | mapDispatchToProps: MDP 126 | ): (component: Com) => ComponentType<$Diff & SP> & $Shape; 127 | 128 | declare export function connect< 129 | Com: ComponentType<*>, 130 | A, 131 | S: Object, 132 | DP: Object, 133 | SP: Object, 134 | RSP: Object, 135 | RDP: Object, 136 | MP: Object, 137 | RMP: Object, 138 | CP: $Diff, RMP>, 139 | ST: $Subtype<{[_: $Keys]: any}> 140 | >( 141 | mapStateToProps: MapStateToProps, 142 | mapDispatchToProps: ?MapDispatchToProps, 143 | mergeProps: MergeProps 144 | ): (component: Com) => ComponentType & $Shape; 145 | 146 | declare export function connect< 147 | Com: ComponentType<*>, 148 | A, 149 | S: Object, 150 | DP: Object, 151 | SP: Object, 152 | RSP: Object, 153 | RDP: Object, 154 | MDP: Object, 155 | MP: Object, 156 | RMP: Object, 157 | CP: $Diff, RMP>, 158 | ST: $Subtype<{[_: $Keys]: any}> 159 | >( 160 | mapStateToProps: MapStateToProps, 161 | mapDispatchToProps: MDP, 162 | mergeProps: MergeProps 163 | ): (component: Com) => ComponentType & $Shape; 164 | 165 | declare export function connect, 166 | A, 167 | S: Object, 168 | DP: Object, 169 | SP: Object, 170 | RSP: Object, 171 | RDP: Object, 172 | MP: Object, 173 | RMP: Object, 174 | ST: $Subtype<{[_: $Keys]: any}> 175 | >( 176 | mapStateToProps: ?MapStateToProps, 177 | mapDispatchToProps: ?MapDispatchToProps, 178 | mergeProps: ?MergeProps, 179 | options: ConnectOptions 180 | ): (component: Com) => ComponentType<$Diff, RMP> & SP & DP & MP> & $Shape; 181 | 182 | declare export function connect, 183 | A, 184 | S: Object, 185 | DP: Object, 186 | SP: Object, 187 | RSP: Object, 188 | RDP: Object, 189 | MDP: Object, 190 | MP: Object, 191 | RMP: Object, 192 | ST: $Subtype<{[_: $Keys]: any}> 193 | >( 194 | mapStateToProps: ?MapStateToProps, 195 | mapDispatchToProps: ?MapDispatchToProps, 196 | mergeProps: MDP, 197 | options: ConnectOptions 198 | ): (component: Com) => ComponentType<$Diff, RMP> & SP & DP & MP> & $Shape; 199 | 200 | declare export default { 201 | Provider: typeof Provider, 202 | createProvider: typeof createProvider, 203 | connect: typeof connect, 204 | }; 205 | } 206 | -------------------------------------------------------------------------------- /flow-typed/npm/react-router-dom_v4.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 9fac6739b666e8a59414aa13358b677e 2 | // flow-typed version: 187bd8b1be/react-router-dom_v4.x.x/flow_>=v0.63.x 3 | 4 | declare module "react-router-dom" { 5 | declare export class BrowserRouter extends React$Component<{| 6 | basename?: string, 7 | forceRefresh?: boolean, 8 | getUserConfirmation?: GetUserConfirmation, 9 | keyLength?: number, 10 | children?: React$Node 11 | |}> {} 12 | 13 | declare export class HashRouter extends React$Component<{| 14 | basename?: string, 15 | getUserConfirmation?: GetUserConfirmation, 16 | hashType?: "slash" | "noslash" | "hashbang", 17 | children?: React$Node 18 | |}> {} 19 | 20 | declare export class Link extends React$Component<{ 21 | className?: string, 22 | to: string | LocationShape, 23 | replace?: boolean, 24 | children?: React$Node 25 | }> {} 26 | 27 | declare export class NavLink extends React$Component<{ 28 | to: string | LocationShape, 29 | activeClassName?: string, 30 | className?: string, 31 | activeStyle?: Object, 32 | style?: Object, 33 | isActive?: (match: Match, location: Location) => boolean, 34 | children?: React$Node, 35 | exact?: boolean, 36 | strict?: boolean 37 | }> {} 38 | 39 | // NOTE: Below are duplicated from react-router. If updating these, please 40 | // update the react-router and react-router-native types as well. 41 | declare export type Location = { 42 | pathname: string, 43 | search: string, 44 | hash: string, 45 | state?: any, 46 | key?: string 47 | }; 48 | 49 | declare export type LocationShape = { 50 | pathname?: string, 51 | search?: string, 52 | hash?: string, 53 | state?: any 54 | }; 55 | 56 | declare export type HistoryAction = "PUSH" | "REPLACE" | "POP"; 57 | 58 | declare export type RouterHistory = { 59 | length: number, 60 | location: Location, 61 | action: HistoryAction, 62 | listen( 63 | callback: (location: Location, action: HistoryAction) => void 64 | ): () => void, 65 | push(path: string | LocationShape, state?: any): void, 66 | replace(path: string | LocationShape, state?: any): void, 67 | go(n: number): void, 68 | goBack(): void, 69 | goForward(): void, 70 | canGo?: (n: number) => boolean, 71 | block( 72 | callback: (location: Location, action: HistoryAction) => boolean 73 | ): void, 74 | // createMemoryHistory 75 | index?: number, 76 | entries?: Array 77 | }; 78 | 79 | declare export type Match = { 80 | params: { [key: string]: ?string }, 81 | isExact: boolean, 82 | path: string, 83 | url: string 84 | }; 85 | 86 | declare export type ContextRouter = {| 87 | history: RouterHistory, 88 | location: Location, 89 | match: Match, 90 | staticContext?: StaticRouterContext 91 | |}; 92 | 93 | declare type ContextRouterVoid = { 94 | history: RouterHistory | void, 95 | location: Location | void, 96 | match: Match | void, 97 | staticContext?: StaticRouterContext | void 98 | }; 99 | 100 | declare export type GetUserConfirmation = ( 101 | message: string, 102 | callback: (confirmed: boolean) => void 103 | ) => void; 104 | 105 | declare export type StaticRouterContext = { 106 | url?: string 107 | }; 108 | 109 | declare export class StaticRouter extends React$Component<{| 110 | basename?: string, 111 | location?: string | Location, 112 | context: StaticRouterContext, 113 | children?: React$Node 114 | |}> {} 115 | 116 | declare export class MemoryRouter extends React$Component<{| 117 | initialEntries?: Array, 118 | initialIndex?: number, 119 | getUserConfirmation?: GetUserConfirmation, 120 | keyLength?: number, 121 | children?: React$Node 122 | |}> {} 123 | 124 | declare export class Router extends React$Component<{| 125 | history: RouterHistory, 126 | children?: React$Node 127 | |}> {} 128 | 129 | declare export class Prompt extends React$Component<{| 130 | message: string | ((location: Location) => string | boolean), 131 | when?: boolean 132 | |}> {} 133 | 134 | declare export class Redirect extends React$Component<{| 135 | to: string | LocationShape, 136 | push?: boolean, 137 | from?: string, 138 | exact?: boolean, 139 | strict?: boolean 140 | |}> {} 141 | 142 | declare export class Route extends React$Component<{| 143 | component?: React$ComponentType<*>, 144 | render?: (router: ContextRouter) => React$Node, 145 | children?: React$ComponentType | React$Node, 146 | path?: string, 147 | exact?: boolean, 148 | strict?: boolean, 149 | location?: LocationShape, 150 | sensitive?: boolean 151 | |}> {} 152 | 153 | declare export class Switch extends React$Component<{| 154 | children?: React$Node, 155 | location?: Location 156 | |}> {} 157 | 158 | declare export function withRouter>( 159 | WrappedComponent: Component 160 | ): React$ComponentType< 161 | $Diff, ContextRouterVoid> 162 | >; 163 | 164 | declare type MatchPathOptions = { 165 | path?: string, 166 | exact?: boolean, 167 | sensitive?: boolean, 168 | strict?: boolean 169 | }; 170 | 171 | declare export function matchPath( 172 | pathname: string, 173 | options?: MatchPathOptions | string, 174 | parent?: Match 175 | ): null | Match; 176 | 177 | declare export function generatePath(pattern?: string, params?: Object): string; 178 | } 179 | -------------------------------------------------------------------------------- /flow-typed/npm/react-router_v4.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: e15aeed0d3686f71822b54cde7b71c83 2 | // flow-typed version: fbf3e77efa/react-router_v4.x.x/flow_>=v0.63.x 3 | 4 | declare module "react-router" { 5 | // NOTE: many of these are re-exported by react-router-dom and 6 | // react-router-native, so when making changes, please be sure to update those 7 | // as well. 8 | declare export type Location = { 9 | pathname: string, 10 | search: string, 11 | hash: string, 12 | state?: any, 13 | key?: string 14 | }; 15 | 16 | declare export type LocationShape = { 17 | pathname?: string, 18 | search?: string, 19 | hash?: string, 20 | state?: any 21 | }; 22 | 23 | declare export type HistoryAction = "PUSH" | "REPLACE" | "POP"; 24 | 25 | declare export type RouterHistory = { 26 | length: number, 27 | location: Location, 28 | action: HistoryAction, 29 | listen( 30 | callback: (location: Location, action: HistoryAction) => void 31 | ): () => void, 32 | push(path: string | LocationShape, state?: any): void, 33 | replace(path: string | LocationShape, state?: any): void, 34 | go(n: number): void, 35 | goBack(): void, 36 | goForward(): void, 37 | canGo?: (n: number) => boolean, 38 | block( 39 | callback: (location: Location, action: HistoryAction) => boolean 40 | ): void, 41 | // createMemoryHistory 42 | index?: number, 43 | entries?: Array 44 | }; 45 | 46 | declare export type Match = { 47 | params: { [key: string]: ?string }, 48 | isExact: boolean, 49 | path: string, 50 | url: string 51 | }; 52 | 53 | declare export type ContextRouter = {| 54 | history: RouterHistory, 55 | location: Location, 56 | match: Match, 57 | staticContext?: StaticRouterContext 58 | |}; 59 | 60 | declare export type GetUserConfirmation = ( 61 | message: string, 62 | callback: (confirmed: boolean) => void 63 | ) => void; 64 | 65 | declare type StaticRouterContext = { 66 | url?: string 67 | }; 68 | 69 | declare export class StaticRouter extends React$Component<{ 70 | basename?: string, 71 | location?: string | Location, 72 | context: StaticRouterContext, 73 | children?: React$Node 74 | }> {} 75 | 76 | declare export class MemoryRouter extends React$Component<{ 77 | initialEntries?: Array, 78 | initialIndex?: number, 79 | getUserConfirmation?: GetUserConfirmation, 80 | keyLength?: number, 81 | children?: React$Node 82 | }> {} 83 | 84 | declare export class Router extends React$Component<{ 85 | history: RouterHistory, 86 | children?: React$Node 87 | }> {} 88 | 89 | declare export class Prompt extends React$Component<{ 90 | message: string | ((location: Location) => string | true), 91 | when?: boolean 92 | }> {} 93 | 94 | declare export class Redirect extends React$Component<{| 95 | to: string | LocationShape, 96 | push?: boolean, 97 | from?: string, 98 | exact?: boolean, 99 | strict?: boolean 100 | |}> {} 101 | 102 | 103 | declare export class Route extends React$Component<{| 104 | component?: React$ComponentType<*>, 105 | render?: (router: ContextRouter) => React$Node, 106 | children?: React$ComponentType | React$Node, 107 | path?: string, 108 | exact?: boolean, 109 | strict?: boolean, 110 | location?: LocationShape, 111 | sensitive?: boolean 112 | |}> {} 113 | 114 | declare export class Switch extends React$Component<{| 115 | children?: React$Node, 116 | location?: Location 117 | |}> {} 118 | 119 | declare export function withRouter

( 120 | Component: React$ComponentType<{| ...ContextRouter, ...P |}> 121 | ): React$ComponentType

; 122 | 123 | declare type MatchPathOptions = { 124 | path?: string, 125 | exact?: boolean, 126 | strict?: boolean, 127 | sensitive?: boolean 128 | }; 129 | 130 | declare export function matchPath( 131 | pathname: string, 132 | options?: MatchPathOptions | string 133 | ): null | Match; 134 | } 135 | -------------------------------------------------------------------------------- /flow-typed/npm/redux_v4.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: df80bdd535bfed9cf3223e077f3b4543 2 | // flow-typed version: c4c8963c9c/redux_v4.x.x/flow_>=v0.55.x 3 | 4 | declare module 'redux' { 5 | 6 | /* 7 | 8 | S = State 9 | A = Action 10 | D = Dispatch 11 | 12 | */ 13 | 14 | declare export type DispatchAPI = (action: A) => A; 15 | declare export type Dispatch }> = DispatchAPI; 16 | 17 | declare export type MiddlewareAPI> = { 18 | dispatch: D; 19 | getState(): S; 20 | }; 21 | 22 | declare export type Store> = { 23 | // rewrite MiddlewareAPI members in order to get nicer error messages (intersections produce long messages) 24 | dispatch: D; 25 | getState(): S; 26 | subscribe(listener: () => void): () => void; 27 | replaceReducer(nextReducer: Reducer): void 28 | }; 29 | 30 | declare export type Reducer = (state: S | void, action: A) => S; 31 | 32 | declare export type CombinedReducer = (state: $Shape & {} | void, action: A) => S; 33 | 34 | declare export type Middleware> = 35 | (api: MiddlewareAPI) => 36 | (next: D) => D; 37 | 38 | declare export type StoreCreator> = { 39 | (reducer: Reducer, enhancer?: StoreEnhancer): Store; 40 | (reducer: Reducer, preloadedState: S, enhancer?: StoreEnhancer): Store; 41 | }; 42 | 43 | declare export type StoreEnhancer> = (next: StoreCreator) => StoreCreator; 44 | 45 | declare export function createStore(reducer: Reducer, enhancer?: StoreEnhancer): Store; 46 | declare export function createStore(reducer: Reducer, preloadedState?: S, enhancer?: StoreEnhancer): Store; 47 | 48 | declare export function applyMiddleware(...middlewares: Array>): StoreEnhancer; 49 | 50 | declare export type ActionCreator = (...args: Array) => A; 51 | declare export type ActionCreators = { [key: K]: ActionCreator }; 52 | 53 | declare export function bindActionCreators, D: DispatchAPI>(actionCreator: C, dispatch: D): C; 54 | declare export function bindActionCreators, D: DispatchAPI>(actionCreators: C, dispatch: D): C; 55 | 56 | declare export function combineReducers(reducers: O): CombinedReducer<$ObjMap(r: Reducer) => S>, A>; 57 | 58 | declare export var compose: $Compose; 59 | } 60 | -------------------------------------------------------------------------------- /flow-typed/npm/uuid_v3.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 3cf668e64747095cab0bb360cf2fb34f 2 | // flow-typed version: d659bd0cb8/uuid_v3.x.x/flow_>=v0.32.x 3 | 4 | declare module "uuid" { 5 | declare class uuid { 6 | static ( 7 | options?: {| 8 | random?: number[], 9 | rng?: () => number[] | Buffer 10 | |}, 11 | buffer?: number[] | Buffer, 12 | offset?: number 13 | ): string, 14 | 15 | static v1( 16 | options?: {| 17 | node?: number[], 18 | clockseq?: number, 19 | msecs?: number | Date, 20 | nsecs?: number 21 | |}, 22 | buffer?: number[] | Buffer, 23 | offset?: number 24 | ): string, 25 | 26 | static v4( 27 | options?: {| 28 | random?: number[], 29 | rng?: () => number[] | Buffer 30 | |}, 31 | buffer?: number[] | Buffer, 32 | offset?: number 33 | ): string 34 | } 35 | declare module.exports: Class; 36 | } 37 | 38 | declare module "uuid/v1" { 39 | declare class v1 { 40 | static ( 41 | options?: {| 42 | node?: number[], 43 | clockseq?: number, 44 | msecs?: number | Date, 45 | nsecs?: number 46 | |}, 47 | buffer?: number[] | Buffer, 48 | offset?: number 49 | ): string 50 | } 51 | 52 | declare module.exports: Class; 53 | } 54 | 55 | declare module "uuid/v3" { 56 | declare class v3 { 57 | static ( 58 | name?: string | number[], 59 | namespace?: string | number[], 60 | buffer?: number[] | Buffer, 61 | offset?: number 62 | ): string, 63 | 64 | static name: string, 65 | static DNS: string, 66 | static URL: string 67 | } 68 | 69 | declare module.exports: Class; 70 | } 71 | 72 | declare module "uuid/v4" { 73 | declare class v4 { 74 | static ( 75 | options?: {| 76 | random?: number[], 77 | rng?: () => number[] | Buffer 78 | |}, 79 | buffer?: number[] | Buffer, 80 | offset?: number 81 | ): string 82 | } 83 | 84 | declare module.exports: Class; 85 | } 86 | 87 | declare module "uuid/v5" { 88 | declare class v5 { 89 | static ( 90 | name?: string | number[], 91 | namespace?: string | number[], 92 | buffer?: number[] | Buffer, 93 | offset?: number 94 | ): string, 95 | 96 | static name: string, 97 | static DNS: string, 98 | static URL: string 99 | } 100 | 101 | declare module.exports: Class; 102 | } 103 | -------------------------------------------------------------------------------- /flow-typed/webpack.js: -------------------------------------------------------------------------------- 1 | declare type ModuleHotStatus = 2 | | 'idle' // The process is waiting for a call to check (see below) 3 | | 'check' // The process is checking for updates 4 | | 'prepare' // The process is getting ready for the update (e.g. downloading the updated module) 5 | | 'ready' // The update is prepared and available 6 | | 'dispose' // The process is calling the dispose handlers on the modules that will be replaced 7 | | 'apply' // The process is calling the accept handlers and re-executing self-accepted modules 8 | | 'abort' // An update was aborted, but the system is still in it's previous state 9 | | 'fail' // An update has thrown an exception and the system's state has been compromised 10 | ; 11 | 12 | declare type ModuleHotStatusHandler = (status: ModuleHotStatus) => any 13 | 14 | declare interface ModuleHot { 15 | data: any; 16 | accept(paths?: string | Array, callback?: () => any): void; 17 | decline(paths?: string | Array): void; 18 | dispose(callback: (data?: mixed) => any): void; 19 | addDisposeHandler(callback: (data: mixed) => any): void; 20 | status(): ModuleHotStatus; 21 | check(autoApply: boolean | Object): Promise; // TODO 22 | apply(options: Object): Promise; // TODO 23 | addStatusHandler(callback: ModuleHotStatusHandler): void; 24 | removeStatusHandler(callback: ModuleHotStatusHandler): void; 25 | }; 26 | 27 | declare var module: { 28 | hot?: ModuleHot, 29 | }; 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "practical-react-ssr", 3 | "version": "0.1.0", 4 | "description": "A React SSR template designed for real-world use.", 5 | "keywords": [], 6 | "homepage": "https://github.com/glassechidna/practical-react-ssr", 7 | "bugs": { 8 | "url": "https://github.com/glassechidna/practical-react-ssr/issues" 9 | }, 10 | "repository": "git+https://github.com/glassechidna/practical-react-ssr.git", 11 | "author": { 12 | "name": "Benjamin Dobell", 13 | "email": "contact@glassechidna.com.au" 14 | }, 15 | "license": "MIT", 16 | "main": "dist/bundle.js", 17 | "scripts": { 18 | "build": "webpack --env.production", 19 | "analyze": "webpack --env.production --env.analyze", 20 | "debug": "NODE_ENV=development node ./scripts/start-debug.js", 21 | "debug-hot": "NODE_ENV=development node ./scripts/start-hot.js", 22 | "start": "NODE_ENV=production node ./dist/server.js", 23 | "start-hot": "NODE_ENV=production node ./scripts/start-hot.js", 24 | "console": "NODE_ENV=development node ./scripts/console.js" 25 | }, 26 | "dependencies": { 27 | "@babel/runtime": "^7.0.0-rc.1", 28 | "bcrypt": "^3.0", 29 | "body-parser": "^1.18.2", 30 | "cookie-session": "^2.0.0-beta.3", 31 | "express": "^4.16.2", 32 | "history": "^4.7.2", 33 | "jss-preset-default": "^4.3.0", 34 | "passport": "^0.4.0", 35 | "passport-local": "^1.0.0", 36 | "pg": "^7.4.1", 37 | "pg-hstore": "^2.3.2", 38 | "react": "^16.2.0", 39 | "react-dom": "^16.2.0", 40 | "react-jss": "^8.3.3", 41 | "react-redux": "^5.0.7", 42 | "react-router": "^4.2.0", 43 | "react-router-dom": "^4.2.0", 44 | "react-router-redux": "^5.0.0-alpha.9", 45 | "redux": "^4.0.0", 46 | "redux-thunk": "^2.2.0", 47 | "sequelize": "^4.36.0", 48 | "sequelize-cli": "^4.0.0", 49 | "serialize-javascript": "^1.4.0", 50 | "uuid": "^3.2.1" 51 | }, 52 | "devDependencies": { 53 | "@babel/core": "^7.0.0-rc.1", 54 | "@babel/plugin-proposal-class-properties": "^7.0.0-rc.1", 55 | "@babel/plugin-proposal-json-strings": "^7.0.0-rc.1", 56 | "@babel/plugin-syntax-dynamic-import": "^7.0.0-rc.1", 57 | "@babel/plugin-syntax-import-meta": "^7.0.0-rc.1", 58 | "@babel/plugin-transform-runtime": "^7.0.0-rc.1", 59 | "@babel/preset-env": "^7.0.0-rc.1", 60 | "@babel/preset-flow": "^7.0.0-rc.1", 61 | "@babel/preset-react": "^7.0.0-rc.1", 62 | "@babel/register": "^7.0.0-rc.1", 63 | "babel-loader": "^8.0.0-beta.0", 64 | "css-loader": "^1.0.0", 65 | "css-modules-require-hook": "^4.2.2", 66 | "css.escape": "^1.5.1", 67 | "dotenv": "^6.0.0", 68 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 69 | "file-loader": "^1.1.5", 70 | "flow-bin": "^0.78.0", 71 | "react-hot-loader": "^4.0.0", 72 | "redux-cli-logger": "^2.0.0", 73 | "redux-logger": "^3.0.6", 74 | "shebang-loader": "^0.0.1", 75 | "style-loader": "^0.22.1", 76 | "url-loader": "^1.0.1", 77 | "webpack": "^4.16", 78 | "webpack-bundle-analyzer": "^2.11.1", 79 | "webpack-cli": "^3.1.0", 80 | "webpack-dev-middleware": "^3.0.1", 81 | "webpack-hot-middleware": "^2.19.1", 82 | "webpack-node-externals": "^1.6.0" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /scripts/console.js: -------------------------------------------------------------------------------- 1 | const repl = require('repl') 2 | 3 | require('@babel/register') 4 | 5 | global.__BROWSER__ = false 6 | global.__SERVER__ = true 7 | 8 | require('../src/server/env') 9 | require('../src/server/db/index') 10 | 11 | const replServer = repl.start({}) 12 | 13 | for (const [name, model] of Object.entries(require('../src/server/db/models/index'))) { 14 | replServer.context[name] = model 15 | } 16 | -------------------------------------------------------------------------------- /scripts/sequelize-cli.config.js: -------------------------------------------------------------------------------- 1 | require('@babel/register') 2 | 3 | const path = require('path') 4 | 5 | try { 6 | const dotenv = require('dotenv') 7 | dotenv.load({path: path.resolve(__dirname, '../', '.env')}) 8 | dotenv.load({path: path.resolve(__dirname, '../', '.env.' + env)}) 9 | } catch (e) { 10 | /* Do nothing */ 11 | } 12 | 13 | const urlMatch = process.env.DATABASE_URL.match(/postgres:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/) 14 | 15 | const postgres = { 16 | database: urlMatch[5], 17 | username: urlMatch[1], 18 | password: urlMatch[2], 19 | port: urlMatch[4], 20 | host: urlMatch[3], 21 | dialect: 'postgres', 22 | protocol: 'postgres', 23 | } 24 | 25 | module.exports = { 26 | development: { 27 | ...postgres, 28 | }, 29 | production: { 30 | ...postgres, 31 | dialectOptions: { 32 | ssl: true, 33 | }, 34 | }, 35 | } 36 | -------------------------------------------------------------------------------- /scripts/start-debug.js: -------------------------------------------------------------------------------- 1 | require('@babel/register') 2 | 3 | const cssRequireHook = require('css-modules-require-hook') 4 | 5 | cssRequireHook({ 6 | generateScopedName: '[path][name]__[local]--[hash:base64:5]', 7 | }) 8 | 9 | // Constants (typically compile-time string substituted) 10 | global.__BROWSER__ = false 11 | global.__SERVER__ = true 12 | 13 | require('../src/server/index') 14 | -------------------------------------------------------------------------------- /scripts/start-hot.js: -------------------------------------------------------------------------------- 1 | require('@babel/register') 2 | 3 | const webpack = require('webpack') 4 | 5 | const serverConfig = require('../webpack.config.babel').serverConfig 6 | 7 | const environment = process.env.NODE_ENV === 'development' ? {development: true} : {production: true} 8 | const config = serverConfig({...environment}) 9 | const bundlePath = `${config.output.path}/${config.output.filename}` 10 | 11 | const compiler = webpack(config) 12 | const compilerOptions = { 13 | aggregateTimeout: 300, 14 | poll: true, 15 | } 16 | 17 | let server = null 18 | 19 | const openSockets = new Map() 20 | let nextSocketId = 0 21 | 22 | function loadServer() { 23 | delete require.cache[require.resolve(bundlePath)] 24 | server = require(bundlePath).default 25 | 26 | server.on('connection', (socket) => { 27 | const socketId = nextSocketId++ 28 | openSockets.set(socketId, socket) 29 | 30 | socket.on('close', () => { 31 | openSockets.delete(socketId) 32 | }) 33 | }) 34 | 35 | } 36 | 37 | compiler.watch(compilerOptions, (err, stats) => { 38 | if (err) { 39 | console.log(`Server bundling error: ${JSON.stringify(err)}`) 40 | return 41 | } 42 | 43 | if (server) { 44 | for (const socket of openSockets.values()) { 45 | socket.destroy() 46 | } 47 | 48 | server.close(() => { 49 | loadServer() 50 | console.log('Server restarted') 51 | }) 52 | } else { 53 | loadServer() 54 | console.log("Server started.") 55 | } 56 | }) 57 | -------------------------------------------------------------------------------- /src/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "development": { 4 | "presets": [ 5 | [ 6 | "@babel/env", 7 | { 8 | "targets": { 9 | "node": "current" 10 | } 11 | } 12 | ] 13 | ] 14 | }, 15 | "production": { 16 | "presets": [ 17 | [ 18 | "@babel/env", 19 | { 20 | "modules": false 21 | } 22 | ] 23 | ] 24 | } 25 | }, 26 | "presets": [ 27 | "@babel/flow", 28 | "@babel/react" 29 | ], 30 | "plugins": [ 31 | "@babel/plugin-syntax-dynamic-import", 32 | "@babel/plugin-syntax-import-meta", 33 | "@babel/plugin-proposal-class-properties", 34 | "@babel/plugin-proposal-json-strings", 35 | "@babel/transform-runtime", 36 | "react-hot-loader/babel" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type { UserAction } from './user' 4 | 5 | export type Action = UserAction 6 | -------------------------------------------------------------------------------- /src/actions/user.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as api from '../api' 4 | 5 | import { AsyncStates } from '../api/types' 6 | 7 | 8 | import type { Dispatch } from '../store' 9 | 10 | import type { 11 | DoneAsyncState, 12 | FailedAsyncState, 13 | InProgressAsyncState, 14 | User, 15 | } from '../api/types' 16 | 17 | 18 | export const LOGGING_IN = Object.freeze({type: 'LOGGING_IN'}) 19 | 20 | export type LoggingIn = {| 21 | type: $Values, 22 | state: InProgressAsyncState, 23 | |} | {| 24 | type: $Values, 25 | state: DoneAsyncState, 26 | user: User, 27 | |} | {| 28 | type: $Values, 29 | state: FailedAsyncState, 30 | |} 31 | 32 | export const SET_USER = Object.freeze({type: 'SET_USER'}) 33 | 34 | export type SetUser = {| 35 | type: $Values, 36 | user: User, 37 | |} 38 | 39 | export function login(username: string, password: string) { 40 | return async function(dispatch: Dispatch): Promise { 41 | const inProgress: InProgressAsyncState = AsyncStates.inProgress() 42 | 43 | try { 44 | dispatch({type: LOGGING_IN.type, state: inProgress}) 45 | const user = await api.login(username, password) 46 | dispatch({type: LOGGING_IN.type, state: AsyncStates.done(inProgress), user}) 47 | return user 48 | } catch (e) { 49 | const message = e.name === api.DISPLAYABLE_ERROR ? e.message : null 50 | const failed: FailedAsyncState = AsyncStates.failed(inProgress, message) 51 | dispatch({type: LOGGING_IN.type, state: failed}) 52 | throw e 53 | } 54 | } 55 | } 56 | 57 | export function setUser(user: User): SetUser { 58 | return {type: SET_USER.type, user} 59 | } 60 | 61 | export type UserAction = LoggingIn | 62 | SetUser 63 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { Urls } from "./routes" 4 | 5 | 6 | import type { User } from './types' 7 | 8 | 9 | const HEADERS_SEND_RECEIVE_JSON = Object.freeze({ 10 | 'Accept': 'application/json', 11 | 'Content-Type': 'application/json', 12 | }) 13 | 14 | export const DISPLAYABLE_ERROR = 'au.com.glassechidna.DisplayableError' 15 | 16 | export class DisplayableError extends Error { 17 | constructor(message: string) { 18 | super(message) 19 | 20 | this.name = DISPLAYABLE_ERROR 21 | } 22 | } 23 | 24 | export const RESPONSE_ERROR = 'au.com.glassechidna.ResponseError' 25 | 26 | class ResponseError extends Error { 27 | response: any 28 | 29 | constructor(response: Response, message?: ?string) { 30 | super(message ? message : response.status + ": " + response.url) 31 | 32 | this.name = RESPONSE_ERROR 33 | this.response = response 34 | } 35 | } 36 | 37 | async function validateResponseOk(response, jsonErrorHandler: ?(Object) => ?boolean) { 38 | if (!response.ok) { 39 | let contentType = response.headers.get('Content-Type') 40 | 41 | if (contentType && contentType.startsWith('application/json')) { 42 | const data = await response.clone().json() 43 | 44 | if (!jsonErrorHandler || !jsonErrorHandler(data)) { 45 | if (data.reason) { 46 | throw new DisplayableError(data.reason) 47 | } else { 48 | throw new ResponseError(response) 49 | } 50 | } 51 | } else { 52 | throw new ResponseError(response) 53 | } 54 | } 55 | 56 | return true 57 | } 58 | 59 | export async function login(username: string, password: string): Promise { 60 | const response = await fetch(Urls.login(), { 61 | method: 'POST', 62 | headers: HEADERS_SEND_RECEIVE_JSON, 63 | body: JSON.stringify({username, password}), 64 | credentials: 'same-origin', 65 | }) 66 | 67 | await validateResponseOk(response) 68 | 69 | const data = await response.json() 70 | 71 | return { 72 | id: data.id, 73 | email: data.email, 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/api/routes.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export const Paths = Object.freeze({ 4 | login: () => '/login', 5 | }) 6 | 7 | // $FlowIssue: Spread is fine here, ignore flow. 8 | export const Urls = Object.freeze(Object.assign(...Object.entries(Paths).map(([name, path]) => ({ 9 | // $FlowIssue: Object.entries() presently drops type information 10 | [name]: () => global.__SERVER_HOST__ + path.apply(null, arguments), 11 | })))) 12 | -------------------------------------------------------------------------------- /src/api/types.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import getUuid from 'uuid/v4' 4 | 5 | export type UnixDate = number 6 | 7 | export const AsyncStatus = Object.freeze({ 8 | INITIAL: 'INITIAL', 9 | DONE: 'DONE', 10 | IN_PROGRESS: 'IN_PROGRESS', 11 | FAILED: 'FAILED', 12 | }) 13 | 14 | export type InitialAsyncState = {| 15 | status: 'INITIAL', 16 | |} 17 | 18 | export type DoneAsyncState = {| 19 | status: 'DONE', 20 | uuid: string, 21 | startTime: UnixDate, 22 | |} 23 | 24 | export type InProgressAsyncState = {| 25 | status: 'IN_PROGRESS', 26 | uuid: string, 27 | startTime: UnixDate, 28 | |} 29 | 30 | export type FailedAsyncState = {| 31 | status: 'FAILED', 32 | uuid: string, 33 | startTime: UnixDate, 34 | errorMessage?: string, 35 | |} 36 | 37 | export type AsyncState = InitialAsyncState | DoneAsyncState | InProgressAsyncState | FailedAsyncState 38 | export type ActiveAsyncState = DoneAsyncState | InProgressAsyncState | FailedAsyncState 39 | 40 | export class AsyncStates { 41 | static initial(): InitialAsyncState { 42 | return {status: AsyncStatus.INITIAL} 43 | } 44 | 45 | static inProgress(uuid: string = getUuid(), startTime: UnixDate = Date.now()): InProgressAsyncState { 46 | return {status: AsyncStatus.IN_PROGRESS, uuid, startTime} 47 | } 48 | 49 | static done(inProgressState: InProgressAsyncState): DoneAsyncState { 50 | const {uuid, startTime} = inProgressState 51 | return {status: AsyncStatus.DONE, uuid, startTime} 52 | } 53 | 54 | static failed(inProgressState: InProgressAsyncState, errorMessage?: ?string): FailedAsyncState { 55 | const {uuid, startTime} = inProgressState 56 | return errorMessage ? {status: AsyncStatus.FAILED, uuid, startTime, errorMessage} : { 57 | status: AsyncStatus.FAILED, 58 | uuid, 59 | startTime, 60 | } 61 | } 62 | 63 | static update(oldState: AsyncState, newState: ActiveAsyncState): AsyncState { 64 | return (newState.status === AsyncStatus.IN_PROGRESS || oldState.uuid === newState.uuid) ? newState : oldState 65 | } 66 | } 67 | 68 | export function isAsyncStateComplete(state : AsyncState) { 69 | return state.status === AsyncStatus.DONE || state.status === AsyncStatus.FAILED 70 | } 71 | 72 | export type User = { 73 | id: number, 74 | email: string, 75 | } 76 | -------------------------------------------------------------------------------- /src/app/components/Dashboard/Home/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React, { Component } from 'react' 4 | 5 | 6 | class Home extends Component<*> { 7 | render() { 8 | return ( 9 |

14 | ) 15 | } 16 | } 17 | 18 | export default Home 19 | -------------------------------------------------------------------------------- /src/app/components/Dashboard/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React, { Component } from 'react' 4 | 5 | import { 6 | Route as SwitchRoute, 7 | Switch, 8 | } from 'react-router-dom' 9 | 10 | 11 | import routes, { routeProps } from '../../../routes' 12 | 13 | import EnsureLoggedInContainer from '../Login/EnsureLoggedInContainer' 14 | 15 | 16 | import type { Location } from 'react-router' 17 | 18 | import type { Route } from '../../../routes' 19 | 20 | 21 | type Props = { 22 | location: Location, 23 | } 24 | 25 | class Dashboard extends Component { 26 | render() { 27 | return ( 28 | 29 | 30 | {Object.values(routes.DASHBOARD.subroutes).map(route => { 31 | const {path, ...otherProps} = routeProps(((route: $FlowIssue): Route)) 32 | return 33 | })} 34 | 35 | 36 | ) 37 | } 38 | } 39 | 40 | export default Dashboard 41 | -------------------------------------------------------------------------------- /src/app/components/Login/EnsureLoggedInContainer.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React, { Component } from 'react' 4 | 5 | import { connect } from 'react-redux' 6 | 7 | import { Redirect } from 'react-router' 8 | 9 | import routes from '../../../routes' 10 | 11 | 12 | import type { Node } from 'react' 13 | 14 | import type { AppState } from '../../../reducers' 15 | 16 | 17 | type Props = { 18 | children?: Node, 19 | } 20 | 21 | type StateProps = { 22 | isLoggedIn: boolean, 23 | } 24 | 25 | type InnerProps = { 26 | ...$Exact, 27 | ...$Exact, 28 | } 29 | 30 | class EnsureLoggedInContainer extends Component { 31 | render() { 32 | if (this.props.isLoggedIn) { 33 | return this.props.children 34 | } else { 35 | return 36 | } 37 | } 38 | } 39 | 40 | function mapStateToProps(state: AppState, ownProps: Props): InnerProps { 41 | return { 42 | ...ownProps, 43 | isLoggedIn: !!state.user.currentUser, 44 | } 45 | } 46 | 47 | export default connect(mapStateToProps)(EnsureLoggedInContainer) 48 | -------------------------------------------------------------------------------- /src/app/components/Login/LoginForm.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React, { Component } from 'react' 4 | 5 | import { connect } from 'react-redux' 6 | 7 | import { Redirect } from 'react-router' 8 | 9 | import { AsyncStatus } from '../../../api/types' 10 | 11 | import { login } from '../../../actions/user' 12 | 13 | import routes from '../../../routes' 14 | 15 | 16 | import type { AppState } from '../../../reducers' 17 | import type { Dispatch } from '../../../store' 18 | 19 | 20 | type Props = { 21 | } 22 | 23 | type StateProps = { 24 | ...$Exact, 25 | loggingIn: boolean, 26 | loggedIn: boolean, 27 | } 28 | 29 | type InnerProps = { 30 | ...$Exact, 31 | dispatch: Dispatch, 32 | } 33 | 34 | 35 | type State = {| 36 | email: string, 37 | password: string, 38 | |} 39 | 40 | class LoginForm extends Component { 41 | constructor(props: InnerProps) { 42 | super(props) 43 | this.state = {email: '', password: ''} 44 | } 45 | 46 | render() { 47 | if (this.props.loggedIn) { 48 | return () 49 | } 50 | 51 | return ( 52 |
53 | {!this.props.loggingIn && ( 54 |
55 |
56 | 62 |
63 |
64 | 70 |
71 | 72 |
73 | )} 74 |
75 | ) 76 | } 77 | 78 | _onLogin = (event) => { 79 | event.preventDefault() 80 | 81 | const {email, password} = this.state 82 | this.props.dispatch(login(email, password)) 83 | } 84 | } 85 | 86 | function mapStateToProps(state: AppState, ownProps: Props): StateProps { 87 | const loggingIn = state.user.loggingIn.status === AsyncStatus.IN_PROGRESS 88 | const loggedIn = !!state.user.currentUser 89 | 90 | return { 91 | ...ownProps, 92 | loggingIn, 93 | loggedIn, 94 | } 95 | } 96 | 97 | export default connect(mapStateToProps)(LoginForm) 98 | -------------------------------------------------------------------------------- /src/app/components/Login/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React, { Component } from 'react' 4 | 5 | import LoginForm from './LoginForm' 6 | 7 | class Login extends Component<*> { 8 | render() { 9 | return ( 10 |
11 | 12 |
13 | ) 14 | } 15 | } 16 | 17 | const styles = { 18 | app: {}, 19 | } 20 | 21 | export default Login 22 | -------------------------------------------------------------------------------- /src/app/index.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | border: 0; 5 | height: 100%; 6 | } 7 | 8 | .root { 9 | height: 100%; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React, { Component } from 'react' 4 | 5 | import { 6 | Route as SwitchRoute, 7 | Switch, 8 | } from 'react-router-dom' 9 | import { ConnectedRouter } from 'react-router-redux' 10 | 11 | import PropTypes from 'prop-types' 12 | 13 | import './index.css' 14 | 15 | import routes, { 16 | routeProps, 17 | } from '../routes' 18 | 19 | 20 | import type { Route } from '../routes' 21 | 22 | 23 | export type Props = { 24 | history: any, 25 | } 26 | 27 | class App extends Component { 28 | static childContextTypes = { 29 | router: PropTypes.object.isRequired, 30 | } 31 | 32 | getChildContext() { 33 | // staticContext is detected by react-router's `Redirect` component. It's existence causes the redirection to be performed in 34 | // componentWillMount() on the server, as componentDidMount() is never called on the server. 35 | return { 36 | ...this.context, 37 | router: { 38 | ...(global.__SERVER__ ? {staticContext: {}} : {}), 39 | }, 40 | } 41 | } 42 | 43 | componentDidMount() { 44 | const jssStyles = document.getElementById('preloaded-jss') 45 | 46 | if (jssStyles && jssStyles.parentNode) { 47 | jssStyles.parentNode.removeChild(jssStyles) 48 | } 49 | } 50 | 51 | render() { 52 | const history = this.props.history 53 | 54 | return ( 55 | 56 | 57 | {Object.values(routes).map(route => { 58 | const {path, ...otherProps} = routeProps(((route: $FlowIssue): Route)) 59 | return 60 | })} 61 | 62 | 63 | ) 64 | } 65 | } 66 | 67 | export default App 68 | -------------------------------------------------------------------------------- /src/browser/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from 'react' 4 | 5 | import { hydrate } from 'react-dom' 6 | 7 | import { AppContainer } from 'react-hot-loader' 8 | 9 | import { Provider } from 'react-redux' 10 | 11 | import createHistory from 'history/createBrowserHistory' 12 | 13 | import App from '../app' 14 | 15 | import createStore from '../store' 16 | 17 | // JSS 18 | 19 | import { create as createJss } from 'jss' 20 | import jssPreset from 'jss-preset-default' 21 | 22 | import JssProvider from 'react-jss/lib/JssProvider' 23 | 24 | 25 | const preloadedState = window.__PRELOADED_STATE__ 26 | delete window.__PRELOADED_STATE__ 27 | 28 | const history = createHistory() 29 | const store = createStore(history, preloadedState) 30 | 31 | function render(AppComponent: any) { 32 | const jss = createJss(jssPreset()) 33 | 34 | return ( 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ) 43 | } 44 | 45 | let rootElement = document.getElementById('root') 46 | 47 | if (rootElement) { 48 | hydrate(render(App), rootElement) 49 | } else { 50 | throw new Error('App root element not found') 51 | } 52 | 53 | if (module.hot) { 54 | // $FlowFixMe - module.hot is global so flow has a lil freak-out 55 | module.hot.accept('../app', () => { 56 | const nextApp = require('../app').default 57 | rootElement = document.getElementById('root') 58 | 59 | if (rootElement) { 60 | hydrate(render(App), rootElement) 61 | } else { 62 | throw new Error('App root element not found') 63 | } 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { 4 | combineReducers, 5 | } from 'redux' 6 | 7 | import { routerReducer as router } from 'react-router-redux' 8 | 9 | import user from './user' 10 | 11 | 12 | import type { Reducer } from 'redux' 13 | 14 | import type { Action } from '../actions' 15 | import type { UserState } from './user' 16 | 17 | export type AppState = { 18 | router: Object, 19 | user: UserState, 20 | } 21 | 22 | const appReducer: Reducer = combineReducers({ 23 | router, 24 | user, 25 | }) 26 | 27 | export default appReducer 28 | -------------------------------------------------------------------------------- /src/reducers/user.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { 4 | AsyncStates, 5 | } from '../api/types' 6 | 7 | import * as UserActions from '../actions/user' 8 | 9 | import type { 10 | AsyncState, 11 | User, 12 | } from '../api/types' 13 | 14 | import type { Action } from '../actions' 15 | 16 | export type UserState = { 17 | loggingIn: AsyncState, 18 | currentUser: ?User, 19 | } 20 | 21 | const initialState: UserState = { 22 | loggingIn: AsyncStates.initial(), 23 | currentUser: null, 24 | } 25 | 26 | const user = (state: UserState = initialState, action: Action): UserState => { 27 | switch (action.type) { 28 | case UserActions.LOGGING_IN.type: { 29 | const loggingIn = AsyncStates.update(state.loggingIn, action.state) 30 | 31 | if (loggingIn !== state.loggingIn) { 32 | const currentUser = action.user ? action.user : state.currentUser 33 | 34 | return { 35 | ...state, 36 | currentUser, 37 | loggingIn, 38 | } 39 | } 40 | 41 | break 42 | } 43 | 44 | case UserActions.SET_USER.type: { 45 | return { 46 | ...state, 47 | currentUser: action.user, 48 | } 49 | } 50 | } 51 | 52 | return state 53 | } 54 | 55 | export default user 56 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from 'react' 4 | 5 | import Home from '../app/components/Dashboard/Home' 6 | 7 | import Login from '../app/components/Login' 8 | import Dashboard from '../app/components/Dashboard' 9 | 10 | 11 | import type { Location } from 'react-router' 12 | 13 | 14 | import type { AppState } from '../reducers' 15 | 16 | 17 | type OptionalPathRoute = {| 18 | path?: (options? : Object) => string, 19 | exact?: boolean, 20 | strict?: boolean, 21 | sensitive?: boolean, 22 | component: React$ComponentType, 23 | icon?: ?React$ComponentType, 24 | title: (location : Location, state : ?AppState) => string, 25 | longTitle?: (location : Location, state : ?AppState) => string, 26 | subroutes?: { [string]: $Exact }, 27 | |} 28 | 29 | export type BaseRoute> = { 30 | path: (options? : Object) => string, 31 | exact?: boolean, 32 | strict?: boolean, 33 | sensitive?: boolean, 34 | component: React$ComponentType, 35 | icon?: ?React$ComponentType, 36 | title: (location : Location, state : ?AppState) => string, 37 | longTitle?: (location : Location, state : ?AppState) => string, 38 | subroutes?: RouteMap, 39 | } 40 | 41 | export type Route = {| 42 | ...$Exact>, 43 | |} 44 | 45 | export type RouteMap> = { [string]: $Exact } 46 | 47 | export type RouteOptions = { 48 | component: React$ComponentType<*>, 49 | path?: string, 50 | exact?: boolean, 51 | strict?: boolean, 52 | sensitive?: boolean 53 | } 54 | 55 | export function routeProps(route : BaseRoute<*>, options? : Object = {}) : RouteOptions { 56 | const {component, exact, path, sensitive, strict} = route 57 | 58 | const pathOptions : RouteOptions = { 59 | component 60 | } 61 | 62 | if (exact) { 63 | pathOptions.exact = exact 64 | } 65 | 66 | if (path) { 67 | pathOptions.path = path() 68 | } 69 | 70 | if (sensitive) { 71 | pathOptions.sensitive = sensitive 72 | } 73 | 74 | if (strict) { 75 | pathOptions.strict = strict 76 | } 77 | 78 | return pathOptions 79 | } 80 | 81 | export function flattenRoutes(routes: ?RouteMap): Route[] { 82 | const flatRoutes: Route[] = [] 83 | 84 | if (routes) { 85 | Object.values(routes).forEach(r => { 86 | const route = ((r: $FlowIssue): Route) 87 | flatRoutes.push(route) 88 | flatRoutes.push(...flattenRoutes(route.subroutes)) 89 | }) 90 | } 91 | 92 | return flatRoutes 93 | } 94 | 95 | function propagatePaths(tempRoutes: { [string]: $Exact }, pathPrefix : (options? : Object) => string = () => ''): RouteMap { 96 | const routes: RouteMap = {} 97 | 98 | Object.keys(tempRoutes).forEach(key => { 99 | const tempRoute = tempRoutes[key] 100 | const {path, exact, strict, sensitive, component, icon, title, longTitle, subroutes} = tempRoute 101 | const effectivePath = path ? path() : '' 102 | const effectivePathPrefix = pathPrefix() 103 | 104 | const propagatedPath = path ? ( 105 | effectivePathPrefix.endsWith('/') || effectivePath.startsWith('/') ? ( 106 | (options? : Object) => pathPrefix(options) + path(options) 107 | ) : ( 108 | (options? : Object) => pathPrefix(options) + '/' + path(options) 109 | ) 110 | ) : ( 111 | effectivePathPrefix.endsWith('/') || effectivePath.startsWith('/') ? ( 112 | (options? : Object) => pathPrefix(options) 113 | ) : ( 114 | (options? : Object) => pathPrefix(options) + '/' 115 | ) 116 | ) 117 | 118 | const propagatedSubroutes = subroutes ? propagatePaths(subroutes, propagatedPath) : undefined 119 | 120 | routes[key] = { 121 | exact, 122 | strict, 123 | sensitive, 124 | component, 125 | icon, 126 | title, 127 | longTitle, 128 | path: propagatedPath, 129 | subroutes: propagatedSubroutes, 130 | } 131 | }) 132 | 133 | return routes 134 | } 135 | 136 | export default propagatePaths({ 137 | LOGIN: { 138 | path: () => '/login', 139 | component: Login, 140 | title: () => 'Login', 141 | }, 142 | DASHBOARD: { 143 | path: () => '/', 144 | component: Dashboard, 145 | title: () => 'Dashboard', 146 | subroutes: { 147 | HOME: { 148 | component: Home, 149 | title: () => 'Dashboard', 150 | }, 151 | }, 152 | }, 153 | }) 154 | -------------------------------------------------------------------------------- /src/server/api/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { Router } from 'express' 4 | 5 | import { Paths } from '../../api/routes' 6 | 7 | import { authenticate } from '../authentication/passport' 8 | 9 | 10 | import type { 11 | $Request, 12 | $Response, 13 | } from 'express' 14 | 15 | 16 | const router = Router() 17 | 18 | router.post(Paths.login(), authenticate(), async (req: $Subtype<$Request>, res: $Response) => { 19 | const {id, email} = req.user 20 | res.json({id, email}) 21 | }) 22 | 23 | export default router 24 | -------------------------------------------------------------------------------- /src/server/authentication/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { compare as bcryptCompare } from 'bcrypt' 4 | 5 | import User from '../db/models/user' 6 | 7 | export default async function(email: String, password: String): Promise { 8 | const user = await User.findOne({where: {email: email}}) 9 | 10 | if (user) { 11 | if (await bcryptCompare(password, user.passwordHash)) { 12 | return user 13 | } 14 | } 15 | 16 | return null 17 | } 18 | -------------------------------------------------------------------------------- /src/server/authentication/passport.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import passportModule from 'passport' 4 | import LocalStrategy from 'passport-local' 5 | 6 | import User from '../db/models/user' 7 | 8 | import authenticateUser from '.' 9 | 10 | passportModule.use(new LocalStrategy( 11 | function(email, password, done) { 12 | authenticateUser(email, password).then(user => { 13 | if (user) { 14 | done(null, user) 15 | } else { 16 | done(null, false, {message: 'Invalid username or password.'}) 17 | } 18 | }).catch(e => { 19 | done(e) 20 | }) 21 | }, 22 | )) 23 | 24 | passportModule.serializeUser((user: User, done) => { 25 | done(null, user.id) 26 | }) 27 | 28 | passportModule.deserializeUser((id: number, done) => { 29 | User.findById(id).then(user => done(null, user)).catch(e => done(e, null)) 30 | }) 31 | 32 | export const passport = passportModule.initialize.bind(passportModule) 33 | export const session = passportModule.session.bind(passportModule) 34 | 35 | export function authenticate(options: Object = {}) { 36 | return passportModule.authenticate('local', options) 37 | } 38 | -------------------------------------------------------------------------------- /src/server/db/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import Sequelize from 'sequelize' 4 | 5 | const url = process.env.DATABASE_URL || '' 6 | 7 | const sequelize = new Sequelize(url, { 8 | dialect: 'postgres', 9 | protocol: 'postgres', 10 | operatorsAliases: false, 11 | dialectOptions: { 12 | ssl: process.env.NODE_ENV === 'production', 13 | }, 14 | }) 15 | 16 | export default sequelize 17 | -------------------------------------------------------------------------------- /src/server/db/migrations/20180312145544-create-user.js: -------------------------------------------------------------------------------- 1 | export default { 2 | up: (queryInterface, Sequelize) => { 3 | return queryInterface.createTable('Users', { 4 | id: { 5 | allowNull: false, 6 | autoIncrement: true, 7 | primaryKey: true, 8 | type: Sequelize.INTEGER, 9 | }, 10 | email: { 11 | type: Sequelize.STRING, 12 | allowNull: false, 13 | unique: true, 14 | }, 15 | passwordHash: { 16 | type: Sequelize.STRING, 17 | allowNull: false, 18 | }, 19 | createdAt: { 20 | allowNull: false, 21 | type: Sequelize.DATE, 22 | }, 23 | updatedAt: { 24 | allowNull: false, 25 | type: Sequelize.DATE, 26 | }, 27 | }) 28 | }, 29 | down: (queryInterface, Sequelize) => { 30 | return queryInterface.dropTable('Users') 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /src/server/db/models/index.js: -------------------------------------------------------------------------------- 1 | import User from './user' 2 | 3 | export { User } 4 | -------------------------------------------------------------------------------- /src/server/db/models/user/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { default as bcrypt } from 'bcrypt' 4 | 5 | import Sequelize from 'sequelize' 6 | 7 | import sequelize from '../../' 8 | 9 | const User = sequelize.define('User', { 10 | email: Sequelize.STRING, 11 | password: Sequelize.VIRTUAL, 12 | passwordHash: Sequelize.STRING, 13 | }) 14 | 15 | User.beforeCreate(async function(user, options) { 16 | user.passwordHash = await bcrypt.hash(user.password, 10) 17 | }) 18 | 19 | User.prototype.publicRepresentation = function() { 20 | const {id, email} = this 21 | return { 22 | id, 23 | email, 24 | } 25 | } 26 | 27 | export default User 28 | -------------------------------------------------------------------------------- /src/server/db/seeds/20180312165657-admin-user.js: -------------------------------------------------------------------------------- 1 | import User from '../models/user' 2 | 3 | export default { 4 | up: (queryInterface, Sequelize) => { 5 | return User.create({email: 'admin@localhost', password: 'practicalSSR'}) 6 | }, 7 | 8 | down: (queryInterface, Sequelize) => { 9 | return User.destroy({where: {email: 'admin@localhost'}}) 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /src/server/env.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | try { 4 | const dotenv = require('dotenv') 5 | dotenv.load() 6 | dotenv.load({path: `.env.${process.env.NODE_ENV || ''}`}) 7 | } catch (e) { 8 | /* Do nothing */ 9 | } 10 | 11 | if (process.env.NODE_ENV === 'development') { 12 | require('css.escape') 13 | } 14 | 15 | // Universal constants - these are compile-time constants for the browser bundle 16 | global.__SERVER_HOST__ = `${process.env.HOSTNAME || 'undefined'}:${process.env.PORT || 'undefined'}` 17 | -------------------------------------------------------------------------------- /src/server/hmr.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import webpack from 'webpack' 4 | import webpackDevMiddleware from 'webpack-dev-middleware' 5 | import webpackHotMiddleware from 'webpack-hot-middleware' 6 | 7 | import { browserConfig } from '../../webpack.config.babel' 8 | 9 | const config = browserConfig({development: true}) 10 | 11 | module.exports = (app: any) => { 12 | const compiler = webpack(config) 13 | 14 | app.use(webpackDevMiddleware(compiler, { 15 | noInfo: true, 16 | hot: true, 17 | publicPath: config.output.publicPath, 18 | })) 19 | 20 | app.use(webpackHotMiddleware(compiler, { 21 | log: console.log, 22 | reload: true, 23 | })) 24 | 25 | return app 26 | } 27 | -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import './env' 4 | 5 | 6 | import Express from 'express' 7 | 8 | import cookieSession from 'cookie-session' 9 | 10 | import { json as jsonParser, urlencoded as urlEncodingParser } from 'body-parser' 11 | 12 | import React from 'react' 13 | import { renderToString } from 'react-dom/server' 14 | 15 | import { AppContainer } from 'react-hot-loader' 16 | 17 | import { Provider } from 'react-redux' 18 | 19 | import { matchPath } from 'react-router' 20 | 21 | import serialize from 'serialize-javascript' 22 | 23 | import createHistory from 'history/createMemoryHistory' 24 | 25 | import createStore from '../store' 26 | 27 | import { create as createJss } from 'jss' 28 | import jssPreset from 'jss-preset-default' 29 | import JssProvider from 'react-jss/lib/JssProvider' 30 | import { SheetsRegistry } from 'react-jss/lib/jss' 31 | 32 | 33 | import styles from '../app/index.css' 34 | 35 | import './db' 36 | 37 | import App from '../app' 38 | 39 | import api from './api' // Non-react routes 40 | import routes, { flattenRoutes } from './routes' 41 | import { routeProps } from '../routes' 42 | 43 | import { passport, session } from './authentication/passport' 44 | 45 | 46 | import type { $Request, $Response, NextFunction } from 'express' 47 | 48 | import type { AppState } from '../reducers' 49 | import type { ServerRoute } from './routes' 50 | 51 | 52 | const PROD = process.env.NODE_ENV === 'production' 53 | 54 | const app = Express() 55 | const port = parseInt(process.env.PORT || 8080) 56 | 57 | const flattenedRoutes : ServerRoute[] = flattenRoutes(routes) 58 | 59 | function renderHtml(html: string, css: string, preloadedState: AppState, title = 'Glass Echidna React SSR Template') { 60 | return ` 61 | 62 | 63 | 64 | ${title} 65 | 66 | 67 | 68 |
${html}
69 | 70 | 73 | 74 | 75 | 76 | ` 77 | } 78 | 79 | function render(req: $Subtype<$Request>, res: $Response) { 80 | const history = createHistory({initialEntries: [req.path]}) 81 | const initialPath = history.location.pathname 82 | 83 | const store = createStore(history) 84 | 85 | const jss = createJss(jssPreset()) 86 | 87 | const sheetsRegistry = new SheetsRegistry() 88 | 89 | const matchedRoute = flattenedRoutes.find(route => matchPath(req.url, routeProps(route))) 90 | const promises = [] 91 | 92 | if (matchedRoute && matchedRoute.loadData) { 93 | promises.push(...matchedRoute.loadData.map(loadData => loadData(store.dispatch, req, res))) 94 | } 95 | 96 | return Promise.all(promises).then(() => { 97 | // Server's node instance can't reload files (HMR is for serving files to the client) so SSR is disabled during development to avoid getting out of sync. 98 | const html = renderToString( 99 | 100 | 101 | 102 | 103 | 104 | 105 | , 106 | ) 107 | 108 | // If we encountered redirect, our history will be pointing to another location, seems as this is SSR, we should redirect now. 109 | if (history.location.pathname !== initialPath) { 110 | res.redirect(302, history.location.pathname) 111 | } else { 112 | // Grab the initial state from our Redux store 113 | const preloadedState = store.getState() 114 | 115 | // Send the rendered page back to the client 116 | res.send(renderHtml(html, sheetsRegistry.toString(), preloadedState)) 117 | } 118 | }).catch(e => { 119 | console.error(e) 120 | }) 121 | } 122 | 123 | app.use(cookieSession({name: 'session', secret: process.env.COOKIE_SESSION_SECRET})) 124 | app.use(jsonParser()) 125 | app.use(urlEncodingParser({extended: true})) 126 | 127 | app.use(passport()) 128 | app.use(session()) 129 | 130 | if (PROD) { 131 | app.use('/static', Express.static('./dist/static')) 132 | app.use('/', api) 133 | app.get('*', render) 134 | } else { 135 | const HMR = require('./hmr.js') 136 | HMR(app) 137 | app.use('/', api) 138 | app.get('*', render) 139 | } 140 | 141 | const server = app.listen(port) 142 | 143 | export default server 144 | -------------------------------------------------------------------------------- /src/server/routes/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import routes from '../../routes' 4 | 5 | import { setUser } from '../../actions/user' 6 | import User from '../db/models/user' 7 | 8 | 9 | import type { $Request, $Response } from 'express' 10 | 11 | import type { Dispatch } from '../../store' 12 | 13 | import type { 14 | BaseRoute, 15 | RouteMap, 16 | } from '../../routes' 17 | 18 | 19 | type LoadData = ((any => boolean, $Request, $Response) => Promise<*>)[] 20 | 21 | export type ServerRoute = {| 22 | ...$Exact>, 23 | loadData?: LoadData 24 | |} 25 | 26 | async function setReduxUser(dispatch: Dispatch, req: $Subtype<$Request>, res: $Response) { 27 | if (req.user) { 28 | const user = ((req.user: any): ?User) 29 | 30 | if (user) { 31 | dispatch(setUser(user.publicRepresentation())) 32 | } 33 | } 34 | } 35 | 36 | const serverRoutes: RouteMap = { 37 | ...routes, 38 | LOGIN: { 39 | ...routes.LOGIN, 40 | loadData: [setReduxUser], 41 | }, 42 | DASHBOARD: { 43 | ...routes.DASHBOARD, 44 | loadData: [setReduxUser], 45 | }, 46 | } 47 | 48 | export function flattenRoutes(routes: ?RouteMap, parentLoadData: LoadData = []): ServerRoute[] { 49 | const flatRoutes: ServerRoute[] = [] 50 | 51 | if (routes) { 52 | Object.values(routes).forEach(r => { 53 | const route = ((r: any): ServerRoute) 54 | const loadData = route.loadData ? parentLoadData.concat(route.loadData) : parentLoadData 55 | 56 | // $FlowIssue: One day flow will get it together... one day... 57 | const newRoute: ServerRoute = { 58 | ...route, 59 | loadData, 60 | } 61 | 62 | flatRoutes.push(newRoute) 63 | flatRoutes.push(...flattenRoutes(route.subroutes, loadData)) 64 | }) 65 | } 66 | 67 | return flatRoutes 68 | } 69 | 70 | export default serverRoutes 71 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { 4 | applyMiddleware, 5 | createStore as reduxCreateStore, 6 | } from 'redux' 7 | 8 | import { 9 | routerMiddleware, 10 | } from 'react-router-redux' 11 | 12 | import thunk from 'redux-thunk' 13 | 14 | import appReducer from '../reducers' 15 | 16 | 17 | import type { Store } from 'redux' 18 | 19 | import type { Action } from '../actions' 20 | import type { AppState } from '../reducers' 21 | 22 | 23 | type PromiseAction = Promise 24 | type ThunkAction = (dispatch: Dispatch, getState: () => AppState) => any 25 | 26 | export type Dispatch = (action: Action | ThunkAction | PromiseAction) => any 27 | 28 | 29 | export default (history: any, preloadedState?: AppState) => { 30 | const middleware = [thunk, routerMiddleware(history)] 31 | 32 | if (process.env.NODE_ENV === 'development') { 33 | middleware.push(require('redux-logger').createLogger()) 34 | } 35 | 36 | const store: Store = reduxCreateStore(appReducer, preloadedState, applyMiddleware(...middleware)) 37 | 38 | if (module.hot) { 39 | // $FlowFixMe - module.hot is global so flow has a lil freak-out 40 | module.hot.accept('../reducers', () => { 41 | const nextAppReducer = require('../reducers').default 42 | store.replaceReducer(nextAppReducer) 43 | }) 44 | } 45 | 46 | 47 | return store 48 | } 49 | -------------------------------------------------------------------------------- /webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import path from 'path' 4 | 5 | import { 6 | DefinePlugin, 7 | HotModuleReplacementPlugin, 8 | IgnorePlugin, 9 | optimize, 10 | } from 'webpack' 11 | 12 | import ExtractTextPlugin from 'extract-text-webpack-plugin' 13 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer' 14 | 15 | import webpackNodeExternals from 'webpack-node-externals' 16 | 17 | const SERVER_HOST = process.env.SERVER_HOST || 'http://localhost:8080' 18 | 19 | const fileExtensions = { 20 | fonts: [ 21 | 'eot', 22 | 'otf', 23 | 'ttf', 24 | 'woff', 25 | 'woff2', 26 | ], 27 | images: [ 28 | 'gif', 29 | 'jpeg', 30 | 'jpg', 31 | 'png', 32 | 'svg', 33 | ], 34 | styleSheets: ['css'], 35 | } 36 | 37 | const commonRules = [ 38 | { 39 | test: /.js$/, 40 | exclude: [/node_modules/], 41 | use: [ 42 | { 43 | loader: 'babel-loader', 44 | options: { 45 | cacheDirectory: true, 46 | presets: [ 47 | '@babel/env', 48 | '@babel/flow', 49 | '@babel/react', 50 | ], 51 | plugins: [ 52 | '@babel/plugin-syntax-dynamic-import', 53 | '@babel/plugin-syntax-import-meta', 54 | '@babel/plugin-proposal-class-properties', 55 | '@babel/plugin-proposal-json-strings', 56 | '@babel/transform-runtime', 57 | 'react-hot-loader/babel', 58 | ], 59 | }, 60 | }, 61 | ], 62 | }, 63 | { 64 | test: /.js$/, 65 | include: [/node_modules\/rc/], 66 | use: [ 67 | { 68 | loader: 'shebang-loader', 69 | }, 70 | ], 71 | }, 72 | ] 73 | 74 | export const serverConfig = (env: Object, argv?: any[]) => { 75 | env = env || {} 76 | 77 | return { 78 | mode: env.production ? 'production' : 'development', 79 | entry: './src/server/index.js', 80 | output: { 81 | path: path.resolve(__dirname, 'dist'), 82 | filename: 'server.js', 83 | libraryTarget: 'umd', 84 | }, 85 | plugins: [ 86 | new DefinePlugin({ 87 | 'global.__BROWSER__': JSON.stringify(false), 88 | 'global.__SERVER__': JSON.stringify(true), 89 | }), 90 | ], 91 | target: 'node', 92 | module: { 93 | rules: [ 94 | ...commonRules, 95 | { 96 | test: new RegExp(`\.(${[...fileExtensions.fonts, ...fileExtensions.images].join('|')})$`), 97 | use: [ 98 | { 99 | loader: 'null', 100 | }, 101 | ], 102 | }, 103 | { 104 | test: new RegExp(`\.(${fileExtensions.styleSheets.join('|')})$`), 105 | use: [ 106 | { 107 | loader: 'css-loader/locals', 108 | options: { 109 | modules: true, 110 | localIdentName: '[path][name]__[local]--[hash:base64:5]', 111 | }, 112 | }, 113 | ], 114 | }, 115 | ], 116 | }, 117 | externals: [webpackNodeExternals({ 118 | whitelist: [ 119 | function(module) { 120 | return /\.(?!(?:jsx?|json)$).{1,5}$/i.exec(require.resolve(module)) 121 | }, 122 | ], 123 | })], 124 | devtool: env.production ? 'source-maps' : 'eval-source-map', 125 | devServer: { 126 | contentBase: path.join(__dirname, 'dist'), 127 | }, 128 | } 129 | } 130 | 131 | export const browserConfig = (env: Object, argv?: any[]) => { 132 | env = env || {} 133 | 134 | return { 135 | mode: env.production ? 'production' : 'development', 136 | entry: [ 137 | ...(env.production ? [] : [ 138 | 'react-hot-loader/patch', 139 | 'webpack-hot-middleware/client?noInfo=false', 140 | ]), 141 | './src/browser/index.js', 142 | ], 143 | output: { 144 | path: path.resolve(__dirname, 'dist', 'static'), 145 | filename: 'bundle.js', 146 | publicPath: '/static/', 147 | ...(env.production ? {} : { 148 | crossOriginLoading: 'anonymous', 149 | }), 150 | }, 151 | plugins: [ 152 | new DefinePlugin({ 153 | 'global.__BROWSER__': JSON.stringify(true), 154 | 'global.__SERVER__': JSON.stringify(false), 155 | 'global.__SERVER_HOST__': JSON.stringify(SERVER_HOST), 156 | 'process.env.NODE_ENV': JSON.stringify(env.production ? 'production' : 'development'), 157 | ...(env.production ? {'module.hot': JSON.stringify(false)} : {}), 158 | }), 159 | new ExtractTextPlugin('styles.css'), 160 | ...(env.production ? [] : [ 161 | new HotModuleReplacementPlugin(), 162 | ]), 163 | ...(env.production ? [ 164 | new optimize.AggressiveMergingPlugin(), 165 | ] : []), 166 | ...(env.analyze ? [ 167 | new BundleAnalyzerPlugin(), 168 | ] : []), 169 | ], 170 | module: { 171 | rules: [ 172 | ...commonRules, 173 | { 174 | test: new RegExp(`\.(${[...fileExtensions.fonts, ...fileExtensions.images].join('|')})$`), 175 | use: [ 176 | { 177 | loader: 'url-loader', 178 | options: { 179 | limit: 8192, 180 | fallback: 'file-loader', 181 | }, 182 | }, 183 | ], 184 | }, 185 | { 186 | test: new RegExp(`\.(${fileExtensions.styleSheets.join('|')})$`), 187 | loaders: ExtractTextPlugin.extract({ 188 | fallback: 'style-loader', 189 | use: [ 190 | { 191 | loader: 'css-loader', 192 | options: { 193 | modules: true, 194 | localIdentName: '[path][name]__[local]--[hash:base64:5]', 195 | }, 196 | }, 197 | ], 198 | }), 199 | }, 200 | ], 201 | }, 202 | devtool: env.production ? 'source-maps' : 'cheap-module-source-map', 203 | devServer: { 204 | contentBase: path.join(__dirname, 'dist', 'static'), 205 | }, 206 | } 207 | } 208 | 209 | export default [serverConfig, browserConfig] 210 | --------------------------------------------------------------------------------