├── api ├── .nvmrc ├── .eslintrc ├── config │ ├── billing.js │ ├── email.js │ ├── server.js │ ├── index.js │ ├── database.js │ └── auth.js ├── utils │ ├── error-utils.js │ ├── email-utils.js │ ├── user-utils.js │ └── validation-utils.js ├── Dockerfile ├── routes │ ├── index.js │ ├── billing.js │ ├── user.js │ └── auth.js ├── index.js ├── constants.js ├── package.json ├── controllers │ ├── user.js │ ├── billing.js │ └── auth.js ├── models │ └── user.js └── test │ └── user.test.js ├── app ├── .nvmrc ├── src │ ├── constants │ │ ├── ui-constants.js │ │ └── key-constants.js │ ├── components │ │ ├── authentication │ │ │ ├── authentication.scss │ │ │ ├── forgot-password.js │ │ │ ├── reset-password.js │ │ │ ├── login.js │ │ │ └── register.js │ │ ├── general │ │ │ ├── card.scss │ │ │ ├── card.js │ │ │ ├── modal.scss │ │ │ └── modal.js │ │ ├── form-fields │ │ │ ├── checkbox.js │ │ │ ├── text-area.js │ │ │ ├── text-input.js │ │ │ ├── select.js │ │ │ └── generic-form.js │ │ ├── notification │ │ │ └── alert.js │ │ ├── hoc │ │ │ └── require-auth.js │ │ ├── billing │ │ │ └── credit-card-fields.js │ │ └── header │ │ │ └── header.js │ ├── routes │ │ ├── authenticated │ │ │ └── index.js │ │ └── index.js │ ├── assets │ │ └── stylesheets │ │ │ ├── partials │ │ │ ├── _variables.scss │ │ │ ├── _mixins.scss │ │ │ ├── _state.scss │ │ │ ├── _grid.scss │ │ │ ├── _buttons.scss │ │ │ ├── _header.scss │ │ │ └── _forms.scss │ │ │ └── base.scss │ ├── util │ │ ├── redux-constants.js │ │ ├── proptype-utils.js │ │ ├── cookie-utils.js │ │ ├── environment-utils.js │ │ ├── i18n.js │ │ ├── store-utils.js │ │ └── http-utils.js │ ├── index.html │ ├── redux │ │ ├── index.js │ │ └── modules │ │ │ ├── billing.js │ │ │ ├── user.js │ │ │ └── authentication.js │ ├── index.js │ └── i18n.js ├── .babelrc ├── .eslintrc ├── webpack.config.js └── package.json ├── .vscode └── launch.json ├── .gitignore └── README.md /api/.nvmrc: -------------------------------------------------------------------------------- 1 | 8.1.4 2 | -------------------------------------------------------------------------------- /app/.nvmrc: -------------------------------------------------------------------------------- 1 | 8.1.4 2 | -------------------------------------------------------------------------------- /app/src/constants/ui-constants.js: -------------------------------------------------------------------------------- 1 | export const mobileBreakpoint = 766; 2 | -------------------------------------------------------------------------------- /app/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "react" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /app/src/components/authentication/authentication.scss: -------------------------------------------------------------------------------- 1 | .auth-box { 2 | width: 425px; 3 | margin: 0 auto; 4 | } -------------------------------------------------------------------------------- /app/src/constants/key-constants.js: -------------------------------------------------------------------------------- 1 | // Demo test Stripe publishable key (replace with your own) 2 | export const STRIPE_PUBLIC_KEY = 'pk_test_6pRNASCoBOKtIshFeQd4XMUh'; 3 | -------------------------------------------------------------------------------- /api/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "es6": true, 8 | "jest": true 9 | }, 10 | "rules": { 11 | "no-underscore-dangle": ["error", { "allow": ["_id"] }], 12 | "jsx-a11y/href-no-hash": "off", 13 | "no-console": "off" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /api/config/billing.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | // Default to dev presets 3 | const billingConfig = { 4 | stripeApiKey: 'your-key', 5 | }; 6 | 7 | switch (process.env.NODE_ENV) { 8 | case 'production': 9 | break; 10 | case 'stage': 11 | break; 12 | case 'dev': 13 | default: 14 | break; 15 | } 16 | 17 | return billingConfig; 18 | }; 19 | -------------------------------------------------------------------------------- /api/utils/error-utils.js: -------------------------------------------------------------------------------- 1 | const errorHandler = async (ctx, next) => { 2 | try { 3 | await next(); 4 | } catch (err) { 5 | ctx.status = err.status || 500; 6 | ctx.body = err.message; 7 | ctx.app.emit('error', err, ctx); 8 | } 9 | }; 10 | 11 | const errorLogger = (err) => { 12 | console.error(err); 13 | }; 14 | 15 | module.exports = { 16 | errorHandler, 17 | errorLogger, 18 | }; 19 | -------------------------------------------------------------------------------- /api/config/email.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | // Default to dev presets 3 | const emailConfig = { 4 | apiKey: 'key-xxxx', 5 | domain: 'mg.yourdomain.com', 6 | }; 7 | 8 | switch (process.env.NODE_ENV) { 9 | case 'production': 10 | break; 11 | case 'stage': 12 | break; 13 | case 'dev': 14 | default: 15 | break; 16 | } 17 | 18 | return emailConfig; 19 | }; 20 | -------------------------------------------------------------------------------- /app/src/routes/authenticated/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Switch } from 'react-router-dom'; 3 | import { Translate } from 'react-i18nify'; 4 | 5 | const AuthenticatedRoutes = () => ( 6 | 7 |
} /> 8 |
9 | ); 10 | 11 | export default AuthenticatedRoutes; 12 | -------------------------------------------------------------------------------- /app/src/assets/stylesheets/partials/_variables.scss: -------------------------------------------------------------------------------- 1 | // Colors 2 | $primary-color: #0474D8; 3 | $secondary-color: #363636; 4 | $gray-border: #dbdbdb; 5 | $error-color: #f96161; 6 | 7 | // Forms 8 | $input-color: #000; 9 | $input-bg: #FFF; 10 | $input-border: #CCC; 11 | $focus-color: #79b8f5; 12 | $border-radius: 3px; 13 | $unit: 8px; 14 | 15 | // Header 16 | $header-height: 60px; 17 | 18 | // Spacing 19 | $wrapper-horiz-pad: 10px; -------------------------------------------------------------------------------- /app/src/util/redux-constants.js: -------------------------------------------------------------------------------- 1 | // Namespace for application 2 | export const APP_NAMESPACE = 'mkrn-starter'; 3 | 4 | // Request status indicators 5 | export const PENDING = 'PENDING'; 6 | export const SUCCESS = 'SUCCESS'; 7 | export const ERROR = 'ERROR'; 8 | 9 | // Request types (lowercase for easy axios access) 10 | export const DELETE = 'delete'; 11 | export const GET = 'get'; 12 | export const POST = 'post'; 13 | export const PUT = 'put'; 14 | -------------------------------------------------------------------------------- /api/config/server.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | // Default to dev presets 3 | const serverConfig = { 4 | port: 3000, 5 | }; 6 | 7 | switch (process.env.NODE_ENV) { 8 | case 'production': 9 | break; 10 | case 'stage': 11 | break; 12 | case 'test': 13 | Object.assign(serverConfig, { port: 3001 }); 14 | break; 15 | case 'dev': 16 | default: 17 | break; 18 | } 19 | 20 | return serverConfig; 21 | }; 22 | -------------------------------------------------------------------------------- /app/src/components/general/card.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | background-color: white; 3 | -webkit-box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1); 4 | box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1); 5 | max-width: 100%; 6 | position: relative; 7 | 8 | .card- { 9 | &content { 10 | padding: 1.5rem; 11 | } 12 | 13 | &media { 14 | display: block; 15 | position: relative; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | # configure container 2 | FROM node:8 3 | 4 | # install and update system dependencies 5 | RUN apt-get update 6 | RUN apt-get install -y zip curl vim 7 | RUN npm install -g pm2 8 | 9 | # create api directory 10 | RUN mkdir -p /www/mkrn-starter-apipm2 11 | WORKDIR /www/mkrn-starter-api 12 | COPY . /www/mkrn-starter-api 13 | 14 | # install application 15 | RUN yarn install 16 | 17 | # add source 18 | EXPOSE 3000 19 | CMD ["pm2", "start", "--no-daemon", "index.js"] 20 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${file}" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /api/routes/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const combineRouters = require('koa-combine-routers'); 4 | 5 | const baseName = path.basename(module.filename); 6 | 7 | const routes = fs 8 | .readdirSync(path.join(__dirname)) 9 | .filter(file => (file.indexOf('.') !== 0) && (file !== baseName) && (file.slice(-3) === '.js')) 10 | .map(file => require(path.join(__dirname, file))); 11 | 12 | const rootRouter = combineRouters(routes); 13 | 14 | module.exports = rootRouter; 15 | -------------------------------------------------------------------------------- /api/config/index.js: -------------------------------------------------------------------------------- 1 | const serverConfig = require('./server')(); 2 | const databaseConfig = require('./database')(); 3 | const emailConfig = require('./email')(); 4 | const authConfig = require('./auth'); 5 | const billingConfig = require('./billing'); 6 | 7 | module.exports = { 8 | server: serverConfig, 9 | database: databaseConfig, 10 | passport: authConfig.passport(), 11 | email: emailConfig, 12 | auth: { 13 | secret: authConfig.opts.secret, 14 | jwtExpiration: authConfig.opts.expiration, 15 | }, 16 | billing: billingConfig, 17 | }; 18 | -------------------------------------------------------------------------------- /app/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "es6": true, 8 | "jest": true 9 | }, 10 | "rules": { 11 | "jsx-a11y/href-no-hash": "off", 12 | "no-underscore-dangle": ["error", { "allow": ["_id"] }], 13 | "no-console": "off", 14 | "no-param-reassign": "off", 15 | "no-script-url": "off", 16 | "react/jsx-filename-extension": "off", 17 | "react/require-default-props": "off" 18 | }, 19 | "globals": { 20 | "Stripe": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/util/proptype-utils.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | export const errorPropTypes = PropTypes.arrayOf(PropTypes.shape({ 4 | error: PropTypes.string, 5 | })); 6 | 7 | export const fieldPropTypes = { 8 | input: PropTypes.shape({ 9 | name: PropTypes.string, 10 | value: PropTypes.string, 11 | }), 12 | meta: PropTypes.shape({ 13 | error: PropTypes.string, 14 | }), 15 | id: PropTypes.string, 16 | placeholder: PropTypes.string, 17 | type: PropTypes.string, 18 | extraClasses: PropTypes.string, 19 | label: PropTypes.string, 20 | }; 21 | -------------------------------------------------------------------------------- /api/routes/billing.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router'); 2 | const billingControllers = require('../controllers/billing'); 3 | const authControllers = require('../controllers/auth'); 4 | 5 | const { 6 | stripeWebhook, 7 | createSubscription, 8 | createCustomer, 9 | } = billingControllers; 10 | 11 | const { 12 | jwtAuth, 13 | } = authControllers; 14 | 15 | const router = new Router({ prefix: '/billing' }); 16 | 17 | router.post('/webhook', stripeWebhook); 18 | router.post('/subscription', jwtAuth, createSubscription, createCustomer); 19 | 20 | module.exports = router; 21 | -------------------------------------------------------------------------------- /app/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MKRN Starter 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /api/routes/user.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router'); 2 | const userControllers = require('../controllers/user'); 3 | const authControllers = require('../controllers/auth'); 4 | 5 | const { 6 | jwtAuth, 7 | } = authControllers; 8 | 9 | const { 10 | getUser, 11 | getUsers, 12 | deleteUser, 13 | editUser, 14 | } = userControllers; 15 | 16 | const router = new Router({ prefix: '/user' }); 17 | 18 | router.get('/', jwtAuth, getUsers); 19 | router.get('/:id', jwtAuth, getUser); 20 | router.delete('/:id', jwtAuth, deleteUser); 21 | router.put('/:id', jwtAuth, editUser); 22 | 23 | module.exports = router; 24 | -------------------------------------------------------------------------------- /app/src/components/form-fields/checkbox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { fieldPropTypes } from '../../util/proptype-utils'; 3 | 4 | const Checkbox = ({ input, meta, id, label = '', extraClasses = '' }) => ( 5 |
6 | 7 | 8 | {meta.touched && meta.error &&
{meta.error}
} 9 |
10 | ); 11 | 12 | Checkbox.propTypes = fieldPropTypes; 13 | 14 | export default Checkbox; 15 | -------------------------------------------------------------------------------- /api/routes/auth.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router'); 2 | const authControllers = require('../controllers/auth'); 3 | 4 | const { 5 | jwtAuth, 6 | login, 7 | register, 8 | forgotPassword, 9 | resetPassword, 10 | getAuthenticatedUser, 11 | } = authControllers; 12 | 13 | const router = new Router({ prefix: '/auth' }); 14 | 15 | router.post('/register', register); 16 | router.post('/login', login); 17 | router.post('/forgot-password', forgotPassword); 18 | router.post('/reset-password/:resetToken', resetPassword); 19 | router.get('/profile', jwtAuth, getAuthenticatedUser); 20 | 21 | module.exports = router; 22 | -------------------------------------------------------------------------------- /api/config/database.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | // Default to dev presets 3 | const dbConfig = { 4 | url: 'mongodb://localhost:27017/dev', 5 | opts: { 6 | useNewUrlParser: true, 7 | useUnifiedTopology: true, 8 | keepAlive: 300000, 9 | }, 10 | }; 11 | 12 | switch (process.env.NODE_ENV) { 13 | case 'production': 14 | break; 15 | case 'stage': 16 | break; 17 | case 'test': 18 | Object.assign(dbConfig, { url: 'mongodb://localhost:27017/test' }); 19 | break; 20 | case 'dev': 21 | default: 22 | break; 23 | } 24 | 25 | return dbConfig; 26 | }; 27 | -------------------------------------------------------------------------------- /api/utils/email-utils.js: -------------------------------------------------------------------------------- 1 | const config = require('../config'); 2 | const mailgun = require('mailgun-js')(config.email); 3 | 4 | exports.sendEmail = (recipient, message, attachment) => 5 | new Promise((resolve, reject) => { 6 | const data = { 7 | from: 'Your Site ', 8 | to: recipient, 9 | subject: message.subject, 10 | text: message.text, 11 | inline: attachment, 12 | html: message.html, 13 | }; 14 | 15 | mailgun.messages().send(data, (error) => { 16 | if (error) { 17 | return reject(error); 18 | } 19 | return resolve(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /app/src/assets/stylesheets/partials/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin font-face($font-family, $size, $weight: null) { 2 | font-family: $font-family; 3 | font-size: $size; 4 | font-weight: $weight; 5 | } 6 | 7 | @mixin breakpoint($point) { 8 | @if $point == small { 9 | @media (max-width: 30em) { @content; } 10 | } 11 | @else if $point == medium { 12 | @media (min-width: 30em) and (max-width: 50em) { @content; } 13 | } 14 | @else if $point == large { 15 | @media (min-width: 50em) and (max-width: 68.75em) { @content; } 16 | } 17 | } 18 | 19 | @mixin clearfix() { 20 | &::after { 21 | content: ""; 22 | display: table; 23 | clear: both; 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/redux/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, combineReducers } from 'redux'; 2 | import { reducer as formReducer } from 'redux-form'; 3 | import reduxThunk from 'redux-thunk'; 4 | import userReducer from './modules/user'; 5 | import authenticationReducer from './modules/authentication'; 6 | 7 | const createStoreWithMiddleware = applyMiddleware(reduxThunk)(createStore); 8 | 9 | const rootReducer = combineReducers({ 10 | authentication: authenticationReducer, 11 | user: userReducer, 12 | form: formReducer 13 | }); 14 | 15 | const configureStore = initialState => createStoreWithMiddleware(rootReducer, initialState); 16 | export default configureStore; 17 | -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa'); 2 | const mongoose = require('mongoose'); 3 | const logger = require('koa-logger'); 4 | const cors = require('kcors'); 5 | const bodyParser = require('koa-bodyparser'); 6 | const routes = require('./routes'); 7 | const config = require('./config'); 8 | 9 | // Make mongoose use native ES6 promises 10 | mongoose.Promise = global.Promise; 11 | 12 | // Connect to MongoDB 13 | mongoose.connect(config.database.url, config.database.opts); 14 | 15 | const app = new Koa() 16 | .use(cors()) 17 | .use(logger()) 18 | .use(bodyParser()) 19 | .use(routes); 20 | 21 | const server = app.listen(config.server.port); 22 | 23 | module.exports = server; 24 | -------------------------------------------------------------------------------- /app/src/components/form-fields/text-area.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { fieldPropTypes } from '../../util/proptype-utils'; 3 | 4 | const Textarea = ({ input, meta, id, placeholder, type, label = '', extraClasses = '' }) => ( 5 |