mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +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",
|
"input": "node_modules/monaco-editor",
|
||||||
"output": "assets/monaco-editor/"
|
"output": "assets/monaco-editor/"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "node_modules/@taiga-ui/icons/src",
|
||||||
|
"output": "assets/taiga-ui/icons"
|
||||||
|
},
|
||||||
"projects/ui/src/manifest.webmanifest",
|
"projects/ui/src/manifest.webmanifest",
|
||||||
{
|
{
|
||||||
"glob": "ngsw.json",
|
"glob": "ngsw.json",
|
||||||
@@ -44,6 +49,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
|
"node_modules/@taiga-ui/core/styles/taiga-ui-theme.less",
|
||||||
"projects/shared/styles/variables.scss",
|
"projects/shared/styles/variables.scss",
|
||||||
"projects/shared/styles/global.scss",
|
"projects/shared/styles/global.scss",
|
||||||
"projects/shared/styles/shared.scss",
|
"projects/shared/styles/shared.scss",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
},
|
},
|
||||||
"mocks": {
|
"mocks": {
|
||||||
"maskAs": "tor",
|
"maskAs": "tor",
|
||||||
|
"maskAsHttps": true,
|
||||||
"skipStartupAlerts": true
|
"skipStartupAlerts": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
26
frontend/package-lock.json
generated
26
frontend/package-lock.json
generated
@@ -45,7 +45,7 @@
|
|||||||
"monaco-editor": "^0.33.0",
|
"monaco-editor": "^0.33.0",
|
||||||
"mustache": "^4.2.0",
|
"mustache": "^4.2.0",
|
||||||
"ng-qrcode": "^7.0.0",
|
"ng-qrcode": "^7.0.0",
|
||||||
"node-jose": "^2.1.1",
|
"node-jose": "^2.2.0",
|
||||||
"patch-db-client": "file: ../../../patch-db/client",
|
"patch-db-client": "file: ../../../patch-db/client",
|
||||||
"pbkdf2": "^3.1.2",
|
"pbkdf2": "^3.1.2",
|
||||||
"rxjs": "^7.5.6",
|
"rxjs": "^7.5.6",
|
||||||
@@ -9506,9 +9506,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/long": {
|
"node_modules/long": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/long/-/long-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
|
||||||
"integrity": "sha512-9RTUNjK60eJbx3uz+TEGF7fUr29ZDxR5QzXcyDpeSfeH28S9ycINflOgOlppit5U+4kNTe83KQnMEerw7GmE8w=="
|
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
|
||||||
},
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "7.14.0",
|
"version": "7.14.0",
|
||||||
@@ -10955,9 +10955,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pako": {
|
"node_modules/pako": {
|
||||||
"version": "2.0.4",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
|
||||||
"integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg=="
|
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="
|
||||||
},
|
},
|
||||||
"node_modules/parent-module": {
|
"node_modules/parent-module": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
@@ -22148,9 +22148,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"long": {
|
"long": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/long/-/long-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
|
||||||
"integrity": "sha512-9RTUNjK60eJbx3uz+TEGF7fUr29ZDxR5QzXcyDpeSfeH28S9ycINflOgOlppit5U+4kNTe83KQnMEerw7GmE8w=="
|
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
|
||||||
},
|
},
|
||||||
"lru-cache": {
|
"lru-cache": {
|
||||||
"version": "7.14.0",
|
"version": "7.14.0",
|
||||||
@@ -23251,9 +23251,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pako": {
|
"pako": {
|
||||||
"version": "2.0.4",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
|
||||||
"integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg=="
|
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="
|
||||||
},
|
},
|
||||||
"parent-module": {
|
"parent-module": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
"monaco-editor": "^0.33.0",
|
"monaco-editor": "^0.33.0",
|
||||||
"mustache": "^4.2.0",
|
"mustache": "^4.2.0",
|
||||||
"ng-qrcode": "^7.0.0",
|
"ng-qrcode": "^7.0.0",
|
||||||
"node-jose": "^2.1.1",
|
"node-jose": "^2.2.0",
|
||||||
"patch-db-client": "file: ../../../patch-db/client",
|
"patch-db-client": "file: ../../../patch-db/client",
|
||||||
"pbkdf2": "^3.1.2",
|
"pbkdf2": "^3.1.2",
|
||||||
"rxjs": "^7.5.6",
|
"rxjs": "^7.5.6",
|
||||||
|
|||||||
@@ -38,12 +38,13 @@ export class HttpService {
|
|||||||
|
|
||||||
async rpcRequest<T>(
|
async rpcRequest<T>(
|
||||||
opts: RPCOptions,
|
opts: RPCOptions,
|
||||||
|
fullUrl?: string,
|
||||||
): Promise<LocalHttpResponse<RPCResponse<T>>> {
|
): Promise<LocalHttpResponse<RPCResponse<T>>> {
|
||||||
const { method, headers, params, timeout } = opts
|
const { method, headers, params, timeout } = opts
|
||||||
|
|
||||||
return this.httpRequest<RPCResponse<T>>({
|
return this.httpRequest<RPCResponse<T>>({
|
||||||
method: Method.POST,
|
method: Method.POST,
|
||||||
url: this.relativeUrl,
|
url: fullUrl || this.relativeUrl,
|
||||||
headers,
|
headers,
|
||||||
body: { method, params },
|
body: { method, params },
|
||||||
timeout,
|
timeout,
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ export type WorkspaceConfig = {
|
|||||||
community: 'https://community-registry.start9.com/'
|
community: 'https://community-registry.start9.com/'
|
||||||
}
|
}
|
||||||
mocks: {
|
mocks: {
|
||||||
maskAs: 'tor' | 'lan'
|
maskAs: 'tor' | 'local' | 'localhost'
|
||||||
|
// enables local development in secure mode
|
||||||
|
maskAsHttps: boolean
|
||||||
skipStartupAlerts: boolean
|
skipStartupAlerts: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,63 +15,33 @@
|
|||||||
|
|
||||||
<!-- Installed -->
|
<!-- Installed -->
|
||||||
<ng-template #installed>
|
<ng-template #installed>
|
||||||
<!-- SECURE -->
|
<ng-container *ngIf="pkg | toDependencies as dependencies">
|
||||||
<ng-container *ngIf="secure; else insecure">
|
<ion-item-group *ngIf="pkg | toStatus as status">
|
||||||
<ng-container *ngIf="pkg | toDependencies as dependencies">
|
<!-- ** status ** -->
|
||||||
<ion-item-group *ngIf="pkg | toStatus as status">
|
<app-show-status
|
||||||
<!-- ** status ** -->
|
[pkg]="pkg"
|
||||||
<app-show-status
|
[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"
|
[pkg]="pkg"
|
||||||
|
></app-show-health-checks>
|
||||||
|
<!-- ** dependencies ** -->
|
||||||
|
<app-show-dependencies
|
||||||
|
*ngIf="dependencies.length"
|
||||||
[dependencies]="dependencies"
|
[dependencies]="dependencies"
|
||||||
[status]="status"
|
></app-show-dependencies>
|
||||||
></app-show-status>
|
<!-- ** menu ** -->
|
||||||
<!-- ** installed && !backing-up ** -->
|
<app-show-menu [buttons]="pkg | toButtons"></app-show-menu>
|
||||||
<ng-container *ngIf="isInstalled(pkg) && !isBackingUp(status)">
|
<!-- ** additional ** -->
|
||||||
<!-- ** health checks ** -->
|
<app-show-additional [pkg]="pkg"></app-show-additional>
|
||||||
<app-show-health-checks
|
</ng-container>
|
||||||
*ngIf="isRunning(status)"
|
</ion-item-group>
|
||||||
[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>
|
|
||||||
</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>
|
</ng-template>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
</ng-container>
|
</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 { NavController } from '@ionic/angular'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import {
|
import {
|
||||||
@@ -13,9 +13,6 @@ import {
|
|||||||
import { tap } from 'rxjs/operators'
|
import { tap } from 'rxjs/operators'
|
||||||
import { ActivatedRoute } from '@angular/router'
|
import { ActivatedRoute } from '@angular/router'
|
||||||
import { getPkgId } from '@start9labs/shared'
|
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 = [
|
const STATES = [
|
||||||
PackageState.Installing,
|
PackageState.Installing,
|
||||||
@@ -29,8 +26,6 @@ const STATES = [
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class AppShowPage {
|
export class AppShowPage {
|
||||||
readonly secure = this.config.isSecure()
|
|
||||||
|
|
||||||
private readonly pkgId = getPkgId(this.route)
|
private readonly pkgId = getPkgId(this.route)
|
||||||
|
|
||||||
readonly pkg$ = this.patch.watch$('package-data', this.pkgId).pipe(
|
readonly pkg$ = this.patch.watch$('package-data', this.pkgId).pipe(
|
||||||
@@ -44,8 +39,6 @@ export class AppShowPage {
|
|||||||
private readonly route: ActivatedRoute,
|
private readonly route: ActivatedRoute,
|
||||||
private readonly navCtrl: NavController,
|
private readonly navCtrl: NavController,
|
||||||
private readonly patch: PatchDB<DataModel>,
|
private readonly patch: PatchDB<DataModel>,
|
||||||
private readonly config: ConfigService,
|
|
||||||
@Inject(DOCUMENT) private readonly document: Document,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
isInstalled({ state }: PackageDataEntry): boolean {
|
isInstalled({ state }: PackageDataEntry): boolean {
|
||||||
@@ -63,11 +56,4 @@ export class AppShowPage {
|
|||||||
showProgress({ state }: PackageDataEntry): boolean {
|
showProgress({ state }: PackageDataEntry): boolean {
|
||||||
return STATES.includes(state)
|
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 { FormsModule } from '@angular/forms'
|
||||||
import { IonicModule } from '@ionic/angular'
|
import { IonicModule } from '@ionic/angular'
|
||||||
import { LoginPage } from './login.page'
|
import { LoginPage } from './login.page'
|
||||||
|
import { CAWizardComponent } from './ca-wizard/ca-wizard.component'
|
||||||
import { SharedPipesModule } from '@start9labs/shared'
|
import { SharedPipesModule } from '@start9labs/shared'
|
||||||
|
import { TuiHintModule, TuiTooltipModule } from '@taiga-ui/core'
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
@@ -20,7 +22,9 @@ const routes: Routes = [
|
|||||||
IonicModule,
|
IonicModule,
|
||||||
SharedPipesModule,
|
SharedPipesModule,
|
||||||
RouterModule.forChild(routes),
|
RouterModule.forChild(routes),
|
||||||
|
TuiTooltipModule,
|
||||||
|
TuiHintModule,
|
||||||
],
|
],
|
||||||
declarations: [LoginPage],
|
declarations: [LoginPage, CAWizardComponent],
|
||||||
})
|
})
|
||||||
export class LoginPageModule {}
|
export class LoginPageModule {}
|
||||||
|
|||||||
@@ -1,55 +1,81 @@
|
|||||||
<ion-content class="content">
|
<ion-content class="content">
|
||||||
<ion-grid class="grid">
|
<!-- Local HTTP -->
|
||||||
<ion-row class="row">
|
<ng-container *ngIf="config.isLocalHttp(); else notLanHttp">
|
||||||
<ion-col>
|
<ca-wizard></ca-wizard>
|
||||||
<img src="assets/img/logo.png" alt="Start9" class="logo" />
|
</ng-container>
|
||||||
|
|
||||||
<ion-card class="card">
|
<!-- not Local HTTP -->
|
||||||
<ion-card-header>
|
<ng-template #notLanHttp>
|
||||||
<ion-card-title class="title">StartOS Login</ion-card-title>
|
<div *ngIf="config.isTorHttp()" class="banner">
|
||||||
</ion-card-header>
|
<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">
|
<ion-grid class="grid">
|
||||||
<form class="form" (submit)="submit()">
|
<ion-row class="row">
|
||||||
<ion-item-group>
|
<ion-col>
|
||||||
<ion-item color="dark">
|
<img src="assets/img/logo.png" alt="Start9" class="logo" />
|
||||||
<ion-icon
|
|
||||||
slot="start"
|
<ion-card class="card">
|
||||||
name="key-outline"
|
<ion-card-header>
|
||||||
style="margin-right: 16px"
|
<ion-card-title class="title">StartOS Login</ion-card-title>
|
||||||
></ion-icon>
|
</ion-card-header>
|
||||||
<ion-input
|
|
||||||
name="password"
|
<ion-card-content class="ion-margin">
|
||||||
placeholder="Password"
|
<form class="form" (submit)="submit()">
|
||||||
[type]="unmasked ? 'text' : 'password'"
|
<ion-item-group>
|
||||||
[(ngModel)]="password"
|
<ion-item color="dark" class="login-item">
|
||||||
(ionChange)="error = ''"
|
|
||||||
maxlength="64"
|
|
||||||
></ion-input>
|
|
||||||
<ion-button fill="clear" color="light" (click)="toggleMask()">
|
|
||||||
<ion-icon
|
<ion-icon
|
||||||
slot="icon-only"
|
slot="start"
|
||||||
size="small"
|
name="key-outline"
|
||||||
[name]="unmasked ? 'eye-off-outline' : 'eye-outline'"
|
style="margin-right: 16px"
|
||||||
></ion-icon>
|
></ion-icon>
|
||||||
</ion-button>
|
<ion-input
|
||||||
</ion-item>
|
name="password"
|
||||||
</ion-item-group>
|
placeholder="Password"
|
||||||
<ion-button
|
[type]="unmasked ? 'text' : 'password'"
|
||||||
class="login-button"
|
[(ngModel)]="password"
|
||||||
type="submit"
|
(ionChange)="error = ''"
|
||||||
expand="block"
|
maxlength="64"
|
||||||
color="tertiary"
|
></ion-input>
|
||||||
>
|
<ion-button
|
||||||
Login
|
fill="clear"
|
||||||
</ion-button>
|
color="light"
|
||||||
</form>
|
(click)="unmasked = !unmasked"
|
||||||
<p class="error">
|
>
|
||||||
<ion-text color="danger">{{ error }}</ion-text>
|
<ion-icon
|
||||||
</p>
|
slot="icon-only"
|
||||||
</ion-card-content>
|
size="small"
|
||||||
</ion-card>
|
[name]="unmasked ? 'eye-off-outline' : 'eye-outline'"
|
||||||
</ion-col>
|
></ion-icon>
|
||||||
</ion-row>
|
</ion-button>
|
||||||
</ion-grid>
|
</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>
|
</ion-content>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
height: 90%;
|
height: 100%;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
@@ -34,18 +34,30 @@
|
|||||||
padding-top: 4px;
|
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;
|
--border-radius: 0 4px 4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ion-item {
|
.login-item {
|
||||||
--border-style: solid;
|
--border-style: solid;
|
||||||
--border-color: var(--ion-color-light);
|
--border-color: var(--ion-color-light);
|
||||||
--border-radius: 4px 0 0 4px;
|
--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),
|
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);
|
0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||||
|
|
||||||
ion-button {
|
.side-button {
|
||||||
--border-radius: 4px;
|
--border-radius: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,12 +92,12 @@ ion-card {
|
|||||||
|
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
@media (max-width: 500px) {
|
||||||
ion-button {
|
.side-button {
|
||||||
--border-radius: 4px;
|
--border-radius: 4px;
|
||||||
margin-top: 0.7rem;
|
margin-top: 0.7rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
ion-item {
|
.login-item {
|
||||||
--border-radius: 4px;
|
--border-radius: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component, Inject } from '@angular/core'
|
||||||
import { LoadingController, getPlatforms } from '@ionic/angular'
|
import { getPlatforms, LoadingController } from '@ionic/angular'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { AuthService } from 'src/app/services/auth.service'
|
import { AuthService } from 'src/app/services/auth.service'
|
||||||
import { Router } from '@angular/router'
|
import { Router } from '@angular/router'
|
||||||
import { ConfigService } from 'src/app/services/config.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({
|
@Component({
|
||||||
selector: 'login',
|
selector: 'login',
|
||||||
@@ -14,42 +17,71 @@ export class LoginPage {
|
|||||||
password = ''
|
password = ''
|
||||||
unmasked = false
|
unmasked = false
|
||||||
error = ''
|
error = ''
|
||||||
loader?: HTMLIonLoadingElement
|
|
||||||
secure = this.config.isSecure()
|
downloadClicked = false
|
||||||
|
instructionsClicked = false
|
||||||
|
polling = false
|
||||||
|
caTrusted = false
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly router: Router,
|
private readonly router: Router,
|
||||||
private readonly authService: AuthService,
|
private readonly authService: AuthService,
|
||||||
private readonly loadingCtrl: LoadingController,
|
private readonly loadingCtrl: LoadingController,
|
||||||
private readonly api: ApiService,
|
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() {
|
async ngOnInit() {
|
||||||
if (!this.secure) {
|
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 {
|
try {
|
||||||
await this.api.getPubKey()
|
await this.testHttps()
|
||||||
} catch (e: any) {
|
this.polling = false
|
||||||
this.error = e.message
|
} catch (e) {
|
||||||
|
console.warn('Failed Https connection attempt')
|
||||||
|
await pauseFor(2000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
launchHttps() {
|
||||||
this.loader?.dismiss()
|
const host = this.config.getHost()
|
||||||
}
|
this.windowRef.open(`https://${host}`, '_blank', 'noreferrer')
|
||||||
|
|
||||||
toggleMask() {
|
|
||||||
this.unmasked = !this.unmasked
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async submit() {
|
async submit() {
|
||||||
this.error = ''
|
this.error = ''
|
||||||
|
|
||||||
this.loader = await this.loadingCtrl.create({
|
const loader = await this.loadingCtrl.create({
|
||||||
message: 'Logging in...',
|
message: 'Logging in...',
|
||||||
})
|
})
|
||||||
await this.loader.present()
|
await loader.present()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
document.cookie = ''
|
document.cookie = ''
|
||||||
@@ -58,9 +90,7 @@ export class LoginPage {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
await this.api.login({
|
await this.api.login({
|
||||||
password: this.secure
|
password: this.password,
|
||||||
? this.password
|
|
||||||
: await this.api.encrypt(this.password),
|
|
||||||
metadata: { platforms: getPlatforms() },
|
metadata: { platforms: getPlatforms() },
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -71,7 +101,16 @@ export class LoginPage {
|
|||||||
// code 7 is for incorrect password
|
// code 7 is for incorrect password
|
||||||
this.error = e.code === 7 ? 'Invalid Password' : e.message
|
this.error = e.code === 7 ? 'Invalid Password' : e.message
|
||||||
} finally {
|
} 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 -->
|
<!-- loaded -->
|
||||||
<ion-item-group *ngIf="server$ | async as server; else loading">
|
<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-icon slot="start" name="warning-outline"></ion-icon>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2 style="font-weight: bold">Http detected</h2>
|
<h2 style="font-weight: bold">Http detected</h2>
|
||||||
<p style="font-weight: 600">
|
<p style="font-weight: 600">
|
||||||
{{ isTorHttp ? 'Tor is faster over https.' : 'Your connection is
|
Tor is faster over https.
|
||||||
insecure.' }}
|
|
||||||
<a
|
<a
|
||||||
[routerLink]="['/system', 'root-ca']"
|
[routerLink]="['/system', 'root-ca']"
|
||||||
style="color: var(--ion-color-light)"
|
style="color: var(--ion-color-light)"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { ActivatedRoute } from '@angular/router'
|
import { ActivatedRoute } from '@angular/router'
|
||||||
import { PatchDB } from 'patch-db-client'
|
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 { ErrorToastService } from '@start9labs/shared'
|
||||||
import { EOSService } from 'src/app/services/eos.service'
|
import { EOSService } from 'src/app/services/eos.service'
|
||||||
import { ClientStorageService } from 'src/app/services/client-storage.service'
|
import { ClientStorageService } from 'src/app/services/client-storage.service'
|
||||||
@@ -41,9 +41,7 @@ export class ServerShowPage {
|
|||||||
readonly showUpdate$ = this.eosService.showUpdate$
|
readonly showUpdate$ = this.eosService.showUpdate$
|
||||||
readonly showDiskRepair$ = this.ClientStorageService.showDiskRepair$
|
readonly showDiskRepair$ = this.ClientStorageService.showDiskRepair$
|
||||||
|
|
||||||
readonly secure = this.config.isSecure()
|
readonly isTorHttp = this.config.isTorHttp()
|
||||||
readonly isTorHttp =
|
|
||||||
this.config.isTor() && this.document.location.protocol === 'http:'
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly alertCtrl: AlertController,
|
private readonly alertCtrl: AlertController,
|
||||||
@@ -308,10 +306,8 @@ export class ServerShowPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async launchHttps() {
|
async launchHttps() {
|
||||||
const onTor = this.config.isTor()
|
const { 'tor-address': torAddress } = await getServerInfo(this.patch)
|
||||||
const { 'lan-address': lanAddress, 'tor-address': torAddress } =
|
window.open(torAddress)
|
||||||
await getServerInfo(this.patch)
|
|
||||||
onTor ? window.open(torAddress) : window.open(lanAddress)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addClick(title: string) {
|
addClick(title: string) {
|
||||||
@@ -465,7 +461,7 @@ export class ServerShowPage {
|
|||||||
action: () =>
|
action: () =>
|
||||||
this.navCtrl.navigateForward(['backup'], { relativeTo: this.route }),
|
this.navCtrl.navigateForward(['backup'], { relativeTo: this.route }),
|
||||||
detail: true,
|
detail: true,
|
||||||
disabled$: of(!this.secure),
|
disabled$: of(false),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Restore From Backup',
|
title: 'Restore From Backup',
|
||||||
@@ -474,10 +470,7 @@ export class ServerShowPage {
|
|||||||
action: () =>
|
action: () =>
|
||||||
this.navCtrl.navigateForward(['restore'], { relativeTo: this.route }),
|
this.navCtrl.navigateForward(['restore'], { relativeTo: this.route }),
|
||||||
detail: true,
|
detail: true,
|
||||||
disabled$: combineLatest([
|
disabled$: this.eosService.updatingOrBackingUp$,
|
||||||
this.eosService.updatingOrBackingUp$,
|
|
||||||
of(this.secure),
|
|
||||||
]).pipe(map(([updating, secure]) => updating || !secure)),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
Manage: [
|
Manage: [
|
||||||
@@ -545,7 +538,7 @@ export class ServerShowPage {
|
|||||||
icon: 'key-outline',
|
icon: 'key-outline',
|
||||||
action: () => this.presentAlertResetPassword(),
|
action: () => this.presentAlertResetPassword(),
|
||||||
detail: false,
|
detail: false,
|
||||||
disabled$: of(!this.secure),
|
disabled$: of(false),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Experimental Features',
|
title: 'Experimental Features',
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export class WifiPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async presentAlertCountry(): Promise<void> {
|
async presentAlertCountry(): Promise<void> {
|
||||||
if (!this.config.isLan) {
|
if (!this.config.isLan()) {
|
||||||
const alert = await this.alertCtrl.create({
|
const alert = await this.alertCtrl.create({
|
||||||
header: 'Cannot Complete Action',
|
header: 'Cannot Complete Action',
|
||||||
message:
|
message:
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export module RR {
|
|||||||
// auth
|
// auth
|
||||||
|
|
||||||
export type LoginReq = {
|
export type LoginReq = {
|
||||||
password: Encrypted | string
|
password: string
|
||||||
metadata: SessionMetadata
|
metadata: SessionMetadata
|
||||||
} // auth.login - unauthed
|
} // auth.login - unauthed
|
||||||
export type loginRes = null
|
export type loginRes = null
|
||||||
@@ -465,7 +465,3 @@ declare global {
|
|||||||
parse<T>(text: Stringified<T>, reviver?: (key: any, value: any) => any): T
|
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 { BehaviorSubject, Observable } from 'rxjs'
|
||||||
import { Update } from 'patch-db-client'
|
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 { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import { Log } from '@start9labs/shared'
|
import { Log } from '@start9labs/shared'
|
||||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||||
import type { JWK } from 'node-jose'
|
|
||||||
|
|
||||||
export abstract class ApiService {
|
export abstract class ApiService {
|
||||||
protected readonly jose = import('node-jose')
|
|
||||||
|
|
||||||
readonly patchStream$ = new BehaviorSubject<Update<DataModel>[]>([])
|
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
|
// http
|
||||||
|
|
||||||
@@ -41,8 +25,6 @@ export abstract class ApiService {
|
|||||||
|
|
||||||
// auth
|
// auth
|
||||||
|
|
||||||
abstract getPubKey(): Promise<void>
|
|
||||||
|
|
||||||
abstract login(params: RR.LoginReq): Promise<RR.loginRes>
|
abstract login(params: RR.LoginReq): Promise<RR.loginRes>
|
||||||
|
|
||||||
abstract logout(params: RR.LogoutReq): Promise<RR.LogoutRes>
|
abstract logout(params: RR.LogoutReq): Promise<RR.LogoutRes>
|
||||||
@@ -57,7 +39,7 @@ export abstract class ApiService {
|
|||||||
|
|
||||||
// server
|
// server
|
||||||
|
|
||||||
abstract echo(params: RR.EchoReq): Promise<RR.EchoRes>
|
abstract echo(params: RR.EchoReq, urlOverride?: string): Promise<RR.EchoRes>
|
||||||
|
|
||||||
abstract openPatchWebsocket$(): Observable<Update<DataModel>>
|
abstract openPatchWebsocket$(): Observable<Update<DataModel>>
|
||||||
|
|
||||||
|
|||||||
@@ -66,18 +66,6 @@ export class LiveApiService extends ApiService {
|
|||||||
|
|
||||||
// auth
|
// 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> {
|
async login(params: RR.LoginReq): Promise<RR.loginRes> {
|
||||||
return this.rpcRequest({ method: 'auth.login', params }, false)
|
return this.rpcRequest({ method: 'auth.login', params }, false)
|
||||||
}
|
}
|
||||||
@@ -102,8 +90,8 @@ export class LiveApiService extends ApiService {
|
|||||||
|
|
||||||
// server
|
// server
|
||||||
|
|
||||||
async echo(params: RR.EchoReq): Promise<RR.EchoRes> {
|
async echo(params: RR.EchoReq, urlOverride?: string): Promise<RR.EchoRes> {
|
||||||
return this.rpcRequest({ method: 'echo', params }, false)
|
return this.rpcRequest({ method: 'echo', params }, false, urlOverride)
|
||||||
}
|
}
|
||||||
|
|
||||||
openPatchWebsocket$(): Observable<Update<DataModel>> {
|
openPatchWebsocket$(): Observable<Update<DataModel>> {
|
||||||
@@ -453,6 +441,7 @@ export class LiveApiService extends ApiService {
|
|||||||
private async rpcRequest<T>(
|
private async rpcRequest<T>(
|
||||||
options: RPCOptions,
|
options: RPCOptions,
|
||||||
addHeader = true,
|
addHeader = true,
|
||||||
|
urlOverride?: string,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
if (addHeader) {
|
if (addHeader) {
|
||||||
options.headers = {
|
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 encodedUpdates = res.headers.get('x-patch-updates')
|
||||||
const encodedError = res.headers.get('x-patch-error')
|
const encodedError = res.headers.get('x-patch-error')
|
||||||
|
|
||||||
|
|||||||
@@ -113,24 +113,6 @@ export class MockApiService extends ApiService {
|
|||||||
|
|
||||||
// auth
|
// 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> {
|
async login(params: RR.LoginReq): Promise<RR.loginRes> {
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
|
|
||||||
@@ -165,7 +147,13 @@ export class MockApiService extends ApiService {
|
|||||||
|
|
||||||
// server
|
// 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)
|
await pauseFor(2000)
|
||||||
return params.message
|
return params.message
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ export class ConfigService {
|
|||||||
constructor(@Inject(DOCUMENT) private readonly document: Document) {}
|
constructor(@Inject(DOCUMENT) private readonly document: Document) {}
|
||||||
|
|
||||||
hostname = this.document.location.hostname
|
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
|
version = require('../../../../../package.json').version as string
|
||||||
useMocks = useMocks
|
useMocks = useMocks
|
||||||
mocks = mocks
|
mocks = mocks
|
||||||
@@ -36,17 +40,32 @@ export class ConfigService {
|
|||||||
supportsWebSockets = !!window.WebSocket || this.isConsulate
|
supportsWebSockets = !!window.WebSocket || this.isConsulate
|
||||||
|
|
||||||
isTor(): boolean {
|
isTor(): boolean {
|
||||||
return (
|
return useMocks ? mocks.maskAs === 'tor' : this.hostname.endsWith('.onion')
|
||||||
this.hostname.endsWith('.onion') || (useMocks && mocks.maskAs === 'tor')
|
}
|
||||||
)
|
|
||||||
|
isLocal(): boolean {
|
||||||
|
return useMocks
|
||||||
|
? mocks.maskAs === 'local'
|
||||||
|
: this.hostname.endsWith('.local')
|
||||||
|
}
|
||||||
|
|
||||||
|
isLocalhost(): boolean {
|
||||||
|
return useMocks
|
||||||
|
? mocks.maskAs === 'localhost'
|
||||||
|
: this.hostname === 'localhost'
|
||||||
}
|
}
|
||||||
|
|
||||||
isLan(): boolean {
|
isLan(): boolean {
|
||||||
return (
|
// @TODO will not work once clearnet arrives
|
||||||
this.hostname === 'localhost' ||
|
return !this.isTor()
|
||||||
this.hostname.endsWith('.local') ||
|
}
|
||||||
(useMocks && mocks.maskAs === 'lan')
|
|
||||||
)
|
isTorHttp(): boolean {
|
||||||
|
return this.isTor() && !this.isHttps()
|
||||||
|
}
|
||||||
|
|
||||||
|
isLocalHttp(): boolean {
|
||||||
|
return this.isLocal() && !this.isHttps()
|
||||||
}
|
}
|
||||||
|
|
||||||
isSecure(): boolean {
|
isSecure(): boolean {
|
||||||
@@ -66,13 +85,21 @@ export class ConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
launchableURL(pkg: PackageDataEntry): string {
|
launchableURL(pkg: PackageDataEntry): string {
|
||||||
if (this.isLan() && hasLanUi(pkg.manifest.interfaces)) {
|
if (this.isLan() && hasLocalUi(pkg.manifest.interfaces)) {
|
||||||
return `https://${lanUiAddress(pkg)}`
|
return `https://${lanUiAddress(pkg)}`
|
||||||
} else {
|
} else {
|
||||||
// leave http for services
|
// leave http for services
|
||||||
return `http://${torUiAddress(pkg)}`
|
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 {
|
export function hasTorUi(interfaces: Record<string, InterfaceDef>): boolean {
|
||||||
@@ -80,7 +107,7 @@ export function hasTorUi(interfaces: Record<string, InterfaceDef>): boolean {
|
|||||||
return !!int?.['tor-config']
|
return !!int?.['tor-config']
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasLanUi(interfaces: Record<string, InterfaceDef>): boolean {
|
export function hasLocalUi(interfaces: Record<string, InterfaceDef>): boolean {
|
||||||
const int = getUiInterfaceValue(interfaces)
|
const int = getUiInterfaceValue(interfaces)
|
||||||
return !!int?.['lan-config']
|
return !!int?.['lan-config']
|
||||||
}
|
}
|
||||||
@@ -102,7 +129,7 @@ export function lanUiAddress({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function hasUi(interfaces: Record<string, InterfaceDef>): boolean {
|
export function hasUi(interfaces: Record<string, InterfaceDef>): boolean {
|
||||||
return hasTorUi(interfaces) || hasLanUi(interfaces)
|
return hasTorUi(interfaces) || hasLocalUi(interfaces)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeProtocol(str: string): string {
|
export function removeProtocol(str: string): string {
|
||||||
|
|||||||
@@ -53,8 +53,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
(window as any).global = window
|
(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'
|
import './zone-flags'
|
||||||
|
|
||||||
@@ -62,8 +61,7 @@ import './zone-flags'
|
|||||||
* Zone JS is required by default for Angular itself.
|
* 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
|
* APPLICATION IMPORTS
|
||||||
|
|||||||
@@ -339,3 +339,12 @@ ul {
|
|||||||
padding-left: 40px;
|
padding-left: 40px;
|
||||||
list-style-type: disc;
|
list-style-type: disc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// override for taiga styles
|
||||||
|
p {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg:not(:root) {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user