feat(auth): refresh access token on-demand when receiving 401 error

This commit is contained in:
Pablo Lara
2024-10-05 14:29:41 +02:00
parent 735f830251
commit ff74edcc04
10 changed files with 152 additions and 87 deletions
+11 -1
View File
@@ -3,6 +3,7 @@
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { auth } from "@/auth.config";
import { parseStringify } from "@/lib";
export const getProviders = async ({
@@ -11,10 +12,12 @@ export const getProviders = async ({
sort = "",
filters = {},
}) => {
const session = await auth();
if (isNaN(Number(page)) || page < 1) redirect("/providers");
const keyServer = process.env.API_BASE_URL;
const url = new URL(`${keyServer}/providers?sort=-inserted_at`);
const url = new URL(`${keyServer}/providers`);
if (page) url.searchParams.append("page[number]", page.toString());
if (query) url.searchParams.append("filter[search]", query);
@@ -31,6 +34,7 @@ export const getProviders = async ({
const providers = await fetch(url.toString(), {
headers: {
Accept: "application/vnd.api+json",
Authorization: `Bearer ${session?.accessToken}`,
},
});
const data = await providers.json();
@@ -44,6 +48,7 @@ export const getProviders = async ({
};
export const getProvider = async (formData: FormData) => {
// const session = await auth();
const providerId = formData.get("id");
const keyServer = process.env.API_BASE_URL;
@@ -66,6 +71,7 @@ export const getProvider = async (formData: FormData) => {
};
export const updateProvider = async (formData: FormData) => {
// const session = await auth();
const keyServer = process.env.API_BASE_URL;
const providerId = formData.get("providerId");
@@ -102,6 +108,7 @@ export const updateProvider = async (formData: FormData) => {
};
export const addProvider = async (formData: FormData) => {
const session = await auth();
const keyServer = process.env.API_BASE_URL;
const providerType = formData.get("providerType");
@@ -116,6 +123,7 @@ export const addProvider = async (formData: FormData) => {
headers: {
"Content-Type": "application/vnd.api+json",
Accept: "application/vnd.api+json",
Authorization: `Bearer ${session?.accessToken}`,
},
body: JSON.stringify({
data: {
@@ -140,6 +148,7 @@ export const addProvider = async (formData: FormData) => {
};
export const checkConnectionProvider = async (formData: FormData) => {
// const session = await auth();
const keyServer = process.env.API_BASE_URL;
const providerId = formData.get("id");
@@ -164,6 +173,7 @@ export const checkConnectionProvider = async (formData: FormData) => {
};
export const deleteProvider = async (formData: FormData) => {
// const session = await auth();
const keyServer = process.env.API_BASE_URL;
const providerId = formData.get("id");
+6
View File
@@ -2,6 +2,7 @@ import { Spacer } from "@nextui-org/react";
import { redirect } from "next/navigation";
import React from "react";
// import { getUserByMe } from "@/actions/auth/auth";
import { auth } from "@/auth.config";
import { Header } from "@/components/ui";
@@ -13,12 +14,17 @@ export default async function Profile() {
redirect("/sign-in");
}
// const user = await getUserByMe();
// console.log("user", user);
return (
<>
<Header title="User Profile" icon="ci:users" />
<Spacer y={4} />
<Spacer y={6} />
<pre>{JSON.stringify(session.user, null, 2)}</pre>
<pre>{JSON.stringify(session.userId, null, 2)}</pre>
<pre>{JSON.stringify(session.tenantId, null, 2)}</pre>
<pre>{JSON.stringify(session, null, 2)}</pre>
</>
);
}
+1 -6
View File
@@ -3,7 +3,7 @@ import { redirect } from "next/navigation";
import { Suspense } from "react";
import { getUsers } from "@/actions/users";
import { auth } from "@/auth.config";
// import { auth } from "@/auth.config";
import { Header } from "@/components/ui";
import {
AddUserModal,
@@ -18,11 +18,6 @@ export default async function Users({
}: {
searchParams: SearchParamsProps;
}) {
const session = await auth();
if (session?.user?.role !== "admin") {
redirect("/");
}
const searchParamsKey = JSON.stringify(searchParams || {});
return (
-10
View File
@@ -1,10 +0,0 @@
import { NextResponse } from "next/server";
import data from "../../../dataProviders.json";
export async function GET() {
// Simulate fetching data with a delay
await new Promise((resolve) => setTimeout(resolve, 2000));
return NextResponse.json({ providers: data });
}
+71 -53
View File
@@ -1,58 +1,59 @@
import { jwtDecode, JwtDecodeOptions, JwtPayload } from "jwt-decode";
import { jwtDecode, JwtPayload } from "jwt-decode";
import NextAuth, { type NextAuthConfig, User } from "next-auth";
import Credentials from "next-auth/providers/credentials";
import { z } from "zod";
import { getToken } from "./actions/auth";
import { getToken, getUserByMe } from "./actions/auth";
interface CustomJwtPayload extends JwtPayload {
user_id: string;
tenant_id: string;
}
const refreshAccessToken = async (token: JwtDecodeOptions) => {
const keyServer = process.env.API_BASE_URL;
const url = new URL(`${keyServer}/tokens/refresh`);
// const refreshAccessToken = async (token: JwtPayload) => {
// // console.log("tokenDESDE EL REFRESH", token.refreshToken);
// const keyServer = process.env.API_BASE_URL;
// const url = new URL(`${keyServer}/tokens/refresh`);
const bodyData = {
data: {
type: "TokenRefresh",
attributes: {
refresh: token.refreshToken,
},
},
};
// const bodyData = {
// data: {
// type: "TokenRefresh",
// attributes: {
// refresh: (token as any).refreshToken,
// },
// },
// };
try {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/vnd.api+json",
Accept: "application/vnd.api+json",
},
body: JSON.stringify(bodyData),
});
// console.log("response", response);
const newTokens = await response.json();
// try {
// const response = await fetch(url, {
// method: "POST",
// headers: {
// "Content-Type": "application/vnd.api+json",
// Accept: "application/vnd.api+json",
// },
// body: JSON.stringify(bodyData),
// });
// // console.log("response", response);
// const newTokens = await response.json();
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// if (!response.ok) {
// throw new Error(`HTTP error! status: ${response.status}`);
// }
return {
...token,
accessToken: newTokens.data.attributes.access,
refreshToken: newTokens.data.attributes.refresh,
};
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error refreshing access token:", error);
return {
...token,
error: "RefreshAccessTokenError",
};
}
};
// return {
// ...token,
// accessToken: newTokens.data.attributes.access,
// refreshToken: newTokens.data.attributes.refresh,
// };
// } catch (error) {
// // eslint-disable-next-line no-console
// console.error("Error refreshing access token:", error);
// return {
// ...token,
// error: "RefreshAccessTokenError",
// };
// }
// };
export const authConfig = {
session: {
@@ -84,8 +85,21 @@ export const authConfig = {
const tokenResponse = await getToken(parsedCredentials.data);
if (!tokenResponse) return null;
// const user = await getUserById(isUserValid.userId);
return tokenResponse;
const userMeResponse = await getUserByMe(tokenResponse.accessToken);
const user = {
name: userMeResponse.name,
email: userMeResponse.email,
company: userMeResponse?.company,
dateJoined: userMeResponse.dateJoined,
};
return {
...user,
accessToken: tokenResponse.accessToken,
refreshToken: tokenResponse.refreshToken,
};
},
}),
],
@@ -109,23 +123,23 @@ export const authConfig = {
jwt: async ({ token, account, user }) => {
// eslint-disable-next-line no-console
console.log(`In the jwt token - is ${JSON.stringify(token)}`);
// console.log(`In the jwt token - is ${JSON.stringify(token)}`);
if (token?.accessToken) {
const decodedToken = jwtDecode(
token.accessToken as string,
) as CustomJwtPayload;
// eslint-disable-next-line no-console
console.log("decodedToken", decodedToken);
// console.log("decodedToken", decodedToken);
token.accessTokenExpires = (decodedToken.exp as number) * 1000;
token.user_id = decodedToken.user_id;
token.tenant_id = decodedToken.tenant_id;
}
const userInfo = {
name: "Leandro",
companyName: "Bitnami",
email: "john@doe.com",
date_joined: "2024-02-02",
name: user?.name,
companyName: user?.company,
email: user?.email,
dateJoined: user?.dateJoined,
};
if (account && user) {
@@ -151,11 +165,15 @@ export const authConfig = {
if (
typeof token.accessTokenExpires === "number" &&
Date.now() < token.accessTokenExpires
)
) {
console.log("TOKEN IS VALID");
return token;
} else {
console.log("TOKEN IS EXPIRED");
}
return token;
// If the access token is expired, try to refresh it
return refreshAccessToken(token as unknown as JwtDecodeOptions);
// return refreshAccessToken(token as unknown as JwtDecodeOptions);
},
session: async ({ session, token }) => {
@@ -169,7 +187,7 @@ export const authConfig = {
session.user = token.user as any;
}
console.log("session", session);
// console.log("session", session);
return session;
},
},
+9 -11
View File
@@ -6,7 +6,7 @@ import clsx from "clsx";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useSession } from "next-auth/react";
import React, { useCallback } from "react";
import React, { Suspense, useCallback } from "react";
import { useMediaQuery } from "usehooks-ts";
import { logOut } from "@/actions/auth";
@@ -72,22 +72,20 @@ export const SidebarWrap = () => {
<Spacer y={8} />
<Link href={"/profile"}>
<UserAvatar
userName={session?.user?.name ?? "Guest"}
position={session?.user?.companyName ?? "Company Name"}
isCompact={isCompact}
/>{" "}
<Suspense fallback={<p>Loading...</p>}>
<UserAvatar
userName={session?.user?.name ?? "Guest"}
position={session?.user?.companyName ?? "Company Name"}
isCompact={isCompact}
/>
</Suspense>
</Link>
<ScrollShadow hideScrollBar className="-mr-6 h-full max-h-full py-6 pr-6">
<Sidebar
defaultSelectedKey="overview"
isCompact={isCompact}
items={
session?.user?.role === "admin"
? sectionItemsWithTeams
: sectionItems
}
items={sectionItemsWithTeams}
selectedKeys={[currentPath]}
/>
</ScrollShadow>
+1 -1
View File
@@ -12,7 +12,7 @@ declare module "next-auth" {
user: {
name: string;
email: string;
company?: string;
companyName?: string;
dateJoined: string;
} & DefaultSession["user"];
userId: string;
-5
View File
@@ -1,4 +1,3 @@
import { JwtPayload } from "jwt-decode";
import { SVGProps } from "react";
export type IconSvgProps = SVGProps<SVGSVGElement> & {
@@ -31,10 +30,6 @@ export interface SearchParamsProps {
[key: string]: string | string[] | undefined;
}
export interface CustomJwtPayload extends JwtPayload {
user_id: string;
tenant_id: string;
}
export interface ProviderProps {
id: string;
type: "providers";
+1
View File
@@ -0,0 +1 @@
export * from "./users";
+52
View File
@@ -0,0 +1,52 @@
export interface UserAttributes {
name: string;
email: string;
company_name: string;
date_joined: string;
}
export interface Membership {
type: string;
id: string;
}
export interface MembershipMeta {
count: number;
}
export interface UserRelationships {
memberships: {
meta: MembershipMeta;
data: Membership[];
};
}
export interface UserData {
type: string;
id: string;
attributes: UserAttributes;
relationships: UserRelationships;
}
export interface Meta {
version: string;
}
export interface UserProps {
data: UserData;
meta: Meta;
}
export interface TokenAttributes {
refreshToken: string;
accessToken: string;
}
export interface TokenData {
type: string;
attributes: TokenAttributes;
}
export interface SignInResponse {
data: TokenData;
}