diff --git a/scripts/supabase/generate_types b/scripts/supabase/generate_types
index 4566356..eb5c7de 100755
--- a/scripts/supabase/generate_types
+++ b/scripts/supabase/generate_types
@@ -109,8 +109,8 @@ sudo -E npx supabase gen types typescript \
# Check if the command was successful
if [ $? -eq 0 ] && [ -s "$TEMP_FILE" ] && ! grep -q "Error" "$TEMP_FILE"; then
# Move the temp file to the final destination
- mv "$TEMP_FILE" "$OUTPUT_DIR/types.ts"
- echo -e "${GREEN}✓ TypeScript types successfully generated at $OUTPUT_DIR/types.ts${NC}"
+ mv "$TEMP_FILE" "$OUTPUT_DIR/database.types.ts"
+ echo -e "${GREEN}✓ TypeScript types successfully generated at $OUTPUT_DIR/database.types.ts${NC}"
# Show the first few lines to confirm it looks right
echo -e "${YELLOW}Preview of generated types:${NC}"
@@ -130,4 +130,4 @@ unset DB_URL
echo -e "${GREEN}${BOLD}Type generation complete!${NC}"
echo -e "You can now use these types in your Next.js application."
-echo -e "Import them with: ${BLUE}import { Database } from '@/utils/supabase/types'${NC}"
+echo -e "Import them with: ${BLUE}import { Database } from '@/utils/supabase/database.types'${NC}"
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 2d79c7f..2c36ac1 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -2,6 +2,9 @@ import '@/styles/globals.css';
import { type Metadata } from 'next';
import { Geist } from 'next/font/google';
+import {
+ ReactQueryClientProvider,
+} from '@/lib/hooks/context';
export const metadata: Metadata = {
title: 'Create T3 App',
@@ -18,8 +21,10 @@ export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
-
-
{children}
-
+
+
+ {children}
+
+
);
}
diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx
index f545f66..a22ae51 100644
--- a/src/components/ui/sidebar.tsx
+++ b/src/components/ui/sidebar.tsx
@@ -5,7 +5,7 @@ import { Slot } from '@radix-ui/react-slot';
import { cva, VariantProps } from 'class-variance-authority';
import { PanelLeftIcon } from 'lucide-react';
-import { useIsMobile } from '@/hooks/use-mobile';
+import { useIsMobile } from '@/lib/hooks/context/use-mobile';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
diff --git a/src/instrumentation-client.ts b/src/instrumentation-client.ts
new file mode 100644
index 0000000..7f44f8d
--- /dev/null
+++ b/src/instrumentation-client.ts
@@ -0,0 +1,36 @@
+// This file configures the initialization of Sentry on the client.
+// The added config here will be used whenever a users loads a page in their browser.
+// https://docs.sentry.io/platforms/javascript/guides/nextjs/
+import * as Sentry from '@sentry/nextjs';
+
+Sentry.init({
+ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN!,
+
+ // Adds request headers and IP for users, for more info visit:
+ // https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/options/#sendDefaultPii
+ sendDefaultPii: true,
+
+ // Set tracesSampleRate to 1.0 to capture 100%
+ // of transactions for tracing.
+ // We recommend adjusting this value in production
+ // Learn more at
+ // https://docs.sentry.io/platforms/javascript/configuration/options/#traces-sample-rate
+ tracesSampleRate: 1.0,
+ // Replay may only be enabled for the client-side
+ integrations: [Sentry.replayIntegration()],
+
+ // Capture Replay for 10% of all sessions,
+ // plus for 100% of sessions with an error
+ // Learn more at
+ // https://docs.sentry.io/platforms/javascript/session-replay/configuration/#general-integration-configuration
+ replaysSessionSampleRate: 0.1,
+ replaysOnErrorSampleRate: 1.0,
+
+ // Note: if you want to override the automatic release value, do not set a
+ // `release` value here - use the environment variable `SENTRY_RELEASE`, so
+ // that it will also get attached to your source maps
+});
+
+// This export will instrument router navigations, and is only relevant if you enable tracing.
+// `captureRouterTransitionStart` is available from SDK version 9.12.0 onwards
+export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
diff --git a/src/instrumentation.ts b/src/instrumentation.ts
new file mode 100644
index 0000000..b77a16f
--- /dev/null
+++ b/src/instrumentation.ts
@@ -0,0 +1,10 @@
+import * as Sentry from '@sentry/nextjs';
+import type { Instrumentation } from 'next';
+
+export const register = async () => {
+ await import('../sentry.server.config');
+};
+
+export const onRequestError: Instrumentation.onRequestError = (...args) => {
+ Sentry.captureRequestError(...args);
+};
diff --git a/src/lib/hooks/context/index.tsx b/src/lib/hooks/context/index.tsx
new file mode 100644
index 0000000..080964a
--- /dev/null
+++ b/src/lib/hooks/context/index.tsx
@@ -0,0 +1,4 @@
+export { useIsMobile } from './use-mobile';
+export { ReactQueryClientProvider } from './use-query';
+export { ThemeProvider, ThemeToggle } from './use-theme';
+export { TVModeProvider, useTVMode, TVToggle } from './use-tv-mode';
diff --git a/src/lib/hooks/context/use-auth.tsx b/src/lib/hooks/context/use-auth.tsx
new file mode 100644
index 0000000..32e7362
--- /dev/null
+++ b/src/lib/hooks/context/use-auth.tsx
@@ -0,0 +1,11 @@
+'use client';
+
+import React, {
+ type ReactNode,
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useRef,
+ useState,
+} from 'react';
diff --git a/src/hooks/use-mobile.ts b/src/lib/hooks/context/use-mobile.ts
similarity index 90%
rename from src/hooks/use-mobile.ts
rename to src/lib/hooks/context/use-mobile.ts
index 821f8ff..4056236 100644
--- a/src/hooks/use-mobile.ts
+++ b/src/lib/hooks/context/use-mobile.ts
@@ -2,7 +2,7 @@ import * as React from 'react';
const MOBILE_BREAKPOINT = 768;
-export function useIsMobile() {
+const useIsMobile = () => {
const [isMobile, setIsMobile] = React.useState(
undefined,
);
@@ -18,4 +18,6 @@ export function useIsMobile() {
}, []);
return !!isMobile;
-}
+};
+
+export { useIsMobile };
diff --git a/src/lib/hooks/context/use-query.tsx b/src/lib/hooks/context/use-query.tsx
new file mode 100644
index 0000000..94659a4
--- /dev/null
+++ b/src/lib/hooks/context/use-query.tsx
@@ -0,0 +1,22 @@
+'use client'
+
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { useState } from 'react'
+
+const ReactQueryClientProvider = ({ children }: { children: React.ReactNode }) => {
+ const [queryClient] = useState(
+ () =>
+ new QueryClient({
+ defaultOptions: {
+ queries: {
+ // With SSR, we usually want to set some default staleTime
+ // above 0 to avoid refetching immediately on the client
+ staleTime: 60 * 1000,
+ },
+ },
+ })
+ )
+ return {children}
+};
+
+export { ReactQueryClientProvider };
diff --git a/src/lib/hooks/context/use-theme.tsx b/src/lib/hooks/context/use-theme.tsx
new file mode 100644
index 0000000..71c808a
--- /dev/null
+++ b/src/lib/hooks/context/use-theme.tsx
@@ -0,0 +1,69 @@
+'use client';
+import * as React from 'react';
+import { ThemeProvider as NextThemesProvider } from 'next-themes';
+import { Moon, Sun } from 'lucide-react';
+import { useTheme } from 'next-themes';
+import { Button } from '@/components/ui';
+
+const ThemeProvider = ({
+ children,
+ ...props
+}: React.ComponentProps) => {
+ const [mounted, setMounted] = React.useState(false);
+
+ React.useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ if (!mounted) return null;
+
+ return {children};
+};
+
+type ThemeToggleProps = React.ButtonHTMLAttributes & {
+ size?: number;
+};
+
+const ThemeToggle = ({ size = 1, ...props }: ThemeToggleProps) => {
+ const { setTheme, resolvedTheme } = useTheme();
+ const [mounted, setMounted] = React.useState(false);
+
+ React.useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ if (!mounted) {
+ return (
+
+ );
+ }
+
+ const toggleTheme = () => {
+ if (resolvedTheme === 'dark') setTheme('light');
+ else setTheme('dark');
+ };
+
+ return (
+
+ );
+};
+
+export { ThemeProvider, ThemeToggle };
diff --git a/src/lib/hooks/context/use-tv-mode.tsx b/src/lib/hooks/context/use-tv-mode.tsx
new file mode 100644
index 0000000..dce7512
--- /dev/null
+++ b/src/lib/hooks/context/use-tv-mode.tsx
@@ -0,0 +1,78 @@
+'use client';
+import Image from 'next/image';
+import React, { createContext, useContext, useState } from 'react';
+import type { ReactNode } from 'react';
+import { Button, type buttonVariants } from '@/components/ui';
+import { type ComponentProps } from 'react';
+import { type VariantProps } from 'class-variance-authority';
+
+type TVModeContextProps = {
+ tvMode: boolean;
+ toggleTVMode: () => void;
+};
+
+type TVToggleProps = {
+ className?: ComponentProps<'button'>['className'];
+ buttonSize?: VariantProps['size'];
+ buttonVariant?: VariantProps['variant'];
+ imageWidth?: number;
+ imageHeight?: number;
+};
+
+const TVModeContext = createContext(undefined);
+
+const TVModeProvider = ({ children }: { children: ReactNode }) => {
+ const [tvMode, setTVMode] = useState(false);
+ const toggleTVMode = () => {
+ setTVMode((prev) => !prev);
+ };
+ return (
+
+ {children}
+
+ );
+};
+
+const useTVMode = () => {
+ const context = useContext(TVModeContext);
+ if (!context) {
+ throw new Error('useTVMode must be used within a TVModeProvider');
+ }
+ return context;
+};
+
+const TVToggle = ({
+ className = 'my-auto cursor-pointer',
+ buttonSize = 'default',
+ buttonVariant = 'link',
+ imageWidth = 25,
+ imageHeight = 25,
+}: TVToggleProps) => {
+ const { tvMode, toggleTVMode } = useTVMode();
+ return (
+
+ );
+};
+
+export { TVModeProvider, useTVMode, TVToggle };
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 2819a83..1ee4273 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -1,6 +1,20 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
-export function cn(...inputs: ClassValue[]) {
+export const cn = (...inputs: ClassValue[]) => {
return twMerge(clsx(inputs));
-}
+};
+
+export const ccn = ({
+ context,
+ className,
+ on = '',
+ off = '',
+}: {
+ context: boolean;
+ className: string;
+ on: string;
+ off: string;
+}) => {
+ return className + ' ' + (context ? on : off);
+};
diff --git a/src/middleware.ts b/src/middleware.ts
new file mode 100644
index 0000000..a2c1306
--- /dev/null
+++ b/src/middleware.ts
@@ -0,0 +1,24 @@
+import { type NextRequest } from 'next/server';
+import { updateSession } from '@/utils/supabase/middleware';
+import { banSuspiciousIPs } from '@/utils/ban-suspicious-ips';
+
+export const middleware = async (request: NextRequest) => {
+ const banResponse = banSuspiciousIPs(request);
+ if (banResponse) return banResponse;
+ return await updateSession(request);
+};
+
+export const config = {
+ matcher: [
+ /*
+ * Match all request paths except:
+ * - _next/static (static files)
+ * - _next/image (image optimization files)
+ * - favicon.ico (favicon file)
+ * - /monitoring-tunnel (Sentry monitoring)
+ * - images - .svg, .png, .jpg, .jpeg, .gif, .webp
+ * Feel free to modify this pattern to include more paths.
+ */
+ '/((?!_next/static|_next/image|favicon.ico|monitoring-tunnel|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
+ ],
+};
diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts
index d6ca9d7..7ad58a1 100644
--- a/src/server/db/schema.ts
+++ b/src/server/db/schema.ts
@@ -1,4 +1,5 @@
// https://orm.drizzle.team/docs/sql-schema-declaration
+// honestly I don't know why I would want Drizzle with Supabase but I'll keep it for now.
import { sql } from 'drizzle-orm';
import {
@@ -9,11 +10,13 @@ import {
text,
timestamp,
uuid,
+ varchar,
} from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
- id: uuid('id').primaryKey(),
- name: text('name').notNull(),
+ id: serial('id').primaryKey(),
+ fullName: text('full_name'),
+ phone: varchar('phone', { length: 256 }),
status: text('status').notNull(),
updatedAt: timestamp('updatedAt')
.default(sql`CURRENT_TIMESTAMP`)
diff --git a/src/utils/ban-suspicious-ips.ts b/src/utils/ban-suspicious-ips.ts
new file mode 100644
index 0000000..7216c4d
--- /dev/null
+++ b/src/utils/ban-suspicious-ips.ts
@@ -0,0 +1,102 @@
+import { type NextRequest, NextResponse } from 'next/server';
+
+// In-memory store for tracking IPs (use Redis in production)
+const ipAttempts = new Map();
+const bannedIPs = new Set();
+
+// Ban Arctic Wolf Explicitly
+bannedIPs.add('::ffff:10.0.1.49');
+
+// Suspicious patterns that indicate malicious activity
+const MALICIOUS_PATTERNS = [
+ /web-inf/i,
+ /\.jsp/i,
+ /\.php/i,
+ /puttest/i,
+ /WEB-INF/i,
+ /\.xml$/i,
+ /perl/i,
+ /xampp/i,
+ /phpwebgallery/i,
+ /FileManager/i,
+ /standalonemanager/i,
+ /h2console/i,
+ /WebAdmin/i,
+ /login_form\.php/i,
+ /%2e/i,
+ /%u002e/i,
+ /\.%00/i,
+ /\.\./,
+ /lcgi/i,
+];
+
+// Suspicious HTTP methods
+const SUSPICIOUS_METHODS = ['TRACE', 'PUT', 'DELETE', 'PATCH'];
+
+const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute
+const MAX_ATTEMPTS = 10; // Max suspicious requests per window
+const BAN_DURATION = 30 * 60 * 1000; // 30 minutes
+
+const getClientIP = (request: NextRequest): string => {
+ const forwarded = request.headers.get('x-forwarded-for');
+ const realIP = request.headers.get('x-real-ip');
+ const cfConnectingIP = request.headers.get('cf-connecting-ip');
+
+ if (forwarded) return (forwarded.split(',')[0] ?? '').trim();
+ if (realIP) return realIP;
+ if (cfConnectingIP) return cfConnectingIP;
+ return request.headers.get('host') ?? 'unknown';
+};
+
+const isPathSuspicious = (pathname: string): boolean => {
+ return MALICIOUS_PATTERNS.some((pattern) => pattern.test(pathname));
+};
+
+const isMethodSuspicious = (method: string): boolean => {
+ return SUSPICIOUS_METHODS.includes(method);
+};
+
+const updateIPAttempts = (ip: string): boolean => {
+ const now = Date.now();
+ const attempts = ipAttempts.get(ip);
+
+ if (!attempts || now - attempts.lastAttempt > RATE_LIMIT_WINDOW) {
+ ipAttempts.set(ip, { count: 1, lastAttempt: now });
+ return false;
+ }
+ attempts.count++;
+ attempts.lastAttempt = now;
+ if (attempts.count > MAX_ATTEMPTS) {
+ bannedIPs.add(ip);
+ ipAttempts.delete(ip);
+ // Auto-unban after duration (in production, use a proper scheduler)
+ setTimeout(() => {
+ bannedIPs.delete(ip);
+ }, BAN_DURATION);
+ return true;
+ }
+ return false;
+};
+
+export const banSuspiciousIPs = (request: NextRequest): NextResponse | null => {
+ const { pathname } = request.nextUrl;
+ const method = request.method;
+ const ip = getClientIP(request);
+
+ if (bannedIPs.has(ip)) return new NextResponse('Access denied.', { status: 403 });
+
+ const isSuspiciousPath = isPathSuspicious(pathname);
+ const isSuspiciousMethod = isMethodSuspicious(method);
+
+ if (isSuspiciousPath || isSuspiciousMethod) {
+ const shouldBan = updateIPAttempts(ip);
+ if (shouldBan) {
+ console.log(`🔨 IP ${ip} has been banned for suspicious activity`);
+ return new NextResponse('Access denied - IP banned. Please fuck off.', {
+ status: 403,
+ });
+ }
+ return new NextResponse('Not Found', { status: 404 });
+ }
+ return null;
+};
diff --git a/src/utils/supabase/client.ts b/src/utils/supabase/client.ts
new file mode 100644
index 0000000..6e38d28
--- /dev/null
+++ b/src/utils/supabase/client.ts
@@ -0,0 +1,23 @@
+'use client';
+
+import { createBrowserClient } from '@supabase/ssr';
+import type { Database } from '@/utils/supabase/database.types';
+import type { SupabaseClient } from '@/utils/supabase/types';
+import { useMemo } from 'react';
+
+let client: SupabaseClient | undefined;
+
+const getSupbaseClient = () => {
+ if (client) return client;
+ client = createBrowserClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ );
+ return client;
+};
+
+const useSupabaseClient = () => {
+ return useMemo(getSupbaseClient, []);
+};
+
+export { useSupabaseClient };
diff --git a/src/utils/supabase/database.types.ts b/src/utils/supabase/database.types.ts
new file mode 100644
index 0000000..c114c75
--- /dev/null
+++ b/src/utils/supabase/database.types.ts
@@ -0,0 +1,203 @@
+export type Json =
+ | string
+ | number
+ | boolean
+ | null
+ | { [key: string]: Json | undefined }
+ | Json[];
+
+export type Database = {
+ public: {
+ Tables: {
+ profiles: {
+ Row: {
+ avatar_url: string | null;
+ email: string | null;
+ full_name: string | null;
+ id: string;
+ provider: string | null;
+ updated_at: string | null;
+ };
+ Insert: {
+ avatar_url?: string | null;
+ email?: string | null;
+ full_name?: string | null;
+ id: string;
+ provider?: string | null;
+ updated_at?: string | null;
+ };
+ Update: {
+ avatar_url?: string | null;
+ email?: string | null;
+ full_name?: string | null;
+ id?: string;
+ provider?: string | null;
+ updated_at?: string | null;
+ };
+ Relationships: [];
+ };
+ statuses: {
+ Row: {
+ created_at: string;
+ id: string;
+ status: string;
+ updated_by_id: string | null;
+ user_id: string;
+ };
+ Insert: {
+ created_at?: string;
+ id?: string;
+ status: string;
+ updated_by_id?: string | null;
+ user_id: string;
+ };
+ Update: {
+ created_at?: string;
+ id?: string;
+ status?: string;
+ updated_by_id?: string | null;
+ user_id?: string;
+ };
+ Relationships: [
+ {
+ foreignKeyName: 'statuses_updated_by_id_fkey';
+ columns: ['updated_by_id'];
+ isOneToOne: false;
+ referencedRelation: 'profiles';
+ referencedColumns: ['id'];
+ },
+ {
+ foreignKeyName: 'statuses_user_id_fkey';
+ columns: ['user_id'];
+ isOneToOne: false;
+ referencedRelation: 'profiles';
+ referencedColumns: ['id'];
+ },
+ ];
+ };
+ };
+ Views: {
+ [_ in never]: never;
+ };
+ Functions: {
+ [_ in never]: never;
+ };
+ Enums: {
+ [_ in never]: never;
+ };
+ CompositeTypes: {
+ [_ in never]: never;
+ };
+ };
+};
+
+type DefaultSchema = Database[Extract];
+
+export type Tables<
+ DefaultSchemaTableNameOrOptions extends
+ | keyof (DefaultSchema['Tables'] & DefaultSchema['Views'])
+ | { schema: keyof Database },
+ TableName extends DefaultSchemaTableNameOrOptions extends {
+ schema: keyof Database;
+ }
+ ? keyof (Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] &
+ Database[DefaultSchemaTableNameOrOptions['schema']]['Views'])
+ : never = never,
+> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
+ ? (Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] &
+ Database[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 Database },
+ TableName extends DefaultSchemaTableNameOrOptions extends {
+ schema: keyof Database;
+ }
+ ? keyof Database[DefaultSchemaTableNameOrOptions['schema']]['Tables']
+ : never = never,
+> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
+ ? Database[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 Database },
+ TableName extends DefaultSchemaTableNameOrOptions extends {
+ schema: keyof Database;
+ }
+ ? keyof Database[DefaultSchemaTableNameOrOptions['schema']]['Tables']
+ : never = never,
+> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
+ ? Database[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 Database },
+ EnumName extends DefaultSchemaEnumNameOrOptions extends {
+ schema: keyof Database;
+ }
+ ? keyof Database[DefaultSchemaEnumNameOrOptions['schema']]['Enums']
+ : never = never,
+> = DefaultSchemaEnumNameOrOptions extends { schema: keyof Database }
+ ? Database[DefaultSchemaEnumNameOrOptions['schema']]['Enums'][EnumName]
+ : DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema['Enums']
+ ? DefaultSchema['Enums'][DefaultSchemaEnumNameOrOptions]
+ : never;
+
+export type CompositeTypes<
+ PublicCompositeTypeNameOrOptions extends
+ | keyof DefaultSchema['CompositeTypes']
+ | { schema: keyof Database },
+ CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
+ schema: keyof Database;
+ }
+ ? keyof Database[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes']
+ : never = never,
+> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database }
+ ? Database[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'][CompositeTypeName]
+ : PublicCompositeTypeNameOrOptions extends keyof DefaultSchema['CompositeTypes']
+ ? DefaultSchema['CompositeTypes'][PublicCompositeTypeNameOrOptions]
+ : never;
+
+export const Constants = {
+ public: {
+ Enums: {},
+ },
+} as const;
diff --git a/src/utils/supabase/index.ts b/src/utils/supabase/index.ts
new file mode 100644
index 0000000..44bc7a5
--- /dev/null
+++ b/src/utils/supabase/index.ts
@@ -0,0 +1,5 @@
+export { useSupabaseClient } from './client';
+export { updateSession } from './middleware';
+export { useSupabaseServer } from './server';
+export type { Database } from './database.types';
+export type * from './types';
diff --git a/src/utils/supabase/middleware.ts b/src/utils/supabase/middleware.ts
new file mode 100644
index 0000000..e82fec5
--- /dev/null
+++ b/src/utils/supabase/middleware.ts
@@ -0,0 +1,56 @@
+import { createServerClient } from '@supabase/ssr';
+import { type NextRequest, NextResponse } from 'next/server';
+import type { Database } from '@/utils/supabase/database.types';
+
+export const updateSession = async (
+ request: NextRequest,
+): Promise => {
+ try {
+ // Create an unmodified response
+ let response = NextResponse.next({
+ request: {
+ headers: request.headers,
+ },
+ });
+
+ const supabase = createServerClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ {
+ cookies: {
+ getAll() {
+ return request.cookies.getAll();
+ },
+ setAll(cookiesToSet) {
+ cookiesToSet.forEach(({ name, value }) =>
+ request.cookies.set(name, value),
+ );
+ response = NextResponse.next({
+ request,
+ });
+ cookiesToSet.forEach(({ name, value, options }) =>
+ response.cookies.set(name, value, options),
+ );
+ },
+ },
+ },
+ );
+
+ // This will refresh session if expired - required for Server Components
+ // https://supabase.com/docs/guides/auth/server-side/nextjs
+ const user = await supabase.auth.getUser();
+
+ // protected routes
+ if (request.nextUrl.pathname.startsWith('/reset-password') && user.error) {
+ return NextResponse.redirect(new URL('/sign-in', request.url));
+ }
+
+ return response;
+ } catch (e) {
+ return NextResponse.next({
+ request: {
+ headers: request.headers,
+ },
+ });
+ }
+};
diff --git a/src/utils/supabase/server.ts b/src/utils/supabase/server.ts
new file mode 100644
index 0000000..6b9d69c
--- /dev/null
+++ b/src/utils/supabase/server.ts
@@ -0,0 +1,30 @@
+'use server';
+
+import 'server-only';
+import { createServerClient } from '@supabase/ssr';
+import type { Database } from '@/utils/supabase/database.types';
+import { cookies } from 'next/headers';
+
+export const useSupabaseServer = async () => {
+ const cookieStore = await cookies();
+ return createServerClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ {
+ cookies: {
+ getAll() {
+ return cookieStore.getAll();
+ },
+ setAll(cookiesToSet) {
+ try {
+ cookiesToSet.forEach(({ name, value, options }) => {
+ cookieStore.set(name, value, options);
+ });
+ } catch (error) {
+ console.error(`Error setting cookies: ${error as string}`);
+ }
+ },
+ },
+ },
+ );
+};
diff --git a/src/utils/supabase/types.ts b/src/utils/supabase/types.ts
new file mode 100644
index 0000000..5949a7e
--- /dev/null
+++ b/src/utils/supabase/types.ts
@@ -0,0 +1,30 @@
+import type { Database } from '@/utils/supabase/database.types';
+import type { SupabaseClient as SBClient } from '@supabase/supabase-js'
+
+export type SupabaseClient = SBClient;
+
+export type { User } from '@supabase/supabase-js';
+
+// Table row types
+export type Profile = Database['public']['Tables']['profiles']['Row'];
+export type Status = Database['public']['Tables']['statuses']['Row'];
+
+// Insert types
+export type ProfileInsert = Database['public']['Tables']['profiles']['Insert'];
+export type StatusInsert = Database['public']['Tables']['statuses']['Insert'];
+
+// Update types
+export type ProfileUpdate = Database['public']['Tables']['profiles']['Update'];
+export type StatusUpdate = Database['public']['Tables']['statuses']['Update'];
+
+// Generic helper to get any table's row type
+export type TableRow =
+ Database['public']['Tables'][T]['Row'];
+
+// Generic helper to get any table's insert type
+export type TableInsert =
+ Database['public']['Tables'][T]['Insert'];
+
+// Generic helper to get any table's update type
+export type TableUpdate =
+ Database['public']['Tables'][T]['Update'];