SM-365: Add SM Import & Export (#4323)

* SM-365: Add scaffolding for settings, import, and export components

* SM-365: Build out SM export component and retrieve org name

* Add password verification

* Add SMExportService

* SM-365: Add full export functionality for client side

* SM-365: Add SM Import UI, combine import & export services, general cleanup

* SM-365: Small updates, fix settings navigation for SM

* SM-365: Refactorings based on PR comments, part 1

* SM-365: Refactorings based on PR comments, part 2

* SM-365: remove unneeded import file parsing code

* Attempt New SM Export Auth Flow (#4596)

* Attempt new sm-export auth flow

* Fix component

* SM-365: Add error messaging for failed import

* SM-365: Fix import error dialog

* SM-365: Fix layout of pages, title, and success messaging

* SM-365: Address majority of PR comments, clear import form on success

* SM-365: Refactor error handling, refactor date formatting

* SM-365: Refactored names, logic, added SM porting api service, added needed error checking, etc.

* SM-365: Refactor fileContents to pastedContents to be more clear

* SM-365: Refactoring based on PR comments

* SM-365: Update based on PR comments, refactoring ngOnInit for sm-import

* SM-365: Fix wrong type on choose import file button
This commit is contained in:
Colton Hurst 2023-02-13 10:52:47 -05:00 committed by GitHub
parent d65acc3bad
commit 63563bd87d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 817 additions and 2 deletions

View File

@ -1249,6 +1249,9 @@
"importSuccess": {
"message": "Data successfully imported"
},
"dataExportSuccess": {
"message": "Data successfully exported"
},
"importWarning": {
"message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?",
"placeholders": {
@ -6111,6 +6114,33 @@
}
}
},
"exportData": {
"message": "Export data"
},
"exportingOrganizationSecretDataTitle": {
"message": "Exporting Organization Secret Data"
},
"exportingOrganizationSecretDataDescription": {
"message": "Only the Secrets Manager data associated with $ORGANIZATION$ will be exported. Items in other products or from other organizations will not be included.",
"placeholders": {
"ORGANIZATION": {
"content": "$1",
"example": "My Org Name"
}
}
},
"fileUpload": {
"message": "File upload"
},
"acceptedFormats": {
"message": "Accepted Formats:"
},
"copyPasteImportContents": {
"message": "Copy & paste import contents:"
},
"or": {
"message": "or"
},
"licenseAndBillingManagement": {
"message": "License and billing management"
},
@ -6188,5 +6218,14 @@
},
"userAccessSecretsManager": {
"message": "This user can access the Secrets Manager Beta"
},
"resolveTheErrorsBelowAndTryAgain": {
"message": "Resolve the errors below and try again."
},
"description": {
"message": "Description"
},
"errorReadingImportFile": {
"message": "An error occurred when trying to read the import file"
}
}

View File

@ -11,4 +11,7 @@
route="service-accounts"
></bit-nav-item>
<bit-nav-item icon="bwi-trash" [text]="'trash' | i18n" route="trash"></bit-nav-item>
<bit-nav-item icon="bwi-cog" [text]="'settings' | i18n" route="settings"></bit-nav-item>
<bit-nav-group icon="bwi-cog" [text]="'settings' | i18n">
<bit-nav-item [text]="'importData' | i18n" route="settings/import"></bit-nav-item>
<bit-nav-item [text]="'exportData' | i18n" route="settings/export"></bit-nav-item>
</bit-nav-group>

View File

@ -0,0 +1,27 @@
<bit-dialog>
<span bitDialogTitle>
{{ "importError" | i18n }}
</span>
<span bitDialogContent>
<div>{{ "resolveTheErrorsBelowAndTryAgain" | i18n }}</div>
<bit-table>
<ng-container header>
<tr>
<th bitCell>{{ "name" | i18n }}</th>
<th bitCell>{{ "description" | i18n }}</th>
</tr>
</ng-container>
<ng-template body>
<tr bitRow *ngFor="let line of errorLines">
<td bitCell class="tw-break-all">[{{ line.id }}] [{{ line.type }}] {{ line.key }}</td>
<td bitCell>{{ line.errorMessage }}</td>
</tr>
</ng-template>
</bit-table>
</span>
<div bitDialogFooter>
<button bitButton bitDialogClose buttonType="primary" type="button">
{{ "ok" | i18n }}
</button>
</div>
</bit-dialog>

