1.0.0 beta

This commit is contained in:
Jeffrey Paul 2020-03-10 18:52:30 -07:00
parent 691d54ee3e
commit 79e1f6e9e3
16 changed files with 343 additions and 87 deletions

View File

@ -1,3 +1,4 @@
dist/ dist/
db.sqlite db.sqlite
build/ build/
.git

View File

@ -1,27 +1,43 @@
# focal amd64 as of 2020-03-10 # focal amd64 as of 2020-03-10
FROM ubuntu@sha256:d050ed7278c16ff627e4a70d7d353f1a2ec74d8a0b66e5a865356d92f5f6d87b FROM ubuntu@sha256:d050ed7278c16ff627e4a70d7d353f1a2ec74d8a0b66e5a865356d92f5f6d87b
#######################################################################33 ################################################################################
## Mirror Setup ## Mirror Setup
## - option to use local mirror to speed build ## - option to use local mirror to speed build
#######################################################################33 ################################################################################
ARG UBUNTU_MIRROR=http://archive.ubuntu.com/ubuntu ARG UBUNTU_MIRROR=http://archive.ubuntu.com/ubuntu
RUN echo "deb $UBUNTU_MIRROR focal main universe restricted multiverse" > /etc/apt/sources.list.new && \ RUN echo "deb $UBUNTU_MIRROR focal main universe restricted multiverse" > \
echo "deb $UBUNTU_MIRROR focal-updates main universe restricted multiverse" >> /etc/apt/sources.list.new && \ /etc/apt/sources.list.new && \
echo "deb $UBUNTU_MIRROR focal-security main universe restricted multiverse" >> /etc/apt/sources.list.new && \ echo "deb $UBUNTU_MIRROR focal-updates main universe restricted multiverse" >> \
echo "deb $UBUNTU_MIRROR focal-backports main universe restricted multiverse" >> /etc/apt/sources.list.new && \ /etc/apt/sources.list.new && \
mv /etc/apt/sources.list.new /etc/apt/sources.list echo "deb $UBUNTU_MIRROR focal-security main universe restricted multiverse" >> \
/etc/apt/sources.list.new && \
echo "deb $UBUNTU_MIRROR focal-backports main universe restricted multiverse" >> \
/etc/apt/sources.list.new && \
mv /etc/apt/sources.list.new /etc/apt/sources.list
#######################################################################33
ARG UID=61000
ARG GID=61000
RUN groupadd \
--system --gid $GID \
app && \
useradd \
--system --gid $GID --uid $UID \
--no-log-init -m -s /bin/false --home-dir /home/app \
app
################################################################################
## Versions ## Versions
#######################################################################33 ################################################################################
# master as of 2020-03-10 # master as of 2020-03-10
ARG PYENV_COMMIT=df9fa1dc30b6448ef8605e2c2d4dfc2a94d6a35d ARG PYENV_COMMIT=df9fa1dc30b6448ef8605e2c2d4dfc2a94d6a35d
ARG PYTHON_VERSION=3.8.1 ARG PYTHON_VERSION=3.8.1
#######################################################################33 ################################################################################
## Packages ## Packages
#######################################################################33 ################################################################################
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
RUN apt update && \ RUN apt update && \
apt upgrade -y && \ apt upgrade -y && \
@ -39,6 +55,7 @@ RUN apt update && \
libssl-dev \ libssl-dev \
llvm \ llvm \
locales \ locales \
locales \
make \ make \
python-openssl \ python-openssl \
tk-dev \ tk-dev \
@ -46,30 +63,49 @@ RUN apt update && \
xz-utils \ xz-utils \
zlib1g-dev \ zlib1g-dev \
&& \ && \
echo 'en_US.UTF-8 UTF-8' > /etc/locale.gen && \
dpkg-reconfigure --frontend=noninteractive locales && \
update-locale LANG=en_US.UTF-8 && \
mkdir -p /var/app && \ mkdir -p /var/app && \
git clone https://github.com/pyenv/pyenv.git /usr/local/pyenv && \ chown app:app /var/app
cd /usr/local/pyenv && \
ENV LANG en_US.UTF-8
USER app
WORKDIR /home/app
ENV HOME /home/app
RUN git clone https://github.com/pyenv/pyenv.git $HOME/.pyenv && \
cd $HOME/.pyenv && \
git checkout $PYENV_COMMIT git checkout $PYENV_COMMIT
ENV PYENV_ROOT /usr/local/pyenv ENV PYENV_ROOT $HOME/.pyenv
ENV PATH $PYENV_ROOT/bin:$PATH ENV PATH $PYENV_ROOT/shims:$PYENV_ROOT/bin:$PATH
################################################################################
#######################################################################33
## Python ## Python
#######################################################################33 ################################################################################
RUN pyenv install $PYTHON_VERSION && \ RUN pyenv install $PYTHON_VERSION && \
pyenv global $PYTHON_VERISON pyenv global $PYTHON_VERSION && \
pyenv rehash && \
pip install --upgrade pip && \
pip install pipenv
RUN ls $PYENV_ROOT/bin/
#######################################################################33 ################################################################################
## Install Deps ## Install App Deps
#######################################################################33 ################################################################################j
ADD ./Pipfile ./Pipfile.lock /var/app/
WORKDIR /var/app WORKDIR /var/app
RUN pipenv install --python $(which python3) COPY ./Pipfile ./Pipfile.lock /var/app/
RUN pipenv install --python $PYENV_ROOT/shims/python
#######################################################################33 ################################################################################
## Install App ## Install App
#######################################################################33 ################################################################################
ADD ./* /var/app/ COPY . /var/app
VOLUME /data
ENV PYTHONPATH /var/app
CMD pipenv run python ./bin/tvidd

14
LICENSE Normal file
View File

@ -0,0 +1,14 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.

View File

@ -7,11 +7,11 @@ default: docker
peinstall: peinstall:
pipenv install --python $(shell which python3) pipenv install --python $(shell which python3)
serve: develop:
pipenv run python ./bin/tvidd pipenv run python ./bin/tvidd
clean: clean:
rm -rf build dist tvid.egg-info rm -rf build dist tvid.egg-info
docker: docker:
docker build --build-arg UBUNTU_MIRROR="http://ubuntu.datavi.be/ubuntu" . docker build -t sneak/tvid --build-arg UBUNTU_MIRROR="http://ubuntu.datavi.be/ubuntu" .

View File

@ -9,9 +9,30 @@ Log in to the admin panel and enter the URL for that TV ID, and within 60
seconds, that display will bounce to that URL, or any other time it turns seconds, that display will bounce to that URL, or any other time it turns
on. on.
You can reconfigure the target URL at any time, and the next time that
display reboots or reloads (you should be rebooting your displays daily) it
will get redirected to the new target.
# configuration knobs
## environment variables
* set `ADMIN_PSK` to the admin password (for `/admin` url)
## state storage
* writes sqlite database into `/data`, mount that volume somewhere
# todo # todo
* make display id animate: https://codepen.io/stezu/pen/cmLrI * fix CSRF
* fix FIXMEs
* fix logging output
* put git short id into version string
# license
WTFPL
# author # author

View File

@ -1,8 +1,11 @@
from setuptools import setup, find_packages from setuptools import setup, find_packages
# for some reason that console_scripts entrypoint fails
# and i have no idea why so there is a bin in bin/
setup( setup(
name='tvid', name='tvid',
version='0.0.1', version='1.0.0',
packages=find_packages(), packages=find_packages(),
license='WTFPL', license='WTFPL',
long_description=open('README.md').read(), long_description=open('README.md').read(),

View File

@ -14,3 +14,4 @@ class TV(Base):
displayid = Column(String(20)) displayid = Column(String(20))
lastSeen = Column(DateTime) lastSeen = Column(DateTime)
target = Column(String(255)) target = Column(String(255))
memo = Column(String(255))

View File

@ -19,9 +19,9 @@ import urllib.parse
import os import os
import random import random
import string import string
from datetime import datetime from datetime import datetime, timedelta
VERSION = '0.0.1' VERSION = '1.0.0'
PORT = os.environ.get('PORT', 8080) PORT = os.environ.get('PORT', 8080)
DEBUG = os.environ.get('DEBUG', False) DEBUG = os.environ.get('DEBUG', False)
SQLITE_FILENAME = os.environ.get('SQLITE_FILENAME','/data/db.sqlite') SQLITE_FILENAME = os.environ.get('SQLITE_FILENAME','/data/db.sqlite')
@ -46,14 +46,14 @@ def serve():
# pull in models # pull in models
from .db import TV from .db import TV
engine = create_engine(DATABASE_URL, echo=True) engine = create_engine(DATABASE_URL, echo=False)
plugin = sqlalchemy.Plugin( plugin = sqlalchemy.Plugin(
engine, # SQLAlchemy engine created with create_engine function. engine,
SQLBASE.metadata, # SQLAlchemy metadata, required only if create=True. SQLBASE.metadata,
keyword='db', # Keyword used to inject session database in a route (default 'db'). keyword='db',
create=True, # If it is true, execute `metadata.create_all(engine)` when plugin is applied (default False). create=True,
commit=True, # If it is true, plugin commit changes after route is executed (default True). commit=True,
use_kwargs=False use_kwargs=False
) )
@ -63,17 +63,18 @@ def serve():
# cookie'd value (whether preexisting or new). # cookie'd value (whether preexisting or new).
@app.get('/') @app.get('/')
def indexpage(): def indexpage():
c = request.get_cookie("tvid") c = request.get_cookie("displayid")
if c: if c:
# redirect # redirect
redirect('/tv/' + c) return redirect('/tv/' + c)
else: else:
newid = genRandomTVID() newid = genRandomTVID()
response.set_cookie("tvid", newid) response.set_cookie("displayid", newid)
redirect('/tv/' + newid) return redirect('/tv/' + newid)
@app.get('/style.css') @app.get('/style.css')
def stylesheet(): def stylesheet():
response.content_type = 'text/css'
return template('style') return template('style')
# here we check to see if they have a redirect URL in the db. if they do # here we check to see if they have a redirect URL in the db. if they do
@ -81,10 +82,8 @@ def serve():
# reloading the page once per hour. # reloading the page once per hour.
@app.get('/tv/<displayid>') @app.get('/tv/<displayid>')
def tvpage(db, displayid=None): def tvpage(db, displayid=None):
# FIXME regex check id to make sure displayid is right format, # FIXME check for cookie, this is broken
# return error if not if displayid is None:
if id is None:
return template('nocookie') return template('nocookie')
# check db for tv id # check db for tv id
@ -93,7 +92,7 @@ def serve():
tv.lastSeen = datetime.now() tv.lastSeen = datetime.now()
db.add(tv) db.add(tv)
if tv.target: if tv.target:
redirect(tv.target) return redirect(tv.target)
else: else:
return template('displayid', id=displayid, version=VERSION) return template('displayid', id=displayid, version=VERSION)
else: else:
@ -105,6 +104,40 @@ def serve():
db.add(newtv) db.add(newtv)
return template('displayid', id=displayid, version=VERSION) return template('displayid', id=displayid, version=VERSION)
@app.get('/admin/edit/<displayid>')
def displayeditform(db, displayid=None):
c = request.get_cookie("psk")
if not c:
return redirect('/login')
if c != ADMIN_PSK:
return redirect('/logout')
if not displayid:
return redirect('/admin')
tv = db.query(TV).filter_by(displayid=displayid).first()
if tv is None:
return redirect('/admin')
return template('displayeditform', tv=tv, version=VERSION)
@app.post('/admin/edit')
def displayedithandler(db):
# FIXME SECURITY csrf issue
c = request.get_cookie("psk")
if not c:
return redirect('/login')
if c != ADMIN_PSK:
return redirect('/logout')
displayid = request.forms.get('displayid')
tv = db.query(TV).filter_by(displayid=displayid).first()
if tv is None:
return redirect('/admin')
# FIXME make sure this is a valid URL
tv.target = request.forms.get('target')
tv.memo = request.forms.get('formmemo')
db.add(tv)
db.commit()
return redirect('/admin')
# here we display the administration list of TVs if logged in # here we display the administration list of TVs if logged in
# if logged out then redirect to /login # if logged out then redirect to /login
# FIXME make this use sessions instead of just storing PSK in a cookie # FIXME make this use sessions instead of just storing PSK in a cookie
@ -113,24 +146,19 @@ def serve():
def adminpage(db): def adminpage(db):
c = request.get_cookie("psk") c = request.get_cookie("psk")
if not c: if not c:
redirect('/login') return redirect('/login')
return
if c != ADMIN_PSK: if c != ADMIN_PSK:
redirect('/logout') return redirect('/logout')
return
tvs = db.query(TV).order_by(TV.lastSeen) # first, cleanup db of old entries:
week_ago = datetime.now() - timedelta(days=7)
db.query(TV).filter(TV.lastSeen < week_ago).delete()
db.commit()
tvs = db.query(TV).order_by(TV.lastSeen.desc())
response.headers['Cache-Control'] = 'no-cache'
return template('adminpanel', tvs=tvs, version=VERSION) return template('adminpanel', tvs=tvs, version=VERSION)
@app.post('/admin')
def savesettings():
c = request.get_cookie("psk")
if not c:
redirect('/login')
return
if c != ADMIN_PSK:
redirect('/logout')
return
raise NotImplementedError()
# here we ask for a password: # here we ask for a password:
@app.get('/login') @app.get('/login')
@ -142,25 +170,22 @@ def serve():
def checklogin(): def checklogin():
attemptedPass = request.forms.get('password') attemptedPass = request.forms.get('password')
if not attemptedPass: if not attemptedPass:
redirect( return redirect(
'/login?msg=' + '/login?msg=' +
urllib.parse.quote_plus(u"Incorrect password.") urllib.parse.quote_plus(u"Incorrect password.")
) )
return
if attemptedPass != ADMIN_PSK: if attemptedPass != ADMIN_PSK:
redirect( return redirect(
'/login?msg=' + '/login?msg=' +
urllib.parse.quote_plus(u"Incorrect password.") urllib.parse.quote_plus(u"Incorrect password.")
) )
return
# password is right, cookie them: # password is right, cookie them:
response.set_cookie("psk", attemptedPass) response.set_cookie("psk", attemptedPass)
redirect('/admin') return redirect('/admin')
return
@app.get('/logout') @app.get('/logout')
def logout(): def logout():
response.set_cookie("psk", "") response.set_cookie("psk", "")
redirect('/login') return redirect('/login')
app.run(host='0.0.0.0', port=PORT, debug=DEBUG) app.run(host='0.0.0.0', port=PORT, debug=DEBUG)

17
views/adminpagebase.tpl Normal file
View File

@ -0,0 +1,17 @@
% rebase('base.tpl', refresh=None, title='tvid administration')
<nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4">
<a class="navbar-brand" href="/admin">TVID Admin</a>
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="btn btn-secondary" href="/logout">Log Out</a>
</li>
</ul>
</nav>
<div class="container" id="adminpanel">
{{!base}}
<p><small>Powered by tvid v{{version}}</small></p>
</div>

View File

@ -1,4 +1,27 @@
% rebase('base.tpl', refresh=None, title='tvid administration') % rebase('adminpagebase.tpl', refresh=None, title='tvid administration')
<h1>TVs</h1>
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th scope="col">Display ID</th>
<th scope="col">Descriptive Memo</th>
<th scope="col">Last Seen</th>
<th scope="col">Target URL</th>
<th scope="col">Edit</th>
</tr>
</thead>
<tbody>
<p><small>Powered by tvid v{{version}}</small></p> % for tv in tvs:
<tr>
<th scope="row">{{tv.displayid}}</th>
<td>{{tv.memo or '(none)'}}</td>
<td>{{tv.lastSeen}}</td>
<td>{{tv.target}}</td>
<td><a href="/admin/edit/{{tv.displayid}}" class="btn btn-success btn-sm">Edit</a></td>
</tr>
% end
</tbody>
</table>

View File

@ -7,8 +7,11 @@
% if defined('refresh'): % if defined('refresh'):
<meta http-equiv="refresh" content="{{ get('refresh',60) }}"> <meta http-equiv="refresh" content="{{ get('refresh',60) }}">
% end % end
% include('htmlheader.tpl') % include('htmlheader.tpl')
<link rel="stylesheet" href="/style.css" />
<link rel="stylesheet" href="/style.css" type="text/css">
</head> </head>
<body> <body>
{{!base}} {{!base}}

33
views/displayeditform.tpl Normal file
View File

@ -0,0 +1,33 @@
% rebase('adminpagebase.tpl', refresh=None, title='tvid administration')
<h1>Edit {{tv.displayid}}</h1>
<div class="container" style="width: 50%">
<form action="/admin/edit" method="post">
<div class="form-group">
<label for="displayid">Display ID</label>
<input type="text" class="form-control" id="displayid"
value="{{tv.displayid}}"
name="displayid"
placeholder="{{tv.displayid}}"
readonly>
</div>
<div class="form-group">
<label for="formmemo">Descriptive Memo</label>
<input type="text" class="form-control" id="formmemo"
value="{{tv.memo}}"
name="formmemo"
placeholder="description/location"
>
</div>
<div class="form-group">
<label for="targeturl">Target URL</label>
<input type="text" class="form-control" id="targeturl" name="target" placeholder="https://example.com">
</div>
<button type="submit" class="btn btn-primary">Save</button>
</form>
</div>

View File

@ -4,9 +4,34 @@
<i>Display ID:</i> <i>Display ID:</i>
</p> </p>
<h1>{{id}}</h1> <h1>{{id}}</h1>
<small>(Letters only: I like INDIA, O like
<small>(They're only letters. I like India, O like OSCAR.)<br/>
Oscar.)</small> Powered by tvid v{{version}}</small>
<p><small>Powered by tvid v{{version}}</small></p>
</div> </div>
<script>
var x = 0;
var factor = 1;
function animate() {
console.log("animate called");
var elem = document.getElementById("main");
console.log(elem);
console.log(elem.style);
elem.style.marginTop = x + 'px';
x = x + factor;
if(x > 1080) {
factor = -1;
}
if(x < 0) {
factor = 1;
x = 0;
}
}
$(document).ready(function() {
var b = document.getElementsByTagName("BODY")[0]
b.style.background = '#666666';
console.log("starting...");
setInterval(animate, 20);
});
</script>

View File

@ -1,8 +1,6 @@
% rebase('base.tpl', refresh=None, title=None) % rebase('base.tpl', refresh=None, title=None)
% if defined('msg') and msg: <div class="container">
<div class="card card-body bg-light">{{msg}}</div>
% end
<div class="wrapper fadeInDown"> <div class="wrapper fadeInDown">
<div id="formContent"> <div id="formContent">
@ -12,6 +10,14 @@
<h2>tvid administration</h2> <h2>tvid administration</h2>
</div> </div>
% if defined('msg') and msg:
<p>
<div class="card card-body bg-light">{{msg}}</div>
</p>
% end
<!-- Login Form --> <!-- Login Form -->
<form action="/checklogin" method="post"> <form action="/checklogin" method="post">
<input type="password" id="password" class="fadeIn third" <input type="password" id="password" class="fadeIn third"
@ -22,3 +28,5 @@
</div> </div>
</div> </div>
<p><small>Powered by tvid v{{version}}</small></p> <p><small>Powered by tvid v{{version}}</small></p>
</div>

41
views/navbar.tpl Normal file
View File

@ -0,0 +1,41 @@
% rebase('base.tpl', refresh=None, title='tvid administration')
<nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4">
<a class="navbar-brand" href="/admin">TVID Admin</a>
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="btn btn-secondary" href="/logout">Log Out</a>
</li>
</ul>
</nav>
<div class="container" id="adminpanel">
<h1>TVs</h1>
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th scope="col">Display ID</th>
<th scope="col">Last Seen</th>
<th scope="col">Target URL</th>
<th scope="col">Edit</th>
</tr>
</thead>
<tbody>
% for tv in tvs:
<tr>
<th scope="row">{{tv.displayid}}</th>
<td>{{tv.lastSeen}}</td>
<td>{{tv.target}}</td>
<td><a href="/admin/edit/{{tv.displayid}}" class="btn btn-success btn-sm">Edit</a></td>
</tr>
% end
</tbody>
</table>
<p><small>Powered by tvid v{{version}}</small></p>
</div>

View File

@ -1,12 +1,17 @@
body { #main {
font-family: sans-serif; background: #fff;
width: 100%; width: 100%;
text-align: center; text-align: center;
font-size: 20pt font-size: 36pt;
margin-top: 5em;
} }
h1 { #main h1 {
font-size: 48pt; font-size: 72pt;
}
#main small {
font-size: 14pt;
} }
a { a {