Refactor authentication endpoints and user management; update CORS settings and JWT handling for improved security and compatibility with FastAPI-Users. Remove deprecated user-related endpoints and streamline API structure.

This commit is contained in:
mohamad 2025-05-14 01:04:09 +02:00
parent 72b988b79b
commit 3f0cfff9f1
19 changed files with 87 additions and 487 deletions

View File

@ -1,72 +0,0 @@
# app/api/dependencies.py
import logging
from typing import Optional
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.ext.asyncio import AsyncSession
from jose import JWTError
from app.database import get_db
from app.core.security import verify_access_token
from app.crud import user as crud_user
from app.models import User as UserModel # Import the SQLAlchemy model
from app.config import settings
logger = logging.getLogger(__name__)
# Define the OAuth2 scheme
# tokenUrl should point to your login endpoint relative to the base path
# It's used by Swagger UI for the "Authorize" button flow.
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=settings.OAUTH2_TOKEN_URL)
async def get_current_user(
token: str = Depends(oauth2_scheme),
db: AsyncSession = Depends(get_db)
) -> UserModel:
"""
Dependency to get the current user based on the JWT token.
- Extracts token using OAuth2PasswordBearer.
- Verifies the token (signature, expiry).
- Fetches the user from the database based on the token's subject (email).
- Raises HTTPException 401 if any step fails.
Returns:
The authenticated user's database model instance.
"""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=settings.AUTH_CREDENTIALS_ERROR,
headers={settings.AUTH_HEADER_NAME: settings.AUTH_HEADER_PREFIX},
)
payload = verify_access_token(token)
if payload is None:
logger.warning("Token verification failed (invalid, expired, or malformed).")
raise credentials_exception
email: Optional[str] = payload.get("sub")
if email is None:
logger.error("Token payload missing 'sub' (subject/email).")
raise credentials_exception # Token is malformed
# Fetch user from database
user = await crud_user.get_user_by_email(db, email=email)
if user is None:
logger.warning(f"User corresponding to token subject not found: {email}")
# Could happen if user deleted after token issuance
raise credentials_exception # Treat as invalid credentials
logger.debug(f"Authenticated user retrieved: {user.email} (ID: {user.id})")
return user
# Optional: Dependency for getting the *active* current user
# You might add an `is_active` flag to your User model later
# async def get_current_active_user(
# current_user: UserModel = Depends(get_current_user)
# ) -> UserModel:
# if not current_user.is_active: # Assuming an is_active attribute
# logger.warning(f"Authentication attempt by inactive user: {current_user.email}")
# raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user")
# return current_user

View File

@ -1,8 +1,6 @@
from fastapi import APIRouter from fastapi import APIRouter
from app.api.v1.endpoints import health from app.api.v1.endpoints import health
from app.api.v1.endpoints import auth
from app.api.v1.endpoints import users
from app.api.v1.endpoints import groups from app.api.v1.endpoints import groups
from app.api.v1.endpoints import invites from app.api.v1.endpoints import invites
from app.api.v1.endpoints import lists from app.api.v1.endpoints import lists
@ -14,8 +12,6 @@ from app.api.v1.endpoints import financials
api_router_v1 = APIRouter() api_router_v1 = APIRouter()
api_router_v1.include_router(health.router) api_router_v1.include_router(health.router)
api_router_v1.include_router(auth.router, prefix="/auth", tags=["Authentication"])
api_router_v1.include_router(users.router, prefix="/users", tags=["Users"])
api_router_v1.include_router(groups.router, prefix="/groups", tags=["Groups"]) api_router_v1.include_router(groups.router, prefix="/groups", tags=["Groups"])
api_router_v1.include_router(invites.router, prefix="/invites", tags=["Invites"]) api_router_v1.include_router(invites.router, prefix="/invites", tags=["Invites"])
api_router_v1.include_router(lists.router, prefix="/lists", tags=["Lists"]) api_router_v1.include_router(lists.router, prefix="/lists", tags=["Lists"])

View File

