diff --git a/.env b/.env new file mode 100644 index 0000000..14fb927 --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +SUPABASE_PUBLISHABLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNtc2FybWdvaXJsY2VkbXF2ZGdjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzY0MzczNjAsImV4cCI6MjA5MjAxMzM2MH0.AjMSQOn3BtzV-fJ2x9SWoM2ozjLyPD6g3dmTSeCT9II" +SUPABASE_URL="https://smsarmgoirlcedmqvdgc.supabase.co" +VITE_SUPABASE_PROJECT_ID="smsarmgoirlcedmqvdgc" +VITE_SUPABASE_PUBLISHABLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNtc2FybWdvaXJsY2VkbXF2ZGdjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzY0MzczNjAsImV4cCI6MjA5MjAxMzM2MH0.AjMSQOn3BtzV-fJ2x9SWoM2ozjLyPD6g3dmTSeCT9II" +VITE_SUPABASE_URL="https://smsarmgoirlcedmqvdgc.supabase.co" diff --git a/bun.lockb b/bun.lockb index 6a506b6..4d5c249 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index ad02dd6..c5445e7 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-tooltip": "^1.2.8", + "@supabase/supabase-js": "^2.103.3", "@tailwindcss/vite": "^4.2.1", "@tanstack/react-query": "^5.99.0", "@tanstack/react-router": "^1.168.0", diff --git a/src/integrations/supabase/auth-middleware.ts b/src/integrations/supabase/auth-middleware.ts new file mode 100644 index 0000000..4e8f69d --- /dev/null +++ b/src/integrations/supabase/auth-middleware.ts @@ -0,0 +1,77 @@ +// 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' + + + +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) { + throw new Response( + 'Missing Supabase environment variables. Ensure SUPABASE_URL and SUPABASE_PUBLISHABLE_KEY are set.', + { status: 500 } + ); + } + + const request = getRequest(); + + if (!request?.headers) { + throw new Response('Unauthorized: No request headers available', { status: 401 }); + } + + const authHeader = request.headers.get('authorization'); + + if (!authHeader) { + throw new Response('Unauthorized: No authorization header provided', { status: 401 }); + } + + if (!authHeader.startsWith('Bearer ')) { + throw new Response('Unauthorized: Only Bearer tokens are supported', { status: 401 }); + } + + const token = authHeader.replace('Bearer ', ''); + if (!token) { + throw new Response('Unauthorized: No token provided', { status: 401 }); + } + + const supabase = createClient( + SUPABASE_URL!, + SUPABASE_PUBLISHABLE_KEY!, + { + global: { + 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 Response('Unauthorized: Invalid token', { status: 401 }); + } + + if (!data.claims.sub) { + throw new Response('Unauthorized: No user ID found in token', { status: 401 }); + } + + 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..bfe7546 --- /dev/null +++ b/src/integrations/supabase/client.server.ts @@ -0,0 +1,37 @@ +// 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 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) { + throw new Error( + 'Missing Supabase server environment variables. Ensure SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are set.' + ); + } + + return createClient(SUPABASE_URL, 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 +// Import like: import { supabaseAdmin } from "@/integrations/supabase/client.server"; +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..c81d68f --- /dev/null +++ b/src/integrations/supabase/client.ts @@ -0,0 +1,36 @@ +// This file is automatically generated. Do not edit it directly. +import { createClient } from '@supabase/supabase-js'; +import type { Database } from './types'; + +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) { + throw new Error( + 'Missing Supabase environment variables. Ensure SUPABASE_URL and SUPABASE_PUBLISHABLE_KEY (or VITE_ prefixed versions) are set in your .env file.' + ); + } + + return createClient(SUPABASE_URL, 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/supabase/config.toml b/supabase/config.toml new file mode 100644 index 0000000..b925aa7 --- /dev/null +++ b/supabase/config.toml @@ -0,0 +1 @@ +project_id = "smsarmgoirlcedmqvdgc" \ No newline at end of file