src/app/features/api-client/api-client-list/api-client-list.component.ts

Description

Admin page for account managers to create and manage the credentials ("API clients") of external systems that access this system's data through the API.

Example

Metadata

Relationships

Used by

No results matching.

Depends on

Index

Properties
Methods

Methods

Async create
create()
Returns : any
Async delete
delete(apiClient: ApiClient)
Parameters :
Name Type Optional
apiClient ApiClient No
Returns : any
Async editScopes
editScopes(apiClient: ApiClient)
Parameters :
Name Type Optional
apiClient ApiClient No
Returns : any
Async regenerateSecret
regenerateSecret(apiClient: ApiClient)
Parameters :
Name Type Optional
apiClient ApiClient No
Returns : any

Properties

Readonly apiClients
Type : unknown
Default value : resource({ params: () => (this.isAvailable() ? {} : undefined), loader: async () => (await this.apiClientService.getApiClients()).map((apiClient) => ({ ...apiClient, scopeLabels: apiClient.scopes.map((scope) => getScopeLabel(scope)), })), })

the API clients, loaded once the backend is known to offer them

Readonly displayedColumns
Type : []
Default value : [ "name", "clientId", "scopes", "created", "actions", ]
Readonly isAvailable
Type : unknown
Default value : this.apiClientService.isAvailable
Readonly isCheckingAvailability
Type : unknown
Default value : this.apiClientService.isCheckingAvailability
Readonly loadingError
Type : unknown
Default value : computed(() => this.apiClients.error() ? errorMessageOf(this.apiClients.error()) : undefined, )
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  inject,
  resource,
} from "@angular/core";
import { MatDialog } from "@angular/material/dialog";
import { firstValueFrom } from "rxjs";
import { MatButtonModule } from "@angular/material/button";
import { MatTableModule } from "@angular/material/table";
import { MatTooltipModule } from "@angular/material/tooltip";
import { MatProgressBarModule } from "@angular/material/progress-bar";
import { FaIconComponent } from "@fortawesome/angular-fontawesome";
import { ViewTitleComponent } from "#src/app/core/common-components/view-title/view-title.component";
import { HintBoxComponent } from "#src/app/core/common-components/hint-box/hint-box.component";
import { ConfirmationDialogService } from "#src/app/core/common-components/confirmation-dialog/confirmation-dialog.service";
import { AlertService } from "#src/app/core/alerts/alert.service";
import { CustomDatePipe } from "#src/app/core/basic-datatypes/date/custom-date.pipe";
import {
  ApiClient,
  ApiClientCredentials,
  ApiClientService,
  errorMessageOf,
  logRequestError,
} from "../api-client.service";
import { getScopeLabel } from "../api-client-scopes";
import { CreateApiClientDialogComponent } from "../create-api-client-dialog/create-api-client-dialog.component";
import { EditApiClientScopesDialogComponent } from "../edit-api-client-scopes-dialog/edit-api-client-scopes-dialog.component";
import {
  ApiClientCredentialsDialogComponent,
  ApiClientCredentialsDialogData,
} from "../api-client-credentials-dialog/api-client-credentials-dialog.component";

/**
 * Admin page for account managers to create and manage the credentials ("API clients")
 * of external systems that access this system's data through the API.
 */
@Component({
  selector: "app-api-client-list",
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [
    ViewTitleComponent,
    HintBoxComponent,
    MatTableModule,
    MatButtonModule,
    MatTooltipModule,
    MatProgressBarModule,
    FaIconComponent,
    CustomDatePipe,
  ],
  templateUrl: "./api-client-list.component.html",
  styleUrl: "./api-client-list.component.scss",
})
export class ApiClientListComponent {
  private readonly apiClientService = inject(ApiClientService);
  private readonly dialog = inject(MatDialog);
  private readonly confirmationDialog = inject(ConfirmationDialogService);
  private readonly alertService = inject(AlertService);

  readonly isCheckingAvailability =
    this.apiClientService.isCheckingAvailability;
  readonly isAvailable = this.apiClientService.isAvailable;
  /** the API clients, loaded once the backend is known to offer them */
  readonly apiClients = resource({
    params: () => (this.isAvailable() ? {} : undefined),
    loader: async () =>
      (await this.apiClientService.getApiClients()).map((apiClient) => ({
        ...apiClient,
        scopeLabels: apiClient.scopes.map((scope) => getScopeLabel(scope)),
      })),
  });
  readonly loadingError = computed(() =>
    this.apiClients.error()
      ? errorMessageOf(this.apiClients.error())
      : undefined,
  );

  readonly displayedColumns = [
    "name",
    "clientId",
    "scopes",
    "created",
    "actions",
  ];

  async create() {
    const dialogRef = this.dialog.open<
      CreateApiClientDialogComponent,
      unknown,
      ApiClientCredentialsDialogData
    >(CreateApiClientDialogComponent, { width: "600px" });
    const result = await firstValueFrom(dialogRef.afterClosed());
    if (result) {
      this.showCredentials(result);
      this.apiClients.reload();
    }
  }

  async editScopes(apiClient: ApiClient) {
    const dialogRef = this.dialog.open<
      EditApiClientScopesDialogComponent,
      ApiClient,
      ApiClient
    >(EditApiClientScopesDialogComponent, { data: apiClient, width: "600px" });
    if (await firstValueFrom(dialogRef.afterClosed())) {
      this.apiClients.reload();
    }
  }

