├── client ├── .eslintignore ├── robots.txt ├── src │ ├── styles │ │ ├── index.less │ │ ├── theme.less │ │ └── custom.less │ ├── store │ │ ├── index.js │ │ └── modules │ │ │ ├── auth.js │ │ │ └── things.js │ ├── components │ │ ├── Footer.vue │ │ ├── Header.vue │ │ ├── things │ │ │ ├── Create.vue │ │ │ ├── Display.vue │ │ │ └── Edit.vue │ │ ├── Sider.vue │ │ ├── account │ │ │ └── ChangePassword.vue │ │ └── admin │ │ │ └── UserTable.vue │ ├── views │ │ ├── admin │ │ │ └── Users.vue │ │ ├── Home.vue │ │ ├── Things.vue │ │ ├── account │ │ │ ├── Settings.vue │ │ │ ├── Login.vue │ │ │ └── Signup.vue │ │ └── Common.vue │ ├── App.vue │ ├── api │ │ ├── user.service.js │ │ ├── thing.service.js │ │ ├── http-common.js │ │ └── auth.service.js │ ├── main.js │ └── router.js ├── index.html ├── .babelrc ├── .eslintrc.json ├── package.json └── webpack.config.js ├── server ├── .dockerignore ├── .env.default ├── src │ ├── config │ │ ├── index.js │ │ ├── seed.js │ │ └── security.js │ ├── api │ │ ├── thing │ │ │ ├── thing.model.js │ │ │ └── index.js │ │ ├── health-check │ │ │ └── index.js │ │ ├── auth │ │ │ ├── index.js │ │ │ └── service.js │ │ └── user │ │ │ ├── user.model.js │ │ │ └── index.js │ ├── entry.js │ ├── main.js │ └── routes.js ├── Dockerfile ├── docker-compose.yml ├── .eslintrc.json ├── package.json ├── wait-for-it.sh └── package-lock.json ├── .github ├── FUNDING.YML └── workflows │ └── server-ci.yml ├── kubernetes ├── secrets.yml ├── ingress.yml ├── mongo.yml └── server.yml ├── .gitignore ├── LICENSE └── README.md /client/.eslintignore: -------------------------------------------------------------------------------- 1 | dist/* -------------------------------------------------------------------------------- /server/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env -------------------------------------------------------------------------------- /client/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / -------------------------------------------------------------------------------- /server/.env.default: -------------------------------------------------------------------------------- 1 | PORT= 2 | DB_URI= 3 | AUTH_SECRET= -------------------------------------------------------------------------------- /client/src/styles/index.less: -------------------------------------------------------------------------------- 1 | @import "./theme"; // Custom theme of iview 2 | @import "./custom"; -------------------------------------------------------------------------------- /.github/FUNDING.YML: -------------------------------------------------------------------------------- 1 | # repo: yunhan0/koa-vue-fullstack 2 | # filename: FUNDING.YML 3 | 4 | github: yunhan0 5 | -------------------------------------------------------------------------------- /kubernetes/secrets.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: koa-vue-fullstack-stack-secrets 5 | type: Opaque -------------------------------------------------------------------------------- /client/src/styles/theme.less: -------------------------------------------------------------------------------- 1 | @import '~iview/src/styles/index.less'; 2 | 3 | /* Here are the variables to cover, such as: 4 | * Note: For entire variable list, check out 5 | * https://github.com/iview/iview/blob/2.0/src/styles/custom.less 6 | */ -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Snapshot 7 | 8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /client/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | /** Vuex **/ 3 | import Vuex from 'vuex' 4 | /** Modules **/ 5 | import auth from './modules/auth' 6 | import things from './modules/things' 7 | 8 | Vue.use(Vuex) 9 | 10 | export default new Vuex.Store({ 11 | modules: { auth, things } 12 | }) -------------------------------------------------------------------------------- /client/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "@babel/preset-env" ], 3 | "plugins": [ 4 | "@babel/plugin-syntax-dynamic-import", 5 | "@babel/plugin-proposal-object-rest-spread", 6 | ["import", { 7 | "libraryName": "iview", 8 | "libraryDirectory": "src/components" 9 | }] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /client/src/components/Footer.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | 17 | 20 | -------------------------------------------------------------------------------- /client/src/views/admin/Users.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 20 | -------------------------------------------------------------------------------- /client/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | 23 | 26 | -------------------------------------------------------------------------------- /server/src/config/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Server port 3 | port: process.env.PORT || 3000, 4 | 5 | // MongoDB connection options 6 | mongo: { 7 | uri: process.env.DB_URI || 'mongodb://localhost/snapshot' 8 | }, 9 | 10 | secret: { 11 | // Used for Jwt, default secret is randomly generated 12 | auth: process.env.AUTH_SECRET || 'EwIZ9MJWyJ' 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /server/src/api/thing/thing.model.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const mongoose = require('mongoose') 4 | 5 | // With Mongoose, everything is derived from a Schema. 6 | var thingSchema = mongoose.Schema({ 7 | name: { 8 | type: String, 9 | required: [true, 'name is required'] 10 | }, 11 | info: String 12 | }) 13 | 14 | // Compiling our schema into a Model. 15 | module.exports = mongoose.model('Thing', thingSchema) 16 | -------------------------------------------------------------------------------- /client/src/styles/custom.less: -------------------------------------------------------------------------------- 1 | .beautiful-gradient { 2 | background: #355C7D; /* fallback for old browsers */ 3 | background: -webkit-linear-gradient(to bottom, #C06C84, #6C5B7B, #355C7D); /* Chrome 10-25, Safari 5.1-6 */ 4 | background: linear-gradient(to bottom, #C06C84, #6C5B7B, #355C7D); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ 5 | } 6 | 7 | .text-center { 8 | text-align: center; 9 | } 10 | 11 | .text-right { 12 | text-align: right; 13 | } -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an node 10 runtime as a parent image 2 | FROM node:10.12.0 3 | 4 | # Set environment vaiable: server port as 3002 5 | ENV AUTH_SECRET=vHJcV7 6 | 7 | # Set the working directory to /server 8 | WORKDIR /server 9 | 10 | # Copy the current directory contents into the container at /server 11 | ADD . /server 12 | 13 | RUN npm install 14 | 15 | # Make port 3000 available to the world outside this container 16 | EXPOSE 3000 17 | 18 | CMD ["npm", "run", "build"] 19 | -------------------------------------------------------------------------------- /client/src/views/Things.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 28 | -------------------------------------------------------------------------------- /kubernetes/ingress.yml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: koa-vue-fullstack-ingress 5 | annotations: 6 | kubernetes.io/ingress.class: nginx 7 | cert-manager.io/cluster-issuer: letsencrypt-prod 8 | spec: 9 | tls: 10 | - hosts: 11 | - k8s.yunhan.li 12 | secretName: koa-vue-fullstack-stack-secrets 13 | rules: 14 | - host: k8s.yunhan.li 15 | http: 16 | paths: 17 | - path: / 18 | backend: 19 | serviceName: koa-vue-fullstack-server 20 | servicePort: 3001 -------------------------------------------------------------------------------- /server/src/api/health-check/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Router = require('koa-router') 4 | const mongoose = require('mongoose') 5 | 6 | let router = new Router() 7 | 8 | router 9 | .get('/health-check', async (ctx) => { 10 | const state = mongoose.connection.readyState 11 | // When state === 0, it means the connection is disconnected 12 | if (!state) { 13 | throw new Error('connection to mongodb is broken') 14 | } 15 | 16 | ctx.status = 204 17 | ctx.body = null 18 | }) 19 | 20 | 21 | module.exports = router 22 | -------------------------------------------------------------------------------- /server/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | server: 4 | container_name: server 5 | image: yunhan0/koa-vue-fullstack:server 6 | # image: server 7 | # build: . 8 | restart: always 9 | ports: 10 | - "3000:3001" # container port: server port 11 | environment: 12 | - DB_URI=mongodb://mongo:27017/snapshot 13 | links: 14 | - mongo 15 | command: ["./wait-for-it.sh", "mongo:27017", "--", "npm", "run", "build"] 16 | mongo: 17 | container_name: mongo 18 | image: mongo 19 | volumes: 20 | - ./data:/data/db 21 | ports: 22 | - "27017:27017" -------------------------------------------------------------------------------- /client/src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 29 | 30 | 33 | -------------------------------------------------------------------------------- /client/src/views/account/Settings.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 31 | -------------------------------------------------------------------------------- /client/src/api/user.service.js: -------------------------------------------------------------------------------- 1 | import HTTP from './http-common' 2 | 3 | /* 4 | * Service should be singleton, 5 | * hence we could declare a simple object literal. 6 | */ 7 | let UserResource = { 8 | show() { // Show all the users 9 | return HTTP.get('users/') 10 | }, 11 | 12 | get() { // Get current user 13 | return HTTP.get('users/me') 14 | }, 15 | 16 | create(body) { 17 | return HTTP.post('users/', body) 18 | }, 19 | 20 | changePassword(body) { 21 | return HTTP.put('users/me/password', body) 22 | }, 23 | 24 | delete(id) { // Delete a thing 25 | return HTTP.delete('users/' + id) 26 | } 27 | } 28 | 29 | export default UserResource -------------------------------------------------------------------------------- /server/src/entry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Entry file 3 | */ 4 | 5 | /* 6 | * CREATE a .env file under server folder, and copy and paste the contents of 7 | * .env.default file into this .env, and assign values 8 | */ 9 | 10 | /* 11 | * Should not use .env files in your production environment though 12 | * and rather set the values directly on the respective host. 13 | */ 14 | 15 | if (process.env.NODE_ENV !== 'production') { 16 | const config = require('dotenv').config() 17 | if (config.error) { 18 | throw config.error 19 | } 20 | } 21 | console.log('===== Running ' + process.env.NODE_ENV + ' mode =====') 22 | console.log('DB_URI: ' + process.env.DB_URI) 23 | console.log('PORT: ' + process.env.PORT) 24 | require('./main') 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Build files 40 | dist 41 | 42 | # Docker 43 | data/ 44 | 45 | # Environment Variable 46 | .env 47 | 48 | # Webpack Stats 49 | stats.json 50 | 51 | .DS_Store 52 | 53 | # VS Code 54 | .vscode -------------------------------------------------------------------------------- /client/src/api/thing.service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * GET /api/things -> index 3 | * POST /api/things -> create 4 | * GET /api/things/:id -> show 5 | * PUT /api/things/:id -> update 6 | * DELETE /api/things/:id -> delete 7 | */ 8 | 9 | import HTTP from './http-common' 10 | 11 | /* 12 | * Service should be singleton, 13 | * hence we could declare a simple object literal. 14 | */ 15 | let ThingResource = { 16 | show() { // Show all the things 17 | return HTTP.get('things') 18 | }, 19 | 20 | get(id) { // Get a specific thing 21 | return HTTP.get('things/' + id) 22 | }, 23 | 24 | create(body) { // Create a thing 25 | return HTTP.post('things', body) 26 | }, 27 | 28 | delete(id) { // Delete a thing 29 | return HTTP.delete('things/' + id) 30 | }, 31 | 32 | update(id, body) { // Update a thing 33 | return HTTP.put('things/' + id, body) 34 | } 35 | } 36 | 37 | export default ThingResource -------------------------------------------------------------------------------- /server/src/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Main file after entry 3 | */ 4 | 'use strict' 5 | 6 | const http = require('http') 7 | const koa = require('koa') 8 | const bodyParser = require('koa-bodyparser') 9 | const mongoose = require('mongoose') 10 | 11 | const config = require('./config') 12 | // Connect to MongoDB 13 | mongoose.connect(config.mongo.uri, { 14 | useNewUrlParser: true, 15 | useCreateIndex: true, 16 | useUnifiedTopology: true, 17 | useFindAndModify: false 18 | }) 19 | 20 | var db = mongoose.connection 21 | db.on('error', console.error.bind(console, 'connection error:')) 22 | db.once('open', function() { 23 | console.log('Database connected') 24 | }) 25 | 26 | // Populate databases with sample data 27 | require('./config/seed') 28 | 29 | // Setup server 30 | const app = new koa() 31 | // Koa bodyparser 32 | app.use(bodyParser()) 33 | // Security 34 | require('./config/security')(app) 35 | // Routing 36 | require('./routes')(app) 37 | 38 | http.createServer(app.callback()).listen(config.port) 39 | -------------------------------------------------------------------------------- /server/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "ecmaVersion": 2017 9 | }, 10 | "rules": { 11 | "indent": [ 12 | "error", 13 | 2 14 | ], 15 | "max-len": [ 16 | "error", 17 | 80 18 | ], 19 | "operator-linebreak": [ 20 | "error", 21 | "after" 22 | ], 23 | "multiline-comment-style": [ 24 | "error", 25 | "starred-block" 26 | ], 27 | "linebreak-style": [ 28 | "error", 29 | "unix" 30 | ], 31 | "quotes": [ 32 | "error", 33 | "single" 34 | ], 35 | "semi": [ 36 | "error", 37 | "never" 38 | ], 39 | "eol-last": [ 40 | "error", 41 | "always" 42 | ], 43 | "no-console": "off" 44 | } 45 | } -------------------------------------------------------------------------------- /server/src/routes.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | // Customized error handling, default error code: 500 3 | app.use(async (ctx, next) => { 4 | try { 5 | await next() 6 | } catch (err) { 7 | // Handle Validation Error 8 | if(err.name === 'ValidationError') { 9 | err.status = 422 10 | } 11 | // Handle Mongoose Item Duplication Error 12 | if (err.name === 'BulkWriteError' && err.code === 11000) { 13 | err.status = 422 14 | } 15 | ctx.status = err.status || 500 16 | ctx.body = { message: err.message } 17 | } 18 | }) 19 | 20 | // Health check does not require authentication 21 | app.use(require('./api/health-check').routes()) 22 | 23 | app.use(require('./api/auth').routes()) 24 | app.use(require('./api/user').routes()) 25 | // route middleware to verify a token 26 | app.use(require('./api/auth/service').isAuthenticated) 27 | app 28 | .use(require('./api/thing').routes()) 29 | .use(require('./api/thing').allowedMethods()) 30 | } 31 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "snapshot-server", 3 | "version": "1.0.0", 4 | "description": "A snapshot of server dev using node, koa, and mongoose", 5 | "author": "Yunhan", 6 | "license": "ISC", 7 | "main": "src/entry.js", 8 | "scripts": { 9 | "eslint": "./node_modules/.bin/eslint src/", 10 | "eslint-fix": "./node_modules/.bin/eslint src/ --fix", 11 | "start": "NODE_ENV=development nodemon src/entry.js", 12 | "build": "NODE_ENV=production node src/entry.js" 13 | }, 14 | "dependencies": { 15 | "@koa/cors": "^2.2.1", 16 | "bcrypt": "^5.0.0", 17 | "dotenv": "^5.0.1", 18 | "jsonwebtoken": "^8.1.1", 19 | "koa": "^2.2.0", 20 | "koa-bodyparser": "^4.2.0", 21 | "koa-compose": "^4.1.0", 22 | "koa-convert": "^1.2.0", 23 | "koa-lusca": "^2.2.0", 24 | "koa-router": "^7.1.1", 25 | "koa-session": "^5.8.1", 26 | "mongoose": "^5.7.5" 27 | }, 28 | "devDependencies": { 29 | "babel-eslint": "^10.0.1", 30 | "eslint": "^4.19.1", 31 | "nodemon": "^2.0.6" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /client/src/api/http-common.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Axios is the most popular HTTP Client library, 3 | * by the time of writing this piece of code. 4 | */ 5 | import axios from 'axios' 6 | /** Store **/ 7 | import store from '../store/' 8 | /** Router **/ 9 | import router from '../router' 10 | 11 | // A new instance of axios with a custom config. 12 | let HTTP = axios.create({ 13 | baseURL: 'http://localhost:3000/api/' 14 | }) 15 | 16 | // Add a request interceptor 17 | HTTP.interceptors.request.use(function (config) { 18 | if (localStorage.getItem('token') !== null) { 19 | config.headers.common['access_token'] = localStorage.getItem('token') 20 | } 21 | return config 22 | }, function(error) { 23 | return Promise.reject(error) 24 | }) 25 | 26 | // Add a response interceptor 27 | HTTP.interceptors.response.use(function (response) { 28 | return response 29 | }, function(error) { 30 | if (error.response.status === 401) { 31 | store.dispatch('logout') 32 | router.push('/login') 33 | } 34 | return Promise.reject(error.response.data) 35 | }) 36 | 37 | export default HTTP -------------------------------------------------------------------------------- /client/src/views/Common.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 43 | -------------------------------------------------------------------------------- /client/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "commonjs": true 6 | }, 7 | "parserOptions": { 8 | "parser": "babel-eslint", 9 | "ecmaVersion": 2017, 10 | "sourceType": "module" 11 | }, 12 | "extends": [ 13 | "eslint:recommended", 14 | "plugin:vue/recommended" 15 | ], 16 | "rules": { 17 | "strict": 0, 18 | "indent": [ 19 | "error", 20 | 2 21 | ], 22 | "max-len": [ 23 | "error", 24 | 80 25 | ], 26 | "operator-linebreak": [ 27 | "error", 28 | "after" 29 | ], 30 | "multiline-comment-style": [ 31 | "error", 32 | "starred-block" 33 | ], 34 | "linebreak-style": [ 35 | "error", 36 | "unix" 37 | ], 38 | "quotes": [ 39 | "error", 40 | "single" 41 | ], 42 | "semi": [ 43 | "error", 44 | "never" 45 | ], 46 | "eol-last": [ 47 | "error", 48 | "always" 49 | ], 50 | "no-console": "off", 51 | "vue/no-parsing-error": [ 52 | 2, { 53 | "x-invalid-end-tag": false 54 | } 55 | ] 56 | }, 57 | "plugins": [ 58 | "vue" 59 | ] 60 | } -------------------------------------------------------------------------------- /kubernetes/mongo.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: mongodb-deployment 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: mongo 11 | template: 12 | metadata: 13 | labels: 14 | app: mongo 15 | spec: 16 | volumes: 17 | - name: mongodb-storage 18 | persistentVolumeClaim: 19 | claimName: mongodb-volume-claim 20 | containers: 21 | - name: mongo 22 | image: mongo:latest 23 | ports: 24 | - containerPort: 27017 25 | volumeMounts: 26 | - mountPath: /data/db 27 | name: mongodb-storage 28 | 29 | --- 30 | apiVersion: v1 31 | kind: PersistentVolumeClaim 32 | metadata: 33 | name: mongodb-volume-claim 34 | spec: 35 | accessModes: 36 | - ReadWriteOnce 37 | resources: 38 | requests: 39 | storage: 2Gi 40 | 41 | --- 42 | apiVersion: v1 43 | kind: Service 44 | metadata: 45 | name: mongodb-service 46 | spec: 47 | ports: 48 | - port: 27017 49 | targetPort: 27017 50 | selector: 51 | app: mongo 52 | type: ClusterIP 53 | -------------------------------------------------------------------------------- /.github/workflows/server-ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | # pull_request: 7 | # branches: [ master ] 8 | 9 | jobs: 10 | path-context: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out the repo 14 | uses: actions/checkout@v2 15 | - name: Set up QEMU 16 | uses: docker/setup-qemu-action@v1 17 | - name: Set up Docker Buildx 18 | uses: docker/setup-buildx-action@v1 19 | - name: Login to DockerHub 20 | uses: docker/login-action@v1 21 | with: 22 | username: ${{ secrets.DOCKERHUB_USERNAME }} 23 | password: ${{ secrets.DOCKERHUB_TOKEN }} 24 | - name: Build and push 25 | id: docker_build 26 | uses: docker/build-push-action@v2 27 | with: 28 | context: ./server 29 | file: ./server/Dockerfile 30 | push: true 31 | tags: yunhan0/koa-vue-fullstack:server 32 | - name: Image digest 33 | run: echo ${{ steps.docker_build.outputs.digest }} 34 | - name: Trigger deploy 35 | uses: Consensys/kubernetes-action@master 36 | env: 37 | KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_DATA }} 38 | with: 39 | args: apply -f ./kubernetes/server.yml 40 | -------------------------------------------------------------------------------- /server/src/config/seed.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Populate DB with sample data on server start 3 | */ 4 | 5 | 'use strict' 6 | const Thing = require('../api/thing/thing.model') 7 | const User = require('../api/user/user.model') 8 | 9 | const seeding = async() => { 10 | let numberOfThings = await Thing.countDocuments() 11 | // populate initial things if nothing is in the database 12 | if (numberOfThings == 0) { 13 | await Thing.create({ 14 | name: 'Our first Koa and Node app', info: 'Lightweight server' 15 | }, { 16 | name: 'Mongo is here', info: 'We use mongodb to store data!' 17 | }) 18 | console.log('Finish populating database - things') 19 | } 20 | 21 | let numberOfUsers = await User.countDocuments() 22 | // populate initial users if no user is in the database 23 | if (numberOfUsers == 0) { 24 | await User.create({ 25 | name: 'tester', 26 | email: 'test@example.com', 27 | password: 'helloworld', 28 | role: 'user' 29 | }) 30 | await User.create({ 31 | name: 'admin', 32 | email: 'admin@example.com', 33 | password: '123456', 34 | role: 'admin' 35 | }) 36 | console.log('Finish populating database - users') 37 | } 38 | } 39 | 40 | seeding() 41 | -------------------------------------------------------------------------------- /kubernetes/server.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: koa-vue-fullstack-server 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: koa-vue-fullstack-server 11 | template: 12 | metadata: 13 | labels: 14 | app: koa-vue-fullstack-server 15 | spec: 16 | containers: 17 | - name: koa-vue-fullstack-server 18 | image: yunhan0/koa-vue-fullstack:server 19 | imagePullPolicy: Always 20 | ports: 21 | - containerPort: 3000 22 | env: 23 | - name: DB_URI 24 | value: mongodb://mongodb-service.yunhan.svc.cluster.local:27017/snapshot 25 | livenessProbe: 26 | httpGet: 27 | path: /health-check 28 | port: 3000 29 | initialDelaySeconds: 15 30 | readinessProbe: 31 | httpGet: 32 | path: /health-check 33 | port: 3000 34 | 35 | --- 36 | apiVersion: v1 37 | kind: Service 38 | metadata: 39 | name: koa-vue-fullstack-server 40 | spec: 41 | ports: 42 | - port: 3001 43 | protocol: TCP 44 | targetPort: 3000 45 | selector: 46 | app: koa-vue-fullstack-server 47 | type: ClusterIP 48 | -------------------------------------------------------------------------------- /server/src/api/auth/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Router = require('koa-router') 4 | const jwt = require('jsonwebtoken') 5 | const secret = require('../../config').secret.auth 6 | var User = require('../user/user.model') 7 | 8 | let router = new Router({ 9 | prefix: '/api/auth' 10 | }) 11 | 12 | router 13 | .post('/login', async(ctx) => { 14 | try { 15 | let email = ctx.request.body.email, 16 | password = ctx.request.body.password 17 | let user = await User.findOne({email: email.toLowerCase()}) 18 | 19 | if(!user) { 20 | ctx.throw(404, 'This email is not registered.') 21 | } 22 | 23 | let authenticated = await user.comparePassword(password) 24 | if(!authenticated) { 25 | ctx.throw(401, 'This password is not correct.') 26 | } else { 27 | // Sign token 28 | let token = await jwt.sign({id: user._id, role: user.role}, secret, { 29 | expiresIn: '1d' 30 | }) 31 | ctx.body = {token: token} 32 | } 33 | } catch(err) { 34 | throw err 35 | } 36 | }) 37 | /* 38 | * .post('/forget', async(ctx, next) => { 39 | * }) 40 | */ 41 | 42 | /* 43 | * .post('/reset', async(ctx, next) => { 44 | * }) 45 | */ 46 | 47 | module.exports = router 48 | -------------------------------------------------------------------------------- /client/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | /** Router **/ 4 | import router from './router' 5 | /** Store **/ 6 | import store from './store/' 7 | import AuthService from './api/auth.service' 8 | /** Style **/ 9 | import './styles/index.less' 10 | /** iView **/ 11 | import { Button, Input, Row, Col, Card, Message, locale } from 'iview' 12 | // Configure iView language 13 | import lang from 'iview/dist/locale/en-US' 14 | locale(lang) 15 | /* 16 | * Import iView on demand, below is generally used components. 17 | */ 18 | Vue.component('Button', Button) 19 | Vue.component('Input', Input) 20 | Vue.component('Row', Row) 21 | Vue.component('Col', Col) 22 | Vue.component('Card', Card) 23 | Vue.prototype.$Message = Message 24 | 25 | function initialisation() { 26 | new Vue({ 27 | el:'#app', 28 | router, 29 | store, 30 | render: h=>h(App) 31 | }) 32 | } 33 | 34 | (function () { 35 | if (localStorage.getItem('token')) { 36 | return AuthService.getCurrentUser() 37 | .then(user => { 38 | store.dispatch('autoLogin', user) 39 | initialisation() 40 | }) 41 | /*eslint no-unused-vars: ["error", {"args": "none"}]*/ 42 | .catch(err => { 43 | initialisation() 44 | }) 45 | } else { 46 | initialisation() 47 | } 48 | })() -------------------------------------------------------------------------------- /client/src/store/modules/auth.js: -------------------------------------------------------------------------------- 1 | import AuthService from '../../api/auth.service' 2 | 3 | export default { 4 | state: { 5 | user: null 6 | }, 7 | getters: { 8 | isAuthenticated: state => { 9 | return !!state.user 10 | }, 11 | 12 | isAdmin: state => { 13 | return state.user.role === 'admin' 14 | }, 15 | 16 | getCurrentUser: state => { 17 | return state.user 18 | } 19 | }, 20 | mutations: { 21 | LOGIN: (state, data) => { 22 | state.user = data 23 | }, 24 | LOGOUT: state => { 25 | state.user = null 26 | } 27 | }, 28 | actions: { 29 | login: ({ commit }, body) => { 30 | return AuthService.login(body) 31 | .then(user => { 32 | commit('LOGIN', user) 33 | }) 34 | .catch(err => { 35 | throw err 36 | }) 37 | }, 38 | 39 | logout: ({ commit }) => { 40 | localStorage.removeItem('token') 41 | commit('LOGOUT') 42 | }, 43 | 44 | autoLogin: ({commit}, user) => { 45 | commit('LOGIN', user) 46 | }, 47 | 48 | signup: ({ commit }, body) => { 49 | return AuthService.signup(body) 50 | .then(user => { 51 | commit('LOGIN', user) 52 | }) 53 | .catch(err => { 54 | throw err 55 | }) 56 | }, 57 | } 58 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Yunhan Li 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /server/src/config/security.js: -------------------------------------------------------------------------------- 1 | // Cross origin resource sharing 2 | const cors = require('@koa/cors') 3 | // Application security for koa. 4 | const lusca = require('koa-lusca') 5 | /* 6 | * Simple session middleware for koa 7 | * const session = require('koa-session') 8 | * Convert legacy koa syntax 9 | */ 10 | const convert = require('koa-convert') 11 | 12 | module.exports = function(app) { 13 | app.use(cors()) 14 | // app.use(session(app)) 15 | app.use(convert( 16 | lusca({ 17 | /* 18 | * The X-Frame-Options HTTP response header can be used to indicate 19 | * whether or not a browser should be allowed to render a page in 20 | * a or