Skip to content
Open
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
11 changes: 11 additions & 0 deletions backend/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
DATABASE_URL: str = "postgresql+asyncpg://user:password@localhost/dbname"
STRIPE_SECRET_KEY: str = "sk_test_..."
STRIPE_WEBHOOK_SECRET: str = "whsec_..."

class Config:
env_file = ".env"

settings = Settings()
13 changes: 13 additions & 0 deletions backend/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from sqlmodel import SQLModel, create_engine
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
from .config import settings

engine = create_async_engine(settings.DATABASE_URL, echo=True, future=True)

async def get_session() -> AsyncSession:
async_session = sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)
async with async_session() as session:
yield session
13 changes: 13 additions & 0 deletions backend/deps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from fastapi import Header, HTTPException
from typing import Optional
from uuid import UUID

async def get_current_user(x_user_id: Optional[str] = Header(None)) -> UUID:
if not x_user_id:
# Mock user for dev/testing if not provided
# In production, verify Firebase token or similar
return UUID("00000000-0000-0000-0000-000000000000")
try:
return UUID(x_user_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid User ID format")
19 changes: 19 additions & 0 deletions backend/generate_ddl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from sqlmodel import SQLModel, create_engine
from backend.models import User, Lineage, Blueprint, VectorState, EntropyEvent, InversionOutcome, Experiment, SEDAEvent, SubscriptionEvent

# Hack to print DDL
import io
from sqlalchemy.schema import CreateTable
from sqlalchemy.dialects import postgresql

def generate_ddl():
engine = create_engine("postgresql://")
metadata = SQLModel.metadata

# Sort tables by dependency (naive approach or just use metadata.sorted_tables)
for table in metadata.sorted_tables:
print(CreateTable(table).compile(engine, dialect=postgresql.dialect()))
print(";")

if __name__ == "__main__":
generate_ddl()
22 changes: 22 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from backend.routers import resolver, inversion, ops, billing

app = FastAPI(title="DEFRAG API", version="1.0.0")

app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

app.include_router(ops.router, prefix="/ops", tags=["ops"])
app.include_router(resolver.router, prefix="/resolver", tags=["resolver"])
app.include_router(inversion.router, prefix="/inversion", tags=["inversion"])
app.include_router(billing.router, prefix="/billing", tags=["billing"])

@app.get("/")
async def root():
return {"message": "Welcome to DEFRAG API"}
81 changes: 81 additions & 0 deletions backend/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from typing import Optional, List, Dict, Any
from uuid import UUID, uuid4
from datetime import datetime
from sqlmodel import Field, SQLModel, Relationship
from sqlalchemy import Column, JSON

class User(SQLModel, table=True):
id: UUID = Field(default_factory=uuid4, primary_key=True)
email: str = Field(index=True, unique=True)
stripe_customer_id: Optional[str] = Field(default=None, index=True)
subscription_tier: str = Field(default="Free") # Free, Pro, Lineage
created_at: datetime = Field(default_factory=datetime.utcnow)

blueprints: List["Blueprint"] = Relationship(back_populates="user")
lineages: List["Lineage"] = Relationship(back_populates="creator")

class Lineage(SQLModel, table=True):
id: UUID = Field(default_factory=uuid4, primary_key=True)
name: str
created_by: UUID = Field(foreign_key="user.id")
members: List[Dict[str, Any]] = Field(default=[], sa_column=Column(JSON))
created_at: datetime = Field(default_factory=datetime.utcnow)

creator: Optional[User] = Relationship(back_populates="lineages")
vector_states: List["VectorState"] = Relationship(back_populates="lineage")

class Blueprint(SQLModel, table=True):
id: UUID = Field(default_factory=uuid4, primary_key=True)
user_id: UUID = Field(foreign_key="user.id")
data: Dict[str, Any] = Field(default={}, sa_column=Column(JSON))
created_at: datetime = Field(default_factory=datetime.utcnow)

user: Optional[User] = Relationship(back_populates="blueprints")

class VectorState(SQLModel, table=True):
id: UUID = Field(default_factory=uuid4, primary_key=True)
lineage_id: UUID = Field(foreign_key="lineage.id")
vectors: List[Dict[str, Any]] = Field(default=[], sa_column=Column(JSON))
voltage: int
timestamp: datetime = Field(default_factory=datetime.utcnow)

lineage: Optional[Lineage] = Relationship(back_populates="vector_states")

class EntropyEvent(SQLModel, table=True):
id: UUID = Field(default_factory=uuid4, primary_key=True)
user_id: UUID = Field(foreign_key="user.id")
event_type: str
payload: Dict[str, Any] = Field(default={}, sa_column=Column(JSON))
timestamp: datetime = Field(default_factory=datetime.utcnow)

class InversionOutcome(SQLModel, table=True):
id: UUID = Field(default_factory=uuid4, primary_key=True)
user_id: UUID = Field(foreign_key="user.id")
input_text: str
shadow_identified: str
protocol_generated: Dict[str, Any] = Field(default={}, sa_column=Column(JSON))
timestamp: datetime = Field(default_factory=datetime.utcnow)

class Experiment(SQLModel, table=True):
id: UUID = Field(default_factory=uuid4, primary_key=True)
user_id: UUID = Field(foreign_key="user.id")
name: str
status: str
results: Dict[str, Any] = Field(default={}, sa_column=Column(JSON))
created_at: datetime = Field(default_factory=datetime.utcnow)

class SEDAEvent(SQLModel, table=True):
id: UUID = Field(default_factory=uuid4, primary_key=True)
user_id: UUID = Field(foreign_key="user.id")
score: int
tier: str
action: str
timestamp: datetime = Field(default_factory=datetime.utcnow)

class SubscriptionEvent(SQLModel, table=True):
id: UUID = Field(default_factory=uuid4, primary_key=True)
user_id: Optional[UUID] = Field(default=None, foreign_key="user.id")
stripe_event_id: str = Field(unique=True)
event_type: str
status: str
timestamp: datetime = Field(default_factory=datetime.utcnow)
10 changes: 10 additions & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
fastapi
uvicorn[standard]
sqlmodel
asyncpg
stripe
pydantic-settings
pytest
httpx
flatlib
psycopg2-binary
67 changes: 67 additions & 0 deletions backend/routers/billing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from fastapi import APIRouter, Header, Request, HTTPException, Depends
from backend.config import settings
import stripe
from backend.database import get_session
from sqlalchemy.ext.asyncio import AsyncSession
from backend.models import SubscriptionEvent, User
from sqlmodel import select

stripe.api_key = settings.STRIPE_SECRET_KEY

router = APIRouter()

@router.post("/checkout")
async def create_checkout_session(tier: str = "Pro"):
# Create checkout session
try:
session = stripe.checkout.Session.create(
payment_method_types=['card'],
line_items=[{
'price_data': {
'currency': 'usd',
'product_data': {
'name': f'Defrag {tier} Subscription',
},
'unit_amount': 2000 if tier == "Pro" else 10000, # Mock prices
'recurring': {'interval': 'month'},
},
'quantity': 1,
}],
mode='subscription',
success_url='https://defrag.app/success?session_id={CHECKOUT_SESSION_ID}',
cancel_url='https://defrag.app/cancel',
)
return {"sessionId": session.id, "url": session.url}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))

