Skip to content

Commit 958b052

Browse files
authored
Merge pull request #18 from beackers/editbulletins
fixes #17
2 parents 95a8e40 + 3cb5e42 commit 958b052

File tree

10 files changed

+496
-160
lines changed

10 files changed

+496
-160
lines changed

app.py

Lines changed: 110 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from flask import Flask, render_template, jsonify, request, session, abort, redirect, flash
22
from werkzeug.security import generate_password_hash, check_password_hash
33
from flask_socketio import SocketIO, send, emit
4-
import secrets, time, socket, hashlib, json, logging, sqlite3, functools
5-
import userfunc
4+
import secrets, time, socket, hashlib, json, logging, sqlite3, functools, re
5+
import userfunc, bullfunc
66

77

88
# -------- SETUP -------- #
@@ -21,14 +21,21 @@ def coloredText(text, code):
2121

2222
# Logging
2323
def startLogger():
24+
ansiscapere = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]")
25+
class AnsiEscapeFormatter(logging.Formatter):
26+
def format(self, record):
27+
msg = super().format(record)
28+
return ansiescapere.("", msg)
29+
2430
log = logging.getLogger(__name__)
2531
log.setLevel("INFO")
2632
if not log.handlers:
2733
handler = logging.FileHandler("static/app.log", encoding="utf-8")
2834
streamHandler = logging.StreamHandler()
2935
fmt = "{asctime} [{levelname}]: \n{message}"
3036
formatter = logging.Formatter(fmt, style="{")
31-
handler.setFormatter(formatter)
37+
escapedformatter = AnsiEscapeFormatter(fmt, style="{")
38+
handler.setFormatter(escapedformatter)
3239
streamHandler.setFormatter(formatter)
3340
log.addHandler(handler)
3441
log.addHandler(streamHandler)
@@ -37,6 +44,8 @@ def startLogger():
3744
log = startLogger()
3845
log.info("System check starting.")
3946
log.info(coloredText("Secret key generated!", "34"))
47+
logging.getLogger("werkzeug").setLevel(logging.ERROR)
48+
app.logger.setLevel(logging.WARNING)
4049

4150

4251
# Database
@@ -161,6 +170,10 @@ def showlog():
161170
with open("static/app.log", "r") as log:
162171
return log.read().encode("utf-8"), 200
163172

173+
@app.errorhandler(500)
174+
def err500(e):
175+
return f"{str(e)}\n{e.name}\n{e.description}", e.code
176+
164177
# ======== Control Panel ======== #
165178

166179
@app.route("/control", methods=['GET'])
@@ -172,7 +185,9 @@ def control():
172185
cur = c.cursor()
173186
cur.execute("SELECT * FROM users ORDER BY name")
174187
users = cur.fetchall()
175-
return render_template("control.html", csrf=session["csrf"], users=users)
188+
bulletins = bullfunc.Bulletin.get_all_bulletins()
189+
bulletins = [ b.to_dict() for b in bulletins ]
190+
return render_template("control.html", csrf=session["csrf"], users=users, bulletins=bulletins)
176191

177192
@app.route("/controlapi", methods=['GET', 'POST'])
178193
@logged_in(1)
@@ -217,16 +232,17 @@ def add_user():
217232
abort(403)
218233

