Making progress on rewrite. Looking into supabase cache helpers.
This commit is contained in:
@ -109,8 +109,8 @@ sudo -E npx supabase gen types typescript \
|
|||||||
# Check if the command was successful
|
# Check if the command was successful
|
||||||
if [ $? -eq 0 ] && [ -s "$TEMP_FILE" ] && ! grep -q "Error" "$TEMP_FILE"; then
|
if [ $? -eq 0 ] && [ -s "$TEMP_FILE" ] && ! grep -q "Error" "$TEMP_FILE"; then
|
||||||
# Move the temp file to the final destination
|
# Move the temp file to the final destination
|
||||||
mv "$TEMP_FILE" "$OUTPUT_DIR/types.ts"
|
mv "$TEMP_FILE" "$OUTPUT_DIR/database.types.ts"
|
||||||
echo -e "${GREEN}✓ TypeScript types successfully generated at $OUTPUT_DIR/types.ts${NC}"
|
echo -e "${GREEN}✓ TypeScript types successfully generated at $OUTPUT_DIR/database.types.ts${NC}"
|
||||||
|
|
||||||
# Show the first few lines to confirm it looks right
|
# Show the first few lines to confirm it looks right
|
||||||
echo -e "${YELLOW}Preview of generated types:${NC}"
|
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 "${GREEN}${BOLD}Type generation complete!${NC}"
|
||||||
echo -e "You can now use these types in your Next.js application."
|
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}"
|
||||||
|
@ -2,6 +2,9 @@ import '@/styles/globals.css';
|
|||||||
|
|
||||||
import { type Metadata } from 'next';
|
import { type Metadata } from 'next';
|
||||||
import { Geist } from 'next/font/google';
|
import { Geist } from 'next/font/google';
|
||||||
|
import {
|
||||||
|
ReactQueryClientProvider,
|
||||||
|
} from '@/lib/hooks/context';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Create T3 App',
|
title: 'Create T3 App',
|
||||||
@ -18,8 +21,10 @@ export default function RootLayout({
|
|||||||
children,
|
children,
|
||||||
}: Readonly<{ children: React.ReactNode }>) {
|
}: Readonly<{ children: React.ReactNode }>) {
|
||||||
return (
|
return (
|
||||||
<html lang='en' className={`${geist.variable}`}>
|
<ReactQueryClientProvider>
|
||||||
<body>{children}</body>
|
<html lang='en' className={`${geist.variable}`}>
|
||||||
</html>
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
</ReactQueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import { Slot } from '@radix-ui/react-slot';
|
|||||||
import { cva, VariantProps } from 'class-variance-authority';
|
import { cva, VariantProps } from 'class-variance-authority';
|
||||||
import { PanelLeftIcon } from 'lucide-react';
|
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 { cn } from '@/lib/utils';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
|
36
src/instrumentation-client.ts
Normal file
36
src/instrumentation-client.ts
Normal file
@ -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;
|
10
src/instrumentation.ts
Normal file
10
src/instrumentation.ts
Normal file
@ -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);
|
||||||
|
};
|
4
src/lib/hooks/context/index.tsx
Normal file
4
src/lib/hooks/context/index.tsx
Normal file
@ -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';
|
11
src/lib/hooks/context/use-auth.tsx
Normal file
11
src/lib/hooks/context/use-auth.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, {
|
||||||
|
type ReactNode,
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
@ -2,7 +2,7 @@ import * as React from 'react';
|
|||||||
|
|
||||||
const MOBILE_BREAKPOINT = 768;
|
const MOBILE_BREAKPOINT = 768;
|
||||||
|
|
||||||
export function useIsMobile() {
|
const useIsMobile = () => {
|
||||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
@ -18,4 +18,6 @@ export function useIsMobile() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return !!isMobile;
|
return !!isMobile;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export { useIsMobile };
|
22
src/lib/hooks/context/use-query.tsx
Normal file
22
src/lib/hooks/context/use-query.tsx
Normal file
@ -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 <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ReactQueryClientProvider };
|
69
src/lib/hooks/context/use-theme.tsx
Normal file
69
src/lib/hooks/context/use-theme.tsx
Normal file
@ -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<typeof NextThemesProvider>) => {
|
||||||
|
const [mounted, setMounted] = React.useState(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!mounted) return null;
|
||||||
|
|
||||||
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ThemeToggleProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||||
|
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 (
|
||||||
|
<Button variant='outline' size='icon' {...props}>
|
||||||
|
<span style={{ height: `${size}rem`, width: `${size}rem` }} />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
if (resolvedTheme === 'dark') setTheme('light');
|
||||||
|
else setTheme('dark');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
size='icon'
|
||||||
|
className='cursor-pointer'
|
||||||
|
onClick={toggleTheme}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Sun
|
||||||
|
style={{ height: `${size}rem`, width: `${size}rem` }}
|
||||||
|
className='rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0'
|
||||||
|
/>
|
||||||
|
<Moon
|
||||||
|
style={{ height: `${size}rem`, width: `${size}rem` }}
|
||||||
|
className='absolute rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100'
|
||||||
|
/>
|
||||||
|
<span className='sr-only'>Toggle theme</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ThemeProvider, ThemeToggle };
|
78
src/lib/hooks/context/use-tv-mode.tsx
Normal file
78
src/lib/hooks/context/use-tv-mode.tsx
Normal file
@ -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<typeof buttonVariants>['size'];
|
||||||
|
buttonVariant?: VariantProps<typeof buttonVariants>['variant'];
|
||||||
|
imageWidth?: number;
|
||||||
|
imageHeight?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TVModeContext = createContext<TVModeContextProps | undefined>(undefined);
|
||||||
|
|
||||||
|
const TVModeProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const [tvMode, setTVMode] = useState(false);
|
||||||
|
const toggleTVMode = () => {
|
||||||
|
setTVMode((prev) => !prev);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<TVModeContext.Provider value={{ tvMode, toggleTVMode }}>
|
||||||
|
{children}
|
||||||
|
</TVModeContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Button
|
||||||
|
onClick={toggleTVMode}
|
||||||
|
className={className}
|
||||||
|
size={buttonSize}
|
||||||
|
variant={buttonVariant}
|
||||||
|
>
|
||||||
|
{tvMode ? (
|
||||||
|
<Image
|
||||||
|
src='/icons/tv/exit.svg'
|
||||||
|
alt='Exit TV Mode'
|
||||||
|
width={imageWidth}
|
||||||
|
height={imageHeight}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
src='/icons/tv/enter.svg'
|
||||||
|
alt='Exit TV Mode'
|
||||||
|
width={imageWidth}
|
||||||
|
height={imageHeight}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { TVModeProvider, useTVMode, TVToggle };
|
@ -1,6 +1,20 @@
|
|||||||
import { clsx, type ClassValue } from 'clsx';
|
import { clsx, type ClassValue } from 'clsx';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export const cn = (...inputs: ClassValue[]) => {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export const ccn = ({
|
||||||
|
context,
|
||||||
|
className,
|
||||||
|
on = '',
|
||||||
|
off = '',
|
||||||
|
}: {
|
||||||
|
context: boolean;
|
||||||
|
className: string;
|
||||||
|
on: string;
|
||||||
|
off: string;
|
||||||
|
}) => {
|
||||||
|
return className + ' ' + (context ? on : off);
|
||||||
|
};
|
||||||
|
24
src/middleware.ts
Normal file
24
src/middleware.ts
Normal file
@ -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)$).*)',
|
||||||
|
],
|
||||||
|
};
|
@ -1,4 +1,5 @@
|
|||||||
// https://orm.drizzle.team/docs/sql-schema-declaration
|
// 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 { sql } from 'drizzle-orm';
|
||||||
import {
|
import {
|
||||||
@ -9,11 +10,13 @@ import {
|
|||||||
text,
|
text,
|
||||||
timestamp,
|
timestamp,
|
||||||
uuid,
|
uuid,
|
||||||
|
varchar,
|
||||||
} from 'drizzle-orm/pg-core';
|
} from 'drizzle-orm/pg-core';
|
||||||
|
|
||||||
export const users = pgTable('users', {
|
export const users = pgTable('users', {
|
||||||
id: uuid('id').primaryKey(),
|
id: serial('id').primaryKey(),
|
||||||
name: text('name').notNull(),
|
fullName: text('full_name'),
|
||||||
|
phone: varchar('phone', { length: 256 }),
|
||||||
status: text('status').notNull(),
|
status: text('status').notNull(),
|
||||||
updatedAt: timestamp('updatedAt')
|
updatedAt: timestamp('updatedAt')
|
||||||
.default(sql`CURRENT_TIMESTAMP`)
|
.default(sql`CURRENT_TIMESTAMP`)
|
||||||
|
102
src/utils/ban-suspicious-ips.ts
Normal file
102
src/utils/ban-suspicious-ips.ts
Normal file
@ -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<string, { count: number; lastAttempt: number }>();
|
||||||
|
const bannedIPs = new Set<string>();
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
};
|
23
src/utils/supabase/client.ts
Normal file
23
src/utils/supabase/client.ts
Normal file
@ -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<Database>(
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||||
|
);
|
||||||
|
return client;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useSupabaseClient = () => {
|
||||||
|
return useMemo(getSupbaseClient, []);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useSupabaseClient };
|
203
src/utils/supabase/database.types.ts
Normal file
203
src/utils/supabase/database.types.ts
Normal file
@ -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<keyof Database, 'public'>];
|
||||||
|
|
||||||
|
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;
|
5
src/utils/supabase/index.ts
Normal file
5
src/utils/supabase/index.ts
Normal file
@ -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';
|
56
src/utils/supabase/middleware.ts
Normal file
56
src/utils/supabase/middleware.ts
Normal file
@ -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<NextResponse> => {
|
||||||
|
try {
|
||||||
|
// Create an unmodified response
|
||||||
|
let response = NextResponse.next({
|
||||||
|
request: {
|
||||||
|
headers: request.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const supabase = createServerClient<Database>(
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
30
src/utils/supabase/server.ts
Normal file
30
src/utils/supabase/server.ts
Normal file
@ -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<Database>(
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
30
src/utils/supabase/types.ts
Normal file
30
src/utils/supabase/types.ts
Normal file
@ -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<Database>;
|
||||||
|
|
||||||
|
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<T extends keyof Database['public']['Tables']> =
|
||||||
|
Database['public']['Tables'][T]['Row'];
|
||||||
|
|
||||||
|
// Generic helper to get any table's insert type
|
||||||
|
export type TableInsert<T extends keyof Database['public']['Tables']> =
|
||||||
|
Database['public']['Tables'][T]['Insert'];
|
||||||
|
|
||||||
|
// Generic helper to get any table's update type
|
||||||
|
export type TableUpdate<T extends keyof Database['public']['Tables']> =
|
||||||
|
Database['public']['Tables'][T]['Update'];
|
Reference in New Issue
Block a user