Added Frontend for Secrets

This commit is contained in:
DerDavidBohl
2025-10-03 00:27:06 +02:00
parent d15e3617ea
commit cc2c06851e
20 changed files with 317 additions and 18 deletions
@@ -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}`);
}
}
+6
View File
@@ -0,0 +1,6 @@
export interface Secret {
key: string | null;
value: string | null;
environmentVariable: string;
deployments: Array<string>;
}
+4 -1
View File
@@ -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>
+4 -2
View File
@@ -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'
})
+2 -2
View File
@@ -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
}
];
@@ -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,
});
}
}