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>
218 lines
6.2 KiB
Python
218 lines
6.2 KiB
Python
"""Authentication endpoints."""
|
|
from datetime import datetime, timedelta
|
|
from typing import Annotated
|
|
|
|
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status
|
|
from jose import JWTError, jwt
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.config import settings
|
|
from app.core.deps import get_db
|
|
from app.core.security import create_access_token, get_password_hash, verify_password
|
|
from app.models.user import User
|
|
from app.schemas.auth import EmailVerificationRequest, LoginRequest, UserRegister
|
|
from app.schemas.user import Token
|
|
from app.services.email_service import send_email
|
|
|
|
router = APIRouter(prefix="/auth", tags=["auth"])
|
|
|
|
|
|
@router.post("/login", response_model=Token)
|
|
def login(
|
|
login_data: LoginRequest,
|
|
db: Annotated[Session, Depends(get_db)],
|
|
) -> Token:
|
|
"""
|
|
Login with email and password.
|
|
|
|
Returns JWT token for authenticated requests.
|
|
"""
|
|
user = db.query(User).filter(User.email == login_data.email).first()
|
|
|
|
if not user or not verify_password(login_data.password, str(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_403_FORBIDDEN,
|
|
detail="User account is disabled",
|
|
)
|
|
|
|
access_token = create_access_token(subject=int(user.id))
|
|
|
|
return Token(access_token=access_token, token_type="bearer")
|
|
|
|
|
|
@router.post("/register", status_code=201)
|
|
async def register(
|
|
data: UserRegister,
|
|
background_tasks: BackgroundTasks,
|
|
db: Annotated[Session, Depends(get_db)],
|
|
) -> dict:
|
|
"""
|
|
Register new user with email verification.
|
|
|
|
Creates an inactive user account and sends verification email.
|
|
"""
|
|
# Check if email already exists
|
|
existing = db.query(User).filter(User.email == data.email).first()
|
|
if existing:
|
|
raise HTTPException(status_code=400, detail="Email already registered")
|
|
|
|
# Create user (inactive until verified)
|
|
user = User(
|
|
email=data.email,
|
|
hashed_password=get_password_hash(data.password),
|
|
full_name=data.full_name,
|
|
organization=data.organization,
|
|
role="user", # Default role
|
|
is_active=False, # Inactive until email verified
|
|
)
|
|
|
|
db.add(user)
|
|
db.commit()
|
|
db.refresh(user)
|
|
|
|
# Generate verification token (JWT, expires in 24h)
|
|
verification_token = jwt.encode(
|
|
{
|
|
"sub": str(user.id),
|
|
"type": "email_verification",
|
|
"exp": datetime.utcnow() + timedelta(hours=24),
|
|
},
|
|
settings.secret_key,
|
|
algorithm="HS256",
|
|
)
|
|
|
|
# Send verification email (background task)
|
|
verification_link = f"{settings.frontend_url}/verify?token={verification_token}"
|
|
|
|
subject = "Verifică contul tău - Space Booking"
|
|
body = f"""Bună ziua {user.full_name},
|
|
|
|
Bine ai venit pe platforma Space Booking!
|
|
|
|
Pentru a-ți activa contul, te rugăm să accesezi link-ul de mai jos:
|
|
|
|
{verification_link}
|
|
|
|
Link-ul va expira în 24 de ore.
|
|
|
|
Dacă nu ai creat acest cont, te rugăm să ignori acest email.
|
|
|
|
Cu stimă,
|
|
Echipa Space Booking
|
|
"""
|
|
|
|
background_tasks.add_task(send_email, user.email, subject, body)
|
|
|
|
return {
|
|
"message": "Registration successful. Please check your email to verify your account.",
|
|
"email": user.email,
|
|
}
|
|
|
|
|
|
@router.post("/verify")
|
|
def verify_email(
|
|
data: EmailVerificationRequest,
|
|
db: Annotated[Session, Depends(get_db)],
|
|
) -> dict:
|
|
"""
|
|
Verify email address with token.
|
|
|
|
Activates the user account if token is valid.
|
|
"""
|
|
try:
|
|
# Decode token
|
|
payload = jwt.decode(
|
|
data.token,
|
|
settings.secret_key,
|
|
algorithms=["HS256"],
|
|
)
|
|
|
|
# Check token type
|
|
if payload.get("type") != "email_verification":
|
|
raise HTTPException(status_code=400, detail="Invalid verification token")
|
|
|
|
user_id = int(payload.get("sub"))
|
|
|
|
# Get user
|
|
user = db.query(User).filter(User.id == user_id).first()
|
|
if not user:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
|
|
# Check if already verified
|
|
if user.is_active:
|
|
return {"message": "Email already verified"}
|
|
|
|
# Activate user
|
|
user.is_active = True
|
|
db.commit()
|
|
|
|
return {"message": "Email verified successfully. You can now log in."}
|
|
|
|
except jwt.ExpiredSignatureError:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="Verification link expired. Please request a new one.",
|
|
)
|
|
except JWTError:
|
|
raise HTTPException(status_code=400, detail="Invalid verification token")
|
|
|
|
|
|
@router.post("/resend-verification")
|
|
async def resend_verification(
|
|
email: str,
|
|
background_tasks: BackgroundTasks,
|
|
db: Annotated[Session, Depends(get_db)],
|
|
) -> dict:
|
|
"""
|
|
Resend verification email.
|
|
|
|
For security, always returns success even if email doesn't exist.
|
|
"""
|
|
user = db.query(User).filter(User.email == email).first()
|
|
if not user:
|
|
# Don't reveal if email exists
|
|
return {"message": "If the email exists, a verification link has been sent."}
|
|
|
|
if user.is_active:
|
|
raise HTTPException(status_code=400, detail="Account already verified")
|
|
|
|
# Generate new token
|
|
verification_token = jwt.encode(
|
|
{
|
|
"sub": str(user.id),
|
|
"type": "email_verification",
|
|
"exp": datetime.utcnow() + timedelta(hours=24),
|
|
},
|
|
settings.secret_key,
|
|
algorithm="HS256",
|
|
)
|
|
|
|
# Send email
|
|
verification_link = f"{settings.frontend_url}/verify?token={verification_token}"
|
|
|
|
subject = "Verifică contul tău - Space Booking"
|
|
body = f"""Bună ziua {user.full_name},
|
|
|
|
Ai solicitat un nou link de verificare pentru contul tău pe Space Booking.
|
|
|
|
Pentru a-ți activa contul, te rugăm să accesezi link-ul de mai jos:
|
|
|
|
{verification_link}
|
|
|
|
Link-ul va expira în 24 de ore.
|
|
|
|
Cu stimă,
|
|
Echipa Space Booking
|
|
"""
|
|
|
|
background_tasks.add_task(send_email, user.email, subject, body)
|
|
|
|
return {"message": "If the email exists, a verification link has been sent."}
|