@ -1,136 +0,0 @@
# app/api/v1/endpoints/auth.py
import logging
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.schemas.user import UserCreate, UserPublic
from app.schemas.auth import Token
from app.crud import user as crud_user
from app.core.security import (
verify_password,
create_access_token,
create_refresh_token,
verify_refresh_token
)
from app.core.exceptions import (
EmailAlreadyRegisteredError,
InvalidCredentialsError,
UserCreationError
)
from app.config import settings
logger = logging.getLogger(__name__)
router = APIRouter()
@router.post(
"/signup",
response_model=UserPublic,
status_code=201,
summary="Register New User",
description="Creates a new user account.",
tags=["Authentication"]
)
async def signup(
user_in: UserCreate,
db: AsyncSession = Depends(get_db)
):
"""
Handles user registration.
- Validates input data.
- Checks if email already exists.
- Hashes the password.
- Stores the new user in the database.
"""
logger.info(f"Signup attempt for email: {user_in.email}")
existing_user = await crud_user.get_user_by_email(db, email=user_in.email)
if existing_user:
logger.warning(f"Signup failed: Email already registered - {user_in.email}")
raise EmailAlreadyRegisteredError()
try:
created_user = await crud_user.create_user(db=db, user_in=user_in)
logger.info(f"User created successfully: {created_user.email} (ID: {created_user.id})")
return created_user
except Exception as e:
logger.error(f"Error during user creation for {user_in.email}: {e}", exc_info=True)
raise UserCreationError()
@router.post(
"/login",
response_model=Token,
summary="User Login",
description="Authenticates a user and returns an access and refresh token.",
tags=["Authentication"]
)
async def login(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
db: AsyncSession = Depends(get_db)
):
"""
Handles user login.
- Finds user by email (provided in 'username' field of form).
- Verifies the provided password against the stored hash.
- Generates and returns JWT access and refresh tokens upon successful authentication.
"""
logger.info(f"Login attempt for user: {form_data.username}")
user = await crud_user.get_user_by_email(db, email=form_data.username)
if not user or not verify_password(form_data.password, user.password_hash):
logger.warning(f"Login failed: Invalid credentials for user {form_data.username}")
raise InvalidCredentialsError()
access_token = create_access_token(subject=user.email)
refresh_token = create_refresh_token(subject=user.email)
logger.info(f"Login successful, tokens generated for user: {user.email}")
return Token(
access_token=access_token,
refresh_token=refresh_token,
token_type=settings.TOKEN_TYPE
)
@router.post(
"/refresh",
response_model=Token,
summary="Refresh Access Token",
description="Refreshes an access token using a refresh token.",
tags=["Authentication"]
)
async def refresh_token(
refresh_token_str: str,
db: AsyncSession = Depends(get_db)
):
"""
Handles access token refresh.
- Verifies the provided refresh token.
- If valid, generates and returns a new JWT access token and a new refresh token.
"""
logger.info("Access token refresh attempt")
payload = verify_refresh_token(refresh_token_str)
if not payload:
logger.warning("Refresh token invalid or expired")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired refresh token",
headers={"WWW-Authenticate": "Bearer"},
)
user_email = payload.get("sub")
if not user_email:
logger.error("User email not found in refresh token payload")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid refresh token payload",
headers={"WWW-Authenticate": "Bearer"},
)
new_access_token = create_access_token(subject=user_email)
new_refresh_token = create_refresh_token(subject=user_email)
logger.info(f"Access token refreshed and new refresh token issued for user: {user_email}")
return Token(
access_token=new_access_token,
refresh_token=new_refresh_token,
token_type=settings.TOKEN_TYPE
)

View File

