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:
		
							
								
								
									
										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>
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user