mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
port labels and move logout to settings
This commit is contained in:
@@ -11,6 +11,7 @@ use crate::db::model::public::NetworkInterfaceType;
|
|||||||
use crate::net::forward::add_iptables_rule;
|
use crate::net::forward::add_iptables_rule;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::tunnel::context::TunnelContext;
|
use crate::tunnel::context::TunnelContext;
|
||||||
|
use crate::tunnel::db::PortForwardEntry;
|
||||||
use crate::tunnel::wg::{WIREGUARD_INTERFACE_NAME, WgConfig, WgSubnetClients, WgSubnetConfig};
|
use crate::tunnel::wg::{WIREGUARD_INTERFACE_NAME, WgConfig, WgSubnetClients, WgSubnetConfig};
|
||||||
use crate::util::serde::{HandlerExtSerde, display_serializable};
|
use crate::util::serde::{HandlerExtSerde, display_serializable};
|
||||||
|
|
||||||
@@ -51,6 +52,14 @@ pub fn tunnel_api<C: Context>() -> ParentHandler<C> {
|
|||||||
.no_display()
|
.no_display()
|
||||||
.with_about("about.remove-port-forward")
|
.with_about("about.remove-port-forward")
|
||||||
.with_call_remote::<CliContext>(),
|
.with_call_remote::<CliContext>(),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
"update-label",
|
||||||
|
from_fn_async(update_forward_label)
|
||||||
|
.with_metadata("sync_db", Value::Bool(true))
|
||||||
|
.no_display()
|
||||||
|
.with_about("about.update-port-forward-label")
|
||||||
|
.with_call_remote::<CliContext>(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
@@ -453,11 +462,17 @@ pub async fn show_config(
|
|||||||
pub struct AddPortForwardParams {
|
pub struct AddPortForwardParams {
|
||||||
source: SocketAddrV4,
|
source: SocketAddrV4,
|
||||||
target: SocketAddrV4,
|
target: SocketAddrV4,
|
||||||
|
#[arg(long)]
|
||||||
|
label: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_forward(
|
pub async fn add_forward(
|
||||||
ctx: TunnelContext,
|
ctx: TunnelContext,
|
||||||
AddPortForwardParams { source, target }: AddPortForwardParams,
|
AddPortForwardParams {
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
label,
|
||||||
|
}: AddPortForwardParams,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let prefix = ctx
|
let prefix = ctx
|
||||||
.net_iface
|
.net_iface
|
||||||
@@ -482,10 +497,12 @@ pub async fn add_forward(
|
|||||||
m.insert(source, rc);
|
m.insert(source, rc);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let entry = PortForwardEntry { target, label };
|
||||||
|
|
||||||
ctx.db
|
ctx.db
|
||||||
.mutate(|db| {
|
.mutate(|db| {
|
||||||
db.as_port_forwards_mut()
|
db.as_port_forwards_mut()
|
||||||
.insert(&source, &target)
|
.insert(&source, &entry)
|
||||||
.and_then(|replaced| {
|
.and_then(|replaced| {
|
||||||
if replaced.is_some() {
|
if replaced.is_some() {
|
||||||
Err(Error::new(
|
Err(Error::new(
|
||||||
@@ -523,3 +540,31 @@ pub async fn remove_forward(
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Parser)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct UpdatePortForwardLabelParams {
|
||||||
|
source: SocketAddrV4,
|
||||||
|
label: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_forward_label(
|
||||||
|
ctx: TunnelContext,
|
||||||
|
UpdatePortForwardLabelParams { source, label }: UpdatePortForwardLabelParams,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
ctx.db
|
||||||
|
.mutate(|db| {
|
||||||
|
db.as_port_forwards_mut().mutate(|pf| {
|
||||||
|
let entry = pf.0.get_mut(&source).ok_or_else(|| {
|
||||||
|
Error::new(
|
||||||
|
eyre!("Port forward from {source} not found"),
|
||||||
|
ErrorKind::NotFound,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
entry.label = label.clone();
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.result
|
||||||
|
}
|
||||||
|
|||||||
@@ -184,7 +184,8 @@ impl TunnelContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut active_forwards = BTreeMap::new();
|
let mut active_forwards = BTreeMap::new();
|
||||||
for (from, to) in peek.as_port_forwards().de()?.0 {
|
for (from, entry) in peek.as_port_forwards().de()?.0 {
|
||||||
|
let to = entry.target;
|
||||||
let prefix = net_iface
|
let prefix = net_iface
|
||||||
.peek(|i| {
|
.peek(|i| {
|
||||||
i.iter()
|
i.iter()
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ impl Model<TunnelDatabase> {
|
|||||||
}
|
}
|
||||||
self.as_port_forwards_mut().mutate(|pf| {
|
self.as_port_forwards_mut().mutate(|pf| {
|
||||||
Ok(pf.0.retain(|k, v| {
|
Ok(pf.0.retain(|k, v| {
|
||||||
if keep_targets.contains(v.ip()) {
|
if keep_targets.contains(v.target.ip()) {
|
||||||
keep_sources.insert(*k);
|
keep_sources.insert(*k);
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
@@ -70,11 +70,19 @@ fn export_bindings_tunnel_db() {
|
|||||||
TunnelDatabase::export_all_to("bindings/tunnel").unwrap();
|
TunnelDatabase::export_all_to("bindings/tunnel").unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct PortForwardEntry {
|
||||||
|
pub target: SocketAddrV4,
|
||||||
|
#[serde(default)]
|
||||||
|
pub label: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, TS)]
|
#[derive(Clone, Debug, Default, Deserialize, Serialize, TS)]
|
||||||
pub struct PortForwards(pub BTreeMap<SocketAddrV4, SocketAddrV4>);
|
pub struct PortForwards(pub BTreeMap<SocketAddrV4, PortForwardEntry>);
|
||||||
impl Map for PortForwards {
|
impl Map for PortForwards {
|
||||||
type Key = SocketAddrV4;
|
type Key = SocketAddrV4;
|
||||||
type Value = SocketAddrV4;
|
type Value = PortForwardEntry;
|
||||||
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
|
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
|
||||||
Self::key_string(key)
|
Self::key_string(key)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
import { Router, RouterLink, RouterLinkActive } from '@angular/router'
|
import { RouterLink, RouterLinkActive } from '@angular/router'
|
||||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
|
||||||
import { TuiButton } from '@taiga-ui/core'
|
import { TuiButton } from '@taiga-ui/core'
|
||||||
import { TuiBadgeNotification } from '@taiga-ui/kit'
|
import { TuiBadgeNotification } from '@taiga-ui/kit'
|
||||||
import { ApiService } from 'src/app/services/api/api.service'
|
|
||||||
import { AuthService } from 'src/app/services/auth.service'
|
|
||||||
import { SidebarService } from 'src/app/services/sidebar.service'
|
import { SidebarService } from 'src/app/services/sidebar.service'
|
||||||
import { UpdateService } from 'src/app/services/update.service'
|
import { UpdateService } from 'src/app/services/update.service'
|
||||||
|
|
||||||
@@ -38,15 +35,6 @@ import { UpdateService } from 'src/app/services/update.service'
|
|||||||
}
|
}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
tuiButton
|
|
||||||
iconStart="@tui.log-out"
|
|
||||||
appearance="neutral"
|
|
||||||
size="s"
|
|
||||||
(click)="logout()"
|
|
||||||
>
|
|
||||||
Logout
|
|
||||||
</button>
|
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
:host {
|
:host {
|
||||||
@@ -79,12 +67,6 @@ import { UpdateService } from 'src/app/services/update.service'
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 0;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
:host-context(tui-root._mobile) {
|
:host-context(tui-root._mobile) {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 3.5rem;
|
top: 3.5rem;
|
||||||
@@ -106,12 +88,7 @@ import { UpdateService } from 'src/app/services/update.service'
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
export class Nav {
|
export class Nav {
|
||||||
private readonly service = inject(AuthService)
|
|
||||||
private readonly router = inject(Router)
|
|
||||||
protected readonly sidebars = inject(SidebarService)
|
protected readonly sidebars = inject(SidebarService)
|
||||||
protected readonly api = inject(ApiService)
|
|
||||||
private readonly loader = inject(LoadingService)
|
|
||||||
private readonly errorService = inject(ErrorService)
|
|
||||||
protected readonly update = inject(UpdateService)
|
protected readonly update = inject(UpdateService)
|
||||||
|
|
||||||
protected readonly routes = [
|
protected readonly routes = [
|
||||||
@@ -131,18 +108,4 @@ export class Nav {
|
|||||||
link: 'port-forwards',
|
link: 'port-forwards',
|
||||||
},
|
},
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
protected async logout() {
|
|
||||||
const loader = this.loader.open().subscribe()
|
|
||||||
try {
|
|
||||||
await this.api.logout()
|
|
||||||
this.service.authenticated.set(false)
|
|
||||||
this.router.navigate(['.'])
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e)
|
|
||||||
this.errorService.handleError(e)
|
|
||||||
} finally {
|
|
||||||
loader.unsubscribe()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ import { MappedDevice, PortForwardsData } from './utils'
|
|||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
<form tuiForm [formGroup]="form">
|
<form tuiForm [formGroup]="form">
|
||||||
|
<tui-textfield>
|
||||||
|
<label tuiLabel>Label</label>
|
||||||
|
<input tuiTextfield formControlName="label" />
|
||||||
|
</tui-textfield>
|
||||||
|
<tui-error formControlName="label" [error]="[] | tuiFieldError | async" />
|
||||||
<tui-textfield tuiChevron>
|
<tui-textfield tuiChevron>
|
||||||
<label tuiLabel>External IP</label>
|
<label tuiLabel>External IP</label>
|
||||||
@if (mobile) {
|
@if (mobile) {
|
||||||
@@ -161,6 +166,7 @@ export class PortForwardsAdd {
|
|||||||
injectContext<TuiDialogContext<void, PortForwardsData>>()
|
injectContext<TuiDialogContext<void, PortForwardsData>>()
|
||||||
|
|
||||||
protected readonly form = inject(NonNullableFormBuilder).group({
|
protected readonly form = inject(NonNullableFormBuilder).group({
|
||||||
|
label: ['', Validators.required],
|
||||||
externalip: ['', Validators.required],
|
externalip: ['', Validators.required],
|
||||||
externalport: [null as number | null, Validators.required],
|
externalport: [null as number | null, Validators.required],
|
||||||
device: [null as MappedDevice | null, Validators.required],
|
device: [null as MappedDevice | null, Validators.required],
|
||||||
@@ -185,19 +191,21 @@ export class PortForwardsAdd {
|
|||||||
|
|
||||||
const loader = this.loading.open().subscribe()
|
const loader = this.loading.open().subscribe()
|
||||||
|
|
||||||
const { externalip, externalport, device, internalport, also80 } =
|
const { label, externalip, externalport, device, internalport, also80 } =
|
||||||
this.form.getRawValue()
|
this.form.getRawValue()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.api.addForward({
|
await this.api.addForward({
|
||||||
source: `${externalip}:${externalport}`,
|
source: `${externalip}:${externalport}`,
|
||||||
target: `${device!.ip}:${internalport}`,
|
target: `${device!.ip}:${internalport}`,
|
||||||
|
label,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (externalport === 443 && internalport === 443 && also80) {
|
if (externalport === 443 && internalport === 443 && also80) {
|
||||||
await this.api.addForward({
|
await this.api.addForward({
|
||||||
source: `${externalip}:80`,
|
source: `${externalip}:80`,
|
||||||
target: `${device!.ip}:443`,
|
target: `${device!.ip}:443`,
|
||||||
|
label: `${label} (HTTP redirect)`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { AsyncPipe } from '@angular/common'
|
||||||
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
|
import {
|
||||||
|
NonNullableFormBuilder,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
Validators,
|
||||||
|
} from '@angular/forms'
|
||||||
|
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||||
|
import {
|
||||||
|
TuiButton,
|
||||||
|
TuiDialogContext,
|
||||||
|
TuiError,
|
||||||
|
TuiTextfield,
|
||||||
|
} from '@taiga-ui/core'
|
||||||
|
import { TuiFieldErrorPipe } from '@taiga-ui/kit'
|
||||||
|
import { TuiForm } from '@taiga-ui/layout'
|
||||||
|
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||||
|
import { ApiService } from 'src/app/services/api/api.service'
|
||||||
|
|
||||||
|
export interface EditLabelData {
|
||||||
|
readonly source: string
|
||||||
|
readonly label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: `
|
||||||
|
<form tuiForm [formGroup]="form">
|
||||||
|
<tui-textfield>
|
||||||
|
<label tuiLabel>Label</label>
|
||||||
|
<input tuiTextfield formControlName="label" />
|
||||||
|
</tui-textfield>
|
||||||
|
<tui-error formControlName="label" [error]="[] | tuiFieldError | async" />
|
||||||
|
<footer>
|
||||||
|
<button tuiButton [disabled]="form.invalid" (click)="onSave()">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
`,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [
|
||||||
|
AsyncPipe,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
TuiButton,
|
||||||
|
TuiError,
|
||||||
|
TuiFieldErrorPipe,
|
||||||
|
TuiTextfield,
|
||||||
|
TuiForm,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class PortForwardsEditLabel {
|
||||||
|
private readonly api = inject(ApiService)
|
||||||
|
private readonly loading = inject(LoadingService)
|
||||||
|
private readonly errorService = inject(ErrorService)
|
||||||
|
|
||||||
|
protected readonly context =
|
||||||
|
injectContext<TuiDialogContext<void, EditLabelData>>()
|
||||||
|
|
||||||
|
protected readonly form = inject(NonNullableFormBuilder).group({
|
||||||
|
label: [this.context.data.label, Validators.required],
|
||||||
|
})
|
||||||
|
|
||||||
|
protected async onSave() {
|
||||||
|
const loader = this.loading.open().subscribe()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.api.updateForwardLabel({
|
||||||
|
source: this.context.data.source,
|
||||||
|
label: this.form.getRawValue().label,
|
||||||
|
})
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e)
|
||||||
|
this.errorService.handleError(e)
|
||||||
|
} finally {
|
||||||
|
loader.unsubscribe()
|
||||||
|
this.context.$implicit.complete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PORT_FORWARDS_EDIT_LABEL = new PolymorpheusComponent(
|
||||||
|
PortForwardsEditLabel,
|
||||||
|
)
|
||||||
@@ -6,15 +6,20 @@ import {
|
|||||||
Signal,
|
Signal,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { toSignal } from '@angular/core/rxjs-interop'
|
import { toSignal } from '@angular/core/rxjs-interop'
|
||||||
import { ReactiveFormsModule } from '@angular/forms'
|
|
||||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||||
import { utils } from '@start9labs/start-sdk'
|
import { utils } from '@start9labs/start-sdk'
|
||||||
import { TuiButton } from '@taiga-ui/core'
|
import {
|
||||||
|
TuiButton,
|
||||||
|
TuiDataList,
|
||||||
|
TuiDropdown,
|
||||||
|
TuiTextfield,
|
||||||
|
} from '@taiga-ui/core'
|
||||||
import { TuiDialogService } from '@taiga-ui/experimental'
|
import { TuiDialogService } from '@taiga-ui/experimental'
|
||||||
import { TUI_CONFIRM } from '@taiga-ui/kit'
|
import { TUI_CONFIRM } from '@taiga-ui/kit'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { filter, map } from 'rxjs'
|
import { filter, map } from 'rxjs'
|
||||||
import { PORT_FORWARDS_ADD } from 'src/app/routes/home/routes/port-forwards/add'
|
import { PORT_FORWARDS_ADD } from 'src/app/routes/home/routes/port-forwards/add'
|
||||||
|
import { PORT_FORWARDS_EDIT_LABEL } from 'src/app/routes/home/routes/port-forwards/edit-label'
|
||||||
import { ApiService } from 'src/app/services/api/api.service'
|
import { ApiService } from 'src/app/services/api/api.service'
|
||||||
import { TunnelData } from 'src/app/services/patch-db/data-model'
|
import { TunnelData } from 'src/app/services/patch-db/data-model'
|
||||||
|
|
||||||
@@ -25,6 +30,7 @@ import { MappedDevice, MappedForward } from './utils'
|
|||||||
<table class="g-table">
|
<table class="g-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th>Label</th>
|
||||||
<th>External IP</th>
|
<th>External IP</th>
|
||||||
<th>External Port</th>
|
<th>External Port</th>
|
||||||
<th>Device</th>
|
<th>Device</th>
|
||||||
@@ -39,6 +45,7 @@ import { MappedDevice, MappedForward } from './utils'
|
|||||||
<tbody>
|
<tbody>
|
||||||
@for (forward of forwards(); track $index) {
|
@for (forward of forwards(); track $index) {
|
||||||
<tr>
|
<tr>
|
||||||
|
<td>{{ forward.label || '—' }}</td>
|
||||||
<td>{{ forward.externalip }}</td>
|
<td>{{ forward.externalip }}</td>
|
||||||
<td>{{ forward.externalport }}</td>
|
<td>{{ forward.externalport }}</td>
|
||||||
<td>{{ forward.device.name }}</td>
|
<td>{{ forward.device.name }}</td>
|
||||||
@@ -47,11 +54,30 @@ import { MappedDevice, MappedForward } from './utils'
|
|||||||
<button
|
<button
|
||||||
tuiIconButton
|
tuiIconButton
|
||||||
size="xs"
|
size="xs"
|
||||||
|
tuiDropdown
|
||||||
|
tuiDropdownOpen
|
||||||
appearance="flat-grayscale"
|
appearance="flat-grayscale"
|
||||||
iconStart="@tui.trash"
|
iconStart="@tui.ellipsis-vertical"
|
||||||
(click)="onDelete(forward)"
|
|
||||||
>
|
>
|
||||||
Actions
|
Actions
|
||||||
|
<tui-data-list *tuiTextfieldDropdown size="s">
|
||||||
|
<button
|
||||||
|
tuiOption
|
||||||
|
iconStart="@tui.pencil"
|
||||||
|
new
|
||||||
|
(click)="onEditLabel(forward)"
|
||||||
|
>
|
||||||
|
{{ forward.label ? 'Rename' : 'Add label' }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
tuiOption
|
||||||
|
iconStart="@tui.trash"
|
||||||
|
new
|
||||||
|
(click)="onDelete(forward)"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</tui-data-list>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -62,7 +88,7 @@ import { MappedDevice, MappedForward } from './utils'
|
|||||||
</table>
|
</table>
|
||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [ReactiveFormsModule, TuiButton],
|
imports: [TuiButton, TuiDropdown, TuiDataList, TuiTextfield],
|
||||||
})
|
})
|
||||||
export default class PortForwards {
|
export default class PortForwards {
|
||||||
private readonly dialogs = inject(TuiDialogService)
|
private readonly dialogs = inject(TuiDialogService)
|
||||||
@@ -100,15 +126,16 @@ export default class PortForwards {
|
|||||||
)
|
)
|
||||||
|
|
||||||
protected readonly forwards = computed(() =>
|
protected readonly forwards = computed(() =>
|
||||||
Object.entries(this.portForwards() || {}).map(([source, target]) => {
|
Object.entries(this.portForwards() || {}).map(([source, entry]) => {
|
||||||
const sourceSplit = source.split(':')
|
const sourceSplit = source.split(':')
|
||||||
const targetSplit = target.split(':')
|
const targetSplit = entry.target.split(':')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
externalip: sourceSplit[0]!,
|
externalip: sourceSplit[0]!,
|
||||||
externalport: sourceSplit[1]!,
|
externalport: sourceSplit[1]!,
|
||||||
device: this.devices().find(d => d.ip === targetSplit[0])!,
|
device: this.devices().find(d => d.ip === targetSplit[0])!,
|
||||||
internalport: targetSplit[1]!,
|
internalport: targetSplit[1]!,
|
||||||
|
label: entry.label,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -122,6 +149,18 @@ export default class PortForwards {
|
|||||||
.subscribe()
|
.subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected onEditLabel(forward: MappedForward): void {
|
||||||
|
this.dialogs
|
||||||
|
.open(PORT_FORWARDS_EDIT_LABEL, {
|
||||||
|
label: 'Edit label',
|
||||||
|
data: {
|
||||||
|
source: `${forward.externalip}:${forward.externalport}`,
|
||||||
|
label: forward.label,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
protected onDelete({ externalip, externalport }: MappedForward): void {
|
protected onDelete({ externalip, externalport }: MappedForward): void {
|
||||||
this.dialogs
|
this.dialogs
|
||||||
.open(TUI_CONFIRM, { label: 'Are you sure?' })
|
.open(TUI_CONFIRM, { label: 'Are you sure?' })
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export interface MappedForward {
|
|||||||
readonly externalport: string
|
readonly externalport: string
|
||||||
readonly device: MappedDevice
|
readonly device: MappedDevice
|
||||||
readonly internalport: string
|
readonly internalport: string
|
||||||
|
readonly label: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PortForwardsData {
|
export interface PortForwardsData {
|
||||||
|
|||||||
@@ -4,11 +4,14 @@ import {
|
|||||||
inject,
|
inject,
|
||||||
signal,
|
signal,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { ErrorService } from '@start9labs/shared'
|
import { Router } from '@angular/router'
|
||||||
|
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||||
import { TuiAppearance, TuiButton, TuiTitle } from '@taiga-ui/core'
|
import { TuiAppearance, TuiButton, TuiTitle } from '@taiga-ui/core'
|
||||||
import { TuiDialogService } from '@taiga-ui/experimental'
|
import { TuiDialogService } from '@taiga-ui/experimental'
|
||||||
import { TuiBadge, TuiButtonLoading } from '@taiga-ui/kit'
|
import { TuiBadge, TuiButtonLoading } from '@taiga-ui/kit'
|
||||||
import { TuiCard, TuiCell } from '@taiga-ui/layout'
|
import { TuiCard, TuiCell } from '@taiga-ui/layout'
|
||||||
|
import { ApiService } from 'src/app/services/api/api.service'
|
||||||
|
import { AuthService } from 'src/app/services/auth.service'
|
||||||
import { UpdateService } from 'src/app/services/update.service'
|
import { UpdateService } from 'src/app/services/update.service'
|
||||||
|
|
||||||
import { CHANGE_PASSWORD } from './change-password'
|
import { CHANGE_PASSWORD } from './change-password'
|
||||||
@@ -50,6 +53,20 @@ import { CHANGE_PASSWORD } from './change-password'
|
|||||||
</span>
|
</span>
|
||||||
<button tuiButton size="s" (click)="onChangePassword()">Change</button>
|
<button tuiButton size="s" (click)="onChangePassword()">Change</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div tuiCell>
|
||||||
|
<span tuiTitle>
|
||||||
|
<strong>Logout</strong>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
tuiButton
|
||||||
|
size="s"
|
||||||
|
appearance="secondary-destructive"
|
||||||
|
iconStart="@tui.log-out"
|
||||||
|
(click)="onLogout()"
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
@@ -66,6 +83,10 @@ import { CHANGE_PASSWORD } from './change-password'
|
|||||||
export default class Settings {
|
export default class Settings {
|
||||||
private readonly dialogs = inject(TuiDialogService)
|
private readonly dialogs = inject(TuiDialogService)
|
||||||
private readonly errorService = inject(ErrorService)
|
private readonly errorService = inject(ErrorService)
|
||||||
|
private readonly api = inject(ApiService)
|
||||||
|
private readonly auth = inject(AuthService)
|
||||||
|
private readonly router = inject(Router)
|
||||||
|
private readonly loading = inject(LoadingService)
|
||||||
|
|
||||||
protected readonly update = inject(UpdateService)
|
protected readonly update = inject(UpdateService)
|
||||||
protected readonly checking = signal(false)
|
protected readonly checking = signal(false)
|
||||||
@@ -98,4 +119,18 @@ export default class Settings {
|
|||||||
this.applying.set(false)
|
this.applying.set(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async onLogout() {
|
||||||
|
const loader = this.loading.open().subscribe()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.api.logout()
|
||||||
|
this.auth.authenticated.set(false)
|
||||||
|
this.router.navigate(['/'])
|
||||||
|
} catch (e: any) {
|
||||||
|
this.errorService.handleError(e)
|
||||||
|
} finally {
|
||||||
|
loader.unsubscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export abstract class ApiService {
|
|||||||
// forwards
|
// forwards
|
||||||
abstract addForward(params: AddForwardReq): Promise<null> // port-forward.add
|
abstract addForward(params: AddForwardReq): Promise<null> // port-forward.add
|
||||||
abstract deleteForward(params: DeleteForwardReq): Promise<null> // port-forward.remove
|
abstract deleteForward(params: DeleteForwardReq): Promise<null> // port-forward.remove
|
||||||
|
abstract updateForwardLabel(params: UpdateForwardLabelReq): Promise<null> // port-forward.update-label
|
||||||
// update
|
// update
|
||||||
abstract checkUpdate(): Promise<TunnelUpdateResult> // update.check
|
abstract checkUpdate(): Promise<TunnelUpdateResult> // update.check
|
||||||
abstract applyUpdate(): Promise<TunnelUpdateResult> // update.apply
|
abstract applyUpdate(): Promise<TunnelUpdateResult> // update.apply
|
||||||
@@ -60,12 +61,18 @@ export type DeleteDeviceReq = {
|
|||||||
export type AddForwardReq = {
|
export type AddForwardReq = {
|
||||||
source: string // externalip:port
|
source: string // externalip:port
|
||||||
target: string // internalip:port
|
target: string // internalip:port
|
||||||
|
label: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DeleteForwardReq = {
|
export type DeleteForwardReq = {
|
||||||
source: string
|
source: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UpdateForwardLabelReq = {
|
||||||
|
source: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
export type TunnelUpdateResult = {
|
export type TunnelUpdateResult = {
|
||||||
status: string
|
status: string
|
||||||
installed: string
|
installed: string
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
LoginReq,
|
LoginReq,
|
||||||
SubscribeRes,
|
SubscribeRes,
|
||||||
TunnelUpdateResult,
|
TunnelUpdateResult,
|
||||||
|
UpdateForwardLabelReq,
|
||||||
UpsertDeviceReq,
|
UpsertDeviceReq,
|
||||||
UpsertSubnetReq,
|
UpsertSubnetReq,
|
||||||
} from './api.service'
|
} from './api.service'
|
||||||
@@ -104,6 +105,10 @@ export class LiveApiService extends ApiService {
|
|||||||
return this.rpcRequest({ method: 'port-forward.remove', params })
|
return this.rpcRequest({ method: 'port-forward.remove', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateForwardLabel(params: UpdateForwardLabelReq): Promise<null> {
|
||||||
|
return this.rpcRequest({ method: 'port-forward.update-label', params })
|
||||||
|
}
|
||||||
|
|
||||||
// update
|
// update
|
||||||
|
|
||||||
async checkUpdate(): Promise<TunnelUpdateResult> {
|
async checkUpdate(): Promise<TunnelUpdateResult> {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
LoginReq,
|
LoginReq,
|
||||||
SubscribeRes,
|
SubscribeRes,
|
||||||
TunnelUpdateResult,
|
TunnelUpdateResult,
|
||||||
|
UpdateForwardLabelReq,
|
||||||
UpsertDeviceReq,
|
UpsertDeviceReq,
|
||||||
UpsertSubnetReq,
|
UpsertSubnetReq,
|
||||||
} from './api.service'
|
} from './api.service'
|
||||||
@@ -24,7 +25,12 @@ import {
|
|||||||
Revision,
|
Revision,
|
||||||
} from 'patch-db-client'
|
} from 'patch-db-client'
|
||||||
import { toObservable } from '@angular/core/rxjs-interop'
|
import { toObservable } from '@angular/core/rxjs-interop'
|
||||||
import { mockTunnelData, WgClient, WgSubnet } from '../patch-db/data-model'
|
import {
|
||||||
|
mockTunnelData,
|
||||||
|
PortForwardEntry,
|
||||||
|
WgClient,
|
||||||
|
WgSubnet,
|
||||||
|
} from '../patch-db/data-model'
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -171,11 +177,26 @@ export class MockApiService extends ApiService {
|
|||||||
async addForward(params: AddForwardReq): Promise<null> {
|
async addForward(params: AddForwardReq): Promise<null> {
|
||||||
await pauseFor(1000)
|
await pauseFor(1000)
|
||||||
|
|
||||||
const patch: AddOperation<string>[] = [
|
const patch: AddOperation<PortForwardEntry>[] = [
|
||||||
{
|
{
|
||||||
op: PatchOp.ADD,
|
op: PatchOp.ADD,
|
||||||
path: `/portForwards/${params.source}`,
|
path: `/portForwards/${params.source}`,
|
||||||
value: params.target,
|
value: { target: params.target, label: params.label || '' },
|
||||||
|
},
|
||||||
|
]
|
||||||
|
this.mockRevision(patch)
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateForwardLabel(params: UpdateForwardLabelReq): Promise<null> {
|
||||||
|
await pauseFor(1000)
|
||||||
|
|
||||||
|
const patch: ReplaceOperation<string>[] = [
|
||||||
|
{
|
||||||
|
op: PatchOp.REPLACE,
|
||||||
|
path: `/portForwards/${params.source}/label`,
|
||||||
|
value: params.label,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
this.mockRevision(patch)
|
this.mockRevision(patch)
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import { T } from '@start9labs/start-sdk'
|
import { T } from '@start9labs/start-sdk'
|
||||||
|
|
||||||
|
export type PortForwardEntry = {
|
||||||
|
target: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
export type TunnelData = {
|
export type TunnelData = {
|
||||||
wg: WgServer
|
wg: WgServer
|
||||||
portForwards: Record<string, string>
|
portForwards: Record<string, PortForwardEntry>
|
||||||
gateways: Record<string, T.NetworkInterfaceInfo>
|
gateways: Record<string, T.NetworkInterfaceInfo>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,8 +44,8 @@ export const mockTunnelData: TunnelData = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
portForwards: {
|
portForwards: {
|
||||||
'69.1.1.42:443': '10.59.0.2:443',
|
'69.1.1.42:443': { target: '10.59.0.2:443', label: 'HTTPS' },
|
||||||
'69.1.1.42:3000': '10.59.0.2:3000',
|
'69.1.1.42:3000': { target: '10.59.0.2:3000', label: 'Grafana' },
|
||||||
},
|
},
|
||||||
gateways: {
|
gateways: {
|
||||||
eth0: {
|
eth0: {
|
||||||
|
|||||||
Reference in New Issue
Block a user