initial commit - LeDiscord plateforme des copains

This commit is contained in:
EvanChal
2025-08-21 00:28:21 +02:00
commit b7a84a53aa
93 changed files with 16247 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
from . import auth, users, events, albums, posts, vlogs, stats, admin, notifications, settings, information, tickets
__all__ = ["auth", "users", "events", "albums", "posts", "vlogs", "stats", "admin", "notifications", "settings", "information", "tickets"]

View File

@@ -0,0 +1,444 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from sqlalchemy import func
from typing import List
import os
from pathlib import Path
from config.database import get_db
from config.settings import settings
from models.user import User
from models.album import Album, Media
from models.vlog import Vlog
from models.event import Event
from models.post import Post
from utils.security import get_admin_user
router = APIRouter()
def get_directory_size(path):
"""Calculate total size of a directory."""
total = 0
try:
for entry in os.scandir(path):
if entry.is_file():
total += entry.stat().st_size
elif entry.is_dir():
total += get_directory_size(entry.path)
except (OSError, PermissionError):
pass
return total
def format_bytes(bytes):
"""Format bytes to human readable string."""
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if bytes < 1024.0:
return f"{bytes:.2f} {unit}"
bytes /= 1024.0
return f"{bytes:.2f} PB"
@router.get("/dashboard")
async def get_admin_dashboard(
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user)
):
"""Get admin dashboard information."""
# User statistics
total_users = db.query(User).count()
active_users = db.query(User).filter(User.is_active == True).count()
admin_users = db.query(User).filter(User.is_admin == True).count()
# Content statistics
total_events = db.query(Event).count()
total_posts = db.query(Post).count()
total_vlogs = db.query(Vlog).count()
total_media = db.query(Media).count()
# Storage statistics
upload_path = Path(settings.UPLOAD_PATH)
total_storage = 0
storage_breakdown = {}
if upload_path.exists():
# Calculate storage by category
categories = ['avatars', 'albums', 'vlogs', 'posts']
for category in categories:
category_path = upload_path / category
if category_path.exists():
size = get_directory_size(category_path)
storage_breakdown[category] = {
"bytes": size,
"formatted": format_bytes(size)
}
total_storage += size
# Database storage
db_size = db.query(func.sum(Media.file_size)).scalar() or 0
return {
"users": {
"total": total_users,
"active": active_users,
"admins": admin_users
},
"content": {
"events": total_events,
"posts": total_posts,
"vlogs": total_vlogs,
"media_files": total_media
},
"storage": {
"total_bytes": total_storage,
"total_formatted": format_bytes(total_storage),
"breakdown": storage_breakdown,
"database_tracked": format_bytes(db_size)
}
}
@router.get("/users")
async def get_all_users_admin(
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user)
):
"""Get all users with admin details."""
users = db.query(User).all()
user_list = []
for user in users:
# Calculate user storage
user_media = db.query(func.sum(Media.file_size)).join(Album).filter(
Album.creator_id == user.id
).scalar() or 0
user_vlogs = db.query(func.sum(Vlog.id)).filter(
Vlog.author_id == user.id
).scalar() or 0
user_list.append({
"id": user.id,
"email": user.email,
"username": user.username,
"full_name": user.full_name,
"is_active": user.is_active,
"is_admin": user.is_admin,
"created_at": user.created_at,
"attendance_rate": user.attendance_rate,
"storage_used": format_bytes(user_media),
"content_count": {
"posts": db.query(Post).filter(Post.author_id == user.id).count(),
"vlogs": user_vlogs,
"albums": db.query(Album).filter(Album.creator_id == user.id).count()
}
})
return user_list
@router.put("/users/{user_id}/toggle-active")
async def toggle_user_active(
user_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user)
):
"""Toggle user active status."""
if user_id == current_user.id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Cannot deactivate your own account"
)
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
user.is_active = not user.is_active
db.commit()
return {
"message": f"User {'activated' if user.is_active else 'deactivated'} successfully",
"is_active": user.is_active
}
@router.put("/users/{user_id}/toggle-admin")
async def toggle_user_admin(
user_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user)
):
"""Toggle user admin status."""
if user_id == current_user.id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Cannot change your own admin status"
)
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
user.is_admin = not user.is_admin
db.commit()
return {
"message": f"User admin status {'granted' if user.is_admin else 'revoked'} successfully",
"is_admin": user.is_admin
}
@router.delete("/users/{user_id}")
async def delete_user(
user_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user)
):
"""Delete a user and all their content."""
if user_id == current_user.id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Cannot delete your own account"
)
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
# Delete user's files
user_dirs = [
Path(settings.UPLOAD_PATH) / "avatars" / str(user_id),
Path(settings.UPLOAD_PATH) / "vlogs" / str(user_id)
]
for dir_path in user_dirs:
if dir_path.exists():
import shutil
shutil.rmtree(dir_path)
# Delete user (cascade will handle related records)
db.delete(user)
db.commit()
return {"message": "User deleted successfully"}
@router.get("/storage/cleanup")
async def cleanup_storage(
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user)
):
"""Find and optionally clean up orphaned files."""
upload_path = Path(settings.UPLOAD_PATH)
orphaned_files = []
total_orphaned_size = 0
try:
# Check for orphaned media files
if (upload_path / "albums").exists():
for album_dir in (upload_path / "albums").iterdir():
if album_dir.is_dir():
try:
album_id = int(album_dir.name)
album = db.query(Album).filter(Album.id == album_id).first()
if not album:
# Entire album directory is orphaned
size = get_directory_size(album_dir)
orphaned_files.append({
"path": str(album_dir),
"type": "album_directory",
"size": format_bytes(size)
})
total_orphaned_size += size
else:
# Check individual files
for file_path in album_dir.glob("*"):
if file_path.is_file():
relative_path = f"/albums/{album_id}/{file_path.name}"
media = db.query(Media).filter(
(Media.file_path == relative_path) |
(Media.thumbnail_path == relative_path)
).first()
if not media:
size = file_path.stat().st_size
orphaned_files.append({
"path": str(file_path),
"type": "media_file",
"size": format_bytes(size)
})
total_orphaned_size += size
except (ValueError, OSError) as e:
print(f"Error processing album directory {album_dir}: {e}")
continue
# Check for orphaned avatar files
if (upload_path / "avatars").exists():
for user_dir in (upload_path / "avatars").iterdir():
if user_dir.is_dir():
try:
user_id = int(user_dir.name)
user = db.query(User).filter(User.id == user_id).first()
if not user:
size = get_directory_size(user_dir)
orphaned_files.append({
"path": str(user_dir),
"type": "user_avatar_directory",
"size": format_bytes(size)
})
total_orphaned_size += size
except (ValueError, OSError) as e:
print(f"Error processing avatar directory {user_dir}: {e}")
continue
# Check for orphaned vlog files
if (upload_path / "vlogs").exists():
for user_dir in (upload_path / "vlogs").iterdir():
if user_dir.is_dir():
try:
user_id = int(user_dir.name)
user = db.query(User).filter(User.id == user_id).first()
if not user:
size = get_directory_size(user_dir)
orphaned_files.append({
"path": str(user_dir),
"type": "user_vlog_directory",
"size": format_bytes(size)
})
total_orphaned_size += size
except (ValueError, OSError) as e:
print(f"Error processing vlog directory {user_dir}: {e}")
continue
return {
"orphaned_files": orphaned_files,
"total_orphaned": len(orphaned_files),
"total_orphaned_size": format_bytes(total_orphaned_size),
"message": f"Trouvé {len(orphaned_files)} fichier(s) orphelin(s) pour un total de {format_bytes(total_orphaned_size)}. Utilisez DELETE pour les supprimer."
}
except Exception as e:
print(f"Error during cleanup scan: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error during cleanup scan: {str(e)}"
)
@router.delete("/storage/cleanup")
async def delete_orphaned_files(
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user)
):
"""Delete orphaned files."""
upload_path = Path(settings.UPLOAD_PATH)
deleted_files = []
total_freed_space = 0
try:
# Check for orphaned media files
if (upload_path / "albums").exists():
for album_dir in (upload_path / "albums").iterdir():
if album_dir.is_dir():
try:
album_id = int(album_dir.name)
album = db.query(Album).filter(Album.id == album_id).first()
if not album:
# Entire album directory is orphaned
size = get_directory_size(album_dir)
import shutil
shutil.rmtree(album_dir)
deleted_files.append({
"path": str(album_dir),
"type": "album_directory",
"size": format_bytes(size)
})
total_freed_space += size
else:
# Check individual files
for file_path in album_dir.glob("*"):
if file_path.is_file():
relative_path = f"/albums/{album_id}/{file_path.name}"
media = db.query(Media).filter(
(Media.file_path == relative_path) |
(Media.thumbnail_path == relative_path)
).first()
if not media:
size = file_path.stat().st_size
file_path.unlink() # Delete the file
deleted_files.append({
"path": str(file_path),
"type": "media_file",
"size": format_bytes(size)
})
total_freed_space += size
except (ValueError, OSError) as e:
print(f"Error processing album directory {album_dir}: {e}")
continue
# Check for orphaned avatar files
if (upload_path / "avatars").exists():
for user_dir in (upload_path / "avatars").iterdir():
if user_dir.is_dir():
try:
user_id = int(user_dir.name)
user = db.query(User).filter(User.id == user_id).first()
if not user:
# User directory is orphaned
size = get_directory_size(user_dir)
import shutil
shutil.rmtree(user_dir)
deleted_files.append({
"path": str(user_dir),
"type": "user_avatar_directory",
"size": format_bytes(size)
})
total_freed_space += size
except (ValueError, OSError) as e:
print(f"Error processing avatar directory {user_dir}: {e}")
continue
# Check for orphaned vlog files
if (upload_path / "vlogs").exists():
for user_dir in (upload_path / "vlogs").iterdir():
if user_dir.is_dir():
try:
user_id = int(user_dir.name)
user = db.query(User).filter(User.id == user_id).first()
if not user:
# User directory is orphaned
size = get_directory_size(user_dir)
import shutil
shutil.rmtree(user_dir)
deleted_files.append({
"path": str(user_dir),
"type": "user_vlog_directory",
"size": format_bytes(size)
})
total_freed_space += size
except (ValueError, OSError) as e:
print(f"Error processing vlog directory {user_dir}: {e}")
continue
return {
"message": "Cleanup completed successfully",
"deleted_files": deleted_files,
"total_deleted": len(deleted_files),
"total_freed_space": format_bytes(total_freed_space),
"details": f"Supprimé {len(deleted_files)} fichier(s) pour libérer {format_bytes(total_freed_space)}"
}
except Exception as e:
print(f"Error during cleanup: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error during cleanup: {str(e)}"
)

View File

@@ -0,0 +1,464 @@
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form
from sqlalchemy.orm import Session
from typing import List, Optional
import os
import uuid
from pathlib import Path
from PIL import Image
import magic
from config.database import get_db
from config.settings import settings
from models.album import Album, Media, MediaType, MediaLike
from models.user import User
from schemas.album import AlbumCreate, AlbumUpdate, AlbumResponse
from utils.security import get_current_active_user
from utils.settings_service import SettingsService
router = APIRouter()
@router.post("/", response_model=AlbumResponse)
async def create_album(
album_data: AlbumCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Create a new album."""
album = Album(
**album_data.dict(),
creator_id=current_user.id
)
db.add(album)
db.commit()
db.refresh(album)
return format_album_response(album, db, current_user.id)
@router.get("/", response_model=List[AlbumResponse])
async def get_albums(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user),
event_id: Optional[int] = None
):
"""Get all albums, optionally filtered by event."""
query = db.query(Album)
if event_id:
query = query.filter(Album.event_id == event_id)
albums = query.order_by(Album.created_at.desc()).all()
return [format_album_response(album, db, current_user.id) for album in albums]
@router.get("/{album_id}", response_model=AlbumResponse)
async def get_album(
album_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Get a specific album."""
album = db.query(Album).filter(Album.id == album_id).first()
if not album:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Album not found"
)
return format_album_response(album, db, current_user.id)
@router.put("/{album_id}", response_model=AlbumResponse)
async def update_album(
album_id: int,
album_update: AlbumUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Update an album."""
album = db.query(Album).filter(Album.id == album_id).first()
if not album:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Album not found"
)
if album.creator_id != current_user.id and not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to update this album"
)
for field, value in album_update.dict(exclude_unset=True).items():
setattr(album, field, value)
db.commit()
db.refresh(album)
return format_album_response(album, db, current_user.id)
@router.delete("/{album_id}")
async def delete_album(
album_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Delete an album."""
album = db.query(Album).filter(Album.id == album_id).first()
if not album:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Album not found"
)
if album.creator_id != current_user.id and not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to delete this album"
)
# Delete media files
for media in album.media:
try:
if media.file_path:
os.remove(settings.UPLOAD_PATH + media.file_path)
if media.thumbnail_path:
os.remove(settings.UPLOAD_PATH + media.thumbnail_path)
except:
pass
db.delete(album)
db.commit()
return {"message": "Album deleted successfully"}
@router.post("/{album_id}/media")
async def upload_media(
album_id: int,
files: List[UploadFile] = File(...),
captions: Optional[List[str]] = Form(None),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Upload media files to an album."""
album = db.query(Album).filter(Album.id == album_id).first()
if not album:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Album not found"
)
if album.creator_id != current_user.id and not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to add media to this album"
)
# Check max media per album limit
from utils.settings_service import SettingsService
max_media_per_album = SettingsService.get_setting(db, "max_media_per_album", 50)
current_media_count = len(album.media)
if current_media_count + len(files) > max_media_per_album:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Album would exceed maximum of {max_media_per_album} media files. Current: {current_media_count}, trying to add: {len(files)}"
)
# Create album directory
album_dir = Path(settings.UPLOAD_PATH) / "albums" / str(album_id)
album_dir.mkdir(parents=True, exist_ok=True)
thumbnails_dir = album_dir / "thumbnails"
thumbnails_dir.mkdir(exist_ok=True)
uploaded_media = []
for i, file in enumerate(files):
# Check file size
contents = await file.read()
file_size = len(contents)
# Get dynamic upload limits
max_size = SettingsService.get_max_upload_size(db, file.content_type or "unknown")
if file_size > max_size:
max_size_mb = max_size // (1024 * 1024)
raise HTTPException(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
detail=f"File {file.filename} exceeds maximum size of {max_size_mb}MB"
)
# Check file type
mime = magic.from_buffer(contents, mime=True)
if not SettingsService.is_file_type_allowed(db, mime):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"File {file.filename} has unsupported type: {mime}"
)
# Determine media type
if mime.startswith('image/'):
media_type = MediaType.IMAGE
elif mime.startswith('video/'):
media_type = MediaType.VIDEO
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"File {file.filename} has unsupported type: {mime}"
)
# Generate unique filename
file_extension = file.filename.split(".")[-1]
filename = f"{uuid.uuid4()}.{file_extension}"
file_path = album_dir / filename
# Save file
with open(file_path, "wb") as f:
f.write(contents)
# Process media
thumbnail_path = None
width = height = duration = None
if media_type == MediaType.IMAGE:
# Get image dimensions and create thumbnail
try:
img = Image.open(file_path)
width, height = img.size
# Create thumbnail with better quality
thumbnail = img.copy()
thumbnail.thumbnail((800, 800)) # Larger thumbnail for better quality
thumbnail_filename = f"thumb_{filename}"
thumbnail_path_full = thumbnails_dir / thumbnail_filename
thumbnail.save(thumbnail_path_full, quality=95, optimize=True) # High quality
thumbnail_path = f"/albums/{album_id}/thumbnails/{thumbnail_filename}"
except Exception as e:
print(f"Error processing image: {e}")
# Create media record
media = Media(
album_id=album_id,
file_path=f"/albums/{album_id}/{filename}",
thumbnail_path=thumbnail_path,
media_type=media_type,
caption=captions[i] if captions and i < len(captions) else None,
file_size=file_size,
width=width,
height=height,
duration=duration
)
db.add(media)
uploaded_media.append(media)
# Set first image as album cover if not set
if not album.cover_image and uploaded_media:
first_image = next((m for m in uploaded_media if m.media_type == MediaType.IMAGE), None)
if first_image:
album.cover_image = first_image.thumbnail_path or first_image.file_path
db.commit()
# Update event cover image if this album is linked to an event
if album.event_id:
update_event_cover_from_album(album.event_id, db)
return {
"message": f"Successfully uploaded {len(uploaded_media)} files",
"media_count": len(uploaded_media)
}
@router.delete("/{album_id}/media/{media_id}")
async def delete_media(
album_id: int,
media_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Delete a media file from an album."""
media = db.query(Media).filter(
Media.id == media_id,
Media.album_id == album_id
).first()
if not media:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Media not found"
)
album = media.album
if album.creator_id != current_user.id and not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to delete this media"
)
# Delete files
try:
if media.file_path:
os.remove(settings.UPLOAD_PATH + media.file_path)
if media.thumbnail_path:
os.remove(settings.UPLOAD_PATH + media.thumbnail_path)
except:
pass
db.delete(media)
db.commit()
# Update event cover image if this album is linked to an event
if album.event_id:
update_event_cover_from_album(album.event_id, db)
return {"message": "Media deleted successfully"}
@router.post("/{album_id}/media/{media_id}/like")
async def toggle_media_like(
album_id: int,
media_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Toggle like on a media file."""
media = db.query(Media).filter(
Media.id == media_id,
Media.album_id == album_id
).first()
if not media:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Media not found"
)
existing_like = db.query(MediaLike).filter(
MediaLike.media_id == media_id,
MediaLike.user_id == current_user.id
).first()
if existing_like:
# Unlike
db.delete(existing_like)
media.likes_count = max(0, media.likes_count - 1)
message = "Like removed"
else:
# Like
like = MediaLike(media_id=media_id, user_id=current_user.id)
db.add(like)
media.likes_count += 1
message = "Media liked"
db.commit()
# Update event cover image if this album is linked to an event
album = media.album
if album.event_id:
update_event_cover_from_album(album.event_id, db)
return {"message": message, "likes_count": media.likes_count}
def format_album_response(album: Album, db: Session, current_user_id: int) -> dict:
"""Format album response with additional information."""
# Get top media (most liked)
top_media = db.query(Media).filter(
Media.album_id == album.id
).order_by(Media.likes_count.desc()).limit(3).all()
# Format media with likes information
formatted_media = []
for media in album.media:
# Check if current user liked this media
is_liked = db.query(MediaLike).filter(
MediaLike.media_id == media.id,
MediaLike.user_id == current_user_id
).first() is not None
# Format likes
likes = []
for like in media.likes:
user = db.query(User).filter(User.id == like.user_id).first()
if user:
likes.append({
"id": like.id,
"user_id": user.id,
"username": user.username,
"full_name": user.full_name,
"avatar_url": user.avatar_url,
"created_at": like.created_at
})
formatted_media.append({
"id": media.id,
"file_path": media.file_path,
"thumbnail_path": media.thumbnail_path,
"media_type": media.media_type,
"caption": media.caption,
"file_size": media.file_size,
"width": media.width,
"height": media.height,
"duration": media.duration,
"likes_count": media.likes_count,
"is_liked": is_liked,
"likes": likes,
"created_at": media.created_at
})
# Format top media
formatted_top_media = []
for media in top_media:
is_liked = db.query(MediaLike).filter(
MediaLike.media_id == media.id,
MediaLike.user_id == current_user_id
).first() is not None
likes = []
for like in media.likes:
user = db.query(User).filter(User.id == like.user_id).first()
if user:
likes.append({
"id": like.id,
"user_id": user.id,
"username": user.username,
"full_name": user.full_name,
"avatar_url": user.avatar_url,
"created_at": like.created_at
})
formatted_top_media.append({
"id": media.id,
"file_path": media.file_path,
"thumbnail_path": media.thumbnail_path,
"media_type": media.media_type,
"caption": media.caption,
"file_size": media.file_size,
"width": media.width,
"height": media.height,
"duration": media.duration,
"likes_count": media.likes_count,
"is_liked": is_liked,
"likes": likes,
"created_at": media.created_at
})
return {
"id": album.id,
"title": album.title,
"description": album.description,
"creator_id": album.creator_id,
"event_id": album.event_id,
"cover_image": album.cover_image,
"created_at": album.created_at,
"updated_at": album.updated_at,
"creator_name": album.creator.full_name,
"media_count": len(album.media),
"media": formatted_media,
"top_media": formatted_top_media,
"event_title": album.event.title if album.event else None
}
def update_event_cover_from_album(event_id: int, db: Session):
"""Update event cover image from the most liked media in linked albums."""
from models.event import Event
event = db.query(Event).filter(Event.id == event_id).first()
if not event:
return
# Get the most liked media from all albums linked to this event
most_liked_media = db.query(Media).join(Album).filter(
Album.event_id == event_id,
Media.media_type == MediaType.IMAGE
).order_by(Media.likes_count.desc()).first()
if most_liked_media:
event.cover_image = most_liked_media.thumbnail_path or most_liked_media.file_path
db.commit()

View File

@@ -0,0 +1,97 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from datetime import timedelta
from config.database import get_db
from config.settings import settings
from models.user import User
from schemas.user import UserCreate, UserResponse, Token
from utils.security import verify_password, get_password_hash, create_access_token
router = APIRouter()
@router.post("/register", response_model=Token)
async def register(user_data: UserCreate, db: Session = Depends(get_db)):
"""Register a new user."""
# Check if registration is enabled
from utils.settings_service import SettingsService
if not SettingsService.get_setting(db, "enable_registration", True):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Registration is currently disabled"
)
# Check if max users limit is reached
max_users = SettingsService.get_setting(db, "max_users", 50)
current_users_count = db.query(User).count()
if current_users_count >= max_users:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Maximum number of users reached"
)
# Check if email already exists
if db.query(User).filter(User.email == user_data.email).first():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered"
)
# Check if username already exists
if db.query(User).filter(User.username == user_data.username).first():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Username already taken"
)
# Create new user
user = User(
email=user_data.email,
username=user_data.username,
full_name=user_data.full_name,
hashed_password=get_password_hash(user_data.password)
)
db.add(user)
db.commit()
db.refresh(user)
# Create access token
access_token = create_access_token(
data={"sub": str(user.id), "email": user.email}
)
return {
"access_token": access_token,
"token_type": "bearer",
"user": user
}
@router.post("/login", response_model=Token)
async def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
"""Login with email and password."""
# Find user by email (username field is used for email in OAuth2PasswordRequestForm)
user = db.query(User).filter(User.email == form_data.username).first()
if not user or not verify_password(form_data.password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
headers={"WWW-Authenticate": "Bearer"},
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Inactive user"
)
# Create access token
access_token = create_access_token(
data={"sub": str(user.id), "email": user.email}
)
return {
"access_token": access_token,
"token_type": "bearer",
"user": user
}

View File

@@ -0,0 +1,257 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from datetime import datetime
from config.database import get_db
from models.event import Event, EventParticipation, ParticipationStatus
from models.user import User
from models.notification import Notification, NotificationType
from schemas.event import EventCreate, EventUpdate, EventResponse, ParticipationUpdate
from utils.security import get_current_active_user
from utils.email import send_event_notification
router = APIRouter()
@router.post("/", response_model=EventResponse)
async def create_event(
event_data: EventCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Create a new event."""
event = Event(
**event_data.dict(),
creator_id=current_user.id
)
db.add(event)
db.commit()
db.refresh(event)
# Create participations for all active users
users = db.query(User).filter(User.is_active == True).all()
for user in users:
participation = EventParticipation(
event_id=event.id,
user_id=user.id,
status=ParticipationStatus.PENDING
)
db.add(participation)
# Create notification
if user.id != current_user.id:
notification = Notification(
user_id=user.id,
type=NotificationType.EVENT_INVITATION,
title=f"Nouvel événement: {event.title}",
message=f"{current_user.full_name} a créé un nouvel événement",
link=f"/events/{event.id}"
)
db.add(notification)
# Send email notification (async task would be better)
try:
send_event_notification(user.email, event)
except:
pass # Don't fail if email sending fails
db.commit()
return format_event_response(event, db)
@router.get("/", response_model=List[EventResponse])
async def get_events(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user),
upcoming: bool = None
):
"""Get all events, optionally filtered by upcoming status."""
query = db.query(Event)
if upcoming is True:
# Only upcoming events
query = query.filter(Event.date >= datetime.utcnow())
elif upcoming is False:
# Only past events
query = query.filter(Event.date < datetime.utcnow())
# If upcoming is None, return all events
events = query.order_by(Event.date.desc()).all()
return [format_event_response(event, db) for event in events]
@router.get("/upcoming", response_model=List[EventResponse])
async def get_upcoming_events(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Get only upcoming events."""
events = db.query(Event).filter(
Event.date >= datetime.utcnow()
).order_by(Event.date).all()
return [format_event_response(event, db) for event in events]
@router.get("/past", response_model=List[EventResponse])
async def get_past_events(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Get only past events."""
events = db.query(Event).filter(
Event.date < datetime.utcnow()
).order_by(Event.date.desc()).all()
return [format_event_response(event, db) for event in events]
@router.get("/{event_id}", response_model=EventResponse)
async def get_event(
event_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Get a specific event."""
event = db.query(Event).filter(Event.id == event_id).first()
if not event:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Event not found"
)
return format_event_response(event, db)
@router.put("/{event_id}", response_model=EventResponse)
async def update_event(
event_id: int,
event_update: EventUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Update an event."""
event = db.query(Event).filter(Event.id == event_id).first()
if not event:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Event not found"
)
if event.creator_id != current_user.id and not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to update this event"
)
for field, value in event_update.dict(exclude_unset=True).items():
setattr(event, field, value)
db.commit()
db.refresh(event)
return format_event_response(event, db)
@router.delete("/{event_id}")
async def delete_event(
event_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Delete an event."""
event = db.query(Event).filter(Event.id == event_id).first()
if not event:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Event not found"
)
if event.creator_id != current_user.id and not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to delete this event"
)
db.delete(event)
db.commit()
return {"message": "Event deleted successfully"}
@router.put("/{event_id}/participation", response_model=EventResponse)
async def update_participation(
event_id: int,
participation_update: ParticipationUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Update user participation status for an event."""
participation = db.query(EventParticipation).filter(
EventParticipation.event_id == event_id,
EventParticipation.user_id == current_user.id
).first()
if not participation:
# Create new participation if it doesn't exist
participation = EventParticipation(
event_id=event_id,
user_id=current_user.id,
status=participation_update.status
)
db.add(participation)
else:
participation.status = participation_update.status
participation.response_date = datetime.utcnow()
db.commit()
# Update user attendance rate
update_user_attendance_rate(current_user, db)
event = db.query(Event).filter(Event.id == event_id).first()
return format_event_response(event, db)
def format_event_response(event: Event, db: Session) -> dict:
"""Format event response with participation counts."""
participations = []
present_count = absent_count = maybe_count = pending_count = 0
for p in event.participations:
user = db.query(User).filter(User.id == p.user_id).first()
participations.append({
"user_id": user.id,
"username": user.username,
"full_name": user.full_name,
"avatar_url": user.avatar_url,
"status": p.status,
"response_date": p.response_date
})
if p.status == ParticipationStatus.PRESENT:
present_count += 1
elif p.status == ParticipationStatus.ABSENT:
absent_count += 1
elif p.status == ParticipationStatus.MAYBE:
maybe_count += 1
else:
pending_count += 1
return {
"id": event.id,
"title": event.title,
"description": event.description,
"location": event.location,
"date": event.date,
"end_date": event.end_date,
"creator_id": event.creator_id,
"cover_image": event.cover_image,
"created_at": event.created_at,
"updated_at": event.updated_at,
"creator_name": event.creator.full_name,
"participations": participations,
"present_count": present_count,
"absent_count": absent_count,
"maybe_count": maybe_count,
"pending_count": pending_count
}
def update_user_attendance_rate(user: User, db: Session):
"""Update user attendance rate based on past events."""
past_participations = db.query(EventParticipation).join(Event).filter(
EventParticipation.user_id == user.id,
Event.date < datetime.utcnow(),
EventParticipation.status.in_([ParticipationStatus.PRESENT, ParticipationStatus.ABSENT])
).all()
if past_participations:
present_count = sum(1 for p in past_participations if p.status == ParticipationStatus.PRESENT)
user.attendance_rate = (present_count / len(past_participations)) * 100
db.commit()

View File

@@ -0,0 +1,143 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from datetime import datetime
from config.database import get_db
from models.information import Information
from models.user import User
from schemas.information import InformationCreate, InformationUpdate, InformationResponse
from utils.security import get_current_active_user, get_admin_user
router = APIRouter()
@router.get("/", response_model=List[InformationResponse])
async def get_informations(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user),
category: str = None,
published_only: bool = True
):
"""Get all informations, optionally filtered by category and published status."""
query = db.query(Information)
if published_only:
query = query.filter(Information.is_published == True)
if category:
query = query.filter(Information.category == category)
informations = query.order_by(Information.priority.desc(), Information.created_at.desc()).all()
return informations
@router.get("/public", response_model=List[InformationResponse])
async def get_public_informations(
db: Session = Depends(get_db),
category: str = None
):
"""Get public informations without authentication."""
query = db.query(Information).filter(Information.is_published == True)
if category:
query = query.filter(Information.category == category)
informations = query.order_by(Information.priority.desc(), Information.created_at.desc()).all()
return informations
@router.get("/{information_id}", response_model=InformationResponse)
async def get_information(
information_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Get a specific information."""
information = db.query(Information).filter(Information.id == information_id).first()
if not information:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Information not found"
)
if not information.is_published and not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Information not found"
)
return information
@router.post("/", response_model=InformationResponse)
async def create_information(
information_data: InformationCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user)
):
"""Create a new information (admin only)."""
information = Information(**information_data.dict())
db.add(information)
db.commit()
db.refresh(information)
return information
@router.put("/{information_id}", response_model=InformationResponse)
async def update_information(
information_id: int,
information_update: InformationUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user)
):
"""Update an information (admin only)."""
information = db.query(Information).filter(Information.id == information_id).first()
if not information:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Information not found"
)
for field, value in information_update.dict(exclude_unset=True).items():
setattr(information, field, value)
information.updated_at = datetime.utcnow()
db.commit()
db.refresh(information)
return information
@router.delete("/{information_id}")
async def delete_information(
information_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user)
):
"""Delete an information (admin only)."""
information = db.query(Information).filter(Information.id == information_id).first()
if not information:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Information not found"
)
db.delete(information)
db.commit()
return {"message": "Information deleted successfully"}
@router.put("/{information_id}/toggle-publish")
async def toggle_information_publish(
information_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user)
):
"""Toggle information published status (admin only)."""
information = db.query(Information).filter(Information.id == information_id).first()
if not information:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Information not found"
)
information.is_published = not information.is_published
information.updated_at = datetime.utcnow()
db.commit()
return {
"message": f"Information {'published' if information.is_published else 'unpublished'} successfully",
"is_published": information.is_published
}

View File

@@ -0,0 +1,79 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from datetime import datetime
from config.database import get_db
from models.notification import Notification
from models.user import User
from schemas.notification import NotificationResponse
from utils.security import get_current_active_user
router = APIRouter()
@router.get("/", response_model=List[NotificationResponse])
async def get_user_notifications(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user),
limit: int = 50,
offset: int = 0
):
"""Get user notifications."""
notifications = db.query(Notification).filter(
Notification.user_id == current_user.id
).order_by(Notification.created_at.desc()).limit(limit).offset(offset).all()
return notifications
@router.put("/{notification_id}/read")
async def mark_notification_read(
notification_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Mark a notification as read."""
notification = db.query(Notification).filter(
Notification.id == notification_id,
Notification.user_id == current_user.id
).first()
if not notification:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Notification not found"
)
notification.is_read = True
notification.read_at = datetime.utcnow()
db.commit()
return {"message": "Notification marked as read"}
@router.put("/read-all")
async def mark_all_notifications_read(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Mark all user notifications as read."""
db.query(Notification).filter(
Notification.user_id == current_user.id,
Notification.is_read == False
).update({
"is_read": True,
"read_at": datetime.utcnow()
})
db.commit()
return {"message": "All notifications marked as read"}
@router.get("/unread-count")
async def get_unread_count(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Get count of unread notifications."""
count = db.query(Notification).filter(
Notification.user_id == current_user.id,
Notification.is_read == False
).count()
return {"unread_count": count}

View File

@@ -0,0 +1,347 @@
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
from sqlalchemy.orm import Session
from typing import List
from datetime import datetime
import os
import uuid
from pathlib import Path
from PIL import Image
from config.database import get_db
from config.settings import settings
from models.post import Post, PostMention, PostLike, PostComment
from models.user import User
from models.notification import Notification, NotificationType
from utils.notification_service import NotificationService
from schemas.post import PostCreate, PostUpdate, PostResponse, PostCommentCreate
from utils.security import get_current_active_user
from utils.settings_service import SettingsService
router = APIRouter()
@router.post("/", response_model=PostResponse)
async def create_post(
post_data: PostCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Create a new post."""
post = Post(
author_id=current_user.id,
content=post_data.content,
image_url=post_data.image_url
)
db.add(post)
db.flush() # Get the post ID before creating mentions
# Create mentions
for user_id in post_data.mentioned_user_ids:
mentioned_user = db.query(User).filter(User.id == user_id).first()
if mentioned_user:
mention = PostMention(
post_id=post.id,
mentioned_user_id=user_id
)
db.add(mention)
# Create notification for mentioned user
NotificationService.create_mention_notification(
db=db,
mentioned_user_id=user_id,
author=current_user,
content_type="post",
content_id=post.id
)
db.commit()
db.refresh(post)
return format_post_response(post, db)
@router.get("/", response_model=List[PostResponse])
async def get_posts(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user),
limit: int = 50,
offset: int = 0
):
"""Get all posts."""
posts = db.query(Post).order_by(Post.created_at.desc()).limit(limit).offset(offset).all()
return [format_post_response(post, db) for post in posts]
@router.get("/{post_id}", response_model=PostResponse)
async def get_post(
post_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Get a specific post."""
post = db.query(Post).filter(Post.id == post_id).first()
if not post:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Post not found"
)
return format_post_response(post, db)
@router.put("/{post_id}", response_model=PostResponse)
async def update_post(
post_id: int,
post_update: PostUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Update a post."""
post = db.query(Post).filter(Post.id == post_id).first()
if not post:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Post not found"
)
if post.author_id != current_user.id and not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to update this post"
)
for field, value in post_update.dict(exclude_unset=True).items():
setattr(post, field, value)
post.updated_at = datetime.utcnow()
db.commit()
db.refresh(post)
return format_post_response(post, db)
@router.delete("/{post_id}")
async def delete_post(
post_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Delete a post."""
post = db.query(Post).filter(Post.id == post_id).first()
if not post:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Post not found"
)
if post.author_id != current_user.id and not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to delete this post"
)
db.delete(post)
db.commit()
return {"message": "Post deleted successfully"}
@router.post("/upload-image")
async def upload_post_image(
file: UploadFile = File(...),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Upload an image for a post."""
# Validate file type
if not SettingsService.is_file_type_allowed(db, file.content_type or "unknown"):
allowed_types = SettingsService.get_setting(db, "allowed_image_types",
["image/jpeg", "image/png", "image/gif", "image/webp"])
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid file type. Allowed types: {', '.join(allowed_types)}"
)
# Check file size
contents = await file.read()
file_size = len(contents)
max_size = SettingsService.get_max_upload_size(db, file.content_type or "image/jpeg")
if file_size > max_size:
max_size_mb = max_size // (1024 * 1024)
raise HTTPException(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
detail=f"File too large. Maximum size: {max_size_mb}MB"
)
# Create posts directory
posts_dir = Path(settings.UPLOAD_PATH) / "posts" / str(current_user.id)
posts_dir.mkdir(parents=True, exist_ok=True)
# Generate unique filename
file_extension = file.filename.split(".")[-1]
filename = f"{uuid.uuid4()}.{file_extension}"
file_path = posts_dir / filename
# Save file
with open(file_path, "wb") as f:
f.write(contents)
# Resize image if it's REALLY too large (4K support)
try:
img = Image.open(file_path)
if img.size[0] > 3840 or img.size[1] > 2160: # 4K threshold
img.thumbnail((3840, 2160), Image.Resampling.LANCZOS)
img.save(file_path, quality=95, optimize=True) # High quality
except Exception as e:
print(f"Error processing image: {e}")
# Return the image URL
image_url = f"/posts/{current_user.id}/{filename}"
return {"image_url": image_url}
@router.post("/{post_id}/like")
async def toggle_post_like(
post_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Toggle like on a post."""
post = db.query(Post).filter(Post.id == post_id).first()
if not post:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Post not found"
)
# Check if user already liked the post
existing_like = db.query(PostLike).filter(
PostLike.post_id == post_id,
PostLike.user_id == current_user.id
).first()
if existing_like:
# Unlike
db.delete(existing_like)
post.likes_count = max(0, post.likes_count - 1)
message = "Like removed"
is_liked = False
else:
# Like
like = PostLike(post_id=post_id, user_id=current_user.id)
db.add(like)
post.likes_count += 1
message = "Post liked"
is_liked = True
db.commit()
return {"message": message, "is_liked": is_liked, "likes_count": post.likes_count}
@router.post("/{post_id}/comment")
async def add_post_comment(
post_id: int,
comment_data: PostCommentCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Add a comment to a post."""
post = db.query(Post).filter(Post.id == post_id).first()
if not post:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Post not found"
)
comment = PostComment(
post_id=post_id,
author_id=current_user.id,
content=comment_data.content
)
db.add(comment)
db.commit()
db.refresh(comment)
return {
"message": "Comment added successfully",
"comment": {
"id": comment.id,
"content": comment.content,
"author_id": comment.author_id,
"author_name": current_user.full_name,
"author_avatar": current_user.avatar_url,
"created_at": comment.created_at
}
}
@router.delete("/{post_id}/comment/{comment_id}")
async def delete_post_comment(
post_id: int,
comment_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Delete a comment from a post."""
comment = db.query(PostComment).filter(
PostComment.id == comment_id,
PostComment.post_id == post_id
).first()
if not comment:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Comment not found"
)
if comment.author_id != current_user.id and not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to delete this comment"
)
db.delete(comment)
db.commit()
return {"message": "Comment deleted successfully"}
def format_post_response(post: Post, db: Session) -> dict:
"""Format post response with author and mentions information."""
mentioned_users = []
for mention in post.mentions:
user = mention.mentioned_user
mentioned_users.append({
"id": user.id,
"username": user.username,
"full_name": user.full_name,
"avatar_url": user.avatar_url
})
# Get likes and comments
likes = []
for like in post.likes:
user = db.query(User).filter(User.id == like.user_id).first()
if user:
likes.append({
"id": like.id,
"user_id": user.id,
"username": user.username,
"full_name": user.full_name,
"avatar_url": user.avatar_url,
"created_at": like.created_at
})
comments = []
for comment in post.comments:
user = db.query(User).filter(User.id == comment.author_id).first()
if user:
comments.append({
"id": comment.id,
"content": comment.content,
"author_id": user.id,
"author_name": user.full_name,
"author_avatar": user.avatar_url,
"created_at": comment.created_at
})
return {
"id": post.id,
"author_id": post.author_id,
"content": post.content,
"image_url": post.image_url,
"likes_count": post.likes_count,
"comments_count": post.comments_count,
"created_at": post.created_at,
"updated_at": post.updated_at,
"author_name": post.author.full_name,
"author_avatar": post.author.avatar_url,
"mentioned_users": mentioned_users,
"likes": likes,
"comments": comments
}

