From 4fc87b71342aa955616d48c8157cf081ad55374a Mon Sep 17 00:00:00 2001 From: Rafi Arrafif Date: Tue, 17 Feb 2026 21:32:27 +0700 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=9B=82=20security:=20fix=20auth=20tok?= =?UTF-8?q?en=20validation=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(safe-mode-page)/auth/logout/page.tsx | 9 +++++++++ .../auth/providers/[name]/callback/page.tsx | 0 shared/helpers/backendFetch.ts | 14 +++++--------- shared/models/auth/logout.ts | 6 +++--- shared/models/auth/validateAndDecodeJWT.ts | 17 +++++++---------- 5 files changed, 24 insertions(+), 22 deletions(-) create mode 100644 app/(safe-mode-page)/auth/logout/page.tsx rename app/{(session)/(clean) => (safe-mode-page)}/auth/providers/[name]/callback/page.tsx (100%) diff --git a/app/(safe-mode-page)/auth/logout/page.tsx b/app/(safe-mode-page)/auth/logout/page.tsx new file mode 100644 index 0000000..3d45472 --- /dev/null +++ b/app/(safe-mode-page)/auth/logout/page.tsx @@ -0,0 +1,9 @@ +import { cookies } from "next/headers"; +import { redirect } from "next/navigation"; + +const page = async () => { + (await cookies()).delete("auth_token"); + redirect("/"); +}; + +export default page; diff --git a/app/(session)/(clean)/auth/providers/[name]/callback/page.tsx b/app/(safe-mode-page)/auth/providers/[name]/callback/page.tsx similarity index 100% rename from app/(session)/(clean)/auth/providers/[name]/callback/page.tsx rename to app/(safe-mode-page)/auth/providers/[name]/callback/page.tsx diff --git a/shared/helpers/backendFetch.ts b/shared/helpers/backendFetch.ts index abb0e99..afe346f 100644 --- a/shared/helpers/backendFetch.ts +++ b/shared/helpers/backendFetch.ts @@ -5,6 +5,7 @@ import { UAParser } from "ua-parser-js"; export interface BackendResponse { success: boolean; + status: number; message: string; data?: T; error?: unknown; @@ -34,16 +35,11 @@ export const backendFetch = async (path: string, options: RequestInit = {}) => { ...options.headers, }, cache: "default", - }); + }).then((response) => response.json()); - const resJson = (await res.json()) as BackendResponse; - - if (!res.ok) { - throw new Error(`Elysia error: ${resJson.error}`); - } - - return resJson; - } catch { + return res as BackendResponse; + } catch (res) { + if (process.env.NODE_ENV === "development") return res; redirect("/status?reason=backend-unreachable"); } }; diff --git a/shared/models/auth/logout.ts b/shared/models/auth/logout.ts index 4e01ddb..23ab5bf 100644 --- a/shared/models/auth/logout.ts +++ b/shared/models/auth/logout.ts @@ -1,12 +1,12 @@ "use server"; -import { backendFetch } from "@/shared/helpers/backendFetch"; +import { backendFetch, BackendResponse } from "@/shared/helpers/backendFetch"; import { cookies } from "next/headers"; export const logout = async () => { - const res = await backendFetch("auth/logout", { + const res = (await backendFetch("auth/logout", { method: "POST", - }); + })) as BackendResponse; if (res.success) { (await cookies()).delete("auth_token"); diff --git a/shared/models/auth/validateAndDecodeJWT.ts b/shared/models/auth/validateAndDecodeJWT.ts index ba60b0d..6c2ef10 100644 --- a/shared/models/auth/validateAndDecodeJWT.ts +++ b/shared/models/auth/validateAndDecodeJWT.ts @@ -1,6 +1,7 @@ "use server"; import { backendFetch, BackendResponse } from "@/shared/helpers/backendFetch"; +import { redirect } from "next/navigation"; import { cookies } from "next/headers"; export interface UserSession { @@ -30,18 +31,14 @@ export interface UserSession { } export const validateAndDecodeJWT = async (): Promise => { - const cookieHeader = (await cookies()).get("auth_token")?.value; - - if (!cookieHeader) { - return null; - } - + "use server"; const res = (await backendFetch("auth/token/validate", { method: "POST", - body: JSON.stringify({ - token: cookieHeader, - }), })) as BackendResponse; - return res.data!; + if (res.status === 403) { + redirect("/auth/logout"); + } + + return res.data ?? null; }; From 0c9ca45b36883204066857677dce7c6e4afc5ba4 Mon Sep 17 00:00:00 2001 From: Rafi Arrafif Date: Wed, 18 Feb 2026 12:27:24 +0700 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=A5=85=20fix:=20handle=20logout=20fai?= =?UTF-8?q?lure=20warning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/layout.tsx | 4 +++- shared/models/auth/logout.ts | 7 ++----- shared/widgets/navbar/components/LogoutAlert.tsx | 14 +++++++++++--- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/app/layout.tsx b/app/layout.tsx index f65b89c..b87e432 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono, Inter } from "next/font/google"; import "./globals.css"; +import { Toaster } from "@/shared/libs/shadcn/ui/sonner"; const inter = Inter({ subsets: ["latin"], variable: "--font-sans" }); @@ -29,7 +30,8 @@ export default function RootLayout({ - {children} +
{children}
+ ); diff --git a/shared/models/auth/logout.ts b/shared/models/auth/logout.ts index 23ab5bf..92ffd02 100644 --- a/shared/models/auth/logout.ts +++ b/shared/models/auth/logout.ts @@ -2,6 +2,7 @@ import { backendFetch, BackendResponse } from "@/shared/helpers/backendFetch"; import { cookies } from "next/headers"; +import { redirect } from "next/navigation"; export const logout = async () => { const res = (await backendFetch("auth/logout", { @@ -9,11 +10,7 @@ export const logout = async () => { })) as BackendResponse; if (res.success) { - (await cookies()).delete("auth_token"); - return { - success: true, - message: "Logged out successfully", - }; + redirect("/auth/logout"); } else { return { success: false, diff --git a/shared/widgets/navbar/components/LogoutAlert.tsx b/shared/widgets/navbar/components/LogoutAlert.tsx index b15bd34..9595904 100644 --- a/shared/widgets/navbar/components/LogoutAlert.tsx +++ b/shared/widgets/navbar/components/LogoutAlert.tsx @@ -12,6 +12,7 @@ import { Spinner } from "@/shared/libs/shadcn/ui/spinner"; import { logout } from "@/shared/models/auth/logout"; import { Button } from "@base-ui/react"; import React from "react"; +import { toast } from "sonner"; const LogoutAlert = ({ openState, @@ -23,9 +24,16 @@ const LogoutAlert = ({ const [isLoading, setIsLoading] = React.useState(false); const continueLogout = async () => { setIsLoading(true); - await logout().then((res) => - res.success ? window.location.reload() : setIsLoading(false), - ); + const res = await logout(); + if (!res.success) { + setIsLoading(false); + toast.error(res.message || "Logout failed", { + position: "bottom-right", + description: + "An error occurred while logging out. Please try again later.", + richColors: true, + }); + } }; return ( From 39124f0db4d052d0c836964b43f38674f06ddf27 Mon Sep 17 00:00:00 2001 From: Rafi Arrafif Date: Wed, 18 Feb 2026 12:53:58 +0700 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=9A=B8=20ux:=20improve=20logout=20flo?= =?UTF-8?q?w=20completely?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(safe-mode-page)/auth/logout/page.tsx | 9 --------- app/(safe-mode-page)/auth/logout/route.tsx | 7 +++++++ shared/models/auth/logout.ts | 7 ++++--- shared/widgets/navbar/components/LogoutAlert.tsx | 12 ++++++++++++ 4 files changed, 23 insertions(+), 12 deletions(-) delete mode 100644 app/(safe-mode-page)/auth/logout/page.tsx create mode 100644 app/(safe-mode-page)/auth/logout/route.tsx diff --git a/app/(safe-mode-page)/auth/logout/page.tsx b/app/(safe-mode-page)/auth/logout/page.tsx deleted file mode 100644 index 3d45472..0000000 --- a/app/(safe-mode-page)/auth/logout/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { cookies } from "next/headers"; -import { redirect } from "next/navigation"; - -const page = async () => { - (await cookies()).delete("auth_token"); - redirect("/"); -}; - -export default page; diff --git a/app/(safe-mode-page)/auth/logout/route.tsx b/app/(safe-mode-page)/auth/logout/route.tsx new file mode 100644 index 0000000..d33ed31 --- /dev/null +++ b/app/(safe-mode-page)/auth/logout/route.tsx @@ -0,0 +1,7 @@ +import { cookies } from "next/headers"; +import { NextResponse } from "next/server"; + +export const GET = async (request: Request) => { + (await cookies()).delete("auth_token"); + return NextResponse.redirect(new URL("/", request.url), 303); +}; diff --git a/shared/models/auth/logout.ts b/shared/models/auth/logout.ts index 92ffd02..4502582 100644 --- a/shared/models/auth/logout.ts +++ b/shared/models/auth/logout.ts @@ -1,8 +1,6 @@ "use server"; import { backendFetch, BackendResponse } from "@/shared/helpers/backendFetch"; -import { cookies } from "next/headers"; -import { redirect } from "next/navigation"; export const logout = async () => { const res = (await backendFetch("auth/logout", { @@ -10,7 +8,10 @@ export const logout = async () => { })) as BackendResponse; if (res.success) { - redirect("/auth/logout"); + return { + success: true, + message: "Logout successful", + }; } else { return { success: false, diff --git a/shared/widgets/navbar/components/LogoutAlert.tsx b/shared/widgets/navbar/components/LogoutAlert.tsx index 9595904..cea86e6 100644 --- a/shared/widgets/navbar/components/LogoutAlert.tsx +++ b/shared/widgets/navbar/components/LogoutAlert.tsx @@ -11,6 +11,7 @@ import { import { Spinner } from "@/shared/libs/shadcn/ui/spinner"; import { logout } from "@/shared/models/auth/logout"; import { Button } from "@base-ui/react"; +import { useRouter } from "next/navigation"; import React from "react"; import { toast } from "sonner"; @@ -21,6 +22,7 @@ const LogoutAlert = ({ openState: boolean; setOpenState: React.Dispatch>; }) => { + const router = useRouter(); const [isLoading, setIsLoading] = React.useState(false); const continueLogout = async () => { setIsLoading(true); @@ -33,6 +35,16 @@ const LogoutAlert = ({ "An error occurred while logging out. Please try again later.", richColors: true, }); + } else { + toast.success(res.message || "Logout successful", { + position: "bottom-right", + description: "You have been logged out successfully.", + richColors: true, + }); + router.push("/auth/logout"); + setTimeout(() => { + window.location.reload(); + }, 2000); } }; From 879afd94dee46f123fdf8dc77ef86b7097fd4a17 Mon Sep 17 00:00:00 2001 From: Rafi Arrafif Date: Wed, 18 Feb 2026 12:56:29 +0700 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=A9=B9=20fix:=20resolve=20build=20err?= =?UTF-8?q?or?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- features/authCallback/actions/submitProviderCallback.ts | 1 + shared/models/auth/validateAndDecodeJWT.ts | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/features/authCallback/actions/submitProviderCallback.ts b/features/authCallback/actions/submitProviderCallback.ts index 3a0d179..8afb005 100644 --- a/features/authCallback/actions/submitProviderCallback.ts +++ b/features/authCallback/actions/submitProviderCallback.ts @@ -47,6 +47,7 @@ export const submitProviderCallback = async ( } catch (error) { return { success: false, + status: 500, message: "Error submitting provider callback", error: error, }; diff --git a/shared/models/auth/validateAndDecodeJWT.ts b/shared/models/auth/validateAndDecodeJWT.ts index 6c2ef10..96c3ac8 100644 --- a/shared/models/auth/validateAndDecodeJWT.ts +++ b/shared/models/auth/validateAndDecodeJWT.ts @@ -2,7 +2,6 @@ import { backendFetch, BackendResponse } from "@/shared/helpers/backendFetch"; import { redirect } from "next/navigation"; -import { cookies } from "next/headers"; export interface UserSession { id: string;