diff --git a/inventory-app/doorboy.py b/inventory-app/doorboy.py index 64f5bf6..5db9531 100644 --- a/inventory-app/doorboy.py +++ b/inventory-app/doorboy.py @@ -8,7 +8,7 @@ from flask_wtf import FlaskForm from oidc import login_required, read_user from pymongo import MongoClient from wtforms import BooleanField, IntegerField, SelectField, StringField, validators -from wtforms.validators import DataRequired +from wtforms.validators import DataRequired, InputRequired page_doorboy = Blueprint("doorboy", __name__) db = MongoClient(const.MONGO_URI).get_default_database() @@ -82,9 +82,19 @@ def save_doorboy_edit(token_id): }) return redirect("/m/doorboy/me") +def hold_duration_check(form, field): + # 0 cancels/closes an active hold immediately; any real hold must last at + # least the documented 5-second minimum (and at most 6 hours). Reject 1-4 + # so a typo cannot arm an unusably short hold. field.data is None when the + # submitted value was not a valid integer; leave that to the other validators. + if field.data is None: + return + if field.data != 0 and not (5 <= field.data <= 21600): + raise validators.ValidationError("Duration must be 0 (to close now) or between 5 and 21600 seconds") + class HoldDoorForm(FlaskForm): - door_name = SelectField("Door name", choices=[(j,j) for j in ["grounddoor", "frontdoor", "backdoor"]], validators=[DataRequired()]) - duration = IntegerField('Duration in seconds', validators=[DataRequired(), validators.NumberRange(min=5, max=21600)]) + door_name = SelectField("Door name", choices=[(j,j) for j in ["grounddoor", "frontdoor", "backdoor", "workshopdoor"]], validators=[DataRequired()]) + duration = IntegerField('Duration in seconds', validators=[InputRequired(), hold_duration_check]) # duration=0 to override and close right away @page_doorboy.route("/m/doorboy/hold", methods=["POST"]) @@ -92,16 +102,32 @@ class HoldDoorForm(FlaskForm): def view_doorboy_hold(): user = read_user() form = HoldDoorForm(request.form) - if form.validate_on_submit(): - db.doorlog.insert_one({ - "method": "hold", - "timestamp": datetime.utcnow(), - "door": form.door_name.data, - "approved": True, - "user": user["username"], - "expires": datetime.utcnow() + timedelta(seconds=form.duration.data) - }) - return redirect("/m/doorboy") + if not form.validate_on_submit(): + # Validation or CSRF failure: report it instead of redirecting as if the + # hold had been accepted, so the user knows the door will not be held. + errors = "; ".join(m for messages in form.errors.values() for m in messages) + return "Invalid hold request: %s" % (errors or "bad or expired form"), 400 + + door = form.door_name.data + if door == "workshopdoor": + access_group = "k-space:workshop" + else: + access_group = "k-space:floor" + approved = access_group in g.users_lookup.get(user["username"], User()).groups + db.doorlog.insert_one({ + "method": "hold", + "timestamp": datetime.utcnow(), + "door": door, + "approved": approved, + "user": user["username"], + "expires": datetime.utcnow() + timedelta(seconds=form.duration.data) + }) + + if approved: + return redirect("/m/doorboy") + # Logged for audit, but the proxy only honors approved holds, so tell the + # user the door will not actually be held rather than redirecting silently. + return "You are not in the access group required to hold this door", 403 # Writes open event to log, which is picked up by doorboy-proxy. @page_doorboy.route("/m/doorboy//open") @@ -135,6 +161,7 @@ def view_doorboy(): user = read_user() workshop_access = "k-space:workshop" in g.users_lookup.get(user["username"], User()).groups + hold_form = HoldDoorForm() latest_swipes = db.inventory.find({"component": "doorboy", "type":"token"}).sort([("last_seen", -1)]).limit(10) return render_template("doorboy.html", **locals()) diff --git a/inventory-app/main.py b/inventory-app/main.py index 571d595..17dced4 100755 --- a/inventory-app/main.py +++ b/inventory-app/main.py @@ -130,6 +130,11 @@ mongodb = mongoclient.get_default_database() mongodb.inventory.create_index("shortener.slug", sparse=True, unique=True) mongodb.inventory.create_index("token.uid_hash", sparse=True, unique=True) #mongodb.inventory.create_index("token.uid_hash", unique=True) +# Supports doorboy-proxy's per-poll "newest active hold for this door" lookup +# (find_one({method,door,approved}, sort=timestamp desc)) without a collection scan. +# method is the leading equality field so the common no-active-hold poll does not +# scan every approved event for the door; timestamp trails for the descending sort. +mongodb.doorlog.create_index([("method", 1), ("door", 1), ("approved", 1), ("timestamp", -1)]) CATEGORY_COLORS = ( ('membership-fee', '#acc236'), diff --git a/inventory-app/templates/doorboy.html b/inventory-app/templates/doorboy.html index bf3652e..a1e6e1a 100644 --- a/inventory-app/templates/doorboy.html +++ b/inventory-app/templates/doorboy.html @@ -37,6 +37,22 @@ +
+

Hold door open

+
+ {{ hold_form.csrf_token }} + + + +

Duration in seconds: 300 = 5 min, 3600 = 1h, max 21600 = 6h, 0 = cancel/close now.

+
+
+