From 7723cd0d70e424cf801783aeeed2ef0374eba551 Mon Sep 17 00:00:00 2001 From: Kobe Date: Fri, 30 May 2025 13:48:07 +0200 Subject: [PATCH] logging auth, conversations, and contacts --- routes/__pycache__/auth.cpython-313.pyc | Bin 7610 -> 8580 bytes routes/__pycache__/contacts.cpython-313.pyc | Bin 15720 -> 19496 bytes .../__pycache__/conversations.cpython-313.pyc | Bin 20141 -> 24171 bytes routes/auth.py | 112 ++++++++++----- routes/contacts.py | 128 ++++++++++++++++++ routes/conversations.py | 105 +++++++++++++- 6 files changed, 309 insertions(+), 36 deletions(-) diff --git a/routes/__pycache__/auth.cpython-313.pyc b/routes/__pycache__/auth.cpython-313.pyc index c91e76d459a143f4ad26dc30b00058d02a4c3f33..c44381b80c67fc96fcc86d23225eead8584be5fa 100644 GIT binary patch delta 2738 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 U^AHJt8H`ML$9+`COlzM10xN9CJOBUy diff --git a/routes/__pycache__/contacts.cpython-313.pyc b/routes/__pycache__/contacts.cpython-313.pyc index 0ce2c564589848c3c6db5da410479ad18476d02e..07b229186476ea8a0b7a834b8fd225ed94cfe511 100644 GIT binary patch delta 5451 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 DutPV? delta 2937 zcmai0ZERa-6~3>(>W|pXhaJc9M{bieiIb-7h)tTbO_Mgs*0^MCZks`3xO3vW*lX+? z-s?1_+eoV+MiFdkkB|T%*guv(h|Pcy`-#xB{XnW#sgo5!U;^z2Xhtn!pn*8&b?h{u z6<6|e-+Rt^&pqco&pFp`e3yN6&2!)Fc1n2tefC)9*L}AWE_&c_%B_87O`HiZv9I^H0Lz-l3c&}()$o^Q;gzL>XnALsj`-(`Lo+%X+BY1 zDUI+|UtgqY`r-!TE!B3DtxVu17`(h@QU(#|KF#nagE=~b*H$feNI2Xrr*=0Nvb>S-|>!QAzFK|0; zwdJ%zMo$Z8B}+P0NbzZsH(p6CXYo-}^}K4VF66QW4D`4DQ2l2}ZAWifK9$pW1N_S$ z_lb~a=w-E7xdP)Y{p(f|+7O}+ewxRvo`l!i z23RlLZJPzT{mDUx;(Vz5PoP6_%@0&pimTeo)qAHj9|O;BW6Rr3GxYs!x@YC`f!nL}IbhaR$=ejtr&%s{~ zcC23_?JNP2EE?KF0saiB@TC%;ClLIZqZKs=r!(Oef;7QyCi1dJOXE2vq=pJ6h=r(J92Ex8lk z>wbl8!CX&I!=~N&n)9afo;`5K9=K}{Zo*9TJuGgxS9TGvMREV^ge{N1sP_KKWHzV% zT@3YKu$nD1UYhlw$C`^{11~f1#(UvPZ#6gm(WcJHf)55UcKFBpR@ zPH(js6Y!nE*(!Ax3=pkIZO2tQ zZ2l}QQv?eLC7b9^qh#Y+PQRk@J+&2t*(tC=`@Tp(6GF5wZ=ls8fgo0rRyu)(5Os2~ zM@)tD(z7V0KCjH5dVM*qu4ESrt9*L~JWo0r4E!=d7U7Xoy+WhvP`HX_8)SOR3~%C8 zw+@5J{6R|ywoa@gmog_G#iiMiBgmz%kF>ieJ;9|PFya!Zy%Vti)J@h1AD;@DX8iDn z(YESC0B$*3VQ9<^KMr}|Ta!J^1}}|u+XNeC;lY?3q7IF^_m;STQXH;FPq&<}9dXH) z)|NFK*wjWF=7&y-&d8vca28e?$5Sz_4Z;^u`7v36ggHNhlkdedBbdVEChuiuzQ*0hd**9QT>~LdpfH~mjlYQfr{P2C( z_7lc@(9*7oo%s@c)yiY>VmCY|Mm zC~nEh^;In1II6^ViPJdaL`F{45mED2T2UO8GTKk%^!oJbvNpoMjY9e>X}kj$r({^0 zdV}?WobcNESeBLNU^pR%4ze7(ZX%6tc&3^`>dRn)A^;mZJ#;NLtZZ_CIh|2!N9mfHHKZ$6&1|8vwQI;)SIOF_talCBN>~G%TZ5g-)~zAySS9NO zSzFER_rZ0`x4G%O^=r)QSv7AJm2Fr4(&SfpsJlcF#o#hpoBe0O0+GCG88^UhP0#XVe+ijUwsG}(@3x@ zE(PLnz$94?WjBzU+V|K46$O2N3Q zXL5r74onVQDl##4@PV;tcsj!03tavo?9xp~rVmE=vGLe!G$Cj*Pm>ez3lc#?Pjf8s zz)VC?PDB#n$*K4W*Jkpwf{@C>>u`=K(o@*diqi?QiEtuJC*==gR}7ho@XSmkIw43W zqY2)KYH%FCeJtKc6eZxz;;3JP&)aRqSU zu;Buw`Om^~&9lJhhF|>Byt7Eo&fQQj>Y~N6`m64y^S$SKmn!Sd#7@VSs$0|5-KpyC zWL3|_zDrZ-jk{7CcO^FjlkVM%W>4DeO_{xm_H}7{XUg8W9GC z>CT}KI)@f2hk?Gl`^*E+J&@`gN>vVDGpb4r*X3HZ=7yP3YoE|OMy$ncB{#1d8T$xJ z7yEW^>44jG!bDiBX@0tRSW@&Ltd{>BAQxEv6x>eO$w)aD93*lGTcKxFkAg_2bYozVgQ0>-wJDE*PR>a!j- z*<|@rqXix{^f$?W>D=@Ct&gzm*I<@)#8^3CbVDz_I5ilv2aTC!Ny=BE3*lnv1#GbG zZTaWgY)}3UjzP0xL|wio+oJwLD$W)D|Cy^kJ9qxhX~Cs{!yuP?oaD;JVscfXBEd?d z<@RgV`y`x!INf&gLS>QJILf~WQ;7%s61JX4@&octa;1HbetOd}MCB9s@I2KT~04D1sg2f4%Uu|A4#D&xV0 zX1vB(Y-QZeNxOOOaeO|Cgz7z2zwhCr9?26(o&u=1c)G~FZHgpl^J$fHeJ6b2Z|ub8bmncMA8mr{2lnI6@3IDBbX6#}vD+P?BGPAVA@{g8 zu|Fs0TydivCZHFm3`znH8uTOq7a9huAbmcoXNC5Avcma0*oP{lAdMR`fm15ZVF824 z3uRj<9F(FJR%fi{HrmUnLDMyU2#X;zInZMzBc;{?ctCrn7eZB3?J7OQP?=wnMJpc0 zYxt(3lZboIoKUe!eKVntk4(oPww{DpGiuJi0&PJfc2Lze=G&rj=NK9F77DWxltC?{ zXca3mt;*^<`73wR94b5i$N19H+iN`b-{M;_w&zt}P>3)1w%X7C8NMx}`yEv8X+XG* z^6SN!v-*Dt2EGBm_@<2dce5XpCC^SY0KI9^(s{MUcYf~N+@iNV?d?i=yOvyy=bO$o zE!B0T>v~gly-DvKm)W1xr#J0MZQ7IUyE|F4cd=&O`TNe@w^-l&>d1wWmwPWdFW&h# zqkl1)>>f&bhZpM_U#+`P2fANg1kqM+fM}~XJ~8;%U|MHO>1-eB?4RojNpqDA0N-d; zgSG&cN8`Z1E1uTJ$Zx6=N=X=;Tn6PKUgK*`J%H7utFW3cV1<#lTstHqFrSujASRP? zibiQv1~y}>Y$nDwjVy}T@iSM617=giB`qTIGc!z75z0lJpdw%;P3;xrKdQC8O6aM| zz9cBm>S|QA=l%wWY=9fXFj^g|$;qg*G7z_F$<`XXHWzdPI+~h2R$HRaNktVw1$nWi zhHXOIZBS%KJD8Di@k{nRl6liz^i8*!&Aw{3tW8cp<~OpAVfKXalgXi4H`{{mC|&su zfcUsup;q$iHmfAiNtTT^qd)sGVD2nz1~?OSB{k(MGHK{4 z%6?T~W9CGQt2tx#=%SjSCVL2BaC;$i2J{MQ$Ug6o4OB0abgZR%pL+FDMNonVw|mNw z8O?+?XIorBP{I`kS+2-0%jF=_L)mOZk!$67`4;qgi%C~q(_9YTLQ*J=9X!h44=(^* zOM@>a9sGBZV7R^|lbM}?93ta;Ww*%ZG8J^Daj#rNAR2Lq=#|B`JD27Nn%LCD*zxex zY$PsdqLKSDZT?|$vaY#_x?DLfXif+=;TOR7>ORg0FIw8fXQ_!fP=Y2UUFeA^Z(`hmV|I^%!NpYm->RrFs|%1oL^ zH8%{5$wor;pJyS^|0emWti5WjOS*^AX}>8`<4*I?2=v`{si3dK^M znQLmfz37@&i)oqx(zHr3P0NBN#7uWIoVxQE%UA~3o7W9YmdA*V; z?`Pwcut{&0m5#VoKd!Wo)XLt4c7y5N9?wXt`MqlUNT=+C4G>ou`L~8*4PsO&{Jt^) z;+lczvrWN)lL{13B$X*2Q0I43--2ZZIg}Y%G}azK}F@RHaoT4ugY@e=>LASUR+j^p9O5wZ3F zMrmwr(W6m%O}96>wQxND`}kIBbQt&fb2z)4Ry1w_LxMtF36!DeN)hnHv6h*s9tp)_ zKP)L*VA02-{Kp_s&4Q?lIExy{jpp(>!(wsmRr|U#bEoGPoegPcYs%TWR8f7V>2wpI zowTbnL8+)3&yht?lwy z($fTtE5`@iww@+Heb+sj@9(P2O(Y_ltavgKuwaqpBh5#AWJV`cK|MFXV+8RR0g^* zFdH{y`yo@#4D3|^0GC22lR|)N%LF)S6tE1ZYg7OadhG!=p{ScY-Lch)u|kl=Cr?BK=`245 znuYFEP{O$+pu)%}$juJxoHm?DgvXCe11G)<&Xjue7eLT!UmHYIJyC-Y_1>a0rT&-t zaA&xsg6Wvp&#SgFHwd@ z!GFLOmIb`(ydv@fa;K5JKuXs4*zSR*pu-YS#!}+^m%!>rO?(}o_vIohW2*d3nYP4! z-2|;iYnDq%ub(4H|1Q%Fy|z?w4cW_OjJ)tV15!6CNLQEBqlad^9A^3nyQyGXz3Qz( z|F-Iiw>27Qzg?{WdcsY8*&lSdBres{dKV;(+eks{L-S|5-;x?aeK3|@aI!Ll6N(@p z>%b<8=~BJ6xfSW*omtuZ9UPuvg~Zl;Pqsz<;6U~%kB~WK7lgFXZF++8+KhO(nsGBh zIUqURDnz6U4do`(!CDcF10vGvwzEy}Cq|=+E67w*26{W#VQLPtz$ij7`_Amh{GBVz zBC`-j-s$zbzr}|V&mB&77bz^Y%mGFV0rVCxu4u(}?8u)RjY~6Fu$5tE!NJY^eNY?Nl$B-}@T@f^Jmc|wNcBXc%%b-PR6h;9yw1VZs`}svAKSS~>BnQcr1zN|eRnH2&~E$JQ@U-gqP& zA6wxmpzM@$8)EF4TgAe9cbrzD_$3^#o#A72K{J!%^gjzh5l=wref&hxMC4F-b}G>j zK905T#+m40{u6xfr${~n0wo0MLg)u3kegqZJ`R;}sD_1)&*5n59&%m`H^V1`obmU|DE2D~)_+MFH<#_kg$io%SWq+m8K0$au>2P&`fDI@BakfK&B0!j)8Wo% zohq+lA`kvZQ+6NznJ~2AX#Vu>iFYjvj^N)~F1tT; z`d-`oqyE?XKXz_-*YbfQm^?altx%2eUI+2Mf^=^At@XN>aqebsepMkecgGP^{+-;u zLt%d3@86-F-@J8FqNNt~67=xTlOOeOSdpI4QVx#cr;yN~r^FWBLkvBlV$ztT!ACr7 z(bhBrpqKY=;rSRZsJGld9+|=anIHyJbG(XjsGj2g2Xy1v2>q)=CGIlh77q@T%Z;$7 zZ<5mkH=#m61|O~!_lYtaHH6RipQHs+mLsT>N3VCK7^9` zk-j}+TL*@>jBx{dw}`rmng&W7n}yI?hLX=Bp&xz$TUe&$u~x;ukK|({7(eNO^SHVT z41WL_G|;MgAm^K%ifrU=pY;4c!X@_7CFqbznlA<(QmSW42X8+|a ymphZtVuu!4<05OmCULRK)#HX?959#r$%aw8b(Foee<$=KcliK0KI)U delta 4864 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 diff --git a/routes/auth.py b/routes/auth.py index 23dc561..37170ed 100644 --- a/routes/auth.py +++ b/routes/auth.py @@ -2,8 +2,8 @@ from flask import render_template, request, flash, redirect, url_for from flask_login import login_user, logout_user, login_required, current_user from models import db, User from functools import wraps -from utils import log_event from datetime import datetime +from utils import log_event def require_password_change(f): @wraps(f) @@ -29,24 +29,32 @@ def init_routes(auth_bp): if not user or not user.check_password(password): # Log failed login attempt - log_event('user_login', { - 'email': email, - 'success': False, - 'reason': 'invalid_credentials' - }) + log_event( + event_type='user_login', + details={ + 'email': email, + 'success': False, + 'reason': 'invalid_credentials' + } + ) + db.session.commit() flash('Please check your login details and try again.', 'danger') return redirect(url_for('auth.login')) login_user(user, remember=remember) # Log successful login - log_event('user_login', { - 'user_id': user.id, - 'email': email, - 'success': True, - 'remember': remember, - 'using_default_password': password == 'changeme' - }, user.id) + log_event( + event_type='user_login', + details={ + 'user_id': user.id, + 'user_name': f"{user.username} {user.last_name}", + 'email': user.email, + 'success': True, + 'remember': remember + } + ) + db.session.commit() # Check if user is using default password if password == 'changeme': @@ -86,15 +94,17 @@ def init_routes(auth_bp): db.session.add(new_user) db.session.commit() - # Log user registration - log_event('user_register', { - 'email': email, - 'username': username, - 'timestamp': datetime.utcnow().isoformat(), - 'ip_address': request.remote_addr, - 'user_agent': request.user_agent.string, - 'registration_method': 'web_form' - }, new_user.id) + # Log successful registration + log_event( + event_type='user_create', + details={ + 'user_id': new_user.id, + 'user_name': f"{new_user.username} {new_user.last_name}", + 'email': new_user.email, + 'method': 'web_form' + } + ) + db.session.commit() login_user(new_user) return redirect(url_for('main.dashboard')) @@ -104,12 +114,16 @@ def init_routes(auth_bp): @auth_bp.route('/logout') @login_required def logout(): - # Log logout event - log_event('user_logout', { - 'user_id': current_user.id, - 'email': current_user.email - }, current_user.id) - + # Log logout event before logging out + log_event( + event_type='user_logout', + details={ + 'user_id': current_user.id, + 'user_name': f"{current_user.username} {current_user.last_name}", + 'email': current_user.email + } + ) + db.session.commit() logout_user() return redirect(url_for('auth.login')) @@ -122,22 +136,50 @@ def init_routes(auth_bp): confirm_password = request.form.get('confirm_password') if not current_user.check_password(current_password): + # Log failed password change attempt + log_event( + event_type='user_update', + details={ + 'user_id': current_user.id, + 'user_name': f"{current_user.username} {current_user.last_name}", + 'update_type': 'password_change', + 'success': False, + 'reason': 'invalid_current_password' + } + ) + db.session.commit() flash('Current password is incorrect.', 'danger') return redirect(url_for('auth.change_password')) if new_password != confirm_password: + # Log failed password change attempt + log_event( + event_type='user_update', + details={ + 'user_id': current_user.id, + 'user_name': f"{current_user.username} {current_user.last_name}", + 'update_type': 'password_change', + 'success': False, + 'reason': 'passwords_dont_match' + } + ) + db.session.commit() flash('New passwords do not match.', 'danger') return redirect(url_for('auth.change_password')) current_user.set_password(new_password) - db.session.commit() - # Log password change - log_event('user_update', { - 'user_id': current_user.id, - 'email': current_user.email, - 'update_type': 'password_change' - }, current_user.id) + # Log successful password change + log_event( + event_type='user_update', + details={ + 'user_id': current_user.id, + 'user_name': f"{current_user.username} {current_user.last_name}", + 'update_type': 'password_change', + 'success': True + } + ) + db.session.commit() flash('Password changed successfully!', 'success') return redirect(url_for('main.dashboard')) diff --git a/routes/contacts.py b/routes/contacts.py index d48f176..3b18bcc 100644 --- a/routes/contacts.py +++ b/routes/contacts.py @@ -5,6 +5,7 @@ from forms import UserForm from flask import abort from sqlalchemy import or_ from routes.auth import require_password_change +from utils import log_event import json import os from werkzeug.utils import secure_filename @@ -120,6 +121,22 @@ def new_contact(): user.set_password('changeme') # Set default password db.session.add(user) db.session.commit() + + # Log user creation event + log_event( + event_type='user_create', + details={ + 'created_by': current_user.id, + 'created_by_name': f"{current_user.username} {current_user.last_name}", + 'user_id': user.id, + 'user_name': f"{user.username} {user.last_name}", + 'email': user.email, + 'is_admin': user.is_admin, + 'method': 'admin_creation' + } + ) + db.session.commit() + flash('User created successfully! They will need to change their password on first login.', 'success') return redirect(url_for('contacts.contacts_list')) return render_template('contacts/form.html', form=form, title='New User', total_admins=total_admins) @@ -138,6 +155,16 @@ def edit_profile(): flash('There must be at least one admin user in the system.', 'error') return render_template('contacts/form.html', form=form, title='Edit Profile', total_admins=total_admins) + # Store old values for comparison + old_values = { + 'user_name': f"{current_user.username} {current_user.last_name}", + 'email': current_user.email, + 'phone': current_user.phone, + 'company': current_user.company, + 'position': current_user.position, + 'is_admin': current_user.is_admin + } + current_user.username = form.first_name.data current_user.last_name = form.last_name.data current_user.email = form.email.data @@ -146,10 +173,39 @@ def edit_profile(): current_user.position = form.position.data current_user.notes = form.notes.data current_user.is_admin = form.is_admin.data + # Set password if provided + password_changed = False if form.new_password.data: current_user.set_password(form.new_password.data) + password_changed = True + db.session.commit() + + # Log profile update event + log_event( + event_type='user_update', + details={ + 'user_id': current_user.id, + 'user_name': f"{current_user.username} {current_user.last_name}", + 'updated_by': current_user.id, + 'updated_by_name': f"{current_user.username} {current_user.last_name}", + 'old_values': old_values, + 'new_values': { + 'username': current_user.username, + 'last_name': current_user.last_name, + 'email': current_user.email, + 'phone': current_user.phone, + 'company': current_user.company, + 'position': current_user.position, + 'is_admin': current_user.is_admin + }, + 'password_changed': password_changed, + 'method': 'self_update' + } + ) + db.session.commit() + flash('Profile updated successfully!', 'success') return redirect(url_for('contacts.contacts_list')) @@ -220,6 +276,17 @@ def edit_contact(id): if existing_user: flash('A user with this email already exists.', 'error') return render_template('contacts/form.html', form=form, title='Edit User', total_admins=total_admins, user=user) + + # Store old values for comparison + old_values = { + 'user_name': f"{user.username} {user.last_name}", + 'email': user.email, + 'phone': user.phone, + 'company': user.company, + 'position': user.position, + 'is_admin': user.is_admin + } + user.username = form.first_name.data user.last_name = form.last_name.data user.email = form.email.data @@ -228,10 +295,38 @@ def edit_contact(id): user.position = form.position.data user.notes = form.notes.data user.is_admin = form.is_admin.data + # Set password if provided + password_changed = False if form.new_password.data: user.set_password(form.new_password.data) + password_changed = True + db.session.commit() + + # Log user update event + log_event( + event_type='user_update', + details={ + 'user_id': user.id, + 'user_name': f"{user.username} {user.last_name}", + 'updated_by': current_user.id, + 'updated_by_name': f"{current_user.username} {current_user.last_name}", + 'old_values': old_values, + 'new_values': { + 'user_name': f"{user.username} {user.last_name}", + 'email': user.email, + 'phone': user.phone, + 'company': user.company, + 'position': user.position, + 'is_admin': user.is_admin + }, + 'password_changed': password_changed, + 'method': 'admin_update' + } + ) + db.session.commit() + flash('User updated successfully!', 'success') return redirect(url_for('contacts.contacts_list')) return render_template('contacts/form.html', form=form, title='Edit User', total_admins=total_admins, user=user) @@ -246,6 +341,21 @@ def delete_contact(id): if user.email == current_user.email: flash('You cannot delete your own account.', 'error') return redirect(url_for('contacts.contacts_list')) + + # Log user deletion event + log_event( + event_type='user_delete', + details={ + 'user_id': user.id, + 'user_name': f"{user.username} {user.last_name}", + 'deleted_by': current_user.id, + 'deleted_by_name': f"{current_user.username} {current_user.last_name}", + 'email': user.email, + 'is_admin': user.is_admin + } + ) + db.session.commit() + db.session.delete(user) db.session.commit() flash('User deleted successfully!', 'success') @@ -261,7 +371,25 @@ def toggle_active(id): if user.email == current_user.email: flash('You cannot deactivate your own account.', 'error') return redirect(url_for('contacts.contacts_list')) + + old_status = user.is_active user.is_active = not user.is_active + + # Log status change event + log_event( + event_type='user_update', + details={ + 'user_id': user.id, + 'user_name': f"{user.username} {user.last_name}", + 'updated_by': current_user.id, + 'updated_by_name': f"{current_user.username} {current_user.last_name}", + 'update_type': 'status_change', + 'old_status': old_status, + 'new_status': user.is_active, + 'email': user.email + } + ) db.session.commit() + flash(f'User marked as {"active" if user.is_active else "inactive"}!', 'success') return redirect(url_for('contacts.contacts_list')) \ No newline at end of file diff --git a/routes/conversations.py b/routes/conversations.py index 3bcdbe9..523a9b7 100644 --- a/routes/conversations.py +++ b/routes/conversations.py @@ -3,6 +3,7 @@ from flask_login import login_required, current_user from models import db, Conversation, User, Message, MessageAttachment from forms import ConversationForm from routes.auth import require_password_change +from utils import log_event import os from werkzeug.utils import secure_filename from datetime import datetime @@ -83,6 +84,21 @@ def create_conversation(): db.session.add(conversation) db.session.commit() + + # Log conversation creation + log_event( + event_type='conversation_create', + details={ + 'conversation_id': conversation.id, + 'created_by': current_user.id, + 'created_by_name': f"{current_user.username} {current_user.last_name}", + 'name': conversation.name, + 'description': conversation.description, + 'member_count': len(conversation.members), + 'member_ids': [member.id for member in conversation.members] + } + ) + db.session.commit() flash('Conversation created successfully!', 'success') return redirect(url_for('conversations.conversations')) @@ -147,6 +163,22 @@ def add_member(conversation_id): else: conversation.members.append(user) db.session.commit() + + # Log member addition + log_event( + event_type='conversation_member_add', + details={ + 'conversation_id': conversation.id, + 'conversation_name': conversation.name, + 'added_by': current_user.id, + 'added_by_name': f"{current_user.username} {current_user.last_name}", + 'added_user_id': user.id, + 'added_by_name': f"{current_user.username} {current_user.last_name}", + 'added_user_email': user.email + } + ) + db.session.commit() + flash(f'{user.username} has been added to the conversation.', 'success') return redirect(url_for('conversations.conversation_members', conversation_id=conversation_id)) @@ -169,6 +201,22 @@ def remove_member(conversation_id, user_id): else: conversation.members.remove(user) db.session.commit() + + # Log member removal + log_event( + event_type='conversation_member_remove', + details={ + 'conversation_id': conversation.id, + 'conversation_name': conversation.name, + 'removed_by': current_user.id, + 'removed_by_name': f"{current_user.username} {current_user.last_name}", + 'removed_user_id': user.id, + 'removed_user_name': f"{user.username} {user.last_name}", + 'removed_user_email': user.email + } + ) + db.session.commit() + flash('User has been removed from the conversation.', 'success') return redirect(url_for('conversations.conversation_members', conversation_id=conversation_id)) @@ -184,6 +232,13 @@ def edit_conversation(conversation_id): form = ConversationForm(obj=conversation) if request.method == 'POST': + # Store old values for comparison + old_values = { + 'name': conversation.name, + 'description': conversation.description, + 'member_ids': [member.id for member in conversation.members] + } + # Get members from the form data member_ids = request.form.getlist('members') @@ -205,6 +260,25 @@ def edit_conversation(conversation_id): conversation.members.append(user) db.session.commit() + + # Log conversation update + log_event( + event_type='conversation_update', + details={ + 'conversation_id': conversation.id, + 'updated_by': current_user.id, + 'updated_by_name': f"{current_user.username} {current_user.last_name}", + 'old_values': old_values, + 'new_values': { + 'name': conversation.name, + 'description': conversation.description, + 'member_ids': [member.id for member in conversation.members], + 'member_names': [f"{member.username} {member.last_name}" for member in conversation.members] + } + } + ) + db.session.commit() + flash('Conversation members updated successfully!', 'success') # Check if redirect parameter is provided @@ -227,6 +301,20 @@ def delete_conversation(conversation_id): conversation = Conversation.query.get_or_404(conversation_id) + # Log conversation deletion + log_event( + event_type='conversation_delete', + details={ + 'conversation_id': conversation.id, + 'conversation_name': conversation.name, + 'deleted_by': current_user.id, + 'deleted_by_name': f"{current_user.username} {current_user.last_name}", + 'member_count': len(conversation.members), + 'message_count': Message.query.filter_by(conversation_id=conversation_id).count() + } + ) + db.session.commit() + # Delete all messages in the conversation Message.query.filter_by(conversation_id=conversation_id).delete() @@ -264,7 +352,6 @@ def get_messages(conversation_id): 'created_at': message.created_at.strftime('%b %d, %Y %H:%M'), 'sender_id': str(message.user_id), 'sender_name': f"{message.user.username} {message.user.last_name}", - 'sender_avatar': url_for('profile_pic', filename=message.user.profile_picture) if message.user.profile_picture else url_for('static', filename='default-avatar.png'), 'attachments': [{ 'name': attachment.name, 'size': attachment.size, @@ -323,6 +410,22 @@ def send_message(conversation_id): attachments.append(attachment) db.session.commit() + + # Log message creation + log_event( + event_type='message_create', + details={ + 'message_id': message.id, + 'conversation_id': conversation_id, + 'conversation_name': conversation.name, + 'sender_id': current_user.id, + 'sender_name': f"{current_user.username} {current_user.last_name}", + 'has_attachments': len(attachments) > 0, + 'attachment_count': len(attachments), + 'attachment_types': [get_file_extension(att.name) for att in attachments] if attachments else [] + } + ) + db.session.commit() # Prepare message data for response message_data = {