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/
db.sqlite
build/
.git

View File

@ -1,27 +1,43 @@
# focal amd64 as of 2020-03-10
FROM ubuntu@sha256:d050ed7278c16ff627e4a70d7d353f1a2ec74d8a0b66e5a865356d92f5f6d87b
#######################################################################33
################################################################################
## Mirror Setup
## - option to use local mirror to speed build
#######################################################################33
################################################################################
ARG UBUNTU_MIRROR=http://archive.ubuntu.com/ubuntu
RUN echo "deb $UBUNTU_MIRROR focal main universe restricted multiverse" > /etc/apt/sources.list.new && \
echo "deb $UBUNTU_MIRROR focal-updates main universe restricted multiverse" >> /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-backports main universe restricted multiverse" >> /etc/apt/sources.list.new && \
RUN echo "deb $UBUNTU_MIRROR focal main universe restricted multiverse" > \
/etc/apt/sources.list.new && \
echo "deb $UBUNTU_MIRROR focal-updates main universe restricted multiverse" >> \
/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-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
#######################################################################33
################################################################################
# master as of 2020-03-10
ARG PYENV_COMMIT=df9fa1dc30b6448ef8605e2c2d4dfc2a94d6a35d
ARG PYTHON_VERSION=3.8.1
#######################################################################33
################################################################################
## Packages
#######################################################################33
################################################################################
ARG DEBIAN_FRONTEND=noninteractive
RUN apt update && \
apt upgrade -y && \
@ -39,6 +55,7 @@ RUN apt update && \
libssl-dev \
llvm \
locales \
locales \
make \
python-openssl \
tk-dev \
@ -46,30 +63,49 @@ RUN apt update && \
xz-utils \
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 && \
git clone https://github.com/pyenv/pyenv.git /usr/local/pyenv && \
cd /usr/local/pyenv && \
chown app:app /var/app
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
ENV PYENV_ROOT /usr/local/pyenv
ENV PATH $PYENV_ROOT/bin:$PATH
ENV PYENV_ROOT $HOME/.pyenv
ENV PATH $PYENV_ROOT/shims:$PYENV_ROOT/bin:$PATH
#######################################################################33
################################################################################
## Python
#######################################################################33
################################################################################
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
#######################################################################33
ADD ./Pipfile ./Pipfile.lock /var/app/
################################################################################
## Install App Deps
################################################################################j
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
#######################################################################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:
pipenv install --python $(shell which python3)
serve:
develop:
pipenv run python ./bin/tvidd
clean:
rm -rf build dist tvid.egg-info
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
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
* 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

View File

@ -1,8 +1,11 @@
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(
name='tvid',
version='0.0.1',
version='1.0.0',
packages=find_packages(),
license='WTFPL',
long_description=open('README.md').read(),

View File

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

View File

@ -19,9 +19,9 @@ import urllib.parse
import os
import random
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)
DEBUG = os.environ.get('DEBUG', False)
SQLITE_FILENAME = os.environ.get('SQLITE_FILENAME','/data/db.sqlite')
@ -46,14 +46,14 @@ def serve():
# pull in models
from .db import TV
engine = create_engine(DATABASE_URL, echo=True)
engine = create_engine(DATABASE_URL, echo=False)
plugin = sqlalchemy.Plugin(
engine, # SQLAlchemy engine created with create_engine function.
SQLBASE.metadata, # SQLAlchemy metadata, required only if create=True.
keyword='db', # Keyword used to inject session database in a route (default 'db').
create=True, # If it is true, execute `metadata.create_all(engine)` when plugin is applied (default False).
commit=True, # If it is true, plugin commit changes after route is executed (default True).
engine,
SQLBASE.metadata,
keyword='db',
create=True,
commit=True,
use_kwargs=False
)
@ -63,17 +63,18 @@ def serve():
# cookie'd value (whether preexisting or new).
@app.get('/')
def indexpage():
c = request.get_cookie("tvid")
c = request.get_cookie("displayid")
if c:
# redirect
redirect('/tv/' + c)
return redirect('/tv/' + c)
else:
newid = genRandomTVID()
response.set_cookie("tvid", newid)
redirect('/tv/' + newid)
response.set_cookie("displayid", newid)
return redirect('/tv/' + newid)
@app.get('/style.css')
def stylesheet():
response.content_type = 'text/css'
return template('style')
# 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.
@app.get('/tv/<displayid>')
def tvpage(db, displayid=None):
# FIXME regex check id to make sure displayid is right format,
# return error if not
if id is None:
# FIXME check for cookie, this is broken
if displayid is None:
return template('nocookie')
# check db for tv id
@ -93,7 +92,7 @@ def serve():
tv.lastSeen = datetime.now()
db.add(tv)
if tv.target:
redirect(tv.target)
return redirect(tv.target)
else:
return template('displayid', id=displayid, version=VERSION)
else:
@ -105,6 +104,40 @@ def serve():
db.add(newtv)
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
# if logged out then redirect to /login
# FIXME make this use sessions instead of just storing PSK in a cookie
@ -113,24 +146,19 @@ def serve():
def adminpage(db):
c = request.get_cookie("psk")
if not c:
redirect('/login')
return
return redirect('/login')
if c != ADMIN_PSK:
redirect('/logout')
return
tvs = db.query(TV).order_by(TV.lastSeen)
return redirect('/logout')
# 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)
@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:
@app.get('/login')
@ -142,25 +170,22 @@ def serve():
def checklogin():
attemptedPass = request.forms.get('password')
if not attemptedPass:
redirect(
return redirect(
'/login?msg=' +
urllib.parse.quote_plus(u"Incorrect password.")
)
return
if attemptedPass != ADMIN_PSK:
redirect(
return redirect(
'/login?msg=' +
urllib.parse.quote_plus(u"Incorrect password.")
)
return
# password is right, cookie them:
response.set_cookie("psk", attemptedPass)
redirect('/admin')
return
return redirect('/admin')
@app.get('/logout')
def logout():
response.set_cookie("psk", "")
redirect('/login')
return redirect('/login')
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'):
<meta http-equiv="refresh" content="{{ get('refresh',60) }}">
% end
% include('htmlheader.tpl')
<link rel="stylesheet" href="/style.css" />
<link rel="stylesheet" href="/style.css" type="text/css">
</head>
<body>
{{!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>
</p>
<h1>{{id}}</h1>
<small>(They're only letters. I like India, O like
Oscar.)</small>
<p><small>Powered by tvid v{{version}}</small></p>
<small>(Letters only: I like INDIA, O like
OSCAR.)<br/>
Powered by tvid v{{version}}</small>
</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)
% if defined('msg') and msg:
<div class="card card-body bg-light">{{msg}}</div>
% end
<div class="container">
<div class="wrapper fadeInDown">
<div id="formContent">
@ -12,6 +10,14 @@
<h2>tvid administration</h2>
</div>
% if defined('msg') and msg:
<p>
<div class="card card-body bg-light">{{msg}}</div>
</p>
% end
<!-- Login Form -->
<form action="/checklogin" method="post">
<input type="password" id="password" class="fadeIn third"
@ -22,3 +28,5 @@
</div>
</div>
<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 {
font-family: sans-serif;
#main {
background: #fff;
width: 100%;
text-align: center;
font-size: 20pt
font-size: 36pt;
margin-top: 5em;
}
h1 {
font-size: 48pt;
#main h1 {
font-size: 72pt;
}
#main small {
font-size: 14pt;
}
a {