Skip to content

Commit 78d0be7

Browse files
feat: connect backend to frontend
1 parent 0513047 commit 78d0be7

File tree

11 files changed

+127
-72
lines changed

11 files changed

+127
-72
lines changed

backend/src/auth/api.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from datetime import timedelta
22
from typing import Annotated
33

4-
from fastapi import APIRouter, Depends, HTTPException
4+
from fastapi import APIRouter, Depends, HTTPException, Request
55
from fastapi.security import OAuth2PasswordRequestForm
66

77
from src.auth.schemas import Token
@@ -15,6 +15,7 @@
1515

1616
@router.post("/tokens")
1717
def login_access_token(
18+
request: Request,
1819
session: SessionDep, form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
1920
) -> Token:
2021
user = services.authenticate(
@@ -25,8 +26,17 @@ def login_access_token(
2526
elif not user.is_active:
2627
raise HTTPException(status_code=400, detail="Inactive user")
2728
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
28-
return Token(
29-
access_token=services.create_access_token(
30-
user.id, expires_delta=access_token_expires
31-
)
29+
30+
# Create token
31+
token = services.create_access_token(
32+
user.id, expires_delta=access_token_expires
3233
)
34+
35+
# Also store user in session for consistency with Auth0 flow
36+
request.session["user"] = {
37+
"email": user.email,
38+
"id": str(user.id),
39+
}
40+
request.session["user_id"] = str(user.id)
41+
42+
return Token(access_token=token)

backend/src/auth/auth0_api.py

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
from fastapi.responses import RedirectResponse
33
from authlib.integrations.starlette_client import OAuth
44
from src.core.config import settings
5-
5+
from src.core.db import get_db
6+
from src.auth.services import get_or_create_user_by_email # Fix the import path
67
router = APIRouter(tags=["auth"])
78

89
oauth = OAuth()
@@ -15,7 +16,7 @@
1516
)
1617

1718

18-
@router.get("/login")
19+
@router.get("/login", name="auth0_login")
1920
async def login(request: Request):
2021
redirect_uri = request.url_for("auth0_callback")
2122
return await oauth.auth0.authorize_redirect(
@@ -32,27 +33,32 @@ async def auth0_callback(request: Request):
3233

3334
user = token.get("userinfo") or await oauth.auth0.userinfo(token=token)
3435

36+
# Create or get the user from database
37+
db = next(get_db())
38+
db_user = get_or_create_user_by_email(
39+
session=db,
40+
email=user["email"],
41+
defaults={
42+
"auth0_id": user["sub"],
43+
"full_name": user.get("name"),
44+
"picture": user.get("picture"),
45+
"is_active": True,
46+
},
47+
)
48+
49+
# Store in session
3550
request.session["user"] = {
3651
"email": user["email"],
3752
"name": user.get("name"),
3853
"picture": user.get("picture"),
3954
"sub": user.get("sub"),
4055
}
56+
request.session["user_id"] = str(db_user.id)
4157

4258
return RedirectResponse(url="http://localhost:5173/collections")
4359

4460

45-
@router.get("/logout")
61+
@router.get("/logout", name="auth0_logout")
4662
async def logout(request: Request):
4763
request.session.clear()
48-
return RedirectResponse(
49-
url=f"https://{settings.AUTH0_DOMAIN}/v2/logout"
50-
f"?client_id={settings.AUTH0_CLIENT_ID}"
51-
f"&returnTo=http://localhost:5173"
52-
)
53-
54-
55-
@router.get("/me")
56-
async def me(request: Request):
57-
user = request.session.get("user")
58-
return {"authenticated": bool(user), "user": user}
64+
return {"detail": "Logged out"}

backend/src/auth/services.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from typing import Annotated, Any
33

44
import jwt
5-
from fastapi import Depends, HTTPException, status, Request
5+
from fastapi import Depends, HTTPException, Request
66
from fastapi.security import OAuth2PasswordBearer
77
from jwt.exceptions import InvalidTokenError
88
from passlib.context import CryptContext
@@ -42,10 +42,14 @@ def get_user_from_token(
4242
try:
4343
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[ALGORITHM])
4444
token_data = TokenPayload(**payload)
45+
4546
user = session.get(User, token_data.sub)
47+
4648
if not user or not user.is_active:
4749
raise HTTPException(status_code=401, detail="Invalid user")
50+
4851
return user
52+
4953
except (InvalidTokenError, ValidationError):
5054
raise HTTPException(status_code=403, detail="Invalid token")
5155

@@ -55,18 +59,12 @@ def get_current_user(
5559
session: SessionDep,
5660
token: Annotated[str | None, Depends(OAuth2PasswordBearer(tokenUrl=f"{settings.API_V1_STR}/tokens", auto_error=False))] = None,
5761
) -> User:
58-
print("in get current user")
59-
# Prefer session (Auth0 flow)
6062
session_user = request.session.get("user")
6163
if session_user:
62-
print("Session user found:", session_user["email"])
63-
res = get_user_from_session(request, session)
64-
print("User from session:", res)
65-
return res
66-
# Fallback to token (JWT flow)
64+
return get_user_from_session(request, session)
6765
if token:
6866
return get_user_from_token(session, token)
69-
67+
7068
raise HTTPException(status_code=401, detail="Not authenticated")
7169

7270

@@ -83,15 +81,15 @@ def authenticate(*, session: Session, email: str, password: str) -> User | None:
8381
db_user = get_user_by_email(session=session, email=email)
8482
if not db_user:
8583
return None
86-
84+
8785
# Auth0 users may not have a password
8886
if not db_user.hashed_password:
8987
# Return None for users without a password when using password authentication
9088
return None
91-
89+
9290
if not verify_password(password, db_user.hashed_password):
9391
return None
94-
92+
9593
return db_user
9694

9795

backend/src/core/config.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class Settings(BaseSettings):
3030
SECRET_KEY: str = secrets.token_urlsafe(32)
3131
# 60 minutes * 24 hours * 8 days = 8 days
3232
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8
33+
AUTH_EXPIRE_MINUTES: int = 60 * 24 * 8
3334
DOMAIN: str = "localhost"
3435
ENVIRONMENT: Literal["local", "staging", "production"] = "local"
3536

@@ -98,6 +99,6 @@ def _enforce_non_default_secrets(self) -> Self:
9899
self._check_default_secret("AUTH0_CLIENT_SECRET", self.AUTH0_CLIENT_SECRET)
99100

100101
return self
101-
102+
102103

103104
settings = Settings() # type: ignore

backend/src/main.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,13 @@ def custom_generate_unique_id(route: APIRoute) -> str:
3030
allow_headers=["*"],
3131
)
3232

33+
# Setup session middleware
3334
app.add_middleware(
3435
SessionMiddleware,
3536
secret_key=settings.SECRET_KEY,
3637
same_site="lax", # adjust for production
37-
https_only=False
38+
https_only=False,
39+
max_age=settings.AUTH_EXPIRE_MINUTES * 60,
3840
)
3941

4042
app.include_router(api_router, prefix=settings.API_V1_STR)

backend/src/users/api.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ def read_user_me(request: Request, current_user: CurrentUser) -> Any:
1616
"""
1717
Get current user.
1818
"""
19-
print("session content:", request.session)
2019
return current_user
2120

2221

frontend/src/components/commonUI/Drawer.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ import {
1313
import useAuth from '@/hooks/useAuth'
1414
import { getCollections } from '@/services/collections'
1515
import { HStack, IconButton, Image, List, Spinner, Text, VStack } from '@chakra-ui/react'
16-
import { useQuery, useQueryClient } from '@tanstack/react-query'
16+
import { useQuery } from '@tanstack/react-query'
1717
import { Link, useNavigate } from '@tanstack/react-router'
1818
import { useTranslation } from 'react-i18next'
1919
import { FiLogOut, FiMoon, FiSun } from 'react-icons/fi'
20+
import { UsersService } from '@/client'
2021
import { DefaultButton } from './Button'
2122
import LanguageSelector from './LanguageSelector'
2223

@@ -54,14 +55,23 @@ function Drawer({ isOpen, setIsOpen }: { isOpen: boolean; setIsOpen: (open: bool
5455
const { t } = useTranslation()
5556
const { logout } = useAuth()
5657
const { colorMode, toggleColorMode } = useColorMode()
57-
const queryClient = useQueryClient()
58-
const currentUser = queryClient.getQueryData<{ email: string }>(['currentUser'])
59-
const flashcardsService = { getCollections }
6058
const navigate = useNavigate()
59+
60+
const { data: currentUser } = useQuery({
61+
queryKey: ['currentUser'],
62+
queryFn: async () => {
63+
try {
64+
return await UsersService.readUserMe()
65+
} catch (error) {
66+
return null
67+
}
68+
}
69+
})
6170

71+
// Fetch collections data
6272
const { data, isLoading } = useQuery({
6373
queryKey: ['collections'],
64-
queryFn: flashcardsService.getCollections,
74+
queryFn: getCollections,
6575
placeholderData: (prevData) => prevData,
6676
})
6777

frontend/src/hooks/useAuthContext.tsx

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type React from 'react'
22
import { createContext, useContext, useEffect, useState } from 'react'
3+
import { UsersService } from '@/client'
34

45
const GUEST_MODE_KEY = 'guest_mode'
56
const ACCESS_TOKEN_KEY = 'access_token'
@@ -8,26 +9,47 @@ interface AuthContextType {
89
isGuest: boolean
910
isLoggedIn: boolean
1011
setGuestMode: (value: boolean) => void
11-
logout: () => void
12+
logout: () => Promise<void>
1213
}
1314

1415
const AuthContext = createContext<AuthContextType | undefined>(undefined)
1516

1617
function AuthProvider({ children }: { children: React.ReactNode }) {
1718
const [isGuest, setIsGuest] = useState(() => localStorage.getItem(GUEST_MODE_KEY) === 'true')
18-
const [isLoggedIn, setIsLoggedIn] = useState(
19-
() =>
20-
Boolean(localStorage.getItem(ACCESS_TOKEN_KEY)) ||
21-
localStorage.getItem(GUEST_MODE_KEY) === 'true',
19+
const [isLoggedIn, setIsLoggedIn] = useState(() =>
20+
Boolean(localStorage.getItem(ACCESS_TOKEN_KEY)) || localStorage.getItem(GUEST_MODE_KEY) === 'true'
2221
)
2322

2423
useEffect(() => {
24+
console.log("isLoggedIn", isLoggedIn)
25+
26+
const checkUser = async () => {
27+
if (localStorage.getItem(GUEST_MODE_KEY) === 'true') {
28+
setIsLoggedIn(true)
29+
return
30+
}
31+
32+
try {
33+
const user = await UsersService.readUserMe()
34+
if (user) {
35+
setIsLoggedIn(true)
36+
}
37+
} catch (error) {
38+
console.error('Error fetching user:', error)
39+
}
40+
}
41+
checkUser()
42+
}, [])
43+
44+
useEffect(() => {
45+
console.log('AuthProvider mounted')
2546
const handleStorage = (e: StorageEvent) => {
47+
console.log('Storage event:', e.key)
2648
if (e.key === GUEST_MODE_KEY || e.key === ACCESS_TOKEN_KEY) {
2749
setIsGuest(localStorage.getItem(GUEST_MODE_KEY) === 'true')
2850
setIsLoggedIn(
2951
Boolean(localStorage.getItem(ACCESS_TOKEN_KEY)) ||
30-
localStorage.getItem(GUEST_MODE_KEY) === 'true',
52+
localStorage.getItem(GUEST_MODE_KEY) === 'true'
3153
)
3254
}
3355
}
@@ -45,7 +67,15 @@ function AuthProvider({ children }: { children: React.ReactNode }) {
4567
setIsLoggedIn(value || Boolean(localStorage.getItem(ACCESS_TOKEN_KEY)))
4668
}
4769

48-
const logout = () => {
70+
const logout = async () => {
71+
try {
72+
await fetch('http://localhost:8000/api/v1/auth0/logout', {
73+
method: 'GET',
74+
credentials: 'include',
75+
})
76+
} catch (e) {
77+
console.error('Logout request failed', e)
78+
}
4979
localStorage.removeItem(ACCESS_TOKEN_KEY)
5080
setGuestMode(false)
5181
setIsLoggedIn(false)
@@ -64,4 +94,4 @@ function useAuthContext() {
6494
return ctx
6595
}
6696

67-
export { AuthProvider, useAuthContext }
97+
export { AuthProvider, useAuthContext }

frontend/src/routes/_layout.tsx

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,26 @@
11
import Navbar from '@/components/commonUI/Navbar'
22
import { Toaster } from '@/components/ui/toaster'
3+
import { UsersService } from '@/client'
34
import { Container } from '@chakra-ui/react'
45
import { Outlet, createFileRoute, redirect } from '@tanstack/react-router'
56

67
export const Route = createFileRoute('/_layout')({
78
component: Layout,
89
beforeLoad: async () => {
9-
// ✅ Auth check using backend session-aware endpoint
10+
const isGuest = localStorage.getItem('guest_mode') === 'true';
11+
if (isGuest) {
12+
return;
13+
}
1014
try {
11-
const res = await fetch('http://localhost:8000/api/v1/users/me', {
12-
credentials: 'include', // 🔒 Required to send session cookie
13-
})
14-
15-
console.log("res:", res)
16-
17-
const data = await res.json()
18-
console.log("data:", data)
19-
const isLoggedIn = data?.email != null
20-
const isGuest = localStorage.getItem('guest_mode') === 'true'
21-
22-
if (!isLoggedIn && !isGuest) {
23-
console.log("Not logged in or guest")
24-
throw redirect({ to: '/login' })
15+
const user = await UsersService.readUserMe();
16+
if (!user) {
17+
throw redirect({ to: '/login' });
2518
}
2619
} catch (err) {
27-
console.error('Failed to check auth state:', err)
28-
throw redirect({ to: '/login' })
20+
console.error('Failed to check auth state:', err);
21+
throw redirect({ to: '/login' });
2922
}
30-
},
23+
}
3124
})
3225

3326
function Layout() {

0 commit comments

Comments
 (0)