@ -7,7 +7,7 @@ from sqlalchemy.orm import Session, selectinload
from decimal import Decimal, ROUND_HALF_UP from decimal import Decimal, ROUND_HALF_UP
from app.database import get_db from app.database import get_db
from app.api.dependencies import get_current_user from app.auth import current_active_user
from app.models import ( from app.models import (
User as UserModel, User as UserModel,
Group as GroupModel, Group as GroupModel,
@ -41,7 +41,7 @@ router = APIRouter()
async def get_list_cost_summary( async def get_list_cost_summary(
list_id: int, list_id: int,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), current_user: UserModel = Depends(current_active_user),
): ):
""" """
Retrieves a calculated cost summary for a specific list, detailing total costs, Retrieves a calculated cost summary for a specific list, detailing total costs,
@ -184,7 +184,7 @@ async def get_list_cost_summary(
async def get_group_balance_summary( async def get_group_balance_summary(
group_id: int, group_id: int,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), current_user: UserModel = Depends(current_active_user),
): ):
""" """
Retrieves a detailed financial balance summary for all users within a specific group. Retrieves a detailed financial balance summary for all users within a specific group.

View File

@ -6,7 +6,7 @@ from sqlalchemy import select
from typing import List as PyList, Optional, Sequence from typing import List as PyList, Optional, Sequence
from app.database import get_db from app.database import get_db
from app.api.dependencies import get_current_user from app.auth import current_active_user
from app.models import User as UserModel, Group as GroupModel, List as ListModel, UserGroup as UserGroupModel, UserRoleEnum from app.models import User as UserModel, Group as GroupModel, List as ListModel, UserGroup as UserGroupModel, UserRoleEnum
from app.schemas.expense import ( from app.schemas.expense import (
ExpenseCreate, ExpensePublic, ExpenseCreate, ExpensePublic,
@ -47,7 +47,7 @@ async def check_list_access_for_financials(db: AsyncSession, list_id: int, user_
async def create_new_expense( async def create_new_expense(
expense_in: ExpenseCreate, expense_in: ExpenseCreate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), current_user: UserModel = Depends(current_active_user),
): ):
logger.info(f"User {current_user.email} creating expense: {expense_in.description}") logger.info(f"User {current_user.email} creating expense: {expense_in.description}")
effective_group_id = expense_in.group_id effective_group_id = expense_in.group_id
@ -110,7 +110,7 @@ async def create_new_expense(
async def get_expense( async def get_expense(
expense_id: int, expense_id: int,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), current_user: UserModel = Depends(current_active_user),
): ):
logger.info(f"User {current_user.email} requesting expense ID {expense_id}") logger.info(f"User {current_user.email} requesting expense ID {expense_id}")
expense = await crud_expense.get_expense_by_id(db, expense_id=expense_id) expense = await crud_expense.get_expense_by_id(db, expense_id=expense_id)
@ -131,7 +131,7 @@ async def list_list_expenses(
skip: int = Query(0, ge=0), skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=200), limit: int = Query(100, ge=1, le=200),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), current_user: UserModel = Depends(current_active_user),
): ):
logger.info(f"User {current_user.email} listing expenses for list ID {list_id}") logger.info(f"User {current_user.email} listing expenses for list ID {list_id}")
await check_list_access_for_financials(db, list_id, current_user.id) await check_list_access_for_financials(db, list_id, current_user.id)
@ -144,7 +144,7 @@ async def list_group_expenses(
skip: int = Query(0, ge=0), skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=200), limit: int = Query(100, ge=1, le=200),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), current_user: UserModel = Depends(current_active_user),
): ):
logger.info(f"User {current_user.email} listing expenses for group ID {group_id}") logger.info(f"User {current_user.email} listing expenses for group ID {group_id}")
await crud_group.check_group_membership(db, group_id=group_id, user_id=current_user.id, action="list expenses for") await crud_group.check_group_membership(db, group_id=group_id, user_id=current_user.id, action="list expenses for")
@ -156,7 +156,7 @@ async def update_expense_details(
expense_id: int, expense_id: int,
expense_in: ExpenseUpdate, expense_in: ExpenseUpdate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), current_user: UserModel = Depends(current_active_user),
): ):
""" """
Updates an existing expense (description, currency, expense_date only). Updates an existing expense (description, currency, expense_date only).
@ -210,7 +210,7 @@ async def delete_expense_record(
expense_id: int, expense_id: int,
expected_version: Optional[int] = Query(None, description="Expected version for optimistic locking"), expected_version: Optional[int] = Query(None, description="Expected version for optimistic locking"),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), current_user: UserModel = Depends(current_active_user),
): ):
""" """
Deletes an expense and its associated splits. Deletes an expense and its associated splits.
@ -274,7 +274,7 @@ async def delete_expense_record(
async def create_new_settlement( async def create_new_settlement(
settlement_in: SettlementCreate, settlement_in: SettlementCreate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), current_user: UserModel = Depends(current_active_user),
): ):
logger.info(f"User {current_user.email} recording settlement in group {settlement_in.group_id}") logger.info(f"User {current_user.email} recording settlement in group {settlement_in.group_id}")
await crud_group.check_group_membership(db, group_id=settlement_in.group_id, user_id=current_user.id, action="record settlements in") await crud_group.check_group_membership(db, group_id=settlement_in.group_id, user_id=current_user.id, action="record settlements in")
@ -300,7 +300,7 @@ async def create_new_settlement(
async def get_settlement( async def get_settlement(
settlement_id: int, settlement_id: int,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), current_user: UserModel = Depends(current_active_user),
): ):
logger.info(f"User {current_user.email} requesting settlement ID {settlement_id}") logger.info(f"User {current_user.email} requesting settlement ID {settlement_id}")
settlement = await crud_settlement.get_settlement_by_id(db, settlement_id=settlement_id) settlement = await crud_settlement.get_settlement_by_id(db, settlement_id=settlement_id)
@ -322,7 +322,7 @@ async def list_group_settlements(
skip: int = Query(0, ge=0), skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=200), limit: int = Query(100, ge=1, le=200),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), current_user: UserModel = Depends(current_active_user),
): ):
logger.info(f"User {current_user.email} listing settlements for group ID {group_id}") logger.info(f"User {current_user.email} listing settlements for group ID {group_id}")
await crud_group.check_group_membership(db, group_id=group_id, user_id=current_user.id, action="list settlements for this group") await crud_group.check_group_membership(db, group_id=group_id, user_id=current_user.id, action="list settlements for this group")
@ -334,7 +334,7 @@ async def update_settlement_details(
settlement_id: int, settlement_id: int,
settlement_in: SettlementUpdate, settlement_in: SettlementUpdate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), current_user: UserModel = Depends(current_active_user),
): ):
""" """
Updates an existing settlement (description, settlement_date only). Updates an existing settlement (description, settlement_date only).
@ -388,7 +388,7 @@ async def delete_settlement_record(
settlement_id: int, settlement_id: int,
expected_version: Optional[int] = Query(None, description="Expected version for optimistic locking"), expected_version: Optional[int] = Query(None, description="Expected version for optimistic locking"),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), current_user: UserModel = Depends(current_active_user),
): ):
""" """
Deletes a settlement. Deletes a settlement.

View File