View File

@@ -0,0 +1,287 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List, Dict, Any
from datetime import datetime
from config.database import get_db
from models.settings import SystemSettings
from models.user import User
from schemas.settings import (
SystemSettingCreate,
SystemSettingUpdate,
SystemSettingResponse,
SettingsCategoryResponse,
UploadLimitsResponse
)
from utils.security import get_admin_user
router = APIRouter()
@router.get("/", response_model=List[SystemSettingResponse])
async def get_all_settings(
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user)
):
"""Get all system settings (admin only)."""
settings = db.query(SystemSettings).order_by(SystemSettings.category, SystemSettings.key).all()
return settings
@router.get("/category/{category}", response_model=SettingsCategoryResponse)
async def get_settings_by_category(
category: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user)
):
"""Get settings by category (admin only)."""
settings = db.query(SystemSettings).filter(SystemSettings.category == category).all()
return SettingsCategoryResponse(category=category, settings=settings)
@router.get("/upload-limits", response_model=UploadLimitsResponse)
async def get_upload_limits(
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user)
):
"""Get current upload limits configuration."""
settings = db.query(SystemSettings).filter(
SystemSettings.category == "uploads"
).all()
# Convertir en dictionnaire pour faciliter l'accès
settings_dict = {s.key: s.value for s in settings}
# Debug: afficher les valeurs récupérées
print(f"DEBUG - Upload limits from DB: {settings_dict}")
return UploadLimitsResponse(
max_album_size_mb=int(settings_dict.get("max_album_size_mb", "100")),
max_vlog_size_mb=int(settings_dict.get("max_vlog_size_mb", "500")),
max_image_size_mb=int(settings_dict.get("max_image_size_mb", "10")),
max_video_size_mb=int(settings_dict.get("max_video_size_mb", "100")),
max_media_per_album=int(settings_dict.get("max_media_per_album", "50")),
allowed_image_types=settings_dict.get("allowed_image_types", "image/jpeg,image/png,image/gif,image/webp").split(","),
allowed_video_types=settings_dict.get("allowed_video_types", "video/mp4,video/mpeg,video/quicktime,video/webm").split(",")
)
@router.post("/", response_model=SystemSettingResponse)
async def create_setting(
setting_data: SystemSettingCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user)
):
"""Create a new system setting (admin only)."""
# Vérifier si la clé existe déjà
existing = db.query(SystemSettings).filter(SystemSettings.key == setting_data.key).first()
if existing:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Setting key already exists"
)
setting = SystemSettings(**setting_data.dict())
db.add(setting)
db.commit()
db.refresh(setting)
return setting
@router.put("/{setting_key}", response_model=SystemSettingResponse)
async def update_setting(
setting_key: str,
setting_update: SystemSettingUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user)
):
"""Update a system setting (admin only)."""
setting = db.query(SystemSettings).filter(SystemSettings.key == setting_key).first()
if not setting:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Setting not found"
)
if not setting.is_editable:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="This setting cannot be modified"
)
# Validation des valeurs selon le type de paramètre
if setting_key.startswith("max_") and setting_key.endswith("_mb"):
try:
size_mb = int(setting_update.value)
if size_mb <= 0 or size_mb > 10000: # Max 10GB
raise ValueError("Size must be between 1 and 10000 MB")
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid size value: {str(e)}"
)
setting.value = setting_update.value
setting.updated_at = datetime.utcnow()
db.commit()
db.refresh(setting)
return setting
@router.delete("/{setting_key}")
async def delete_setting(
setting_key: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user)
):
"""Delete a system setting (admin only)."""
setting = db.query(SystemSettings).filter(SystemSettings.key == setting_key).first()
if not setting:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Setting not found"
)
if not setting.is_editable:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="This setting cannot be deleted"
)
db.delete(setting)
db.commit()
return {"message": "Setting deleted successfully"}
@router.get("/test-upload-limits")
async def test_upload_limits(
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user)
):
"""Test endpoint to verify upload limits are working correctly."""
from utils.settings_service import SettingsService
# Test image upload limit
image_limit = SettingsService.get_max_upload_size(db, "image/jpeg")
image_limit_mb = image_limit // (1024 * 1024)
# Test video upload limit
video_limit = SettingsService.get_max_upload_size(db, "video/mp4")
video_limit_mb = video_limit // (1024 * 1024)
# Test file type validation
image_allowed = SettingsService.is_file_type_allowed(db, "image/jpeg")
video_allowed = SettingsService.is_file_type_allowed(db, "video/mp4")
invalid_allowed = SettingsService.is_file_type_allowed(db, "application/pdf")
return {
"image_upload_limit": {
"bytes": image_limit,
"mb": image_limit_mb,
"content_type": "image/jpeg"
},
"video_upload_limit": {
"bytes": video_limit,
"mb": video_limit_mb,
"content_type": "video/mp4"
},
"file_type_validation": {
"image_jpeg_allowed": image_allowed,
"video_mp4_allowed": video_allowed,
"pdf_not_allowed": not invalid_allowed
},
"message": "Upload limits test completed"
}
@router.get("/public/registration-status")
async def get_public_registration_status(
db: Session = Depends(get_db)
):
"""Get registration status without authentication (public endpoint)."""
from utils.settings_service import SettingsService
try:
enable_registration = SettingsService.get_setting(db, "enable_registration", True)
max_users = SettingsService.get_setting(db, "max_users", 50)
current_users_count = db.query(User).count()
return {
"registration_enabled": enable_registration,
"max_users": max_users,
"current_users_count": current_users_count,
"can_register": enable_registration and current_users_count < max_users
}
except Exception as e:
# En cas d'erreur, on retourne des valeurs par défaut sécurisées
return {
"registration_enabled": False,
"max_users": 50,
"current_users_count": 0,
"can_register": False
}
@router.post("/initialize-defaults")
async def initialize_default_settings(
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user)
):
"""Initialize default system settings (admin only)."""
default_settings = [
{
"key": "max_album_size_mb",
"value": "100",
"description": "Taille maximale des albums en MB",
"category": "uploads"
},
{
"key": "max_vlog_size_mb",
"value": "500",
"description": "Taille maximale des vlogs en MB",
"category": "uploads"
},
{
"key": "max_image_size_mb",
"value": "10",
"description": "Taille maximale des images en MB",
"category": "uploads"
},
{
"key": "max_video_size_mb",
"value": "100",
"description": "Taille maximale des vidéos en MB",
"category": "uploads"
},
{
"key": "max_media_per_album",
"value": "50",
"description": "Nombre maximum de médias par album",
"category": "uploads"
},
{
"key": "allowed_image_types",
"value": "image/jpeg,image/png,image/gif,image/webp",
"description": "Types d'images autorisés (séparés par des virgules)",
"category": "uploads"
},
{
"key": "allowed_video_types",
"value": "video/mp4,video/mpeg,video/quicktime,video/webm",
"description": "Types de vidéos autorisés (séparés par des virgules)",
"category": "uploads"
},
{
"key": "max_users",
"value": "50",
"description": "Nombre maximum d'utilisateurs",
"category": "general"
},
{
"key": "enable_registration",
"value": "true",
"description": "Autoriser les nouvelles inscriptions",
"category": "general"
}
]
created_count = 0
for setting_data in default_settings:
existing = db.query(SystemSettings).filter(SystemSettings.key == setting_data["key"]).first()
if not existing:
setting = SystemSettings(**setting_data)
db.add(setting)
created_count += 1
db.commit()
return {"message": f"{created_count} default settings created"}

