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
import const
import pytz
from bson.objectid import ObjectId
from common import User
from dateutil.parser import ParserError, parse
from flask import Blueprint, abort, g, redirect, render_template, request
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 import BooleanField, IntegerField, SelectField, StringField, validators
from wtforms.validators import DataRequired
page_doorboy = Blueprint("doorboy", __name__)
@@ -41,48 +38,6 @@ def view_doorboy_claim(event_id):
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")
enabled = BooleanField("Enabled")
@@ -148,6 +103,7 @@ def view_doorboy_hold():
})
return redirect("/m/doorboy")
# Writes open event to log, which is picked up by doorboy-proxy.
@page_doorboy.route("/m/doorboy/<door>/open")
@login_required
def view_doorboy_open(door):
@@ -182,14 +138,9 @@ def view_doorboy_open(door):
def view_doorboy():
user = read_user()
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")
@login_required(groups=["k-space:board", "k-space:kubernetes:admins"])
def view_user_cards(username):
return view_user_cards_inner(username)
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/me")
@login_required
@@ -210,71 +161,9 @@ def view_user_cards_inner(username):
}).sort([("last_seen", -1)])
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"])
def view_doorboy_admin():
results = db.inventory.aggregate([
{ "$match": {"component": "doorboy", "type": "token"} },
{
"$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())
def view_user_events(username):
latest_events = db.eventlog.find({"component": "doorboy", "inventory.owner_id": username}).sort([("timestamp", -1)])
return render_template("doorboy_log.html", latest_events=latest_events)

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 %}