diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d3fd69a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,24 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +# Matches multiple files with brace expansion notation +# Set default charset +[*.{js,py}] +charset = utf-8 + +# 4 space indentation +[*.py] +indent_style = space +indent_size = 4 + +[*.html] +indent_style = space +indent_size = 2 diff --git a/.gitignore b/.gitignore index 7f7cccc..5c6f97b 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,9 @@ docs/_build/ # PyBuilder target/ +# PyCharm +.idea/ + +# Other +venv/ +*.sqlite diff --git a/LICENSE b/LICENSE index 472ac23..8db1e3f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ MIT License -Copyright (c) +Copyright (c) 2020 Arti Zirk Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/README.md b/README.md index 377eefd..452ec6d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ -# doors +# K-Doors + +[K-Space](https://k-space.ee) door system. This repo contains two sub projects: + +* [K-Door-Web](kdoorweb) - Central door admin web interface +* [K-Door-Pi](kdoorpi) - Door client that runs on Raspberry Pi and talks to the hardware. -K-Space door system \ No newline at end of file diff --git a/doors/__main__.py b/doors/__main__.py deleted file mode 100644 index e69de29..0000000 diff --git a/doors/db.py b/doors/db.py deleted file mode 100644 index 31471dd..0000000 --- a/doors/db.py +++ /dev/null @@ -1,10 +0,0 @@ -import sqlite3 - - -class DB: - - def __init__(self, path): - self.path = path - - - diff --git a/kdoorpi/README.md b/kdoorpi/README.md new file mode 100644 index 0000000..de44a34 --- /dev/null +++ b/kdoorpi/README.md @@ -0,0 +1,15 @@ +# K-Door-Pi + +Client for K-Space door system. Run on Raspberry Pi and controlls the doors + +# Requirements + +This project needs at least Linux kernel 4.8 and uses crossplatform [libgpiod] +to talk to the hardware. Under Debian/Ubuntu you have to install [python3-libgpiod]. + + sudo apt install python3-libgpiod + + +[libgpiod]: https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/about/ +[python3-libgpiod]: https://packages.debian.org/sid/python3-libgpiod + diff --git a/doors/__init__.py b/kdoorpi/kdoorpi/__init__.py similarity index 100% rename from doors/__init__.py rename to kdoorpi/kdoorpi/__init__.py diff --git a/kdoorpi/pyproject.toml b/kdoorpi/pyproject.toml new file mode 100644 index 0000000..121a39f --- /dev/null +++ b/kdoorpi/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools >= 40.6.0", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/kdoorpi/setup.py b/kdoorpi/setup.py new file mode 100644 index 0000000..95f3eb0 --- /dev/null +++ b/kdoorpi/setup.py @@ -0,0 +1,23 @@ +from setuptools import find_packages, setup + +setup( + name='kdoorpi', + version='0.0.0', + author="Arti Zirk", + author_email="arti@zirk.me", + description="K-Space Door client that talks to the hardware", + packages=find_packages(), + include_package_data=True, + zip_safe=False, + python_requires='>=3.5', + install_requires=[], + extras_require={}, + classifiers=[ + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3 :: Only', + 'Topic :: System :: Networking', + 'Intended Audience :: System Administrators', + ] + +) diff --git a/kdoorweb/MANIFEST.in b/kdoorweb/MANIFEST.in new file mode 100644 index 0000000..8ccd2c8 --- /dev/null +++ b/kdoorweb/MANIFEST.in @@ -0,0 +1,3 @@ +graft update_service/static +graft update_service/views +global-exclude *.py[cod] diff --git a/kdoorweb/README.md b/kdoorweb/README.md new file mode 100644 index 0000000..bcc8a75 --- /dev/null +++ b/kdoorweb/README.md @@ -0,0 +1,31 @@ +# K-Door-web + +# Development setup + + python3 -m venv venv + source venv/bin/activate + pip install -r requirements.txt + +## Initialize database + + source venv/bin/activate + + + +## Run dev server + + source venv/bin/activate + bottle.py --debug --reload kdoorweb:application + +## Run unittests + + source venv/bin/activate + python -m unittest discover tests + +## Update requirements.txt + + source venv/bin/activate + pip-compile --upgrade setup.py + pip-compile --upgrade dev-requirements.in + # Apply updates to current venv + pip-sync dev-requirements.txt requirements.txt diff --git a/kdoorweb/dev-requirements.in b/kdoorweb/dev-requirements.in new file mode 100644 index 0000000..def62ac --- /dev/null +++ b/kdoorweb/dev-requirements.in @@ -0,0 +1,2 @@ +-c requirements.txt +pip-tools diff --git a/kdoorweb/dev-requirements.txt b/kdoorweb/dev-requirements.txt new file mode 100644 index 0000000..fa61895 --- /dev/null +++ b/kdoorweb/dev-requirements.txt @@ -0,0 +1,12 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile dev-requirements.in +# +click==7.1.2 # via pip-tools +pip-tools==5.3.1 # via -r dev-requirements.in +six==1.15.0 # via pip-tools + +# The following packages are considered to be unsafe in a requirements file: +# pip diff --git a/kdoorweb/kdoorweb/__init__.py b/kdoorweb/kdoorweb/__init__.py new file mode 100644 index 0000000..d105fe5 --- /dev/null +++ b/kdoorweb/kdoorweb/__init__.py @@ -0,0 +1 @@ +from .web import application diff --git a/kdoorweb/kdoorweb/__main__.py b/kdoorweb/kdoorweb/__main__.py new file mode 100644 index 0000000..bd87b33 --- /dev/null +++ b/kdoorweb/kdoorweb/__main__.py @@ -0,0 +1,9 @@ +import sys +from . import application + +if __name__ == "__main__": + if len(sys.argv) > 1: + port = int(sys.argv[1]) + else: + port = 8080 + application.run(host='127.0.0.1', port=port) diff --git a/kdoorweb/kdoorweb/db.py b/kdoorweb/kdoorweb/db.py new file mode 100644 index 0000000..9359198 --- /dev/null +++ b/kdoorweb/kdoorweb/db.py @@ -0,0 +1,93 @@ +import datetime +import sqlite3 + +SCHEMA_VERSION = 1 + + +class DB: + + def __init__(self, dbfile=":memory:"): + self.dbfile = dbfile + self.db = sqlite3.connect(self.dbfile) + + @staticmethod + def create_db(dbfile): + db = sqlite3.connect(dbfile) + db.executescript(""" + create table versions ( + version integer, + upgraded text + ); + create table users ( + id integer primary key, + user text, + real_name text, + email text, + disabled integer, + admin integer + ); + create table keycards ( + id integer primary key, + user_id integer, + card_uid blob, + name text, + created text, + + foreign key (user_id) + references users (id) + on delete cascade + ); + create table doors ( + id integer primary key, + name text, + note text, + api_key text, + created text, + disabled integer + ); + create table door_log ( + id integer primary key, + timestamp integer, + door_id integer, + keycard_id integer, + user_id integer, + + foreign key (door_id) + references doors (id) + on delete set null, + foreign key (user_id) + references users (id) + on delete set null, + foreign key (keycard_id) + references keycards (id) + on delete set null + ) + """) + db.execute( + "insert into versions (version, upgraded) values (?, ?)", + (SCHEMA_VERSION, str(datetime.datetime.now())) + ) + db.commit() + + def list_users(*, page=0, count=20, query=None): + pass + + def add_user(self, user, real_name=None, email=None, disabled=False, admin=False): + self.add_users([( + user, real_name, email, disabled, admin + )]) + + def add_users(self, users): + self.db.executemany(""" + insert into users(user, real_name, email, disabled, admin) + values(?, ?, ?, ?, ?) + """, users) + self.db.commit() + + + + +if __name__ == "__main__": + #DB.create_db("kdoorweb.sqlite") + db = DB("kdoorweb.sqlite") + db.add_user("juku") diff --git a/kdoorweb/kdoorweb/static/artistyle.css b/kdoorweb/kdoorweb/static/artistyle.css new file mode 100644 index 0000000..b178313 --- /dev/null +++ b/kdoorweb/kdoorweb/static/artistyle.css @@ -0,0 +1,152 @@ +html { + box-sizing: border-box; +} +*, *:before, *:after { + box-sizing: inherit; +} + +body { + margin: 40px auto; + max-width: 650px; + line-height: 1.6; + font-size: 18px; + color: #444; + padding: 0 10px; +} + +h1, h2, h3 { + line-height: 1.2 +} + +hr { + display: block; + height: 1px; + border: 0; + border-top: 1px solid #ccc; + margin: 1em 0; + padding: 0; +} + +header { + display: flex; + align-items: flex-end; + justify-content: space-between; + flex-wrap: wrap; +} + +header h1 { + margin: 0; +} + +.page-header { + margin-bottom: 1.5em; +} + +.page-header h1, .page-header h2, .page-header h3{ + margin: 0; +} + +table { + table-layout: fixed; + border-collapse: collapse; + width: 100%; + margin: 1em 0; +} + +td, th { + border: 1px solid #dddddd; + text-align: left; + padding: 8px; +} + +.even tr:nth-child(even) { + background-color: #f5f5f5; +} + +.double tr:nth-child(4n+0) { + background-color: #f5f5f5; +} +.double tr:nth-child(4n+3) { + background-color: #f5f5f5; +} + +.double th:first-child { + width: 50%; +} + +.flash { + margin: 1em 0; + padding: 1em; + background: #f6e7db; + border: 1px solid #000000; +} + +.errors { + margin: 0.1em; + padding: 0.1em 0.5em; + background: #ffb3b3; + border: 1px solid #000000; + flex: 100% +} +ul.errors { + list-style-type: none; +} + +/* Top-down Form +Label [Input] +-----Button----- +*/ + +form { + display:flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; +} + +form label { + flex-basis: 30%; +} + +form input { + flex-basis: 70%; + margin: 0.4em 0; + height: 2em; +} + +form input[type=submit], form input[type=button] { + flex-basis: 100%; +} + +form .basis_auto { + flex-basis: auto !important; +} + +form textarea { + height: 30em; + flex-basis: 100%; + margin-bottom:0.4em; +} + +/* Inline Form +Label [Input] Button +*/ +form.inline { + flex-wrap: nowrap; + align-content: stretch; + align-items: center; +} + +form.inline label, form.inline input { + align-self: unset; + flex: 1 1 auto; + margin: 0 0.3em 1em; +} + +form.inline label, +form.inline input[type=submit], +form.inline input[type=reset], +form.inline input[type=button] { + flex: 0 1 auto; +} diff --git a/kdoorweb/kdoorweb/views/base.html b/kdoorweb/kdoorweb/views/base.html new file mode 100644 index 0000000..be03984 --- /dev/null +++ b/kdoorweb/kdoorweb/views/base.html @@ -0,0 +1,23 @@ + + + + + + K-Space Door system + + + +
+

