slimming doorboy admin methods

- enable/disable unused and broken, edit is good enough
- admin use-case last many years has only been 'when has x'
  - can be expanded to 'who at x time'
  - current was broken anyway
This commit is contained in:
2025-12-30 03:48:38 +02:00
parent 44c64cc44e
commit 4b2c2f6368
3 changed files with 38 additions and 202 deletions

View File

@@ -1,16 +1,13 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
import const import const
import pytz
from bson.objectid import ObjectId from bson.objectid import ObjectId
from common import User from common import User
from dateutil.parser import ParserError, parse
from flask import Blueprint, abort, g, redirect, render_template, request from flask import Blueprint, abort, g, redirect, render_template, request
from flask_wtf import FlaskForm 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, from wtforms import BooleanField, IntegerField, SelectField, StringField, validators
validators)
from wtforms.validators import DataRequired from wtforms.validators import DataRequired
page_doorboy = Blueprint("doorboy", __name__) page_doorboy = Blueprint("doorboy", __name__)
@@ -41,48 +38,6 @@ def view_doorboy_claim(event_id):
return redirect("/m/doorboy") 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): class TokenEditForm(FlaskForm):
comment = StringField("Comment") comment = StringField("Comment")
enabled = BooleanField("Enabled") enabled = BooleanField("Enabled")
@@ -148,6 +103,7 @@ def view_doorboy_hold():
}) })
return redirect("/m/doorboy") return redirect("/m/doorboy")
# 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")
@login_required @login_required
def view_doorboy_open(door): def view_doorboy_open(door):
@@ -182,14 +138,9 @@ def view_doorboy_open(door):
def view_doorboy(): 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
# 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/user/<username>/cards") latest_swipes = db.inventory.find({"component": "doorboy", "type":"token"}).sort([("last_seen", -1)]).limit(10)
@login_required(groups=["k-space:board", "k-space:kubernetes:admins"]) return render_template("doorboy.html", **locals())
def view_user_cards(username):
return view_user_cards_inner(username)
@page_doorboy.route("/m/doorboy/me") @page_doorboy.route("/m/doorboy/me")
@login_required @login_required
@@ -210,71 +161,9 @@ def view_user_cards_inner(username):
}).sort([("last_seen", -1)]) }).sort([("last_seen", -1)])
return render_template("doorboy_user.html", **locals()) return render_template("doorboy_user.html", **locals())
@page_doorboy.route("/m/doorboy/admin") #TODO: only returns UID opens, not web or slack
@page_doorboy.route("/m/doorboy/log/<username>")
@login_required(groups=["k-space:board", "k-space:kubernetes:admins"]) @login_required(groups=["k-space:board", "k-space:kubernetes:admins"])
def view_doorboy_admin(): def view_user_events(username):
results = db.inventory.aggregate([ latest_events = db.eventlog.find({"component": "doorboy", "inventory.owner_id": username}).sort([("timestamp", -1)])
{ "$match": {"component": "doorboy", "type": "token"} }, return render_template("doorboy_log.html", latest_events=latest_events)
{
"$group": {
"_id": "$inventory.owner.username",
"cards": {
"$push" : {"$mergeObjects": [
"$token",
{"last_seen": "$last_seen"},
{"_id": "$_id"},
{"old_display_name": "$inventory.owner.display_name"},
{"old_foreign_id": "$inventory.owner.foreign_id"}
]}
}
}
},
{ "$sort": { "_id" : 1 } }
])
user_keyfobs = {r["_id"] : r["cards"] for r in results}
orphaned_keyfobs = user_keyfobs.pop(None)
no_keyfobs = [u for u in g.users if not user_keyfobs.get(u.username)]
no_keyfobs = list(filter(lambda u : u.user_type == "person", no_keyfobs))
last_seen = {key : max(datetime_handle(card.get("last_seen")) for card in value) for key, value in user_keyfobs.items()}
orphaned_keyfobs = sorted(orphaned_keyfobs, key = lambda o : (not bool(o.get("comment")), o.get("comment", "")))
no_keyfobs = sorted(no_keyfobs, key = lambda u : u.display_name or u.username)
last_seen = dict(sorted(last_seen.items(), key=lambda i : datetime_handle(i[1]), reverse=True))
return render_template("doorboy_admin.html", **locals())
def datetime_handle(item):
if not item:
dt = datetime.min
elif type(item) is str:
try:
dt = parse(item)
except ParserError as e:
print(e)
dt = datetime.min
elif type(item) is datetime:
dt = item
else:
dt = datetime.min
try:
dt = pytz.UTC.localize(dt)
except ValueError:
pass
return dt
@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_hash": token.get("token").get("uid_hash")}).sort([("timestamp", -1)])
return render_template("doorboy.html", **locals())

