From 992cdbd8dd67ab1017e618726049e660ca5f76af Mon Sep 17 00:00:00 2001 From: Muhammad Raza Bhatti Date: Tue, 27 May 2025 15:25:14 +0500 Subject: [PATCH 1/8] decoupled validators, slight user routers refactor --- app/routers/users.py | 16 ++++++++++++---- app/validators/auth.py | 5 +++++ app/validators/users.py | 7 +------ 3 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 app/validators/auth.py diff --git a/app/routers/users.py b/app/routers/users.py index c6cc938..2d564c4 100644 --- a/app/routers/users.py +++ b/app/routers/users.py @@ -1,8 +1,13 @@ -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, HTTPException, status, Depends from fastapi.responses import JSONResponse +from typing import List + from sqlalchemy.orm import Session from app.db.session import get_db -from app.validators.users import UserCreate, UserUpdate, UserOut, TokenData + +from app.validators.users import UserCreate, UserUpdate, UserOut +from app.validators.auth import TokenData + from app.services.users import ( create_user_service, list_users_service, @@ -10,8 +15,11 @@ delete_user_service, update_user_service, ) -from typing import List -from app.utils.jwt import get_current_user, admin_required + +from app.utils.jwt import ( + get_current_user, + admin_required, +) router = APIRouter() diff --git a/app/validators/auth.py b/app/validators/auth.py new file mode 100644 index 0000000..8fbead5 --- /dev/null +++ b/app/validators/auth.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + +class TokenData(BaseModel): + user_id: int + role: str \ No newline at end of file diff --git a/app/validators/users.py b/app/validators/users.py index d061201..c8a12e7 100644 --- a/app/validators/users.py +++ b/app/validators/users.py @@ -23,9 +23,4 @@ class UserOut(BaseModel): updated_at: datetime class Config: - orm_mode = True - - -class TokenData(BaseModel): - user_id: int - role: str \ No newline at end of file + orm_mode = True \ No newline at end of file From 6ae3e6faf6baaa13406df79f64095749a935e715 Mon Sep 17 00:00:00 2001 From: Muhammad Raza Bhatti Date: Tue, 27 May 2025 15:55:52 +0500 Subject: [PATCH 2/8] setting up new db structure --- app/db/models/permissions.py | 10 ++++++++++ app/db/models/roles.py | 11 +++++++++++ app/db/models/rules.py | 13 +++++++++++++ app/db/models/users.py | 9 +++++---- app/db/seeds/permissions.py | 0 app/db/seeds/roles.py | 0 app/db/seeds/rules.py | 0 7 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 app/db/models/permissions.py create mode 100644 app/db/models/roles.py create mode 100644 app/db/models/rules.py create mode 100644 app/db/seeds/permissions.py create mode 100644 app/db/seeds/roles.py create mode 100644 app/db/seeds/rules.py diff --git a/app/db/models/permissions.py b/app/db/models/permissions.py new file mode 100644 index 0000000..889a754 --- /dev/null +++ b/app/db/models/permissions.py @@ -0,0 +1,10 @@ +# app/db/models/permission.py +from sqlalchemy import Column, String +from sqlalchemy.orm import relationship +from app.db.base import Base + +class Permission(Base): + __tablename__ = "permissions" + + name = Column(String(100), primary_key=True) # e.g. "can_create_users", "can_view_own_profile" + rules = relationship("Rule", back_populates="permission", cascade="all, delete-orphan") diff --git a/app/db/models/roles.py b/app/db/models/roles.py new file mode 100644 index 0000000..907dc9e --- /dev/null +++ b/app/db/models/roles.py @@ -0,0 +1,11 @@ +# app/db/models/role.py +from sqlalchemy import Column, String +from sqlalchemy.orm import relationship +from app.db.base import Base + +class Role(Base): + __tablename__ = "roles" + + name = Column(String(50), primary_key=True) # e.g. "admin", "customer" + users = relationship("User", back_populates="role") + rules = relationship("Rule", back_populates="role", cascade="all, delete-orphan") diff --git a/app/db/models/rules.py b/app/db/models/rules.py new file mode 100644 index 0000000..69f0701 --- /dev/null +++ b/app/db/models/rules.py @@ -0,0 +1,13 @@ +# app/db/models/rule.py +from sqlalchemy import Column, String, ForeignKey +from sqlalchemy.orm import relationship +from app.db.base import Base + +class Rule(Base): + __tablename__ = "rules" + + role_name = Column(String(50), ForeignKey("roles.name"), primary_key=True) + permission_name = Column(String(100), ForeignKey("permissions.name"), primary_key=True) + + role = relationship("Role", back_populates="rules") + permission = relationship("Permission", back_populates="rules") diff --git a/app/db/models/users.py b/app/db/models/users.py index 4f29f7b..c519d54 100644 --- a/app/db/models/users.py +++ b/app/db/models/users.py @@ -1,7 +1,7 @@ -from sqlalchemy import Column, Integer, String, DateTime, func +# app/db/models/user.py +from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, func from sqlalchemy.orm import relationship from app.db.base import Base -from app.db.models.orders import Order class User(Base): __tablename__ = "users" @@ -10,8 +10,9 @@ class User(Base): username = Column(String(255), unique=True, nullable=False) email = Column(String(255), unique=True, nullable=False) hashed_password = Column(String(255), nullable=False) - role = Column(String(50), nullable=False, default="customer") + role_name = Column(String(50), ForeignKey("roles.name"), nullable=False, default="customer") created_at = Column(DateTime, server_default=func.now(), nullable=False) updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False) - orders = relationship(Order, back_populates="user", cascade="all, delete-orphan") + role = relationship("Role", back_populates="users") + orders = relationship("Order", back_populates="user", cascade="all, delete-orphan") diff --git a/app/db/seeds/permissions.py b/app/db/seeds/permissions.py new file mode 100644 index 0000000..e69de29 diff --git a/app/db/seeds/roles.py b/app/db/seeds/roles.py new file mode 100644 index 0000000..e69de29 diff --git a/app/db/seeds/rules.py b/app/db/seeds/rules.py new file mode 100644 index 0000000..e69de29 From b9057807be01b2cfae7e67c07fafe2d0620077bf Mon Sep 17 00:00:00 2001 From: Muhammad Raza Bhatti Date: Tue, 27 May 2025 16:07:49 +0500 Subject: [PATCH 3/8] seeder added --- app/db/seed.py | 114 ++++++++++++++++++++++++++++++++++++ app/db/seeds/permissions.py | 0 app/db/seeds/roles.py | 0 app/db/seeds/rules.py | 0 4 files changed, 114 insertions(+) create mode 100644 app/db/seed.py delete mode 100644 app/db/seeds/permissions.py delete mode 100644 app/db/seeds/roles.py delete mode 100644 app/db/seeds/rules.py diff --git a/app/db/seed.py b/app/db/seed.py new file mode 100644 index 0000000..98ce99c --- /dev/null +++ b/app/db/seed.py @@ -0,0 +1,114 @@ +from app.db.session import SessionLocal +from app.db.models.roles import Role +from app.db.models.permissions import Permission +from app.db.models.rules import Rule +from app.db.models.users import User +from app.utils.hashing import hash_password + +import os +from dotenv import load_dotenv +load_dotenv() + +def seed_roles(db): + roles = [ + Role(name="admin"), + Role(name="customer"), + # add more roles if needed + ] + for role in roles: + existing = db.query(Role).filter(Role.name == role.name).first() + if not existing: + db.add(role) + db.commit() + + +def seed_permissions(db): + permissions = [ + # User related permissions + Permission(name="can_create_user"), + Permission(name="can_list_users"), + Permission(name="can_get_user"), + Permission(name="can_update_user"), + Permission(name="can_delete_user"), + + #Permission(name="can_get_own_profile"), + #Permission(name="can_update_own_profile"), + ] + for perm in permissions: + existing = db.query(Permission).filter(Permission.name == perm.name).first() + if not existing: + db.add(perm) + db.commit() + + +def seed_rules(db): + # Fetch roles + admin_role = db.query(Role).filter(Role.name == "admin").first() + customer_role = db.query(Role).filter(Role.name == "customer").first() + + # Fetch permissions + can_create_user = db.query(Permission).filter(Permission.name == "can_create_user").first() + can_list_users = db.query(Permission).filter(Permission.name == "can_list_users").first() + can_get_user = db.query(Permission).filter(Permission.name == "can_get_user").first() + can_update_user = db.query(Permission).filter(Permission.name == "can_update_user").first() + can_delete_user = db.query(Permission).filter(Permission.name == "can_delete_user").first() + + rules = [ + # Admin permissions - full user management + Rule(role_id=admin_role.id, permission_id=can_create_user.id), + Rule(role_id=admin_role.id, permission_id=can_list_users.id), + Rule(role_id=admin_role.id, permission_id=can_get_user.id), + Rule(role_id=admin_role.id, permission_id=can_update_user.id), + Rule(role_id=admin_role.id, permission_id=can_delete_user.id), + + # Customer permissions - maybe limited or none here + # Rule(role_id=customer_role.id, permission_id=can_get_own_profile.id), + # Rule(role_id=customer_role.id, permission_id=can_update_own_profile.id), + ] + + for rule in rules: + existing = db.query(Rule).filter( + Rule.role_id == rule.role_id, + Rule.permission_id == rule.permission_id + ).first() + if not existing: + db.add(rule) + + db.commit() + + + +def seed_admin_user(db): + admin_role = db.query(Role).filter(Role.name == "admin").first() + existing_admin = db.query(User).filter(User.role_name == admin_role.name).first() + if existing_admin: + print("Admin user already exists, skipping creation.") + return + + admin_username = os.getenv("ADMIN_USERNAME", "admin") + admin_email = os.getenv("ADMIN_EMAIL", "admin@example.com") + admin_password = os.getenv("ADMIN_PASSWORD", "adminpassword123") + + admin_user = User( + username=admin_username, + email=admin_email, + hashed_password=hash_password(admin_password), + role_name=admin_role.name + ) + db.add(admin_user) + db.commit() + print("Admin user created.") + +def seed_all(): + db = SessionLocal() + try: + seed_roles(db) + seed_permissions(db) + seed_rules(db) + seed_admin_user(db) + finally: + db.close() + + +if __name__ == "__main__": + seed_all() diff --git a/app/db/seeds/permissions.py b/app/db/seeds/permissions.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/db/seeds/roles.py b/app/db/seeds/roles.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/db/seeds/rules.py b/app/db/seeds/rules.py deleted file mode 100644 index e69de29..0000000 From a0311e5230ef315b9e56742b89081f843d83589d Mon Sep 17 00:00:00 2001 From: Muhammad Raza Bhatti Date: Tue, 27 May 2025 16:51:43 +0500 Subject: [PATCH 4/8] seeding completed, schema redefined --- app/db/alembic/env.py | 4 +- .../5cfc72e753ba_transforming_schema.py | 54 +++++++++++++++++++ app/db/models/orders.py | 1 + app/db/models/permissions.py | 3 +- app/db/models/roles.py | 3 +- app/db/models/rules.py | 1 - app/db/models/users.py | 7 +-- app/db/seed.py | 39 +++++++++----- 8 files changed, 89 insertions(+), 23 deletions(-) create mode 100644 app/db/alembic/versions/5cfc72e753ba_transforming_schema.py diff --git a/app/db/alembic/env.py b/app/db/alembic/env.py index 59e48c6..fe81294 100644 --- a/app/db/alembic/env.py +++ b/app/db/alembic/env.py @@ -18,8 +18,8 @@ def import_all_models(): # Correct the path to the models directory models_dir = os.path.join(os.path.dirname(__file__), "../models") - models_dir = os.path.abspath(models_dir) # Ensure it's an absolute path - + models_dir = os.path.abspath(models_dir) + for filename in os.listdir(models_dir): if filename.endswith(".py") and filename != "__init__.py": module_name = f"app.db.models.{filename[:-3]}" diff --git a/app/db/alembic/versions/5cfc72e753ba_transforming_schema.py b/app/db/alembic/versions/5cfc72e753ba_transforming_schema.py new file mode 100644 index 0000000..e4b6cdb --- /dev/null +++ b/app/db/alembic/versions/5cfc72e753ba_transforming_schema.py @@ -0,0 +1,54 @@ +"""transforming schema + +Revision ID: 5cfc72e753ba +Revises: 075b701a1a93 +Create Date: 2025-05-27 16:16:34.166953 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '5cfc72e753ba' +down_revision: Union[str, None] = '075b701a1a93' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('permissions', + sa.Column('name', sa.String(length=100), nullable=False), + sa.PrimaryKeyConstraint('name') + ) + op.create_table('roles', + sa.Column('name', sa.String(length=50), nullable=False), + sa.PrimaryKeyConstraint('name') + ) + op.create_table('rules', + sa.Column('role_name', sa.String(length=50), nullable=False), + sa.Column('permission_name', sa.String(length=100), nullable=False), + sa.ForeignKeyConstraint(['permission_name'], ['permissions.name'], ), + sa.ForeignKeyConstraint(['role_name'], ['roles.name'], ), + sa.PrimaryKeyConstraint('role_name', 'permission_name') + ) + op.add_column('users', sa.Column('role_name', sa.String(length=50), nullable=False)) + op.create_foreign_key(None, 'users', 'roles', ['role_name'], ['name']) + op.drop_column('users', 'role') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('users', sa.Column('role', sa.VARCHAR(length=50), autoincrement=False, nullable=False)) + op.drop_constraint(None, 'users', type_='foreignkey') + op.drop_column('users', 'role_name') + op.drop_table('rules') + op.drop_table('roles') + op.drop_table('permissions') + # ### end Alembic commands ### diff --git a/app/db/models/orders.py b/app/db/models/orders.py index eae63cb..3a93ef0 100644 --- a/app/db/models/orders.py +++ b/app/db/models/orders.py @@ -1,6 +1,7 @@ from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Numeric, func from sqlalchemy.orm import relationship from app.db.base import Base +#from app.db.models.users import User class Order(Base): __tablename__ = "orders" diff --git a/app/db/models/permissions.py b/app/db/models/permissions.py index 889a754..e9d8c3e 100644 --- a/app/db/models/permissions.py +++ b/app/db/models/permissions.py @@ -1,4 +1,3 @@ -# app/db/models/permission.py from sqlalchemy import Column, String from sqlalchemy.orm import relationship from app.db.base import Base @@ -6,5 +5,5 @@ class Permission(Base): __tablename__ = "permissions" - name = Column(String(100), primary_key=True) # e.g. "can_create_users", "can_view_own_profile" + name = Column(String(100), primary_key=True) rules = relationship("Rule", back_populates="permission", cascade="all, delete-orphan") diff --git a/app/db/models/roles.py b/app/db/models/roles.py index 907dc9e..10a0602 100644 --- a/app/db/models/roles.py +++ b/app/db/models/roles.py @@ -1,4 +1,3 @@ -# app/db/models/role.py from sqlalchemy import Column, String from sqlalchemy.orm import relationship from app.db.base import Base @@ -6,6 +5,6 @@ class Role(Base): __tablename__ = "roles" - name = Column(String(50), primary_key=True) # e.g. "admin", "customer" + name = Column(String(50), primary_key=True) users = relationship("User", back_populates="role") rules = relationship("Rule", back_populates="role", cascade="all, delete-orphan") diff --git a/app/db/models/rules.py b/app/db/models/rules.py index 69f0701..04d8840 100644 --- a/app/db/models/rules.py +++ b/app/db/models/rules.py @@ -1,4 +1,3 @@ -# app/db/models/rule.py from sqlalchemy import Column, String, ForeignKey from sqlalchemy.orm import relationship from app.db.base import Base diff --git a/app/db/models/users.py b/app/db/models/users.py index c519d54..011ebf6 100644 --- a/app/db/models/users.py +++ b/app/db/models/users.py @@ -1,7 +1,8 @@ -# app/db/models/user.py from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, func from sqlalchemy.orm import relationship from app.db.base import Base +from app.db.models.roles import Role +from app.db.models.orders import Order # Import Order model for relationship class User(Base): __tablename__ = "users" @@ -14,5 +15,5 @@ class User(Base): created_at = Column(DateTime, server_default=func.now(), nullable=False) updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False) - role = relationship("Role", back_populates="users") - orders = relationship("Order", back_populates="user", cascade="all, delete-orphan") + role = relationship(Role, back_populates="users") + orders = relationship(Order, back_populates="user", cascade="all, delete-orphan") # Use string reference \ No newline at end of file diff --git a/app/db/seed.py b/app/db/seed.py index 98ce99c..85bac00 100644 --- a/app/db/seed.py +++ b/app/db/seed.py @@ -7,8 +7,17 @@ import os from dotenv import load_dotenv + load_dotenv() +from sqlalchemy import text + +def clear_tables(db): + # Truncate with restart identity to reset auto-increment IDs + db.execute(text("TRUNCATE TABLE rules, permissions, roles, users RESTART IDENTITY CASCADE")) + db.commit() + + def seed_roles(db): roles = [ Role(name="admin"), @@ -30,9 +39,9 @@ def seed_permissions(db): Permission(name="can_get_user"), Permission(name="can_update_user"), Permission(name="can_delete_user"), - - #Permission(name="can_get_own_profile"), - #Permission(name="can_update_own_profile"), + + Permission(name="can_get_own_profile"), + Permission(name="can_update_own_profile"), ] for perm in permissions: existing = db.query(Permission).filter(Permission.name == perm.name).first() @@ -52,24 +61,27 @@ def seed_rules(db): can_get_user = db.query(Permission).filter(Permission.name == "can_get_user").first() can_update_user = db.query(Permission).filter(Permission.name == "can_update_user").first() can_delete_user = db.query(Permission).filter(Permission.name == "can_delete_user").first() + + can_get_own_profile = db.query(Permission).filter(Permission.name == "can_get_own_profile").first() + can_update_own_profile = db.query(Permission).filter(Permission.name == "can_update_own_profile").first() rules = [ # Admin permissions - full user management - Rule(role_id=admin_role.id, permission_id=can_create_user.id), - Rule(role_id=admin_role.id, permission_id=can_list_users.id), - Rule(role_id=admin_role.id, permission_id=can_get_user.id), - Rule(role_id=admin_role.id, permission_id=can_update_user.id), - Rule(role_id=admin_role.id, permission_id=can_delete_user.id), + Rule(role_name=admin_role.name, permission_name=can_create_user.name), + Rule(role_name=admin_role.name, permission_name=can_list_users.name), + Rule(role_name=admin_role.name, permission_name=can_get_user.name), + Rule(role_name=admin_role.name, permission_name=can_update_user.name), + Rule(role_name=admin_role.name, permission_name=can_delete_user.name), # Customer permissions - maybe limited or none here - # Rule(role_id=customer_role.id, permission_id=can_get_own_profile.id), - # Rule(role_id=customer_role.id, permission_id=can_update_own_profile.id), + Rule(role_name=customer_role.name, permission_name=can_get_own_profile.name), + Rule(role_name=customer_role.name, permission_name=can_update_own_profile.name), ] for rule in rules: existing = db.query(Rule).filter( - Rule.role_id == rule.role_id, - Rule.permission_id == rule.permission_id + Rule.role_name == rule.role_name, + Rule.permission_name == rule.permission_name ).first() if not existing: db.add(rule) @@ -77,7 +89,6 @@ def seed_rules(db): db.commit() - def seed_admin_user(db): admin_role = db.query(Role).filter(Role.name == "admin").first() existing_admin = db.query(User).filter(User.role_name == admin_role.name).first() @@ -99,9 +110,11 @@ def seed_admin_user(db): db.commit() print("Admin user created.") + def seed_all(): db = SessionLocal() try: + clear_tables(db) seed_roles(db) seed_permissions(db) seed_rules(db) From 4537eb3910389eadc89c346a7bf84e2e11258dd8 Mon Sep 17 00:00:00 2001 From: Muhammad Raza Bhatti Date: Tue, 27 May 2025 18:02:14 +0500 Subject: [PATCH 5/8] auth reworked to use dual system, testing with routers next, users and orders routers completion pending --- app/db/session.py | 2 -- app/routers/auth.py | 67 +++++++++++++++++++++++++++++------- app/services/auth.py | 28 +++++++++++++++ app/utils/jwt.py | 77 ++++++++++++++++++++++++++---------------- app/validators/auth.py | 4 ++- 5 files changed, 133 insertions(+), 45 deletions(-) diff --git a/app/db/session.py b/app/db/session.py index 8390218..22b7bb0 100644 --- a/app/db/session.py +++ b/app/db/session.py @@ -1,5 +1,3 @@ -# app/database/session.py - import os from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, Session diff --git a/app/routers/auth.py b/app/routers/auth.py index 5b83fb7..ccb3031 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -2,21 +2,62 @@ from fastapi.security import OAuth2PasswordRequestForm from sqlalchemy.orm import Session from app.db.session import get_db -from app.services.auth import authenticate_user, create_token_for_user +from app.services.auth import authenticate_user, get_permissions_for_role +from app.utils.jwt import create_access_token, create_refresh_token, decode_token +from app.db.models.users import User -router = APIRouter() +router = APIRouter(prefix="/auth") -@router.post("/token") -def login_for_access_token( - db: Session = Depends(get_db), - form_data: OAuth2PasswordRequestForm = Depends() +@router.post("/login") +def login( + form_data: OAuth2PasswordRequestForm = Depends(), + db: Session = Depends(get_db) ): user = authenticate_user(db, form_data.username, form_data.password) if not user: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect username or password", - headers={"WWW-Authenticate": "Bearer"}, - ) - token = create_token_for_user(user) - return {"access_token": token, "token_type": "bearer"} + raise HTTPException(status_code=401, detail="Invalid credentials") + + # Fetch permissions for user's role from DB + permissions = get_permissions_for_role(db, user.role_name) + + user_data = { + "id": user.id, + "role": user.role_name, + "permissions": permissions, + } + + access_token = create_access_token(user_data) + refresh_token = create_refresh_token(user_data) + + return { + "access_token": access_token, + "refresh_token": refresh_token, + "token_type": "bearer" + } + + +@router.post("/refresh") +def refresh_token(refresh_token: str, db: Session = Depends(get_db)): + payload = decode_token(refresh_token) + + if payload.get("type") != "refresh": + raise HTTPException(status_code=400, detail="Invalid token type") + + user_id = int(payload.get("sub")) + + # Fetch fresh user info from DB + user = db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException(status_code=404, detail="User not found") + + # Fetch permissions fresh from DB + permissions = get_permissions_for_role(db, user.role_name) + + user_data = { + "id": user.id, + "role": user.role_name, + "permissions": permissions, + } + + new_access_token = create_access_token(user_data) + return {"access_token": new_access_token, "token_type": "bearer"} diff --git a/app/services/auth.py b/app/services/auth.py index 6de5b8c..05a0881 100644 --- a/app/services/auth.py +++ b/app/services/auth.py @@ -2,6 +2,10 @@ from datetime import timedelta from app.utils.jwt import create_access_token from app.services.users import get_user_by_username +from sqlalchemy.orm import Session +from app.db.models.rules import Rule +from datetime import timedelta + pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") @@ -24,3 +28,27 @@ def create_token_for_user(user): access_token_expires = timedelta(minutes=60) token = create_access_token(token_data, expires_delta=access_token_expires) return token + +def get_permissions_for_role(db: Session, role_name: str) -> list[str]: + permissions = ( + db.query(Rule.permission_name) + .filter(Rule.role_name == role_name) + .all() + ) + return [perm.permission_name for perm in permissions] + +def create_token_for_user(db: Session, user): + # Fetch permissions from DB + permissions = get_permissions_for_role(db, user.role_name) + + token_data = { + "sub": str(user.id), + "role": user.role_name, + "permissions": permissions, + } + + # Set token expiry, e.g. 15 minutes + expires_delta = timedelta(minutes=15) + + access_token = create_access_token(token_data, expires_delta) + return access_token diff --git a/app/utils/jwt.py b/app/utils/jwt.py index 336f30e..ec7fb23 100644 --- a/app/utils/jwt.py +++ b/app/utils/jwt.py @@ -1,47 +1,66 @@ import os +import datetime +import jwt from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer -import jwt from jwt import PyJWTError -from app.validators.users import TokenData +from app.validators.auth import TokenData -JWT_SECRET = os.getenv("JWT_SECRET", "your-secret") # Use env var in prod +JWT_SECRET = os.getenv("JWT_SECRET", "your-secret") JWT_ALGORITHM = os.getenv("JWT_ALGORITHM", "HS256") +ACCESS_TOKEN_EXPIRE_MINUTES = 15 +REFRESH_TOKEN_EXPIRE_DAYS = 7 -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") # URL for token endpoint +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login") -def create_access_token(data: dict, expires_delta=None): - import datetime +def create_token(data: dict, expires_delta: datetime.timedelta) -> str: to_encode = data.copy() - if expires_delta: - expire = datetime.datetime.utcnow() + expires_delta - else: - expire = datetime.datetime.utcnow() + datetime.timedelta(minutes=15) - to_encode.update({"exp": expire}) - encoded_jwt = jwt.encode(to_encode, JWT_SECRET, algorithm=JWT_ALGORITHM) - return encoded_jwt + to_encode["exp"] = datetime.datetime.utcnow() + expires_delta + return jwt.encode(to_encode, JWT_SECRET, algorithm=JWT_ALGORITHM) -def get_current_user(token: str = Depends(oauth2_scheme)) -> TokenData: - credentials_exception = HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", - headers={"WWW-Authenticate": "Bearer"}, +def create_access_token(user: dict) -> str: + return create_token( + data={ + "sub": str(user["id"]), + "role": user["role"], + "type": "access" + }, + expires_delta=datetime.timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) ) + +def create_refresh_token(user: dict) -> str: + return create_token( + data={ + "sub": str(user["id"]), + "type": "refresh" + }, + expires_delta=datetime.timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS) + ) + +def decode_token(token: str) -> dict: try: payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM]) - user_id: int = payload.get("user_id") - role: str = payload.get("role") - if user_id is None or role is None: - raise credentials_exception - token_data = TokenData(user_id=user_id, role=role) + return payload except PyJWTError: - raise credentials_exception - return token_data + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid or expired token", + headers={"WWW-Authenticate": "Bearer"}, + ) + +def get_current_user(token: str = Depends(oauth2_scheme)) -> TokenData: + payload = decode_token(token) + if payload.get("type") != "access": + raise HTTPException(status_code=400, detail="Invalid token type") + user_id = int(payload.get("sub")) + role = payload.get("role") + if user_id is None or role is None: + raise HTTPException(status_code=401, detail="Invalid token payload") + return TokenData(user_id=user_id, role=role) def admin_required(current_user: TokenData = Depends(get_current_user)): if current_user.role != "admin": - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Admin privileges required.", - ) + raise HTTPException(status_code=403, detail="Admin privileges required.") return current_user + + diff --git a/app/validators/auth.py b/app/validators/auth.py index 8fbead5..b1167bf 100644 --- a/app/validators/auth.py +++ b/app/validators/auth.py @@ -1,5 +1,7 @@ from pydantic import BaseModel +from typing import List class TokenData(BaseModel): user_id: int - role: str \ No newline at end of file + role: str + permissions: List[str] = [] From 039999645534cc73a5e9bbb1361a0a3e2ae9f2bf Mon Sep 17 00:00:00 2001 From: Muhammad Raza Bhatti Date: Tue, 27 May 2025 18:17:43 +0500 Subject: [PATCH 6/8] auth further refactored, isolated db interaction into db/crud/auth, and hashing.py refactored into security.py, reworked and reused in auth --- app/db/crud/auth.py | 14 ++++++++++++++ app/services/auth.py | 13 ++----------- app/utils/{hashing.py => security.py} | 2 ++ 3 files changed, 18 insertions(+), 11 deletions(-) create mode 100644 app/db/crud/auth.py rename app/utils/{hashing.py => security.py} (58%) diff --git a/app/db/crud/auth.py b/app/db/crud/auth.py new file mode 100644 index 0000000..4a9f2cb --- /dev/null +++ b/app/db/crud/auth.py @@ -0,0 +1,14 @@ +from sqlalchemy.orm import Session +from app.db.models.users import User +from app.db.models.rules import Rule + +def get_user_by_username(db: Session, username: str) -> User | None: + return db.query(User).filter(User.username == username).first() + +def get_permissions_for_role(db: Session, role_name: str) -> list[str]: + permissions = ( + db.query(Rule.permission_name) + .filter(Rule.role_name == role_name) + .all() + ) + return [perm.permission_name for perm in permissions] diff --git a/app/services/auth.py b/app/services/auth.py index 05a0881..e356c1a 100644 --- a/app/services/auth.py +++ b/app/services/auth.py @@ -1,19 +1,10 @@ -from passlib.context import CryptContext from datetime import timedelta from app.utils.jwt import create_access_token -from app.services.users import get_user_by_username +from app.db.crud.auth import get_user_by_username, get_permissions_for_role from sqlalchemy.orm import Session from app.db.models.rules import Rule from datetime import timedelta - - -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") - -def hash_password(password: str) -> str: - return pwd_context.hash(password) - -def verify_password(plain_password: str, hashed_password: str) -> bool: - return pwd_context.verify(plain_password, hashed_password) +from app.utils.security import verify_password def authenticate_user(db, username: str, password: str): user = get_user_by_username(db, username) diff --git a/app/utils/hashing.py b/app/utils/security.py similarity index 58% rename from app/utils/hashing.py rename to app/utils/security.py index 26d57cf..f57fe04 100644 --- a/app/utils/hashing.py +++ b/app/utils/security.py @@ -5,3 +5,5 @@ def hash_password(password: str) -> str: return pwd_context.hash(password) +def verify_password(plain_password: str, hashed_password: str) -> bool: + return pwd_context.verify(plain_password, hashed_password) \ No newline at end of file From 5bc7924597fc1230b88fedddb758d55b48cab2c0 Mon Sep 17 00:00:00 2001 From: Muhammad Raza Bhatti Date: Tue, 27 May 2025 19:31:12 +0500 Subject: [PATCH 7/8] models wrapped inside init.py to fix import order issues for ORM internals, auth/login now working. --- ...af167_testing_schema_change_potentially.py | 32 +++++++++++++++ app/db/crud/auth.py | 17 ++++++-- app/db/crud/orders.py | 2 +- app/db/crud/users.py | 2 +- app/db/models/__init__.py | 9 ++++ app/db/models/orders.py | 1 - app/db/models/users.py | 6 +-- app/db/seed.py | 10 ++--- app/db/session.py | 3 ++ app/routers/auth.py | 4 +- app/routers/users.py | 41 ++++++++----------- app/services/auth.py | 11 +---- app/services/users.py | 2 +- app/utils/jwt.py | 21 +++++++--- app/validators/auth.py | 2 +- requirements.txt | 5 +++ 16 files changed, 111 insertions(+), 57 deletions(-) create mode 100644 app/db/alembic/versions/9951448af167_testing_schema_change_potentially.py create mode 100644 app/db/models/__init__.py diff --git a/app/db/alembic/versions/9951448af167_testing_schema_change_potentially.py b/app/db/alembic/versions/9951448af167_testing_schema_change_potentially.py new file mode 100644 index 0000000..17e2ad4 --- /dev/null +++ b/app/db/alembic/versions/9951448af167_testing_schema_change_potentially.py @@ -0,0 +1,32 @@ +"""testing schema change potentially + +Revision ID: 9951448af167 +Revises: 5cfc72e753ba +Create Date: 2025-05-27 19:07:18.419756 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '9951448af167' +down_revision: Union[str, None] = '5cfc72e753ba' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/app/db/crud/auth.py b/app/db/crud/auth.py index 4a9f2cb..7eb5784 100644 --- a/app/db/crud/auth.py +++ b/app/db/crud/auth.py @@ -1,9 +1,20 @@ from sqlalchemy.orm import Session -from app.db.models.users import User -from app.db.models.rules import Rule +from app.db.models import User, Rule def get_user_by_username(db: Session, username: str) -> User | None: - return db.query(User).filter(User.username == username).first() + print("Debug: Listing all users in the database:") + all_users = db.query(User).all() + for user in all_users: + print(f" - id={user.id}, username={user.username}, email={user.email}, role={user.role_name}") + + print(f"Debug: Searching for user with username='{username}'") + user = db.query(User).filter(User.username == username).first() + if user: + print(f"Debug: Found user with id={user.id}") + else: + print("Debug: No user found") + return user + def get_permissions_for_role(db: Session, role_name: str) -> list[str]: permissions = ( diff --git a/app/db/crud/orders.py b/app/db/crud/orders.py index addd54d..110d953 100644 --- a/app/db/crud/orders.py +++ b/app/db/crud/orders.py @@ -1,5 +1,5 @@ from sqlalchemy.orm import Session -from app.db.models.orders import Order +from app.db.models import Order def get_order_by_id(db: Session, order_id: int) -> Order | None: return db.query(Order).filter(Order.id == order_id).first() diff --git a/app/db/crud/users.py b/app/db/crud/users.py index f3b4626..ec20710 100644 --- a/app/db/crud/users.py +++ b/app/db/crud/users.py @@ -1,6 +1,6 @@ from sqlalchemy.orm import Session from fastapi import HTTPException, status -from app.db.models.users import User +from app.db.models import User from app.validators.users import UserUpdate def get_user_by_id(db: Session, user_id: int) -> User | None: diff --git a/app/db/models/__init__.py b/app/db/models/__init__.py new file mode 100644 index 0000000..c0fcd0d --- /dev/null +++ b/app/db/models/__init__.py @@ -0,0 +1,9 @@ +# Import your declarative base from app/db/base.py +from app.db.base import Base + +# Import all your model classes from their respective files within this directory +from .orders import Order +from .permissions import Permission +from .roles import Role +from .rules import Rule +from .users import User diff --git a/app/db/models/orders.py b/app/db/models/orders.py index 3a93ef0..eae63cb 100644 --- a/app/db/models/orders.py +++ b/app/db/models/orders.py @@ -1,7 +1,6 @@ from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Numeric, func from sqlalchemy.orm import relationship from app.db.base import Base -#from app.db.models.users import User class Order(Base): __tablename__ = "orders" diff --git a/app/db/models/users.py b/app/db/models/users.py index 011ebf6..c0508d8 100644 --- a/app/db/models/users.py +++ b/app/db/models/users.py @@ -1,8 +1,6 @@ from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, func from sqlalchemy.orm import relationship from app.db.base import Base -from app.db.models.roles import Role -from app.db.models.orders import Order # Import Order model for relationship class User(Base): __tablename__ = "users" @@ -15,5 +13,5 @@ class User(Base): created_at = Column(DateTime, server_default=func.now(), nullable=False) updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False) - role = relationship(Role, back_populates="users") - orders = relationship(Order, back_populates="user", cascade="all, delete-orphan") # Use string reference \ No newline at end of file + role = relationship("Role", back_populates="users") + orders = relationship("Order", back_populates="user", cascade="all, delete-orphan") # Use string reference \ No newline at end of file diff --git a/app/db/seed.py b/app/db/seed.py index 85bac00..4aaa1cb 100644 --- a/app/db/seed.py +++ b/app/db/seed.py @@ -1,17 +1,15 @@ from app.db.session import SessionLocal from app.db.models.roles import Role from app.db.models.permissions import Permission -from app.db.models.rules import Rule -from app.db.models.users import User -from app.utils.hashing import hash_password +from app.db.models import Rule, User +from app.utils.security import hash_password import os from dotenv import load_dotenv - -load_dotenv() - from sqlalchemy import text +load_dotenv() # Load environment variables from .env file + def clear_tables(db): # Truncate with restart identity to reset auto-increment IDs db.execute(text("TRUNCATE TABLE rules, permissions, roles, users RESTART IDENTITY CASCADE")) diff --git a/app/db/session.py b/app/db/session.py index 22b7bb0..db45ea0 100644 --- a/app/db/session.py +++ b/app/db/session.py @@ -1,8 +1,11 @@ import os +from dotenv import load_dotenv from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, Session from typing import Generator +load_dotenv() + DB_HOST = os.getenv("DB_HOST", "localhost") DB_PORT = os.getenv("DB_PORT", "5432") DB_USER = os.getenv("POSTGRES_USER", "postgres") diff --git a/app/routers/auth.py b/app/routers/auth.py index ccb3031..a05d7ef 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -4,9 +4,9 @@ from app.db.session import get_db from app.services.auth import authenticate_user, get_permissions_for_role from app.utils.jwt import create_access_token, create_refresh_token, decode_token -from app.db.models.users import User +from app.db.models import User -router = APIRouter(prefix="/auth") +router = APIRouter() @router.post("/login") def login( diff --git a/app/routers/users.py b/app/routers/users.py index 2d564c4..044fa6e 100644 --- a/app/routers/users.py +++ b/app/routers/users.py @@ -18,16 +18,16 @@ from app.utils.jwt import ( get_current_user, - admin_required, + permission_required, ) router = APIRouter() -@router.post("/", summary="Create a new user (Admin only)", response_model=UserOut) +@router.post("/", summary="Create a new user", response_model=UserOut) def create_user( user_in: UserCreate, db: Session = Depends(get_db), - current_user: TokenData = Depends(admin_required), + current_user: TokenData = Depends(permission_required("create_user")), ): try: user = create_user_service(db, user_in) @@ -41,10 +41,10 @@ def create_user( detail="Unexpected error occurred.", ) - -@router.get("/", summary="List all users (Admin only)", response_model=List[UserOut]) +@router.get("/", summary="List all users", response_model=List[UserOut]) def list_users( - db: Session = Depends(get_db), current_user: TokenData = Depends(admin_required) + db: Session = Depends(get_db), + current_user: TokenData = Depends(permission_required("list_users")), ): try: users = list_users_service(db) @@ -56,10 +56,10 @@ def list_users( detail="Unexpected error occurred.", ) - @router.get("/me", summary="Get current user's profile", response_model=UserOut) def get_me( - db: Session = Depends(get_db), current_user: TokenData = Depends(get_current_user) + db: Session = Depends(get_db), + current_user: TokenData = Depends(get_current_user), ): try: user = get_user_service(db, current_user.user_id) @@ -73,7 +73,6 @@ def get_me( detail="Unexpected error occurred.", ) - @router.put("/me", summary="Update current user's profile", response_model=UserOut) def update_me( user_update: UserUpdate, @@ -92,15 +91,13 @@ def update_me( detail="Unexpected error occurred.", ) - @router.get("/{user_id}", summary="Get user by ID", response_model=UserOut) def get_user( user_id: int, db: Session = Depends(get_db), current_user: TokenData = Depends(get_current_user), ): - # Admin or user themself allowed - if current_user.role != "admin" and current_user.user_id != user_id: + if "get_any_user" not in current_user.permissions and current_user.user_id != user_id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied.", @@ -117,7 +114,6 @@ def get_user( detail="Unexpected error occurred.", ) - @router.put("/{user_id}", summary="Update user by ID", response_model=UserOut) def update_user( user_id: int, @@ -125,8 +121,7 @@ def update_user( db: Session = Depends(get_db), current_user: TokenData = Depends(get_current_user), ): - # Admin or user themself allowed - if current_user.role != "admin" and current_user.user_id != user_id: + if "update_any_user" not in current_user.permissions and current_user.user_id != user_id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied.", @@ -143,12 +138,11 @@ def update_user( detail="Unexpected error occurred.", ) - @router.delete("/{user_id}", summary="Delete user by ID", status_code=200) def delete_user( user_id: int, db: Session = Depends(get_db), - current_user: TokenData = Depends(admin_required), + current_user: TokenData = Depends(permission_required("delete_user")), ): try: delete_user_service(db, user_id) @@ -164,9 +158,10 @@ def delete_user( detail="Unexpected error occurred.", ) -@router.get("/{user_id}/orders", summary="List orders by user (Admin only)") -def list_user_orders( - user_id: int, current_user: TokenData = Depends(admin_required) -): - # Implement orders fetching logic here, admin only - pass + +# @router.get("/{user_id}/orders", summary="List orders by user (Admin only)") +# def list_user_orders( +# user_id: int, current_user: TokenData = Depends(admin_required) +# ): +# # Implement orders fetching logic here, admin only +# pass diff --git a/app/services/auth.py b/app/services/auth.py index e356c1a..587b3ac 100644 --- a/app/services/auth.py +++ b/app/services/auth.py @@ -2,23 +2,19 @@ from app.utils.jwt import create_access_token from app.db.crud.auth import get_user_by_username, get_permissions_for_role from sqlalchemy.orm import Session -from app.db.models.rules import Rule +from app.db.models import Rule from datetime import timedelta from app.utils.security import verify_password def authenticate_user(db, username: str, password: str): user = get_user_by_username(db, username) if not user: + return False if not verify_password(password, user.hashed_password): return False return user -def create_token_for_user(user): - token_data = {"user_id": user.id, "role": user.role} - access_token_expires = timedelta(minutes=60) - token = create_access_token(token_data, expires_delta=access_token_expires) - return token def get_permissions_for_role(db: Session, role_name: str) -> list[str]: permissions = ( @@ -29,7 +25,6 @@ def get_permissions_for_role(db: Session, role_name: str) -> list[str]: return [perm.permission_name for perm in permissions] def create_token_for_user(db: Session, user): - # Fetch permissions from DB permissions = get_permissions_for_role(db, user.role_name) token_data = { @@ -38,8 +33,6 @@ def create_token_for_user(db: Session, user): "permissions": permissions, } - # Set token expiry, e.g. 15 minutes expires_delta = timedelta(minutes=15) - access_token = create_access_token(token_data, expires_delta) return access_token diff --git a/app/services/users.py b/app/services/users.py index f0a1f3b..194ef3d 100644 --- a/app/services/users.py +++ b/app/services/users.py @@ -2,7 +2,7 @@ from app.validators.users import UserCreate, UserUpdate from app.db.crud.users import get_user_by_email, get_user_by_username, create_user as db_create_user, get_all_users, get_user_by_id, delete_user_by_id, update_user_db from fastapi import HTTPException, status -from app.utils.hashing import hash_password +from app.utils.security import hash_password def create_user_service(db: Session, user_in: UserCreate): # Check if user exists by email or username diff --git a/app/utils/jwt.py b/app/utils/jwt.py index ec7fb23..7a7af61 100644 --- a/app/utils/jwt.py +++ b/app/utils/jwt.py @@ -5,6 +5,7 @@ from fastapi.security import OAuth2PasswordBearer from jwt import PyJWTError from app.validators.auth import TokenData +from typing import Callable JWT_SECRET = os.getenv("JWT_SECRET", "your-secret") JWT_ALGORITHM = os.getenv("JWT_ALGORITHM", "HS256") @@ -23,11 +24,13 @@ def create_access_token(user: dict) -> str: data={ "sub": str(user["id"]), "role": user["role"], + "permissions": user.get("permissions", []), "type": "access" }, expires_delta=datetime.timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) ) + def create_refresh_token(user: dict) -> str: return create_token( data={ @@ -52,15 +55,23 @@ def get_current_user(token: str = Depends(oauth2_scheme)) -> TokenData: payload = decode_token(token) if payload.get("type") != "access": raise HTTPException(status_code=400, detail="Invalid token type") + user_id = int(payload.get("sub")) role = payload.get("role") + permissions = payload.get("permissions", []) + if user_id is None or role is None: raise HTTPException(status_code=401, detail="Invalid token payload") - return TokenData(user_id=user_id, role=role) -def admin_required(current_user: TokenData = Depends(get_current_user)): - if current_user.role != "admin": - raise HTTPException(status_code=403, detail="Admin privileges required.") - return current_user + return TokenData(user_id=user_id, role=role, permissions=permissions) +def permission_required(permission: str) -> Callable: + def _permission_dependency(current_user: TokenData = Depends(get_current_user)) -> TokenData: + if permission not in current_user.permissions: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"Permission '{permission}' required." + ) + return current_user + return _permission_dependency \ No newline at end of file diff --git a/app/validators/auth.py b/app/validators/auth.py index b1167bf..08e6cf0 100644 --- a/app/validators/auth.py +++ b/app/validators/auth.py @@ -4,4 +4,4 @@ class TokenData(BaseModel): user_id: int role: str - permissions: List[str] = [] + permissions: List[str] diff --git a/requirements.txt b/requirements.txt index ad680d2..7fc9119 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +alembic==1.16.1 annotated-types==0.7.0 anyio==4.9.0 bcrypt==4.3.0 @@ -9,15 +10,19 @@ fastapi==0.115.12 greenlet==3.2.2 h11==0.16.0 idna==3.10 +Mako==1.3.10 +MarkupSafe==3.0.2 passlib==1.7.4 psycopg2-binary==2.9.10 pydantic==2.11.5 pydantic_core==2.33.2 PyJWT==2.10.1 python-dotenv==1.1.0 +python-multipart==0.0.20 sniffio==1.3.1 SQLAlchemy==2.0.41 starlette==0.46.2 +tomli==2.2.1 typing-inspection==0.4.1 typing_extensions==4.13.2 uvicorn==0.34.2 From 5763bf5a159d0ff5091705856b1c70dc0d0e7860 Mon Sep 17 00:00:00 2001 From: Muhammad Raza Bhatti Date: Tue, 27 May 2025 19:46:25 +0500 Subject: [PATCH 8/8] auth/refresh reworked to follow best practices, tested to make sure it works --- app/routers/auth.py | 11 ++++++----- app/validators/auth.py | 4 ++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app/routers/auth.py b/app/routers/auth.py index a05d7ef..8c9c617 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -5,6 +5,7 @@ from app.services.auth import authenticate_user, get_permissions_for_role from app.utils.jwt import create_access_token, create_refresh_token, decode_token from app.db.models import User +from app.validators.auth import RefreshTokenRequest router = APIRouter() @@ -35,22 +36,22 @@ def login( "token_type": "bearer" } - @router.post("/refresh") -def refresh_token(refresh_token: str, db: Session = Depends(get_db)): - payload = decode_token(refresh_token) +def refresh_token( + data: RefreshTokenRequest, + db: Session = Depends(get_db) +): + payload = decode_token(data.refresh_token) if payload.get("type") != "refresh": raise HTTPException(status_code=400, detail="Invalid token type") user_id = int(payload.get("sub")) - # Fetch fresh user info from DB user = db.query(User).filter(User.id == user_id).first() if not user: raise HTTPException(status_code=404, detail="User not found") - # Fetch permissions fresh from DB permissions = get_permissions_for_role(db, user.role_name) user_data = { diff --git a/app/validators/auth.py b/app/validators/auth.py index 08e6cf0..09fc1ac 100644 --- a/app/validators/auth.py +++ b/app/validators/auth.py @@ -5,3 +5,7 @@ class TokenData(BaseModel): user_id: int role: str permissions: List[str] + + +class RefreshTokenRequest(BaseModel): + refresh_token: str