@duplicati/ngclient 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/.dockerignore +13 -0
  2. package/.github/workflows/npm-publish.yml +59 -0
  3. package/.prettierignore +13 -0
  4. package/.prettierrc +12 -0
  5. package/.vscode/extensions.json +4 -0
  6. package/.vscode/launch.json +13 -0
  7. package/.vscode/settings.json +8 -0
  8. package/.vscode/tasks.json +24 -0
  9. package/Dockerfile +38 -0
  10. package/LICENSE +21 -0
  11. package/README.md +27 -0
  12. package/angular.json +98 -0
  13. package/bun.lockb +0 -0
  14. package/openapi-ts.config.ts +13 -0
  15. package/package.json +46 -0
  16. package/scripts/generate-package-json.ts +15 -0
  17. package/src/app/about/about.component.html +15 -0
  18. package/src/app/about/about.component.scss +19 -0
  19. package/src/app/about/about.component.ts +13 -0
  20. package/src/app/about/changelog/changelog.component.html +3 -0
  21. package/src/app/about/changelog/changelog.component.scss +7 -0
  22. package/src/app/about/changelog/changelog.component.ts +18 -0
  23. package/src/app/about/general/general.component.html +9 -0
  24. package/src/app/about/general/general.component.scss +23 -0
  25. package/src/app/about/general/general.component.ts +30 -0
  26. package/src/app/about/libraries/libraries.component.html +3 -0
  27. package/src/app/about/libraries/libraries.component.scss +7 -0
  28. package/src/app/about/libraries/libraries.component.ts +18 -0
  29. package/src/app/about/logs/logs.component.html +5 -0
  30. package/src/app/about/logs/logs.component.scss +7 -0
  31. package/src/app/about/logs/logs.component.ts +18 -0
  32. package/src/app/about/system-info/system-info.component.html +3 -0
  33. package/src/app/about/system-info/system-info.component.scss +7 -0
  34. package/src/app/about/system-info/system-info.component.ts +17 -0
  35. package/src/app/add-backup/add-backup.component.html +29 -0
  36. package/src/app/add-backup/add-backup.component.scss +38 -0
  37. package/src/app/add-backup/add-backup.component.ts +14 -0
  38. package/src/app/app.component.ts +35 -0
  39. package/src/app/app.config.ts +23 -0
  40. package/src/app/app.routes.ts +121 -0
  41. package/src/app/core/components/file-tree/file-tree.component.html +87 -0
  42. package/src/app/core/components/file-tree/file-tree.component.scss +80 -0
  43. package/src/app/core/components/file-tree/file-tree.component.ts +485 -0
  44. package/src/app/core/components/logo/logo.component.html +22 -0
  45. package/src/app/core/components/logo/logo.component.scss +12 -0
  46. package/src/app/core/components/logo/logo.component.ts +11 -0
  47. package/src/app/core/components/status-bar/status-bar.component.html +38 -0
  48. package/src/app/core/components/status-bar/status-bar.component.scss +27 -0
  49. package/src/app/core/components/status-bar/status-bar.component.ts +33 -0
  50. package/src/app/core/components/status-bar/status-bar.state.ts +182 -0
  51. package/src/app/core/components/toggle-card/toggle-card.component.html +11 -0
  52. package/src/app/core/components/toggle-card/toggle-card.component.scss +49 -0
  53. package/src/app/core/components/toggle-card/toggle-card.component.ts +28 -0
  54. package/src/app/core/interceptors/http.interceptor.ts +69 -0
  55. package/src/app/core/interceptors/websocket.interceptor.ts +66 -0
  56. package/src/app/core/openapi/core/ApiError.ts +21 -0
  57. package/src/app/core/openapi/core/ApiRequestOptions.ts +21 -0
  58. package/src/app/core/openapi/core/ApiResult.ts +7 -0
  59. package/src/app/core/openapi/core/OpenAPI.ts +55 -0
  60. package/src/app/core/openapi/core/request.ts +337 -0
  61. package/src/app/core/openapi/index.ts +6 -0
  62. package/src/app/core/openapi/schemas.gen.ts +3611 -0
  63. package/src/app/core/openapi/services.gen.ts +1460 -0
  64. package/src/app/core/openapi/types.gen.ts +1510 -0
  65. package/src/app/core/pipes/duration.pipe.ts +27 -0
  66. package/src/app/core/pipes/relative-time.pipe.ts +14 -0
  67. package/src/app/core/providers/dayjs.ts +16 -0
  68. package/src/app/core/services/dict.en.ts +0 -0
  69. package/src/app/core/services/dictionary.service.ts +12 -0
  70. package/src/app/core/services/localstorage.token.ts +82 -0
  71. package/src/app/core/services/password-generator.service.ts +39 -0
  72. package/src/app/core/services/relay-websocket.service.ts +268 -0
  73. package/src/app/core/services/timespan-literals.service.ts +25 -0
  74. package/src/app/core/states/app-auth.state.ts +87 -0
  75. package/src/app/core/states/backups.state.ts +71 -0
  76. package/src/app/core/states/relayconfig.state.ts +23 -0
  77. package/src/app/core/states/sysinfo.state.ts +31 -0
  78. package/src/app/core/states/tasks.state.ts +34 -0
  79. package/src/app/core/types/subscribed.ts +7 -0
  80. package/src/app/core/validators/custom.validators.ts +120 -0
  81. package/src/app/create-edit-backup/create-edit-backup.component.html +28 -0
  82. package/src/app/create-edit-backup/create-edit-backup.component.scss +15 -0
  83. package/src/app/create-edit-backup/create-edit-backup.component.ts +33 -0
  84. package/src/app/create-edit-backup/create-edit-backup.state.ts +582 -0
  85. package/src/app/create-edit-backup/destination/destination.component.html +367 -0
  86. package/src/app/create-edit-backup/destination/destination.component.scss +85 -0
  87. package/src/app/create-edit-backup/destination/destination.component.ts +249 -0
  88. package/src/app/create-edit-backup/destination/destination.config.ts +103 -0
  89. package/src/app/create-edit-backup/destination/destination.mapper.ts +177 -0
  90. package/src/app/create-edit-backup/general/general.component.html +108 -0
  91. package/src/app/create-edit-backup/general/general.component.scss +34 -0
  92. package/src/app/create-edit-backup/general/general.component.ts +136 -0
  93. package/src/app/create-edit-backup/options/options.component.html +95 -0
  94. package/src/app/create-edit-backup/options/options.component.scss +42 -0
  95. package/src/app/create-edit-backup/options/options.component.ts +113 -0
  96. package/src/app/create-edit-backup/schedule/schedule.component.html +102 -0
  97. package/src/app/create-edit-backup/schedule/schedule.component.scss +38 -0
  98. package/src/app/create-edit-backup/schedule/schedule.component.ts +137 -0
  99. package/src/app/create-edit-backup/source-data/source-data.component.html +93 -0
  100. package/src/app/create-edit-backup/source-data/source-data.component.scss +41 -0
  101. package/src/app/create-edit-backup/source-data/source-data.component.ts +114 -0
  102. package/src/app/home/backup.const.ts +167 -0
  103. package/src/app/home/home.component.html +98 -0
  104. package/src/app/home/home.component.scss +88 -0
  105. package/src/app/home/home.component.ts +54 -0
  106. package/src/app/layout/layout.component.html +64 -0
  107. package/src/app/layout/layout.component.scss +24 -0
  108. package/src/app/layout/layout.component.ts +51 -0
  109. package/src/app/layout/layout.state.ts +70 -0
  110. package/src/app/login/login.component.html +12 -0
  111. package/src/app/login/login.component.scss +25 -0
  112. package/src/app/login/login.component.ts +34 -0
  113. package/src/app/logout/logout.component.html +1 -0
  114. package/src/app/logout/logout.component.scss +3 -0
  115. package/src/app/logout/logout.component.ts +18 -0
  116. package/src/app/restore/restore.component.html +56 -0
  117. package/src/app/restore/restore.component.scss +37 -0
  118. package/src/app/restore/restore.component.ts +55 -0
  119. package/src/app/restore-flow/options/options.component.html +57 -0
  120. package/src/app/restore-flow/options/options.component.scss +16 -0
  121. package/src/app/restore-flow/options/options.component.ts +58 -0
  122. package/src/app/restore-flow/restore-flow.component.html +14 -0
  123. package/src/app/restore-flow/restore-flow.component.scss +15 -0
  124. package/src/app/restore-flow/restore-flow.component.ts +23 -0
  125. package/src/app/restore-flow/restore-flow.state.ts +120 -0
  126. package/src/app/restore-flow/select-files/select-files.component.html +42 -0
  127. package/src/app/restore-flow/select-files/select-files.component.scss +16 -0
  128. package/src/app/restore-flow/select-files/select-files.component.ts +129 -0
  129. package/src/app/settings/settings.component.html +256 -0
  130. package/src/app/settings/settings.component.scss +106 -0
  131. package/src/app/settings/settings.component.ts +475 -0
  132. package/src/assets/duplicati-logo.png +0 -0
  133. package/src/assets/duplicati-logo.svg +10 -0
  134. package/src/assets/favicon.ico +0 -0
  135. package/src/assets/images/backup.png +0 -0
  136. package/src/assets/images/dark-theme.png +0 -0
  137. package/src/assets/images/folder.png +0 -0
  138. package/src/assets/images/light-theme.png +0 -0
  139. package/src/assets/images/new-backup.png +0 -0
  140. package/src/assets/images/restore.png +0 -0
  141. package/src/assets/images/system-theme.png +0 -0
  142. package/src/assets/spk.css +33 -0
  143. package/src/assets/spk.woff2 +0 -0
  144. package/src/environments/environment-token.ts +4 -0
  145. package/src/environments/environment.prod.ts +6 -0
  146. package/src/environments/environment.ts +6 -0
  147. package/src/index.html +15 -0
  148. package/src/main.ts +6 -0
  149. package/src/proxy.conf.mjs +6 -0
  150. package/src/styles.scss +41 -0
  151. package/tsconfig.app.json +15 -0
  152. package/tsconfig.json +30 -0