219234
log.debug("/control/user/add: form passed basic qualifications")
220-
userfunc.new_user(
235+
new = userfunc.User.new_user(
221236
callsign = newuser["callsign"].lower(),
222237
name = newuser["name"],
223238
active = 1,
224239
permissions = newuser["permissions"],
225240
pwd = newuser["password"]
226241
)
227-
if newuser["permissions"] == 1 and admin_exists() and BOOTSTRAP_ADMIN:
242+
if new.permissions > 0 and admin_exists() and BOOTSTRAP_ADMIN is not None:
228243
BOOTSTRAP_ADMIN = None
229244
session.clear()
245+
return redirect("/login", 301)
230246
log.info(f"New user added! \nCallsign: {newuser["callsign"]}")
231247
return redirect("/control", 301)
232248

@@ -242,33 +258,42 @@ def view_or_edit_user(id: int):
242258
if request.method == "GET":
243259
return render_template("view_user.html", csrf=session["csrf"], user=user)
244260
elif request.method == "DELETE":
245-
if user.permissions == 1 and admin_exists() == 1:
261+
if user.permissions >= 1 and admin_exists() == 1:
246262
log.critical("Last active admin was almost deleted!")
247263
return "cannot delete last admin", 409
248264
if request.headers.get("csrf") != session["csrf"]:
249265
return "CSRF token didn't match", 403
250266
user.delete()
251267
return jsonify({"status": 200}), 200
252268
elif request.method == "POST":
253-
f = request.get_json()
269+
f = dict(request.get_json())
254270
if session["csrf"] != f["csrf"]: return "CSRF token doesn't match. Try reloading.", 409
255-
active = int(f["active"])
256-
if active == 0 and user.permissions == 1 and admin_exists() == 1:
271+
f["active"] = bool(int(f.get("active") or 0))
272+
f["permissions"] = int(f.get("permissions") or 0)
273+
if f["active"] == 0 and user.permissions == 1 and admin_exists() == 1:
257274
log.critical("Nearly deactivated last admin!")
258275
return "cannot deactivate last admin", 409
259-
if user.permissions == 1 and admin_exists() == 1 and int(f.get("permissions")) == 0:
276+
if user.permissions == 1 and admin_exists() == 1 and f["permissions"] < 1:
260277
log.critical("Nearly locked all users out of control panel!")
261278
return "cannot change last admin to normal user", 409
262-
permissions = int(f.get("permissions"))
263-
user.edit(
264-
name=f.get("name"),
265-
permissions=permissions,
266-
callsign=f.get("callsign").lower(),
267-
active=active
268-
)
269-
if f.get("password"):
270-
user.set_new_password(f["password"])
271-
return redirect("/control", code=301)
279+
editable_fields = ["callsign", "name", "active", "permissions"]
280+
old = user.to_dict()
281+
diff = {
282+
k: f[k]
283+
for k in editable_fields
284+
if k in f and f[k] != old[k]
285+
}
286+
if not diff and f["pwdhash"] is None:
287+
return "No changes were submitted", 301
288+
if diff:
289+
try: user.edit(**diff)
290+
except Exception as e:
291+
log.exception(e)
292+
return str(e), 500
293+
if f["pwdhash"] is not None:
294+
user.set_new_password(f["pwdhash"])
295+
log.info(f"{coloredText(user.callsign, 36)}'s password was changed by {coloredText(session["user"], 31)}")
296+
return "Changes saved", 200
272297

273298

274299
# --------- LOGIN ---------- #
@@ -325,71 +350,79 @@ def login():
325350

326351
# ======== Bulletins ======== #
327352

328-
@app.route("/bulletins", methods=['GET'])
353+
@app.route("/bulletins", methods=['GET', "POST"])
329354
@logged_in()
330355
@needs_csrf
331356
def bulletins():
332357
with open("static/config.json", "r") as f:
333358
config = json.load(f)
334359
if not config.get("services").get("bulletins"): return render_template("disabled.html"), 403
335-
return render_template("bulletins.html", csrf=session["csrf"], uname="None")
336-
337-
@app.route("/bulletinsapi", methods=["GET"])
338-
@logged_in()
339-
def bulletinsapiget():
340-
with sqlite3.connect("myop.db") as c:
341-
c.row_factory = sqlite3.Row
342-
cur = c.cursor()
343-
cur.execute("SELECT * FROM bulletins")
344-
b = cur.fetchall()
345-
data = {
346-
"bulletins": []
347-
}
348-
for bulletin in b:
349-
data["bulletins"].append({
350-
"origin": bulletin["origin"],
351-
"title": bulletin["title"],
352-
"body": bulletin["body"],
353-
"timestamp": bulletin["timestamp"],
354-
"expires": bulletin["expires"]
355-
})
356-
return jsonify(data), 200
357-
358-
@app.route("/bulletinsapi", methods=["POST"])
359-
@logged_in()
360-
def bulletinsapipost():
361-
posted = request.form.get("csrf")
362-
stored = session.get("csrf")
363-
if not posted or posted != stored:
364-
abort(403, "CSRF didn't match. Try reloading the page.")
365-
bulletin = request.form
366-
if not isinstance(bulletin, dict):
367-
abort(403, "Bulletin must be in the form of a dictionary. Contact administrators.")
368-
if "title" not in bulletin or "expires" not in bulletin:
369-
abort(403, "bulletin must have a title and expiration time. Revise the bulletin.")
370-
expires = bulletin["expires"]
371-
expires = (time.time() + (int(expires)*60))*1000
372-
with sqlite3.connect("myop.db") as c:
373-
cur = c.cursor()
374-
cur.execute(
375-
"INSERT INTO bulletins (origin, title, body, timestamp, expires) VALUES (?,?,?,?,?)",
376-
(session["user"],
377-
bulletin.get("title"),
378-
bulletin.get("body"),
379-
time.time()*1000,
380-
expires)
360+
if request.method == "GET":
361+
return render_template("bulletins.html", csrf=session["csrf"])
362+
elif request.method == "POST":
363+
newbull = request.form
364+
if not newbull.get("csrf") or newbull["csrf"] != session["csrf"]:
365+
abort(409, "CSRF token didn't match. Try reloading.")
366+
bullfunc.Bulletin.new_bulletin(
367+
origin = session["user"],
368+
title = newbull["title"],
369+
body = newbull.get("body"),
370+
expiresin = newbull["expiresin"]
381371
)
382-
c.commit()
383-
return redirect("/bulletins"), 301
372+
return render_template("bulletins.html", csrf=session["csrf"], origin=session["user"])
373+
384374

385-
@app.route("/bulletinsapi", methods=['DELETE'])
375+
376+
@app.route("/bulletins/all", methods=["GET", "DELETE"])
386377
@logged_in()
387-
def bulletinsapidelete():
388-
with sqlite3.connect("myop.db") as c:
389-
cur = c.cursor()
390-
cur.execute("DELETE FROM bulletins;")
391-
c.commit()
392-
return jsonify({"status": 200})
378+
@needs_csrf
379+
def allbulletins():
380+
if request.method == "GET":
381+
bulletins = bullfunc.Bulletin.get_all_bulletins()
382+
bulletins = [ b.to_dict() for b in bulletins ]
383+
return jsonify({
384+
"bulletins": bulletins,
385+
"status": 200
386+
})
387+
elif request.method == "DELETE":
388+
j = request.get_json()
389+
if j.get("csrf") != session["csrf"]:
390+
return "CSRF token didn't match", 409
391+
bulletins = bullfunc.Bulletin.get_all_bulletins()
392+
for b in bulletins:
393+
try: b.delete()
394+
except Exception as e:
395+
log.info(f"Failed to delete bulletin with ID {b.id}\n{e}")
396+
return f"Couldn't delete bulletin {b.id}", 500
397+
return jsonify({
398+
"status": 200
399+
})
400+
401+
@app.route("/bulletins/<int:id>", methods=["GET", "UPDATE", "DELETE"])
402+
@logged_in()
403+
@needs_csrf
404+
def onebulletin(id: int):
405+
bulletin = bullfunc.Bulletin(id)
406+
if request.method == "GET":
407+
return render_template("view_bulletin.html", bulletin=bulletin)
408+
elif request.method == "UPDATE":
409+
j = request.get_json()
410+
og = bulletin.to_dict()
411+
acceptable_fields = { "title", "origin", "body" }
412+
diff = {
413+
k: j[k]
414+
for k in acceptable_fields
415+
if k in j and j[k] != og[k]
416+
}
417+
if not diff: return "No changes were submitted", 304
418+
try: bulletin.edit(**diff)
419+
except Exception as e:
420+
log.error(f"/bulletins/{id}: error in editing bulletin {bulletin.id}\n{e}")
421+
return e, 500
422+
return "Accepted and edited", 200
423+
elif request.method == "DELETE":
424+
bulletin.delete()
425+
return "Successfully deleted post", 200
393426

394427

395428

0 commit comments

Comments
 (0)