Skip to content

Commit

Permalink
Bugfix in booking page, more sanitization, tweaks here & there
Browse files Browse the repository at this point in the history
  • Loading branch information
sondregronas committed Aug 9, 2023
1 parent 923692e commit a7593e4
Show file tree
Hide file tree
Showing 20 changed files with 253 additions and 91 deletions.
5 changes: 5 additions & 0 deletions BookingSystem/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
API_TOKEN = os.getenv('API_TOKEN')
REGEX_ITEM = r'^(?:(?![\s])[ÆØÅæøåa-zA-Z0-9_\s\-]*[ÆØÅæøåa-zA-Z0-9_\-]+)$'

MIN_DAYS = int(os.getenv('MIN_DAYS', 1))
MAX_DAYS = int(os.getenv('MAX_DAYS', 14))
MIN_LABELS = int(os.getenv('MIN_LABELS', 0))
MAX_LABELS = int(os.getenv('MAX_LABELS', 10))

# Logger setup
logger = Logger(__name__)
if os.getenv('DEBUG') == 'True':
Expand Down
84 changes: 52 additions & 32 deletions BookingSystem/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@
import flask
import requests

import groups
import inventory
import mail
import user
from __init__ import DATABASE, LABEL_SERVER
from __init__ import DATABASE, LABEL_SERVER, MIN_DAYS, MAX_DAYS, MIN_LABELS, MAX_LABELS
from inventory import Item
from sanitizer import VALIDATORS, MINMAX, sanitize, handle_api_exception
from sanitizer import VALIDATORS, MINMAX, sanitize, handle_api_exception, APIException
from utils import login_required, next_july

api = flask.Blueprint('api', __name__)
Expand Down Expand Up @@ -112,6 +111,7 @@ def delete_item(item_id: str) -> flask.Response:

@api.route('/items/<item_id>/label/<variant>/preview', methods=['GET'])
@login_required(admin_only=True)
@handle_api_exception
def get_label_preview(item_id: str, variant: str = 'qr') -> flask.Response:
"""Get a label preview for an item."""
item = inventory.get(item_id)
Expand All @@ -126,7 +126,7 @@ def print_label(item_id: str) -> flask.Response:
# START: Validation
validation_map = {
'print_label_count': VALIDATORS.INT,
'print_label_count_minmax': MINMAX(0, 9),
'print_label_count_minmax': MINMAX(MIN_LABELS, MAX_LABELS),
'print_label_type': VALIDATORS.LABEL_TYPE,
}
form = sanitize(validation_map, flask.request.form)
Expand All @@ -152,7 +152,7 @@ def book_equipment() -> flask.Response:
validation_map = {
'user': VALIDATORS.UNIQUE_ID,
'days': VALIDATORS.INT,
'days_minmax': MINMAX(1, 90),
'days_minmax': MINMAX(MIN_DAYS, MAX_DAYS),
'equipment': VALIDATORS.ITEM_LIST_EXISTS,
}
form = sanitize(validation_map, flask.request.form)
Expand All @@ -179,21 +179,19 @@ def return_equipment(item_id: str) -> flask.Response:
def get_user(userid: str) -> flask.Response:
"""Get user as JSON."""
u = user.get(userid)
if not u:
return flask.abort(404)
return flask.jsonify(u)


@api.route('/users', methods=['POST'])
@login_required()
@handle_api_exception
def register_student() -> flask.Response:
"""Add/update a class in the database."""
con = sqlite3.connect(DATABASE)
cur = con.cursor()

selected_classroom = flask.request.form.get('classroom')
if selected_classroom not in groups.get_all():
return flask.abort(418) # I'm a teapot
sanitize({'classroom': VALIDATORS.GROUP}, {'classroom': selected_classroom})

