diff --git a/README.md b/README.md index 02e698e..7ce2526 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ pnpm add oidc-client-rx @outposts/injection-js @abraham/reflection ```typescript import '@abraham/reflection'; // or 'reflect-metadata' | 'core-js/es7/reflect' import { type Injector, ReflectiveInjector } from '@outposts/injection-js'; -import { LogLevel, OidcSecurityService, provideAuth } from 'oidc-client-rx'; +import { LogLevel, OidcSecurityService, provideAuth, withDefaultFeatures } from 'oidc-client-rx'; const injector = ReflectiveInjector.resolveAndCreate( provideAuth( @@ -43,7 +43,8 @@ const injector = ReflectiveInjector.resolveAndCreate( logLevel: LogLevel.Debug, ... }, - } + }, + withDefaultFeatures() ) ) as Injector; diff --git a/examples/react-tanstack-router/src/index.tsx b/examples/react-tanstack-router/src/index.tsx index d4c4090..e5213ab 100644 --- a/examples/react-tanstack-router/src/index.tsx +++ b/examples/react-tanstack-router/src/index.tsx @@ -1,7 +1,12 @@ import '@abraham/reflection'; // or 'reflect-metadata' | 'core-js/es7/reflect' import { type Injector, ReflectiveInjector } from '@outposts/injection-js'; import { RouterProvider, createRouter } from '@tanstack/react-router'; -import { LogLevel, OidcSecurityService, provideAuth } from 'oidc-client-rx'; +import { + LogLevel, + OidcSecurityService, + provideAuth, + withDefaultFeatures, +} from 'oidc-client-rx'; import { InjectorContextVoidInjector, InjectorProvider, @@ -51,6 +56,11 @@ const injector = ReflectiveInjector.resolveAndCreate( }, }, }, + withDefaultFeatures( + // the after feature will replace the before same type feature + // so the following line can be ignored + { router: { enabled: false } } + ), withTanstackRouter(router) ) ) as Injector; diff --git a/src/adapters/tanstack-router/index.ts b/src/adapters/tanstack-router/index.ts index 9c3e5e6..03a5467 100644 --- a/src/adapters/tanstack-router/index.ts +++ b/src/adapters/tanstack-router/index.ts @@ -1,7 +1,7 @@ import { InjectionToken, inject } from '@outposts/injection-js'; import type { Router } from '@tanstack/react-router'; -import { AbstractRouter } from 'src/router'; -import type { AuthFeature } from '../../provide-auth'; +import type { AuthFeature } from '../../features'; +import { AbstractRouter } from '../../router'; export type TanStackRouter = Router; diff --git a/src/api/data.service.spec.ts b/src/api/data.service.spec.ts index e2d30d5..88ddd5f 100644 --- a/src/api/data.service.spec.ts +++ b/src/api/data.service.spec.ts @@ -1,17 +1,17 @@ import { TestBed } from '@/testing'; import { + type DefaultHttpTestingController, HTTP_CLIENT_TEST_CONTROLLER, provideHttpClientTesting, } from '@/testing/http'; import { HttpHeaders } from '@ngify/http'; -import type { HttpTestingController } from '@ngify/http/testing'; import { ReplaySubject, firstValueFrom, share } from 'rxjs'; import { DataService } from './data.service'; import { HttpBaseService } from './http-base.service'; describe('Data Service', () => { let dataService: DataService; - let httpMock: HttpTestingController; + let httpMock: DefaultHttpTestingController; beforeEach(() => { TestBed.configureTestingModule({ diff --git a/src/api/http-base.service.ts b/src/api/http-base.service.ts index d11d472..83b1fc8 100644 --- a/src/api/http-base.service.ts +++ b/src/api/http-base.service.ts @@ -1,11 +1,10 @@ -import { HttpClient, type HttpHeaders } from '@ngify/http'; import { Injectable, inject } from '@outposts/injection-js'; import type { Observable } from 'rxjs'; -import type { HttpParams } from '../http'; +import { HTTP_CLIENT, type HttpHeaders, type HttpParams } from '../http'; @Injectable() export class HttpBaseService { - private readonly http = inject(HttpClient); + private readonly http = inject(HTTP_CLIENT); get( url: string, @@ -22,7 +21,7 @@ export class HttpBaseService { body: unknown, options: { headers?: HttpHeaders; params?: HttpParams } = {} ): Observable { - return this.http.post(url, body, { + return this.http.post(url, body as any, { ...options, params: options.params?.toNgify(), }); diff --git a/src/auth.module.spec.ts b/src/auth.module.spec.ts deleted file mode 100644 index 954e61f..0000000 --- a/src/auth.module.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { TestBed } from '@/testing'; -import { of } from 'rxjs'; -import { PASSED_CONFIG } from './auth-config'; -import { AuthModule } from './auth.module'; -import { ConfigurationService } from './config/config.service'; -import { - StsConfigHttpLoader, - StsConfigLoader, - StsConfigStaticLoader, -} from './config/loader/config-loader'; -import { mockProvider } from './testing/mock'; - -describe('AuthModule', () => { - describe('APP_CONFIG', () => { - let authModule: AuthModule; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [AuthModule.forRoot({ config: { authority: 'something' } })], - providers: [mockProvider(ConfigurationService)], - }).compileComponents(); - authModule = TestBed.getImportByType(AuthModule); - }); - - it('should create', () => { - expect(AuthModule).toBeDefined(); - expect(authModule).toBeDefined(); - }); - - it('should provide config', () => { - const config = authModule.get(PASSED_CONFIG); - - expect(config).toEqual({ config: { authority: 'something' } }); - }); - - it('should create StsConfigStaticLoader if config is passed', () => { - const configLoader = authModule.get(StsConfigLoader); - - expect(configLoader instanceof StsConfigStaticLoader).toBe(true); - }); - }); - - describe('StsConfigHttpLoader', () => { - let authModule: AuthModule; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ - AuthModule.forRoot({ - loader: { - provide: StsConfigLoader, - useFactory: () => new StsConfigHttpLoader(of({})), - }, - }), - ], - providers: [mockProvider(ConfigurationService)], - }).compileComponents(); - authModule = TestBed.getImportByType(AuthModule); - }); - - it('should create StsConfigStaticLoader if config is passed', () => { - const configLoader = authModule.get(StsConfigLoader); - - expect(configLoader instanceof StsConfigHttpLoader).toBe(true); - }); - }); -}); diff --git a/src/auth.module.ts b/src/auth.module.ts deleted file mode 100644 index cf06597..0000000 --- a/src/auth.module.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { - type InjectionToken, - Injector, - ReflectiveInjector, - type Type, -} from '@outposts/injection-js'; -import type { PassedInitialConfig } from './auth-config'; -import type { Module } from './injection'; -import { _provideAuth } from './provide-auth'; - -export interface AuthModuleOptions { - passedConfig: PassedInitialConfig; - parentInjector?: ReflectiveInjector; -} - -export class AuthModule extends Injector { - passedConfig: PassedInitialConfig; - injector: ReflectiveInjector; - parentInjector?: Injector; - - constructor(passedConfig?: PassedInitialConfig, parentInjector?: Injector) { - super(); - this.passedConfig = passedConfig ?? {}; - this.parentInjector = parentInjector; - this.injector = ReflectiveInjector.resolveAndCreate( - [..._provideAuth(this.passedConfig)], - this.parentInjector - ); - } - - static forRoot(passedConfig?: PassedInitialConfig): Module { - return (parentInjector?: Injector) => - new AuthModule(passedConfig, parentInjector); - } - - get(token: Type | InjectionToken, notFoundValue?: T): T; - get(token: any, notFoundValue?: any): any; - get(token: unknown, notFoundValue?: unknown): any { - return this.injector.get(token, notFoundValue); - } -} diff --git a/src/features.ts b/src/features.ts new file mode 100644 index 0000000..1bb4fc3 --- /dev/null +++ b/src/features.ts @@ -0,0 +1,131 @@ +import type { HttpFeature } from '@ngify/http'; +import type { Provider } from '@outposts/injection-js'; +import { DOCUMENT } from './dom'; +import { provideHttpClient } from './http'; +import { + AbstractRouter, + VanillaHistoryRouter, + VanillaLocationRouter, +} from './router'; +import { AbstractSecurityStorage } from './storage/abstract-security-storage'; +import { DefaultLocalStorageService } from './storage/default-localstorage.service'; +import { DefaultSessionStorageService } from './storage/default-sessionstorage.service'; +import { PLATFORM_ID } from './utils/platform-provider/platform.provider'; + +/** + * A feature to be used with `provideAuth`. + */ +export interface AuthFeature { + ɵproviders: Provider[]; +} + +export interface BrowserPlatformFeatureOptions { + enabled?: boolean; +} + +export function withBrowserPlatform({ + enabled = true, +}: BrowserPlatformFeatureOptions = {}): AuthFeature { + return { + ɵproviders: enabled + ? [ + { + provide: DOCUMENT, + useFactory: () => document, + }, + { + provide: PLATFORM_ID, + useValue: 'browser', + }, + ] + : [], + }; +} + +export interface HttpClientFeatureOptions { + enabled?: boolean; + features?: HttpFeature[]; +} + +export function withHttpClient({ + features, + enabled = true, +}: HttpClientFeatureOptions = {}): AuthFeature { + return { + ɵproviders: enabled ? provideHttpClient(features) : [], + }; +} + +export type SecurityStorageType = 'session-storage' | 'local-storage'; + +export interface SecurityStorageFeatureOptions { + enabled?: boolean; + type?: SecurityStorageType; +} + +export function withSecurityStorage({ + enabled = true, + type = 'session-storage', +}: SecurityStorageFeatureOptions = {}): AuthFeature { + return { + ɵproviders: enabled + ? [ + type === 'session-storage' + ? { + provide: AbstractSecurityStorage, + useClass: DefaultLocalStorageService, + } + : { + provide: AbstractSecurityStorage, + useClass: DefaultSessionStorageService, + }, + ] + : [], + }; +} + +export type VanillaRouterType = 'location' | 'history'; + +export interface VanillaRouterFeatureOptions { + enabled?: boolean; + type?: VanillaRouterType; +} + +export function withVanillaRouter({ + enabled = true, + type = 'history', +}: VanillaRouterFeatureOptions = {}): AuthFeature { + return { + ɵproviders: enabled + ? [ + type === 'location' + ? { + provide: AbstractRouter, + useClass: VanillaLocationRouter, + } + : { + provide: AbstractRouter, + useClass: VanillaHistoryRouter, + }, + ] + : [], + }; +} + +export interface DefaultFeaturesOptions { + browserPlatform?: BrowserPlatformFeatureOptions; + securityStorage?: SecurityStorageFeatureOptions; + router?: VanillaRouterFeatureOptions; + httpClient?: HttpClientFeatureOptions; +} + +export function withDefaultFeatures( + options: DefaultFeaturesOptions = {} +): AuthFeature[] { + return [ + withBrowserPlatform(options.browserPlatform), + withSecurityStorage(options.securityStorage), + withHttpClient(options.httpClient), + withVanillaRouter(options.router), + ].filter(Boolean) as AuthFeature[]; +} diff --git a/src/http/index.ts b/src/http/index.ts index f945cbe..8e48418 100644 --- a/src/http/index.ts +++ b/src/http/index.ts @@ -1,10 +1,15 @@ import { + // biome-ignore lint/nursery/noExportedImports: + HttpClient as DefaultHttpClient, type HttpBackend, - HttpClient, type HttpFeature, HttpFeatureKind, + // biome-ignore lint/nursery/noExportedImports: + HttpHeaders, type HttpInterceptor, type HttpInterceptorFn, + type HttpRequest, + type HttpParams as NgifyHttpParams, withInterceptors, withLegacyInterceptors, } from '@ngify/http'; @@ -13,8 +18,12 @@ import { Optional, type Provider, } from '@outposts/injection-js'; +import type { Observable } from 'rxjs'; import type { ArrayOrNullableOne } from '../utils/types'; -export { HttpParams, type HttpParamsOptions } from './params'; +// biome-ignore lint/nursery/noExportedImports: +import { HttpParams, type HttpParamsOptions } from './params'; + +export { HttpParams, HttpHeaders, type HttpParamsOptions, DefaultHttpClient }; export const HTTP_FEATURES = new InjectionToken('HTTP_FEATURES'); @@ -112,14 +121,29 @@ export function provideHttpClient(features: HttpFeature[] = []): Provider[] { multi: true, }, { - provide: HttpClient, + provide: HTTP_CLIENT, useFactory: (features: ArrayOrNullableOne[]) => { const normalizedFeatures = [features] .flat(Number.MAX_SAFE_INTEGER) .filter(Boolean) as HttpFeature[]; - return new HttpClient(...normalizedFeatures); + return new DefaultHttpClient(...normalizedFeatures); }, deps: [HTTP_FEATURES], }, ]; } + +export type HttpClient = { + get( + url: string, + options?: { headers?: HttpHeaders; params?: NgifyHttpParams } + ): Observable; + + post( + url: string, + body?: HttpRequest['body'], + options?: { headers?: HttpHeaders; params?: NgifyHttpParams } + ): Observable; +}; + +export const HTTP_CLIENT = new InjectionToken('HTTP_CLIENT'); diff --git a/src/index.ts b/src/index.ts index c93397b..05022d9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,6 @@ export type { PassedInitialConfig } from './auth-config'; export * from './auth-options'; export * from './auth-state/auth-result'; export * from './auth-state/auth-state'; -export * from './auth.module'; export * from './auto-login/auto-login-partial-routes.guard'; export * from './config/auth-well-known/auth-well-known-endpoints'; export * from './config/config.service'; @@ -31,3 +30,4 @@ export * from './validation/validation-result'; export * from './injection'; export * from './router'; export * from './http'; +export * from './features'; diff --git a/src/interceptor/auth.interceptor.spec.ts b/src/interceptor/auth.interceptor.spec.ts index 1f25c88..b545d6b 100644 --- a/src/interceptor/auth.interceptor.spec.ts +++ b/src/interceptor/auth.interceptor.spec.ts @@ -1,11 +1,15 @@ -import { TestBed } from '@/testing'; import { + type DefaultHttpTestingController, HTTP_CLIENT_TEST_CONTROLLER, + TestBed, provideHttpClientTesting, } from '@/testing'; -import { HttpClient } from '@ngify/http'; -import type { HttpTestingController } from '@ngify/http/testing'; -import { HTTP_INTERCEPTOR_FNS, HTTP_LEGACY_INTERCEPTORS } from 'oidc-client-rx'; +import { + type DefaultHttpClient, + HTTP_CLIENT, + HTTP_INTERCEPTOR_FNS, + HTTP_LEGACY_INTERCEPTORS, +} from 'oidc-client-rx'; import { ReplaySubject, firstValueFrom, share } from 'rxjs'; import { vi } from 'vitest'; import { AuthStateService } from '../auth-state/auth-state.service'; @@ -16,9 +20,9 @@ import { AuthInterceptor, authInterceptor } from './auth.interceptor'; import { ClosestMatchingRouteService } from './closest-matching-route.service'; describe('AuthHttpInterceptor', () => { - let httpTestingController: HttpTestingController; + let httpTestingController: DefaultHttpTestingController; let configurationService: ConfigurationService; - let httpClient: HttpClient; + let httpClient: DefaultHttpClient; let authStateService: AuthStateService; let closestMatchingRouteService: ClosestMatchingRouteService; @@ -40,7 +44,7 @@ describe('AuthHttpInterceptor', () => { ], }); - httpClient = TestBed.inject(HttpClient); + httpClient = TestBed.inject(HTTP_CLIENT) as DefaultHttpClient; httpTestingController = TestBed.inject(HTTP_CLIENT_TEST_CONTROLLER); configurationService = TestBed.inject(ConfigurationService); authStateService = TestBed.inject(AuthStateService); @@ -72,7 +76,7 @@ describe('AuthHttpInterceptor', () => { ], }); - httpClient = TestBed.inject(HttpClient); + httpClient = TestBed.inject(HTTP_CLIENT) as DefaultHttpClient; httpTestingController = TestBed.inject(HTTP_CLIENT_TEST_CONTROLLER); configurationService = TestBed.inject(ConfigurationService); authStateService = TestBed.inject(AuthStateService); diff --git a/src/provide-auth.ts b/src/provide-auth.ts index 9ebd4e7..ffd0890 100644 --- a/src/provide-auth.ts +++ b/src/provide-auth.ts @@ -1,4 +1,3 @@ -import { HttpClient } from '@ngify/http'; import type { Provider } from '@outposts/injection-js'; import { DataService } from './api/data.service'; import { HttpBaseService } from './api/http-base.service'; @@ -24,6 +23,7 @@ import { StsConfigLoader } from './config/loader/config-loader'; import { ConfigValidationService } from './config/validation/config-validation.service'; import { DOCUMENT } from './dom'; import { JwkExtractor } from './extractors/jwk.extractor'; +import type { AuthFeature } from './features'; import { CodeFlowCallbackHandlerService } from './flows/callback-handling/code-flow-callback-handler.service'; import { HistoryJwtKeysCallbackHandlerService } from './flows/callback-handling/history-jwt-keys-callback-handler.service'; import { ImplicitFlowCallbackHandlerService } from './flows/callback-handling/implicit-flow-callback-handler.service'; @@ -62,10 +62,7 @@ import { UserService } from './user-data/user.service'; import { CryptoService } from './utils/crypto/crypto.service'; import { EqualityService } from './utils/equality/equality.service'; import { FlowHelper } from './utils/flowHelper/flow-helper.service'; -import { - PLATFORM_ID, - PlatformProvider, -} from './utils/platform-provider/platform.provider'; +import { PlatformProvider } from './utils/platform-provider/platform.provider'; import { RedirectService } from './utils/redirect/redirect.service'; import { TokenHelperService } from './utils/tokenHelper/token-helper.service'; import { CurrentUrlService } from './utils/url/current-url.service'; @@ -75,20 +72,17 @@ import { JwtWindowCryptoService } from './validation/jwt-window-crypto.service'; import { StateValidationService } from './validation/state-validation.service'; import { TokenValidationService } from './validation/token-validation.service'; -/** - * A feature to be used with `provideAuth`. - */ -export interface AuthFeature { - ɵproviders: Provider[]; -} - export function provideAuth( passedConfig: PassedInitialConfig, - ...features: AuthFeature[] + ...features: (AuthFeature | AuthFeature[])[] ): Provider[] { const providers = _provideAuth(passedConfig); - for (const feature of features) { + const normailizedFeatures = features + .flat(Number.MAX_SAFE_INTEGER) + .filter(Boolean) as AuthFeature[]; + + for (const feature of normailizedFeatures) { providers.push(...feature.ɵproviders); } @@ -97,15 +91,6 @@ export function provideAuth( export function _provideAuth(passedConfig: PassedInitialConfig): Provider[] { return [ - { - provide: DOCUMENT, - useFactory: () => document, - }, - HttpClient, - { - provide: PLATFORM_ID, - useValue: 'browser', - }, // Make the PASSED_CONFIG available through injection { provide: PASSED_CONFIG, useValue: passedConfig }, // Create the loader: Either the one getting passed or a static one diff --git a/src/router/index.ts b/src/router/index.ts index a6f46b1..b1435b9 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,3 +1,6 @@ +import { inject } from '@outposts/injection-js'; +import { DOCUMENT } from 'src/dom'; + export type RouteData = { [key: string | symbol]: any; }; @@ -26,3 +29,55 @@ export abstract class AbstractRouter< abstract getCurrentNavigation(): NAVIGATION; } + +export class VanillaLocationRouter extends AbstractRouter { + private document = inject(DOCUMENT); + + private get location(): Location { + const location = this.document.defaultView?.window?.location; + if (!location) { + throw new Error('current document do not support Location API'); + } + return location; + } + + navigateByUrl(url: string): void { + this.location.href = url; + } + + getCurrentNavigation() { + return { + extractedUrl: `${this.location.pathname}${this.location.search}${this.location.hash}`, + }; + } +} + +export class VanillaHistoryRouter extends AbstractRouter { + private document = inject(DOCUMENT); + + private get history(): History { + const history = this.document.defaultView?.window?.history; + if (!history) { + throw new Error('current document do not support History API'); + } + return history; + } + + private get location(): Location { + const location = this.document.defaultView?.window?.location; + if (!location) { + throw new Error('current document do not support Location API'); + } + return location; + } + + navigateByUrl(url: string): void { + this.history.pushState({}, '', url); + } + + getCurrentNavigation() { + return { + extractedUrl: `${this.location.pathname}${this.location.search}${this.location.hash}`, + }; + } +} diff --git a/src/testing/http.ts b/src/testing/http.ts index 724ae92..5a3c455 100644 --- a/src/testing/http.ts +++ b/src/testing/http.ts @@ -2,6 +2,8 @@ import { HttpClientTestingBackend } from '@ngify/http/testing'; import { InjectionToken, type Provider } from '@outposts/injection-js'; import { HTTP_BACKEND, provideHttpClient } from 'oidc-client-rx'; +export { HttpTestingController as DefaultHttpTestingController } from '@ngify/http/testing'; + export const HTTP_CLIENT_TEST_CONTROLLER = new InjectionToken('HTTP_CLIENT_TEST_CONTROLLER'); diff --git a/src/testing/index.ts b/src/testing/index.ts index c458acb..71b74a6 100644 --- a/src/testing/index.ts +++ b/src/testing/index.ts @@ -6,4 +6,8 @@ export { } from './spy'; export { createRetriableStream } from './create-retriable-stream.helper'; export { MockRouter, mockRouterProvider } from './router'; -export { provideHttpClientTesting, HTTP_CLIENT_TEST_CONTROLLER } from './http'; +export { + provideHttpClientTesting, + HTTP_CLIENT_TEST_CONTROLLER, + DefaultHttpTestingController, +} from './http';