feat: add account service with API key generation and credit ledger
This commit is contained in:
parent
235573cdce
commit
9fd298235e
137
src/proxy_pool/accounts/service.py
Normal file
137
src/proxy_pool/accounts/service.py
Normal file
@ -0,0 +1,137 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import secrets
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from proxy_pool.accounts.models import ApiKey, CreditLedger, CreditTxType, User
|
||||
from proxy_pool.config import AccountSettings
|
||||
|
||||
|
||||
def generate_api_key(prefix: str) -> tuple[str, str, str]:
|
||||
"""Generate an API key. Returns (raw_key, key_hash, prefix_fragment)."""
|
||||
random_part = secrets.token_urlsafe(36)
|
||||
raw_key = f"{prefix}{random_part}"
|
||||
key_hash = hashlib.sha256(raw_key.encode()).hexdigest()
|
||||
prefix_fragment = raw_key[:8]
|
||||
return raw_key, key_hash, prefix_fragment
|
||||
|
||||
|
||||
def hash_api_key(raw_key: str) -> str:
|
||||
return hashlib.sha256(raw_key.encode()).hexdigest()
|
||||
|
||||
|
||||
async def create_user(
|
||||
db: AsyncSession,
|
||||
email: str,
|
||||
display_name: str | None,
|
||||
settings: AccountSettings,
|
||||
) -> tuple[User, str]:
|
||||
"""Create a user with an initial API key and starting credits.
|
||||
|
||||
Returns (user, raw_api_key). The raw key is only available at creation time.
|
||||
"""
|
||||
user = User(email=email, display_name=display_name)
|
||||
db.add(user)
|
||||
await db.flush()
|
||||
|
||||
raw_key, key_hash, prefix = generate_api_key(settings.api_key_prefix)
|
||||
api_key = ApiKey(
|
||||
user_id=user.id,
|
||||
key_hash=key_hash,
|
||||
prefix=prefix,
|
||||
label="default",
|
||||
)
|
||||
db.add(api_key)
|
||||
|
||||
if settings.default_credits > 0:
|
||||
ledger_entry = CreditLedger(
|
||||
user_id=user.id,
|
||||
amount=settings.default_credits,
|
||||
tx_type=CreditTxType.PURCHASE,
|
||||
description="Initial credits",
|
||||
)
|
||||
db.add(ledger_entry)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
|
||||
return user, raw_key
|
||||
|
||||
|
||||
async def verify_api_key(
|
||||
db: AsyncSession,
|
||||
raw_key: str,
|
||||
) -> User | None:
|
||||
"""Verify an API key and return the associated user, or None."""
|
||||
prefix = raw_key[:8]
|
||||
key_hash = hash_api_key(raw_key)
|
||||
|
||||
stmt = (
|
||||
select(ApiKey)
|
||||
.where(
|
||||
ApiKey.prefix == prefix,
|
||||
ApiKey.key_hash == key_hash,
|
||||
ApiKey.is_active.is_(True),
|
||||
)
|
||||
.where((ApiKey.expires_at.is_(None)) | (ApiKey.expires_at >= func.now()))
|
||||
)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
api_key = result.scalar_one_or_none()
|
||||
|
||||
if api_key is None:
|
||||
return None
|
||||
|
||||
api_key.last_used_at = func.now()
|
||||
|
||||
user = await db.get(User, api_key.user_id)
|
||||
if user is None or not user.is_active:
|
||||
return None
|
||||
|
||||
return user
|
||||
|
||||
|
||||
async def get_credit_balance(
|
||||
db: AsyncSession,
|
||||
user_id: UUID,
|
||||
) -> int:
|
||||
"""Compute the current credit balance from the ledger."""
|
||||
stmt = select(func.coalesce(func.sum(CreditLedger.amount), 0)).where(
|
||||
CreditLedger.user_id == user_id,
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalar_one()
|
||||
|
||||
|
||||
async def debit_credits(
|
||||
db: AsyncSession,
|
||||
user_id: UUID,
|
||||
amount: int,
|
||||
tx_type: CreditTxType,
|
||||
description: str,
|
||||
reference_id: UUID | None = None,
|
||||
) -> int:
|
||||
"""Debit credits and return new balance.
|
||||
|
||||
Raises ValueError if insufficient credits.
|
||||
"""
|
||||
balance = await get_credit_balance(db, user_id)
|
||||
if balance < amount:
|
||||
msg = f"Insufficient credits: need {amount}, have {balance}"
|
||||
raise ValueError(msg)
|
||||
|
||||
entry = CreditLedger(
|
||||
user_id=user_id,
|
||||
amount=-amount,
|
||||
tx_type=tx_type,
|
||||
description=description,
|
||||
reference_id=reference_id,
|
||||
)
|
||||
db.add(entry)
|
||||
await db.flush()
|
||||
|
||||
return balance - amount
|
||||
Loading…
x
Reference in New Issue
Block a user