diff --git a/.env b/.env new file mode 100644 index 0000000..cb0fa4c --- /dev/null +++ b/.env @@ -0,0 +1,6 @@ +SUPABASE_PROJECT_ID="fvjpfhfgkhgtwtzrgcgx" +SUPABASE_PUBLISHABLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImZ2anBmaGZna2hndHd0enJnY2d4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODI4NDE0NDIsImV4cCI6MjA5ODQxNzQ0Mn0.B7dh8P2ZfUbJMtAGVbbRyyNFk71oqhJuqd0BNki-Dqk" +SUPABASE_URL="https://fvjpfhfgkhgtwtzrgcgx.supabase.co" +VITE_SUPABASE_PROJECT_ID="fvjpfhfgkhgtwtzrgcgx" +VITE_SUPABASE_PUBLISHABLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImZ2anBmaGZna2hndHd0enJnY2d4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODI4NDE0NDIsImV4cCI6MjA5ODQxNzQ0Mn0.B7dh8P2ZfUbJMtAGVbbRyyNFk71oqhJuqd0BNki-Dqk" +VITE_SUPABASE_URL="https://fvjpfhfgkhgtwtzrgcgx.supabase.co" diff --git a/src/integrations/supabase/auth-attacher.ts b/src/integrations/supabase/auth-attacher.ts new file mode 100644 index 0000000..5bc57b7 --- /dev/null +++ b/src/integrations/supabase/auth-attacher.ts @@ -0,0 +1,15 @@ +// This file is automatically generated. Do not edit it directly. +import { createMiddleware } from '@tanstack/react-start' +import { supabase } from './client' + +// Must be registered as a global `functionMiddleware` in `src/start.ts`; otherwise +// the browser never attaches the bearer token to serverFn RPCs. +export const attachSupabaseAuth = createMiddleware({ type: 'function' }).client( + async ({ next }) => { + const { data } = await supabase.auth.getSession() + const token = data.session?.access_token + return next({ + headers: token ? { Authorization: `Bearer ${token}` } : {}, + }) + }, +) diff --git a/src/integrations/supabase/auth-middleware.ts b/src/integrations/supabase/auth-middleware.ts new file mode 100644 index 0000000..0aefbe6 --- /dev/null +++ b/src/integrations/supabase/auth-middleware.ts @@ -0,0 +1,109 @@ +// This file is automatically generated. Do not edit it directly. +import { createMiddleware } from '@tanstack/react-start' +import { getRequest } from '@tanstack/react-start/server' +import { createClient } from '@supabase/supabase-js' +import type { Database } from './types' + + + +function isNewSupabaseApiKey(value: string): boolean { + return value.startsWith('sb_publishable_') || value.startsWith('sb_secret_'); +} + +function createSupabaseFetch(supabaseKey: string): typeof fetch { + return (input, init) => { + const headers = new Headers( + typeof Request !== 'undefined' && input instanceof Request ? input.headers : undefined, + ); + + if (init?.headers) { + new Headers(init.headers).forEach((value, key) => headers.set(key, value)); + } + + // New Supabase API keys are opaque strings, not bearer JWTs. + if (isNewSupabaseApiKey(supabaseKey) && headers.get('Authorization') === `Bearer ${supabaseKey}`) { + headers.delete('Authorization'); + } + + headers.set('apikey', supabaseKey); + return fetch(input, { ...init, headers }); + }; +} + +export const requireSupabaseAuth = createMiddleware({ type: 'function' }).server( + async ({ next }) => { + + const SUPABASE_URL = process.env.SUPABASE_URL; + const SUPABASE_PUBLISHABLE_KEY = process.env.SUPABASE_PUBLISHABLE_KEY; + + if (!SUPABASE_URL || !SUPABASE_PUBLISHABLE_KEY) { + const missing = [ + ...(!SUPABASE_URL ? ['SUPABASE_URL'] : []), + ...(!SUPABASE_PUBLISHABLE_KEY ? ['SUPABASE_PUBLISHABLE_KEY'] : []), + ]; + const message = `Missing Supabase environment variable(s): ${missing.join(', ')}. Connect Supabase in Lovable Cloud.`; + console.error(`[Supabase] ${message}`); + throw new Error(message); + } + + const request = getRequest(); + + if (!request?.headers) { + throw new Error('Unauthorized: No request headers available'); + } + + const authHeader = request.headers.get('authorization'); + + if (!authHeader) { + throw new Error('Unauthorized: No authorization header provided'); + } + + if (!authHeader.startsWith('Bearer ')) { + throw new Error('Unauthorized: Only Bearer tokens are supported'); + } + + const token = authHeader.replace('Bearer ', ''); + if (!token) { + throw new Error('Unauthorized: No token provided'); + } + + if (token.split('.').length !== 3) { + throw new Error('Unauthorized: Invalid token'); + } + + const supabase = createClient( + SUPABASE_URL!, + SUPABASE_PUBLISHABLE_KEY!, + { + global: { + fetch: createSupabaseFetch(SUPABASE_PUBLISHABLE_KEY!), + headers: { + Authorization: `Bearer ${token}`, + }, + }, + auth: { + storage: undefined, + persistSession: false, + autoRefreshToken: false, + }, + } + ); + + const { data, error } = await supabase.auth.getClaims(token); + if (error || !data?.claims) { + throw new Error('Unauthorized: Invalid token'); + } + + if (!data.claims.sub) { + throw new Error('Unauthorized: No user ID found in token'); + } + + return next({ + context: { + supabase, + userId: data.claims.sub, + claims: data.claims, + }, + }); + }, +); diff --git a/src/integrations/supabase/client.server.ts b/src/integrations/supabase/client.server.ts new file mode 100644 index 0000000..30e131b --- /dev/null +++ b/src/integrations/supabase/client.server.ts @@ -0,0 +1,69 @@ +// This file is automatically generated. Do not edit it directly. +// Server-side Supabase client with service role key - bypasses RLS. +// Use this for admin operations in server functions and server routes only. +// For user-authenticated queries (with RLS), use the auth middleware instead. +import { createClient } from '@supabase/supabase-js'; +import type { Database } from './types'; + +function isNewSupabaseApiKey(value: string): boolean { + return value.startsWith('sb_publishable_') || value.startsWith('sb_secret_'); +} + +function createSupabaseFetch(supabaseKey: string): typeof fetch { + return (input, init) => { + const headers = new Headers( + typeof Request !== 'undefined' && input instanceof Request ? input.headers : undefined, + ); + + if (init?.headers) { + new Headers(init.headers).forEach((value, key) => headers.set(key, value)); + } + + // New Supabase API keys are opaque strings, not bearer JWTs. + if (isNewSupabaseApiKey(supabaseKey) && headers.get('Authorization') === `Bearer ${supabaseKey}`) { + headers.delete('Authorization'); + } + + headers.set('apikey', supabaseKey); + return fetch(input, { ...init, headers }); + }; +} + +function createSupabaseAdminClient() { + const SUPABASE_URL = process.env.SUPABASE_URL; + const SUPABASE_SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY; + + if (!SUPABASE_URL || !SUPABASE_SERVICE_ROLE_KEY) { + const missing = [ + ...(!SUPABASE_URL ? ['SUPABASE_URL'] : []), + ...(!SUPABASE_SERVICE_ROLE_KEY ? ['SUPABASE_SERVICE_ROLE_KEY'] : []), + ]; + const message = `Missing Supabase environment variable(s): ${missing.join(', ')}. Connect Supabase in Lovable Cloud.`; + console.error(`[Supabase] ${message}`); + throw new Error(message); + } + + return createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, { + global: { + fetch: createSupabaseFetch(SUPABASE_SERVICE_ROLE_KEY), + }, + auth: { + storage: undefined, + persistSession: false, + autoRefreshToken: false, + } + }); +} + +let _supabaseAdmin: ReturnType | undefined; + +// Server-side Supabase client with service role - bypasses RLS +// SECURITY: Only use this for trusted server-side operations, never expose to client code +// Load inside server handlers: const { supabaseAdmin } = await import("@/integrations/supabase/client.server"); +// Top-level import is safe only in other .server.ts modules - route files and *.functions.ts ship to the client bundle. +export const supabaseAdmin = new Proxy({} as ReturnType, { + get(_, prop, receiver) { + if (!_supabaseAdmin) _supabaseAdmin = createSupabaseAdminClient(); + return Reflect.get(_supabaseAdmin, prop, receiver); + }, +}); diff --git a/src/integrations/supabase/client.ts b/src/integrations/supabase/client.ts new file mode 100644 index 0000000..8bd3a40 --- /dev/null +++ b/src/integrations/supabase/client.ts @@ -0,0 +1,68 @@ +// This file is automatically generated. Do not edit it directly. +import { createClient } from '@supabase/supabase-js'; +import type { Database } from './types'; + +function isNewSupabaseApiKey(value: string): boolean { + return value.startsWith('sb_publishable_') || value.startsWith('sb_secret_'); +} + +function createSupabaseFetch(supabaseKey: string): typeof fetch { + return (input, init) => { + const headers = new Headers( + typeof Request !== 'undefined' && input instanceof Request ? input.headers : undefined, + ); + + if (init?.headers) { + new Headers(init.headers).forEach((value, key) => headers.set(key, value)); + } + + // New Supabase API keys are opaque strings, not bearer JWTs. + if (isNewSupabaseApiKey(supabaseKey) && headers.get('Authorization') === `Bearer ${supabaseKey}`) { + headers.delete('Authorization'); + } + + headers.set('apikey', supabaseKey); + return fetch(input, { ...init, headers }); + }; +} + + +function createSupabaseClient() { + // Use import.meta.env for client-side (Vite build-time replacement) + // Fall back to process.env for SSR (server-side rendering) + const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL || process.env.SUPABASE_URL; + const SUPABASE_PUBLISHABLE_KEY = import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY || process.env.SUPABASE_PUBLISHABLE_KEY; + + if (!SUPABASE_URL || !SUPABASE_PUBLISHABLE_KEY) { + const missing = [ + ...(!SUPABASE_URL ? ['SUPABASE_URL'] : []), + ...(!SUPABASE_PUBLISHABLE_KEY ? ['SUPABASE_PUBLISHABLE_KEY'] : []), + ]; + const message = `Missing Supabase environment variable(s): ${missing.join(', ')}. Connect Supabase in Lovable Cloud.`; + console.error(`[Supabase] ${message}`); + throw new Error(message); + } + + return createClient(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, { + global: { + fetch: createSupabaseFetch(SUPABASE_PUBLISHABLE_KEY), + }, + auth: { + storage: typeof window !== 'undefined' ? localStorage : undefined, + persistSession: true, + autoRefreshToken: true, + } + }); +} + +let _supabase: ReturnType | undefined; + +// Import the supabase client like this: +// import { supabase } from "@/integrations/supabase/client"; +export const supabase = new Proxy({} as ReturnType, { + get(_, prop, receiver) { + if (!_supabase) _supabase = createSupabaseClient(); + return Reflect.get(_supabase, prop, receiver); + }, +}); + diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts new file mode 100644 index 0000000..ba7e5bb --- /dev/null +++ b/src/integrations/supabase/types.ts @@ -0,0 +1,155 @@ +export type Json = + | string + | number + | boolean + | null + | { [key: string]: Json | undefined } + | Json[] + +export type Database = { + // Allows to automatically instantiate createClient with right options + // instead of createClient(URL, KEY) + __InternalSupabase: { + PostgrestVersion: "14.5" + } + public: { + Tables: { + [_ in never]: never + } + Views: { + [_ in never]: never + } + Functions: { + [_ in never]: never + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } +} + +type DatabaseWithoutInternals = Omit + +type DefaultSchema = DatabaseWithoutInternals[Extract] + +export type Tables< + DefaultSchemaTableNameOrOptions extends + | keyof (DefaultSchema["Tables"] & DefaultSchema["Views"]) + | { schema: keyof DatabaseWithoutInternals }, + TableName extends DefaultSchemaTableNameOrOptions extends { + schema: keyof DatabaseWithoutInternals + } + ? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] & + DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"]) + : never = never, +> = DefaultSchemaTableNameOrOptions extends { + schema: keyof DatabaseWithoutInternals +} + ? (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] & + DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends { + Row: infer R + } + ? R + : never + : DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] & + DefaultSchema["Views"]) + ? (DefaultSchema["Tables"] & + DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends { + Row: infer R + } + ? R + : never + : never + +export type TablesInsert< + DefaultSchemaTableNameOrOptions extends + | keyof DefaultSchema["Tables"] + | { schema: keyof DatabaseWithoutInternals }, + TableName extends DefaultSchemaTableNameOrOptions extends { + schema: keyof DatabaseWithoutInternals + } + ? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] + : never = never, +> = DefaultSchemaTableNameOrOptions extends { + schema: keyof DatabaseWithoutInternals +} + ? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends { + Insert: infer I + } + ? I + : never + : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"] + ? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends { + Insert: infer I + } + ? I + : never + : never + +export type TablesUpdate< + DefaultSchemaTableNameOrOptions extends + | keyof DefaultSchema["Tables"] + | { schema: keyof DatabaseWithoutInternals }, + TableName extends DefaultSchemaTableNameOrOptions extends { + schema: keyof DatabaseWithoutInternals + } + ? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] + : never = never, +> = DefaultSchemaTableNameOrOptions extends { + schema: keyof DatabaseWithoutInternals +} + ? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends { + Update: infer U + } + ? U + : never + : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"] + ? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends { + Update: infer U + } + ? U + : never + : never + +export type Enums< + DefaultSchemaEnumNameOrOptions extends + | keyof DefaultSchema["Enums"] + | { schema: keyof DatabaseWithoutInternals }, + EnumName extends DefaultSchemaEnumNameOrOptions extends { + schema: keyof DatabaseWithoutInternals + } + ? keyof DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"] + : never = never, +> = DefaultSchemaEnumNameOrOptions extends { + schema: keyof DatabaseWithoutInternals +} + ? DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName] + : DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"] + ? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions] + : never + +export type CompositeTypes< + PublicCompositeTypeNameOrOptions extends + | keyof DefaultSchema["CompositeTypes"] + | { schema: keyof DatabaseWithoutInternals }, + CompositeTypeName extends PublicCompositeTypeNameOrOptions extends { + schema: keyof DatabaseWithoutInternals + } + ? keyof DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"] + : never = never, +> = PublicCompositeTypeNameOrOptions extends { + schema: keyof DatabaseWithoutInternals +} + ? DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName] + : PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"] + ? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions] + : never + +export const Constants = { + public: { + Enums: {}, + }, +} as const diff --git a/src/start.ts b/src/start.ts index d6152ee..2086427 100644 --- a/src/start.ts +++ b/src/start.ts @@ -1,6 +1,7 @@ import { createStart, createMiddleware } from "@tanstack/react-start"; import { renderErrorPage } from "./lib/error-page"; +import { attachSupabaseAuth } from "@/integrations/supabase/auth-attacher"; const errorMiddleware = createMiddleware().server(async ({ next }) => { try { @@ -18,5 +19,6 @@ const errorMiddleware = createMiddleware().server(async ({ next }) => { }); export const startInstance = createStart(() => ({ + functionMiddleware: [attachSupabaseAuth], requestMiddleware: [errorMiddleware], })); diff --git a/supabase/config.toml b/supabase/config.toml new file mode 100644 index 0000000..24c1857 --- /dev/null +++ b/supabase/config.toml @@ -0,0 +1 @@ +project_id = "fvjpfhfgkhgtwtzrgcgx" \ No newline at end of file