From ff74edcc048a40ec06f59e35c402ba90a86d38e9 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Sat, 5 Oct 2024 14:29:41 +0200 Subject: [PATCH] feat(auth): refresh access token on-demand when receiving 401 error --- actions/providers/providers.ts | 12 ++- app/(prowler)/profile/page.tsx | 6 ++ app/(prowler)/users/page.tsx | 7 +- app/api/providers/route.ts | 10 --- auth.config.ts | 124 +++++++++++++++----------- components/ui/sidebar/SidebarWrap.tsx | 20 ++--- nextauth.d.ts | 2 +- types/components.ts | 5 -- types/users/index.ts | 1 + types/users/users.ts | 52 +++++++++++ 10 files changed, 152 insertions(+), 87 deletions(-) delete mode 100644 app/api/providers/route.ts create mode 100644 types/users/index.ts create mode 100644 types/users/users.ts diff --git a/actions/providers/providers.ts b/actions/providers/providers.ts index ea37ca8986..8c67196f92 100644 --- a/actions/providers/providers.ts +++ b/actions/providers/providers.ts @@ -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"); diff --git a/app/(prowler)/profile/page.tsx b/app/(prowler)/profile/page.tsx index 76c72a8800..91fa4907f0 100644 --- a/app/(prowler)/profile/page.tsx +++ b/app/(prowler)/profile/page.tsx @@ -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 ( <>
{JSON.stringify(session.user, null, 2)}
+
{JSON.stringify(session.userId, null, 2)}
+
{JSON.stringify(session.tenantId, null, 2)}
+
{JSON.stringify(session, null, 2)}
); } diff --git a/app/(prowler)/users/page.tsx b/app/(prowler)/users/page.tsx index 0a2f240f57..34a887589f 100644 --- a/app/(prowler)/users/page.tsx +++ b/app/(prowler)/users/page.tsx @@ -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 ( diff --git a/app/api/providers/route.ts b/app/api/providers/route.ts deleted file mode 100644 index ef9b5995e2..0000000000 --- a/app/api/providers/route.ts +++ /dev/null @@ -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 }); -} diff --git a/auth.config.ts b/auth.config.ts index 2370fe02bb..a249c85aaa 100644 --- a/auth.config.ts +++ b/auth.config.ts @@ -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; }, }, diff --git a/components/ui/sidebar/SidebarWrap.tsx b/components/ui/sidebar/SidebarWrap.tsx index 59b29bb458..e4e0043eac 100644 --- a/components/ui/sidebar/SidebarWrap.tsx +++ b/components/ui/sidebar/SidebarWrap.tsx @@ -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 = () => { - {" "} + Loading...

}> + +
diff --git a/nextauth.d.ts b/nextauth.d.ts index e4eaa51f8a..d5b6f472f9 100644 --- a/nextauth.d.ts +++ b/nextauth.d.ts @@ -12,7 +12,7 @@ declare module "next-auth" { user: { name: string; email: string; - company?: string; + companyName?: string; dateJoined: string; } & DefaultSession["user"]; userId: string; diff --git a/types/components.ts b/types/components.ts index 9ad3507108..6bac7ce1d2 100644 --- a/types/components.ts +++ b/types/components.ts @@ -1,4 +1,3 @@ -import { JwtPayload } from "jwt-decode"; import { SVGProps } from "react"; export type IconSvgProps = SVGProps & { @@ -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"; diff --git a/types/users/index.ts b/types/users/index.ts new file mode 100644 index 0000000000..ddf77b4624 --- /dev/null +++ b/types/users/index.ts @@ -0,0 +1 @@ +export * from "./users"; diff --git a/types/users/users.ts b/types/users/users.ts new file mode 100644 index 0000000000..61a92ae70e --- /dev/null +++ b/types/users/users.ts @@ -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; +}