mirror of
https://github.com/OHV-IT/collabrix.git
synced 2025-12-15 16:48:36 +01:00
- Added complete Kanban board functionality with drag-and-drop - Implemented auto-save for Kanban card editing (no more edit button) - Added route persistence to remember last visited page on reload - Improved Kanban UI design with slimmer borders and compact layout - Added checklist functionality for Kanban cards - Enhanced file upload and direct messaging features - Improved authentication and user management - Added toast notifications system - Various UI/UX improvements and bug fixes
250 lines
8.4 KiB
Python
250 lines
8.4 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks
|
|
from sqlmodel import Session, select
|
|
from sqlalchemy.orm import joinedload
|
|
from typing import List
|
|
import os
|
|
from app.database import get_session
|
|
from app.models import Message, Channel, User, FileAttachment
|
|
from app.schemas import MessageCreate, MessageResponse
|
|
from app.auth import get_current_user
|
|
from app.websocket import manager
|
|
|
|
router = APIRouter(prefix="/messages", tags=["Messages"])
|
|
|
|
|
|
def user_has_channel_access(user: User, channel_id: int, session: Session) -> bool:
|
|
"""Check if user has access to a channel"""
|
|
channel = session.get(Channel, channel_id)
|
|
if not channel:
|
|
return False
|
|
|
|
user_dept_ids = [dept.id for dept in user.departments]
|
|
return channel.department_id in user_dept_ids
|
|
|
|
|
|
@router.post("/", response_model=MessageResponse, status_code=status.HTTP_201_CREATED)
|
|
async def create_message(
|
|
message_data: MessageCreate,
|
|
background_tasks: BackgroundTasks,
|
|
session: Session = Depends(get_session),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Create a new message in a channel"""
|
|
# Check if channel exists
|
|
channel = session.get(Channel, message_data.channel_id)
|
|
if not channel:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Channel not found"
|
|
)
|
|
|
|
# Check if user has access to this channel
|
|
if not user_has_channel_access(current_user, message_data.channel_id, session):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="You don't have access to this channel"
|
|
)
|
|
|
|
new_message = Message(
|
|
content=message_data.content,
|
|
sender_id=current_user.id,
|
|
channel_id=message_data.channel_id,
|
|
snippet_id=message_data.snippet_id,
|
|
reply_to_id=message_data.reply_to_id
|
|
)
|
|
|
|
session.add(new_message)
|
|
session.commit()
|
|
session.refresh(new_message)
|
|
|
|
# Build response dict manually to avoid issues with relationships
|
|
reply_to_data = None
|
|
if new_message.reply_to_id:
|
|
reply_msg = session.get(Message, new_message.reply_to_id)
|
|
if reply_msg:
|
|
reply_sender = session.get(User, reply_msg.sender_id)
|
|
reply_to_data = {
|
|
"id": reply_msg.id,
|
|
"content": reply_msg.content,
|
|
"sender_username": reply_sender.username if reply_sender else "Unknown"
|
|
}
|
|
|
|
response_data = {
|
|
"id": new_message.id,
|
|
"content": new_message.content,
|
|
"channel_id": new_message.channel_id,
|
|
"sender_id": new_message.sender_id,
|
|
"sender_username": current_user.username,
|
|
"sender_full_name": current_user.full_name,
|
|
"sender_profile_picture": current_user.profile_picture,
|
|
"created_at": new_message.created_at.isoformat(),
|
|
"snippet_id": new_message.snippet_id,
|
|
"reply_to_id": new_message.reply_to_id,
|
|
"reply_to": reply_to_data,
|
|
"snippet": None,
|
|
"attachments": [],
|
|
"is_deleted": False
|
|
}
|
|
|
|
# Broadcast to all connected clients in this channel
|
|
await manager.broadcast_to_channel(
|
|
{
|
|
"type": "message",
|
|
"message": response_data
|
|
},
|
|
message_data.channel_id
|
|
)
|
|
|
|
# Return proper response
|
|
response = MessageResponse.model_validate(new_message)
|
|
response.sender_username = current_user.username
|
|
response.sender_full_name = current_user.full_name
|
|
response.sender_profile_picture = current_user.profile_picture
|
|
|
|
return response
|
|
|
|
|
|
@router.get("/channel/{channel_id}", response_model=List[MessageResponse])
|
|
def get_channel_messages(
|
|
channel_id: int,
|
|
limit: int = 50,
|
|
offset: int = 0,
|
|
session: Session = Depends(get_session),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Get messages from a channel"""
|
|
# Check if user has access to this channel
|
|
if not user_has_channel_access(current_user, channel_id, session):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="You don't have access to this channel"
|
|
)
|
|
|
|
statement = (
|
|
select(Message)
|
|
.where(Message.channel_id == channel_id)
|
|
.options(joinedload(Message.attachments))
|
|
.order_by(Message.created_at.desc())
|
|
.offset(offset)
|
|
.limit(limit)
|
|
)
|
|
messages = session.exec(statement).unique().all()
|
|
|
|
# Add sender usernames and reply_to info
|
|
responses = []
|
|
for msg in messages:
|
|
msg_response = MessageResponse.model_validate(msg)
|
|
sender = session.get(User, msg.sender_id)
|
|
msg_response.sender_username = sender.username if sender else "Unknown"
|
|
msg_response.sender_full_name = sender.full_name if sender else None
|
|
msg_response.sender_profile_picture = sender.profile_picture if sender else None
|
|
|
|
# Add reply_to info if exists
|
|
if msg.reply_to_id:
|
|
reply_msg = session.get(Message, msg.reply_to_id)
|
|
if reply_msg:
|
|
reply_sender = session.get(User, reply_msg.sender_id)
|
|
msg_response.reply_to = {
|
|
"id": reply_msg.id,
|
|
"content": reply_msg.content,
|
|
"sender_username": reply_sender.username if reply_sender else "Unknown"
|
|
}
|
|
|
|
responses.append(msg_response)
|
|
|
|
# Reverse to show oldest first
|
|
return list(reversed(responses))
|
|
|
|
|
|
@router.get("/{message_id}", response_model=MessageResponse)
|
|
def get_message(
|
|
message_id: int,
|
|
session: Session = Depends(get_session),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Get a specific message"""
|
|
message = session.get(Message, message_id)
|
|
if not message:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Message not found"
|
|
)
|
|
|
|
# Check if user has access to this message's channel
|
|
if not user_has_channel_access(current_user, message.channel_id, session):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="You don't have access to this message"
|
|
)
|
|
|
|
response = MessageResponse.model_validate(message)
|
|
sender = session.get(User, message.sender_id)
|
|
response.sender_username = sender.username if sender else "Unknown"
|
|
response.sender_full_name = sender.full_name if sender else None
|
|
response.sender_profile_picture = sender.profile_picture if sender else None
|
|
|
|
return response
|
|
|
|
|
|
@router.delete("/{message_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_message(
|
|
message_id: int,
|
|
session: Session = Depends(get_session),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Delete a message and its attachments"""
|
|
message = session.get(Message, message_id)
|
|
if not message:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Message not found"
|
|
)
|
|
|
|
# Only sender can delete their own messages
|
|
if message.sender_id != current_user.id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="You can only delete your own messages"
|
|
)
|
|
|
|
# Check channel access
|
|
if not user_has_channel_access(current_user, message.channel_id, session):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="You don't have access to this channel"
|
|
)
|
|
|
|
channel_id = message.channel_id
|
|
|
|
# Delete all file attachments
|
|
statement = select(FileAttachment).where(FileAttachment.message_id == message_id)
|
|
attachments = session.exec(statement).all()
|
|
|
|
for attachment in attachments:
|
|
# Delete physical file
|
|
if os.path.exists(attachment.file_path):
|
|
try:
|
|
os.remove(attachment.file_path)
|
|
except Exception as e:
|
|
print(f"Error deleting file {attachment.file_path}: {e}")
|
|
|
|
# Delete database record
|
|
session.delete(attachment)
|
|
|
|
# Mark message as deleted instead of removing it
|
|
message.is_deleted = True
|
|
message.content = "Diese Nachricht wurde gelöscht"
|
|
session.add(message)
|
|
session.commit()
|
|
|
|
# Broadcast deletion to all clients
|
|
await manager.broadcast_to_channel(
|
|
{
|
|
"type": "message_deleted",
|
|
"message_id": message_id
|
|
},
|
|
channel_id
|
|
)
|
|
|
|
return None
|