Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions app/api/routes/questions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from fastapi import APIRouter
from fastapi import APIRouter, Depends

from app.rbac import check_for_permission
from app.settings import settings
from app.utils.http_client import make_request

Expand All @@ -10,7 +11,9 @@


@router.get("/daily_questions")
def get_daily_questions():
def get_daily_questions(
_check_permission=Depends(check_for_permission("VIEW_QUESTIONS")),
):
"""Fetch daily questions from the external generator service."""
url = f"{GENERATOR_SERVICE_URL}/api/v1/questions/get_daily_question"
return make_request("GET", url)
2 changes: 2 additions & 0 deletions app/api/routes/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,7 @@ def read_users_me(current_user=Depends(get_current_user)):
"id": current_user.id,
"email": current_user.email,
"name": current_user.name,
"role": current_user.role,
"auth_type": current_user.auth_type,
}
return {"user": current_user}
40 changes: 40 additions & 0 deletions app/migrations/versions/417469bac4de_removed_auth_type_from_otp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""removed auth-type from OTP

Revision ID: 417469bac4de
Revises: a6d1c6c13842
Create Date: 2025-08-31 09:57:49.661025

"""

from typing import Sequence, Union

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision: str = "417469bac4de"
down_revision: Union[str, Sequence[str], None] = "a6d1c6c13842"
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.drop_column("otps", "provider_id")
op.drop_column("otps", "auth_type")
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"otps",
sa.Column("auth_type", sa.VARCHAR(), autoincrement=False, nullable=False),
)
op.add_column(
"otps",
sa.Column("provider_id", sa.VARCHAR(), autoincrement=False, nullable=True),
)
# ### end Alembic commands ###
38 changes: 38 additions & 0 deletions app/migrations/versions/a6d1c6c13842_add_role_field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""add role field

Revision ID: a6d1c6c13842
Revises: a4b5fabfdeac
Create Date: 2025-08-31 09:15:05.419403

"""

from typing import Sequence, Union

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision: str = "a6d1c6c13842"
down_revision: Union[str, Sequence[str], None] = "a4b5fabfdeac"
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! ###
userrole_enum = sa.Enum("ADMIN", "USER", name="userrole")
userrole_enum.create(op.get_bind())

# Then add the column using the enum type
op.add_column("users", sa.Column("role", userrole_enum, nullable=False))
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("users", "role")
# Then drop the enum type
sa.Enum(name="userrole").drop(op.get_bind())
# ### end Alembic commands ###
2 changes: 0 additions & 2 deletions app/models/otp.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ class OTP(Base):
name = Column(String, nullable=False)
password = Column(String, nullable=True)
is_active = Column(Integer, default=1) # 1 for True, 0 for False
provider_id = Column(String, nullable=True)
auth_type = Column(String, nullable=False, default="local")
created_at = Column(DateTime, default=datetime.utcnow)
expires_at = Column(DateTime, nullable=False)

Expand Down
12 changes: 2 additions & 10 deletions app/models/user.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
from enum import Enum

from sqlalchemy import Boolean, Column, DateTime
from sqlalchemy import Enum as SQLEnum
from sqlalchemy import Integer, String

from app.config.database import Base


# TODO: removed as AuthType is now in app.schemas.users
class AuthType(Enum):
LOCAL = "local"
GOOGLE = "google"
GITHUB = "github"
# Add more as needed
from app.utils.enums import AuthType, UserRole


class User(Base):
Expand All @@ -27,4 +18,5 @@ class User(Base):
String, unique=True, nullable=True
) # For OAuth or external providers
auth_type = Column(SQLEnum(AuthType), nullable=False, default=AuthType.LOCAL)
role = Column(SQLEnum(UserRole), nullable=False, default=UserRole.USER)
created_at = Column(DateTime, nullable=False)
30 changes: 30 additions & 0 deletions app/rbac.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from fastapi import Depends, HTTPException, status

from app.services.user import get_current_user
from app.utils.enums import UserRole

FEATURES = [
"VIEW_QUESTIONS",
"MODIFY_QUESTIONS",
]

