diff --git a/inventory-app/doorboy.py b/inventory-app/doorboy.py new file mode 100644 index 0000000..1a437ed --- /dev/null +++ b/inventory-app/doorboy.py @@ -0,0 +1,201 @@ +from datetime import datetime, timedelta + +from bson.objectid import ObjectId +from flask import Blueprint, g, redirect, render_template, request +from flask_wtf import FlaskForm +from pymongo import MongoClient +from wtforms import StringField, IntegerField, SelectField, validators +from wtforms.validators import DataRequired + +import const +from common import spam, users_lookup +from oidc import login_required, read_user + +page_doorboy = Blueprint("doorboy", __name__) +db = MongoClient(const.MONGO_URI).get_default_database() + +@page_doorboy.route("/m/doorboy//claim") +@login_required +def view_doorboy_claim(event_id): + user = read_user() + + # Find swipe event OR token object by id to get card UID + event = db.inventory.find_one({ + "_id": ObjectId(event_id) + }) + + # Find token object to associate with user + token = db.inventory.update_one({ + "type": "token", + "token.uid_hash": event["token"]["uid_hash"], + "inventory.owner.username": { "$exists": False } + }, { + "$set": { + "token.enabled": datetime.utcnow(), + "inventory.owner.display_name": user["name"], + "inventory.owner.username": user["username"] + } + }) + + return redirect("/m/doorboy") + + + +@page_doorboy.route("/m/doorboy//disable") +@login_required +def view_doorboy_disable(token_id): + user = read_user() + db.inventory.update_one({ + "component": "doorboy", + "type": "token", + "_id": ObjectId(token_id), + "inventory.owner.username": user["username"] + }, { + "$set": { + "token.disabled": datetime.utcnow() + }, + "$unset": { + "token.enabled": "" + } + + }) + return redirect("/m/doorboy") + +@page_doorboy.route("/m/doorboy//enable") +@login_required +def view_doorboy_enable(token_id): + user = read_user() + db.inventory.update_one({ + "component": "doorboy", + "type": "token", + "_id": ObjectId(token_id), + "inventory.owner.username": user["username"] + }, { + "$set": { + "token.enabled": datetime.utcnow(), + }, + "$unset": { + "token.disabled": "" + } + }) + return redirect("/m/doorboy") + + +class TokenEditForm(FlaskForm): + comment = StringField("Comment") + +@page_doorboy.route("/m/doorboy//edit", methods=["GET"]) +@login_required +def view_doorboy_edit(token_id): + user = read_user() + token = db.inventory.find_one({ + "component": "doorboy", + "type": "token", + "_id": ObjectId(token_id), + "inventory.owner.username": user["username"] + }) + form = TokenEditForm() + form.comment.data = token["token"].get("comment", "") + return render_template("doorboy_token_edit.html", form=form, token=token) + +@page_doorboy.route("/m/doorboy//edit", methods=["POST"]) +@login_required +def save_doorboy_edit(token_id): + user = read_user() + form = TokenEditForm(request.form) + if form.validate_on_submit(): + db.inventory.update_one({ + "component": "doorboy", + "type": "token", + "_id": ObjectId(token_id), + "inventory.owner.username": user["username"] + }, { + "$set": { + "token.comment": form.comment.data, + } + }) + return redirect("/m/doorboy") + +class HoldDoorForm(FlaskForm): + door_name = SelectField("Door name", choices=[(j,j) for j in ["ground", "front", "back"]], validators=[DataRequired()]) + duration = IntegerField('Duration in seconds', validators=[DataRequired(), validators.NumberRange(min=5, max=21600)]) + +@page_doorboy.route("/m/doorboy/hold", methods=["POST"]) +@login_required +def view_doorboy_hold(): + user = read_user() + form = HoldDoorForm(request.form) + now = datetime.utcnow() + if form.validate_on_submit(): + db.eventlog.insert_one({ + "component": "doorboy", + "type": "hold", + "requester": user["name"], + "door": form.door_name.data, + "expires": datetime.utcnow() + timedelta(seconds=form.duration.data) + }) + return redirect("/m/doorboy") + + +@page_doorboy.route("/m/doorboy//open") +@login_required +def view_doorboy_open(door): + user = read_user() + if door not in ("ground", "front", "back"): raise + approved = user["username"] in users_lookup + db.eventlog.insert_one({ + "method": "web", + "approved": approved, + "duration": 5, + "component": "doorboy", + "type": "open-door", + "door": door, + "member_id": user["username"], + "member": user["name"], + "timestamp": datetime.utcnow(), + }) + status = "Permitted" if approved else "Denied" + subject = user["name"] + msg = "%s %s door access for %s via https://inventory.k-space.ee/m/doorboy" % (status, door, subject) + spam(msg) + return redirect("/m/doorboy") + + +@page_doorboy.route("/m/doorboy/slam", methods=["POST"]) +@login_required +def view_doorboy_slam(): + user = read_user() + db.eventlog.insert_one({ + "component": "doorboy", + "type": "hold", + "requester": user["name"], + "door": form.door_name.data, + "expires": datetime.utcnow() + timedelta(minutes=form.duration_min.data) + }) + return redirect("/m/doorboy") + + +@page_doorboy.route("/m/doorboy") +@login_required +def view_doorboy(): + user = read_user() + latest_events = db.eventlog.find({"component": "doorboy", "type":"open-door"}).sort([("timestamp", -1)]).limit(10); + latest_swipes = db.inventory.find({"component": "doorboy", "type":"token"}).sort([("last_seen", -1)]).limit(10); + return render_template("doorboy.html", **locals()) + + +@page_doorboy.route("/m/doorboy/swipes") +@login_required +def view_doorboy_events(): + user = read_user() + latest_events = db.eventlog.find({"component": "doorboy", "event":"card-swiped"}).sort([("timestamp", -1)]).limit(500); + return render_template("doorboy.html", **locals()) + + +@page_doorboy.route("/m/doorboy//events") +@login_required +def view_doorboy_token_events(token_id): + user = read_user() + token = db.inventory.find_one({"_id": ObjectId(token_id)}) + latest_events = db.eventlog.find({"component": "doorboy", "event":"card-swiped", "token.uid": token.get("token").get("uid")}).sort([("timestamp", -1)]) + return render_template("doorboy.html", **locals()) diff --git a/inventory-app/main.py b/inventory-app/main.py index 1e46c02..adb5c66 100755 --- a/inventory-app/main.py +++ b/inventory-app/main.py @@ -47,6 +47,7 @@ import const from common import CustomForm, devenv, flatten, format_name, spam, users_lookup, User from inventory import page_inventory from oidc import page_oidc, login_required, read_user +from doorboy import page_doorboy from api import page_api def check_foreign_key_format(item): @@ -128,6 +129,7 @@ app.wsgi_app = ReverseProxied(app.wsgi_app) app.register_blueprint(page_inventory) app.register_blueprint(page_oidc) app.register_blueprint(page_api) +app.register_blueprint(page_doorboy) metrics = PrometheusMetrics(app, group_by="path") app.config['SECRET_KEY'] = const.SECRET_KEY diff --git a/inventory-app/templates/doorboy.html b/inventory-app/templates/doorboy.html new file mode 100644 index 0000000..ef58ede --- /dev/null +++ b/inventory-app/templates/doorboy.html @@ -0,0 +1,77 @@ +{% extends 'base.html' %} + +{% block content %} +
+ +

