mirror of
https://github.com/docker-training/node-bulletin-board.git
synced 2025-08-15 22:39:32 +08:00
Add v4, with proxy, app and db
This commit is contained in:
parent
3f2894bea0
commit
1e92429385
2
bulletin-board-app/.gitignore
vendored
Normal file
2
bulletin-board-app/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
10
bulletin-board-app/Dockerfile
Normal file
10
bulletin-board-app/Dockerfile
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
FROM node:6.11.5
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
COPY package.json .
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
CMD [ "npm", "start" ]
|
||||||
|
|
||||||
|
COPY . .
|
21
bulletin-board-app/LICENSE
Normal file
21
bulletin-board-app/LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2016 Ryan Chenkie <ryan@elevatedigital.io> (http://elevatedigital.io)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
54
bulletin-board-app/app.js
Normal file
54
bulletin-board-app/app.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
new Vue({
|
||||||
|
el: '#events',
|
||||||
|
|
||||||
|
data: {
|
||||||
|
event: { title: '', detail: '', date: '' },
|
||||||
|
events: []
|
||||||
|
},
|
||||||
|
|
||||||
|
ready: function () {
|
||||||
|
this.fetchEvents();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
fetchEvents: function () {
|
||||||
|
var events = [];
|
||||||
|
this.$http.get('/api/events')
|
||||||
|
.success(function (events) {
|
||||||
|
this.$set('events', events);
|
||||||
|
console.log(events);
|
||||||
|
})
|
||||||
|
.error(function (err) {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
addEvent: function () {
|
||||||
|
if (this.event.title.trim()) {
|
||||||
|
this.$http.post('/api/events', this.event)
|
||||||
|
.success(function (res) {
|
||||||
|
this.events.push(this.event);
|
||||||
|
console.log('Event added!');
|
||||||
|
})
|
||||||
|
.error(function (err) {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteEvent: function (id) {
|
||||||
|
if (confirm('Are you sure you want to delete this event?')) {
|
||||||
|
this.$http.delete('api/events/' + id)
|
||||||
|
.success(function (res) {
|
||||||
|
console.log(res);
|
||||||
|
var index = this.events.find(x => x.id === id)
|
||||||
|
this.events.splice(index, 1);
|
||||||
|
})
|
||||||
|
.error(function (err) {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
43
bulletin-board-app/backend/api.js
Normal file
43
bulletin-board-app/backend/api.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
var db = require('./db.js');
|
||||||
|
|
||||||
|
exports.events = function (req, res) {
|
||||||
|
console.log('Loading DB events...');
|
||||||
|
db.Events
|
||||||
|
.findAll()
|
||||||
|
.then(events => {
|
||||||
|
console.log('Fetched events, count: ' + events.length);
|
||||||
|
res.json(events);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('** Fetch failed: ', err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.event = function (req, res) {
|
||||||
|
console.log('Handling event call, method: ' + req.method + ', event ID: ' + req.params.eventId)
|
||||||
|
switch(req.method) {
|
||||||
|
case "DELETE":
|
||||||
|
db.Events
|
||||||
|
.destroy({
|
||||||
|
where: {
|
||||||
|
id: req.params.eventId
|
||||||
|
}
|
||||||
|
}).then(function() {
|
||||||
|
console.log('Deleted event with id: ' + req.params.eventId)
|
||||||
|
res.status(200).end();
|
||||||
|
});
|
||||||
|
break
|
||||||
|
case "POST":
|
||||||
|
db.Events
|
||||||
|
.create({
|
||||||
|
title: req.body.title,
|
||||||
|
detail: req.body.detail,
|
||||||
|
date: req.body.date
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
res.send('{}');
|
||||||
|
res.status(201).end();
|
||||||
|
});
|
||||||
|
break
|
||||||
|
}
|
||||||
|
};
|
34
bulletin-board-app/backend/db.js
Normal file
34
bulletin-board-app/backend/db.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
var Sequelize = require('sequelize');
|
||||||
|
var username = 'sa';
|
||||||
|
var password = 'DockerCon!!!';
|
||||||
|
var host = 'bb-db';
|
||||||
|
var dbName = 'BulletinBoard';
|
||||||
|
|
||||||
|
var sequelize = new Sequelize(dbName, username, password, {
|
||||||
|
dialect: 'mssql',
|
||||||
|
host: host,
|
||||||
|
port: 1433,
|
||||||
|
dialectOptions: {
|
||||||
|
requestTimeout: 30000
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sequelize
|
||||||
|
.authenticate()
|
||||||
|
.then(() => {
|
||||||
|
console.log('Successful connection to SQL Server.');
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('** SQL Server connection failed: ', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
var Event = sequelize.define('event', {
|
||||||
|
title: Sequelize.STRING,
|
||||||
|
detail: Sequelize.STRING,
|
||||||
|
date: Sequelize.DATE
|
||||||
|
});
|
||||||
|
|
||||||
|
Event.sync();
|
||||||
|
|
||||||
|
exports.Events = Event;
|
3
bulletin-board-app/backend/index.js
Normal file
3
bulletin-board-app/backend/index.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
exports.index = function (req, res) {
|
||||||
|
res.render('index');
|
||||||
|
}
|
BIN
bulletin-board-app/fonts/geomanist/hinted-Geomanist-Book.woff2
Normal file
BIN
bulletin-board-app/fonts/geomanist/hinted-Geomanist-Book.woff2
Normal file
Binary file not shown.
55
bulletin-board-app/index.html
Normal file
55
bulletin-board-app/index.html
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Bulletin Board</title>
|
||||||
|
<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="site.css">
|
||||||
|
<style>
|
||||||
|
.form-control {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.btn-danger {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="jumbotron">
|
||||||
|
<h1>Welcome to the Bulletin Board</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container" id="events">
|
||||||
|
<div class="col-sm-7">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3>Add an Event</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div>
|
||||||
|
<input class="form-control" placeholder="Title" v-model="event.title">
|
||||||
|
<textarea class="form-control" placeholder="Detail" v-model="event.detail"></textarea>
|
||||||
|
<input type="date" class="form-control" placeholder="Date" v-model="event.date">
|
||||||
|
<button class="btn btn-primary" v-on:click="addEvent">Submit</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-5">
|
||||||
|
<div class="list-group">
|
||||||
|
<a href="#" class="list-group-item" v-for="event in events">
|
||||||
|
<h4 class="list-group-item-heading"><i class="glyphicon glyphicon-bullhorn"></i> {{ event.title }}</h4>
|
||||||
|
<h5><i class="glyphicon glyphicon-calendar" v-if="event.date"></i> {{ event.date }}</h5>
|
||||||
|
<p class="list-group-item-text" v-if="event.detail">{{ event.detail }}</p>
|
||||||
|
<button class="btn btn-xs btn-danger" v-on:click="deleteEvent(event.id)">Delete</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
<script src="node_modules/vue/dist/vue.min.js"></script>
|
||||||
|
<script src="node_modules/vue-resource/dist/vue-resource.min.js"></script>
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</html>
|
24
bulletin-board-app/package.json
Normal file
24
bulletin-board-app/package.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "vue-event-bulletin",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Demo application for the scotch.io tutorial",
|
||||||
|
"main": "server.js",
|
||||||
|
"author": "Ryan Chenkie, Jason Lam",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bootstrap": "^3.3.6",
|
||||||
|
"ejs": "^2.3.4",
|
||||||
|
"express": "^4.13.3",
|
||||||
|
"morgan": "^1.6.1",
|
||||||
|
"vue": "^1.0.10",
|
||||||
|
"vue-resource": "^0.1.17",
|
||||||
|
"tedious": "^2.0.1",
|
||||||
|
"sequelize": "^4.20.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"body-parser": "^1.14.1",
|
||||||
|
"errorhandler": "^1.4.2",
|
||||||
|
"method-override": "^2.3.5",
|
||||||
|
"morgan": "^1.6.1"
|
||||||
|
}
|
||||||
|
}
|
20
bulletin-board-app/readme.md
Normal file
20
bulletin-board-app/readme.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
## Vue Events Bulletin Board
|
||||||
|
|
||||||
|
This is the code for the Vue.js [tutorial on Scotch.io](https://scotch.io/tutorials/build-a-single-page-time-tracking-app-with-vue-js-introduction). In the tutorial we build a events bulletin board application and cover the basics of [Vue](http://vuejs.org/).
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Run `npm install`.
|
||||||
|
2. Run `node server.js`.
|
||||||
|
3. Visit [http://localhost:8080](http://localhost:8080).
|
||||||
|
|
||||||
|
## RESTful API (contributed by Jason Lam)
|
||||||
|
|
||||||
|
1. **Use Node.js & Express for backend server and router.**
|
||||||
|
2. **RESTful requests towards the server to simulate CRUD on *events* model, instead of local hardcoded ones.**
|
||||||
|
3. Translated into Traditional Chinese.
|
||||||
|
|
||||||
|
## RESTful API written in Go
|
||||||
|
|
||||||
|
If you would like to use a backend written in Go, [thewhitetulip](http://github.com/thewhitetulip) has written on. See [the source code](https://github.com/thewhitetulip/go-vue-events).
|
||||||
|
|
38
bulletin-board-app/server.js
Normal file
38
bulletin-board-app/server.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
var express = require('express'),
|
||||||
|
bodyParser = require('body-parser'),
|
||||||
|
methodOverride = require('method-override'),
|
||||||
|
errorHandler = require('errorhandler'),
|
||||||
|
morgan = require('morgan'),
|
||||||
|
routes = require('./backend'),
|
||||||
|
api = require('./backend/api');
|
||||||
|
|
||||||
|
var app = module.exports = express();
|
||||||
|
|
||||||
|
app.engine('html', require('ejs').renderFile);
|
||||||
|
app.set('view engine', 'html');
|
||||||
|
app.use(morgan('dev'));
|
||||||
|
app.use(bodyParser.urlencoded({ extended: false }));
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
app.use(methodOverride());
|
||||||
|
app.use(express.static(__dirname + '/'));
|
||||||
|
app.use('/build', express.static('public'));
|
||||||
|
|
||||||
|
var env = process.env.NODE_ENV;
|
||||||
|
if ('development' == env) {
|
||||||
|
app.use(errorHandler({
|
||||||
|
dumpExceptions: true,
|
||||||
|
showStack: true
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('production' == app.get('env')) {
|
||||||
|
app.use(errorHandler());
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get('/', routes.index);
|
||||||
|
app.get('/api/events', api.events);
|
||||||
|
app.post('/api/events', api.event);
|
||||||
|
app.delete('/api/events/:eventId', api.event)
|
||||||
|
|
||||||
|
app.listen(8080);
|
||||||
|
console.log('Magic happens on port 8080...');
|
50
bulletin-board-app/site.css
Normal file
50
bulletin-board-app/site.css
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
/* v2 */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Geomanist Book';
|
||||||
|
src: url(fonts/geomanist/hinted-Geomanist-Book.eot);
|
||||||
|
src: url(fonts/geomanist/hinted-Geomanist-Book.eot?#iefix) format('embedded-opentype'),url(../fonts/geomanist/hinted-Geomanist-Book.woff2) format('woff2'),url(../fonts/geomanist/hinted-Geomanist-Book.woff) format('woff'),url(../fonts/geomanist/hinted-Geomanist-Book.ttf) format('truetype'),url(../fonts/geomanist/hinted-Geomanist-Book.svg#Geomanist-Book) format('svg');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
padding-bottom: 20px;
|
||||||
|
background-color: rgb(98, 41, 145);
|
||||||
|
color: #FFF;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wrapping element */
|
||||||
|
/* Set some basic padding to keep content from hitting the edges */
|
||||||
|
.body-content {
|
||||||
|
padding-left: 15px;
|
||||||
|
padding-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Set widths on the form inputs since otherwise they're 100% wide */
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Responsive: Portrait tablets and up */
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.body-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* v2 */
|
||||||
|
|
||||||
|
.jumbotron {
|
||||||
|
font-family: 'Geomanist Book';
|
||||||
|
background-color: #380a66;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2 {
|
||||||
|
font-family: 'Geomanist Book';
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
10
bulletin-board-db/Dockerfile
Normal file
10
bulletin-board-db/Dockerfile
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
FROM microsoft/mssql-server-linux:2017-CU1
|
||||||
|
|
||||||
|
ENV ACCEPT_EULA=Y \
|
||||||
|
MSSQL_SA_PASSWORD=DockerCon!!!
|
||||||
|
|
||||||
|
WORKDIR /init
|
||||||
|
COPY init-db.* ./
|
||||||
|
|
||||||
|
RUN chmod +x ./init-db.sh
|
||||||
|
RUN /opt/mssql/bin/sqlservr & ./init-db.sh
|
3
bulletin-board-db/init-db.sh
Normal file
3
bulletin-board-db/init-db.sh
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
sleep 30s
|
||||||
|
|
||||||
|
/opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P DockerCon!!! -i init-db.sql
|
20
bulletin-board-db/init-db.sql
Normal file
20
bulletin-board-db/init-db.sql
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
CREATE DATABASE BulletinBoard;
|
||||||
|
GO
|
||||||
|
|
||||||
|
USE BulletinBoard;
|
||||||
|
|
||||||
|
CREATE TABLE Events (
|
||||||
|
Id INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
|
||||||
|
Title NVARCHAR(50),
|
||||||
|
Detail NVARCHAR(200),
|
||||||
|
[Date] DATETIMEOFFSET,
|
||||||
|
CreatedAt DATETIMEOFFSET NOT NULL,
|
||||||
|
UpdatedAt DATETIMEOFFSET NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO Events (Title, Detail, [Date], CreatedAt, UpdatedAt) VALUES
|
||||||
|
(N'Docker for Beginners', N'Introduction to Docker using Node.js', '2017-11-21', GETDATE(), GETDATE()),
|
||||||
|
(N'Advanced Orchestration', N'Deep dive into Docker Swarm', '2017-12-25', GETDATE(), GETDATE()),
|
||||||
|
(N'Docker on Windows', N'From 101 to production', '2018-01-01', GETDATE(), GETDATE());
|
||||||
|
|
||||||
|
SELECT * FROM BulletinBoard.dbo.Events;
|
4
bulletin-board-proxy/Dockerfile
Normal file
4
bulletin-board-proxy/Dockerfile
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
FROM nginx:1.13.6
|
||||||
|
|
||||||
|
RUN mkdir -p /data/nginx/cache
|
||||||
|
COPY nginx.conf /etc/nginx/nginx.conf
|
43
bulletin-board-proxy/nginx.conf
Normal file
43
bulletin-board-proxy/nginx.conf
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
user nginx;
|
||||||
|
worker_processes 1;
|
||||||
|
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=STATIC:30m inactive=24h max_size=200m use_temp_path=off;
|
||||||
|
|
||||||
|
proxy_pass_request_headers on;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header Connection keep-alive;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_proxied any;
|
||||||
|
|
||||||
|
add_header X-Host $hostname;
|
||||||
|
add_header X-Cache-Status $upstream_cache_status;
|
||||||
|
|
||||||
|
map $sent_http_content_type $expires {
|
||||||
|
default off;
|
||||||
|
~image/ 6M;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
expires $expires;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://bb-app:8080/;
|
||||||
|
proxy_cache STATIC;
|
||||||
|
proxy_cache_valid 200 1h;
|
||||||
|
proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
docker-compose.yml
Normal file
29
docker-compose.yml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
version: '3.3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
bb-app:
|
||||||
|
image: sixeyed/bulletin-board-app
|
||||||
|
build:
|
||||||
|
context: ./bulletin-board-app
|
||||||
|
networks:
|
||||||
|
- bb-net
|
||||||
|
|
||||||
|
bb-db:
|
||||||
|
image: sixeyed/bulletin-board-db
|
||||||
|
build:
|
||||||
|
context: ./bulletin-board-db
|
||||||
|
networks:
|
||||||
|
- bb-net
|
||||||
|
|
||||||
|
bb-proxy:
|
||||||
|
image: sixeyed/bulletin-board-proxy
|
||||||
|
build:
|
||||||
|
context: ./bulletin-board-proxy
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
networks:
|
||||||
|
- bb-net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
bb-net:
|
Loading…
x
Reference in New Issue
Block a user