Spaces:
Running
Running
Download multi_llm_chatbot_backend/app/api/routes/auth.py from BrainForge/Cybersecurity-Panel: direct link, hf CLI and curl.
- Browser
- Download file 16.2 kB
-
https://huggingface.co/spaces/BrainForge/Cybersecurity-Panel/resolve/main/multi_llm_chatbot_backend/app/api/routes/auth.py
- Command line
-
hf download hf://spaces/BrainForge/Cybersecurity-Panel/multi_llm_chatbot_backend/app/api/routes/auth.py
-
curl -L -o auth.py https://huggingface.co/spaces/BrainForge/Cybersecurity-Panel/resolve/main/multi_llm_chatbot_backend/app/api/routes/auth.py
16.2 kB
| from fastapi import APIRouter, HTTPException, Depends, status | |
| from datetime import datetime, timedelta | |
| from app.models.user import UserCreate, UserLogin, User, Token, UserResponse, UserUpdate | |
| from pydantic import BaseModel, Field, model_validator | |
| from typing import Optional, List, Literal | |
| from uuid import uuid4 | |
| from app.core.auth import ( | |
| get_password_hash, | |
| verify_password, | |
| authenticate_user, | |
| create_access_token, | |
| get_user_by_email, | |
| get_current_active_user, | |
| create_user_response, | |
| ACCESS_TOKEN_EXPIRE_MINUTES | |
| ) | |
| from app.core.database import get_database | |
| from app.core.guest_demo import resolve_persona, derive_guest_label, seed_guest_demo, clear_guest_sample_data | |
| from app.core import user_knowledge | |
| import logging | |
| import secrets | |
| logger = logging.getLogger(__name__) | |
| class MessageResponse(BaseModel): | |
| message: str | |
| class ChangePasswordRequest(BaseModel): | |
| current_password: str | |
| new_password: str | |
| def passwords_must_differ(self): | |
| if self.current_password == self.new_password: | |
| raise ValueError("New password must be different from the current password") | |
| return self | |
| class DeleteAccountRequest(BaseModel): | |
| password: str = "" | |
| class GuestExploreRequest(BaseModel): | |
| """Intake for Explore as guest — two preset paths or free text.""" | |
| choice: Literal["personal", "business", "other"] = "personal" | |
| free_text: Optional[str] = Field(default=None, max_length=800) | |
| class ClearSampleResponse(BaseModel): | |
| message: str | |
| cleared: List[str] | |
| router = APIRouter() | |
| async def signup(user_data: UserCreate): | |
| """ | |
| Register a new user and return an access token. | |
| @param user_data: UserCreate with name, email, password, and optional academic fields | |
| @return: Token containing a JWT access token and the created UserResponse | |
| """ | |
| try: | |
| db = get_database() | |
| # Check if user already exists | |
| existing_user = await get_user_by_email(user_data.email) | |
| if existing_user: | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="Email already registered" | |
| ) | |
| # Create new user | |
| hashed_password = get_password_hash(user_data.password) | |
| user = User( | |
| firstName=user_data.firstName, | |
| lastName=user_data.lastName, | |
| email=user_data.email, | |
| hashed_password=hashed_password, | |
| academicStage=user_data.academicStage, | |
| researchArea=user_data.researchArea, | |
| created_at=datetime.utcnow(), | |
| is_active=True | |
| ) | |
| # Insert user into database | |
| result = await db.users.insert_one(user.dict(by_alias=True)) | |
| user.id = result.inserted_id | |
| profile_seed: dict = { | |
| "user_id": user.id, | |
| "updated_at": datetime.utcnow(), | |
| } | |
| if user_data.academicStage: | |
| profile_seed["knowledge_level"] = user_data.academicStage | |
| if user_data.researchArea: | |
| profile_seed["timezone"] = user_data.researchArea | |
| await db.user_profiles.update_one( | |
| {"user_id": user.id}, | |
| {"$set": profile_seed}, | |
| upsert=True, | |
| ) | |
| # Create access token | |
| access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) | |
| access_token = create_access_token( | |
| data={"sub": str(user.id)}, | |
| expires_delta=access_token_expires | |
| ) | |
| return Token( | |
| access_token=access_token, | |
| token_type="bearer", | |
| user=create_user_response(user) | |
| ) | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Error during signup: {e}") | |
| raise HTTPException( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail="Could not create user account" | |
| ) | |
| async def login(user_credentials: UserLogin): | |
| """ | |
| Authenticate a user and return an access token. | |
| @param user_credentials: UserLogin with email and password | |
| @return: Token containing a JWT access token and the authenticated UserResponse | |
| """ | |
| try: | |
| # Authenticate user | |
| user = await authenticate_user(user_credentials.email, user_credentials.password) | |
| if not user: | |
| raise HTTPException( | |
| status_code=status.HTTP_401_UNAUTHORIZED, | |
| detail="Incorrect email or password", | |
| headers={"WWW-Authenticate": "Bearer"}, | |
| ) | |
| # Update last login time | |
| db = get_database() | |
| await db.users.update_one( | |
| {"_id": user.id}, | |
| {"$set": {"last_login": datetime.utcnow()}} | |
| ) | |
| user.last_login = datetime.utcnow() | |
| # Session-start trigger: refresh the dual user summaries (plan §4.2). | |
| user_knowledge.schedule_summary_regeneration(user.id) | |
| # Create access token | |
| access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) | |
| access_token = create_access_token( | |
| data={"sub": str(user.id)}, | |
| expires_delta=access_token_expires | |
| ) | |
| return Token( | |
| access_token=access_token, | |
| token_type="bearer", | |
| user=create_user_response(user) | |
| ) | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Error during login: {e}") | |
| raise HTTPException( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail="Login failed" | |
| ) | |
| async def get_current_user_profile(current_user: User = Depends(get_current_active_user)): | |
| """ | |
| Retrieve the profile of the currently authenticated user. | |
| @param current_user: Authenticated user from dependency injection | |
| @return: UserResponse with the user's profile information | |
| """ | |
| return create_user_response(current_user) | |
| async def logout(): | |
| """ | |
| Log out the current user (client should discard the token). | |
| @return: MessageResponse with a confirmation message | |
| """ | |
| return MessageResponse(message="Successfully logged out") | |
| async def verify_token(current_user: User = Depends(get_current_active_user)): | |
| """ | |
| Validate the caller's JWT and return their profile. | |
| @param current_user: Authenticated user from dependency injection | |
| @return: UserResponse with the user's profile information | |
| """ | |
| return create_user_response(current_user) | |
| async def change_password( | |
| body: ChangePasswordRequest, | |
| current_user: User = Depends(get_current_active_user), | |
| ): | |
| """ | |
| Change the authenticated user's password. | |
| @param body: ChangePasswordRequest with the current and new passwords | |
| @param current_user: Authenticated user from dependency injection | |
| @return: MessageResponse with a confirmation message | |
| """ | |
| try: | |
| if not verify_password(body.current_password, current_user.hashed_password): | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="Current password is incorrect", | |
| ) | |
| if len(body.new_password) < 8: | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="New password must be at least 8 characters", | |
| ) | |
| db = get_database() | |
| await db.users.update_one( | |
| {"_id": current_user.id}, | |
| {"$set": {"hashed_password": get_password_hash(body.new_password)}}, | |
| ) | |
| return MessageResponse(message="Password changed successfully") | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Error during password change: {e}") | |
| raise HTTPException( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail="Could not change password" | |
| ) | |
| async def update_profile( | |
| body: UserUpdate, | |
| current_user: User = Depends(get_current_active_user), | |
| ): | |
| """Update account fields (name, email, avatar, signup preferences).""" | |
| try: | |
| db = get_database() | |
| updates = {k: v for k, v in body.dict().items() if v is not None} | |
| if body.firstName is not None: | |
| updates["firstName"] = body.firstName.strip() | |
| if body.lastName is not None: | |
| updates["lastName"] = body.lastName.strip() | |
| if not updates: | |
| return create_user_response(current_user) | |
| if "email" in updates and updates["email"] != current_user.email: | |
| existing = await get_user_by_email(updates["email"]) | |
| if existing: | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="Email already in use", | |
| ) | |
| await db.users.update_one({"_id": current_user.id}, {"$set": updates}) | |
| profile_sync: dict = {"updated_at": datetime.utcnow()} | |
| if body.academicStage is not None: | |
| profile_sync["knowledge_level"] = body.academicStage | |
| if body.researchArea is not None: | |
| profile_sync["timezone"] = body.researchArea | |
| if len(profile_sync) > 1: | |
| await db.user_profiles.update_one( | |
| {"user_id": current_user.id}, | |
| {"$set": profile_sync, "$setOnInsert": {"user_id": current_user.id}}, | |
| upsert=True, | |
| ) | |
| updated_user = await db.users.find_one({"_id": current_user.id}) | |
| return create_user_response(User(**updated_user)) | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Error during profile update: {e}") | |
| raise HTTPException( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail="Could not update profile" | |
| ) | |
| async def delete_account( | |
| body: DeleteAccountRequest, | |
| current_user: User = Depends(get_current_active_user), | |
| ): | |
| """ | |
| Permanently delete the authenticated user's account, chat sessions, canvas, | |
| profile, onboarding, user facts, and user summaries. | |
| @param body: DeleteAccountRequest with the user's password for confirmation | |
| @param current_user: Authenticated user from dependency injection | |
| @return: MessageResponse with a confirmation message | |
| """ | |
| try: | |
| is_guest = bool(getattr(current_user, "is_guest", False)) | |
| if not is_guest: | |
| if not body.password or not verify_password(body.password, current_user.hashed_password): | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="Incorrect password", | |
| ) | |
| db = get_database() | |
| uid = current_user.id | |
| # Purge uploaded-document chunks (ChromaDB) for each of the user's | |
| # chats before the chat sessions themselves are removed. | |
| try: | |
| from app.core.rag_manager import get_rag_manager | |
| rag = get_rag_manager() | |
| sessions = await db.chat_sessions.find( | |
| {"user_id": uid} | |
| ).to_list(length=1000) | |
| for s in sessions: | |
| rag.delete_session_documents(f"chat_{s['_id']}") | |
| except Exception as rag_err: | |
| logger.warning(f"RAG purge during account deletion failed: {rag_err}") | |
| await db.chat_sessions.delete_many({"user_id": uid}) | |
| await db.phd_canvases.delete_many({"user_id": uid}) | |
| await db.user_profiles.delete_many({"user_id": uid}) | |
| await db.onboarding_conversations.delete_many({"user_id": uid}) | |
| await db.user_facts.delete_many({"user_id": uid}) | |
| await db.user_summaries.delete_many({"user_id": uid}) | |
| await db.goal_tracks.delete_many({"user_id": uid}) | |
| await db.assessments.delete_many({"user_id": uid}) | |
| await db.users.delete_one({"_id": uid}) | |
| return MessageResponse(message="Account deleted") | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Error during account deletion: {e}") | |
| raise HTTPException( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail="Could not delete account" | |
| ) | |
| async def explore_as_guest(body: GuestExploreRequest): | |
| """Create a temporary guest user, seed demo data from intake, return JWT.""" | |
| try: | |
| if body.choice == "other" and not (body.free_text or "").strip(): | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="Please describe what you need help with", | |
| ) | |
| persona = resolve_persona(body.choice, body.free_text) | |
| label = derive_guest_label(persona, body.free_text) | |
| db = get_database() | |
| guest_id = uuid4().hex[:12] | |
| email = f"guest.{guest_id}@guest.neonai.dev" | |
| first = "Guest" | |
| last = { | |
| "personal": "Explorer", | |
| "business": "Advisor Demo", | |
| "other": "Explorer", | |
| }.get(persona, "Explorer") | |
| user = User( | |
| firstName=first, | |
| lastName=last, | |
| email=email, | |
| hashed_password=get_password_hash(secrets.token_urlsafe(32)), | |
| academicStage="Beginner" if persona == "personal" else "Intermediate", | |
| researchArea="Guest demo", | |
| created_at=datetime.utcnow(), | |
| is_active=True, | |
| is_guest=True, | |
| guest_persona=persona, | |
| guest_label=label, | |
| ) | |
| result = await db.users.insert_one(user.dict(by_alias=True)) | |
| user.id = result.inserted_id | |
| await seed_guest_demo( | |
| user.id, | |
| persona, | |
| free_text=(body.free_text or "").strip() or None, | |
| ) | |
| # Session-start trigger: summarize the seeded demo facts so advisors | |
| # have user context from the first guest message. | |
| user_knowledge.schedule_summary_regeneration(user.id) | |
| access_token = create_access_token( | |
| data={"sub": str(user.id)}, | |
| expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES), | |
| ) | |
| return Token( | |
| access_token=access_token, | |
| token_type="bearer", | |
| user=create_user_response(user), | |
| ) | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Error creating guest session: {e}", exc_info=True) | |
| raise HTTPException( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail="Could not start guest session", | |
| ) | |
| async def clear_guest_sample( | |
| current_user: User = Depends(get_current_active_user), | |
| ): | |
| """Remove all seeded demo data for the current guest (or any user requesting reset).""" | |
| try: | |
| cleared = await clear_guest_sample_data(current_user.id) | |
| return ClearSampleResponse( | |
| message="Sample data removed. You are still in guest mode with a clean slate.", | |
| cleared=cleared, | |
| ) | |
| except Exception as e: | |
| logger.error(f"Error clearing guest sample data: {e}", exc_info=True) | |
| raise HTTPException( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail="Could not clear sample data", | |
| ) |