forked from k-space/inventory-app
Compare commits
3 Commits
issue-7-ho
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f0c75d4d8 | |||
| 7484471b6b | |||
| 57ba7d1f58 |
@@ -8,7 +8,7 @@ from flask_wtf import FlaskForm
|
|||||||
from oidc import login_required, read_user
|
from oidc import login_required, read_user
|
||||||
from pymongo import MongoClient
|
from pymongo import MongoClient
|
||||||
from wtforms import BooleanField, IntegerField, SelectField, StringField, validators
|
from wtforms import BooleanField, IntegerField, SelectField, StringField, validators
|
||||||
from wtforms.validators import DataRequired, InputRequired
|
from wtforms.validators import DataRequired
|
||||||
|
|
||||||
page_doorboy = Blueprint("doorboy", __name__)
|
page_doorboy = Blueprint("doorboy", __name__)
|
||||||
db = MongoClient(const.MONGO_URI).get_default_database()
|
db = MongoClient(const.MONGO_URI).get_default_database()
|
||||||
@@ -34,13 +34,13 @@ def view_doorboy_claim(event_id):
|
|||||||
"inventory.owner.username": { "$exists": False }
|
"inventory.owner.username": { "$exists": False }
|
||||||
}, {
|
}, {
|
||||||
"$set": {
|
"$set": {
|
||||||
"token.enabled": datetime.utcnow(),
|
"token.enabled": True,
|
||||||
"inventory.owner.display_name": user["name"],
|
"inventory.owner.display_name": user["name"],
|
||||||
"inventory.owner.username": user["username"]
|
"inventory.owner.username": user["username"]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return redirect("/m/doorboy")
|
return redirect(f"/m/doorboy/{event_id }/edit")
|
||||||
|
|
||||||
class TokenEditForm(FlaskForm):
|
class TokenEditForm(FlaskForm):
|
||||||
comment = StringField("Comment")
|
comment = StringField("Comment")
|
||||||
@@ -82,19 +82,9 @@ def save_doorboy_edit(token_id):
|
|||||||
})
|
})
|
||||||
return redirect("/m/doorboy/me")
|
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):
|
class HoldDoorForm(FlaskForm):
|
||||||
door_name = SelectField("Door name", choices=[(j,j) for j in ["grounddoor", "frontdoor", "backdoor", "workshopdoor"]], validators=[DataRequired()])
|
door_name = SelectField("Door name", choices=[(j,j) for j in ["grounddoor", "frontdoor", "backdoor"]], validators=[DataRequired()])
|
||||||
duration = IntegerField('Duration in seconds', validators=[InputRequired(), hold_duration_check])
|
duration = IntegerField('Duration in seconds', validators=[DataRequired(), validators.NumberRange(min=5, max=21600)])
|
||||||
|
|
||||||
# duration=0 to override and close right away
|
# duration=0 to override and close right away
|
||||||
@page_doorboy.route("/m/doorboy/hold", methods=["POST"])
|
@page_doorboy.route("/m/doorboy/hold", methods=["POST"])
|
||||||
@@ -102,32 +92,16 @@ class HoldDoorForm(FlaskForm):
|
|||||||
def view_doorboy_hold():
|
def view_doorboy_hold():
|
||||||
user = read_user()
|
user = read_user()
|
||||||
form = HoldDoorForm(request.form)
|
form = HoldDoorForm(request.form)
|
||||||
if not form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
# Validation or CSRF failure: report it instead of redirecting as if the
|
db.doorlog.insert_one({
|
||||||
# hold had been accepted, so the user knows the door will not be held.
|
"method": "hold",
|
||||||
errors = "; ".join(m for messages in form.errors.values() for m in messages)
|
"timestamp": datetime.utcnow(),
|
||||||
return "Invalid hold request: %s" % (errors or "bad or expired form"), 400
|
"door": form.door_name.data,
|
||||||
|
"approved": True,
|
||||||
door = form.door_name.data
|
"user": user["username"],
|
||||||
if door == "workshopdoor":
|
"expires": datetime.utcnow() + timedelta(seconds=form.duration.data)
|
||||||
access_group = "k-space:workshop"
|
})
|
||||||
else:
|
return redirect("/m/doorboy")
|
||||||
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.
|
# Writes open event to log, which is picked up by doorboy-proxy.
|
||||||
@page_doorboy.route("/m/doorboy/<door>/open")
|
@page_doorboy.route("/m/doorboy/<door>/open")
|
||||||
@@ -161,7 +135,6 @@ def view_doorboy():
|
|||||||
user = read_user()
|
user = read_user()
|
||||||
workshop_access = "k-space:workshop" in g.users_lookup.get(user["username"], User()).groups
|
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)
|
latest_swipes = db.inventory.find({"component": "doorboy", "type":"token"}).sort([("last_seen", -1)]).limit(10)
|
||||||
return render_template("doorboy.html", **locals())
|
return render_template("doorboy.html", **locals())
|
||||||
|
|
||||||
|
|||||||
@@ -130,11 +130,6 @@ mongodb = mongoclient.get_default_database()
|
|||||||
mongodb.inventory.create_index("shortener.slug", sparse=True, unique=True)
|
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", sparse=True, unique=True)
|
||||||
#mongodb.inventory.create_index("token.uid_hash", 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 = (
|
CATEGORY_COLORS = (
|
||||||
('membership-fee', '#acc236'),
|
('membership-fee', '#acc236'),
|
||||||
|
|||||||
@@ -37,22 +37,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3>Hold door open</h3>
|
|
||||||
<form method="POST" action="/m/doorboy/hold" autocomplete="off">
|
|
||||||
{{ hold_form.csrf_token }}
|
|
||||||
<select name="door_name">
|
|
||||||
<option value="grounddoor">Ground door</option>
|
|
||||||
<option value="frontdoor">Front door</option>
|
|
||||||
<option value="backdoor">Back door</option>
|
|
||||||
{% if workshop_access %}<option value="workshopdoor">Workshop</option>{% endif %}
|
|
||||||
</select>
|
|
||||||
<input type="number" name="duration" min="0" max="21600" value="300">
|
|
||||||
<button class="waves-effect waves-light btn" type="submit">Hold</button>
|
|
||||||
<p>Duration in seconds: 300 = 5 min, 3600 = 1h, max 21600 = 6h, 0 = cancel/close now.</p>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
<div><ul>
|
<div><ul>
|
||||||
<li>Doors locations: <a href="https://k-space.ee/where">k-space.ee/where</a></li>
|
<li>Doors locations: <a href="https://k-space.ee/where">k-space.ee/where</a></li>
|
||||||
@@ -72,7 +56,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h4>Keycards last seen:</h4>
|
<h4>Keycards last seen:</h4>
|
||||||
Does not include door opens by webhook.
|
Does not include door opens by webhook. Keycards are synced to doors every 3 minutes.
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -106,6 +90,7 @@ Does not include door opens by webhook.
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<p><a href="/m/doorboy/log">All logs</a> (link for admins only)</p>
|
||||||
|
|
||||||
{% if false %}
|
{% if false %}
|
||||||
<!-- whole section commented out -->
|
<!-- whole section commented out -->
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<h4>Doorboy info for {{subject_user.username | display_name}}</h4>
|
<h4>Doorboy info for {{subject_user.username | display_name}}</h4>
|
||||||
<br/>
|
<br/>
|
||||||
<h5>Users cards:</h5>
|
<h5>Users cards:</h5>
|
||||||
|
<p>Keycards are synced to doors every 3 minutes.</p>
|
||||||
<table class="striped">
|
<table class="striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
Reference in New Issue
Block a user