1
0
mirror of https://github.com/bitwarden/directory-connector synced 2025-12-05 23:53:21 +00:00

Add SafeProvider (#582)

This commit is contained in:
Thomas Rittson
2024-09-06 08:11:57 +10:00
committed by GitHub
parent 6d355812e0
commit dd10538d0f
5 changed files with 249 additions and 69 deletions

1
.gitignore vendored
View File

@@ -26,6 +26,7 @@ npm-debug.log
# Build directories # Build directories
dist dist
build build
build-cli
.angular/cache .angular/cache
# Testing # Testing

View File

@@ -38,6 +38,9 @@ import { StateMigrationService } from "@/jslib/common/src/services/stateMigratio
import { TokenService } from "@/jslib/common/src/services/token.service"; import { TokenService } from "@/jslib/common/src/services/token.service";
import { TwoFactorService } from "@/jslib/common/src/services/twoFactor.service"; import { TwoFactorService } from "@/jslib/common/src/services/twoFactor.service";
import { SafeInjectionToken, SECURE_STORAGE, WINDOW } from '../../../../src/app/services/injection-tokens';
import { SafeProvider, safeProvider } from '../../../../src/app/services/safe-provider';
import { BroadcasterService } from "./broadcaster.service"; import { BroadcasterService } from "./broadcaster.service";
import { ModalService } from "./modal.service"; import { ModalService } from "./modal.service";
import { ValidationService } from "./validation.service"; import { ValidationService } from "./validation.service";
@@ -45,20 +48,20 @@ import { ValidationService } from "./validation.service";
@NgModule({ @NgModule({
declarations: [], declarations: [],
providers: [ providers: [
{ provide: "WINDOW", useValue: window }, safeProvider({ provide: WINDOW, useValue: window }),
{ safeProvider({
provide: LOCALE_ID, provide: LOCALE_ID as SafeInjectionToken<string>,
useFactory: (i18nService: I18nServiceAbstraction) => i18nService.translationLocale, useFactory: (i18nService: I18nServiceAbstraction) => i18nService.translationLocale,
deps: [I18nServiceAbstraction], deps: [I18nServiceAbstraction],
}, }),
ValidationService, safeProvider(ValidationService),
ModalService, safeProvider(ModalService),
{ safeProvider({
provide: AppIdServiceAbstraction, provide: AppIdServiceAbstraction,
useClass: AppIdService, useClass: AppIdService,
deps: [StorageServiceAbstraction], deps: [StorageServiceAbstraction],
}, }),
{ safeProvider({
provide: AuthServiceAbstraction, provide: AuthServiceAbstraction,
useClass: AuthService, useClass: AuthService,
deps: [ deps: [
@@ -75,15 +78,15 @@ import { ValidationService } from "./validation.service";
TwoFactorServiceAbstraction, TwoFactorServiceAbstraction,
I18nServiceAbstraction, I18nServiceAbstraction,
], ],
}, }),
{ provide: LogService, useFactory: () => new ConsoleLogService(false) }, safeProvider({ provide: LogService, useFactory: () => new ConsoleLogService(false), deps: [] }),
{ safeProvider({
provide: EnvironmentServiceAbstraction, provide: EnvironmentServiceAbstraction,
useClass: EnvironmentService, useClass: EnvironmentService,
deps: [StateServiceAbstraction], deps: [StateServiceAbstraction],
}, }),
{ provide: TokenServiceAbstraction, useClass: TokenService, deps: [StateServiceAbstraction] }, safeProvider({ provide: TokenServiceAbstraction, useClass: TokenService, deps: [StateServiceAbstraction] }),
{ safeProvider({
provide: CryptoServiceAbstraction, provide: CryptoServiceAbstraction,
useClass: CryptoService, useClass: CryptoService,
deps: [ deps: [
@@ -92,13 +95,13 @@ import { ValidationService } from "./validation.service";
LogService, LogService,
StateServiceAbstraction, StateServiceAbstraction,
], ],
}, }),
{ safeProvider({
provide: PasswordGenerationServiceAbstraction, provide: PasswordGenerationServiceAbstraction,
useClass: PasswordGenerationService, useClass: PasswordGenerationService,
deps: [CryptoServiceAbstraction, PolicyServiceAbstraction, StateServiceAbstraction], deps: [CryptoServiceAbstraction, PolicyServiceAbstraction, StateServiceAbstraction],
}, }),
{ safeProvider({
provide: ApiServiceAbstraction, provide: ApiServiceAbstraction,
useFactory: ( useFactory: (
tokenService: TokenServiceAbstraction, tokenService: TokenServiceAbstraction,
@@ -121,9 +124,9 @@ import { ValidationService } from "./validation.service";
MessagingServiceAbstraction, MessagingServiceAbstraction,
AppIdServiceAbstraction, AppIdServiceAbstraction,
], ],
}, }),
{ provide: BroadcasterServiceAbstraction, useClass: BroadcasterService }, safeProvider({ provide: BroadcasterServiceAbstraction, useClass: BroadcasterService, useAngularDecorators: true }),
{ safeProvider({
provide: StateServiceAbstraction, provide: StateServiceAbstraction,
useFactory: ( useFactory: (
storageService: StorageServiceAbstraction, storageService: StorageServiceAbstraction,
@@ -140,12 +143,12 @@ import { ValidationService } from "./validation.service";
), ),
deps: [ deps: [
StorageServiceAbstraction, StorageServiceAbstraction,
"SECURE_STORAGE", SECURE_STORAGE,
LogService, LogService,
StateMigrationServiceAbstraction, StateMigrationServiceAbstraction,
], ],
}, }),
{ safeProvider({
provide: StateMigrationServiceAbstraction, provide: StateMigrationServiceAbstraction,
useFactory: ( useFactory: (
storageService: StorageServiceAbstraction, storageService: StorageServiceAbstraction,
@@ -156,14 +159,14 @@ import { ValidationService } from "./validation.service";
secureStorageService, secureStorageService,
new StateFactory(GlobalState, Account), new StateFactory(GlobalState, Account),
), ),
deps: [StorageServiceAbstraction, "SECURE_STORAGE"], deps: [StorageServiceAbstraction, SECURE_STORAGE],
}, }),
{ safeProvider({
provide: PolicyServiceAbstraction, provide: PolicyServiceAbstraction,
useClass: PolicyService, useClass: PolicyService,
deps: [StateServiceAbstraction, OrganizationServiceAbstraction, ApiServiceAbstraction], deps: [StateServiceAbstraction, OrganizationServiceAbstraction, ApiServiceAbstraction],
}, }),
{ safeProvider({
provide: KeyConnectorServiceAbstraction, provide: KeyConnectorServiceAbstraction,
useClass: KeyConnectorService, useClass: KeyConnectorService,
deps: [ deps: [
@@ -175,17 +178,17 @@ import { ValidationService } from "./validation.service";
OrganizationServiceAbstraction, OrganizationServiceAbstraction,
CryptoFunctionServiceAbstraction, CryptoFunctionServiceAbstraction,
], ],
}, }),
{ safeProvider({
provide: OrganizationServiceAbstraction, provide: OrganizationServiceAbstraction,
useClass: OrganizationService, useClass: OrganizationService,
deps: [StateServiceAbstraction], deps: [StateServiceAbstraction],
}, }),
{ safeProvider({
provide: TwoFactorServiceAbstraction, provide: TwoFactorServiceAbstraction,
useClass: TwoFactorService, useClass: TwoFactorService,
deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction], deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction],
}, }),
], ] satisfies SafeProvider[],
}) })
export class JslibServicesModule {} export class JslibServicesModule {}

