11from flask import Flask , render_template , jsonify , request , session , abort , redirect , flash
22from werkzeug .security import generate_password_hash , check_password_hash
33from 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
2323def 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():
3744log = startLogger ()
3845log .info ("System check starting." )
3946log .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! \n Callsign: { 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
331356def 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