@ -6,7 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db from app.database import get_db
from app.api.dependencies import get_current_user from app.auth import current_active_user
from app.models import User as UserModel, UserRoleEnum # Import model and enum from app.models import User as UserModel, UserRoleEnum # Import model and enum
from app.schemas.group import GroupCreate, GroupPublic from app.schemas.group import GroupCreate, GroupPublic
from app.schemas.invite import InviteCodePublic from app.schemas.invite import InviteCodePublic
@ -37,7 +37,7 @@ router = APIRouter()
async def create_group( async def create_group(
group_in: GroupCreate, group_in: GroupCreate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), current_user: UserModel = Depends(current_active_user),
): ):
"""Creates a new group, adding the creator as the owner.""" """Creates a new group, adding the creator as the owner."""
logger.info(f"User {current_user.email} creating group: {group_in.name}") logger.info(f"User {current_user.email} creating group: {group_in.name}")
@ -55,7 +55,7 @@ async def create_group(
) )
async def read_user_groups( async def read_user_groups(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), current_user: UserModel = Depends(current_active_user),
): ):
"""Retrieves all groups the current user is a member of.""" """Retrieves all groups the current user is a member of."""
logger.info(f"Fetching groups for user: {current_user.email}") logger.info(f"Fetching groups for user: {current_user.email}")
@ -72,7 +72,7 @@ async def read_user_groups(
async def read_group( async def read_group(
group_id: int, group_id: int,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), current_user: UserModel = Depends(current_active_user),
): ):
"""Retrieves details for a specific group, including members, if the user is part of it.""" """Retrieves details for a specific group, including members, if the user is part of it."""
logger.info(f"User {current_user.email} requesting details for group ID: {group_id}") logger.info(f"User {current_user.email} requesting details for group ID: {group_id}")
@ -99,7 +99,7 @@ async def read_group(
async def create_group_invite( async def create_group_invite(
group_id: int, group_id: int,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), current_user: UserModel = Depends(current_active_user),
): ):
"""Generates a new invite code for the group. Requires owner/admin role (MVP: owner only).""" """Generates a new invite code for the group. Requires owner/admin role (MVP: owner only)."""
logger.info(f"User {current_user.email} attempting to create invite for group {group_id}") logger.info(f"User {current_user.email} attempting to create invite for group {group_id}")
@ -132,7 +132,7 @@ async def create_group_invite(
async def leave_group( async def leave_group(
group_id: int, group_id: int,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), current_user: UserModel = Depends(current_active_user),
): ):
"""Removes the current user from the specified group.""" """Removes the current user from the specified group."""
logger.info(f"User {current_user.email} attempting to leave group {group_id}") logger.info(f"User {current_user.email} attempting to leave group {group_id}")
@ -171,7 +171,7 @@ async def remove_group_member(
group_id: int, group_id: int,
user_id_to_remove: int, user_id_to_remove: int,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), current_user: UserModel = Depends(current_active_user),
): ):
"""Removes a specified user from the group. Requires current user to be owner.""" """Removes a specified user from the group. Requires current user to be owner."""
logger.info(f"Owner {current_user.email} attempting to remove user {user_id_to_remove} from group {group_id}") logger.info(f"Owner {current_user.email} attempting to remove user {user_id_to_remove} from group {group_id}")
@ -210,7 +210,7 @@ async def remove_group_member(
async def read_group_lists( async def read_group_lists(
group_id: int, group_id: int,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), current_user: UserModel = Depends(current_active_user),
): ):
"""Retrieves all lists belonging to a specific group, if the user is a member.""" """Retrieves all lists belonging to a specific group, if the user is a member."""
logger.info(f"User {current_user.email} requesting lists for group ID: {group_id}") logger.info(f"User {current_user.email} requesting lists for group ID: {group_id}")

View File

@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db from app.database import get_db
from app.api.dependencies import get_current_user from app.auth import current_active_user
from app.models import User as UserModel, UserRoleEnum from app.models import User as UserModel, UserRoleEnum
from app.schemas.invite import InviteAccept from app.schemas.invite import InviteAccept
from app.schemas.message import Message from app.schemas.message import Message
@ -31,7 +31,7 @@ router = APIRouter()
async def accept_invite( async def accept_invite(
invite_in: InviteAccept, invite_in: InviteAccept,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), current_user: UserModel = Depends(current_active_user),
): ):
"""Accepts a group invite using the provided invite code.""" """Accepts a group invite using the provided invite code."""
logger.info(f"User {current_user.email} attempting to accept invite code: {invite_in.invite_code}") logger.info(f"User {current_user.email} attempting to accept invite code: {invite_in.invite_code}")

View File

@ -6,7 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException, status, Response, Query
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db from app.database import get_db
from app.api.dependencies import get_current_user from app.auth import current_active_user
# --- Import Models Correctly --- # --- Import Models Correctly ---
from app.models import User as UserModel from app.models import User as UserModel
from app.models import Item as ItemModel # <-- IMPORT Item and alias it from app.models import Item as ItemModel # <-- IMPORT Item and alias it
@ -24,7 +24,7 @@ router = APIRouter()
async def get_item_and_verify_access( async def get_item_and_verify_access(
item_id: int, item_id: int,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user) current_user: UserModel = Depends(current_active_user)
) -> ItemModel: ) -> ItemModel:
"""Dependency to get an item and verify the user has access to its list.""" """Dependency to get an item and verify the user has access to its list."""
item_db = await crud_item.get_item_by_id(db, item_id=item_id) item_db = await crud_item.get_item_by_id(db, item_id=item_id)
@ -53,7 +53,7 @@ async def create_list_item(
list_id: int, list_id: int,
item_in: ItemCreate, item_in: ItemCreate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), current_user: UserModel = Depends(current_active_user),
): ):
"""Adds a new item to a specific list. User must have access to the list.""" """Adds a new item to a specific list. User must have access to the list."""
user_email = current_user.email # Access email attribute before async operations user_email = current_user.email # Access email attribute before async operations
@ -81,7 +81,7 @@ async def create_list_item(
async def read_list_items( async def read_list_items(
list_id: int, list_id: int,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), current_user: UserModel = Depends(current_active_user),
# Add sorting/filtering params later if needed: sort_by: str = 'created_at', order: str = 'asc' # Add sorting/filtering params later if needed: sort_by: str = 'created_at', order: str = 'asc'
): ):
"""Retrieves all items for a specific list if the user has access.""" """Retrieves all items for a specific list if the user has access."""
@ -112,7 +112,7 @@ async def update_item(
item_in: ItemUpdate, item_in: ItemUpdate,
item_db: ItemModel = Depends(get_item_and_verify_access), # Use dependency to get item and check list access item_db: ItemModel = Depends(get_item_and_verify_access), # Use dependency to get item and check list access
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), # Need user ID for completed_by current_user: UserModel = Depends(current_active_user), # Need user ID for completed_by
): ):
""" """
Updates an item's details (name, quantity, is_complete, price). Updates an item's details (name, quantity, is_complete, price).
@ -153,7 +153,7 @@ async def delete_item(
expected_version: Optional[int] = Query(None, description="The expected version of the item to delete for optimistic locking."), expected_version: Optional[int] = Query(None, description="The expected version of the item to delete for optimistic locking."),
item_db: ItemModel = Depends(get_item_and_verify_access), # Use dependency to get item and check list access item_db: ItemModel = Depends(get_item_and_verify_access), # Use dependency to get item and check list access
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), # Log who deleted it current_user: UserModel = Depends(current_active_user), # Log who deleted it
): ):
""" """
Deletes an item. User must have access to the list the item belongs to. Deletes an item. User must have access to the list the item belongs to.

