Karan6124 commited on
Commit
5d3cd52
·
1 Parent(s): 02ed8e4

feat(auth): implement Google OAuth and Email/Password authentication

Browse files
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
- {/* Begin Investigation CTA */}
101
- <button
102
- onClick={handleBeginInvestigation}
103
- 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"
104
- >
105
- Begin Investigation
106
- <svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
107
- <circle cx="11" cy="11" r="7" />
108
- <line x1="16.5" y1="16.5" x2="21" y2="21" />
109
- </svg>
110
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- <button
212
- onClick={handleBeginInvestigation}
213
- 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"
214
- >
215
- Begin Investigation 🔍
216
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- // Placeholder: wire up to Google OAuth + JWT when ready
6
- const handleBeginInvestigation = () => {
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={handleBeginInvestigation}
 
 
 
 
 
 
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"