Format and make ban-suspicious-ips more better
This commit is contained in:
14
package.json
14
package.json
@@ -52,7 +52,7 @@
|
|||||||
"@supabase-cache-helpers/postgrest-react-query": "^1.13.4",
|
"@supabase-cache-helpers/postgrest-react-query": "^1.13.4",
|
||||||
"@supabase-cache-helpers/storage-react-query": "^1.3.5",
|
"@supabase-cache-helpers/storage-react-query": "^1.3.5",
|
||||||
"@supabase/ssr": "^0.6.1",
|
"@supabase/ssr": "^0.6.1",
|
||||||
"@supabase/supabase-js": "^2.51.0",
|
"@supabase/supabase-js": "^2.52.0",
|
||||||
"@t3-oss/env-nextjs": "^0.12.0",
|
"@t3-oss/env-nextjs": "^0.12.0",
|
||||||
"@tanstack/react-query": "^5.83.0",
|
"@tanstack/react-query": "^5.83.0",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
"import-in-the-middle": "^1.14.2",
|
"import-in-the-middle": "^1.14.2",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.522.0",
|
"lucide-react": "^0.522.0",
|
||||||
"next": "^15.4.1",
|
"next": "^15.4.2",
|
||||||
"next-plausible": "^3.12.4",
|
"next-plausible": "^3.12.4",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"postgres": "^3.4.7",
|
"postgres": "^3.4.7",
|
||||||
@@ -86,22 +86,22 @@
|
|||||||
"@tailwindcss/postcss": "^4.1.11",
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
"@types/express": "^5.0.3",
|
"@types/express": "^5.0.3",
|
||||||
"@types/node": "^20.19.8",
|
"@types/node": "^20.19.9",
|
||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.6",
|
||||||
"drizzle-kit": "^0.30.6",
|
"drizzle-kit": "^0.30.6",
|
||||||
"eslint": "^9.31.0",
|
"eslint": "^9.31.0",
|
||||||
"eslint-config-next": "^15.4.1",
|
"eslint-config-next": "^15.4.2",
|
||||||
"eslint-config-prettier": "^10.1.5",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-drizzle": "^0.2.3",
|
"eslint-plugin-drizzle": "^0.2.3",
|
||||||
"eslint-plugin-prettier": "^5.5.1",
|
"eslint-plugin-prettier": "^5.5.3",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.11",
|
||||||
"tw-animate-css": "^1.3.5",
|
"tw-animate-css": "^1.3.5",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"typescript-eslint": "^8.37.0"
|
"typescript-eslint": "^8.38.0"
|
||||||
},
|
},
|
||||||
"ct3aMetadata": {
|
"ct3aMetadata": {
|
||||||
"initVersion": "7.39.3"
|
"initVersion": "7.39.3"
|
||||||
|
@@ -15,8 +15,9 @@ const AuthSuccessPage = () => {
|
|||||||
// Small delay to ensure state is updated
|
// Small delay to ensure state is updated
|
||||||
setTimeout(() => router.push('/'), 100);
|
setTimeout(() => router.push('/'), 100);
|
||||||
};
|
};
|
||||||
handleAuthSuccess()
|
handleAuthSuccess().catch((error) =>
|
||||||
.catch(error => console.error(`Error handling auth success: ${error}`));
|
console.error(`Error handling auth success: ${error}`),
|
||||||
|
);
|
||||||
}, [refreshUser, router]);
|
}, [refreshUser, router]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@@ -4,7 +4,7 @@ import { ForgotPasswordCard } from '@/components/default/auth/cards/client';
|
|||||||
const ForgotPasswordPage = () => {
|
const ForgotPasswordPage = () => {
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col items-center min-h-[50vh]'>
|
<div className='flex flex-col items-center min-h-[50vh]'>
|
||||||
<ForgotPasswordCard cardProps={{className: 'my-auto'}}/>
|
<ForgotPasswordCard cardProps={{ className: 'my-auto' }} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -1,9 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
const ProfilePage = () => {
|
const ProfilePage = () => {
|
||||||
return (
|
return <div className='flex flex-col items-center min-h-[90vh]'></div>;
|
||||||
<div className='flex flex-col items-center min-h-[90vh]'>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
export default ProfilePage;
|
export default ProfilePage;
|
||||||
|
@@ -20,9 +20,11 @@ type GlobalErrorProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const GlobalError = ({ error, reset = undefined }: GlobalErrorProps) => {
|
const GlobalError = ({ error, reset = undefined }: GlobalErrorProps) => {
|
||||||
useEffect(() => { Sentry.captureException(error) }, [error]);
|
useEffect(() => {
|
||||||
|
Sentry.captureException(error);
|
||||||
|
}, [error]);
|
||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
lang='en'
|
lang='en'
|
||||||
className={cn('font-sans antialiased', fontSans.variable)}
|
className={cn('font-sans antialiased', fontSans.variable)}
|
||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
|
@@ -21,7 +21,7 @@ export const generateMetadata = (): Metadata => {
|
|||||||
template: '%s | Next Template',
|
template: '%s | Next Template',
|
||||||
default: 'Next Template',
|
default: 'Next Template',
|
||||||
},
|
},
|
||||||
description: 'Gib\'s Next Template',
|
description: "Gib's Next Template",
|
||||||
applicationName: 'Next Template',
|
applicationName: 'Next Template',
|
||||||
keywords: 'Next.js, Supabase, Tailwind, Tanstack, React, Query, T3, Gib',
|
keywords: 'Next.js, Supabase, Tailwind, Tanstack, React, Query, T3, Gib',
|
||||||
authors: [{ name: 'Gib', url: 'https://gbrown.org' }],
|
authors: [{ name: 'Gib', url: 'https://gbrown.org' }],
|
||||||
@@ -217,7 +217,9 @@ const RootLayout = async ({
|
|||||||
children,
|
children,
|
||||||
}: Readonly<{ children: React.ReactNode }>) => {
|
}: Readonly<{ children: React.ReactNode }>) => {
|
||||||
const client = await SupabaseServer();
|
const client = await SupabaseServer();
|
||||||
const { data: { user } } = await getCurrentUser(client);
|
const {
|
||||||
|
data: { user },
|
||||||
|
} = await getCurrentUser(client);
|
||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
lang='en'
|
lang='en'
|
||||||
@@ -241,9 +243,7 @@ const RootLayout = async ({
|
|||||||
>
|
>
|
||||||
<TVModeProvider>
|
<TVModeProvider>
|
||||||
<Header />
|
<Header />
|
||||||
<main className='min-h-[90vh]'>
|
<main className='min-h-[90vh]'>{children}</main>
|
||||||
{children}
|
|
||||||
</main>
|
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</TVModeProvider>
|
</TVModeProvider>
|
||||||
</PlausibleProvider>
|
</PlausibleProvider>
|
||||||
|
@@ -3,7 +3,7 @@ import { SignInCard } from '@/components/default/auth/cards/client';
|
|||||||
const HomePage = () => {
|
const HomePage = () => {
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col items-center min-h-[90vh]'>
|
<div className='flex flex-col items-center min-h-[90vh]'>
|
||||||
<SignInCard containerProps={{className: 'my-auto'}}/>
|
<SignInCard containerProps={{ className: 'my-auto' }} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -1,3 +1,9 @@
|
|||||||
export { SignInWithApple, type SignInWithAppleProps } from './sign-in-with-apple';
|
export {
|
||||||
export { SignInWithMicrosoft, type SignInWithMicrosoftProps } from './sign-in-with-microsoft';
|
SignInWithApple,
|
||||||
|
type SignInWithAppleProps,
|
||||||
|
} from './sign-in-with-apple';
|
||||||
|
export {
|
||||||
|
SignInWithMicrosoft,
|
||||||
|
type SignInWithMicrosoftProps,
|
||||||
|
} from './sign-in-with-microsoft';
|
||||||
export { SignInLinkButton } from './sign-in-link';
|
export { SignInLinkButton } from './sign-in-link';
|
||||||
|
@@ -25,11 +25,11 @@ export const SignInWithApple = ({
|
|||||||
formProps,
|
formProps,
|
||||||
textProps,
|
textProps,
|
||||||
iconProps,
|
iconProps,
|
||||||
} : SignInWithAppleProps) => {
|
}: SignInWithAppleProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { loading, refreshUser } = useAuth();
|
const { loading, refreshUser } = useAuth();
|
||||||
const [statusMessage, setStatusMessage] = useState('');
|
const [statusMessage, setStatusMessage] = useState('');
|
||||||
const [ isLoading, setIsLoading ] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const supabase = SupabaseClient()!;
|
const supabase = SupabaseClient()!;
|
||||||
|
|
||||||
const handleSignInWithApple = async (e: React.FormEvent) => {
|
const handleSignInWithApple = async (e: React.FormEvent) => {
|
||||||
@@ -63,12 +63,18 @@ export const SignInWithApple = ({
|
|||||||
className={cn('w-full', submitButtonProps?.className)}
|
className={cn('w-full', submitButtonProps?.className)}
|
||||||
>
|
>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<FaApple {...iconProps} className={cn('size-5', iconProps?.className)} />
|
<FaApple
|
||||||
<p {...textProps} className={cn('text-[1.0rem]', textProps?.className)} >
|
{...iconProps}
|
||||||
|
className={cn('size-5', iconProps?.className)}
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
{...textProps}
|
||||||
|
className={cn('text-[1.0rem]', textProps?.className)}
|
||||||
|
>
|
||||||
Sign In with Apple
|
Sign In with Apple
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</SubmitButton>
|
</SubmitButton>
|
||||||
{statusMessage && <StatusMessage message={{ error: statusMessage }} />}
|
{statusMessage && <StatusMessage message={{ error: statusMessage }} />}
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
@@ -25,11 +25,11 @@ export const SignInWithMicrosoft = ({
|
|||||||
formProps,
|
formProps,
|
||||||
textProps,
|
textProps,
|
||||||
iconProps,
|
iconProps,
|
||||||
} : SignInWithMicrosoftProps) => {
|
}: SignInWithMicrosoftProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { loading, refreshUser } = useAuth();
|
const { loading, refreshUser } = useAuth();
|
||||||
const [statusMessage, setStatusMessage] = useState('');
|
const [statusMessage, setStatusMessage] = useState('');
|
||||||
const [ isLoading, setIsLoading ] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const supabase = SupabaseClient()!;
|
const supabase = SupabaseClient()!;
|
||||||
|
|
||||||
const handleSignInWithMicrosoft = async (e: React.FormEvent) => {
|
const handleSignInWithMicrosoft = async (e: React.FormEvent) => {
|
||||||
@@ -63,12 +63,18 @@ export const SignInWithMicrosoft = ({
|
|||||||
className={cn('w-full', submitButtonProps?.className)}
|
className={cn('w-full', submitButtonProps?.className)}
|
||||||
>
|
>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<FaMicrosoft {...iconProps} className={cn('size-5', iconProps?.className)} />
|
<FaMicrosoft
|
||||||
<p {...textProps} className={cn('text-[1.0rem]', textProps?.className)}>
|
{...iconProps}
|
||||||
|
className={cn('size-5', iconProps?.className)}
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
{...textProps}
|
||||||
|
className={cn('text-[1.0rem]', textProps?.className)}
|
||||||
|
>
|
||||||
Sign In with Microsoft
|
Sign In with Microsoft
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</SubmitButton>
|
</SubmitButton>
|
||||||
{statusMessage && <StatusMessage message={{ error: statusMessage }} />}
|
{statusMessage && <StatusMessage message={{ error: statusMessage }} />}
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
@@ -1,12 +1,15 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { SubmitButton, type SubmitButtonProps } from '@/components/default/forms';
|
import {
|
||||||
|
SubmitButton,
|
||||||
|
type SubmitButtonProps,
|
||||||
|
} from '@/components/default/forms';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useAuth } from '@/lib/hooks/context';
|
import { useAuth } from '@/lib/hooks/context';
|
||||||
import { signOut } from '@/lib/queries';
|
import { signOut } from '@/lib/queries';
|
||||||
import { SupabaseClient } from '@/utils/supabase';
|
import { SupabaseClient } from '@/utils/supabase';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
type SignOutProps = Omit<SubmitButtonProps, 'disabled' | 'onClick'>
|
type SignOutProps = Omit<SubmitButtonProps, 'disabled' | 'onClick'>;
|
||||||
|
|
||||||
export const SignOut = ({
|
export const SignOut = ({
|
||||||
className,
|
className,
|
||||||
@@ -38,7 +41,7 @@ export const SignOut = ({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'text-[1.0rem] font-semibold \
|
'text-[1.0rem] font-semibold \
|
||||||
hover:bg-red-700/60 dark:hover:bg-red-300/80',
|
hover:bg-red-700/60 dark:hover:bg-red-300/80',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Sign Out
|
Sign Out
|
||||||
|
@@ -21,7 +21,7 @@ export const SignInWithApple = async ({
|
|||||||
formProps,
|
formProps,
|
||||||
textProps,
|
textProps,
|
||||||
iconProps,
|
iconProps,
|
||||||
} : SignInWithAppleProps) => {
|
}: SignInWithAppleProps) => {
|
||||||
const supabase = await SupabaseServer();
|
const supabase = await SupabaseServer();
|
||||||
|
|
||||||
const handleSignInWithApple = async () => {
|
const handleSignInWithApple = async () => {
|
||||||
@@ -29,7 +29,9 @@ export const SignInWithApple = async ({
|
|||||||
if (!supabase) throw new Error('Supabase client not found');
|
if (!supabase) throw new Error('Supabase client not found');
|
||||||
const result = await signInWithApple(supabase);
|
const result = await signInWithApple(supabase);
|
||||||
if (result.error)
|
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;
|
else if (result.data.url) window.location.href = result.data.url;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -48,8 +50,14 @@ export const SignInWithApple = async ({
|
|||||||
className={cn('w-full', submitButtonProps?.className)}
|
className={cn('w-full', submitButtonProps?.className)}
|
||||||
>
|
>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<FaApple {...iconProps} className={cn('size-5', iconProps?.className)} />
|
<FaApple
|
||||||
<p {...textProps} className={cn('text-[1.0rem]', textProps?.className)}>
|
{...iconProps}
|
||||||
|
className={cn('size-5', iconProps?.className)}
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
{...textProps}
|
||||||
|
className={cn('text-[1.0rem]', textProps?.className)}
|
||||||
|
>
|
||||||
Sign In with Apple
|
Sign In with Apple
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -21,7 +21,7 @@ export const SignInWithMicrosoft = async ({
|
|||||||
formProps,
|
formProps,
|
||||||
textProps,
|
textProps,
|
||||||
iconProps,
|
iconProps,
|
||||||
} : SignInWithMicrosoftProps) => {
|
}: SignInWithMicrosoftProps) => {
|
||||||
const supabase = await SupabaseServer();
|
const supabase = await SupabaseServer();
|
||||||
|
|
||||||
const handleSignInWithMicrosoft = async () => {
|
const handleSignInWithMicrosoft = async () => {
|
||||||
@@ -29,7 +29,9 @@ export const SignInWithMicrosoft = async ({
|
|||||||
if (!supabase) throw new Error('Supabase client not found');
|
if (!supabase) throw new Error('Supabase client not found');
|
||||||
const result = await signInWithMicrosoft(supabase);
|
const result = await signInWithMicrosoft(supabase);
|
||||||
if (result.error)
|
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;
|
else if (result.data.url) window.location.href = result.data.url;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -48,8 +50,14 @@ export const SignInWithMicrosoft = async ({
|
|||||||
className={cn('w-full', submitButtonProps?.className)}
|
className={cn('w-full', submitButtonProps?.className)}
|
||||||
>
|
>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<FaMicrosoft {...iconProps} className={cn('size-5', iconProps?.className)} />
|
<FaMicrosoft
|
||||||
<p {...textProps} className={cn('text-[1.0rem]', textProps?.className)}>
|
{...iconProps}
|
||||||
|
className={cn('size-5', iconProps?.className)}
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
{...textProps}
|
||||||
|
className={cn('text-[1.0rem]', textProps?.className)}
|
||||||
|
>
|
||||||
Sign In with Microsoft
|
Sign In with Microsoft
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,12 +1,18 @@
|
|||||||
'use server';
|
'use server';
|
||||||
import 'server-only';
|
import 'server-only';
|
||||||
import { redirect } from 'next/navigation';
|
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 { signOut } from '@/lib/queries';
|
||||||
import { SupabaseServer } from '@/utils/supabase';
|
import { SupabaseServer } from '@/utils/supabase';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
type SignOutProps = Omit<SubmitButtonProps, 'disabled' | 'onClick' | 'formAction'>
|
type SignOutProps = Omit<
|
||||||
|
SubmitButtonProps,
|
||||||
|
'disabled' | 'onClick' | 'formAction'
|
||||||
|
>;
|
||||||
|
|
||||||
export const SignOut = async ({
|
export const SignOut = async ({
|
||||||
className,
|
className,
|
||||||
@@ -35,7 +41,7 @@ export const SignOut = async ({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'text-[1.0rem] font-semibold \
|
'text-[1.0rem] font-semibold \
|
||||||
hover:bg-red-700/60 dark:hover:bg-red-300/80',
|
hover:bg-red-700/60 dark:hover:bg-red-300/80',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Sign Out
|
Sign Out
|
||||||
|
@@ -27,7 +27,7 @@ import { cn } from '@/lib/utils';
|
|||||||
|
|
||||||
const forgotPasswordFormSchema = z.object({
|
const forgotPasswordFormSchema = z.object({
|
||||||
email: z.string().email({
|
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(() => {
|
useEffect(() => {
|
||||||
if (isAuthenticated) router.push('/')
|
if (isAuthenticated) router.push('/');
|
||||||
}, [isAuthenticated, router]);
|
}, [isAuthenticated, router]);
|
||||||
|
|
||||||
const handleForgotPassword = async (values: z.infer<typeof forgotPasswordFormSchema>) => {
|
const handleForgotPassword = async (
|
||||||
|
values: z.infer<typeof forgotPasswordFormSchema>,
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
setStatusMessage('');
|
setStatusMessage('');
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
@@ -100,7 +102,10 @@ export const ForgotPasswordCard = ({
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription
|
<CardDescription
|
||||||
{...cardDescriptionProps}
|
{...cardDescriptionProps}
|
||||||
className={cn('text-sm text-foreground', cardDescriptionProps?.className)}
|
className={cn(
|
||||||
|
'text-sm text-foreground',
|
||||||
|
cardDescriptionProps?.className,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
Don't have an account?{' '}
|
Don't have an account?{' '}
|
||||||
<Link
|
<Link
|
||||||
@@ -117,7 +122,10 @@ export const ForgotPasswordCard = ({
|
|||||||
<form
|
<form
|
||||||
{...formProps}
|
{...formProps}
|
||||||
onSubmit={form.handleSubmit(handleForgotPassword)}
|
onSubmit={form.handleSubmit(handleForgotPassword)}
|
||||||
className={cn('flex flex-col min-w-64 space-y-6', formProps?.className)}
|
className={cn(
|
||||||
|
'flex flex-col min-w-64 space-y-6',
|
||||||
|
formProps?.className,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -141,10 +149,7 @@ export const ForgotPasswordCard = ({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<SubmitButton
|
<SubmitButton disabled={loading} {...buttonProps}>
|
||||||
disabled={loading}
|
|
||||||
{...buttonProps}
|
|
||||||
>
|
|
||||||
Reset Password
|
Reset Password
|
||||||
</SubmitButton>
|
</SubmitButton>
|
||||||
{statusMessage &&
|
{statusMessage &&
|
||||||
|
@@ -36,10 +36,10 @@ import { cn } from '@/lib/utils';
|
|||||||
|
|
||||||
const signInFormSchema = z.object({
|
const signInFormSchema = z.object({
|
||||||
email: z.string().email({
|
email: z.string().email({
|
||||||
message: 'Please enter a valid email address.'
|
message: 'Please enter a valid email address.',
|
||||||
}),
|
}),
|
||||||
password: z.string().min(8, {
|
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<typeof Card>;
|
cardProps?: ComponentProps<typeof Card>;
|
||||||
formProps?: Omit<ComponentProps<'form'>, 'onSubmit'>;
|
formProps?: Omit<ComponentProps<'form'>, 'onSubmit'>;
|
||||||
formLabelProps?: ComponentProps<typeof FormLabel>;
|
formLabelProps?: ComponentProps<typeof FormLabel>;
|
||||||
submitButtonProps?: Omit<ComponentProps<typeof SubmitButton>,
|
submitButtonProps?: Omit<
|
||||||
'pendingText' | 'disabled'>;
|
ComponentProps<typeof SubmitButton>,
|
||||||
|
'pendingText' | 'disabled'
|
||||||
|
>;
|
||||||
signInWithAppleProps?: SignInWithAppleProps;
|
signInWithAppleProps?: SignInWithAppleProps;
|
||||||
signInWithMicrosoftProps?: SignInWithMicrosoftProps;
|
signInWithMicrosoftProps?: SignInWithMicrosoftProps;
|
||||||
};
|
};
|
||||||
@@ -89,7 +91,6 @@ export const SignInCard = ({
|
|||||||
signInWithAppleProps,
|
signInWithAppleProps,
|
||||||
signInWithMicrosoftProps,
|
signInWithMicrosoftProps,
|
||||||
}: SignInCardProps) => {
|
}: SignInCardProps) => {
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { isAuthenticated, loading, refreshUser } = useAuth();
|
const { isAuthenticated, loading, refreshUser } = useAuth();
|
||||||
const [statusMessage, setStatusMessage] = useState('');
|
const [statusMessage, setStatusMessage] = useState('');
|
||||||
@@ -154,10 +155,7 @@ export const SignInCard = ({
|
|||||||
{...containerProps}
|
{...containerProps}
|
||||||
className={cn('p-4 bg-card/25 min-h-[720px]', containerProps?.className)}
|
className={cn('p-4 bg-card/25 min-h-[720px]', containerProps?.className)}
|
||||||
>
|
>
|
||||||
<Tabs
|
<Tabs {...tabsProps} className={cn('items-center', tabsProps?.className)}>
|
||||||
{...tabsProps}
|
|
||||||
className={cn('items-center', tabsProps?.className)}
|
|
||||||
>
|
|
||||||
<TabsList
|
<TabsList
|
||||||
{...tabsListProps}
|
{...tabsListProps}
|
||||||
className={cn('py-6', tabsListProps?.className)}
|
className={cn('py-6', tabsListProps?.className)}
|
||||||
@@ -167,7 +165,7 @@ export const SignInCard = ({
|
|||||||
{...tabsTriggerProps}
|
{...tabsTriggerProps}
|
||||||
className={cn(
|
className={cn(
|
||||||
'p-6 text-2xl font-bold cursor-pointer',
|
'p-6 text-2xl font-bold cursor-pointer',
|
||||||
tabsTriggerProps?.className
|
tabsTriggerProps?.className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Sign In
|
Sign In
|
||||||
@@ -196,7 +194,10 @@ export const SignInCard = ({
|
|||||||
<form
|
<form
|
||||||
onSubmit={signInForm.handleSubmit(handleSignIn)}
|
onSubmit={signInForm.handleSubmit(handleSignIn)}
|
||||||
{...formProps}
|
{...formProps}
|
||||||
className={cn('flex flex-col space-y-6', formProps?.className)}
|
className={cn(
|
||||||
|
'flex flex-col space-y-6',
|
||||||
|
formProps?.className,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={signInForm.control}
|
control={signInForm.control}
|
||||||
@@ -232,11 +233,7 @@ export const SignInCard = ({
|
|||||||
>
|
>
|
||||||
Password
|
Password
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<Link
|
<Link href='/forgot-password'>Forgot Password?</Link>
|
||||||
href='/forgot-password'
|
|
||||||
>
|
|
||||||
Forgot Password?
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
@@ -257,14 +254,14 @@ export const SignInCard = ({
|
|||||||
<StatusMessage message={{ error: statusMessage }} />
|
<StatusMessage message={{ error: statusMessage }} />
|
||||||
) : (
|
) : (
|
||||||
<StatusMessage message={{ message: statusMessage }} />
|
<StatusMessage message={{ message: statusMessage }} />
|
||||||
))}
|
))}
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
pendingText='Signing In...'
|
pendingText='Signing In...'
|
||||||
{...submitButtonProps}
|
{...submitButtonProps}
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-lg font-semibold w-2/3 mx-auto',
|
'text-lg font-semibold w-2/3 mx-auto',
|
||||||
submitButtonProps?.className
|
submitButtonProps?.className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Sign In
|
Sign In
|
||||||
@@ -278,10 +275,11 @@ export const SignInCard = ({
|
|||||||
</div>
|
</div>
|
||||||
<SignInWithMicrosoft
|
<SignInWithMicrosoft
|
||||||
{...signInWithMicrosoftProps}
|
{...signInWithMicrosoftProps}
|
||||||
submitButtonProps = {{
|
submitButtonProps={{
|
||||||
className: cn(
|
className: cn(
|
||||||
'flex w-5/6 m-auto',
|
'flex w-5/6 m-auto',
|
||||||
signInWithMicrosoftProps?.submitButtonProps?.className),
|
signInWithMicrosoftProps?.submitButtonProps?.className,
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
textProps={{
|
textProps={{
|
||||||
...signInWithMicrosoftProps?.textProps,
|
...signInWithMicrosoftProps?.textProps,
|
||||||
@@ -300,10 +298,11 @@ export const SignInCard = ({
|
|||||||
/>
|
/>
|
||||||
<SignInWithApple
|
<SignInWithApple
|
||||||
{...signInWithAppleProps}
|
{...signInWithAppleProps}
|
||||||
submitButtonProps = {{
|
submitButtonProps={{
|
||||||
className: cn(
|
className: cn(
|
||||||
'flex w-5/6 m-auto',
|
'flex w-5/6 m-auto',
|
||||||
signInWithAppleProps?.submitButtonProps?.className),
|
signInWithAppleProps?.submitButtonProps?.className,
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
textProps={{
|
textProps={{
|
||||||
...signInWithAppleProps?.textProps,
|
...signInWithAppleProps?.textProps,
|
||||||
@@ -334,7 +333,10 @@ export const SignInCard = ({
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<Form {...signUpForm}>
|
<Form {...signUpForm}>
|
||||||
<form
|
<form
|
||||||
className={cn('flex flex-col space-y-6', formProps?.className)}
|
className={cn(
|
||||||
|
'flex flex-col space-y-6',
|
||||||
|
formProps?.className,
|
||||||
|
)}
|
||||||
onSubmit={signUpForm.handleSubmit(handleSignUp)}
|
onSubmit={signUpForm.handleSubmit(handleSignUp)}
|
||||||
{...formProps}
|
{...formProps}
|
||||||
>
|
>
|
||||||
@@ -430,14 +432,14 @@ export const SignInCard = ({
|
|||||||
<StatusMessage message={{ error: statusMessage }} />
|
<StatusMessage message={{ error: statusMessage }} />
|
||||||
) : (
|
) : (
|
||||||
<StatusMessage message={{ success: statusMessage }} />
|
<StatusMessage message={{ success: statusMessage }} />
|
||||||
))}
|
))}
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
pendingText='Signing Up...'
|
pendingText='Signing Up...'
|
||||||
{...submitButtonProps}
|
{...submitButtonProps}
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-lg font-semibold w-2/3 mx-auto',
|
'text-lg font-semibold w-2/3 mx-auto',
|
||||||
submitButtonProps?.className
|
submitButtonProps?.className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Sign Up
|
Sign Up
|
||||||
@@ -451,10 +453,11 @@ export const SignInCard = ({
|
|||||||
</div>
|
</div>
|
||||||
<SignInWithMicrosoft
|
<SignInWithMicrosoft
|
||||||
{...signInWithMicrosoftProps}
|
{...signInWithMicrosoftProps}
|
||||||
submitButtonProps = {{
|
submitButtonProps={{
|
||||||
className: cn(
|
className: cn(
|
||||||
'flex w-5/6 m-auto',
|
'flex w-5/6 m-auto',
|
||||||
signInWithMicrosoftProps?.submitButtonProps?.className),
|
signInWithMicrosoftProps?.submitButtonProps?.className,
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
textProps={{
|
textProps={{
|
||||||
className: cn(
|
className: cn(
|
||||||
@@ -471,10 +474,11 @@ export const SignInCard = ({
|
|||||||
/>
|
/>
|
||||||
<SignInWithApple
|
<SignInWithApple
|
||||||
{...signInWithAppleProps}
|
{...signInWithAppleProps}
|
||||||
submitButtonProps = {{
|
submitButtonProps={{
|
||||||
className: cn(
|
className: cn(
|
||||||
'flex w-5/6 m-auto',
|
'flex w-5/6 m-auto',
|
||||||
signInWithAppleProps?.submitButtonProps?.className),
|
signInWithAppleProps?.submitButtonProps?.className,
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
textProps={{
|
textProps={{
|
||||||
className: cn(
|
className: cn(
|
||||||
@@ -496,4 +500,3 @@ export const SignInCard = ({
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -2,11 +2,7 @@
|
|||||||
import { useFileUpload } from '@/lib/hooks';
|
import { useFileUpload } from '@/lib/hooks';
|
||||||
import { useAuth } from '@/lib/hooks/context';
|
import { useAuth } from '@/lib/hooks/context';
|
||||||
import { SupabaseClient } from '@/utils/supabase';
|
import { SupabaseClient } from '@/utils/supabase';
|
||||||
import {
|
import { BasedAvatar, Card, CardContent } from '@/components/ui';
|
||||||
BasedAvatar,
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
} from '@/components/ui';
|
|
||||||
import { Loader2, Pencil, Upload } from 'lucide-react';
|
import { Loader2, Pencil, Upload } from 'lucide-react';
|
||||||
import type { ComponentProps, ChangeEvent } from 'react';
|
import type { ComponentProps, ChangeEvent } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -34,11 +30,10 @@ export const AvatarUpload = ({
|
|||||||
}: AvatarUploadProps) => {
|
}: AvatarUploadProps) => {
|
||||||
const { profile, isAuthenticated } = useAuth();
|
const { profile, isAuthenticated } = useAuth();
|
||||||
const client = SupabaseClient()!;
|
const client = SupabaseClient()!;
|
||||||
const {
|
const { isUploading, fileInputRef, uploadAvatarMutation } = useFileUpload(
|
||||||
isUploading,
|
client,
|
||||||
fileInputRef,
|
'avatars',
|
||||||
uploadAvatarMutation
|
);
|
||||||
} = useFileUpload(client, 'avatars');
|
|
||||||
|
|
||||||
const handleAvatarClick = () => {
|
const handleAvatarClick = () => {
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
@@ -54,11 +49,12 @@ export const AvatarUpload = ({
|
|||||||
if (!file) throw new Error('No file selected!');
|
if (!file) throw new Error('No file selected!');
|
||||||
if (!client) throw new Error('Supabase client not found!');
|
if (!client) throw new Error('Supabase client not found!');
|
||||||
if (!isAuthenticated) throw new Error('User is not authenticated!');
|
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!');
|
if (file.size > 8 * 1024 * 1024) throw new Error('File is too large!');
|
||||||
|
|
||||||
const avatarPath = profile?.avatar_url ??
|
const avatarPath =
|
||||||
`${profile?.id}.${file.name.split('.').pop()}`;
|
profile?.avatar_url ?? `${profile?.id}.${file.name.split('.').pop()}`;
|
||||||
|
|
||||||
const avatarUrl = await uploadAvatarMutation.mutateAsync({
|
const avatarUrl = await uploadAvatarMutation.mutateAsync({
|
||||||
file,
|
file,
|
||||||
@@ -70,26 +66,25 @@ export const AvatarUpload = ({
|
|||||||
replace: avatarPath,
|
replace: avatarPath,
|
||||||
});
|
});
|
||||||
if (avatarUrl) await onAvatarUploaded(avatarUrl);
|
if (avatarUrl) await onAvatarUploaded(avatarUrl);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(`Error: ${error as string}`);
|
toast.error(`Error: ${error as string}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card {...cardProps} className={cn('', cardProps?.className)}>
|
||||||
{...cardProps}
|
|
||||||
className={cn('', cardProps?.className)}
|
|
||||||
>
|
|
||||||
<CardContent
|
<CardContent
|
||||||
{...cardContentProps}
|
{...cardContentProps}
|
||||||
className={cn('flex flex-col items-center', cardContentProps?.className)}
|
className={cn(
|
||||||
|
'flex flex-col items-center',
|
||||||
|
cardContentProps?.className,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
{...containerProps}
|
{...containerProps}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative group cursor-pointer mb-4',
|
'relative group cursor-pointer mb-4',
|
||||||
containerProps?.className
|
containerProps?.className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<BasedAvatar
|
<BasedAvatar
|
||||||
@@ -104,13 +99,15 @@ export const AvatarUpload = ({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'absolute inset-0 rounded-full bg-black/0\
|
'absolute inset-0 rounded-full bg-black/0\
|
||||||
group-hover:bg-black/50 transition-all flex\
|
group-hover:bg-black/50 transition-all flex\
|
||||||
items-center justify-center'
|
items-center justify-center',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Upload
|
<Upload
|
||||||
{...iconProps}
|
{...iconProps}
|
||||||
className={cn('text-white opacity-0 group-hover:opacity-100\
|
className={cn(
|
||||||
transition-opacity', iconProps?.className
|
'text-white opacity-0 group-hover:opacity-100\
|
||||||
|
transition-opacity',
|
||||||
|
iconProps?.className,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -124,7 +121,8 @@ export const AvatarUpload = ({
|
|||||||
{...iconProps}
|
{...iconProps}
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-white opacity-100 group-hover:opacity-0\
|
'text-white opacity-100 group-hover:opacity-0\
|
||||||
transition-opacity', iconProps?.className
|
transition-opacity',
|
||||||
|
iconProps?.className,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -28,9 +28,7 @@ type ProfileFormProps = {
|
|||||||
onSubmit: (values: z.infer<typeof formSchema>) => Promise<void>;
|
onSubmit: (values: z.infer<typeof formSchema>) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ProfileForm = ({
|
export const ProfileForm = ({ onSubmit }: ProfileFormProps) => {
|
||||||
onSubmit,
|
|
||||||
}: ProfileFormProps) => {
|
|
||||||
const { profile, loading } = useAuth();
|
const { profile, loading } = useAuth();
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
@@ -48,14 +46,10 @@ export const ProfileForm = ({
|
|||||||
});
|
});
|
||||||
}, [profile, form]);
|
}, [profile, form]);
|
||||||
|
|
||||||
const handleSubmit = async (values: z.infer<typeof formSchema>) => {
|
|
||||||
await onSubmit(values);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(handleSubmit)} className='space-y-6'>
|
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='full_name'
|
name='full_name'
|
||||||
|
@@ -3,9 +3,24 @@ import { z } from 'zod';
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useAuth } from '@/lib/hooks/context';
|
import { useAuth } from '@/lib/hooks/context';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useState } from 'react';
|
import { useState, type ComponentProps } from 'react';
|
||||||
import { StatusMessage, SubmitButton } from '@/components/default/forms';
|
import { StatusMessage, SubmitButton } from '@/components/default/forms';
|
||||||
import { type Result } from '@/utils/supabase/types'
|
import { type Result } from '@/utils/supabase/types';
|
||||||
|
import {
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
Input,
|
||||||
|
} from '@/components/ui';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const formSchema = z
|
const formSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -20,13 +35,35 @@ const formSchema = z
|
|||||||
});
|
});
|
||||||
|
|
||||||
type ResetPasswordFormProps = {
|
type ResetPasswordFormProps = {
|
||||||
onSubmit: (formData: FormData) => Promise<Result<null>>;
|
onSubmit: (values: z.infer<typeof formSchema>) => Promise<Result<null>>;
|
||||||
message?: string;
|
message?: string;
|
||||||
|
cardHeaderProps?: ComponentProps<typeof CardHeader>;
|
||||||
|
cardTitleProps?: ComponentProps<typeof CardTitle>;
|
||||||
|
cardDescriptionProps?: ComponentProps<typeof CardDescription>;
|
||||||
|
cardContentProps?: ComponentProps<typeof CardContent>;
|
||||||
|
formProps?: Omit<ComponentProps<'form'>, 'onSubmit'>;
|
||||||
|
formLabelProps?: ComponentProps<typeof FormLabel>;
|
||||||
|
inputProps?: Omit<ComponentProps<typeof Input>, 'type'>;
|
||||||
|
formDescriptionProps?: ComponentProps<typeof FormDescription>;
|
||||||
|
formMessageProps?: ComponentProps<typeof FormMessage>;
|
||||||
|
statusMessageProps?: Omit<ComponentProps<typeof StatusMessage>, 'message'>;
|
||||||
|
submitButtonProps?: Omit<ComponentProps<typeof SubmitButton>, 'disabled'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ResetPasswordForm = ({
|
export const ResetPasswordForm = ({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
message,
|
message,
|
||||||
|
cardHeaderProps,
|
||||||
|
cardTitleProps,
|
||||||
|
cardDescriptionProps,
|
||||||
|
cardContentProps,
|
||||||
|
formProps,
|
||||||
|
formLabelProps,
|
||||||
|
inputProps,
|
||||||
|
formDescriptionProps,
|
||||||
|
formMessageProps,
|
||||||
|
statusMessageProps,
|
||||||
|
submitButtonProps,
|
||||||
}: ResetPasswordFormProps) => {
|
}: ResetPasswordFormProps) => {
|
||||||
const { loading } = useAuth();
|
const { loading } = useAuth();
|
||||||
const [statusMessage, setStatusMessage] = useState(message ?? '');
|
const [statusMessage, setStatusMessage] = useState(message ?? '');
|
||||||
@@ -41,14 +78,96 @@ export const ResetPasswordForm = ({
|
|||||||
|
|
||||||
const handleSubmit = async (values: z.infer<typeof formSchema>) => {
|
const handleSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const { error } = await onSubmit(values);
|
||||||
formData.append('password', values.password);
|
if (error) throw new Error(error.message);
|
||||||
formData.append('confirmPassword', values.confirmPassword);
|
setStatusMessage('Password reset successfully.');
|
||||||
await onSubmit(formData);
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
setStatusMessage(`Error: ${error as string}`);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CardHeader
|
||||||
|
{...cardHeaderProps}
|
||||||
|
className={cn('pb-5', cardHeaderProps?.className)}
|
||||||
|
>
|
||||||
|
<CardTitle
|
||||||
|
{...cardTitleProps}
|
||||||
|
className={cn('text-2xl font-semibold', cardTitleProps?.className)}
|
||||||
|
>
|
||||||
|
Change Password
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription {...cardDescriptionProps}>
|
||||||
|
Update your password to keep your account secure
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent {...cardContentProps}>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
{...formProps}
|
||||||
|
onSubmit={form.handleSubmit(handleSubmit)}
|
||||||
|
className={cn('space-y-6', formProps?.className)}
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='password'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel {...formLabelProps}>New Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...inputProps} type='password' {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription {...formDescriptionProps}>
|
||||||
|
Enter your new password. Must be at least 8 characters.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage {...formMessageProps} />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='confirmPassword'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel {...formLabelProps}>Confirm Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...inputProps} type='password' {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription {...formDescriptionProps}>
|
||||||
|
Please re-enter your new password to confirm.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage {...formMessageProps} />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{statusMessage &&
|
||||||
|
(statusMessage.includes('Error') ||
|
||||||
|
statusMessage.includes('error') ||
|
||||||
|
statusMessage.includes('failed') ||
|
||||||
|
statusMessage.includes('invalid') ? (
|
||||||
|
<StatusMessage
|
||||||
|
{...statusMessageProps}
|
||||||
|
message={{ error: statusMessage }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<StatusMessage
|
||||||
|
{...statusMessageProps}
|
||||||
|
message={{ message: statusMessage }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<div className='flex justify-center'>
|
||||||
|
<SubmitButton
|
||||||
|
disabled={loading}
|
||||||
|
pendingText='Updating Password...'
|
||||||
|
{...submitButtonProps}
|
||||||
|
>
|
||||||
|
Update Password
|
||||||
|
</SubmitButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</CardContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@@ -1,10 +1,7 @@
|
|||||||
import { type ComponentProps } from 'react';
|
import { type ComponentProps } from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
type Message =
|
type Message = { success: string } | { error: string } | { message: string };
|
||||||
| { success: string}
|
|
||||||
| { error: string}
|
|
||||||
| { message: string}
|
|
||||||
|
|
||||||
type StatusMessageProps = {
|
type StatusMessageProps = {
|
||||||
message: Message;
|
message: Message;
|
||||||
@@ -30,9 +27,9 @@ export const StatusMessage = ({
|
|||||||
<div
|
<div
|
||||||
{...textProps}
|
{...textProps}
|
||||||
className={cn(
|
className={cn(
|
||||||
'dark:text-green-500 text-green-700',
|
'dark:text-green-500 text-green-700',
|
||||||
textProps?.className
|
textProps?.className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{message.success}
|
{message.success}
|
||||||
</div>
|
</div>
|
||||||
@@ -45,13 +42,7 @@ export const StatusMessage = ({
|
|||||||
{message.error}
|
{message.error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{'message' in message && (
|
{'message' in message && <div {...textProps}>{message.message}</div>}
|
||||||
<div
|
|
||||||
{...textProps}
|
|
||||||
>
|
|
||||||
{message.message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -44,7 +44,7 @@ export const SubmitButton = ({
|
|||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
children
|
children
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@@ -1,7 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
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 { cn } from '@/lib/utils';
|
||||||
import { AvatarDropdown } from './avatar-dropdown';
|
import { AvatarDropdown } from './avatar-dropdown';
|
||||||
import { type ComponentProps } from 'react';
|
import { type ComponentProps } from 'react';
|
||||||
@@ -11,10 +15,7 @@ type Props = {
|
|||||||
themeToggleProps?: ThemeToggleProps;
|
themeToggleProps?: ThemeToggleProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Header = ({
|
const Header = ({ headerProps, themeToggleProps }: Props) => {
|
||||||
headerProps,
|
|
||||||
themeToggleProps,
|
|
||||||
}: Props) => {
|
|
||||||
const { isAuthenticated } = useAuth();
|
const { isAuthenticated } = useAuth();
|
||||||
|
|
||||||
const Controls = () => (
|
const Controls = () => (
|
||||||
@@ -28,7 +29,7 @@ const Header = ({
|
|||||||
...themeToggleProps?.buttonProps,
|
...themeToggleProps?.buttonProps,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{isAuthenticated && ( <AvatarDropdown /> )}
|
{isAuthenticated && <AvatarDropdown />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -41,7 +42,6 @@ const Header = ({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className='flex items-center justify-between'>
|
<div className='flex items-center justify-between'>
|
||||||
|
|
||||||
{/* Left spacer for perfect centering */}
|
{/* Left spacer for perfect centering */}
|
||||||
<div className='flex flex-1 justify-start'>
|
<div className='flex flex-1 justify-start'>
|
||||||
<div className='sm:w-[120px] md:w-[160px]' />
|
<div className='sm:w-[120px] md:w-[160px]' />
|
||||||
@@ -76,10 +76,8 @@ const Header = ({
|
|||||||
<div className='flex-1 flex justify-end'>
|
<div className='flex-1 flex justify-end'>
|
||||||
<Controls />
|
<Controls />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|
||||||
};
|
};
|
||||||
export default Header;
|
export default Header;
|
||||||
|
@@ -34,7 +34,11 @@ const BasedAvatar = ({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{src ? (
|
{src ? (
|
||||||
<AvatarImage {...imageProps} src={src} className={imageProps?.className} />
|
<AvatarImage
|
||||||
|
{...imageProps}
|
||||||
|
src={src}
|
||||||
|
className={imageProps?.className}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<AvatarPrimitive.Fallback
|
<AvatarPrimitive.Fallback
|
||||||
{...fallbackProps}
|
{...fallbackProps}
|
||||||
@@ -51,7 +55,10 @@ const BasedAvatar = ({
|
|||||||
.join('')
|
.join('')
|
||||||
.toUpperCase()
|
.toUpperCase()
|
||||||
) : (
|
) : (
|
||||||
<User {...userIconProps} className={cn('', userIconProps?.className)} />
|
<User
|
||||||
|
{...userIconProps}
|
||||||
|
className={cn('', userIconProps?.className)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</AvatarPrimitive.Fallback>
|
</AvatarPrimitive.Fallback>
|
||||||
)}
|
)}
|
||||||
|
@@ -1,10 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React, {
|
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||||
createContext,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useState
|
|
||||||
} from 'react';
|
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
useQuery as useSupabaseQuery,
|
useQuery as useSupabaseQuery,
|
||||||
@@ -40,13 +35,12 @@ export const AuthContextProvider = ({
|
|||||||
const [user, setUser] = useState<User | null>(initialUser ?? null);
|
const [user, setUser] = useState<User | null>(initialUser ?? null);
|
||||||
|
|
||||||
// User query with initial data
|
// User query with initial data
|
||||||
const {
|
const { data: userData, isLoading: userLoading } = useQuery({
|
||||||
data: userData,
|
|
||||||
isLoading: userLoading,
|
|
||||||
} = useQuery({
|
|
||||||
queryKey: ['auth', 'user'],
|
queryKey: ['auth', 'user'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data: { user } } = await supabase.auth.getUser();
|
const {
|
||||||
|
data: { user },
|
||||||
|
} = await supabase.auth.getUser();
|
||||||
return user;
|
return user;
|
||||||
},
|
},
|
||||||
initialData: initialUser,
|
initialData: initialUser,
|
||||||
@@ -54,10 +48,7 @@ export const AuthContextProvider = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Profile query using Supabase Cache Helpers
|
// Profile query using Supabase Cache Helpers
|
||||||
const {
|
const { data: profileData, isLoading: profileLoading } = useSupabaseQuery(
|
||||||
data: profileData,
|
|
||||||
isLoading: profileLoading,
|
|
||||||
} = useSupabaseQuery(
|
|
||||||
supabase
|
supabase
|
||||||
.from('profiles')
|
.from('profiles')
|
||||||
.select('*')
|
.select('*')
|
||||||
@@ -65,7 +56,7 @@ export const AuthContextProvider = ({
|
|||||||
.single(),
|
.single(),
|
||||||
{
|
{
|
||||||
enabled: !!userData?.id,
|
enabled: !!userData?.id,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update profile mutation
|
// Update profile mutation
|
||||||
@@ -75,21 +66,26 @@ export const AuthContextProvider = ({
|
|||||||
'*',
|
'*',
|
||||||
{
|
{
|
||||||
onSuccess: () => toast.success('Profile updated successfully!'),
|
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
|
// Auth state listener
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { data: { subscription } } = supabase.auth.onAuthStateChange(
|
const {
|
||||||
async (event, session) => {
|
data: { subscription },
|
||||||
setUser(session?.user ?? null);
|
} = supabase.auth.onAuthStateChange(async (event, session) => {
|
||||||
|
setUser(session?.user ?? null);
|
||||||
|
|
||||||
if (event === 'SIGNED_IN' || event === 'SIGNED_OUT' || event === 'TOKEN_REFRESHED') {
|
if (
|
||||||
await queryClient.invalidateQueries({ queryKey: ['auth'] });
|
event === 'SIGNED_IN' ||
|
||||||
}
|
event === 'SIGNED_OUT' ||
|
||||||
|
event === 'TOKEN_REFRESHED'
|
||||||
|
) {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['auth'] });
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
return () => subscription.unsubscribe();
|
return () => subscription.unsubscribe();
|
||||||
}, [supabase.auth, queryClient]);
|
}, [supabase.auth, queryClient]);
|
||||||
@@ -116,11 +112,7 @@ export const AuthContextProvider = ({
|
|||||||
refreshUser,
|
refreshUser,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||||
<AuthContext.Provider value={value}>
|
|
||||||
{children}
|
|
||||||
</AuthContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAuth = () => {
|
export const useAuth = () => {
|
||||||
|
@@ -14,17 +14,18 @@ const enum QueryErrorCodes {
|
|||||||
FETCH_AVATAR_FAILED = 'FETCH_AVATAR_FAILED',
|
FETCH_AVATAR_FAILED = 'FETCH_AVATAR_FAILED',
|
||||||
UPDATE_PROFILE_FAILED = 'UPDATE_PROFILE_FAILED',
|
UPDATE_PROFILE_FAILED = 'UPDATE_PROFILE_FAILED',
|
||||||
UPLOAD_PHOTO_FAILED = 'UPLOAD_PHOTO_FAILED',
|
UPLOAD_PHOTO_FAILED = 'UPLOAD_PHOTO_FAILED',
|
||||||
};
|
}
|
||||||
|
|
||||||
const queryCacheOnError = (error: unknown, query: any) => {
|
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) {
|
switch (query.meta?.errCode) {
|
||||||
case QueryErrorCodes.FETCH_USER_FAILED:
|
case QueryErrorCodes.FETCH_USER_FAILED:
|
||||||
break;
|
break;
|
||||||
case QueryErrorCodes.FETCH_PROFILE_FAILED:
|
case QueryErrorCodes.FETCH_PROFILE_FAILED:
|
||||||
break;
|
break;
|
||||||
case QueryErrorCodes.FETCH_AVATAR_FAILED:
|
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;
|
break;
|
||||||
default:
|
default:
|
||||||
console.error('Query error:', error);
|
console.error('Query error:', error);
|
||||||
@@ -38,13 +39,14 @@ const mutationCacheOnError = (
|
|||||||
context: unknown,
|
context: unknown,
|
||||||
mutation: any,
|
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) {
|
switch (mutation.meta?.errCode) {
|
||||||
case QueryErrorCodes.UPDATE_PROFILE_FAILED:
|
case QueryErrorCodes.UPDATE_PROFILE_FAILED:
|
||||||
toast.error(`Failed to update user profile: ${errorMessage}`)
|
toast.error(`Failed to update user profile: ${errorMessage}`);
|
||||||
break;
|
break;
|
||||||
case QueryErrorCodes.UPLOAD_PHOTO_FAILED:
|
case QueryErrorCodes.UPLOAD_PHOTO_FAILED:
|
||||||
toast.error(`Failed to upload photo: ${errorMessage}`)
|
toast.error(`Failed to upload photo: ${errorMessage}`);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.error('Mutation error:', error);
|
console.error('Mutation error:', error);
|
||||||
@@ -52,7 +54,6 @@ const mutationCacheOnError = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const QueryClientProvider = ({ children }: { children: React.ReactNode }) => {
|
const QueryClientProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
const [queryClient] = useState(
|
const [queryClient] = useState(
|
||||||
() =>
|
() =>
|
||||||
@@ -72,9 +73,13 @@ const QueryClientProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
gcTime: Infinity,
|
gcTime: Infinity,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
)
|
);
|
||||||
return <ReactQueryClientProvider client={queryClient}>{children}</ReactQueryClientProvider>
|
return (
|
||||||
|
<ReactQueryClientProvider client={queryClient}>
|
||||||
|
{children}
|
||||||
|
</ReactQueryClientProvider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { QueryClientProvider, QueryErrorCodes };
|
export { QueryClientProvider, QueryErrorCodes };
|
||||||
|
@@ -1,9 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import {
|
import { useEffect, useState, type ComponentProps } from 'react';
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
type ComponentProps,
|
|
||||||
} from 'react';
|
|
||||||
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
||||||
import { Moon, Sun } from 'lucide-react';
|
import { Moon, Sun } from 'lucide-react';
|
||||||
import { useTheme } from 'next-themes';
|
import { useTheme } from 'next-themes';
|
||||||
@@ -14,10 +10,11 @@ const ThemeProvider = ({
|
|||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: ComponentProps<typeof NextThemesProvider>) => {
|
}: ComponentProps<typeof NextThemesProvider>) => {
|
||||||
|
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
useEffect(() => { setMounted(true) }, []);
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (!mounted) return null;
|
if (!mounted) return null;
|
||||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||||
@@ -28,15 +25,13 @@ type ThemeToggleProps = {
|
|||||||
buttonProps?: Omit<ComponentProps<typeof Button>, 'onClick'>;
|
buttonProps?: Omit<ComponentProps<typeof Button>, 'onClick'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ThemeToggle = ({
|
const ThemeToggle = ({ size = 1, buttonProps }: ThemeToggleProps) => {
|
||||||
size = 1,
|
|
||||||
buttonProps,
|
|
||||||
}: ThemeToggleProps) => {
|
|
||||||
|
|
||||||
const { setTheme, resolvedTheme } = useTheme();
|
const { setTheme, resolvedTheme } = useTheme();
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
useEffect(() => { setMounted(true) }, []);
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return (
|
return (
|
||||||
|
@@ -11,11 +11,13 @@ type UploadToStorageProps = {
|
|||||||
client: SBClientWithDatabase;
|
client: SBClientWithDatabase;
|
||||||
file: File;
|
file: File;
|
||||||
bucket: string;
|
bucket: string;
|
||||||
resize?: false | {
|
resize?:
|
||||||
maxWidth?: number;
|
| false
|
||||||
maxHeight?: number;
|
| {
|
||||||
quality?: number;
|
maxWidth?: number;
|
||||||
};
|
maxHeight?: number;
|
||||||
|
quality?: number;
|
||||||
|
};
|
||||||
replace?: false | string;
|
replace?: false | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -26,66 +28,70 @@ const useFileUpload = (client: SBClientWithDatabase, bucket: string) => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
// Initialize the upload hook at the top level
|
// Initialize the upload hook at the top level
|
||||||
const { mutateAsync: upload } = useUpload(
|
const { mutateAsync: upload } = useUpload(client.storage.from(bucket), {
|
||||||
client.storage.from(bucket),
|
buildFileName: ({ fileName, path }) => path ?? fileName,
|
||||||
{
|
});
|
||||||
buildFileName: ({ fileName, path }) => path ?? fileName,
|
|
||||||
}
|
const uploadToStorage = useCallback(
|
||||||
|
async ({
|
||||||
|
file,
|
||||||
|
resize = false,
|
||||||
|
replace = false,
|
||||||
|
}: Omit<UploadToStorageProps, 'client' | 'bucket'>) => {
|
||||||
|
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<UploadToStorageProps, 'client' | 'bucket'>) => {
|
|
||||||
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({
|
const uploadMutation = useMutation({
|
||||||
mutationFn: uploadToStorage,
|
mutationFn: uploadToStorage,
|
||||||
onSuccess: (result) => {
|
onSuccess: (result) => {
|
||||||
@@ -96,38 +102,51 @@ const useFileUpload = (client: SBClientWithDatabase, bucket: string) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
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 },
|
meta: { errCode: QueryErrorCodes.UPLOAD_PHOTO_FAILED },
|
||||||
});
|
});
|
||||||
|
|
||||||
const uploadAvatarMutation = useMutation({
|
const uploadAvatarMutation = useMutation({
|
||||||
mutationFn: async (props: Omit<UploadToStorageProps, 'client' | 'bucket'>) => {
|
mutationFn: async (
|
||||||
|
props: Omit<UploadToStorageProps, 'client' | 'bucket'>,
|
||||||
|
) => {
|
||||||
const { data, error } = await uploadToStorage(props);
|
const { data, error } = await uploadToStorage(props);
|
||||||
if (error) throw new Error(`Error uploading avatar: ${error as string}`);
|
if (error) throw new Error(`Error uploading avatar: ${error as string}`);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess: (avatarUrl) => {
|
onSuccess: (avatarUrl) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['auth'] })
|
queryClient
|
||||||
.catch((error) => console.error('Error invalidating auth query:', error));
|
.invalidateQueries({ queryKey: ['auth'] })
|
||||||
|
.catch((error) =>
|
||||||
|
console.error('Error invalidating auth query:', error),
|
||||||
|
);
|
||||||
queryClient.setQueryData(['auth, user'], (oldUser: User) => oldUser);
|
queryClient.setQueryData(['auth, user'], (oldUser: User) => oldUser);
|
||||||
if (profile?.id) {
|
if (profile?.id) {
|
||||||
queryClient.setQueryData(['profiles', profile.id], (oldProfile: Profile) => ({
|
queryClient.setQueryData(
|
||||||
...oldProfile,
|
['profiles', profile.id],
|
||||||
avatar_url: avatarUrl,
|
(oldProfile: Profile) => ({
|
||||||
updated_at: new Date().toISOString(),
|
...oldProfile,
|
||||||
}));
|
avatar_url: avatarUrl,
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
toast.success('Avatar uploaded successfully!');
|
toast.success('Avatar uploaded successfully!');
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
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 },
|
meta: { errCode: QueryErrorCodes.UPLOAD_PHOTO_FAILED },
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isUploading: isUploading || uploadMutation.isPending || uploadAvatarMutation.isPending,
|
isUploading:
|
||||||
|
isUploading || uploadMutation.isPending || uploadAvatarMutation.isPending,
|
||||||
fileInputRef,
|
fileInputRef,
|
||||||
uploadToStorage,
|
uploadToStorage,
|
||||||
uploadMutation,
|
uploadMutation,
|
||||||
|
@@ -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 signUp = (client: SBClientWithDatabase, formData: FormData) => {
|
||||||
const full_name = formData.get('name') as string;
|
const full_name = formData.get('name') as string;
|
||||||
@@ -14,8 +18,8 @@ const signUp = (client: SBClientWithDatabase, formData: FormData) => {
|
|||||||
full_name,
|
full_name,
|
||||||
email,
|
email,
|
||||||
provider: 'email',
|
provider: 'email',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -62,24 +66,21 @@ const resetPassword = (client: SBClientWithDatabase, formData: FormData) => {
|
|||||||
|
|
||||||
const signOut = (client: SBClientWithDatabase) => {
|
const signOut = (client: SBClientWithDatabase) => {
|
||||||
return client.auth.signOut();
|
return client.auth.signOut();
|
||||||
}
|
};
|
||||||
|
|
||||||
const getCurrentUser = (client: SBClientWithDatabase) => {
|
const getCurrentUser = (client: SBClientWithDatabase) => {
|
||||||
return client.auth.getUser();
|
return client.auth.getUser();
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProfile = (client: SBClientWithDatabase, userId: string) => {
|
const getProfile = (client: SBClientWithDatabase, userId: string) => {
|
||||||
return client
|
return client.from(`profiles`).select(`*`).eq(`id`, userId).single();
|
||||||
.from(`profiles`)
|
|
||||||
.select(`*`)
|
|
||||||
.eq(`id`, userId)
|
|
||||||
.single();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserWithStatus = (client: SBClientWithDatabase, userId: string) => {
|
const getUserWithStatus = (client: SBClientWithDatabase, userId: string) => {
|
||||||
return client
|
return client
|
||||||
.from(`profiles`)
|
.from(`profiles`)
|
||||||
.select(`
|
.select(
|
||||||
|
`
|
||||||
id,
|
id,
|
||||||
updated_at,
|
updated_at,
|
||||||
email,
|
email,
|
||||||
@@ -91,7 +92,8 @@ const getUserWithStatus = (client: SBClientWithDatabase, userId: string) => {
|
|||||||
created_at,
|
created_at,
|
||||||
updated_by:profiles!updated_by_id(*)
|
updated_by:profiles!updated_by_id(*)
|
||||||
)
|
)
|
||||||
`)
|
`,
|
||||||
|
)
|
||||||
.eq(`id`, userId)
|
.eq(`id`, userId)
|
||||||
.throwOnError()
|
.throwOnError()
|
||||||
.single();
|
.single();
|
||||||
@@ -122,5 +124,5 @@ export {
|
|||||||
signInWithMicrosoft,
|
signInWithMicrosoft,
|
||||||
signOut,
|
signOut,
|
||||||
signUp,
|
signUp,
|
||||||
updateProfile
|
updateProfile,
|
||||||
};
|
};
|
||||||
|
@@ -9,7 +9,7 @@ export {
|
|||||||
signInWithMicrosoft,
|
signInWithMicrosoft,
|
||||||
signOut,
|
signOut,
|
||||||
signUp,
|
signUp,
|
||||||
updateProfile
|
updateProfile,
|
||||||
} from './auth';
|
} from './auth';
|
||||||
export {
|
export {
|
||||||
deleteFiles,
|
deleteFiles,
|
||||||
@@ -20,5 +20,5 @@ export {
|
|||||||
listFiles,
|
listFiles,
|
||||||
resizeImage,
|
resizeImage,
|
||||||
uploadFile,
|
uploadFile,
|
||||||
updateFile
|
updateFile,
|
||||||
} from './storage';
|
} from './storage';
|
||||||
|
@@ -36,10 +36,7 @@ type ResizeImageProps = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAvatarUrl = (
|
const getAvatarUrl = (client: SBClientWithDatabase, path: string) => {
|
||||||
client: SBClientWithDatabase,
|
|
||||||
path: string,
|
|
||||||
) => {
|
|
||||||
return getPublicUrl({
|
return getPublicUrl({
|
||||||
client,
|
client,
|
||||||
bucket: 'avatars',
|
bucket: 'avatars',
|
||||||
@@ -48,7 +45,7 @@ const getAvatarUrl = (
|
|||||||
width: 128,
|
width: 128,
|
||||||
height: 128,
|
height: 128,
|
||||||
quality: 0.8,
|
quality: 0.8,
|
||||||
}
|
},
|
||||||
}).data.publicUrl;
|
}).data.publicUrl;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -61,12 +58,12 @@ const getPublicUrl = ({
|
|||||||
}: GetStorageProps) => {
|
}: GetStorageProps) => {
|
||||||
return client.storage
|
return client.storage
|
||||||
.from(bucket)
|
.from(bucket)
|
||||||
.getPublicUrl(path, { download, transform});
|
.getPublicUrl(path, { download, transform });
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSignedAvatarUrl = (
|
const getSignedAvatarUrl = (
|
||||||
client: SBClientWithDatabase,
|
client: SBClientWithDatabase,
|
||||||
avatarUrl: string
|
avatarUrl: string,
|
||||||
) => {
|
) => {
|
||||||
return getSignedUrl({
|
return getSignedUrl({
|
||||||
client,
|
client,
|
||||||
@@ -90,7 +87,7 @@ const getSignedUrl = ({
|
|||||||
}: GetStorageProps) => {
|
}: GetStorageProps) => {
|
||||||
return client.storage
|
return client.storage
|
||||||
.from(bucket)
|
.from(bucket)
|
||||||
.createSignedUrl(path, seconds, { download, transform});
|
.createSignedUrl(path, seconds, { download, transform });
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadFile = ({
|
const uploadFile = ({
|
||||||
@@ -100,9 +97,7 @@ const uploadFile = ({
|
|||||||
file,
|
file,
|
||||||
options = {},
|
options = {},
|
||||||
}: UploadStorageProps) => {
|
}: UploadStorageProps) => {
|
||||||
return client.storage
|
return client.storage.from(bucket).upload(path, file, options);
|
||||||
.from(bucket)
|
|
||||||
.upload(path, file, options);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateFile = ({
|
const updateFile = ({
|
||||||
@@ -114,9 +109,7 @@ const updateFile = ({
|
|||||||
upsert: true,
|
upsert: true,
|
||||||
},
|
},
|
||||||
}: UploadStorageProps) => {
|
}: UploadStorageProps) => {
|
||||||
return client.storage
|
return client.storage.from(bucket).update(path, file, options);
|
||||||
.from(bucket)
|
|
||||||
.update(path, file, options);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteFiles = ({
|
const deleteFiles = ({
|
||||||
@@ -127,7 +120,7 @@ const deleteFiles = ({
|
|||||||
client: SBClientWithDatabase;
|
client: SBClientWithDatabase;
|
||||||
bucket: string;
|
bucket: string;
|
||||||
path: string[];
|
path: string[];
|
||||||
}) => {
|
}) => {
|
||||||
return client.storage.from(bucket).remove(path);
|
return client.storage.from(bucket).remove(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -141,9 +134,9 @@ const listFiles = ({
|
|||||||
bucket: string;
|
bucket: string;
|
||||||
path?: string;
|
path?: string;
|
||||||
options?: {
|
options?: {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
sortBy?: { column: string, order: 'asc' | 'desc' };
|
sortBy?: { column: string; order: 'asc' | 'desc' };
|
||||||
};
|
};
|
||||||
}) => {
|
}) => {
|
||||||
return client.storage.from(bucket).list(path, options);
|
return client.storage.from(bucket).list(path, options);
|
||||||
@@ -190,7 +183,7 @@ const resizeImage = async ({
|
|||||||
resolve(resizedFile);
|
resolve(resizedFile);
|
||||||
},
|
},
|
||||||
'image/jpeg',
|
'image/jpeg',
|
||||||
(options.quality && options.quality < 1 && options.quality > 0)
|
options.quality && options.quality < 1 && options.quality > 0
|
||||||
? options.quality
|
? options.quality
|
||||||
: 0.8,
|
: 0.8,
|
||||||
);
|
);
|
||||||
@@ -208,5 +201,5 @@ export {
|
|||||||
listFiles,
|
listFiles,
|
||||||
resizeImage,
|
resizeImage,
|
||||||
uploadFile,
|
uploadFile,
|
||||||
updateFile
|
updateFile,
|
||||||
};
|
};
|
||||||
|
@@ -1,11 +1,21 @@
|
|||||||
import { type NextRequest } from 'next/server';
|
import { type NextRequest, NextResponse } from 'next/server';
|
||||||
import { updateSession } from '@/utils/supabase/middleware';
|
import { updateSession } from '@/utils/supabase/middleware';
|
||||||
import { banSuspiciousIPs } from '@/utils/ban-suspicious-ips';
|
import { banSuspiciousIPs } from '@/utils/ban-suspicious-ips';
|
||||||
|
|
||||||
export const middleware = async (request: NextRequest) => {
|
export const middleware = async (request: NextRequest) => {
|
||||||
const banResponse = banSuspiciousIPs(request);
|
const banResponse = banSuspiciousIPs(request);
|
||||||
if (banResponse) return banResponse;
|
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 = {
|
export const config = {
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
import { type NextRequest, NextResponse } from 'next/server';
|
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<string, { count: number; lastAttempt: number }>();
|
const ipAttempts = new Map<string, { count: number; lastAttempt: number }>();
|
||||||
|
const ip404Attempts = new Map<string, { count: number; lastAttempt: number }>();
|
||||||
const bannedIPs = new Set<string>();
|
const bannedIPs = new Set<string>();
|
||||||
|
|
||||||
// Ban Arctic Wolf Explicitly
|
// Ban Arctic Wolf Explicitly
|
||||||
@@ -9,6 +10,7 @@ bannedIPs.add('::ffff:10.0.1.49');
|
|||||||
|
|
||||||
// Suspicious patterns that indicate malicious activity
|
// Suspicious patterns that indicate malicious activity
|
||||||
const MALICIOUS_PATTERNS = [
|
const MALICIOUS_PATTERNS = [
|
||||||
|
// Your existing patterns
|
||||||
/web-inf/i,
|
/web-inf/i,
|
||||||
/\.jsp/i,
|
/\.jsp/i,
|
||||||
/\.php/i,
|
/\.php/i,
|
||||||
@@ -28,6 +30,37 @@ const MALICIOUS_PATTERNS = [
|
|||||||
/\.%00/i,
|
/\.%00/i,
|
||||||
/\.\./,
|
/\.\./,
|
||||||
/lcgi/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
|
// 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 MAX_ATTEMPTS = 10; // Max suspicious requests per window
|
||||||
const BAN_DURATION = 30 * 60 * 1000; // 30 minutes
|
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 getClientIP = (request: NextRequest): string => {
|
||||||
const forwarded = request.headers.get('x-forwarded-for');
|
const forwarded = request.headers.get('x-forwarded-for');
|
||||||
const realIP = request.headers.get('x-real-ip');
|
const realIP = request.headers.get('x-real-ip');
|
||||||
@@ -64,17 +101,51 @@ const updateIPAttempts = (ip: string): boolean => {
|
|||||||
ipAttempts.set(ip, { count: 1, lastAttempt: now });
|
ipAttempts.set(ip, { count: 1, lastAttempt: now });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
attempts.count++;
|
attempts.count++;
|
||||||
attempts.lastAttempt = now;
|
attempts.lastAttempt = now;
|
||||||
|
|
||||||
if (attempts.count > MAX_ATTEMPTS) {
|
if (attempts.count > MAX_ATTEMPTS) {
|
||||||
bannedIPs.add(ip);
|
bannedIPs.add(ip);
|
||||||
ipAttempts.delete(ip);
|
ipAttempts.delete(ip);
|
||||||
// Auto-unban after duration (in production, use a proper scheduler)
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
bannedIPs.delete(ip);
|
bannedIPs.delete(ip);
|
||||||
}, BAN_DURATION);
|
}, BAN_DURATION);
|
||||||
|
|
||||||
return true;
|
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;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,20 +154,48 @@ export const banSuspiciousIPs = (request: NextRequest): NextResponse | null => {
|
|||||||
const method = request.method;
|
const method = request.method;
|
||||||
const ip = getClientIP(request);
|
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 isSuspiciousPath = isPathSuspicious(pathname);
|
||||||
const isSuspiciousMethod = isMethodSuspicious(method);
|
const isSuspiciousMethod = isMethodSuspicious(method);
|
||||||
|
|
||||||
|
// Handle suspicious activity
|
||||||
if (isSuspiciousPath || isSuspiciousMethod) {
|
if (isSuspiciousPath || isSuspiciousMethod) {
|
||||||
const shouldBan = updateIPAttempts(ip);
|
const shouldBan = updateIPAttempts(ip);
|
||||||
|
|
||||||
if (shouldBan) {
|
if (shouldBan) {
|
||||||
console.log(`🔨 IP ${ip} has been banned for suspicious activity`);
|
console.log(`🔨 IP ${ip} has been banned for suspicious activity`);
|
||||||
return new NextResponse('Access denied - IP banned. Please fuck off.', {
|
return new NextResponse('Access denied - IP banned. Please fuck off.', {
|
||||||
status: 403,
|
status: 403,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return new NextResponse('Not Found', { status: 404 });
|
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;
|
return null;
|
||||||
};
|
};
|
||||||
|
@@ -5,7 +5,7 @@ import type { Database, SBClientWithDatabase } from '@/utils/supabase';
|
|||||||
let client: SBClientWithDatabase | undefined;
|
let client: SBClientWithDatabase | undefined;
|
||||||
|
|
||||||
const getSupbaseClient = (): SBClientWithDatabase | undefined => {
|
const getSupbaseClient = (): SBClientWithDatabase | undefined => {
|
||||||
if (client) return client;
|
if (client) return client;
|
||||||
client = createBrowserClient<Database>(
|
client = createBrowserClient<Database>(
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||||
|
@@ -4,197 +4,196 @@ export type Json =
|
|||||||
| boolean
|
| boolean
|
||||||
| null
|
| null
|
||||||
| { [key: string]: Json | undefined }
|
| { [key: string]: Json | undefined }
|
||||||
| Json[]
|
| Json[];
|
||||||
|
|
||||||
export type Database = {
|
export type Database = {
|
||||||
public: {
|
public: {
|
||||||
Tables: {
|
Tables: {
|
||||||
profiles: {
|
profiles: {
|
||||||
Row: {
|
Row: {
|
||||||
avatar_url: string | null
|
avatar_url: string | null;
|
||||||
current_status_id: string | null
|
current_status_id: string | null;
|
||||||
email: string | null
|
email: string | null;
|
||||||
full_name: string | null
|
full_name: string | null;
|
||||||
id: string
|
id: string;
|
||||||
provider: string | null
|
provider: string | null;
|
||||||
updated_at: string | null
|
updated_at: string | null;
|
||||||
}
|
};
|
||||||
Insert: {
|
Insert: {
|
||||||
avatar_url?: string | null
|
avatar_url?: string | null;
|
||||||
current_status_id?: string | null
|
current_status_id?: string | null;
|
||||||
email?: string | null
|
email?: string | null;
|
||||||
full_name?: string | null
|
full_name?: string | null;
|
||||||
id: string
|
id: string;
|
||||||
provider?: string | null
|
provider?: string | null;
|
||||||
updated_at?: string | null
|
updated_at?: string | null;
|
||||||
}
|
};
|
||||||
Update: {
|
Update: {
|
||||||
avatar_url?: string | null
|
avatar_url?: string | null;
|
||||||
current_status_id?: string | null
|
current_status_id?: string | null;
|
||||||
email?: string | null
|
email?: string | null;
|
||||||
full_name?: string | null
|
full_name?: string | null;
|
||||||
id?: string
|
id?: string;
|
||||||
provider?: string | null
|
provider?: string | null;
|
||||||
updated_at?: string | null
|
updated_at?: string | null;
|
||||||
}
|
};
|
||||||
Relationships: [
|
Relationships: [
|
||||||
{
|
{
|
||||||
foreignKeyName: "profiles_current_status_id_fkey"
|
foreignKeyName: 'profiles_current_status_id_fkey';
|
||||||
columns: ["current_status_id"]
|
columns: ['current_status_id'];
|
||||||
isOneToOne: false
|
isOneToOne: false;
|
||||||
referencedRelation: "statuses"
|
referencedRelation: 'statuses';
|
||||||
referencedColumns: ["id"]
|
referencedColumns: ['id'];
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
}
|
};
|
||||||
statuses: {
|
statuses: {
|
||||||
Row: {
|
Row: {
|
||||||
created_at: string
|
created_at: string;
|
||||||
id: string
|
id: string;
|
||||||
status: string
|
status: string;
|
||||||
updated_by_id: string | null
|
updated_by_id: string | null;
|
||||||
user_id: string
|
user_id: string;
|
||||||
}
|
};
|
||||||
Insert: {
|
Insert: {
|
||||||
created_at?: string
|
created_at?: string;
|
||||||
id?: string
|
id?: string;
|
||||||
status: string
|
status: string;
|
||||||
updated_by_id?: string | null
|
updated_by_id?: string | null;
|
||||||
user_id: string
|
user_id: string;
|
||||||
}
|
};
|
||||||
Update: {
|
Update: {
|
||||||
created_at?: string
|
created_at?: string;
|
||||||
id?: string
|
id?: string;
|
||||||
status?: string
|
status?: string;
|
||||||
updated_by_id?: string | null
|
updated_by_id?: string | null;
|
||||||
user_id?: string
|
user_id?: string;
|
||||||
}
|
};
|
||||||
Relationships: []
|
Relationships: [];
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
Views: {
|
Views: {
|
||||||
[_ in never]: never
|
[_ in never]: never;
|
||||||
}
|
};
|
||||||
Functions: {
|
Functions: {
|
||||||
[_ in never]: never
|
[_ in never]: never;
|
||||||
}
|
};
|
||||||
Enums: {
|
Enums: {
|
||||||
[_ in never]: never
|
[_ in never]: never;
|
||||||
}
|
};
|
||||||
CompositeTypes: {
|
CompositeTypes: {
|
||||||
[_ in never]: never
|
[_ in never]: never;
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
type DefaultSchema = Database[Extract<keyof Database, "public">]
|
type DefaultSchema = Database[Extract<keyof Database, 'public'>];
|
||||||
|
|
||||||
export type Tables<
|
export type Tables<
|
||||||
DefaultSchemaTableNameOrOptions extends
|
DefaultSchemaTableNameOrOptions extends
|
||||||
| keyof (DefaultSchema["Tables"] & DefaultSchema["Views"])
|
| keyof (DefaultSchema['Tables'] & DefaultSchema['Views'])
|
||||||
| { schema: keyof Database },
|
| { schema: keyof Database },
|
||||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||||
schema: keyof Database
|
schema: keyof Database;
|
||||||
}
|
}
|
||||||
? keyof (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
? keyof (Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] &
|
||||||
Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])
|
Database[DefaultSchemaTableNameOrOptions['schema']]['Views'])
|
||||||
: never = never,
|
: never = never,
|
||||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||||
? (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
? (Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] &
|
||||||
Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends {
|
Database[DefaultSchemaTableNameOrOptions['schema']]['Views'])[TableName] extends {
|
||||||
Row: infer R
|
Row: infer R;
|
||||||
}
|
}
|
||||||
? R
|
? R
|
||||||
: never
|
: never
|
||||||
: DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] &
|
: DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema['Tables'] &
|
||||||
DefaultSchema["Views"])
|
DefaultSchema['Views'])
|
||||||
? (DefaultSchema["Tables"] &
|
? (DefaultSchema['Tables'] &
|
||||||
DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends {
|
DefaultSchema['Views'])[DefaultSchemaTableNameOrOptions] extends {
|
||||||
Row: infer R
|
Row: infer R;
|
||||||
}
|
}
|
||||||
? R
|
? R
|
||||||
: never
|
: never
|
||||||
: never
|
: never;
|
||||||
|
|
||||||
export type TablesInsert<
|
export type TablesInsert<
|
||||||
DefaultSchemaTableNameOrOptions extends
|
DefaultSchemaTableNameOrOptions extends
|
||||||
| keyof DefaultSchema["Tables"]
|
| keyof DefaultSchema['Tables']
|
||||||
| { schema: keyof Database },
|
| { schema: keyof Database },
|
||||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||||
schema: keyof Database
|
schema: keyof Database;
|
||||||
}
|
}
|
||||||
? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
? keyof Database[DefaultSchemaTableNameOrOptions['schema']]['Tables']
|
||||||
: never = never,
|
: never = never,
|
||||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||||
? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
? Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends {
|
||||||
Insert: infer I
|
Insert: infer I;
|
||||||
}
|
}
|
||||||
? I
|
? I
|
||||||
: never
|
: never
|
||||||
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
|
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema['Tables']
|
||||||
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
|
? DefaultSchema['Tables'][DefaultSchemaTableNameOrOptions] extends {
|
||||||
Insert: infer I
|
Insert: infer I;
|
||||||
}
|
}
|
||||||
? I
|
? I
|
||||||
: never
|
: never
|
||||||
: never
|
: never;
|
||||||
|
|
||||||
export type TablesUpdate<
|
export type TablesUpdate<
|
||||||
DefaultSchemaTableNameOrOptions extends
|
DefaultSchemaTableNameOrOptions extends
|
||||||
| keyof DefaultSchema["Tables"]
|
| keyof DefaultSchema['Tables']
|
||||||
| { schema: keyof Database },
|
| { schema: keyof Database },
|
||||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||||
schema: keyof Database
|
schema: keyof Database;
|
||||||
}
|
}
|
||||||
? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
? keyof Database[DefaultSchemaTableNameOrOptions['schema']]['Tables']
|
||||||
: never = never,
|
: never = never,
|
||||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||||
? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
? Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends {
|
||||||
Update: infer U
|
Update: infer U;
|
||||||
}
|
}
|
||||||
? U
|
? U
|
||||||
: never
|
: never
|
||||||
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
|
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema['Tables']
|
||||||
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
|
? DefaultSchema['Tables'][DefaultSchemaTableNameOrOptions] extends {
|
||||||
Update: infer U
|
Update: infer U;
|
||||||
}
|
}
|
||||||
? U
|
? U
|
||||||
: never
|
: never
|
||||||
: never
|
: never;
|
||||||
|
|
||||||
export type Enums<
|
export type Enums<
|
||||||
DefaultSchemaEnumNameOrOptions extends
|
DefaultSchemaEnumNameOrOptions extends
|
||||||
| keyof DefaultSchema["Enums"]
|
| keyof DefaultSchema['Enums']
|
||||||
| { schema: keyof Database },
|
| { schema: keyof Database },
|
||||||
EnumName extends DefaultSchemaEnumNameOrOptions extends {
|
EnumName extends DefaultSchemaEnumNameOrOptions extends {
|
||||||
schema: keyof Database
|
schema: keyof Database;
|
||||||
}
|
}
|
||||||
? keyof Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"]
|
? keyof Database[DefaultSchemaEnumNameOrOptions['schema']]['Enums']
|
||||||
: never = never,
|
: never = never,
|
||||||
> = DefaultSchemaEnumNameOrOptions extends { schema: keyof Database }
|
> = DefaultSchemaEnumNameOrOptions extends { schema: keyof Database }
|
||||||
? Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName]
|
? Database[DefaultSchemaEnumNameOrOptions['schema']]['Enums'][EnumName]
|
||||||
: DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"]
|
: DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema['Enums']
|
||||||
? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions]
|
? DefaultSchema['Enums'][DefaultSchemaEnumNameOrOptions]
|
||||||
: never
|
: never;
|
||||||
|
|
||||||
export type CompositeTypes<
|
export type CompositeTypes<
|
||||||
PublicCompositeTypeNameOrOptions extends
|
PublicCompositeTypeNameOrOptions extends
|
||||||
| keyof DefaultSchema["CompositeTypes"]
|
| keyof DefaultSchema['CompositeTypes']
|
||||||
| { schema: keyof Database },
|
| { schema: keyof Database },
|
||||||
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
|
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
|
||||||
schema: keyof Database
|
schema: keyof Database;
|
||||||
}
|
}
|
||||||
? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
|
? keyof Database[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes']
|
||||||
: never = never,
|
: never = never,
|
||||||
> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database }
|
> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database }
|
||||||
? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
|
? Database[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'][CompositeTypeName]
|
||||||
: PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"]
|
: PublicCompositeTypeNameOrOptions extends keyof DefaultSchema['CompositeTypes']
|
||||||
? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
|
? DefaultSchema['CompositeTypes'][PublicCompositeTypeNameOrOptions]
|
||||||
: never
|
: never;
|
||||||
|
|
||||||
export const Constants = {
|
export const Constants = {
|
||||||
public: {
|
public: {
|
||||||
Enums: {},
|
Enums: {},
|
||||||
},
|
},
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import type { Database } from '@/utils/supabase/database.types';
|
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<Database>;
|
export type SBClientWithDatabase = SBClient<Database>;
|
||||||
|
|
||||||
@@ -11,17 +11,17 @@ export type Result<T> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type UserRecord = {
|
export type UserRecord = {
|
||||||
id: string,
|
id: string;
|
||||||
updated_at: string | null,
|
updated_at: string | null;
|
||||||
email: string | null,
|
email: string | null;
|
||||||
full_name: string | null,
|
full_name: string | null;
|
||||||
avatar_url: string | null,
|
avatar_url: string | null;
|
||||||
provider: string | null,
|
provider: string | null;
|
||||||
status: {
|
status: {
|
||||||
status: string,
|
status: string;
|
||||||
created_at: string,
|
created_at: string;
|
||||||
updated_by: Profile | null,
|
updated_by: Profile | null;
|
||||||
}
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AsyncReturnType<T extends (...args: any) => Promise<any>> =
|
export type AsyncReturnType<T extends (...args: any) => Promise<any>> =
|
||||||
|
Reference in New Issue
Block a user