enable switching to https on login page (#2406)

* enable switching to https on login page

* add trust Root CA to http login page

* add node-jose back for setup wiz

* add tooltips, branding, logic for launch box spinner display, and enable config to toggle https mode on mocks

* cleanup

* copy changes

* style fixes

* abstract component, fix https mocks

* always show login from localhost

* launch .local when on IP

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
This commit is contained in:
Lucy
2023-09-26 14:47:47 -04:00
committed by GitHub
parent 8e21504bdb
commit 0daaf3b1ec
25 changed files with 511 additions and 255 deletions

View File

@@ -36,6 +36,11 @@
"input": "node_modules/monaco-editor",
"output": "assets/monaco-editor/"
},
{
"glob": "**/*",
"input": "node_modules/@taiga-ui/icons/src",
"output": "assets/taiga-ui/icons"
},
"projects/ui/src/manifest.webmanifest",
{
"glob": "ngsw.json",
@@ -44,6 +49,7 @@
}
],
"styles": [
"node_modules/@taiga-ui/core/styles/taiga-ui-theme.less",
"projects/shared/styles/variables.scss",
"projects/shared/styles/global.scss",
"projects/shared/styles/shared.scss",

View File

@@ -14,6 +14,7 @@
},
"mocks": {
"maskAs": "tor",
"maskAsHttps": true,
"skipStartupAlerts": true
}
},

View File

