diff --git a/__pycache__/app.cpython-313.pyc b/__pycache__/app.cpython-313.pyc index 45ae237..293a4a7 100644 Binary files a/__pycache__/app.cpython-313.pyc and b/__pycache__/app.cpython-313.pyc differ diff --git a/__pycache__/models.cpython-313.pyc b/__pycache__/models.cpython-313.pyc index 56acc2b..19e3c6d 100644 Binary files a/__pycache__/models.cpython-313.pyc and b/__pycache__/models.cpython-313.pyc differ diff --git a/entrypoint.sh b/entrypoint.sh index 38fb14e..fcf9b88 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -29,6 +29,20 @@ flask db init flask db migrate -m "Initial migration" flask db upgrade +# Create events table +echo "Creating events table..." +python3 -c " +from migrations.add_events_table import upgrade +from app import create_app +app = create_app() +with app.app_context(): + try: + upgrade() + print('Events table created successfully') + except Exception as e: + print(f'Error creating events table: {e}') +" + # Create default site settings if they don't exist echo "Creating default site settings..." python3 -c " diff --git a/migrations/add_events_table.py b/migrations/add_events_table.py new file mode 100644 index 0000000..db198aa --- /dev/null +++ b/migrations/add_events_table.py @@ -0,0 +1,61 @@ +import os +import sys +from pathlib import Path + +# Add the parent directory to Python path so we can import from root +sys.path.append(str(Path(__file__).parent.parent)) + +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from extensions import db +from sqlalchemy import text + +def upgrade(): + # Create events table + with db.engine.connect() as conn: + conn.execute(text(''' + CREATE TABLE IF NOT EXISTS events ( + id SERIAL PRIMARY KEY, + event_type VARCHAR(50) NOT NULL, + user_id INTEGER NOT NULL REFERENCES "user" (id), + timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + details JSONB, + ip_address VARCHAR(45), + user_agent VARCHAR(255) + ); + + -- Create index on event_type for faster filtering + CREATE INDEX IF NOT EXISTS idx_events_event_type ON events(event_type); + + -- Create index on timestamp for faster date-based queries + CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp); + + -- Create index on user_id for faster user-based queries + CREATE INDEX IF NOT EXISTS idx_events_user_id ON events(user_id); + ''')) + conn.commit() + +def downgrade(): + # Drop events table and its indexes + with db.engine.connect() as conn: + conn.execute(text(''' + DROP INDEX IF EXISTS idx_events_event_type; + DROP INDEX IF EXISTS idx_events_timestamp; + DROP INDEX IF EXISTS idx_events_user_id; + DROP TABLE IF EXISTS events; + ''')) + conn.commit() + +if __name__ == '__main__': + app = Flask(__name__) + + # Use the same database configuration as in app.py + app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', 'postgresql://postgres:1253@localhost:5432/docupulse') + app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + + print("Connecting to database...") + + db.init_app(app) + + with app.app_context(): + upgrade() \ No newline at end of file diff --git a/models.py b/models.py index 0c9dd28..02a1fb8 100644 --- a/models.py +++ b/models.py @@ -4,6 +4,7 @@ from werkzeug.security import generate_password_hash, check_password_hash from datetime import datetime from sqlalchemy.orm import relationship from extensions import db +from enum import Enum # Association table for room members room_members = db.Table('room_members', @@ -201,4 +202,62 @@ class MessageAttachment(db.Model): message = db.relationship('Message', back_populates='attachments') def __repr__(self): - return f'' \ No newline at end of file + return f'' + +class EventType(Enum): + # User events + USER_LOGIN = 'user_login' + USER_LOGOUT = 'user_logout' + USER_CREATE = 'user_create' + 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_MOVE = 'file_move' + FILE_STAR = 'file_star' + FILE_UNSTAR = 'file_unstar' + + # Conversation events + 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' + +class Event(db.Model): + __tablename__ = 'events' + id = db.Column(db.Integer, primary_key=True) + event_type = db.Column(db.String(50), nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + timestamp = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) + details = db.Column(db.JSON) # Store additional event-specific data + ip_address = db.Column(db.String(45)) # IPv6 addresses can be up to 45 chars + user_agent = db.Column(db.String(255)) + + # Relationships + user = db.relationship('User', backref='events') + + def __repr__(self): + return f'' \ No newline at end of file diff --git a/routes/__pycache__/conversations.cpython-313.pyc b/routes/__pycache__/conversations.cpython-313.pyc index 28baa38..bc544d1 100644 Binary files a/routes/__pycache__/conversations.cpython-313.pyc and b/routes/__pycache__/conversations.cpython-313.pyc differ diff --git a/routes/__pycache__/main.cpython-313.pyc b/routes/__pycache__/main.cpython-313.pyc index 66e2362..28d31bb 100644 Binary files a/routes/__pycache__/main.cpython-313.pyc and b/routes/__pycache__/main.cpython-313.pyc differ diff --git a/utils/event_logger.py b/utils/event_logger.py new file mode 100644 index 0000000..01565cb --- /dev/null +++ b/utils/event_logger.py @@ -0,0 +1,123 @@ +from flask import request +from models import Event, EventType, db +from typing import Optional, Dict, Any, List +from datetime import datetime + +def log_event( + event_type: EventType, + user_id: int, + details: Optional[Dict[str, Any]] = None +) -> Event: + """ + Log an event in the system. + + Args: + event_type: The type of event from EventType enum + user_id: The ID of the user performing the action + details: Optional dictionary containing additional event-specific data + + Returns: + The created Event object + """ + event = Event( + event_type=event_type.value, + user_id=user_id, + details=details or {}, + ip_address=request.remote_addr if request else None, + user_agent=request.user_agent.string if request and request.user_agent else None + ) + + db.session.add(event) + db.session.commit() + return event + +def get_user_events( + user_id: int, + event_type: Optional[EventType] = None, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None, + limit: int = 100 +) -> List[Event]: + """ + Retrieve events for a specific user with optional filtering. + + Args: + user_id: The ID of the user to get events for + event_type: Optional event type to filter by + start_date: Optional start date to filter events + end_date: Optional end date to filter events + limit: Maximum number of events to return + + Returns: + List of Event objects matching the criteria + """ + query = Event.query.filter_by(user_id=user_id) + + if event_type: + query = query.filter_by(event_type=event_type.value) + if start_date: + query = query.filter(Event.timestamp >= start_date) + if end_date: + query = query.filter(Event.timestamp <= end_date) + + return query.order_by(Event.timestamp.desc()).limit(limit).all() + +def get_room_events( + room_id: int, + event_type: Optional[EventType] = None, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None, + limit: int = 100 +) -> List[Event]: + """ + Retrieve events related to a specific room with optional filtering. + + Args: + room_id: The ID of the room to get events for + event_type: Optional event type to filter by + start_date: Optional start date to filter events + end_date: Optional end date to filter events + limit: Maximum number of events to return + + Returns: + List of Event objects matching the criteria + """ + query = Event.query.filter(Event.details['room_id'].astext.cast(Integer) == room_id) + + if event_type: + query = query.filter_by(event_type=event_type.value) + if start_date: + query = query.filter(Event.timestamp >= start_date) + if end_date: + query = query.filter(Event.timestamp <= end_date) + + return query.order_by(Event.timestamp.desc()).limit(limit).all() + +def get_recent_events( + event_type: Optional[EventType] = None, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None, + limit: int = 100 +) -> List[Event]: + """ + Retrieve recent events across the system with optional filtering. + + Args: + event_type: Optional event type to filter by + start_date: Optional start date to filter events + end_date: Optional end date to filter events + limit: Maximum number of events to return + + Returns: + List of Event objects matching the criteria + """ + query = Event.query + + if event_type: + query = query.filter_by(event_type=event_type.value) + if start_date: + query = query.filter(Event.timestamp >= start_date) + if end_date: + query = query.filter(Event.timestamp <= end_date) + + return query.order_by(Event.timestamp.desc()).limit(limit).all() \ No newline at end of file