diff --git a/__pycache__/models.cpython-313.pyc b/__pycache__/models.cpython-313.pyc index 7cefa31..ca58e88 100644 Binary files a/__pycache__/models.cpython-313.pyc and b/__pycache__/models.cpython-313.pyc differ diff --git a/app.py b/app.py index 48d597b..fcdde20 100644 --- a/app.py +++ b/app.py @@ -29,6 +29,8 @@ def create_app(): app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'your-secure-secret-key-here') app.config['UPLOAD_FOLDER'] = os.path.join(app.root_path, 'static', 'uploads') app.config['CSS_VERSION'] = os.getenv('CSS_VERSION', '1.0.3') # Add CSS version for cache busting + app.config['SERVER_NAME'] = os.getenv('SERVER_NAME', '127.0.0.1:5000') + app.config['PREFERRED_URL_SCHEME'] = os.getenv('PREFERRED_URL_SCHEME', 'http') # Initialize extensions db.init_app(app) diff --git a/models.py b/models.py index 202ee97..576a659 100644 --- a/models.py +++ b/models.py @@ -34,7 +34,11 @@ class User(UserMixin, db.Model): is_active = db.Column(db.Boolean, default=True) profile_picture = db.Column(db.String(255)) preferred_view = db.Column(db.String(10), default='grid', nullable=False) # 'grid' or 'list' - room_permissions = relationship('RoomMemberPermission', back_populates='user') + room_permissions = relationship( + 'RoomMemberPermission', + back_populates='user', + cascade='all, delete-orphan' + ) def set_password(self, password): self.password_hash = generate_password_hash(password) @@ -50,10 +54,10 @@ class Room(db.Model): name = db.Column(db.String(100), nullable=False) description = db.Column(db.Text) created_at = db.Column(db.DateTime, default=datetime.utcnow) - created_by = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + created_by = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False) # Relationships - creator = db.relationship('User', backref='created_rooms', foreign_keys=[created_by]) + creator = db.relationship('User', backref=db.backref('created_rooms', cascade='all, delete-orphan'), foreign_keys=[created_by]) members = db.relationship('User', secondary=room_members, backref=db.backref('rooms', lazy='dynamic')) member_permissions = relationship('RoomMemberPermission', back_populates='room', cascade='all, delete-orphan') files = db.relationship('RoomFile', back_populates='room', cascade='all, delete-orphan') @@ -65,7 +69,7 @@ class Room(db.Model): class RoomMemberPermission(db.Model): __tablename__ = 'room_member_permissions' room_id = db.Column(db.Integer, db.ForeignKey('room.id'), primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), primary_key=True) can_view = db.Column(db.Boolean, default=True, nullable=False) can_download = db.Column(db.Boolean, default=False, nullable=False) can_upload = db.Column(db.Boolean, default=False, nullable=False) @@ -86,13 +90,13 @@ class RoomFile(db.Model): type = db.Column(db.String(10), nullable=False) # 'file' or 'folder' size = db.Column(db.Integer) # in bytes, null for folders modified = db.Column(db.Float) # timestamp - uploaded_by = db.Column(db.Integer, db.ForeignKey('user.id')) + uploaded_by = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE')) uploaded_at = db.Column(db.DateTime, default=datetime.utcnow) deleted = db.Column(db.Boolean, default=False) # New field for deleted status - deleted_by = db.Column(db.Integer, db.ForeignKey('user.id')) # New field for tracking who deleted the file + deleted_by = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE')) deleted_at = db.Column(db.DateTime) # New field for tracking when the file was deleted - uploader = db.relationship('User', backref='uploaded_files', foreign_keys=[uploaded_by]) - deleter = db.relationship('User', backref='deleted_room_files', foreign_keys=[deleted_by]) + uploader = db.relationship('User', backref=db.backref('uploaded_files', cascade='all, delete-orphan'), foreign_keys=[uploaded_by]) + deleter = db.relationship('User', backref=db.backref('deleted_room_files', cascade='all, delete-orphan'), foreign_keys=[deleted_by]) room = db.relationship('Room', back_populates='files') starred_by = db.relationship('User', secondary='user_starred_file', backref='starred_files') @@ -102,7 +106,7 @@ class RoomFile(db.Model): class UserStarredFile(db.Model): __tablename__ = 'user_starred_file' id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False) file_id = db.Column(db.Integer, db.ForeignKey('room_file.id'), nullable=False) starred_at = db.Column(db.DateTime, default=datetime.utcnow) @@ -123,13 +127,13 @@ class TrashedFile(db.Model): type = db.Column(db.String(10), nullable=False) # 'file' or 'folder' size = db.Column(db.Integer) # in bytes, null for folders modified = db.Column(db.Float) # timestamp - uploaded_by = db.Column(db.Integer, db.ForeignKey('user.id')) + uploaded_by = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE')) uploaded_at = db.Column(db.DateTime, default=datetime.utcnow) - deleted_by = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + deleted_by = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False) deleted_at = db.Column(db.DateTime, default=datetime.utcnow) room = db.relationship('Room', backref='trashed_files') - uploader = db.relationship('User', foreign_keys=[uploaded_by], backref='uploaded_trashed_files') - deleter = db.relationship('User', foreign_keys=[deleted_by], backref='deleted_trashed_files') # Changed from deleted_files to deleted_trashed_files + uploader = db.relationship('User', foreign_keys=[uploaded_by], backref=db.backref('uploaded_trashed_files', cascade='all, delete-orphan')) + deleter = db.relationship('User', foreign_keys=[deleted_by], backref=db.backref('deleted_trashed_files', cascade='all, delete-orphan')) def __repr__(self): return f'' @@ -197,10 +201,10 @@ class Conversation(db.Model): name = db.Column(db.String(100), nullable=False) description = db.Column(db.Text) created_at = db.Column(db.DateTime, default=datetime.utcnow) - created_by = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + created_by = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False) # Relationships - creator = db.relationship('User', backref='created_conversations', foreign_keys=[created_by]) + creator = db.relationship('User', backref=db.backref('created_conversations', cascade='all, delete-orphan'), foreign_keys=[created_by]) members = db.relationship('User', secondary=conversation_members, backref=db.backref('conversations', lazy='dynamic')) messages = db.relationship('Message', back_populates='conversation', cascade='all, delete-orphan') @@ -212,11 +216,11 @@ class Message(db.Model): content = db.Column(db.Text, nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) conversation_id = db.Column(db.Integer, db.ForeignKey('conversation.id'), nullable=False) - user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False) # Relationships conversation = db.relationship('Conversation', back_populates='messages') - user = db.relationship('User', backref='messages') + user = db.relationship('User', backref=db.backref('messages', cascade='all, delete-orphan')) attachments = db.relationship('MessageAttachment', back_populates='message', cascade='all, delete-orphan') def __repr__(self): @@ -284,14 +288,14 @@ 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=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=True) 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') + user = db.relationship('User', backref=db.backref('events', cascade='all, delete-orphan')) def __repr__(self): return f'' @@ -316,14 +320,14 @@ class Notif(db.Model): __tablename__ = 'notifs' id = db.Column(db.Integer, primary_key=True) notif_type = db.Column(db.String(50), nullable=False) - user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) - sender_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False) + sender_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=True) timestamp = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) read = db.Column(db.Boolean, default=False, nullable=False) details = db.Column(db.JSON) # Store additional notification-specific data # Relationships - user = db.relationship('User', foreign_keys=[user_id], backref='notifications') + user = db.relationship('User', foreign_keys=[user_id], backref=db.backref('notifications', cascade='all, delete-orphan')) sender = db.relationship('User', foreign_keys=[sender_id], backref='sent_notifications') def __repr__(self): @@ -337,11 +341,11 @@ class EmailTemplate(db.Model): body = db.Column(db.Text, nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - created_by = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + created_by = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False) is_active = db.Column(db.Boolean, default=True) # Relationships - creator = db.relationship('User', backref='created_email_templates', foreign_keys=[created_by]) + creator = db.relationship('User', backref=db.backref('created_email_templates', cascade='all, delete-orphan'), foreign_keys=[created_by]) def __repr__(self): return f'' @@ -368,14 +372,14 @@ class Mail(db.Model): class PasswordSetupToken(db.Model): __tablename__ = 'password_setup_tokens' id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False) token = db.Column(db.String(100), unique=True, nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) expires_at = db.Column(db.DateTime, nullable=False) used = db.Column(db.Boolean, default=False) # Relationships - user = db.relationship('User', backref='password_setup_tokens') + user = db.relationship('User', backref=db.backref('password_setup_tokens', cascade='all, delete-orphan')) def is_valid(self): return not self.used and datetime.utcnow() < self.expires_at diff --git a/routes/__pycache__/auth.cpython-313.pyc b/routes/__pycache__/auth.cpython-313.pyc index b653a18..c24b956 100644 Binary files a/routes/__pycache__/auth.cpython-313.pyc and b/routes/__pycache__/auth.cpython-313.pyc differ diff --git a/routes/__pycache__/contacts.cpython-313.pyc b/routes/__pycache__/contacts.cpython-313.pyc index 24cac0c..33a378d 100644 Binary files a/routes/__pycache__/contacts.cpython-313.pyc and b/routes/__pycache__/contacts.cpython-313.pyc differ diff --git a/routes/__pycache__/main.cpython-313.pyc b/routes/__pycache__/main.cpython-313.pyc index fcba5ca..3b7f8ed 100644 Binary files a/routes/__pycache__/main.cpython-313.pyc and b/routes/__pycache__/main.cpython-313.pyc differ diff --git a/routes/auth.py b/routes/auth.py index 566d03f..e07a853 100644 --- a/routes/auth.py +++ b/routes/auth.py @@ -11,9 +11,19 @@ auth_bp = Blueprint('auth', __name__) def require_password_change(f): @wraps(f) def decorated_function(*args, **kwargs): - if current_user.is_authenticated and current_user.check_password('changeme'): - flash('Please change your password before continuing.', 'warning') - return redirect(url_for('auth.change_password')) + if current_user.is_authenticated: + # Check if user has any valid password setup tokens + has_valid_token = PasswordSetupToken.query.filter_by( + user_id=current_user.id, + used=False + ).filter(PasswordSetupToken.expires_at > datetime.utcnow()).first() is not None + + if has_valid_token: + flash('Please set up your password before continuing.', 'warning') + return redirect(url_for('auth.setup_password', token=current_user.password_setup_tokens[0].token)) + elif current_user.check_password('changeme'): + flash('Please change your password before continuing.', 'warning') + return redirect(url_for('auth.change_password')) return f(*args, **kwargs) return decorated_function @@ -280,6 +290,7 @@ def init_routes(auth_bp): # Log password setup event log_event( event_type='user_update', + user_id=user.id, details={ 'user_id': user.id, 'user_name': f"{user.username} {user.last_name}", @@ -290,7 +301,9 @@ def init_routes(auth_bp): db.session.commit() - flash('Password set up successfully! You can now log in.', 'success') - return redirect(url_for('auth.login')) + # Log the user in and redirect to dashboard + login_user(user) + flash('Password set up successfully! Welcome to DocuPulse.', 'success') + return redirect(url_for('main.dashboard')) return render_template('auth/setup_password.html') \ No newline at end of file diff --git a/routes/main.py b/routes/main.py index 0a049f8..fc69fbf 100644 --- a/routes/main.py +++ b/routes/main.py @@ -70,7 +70,7 @@ def init_routes(main_bp): Event.user_id == current_user.id, # User's own actions db.and_( Event.event_type.in_(['conversation_create', 'message_create']), # Conversation-related events - Event.details['conversation_id'].cast(db.Integer).in_( + db.cast(text("(details->>'conversation_id')::integer"), db.Integer).in_( db.session.query(Conversation.id) .join(Conversation.members) .filter(User.id == current_user.id) diff --git a/templates/auth/setup_password.html b/templates/auth/setup_password.html index 1501c7a..a828382 100644 --- a/templates/auth/setup_password.html +++ b/templates/auth/setup_password.html @@ -8,28 +8,6 @@

