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.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..10db982 100644 --- a/projects/angular-sdk/src/lib/descope-auth.service.ts +++ b/projects/angular-sdk/src/lib/descope-auth.service.ts @@ -1,10 +1,12 @@ 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, Observable, tap } from 'rxjs'; +import { observabilify, Observablefied } from './helpers'; type DescopeSDK = ReturnType; +type AngularDescopeSDK = Observablefied; export interface DescopeSession { isAuthenticated: boolean; @@ -18,18 +20,28 @@ export type DescopeUser = { user: UserResponse; isUserLoading: boolean }; providedIn: 'root' }) export class DescopeAuthService { - private sdk: DescopeSDK; + 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 = createSdk({ - ...config, - persistTokens: true, - autoRefresh: true - }); + this.sdk = observabilify( + createSdk({ + ...config, + persistTokens: true, + autoRefresh: true + }) + ); this.sdk.onSessionTokenChange(this.setSession.bind(this)); this.sdk.onUserChange(this.setUser.bind(this)); this.sessionSubject = new BehaviorSubject({ @@ -40,45 +52,34 @@ export class DescopeAuthService { this.descopeSession$ = this.sessionSubject.asObservable(); this.userSubject = new BehaviorSubject({ isUserLoading: false, - user: { - loginIds: [], - userId: '', - createTime: 0, - TOTP: false, - SAML: false - } + user: this.EMPTY_USER }); 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( + 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({ @@ -95,7 +96,24 @@ export class DescopeAuthService { ...beforeRefreshUser, isUserLoading: true }); - return from(this.sdk.me()).pipe( + 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({ 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; +} 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..8b2dd8b 100644 --- a/projects/demo-app/src/app/home/home.component.ts +++ b/projects/demo-app/src/app/home/home.component.ts @@ -14,33 +14,45 @@ export class HomeComponent { ) {} signUp() { - this.authService.passwordSignUp().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); - } - }); + 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); + 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.passwordLogin().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)); + } + }); } } 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);