refactor: update session handling across components to use new session structure and improve loading states
This commit is contained in:
parent
621d227760
commit
3f477ca3b8
18 changed files with 226 additions and 132 deletions
|
|
@ -16,7 +16,6 @@ import {
|
||||||
useSession,
|
useSession,
|
||||||
} from "~/providers/SessionProvider";
|
} from "~/providers/SessionProvider";
|
||||||
import type { UsersId } from "~/schemas/public/Users";
|
import type { UsersId } from "~/schemas/public/Users";
|
||||||
import type { ValidSession } from "~/server/api/trpc";
|
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { UserViewHeader } from "./area-riservata/userViewHeader";
|
import { UserViewHeader } from "./area-riservata/userViewHeader";
|
||||||
|
|
||||||
|
|
@ -73,9 +72,9 @@ export const AreaRiservataLayout = ({
|
||||||
const { data: bannerData } = api.banners.getBannerData.useQuery({
|
const { data: bannerData } = api.banners.getBannerData.useQuery({
|
||||||
area_riservata: true,
|
area_riservata: true,
|
||||||
});
|
});
|
||||||
const session = useSession();
|
const { status, user } = useSession();
|
||||||
if (!ignoreSessionCheck) {
|
if (!ignoreSessionCheck) {
|
||||||
if (session.status === "pending")
|
if (status === "LOADING")
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -97,7 +96,7 @@ export const AreaRiservataLayout = ({
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
if (session.status !== "success" || !session.user) {
|
if (status !== "AUTHENTICATED") {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -122,6 +121,27 @@ export const AreaRiservataLayout = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (status !== "AUTHENTICATED") {
|
||||||
|
window.location.reload();
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex h-full min-h-screen flex-col text-clip",
|
||||||
|
bodyClassName,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<SiteHeader />
|
||||||
|
|
||||||
|
<main
|
||||||
|
className={cn("flex h-full flex-1 overflow-auto", containerClassName)}
|
||||||
|
>
|
||||||
|
<div className="flex h-auto w-full flex-col md:flex-row">
|
||||||
|
<LoadingPage />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -143,10 +163,10 @@ export const AreaRiservataLayout = ({
|
||||||
{noSidebar ? null : (
|
{noSidebar ? null : (
|
||||||
<Sidebar
|
<Sidebar
|
||||||
defaultOpen={getCookie(SIDEBAR_COOKIE_NAME) === "true"}
|
defaultOpen={getCookie(SIDEBAR_COOKIE_NAME) === "true"}
|
||||||
isAdmin={session.user?.isAdmin || false}
|
isAdmin={user?.isAdmin || false}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<EnforcedSessionContext.Provider value={session as ValidSession}>
|
<EnforcedSessionContext.Provider value={{ user, status }}>
|
||||||
{children}
|
{children}
|
||||||
</EnforcedSessionContext.Provider>
|
</EnforcedSessionContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -160,12 +180,12 @@ export const AreaRiservataLayoutUserView = ({
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode & { props: { userId: UsersId } };
|
children: ReactNode & { props: { userId: UsersId } };
|
||||||
}) => {
|
}) => {
|
||||||
const session = useSession();
|
const { status, user } = useSession();
|
||||||
const props = children.props as { userId: UsersId };
|
const props = children.props as { userId: UsersId };
|
||||||
if (session.status === "pending") {
|
if (status === "LOADING") {
|
||||||
return <LoadingPage />;
|
return <LoadingPage />;
|
||||||
}
|
}
|
||||||
if (session.status === "error" || !session.user) {
|
if (status !== "AUTHENTICATED") {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
return <LoadingPage />;
|
return <LoadingPage />;
|
||||||
}
|
}
|
||||||
|
|
@ -174,7 +194,7 @@ export const AreaRiservataLayoutUserView = ({
|
||||||
<SiteHeader />
|
<SiteHeader />
|
||||||
<main className="flex h-full flex-1 overflow-auto">
|
<main className="flex h-full flex-1 overflow-auto">
|
||||||
<UserViewProvider userId={props.userId}>
|
<UserViewProvider userId={props.userId}>
|
||||||
<EnforcedSessionContext.Provider value={session as ValidSession}>
|
<EnforcedSessionContext.Provider value={{ user, status }}>
|
||||||
<Head>
|
<Head>
|
||||||
<title>User View - Infoalloggi.it</title>
|
<title>User View - Infoalloggi.it</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
|
||||||
|
|
@ -7,20 +7,16 @@ import { Button } from "~/components/ui/button";
|
||||||
import { useAnnuncio } from "~/providers/AnnuncioProvider";
|
import { useAnnuncio } from "~/providers/AnnuncioProvider";
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
import { useServizio } from "~/providers/ServizioProvider";
|
import { useServizio } from "~/providers/ServizioProvider";
|
||||||
import type { SessionContextType } from "~/providers/SessionProvider";
|
import { useSession } from "~/providers/SessionProvider";
|
||||||
import type { AnnunciId } from "~/schemas/public/Annunci";
|
import type { AnnunciId } from "~/schemas/public/Annunci";
|
||||||
import type { UsersId } from "~/schemas/public/Users";
|
import type { UsersId } from "~/schemas/public/Users";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
export const AnnuncioInteractions = ({
|
export const AnnuncioInteractions = ({ disabled }: { disabled?: boolean }) => {
|
||||||
session,
|
const { user } = useSession();
|
||||||
disabled,
|
|
||||||
}: {
|
|
||||||
session: SessionContextType;
|
|
||||||
disabled?: boolean;
|
|
||||||
}) => {
|
|
||||||
const { id, tipo } = useAnnuncio();
|
const { id, tipo } = useAnnuncio();
|
||||||
const { locale } = useTranslation();
|
const { locale } = useTranslation();
|
||||||
|
if (!user) return null;
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full rounded-md bg-red-500 p-2 text-center font-semibold text-base text-white">
|
<div className="w-full rounded-md bg-red-500 p-2 text-center font-semibold text-base text-white">
|
||||||
|
|
@ -32,12 +28,12 @@ export const AnnuncioInteractions = ({
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col justify-around gap-3 md:gap-5">
|
<div className="flex w-full flex-col justify-around gap-3 md:gap-5">
|
||||||
{session.user ? (
|
{user ? (
|
||||||
tipo ? (
|
tipo ? (
|
||||||
<ServizioInteraction
|
<ServizioInteraction
|
||||||
annuncioId={id}
|
annuncioId={id}
|
||||||
tipologia={tipo}
|
tipologia={tipo}
|
||||||
userId={session.user.id}
|
userId={user.id}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span>{locale === "it" ? "Errore" : "Error"}</span>
|
<span>{locale === "it" ? "Errore" : "Error"}</span>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
"use client";
|
|
||||||
import { LogIn, LogOut } from "lucide-react";
|
import { LogIn, LogOut } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
|
|
@ -35,7 +34,7 @@ export const UserHeaderSection = ({
|
||||||
inSidebar,
|
inSidebar,
|
||||||
}: LoginButtonProps) => {
|
}: LoginButtonProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const session = useSession();
|
const { status, user } = useSession();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const path = usePathname();
|
const path = usePathname();
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
@ -51,7 +50,7 @@ export const UserHeaderSection = ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (session.status === "pending") {
|
if (status === "LOADING") {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center px-3 py-2">
|
<div className="flex items-center px-3 py-2">
|
||||||
<LoadingSpinner className="size-5" />
|
<LoadingSpinner className="size-5" />
|
||||||
|
|
@ -61,7 +60,7 @@ export const UserHeaderSection = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{!session || !session.user ? (
|
{status !== "AUTHENTICATED" ? (
|
||||||
<Button
|
<Button
|
||||||
aria-label="Login Button"
|
aria-label="Login Button"
|
||||||
className="flex h-11.5 w-full items-center justify-center gap-x-1 rounded-lg p-2"
|
className="flex h-11.5 w-full items-center justify-center gap-x-1 rounded-lg p-2"
|
||||||
|
|
@ -89,25 +88,23 @@ export const UserHeaderSection = ({
|
||||||
>
|
>
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
className="size-10"
|
className="size-10"
|
||||||
userId={session.user.id}
|
userId={user.id}
|
||||||
username={session.user.username}
|
username={user.username}
|
||||||
/>
|
/>
|
||||||
{inSidebar ? (
|
{inSidebar ? (
|
||||||
<span>{session.user.username}</span>
|
<span>{user.username}</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="navqry:block hidden">
|
<span className="navqry:block hidden">{user.username}</span>
|
||||||
{session.user.username}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-56" forceMount>
|
<DropdownMenuContent align="end" className="w-56" forceMount>
|
||||||
{session.user.isAdmin ? (
|
{user.isAdmin ? (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuLabel className="font-normal">
|
<DropdownMenuLabel className="font-normal">
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="font-medium text-sm leading-none">
|
<p className="font-medium text-sm leading-none">
|
||||||
{session.user.username}
|
{user.username}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-muted-foreground text-xs leading-none">
|
<p className="text-muted-foreground text-xs leading-none">
|
||||||
Amministratore
|
Amministratore
|
||||||
|
|
@ -136,10 +133,10 @@ export const UserHeaderSection = ({
|
||||||
<DropdownMenuLabel className="font-normal">
|
<DropdownMenuLabel className="font-normal">
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="font-medium text-sm leading-none">
|
<p className="font-medium text-sm leading-none">
|
||||||
{session.user.username}
|
{user.username}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-muted-foreground text-xs leading-none">
|
<p className="text-muted-foreground text-xs leading-none">
|
||||||
{session.user.email}
|
{user.email}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
|
|
|
||||||
|
|
@ -121,14 +121,14 @@ const AnnuncioDettaglio: NextPage<AnnuncioProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const AnnuncioView = ({ data, flag }: AnnuncioProps) => {
|
const AnnuncioView = ({ data, flag }: AnnuncioProps) => {
|
||||||
const session = useSession();
|
const { status, user } = useSession();
|
||||||
useStaleImageReload();
|
useStaleImageReload();
|
||||||
|
|
||||||
const isDesktop = useMediaQuery("(min-width: 768px)");
|
const isDesktop = useMediaQuery("(min-width: 768px)");
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!data ||
|
!data ||
|
||||||
session.status === "error" ||
|
status !== "AUTHENTICATED" ||
|
||||||
(data && data.stato === "Sospeso")
|
(data && data.stato === "Sospeso")
|
||||||
) {
|
) {
|
||||||
return <FailedAnnuncioLoading />;
|
return <FailedAnnuncioLoading />;
|
||||||
|
|
@ -150,7 +150,7 @@ const AnnuncioView = ({ data, flag }: AnnuncioProps) => {
|
||||||
<div className="mb-6 flex flex-col md:mb-0 md:w-1/3">
|
<div className="mb-6 flex flex-col md:mb-0 md:w-1/3">
|
||||||
<div className="flex w-full flex-col flex-nowrap items-center justify-center gap-6">
|
<div className="flex w-full flex-col flex-nowrap items-center justify-center gap-6">
|
||||||
<CardInfos />
|
<CardInfos />
|
||||||
{session.user?.isAdmin ? (
|
{user?.isAdmin ? (
|
||||||
<>
|
<>
|
||||||
<Link
|
<Link
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
|
@ -175,10 +175,7 @@ const AnnuncioView = ({ data, flag }: AnnuncioProps) => {
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<AnnuncioInteractions
|
<AnnuncioInteractions disabled={flag === "true"} />
|
||||||
disabled={flag === "true"}
|
|
||||||
session={session}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<ComeFunziona />
|
<ComeFunziona />
|
||||||
{data.tipo &&
|
{data.tipo &&
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@ export default createNextApiHandler({
|
||||||
createContext: createTRPCContext,
|
createContext: createTRPCContext,
|
||||||
|
|
||||||
onError: ({ path, error }) => {
|
onError: ({ path, error }) => {
|
||||||
|
if (error.message.startsWith("SESSION_EXPIRED")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (env.NODE_ENV === "development") {
|
if (env.NODE_ENV === "development") {
|
||||||
console.error(
|
console.error(
|
||||||
`❌ tRPC failed on ${path ?? "<no-path>"}, msg: ${error.message}`,
|
`❌ tRPC failed on ${path ?? "<no-path>"}, msg: ${error.message}`,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import { api } from "~/utils/api";
|
||||||
* Pagina di gestione chat per admin: /area-riservata/admin/chats
|
* Pagina di gestione chat per admin: /area-riservata/admin/chats
|
||||||
*/
|
*/
|
||||||
const AdminChats: NextPageWithLayout = () => {
|
const AdminChats: NextPageWithLayout = () => {
|
||||||
const session = useSession();
|
const { status } = useSession();
|
||||||
|
|
||||||
const { data: activeChats, isLoading: isLoadingActiveChats } =
|
const { data: activeChats, isLoading: isLoadingActiveChats } =
|
||||||
api.chat.getActiveChats.useQuery();
|
api.chat.getActiveChats.useQuery();
|
||||||
|
|
@ -23,14 +23,10 @@ const AdminChats: NextPageWithLayout = () => {
|
||||||
const { data: inactiveUsers, isLoading: isLoadingInactiveUsers } =
|
const { data: inactiveUsers, isLoading: isLoadingInactiveUsers } =
|
||||||
api.users.getActiveUsersWithoutChat.useQuery();
|
api.users.getActiveUsersWithoutChat.useQuery();
|
||||||
|
|
||||||
if (
|
if (status === "LOADING" || isLoadingActiveChats || isLoadingInactiveUsers)
|
||||||
session.status === "pending" ||
|
|
||||||
isLoadingActiveChats ||
|
|
||||||
isLoadingInactiveUsers
|
|
||||||
)
|
|
||||||
return <LoadingPage />;
|
return <LoadingPage />;
|
||||||
|
|
||||||
if (session.status !== "success" || !session.user) {
|
if (status !== "AUTHENTICATED") {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
return <LoadingPage />;
|
return <LoadingPage />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,11 @@ import { useSession } from "~/providers/SessionProvider";
|
||||||
*/
|
*/
|
||||||
const AggiornaPasswordPage: NextPage = () => {
|
const AggiornaPasswordPage: NextPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const session = useSession();
|
const { status } = useSession();
|
||||||
if (session.status === "pending") {
|
if (status === "LOADING") {
|
||||||
return <LoadingPage />;
|
return <LoadingPage />;
|
||||||
}
|
}
|
||||||
if (session.status === "error" || !session.user) {
|
if (status !== "AUTHENTICATED") {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
return <Status500 />;
|
return <Status500 />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,17 +6,25 @@ import {
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
} from "react";
|
} from "react";
|
||||||
import type { Session, ValidSession } from "~/server/api/trpc";
|
import { LoadingPage } from "~/components/loading";
|
||||||
|
import type { Session, SessionStatus } from "~/server/api/trpc";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
type SessionStatus = "pending" | "success" | "error";
|
export type ValidSession = {
|
||||||
export type SessionContextType = {
|
user: Session;
|
||||||
user: Session | null;
|
status: Extract<SessionStatus, "AUTHENTICATED">;
|
||||||
status: SessionStatus;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type InvalidStatus = Exclude<SessionStatus, "AUTHENTICATED"> | "LOADING";
|
||||||
|
export type InvalidSession = {
|
||||||
|
status: InvalidStatus;
|
||||||
|
user: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SessionContextType = ValidSession | InvalidSession;
|
||||||
|
|
||||||
const SessionContext = createContext<SessionContextType>({
|
const SessionContext = createContext<SessionContextType>({
|
||||||
status: "pending",
|
status: "LOADING",
|
||||||
user: null,
|
user: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -24,24 +32,20 @@ export const SessionProvider: FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const {
|
const { data, isLoading } = api.auth.getSession.useQuery(undefined, {
|
||||||
data: user,
|
|
||||||
status,
|
|
||||||
isLoading,
|
|
||||||
isError,
|
|
||||||
error,
|
|
||||||
} = api.auth.getSession.useQuery(undefined, {
|
|
||||||
refetchInterval: 30000,
|
refetchInterval: 30000,
|
||||||
refetchOnMount: true,
|
//refetchOnMount: true,
|
||||||
refetchOnReconnect: true,
|
//refetchOnReconnect: true,
|
||||||
refetchOnWindowFocus: true,
|
refetchOnWindowFocus: true,
|
||||||
staleTime: 0,
|
staleTime: Infinity,
|
||||||
|
retry: false,
|
||||||
|
meta: {
|
||||||
|
isSessionQuery: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isError && error) {
|
const session = data?.session ?? null;
|
||||||
console.error("Session fetch error: ", error);
|
const sessionStatus = data?.sessionStatus ?? "LOADING";
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invalidate session on route change
|
// Invalidate session on route change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -58,17 +62,28 @@ export const SessionProvider: FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
}, [router, utils]);
|
}, [router, utils]);
|
||||||
|
|
||||||
// Invalidate session if user is null
|
// Invalidate session if user is null
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
if (!user) {
|
// if (!session && status === "success") {
|
||||||
utils.auth.getSession.invalidate().catch((error: Error) => {
|
// utils.auth.getSession.invalidate().catch((error: Error) => {
|
||||||
console.error("Error during session invalidation:", error);
|
// console.error("Error during session invalidation:", error);
|
||||||
});
|
// });
|
||||||
|
// }
|
||||||
|
// }, [session, status]);
|
||||||
|
|
||||||
|
const sessionValue: SessionContextType =
|
||||||
|
session && sessionStatus === "AUTHENTICATED"
|
||||||
|
? {
|
||||||
|
user: session, // TypeScript knows user is non-null here
|
||||||
|
status: "AUTHENTICATED", // matches ValidSession
|
||||||
}
|
}
|
||||||
}, [user]);
|
: {
|
||||||
|
user: null, // matches InvalidSession
|
||||||
|
status: sessionStatus as InvalidStatus, // TypeScript knows this is not "AUTHENTICATED"
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SessionContext.Provider value={{ status, user: user || null }}>
|
<SessionContext.Provider value={sessionValue}>
|
||||||
{children}
|
{isLoading ? <LoadingPage /> : children}
|
||||||
</SessionContext.Provider>
|
</SessionContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,8 @@ export const apisProxy: ProxyFn = async (req: NextRequest) => {
|
||||||
return new NextResponse("Unauthorized", { status: 401 });
|
return new NextResponse("Unauthorized", { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = await verifyToken(accessToken);
|
const { expired, session } = await verifyToken(accessToken);
|
||||||
if (!payload) {
|
if (!session || expired) {
|
||||||
console.log("Invalid token, referrer:", req.referrer, " url:", req.url);
|
console.log("Invalid token, referrer:", req.referrer, " url:", req.url);
|
||||||
return new NextResponse("Unauthorized", { status: 401 });
|
return new NextResponse("Unauthorized", { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,12 @@ export const authProxy: ProxyFn = async (req: NextRequest) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If access token is present, check if it's valid
|
// If access token is present, check if it's valid
|
||||||
const accessTokenPayload = await verifyToken(accessToken);
|
const { expired, session } = await verifyToken(accessToken);
|
||||||
if (!accessTokenPayload) {
|
if (expired && refreshToken) {
|
||||||
|
// If access token is expired but refresh token is present, attempt to refresh
|
||||||
|
return await refreshAuthToken(req, refreshToken, false);
|
||||||
|
}
|
||||||
|
if (!session) {
|
||||||
// If access token is invalid, delete it and redirect to login
|
// If access token is invalid, delete it and redirect to login
|
||||||
const destination = new URL("/login", req.nextUrl);
|
const destination = new URL("/login", req.nextUrl);
|
||||||
destination.searchParams.set("redirect", path);
|
destination.searchParams.set("redirect", path);
|
||||||
|
|
@ -61,7 +65,7 @@ export const authProxy: ProxyFn = async (req: NextRequest) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path.startsWith("/area-riservata/admin")) {
|
if (path.startsWith("/area-riservata/admin")) {
|
||||||
if (!accessTokenPayload.isAdmin) {
|
if (!session.isAdmin) {
|
||||||
return NextResponse.redirect(
|
return NextResponse.redirect(
|
||||||
new URL("/area-riservata/dashboard", req.nextUrl),
|
new URL("/area-riservata/dashboard", req.nextUrl),
|
||||||
);
|
);
|
||||||
|
|
@ -87,9 +91,12 @@ export const authProxy: ProxyFn = async (req: NextRequest) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no redirect present, redirect to dashboard
|
// If no redirect present, redirect to dashboard
|
||||||
const payload = await verifyToken(accessToken);
|
const { expired, session } = await verifyToken(accessToken);
|
||||||
if (payload) {
|
if (expired && refreshToken) {
|
||||||
if (payload.isAdmin) {
|
return await refreshAuthToken(req, refreshToken, true);
|
||||||
|
}
|
||||||
|
if (session && !expired) {
|
||||||
|
if (session.isAdmin) {
|
||||||
return NextResponse.redirect(
|
return NextResponse.redirect(
|
||||||
new URL("/area-riservata/dashboard", req.nextUrl),
|
new URL("/area-riservata/dashboard", req.nextUrl),
|
||||||
);
|
);
|
||||||
|
|
@ -118,8 +125,8 @@ export const authProxy: ProxyFn = async (req: NextRequest) => {
|
||||||
return await refreshAuthToken(req, refreshToken, false);
|
return await refreshAuthToken(req, refreshToken, false);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const payload = await verifyToken(accessToken);
|
const { expired, session } = await verifyToken(accessToken);
|
||||||
if (!payload) {
|
if (!session || expired) {
|
||||||
if (refreshToken) {
|
if (refreshToken) {
|
||||||
return await refreshAuthToken(req, refreshToken, false);
|
return await refreshAuthToken(req, refreshToken, false);
|
||||||
}
|
}
|
||||||
|
|
@ -138,9 +145,10 @@ const refreshAuthToken = async (
|
||||||
refreshToken: string,
|
refreshToken: string,
|
||||||
onSuccessDashboard: boolean,
|
onSuccessDashboard: boolean,
|
||||||
) => {
|
) => {
|
||||||
|
//console.log("Attempting to refresh access token")
|
||||||
// If refresh token is present, verify it
|
// If refresh token is present, verify it
|
||||||
const refreshTokenPayload = await verifyToken(refreshToken);
|
const { expired, session } = await verifyToken(refreshToken);
|
||||||
if (!refreshTokenPayload) {
|
if (!session || expired) {
|
||||||
// If refresh token is invalid, delete it and redirect to login
|
// If refresh token is invalid, delete it and redirect to login
|
||||||
const destination = new URL("/login", req.nextUrl);
|
const destination = new URL("/login", req.nextUrl);
|
||||||
destination.searchParams.set("redirect", req.nextUrl.pathname);
|
destination.searchParams.set("redirect", req.nextUrl.pathname);
|
||||||
|
|
@ -156,7 +164,7 @@ const refreshAuthToken = async (
|
||||||
const response = NextResponse.redirect(new URL(destination, req.nextUrl));
|
const response = NextResponse.redirect(new URL(destination, req.nextUrl));
|
||||||
|
|
||||||
const newAccessToken = await signToken(
|
const newAccessToken = await signToken(
|
||||||
refreshTokenPayload,
|
session,
|
||||||
TOKEN_CONFIG.ACCESS_TOKEN_EXPIRY,
|
TOKEN_CONFIG.ACCESS_TOKEN_EXPIRY,
|
||||||
"accessToken",
|
"accessToken",
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,11 @@ export const pswChangeProxy: ProxyFn = async (req: NextRequest) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const accessTokenPayload = await verifyToken(accessToken);
|
const { expired, session } = await verifyToken(accessToken);
|
||||||
if (!accessTokenPayload) {
|
if (!session || expired) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (accessTokenPayload.mustChangePassword) {
|
if (session.mustChangePassword) {
|
||||||
return NextResponse.redirect(
|
return NextResponse.redirect(
|
||||||
new URL("/auth/aggiorna-password", req.nextUrl),
|
new URL("/auth/aggiorna-password", req.nextUrl),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ export const authRouter = createTRPCRouter({
|
||||||
|
|
||||||
getSession: publicProcedure.query(async ({ ctx }) => {
|
getSession: publicProcedure.query(async ({ ctx }) => {
|
||||||
if (ctx.session?.isBlocked) return null;
|
if (ctx.session?.isBlocked) return null;
|
||||||
return ctx.session;
|
return { session: ctx.session, sessionStatus: ctx.sessionStatus };
|
||||||
}),
|
}),
|
||||||
loginUser: publicProcedure
|
loginUser: publicProcedure
|
||||||
.input(
|
.input(
|
||||||
|
|
@ -90,7 +90,7 @@ export const authRouter = createTRPCRouter({
|
||||||
}),
|
}),
|
||||||
|
|
||||||
logoutUser: protectedProcedure.mutation(async ({ ctx }) => {
|
logoutUser: protectedProcedure.mutation(async ({ ctx }) => {
|
||||||
await logOut({ ctx: ctx });
|
await logOut({ req: ctx.req, res: ctx.res });
|
||||||
return "logout success";
|
return "logout success";
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@
|
||||||
|
|
||||||
import { initTRPC, TRPCError } from "@trpc/server";
|
import { initTRPC, TRPCError } from "@trpc/server";
|
||||||
import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
|
import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
import z, { ZodError } from "zod/v4";
|
import z, { ZodError } from "zod/v4";
|
||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
|
|
@ -36,23 +35,40 @@ export const sessionSchema = z.object({
|
||||||
|
|
||||||
export type Session = z.infer<typeof sessionSchema>;
|
export type Session = z.infer<typeof sessionSchema>;
|
||||||
|
|
||||||
export type ValidSession = {
|
export type SessionStatus =
|
||||||
user: Session;
|
| "AUTHENTICATED"
|
||||||
status: "success";
|
| "EXPIRED"
|
||||||
};
|
| "INVALID"
|
||||||
|
| "NO_SESSION";
|
||||||
|
|
||||||
interface CreateInnerContextOptions extends Partial<CreateNextContextOptions> {
|
interface CreateInnerContextOptions extends Partial<CreateNextContextOptions> {
|
||||||
session: Session | null;
|
accessToken?: string;
|
||||||
res?: NextApiResponse;
|
}
|
||||||
req?: NextApiRequest;
|
export const createInnerTRPCContext = async ({
|
||||||
|
accessToken,
|
||||||
|
...opts
|
||||||
|
}: CreateInnerContextOptions) => {
|
||||||
|
let sessionData: Session | null = null;
|
||||||
|
let sessionStatus: SessionStatus = "NO_SESSION";
|
||||||
|
|
||||||
|
if (accessToken) {
|
||||||
|
const { expired, session } = await verifyToken(accessToken);
|
||||||
|
if (expired) {
|
||||||
|
sessionStatus = "EXPIRED";
|
||||||
|
} else if (session) {
|
||||||
|
sessionData = session;
|
||||||
|
sessionStatus = "AUTHENTICATED";
|
||||||
|
} else {
|
||||||
|
sessionStatus = "INVALID";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createInnerTRPCContext(opts: CreateInnerContextOptions) {
|
|
||||||
return {
|
return {
|
||||||
...opts,
|
...opts,
|
||||||
session: opts.session,
|
session: sessionData,
|
||||||
|
sessionStatus,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const createTRPCContext = async ({
|
export const createTRPCContext = async ({
|
||||||
req,
|
req,
|
||||||
|
|
@ -63,19 +79,11 @@ export const createTRPCContext = async ({
|
||||||
"You are missing `req` or `res` in your call to createTRPCContext.",
|
"You are missing `req` or `res` in your call to createTRPCContext.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const accessToken = req.cookies[TOKEN_CONFIG.ACCESS_TOKEN_COOKIE_NAME];
|
const accessToken = req.cookies[TOKEN_CONFIG.ACCESS_TOKEN_COOKIE_NAME];
|
||||||
// Get the session from the server using the getServerSession wrapper function
|
|
||||||
|
|
||||||
const session = accessToken ? await verifyToken(accessToken) : null;
|
return await createInnerTRPCContext({ accessToken, req, res });
|
||||||
const contextInner = createInnerTRPCContext({ session });
|
|
||||||
return {
|
|
||||||
...contextInner,
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
};
|
};
|
||||||
};
|
export type Context = Awaited<ReturnType<typeof createTRPCContext>>;
|
||||||
export type Context = Awaited<ReturnType<typeof createInnerTRPCContext>>;
|
|
||||||
export const addApiAuthToContext = (ctx: Context) => {
|
export const addApiAuthToContext = (ctx: Context) => {
|
||||||
if (!ctx.req || !ctx.res) {
|
if (!ctx.req || !ctx.res) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
@ -146,6 +154,12 @@ export const publicProcedure = t.procedure;
|
||||||
|
|
||||||
/** Reusable middleware that enforces users are logged in before running the procedure. */
|
/** Reusable middleware that enforces users are logged in before running the procedure. */
|
||||||
const enforceUserIsAuthed = t.middleware(({ ctx, next, path }) => {
|
const enforceUserIsAuthed = t.middleware(({ ctx, next, path }) => {
|
||||||
|
if (ctx.sessionStatus === "EXPIRED") {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: `SESSION_EXPIRED, error in path: ${path} [${ctx.req?.url}]`,
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!ctx.session) {
|
if (!ctx.session) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
|
|
@ -202,6 +216,12 @@ export const overridableProcedure = t.procedure.use(
|
||||||
|
|
||||||
/** Reusable middleware that enforces users are logged in before running the procedure. */
|
/** Reusable middleware that enforces users are logged in before running the procedure. */
|
||||||
const enforceUserIsAdmin = t.middleware(({ ctx, next, path }) => {
|
const enforceUserIsAdmin = t.middleware(({ ctx, next, path }) => {
|
||||||
|
if (ctx.sessionStatus === "EXPIRED") {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: `SESSION_EXPIRED, error in path: ${path} [${ctx.req?.url}]`,
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!ctx.session || !ctx.session.isAdmin) {
|
if (!ctx.session || !ctx.session.isAdmin) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@ export const TOKEN_CONFIG = {
|
||||||
ACCESS_TOKEN_COOKIE_NAME: "access_token",
|
ACCESS_TOKEN_COOKIE_NAME: "access_token",
|
||||||
REFRESH_TOKEN_COOKIE_NAME: "refresh_token",
|
REFRESH_TOKEN_COOKIE_NAME: "refresh_token",
|
||||||
OVERRIDE_TOKEN_COOKIE_NAME: "override_token",
|
OVERRIDE_TOKEN_COOKIE_NAME: "override_token",
|
||||||
ACCESS_TOKEN_EXPIRY: "1h",
|
ACCESS_TOKEN_EXPIRY: "2h",
|
||||||
REFRESH_TOKEN_EXPIRY: "7d",
|
REFRESH_TOKEN_EXPIRY: "7d",
|
||||||
ACCESS_TOKEN_COOKIE_EXPIRY: 1,
|
ACCESS_TOKEN_COOKIE_EXPIRY: 2,
|
||||||
REFRESH_TOKEN_COOKIE_EXPIRY: 7,
|
REFRESH_TOKEN_COOKIE_EXPIRY: 7,
|
||||||
COOKIE_OPTIONS: {
|
COOKIE_OPTIONS: {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { jwtVerify, SignJWT } from "jose";
|
import { jwtVerify, SignJWT } from "jose";
|
||||||
|
import { JWTExpired } from "jose/errors";
|
||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
import { type Session, sessionSchema } from "~/server/api/trpc";
|
import { type Session, sessionSchema } from "~/server/api/trpc";
|
||||||
|
|
||||||
|
|
@ -24,17 +25,40 @@ export async function signToken(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function verifyToken(token: string): Promise<Session | null> {
|
type VerifySuccess = {
|
||||||
|
expired: false;
|
||||||
|
session: Session;
|
||||||
|
};
|
||||||
|
|
||||||
|
type VerifyExpired = {
|
||||||
|
expired: true;
|
||||||
|
session: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type VerifyFailure = {
|
||||||
|
expired: boolean;
|
||||||
|
session: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type VerifyResult = VerifySuccess | VerifyExpired | VerifyFailure;
|
||||||
|
|
||||||
|
export async function verifyToken(token: string): Promise<VerifyResult> {
|
||||||
try {
|
try {
|
||||||
const { payload } = await jwtVerify(token, secret);
|
const { payload } = await jwtVerify(token, secret, {
|
||||||
|
requiredClaims: ["exp"],
|
||||||
|
});
|
||||||
|
|
||||||
const parse = sessionSchema.safeParse(payload.payload);
|
const parse = sessionSchema.safeParse(payload.payload);
|
||||||
if (!parse.success) {
|
if (!parse.success) {
|
||||||
//console.error("JWT payload invalid:", parse.error);
|
//console.error("JWT payload invalid:", parse.error);
|
||||||
return null;
|
return { expired: false, session: null };
|
||||||
}
|
}
|
||||||
return parse.data;
|
return { expired: false, session: parse.data };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (e instanceof JWTExpired) {
|
||||||
|
return { expired: true, session: null };
|
||||||
|
}
|
||||||
console.error("JWT verification Internal Error:", (e as Error).message);
|
console.error("JWT verification Internal Error:", (e as Error).message);
|
||||||
return null;
|
return { expired: false, session: null };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { deleteCookie, setCookie } from "cookies-next";
|
import { deleteCookie, setCookie } from "cookies-next";
|
||||||
import { add } from "date-fns";
|
import { add } from "date-fns";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import type { UsersId } from "~/schemas/public/Users";
|
import type { UsersId } from "~/schemas/public/Users";
|
||||||
import type { Context } from "~/server/api/trpc";
|
import type { Context } from "~/server/api/trpc";
|
||||||
import { signToken } from "~/server/auth/jwt";
|
import { signToken } from "~/server/auth/jwt";
|
||||||
|
|
@ -226,7 +227,13 @@ export const swapAuth = async ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const logOut = async ({ ctx: { req, res } }: { ctx: Context }) => {
|
export const logOut = async ({
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
}: {
|
||||||
|
req?: NextApiRequest;
|
||||||
|
res?: NextApiResponse;
|
||||||
|
}) => {
|
||||||
checkCtx({ req, res });
|
checkCtx({ req, res });
|
||||||
try {
|
try {
|
||||||
await deleteCookie(TOKEN_CONFIG.ACCESS_TOKEN_COOKIE_NAME, { req, res });
|
await deleteCookie(TOKEN_CONFIG.ACCESS_TOKEN_COOKIE_NAME, { req, res });
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,14 @@ import { verifyToken } from "~/server/auth/jwt";
|
||||||
|
|
||||||
export const generateSSGHelper = () =>
|
export const generateSSGHelper = () =>
|
||||||
createServerSideHelpers({
|
createServerSideHelpers({
|
||||||
ctx: { session: null },
|
ctx: { session: null, sessionStatus: "NO_SESSION" },
|
||||||
router: appRouter,
|
router: appRouter,
|
||||||
transformer: superjson, // optional - adds superjson serialization
|
transformer: superjson, // optional - adds superjson serialization
|
||||||
});
|
});
|
||||||
|
|
||||||
const generateHelperWSession = (session: Session) =>
|
const generateHelperWSession = (session: Session) =>
|
||||||
createServerSideHelpers({
|
createServerSideHelpers({
|
||||||
ctx: { session: session },
|
ctx: { session: session, sessionStatus: "AUTHENTICATED" },
|
||||||
router: appRouter,
|
router: appRouter,
|
||||||
transformer: superjson, // optional - adds superjson serialization
|
transformer: superjson, // optional - adds superjson serialization
|
||||||
});
|
});
|
||||||
|
|
@ -24,9 +24,9 @@ export const TrpcAuthedFetchingIstance = async ({
|
||||||
access_token: string | undefined;
|
access_token: string | undefined;
|
||||||
}) => {
|
}) => {
|
||||||
if (access_token) {
|
if (access_token) {
|
||||||
const session = await verifyToken(access_token);
|
const { expired, session } = await verifyToken(access_token);
|
||||||
|
|
||||||
if (session) {
|
if (!expired && session) {
|
||||||
const trpc = generateHelperWSession(session);
|
const trpc = generateHelperWSession(session);
|
||||||
|
|
||||||
return { session, trpc };
|
return { session, trpc };
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,11 @@ const errorLink: TRPCLink<AppRouter> = () => {
|
||||||
const unsubscribe = next(op).subscribe({
|
const unsubscribe = next(op).subscribe({
|
||||||
complete: observer.complete,
|
complete: observer.complete,
|
||||||
error(err) {
|
error(err) {
|
||||||
|
if (err.message.startsWith("SESSION_EXPIRED")) {
|
||||||
|
// Silently reload — the auth proxy will refresh the token
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
(err instanceof TRPCClientError &&
|
(err instanceof TRPCClientError &&
|
||||||
err.data?.code === "UNAUTHORIZED") ||
|
err.data?.code === "UNAUTHORIZED") ||
|
||||||
|
|
@ -34,6 +39,10 @@ const errorLink: TRPCLink<AppRouter> = () => {
|
||||||
err.data?.code === "CONFLICT" ||
|
err.data?.code === "CONFLICT" ||
|
||||||
err.data?.code === "NOT_FOUND"
|
err.data?.code === "NOT_FOUND"
|
||||||
) {
|
) {
|
||||||
|
console.warn(
|
||||||
|
`Received ${err.data?.code} error from tRPC API, reloading the page to trigger auth flow or show error page.`,
|
||||||
|
err,
|
||||||
|
);
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
observer.error(err);
|
observer.error(err);
|
||||||
|
|
@ -54,7 +63,9 @@ export const api = createTRPCNext<AppRouter>({
|
||||||
enabled: (opts) =>
|
enabled: (opts) =>
|
||||||
(process.env.NODE_ENV === "development" &&
|
(process.env.NODE_ENV === "development" &&
|
||||||
typeof window !== "undefined") ||
|
typeof window !== "undefined") ||
|
||||||
(opts.direction === "down" && opts.result instanceof Error),
|
(opts.direction === "down" &&
|
||||||
|
opts.result instanceof Error &&
|
||||||
|
!opts.result.message.startsWith("SESSION_EXPIRED")),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
splitLink({
|
splitLink({
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue