feat: Space Booking System - MVP complet

Sistem web pentru rezervarea de birouri și săli de ședință
cu flux de aprobare administrativă.

Stack: FastAPI + Vue.js 3 + SQLite + TypeScript

Features implementate:
- Autentificare JWT + Self-registration cu email verification
- CRUD Spații, Utilizatori, Settings (Admin)
- Calendar interactiv (FullCalendar) cu drag-and-drop
- Creare rezervări cu validare (durată, program, overlap, max/zi)
- Rezervări recurente (săptămânal)
- Admin: aprobare/respingere/anulare cereri
- Admin: creare directă rezervări (bypass approval)
- Admin: editare orice rezervare
- User: editare/anulare rezervări proprii
- Notificări in-app (bell icon + dropdown)
- Notificări email (async SMTP cu BackgroundTasks)
- Jurnal acțiuni administrative (audit log)
- Rapoarte avansate (utilizare, top users, approval rate)
- Șabloane rezervări (booking templates)
- Atașamente fișiere (upload/download)
- Conflict warnings (verificare disponibilitate real-time)
- Integrare Google Calendar (OAuth2)
- Suport timezone (UTC storage + user preference)
- 225+ teste backend

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-02-09 17:51:29 +00:00
commit df4031d99c
113 changed files with 24491 additions and 0 deletions

View File

@@ -0,0 +1 @@
# Core module

View File

@@ -0,0 +1,48 @@
"""Application configuration."""
from typing import List
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""Application settings."""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False
)
# App
app_name: str = "Space Booking API"
debug: bool = False
# Database
database_url: str = "sqlite:///./space_booking.db"
# JWT
secret_key: str = "your-secret-key-change-in-production"
algorithm: str = "HS256"
access_token_expire_minutes: int = 1440 # 24 hours
# SMTP
smtp_host: str = "localhost"
smtp_port: int = 1025 # MailHog default
smtp_user: str = ""
smtp_password: str = ""
smtp_from_address: str = "noreply@space-booking.local"
smtp_enabled: bool = False # Disable by default for dev
# Frontend
frontend_url: str = "http://localhost:5173"
# Google Calendar OAuth
google_client_id: str = ""
google_client_secret: str = ""
google_redirect_uri: str = "http://localhost:8000/api/integrations/google/callback"
google_scopes: List[str] = [
"https://www.googleapis.com/auth/calendar",
"https://www.googleapis.com/auth/calendar.events"
]
settings = Settings()

52
backend/app/core/deps.py Normal file
View File

@@ -0,0 +1,52 @@
"""Dependencies for FastAPI routes."""
from typing import Annotated
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jose import JWTError, jwt
from sqlalchemy.orm import Session
from app.core.config import settings
from app.db.session import get_db
from app.models.user import User
security = HTTPBearer()
def get_current_user(
credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)],
db: Annotated[Session, Depends(get_db)],
) -> User:
"""Get current authenticated user from JWT token."""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
token = credentials.credentials
payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
user_id: str | None = payload.get("sub")
if user_id is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = db.query(User).filter(User.id == int(user_id)).first()
if user is None or not user.is_active:
raise credentials_exception
return user
def get_current_admin(
current_user: Annotated[User, Depends(get_current_user)],
) -> User:
"""Verify current user is admin."""
if current_user.role != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions",
)
return current_user

View File

@@ -0,0 +1,36 @@
"""Security utilities for authentication and authorization."""
from datetime import datetime, timedelta
from typing import Any
from jose import jwt
from passlib.context import CryptContext
from app.core.config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify a password against a hash."""
result: bool = pwd_context.verify(plain_password, hashed_password)
return result
def get_password_hash(password: str) -> str:
"""Generate password hash."""
hashed: str = pwd_context.hash(password)
return hashed
def create_access_token(subject: str | int, expires_delta: timedelta | None = None) -> str:
"""Create JWT access token."""
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(
minutes=settings.access_token_expire_minutes
)
to_encode: dict[str, Any] = {"exp": expire, "sub": str(subject)}
encoded_jwt: str = jwt.encode(to_encode, settings.secret_key, algorithm=settings.algorithm)
return encoded_jwt