Skip to content

Commit

Permalink
Merge pull request #154 from Vizzuality/chore/expire-session
Browse files Browse the repository at this point in the history
expiring session
  • Loading branch information
mluena authored Oct 17, 2024
2 parents 690e43b + 71de5ce commit 8ec431f
Show file tree
Hide file tree
Showing 8 changed files with 89 additions and 10 deletions.
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"express": "4.18.2",
"glslify": "^7.1.1",
"jotai": "^2.5.0",
"jwt-decode": "4.0.0",
"lodash-es": "^4.17.21",
"mapbox-gl": "2.15.0",
"next": "^14.0.3",
Expand Down
36 changes: 34 additions & 2 deletions client/src/app/api/auth/[...nextauth]/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import CredentialsProvider from "next-auth/providers/credentials";

import { postAuthLocal } from "@/types/generated/users-permissions-auth";

import { jwtDecode } from "jwt-decode";

const SESSION_MAX_AGE = 30 * 24 * 60 * 60; // 30 days

export const authOptions: AuthOptions = {
providers: [
CredentialsProvider({
Expand Down Expand Up @@ -33,7 +37,7 @@ export const authOptions: AuthOptions = {
},
}),
],
session: { strategy: "jwt" },
session: { strategy: "jwt", maxAge: SESSION_MAX_AGE },
callbacks: {
async session({ session, token }) {
const sanitizedToken = Object.keys(token).reduce((p, c) => {
Expand All @@ -44,7 +48,35 @@ export const authOptions: AuthOptions = {
return p;
}
}, {});
return { ...session, user: sanitizedToken, apiToken: token.apiToken };

const decoded = jwtDecode(token.apiToken);

const sessionToken = session.sessionToken ?? token;

if (sessionToken && decoded.iat) {
const now = new Date().getTime();
const exp = decoded.iat * 1000 + SESSION_MAX_AGE * 1000;

if (now < exp) {
return {
...session,
user: sanitizedToken,
apiToken: token.apiToken,
};
} else {
return {
...session,
user: sanitizedToken,
error: "ExpiredTokenError",
};
}
}

return {
...session,
user: sanitizedToken,
apiToken: token.apiToken,
};
},
async jwt({ token, user }) {
if (typeof user !== "undefined") {
Expand Down
2 changes: 2 additions & 0 deletions client/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import PoweredBy from "@/containers/powered-by";
import { metropolis, openSans } from "@/styles/fonts";

import LayoutProviders from "./layout-providers";
import SessionChecker from "@/components/session-checker";

export const metadata: Metadata = {
title: { template: "%s | CCSA", default: "CCSA" },
Expand Down Expand Up @@ -55,6 +56,7 @@ export default async function RootLayout({ children }: PropsWithChildren) {
<Hydrate state={dehydratedState}>
<html lang="en" className={`${openSans.variable} ${metropolis.variable}`}>
<body>
<SessionChecker />
{children}

<div className="fixed bottom-0 left-1/2 z-10 -translate-x-1/2">
Expand Down
26 changes: 26 additions & 0 deletions client/src/components/session-checker/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"use client";

import { PropsWithChildren, useEffect } from "react";
import { signOut, useSession } from "next-auth/react";

import { privatePaths } from "@/middleware";
import { usePathname } from "next/navigation";

export default function SessionChecker({ children }: PropsWithChildren) {
const { data: sessionData } = useSession();
const pathname = usePathname();

useEffect(() => {
if (!!sessionData?.error) {
// Check if the current path is part of private paths
const isPrivatePath = privatePaths.includes(pathname);

// Sign out user and redirect them to signin page if needed
signOut({
callbackUrl: isPrivatePath ? "/signin" : undefined, // Only add callbackUrl for private paths
});
}
}, [sessionData, pathname]);

return <>{children}</>;
}
11 changes: 5 additions & 6 deletions client/src/containers/navigation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,21 @@ import { LuUser2 } from "react-icons/lu";

import { cn } from "@/lib/classnames";

import { useGetUsersMe } from "@/types/generated/users-permissions-users-roles";

import { useSyncSearchParams } from "@/app/store";

import CollaboratorsSvg from "@/svgs/collaborators.svg";
import ExploreSVG from "@/svgs/explore.svg";
import OtherToolsSvg from "@/svgs/other-tools.svg";
import ProjectsSVG from "@/svgs/projects.svg";
import {useSession} from "next-auth/react";

const Navigation = (): JSX.Element => {
const pathname = usePathname();

const sp = useSyncSearchParams();
const { data: user } = useGetUsersMe();
const { data: session } = useSession();

const userNameWithoutSpaces = !user?.username?.includes(" ");
const userNameWithoutSpaces = !session?.user?.username?.includes(" ");

return (
<nav className="relative z-20 flex h-full w-20 shrink-0 flex-col justify-between border-r-2 border-gray-300/20 bg-white">
Expand Down Expand Up @@ -151,7 +150,7 @@ const Navigation = (): JSX.Element => {
})}
/>
<Link
href={!user ? "/signin" : "/dashboard"}
href={!session ? "/signin" : "/dashboard"}
className={cn({
"flex flex-col items-center justify-center space-y-2 py-5 transition-colors": true,
"bg-[#FF7816]/10": pathname === "/collaborators",
Expand All @@ -173,7 +172,7 @@ const Navigation = (): JSX.Element => {
"overflow-hidden truncate px-2": userNameWithoutSpaces,
})}
>
{user ? user?.username : "Log in"}
{session ? session.user.username : "Log in"}
</span>
</Link>
</div>
Expand Down
4 changes: 2 additions & 2 deletions client/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { NextFetchEvent, NextResponse } from "next/server";

import { NextRequestWithAuth, withAuth } from "next-auth/middleware";

const privatePaths = ["/dashboard", "/datasets"];
export const privatePaths = ["/dashboard", "/datasets"];

const dataMiddleware = async (request: Request) => {
// Store current request url in a custom header, which you can read later
Expand All @@ -20,7 +20,7 @@ const dataMiddleware = async (request: Request) => {

const authMiddleware = withAuth({
callbacks: {
authorized: ({ token }) => !!token,
authorized: ({ token }) => !!token && !token.error,
},
});

Expand Down
11 changes: 11 additions & 0 deletions client/src/types/next-auth.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { JWT } from "next-auth/jwt";
import "next-auth";

declare module "next-auth" {
Expand All @@ -15,5 +16,15 @@ declare module "next-auth" {
interface Session {
apiToken: string;
user: User;
error: string;
sessionToken: JWT;
}
}

declare module "next-auth/jwt" {
interface JWT {
exp: number;
iat: number;
apiToken: string;
}
}
8 changes: 8 additions & 0 deletions client/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6419,6 +6419,7 @@ __metadata:
glslify: ^7.1.1
husky: 8.0.3
jotai: ^2.5.0
jwt-decode: 4.0.0
lodash-es: ^4.17.21
mapbox-gl: 2.15.0
next: ^14.0.3
Expand Down Expand Up @@ -10525,6 +10526,13 @@ __metadata:
languageName: node
linkType: hard

"jwt-decode@npm:4.0.0":
version: 4.0.0
resolution: "jwt-decode@npm:4.0.0"
checksum: 390e2edcb31a92e86c8cbdd1edeea4c0d62acd371f8a8f0a8878e499390c0ecf4c658b365c4e941e4ef37d0170e4ca650aaa49f99a45c0b9695a235b210154b0
languageName: node
linkType: hard

"kdbush@npm:^4.0.1, kdbush@npm:^4.0.2":
version: 4.0.2
resolution: "kdbush@npm:4.0.2"
Expand Down

0 comments on commit 8ec431f

Please sign in to comment.