View File

@ -6,7 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException, status, Response, Query #
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db from app.database import get_db
from app.api.dependencies import get_current_user from app.auth import current_active_user
from app.models import User as UserModel from app.models import User as UserModel
from app.schemas.list import ListCreate, ListUpdate, ListPublic, ListDetail from app.schemas.list import ListCreate, ListUpdate, ListPublic, ListDetail
from app.schemas.message import Message # For simple responses from app.schemas.message import Message # For simple responses
@ -34,7 +34,7 @@ router = APIRouter()
async def create_list( async def create_list(
list_in: ListCreate, list_in: ListCreate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), current_user: UserModel = Depends(current_active_user),
): ):
""" """
Creates a new shopping list. Creates a new shopping list.
@ -64,7 +64,7 @@ async def create_list(
) )
async def read_lists( async def read_lists(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), current_user: UserModel = Depends(current_active_user),
# Add pagination parameters later if needed: skip: int = 0, limit: int = 100 # Add pagination parameters later if needed: skip: int = 0, limit: int = 100
): ):
""" """
@ -86,7 +86,7 @@ async def read_lists(
async def read_list( async def read_list(
list_id: int, list_id: int,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), current_user: UserModel = Depends(current_active_user),
): ):
""" """
Retrieves details for a specific list, including its items, Retrieves details for a specific list, including its items,
@ -111,7 +111,7 @@ async def update_list(
list_id: int, list_id: int,
list_in: ListUpdate, list_in: ListUpdate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), current_user: UserModel = Depends(current_active_user),
): ):
""" """
Updates a list's details (name, description, is_complete). Updates a list's details (name, description, is_complete).
@ -149,7 +149,7 @@ async def delete_list(
list_id: int, list_id: int,
expected_version: Optional[int] = Query(None, description="The expected version of the list to delete for optimistic locking."), expected_version: Optional[int] = Query(None, description="The expected version of the list to delete for optimistic locking."),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), current_user: UserModel = Depends(current_active_user),
): ):
""" """
Deletes a list. Requires user to be the creator of the list. Deletes a list. Requires user to be the creator of the list.
@ -184,7 +184,7 @@ async def delete_list(
async def read_list_status( async def read_list_status(
list_id: int, list_id: int,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), current_user: UserModel = Depends(current_active_user),
): ):
""" """
Retrieves the last update time for the list and its items, plus item count. Retrieves the last update time for the list and its items, plus item count.

View File

@ -4,7 +4,7 @@ from typing import List
from fastapi import APIRouter, Depends, UploadFile, File, HTTPException, status from fastapi import APIRouter, Depends, UploadFile, File, HTTPException, status
from google.api_core import exceptions as google_exceptions from google.api_core import exceptions as google_exceptions
from app.api.dependencies import get_current_user from app.auth import current_active_user
from app.models import User as UserModel from app.models import User as UserModel
from app.schemas.ocr import OcrExtractResponse from app.schemas.ocr import OcrExtractResponse
from app.core.gemini import extract_items_from_image_gemini, gemini_initialization_error, GeminiOCRService from app.core.gemini import extract_items_from_image_gemini, gemini_initialization_error, GeminiOCRService
@ -30,7 +30,7 @@ ocr_service = GeminiOCRService()
tags=["OCR"] tags=["OCR"]
) )
async def ocr_extract_items( async def ocr_extract_items(
current_user: UserModel = Depends(get_current_user), current_user: UserModel = Depends(current_active_user),
image_file: UploadFile = File(..., description="Image file (JPEG, PNG, WEBP) of the shopping list or receipt."), image_file: UploadFile = File(..., description="Image file (JPEG, PNG, WEBP) of the shopping list or receipt."),
): ):
""" """

View File

@ -1,30 +0,0 @@
# app/api/v1/endpoints/users.py
import logging
from fastapi import APIRouter, Depends, HTTPException
from app.api.dependencies import get_current_user # Import the dependency
from app.schemas.user import UserPublic # Import the response schema
from app.models import User as UserModel # Import the DB model for type hinting
logger = logging.getLogger(__name__)
router = APIRouter()
@router.get(
"/me",
response_model=UserPublic, # Use the public schema to avoid exposing hash
summary="Get Current User",
description="Retrieves the details of the currently authenticated user.",
tags=["Users"]
)
async def read_users_me(
current_user: UserModel = Depends(get_current_user) # Apply the dependency
):
"""
Returns the data for the user associated with the current valid access token.
"""
logger.info(f"Fetching details for current user: {current_user.email}")
# The 'current_user' object is the SQLAlchemy model instance returned by the dependency.
# Pydantic's response_model will automatically convert it using UserPublic schema.
return current_user
# Add other user-related endpoints here later (e.g., update user, list users (admin))

View File

