Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue SDK v1.1.0 #98

Merged
merged 3 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,7 @@
"build:sdk-react": "yarn build:lexicon && yarn build:core && yarn workspace @fusionauth/react-sdk build",
"build:sdk-vue": "yarn build:lexicon && yarn build:core && yarn workspace @fusionauth/vue-sdk build",
"yalc-pub:sdk-react": "yarn build:sdk-react && yalc publish packages/sdk-react",
"yalc-push:sdk-react": "yarn build:sdk-react && yalc push packages/sdk-react",
"yalc-pub:sdk-vue": "yarn build:sdk-vue && yalc publish packages/sdk-vue",
"yalc-push:sdk-vue": "yarn build:sdk-vue && yalc push packages/sdk-vue",
"test": "yarn test:lexicon && yarn test:core && yarn test:sdk-react && yarn test:sdk-angular && yarn test:sdk-vue",
"test:core": "yarn workspace @fusionauth-sdk/core test",
"test:lexicon": "yarn workspace @fusionauth-sdk/lexicon test",
Expand Down
32 changes: 29 additions & 3 deletions packages/core/src/CookieHelpers/CookieHelpers.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,44 @@
import { describe, it, expect, afterEach } from 'vitest';

import { getAccessTokenExpirationMoment } from '.';
import { CookieAdapter, getAccessTokenExpirationMoment } from '.';

