193 lines
5.7 KiB
PL/PgSQL
193 lines
5.7 KiB
PL/PgSQL
-- Rate Limiting Migration
|
|
-- Adds rate limiting tables and functions for API protection
|
|
|
|
-- 1. Create rate_limit_rules table
|
|
CREATE TABLE IF NOT EXISTS rate_limit_rules (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
endpoint TEXT NOT NULL UNIQUE, -- e.g., 'create_trip', 'ai_suggest', 'export_data'
|
|
max_requests INTEGER NOT NULL DEFAULT 10,
|
|
window_seconds INTEGER NOT NULL DEFAULT 60, -- Time window in seconds
|
|
enabled BOOLEAN NOT NULL DEFAULT true,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
-- 2. Create rate_limit_logs table
|
|
CREATE TABLE IF NOT EXISTS rate_limit_logs (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
ip_address INET,
|
|
endpoint TEXT NOT NULL,
|
|
request_count INTEGER NOT NULL DEFAULT 1,
|
|
window_start TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
blocked BOOLEAN NOT NULL DEFAULT false,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
-- 3. Add indexes
|
|
CREATE INDEX IF NOT EXISTS idx_rate_limit_logs_user_endpoint ON rate_limit_logs(user_id, endpoint, window_start);
|
|
CREATE INDEX IF NOT EXISTS idx_rate_limit_logs_ip_endpoint ON rate_limit_logs(ip_address, endpoint, window_start);
|
|
CREATE INDEX IF NOT EXISTS idx_rate_limit_logs_created_at ON rate_limit_logs(created_at DESC);
|
|
|
|
-- 4. Insert default rate limit rules
|
|
INSERT INTO rate_limit_rules (endpoint, max_requests, window_seconds) VALUES
|
|
('create_trip', 10, 3600), -- 10 trips per hour
|
|
('ai_suggest', 20, 3600), -- 20 AI suggestions per hour
|
|
('export_data', 3, 86400), -- 3 data exports per day
|
|
('delete_account', 1, 86400), -- 1 account deletion per day
|
|
('send_email', 10, 3600), -- 10 emails per hour
|
|
('api_general', 100, 60), -- 100 general API calls per minute
|
|
('login_attempt', 5, 300), -- 5 login attempts per 5 minutes
|
|
('signup', 3, 3600) -- 3 signups per hour per IP
|
|
ON CONFLICT (endpoint) DO NOTHING;
|
|
|
|
-- 5. Create function to check rate limit
|
|
CREATE OR REPLACE FUNCTION check_rate_limit(
|
|
p_user_id UUID,
|
|
p_ip_address INET,
|
|
p_endpoint TEXT
|
|
) RETURNS JSONB AS $$
|
|
DECLARE
|
|
v_rule RECORD;
|
|
v_log RECORD;
|
|
v_window_start TIMESTAMPTZ;
|
|
v_allowed BOOLEAN := true;
|
|
v_remaining INTEGER;
|
|
BEGIN
|
|
-- Get rate limit rule
|
|
SELECT * INTO v_rule
|
|
FROM rate_limit_rules
|
|
WHERE endpoint = p_endpoint AND enabled = true;
|
|
|
|
-- If no rule exists, allow the request
|
|
IF NOT FOUND THEN
|
|
RETURN jsonb_build_object(
|
|
'allowed', true,
|
|
'remaining', 999,
|
|
'reset_at', NULL
|
|
);
|
|
END IF;
|
|
|
|
-- Calculate window start time
|
|
v_window_start := NOW() - (v_rule.window_seconds || ' seconds')::INTERVAL;
|
|
|
|
-- Check user-based rate limit (if user_id provided)
|
|
IF p_user_id IS NOT NULL THEN
|
|
SELECT
|
|
SUM(request_count) as total_requests
|
|
INTO v_log
|
|
FROM rate_limit_logs
|
|
WHERE user_id = p_user_id
|
|
AND endpoint = p_endpoint
|
|
AND window_start >= v_window_start;
|
|
|
|
IF v_log.total_requests >= v_rule.max_requests THEN
|
|
v_allowed := false;
|
|
END IF;
|
|
|
|
v_remaining := GREATEST(0, v_rule.max_requests - COALESCE(v_log.total_requests, 0));
|
|
ELSE
|
|
-- Check IP-based rate limit
|
|
SELECT
|
|
SUM(request_count) as total_requests
|
|
INTO v_log
|
|
FROM rate_limit_logs
|
|
WHERE ip_address = p_ip_address
|
|
AND endpoint = p_endpoint
|
|
AND window_start >= v_window_start;
|
|
|
|
IF v_log.total_requests >= v_rule.max_requests THEN
|
|
v_allowed := false;
|
|
END IF;
|
|
|
|
v_remaining := GREATEST(0, v_rule.max_requests - COALESCE(v_log.total_requests, 0));
|
|
END IF;
|
|
|
|
-- Log the request
|
|
INSERT INTO rate_limit_logs (
|
|
user_id,
|
|
ip_address,
|
|
endpoint,
|
|
request_count,
|
|
window_start,
|
|
blocked
|
|
) VALUES (
|
|
p_user_id,
|
|
p_ip_address,
|
|
p_endpoint,
|
|
1,
|
|
NOW(),
|
|
NOT v_allowed
|
|
);
|
|
|
|
RETURN jsonb_build_object(
|
|
'allowed', v_allowed,
|
|
'remaining', v_remaining,
|
|
'reset_at', NOW() + (v_rule.window_seconds || ' seconds')::INTERVAL,
|
|
'limit', v_rule.max_requests,
|
|
'window_seconds', v_rule.window_seconds
|
|
);
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
-- 6. Create function to clean old rate limit logs (run periodically)
|
|
CREATE OR REPLACE FUNCTION cleanup_rate_limit_logs()
|
|
RETURNS INTEGER AS $$
|
|
DECLARE
|
|
v_deleted INTEGER;
|
|
BEGIN
|
|
-- Delete logs older than 7 days
|
|
DELETE FROM rate_limit_logs
|
|
WHERE created_at < NOW() - INTERVAL '7 days';
|
|
|
|
GET DIAGNOSTICS v_deleted = ROW_COUNT;
|
|
|
|
RETURN v_deleted;
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
-- 7. Create RLS policies
|
|
ALTER TABLE rate_limit_rules ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE rate_limit_logs ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- Admins can view and manage rate limit rules
|
|
CREATE POLICY "Admins can view rate limit rules"
|
|
ON rate_limit_rules FOR SELECT
|
|
USING (
|
|
EXISTS (
|
|
SELECT 1 FROM profiles
|
|
WHERE profiles.id = auth.uid()
|
|
AND profiles.role = 'admin'
|
|
)
|
|
);
|
|
|
|
CREATE POLICY "Admins can manage rate limit rules"
|
|
ON rate_limit_rules FOR ALL
|
|
USING (
|
|
EXISTS (
|
|
SELECT 1 FROM profiles
|
|
WHERE profiles.id = auth.uid()
|
|
AND profiles.role = 'admin'
|
|
)
|
|
);
|
|
|
|
-- Users can view their own rate limit logs
|
|
CREATE POLICY "Users can view their own rate limit logs"
|
|
ON rate_limit_logs FOR SELECT
|
|
USING (auth.uid() = user_id);
|
|
|
|
-- Admins can view all rate limit logs
|
|
CREATE POLICY "Admins can view all rate limit logs"
|
|
ON rate_limit_logs FOR SELECT
|
|
USING (
|
|
EXISTS (
|
|
SELECT 1 FROM profiles
|
|
WHERE profiles.id = auth.uid()
|
|
AND profiles.role = 'admin'
|
|
)
|
|
);
|
|
|
|
-- Service role can insert rate limit logs
|
|
CREATE POLICY "Service role can insert rate limit logs"
|
|
ON rate_limit_logs FOR INSERT
|
|
WITH CHECK (true); |