Files
space-booking/backend/app/api/google_calendar.py
Claude Agent df4031d99c 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>
2026-02-09 17:51:29 +00:00

177 lines
5.8 KiB
Python

"""Google Calendar integration endpoints."""
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, Query, status
from google_auth_oauthlib.flow import Flow
from sqlalchemy.orm import Session
from app.core.config import settings
from app.core.deps import get_current_user, get_db
from app.models.google_calendar_token import GoogleCalendarToken
from app.models.user import User
router = APIRouter()
@router.get("/integrations/google/connect")
def connect_google(
current_user: Annotated[User, Depends(get_current_user)],
) -> dict[str, str]:
"""
Start Google OAuth flow.
Returns authorization URL that user should visit to grant access.
"""
if not settings.google_client_id or not settings.google_client_secret:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Google Calendar integration not configured",
)
try:
flow = Flow.from_client_config(
{
"web": {
"client_id": settings.google_client_id,
"client_secret": settings.google_client_secret,
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"redirect_uris": [settings.google_redirect_uri],
}
},
scopes=[
"https://www.googleapis.com/auth/calendar",
"https://www.googleapis.com/auth/calendar.events",
],
redirect_uri=settings.google_redirect_uri,
)
authorization_url, state = flow.authorization_url(
access_type="offline", include_granted_scopes="true", prompt="consent"
)
# Note: In production, store state in session/cache and validate it in callback
return {"authorization_url": authorization_url, "state": state}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to start OAuth flow: {str(e)}",
)
@router.get("/integrations/google/callback")
def google_callback(
code: Annotated[str, Query()],
state: Annotated[str, Query()],
db: Annotated[Session, Depends(get_db)],
current_user: Annotated[User, Depends(get_current_user)],
) -> dict[str, str]:
"""
Handle Google OAuth callback.
Exchange authorization code for tokens and store them.
"""
if not settings.google_client_id or not settings.google_client_secret:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Google Calendar integration not configured",
)
try:
flow = Flow.from_client_config(
{
"web": {
"client_id": settings.google_client_id,
"client_secret": settings.google_client_secret,
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"redirect_uris": [settings.google_redirect_uri],
}
},
scopes=[
"https://www.googleapis.com/auth/calendar",
"https://www.googleapis.com/auth/calendar.events",
],
redirect_uri=settings.google_redirect_uri,
state=state,
)
# Exchange code for tokens
flow.fetch_token(code=code)
credentials = flow.credentials
# Store tokens
token_record = (
db.query(GoogleCalendarToken)
.filter(GoogleCalendarToken.user_id == current_user.id)
.first()
)
if token_record:
token_record.access_token = credentials.token # type: ignore[assignment]
token_record.refresh_token = credentials.refresh_token # type: ignore[assignment]
token_record.token_expiry = credentials.expiry # type: ignore[assignment]
else:
token_record = GoogleCalendarToken(
user_id=current_user.id, # type: ignore[arg-type]
access_token=credentials.token,
refresh_token=credentials.refresh_token,
token_expiry=credentials.expiry,
)
db.add(token_record)
db.commit()
return {"message": "Google Calendar connected successfully"}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"OAuth failed: {str(e)}",
)
@router.delete("/integrations/google/disconnect")
def disconnect_google(
db: Annotated[Session, Depends(get_db)],
current_user: Annotated[User, Depends(get_current_user)],
) -> dict[str, str]:
"""
Disconnect Google Calendar.
Removes stored tokens for the current user.
"""
token_record = (
db.query(GoogleCalendarToken)
.filter(GoogleCalendarToken.user_id == current_user.id)
.first()
)
if token_record:
db.delete(token_record)
db.commit()
return {"message": "Google Calendar disconnected"}
@router.get("/integrations/google/status")
def google_status(
db: Annotated[Session, Depends(get_db)],
current_user: Annotated[User, Depends(get_current_user)],
) -> dict[str, bool | str | None]:
"""
Check Google Calendar connection status.
Returns whether user has connected their Google Calendar account.
"""
token_record = (
db.query(GoogleCalendarToken)
.filter(GoogleCalendarToken.user_id == current_user.id)
.first()
)
return {
"connected": token_record is not None,
"expires_at": token_record.token_expiry.isoformat() if token_record and token_record.token_expiry else None,
}