├── app
├── client
│ ├── styles
│ │ ├── login.less
│ │ ├── style.less
│ │ └── base.less
│ ├── public
│ │ └── img
│ │ │ ├── favicon.ico
│ │ │ ├── favicon-16x16.png
│ │ │ ├── favicon-32x32.png
│ │ │ ├── favicon-96x96.png
│ │ │ └── favicon-192x192.png
│ ├── js
│ │ ├── index.js
│ │ ├── lib
│ │ │ └── jquery.js
│ │ ├── account
│ │ │ ├── index.js
│ │ │ └── pw-change.js
│ │ ├── user.js
│ │ └── login.js
│ ├── login.html
│ └── index.html
├── README.md
├── server
│ ├── db.js
│ ├── redis.js
│ ├── config.js
│ ├── index.js
│ └── user.js
├── server.config.js
├── package.json
└── webpack.config.js
├── .gitignore
├── preview.png
├── docker
├── node
│ └── Dockerfile
└── db
│ ├── Dockerfile
│ ├── data
│ └── structure.sql
│ └── my.cnf
├── .env
├── docker-compose.yml
└── README.md
/app/client/styles/login.less:
--------------------------------------------------------------------------------
1 | @import 'base.less';
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /db/my.cnf
2 | .DS_Store
3 | node_modules
4 | .cache
5 | dist
--------------------------------------------------------------------------------
/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spider-yamet/docker-node-app/HEAD/preview.png
--------------------------------------------------------------------------------
/app/client/public/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spider-yamet/docker-node-app/HEAD/app/client/public/img/favicon.ico
--------------------------------------------------------------------------------
/app/README.md:
--------------------------------------------------------------------------------
1 | Node App
2 | ===================
3 |
4 | Documentation needed (how server/client work together, what /public is for, etc)
--------------------------------------------------------------------------------
/app/client/public/img/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spider-yamet/docker-node-app/HEAD/app/client/public/img/favicon-16x16.png
--------------------------------------------------------------------------------
/app/client/public/img/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spider-yamet/docker-node-app/HEAD/app/client/public/img/favicon-32x32.png
--------------------------------------------------------------------------------
/app/client/public/img/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spider-yamet/docker-node-app/HEAD/app/client/public/img/favicon-96x96.png
--------------------------------------------------------------------------------
/app/client/public/img/favicon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spider-yamet/docker-node-app/HEAD/app/client/public/img/favicon-192x192.png
--------------------------------------------------------------------------------
/docker/node/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:10
2 |
3 | ENV NPM_CONFIG_LOGLEVEL info
4 |
5 | EXPOSE 80
6 |
7 | WORKDIR /home/app
8 |
9 | RUN npm install pm2 -g
10 |
11 | CMD ["npm", "start"]
12 |
--------------------------------------------------------------------------------
/docker/db/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mysql/mysql-server:5.7
2 |
3 | # install `pv` (Pipe Viewer) for the sync script
4 | RUN yum install -y http://www.ivarch.com/programs/rpms/pv-1.6.6-1.x86_64.rpm
5 |
6 | WORKDIR /home
--------------------------------------------------------------------------------
/app/server/db.js:
--------------------------------------------------------------------------------
1 | const config = require('./config');
2 | const DB = require('bui-server/db')
3 | const Model = require('bui-server/model')
4 |
5 | const db = new DB(config.mysql)
6 |
7 | Model.setDB(db)
8 |
9 | module.exports = db
--------------------------------------------------------------------------------
/app/client/styles/style.less:
--------------------------------------------------------------------------------
1 | @import 'base.less';
2 |
3 | body {
4 | display: flex;
5 | flex-direction: column;
6 | justify-content: center;
7 | align-items: center;
8 | }
9 |
10 | h1 {
11 | margin-bottom: 0em;
12 | }
--------------------------------------------------------------------------------
/app/server/redis.js:
--------------------------------------------------------------------------------
1 | const redis = require("redis")
2 |
3 | let client = redis.createClient({
4 | host: process.env.REDIS_HOST,
5 | port: 6379
6 | })
7 |
8 | client.unref()
9 | client.on('error', console.log)
10 |
11 | module.exports = client
--------------------------------------------------------------------------------
/app/server/config.js:
--------------------------------------------------------------------------------
1 | let env = process.env
2 |
3 | module.exports = {
4 |
5 | redis: {
6 | port: env.REDIS_PORT,
7 | host: env.REDIS_HOST,
8 | },
9 |
10 | mysql: {
11 | host : env.MYSQL_HOST,
12 | user : env.MYSQL_USER,
13 | password : env.MYSQL_PW,
14 | database : env.MYSQL_DATABASE
15 | }
16 | }
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | # Change the port(s) if it conflicts with one of your existing services
2 | APP_PORT=8080
3 | DEBUG_PORT=9229
4 | MYSQL_PORT=3310
5 | SESSION_SECRET=alw2h8h42iuS&34nj2SE23rfa
6 |
7 | # You shouldn't need to change any of these, they are used inside the docker containers
8 | MYSQL_HOST=db
9 | MYSQL_USER=root
10 | MYSQL_PW=password
11 | MYSQL_DATABASE=nodeapp
12 |
13 | MYSQL_ROOT_HOST=%
14 | MYSQL_ROOT_PASSWORD=password
15 |
16 | REDIS_HOST=redis
17 | REDIS_PORT=6379
18 |
19 | USER=docker
--------------------------------------------------------------------------------
/app/client/js/index.js:
--------------------------------------------------------------------------------
1 | import '../styles/style.less'
2 |
3 | import user from './user'
4 | import router from 'bui/router'
5 | import 'bui/helpers/backbone'
6 | import 'bui/helpers/lit-element'
7 | import 'form/backbone-ext'
8 | import 'bui/elements/btn'
9 | import './account'
10 |
11 | globalThis.goTo = (path,props)=>{
12 | router.goTo(path, props)
13 | }
14 |
15 | globalThis.logout = ()=>{
16 | user.logout()
17 | }
18 |
19 | router.start()
20 |
21 | window.addEventListener('DOMContentLoaded', e=>{
22 | document.body.classList.add('show')
23 | })
--------------------------------------------------------------------------------
/app/server.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | apps : [{
3 | name: "server",
4 | script: "./server/index.js",
5 | instances: 1,
6 | exec_mode: 'fork',
7 | watch: [
8 | './server'
9 | ],
10 | ignore_watch: [
11 | "./**/*.git",
12 | "./**/*.md"
13 | ],
14 | autorestart: true,
15 | restart_delay: 1000,
16 | node_args: '--inspect=0.0.0.0',
17 | env: {
18 | "NODE_ENV": "development",
19 | },
20 | env_production : {
21 | "NODE_ENV": "production"
22 | }
23 | }]
24 | }
--------------------------------------------------------------------------------
/docker/db/data/structure.sql:
--------------------------------------------------------------------------------
1 |
2 | USE nodeapp;
3 |
4 | SET @PREVIOUS_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS;
5 | SET FOREIGN_KEY_CHECKS = 0;
6 |
7 | DROP TABLE IF EXISTS `users`;
8 |
9 | CREATE TABLE `users` (
10 | `id` int(11) NOT NULL AUTO_INCREMENT,
11 | `name` varchar(128) DEFAULT NULL,
12 | `email` varchar(128) DEFAULT NULL,
13 | `password` varchar(128) DEFAULT NULL,
14 | PRIMARY KEY (`id`)
15 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
16 |
17 | -- insert an example user
18 | INSERT INTO users (`name`, `email`, `password`) VALUES
19 | ('John Doe', 'test@example.com', '$2b$10$raorHGAz4mAFzZHqjchwJueVg6uu0P8w0aPveIH/LnGmugf862Qlu');
20 |
21 | SET FOREIGN_KEY_CHECKS = @PREVIOUS_FOREIGN_KEY_CHECKS;
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | db:
5 | container_name: nodeapp_db
6 | build: docker/db
7 | ports:
8 | - '${MYSQL_PORT}:3306'
9 | volumes:
10 | - ./docker/db/data:/docker-entrypoint-initdb.d
11 | - db_nodeapp_data:/var/lib/mysql
12 | - ./docker/db/my.cnf:/etc/my.cnf
13 | env_file:
14 | - ./.env
15 | node:
16 | container_name: nodeapp
17 | build: docker/node
18 | tty: true
19 | volumes:
20 | - ./app:/home/app
21 | env_file:
22 | - ./.env
23 | ports:
24 | - '${APP_PORT}:80'
25 | - '${DEBUG_PORT}:9229'
26 | redis:
27 | container_name: nodeapp_redis
28 | image: 'redis:latest'
29 | ports:
30 | - '${REDIS_PORT}:6379'
31 | volumes:
32 | db_nodeapp_data:
--------------------------------------------------------------------------------
/app/client/js/lib/jquery.js:
--------------------------------------------------------------------------------
1 | /*
2 | Backbone imports jQuery and Webpack complains if it can't import
3 | jQuery. Here we are faking out the jQuery import (see alias in
4 | webpack.config.js) and then only implementing the absolute
5 | minimum that Backbone requires
6 | */
7 | module.exports = {
8 |
9 | // Backbone.ajax calls $.ajax – create our own ajax
10 | ajax(opts) {
11 | fetch(opts.url, {
12 | method: opts.type,
13 | headers: {
14 | 'Content-Type': 'application/json'
15 | },
16 | body: opts.data
17 | }).then(r=>r.json())
18 | .then(resp=>{
19 | opts.success(resp)
20 | }, err=>{
21 | opts.error(null, err.message, err)
22 | })
23 | }
24 |
25 | }
--------------------------------------------------------------------------------
/app/client/styles/base.less:
--------------------------------------------------------------------------------
1 | @import '~bui/helpers/colors.less';
2 |
3 | :root {
4 | --theme: var(--blue);
5 | --formControlTheme: var(--theme);
6 | }
7 |
8 | html, body {
9 | min-height: 100%;
10 | background: var(--dark);
11 | }
12 |
13 | body {
14 | min-height: 100vh;
15 | margin: 0;
16 | padding: 0;
17 | font-family: Roboto, sans-serif;
18 | background: var(--gray-100);
19 | color: var(--gray-900);
20 | opacity: 0;
21 | visibility: hidden;
22 | transition: opacity 800ms cubic-bezier(0.4, 0, 0.2, 1);
23 |
24 | &.show {
25 | opacity: 1;
26 | visibility: visible;
27 | }
28 |
29 | -webkit-font-smoothing: antialiased;
30 | -moz-osx-font-smoothing: grayscale;
31 | font-smoothing: antialiased;
32 | text-rendering: optimizeLegibility;
33 | }
--------------------------------------------------------------------------------
/app/client/login.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Node App
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/docker/db/my.cnf:
--------------------------------------------------------------------------------
1 | # For advice on how to change settings please see
2 | # http://dev.mysql.com/doc/refman/5.7/en/server-configuration-defaults.html
3 |
4 | [mysqld]
5 | #
6 | # Remove leading # and set to the amount of RAM for the most important data
7 | # cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%.
8 | # innodb_buffer_pool_size = 128M
9 | #
10 | # Remove leading # to turn on a very important data integrity option: logging
11 | # changes to the binary log between backups.
12 | # log_bin
13 | #
14 | # Remove leading # to set options mainly useful for reporting servers.
15 | # The server defaults are faster for transactions and fast SELECTs.
16 | # Adjust sizes as needed, experiment to find the optimal values.
17 | # join_buffer_size = 128M
18 | # sort_buffer_size = 2M
19 | # read_rnd_buffer_size = 2M
20 | skip-host-cache
21 | skip-name-resolve
22 | datadir=/var/lib/mysql
23 | socket=/var/lib/mysql/mysql.sock
24 | secure-file-priv=/var/lib/mysql-files
25 | user=mysql
26 |
27 | # Disabling symbolic-links is recommended to prevent assorted security risks
28 | symbolic-links=0
29 |
30 | log-error=/var/log/mysqld.log
31 | pid-file=/var/run/mysqld/mysqld.pid
32 | sql-mode='NO_ENGINE_SUBSTITUTION'
--------------------------------------------------------------------------------
/app/client/js/account/index.js:
--------------------------------------------------------------------------------
1 | import { LitElement, html, css } from 'lit-element'
2 | import Panel from 'panel'
3 | import user from '../user'
4 | import PasswordChange from './pw-change'
5 |
6 | Panel.register('account', 'a-user-account', {
7 | title: 'Account',
8 | width: '500px',
9 | })
10 |
11 | customElements.define('a-user-account', class extends LitElement{
12 |
13 | static get styles(){return css`
14 | :host {
15 | display: grid;
16 | grid-template-rows: auto 1fr;
17 | overflow: hidden;
18 | position:relative;
19 | }
20 |
21 | main {
22 | overflow: auto;
23 | padding: 1em;
24 | }
25 | `}
26 |
27 | constructor(){
28 | super()
29 | this.model = user
30 | }
31 |
32 | render(){return html`
33 |
34 |
35 |
36 | ${this.model.get('email')}
37 |
38 | Change Password
39 |
40 | `}
41 |
42 | changePassword(){
43 | PasswordChange.open(this.model)
44 | }
45 |
46 | })
47 |
48 | export default customElements.get('a-user-account')
--------------------------------------------------------------------------------
/app/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Node App
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | Your node app
22 | Begin coding your app in: /client/js/index.js
23 |
24 |
25 |
26 |
27 | User Account
28 | Sign out
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/app/client/js/user.js:
--------------------------------------------------------------------------------
1 | import store from 'bui/util/store'
2 | import {Model} from 'backbone'
3 |
4 | export class User extends Model {
5 |
6 | urlRoot(){ return '/api/user' }
7 |
8 | constructor(){
9 | super()
10 |
11 | let cached = store('user')
12 | if( cached )
13 | this.set(cached, {silent: true})
14 |
15 | window.user = this // TEMP
16 | }
17 |
18 | get name(){ return this.get('name') }
19 | get email(){ return this.get('email') }
20 |
21 | get initials(){
22 | return user.name.split(' ').map(s=>s[0]).slice(0,2).join('').toUpperCase()
23 | }
24 |
25 | logout(){
26 | store('user', null)
27 | window.location = '/logout'
28 | }
29 |
30 | async login(password, email){
31 |
32 | email = email || this.get('email')
33 |
34 | let formData = new FormData();
35 | formData.append('email', email);
36 | formData.append('password', password);
37 |
38 | return fetch('/login', {
39 | method: 'post',
40 | credentials: 'include',
41 | body: formData
42 | }).then(r=>r.json()).then(resp=>{
43 |
44 | if( resp && !resp.error ){
45 | store('user', resp)
46 | }
47 |
48 | return resp
49 | })
50 | }
51 |
52 | }
53 |
54 | // singleton logged in user
55 | export default new User()
--------------------------------------------------------------------------------
/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nodeapp",
3 | "version": "1.0.0",
4 | "description": "Node App Template",
5 | "main": "client/index.js",
6 | "scripts": {
7 | "start": "npm install --quiet && npm run client-build && npm run server",
8 | "server": "pm2-runtime start server.config.js",
9 | "cd-server": "docker exec -it nodeapp bash",
10 | "install": "docker exec -it nodeapp npm install",
11 | "dev": "docker exec -it nodeapp npm run client",
12 | "client": "webpack --env dev --display errors-only --display-entrypoints --display-used-exports",
13 | "client-build": "webpack --env prod --display minimal"
14 | },
15 | "author": "Kevin Jantzer",
16 | "license": "ISC",
17 | "dependencies": {
18 | "backbone": "^1.4.0",
19 | "bcrypt": "^3.0.6",
20 | "blackstone-ui": "git+https://github.com/kjantzer/bui.git",
21 | "bui-server": "git+https://github.com/kjantzer/bui-server.git",
22 | "body-parser": "^1.18.2",
23 | "connect-redis": "^4.0.3",
24 | "express": "^4.9.7",
25 | "express-fileupload": "^1.1.6-alpha.5",
26 | "express-session": "^1.17.0",
27 | "lit-element": "^2.2.1",
28 | "mysql": "^2.17.1",
29 | "passport": "^0.4.0",
30 | "passport-local": "^1.0.0",
31 | "redis": "^2.8.0"
32 | },
33 | "devDependencies": {
34 | "css-loader": "^3.2.0",
35 | "less": "^3.10.3",
36 | "less-loader": "^5.0.0",
37 | "raw-loader": "^0.5.1",
38 | "style-loader": "^1.0.0",
39 | "webpack": "^4.20.2",
40 | "webpack-bundle-analyzer": "^3.5.2",
41 | "webpack-cli": "^3.1.2"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Node App
2 | ================
3 |
4 | A starting point for node.js based apps. Uses Docker to setup a node.js, mysql, and redis server. Webpack is used to bundle the client JS.
5 |
6 | [](https://i.imgur.com/YajROul.mp4)
7 |
8 |
11 |
12 | ## Getting Started on Development
13 |
14 | ### Prerequisite
15 | You need [docker installed](https://docs.docker.com/compose/install/)
16 |
17 | ### 1.) Check the settings in `.env`
18 | You should change the `SESSION_SECRET` and update the app and mysql ports if they are going to conflict with any of your existing services
19 |
20 | ### 2.) Start the app containers
21 | A node.js server, mysql database, and redis service will be started. The node server will auto restart after crashing or when changes are made to `/app/server`
22 |
23 | ```bash
24 | $ cd /this/dir
25 | $ docker-compose up
26 | ```
27 |
28 | If all goes as planned, you should see this message:
29 |
30 | ```
31 | App running: localhost:8080
32 | ```
33 |
34 | > port may be different if you canged it in `.env`
35 |
36 | ### 3.) Start the developing
37 | [Webpack](https://webpack.js.org/) will be used to watch the files in `/app/client` and rebuild them.
38 |
39 | ```bash
40 | $ cd app
41 | $ npm run dev
42 | ```
43 |
44 | > The webpack bundler should be run from within the docker container.
45 | > The `dev` script is setup to do that
46 |
47 | ## Commands
48 | Some commands for developing
49 |
50 | - `docker-compose up` - start the app
51 | - `docker-compose down` - end the app
52 | - `npm run dev` - watch for client changes and rebuild
53 | - `npm run install` - install missing/new JS dependencies
54 | - `npm run cd-server` - "change directory" to the node server
55 |
56 | ## Dependency notes
57 | `jQuery` is imported by Backbone.js even though its not a hard dependency. Because of this,
58 | webpack complains if we dont have jQuery. To get around this, `client/jquery.js` was created
59 | that only includes the absolute minimum
60 |
61 | ## Code Report
62 | Keeping the client code slim and fast is important. An analyzer has been setup as part
63 | of the initial webpack build so we can analyze what code oversized.
64 |
65 | It can be accessed from the root: `localhost:8080/bundle-report.html`
66 |
67 |
--------------------------------------------------------------------------------
/app/server/index.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const http = require('http')
3 | const express = require('express')
4 | const passport = require('passport')
5 | const LocalStrategy = require('passport-local').Strategy;
6 |
7 | const User = require('./user')
8 | const API = require('bui-server/api')
9 |
10 | const PORT = 80
11 | global.ROOT_PATH = path.join(__dirname, '..');
12 | global.DIST_PATH = path.join(__dirname, '..', 'dist');
13 | global.CLIENT_PATH = path.join(__dirname, '..', 'client');
14 | global.SERVER_PATH = path.join(__dirname, '..', 'server');
15 |
16 | const app = express();
17 |
18 | passport.use(new LocalStrategy({
19 | usernameField: 'email',
20 | }, async (email, password, done)=>{
21 | User.login(email, password).then(user=>{
22 | done(null, user)
23 | }, err=>{
24 | done(err)
25 | })
26 | }));
27 |
28 | passport.serializeUser(function(user, done) {
29 | done(null, user.id);
30 | });
31 |
32 | passport.deserializeUser(async (id, done)=>{
33 | User.deserializeUser(id).then(user=>{
34 | done(null, user)
35 | }, err=>{
36 | done(err)
37 | })
38 | });
39 |
40 |
41 | // serve up static files from /dist and /client/public directories
42 | app.use(express.static(DIST_PATH, {index: false}))
43 | app.use(express.static(CLIENT_PATH+'/public', {index: false}))
44 |
45 | // setup parsers for incoming data
46 | app.use(require('body-parser').json({limit: '50mb'}))
47 | app.use(require('express-fileupload')());
48 |
49 | const session = require('express-session')
50 | let RedisStore = require('connect-redis')(session)
51 | let store = new RedisStore({client: require('./redis')})
52 |
53 | app.use(session({
54 | store: store,
55 | secret: process.env.SESSION_SECRET,
56 | resave: true,
57 | saveUninitialized: true
58 | }))
59 |
60 | app.use(passport.initialize());
61 | app.use(passport.session());
62 |
63 | // App Page
64 | app.get('/', (req, res)=>{
65 |
66 | if( !req.isAuthenticated() )
67 | return res.redirect('/login')
68 |
69 | res.sendFile(CLIENT_PATH+'/index.html')
70 | })
71 |
72 | // Login Page
73 | app.get('/login', (req, res)=>{
74 |
75 | if( req.isAuthenticated() ){
76 | return res.redirect('/')
77 | }
78 |
79 | res.sendFile(CLIENT_PATH+'/login.html')
80 | })
81 |
82 | // Logout Page, then redirect to login
83 | app.get('/logout', (req, res)=>{
84 | req.logout()
85 | res.redirect('/login');
86 | })
87 |
88 | // AJAX Login
89 | app.post('/login', (req, res, next)=>{
90 | passport.authenticate('local', (err, user, info)=>{
91 |
92 | if(err) {
93 | return res.status(401).send({error:err.message, trace: err.trace});
94 | }
95 |
96 | req.login(user, err=>{
97 | if( err )
98 | return res.status(500).send({error:err.message, trace: err.trace});
99 |
100 | res.json(user);
101 | })
102 |
103 | })(req, res, next);
104 | });
105 |
106 | app.get('/hash-pw/:pw', async (req, res)=>{
107 | let hash = await User.hashPassword(req.params.pw)
108 | res.send(hash)
109 | })
110 |
111 | new API(app, [
112 | User
113 | ], {root: '/api'})
114 |
115 | // start up node server
116 | const server = http.Server(app);
117 | server.listen(PORT, function(){console.log('\nApp running: localhost:'+process.env.APP_PORT);});
118 |
--------------------------------------------------------------------------------
/app/server/user.js:
--------------------------------------------------------------------------------
1 | const db = require('./db')
2 | const bcrypt = require('bcrypt')
3 | var crypto = require("crypto");
4 |
5 | const MIN_PW_LEN = 8
6 | const serializedUsers = new Map()
7 |
8 | module.exports = class User {
9 |
10 | static get api(){return {
11 | routes: [
12 | // ['put', '/user/:id?', 'update'],
13 | // ['patch', '/user/:id?', 'update'],
14 | ['put', '/user/:id/change-password', 'changePassword']
15 | ]
16 | }}
17 |
18 | constructor(attrs={}){
19 | this.attrs = attrs
20 |
21 | this.attrs.email_hash = null
22 |
23 | if( attrs.email ){
24 | this.attrs.email_hash = crypto.createHash('md5').update(attrs.email).digest("hex");
25 | }
26 | }
27 |
28 | get id(){ return this.attrs.id }
29 | get email(){ return this.attrs.email }
30 | get name(){ return this.attrs.name }
31 |
32 | toJSON(){
33 | let data = Object.assign({}, this.attrs)
34 | delete data.password
35 | return data
36 | }
37 |
38 | toString(){
39 | return JSON.stringify(this.toJSON())
40 | }
41 |
42 | async update(req){
43 |
44 | let results = await db.q(`UPDATE users SET ? WHERE ?`, [
45 | req.body,
46 | {id: req.user.id}
47 | ])
48 |
49 | // TODO: improve how attrs are updated?
50 | req.user.attrs = Object.assign(req.user.attrs, req.body)
51 |
52 | return req.body
53 | }
54 |
55 | async verifyPassword(pw){
56 | return bcrypt.compare(pw, this.attrs.password)
57 | }
58 |
59 | async changePassword(req){
60 |
61 | let user = await User.findByID(req.user.id)
62 | let {currentPW, newPW} = req.body
63 |
64 | if( currentPW == newPW )
65 | throw new Error('same password')
66 |
67 | if( !newPW || newPW.length < MIN_PW_LEN )
68 | throw new Error('too short')
69 |
70 | if( !await user.verifyPassword(currentPW) )
71 | throw new Error('invalid current password')
72 |
73 | newPW = await User.hashPassword(newPW)
74 |
75 | user.update({
76 | body: {password: newPW},
77 | user: user
78 | })
79 |
80 | return true
81 | }
82 |
83 | static async encryptPassword(pw){
84 | return bcrypt.hash(pw, 10)
85 | }
86 |
87 | static async findByID(id){
88 | let resp = await db.q('SELECT * FROM users WHERE id = ?', id)
89 | return resp && new User(resp[0])
90 | }
91 |
92 | static async findByEmail(email){
93 |
94 | let resp = await db.q('SELECT * FROM users WHERE email = ?', email)
95 |
96 | if( !resp || resp.length == 0 )
97 | throw Error('email not found')
98 |
99 | return new User(resp[0])
100 | }
101 |
102 | static async login(email, password){
103 | let user = await User.findByEmail(email)
104 |
105 | if( !await user.verifyPassword(password) ){
106 | throw Error('password does not match')
107 | }
108 |
109 | return user
110 | }
111 |
112 | static deserializeUser(id){
113 | if( !serializedUsers.get(id) ){
114 | let user = User.findByID(id)
115 | if( user )
116 | serializedUsers.set(id, user)
117 | }
118 |
119 | return serializedUsers.get(id)
120 | }
121 |
122 | static hashPassword(pw){
123 | return pw ? bcrypt.hash(pw, 10) : null;
124 | }
125 |
126 | }
--------------------------------------------------------------------------------
/app/webpack.config.js:
--------------------------------------------------------------------------------
1 | /*
2 | Webpack Config
3 |
4 | This file tells webpack how to bundle our assets
5 | */
6 | const path = require('path');
7 | // const webpack = require('webpack');
8 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
9 |
10 | /*
11 | NOTE: we may want to include a plugin to make the css build to a file separate from the JS
12 | */
13 |
14 | module.exports = env => { const DEV = env == 'dev'; return {
15 |
16 | plugins: [
17 | // to reduce JS size, leave out moment locales...we're not international...yet
18 | // new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
19 |
20 | // generate a report of the size of JS modules
21 | new BundleAnalyzerPlugin({
22 | analyzerMode: DEV ? 'static' : 'disabled',
23 | reportFilename: 'bundle-report.html'
24 | })
25 | ],
26 |
27 | // not sure if this matters yet, but webpack requires mode to be set
28 | mode: env=='prod' ? 'production' : 'development',
29 |
30 | // rebuild the files when they change
31 | watch: DEV,
32 |
33 | // create a source map on build
34 | devtool: 'source-map',
35 |
36 | // two entry points will be built, one for the main app and one for login
37 | entry: {
38 | index: './client/js/index.js',
39 | login: './client/js/login.js',
40 | },
41 |
42 | // put the built files in /dist
43 | output: {
44 | filename: '[name].js',
45 | path: path.resolve(__dirname, 'dist'),
46 | },
47 |
48 | // adjust how imports are resolved
49 | resolve: {
50 |
51 | // limit where modules should be loaded from
52 | modules: [
53 | // prefer our node modules (and not nested node modules in /catalog)
54 | // needed to keep from duplicate lit-elements imported
55 | path.resolve(__dirname, 'node_modules'),
56 | "node_modules",
57 | ],
58 |
59 | // make aliases for modules so we can type less
60 | alias: {
61 | 'bui': 'blackstone-ui',
62 | 'form': 'bui/presenters/form-control',
63 | 'panel': 'bui/presenters/panel',
64 | 'tabs': 'bui/presenters/tabs',
65 | 'popover': 'bui/presenters/popover',
66 | 'dialog': 'bui/presenters/dialog',
67 | 'menu': 'bui/presenters/menu',
68 |
69 | // backbone requires jquery...use our own version
70 | 'jquery': path.resolve(__dirname, 'client/js/lib/jquery')
71 | }
72 | },
73 |
74 | // inform webpack how to handle certain types of modules
75 | module: {
76 |
77 | rules: [
78 | // load HTML/txt/svgs modules as raw text
79 | {
80 | test: /\.html$|\.txt$|\.svg$|\.md$|\.css$/,
81 | use: 'raw-loader'
82 | },
83 |
84 | // load less files, compile to css, and append to DOM
85 | {
86 | test: /\.(less)$/,
87 | use: [
88 | {
89 | loader: 'style-loader', // creates style nodes from JS strings
90 | },
91 | {
92 | loader: 'css-loader', // translates CSS into CommonJS
93 | options: {
94 | sourceMap: true,
95 | },
96 | },
97 | {
98 | loader: 'less-loader', // compiles Less to CSS
99 | options: {
100 | sourceMap: true,
101 | },
102 | },
103 | ]
104 | }]
105 | }
106 |
107 | }}
--------------------------------------------------------------------------------
/app/client/js/login.js:
--------------------------------------------------------------------------------
1 | import '../styles/login.less'
2 |
3 | import { LitElement, html, css } from 'lit-element'
4 | import 'bui/elements/paper'
5 | import 'bui/elements/btn'
6 | import 'form/form-control'
7 | import user from './user'
8 |
9 | customElements.define('a-login', class extends LitElement{
10 |
11 | static get styles(){return css`
12 | :host {
13 | background: var(--dark-black);
14 | display: block;
15 | position:relative;
16 | height: 100vh;
17 | display: flex;
18 | justify-content: center;
19 | align-items: center;
20 | }
21 |
22 | b-paper {
23 | width: 400px;
24 | max-width: 96%;
25 | display: grid;
26 | position:relative;
27 | gap: 1em;
28 | }
29 |
30 | b-paper > img {
31 | height: 140px;
32 | margin: 0 auto -.5em;
33 | right: -20px;
34 | position: relative;
35 | }
36 |
37 | form-control {
38 | display: block;
39 | }
40 |
41 | h1 {
42 | text-align: center;
43 | margin: 0;
44 | }
45 |
46 | .error:empty {
47 | display: none;
48 | }
49 |
50 | .error {
51 | margin: 0;
52 | color: var(--red);
53 | }
54 | `}
55 |
56 | firstUpdated(){
57 | document.body.classList.add('show')
58 | }
59 |
60 | render(){return html`
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | Sign In
73 |
74 |
75 |
76 | `}
77 |
78 | toggleSeePW(e){
79 | let input = e.currentTarget.previousElementSibling
80 | input.type = input.type == 'text' ? 'password' : 'text'
81 | e.currentTarget.name = input.type == 'text' ? 'eye-1' : 'eye-off'
82 | }
83 |
84 | onKeydown(e){
85 | if( e.key == 'Enter' )
86 | e.currentTarget.parentElement.nextElementSibling.click()
87 | }
88 |
89 | login(e){
90 |
91 | let btn = e.currentTarget
92 |
93 | // already logging in
94 | if( btn.spin ) return
95 |
96 | let email = this.shadowRoot.querySelector('#email')
97 | let pw = this.shadowRoot.querySelector('#password')
98 |
99 | if( email.isInvalid || !email.value || !pw.value )
100 | return console.log('missing creds')
101 |
102 | btn.spin = true
103 |
104 | user.login(pw.value, email.value).then(resp=>{
105 |
106 | btn.spin = false
107 |
108 | if( resp.error ){
109 | btn.nextElementSibling.innerHTML = resp.error
110 | return
111 | }
112 |
113 | window.location = '/'+location.hash
114 |
115 | }, err=>{
116 | btn.spin = false
117 | btn.nextElementSibling.innerHTML = 'Problem signing in'
118 | })
119 |
120 | }
121 |
122 | })
123 |
124 | export default customElements.get('a-login')
--------------------------------------------------------------------------------
/app/client/js/account/pw-change.js:
--------------------------------------------------------------------------------
1 | import { LitElement, html, css } from 'lit-element'
2 | import Panel from 'panel'
3 | import Dialog from 'dialog'
4 | import 'bui/elements/spinner-overlay'
5 | import 'form/form-handler'
6 | import 'form/form-control'
7 |
8 | const MIN_LEN = 8
9 | let View
10 |
11 | customElements.define('a-password-change', class extends LitElement{
12 |
13 | static open(model){
14 |
15 | // reuse the same view
16 | if( !View )
17 | View = document.createElement('a-password-change')
18 |
19 | if( !model ) return;
20 |
21 | View.model = model
22 |
23 | new Panel(View, {
24 | title: 'Password',
25 | width: '360px',
26 | height: '420px',
27 | anchor: 'center',
28 | onClose: View.onClose.bind(View)
29 | }).open()
30 | }
31 |
32 | static get styles(){return css`
33 | :host {
34 | display: block;
35 | }
36 |
37 | form-handler {
38 | display: grid;
39 | position:relative;
40 | padding: 1em;
41 | /* gap: 1em; */
42 | }
43 | `}
44 |
45 | render(){return html`
46 |
47 |
48 | Cancel
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | Use at least ${MIN_LEN} characters
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | Change Password
70 |
71 |
72 | `}
73 |
74 | changePassword(){
75 | let currentPW = this.formHandler.get('pw_current').value
76 | let newPW = this.formHandler.get('pw_new').value
77 | let confirmPW = this.formHandler.get('pw_new_confirm').value
78 |
79 | if( currentPW != newPW
80 | && newPW.length >= MIN_LEN
81 | && newPW === confirmPW ){
82 | this.spinner.show = true
83 | console.log('update pw', currentPW, newPW);
84 |
85 | fetch(this.model.url()+'/change-password', {
86 | method: 'PUT',
87 | headers: {
88 | 'Content-Type': 'application/json'
89 | },
90 | body: JSON.stringify({currentPW, newPW})
91 | }).then(r=>r.json())
92 | .then(resp=>{
93 |
94 | this.spinner.show = false
95 |
96 | if( resp.error ){
97 | Dialog.error({title: 'Error', msg: resp.error}).modal()
98 | return
99 | }
100 |
101 | this.close()
102 |
103 | Dialog.success({msg: 'Password Changed'}).modal()
104 | })
105 | }
106 | }
107 |
108 | validateConfirm(val, el){
109 | if( !val )return true;
110 | let key = el.parentElement.key
111 |
112 | el.popover&&el.popover.close()
113 |
114 | let currentPW = this.formHandler.get('pw_current')
115 | let newPW = this.formHandler.get('pw_new')
116 | let confirmPW = this.formHandler.get('pw_new_confirm')
117 |
118 | if( key == 'pw_new' && val.length < MIN_LEN ){
119 | // Dialog.error({msg: 'Too short', btns: false}).popover(el)
120 | return false
121 | }
122 |
123 | if( key == 'pw_new' && currentPW.value == newPW.value ){
124 | // Dialog.error({msg: 'Same password as your current', btns: false}).popover(el)
125 | return false
126 | }
127 |
128 | if( key == 'pw_new_confirm' && newPW.value != confirmPW.value ){
129 | // Dialog.error({msg: 'Not the same', btns: false}).popover(el)
130 | return false
131 | }
132 | }
133 |
134 | onClose(){
135 | this.formHandler.editors.forEach(el=>el.value='')
136 | }
137 |
138 | close(){
139 | this.model = null
140 | this.panel&&this.panel.close()
141 | }
142 |
143 | })
144 |
145 | export default customElements.get('a-password-change')
--------------------------------------------------------------------------------