@@ -1,3 +1,4 @@ | |||
dist/ | |||
db.sqlite | |||
build/ | |||
.git |
@@ -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,48 @@ 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 && \ | |||
git checkout $PYENV_COMMIT | |||
chown app:app /var/app | |||
ENV PYENV_ROOT /usr/local/pyenv | |||
ENV PATH $PYENV_ROOT/bin:$PATH | |||
ENV LANG en_US.UTF-8 | |||
USER app | |||
WORKDIR /home/app | |||
ENV HOME /home/app | |||
#######################################################################33 | |||
RUN git clone https://github.com/pyenv/pyenv.git $HOME/.pyenv && \ | |||
cd $HOME/.pyenv && \ | |||
git checkout $PYENV_COMMIT | |||
ENV PYENV_ROOT $HOME/.pyenv | |||
ENV PATH $PYENV_ROOT/shims:$PYENV_ROOT/bin:$PATH | |||
################################################################################ | |||
## 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 | |||
CMD ['pipenv', 'run', 'bin/tvidd'] |
@@ -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" . |
@@ -14,3 +14,4 @@ class TV(Base): | |||
displayid = Column(String(20)) | |||
lastSeen = Column(DateTime) | |||
target = Column(String(255)) | |||
memo = Column(String(255)) |
@@ -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) |
@@ -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> |
@@ -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> |
@@ -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}} |
@@ -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> |
@@ -4,9 +4,34 @@ | |||
<i>Display ID:</i> | |||
</p> | |||
<h1>{{id}}</h1> | |||
<small>(Letters only: I like INDIA, O like | |||
OSCAR.)<br/> | |||
Powered by tvid v{{version}}</small> | |||
</div> | |||
<small>(They're only letters. I like India, O like | |||
Oscar.)</small> | |||
<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; | |||
} | |||
} | |||
<p><small>Powered by tvid v{{version}}</small></p> | |||
</div> | |||
$(document).ready(function() { | |||
var b = document.getElementsByTagName("BODY")[0] | |||
b.style.background = '#666666'; | |||
console.log("starting..."); | |||
setInterval(animate, 20); | |||
}); | |||
</script> |
@@ -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> |
@@ -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> |
@@ -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 { |