View File

@@ -1,82 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<ul class="collapsible expandable">
<li class="">
<div class="collapsible-header">
<i class="material-icons">access_time</i>Last seen
<div style="margin-left: auto;">
{{last_seen | length}}
</div>
</div>
<ul class="collapsible-body collection collapsible-collection">
{% for u, t in last_seen.items() %}
<li class="collection-item">
<a href="/m/doorboy/user/{{u}}/cards">
<i class="material-icons tiny">person</i>
{{u | display_name}}
</a>
<div class="secondary-content black-text">
<i class="material-icons tiny">access_time</i>
{{t | timeago}}
</div>
</li>
{% endfor %}
</ul>
</li>
<li>
<div class="red lighten-3 collapsible-header">
<i class="material-icons">error_outline</i>No keyfobs enrolled
<div style="margin-left: auto;">
{{no_keyfobs | length}}
</div>
</div>
<div class="collapsible-body collection collapsible-collection">
{% for u in no_keyfobs %}
<div class="collection-item">{{u.display_name or u.username}}</div>
{% endfor %}
</div>
</li>
<li>
<div class="red lighten-3 collapsible-header">
<i class="material-icons">error</i>Orphaned keyfobs
<div style="margin-left: auto;">
{{orphaned_keyfobs | length}}
</div>
</div>
<div class="collapsible-body collapsible-collection">
<table>
<thead>
<tr>
<th>Comment</th>
<th>Hash tail</th>
<th>Old ownership info</th>
</tr>
</thead>
<tbody>
{% for c in orphaned_keyfobs %}
<tr>
<td>{{c.comment or "unnamed"}}</td>
<td><a href="/m/doorboy/{{ c._id }}/events">{{ c.uid_hash[-6:] }}</a></td>
<td>
{{c.old_display_name}}
{% if c.old_foreign_id %}
({{c.old_foreign_id}})
{% endif %}
</td>
</tr>
{% endfor %}
<tbody>
</table>
</div>
</li>
</ul>
<style>
.collapsible-collection {
padding: 0;
}
</style>
{% endblock %}

View File

@@ -0,0 +1,29 @@
{% extends 'base.html' %}
{% block content %}
<h3>This page only shows UID opens!</h3>
<p>Does not include Slack opens, Web opens (via Doorboy/Inventory). Formats need to be unified. <b>Use #door-log Slack channel Ctrl-f instead!</b></p>
<table>
<thead>
<tr>
<th>Approved</th>
<th>Timestamp</th>
<th>Door</th>
<th>Who (UID hash)</th>
</tr>
</thead>
<tbody>
{% for o in latest_events %}
<tr>
<td>{% if o.approved %}<i class="material-icons">check_circle</i>{% else %}no{% endif %}</td>
<td>{{ o.timestamp | timeago }}</td>
<td>{{ o.door }}</td>
<td>{{ o.inventory.owner_id }} (<a href="/m/doorboy/{{ o._id }}/events">{{ o.token.uid_hash[-6:] }})</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}