mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
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:
@@ -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",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
},
|
||||
"mocks": {
|
||||
"maskAs": "tor",
|
||||
"maskAsHttps": true,
|
||||
"skipStartupAlerts": true
|
||||
}
|
||||
},
|
||||
|
||||
26
frontend/package-lock.json
generated
26
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
|
||||
<!-- 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 ** -->
|
||||
@@ -44,34 +42,6 @@
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
</ng-container>
|
||||
</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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
{{ 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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
@@ -1,4 +1,25 @@
|
||||
<ion-content class="content">
|
||||
<!-- Local HTTP -->
|
||||
<ng-container *ngIf="config.isLocalHttp(); else notLanHttp">
|
||||
<ca-wizard></ca-wizard>
|
||||
</ng-container>
|
||||
|
||||
<!-- 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-grid class="grid">
|
||||
<ion-row class="row">
|
||||
<ion-col>
|
||||
@@ -12,7 +33,7 @@
|
||||
<ion-card-content class="ion-margin">
|
||||
<form class="form" (submit)="submit()">
|
||||
<ion-item-group>
|
||||
<ion-item color="dark">
|
||||
<ion-item color="dark" class="login-item">
|
||||
<ion-icon
|
||||
slot="start"
|
||||
name="key-outline"
|
||||
@@ -26,7 +47,11 @@
|
||||
(ionChange)="error = ''"
|
||||
maxlength="64"
|
||||
></ion-input>
|
||||
<ion-button fill="clear" color="light" (click)="toggleMask()">
|
||||
<ion-button
|
||||
fill="clear"
|
||||
color="light"
|
||||
(click)="unmasked = !unmasked"
|
||||
>
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
size="small"
|
||||
@@ -36,7 +61,7 @@
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
<ion-button
|
||||
class="login-button"
|
||||
class="login-button side-button"
|
||||
type="submit"
|
||||
expand="block"
|
||||
color="tertiary"
|
||||
@@ -52,4 +77,5 @@
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>>
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -64,7 +63,6 @@ import './zone-flags'
|
||||
|
||||
import 'zone.js/dist/zone' // Included with Angular CLI.
|
||||
|
||||
|
||||
/***************************************************************************************************
|
||||
* APPLICATION IMPORTS
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user