Browse Source

1.0.0 beta

prod
Jeffrey Paul 8 months ago
parent
commit
79e1f6e9e3
16 changed files with 342 additions and 86 deletions
  1. +1
    -0
      .dockerignore
  2. +64
    -28
      Dockerfile
  3. +14
    -0
      LICENSE
  4. +2
    -2
      Makefile
  5. +22
    -1
      README.md
  6. +4
    -1
      setup.py
  7. +1
    -0
      tvid/db.py
  8. +64
    -39
      tvid/server.py
  9. +17
    -0
      views/adminpagebase.tpl
  10. +25
    -2
      views/adminpanel.tpl
  11. +4
    -1
      views/base.tpl
  12. +33
    -0
      views/displayeditform.tpl
  13. +29
    -4
      views/displayid.tpl
  14. +11
    -3
      views/loginform.tpl
  15. +41
    -0
      views/navbar.tpl
  16. +10
    -5
      views/style.tpl

+ 1
- 0
.dockerignore View File

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

+ 64
- 28
Dockerfile 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 && \
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 && \
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

ENV PYTHONPATH /var/app
CMD pipenv run python ./bin/tvidd

+ 14
- 0
LICENSE 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.


+ 2
- 2
Makefile 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" .

+ 22
- 1
README.md 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


+ 4
- 1
setup.py 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(),

+ 1
- 0
tvid/db.py View File

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

+ 64
- 39
tvid/server.py 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
- 0
views/adminpagebase.tpl 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>

+ 25
- 2
views/adminpanel.tpl 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>

+ 4
- 1
views/base.tpl 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
- 0
views/displayeditform.tpl 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>

+ 29
- 4
views/displayid.tpl View File

@@ -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>

+ 11
- 3
views/loginform.tpl 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
- 0
views/navbar.tpl 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>

+ 10
- 5
views/style.tpl 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 {

Loading…
Cancel
Save