mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-05 05:05:08 +02:00
feat: add screenshot storage migration + web URL config
Supabase migration 004 creates: - pr-screenshots storage bucket (private, service_role read) - screenshots table with RLS (auth insert, public read metadata) - device_codes table for RFC 8628 auth flow (service_role only) - pg_cron cleanup for expired codes and orphan screenshots Also adds GSTACK_WEB_URL to config.sh for gstack.gg integration. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,3 +8,6 @@ GSTACK_SUPABASE_ANON_KEY="sb_publishable_tR4i6cyMIrYTE3s6OyHGHw_ppx2p6WK"
|
||||
|
||||
# Telemetry ingest endpoint (Data API)
|
||||
GSTACK_TELEMETRY_ENDPOINT="${GSTACK_SUPABASE_URL}/rest/v1"
|
||||
|
||||
# gstack.gg web app (auth + screenshot upload)
|
||||
GSTACK_WEB_URL="https://gstack.gg"
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
-- 004_screenshot_storage.sql
|
||||
-- PR screenshot storage + device code auth for CLI → web auth flow
|
||||
|
||||
-- ─── Storage bucket (PRIVATE — proxy adds watermark) ─────────────
|
||||
INSERT INTO storage.buckets (id, name, public)
|
||||
VALUES ('pr-screenshots', 'pr-screenshots', false)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- Storage RLS: authenticated users upload to their own prefix
|
||||
CREATE POLICY "auth_upload_own_prefix" ON storage.objects
|
||||
FOR INSERT TO authenticated
|
||||
WITH CHECK (bucket_id = 'pr-screenshots' AND (storage.foldername(name))[1] = auth.uid()::text);
|
||||
|
||||
-- Storage RLS: service_role reads (proxy fetches via service key)
|
||||
-- No public read — raw images must go through watermark proxy
|
||||
CREATE POLICY "service_read_screenshots" ON storage.objects
|
||||
FOR SELECT TO service_role
|
||||
USING (bucket_id = 'pr-screenshots');
|
||||
|
||||
-- Storage RLS: authenticated users can delete their own uploads
|
||||
CREATE POLICY "auth_delete_own" ON storage.objects
|
||||
FOR DELETE TO authenticated
|
||||
USING (bucket_id = 'pr-screenshots' AND (storage.foldername(name))[1] = auth.uid()::text);
|
||||
|
||||
-- ─── Screenshots metadata table ──────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS screenshots (
|
||||
id TEXT PRIMARY KEY, -- 8-char nanoid
|
||||
user_id UUID NOT NULL REFERENCES auth.users(id),
|
||||
storage_path TEXT NOT NULL, -- path in pr-screenshots bucket
|
||||
repo_slug TEXT NOT NULL, -- slugified repo name
|
||||
branch TEXT NOT NULL, -- slugified branch name
|
||||
viewport TEXT, -- e.g. 'mobile', 'tablet', 'desktop'
|
||||
pr_number INTEGER, -- populated after PR creation
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX idx_screenshots_user ON screenshots(user_id);
|
||||
CREATE INDEX idx_screenshots_repo_branch ON screenshots(repo_slug, branch);
|
||||
|
||||
-- RLS on screenshots: auth insert own, public read metadata, auth delete own
|
||||
ALTER TABLE screenshots ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "auth_insert_own_screenshots" ON screenshots
|
||||
FOR INSERT TO authenticated
|
||||
WITH CHECK (user_id = auth.uid());
|
||||
|
||||
CREATE POLICY "public_read_screenshots" ON screenshots
|
||||
FOR SELECT TO anon, authenticated
|
||||
USING (true);
|
||||
|
||||
CREATE POLICY "auth_delete_own_screenshots" ON screenshots
|
||||
FOR DELETE TO authenticated
|
||||
USING (user_id = auth.uid());
|
||||
|
||||
-- ─── Device codes table (RFC 8628 pattern) ───────────────────────
|
||||
CREATE TABLE IF NOT EXISTS device_codes (
|
||||
code TEXT PRIMARY KEY, -- server-generated device code
|
||||
device_secret TEXT NOT NULL, -- PKCE-like secret for verification
|
||||
user_code TEXT NOT NULL, -- short human-readable code (e.g. ABCD-1234)
|
||||
user_id UUID REFERENCES auth.users(id), -- NULL until user approves
|
||||
status TEXT NOT NULL DEFAULT 'pending', -- pending | approved | expired
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
expires_at TIMESTAMPTZ NOT NULL -- 10 minutes from creation
|
||||
);
|
||||
|
||||
-- Index for polling (CLI polls by device_code + secret)
|
||||
CREATE INDEX idx_device_codes_status ON device_codes(code, status);
|
||||
|
||||
-- RLS: service_role only (all access goes through API routes)
|
||||
ALTER TABLE device_codes ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "service_only_device_codes" ON device_codes
|
||||
FOR ALL TO service_role
|
||||
USING (true)
|
||||
WITH CHECK (true);
|
||||
|
||||
-- ─── Cleanup: expired device codes + orphan screenshots ──────────
|
||||
-- Delete expired device codes (> 15 minutes old, generous buffer over 10min expiry)
|
||||
-- Delete orphan screenshots (no PR number after 24h)
|
||||
-- Run via pg_cron if available, otherwise manual/API trigger
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_cron') THEN
|
||||
PERFORM cron.schedule(
|
||||
'cleanup_device_codes',
|
||||
'*/15 * * * *', -- every 15 minutes
|
||||
$$DELETE FROM device_codes WHERE expires_at < now() - interval '5 minutes'$$
|
||||
);
|
||||
PERFORM cron.schedule(
|
||||
'cleanup_orphan_screenshots',
|
||||
'0 */6 * * *', -- every 6 hours
|
||||
$$DELETE FROM screenshots WHERE pr_number IS NULL AND created_at < now() - interval '24 hours'$$
|
||||
);
|
||||
END IF;
|
||||
END $$;
|
||||
Reference in New Issue
Block a user