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:
parent
72b988b79b
commit
3f0cfff9f1
@ -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
|
@ -1,8 +1,6 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
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 invites
|
||||
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.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(invites.router, prefix="/invites", tags=["Invites"])
|
||||
api_router_v1.include_router(lists.router, prefix="/lists", tags=["Lists"])
|
||||
|
@ -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
|
||||
)
|
@ -7,7 +7,7 @@ from sqlalchemy.orm import Session, selectinload
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
|
||||
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,
|
||||
@ -41,7 +41,7 @@ router = APIRouter()
|
||||
async def get_list_cost_summary(
|
||||
list_id: int,
|
||||
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,
|
||||
@ -184,7 +184,7 @@ async def get_list_cost_summary(
|
||||
async def get_group_balance_summary(
|
||||
group_id: int,
|
||||
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.
|
||||
|
@ -6,7 +6,7 @@ from sqlalchemy import select
|
||||
from typing import List as PyList, Optional, Sequence
|
||||
|
||||
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.schemas.expense import (
|
||||
ExpenseCreate, ExpensePublic,
|
||||
@ -47,7 +47,7 @@ async def check_list_access_for_financials(db: AsyncSession, list_id: int, user_
|
||||
async def create_new_expense(
|
||||
expense_in: ExpenseCreate,
|
||||
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}")
|
||||
effective_group_id = expense_in.group_id
|
||||
@ -110,7 +110,7 @@ async def create_new_expense(
|
||||
async def get_expense(
|
||||
expense_id: int,
|
||||
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}")
|
||||
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),
|
||||
limit: int = Query(100, ge=1, le=200),
|
||||
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}")
|
||||
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),
|
||||
limit: int = Query(100, ge=1, le=200),
|
||||
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}")
|
||||
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_in: ExpenseUpdate,
|
||||
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).
|
||||
@ -210,7 +210,7 @@ async def delete_expense_record(
|
||||
expense_id: int,
|
||||
expected_version: Optional[int] = Query(None, description="Expected version for optimistic locking"),
|
||||
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.
|
||||
@ -274,7 +274,7 @@ async def delete_expense_record(
|
||||
async def create_new_settlement(
|
||||
settlement_in: SettlementCreate,
|
||||
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}")
|
||||
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(
|
||||
settlement_id: int,
|
||||
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}")
|
||||
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),
|
||||
limit: int = Query(100, ge=1, le=200),
|
||||
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}")
|
||||
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_in: SettlementUpdate,
|
||||
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).
|
||||
@ -388,7 +388,7 @@ async def delete_settlement_record(
|
||||
settlement_id: int,
|
||||
expected_version: Optional[int] = Query(None, description="Expected version for optimistic locking"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: UserModel = Depends(get_current_user),
|
||||
current_user: UserModel = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Deletes a settlement.
|
||||
|
@ -6,7 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
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.schemas.group import GroupCreate, GroupPublic
|
||||
from app.schemas.invite import InviteCodePublic
|
||||
@ -37,7 +37,7 @@ router = APIRouter()
|
||||
async def create_group(
|
||||
group_in: GroupCreate,
|
||||
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."""
|
||||
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(
|
||||
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."""
|
||||
logger.info(f"Fetching groups for user: {current_user.email}")
|
||||
@ -72,7 +72,7 @@ async def read_user_groups(
|
||||
async def read_group(
|
||||
group_id: int,
|
||||
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."""
|
||||
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(
|
||||
group_id: int,
|
||||
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)."""
|
||||
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(
|
||||
group_id: int,
|
||||
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."""
|
||||
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,
|
||||
user_id_to_remove: int,
|
||||
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."""
|
||||
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(
|
||||
group_id: int,
|
||||
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."""
|
||||
logger.info(f"User {current_user.email} requesting lists for group ID: {group_id}")
|
||||
|
@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
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.schemas.invite import InviteAccept
|
||||
from app.schemas.message import Message
|
||||
@ -31,7 +31,7 @@ router = APIRouter()
|
||||
async def accept_invite(
|
||||
invite_in: InviteAccept,
|
||||
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."""
|
||||
logger.info(f"User {current_user.email} attempting to accept invite code: {invite_in.invite_code}")
|
||||
|
@ -6,7 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException, status, Response, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.api.dependencies import get_current_user
|
||||
from app.auth import current_active_user
|
||||
# --- Import Models Correctly ---
|
||||
from app.models import User as UserModel
|
||||
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(
|
||||
item_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: UserModel = Depends(get_current_user)
|
||||
current_user: UserModel = Depends(current_active_user)
|
||||
) -> ItemModel:
|
||||
"""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)
|
||||
@ -53,7 +53,7 @@ async def create_list_item(
|
||||
list_id: int,
|
||||
item_in: ItemCreate,
|
||||
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."""
|
||||
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(
|
||||
list_id: int,
|
||||
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'
|
||||
):
|
||||
"""Retrieves all items for a specific list if the user has access."""
|
||||
@ -112,7 +112,7 @@ async def update_item(
|
||||
item_in: ItemUpdate,
|
||||
item_db: ItemModel = Depends(get_item_and_verify_access), # Use dependency to get item and check list access
|
||||
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).
|
||||
@ -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."),
|
||||
item_db: ItemModel = Depends(get_item_and_verify_access), # Use dependency to get item and check list access
|
||||
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.
|
||||
|
@ -6,7 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException, status, Response, Query #
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
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.schemas.list import ListCreate, ListUpdate, ListPublic, ListDetail
|
||||
from app.schemas.message import Message # For simple responses
|
||||
@ -34,7 +34,7 @@ router = APIRouter()
|
||||
async def create_list(
|
||||
list_in: ListCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: UserModel = Depends(get_current_user),
|
||||
current_user: UserModel = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Creates a new shopping list.
|
||||
@ -64,7 +64,7 @@ async def create_list(
|
||||
)
|
||||
async def read_lists(
|
||||
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
|
||||
):
|
||||
"""
|
||||
@ -86,7 +86,7 @@ async def read_lists(
|
||||
async def read_list(
|
||||
list_id: int,
|
||||
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,
|
||||
@ -111,7 +111,7 @@ async def update_list(
|
||||
list_id: int,
|
||||
list_in: ListUpdate,
|
||||
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).
|
||||
@ -149,7 +149,7 @@ async def delete_list(
|
||||
list_id: int,
|
||||
expected_version: Optional[int] = Query(None, description="The expected version of the list to delete for optimistic locking."),
|
||||
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.
|
||||
@ -184,7 +184,7 @@ async def delete_list(
|
||||
async def read_list_status(
|
||||
list_id: int,
|
||||
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.
|
||||
|
@ -4,7 +4,7 @@ from typing import List
|
||||
from fastapi import APIRouter, Depends, UploadFile, File, HTTPException, status
|
||||
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.schemas.ocr import OcrExtractResponse
|
||||
from app.core.gemini import extract_items_from_image_gemini, gemini_initialization_error, GeminiOCRService
|
||||
@ -30,7 +30,7 @@ ocr_service = GeminiOCRService()
|
||||
tags=["OCR"]
|
||||
)
|
||||
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."),
|
||||
):
|
||||
"""
|
||||
|
@ -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))
|
@ -13,6 +13,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from authlib.integrations.starlette_client import OAuth
|
||||
from starlette.config import Config
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
from starlette.responses import Response
|
||||
|
||||
from .database import get_async_session
|
||||
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}")
|
||||
|
||||
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.")
|
||||
|
||||
@ -73,7 +74,7 @@ async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db
|
||||
bearer_transport = BearerTransport(tokenUrl="auth/jwt/login")
|
||||
|
||||
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(
|
||||
name="jwt",
|
||||
|
@ -17,8 +17,7 @@ class Settings(BaseSettings):
|
||||
# --- JWT Settings --- (SECRET_KEY is used by FastAPI-Users)
|
||||
SECRET_KEY: str # Must be set via environment variable
|
||||
# ALGORITHM: str = "HS256" # Handled by FastAPI-Users strategy
|
||||
# ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 # Handled by FastAPI-Users strategy
|
||||
# REFRESH_TOKEN_EXPIRE_MINUTES: int = 10080 # Handled by FastAPI-Users strategy
|
||||
# ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 # This specific line is commented, the one under Session Settings is used.
|
||||
|
||||
# --- OCR Settings ---
|
||||
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_REDOC_URL: str = "/api/redoc"
|
||||
CORS_ORIGINS: list[str] = [
|
||||
"http://localhost:5173",
|
||||
"http://localhost:8000",
|
||||
# Add your deployed frontend URL here later
|
||||
# "https://your-frontend-domain.com",
|
||||
"http://localhost:5173", # Frontend dev server
|
||||
"http://localhost:5174", # Alternative Vite port
|
||||
"http://localhost:8000", # Backend server
|
||||
"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
|
||||
|
||||
@ -81,15 +82,6 @@ Organic Bananas
|
||||
HEALTH_STATUS_OK: str = "ok"
|
||||
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_400_DETAIL: str = "Bad Request"
|
||||
HTTP_401_DETAIL: str = "Unauthorized"
|
||||
@ -119,7 +111,7 @@ Organic Bananas
|
||||
|
||||
# Session Settings
|
||||
SESSION_SECRET_KEY: str = "your-session-secret-key" # Change this in production
|
||||
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
env_file_encoding = 'utf-8'
|
||||
|
@ -45,120 +45,14 @@ def hash_password(password: str) -> str:
|
||||
|
||||
|
||||
# --- JSON Web Tokens (JWT) ---
|
||||
|
||||
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
|
||||
# FastAPI-Users now handles all tokenization.
|
||||
|
||||
# You might add a function here later to extract the 'sub' (subject/user id)
|
||||
# specifically, often used in dependency injection for authentication.
|
||||
# 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:
|
||||
# return payload.get("sub")
|
||||
# return None
|
@ -49,23 +49,13 @@ app.add_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(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.CORS_ORIGINS,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
expose_headers=["*"]
|
||||
)
|
||||
# --- End CORS Middleware ---
|
||||
|
||||
@ -114,7 +104,7 @@ async def read_root():
|
||||
Useful for basic reachability checks.
|
||||
"""
|
||||
logger.info("Root endpoint '/' accessed.")
|
||||
return {"message": settings.ROOT_MESSAGE}
|
||||
return {"message": "Welcome to the API"}
|
||||
# --- End Root Endpoint ---
|
||||
|
||||
|
||||
|
@ -12,6 +12,16 @@ class UserBase(BaseModel):
|
||||
class UserCreate(UserBase):
|
||||
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
|
||||
class UserUpdate(UserBase):
|
||||
password: Optional[str] = None
|
||||
|
@ -8,24 +8,23 @@ export const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:80
|
||||
export const API_ENDPOINTS = {
|
||||
// Auth
|
||||
AUTH: {
|
||||
LOGIN: '/auth/login',
|
||||
SIGNUP: '/auth/signup',
|
||||
REFRESH_TOKEN: '/auth/refresh-token',
|
||||
LOGOUT: '/auth/logout',
|
||||
VERIFY_EMAIL: '/auth/verify-email',
|
||||
RESET_PASSWORD: '/auth/reset-password',
|
||||
LOGIN: '/auth/jwt/login',
|
||||
SIGNUP: '/auth/register',
|
||||
LOGOUT: '/auth/jwt/logout',
|
||||
VERIFY_EMAIL: '/auth/verify',
|
||||
RESET_PASSWORD: '/auth/forgot-password',
|
||||
FORGOT_PASSWORD: '/auth/forgot-password',
|
||||
},
|
||||
|
||||
// Users
|
||||
USERS: {
|
||||
PROFILE: '/users/me',
|
||||
UPDATE_PROFILE: '/users/me',
|
||||
PASSWORD: '/users/password',
|
||||
AVATAR: '/users/avatar',
|
||||
SETTINGS: '/users/settings',
|
||||
NOTIFICATIONS: '/users/notifications',
|
||||
PREFERENCES: '/users/preferences',
|
||||
UPDATE_PROFILE: '/api/v1/users/me',
|
||||
PASSWORD: '/api/v1/users/password',
|
||||
AVATAR: '/api/v1/users/avatar',
|
||||
SETTINGS: '/api/v1/users/settings',
|
||||
NOTIFICATIONS: '/api/v1/users/notifications',
|
||||
PREFERENCES: '/api/v1/users/preferences',
|
||||
},
|
||||
|
||||
// Lists
|
||||
|
@ -9,6 +9,7 @@ const api = axios.create({
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
withCredentials: true, // Enable sending cookies and authentication headers
|
||||
});
|
||||
|
||||
// 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
|
||||
// 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.
|
||||
const response = await api.post('/api/v1/auth/refresh', {
|
||||
const response = await api.post('/auth/jwt/refresh', {
|
||||
refresh_token: refreshTokenValue,
|
||||
});
|
||||
|
||||
@ -77,10 +78,8 @@ export { api, globalAxios };
|
||||
import { API_VERSION, API_ENDPOINTS } from '@/config/api-config';
|
||||
|
||||
export const getApiUrl = (endpoint: string): string => {
|
||||
// Assuming API_BASE_URL already includes http://localhost:8000
|
||||
// and endpoint starts with /
|
||||
// The original `getApiUrl` added /api/v1, ensure this is correct for your setup
|
||||
return `${API_BASE_URL}/api/${API_VERSION}${endpoint}`;
|
||||
// The API_ENDPOINTS already include the full path, so we just need to combine with base URL
|
||||
return `${API_BASE_URL}${endpoint}`;
|
||||
};
|
||||
|
||||
export const apiClient = {
|
||||
|
@ -7,7 +7,6 @@ import router from '@/router';
|
||||
|
||||
interface AuthState {
|
||||
accessToken: string | null;
|
||||
refreshToken: string | null;
|
||||
user: {
|
||||
email: string;
|
||||
name: string;
|
||||
@ -18,7 +17,6 @@ interface AuthState {
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
// State
|
||||
const accessToken = ref<string | null>(localStorage.getItem('token'));
|
||||
const refreshToken = ref<string | null>(localStorage.getItem('refresh_token'));
|
||||
const user = ref<AuthState['user']>(null);
|
||||
|
||||
// Getters
|
||||
@ -26,19 +24,15 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
const getUser = computed(() => user.value);
|
||||
|
||||
// Actions
|
||||
const setTokens = (tokens: { access_token: string; refresh_token: string }) => {
|
||||
const setTokens = (tokens: { access_token: string }) => {
|
||||
accessToken.value = tokens.access_token;
|
||||
refreshToken.value = tokens.refresh_token;
|
||||
localStorage.setItem('token', tokens.access_token);
|
||||
localStorage.setItem('refresh_token', tokens.refresh_token);
|
||||
};
|
||||
|
||||
const clearTokens = () => {
|
||||
accessToken.value = null;
|
||||
refreshToken.value = null;
|
||||
user.value = null;
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('refresh_token');
|
||||
};
|
||||
|
||||
const setUser = (userData: AuthState['user']) => {
|
||||
@ -56,20 +50,8 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
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();
|
||||
return null;
|
||||
}
|
||||
clearTokens();
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@ -84,8 +66,8 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { access_token, refresh_token } = response.data;
|
||||
setTokens({ access_token, refresh_token });
|
||||
const { access_token } = response.data;
|
||||
setTokens({ access_token });
|
||||
await fetchCurrentUser();
|
||||
return response.data;
|
||||
};
|
||||
@ -95,29 +77,6 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
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 () => {
|
||||
clearTokens();
|
||||
await router.push('/auth/login');
|
||||
@ -125,7 +84,6 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
user,
|
||||
isAuthenticated,
|
||||
getUser,
|
||||
@ -135,7 +93,6 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
fetchCurrentUser,
|
||||
login,
|
||||
signup,
|
||||
refreshAccessToken,
|
||||
logout,
|
||||
};
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user