@ -13,6 +13,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from authlib.integrations.starlette_client import OAuth from authlib.integrations.starlette_client import OAuth
from starlette.config import Config from starlette.config import Config
from starlette.middleware.sessions import SessionMiddleware from starlette.middleware.sessions import SessionMiddleware
from starlette.responses import Response
from .database import get_async_session from .database import get_async_session
from .models import User from .models import User
@ -60,7 +61,7 @@ class UserManager(IntegerIDMixin, BaseUserManager[User, int]):
print(f"Verification requested for user {user.id}. Verification token: {token}") print(f"Verification requested for user {user.id}. Verification token: {token}")
async def on_after_login( async def on_after_login(
self, user: User, request: Optional[Request] = None self, user: User, request: Optional[Request] = None, response: Optional[Response] = None
): ):
print(f"User {user.id} has logged in.") print(f"User {user.id} has logged in.")
@ -73,7 +74,7 @@ async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db
bearer_transport = BearerTransport(tokenUrl="auth/jwt/login") bearer_transport = BearerTransport(tokenUrl="auth/jwt/login")
def get_jwt_strategy() -> JWTStrategy: def get_jwt_strategy() -> JWTStrategy:
return JWTStrategy(secret=settings.SECRET_KEY, lifetime_seconds=3600) return JWTStrategy(secret=settings.SECRET_KEY, lifetime_seconds=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60)
auth_backend = AuthenticationBackend( auth_backend = AuthenticationBackend(
name="jwt", name="jwt",

View File

@ -17,8 +17,7 @@ class Settings(BaseSettings):
# --- JWT Settings --- (SECRET_KEY is used by FastAPI-Users) # --- JWT Settings --- (SECRET_KEY is used by FastAPI-Users)
SECRET_KEY: str # Must be set via environment variable SECRET_KEY: str # Must be set via environment variable
# ALGORITHM: str = "HS256" # Handled by FastAPI-Users strategy # ALGORITHM: str = "HS256" # Handled by FastAPI-Users strategy
# ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 # Handled by FastAPI-Users strategy # ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 # This specific line is commented, the one under Session Settings is used.
# REFRESH_TOKEN_EXPIRE_MINUTES: int = 10080 # Handled by FastAPI-Users strategy
# --- OCR Settings --- # --- OCR Settings ---
MAX_FILE_SIZE_MB: int = 10 # Maximum allowed file size for OCR processing MAX_FILE_SIZE_MB: int = 10 # Maximum allowed file size for OCR processing
@ -60,10 +59,12 @@ Organic Bananas
API_DOCS_URL: str = "/api/docs" API_DOCS_URL: str = "/api/docs"
API_REDOC_URL: str = "/api/redoc" API_REDOC_URL: str = "/api/redoc"
CORS_ORIGINS: list[str] = [ CORS_ORIGINS: list[str] = [
"http://localhost:5173", "http://localhost:5173", # Frontend dev server
"http://localhost:8000", "http://localhost:5174", # Alternative Vite port
# Add your deployed frontend URL here later "http://localhost:8000", # Backend server
# "https://your-frontend-domain.com", "http://127.0.0.1:5173", # Frontend with IP
"http://127.0.0.1:5174", # Alternative Vite with IP
"http://127.0.0.1:8000", # Backend with IP
] ]
FRONTEND_URL: str = "http://localhost:5173" # URL for the frontend application FRONTEND_URL: str = "http://localhost:5173" # URL for the frontend application
@ -81,15 +82,6 @@ Organic Bananas
HEALTH_STATUS_OK: str = "ok" HEALTH_STATUS_OK: str = "ok"
HEALTH_STATUS_ERROR: str = "error" HEALTH_STATUS_ERROR: str = "error"
# --- Auth Settings --- (These are largely handled by FastAPI-Users now)
OAUTH2_TOKEN_URL: str = "/api/v1/auth/login" # FastAPI-Users has its own token URL
TOKEN_TYPE: str = "bearer"
# AUTH_HEADER_PREFIX: str = "Bearer"
# AUTH_HEADER_NAME: str = "WWW-Authenticate"
# AUTH_CREDENTIALS_ERROR: str = "Could not validate credentials"
# AUTH_INVALID_CREDENTIALS: str = "Incorrect email or password"
# AUTH_NOT_AUTHENTICATED: str = "Not authenticated"
# --- HTTP Status Messages --- # --- HTTP Status Messages ---
HTTP_400_DETAIL: str = "Bad Request" HTTP_400_DETAIL: str = "Bad Request"
HTTP_401_DETAIL: str = "Unauthorized" HTTP_401_DETAIL: str = "Unauthorized"
@ -119,7 +111,7 @@ Organic Bananas
# Session Settings # Session Settings
SESSION_SECRET_KEY: str = "your-session-secret-key" # Change this in production SESSION_SECRET_KEY: str = "your-session-secret-key" # Change this in production
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
class Config: class Config:
env_file = ".env" env_file = ".env"
env_file_encoding = 'utf-8' env_file_encoding = 'utf-8'

View File

@ -45,120 +45,14 @@ def hash_password(password: str) -> str:
# --- JSON Web Tokens (JWT) --- # --- JSON Web Tokens (JWT) ---
# FastAPI-Users now handles all tokenization.
def create_access_token(subject: Union[str, Any], expires_delta: Optional[timedelta] = None) -> str:
"""
Creates a JWT access token.
Args:
subject: The subject of the token (e.g., user ID or email).
expires_delta: Optional timedelta object for token expiry. If None,
uses ACCESS_TOKEN_EXPIRE_MINUTES from settings.
Returns:
The encoded JWT access token string.
"""
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
)
# Data to encode in the token payload
to_encode = {"exp": expire, "sub": str(subject), "type": "access"}
encoded_jwt = jwt.encode(
to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM
)
return encoded_jwt
def create_refresh_token(subject: Union[str, Any], expires_delta: Optional[timedelta] = None) -> str:
"""
Creates a JWT refresh token.
Args:
subject: The subject of the token (e.g., user ID or email).
expires_delta: Optional timedelta object for token expiry. If None,
uses REFRESH_TOKEN_EXPIRE_MINUTES from settings.
Returns:
The encoded JWT refresh token string.
"""
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(
minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES
)
# Data to encode in the token payload
to_encode = {"exp": expire, "sub": str(subject), "type": "refresh"}
encoded_jwt = jwt.encode(
to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM
)
return encoded_jwt
def verify_access_token(token: str) -> Optional[dict]:
"""
Verifies a JWT access token and returns its payload if valid.
Args:
token: The JWT token string to verify.
Returns:
The decoded token payload (dict) if the token is valid and not expired,
otherwise None.
"""
try:
# Decode the token. This also automatically verifies:
# - Signature (using SECRET_KEY and ALGORITHM)
# - Expiration ('exp' claim)
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
)
if payload.get("type") != "access":
raise JWTError("Invalid token type")
return payload
except JWTError as e:
# Handles InvalidSignatureError, ExpiredSignatureError, etc.
print(f"JWT Error: {e}") # Log the error for debugging
return None
except Exception as e:
# Handle other potential unexpected errors during decoding
print(f"Unexpected error decoding JWT: {e}")
return None
def verify_refresh_token(token: str) -> Optional[dict]:
"""
Verifies a JWT refresh token and returns its payload if valid.
Args:
token: The JWT token string to verify.
Returns:
The decoded token payload (dict) if the token is valid, not expired,
and is a refresh token, otherwise None.
"""
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
)
if payload.get("type") != "refresh":
raise JWTError("Invalid token type")
return payload
except JWTError as e:
print(f"JWT Error: {e}") # Log the error for debugging
return None
except Exception as e:
print(f"Unexpected error decoding JWT: {e}")
return None
# You might add a function here later to extract the 'sub' (subject/user id) # You might add a function here later to extract the 'sub' (subject/user id)
# specifically, often used in dependency injection for authentication. # specifically, often used in dependency injection for authentication.
# def get_subject_from_token(token: str) -> Optional[str]: # def get_subject_from_token(token: str) -> Optional[str]:
# payload = verify_access_token(token) # # This would need to use FastAPI-Users' token verification if ever implemented
# # For example, by decoding the token using the strategy from the auth backend
# payload = {} # Placeholder for actual token decoding logic
# if payload: # if payload:
# return payload.get("sub") # return payload.get("sub")
# return None # return None