View File

@ -0,0 +1,27 @@
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { SecretsManagerImportError } from "../models/error/sm-import-error";
import { SecretsManagerImportErrorLine } from "../models/error/sm-import-error-line";
export interface SecretsManagerImportErrorDialogOperation {
error: SecretsManagerImportError;
}
@Component({
selector: "sm-import-error-dialog",
templateUrl: "./sm-import-error-dialog.component.html",
})
export class SecretsManagerImportErrorDialogComponent {
errorLines: SecretsManagerImportErrorLine[];
constructor(
public dialogRef: DialogRef,
private i18nService: I18nService,
@Inject(DIALOG_DATA) public data: SecretsManagerImportErrorDialogOperation
) {
this.errorLines = data.error.lines;
}
}

View File

@ -0,0 +1,6 @@
export class SecretsManagerImportErrorLine {
id: number;
type: "Project" | "Secret";
key: "string";
errorMessage: string;
}

View File

@ -0,0 +1,9 @@
import { SecretsManagerImportErrorLine } from "./sm-import-error-line";
export class SecretsManagerImportError extends Error {
constructor(message?: string) {
super(message);
}
lines: SecretsManagerImportErrorLine[];
}

View File

@ -0,0 +1,7 @@
import { SecretsManagerImportedProjectRequest } from "./sm-imported-project.request";
import { SecretsManagerImportedSecretRequest } from "./sm-imported-secret.request";
export class SecretsManagerImportRequest {
projects: SecretsManagerImportedProjectRequest[];
secrets: SecretsManagerImportedSecretRequest[];
}

View File

@ -0,0 +1,6 @@
import { EncString } from "@bitwarden/common/models/domain/enc-string";
export class SecretsManagerImportedProjectRequest {
id: string;
name: EncString;
}

View File

@ -0,0 +1,9 @@
import { EncString } from "@bitwarden/common/models/domain/enc-string";
export class SecretsManagerImportedSecretRequest {
id: string;
key: EncString;
value: EncString;
note: EncString;
projectIds: string[];
}

View File

@ -0,0 +1,19 @@
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
import { SecretsManagerExportedProjectResponse } from "./sm-exported-project.response";
import { SecretsManagerExportedSecretResponse } from "./sm-exported-secret.response";
export class SecretsManagerExportResponse extends BaseResponse {
projects: SecretsManagerExportedProjectResponse[];
secrets: SecretsManagerExportedSecretResponse[];
constructor(response: any) {
super(response);
const projects = this.getResponseProperty("Projects");
const secrets = this.getResponseProperty("Secrets");
this.projects = projects?.map((k: any) => new SecretsManagerExportedProjectResponse(k));
this.secrets = secrets?.map((k: any) => new SecretsManagerExportedSecretResponse(k));
}
}

View File

@ -0,0 +1,13 @@
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
export class SecretsManagerExportedProjectResponse extends BaseResponse {
id: string;
name: string;
constructor(response: any) {
super(response);
this.id = this.getResponseProperty("Id");
this.name = this.getResponseProperty("Name");
}
}

View File

@ -0,0 +1,21 @@
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
export class SecretsManagerExportedSecretResponse extends BaseResponse {
id: string;
key: string;
value: string;
note: string;
projectIds: string[];
constructor(response: any) {
super(response);
this.id = this.getResponseProperty("Id");
this.key = this.getResponseProperty("Key");
this.value = this.getResponseProperty("Value");
this.note = this.getResponseProperty("Note");
const projectIds = this.getResponseProperty("ProjectIds");
this.projectIds = projectIds?.map((id: any) => id.toString());
}
}

View File

