mirror of
https://github.com/DerDavidBohl/dirigent-spring.git
synced 2026-05-01 03:27:47 -05:00
Added Frontend for Secrets
This commit is contained in:
@@ -36,8 +36,11 @@ public class SecretService {
|
||||
public void saveSecret(String key, String environmentVariable, String value, List<String> deployments) {
|
||||
try {
|
||||
|
||||
String encrypted = encrypt(value);
|
||||
Secret secret = new Secret(key, environmentVariable, encrypted, deployments);
|
||||
Secret secret = secretRepository.findById(key).orElseGet(() -> new Secret(key, environmentVariable, value, deployments));
|
||||
|
||||
if(value != null )
|
||||
secret.setEncryptedValue(encrypt(value));
|
||||
|
||||
secretRepository.save(secret);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Saving Secret failed", e);
|
||||
@@ -52,7 +55,7 @@ public class SecretService {
|
||||
try {
|
||||
result.put(secret.getEnvironmentVariable(), decrypt(secret.getEncryptedValue()));
|
||||
} catch(Exception ex) {
|
||||
log.error("Failed to decrypt secret <" + secret.getKey() + "> for Env Var <" + secret.getEnvironmentVariable() + "> and Deployment <" + deployment + ">.");
|
||||
log.error("Failed to decrypt secret <{}> for Env Var <{}> and Deployment <{}>.", secret.getKey(), secret.getEnvironmentVariable(), deployment);
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
@@ -78,6 +81,10 @@ public class SecretService {
|
||||
}
|
||||
|
||||
private String decrypt(String encrypted) throws Exception {
|
||||
|
||||
if(encrypted == null)
|
||||
return null;
|
||||
|
||||
SecretKeySpec keySpec = new SecretKeySpec(encryptionKey.getBytes(), ALGORITHM);
|
||||
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
||||
cipher.init(Cipher.DECRYPT_MODE, keySpec);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Observable, ReplaySubject} from 'rxjs';
|
||||
import {Deployment} from './deploymentState';
|
||||
import {Observable, ReplaySubject, tap} from 'rxjs';
|
||||
import {Deployment} from './deployment';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import { SystemInformation } from './system-information';
|
||||
import {Secret} from './secret';
|
||||
import {SystemInformation} from './system-information';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -10,6 +11,7 @@ import { SystemInformation } from './system-information';
|
||||
export class ApiService {
|
||||
|
||||
private _deploymentStates: ReplaySubject<Array<Deployment>> = new ReplaySubject<Array<Deployment>>(1);
|
||||
private _secrets: ReplaySubject<Array<Secret>> = new ReplaySubject<Array<Secret>>(1);
|
||||
|
||||
constructor(private http: HttpClient) {
|
||||
}
|
||||
@@ -18,6 +20,10 @@ export class ApiService {
|
||||
return this._deploymentStates.asObservable();
|
||||
}
|
||||
|
||||
get secrets$(): Observable<Array<Secret>> {
|
||||
return this._secrets.asObservable();
|
||||
}
|
||||
|
||||
updateDeploymentStates(): void {
|
||||
this.getAllDeploymentStates().subscribe(r => this._deploymentStates.next(r));
|
||||
}
|
||||
@@ -37,4 +43,22 @@ export class ApiService {
|
||||
startDeployment(deploymentState: Deployment, force: boolean): Observable<void> {
|
||||
return this.http.post<void>(`api/v1/deployments/${deploymentState.name}/start?forceRecreate=${force}`, {});
|
||||
}
|
||||
|
||||
getSecrets(): Observable<Array<Secret>> {
|
||||
return this.http.get<Array<Secret>>('api/v1/secrets');
|
||||
}
|
||||
|
||||
reloadSecrets(): void {
|
||||
this.getSecrets().subscribe((r) => this._secrets.next(r));
|
||||
}
|
||||
|
||||
putSecret(secret: Secret): Observable<void> {
|
||||
return this.http.put<void>(`api/v1/secrets/${secret.key}`, secret).pipe(
|
||||
tap(() => this.reloadSecrets())
|
||||
);
|
||||
}
|
||||
|
||||
deleteSecret(secret: Secret): Observable<void> {
|
||||
return this.http.delete<void>(`api/v1/secrets/${secret.key}`);
|
||||
}
|
||||
}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
export interface Secret {
|
||||
key: string | null;
|
||||
value: string | null;
|
||||
environmentVariable: string;
|
||||
deployments: Array<string>;
|
||||
}
|
||||
Vendored
@@ -3,5 +3,8 @@
|
||||
<img alt="Icon" class="inline h-15" src="icon-background.svg"/>
|
||||
Dirigent
|
||||
</h1>
|
||||
<router-outlet></router-outlet>
|
||||
<mat-tab-group>
|
||||
<mat-tab label="Deployments"><app-deployments></app-deployments></mat-tab>
|
||||
<mat-tab label="Secrets"><app-secrets></app-secrets></mat-tab>
|
||||
</mat-tab-group>
|
||||
</main>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {RouterOutlet} from '@angular/router';
|
||||
import {MatTab, MatTabGroup} from '@angular/material/tabs';
|
||||
import {DeploymentsComponent} from './deployments/deployments.component';
|
||||
import {SecretsComponent} from './secrets/secrets.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [RouterOutlet],
|
||||
imports: [MatTabGroup, MatTab, DeploymentsComponent, SecretsComponent],
|
||||
templateUrl: './app.component.html',
|
||||
styleUrl: './app.component.css'
|
||||
})
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {Routes} from '@angular/router';
|
||||
import {OverviewComponent} from './overview/overview.component';
|
||||
import {DeploymentsComponent} from './deployments/deployments.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: OverviewComponent
|
||||
component: DeploymentsComponent
|
||||
}
|
||||
];
|
||||
|
||||
+7
-7
@@ -11,8 +11,8 @@ import {
|
||||
MatRowDef,
|
||||
MatTable
|
||||
} from '@angular/material/table';
|
||||
import {Deployment} from './deploymentState';
|
||||
import {ApiService} from './api.service';
|
||||
import {Deployment} from '../api/deployment';
|
||||
import {ApiService} from '../api/api.service';
|
||||
import {MatMenu, MatMenuItem, MatMenuTrigger} from '@angular/material/menu';
|
||||
import {MatIcon} from '@angular/material/icon';
|
||||
import {MatChip, MatChipListbox, MatChipListboxChange, MatChipOption} from '@angular/material/chips';
|
||||
@@ -25,10 +25,10 @@ import {AsyncPipe} from '@angular/common';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {MatSort, MatSortHeader, Sort} from '@angular/material/sort';
|
||||
import {MatFormField, MatInput, MatLabel} from '@angular/material/input';
|
||||
import { SystemInformation } from './system-information';
|
||||
import { SystemInformation } from '../api/system-information';
|
||||
|
||||
@Component({
|
||||
selector: 'app-overview',
|
||||
selector: 'app-deployments',
|
||||
imports: [
|
||||
MatTable,
|
||||
MatColumnDef,
|
||||
@@ -56,10 +56,10 @@ import { SystemInformation } from './system-information';
|
||||
MatInput,
|
||||
MatFormField
|
||||
],
|
||||
templateUrl: './overview.component.html',
|
||||
styleUrl: './overview.component.css',
|
||||
templateUrl: './deployments.component.html',
|
||||
styleUrl: './deployments.component.css',
|
||||
})
|
||||
export class OverviewComponent implements OnInit {
|
||||
export class DeploymentsComponent implements OnInit {
|
||||
|
||||
selectedFilterValues$ = new ReplaySubject<Array<string>>(1);
|
||||
sort$ = new ReplaySubject<Sort>(1);
|
||||
@@ -0,0 +1,59 @@
|
||||
|
||||
<mat-dialog-content>
|
||||
@if (originalSecret.key) {
|
||||
<h2 class="text-xl mb-3">Edit Secret</h2>
|
||||
}
|
||||
@if (!originalSecret.key) {
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Key</mat-label>
|
||||
<input type="text" matInput [(ngModel)]="secret.key">
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Environment Variable</mat-label>
|
||||
<input type="text" matInput [(ngModel)]="secret.environmentVariable">
|
||||
</mat-form-field>
|
||||
|
||||
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Value</mat-label>
|
||||
<input type="password" matInput [(ngModel)]="secret.value">
|
||||
</mat-form-field>
|
||||
|
||||
|
||||
<mat-form-field class="w-full">
|
||||
|
||||
<mat-label>Deployments</mat-label>
|
||||
<mat-chip-grid #chipGrid>
|
||||
@for (deployment of secret.deployments; track deployment) {
|
||||
<mat-chip-row
|
||||
(removed)="removeDeployment(deployment)"
|
||||
>
|
||||
{{deployment}}
|
||||
<button matChipRemove>
|
||||
<mat-icon>cancel</mat-icon>
|
||||
</button>
|
||||
</mat-chip-row>
|
||||
}
|
||||
<input
|
||||
[matChipInputFor]="chipGrid"
|
||||
[matChipInputSeparatorKeyCodes]="[ENTER, SPACE] "
|
||||
(matChipInputTokenEnd)="addDeployment($event)"
|
||||
/>
|
||||
</mat-chip-grid>
|
||||
</mat-form-field>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions>
|
||||
<button mat-button (click)="save()" [disabled]="!changed()">Save</button>
|
||||
|
||||
@if (originalSecret.key && !sureDelete) {
|
||||
<button mat-button (click)="delete()">Delete</button>
|
||||
}
|
||||
@if (originalSecret.key && sureDelete) {
|
||||
<button mat-button (click)="delete()">Sure?</button>
|
||||
}
|
||||
<button mat-button (click)="cancel()">Cancel</button>
|
||||
</mat-dialog-actions>
|
||||
@@ -0,0 +1,78 @@
|
||||
import {ChangeDetectionStrategy, Component, inject, Inject} from '@angular/core';
|
||||
import {MAT_DIALOG_DATA, MatDialogActions, MatDialogContent, MatDialogRef} from '@angular/material/dialog';
|
||||
import {Secret} from '../../api/secret';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {MatFormField, MatInput, MatLabel} from '@angular/material/input';
|
||||
import {MatButton} from '@angular/material/button';
|
||||
import {MatChipGrid, MatChipInput, MatChipInputEvent, MatChipRow} from '@angular/material/chips';
|
||||
import {ENTER, SPACE} from '@angular/cdk/keycodes';
|
||||
import {MatIcon} from '@angular/material/icon';
|
||||
import {ApiService} from '../../api/api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-secret-dialog',
|
||||
imports: [
|
||||
FormsModule,
|
||||
MatInput,
|
||||
MatFormField,
|
||||
MatLabel,
|
||||
MatButton,
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
MatChipGrid,
|
||||
MatChipRow,
|
||||
MatIcon,
|
||||
MatChipInput
|
||||
],
|
||||
templateUrl: './edit-secret-dialog.component.html',
|
||||
styleUrl: './edit-secret-dialog.component.css',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class EditSecretDialogComponent {
|
||||
|
||||
protected readonly ENTER = ENTER;
|
||||
protected readonly SPACE = SPACE;
|
||||
readonly dialogRef = inject(MatDialogRef<EditSecretDialogComponent>);
|
||||
secret: Secret;
|
||||
originalSecret: Secret;
|
||||
sureDelete: boolean = false;
|
||||
|
||||
constructor(@Inject(MAT_DIALOG_DATA) public data: Secret, private apiService: ApiService) {
|
||||
this.secret = structuredClone(data);
|
||||
this.originalSecret = structuredClone(data);
|
||||
}
|
||||
|
||||
changed(): boolean {
|
||||
return JSON.stringify(this.secret) !== JSON.stringify(this.originalSecret);
|
||||
}
|
||||
|
||||
delete() {
|
||||
|
||||
if (!this.sureDelete) {
|
||||
this.sureDelete = true;
|
||||
return;
|
||||
}
|
||||
this.apiService.deleteSecret(this.secret).subscribe(() => this.dialogRef.close());
|
||||
}
|
||||
|
||||
save() {
|
||||
this.apiService.putSecret(this.secret).subscribe(() => this.dialogRef.close());
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
removeDeployment(deployment: string) {
|
||||
this.secret.deployments = this.secret.deployments.filter(d => d !== deployment);
|
||||
}
|
||||
|
||||
addDeployment($event: MatChipInputEvent) {
|
||||
|
||||
$event.chipInput.clear();
|
||||
|
||||
if (this.secret.deployments.includes($event.value)) return;
|
||||
|
||||
this.secret.deployments.push($event.value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<h2 class="text-2xl">Your Secrets</h2>
|
||||
|
||||
<button mat-stroked-button color="primary" (click)="add()">Add Secret</button>
|
||||
|
||||
<table [dataSource]="secrets$" mat-table >
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th *matHeaderCellDef mat-header-cell >Actions</th>
|
||||
<td *matCellDef="let element" mat-cell>
|
||||
<button mat-icon-button (click)="edit(element)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="key">
|
||||
<th *matHeaderCellDef mat-header-cell >Name</th>
|
||||
<td *matCellDef="let element" mat-cell> {{ element.key }}</td>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<ng-container matColumnDef="environmentVariable">
|
||||
<th *matHeaderCellDef mat-header-cell >Environment Variable</th>
|
||||
<td *matCellDef="let element" mat-cell> {{ element.environmentVariable }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="deployments">
|
||||
<th *matHeaderCellDef mat-header-cell >Deployments</th>
|
||||
<td *matCellDef="let element" mat-cell>
|
||||
|
||||
<mat-chip-set>
|
||||
@for (deployment of element.deployments; track deployment) {
|
||||
<mat-chip>
|
||||
{{deployment}}
|
||||
</mat-chip>
|
||||
}
|
||||
</mat-chip-set>
|
||||
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||
<tr *matRowDef="let row; columns: displayedColumns;" mat-row></tr>
|
||||
</table>
|
||||
@@ -0,0 +1,74 @@
|
||||
import {ChangeDetectionStrategy, Component, inject} from '@angular/core';
|
||||
import {ApiService} from '../api/api.service';
|
||||
import {Observable} from 'rxjs';
|
||||
import {Secret} from '../api/secret';
|
||||
import {
|
||||
MatCell,
|
||||
MatCellDef,
|
||||
MatColumnDef,
|
||||
MatHeaderCell,
|
||||
MatHeaderCellDef,
|
||||
MatHeaderRow,
|
||||
MatHeaderRowDef,
|
||||
MatRow,
|
||||
MatRowDef,
|
||||
MatTable
|
||||
} from '@angular/material/table';
|
||||
import {MatIcon} from '@angular/material/icon';
|
||||
import {MatChip, MatChipSet} from '@angular/material/chips';
|
||||
import {MatButton, MatIconButton} from '@angular/material/button';
|
||||
import {MatDialog} from '@angular/material/dialog';
|
||||
import {EditSecretDialogComponent} from './edit-secret-dialog/edit-secret-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-secrets',
|
||||
imports: [
|
||||
MatCell,
|
||||
MatCellDef,
|
||||
MatColumnDef,
|
||||
MatHeaderCell,
|
||||
MatIcon,
|
||||
MatTable,
|
||||
MatHeaderCellDef,
|
||||
MatHeaderRowDef,
|
||||
MatRowDef,
|
||||
MatHeaderRow,
|
||||
MatRow,
|
||||
MatIconButton,
|
||||
MatChipSet,
|
||||
MatChip,
|
||||
MatButton,
|
||||
],
|
||||
templateUrl: './secrets.component.html',
|
||||
styleUrl: './secrets.component.css',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class SecretsComponent {
|
||||
|
||||
secrets$: Observable<Array<Secret>>;
|
||||
displayedColumns = ['actions', 'key', 'environmentVariable', 'deployments'];
|
||||
|
||||
readonly dialog = inject(MatDialog);
|
||||
|
||||
constructor(private apiService: ApiService) {
|
||||
this.apiService.reloadSecrets();
|
||||
this.secrets$ = apiService.secrets$;
|
||||
}
|
||||
|
||||
edit(element: Secret) {
|
||||
this.dialog.open(EditSecretDialogComponent, {
|
||||
data: element
|
||||
});
|
||||
}
|
||||
|
||||
add() {
|
||||
this.dialog.open(EditSecretDialogComponent, {
|
||||
data: {
|
||||
key: null,
|
||||
value: null,
|
||||
environmentVariable: '',
|
||||
deployments: []
|
||||
} as Secret,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user