fix: error layout global app

This commit is contained in:
Théo LAGACHE
2026-02-25 13:00:52 +01:00
parent 31285dd0d6
commit c8d390726c
5 changed files with 198 additions and 172 deletions
+2 -2
View File
@@ -4,7 +4,7 @@ import { currentUser } from "@/lib/auth/current-user";
import { AuthLogoSection } from "@/components/wrappers/auth/auth-logo-section"; import { AuthLogoSection } from "@/components/wrappers/auth/auth-logo-section";
import { env } from "@/env.mjs"; import { env } from "@/env.mjs";
import { Heart } from "lucide-react"; import { Heart } from "lucide-react";
import { AuthLayout } from "@/components/wrappers/auth/auth-layout"; import { ErrorLayout } from "@/components/wrappers/common/error-layout";
export default async function Layout({ export default async function Layout({
children, children,
@@ -24,7 +24,7 @@ export default async function Layout({
<AuthLogoSection /> <AuthLogoSection />
<div className="mt-4"> <div className="mt-4">
<Suspense> <Suspense>
<AuthLayout>{children}</AuthLayout> <ErrorLayout>{children}</ErrorLayout>
</Suspense> </Suspense>
</div> </div>
</div> </div>
+27 -23
View File
@@ -1,27 +1,31 @@
import React, {ReactNode} from "react"; import { ReactNode, Suspense } from "react";
import {redirect} from "next/navigation"; import { redirect } from "next/navigation";
import {SidebarInset, SidebarProvider} from "@/components/ui/sidebar"; import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
import {AppSidebar} from "@/components/wrappers/dashboard/common/sidebar/app-sidebar"; import { AppSidebar } from "@/components/wrappers/dashboard/common/sidebar/app-sidebar";
import {Header} from "@/features/layout/Header"; import { Header } from "@/features/layout/Header";
import {currentUser} from "@/lib/auth/current-user"; import { currentUser } from "@/lib/auth/current-user";
import {ThemeMetaUpdater} from "@/features/browser/theme-meta-updater"; import { ThemeMetaUpdater } from "@/features/browser/theme-meta-updater";
import {BackupModalProvider} from "@/components/wrappers/dashboard/database/backup/backup-modal-context"; import { ErrorLayout } from "@/components/wrappers/common/error-layout";
export default async function Layout({children}: { children: ReactNode }) { export default async function Layout({ children }: { children: ReactNode }) {
const user = await currentUser(); const user = await currentUser();
if (!user) redirect("/login"); if (!user) redirect("/login");
return ( return (
<SidebarProvider> <SidebarProvider>
<div className="flex flex-col lg:flex-row w-full"> <div className="flex flex-col lg:flex-row w-full">
<ThemeMetaUpdater/> <ThemeMetaUpdater />
<AppSidebar/> <AppSidebar />
<SidebarInset> <SidebarInset>
<Header/> <Header />
<main className="h-full">{children}</main> <Suspense>
</SidebarInset> <ErrorLayout>
</div> <main className="h-full">{children}</main>
</SidebarProvider> </ErrorLayout>
); </Suspense>
</SidebarInset>
</div>
</SidebarProvider>
);
} }
@@ -1,156 +1,169 @@
"use client"; "use client";
import {useRouter} from "next/navigation"; import { useRouter } from "next/navigation";
import {useMutation} from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import {toast} from "sonner"; import { toast } from "sonner";
import {CardContent, CardHeader} from "@/components/ui/card"; import { CardContent, CardHeader } from "@/components/ui/card";
import {Form} from "@/components/ui/form"; import { Form } from "@/components/ui/form";
import {TooltipProvider, TooltipTrigger, Tooltip, TooltipContent} from "@/components/ui/tooltip"; import {
import {RegisterSchema, RegisterType} from "@/components/wrappers/auth/register/register-form/register-form.schema"; TooltipProvider,
import {signUp} from "@/lib/auth/auth-client"; TooltipTrigger,
import {useZodForm} from "@/components/ui/form"; Tooltip,
import {CardAuth} from "@/features/layout/card-auth"; TooltipContent,
import {FormField} from "@/components/ui/form"; } from "@/components/ui/tooltip";
import {FormItem} from "@/components/ui/form"; import {
import {FormLabel} from "@/components/ui/form"; RegisterSchema,
import {FormControl} from "@/components/ui/form"; RegisterType,
import {FormMessage} from "@/components/ui/form"; } from "@/components/wrappers/auth/register/register-form/register-form.schema";
import {Input} from "@/components/ui/input"; import { signUp } from "@/lib/auth/auth-client";
import {PasswordInput} from "@/components/ui/password-input"; import { useZodForm } from "@/components/ui/form";
import {Button} from "@/components/ui/button"; import { CardAuth } from "@/features/layout/card-auth";
import { FormField } from "@/components/ui/form";
import { FormItem } from "@/components/ui/form";
import { FormLabel } from "@/components/ui/form";
import { FormControl } from "@/components/ui/form";
import { FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { PasswordInput } from "@/components/ui/password-input";
import { Button } from "@/components/ui/button";
import Link from "next/link"; import Link from "next/link";
import {Info} from "lucide-react"; import { Info } from "lucide-react";
export type registerFormProps = { export type registerFormProps = {
defaultValues?: RegisterType; defaultValues?: RegisterType;
}; };
export const RegisterForm = (props: registerFormProps) => { export const RegisterForm = (props: registerFormProps) => {
const form = useZodForm({
schema: RegisterSchema,
});
const form = useZodForm({ const router = useRouter();
schema: RegisterSchema, const mutation = useMutation({
}); mutationFn: async (values: RegisterType) => {
await signUp.email(values, {
const router = useRouter(); onSuccess: () => {
const mutation = useMutation({ toast.success(`Account successfully created`);
mutationFn: async (values: RegisterType) => { router.refresh();
// @ts-ignore router.push(`/login`);
await signUp.email(values, {
onSuccess: () => {
toast.success(`Account successfully created`);
router.refresh();
router.push(`/login`);
},
onError: (error) => {
toast.error(error.error.message);
},
});
}, },
}); onError: (error) => {
toast.error(error.error.message);
},
});
},
});
return (
return ( <TooltipProvider>
<TooltipProvider> <CardAuth>
<CardAuth> <CardHeader>
<CardHeader> <div className="grid gap-2 text-center mb-2">
<div className="grid gap-2 text-center mb-2"> <h1 className="text-3xl font-bold">Create an account</h1>
<h1 className="text-3xl font-bold">Create an account</h1> <p className="text-balance text-muted-foreground">
<p className="text-balance text-muted-foreground">Enter your informations below to register</p> Enter your informations below to register
</div> </p>
</CardHeader> </div>
<CardContent> </CardHeader>
<Form <CardContent>
form={form} <Form
className="flex flex-col gap-4" form={form}
onSubmit={async (values) => { className="flex flex-col gap-4"
await mutation.mutateAsync(values); onSubmit={async (values) => {
}} await mutation.mutateAsync(values);
> }}
<FormField >
control={form.control} <FormField
name="name" control={form.control}
defaultValue="" name="name"
render={({field}) => ( defaultValue=""
<FormItem> render={({ field }) => (
<FormLabel>Name</FormLabel> <FormItem>
<FormControl> <FormLabel>Name</FormLabel>
<Input placeholder="Your name" {...field} /> <FormControl>
</FormControl> <Input placeholder="Your name" {...field} />
<FormMessage/> </FormControl>
</FormItem> <FormMessage />
)} </FormItem>
/> )}
<FormField />
control={form.control} <FormField
name="email" control={form.control}
defaultValue="" name="email"
render={({field}) => ( defaultValue=""
<FormItem> render={({ field }) => (
<FormLabel>Email</FormLabel> <FormItem>
<FormControl> <FormLabel>Email</FormLabel>
<Input placeholder="example@portabase.io" {...field} /> <FormControl>
</FormControl> <Input placeholder="example@portabase.io" {...field} />
<FormMessage/> </FormControl>
</FormItem> <FormMessage />
)} </FormItem>
/> )}
<FormField />
control={form.control} <FormField
name="password" control={form.control}
defaultValue="" name="password"
render={({field}) => ( defaultValue=""
<FormItem> render={({ field }) => (
<FormLabel className="flex"> <FormItem>
Password <FormLabel className="flex">
<TooltipProvider> Password
<Tooltip> <TooltipProvider>
<TooltipTrigger asChild> <Tooltip>
<Info className="ml-3" size="15"/> <TooltipTrigger asChild>
</TooltipTrigger> <Info size="15" />
<TooltipContent> </TooltipTrigger>
<p> <TooltipContent>
Min. 8 characters, 1 uppercase (A-Z), 1 lowercase (a-z), 1 <p>
number (0-9), 1 special character (!, @, Min. 8 characters, 1 uppercase (A-Z), 1 lowercase
etc.) (a-z), 1 number (0-9), 1 special character (!, @,
</p> etc.)
</TooltipContent> </p>
</Tooltip> </TooltipContent>
</TooltipProvider> </Tooltip>
</FormLabel> </TooltipProvider>
<FormControl> </FormLabel>
<PasswordInput placeholder="Your password" {...field} /> <FormControl>
</FormControl> <PasswordInput placeholder="Your password" {...field} />
<FormMessage/> </FormControl>
</FormItem> <FormMessage />
)} </FormItem>
/> )}
<FormField />
control={form.control} <FormField
name="confirmPassword" control={form.control}
defaultValue="" name="confirmPassword"
render={({field}) => ( defaultValue=""
<FormItem> render={({ field }) => (
<FormLabel>Password Confirmation</FormLabel> <FormItem>
<FormControl> <FormLabel>Password Confirmation</FormLabel>
<PasswordInput placeholder="Conform your password" {...field} /> <FormControl>
</FormControl> <PasswordInput
<FormMessage/> placeholder="Conform your password"
</FormItem> {...field}
)} />
/> </FormControl>
<Button type="submit" className="h-11" disabled={mutation.isPending}> <FormMessage />
Sign up </FormItem>
</Button> )}
<div className="mt-4 text-center text-sm"> />
Already have an account ?{" "} <Button
<Link href="/login" className="underline"> type="submit"
Sign in className="h-11"
</Link> disabled={mutation.isPending}
</div> >
Sign up
</Form> </Button>
</CardContent> <div className="mt-4 text-center text-sm">
</CardAuth> Already have an account ?{" "}
</TooltipProvider> <Link href="/login" className="underline">
); Sign in
</Link>
</div>
</Form>
</CardContent>
</CardAuth>
</TooltipProvider>
);
}; };
@@ -4,7 +4,7 @@ import React, { useEffect } from "react";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { toast } from "sonner"; import { toast } from "sonner";
export const AuthLayout = ({ children }: { children: React.ReactNode }) => { export const ErrorLayout = ({ children }: { children: React.ReactNode }) => {
const url = useSearchParams(); const url = useSearchParams();
useEffect(() => { useEffect(() => {
@@ -15,6 +15,8 @@ export const AuthLayout = ({ children }: { children: React.ReactNode }) => {
const errorMessages: Record<string, string> = { const errorMessages: Record<string, string> = {
pending: "Your account is not active.", pending: "Your account is not active.",
invalid_or_expired_token: "Password reset invalid token.", invalid_or_expired_token: "Password reset invalid token.",
account_already_linked_to_different_user:
"An account already exists with the same email address but different sign-in credentials. Sign in using a provider associated with this email address.",
}; };
const match = Object.entries(errorMessages).find(([key]) => const match = Object.entries(errorMessages).find(([key]) =>
@@ -28,7 +30,8 @@ export const AuthLayout = ({ children }: { children: React.ReactNode }) => {
const params = new URLSearchParams(url.toString()); const params = new URLSearchParams(url.toString());
params.delete("error"); params.delete("error");
const newUrl = const newUrl =
window.location.pathname + (params.toString() ? `?${params.toString()}` : ""); window.location.pathname +
(params.toString() ? `?${params.toString()}` : "");
window.history.replaceState({}, document.title, newUrl); window.history.replaceState({}, document.title, newUrl);
}, [url]); }, [url]);
+7 -1
View File
@@ -44,6 +44,12 @@ import { getOAuthProviders } from "./oauth";
const oidcProviders = getOidcProviders(); const oidcProviders = getOidcProviders();
export const auth = betterAuth({ export const auth = betterAuth({
onAPIError: {
errorURL: "/dashboard/home",
onError: (error, ctx) => {
//todo: capture errors in a monitoring service
},
},
database: drizzleAdapter(db, { database: drizzleAdapter(db, {
provider: "pg", provider: "pg",
schema: drizzleDb.schemas, schema: drizzleDb.schemas,
@@ -134,7 +140,7 @@ export const auth = betterAuth({
trustedProviders: [ trustedProviders: [
"credential", "credential",
...getOAuthProviders().map((p) => p.id), ...getOAuthProviders().map((p) => p.id),
...oidcProviders.map((p) => p.id) ...oidcProviders.map((p) => p.id),
], ],
allowDifferentEmails: true, allowDifferentEmails: true,
}, },