From f00d569db39d6075cdff3047ca7be203ec9770e5 Mon Sep 17 00:00:00 2001 From: Kobe Date: Thu, 29 May 2025 14:27:15 +0200 Subject: [PATCH] Added events system --- __pycache__/models.cpython-313.pyc | Bin 20607 -> 20138 bytes app.py | 1 + models.py | 41 +-- routes/__pycache__/auth.cpython-313.pyc | Bin 6359 -> 7078 bytes .../__pycache__/conversations.cpython-313.pyc | Bin 20141 -> 21482 bytes routes/__pycache__/main.cpython-313.pyc | Bin 35731 -> 41967 bytes routes/auth.py | 29 ++ routes/conversations.py | 51 +++- routes/main.py | 171 ++++++++++- static/js/events.js | 272 ++++++++++++++++++ static/js/file-grid.js | 85 ++++-- static/js/room-members.js | 98 ++++++- static/js/rooms-list.js | 133 +++++++++ templates/settings/settings.html | 14 +- templates/settings/tabs/events.html | 165 +++++++++++ templates/starred/starred.html | 89 ++++++ templates/trash/trash.html | 62 ++++ utils/__init__.py | 18 ++ utils/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 581 bytes .../__pycache__/event_logger.cpython-313.pyc | Bin 0 -> 5535 bytes utils/__pycache__/helpers.cpython-313.pyc | Bin 0 -> 1917 bytes utils/__pycache__/permissions.cpython-313.pyc | Bin 0 -> 1163 bytes utils.py => utils/helpers.py | 36 --- utils/permissions.py | 35 +++ 24 files changed, 1186 insertions(+), 114 deletions(-) create mode 100644 static/js/events.js create mode 100644 templates/settings/tabs/events.html create mode 100644 utils/__init__.py create mode 100644 utils/__pycache__/__init__.cpython-313.pyc create mode 100644 utils/__pycache__/event_logger.cpython-313.pyc create mode 100644 utils/__pycache__/helpers.cpython-313.pyc create mode 100644 utils/__pycache__/permissions.cpython-313.pyc rename utils.py => utils/helpers.py (54%) create mode 100644 utils/permissions.py diff --git a/__pycache__/models.cpython-313.pyc b/__pycache__/models.cpython-313.pyc index 19e3c6de6ae7a5981dfccd6cd4a48ac33ee60c4e..bc5267f62d72eaed0b0e2a9a349af4880392617a 100644 GIT binary patch delta 704 zcmYL`KWx)L6vll~mLzrvBx$H2LJ?&MAX+hX>0pv;oYa3R+i6G3YK|l*u_N0lYbO3I zfH)>31T!1T%+5w7mdIjZ$qJ}thRd@ftkb*i_r9m^?&QB8(8qTue=Z1Fj(snT-ND1WupW0dd2q=!BtjG8i*$ukS>^?_Q_XeQ_vsO>Ec(ZnaGd z8&M9hd)SmL9Od@g3bwn4iY_%1XkrC3M*yd(A8X+8fCe@#-M~=>NY#(l2?o|Am7*4V zp`q)l-7ql310;#DlN_7@r=z#EC<{`-(h;U&Ypb4&^C~t?sfBG5YgSa0EK6$0D%1A* zQuNnGnO5Zdp3DC76K-+2I_oU1+?X9KN;hX}tN{P_inW=tC|sMhV|S(Kj$Eq6JqQrD z&(f7eTwz??{+6!Hzsj8yTF%9TODJ{D(K||jHt2h$iYoN8qGbz+a37EoZIDs=TjVECSXW=BC+RZLZ*+27l z+ra}1;pz@XdJDNdIM_EXz(956RdBOu_GJ`&83iVbP%x4CoS5o{;YJ4&7@5sOZVxJ(;suDH47`e}C{kZW!Ix2B zvIqqUNUOwD5JfjSm{5VFYyme$bDKbwiqL>XMXvf*g<=^HC@X^KhA4>2woEbDLpBWs z707}F+z|@YxpxaSL3MljitK@%D9m0XJ?zCoq8-mG@)l5`pn07KMonH-U?Ybm0HLr3 z{0;P(P2Oj=hnTMyG)>5_OF-7>IugOnUU zcalpNF|*PQET){Pr`' class EventType(Enum): - # User events USER_LOGIN = 'user_login' USER_LOGOUT = 'user_logout' - USER_CREATE = 'user_create' + USER_REGISTER = 'user_register' USER_UPDATE = 'user_update' - USER_DELETE = 'user_delete' - - # Room events - ROOM_CREATE = 'room_create' - ROOM_UPDATE = 'room_update' - ROOM_DELETE = 'room_delete' - ROOM_MEMBER_ADD = 'room_member_add' - ROOM_MEMBER_REMOVE = 'room_member_remove' - ROOM_PERMISSION_UPDATE = 'room_permission_update' - - # File events FILE_UPLOAD = 'file_upload' - FILE_DOWNLOAD = 'file_download' FILE_DELETE = 'file_delete' - FILE_RENAME = 'file_rename' + FILE_DOWNLOAD = 'file_download' + FILE_RESTORE = 'file_restore' FILE_MOVE = 'file_move' + FILE_RENAME = 'file_rename' FILE_STAR = 'file_star' FILE_UNSTAR = 'file_unstar' - - # Conversation events + ROOM_CREATE = 'room_create' + ROOM_DELETE = 'room_delete' + ROOM_UPDATE = 'room_update' + ROOM_JOIN = 'room_join' + ROOM_LEAVE = 'room_leave' CONVERSATION_CREATE = 'conversation_create' - CONVERSATION_UPDATE = 'conversation_update' CONVERSATION_DELETE = 'conversation_delete' - CONVERSATION_MEMBER_ADD = 'conversation_member_add' - CONVERSATION_MEMBER_REMOVE = 'conversation_member_remove' - - # Message events - MESSAGE_CREATE = 'message_create' - MESSAGE_UPDATE = 'message_update' - MESSAGE_DELETE = 'message_delete' - MESSAGE_ATTACHMENT_ADD = 'message_attachment_add' - MESSAGE_ATTACHMENT_REMOVE = 'message_attachment_remove' - - # Settings events - SETTINGS_UPDATE = 'settings_update' + MESSAGE_SENT = 'message_sent' + ATTACHMENT_DOWNLOAD = 'attachment_download' class Event(db.Model): __tablename__ = 'events' diff --git a/routes/__pycache__/auth.cpython-313.pyc b/routes/__pycache__/auth.cpython-313.pyc index 3792ef580305a6f27c6b6e6256f3aab83f7c272b..76dea33a6a83b3cfae1c2a6560537ad9ac63ec02 100644 GIT binary patch delta 1959 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 diff --git a/routes/__pycache__/conversations.cpython-313.pyc b/routes/__pycache__/conversations.cpython-313.pyc index bc544d10d6f37abf868f34a0587c4651e66912b6..80b98ba006015c6d4d4679a049df56828fbdc34f 100644 GIT binary patch delta 5083 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 LnuFv3;dcKAkmiIh delta 3885 zcma)8du&tJ8NcWH5kKNMiJiob9dJSjF(EGqD~6PJU?hQ%Txg(4ah>D>hS*Nebv8=Z zhSF_ilojaLsoMmjDw;N?c4M-(Y3R`o~baAhd@jZQA#piybHJ zq+Q8BpZop3bME&#-#LHz0ekHvE4t@$IV5~OS+`{O)uqRaeC*QkdHtJ0k|HVQuF_5q z_lU8j%iCGT%f#5)RnE(iFX*c1^l_h%*}D9le(n!RgRDkU?1PnxLvcn-JTO?^w*)@4 zdth{xXTDOXxE9OwJxSuTS|z1uFXOWnHxDVrl@hO3N)T%X1AXh^!!_Qb#mrD0bPSd$ z9tfENmRdowdQXX0DO1XuWu+n_3*IOAxmn)rpEkQvuyr{$FpUigc3zIH!j)NWo`*NLN7v$J^)8^I-wlT+sr};D= z*`RrsHQWGJ^ApSh2hDA8-rSt|xA`^MvI8Sq2R|vuta^xY(T|`@VVQ5Ex9tQ22pT0I z#2-P_OLwX1=+5}48ddkD)ua|rCHW8%VK{AD%%Yh;+JUJ52E7sv0PoWrx~%YvCnVJL}2( z!4;F?(fJOOMX|!A)`jH*ji@UgrC_V1*dlVR6t+WCNq^NCigo9Rstxn_SQ?Gi9m8CW zrPYz>(Bts;k{!VcUWht;FF^r;g@7#Jr7%`nwNNZ!9*L!6{3|5gNsvU)t+BB&H94Z2 z;>k33lUO(zhqp>++sybLw(tYs@Kgo#f^kjd(fEjNrh!J^zz|RnV7bTdID+p>@uw}w z#5@*e`pD*jAA0LCuGhS~1cwNY6TE<+5oOp+ zB0&;}N_i8G2CHDH@B3)-2foMU;^#>xXZOF_ptZyYPX{9GD7+D9W{2QTAXp~C1*BO> zAaam|V9@tKg5D|86x^s`-%QhVmf+t(l{~{>Z?MvG0%c>F!@(1#vX^KXVa2y;bd=yQ zEDN2OcnXK%QeH%o&og62DcDHw070BUgo*)0YiVPDns9Kxh%`0Lze;naycdalg=C+TL%y2`$^;i>XQv(EzhCtS zP^>$QjNuN{_;VmP_Ed>JSt9Ju-=q92s-Mjy8{M*#_PuCqud}?yc90drr!7@*Zn?*X z{mUF-eP!^|6+wBR9JV{E;2&jP_{3ev=&c3L&35K>zeTJ7{?pp(%!QgCh3LXom#FWe zyx^^v3SKc4qW?S0}X0{*hENQ`A9*oRo1F$-xR*#{EZr0-aRoyhsN6!=(IVr#(;YpI_8vHi0 z%qV4QHsR3!E@iRh?5HGiZzM_NQRFt~7tgg1o@;dcTQp5Hy0^(78y3gTbUdN*GekIZ z+moMyZ!T@AyF^klonJ% za4Z2bK-_GBit{B0AY)>fFzeJi1Lr3e34(>;lG0N!q#uIyPpupS!M zMCN=Emzwbu;q_^Njw$isAovZ!7QE$%){xCWbogrLAfevW?I|5Oor)YGU;2i==K7W^Bs!WTFojXA-TX)|t$7pt^y+_I- z)KuL%i|aNK-M#ALx|v48WZf==BFNWh##MsfBWQG7hNeBn?XXb8dr-Fe`9~6l&4-sq z?G>4-AGR-R%)Gch&g4lO6t&mE@P;dv%cjW2VtAml+evL)G{uiu=K0Qg28&jen-$Ae zGo0)4dKGKLR4Y|W1G7*~JOd#((p`mp??>G+W`n|w^+xwIj(Ff3zc5k4i%~W2V5fNG zUoz-*#t|>_D0aPrSW1^sZjKGf5p`b&=P9n+JN6B$;vyNg8Wnk-^h8X>Hz;7bm#G?M z$B2{>A`d(k@f7kqC?25Dyd%L=n_|okA8hiPZn=HX($il{KAvCExVllg&{({t#dP7p z`ZW!vi*p%{FE&^Z!_Dw&&#n+|ixf|2oX$br$y4KLRcnfko#cW_ayLqu4_QpTT>>1Nt!32%aV2h50gm~W9+sxd_x+(E)Cz7+HOl7 z?@1l+OEvFH&G2CFi9nETX7@~z>?bHDXrHu_aI!t~x87D(>1OTI`7SnTMd4%(tW^4Y ym$QQDbT857{YkuPZ diff --git a/routes/__pycache__/main.cpython-313.pyc b/routes/__pycache__/main.cpython-313.pyc index 28d31bbb015e78197884db5123a30176c2bd1780..21b9a7e68a7ae5de7c38f4e04f5455b4789be60d 100644 GIT binary patch delta 8831 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 delta 4112 zcma)94RBP|6~1@h{{Lk|HX-D1cgbSd1c*QqFk(y)1a<>~EQ^E$md)+sfX&JK+^(YO*wp3^Hb|#Eg}2tkCG7SBz&Bru^yq6Qaccv{=Y8JD$f)IR3XJ*LsdiO&UJG9IOcBc@X7f<>mov=kmOoz&Oh*j`v^_R>n& zWv-$%V_EZP4fVlItA0#Z!yBu9!0G!}zeP7dyy1x6iuxY7+OTHh9Z2HpD8_^oj>J-l zG_xbG0KtX=gCt5)B9mrLYza~}rD&zZVNucdC6cnp*lM`DvD~*ArIzeKG82i3v2gDo zYeEyZB3qY`(ivG1l46>zg&#JS(qed}v67a+yNx#wEk+fiB(pv_krG)cHuYIKn$8?z zi?CrxiK$+ZNs16j4=O?;9cB)dpOr)wPQ(-;F3PNhvzWlaSH-p=t;jskXWUyKP6frv(E8(SR5e!((`ZNvEZaq|#R+yU)5fb51>Gr}^TLoNQ>@6+r@>!e4plRNy%V+uKeA;_SHyPPdiNjU|EqQKQ2nn+mdkF~%aBjqj z({n_45W83B4b2Gn&eEi{(7MTm(=IfHX6KsLg@n#VXd5ZA>GIayB&3Ch{Z2R*aN)ou zL2F*rtsB?(zwS3f zdtlZn_>ay4ruL$|lW;v?)}-~|$oUK%dh@2VA*7!T6HnpYadoL~Tmt9kE&-(4T@{N- zNT2uHJ%0&{gIb7f-+cGHRW14aT*{luzxg_U=IVP$H|{9)F_1_DA!#EFY}cPxu~Wiw?an{Lyzp-c)f%k)43eJFz~Rsrg-PcksM;k9SBlEuiCsH} zu6XOtwx4dF@HSlZHjMU+d)GhIe#z>YT(azJ&*`3tC5;!CG>-Oc!CX|2 zk5UTflaE1qZ>z1lm^0NUs1L!=j$#^udTX&YL_>rZcAb~_jPTsPWj6y?VznF;L5}n) z`e-JZVbb9eNtES8Ixf}ci4_OrRMMw~%J6cVo9p{*x&5;b+IAMvLKp~FE>*Vy92pRw5`RWdKwaMsJ?1)79T?ihNktbEd$VzMtFtFiraDxJ)XNC7Ud`=j zb_NAcBj8B<1|u`}eNOqb^VHoa{}BgC%K>*6^KwG5Wd~vrSqw+P8TZlTSWEnV!SX69 z&$xa5?iTK=y}#b}8t3}#^J3Qa|N5nRGZRMDhuJVb(ceIikB#?VH{3Ajq*O+~1|LaR z`OxplYb$YR#Sja`ruB&IMk<<{uCy5#B`jBf@2bw-DY&_!H(Idk5hPf(-%7?YzZNgP><__$XWD z$<@PTCZ5UFLp3T`h;e1q1Zz6zr)S2F9DK+~-vj%Qn_sxBA!XdYfhYal+Hb+vK{(y?h(6=)W-<_& z;*|IHSs88zb{B$r?ircb$M53&H@DJrn*movLX4&f&V z&m;UCA@~c#Un5*V7)SUkLNSJVlv7dxhrItYv&ijmOCp_+!>XGStKjQ$#j*E}n6Pic8`stn#fb;>raX{9<|^0_6SZ{(?3 zuVU#-Mx_2Q&uIRIW#)V}B>cLPiHS)`y;Q)qrxpzW7kA$BpY 0, + 'attachment_count': len(attachments) + } + ) + # Prepare message data for response message_data = { 'id': message.id, @@ -358,6 +394,19 @@ def download_attachment(message_id, attachment_index): try: attachment = message.attachments[attachment_index] + + # Log attachment download + log_event( + event_type=EventType.ATTACHMENT_DOWNLOAD, + user_id=current_user.id, + details={ + 'conversation_id': conversation.id, + 'message_id': message_id, + 'attachment_name': attachment.name, + 'attachment_size': attachment.size + } + ) + return send_file( attachment.path, as_attachment=True, diff --git a/routes/main.py b/routes/main.py index 9038adf..67159fd 100644 --- a/routes/main.py +++ b/routes/main.py @@ -1,7 +1,8 @@ -from flask import render_template, Blueprint, redirect, url_for, request, flash, Response +from flask import render_template, Blueprint, redirect, url_for, request, flash, Response, jsonify from flask_login import current_user, login_required -from models import User, db, Room, RoomFile, RoomMemberPermission, SiteSettings +from models import User, db, Room, RoomFile, RoomMemberPermission, SiteSettings, Event, EventType from routes.auth import require_password_change +from utils.event_logger import log_event import os from werkzeug.utils import secure_filename from sqlalchemy import func, case, literal_column, text @@ -279,6 +280,14 @@ def init_routes(main_bp): os.remove(old_picture_path) current_user.profile_picture = None db.session.commit() + + # Log profile picture removal + log_event( + event_type=EventType.USER_UPDATE, + user_id=current_user.id, + details={'action': 'remove_profile_picture'} + ) + flash('Profile picture removed successfully!', 'success') return redirect(url_for('main.profile')) @@ -289,6 +298,10 @@ def init_routes(main_bp): if existing_user: flash('A user with this email already exists.', 'error') return render_template('profile/profile.html') + + # Track changes for event logging + changes = {} + # Handle profile picture upload file = request.files.get('profile_picture') if file and file.filename: @@ -296,14 +309,31 @@ def init_routes(main_bp): file_path = os.path.join(UPLOAD_FOLDER, filename) file.save(file_path) current_user.profile_picture = filename + changes['profile_picture'] = True + # Update user information - current_user.username = request.form.get('first_name') - current_user.last_name = request.form.get('last_name') - current_user.email = new_email - current_user.phone = request.form.get('phone') - current_user.company = request.form.get('company') - current_user.position = request.form.get('position') - current_user.notes = request.form.get('notes') + if current_user.username != request.form.get('first_name'): + current_user.username = request.form.get('first_name') + changes['username'] = True + if current_user.last_name != request.form.get('last_name'): + current_user.last_name = request.form.get('last_name') + changes['last_name'] = True + if current_user.email != new_email: + current_user.email = new_email + changes['email'] = True + if current_user.phone != request.form.get('phone'): + current_user.phone = request.form.get('phone') + changes['phone'] = True + if current_user.company != request.form.get('company'): + current_user.company = request.form.get('company') + changes['company'] = True + if current_user.position != request.form.get('position'): + current_user.position = request.form.get('position') + changes['position'] = True + if current_user.notes != request.form.get('notes'): + current_user.notes = request.form.get('notes') + changes['notes'] = True + # Handle password change if provided new_password = request.form.get('new_password') confirm_password = request.form.get('confirm_password') @@ -312,9 +342,20 @@ def init_routes(main_bp): flash('Passwords do not match.', 'error') return render_template('profile/profile.html') current_user.set_password(new_password) + changes['password'] = True flash('Password updated successfully.', 'success') + try: db.session.commit() + + # Log profile update if any changes were made + if changes: + log_event( + event_type=EventType.USER_UPDATE, + user_id=current_user.id, + details={'changes': changes} + ) + flash('Profile updated successfully!', 'success') except Exception as e: db.session.rollback() @@ -355,11 +396,18 @@ def init_routes(main_bp): site_settings = SiteSettings.get_settings() active_tab = request.args.get('tab', 'colors') + + # Get events for the events tab + events = [] + if active_tab == 'events': + events = Event.query.order_by(Event.timestamp.desc()).limit(50).all() + return render_template('settings/settings.html', primary_color=site_settings.primary_color, secondary_color=site_settings.secondary_color, active_tab=active_tab, - site_settings=site_settings) + site_settings=site_settings, + events=events) @main_bp.route('/settings/colors', methods=['POST']) @login_required @@ -530,4 +578,105 @@ def init_routes(main_bp): logger.info(f"[Dynamic Colors] Generated CSS with primary color: {primary_color}") logger.info(f"[Dynamic Colors] Cache version: {site_settings.updated_at.timestamp()}") - return Response(css, mimetype='text/css') \ No newline at end of file + return Response(css, mimetype='text/css') + + @main_bp.route('/api/events') + @login_required + def get_events(): + if not current_user.is_admin: + return jsonify({'success': False, 'error': 'Unauthorized'}), 403 + + # Get filter parameters + page = request.args.get('page', 1, type=int) + event_type = request.args.get('eventType') + date_range = request.args.get('dateRange', '24h') + user_id = request.args.get('userId') + + # Build query + query = Event.query + + # Apply filters + if event_type: + query = query.filter_by(event_type=event_type) + if user_id: + query = query.filter_by(user_id=user_id) + + # Apply date range filter + if date_range != 'all': + now = datetime.utcnow() + if date_range == '24h': + start_date = now - timedelta(days=1) + elif date_range == '7d': + start_date = now - timedelta(days=7) + elif date_range == '30d': + start_date = now - timedelta(days=30) + query = query.filter(Event.timestamp >= start_date) + + # Get total count for pagination + total_count = query.count() + per_page = 50 + total_pages = (total_count + per_page - 1) // per_page + + # Get paginated results + events = query.order_by(Event.timestamp.desc())\ + .offset((page - 1) * per_page)\ + .limit(per_page)\ + .all() + + return jsonify({ + 'success': True, + 'events': [{ + 'id': event.id, + 'event_type': event.event_type, + 'timestamp': event.timestamp.isoformat(), + 'user': { + 'id': event.user.id, + 'username': event.user.username, + 'last_name': event.user.last_name or '' + }, + 'ip_address': event.ip_address, + 'details': event.details + } for event in events], + 'total_pages': total_pages + }) + + @main_bp.route('/api/events/') + @login_required + def get_event_details(event_id): + if not current_user.is_admin: + return jsonify({'success': False, 'error': 'Unauthorized'}), 403 + + event = Event.query.get_or_404(event_id) + + return jsonify({ + 'success': True, + 'event': { + 'id': event.id, + 'event_type': event.event_type, + 'timestamp': event.timestamp.isoformat(), + 'user': { + 'id': event.user.id, + 'username': event.user.username + }, + 'ip_address': event.ip_address, + 'user_agent': event.user_agent, + 'details': event.details + } + }) + + @main_bp.route('/api/users') + @login_required + def get_users(): + if not current_user.is_admin: + return jsonify({'success': False, 'error': 'Unauthorized'}), 403 + + users = User.query.order_by(User.username).all() + + return jsonify({ + 'success': True, + 'users': [{ + 'id': user.id, + 'username': user.username, + 'last_name': user.last_name or '' + } for user in users] + }) \ No newline at end of file diff --git a/static/js/events.js b/static/js/events.js new file mode 100644 index 0000000..e1e1147 --- /dev/null +++ b/static/js/events.js @@ -0,0 +1,272 @@ +/** + * @fileoverview Manages the events page functionality. + * This file handles event filtering, pagination, and modal interactions. + */ + +document.addEventListener('DOMContentLoaded', function() { + // Elements + const eventTypeFilter = document.getElementById('eventTypeFilter'); + const dateRangeFilter = document.getElementById('dateRangeFilter'); + const userFilter = document.getElementById('userFilter'); + const applyFiltersBtn = document.getElementById('applyFilters'); + const eventsTableBody = document.getElementById('eventsTableBody'); + const prevPageBtn = document.getElementById('prevPage'); + const nextPageBtn = document.getElementById('nextPage'); + const currentPageSpan = document.getElementById('currentPage'); + const totalPagesSpan = document.getElementById('totalPages'); + const eventDetailsModal = document.getElementById('eventDetailsModal'); + const eventDetailsContent = document.getElementById('eventDetailsContent'); + + // State + let currentPage = 1; + let totalPages = 1; + let currentFilters = { + eventType: '', + dateRange: '24h', + userId: '' + }; + + /** + * Loads events based on current filters and page + */ + async function loadEvents() { + try { + const response = await fetch(`/api/events?page=${currentPage}&${new URLSearchParams(currentFilters)}`); + const data = await response.json(); + + if (data.success) { + renderEvents(data.events); + updatePagination(data.total_pages); + } else { + console.error('Failed to load events:', data.error); + } + } catch (error) { + console.error('Error loading events:', error); + } + } + + /** + * Renders events in the table + */ + function renderEvents(events) { + if (!Array.isArray(events)) { + console.error('Invalid events data received:', events); + return; + } + + const eventRows = events.map(event => { + if (!event || typeof event !== 'object') { + console.error('Invalid event data:', event); + return ''; + } + + // Determine badge color and text based on event type + let badgeClass = 'bg-secondary'; + let eventText = event.event_type || 'Unknown Event'; + + switch(event.event_type) { + case 'user_login': + badgeClass = 'bg-success'; + eventText = 'User Login'; + break; + case 'user_logout': + badgeClass = 'bg-secondary'; + eventText = 'User Logout'; + break; + case 'user_register': + badgeClass = 'bg-info'; + eventText = 'User Registration'; + break; + case 'user_update': + badgeClass = 'bg-primary'; + eventText = 'User Update'; + break; + case 'file_upload': + badgeClass = 'bg-success'; + eventText = 'File Upload'; + break; + case 'file_delete': + badgeClass = 'bg-danger'; + eventText = 'File Delete'; + break; + case 'file_download': + badgeClass = 'bg-info'; + eventText = 'File Download'; + break; + case 'file_restore': + badgeClass = 'bg-warning'; + eventText = 'File Restore'; + break; + case 'file_move': + badgeClass = 'bg-primary'; + eventText = 'File Move'; + break; + case 'file_rename': + badgeClass = 'bg-info'; + eventText = 'File Rename'; + break; + case 'file_star': + badgeClass = 'bg-warning'; + eventText = 'File Star'; + break; + case 'file_unstar': + badgeClass = 'bg-secondary'; + eventText = 'File Unstar'; + break; + case 'room_create': + badgeClass = 'bg-success'; + eventText = 'Room Create'; + break; + case 'room_delete': + badgeClass = 'bg-danger'; + eventText = 'Room Delete'; + break; + case 'room_update': + badgeClass = 'bg-primary'; + eventText = 'Room Update'; + break; + case 'room_join': + badgeClass = 'bg-info'; + eventText = 'Room Join'; + break; + case 'room_leave': + badgeClass = 'bg-secondary'; + eventText = 'Room Leave'; + break; + case 'conversation_create': + badgeClass = 'bg-success'; + eventText = 'Conversation Create'; + break; + case 'conversation_delete': + badgeClass = 'bg-danger'; + eventText = 'Conversation Delete'; + break; + case 'message_sent': + badgeClass = 'bg-primary'; + eventText = 'Message Sent'; + break; + case 'attachment_download': + badgeClass = 'bg-info'; + eventText = 'Attachment Download'; + break; + } + + const timestamp = event.timestamp ? new Date(event.timestamp).toLocaleString() : '-'; + const username = event.user?.username || 'Unknown User'; + const lastName = event.user?.last_name || ''; + const eventId = event.id || ''; + const ipAddress = event.ip_address || '-'; + + return ` + + ${timestamp} + ${eventText} + ${username} ${lastName} + + + + ${ipAddress} + + `; + }).join(''); + + eventsTableBody.innerHTML = eventRows; + + // Add event listeners to detail buttons + document.querySelectorAll('[data-event-id]').forEach(button => { + button.addEventListener('click', () => { + const eventId = button.dataset.eventId; + if (eventId) { + showEventDetails(eventId); + } + }); + }); + } + + /** + * Updates pagination controls + */ + function updatePagination(total) { + totalPages = total; + currentPageSpan.textContent = currentPage; + totalPagesSpan.textContent = totalPages; + + prevPageBtn.classList.toggle('disabled', currentPage === 1); + nextPageBtn.classList.toggle('disabled', currentPage === totalPages); + } + + /** + * Shows event details in modal + */ + async function showEventDetails(eventId) { + try { + const response = await fetch(`/api/events/${eventId}`); + const data = await response.json(); + + if (data.success) { + const event = data.event; + eventDetailsContent.textContent = JSON.stringify(event.details, null, 2); + } else { + console.error('Failed to load event details:', data.error); + } + } catch (error) { + console.error('Error loading event details:', error); + } + } + + /** + * Loads users for the user filter + */ + async function loadUsers() { + try { + const response = await fetch('/api/users'); + const data = await response.json(); + + const userFilter = document.getElementById('userFilter'); + userFilter.innerHTML = ''; + + data.users.forEach(user => { + const option = document.createElement('option'); + option.value = user.id; + option.textContent = `${user.username} ${user.last_name || ''}`.trim(); + userFilter.appendChild(option); + }); + } catch (error) { + console.error('Error loading users:', error); + } + } + + // Event Listeners + applyFiltersBtn.addEventListener('click', () => { + currentFilters = { + eventType: eventTypeFilter.value, + dateRange: dateRangeFilter.value, + userId: userFilter.value + }; + currentPage = 1; + loadEvents(); + }); + + prevPageBtn.addEventListener('click', () => { + if (currentPage > 1) { + currentPage--; + loadEvents(); + } + }); + + nextPageBtn.addEventListener('click', () => { + if (currentPage < totalPages) { + currentPage++; + loadEvents(); + } + }); + + // Initialize + loadUsers(); + loadEvents(); +}); \ No newline at end of file diff --git a/static/js/file-grid.js b/static/js/file-grid.js index 36ffc80..95a99e2 100644 --- a/static/js/file-grid.js +++ b/static/js/file-grid.js @@ -354,6 +354,24 @@ function toggleStar(filename, path = '', roomId) { .then(r => r.json()) .then(res => { if (res.success) { + // Log the star/unstar event + fetch('/api/events/log', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken + }, + body: JSON.stringify({ + event_type: res.starred ? 'file_star' : 'file_unstar', + details: { + filename: filename, + path: path, + room_id: roomId, + timestamp: new Date().toISOString() + } + }) + }); + // Remove the file from the current view since it's no longer starred currentFiles = currentFiles.filter(f => !(f.name === filename && f.path === path && f.room_id === roomId)); renderFiles(currentFiles); @@ -394,6 +412,24 @@ function restoreFile(filename, path = '', roomId) { .then(r => r.json()) .then(res => { if (res.success) { + // Log the restore event + fetch('/api/events/log', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken + }, + body: JSON.stringify({ + event_type: 'file_restore', + details: { + filename: filename, + path: path, + room_id: roomId, + timestamp: new Date().toISOString() + } + }) + }); + // Remove the file from the current view since it's been restored currentFiles = currentFiles.filter(f => !(f.name === filename && f.path === path && f.room_id === roomId)); renderFiles(currentFiles); @@ -434,7 +470,7 @@ function permanentDeleteFile() { return; } - fetch(`/api/rooms/${roomId}/delete-permanent`, { + fetch(`/api/rooms/${roomId}/delete_permanent`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -445,39 +481,40 @@ function permanentDeleteFile() { path: path }) }) - .then(response => { - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - // Check if the response is empty - const contentType = response.headers.get('content-type'); - if (contentType && contentType.includes('application/json')) { - return response.json(); - } - return { success: true }; // If no JSON response, assume success - }) + .then(r => r.json()) .then(res => { if (res.success) { - // Remove the file from the current view since it's been deleted + // Log the permanent delete event + fetch('/api/events/log', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken + }, + body: JSON.stringify({ + event_type: 'file_delete_permanent', + details: { + filename: filename, + path: path, + room_id: roomId, + timestamp: new Date().toISOString() + } + }) + }); + + // Remove the file from the current view currentFiles = currentFiles.filter(f => !(f.name === filename && f.path === path && f.room_id === roomId)); renderFiles(currentFiles); + // Close the modal const modal = bootstrap.Modal.getInstance(document.getElementById('permanentDeleteModal')); - if (modal) { - modal.hide(); - } + modal.hide(); } else { - console.error('Failed to delete file:', res.error || 'Unknown error'); + console.error('Failed to delete file permanently:', res.error); } }) .catch(error => { - console.error('Error deleting file:', error); - // Show error to user - const modal = bootstrap.Modal.getInstance(document.getElementById('permanentDeleteModal')); - if (modal) { - modal.hide(); - } - // You might want to show an error message to the user here + console.error('Error deleting file permanently:', error); }); } diff --git a/static/js/room-members.js b/static/js/room-members.js index 198174c..49ef5bf 100644 --- a/static/js/room-members.js +++ b/static/js/room-members.js @@ -13,19 +13,95 @@ * - Auto-save functionality for permission changes * @function */ -$(document).ready(function() { - // Initialize Select2 for user selection +document.addEventListener('DOMContentLoaded', function() { + // Initialize Select2 $('.select2').select2({ - theme: 'bootstrap-5', - width: '100%', - placeholder: 'Search for a user...', - allowClear: true + theme: 'bootstrap-5' }); - - // Auto-submit permission form on checkbox change - document.querySelectorAll('.auto-save-perms-form input[type="checkbox"]').forEach(function(checkbox) { - checkbox.addEventListener('change', function() { - this.closest('form').submit(); + + // Log when a member is added to the room + const addMemberForm = document.querySelector('form[action*="/add_member"]'); + if (addMemberForm) { + addMemberForm.addEventListener('submit', function(e) { + const formData = new FormData(this); + const userId = formData.get('user_id'); + const roomId = this.action.split('/rooms/')[1].split('/add_member')[0]; + + fetch('/api/events/log', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + }, + body: JSON.stringify({ + event_type: 'room_join', + details: { + room_id: roomId, + user_id: userId, + timestamp: new Date().toISOString() + } + }) + }); + }); + } + + // Log when a member is removed from the room + const removeMemberForms = document.querySelectorAll('form[action*="/remove_member"]'); + removeMemberForms.forEach(form => { + form.addEventListener('submit', function(e) { + const roomId = this.action.split('/rooms/')[1].split('/remove_member')[0]; + const userId = this.action.split('/users/')[1]; + + fetch('/api/events/log', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + }, + body: JSON.stringify({ + event_type: 'room_leave', + details: { + room_id: roomId, + user_id: userId, + timestamp: new Date().toISOString() + } + }) + }); + }); + }); + + // Log when member permissions are updated + const permissionForms = document.querySelectorAll('.auto-save-perms-form'); + permissionForms.forEach(form => { + form.addEventListener('change', function(e) { + const roomId = this.action.split('/rooms/')[1].split('/update_member_permissions')[0]; + const userId = this.action.split('/users/')[1]; + const formData = new FormData(this); + + fetch('/api/events/log', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + }, + body: JSON.stringify({ + event_type: 'room_member_permissions_update', + details: { + room_id: roomId, + user_id: userId, + permissions: { + can_view: formData.get('can_view') === '1', + can_download: formData.get('can_download') === 'on', + can_upload: formData.get('can_upload') === 'on', + can_delete: formData.get('can_delete') === 'on', + can_rename: formData.get('can_rename') === 'on', + can_move: formData.get('can_move') === 'on', + can_share: formData.get('can_share') === 'on' + }, + timestamp: new Date().toISOString() + } + }) + }); }); }); }); \ No newline at end of file diff --git a/static/js/rooms-list.js b/static/js/rooms-list.js index 75068ad..271fa5c 100644 --- a/static/js/rooms-list.js +++ b/static/js/rooms-list.js @@ -46,4 +46,137 @@ document.addEventListener('DOMContentLoaded', function() { form.submit(); }); } + + // Log when rooms page is viewed + fetch('/api/events/log', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + }, + body: JSON.stringify({ + event_type: 'room_list_view', + details: { + timestamp: new Date().toISOString() + } + }) + }); + + // Log when a room is opened + const openRoomButtons = document.querySelectorAll('a[href*="/room/"]'); + openRoomButtons.forEach(button => { + button.addEventListener('click', function(e) { + const roomId = this.href.split('/room/')[1]; + fetch('/api/events/log', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + }, + body: JSON.stringify({ + event_type: 'room_open', + details: { + room_id: roomId, + timestamp: new Date().toISOString() + } + }) + }); + }); + }); + + // Log when a room is deleted + const deleteRoomForms = document.querySelectorAll('form[action*="/delete"]'); + deleteRoomForms.forEach(form => { + form.addEventListener('submit', function(e) { + const roomId = this.action.split('/rooms/')[1].split('/delete')[0]; + fetch('/api/events/log', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + }, + body: JSON.stringify({ + event_type: 'room_delete', + details: { + room_id: roomId, + timestamp: new Date().toISOString() + } + }) + }); + }); + }); + + // Log when a room is created + const createRoomForm = document.querySelector('form[action*="/create"]'); + if (createRoomForm) { + createRoomForm.addEventListener('submit', function(e) { + const formData = new FormData(this); + fetch('/api/events/log', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + }, + body: JSON.stringify({ + event_type: 'room_create', + details: { + room_name: formData.get('name'), + description: formData.get('description'), + timestamp: new Date().toISOString() + } + }) + }); + }); + } + + // Log when a room is edited + const editRoomForm = document.querySelector('form[action*="/edit"]'); + if (editRoomForm) { + editRoomForm.addEventListener('submit', function(e) { + const roomId = this.action.split('/rooms/')[1].split('/edit')[0]; + const formData = new FormData(this); + fetch('/api/events/log', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + }, + body: JSON.stringify({ + event_type: 'room_update', + details: { + room_id: roomId, + room_name: formData.get('name'), + description: formData.get('description'), + timestamp: new Date().toISOString() + } + }) + }); + }); + } + + // Log when room search is performed + if (searchInput) { + let searchTimeout; + searchInput.addEventListener('input', function(e) { + clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => { + if (this.value.length > 0) { + fetch('/api/events/log', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + }, + body: JSON.stringify({ + event_type: 'room_search', + details: { + search_term: this.value, + timestamp: new Date().toISOString() + } + }) + }); + } + }, 500); + }); + } }); \ No newline at end of file diff --git a/templates/settings/settings.html b/templates/settings/settings.html index d75f4b9..5af4a76 100644 --- a/templates/settings/settings.html +++ b/templates/settings/settings.html @@ -4,6 +4,7 @@ {% from "settings/tabs/company_info.html" import company_info_tab %} {% from "settings/tabs/security.html" import security_tab %} {% from "settings/tabs/debugging.html" import debugging_tab %} +{% from "settings/tabs/events.html" import events_tab %} {% from "settings/components/reset_colors_modal.html" import reset_colors_modal %} {% block title %}Settings - DocuPulse{% endblock %} @@ -36,6 +37,11 @@ Company Info +