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') + + + +
+ {{!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

+ + + + + + + + + + + -

Powered by tvid v{{version}}

+% for tv in tvs: + + + + + + + +% end + + +
Display IDDescriptive MemoLast SeenTarget URLEdit
{{tv.displayid}}{{tv.memo or '(none)'}}{{tv.lastSeen}}{{tv.target}}Edit
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') + + + +
+

TVs

+ + + + + + + + + + + + + % for tv in tvs: + + + + + + + % end + + +
Display IDLast SeenTarget URLEdit
{{tv.displayid}}{{tv.lastSeen}}{{tv.target}}Edit
+ +

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 {