Files

145 lines
5.5 KiB
Python

import uuid
from datetime import datetime
from app import db
class ProjectMember(db.Model):
__tablename__ = 'project_members'
# A user can only hold one membership row per project. Without this
# constraint the accept_invitation flow is a TOCTOU race: two concurrent
# requests both pass is_member(user_id)=False and both INSERT.
__table_args__ = (
db.UniqueConstraint('project_id', 'user_id',
name='uq_project_members_project_user'),
)
id = db.Column(db.String(36), primary_key=True,
default=lambda: str(uuid.uuid4()))
project_id = db.Column(db.String(36), db.ForeignKey(
'projects.id'), nullable=False)
user_id = db.Column(db.String(36), db.ForeignKey(
'users.id'), nullable=False)
# 'owner', 'admin', 'member'
role = db.Column(db.String(20), default='member')
joined_at = db.Column(db.DateTime, default=datetime.utcnow)
# Relationships
project = db.relationship('Project', backref=db.backref('memberships', cascade='all, delete-orphan'))
user = db.relationship('User', backref=db.backref('project_memberships', cascade='all, delete-orphan'))
def to_dict(self):
return {
'id': self.id,
'project_id': self.project_id,
'user_id': self.user_id,
'role': self.role,
'joined_at': self.joined_at.isoformat(),
'user': self.user.to_dict() if self.user else None
}
class ProjectInvitation(db.Model):
__tablename__ = 'project_invitations'
id = db.Column(db.String(36), primary_key=True,
default=lambda: str(uuid.uuid4()))
project_id = db.Column(db.String(36), db.ForeignKey(
'projects.id'), nullable=False)
email = db.Column(db.String(120), nullable=False)
invited_by = db.Column(
db.String(36), db.ForeignKey('users.id'), nullable=False)
role = db.Column(db.String(20), default='member')
# 'pending', 'accepted', 'declined', 'expired'
status = db.Column(db.String(20), default='pending')
token = db.Column(db.String(255), unique=True, nullable=False)
expires_at = db.Column(db.DateTime, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# Relationships
project = db.relationship('Project', backref=db.backref('invitations', cascade='all, delete-orphan'))
inviter = db.relationship('User', backref=db.backref('sent_invitations', cascade='all, delete-orphan'))
def to_dict(self):
return {
'id': self.id,
'project_id': self.project_id,
'email': self.email,
'invited_by': self.invited_by,
'role': self.role,
'status': self.status,
'expires_at': self.expires_at.isoformat(),
'created_at': self.created_at.isoformat()
}
class Project(db.Model):
__tablename__ = 'projects'
id = db.Column(db.String(36), primary_key=True,
default=lambda: str(uuid.uuid4()))
name = db.Column(db.String(100), nullable=False)
description = db.Column(db.Text)
user_id = db.Column(db.String(36), db.ForeignKey(
'users.id'), nullable=False)
is_active = db.Column(db.Boolean, default=True)
discord_webhook_url = db.Column(db.String(500))
discord_notifications_enabled = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(
db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
api_keys = db.relationship(
'APIKey', backref='project', lazy=True, cascade='all, delete-orphan')
content = db.relationship(
'Content', backref='project', lazy=True, cascade='all, delete-orphan')
moderation_rules = db.relationship(
'ModerationRule', backref='project', lazy=True, cascade='all, delete-orphan')
@property
def members(self):
"""Get all members of the project"""
return [membership.user for membership in self.memberships]
@property
def member_ids(self):
"""Get all member user IDs"""
return [membership.user_id for membership in self.memberships]
def is_member(self, user_id):
"""Check if a user is a member of this project"""
return user_id in self.member_ids or user_id == self.user_id
def get_member_role(self, user_id):
"""Get the role of a user in this project"""
if user_id == self.user_id:
return 'owner'
membership = next(
(m for m in self.memberships if m.user_id == user_id), None)
return membership.role if membership else None
def can_manage_members(self, user_id):
"""Check if a user can manage project members"""
role = self.get_member_role(user_id)
return role in ['owner', 'admin']
def to_dict(self):
return {
'id': self.id,
'name': self.name,
'description': self.description,
'user_id': self.user_id,
'is_active': self.is_active,
'discord_webhook_url': self.discord_webhook_url,
'discord_notifications_enabled': self.discord_notifications_enabled,
'created_at': self.created_at.isoformat(),
'updated_at': self.updated_at.isoformat(),
'api_keys_count': len(self.api_keys),
'content_count': len(self.content),
'members_count': len(self.memberships)
}
def __repr__(self):
return f'<Project {self.name}>'