From 517bf80fc871dd73e54dd6e6e3eb23e6d2b45725 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Thu, 12 Mar 2026 13:38:42 -0600 Subject: [PATCH] feat: update start-tunnel web app for typed tunnel API - Use generated TS types for tunnel API params and data models - Simplify API service methods to use typed RPC calls - Update port forward UI for optional labels --- .../app/routes/home/routes/devices/utils.ts | 5 +- .../home/routes/port-forwards/edit-label.ts | 3 +- .../routes/home/routes/port-forwards/utils.ts | 5 +- .../src/app/services/api/api.service.ts | 95 ++++++------------- .../src/app/services/api/live-api.service.ts | 62 ++++++------ .../src/app/services/api/mock-api.service.ts | 65 +++++-------- .../src/app/services/patch-db/data-model.ts | 42 ++------ .../src/app/services/update.service.ts | 7 +- .../ui/src/app/services/api/mock-patch.ts | 2 +- 9 files changed, 107 insertions(+), 179 deletions(-) diff --git a/web/projects/start-tunnel/src/app/routes/home/routes/devices/utils.ts b/web/projects/start-tunnel/src/app/routes/home/routes/devices/utils.ts index 8d3bf45cd..4ef719191 100644 --- a/web/projects/start-tunnel/src/app/routes/home/routes/devices/utils.ts +++ b/web/projects/start-tunnel/src/app/routes/home/routes/devices/utils.ts @@ -1,8 +1,7 @@ import { Signal } from '@angular/core' import { AbstractControl } from '@angular/forms' -import { utils } from '@start9labs/start-sdk' +import { T, utils } from '@start9labs/start-sdk' import { IpNet } from '@start9labs/start-sdk/util' -import { WgServer } from 'src/app/services/patch-db/data-model' export interface MappedDevice { readonly subnet: { @@ -16,7 +15,7 @@ export interface MappedDevice { export interface MappedSubnet { readonly range: string readonly name: string - readonly clients: WgServer['subnets']['']['clients'] + readonly clients: T.Tunnel.WgSubnetClients } export interface DeviceData { diff --git a/web/projects/start-tunnel/src/app/routes/home/routes/port-forwards/edit-label.ts b/web/projects/start-tunnel/src/app/routes/home/routes/port-forwards/edit-label.ts index 3f98f0a74..e0838b42a 100644 --- a/web/projects/start-tunnel/src/app/routes/home/routes/port-forwards/edit-label.ts +++ b/web/projects/start-tunnel/src/app/routes/home/routes/port-forwards/edit-label.ts @@ -15,11 +15,12 @@ import { import { TuiFieldErrorPipe } from '@taiga-ui/kit' import { TuiForm } from '@taiga-ui/layout' import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus' +import { T } from '@start9labs/start-sdk' import { ApiService } from 'src/app/services/api/api.service' export interface EditLabelData { readonly source: string - readonly label: string + readonly label: T.Tunnel.PortForwardEntry['label'] } @Component({ diff --git a/web/projects/start-tunnel/src/app/routes/home/routes/port-forwards/utils.ts b/web/projects/start-tunnel/src/app/routes/home/routes/port-forwards/utils.ts index c9b55f25f..861ea5528 100644 --- a/web/projects/start-tunnel/src/app/routes/home/routes/port-forwards/utils.ts +++ b/web/projects/start-tunnel/src/app/routes/home/routes/port-forwards/utils.ts @@ -1,4 +1,5 @@ import { Signal } from '@angular/core' +import { T } from '@start9labs/start-sdk' export interface MappedDevice { readonly ip: string @@ -10,8 +11,8 @@ export interface MappedForward { readonly externalport: string readonly device: MappedDevice readonly internalport: string - readonly label: string - readonly enabled: boolean + readonly label: T.Tunnel.PortForwardEntry['label'] + readonly enabled: T.Tunnel.PortForwardEntry['enabled'] } export interface PortForwardsData { diff --git a/web/projects/start-tunnel/src/app/services/api/api.service.ts b/web/projects/start-tunnel/src/app/services/api/api.service.ts index cb1b29e57..f25d22b93 100644 --- a/web/projects/start-tunnel/src/app/services/api/api.service.ts +++ b/web/projects/start-tunnel/src/app/services/api/api.service.ts @@ -1,7 +1,8 @@ import { Injectable } from '@angular/core' +import { T } from '@start9labs/start-sdk' import { Dump } from 'patch-db-client' -import { TunnelData } from '../patch-db/data-model' import { Observable } from 'rxjs' +import { TunnelData } from '../patch-db/data-model' @Injectable({ providedIn: 'root', @@ -10,77 +11,43 @@ export abstract class ApiService { abstract openWebsocket$(guid: string): Observable abstract subscribe(): Promise // db.subscribe // auth - abstract login(params: LoginReq): Promise // auth.login + abstract login(params: T.Tunnel.SetPasswordParams): Promise // auth.login abstract logout(): Promise // auth.logout - abstract setPassword(params: LoginReq): Promise // auth.set-password + abstract setPassword(params: T.Tunnel.SetPasswordParams): Promise // auth.set-password // subnets - abstract addSubnet(params: UpsertSubnetReq): Promise // subnet.add - abstract editSubnet(params: UpsertSubnetReq): Promise // subnet.add - abstract deleteSubnet(params: DeleteSubnetReq): Promise // subnet.remove + abstract addSubnet( + params: T.Tunnel.SubnetParams & T.Tunnel.AddSubnetParams, + ): Promise // subnet.add + abstract editSubnet( + params: T.Tunnel.SubnetParams & T.Tunnel.AddSubnetParams, + ): Promise // subnet.edit + abstract deleteSubnet(params: T.Tunnel.SubnetParams): Promise // subnet.remove // devices - abstract addDevice(params: UpsertDeviceReq): Promise // device.add - abstract editDevice(params: UpsertDeviceReq): Promise // device.add - abstract deleteDevice(params: DeleteDeviceReq): Promise // device.remove - abstract showDeviceConfig(params: DeleteDeviceReq): Promise // device.show-config + abstract addDevice(params: T.Tunnel.AddDeviceParams): Promise // device.add + abstract editDevice(params: T.Tunnel.AddDeviceParams): Promise // device.edit + abstract deleteDevice(params: T.Tunnel.RemoveDeviceParams): Promise // device.remove + abstract showDeviceConfig( + params: T.Tunnel.RemoveDeviceParams, + ): Promise // device.show-config // forwards - abstract addForward(params: AddForwardReq): Promise // port-forward.add - abstract deleteForward(params: DeleteForwardReq): Promise // port-forward.remove - abstract updateForwardLabel(params: UpdateForwardLabelReq): Promise // port-forward.update-label - abstract setForwardEnabled(params: SetForwardEnabledReq): Promise // port-forward.set-enabled + abstract addForward( + params: T.Tunnel.AddPortForwardParams, + ): Promise // port-forward.add + abstract deleteForward( + params: T.Tunnel.RemovePortForwardParams, + ): Promise // port-forward.remove + abstract updateForwardLabel( + params: T.Tunnel.UpdatePortForwardLabelParams, + ): Promise // port-forward.update-label + abstract setForwardEnabled( + params: T.Tunnel.SetPortForwardEnabledParams, + ): Promise // port-forward.set-enabled // update - abstract checkUpdate(): Promise // update.check - abstract applyUpdate(): Promise // update.apply + abstract checkUpdate(): Promise // update.check + abstract applyUpdate(): Promise // update.apply } export type SubscribeRes = { dump: Dump guid: string } - -export type LoginReq = { password: string } - -export type UpsertSubnetReq = { - name: string - subnet: string -} - -export type DeleteSubnetReq = { - subnet: string -} - -export type UpsertDeviceReq = { - name: string - subnet: string - ip: string -} - -export type DeleteDeviceReq = { - subnet: string - ip: string -} - -export type AddForwardReq = { - source: string // externalip:port - target: string // internalip:port - label: string -} - -export type DeleteForwardReq = { - source: string -} - -export type UpdateForwardLabelReq = { - source: string - label: string -} - -export type SetForwardEnabledReq = { - source: string - enabled: boolean -} - -export type TunnelUpdateResult = { - status: string - installed: string - candidate: string -} diff --git a/web/projects/start-tunnel/src/app/services/api/live-api.service.ts b/web/projects/start-tunnel/src/app/services/api/live-api.service.ts index f0ed98b97..83aaa8515 100644 --- a/web/projects/start-tunnel/src/app/services/api/live-api.service.ts +++ b/web/projects/start-tunnel/src/app/services/api/live-api.service.ts @@ -8,20 +8,8 @@ import { } from '@start9labs/shared' import { filter, firstValueFrom, Observable } from 'rxjs' import { webSocket } from 'rxjs/webSocket' -import { - AddForwardReq, - ApiService, - DeleteDeviceReq, - DeleteForwardReq, - DeleteSubnetReq, - LoginReq, - SubscribeRes, - TunnelUpdateResult, - SetForwardEnabledReq, - UpdateForwardLabelReq, - UpsertDeviceReq, - UpsertSubnetReq, -} from './api.service' +import { T } from '@start9labs/start-sdk' +import { ApiService, SubscribeRes } from './api.service' import { AuthService } from '../auth.service' import { PATCH_CACHE } from '../patch-db/patch-db-source' @@ -54,7 +42,7 @@ export class LiveApiService extends ApiService { // auth - async login(params: LoginReq): Promise { + async login(params: T.Tunnel.SetPasswordParams): Promise { return this.rpcRequest({ method: 'auth.login', params }) } @@ -62,75 +50,87 @@ export class LiveApiService extends ApiService { return this.rpcRequest({ method: 'auth.logout', params: {} }) } - async setPassword(params: LoginReq): Promise { + async setPassword(params: T.Tunnel.SetPasswordParams): Promise { return this.rpcRequest({ method: 'auth.set-password', params }) } - async addSubnet(params: UpsertSubnetReq): Promise { + async addSubnet( + params: T.Tunnel.SubnetParams & T.Tunnel.AddSubnetParams, + ): Promise { return this.upsertSubnet(params) } - async editSubnet(params: UpsertSubnetReq): Promise { + async editSubnet( + params: T.Tunnel.SubnetParams & T.Tunnel.AddSubnetParams, + ): Promise { return this.upsertSubnet(params) } - async deleteSubnet(params: DeleteSubnetReq): Promise { + async deleteSubnet(params: T.Tunnel.SubnetParams): Promise { return this.rpcRequest({ method: 'subnet.remove', params }) } // devices - async addDevice(params: UpsertDeviceReq): Promise { + async addDevice(params: T.Tunnel.AddDeviceParams): Promise { return this.upsertDevice(params) } - async editDevice(params: UpsertDeviceReq): Promise { + async editDevice(params: T.Tunnel.AddDeviceParams): Promise { return this.upsertDevice(params) } - async deleteDevice(params: DeleteDeviceReq): Promise { + async deleteDevice(params: T.Tunnel.RemoveDeviceParams): Promise { return this.rpcRequest({ method: 'device.remove', params }) } - async showDeviceConfig(params: DeleteDeviceReq): Promise { + async showDeviceConfig(params: T.Tunnel.RemoveDeviceParams): Promise { return this.rpcRequest({ method: 'device.show-config', params }) } // forwards - async addForward(params: AddForwardReq): Promise { + async addForward(params: T.Tunnel.AddPortForwardParams): Promise { return this.rpcRequest({ method: 'port-forward.add', params }) } - async deleteForward(params: DeleteForwardReq): Promise { + async deleteForward( + params: T.Tunnel.RemovePortForwardParams, + ): Promise { return this.rpcRequest({ method: 'port-forward.remove', params }) } - async updateForwardLabel(params: UpdateForwardLabelReq): Promise { + async updateForwardLabel( + params: T.Tunnel.UpdatePortForwardLabelParams, + ): Promise { return this.rpcRequest({ method: 'port-forward.update-label', params }) } - async setForwardEnabled(params: SetForwardEnabledReq): Promise { + async setForwardEnabled( + params: T.Tunnel.SetPortForwardEnabledParams, + ): Promise { return this.rpcRequest({ method: 'port-forward.set-enabled', params }) } // update - async checkUpdate(): Promise { + async checkUpdate(): Promise { return this.rpcRequest({ method: 'update.check', params: {} }) } - async applyUpdate(): Promise { + async applyUpdate(): Promise { return this.rpcRequest({ method: 'update.apply', params: {} }) } // private - private async upsertSubnet(params: UpsertSubnetReq): Promise { + private async upsertSubnet( + params: T.Tunnel.SubnetParams & T.Tunnel.AddSubnetParams, + ): Promise { return this.rpcRequest({ method: 'subnet.add', params }) } - private async upsertDevice(params: UpsertDeviceReq): Promise { + private async upsertDevice(params: T.Tunnel.AddDeviceParams): Promise { return this.rpcRequest({ method: 'device.add', params }) } diff --git a/web/projects/start-tunnel/src/app/services/api/mock-api.service.ts b/web/projects/start-tunnel/src/app/services/api/mock-api.service.ts index d24562b9d..ed3ebe95e 100644 --- a/web/projects/start-tunnel/src/app/services/api/mock-api.service.ts +++ b/web/projects/start-tunnel/src/app/services/api/mock-api.service.ts @@ -1,21 +1,9 @@ import { inject, Injectable } from '@angular/core' import { shareReplay, Subject, tap } from 'rxjs' import { WebSocketSubject } from 'rxjs/webSocket' -import { - AddForwardReq, - ApiService, - DeleteDeviceReq, - DeleteForwardReq, - DeleteSubnetReq, - LoginReq, - SubscribeRes, - TunnelUpdateResult, - SetForwardEnabledReq, - UpdateForwardLabelReq, - UpsertDeviceReq, - UpsertSubnetReq, -} from './api.service' +import { ApiService, SubscribeRes } from './api.service' import { pauseFor } from '@start9labs/shared' +import { T } from '@start9labs/start-sdk' import { AuthService } from '../auth.service' import { AddOperation, @@ -26,12 +14,7 @@ import { Revision, } from 'patch-db-client' import { toObservable } from '@angular/core/rxjs-interop' -import { - mockTunnelData, - PortForwardEntry, - WgClient, - WgSubnet, -} from '../patch-db/data-model' +import { mockTunnelData } from '../patch-db/data-model' @Injectable({ providedIn: 'root', @@ -66,7 +49,7 @@ export class MockApiService extends ApiService { } } - async login(params: LoginReq): Promise { + async login(params: T.Tunnel.SetPasswordParams): Promise { await pauseFor(1000) return null } @@ -76,15 +59,15 @@ export class MockApiService extends ApiService { return null } - async setPassword(params: LoginReq): Promise { + async setPassword(params: T.Tunnel.SetPasswordParams): Promise { await pauseFor(1000) return null } - async addSubnet(params: UpsertSubnetReq): Promise { + async addSubnet(params: T.Tunnel.SubnetParams & T.Tunnel.AddSubnetParams): Promise { await pauseFor(1000) - const patch: AddOperation[] = [ + const patch: AddOperation[] = [ { op: PatchOp.ADD, path: `/wg/subnets/${replaceSlashes(params.subnet)}`, @@ -96,7 +79,7 @@ export class MockApiService extends ApiService { return null } - async editSubnet(params: UpsertSubnetReq): Promise { + async editSubnet(params: T.Tunnel.SubnetParams & T.Tunnel.AddSubnetParams): Promise { await pauseFor(1000) const patch: ReplaceOperation[] = [ @@ -111,7 +94,7 @@ export class MockApiService extends ApiService { return null } - async deleteSubnet(params: DeleteSubnetReq): Promise { + async deleteSubnet(params: T.Tunnel.SubnetParams): Promise { await pauseFor(1000) const patch: RemoveOperation[] = [ @@ -125,14 +108,14 @@ export class MockApiService extends ApiService { return null } - async addDevice(params: UpsertDeviceReq): Promise { + async addDevice(params: T.Tunnel.AddDeviceParams): Promise { await pauseFor(1000) - const patch: AddOperation[] = [ + const patch: AddOperation[] = [ { op: PatchOp.ADD, path: `/wg/subnets/${replaceSlashes(params.subnet)}/clients/${params.ip}`, - value: { name: params.name }, + value: { name: params.name, key: '', psk: '' }, }, ] this.mockRevision(patch) @@ -140,7 +123,7 @@ export class MockApiService extends ApiService { return null } - async editDevice(params: UpsertDeviceReq): Promise { + async editDevice(params: T.Tunnel.AddDeviceParams): Promise { await pauseFor(1000) const patch: ReplaceOperation[] = [ @@ -155,7 +138,7 @@ export class MockApiService extends ApiService { return null } - async deleteDevice(params: DeleteDeviceReq): Promise { + async deleteDevice(params: T.Tunnel.RemoveDeviceParams): Promise { await pauseFor(1000) const patch: RemoveOperation[] = [ @@ -169,22 +152,22 @@ export class MockApiService extends ApiService { return null } - async showDeviceConfig(params: DeleteDeviceReq): Promise { + async showDeviceConfig(params: T.Tunnel.RemoveDeviceParams): Promise { await pauseFor(1000) return MOCK_CONFIG } - async addForward(params: AddForwardReq): Promise { + async addForward(params: T.Tunnel.AddPortForwardParams): Promise { await pauseFor(1000) - const patch: AddOperation[] = [ + const patch: AddOperation[] = [ { op: PatchOp.ADD, path: `/portForwards/${params.source}`, value: { target: params.target, - label: params.label || '', + label: params.label || null, enabled: true, }, }, @@ -194,10 +177,10 @@ export class MockApiService extends ApiService { return null } - async updateForwardLabel(params: UpdateForwardLabelReq): Promise { + async updateForwardLabel(params: T.Tunnel.UpdatePortForwardLabelParams): Promise { await pauseFor(1000) - const patch: ReplaceOperation[] = [ + const patch: ReplaceOperation[] = [ { op: PatchOp.REPLACE, path: `/portForwards/${params.source}/label`, @@ -209,7 +192,7 @@ export class MockApiService extends ApiService { return null } - async setForwardEnabled(params: SetForwardEnabledReq): Promise { + async setForwardEnabled(params: T.Tunnel.SetPortForwardEnabledParams): Promise { await pauseFor(1000) const patch: ReplaceOperation[] = [ @@ -224,7 +207,7 @@ export class MockApiService extends ApiService { return null } - async deleteForward(params: DeleteForwardReq): Promise { + async deleteForward(params: T.Tunnel.RemovePortForwardParams): Promise { await pauseFor(1000) const patch: RemoveOperation[] = [ @@ -238,7 +221,7 @@ export class MockApiService extends ApiService { return null } - async checkUpdate(): Promise { + async checkUpdate(): Promise { await pauseFor(1000) return { status: 'update-available', @@ -247,7 +230,7 @@ export class MockApiService extends ApiService { } } - async applyUpdate(): Promise { + async applyUpdate(): Promise { await pauseFor(2000) return { status: 'updating', diff --git a/web/projects/start-tunnel/src/app/services/patch-db/data-model.ts b/web/projects/start-tunnel/src/app/services/patch-db/data-model.ts index 8bb5e23e0..0ac9d0c30 100644 --- a/web/projects/start-tunnel/src/app/services/patch-db/data-model.ts +++ b/web/projects/start-tunnel/src/app/services/patch-db/data-model.ts @@ -1,45 +1,21 @@ import { T } from '@start9labs/start-sdk' -export type PortForwardEntry = { - target: string - label: string - enabled: boolean -} - -export type TunnelData = { - wg: WgServer - portForwards: Record - gateways: Record -} - -export type WgServer = { - subnets: Record -} - -export type WgSubnet = { - name: string - clients: Record -} - -export type WgClient = { - name: string -} +export type TunnelData = Pick< + T.Tunnel.TunnelDatabase, + 'wg' | 'portForwards' | 'gateways' +> export const mockTunnelData: TunnelData = { wg: { + port: 51820, + key: '', subnets: { '10.59.0.0/24': { name: 'Family', clients: { - '10.59.0.2': { - name: 'Start9 Server', - }, - '10.59.0.3': { - name: 'Phone', - }, - '10.59.0.4': { - name: 'Laptop', - }, + '10.59.0.2': { name: 'Start9 Server', key: '', psk: '' }, + '10.59.0.3': { name: 'Phone', key: '', psk: '' }, + '10.59.0.4': { name: 'Laptop', key: '', psk: '' }, }, }, }, diff --git a/web/projects/start-tunnel/src/app/services/update.service.ts b/web/projects/start-tunnel/src/app/services/update.service.ts index 861b5c057..b246579ef 100644 --- a/web/projects/start-tunnel/src/app/services/update.service.ts +++ b/web/projects/start-tunnel/src/app/services/update.service.ts @@ -14,7 +14,8 @@ import { switchMap, takeWhile, } from 'rxjs' -import { ApiService, TunnelUpdateResult } from './api/api.service' +import { T } from '@start9labs/start-sdk' +import { ApiService } from './api/api.service' import { AuthService } from './auth.service' @Component({ @@ -34,7 +35,7 @@ export class UpdateService { private readonly dialogs = inject(TuiDialogService) private readonly errorService = inject(ErrorService) - readonly result = signal(null) + readonly result = signal(null) readonly hasUpdate = computed( () => this.result()?.status === 'update-available', ) @@ -60,7 +61,7 @@ export class UpdateService { this.setResult(result) } - private setResult(result: TunnelUpdateResult): void { + private setResult(result: T.Tunnel.TunnelUpdateResult): void { this.result.set(result) if (result.status === 'updating') { diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index 557310204..56bb797c9 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -239,7 +239,7 @@ export const mockPatchData: DataModel = { caFingerprint: '63:2B:11:99:44:40:17:DF:37:FC:C3:DF:0F:3D:15', ntpSynced: false, smtp: null, - ifconfigUrl: 'https://ifconfig.co', + echoipUrls: ['https://ipconfig.me', 'https://ifconfig.co'], platform: 'x86_64-nonfree', zram: true, governor: 'performance',