View File

@@ -0,0 +1,17 @@
import { InjectionToken } from "@angular/core";
import { StorageService } from "../../../jslib/common/src/abstractions/storage.service";
declare const tag: unique symbol;
/**
* A (more) typesafe version of InjectionToken which will more strictly enforce the generic type parameter.
* @remarks The default angular implementation does not use the generic type to define the structure of the object,
* so the structural type system will not complain about a mismatch in the type parameter.
* This is solved by assigning T to an arbitrary private property.
*/
export class SafeInjectionToken<T> extends InjectionToken<T> {
private readonly [tag]: T;
}
export const SECURE_STORAGE = new SafeInjectionToken<StorageService>("SECURE_STORAGE");
export const WINDOW = new SafeInjectionToken<Window>("WINDOW");

View File

@@ -0,0 +1,144 @@
import { Provider } from "@angular/core";
import { Constructor, Opaque } from "type-fest";
import { SafeInjectionToken } from "./injection-tokens";
// ******
// NOTE: this is a copy/paste of safe-provider.ts from the clients repository.
// The clients repository remains the primary version of this code.
// Make any changes there and copy it back to this repository.
// ******
/**
* The return type of the {@link safeProvider} helper function.
* Used to distinguish a type safe provider definition from a non-type safe provider definition.
*/
export type SafeProvider = Opaque<Provider>;
// TODO: type-fest also provides a type like this when we upgrade >= 3.7.0
type AbstractConstructor<T> = abstract new (...args: any) => T;
type MapParametersToDeps<T> = {
[K in keyof T]: AbstractConstructor<T[K]> | SafeInjectionToken<T[K]>;
};
type SafeInjectionTokenType<T> = T extends SafeInjectionToken<infer J> ? J : never;
/**
* Gets the instance type from a constructor, abstract constructor, or SafeInjectionToken
*/
type ProviderInstanceType<T> =
T extends SafeInjectionToken<any>
? InstanceType<SafeInjectionTokenType<T>>
: T extends Constructor<any> | AbstractConstructor<any>
? InstanceType<T>
: never;
/**
* Represents a dependency provided with the useClass option.
*/
type SafeClassProvider<
A extends AbstractConstructor<any> | SafeInjectionToken<any>,
I extends Constructor<ProviderInstanceType<A>>,
D extends MapParametersToDeps<ConstructorParameters<I>>,
> = {
provide: A;
useClass: I;
deps: D;
};
/**
* Represents a dependency provided with the useValue option.
*/
type SafeValueProvider<A extends SafeInjectionToken<any>, V extends SafeInjectionTokenType<A>> = {
provide: A;
useValue: V;
};
/**
* Represents a dependency provided with the useFactory option.
*/
type SafeFactoryProvider<
A extends AbstractConstructor<any> | SafeInjectionToken<any>,
I extends (...args: any) => ProviderInstanceType<A>,
D extends MapParametersToDeps<Parameters<I>>,
> = {
provide: A;
useFactory: I;
deps: D;
multi?: boolean;
};
/**
* Represents a dependency provided with the useExisting option.
*/
type SafeExistingProvider<
A extends Constructor<any> | AbstractConstructor<any> | SafeInjectionToken<any>,
I extends Constructor<ProviderInstanceType<A>> | AbstractConstructor<ProviderInstanceType<A>>,
> = {
provide: A;
useExisting: I;
};
/**
* Represents a dependency where there is no abstract token, the token is the implementation
*/
type SafeConcreteProvider<
I extends Constructor<any>,
D extends MapParametersToDeps<ConstructorParameters<I>>,
> = {
provide: I;
deps: D;
};
/**
* If useAngularDecorators: true is specified, do not require a deps array.
* This is a manual override for where @Injectable decorators are used
*/
type UseAngularDecorators<T extends { deps: any }> = Omit<T, "deps"> & {
useAngularDecorators: true;
};
/**
* Represents a type with a deps array that may optionally be overridden with useAngularDecorators
*/
type AllowAngularDecorators<T extends { deps: any }> = T | UseAngularDecorators<T>;
/**
* A factory function that creates a provider for the ngModule providers array.
* This (almost) guarantees type safety for your provider definition. It does nothing at runtime.
* Warning: the useAngularDecorators option provides an override where your class uses the Injectable decorator,
* however this cannot be enforced by the type system and will not cause an error if the decorator is not used.
* @example safeProvider({ provide: MyService, useClass: DefaultMyService, deps: [AnotherService] })
* @param provider Your provider object in the usual shape (e.g. using useClass, useValue, useFactory, etc.)
* @returns The exact same object without modification (pass-through).
*/
export const safeProvider = <
// types for useClass
AClass extends AbstractConstructor<any> | SafeInjectionToken<any>,
IClass extends Constructor<ProviderInstanceType<AClass>>,
DClass extends MapParametersToDeps<ConstructorParameters<IClass>>,
// types for useValue
AValue extends SafeInjectionToken<any>,
VValue extends SafeInjectionTokenType<AValue>,
// types for useFactory
AFactory extends AbstractConstructor<any> | SafeInjectionToken<any>,
IFactory extends (...args: any) => ProviderInstanceType<AFactory>,
DFactory extends MapParametersToDeps<Parameters<IFactory>>,
// types for useExisting
AExisting extends Constructor<any> | AbstractConstructor<any> | SafeInjectionToken<any>,
IExisting extends
| Constructor<ProviderInstanceType<AExisting>>
| AbstractConstructor<ProviderInstanceType<AExisting>>,
// types for no token
IConcrete extends Constructor<any>,
DConcrete extends MapParametersToDeps<ConstructorParameters<IConcrete>>,
>(
provider:
| AllowAngularDecorators<SafeClassProvider<AClass, IClass, DClass>>
| SafeValueProvider<AValue, VValue>
| AllowAngularDecorators<SafeFactoryProvider<AFactory, IFactory, DFactory>>
| SafeExistingProvider<AExisting, IExisting>
| AllowAngularDecorators<SafeConcreteProvider<IConcrete, DConcrete>>
| Constructor<unknown>,
): SafeProvider => provider as SafeProvider;

