fix: toast error message,social legacy

This commit is contained in:
Théo LAGACHE
2026-02-26 15:29:00 +01:00
parent c8d390726c
commit 8d414080a4
11 changed files with 269 additions and 273 deletions
+13 -10
View File
@@ -1,10 +1,10 @@
# Environnement # Environment
NODE_ENV=development NODE_ENV=development
# Base de données # Database
DATABASE_URL=postgresql://devuser:changeme@localhost:5433/devdb?schema=public DATABASE_URL=postgresql://devuser:changeme@localhost:5433/devdb?schema=public
# Projet # Project
PROJECT_NAME="Portabase" PROJECT_NAME="Portabase"
PROJECT_DESCRIPTION="Portabase is a powerful database manager" PROJECT_DESCRIPTION="Portabase is a powerful database manager"
PROJECT_URL=http://localhost:8887 PROJECT_URL=http://localhost:8887
@@ -21,11 +21,7 @@ SMTP_FROM=
#Port 465 -> secure: true #Port 465 -> secure: true
#By default : true #By default : true
# Google # OIDC SSO Authentification
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=
AUTH_GOOGLE_METHOD=
AUTH_OIDC_ID="" AUTH_OIDC_ID=""
AUTH_OIDC_TITLE="" AUTH_OIDC_TITLE=""
AUTH_OIDC_DESC="" AUTH_OIDC_DESC=""
@@ -40,14 +36,21 @@ AUTH_OIDC_JWKS_ENDPOINT=""
AUTH_OIDC_PKCE=true AUTH_OIDC_PKCE=true
ALLOWED_GROUP="admin" ALLOWED_GROUP="admin"
# Social OAuth2 Authentification
AUTH_SOCIAL_ID=""
AUTH_SOCIAL_TITLE=""
AUTH_SOCIAL_DESC=""
AUTH_SOCIAL_ICON=""
AUTH_SOCIAL_CLIENT=""
AUTH_SOCIAL_SECRET=""
AUTH_SOCIAL_APPLE_APP_BUNDLE_IDENTIFIER=""
# If true, the user's role will be updated from OIDC claims on every login. # If true, the user's role will be updated from OIDC claims on every login.
# If false, the role is only set when the user is first created. # If false, the role is only set when the user is first created.
AUTH_SYNC_OIDC_ROLES_ON_LOGIN=true AUTH_SYNC_OIDC_ROLES_ON_LOGIN=true
AUTH_EMAIL_PASSWORD_ENABLED=true AUTH_EMAIL_PASSWORD_ENABLED=true
AUTH_SIGNUP_ENABLED=true AUTH_SIGNUP_ENABLED=true
AUTH_PASSKEY_ENABLED=true AUTH_PASSKEY_ENABLED=true
# Retention # Retention
RETENTION_CRON="* * * * *" RETENTION_CRON="* * * * *"
+1 -5
View File
@@ -22,11 +22,7 @@ export default async function Layout({
<div className="flex flex-col items-center justify-center flex-1"> <div className="flex flex-col items-center justify-center flex-1">
<div className="mx-auto w-full max-w-md"> <div className="mx-auto w-full max-w-md">
<AuthLogoSection /> <AuthLogoSection />
<div className="mt-4"> <div className="mt-4">{children}</div>
<Suspense>
<ErrorLayout>{children}</ErrorLayout>
</Suspense>
</div>
</div> </div>
</div> </div>
<footer className="mt-8 text-center text-xs text-muted-foreground flex flex-col gap-1"> <footer className="mt-8 text-center text-xs text-muted-foreground flex flex-col gap-1">
+1 -6
View File
@@ -1,4 +1,4 @@
import { ReactNode, Suspense } from "react"; import { ReactNode } 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";
@@ -6,7 +6,6 @@ import { AppSidebar } from "@/components/wrappers/dashboard/common/sidebar/app-s
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 { 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();
@@ -19,11 +18,7 @@ export default async function Layout({ children }: { children: ReactNode }) {
<AppSidebar /> <AppSidebar />
<SidebarInset> <SidebarInset>
<Header /> <Header />
<Suspense>
<ErrorLayout>
<main className="h-full">{children}</main> <main className="h-full">{children}</main>
</ErrorLayout>
</Suspense>
</SidebarInset> </SidebarInset>
</div> </div>
</SidebarProvider> </SidebarProvider>
+25 -77
View File
@@ -1,85 +1,33 @@
// "use client" "use client";
//
// import {useSearchParams} from "next/navigation"
//
// enum Error {
// Configuration = "Configuration",
// }
//
// const errorMap = {
// [Error.Configuration]: (
// <p>
// There was a problem when trying to authenticate. Please contact us if this
// error persists. Unique error code:{" "}
// <code className="rounded-sm bg-slate-100 p-1 text-xs">Configuration</code>
// </p>
// ),
// }
//
// export default function AuthErrorPage() {
// const search = useSearchParams()
// const error = search.get("error") as Error
//
// return (
// <div className="flex h-screen w-full flex-col items-center justify-center">
// <a
// href="#"
// className="block max-w-sm rounded-lg border border-gray-200 bg-white p-6 text-center shadow-sm hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700"
// >
// <h5 className="mb-2 flex flex-row items-center justify-center gap-2 text-xl font-bold tracking-tight text-gray-900 dark:text-white">
// Something went wrong
// </h5>
// <div className="font-normal text-gray-700 dark:text-gray-400">
// {errorMap[error] || "Please contact us if this error persists."}
// </div>
// </a>
// </div>
// )
// }
"use client"
import { useSearchParams } from "next/navigation" import { Loader2 } from "lucide-react";
import React, { Suspense } from "react" import { useRouter, useSearchParams } from "next/navigation";
import { useEffect } from "react";
import { useSession } from "@/lib/auth/auth-client";
enum Error { export default function ErrorPage() {
Configuration = "Configuration", const router = useRouter();
} const searchParams = useSearchParams();
const { data: session, isPending } = useSession();
const errorMap = { useEffect(() => {
[Error.Configuration]: ( if (isPending) {
<p> return;
There was a problem when trying to authenticate. Please contact us if this }
error persists. Unique error code:{" "}
<code className="rounded-sm bg-slate-100 p-1 text-xs">Configuration</code>
</p>
),
}
function AuthErrorPageContent() { const error = searchParams.get("error");
const search = useSearchParams() const params = new URLSearchParams();
const error = search.get("error") as Error if (error) {
params.set("error", error);
}
const destination = session ? "/dashboard/home" : "/login";
router.replace(`${destination}?${params.toString()}`);
}, [isPending, session, router, searchParams]);
return ( return (
<div className="flex h-screen w-full flex-col items-center justify-center"> <div className="flex h-screen w-full items-center justify-center">
<a <Loader2 className="h-8 w-8 animate-spin" />
href="#"
className="block max-w-sm rounded-lg border border-gray-200 bg-white p-6 text-center shadow-sm hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700"
>
<h5 className="mb-2 flex flex-row items-center justify-center gap-2 text-xl font-bold tracking-tight text-gray-900 dark:text-white">
Something went wrong
</h5>
<div className="font-normal text-gray-700 dark:text-gray-400">
{errorMap[error] || "Please contact us if this error persists."}
</div> </div>
</a> );
</div>
)
}
export default function AuthErrorPage() {
return (
<Suspense fallback={<div>Loading...</div>}>
<AuthErrorPageContent />
</Suspense>
)
} }
+19 -15
View File
@@ -1,37 +1,41 @@
import React from "react"; import type { Metadata } from "next";
import type {Metadata} from "next"; import type React from "react";
import "./globals.css"; import "./globals.css";
import {Providers} from "./providers"; import { ConsoleSilencer } from "@/components/wrappers/common/console-silencer";
import {cn} from "@/lib/utils"; import { author, geistMono, poppins } from "@/fonts/fonts";
import {ConsoleSilencer} from "@/components/wrappers/common/console-silencer"; import { cn } from "@/lib/utils";
import {author, geistMono, poppins} from "@/fonts/fonts"; import { Providers } from "./providers";
const title = process.env.PROJECT_NAME ?? "App Title"; const title = process.env.PROJECT_NAME ?? "App Title";
export const metadata: Metadata = { export const metadata: Metadata = {
title: { title: {
default: title, default: title,
template: `%s - ${title}` template: `%s - ${title}`,
}, },
description: process.env.PROJECT_DESCRIPTION ?? undefined, description: process.env.PROJECT_DESCRIPTION ?? undefined,
}; };
export default async function RootLayout({ export default async function RootLayout({
children, children,
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="en" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
<head> <head>
<meta name="apple-mobile-web-app-title" content={title}/> <meta name="apple-mobile-web-app-title" content={title} />
</head> </head>
<body className={cn(poppins.variable, author.variable, geistMono.variable, "font-sans h-full")}> <body
<ConsoleSilencer/> className={cn(
<Providers> poppins.variable,
{children} author.variable,
</Providers> geistMono.variable,
"font-sans h-full",
)}
>
<ConsoleSilencer />
<Providers>{children}</Providers>
</body> </body>
</html> </html>
); );
+8 -4
View File
@@ -1,15 +1,19 @@
import BackButton from "@/components/wrappers/common/button/back-button"; import BackButton from "@/components/wrappers/common/button/back-button";
export default async function NotFound() { export default async function NotFound() {
return( return (
<div className="flex items-center min-h-screen px-4 py-12 sm:px-6 md:px-8 lg:px-12 xl:px-16"> <div className="flex items-center min-h-screen px-4 py-12 sm:px-6 md:px-8 lg:px-12 xl:px-16">
<div className="w-full space-y-6 text-center"> <div className="w-full space-y-6 text-center">
<div className="space-y-3"> <div className="space-y-3">
<h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">Not found</h1> <h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">
<p className="leading-7 [&:not(:first-child)]:mt-6">The content you are trying to view is not available.</p> Not found
</h1>
<p className="leading-7 not-first:mt-6">
The content you are trying to view is not available.
</p>
</div> </div>
<BackButton>Go home</BackButton> <BackButton>Go home</BackButton>
</div> </div>
</div> </div>
) );
} }
+11 -13
View File
@@ -1,12 +1,12 @@
"use client"; "use client";
import {PropsWithChildren, Suspense} from "react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {ThemeProvider} from "@/features/theme/theme-provider"; import { type PropsWithChildren, Suspense } from "react";
import {Toaster} from "@/components/ui/sonner";
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
import {ThemeMetaUpdaterRoot} from "@/features/browser/theme-meta-updater-root";
import { Toaster } from "@/components/ui/sonner";
import { ErrorLayout } from "@/components/wrappers/common/error-layout";
import { ThemeMetaUpdaterRoot } from "@/features/browser/theme-meta-updater-root";
import { ThemeProvider } from "@/features/theme/theme-provider";
export type ProviderProps = PropsWithChildren<{}>; export type ProviderProps = PropsWithChildren<{}>;
const queryClient = new QueryClient(); const queryClient = new QueryClient();
@@ -14,15 +14,13 @@ const queryClient = new QueryClient();
export const Providers = (props: ProviderProps) => { export const Providers = (props: ProviderProps) => {
return ( return (
<Suspense fallback={null}> <Suspense fallback={null}>
<ThemeProvider <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
attribute="class" <ThemeMetaUpdaterRoot />
defaultTheme="system"
enableSystem
>
<ThemeMetaUpdaterRoot/>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<Toaster/> <ErrorLayout>
<Toaster />
{props.children} {props.children}
</ErrorLayout>
</QueryClientProvider> </QueryClientProvider>
</ThemeProvider> </ThemeProvider>
</Suspense> </Suspense>
+7 -2
View File
@@ -29,7 +29,6 @@ export const env = createEnv({
AUTH_GOOGLE_ID: z.string().optional(), AUTH_GOOGLE_ID: z.string().optional(),
AUTH_GOOGLE_SECRET: z.string().optional(), AUTH_GOOGLE_SECRET: z.string().optional(),
AUTH_GOOGLE_METHOD: z.boolean().default(false),
AUTH_GITHUB_ID: z.string().optional(), AUTH_GITHUB_ID: z.string().optional(),
AUTH_GITHUB_SECRET: z.string().optional(), AUTH_GITHUB_SECRET: z.string().optional(),
@@ -69,7 +68,7 @@ export const env = createEnv({
AUTH_SYNC_OIDC_ROLES_ON_LOGIN: z.enum(["true", "false"]).default("true"), AUTH_SYNC_OIDC_ROLES_ON_LOGIN: z.enum(["true", "false"]).default("true"),
AUTH_ROLE_MAP: z.string().optional(), AUTH_ROLE_MAP: z.string().optional(),
AUTH_DEFAULT_ROLE: z.string().optional(), AUTH_DEFAULT_ROLE: z.string().optional().default("pending"),
AUTH_ALLOW_LINKING: z.enum(["true", "false"]).default("false"), AUTH_ALLOW_LINKING: z.enum(["true", "false"]).default("false"),
AUTH_ALLOW_UNLINKING: z.enum(["true", "false"]).default("false"), AUTH_ALLOW_UNLINKING: z.enum(["true", "false"]).default("false"),
@@ -110,6 +109,12 @@ export const env = createEnv({
AUTH_OIDC_JWKS_ENDPOINT: process.env.AUTH_OIDC_JWKS_ENDPOINT, AUTH_OIDC_JWKS_ENDPOINT: process.env.AUTH_OIDC_JWKS_ENDPOINT,
AUTH_OIDC_PKCE: process.env.AUTH_OIDC_PKCE, AUTH_OIDC_PKCE: process.env.AUTH_OIDC_PKCE,
AUTH_GOOGLE_ID: process.env.AUTH_GOOGLE_ID,
AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET,
AUTH_GITHUB_ID: process.env.AUTH_GITHUB_ID,
AUTH_GITHUB_SECRET: process.env.AUTH_GITHUB_SECRET,
AUTH_SOCIAL_ID: process.env.AUTH_SOCIAL_ID, AUTH_SOCIAL_ID: process.env.AUTH_SOCIAL_ID,
AUTH_SOCIAL_TITLE: process.env.AUTH_SOCIAL_TITLE, AUTH_SOCIAL_TITLE: process.env.AUTH_SOCIAL_TITLE,
AUTH_SOCIAL_DESC: process.env.AUTH_SOCIAL_DESC, AUTH_SOCIAL_DESC: process.env.AUTH_SOCIAL_DESC,
+1 -1
View File
@@ -45,7 +45,7 @@ const oidcProviders = getOidcProviders();
export const auth = betterAuth({ export const auth = betterAuth({
onAPIError: { onAPIError: {
errorURL: "/dashboard/home", errorURL: "/error",
onError: (error, ctx) => { onError: (error, ctx) => {
//todo: capture errors in a monitoring service //todo: capture errors in a monitoring service
}, },
+32
View File
@@ -56,6 +56,38 @@ function getProviderIcon(providerId: string, envIcon?: string): string {
export function getOAuthProviders(): OAuthProvider[] { export function getOAuthProviders(): OAuthProvider[] {
const providers: OAuthProvider[] = []; const providers: OAuthProvider[] = [];
if (env.AUTH_GOOGLE_ID && env.AUTH_GOOGLE_SECRET) {
providers.push({
id: "google",
title: "Google",
description: "Sign in with your Google account.",
icon: getProviderIcon("google"),
client: env.AUTH_GOOGLE_ID,
secret: env.AUTH_GOOGLE_SECRET,
allowedGroup: env.ALLOWED_GROUP,
roleMap: env.AUTH_ROLE_MAP ?? undefined,
defaultRole: env.AUTH_DEFAULT_ROLE ?? "pending",
allowLinking: env.AUTH_ALLOW_LINKING !== "false",
allowUnlinking: env.AUTH_ALLOW_UNLINKING !== "false",
});
}
if (env.AUTH_GITHUB_ID && env.AUTH_GITHUB_SECRET) {
providers.push({
id: "github",
title: "GitHub",
description: "Sign in with your GitHub account.",
icon: getProviderIcon("github"),
client: env.AUTH_GITHUB_ID,
secret: env.AUTH_GITHUB_SECRET,
allowedGroup: env.ALLOWED_GROUP,
roleMap: env.AUTH_ROLE_MAP,
defaultRole: env.AUTH_DEFAULT_ROLE,
allowLinking: env.AUTH_ALLOW_LINKING !== "false",
allowUnlinking: env.AUTH_ALLOW_UNLINKING !== "false",
});
}
if (env.AUTH_SOCIAL_CLIENT && env.AUTH_SOCIAL_ID) { if (env.AUTH_SOCIAL_CLIENT && env.AUTH_SOCIAL_ID) {
providers.push({ providers.push({
id: env.AUTH_SOCIAL_ID, id: env.AUTH_SOCIAL_ID,
+29 -18
View File
@@ -1,11 +1,10 @@
import {env} from "@/env.mjs"; import { env } from "@/env.mjs";
import {db, makeMigration} from "@/db"; import { db, makeMigration } from "@/db";
import {eq} from "drizzle-orm"; import { eq } from "drizzle-orm";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {cleaningJob, retentionJob} from "@/lib/tasks"; import { cleaningJob, retentionJob } from "@/lib/tasks";
import {generateRSAKeys, getOrCreateMasterKey} from "@/utils/rsa-keys"; import { generateRSAKeys, getOrCreateMasterKey } from "@/utils/rsa-keys";
import {StorageProviderKind} from "@/features/storages/types"; import { StorageProviderKind } from "@/features/storages/types";
export async function init() { export async function init() {
consoleAscii(); consoleAscii();
@@ -14,10 +13,19 @@ export async function init() {
await generateRSAKeys(); await generateRSAKeys();
await makeMigration(); await makeMigration();
await createDefaultOrganization(); await createDefaultOrganization();
await createSettingsIfNotExist() await createSettingsIfNotExist();
console.log("====Initialization completed===="); console.log("====Initialization completed====");
await setupCronJobs() await setupCronJobs();
await setupCleaningJobs() await setupCleaningJobs();
if (
(env.AUTH_GOOGLE_ID && env.AUTH_GOOGLE_SECRET) ||
(env.AUTH_GITHUB_ID && env.AUTH_GITHUB_SECRET)
) {
console.warn(
"[Deprecation Warning] You have set up OAuth credentials in your environment variables, but the format is now different. Please update your environment variables to use the new format. For example, if you were using AUTH_GOOGLE_ID and AUTH_GOOGLE_SECRET, you should now use AUTH_GOOGLE_CLIENT and AUTH_GOOGLE_SECRET. Please refer to the documentation for more details.",
);
}
} }
async function setupCronJobs() { async function setupCronJobs() {
@@ -61,7 +69,6 @@ async function createSettingsIfNotExist() {
.values(systemSettingsValues) .values(systemSettingsValues)
.returning(); .returning();
const localStorageValues = { const localStorageValues = {
provider: "local" as StorageProviderKind, provider: "local" as StorageProviderKind,
enabled: true, enabled: true,
@@ -86,17 +93,15 @@ async function createSettingsIfNotExist() {
.values(localStorageValues) .values(localStorageValues)
.returning(); .returning();
if (!finalSystemSetting.defaultStorageChannelId) { if (!finalSystemSetting.defaultStorageChannelId) {
await tx await tx
.update(drizzleDb.schemas.setting) .update(drizzleDb.schemas.setting)
.set({defaultStorageChannelId: localStorage.id}) .set({ defaultStorageChannelId: localStorage.id })
.where(eq(drizzleDb.schemas.setting.id, finalSystemSetting.id)); .where(eq(drizzleDb.schemas.setting.id, finalSystemSetting.id));
} }
}); });
} }
async function createDefaultOrganization() { async function createDefaultOrganization() {
const defaultOrganizationConf = { const defaultOrganizationConf = {
slug: "default", slug: "default",
@@ -104,11 +109,17 @@ async function createDefaultOrganization() {
createdAt: new Date(), createdAt: new Date(),
}; };
const [existing] = await db.select().from(drizzleDb.schemas.organization).where(eq(drizzleDb.schemas.organization.slug, "default")).limit(1); const [existing] = await db
.select()
.from(drizzleDb.schemas.organization)
.where(eq(drizzleDb.schemas.organization.slug, "default"))
.limit(1);
if (!existing) { if (!existing) {
console.log("==== Creating default Organization... ===="); console.log("==== Creating default Organization... ====");
await db.insert(drizzleDb.schemas.organization).values(defaultOrganizationConf); await db
.insert(drizzleDb.schemas.organization)
.values(defaultOrganizationConf);
} }
} }
@@ -122,6 +133,6 @@ function consoleAscii() {
" /_/ \\____/_/ \\__/\\__,_/_.___/\\__,_/____/\\___/ \n" + " /_/ \\____/_/ \\__/\\__,_/_.___/\\__,_/____/\\___/ \n" +
" \n" + " \n" +
` Community Edition v${env.NEXT_PUBLIC_PROJECT_VERSION} \n ` + ` Community Edition v${env.NEXT_PUBLIC_PROJECT_VERSION} \n ` +
" \n" " \n",
) );
} }