describe('getAccessTokenExpirationMoment', () => {
afterEach(() => {
document.cookie =
'app.at_exp' + '=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
});

it('Should get the "app.at_exp" cookie value in milliseconds', () => {
const exp = Date.now();
document.cookie = `app.at_exp=${exp}`;
expect(getAccessTokenExpirationMoment()).toBe(exp * 1000);
});
it('Should return null if the cookie is not set', () => {
expect(getAccessTokenExpirationMoment()).toBeNull();

it('Should return -1 if the cookie is not set', () => {
expect(getAccessTokenExpirationMoment()).toBe(-1);
});

it('Accepts a specific cookieName if one is provided', () => {
const cookieName = 'my-special-cookie';
const exp = 1200;
document.cookie = `${cookieName}=${exp}`;

expect(getAccessTokenExpirationMoment(cookieName)).toBe(1200000);

document.cookie =
cookieName + '=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
});

it('Will get the value from a cookieAdapter if one is passed in', () => {
const value = '500';
const cookieAdapter: CookieAdapter = {
at_exp() {
return value;
},
};

expect(getAccessTokenExpirationMoment(undefined, cookieAdapter)).toBe(
500000,
);
});
});
41 changes: 36 additions & 5 deletions packages/core/src/CookieHelpers/CookieHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,45 @@
/**
* Parses document.cookie for the access token expiration cookie value.
* @returns {(number | null)} The moment of expiration in milliseconds since epoch.
* Gets the `app.at_exp` cookie and converts it to milliseconds since epoch.
* Returns -1 if the cookie is not present.
* @param cookieName - defaults to `app.at_exp`.
* @param adapter - SSR frameworks like Nuxt and Next will pass in an adapter.
*/
export function getAccessTokenExpirationMoment(
cookieName: string = 'app.at_exp',
): number | null {
const expCookie = document.cookie
adapter?: CookieAdapter,
): number | -1 {
if (adapter) {
return toMilliseconds(adapter.at_exp(cookieName));
}

let cookie;

try {
// `document` throws a ReferenceError if this runs in a
// non-browser environment such as an SSR framework like Nuxt or Next.
cookie = document.cookie;
} catch {
console.error(
'Error accessing cookies in fusionauth. If you are using SSR you must configure the SDK with a cookie adapter',
);
return -1;
}

const expCookie = cookie
.split('; ')
.map(c => c.split('='))
.find(([name]) => name === cookieName);
const cookieValue = expCookie?.[1];
return cookieValue ? parseInt(cookieValue) * 1000 : null;

return toMilliseconds(cookieValue);
}

export interface CookieAdapter {
/** returns the `app.at_exp` cookie without manipulating the value. */
at_exp: (cookieName?: string) => number | string | undefined;
}

function toMilliseconds(seconds?: number | string): number {
if (!seconds) return -1;
else return Number(seconds) * 1000;
}
22 changes: 18 additions & 4 deletions packages/core/src/RedirectHelper/RedirectHelper.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,38 @@
/** A class responsible for storing a redirect value in localStorage and cleanup afterward. */
export class RedirectHelper {
private readonly REDIRECT_VALUE = 'fa-sdk-redirect-value';
private get storage(): Storage {
try {
return localStorage;
} catch {
// fallback for non-browser environments where localStorage is not defined.
return {
/* eslint-disable */
setItem(_key: string, _value: string) {},
getItem(_key: string) {},
removeItem(_key: string) {},
/* eslint-enable */
} as Storage;
}
}

handlePreRedirect(state?: string) {
const valueForStorage = `${this.generateRandomString()}:${state ?? ''}`;
localStorage.setItem(this.REDIRECT_VALUE, valueForStorage);
this.storage.setItem(this.REDIRECT_VALUE, valueForStorage);
}

handlePostRedirect(callback?: (state?: string) => void) {
const stateValue = this.stateValue ?? undefined;
callback?.(stateValue);
localStorage.removeItem(this.REDIRECT_VALUE);
this.storage.removeItem(this.REDIRECT_VALUE);
}

get didRedirect() {
return Boolean(localStorage.getItem(this.REDIRECT_VALUE));
return Boolean(this.storage.getItem(this.REDIRECT_VALUE));
}

private get stateValue() {
const redirectValue = localStorage.getItem(this.REDIRECT_VALUE);
const redirectValue = this.storage.getItem(this.REDIRECT_VALUE);

if (!redirectValue) {
return null;
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/SDKConfig/SDKConfig.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { CookieAdapter } from '..';

/**
* Config for FusionAuth Web SDKs
*/
Expand Down Expand Up @@ -83,6 +85,11 @@ export interface SDKConfig {
*/
onAutoRefreshFailure?: (error: Error) => void;

/**
* Adapter pattern for SSR frameworks such as next or nuxt
*/
cookieAdapter?: CookieAdapter;

/**
* Callback to be invoked at the moment of access token expiration
*/
Expand Down
22 changes: 8 additions & 14 deletions packages/core/src/SDKCore/SDKCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,18 +80,17 @@ export class SDKCore {
}

initAutoRefresh(): NodeJS.Timeout | undefined {
const tokenExpirationMoment = this.at_exp;
const secondsBeforeRefresh =
this.config.autoRefreshSecondsBeforeExpiry ?? 10;

if (!tokenExpirationMoment) {
if (!this.isLoggedIn) {
return;
}

const secondsBeforeRefresh =
this.config.autoRefreshSecondsBeforeExpiry ?? 10;

const millisecondsBeforeRefresh = secondsBeforeRefresh * 1000;

const now = new Date().getTime();
const refreshTime = tokenExpirationMoment - millisecondsBeforeRefresh;
const refreshTime = this.at_exp - millisecondsBeforeRefresh;
const timeTillRefresh = Math.max(refreshTime - now, 0);

return setTimeout(async () => {
Expand All @@ -111,28 +110,23 @@ export class SDKCore {
}

get isLoggedIn() {
if (!this.at_exp) {
return false;
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JakeLo123 isLoggedIn() feels like it should always return a bool no?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it will return a bool. I removed this if(!this.at_exp) { return false } guard because I switch at_exp from type number | null to number where it's just -1 if it's not present.

return this.at_exp > new Date().getTime();
}

/** The moment of access token expiration in milliseconds since epoch. */
private get at_exp(): number | null {
private get at_exp(): number | -1 {
return getAccessTokenExpirationMoment(
this.config.accessTokenExpireCookieName,
this.config.cookieAdapter,
);
}

/** Schedules `onTokenExpiration` at moment of access token expiration. */
private scheduleTokenExpiration(): void {
clearTimeout(this.tokenExpirationTimeout);

const expirationMoment = this.at_exp ?? -1;

const now = new Date().getTime();
const millisecondsTillExpiration = expirationMoment - now;
const millisecondsTillExpiration = this.at_exp - now;

if (millisecondsTillExpiration > 0) {
this.tokenExpirationTimeout = setTimeout(
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './SDKCore';
export * from './SDKConfig';
export * from './SDKContext';
export * from './testUtils';
export { type CookieAdapter } from './CookieHelpers';
5 changes: 5 additions & 0 deletions packages/sdk-vue/CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
fusionauth-vue-sdk Changes

Changes in 1.1.0

- The SDK now supports [Nuxt](https://nuxt.com/).
- Adds `nuxtUseCookie` option to `FusionAuthConfig` to handle SSR.

Changes in 1.0.1

- Adds `onAutoRefreshFailure` option to `FusionAuthConfig`.
Expand Down
22 changes: 17 additions & 5 deletions packages/sdk-vue/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ An SDK for using FusionAuth in Vue applications.
- [Installation](#installation)
- [Usage](#usage)
- [Configuring the SDK](#configuring-the-sdk)
- [Configuring with Nuxt](#configuring-with-nuxt)
- [useFusionAuth Composable](#usefusionauth-composable)
- [State parameter](#state-parameter)
- [UI Components](#ui-components)
Expand Down Expand Up @@ -105,6 +106,21 @@ If you want to use the pre-styled buttons, don't forget to import the css file:
import '@fusionauth/vue-sdk/dist/style.css';
```

#### Configuring with [Nuxt](https://nuxt.com/)

If you're using the SDK in a nuxt app, pass the [`useCookie`](https://nuxt.com/docs/api/composables/use-cookie) composable into the config object in your plugin definition.

```typescript
import { useCookie } from "#app";

export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(FusionAuthVuePlugin, {
...config
nuxtUseCookie: useCookie,
});
});
```

### `useFusionAuth` composable

You can interact with the SDK by using the `useFusionAuth`, which leverages [Vue's Composition API](https://vuejs.org/guide/reusability/composables).
Expand Down Expand Up @@ -216,11 +232,7 @@ Use backticks for code in this readme. This readme is included on the FusionAuth

## Known issues

### Nuxt

This issue affects versions `<=1.0.0`.

If you are using [Nuxt](https://nuxt.com/) or any type of SSR (server side rendering), the SDK will not work. [See details here.](https://github.com/FusionAuth/fusionauth-javascript-sdk/issues/74)
None.

## Releases

Expand Down
26 changes: 21 additions & 5 deletions packages/sdk-vue/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,17 @@ An SDK for using FusionAuth in Vue applications.
- [Installation](#installation)
- [Usage](#usage)
- [Configuring the SDK](#configuring-the-sdk)
- [Configuring with Nuxt](#configuring-with-nuxt)
- [useFusionAuth Composable](#usefusionauth-composable)
- [State parameter](#state-parameter)
- [UI Components](#ui-components)
- [Protecting Content](#protecting-content)
- [Pre-built buttons](#pre-built-buttons)
- [Quickstart](#quickstart)
- [Documentation](#documentation)
- [Known Issues](#known-issues)
- [Releases](#releases)
- [Upgrade Policy](#upgrade-policy)
- [Quickstart](#quickstart)
- [Documentation](#documentation)
- [Known Issues](#known-issues)
- [Releases](#releases)
- [Upgrade Policy](#upgrade-policy)

<!--
this tag, and the corresponding end tag, are used to delineate what is pulled into the FusionAuth docs site (the client libraries pages). Don't remove unless you also change the docs site.
Expand Down Expand Up @@ -107,6 +108,21 @@ If you want to use the pre-styled buttons, don't forget to import the css file:
import '@fusionauth/vue-sdk/dist/style.css';
```

#### Configuring with Nuxt

If you're using the SDK in a nuxt app, pass the [`useCookie`](https://nuxt.com/docs/api/composables/use-cookie) composable into the config object in your plugin definition.

```typescript
import { useCookie } from "#app";

export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(FusionAuthVuePlugin, {
...config
nuxtUseCookie: useCookie,
});
});
```

### `useFusionAuth` composable

You can interact with the SDK by using the `useFusionAuth`, which leverages [Vue's Composition API](https://vuejs.org/guide/reusability/composables).
Expand Down
Loading