@ -0,0 +1,17 @@
export class SecretsManagerExport {
projects: SecretsManagerExportProject[];
secrets: SecretsManagerExportSecret[];
}
export class SecretsManagerExportProject {
id: string;
name: string;
}
export class SecretsManagerExportSecret {
id: string;
key: string;
value: string;
note: string;
projectIds: string[];
}

View File

@ -0,0 +1,20 @@
<sm-header></sm-header>
<form [formGroup]="formGroup" [bitSubmit]="submit">
<div class="tw-my-4 tw-max-w-xl">
<app-callout type="info" title="{{ 'exportingOrganizationSecretDataTitle' | i18n }}">
{{ "exportingOrganizationSecretDataDescription" | i18n: orgName }}
</app-callout>
</div>
<bit-form-field class="tw-max-w-sm">
<bit-label>{{ "fileFormat" | i18n }}</bit-label>
<select bitInput formControlName="format">
<option *ngFor="let format of exportFormats" [ngValue]="format">{{ format }}</option>
</select>
</bit-form-field>
<button bitButton bitFormButton type="submit" buttonType="primary">
{{ "exportData" | i18n }}
</button>
</form>

View File

@ -0,0 +1,117 @@
import { Component, OnDestroy, OnInit } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
import { Subject, switchMap, takeUntil } from "rxjs";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { UserVerificationPromptComponent } from "@bitwarden/web-vault/app/components/user-verification-prompt.component";
import { SecretsManagerPortingApiService } from "../services/sm-porting-api.service";
import { SecretsManagerPortingService } from "../services/sm-porting.service";
@Component({
selector: "sm-export",
templateUrl: "./sm-export.component.html",
})
export class SecretsManagerExportComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
protected orgName: string;
protected orgId: string;
protected exportFormats: string[] = ["json"];
protected formGroup = new FormGroup({
format: new FormControl("json", [Validators.required]),
});
constructor(
private route: ActivatedRoute,
private i18nService: I18nService,
private organizationService: OrganizationService,
private platformUtilsService: PlatformUtilsService,
private smPortingService: SecretsManagerPortingService,
private fileDownloadService: FileDownloadService,
private logService: LogService,
private modalService: ModalService,
private secretsManagerApiService: SecretsManagerPortingApiService
) {}
async ngOnInit() {
this.route.params
.pipe(
switchMap(async (params) => await this.organizationService.get(params.organizationId)),
takeUntil(this.destroy$)
)
.subscribe((organization) => {
this.orgName = organization.name;
this.orgId = organization.id;
});
this.formGroup.get("format").disable();
}
async ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
submit = async () => {
this.formGroup.markAllAsTouched();
if (this.formGroup.invalid) {
return;
}
const userVerified = await this.verifyUser();
if (!userVerified) {
return;
}
await this.doExport();
};
private async doExport() {
try {
const exportData = await this.secretsManagerApiService.export(
this.orgId,
this.formGroup.get("format").value
);
await this.downloadFile(exportData, this.formGroup.get("format").value);
this.platformUtilsService.showToast("success", null, this.i18nService.t("dataExportSuccess"));
} catch (e) {
this.logService.error(e);
}
}
private async downloadFile(data: string, format: string) {
const fileName = await this.smPortingService.getFileName(null, format);
this.fileDownloadService.download({
fileName: fileName,
blobData: data,
blobOptions: { type: "text/plain" },
});
}
private verifyUser() {
const ref = this.modalService.open(UserVerificationPromptComponent, {
allowMultipleModals: true,
data: {
confirmDescription: "exportWarningDesc",
confirmButtonText: "exportVault",
modalTitle: "confirmVaultExport",
},
});
if (ref == null) {
return;
}
return ref.onClosedPromise();
}
}

View File