K-Space Door system

+ +
+
+ + {{!base}} + + + diff --git a/kdoorweb/kdoorweb/views/list.html b/kdoorweb/kdoorweb/views/list.html new file mode 100644 index 0000000..b7e0db0 --- /dev/null +++ b/kdoorweb/kdoorweb/views/list.html @@ -0,0 +1,20 @@ +% rebase('base.html') + +

Users List

+ + + + + + + + + + + + + + + + +
Namenr of cardsLast access
Arti Zirk2-
diff --git a/kdoorweb/kdoorweb/views/login.html b/kdoorweb/kdoorweb/views/login.html new file mode 100644 index 0000000..14034f2 --- /dev/null +++ b/kdoorweb/kdoorweb/views/login.html @@ -0,0 +1,15 @@ +% rebase('base.html') + +% if error: +

Error: {{ error }}

+% end + +
+ + + + + + + +
diff --git a/kdoorweb/kdoorweb/web.py b/kdoorweb/kdoorweb/web.py new file mode 100644 index 0000000..5d5b9e6 --- /dev/null +++ b/kdoorweb/kdoorweb/web.py @@ -0,0 +1,79 @@ +import os +import secrets + +from bottle import Bottle, view, TEMPLATE_PATH, static_file, \ + request, redirect, response + +application = app = Bottle() + +# TODO: Could be replaced with importlib.resources +TEMPLATE_PATH.append(os.path.join(os.path.dirname(__file__), "views")) +STATIC_PATH = os.path.join(os.path.dirname(__file__), "static") + +COOKIE_KEY = secrets.token_bytes(32) + + +def check_auth(callback): + def wrapper(*args, **kwargs): + user = request.get_cookie("user", key=COOKIE_KEY) + if user: + print(f"logged in as {user}") + return callback(*args, **kwargs) + else: + print("not logged in") + response.set_cookie("error", "Not logged in") + redirect("/login") + return wrapper + + +app.install(check_auth) + + +@app.route('/static/', skip=[check_auth]) +def callback(path): + return static_file(path, root=STATIC_PATH) + + +@app.route("/", skip=[check_auth]) +def index(): + redirect("/login") + + +@app.route('/login', skip=[check_auth]) +@view("login.html") +def login(): + error = request.get_cookie("error") + if error: + response.set_cookie("error", "") + return {"error": error} + + +@app.post('/login', skip=[check_auth]) +def do_login(): + print(dict(request.forms)) + user = request.forms.get("user") + print(f"user {user}") + if user: + response.set_cookie("user", user, key=COOKIE_KEY) + redirect("/list") + else: + response.set_cookie("error", "No user") + redirect("/login") + + +@app.route("/logout") +def logout(): + response.set_cookie("user", "", key=COOKIE_KEY) + redirect("/login") + + +@app.route("/list") +@view("list.html") +def list(): + return {} + + +@app.route("/log") +@view("log") +def log(): + return {} diff --git a/kdoorweb/pyproject.toml b/kdoorweb/pyproject.toml new file mode 100644 index 0000000..121a39f --- /dev/null +++ b/kdoorweb/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools >= 40.6.0", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/kdoorweb/requirements.txt b/kdoorweb/requirements.txt new file mode 100644 index 0000000..63f35ed --- /dev/null +++ b/kdoorweb/requirements.txt @@ -0,0 +1,7 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile setup.py +# +bottle==0.12.18 # via kdoorweb (setup.py) diff --git a/kdoorweb/setup.py b/kdoorweb/setup.py new file mode 100644 index 0000000..935f059 --- /dev/null +++ b/kdoorweb/setup.py @@ -0,0 +1,26 @@ +from setuptools import find_packages, setup + +setup( + name='kdoorweb', + version='0.0.0', + author="Arti Zirk", + author_email="arti@zirk.me", + description="K-Space Door Administraion Web Interface", + packages=find_packages(), + include_package_data=True, + zip_safe=False, + python_requires='>=3.6', + install_requires=[ + 'bottle' + ], + classifiers=[ + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3 :: Only', + 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', + 'Topic :: System :: Networking', + 'Intended Audience :: System Administrators', + 'Framework :: Bottle' + ] + +)