Format and make ban-suspicious-ips more better

This commit is contained in:
2025-07-22 09:20:01 -05:00
parent 77c88ea9de
commit f071b6c19b
36 changed files with 716 additions and 444 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -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"

View File

@@ -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 (

View File

@@ -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>
); );
}; };

View File

@@ -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;

View File

@@ -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

View File

@@ -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>

View File

@@ -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>
); );
}; };

View File

@@ -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';

View File

@@ -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>
); );

View File

@@ -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>
); );

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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&apos;t have an account?{' '} Don&apos;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 &&

View File

@@ -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>
); );
}; };

View File

@@ -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>

View File

@@ -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'

View File

@@ -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>
</>
);
}; };

View File

@@ -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>
); );
}; };

View File

@@ -44,7 +44,7 @@ export const SubmitButton = ({
</p> </p>
</> </>
) : ( ) : (
children children
)} )}
</Button> </Button>
); );

View File

@@ -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;

View File

@@ -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>
)} )}

View File

@@ -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 = () => {

View File

@@ -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 };

View File

@@ -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 (

View File

@@ -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,

View File

@@ -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,
}; };

View File

@@ -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';

View File

@@ -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,
}; };

View File

@@ -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 = {

View File

@@ -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;
}; };

View File

@@ -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!,

View File

@@ -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;

View File

@@ -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>> =