diff --git a/.gitignore b/.gitignore index fa8071c..109502a 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,9 @@ test.* # Autosaves *.dia~ +## Logging files +*.log + ## Deployment files diff --git a/app/__init__.py b/app/__init__.py index 2f960d7..14632b2 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -9,6 +9,7 @@ from config import FlaskApplicationSettings, V5Config app = Flask(__name__) app.config.from_object(FlaskApplicationSettings) +app.v5logger = V5Config.v5logger() # Check security of secret if FlaskApplicationSettings.SECRET_KEY == "a-random-secret-key": diff --git a/app/models/user.py b/app/models/user.py index a4fe29e..13ae483 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -397,7 +397,7 @@ class Member(User): Notify a user with a message. An hyperlink can be added to redirect to the notification source """ - return + # return n = Notification(self.id, message, href=href) db.session.add(n) db.session.commit() diff --git a/app/routes/account/account.py b/app/routes/account/account.py index 489e169..72c3c07 100644 --- a/app/routes/account/account.py +++ b/app/routes/account/account.py @@ -45,6 +45,7 @@ def edit_account(): db.session.commit() current_user.update_trophies("on-profile-update") flash('Modifications effectuées', 'ok') + app.v5logger.info(f"<{current_user.name}> has edited their account") return redirect(request.url) else: flash('Erreur lors de la modification', 'error') @@ -64,6 +65,7 @@ def ask_reset_password(): m = Member.query.filter_by(email=form.email.data).first() if m is not None: send_reset_password_mail(m.name, m.email) + app.v5logger.info(f"<{m.name}> has asked a password reset token") flash('Un email a été envoyé à l\'adresse renseignée', 'ok') return redirect(url_for('login')) elif request.method == "POST": @@ -89,6 +91,7 @@ def reset_password(token): db.session.merge(m) db.session.commit() flash('Modifications effectuées', 'ok') + app.v5logger.info(f"<{m.name}> has reset their password") return redirect(url_for('login')) else: flash('Erreur lors de la modification', 'error') @@ -104,6 +107,7 @@ def delete_account(): if del_form.submit.data: if del_form.validate_on_submit(): + name = current_user.name if del_form.transfer.data: guest = Guest(current_user.generate_guest_name()) db.session.add(guest) @@ -118,6 +122,7 @@ def delete_account(): logout_user() db.session.commit() flash('Compte supprimé', 'ok') + app.v5logger.info(f"<{name}> has deleted their account ({'with' if del_form.transfer.data else 'without'} guest transfer)") return redirect(url_for('index')) else: flash('Erreur lors de la suppression du compte', 'error') @@ -143,6 +148,7 @@ def register(): # Email validation message send_validation_mail(member.name, member.email) + app.v5logger.info(f"<{member.name}> registered") return redirect(url_for('validation') + "?email=" + form.email.data) return render('account/register.html', title='Register', @@ -180,4 +186,5 @@ def activate_account(token): db.session.commit() flash("L'email a bien été confirmé", "ok") + app.v5logger.info(f"<{m.name}> has activated their account") return redirect(url_for('login')) diff --git a/app/routes/account/login.py b/app/routes/account/login.py index b00293b..8d25509 100644 --- a/app/routes/account/login.py +++ b/app/routes/account/login.py @@ -49,6 +49,7 @@ def login(): login_user(member, remember=form.remember_me.data, duration=datetime.timedelta(days=7)) member.update_trophies("on-login") + app.v5logger.info(f"<{member.name}> has logged in") # Redirect safely (https://huit.re/open-redirect) def is_safe_url(target): @@ -71,8 +72,10 @@ def login(): @login_required @check_csrf def logout(): + name = current_user.name logout_user() flash('Déconnexion réussie', 'info') + app.v5logger.info(f"<{name}> has logged out") if request.referrer: return redirect(request.referrer) return redirect(url_for('index')) diff --git a/app/routes/account/polls.py b/app/routes/account/polls.py index 2018462..1a3e9bf 100644 --- a/app/routes/account/polls.py +++ b/app/routes/account/polls.py @@ -28,5 +28,6 @@ def account_polls(): db.session.commit() flash(f"Le sondage {p.id} a été créé", "info") + app.v5logger.info(f"<{current_user.name}> has created the form #{p.id}") return render("account/polls.html", polls=polls, form=form) diff --git a/app/routes/admin/account.py b/app/routes/admin/account.py index 9adecbd..8a7ff6f 100644 --- a/app/routes/admin/account.py +++ b/app/routes/admin/account.py @@ -68,6 +68,7 @@ def adm_edit_account(user_id): # TODO: send an email to member saying his account has been modified user.notify(f"Vos informations personnelles ont été modifiées par {current_user.name}.") flash('Modifications effectuées', 'ok') + app.v5logger.info(f"[admin] <{current_user.name}> has edited <{user.name}>'s data") return redirect(request.url) else: flash('Erreur lors de la modification', 'error') @@ -88,6 +89,7 @@ def adm_edit_account(user_id): db.session.merge(user) db.session.commit() flash('Modifications effectuées', 'ok') + app.v5logger.info(f"[admin] <{current_user.name}> has edited <{user.name}>'s trophies") return redirect(request.url) else: flash("Erreur lors de la modification des trophées", 'error') @@ -105,6 +107,7 @@ def adm_edit_account(user_id): db.session.merge(user) db.session.commit() flash('Modifications effectuées', 'ok') + app.v5logger.info(f"[admin] <{current_user.name}> has edited <{user.name}>'s groups") return redirect(request.url) else: flash("Erreur lors de la modification des groupes", 'error') @@ -154,6 +157,7 @@ def adm_delete_account(user_id): user.delete() db.session.commit() flash('Compte supprimé', 'ok') + app.v5logger.info(f"[admin] <{current_user.name}> has deleted <{user.name}> account") return redirect(url_for('adm')) else: flash('Erreur lors de la suppression du compte', 'error') diff --git a/app/routes/admin/login_as.py b/app/routes/admin/login_as.py index 208775a..500eabf 100644 --- a/app/routes/admin/login_as.py +++ b/app/routes/admin/login_as.py @@ -42,6 +42,7 @@ def adm_login_as(): # Create a safe token to flee when needed s = Serializer(app.config["SECRET_KEY"]) vandal_token = s.dumps(current_user.id) + vandal_name = current_user.name # Login and display some messages login_user(user) @@ -51,9 +52,11 @@ def adm_login_as(): else: flash(f"Connecté en tant que {user.name}") + app.v5logger.info(f"[admin] <{vandal_name}> has logged in as <{user.name}>") + # Return the response resp = make_response(redirect(url_for('index'))) - resp.set_cookie('vandale', vandal_token) + resp.set_cookie('vandale', vandal_token, path='/') return resp # Else return form @@ -75,14 +78,17 @@ def adm_logout_as(): flash("Vous avez vraiment agi de manière stupide.", "error") abort(403) + victim_name = current_user.name user = Member.query.get(id) logout_user() login_user(user) + app.v5logger.info(f"[admin] <{user.name}> has logged out from <{victim_name}>'s account") + if request.referrer: resp = make_response(redirect(request.referrer)) else: resp = make_response(redirect(url_for('index'))) - resp.set_cookie('vandale', '', expires=0) + resp.set_cookie('vandale', '', expires=0, path='/') return resp diff --git a/app/routes/forum/index.py b/app/routes/forum/index.py index d843872..6747ac1 100644 --- a/app/routes/forum/index.py +++ b/app/routes/forum/index.py @@ -73,6 +73,7 @@ def forum_page(f, page=1): current_user.update_trophies('new-post') flash('Le sujet a bien été créé', 'ok') + app.v5logger.info(f"<{t.author.name}> has created the topic #{t.id}") return redirect(url_for('forum_topic', f=f, page=(t,1))) # Paginate topic pages diff --git a/app/routes/forum/topic.py b/app/routes/forum/topic.py index 10dab56..e369a48 100644 --- a/app/routes/forum/topic.py +++ b/app/routes/forum/topic.py @@ -54,6 +54,7 @@ def forum_topic(f, page): current_user.update_trophies('new-post') flash('Message envoyé', 'ok') + app.v5logger.info(f"<{c.author.name}> has posted a the comment #{c.id}") # Redirect to empty the form return redirect(url_for('forum_topic', f=f, page=(t, "fin"), _anchor=str(c.id))) diff --git a/app/routes/polls/vote.py b/app/routes/polls/vote.py index 7079bdb..7e56a6c 100644 --- a/app/routes/polls/vote.py +++ b/app/routes/polls/vote.py @@ -34,7 +34,8 @@ def poll_vote(poll_id): db.session.add(answer) db.session.commit() - flash('Le vote a été pris en compte', 'info') + flash('Le vote a été pris en compte', 'ok') + app.v5logger.info(f"<{current_user.name}> has voted on the poll #{poll.id}") if request.referrer: return redirect(request.referrer) diff --git a/app/routes/posts/edit.py b/app/routes/posts/edit.py index 1a3a12a..31230c4 100644 --- a/app/routes/posts/edit.py +++ b/app/routes/posts/edit.py @@ -84,6 +84,10 @@ def edit_post(postid): for a, file in attachments: a.set_file(file) + flash('Modifications enregistrées', 'ok') + admin_msg = "[admin] " if current_user != p.author else "" + app.v5logger.info(f"{admin_msg}<{current_user.name}> has edited the post #{p.id}") + # Determine topic URL now, in case forum was changed if isinstance(p, Topic): return redirect(url_for('forum_topic', f=p.forum, page=(p,1))) @@ -111,6 +115,10 @@ def delete_post(postid): if not current_user.can_delete_post(p): abort(403) + # Is a penalty deletion + is_penalty = request.args.get('penalty') == 'True' \ + and current_user.priv('delete.posts') + # Users who need to have their trophies updated authors = set() @@ -126,16 +134,21 @@ def delete_post(postid): authors.add(comment.author) if isinstance(p.author, Member): - factor = 3 if request.args.get('penalty') == 'True' else 1 + factor = 3 if is_penalty else 1 p.author.add_xp(xp * factor) db.session.merge(p.author) authors.add(p.author) + admin_msg = "[admin] " if current_user != p.author else "" p.delete() db.session.commit() for author in authors: author.update_trophies("new-post") + + flash("Le contenu a été supprimé", 'ok') + penalty_msg = " (with penalty)" if is_penalty else "" + app.v5logger.info(f"{admin_msg}<{current_user.name}> has deleted the post #{p.id}{penalty_msg}") return redirect(next_page) @@ -149,6 +162,9 @@ def set_post_topcomment(postid): comment.thread.top_comment = comment db.session.add(comment.thread) db.session.commit() + flash("Le post a été défini comme nouvel en-tête", 'ok') + admin_msg = "[admin] " if current_user != comment.author else "" + app.v5logger.info(f"{admin_msg}<{current_user.name}> has set a new top comment on thread #{comment.thread.id}") return redirect(request.referrer) @@ -191,8 +207,11 @@ def move_post(postid): comment.thread = thread db.session.add(comment) db.session.commit() + flash("Le topic a été déplacé", 'ok') + admin_msg = "[admin] " if current_user != comment.author else "" + app.v5logger.info(f"{admin_msg}<{current_user.name}> has moved the comment #{comment.id} to thread #{thread.id}") return redirect(url_for('forum_topic', f=t.forum, page=(t,1))) - + return render('post/move_post.html', comment=comment, search_form=search_form, move_form=move_form) @@ -212,8 +231,10 @@ def lock_thread(postid): db.session.commit() if post.thread.locked: - flash(f"Le thread a été verrouillé") + flash(f"Le thread a été verrouillé", 'ok') + app.v5logger.info(f"[admin] <{current_user.name}> has locked the thread #{post.thread.id}") else: - flash(f"Le thread a été déverrouillé") + flash(f"Le thread a été déverrouillé", 'ok') + app.v5logger.info(f"[admin] <{current_user.name}> has unlocked the thread #{post.thread.id}") return redirect(request.referrer) \ No newline at end of file diff --git a/app/routes/programs/program.py b/app/routes/programs/program.py index 30bde3d..3a96ef3 100644 --- a/app/routes/programs/program.py +++ b/app/routes/programs/program.py @@ -41,6 +41,7 @@ def program_view(page): current_user.update_trophies('new-post') flash('Message envoyé', 'ok') + app.v5logger.info(f"<{c.author.name}> has posted a the comment #{c.id}") # Redirect to empty the form return redirect(url_for('program_view', page=(p, "fin"), _anchor=str(c.id))) diff --git a/app/routes/programs/submit.py b/app/routes/programs/submit.py index a990ac1..f67ba14 100644 --- a/app/routes/programs/submit.py +++ b/app/routes/programs/submit.py @@ -54,6 +54,7 @@ def program_submit(): current_user.update_trophies('new-program') flash('Le programme a bien été soumis', 'ok') + app.v5logger.info(f"<{p.author.name}> has submitted the program #{c.id}") return redirect(url_for('program_index')) return render('/programs/submit.html', form=form) diff --git a/app/templates/programs/submit.html b/app/templates/programs/submit.html index fb6aede..1c896fc 100644 --- a/app/templates/programs/submit.html +++ b/app/templates/programs/submit.html @@ -2,7 +2,7 @@ {% import "widgets/editor.html" as widget_editor %} {% import "widgets/tag_selector.html" as widget_tag_selector with context %} -{% set tabtitle = f"Programmes - Soumettre un programme" %} +{% set tabtitle = "Programmes - Soumettre un programme" %} {% block title %} Programmes »

Soumettre un programme

diff --git a/config.py b/config.py index 063ece9..519ea11 100644 --- a/config.py +++ b/config.py @@ -1,4 +1,5 @@ import os +import logging from datetime import timedelta try: @@ -86,6 +87,18 @@ class DefaultConfig(object): # Tab title prefix. Useful to dissociate local/dev/prod tabs TABTITLE_PREFIX = "" + @staticmethod + def v5logger(): + """ A fully configured logger for v5 activity logs """ + logger = logging.getLogger('v5') + logger.setLevel(logging.DEBUG) + formatter = logging.Formatter('[%(asctime)s] %(levelname)s: %(message)s') + handler = logging.FileHandler('v5_activity.log') + handler.setLevel(logging.DEBUG) + handler.setFormatter(formatter) + logger.addHandler(handler) + return logger + class V5Config(LocalConfig, DefaultConfig): """