1.0.0 beta

This commit is contained in:
Jeffrey Paul 2020-03-10 18:52:30 -07:00
parent 691d54ee3e
commit c4ef8d0e96
13 changed files with 302 additions and 85 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 && \
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 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,48 @@ 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
CMD ['pipenv', 'run', 'bin/tvidd']

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

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