data = {
'name': flask.session.get('user').name,
Expand All @@ -212,68 +210,84 @@ def register_student() -> flask.Response:
return flask.redirect(flask.request.referrer)


@api.route('/groups', methods=['POST'])
@api.route('/groups', methods=['PUT'])
@login_required(admin_only=True)
@handle_api_exception
def update_groups() -> flask.Response:
"""Update a class in the database."""
new_groups = list()
for group in flask.request.form.get('groups').split('\n'):
if not group.strip():
continue
sanitize({'group': VALIDATORS.GROUP_NAME}, {'group': group.strip()})
new_groups.append(group.strip())

con = sqlite3.connect(DATABASE)
cur = con.cursor()
# noinspection SqlWithoutWhere
cur.execute('DELETE FROM groups')
con.commit()

for group in flask.request.form.get('groups').split('\n'):
if not group.strip():
continue
cur.execute('INSERT INTO groups (classroom) VALUES (?)', (group.strip(),))
for group in new_groups:
cur.execute('INSERT INTO groups (classroom) VALUES (?)', (group,))

con.commit()
con.close()
return flask.redirect(flask.request.referrer)
return flask.Response('Gruppene ble oppdatert.', status=200)


@api.route('/categories', methods=['POST'])
@api.route('/categories', methods=['PUT'])
@login_required(admin_only=True)
@handle_api_exception
def update_categories() -> flask.Response:
"""Update every category in the database."""
new_categories = list()
for category in flask.request.form.get('categories').split('\n'):
if not category.strip():
continue
sanitize({'category': VALIDATORS.CATEGORY_NAME}, {'category': category.strip()})
new_categories.append(category.strip())

con = sqlite3.connect(DATABASE)
cur = con.cursor()
# noinspection SqlWithoutWhere
cur.execute('DELETE FROM categories')
con.commit()

for category in flask.request.form.get('categories').split('\n'):
if not category.strip():
continue
cur.execute('INSERT INTO categories (name) VALUES (?)', (category.strip(),))
for category in new_categories:
cur.execute('INSERT INTO categories (name) VALUES (?)', (category,))

con.commit()
con.close()
return flask.redirect(flask.request.referrer)
return flask.Response('Kategoriene ble oppdatert.', status=200)


@api.route('/emails', methods=['POST'])
@api.route('/emails', methods=['PUT'])
@login_required(admin_only=True)
@handle_api_exception
def update_emails() -> flask.Response:
"""Update every email in the database."""
new_emails = list()
for email in flask.request.form.get('emails').split('\n'):
if not email.strip():
continue
sanitize({'email': VALIDATORS.EMAIL}, {'email': email.strip()})
new_emails.append(email.strip())

con = sqlite3.connect(DATABASE)
cur = con.cursor()
# noinspection SqlWithoutWhere
cur.execute('DELETE FROM emails')
con.commit()

for email in flask.request.form.get('emails').split('\n'):
if not email.strip():
continue
cur.execute('INSERT INTO emails (email) VALUES (?)', (email.strip(),))
for email in new_emails:
cur.execute('INSERT INTO emails (email) VALUES (?)', (email,))

con.commit()
con.close()
return flask.redirect(flask.request.referrer)
return flask.Response('E-postene ble oppdatert.', status=200)


@api.route('/email/report', methods=['POST'])
@login_required(admin_only=True, api=True)
@handle_api_exception
def email_report() -> flask.Response:
"""Emails a report to all users in the emails table.
Expand All @@ -282,13 +296,18 @@ def email_report() -> flask.Response:
If the interval parameter is set, the report will only be sent
if the last report was sent more than interval days ago.
You can only send an email once every hour by default. (Handled by mail.py)
"""
interval = flask.request.args.get('interval')

if interval:
last_sent = datetime.fromtimestamp(float(mail.get_last_sent())).date()
sanitize({'interval': VALIDATORS.INT}, flask.request.args)
current_date = datetime.now().date()
last_sent = datetime.fromtimestamp(float(mail.get_last_sent())).date()
if last_sent and (current_date - last_sent).days < int(interval):
return flask.Response(f'Ikke sendt - mindre enn {interval} dager siden forrige rapport.', status=200)
raise APIException(f'Ikke sendt - mindre enn {interval} dager siden forrige rapport.', 200)

return mail.send_report()


Expand All @@ -300,6 +319,7 @@ def prune_inactive_users() -> flask.Response:
Example cronjob:
0 1 1 7 * curl -X POST "http://localhost:5000/users/prune_inactive?token=<token>"
"""
# TODO: run this every day as a background task here instead of an external cronjob?
user.prune_inactive()
return flask.Response('Inaktive brukere ble fjernet.', status=200)

Expand Down
8 changes: 6 additions & 2 deletions BookingSystem/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import mail
import routes
import user
from __init__ import logger, REGEX_ITEM
from __init__ import logger, REGEX_ITEM, MIN_DAYS, MAX_DAYS, MIN_LABELS, MAX_LABELS
from db import init_db
from flask_session import Session

Expand Down Expand Up @@ -60,7 +60,11 @@ def context_processor() -> dict:
used_ids=inventory.get_all_ids(),
users=user.get_all_active_users(),
unavailable_items=inventory.get_all_unavailable(),
overdue_items=inventory.get_all_overdue())
overdue_items=inventory.get_all_overdue(),
MIN_DAYS=MIN_DAYS,
MAX_DAYS=MAX_DAYS,
MIN_LABELS=MIN_LABELS,
MAX_LABELS=MAX_LABELS, )

@app.errorhandler(401)
def unauthorized(_) -> flask.Response:
Expand Down
15 changes: 12 additions & 3 deletions BookingSystem/inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ def api_repr(self) -> dict:
'last_seen': self.last_seen
}

def mail_repr(self) -> str:
return f'<strong>{self.id}</strong> <small>{self.name} ({self.category})</small>'

def __str__(self) -> str:
if self.order_due_date:
return f'{self.lender_name}: {self.id} - {self.name} ({self.category}, {parser.parse(self.order_due_date):%d.%m.%Y})'
Expand All @@ -79,6 +82,12 @@ def lender_association(self) -> str:
return 'Sjekk historikk'
return self.user.get('classroom') or 'Lærer'

@property
def lender_association_mail(self) -> str:
if not self.user.get('name'):
return 'Slettet bruker [Sjekk historikk]'
return self.user.get('classroom') or 'Ansatt'

@property
def classroom(self) -> str:
if '(' in self.lender_association:
Expand Down Expand Up @@ -161,7 +170,7 @@ def delete(item_id: str) -> None:
logger.info(f'Slettet utstyr {item_id}.')
except sqlite3.IntegrityError:
logger.error(f'{item_id} eksisterer ikke.')
raise APIException(f'{item_id} eksisterer ikke.')
raise APIException(f'{item_id} eksisterer ikke.', status_code=404)
finally:
con.close()
audits.audit('ITEM_REM', f'{item_id} ble slettet.')
Expand All @@ -170,12 +179,12 @@ def delete(item_id: str) -> None:
def get(item_id: str) -> Item:
"""Return a JSON object of the item with the given ID."""
con = sqlite3.connect(DATABASE)
item = Item(*con.execute(read_sql_query('get_item.sql'), {'id': item_id}).fetchone())
item = con.execute(read_sql_query('get_item.sql'), {'id': item_id}).fetchone()
con.close()
if not item:
logger.error(f'{item_id} eksisterer ikke.')
raise APIException(f'{item_id} eksisterer ikke.')
return item
return Item(*item)


def get_all() -> list[Item]:
Expand Down
22 changes: 16 additions & 6 deletions BookingSystem/mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import inventory
from __init__ import DATABASE, logger
from sanitizer import APIException

SMTP_SERVER = os.getenv('SMTP_SERVER')
SMTP_PORT = int(os.getenv('SMTP_PORT')) if os.getenv('SMTP_PORT') else 587
Expand Down Expand Up @@ -66,9 +67,9 @@ def formatted_overdue_items() -> str:
<br><br>
"""
items = [item for item in inventory.get_all_unavailable() if item.overdue]
pairs = {item.lender_association: [item2
for item2 in items
if item2.lender_association == item.lender_association]
pairs = {item.lender_association_mail: [item2.mail_repr()
for item2 in items
if item2.lender_association_mail == item.lender_association_mail]
for item in items}
sorted_pairs = {key: pairs[key] for key in sorted(pairs) if pairs[key]}
w = '<td width="10" style="width: 10px;"></td>'
Expand All @@ -84,11 +85,19 @@ def formatted_overdue_items() -> str:


def send_report() -> flask.Response:
"""Send an e-mail to all emails in the database."""
"""Send an e-mail to all emails in the database.
Force is for debugging only.
"""
# If the last sent email was sent within the past hour, raise an exception
if get_last_sent():
if datetime.now().timestamp() - float(get_last_sent()) < 3600:
raise APIException('Rapport ble ikke sendt: forrige rapport ble sendt for under en time siden.', 400)

items = [item for item in inventory.get_all_unavailable() if item.overdue]
if not items:
update_last_sent()
return flask.Response('Ikke sendt (intet å rapportere!)', status=400)
raise APIException('Rapport ble ikke sendt: finner ikke overskredet utstyr.', 400)

title = f'[UtstyrServer] Rapport for overskredet utstyr {datetime.now().strftime("%d.%m.%Y")}'
recipients = get_all_emails()
Expand Down Expand Up @@ -120,8 +129,9 @@ def send_report() -> flask.Response:
<br>{SMTP_FROM}</p>
""".encode('utf-8')
server.sendmail(SMTP_USERNAME, recipients, message)
# TODO: Handle exceptions properly
except Exception as e:
logger.warning(f'Failed to send email: {e}')
return flask.Response('Klarte ikke sende rapport, prøv igjen eller kontakt en administrator.', status=500)
raise APIException('Klarte ikke sende rapport, prøv igjen eller kontakt en administrator.', 500)
update_last_sent()
return flask.Response('Rapport sendt!', status=200)
3 changes: 3 additions & 0 deletions BookingSystem/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import mail
from __init__ import KIOSK_FQDN, LABEL_SERVER
from db import add_admin
from sanitizer import handle_api_exception
from utils import login_required

app = flask.blueprints.Blueprint('app', __name__)
Expand Down Expand Up @@ -72,12 +73,14 @@ def inventar_add() -> str:

@app.route('/inventar/edit/<item_id>')
@login_required(admin_only=True)
@handle_api_exception
def edit_item(item_id: str) -> str:
return flask.render_template('inventar_edit.html', item=inventory.get(item_id))


@app.route('/inventar/print/<item_id>')
@login_required(admin_only=True)
@handle_api_exception
def print_item(item_id: str) -> str:
return flask.render_template('inventar_print.html', item=inventory.get(item_id))

Expand Down
Loading

0 comments on commit a7593e4

Please sign in to comment.