├── 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 |
2 |
5 |
6 |
7 |
16 |
17 |
20 |
--------------------------------------------------------------------------------
/client/src/views/admin/Users.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
20 |
--------------------------------------------------------------------------------
/client/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Homepage
4 |
5 | Demo: Things
6 |
7 |
8 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
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 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 |
29 |
30 |
33 |
--------------------------------------------------------------------------------
/client/src/views/account/Settings.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
11 |
12 |
16 | Col-12
17 |
18 |
19 |
20 |
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 |
2 |
3 |
9 |
10 |
11 |
12 |
13 |
17 | Home
18 |
19 | {{ $route.name }}
20 |
21 |
22 |
23 |
24 |
25 |
26 |
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