View File

@@ -0,0 +1,314 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from sqlalchemy import func
from datetime import datetime, timedelta
from config.database import get_db
from models.user import User
from models.event import Event, EventParticipation, ParticipationStatus
from models.album import Album, Media
from models.post import Post
from models.vlog import Vlog
from utils.security import get_current_active_user
router = APIRouter()
@router.get("/overview")
async def get_overview_stats(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Get general overview statistics."""
total_users = db.query(User).filter(User.is_active == True).count()
total_events = db.query(Event).count()
total_albums = db.query(Album).count()
total_posts = db.query(Post).count()
total_vlogs = db.query(Vlog).count()
total_media = db.query(Media).count()
# Recent activity (last 30 days)
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
recent_events = db.query(Event).filter(Event.created_at >= thirty_days_ago).count()
recent_posts = db.query(Post).filter(Post.created_at >= thirty_days_ago).count()
recent_vlogs = db.query(Vlog).filter(Vlog.created_at >= thirty_days_ago).count()
return {
"total_users": total_users,
"total_events": total_events,
"total_albums": total_albums,
"total_posts": total_posts,
"total_vlogs": total_vlogs,
"total_media": total_media,
"recent_events": recent_events,
"recent_posts": recent_posts,
"recent_vlogs": recent_vlogs
}
@router.get("/attendance")
async def get_attendance_stats(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Get attendance statistics for all users."""
users = db.query(User).filter(User.is_active == True).all()
attendance_stats = []
for user in users:
# Get past events participation
past_participations = db.query(EventParticipation).join(Event).filter(
EventParticipation.user_id == user.id,
Event.date < datetime.utcnow()
).all()
total_past_events = len(past_participations)
present_count = sum(1 for p in past_participations if p.status == ParticipationStatus.PRESENT)
absent_count = sum(1 for p in past_participations if p.status == ParticipationStatus.ABSENT)
# Get upcoming events participation
upcoming_participations = db.query(EventParticipation).join(Event).filter(
EventParticipation.user_id == user.id,
Event.date >= datetime.utcnow()
).all()
upcoming_present = sum(1 for p in upcoming_participations if p.status == ParticipationStatus.PRESENT)
upcoming_maybe = sum(1 for p in upcoming_participations if p.status == ParticipationStatus.MAYBE)
upcoming_pending = sum(1 for p in upcoming_participations if p.status == ParticipationStatus.PENDING)
attendance_stats.append({
"user_id": user.id,
"username": user.username,
"full_name": user.full_name,
"avatar_url": user.avatar_url,
"attendance_rate": user.attendance_rate,
"total_past_events": total_past_events,
"present_count": present_count,
"absent_count": absent_count,
"upcoming_present": upcoming_present,
"upcoming_maybe": upcoming_maybe,
"upcoming_pending": upcoming_pending
})
# Sort by attendance rate
attendance_stats.sort(key=lambda x: x["attendance_rate"], reverse=True)
return {
"attendance_stats": attendance_stats,
"best_attendee": attendance_stats[0] if attendance_stats else None,
"worst_attendee": attendance_stats[-1] if attendance_stats else None
}
@router.get("/fun")
async def get_fun_stats(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Get fun statistics about the group."""
# Most active poster
post_counts = db.query(
User.id,
User.username,
User.full_name,
func.count(Post.id).label("post_count")
).join(Post).group_by(User.id).order_by(func.count(Post.id).desc()).first()
# Most mentioned user
mention_counts = db.query(
User.id,
User.username,
User.full_name,
func.count().label("mention_count")
).join(Post.mentions).group_by(User.id).order_by(func.count().desc()).first()
# Biggest vlogger
vlog_counts = db.query(
User.id,
User.username,
User.full_name,
func.count(Vlog.id).label("vlog_count")
).join(Vlog).group_by(User.id).order_by(func.count(Vlog.id).desc()).first()
# Photo addict (most albums created)
album_counts = db.query(
User.id,
User.username,
User.full_name,
func.count(Album.id).label("album_count")
).join(Album).group_by(User.id).order_by(func.count(Album.id).desc()).first()
# Event organizer (most events created)
event_counts = db.query(
User.id,
User.username,
User.full_name,
func.count(Event.id).label("event_count")
).join(Event, Event.creator_id == User.id).group_by(User.id).order_by(func.count(Event.id).desc()).first()
# Most viewed vlog
most_viewed_vlog = db.query(Vlog).order_by(Vlog.views_count.desc()).first()
# Longest event streak (consecutive events attended)
# This would require more complex logic to calculate
return {
"most_active_poster": {
"user_id": post_counts[0] if post_counts else None,
"username": post_counts[1] if post_counts else None,
"full_name": post_counts[2] if post_counts else None,
"post_count": post_counts[3] if post_counts else 0
} if post_counts else None,
"most_mentioned": {
"user_id": mention_counts[0] if mention_counts else None,
"username": mention_counts[1] if mention_counts else None,
"full_name": mention_counts[2] if mention_counts else None,
"mention_count": mention_counts[3] if mention_counts else 0
} if mention_counts else None,
"biggest_vlogger": {
"user_id": vlog_counts[0] if vlog_counts else None,
"username": vlog_counts[1] if vlog_counts else None,
"full_name": vlog_counts[2] if vlog_counts else None,
"vlog_count": vlog_counts[3] if vlog_counts else 0
} if vlog_counts else None,
"photo_addict": {
"user_id": album_counts[0] if album_counts else None,
"username": album_counts[1] if album_counts else None,
"full_name": album_counts[2] if album_counts else None,
"album_count": album_counts[3] if album_counts else 0
} if album_counts else None,
"event_organizer": {
"user_id": event_counts[0] if event_counts else None,
"username": event_counts[1] if event_counts else None,
"full_name": event_counts[2] if event_counts else None,
"event_count": event_counts[3] if event_counts else 0
} if event_counts else None,
"most_viewed_vlog": {
"id": most_viewed_vlog.id,
"title": most_viewed_vlog.title,
"author_name": most_viewed_vlog.author.full_name,
"views_count": most_viewed_vlog.views_count
} if most_viewed_vlog else None
}
@router.get("/user/{user_id}")
async def get_user_stats(
user_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Get statistics for a specific user."""
user = db.query(User).filter(User.id == user_id).first()
if not user:
return {"error": "User not found"}
posts_count = db.query(Post).filter(Post.author_id == user_id).count()
vlogs_count = db.query(Vlog).filter(Vlog.author_id == user_id).count()
albums_count = db.query(Album).filter(Album.creator_id == user_id).count()
events_created = db.query(Event).filter(Event.creator_id == user_id).count()
# Get participation stats
participations = db.query(EventParticipation).filter(EventParticipation.user_id == user_id).all()
present_count = sum(1 for p in participations if p.status == ParticipationStatus.PRESENT)
absent_count = sum(1 for p in participations if p.status == ParticipationStatus.ABSENT)
maybe_count = sum(1 for p in participations if p.status == ParticipationStatus.MAYBE)
# Total views on vlogs
total_vlog_views = db.query(func.sum(Vlog.views_count)).filter(Vlog.author_id == user_id).scalar() or 0
return {
"user": {
"id": user.id,
"username": user.username,
"full_name": user.full_name,
"avatar_url": user.avatar_url,
"attendance_rate": user.attendance_rate,
"member_since": user.created_at
},
"content_stats": {
"posts_count": posts_count,
"vlogs_count": vlogs_count,
"albums_count": albums_count,
"events_created": events_created,
"total_vlog_views": total_vlog_views
},
"participation_stats": {
"total_events": len(participations),
"present_count": present_count,
"absent_count": absent_count,
"maybe_count": maybe_count
}
}
@router.get("/activity/user/{user_id}")
async def get_user_activity(
user_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Get recent activity for a specific user."""
user = db.query(User).filter(User.id == user_id).first()
if not user:
return {"error": "User not found"}
# Get recent posts
recent_posts = db.query(Post).filter(
Post.author_id == user_id
).order_by(Post.created_at.desc()).limit(5).all()
# Get recent vlogs
recent_vlogs = db.query(Vlog).filter(
Vlog.author_id == user_id
).order_by(Vlog.created_at.desc()).limit(5).all()
# Get recent albums
recent_albums = db.query(Album).filter(
Album.creator_id == user_id
).order_by(Album.created_at.desc()).limit(5).all()
# Get recent events created
recent_events = db.query(Event).filter(
Event.creator_id == user_id
).order_by(Event.created_at.desc()).limit(5).all()
# Combine and sort by date
activities = []
for post in recent_posts:
activities.append({
"id": post.id,
"type": "post",
"description": f"A publié : {post.content[:50]}...",
"created_at": post.created_at,
"link": f"/posts/{post.id}"
})
for vlog in recent_vlogs:
activities.append({
"id": vlog.id,
"type": "vlog",
"description": f"A publié un vlog : {vlog.title}",
"created_at": vlog.created_at,
"link": f"/vlogs/{vlog.id}"
})
for album in recent_albums:
activities.append({
"id": album.id,
"type": "album",
"description": f"A créé un album : {album.title}",
"created_at": album.created_at,
"link": f"/albums/{album.id}"
})
for event in recent_events:
activities.append({
"id": event.id,
"type": "event",
"description": f"A créé un événement : {event.title}",
"created_at": event.created_at,
"link": f"/events/{event.id}"
})
# Sort by creation date (most recent first)
activities.sort(key=lambda x: x["created_at"], reverse=True)
return {
"activity": activities[:10] # Return top 10 most recent activities
}

View File

@@ -0,0 +1,295 @@
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form
from sqlalchemy.orm import Session
from typing import List, Optional
from datetime import datetime
import os
import uuid
from pathlib import Path
from PIL import Image
from config.database import get_db
from config.settings import settings
from models.ticket import Ticket, TicketType, TicketStatus, TicketPriority
from models.user import User
from schemas.ticket import TicketCreate, TicketUpdate, TicketResponse, TicketAdminUpdate
from utils.security import get_current_active_user, get_admin_user
router = APIRouter()
@router.get("/", response_model=List[TicketResponse])
async def get_user_tickets(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Get current user's tickets."""
tickets = db.query(Ticket).filter(Ticket.user_id == current_user.id).order_by(Ticket.created_at.desc()).all()
return [format_ticket_response(ticket, db) for ticket in tickets]
@router.get("/admin", response_model=List[TicketResponse])
async def get_all_tickets_admin(
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user),
status_filter: Optional[TicketStatus] = None,
type_filter: Optional[TicketType] = None,
priority_filter: Optional[TicketPriority] = None
):
"""Get all tickets (admin only)."""
query = db.query(Ticket)
if status_filter:
query = query.filter(Ticket.status == status_filter)
if type_filter:
query = query.filter(Ticket.ticket_type == type_filter)
if priority_filter:
query = query.filter(Ticket.priority == priority_filter)
tickets = query.order_by(Ticket.created_at.desc()).all()
return [format_ticket_response(ticket, db) for ticket in tickets]
@router.get("/{ticket_id}", response_model=TicketResponse)
async def get_ticket(
ticket_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Get a specific ticket."""
ticket = db.query(Ticket).filter(Ticket.id == ticket_id).first()
if not ticket:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Ticket not found"
)
# Users can only see their own tickets, admins can see all
if ticket.user_id != current_user.id and not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to view this ticket"
)
return format_ticket_response(ticket, db)
@router.post("/", response_model=TicketResponse)
async def create_ticket(
title: str = Form(...),
description: str = Form(...),
ticket_type: str = Form("other"), # Changed from TicketType to str
priority: str = Form("medium"), # Changed from TicketPriority to str
screenshot: Optional[UploadFile] = File(None),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Create a new ticket."""
print(f"DEBUG - Received ticket data: title='{title}', description='{description}', ticket_type='{ticket_type}', priority='{priority}'")
# Validate and convert ticket_type
try:
ticket_type_enum = TicketType(ticket_type)
print(f"DEBUG - Ticket type converted successfully: {ticket_type_enum}")
except ValueError as e:
print(f"DEBUG - Error converting ticket_type '{ticket_type}': {e}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid ticket type: {ticket_type}. Valid types: {[t.value for t in TicketType]}"
)
# Validate and convert priority
try:
priority_enum = TicketPriority(priority)
print(f"DEBUG - Priority converted successfully: {priority_enum}")
except ValueError as e:
print(f"DEBUG - Error converting priority '{priority}': {e}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid priority: {priority}. Valid priorities: {[p.value for p in TicketPriority]}"
)
print("DEBUG - Starting ticket creation...")
# Create tickets directory
tickets_dir = Path(settings.UPLOAD_PATH) / "tickets" / str(current_user.id)
tickets_dir.mkdir(parents=True, exist_ok=True)
print(f"DEBUG - Created tickets directory: {tickets_dir}")
screenshot_path = None
if screenshot:
print(f"DEBUG - Processing screenshot: {screenshot.filename}, content_type: {screenshot.content_type}")
# Validate file type
if not screenshot.content_type.startswith('image/'):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Screenshot must be an image file"
)
# Generate unique filename
file_extension = screenshot.filename.split(".")[-1]
filename = f"ticket_{uuid.uuid4()}.{file_extension}"
file_path = tickets_dir / filename
# Save and optimize image
contents = await screenshot.read()
with open(file_path, "wb") as f:
f.write(contents)
# Optimize image
try:
img = Image.open(file_path)
if img.size[0] > 1920 or img.size[1] > 1080:
img.thumbnail((1920, 1080), Image.Resampling.LANCZOS)
img.save(file_path, quality=85, optimize=True)
except Exception as e:
print(f"Error optimizing screenshot: {e}")
screenshot_path = f"/tickets/{current_user.id}/{filename}"
print(f"DEBUG - Screenshot saved: {screenshot_path}")
print("DEBUG - Creating ticket object...")
ticket = Ticket(
title=title,
description=description,
ticket_type=ticket_type_enum,
priority=priority_enum,
user_id=current_user.id,
screenshot_path=screenshot_path
)
print("DEBUG - Adding ticket to database...")
db.add(ticket)
db.commit()
db.refresh(ticket)
print(f"DEBUG - Ticket created successfully with ID: {ticket.id}")
return format_ticket_response(ticket, db)
@router.put("/{ticket_id}", response_model=TicketResponse)
async def update_ticket(
ticket_id: int,
ticket_update: TicketUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Update a ticket."""
ticket = db.query(Ticket).filter(Ticket.id == ticket_id).first()
if not ticket:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Ticket not found"
)
# Users can only update their own tickets, admins can update all
if ticket.user_id != current_user.id and not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to update this ticket"
)
# Regular users can only update title, description, and type
if not current_user.is_admin:
update_data = ticket_update.dict(exclude_unset=True)
allowed_fields = ['title', 'description', 'ticket_type']
update_data = {k: v for k, v in update_data.items() if k in allowed_fields}
else:
update_data = ticket_update.dict(exclude_unset=True)
# Update resolved_at if status is resolved
if 'status' in update_data and update_data['status'] == TicketStatus.RESOLVED:
update_data['resolved_at'] = datetime.utcnow()
for field, value in update_data.items():
setattr(ticket, field, value)
ticket.updated_at = datetime.utcnow()
db.commit()
db.refresh(ticket)
return format_ticket_response(ticket, db)
@router.put("/{ticket_id}/admin", response_model=TicketResponse)
async def update_ticket_admin(
ticket_id: int,
ticket_update: TicketAdminUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user)
):
"""Update a ticket as admin."""
ticket = db.query(Ticket).filter(Ticket.id == ticket_id).first()
if not ticket:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Ticket not found"
)
update_data = ticket_update.dict(exclude_unset=True)
# Update resolved_at if status is resolved
if 'status' in update_data and update_data['status'] == TicketStatus.RESOLVED:
update_data['resolved_at'] = datetime.utcnow()
for field, value in update_data.items():
setattr(ticket, field, value)
ticket.updated_at = datetime.utcnow()
db.commit()
db.refresh(ticket)
return format_ticket_response(ticket, db)
@router.delete("/{ticket_id}")
async def delete_ticket(
ticket_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Delete a ticket."""
ticket = db.query(Ticket).filter(Ticket.id == ticket_id).first()
if not ticket:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Ticket not found"
)
# Users can only delete their own tickets, admins can delete all
if ticket.user_id != current_user.id and not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to delete this ticket"
)
# Delete screenshot if exists
if ticket.screenshot_path:
try:
screenshot_file = settings.UPLOAD_PATH + ticket.screenshot_path
if os.path.exists(screenshot_file):
os.remove(screenshot_file)
except Exception as e:
print(f"Error deleting screenshot: {e}")
db.delete(ticket)
db.commit()
return {"message": "Ticket deleted successfully"}
def format_ticket_response(ticket: Ticket, db: Session) -> dict:
"""Format ticket response with user information."""
user = db.query(User).filter(User.id == ticket.user_id).first()
assigned_admin = None
if ticket.assigned_to:
assigned_admin = db.query(User).filter(User.id == ticket.assigned_to).first()
return {
"id": ticket.id,
"title": ticket.title,
"description": ticket.description,
"ticket_type": ticket.ticket_type,
"status": ticket.status,
"priority": ticket.priority,
"user_id": ticket.user_id,
"assigned_to": ticket.assigned_to,
"screenshot_path": ticket.screenshot_path,
"admin_notes": ticket.admin_notes,
"created_at": ticket.created_at,
"updated_at": ticket.updated_at,
"resolved_at": ticket.resolved_at,
"user_name": user.full_name if user else "Unknown",
"user_email": user.email if user else "Unknown",
"assigned_admin_name": assigned_admin.full_name if assigned_admin else None
}

View File

@@ -0,0 +1,111 @@
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
from sqlalchemy.orm import Session
from typing import List
import os
import uuid
from PIL import Image
from config.database import get_db
from config.settings import settings
from models.user import User
from schemas.user import UserResponse, UserUpdate
from utils.security import get_current_active_user
from utils.settings_service import SettingsService
from pathlib import Path
router = APIRouter()
@router.get("/me", response_model=UserResponse)
async def get_current_user_info(current_user: User = Depends(get_current_active_user)):
"""Get current user information."""
return current_user
@router.get("/", response_model=List[UserResponse])
async def get_all_users(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Get all users."""
users = db.query(User).filter(User.is_active == True).all()
return users
@router.get("/{user_id}", response_model=UserResponse)
async def get_user(
user_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Get a specific user by ID."""
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return user
@router.put("/me", response_model=UserResponse)
async def update_current_user(
user_update: UserUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Update current user information."""
if user_update.full_name:
current_user.full_name = user_update.full_name
if user_update.bio is not None:
current_user.bio = user_update.bio
if user_update.avatar_url is not None:
current_user.avatar_url = user_update.avatar_url
db.commit()
db.refresh(current_user)
return current_user
@router.post("/me/avatar", response_model=UserResponse)
async def upload_avatar(
file: UploadFile = File(...),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Upload user avatar."""
# Validate file type
if not SettingsService.is_file_type_allowed(db, file.content_type or "unknown"):
allowed_types = SettingsService.get_setting(db, "allowed_image_types",
["image/jpeg", "image/png", "image/gif", "image/webp"])
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid file type. Allowed types: {', '.join(allowed_types)}"
)
# Create user directory
user_dir = Path(settings.UPLOAD_PATH) / "avatars" / str(current_user.id)
user_dir.mkdir(parents=True, exist_ok=True)
# Generate unique filename
file_extension = file.filename.split(".")[-1]
filename = f"{uuid.uuid4()}.{file_extension}"
file_path = user_dir / filename
# Save and resize image
contents = await file.read()
with open(file_path, "wb") as f:
f.write(contents)
# Resize image to 800x800 with high quality
try:
img = Image.open(file_path)
img.thumbnail((800, 800))
img.save(file_path, quality=95, optimize=True)
except Exception as e:
os.remove(file_path)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error processing image"
)
# Update user avatar URL
current_user.avatar_url = f"/avatars/{current_user.id}/{filename}"
db.commit()
db.refresh(current_user)
return current_user

View File

@@ -0,0 +1,374 @@
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form
from sqlalchemy.orm import Session
from typing import List
import os
import uuid
from pathlib import Path
from config.database import get_db
from config.settings import settings
from models.vlog import Vlog, VlogLike, VlogComment
from models.user import User
from schemas.vlog import VlogCreate, VlogUpdate, VlogResponse, VlogCommentCreate
from utils.security import get_current_active_user
from utils.video_utils import generate_video_thumbnail, get_video_duration
from utils.settings_service import SettingsService
router = APIRouter()
@router.post("/", response_model=VlogResponse)
async def create_vlog(
vlog_data: VlogCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Create a new vlog."""
vlog = Vlog(
author_id=current_user.id,
**vlog_data.dict()
)
db.add(vlog)
db.commit()
db.refresh(vlog)
return format_vlog_response(vlog, db, current_user.id)
@router.get("/", response_model=List[VlogResponse])
async def get_vlogs(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user),
limit: int = 20,
offset: int = 0
):
"""Get all vlogs."""
vlogs = db.query(Vlog).order_by(Vlog.created_at.desc()).limit(limit).offset(offset).all()
return [format_vlog_response(vlog, db, current_user.id) for vlog in vlogs]
@router.get("/{vlog_id}", response_model=VlogResponse)
async def get_vlog(
vlog_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Get a specific vlog."""
vlog = db.query(Vlog).filter(Vlog.id == vlog_id).first()
if not vlog:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Vlog not found"
)
# Increment view count
vlog.views_count += 1
db.commit()
return format_vlog_response(vlog, db, current_user.id)
@router.put("/{vlog_id}", response_model=VlogResponse)
async def update_vlog(
vlog_id: int,
vlog_update: VlogUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Update a vlog."""
vlog = db.query(Vlog).filter(Vlog.id == vlog_id).first()
if not vlog:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Vlog not found"
)
if vlog.author_id != current_user.id and not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to update this vlog"
)
for field, value in vlog_update.dict(exclude_unset=True).items():
setattr(vlog, field, value)
db.commit()
db.refresh(vlog)
return format_vlog_response(vlog, db, current_user.id)
@router.delete("/{vlog_id}")
async def delete_vlog(
vlog_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Delete a vlog."""
vlog = db.query(Vlog).filter(Vlog.id == vlog_id).first()
if not vlog:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Vlog not found"
)
if vlog.author_id != current_user.id and not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to delete this vlog"
)
# Delete video files
try:
if vlog.video_url:
os.remove(settings.UPLOAD_PATH + vlog.video_url)
if vlog.thumbnail_url:
os.remove(settings.UPLOAD_PATH + vlog.thumbnail_url)
except:
pass
db.delete(vlog)
db.commit()
return {"message": "Vlog deleted successfully"}
@router.post("/{vlog_id}/like")
async def toggle_vlog_like(
vlog_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Toggle like on a vlog."""
vlog = db.query(Vlog).filter(Vlog.id == vlog_id).first()
if not vlog:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Vlog not found"
)
existing_like = db.query(VlogLike).filter(
VlogLike.vlog_id == vlog_id,
VlogLike.user_id == current_user.id
).first()
if existing_like:
# Unlike
db.delete(existing_like)
vlog.likes_count = max(0, vlog.likes_count - 1)
message = "Like removed"
else:
# Like
like = VlogLike(vlog_id=vlog_id, user_id=current_user.id)
db.add(like)
vlog.likes_count += 1
message = "Vlog liked"
db.commit()
return {"message": message, "likes_count": vlog.likes_count}
@router.post("/{vlog_id}/comment")
async def add_vlog_comment(
vlog_id: int,
comment_data: VlogCommentCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Add a comment to a vlog."""
vlog = db.query(Vlog).filter(Vlog.id == vlog_id).first()
if not vlog:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Vlog not found"
)
comment = VlogComment(
vlog_id=vlog_id,
user_id=current_user.id,
content=comment_data.content
)
db.add(comment)
db.commit()
db.refresh(comment)
return {
"message": "Comment added successfully",
"comment": {
"id": comment.id,
"content": comment.content,
"user_id": comment.user_id,
"username": current_user.username,
"full_name": current_user.full_name,
"avatar_url": current_user.avatar_url,
"created_at": comment.created_at
}
}
@router.delete("/{vlog_id}/comment/{comment_id}")
async def delete_vlog_comment(
vlog_id: int,
comment_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Delete a comment from a vlog."""
comment = db.query(VlogComment).filter(
VlogComment.id == comment_id,
VlogComment.vlog_id == vlog_id
).first()
if not comment:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Comment not found"
)
if comment.user_id != current_user.id and not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to delete this comment"
)
db.delete(comment)
db.commit()
return {"message": "Comment deleted successfully"}
@router.post("/upload")
async def upload_vlog_video(
title: str = Form(...),
description: str = Form(None),
video: UploadFile = File(...),
thumbnail: UploadFile = File(None),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Upload a vlog video."""
# Validate video file
if not SettingsService.is_file_type_allowed(db, video.content_type or "unknown"):
allowed_types = SettingsService.get_setting(db, "allowed_video_types",
["video/mp4", "video/mpeg", "video/quicktime", "video/webm"])
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid video type. Allowed types: {', '.join(allowed_types)}"
)
# Check file size
video_content = await video.read()
max_size = SettingsService.get_max_upload_size(db, video.content_type or "video/mp4")
if len(video_content) > max_size:
max_size_mb = max_size // (1024 * 1024)
raise HTTPException(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
detail=f"Video file too large. Maximum size: {max_size_mb}MB"
)
# Create vlog directory
vlog_dir = Path(settings.UPLOAD_PATH) / "vlogs" / str(current_user.id)
vlog_dir.mkdir(parents=True, exist_ok=True)
# Save video
video_extension = video.filename.split(".")[-1]
video_filename = f"{uuid.uuid4()}.{video_extension}"
video_path = vlog_dir / video_filename
with open(video_path, "wb") as f:
f.write(video_content)
# Process thumbnail
thumbnail_url = None
if thumbnail:
if thumbnail.content_type not in settings.ALLOWED_IMAGE_TYPES:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid thumbnail type"
)
thumbnail_content = await thumbnail.read()
thumbnail_extension = thumbnail.filename.split(".")[-1]
thumbnail_filename = f"thumb_{uuid.uuid4()}.{thumbnail_extension}"
thumbnail_path = vlog_dir / thumbnail_filename
with open(thumbnail_path, "wb") as f:
f.write(thumbnail_content)
thumbnail_url = f"/vlogs/{current_user.id}/{thumbnail_filename}"
else:
# Generate automatic thumbnail from video
try:
thumbnail_filename = f"auto_thumb_{uuid.uuid4()}.jpg"
thumbnail_path = vlog_dir / thumbnail_filename
if generate_video_thumbnail(str(video_path), str(thumbnail_path)):
thumbnail_url = f"/vlogs/{current_user.id}/{thumbnail_filename}"
except Exception as e:
print(f"Error generating automatic thumbnail: {e}")
# Continue without thumbnail if generation fails
# Get video duration
duration = None
try:
duration = int(get_video_duration(str(video_path)))
except Exception as e:
print(f"Error getting video duration: {e}")
# Create vlog record
vlog = Vlog(
author_id=current_user.id,
title=title,
description=description,
video_url=f"/vlogs/{current_user.id}/{video_filename}",
thumbnail_url=thumbnail_url,
duration=duration
)
db.add(vlog)
db.commit()
db.refresh(vlog)
return format_vlog_response(vlog, db, current_user.id)
def format_vlog_response(vlog: Vlog, db: Session, current_user_id: int) -> dict:
"""Format vlog response with author information, likes, and comments."""
# Check if current user liked this vlog
is_liked = db.query(VlogLike).filter(
VlogLike.vlog_id == vlog.id,
VlogLike.user_id == current_user_id
).first() is not None
# Format likes
likes = []
for like in vlog.likes:
user = db.query(User).filter(User.id == like.user_id).first()
if user:
likes.append({
"id": like.id,
"user_id": user.id,
"username": user.username,
"full_name": user.full_name,
"avatar_url": user.avatar_url,
"created_at": like.created_at
})
# Format comments
comments = []
for comment in vlog.comments:
user = db.query(User).filter(User.id == comment.user_id).first()
if user:
comments.append({
"id": comment.id,
"user_id": user.id,
"username": user.username,
"full_name": user.full_name,
"avatar_url": user.avatar_url,
"content": comment.content,
"created_at": comment.created_at,
"updated_at": comment.updated_at
})
return {
"id": vlog.id,
"author_id": vlog.author_id,
"title": vlog.title,
"description": vlog.description,
"video_url": vlog.video_url,
"thumbnail_url": vlog.thumbnail_url,
"duration": vlog.duration,
"views_count": vlog.views_count,
"likes_count": vlog.likes_count,
"created_at": vlog.created_at,
"updated_at": vlog.updated_at,
"author_name": vlog.author.full_name,
"author_avatar": vlog.author.avatar_url,
"is_liked": is_liked,
"likes": likes,
"comments": comments
}