From ef4b0aeb981e80ab32a5badb017d453d0f044451 Mon Sep 17 00:00:00 2001 From: Itai Levi Date: Mon, 7 Oct 2024 17:55:55 -0400 Subject: [PATCH 1/6] feat(AuthProvider): only refresh after last refresh time --- package-lock.json | 18 +++++++++--------- package.json | 3 ++- src/client/AuthProvider.tsx | 33 ++++++++++++++++++++++++--------- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index 51cbda9..99d30d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,14 @@ { "name": "@propelauth/nextjs", - "version": "0.1.10", + "version": "0.1.11", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@propelauth/nextjs", - "version": "0.1.10", + "version": "0.1.11", "dependencies": { - "@propelauth/node-apis": "^2.1.21", + "@propelauth/node-apis": "^2.1.22", "jose": "^5.2.4" }, "devDependencies": { @@ -615,9 +615,9 @@ } }, "node_modules/@propelauth/node-apis": { - "version": "2.1.21", - "resolved": "https://registry.npmjs.org/@propelauth/node-apis/-/node-apis-2.1.21.tgz", - "integrity": "sha512-YFZiHjG8MpJ6IIOXC88RTk4HYsGrjA8zLOqSpuCdZuHCX2HrkQX8JduXM2oAsFzBvCQbxquNbhEpxYRx7dCKJg==" + "version": "2.1.22", + "resolved": "https://registry.npmjs.org/@propelauth/node-apis/-/node-apis-2.1.22.tgz", + "integrity": "sha512-yjcVQRWuYT5B5gbbJTz/1S5vEfB+djQIoH906Vb48Ku683ada+MPHF28FHjeqM0xmqhIgPkiyrx/3f4sl0UUMQ==" }, "node_modules/@swc/counter": { "version": "0.1.3", @@ -2256,9 +2256,9 @@ } }, "@propelauth/node-apis": { - "version": "2.1.21", - "resolved": "https://registry.npmjs.org/@propelauth/node-apis/-/node-apis-2.1.21.tgz", - "integrity": "sha512-YFZiHjG8MpJ6IIOXC88RTk4HYsGrjA8zLOqSpuCdZuHCX2HrkQX8JduXM2oAsFzBvCQbxquNbhEpxYRx7dCKJg==" + "version": "2.1.22", + "resolved": "https://registry.npmjs.org/@propelauth/node-apis/-/node-apis-2.1.22.tgz", + "integrity": "sha512-yjcVQRWuYT5B5gbbJTz/1S5vEfB+djQIoH906Vb48Ku683ada+MPHF28FHjeqM0xmqhIgPkiyrx/3f4sl0UUMQ==" }, "@swc/counter": { "version": "0.1.3", diff --git a/package.json b/package.json index 6f0dcc0..4995d5a 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,8 @@ "scripts": { "build": "tsup", "dev": "tsup --watch", - "test": "jest" + "test": "jest", + "prepublishOnly": "npm run build" }, "devDependencies": { "@types/node": "^20.3.1", diff --git a/src/client/AuthProvider.tsx b/src/client/AuthProvider.tsx index e3eee2b..1683681 100644 --- a/src/client/AuthProvider.tsx +++ b/src/client/AuthProvider.tsx @@ -1,10 +1,10 @@ 'use client' -import React, { useCallback, useEffect, useReducer } from 'react' -import { doesLocalStorageMatch, hasWindow, isEqual, saveUserToLocalStorage, USER_INFO_KEY } from './utils' import { useRouter } from 'next/navigation.js' -import { User } from './useUser' +import React, { useCallback, useEffect, useReducer, useState } from 'react' import { toOrgIdToOrgMemberInfo } from '../user' +import { User } from './useUser' +import { doesLocalStorageMatch, hasWindow, isEqual, saveUserToLocalStorage, USER_INFO_KEY } from './utils' export interface RedirectToSignupOptions { postSignupRedirectPath?: string @@ -46,9 +46,12 @@ interface InternalAuthState { setActiveOrg: (orgId: string) => Promise } +const DEFAULT_MIN_SECONDS_BEFORE_REFRESH = 120 + export type AuthProviderProps = { authUrl: string reloadOnAuthChange?: boolean + minSecondsBeforeRefresh?: number children?: React.ReactNode refreshOnFocus?: boolean } @@ -74,7 +77,7 @@ type AuthState = { authChangeDetected: boolean } -const initialAuthState = { +const initialAuthState: AuthState = { loading: true, userAndAccessToken: { user: undefined, @@ -129,7 +132,9 @@ function authStateReducer(_state: AuthState, action: AuthStateAction): AuthState } export const AuthProvider = (props: AuthProviderProps) => { + const minSecondsBeforeRefresh = props.minSecondsBeforeRefresh ?? DEFAULT_MIN_SECONDS_BEFORE_REFRESH const [authState, dispatchInner] = useReducer(authStateReducer, initialAuthState) + const [lastRefresh, setLastRefresh] = useState() const router = useRouter() const reloadOnAuthChange = props.reloadOnAuthChange ?? true @@ -185,11 +190,21 @@ export const AuthProvider = (props: AuthProviderProps) => { } if (!action.error) { dispatch(action) + setLastRefresh(Math.floor(Date.now() / 1000)) } else if (action.error === 'unexpected') { clearAndSetRetryTimer() } } + // If we were offline or on a different tab, when we return, refetch auth info + // Some browsers trigger focus more often than we'd like, so we'll debounce a little here as well + const refreshOnOnlineOrFocus = async function () { + const currentTimeSecs = Math.floor(Date.now() / 1000) + if (lastRefresh && currentTimeSecs > lastRefresh + minSecondsBeforeRefresh) { + await refreshToken() + } + } + async function onStorageEvent(event: StorageEvent) { if ( event.key === USER_INFO_KEY && @@ -203,11 +218,11 @@ export const AuthProvider = (props: AuthProviderProps) => { if (hasWindow()) { window.addEventListener('storage', onStorageEvent) - window.addEventListener('online', refreshToken) + window.addEventListener('online', refreshOnOnlineOrFocus) // Default for refreshOnFocus is true if (props.refreshOnFocus !== false) { - window.addEventListener('focus', refreshToken) + window.addEventListener('focus', refreshOnOnlineOrFocus) } } @@ -219,11 +234,11 @@ export const AuthProvider = (props: AuthProviderProps) => { } if (hasWindow()) { window.removeEventListener('storage', onStorageEvent) - window.removeEventListener('online', refreshToken) - window.removeEventListener('focus', refreshToken) + window.removeEventListener('online', refreshOnOnlineOrFocus) + window.removeEventListener('focus', refreshOnOnlineOrFocus) } } - }, [dispatch, authState.userAndAccessToken.user]) + }, [dispatch, authState.userAndAccessToken.user, lastRefresh]) const logout = useCallback(async () => { await fetch('/api/auth/logout', { From e532a24fea4029ea12e92195c902510012e1cac0 Mon Sep 17 00:00:00 2001 From: Itai Levi Date: Tue, 8 Oct 2024 10:00:03 -0400 Subject: [PATCH 2/6] fix(AuthProvider): set last refresh on initial refresh token fetch --- src/client/AuthProvider.tsx | 15 +++++++++++---- src/client/utils.ts | 6 +++++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/client/AuthProvider.tsx b/src/client/AuthProvider.tsx index 1683681..dc8f136 100644 --- a/src/client/AuthProvider.tsx +++ b/src/client/AuthProvider.tsx @@ -4,7 +4,14 @@ import { useRouter } from 'next/navigation.js' import React, { useCallback, useEffect, useReducer, useState } from 'react' import { toOrgIdToOrgMemberInfo } from '../user' import { User } from './useUser' -import { doesLocalStorageMatch, hasWindow, isEqual, saveUserToLocalStorage, USER_INFO_KEY } from './utils' +import { + currentTimeSecs, + doesLocalStorageMatch, + hasWindow, + isEqual, + saveUserToLocalStorage, + USER_INFO_KEY, +} from './utils' export interface RedirectToSignupOptions { postSignupRedirectPath?: string @@ -162,6 +169,7 @@ export const AuthProvider = (props: AuthProviderProps) => { const action = await apiGetUserInfo() if (!didCancel && !action.error) { dispatch(action) + setLastRefresh(currentTimeSecs()) } } @@ -190,7 +198,7 @@ export const AuthProvider = (props: AuthProviderProps) => { } if (!action.error) { dispatch(action) - setLastRefresh(Math.floor(Date.now() / 1000)) + setLastRefresh(currentTimeSecs()) } else if (action.error === 'unexpected') { clearAndSetRetryTimer() } @@ -199,8 +207,7 @@ export const AuthProvider = (props: AuthProviderProps) => { // If we were offline or on a different tab, when we return, refetch auth info // Some browsers trigger focus more often than we'd like, so we'll debounce a little here as well const refreshOnOnlineOrFocus = async function () { - const currentTimeSecs = Math.floor(Date.now() / 1000) - if (lastRefresh && currentTimeSecs > lastRefresh + minSecondsBeforeRefresh) { + if (lastRefresh && currentTimeSecs() > lastRefresh + minSecondsBeforeRefresh) { await refreshToken() } } diff --git a/src/client/utils.ts b/src/client/utils.ts index 22f531f..903401e 100644 --- a/src/client/utils.ts +++ b/src/client/utils.ts @@ -7,6 +7,10 @@ export function hasWindow(): boolean { return typeof window !== 'undefined' } +export const currentTimeSecs = (): number => { + return Math.floor(Date.now() / 1000) +} + export function saveUserToLocalStorage(user: User | undefined) { if (user) { localStorage.setItem(USER_INFO_KEY, JSON.stringify(user)) @@ -79,4 +83,4 @@ export function isEqual(a: any, b: any): boolean { // We need to make sure that the comparison is done with objects that have gone through the same transformation, so we mimic the localStorage transformation to json and back function jsonSerialize(userFromToken: UserFromToken) { return JSON.parse(JSON.stringify(userFromToken)) -} \ No newline at end of file +} From e540ab65547e22401c1e78ab3c19c330792008db Mon Sep 17 00:00:00 2001 From: Itai Levi Date: Tue, 8 Oct 2024 10:17:45 -0400 Subject: [PATCH 3/6] feat(app-router): add fucntion to get user and access token together --- src/server/app-router-index.ts | 13 +++++++------ src/server/app-router.ts | 15 ++++++++++++--- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/server/app-router-index.ts b/src/server/app-router-index.ts index 40bbacc..6aa4054 100644 --- a/src/server/app-router-index.ts +++ b/src/server/app-router-index.ts @@ -1,11 +1,12 @@ -export { UnauthorizedException, ConfigurationException } from './exceptions' export { + authMiddleware, + getAccessToken, + getCurrentPath, + getCurrentUrl, getRouteHandlers, getUser, + getUserAndAccessToken, getUserOrRedirect, - getAccessToken, - authMiddleware, - getCurrentUrl, - getCurrentPath, } from './app-router' -export type { RouteHandlerArgs, RedirectOptions } from './app-router' +export type { RedirectOptions, RouteHandlerArgs } from './app-router' +export { ConfigurationException, UnauthorizedException } from './exceptions' diff --git a/src/server/app-router.ts b/src/server/app-router.ts index 714a2e5..7503b28 100644 --- a/src/server/app-router.ts +++ b/src/server/app-router.ts @@ -1,6 +1,8 @@ -import { redirect } from 'next/navigation.js' import { cookies, headers } from 'next/headers.js' +import { redirect } from 'next/navigation.js' import { NextRequest, NextResponse } from 'next/server.js' +import { ACTIVE_ORG_ID_COOKIE_NAME } from '../shared' +import { UserFromToken } from './index' import { ACCESS_TOKEN_COOKIE_NAME, CALLBACK_PATH, @@ -22,8 +24,6 @@ import { validateAccessToken, validateAccessTokenOrUndefined, } from './shared' -import { UserFromToken } from './index' -import { ACTIVE_ORG_ID_COOKIE_NAME } from '../shared' export type RedirectOptions = | { @@ -56,6 +56,15 @@ export async function getUser(): Promise { return undefined } +export async function getUserAndAccessToken(): Promise<{ user?: UserFromToken; accessToken?: string }> { + const accessToken = getAccessToken() + if (accessToken) { + const user = await validateAccessTokenOrUndefined(accessToken) + return { user, accessToken } + } + return { user: undefined, accessToken } +} + export function getAccessToken(): string | undefined { return headers().get(CUSTOM_HEADER_FOR_ACCESS_TOKEN) || cookies().get(ACCESS_TOKEN_COOKIE_NAME)?.value } From 07c463dc953b910de31178b9ea075771fc05d3ee Mon Sep 17 00:00:00 2001 From: Itai Levi Date: Thu, 10 Oct 2024 15:06:10 -0400 Subject: [PATCH 4/6] refactor(getUserAndAccessToken): return either both or neither --- src/server/app-router.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/server/app-router.ts b/src/server/app-router.ts index 7503b28..901e774 100644 --- a/src/server/app-router.ts +++ b/src/server/app-router.ts @@ -56,13 +56,25 @@ export async function getUser(): Promise { return undefined } -export async function getUserAndAccessToken(): Promise<{ user?: UserFromToken; accessToken?: string }> { +export type UserAndAccessToken = + | { + user: UserFromToken + accessToken: string + } + | { + user: never + accessToken: never + } + +export async function getUserAndAccessToken(): Promise { const accessToken = getAccessToken() if (accessToken) { const user = await validateAccessTokenOrUndefined(accessToken) - return { user, accessToken } + if (user) { + return { user, accessToken } + } } - return { user: undefined, accessToken } + return { user: undefined as never, accessToken: undefined as never } } export function getAccessToken(): string | undefined { From f0d6209038a64a7a1e4d55bbcd47cbb3ad9345bd Mon Sep 17 00:00:00 2001 From: Itai Levi Date: Thu, 10 Oct 2024 16:44:30 -0400 Subject: [PATCH 5/6] fix(app-router): make user undefined instead of never --- src/server/app-router.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/app-router.ts b/src/server/app-router.ts index 901e774..13e4e91 100644 --- a/src/server/app-router.ts +++ b/src/server/app-router.ts @@ -62,7 +62,7 @@ export type UserAndAccessToken = accessToken: string } | { - user: never + user: undefined accessToken: never } @@ -74,7 +74,7 @@ export async function getUserAndAccessToken(): Promise { return { user, accessToken } } } - return { user: undefined as never, accessToken: undefined as never } + return { user: undefined, accessToken: undefined as never } } export function getAccessToken(): string | undefined { From a6bf2f3d66cc641ca8e741149c4c4be96f366046 Mon Sep 17 00:00:00 2001 From: Itai Levi Date: Thu, 10 Oct 2024 16:46:24 -0400 Subject: [PATCH 6/6] fix(AuthProvider): fix when we fetch last refresh --- src/client/AuthProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/AuthProvider.tsx b/src/client/AuthProvider.tsx index dc8f136..f7f3073 100644 --- a/src/client/AuthProvider.tsx +++ b/src/client/AuthProvider.tsx @@ -207,7 +207,7 @@ export const AuthProvider = (props: AuthProviderProps) => { // If we were offline or on a different tab, when we return, refetch auth info // Some browsers trigger focus more often than we'd like, so we'll debounce a little here as well const refreshOnOnlineOrFocus = async function () { - if (lastRefresh && currentTimeSecs() > lastRefresh + minSecondsBeforeRefresh) { + if (!lastRefresh || currentTimeSecs() > lastRefresh + minSecondsBeforeRefresh) { await refreshToken() } }