USER_FEATURES = [
"VIEW_QUESTIONS",
]

FEATURE_ROLE_MAP = {
UserRole.ADMIN: FEATURES,
UserRole.USER: USER_FEATURES,
}


def check_for_permission(feature_name: str):
def checker(current_user=Depends(get_current_user)):
allowed_features = FEATURE_ROLE_MAP.get(current_user.role, [])
if feature_name not in allowed_features:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions"
)
return True

return checker
14 changes: 5 additions & 9 deletions app/schemas/users.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,23 @@
from datetime import datetime
from enum import Enum
from typing import Optional

from pydantic import BaseModel, EmailStr

from app.utils.enums import AuthType, UserRole


class OTPVerifyRequest(BaseModel):
email: EmailStr
otp_code: str


class AuthType(str, Enum):
local = "local"
google = "google"
github = "github"
# Add more as needed


class UserBase(BaseModel):
email: EmailStr
name: str
is_active: bool = True
auth_type: AuthType = AuthType.local
auth_type: AuthType = AuthType.LOCAL
provider_id: Optional[str] = None
role: UserRole = UserRole.USER


class UserCreate(UserBase):
Expand All @@ -36,6 +31,7 @@ class UserUpdate(BaseModel):
is_active: Optional[bool] = True
auth_type: Optional[AuthType] = None
provider_id: Optional[str] = None
role: Optional[UserRole] = None


class UserInDB(UserBase):
Expand Down
7 changes: 4 additions & 3 deletions app/services/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
from app.config.github import github
from app.config.google import google
from app.models.user import User
from app.schemas.users import AuthType, UserCreate
from app.schemas.users import UserCreate
from app.services.user import create_user
from app.utils.auth import create_access_token, verify_password
from app.utils.enums import AuthType


def authenticate_user(db, email: str, password: str):
Expand Down Expand Up @@ -41,7 +42,7 @@ def create_github_user(profile, email: str, db):
name=name,
is_active=True,
provider_id=provider_id,
auth_type=AuthType.github,
auth_type=AuthType.GITHUB,
)
user = create_user(db, user_in)
return user
Expand All @@ -56,7 +57,7 @@ def create_google_user(profile, email: str, db):
name=name,
is_active=True,
provider_id=provider_id,
auth_type=AuthType.google,
auth_type=AuthType.GOOGLE,
)
user = create_user(db, user_in)
return user
Expand Down
7 changes: 1 addition & 6 deletions app/services/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def create_user(db: Session, user_in: UserCreate) -> User:
if existing:
raise HTTPException(status_code=400, detail="Email already registered")

hashed_password = hash_password(user_in.password) if user_in.password else None
hashed_password = user_in.password if user_in.password else None
user = User(
email=user_in.email,
name=user_in.name,
Expand Down Expand Up @@ -96,9 +96,6 @@ def register_with_otp(db: Session, user_in: UserCreate):
name=user_in.name,
password=hashed_password,
is_active=1 if getattr(user_in, "is_active", True) else 0,
provider_id=getattr(user_in, "provider_id", None),
# TODO: update this to use only local auth type
auth_type=user_in.auth_type.value if hasattr(user_in, "auth_type") else "local",
created_at=datetime.utcnow(),
expires_at=expires_at,
)
Expand Down Expand Up @@ -128,8 +125,6 @@ def verify_otp_and_create_user(db: Session, email: str, otp_code: str):
name=otp_entry.name,
password=otp_entry.password, # Already hashed
is_active=bool(otp_entry.is_active),
provider_id=otp_entry.provider_id,
auth_type=otp_entry.auth_type,
)
try:
user = create_user(db, user_in)
Expand Down
14 changes: 14 additions & 0 deletions app/utils/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from enum import Enum


# TODO: removed as AuthType is now in app.schemas.users
class AuthType(Enum):
LOCAL = "local"
GOOGLE = "google"
GITHUB = "github"
# Add more as needed


class UserRole(Enum):
ADMIN = "admin"
USER = "user"