@router.post("/webhook")
async def stripe_webhook(request: Request, stripe_signature: str = Header(None), session: AsyncSession = Depends(get_session)):
payload = await request.body()

try:
event = stripe.Webhook.construct_event(
payload, stripe_signature, settings.STRIPE_WEBHOOK_SECRET
)
except ValueError as e:
raise HTTPException(status_code=400, detail="Invalid payload")
except stripe.error.SignatureVerificationError as e:
raise HTTPException(status_code=400, detail="Invalid signature")

# Handle the event
if event['type'] in ['checkout.session.completed', 'customer.subscription.updated', 'customer.subscription.deleted']:
# Log event
sub_event = SubscriptionEvent(
stripe_event_id=event['id'],
event_type=event['type'],
status="processed"
)
session.add(sub_event)

# Update User subscription logic would go here
# user = session.exec(select(User).where(User.stripe_customer_id == ...)).first()
# if user: ...

await session.commit()

return {"status": "success"}
33 changes: 33 additions & 0 deletions backend/routers/inversion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from backend.services.inversion import inversion_service
from backend.deps import get_current_user
from uuid import UUID
from backend.database import get_session
from sqlalchemy.ext.asyncio import AsyncSession
from backend.models import InversionOutcome

router = APIRouter()

class InversionRequest(BaseModel):
text: str

@router.post("/")
async def perform_inversion(
request: InversionRequest,
user_id: UUID = Depends(get_current_user),
session: AsyncSession = Depends(get_session)
):
result = inversion_service.perform_shadow_inversion(request.text)

outcome = InversionOutcome(
user_id=user_id,
input_text=request.text,
shadow_identified=result["identified_pattern"],
protocol_generated=result
)
session.add(outcome)
await session.commit()
await session.refresh(outcome)

return result
7 changes: 7 additions & 0 deletions backend/routers/ops.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from fastapi import APIRouter

router = APIRouter()

@router.get("/health")
async def health_check():
return {"status": "ok", "version": "1.0.0"}
40 changes: 40 additions & 0 deletions backend/routers/resolver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from backend.services.resolver import resolver_service
from backend.deps import get_current_user
from uuid import UUID
from backend.database import get_session
from sqlalchemy.ext.asyncio import AsyncSession
from backend.models import Blueprint

router = APIRouter()

class ResolverRequest(BaseModel):
date: str
time: str
lat: float
lon: float

@router.post("/")
async def resolve_blueprint(
request: ResolverRequest,
user_id: UUID = Depends(get_current_user),
session: AsyncSession = Depends(get_session)
):
result = resolver_service.resolve_blueprint(
request.date, request.time, request.lat, request.lon
)

if "error" in result:
raise HTTPException(status_code=400, detail=result["error"])

# Save Blueprint?
# prompt says "Build ... backend that exposes resolver"
# Usually we save it.

blueprint = Blueprint(user_id=user_id, data=result)
session.add(blueprint)
await session.commit()
await session.refresh(blueprint)

return {"id": str(blueprint.id), "data": result}
Loading