  async regenerateSecret(apiClient: ApiClient) {
    const confirmed = await this.confirmationDialog.getConfirmation(
      $localize`Generate new secret?`,
      $localize`The external system "${apiClient.name}" cannot access the API with its current secret anymore, until you give it the new one.`,
    );
    if (!confirmed) {
      return;
    }

    let credentials: ApiClientCredentials;
    try {
      credentials = await this.apiClientService.regenerateSecret(
        apiClient.clientId,
      );
    } catch (err) {
      logRequestError("Could not regenerate the secret of an API client", err);
      this.alertService.addDanger(errorMessageOf(err));
      return;
    }
    this.showCredentials({ credentials, scopes: apiClient.scopes });
  }

  async delete(apiClient: ApiClient) {
    const confirmed = await this.confirmationDialog.getConfirmation(
      $localize`Delete API client?`,
      $localize`The external system "${apiClient.name}" cannot access the API anymore. This cannot be undone.`,
    );
    if (!confirmed) {
      return;
    }

    try {
      await this.apiClientService.deleteApiClient(apiClient.clientId);
    } catch (err) {
      logRequestError("Could not delete an API client", err);
      this.alertService.addDanger(errorMessageOf(err));
    }
    this.apiClients.reload();
  }

  private showCredentials(data: ApiClientCredentialsDialogData) {
    this.dialog.open(ApiClientCredentialsDialogComponent, {
      data,
      width: "600px",
      // the secret is not shown again, so it must not get lost by an accidental click outside
      disableClose: true,
    });
  }
}
<div class="flex-row flex-wrap">
  <div class="flex-grow">
    <app-view-title i18n>API Access</app-view-title>
  </div>
  @if (isAvailable()) {
    <button
      mat-stroked-button
      color="accent"
      class="standard-add-button"
      (click)="create()"
    >
      <fa-icon icon="plus-circle" class="standard-icon-with-text"></fa-icon>
      <span i18n>New API Client</span>
    </button>
  }
</div>

<p i18n class="margin-bottom-regular">
  External systems can access data of this system through the API, e.g. to
  import report results into another platform or to let their users sign in
  here. Each external system gets its own credentials ("API client"), which you
  can create and revoke here.
</p>

@if (isCheckingAvailability()) {
  <mat-progress-bar mode="indeterminate"></mat-progress-bar>
} @else if (!isAvailable()) {
  <app-hint-box>
    <span i18n>
      API access is not available for this system. Please contact your technical
      support team to enable the API integration.
    </span>
  </app-hint-box>
} @else if (loadingError(); as loadingError) {
  <p class="color-error" role="alert">{{ loadingError }}</p>
} @else if (apiClients.hasValue()) {
  @let clients = apiClients.value();
  @if (clients.length === 0) {
    <p i18n>There are no API clients yet.</p>
  } @else {
    <div class="table-container">
      <table mat-table [dataSource]="clients" class="full-width table">
        <ng-container matColumnDef="name">
          <th mat-header-cell *matHeaderCellDef i18n>Name</th>
          <td mat-cell *matCellDef="let apiClient">{{ apiClient.name }}</td>
        </ng-container>

        <ng-container matColumnDef="clientId">
          <th mat-header-cell *matHeaderCellDef i18n>Client ID</th>
          <td mat-cell *matCellDef="let apiClient">
            <code>{{ apiClient.clientId }}</code>
          </td>
        </ng-container>

        <ng-container matColumnDef="scopes">
          <th mat-header-cell *matHeaderCellDef i18n>Access</th>
          <td mat-cell *matCellDef="let apiClient">
            @for (label of apiClient.scopeLabels; track label) {
              <div>{{ label }}</div>
            }
          </td>
        </ng-container>

        <ng-container matColumnDef="created">
          <th mat-header-cell *matHeaderCellDef i18n>Created</th>
          <td mat-cell *matCellDef="let apiClient">
            {{ apiClient.createdAt | customDate: "mediumDate" }}
            @if (apiClient.createdBy) {
              <div class="hint-text">{{ apiClient.createdBy }}</div>
            }
          </td>
        </ng-container>

        <ng-container matColumnDef="actions">
          <th mat-header-cell *matHeaderCellDef></th>
          <td mat-cell *matCellDef="let apiClient" class="text-align-end">
            <button
              mat-icon-button
              (click)="editScopes(apiClient)"
              matTooltip="Change access"
              i18n-matTooltip
              aria-label="Change access"
              i18n-aria-label
            >
              <fa-icon icon="pen"></fa-icon>
            </button>
            <button
              mat-icon-button
              (click)="regenerateSecret(apiClient)"
              matTooltip="Generate new secret"
              i18n-matTooltip
              aria-label="Generate new secret"
              i18n-aria-label
            >
              <fa-icon icon="key"></fa-icon>
            </button>
            <button
              mat-icon-button
              (click)="delete(apiClient)"
              matTooltip="Delete"
              i18n-matTooltip
              aria-label="Delete"
              i18n-aria-label
            >
              <fa-icon icon="trash"></fa-icon>
            </button>
          </td>
        </ng-container>

        <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
        <tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
      </table>
    </div>
  }

  <div class="margin-top-regular">
    <span class="hint-text" i18n>
      Only API clients created on this page are listed here. Integrations that
      your technical support team set up for you are managed by them.
    </span>
  </div>
} @else {
  <mat-progress-bar mode="indeterminate"></mat-progress-bar>
}

./api-client-list.component.scss

@use "mixins/list-table";

// same table styling as the other admin lists (user accounts, roles)
@include list-table.container-and-table;
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""