Set Up Your Password

-
-
-
-
- -
-
-

Password Requirements

-
-
    -
  • At least 8 characters long
  • -
  • At least one uppercase letter
  • -
  • At least one lowercase letter
  • -
  • At least one number
  • -
  • At least one special character
  • -
-
-
-
-
-
-
@@ -50,12 +28,119 @@ class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2" style="--tw-ring-color: var(--primary-color);">
+ +
+

Password Requirements:

+
    +
  • + At least 8 characters long +
  • +
  • + At least one uppercase letter +
  • +
  • + At least one lowercase letter +
  • +
  • + At least one number +
  • +
  • + At least one special character +
  • +
+
- + +{% block extra_js %} + +{% endblock %} {% endblock %} \ No newline at end of file diff --git a/templates/settings/tabs/email_templates.html b/templates/settings/tabs/email_templates.html index 0fff081..3198615 100644 --- a/templates/settings/tabs/email_templates.html +++ b/templates/settings/tabs/email_templates.html @@ -78,7 +78,9 @@ const templateVariables = { 'user.position': 'The position of the user in their company', 'created_at': 'The date and time when the account was created', 'site.company_name': 'The name of your company', - 'site.company_website': 'Your company website URL' + 'site.company_website': 'Your company website URL', + 'setup_link': 'The link to set up the user\'s password (expires in 24 hours)', + 'created_by': 'The name of the admin who created the account' }, 'Password Reset': { 'user.username': 'The username of the account', diff --git a/utils/__pycache__/notification.cpython-313.pyc b/utils/__pycache__/notification.cpython-313.pyc index 4d2532f..c280b52 100644 Binary files a/utils/__pycache__/notification.cpython-313.pyc and b/utils/__pycache__/notification.cpython-313.pyc differ diff --git a/utils/notification.py b/utils/notification.py index 0b54bdc..fe4298f 100644 --- a/utils/notification.py +++ b/utils/notification.py @@ -131,6 +131,9 @@ def generate_mail_from_notification(notif: Notif) -> Optional[Mail]: if attr in notif.details[obj_name]: filled_body = filled_body.replace(f'{{{{ {key} }}}}', str(notif.details[obj_name][attr])) else: + # Special handling for setup_link to ensure it's a proper URL + if key == 'setup_link' and value.startswith('http://http//'): + value = value.replace('http://http//', 'http://') filled_body = filled_body.replace(f'{{{{ {key} }}}}', str(value)) # Handle special URL variables