Skip to content

Commit

Permalink
Merge pull request #109 from FusionAuth/108-angular-ssr-support
Browse files Browse the repository at this point in the history
[Angular] SSR support
  • Loading branch information
JakeLo123 authored Jun 4, 2024
2 parents 7968219 + a54172c commit 5553762
Show file tree
Hide file tree
Showing 9 changed files with 148 additions and 70 deletions.
3 changes: 0 additions & 3 deletions packages/core/src/CookieHelpers/CookieHelpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ describe('getAccessTokenExpirationMoment', () => {
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', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/CookieHelpers/CookieHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* 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.
* @param adapter - SSR frameworks like Nuxt, Next, and angular/ssr will pass in an adapter.
*/
export function getAccessTokenExpirationMoment(
cookieName: string = 'app.at_exp',
Expand Down
5 changes: 4 additions & 1 deletion packages/sdk-angular/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"root": false,
"extends": "../../.eslintrc"
"extends": "../../.eslintrc",
"rules": {
"@typescript-eslint/ban-types": "off"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// import { CookieAdapter } from '@fusionauth-sdk/core';

/** An adapter class that supports accessing cookies with SSR */
export class SSRCookieAdapter /* implements CookieAdapter */ {
constructor(private isBrowser: boolean) {}

at_exp(cookieName: string = 'app.at_exp') {
if (!this.isBrowser) {
return;
}

try {
const expCookie = document.cookie
.split('; ')
.map(c => c.split('='))
.find(([name]) => name === cookieName);
return expCookie?.[1];
} catch (error) {
console.error('Error within the SSRCookieAdapter: ', error);
return -1;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ class g {
i(this, 'registerPath');
i(this, 'logoutPath');
i(this, 'tokenRefreshPath');
i(this, 'postLogoutRedirectUri');
(this.serverUrl = e.serverUrl),
(this.clientId = e.clientId),
(this.redirectUri = e.redirectUri),
(this.scope = e.scope),
(this.postLogoutRedirectUri = e.postLogoutRedirectUri),
(this.mePath = e.mePath ?? '/app/me'),
(this.loginPath = e.loginPath ?? '/app/login'),
(this.registerPath = e.registerPath ?? '/app/register'),
Expand Down Expand Up @@ -78,19 +80,32 @@ class p {
constructor() {
i(this, 'REDIRECT_VALUE', 'fa-sdk-redirect-value');
}
get storage() {
try {
return localStorage;
} catch {
return {
/* eslint-disable */
setItem(e, t) {},
getItem(e) {},
removeItem(e) {},
/* eslint-enable */
};
}
}
handlePreRedirect(e) {
const t = `${this.generateRandomString()}:${e ?? ''}`;
localStorage.setItem(this.REDIRECT_VALUE, t);
this.storage.setItem(this.REDIRECT_VALUE, t);
}
handlePostRedirect(e) {
const t = this.stateValue ?? void 0;
e == null || e(t), localStorage.removeItem(this.REDIRECT_VALUE);
e == null || e(t), this.storage.removeItem(this.REDIRECT_VALUE);
}
get didRedirect() {
return !!localStorage.getItem(this.REDIRECT_VALUE);
return !!this.storage.getItem(this.REDIRECT_VALUE);
}
get stateValue() {
const e = localStorage.getItem(this.REDIRECT_VALUE);
const e = this.storage.getItem(this.REDIRECT_VALUE);
if (!e) return null;
const [, ...t] = e.split(':');
return t.join(':');
Expand All @@ -103,13 +118,28 @@ class p {
);
}
}
function m(r = 'app.at_exp') {
const e = document.cookie
function m(r = 'app.at_exp', e) {
if (e) return c(e.at_exp(r));
let t;
try {
t = document.cookie;
} catch {
return (
console.error(
'Error accessing cookies in fusionauth. If you are using SSR you must configure the SDK with a cookie adapter',
),
-1
);
}
const s = t
.split('; ')
.map(s => s.split('='))
.find(([s]) => s === r),
t = e == null ? void 0 : e[1];
return t ? parseInt(t) * 1e3 : null;
.map(n => n.split('='))
.find(([n]) => n === r),
o = s == null ? void 0 : s[1];
return c(o);
}
function c(r) {
return r ? Number(r) * 1e3 : -1;
}
class U {
constructor(e) {
Expand All @@ -122,13 +152,13 @@ class U {
serverUrl: e.serverUrl,
clientId: e.clientId,
redirectUri: e.redirectUri,
postLogoutRedirectUri: e.postLogoutRedirectUri,
scope: e.scope,
mePath: e.mePath,
loginPath: e.loginPath,
registerPath: e.registerPath,
logoutPath: e.logoutPath,
tokenRefreshPath: e.tokenRefreshPath,
postLogoutRedirectUri: e.postLogoutRedirectUri,
})),
this.scheduleTokenExpiration();
}
Expand Down Expand Up @@ -170,56 +200,54 @@ class U {
return this.scheduleTokenExpiration(), e;
}
initAutoRefresh() {
const e = this.at_exp,
t = this.config.autoRefreshSecondsBeforeExpiry ?? 10;
if (!e) return;
const s = t * 1e3,
o = /* @__PURE__ */ new Date().getTime(),
h = e - s,
l = Math.max(h - o, 0);
if (!this.isLoggedIn) return;
const t = (this.config.autoRefreshSecondsBeforeExpiry ?? 10) * 1e3,
s = /* @__PURE__ */ new Date().getTime(),
o = this.at_exp - t,
n = Math.max(o - s, 0);
return setTimeout(async () => {
let n, a;
let a, h;
try {
await this.refreshToken(), this.initAutoRefresh();
} catch (c) {
(a = (n = this.config).onAutoRefreshFailure) == null || a.call(n, c);
} catch (l) {
(h = (a = this.config).onAutoRefreshFailure) == null || h.call(a, l);
}
}, l);
}, n);
}
handlePostRedirect(e) {
this.isLoggedIn &&
this.redirectHelper.didRedirect &&
this.redirectHelper.handlePostRedirect(e);
}
get isLoggedIn() {
return this.at_exp
? this.at_exp > /* @__PURE__ */ new Date().getTime()
: !1;
return this.at_exp > /* @__PURE__ */ new Date().getTime();
}
/** The moment of access token expiration in milliseconds since epoch. */
get at_exp() {
return m(this.config.accessTokenExpireCookieName);
return m(
this.config.accessTokenExpireCookieName,
this.config.cookieAdapter,
);
}
/** Schedules `onTokenExpiration` at moment of access token expiration. */
scheduleTokenExpiration() {
clearTimeout(this.tokenExpirationTimeout);
const e = this.at_exp ?? -1,
t = /* @__PURE__ */ new Date().getTime(),
s = e - t;
s > 0 &&
const e = /* @__PURE__ */ new Date().getTime(),
t = this.at_exp - e;
t > 0 &&
(this.tokenExpirationTimeout = setTimeout(
this.config.onTokenExpiration,
s,
t,
));
}
}
function P() {
function f() {
const r = /* @__PURE__ */ new Date();
r.setHours(r.getHours() + 1);
const e = r.getTime() / 1e3;
document.cookie = `app.at_exp=${e}`;
}
function f() {
function P() {
document.cookie = 'app.at_exp=;expires=Thu, 01 Jan 1970 00:00:00 GMT';
}
function w(r) {
Expand All @@ -231,7 +259,8 @@ function w(r) {
}
export {
U as SDKCore,
P as mockIsLoggedIn,
f as mockIsLoggedIn,
w as mockWindowLocation,
f as removeAt_expCookie,
P as removeAt_expCookie,
};
//# sourceMappingURL=index.js.map
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ModuleWithProviders, NgModule } from '@angular/core';
import { FusionAuthConfig } from './types';
import { FusionAuthService } from './fusion-auth.service';
import { FUSIONAUTH_SERVICE_CONFIG } from './injectionToken';
import { FusionAuthLoginButtonComponent } from './components/fusionauth-login.button/fusion-auth-login-button.component';
import { FusionAuthLogoutButtonComponent } from './components/fusionauth-logout.button/fusion-auth-logout-button.component';
import { FusionAuthRegisterButtonComponent } from './components/fusionauth-register.button/fusion-auth-register-button.component';
Expand All @@ -25,10 +26,8 @@ export class FusionAuthModule {
return {
ngModule: FusionAuthModule,
providers: [
{
provide: FusionAuthService,
useValue: new FusionAuthService(fusionAuthConfig),
},
{ provide: FUSIONAUTH_SERVICE_CONFIG, useValue: fusionAuthConfig },
FusionAuthService,
],
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,40 @@
import { TestBed } from '@angular/core/testing';
import { fakeAsync, flush, tick } from '@angular/core/testing';
import { take } from 'rxjs';

import { FusionAuthConfig } from './types';
import { FusionAuthService } from './fusion-auth.service';
import { fakeAsync, flush, tick } from '@angular/core/testing';
import { FusionAuthModule } from './fusion-auth.module';
import { mockIsLoggedIn, removeAt_expCookie } from './core';
import { take } from 'rxjs';

const config: FusionAuthConfig = {
clientId: 'a-client-id',
redirectUri: 'http://my-app.com',
serverUrl: 'http://localhost:9011',
};

function configureTestingModule(config: FusionAuthConfig) {
TestBed.configureTestingModule({
imports: [FusionAuthModule.forRoot(config)],
});
return TestBed.inject(FusionAuthService);
}

describe('FusionAuthService', () => {
afterEach(() => {
removeAt_expCookie();
localStorage.clear();
});

const config: FusionAuthConfig = {
clientId: 'a-client-id',
redirectUri: 'http://my-app.com',
serverUrl: 'http://localhost:9011',
};

it('Can be configured to automatically handle getting userInfo', (done: DoneFn) => {
mockIsLoggedIn();

const user = { email: '[email protected]' };
spyOn(window, 'fetch').and.resolveTo(
new Response(
JSON.stringify({
email: '[email protected]',
}),
{ status: 200 },
),
new Response(JSON.stringify(user), { status: 200 }),
);

const service = new FusionAuthService(config);
const service = configureTestingModule(config);

service.getUserInfoObservable().subscribe({
next: userInfo => {
Expand All @@ -46,7 +52,7 @@ describe('FusionAuthService', () => {
new Response(null, { status: responseStatus }),
);

const service = new FusionAuthService(config);
const service = configureTestingModule(config);

service.getUserInfoObservable().subscribe({
error: error => {
Expand All @@ -61,7 +67,7 @@ describe('FusionAuthService', () => {
it("Contains an observable 'isLoggedin$' property that becomes false as the access token expires.", fakeAsync(() => {
mockIsLoggedIn(); // sets `app.at_exp` cookie so user is logged in for 1 hour.

const service = new FusionAuthService(config);
const service = configureTestingModule(config);

tick(60 * 59 * 1000);
service.isLoggedIn$.pipe(take(1)).subscribe(isLoggedIn => {
Expand All @@ -80,7 +86,7 @@ describe('FusionAuthService', () => {
mockIsLoggedIn();
const spy = spyOn(FusionAuthService.prototype, 'initAutoRefresh');

const service = new FusionAuthService({
const service = configureTestingModule({
...config,
shouldAutoRefresh: true,
});
Expand All @@ -90,14 +96,18 @@ describe('FusionAuthService', () => {
});

it("Does not invoke 'initAutoRefresh' if the user is not logged in", () => {
const spy = spyOn(FusionAuthService.prototype, 'initAutoRefresh');
const service = new FusionAuthService({
const initAutoRefreshSpy = spyOn(
FusionAuthService.prototype,
'initAutoRefresh',
);

const service = configureTestingModule({
...config,
shouldAutoRefresh: true,
});

expect(service.isLoggedIn()).toBe(false);
expect(spy).not.toHaveBeenCalled();
expect(initAutoRefreshSpy).not.toHaveBeenCalled();
});

it("Invokes an 'onRedirect' callback", () => {
Expand All @@ -107,8 +117,7 @@ describe('FusionAuthService', () => {
localStorage.setItem('fa-sdk-redirect-value', `abc123:${stateValue}`);

const onRedirect = jasmine.createSpy('onRedirectSpy');

new FusionAuthService({ ...config, onRedirect });
configureTestingModule({ ...config, onRedirect });

expect(onRedirect).toHaveBeenCalledWith('/welcome-page');
});
Expand All @@ -118,7 +127,7 @@ describe('FusionAuthService', () => {
spyOn(window, 'fetch').and.resolveTo(new Response(null, { status: 400 }));
const onAutoRefreshFailure = jasmine.createSpy('onAutoRefreshFailureSpy');

new FusionAuthService({
configureTestingModule({
...config,
shouldAutoRefresh: true,
onAutoRefreshFailure,
Expand Down
Loading

0 comments on commit 5553762

Please sign in to comment.