From d71cd1def4898c32d2d53fb00d8068cbe7250c9d Mon Sep 17 00:00:00 2001 From: Piotr Bochenek Date: Mon, 23 Oct 2023 20:18:44 +0200 Subject: [PATCH 1/7] - Observiblify sdk and expose only this object - fix refresh session and user --- .../src/lib/angular-sdk.component.spec.ts | 26 -- .../src/lib/angular-sdk.component.ts | 8 - .../angular-sdk/src/lib/angular-sdk.module.ts | 9 - .../angular-sdk/src/lib/descope-auth.guard.ts | 1 + .../src/lib/descope-auth.module.ts | 4 +- .../src/lib/descope-auth.service.ts | 282 ++++++++++-------- projects/angular-sdk/src/public-api.ts | 1 - .../demo-app/src/app/home/home.component.ts | 12 +- .../src/app/protected/protected.component.ts | 2 +- 9 files changed, 177 insertions(+), 168 deletions(-) delete mode 100644 projects/angular-sdk/src/lib/angular-sdk.component.spec.ts delete mode 100644 projects/angular-sdk/src/lib/angular-sdk.component.ts delete mode 100644 projects/angular-sdk/src/lib/angular-sdk.module.ts diff --git a/projects/angular-sdk/src/lib/angular-sdk.component.spec.ts b/projects/angular-sdk/src/lib/angular-sdk.component.spec.ts deleted file mode 100644 index 4459764..0000000 --- a/projects/angular-sdk/src/lib/angular-sdk.component.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { AngularSdkComponent } from './angular-sdk.component'; -import { DescopeAuthConfig } from './descope-auth.module'; - -describe('AngularSdkComponent', () => { - let component: AngularSdkComponent; - let fixture: ComponentFixture; - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [AngularSdkComponent], - providers: [ - DescopeAuthConfig, - { provide: DescopeAuthConfig, useValue: { projectId: 'test' } } - ] - }); - fixture = TestBed.createComponent(AngularSdkComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/projects/angular-sdk/src/lib/angular-sdk.component.ts b/projects/angular-sdk/src/lib/angular-sdk.component.ts deleted file mode 100644 index 197a0f4..0000000 --- a/projects/angular-sdk/src/lib/angular-sdk.component.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'lib-angular-sdk', - template: `

angular-sdk works!

`, - styles: [] -}) -export class AngularSdkComponent {} diff --git a/projects/angular-sdk/src/lib/angular-sdk.module.ts b/projects/angular-sdk/src/lib/angular-sdk.module.ts deleted file mode 100644 index 383abd3..0000000 --- a/projects/angular-sdk/src/lib/angular-sdk.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { NgModule } from '@angular/core'; -import { AngularSdkComponent } from './angular-sdk.component'; - -@NgModule({ - declarations: [AngularSdkComponent], - imports: [], - exports: [AngularSdkComponent] -}) -export class AngularSdkModule {} diff --git a/projects/angular-sdk/src/lib/descope-auth.guard.ts b/projects/angular-sdk/src/lib/descope-auth.guard.ts index d417b5b..5aadecb 100644 --- a/projects/angular-sdk/src/lib/descope-auth.guard.ts +++ b/projects/angular-sdk/src/lib/descope-auth.guard.ts @@ -9,6 +9,7 @@ export const descopeAuthGuard = (route: ActivatedRouteSnapshot) => { const router = inject(Router); const fallbackUrl = route.data['descopeFallbackUrl']; const isAuthenticated = authService.isAuthenticated(); + return true; if (!isAuthenticated && !!fallbackUrl) { return from(router.navigate([fallbackUrl])); } diff --git a/projects/angular-sdk/src/lib/descope-auth.module.ts b/projects/angular-sdk/src/lib/descope-auth.module.ts index d761d52..a38978e 100644 --- a/projects/angular-sdk/src/lib/descope-auth.module.ts +++ b/projects/angular-sdk/src/lib/descope-auth.module.ts @@ -4,16 +4,14 @@ import { Optional, SkipSelf } from '@angular/core'; -import { AngularSdkComponent } from './angular-sdk.component'; export class DescopeAuthConfig { projectId = ''; } @NgModule({ - declarations: [AngularSdkComponent], imports: [], - exports: [AngularSdkComponent] + exports: [] }) export class DescopeAuthModule { constructor(@Optional() @SkipSelf() parentModule?: DescopeAuthModule) { diff --git a/projects/angular-sdk/src/lib/descope-auth.service.ts b/projects/angular-sdk/src/lib/descope-auth.service.ts index cb0274d..8efcbea 100644 --- a/projects/angular-sdk/src/lib/descope-auth.service.ts +++ b/projects/angular-sdk/src/lib/descope-auth.service.ts @@ -1,133 +1,179 @@ import { Injectable } from '@angular/core'; import { DescopeAuthConfig } from './descope-auth.module'; -import createSdk from '@descope/web-js-sdk'; import type { UserResponse } from '@descope/web-js-sdk'; -import { BehaviorSubject, finalize, from, Observable } from 'rxjs'; +import createSdk from '@descope/web-js-sdk'; +import {BehaviorSubject, finalize, from, Observable, tap} from 'rxjs'; type DescopeSDK = ReturnType; +type Observablefied = { + [K in keyof T]: T[K] extends (...args: infer Args) => Promise + ? (...args: Args) => Observable + : T[K] extends object + ? Observablefied + : T[K]; +}; + +type AngularDescopeSDK = Observablefied; + +function makeObservable(value: T): Observablefied { + const observableValue: any = {}; + + for (const key in value) { + if (typeof value[key] === 'function') { + let fn = value[key] as Function; + observableValue[key] = (...args: unknown[]) => { + return from(fn(...args)) + }; + } else if (typeof value[key] === 'object' && value[key] !== null) { + observableValue[key] = makeObservable(value[key]); + } else { + observableValue[key] = value[key]; + } + } + + return observableValue as Observablefied; +} + export interface DescopeSession { - isAuthenticated: boolean; - isSessionLoading: boolean; - sessionToken: string | null; + isAuthenticated: boolean; + isSessionLoading: boolean; + sessionToken: string | null; } export type DescopeUser = { user: UserResponse; isUserLoading: boolean }; + @Injectable({ - providedIn: 'root' + providedIn: 'root' }) export class DescopeAuthService { - private sdk: DescopeSDK; - private readonly sessionSubject: BehaviorSubject; - private readonly userSubject: BehaviorSubject; - readonly descopeSession$: Observable; - readonly descopeUser$: Observable; - - constructor(config: DescopeAuthConfig) { - this.sdk = createSdk({ - ...config, - persistTokens: true, - autoRefresh: true - }); - this.sdk.onSessionTokenChange(this.setSession.bind(this)); - this.sdk.onUserChange(this.setUser.bind(this)); - this.sessionSubject = new BehaviorSubject({ - isAuthenticated: false, - isSessionLoading: false, - sessionToken: '' - }); - this.descopeSession$ = this.sessionSubject.asObservable(); - this.userSubject = new BehaviorSubject({ - isUserLoading: false, - user: { - loginIds: [], - userId: '', - createTime: 0, - TOTP: false, - SAML: false - } - }); - this.descopeUser$ = this.userSubject.asObservable(); - } - - passwordSignUp() { - const user = { - name: 'Joe Person', - phone: '+15555555555', - email: 'email@company.com' - }; - return from( - this.sdk.password.signUp('piotr+angular@velocit.dev', '!QAZ2wsx', user) - ); - } - - passwordLogin() { - return from( - this.sdk.password.signIn('piotr+angular@velocit.dev', '!QAZ2wsx') - ); - } - - logout() { - return from(this.sdk.logout()); - } - - refreshSession() { - const beforeRefreshSession = this.sessionSubject.value; - this.sessionSubject.next({ - ...beforeRefreshSession, - isSessionLoading: true - }); - return from(this.sdk.refresh()).pipe( - finalize(() => { - const afterRefreshSession = this.sessionSubject.value; - this.sessionSubject.next({ - ...afterRefreshSession, - isSessionLoading: false - }); - }) - ); - } - - refreshUser() { - const beforeRefreshUser = this.userSubject.value; - this.userSubject.next({ - ...beforeRefreshUser, - isUserLoading: true - }); - return from(this.sdk.me()).pipe( - finalize(() => { - const afterRefreshUser = this.userSubject.value; - this.userSubject.next({ - ...afterRefreshUser, - isUserLoading: false - }); - }) - ); - } - - getSessionToken() { - return this.sessionSubject.value.sessionToken; - } - - isAuthenticated() { - return this.sessionSubject.value.isAuthenticated; - } - - private setSession(sessionToken: string | null) { - const currentSession = this.sessionSubject.value; - this.sessionSubject.next({ - sessionToken, - isAuthenticated: !!sessionToken, - isSessionLoading: currentSession.isSessionLoading - }); - } - - private setUser(user: UserResponse) { - const currentUser = this.userSubject.value; - this.userSubject.next({ - isUserLoading: currentUser.isUserLoading, - user - }); - } + public sdk: AngularDescopeSDK; + private readonly sessionSubject: BehaviorSubject; + private readonly userSubject: BehaviorSubject; + readonly descopeSession$: Observable; + readonly descopeUser$: Observable; + + private readonly EMPTY_USER = { + loginIds: [], + userId: '', + createTime: 0, + TOTP: false, + SAML: false + }; + + constructor(config: DescopeAuthConfig) { + let baseSdk = createSdk({ + ...config, + persistTokens: true, + autoRefresh: true + }); + this.sdk = makeObservable(baseSdk) + // this.sdk.onSessionTokenChange(this.setSession.bind(this)); + // this.sdk.onUserChange(this.setUser.bind(this)); + this.sessionSubject = new BehaviorSubject({ + isAuthenticated: false, + isSessionLoading: false, + sessionToken: '' + }); + this.descopeSession$ = this.sessionSubject.asObservable(); + this.userSubject = new BehaviorSubject({ + isUserLoading: false, + user: this.EMPTY_USER + }); + this.descopeUser$ = this.userSubject.asObservable(); + } + + + refreshSession() { + const beforeRefreshSession = this.sessionSubject.value; + this.sessionSubject.next({ + ...beforeRefreshSession, + isSessionLoading: true + }); + return this.sdk.refresh().pipe( + tap(data => { + const afterRequestSession = this.sessionSubject.value; + if (data.ok && data.data) { + this.sessionSubject.next({ + ...afterRequestSession, + sessionToken: data.data.sessionJwt, + isAuthenticated: !!data.data.sessionJwt + }); + } else { + this.sessionSubject.next({ + ...afterRequestSession, + sessionToken: "", + isAuthenticated: false + }); + } + }), + finalize(() => { + const afterRefreshSession = this.sessionSubject.value; + this.sessionSubject.next({ + ...afterRefreshSession, + isSessionLoading: false + }); + }) + ); + } + + refreshUser() { + const beforeRefreshUser = this.userSubject.value; + this.userSubject.next({ + ...beforeRefreshUser, + isUserLoading: true + }); + return this.sdk.me().pipe( + tap((data) => { + const afterRequestUser = this.userSubject.value; + console.log(data); + if (data.data) { + this.userSubject.next({ + ...afterRequestUser, + user: { + ...data.data + } + }); + } else { + this.userSubject.next({ + ...afterRequestUser, + user: this.EMPTY_USER + }); + } + }), + finalize(() => { + const afterRefreshUser = this.userSubject.value; + this.userSubject.next({ + ...afterRefreshUser, + isUserLoading: false + }); + }) + ); + } + + getSessionToken() { + return this.sessionSubject.value.sessionToken; + } + + isAuthenticated() { + return this.sessionSubject.value.isAuthenticated; + } + + private setSession(sessionToken: string | null) { + const currentSession = this.sessionSubject.value; + this.sessionSubject.next({ + sessionToken, + isAuthenticated: !!sessionToken, + isSessionLoading: currentSession.isSessionLoading + }); + } + + private setUser(user: UserResponse) { + const currentUser = this.userSubject.value; + this.userSubject.next({ + isUserLoading: currentUser.isUserLoading, + user + }); + } } diff --git a/projects/angular-sdk/src/public-api.ts b/projects/angular-sdk/src/public-api.ts index c15d9a8..0f6c6c4 100644 --- a/projects/angular-sdk/src/public-api.ts +++ b/projects/angular-sdk/src/public-api.ts @@ -3,6 +3,5 @@ */ export * from './lib/descope-auth.service'; -export * from './lib/angular-sdk.component'; export * from './lib/descope-auth.guard'; export * from './lib/descope-auth.module'; diff --git a/projects/demo-app/src/app/home/home.component.ts b/projects/demo-app/src/app/home/home.component.ts index 17e504e..65f987e 100644 --- a/projects/demo-app/src/app/home/home.component.ts +++ b/projects/demo-app/src/app/home/home.component.ts @@ -14,7 +14,13 @@ export class HomeComponent { ) {} signUp() { - this.authService.passwordSignUp().subscribe((resp) => { + const user = { + name: 'Joe Person', + phone: '+15555555555', + email: 'email@company.com' + }; + + this.authService.sdk.password.signUp('piotr+angular@velocit.dev', '!QAZ2wsx', user).subscribe((resp) => { if (!resp.ok) { console.log('Failed to sign up via password'); console.log('Status Code: ' + resp.code); @@ -28,8 +34,10 @@ export class HomeComponent { }); } + + login() { - this.authService.passwordLogin().subscribe((resp) => { + this.authService.sdk.password.signIn('piotr+angular@velocit.dev', '!QAZ2wsx').subscribe((resp) => { if (!resp.ok) { console.log('Failed to sign in via password'); console.log('Status Code: ' + resp.code); diff --git a/projects/demo-app/src/app/protected/protected.component.ts b/projects/demo-app/src/app/protected/protected.component.ts index fe97149..305cd14 100644 --- a/projects/demo-app/src/app/protected/protected.component.ts +++ b/projects/demo-app/src/app/protected/protected.component.ts @@ -14,7 +14,7 @@ export class ProtectedComponent { ) {} logout() { - this.authService.logout().subscribe((resp) => { + this.authService.sdk.logout().subscribe((resp) => { if (!resp.ok) { console.log('Failed to logout'); console.log('Status Code: ' + resp.code); From e3e9e01b83718b3270e1e4e19947de83de4e9e7c Mon Sep 17 00:00:00 2001 From: Piotr Bochenek Date: Mon, 23 Oct 2023 20:42:42 +0200 Subject: [PATCH 2/7] workaround for onSessionChange --- projects/angular-sdk/src/lib/descope-auth.service.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/projects/angular-sdk/src/lib/descope-auth.service.ts b/projects/angular-sdk/src/lib/descope-auth.service.ts index 8efcbea..68ea355 100644 --- a/projects/angular-sdk/src/lib/descope-auth.service.ts +++ b/projects/angular-sdk/src/lib/descope-auth.service.ts @@ -23,7 +23,12 @@ function makeObservable(value: T): Observablefied { if (typeof value[key] === 'function') { let fn = value[key] as Function; observableValue[key] = (...args: unknown[]) => { - return from(fn(...args)) + const fnResult = fn(...args); + if (fnResult instanceof Promise) { + return from(fnResult) + } else { + return fnResult + } }; } else if (typeof value[key] === 'object' && value[key] !== null) { observableValue[key] = makeObservable(value[key]); @@ -68,9 +73,9 @@ export class DescopeAuthService { persistTokens: true, autoRefresh: true }); + baseSdk.onSessionTokenChange(this.setSession.bind(this)); + baseSdk.onUserChange(this.setUser.bind(this)); this.sdk = makeObservable(baseSdk) - // this.sdk.onSessionTokenChange(this.setSession.bind(this)); - // this.sdk.onUserChange(this.setUser.bind(this)); this.sessionSubject = new BehaviorSubject({ isAuthenticated: false, isSessionLoading: false, From 4fadb249bf6902fcc7887e68887857aed133fea5 Mon Sep 17 00:00:00 2001 From: Piotr Bochenek Date: Mon, 23 Oct 2023 20:49:55 +0200 Subject: [PATCH 3/7] make it work also with non promise functions --- .../src/lib/descope-auth.service.ts | 321 +++++++++--------- 1 file changed, 161 insertions(+), 160 deletions(-) diff --git a/projects/angular-sdk/src/lib/descope-auth.service.ts b/projects/angular-sdk/src/lib/descope-auth.service.ts index 68ea355..5ec8918 100644 --- a/projects/angular-sdk/src/lib/descope-auth.service.ts +++ b/projects/angular-sdk/src/lib/descope-auth.service.ts @@ -2,183 +2,184 @@ import { Injectable } from '@angular/core'; import { DescopeAuthConfig } from './descope-auth.module'; import type { UserResponse } from '@descope/web-js-sdk'; import createSdk from '@descope/web-js-sdk'; -import {BehaviorSubject, finalize, from, Observable, tap} from 'rxjs'; +import { BehaviorSubject, finalize, from, Observable, tap } from 'rxjs'; type DescopeSDK = ReturnType; type Observablefied = { - [K in keyof T]: T[K] extends (...args: infer Args) => Promise - ? (...args: Args) => Observable - : T[K] extends object - ? Observablefied - : T[K]; + [K in keyof T]: T[K] extends (...args: infer Args) => Promise + ? (...args: Args) => Observable + : T[K] extends (...args: infer Args) => infer R + ? (...args: Args) => Observable + : T[K] extends object + ? Observablefied + : T[K]; }; type AngularDescopeSDK = Observablefied; function makeObservable(value: T): Observablefied { - const observableValue: any = {}; - - for (const key in value) { - if (typeof value[key] === 'function') { - let fn = value[key] as Function; - observableValue[key] = (...args: unknown[]) => { - const fnResult = fn(...args); - if (fnResult instanceof Promise) { - return from(fnResult) - } else { - return fnResult - } - }; - } else if (typeof value[key] === 'object' && value[key] !== null) { - observableValue[key] = makeObservable(value[key]); - } else { - observableValue[key] = value[key]; - } - } - - return observableValue as Observablefied; + const observableValue: any = {}; + + for (const key in value) { + if (typeof value[key] === 'function') { + const fn = value[key] as Function; + observableValue[key] = (...args: unknown[]) => { + const fnResult = fn(...args); + if (fnResult instanceof Promise) { + return from(fnResult); + } else { + return fnResult; + } + }; + } else if (typeof value[key] === 'object' && value[key] !== null) { + observableValue[key] = makeObservable(value[key]); + } else { + observableValue[key] = value[key]; + } + } + + return observableValue as Observablefied; } export interface DescopeSession { - isAuthenticated: boolean; - isSessionLoading: boolean; - sessionToken: string | null; + isAuthenticated: boolean; + isSessionLoading: boolean; + sessionToken: string | null; } export type DescopeUser = { user: UserResponse; isUserLoading: boolean }; - @Injectable({ - providedIn: 'root' + providedIn: 'root' }) export class DescopeAuthService { - public sdk: AngularDescopeSDK; - private readonly sessionSubject: BehaviorSubject; - private readonly userSubject: BehaviorSubject; - readonly descopeSession$: Observable; - readonly descopeUser$: Observable; - - private readonly EMPTY_USER = { - loginIds: [], - userId: '', - createTime: 0, - TOTP: false, - SAML: false - }; - - constructor(config: DescopeAuthConfig) { - let baseSdk = createSdk({ - ...config, - persistTokens: true, - autoRefresh: true - }); - baseSdk.onSessionTokenChange(this.setSession.bind(this)); - baseSdk.onUserChange(this.setUser.bind(this)); - this.sdk = makeObservable(baseSdk) - this.sessionSubject = new BehaviorSubject({ - isAuthenticated: false, - isSessionLoading: false, - sessionToken: '' - }); - this.descopeSession$ = this.sessionSubject.asObservable(); - this.userSubject = new BehaviorSubject({ - isUserLoading: false, - user: this.EMPTY_USER - }); - this.descopeUser$ = this.userSubject.asObservable(); - } - - - refreshSession() { - const beforeRefreshSession = this.sessionSubject.value; - this.sessionSubject.next({ - ...beforeRefreshSession, - isSessionLoading: true - }); - return this.sdk.refresh().pipe( - tap(data => { - const afterRequestSession = this.sessionSubject.value; - if (data.ok && data.data) { - this.sessionSubject.next({ - ...afterRequestSession, - sessionToken: data.data.sessionJwt, - isAuthenticated: !!data.data.sessionJwt - }); - } else { - this.sessionSubject.next({ - ...afterRequestSession, - sessionToken: "", - isAuthenticated: false - }); - } - }), - finalize(() => { - const afterRefreshSession = this.sessionSubject.value; - this.sessionSubject.next({ - ...afterRefreshSession, - isSessionLoading: false - }); - }) - ); - } - - refreshUser() { - const beforeRefreshUser = this.userSubject.value; - this.userSubject.next({ - ...beforeRefreshUser, - isUserLoading: true - }); - return this.sdk.me().pipe( - tap((data) => { - const afterRequestUser = this.userSubject.value; - console.log(data); - if (data.data) { - this.userSubject.next({ - ...afterRequestUser, - user: { - ...data.data - } - }); - } else { - this.userSubject.next({ - ...afterRequestUser, - user: this.EMPTY_USER - }); - } - }), - finalize(() => { - const afterRefreshUser = this.userSubject.value; - this.userSubject.next({ - ...afterRefreshUser, - isUserLoading: false - }); - }) - ); - } - - getSessionToken() { - return this.sessionSubject.value.sessionToken; - } - - isAuthenticated() { - return this.sessionSubject.value.isAuthenticated; - } - - private setSession(sessionToken: string | null) { - const currentSession = this.sessionSubject.value; - this.sessionSubject.next({ - sessionToken, - isAuthenticated: !!sessionToken, - isSessionLoading: currentSession.isSessionLoading - }); - } - - private setUser(user: UserResponse) { - const currentUser = this.userSubject.value; - this.userSubject.next({ - isUserLoading: currentUser.isUserLoading, - user - }); - } + public sdk: AngularDescopeSDK; + private readonly sessionSubject: BehaviorSubject; + private readonly userSubject: BehaviorSubject; + readonly descopeSession$: Observable; + readonly descopeUser$: Observable; + + private readonly EMPTY_USER = { + loginIds: [], + userId: '', + createTime: 0, + TOTP: false, + SAML: false + }; + + constructor(config: DescopeAuthConfig) { + this.sdk = makeObservable( + createSdk({ + ...config, + persistTokens: true, + autoRefresh: true + }) + ); + this.sdk.onSessionTokenChange(this.setSession.bind(this)); + this.sdk.onUserChange(this.setUser.bind(this)); + this.sessionSubject = new BehaviorSubject({ + isAuthenticated: false, + isSessionLoading: false, + sessionToken: '' + }); + this.descopeSession$ = this.sessionSubject.asObservable(); + this.userSubject = new BehaviorSubject({ + isUserLoading: false, + user: this.EMPTY_USER + }); + this.descopeUser$ = this.userSubject.asObservable(); + } + + refreshSession() { + const beforeRefreshSession = this.sessionSubject.value; + this.sessionSubject.next({ + ...beforeRefreshSession, + isSessionLoading: true + }); + return this.sdk.refresh().pipe( + tap((data) => { + const afterRequestSession = this.sessionSubject.value; + if (data.ok && data.data) { + this.sessionSubject.next({ + ...afterRequestSession, + sessionToken: data.data.sessionJwt, + isAuthenticated: !!data.data.sessionJwt + }); + } else { + this.sessionSubject.next({ + ...afterRequestSession, + sessionToken: '', + isAuthenticated: false + }); + } + }), + finalize(() => { + const afterRefreshSession = this.sessionSubject.value; + this.sessionSubject.next({ + ...afterRefreshSession, + isSessionLoading: false + }); + }) + ); + } + + refreshUser() { + const beforeRefreshUser = this.userSubject.value; + this.userSubject.next({ + ...beforeRefreshUser, + isUserLoading: true + }); + return this.sdk.me().pipe( + tap((data) => { + const afterRequestUser = this.userSubject.value; + console.log(data); + if (data.data) { + this.userSubject.next({ + ...afterRequestUser, + user: { + ...data.data + } + }); + } else { + this.userSubject.next({ + ...afterRequestUser, + user: this.EMPTY_USER + }); + } + }), + finalize(() => { + const afterRefreshUser = this.userSubject.value; + this.userSubject.next({ + ...afterRefreshUser, + isUserLoading: false + }); + }) + ); + } + + getSessionToken() { + return this.sessionSubject.value.sessionToken; + } + + isAuthenticated() { + return this.sessionSubject.value.isAuthenticated; + } + + private setSession(sessionToken: string | null) { + const currentSession = this.sessionSubject.value; + this.sessionSubject.next({ + sessionToken, + isAuthenticated: !!sessionToken, + isSessionLoading: currentSession.isSessionLoading + }); + } + + private setUser(user: UserResponse) { + const currentUser = this.userSubject.value; + this.userSubject.next({ + isUserLoading: currentUser.isUserLoading, + user + }); + } } From 938b4ee6227a40af419b3cff992f19bfdf5f8ae7 Mon Sep 17 00:00:00 2001 From: Piotr Bochenek Date: Mon, 23 Oct 2023 20:51:45 +0200 Subject: [PATCH 4/7] one more type fix --- projects/angular-sdk/src/lib/descope-auth.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/angular-sdk/src/lib/descope-auth.service.ts b/projects/angular-sdk/src/lib/descope-auth.service.ts index 5ec8918..787b71d 100644 --- a/projects/angular-sdk/src/lib/descope-auth.service.ts +++ b/projects/angular-sdk/src/lib/descope-auth.service.ts @@ -10,7 +10,7 @@ type Observablefied = { [K in keyof T]: T[K] extends (...args: infer Args) => Promise ? (...args: Args) => Observable : T[K] extends (...args: infer Args) => infer R - ? (...args: Args) => Observable + ? (...args: Args) => R : T[K] extends object ? Observablefied : T[K]; From 75e3b81344e0fa635f5f31d0169f25e292f7b368 Mon Sep 17 00:00:00 2001 From: Piotr Bochenek Date: Tue, 24 Oct 2023 13:38:55 +0200 Subject: [PATCH 5/7] fix linting --- .../src/lib/descope-auth.service.ts | 3 +- .../demo-app/src/app/home/home.component.ts | 58 ++++++++++--------- 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/projects/angular-sdk/src/lib/descope-auth.service.ts b/projects/angular-sdk/src/lib/descope-auth.service.ts index 787b71d..df6990f 100644 --- a/projects/angular-sdk/src/lib/descope-auth.service.ts +++ b/projects/angular-sdk/src/lib/descope-auth.service.ts @@ -19,11 +19,12 @@ type Observablefied = { type AngularDescopeSDK = Observablefied; function makeObservable(value: T): Observablefied { + /* eslint-disable @typescript-eslint/no-explicit-any */ const observableValue: any = {}; for (const key in value) { if (typeof value[key] === 'function') { - const fn = value[key] as Function; + const fn = value[key] as (...args: unknown[]) => unknown; observableValue[key] = (...args: unknown[]) => { const fnResult = fn(...args); if (fnResult instanceof Promise) { diff --git a/projects/demo-app/src/app/home/home.component.ts b/projects/demo-app/src/app/home/home.component.ts index 65f987e..8b2dd8b 100644 --- a/projects/demo-app/src/app/home/home.component.ts +++ b/projects/demo-app/src/app/home/home.component.ts @@ -20,35 +20,39 @@ export class HomeComponent { email: 'email@company.com' }; - this.authService.sdk.password.signUp('piotr+angular@velocit.dev', '!QAZ2wsx', user).subscribe((resp) => { - if (!resp.ok) { - console.log('Failed to sign up via password'); - console.log('Status Code: ' + resp.code); - console.log('Error Code: ' + resp.error?.errorCode); - console.log('Error Description: ' + resp.error?.errorDescription); - console.log('Error Message: ' + resp.error?.errorMessage); - } else { - console.log('Successfully signed up via password'); - console.log(resp); - } - }); + this.authService.sdk.password + .signUp('piotr+angular@velocit.dev', '!QAZ2wsx', user) + .subscribe((resp) => { + if (!resp.ok) { + console.log('Failed to sign up via password'); + console.log('Status Code: ' + resp.code); + console.log('Error Code: ' + resp.error?.errorCode); + console.log('Error Description: ' + resp.error?.errorDescription); + console.log('Error Message: ' + resp.error?.errorMessage); + } else { + console.log('Successfully signed up via password'); + console.log(resp); + } + }); } - - login() { - this.authService.sdk.password.signIn('piotr+angular@velocit.dev', '!QAZ2wsx').subscribe((resp) => { - if (!resp.ok) { - console.log('Failed to sign in via password'); - console.log('Status Code: ' + resp.code); - console.log('Error Code: ' + resp.error?.errorCode); - console.log('Error Description: ' + resp.error?.errorDescription); - console.log('Error Message: ' + resp.error?.errorMessage); - } else { - console.log('Successfully signed in via password'); - console.log(resp); - this.router.navigate(['/protected']).catch((err) => console.error(err)); - } - }); + this.authService.sdk.password + .signIn('piotr+angular@velocit.dev', '!QAZ2wsx') + .subscribe((resp) => { + if (!resp.ok) { + console.log('Failed to sign in via password'); + console.log('Status Code: ' + resp.code); + console.log('Error Code: ' + resp.error?.errorCode); + console.log('Error Description: ' + resp.error?.errorDescription); + console.log('Error Message: ' + resp.error?.errorMessage); + } else { + console.log('Successfully signed in via password'); + console.log(resp); + this.router + .navigate(['/protected']) + .catch((err) => console.error(err)); + } + }); } } From 3c524d79e1a2e52db651f41bc768a32c62c6a73a Mon Sep 17 00:00:00 2001 From: Piotr Bochenek Date: Tue, 24 Oct 2023 13:39:14 +0200 Subject: [PATCH 6/7] fix guard --- projects/angular-sdk/src/lib/descope-auth.guard.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/projects/angular-sdk/src/lib/descope-auth.guard.ts b/projects/angular-sdk/src/lib/descope-auth.guard.ts index 5aadecb..d417b5b 100644 --- a/projects/angular-sdk/src/lib/descope-auth.guard.ts +++ b/projects/angular-sdk/src/lib/descope-auth.guard.ts @@ -9,7 +9,6 @@ export const descopeAuthGuard = (route: ActivatedRouteSnapshot) => { const router = inject(Router); const fallbackUrl = route.data['descopeFallbackUrl']; const isAuthenticated = authService.isAuthenticated(); - return true; if (!isAuthenticated && !!fallbackUrl) { return from(router.navigate([fallbackUrl])); } From 7d089c26b4431a022e4030e9949fa8224883b4f3 Mon Sep 17 00:00:00 2001 From: Piotr Bochenek Date: Tue, 24 Oct 2023 17:55:02 +0200 Subject: [PATCH 7/7] unit tests for observabilify --- .../src/lib/descope-auth.service.ts | 41 +------ projects/angular-sdk/src/lib/helpers.spec.ts | 103 ++++++++++++++++++ projects/angular-sdk/src/lib/helpers.ts | 36 ++++++ 3 files changed, 142 insertions(+), 38 deletions(-) create mode 100644 projects/angular-sdk/src/lib/helpers.spec.ts create mode 100644 projects/angular-sdk/src/lib/helpers.ts diff --git a/projects/angular-sdk/src/lib/descope-auth.service.ts b/projects/angular-sdk/src/lib/descope-auth.service.ts index df6990f..10db982 100644 --- a/projects/angular-sdk/src/lib/descope-auth.service.ts +++ b/projects/angular-sdk/src/lib/descope-auth.service.ts @@ -2,47 +2,12 @@ import { Injectable } from '@angular/core'; import { DescopeAuthConfig } from './descope-auth.module'; import type { UserResponse } from '@descope/web-js-sdk'; import createSdk from '@descope/web-js-sdk'; -import { BehaviorSubject, finalize, from, Observable, tap } from 'rxjs'; +import { BehaviorSubject, finalize, Observable, tap } from 'rxjs'; +import { observabilify, Observablefied } from './helpers'; type DescopeSDK = ReturnType; - -type Observablefied = { - [K in keyof T]: T[K] extends (...args: infer Args) => Promise - ? (...args: Args) => Observable - : T[K] extends (...args: infer Args) => infer R - ? (...args: Args) => R - : T[K] extends object - ? Observablefied - : T[K]; -}; - type AngularDescopeSDK = Observablefied; -function makeObservable(value: T): Observablefied { - /* eslint-disable @typescript-eslint/no-explicit-any */ - const observableValue: any = {}; - - for (const key in value) { - if (typeof value[key] === 'function') { - const fn = value[key] as (...args: unknown[]) => unknown; - observableValue[key] = (...args: unknown[]) => { - const fnResult = fn(...args); - if (fnResult instanceof Promise) { - return from(fnResult); - } else { - return fnResult; - } - }; - } else if (typeof value[key] === 'object' && value[key] !== null) { - observableValue[key] = makeObservable(value[key]); - } else { - observableValue[key] = value[key]; - } - } - - return observableValue as Observablefied; -} - export interface DescopeSession { isAuthenticated: boolean; isSessionLoading: boolean; @@ -70,7 +35,7 @@ export class DescopeAuthService { }; constructor(config: DescopeAuthConfig) { - this.sdk = makeObservable( + this.sdk = observabilify( createSdk({ ...config, persistTokens: true, diff --git a/projects/angular-sdk/src/lib/helpers.spec.ts b/projects/angular-sdk/src/lib/helpers.spec.ts new file mode 100644 index 0000000..c5f5b5e --- /dev/null +++ b/projects/angular-sdk/src/lib/helpers.spec.ts @@ -0,0 +1,103 @@ +import { observabilify, Observablefied } from './helpers'; +import { lastValueFrom, Observable } from 'rxjs'; + +describe('Helpers', () => { + describe('Observabilify', () => { + it('should not affect simple object', () => { + //GIVEN + const obj = { + field1: 'string', + field2: 123, + nested: { + field1: 'string', + field2: 123 + } + }; + type TestType = typeof obj; + + //WHEN + const result: Observablefied = observabilify(obj); + + //THEN + expect(result).toStrictEqual(obj); + }); + + it('should not affect simple object with non async functions', () => { + //GIVEN + const obj = { + field1: 'string', + field2: 123, + fn: (arg1: string, arg2: number): string => { + return arg1 + arg2.toString(); + }, + nested: { + fn2: (arg: string): string => { + return arg; + }, + field1: 'string', + field2: 123 + } + }; + type TestType = typeof obj; + const expected1 = obj.fn('Test', 1); + const expected2 = obj.nested.fn2('Test'); + + //WHEN + const transformed: Observablefied = + observabilify(obj); + const actual1 = transformed.fn('Test', 1); + const actual2 = transformed.nested.fn2('Test'); + + //THEN + expect(expected1).toStrictEqual(actual1); + expect(expected2).toStrictEqual(actual2); + }); + + it('should transform async functions', async () => { + //GIVEN + const obj = { + field1: 'string', + field2: 123, + fn: (arg1: string, arg2: number): string => { + return arg1 + arg2.toString(); + }, + asyncFn: (arg1: string, arg2: number): Promise => { + return Promise.resolve(arg1 + arg2.toString()); + }, + nested: { + fn2: (arg: string) => { + return arg; + }, + asyncFn: (arg: string): Promise => { + return Promise.resolve(arg); + }, + field1: 'string', + field2: 123 + } + }; + type TestType = typeof obj; + const expected1 = obj.fn('Test', 1); + const expected2 = obj.nested.fn2('Test'); + const expected3 = await obj.asyncFn('Test', 1); + const expected4 = await obj.nested.asyncFn('Test'); + + //WHEN + const transformed: Observablefied = + observabilify(obj); + const actual1 = transformed.fn('Test', 1); + const actual2 = transformed.nested.fn2('Test'); + const actual3Async = transformed.asyncFn('Test', 1); + const actual3 = await lastValueFrom(actual3Async); + const actual4Async = transformed.nested.asyncFn('Test'); + const actual4 = await lastValueFrom(actual4Async); + + //THEN + expect(expected1).toStrictEqual(actual1); + expect(expected2).toStrictEqual(actual2); + expect(actual3Async).toBeInstanceOf(Observable); + expect(actual4Async).toBeInstanceOf(Observable); + expect(expected3).toStrictEqual(actual3); + expect(expected4).toStrictEqual(actual4); + }); + }); +}); diff --git a/projects/angular-sdk/src/lib/helpers.ts b/projects/angular-sdk/src/lib/helpers.ts new file mode 100644 index 0000000..e586939 --- /dev/null +++ b/projects/angular-sdk/src/lib/helpers.ts @@ -0,0 +1,36 @@ +import { from, Observable } from 'rxjs'; + +export type Observablefied = { + [K in keyof T]: T[K] extends (...args: infer Args) => Promise + ? (...args: Args) => Observable + : T[K] extends (...args: infer Args) => infer R + ? (...args: Args) => R + : T[K] extends object + ? Observablefied + : T[K]; +}; + +export function observabilify(value: T): Observablefied { + /* eslint-disable @typescript-eslint/no-explicit-any */ + const observableValue: any = {}; + + for (const key in value) { + if (typeof value[key] === 'function') { + const fn = value[key] as (...args: unknown[]) => unknown; + observableValue[key] = (...args: unknown[]) => { + const fnResult = fn(...args); + if (fnResult instanceof Promise) { + return from(fnResult); + } else { + return fnResult; + } + }; + } else if (typeof value[key] === 'object' && value[key] !== null) { + observableValue[key] = observabilify(value[key]); + } else { + observableValue[key] = value[key]; + } + } + + return observableValue as Observablefied; +}