diff --git a/backend/MIGRATIONS.md b/backend/MIGRATIONS.md new file mode 100644 index 0000000..4786591 --- /dev/null +++ b/backend/MIGRATIONS.md @@ -0,0 +1,137 @@ +# Guide des Migrations Alembic + +Ce projet utilise Alembic pour gérer les migrations de base de données de manière versionnée et contrôlée. + +## 🚀 Démarrage rapide + +### Première utilisation + +Si c'est la première fois que vous utilisez Alembic sur ce projet : + +```bash +# Depuis le conteneur backend +docker compose exec backend alembic revision --autogenerate -m "Initial migration" + +# Appliquer la migration +docker compose exec backend alembic upgrade head +``` + +### Commandes courantes + +```bash +# Créer une nouvelle migration (autogenerate depuis les modèles) +docker compose exec backend alembic revision --autogenerate -m "Description de la migration" + +# Appliquer toutes les migrations en attente +docker compose exec backend alembic upgrade head + +# Revenir en arrière d'une migration +docker compose exec backend alembic downgrade -1 + +# Voir la migration actuelle +docker compose exec backend alembic current + +# Voir l'historique des migrations +docker compose exec backend alembic history +``` + +## 📋 Workflow de développement + +### 1. Modifier un modèle + +Éditez les fichiers dans `backend/models/` pour ajouter/modifier des colonnes, tables, etc. + +### 2. Créer une migration + +```bash +docker compose exec backend alembic revision --autogenerate -m "Ajout du champ X à la table Y" +``` + +Alembic va automatiquement détecter les changements dans vos modèles SQLAlchemy. + +### 3. Vérifier la migration + +Ouvrez le fichier généré dans `backend/migrations/versions/` et vérifiez que les changements sont corrects. + +### 4. Appliquer la migration + +```bash +docker compose exec backend alembic upgrade head +``` + +### 5. Tester + +Vérifiez que votre application fonctionne correctement avec les nouvelles migrations. + +## ⚠️ Notes importantes + +- **Ne modifiez jamais manuellement** les fichiers de migration existants qui ont déjà été appliqués +- **Toujours tester** les migrations en développement avant de les appliquer en production +- **Sauvegardez votre base de données** avant d'appliquer des migrations en production +- Les migrations sont **versionnées** : chaque migration a un ID unique et un historique + +## 🔄 Migration manuelle (sans autogenerate) + +Si vous avez besoin de créer une migration manuelle (pour des données, des index complexes, etc.) : + +```bash +docker compose exec backend alembic revision -m "Description" +``` + +Puis éditez le fichier généré dans `backend/migrations/versions/` pour ajouter votre logique. + +## 🐛 Dépannage + +### Migration en conflit + +Si vous avez des conflits de migration : + +```bash +# Voir l'état actuel +docker compose exec backend alembic current + +# Voir l'historique +docker compose exec backend alembic history + +# Revenir à une version spécifique +docker compose exec backend alembic downgrade +``` + +### Migration qui échoue + +Si une migration échoue : + +1. Vérifiez les logs : `docker compose logs backend` +2. Vérifiez l'état de la base : `docker compose exec backend alembic current` +3. Si nécessaire, corrigez la migration et réessayez + +### Réinitialiser les migrations (⚠️ DANGEREUX) + +**ATTENTION** : Cela supprime toutes les données ! + +```bash +# Supprimer toutes les tables +docker compose exec backend alembic downgrade base + +# Recréer depuis le début +docker compose exec backend alembic upgrade head +``` + +## 📁 Structure des fichiers + +``` +backend/ +├── alembic.ini # Configuration Alembic +├── migrations/ +│ ├── env.py # Configuration de l'environnement +│ ├── script.py.mako # Template pour les migrations +│ ├── versions/ # Fichiers de migration (générés) +│ └── README.md # Documentation +└── models/ # Modèles SQLAlchemy +``` + +## 🔗 Ressources + +- [Documentation Alembic](https://alembic.sqlalchemy.org/) +- [SQLAlchemy Documentation](https://docs.sqlalchemy.org/) + diff --git a/backend/alembic.ini b/backend/alembic.ini new file mode 100644 index 0000000..7825555 --- /dev/null +++ b/backend/alembic.ini @@ -0,0 +1,115 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = migrations + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python-dateutil library that can be +# installed by adding `alembic[tz]` to the pip requirements +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to migrations/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = driver://user:pass@localhost/dbname + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# post_write_hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the exec runner, execute a binary +# post_write_hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = --fix REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S + diff --git a/backend/api/routers/events.py b/backend/api/routers/events.py index 9bb8707..6806333 100644 --- a/backend/api/routers/events.py +++ b/backend/api/routers/events.py @@ -19,40 +19,76 @@ async def create_event( current_user: User = Depends(get_current_active_user) ): """Create a new event.""" + event_dict = event_data.dict(exclude={'invited_user_ids'}) event = Event( - **event_data.dict(), + **event_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) + # Gérer les invitations selon le type d'événement + if event.is_private: + # Événement privé : inviter uniquement les utilisateurs sélectionnés + invited_user_ids = event_data.invited_user_ids or [] + # Toujours inclure le créateur + if current_user.id not in invited_user_ids: + invited_user_ids.append(current_user.id) - # Create notification - if user.id != current_user.id: - notification = Notification( + for user_id in invited_user_ids: + user = db.query(User).filter(User.id == user_id, User.is_active == True).first() + if user: + 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"Invitation à un événement privé: {event.title}", + message=f"{current_user.full_name} vous a invité à un événement privé", + link=f"/events/{event.id}" + ) + db.add(notification) + + # Send email notification + try: + send_event_notification(user.email, event) + except: + pass + else: + # Événement public : inviter tous les utilisateurs actifs + users = db.query(User).filter(User.is_active == True).all() + for user in users: + participation = EventParticipation( + event_id=event.id, 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}" + status=ParticipationStatus.PENDING ) - db.add(notification) + db.add(participation) - # Send email notification (async task would be better) - try: - send_event_notification(user.email, event) - except: - pass # Don't fail if email sending fails + # 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 + try: + send_event_notification(user.email, event) + except: + pass db.commit() return format_event_response(event, db) @@ -75,7 +111,23 @@ async def get_events( # 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] + + # Filtrer les événements privés : ne montrer que ceux où l'utilisateur est invité + filtered_events = [] + for event in events: + if event.is_private: + # Vérifier si l'utilisateur est invité (a une participation) + participation = db.query(EventParticipation).filter( + EventParticipation.event_id == event.id, + EventParticipation.user_id == current_user.id + ).first() + if participation or event.creator_id == current_user.id or current_user.is_admin: + filtered_events.append(event) + else: + # Événement public : visible par tous + filtered_events.append(event) + + return [format_event_response(event, db) for event in filtered_events] @router.get("/upcoming", response_model=List[EventResponse]) async def get_upcoming_events( @@ -86,7 +138,21 @@ async def get_upcoming_events( events = db.query(Event).options(joinedload(Event.creator)).filter( Event.date >= datetime.utcnow() ).order_by(Event.date).all() - return [format_event_response(event, db) for event in events] + + # Filtrer les événements privés + filtered_events = [] + for event in events: + if event.is_private: + participation = db.query(EventParticipation).filter( + EventParticipation.event_id == event.id, + EventParticipation.user_id == current_user.id + ).first() + if participation or event.creator_id == current_user.id or current_user.is_admin: + filtered_events.append(event) + else: + filtered_events.append(event) + + return [format_event_response(event, db) for event in filtered_events] @router.get("/past", response_model=List[EventResponse]) async def get_past_events( @@ -97,7 +163,21 @@ async def get_past_events( events = db.query(Event).options(joinedload(Event.creator)).filter( Event.date < datetime.utcnow() ).order_by(Event.date.desc()).all() - return [format_event_response(event, db) for event in events] + + # Filtrer les événements privés + filtered_events = [] + for event in events: + if event.is_private: + participation = db.query(EventParticipation).filter( + EventParticipation.event_id == event.id, + EventParticipation.user_id == current_user.id + ).first() + if participation or event.creator_id == current_user.id or current_user.is_admin: + filtered_events.append(event) + else: + filtered_events.append(event) + + return [format_event_response(event, db) for event in filtered_events] @router.get("/{event_id}", response_model=EventResponse) async def get_event( @@ -112,6 +192,19 @@ async def get_event( status_code=status.HTTP_404_NOT_FOUND, detail="Event not found" ) + + # Vérifier l'accès pour les événements privés + if event.is_private: + participation = db.query(EventParticipation).filter( + EventParticipation.event_id == event.id, + EventParticipation.user_id == current_user.id + ).first() + if not participation and event.creator_id != current_user.id and not current_user.is_admin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="You don't have access to this private event" + ) + return format_event_response(event, db) @router.put("/{event_id}", response_model=EventResponse) @@ -166,6 +259,74 @@ async def delete_event( db.commit() return {"message": "Event deleted successfully"} +@router.post("/{event_id}/invite", response_model=EventResponse) +async def invite_users_to_event( + event_id: int, + user_ids: List[int], + db: Session = Depends(get_db), + current_user: User = Depends(get_current_active_user) +): + """Invite users to a private 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" + ) + + # Vérifier que l'événement est privé et que l'utilisateur est le créateur ou admin + if not event.is_private: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="This endpoint is only for private events" + ) + + if event.creator_id != current_user.id and not current_user.is_admin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Only the event creator can invite users" + ) + + # Inviter les utilisateurs + for user_id in user_ids: + # Vérifier si l'utilisateur existe et est actif + user = db.query(User).filter(User.id == user_id, User.is_active == True).first() + if not user: + continue + + # Vérifier si l'utilisateur n'est pas déjà invité + existing_participation = db.query(EventParticipation).filter( + EventParticipation.event_id == event_id, + EventParticipation.user_id == user_id + ).first() + + if not existing_participation: + participation = EventParticipation( + event_id=event.id, + user_id=user.id, + status=ParticipationStatus.PENDING + ) + db.add(participation) + + # Créer une notification + notification = Notification( + user_id=user.id, + type=NotificationType.EVENT_INVITATION, + title=f"Invitation à un événement privé: {event.title}", + message=f"{current_user.full_name} vous a invité à un événement privé", + link=f"/events/{event.id}" + ) + db.add(notification) + + # Envoyer un email + try: + send_event_notification(user.email, event) + except: + pass + + db.commit() + return format_event_response(event, db) + @router.put("/{event_id}/participation", response_model=EventResponse) async def update_participation( event_id: int, @@ -174,6 +335,25 @@ async def update_participation( current_user: User = Depends(get_current_active_user) ): """Update user participation status for 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" + ) + + # Pour les événements privés, vérifier que l'utilisateur est invité + if event.is_private: + participation_check = db.query(EventParticipation).filter( + EventParticipation.event_id == event_id, + EventParticipation.user_id == current_user.id + ).first() + if not participation_check and event.creator_id != current_user.id and not current_user.is_admin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="You are not invited to this private event" + ) + participation = db.query(EventParticipation).filter( EventParticipation.event_id == event_id, EventParticipation.user_id == current_user.id @@ -236,6 +416,7 @@ def format_event_response(event: Event, db: Session) -> dict: "end_date": event.end_date, "creator_id": event.creator_id, "cover_image": event.cover_image, + "is_private": event.is_private if event.is_private is not None else False, "created_at": event.created_at, "updated_at": event.updated_at, "creator_name": creator.full_name if creator else "Unknown", diff --git a/backend/app.py b/backend/app.py index 6cf6a59..c74e0a8 100644 --- a/backend/app.py +++ b/backend/app.py @@ -175,8 +175,9 @@ def init_default_settings(): async def lifespan(app: FastAPI): # Startup print("Starting LeDiscord backend...") - # Create tables - Base.metadata.create_all(bind=engine) + # Note: Database migrations are handled by Alembic + # Run migrations manually with: alembic upgrade head + # Base.metadata.create_all(bind=engine) # Disabled in favor of Alembic migrations # Initialize database with admin user init_database() # Initialize default settings diff --git a/backend/migrations/README.md b/backend/migrations/README.md new file mode 100644 index 0000000..e270552 --- /dev/null +++ b/backend/migrations/README.md @@ -0,0 +1,43 @@ +# Migrations Alembic + +Ce dossier contient les migrations de base de données gérées par Alembic. + +## Commandes utiles + +### Créer une nouvelle migration +```bash +alembic revision --autogenerate -m "Description de la migration" +``` + +### Appliquer les migrations +```bash +alembic upgrade head +``` + +### Revenir en arrière d'une migration +```bash +alembic downgrade -1 +``` + +### Voir l'historique des migrations +```bash +alembic history +``` + +### Voir la migration actuelle +```bash +alembic current +``` + +### Créer une migration vide (sans autogenerate) +```bash +alembic revision -m "Description de la migration" +``` + +## Notes importantes + +- Les migrations sont automatiquement détectées depuis les modèles SQLAlchemy dans `models/` +- Ne modifiez jamais manuellement les fichiers de migration existants +- Testez toujours les migrations en développement avant de les appliquer en production +- En cas de problème, vous pouvez toujours revenir en arrière avec `alembic downgrade` + diff --git a/backend/migrations/env.py b/backend/migrations/env.py new file mode 100644 index 0000000..d17415c --- /dev/null +++ b/backend/migrations/env.py @@ -0,0 +1,104 @@ +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +from config.database import Base +from config.settings import settings + +# Import all models so Alembic can detect them +from models import ( + User, + Event, + EventParticipation, + Album, + Media, + MediaLike, + Post, + PostMention, + PostLike, + PostComment, + Vlog, + VlogLike, + VlogComment, + Notification, + SystemSettings, + Information, + Ticket +) + +# set the sqlalchemy.url from settings +config.set_main_option("sqlalchemy.url", settings.DATABASE_URL) + +target_metadata = Base.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() + diff --git a/backend/migrations/script.py.mako b/backend/migrations/script.py.mako new file mode 100644 index 0000000..3c2e787 --- /dev/null +++ b/backend/migrations/script.py.mako @@ -0,0 +1,27 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} + diff --git a/backend/migrations/versions/89527c8da8e1_add_is_private_field_to_events.py b/backend/migrations/versions/89527c8da8e1_add_is_private_field_to_events.py new file mode 100644 index 0000000..70ffbcb --- /dev/null +++ b/backend/migrations/versions/89527c8da8e1_add_is_private_field_to_events.py @@ -0,0 +1,38 @@ +"""Add is_private field to events + +Revision ID: 89527c8da8e1 +Revises: +Create Date: 2026-01-25 18:43:00.881982 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '89527c8da8e1' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + # Ajouter la colonne avec nullable=True d'abord + op.add_column('events', sa.Column('is_private', sa.Boolean(), nullable=True)) + # Mettre à jour toutes les lignes existantes avec False + op.execute("UPDATE events SET is_private = FALSE WHERE is_private IS NULL") + # Rendre la colonne non-nullable avec une valeur par défaut + op.alter_column('events', 'is_private', + existing_type=sa.Boolean(), + nullable=False, + server_default='false') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('events', 'is_private') + # ### end Alembic commands ### diff --git a/backend/models/__init__.py b/backend/models/__init__.py index 47a6757..76e5f00 100644 --- a/backend/models/__init__.py +++ b/backend/models/__init__.py @@ -1,7 +1,7 @@ from .user import User from .event import Event, EventParticipation from .album import Album, Media, MediaLike -from .post import Post, PostMention +from .post import Post, PostMention, PostLike, PostComment from .vlog import Vlog, VlogLike, VlogComment from .notification import Notification from .settings import SystemSettings @@ -17,6 +17,8 @@ __all__ = [ "MediaLike", "Post", "PostMention", + "PostLike", + "PostComment", "Vlog", "VlogLike", "VlogComment", diff --git a/backend/models/event.py b/backend/models/event.py index b9a436c..23b84cc 100644 --- a/backend/models/event.py +++ b/backend/models/event.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Text, Enum as SQLEnum, Float +from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Text, Enum as SQLEnum, Float, Boolean from sqlalchemy.orm import relationship from datetime import datetime import enum @@ -23,6 +23,7 @@ class Event(Base): end_date = Column(DateTime) creator_id = Column(Integer, ForeignKey("users.id"), nullable=False) cover_image = Column(String) + is_private = Column(Boolean, default=False) # Événement privé ou public created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) diff --git a/backend/schemas/event.py b/backend/schemas/event.py index d421064..7693112 100644 --- a/backend/schemas/event.py +++ b/backend/schemas/event.py @@ -14,6 +14,8 @@ class EventBase(BaseModel): class EventCreate(EventBase): cover_image: Optional[str] = None + is_private: bool = False + invited_user_ids: Optional[List[int]] = None # Liste des IDs des utilisateurs invités (pour événements privés) class EventUpdate(BaseModel): title: Optional[str] = Field(None, min_length=1, max_length=200) @@ -24,6 +26,7 @@ class EventUpdate(BaseModel): date: Optional[datetime] = None end_date: Optional[datetime] = None cover_image: Optional[str] = None + is_private: Optional[bool] = None class ParticipationResponse(BaseModel): user_id: int @@ -42,6 +45,7 @@ class EventResponse(EventBase): creator_name: str creator_avatar: Optional[str] = None cover_image: Optional[str] + is_private: bool = False created_at: datetime participations: List[ParticipationResponse] = [] present_count: int = 0 diff --git a/backend/scripts/migrate.sh b/backend/scripts/migrate.sh new file mode 100755 index 0000000..8d1e33d --- /dev/null +++ b/backend/scripts/migrate.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# Script helper pour gérer les migrations Alembic + +set -e + +case "$1" in + create) + if [ -z "$2" ]; then + echo "Usage: ./scripts/migrate.sh create 'message de migration'" + exit 1 + fi + alembic revision --autogenerate -m "$2" + ;; + upgrade) + alembic upgrade head + ;; + downgrade) + if [ -z "$2" ]; then + alembic downgrade -1 + else + alembic downgrade "$2" + fi + ;; + current) + alembic current + ;; + history) + alembic history + ;; + *) + echo "Usage: ./scripts/migrate.sh {create|upgrade|downgrade|current|history}" + echo "" + echo "Commands:" + echo " create 'message' - Créer une nouvelle migration" + echo " upgrade - Appliquer toutes les migrations en attente" + echo " downgrade [rev] - Revenir en arrière (par défaut -1)" + echo " current - Afficher la migration actuelle" + echo " history - Afficher l'historique des migrations" + exit 1 + ;; +esac + diff --git a/frontend/Dockerfile b/frontend/Dockerfile index c8e4562..0eb9fb6 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,15 +1,67 @@ -FROM node:18-alpine +# Stage 1 : Build prod +FROM node:20-alpine AS builder WORKDIR /app -# Copy package files first for better caching COPY package*.json ./ - -# Install dependencies RUN npm ci -# Copy application files COPY . . -# Run the application in development mode -CMD ["npm", "run", "dev"] +ARG VITE_API_URL=https://api.lediscord.com +ARG VITE_APP_URL=https://lediscord.com +ARG VITE_UPLOAD_URL=https://api.lediscord.com/uploads + +ENV VITE_API_URL=$VITE_API_URL +ENV VITE_APP_URL=$VITE_APP_URL +ENV VITE_UPLOAD_URL=$VITE_UPLOAD_URL + +RUN npm run build + +# Stage 2 : Image finale avec les deux modes +FROM node:20-alpine + +RUN apk add --no-cache nginx + +WORKDIR /app + +# Copier les sources pour le mode dev +COPY package*.json ./ +RUN npm ci + +COPY . . + +# Copier le build prod +COPY --from=builder /app/dist /usr/share/nginx/html + +# Config nginx +RUN echo 'server { \ + listen 8080; \ + root /usr/share/nginx/html; \ + index index.html; \ + location / { \ + try_files $uri $uri/ /index.html; \ + } \ + location /assets { \ + expires 1y; \ + add_header Cache-Control "public, immutable"; \ + } \ +}' > /etc/nginx/conf.d/default.conf + +# Script d'entrée +COPY < -
+
diff --git a/frontend/src/views/Events.vue b/frontend/src/views/Events.vue index 7c24888..3bed5d4 100644 --- a/frontend/src/views/Events.vue +++ b/frontend/src/views/Events.vue @@ -71,7 +71,14 @@
-
+
+ + 🔒 Privé +
+ +
+ +
+ + +
+
+ + +
+ +
+
+
+ +
+ +
+
+
{{ user.full_name }}
+
@{{ user.username }}
+
+
+ +
+
+

+ Sélectionnez les membres à inviter à cet événement privé +

+
+