View File

@@ -38,11 +38,13 @@ import { StateMigrationService } from "../../services/stateMigration.service";
import { SyncService } from "../../services/sync.service"; import { SyncService } from "../../services/sync.service";
import { AuthGuardService } from "./auth-guard.service"; import { AuthGuardService } from "./auth-guard.service";
import { SafeInjectionToken, SECURE_STORAGE, WINDOW } from "./injection-tokens";
import { LaunchGuardService } from "./launch-guard.service"; import { LaunchGuardService } from "./launch-guard.service";
import { SafeProvider, safeProvider } from "./safe-provider";
export function initFactory( export function initFactory(
environmentService: EnvironmentServiceAbstraction, environmentService: EnvironmentServiceAbstraction,
i18nService: I18nService, i18nService: I18nServiceAbstraction,
platformUtilsService: PlatformUtilsServiceAbstraction, platformUtilsService: PlatformUtilsServiceAbstraction,
stateService: StateServiceAbstraction, stateService: StateServiceAbstraction,
cryptoService: CryptoServiceAbstraction, cryptoService: CryptoServiceAbstraction,
@@ -50,7 +52,7 @@ export function initFactory(
return async () => { return async () => {
await stateService.init(); await stateService.init();
await environmentService.setUrlsFromStorage(); await environmentService.setUrlsFromStorage();
await i18nService.init(); await (i18nService as I18nService).init();
const htmlEl = window.document.documentElement; const htmlEl = window.document.documentElement;
htmlEl.classList.add("os_" + platformUtilsService.getDeviceString()); htmlEl.classList.add("os_" + platformUtilsService.getDeviceString());
htmlEl.classList.add("locale_" + i18nService.translationLocale); htmlEl.classList.add("locale_" + i18nService.translationLocale);
@@ -78,8 +80,8 @@ export function initFactory(
imports: [JslibServicesModule], imports: [JslibServicesModule],
declarations: [], declarations: [],
providers: [ providers: [
{ safeProvider({
provide: APP_INITIALIZER, provide: APP_INITIALIZER as SafeInjectionToken<() => void>,
useFactory: initFactory, useFactory: initFactory,
deps: [ deps: [
EnvironmentServiceAbstraction, EnvironmentServiceAbstraction,
@@ -89,21 +91,29 @@ export function initFactory(
CryptoServiceAbstraction, CryptoServiceAbstraction,
], ],
multi: true, multi: true,
}, }),
{ provide: LogServiceAbstraction, useClass: ElectronLogService, deps: [] }, safeProvider({ provide: LogServiceAbstraction, useClass: ElectronLogService, deps: [] }),
{ safeProvider({
provide: I18nServiceAbstraction, provide: I18nServiceAbstraction,
useFactory: (window: Window) => new I18nService(window.navigator.language, "./locales"), useFactory: (window: Window) => new I18nService(window.navigator.language, "./locales"),
deps: ["WINDOW"], deps: [WINDOW],
}, }),
{ safeProvider({
provide: MessagingServiceAbstraction, provide: MessagingServiceAbstraction,
useClass: ElectronRendererMessagingService, useClass: ElectronRendererMessagingService,
deps: [BroadcasterServiceAbstraction], deps: [BroadcasterServiceAbstraction],
}, }),
{ provide: StorageServiceAbstraction, useClass: ElectronRendererStorageService }, safeProvider({
{ provide: "SECURE_STORAGE", useClass: ElectronRendererSecureStorageService }, provide: StorageServiceAbstraction,
{ useClass: ElectronRendererStorageService,
deps: [],
}),
safeProvider({
provide: SECURE_STORAGE,
useClass: ElectronRendererSecureStorageService,
deps: [],
}),
safeProvider({
provide: PlatformUtilsServiceAbstraction, provide: PlatformUtilsServiceAbstraction,
useFactory: ( useFactory: (
i18nService: I18nServiceAbstraction, i18nService: I18nServiceAbstraction,
@@ -111,9 +121,13 @@ export function initFactory(
stateService: StateServiceAbstraction, stateService: StateServiceAbstraction,
) => new ElectronPlatformUtilsService(i18nService, messagingService, false, stateService), ) => new ElectronPlatformUtilsService(i18nService, messagingService, false, stateService),
deps: [I18nServiceAbstraction, MessagingServiceAbstraction, StateServiceAbstraction], deps: [I18nServiceAbstraction, MessagingServiceAbstraction, StateServiceAbstraction],
}, }),
{ provide: CryptoFunctionServiceAbstraction, useClass: NodeCryptoFunctionService, deps: [] }, safeProvider({
{ provide: CryptoFunctionServiceAbstraction,
useClass: NodeCryptoFunctionService,
deps: [],
}),
safeProvider({
provide: ApiServiceAbstraction, provide: ApiServiceAbstraction,
useFactory: ( useFactory: (
tokenService: TokenServiceAbstraction, tokenService: TokenServiceAbstraction,
@@ -141,8 +155,8 @@ export function initFactory(
MessagingServiceAbstraction, MessagingServiceAbstraction,
AppIdServiceAbstraction, AppIdServiceAbstraction,
], ],
}, }),
{ safeProvider({
provide: AuthServiceAbstraction, provide: AuthServiceAbstraction,
useClass: AuthService, useClass: AuthService,
deps: [ deps: [
@@ -159,8 +173,8 @@ export function initFactory(
TwoFactorServiceAbstraction, TwoFactorServiceAbstraction,
I18nServiceAbstraction, I18nServiceAbstraction,
], ],
}, }),
{ safeProvider({
provide: SyncService, provide: SyncService,
useClass: SyncService, useClass: SyncService,
deps: [ deps: [
@@ -172,10 +186,10 @@ export function initFactory(
EnvironmentServiceAbstraction, EnvironmentServiceAbstraction,
StateServiceAbstraction, StateServiceAbstraction,
], ],
}, }),
AuthGuardService, safeProvider(AuthGuardService),
LaunchGuardService, safeProvider(LaunchGuardService),
{ safeProvider({
provide: StateMigrationServiceAbstraction, provide: StateMigrationServiceAbstraction,
useFactory: ( useFactory: (
storageService: StorageServiceAbstraction, storageService: StorageServiceAbstraction,
@@ -186,9 +200,9 @@ export function initFactory(
secureStorageService, secureStorageService,
new StateFactory(GlobalState, Account), new StateFactory(GlobalState, Account),
), ),
deps: [StorageServiceAbstraction, "SECURE_STORAGE"], deps: [StorageServiceAbstraction, SECURE_STORAGE],
}, }),
{ safeProvider({
provide: StateServiceAbstraction, provide: StateServiceAbstraction,
useFactory: ( useFactory: (
storageService: StorageServiceAbstraction, storageService: StorageServiceAbstraction,
@@ -206,15 +220,16 @@ export function initFactory(
), ),
deps: [ deps: [
StorageServiceAbstraction, StorageServiceAbstraction,
"SECURE_STORAGE", SECURE_STORAGE,
LogServiceAbstraction, LogServiceAbstraction,
StateMigrationServiceAbstraction, StateMigrationServiceAbstraction,
], ],
}, }),
{ safeProvider({
provide: TwoFactorServiceAbstraction, provide: TwoFactorServiceAbstraction,
useClass: NoopTwoFactorService, useClass: NoopTwoFactorService,
}, deps: [],
], }),
] satisfies SafeProvider[],
}) })
export class ServicesModule {} export class ServicesModule {}