Skip to content

Commit 30fd857

Browse files
authored
Add admin user functionality
1 parent a5a95cf commit 30fd857

10 files changed

Lines changed: 188 additions & 172 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""add admin users
2+
3+
Revision ID: f45e46b231f3
4+
Revises: 683fc811a969
5+
Create Date: 2025-09-11 13:14:17.066592
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
11+
12+
# revision identifiers, used by Alembic.
13+
revision = 'f45e46b231f3'
14+
down_revision = '683fc811a969'
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade() -> None:
20+
op.create_table(
21+
"admin_users",
22+
sa.Column("user_id", sa.Integer(), nullable=False),
23+
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
24+
sa.PrimaryKeyConstraint("user_id"),
25+
)
26+
27+
28+
def downgrade() -> None:
29+
op.drop_table("admin_users")

app/api/dependencies.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from datetime import datetime
1010

1111
import aiohttp
12-
import requests
1312
from fastapi import Depends, HTTPException, Request, status
1413
from fastapi.security import APIKeyHeader, OAuth2PasswordBearer
1514
from jose import JWTError, jwt
@@ -113,6 +112,7 @@ async def get_current_user(
113112
result = await db.execute(
114113
select(User)
115114
.options(selectinload(User.api_keys)) # Eager load Forge API keys
115+
.options(selectinload(User.admin_users)) # Eager load admin users
116116
.filter(User.username == token_data.username)
117117
)
118118
user = result.scalar_one_or_none()
@@ -393,6 +393,7 @@ async def get_current_user_from_clerk(
393393
result = await db.execute(
394394
select(User)
395395
.options(selectinload(User.api_keys)) # Eager load Forge API keys
396+
.options(selectinload(User.admin_users)) # Eager load admin users
396397
.filter(User.clerk_user_id == clerk_user_id)
397398
)
398399
user = result.scalar_one_or_none()
@@ -512,3 +513,12 @@ async def get_current_active_user_from_clerk(
512513
if not current_user.is_active:
513514
raise HTTPException(status_code=400, detail="Inactive user")
514515
return current_user
516+
517+
518+
async def get_current_active_admin_user_from_clerk(
519+
current_user: User = Depends(get_current_active_user_from_clerk),
520+
):
521+
"""Ensure the user from Clerk is an admin"""
522+
if not current_user.admin_users:
523+
raise HTTPException(status_code=401, detail="User is not an admin")
524+
return current_user

app/api/routes/admin.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from fastapi import APIRouter, Depends, HTTPException
2+
from sqlalchemy import select
3+
from decimal import Decimal
4+
from pydantic import BaseModel
5+
import uuid
6+
7+
from app.api.dependencies import get_current_active_admin_user_from_clerk
8+
from app.core.database import get_async_db
9+
from sqlalchemy.ext.asyncio import AsyncSession
10+
from app.models.user import User
11+
from app.models.stripe import StripePayment
12+
from app.core.logger import get_logger
13+
from app.api.schemas.admin import AddBalanceRequest
14+
from app.services.wallet_service import WalletService
15+
16+
logger = get_logger(name="admin")
17+
router = APIRouter()
18+
19+
class AddBalanceResponse(BaseModel):
20+
balance: Decimal
21+
blocked: bool
22+
23+
24+
@router.post("/add-balance")
25+
async def add_balance(
26+
add_balance_request: AddBalanceRequest,
27+
current_user: User = Depends(get_current_active_admin_user_from_clerk),
28+
db: AsyncSession = Depends(get_async_db),
29+
):
30+
"""Add balance to a user"""
31+
user_id = add_balance_request.user_id
32+
email = add_balance_request.email
33+
amount = add_balance_request.amount
34+
35+
result = await db.execute(
36+
select(User)
37+
.where(
38+
user_id is None or User.id == user_id,
39+
email is None or User.email == email,
40+
)
41+
)
42+
user = result.scalar_one_or_none()
43+
if not user:
44+
raise HTTPException(status_code=404, detail="User not found")
45+
46+
amount_decimal = Decimal(amount / 100.0)
47+
result = await WalletService.adjust(db, user.id, amount_decimal, f"Admin {current_user.id} added balance for user {user.id}")
48+
if not result.get("success"):
49+
raise HTTPException(status_code=400, detail=f"Failed to add balance for user {user.id}: {result.get('reason')}")
50+
51+
# add the amount to the user's stripe payment
52+
stripe_payment = StripePayment(
53+
id=f"tb_admin_{uuid.uuid4().hex}",
54+
user_id=user.id,
55+
amount=amount,
56+
currency="USD",
57+
status="completed",
58+
raw_data={"reason": f"Admin {current_user.id} added balance for user {user.id}"},
59+
)
60+
db.add(stripe_payment)
61+
await db.commit()
62+
logger.info(f"Added balance {amount_decimal} for user {user.id} by admin {current_user.id}")
63+
64+
return AddBalanceResponse(balance=result.get("balance"), blocked=result.get("blocked"))

app/api/routes/users.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ async def read_user_me(
8686
else:
8787
user_data["forge_api_keys"] = []
8888

89+
if current_user.admin_users:
90+
user_data["is_admin"] = True
91+
8992
return MaskedUser(**user_data)
9093

9194

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

110+
if current_user.admin_users:
111+
user_data["is_admin"] = True
112+
107113
return MaskedUser(**user_data)
108114

109115

0 commit comments

Comments
 (0)