@@ -45,7 +45,7 @@
"monaco-editor": "^0.33.0",
"mustache": "^4.2.0",
"ng-qrcode": "^7.0.0",
"node-jose": "^2.1.1",
"node-jose": "^2.2.0",
"patch-db-client": "file: ../../../patch-db/client",
"pbkdf2": "^3.1.2",
"rxjs": "^7.5.6",
@@ -9506,9 +9506,9 @@
}
},
"node_modules/long": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.0.tgz",
"integrity": "sha512-9RTUNjK60eJbx3uz+TEGF7fUr29ZDxR5QzXcyDpeSfeH28S9ycINflOgOlppit5U+4kNTe83KQnMEerw7GmE8w=="
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
},
"node_modules/lru-cache": {
"version": "7.14.0",
@@ -10955,9 +10955,9 @@
}
},
"node_modules/pako": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz",
"integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg=="
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="
},
"node_modules/parent-module": {
"version": "1.0.1",
@@ -22148,9 +22148,9 @@
}
},
"long": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.0.tgz",
"integrity": "sha512-9RTUNjK60eJbx3uz+TEGF7fUr29ZDxR5QzXcyDpeSfeH28S9ycINflOgOlppit5U+4kNTe83KQnMEerw7GmE8w=="
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
},
"lru-cache": {
"version": "7.14.0",
@@ -23251,9 +23251,9 @@
}
},
"pako": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz",
"integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg=="
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="
},
"parent-module": {
"version": "1.0.1",

View File

@@ -70,7 +70,7 @@
"monaco-editor": "^0.33.0",
"mustache": "^4.2.0",
"ng-qrcode": "^7.0.0",
"node-jose": "^2.1.1",
"node-jose": "^2.2.0",
"patch-db-client": "file: ../../../patch-db/client",
"pbkdf2": "^3.1.2",
"rxjs": "^7.5.6",

View File

@@ -38,12 +38,13 @@ export class HttpService {
async rpcRequest<T>(
opts: RPCOptions,
fullUrl?: string,
): Promise<LocalHttpResponse<RPCResponse<T>>> {
const { method, headers, params, timeout } = opts
return this.httpRequest<RPCResponse<T>>({
method: Method.POST,
url: this.relativeUrl,
url: fullUrl || this.relativeUrl,
headers,
body: { method, params },
timeout,

View File

@@ -15,7 +15,9 @@ export type WorkspaceConfig = {
community: 'https://community-registry.start9.com/'
}
mocks: {
maskAs: 'tor' | 'lan'
maskAs: 'tor' | 'local' | 'localhost'
// enables local development in secure mode
maskAsHttps: boolean
skipStartupAlerts: boolean
}
}

View File

@@ -15,63 +15,33 @@
<!-- Installed -->
<ng-template #installed>
<!-- SECURE -->
<ng-container *ngIf="secure; else insecure">
<ng-container *ngIf="pkg | toDependencies as dependencies">
<ion-item-group *ngIf="pkg | toStatus as status">
<!-- ** status ** -->
<app-show-status
<ng-container *ngIf="pkg | toDependencies as dependencies">
<ion-item-group *ngIf="pkg | toStatus as status">
<!-- ** status ** -->
<app-show-status
[pkg]="pkg"
[dependencies]="dependencies"
[status]="status"
></app-show-status>
<!-- ** installed && !backing-up ** -->
<ng-container *ngIf="isInstalled(pkg) && !isBackingUp(status)">
<!-- ** health checks ** -->
<app-show-health-checks
*ngIf="isRunning(status)"
[pkg]="pkg"
></app-show-health-checks>
<!-- ** dependencies ** -->
<app-show-dependencies
*ngIf="dependencies.length"
[dependencies]="dependencies"
[status]="status"
></app-show-status>
<!-- ** installed && !backing-up ** -->
<ng-container *ngIf="isInstalled(pkg) && !isBackingUp(status)">
<!-- ** health checks ** -->
<app-show-health-checks
*ngIf="isRunning(status)"
[pkg]="pkg"
></app-show-health-checks>
<!-- ** dependencies ** -->
<app-show-dependencies
*ngIf="dependencies.length"
[dependencies]="dependencies"
></app-show-dependencies>
<!-- ** menu ** -->
<app-show-menu [buttons]="pkg | toButtons"></app-show-menu>
<!-- ** additional ** -->
<app-show-additional [pkg]="pkg"></app-show-additional>
</ng-container>
</ion-item-group>
</ng-container>
></app-show-dependencies>
<!-- ** menu ** -->
<app-show-menu [buttons]="pkg | toButtons"></app-show-menu>
<!-- ** additional ** -->
<app-show-additional [pkg]="pkg"></app-show-additional>
</ng-container>
</ion-item-group>
</ng-container>
<!-- INSECURE -->
<ng-template #insecure>
<ion-grid style="max-width: 540px">
<ion-row class="ion-align-items-center">
<ion-col class="ion-text-center">
<h2>
<ion-text color="warning">Http detected</ion-text>
</h2>
<p class="ion-padding-bottom">
Your connection is insecure.
<a
[routerLink]="['/system', 'root-ca']"
style="color: var(--ion-color-dark)"
>
Download and trust your server's Root CA
</a>
, then switch to https.
</p>
<ion-button (click)="launchHttps()">
Open https
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-button>
</ion-col>
</ion-row>
</ion-grid>
</ng-template>
</ng-template>
</ion-content>
</ng-container>

View File

@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { NavController } from '@ionic/angular'
import { PatchDB } from 'patch-db-client'
import {
@@ -13,9 +13,6 @@ import {
import { tap } from 'rxjs/operators'
import { ActivatedRoute } from '@angular/router'
import { getPkgId } from '@start9labs/shared'
import { DOCUMENT } from '@angular/common'
import { ConfigService } from 'src/app/services/config.service'
import { getServerInfo } from 'src/app/util/get-server-info'
const STATES = [
PackageState.Installing,
@@ -29,8 +26,6 @@ const STATES = [
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppShowPage {
readonly secure = this.config.isSecure()
private readonly pkgId = getPkgId(this.route)
readonly pkg$ = this.patch.watch$('package-data', this.pkgId).pipe(
@@ -44,8 +39,6 @@ export class AppShowPage {
private readonly route: ActivatedRoute,
private readonly navCtrl: NavController,
private readonly patch: PatchDB<DataModel>,
private readonly config: ConfigService,
@Inject(DOCUMENT) private readonly document: Document,
) {}
isInstalled({ state }: PackageDataEntry): boolean {
@@ -63,11 +56,4 @@ export class AppShowPage {
showProgress({ state }: PackageDataEntry): boolean {
return STATES.includes(state)
}
async launchHttps() {
const onTor = this.config.isTor()
const { 'lan-address': lanAddress, 'tor-address': torAddress } =
await getServerInfo(this.patch)
onTor ? window.open(torAddress) : window.open(lanAddress)
}
}

View File

@@ -0,0 +1,108 @@
<ion-grid class="grid-wiz">
<img width="60px" height="60px" src="/assets/img/icon_transparent.png" />
<ion-row>
<ion-col class="ion-text-center">
<ion-icon name="lock-closed-outline" class="wiz-icon"></ion-icon>
</ion-col>
</ion-row>
<ion-row>
<ion-col class="ion-text-center">
<h2><b>Trust your Root Certificate Authority (CA)</b></h2>
<p>
Download and trust your server's Root CA to establish secure, encrypted
(
<b>HTTPS</b>
) connections with your server
</p>
</ion-col>
</ion-row>
<ion-row>
<ion-col sizeXs="12" sizeLg="4">
<div class="wiz-card">
<ion-row class="ion-justify-content-between">
<b class="wiz-step">1</b>
<tui-tooltip
content="Your server uses its Root CA to generate SSL/TLS certificates for itself and its installed services. These certificates are used to encrypt network traffic with your client devices."
direction="right"
></tui-tooltip>
</ion-row>
<div class="ion-text-center">
<h2>Download Root CA</h2>
<p>Download your server's Root CA</p>
</div>
<ion-button class="wiz-card-button" shape="round" (click)="download()">
<ion-icon slot="start" name="download-outline"></ion-icon>
Download
</ion-button>
</div>
</ion-col>
<ion-col sizeXs="12" sizeLg="4">
<div class="wiz-card" [class.disabled]="!downloadClicked">
<ion-row class="ion-justify-content-between">
<b class="wiz-step">2</b>
<tui-tooltip
content="By trusting your server's Root CA, your device can verify the authenticity of its encrypted communications with your server and installed services. You will need to trust the Root CA on every device used to connect to your server."
direction="right"
></tui-tooltip>
</ion-row>
<div class="ion-text-center">
<h2>Trust Root CA</h2>
<p>Follow instructions for your OS</p>
</div>
<ion-button
class="wiz-card-button"
shape="round"
(click)="instructions()"
[disabled]="!downloadClicked"
>
View Docs
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-button>
</div>
</ion-col>
<ion-col sizeXs="12" sizeLg="4">
<div class="wiz-card" [class.disabled]="!polling && !caTrusted">
<b class="wiz-step">3</b>
<div class="ion-text-center">
<h2>Go To Login</h2>
<p *ngIf="instructionsClicked; else space" class="inline-center">
<ion-spinner
class="wiz-spinner"
*ngIf="!caTrusted; else trusted"
></ion-spinner>
<ng-template #trusted>
<ion-icon name="ribbon-outline" color="success"></ion-icon>
</ng-template>
&nbsp;{{ caTrusted ? 'Root CA trusted!' : 'Waiting for trust...' }}
</p>
<ng-template #space>
<!-- to keep alignment -->
<p><br /></p>
</ng-template>
</div>
<ion-button
class="wiz-card-button"
shape="round"
(click)="launchHttps()"
[disabled]="!caTrusted"
>
Open
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-button>
</div>
</ion-col>
</ion-row>
<ion-row>
<ion-col class="ion-text-center">
<ion-button fill="clear" (click)="launchHttps()" [disabled]="caTrusted">
Skip
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-button>
</ion-col>
</ion-row>
</ion-grid>
<a
id="install-cert"
href="/public/eos/local.crt"
[download]="document.location.hostname"
></a>

View File

@@ -0,0 +1,44 @@
.grid-wiz {
--ion-grid-padding: 36px;
height: 100%
}
.wiz-icon {
font-size: 84px;
}
.wiz-card {
background: #414141;
margin: 24px;
padding: 16px;
height: 280px;
border-radius: 16px;
display: grid;
& h2 {
font-weight: 600;
}
}
.wiz-card-button {
justify-self: center;
white-space: normal;
}
.wiz-spinner {
width: 14px;
height: 14px;
}
.disabled {
filter: saturate(0.2) contrast(0.5)
}
.wiz-step {
margin-top: 4px;
}
.inline-center {
display: inline-flex;
align-items: center;
}

View File

@@ -0,0 +1,76 @@
import { Component, Inject } from '@angular/core'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ConfigService } from 'src/app/services/config.service'
import { pauseFor, RELATIVE_URL } from '@start9labs/shared'
import { DOCUMENT } from '@angular/common'
import { WINDOW } from '@ng-web-apis/common'
@Component({
selector: 'ca-wizard',
templateUrl: './ca-wizard.component.html',
styleUrls: ['./ca-wizard.component.scss'],
})
export class CAWizardComponent {
downloadClicked = false
instructionsClicked = false
polling = false
caTrusted = false
constructor(
private readonly api: ApiService,
public readonly config: ConfigService,
@Inject(RELATIVE_URL) private readonly relativeUrl: string,
@Inject(DOCUMENT) public readonly document: Document,
@Inject(WINDOW) private readonly windowRef: Window,
) {}
async ngOnInit() {
if (!this.config.isSecure()) {
await this.testHttps().catch(e =>
console.warn('Failed Https connection attempt'),
)
}
}
download() {
this.downloadClicked = true
this.document.getElementById('install-cert')?.click()
}
instructions() {
this.windowRef.open(
'https://docs.start9.com/getting-started/trust-ca/#trust-your-root-ca',
'_blank',
'noreferrer',
)
this.instructionsClicked = true
this.startDaemon()
}
private async startDaemon(): Promise<void> {
this.polling = true
while (this.polling) {
try {
await this.testHttps()
this.polling = false
} catch (e) {
console.warn('Failed Https connection attempt')
await pauseFor(2000)
}
}
}
launchHttps() {
const host = this.config.getHost()
this.windowRef.open(`https://${host}`, '_blank', 'noreferrer')
}
private async testHttps() {
const url = `https://${this.document.location.host}${this.relativeUrl}`
await this.api.echo({ message: 'ping' }, url).then(() => {
this.downloadClicked = true
this.instructionsClicked = true
this.caTrusted = true
})
}
}

View File

@@ -4,7 +4,9 @@ import { CommonModule } from '@angular/common'
import { FormsModule } from '@angular/forms'
import { IonicModule } from '@ionic/angular'
import { LoginPage } from './login.page'
import { CAWizardComponent } from './ca-wizard/ca-wizard.component'
import { SharedPipesModule } from '@start9labs/shared'
import { TuiHintModule, TuiTooltipModule } from '@taiga-ui/core'
const routes: Routes = [
{
@@ -20,7 +22,9 @@ const routes: Routes = [
IonicModule,
SharedPipesModule,
RouterModule.forChild(routes),
TuiTooltipModule,
TuiHintModule,
],
declarations: [LoginPage],
declarations: [LoginPage, CAWizardComponent],
})
export class LoginPageModule {}

View File

@@ -1,55 +1,81 @@
<ion-content class="content">
<ion-grid class="grid">
<ion-row class="row">
<ion-col>
<img src="assets/img/logo.png" alt="Start9" class="logo" />
<!-- Local HTTP -->
<ng-container *ngIf="config.isLocalHttp(); else notLanHttp">
<ca-wizard></ca-wizard>
</ng-container>
<ion-card class="card">
<ion-card-header>
<ion-card-title class="title">StartOS Login</ion-card-title>
</ion-card-header>
<!-- not Local HTTP -->
<ng-template #notLanHttp>
<div *ngIf="config.isTorHttp()" class="banner">
<ion-item color="warning">
<ion-icon slot="start" name="warning-outline"></ion-icon>
<ion-label>
<h2 style="font-weight: bold">Http detected</h2>
<p style="font-weight: 600">Tor is faster over https.</p>
</ion-label>
<ion-button slot="end" color="light" (click)="launchHttps()">
Open Https
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-button>
</ion-item>
</div>
<ion-card-content class="ion-margin">
<form class="form" (submit)="submit()">
<ion-item-group>
<ion-item color="dark">
<ion-icon
slot="start"
name="key-outline"
style="margin-right: 16px"
></ion-icon>
<ion-input
name="password"
placeholder="Password"
[type]="unmasked ? 'text' : 'password'"
[(ngModel)]="password"
(ionChange)="error = ''"
maxlength="64"
></ion-input>
<ion-button fill="clear" color="light" (click)="toggleMask()">
<ion-grid class="grid">
<ion-row class="row">
<ion-col>
<img src="assets/img/logo.png" alt="Start9" class="logo" />
<ion-card class="card">
<ion-card-header>
<ion-card-title class="title">StartOS Login</ion-card-title>
</ion-card-header>
<ion-card-content class="ion-margin">
<form class="form" (submit)="submit()">
<ion-item-group>
<ion-item color="dark" class="login-item">
<ion-icon
slot="icon-only"
size="small"
[name]="unmasked ? 'eye-off-outline' : 'eye-outline'"
slot="start"
name="key-outline"
style="margin-right: 16px"
></ion-icon>
</ion-button>
</ion-item>
</ion-item-group>
<ion-button
class="login-button"
type="submit"
expand="block"
color="tertiary"
>
Login
</ion-button>
</form>
<p class="error">
<ion-text color="danger">{{ error }}</ion-text>
</p>
</ion-card-content>
</ion-card>
</ion-col>
</ion-row>
</ion-grid>
<ion-input
name="password"
placeholder="Password"
[type]="unmasked ? 'text' : 'password'"
[(ngModel)]="password"
(ionChange)="error = ''"
maxlength="64"
></ion-input>
<ion-button
fill="clear"
color="light"
(click)="unmasked = !unmasked"
>
<ion-icon
slot="icon-only"
size="small"
[name]="unmasked ? 'eye-off-outline' : 'eye-outline'"
></ion-icon>
</ion-button>
</ion-item>
</ion-item-group>
<ion-button
class="login-button side-button"
type="submit"
expand="block"
color="tertiary"
>
Login
</ion-button>
</form>
<p class="error">
<ion-text color="danger">{{ error }}</ion-text>
</p>
</ion-card-content>
</ion-card>
</ion-col>
</ion-row>
</ion-grid>
</ng-template>
</ion-content>

View File

@@ -18,7 +18,7 @@
}
.row {
height: 90%;
height: 100%;
align-items: center;
text-align: center;
}
@@ -34,18 +34,30 @@
padding-top: 4px;
}
ion-button {
.banner {
position: absolute;
padding: 20px;
width: 100%;
display: inline-block;
ion-item {
max-width: 800px;
margin: auto;
}
}
.side-button {
--border-radius: 0 4px 4px 0;
}
ion-item {
.login-item {
--border-style: solid;
--border-color: var(--ion-color-light);
--border-radius: 4px 0 0 4px;
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14),
0 1px 5px 0 rgba(0, 0, 0, 0.12);
ion-button {
.side-button {
--border-radius: 4px;
}
}
@@ -80,12 +92,12 @@ ion-card {
@media (max-width: 500px) {
ion-button {
.side-button {
--border-radius: 4px;
margin-top: 0.7rem;
}
ion-item {
.login-item {
--border-radius: 4px;
}
}

View File

@@ -1,9 +1,12 @@
import { Component } from '@angular/core'
import { LoadingController, getPlatforms } from '@ionic/angular'
import { Component, Inject } from '@angular/core'
import { getPlatforms, LoadingController } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { AuthService } from 'src/app/services/auth.service'
import { Router } from '@angular/router'
import { ConfigService } from 'src/app/services/config.service'
import { pauseFor, RELATIVE_URL } from '@start9labs/shared'
import { DOCUMENT } from '@angular/common'
import { WINDOW } from '@ng-web-apis/common'
@Component({
selector: 'login',
@@ -14,42 +17,71 @@ export class LoginPage {
password = ''
unmasked = false
error = ''
loader?: HTMLIonLoadingElement
secure = this.config.isSecure()
downloadClicked = false
instructionsClicked = false
polling = false
caTrusted = false
constructor(
private readonly router: Router,
private readonly authService: AuthService,
private readonly loadingCtrl: LoadingController,
private readonly api: ApiService,
private readonly config: ConfigService,
public readonly config: ConfigService,
@Inject(RELATIVE_URL) private readonly relativeUrl: string,
@Inject(DOCUMENT) public readonly document: Document,
@Inject(WINDOW) private readonly windowRef: Window,
) {}
async ionViewDidEnter() {
if (!this.secure) {
async ngOnInit() {
if (!this.config.isSecure()) {
await this.testHttps().catch(e =>
console.warn('Failed Https connection attempt'),
)
}
}
download() {
this.downloadClicked = true
this.document.getElementById('install-cert')?.click()
}
instructions() {
this.windowRef.open(
'https://docs.start9.com/getting-started/trust-ca/#trust-your-server-s-root-ca',
'_blank',
'noreferrer',
)
this.instructionsClicked = true
this.startDaemon()
}
private async startDaemon(): Promise<void> {
this.polling = true
while (this.polling) {
try {
await this.api.getPubKey()
} catch (e: any) {
this.error = e.message
await this.testHttps()
this.polling = false
} catch (e) {
console.warn('Failed Https connection attempt')
await pauseFor(2000)
}
}
}
ngOnDestroy() {
this.loader?.dismiss()
}
toggleMask() {
this.unmasked = !this.unmasked
launchHttps() {
const host = this.config.getHost()
this.windowRef.open(`https://${host}`, '_blank', 'noreferrer')
}
async submit() {
this.error = ''
this.loader = await this.loadingCtrl.create({
const loader = await this.loadingCtrl.create({
message: 'Logging in...',
})
await this.loader.present()
await loader.present()
try {
document.cookie = ''
@@ -58,9 +90,7 @@ export class LoginPage {
return
}
await this.api.login({
password: this.secure
? this.password
: await this.api.encrypt(this.password),
password: this.password,
metadata: { platforms: getPlatforms() },
})
@@ -71,7 +101,16 @@ export class LoginPage {
// code 7 is for incorrect password
this.error = e.code === 7 ? 'Invalid Password' : e.message
} finally {
this.loader.dismiss()
loader.dismiss()
}
}
private async testHttps() {
const url = `https://${this.document.location.host}${this.relativeUrl}`
await this.api.echo({ message: 'ping' }, url).then(() => {
this.downloadClicked = true
this.instructionsClicked = true
this.caTrusted = true
})
}
}

View File

@@ -15,13 +15,12 @@
<!-- loaded -->
<ion-item-group *ngIf="server$ | async as server; else loading">
<ion-item *ngIf="!secure || isTorHttp" color="warning">
<ion-item *ngIf="isTorHttp" color="warning">
<ion-icon slot="start" name="warning-outline"></ion-icon>
<ion-label>
<h2 style="font-weight: bold">Http detected</h2>
<p style="font-weight: 600">
{{ isTorHttp ? 'Tor is faster over https.' : 'Your connection is
insecure.' }}
Tor is faster over https.
<a
[routerLink]="['/system', 'root-ca']"
style="color: var(--ion-color-light)"

View File

@@ -9,7 +9,7 @@ import {
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ActivatedRoute } from '@angular/router'
import { PatchDB } from 'patch-db-client'
import { combineLatest, firstValueFrom, map, Observable, of } from 'rxjs'
import { firstValueFrom, Observable, of } from 'rxjs'
import { ErrorToastService } from '@start9labs/shared'
import { EOSService } from 'src/app/services/eos.service'
import { ClientStorageService } from 'src/app/services/client-storage.service'
@@ -41,9 +41,7 @@ export class ServerShowPage {
readonly showUpdate$ = this.eosService.showUpdate$
readonly showDiskRepair$ = this.ClientStorageService.showDiskRepair$
readonly secure = this.config.isSecure()
readonly isTorHttp =
this.config.isTor() && this.document.location.protocol === 'http:'
readonly isTorHttp = this.config.isTorHttp()
constructor(
private readonly alertCtrl: AlertController,
@@ -308,10 +306,8 @@ export class ServerShowPage {
}
async launchHttps() {
const onTor = this.config.isTor()
const { 'lan-address': lanAddress, 'tor-address': torAddress } =
await getServerInfo(this.patch)
onTor ? window.open(torAddress) : window.open(lanAddress)
const { 'tor-address': torAddress } = await getServerInfo(this.patch)
window.open(torAddress)
}
addClick(title: string) {
@@ -465,7 +461,7 @@ export class ServerShowPage {
action: () =>
this.navCtrl.navigateForward(['backup'], { relativeTo: this.route }),
detail: true,
disabled$: of(!this.secure),
disabled$: of(false),
},
{
title: 'Restore From Backup',
@@ -474,10 +470,7 @@ export class ServerShowPage {
action: () =>
this.navCtrl.navigateForward(['restore'], { relativeTo: this.route }),
detail: true,
disabled$: combineLatest([
this.eosService.updatingOrBackingUp$,
of(this.secure),
]).pipe(map(([updating, secure]) => updating || !secure)),
disabled$: this.eosService.updatingOrBackingUp$,
},
],
Manage: [
@@ -545,7 +538,7 @@ export class ServerShowPage {
icon: 'key-outline',
action: () => this.presentAlertResetPassword(),
detail: false,
disabled$: of(!this.secure),
disabled$: of(false),
},
{
title: 'Experimental Features',

View File

@@ -57,7 +57,7 @@ export class WifiPage {
}
async presentAlertCountry(): Promise<void> {
if (!this.config.isLan) {
if (!this.config.isLan()) {
const alert = await this.alertCtrl.create({
header: 'Cannot Complete Action',
message:

View File

@@ -22,7 +22,7 @@ export module RR {
// auth
export type LoginReq = {
password: Encrypted | string
password: string
metadata: SessionMetadata
} // auth.login - unauthed
export type loginRes = null
@@ -465,7 +465,3 @@ declare global {
parse<T>(text: Stringified<T>, reviver?: (key: any, value: any) => any): T
}
}
export type Encrypted = {
encrypted: string
}

View File

@@ -1,28 +1,12 @@
import { BehaviorSubject, Observable } from 'rxjs'
import { Update } from 'patch-db-client'
import { Encrypted, RR } from './api.types'
import { RR } from './api.types'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { Log } from '@start9labs/shared'
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
import type { JWK } from 'node-jose'
export abstract class ApiService {
protected readonly jose = import('node-jose')
readonly patchStream$ = new BehaviorSubject<Update<DataModel>[]>([])
pubkey?: JWK.Key
async encrypt(toEncrypt: string): Promise<Encrypted> {
const { pubkey } = this
if (!pubkey) throw new Error('No pubkey found!')
const encrypted = await this.jose.then(jose =>
jose.JWE.createEncrypt(pubkey).update(toEncrypt).final(),
)
return { encrypted }
}
// http
@@ -41,8 +25,6 @@ export abstract class ApiService {
// auth
abstract getPubKey(): Promise<void>
abstract login(params: RR.LoginReq): Promise<RR.loginRes>
abstract logout(params: RR.LogoutReq): Promise<RR.LogoutRes>
@@ -57,7 +39,7 @@ export abstract class ApiService {
// server
abstract echo(params: RR.EchoReq): Promise<RR.EchoRes>
abstract echo(params: RR.EchoReq, urlOverride?: string): Promise<RR.EchoRes>
abstract openPatchWebsocket$(): Observable<Update<DataModel>>

View File

@@ -66,18 +66,6 @@ export class LiveApiService extends ApiService {
// auth
/**
* We want to update the pubkey, which means that we will call in clearnet the
* getPubKey, and all the information is never in the clear, and only public
* information is sent across the network.
*/
async getPubKey() {
this.pubkey = await this.rpcRequest({
method: 'auth.get-pubkey',
params: {},
})
}
async login(params: RR.LoginReq): Promise<RR.loginRes> {
return this.rpcRequest({ method: 'auth.login', params }, false)
}
@@ -102,8 +90,8 @@ export class LiveApiService extends ApiService {
// server
async echo(params: RR.EchoReq): Promise<RR.EchoRes> {
return this.rpcRequest({ method: 'echo', params }, false)
async echo(params: RR.EchoReq, urlOverride?: string): Promise<RR.EchoRes> {
return this.rpcRequest({ method: 'echo', params }, false, urlOverride)
}
openPatchWebsocket$(): Observable<Update<DataModel>> {
@@ -453,6 +441,7 @@ export class LiveApiService extends ApiService {
private async rpcRequest<T>(
options: RPCOptions,
addHeader = true,
urlOverride?: string,
): Promise<T> {
if (addHeader) {
options.headers = {
@@ -461,7 +450,7 @@ export class LiveApiService extends ApiService {
}
}
const res = await this.http.rpcRequest<T>(options)
const res = await this.http.rpcRequest<T>(options, urlOverride)
const encodedUpdates = res.headers.get('x-patch-updates')
const encodedError = res.headers.get('x-patch-error')

View File

@@ -113,24 +113,6 @@ export class MockApiService extends ApiService {
// auth
async getPubKey() {
await pauseFor(1000)
// randomly generated
// const keystore = jose.JWK.createKeyStore()
// this.pubkey = await keystore.generate('EC', 'P-256')
// generated from backend
this.pubkey = await this.jose.then(jose =>
jose.JWK.asKey({
kty: 'EC',
crv: 'P-256',
x: 'yHTDYSfjU809fkSv9MmN4wuojf5c3cnD7ZDN13n-jz4',
y: '8Mpkn744A5KDag0DmX2YivB63srjbugYZzWc3JOpQXI',
}),
)
}
async login(params: RR.LoginReq): Promise<RR.loginRes> {
await pauseFor(2000)
@@ -165,7 +147,13 @@ export class MockApiService extends ApiService {
// server
async echo(params: RR.EchoReq): Promise<RR.EchoRes> {
async echo(params: RR.EchoReq, url?: string): Promise<RR.EchoRes> {
if (url) {
const num = Math.floor(Math.random() * 10) + 1
console.warn(num)
if (num > 8) return params.message
throw new Error()
}
await pauseFor(2000)
return params.message
}

View File

@@ -23,6 +23,10 @@ export class ConfigService {
constructor(@Inject(DOCUMENT) private readonly document: Document) {}
hostname = this.document.location.hostname
// includes port
host = this.document.location.host
// includes ":" (e.g. "http:")
protocol = this.document.location.protocol
version = require('../../../../../package.json').version as string
useMocks = useMocks
mocks = mocks
@@ -36,17 +40,32 @@ export class ConfigService {
supportsWebSockets = !!window.WebSocket || this.isConsulate
isTor(): boolean {
return (
this.hostname.endsWith('.onion') || (useMocks && mocks.maskAs === 'tor')
)
return useMocks ? mocks.maskAs === 'tor' : this.hostname.endsWith('.onion')
}
isLocal(): boolean {
return useMocks
? mocks.maskAs === 'local'
: this.hostname.endsWith('.local')
}
isLocalhost(): boolean {
return useMocks
? mocks.maskAs === 'localhost'
: this.hostname === 'localhost'
}
isLan(): boolean {
return (
this.hostname === 'localhost' ||
this.hostname.endsWith('.local') ||
(useMocks && mocks.maskAs === 'lan')
)
// @TODO will not work once clearnet arrives
return !this.isTor()
}
isTorHttp(): boolean {
return this.isTor() && !this.isHttps()
}
isLocalHttp(): boolean {
return this.isLocal() && !this.isHttps()
}
isSecure(): boolean {
@@ -66,13 +85,21 @@ export class ConfigService {
}
launchableURL(pkg: PackageDataEntry): string {
if (this.isLan() && hasLanUi(pkg.manifest.interfaces)) {
if (this.isLan() && hasLocalUi(pkg.manifest.interfaces)) {
return `https://${lanUiAddress(pkg)}`
} else {
// leave http for services
return `http://${torUiAddress(pkg)}`
}
}
getHost(): string {
return this.host
}
private isHttps(): boolean {
return useMocks ? mocks.maskAsHttps : this.protocol === 'https:'
}
}
export function hasTorUi(interfaces: Record<string, InterfaceDef>): boolean {
@@ -80,7 +107,7 @@ export function hasTorUi(interfaces: Record<string, InterfaceDef>): boolean {
return !!int?.['tor-config']
}
export function hasLanUi(interfaces: Record<string, InterfaceDef>): boolean {
export function hasLocalUi(interfaces: Record<string, InterfaceDef>): boolean {
const int = getUiInterfaceValue(interfaces)
return !!int?.['lan-config']
}
@@ -102,7 +129,7 @@ export function lanUiAddress({
}
export function hasUi(interfaces: Record<string, InterfaceDef>): boolean {
return hasTorUi(interfaces) || hasLanUi(interfaces)
return hasTorUi(interfaces) || hasLocalUi(interfaces)
}
export function removeProtocol(str: string): string {

View File

@@ -53,8 +53,7 @@
*/
(window as any).global = window
global.Buffer = global.Buffer || require('buffer').Buffer;
(window as any).process = { env: { DEBUG: undefined }, browser: true }
; (window as any).process = { env: { DEBUG: undefined }, browser: true }
import './zone-flags'
@@ -62,8 +61,7 @@ import './zone-flags'
* Zone JS is required by default for Angular itself.
*/
import 'zone.js/dist/zone' // Included with Angular CLI.
import 'zone.js/dist/zone' // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS

View File

@@ -339,3 +339,12 @@ ul {
padding-left: 40px;
list-style-type: disc;
}
// override for taiga styles
p {
font-size: 1rem;
}
svg:not(:root) {
overflow: auto;
}