mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
frontend support for setting and changing hostname
This commit is contained in:
2
Makefile
2
Makefile
@@ -283,7 +283,7 @@ core/bindings/index.ts: $(call ls-files, core) $(ENVIRONMENT_FILE)
|
|||||||
rm -rf core/bindings
|
rm -rf core/bindings
|
||||||
./core/build/build-ts.sh
|
./core/build/build-ts.sh
|
||||||
ls core/bindings/*.ts | sed 's/core\/bindings\/\([^.]*\)\.ts/export { \1 } from ".\/\1";/g' | grep -v '"./index"' | tee core/bindings/index.ts
|
ls core/bindings/*.ts | sed 's/core\/bindings\/\([^.]*\)\.ts/export { \1 } from ".\/\1";/g' | grep -v '"./index"' | tee core/bindings/index.ts
|
||||||
npm --prefix sdk/base exec -- prettier --config=./sdk/base/package.json -w ./core/bindings/*.ts
|
npm --prefix sdk/base exec -- prettier --config=./sdk/base/package.json -w './core/bindings/**/*.ts'
|
||||||
touch core/bindings/index.ts
|
touch core/bindings/index.ts
|
||||||
|
|
||||||
sdk/dist/package.json sdk/baseDist/package.json: $(call ls-files, sdk) sdk/base/lib/osBindings/index.ts
|
sdk/dist/package.json sdk/baseDist/package.json: $(call ls-files, sdk) sdk/base/lib/osBindings/index.ts
|
||||||
|
|||||||
17
docs/TODO.md
17
docs/TODO.md
@@ -23,19 +23,14 @@ Pending tasks for AI agents. Remove items when completed.
|
|||||||
other crate types. Extracting them requires either moving the type definitions into the sub-crate
|
other crate types. Extracting them requires either moving the type definitions into the sub-crate
|
||||||
(and importing them back into `start-os`) or restructuring to share a common types crate.
|
(and importing them back into `start-os`) or restructuring to share a common types crate.
|
||||||
|
|
||||||
- [ ] Use auto-generated RPC types in the frontend instead of manual duplicates
|
- [ ] Make `SetupExecuteParams.password` optional in the backend - @dr-bonez
|
||||||
|
|
||||||
**Problem**: The web frontend manually defines ~755 lines of API request/response types in
|
**Problem**: In `core/src/setup.rs`, `SetupExecuteParams` has `password: EncryptedWire` (non-nullable),
|
||||||
`web/projects/ui/src/app/services/api/api.types.ts` that can drift from the actual Rust types.
|
but the frontend needs to send `null` for restore/transfer flows where the user keeps their existing
|
||||||
|
password. The `AttachParams` type correctly uses `Option<EncryptedWire>` for this purpose.
|
||||||
|
|
||||||
**Current state**: The Rust backend already has `#[ts(export)]` on RPC param types (e.g.
|
**Fix**: Change `password: EncryptedWire` to `password: Option<EncryptedWire>` in `SetupExecuteParams`
|
||||||
`AddTunnelParams`, `SetWifiEnabledParams`, `LoginParams`), and they are generated into
|
and handle the `None` case in the `execute` handler (similar to how `attach` handles it).
|
||||||
`core/bindings/`. However, commit `71b83245b` ("Chore/unexport api ts #2585", April 2024)
|
|
||||||
deliberately stopped building them into the SDK and had the frontend maintain its own types.
|
|
||||||
|
|
||||||
**Goal**: Reverse that decision — pipe the generated RPC types through the SDK into the frontend
|
|
||||||
so `api.types.ts` can import them instead of duplicating them. This eliminates drift between
|
|
||||||
backend and frontend API contracts.
|
|
||||||
|
|
||||||
- [ ] Auto-configure port forwards via UPnP/NAT-PMP/PCP - @dr-bonez
|
- [ ] Auto-configure port forwards via UPnP/NAT-PMP/PCP - @dr-bonez
|
||||||
|
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ export type AttachParams = {
|
|||||||
password: EncryptedWire | null
|
password: EncryptedWire | null
|
||||||
guid: string
|
guid: string
|
||||||
kiosk?: boolean
|
kiosk?: boolean
|
||||||
|
hostname: string | null
|
||||||
}
|
}
|
||||||
|
|||||||
3
sdk/base/lib/osBindings/SetServerHostnameParams.ts
Normal file
3
sdk/base/lib/osBindings/SetServerHostnameParams.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type SetServerHostnameParams = { hostname: string }
|
||||||
@@ -7,4 +7,5 @@ export type SetupExecuteParams = {
|
|||||||
password: EncryptedWire
|
password: EncryptedWire
|
||||||
recoverySource: RecoverySource<EncryptedWire> | null
|
recoverySource: RecoverySource<EncryptedWire> | null
|
||||||
kiosk?: boolean
|
kiosk?: boolean
|
||||||
|
hostname: string | null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -257,6 +257,7 @@ export { SetMainStatusStatus } from './SetMainStatusStatus'
|
|||||||
export { SetMainStatus } from './SetMainStatus'
|
export { SetMainStatus } from './SetMainStatus'
|
||||||
export { SetNameParams } from './SetNameParams'
|
export { SetNameParams } from './SetNameParams'
|
||||||
export { SetOutboundGatewayParams } from './SetOutboundGatewayParams'
|
export { SetOutboundGatewayParams } from './SetOutboundGatewayParams'
|
||||||
|
export { SetServerHostnameParams } from './SetServerHostnameParams'
|
||||||
export { SetStaticDnsParams } from './SetStaticDnsParams'
|
export { SetStaticDnsParams } from './SetStaticDnsParams'
|
||||||
export { SetupExecuteParams } from './SetupExecuteParams'
|
export { SetupExecuteParams } from './SetupExecuteParams'
|
||||||
export { SetupInfo } from './SetupInfo'
|
export { SetupInfo } from './SetupInfo'
|
||||||
|
|||||||
@@ -31,23 +31,15 @@ export class AppComponent {
|
|||||||
|
|
||||||
switch (status.status) {
|
switch (status.status) {
|
||||||
case 'needs-install':
|
case 'needs-install':
|
||||||
// Restore keyboard from status if it was previously set
|
|
||||||
if (status.keyboard) {
|
|
||||||
this.stateService.keyboard = status.keyboard.layout
|
|
||||||
}
|
|
||||||
// Start the install flow
|
|
||||||
await this.router.navigate(['/language'])
|
await this.router.navigate(['/language'])
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'incomplete':
|
case 'incomplete':
|
||||||
// Store the data drive info from status
|
// Store the data drive info from status
|
||||||
this.stateService.dataDriveGuid = status.guid
|
if (status.guid) {
|
||||||
this.stateService.attach = status.attach
|
this.stateService.dataDriveGuid = status.guid
|
||||||
// Restore keyboard from status if it was previously set
|
|
||||||
if (status.keyboard) {
|
|
||||||
this.stateService.keyboard = status.keyboard.layout
|
|
||||||
}
|
}
|
||||||
|
this.stateService.attach = status.attach
|
||||||
await this.router.navigate(['/language'])
|
await this.router.navigate(['/language'])
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,17 @@ import {
|
|||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
Validators,
|
Validators,
|
||||||
} from '@angular/forms'
|
} from '@angular/forms'
|
||||||
import { ErrorService, i18nPipe, LoadingService } from '@start9labs/shared'
|
import {
|
||||||
|
ErrorService,
|
||||||
|
generateHostname,
|
||||||
|
i18nPipe,
|
||||||
|
LoadingService,
|
||||||
|
} from '@start9labs/shared'
|
||||||
import { TuiAutoFocus, TuiMapperPipe, TuiValidator } from '@taiga-ui/cdk'
|
import { TuiAutoFocus, TuiMapperPipe, TuiValidator } from '@taiga-ui/cdk'
|
||||||
import {
|
import {
|
||||||
TuiButton,
|
TuiButton,
|
||||||
TuiError,
|
TuiError,
|
||||||
|
TuiHint,
|
||||||
TuiIcon,
|
TuiIcon,
|
||||||
TuiTextfield,
|
TuiTextfield,
|
||||||
TuiTitle,
|
TuiTitle,
|
||||||
@@ -20,6 +26,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
TuiFieldErrorPipe,
|
TuiFieldErrorPipe,
|
||||||
TuiPassword,
|
TuiPassword,
|
||||||
|
TuiTooltip,
|
||||||
tuiValidationErrorsProvider,
|
tuiValidationErrorsProvider,
|
||||||
} from '@taiga-ui/kit'
|
} from '@taiga-ui/kit'
|
||||||
import { TuiCardLarge, TuiHeader } from '@taiga-ui/layout'
|
import { TuiCardLarge, TuiHeader } from '@taiga-ui/layout'
|
||||||
@@ -31,31 +38,49 @@ import { StateService } from '../services/state.service'
|
|||||||
<header tuiHeader>
|
<header tuiHeader>
|
||||||
<h2 tuiTitle>
|
<h2 tuiTitle>
|
||||||
{{
|
{{
|
||||||
isRequired
|
isFresh
|
||||||
? ('Set Master Password' | i18n)
|
? ('Set Up Your Server' | i18n)
|
||||||
: ('Set New Password (Optional)' | i18n)
|
: ('Set New Password (Optional)' | i18n)
|
||||||
}}
|
}}
|
||||||
<span tuiSubtitle>
|
|
||||||
{{
|
|
||||||
isRequired
|
|
||||||
? ('Make it good. Write it down.' | i18n)
|
|
||||||
: ('Skip to keep your existing password.' | i18n)
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</h2>
|
</h2>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<form [formGroup]="form" (ngSubmit)="submit()">
|
<form [formGroup]="form" (ngSubmit)="submit()">
|
||||||
<tui-textfield>
|
@if (isFresh) {
|
||||||
|
<tui-textfield>
|
||||||
|
<label tuiLabel>{{ 'Server Hostname' | i18n }}</label>
|
||||||
|
<input tuiTextfield tuiAutoFocus formControlName="hostname" />
|
||||||
|
<span class="local-suffix">.local</span>
|
||||||
|
<button
|
||||||
|
tuiIconButton
|
||||||
|
type="button"
|
||||||
|
appearance="icon"
|
||||||
|
iconStart="@tui.refresh-cw"
|
||||||
|
size="xs"
|
||||||
|
[tuiHint]="'Randomize' | i18n"
|
||||||
|
(click)="randomizeHostname()"
|
||||||
|
></button>
|
||||||
|
<tui-icon
|
||||||
|
[tuiTooltip]="
|
||||||
|
'This value will be used as your server hostname and mDNS address on the LAN. Only lowercase letters, numbers, and hyphens are allowed.'
|
||||||
|
| i18n
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</tui-textfield>
|
||||||
|
<tui-error
|
||||||
|
formControlName="hostname"
|
||||||
|
[error]="[] | tuiFieldError | async"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<tui-textfield [style.margin-top.rem]="isFresh ? 1 : 0">
|
||||||
<label tuiLabel>
|
<label tuiLabel>
|
||||||
{{
|
{{ isFresh ? ('Password' | i18n) : ('New Password' | i18n) }}
|
||||||
isRequired ? ('Enter Password' | i18n) : ('New Password' | i18n)
|
|
||||||
}}
|
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
tuiTextfield
|
tuiTextfield
|
||||||
type="password"
|
type="password"
|
||||||
tuiAutoFocus
|
[tuiAutoFocus]="!isFresh"
|
||||||
maxlength="64"
|
maxlength="64"
|
||||||
formControlName="password"
|
formControlName="password"
|
||||||
/>
|
/>
|
||||||
@@ -87,14 +112,14 @@ import { StateService } from '../services/state.service'
|
|||||||
<button
|
<button
|
||||||
tuiButton
|
tuiButton
|
||||||
[disabled]="
|
[disabled]="
|
||||||
isRequired
|
isFresh
|
||||||
? form.invalid
|
? form.invalid
|
||||||
: form.controls.password.value && form.invalid
|
: form.controls.password.value && form.invalid
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
{{ 'Finish' | i18n }}
|
{{ 'Finish' | i18n }}
|
||||||
</button>
|
</button>
|
||||||
@if (!isRequired) {
|
@if (!isFresh) {
|
||||||
<button
|
<button
|
||||||
tuiButton
|
tuiButton
|
||||||
appearance="secondary"
|
appearance="secondary"
|
||||||
@@ -109,6 +134,10 @@ import { StateService } from '../services/state.service'
|
|||||||
</section>
|
</section>
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
|
.local-suffix {
|
||||||
|
color: var(--tui-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -131,6 +160,8 @@ import { StateService } from '../services/state.service'
|
|||||||
TuiMapperPipe,
|
TuiMapperPipe,
|
||||||
TuiHeader,
|
TuiHeader,
|
||||||
TuiTitle,
|
TuiTitle,
|
||||||
|
TuiHint,
|
||||||
|
TuiTooltip,
|
||||||
i18nPipe,
|
i18nPipe,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
@@ -139,6 +170,7 @@ import { StateService } from '../services/state.service'
|
|||||||
minlength: 'Must be 12 characters or greater',
|
minlength: 'Must be 12 characters or greater',
|
||||||
maxlength: 'Must be 64 character or less',
|
maxlength: 'Must be 64 character or less',
|
||||||
match: 'Passwords do not match',
|
match: 'Passwords do not match',
|
||||||
|
pattern: 'Only lowercase letters, numbers, and hyphens allowed',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
@@ -149,16 +181,20 @@ export default class PasswordPage {
|
|||||||
private readonly stateService = inject(StateService)
|
private readonly stateService = inject(StateService)
|
||||||
private readonly i18n = inject(i18nPipe)
|
private readonly i18n = inject(i18nPipe)
|
||||||
|
|
||||||
// Password is required only for fresh install
|
// Fresh install requires password and hostname
|
||||||
readonly isRequired = this.stateService.setupType === 'fresh'
|
readonly isFresh = this.stateService.setupType === 'fresh'
|
||||||
|
|
||||||
readonly form = new FormGroup({
|
readonly form = new FormGroup({
|
||||||
password: new FormControl('', [
|
password: new FormControl('', [
|
||||||
...(this.isRequired ? [Validators.required] : []),
|
...(this.isFresh ? [Validators.required] : []),
|
||||||
Validators.minLength(12),
|
Validators.minLength(12),
|
||||||
Validators.maxLength(64),
|
Validators.maxLength(64),
|
||||||
]),
|
]),
|
||||||
confirm: new FormControl(''),
|
confirm: new FormControl(''),
|
||||||
|
hostname: new FormControl(generateHostname(), [
|
||||||
|
Validators.required,
|
||||||
|
Validators.pattern(/^[a-z0-9][a-z0-9-]*$/),
|
||||||
|
]),
|
||||||
})
|
})
|
||||||
|
|
||||||
readonly validator = (value: string) => (control: AbstractControl) =>
|
readonly validator = (value: string) => (control: AbstractControl) =>
|
||||||
@@ -166,6 +202,10 @@ export default class PasswordPage {
|
|||||||
? null
|
? null
|
||||||
: { match: this.i18n.transform('Passwords do not match') }
|
: { match: this.i18n.transform('Passwords do not match') }
|
||||||
|
|
||||||
|
randomizeHostname() {
|
||||||
|
this.form.controls.hostname.setValue(generateHostname())
|
||||||
|
}
|
||||||
|
|
||||||
async skip() {
|
async skip() {
|
||||||
// Skip means no new password - pass null
|
// Skip means no new password - pass null
|
||||||
await this.executeSetup(null)
|
await this.executeSetup(null)
|
||||||
@@ -177,13 +217,14 @@ export default class PasswordPage {
|
|||||||
|
|
||||||
private async executeSetup(password: string | null) {
|
private async executeSetup(password: string | null) {
|
||||||
const loader = this.loader.open('Starting setup').subscribe()
|
const loader = this.loader.open('Starting setup').subscribe()
|
||||||
|
const hostname = this.form.controls.hostname.value || generateHostname()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.stateService.setupType === 'attach') {
|
if (this.stateService.setupType === 'attach') {
|
||||||
await this.stateService.attachDrive(password)
|
await this.stateService.attachDrive(password, hostname)
|
||||||
} else {
|
} else {
|
||||||
// fresh, restore, or transfer - all use execute
|
// fresh, restore, or transfer - all use execute
|
||||||
await this.stateService.executeSetup(password)
|
await this.stateService.executeSetup(password, hostname)
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.router.navigate(['/loading'])
|
await this.router.navigate(['/loading'])
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { StateService } from '../services/state.service'
|
|||||||
import { DocumentationComponent } from '../components/documentation.component'
|
import { DocumentationComponent } from '../components/documentation.component'
|
||||||
import { MatrixComponent } from '../components/matrix.component'
|
import { MatrixComponent } from '../components/matrix.component'
|
||||||
import { RemoveMediaDialog } from '../components/remove-media.dialog'
|
import { RemoveMediaDialog } from '../components/remove-media.dialog'
|
||||||
import { SetupCompleteRes } from '../types'
|
import { T } from '@start9labs/start-sdk'
|
||||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -193,7 +193,7 @@ export default class SuccessPage implements AfterViewInit {
|
|||||||
|
|
||||||
readonly stateService = inject(StateService)
|
readonly stateService = inject(StateService)
|
||||||
|
|
||||||
result?: SetupCompleteRes
|
result?: T.SetupResult
|
||||||
lanAddress = ''
|
lanAddress = ''
|
||||||
downloaded = false
|
downloaded = false
|
||||||
usbRemoved = false
|
usbRemoved = false
|
||||||
@@ -232,14 +232,11 @@ export default class SuccessPage implements AfterViewInit {
|
|||||||
|
|
||||||
removeMedia() {
|
removeMedia() {
|
||||||
this.dialogs
|
this.dialogs
|
||||||
.openComponent<boolean>(
|
.openComponent<boolean>(new PolymorpheusComponent(RemoveMediaDialog), {
|
||||||
new PolymorpheusComponent(RemoveMediaDialog),
|
size: 's',
|
||||||
{
|
dismissible: false,
|
||||||
size: 's',
|
closeable: false,
|
||||||
dismissible: false,
|
})
|
||||||
closeable: false,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
this.usbRemoved = true
|
this.usbRemoved = true
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,24 +7,16 @@ import {
|
|||||||
} from '@start9labs/shared'
|
} from '@start9labs/shared'
|
||||||
import { T } from '@start9labs/start-sdk'
|
import { T } from '@start9labs/start-sdk'
|
||||||
import { Observable } from 'rxjs'
|
import { Observable } from 'rxjs'
|
||||||
import {
|
import { InstallOsParams, InstallOsRes } from '../types'
|
||||||
SetupStatusRes,
|
|
||||||
InstallOsParams,
|
|
||||||
InstallOsRes,
|
|
||||||
AttachParams,
|
|
||||||
SetupExecuteParams,
|
|
||||||
SetupCompleteRes,
|
|
||||||
EchoReq,
|
|
||||||
} from '../types'
|
|
||||||
|
|
||||||
export abstract class ApiService {
|
export abstract class ApiService {
|
||||||
pubkey?: jose.JWK.Key
|
pubkey?: jose.JWK.Key
|
||||||
|
|
||||||
// echo
|
// echo
|
||||||
abstract echo(params: EchoReq, url: string): Promise<string>
|
abstract echo(params: T.EchoParams, url: string): Promise<string>
|
||||||
|
|
||||||
// Status & Setup
|
// Status & Setup
|
||||||
abstract getStatus(): Promise<SetupStatusRes> // setup.status
|
abstract getStatus(): Promise<T.SetupStatusRes> // setup.status
|
||||||
abstract getPubKey(): Promise<void> // setup.get-pubkey
|
abstract getPubKey(): Promise<void> // setup.get-pubkey
|
||||||
abstract setKeyboard(params: FullKeyboard): Promise<null> // setup.set-keyboard
|
abstract setKeyboard(params: FullKeyboard): Promise<null> // setup.set-keyboard
|
||||||
abstract setLanguage(params: SetLanguageParams): Promise<null> // setup.set-language
|
abstract setLanguage(params: SetLanguageParams): Promise<null> // setup.set-language
|
||||||
@@ -34,8 +26,8 @@ export abstract class ApiService {
|
|||||||
abstract installOs(params: InstallOsParams): Promise<InstallOsRes> // setup.install-os
|
abstract installOs(params: InstallOsParams): Promise<InstallOsRes> // setup.install-os
|
||||||
|
|
||||||
// Setup execution
|
// Setup execution
|
||||||
abstract attach(params: AttachParams): Promise<T.SetupProgress> // setup.attach
|
abstract attach(params: T.AttachParams): Promise<T.SetupProgress> // setup.attach
|
||||||
abstract execute(params: SetupExecuteParams): Promise<T.SetupProgress> // setup.execute
|
abstract execute(params: T.SetupExecuteParams): Promise<T.SetupProgress> // setup.execute
|
||||||
|
|
||||||
// Recovery helpers
|
// Recovery helpers
|
||||||
abstract verifyCifs(
|
abstract verifyCifs(
|
||||||
@@ -43,7 +35,7 @@ export abstract class ApiService {
|
|||||||
): Promise<Record<string, StartOSDiskInfo>> // setup.cifs.verify
|
): Promise<Record<string, StartOSDiskInfo>> // setup.cifs.verify
|
||||||
|
|
||||||
// Completion
|
// Completion
|
||||||
abstract complete(): Promise<SetupCompleteRes> // setup.complete
|
abstract complete(): Promise<T.SetupResult> // setup.complete
|
||||||
abstract exit(): Promise<void> // setup.exit
|
abstract exit(): Promise<void> // setup.exit
|
||||||
abstract shutdown(): Promise<void> // setup.shutdown
|
abstract shutdown(): Promise<void> // setup.shutdown
|
||||||
|
|
||||||
|
|||||||
@@ -15,15 +15,7 @@ import * as jose from 'node-jose'
|
|||||||
import { Observable } from 'rxjs'
|
import { Observable } from 'rxjs'
|
||||||
import { webSocket } from 'rxjs/webSocket'
|
import { webSocket } from 'rxjs/webSocket'
|
||||||
import { ApiService } from './api.service'
|
import { ApiService } from './api.service'
|
||||||
import {
|
import { InstallOsParams, InstallOsRes } from '../types'
|
||||||
SetupStatusRes,
|
|
||||||
InstallOsParams,
|
|
||||||
InstallOsRes,
|
|
||||||
AttachParams,
|
|
||||||
SetupExecuteParams,
|
|
||||||
SetupCompleteRes,
|
|
||||||
EchoReq,
|
|
||||||
} from '../types'
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -46,12 +38,12 @@ export class LiveApiService extends ApiService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async echo(params: EchoReq, url: string): Promise<string> {
|
async echo(params: T.EchoParams, url: string): Promise<string> {
|
||||||
return this.rpcRequest({ method: 'echo', params }, url)
|
return this.rpcRequest({ method: 'echo', params }, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStatus() {
|
async getStatus() {
|
||||||
return this.rpcRequest<SetupStatusRes>({
|
return this.rpcRequest<T.SetupStatusRes>({
|
||||||
method: 'setup.status',
|
method: 'setup.status',
|
||||||
params: {},
|
params: {},
|
||||||
})
|
})
|
||||||
@@ -101,14 +93,14 @@ export class LiveApiService extends ApiService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async attach(params: AttachParams) {
|
async attach(params: T.AttachParams) {
|
||||||
return this.rpcRequest<T.SetupProgress>({
|
return this.rpcRequest<T.SetupProgress>({
|
||||||
method: 'setup.attach',
|
method: 'setup.attach',
|
||||||
params,
|
params,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(params: SetupExecuteParams) {
|
async execute(params: T.SetupExecuteParams) {
|
||||||
if (params.recoverySource?.type === 'backup') {
|
if (params.recoverySource?.type === 'backup') {
|
||||||
const target = params.recoverySource.target
|
const target = params.recoverySource.target
|
||||||
if (target.type === 'cifs') {
|
if (target.type === 'cifs') {
|
||||||
@@ -130,7 +122,7 @@ export class LiveApiService extends ApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async complete() {
|
async complete() {
|
||||||
const res = await this.rpcRequest<SetupCompleteRes>({
|
const res = await this.rpcRequest<T.SetupResult>({
|
||||||
method: 'setup.complete',
|
method: 'setup.complete',
|
||||||
params: {},
|
params: {},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,15 +11,7 @@ import { T } from '@start9labs/start-sdk'
|
|||||||
import * as jose from 'node-jose'
|
import * as jose from 'node-jose'
|
||||||
import { interval, map, Observable } from 'rxjs'
|
import { interval, map, Observable } from 'rxjs'
|
||||||
import { ApiService } from './api.service'
|
import { ApiService } from './api.service'
|
||||||
import {
|
import { InstallOsParams, InstallOsRes } from '../types'
|
||||||
SetupStatusRes,
|
|
||||||
InstallOsParams,
|
|
||||||
InstallOsRes,
|
|
||||||
AttachParams,
|
|
||||||
SetupExecuteParams,
|
|
||||||
SetupCompleteRes,
|
|
||||||
EchoReq,
|
|
||||||
} from '../types'
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -53,7 +45,7 @@ export class MockApiService extends ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async echo(params: EchoReq, url: string): Promise<string> {
|
async echo(params: T.EchoParams, url: string): Promise<string> {
|
||||||
if (url) {
|
if (url) {
|
||||||
const num = Math.floor(Math.random() * 10) + 1
|
const num = Math.floor(Math.random() * 10) + 1
|
||||||
if (num > 8) return params.message
|
if (num > 8) return params.message
|
||||||
@@ -63,23 +55,27 @@ export class MockApiService extends ApiService {
|
|||||||
return params.message
|
return params.message
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStatus(): Promise<SetupStatusRes> {
|
async getStatus(): Promise<T.SetupStatusRes> {
|
||||||
await pauseFor(500)
|
await pauseFor(500)
|
||||||
|
|
||||||
this.statusIndex++
|
this.statusIndex++
|
||||||
|
|
||||||
if (this.statusIndex === 1) {
|
if (this.statusIndex === 1) {
|
||||||
return { status: 'needs-install', keyboard: null }
|
return { status: 'needs-install' }
|
||||||
// return {
|
// return {
|
||||||
// status: 'incomplete',
|
// status: 'incomplete',
|
||||||
// attach: false,
|
// attach: false,
|
||||||
// guid: 'mock-data-guid',
|
// guid: 'mock-data-guid',
|
||||||
// keyboard: null,
|
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.statusIndex > 3) {
|
if (this.statusIndex > 3) {
|
||||||
return { status: 'complete' }
|
return {
|
||||||
|
status: 'complete',
|
||||||
|
hostname: 'adjective-noun',
|
||||||
|
rootCa: encodeBase64(ROOT_CA),
|
||||||
|
needsRestart: this.installCompleted,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -147,7 +143,7 @@ export class MockApiService extends ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async attach(params: AttachParams): Promise<T.SetupProgress> {
|
async attach(params: T.AttachParams): Promise<T.SetupProgress> {
|
||||||
await pauseFor(1000)
|
await pauseFor(1000)
|
||||||
this.statusIndex = 1 // Jump to running state
|
this.statusIndex = 1 // Jump to running state
|
||||||
return {
|
return {
|
||||||
@@ -156,7 +152,7 @@ export class MockApiService extends ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(params: SetupExecuteParams): Promise<T.SetupProgress> {
|
async execute(params: T.SetupExecuteParams): Promise<T.SetupProgress> {
|
||||||
await pauseFor(1000)
|
await pauseFor(1000)
|
||||||
this.statusIndex = 1 // Jump to running state
|
this.statusIndex = 1 // Jump to running state
|
||||||
return {
|
return {
|
||||||
@@ -173,7 +169,7 @@ export class MockApiService extends ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async complete(): Promise<SetupCompleteRes> {
|
async complete(): Promise<T.SetupResult> {
|
||||||
await pauseFor(500)
|
await pauseFor(500)
|
||||||
return {
|
return {
|
||||||
hostname: 'adjective-noun',
|
hostname: 'adjective-noun',
|
||||||
|
|||||||
@@ -48,18 +48,19 @@ export class StateService {
|
|||||||
/**
|
/**
|
||||||
* Called for attach flow (existing data drive)
|
* Called for attach flow (existing data drive)
|
||||||
*/
|
*/
|
||||||
async attachDrive(password: string | null): Promise<void> {
|
async attachDrive(password: string | null, hostname: string): Promise<void> {
|
||||||
await this.api.attach({
|
await this.api.attach({
|
||||||
guid: this.dataDriveGuid,
|
guid: this.dataDriveGuid,
|
||||||
password: password ? await this.api.encrypt(password) : null,
|
password: password ? await this.api.encrypt(password) : null,
|
||||||
|
hostname,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called for fresh, restore, and transfer flows
|
* Called for fresh, restore, and transfer flows
|
||||||
* password is required for fresh, optional for restore/transfer
|
* Password is required for fresh, optional for restore/transfer
|
||||||
*/
|
*/
|
||||||
async executeSetup(password: string | null): Promise<void> {
|
async executeSetup(password: string | null, hostname: string): Promise<void> {
|
||||||
let recoverySource: T.RecoverySource<T.EncryptedWire> | null = null
|
let recoverySource: T.RecoverySource<T.EncryptedWire> | null = null
|
||||||
|
|
||||||
if (this.recoverySource) {
|
if (this.recoverySource) {
|
||||||
@@ -78,7 +79,9 @@ export class StateService {
|
|||||||
|
|
||||||
await this.api.execute({
|
await this.api.execute({
|
||||||
guid: this.dataDriveGuid,
|
guid: this.dataDriveGuid,
|
||||||
|
// @ts-expect-error TODO: backend should make password optional for restore/transfer
|
||||||
password: password ? await this.api.encrypt(password) : null,
|
password: password ? await this.api.encrypt(password) : null,
|
||||||
|
hostname,
|
||||||
recoverySource,
|
recoverySource,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,6 @@
|
|||||||
import {
|
import { DiskInfo, PartitionInfo, StartOSDiskInfo } from '@start9labs/shared'
|
||||||
DiskInfo,
|
|
||||||
FullKeyboard,
|
|
||||||
PartitionInfo,
|
|
||||||
StartOSDiskInfo,
|
|
||||||
} from '@start9labs/shared'
|
|
||||||
import { T } from '@start9labs/start-sdk'
|
|
||||||
|
|
||||||
// === Echo ===
|
// === Install OS === (no binding available)
|
||||||
|
|
||||||
export type EchoReq = {
|
|
||||||
message: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Setup Status ===
|
|
||||||
|
|
||||||
export type SetupStatusRes =
|
|
||||||
| { status: 'needs-install'; keyboard: FullKeyboard | null }
|
|
||||||
| {
|
|
||||||
status: 'incomplete'
|
|
||||||
guid: string
|
|
||||||
attach: boolean
|
|
||||||
keyboard: FullKeyboard | null
|
|
||||||
}
|
|
||||||
| { status: 'running'; progress: T.FullProgress; guid: string }
|
|
||||||
| { status: 'complete' }
|
|
||||||
|
|
||||||
// === Install OS ===
|
|
||||||
|
|
||||||
export interface InstallOsParams {
|
export interface InstallOsParams {
|
||||||
osDrive: string // e.g. /dev/sda
|
osDrive: string // e.g. /dev/sda
|
||||||
@@ -40,48 +15,6 @@ export interface InstallOsRes {
|
|||||||
attach: boolean
|
attach: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Attach ===
|
|
||||||
|
|
||||||
export interface AttachParams {
|
|
||||||
password: T.EncryptedWire | null
|
|
||||||
guid: string // data drive
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Execute ===
|
|
||||||
|
|
||||||
export interface SetupExecuteParams {
|
|
||||||
guid: string
|
|
||||||
password: T.EncryptedWire | null // null = keep existing password (for restore/transfer)
|
|
||||||
recoverySource:
|
|
||||||
| {
|
|
||||||
type: 'migrate'
|
|
||||||
guid: string
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'backup'
|
|
||||||
target:
|
|
||||||
| { type: 'disk'; logicalname: string }
|
|
||||||
| {
|
|
||||||
type: 'cifs'
|
|
||||||
hostname: string
|
|
||||||
path: string
|
|
||||||
username: string
|
|
||||||
password: string | null
|
|
||||||
}
|
|
||||||
password: T.EncryptedWire
|
|
||||||
serverId: string
|
|
||||||
}
|
|
||||||
| null
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Complete ===
|
|
||||||
|
|
||||||
export interface SetupCompleteRes {
|
|
||||||
hostname: string // unique.local
|
|
||||||
rootCa: string
|
|
||||||
needsRestart: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Disk Info Helpers ===
|
// === Disk Info Helpers ===
|
||||||
|
|
||||||
export type StartOSDiskInfoWithId = StartOSDiskInfo & {
|
export type StartOSDiskInfoWithId = StartOSDiskInfo & {
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ import { i18nKey } from '../i18n/i18n.providers'
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</tui-textfield>
|
</tui-textfield>
|
||||||
|
@if (error) {
|
||||||
|
<p class="error">{{ error }}</p>
|
||||||
|
}
|
||||||
<footer class="g-buttons">
|
<footer class="g-buttons">
|
||||||
<button
|
<button
|
||||||
tuiButton
|
tuiButton
|
||||||
@@ -65,6 +68,10 @@ import { i18nKey } from '../i18n/i18n.providers'
|
|||||||
color: var(--tui-status-warning);
|
color: var(--tui-status-warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: var(--tui-status-negative);
|
||||||
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
margin-left: 0.25rem;
|
margin-left: 0.25rem;
|
||||||
@@ -83,6 +90,7 @@ export class PromptModal {
|
|||||||
|
|
||||||
masked = this.options.useMask
|
masked = this.options.useMask
|
||||||
value = this.options.initialValue || ''
|
value = this.options.initialValue || ''
|
||||||
|
error = ''
|
||||||
|
|
||||||
get options(): PromptOptions {
|
get options(): PromptOptions {
|
||||||
return this.context.data
|
return this.context.data
|
||||||
@@ -94,6 +102,12 @@ export class PromptModal {
|
|||||||
|
|
||||||
submit(value: string) {
|
submit(value: string) {
|
||||||
if (value || !this.options.required) {
|
if (value || !this.options.required) {
|
||||||
|
const { pattern, patternError } = this.options
|
||||||
|
if (pattern && !new RegExp(pattern).test(value)) {
|
||||||
|
this.error = patternError || 'Invalid input'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.error = ''
|
||||||
this.context.$implicit.next(value)
|
this.context.$implicit.next(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,4 +124,6 @@ export interface PromptOptions {
|
|||||||
required?: boolean
|
required?: boolean
|
||||||
useMask?: boolean
|
useMask?: boolean
|
||||||
initialValue?: string | null
|
initialValue?: string | null
|
||||||
|
pattern?: string
|
||||||
|
patternError?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -605,10 +605,8 @@ export default {
|
|||||||
657: 'Wählen Sie das Laufwerk mit Ihren bestehenden StartOS-Daten aus',
|
657: 'Wählen Sie das Laufwerk mit Ihren bestehenden StartOS-Daten aus',
|
||||||
658: 'Laufwerk auswählen',
|
658: 'Laufwerk auswählen',
|
||||||
659: 'Keine StartOS-Datenlaufwerke gefunden',
|
659: 'Keine StartOS-Datenlaufwerke gefunden',
|
||||||
660: 'Master-Passwort festlegen',
|
660: 'Richten Sie Ihren Server ein',
|
||||||
661: 'Neues Passwort festlegen (optional)',
|
661: 'Neues Passwort festlegen (optional)',
|
||||||
662: 'Machen Sie es gut. Schreiben Sie es auf.',
|
|
||||||
663: 'Überspringen, um Ihr bestehendes Passwort beizubehalten.',
|
|
||||||
664: 'Passwort eingeben',
|
664: 'Passwort eingeben',
|
||||||
665: 'Passwort bestätigen',
|
665: 'Passwort bestätigen',
|
||||||
666: 'Fertigstellen',
|
666: 'Fertigstellen',
|
||||||
@@ -693,4 +691,12 @@ export default {
|
|||||||
755: 'Schnittstelle(n)',
|
755: 'Schnittstelle(n)',
|
||||||
756: 'Keine Portweiterleitungsregeln',
|
756: 'Keine Portweiterleitungsregeln',
|
||||||
757: 'Portweiterleitungsregeln am Gateway erforderlich',
|
757: 'Portweiterleitungsregeln am Gateway erforderlich',
|
||||||
|
758: 'Server-Hostname',
|
||||||
|
760: 'Dieser Wert wird als Hostname Ihres Servers und mDNS-Adresse im LAN verwendet. Nur Kleinbuchstaben, Zahlen und Bindestriche sind erlaubt.',
|
||||||
|
761: 'Zufällig generieren',
|
||||||
|
762: 'Nur Kleinbuchstaben, Zahlen und Bindestriche erlaubt',
|
||||||
|
763: 'Sie sind derzeit über Ihre .local-Adresse verbunden. Das Ändern des Hostnamens erfordert einen Wechsel zur neuen .local-Adresse.',
|
||||||
|
764: 'Hostname geändert',
|
||||||
|
765: 'Neue Adresse öffnen',
|
||||||
|
766: 'Ihr Server ist jetzt erreichbar unter',
|
||||||
} satisfies i18n
|
} satisfies i18n
|
||||||
|
|||||||
@@ -605,10 +605,8 @@ export const ENGLISH: Record<string, number> = {
|
|||||||
'Select the drive containing your existing StartOS data': 657,
|
'Select the drive containing your existing StartOS data': 657,
|
||||||
'Select Drive': 658,
|
'Select Drive': 658,
|
||||||
'No StartOS data drives found': 659,
|
'No StartOS data drives found': 659,
|
||||||
'Set Master Password': 660,
|
'Set Up Your Server': 660,
|
||||||
'Set New Password (Optional)': 661,
|
'Set New Password (Optional)': 661,
|
||||||
'Make it good. Write it down.': 662,
|
|
||||||
'Skip to keep your existing password.': 663,
|
|
||||||
'Enter Password': 664,
|
'Enter Password': 664,
|
||||||
'Confirm Password': 665,
|
'Confirm Password': 665,
|
||||||
'Finish': 666,
|
'Finish': 666,
|
||||||
@@ -693,4 +691,12 @@ export const ENGLISH: Record<string, number> = {
|
|||||||
'Interface(s)': 755,
|
'Interface(s)': 755,
|
||||||
'No port forwarding rules': 756,
|
'No port forwarding rules': 756,
|
||||||
'Port forwarding rules required on gateway': 757,
|
'Port forwarding rules required on gateway': 757,
|
||||||
|
'Server Hostname': 758,
|
||||||
|
'This value will be used as your server hostname and mDNS address on the LAN. Only lowercase letters, numbers, and hyphens are allowed.': 760,
|
||||||
|
'Randomize': 761,
|
||||||
|
'Only lowercase letters, numbers, and hyphens allowed': 762,
|
||||||
|
'You are currently connected via your .local address. Changing the hostname will require you to switch to the new .local address.': 763,
|
||||||
|
'Hostname Changed': 764,
|
||||||
|
'Open new address': 765,
|
||||||
|
'Your server is now reachable at': 766,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -605,10 +605,8 @@ export default {
|
|||||||
657: 'Seleccione la unidad que contiene sus datos StartOS existentes',
|
657: 'Seleccione la unidad que contiene sus datos StartOS existentes',
|
||||||
658: 'Seleccionar unidad',
|
658: 'Seleccionar unidad',
|
||||||
659: 'No se encontraron unidades de datos StartOS',
|
659: 'No se encontraron unidades de datos StartOS',
|
||||||
660: 'Establecer contraseña maestra',
|
660: 'Configure su servidor',
|
||||||
661: 'Establecer nueva contraseña (opcional)',
|
661: 'Establecer nueva contraseña (opcional)',
|
||||||
662: 'Que sea buena. Escríbala.',
|
|
||||||
663: 'Omitir para mantener su contraseña existente.',
|
|
||||||
664: 'Introducir contraseña',
|
664: 'Introducir contraseña',
|
||||||
665: 'Confirmar contraseña',
|
665: 'Confirmar contraseña',
|
||||||
666: 'Finalizar',
|
666: 'Finalizar',
|
||||||
@@ -693,4 +691,12 @@ export default {
|
|||||||
755: 'Interfaz/Interfaces',
|
755: 'Interfaz/Interfaces',
|
||||||
756: 'Sin reglas de redirección de puertos',
|
756: 'Sin reglas de redirección de puertos',
|
||||||
757: 'Reglas de redirección de puertos requeridas en la puerta de enlace',
|
757: 'Reglas de redirección de puertos requeridas en la puerta de enlace',
|
||||||
|
758: 'Nombre de host del servidor',
|
||||||
|
760: 'Este valor se usará como el nombre de host de su servidor y la dirección mDNS en la LAN. Solo se permiten letras minúsculas, números y guiones.',
|
||||||
|
761: 'Aleatorizar',
|
||||||
|
762: 'Solo se permiten letras minúsculas, números y guiones',
|
||||||
|
763: 'Actualmente está conectado a través de su dirección .local. Cambiar el nombre de host requerirá que cambie a la nueva dirección .local.',
|
||||||
|
764: 'Nombre de host cambiado',
|
||||||
|
765: 'Abrir nueva dirección',
|
||||||
|
766: 'Su servidor ahora es accesible en',
|
||||||
} satisfies i18n
|
} satisfies i18n
|
||||||
|
|||||||
@@ -605,10 +605,8 @@ export default {
|
|||||||
657: 'Sélectionnez le disque contenant vos données StartOS existantes',
|
657: 'Sélectionnez le disque contenant vos données StartOS existantes',
|
||||||
658: 'Sélectionner le disque',
|
658: 'Sélectionner le disque',
|
||||||
659: 'Aucun disque de données StartOS trouvé',
|
659: 'Aucun disque de données StartOS trouvé',
|
||||||
660: 'Définir le mot de passe maître',
|
660: 'Configurez votre serveur',
|
||||||
661: 'Définir un nouveau mot de passe (facultatif)',
|
661: 'Définir un nouveau mot de passe (facultatif)',
|
||||||
662: 'Choisissez-le bien. Notez-le.',
|
|
||||||
663: 'Ignorer pour conserver votre mot de passe existant.',
|
|
||||||
664: 'Saisir le mot de passe',
|
664: 'Saisir le mot de passe',
|
||||||
665: 'Confirmer le mot de passe',
|
665: 'Confirmer le mot de passe',
|
||||||
666: 'Terminer',
|
666: 'Terminer',
|
||||||
@@ -693,4 +691,12 @@ export default {
|
|||||||
755: 'Interface(s)',
|
755: 'Interface(s)',
|
||||||
756: 'Aucune règle de redirection de port',
|
756: 'Aucune règle de redirection de port',
|
||||||
757: 'Règles de redirection de ports requises sur la passerelle',
|
757: 'Règles de redirection de ports requises sur la passerelle',
|
||||||
|
758: "Nom d'hôte du serveur",
|
||||||
|
760: "Cette valeur sera utilisée comme nom d'hôte de votre serveur et adresse mDNS sur le LAN. Seules les lettres minuscules, les chiffres et les tirets sont autorisés.",
|
||||||
|
761: 'Générer aléatoirement',
|
||||||
|
762: 'Seules les lettres minuscules, les chiffres et les tirets sont autorisés',
|
||||||
|
763: "Vous êtes actuellement connecté via votre adresse .local. Changer le nom d'hôte nécessitera de passer à la nouvelle adresse .local.",
|
||||||
|
764: "Nom d'hôte modifié",
|
||||||
|
765: 'Ouvrir la nouvelle adresse',
|
||||||
|
766: 'Votre serveur est maintenant accessible à',
|
||||||
} satisfies i18n
|
} satisfies i18n
|
||||||
|
|||||||
@@ -605,10 +605,8 @@ export default {
|
|||||||
657: 'Wybierz dysk zawierający istniejące dane StartOS',
|
657: 'Wybierz dysk zawierający istniejące dane StartOS',
|
||||||
658: 'Wybierz dysk',
|
658: 'Wybierz dysk',
|
||||||
659: 'Nie znaleziono dysków danych StartOS',
|
659: 'Nie znaleziono dysków danych StartOS',
|
||||||
660: 'Ustaw hasło główne',
|
660: 'Skonfiguruj swój serwer',
|
||||||
661: 'Ustaw nowe hasło (opcjonalnie)',
|
661: 'Ustaw nowe hasło (opcjonalnie)',
|
||||||
662: 'Zadbaj o nie. Zapisz je.',
|
|
||||||
663: 'Pomiń, aby zachować istniejące hasło.',
|
|
||||||
664: 'Wprowadź hasło',
|
664: 'Wprowadź hasło',
|
||||||
665: 'Potwierdź hasło',
|
665: 'Potwierdź hasło',
|
||||||
666: 'Zakończ',
|
666: 'Zakończ',
|
||||||
@@ -693,4 +691,12 @@ export default {
|
|||||||
755: 'Interfejs(y)',
|
755: 'Interfejs(y)',
|
||||||
756: 'Brak reguł przekierowania portów',
|
756: 'Brak reguł przekierowania portów',
|
||||||
757: 'Reguły przekierowania portów wymagane na bramce',
|
757: 'Reguły przekierowania portów wymagane na bramce',
|
||||||
|
758: 'Nazwa hosta serwera',
|
||||||
|
760: 'Ta wartość będzie używana jako nazwa hosta serwera i adres mDNS w sieci LAN. Dozwolone są tylko małe litery, cyfry i myślniki.',
|
||||||
|
761: 'Losuj',
|
||||||
|
762: 'Dozwolone są tylko małe litery, cyfry i myślniki',
|
||||||
|
763: 'Jesteś obecnie połączony przez adres .local. Zmiana nazwy hosta będzie wymagać przełączenia na nowy adres .local.',
|
||||||
|
764: 'Nazwa hosta zmieniona',
|
||||||
|
765: 'Otwórz nowy adres',
|
||||||
|
766: 'Twój serwer jest teraz dostępny pod adresem',
|
||||||
} satisfies i18n
|
} satisfies i18n
|
||||||
|
|||||||
@@ -61,3 +61,4 @@ export * from './util/to-local-iso-string'
|
|||||||
export * from './util/unused'
|
export * from './util/unused'
|
||||||
export * from './util/keyboards'
|
export * from './util/keyboards'
|
||||||
export * from './util/languages'
|
export * from './util/languages'
|
||||||
|
export * from './util/hostname'
|
||||||
|
|||||||
9088
web/projects/shared/src/util/hostname.ts
Normal file
9088
web/projects/shared/src/util/hostname.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,7 @@ import {
|
|||||||
LANGUAGE_TO_CODE,
|
LANGUAGE_TO_CODE,
|
||||||
LoadingService,
|
LoadingService,
|
||||||
} from '@start9labs/shared'
|
} from '@start9labs/shared'
|
||||||
|
import { WA_WINDOW } from '@ng-web-apis/common'
|
||||||
import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile'
|
import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile'
|
||||||
import { TuiAnimated } from '@taiga-ui/cdk'
|
import { TuiAnimated } from '@taiga-ui/cdk'
|
||||||
import {
|
import {
|
||||||
@@ -45,7 +46,7 @@ import {
|
|||||||
import { TuiCell, tuiCellOptionsProvider } from '@taiga-ui/layout'
|
import { TuiCell, tuiCellOptionsProvider } from '@taiga-ui/layout'
|
||||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { filter } from 'rxjs'
|
import { filter, Subscription } from 'rxjs'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { OSService } from 'src/app/services/os.service'
|
import { OSService } from 'src/app/services/os.service'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
@@ -94,6 +95,18 @@ import { KeyboardSelectComponent } from './keyboard-select.component'
|
|||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div tuiCell tuiAppearance="outline-grayscale">
|
||||||
|
<tui-icon icon="@tui.server" />
|
||||||
|
<span tuiTitle>
|
||||||
|
<strong>{{ 'Server Hostname' | i18n }}</strong>
|
||||||
|
<span tuiSubtitle>
|
||||||
|
{{ server.hostname }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<button tuiButton (click)="onHostname()">
|
||||||
|
{{ 'Change' | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div tuiCell tuiAppearance="outline-grayscale">
|
<div tuiCell tuiAppearance="outline-grayscale">
|
||||||
<tui-icon icon="@tui.app-window" />
|
<tui-icon icon="@tui.app-window" />
|
||||||
<span tuiTitle>
|
<span tuiTitle>
|
||||||
@@ -272,6 +285,7 @@ export default class SystemGeneralComponent {
|
|||||||
private readonly dialog = inject(DialogService)
|
private readonly dialog = inject(DialogService)
|
||||||
private readonly i18n = inject(i18nPipe)
|
private readonly i18n = inject(i18nPipe)
|
||||||
private readonly injector = inject(INJECTOR)
|
private readonly injector = inject(INJECTOR)
|
||||||
|
private readonly win = inject(WA_WINDOW)
|
||||||
|
|
||||||
count = 0
|
count = 0
|
||||||
|
|
||||||
@@ -346,6 +360,82 @@ export default class SystemGeneralComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onHostname() {
|
||||||
|
const sub = this.dialog
|
||||||
|
.openPrompt<string>({
|
||||||
|
label: 'Server Hostname',
|
||||||
|
data: {
|
||||||
|
label: 'Hostname' as i18nKey,
|
||||||
|
message:
|
||||||
|
'This value will be used as your server hostname and mDNS address on the LAN. Only lowercase letters, numbers, and hyphens are allowed.',
|
||||||
|
placeholder: 'start9' as i18nKey,
|
||||||
|
required: true,
|
||||||
|
buttonText: 'Save',
|
||||||
|
initialValue: this.server()?.hostname || '',
|
||||||
|
pattern: '^[a-z0-9][a-z0-9-]*$',
|
||||||
|
patternError:
|
||||||
|
'Hostname must contain only lowercase letters, numbers, and hyphens, and cannot start with a hyphen.',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.subscribe(hostname => {
|
||||||
|
if (this.win.location.hostname.endsWith('.local')) {
|
||||||
|
this.confirmHostnameChange(hostname, sub)
|
||||||
|
} else {
|
||||||
|
this.saveHostname(hostname, sub)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private confirmHostnameChange(hostname: string, promptSub: Subscription) {
|
||||||
|
this.dialog
|
||||||
|
.openConfirm({
|
||||||
|
label: 'Warning',
|
||||||
|
data: {
|
||||||
|
content:
|
||||||
|
'You are currently connected via your .local address. Changing the hostname will require you to switch to the new .local address.',
|
||||||
|
yes: 'Save',
|
||||||
|
no: 'Cancel',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.pipe(filter(Boolean))
|
||||||
|
.subscribe(() => this.saveHostname(hostname, promptSub, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveHostname(
|
||||||
|
hostname: string,
|
||||||
|
promptSub: Subscription,
|
||||||
|
wasLocal = false,
|
||||||
|
) {
|
||||||
|
const loader = this.loader.open('Saving').subscribe()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.api.setHostname({ hostname })
|
||||||
|
|
||||||
|
if (wasLocal) {
|
||||||
|
const { protocol, port } = this.win.location
|
||||||
|
const newUrl = `${protocol}//${hostname}.local${port ? ':' + port : ''}`
|
||||||
|
|
||||||
|
this.dialog
|
||||||
|
.openConfirm({
|
||||||
|
label: 'Hostname Changed',
|
||||||
|
data: {
|
||||||
|
content:
|
||||||
|
`${this.i18n.transform('Your server is now reachable at')} ${hostname}.local` as i18nKey,
|
||||||
|
yes: 'Open new address',
|
||||||
|
no: 'Dismiss',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.pipe(filter(Boolean))
|
||||||
|
.subscribe(() => this.win.open(newUrl, '_blank'))
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
this.errorService.handleError(e)
|
||||||
|
} finally {
|
||||||
|
loader.unsubscribe()
|
||||||
|
promptSub.unsubscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onTitle() {
|
onTitle() {
|
||||||
const sub = this.dialog
|
const sub = this.dialog
|
||||||
.openPrompt<string>({
|
.openPrompt<string>({
|
||||||
|
|||||||
@@ -121,6 +121,8 @@ export abstract class ApiService {
|
|||||||
|
|
||||||
abstract toggleKiosk(enable: boolean): Promise<null>
|
abstract toggleKiosk(enable: boolean): Promise<null>
|
||||||
|
|
||||||
|
abstract setHostname(params: { hostname: string }): Promise<null>
|
||||||
|
|
||||||
abstract setKeyboard(params: FullKeyboard): Promise<null>
|
abstract setKeyboard(params: FullKeyboard): Promise<null>
|
||||||
|
|
||||||
abstract setLanguage(params: SetLanguageParams): Promise<null>
|
abstract setLanguage(params: SetLanguageParams): Promise<null>
|
||||||
|
|||||||
@@ -255,6 +255,10 @@ export class LiveApiService extends ApiService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setHostname(params: { hostname: string }): Promise<null> {
|
||||||
|
return this.rpcRequest({ method: 'server.set-hostname', params })
|
||||||
|
}
|
||||||
|
|
||||||
async setKeyboard(params: FullKeyboard): Promise<null> {
|
async setKeyboard(params: FullKeyboard): Promise<null> {
|
||||||
return this.rpcRequest({ method: 'server.set-keyboard', params })
|
return this.rpcRequest({ method: 'server.set-keyboard', params })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -447,6 +447,21 @@ export class MockApiService extends ApiService {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setHostname(params: { hostname: string }): Promise<null> {
|
||||||
|
await pauseFor(1000)
|
||||||
|
|
||||||
|
const patch = [
|
||||||
|
{
|
||||||
|
op: PatchOp.REPLACE,
|
||||||
|
path: '/serverInfo/hostname',
|
||||||
|
value: params.hostname,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
this.mockRevision(patch)
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
async setKeyboard(params: FullKeyboard): Promise<null> {
|
async setKeyboard(params: FullKeyboard): Promise<null> {
|
||||||
await pauseFor(1000)
|
await pauseFor(1000)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user