@ -0,0 +1,42 @@
<sm-header></sm-header>
<form [formGroup]="formGroup" [bitSubmit]="submit" class="tw-max-w-xl">
<bit-form-field>
<bit-label>{{ "fileUpload" | i18n }}</bit-label>
<div class="file-selector">
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
{{ "chooseFile" | i18n }}
</button>
{{ selectedFile?.name ?? ("noFileChosen" | i18n) }}
</div>
<input
#fileSelector
hidden
bitInput
type="file"
id="file"
class="form-control-file"
name="file"
(change)="setSelectedFile($event)"
accept="application/JSON"
/>
<bit-hint>{{ "acceptedFormats" | i18n }} JSON</bit-hint>
</bit-form-field>
<div class="my-4">
{{ "or" | i18n }}
</div>
<bit-form-field>
<bit-label for="pastedContents">{{ "copyPasteImportContents" | i18n }}</bit-label>
<textarea
bitInput
id="pastedContents"
class="form-control"
name="FileContents"
formControlName="pastedContents"
></textarea>
<bit-hint>{{ "acceptedFormats" | i18n }} JSON</bit-hint>
</bit-form-field>
<button bitButton bitformButton type="submit" buttonType="primary">
{{ "importData" | i18n }}
</button>
</form>

View File

@ -0,0 +1,166 @@
import { Component, OnDestroy, OnInit } from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { DialogService } from "@bitwarden/components";
import {
SecretsManagerImportErrorDialogComponent,
SecretsManagerImportErrorDialogOperation,
} from "../dialog/sm-import-error-dialog.component";
import { SecretsManagerImportError } from "../models/error/sm-import-error";
import { SecretsManagerPortingApiService } from "../services/sm-porting-api.service";
@Component({
selector: "sm-import",
templateUrl: "./sm-import.component.html",
})
export class SecretsManagerImportComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
protected orgId: string = null;
protected selectedFile: File;
protected formGroup = new FormGroup({
pastedContents: new FormControl(""),
});
constructor(
private route: ActivatedRoute,
private i18nService: I18nService,
private organizationService: OrganizationService,
private platformUtilsService: PlatformUtilsService,
protected fileDownloadService: FileDownloadService,
private logService: LogService,
private secretsManagerPortingApiService: SecretsManagerPortingApiService,
private dialogService: DialogService
) {}
async ngOnInit() {
this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params) => {
this.orgId = params.organizationId;
});
}
async ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
submit = async () => {
const fileElement = document.getElementById("file") as HTMLInputElement;
const importContents = await this.getImportContents(
fileElement,
this.formGroup.get("pastedContents").value.trim()
);
if (importContents == null) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("selectFile")
);
return;
}
try {
const error = await this.secretsManagerPortingApiService.import(this.orgId, importContents);
if (error?.lines?.length > 0) {
this.openImportErrorDialog(error);
return;
} else if (error != null) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("errorReadingImportFile")
);
return;
}
this.platformUtilsService.showToast("success", null, this.i18nService.t("importSuccess"));
this.clearForm();
} catch (error) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("errorReadingImportFile")
);
this.logService.error(error);
}
};
protected async getImportContents(
fileElement: HTMLInputElement,
pastedContents: string
): Promise<string> {
const files = fileElement.files;
if (
(files == null || files.length === 0) &&
(pastedContents == null || pastedContents === "")
) {
return null;
}
let fileContents = pastedContents;
if (files != null && files.length > 0) {
try {
const content = await this.getFileContents(files[0]);
if (content != null) {
fileContents = content;
}
} catch (e) {
this.logService.error(e);
}
}
if (fileContents == null || fileContents === "") {
return null;
}
return fileContents;
}
protected setSelectedFile(event: Event) {
const fileInputEl = <HTMLInputElement>event.target;
const file = fileInputEl.files.length > 0 ? fileInputEl.files[0] : null;
this.selectedFile = file;
}
private clearForm() {
(document.getElementById("file") as HTMLInputElement).value = "";
this.selectedFile = null;
this.formGroup.reset({
pastedContents: "",
});
}
private getFileContents(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsText(file, "utf-8");
reader.onload = (evt) => {
resolve((evt.target as any).result);
};
reader.onerror = () => {
reject();
};
});
}
private openImportErrorDialog(error: SecretsManagerImportError) {
this.dialogService.open<unknown, SecretsManagerImportErrorDialogOperation>(
SecretsManagerImportErrorDialogComponent,
{
data: {
error: error,
},
}
);
}
}