Press to open:

+
    +
  • Ground door the one on street level facing KBFI
  • +
  • Front door the one from ground door 5 floors upward
  • +
  • Back door on 5th floor on the Pancake cafeteria side. Note: ground door on cafeteria side is open whenever the cafeteria is open. Other times use the ground door listed above.
  • + +
+ +

Recent door open requests via Slack or the buttons above

+ + + + + + + + + + + + + {% for o in latest_events %} + + + + + + + {% endfor %} + +
 WhenWhoDoor
{% if o.approved %}check_circle{% else %} {% endif %}{{ o.timestamp | timeago }}{% if o.member %}{{ o.member }}{% else %}{{ o.member_id }}{% endif %}{{ o.door }}
+ +

Please claim keycards or keyfobs associated with you here!

+ + + + + + + + + + + + + + + {% for o in latest_swipes %} + + + + + + + + + + {% endfor %} + +
 Last seen WhoUID hash tailGranted 
+ {% if o.inventory and o.inventory.owner %} +   + {% else %} + error{% endif %} + {{ (o.last_seen or o.timestamp) | timeago }}{{ o.door }}{% if o.inventory and o.inventory.owner %}{{ o.inventory.owner.username | display_name }}{% else %}Unknown{% endif %}{{ o.token.uid_hash[-6:] }}{% if o.success %}check_circle{% else %} {% endif %}{% if o.inventory and o.inventory.owner %}{{ o.token.comment }}{% else %}This is mine!{% endif %}
+ +
+{% endblock %} + diff --git a/inventory-app/templates/doorboy_token_edit.html b/inventory-app/templates/doorboy_token_edit.html new file mode 100644 index 0000000..950ea9b --- /dev/null +++ b/inventory-app/templates/doorboy_token_edit.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} + +{% block content %} +
+

Edit door access token {{ token.token.uid_hash[-6:] }}

+
+ {{ form.csrf_token }} + {{ form.comment.label }} {{ form.comment(size=20) }} + +
+
+{% endblock %} diff --git a/inventory-app/templates/menu.html b/inventory-app/templates/menu.html index 4378e07..1e00d0d 100644 --- a/inventory-app/templates/menu.html +++ b/inventory-app/templates/menu.html @@ -1 +1,2 @@
  • Inventory
  • +
  • Doorboyâ„¢