View File

@ -49,23 +49,13 @@ app.add_middleware(
) )
# --- CORS Middleware --- # --- CORS Middleware ---
# Define allowed origins. Be specific in production!
# Use ["*"] for wide open access during early development if needed,
# but restrict it as soon as possible.
# SvelteKit default dev port is 5173
origins = [
"http://localhost:5174",
"http://localhost:8000", # Allow requests from the API itself (e.g., Swagger UI)
# Add your deployed frontend URL here later
# "https://your-frontend-domain.com",
]
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=settings.CORS_ORIGINS, allow_origins=settings.CORS_ORIGINS,
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
expose_headers=["*"]
) )
# --- End CORS Middleware --- # --- End CORS Middleware ---
@ -114,7 +104,7 @@ async def read_root():
Useful for basic reachability checks. Useful for basic reachability checks.
""" """
logger.info("Root endpoint '/' accessed.") logger.info("Root endpoint '/' accessed.")
return {"message": settings.ROOT_MESSAGE} return {"message": "Welcome to the API"}
# --- End Root Endpoint --- # --- End Root Endpoint ---

View File

@ -12,6 +12,16 @@ class UserBase(BaseModel):
class UserCreate(UserBase): class UserCreate(UserBase):
password: str password: str
def create_update_dict(self):
return {
"email": self.email,
"name": self.name,
"password": self.password,
"is_active": True,
"is_superuser": False,
"is_verified": False
}
# Properties to receive via API on update # Properties to receive via API on update
class UserUpdate(UserBase): class UserUpdate(UserBase):
password: Optional[str] = None password: Optional[str] = None

View File