View File

@ -0,0 +1,194 @@
import { Injectable } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/abstractions/encrypt.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { EncString } from "@bitwarden/common/models/domain/enc-string";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { SecretsManagerImportError } from "../models/error/sm-import-error";
import { SecretsManagerImportRequest } from "../models/requests/sm-import.request";
import { SecretsManagerImportedProjectRequest } from "../models/requests/sm-imported-project.request";
import { SecretsManagerImportedSecretRequest } from "../models/requests/sm-imported-secret.request";
import { SecretsManagerExportResponse } from "../models/responses/sm-export.response";
import {
SecretsManagerExport,
SecretsManagerExportProject,
SecretsManagerExportSecret,
} from "../models/sm-export";
@Injectable({
providedIn: "root",
})
export class SecretsManagerPortingApiService {
constructor(
private apiService: ApiService,
private encryptService: EncryptService,
private cryptoService: CryptoService,
private i18nService: I18nService
) {}
async export(organizationId: string, exportFormat = "json"): Promise<string> {
let response = {};
try {
response = await this.apiService.send(
"GET",
"/sm/" + organizationId + "/export?format=" + exportFormat,
null,
true,
true
);
} catch (error) {
return null;
}
return JSON.stringify(
await this.decryptExport(organizationId, new SecretsManagerExportResponse(response)),
null,
" "
);
}
async import(organizationId: string, fileContents: string): Promise<SecretsManagerImportError> {
let requestObject = {};
try {
requestObject = JSON.parse(fileContents);
const requestBody = await this.encryptImport(organizationId, requestObject);
await this.apiService.send(
"POST",
"/sm/" + organizationId + "/import",
requestBody,
true,
true
);
} catch (error) {
const errorResponse = new ErrorResponse(error, 400);
return this.handleServerError(errorResponse, requestObject);
}
}
private async encryptImport(
organizationId: string,
importData: any
): Promise<SecretsManagerImportRequest> {
const encryptedImport = new SecretsManagerImportRequest();
try {
const orgKey = await this.cryptoService.getOrgKey(organizationId);
encryptedImport.projects = [];
encryptedImport.secrets = [];
encryptedImport.projects = await Promise.all(
importData.projects.map(async (p: any) => {
const project = new SecretsManagerImportedProjectRequest();
project.id = p.id;
project.name = await this.encryptService.encrypt(p.name, orgKey);
return project;
})
);
encryptedImport.secrets = await Promise.all(
importData.secrets.map(async (s: any) => {
const secret = new SecretsManagerImportedSecretRequest();
[secret.key, secret.value, secret.note] = await Promise.all([
this.encryptService.encrypt(s.key, orgKey),
this.encryptService.encrypt(s.value, orgKey),
this.encryptService.encrypt(s.note, orgKey),
]);
secret.id = s.id;
secret.projectIds = s.projectIds;
return secret;
})
);
} catch (error) {
return null;
}
return encryptedImport;
}
private async decryptExport(
organizationId: string,
exportData: SecretsManagerExportResponse
): Promise<SecretsManagerExport> {
const orgKey = await this.cryptoService.getOrgKey(organizationId);
const decryptedExport = new SecretsManagerExport();
decryptedExport.projects = [];
decryptedExport.secrets = [];
decryptedExport.projects = await Promise.all(
exportData.projects.map(async (p) => {
const project = new SecretsManagerExportProject();
project.id = p.id;
project.name = await this.encryptService.decryptToUtf8(new EncString(p.name), orgKey);
return project;
})
);
decryptedExport.secrets = await Promise.all(
exportData.secrets.map(async (s) => {
const secret = new SecretsManagerExportSecret();
[secret.key, secret.value, secret.note] = await Promise.all([
this.encryptService.decryptToUtf8(new EncString(s.key), orgKey),
this.encryptService.decryptToUtf8(new EncString(s.value), orgKey),
this.encryptService.decryptToUtf8(new EncString(s.note), orgKey),
]);
secret.id = s.id;
secret.projectIds = s.projectIds;
return secret;
})
);
return decryptedExport;
}
private handleServerError(
errorResponse: ErrorResponse,
importResult: any
): SecretsManagerImportError {
if (errorResponse.validationErrors == null) {
return new SecretsManagerImportError(errorResponse.message);
}
const result = new SecretsManagerImportError();
result.lines = [];
Object.entries(errorResponse.validationErrors).forEach(([key, value], index) => {
let item;
let itemType;
const id = Number(key.match(/[0-9]+/)[0]);
switch (key.match(/^\w+/)[0]) {
case "Projects":
item = importResult.projects[id];
itemType = "Project";
break;
case "Secrets":
item = importResult.secrets[id];
itemType = "Secret";
break;
default:
return;
}
result.lines.push({
id: id + 1,
type: itemType == "Project" ? "Project" : "Secret",
key: item.key,
errorMessage: value.length > 0 ? value[0] : "",
});
});
return result;
}
}

