-
diff --git a/frontend/src/app/components/home/settings/api-keys/api-keys.component.css b/frontend/src/app/components/home/settings/api-keys/api-keys.component.css
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/src/app/components/home/settings/api-keys/api-keys.component.html b/frontend/src/app/components/home/settings/api-keys/api-keys.component.html
new file mode 100644
index 0000000..17ee3b3
--- /dev/null
+++ b/frontend/src/app/components/home/settings/api-keys/api-keys.component.html
@@ -0,0 +1,144 @@
+
+
+
+
+
+
+
+
+
+
+
+ @if (apiKeysStore.isLoading()) {
+
+
+
+
+
+
+ } @else if (!apiKeysStore.apiKey()) {
+
+
+
+
+ vpn_key
+
+
+
No API Key Generated
+
+ Generate an API key to start integrating with our services
+
+
+
+
+ } @else {
+
+
+
+
API Key
+
+
+ {{ apiKeysStore.apiKey()?.key }}
+
+
+
+
+
+
+ @if (showSecret() && apiKeysStore.apiKey()?.secret) {
+
+
Secret Key
+
+
+ {{ apiKeysStore.apiKey()?.secret }}
+
+
+
+
+ warning
+ Save this secret key now. You won't be able to see it again!
+
+
+
+
+
+
+
+ }
+
+
+
+
Generated at
+
+ {{ (apiKeysStore.apiKey()?.created_at ?? '') | custom_date : "" : true }}
+
+
+
+ }
+
+
+
+
+
+
+
+ @if (!apiKeysStore.apiKey() && !apiKeysStore.isLoading()) {
+
+
+ } @else if (apiKeysStore.apiKey()) {
+
+
+ }
+
+
+
+
+
+
+
+
+
+
info
+
+
API Key Usage
+
+ - Use this API key to authenticate your requests
+ - Keep your API key and secret secure
+ - Regenerating will invalidate the previous key
+
+
+
+
diff --git a/frontend/src/app/components/home/settings/api-keys/api-keys.component.ts b/frontend/src/app/components/home/settings/api-keys/api-keys.component.ts
new file mode 100644
index 0000000..50ab605
--- /dev/null
+++ b/frontend/src/app/components/home/settings/api-keys/api-keys.component.ts
@@ -0,0 +1,128 @@
+import { Component, OnInit, effect, inject, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { MatButtonModule } from '@angular/material/button';
+import { MatDividerModule } from '@angular/material/divider';
+import { MatCardModule } from '@angular/material/card';
+import { MatIconModule } from '@angular/material/icon';
+import { MatTooltipModule } from '@angular/material/tooltip';
+import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
+import { MatDialog } from '@angular/material/dialog';
+import { ApiKeysStore } from './store/api-keys.store';
+import { OrganizationStore } from '../../../../store/organization.store';
+import { CustomDatePipe } from '../../../../pipe/date.pipe';
+import { SnackbarService } from '../../../../services/snackbar.service';
+import { DeleteDialogComponent } from '../../common/delete-dialog/delete-dialog.component';
+import { Clipboard } from '@angular/cdk/clipboard';
+import { ApiKeyDto } from '../../../../../dtos/api-key.dto';
+
+@Component({
+ selector: 'app-api-keys',
+ standalone: true,
+ imports: [
+ CommonModule,
+ MatButtonModule,
+ MatDividerModule,
+ MatCardModule,
+ MatIconModule,
+ MatTooltipModule,
+ NgxSkeletonLoaderModule,
+ CustomDatePipe,
+ ],
+ templateUrl: './api-keys.component.html',
+})
+export class ApiKeysComponent implements OnInit {
+ apiKeysStore = inject(ApiKeysStore);
+ organizationStore = inject(OrganizationStore);
+ dialog = inject(MatDialog);
+ snack = inject(SnackbarService);
+ clipboard = inject(Clipboard);
+
+ showSecret = signal(false);
+
+ constructor() {
+ // Watch for newly generated API key to show secret
+ effect(() => {
+ const apiKey = this.apiKeysStore.apiKey();
+ if (apiKey?.secret) {
+ this.showSecret.set(true);
+ }
+ });
+ }
+
+ ngOnInit(): void {
+ const organizationId = this.organizationStore.organizaiton()?.organizationId;
+
+ if (organizationId) {
+ this.apiKeysStore.fetchApiKey({ organizationId });
+ }
+ }
+
+ onGenerateApiKey(): void {
+ const organizationId = this.organizationStore.organizaiton()?.organizationId;
+
+ if (!organizationId) {
+ this.snack.openSnackBar('Organization not found', undefined);
+ return;
+ }
+
+ this.showSecret.set(false);
+ this.apiKeysStore.generateApiKey({ organizationId });
+ }
+
+ onRegenerateApiKey(): void {
+ const org = this.organizationStore.organizaiton();
+
+ if (!org?.organizationId) {
+ this.snack.openSnackBar('Organization not found', undefined);
+ return;
+ }
+
+ this.dialog.open(DeleteDialogComponent, {
+ width: '448px',
+ data: {
+ title: 'Regenerate API Key?',
+ description: 'Are you sure you want to regenerate your API key? This will invalidate your current key and any applications using it will stop working until you update them with the new key.',
+ buttonText: 'Regenerate',
+ onDelete: async () => {
+ try {
+ this.showSecret.set(false);
+ await this.apiKeysStore.generateApiKey({ organizationId: org.organizationId! });
+ this.dialog.closeAll();
+ } catch (err) {
+ this.snack.openSnackBar('Failed to regenerate API key', undefined);
+ }
+ }
+ }
+ });
+ }
+
+ copyToClipboard(text: string): void {
+ const success = this.clipboard.copy(text);
+
+ if (success) {
+ this.snack.openSnackBar('Copied to clipboard', undefined);
+ } else {
+ this.snack.openSnackBar('Failed to copy', undefined);
+ }
+ }
+
+ downloadTxt(apiKey: ApiKeyDto): void {
+ if (!apiKey) return;
+
+ const textContent =
+ `API Key: ${apiKey.key || ''}\n` +
+ `Secret: ${apiKey.secret || ''}\n` +
+ `Created At: ${apiKey.created_at || ''}\n`;
+
+ const blob = new Blob([textContent], { type: 'text/plain;charset=utf-8;' });
+ const url = URL.createObjectURL(blob);
+
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = 'api-key.txt';
+ link.click();
+
+ URL.revokeObjectURL(url);
+ }
+
+}
diff --git a/frontend/src/app/components/home/settings/api-keys/store/api-keys.store.ts b/frontend/src/app/components/home/settings/api-keys/store/api-keys.store.ts
new file mode 100644
index 0000000..79f7a51
--- /dev/null
+++ b/frontend/src/app/components/home/settings/api-keys/store/api-keys.store.ts
@@ -0,0 +1,114 @@
+import { inject } from '@angular/core';
+import { patchState, signalStore, withMethods, withState } from '@ngrx/signals';
+import { rxMethod } from '@ngrx/signals/rxjs-interop';
+import { catchError, concatMap, EMPTY, pipe, tap } from 'rxjs';
+import { tapResponse } from '@ngrx/operators';
+import { withDevtools } from '@angular-architects/ngrx-toolkit';
+import { ApiKeysService } from '../../../../../services/api-keys.service';
+import { ApiKeyDto } from '../../../../../../dtos/api-key.dto';
+import { SnackbarService } from '../../../../../services/snackbar.service';
+
+type ApiKeysState = {
+ apiKey: ApiKeyDto | null;
+ isLoading: boolean;
+ error: string | null;
+};
+
+const initialState: ApiKeysState = {
+ apiKey: null,
+ isLoading: false,
+ error: null,
+};
+
+export const ApiKeysStore = signalStore(
+ { providedIn: 'root' },
+ withDevtools('api-keys'),
+ withState(initialState),
+
+ withMethods((store, apiKeysService = inject(ApiKeysService), snack = inject(SnackbarService)) => ({
+ /** ---------------------------------------------------------
+ * FETCH API KEY
+ * --------------------------------------------------------- */
+ fetchApiKey: rxMethod<{ organizationId: string }>(
+ pipe(
+ tap(() => patchState(store, { isLoading: true, error: null })),
+
+ concatMap(({ organizationId }) =>
+ apiKeysService.fetchApiKey(organizationId).pipe(
+ tapResponse({
+ next: (response) => {
+ patchState(store, {
+ apiKey: response.data ?? null,
+ isLoading: false,
+ });
+ },
+ error: (error: any) => {
+ patchState(store, {
+ isLoading: false,
+ error: error?.message || 'Failed to fetch API key',
+ });
+
+ snack.openSnackBar(error?.message || 'Failed to fetch API key', undefined);
+ },
+ }),
+
+ catchError((error) => {
+ patchState(store, {
+ isLoading: false,
+ error: error?.message || 'Failed to fetch API key',
+ });
+ return EMPTY;
+ })
+ )
+ )
+ )
+ ),
+
+ /** ---------------------------------------------------------
+ * GENERATE API KEY
+ * --------------------------------------------------------- */
+ generateApiKey: rxMethod<{ organizationId: string }>(
+ pipe(
+ tap(() => patchState(store, { isLoading: true, error: null })),
+
+ concatMap(({ organizationId }) =>
+ apiKeysService.generateApiKey(organizationId).pipe(
+ tapResponse({
+ next: (response) => {
+ patchState(store, {
+ apiKey: response.data ?? null,
+ isLoading: false,
+ });
+
+ snack.openSnackBar('API key generated successfully', undefined);
+ },
+ error: (error: any) => {
+ patchState(store, {
+ isLoading: false,
+ error: error?.message || 'Failed to generate API key',
+ });
+
+ snack.openSnackBar(error?.message || 'Failed to generate API key', undefined);
+ },
+ }),
+
+ catchError((error) => {
+ patchState(store, {
+ isLoading: false,
+ error: error?.message || 'Failed to generate API key',
+ });
+ return EMPTY;
+ })
+ )
+ )
+ )
+ ),
+
+ /** ---------------------------------------------------------
+ * RESET STATE
+ * --------------------------------------------------------- */
+ resetState() {
+ patchState(store, initialState);
+ },
+ }))
+);
diff --git a/frontend/src/app/components/home/settings/settings.component.html b/frontend/src/app/components/home/settings/settings.component.html
index 428865d..97397f3 100644
--- a/frontend/src/app/components/home/settings/settings.component.html
+++ b/frontend/src/app/components/home/settings/settings.component.html
@@ -26,4 +26,11 @@
+
+
+