Refactor application initialization & allow public user load
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing

This commit is contained in:
Garrett Mills 2020-11-11 12:24:02 -06:00
parent 556806ea69
commit a5dc7f7a19
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246
8 changed files with 140 additions and 50 deletions

View File

@ -1,6 +1,6 @@
<ion-app class="dark"> <ion-app class="dark">
<ion-split-pane contentId="main-content" *ngIf="ready$ | async"> <ion-split-pane contentId="main-content" *ngIf="ready$ | async">
<ion-menu class="sidebar" contentId="main-content" content="content" type="push" side="start"> <ion-menu class="sidebar" contentId="main-content" content="content" type="push" side="start" *ngIf="!api.isPublicUser">
<ion-header> <ion-header>
<ion-toolbar color="primary"> <ion-toolbar color="primary">
<ion-title style="font-weight: bold; color: white;">{{ appName }} <ion-title style="font-weight: bold; color: white;">{{ appName }}

View File

@ -1,4 +1,4 @@
import {AfterViewInit, Component, ElementRef, OnInit, ViewChild, HostListener, Host} from '@angular/core'; import {Component, OnInit, ViewChild, HostListener, Host} from '@angular/core';
import { import {
AlertController, AlertController,
@ -23,6 +23,7 @@ import {NodeTypeIcons} from './structures/node-types';
import {NavigationService} from './service/navigation.service'; import {NavigationService} from './service/navigation.service';
import {DatabaseService} from './service/db/database.service'; import {DatabaseService} from './service/db/database.service';
import {EditorService} from './service/editor.service'; import {EditorService} from './service/editor.service';
import {debug} from './utility';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@ -88,7 +89,7 @@ export class AppComponent implements OnInit {
private platform: Platform, private platform: Platform,
private splashScreen: SplashScreen, private splashScreen: SplashScreen,
private statusBar: StatusBar, private statusBar: StatusBar,
private api: ApiService, public readonly api: ApiService,
protected router: Router, protected router: Router,
protected alerts: AlertController, protected alerts: AlertController,
protected popover: PopoverController, protected popover: PopoverController,
@ -99,27 +100,7 @@ export class AppComponent implements OnInit {
protected toasts: ToastController, protected toasts: ToastController,
protected db: DatabaseService, protected db: DatabaseService,
protected editor: EditorService, protected editor: EditorService,
) { ) { }
this.initializeApp();
}
async _doInit() {
await this.db.createSchemata();
this.reloadMenuItems().subscribe(() => {
this.ready$.next(true);
setTimeout(() => {
this.loader.dismiss();
this.menuTree.treeModel.expandAll();
}, 10);
if ( !this.versionInterval ) {
this.versionInterval = setInterval(() => {
this.checkNewVersion();
}, 1000 * 60 * 5); // Check for new version every 5 mins
}
});
}
async checkNewVersion() { async checkNewVersion() {
if ( !this.showedNewVersionAlert && await this.session.newVersionAvailable() ) { if ( !this.showedNewVersionAlert && await this.session.newVersionAvailable() ) {
@ -144,20 +125,8 @@ export class AppComponent implements OnInit {
} }
ngOnInit() { ngOnInit() {
if ( !this.initialized$.getValue() ) { debug('Initializing application.');
this.navService.sidebarRefresh$.subscribe(([_, quiet]) => { this.initializeApp();
this.onMenuRefresh(quiet);
});
const sub = this.initialized$.subscribe((didInit) => {
if (didInit) {
this._doInit();
sub.unsubscribe();
}
});
} else {
this._doInit();
}
} }
showOptions($event) { showOptions($event) {
@ -262,7 +231,7 @@ export class AppComponent implements OnInit {
}); });
} else if ( result.data === 'export_html' ) { } else if ( result.data === 'export_html' ) {
this.exportTargetAsHTML(); this.exportTargetAsHTML();
}; }
}); });
await popover.present(); await popover.present();
@ -420,19 +389,24 @@ export class AppComponent implements OnInit {
} }
async initializeApp() { async initializeApp() {
console.log('app', this); debug('app', this);
this.loader = await this.loading.create({ this.loader = await this.loading.create({
message: 'Starting up...', message: 'Starting up...',
cssClass: 'noded-loading-mask', cssClass: 'noded-loading-mask',
showBackdrop: true, showBackdrop: true,
}); });
debug('Initializing platform and database...');
await this.loader.present(); await this.loader.present();
await this.platform.ready(); await this.platform.ready();
await this.db.createSchemata();
let toast: any; let toast: any;
debug('Subscribing to offline changes...');
this.api.offline$.subscribe(async isOffline => { this.api.offline$.subscribe(async isOffline => {
if ( isOffline && !this.showedOfflineAlert ) { if ( isOffline && !this.showedOfflineAlert ) {
debug('Application went offline!');
toast = await this.toasts.create({ toast = await this.toasts.create({
cssClass: 'compat-toast-container', cssClass: 'compat-toast-container',
message: 'Uh, oh! It looks like you\'re offline. Some features might not work as expected...', message: 'Uh, oh! It looks like you\'re offline. Some features might not work as expected...',
@ -441,39 +415,64 @@ export class AppComponent implements OnInit {
this.showedOfflineAlert = true; this.showedOfflineAlert = true;
await toast.present(); await toast.present();
} else if ( !isOffline && this.showedOfflineAlert ) { } else if ( !isOffline && this.showedOfflineAlert ) {
debug('Appliation went online!');
await toast.dismiss(); await toast.dismiss();
this.showedOfflineAlert = false; this.showedOfflineAlert = false;
await this.api.syncOfflineData(); await this.api.syncOfflineData();
} }
}); });
debug('Getting initial status...');
let stat: any = await this.session.stat(); let stat: any = await this.session.stat();
debug('Got stat:', stat);
if ( !stat.authenticated_user ) { if ( stat.public_user ) {
this.api.isPublicUser = true;
}
if ( stat.authenticated_user ) {
this.api.isAuthenticated = true;
}
this.api.systemBase = stat.system_base;
if ( !this.api.isAuthenticated || this.api.isPublicUser ) {
debug('Unauthenticated or public user...');
if ( !this.api.isOffline ) { if ( !this.api.isOffline ) {
debug('Trying to resume session...');
await this.api.resumeSession(); await this.api.resumeSession();
debug('Checking new status...');
stat = await this.session.stat(); stat = await this.session.stat();
debug('Got session resume stat:', stat);
this.api.isAuthenticated = stat.authenticated_user;
this.api.isPublicUser = stat.public_user;
if ( !stat.authenticated_user ) { if ( !stat.authenticated_user ) {
debug('Not authenticated! Redirecting.');
window.location.href = `${stat.system_base}start`; window.location.href = `${stat.system_base}start`;
return; return;
} }
} else { } else {
debug('Unauthenticated offline user. Purging local data!');
await this.db.purge(); await this.db.purge();
window.location.href = `${stat.system_base}start`; window.location.href = `${stat.system_base}start`;
return; return;
} }
} }
debug('Set app name and system base:', stat.app_name, stat.system_base);
this.session.appName = stat.app_name; this.session.appName = stat.app_name;
this.session.systemBase = stat.system_base; this.session.systemBase = stat.system_base;
debug('Initializing session...');
await this.session.initialize(); await this.session.initialize();
if ( this.session.get('user.preferences.dark_mode') ) { if ( this.session.get('user.preferences.dark_mode') ) {
this.toggleDark(); this.toggleDark();
} }
debug('Hiding native splash screen & setting status bar styles...');
await this.statusBar.styleDefault(); await this.statusBar.styleDefault();
await this.splashScreen.hide(); await this.splashScreen.hide();
@ -495,11 +494,13 @@ export class AppComponent implements OnInit {
} }
} }
if ( this.isPrefetch() ) { if ( this.isPrefetch() && !this.api.isPublicUser ) {
this.loader.message = 'Downloading data...'; // TODO actually do the prefetch debug('Pre-fetching offline data...');
this.loader.message = 'Downloading data...';
try { try {
await this.api.prefetchOfflineData(); await this.api.prefetchOfflineData();
} catch (e) { } catch (e) {
debug('Pre-fetch error:', e);
this.toasts.create({ this.toasts.create({
cssClass: 'compat-toast-container', cssClass: 'compat-toast-container',
message: 'An error occurred while pre-fetching offline data. Not all data was saved.', message: 'An error occurred while pre-fetching offline data. Not all data was saved.',
@ -514,13 +515,37 @@ export class AppComponent implements OnInit {
this.initialized$.next(true); this.initialized$.next(true);
if ( this.session.get('user.preferences.default_page') ) { if ( !this.api.isPublicUser && this.session.get('user.preferences.default_page') ) {
debug('Navigating to default page!');
const id = this.session.get('user.preferences.default_page'); const id = this.session.get('user.preferences.default_page');
const node = this.findNode(id); const node = this.findNode(id);
if ( node ) { if ( node ) {
this.navigateEditorToNode(node); this.navigateEditorToNode(node);
} }
} }
debug('Creating menu subscription...');
this.navService.sidebarRefresh$.subscribe(([_, quiet]) => {
this.onMenuRefresh(quiet);
});
debug('Reloading menu items...');
this.reloadMenuItems().subscribe(() => {
debug('Reloaded menu items. Displaying interface.');
this.ready$.next(true);
setTimeout(() => {
this.loader.dismiss();
this.menuTree?.treeModel?.expandAll();
}, 10);
if ( !this.versionInterval ) {
this.versionInterval = setInterval(() => {
debug('Checking for new application version.');
this.checkNewVersion();
}, 1000 * 60 * 5); // Check for new version every 5 mins
}
});
} }
async doPrefetch() { async doPrefetch() {

View File

@ -8,12 +8,14 @@
</ion-buttons> </ion-buttons>
</ion-toolbar> </ion-toolbar>
<ion-grid style="height: 100%; justify-content: center; display: flex; flex-direction: column; font-size: 24pt; color: #ccc;"> <ion-grid style="height: 100%; justify-content: center; display: flex; flex-direction: column; font-size: 24pt; color: #ccc;">
<ion-row align-items-center> <ion-row align-items-center *ngIf="!api.isPublicUser">
<ion-col>Hi, there! Select or create a page to get started.</ion-col> <ion-col>Hi, there! Select or create a page to get started.</ion-col>
</ion-row> </ion-row>
<ion-row align-items-center style="margin-top: 30px;"> <ion-row align-items-center style="margin-top: 30px;" *ngIf="!api.isPublicUser">
<ion-col>(You can press <code>Ctrl</code> + <code>/</code> to search everywhere.)</ion-col> <ion-col>(You can press <code>Ctrl</code> + <code>/</code> to search everywhere.)</ion-col>
</ion-row> </ion-row>
<ion-row align-items-center *ngIf="api.isPublicUser">
Redirecting...
</ion-row>
</ion-grid> </ion-grid>

View File

@ -1,15 +1,28 @@
import { Component } from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {ApiService} from '../service/api.service'; import {ApiService} from '../service/api.service';
import {isDebug, debug} from '../utility';
@Component({ @Component({
selector: 'app-home', selector: 'app-home',
templateUrl: 'home.page.html', templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'], styleUrls: ['home.page.scss'],
}) })
export class HomePage { export class HomePage implements OnInit {
constructor( constructor(
protected api: ApiService, public readonly api: ApiService,
) {} ) {}
ngOnInit() {
if ( !this.api.isAuthenticated || this.api.isPublicUser ) {
if ( isDebug() ) {
debug('Forcing authentication...');
setTimeout(() => {
this.api.forceRestart();
}, 2000);
} else {
this.api.forceRestart();
}
}
}
} }

View File

@ -13,6 +13,7 @@ import {DatabaseEntry} from './db/DatabaseEntry';
import {FileGroup} from './db/FileGroup'; import {FileGroup} from './db/FileGroup';
import {Page} from './db/Page'; import {Page} from './db/Page';
import {PageNode} from './db/PageNode'; import {PageNode} from './db/PageNode';
import {debug} from '../utility';
export class ResourceNotAvailableOfflineError extends Error { export class ResourceNotAvailableOfflineError extends Error {
constructor(msg = 'This resource is not yet available offline on this device.') { constructor(msg = 'This resource is not yet available offline on this device.') {
@ -30,6 +31,9 @@ export class OfflinePrefetchWouldOverwriteLocalDataError extends Error {
providedIn: 'root' providedIn: 'root'
}) })
export class ApiService { export class ApiService {
public isAuthenticated = false;
public isPublicUser = false;
public systemBase?: string;
protected baseEndpoint: string = environment.backendBase; protected baseEndpoint: string = environment.backendBase;
protected statUrl: string = environment.statUrl; protected statUrl: string = environment.statUrl;
protected versionUrl: string = environment.versionUrl; protected versionUrl: string = environment.versionUrl;
@ -149,6 +153,10 @@ export class ApiService {
}); });
} }
public forceRestart() {
window.location.href = `${this.systemBase || '/'}start`;
}
public checkOnline(): Promise<boolean> { public checkOnline(): Promise<boolean> {
return new Promise(res => { return new Promise(res => {
fetch(this.statUrl).then(resp => { fetch(this.statUrl).then(resp => {
@ -170,6 +178,10 @@ export class ApiService {
} }
public async needsSync() { public async needsSync() {
if ( !this.isAuthenticated || this.isPublicUser ) {
return false;
}
const offlineKV = await this.db.getKeyValue('needs_online_sync'); const offlineKV = await this.db.getKeyValue('needs_online_sync');
return Boolean(offlineKV.data); return Boolean(offlineKV.data);
} }
@ -456,6 +468,10 @@ export class ApiService {
public getDeviceToken(): Promise<string> { public getDeviceToken(): Promise<string> {
return new Promise(async (res, rej) => { return new Promise(async (res, rej) => {
if ( this.isPublicUser ) {
return res();
}
const tokenKV = await this.db.getKeyValue('device_token'); const tokenKV = await this.db.getKeyValue('device_token');
if ( !tokenKV.data && this.isOffline ) { if ( !tokenKV.data && this.isOffline ) {
return rej(new ResourceNotAvailableOfflineError()); return rej(new ResourceNotAvailableOfflineError());
@ -483,6 +499,11 @@ export class ApiService {
return new Promise(async (res, rej) => { return new Promise(async (res, rej) => {
if ( !this.isOffline ) { if ( !this.isOffline ) {
this.getDeviceToken().then(token => { this.getDeviceToken().then(token => {
debug('Got device token:', token);
if ( !token ) {
return res();
}
this.post(`/session/resume/${token}`).subscribe({ this.post(`/session/resume/${token}`).subscribe({
next: result => { next: result => {
res(); res();

View File

@ -1,3 +1,4 @@
import {environment} from '../environments/environment';
export function uuid_v4() { export function uuid_v4() {
// @ts-ignore // @ts-ignore
@ -17,3 +18,29 @@ export function debounce(func: (...args: any[]) => any, timeout?: number) {
timer = setTimeout(next, timeout > 0 ? timeout : 300); timer = setTimeout(next, timeout > 0 ? timeout : 300);
}; };
} }
export function debugRun(closure: any) {
if ( typeof closure === 'function' ) {
return closure();
}
}
export function isDebug() {
return environment.outputDebug;
}
export function debug(...out: any[]) {
const caller = (new Error())?.stack?.split('\n')?.[1]?.split('@')?.[0]?.replace(/[\/\\<]/g, '');
// Define different types of styles
const baseStyles = [
'color: #fff',
'background-color: #449',
'padding: 2px 4px',
'border-radius: 2px'
].join(';');
if ( environment.outputDebug ) {
console.log(`%c${caller}`, baseStyles, ...out);
}
}

View File

@ -3,4 +3,5 @@ export const environment = {
backendBase: '/api/v1', backendBase: '/api/v1',
statUrl: '/stat?ngsw-bypass', statUrl: '/stat?ngsw-bypass',
versionUrl: '/i/version.html?ngsw-bypass', versionUrl: '/i/version.html?ngsw-bypass',
outputDebug: false,
}; };

View File

@ -7,6 +7,7 @@ export const environment = {
backendBase: '/link_api/api/v1', backendBase: '/link_api/api/v1',
statUrl: '/link_api/stat?ngsw-bypass', statUrl: '/link_api/stat?ngsw-bypass',
versionUrl: '/link_api/assets/version.html?ngsw-bypass', versionUrl: '/link_api/assets/version.html?ngsw-bypass',
outputDebug: true,
}; };
/* /*