mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
Resolve all 5 deferred items from Epic 5 adversarial code review: - Migration 124: Add CASCADE to subscriptionstatus enum drop (prevent orphaned references) - Stripe rate limiting: In-memory per-user limiter (20 calls/60s) on verify-checkout-session - Subscription request cooldown: 24h cooldown before resubmitting rejected requests - Token reset date: Initialize on first subscription activation - Checkout URL validation: Confirmed HTTPS-only (Stripe always returns HTTPS) Implement Story 5.4 (Usage Tracking & Rate Limit Enforcement): - Page quota pre-check at HTTP upload layer - Extend UserRead schema with token quota fields - Frontend 402 error handling in document upload - Quota indicator in dashboard sidebar Story 5.5 (Admin Seed & Approval Flow): - Seed admin user migration with default credentials warning - Subscription approval/rejection routes with admin guard - 24h rejection cooldown enforcement Story 5.6 (Admin-Only Model Config): - Global model config visible across all search spaces - Per-search-space model configs with user access control - Superuser CRUD for global configs Additional fixes from code review: - PageLimitService: PAST_DUE subscriptions enforce free-tier limits - TokenQuotaService: PAST_DUE subscriptions enforce free-tier limits - Config routes: Fixed user_id.is_(None) filter on mutation endpoints - Stripe webhook: Added guard against silent plan downgrade on unrecognized price_id All changes formatted with Ruff (Python) and Biome (TypeScript). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
98 lines
3.4 KiB
TypeScript
98 lines
3.4 KiB
TypeScript
"use client";
|
|
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { CreditCard, Zap } from "lucide-react";
|
|
import Link from "next/link";
|
|
import { useParams } from "next/navigation";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Progress } from "@/components/ui/progress";
|
|
import { stripeApiService } from "@/lib/apis/stripe-api.service";
|
|
|
|
interface PageUsageDisplayProps {
|
|
pagesUsed: number;
|
|
pagesLimit: number;
|
|
tokensUsed: number;
|
|
tokensLimit: number;
|
|
}
|
|
|
|
function formatTokenCount(n: number): string {
|
|
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
if (n >= 1_000) return `${(n / 1_000).toFixed(0)}K`;
|
|
return n.toLocaleString();
|
|
}
|
|
|
|
function progressColor(percent: number): string {
|
|
if (percent > 95) return "[&>div]:bg-red-500";
|
|
if (percent > 80) return "[&>div]:bg-amber-500";
|
|
return "";
|
|
}
|
|
|
|
export function PageUsageDisplay({
|
|
pagesUsed,
|
|
pagesLimit,
|
|
tokensUsed,
|
|
tokensLimit,
|
|
}: PageUsageDisplayProps) {
|
|
const params = useParams();
|
|
const searchSpaceId = params?.search_space_id ?? "";
|
|
const pagePercent = Math.min(100, (pagesUsed / pagesLimit) * 100);
|
|
const tokenPercent = Math.min(100, (tokensUsed / tokensLimit) * 100);
|
|
const { data: stripeStatus } = useQuery({
|
|
queryKey: ["stripe-status"],
|
|
queryFn: () => stripeApiService.getStatus(),
|
|
});
|
|
const pageBuyingEnabled = stripeStatus?.page_buying_enabled ?? true;
|
|
|
|
return (
|
|
<div className="px-3 py-3 border-t">
|
|
<div className="space-y-2">
|
|
{/* Page usage */}
|
|
<div className="space-y-1">
|
|
<div className="flex justify-between items-center text-xs">
|
|
<span className="text-muted-foreground">
|
|
{pagesUsed.toLocaleString()} / {pagesLimit.toLocaleString()} pages
|
|
</span>
|
|
<span className="font-medium">{pagePercent.toFixed(0)}%</span>
|
|
</div>
|
|
<Progress value={pagePercent} className={`h-1.5 ${progressColor(pagePercent)}`} />
|
|
</div>
|
|
|
|
{/* Token usage */}
|
|
<div className="space-y-1">
|
|
<div className="flex justify-between items-center text-xs">
|
|
<span className="text-muted-foreground">
|
|
{formatTokenCount(tokensUsed)} / {formatTokenCount(tokensLimit)} tokens
|
|
</span>
|
|
<span className="font-medium">{tokenPercent.toFixed(0)}%</span>
|
|
</div>
|
|
<Progress value={tokenPercent} className={`h-1.5 ${progressColor(tokenPercent)}`} />
|
|
</div>
|
|
|
|
<Link
|
|
href={`/dashboard/${searchSpaceId}/more-pages`}
|
|
className="group flex w-[calc(100%+0.75rem)] items-center justify-between rounded-md px-1.5 py-1 -mx-1.5 transition-colors hover:bg-accent"
|
|
>
|
|
<span className="flex items-center gap-1.5 text-xs text-muted-foreground group-hover:text-accent-foreground">
|
|
<Zap className="h-3 w-3 shrink-0" />
|
|
Get Free Pages
|
|
</span>
|
|
<Badge className="h-4 rounded px-1 text-[10px] font-semibold leading-none bg-emerald-600 text-white border-transparent hover:bg-emerald-600">
|
|
FREE
|
|
</Badge>
|
|
</Link>
|
|
{pageBuyingEnabled && (
|
|
<Link
|
|
href={`/dashboard/${searchSpaceId}/buy-pages`}
|
|
className="group flex w-[calc(100%+0.75rem)] items-center justify-between rounded-md px-1.5 py-1 -mx-1.5 transition-colors hover:bg-accent"
|
|
>
|
|
<span className="flex items-center gap-1.5 text-xs text-muted-foreground group-hover:text-accent-foreground">
|
|
<CreditCard className="h-3 w-3 shrink-0" />
|
|
Buy Pages
|
|
</span>
|
|
<span className="text-[10px] font-medium text-muted-foreground">$1/1k</span>
|
|
</Link>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|