From 79e1f6e9e3f3e3d877a11bff435a5146b418d8a7 Mon Sep 17 00:00:00 2001
From: sneak
Date: Tue, 10 Mar 2020 18:52:30 -0700
Subject: [PATCH] 1.0.0 beta
---
.dockerignore | 1 +
Dockerfile | 92 +++++++++++++++++++++++-----------
LICENSE | 14 ++++++
Makefile | 4 +-
README.md | 23 ++++++++-
setup.py | 5 +-
tvid/db.py | 1 +
tvid/server.py | 103 +++++++++++++++++++++++---------------
views/adminpagebase.tpl | 17 +++++++
views/adminpanel.tpl | 27 +++++++++-
views/base.tpl | 5 +-
views/displayeditform.tpl | 33 ++++++++++++
views/displayid.tpl | 35 +++++++++++--
views/loginform.tpl | 14 ++++--
views/navbar.tpl | 41 +++++++++++++++
views/style.tpl | 15 ++++--
16 files changed, 343 insertions(+), 87 deletions(-)
create mode 100644 LICENSE
create mode 100644 views/adminpagebase.tpl
create mode 100644 views/displayeditform.tpl
create mode 100644 views/navbar.tpl
diff --git a/.dockerignore b/.dockerignore
index 947d3f5..e4b29ef 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,3 +1,4 @@
dist/
db.sqlite
build/
+.git
diff --git a/Dockerfile b/Dockerfile
index 1c22bac..ac9b0c3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -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 && \
-mv /etc/apt/sources.list.new /etc/apt/sources.list
+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
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..8c3bdb1
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,14 @@
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+ Version 2, December 2004
+
+Copyright (C) 2004 Sam Hocevar
+
+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.
+
diff --git a/Makefile b/Makefile
index b462c41..7dfb487 100644
--- a/Makefile
+++ b/Makefile
@@ -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" .
diff --git a/README.md b/README.md
index 77fdd65..49902ab 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/setup.py b/setup.py
index 9f04bb2..b6f0be1 100644
--- a/setup.py
+++ b/setup.py
@@ -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(),
diff --git a/tvid/db.py b/tvid/db.py
index 4b66f5d..a71a7aa 100644
--- a/tvid/db.py
+++ b/tvid/db.py
@@ -14,3 +14,4 @@ class TV(Base):
displayid = Column(String(20))
lastSeen = Column(DateTime)
target = Column(String(255))
+ memo = Column(String(255))
diff --git a/tvid/server.py b/tvid/server.py
index 4fec629..cf41ef8 100644
--- a/tvid/server.py
+++ b/tvid/server.py
@@ -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/')
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/')
+ 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)
diff --git a/views/adminpagebase.tpl b/views/adminpagebase.tpl
new file mode 100644
index 0000000..1dbae05
--- /dev/null
+++ b/views/adminpagebase.tpl
@@ -0,0 +1,17 @@
+% rebase('base.tpl', refresh=None, title='tvid administration')
+
+ TVID Admin
+
+
+
+
+
+
+
+ {{!base}}
+
Powered by tvid v{{version}}
+
diff --git a/views/adminpanel.tpl b/views/adminpanel.tpl
index a8aa0c8..4f8acc9 100644
--- a/views/adminpanel.tpl
+++ b/views/adminpanel.tpl
@@ -1,4 +1,27 @@
-% rebase('base.tpl', refresh=None, title='tvid administration')
+% rebase('adminpagebase.tpl', refresh=None, title='tvid administration')
+TVs
+
+
+
+ Display ID
+ Descriptive Memo
+ Last Seen
+ Target URL
+ Edit
+
+
+
-Powered by tvid v{{version}}
+% for tv in tvs:
+
+ {{tv.displayid}}
+ {{tv.memo or '(none)'}}
+ {{tv.lastSeen}}
+ {{tv.target}}
+ Edit
+
+% end
+
+
+
diff --git a/views/base.tpl b/views/base.tpl
index dc302f5..3e9bf9e 100644
--- a/views/base.tpl
+++ b/views/base.tpl
@@ -7,8 +7,11 @@
% if defined('refresh'):
% end
+
% include('htmlheader.tpl')
-
+
+
+
{{!base}}
diff --git a/views/displayeditform.tpl b/views/displayeditform.tpl
new file mode 100644
index 0000000..b8c38fe
--- /dev/null
+++ b/views/displayeditform.tpl
@@ -0,0 +1,33 @@
+% rebase('adminpagebase.tpl', refresh=None, title='tvid administration')
+
+Edit {{tv.displayid}}
+
+
diff --git a/views/displayid.tpl b/views/displayid.tpl
index d52582d..0ae0d77 100644
--- a/views/displayid.tpl
+++ b/views/displayid.tpl
@@ -4,9 +4,34 @@
Display ID:
{{id}}
-
- (They're only letters. I like India, O like
- Oscar.)
-
- Powered by tvid v{{version}}
+ (Letters only: I like INDIA, O like
+ OSCAR.)
+ Powered by tvid v{{version}}
+
+
diff --git a/views/loginform.tpl b/views/loginform.tpl
index 6b82752..ab57fe4 100644
--- a/views/loginform.tpl
+++ b/views/loginform.tpl
@@ -1,8 +1,6 @@
% rebase('base.tpl', refresh=None, title=None)
-% if defined('msg') and msg:
- {{msg}}
-% end
+
@@ -12,6 +10,14 @@
tvid administration
+ % if defined('msg') and msg:
+
+
{{msg}}
+
+ % end
+
+
+
Powered by tvid v{{version}}
+
+
diff --git a/views/navbar.tpl b/views/navbar.tpl
new file mode 100644
index 0000000..6d45b45
--- /dev/null
+++ b/views/navbar.tpl
@@ -0,0 +1,41 @@
+% rebase('base.tpl', refresh=None, title='tvid administration')
+
+ TVID Admin
+
+
+
+
+
+
+
+
TVs
+
+
+
+
+ Display ID
+ Last Seen
+ Target URL
+ Edit
+
+
+
+
+ % for tv in tvs:
+
+ {{tv.displayid}}
+ {{tv.lastSeen}}
+ {{tv.target}}
+ Edit
+
+ % end
+
+
+
+
+
Powered by tvid v{{version}}
+
diff --git a/views/style.tpl b/views/style.tpl
index 601bbb2..8625691 100644
--- a/views/style.tpl
+++ b/views/style.tpl
@@ -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 {