Add multi-site docker configuration #2
|
@ -0,0 +1,25 @@
|
||||||
|
FROM node:23-alpine
|
||||||
|
|
||||||
|
RUN mkdir /app
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json /app
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY models/ /app/models/
|
||||||
|
COPY views/ /app/views/
|
||||||
|
COPY public/ /app/public/
|
||||||
|
|
||||||
|
COPY app.js /app/
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
ENV HOST=0.0.0.0
|
||||||
|
ENV PORT=5050
|
||||||
|
|
||||||
|
ENV TITLE="Hall X"
|
||||||
|
ENV PREFIX=
|
||||||
|
#ENV HALLS="/hall-a:Hall A;/hall-b:Hall B"
|
||||||
|
|
||||||
|
CMD node app.js
|
48
README.md
48
README.md
|
@ -2,17 +2,49 @@
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
Clone to `/opt/timer`.
|
### Configuration
|
||||||
Put `openfest-timer@.service` in `/etc/systemd/system`.
|
|
||||||
Requires NodeJS and npm.
|
|
||||||
|
|
||||||
```bash
|
Configuration is done by environmental variables:
|
||||||
systemctl daemon-reload
|
|
||||||
npm install
|
```yaml
|
||||||
systemctl enable --now openfest-timer@hall-a.service
|
HALLS: "/hall-a:Hall A;/hall-b:Hall B"
|
||||||
systemctl enable --now openfest-timer@hall-b.service
|
TITLE: "Hall A" # not in use if HALLS is set to non-empty
|
||||||
|
PREFIX: /hall-a # not in use if HALLS is set to non-empty
|
||||||
|
BASIC_AUTH: "admin:password" # user:plaintext-password
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Prod Deployment
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd timer-repo/
|
||||||
|
git pull
|
||||||
|
podman build -t timer .
|
||||||
|
|
||||||
|
cat > .config/containers/systemd/timer.container <<EOF
|
||||||
|
[Container]
|
||||||
|
ContainerName=timer
|
||||||
|
Image=timer:latest
|
||||||
|
|
||||||
|
Environment=PORT=5050
|
||||||
|
Environment=HALLS="/hall-a:Hall A;/hall-b:Hall B;/test-hall:TEST"
|
||||||
|
Environment=BASIC_AUTH="user:password"
|
||||||
|
|
||||||
|
PublishPort=127.0.0.1:5050:5050
|
||||||
|
|
||||||
|
Network=INSERT_THE_NETWORK
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user restart timer
|
||||||
|
```
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
`docker-compose up --build`
|
||||||
|
|
||||||
## Based on Defcon
|
## Based on Defcon
|
||||||
|
|
||||||
### Defcon
|
### Defcon
|
||||||
|
|
223
app.js
223
app.js
|
@ -4,17 +4,40 @@ const
|
||||||
server = require('http').createServer(app),
|
server = require('http').createServer(app),
|
||||||
Stopwatch = require('./models/stopwatch'),
|
Stopwatch = require('./models/stopwatch'),
|
||||||
errorHandler = require('errorhandler'),
|
errorHandler = require('errorhandler'),
|
||||||
expressLayouts = require('express-ejs-layouts'),
|
expressLayouts = require('express-ejs-layouts');
|
||||||
io = require('socket.io')(server, {});
|
|
||||||
|
const auth = (req, res, next) => {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
|
||||||
|
if (!authHeader) {
|
||||||
|
return res.set("WWW-Authenticate", "Basic realm='timer'").status(401).send('Unauthorized');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const authTokens = authHeader.split(' ');
|
||||||
|
if (authTokens[0] === 'Basic') {
|
||||||
|
const credentials = Buffer.from(authTokens[1], 'base64').toString('utf8');
|
||||||
|
|
||||||
|
if (credentials !== process.env.BASIC_AUTH) { // this is obviously not the most secure way, but we don't care
|
||||||
|
return res.status(401).send('Unauthorized');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return res.status(401).send('Unauthorized');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
|
|
||||||
|
if (process.env.BASIC_AUTH) {
|
||||||
|
app.use(auth);
|
||||||
|
}
|
||||||
|
|
||||||
app.set('views', __dirname + '/views');
|
app.set('views', __dirname + '/views');
|
||||||
app.set('view engine', 'ejs');
|
app.set('view engine', 'ejs');
|
||||||
|
|
||||||
app.use(expressLayouts);
|
|
||||||
app.use(express.static(__dirname + '/public'));
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
app.use(errorHandler({ dumpExceptions: true, showStack: true }));
|
app.use(errorHandler({ dumpExceptions: true, showStack: true }));
|
||||||
}
|
}
|
||||||
|
@ -24,95 +47,119 @@ if (process.env.NODE_ENV === 'production') {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Use the port that Heroku provides or default to 5000
|
// Use the port and host
|
||||||
var port = process.env.PORT || 5000;
|
var port = process.env.PORT || 5000;
|
||||||
var host = process.env.HOST || '0.0.0.0';
|
var host = process.env.HOST || '0.0.0.0';
|
||||||
|
|
||||||
|
var prefix = process.env.PREFIX || '';
|
||||||
|
|
||||||
|
const halls = (process.env.HALLS || ((process.env.PREFIX || "") + ":" + (process.env.TITLE || "TIMER"))).replace("; ?$", "").split(';').map(tuple => {
|
||||||
|
const parts = tuple.split(':');
|
||||||
|
return {"prefix": parts[0].trim(), "title": parts[1].trim()};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Configuring halls:", halls)
|
||||||
|
|
||||||
|
halls.map(hall => {
|
||||||
|
const title = hall["title"]
|
||||||
|
const prefix = hall["prefix"]
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const io = require('socket.io')(server, {path: prefix + '/socket.io'})
|
||||||
|
|
||||||
|
router.use(expressLayouts);
|
||||||
|
router.use(express.static(__dirname + '/public'));
|
||||||
|
|
||||||
|
var stopwatch = new Stopwatch();
|
||||||
|
|
||||||
|
stopwatch.on('tick:stopwatch', function(time) {
|
||||||
|
io.sockets.emit('time', { time: time });
|
||||||
|
});
|
||||||
|
|
||||||
|
stopwatch.on('reset:stopwatch', function(time) {
|
||||||
|
io.sockets.emit('time', { time: time });
|
||||||
|
});
|
||||||
|
|
||||||
|
// stopwatch.start(); // we probably don't want autostart on server start
|
||||||
|
|
||||||
|
io.sockets.on('connection', function (socket) {
|
||||||
|
io.sockets.emit('time', { time: stopwatch.getTime() });
|
||||||
|
|
||||||
|
socket.on('click:start', function () {
|
||||||
|
stopwatch.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('click:stop', function () {
|
||||||
|
stopwatch.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('click:zero', function () {
|
||||||
|
stopwatch.zero();
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('click:reset', function () {
|
||||||
|
stopwatch.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('click:resetShort', function () {
|
||||||
|
stopwatch.resetShort();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
router.get('/', function(req, res) {
|
||||||
|
res.render('index', { title: title, prefix: prefix });
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const control = express.Router();
|
||||||
|
|
||||||
|
control.get('/', function(req, res) {
|
||||||
|
res.render('control', { title: title, prefix: prefix });
|
||||||
|
});
|
||||||
|
control.post('/reset/', function (req, res) {
|
||||||
|
stopwatch.reset();
|
||||||
|
res.send("OK");
|
||||||
|
});
|
||||||
|
control.post('/reset-short/', function (req, res) {
|
||||||
|
stopwatch.resetShort();
|
||||||
|
res.send("OK");
|
||||||
|
});
|
||||||
|
control.post('/start-from-reset/', function (req, res) {
|
||||||
|
stopwatch.reset();
|
||||||
|
stopwatch.start();
|
||||||
|
res.send("OK");
|
||||||
|
});
|
||||||
|
control.post('/start-from-reset-short/', function (req, res) {
|
||||||
|
stopwatch.resetShort();
|
||||||
|
stopwatch.start();
|
||||||
|
res.send("OK");
|
||||||
|
});
|
||||||
|
control.post('/start/', function (req, res) {
|
||||||
|
stopwatch.start();
|
||||||
|
res.send("OK");
|
||||||
|
});
|
||||||
|
control.post('/stop/', function (req, res) {
|
||||||
|
stopwatch.stop();
|
||||||
|
res.send("OK");
|
||||||
|
});
|
||||||
|
control.post('/zero/', function (req, res) {
|
||||||
|
stopwatch.zero();
|
||||||
|
res.send("OK");
|
||||||
|
});
|
||||||
|
|
||||||
|
router.use('/control', control);
|
||||||
|
|
||||||
|
app.use(prefix, router);
|
||||||
|
});
|
||||||
|
|
||||||
server.listen(port, host, function() {
|
server.listen(port, host, function() {
|
||||||
console.log("Express server listening on %j in %s mode", server.address(), app.settings.env);
|
console.log("Express server listening on %j in %s mode", server.address(), app.settings.env);
|
||||||
});
|
});
|
||||||
|
|
||||||
var stopwatch = new Stopwatch();
|
if (halls.length != 1 && halls[0]["prefix"] != "") {
|
||||||
|
app.get('/', function(req, res) {
|
||||||
stopwatch.on('tick:stopwatch', function(time) {
|
res.render("list", { layout: false, halls: halls });
|
||||||
io.sockets.emit('time', { time: time });
|
|
||||||
});
|
|
||||||
|
|
||||||
stopwatch.on('reset:stopwatch', function(time) {
|
|
||||||
io.sockets.emit('time', { time: time });
|
|
||||||
});
|
|
||||||
|
|
||||||
stopwatch.start();
|
|
||||||
|
|
||||||
io.sockets.on('connection', function (socket) {
|
|
||||||
io.sockets.emit('time', { time: stopwatch.getTime() });
|
|
||||||
|
|
||||||
socket.on('click:start', function () {
|
|
||||||
stopwatch.start();
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
socket.on('click:stop', function () {
|
|
||||||
stopwatch.stop();
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('click:zero', function () {
|
|
||||||
stopwatch.zero();
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('click:reset', function () {
|
|
||||||
stopwatch.reset();
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('click:resetShort', function () {
|
|
||||||
stopwatch.resetShort();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// configure title
|
|
||||||
//app.use(function (req, res, next) {
|
|
||||||
// res.locals.title = process.env.TITLE || 'Timer'
|
|
||||||
//});
|
|
||||||
|
|
||||||
app.get('/', function(req, res) {
|
|
||||||
res.render('index', { title: process.env.TITLE });
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const control = express.Router();
|
|
||||||
|
|
||||||
control.get('/', function(req, res) {
|
|
||||||
res.render('control', { title: process.env.TITLE });
|
|
||||||
});
|
|
||||||
control.post('/reset/', function (req, res) {
|
|
||||||
stopwatch.reset();
|
|
||||||
res.send("OK");
|
|
||||||
});
|
|
||||||
control.post('/reset-short/', function (req, res) {
|
|
||||||
stopwatch.resetShort();
|
|
||||||
res.send("OK");
|
|
||||||
});
|
|
||||||
control.post('/start-from-reset/', function (req, res) {
|
|
||||||
stopwatch.reset();
|
|
||||||
stopwatch.start();
|
|
||||||
res.send("OK");
|
|
||||||
});
|
|
||||||
control.post('/start-from-reset-short/', function (req, res) {
|
|
||||||
stopwatch.resetShort();
|
|
||||||
stopwatch.start();
|
|
||||||
res.send("OK");
|
|
||||||
});
|
|
||||||
control.post('/start/', function (req, res) {
|
|
||||||
stopwatch.start();
|
|
||||||
res.send("OK");
|
|
||||||
});
|
|
||||||
control.post('/stop/', function (req, res) {
|
|
||||||
stopwatch.stop();
|
|
||||||
res.send("OK");
|
|
||||||
});
|
|
||||||
control.post('/zero/', function (req, res) {
|
|
||||||
stopwatch.zero();
|
|
||||||
res.send("OK");
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use('/control', control);
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
services:
|
||||||
|
timer:
|
||||||
|
build:
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
context: .
|
||||||
|
environment:
|
||||||
|
HOST: 0.0.0.0
|
||||||
|
#TITLE: "Hall A"
|
||||||
|
#PREFIX: /hall-a
|
||||||
|
#HALLS: "/hall-a:Hall A;/hall-b:Hall B"
|
||||||
|
#BASIC_AUTH: "admin:password"
|
||||||
|
ports:
|
||||||
|
- 127.0.0.1:5050:5050
|
|
@ -1,13 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=OpenFest Timer (%i)
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
EnvironmentFile=/opt/timer/%i.env
|
|
||||||
Type=simple
|
|
||||||
Restart=on-failure
|
|
||||||
WorkingDirectory=/opt/timer
|
|
||||||
ExecStart=/usr/bin/env node app.js
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
|
@ -11,6 +11,6 @@
|
||||||
"underscore": "^1.13.6"
|
"underscore": "^1.13.6"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "0.6.x"
|
"node": "23.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,21 @@
|
||||||
#wrapper {
|
.wrapper {
|
||||||
|
width: 95%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 30vh 14vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper #countdown {
|
||||||
|
font-family: 'Black Ops One', cursive;
|
||||||
|
font-size: 15vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-wrapper {
|
||||||
width: 475px;
|
width: 475px;
|
||||||
height: 171px;
|
height: 171px;
|
||||||
margin: 100px auto;
|
margin: 100px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
#countdown {
|
.control-wrapper #countdown {
|
||||||
font-family: 'Black Ops One', cursive;
|
font-family: 'Black Ops One', cursive;
|
||||||
font-size: 90px;
|
font-size: 90px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//var socket = io.connect(window.location.hostname);
|
// a bit hackish way to find the path but it will get the job done
|
||||||
var socket = io();
|
var socket = io({"path": window.location.pathname.replace(/(\/control\/?|\/)?$/, "") + "/socket.io"});
|
||||||
|
|
||||||
socket.on('time', function (data) {
|
socket.on('time', function (data) {
|
||||||
$('#countdown').html(data.time);
|
$('#countdown').html(data.time);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<div id="wrapper">
|
<div class="control-wrapper">
|
||||||
<h1><%= title %></h1>
|
<h1><%= title %></h1>
|
||||||
<div id="countdown"></div>
|
<div id="countdown"></div>
|
||||||
<button id="start" class="thoughtbot">Start</button>
|
<button id="start" class="thoughtbot">Start</button>
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
<div id="wrapper">
|
<div class="wrapper">
|
||||||
<div id="countdown"></div>
|
<div id="countdown"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,20 +8,20 @@
|
||||||
|
|
||||||
<!-- HTML5 shim, for IE6-8 support of HTML elements -->
|
<!-- HTML5 shim, for IE6-8 support of HTML elements -->
|
||||||
<!--[if lt IE 9]>
|
<!--[if lt IE 9]>
|
||||||
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
|
<script src="//html5shim.googlecode.com/svn/trunk/html5.js"></script>
|
||||||
<![endif]-->
|
<![endif]-->
|
||||||
|
|
||||||
<!-- styles -->
|
<!-- styles -->
|
||||||
<link href="/css/main.css" rel="stylesheet">
|
<link href="<%= prefix %>/css/main.css" rel="stylesheet">
|
||||||
|
|
||||||
<!-- fonts -->
|
<!-- fonts -->
|
||||||
<link href='http://fonts.googleapis.com/css?family=Black+Ops+One' rel='stylesheet' type='text/css'>
|
<link href='//fonts.googleapis.com/css?family=Black+Ops+One' rel='stylesheet' type='text/css'>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%- body %>
|
<%- body %>
|
||||||
<script src="/socket.io/socket.io.js"></script>
|
<script src="<%= prefix %>/socket.io/socket.io.js"></script>
|
||||||
<script src="/js/libs/jquery.js"></script>
|
<script src="<%= prefix %>/js/libs/jquery.js"></script>
|
||||||
<script src="/js/main.js"></script>
|
<script src="<%= prefix %>/js/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Timer</title>
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="author" content="">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<ul>
|
||||||
|
<% for (const hall of halls) { %>
|
||||||
|
<li>
|
||||||
|
<a href="<%= hall["prefix"] %>"><%= hall["title"] %></a>
|
||||||
|
<ul>
|
||||||
|
<a href="<%= hall["prefix"] %>/control">Control <%= hall["title"] %></a>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<% } %>
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue