1
0
forked from arti/doors

Add more web

This commit is contained in:
Arti Zirk 2020-09-13 19:36:05 +03:00
parent aa313497cd
commit 1252f7af9e
9 changed files with 319 additions and 35 deletions

1
.gitignore vendored
View File

@ -64,3 +64,4 @@ target/
# Other # Other
venv/ venv/
*.sqlite *.sqlite
ad.json

View File

@ -1,14 +1,53 @@
import datetime import datetime
import sqlite3 import sqlite3
import inspect
import json
from bottle import HTTPError
SCHEMA_VERSION = 1 SCHEMA_VERSION = 1
class SQLitePlugin:
name = "sqlite"
api = 2
def __init__(self, dbfile):
self.dbfile = dbfile
def apply(self, callback, context):
# Test if the original callback accepts a 'db' keyword.
# Ignore it if it does not need a database handle.
args = inspect.signature(context.callback).parameters
if "db" not in args:
return callback
def wrapper(*args, **kwargs):
db = DB(self.dbfile)
# Add the connection handle as a keyword argument.
kwargs["db"] = db
try:
rv = callback(*args, **kwargs)
except sqlite3.IntegrityError as e:
db.db.rollback()
raise HTTPError(500, "Database Error", e)
finally:
db.close()
return rv
# Replace the route callback with the wrapped one.
return wrapper
class DB: class DB:
def __init__(self, dbfile=":memory:"): def __init__(self, dbfile=":memory:"):
self.dbfile = dbfile self.dbfile = dbfile
self.db = sqlite3.connect(self.dbfile) self.db = sqlite3.connect(self.dbfile)
self.db.row_factory = sqlite3.Row
def close(self):
self.db.close()
@staticmethod @staticmethod
def create_db(dbfile): def create_db(dbfile):
@ -21,7 +60,7 @@ class DB:
create table users ( create table users (
id integer primary key, id integer primary key,
user text, user text,
real_name text, full_name text,
email text, email text,
disabled integer, disabled integer,
admin integer admin integer
@ -32,6 +71,7 @@ class DB:
card_uid blob, card_uid blob,
name text, name text,
created text, created text,
disabled integer,
foreign key (user_id) foreign key (user_id)
references users (id) references users (id)
@ -69,25 +109,69 @@ class DB:
) )
db.commit() db.commit()
def list_users(*, page=0, count=20, query=None): def add_user(self, user, full_name=None, email=None, disabled=False, admin=False):
pass
def add_user(self, user, real_name=None, email=None, disabled=False, admin=False):
self.add_users([( self.add_users([(
user, real_name, email, disabled, admin user, full_name, email, int(disabled), int(admin)
)]) )])
def add_users(self, users): def add_users(self, users):
self.db.executemany(""" self.db.executemany("""
insert into users(user, real_name, email, disabled, admin) insert into users(user, full_name, email, disabled, admin)
values(?, ?, ?, ?, ?) values(?, ?, ?, ?, ?)
""", users) """, users)
self.db.commit() self.db.commit()
def list_users(self):
cur = self.db.execute(
"select id, user, full_name, email from users"
)
return cur.fetchall()
def get_user(self, user_id):
cur = self.db.execute(
"select * from users where id = ?",
(user_id, )
)
return cur.fetchone()
def get_user_by_name(self, user_name):
cur = self.db.execute(
"select * from users where user = ?",
(user_name, )
)
return cur.fetchone()
@staticmethod
def import_ad(json_file):
with open(json_file) as fp:
json_data = json.load(fp)
for user, fields in json_data.items():
if fields.get("considered_active", False) and \
fields.get("groups", {}).get("floor_access", False):
yield (
user,
fields.get("full_name"),
fields.get("personal_mail"),
0,
int(fields.get("groups",{}).get("onboarding", False))
)
if __name__ == "__main__": if __name__ == "__main__":
#DB.create_db("kdoorweb.sqlite") dbfile = "../kdoorweb.sqlite"
db = DB("kdoorweb.sqlite") import os, sys
db.add_user("juku") from pprint import pprint
try:
os.unlink(dbfile)
except FileNotFoundError:
pass
DB.create_db(dbfile)
db = DB(dbfile)
db.add_users(db.import_ad("../ad.json"))
users = db.list_users()
for user in users:
print(dict(user))

View File

@ -1,3 +1,4 @@
% from bottle import request
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
@ -10,14 +11,21 @@
<header> <header>
<h1>K-Space Door system</h1> <h1>K-Space Door system</h1>
<nav> <nav>
<a href="/list">Users List</a> % if request.get("bottle.request.ext.current_user"):
<a href="/log">Door Log</a> <a href="/list">Users</a>
<a href="/logout">Logout</a> % if request.current_user["admin"]:
<a href="/doors">Doors</a>
<a href="/log">Log</a>
% end
<a href="/logout">Logout</a>
% end
</nav> </nav>
</header> </header>
<hr> <hr>
{{!base}} {{!base}}
<hr>
</body> </body>
</html> </html>

View File

@ -0,0 +1,36 @@
% rebase('base.html')
<h3>Doors List</h3>
<form method="post">
<input name="action" value="Lock All" type="submit">
<input name="action" value="Unlock All" type="submit">
</form>
<table class="double">
<thead>
<tr>
<th>Name</th>
<th>State</th>
<th style="width:10em;">Action</th>
</tr>
</thead>
<tbody>
% for door in doors:
<tr>
<td>{{door["name"]}}</td>
<td>{{door["state"]}}</td>
<td>
<form method="post">
<input name="door_id" value="door['id']" type="hidden">
<input name="action" value="Lock" type="submit">
<input name="action" value="Unlock" type="submit">
</form>
</td>
</tr>
<tr>
<td colspan="3">{{door["note"]}}</td>
</tr>
% end
</tbody>
</table>

View File

@ -0,0 +1,58 @@
% rebase('base.html')
<h3>Info {{full_name}}</h3>
<dl style="float:right;">
<dt>Disabled</dt>
<dd>{{disabled}}</dd>
<dt><abbr title="User is in Onboarding AD group">Admin</abbr></dt>
<dd>{{admin}}</dd>
</dl>
<dl>
<dt>User</dt>
<dd>{{user}}</dd>
<dt>Full Name</dt>
<dd>{{full_name}}</dd>
<dt>Email</dt>
<dd>{{email}}</dd>
</dl>
<h3>Keycards</h3>
<table class="even">
<thead>
<tr>
<th>Name</th>
<th>Created</th>
<th>Disabled</th>
<th>Action</th>
</tr>
</thead>
<tbody>
% for keycard in keycards:
<tr>
<td>{{keycard["name"]}}</td>
<td>{{keycard["created"]}}</td>
<td>{{keycard["disabled"]}}</td>
<td>
<form method="post">
<input name="keycard_id" value="keycard['id']" type="hidden">
<input name="action" value="disable" type="hidden">
<input value="Disable" type="submit">
</form>
</td>
</tr>
% end
</tbody>
</table>
<h4>Add new keycard</h4>
<form method="post" class="inline">
<label for="name">Name: </label>
<input id="name" name="name" type="text">
<label for="uid">UID: </label>
<input id="uid" name="uid" type="text">
<input name="action" value="create" type="hidden">
<input value="Add" type="submit">
</form>

View File

@ -6,15 +6,17 @@
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>nr of cards</th> <th style="width:4em;">Cards</th>
<th>Last access</th> <th style="width:10em;">Last access</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
% for user in users:
<tr> <tr>
<td><a href="/info/1">Arti Zirk</a></td> <td><a href="/info/{{user['id']}}">{{user["full_name"]}}</a></td>
<td>2</td> <td>-</td>
<td>-</td> <td>-</td>
</tr> </tr>
% end
</tbody> </tbody>
</table> </table>

View File

@ -0,0 +1,22 @@
% rebase('base.html')
<h3>Access log</h3>
<table class="even">
<thead>
<tr>
<th>Time</th>
<th>Door</th>
<th>User</th>
</tr>
</thead>
<tbody>
% for event in events:
<tr>
<td>{{event["timestamp"]}}</td>
<td>{{event["door"]}}</td>
<td><a href="/info/{{event['user_id']}}">{{event["user_name"]}}</a></td>
</tr>
% end
</tbody>
</table>

View File

@ -1,5 +1,7 @@
% rebase('base.html') % rebase('base.html')
<h3>Login:</h3>
% if error: % if error:
<p>Error: {{ error }}</p> <p>Error: {{ error }}</p>
% end % end

View File

@ -2,22 +2,45 @@ import os
import secrets import secrets
from bottle import Bottle, view, TEMPLATE_PATH, static_file, \ from bottle import Bottle, view, TEMPLATE_PATH, static_file, \
request, redirect, response request, redirect, response, HTTPError
from .db import SQLitePlugin
application = app = Bottle() application = app = Bottle()
# TODO: Could be replaced with importlib.resources # TODO: Could be replaced with importlib.resources
TEMPLATE_PATH.append(os.path.join(os.path.dirname(__file__), "views")) TEMPLATE_PATH.append(os.path.join(os.path.dirname(__file__), "views"))
STATIC_PATH = os.path.join(os.path.dirname(__file__), "static") STATIC_PATH = os.path.join(os.path.dirname(__file__), "static")
SQLITE_PATH = os.path.join(os.path.dirname(__file__), "..", "kdoorweb.sqlite")
COOKIE_KEY = secrets.token_bytes(32) COOKIE_KEY = b"1234"#secrets.token_bytes(32)
def current_user(db):
user_id = request.get_cookie("uid", secret=COOKIE_KEY)
if not user_id:
return
return db.get_user(user_id)
def login_user(uid):
response.set_cookie("uid", uid, secret=COOKIE_KEY)
def logout_user():
response.set_cookie("uid", "", secret=COOKIE_KEY)
def check_auth(callback): def check_auth(callback):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
user = request.get_cookie("user", key=COOKIE_KEY) if "db" not in kwargs:
request.current_user = None
return callback(*args, **kwargs)
user = current_user(kwargs["db"])
request.current_user = user
if user: if user:
print(f"logged in as {user}") print(f"logged in as {user['user']}")
print(request.current_user)
return callback(*args, **kwargs) return callback(*args, **kwargs)
else: else:
print("not logged in") print("not logged in")
@ -26,6 +49,7 @@ def check_auth(callback):
return wrapper return wrapper
app.install(SQLitePlugin(SQLITE_PATH))
app.install(check_auth) app.install(check_auth)
@ -35,7 +59,14 @@ def callback(path):
@app.route("/", skip=[check_auth]) @app.route("/", skip=[check_auth])
def index(): def index(db):
user = current_user(db)
if user:
if user["admin"]:
redirect("/list")
else:
redirect(f"/info/{user['id']}")
redirect("/login") redirect("/login")
@ -49,31 +80,71 @@ def login():
@app.post('/login', skip=[check_auth]) @app.post('/login', skip=[check_auth])
def do_login(): def do_login(db):
print(dict(request.forms)) user_name = request.forms.get("user")
user = request.forms.get("user") user = db.get_user_by_name(user_name)
print(f"user {user}")
if user: if user:
response.set_cookie("user", user, key=COOKIE_KEY) print(f"user {dict(user)}")
redirect("/list") login_user(user["id"])
redirect("/")
else: else:
response.set_cookie("error", "No user") response.set_cookie("error", "Login Failed")
redirect("/login") redirect("/login")
@app.route("/logout") @app.route("/logout")
def logout(): def logout():
response.set_cookie("user", "", key=COOKIE_KEY) logout_user()
redirect("/login") redirect("/login")
@app.route("/list") @app.route("/list")
@view("list.html") @view("list.html")
def list(): def user_list(db):
return {} user = request.current_user
if user and not user["admin"]:
users = [user]
else:
users = db.list_users()
return {"users": users}
@app.route("/info")
def user_info(db):
user = request.current_user
redirect(f"/info/{user['id']}")
@app.route("/info/<user_id>")
@view("info.html")
def info(db, user_id):
user_id = int(user_id)
c_user = request.current_user
if not c_user["admin"] and c_user["id"] != user_id:
raise HTTPError(403, "Logged in user is not admin")
user = db.get_user(user_id)
if not user:
raise HTTPError(404, "User does not exist")
return {**user, "keycards": []}
@app.route("/log") @app.route("/log")
@view("log") @view("log.html")
def log(): def log(db):
return {} return {
"events":
[
{
"timestamp": 0,
"door": "Back Door",
"user_id":1,
"user_name": "Arti Zirk"
}
]
}
@app.route("/doors")
@view("doors.html")
def doors(db):
return {"doors":[]}