@@ -0,0 +1,38 @@
1
+ <div class="current-backup">
2
+ <div class="current-backup-progress">
3
+ @if (statusData()?.task?.Status && statusData()?.task?.Status !== 'Completed') {
4
+ <div class="label">{{ statusData()?.task?.Status }} - {{ statusData()?.backup?.Backup?.Name }}</div>
5
+
6
+ <spk-progress-bar
7
+ class="raised primary"
8
+ [value]="statusData()?.OverallProgress ? statusData()?.OverallProgress! * 100 : 3"></spk-progress-bar>
9
+ } @else if (nextBackup()) {
10
+ <div class="label">{{ nextBackup().backup?.Backup?.Name }} - {{ nextBackup().time | appRelativeTime }}</div>
11
+ <spk-progress-bar class="raised" [value]="3"></spk-progress-bar>
12
+ } @else if (!statusData() || statusData()?.task?.Status === 'Completed') {
13
+ <div class="label">No scheduled tasks</div>
14
+ <spk-progress-bar class="raised" [value]="3"></spk-progress-bar>
15
+ } @else {
16
+ <div class="label">Loading...</div>
17
+ <spk-progress-bar class="raised" [value]="3"></spk-progress-bar>
18
+ }
19
+ </div>
20
+
21
+ <!-- <div class="current-backup-actions">
22
+ <button spk-button>
23
+ <spk-icon>play-circle</spk-icon>
24
+ Start
25
+ </button>
26
+
27
+ <button spk-button disabled>
28
+ <spk-icon>x-circle</spk-icon>
29
+ </button>
30
+ </div> -->
31
+ </div>
32
+
33
+ <!-- <div class="actions">
34
+ <button spk-button>
35
+ <spk-icon>gauge</spk-icon>
36
+ Throttle settings
37
+ </button>
38
+ </div> -->
@@ -0,0 +1,27 @@
1
+ @use '@sparkle-ui/core/styles/helpers' as *;
2
+
3
+ :host {
4
+ display: flex;
5
+ align-items: center;
6
+ justify-content: space-between;
7
+ padding: p2r(16);
8
+ border-bottom: 1px solid var(--base-level-40);
9
+ }
10
+
11
+ .current-backup {
12
+ display: flex;
13
+ align-items: center;
14
+ gap: p2r(8);
15
+ }
16
+
17
+ .current-backup-progress {
18
+ display: flex;
19
+ flex-direction: column;
20
+ gap: p2r(4);
21
+ min-width: p2r(300);
22
+ }
23
+
24
+ .current-backup-actions {
25
+ display: flex;
26
+ gap: p2r(8);
27
+ }
@@ -0,0 +1,33 @@
1
+ import { JsonPipe } from '@angular/common';
2
+ import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
3
+ import { SparkleButtonComponent, SparkleIconComponent, SparkleProgressBarComponent } from '@sparkle-ui/core';
4
+ import { RelativeTimePipe } from '../../pipes/relative-time.pipe';
5
+ import { StatusBarState } from './status-bar.state';
6
+
7
+ @Component({
8
+ selector: 'app-status-bar',
9
+ standalone: true,
10
+ imports: [SparkleButtonComponent, SparkleIconComponent, SparkleProgressBarComponent, RelativeTimePipe, JsonPipe],
11
+ templateUrl: './status-bar.component.html',
12
+ styleUrl: './status-bar.component.scss',
13
+ changeDetection: ChangeDetectionStrategy.OnPush,
14
+ })
15
+ export default class StatusBarComponent {
16
+ #statusBarState = inject(StatusBarState);
17
+
18
+ statusData = this.#statusBarState.statusData;
19
+ serverState = this.#statusBarState.serverState;
20
+
21
+ nextBackup = computed(() => ({
22
+ backup: (this.serverState()?.ProposedSchedule?.[0] as any)?.backup,
23
+ time: (this.serverState()?.ProposedSchedule?.[0] as any)?.Item2,
24
+ }));
25
+
26
+ ngOnInit() {
27
+ this.#statusBarState.start();
28
+ }
29
+
30
+ ngOnDestroy() {
31
+ this.#statusBarState.stop();
32
+ }
33
+ }
@@ -0,0 +1,182 @@
1
+ import { computed, effect, inject, Injectable, signal } from '@angular/core';
2
+ import { finalize, map, of, switchMap } from 'rxjs';
3
+ import {
4
+ DuplicatiServerService,
5
+ GetApiV1ProgressstateResponse,
6
+ GetApiV1ServerstateData,
7
+ GetApiV1TaskByTaskidResponse,
8
+ ProgressStateService,
9
+ ServerStatusDto,
10
+ } from '../../openapi';
11
+ import { Backup, BackupsState } from '../../states/backups.state';
12
+
13
+ type Task = GetApiV1TaskByTaskidResponse;
14
+
15
+ export type Status = GetApiV1ProgressstateResponse & {
16
+ task?: Task | null;
17
+ backup?: Backup | null;
18
+ };
19
+
20
+ @Injectable({
21
+ providedIn: 'root',
22
+ })
23
+ export class StatusBarState {
24
+ #MIN_POLL_INTERVAL = 1000;
25
+ #progState = inject(ProgressStateService);
26
+ #dupServer = inject(DuplicatiServerService);
27
+ #backupState = inject(BackupsState);
28
+ #isPolling = signal(false);
29
+ #isFetching = signal(false);
30
+ #pollingInterval = signal<number | null>(this.#MIN_POLL_INTERVAL);
31
+ #statusData = signal<Status | null>(null);
32
+ #isGettingServerState = signal(false);
33
+ #serverStateLoading = signal(false);
34
+ #serverState = signal<ServerStatusDto | null>(null);
35
+ #blocker = signal<boolean>(false);
36
+
37
+ statusData = this.#statusData.asReadonly();
38
+ serverState = computed(() => {
39
+ const serverState = this.#serverState();
40
+
41
+ if (!serverState) return null;
42
+
43
+ serverState.ProposedSchedule = serverState?.ProposedSchedule?.map((x) => {
44
+ const backup = x.Item1 ? this.#backupState.getBackupById(x.Item1) : null;
45
+
46
+ return {
47
+ ...x,
48
+ backup,
49
+ };
50
+ });
51
+
52
+ return serverState;
53
+ });
54
+ isFetching = this.#isFetching.asReadonly();
55
+
56
+ #activeInterval: number | null = null;
57
+
58
+ #polling = effect(() => {
59
+ const isPolling = this.#isPolling();
60
+ const pollingInterval = this.#pollingInterval();
61
+
62
+ if (!isPolling || !pollingInterval || pollingInterval < this.#MIN_POLL_INTERVAL) {
63
+ this.#activeInterval && window.clearInterval(this.#activeInterval);
64
+ return;
65
+ }
66
+
67
+ this.#activeInterval = window.setInterval(() => {
68
+ this.#getStatus();
69
+ }, pollingInterval);
70
+ });
71
+
72
+ start() {
73
+ this.#isGettingServerState.set(true);
74
+ this.#getServerState();
75
+ }
76
+
77
+ stop() {
78
+ this.#isGettingServerState.set(false);
79
+ }
80
+
81
+ startPollingProgress() {
82
+ this.#getStatus();
83
+ this.#isPolling.set(true);
84
+ }
85
+
86
+ stopPollingProgress() {
87
+ this.#isPolling.set(false);
88
+ }
89
+
90
+ setPollingInterval(time: number) {
91
+ if (time < this.#MIN_POLL_INTERVAL) {
92
+ console.error(`Polling interval must be at least ${this.#MIN_POLL_INTERVAL}ms`);
93
+
94
+ return;
95
+ }
96
+
97
+ this.#pollingInterval.set(time);
98
+ }
99
+
100
+ serverStateEffect = effect(
101
+ () => {
102
+ if (this.#blocker()) {
103
+ this.stopPollingProgress();
104
+ return;
105
+ }
106
+
107
+ if (!this.#serverStateLoading() && this.#isGettingServerState()) {
108
+ setTimeout(() => {
109
+ this.startPollingProgress();
110
+ this.#getServerState();
111
+ }, 1000);
112
+ } else {
113
+ this.stopPollingProgress();
114
+ }
115
+ },
116
+ {
117
+ allowSignalWrites: true,
118
+ }
119
+ );
120
+
121
+ #getServerState() {
122
+ if (this.#serverStateLoading()) return;
123
+
124
+ this.#serverStateLoading.set(true);
125
+ const lastEventId = this.#serverState()?.LastEventID;
126
+
127
+ const params: GetApiV1ServerstateData = lastEventId === null || lastEventId === undefined
128
+ ? {}
129
+ : {
130
+ lastEventId: lastEventId,
131
+ longpoll: true,
132
+ duration: '299s',
133
+ };
134
+
135
+ this.#dupServer
136
+ .getApiV1Serverstate(params)
137
+ .pipe(finalize(() => this.#serverStateLoading.set(false)))
138
+ .subscribe({
139
+ next: (res) => {
140
+ if (this.#serverState()?.LastDataUpdateID !== res.LastDataUpdateID) {
141
+ this.#backupState.getBackups();
142
+ }
143
+
144
+ this.#serverState.set(res);
145
+ },
146
+ });
147
+ }
148
+
149
+ #getStatus() {
150
+ this.#isFetching.set(true);
151
+ this.#progState
152
+ .getApiV1Progressstate()
153
+ .pipe(
154
+ switchMap((x) => {
155
+ const taskId = x.TaskID ?? null;
156
+
157
+ if (taskId === null) return of(x);
158
+
159
+ return this.#dupServer
160
+ .getApiV1TaskByTaskid({
161
+ taskid: taskId,
162
+ })
163
+ .pipe(map((res) => ({ ...x, task: res ?? null })));
164
+ }),
165
+ finalize(() => this.#isFetching.set(false))
166
+ )
167
+ .subscribe({
168
+ next: (res: Status) => {
169
+ const taskId = res.TaskID ?? null;
170
+ const backupId = res.BackupID ?? null;
171
+
172
+ if (taskId !== null && backupId !== null) {
173
+ const backup = this.#backupState.getBackupById(backupId);
174
+
175
+ res.backup = backup;
176
+ }
177
+
178
+ this.#statusData.set(res);
179
+ },
180
+ });
181
+ }
182
+ }
@@ -0,0 +1,11 @@
1
+ <h3 (click)="disallowToggle() || toggle()">
2
+ <ng-content select="[title]">Title</ng-content>
3
+
4
+ @if (!disallowToggle()) {
5
+ <spk-icon>caret-down</spk-icon>
6
+ }
7
+ </h3>
8
+
9
+ <div class="content">
10
+ <ng-content></ng-content>
11
+ </div>
@@ -0,0 +1,49 @@
1
+ @use '@sparkle-ui/core/styles/helpers' as *;
2
+
3
+ :host {
4
+ display: flex;
5
+ flex-direction: column;
6
+ border-radius: var(--shape-3);
7
+ background-color: var(--base-level-20);
8
+
9
+ &.active {
10
+ .content {
11
+ max-height: 1000px;
12
+ transition: max-height 1s linear;
13
+ padding: p2r(0 16 16);
14
+ }
15
+
16
+ spk-icon {
17
+ transform: rotate(180deg);
18
+ }
19
+ }
20
+
21
+ h3 {
22
+ display: flex;
23
+ align-items: center;
24
+ justify-content: space-between;
25
+ font: var(--paragraph-10);
26
+ padding: p2r(16);
27
+ cursor: pointer;
28
+ user-select: none;
29
+ }
30
+
31
+ spk-icon {
32
+ transform: rotate(0);
33
+ transform-origin: center;
34
+ transition: transform 200ms ease;
35
+ }
36
+ }
37
+
38
+ .content {
39
+ display: flex;
40
+ flex-direction: column;
41
+ gap: p2r(16);
42
+
43
+ padding: p2r(0 16 0);
44
+ overflow: hidden;
45
+ max-height: 0;
46
+ transition:
47
+ max-height 250ms linear,
48
+ padding 250ms linear;
49
+ }
@@ -0,0 +1,28 @@
1
+ import { ChangeDetectionStrategy, Component, input, model } from '@angular/core';
2
+ import { SparkleIconComponent } from '@sparkle-ui/core';
3
+
4
+ @Component({
5
+ selector: 'app-toggle-card',
6
+ standalone: true,
7
+ imports: [SparkleIconComponent],
8
+ templateUrl: './toggle-card.component.html',
9
+ styleUrl: './toggle-card.component.scss',
10
+ changeDetection: ChangeDetectionStrategy.OnPush,
11
+ host: {
12
+ '[class.active]': 'isActive()',
13
+ },
14
+ })
15
+ export default class ToggleCardComponent {
16
+ isActive = model(false);
17
+ disallowToggle = input(false);
18
+
19
+ ngOnInit() {
20
+ if (this.disallowToggle()) {
21
+ this.isActive.set(true);
22
+ }
23
+ }
24
+
25
+ toggle() {
26
+ this.isActive.set(!this.isActive());
27
+ }
28
+ }
@@ -0,0 +1,69 @@
1
+ import { HttpInterceptorFn } from '@angular/common/http';
2
+ import { inject } from '@angular/core';
3
+ import { Router } from '@angular/router';
4
+ import { SparkleAlertService } from '@sparkle-ui/core';
5
+ import { catchError, switchMap, throwError } from 'rxjs';
6
+ import { ENVIRONMENT_TOKEN } from '../../../environments/environment-token';
7
+ import { AccessTokenOutput } from '../openapi';
8
+ import { AppAuthState } from '../states/app-auth.state';
9
+
10
+ let count = 0;
11
+
12
+ export const httpInterceptor: HttpInterceptorFn = (req, next) => {
13
+ const auth = inject(AppAuthState);
14
+ const router = inject(Router);
15
+ const env = inject(ENVIRONMENT_TOKEN);
16
+ const sparkleAlertService = inject(SparkleAlertService);
17
+
18
+ let modifiedRequest = req;
19
+
20
+ // Refresh token if it's older than 15 minutes
21
+ if (auth.tokenSetTime() && Date.now() - auth.tokenSetTime()! > 15 * 60 * 1000 && count < 1) {
22
+ count++;
23
+
24
+ return auth.refreshToken().pipe(
25
+ switchMap((newToken: AccessTokenOutput) => {
26
+ modifiedRequest = req.clone({
27
+ headers: req.headers.set('Authorization', `Bearer ${newToken.AccessToken}`),
28
+ });
29
+
30
+ return next(modifiedRequest);
31
+ })
32
+ );
33
+ }
34
+
35
+ // Add Authorization header if token exists and request URL matches base URL
36
+ if (auth.token() && req.url.startsWith(env.baseUrl)) {
37
+ modifiedRequest = req.clone({
38
+ headers: req.headers.set('Authorization', `Bearer ${auth.token()}`),
39
+ });
40
+ }
41
+
42
+ // Handle HTTP errors
43
+ return next(modifiedRequest).pipe(
44
+ catchError((error) => {
45
+ sparkleAlertService.error(error.message);
46
+
47
+ // If 401 error, attempt to refresh token and retry request (uncomment if needed)
48
+ if (error.status === 401 && count < 1) {
49
+ count++;
50
+
51
+ return auth.refreshToken().pipe(
52
+ switchMap(() => {
53
+ return next(
54
+ req.clone({
55
+ headers: req.headers.set('Authorization', `Bearer ${auth.token()}`),
56
+ })
57
+ );
58
+ })
59
+ );
60
+ } else if (error.status === 401) {
61
+ router.navigate(['/logout']);
62
+ count = 0;
63
+ }
64
+
65
+ const errorMsg = `Error Code: ${error.status}, Message: ${error.message}`;
66
+ return throwError(() => errorMsg);
67
+ })
68
+ );
69
+ };
@@ -0,0 +1,66 @@
1
+ import { HttpEvent, HttpHeaders, HttpInterceptorFn, HttpResponse } from '@angular/common/http';
2
+ import { inject } from '@angular/core';
3
+ import { Observable } from 'rxjs';
4
+ import { ENVIRONMENT_TOKEN } from '../../../environments/environment-token';
5
+ import { RelayWebsocketService, RequestMethod } from '../services/relay-websocket.service';
6
+ import { RelayconfigState } from '../states/relayconfig.state';
7
+
8
+ function bufferToStringBase64(str: any) {
9
+ // TODO - Handle files... (Probably a buffer already and should just be returned as is)
10
+ if (typeof str !== 'string') {
11
+ return null;
12
+ }
13
+
14
+ const encoder = new TextEncoder();
15
+ const uint8Array = encoder.encode(str);
16
+ return window.btoa(String.fromCharCode(...uint8Array));
17
+ }
18
+
19
+ export const httpInterceptorWebsocketRelay: HttpInterceptorFn = (req, next) => {
20
+ const relayconfigState = inject(RelayconfigState);
21
+ const relayconfig = relayconfigState.config();
22
+ const environment = inject(ENVIRONMENT_TOKEN);
23
+
24
+ if (relayconfig === null) return next(req);
25
+
26
+ const relayWebsocket = inject(RelayWebsocketService);
27
+ const bodyBase64 = bufferToStringBase64(req.body);
28
+ const headers: { [key: string]: string } = {};
29
+ req.headers.keys().forEach((key) => {
30
+ headers[key] = req.headers.get(key) ?? '';
31
+ });
32
+
33
+ const timeoutHeaderValue = req.headers.get('timeout');
34
+ const timeoutValue = timeoutHeaderValue !== null ? parseInt(timeoutHeaderValue) : environment.defaultTimeout;
35
+
36
+ const p = relayWebsocket.sendCommand(
37
+ relayconfig.accessToken,
38
+ relayconfig.clientId,
39
+ req.method as RequestMethod,
40
+ req.url,
41
+ bodyBase64,
42
+ headers,
43
+ timeoutValue
44
+ );
45
+
46
+ return new Observable<HttpEvent<any>>((observer) => {
47
+ p.then((response) => {
48
+ const httpHeaders = new HttpHeaders();
49
+ if (headers != null) {
50
+ Object.keys(headers).forEach((key) => {
51
+ httpHeaders.set(key, headers[key]);
52
+ });
53
+ }
54
+
55
+ const httpResponse = new HttpResponse({
56
+ body: response.body,
57
+ status: response.code,
58
+ statusText: 'OK',
59
+ headers: httpHeaders,
60
+ });
61
+
62
+ observer.next(httpResponse);
63
+ observer.complete();
64
+ });
65
+ });
66
+ };
@@ -0,0 +1,21 @@
1
+ import type { ApiRequestOptions } from './ApiRequestOptions';
2
+ import type { ApiResult } from './ApiResult';
3
+
4
+ export class ApiError extends Error {
5
+ public readonly url: string;
6
+ public readonly status: number;
7
+ public readonly statusText: string;
8
+ public readonly body: unknown;
9
+ public readonly request: ApiRequestOptions;
10
+
11
+ constructor(request: ApiRequestOptions, response: ApiResult, message: string) {
12
+ super(message);
13
+
14
+ this.name = 'ApiError';
15
+ this.url = response.url;
16
+ this.status = response.status;
17
+ this.statusText = response.statusText;
18
+ this.body = response.body;
19
+ this.request = request;
20
+ }
21
+ }
@@ -0,0 +1,21 @@
1
+ export type ApiRequestOptions<T = unknown> = {
2
+ readonly body?: any;
3
+ readonly cookies?: Record<string, unknown>;
4
+ readonly errors?: Record<number | string, string>;
5
+ readonly formData?: Record<string, unknown> | any[] | Blob | File;
6
+ readonly headers?: Record<string, unknown>;
7
+ readonly mediaType?: string;
8
+ readonly method:
9
+ | 'DELETE'
10
+ | 'GET'
11
+ | 'HEAD'
12
+ | 'OPTIONS'
13
+ | 'PATCH'
14
+ | 'POST'
15
+ | 'PUT';
16
+ readonly path?: Record<string, unknown>;
17
+ readonly query?: Record<string, unknown>;
18
+ readonly responseHeader?: string;
19
+ readonly responseTransformer?: (data: unknown) => Promise<T>;
20
+ readonly url: string;
21
+ };
@@ -0,0 +1,7 @@
1
+ export type ApiResult<TData = any> = {
2
+ readonly body: TData;
3
+ readonly ok: boolean;
4
+ readonly status: number;
5
+ readonly statusText: string;
6
+ readonly url: string;
7
+ };
@@ -0,0 +1,55 @@
1
+ import type { HttpResponse } from '@angular/common/http';
2
+ import type { ApiRequestOptions } from './ApiRequestOptions';
3
+
4
+ type Headers = Record<string, string>;
5
+ type Middleware<T> = (value: T) => T | Promise<T>;
6
+ type Resolver<T> = (options: ApiRequestOptions<T>) => Promise<T>;
7
+
8
+ export class Interceptors<T> {
9
+ _fns: Middleware<T>[];
10
+
11
+ constructor() {
12
+ this._fns = [];
13
+ }
14
+
15
+ eject(fn: Middleware<T>): void {
16
+ const index = this._fns.indexOf(fn);
17
+ if (index !== -1) {
18
+ this._fns = [...this._fns.slice(0, index), ...this._fns.slice(index + 1)];
19
+ }
20
+ }
21
+
22
+ use(fn: Middleware<T>): void {
23
+ this._fns = [...this._fns, fn];
24
+ }
25
+ }
26
+
27
+ export type OpenAPIConfig = {
28
+ BASE: string;
29
+ CREDENTIALS: 'include' | 'omit' | 'same-origin';
30
+ ENCODE_PATH?: ((path: string) => string) | undefined;
31
+ HEADERS?: Headers | Resolver<Headers> | undefined;
32
+ PASSWORD?: string | Resolver<string> | undefined;
33
+ TOKEN?: string | Resolver<string> | undefined;
34
+ USERNAME?: string | Resolver<string> | undefined;
35
+ VERSION: string;
36
+ WITH_CREDENTIALS: boolean;
37
+ interceptors: {
38
+ response: Interceptors<HttpResponse<any>>;
39
+ };
40
+ };
41
+
42
+ export const OpenAPI: OpenAPIConfig = {
43
+ BASE: '',
44
+ CREDENTIALS: 'include',
45
+ ENCODE_PATH: undefined,
46
+ HEADERS: undefined,
47
+ PASSWORD: undefined,
48
+ TOKEN: undefined,
49
+ USERNAME: undefined,
50
+ VERSION: '1',
51
+ WITH_CREDENTIALS: false,
52
+ interceptors: {
53
+ response: new Interceptors(),
54
+ },
55
+ };