fix password input for backups and add adjective noun randomizer

This commit is contained in:
Matt Hill
2026-03-23 08:58:37 -06:00
parent f60a1a9ed0
commit 3d45234aae
11 changed files with 9115 additions and 71 deletions

View File

@@ -7,7 +7,13 @@ import {
Validators,
} from '@angular/forms'
import { Router } from '@angular/router'
import { ErrorService, i18nPipe, normalizeHostname } from '@start9labs/shared'
import {
ErrorService,
i18nPipe,
normalizeHostname,
randomServerName,
serverNameValidator,
} from '@start9labs/shared'
import { TuiAutoFocus, TuiMapperPipe, TuiValidator } from '@taiga-ui/cdk'
import {
TuiButton,
@@ -42,10 +48,17 @@ import { StateService } from '../services/state.service'
@if (isFresh) {
<tui-textfield>
<label tuiLabel>{{ 'Server Name' | i18n }}</label>
<input tuiInput tuiAutoFocus formControlName="name" />
<input tuiInput formControlName="name" />
<button
tuiIconButton
type="button"
appearance="icon"
iconStart="@tui.refresh-cw"
(click)="randomizeName()"
></button>
</tui-textfield>
<tui-error formControlName="name" />
@if (form.controls.name.value?.trim()) {
@if (form.controls.name.value?.trim() && !form.controls.name.errors) {
<tui-error class="g-secondary" error="{{ derivedHostname }}.local" />
}
}
@@ -127,6 +140,8 @@ import { StateService } from '../services/state.service'
minlength: 'Must be 12 characters or greater',
maxlength: 'Must be 64 character or less',
match: 'Passwords do not match',
hostnameMinLength: 'Hostname must be at least 4 characters',
hostnameMaxLength: 'Hostname must be 63 characters or less',
}),
],
})
@@ -147,7 +162,10 @@ export default class PasswordPage {
Validators.maxLength(64),
]),
confirm: new FormControl(''),
name: new FormControl('', this.isFresh ? [Validators.required] : []),
name: new FormControl(
this.isFresh ? randomServerName() : '',
this.isFresh ? [Validators.required, serverNameValidator] : [],
),
})
readonly validator = (value: string) => (control: AbstractControl) =>
@@ -155,6 +173,10 @@ export default class PasswordPage {
? null
: { match: this.i18n.transform('Passwords do not match') }
randomizeName() {
this.form.controls.name.setValue(randomServerName())
}
get derivedHostname(): string {
return normalizeHostname(this.form.controls.name.value || '')
}

View File

@@ -10,8 +10,8 @@ import {
import { T } from '@start9labs/start-sdk'
import * as jose from 'node-jose'
import { interval, map, Observable } from 'rxjs'
import { ApiService } from './api.service'
import { InstallOsParams, InstallOsRes } from '../types'
import { ApiService } from './api.service'
@Injectable({
providedIn: 'root',
@@ -127,7 +127,7 @@ export class MockApiService extends ApiService {
return {
'9876-5432-1234-5678': {
hostname: 'adjective-noun',
version: '0.3.6',
version: '0.4.0',
timestamp: new Date().toISOString(),
passwordHash:
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
@@ -135,7 +135,7 @@ export class MockApiService extends ApiService {
},
'9876-5432-1234-5671': {
hostname: 'adjective-noun',
version: '0.3.6',
version: '0.4.0',
timestamp: new Date().toISOString(),
passwordHash:
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
@@ -299,7 +299,7 @@ const MOCK_DISKS: DiskInfo[] = [
startOs: {
'small-server-id': {
hostname: 'small-server',
version: '0.3.6',
version: '0.4.0',
timestamp: new Date().toISOString(),
passwordHash:
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
@@ -348,7 +348,7 @@ const MOCK_DISKS: DiskInfo[] = [
startOs: {
'1234-5678-9876-5432': {
hostname: 'existing-server',
version: '0.3.6',
version: '0.4.0',
timestamp: new Date().toISOString(),
passwordHash:
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',

View File

@@ -1,7 +1,8 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { TuiAutoFocus } from '@taiga-ui/cdk'
import { TuiButton, TuiDialogContext, TuiInput } from '@taiga-ui/core'
import { TuiButton, TuiDialogContext, TuiIcon, TuiInput } from '@taiga-ui/core'
import { TuiPassword } from '@taiga-ui/kit'
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { i18nPipe } from '../i18n/i18n.pipe'
import { i18nKey } from '../i18n/i18n.providers'
@@ -27,22 +28,12 @@ import { i18nKey } from '../i18n/i18n.providers'
tuiAutoFocus
[ngModelOptions]="{ standalone: true }"
[(ngModel)]="value"
[class.masked]="options.useMask && masked && value"
[placeholder]="options.placeholder || ''"
[type]="options.useMask ? 'password' : 'text'"
[autocomplete]="options.useMask ? 'off' : ''"
/>
@if (options.useMask) {
<button
tuiIconButton
type="button"
appearance="icon"
title="Toggle masking"
size="xs"
class="button"
[iconStart]="masked ? '@tui.eye' : '@tui.eye-off'"
(click)="masked = !masked"
>
{{ 'Reveal/Hide' | i18n }}
</button>
<tui-icon tuiPassword />
}
</tui-textfield>
@if (error) {
@@ -71,24 +62,22 @@ import { i18nKey } from '../i18n/i18n.providers'
.error {
color: var(--tui-status-negative);
}
.button {
pointer-events: auto;
margin-left: 0.25rem;
}
.masked {
-webkit-text-security: disc;
}
`,
imports: [FormsModule, TuiButton, TuiInput, TuiAutoFocus, i18nPipe],
imports: [
FormsModule,
TuiButton,
TuiIcon,
TuiInput,
TuiPassword,
TuiAutoFocus,
i18nPipe,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PromptModal {
private readonly context =
injectContext<TuiDialogContext<string, PromptOptions>>()
masked = this.options.useMask
value = this.options.initialValue || ''
error = ''

View File

@@ -57,3 +57,5 @@ export * from './util/unused'
export * from './util/keyboards'
export * from './util/languages'
export * from './util/hostname'
export * from './util/random-server-name'
export * from './util/server-name-validator'

View File

@@ -1,8 +1,8 @@
/**
* TS port of the Rust `normalize()` function from core/src/hostname.rs.
* Converts a free-text name into a valid hostname.
* Strips non-alphanumeric characters and produces a raw hostname string.
*/
export function normalizeHostname(name: string): string {
export function normalizeHostnameRaw(name: string): string {
let prevWasDash = true
let normalized = ''
@@ -20,5 +20,12 @@ export function normalizeHostname(name: string): string {
normalized = normalized.slice(0, -1)
}
return normalized || 'start9'
return normalized
}
/**
* Converts a free-text name into a valid hostname, with 'start9' fallback.
*/
export function normalizeHostname(name: string): string {
return normalizeHostnameRaw(name) || 'start9'
}

View File

@@ -0,0 +1,16 @@
import { ADJECTIVES, NOUNS } from './server-name-words'
/**
* Generates a random server name in "Adjective Noun" format.
* Uses the same word lists as the Rust backend (core/src/assets/).
*/
export function randomServerName(): string {
const adj = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)]!
const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)]!
return `${capitalize(adj)} ${capitalize(noun)}`
}
function capitalize(word: string): string {
return word.charAt(0).toUpperCase() + word.slice(1)
}

View File

@@ -0,0 +1,26 @@
import { AbstractControl, ValidationErrors } from '@angular/forms'
import { normalizeHostnameRaw } from './hostname'
// Matches backend normalize() threshold (core/src/hostname.rs:109)
const HOSTNAME_MIN_LENGTH = 4
// DNS label limit
const HOSTNAME_MAX_LENGTH = 63
export function serverNameValidator(
control: AbstractControl,
): ValidationErrors | null {
const name = (control.value || '').trim()
if (!name) return null
const hostname = normalizeHostnameRaw(name)
if (hostname.length < HOSTNAME_MIN_LENGTH) {
return { hostnameMinLength: true }
}
if (hostname.length > HOSTNAME_MAX_LENGTH) {
return { hostnameMaxLength: true }
}
return null
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,12 @@
import { Component } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { i18nPipe, normalizeHostname } from '@start9labs/shared'
import { TuiButton, TuiDialogContext, TuiInput } from '@taiga-ui/core'
import {
i18nPipe,
normalizeHostname,
normalizeHostnameRaw,
randomServerName,
} from '@start9labs/shared'
import { TuiButton, TuiDialogContext, TuiError, TuiInput } from '@taiga-ui/core'
import { injectContext } from '@taiga-ui/polymorpheus'
@Component({
@@ -9,15 +14,28 @@ import { injectContext } from '@taiga-ui/polymorpheus'
<tui-textfield>
<label tuiLabel>{{ 'Server Name' | i18n }}</label>
<input tuiInput [(ngModel)]="name" />
<button
tuiIconButton
type="button"
appearance="icon"
iconStart="@tui.refresh-cw"
(click)="randomizeName()"
></button>
</tui-textfield>
@if (name.trim()) {
@if (hostnameError) {
<tui-error [error]="hostnameError" />
} @else if (name.trim()) {
<p class="hostname-preview">{{ normalizeHostname(name) }}.local</p>
}
<footer>
<button tuiButton appearance="secondary" (click)="cancel()">
{{ 'Cancel' | i18n }}
</button>
<button tuiButton [disabled]="!name.trim()" (click)="confirm()">
<button
tuiButton
[disabled]="!name.trim() || hostnameError"
(click)="confirm()"
>
{{ 'Save' | i18n }}
</button>
</footer>
@@ -35,7 +53,7 @@ import { injectContext } from '@taiga-ui/polymorpheus'
margin-top: 1.5rem;
}
`,
imports: [FormsModule, TuiButton, TuiInput, i18nPipe],
imports: [FormsModule, TuiButton, TuiError, TuiInput, i18nPipe],
})
export class ServerNameDialog {
private readonly context =
@@ -49,6 +67,22 @@ export class ServerNameDialog {
name = this.context.data.initialName
readonly normalizeHostname = normalizeHostname
get hostnameError(): string | null {
const name = this.name.trim()
if (!name) return null
const hostname = normalizeHostnameRaw(name)
if (hostname.length < 4) return 'Hostname must be at least 4 characters'
if (hostname.length > 63) return 'Hostname must be 63 characters or less'
return null
}
randomizeName() {
this.name = randomServerName()
}
cancel() {
this.context.completeWith(null)
}

View File

@@ -1,12 +1,12 @@
import { GetPackagesRes } from '@start9labs/marketplace'
import { ISB, IST, T } from '@start9labs/start-sdk'
import {
InstalledState,
PackageDataEntry,
} from 'src/app/services/patch-db/data-model'
import { ActionRes } from './api.types'
import { BTC_ICON, LND_ICON, PROXY_ICON, REGISTRY_ICON } from './api-icons'
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
import { T, ISB, IST } from '@start9labs/start-sdk'
import { GetPackagesRes } from '@start9labs/marketplace'
import { BTC_ICON, LND_ICON, PROXY_ICON, REGISTRY_ICON } from './api-icons'
import { ActionRes } from './api.types'
import markdown from './md-sample.md'
@@ -33,14 +33,14 @@ export namespace Mock {
'0.4.1': {
headline: 'v0.4.1',
releaseNotes: 'Testing some release notes',
sourceVersion: '>=0.3.5:0 <=0.3.6-alpha.17:0',
sourceVersion: '>=0.3.5:0 <=0.4.0-alpha.17:0',
authorized: ['G24CSA5HNYEPIXJNMK7ZM4KD5SX5N6X4'],
iso: {},
squashfs: {
aarch64: {
publishedAt: '2025-03-21T23:55:29.583006392Z',
urls: [
'https://alpha-registry-x.start9.com/startos/v0.3.6-alpha.17/startos-0.3.6-alpha.17-b8ff331~dev_aarch64.squashfs',
'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.17/startos-0.4.0-alpha.17-b8ff331~dev_aarch64.squashfs',
],
commitment: {
hash: 'OUnANnZePtf7rSbj38JESl+iJAV0z0aiZ4opCiwpGbo=',
@@ -54,7 +54,7 @@ export namespace Mock {
'aarch64-nonfree': {
publishedAt: '2025-03-21T23:56:38.299572946Z',
urls: [
'https://alpha-registry-x.start9.com/startos/v0.3.6-alpha.17/startos-0.3.6-alpha.17-b8ff331~dev_aarch64-nonfree.squashfs',
'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.17/startos-0.4.0-alpha.17-b8ff331~dev_aarch64-nonfree.squashfs',
],
commitment: {
hash: '6k+0RcyRQV+5A+h06OqpHxd4IT6IlFkfdy9dfHIP90c=',
@@ -68,7 +68,7 @@ export namespace Mock {
raspberrypi: {
publishedAt: '2025-03-22T00:08:17.083064390Z',
urls: [
'https://alpha-registry-x.start9.com/startos/v0.3.6-alpha.17/startos-0.3.6-alpha.17-b8ff331~dev_raspberrypi.squashfs',
'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.17/startos-0.4.0-alpha.17-b8ff331~dev_raspberrypi.squashfs',
],
commitment: {
hash: 'K+XuTZxo1KVsKjNSV8PPOMruCvAEZwerF9mbpFl53Gk=',
@@ -82,7 +82,7 @@ export namespace Mock {
x86_64: {
publishedAt: '2025-03-22T00:05:57.684319247Z',
urls: [
'https://alpha-registry-x.start9.com/startos/v0.3.6-alpha.17/startos-0.3.6-alpha.17-b8ff331~dev_x86_64.squashfs',
'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.17/startos-0.4.0-alpha.17-b8ff331~dev_x86_64.squashfs',
],
commitment: {
hash: '3UVkx3TQMBPlSU1OnV48Om9vjjA3s+Nk6dX3auYGpBo=',
@@ -96,7 +96,7 @@ export namespace Mock {
'x86_64-nonfree': {
publishedAt: '2025-03-22T00:07:11.893777122Z',
urls: [
'https://alpha-registry-x.start9.com/startos/v0.3.6-alpha.17/startos-0.3.6-alpha.17-b8ff331~dev_x86_64-nonfree.squashfs',
'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.17/startos-0.4.0-alpha.17-b8ff331~dev_x86_64-nonfree.squashfs',
],
commitment: {
hash: 'IS1gJ56n/HlQqFbl1upMOAtLxyxB0cY0H89Ha+9h1lE=',
@@ -454,7 +454,7 @@ export namespace Mock {
marketingUrl: 'https://bitcoin.org',
docsUrls: ['https://bitcoin.org'],
releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6',
osVersion: '0.4.0',
sdkVersion: '0.4.0-beta.49',
gitHash: 'fakehash',
icon: BTC_ICON,
@@ -496,7 +496,7 @@ export namespace Mock {
marketingUrl: 'https://bitcoinknots.org',
docsUrls: ['https://bitcoinknots.org'],
releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6',
osVersion: '0.4.0',
sdkVersion: '0.4.0-beta.49',
gitHash: 'fakehash',
icon: BTC_ICON,
@@ -548,7 +548,7 @@ export namespace Mock {
marketingUrl: 'https://bitcoin.org',
docsUrls: ['https://bitcoin.org'],
releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6',
osVersion: '0.4.0',
sdkVersion: '0.4.0-beta.49',
gitHash: 'fakehash',
icon: BTC_ICON,
@@ -590,7 +590,7 @@ export namespace Mock {
marketingUrl: 'https://bitcoinknots.org',
docsUrls: ['https://bitcoinknots.org'],
releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6',
osVersion: '0.4.0',
sdkVersion: '0.4.0-beta.49',
gitHash: 'fakehash',
icon: BTC_ICON,
@@ -644,7 +644,7 @@ export namespace Mock {
marketingUrl: 'https://lightning.engineering/',
docsUrls: ['https://lightning.engineering/'],
releaseNotes: 'Upstream release to 0.17.5',
osVersion: '0.3.6',
osVersion: '0.4.0',
sdkVersion: '0.4.0-beta.49',
gitHash: 'fakehash',
icon: LND_ICON,
@@ -699,7 +699,7 @@ export namespace Mock {
marketingUrl: 'https://lightning.engineering/',
docsUrls: ['https://lightning.engineering/'],
releaseNotes: 'Upstream release to 0.17.4',
osVersion: '0.3.6',
osVersion: '0.4.0',
sdkVersion: '0.4.0-beta.49',
gitHash: 'fakehash',
icon: LND_ICON,
@@ -758,7 +758,7 @@ export namespace Mock {
marketingUrl: 'https://bitcoin.org',
docsUrls: ['https://bitcoin.org'],
releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6',
osVersion: '0.4.0',
sdkVersion: '0.4.0-beta.49',
gitHash: 'fakehash',
icon: BTC_ICON,
@@ -800,7 +800,7 @@ export namespace Mock {
marketingUrl: 'https://bitcoinknots.org',
docsUrls: [],
releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6',
osVersion: '0.4.0',
sdkVersion: '0.4.0-beta.49',
gitHash: 'fakehash',
icon: BTC_ICON,
@@ -852,7 +852,7 @@ export namespace Mock {
marketingUrl: 'https://lightning.engineering/',
docsUrls: [],
releaseNotes: 'Upstream release and minor fixes.',
osVersion: '0.3.6',
osVersion: '0.4.0',
sdkVersion: '0.4.0-beta.49',
gitHash: 'fakehash',
icon: LND_ICON,
@@ -907,7 +907,7 @@ export namespace Mock {
docsUrls: [],
marketingUrl: '',
releaseNotes: 'Upstream release and minor fixes.',
osVersion: '0.3.6',
osVersion: '0.4.0',
sdkVersion: '0.4.0-beta.49',
gitHash: 'fakehash',
icon: PROXY_ICON,
@@ -1013,7 +1013,7 @@ export namespace Mock {
createdAt: '2019-12-26T14:20:30.872Z',
code: 2,
level: 'success',
title: 'Welcome to StartOS 0.3.6!',
title: 'Welcome to StartOS 0.4.0!',
message: 'Click "View Details" to learn all about the new version',
data: markdown,
seen: false,
@@ -1207,7 +1207,7 @@ export namespace Mock {
'1234-5678-9876-5432': {
hostname: 'adjective-noun',
timestamp: new Date().toISOString(),
version: '0.3.6',
version: '0.4.0',
passwordHash:
// password is asdfasdf
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
@@ -1247,7 +1247,7 @@ export namespace Mock {
'1234-5678-9876-5432': {
hostname: 'adjective-noun',
timestamp: new Date().toISOString(),
version: '0.3.6',
version: '0.4.0',
passwordHash:
// password is asdfasdf
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
@@ -1282,7 +1282,7 @@ export namespace Mock {
// startOs: {
// abcdefgh: {
// hostname: 'adjective-noun.local',
// version: '0.3.6',
// version: '0.4.0',
// timestamp: new Date().toISOString(),
// passwordHash:
// '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
@@ -1321,7 +1321,7 @@ export namespace Mock {
// startOs: {
// 'different-server': {
// hostname: 'different-server.local',
// version: '0.3.6',
// version: '0.4.0',
// timestamp: new Date().toISOString(),
// passwordHash:
// '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
@@ -1387,19 +1387,19 @@ export namespace Mock {
// ]
export const BackupInfo: T.BackupInfo = {
version: '0.3.6',
version: '0.4.0',
timestamp: new Date().toISOString(),
packageBackups: {
bitcoind: {
title: 'Bitcoin Core',
version: '0.21.0:0',
osVersion: '0.3.6',
osVersion: '0.4.0',
timestamp: new Date().toISOString(),
},
'btc-rpc-proxy': {
title: 'Bitcoin Proxy',
version: '0.2.2:0',
osVersion: '0.3.6',
osVersion: '0.4.0',
timestamp: new Date().toISOString(),
},
},

View File

@@ -1,6 +1,6 @@
import { DataModel } from 'src/app/services/patch-db/data-model'
import { Mock } from './api.fixures'
import { knownAuthorities } from 'src/app/utils/acme'
import { Mock } from './api.fixures'
const version = require('../../../../../../package.json').version
export const mockPatchData: DataModel = {