Skip to content

Commit

Permalink
Feat/error handling (#154)
Browse files Browse the repository at this point in the history
* Remove comment from tsup
* Add new toString method to the base error class
* Logger exported for use in project code
* Add error logging using loglevel
  • Loading branch information
silentworks authored Jul 5, 2022
1 parent b21e5a8 commit 38ccf1c
Show file tree
Hide file tree
Showing 18 changed files with 152 additions and 59 deletions.
5 changes: 5 additions & 0 deletions .changeset/fast-frogs-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@supabase/auth-helpers-shared': patch
---

Add new to string method to the base error class
6 changes: 6 additions & 0 deletions .changeset/tough-apples-trade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@supabase/auth-helpers-nextjs': patch
'@supabase/auth-helpers-sveltekit': patch
---

Logger can be imported in your own project for setting log levels
16 changes: 10 additions & 6 deletions packages/nextjs/src/handlers/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import {
COOKIE_OPTIONS,
jwtDecoder,
TOKEN_REFRESH_MARGIN,
CookieNotParsed,
JWTPayloadFailed,
AuthHelperError,
ErrorPayload,
AccessTokenNotFound
AccessTokenNotFound,
CookieNotFound
} from '@supabase/auth-helpers-shared';
import type { NextApiRequest, NextApiResponse } from 'next';
import getUser from '../utils/getUser';
import log from 'loglevel';
import logger from '../utils/log';

interface ResponsePayload {
user: null;
Expand All @@ -32,7 +32,7 @@ export default async function handleUser(
) {
try {
if (!req.cookies) {
throw new CookieNotParsed();
throw new CookieNotFound();
}
const cookieOptions = { ...COOKIE_OPTIONS, ...options.cookieOptions };
const tokenRefreshMargin =
Expand Down Expand Up @@ -81,12 +81,16 @@ export default async function handleUser(
} catch (e) {
let response: ResponsePayload = { user: null, accessToken: null };
if (e instanceof JWTPayloadFailed) {
logger.info('JWTPayloadFailed error has happened!');
response.error = e.toObj();
} else if (e instanceof CookieNotFound) {
logger.warn(e.toString());
} else if (e instanceof AuthHelperError) {
log.debug(e.toObj());
logger.info('AuthHelperError error has happened!');
logger.error(e.toString());
} else {
const error = e as ApiError;
log.debug(error.message);
logger.error(error.message);
}
res.status(200).json(response);
}
Expand Down
1 change: 1 addition & 0 deletions packages/nextjs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export { default as withPageAuth } from './utils/withPageAuth';
export { default as withApiAuth } from './utils/withApiAuth';
export { default as supabaseServerClient } from './utils/supabaseServerClient';
export { supabaseClient, SupabaseClient } from './utils/initSupabase';
export { default as logger } from './utils/log';
37 changes: 28 additions & 9 deletions packages/nextjs/src/utils/getUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,20 @@ import {
NextRequestAdapter,
NextResponseAdapter,
jwtDecoder,
CookieNotParsed,
JWTPayloadFailed,
AccessTokenNotFound,
RefreshTokenNotFound,
AuthHelperError
AuthHelperError,
CookieNotFound,
ErrorPayload
} from '@supabase/auth-helpers-shared';
import log from 'loglevel';
import logger from '../utils/log';

interface ResponsePayload {
user: User | null;
accessToken: string | null;
error?: ErrorPayload;
}

export interface GetUserOptions {
cookieOptions?: CookieOptions;
Expand All @@ -32,7 +39,7 @@ export default async function getUser(
| GetServerSidePropsContext
| { req: NextApiRequest; res: NextApiResponse },
options: GetUserOptions = { forceRefresh: false }
): Promise<{ user: User | null; accessToken: string | null; error?: string }> {
): Promise<ResponsePayload> {
try {
if (
!process.env.NEXT_PUBLIC_SUPABASE_URL ||
Expand All @@ -43,7 +50,7 @@ export default async function getUser(
);
}
if (!context.req.cookies) {
throw new CookieNotParsed();
throw new CookieNotFound();
}
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
Expand All @@ -69,12 +76,14 @@ export default async function getUser(
if (options.forceRefresh || jwtUser.exp < timeNow + tokenRefreshMargin) {
// JWT is expired, let's refresh from Gotrue
if (!refresh_token) throw new RefreshTokenNotFound();
logger.info('Refreshing access token...');
const { data, error } = await supabase.auth.api.refreshAccessToken(
refresh_token
);
if (error) {
throw error;
} else {
logger.info('Saving tokens to cookies...');
setCookies(
new NextRequestAdapter(context.req),
new NextResponseAdapter(context.res),
Expand All @@ -93,6 +102,7 @@ export default async function getUser(
return { user: data!.user!, accessToken: data!.access_token };
}
} else {
logger.info('Getting the user object from the database...');
const { user, error: getUserError } = await supabase.auth.api.getUser(
access_token
);
Expand All @@ -102,10 +112,19 @@ export default async function getUser(
return { user: user!, accessToken: access_token };
}
} catch (e) {
const error = e as ApiError;
if (e instanceof AuthHelperError) {
log.debug(e.toObj());
let response: ResponsePayload = { user: null, accessToken: null };
if (e instanceof JWTPayloadFailed) {
logger.info('JWTPayloadFailed error has happened!');
response.error = e.toObj();
} else if (e instanceof CookieNotFound) {
logger.warn(e.toString());
} else if (e instanceof AuthHelperError) {
logger.info('AuthHelperError error has happened!');
logger.error(e.toString());
} else {
const error = e as ApiError;
logger.error(error.message);
}
return { user: null, accessToken: null, error: error.message };
return response;
}
}
6 changes: 6 additions & 0 deletions packages/nextjs/src/utils/log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import * as loglevel from 'loglevel';

const logger = loglevel.getLogger('@supabase/auth-helpers-nextjs');
logger.info('Starting @supabase/auth-helpers-nextjs logging...');

export default logger;
1 change: 0 additions & 1 deletion packages/nextjs/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type { Options } from 'tsup';
export const tsup: Options = {
dts: true,
entryPoints: ['src/index.ts', 'src/middleware/index.ts'],
// `aws-amplify` is external, but sub-dependencies weren't automatically externalized ("require" statements were included)
external: ['next', 'react', /^@supabase\//],
format: ['cjs'],
// inject: ['src/react-shim.js'],
Expand Down
20 changes: 11 additions & 9 deletions packages/react/src/components/UserProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,16 +95,18 @@ export const UserProvider = (props: Props) => {
setUser(user);
// Set up auto token refresh
if (autoRefreshToken) {
let timeout = 20 * 1000;
const expiresAt = (user as any)?.exp;
if (expiresAt) {
const timeNow = Math.round(Date.now() / 1000);
const expiresIn = expiresAt - timeNow;
const refreshDurationBeforeExpires =
expiresIn > TOKEN_REFRESH_MARGIN ? TOKEN_REFRESH_MARGIN : 0.5;
timeout = (expiresIn - refreshDurationBeforeExpires) * 1000;
if (user) {
let timeout = 20 * 1000;
const expiresAt = (user as any).exp;
if (expiresAt) {
const timeNow = Math.round(Date.now() / 1000);
const expiresIn = expiresAt - timeNow;
const refreshDurationBeforeExpires =
expiresIn > TOKEN_REFRESH_MARGIN ? TOKEN_REFRESH_MARGIN : 0.5;
timeout = (expiresIn - refreshDurationBeforeExpires) * 1000;
}
setTimeout(checkSession, timeout);
}
setTimeout(checkSession, timeout);
}
if (!user) setIsLoading(false);
} catch (_e) {
Expand Down
1 change: 0 additions & 1 deletion packages/react/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type { Options } from 'tsup';
export const tsup: Options = {
dts: true,
entryPoints: ['src/index.tsx'],
// `aws-amplify` is external, but sub-dependencies weren't automatically externalized ("require" statements were included)
external: ['react', /^@supabase\//],
format: ['cjs'],
// inject: ['src/react-shim.js'],
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/src/utils/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export class AuthHelperError extends Error {
source: this.source
};
}

toString() {
return JSON.stringify(this.toObj());
}
}

export class CookieNotFound extends AuthHelperError {
Expand Down
1 change: 0 additions & 1 deletion packages/shared/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type { Options } from 'tsup';
export const tsup: Options = {
dts: true,
entryPoints: ['src/index.ts'],
// `aws-amplify` is external, but sub-dependencies weren't automatically externalized ("require" statements were included)
external: ['react', 'next', /^@supabase\//],
format: ['cjs', 'esm'],
// inject: ['src/react-shim.js'],
Expand Down
5 changes: 3 additions & 2 deletions packages/svelte/src/SupaAuthHelper.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { checkSession, type Session } from './helpers';
import { setIsLoading, setError, user, accessToken } from './store';
import { dequal } from 'dequal';
import { CallbackUrlFailed } from '@supabase/auth-helpers-shared';
// Props
export let supabaseClient: SupabaseClient;
Expand Down Expand Up @@ -63,8 +64,8 @@
body: JSON.stringify({ event, session })
}).then((res) => {
if (!res.ok) {
const err = new Error(`The request to ${callbackUrl} failed`);
setError(err);
const err = new CallbackUrlFailed(callbackUrl);
setError(err.message);
}
});
// Fetch the user from the API route
Expand Down
22 changes: 12 additions & 10 deletions packages/svelte/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export const checkSession = async (props: CheckSessionArgs): Promise<void> => {
);
return;
}
setError(new Error(error));
setError(new Error(error).message);
}
networkRetries = 0;
if (accessToken) {
Expand All @@ -85,16 +85,18 @@ export const checkSession = async (props: CheckSessionArgs): Promise<void> => {

// Set up auto token refresh
if (autoRefreshToken) {
let timeout = 20 * 1000;
const expiresAt = (user as UserExtra).exp;
if (expiresAt) {
const timeNow = Math.round(Date.now() / 1000);
const expiresIn = expiresAt - timeNow;
const refreshDurationBeforeExpires =
expiresIn > TOKEN_REFRESH_MARGIN ? TOKEN_REFRESH_MARGIN : 0.5;
timeout = (expiresIn - refreshDurationBeforeExpires) * 1000;
if (user) {
let timeout = 20 * 1000;
const expiresAt = (user as UserExtra).exp;
if (expiresAt) {
const timeNow = Math.round(Date.now() / 1000);
const expiresIn = expiresAt - timeNow;
const refreshDurationBeforeExpires =
expiresIn > TOKEN_REFRESH_MARGIN ? TOKEN_REFRESH_MARGIN : 0.5;
timeout = (expiresIn - refreshDurationBeforeExpires) * 1000;
}
setTimeout(checkSession, timeout);
}
setTimeout(checkSession, timeout);
}
} catch (_e) {
const err = new CallbackUrlFailed(profileUrl);
Expand Down
4 changes: 2 additions & 2 deletions packages/svelte/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ const setAccessToken = (token: string) => accessToken.set(token);
const isLoading = writable(initialValues.isLoading);
const setIsLoading = (loading: boolean) => isLoading.set(loading);

const error = writable<ErrorPayload | null>(initialValues.error);
const setError = (err: ErrorPayload) => error.set(err);
const error = writable<ErrorPayload | string>(initialValues.error);
const setError = (err: ErrorPayload | string) => error.set(err);

const resetAll = () => {
setUser(initialValues.user);
Expand Down
19 changes: 14 additions & 5 deletions packages/sveltekit/src/handlers/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import {
CookieNotParsed,
JWTPayloadFailed,
AuthHelperError,
AccessTokenNotFound
AccessTokenNotFound,
CookieNotFound
} from '@supabase/auth-helpers-shared';
import { skHelper } from '../instance';
import { getUser, saveTokens } from '../utils/getUser';
import log from 'loglevel';
import logger from '../utils/log';

export interface HandleUserOptions {
cookieOptions?: CookieOptions;
Expand All @@ -27,14 +28,17 @@ export const handleUser = (options: HandleUserOptions = {}) => {

try {
if (!req.headers.has('cookie')) {
throw new CookieNotParsed();
throw new CookieNotFound();
}

const cookieOptions = { ...COOKIE_OPTIONS, ...options.cookieOptions };
const tokenRefreshMargin =
options.tokenRefreshMargin ?? TOKEN_REFRESH_MARGIN;

const cookies = parseCookie(req.headers.get('cookie'));
if (!cookies) {
throw new CookieNotParsed();
}
const access_token = cookies[`${cookieOptions.name}-access-token`];

if (!access_token) {
Expand Down Expand Up @@ -83,13 +87,18 @@ export const handleUser = (options: HandleUserOptions = {}) => {
}
} catch (e) {
if (e instanceof JWTPayloadFailed) {
logger.info('JWTPayloadFailed error has happened!');
event.locals.error = e.toObj();
} else if (e instanceof CookieNotFound) {
logger.warn(e.toString());
} else if (e instanceof AuthHelperError) {
log.debug(e.toObj());
logger.info('AuthHelperError error has happened!');
logger.error(e.toString());
} else {
const error = e as ApiError;
log.debug(error.message);
logger.error(error.message);
}

event.locals.user = null;
event.locals.accessToken = null;
return await resolve(event);
Expand Down
8 changes: 7 additions & 1 deletion packages/sveltekit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@
export type { User, SupabaseClient } from '@supabase/supabase-js';

// Methods
export { handleCallback, handleUser, handleLogout, handleAuth } from './handlers/index';
export {
handleCallback,
handleUser,
handleLogout,
handleAuth
} from './handlers/index';
export { default as getUserAndSaveTokens, getUser } from './utils/getUser';
export { default as withApiAuth } from './utils/withApiAuth';
export { default as withPageAuth } from './utils/withPageAuth';
export { default as supabaseServerClient } from './utils/supabaseServerClient';
export { skHelper } from './instance';
export { default as logger } from './utils/log';
Loading

0 comments on commit 38ccf1c

Please sign in to comment.