B2C/B2B split: DEFAULT_USER_TIER, ALLOWED_DOMAINS, zero-price filter, InboxRescue branding & pricing page

Agent-Logs-Url: https://github.com/christianlouis/pop_puller_to_gmail/sessions/98c1f90b-0287-4908-a0be-ad066c453cc5

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-26 16:10:39 +00:00
parent 53334402aa
commit d4aa353383
8 changed files with 237 additions and 70 deletions
+62 -20
View File
@@ -33,6 +33,48 @@ GOOGLE_LOGIN_SCOPES = [
]
def _domain_of(email: str) -> str:
"""Return the lowercased domain part of an email address."""
return email.split("@")[-1].lower()
def _check_domain_allowed(email: str) -> None:
"""
Raise 403 if ALLOWED_DOMAINS is configured and the email's domain is not
in the list. Always passes when ALLOWED_DOMAINS is empty (no restriction).
"""
if not settings.ALLOWED_DOMAINS:
return
domain = _domain_of(email)
if domain not in settings.ALLOWED_DOMAINS:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=(
f"Registrations are restricted to approved domains. "
f"'{domain}' is not authorised."
),
)
def _default_tier() -> SubscriptionTier:
"""Return the SubscriptionTier that should be assigned to every new user."""
try:
return SubscriptionTier(settings.DEFAULT_USER_TIER)
except ValueError:
logger.warning(
"DEFAULT_USER_TIER '%s' is not a valid tier; falling back to FREE.",
settings.DEFAULT_USER_TIER,
)
return SubscriptionTier.FREE
def _is_admin_email(email: str) -> bool:
return (
settings.ADMIN_EMAIL is not None
and email.lower() == settings.ADMIN_EMAIL.lower()
)
@router.post(
"/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED
)
@@ -48,6 +90,9 @@ async def register(user_in: UserCreate, db: AsyncSession = Depends(get_db)):
status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered"
)
# Domain restriction check (before creating the account)
_check_domain_allowed(user_in.email)
# Create new user
user = User(
email=user_in.email,
@@ -55,12 +100,9 @@ async def register(user_in: UserCreate, db: AsyncSession = Depends(get_db)):
hashed_password=(
get_password_hash(user_in.password) if user_in.password else None
),
subscription_tier=SubscriptionTier.FREE,
subscription_tier=_default_tier(),
is_active=True,
is_superuser=(
settings.ADMIN_EMAIL is not None
and user_in.email.lower() == settings.ADMIN_EMAIL.lower()
),
is_superuser=_is_admin_email(user_in.email),
)
db.add(user)
@@ -103,15 +145,15 @@ async def login(
status_code=status.HTTP_403_FORBIDDEN, detail="User account is inactive"
)
# Domain restriction — superusers always bypass
if not user.is_superuser:
_check_domain_allowed(user.email)
# Update last login
user.last_login_at = datetime.now(timezone.utc) # type: ignore[assignment]
# Auto-promote to superuser if this is the configured admin email
if (
settings.ADMIN_EMAIL is not None
and user.email.lower() == settings.ADMIN_EMAIL.lower()
and not user.is_superuser
):
if not user.is_superuser and _is_admin_email(user.email):
user.is_superuser = True # type: ignore[assignment]
logger.info(f"Auto-promoted admin user: {user.email}")
@@ -163,30 +205,30 @@ async def google_oauth(
# Update last login
user.last_login_at = datetime.now(timezone.utc) # type: ignore[assignment]
# Domain restriction — superusers always bypass
if not user.is_superuser:
_check_domain_allowed(email)
# Auto-promote to superuser if this is the configured admin email
if (
settings.ADMIN_EMAIL is not None
and user.email.lower() == settings.ADMIN_EMAIL.lower()
and not user.is_superuser
):
if not user.is_superuser and _is_admin_email(email):
user.is_superuser = True # type: ignore[assignment]
logger.info(f"Auto-promoted admin user via Google OAuth: {user.email}")
logger.info(f"Existing user logged in with Google: {user.email}")
else:
# Domain restriction check before creating the account
_check_domain_allowed(email)
# Create new user
user = User(
email=email,
full_name=user_info.get("full_name"),
google_id=google_id,
oauth_provider="google",
subscription_tier=SubscriptionTier.FREE,
subscription_tier=_default_tier(),
is_active=True,
last_login_at=datetime.now(timezone.utc),
is_superuser=(
settings.ADMIN_EMAIL is not None
and email.lower() == settings.ADMIN_EMAIL.lower()
),
is_superuser=_is_admin_email(email),
)
db.add(user)
+11 -2
View File
@@ -15,9 +15,18 @@ router = APIRouter()
@router.get("/plans", response_model=List[SubscriptionPlanResponse])
async def list_subscription_plans(db: AsyncSession = Depends(get_db)):
"""List all available subscription plans"""
"""
List subscription plans shown in marketing / pricing pages.
Zero-price plans (price_monthly == 0) are intentionally excluded so that
enterprise / white-label deployments that assign a free plan to all users
don't surface that plan in the public pricing UI.
"""
result = await db.execute(
select(SubscriptionPlan).where(SubscriptionPlan.is_active == True) # noqa: E712
select(SubscriptionPlan).where(
SubscriptionPlan.is_active.is_(True),
SubscriptionPlan.price_monthly > 0,
)
)
return result.scalars().all()
+26
View File
@@ -97,6 +97,15 @@ class Settings(BaseSettings):
ADMIN_EMAIL: Optional[str] = "christianlouis@gmail.com"
ADMIN_PASSWORD: Optional[str] = None
# User defaults & access control
# Tier assigned to every new user on registration: free | basic | pro | enterprise
DEFAULT_USER_TIER: str = "free"
# Comma-separated list of allowed email domains (empty = no restriction).
# When set, only addresses from these domains may register or log in.
# Useful for B2B / Google Workspace installations.
# Example: "company.com,subsidiary.com"
ALLOWED_DOMAINS: List[str] = []
# Mail Server Presets
MAIL_SERVER_PRESETS_FILE: str = "app/data/mail_server_presets.json"
@@ -108,6 +117,23 @@ class Settings(BaseSettings):
return [i.strip() for i in v.split(",")]
return v
@field_validator("ALLOWED_DOMAINS", mode="before")
@classmethod
def assemble_allowed_domains(cls, v: str | List[str]) -> List[str]:
"""Parse allowed domains from a comma-separated environment variable"""
if isinstance(v, str):
return [d.strip().lower() for d in v.split(",") if d.strip()]
return [d.lower() for d in v if d]
@field_validator("DEFAULT_USER_TIER")
@classmethod
def validate_default_user_tier(cls, v: str) -> str:
"""Ensure DEFAULT_USER_TIER is one of the known tier values"""
valid = {"free", "basic", "pro", "enterprise"}
if v.lower() not in valid:
raise ValueError(f"DEFAULT_USER_TIER must be one of {valid}, got '{v}'")
return v.lower()
@field_validator("SECRET_KEY")
@classmethod
def validate_secret_key(cls, v: str) -> str:
+2 -2
View File
@@ -65,7 +65,7 @@ def create_application() -> FastAPI:
app = FastAPI(
title=settings.APP_NAME,
version=settings.APP_VERSION,
description="Multi-tenant POP3/IMAP to Gmail forwarder with subscription management",
description="Poll your legacy POP3/IMAP inboxes and deliver everything to Gmail. For real people, not enterprises.",
docs_url="/api/docs",
redoc_url="/api/redoc",
openapi_url="/api/openapi.json",
@@ -92,7 +92,7 @@ def create_application() -> FastAPI:
async def root():
"""Root endpoint"""
return {
"message": "POP3 Forwarder SaaS API",
"message": "InboxRescue API",
"version": settings.APP_VERSION,
"docs": "/api/docs",
}