From f071b6c19b8b3148712115c2dcc48ce0d73ec59e Mon Sep 17 00:00:00 2001 From: Gib Date: Tue, 22 Jul 2025 09:20:01 -0500 Subject: [PATCH] Format and make ban-suspicious-ips more better --- bun.lockb | Bin 382376 -> 391456 bytes package.json | 14 +- src/app/(auth)/auth/success/page.tsx | 5 +- src/app/(auth)/forgot-password/page.tsx | 2 +- src/app/(auth)/profile/page.tsx | 5 +- src/app/global-error.tsx | 6 +- src/app/layout.tsx | 10 +- src/app/page.tsx | 2 +- .../default/auth/buttons/client/index.tsx | 10 +- .../buttons/client/sign-in-with-apple.tsx | 16 +- .../buttons/client/sign-in-with-microsoft.tsx | 16 +- .../default/auth/buttons/client/sign-out.tsx | 9 +- .../buttons/server/sign-in-with-apple.tsx | 16 +- .../buttons/server/sign-in-with-microsoft.tsx | 16 +- .../default/auth/buttons/server/sign-out.tsx | 12 +- .../auth/cards/client/forgot-password.tsx | 23 +- .../default/auth/cards/client/sign-in.tsx | 63 ++--- .../forms/client/profile/avatar-upload.tsx | 46 ++-- .../forms/client/profile/profile-form.tsx | 10 +- .../client/profile/reset-password-form.tsx | 139 ++++++++++- .../default/forms/status-message.tsx | 19 +- .../default/forms/submit-button.tsx | 2 +- .../default/layout/header/index.tsx | 16 +- src/components/ui/based-avatar.tsx | 11 +- src/lib/hooks/context/use-auth.tsx | 52 ++--- src/lib/hooks/context/use-query.tsx | 25 +- src/lib/hooks/context/use-theme.tsx | 21 +- src/lib/hooks/use-file-upload.ts | 167 +++++++------ src/lib/queries/auth.ts | 26 ++- src/lib/queries/index.ts | 4 +- src/lib/queries/storage.ts | 33 ++- src/middleware.ts | 14 +- src/utils/ban-suspicious-ips.ts | 105 ++++++++- src/utils/supabase/client.ts | 2 +- src/utils/supabase/database.types.ts | 221 +++++++++--------- src/utils/supabase/types.ts | 22 +- 36 files changed, 716 insertions(+), 444 deletions(-) diff --git a/bun.lockb b/bun.lockb index fdccb78c2e2272390a23f9845c4676858998bd9e..c3abeae73b21dafff040dec0078953e2b0bd70f3 100755 GIT binary patch delta 61858 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 delta 57788 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 diff --git a/package.json b/package.json index 4738558..4e3dfcd 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "@supabase-cache-helpers/postgrest-react-query": "^1.13.4", "@supabase-cache-helpers/storage-react-query": "^1.3.5", "@supabase/ssr": "^0.6.1", - "@supabase/supabase-js": "^2.51.0", + "@supabase/supabase-js": "^2.52.0", "@t3-oss/env-nextjs": "^0.12.0", "@tanstack/react-query": "^5.83.0", "@tanstack/react-table": "^8.21.3", @@ -65,7 +65,7 @@ "import-in-the-middle": "^1.14.2", "input-otp": "^1.4.2", "lucide-react": "^0.522.0", - "next": "^15.4.1", + "next": "^15.4.2", "next-plausible": "^3.12.4", "next-themes": "^0.4.6", "postgres": "^3.4.7", @@ -86,22 +86,22 @@ "@tailwindcss/postcss": "^4.1.11", "@types/cors": "^2.8.19", "@types/express": "^5.0.3", - "@types/node": "^20.19.8", + "@types/node": "^20.19.9", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "drizzle-kit": "^0.30.6", "eslint": "^9.31.0", - "eslint-config-next": "^15.4.1", - "eslint-config-prettier": "^10.1.5", + "eslint-config-next": "^15.4.2", + "eslint-config-prettier": "^10.1.8", "eslint-plugin-drizzle": "^0.2.3", - "eslint-plugin-prettier": "^5.5.1", + "eslint-plugin-prettier": "^5.5.3", "postcss": "^8.5.6", "prettier": "^3.6.2", "prettier-plugin-tailwindcss": "^0.6.14", "tailwindcss": "^4.1.11", "tw-animate-css": "^1.3.5", "typescript": "^5.8.3", - "typescript-eslint": "^8.37.0" + "typescript-eslint": "^8.38.0" }, "ct3aMetadata": { "initVersion": "7.39.3" diff --git a/src/app/(auth)/auth/success/page.tsx b/src/app/(auth)/auth/success/page.tsx index a18dad1..b361ae3 100644 --- a/src/app/(auth)/auth/success/page.tsx +++ b/src/app/(auth)/auth/success/page.tsx @@ -15,8 +15,9 @@ const AuthSuccessPage = () => { // Small delay to ensure state is updated setTimeout(() => router.push('/'), 100); }; - handleAuthSuccess() - .catch(error => console.error(`Error handling auth success: ${error}`)); + handleAuthSuccess().catch((error) => + console.error(`Error handling auth success: ${error}`), + ); }, [refreshUser, router]); return ( diff --git a/src/app/(auth)/forgot-password/page.tsx b/src/app/(auth)/forgot-password/page.tsx index 35d1646..9dcbc7a 100644 --- a/src/app/(auth)/forgot-password/page.tsx +++ b/src/app/(auth)/forgot-password/page.tsx @@ -4,7 +4,7 @@ import { ForgotPasswordCard } from '@/components/default/auth/cards/client'; const ForgotPasswordPage = () => { return (
- +
); }; diff --git a/src/app/(auth)/profile/page.tsx b/src/app/(auth)/profile/page.tsx index c80e708..3c73822 100644 --- a/src/app/(auth)/profile/page.tsx +++ b/src/app/(auth)/profile/page.tsx @@ -1,9 +1,6 @@ 'use client'; const ProfilePage = () => { - return ( -
-
- ); + return
; }; export default ProfilePage; diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx index b3fe17f..c4e10ef 100644 --- a/src/app/global-error.tsx +++ b/src/app/global-error.tsx @@ -20,9 +20,11 @@ type GlobalErrorProps = { }; const GlobalError = ({ error, reset = undefined }: GlobalErrorProps) => { - useEffect(() => { Sentry.captureException(error) }, [error]); + useEffect(() => { + Sentry.captureException(error); + }, [error]); return ( - { template: '%s | Next Template', default: 'Next Template', }, - description: 'Gib\'s Next Template', + description: "Gib's Next Template", applicationName: 'Next Template', keywords: 'Next.js, Supabase, Tailwind, Tanstack, React, Query, T3, Gib', authors: [{ name: 'Gib', url: 'https://gbrown.org' }], @@ -217,7 +217,9 @@ const RootLayout = async ({ children, }: Readonly<{ children: React.ReactNode }>) => { const client = await SupabaseServer(); - const { data: { user } } = await getCurrentUser(client); + const { + data: { user }, + } = await getCurrentUser(client); return (
-
- {children} -
+
{children}
diff --git a/src/app/page.tsx b/src/app/page.tsx index b354cf8..80efd7c 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -3,7 +3,7 @@ import { SignInCard } from '@/components/default/auth/cards/client'; const HomePage = () => { return (
- +
); }; diff --git a/src/components/default/auth/buttons/client/index.tsx b/src/components/default/auth/buttons/client/index.tsx index 2adf938..4d2631b 100644 --- a/src/components/default/auth/buttons/client/index.tsx +++ b/src/components/default/auth/buttons/client/index.tsx @@ -1,3 +1,9 @@ -export { SignInWithApple, type SignInWithAppleProps } from './sign-in-with-apple'; -export { SignInWithMicrosoft, type SignInWithMicrosoftProps } from './sign-in-with-microsoft'; +export { + SignInWithApple, + type SignInWithAppleProps, +} from './sign-in-with-apple'; +export { + SignInWithMicrosoft, + type SignInWithMicrosoftProps, +} from './sign-in-with-microsoft'; export { SignInLinkButton } from './sign-in-link'; diff --git a/src/components/default/auth/buttons/client/sign-in-with-apple.tsx b/src/components/default/auth/buttons/client/sign-in-with-apple.tsx index b99dff7..1dc0c97 100644 --- a/src/components/default/auth/buttons/client/sign-in-with-apple.tsx +++ b/src/components/default/auth/buttons/client/sign-in-with-apple.tsx @@ -25,11 +25,11 @@ export const SignInWithApple = ({ formProps, textProps, iconProps, -} : SignInWithAppleProps) => { +}: SignInWithAppleProps) => { const router = useRouter(); const { loading, refreshUser } = useAuth(); const [statusMessage, setStatusMessage] = useState(''); - const [ isLoading, setIsLoading ] = useState(false); + const [isLoading, setIsLoading] = useState(false); const supabase = SupabaseClient()!; const handleSignInWithApple = async (e: React.FormEvent) => { @@ -63,12 +63,18 @@ export const SignInWithApple = ({ className={cn('w-full', submitButtonProps?.className)} >
- -

+ +

Sign In with Apple

- + {statusMessage && } ); diff --git a/src/components/default/auth/buttons/client/sign-in-with-microsoft.tsx b/src/components/default/auth/buttons/client/sign-in-with-microsoft.tsx index 7fab135..1929e9e 100644 --- a/src/components/default/auth/buttons/client/sign-in-with-microsoft.tsx +++ b/src/components/default/auth/buttons/client/sign-in-with-microsoft.tsx @@ -25,11 +25,11 @@ export const SignInWithMicrosoft = ({ formProps, textProps, iconProps, -} : SignInWithMicrosoftProps) => { +}: SignInWithMicrosoftProps) => { const router = useRouter(); const { loading, refreshUser } = useAuth(); const [statusMessage, setStatusMessage] = useState(''); - const [ isLoading, setIsLoading ] = useState(false); + const [isLoading, setIsLoading] = useState(false); const supabase = SupabaseClient()!; const handleSignInWithMicrosoft = async (e: React.FormEvent) => { @@ -63,12 +63,18 @@ export const SignInWithMicrosoft = ({ className={cn('w-full', submitButtonProps?.className)} >
- -

+ +

Sign In with Microsoft

- + {statusMessage && } ); diff --git a/src/components/default/auth/buttons/client/sign-out.tsx b/src/components/default/auth/buttons/client/sign-out.tsx index 802b525..8cf3082 100644 --- a/src/components/default/auth/buttons/client/sign-out.tsx +++ b/src/components/default/auth/buttons/client/sign-out.tsx @@ -1,12 +1,15 @@ 'use client'; -import { SubmitButton, type SubmitButtonProps } from '@/components/default/forms'; +import { + SubmitButton, + type SubmitButtonProps, +} from '@/components/default/forms'; import { useRouter } from 'next/navigation'; import { useAuth } from '@/lib/hooks/context'; import { signOut } from '@/lib/queries'; import { SupabaseClient } from '@/utils/supabase'; import { cn } from '@/lib/utils'; -type SignOutProps = Omit +type SignOutProps = Omit; export const SignOut = ({ className, @@ -38,7 +41,7 @@ export const SignOut = ({ className={cn( 'text-[1.0rem] font-semibold \ hover:bg-red-700/60 dark:hover:bg-red-300/80', - className + className, )} > Sign Out diff --git a/src/components/default/auth/buttons/server/sign-in-with-apple.tsx b/src/components/default/auth/buttons/server/sign-in-with-apple.tsx index 8fcb06c..76d5fdd 100644 --- a/src/components/default/auth/buttons/server/sign-in-with-apple.tsx +++ b/src/components/default/auth/buttons/server/sign-in-with-apple.tsx @@ -21,7 +21,7 @@ export const SignInWithApple = async ({ formProps, textProps, iconProps, -} : SignInWithAppleProps) => { +}: SignInWithAppleProps) => { const supabase = await SupabaseServer(); const handleSignInWithApple = async () => { @@ -29,7 +29,9 @@ export const SignInWithApple = async ({ if (!supabase) throw new Error('Supabase client not found'); const result = await signInWithApple(supabase); if (result.error) - throw new Error(`Error signing in with Microsoft: ${result.error.message}`); + throw new Error( + `Error signing in with Microsoft: ${result.error.message}`, + ); else if (result.data.url) window.location.href = result.data.url; } catch (error) { console.error(error); @@ -48,8 +50,14 @@ export const SignInWithApple = async ({ className={cn('w-full', submitButtonProps?.className)} >
- -

+ +

Sign In with Apple

diff --git a/src/components/default/auth/buttons/server/sign-in-with-microsoft.tsx b/src/components/default/auth/buttons/server/sign-in-with-microsoft.tsx index a3a013e..90b0c40 100644 --- a/src/components/default/auth/buttons/server/sign-in-with-microsoft.tsx +++ b/src/components/default/auth/buttons/server/sign-in-with-microsoft.tsx @@ -21,7 +21,7 @@ export const SignInWithMicrosoft = async ({ formProps, textProps, iconProps, -} : SignInWithMicrosoftProps) => { +}: SignInWithMicrosoftProps) => { const supabase = await SupabaseServer(); const handleSignInWithMicrosoft = async () => { @@ -29,7 +29,9 @@ export const SignInWithMicrosoft = async ({ if (!supabase) throw new Error('Supabase client not found'); const result = await signInWithMicrosoft(supabase); if (result.error) - throw new Error(`Error signing in with Microsoft: ${result.error.message}`); + throw new Error( + `Error signing in with Microsoft: ${result.error.message}`, + ); else if (result.data.url) window.location.href = result.data.url; } catch (error) { console.error(error); @@ -48,8 +50,14 @@ export const SignInWithMicrosoft = async ({ className={cn('w-full', submitButtonProps?.className)} >
- -

+ +

Sign In with Microsoft

diff --git a/src/components/default/auth/buttons/server/sign-out.tsx b/src/components/default/auth/buttons/server/sign-out.tsx index 0802e1e..430aa2c 100644 --- a/src/components/default/auth/buttons/server/sign-out.tsx +++ b/src/components/default/auth/buttons/server/sign-out.tsx @@ -1,12 +1,18 @@ 'use server'; import 'server-only'; import { redirect } from 'next/navigation'; -import { SubmitButton, type SubmitButtonProps } from '@/components/default/forms'; +import { + SubmitButton, + type SubmitButtonProps, +} from '@/components/default/forms'; import { signOut } from '@/lib/queries'; import { SupabaseServer } from '@/utils/supabase'; import { cn } from '@/lib/utils'; -type SignOutProps = Omit +type SignOutProps = Omit< + SubmitButtonProps, + 'disabled' | 'onClick' | 'formAction' +>; export const SignOut = async ({ className, @@ -35,7 +41,7 @@ export const SignOut = async ({ className={cn( 'text-[1.0rem] font-semibold \ hover:bg-red-700/60 dark:hover:bg-red-300/80', - className + className, )} > Sign Out diff --git a/src/components/default/auth/cards/client/forgot-password.tsx b/src/components/default/auth/cards/client/forgot-password.tsx index 3c8e4df..de335c7 100644 --- a/src/components/default/auth/cards/client/forgot-password.tsx +++ b/src/components/default/auth/cards/client/forgot-password.tsx @@ -27,7 +27,7 @@ import { cn } from '@/lib/utils'; const forgotPasswordFormSchema = z.object({ email: z.string().email({ - message: 'Please enter a valid email address.' + message: 'Please enter a valid email address.', }), }); @@ -65,10 +65,12 @@ export const ForgotPasswordCard = ({ }); useEffect(() => { - if (isAuthenticated) router.push('/') + if (isAuthenticated) router.push('/'); }, [isAuthenticated, router]); - const handleForgotPassword = async (values: z.infer) => { + const handleForgotPassword = async ( + values: z.infer, + ) => { try { setStatusMessage(''); const formData = new FormData(); @@ -100,7 +102,10 @@ export const ForgotPasswordCard = ({ Don't have an account?{' '} )} /> - + Reset Password {statusMessage && diff --git a/src/components/default/auth/cards/client/sign-in.tsx b/src/components/default/auth/cards/client/sign-in.tsx index 8a60317..02f130b 100755 --- a/src/components/default/auth/cards/client/sign-in.tsx +++ b/src/components/default/auth/cards/client/sign-in.tsx @@ -36,10 +36,10 @@ import { cn } from '@/lib/utils'; const signInFormSchema = z.object({ email: z.string().email({ - message: 'Please enter a valid email address.' + message: 'Please enter a valid email address.', }), password: z.string().min(8, { - message: 'Password must be at least 8 characters.' + message: 'Password must be at least 8 characters.', }), }); @@ -71,8 +71,10 @@ type SignInCardProps = { cardProps?: ComponentProps; formProps?: Omit, 'onSubmit'>; formLabelProps?: ComponentProps; - submitButtonProps?: Omit, - 'pendingText' | 'disabled'>; + submitButtonProps?: Omit< + ComponentProps, + 'pendingText' | 'disabled' + >; signInWithAppleProps?: SignInWithAppleProps; signInWithMicrosoftProps?: SignInWithMicrosoftProps; }; @@ -89,7 +91,6 @@ export const SignInCard = ({ signInWithAppleProps, signInWithMicrosoftProps, }: SignInCardProps) => { - const router = useRouter(); const { isAuthenticated, loading, refreshUser } = useAuth(); const [statusMessage, setStatusMessage] = useState(''); @@ -154,10 +155,7 @@ export const SignInCard = ({ {...containerProps} className={cn('p-4 bg-card/25 min-h-[720px]', containerProps?.className)} > - + Sign In @@ -196,7 +194,10 @@ export const SignInCard = ({
Password - - Forgot Password? - + Forgot Password? ) : ( - ))} + ))} Sign In @@ -278,10 +275,11 @@ export const SignInCard = ({ @@ -430,14 +432,14 @@ export const SignInCard = ({ ) : ( - ))} + ))} Sign Up @@ -451,10 +453,11 @@ export const SignInCard = ({ ); }; - diff --git a/src/components/default/auth/forms/client/profile/avatar-upload.tsx b/src/components/default/auth/forms/client/profile/avatar-upload.tsx index 11d535b..a80b8de 100755 --- a/src/components/default/auth/forms/client/profile/avatar-upload.tsx +++ b/src/components/default/auth/forms/client/profile/avatar-upload.tsx @@ -2,11 +2,7 @@ import { useFileUpload } from '@/lib/hooks'; import { useAuth } from '@/lib/hooks/context'; import { SupabaseClient } from '@/utils/supabase'; -import { - BasedAvatar, - Card, - CardContent, -} from '@/components/ui'; +import { BasedAvatar, Card, CardContent } from '@/components/ui'; import { Loader2, Pencil, Upload } from 'lucide-react'; import type { ComponentProps, ChangeEvent } from 'react'; import { toast } from 'sonner'; @@ -34,11 +30,10 @@ export const AvatarUpload = ({ }: AvatarUploadProps) => { const { profile, isAuthenticated } = useAuth(); const client = SupabaseClient()!; - const { - isUploading, - fileInputRef, - uploadAvatarMutation - } = useFileUpload(client, 'avatars'); + const { isUploading, fileInputRef, uploadAvatarMutation } = useFileUpload( + client, + 'avatars', + ); const handleAvatarClick = () => { if (!isAuthenticated) { @@ -54,11 +49,12 @@ export const AvatarUpload = ({ if (!file) throw new Error('No file selected!'); if (!client) throw new Error('Supabase client not found!'); if (!isAuthenticated) throw new Error('User is not authenticated!'); - if (!file.type.startsWith('image/')) throw new Error('File is not an image!'); + if (!file.type.startsWith('image/')) + throw new Error('File is not an image!'); if (file.size > 8 * 1024 * 1024) throw new Error('File is too large!'); - const avatarPath = profile?.avatar_url ?? - `${profile?.id}.${file.name.split('.').pop()}`; + const avatarPath = + profile?.avatar_url ?? `${profile?.id}.${file.name.split('.').pop()}`; const avatarUrl = await uploadAvatarMutation.mutateAsync({ file, @@ -70,26 +66,25 @@ export const AvatarUpload = ({ replace: avatarPath, }); if (avatarUrl) await onAvatarUploaded(avatarUrl); - } catch (error) { toast.error(`Error: ${error as string}`); } }; return ( - +
@@ -124,7 +121,8 @@ export const AvatarUpload = ({ {...iconProps} className={cn( 'text-white opacity-100 group-hover:opacity-0\ - transition-opacity', iconProps?.className + transition-opacity', + iconProps?.className, )} /> diff --git a/src/components/default/auth/forms/client/profile/profile-form.tsx b/src/components/default/auth/forms/client/profile/profile-form.tsx index 14d0cdc..cb9c7cb 100644 --- a/src/components/default/auth/forms/client/profile/profile-form.tsx +++ b/src/components/default/auth/forms/client/profile/profile-form.tsx @@ -28,9 +28,7 @@ type ProfileFormProps = { onSubmit: (values: z.infer) => Promise; }; -export const ProfileForm = ({ - onSubmit, -}: ProfileFormProps) => { +export const ProfileForm = ({ onSubmit }: ProfileFormProps) => { const { profile, loading } = useAuth(); const form = useForm>({ resolver: zodResolver(formSchema), @@ -48,14 +46,10 @@ export const ProfileForm = ({ }); }, [profile, form]); - const handleSubmit = async (values: z.infer) => { - await onSubmit(values); - }; - return ( - + Promise>; + onSubmit: (values: z.infer) => Promise>; message?: string; + cardHeaderProps?: ComponentProps; + cardTitleProps?: ComponentProps; + cardDescriptionProps?: ComponentProps; + cardContentProps?: ComponentProps; + formProps?: Omit, 'onSubmit'>; + formLabelProps?: ComponentProps; + inputProps?: Omit, 'type'>; + formDescriptionProps?: ComponentProps; + formMessageProps?: ComponentProps; + statusMessageProps?: Omit, 'message'>; + submitButtonProps?: Omit, 'disabled'>; }; export const ResetPasswordForm = ({ onSubmit, message, + cardHeaderProps, + cardTitleProps, + cardDescriptionProps, + cardContentProps, + formProps, + formLabelProps, + inputProps, + formDescriptionProps, + formMessageProps, + statusMessageProps, + submitButtonProps, }: ResetPasswordFormProps) => { const { loading } = useAuth(); const [statusMessage, setStatusMessage] = useState(message ?? ''); @@ -41,14 +78,96 @@ export const ResetPasswordForm = ({ const handleSubmit = async (values: z.infer) => { try { - const formData = new FormData(); - formData.append('password', values.password); - formData.append('confirmPassword', values.confirmPassword); - await onSubmit(formData); - + const { error } = await onSubmit(values); + if (error) throw new Error(error.message); + setStatusMessage('Password reset successfully.'); } catch (error) { - + setStatusMessage(`Error: ${error as string}`); } - } + }; + return ( + <> + + + Change Password + + + Update your password to keep your account secure + + + + + + ( + + New Password + + + + + Enter your new password. Must be at least 8 characters. + + + + )} + /> + ( + + Confirm Password + + + + + Please re-enter your new password to confirm. + + + + )} + /> + {statusMessage && + (statusMessage.includes('Error') || + statusMessage.includes('error') || + statusMessage.includes('failed') || + statusMessage.includes('invalid') ? ( + + ) : ( + + ))} +
+ + Update Password + +
+ + +
+ + ); }; diff --git a/src/components/default/forms/status-message.tsx b/src/components/default/forms/status-message.tsx index 52cabf3..72a39ae 100644 --- a/src/components/default/forms/status-message.tsx +++ b/src/components/default/forms/status-message.tsx @@ -1,10 +1,7 @@ import { type ComponentProps } from 'react'; import { cn } from '@/lib/utils'; -type Message = -| { success: string} -| { error: string} -| { message: string} +type Message = { success: string } | { error: string } | { message: string }; type StatusMessageProps = { message: Message; @@ -30,9 +27,9 @@ export const StatusMessage = ({
{message.success}
@@ -45,13 +42,7 @@ export const StatusMessage = ({ {message.error} )} - {'message' in message && ( -
- {message.message} -
- )} + {'message' in message &&
{message.message}
} ); }; diff --git a/src/components/default/forms/submit-button.tsx b/src/components/default/forms/submit-button.tsx index 7fdd83c..1df9cdc 100644 --- a/src/components/default/forms/submit-button.tsx +++ b/src/components/default/forms/submit-button.tsx @@ -44,7 +44,7 @@ export const SubmitButton = ({

) : ( - children + children )} ); diff --git a/src/components/default/layout/header/index.tsx b/src/components/default/layout/header/index.tsx index c20fdd3..fd03cb6 100644 --- a/src/components/default/layout/header/index.tsx +++ b/src/components/default/layout/header/index.tsx @@ -1,7 +1,11 @@ 'use client'; import Image from 'next/image'; import Link from 'next/link'; -import { ThemeToggle, type ThemeToggleProps, useAuth } from '@/lib/hooks/context'; +import { + ThemeToggle, + type ThemeToggleProps, + useAuth, +} from '@/lib/hooks/context'; import { cn } from '@/lib/utils'; import { AvatarDropdown } from './avatar-dropdown'; import { type ComponentProps } from 'react'; @@ -11,10 +15,7 @@ type Props = { themeToggleProps?: ThemeToggleProps; }; -const Header = ({ - headerProps, - themeToggleProps, -}: Props) => { +const Header = ({ headerProps, themeToggleProps }: Props) => { const { isAuthenticated } = useAuth(); const Controls = () => ( @@ -28,7 +29,7 @@ const Header = ({ ...themeToggleProps?.buttonProps, }} /> - {isAuthenticated && ( )} + {isAuthenticated && } ); @@ -41,7 +42,6 @@ const Header = ({ )} >
- {/* Left spacer for perfect centering */}
@@ -76,10 +76,8 @@ const Header = ({
-
); - }; export default Header; diff --git a/src/components/ui/based-avatar.tsx b/src/components/ui/based-avatar.tsx index 581489e..ec0fcee 100644 --- a/src/components/ui/based-avatar.tsx +++ b/src/components/ui/based-avatar.tsx @@ -34,7 +34,11 @@ const BasedAvatar = ({ {...props} > {src ? ( - + ) : ( + )} )} diff --git a/src/lib/hooks/context/use-auth.tsx b/src/lib/hooks/context/use-auth.tsx index 86a3dd2..23ac36e 100755 --- a/src/lib/hooks/context/use-auth.tsx +++ b/src/lib/hooks/context/use-auth.tsx @@ -1,10 +1,5 @@ 'use client'; -import React, { - createContext, - useContext, - useEffect, - useState -} from 'react'; +import React, { createContext, useContext, useEffect, useState } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useQuery as useSupabaseQuery, @@ -40,13 +35,12 @@ export const AuthContextProvider = ({ const [user, setUser] = useState(initialUser ?? null); // User query with initial data - const { - data: userData, - isLoading: userLoading, - } = useQuery({ + const { data: userData, isLoading: userLoading } = useQuery({ queryKey: ['auth', 'user'], queryFn: async () => { - const { data: { user } } = await supabase.auth.getUser(); + const { + data: { user }, + } = await supabase.auth.getUser(); return user; }, initialData: initialUser, @@ -54,10 +48,7 @@ export const AuthContextProvider = ({ }); // Profile query using Supabase Cache Helpers - const { - data: profileData, - isLoading: profileLoading, - } = useSupabaseQuery( + const { data: profileData, isLoading: profileLoading } = useSupabaseQuery( supabase .from('profiles') .select('*') @@ -65,7 +56,7 @@ export const AuthContextProvider = ({ .single(), { enabled: !!userData?.id, - } + }, ); // Update profile mutation @@ -75,21 +66,26 @@ export const AuthContextProvider = ({ '*', { onSuccess: () => toast.success('Profile updated successfully!'), - onError: (error) => toast.error(`Failed to update profile: ${error.message}`), - } + onError: (error) => + toast.error(`Failed to update profile: ${error.message}`), + }, ); // Auth state listener useEffect(() => { - const { data: { subscription } } = supabase.auth.onAuthStateChange( - async (event, session) => { - setUser(session?.user ?? null); + const { + data: { subscription }, + } = supabase.auth.onAuthStateChange(async (event, session) => { + setUser(session?.user ?? null); - if (event === 'SIGNED_IN' || event === 'SIGNED_OUT' || event === 'TOKEN_REFRESHED') { - await queryClient.invalidateQueries({ queryKey: ['auth'] }); - } + if ( + event === 'SIGNED_IN' || + event === 'SIGNED_OUT' || + event === 'TOKEN_REFRESHED' + ) { + await queryClient.invalidateQueries({ queryKey: ['auth'] }); } - ); + }); return () => subscription.unsubscribe(); }, [supabase.auth, queryClient]); @@ -116,11 +112,7 @@ export const AuthContextProvider = ({ refreshUser, }; - return ( - - {children} - - ); + return {children}; }; export const useAuth = () => { diff --git a/src/lib/hooks/context/use-query.tsx b/src/lib/hooks/context/use-query.tsx index e25fc1e..64b24c4 100644 --- a/src/lib/hooks/context/use-query.tsx +++ b/src/lib/hooks/context/use-query.tsx @@ -14,17 +14,18 @@ const enum QueryErrorCodes { FETCH_AVATAR_FAILED = 'FETCH_AVATAR_FAILED', UPDATE_PROFILE_FAILED = 'UPDATE_PROFILE_FAILED', UPLOAD_PHOTO_FAILED = 'UPLOAD_PHOTO_FAILED', -}; +} const queryCacheOnError = (error: unknown, query: any) => { - const errorMessage = error instanceof Error ? error.message : error as string; + const errorMessage = + error instanceof Error ? error.message : (error as string); switch (query.meta?.errCode) { case QueryErrorCodes.FETCH_USER_FAILED: break; case QueryErrorCodes.FETCH_PROFILE_FAILED: break; case QueryErrorCodes.FETCH_AVATAR_FAILED: - console.warn('Failed to fetch avatar. User may not have one!') + console.warn('Failed to fetch avatar. User may not have one!'); break; default: console.error('Query error:', error); @@ -38,13 +39,14 @@ const mutationCacheOnError = ( context: unknown, mutation: any, ) => { - const errorMessage = error instanceof Error ? error.message : error as string; + const errorMessage = + error instanceof Error ? error.message : (error as string); switch (mutation.meta?.errCode) { case QueryErrorCodes.UPDATE_PROFILE_FAILED: - toast.error(`Failed to update user profile: ${errorMessage}`) + toast.error(`Failed to update user profile: ${errorMessage}`); break; case QueryErrorCodes.UPLOAD_PHOTO_FAILED: - toast.error(`Failed to upload photo: ${errorMessage}`) + toast.error(`Failed to upload photo: ${errorMessage}`); break; default: console.error('Mutation error:', error); @@ -52,7 +54,6 @@ const mutationCacheOnError = ( } }; - const QueryClientProvider = ({ children }: { children: React.ReactNode }) => { const [queryClient] = useState( () => @@ -72,9 +73,13 @@ const QueryClientProvider = ({ children }: { children: React.ReactNode }) => { gcTime: Infinity, }, }, - }) - ) - return {children} + }), + ); + return ( + + {children} + + ); }; export { QueryClientProvider, QueryErrorCodes }; diff --git a/src/lib/hooks/context/use-theme.tsx b/src/lib/hooks/context/use-theme.tsx index ea37d15..ebd12ea 100644 --- a/src/lib/hooks/context/use-theme.tsx +++ b/src/lib/hooks/context/use-theme.tsx @@ -1,9 +1,5 @@ 'use client'; -import { - useEffect, - useState, - type ComponentProps, -} from 'react'; +import { useEffect, useState, type ComponentProps } from 'react'; import { ThemeProvider as NextThemesProvider } from 'next-themes'; import { Moon, Sun } from 'lucide-react'; import { useTheme } from 'next-themes'; @@ -14,10 +10,11 @@ const ThemeProvider = ({ children, ...props }: ComponentProps) => { - const [mounted, setMounted] = useState(false); - useEffect(() => { setMounted(true) }, []); + useEffect(() => { + setMounted(true); + }, []); if (!mounted) return null; return {children}; @@ -28,15 +25,13 @@ type ThemeToggleProps = { buttonProps?: Omit, 'onClick'>; }; -const ThemeToggle = ({ - size = 1, - buttonProps, -}: ThemeToggleProps) => { - +const ThemeToggle = ({ size = 1, buttonProps }: ThemeToggleProps) => { const { setTheme, resolvedTheme } = useTheme(); const [mounted, setMounted] = useState(false); - useEffect(() => { setMounted(true) }, []); + useEffect(() => { + setMounted(true); + }, []); if (!mounted) { return ( diff --git a/src/lib/hooks/use-file-upload.ts b/src/lib/hooks/use-file-upload.ts index 9543f03..cd69fa8 100644 --- a/src/lib/hooks/use-file-upload.ts +++ b/src/lib/hooks/use-file-upload.ts @@ -11,11 +11,13 @@ type UploadToStorageProps = { client: SBClientWithDatabase; file: File; bucket: string; - resize?: false | { - maxWidth?: number; - maxHeight?: number; - quality?: number; - }; + resize?: + | false + | { + maxWidth?: number; + maxHeight?: number; + quality?: number; + }; replace?: false | string; }; @@ -26,66 +28,70 @@ const useFileUpload = (client: SBClientWithDatabase, bucket: string) => { const queryClient = useQueryClient(); // Initialize the upload hook at the top level - const { mutateAsync: upload } = useUpload( - client.storage.from(bucket), - { - buildFileName: ({ fileName, path }) => path ?? fileName, - } + const { mutateAsync: upload } = useUpload(client.storage.from(bucket), { + buildFileName: ({ fileName, path }) => path ?? fileName, + }); + + const uploadToStorage = useCallback( + async ({ + file, + resize = false, + replace = false, + }: Omit) => { + try { + if (!isAuthenticated) + throw new Error('Error: User is not authenticated!'); + + setIsUploading(true); + + let fileToUpload = file; + if (resize && file.type.startsWith('image/')) + fileToUpload = await resizeImage({ file, options: resize }); + + const fileName = + replace || + `${Date.now()}-${profile?.id}.${file.name.split('.').pop()}`; + + // Create a file object with the custom path + const fileWithPath = Object.assign(fileToUpload, { + webkitRelativePath: fileName, + }); + + const uploadResult = await upload({ files: [fileWithPath] }); + + if (!uploadResult || uploadResult.length === 0) { + throw new Error('Upload failed: No result returned'); + } + + const uploadedFile = uploadResult[0]; + if (!uploadedFile || uploadedFile.error) { + throw new Error( + `Error uploading file: ${uploadedFile?.error.message ?? 'No uploaded file'}`, + ); + } + + // Get signed URL for the uploaded file + const { data: urlData, error: urlError } = await getSignedUrl({ + client, + bucket, + path: uploadedFile.data.path, + }); + + if (urlError) { + throw new Error(`Error getting signed URL: ${urlError.message}`); + } + + return { urlData, error: null }; + } catch (error) { + return { data: null, error }; + } finally { + setIsUploading(false); + if (fileInputRef.current) fileInputRef.current.value = ''; + } + }, + [client, bucket, upload, isAuthenticated, profile?.id], ); - const uploadToStorage = useCallback(async ({ - file, - resize = false, - replace = false, - }: Omit) => { - try { - if (!isAuthenticated) - throw new Error('Error: User is not authenticated!'); - - setIsUploading(true); - - let fileToUpload = file; - if (resize && file.type.startsWith('image/')) - fileToUpload = await resizeImage({ file, options: resize }); - - const fileName = replace || `${Date.now()}-${profile?.id}.${file.name.split('.').pop()}`; - - // Create a file object with the custom path - const fileWithPath = Object.assign(fileToUpload, { - webkitRelativePath: fileName, - }); - - const uploadResult = await upload({ files: [fileWithPath]}); - - if (!uploadResult || uploadResult.length === 0) { - throw new Error('Upload failed: No result returned'); - } - - const uploadedFile = uploadResult[0]; - if (!uploadedFile || uploadedFile.error) { - throw new Error(`Error uploading file: ${uploadedFile?.error.message ?? 'No uploaded file'}`); - } - - // Get signed URL for the uploaded file - const { data: urlData, error: urlError } = await getSignedUrl({ - client, - bucket, - path: uploadedFile.data.path, - }); - - if (urlError) { - throw new Error(`Error getting signed URL: ${urlError.message}`); - } - - return { urlData, error: null }; - } catch (error) { - return { data: null, error }; - } finally { - setIsUploading(false); - if (fileInputRef.current) fileInputRef.current.value = ''; - } - }, [client, bucket, upload, isAuthenticated, profile?.id]); - const uploadMutation = useMutation({ mutationFn: uploadToStorage, onSuccess: (result) => { @@ -96,38 +102,51 @@ const useFileUpload = (client: SBClientWithDatabase, bucket: string) => { } }, onError: (error) => { - toast.error(`Upload failed: ${error instanceof Error ? error.message : error}`); + toast.error( + `Upload failed: ${error instanceof Error ? error.message : error}`, + ); }, meta: { errCode: QueryErrorCodes.UPLOAD_PHOTO_FAILED }, }); const uploadAvatarMutation = useMutation({ - mutationFn: async (props: Omit) => { + mutationFn: async ( + props: Omit, + ) => { const { data, error } = await uploadToStorage(props); if (error) throw new Error(`Error uploading avatar: ${error as string}`); return data; }, onSuccess: (avatarUrl) => { - queryClient.invalidateQueries({ queryKey: ['auth'] }) - .catch((error) => console.error('Error invalidating auth query:', error)); + queryClient + .invalidateQueries({ queryKey: ['auth'] }) + .catch((error) => + console.error('Error invalidating auth query:', error), + ); queryClient.setQueryData(['auth, user'], (oldUser: User) => oldUser); if (profile?.id) { - queryClient.setQueryData(['profiles', profile.id], (oldProfile: Profile) => ({ - ...oldProfile, - avatar_url: avatarUrl, - updated_at: new Date().toISOString(), - })); + queryClient.setQueryData( + ['profiles', profile.id], + (oldProfile: Profile) => ({ + ...oldProfile, + avatar_url: avatarUrl, + updated_at: new Date().toISOString(), + }), + ); } toast.success('Avatar uploaded successfully!'); }, onError: (error) => { - toast.error(`Avatar upload failed: ${error instanceof Error ? error.message : error}`); + toast.error( + `Avatar upload failed: ${error instanceof Error ? error.message : error}`, + ); }, meta: { errCode: QueryErrorCodes.UPLOAD_PHOTO_FAILED }, }); return { - isUploading: isUploading || uploadMutation.isPending || uploadAvatarMutation.isPending, + isUploading: + isUploading || uploadMutation.isPending || uploadAvatarMutation.isPending, fileInputRef, uploadToStorage, uploadMutation, diff --git a/src/lib/queries/auth.ts b/src/lib/queries/auth.ts index b56d9a4..29cb8b7 100644 --- a/src/lib/queries/auth.ts +++ b/src/lib/queries/auth.ts @@ -1,4 +1,8 @@ -import { type Profile, type SBClientWithDatabase, type UserRecord } from '@/utils/supabase'; +import { + type Profile, + type SBClientWithDatabase, + type UserRecord, +} from '@/utils/supabase'; const signUp = (client: SBClientWithDatabase, formData: FormData) => { const full_name = formData.get('name') as string; @@ -14,8 +18,8 @@ const signUp = (client: SBClientWithDatabase, formData: FormData) => { full_name, email, provider: 'email', - } - } + }, + }, }); }; @@ -62,24 +66,21 @@ const resetPassword = (client: SBClientWithDatabase, formData: FormData) => { const signOut = (client: SBClientWithDatabase) => { return client.auth.signOut(); -} +}; const getCurrentUser = (client: SBClientWithDatabase) => { return client.auth.getUser(); }; const getProfile = (client: SBClientWithDatabase, userId: string) => { - return client - .from(`profiles`) - .select(`*`) - .eq(`id`, userId) - .single(); + return client.from(`profiles`).select(`*`).eq(`id`, userId).single(); }; const getUserWithStatus = (client: SBClientWithDatabase, userId: string) => { return client .from(`profiles`) - .select(` + .select( + ` id, updated_at, email, @@ -91,7 +92,8 @@ const getUserWithStatus = (client: SBClientWithDatabase, userId: string) => { created_at, updated_by:profiles!updated_by_id(*) ) - `) + `, + ) .eq(`id`, userId) .throwOnError() .single(); @@ -122,5 +124,5 @@ export { signInWithMicrosoft, signOut, signUp, - updateProfile + updateProfile, }; diff --git a/src/lib/queries/index.ts b/src/lib/queries/index.ts index 077a9f6..e620821 100644 --- a/src/lib/queries/index.ts +++ b/src/lib/queries/index.ts @@ -9,7 +9,7 @@ export { signInWithMicrosoft, signOut, signUp, - updateProfile + updateProfile, } from './auth'; export { deleteFiles, @@ -20,5 +20,5 @@ export { listFiles, resizeImage, uploadFile, - updateFile + updateFile, } from './storage'; diff --git a/src/lib/queries/storage.ts b/src/lib/queries/storage.ts index 658f575..15f636a 100755 --- a/src/lib/queries/storage.ts +++ b/src/lib/queries/storage.ts @@ -36,10 +36,7 @@ type ResizeImageProps = { }; }; -const getAvatarUrl = ( - client: SBClientWithDatabase, - path: string, -) => { +const getAvatarUrl = (client: SBClientWithDatabase, path: string) => { return getPublicUrl({ client, bucket: 'avatars', @@ -48,7 +45,7 @@ const getAvatarUrl = ( width: 128, height: 128, quality: 0.8, - } + }, }).data.publicUrl; }; @@ -61,12 +58,12 @@ const getPublicUrl = ({ }: GetStorageProps) => { return client.storage .from(bucket) - .getPublicUrl(path, { download, transform}); + .getPublicUrl(path, { download, transform }); }; const getSignedAvatarUrl = ( client: SBClientWithDatabase, - avatarUrl: string + avatarUrl: string, ) => { return getSignedUrl({ client, @@ -90,7 +87,7 @@ const getSignedUrl = ({ }: GetStorageProps) => { return client.storage .from(bucket) - .createSignedUrl(path, seconds, { download, transform}); + .createSignedUrl(path, seconds, { download, transform }); }; const uploadFile = ({ @@ -100,9 +97,7 @@ const uploadFile = ({ file, options = {}, }: UploadStorageProps) => { - return client.storage - .from(bucket) - .upload(path, file, options); + return client.storage.from(bucket).upload(path, file, options); }; const updateFile = ({ @@ -114,9 +109,7 @@ const updateFile = ({ upsert: true, }, }: UploadStorageProps) => { - return client.storage - .from(bucket) - .update(path, file, options); + return client.storage.from(bucket).update(path, file, options); }; const deleteFiles = ({ @@ -127,7 +120,7 @@ const deleteFiles = ({ client: SBClientWithDatabase; bucket: string; path: string[]; - }) => { +}) => { return client.storage.from(bucket).remove(path); }; @@ -141,9 +134,9 @@ const listFiles = ({ bucket: string; path?: string; options?: { - limit?: number; - offset?: number; - sortBy?: { column: string, order: 'asc' | 'desc' }; + limit?: number; + offset?: number; + sortBy?: { column: string; order: 'asc' | 'desc' }; }; }) => { return client.storage.from(bucket).list(path, options); @@ -190,7 +183,7 @@ const resizeImage = async ({ resolve(resizedFile); }, 'image/jpeg', - (options.quality && options.quality < 1 && options.quality > 0) + options.quality && options.quality < 1 && options.quality > 0 ? options.quality : 0.8, ); @@ -208,5 +201,5 @@ export { listFiles, resizeImage, uploadFile, - updateFile + updateFile, }; diff --git a/src/middleware.ts b/src/middleware.ts index a2c1306..97d496f 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,11 +1,21 @@ -import { type NextRequest } from 'next/server'; +import { type NextRequest, NextResponse } 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); + const response = await updateSession(request); + const newResponse = NextResponse.next({ + request: { headers: new Headers(request.headers) }, + }); + + if (response.headers) { + response.headers.forEach((value, key) => { + newResponse.headers.set(key, value); + }); + } + return response; }; export const config = { diff --git a/src/utils/ban-suspicious-ips.ts b/src/utils/ban-suspicious-ips.ts index 7216c4d..6e60c32 100644 --- a/src/utils/ban-suspicious-ips.ts +++ b/src/utils/ban-suspicious-ips.ts @@ -1,7 +1,8 @@ import { type NextRequest, NextResponse } from 'next/server'; -// In-memory store for tracking IPs (use Redis in production) +// In-memory stores for tracking IPs (use Redis in production) const ipAttempts = new Map(); +const ip404Attempts = new Map(); const bannedIPs = new Set(); // Ban Arctic Wolf Explicitly @@ -9,6 +10,7 @@ bannedIPs.add('::ffff:10.0.1.49'); // Suspicious patterns that indicate malicious activity const MALICIOUS_PATTERNS = [ + // Your existing patterns /web-inf/i, /\.jsp/i, /\.php/i, @@ -28,6 +30,37 @@ const MALICIOUS_PATTERNS = [ /\.%00/i, /\.\./, /lcgi/i, + + // New patterns from your logs + /\/appliance\//i, + /bomgar/i, + /netburner-logo/i, + /\/ui\/images\//i, + /logon_merge/i, + /logon_t\.gif/i, + /login_top\.gif/i, + /theme1\/images/i, + /\.well-known\/acme-challenge\/.*\.jpg$/i, + /\.well-known\/pki-validation\/.*\.jpg$/i, + + // Path traversal and system file access patterns + /\/etc\/passwd/i, + /\/etc%2fpasswd/i, + /\/etc%5cpasswd/i, + /\/\/+etc/i, + /\\\\+.*etc/i, + /%2f%2f/i, + /%5c%5c/i, + /\/\/+/, + /\\\\+/, + /%00/i, + /%23/i, + + // Encoded path traversal attempts + /%2e%2e/i, + /%252e/i, + /%c0%ae/i, + /%c1%9c/i, ]; // Suspicious HTTP methods @@ -37,6 +70,10 @@ 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 +// 404 rate limiting settings +const RATE_404_WINDOW = 2 * 60 * 1000; // 2 minutes +const MAX_404_ATTEMPTS = 10; // Max 404s before ban + const getClientIP = (request: NextRequest): string => { const forwarded = request.headers.get('x-forwarded-for'); const realIP = request.headers.get('x-real-ip'); @@ -64,17 +101,51 @@ const updateIPAttempts = (ip: string): boolean => { 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; +}; + +const update404Attempts = (ip: string): boolean => { + const now = Date.now(); + const attempts = ip404Attempts.get(ip); + + if (!attempts || now - attempts.lastAttempt > RATE_404_WINDOW) { + ip404Attempts.set(ip, { count: 1, lastAttempt: now }); + return false; + } + + attempts.count++; + attempts.lastAttempt = now; + + if (attempts.count > MAX_404_ATTEMPTS) { + bannedIPs.add(ip); + ip404Attempts.delete(ip); + + console.log( + `🔨 IP ${ip} banned for excessive 404 requests (${attempts.count} in ${RATE_404_WINDOW / 1000}s)`, + ); + + setTimeout(() => { + bannedIPs.delete(ip); + }, BAN_DURATION); + + return true; + } + return false; }; @@ -83,20 +154,48 @@ export const banSuspiciousIPs = (request: NextRequest): NextResponse | null => { const method = request.method; const ip = getClientIP(request); - if (bannedIPs.has(ip)) return new NextResponse('Access denied.', { status: 403 }); + // Check if IP is already banned + if (bannedIPs.has(ip)) { + return new NextResponse('Access denied.', { status: 403 }); + } const isSuspiciousPath = isPathSuspicious(pathname); const isSuspiciousMethod = isMethodSuspicious(method); + // Handle suspicious activity 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; +}; + +// Call this function when you detect a 404 response +export const handle404Response = ( + request: NextRequest, +): NextResponse | null => { + const ip = getClientIP(request); + + if (bannedIPs.has(ip)) { + return new NextResponse('Access denied.', { status: 403 }); + } + + const shouldBan = update404Attempts(ip); + + if (shouldBan) { + return new NextResponse('Access denied - IP banned for excessive 404s.', { + status: 403, + }); + } + return null; }; diff --git a/src/utils/supabase/client.ts b/src/utils/supabase/client.ts index f01dc5e..6349075 100644 --- a/src/utils/supabase/client.ts +++ b/src/utils/supabase/client.ts @@ -5,7 +5,7 @@ import type { Database, SBClientWithDatabase } from '@/utils/supabase'; let client: SBClientWithDatabase | undefined; const getSupbaseClient = (): SBClientWithDatabase | undefined => { - if (client) return client; + if (client) return client; client = createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, diff --git a/src/utils/supabase/database.types.ts b/src/utils/supabase/database.types.ts index 3f41d60..634ac02 100644 --- a/src/utils/supabase/database.types.ts +++ b/src/utils/supabase/database.types.ts @@ -4,197 +4,196 @@ export type Json = | boolean | null | { [key: string]: Json | undefined } - | Json[] + | Json[]; export type Database = { public: { Tables: { profiles: { Row: { - avatar_url: string | null - current_status_id: string | null - email: string | null - full_name: string | null - id: string - provider: string | null - updated_at: string | null - } + avatar_url: string | null; + current_status_id: string | null; + email: string | null; + full_name: string | null; + id: string; + provider: string | null; + updated_at: string | null; + }; Insert: { - avatar_url?: string | null - current_status_id?: string | null - email?: string | null - full_name?: string | null - id: string - provider?: string | null - updated_at?: string | null - } + avatar_url?: string | null; + current_status_id?: string | null; + email?: string | null; + full_name?: string | null; + id: string; + provider?: string | null; + updated_at?: string | null; + }; Update: { - avatar_url?: string | null - current_status_id?: string | null - email?: string | null - full_name?: string | null - id?: string - provider?: string | null - updated_at?: string | null - } + avatar_url?: string | null; + current_status_id?: string | null; + email?: string | null; + full_name?: string | null; + id?: string; + provider?: string | null; + updated_at?: string | null; + }; Relationships: [ { - foreignKeyName: "profiles_current_status_id_fkey" - columns: ["current_status_id"] - isOneToOne: false - referencedRelation: "statuses" - referencedColumns: ["id"] + foreignKeyName: 'profiles_current_status_id_fkey'; + columns: ['current_status_id']; + isOneToOne: false; + referencedRelation: 'statuses'; + referencedColumns: ['id']; }, - ] - } + ]; + }; statuses: { Row: { - created_at: string - id: string - status: string - updated_by_id: string | null - user_id: string - } + 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 - } + 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: [] - } - } + created_at?: string; + id?: string; + status?: string; + updated_by_id?: string | null; + user_id?: string; + }; + Relationships: []; + }; + }; Views: { - [_ in never]: never - } + [_ in never]: never; + }; Functions: { - [_ in never]: never - } + [_ in never]: never; + }; Enums: { - [_ in never]: never - } + [_ in never]: never; + }; CompositeTypes: { - [_ in never]: never - } - } -} + [_ in never]: never; + }; + }; +}; -type DefaultSchema = Database[Extract] +type DefaultSchema = Database[Extract]; export type Tables< DefaultSchemaTableNameOrOptions extends - | keyof (DefaultSchema["Tables"] & DefaultSchema["Views"]) + | keyof (DefaultSchema['Tables'] & DefaultSchema['Views']) | { schema: keyof Database }, TableName extends DefaultSchemaTableNameOrOptions extends { - schema: keyof Database + schema: keyof Database; } - ? keyof (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] & - Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"]) + ? 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 + ? (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 + : DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema['Tables'] & + DefaultSchema['Views']) + ? (DefaultSchema['Tables'] & + DefaultSchema['Views'])[DefaultSchemaTableNameOrOptions] extends { + Row: infer R; } ? R : never - : never + : never; export type TablesInsert< DefaultSchemaTableNameOrOptions extends - | keyof DefaultSchema["Tables"] + | keyof DefaultSchema['Tables'] | { schema: keyof Database }, TableName extends DefaultSchemaTableNameOrOptions extends { - schema: keyof Database + schema: keyof Database; } - ? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] + ? keyof Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] : never = never, > = DefaultSchemaTableNameOrOptions extends { schema: keyof Database } - ? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends { - Insert: infer I + ? Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends { + Insert: infer I; } ? I : never - : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"] - ? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends { - Insert: infer I + : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema['Tables'] + ? DefaultSchema['Tables'][DefaultSchemaTableNameOrOptions] extends { + Insert: infer I; } ? I : never - : never + : never; export type TablesUpdate< DefaultSchemaTableNameOrOptions extends - | keyof DefaultSchema["Tables"] + | keyof DefaultSchema['Tables'] | { schema: keyof Database }, TableName extends DefaultSchemaTableNameOrOptions extends { - schema: keyof Database + schema: keyof Database; } - ? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] + ? keyof Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] : never = never, > = DefaultSchemaTableNameOrOptions extends { schema: keyof Database } - ? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends { - Update: infer U + ? Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends { + Update: infer U; } ? U : never - : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"] - ? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends { - Update: infer U + : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema['Tables'] + ? DefaultSchema['Tables'][DefaultSchemaTableNameOrOptions] extends { + Update: infer U; } ? U : never - : never + : never; export type Enums< DefaultSchemaEnumNameOrOptions extends - | keyof DefaultSchema["Enums"] + | keyof DefaultSchema['Enums'] | { schema: keyof Database }, EnumName extends DefaultSchemaEnumNameOrOptions extends { - schema: keyof Database + schema: keyof Database; } - ? keyof Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"] + ? 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 + ? Database[DefaultSchemaEnumNameOrOptions['schema']]['Enums'][EnumName] + : DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema['Enums'] + ? DefaultSchema['Enums'][DefaultSchemaEnumNameOrOptions] + : never; export type CompositeTypes< PublicCompositeTypeNameOrOptions extends - | keyof DefaultSchema["CompositeTypes"] + | keyof DefaultSchema['CompositeTypes'] | { schema: keyof Database }, CompositeTypeName extends PublicCompositeTypeNameOrOptions extends { - schema: keyof Database + schema: keyof Database; } - ? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"] + ? 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 + ? Database[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'][CompositeTypeName] + : PublicCompositeTypeNameOrOptions extends keyof DefaultSchema['CompositeTypes'] + ? DefaultSchema['CompositeTypes'][PublicCompositeTypeNameOrOptions] + : never; export const Constants = { public: { Enums: {}, }, -} as const - +} as const; diff --git a/src/utils/supabase/types.ts b/src/utils/supabase/types.ts index 2dc08cc..b3da0ee 100644 --- a/src/utils/supabase/types.ts +++ b/src/utils/supabase/types.ts @@ -1,5 +1,5 @@ import type { Database } from '@/utils/supabase/database.types'; -import type { SupabaseClient as SBClient } from '@supabase/supabase-js' +import type { SupabaseClient as SBClient } from '@supabase/supabase-js'; export type SBClientWithDatabase = SBClient; @@ -11,17 +11,17 @@ export type Result = { }; export type UserRecord = { - id: string, - updated_at: string | null, - email: string | null, - full_name: string | null, - avatar_url: string | null, - provider: string | null, + id: string; + updated_at: string | null; + email: string | null; + full_name: string | null; + avatar_url: string | null; + provider: string | null; status: { - status: string, - created_at: string, - updated_by: Profile | null, - } + status: string; + created_at: string; + updated_by: Profile | null; + }; }; export type AsyncReturnType Promise> =