View File

@ -0,0 +1,18 @@
import { formatDate } from "@angular/common";
import { Injectable } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
@Injectable({
providedIn: "root",
})
export class SecretsManagerPortingService {
constructor(private i18nService: I18nService) {}
async getFileName(prefix: string = null, extension = "json"): Promise<string> {
const locale = await firstValueFrom(this.i18nService.locale$);
const dateString = formatDate(new Date(), "yyyyMMddHHmmss", locale);
return "bitwarden" + (prefix ? "_" + prefix : "") + "_export_" + dateString + "." + extension;
}
}

View File

@ -0,0 +1,28 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { SecretsManagerExportComponent } from "./porting/sm-export.component";
import { SecretsManagerImportComponent } from "./porting/sm-import.component";
const routes: Routes = [
{
path: "import",
component: SecretsManagerImportComponent,
data: {
titleId: "importData",
},
},
{
path: "export",
component: SecretsManagerExportComponent,
data: {
titleId: "exportData",
},
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class SettingsRoutingModule {}

View File

@ -0,0 +1,21 @@
import { NgModule } from "@angular/core";
import { SecretsManagerSharedModule } from "../shared/sm-shared.module";
import { SecretsManagerImportErrorDialogComponent } from "./dialog/sm-import-error-dialog.component";
import { SecretsManagerExportComponent } from "./porting/sm-export.component";
import { SecretsManagerImportComponent } from "./porting/sm-import.component";
import { SecretsManagerPortingApiService } from "./services/sm-porting-api.service";
import { SecretsManagerPortingService } from "./services/sm-porting.service";
import { SettingsRoutingModule } from "./settings-routing.module";
@NgModule({
imports: [SecretsManagerSharedModule, SettingsRoutingModule],
declarations: [
SecretsManagerImportComponent,
SecretsManagerExportComponent,
SecretsManagerImportErrorDialogComponent,
],
providers: [SecretsManagerPortingService, SecretsManagerPortingApiService],
})
export class SettingsModule {}

View File

@ -11,6 +11,7 @@ import { OverviewModule } from "./overview/overview.module";
import { ProjectsModule } from "./projects/projects.module";
import { SecretsModule } from "./secrets/secrets.module";
import { ServiceAccountsModule } from "./service-accounts/service-accounts.module";
import { SettingsModule } from "./settings/settings.module";
import { SMGuard } from "./sm.guard";
const routes: Routes = [
@ -48,6 +49,10 @@ const routes: Routes = [
titleId: "serviceAccounts",
},
},
{
path: "settings",
loadChildren: () => SettingsModule,
},
{
path: "",
loadChildren: () => OverviewModule,

View File

@ -5,7 +5,8 @@ export type InputTypes =
| "datetime-local"
| "email"
| "checkbox"
| "search";
| "search"
| "file";
export abstract class BitFormFieldControl {
ariaDescribedBy: string;