Skip to content

Commit 0e3b167

Browse files
Add api usage tracking migration
- Apply new 20251209074024_api_usage_tracking.sql to enable API usage tracking - Create api_usage_logs table, add daily counters to tenants, and related indices/policies - Prepare for per-tenant rate limiting and usage auditing across tenants X-Lovable-Edit-ID: edt-94a767c9-1004-4865-a1b2-fd6b1da9f3cc
2 parents b476f5a + 99312a1 commit 0e3b167

File tree

8 files changed

+206
-8
lines changed

8 files changed

+206
-8
lines changed

src/hooks/useQRMMetrics.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export function useCellQRMMetrics(
3939
);
4040

4141
if (rpcError) throw rpcError;
42-
setMetrics(data as CellQRMMetrics);
42+
setMetrics(data as unknown as CellQRMMetrics);
4343
} catch (err) {
4444
setError(err as Error);
4545
logger.error("Failed to fetch cell QRM metrics", err, {
@@ -138,7 +138,7 @@ export function useNextCellCapacity(
138138
);
139139

140140
if (rpcError) throw rpcError;
141-
setCapacity(data as NextCellCapacity);
141+
setCapacity(data as unknown as NextCellCapacity);
142142
} catch (err) {
143143
setError(err as Error);
144144
logger.error("Failed to check next cell capacity", err, {

src/hooks/useRealtimeSubscription.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,8 @@ export function useRealtimeSubscription(options: RealtimeSubscriptionOptions): v
146146
}
147147

148148
channel = channel.on(
149-
'postgres_changes',
150-
config,
149+
'postgres_changes' as any,
150+
config as any,
151151
(payload: RealtimePostgresChangesPayload<Record<string, unknown>>) => {
152152
logger.debug('Realtime change received', {
153153
operation: 'useRealtimeSubscription',

src/integrations/supabase/types.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,51 @@ export type Database = {
125125
}
126126
Relationships: []
127127
}
128+
api_usage_logs: {
129+
Row: {
130+
api_key_id: string | null
131+
created_at: string | null
132+
date: string
133+
id: string
134+
requests_count: number | null
135+
tenant_id: string
136+
updated_at: string | null
137+
}
138+
Insert: {
139+
api_key_id?: string | null
140+
created_at?: string | null
141+
date?: string
142+
id?: string
143+
requests_count?: number | null
144+
tenant_id: string
145+
updated_at?: string | null
146+
}
147+
Update: {
148+
api_key_id?: string | null
149+
created_at?: string | null
150+
date?: string
151+
id?: string
152+
requests_count?: number | null
153+
tenant_id?: string
154+
updated_at?: string | null
155+
}
156+
Relationships: [
157+
{
158+
foreignKeyName: "api_usage_logs_api_key_id_fkey"
159+
columns: ["api_key_id"]
160+
isOneToOne: false
161+
referencedRelation: "api_keys"
162+
referencedColumns: ["id"]
163+
},
164+
{
165+
foreignKeyName: "api_usage_logs_tenant_id_fkey"
166+
columns: ["tenant_id"]
167+
isOneToOne: false
168+
referencedRelation: "tenants"
169+
referencedColumns: ["id"]
170+
},
171+
]
172+
}
128173
assignments: {
129174
Row: {
130175
assigned_by: string
@@ -2731,6 +2776,8 @@ export type Database = {
27312776
tenants: {
27322777
Row: {
27332778
abbreviation: string | null
2779+
api_requests_reset_at: string | null
2780+
api_requests_today: number | null
27342781
auto_stop_tracking: boolean | null
27352782
billing_country_code: string | null
27362783
billing_email: string | null
@@ -2779,6 +2826,8 @@ export type Database = {
27792826
}
27802827
Insert: {
27812828
abbreviation?: string | null
2829+
api_requests_reset_at?: string | null
2830+
api_requests_today?: number | null
27822831
auto_stop_tracking?: boolean | null
27832832
billing_country_code?: string | null
27842833
billing_email?: string | null
@@ -2827,6 +2876,8 @@ export type Database = {
28272876
}
28282877
Update: {
28292878
abbreviation?: string | null
2879+
api_requests_reset_at?: string | null
2880+
api_requests_today?: number | null
28302881
auto_stop_tracking?: boolean | null
28312882
billing_country_code?: string | null
28322883
billing_email?: string | null
@@ -3277,6 +3328,15 @@ export type Database = {
32773328
unique_users: number
32783329
}[]
32793330
}
3331+
get_api_usage_stats: {
3332+
Args: { p_tenant_id?: string }
3333+
Returns: {
3334+
daily_limit: number
3335+
reset_at: string
3336+
this_month_requests: number
3337+
today_requests: number
3338+
}[]
3339+
}
32803340
get_cell_qrm_metrics: {
32813341
Args: { cell_id_param: string; tenant_id_param: string }
32823342
Returns: Json
@@ -3460,6 +3520,10 @@ export type Database = {
34603520
}
34613521
Returns: boolean
34623522
}
3523+
increment_api_usage: {
3524+
Args: { p_api_key_id?: string; p_tenant_id: string }
3525+
Returns: number
3526+
}
34633527
is_demo_mode: { Args: { p_tenant_id: string }; Returns: boolean }
34643528
is_root_admin: { Args: never; Returns: boolean }
34653529
list_all_tenants: {

src/pages/common/MyPlan.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ const getPricingTiers = (t: (key: string) => string) => [
7373
},
7474
];
7575

76-
export const MyPlan: React.FC = () => {
76+
const MyPlan: React.FC = () => {
7777
const { t } = useTranslation();
7878
const {
7979
subscription,
@@ -406,3 +406,5 @@ export const MyPlan: React.FC = () => {
406406
</div>
407407
);
408408
};
409+
410+
export default MyPlan;

src/pages/common/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
export { default as About } from './About';
33
export { default as ApiDocs } from './ApiDocs';
44
export { default as Help } from './Help';
5-
export { MyPlan } from './MyPlan';
5+
export { default as MyPlan } from './MyPlan';
66
export { default as Pricing } from './Pricing';
77
export { default as PrivacyPolicy } from './PrivacyPolicy';
88
export { default as SubscriptionBlocked } from './SubscriptionBlocked';

supabase/functions/_shared/handler.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export function createApiHandler(
101101
tenantId: "",
102102
apiKeyId: "",
103103
keyPrefix: "",
104+
plan: "free",
104105
};
105106

106107
// Authenticate unless public endpoint

supabase/functions/_shared/validation/errorHandler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,13 +203,13 @@ export function handleError(error: unknown): Response {
203203
// Rate limit errors (429)
204204
if (error instanceof RateLimitError) {
205205
const rateLimitHeaders = getRateLimitHeaders(error.rateLimitResult);
206-
const body: ApiErrorResponse = {
206+
const body = {
207207
success: false,
208208
error: {
209209
code: "RATE_LIMIT_EXCEEDED",
210210
message: error.message,
211211
statusCode: 429,
212-
details: {
212+
rateLimitInfo: {
213213
remaining: error.rateLimitResult.remaining,
214214
resetAt: new Date(error.rateLimitResult.resetAt).toISOString(),
215215
retryAfter: error.rateLimitResult.retryAfter,
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
-- API Usage Tracking Migration
2+
-- Adds daily API usage tracking for plan-based rate limiting
3+
4+
-- Add API usage columns to tenants table for current day tracking
5+
ALTER TABLE public.tenants
6+
ADD COLUMN IF NOT EXISTS api_requests_today INTEGER DEFAULT 0,
7+
ADD COLUMN IF NOT EXISTS api_requests_reset_at TIMESTAMPTZ DEFAULT (CURRENT_DATE + INTERVAL '1 day');
8+
9+
-- Create API usage logs table for historical tracking
10+
CREATE TABLE IF NOT EXISTS public.api_usage_logs (
11+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
12+
tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
13+
api_key_id UUID REFERENCES public.api_keys(id) ON DELETE SET NULL,
14+
date DATE NOT NULL DEFAULT CURRENT_DATE,
15+
requests_count INTEGER DEFAULT 0,
16+
created_at TIMESTAMPTZ DEFAULT NOW(),
17+
updated_at TIMESTAMPTZ DEFAULT NOW(),
18+
UNIQUE(tenant_id, date)
19+
);
20+
21+
-- Index for efficient lookups
22+
CREATE INDEX IF NOT EXISTS idx_api_usage_logs_tenant_date
23+
ON public.api_usage_logs(tenant_id, date DESC);
24+
25+
-- RLS policies for api_usage_logs
26+
ALTER TABLE public.api_usage_logs ENABLE ROW LEVEL SECURITY;
27+
28+
-- Users can view their own tenant's usage
29+
CREATE POLICY "Users can view own tenant API usage"
30+
ON public.api_usage_logs
31+
FOR SELECT
32+
USING (tenant_id = public.get_user_tenant_id());
33+
34+
-- Service role can insert/update
35+
CREATE POLICY "Service role can manage API usage"
36+
ON public.api_usage_logs
37+
FOR ALL
38+
USING (auth.role() = 'service_role');
39+
40+
-- Function to increment API usage (called from Edge Functions)
41+
CREATE OR REPLACE FUNCTION public.increment_api_usage(
42+
p_tenant_id UUID,
43+
p_api_key_id UUID DEFAULT NULL
44+
)
45+
RETURNS INTEGER
46+
LANGUAGE plpgsql
47+
SECURITY DEFINER
48+
SET search_path = public
49+
AS $$
50+
DECLARE
51+
v_today DATE := CURRENT_DATE;
52+
v_new_count INTEGER;
53+
BEGIN
54+
-- Upsert into daily usage log
55+
INSERT INTO public.api_usage_logs (tenant_id, api_key_id, date, requests_count)
56+
VALUES (p_tenant_id, p_api_key_id, v_today, 1)
57+
ON CONFLICT (tenant_id, date)
58+
DO UPDATE SET
59+
requests_count = api_usage_logs.requests_count + 1,
60+
updated_at = NOW()
61+
RETURNING requests_count INTO v_new_count;
62+
63+
-- Also update tenant's current day counter
64+
UPDATE public.tenants
65+
SET
66+
api_requests_today = CASE
67+
WHEN api_requests_reset_at < NOW() THEN 1
68+
ELSE api_requests_today + 1
69+
END,
70+
api_requests_reset_at = CASE
71+
WHEN api_requests_reset_at < NOW() THEN CURRENT_DATE + INTERVAL '1 day'
72+
ELSE api_requests_reset_at
73+
END
74+
WHERE id = p_tenant_id;
75+
76+
RETURN v_new_count;
77+
END;
78+
$$;
79+
80+
-- Function to get current usage for a tenant
81+
CREATE OR REPLACE FUNCTION public.get_api_usage_stats(
82+
p_tenant_id UUID DEFAULT NULL
83+
)
84+
RETURNS TABLE(
85+
today_requests INTEGER,
86+
this_month_requests BIGINT,
87+
reset_at TIMESTAMPTZ,
88+
daily_limit INTEGER
89+
)
90+
LANGUAGE plpgsql
91+
SECURITY DEFINER
92+
SET search_path = public
93+
AS $$
94+
DECLARE
95+
v_tenant_id UUID := COALESCE(p_tenant_id, public.get_user_tenant_id());
96+
v_plan TEXT;
97+
v_daily_limit INTEGER;
98+
BEGIN
99+
-- Get tenant plan
100+
SELECT t.plan INTO v_plan
101+
FROM public.tenants t
102+
WHERE t.id = v_tenant_id;
103+
104+
-- Determine daily limit based on plan
105+
v_daily_limit := CASE v_plan
106+
WHEN 'free' THEN 100
107+
WHEN 'pro' THEN 1000
108+
WHEN 'premium' THEN 10000
109+
WHEN 'enterprise' THEN NULL -- unlimited
110+
ELSE 100
111+
END;
112+
113+
RETURN QUERY
114+
SELECT
115+
COALESCE(t.api_requests_today, 0)::INTEGER as today_requests,
116+
COALESCE((
117+
SELECT SUM(requests_count)
118+
FROM api_usage_logs
119+
WHERE tenant_id = v_tenant_id
120+
AND date >= DATE_TRUNC('month', CURRENT_DATE)
121+
), 0)::BIGINT as this_month_requests,
122+
t.api_requests_reset_at as reset_at,
123+
v_daily_limit as daily_limit
124+
FROM public.tenants t
125+
WHERE t.id = v_tenant_id;
126+
END;
127+
$$;
128+
129+
-- Grant execute permissions
130+
GRANT EXECUTE ON FUNCTION public.increment_api_usage TO service_role;
131+
GRANT EXECUTE ON FUNCTION public.get_api_usage_stats TO authenticated;

0 commit comments

Comments
 (0)