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
29 changes: 29 additions & 0 deletions alembic/versions/f45e46b231f3_add_admin_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""add admin users

Revision ID: f45e46b231f3
Revises: 683fc811a969
Create Date: 2025-09-11 13:14:17.066592

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'f45e46b231f3'
down_revision = '683fc811a969'
branch_labels = None
depends_on = None


def upgrade() -> None:
op.create_table(
"admin_users",
sa.Column("user_id", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("user_id"),
)


def downgrade() -> None:
op.drop_table("admin_users")
12 changes: 11 additions & 1 deletion app/api/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from datetime import datetime

import aiohttp
import requests
from fastapi import Depends, HTTPException, Request, status
from fastapi.security import APIKeyHeader, OAuth2PasswordBearer
from jose import JWTError, jwt
Expand Down Expand Up @@ -113,6 +112,7 @@ async def get_current_user(
result = await db.execute(
select(User)
.options(selectinload(User.api_keys)) # Eager load Forge API keys
.options(selectinload(User.admin_users)) # Eager load admin users
.filter(User.username == token_data.username)
)
user = result.scalar_one_or_none()
Expand Down Expand Up @@ -393,6 +393,7 @@ async def get_current_user_from_clerk(
result = await db.execute(
select(User)
.options(selectinload(User.api_keys)) # Eager load Forge API keys
.options(selectinload(User.admin_users)) # Eager load admin users
.filter(User.clerk_user_id == clerk_user_id)
)
user = result.scalar_one_or_none()
Expand Down Expand Up @@ -512,3 +513,12 @@ async def get_current_active_user_from_clerk(
if not current_user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user


async def get_current_active_admin_user_from_clerk(
current_user: User = Depends(get_current_active_user_from_clerk),
):
"""Ensure the user from Clerk is an admin"""
if not current_user.admin_users:
raise HTTPException(status_code=401, detail="User is not an admin")
return current_user
64 changes: 64 additions & 0 deletions app/api/routes/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from decimal import Decimal
from pydantic import BaseModel
import uuid

from app.api.dependencies import get_current_active_admin_user_from_clerk
from app.core.database import get_async_db
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.user import User
from app.models.stripe import StripePayment
from app.core.logger import get_logger
from app.api.schemas.admin import AddBalanceRequest
from app.services.wallet_service import WalletService

logger = get_logger(name="admin")
router = APIRouter()

class AddBalanceResponse(BaseModel):
balance: Decimal
blocked: bool


@router.post("/add-balance")
async def add_balance(
add_balance_request: AddBalanceRequest,
current_user: User = Depends(get_current_active_admin_user_from_clerk),
db: AsyncSession = Depends(get_async_db),
):
"""Add balance to a user"""
user_id = add_balance_request.user_id
email = add_balance_request.email
amount = add_balance_request.amount

result = await db.execute(
select(User)
.where(
user_id is None or User.id == user_id,
email is None or User.email == email,
)
)
user = result.scalar_one_or_none()
if not user:
raise HTTPException(status_code=404, detail="User not found")

amount_decimal = Decimal(amount / 100.0)
result = await WalletService.adjust(db, user.id, amount_decimal, f"Admin {current_user.id} added balance for user {user.id}")
if not result.get("success"):
raise HTTPException(status_code=400, detail=f"Failed to add balance for user {user.id}: {result.get('reason')}")

# add the amount to the user's stripe payment
stripe_payment = StripePayment(
id=f"tb_admin_{uuid.uuid4().hex}",
user_id=user.id,
amount=amount,
currency="USD",
status="completed",
raw_data={"reason": f"Admin {current_user.id} added balance for user {user.id}"},
)
db.add(stripe_payment)
await db.commit()
logger.info(f"Added balance {amount_decimal} for user {user.id} by admin {current_user.id}")

return AddBalanceResponse(balance=result.get("balance"), blocked=result.get("blocked"))
6 changes: 6 additions & 0 deletions app/api/routes/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ async def read_user_me(
else:
user_data["forge_api_keys"] = []

if current_user.admin_users:
user_data["is_admin"] = True

return MaskedUser(**user_data)


Expand All @@ -104,6 +107,9 @@ async def read_user_me_clerk(
else:
user_data["forge_api_keys"] = []

if current_user.admin_users:
user_data["is_admin"] = True

return MaskedUser(**user_data)


Expand Down
Loading
Loading