Spaces:
Running
Running
feat(auth): implement Google OAuth and Email/Password authentication
Browse files- alembic/versions/54948c59e8f3_add_users_table.py +51 -0
- app/api/auth.py +227 -0
- app/main.py +8 -0
- app/models/database_models.py +16 -0
- frontend/src/App.jsx +19 -4
- frontend/src/components/AuthModal.jsx +201 -0
- frontend/src/components/Navbar.jsx +64 -24
- frontend/src/context/AuthContext.jsx +165 -0
- frontend/src/sections/HeroSection.jsx +11 -8
- pyproject.toml +5 -0
- uv.lock +109 -0
alembic/versions/54948c59e8f3_add_users_table.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""add_users_table
|
| 2 |
+
|
| 3 |
+
Revision ID: 54948c59e8f3
|
| 4 |
+
Revises: b78e4e81098b
|
| 5 |
+
Create Date: 2026-06-14 11:47:12.191919
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
from typing import Sequence, Union
|
| 9 |
+
|
| 10 |
+
from alembic import op
|
| 11 |
+
import sqlalchemy as sa
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
# revision identifiers, used by Alembic.
|
| 15 |
+
revision: str = '54948c59e8f3'
|
| 16 |
+
down_revision: Union[str, Sequence[str], None] = 'b78e4e81098b'
|
| 17 |
+
branch_labels: Union[str, Sequence[str], None] = None
|
| 18 |
+
depends_on: Union[str, Sequence[str], None] = None
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def upgrade() -> None:
|
| 22 |
+
"""Upgrade schema."""
|
| 23 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
| 24 |
+
op.create_table('users',
|
| 25 |
+
sa.Column('id', sa.String(), nullable=False),
|
| 26 |
+
sa.Column('email', sa.String(), nullable=False),
|
| 27 |
+
sa.Column('name', sa.String(), nullable=True),
|
| 28 |
+
sa.Column('avatar_url', sa.String(), nullable=True),
|
| 29 |
+
sa.Column('google_id', sa.String(), nullable=True),
|
| 30 |
+
sa.Column('hashed_password', sa.String(), nullable=True),
|
| 31 |
+
sa.Column('created_at', sa.DateTime(), nullable=True),
|
| 32 |
+
sa.PrimaryKeyConstraint('id')
|
| 33 |
+
)
|
| 34 |
+
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
|
| 35 |
+
op.create_index(op.f('ix_users_google_id'), 'users', ['google_id'], unique=True)
|
| 36 |
+
op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
|
| 37 |
+
op.add_column('cases', sa.Column('user_id', sa.String(), nullable=True))
|
| 38 |
+
op.create_foreign_key(None, 'cases', 'users', ['user_id'], ['id'], ondelete='CASCADE')
|
| 39 |
+
# ### end Alembic commands ###
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def downgrade() -> None:
|
| 43 |
+
"""Downgrade schema."""
|
| 44 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
| 45 |
+
op.drop_constraint(None, 'cases', type_='foreignkey')
|
| 46 |
+
op.drop_column('cases', 'user_id')
|
| 47 |
+
op.drop_index(op.f('ix_users_id'), table_name='users')
|
| 48 |
+
op.drop_index(op.f('ix_users_google_id'), table_name='users')
|
| 49 |
+
op.drop_index(op.f('ix_users_email'), table_name='users')
|
| 50 |
+
op.drop_table('users')
|
| 51 |
+
# ### end Alembic commands ###
|
app/api/auth.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import uuid
|
| 3 |
+
from datetime import datetime, timedelta
|
| 4 |
+
from typing import Optional
|
| 5 |
+
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
| 6 |
+
from fastapi.responses import HTMLResponse, RedirectResponse
|
| 7 |
+
from pydantic import BaseModel, EmailStr
|
| 8 |
+
from sqlalchemy.orm import Session
|
| 9 |
+
from jose import jwt, JWTError
|
| 10 |
+
from passlib.context import CryptContext
|
| 11 |
+
from authlib.integrations.starlette_client import OAuth
|
| 12 |
+
|
| 13 |
+
from app.services.database import get_db
|
| 14 |
+
from app.models.database_models import UserModel
|
| 15 |
+
|
| 16 |
+
# Security config
|
| 17 |
+
SECRET_KEY = os.getenv("JWT_SECRET_KEY", "supersecretagentbondkey123")
|
| 18 |
+
ALGORITHM = "HS256"
|
| 19 |
+
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 days
|
| 20 |
+
|
| 21 |
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
| 22 |
+
|
| 23 |
+
router = APIRouter(prefix="/api/auth", tags=["Authentication"])
|
| 24 |
+
|
| 25 |
+
# Configure OAuth
|
| 26 |
+
oauth = OAuth()
|
| 27 |
+
oauth.register(
|
| 28 |
+
name="google",
|
| 29 |
+
client_id=os.getenv("GOOGLE_CLIENT_ID"),
|
| 30 |
+
client_secret=os.getenv("GOOGLE_CLIENT_SECRET"),
|
| 31 |
+
server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
|
| 32 |
+
client_kwargs={"scope": "openid email profile"},
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
# Schemas
|
| 36 |
+
class UserRegisterRequest(BaseModel):
|
| 37 |
+
email: EmailStr
|
| 38 |
+
password: str
|
| 39 |
+
name: Optional[str] = None
|
| 40 |
+
|
| 41 |
+
class UserLoginRequest(BaseModel):
|
| 42 |
+
email: EmailStr
|
| 43 |
+
password: str
|
| 44 |
+
|
| 45 |
+
class TokenResponse(BaseModel):
|
| 46 |
+
access_token: str
|
| 47 |
+
token_type: str
|
| 48 |
+
|
| 49 |
+
class UserResponse(BaseModel):
|
| 50 |
+
id: str
|
| 51 |
+
email: str
|
| 52 |
+
name: Optional[str] = None
|
| 53 |
+
avatar_url: Optional[str] = None
|
| 54 |
+
created_at: datetime
|
| 55 |
+
|
| 56 |
+
class Config:
|
| 57 |
+
from_attributes = True
|
| 58 |
+
|
| 59 |
+
# Helper functions
|
| 60 |
+
def hash_password(password: str) -> str:
|
| 61 |
+
return pwd_context.hash(password)
|
| 62 |
+
|
| 63 |
+
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
| 64 |
+
return pwd_context.verify(plain_password, hashed_password)
|
| 65 |
+
|
| 66 |
+
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
|
| 67 |
+
to_encode = data.copy()
|
| 68 |
+
if expires_delta:
|
| 69 |
+
expire = datetime.utcnow() + expires_delta
|
| 70 |
+
else:
|
| 71 |
+
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
| 72 |
+
to_encode.update({"exp": expire})
|
| 73 |
+
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
| 74 |
+
return encoded_jwt
|
| 75 |
+
|
| 76 |
+
def get_current_user(request: Request, db: Session = Depends(get_db)) -> UserModel:
|
| 77 |
+
credentials_exception = HTTPException(
|
| 78 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 79 |
+
detail="Could not validate credentials",
|
| 80 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 81 |
+
)
|
| 82 |
+
auth_header = request.headers.get("Authorization")
|
| 83 |
+
if not auth_header or not auth_header.startswith("Bearer "):
|
| 84 |
+
raise credentials_exception
|
| 85 |
+
token = auth_header.split(" ")[1]
|
| 86 |
+
try:
|
| 87 |
+
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
| 88 |
+
user_id: str = payload.get("sub")
|
| 89 |
+
if user_id is None:
|
| 90 |
+
raise credentials_exception
|
| 91 |
+
except JWTError:
|
| 92 |
+
raise credentials_exception
|
| 93 |
+
|
| 94 |
+
user = db.query(UserModel).filter(UserModel.id == user_id).first()
|
| 95 |
+
if user is None:
|
| 96 |
+
raise credentials_exception
|
| 97 |
+
return user
|
| 98 |
+
|
| 99 |
+
# Routes
|
| 100 |
+
@router.post("/register", response_model=TokenResponse)
|
| 101 |
+
def register(payload: UserRegisterRequest, db: Session = Depends(get_db)):
|
| 102 |
+
# Check if user already exists
|
| 103 |
+
existing_user = db.query(UserModel).filter(UserModel.email == payload.email).first()
|
| 104 |
+
if existing_user:
|
| 105 |
+
raise HTTPException(
|
| 106 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 107 |
+
detail="Email already registered"
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
# Create new user
|
| 111 |
+
new_user = UserModel(
|
| 112 |
+
id=uuid.uuid4().hex,
|
| 113 |
+
email=payload.email,
|
| 114 |
+
name=payload.name or payload.email.split("@")[0],
|
| 115 |
+
hashed_password=hash_password(payload.password),
|
| 116 |
+
avatar_url=f"https://api.dicebear.com/7.x/bottts/svg?seed={payload.email}"
|
| 117 |
+
)
|
| 118 |
+
db.add(new_user)
|
| 119 |
+
db.commit()
|
| 120 |
+
db.refresh(new_user)
|
| 121 |
+
|
| 122 |
+
# Generate token
|
| 123 |
+
token = create_access_token(data={"sub": new_user.id})
|
| 124 |
+
return {"access_token": token, "token_type": "bearer"}
|
| 125 |
+
|
| 126 |
+
@router.post("/login", response_model=TokenResponse)
|
| 127 |
+
def login(payload: UserLoginRequest, db: Session = Depends(get_db)):
|
| 128 |
+
# Get user
|
| 129 |
+
user = db.query(UserModel).filter(UserModel.email == payload.email).first()
|
| 130 |
+
if not user or not user.hashed_password:
|
| 131 |
+
raise HTTPException(
|
| 132 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 133 |
+
detail="Invalid email or password"
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
# Check password
|
| 137 |
+
if not verify_password(payload.password, user.hashed_password):
|
| 138 |
+
raise HTTPException(
|
| 139 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 140 |
+
detail="Invalid email or password"
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
# Generate token
|
| 144 |
+
token = create_access_token(data={"sub": user.id})
|
| 145 |
+
return {"access_token": token, "token_type": "bearer"}
|
| 146 |
+
|
| 147 |
+
@router.get("/google")
|
| 148 |
+
async def google_login(request: Request):
|
| 149 |
+
# Google OAuth client expects a session or state to protect against CSRF.
|
| 150 |
+
# For Starlette OAuth to work, we need a redirect_uri.
|
| 151 |
+
redirect_uri = request.url_for("google_callback")
|
| 152 |
+
# Force http redirection to matching protocol if SSL termination happens
|
| 153 |
+
if "https" in str(request.base_url):
|
| 154 |
+
redirect_uri = str(redirect_uri).replace("http://", "https://")
|
| 155 |
+
return await oauth.google.authorize_redirect(request, redirect_uri)
|
| 156 |
+
|
| 157 |
+
@router.get("/google/callback")
|
| 158 |
+
async def google_callback(request: Request, db: Session = Depends(get_db)):
|
| 159 |
+
try:
|
| 160 |
+
token = await oauth.google.authorize_access_token(request)
|
| 161 |
+
except Exception as e:
|
| 162 |
+
raise HTTPException(
|
| 163 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 164 |
+
detail=f"Google authentication failed: {str(e)}"
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
user_info = token.get("userinfo")
|
| 168 |
+
if not user_info:
|
| 169 |
+
raise HTTPException(
|
| 170 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 171 |
+
detail="Failed to retrieve user info from Google"
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
email = user_info.get("email")
|
| 175 |
+
name = user_info.get("name")
|
| 176 |
+
picture = user_info.get("picture")
|
| 177 |
+
google_id = user_info.get("sub")
|
| 178 |
+
|
| 179 |
+
# Find user by google_id or email
|
| 180 |
+
user = db.query(UserModel).filter(
|
| 181 |
+
(UserModel.google_id == google_id) | (UserModel.email == email)
|
| 182 |
+
).first()
|
| 183 |
+
|
| 184 |
+
if not user:
|
| 185 |
+
# Register new user
|
| 186 |
+
user = UserModel(
|
| 187 |
+
id=uuid.uuid4().hex,
|
| 188 |
+
email=email,
|
| 189 |
+
name=name or email.split("@")[0],
|
| 190 |
+
avatar_url=picture or f"https://api.dicebear.com/7.x/bottts/svg?seed={email}",
|
| 191 |
+
google_id=google_id
|
| 192 |
+
)
|
| 193 |
+
db.add(user)
|
| 194 |
+
db.commit()
|
| 195 |
+
db.refresh(user)
|
| 196 |
+
elif not user.google_id:
|
| 197 |
+
# Link existing email account to google
|
| 198 |
+
user.google_id = google_id
|
| 199 |
+
if picture and not user.avatar_url:
|
| 200 |
+
user.avatar_url = picture
|
| 201 |
+
db.commit()
|
| 202 |
+
db.refresh(user)
|
| 203 |
+
|
| 204 |
+
# Generate JWT
|
| 205 |
+
jwt_token = create_access_token(data={"sub": user.id})
|
| 206 |
+
|
| 207 |
+
# Return HTML payload that uses postMessage to send token back to react app
|
| 208 |
+
html_content = f"""
|
| 209 |
+
<!DOCTYPE html>
|
| 210 |
+
<html>
|
| 211 |
+
<head>
|
| 212 |
+
<title>Authenticating...</title>
|
| 213 |
+
</head>
|
| 214 |
+
<body>
|
| 215 |
+
<script>
|
| 216 |
+
window.opener.postMessage({{ type: "AUTH_SUCCESS", token: "{jwt_token}" }}, "*");
|
| 217 |
+
window.close();
|
| 218 |
+
</script>
|
| 219 |
+
<p>Authentication complete. You can close this window now.</p>
|
| 220 |
+
</body>
|
| 221 |
+
</html>
|
| 222 |
+
"""
|
| 223 |
+
return HTMLResponse(content=html_content)
|
| 224 |
+
|
| 225 |
+
@router.get("/me", response_model=UserResponse)
|
| 226 |
+
def get_me(user: UserModel = Depends(get_current_user)):
|
| 227 |
+
return user
|
app/main.py
CHANGED
|
@@ -12,7 +12,9 @@ print("----------------------------")
|
|
| 12 |
from fastapi import FastAPI, Response # type: ignore
|
| 13 |
from fastapi.middleware.cors import CORSMiddleware # type: ignore
|
| 14 |
from prometheus_client import CollectorRegistry, multiprocess, generate_latest, CONTENT_TYPE_LATEST
|
|
|
|
| 15 |
from app.api.routes import router as cases_router
|
|
|
|
| 16 |
|
| 17 |
app = FastAPI(
|
| 18 |
title="Multi-Agent Investigator Engine",
|
|
@@ -20,6 +22,11 @@ app = FastAPI(
|
|
| 20 |
version="0.1.0"
|
| 21 |
)
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
app.add_middleware(
|
| 24 |
CORSMiddleware,
|
| 25 |
allow_origins=["*"],
|
|
@@ -30,6 +37,7 @@ app.add_middleware(
|
|
| 30 |
|
| 31 |
# Register routes
|
| 32 |
app.include_router(cases_router)
|
|
|
|
| 33 |
|
| 34 |
# 2. Expose aggregate multi-process metrics endpoint
|
| 35 |
@app.get("/metrics/")
|
|
|
|
| 12 |
from fastapi import FastAPI, Response # type: ignore
|
| 13 |
from fastapi.middleware.cors import CORSMiddleware # type: ignore
|
| 14 |
from prometheus_client import CollectorRegistry, multiprocess, generate_latest, CONTENT_TYPE_LATEST
|
| 15 |
+
from starlette.middleware.sessions import SessionMiddleware # type: ignore
|
| 16 |
from app.api.routes import router as cases_router
|
| 17 |
+
from app.api.auth import router as auth_router
|
| 18 |
|
| 19 |
app = FastAPI(
|
| 20 |
title="Multi-Agent Investigator Engine",
|
|
|
|
| 22 |
version="0.1.0"
|
| 23 |
)
|
| 24 |
|
| 25 |
+
app.add_middleware(
|
| 26 |
+
SessionMiddleware,
|
| 27 |
+
secret_key=os.getenv("SESSION_SECRET_KEY", "super-secret-session-key-for-oauth")
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
app.add_middleware(
|
| 31 |
CORSMiddleware,
|
| 32 |
allow_origins=["*"],
|
|
|
|
| 37 |
|
| 38 |
# Register routes
|
| 39 |
app.include_router(cases_router)
|
| 40 |
+
app.include_router(auth_router)
|
| 41 |
|
| 42 |
# 2. Expose aggregate multi-process metrics endpoint
|
| 43 |
@app.get("/metrics/")
|
app/models/database_models.py
CHANGED
|
@@ -3,16 +3,32 @@ from sqlalchemy.orm import relationship
|
|
| 3 |
from datetime import datetime
|
| 4 |
from app.services.database import Base
|
| 5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
class CaseContextModel(Base):
|
| 7 |
__tablename__= "cases"
|
| 8 |
|
| 9 |
case_id= Column(String, primary_key= True, index= True)
|
|
|
|
| 10 |
problem_statement= Column(String, nullable= False)
|
| 11 |
status= Column(String, default= "pending", nullable= False)
|
| 12 |
metadata_json= Column(JSON, default= dict)
|
| 13 |
updated_at= Column(DateTime, default= datetime.utcnow, onupdate= datetime.utcnow)
|
| 14 |
|
| 15 |
# Relationships
|
|
|
|
| 16 |
facts= relationship("FactModel", back_populates= "case", cascade= "all, delete-orphan")
|
| 17 |
hypotheses= relationship("HypothesisModel", back_populates= "case", cascade= "all, delete-orphan")
|
| 18 |
evidence= relationship("EvidenceModel", back_populates= "case", cascade= "all, delete-orphan")
|
|
|
|
| 3 |
from datetime import datetime
|
| 4 |
from app.services.database import Base
|
| 5 |
|
| 6 |
+
class UserModel(Base):
|
| 7 |
+
__tablename__ = "users"
|
| 8 |
+
|
| 9 |
+
id = Column(String, primary_key=True, index=True)
|
| 10 |
+
email = Column(String, unique=True, index=True, nullable=False)
|
| 11 |
+
name = Column(String, nullable=True)
|
| 12 |
+
avatar_url = Column(String, nullable=True)
|
| 13 |
+
google_id = Column(String, unique=True, index=True, nullable=True)
|
| 14 |
+
hashed_password = Column(String, nullable=True)
|
| 15 |
+
created_at = Column(DateTime, default=datetime.utcnow)
|
| 16 |
+
|
| 17 |
+
cases = relationship("CaseContextModel", back_populates="user", cascade="all, delete-orphan")
|
| 18 |
+
|
| 19 |
+
|
| 20 |
class CaseContextModel(Base):
|
| 21 |
__tablename__= "cases"
|
| 22 |
|
| 23 |
case_id= Column(String, primary_key= True, index= True)
|
| 24 |
+
user_id= Column(String, ForeignKey("users.id", ondelete="CASCADE"), nullable=True)
|
| 25 |
problem_statement= Column(String, nullable= False)
|
| 26 |
status= Column(String, default= "pending", nullable= False)
|
| 27 |
metadata_json= Column(JSON, default= dict)
|
| 28 |
updated_at= Column(DateTime, default= datetime.utcnow, onupdate= datetime.utcnow)
|
| 29 |
|
| 30 |
# Relationships
|
| 31 |
+
user= relationship("UserModel", back_populates="cases")
|
| 32 |
facts= relationship("FactModel", back_populates= "case", cascade= "all, delete-orphan")
|
| 33 |
hypotheses= relationship("HypothesisModel", back_populates= "case", cascade= "all, delete-orphan")
|
| 34 |
evidence= relationship("EvidenceModel", back_populates= "case", cascade= "all, delete-orphan")
|
frontend/src/App.jsx
CHANGED
|
@@ -1,10 +1,14 @@
|
|
| 1 |
-
import React from "react";
|
| 2 |
import Navbar from "./components/Navbar";
|
| 3 |
import HeroSection from "./sections/HeroSection";
|
| 4 |
import CapabilitiesSection from "./sections/CapabilitiesSection";
|
| 5 |
import FadingVideo from "./components/FadingVideo";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
-
export default function App() {
|
| 8 |
return (
|
| 9 |
<div className="relative w-screen bg-black text-white selection:bg-white selection:text-black min-h-screen">
|
| 10 |
{/* Shared Background Video Layer */}
|
|
@@ -17,13 +21,24 @@ export default function App() {
|
|
| 17 |
</div>
|
| 18 |
|
| 19 |
{/* Shared Navigation Header */}
|
| 20 |
-
<Navbar />
|
| 21 |
|
| 22 |
{/* Hero Section (Section 1) */}
|
| 23 |
-
<HeroSection />
|
| 24 |
|
| 25 |
{/* Capabilities Section (Section 2) */}
|
| 26 |
<CapabilitiesSection />
|
|
|
|
|
|
|
|
|
|
| 27 |
</div>
|
| 28 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
}
|
|
|
|
| 1 |
+
import React, { useState } from "react";
|
| 2 |
import Navbar from "./components/Navbar";
|
| 3 |
import HeroSection from "./sections/HeroSection";
|
| 4 |
import CapabilitiesSection from "./sections/CapabilitiesSection";
|
| 5 |
import FadingVideo from "./components/FadingVideo";
|
| 6 |
+
import { AuthProvider } from "./context/AuthContext";
|
| 7 |
+
import AuthModal from "./components/AuthModal";
|
| 8 |
+
|
| 9 |
+
function MainAppContent() {
|
| 10 |
+
const [isAuthModalOpen, setIsAuthModalOpen] = useState(false);
|
| 11 |
|
|
|
|
| 12 |
return (
|
| 13 |
<div className="relative w-screen bg-black text-white selection:bg-white selection:text-black min-h-screen">
|
| 14 |
{/* Shared Background Video Layer */}
|
|
|
|
| 21 |
</div>
|
| 22 |
|
| 23 |
{/* Shared Navigation Header */}
|
| 24 |
+
<Navbar onOpenAuth={() => setIsAuthModalOpen(true)} />
|
| 25 |
|
| 26 |
{/* Hero Section (Section 1) */}
|
| 27 |
+
<HeroSection onOpenAuth={() => setIsAuthModalOpen(true)} />
|
| 28 |
|
| 29 |
{/* Capabilities Section (Section 2) */}
|
| 30 |
<CapabilitiesSection />
|
| 31 |
+
|
| 32 |
+
{/* Auth Modal */}
|
| 33 |
+
<AuthModal isOpen={isAuthModalOpen} onClose={() => setIsAuthModalOpen(false)} />
|
| 34 |
</div>
|
| 35 |
);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
export default function App() {
|
| 39 |
+
return (
|
| 40 |
+
<AuthProvider>
|
| 41 |
+
<MainAppContent />
|
| 42 |
+
</AuthProvider>
|
| 43 |
+
);
|
| 44 |
}
|
frontend/src/components/AuthModal.jsx
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from "react";
|
| 2 |
+
import { motion, AnimatePresence } from "framer-motion";
|
| 3 |
+
import { useAuth } from "../context/AuthContext";
|
| 4 |
+
|
| 5 |
+
export default function AuthModal({ isOpen, onClose }) {
|
| 6 |
+
const { login, register, loginWithGoogle } = useAuth();
|
| 7 |
+
const [isLoginView, setIsLoginView] = useState(true);
|
| 8 |
+
const [email, setEmail] = useState("");
|
| 9 |
+
const [password, setPassword] = useState("");
|
| 10 |
+
const [name, setName] = useState("");
|
| 11 |
+
const [error, setError] = useState("");
|
| 12 |
+
const [loading, setLoading] = useState(false);
|
| 13 |
+
|
| 14 |
+
if (!isOpen) return null;
|
| 15 |
+
|
| 16 |
+
const handleSubmit = async (e) => {
|
| 17 |
+
e.preventDefault();
|
| 18 |
+
setError("");
|
| 19 |
+
setLoading(true);
|
| 20 |
+
|
| 21 |
+
let res;
|
| 22 |
+
if (isLoginView) {
|
| 23 |
+
res = await login(email, password);
|
| 24 |
+
} else {
|
| 25 |
+
res = await register(email, password, name);
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
setLoading(false);
|
| 29 |
+
if (res.success) {
|
| 30 |
+
onClose();
|
| 31 |
+
} else {
|
| 32 |
+
setError(res.error || "Authentication failed");
|
| 33 |
+
}
|
| 34 |
+
};
|
| 35 |
+
|
| 36 |
+
const handleGoogleClick = async () => {
|
| 37 |
+
setError("");
|
| 38 |
+
setLoading(true);
|
| 39 |
+
const res = await loginWithGoogle();
|
| 40 |
+
setLoading(false);
|
| 41 |
+
if (res.success) {
|
| 42 |
+
onClose();
|
| 43 |
+
} else if (res.error) {
|
| 44 |
+
setError(res.error);
|
| 45 |
+
}
|
| 46 |
+
};
|
| 47 |
+
|
| 48 |
+
return (
|
| 49 |
+
<AnimatePresence>
|
| 50 |
+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
| 51 |
+
{/* Backdrop */}
|
| 52 |
+
<motion.div
|
| 53 |
+
initial={{ opacity: 0 }}
|
| 54 |
+
animate={{ opacity: 1 }}
|
| 55 |
+
exit={{ opacity: 0 }}
|
| 56 |
+
onClick={onClose}
|
| 57 |
+
className="fixed inset-0 bg-black/75 backdrop-blur-md"
|
| 58 |
+
/>
|
| 59 |
+
|
| 60 |
+
{/* Modal Card */}
|
| 61 |
+
<motion.div
|
| 62 |
+
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
| 63 |
+
animate={{ opacity: 1, scale: 1, y: 0 }}
|
| 64 |
+
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
| 65 |
+
transition={{ type: "spring", duration: 0.5 }}
|
| 66 |
+
className="relative w-full max-w-md overflow-hidden rounded-2xl border border-white/10 bg-black/40 p-8 text-white shadow-2xl backdrop-blur-xl"
|
| 67 |
+
style={{
|
| 68 |
+
boxShadow: "0 8px 32px 0 rgba(0, 0, 0, 0.37)",
|
| 69 |
+
}}
|
| 70 |
+
>
|
| 71 |
+
{/* Close button */}
|
| 72 |
+
<button
|
| 73 |
+
onClick={onClose}
|
| 74 |
+
className="absolute top-4 right-4 text-white/50 hover:text-white transition-colors"
|
| 75 |
+
>
|
| 76 |
+
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 77 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
| 78 |
+
</svg>
|
| 79 |
+
</button>
|
| 80 |
+
|
| 81 |
+
{/* Title / Description */}
|
| 82 |
+
<div className="mb-6 text-center">
|
| 83 |
+
<h2 className="text-2xl font-bold tracking-tight text-white mb-2">
|
| 84 |
+
{isLoginView ? "Begin Investigation" : "Create Security Profile"}
|
| 85 |
+
</h2>
|
| 86 |
+
<p className="text-sm text-white/60">
|
| 87 |
+
{isLoginView
|
| 88 |
+
? "Sign in to deploy the Multi-Agent engine"
|
| 89 |
+
: "Register credentials to access shared context"}
|
| 90 |
+
</p>
|
| 91 |
+
</div>
|
| 92 |
+
|
| 93 |
+
{/* Social Sign-In (Google) */}
|
| 94 |
+
<button
|
| 95 |
+
onClick={handleGoogleClick}
|
| 96 |
+
disabled={loading}
|
| 97 |
+
className="flex w-full items-center justify-center gap-3 rounded-lg border border-white/15 bg-white/[0.04] py-3 text-sm font-semibold text-white transition duration-300 hover:bg-white/[0.08] active:bg-white/[0.12] disabled:opacity-50"
|
| 98 |
+
>
|
| 99 |
+
<svg className="h-5 w-5" viewBox="0 0 24 24">
|
| 100 |
+
<path
|
| 101 |
+
fill="currentColor"
|
| 102 |
+
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
| 103 |
+
/>
|
| 104 |
+
<path
|
| 105 |
+
fill="currentColor"
|
| 106 |
+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
| 107 |
+
/>
|
| 108 |
+
<path
|
| 109 |
+
fill="currentColor"
|
| 110 |
+
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.06H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.94l2.85-2.22.81-.63z"
|
| 111 |
+
/>
|
| 112 |
+
<path
|
| 113 |
+
fill="currentColor"
|
| 114 |
+
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.06l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
| 115 |
+
/>
|
| 116 |
+
</svg>
|
| 117 |
+
Continue with Google
|
| 118 |
+
</button>
|
| 119 |
+
|
| 120 |
+
{/* Divider */}
|
| 121 |
+
<div className="my-6 flex items-center justify-between">
|
| 122 |
+
<span className="h-px w-[35%] bg-white/10"></span>
|
| 123 |
+
<span className="text-xs uppercase tracking-widest text-white/40 font-medium">Or Use Credentials</span>
|
| 124 |
+
<span className="h-px w-[35%] bg-white/10"></span>
|
| 125 |
+
</div>
|
| 126 |
+
|
| 127 |
+
{/* Form */}
|
| 128 |
+
<form onSubmit={handleSubmit} className="space-y-4">
|
| 129 |
+
{!isLoginView && (
|
| 130 |
+
<div>
|
| 131 |
+
<label className="block text-xs font-semibold uppercase tracking-wider text-white/50 mb-1.5">
|
| 132 |
+
Full Name
|
| 133 |
+
</label>
|
| 134 |
+
<input
|
| 135 |
+
type="text"
|
| 136 |
+
required
|
| 137 |
+
value={name}
|
| 138 |
+
onChange={(e) => setName(e.target.value)}
|
| 139 |
+
placeholder="Karan Shelar"
|
| 140 |
+
className="w-full rounded-lg border border-white/10 bg-white/[0.03] px-4 py-2.5 text-sm text-white placeholder-white/30 focus:border-white/30 focus:outline-none transition-colors"
|
| 141 |
+
/>
|
| 142 |
+
</div>
|
| 143 |
+
)}
|
| 144 |
+
|
| 145 |
+
<div>
|
| 146 |
+
<label className="block text-xs font-semibold uppercase tracking-wider text-white/50 mb-1.5">
|
| 147 |
+
Email Address
|
| 148 |
+
</label>
|
| 149 |
+
<input
|
| 150 |
+
type="email"
|
| 151 |
+
required
|
| 152 |
+
value={email}
|
| 153 |
+
onChange={(e) => setEmail(e.target.value)}
|
| 154 |
+
placeholder="agent@agentbond.ai"
|
| 155 |
+
className="w-full rounded-lg border border-white/10 bg-white/[0.03] px-4 py-2.5 text-sm text-white placeholder-white/30 focus:border-white/30 focus:outline-none transition-colors"
|
| 156 |
+
/>
|
| 157 |
+
</div>
|
| 158 |
+
|
| 159 |
+
<div>
|
| 160 |
+
<label className="block text-xs font-semibold uppercase tracking-wider text-white/50 mb-1.5">
|
| 161 |
+
Security Password
|
| 162 |
+
</label>
|
| 163 |
+
<input
|
| 164 |
+
type="password"
|
| 165 |
+
required
|
| 166 |
+
value={password}
|
| 167 |
+
onChange={(e) => setPassword(e.target.value)}
|
| 168 |
+
placeholder="••••••••••••"
|
| 169 |
+
className="w-full rounded-lg border border-white/10 bg-white/[0.03] px-4 py-2.5 text-sm text-white placeholder-white/30 focus:border-white/30 focus:outline-none transition-colors"
|
| 170 |
+
/>
|
| 171 |
+
</div>
|
| 172 |
+
|
| 173 |
+
{error && <div className="text-sm text-red-400 font-medium text-center">{error}</div>}
|
| 174 |
+
|
| 175 |
+
<button
|
| 176 |
+
type="submit"
|
| 177 |
+
disabled={loading}
|
| 178 |
+
className="w-full rounded-lg bg-white text-black py-3 text-sm font-semibold tracking-wide hover:bg-white/90 transition-all duration-300 active:scale-[0.98] disabled:opacity-50"
|
| 179 |
+
>
|
| 180 |
+
{loading ? "Decrypting..." : isLoginView ? "Sign In" : "Register"}
|
| 181 |
+
</button>
|
| 182 |
+
</form>
|
| 183 |
+
|
| 184 |
+
{/* Toggle View Link */}
|
| 185 |
+
<div className="mt-6 text-center text-sm text-white/60">
|
| 186 |
+
{isLoginView ? "New Investigator?" : "Already registered?"}{" "}
|
| 187 |
+
<button
|
| 188 |
+
onClick={() => {
|
| 189 |
+
setIsLoginView(!isLoginView);
|
| 190 |
+
setError("");
|
| 191 |
+
}}
|
| 192 |
+
className="text-white hover:underline font-semibold"
|
| 193 |
+
>
|
| 194 |
+
{isLoginView ? "Create a Profile" : "Sign In Here"}
|
| 195 |
+
</button>
|
| 196 |
+
</div>
|
| 197 |
+
</motion.div>
|
| 198 |
+
</div>
|
| 199 |
+
</AnimatePresence>
|
| 200 |
+
);
|
| 201 |
+
}
|
frontend/src/components/Navbar.jsx
CHANGED
|
@@ -1,17 +1,12 @@
|
|
| 1 |
import React, { useState, useEffect } from "react";
|
| 2 |
import { motion, AnimatePresence } from "framer-motion";
|
|
|
|
| 3 |
|
| 4 |
const NAV_LINKS = [
|
| 5 |
{ label: "Home", href: "#home" },
|
| 6 |
{ label: "Capabilities", href: "#capabilities" },
|
| 7 |
];
|
| 8 |
|
| 9 |
-
// Placeholder: replace with real Google OAuth + JWT flow later
|
| 10 |
-
const handleBeginInvestigation = () => {
|
| 11 |
-
// TODO: trigger Google OAuth login → issue JWT → redirect to dashboard
|
| 12 |
-
alert("Google OAuth coming soon! 🔐");
|
| 13 |
-
};
|
| 14 |
-
|
| 15 |
// SVG Icons
|
| 16 |
const GithubIcon = () => (
|
| 17 |
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
@@ -47,7 +42,8 @@ const CloseIcon = () => (
|
|
| 47 |
</svg>
|
| 48 |
);
|
| 49 |
|
| 50 |
-
export default function Navbar() {
|
|
|
|
| 51 |
const [menuOpen, setMenuOpen] = useState(false);
|
| 52 |
const [scrolled, setScrolled] = useState(false);
|
| 53 |
|
|
@@ -97,17 +93,33 @@ export default function Navbar() {
|
|
| 97 |
</a>
|
| 98 |
))}
|
| 99 |
|
| 100 |
-
{
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
<
|
| 108 |
-
<
|
| 109 |
-
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
</div>
|
| 112 |
</div>
|
| 113 |
|
|
@@ -208,12 +220,40 @@ export default function Navbar() {
|
|
| 208 |
|
| 209 |
<div className="h-px bg-white/10 my-2" />
|
| 210 |
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
</motion.div>
|
| 218 |
)}
|
| 219 |
</AnimatePresence>
|
|
|
|
| 1 |
import React, { useState, useEffect } from "react";
|
| 2 |
import { motion, AnimatePresence } from "framer-motion";
|
| 3 |
+
import { useAuth } from "../context/AuthContext";
|
| 4 |
|
| 5 |
const NAV_LINKS = [
|
| 6 |
{ label: "Home", href: "#home" },
|
| 7 |
{ label: "Capabilities", href: "#capabilities" },
|
| 8 |
];
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
// SVG Icons
|
| 11 |
const GithubIcon = () => (
|
| 12 |
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
|
|
| 42 |
</svg>
|
| 43 |
);
|
| 44 |
|
| 45 |
+
export default function Navbar({ onOpenAuth }) {
|
| 46 |
+
const { user, logout } = useAuth();
|
| 47 |
const [menuOpen, setMenuOpen] = useState(false);
|
| 48 |
const [scrolled, setScrolled] = useState(false);
|
| 49 |
|
|
|
|
| 93 |
</a>
|
| 94 |
))}
|
| 95 |
|
| 96 |
+
{user ? (
|
| 97 |
+
<div className="flex items-center gap-2 ml-1">
|
| 98 |
+
<img
|
| 99 |
+
src={user.avatar_url}
|
| 100 |
+
alt={user.name}
|
| 101 |
+
className="w-7 h-7 rounded-full border border-white/20"
|
| 102 |
+
/>
|
| 103 |
+
<span className="text-xs font-semibold max-w-[80px] truncate">{user.name}</span>
|
| 104 |
+
<button
|
| 105 |
+
onClick={logout}
|
| 106 |
+
className="text-xs text-white/50 hover:text-white underline cursor-pointer ml-1"
|
| 107 |
+
>
|
| 108 |
+
Logout
|
| 109 |
+
</button>
|
| 110 |
+
</div>
|
| 111 |
+
) : (
|
| 112 |
+
<button
|
| 113 |
+
onClick={onOpenAuth}
|
| 114 |
+
className="flex items-center gap-1.5 bg-white text-black px-4 py-2 text-sm font-semibold rounded-full hover:bg-white/90 active:scale-95 transition-all whitespace-nowrap ml-1"
|
| 115 |
+
>
|
| 116 |
+
Begin Investigation
|
| 117 |
+
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
| 118 |
+
<circle cx="11" cy="11" r="7" />
|
| 119 |
+
<line x1="16.5" y1="16.5" x2="21" y2="21" />
|
| 120 |
+
</svg>
|
| 121 |
+
</button>
|
| 122 |
+
)}
|
| 123 |
</div>
|
| 124 |
</div>
|
| 125 |
|
|
|
|
| 220 |
|
| 221 |
<div className="h-px bg-white/10 my-2" />
|
| 222 |
|
| 223 |
+
{user ? (
|
| 224 |
+
<div className="flex flex-col gap-2 p-2 rounded-xl border border-white/5 bg-white/[0.02]">
|
| 225 |
+
<div className="flex items-center gap-3 px-2 py-1">
|
| 226 |
+
<img
|
| 227 |
+
src={user.avatar_url}
|
| 228 |
+
alt={user.name}
|
| 229 |
+
className="w-8 h-8 rounded-full border border-white/20"
|
| 230 |
+
/>
|
| 231 |
+
<div className="flex flex-col">
|
| 232 |
+
<span className="text-sm font-semibold">{user.name}</span>
|
| 233 |
+
<span className="text-xs text-white/50">{user.email}</span>
|
| 234 |
+
</div>
|
| 235 |
+
</div>
|
| 236 |
+
<button
|
| 237 |
+
onClick={() => {
|
| 238 |
+
logout();
|
| 239 |
+
setMenuOpen(false);
|
| 240 |
+
}}
|
| 241 |
+
className="w-full flex items-center justify-center gap-2 bg-white/10 hover:bg-white/15 text-white py-2 text-sm font-semibold rounded-lg transition-all"
|
| 242 |
+
>
|
| 243 |
+
Logout
|
| 244 |
+
</button>
|
| 245 |
+
</div>
|
| 246 |
+
) : (
|
| 247 |
+
<button
|
| 248 |
+
onClick={() => {
|
| 249 |
+
onOpenAuth();
|
| 250 |
+
setMenuOpen(false);
|
| 251 |
+
}}
|
| 252 |
+
className="flex items-center justify-center gap-2 bg-white text-black px-4 py-2.5 text-sm font-semibold rounded-full hover:bg-white/90 transition-all"
|
| 253 |
+
>
|
| 254 |
+
Begin Investigation 🔍
|
| 255 |
+
</button>
|
| 256 |
+
)}
|
| 257 |
</motion.div>
|
| 258 |
)}
|
| 259 |
</AnimatePresence>
|
frontend/src/context/AuthContext.jsx
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { createContext, useContext, useState, useEffect } from "react";
|
| 2 |
+
|
| 3 |
+
const AuthContext = createContext(null);
|
| 4 |
+
|
| 5 |
+
const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:8000";
|
| 6 |
+
|
| 7 |
+
export function AuthProvider({ children }) {
|
| 8 |
+
const [user, setUser] = useState(null);
|
| 9 |
+
const [token, setToken] = useState(localStorage.getItem("token"));
|
| 10 |
+
const [loading, setLoading] = useState(true);
|
| 11 |
+
|
| 12 |
+
// Fetch current user details
|
| 13 |
+
const fetchUser = async (authToken) => {
|
| 14 |
+
try {
|
| 15 |
+
const response = await fetch(`${API_BASE_URL}/api/auth/me`, {
|
| 16 |
+
headers: {
|
| 17 |
+
Authorization: `Bearer ${authToken}`,
|
| 18 |
+
},
|
| 19 |
+
});
|
| 20 |
+
if (response.ok) {
|
| 21 |
+
const userData = await response.json();
|
| 22 |
+
setUser(userData);
|
| 23 |
+
} else {
|
| 24 |
+
logout();
|
| 25 |
+
}
|
| 26 |
+
} catch (error) {
|
| 27 |
+
console.error("Failed to fetch user:", error);
|
| 28 |
+
logout();
|
| 29 |
+
} finally {
|
| 30 |
+
setLoading(false);
|
| 31 |
+
}
|
| 32 |
+
};
|
| 33 |
+
|
| 34 |
+
useEffect(() => {
|
| 35 |
+
if (token) {
|
| 36 |
+
fetchUser(token);
|
| 37 |
+
} else {
|
| 38 |
+
setLoading(false);
|
| 39 |
+
}
|
| 40 |
+
}, [token]);
|
| 41 |
+
|
| 42 |
+
// Login with email and password
|
| 43 |
+
const login = async (email, password) => {
|
| 44 |
+
setLoading(true);
|
| 45 |
+
try {
|
| 46 |
+
const response = await fetch(`${API_BASE_URL}/api/auth/login`, {
|
| 47 |
+
method: "POST",
|
| 48 |
+
headers: {
|
| 49 |
+
"Content-Type": "application/json",
|
| 50 |
+
},
|
| 51 |
+
body: JSON.stringify({ email, password }),
|
| 52 |
+
});
|
| 53 |
+
|
| 54 |
+
const data = await response.json();
|
| 55 |
+
if (!response.ok) {
|
| 56 |
+
throw new Error(data.detail || "Login failed");
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
localStorage.setItem("token", data.access_token);
|
| 60 |
+
setToken(data.access_token);
|
| 61 |
+
await fetchUser(data.access_token);
|
| 62 |
+
return { success: true };
|
| 63 |
+
} catch (error) {
|
| 64 |
+
setLoading(false);
|
| 65 |
+
return { success: false, error: error.message };
|
| 66 |
+
}
|
| 67 |
+
};
|
| 68 |
+
|
| 69 |
+
// Register with email and password
|
| 70 |
+
const register = async (email, password, name) => {
|
| 71 |
+
setLoading(true);
|
| 72 |
+
try {
|
| 73 |
+
const response = await fetch(`${API_BASE_URL}/api/auth/register`, {
|
| 74 |
+
method: "POST",
|
| 75 |
+
headers: {
|
| 76 |
+
"Content-Type": "application/json",
|
| 77 |
+
},
|
| 78 |
+
body: JSON.stringify({ email, password, name }),
|
| 79 |
+
});
|
| 80 |
+
|
| 81 |
+
const data = await response.json();
|
| 82 |
+
if (!response.ok) {
|
| 83 |
+
throw new Error(data.detail || "Registration failed");
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
localStorage.setItem("token", data.access_token);
|
| 87 |
+
setToken(data.access_token);
|
| 88 |
+
await fetchUser(data.access_token);
|
| 89 |
+
return { success: true };
|
| 90 |
+
} catch (error) {
|
| 91 |
+
setLoading(false);
|
| 92 |
+
return { success: false, error: error.message };
|
| 93 |
+
}
|
| 94 |
+
};
|
| 95 |
+
|
| 96 |
+
// Login with Google (OAuth flow in a Popup)
|
| 97 |
+
const loginWithGoogle = () => {
|
| 98 |
+
return new Promise((resolve, reject) => {
|
| 99 |
+
const width = 500;
|
| 100 |
+
const height = 600;
|
| 101 |
+
const left = window.screen.width / 2 - width / 2;
|
| 102 |
+
const top = window.screen.height / 2 - height / 2;
|
| 103 |
+
|
| 104 |
+
const popup = window.open(
|
| 105 |
+
`${API_BASE_URL}/api/auth/google`,
|
| 106 |
+
"Google Sign In - AgentBond AI",
|
| 107 |
+
`width=${width},height=${height},top=${top},left=${left}`
|
| 108 |
+
);
|
| 109 |
+
|
| 110 |
+
const handleMessage = async (event) => {
|
| 111 |
+
// Validate origin if production has specific domain
|
| 112 |
+
if (event.data && event.data.type === "AUTH_SUCCESS") {
|
| 113 |
+
const googleToken = event.data.token;
|
| 114 |
+
localStorage.setItem("token", googleToken);
|
| 115 |
+
setToken(googleToken);
|
| 116 |
+
await fetchUser(googleToken);
|
| 117 |
+
window.removeEventListener("message", handleMessage);
|
| 118 |
+
resolve({ success: true });
|
| 119 |
+
}
|
| 120 |
+
};
|
| 121 |
+
|
| 122 |
+
window.addEventListener("message", handleMessage);
|
| 123 |
+
|
| 124 |
+
// Check if popup is closed before completing
|
| 125 |
+
const checkPopupClosed = setInterval(() => {
|
| 126 |
+
if (!popup || popup.closed) {
|
| 127 |
+
clearInterval(checkPopupClosed);
|
| 128 |
+
window.removeEventListener("message", handleMessage);
|
| 129 |
+
resolve({ success: false, error: "Popup closed by user" });
|
| 130 |
+
}
|
| 131 |
+
}, 1000);
|
| 132 |
+
});
|
| 133 |
+
};
|
| 134 |
+
|
| 135 |
+
// Logout
|
| 136 |
+
const logout = () => {
|
| 137 |
+
localStorage.removeItem("token");
|
| 138 |
+
setToken(null);
|
| 139 |
+
setUser(null);
|
| 140 |
+
};
|
| 141 |
+
|
| 142 |
+
return (
|
| 143 |
+
<AuthContext.Provider
|
| 144 |
+
value={{
|
| 145 |
+
user,
|
| 146 |
+
token,
|
| 147 |
+
loading,
|
| 148 |
+
login,
|
| 149 |
+
register,
|
| 150 |
+
loginWithGoogle,
|
| 151 |
+
logout,
|
| 152 |
+
}}
|
| 153 |
+
>
|
| 154 |
+
{children}
|
| 155 |
+
</AuthContext.Provider>
|
| 156 |
+
);
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
export function useAuth() {
|
| 160 |
+
const context = useContext(AuthContext);
|
| 161 |
+
if (!context) {
|
| 162 |
+
throw new Error("useAuth must be used within an AuthProvider");
|
| 163 |
+
}
|
| 164 |
+
return context;
|
| 165 |
+
}
|
frontend/src/sections/HeroSection.jsx
CHANGED
|
@@ -1,13 +1,10 @@
|
|
| 1 |
import React from "react";
|
| 2 |
import { motion } from "framer-motion";
|
| 3 |
import BlurText from "../components/BlurText";
|
|
|
|
| 4 |
|
| 5 |
-
|
| 6 |
-
const
|
| 7 |
-
alert("Google OAuth coming soon! 🔐");
|
| 8 |
-
};
|
| 9 |
-
|
| 10 |
-
export default function HeroSection() {
|
| 11 |
const containerVariants = {
|
| 12 |
initial: {},
|
| 13 |
animate: {
|
|
@@ -110,14 +107,20 @@ export default function HeroSection() {
|
|
| 110 |
{/* Single CTA — centered */}
|
| 111 |
<motion.div variants={itemVariants} className="mt-9 flex justify-center">
|
| 112 |
<button
|
| 113 |
-
onClick={
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
className="flex items-center gap-2.5 rounded-full px-7 py-3.5 text-sm font-semibold text-white liquid-glass-strong hover:scale-105 active:scale-95 transition-all duration-200 shadow-lg"
|
| 115 |
>
|
| 116 |
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
|
| 117 |
<circle cx="11" cy="11" r="7" />
|
| 118 |
<line x1="16.5" y1="16.5" x2="21" y2="21" />
|
| 119 |
</svg>
|
| 120 |
-
Begin Investigation
|
| 121 |
</button>
|
| 122 |
</motion.div>
|
| 123 |
|
|
|
|
| 1 |
import React from "react";
|
| 2 |
import { motion } from "framer-motion";
|
| 3 |
import BlurText from "../components/BlurText";
|
| 4 |
+
import { useAuth } from "../context/AuthContext";
|
| 5 |
|
| 6 |
+
export default function HeroSection({ onOpenAuth }) {
|
| 7 |
+
const { user } = useAuth();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
const containerVariants = {
|
| 9 |
initial: {},
|
| 10 |
animate: {
|
|
|
|
| 107 |
{/* Single CTA — centered */}
|
| 108 |
<motion.div variants={itemVariants} className="mt-9 flex justify-center">
|
| 109 |
<button
|
| 110 |
+
onClick={() => {
|
| 111 |
+
if (user) {
|
| 112 |
+
alert(`Welcome back, ${user.name || 'Agent'}. Accessing secure investigation node...`);
|
| 113 |
+
} else {
|
| 114 |
+
onOpenAuth();
|
| 115 |
+
}
|
| 116 |
+
}}
|
| 117 |
className="flex items-center gap-2.5 rounded-full px-7 py-3.5 text-sm font-semibold text-white liquid-glass-strong hover:scale-105 active:scale-95 transition-all duration-200 shadow-lg"
|
| 118 |
>
|
| 119 |
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
|
| 120 |
<circle cx="11" cy="11" r="7" />
|
| 121 |
<line x1="16.5" y1="16.5" x2="21" y2="21" />
|
| 122 |
</svg>
|
| 123 |
+
{user ? "Deploy Agent Pipeline" : "Begin Investigation"}
|
| 124 |
</button>
|
| 125 |
</motion.div>
|
| 126 |
|
pyproject.toml
CHANGED
|
@@ -17,6 +17,11 @@ dependencies = [
|
|
| 17 |
"prometheus-client==0.20.0",
|
| 18 |
"python-dotenv==1.0.1",
|
| 19 |
"ddgs>=9.14.3",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
]
|
| 21 |
|
| 22 |
[dependency-groups]
|
|
|
|
| 17 |
"prometheus-client==0.20.0",
|
| 18 |
"python-dotenv==1.0.1",
|
| 19 |
"ddgs>=9.14.3",
|
| 20 |
+
"authlib==1.3.0",
|
| 21 |
+
"python-jose[cryptography]==3.3.0",
|
| 22 |
+
"passlib[bcrypt]==1.7.4",
|
| 23 |
+
"bcrypt==4.1.3",
|
| 24 |
+
"httpx==0.27.0",
|
| 25 |
]
|
| 26 |
|
| 27 |
[dependency-groups]
|
uv.lock
CHANGED
|
@@ -73,6 +73,48 @@ wheels = [
|
|
| 73 |
{ url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" },
|
| 74 |
]
|
| 75 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
[[package]]
|
| 77 |
name = "billiard"
|
| 78 |
version = "4.2.4"
|
|
@@ -410,6 +452,18 @@ wheels = [
|
|
| 410 |
{ url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" },
|
| 411 |
]
|
| 412 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 413 |
[[package]]
|
| 414 |
name = "email-validator"
|
| 415 |
version = "2.3.0"
|
|
@@ -845,15 +899,20 @@ version = "0.1.0"
|
|
| 845 |
source = { editable = "." }
|
| 846 |
dependencies = [
|
| 847 |
{ name = "alembic" },
|
|
|
|
|
|
|
| 848 |
{ name = "celery" },
|
| 849 |
{ name = "ddgs" },
|
| 850 |
{ name = "fastapi" },
|
| 851 |
{ name = "google-generativeai" },
|
|
|
|
|
|
|
| 852 |
{ name = "prometheus-client" },
|
| 853 |
{ name = "psycopg2-binary" },
|
| 854 |
{ name = "pydantic" },
|
| 855 |
{ name = "pydantic-settings" },
|
| 856 |
{ name = "python-dotenv" },
|
|
|
|
| 857 |
{ name = "redis" },
|
| 858 |
{ name = "sqlalchemy" },
|
| 859 |
{ name = "uvicorn", extra = ["standard"] },
|
|
@@ -869,15 +928,20 @@ dev = [
|
|
| 869 |
[package.metadata]
|
| 870 |
requires-dist = [
|
| 871 |
{ name = "alembic", specifier = "==1.13.1" },
|
|
|
|
|
|
|
| 872 |
{ name = "celery", specifier = "==5.3.6" },
|
| 873 |
{ name = "ddgs", specifier = ">=9.14.3" },
|
| 874 |
{ name = "fastapi", specifier = "==0.111.0" },
|
| 875 |
{ name = "google-generativeai", specifier = "==0.5.4" },
|
|
|
|
|
|
|
| 876 |
{ name = "prometheus-client", specifier = "==0.20.0" },
|
| 877 |
{ name = "psycopg2-binary", specifier = "==2.9.9" },
|
| 878 |
{ name = "pydantic", specifier = "==2.7.1" },
|
| 879 |
{ name = "pydantic-settings", specifier = "==2.2.1" },
|
| 880 |
{ name = "python-dotenv", specifier = "==1.0.1" },
|
|
|
|
| 881 |
{ name = "redis", specifier = "==5.0.4" },
|
| 882 |
{ name = "sqlalchemy", specifier = "==2.0.30" },
|
| 883 |
{ name = "uvicorn", extras = ["standard"], specifier = "==0.29.0" },
|
|
@@ -1203,6 +1267,20 @@ wheels = [
|
|
| 1203 |
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
| 1204 |
]
|
| 1205 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1206 |
[[package]]
|
| 1207 |
name = "pluggy"
|
| 1208 |
version = "1.6.0"
|
|
@@ -1489,6 +1567,25 @@ wheels = [
|
|
| 1489 |
{ url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863, upload-time = "2024-01-23T06:32:58.246Z" },
|
| 1490 |
]
|
| 1491 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1492 |
[[package]]
|
| 1493 |
name = "python-multipart"
|
| 1494 |
version = "0.0.29"
|
|
@@ -1607,6 +1704,18 @@ wheels = [
|
|
| 1607 |
{ url = "https://files.pythonhosted.org/packages/35/84/a005adcb4d1e6846ba3d62768090c3b943e3f6d8dc5c47af64f33584c4a7/rich_toolkit-0.19.10-py3-none-any.whl", hash = "sha256:93a41f67a09aefe90379f1729495c2fee9ccbcc8cfda48e2ca2ae54a995e32b1", size = 33907, upload-time = "2026-05-21T10:11:43.578Z" },
|
| 1608 |
]
|
| 1609 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1610 |
[[package]]
|
| 1611 |
name = "shellingham"
|
| 1612 |
version = "1.5.4"
|
|
|
|
| 73 |
{ url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" },
|
| 74 |
]
|
| 75 |
|
| 76 |
+
[[package]]
|
| 77 |
+
name = "authlib"
|
| 78 |
+
version = "1.3.0"
|
| 79 |
+
source = { registry = "https://pypi.org/simple" }
|
| 80 |
+
dependencies = [
|
| 81 |
+
{ name = "cryptography" },
|
| 82 |
+
]
|
| 83 |
+
sdist = { url = "https://files.pythonhosted.org/packages/b9/8b/2921c26d3867d365e528a6e79b44efc799fcfbeaf2fc6dfa7b21cce7406d/Authlib-1.3.0.tar.gz", hash = "sha256:959ea62a5b7b5123c5059758296122b57cd2585ae2ed1c0622c21b371ffdae06", size = 145865, upload-time = "2023-12-17T23:51:33.464Z" }
|
| 84 |
+
wheels = [
|
| 85 |
+
{ url = "https://files.pythonhosted.org/packages/25/65/b78eb948b71ab232d08b30c38a2e3b69e6e50c6e166863a0068c877155b9/Authlib-1.3.0-py2.py3-none-any.whl", hash = "sha256:9637e4de1fb498310a56900b3e2043a206b03cb11c05422014b0302cbc814be3", size = 223706, upload-time = "2023-12-17T23:51:31.617Z" },
|
| 86 |
+
]
|
| 87 |
+
|
| 88 |
+
[[package]]
|
| 89 |
+
name = "bcrypt"
|
| 90 |
+
version = "4.1.3"
|
| 91 |
+
source = { registry = "https://pypi.org/simple" }
|
| 92 |
+
sdist = { url = "https://files.pythonhosted.org/packages/ca/e9/0b36987abbcd8c9210c7b86673d88ff0a481b4610630710fb80ba5661356/bcrypt-4.1.3.tar.gz", hash = "sha256:2ee15dd749f5952fe3f0430d0ff6b74082e159c50332a1413d51b5689cf06623", size = 26456, upload-time = "2024-05-04T04:12:51.451Z" }
|
| 93 |
+
wheels = [
|
| 94 |
+
{ url = "https://files.pythonhosted.org/packages/fe/4e/e424a74f0749998d8465c162c5cb9d9f210a5b60444f4120eff0af3fa800/bcrypt-4.1.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:48429c83292b57bf4af6ab75809f8f4daf52aa5d480632e53707805cc1ce9b74", size = 506501, upload-time = "2024-05-04T04:12:07.711Z" },
|
| 95 |
+
{ url = "https://files.pythonhosted.org/packages/7c/8d/ad2efe0ec57ed3c25e588c4543d946a1c72f8ee357a121c0e382d8aaa93f/bcrypt-4.1.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a8bea4c152b91fd8319fef4c6a790da5c07840421c2b785084989bf8bbb7455", size = 284345, upload-time = "2024-05-04T04:12:09.243Z" },
|
| 96 |
+
{ url = "https://files.pythonhosted.org/packages/2f/f6/9c0a6de7ef78d573e10d0b7de3ef82454e2e6eb6fada453ea6c2b8fb3f0a/bcrypt-4.1.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d3b317050a9a711a5c7214bf04e28333cf528e0ed0ec9a4e55ba628d0f07c1a", size = 283395, upload-time = "2024-05-04T04:12:11.116Z" },
|
| 97 |
+
{ url = "https://files.pythonhosted.org/packages/63/56/45312e49c195cd30e1bf4b7f0e039e8b3c46802cd55485947ddcb96caa27/bcrypt-4.1.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:094fd31e08c2b102a14880ee5b3d09913ecf334cd604af27e1013c76831f7b05", size = 284794, upload-time = "2024-05-04T04:12:12.447Z" },
|
| 98 |
+
{ url = "https://files.pythonhosted.org/packages/4c/6a/ce950d4350c734bc5d9b7196a58fedbdc94f564c00b495a1222984431e03/bcrypt-4.1.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4fb253d65da30d9269e0a6f4b0de32bd657a0208a6f4e43d3e645774fb5457f3", size = 283689, upload-time = "2024-05-04T04:12:14.289Z" },
|
| 99 |
+
{ url = "https://files.pythonhosted.org/packages/af/a1/36aa84027ef45558b30a485bc5b0606d5e7357b27b10cc49dce3944f4d1d/bcrypt-4.1.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:193bb49eeeb9c1e2db9ba65d09dc6384edd5608d9d672b4125e9320af9153a15", size = 318065, upload-time = "2024-05-04T04:12:15.641Z" },
|
| 100 |
+
{ url = "https://files.pythonhosted.org/packages/0f/e8/183ead5dd8124e463d0946dfaf86c658225adde036aede8384d21d1794d0/bcrypt-4.1.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:8cbb119267068c2581ae38790e0d1fbae65d0725247a930fc9900c285d95725d", size = 315556, upload-time = "2024-05-04T04:12:17.921Z" },
|
| 101 |
+
{ url = "https://files.pythonhosted.org/packages/2d/5e/edcb4ec57b056ca9d5f9fde31fcda10cc635def48867edff5cc09a348a4f/bcrypt-4.1.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6cac78a8d42f9d120b3987f82252bdbeb7e6e900a5e1ba37f6be6fe4e3848286", size = 324438, upload-time = "2024-05-04T04:12:19.823Z" },
|
| 102 |
+
{ url = "https://files.pythonhosted.org/packages/3b/5d/121130cc85009070fe4e4f5937b213a00db143147bc6c8677b3fd03deec8/bcrypt-4.1.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:01746eb2c4299dd0ae1670234bf77704f581dd72cc180f444bfe74eb80495b64", size = 335368, upload-time = "2024-05-04T04:12:21.055Z" },
|
| 103 |
+
{ url = "https://files.pythonhosted.org/packages/5b/ac/bcb7d3ac8a1107b103f4a95c5be088b984d8045d4150294459a657870bd9/bcrypt-4.1.3-cp37-abi3-win32.whl", hash = "sha256:037c5bf7c196a63dcce75545c8874610c600809d5d82c305dd327cd4969995bf", size = 167120, upload-time = "2024-05-04T04:12:23.021Z" },
|
| 104 |
+
{ url = "https://files.pythonhosted.org/packages/69/57/3856b1728018f5ce85bb678a76e939cb154a2e1f9c5aa69b83ec5652b111/bcrypt-4.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:8a893d192dfb7c8e883c4576813bf18bb9d59e2cfd88b68b725990f033f1b978", size = 158059, upload-time = "2024-05-04T04:12:25.256Z" },
|
| 105 |
+
{ url = "https://files.pythonhosted.org/packages/a8/eb/fbea8d2b370a4cc7f5f0aff9f492177a5813e130edeab9dd388ddd3ef1dc/bcrypt-4.1.3-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d4cf6ef1525f79255ef048b3489602868c47aea61f375377f0d00514fe4a78c", size = 506522, upload-time = "2024-05-04T04:12:27.206Z" },
|
| 106 |
+
{ url = "https://files.pythonhosted.org/packages/a4/9a/4aa31d1de9369737cfa734a60c3d125ecbd1b3ae2c6499986d0ac160ea8b/bcrypt-4.1.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5698ce5292a4e4b9e5861f7e53b1d89242ad39d54c3da451a93cac17b61921a", size = 284401, upload-time = "2024-05-04T04:12:29.098Z" },
|
| 107 |
+
{ url = "https://files.pythonhosted.org/packages/12/d4/13b86b1bb2969a804c2347d0ad72fc3d3d9f5cf0d876c84451c6480e19bc/bcrypt-4.1.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec3c2e1ca3e5c4b9edb94290b356d082b721f3f50758bce7cce11d8a7c89ce84", size = 283414, upload-time = "2024-05-04T04:12:31.507Z" },
|
| 108 |
+
{ url = "https://files.pythonhosted.org/packages/29/3c/6e478265f68eff764571676c0773086d15378fdf5347ddf53e5025c8b956/bcrypt-4.1.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3a5be252fef513363fe281bafc596c31b552cf81d04c5085bc5dac29670faa08", size = 284951, upload-time = "2024-05-04T04:12:33.449Z" },
|
| 109 |
+
{ url = "https://files.pythonhosted.org/packages/97/00/21e34b365b895e6faf9cc5d4e7b97dd419e08f8a7df119792ec206b4a3fa/bcrypt-4.1.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5f7cd3399fbc4ec290378b541b0cf3d4398e4737a65d0f938c7c0f9d5e686611", size = 283703, upload-time = "2024-05-04T04:12:34.794Z" },
|
| 110 |
+
{ url = "https://files.pythonhosted.org/packages/e0/c9/069b0c3683ce969b328b7b3e3218f9d5981d0629f6091b3b1dfa72928f75/bcrypt-4.1.3-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:c4c8d9b3e97209dd7111bf726e79f638ad9224b4691d1c7cfefa571a09b1b2d6", size = 317876, upload-time = "2024-05-04T04:12:36.108Z" },
|
| 111 |
+
{ url = "https://files.pythonhosted.org/packages/2c/fd/0d2d7cc6fc816010f6c6273b778e2f147e2eca1144975b6b71e344b26ca0/bcrypt-4.1.3-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:31adb9cbb8737a581a843e13df22ffb7c84638342de3708a98d5c986770f2834", size = 315555, upload-time = "2024-05-04T04:12:37.536Z" },
|
| 112 |
+
{ url = "https://files.pythonhosted.org/packages/23/85/283450ee672719e216a5e1b0e80cb0c8f225bc0814cbb893155ee4fdbb9e/bcrypt-4.1.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:551b320396e1d05e49cc18dd77d970accd52b322441628aca04801bbd1d52a73", size = 324408, upload-time = "2024-05-04T04:12:38.882Z" },
|
| 113 |
+
{ url = "https://files.pythonhosted.org/packages/9c/64/a016d23b6f513282d8b7f9dd91342929a2e970b2e2c2576d9b76f8f2ee5a/bcrypt-4.1.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6717543d2c110a155e6821ce5670c1f512f602eabb77dba95717ca76af79867d", size = 335334, upload-time = "2024-05-04T04:12:40.442Z" },
|
| 114 |
+
{ url = "https://files.pythonhosted.org/packages/75/35/036d69e46c60394f2ffb474c9a4b3783e84395bf4ad55176771f603069ca/bcrypt-4.1.3-cp39-abi3-win32.whl", hash = "sha256:6004f5229b50f8493c49232b8e75726b568535fd300e5039e255d919fc3a07f2", size = 167071, upload-time = "2024-05-04T04:12:42.256Z" },
|
| 115 |
+
{ url = "https://files.pythonhosted.org/packages/b1/46/fada28872f3f3e121868f4cd2d61dcdc7085a07821debf1320cafeabc0db/bcrypt-4.1.3-cp39-abi3-win_amd64.whl", hash = "sha256:2505b54afb074627111b5a8dc9b6ae69d0f01fea65c2fcaea403448c503d3991", size = 158124, upload-time = "2024-05-04T04:12:44.085Z" },
|
| 116 |
+
]
|
| 117 |
+
|
| 118 |
[[package]]
|
| 119 |
name = "billiard"
|
| 120 |
version = "4.2.4"
|
|
|
|
| 452 |
{ url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" },
|
| 453 |
]
|
| 454 |
|
| 455 |
+
[[package]]
|
| 456 |
+
name = "ecdsa"
|
| 457 |
+
version = "0.19.2"
|
| 458 |
+
source = { registry = "https://pypi.org/simple" }
|
| 459 |
+
dependencies = [
|
| 460 |
+
{ name = "six" },
|
| 461 |
+
]
|
| 462 |
+
sdist = { url = "https://files.pythonhosted.org/packages/25/ca/8de7744cb3bc966c85430ca2d0fcaeea872507c6a4cf6e007f7fe269ed9d/ecdsa-0.19.2.tar.gz", hash = "sha256:62635b0ac1ca2e027f82122b5b81cb706edc38cd91c63dda28e4f3455a2bf930", size = 202432, upload-time = "2026-03-26T09:58:17.675Z" }
|
| 463 |
+
wheels = [
|
| 464 |
+
{ url = "https://files.pythonhosted.org/packages/51/79/119091c98e2bf49e24ed9f3ae69f816d715d2904aefa6a2baa039a2ba0b0/ecdsa-0.19.2-py2.py3-none-any.whl", hash = "sha256:840f5dc5e375c68f36c1a7a5b9caad28f95daa65185c9253c0c08dd952bb7399", size = 150818, upload-time = "2026-03-26T09:58:15.808Z" },
|
| 465 |
+
]
|
| 466 |
+
|
| 467 |
[[package]]
|
| 468 |
name = "email-validator"
|
| 469 |
version = "2.3.0"
|
|
|
|
| 899 |
source = { editable = "." }
|
| 900 |
dependencies = [
|
| 901 |
{ name = "alembic" },
|
| 902 |
+
{ name = "authlib" },
|
| 903 |
+
{ name = "bcrypt" },
|
| 904 |
{ name = "celery" },
|
| 905 |
{ name = "ddgs" },
|
| 906 |
{ name = "fastapi" },
|
| 907 |
{ name = "google-generativeai" },
|
| 908 |
+
{ name = "httpx" },
|
| 909 |
+
{ name = "passlib", extra = ["bcrypt"] },
|
| 910 |
{ name = "prometheus-client" },
|
| 911 |
{ name = "psycopg2-binary" },
|
| 912 |
{ name = "pydantic" },
|
| 913 |
{ name = "pydantic-settings" },
|
| 914 |
{ name = "python-dotenv" },
|
| 915 |
+
{ name = "python-jose", extra = ["cryptography"] },
|
| 916 |
{ name = "redis" },
|
| 917 |
{ name = "sqlalchemy" },
|
| 918 |
{ name = "uvicorn", extra = ["standard"] },
|
|
|
|
| 928 |
[package.metadata]
|
| 929 |
requires-dist = [
|
| 930 |
{ name = "alembic", specifier = "==1.13.1" },
|
| 931 |
+
{ name = "authlib", specifier = "==1.3.0" },
|
| 932 |
+
{ name = "bcrypt", specifier = "==4.1.3" },
|
| 933 |
{ name = "celery", specifier = "==5.3.6" },
|
| 934 |
{ name = "ddgs", specifier = ">=9.14.3" },
|
| 935 |
{ name = "fastapi", specifier = "==0.111.0" },
|
| 936 |
{ name = "google-generativeai", specifier = "==0.5.4" },
|
| 937 |
+
{ name = "httpx", specifier = "==0.27.0" },
|
| 938 |
+
{ name = "passlib", extras = ["bcrypt"], specifier = "==1.7.4" },
|
| 939 |
{ name = "prometheus-client", specifier = "==0.20.0" },
|
| 940 |
{ name = "psycopg2-binary", specifier = "==2.9.9" },
|
| 941 |
{ name = "pydantic", specifier = "==2.7.1" },
|
| 942 |
{ name = "pydantic-settings", specifier = "==2.2.1" },
|
| 943 |
{ name = "python-dotenv", specifier = "==1.0.1" },
|
| 944 |
+
{ name = "python-jose", extras = ["cryptography"], specifier = "==3.3.0" },
|
| 945 |
{ name = "redis", specifier = "==5.0.4" },
|
| 946 |
{ name = "sqlalchemy", specifier = "==2.0.30" },
|
| 947 |
{ name = "uvicorn", extras = ["standard"], specifier = "==0.29.0" },
|
|
|
|
| 1267 |
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
| 1268 |
]
|
| 1269 |
|
| 1270 |
+
[[package]]
|
| 1271 |
+
name = "passlib"
|
| 1272 |
+
version = "1.7.4"
|
| 1273 |
+
source = { registry = "https://pypi.org/simple" }
|
| 1274 |
+
sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844, upload-time = "2020-10-08T19:00:52.121Z" }
|
| 1275 |
+
wheels = [
|
| 1276 |
+
{ url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554, upload-time = "2020-10-08T19:00:49.856Z" },
|
| 1277 |
+
]
|
| 1278 |
+
|
| 1279 |
+
[package.optional-dependencies]
|
| 1280 |
+
bcrypt = [
|
| 1281 |
+
{ name = "bcrypt" },
|
| 1282 |
+
]
|
| 1283 |
+
|
| 1284 |
[[package]]
|
| 1285 |
name = "pluggy"
|
| 1286 |
version = "1.6.0"
|
|
|
|
| 1567 |
{ url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863, upload-time = "2024-01-23T06:32:58.246Z" },
|
| 1568 |
]
|
| 1569 |
|
| 1570 |
+
[[package]]
|
| 1571 |
+
name = "python-jose"
|
| 1572 |
+
version = "3.3.0"
|
| 1573 |
+
source = { registry = "https://pypi.org/simple" }
|
| 1574 |
+
dependencies = [
|
| 1575 |
+
{ name = "ecdsa" },
|
| 1576 |
+
{ name = "pyasn1" },
|
| 1577 |
+
{ name = "rsa" },
|
| 1578 |
+
]
|
| 1579 |
+
sdist = { url = "https://files.pythonhosted.org/packages/e4/19/b2c86504116dc5f0635d29f802da858404d77d930a25633d2e86a64a35b3/python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a", size = 129068, upload-time = "2021-06-05T03:30:40.895Z" }
|
| 1580 |
+
wheels = [
|
| 1581 |
+
{ url = "https://files.pythonhosted.org/packages/bd/2d/e94b2f7bab6773c70efc70a61d66e312e1febccd9e0db6b9e0adf58cbad1/python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a", size = 33530, upload-time = "2021-06-05T03:30:38.099Z" },
|
| 1582 |
+
]
|
| 1583 |
+
|
| 1584 |
+
[package.optional-dependencies]
|
| 1585 |
+
cryptography = [
|
| 1586 |
+
{ name = "cryptography" },
|
| 1587 |
+
]
|
| 1588 |
+
|
| 1589 |
[[package]]
|
| 1590 |
name = "python-multipart"
|
| 1591 |
version = "0.0.29"
|
|
|
|
| 1704 |
{ url = "https://files.pythonhosted.org/packages/35/84/a005adcb4d1e6846ba3d62768090c3b943e3f6d8dc5c47af64f33584c4a7/rich_toolkit-0.19.10-py3-none-any.whl", hash = "sha256:93a41f67a09aefe90379f1729495c2fee9ccbcc8cfda48e2ca2ae54a995e32b1", size = 33907, upload-time = "2026-05-21T10:11:43.578Z" },
|
| 1705 |
]
|
| 1706 |
|
| 1707 |
+
[[package]]
|
| 1708 |
+
name = "rsa"
|
| 1709 |
+
version = "4.9.1"
|
| 1710 |
+
source = { registry = "https://pypi.org/simple" }
|
| 1711 |
+
dependencies = [
|
| 1712 |
+
{ name = "pyasn1" },
|
| 1713 |
+
]
|
| 1714 |
+
sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" }
|
| 1715 |
+
wheels = [
|
| 1716 |
+
{ url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" },
|
| 1717 |
+
]
|
| 1718 |
+
|
| 1719 |
[[package]]
|
| 1720 |
name = "shellingham"
|
| 1721 |
version = "1.5.4"
|