@ -8,24 +8,23 @@ export const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:80
export const API_ENDPOINTS = { export const API_ENDPOINTS = {
// Auth // Auth
AUTH: { AUTH: {
LOGIN: '/auth/login', LOGIN: '/auth/jwt/login',
SIGNUP: '/auth/signup', SIGNUP: '/auth/register',
REFRESH_TOKEN: '/auth/refresh-token', LOGOUT: '/auth/jwt/logout',
LOGOUT: '/auth/logout', VERIFY_EMAIL: '/auth/verify',
VERIFY_EMAIL: '/auth/verify-email', RESET_PASSWORD: '/auth/forgot-password',
RESET_PASSWORD: '/auth/reset-password',
FORGOT_PASSWORD: '/auth/forgot-password', FORGOT_PASSWORD: '/auth/forgot-password',
}, },
// Users // Users
USERS: { USERS: {
PROFILE: '/users/me', PROFILE: '/users/me',
UPDATE_PROFILE: '/users/me', UPDATE_PROFILE: '/api/v1/users/me',
PASSWORD: '/users/password', PASSWORD: '/api/v1/users/password',
AVATAR: '/users/avatar', AVATAR: '/api/v1/users/avatar',
SETTINGS: '/users/settings', SETTINGS: '/api/v1/users/settings',
NOTIFICATIONS: '/users/notifications', NOTIFICATIONS: '/api/v1/users/notifications',
PREFERENCES: '/users/preferences', PREFERENCES: '/api/v1/users/preferences',
}, },
// Lists // Lists

View File

@ -9,6 +9,7 @@ const api = axios.create({
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
withCredentials: true, // Enable sending cookies and authentication headers
}); });
// Request interceptor // Request interceptor
@ -46,7 +47,7 @@ api.interceptors.response.use(
// Use the store's refresh mechanism if it already handles API call and token setting // Use the store's refresh mechanism if it already handles API call and token setting
// However, the interceptor is specifically for retrying requests, so direct call is fine here // However, the interceptor is specifically for retrying requests, so direct call is fine here
// as long as it correctly updates tokens for the subsequent retry. // as long as it correctly updates tokens for the subsequent retry.
const response = await api.post('/api/v1/auth/refresh', { const response = await api.post('/auth/jwt/refresh', {
refresh_token: refreshTokenValue, refresh_token: refreshTokenValue,
}); });
@ -77,10 +78,8 @@ export { api, globalAxios };
import { API_VERSION, API_ENDPOINTS } from '@/config/api-config'; import { API_VERSION, API_ENDPOINTS } from '@/config/api-config';
export const getApiUrl = (endpoint: string): string => { export const getApiUrl = (endpoint: string): string => {
// Assuming API_BASE_URL already includes http://localhost:8000 // The API_ENDPOINTS already include the full path, so we just need to combine with base URL
// and endpoint starts with / return `${API_BASE_URL}${endpoint}`;
// The original `getApiUrl` added /api/v1, ensure this is correct for your setup
return `${API_BASE_URL}/api/${API_VERSION}${endpoint}`;
}; };
export const apiClient = { export const apiClient = {

View File

@ -7,7 +7,6 @@ import router from '@/router';
interface AuthState { interface AuthState {
accessToken: string | null; accessToken: string | null;
refreshToken: string | null;
user: { user: {
email: string; email: string;
name: string; name: string;
@ -18,7 +17,6 @@ interface AuthState {
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', () => {
// State // State
const accessToken = ref<string | null>(localStorage.getItem('token')); const accessToken = ref<string | null>(localStorage.getItem('token'));
const refreshToken = ref<string | null>(localStorage.getItem('refresh_token'));
const user = ref<AuthState['user']>(null); const user = ref<AuthState['user']>(null);
// Getters // Getters
@ -26,19 +24,15 @@ export const useAuthStore = defineStore('auth', () => {
const getUser = computed(() => user.value); const getUser = computed(() => user.value);
// Actions // Actions
const setTokens = (tokens: { access_token: string; refresh_token: string }) => { const setTokens = (tokens: { access_token: string }) => {
accessToken.value = tokens.access_token; accessToken.value = tokens.access_token;
refreshToken.value = tokens.refresh_token;
localStorage.setItem('token', tokens.access_token); localStorage.setItem('token', tokens.access_token);
localStorage.setItem('refresh_token', tokens.refresh_token);
}; };
const clearTokens = () => { const clearTokens = () => {
accessToken.value = null; accessToken.value = null;
refreshToken.value = null;
user.value = null; user.value = null;
localStorage.removeItem('token'); localStorage.removeItem('token');
localStorage.removeItem('refresh_token');
}; };
const setUser = (userData: AuthState['user']) => { const setUser = (userData: AuthState['user']) => {
@ -56,21 +50,9 @@ export const useAuthStore = defineStore('auth', () => {
return response.data; return response.data;
} catch (error: any) { } catch (error: any) {
console.error('AuthStore: Failed to fetch current user:', error); console.error('AuthStore: Failed to fetch current user:', error);
if (error.response?.status === 401) {
try {
await refreshAccessToken();
const response = await apiClient.get(API_ENDPOINTS.USERS.PROFILE);
setUser(response.data);
return response.data;
} catch (refreshOrRetryError) {
console.error('AuthStore: Failed to refresh token or fetch user after refresh:', refreshOrRetryError);
return null;
}
} else {
clearTokens(); clearTokens();
return null; return null;
} }
}
}; };
const login = async (email: string, password: string) => { const login = async (email: string, password: string) => {
@ -84,8 +66,8 @@ export const useAuthStore = defineStore('auth', () => {
}, },
}); });
const { access_token, refresh_token } = response.data; const { access_token } = response.data;
setTokens({ access_token, refresh_token }); setTokens({ access_token });
await fetchCurrentUser(); await fetchCurrentUser();
return response.data; return response.data;
}; };
@ -95,29 +77,6 @@ export const useAuthStore = defineStore('auth', () => {
return response.data; return response.data;
}; };
const refreshAccessToken = async () => {
if (!refreshToken.value) {
clearTokens();
await router.push('/auth/login');
throw new Error('No refresh token available');
}
try {
const response = await apiClient.post(API_ENDPOINTS.AUTH.REFRESH_TOKEN, {
refresh_token: refreshToken.value,
});
const { access_token, refresh_token: newRefreshToken } = response.data;
setTokens({ access_token, refresh_token: newRefreshToken });
return response.data;
} catch (error) {
console.error('AuthStore: Refresh token failed:', error);
clearTokens();
await router.push('/auth/login');
throw error;
}
};
const logout = async () => { const logout = async () => {
clearTokens(); clearTokens();
await router.push('/auth/login'); await router.push('/auth/login');
@ -125,7 +84,6 @@ export const useAuthStore = defineStore('auth', () => {
return { return {
accessToken, accessToken,
refreshToken,
user, user,
isAuthenticated, isAuthenticated,
getUser, getUser,
@ -135,7 +93,6 @@ export const useAuthStore = defineStore('auth', () => {
fetchCurrentUser, fetchCurrentUser,
login, login,
signup, signup,
refreshAccessToken,
logout, logout,
}; };
}); });