mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 04:01:58 +00:00
more translations
This commit is contained in:
@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { SwUpdate } from '@angular/service-worker'
|
||||
import { WA_WINDOW } from '@ng-web-apis/common'
|
||||
import { LoadingService } from '@start9labs/shared'
|
||||
import { i18nPipe, LoadingService } from '@start9labs/shared'
|
||||
import { Version } from '@start9labs/start-sdk'
|
||||
import { TuiResponsiveDialog } from '@taiga-ui/addon-mobile'
|
||||
import { TuiAutoFocus } from '@taiga-ui/cdk'
|
||||
@@ -12,21 +12,23 @@ import { distinctUntilChanged, map, merge, Subject } from 'rxjs'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
// @TODO translations
|
||||
|
||||
@Component({
|
||||
selector: 'refresh-alert',
|
||||
template: `
|
||||
<ng-template
|
||||
[tuiResponsiveDialog]="show()"
|
||||
[tuiResponsiveDialogOptions]="{ label: 'Refresh Needed', size: 's' }"
|
||||
[tuiResponsiveDialogOptions]="{
|
||||
label: i18n.transform('Refresh Needed'),
|
||||
size: 's',
|
||||
}"
|
||||
(tuiResponsiveDialogChange)="dismiss$.next()"
|
||||
>
|
||||
@if (isPwa) {
|
||||
<p>
|
||||
Your user interface is cached and out of date. Attempt to reload the
|
||||
PWA using the button below. If you continue to see this message,
|
||||
uninstall and reinstall the PWA.
|
||||
{{
|
||||
'Your user interface is cached and out of date. Attempt to reload the PWA using the button below. If you continue to see this message, uninstall and reinstall the PWA.'
|
||||
| i18n
|
||||
}}
|
||||
</p>
|
||||
<button
|
||||
tuiButton
|
||||
@@ -36,11 +38,13 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
[tuiAppearanceFocus]="false"
|
||||
(click)="pwaReload()"
|
||||
>
|
||||
Reload
|
||||
{{ 'Refresh' | i18n }}
|
||||
</button>
|
||||
} @else {
|
||||
Your user interface is cached and out of date. Hard refresh the page to
|
||||
get the latest UI.
|
||||
{{
|
||||
'Your user interface is cached and out of date. Hard refresh the page to get the latest UI.'
|
||||
| i18n
|
||||
}}
|
||||
<ul>
|
||||
<li>
|
||||
<b>On Mac</b>
|
||||
@@ -59,13 +63,13 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
[tuiAppearanceFocus]="false"
|
||||
(click)="dismiss$.next()"
|
||||
>
|
||||
Ok
|
||||
{{ 'Ok' | i18n }}
|
||||
</button>
|
||||
}
|
||||
</ng-template>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiResponsiveDialog, TuiButton, TuiAutoFocus],
|
||||
imports: [TuiResponsiveDialog, TuiButton, TuiAutoFocus, i18nPipe],
|
||||
})
|
||||
export class RefreshAlertComponent {
|
||||
private readonly win = inject(WA_WINDOW)
|
||||
@@ -73,6 +77,8 @@ export class RefreshAlertComponent {
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly version = Version.parse(inject(ConfigService).version)
|
||||
|
||||
readonly i18n = inject(i18nPipe)
|
||||
|
||||
readonly dismiss$ = new Subject<void>()
|
||||
readonly isPwa = this.win.matchMedia('(display-mode: standalone)').matches
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@ import { PublicDomainsComponent } from './public-domains/pd.component'
|
||||
import { InterfacePrivateDomainsComponent } from './private-domains.component'
|
||||
import { InterfaceAddressesComponent } from './addresses/addresses.component'
|
||||
|
||||
// @TODO translations
|
||||
|
||||
@Component({
|
||||
selector: 'service-interface',
|
||||
template: `
|
||||
|
||||
@@ -3,7 +3,7 @@ import { T, utils } from '@start9labs/start-sdk'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { GatewayPlus } from 'src/app/services/gateway.service'
|
||||
import { PublicDomain } from './public-domains/pd.service'
|
||||
import { i18nKey } from '@start9labs/shared'
|
||||
import { i18nKey, i18nPipe } from '@start9labs/shared'
|
||||
|
||||
type AddressWithInfo = {
|
||||
url: URL
|
||||
@@ -98,155 +98,6 @@ function cmpClearnet(
|
||||
])
|
||||
}
|
||||
|
||||
// @TODO translations
|
||||
function toDisplayAddress(
|
||||
{ info, url }: AddressWithInfo,
|
||||
gateways: GatewayPlus[],
|
||||
publicDomains: Record<string, T.PublicDomainConfig>,
|
||||
): DisplayAddress {
|
||||
let access: DisplayAddress['access']
|
||||
let gatewayName: DisplayAddress['gatewayName']
|
||||
let type: DisplayAddress['type']
|
||||
let bullets: any[]
|
||||
// let bullets: DisplayAddress['bullets']
|
||||
|
||||
const rootCaRequired = `Requires trusting your server's Root CA`
|
||||
|
||||
// ** Tor **
|
||||
if (info.kind === 'onion') {
|
||||
access = null
|
||||
gatewayName = null
|
||||
type = 'Tor'
|
||||
bullets = [
|
||||
'Connections can be slow or unreliable at times',
|
||||
'Public if you share the address publicly, otherwise private',
|
||||
'Requires using a Tor-enabled device or browser',
|
||||
]
|
||||
// Tor (HTTPS)
|
||||
if (url.protocol.startsWith('https')) {
|
||||
type = `${type} (HTTPS)`
|
||||
bullets = [
|
||||
'Only useful for clients that enforce HTTPS',
|
||||
rootCaRequired,
|
||||
...bullets,
|
||||
]
|
||||
// Tor (HTTP)
|
||||
} else {
|
||||
bullets.unshift(
|
||||
'Ideal for anonymous, censorship-resistant hosting and remote access',
|
||||
)
|
||||
type = `${type} (HTTP)`
|
||||
}
|
||||
// ** Not Tor **
|
||||
} else {
|
||||
const port = info.hostname.sslPort || info.hostname.port
|
||||
const gateway = gateways.find(g => g.id === info.gatewayId)!
|
||||
gatewayName = gateway.ipInfo.name
|
||||
|
||||
const gatewayLanIpv4 = gateway.lanIpv4[0]
|
||||
const isWireguard = gateway.ipInfo.deviceType === 'wireguard'
|
||||
|
||||
const localIdeal = 'Ideal for local access'
|
||||
const lanRequired =
|
||||
'Requires being connected to the same Local Area Network (LAN) as your server, either physically or via VPN'
|
||||
const staticRequired = `Requires setting a static IP address for ${gatewayLanIpv4} in your gateway`
|
||||
const vpnAccess = 'Ideal for VPN access via your'
|
||||
|
||||
// * Local *
|
||||
if (info.hostname.kind === 'local') {
|
||||
type = 'Local'
|
||||
access = 'private'
|
||||
bullets = [
|
||||
localIdeal,
|
||||
'Not recommended for VPN access. VPNs do not support ".local" domains without advanced configuration',
|
||||
lanRequired,
|
||||
rootCaRequired,
|
||||
]
|
||||
// * IPv4 *
|
||||
} else if (info.hostname.kind === 'ipv4') {
|
||||
type = 'IPv4'
|
||||
if (info.public) {
|
||||
access = 'public'
|
||||
bullets = [
|
||||
'Can be used for clearnet access',
|
||||
'Not recommended in most cases. Clearnet domains are preferred',
|
||||
rootCaRequired,
|
||||
]
|
||||
if (!gateway.public) {
|
||||
bullets.push(
|
||||
`Requires port forwarding in gateway "${gatewayName}": ${port} -> ${info.hostname.value}:${port}`,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
access = 'private'
|
||||
if (isWireguard) {
|
||||
bullets = [`${vpnAccess} StartTunnel (or similar)`, rootCaRequired]
|
||||
} else {
|
||||
bullets = [
|
||||
localIdeal,
|
||||
`${vpnAccess} router's Wireguard server`,
|
||||
lanRequired,
|
||||
rootCaRequired,
|
||||
staticRequired,
|
||||
]
|
||||
}
|
||||
}
|
||||
// * IPv6 *
|
||||
} else if (info.hostname.kind === 'ipv6') {
|
||||
type = 'IPv6'
|
||||
access = 'private'
|
||||
bullets = ['Can be used for local access', lanRequired, rootCaRequired]
|
||||
// * Domain *
|
||||
} else {
|
||||
type = 'Domain'
|
||||
if (info.public) {
|
||||
access = 'public'
|
||||
bullets = [
|
||||
`Requires a DNS record for ${info.hostname.value} that resolves to ${gateway.ipInfo.wanIp}`,
|
||||
`Requires port forwarding in gateway "${gatewayName}": ${port} -> ${info.hostname.value}:${port === 443 ? 5443 : port}`,
|
||||
]
|
||||
if (publicDomains[info.hostname.value]?.acme) {
|
||||
bullets.unshift('Ideal for public access via the Internet')
|
||||
} else {
|
||||
bullets = [
|
||||
'Can be used for personal access via the public Internet. VPN is more private and secure',
|
||||
rootCaRequired,
|
||||
...bullets,
|
||||
]
|
||||
}
|
||||
} else {
|
||||
access = 'private'
|
||||
const ipPortBad = 'when using IP addresses and ports is undesirable'
|
||||
const customDnsRequired = `Requires a DNS record for ${info.hostname.value} that resolves to ${gatewayLanIpv4}`
|
||||
if (isWireguard) {
|
||||
bullets = [
|
||||
`${vpnAccess} StartTunnel (or similar) ${ipPortBad}`,
|
||||
customDnsRequired,
|
||||
rootCaRequired,
|
||||
]
|
||||
} else {
|
||||
bullets = [
|
||||
`${localIdeal} ${ipPortBad}`,
|
||||
`${vpnAccess} router's Wireguard server ${ipPortBad}`,
|
||||
customDnsRequired,
|
||||
rootCaRequired,
|
||||
lanRequired,
|
||||
staticRequired,
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
url: url.href,
|
||||
access,
|
||||
gatewayName,
|
||||
type,
|
||||
bullets,
|
||||
}
|
||||
}
|
||||
|
||||
export function getPublicDomains(
|
||||
publicDomains: Record<string, T.PublicDomainConfig>,
|
||||
gateways: GatewayPlus[],
|
||||
@@ -263,6 +114,7 @@ export function getPublicDomains(
|
||||
})
|
||||
export class InterfaceService {
|
||||
private readonly config = inject(ConfigService)
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
|
||||
getAddresses(
|
||||
serviceInterface: T.ServiceInterface,
|
||||
@@ -304,11 +156,11 @@ export class InterfaceService {
|
||||
|
||||
return {
|
||||
common: bestAddrs.map(a =>
|
||||
toDisplayAddress(a, gateways, host.publicDomains),
|
||||
this.toDisplayAddress(a, gateways, host.publicDomains),
|
||||
),
|
||||
uncommon: allAddressesWithInfo
|
||||
.filter(a => !bestAddrs.includes(a))
|
||||
.map(a => toDisplayAddress(a, gateways, host.publicDomains)),
|
||||
.map(a => this.toDisplayAddress(a, gateways, host.publicDomains)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,6 +300,183 @@ export class InterfaceService {
|
||||
) || []
|
||||
)
|
||||
}
|
||||
|
||||
private toDisplayAddress(
|
||||
{ info, url }: AddressWithInfo,
|
||||
gateways: GatewayPlus[],
|
||||
publicDomains: Record<string, T.PublicDomainConfig>,
|
||||
): DisplayAddress {
|
||||
let access: DisplayAddress['access']
|
||||
let gatewayName: DisplayAddress['gatewayName']
|
||||
let type: DisplayAddress['type']
|
||||
let bullets: any[]
|
||||
// let bullets: DisplayAddress['bullets']
|
||||
|
||||
const rootCaRequired = this.i18n.transform(
|
||||
"Requires trusting your server's Root CA",
|
||||
)
|
||||
|
||||
// ** Tor **
|
||||
if (info.kind === 'onion') {
|
||||
access = null
|
||||
gatewayName = null
|
||||
type = 'Tor'
|
||||
bullets = [
|
||||
this.i18n.transform('Connections can be slow or unreliable at times'),
|
||||
this.i18n.transform(
|
||||
'Public if you share the address publicly, otherwise private',
|
||||
),
|
||||
this.i18n.transform('Requires using a Tor-enabled device or browser'),
|
||||
]
|
||||
// Tor (HTTPS)
|
||||
if (url.protocol.startsWith('https')) {
|
||||
type = `${type} (HTTPS)`
|
||||
bullets = [
|
||||
this.i18n.transform('Only useful for clients that enforce HTTPS'),
|
||||
rootCaRequired,
|
||||
...bullets,
|
||||
]
|
||||
// Tor (HTTP)
|
||||
} else {
|
||||
bullets.unshift(
|
||||
this.i18n.transform(
|
||||
'Ideal for anonymous, censorship-resistant hosting and remote access',
|
||||
),
|
||||
)
|
||||
type = `${type} (HTTP)`
|
||||
}
|
||||
// ** Not Tor **
|
||||
} else {
|
||||
const port = info.hostname.sslPort || info.hostname.port
|
||||
const gateway = gateways.find(g => g.id === info.gatewayId)!
|
||||
gatewayName = gateway.ipInfo.name
|
||||
|
||||
const gatewayLanIpv4 = gateway.lanIpv4[0]
|
||||
const isWireguard = gateway.ipInfo.deviceType === 'wireguard'
|
||||
|
||||
const localIdeal = this.i18n.transform('Ideal for local access')
|
||||
const lanRequired = this.i18n.transform(
|
||||
'Requires being connected to the same Local Area Network (LAN) as your server, either physically or via VPN',
|
||||
)
|
||||
const staticRequired = `${this.i18n.transform('Requires setting a static IP address for')} ${gatewayLanIpv4} ${this.i18n.transform('in your gateway')}`
|
||||
const vpnAccess = this.i18n.transform('Ideal for VPN access via')
|
||||
const routerWireguard = this.i18n.transform(
|
||||
"your router's Wireguard server",
|
||||
)
|
||||
const portForwarding = this.i18n.transform(
|
||||
'Requires port forwarding in gateway',
|
||||
)
|
||||
const dnsFor = this.i18n.transform('Requires a DNS record for')
|
||||
const resolvesTo = this.i18n.transform('that resolves to')
|
||||
|
||||
// * Local *
|
||||
if (info.hostname.kind === 'local') {
|
||||
type = this.i18n.transform('Local')
|
||||
access = 'private'
|
||||
bullets = [
|
||||
localIdeal,
|
||||
this.i18n.transform(
|
||||
'Not recommended for VPN access. VPNs do not support ".local" domains without advanced configuration',
|
||||
),
|
||||
lanRequired,
|
||||
rootCaRequired,
|
||||
]
|
||||
// * IPv4 *
|
||||
} else if (info.hostname.kind === 'ipv4') {
|
||||
type = 'IPv4'
|
||||
if (info.public) {
|
||||
access = 'public'
|
||||
bullets = [
|
||||
this.i18n.transform('Can be used for clearnet access'),
|
||||
this.i18n.transform(
|
||||
'Not recommended in most cases. Clearnet domains are preferred',
|
||||
),
|
||||
rootCaRequired,
|
||||
]
|
||||
if (!gateway.public) {
|
||||
bullets.push(
|
||||
`${portForwarding} "${gatewayName}": ${port} -> ${info.hostname.value}:${port}`,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
access = 'private'
|
||||
if (isWireguard) {
|
||||
bullets = [`${vpnAccess} StartTunnel`, rootCaRequired]
|
||||
} else {
|
||||
bullets = [
|
||||
localIdeal,
|
||||
`${vpnAccess} ${routerWireguard}`,
|
||||
lanRequired,
|
||||
rootCaRequired,
|
||||
staticRequired,
|
||||
]
|
||||
}
|
||||
}
|
||||
// * IPv6 *
|
||||
} else if (info.hostname.kind === 'ipv6') {
|
||||
type = 'IPv6'
|
||||
access = 'private'
|
||||
bullets = [
|
||||
this.i18n.transform('Can be used for local access'),
|
||||
lanRequired,
|
||||
rootCaRequired,
|
||||
]
|
||||
// * Domain *
|
||||
} else {
|
||||
type = this.i18n.transform('Domain')
|
||||
if (info.public) {
|
||||
access = 'public'
|
||||
bullets = [
|
||||
`${dnsFor} ${info.hostname.value} ${resolvesTo} ${gateway.ipInfo.wanIp}`,
|
||||
`${portForwarding} "${gatewayName}": ${port} -> ${info.hostname.value}:${port === 443 ? 5443 : port}`,
|
||||
]
|
||||
if (publicDomains[info.hostname.value]?.acme) {
|
||||
bullets.unshift(
|
||||
this.i18n.transform('Ideal for public access via the Internet'),
|
||||
)
|
||||
} else {
|
||||
bullets = [
|
||||
this.i18n.transform(
|
||||
'Can be used for personal access via the public Internet. VPN is more private and secure',
|
||||
),
|
||||
rootCaRequired,
|
||||
...bullets,
|
||||
]
|
||||
}
|
||||
} else {
|
||||
access = 'private'
|
||||
const ipPortBad = this.i18n.transform(
|
||||
'when using IP addresses and ports is undesirable',
|
||||
)
|
||||
const customDnsRequired = `${dnsFor} ${info.hostname.value} ${resolvesTo} ${gatewayLanIpv4}`
|
||||
if (isWireguard) {
|
||||
bullets = [
|
||||
`${vpnAccess} StartTunnel ${ipPortBad}`,
|
||||
customDnsRequired,
|
||||
rootCaRequired,
|
||||
]
|
||||
} else {
|
||||
bullets = [
|
||||
`${localIdeal} ${ipPortBad}`,
|
||||
`${vpnAccess} ${routerWireguard} ${ipPortBad}`,
|
||||
customDnsRequired,
|
||||
rootCaRequired,
|
||||
lanRequired,
|
||||
staticRequired,
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
url: url.href,
|
||||
access,
|
||||
gatewayName,
|
||||
type,
|
||||
bullets,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type MappedServiceInterface = T.ServiceInterface & {
|
||||
|
||||
@@ -26,8 +26,6 @@ import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
import { InterfaceComponent } from './interface.component'
|
||||
|
||||
// @TODO translations
|
||||
|
||||
@Component({
|
||||
selector: 'section[privateDomains]',
|
||||
template: `
|
||||
@@ -113,9 +111,10 @@ export class InterfacePrivateDomainsComponent {
|
||||
spec: await configBuilderToSpec(
|
||||
ISB.InputSpec.of({
|
||||
fqdn: ISB.Value.text({
|
||||
name: 'Domain',
|
||||
description:
|
||||
name: this.i18n.transform('Domain'),
|
||||
description: this.i18n.transform(
|
||||
'Enter a fully qualified domain name. Since the domain is for private use, it can be any domain you want, even one you do not control.',
|
||||
),
|
||||
required: true,
|
||||
default: null,
|
||||
patterns: [utils.Patterns.domain],
|
||||
|
||||
@@ -19,8 +19,6 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { parse } from 'tldts'
|
||||
import { GatewayWithId } from './pd.service'
|
||||
|
||||
// @TODO translations
|
||||
|
||||
@Component({
|
||||
selector: 'dns',
|
||||
template: `
|
||||
@@ -37,11 +35,11 @@ import { GatewayWithId } from './pd.service'
|
||||
[(ngModel)]="ddns"
|
||||
(ngModelChange)="pass.set(undefined)"
|
||||
/>
|
||||
Dynamic DNS
|
||||
{{ 'Dynamic DNS' | i18n }}
|
||||
</label>
|
||||
}
|
||||
|
||||
<table [appTable]="['Type', $any('Host'), 'Value', 'Purpose']">
|
||||
<table [appTable]="['Type', 'Host', 'Value', 'Purpose']">
|
||||
@for (row of rows(); track $index) {
|
||||
<tr>
|
||||
<td>
|
||||
@@ -98,6 +96,7 @@ import { GatewayWithId } from './pd.service'
|
||||
export class DnsComponent {
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
|
||||
readonly ddns = false
|
||||
|
||||
@@ -126,6 +125,8 @@ export class DnsComponent {
|
||||
|
||||
const segments = subdomain.split('.')
|
||||
|
||||
const subdomains = this.i18n.transform('subdomains of')
|
||||
|
||||
return [
|
||||
{
|
||||
host: subdomain,
|
||||
@@ -135,12 +136,12 @@ export class DnsComponent {
|
||||
const parent = segments.slice(i + 1).join('.')
|
||||
return {
|
||||
host: `*.${parent}`,
|
||||
purpose: `subdomains of ${parent}`,
|
||||
purpose: `${subdomains} ${parent}`,
|
||||
}
|
||||
}),
|
||||
{
|
||||
host: '*',
|
||||
purpose: `subdomains of ${domain}`,
|
||||
purpose: `${subdomains} ${domain}`,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
ErrorService,
|
||||
i18nKey,
|
||||
LoadingService,
|
||||
i18nPipe,
|
||||
} from '@start9labs/shared'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { ISB, T, utils } from '@start9labs/start-sdk'
|
||||
@@ -18,8 +19,6 @@ import { toAuthorityName } from 'src/app/utils/acme'
|
||||
import { InterfaceComponent } from '../interface.component'
|
||||
import { DNS } from './dns.component'
|
||||
|
||||
// @TODO translations
|
||||
|
||||
export type PublicDomain = {
|
||||
fqdn: string
|
||||
gateway: GatewayWithId | null
|
||||
@@ -40,6 +39,7 @@ export class PublicDomainService {
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
private readonly dialog = inject(DialogService)
|
||||
private readonly interface = inject(InterfaceComponent)
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
|
||||
readonly data = toSignal(
|
||||
this.patch.watch$('serverInfo', 'network').pipe(
|
||||
@@ -61,9 +61,10 @@ export class PublicDomainService {
|
||||
async add() {
|
||||
const addSpec = ISB.InputSpec.of({
|
||||
fqdn: ISB.Value.text({
|
||||
name: 'Domain',
|
||||
description:
|
||||
name: this.i18n.transform('Domain'),
|
||||
description: this.i18n.transform(
|
||||
'Enter a fully qualified domain name. For example, if you control domain.com, you could enter domain.com or subdomain.domain.com or another.subdomain.domain.com.',
|
||||
),
|
||||
required: true,
|
||||
default: null,
|
||||
patterns: [utils.Patterns.domain],
|
||||
@@ -140,7 +141,7 @@ export class PublicDomainService {
|
||||
showDns(fqdn: string, gateway: GatewayWithId, message: i18nKey) {
|
||||
this.dialog
|
||||
.openComponent(DNS, {
|
||||
label: 'DNS Records' as i18nKey,
|
||||
label: 'DNS Records',
|
||||
size: 'l',
|
||||
data: {
|
||||
fqdn,
|
||||
@@ -177,7 +178,9 @@ export class PublicDomainService {
|
||||
}
|
||||
|
||||
const wanIp = gateway.ipInfo.wanIp
|
||||
let message = `Create one of the DNS records below to cause ${fqdn} to resolve to ${wanIp}`
|
||||
let message = this.i18n.transform(
|
||||
'Create one of the DNS records below.',
|
||||
) as i18nKey
|
||||
|
||||
if (!ip) {
|
||||
setTimeout(
|
||||
@@ -185,7 +188,7 @@ export class PublicDomainService {
|
||||
this.showDns(
|
||||
fqdn,
|
||||
gateway,
|
||||
`No DNS detected for ${fqdn}. ${message}` as i18nKey,
|
||||
`${this.i18n.transform('No DNS record detected for')} ${fqdn}. ${message}` as i18nKey,
|
||||
),
|
||||
250,
|
||||
)
|
||||
@@ -195,7 +198,7 @@ export class PublicDomainService {
|
||||
this.showDns(
|
||||
fqdn,
|
||||
gateway,
|
||||
`Invalid DNS. ${fqdn} is currently resolving to ${ip}. ${message}` as i18nKey,
|
||||
`${this.i18n.transform('Invalid DNS record')}. ${fqdn} ${this.i18n.transform('resolves to')} ${ip}. ${message}` as i18nKey,
|
||||
),
|
||||
250,
|
||||
)
|
||||
@@ -203,8 +206,8 @@ export class PublicDomainService {
|
||||
setTimeout(
|
||||
() =>
|
||||
this.dialog.openAlert(
|
||||
`${fqdn} is successfully resolving to ${wanIp}` as i18nKey,
|
||||
{ label: 'DNS detected!' as i18nKey, appearance: 'positive' },
|
||||
`${fqdn} ${this.i18n.transform('resolves to')} ${wanIp}` as i18nKey,
|
||||
{ label: 'DNS record detected!', appearance: 'positive' },
|
||||
),
|
||||
250,
|
||||
)
|
||||
@@ -224,8 +227,10 @@ export class PublicDomainService {
|
||||
|
||||
return {
|
||||
gateway: ISB.Value.dynamicSelect(() => ({
|
||||
name: 'Gateway',
|
||||
description: 'Select a gateway to use for this domain.',
|
||||
name: this.i18n.transform('Gateway'),
|
||||
description: this.i18n.transform(
|
||||
'Select a gateway to use for this domain.',
|
||||
),
|
||||
values: data.gateways.reduce<Record<string, string>>(
|
||||
(obj, gateway) => ({
|
||||
...obj,
|
||||
@@ -241,9 +246,10 @@ export class PublicDomainService {
|
||||
.map(g => g.id),
|
||||
})),
|
||||
authority: ISB.Value.select({
|
||||
name: 'Certificate Authority',
|
||||
description:
|
||||
'Select the Certificate Authority that will issue the SSL/TLS certificate for this domain',
|
||||
name: this.i18n.transform('Certificate Authority'),
|
||||
description: this.i18n.transform(
|
||||
'Select a Certificate Authority to issue SSL/TLS certificates for this domain',
|
||||
),
|
||||
values: data.authorities,
|
||||
default: '',
|
||||
}),
|
||||
|
||||
@@ -55,7 +55,7 @@ export class AuthorityService {
|
||||
|
||||
const addSpec = ISB.InputSpec.of({
|
||||
provider: ISB.Value.union({
|
||||
name: 'Provider',
|
||||
name: this.i18n.transform('Provider'),
|
||||
default: (availableAuthorities[0]?.url as any) || 'other',
|
||||
variants: ISB.Variants.of({
|
||||
...availableAuthorities.reduce(
|
||||
@@ -69,7 +69,7 @@ export class AuthorityService {
|
||||
{},
|
||||
),
|
||||
other: {
|
||||
name: 'Other',
|
||||
name: this.i18n.transform('Other'),
|
||||
spec: ISB.InputSpec.of({
|
||||
url: ISB.Value.text({
|
||||
name: 'URL',
|
||||
|
||||
@@ -72,82 +72,82 @@ export default class GatewaysComponent {
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
|
||||
async add() {
|
||||
const spec = ISB.InputSpec.of({
|
||||
name: ISB.Value.text({
|
||||
name: this.i18n.transform('Name'),
|
||||
description: this.i18n.transform(
|
||||
'A name to easily identify the gateway',
|
||||
),
|
||||
required: true,
|
||||
default: null,
|
||||
}),
|
||||
type: ISB.Value.select({
|
||||
name: this.i18n.transform('Type'),
|
||||
description: `-**${this.i18n.transform('private')}**: ${this.i18n.transform('select this option if the gateway is configured for private access to authorized clients only. StartTunnel is a private gateway.')}\n-**${this.i18n.transform('public')}**: ${this.i18n.transform('select this option if the gateway is configured for unfettered public access.')}`,
|
||||
default: 'private',
|
||||
values: {
|
||||
private: this.i18n.transform('private'),
|
||||
public: this.i18n.transform('public'),
|
||||
},
|
||||
}),
|
||||
config: ISB.Value.union({
|
||||
name: this.i18n.transform('Wireguard Config File'),
|
||||
default: 'paste',
|
||||
variants: ISB.Variants.of({
|
||||
paste: {
|
||||
name: this.i18n.transform('Copy/Paste'),
|
||||
spec: ISB.InputSpec.of({
|
||||
file: ISB.Value.textarea({
|
||||
name: this.i18n.transform('File Contents'),
|
||||
default: null,
|
||||
required: true,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
upload: {
|
||||
name: this.i18n.transform('Upload'),
|
||||
spec: ISB.InputSpec.of({
|
||||
file: ISB.Value.file({
|
||||
name: this.i18n.transform('File'),
|
||||
required: true,
|
||||
extensions: ['.conf'],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
this.formDialog.open(FormComponent, {
|
||||
label: 'Add gateway',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(gatewaySpec),
|
||||
spec: await configBuilderToSpec(spec),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Save',
|
||||
handler: (input: typeof gatewaySpec._TYPE) => this.save(input),
|
||||
text: this.i18n.transform('Save'),
|
||||
handler: async (input: typeof spec._TYPE) => {
|
||||
const loader = this.loader.open('Saving').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.addTunnel({
|
||||
name: input.name,
|
||||
config: '' as string, // @TODO alex/matt when types arrive
|
||||
public: input.type === 'public',
|
||||
})
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private async save(input: typeof gatewaySpec._TYPE): Promise<boolean> {
|
||||
const loader = this.loader.open('Saving').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.addTunnel({
|
||||
name: input.name,
|
||||
config: '' as string, // @TODO alex/matt when types arrive
|
||||
public: input.type === 'public',
|
||||
})
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const gatewaySpec = ISB.InputSpec.of({
|
||||
name: ISB.Value.text({
|
||||
name: 'Name',
|
||||
description: 'A name to easily identify the gateway',
|
||||
required: true,
|
||||
default: null,
|
||||
}),
|
||||
type: ISB.Value.select({
|
||||
name: 'Type',
|
||||
description:
|
||||
'-**Private**: select this option if the gateway is configured for private access to authorized clients only. StartTunnel is a private gateway.\n-**Public**: select this option if the gateway is configured for unfettered public access.',
|
||||
default: 'private',
|
||||
values: {
|
||||
private: 'Private',
|
||||
public: 'Public',
|
||||
},
|
||||
}),
|
||||
config: ISB.Value.union({
|
||||
name: 'Wireguard Config',
|
||||
default: 'paste',
|
||||
variants: ISB.Variants.of({
|
||||
paste: {
|
||||
name: 'Paste File Contents',
|
||||
spec: ISB.InputSpec.of({
|
||||
file: ISB.Value.textarea({
|
||||
name: 'Paste File Contents',
|
||||
default: null,
|
||||
required: true,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
upload: {
|
||||
name: 'Upload File',
|
||||
spec: ISB.InputSpec.of({
|
||||
file: ISB.Value.file({
|
||||
name: 'File',
|
||||
required: true,
|
||||
extensions: ['.conf'],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -143,6 +143,7 @@ export class GatewaysItemComponent {
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
|
||||
readonly gateway = input.required<GatewayPlus>()
|
||||
|
||||
@@ -169,7 +170,7 @@ export class GatewaysItemComponent {
|
||||
const { ipInfo, id } = this.gateway()
|
||||
const renameSpec = ISB.InputSpec.of({
|
||||
label: ISB.Value.text({
|
||||
name: 'Label',
|
||||
name: this.i18n.transform('Name'),
|
||||
required: true,
|
||||
default: ipInfo?.name || null,
|
||||
}),
|
||||
|
||||
@@ -107,14 +107,29 @@ export default class SystemSSHComponent {
|
||||
protected tableKeys = viewChild<SSHTableComponent<SSHKey>>('table')
|
||||
|
||||
async add(all: readonly SSHKey[]) {
|
||||
const spec = ISB.InputSpec.of({
|
||||
key: ISB.Value.text({
|
||||
name: this.i18n.transform('Public Key'),
|
||||
required: true,
|
||||
default: null,
|
||||
patterns: [
|
||||
{
|
||||
regex:
|
||||
'^(ssh-(rsa|ed25519|dss|ecdsa)|ecdsa-sha2-nistp(256|384|521))\\s+[A-Za-z0-9+/=]+(\\s[^\\s]+)?$',
|
||||
description: this.i18n.transform('must be a valid SSH public key'),
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
this.formDialog.open(FormComponent, {
|
||||
label: 'Add SSH key',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(SSHSpec),
|
||||
spec: await configBuilderToSpec(spec),
|
||||
buttons: [
|
||||
{
|
||||
text: this.i18n.transform('Save'),
|
||||
handler: async ({ key }: typeof SSHSpec._TYPE) => {
|
||||
handler: async ({ key }: typeof spec._TYPE) => {
|
||||
const loader = this.loader.open('Saving').subscribe()
|
||||
|
||||
try {
|
||||
@@ -157,18 +172,3 @@ export default class SystemSSHComponent {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const SSHSpec = ISB.InputSpec.of({
|
||||
key: ISB.Value.text({
|
||||
name: 'Public Key',
|
||||
required: true,
|
||||
default: null,
|
||||
patterns: [
|
||||
{
|
||||
regex:
|
||||
'^(ssh-(rsa|ed25519|dss|ecdsa)|ecdsa-sha2-nistp(256|384|521))\\s+[A-Za-z0-9+/=]+(\\s[^\\s]+)?$',
|
||||
description: 'must be a valid SSH public key',
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user