Move Doorboy page over from members site
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful
This commit is contained in:
parent
ebbe2669a6
commit
2cfccc22a0
201
inventory-app/doorboy.py
Normal file
201
inventory-app/doorboy.py
Normal file
@ -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/<event_id>/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/<token_id>/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/<token_id>/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/<token_id>/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/<token_id>/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/<door>/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/<token_id>/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())
|
@ -47,6 +47,7 @@ import const
|
|||||||
from common import CustomForm, devenv, flatten, format_name, spam, users_lookup, User
|
from common import CustomForm, devenv, flatten, format_name, spam, users_lookup, User
|
||||||
from inventory import page_inventory
|
from inventory import page_inventory
|
||||||
from oidc import page_oidc, login_required, read_user
|
from oidc import page_oidc, login_required, read_user
|
||||||
|
from doorboy import page_doorboy
|
||||||
from api import page_api
|
from api import page_api
|
||||||
|
|
||||||
def check_foreign_key_format(item):
|
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_inventory)
|
||||||
app.register_blueprint(page_oidc)
|
app.register_blueprint(page_oidc)
|
||||||
app.register_blueprint(page_api)
|
app.register_blueprint(page_api)
|
||||||
|
app.register_blueprint(page_doorboy)
|
||||||
metrics = PrometheusMetrics(app, group_by="path")
|
metrics = PrometheusMetrics(app, group_by="path")
|
||||||
|
|
||||||
app.config['SECRET_KEY'] = const.SECRET_KEY
|
app.config['SECRET_KEY'] = const.SECRET_KEY
|
||||||
|
77
inventory-app/templates/doorboy.html
Normal file
77
inventory-app/templates/doorboy.html
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<p>Press to open:</p>
|
||||||
|
<ul>
|
||||||
|
<li><a class="waves-effect waves-light btn" href="/m/doorboy/ground/open">Ground door</a> the one on street level facing KBFI</li>
|
||||||
|
<li><a class="waves-effect waves-light btn" href="/m/doorboy/front/open">Front door</a> the one from ground door 5 floors upward</li>
|
||||||
|
<li><a class="waves-effect waves-light btn" href="/m/doorboy/back/open">Back door</a> 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.</li>
|
||||||
|
<!--
|
||||||
|
<li>Coming soon: <a class="waves-effect waves-light btn" href="/m/doorboy/workshop/open">Workshop door</a> also known as the dirty room</li>
|
||||||
|
<li>Coming later: <a class="waves-effect waves-light btn" href="/m/doorboy/server/open">Server room door</a></li>
|
||||||
|
-->
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>Recent door open requests via Slack or the buttons above</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th> </th>
|
||||||
|
<th>When</th>
|
||||||
|
<th>Who</th>
|
||||||
|
<th>Door</th>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for o in latest_events %}
|
||||||
|
<tr>
|
||||||
|
<td>{% if o.approved %}<i class="material-icons">check_circle</i>{% else %} {% endif %}</td>
|
||||||
|
<td>{{ o.timestamp | timeago }}</td>
|
||||||
|
<td><a href="/m/user/{{ o.member_id }}">{% if o.member %}{{ o.member }}{% else %}{{ o.member_id }}{% endif %}</a></td>
|
||||||
|
<td>{{ o.door }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p>Please claim keycards or keyfobs associated with you here!</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th> </th>
|
||||||
|
<th>Last seen</th>
|
||||||
|
<th> </th>
|
||||||
|
<th>Who</th>
|
||||||
|
<th>UID hash tail</th>
|
||||||
|
<th>Granted</th>
|
||||||
|
<th> </th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for o in latest_swipes %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{% if o.inventory and o.inventory.owner %}
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<i class="material-icons">error</i>{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ (o.last_seen or o.timestamp) | timeago }}</td>
|
||||||
|
<td>{{ o.door }}</td>
|
||||||
|
<td>{% if o.inventory and o.inventory.owner %}<a href="/m/user/{{ o.inventory.owner.username }}">{{ o.inventory.owner.username | display_name }}</a>{% else %}Unknown{% endif %}</td>
|
||||||
|
<td><a href="/m/doorboy/{{ o._id }}/events">{{ o.token.uid_hash[-6:] }}</a></td>
|
||||||
|
<td>{% if o.success %}<i class="material-icons">check_circle</i>{% else %} {% endif %}</td>
|
||||||
|
<td>{% if o.inventory and o.inventory.owner %}{{ o.token.comment }}{% else %}<a class="waves-effect waves-light btn" href="/m/doorboy/{{ o._id }}/claim">This is mine!</a>{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
12
inventory-app/templates/doorboy_token_edit.html
Normal file
12
inventory-app/templates/doorboy_token_edit.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<h4>Edit door access token {{ token.token.uid_hash[-6:] }}</h4>
|
||||||
|
<form method="POST" autocomplete="off">
|
||||||
|
{{ form.csrf_token }}
|
||||||
|
{{ form.comment.label }} {{ form.comment(size=20) }}
|
||||||
|
<button class="btn waves-effect waves-light" type="submit">Save</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -1 +1,2 @@
|
|||||||
<li><a href="/m/inventory?type=machine&type=locker&type=desk">Inventory</a></li>
|
<li><a href="/m/inventory?type=machine&type=locker&type=desk">Inventory</a></li>
|
||||||
|
<li><a href="/m/doorboy">Doorboy™</a></li>
|
||||||
|
Loading…
Reference in New Issue
Block a user