Download src/auth.py from CareerAI/app: direct link, hf CLI and curl.
- Browser
- Download file 12.4 kB
-
https://huggingface.co/spaces/CareerAI/app/resolve/main/src/auth.py
- Command line
-
hf download hf://spaces/CareerAI/app/src/auth.py
-
curl -L -o auth.py https://huggingface.co/spaces/CareerAI/app/resolve/main/src/auth.py
12.4 kB
| from fastapi import APIRouter, Depends, HTTPException, status | |
| from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm | |
| from sqlalchemy.orm import Session | |
| from sqlalchemy import or_ | |
| from datetime import datetime, timedelta | |
| from jose import JWTError, jwt | |
| from fastapi_mail import FastMail, MessageSchema, ConnectionConfig, MessageType | |
| import bcrypt | |
| import json | |
| import random | |
| from src.models import get_db, User, Conversation | |
| import os | |
| # Memory dictionary to mock sent emails for recovery | |
| reset_codes = {} | |
| # REAL EMAIL CONFIGURATION (reads from .env) — only if configured | |
| _mail_from = os.environ.get("MAIL_FROM", "") | |
| _mail_user = os.environ.get("MAIL_USERNAME", "") | |
| _mail_pass = os.environ.get("MAIL_PASSWORD", "") | |
| if _mail_from and "@" in _mail_from and _mail_user and _mail_pass: | |
| conf_mail = ConnectionConfig( | |
| MAIL_USERNAME=_mail_user, | |
| MAIL_PASSWORD=_mail_pass, | |
| MAIL_FROM=_mail_from, | |
| MAIL_PORT=587, | |
| MAIL_SERVER="smtp.gmail.com", | |
| MAIL_STARTTLS=True, | |
| MAIL_SSL_TLS=False, | |
| USE_CREDENTIALS=True, | |
| VALIDATE_CERTS=True | |
| ) | |
| fast_mail = FastMail(conf_mail) | |
| else: | |
| conf_mail = None | |
| fast_mail = None | |
| # JWT configuration (reads from .env) | |
| SECRET_KEY = os.environ.get("SECRET_KEY", "fallback_dev_key_change_this") | |
| ALGORITHM = "HS256" | |
| ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 days validity | |
| oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login") | |
| router = APIRouter(prefix="/api/auth", tags=["auth"]) | |
| def verify_password(plain_password: str, hashed_password: str): | |
| if isinstance(hashed_password, str): | |
| hashed_password = hashed_password.encode('utf-8') | |
| return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password) | |
| def get_password_hash(password: str): | |
| return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8') | |
| def create_access_token(data: dict, expires_delta: timedelta = None): | |
| to_encode = data.copy() | |
| if expires_delta: | |
| expire = datetime.utcnow() + expires_delta | |
| else: | |
| expire = datetime.utcnow() + timedelta(minutes=15) | |
| to_encode.update({"exp": expire}) | |
| return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) | |
| async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)): | |
| credentials_exception = HTTPException( | |
| status_code=status.HTTP_401_UNAUTHORIZED, | |
| detail="Could not validate credentials", | |
| headers={"WWW-Authenticate": "Bearer"}, | |
| ) | |
| try: | |
| payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) | |
| email: str = payload.get("sub") | |
| if email is None: | |
| raise credentials_exception | |
| except JWTError: | |
| raise credentials_exception | |
| user = db.query(User).filter(User.email == email).first() | |
| if user is None: | |
| raise credentials_exception | |
| return user | |
| from fastapi import Header | |
| async def get_user_or_session_id( | |
| authorization: str = Header(None), | |
| x_session_id: str = Header(None) | |
| ) -> str: | |
| """Extracts a private effective user ID to isolate documents and chats.""" | |
| # 1. Try logged-in user from JWT | |
| if authorization and authorization.startswith("Bearer "): | |
| token = authorization.split(" ")[1] | |
| try: | |
| payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) | |
| user_sub = payload.get("sub") | |
| if user_sub: | |
| return f"user_{user_sub}" | |
| except JWTError: | |
| pass | |
| # 2. Try anonymous session header | |
| if x_session_id: | |
| return f"guest_{x_session_id}" | |
| # 3. Fallback | |
| return "anonymous" | |
| from pydantic import BaseModel, EmailStr | |
| from typing import Optional | |
| class UserCreate(BaseModel): | |
| name: str | |
| email: EmailStr | |
| password: str | |
| class UserUpdate(BaseModel): | |
| name: Optional[str] = None | |
| picture: Optional[str] = None | |
| class ForgotPasswordBody(BaseModel): | |
| email: EmailStr | |
| class ResetPasswordBody(BaseModel): | |
| email: EmailStr | |
| code: str | |
| new_password: str | |
| class GoogleLogin(BaseModel): | |
| token: str | |
| def register(user: UserCreate, db: Session = Depends(get_db)): | |
| db_user = db.query(User).filter(User.email == user.email).first() | |
| if db_user: | |
| raise HTTPException(status_code=400, detail="El correo ya está registrado") | |
| new_user = User( | |
| email=user.email, | |
| name=user.name, | |
| hashed_password=get_password_hash(user.password), | |
| picture="https://ui-avatars.com/api/?name=" + user.name.replace(" ", "+") | |
| ) | |
| db.add(new_user) | |
| db.commit() | |
| db.refresh(new_user) | |
| access_token = create_access_token( | |
| data={"sub": new_user.email}, expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) | |
| ) | |
| return {"access_token": access_token, "token_type": "bearer", "user": {"name": new_user.name, "email": new_user.email, "picture": new_user.picture}} | |
| def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): | |
| user = db.query(User).filter(User.email == form_data.username).first() | |
| if not user or not user.hashed_password: | |
| raise HTTPException(status_code=400, detail="Correo o contraseña incorrectos") | |
| if not verify_password(form_data.password, user.hashed_password): | |
| raise HTTPException(status_code=400, detail="Correo o contraseña incorrectos") | |
| access_token = create_access_token( | |
| data={"sub": user.email}, expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) | |
| ) | |
| return {"access_token": access_token, "token_type": "bearer", "user": {"name": user.name, "email": user.email, "picture": user.picture}} | |
| async def forgot_password(body: ForgotPasswordBody, db: Session = Depends(get_db)): | |
| user = db.query(User).filter(User.email == body.email).first() | |
| if not user: | |
| # Prevent user-enumeration, always return ok | |
| return {"status": "ok", "message": "Si el correo está registrado, se envió un código temporal."} | |
| code = str(random.randint(100000, 999999)) | |
| reset_codes[body.email] = code | |
| # Send actual email if configured, otherwise print to terminal | |
| if fast_mail is not None: | |
| message = MessageSchema( | |
| subject="Recuperación de Contraseña - CareerAI", | |
| recipients=[body.email], | |
| body=f"Hola {user.name},\n\nHemos recibido una solicitud para restablecer tu contraseña.\n\nTu código de recuperación es: {code}\n\nSi no fuiste tú, ignora este mensaje.", | |
| subtype=MessageType.plain | |
| ) | |
| try: | |
| await fast_mail.send_message(message) | |
| print(f"📧 Correo Real enviado exitosamente a {body.email}") | |
| except Exception as e: | |
| print(f"❌ Error enviando el correo real: {str(e)}") | |
| else: | |
| print("\n" + "="*50) | |
| print("📧 SIMULACIÓN (Email no configurado en producción):") | |
| print(f"Para: {body.email}") | |
| print("Asunto: Recuperación de tu contraseña") | |
| print(f"Tu código de recuperación temporal es: {code}") | |
| print("="*50 + "\n") | |
| return {"status": "ok", "message": "Si el correo está registrado, se envió un código temporal."} | |
| def reset_password(body: ResetPasswordBody, db: Session = Depends(get_db)): | |
| if reset_codes.get(body.email) != body.code: | |
| raise HTTPException(status_code=400, detail="Código inválido o ya ha expirado") | |
| user = db.query(User).filter(User.email == body.email).first() | |
| if not user: | |
| raise HTTPException(status_code=400, detail="Usuario no encontrado") | |
| user.hashed_password = get_password_hash(body.new_password) | |
| db.commit() | |
| reset_codes.pop(body.email, None) # Invalidate token safely | |
| return {"status": "ok", "message": "Contraseña actualizada exitosamente"} | |
| # Try to import Google Auth (if installed) | |
| try: | |
| from google.oauth2 import id_token | |
| from google.auth.transport import requests as google_requests | |
| GOOGLE_AUTH_AVAILABLE = True | |
| except ImportError: | |
| GOOGLE_AUTH_AVAILABLE = False | |
| def google_login(google_data: GoogleLogin, db: Session = Depends(get_db)): | |
| if not GOOGLE_AUTH_AVAILABLE: | |
| raise HTTPException(status_code=500, detail="Google Auth is not installed properly") | |
| try: | |
| # Avoid verifying clientId to allow any client side requests for demo purposes | |
| # In production use ONLY your registered CLIENT_ID | |
| idinfo = id_token.verify_oauth2_token( | |
| google_data.token, | |
| google_requests.Request() | |
| ) | |
| email = idinfo['email'] | |
| name = idinfo.get('name', 'Google User') | |
| picture = idinfo.get('picture', '') | |
| google_id = idinfo['sub'] | |
| user = db.query(User).filter(or_(User.email == email, User.google_id == google_id)).first() | |
| if not user: | |
| # Create user automatically | |
| user = User(email=email, name=name, picture=picture, google_id=google_id) | |
| db.add(user) | |
| db.commit() | |
| db.refresh(user) | |
| else: | |
| # Update user info if needed | |
| if not user.google_id: | |
| user.google_id = google_id | |
| if picture: | |
| user.picture = picture | |
| db.commit() | |
| access_token = create_access_token( | |
| data={"sub": user.email}, expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) | |
| ) | |
| return {"access_token": access_token, "token_type": "bearer", "user": {"name": user.name, "email": user.email, "picture": user.picture}} | |
| except ValueError as e: | |
| raise HTTPException(status_code=400, detail="Token de Google inválido") | |
| def get_me(current_user: User = Depends(get_current_user)): | |
| return {"name": current_user.name, "email": current_user.email, "picture": current_user.picture} | |
| def update_me(user_update: UserUpdate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): | |
| if user_update.name is not None: | |
| current_user.name = user_update.name | |
| if user_update.picture is not None: | |
| current_user.picture = user_update.picture | |
| db.commit() | |
| db.refresh(current_user) | |
| return {"name": current_user.name, "email": current_user.email, "picture": current_user.picture} | |
| # ================= Conversations Router Endpoints ================= | |
| conv_router = APIRouter(prefix="/api/conversations", tags=["conversations"]) | |
| class ConversationBody(BaseModel): | |
| id: str | |
| title: str | |
| messages: list | |
| def list_conversations(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): | |
| convs = db.query(Conversation).filter(Conversation.user_id == current_user.id).order_by(Conversation.updated_at.desc()).all() | |
| # Format according to frontend expectations | |
| return [{"id": c.id, "title": c.title, "messages": c.messages} for c in convs] | |
| def save_conversation(data: ConversationBody, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): | |
| conv = db.query(Conversation).filter(Conversation.id == data.id).first() | |
| if conv: | |
| if conv.user_id != current_user.id: | |
| raise HTTPException(status_code=403, detail="Not authorized") | |
| conv.title = data.title | |
| conv.messages = data.messages | |
| # updated_at will auto-update | |
| else: | |
| conv = Conversation( | |
| id=data.id, | |
| user_id=current_user.id, | |
| title=data.title, | |
| messages=data.messages | |
| ) | |
| db.add(conv) | |
| db.commit() | |
| return {"status": "ok", "message": "Conversación guardada"} | |
| def delete_conversation(conv_id: str, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): | |
| conv = db.query(Conversation).filter(Conversation.id == conv_id).first() | |
| if not conv: | |
| raise HTTPException(status_code=404, detail="Not found") | |
| if conv.user_id != current_user.id: | |
| raise HTTPException(status_code=403, detail="Not authorized") | |
| db.delete(conv) | |
| db.commit() | |
| return {"status": "ok", "message": "Conversación eliminada"} | |