#4 - add support for sharing and viewing a page publicly without login

This commit is contained in:
Garrett Mills 2021-03-04 11:26:39 -06:00
parent 4f14a40994
commit 01c2fc18f2
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246
13 changed files with 119 additions and 25 deletions

View File

@ -3,6 +3,7 @@ import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
import {LoginPage} from './pages/login/login.page'; import {LoginPage} from './pages/login/login.page';
import {AuthService} from './service/auth.service'; import {AuthService} from './service/auth.service';
import {GuestOnlyGuard} from './service/guard/GuestOnly.guard'; import {GuestOnlyGuard} from './service/guard/GuestOnly.guard';
import {EditorGuard} from './service/guard/Editor.guard';
const routes: Routes = [ const routes: Routes = [
{ {
@ -17,7 +18,7 @@ const routes: Routes = [
}, },
{ {
path: 'editor', path: 'editor',
canActivate: [AuthService], canActivate: [EditorGuard],
loadChildren: () => import('./components/components.module').then( m => m.ComponentsModule) loadChildren: () => import('./components/components.module').then( m => m.ComponentsModule)
}, },
{ {

View File

@ -17,6 +17,9 @@
</ion-item> </ion-item>
</ion-card> </ion-card>
</ion-col> </ion-col>
<ion-col>
<div class="not-available" *ngIf="fileRecords.length < 1 && readonly">There are no files in this group yet.</div>
</ion-col>
</ion-row> </ion-row>
</ion-grid> </ion-grid>
</div> </div>

View File

@ -2,11 +2,11 @@ div.files-wrapper {
border: 2px solid #8c8c8c; border: 2px solid #8c8c8c;
border-radius: 3px; border-radius: 3px;
margin-top: 15px; margin-top: 15px;
}
&.not-available { .not-available {
height: 150px; height: 150px;
text-align: center; text-align: center;
padding-top: 40px; padding-top: 40px;
color: #494949; color: #494949;
}
} }

View File

@ -50,6 +50,8 @@ export class FilesComponent extends EditorNodeContract implements OnInit {
this.node.Value = {}; this.node.Value = {};
} }
console.log('files load', this)
if ( !this.node.Value.Value && !this.readonly ) { if ( !this.node.Value.Value && !this.readonly ) {
this.dbRecord = await this.api.createFileGroup(this.page.UUID, this.node.UUID); this.dbRecord = await this.api.createFileGroup(this.page.UUID, this.node.UUID);
this.fileRecords = this.dbRecord.files; this.fileRecords = this.dbRecord.files;

View File

@ -22,9 +22,9 @@
(click)="dismiss()" (click)="dismiss()"
><i class="fa fa-times"></i></button> ><i class="fa fa-times"></i></button>
</div> </div>
<ion-buttons *ngIf="!readonly" style="flex-wrap: wrap;"> <ion-buttons style="flex-wrap: wrap;">
<ion-button [disabled]="isLoading" (click)="onActionClick('folder-add')"><i class="fa fa-folder-plus"></i>&nbsp;&nbsp;Folder</ion-button> <ion-button *ngIf="!readonly" [disabled]="isLoading" (click)="onActionClick('folder-add')"><i class="fa fa-folder-plus"></i>&nbsp;&nbsp;Folder</ion-button>
<ion-button [disabled]="isLoading" (click)="onUploadFilesClick($event)"><i class="fa fa-upload"></i>&nbsp;&nbsp;Upload Files</ion-button> <ion-button *ngIf="!readonly" [disabled]="isLoading" (click)="onUploadFilesClick($event)"><i class="fa fa-upload"></i>&nbsp;&nbsp;Upload Files</ion-button>
<ion-button [disabled]="isLoading" (click)="openFileBox()" *ngIf="!fullPage"><i class="fa fa-external-link-alt"></i>&nbsp;&nbsp;Open</ion-button> <ion-button [disabled]="isLoading" (click)="openFileBox()" *ngIf="!fullPage"><i class="fa fa-external-link-alt"></i>&nbsp;&nbsp;Open</ion-button>
<div class="buffer"></div> <div class="buffer"></div>
@ -38,7 +38,7 @@
></ion-input> ></ion-input>
</div> </div>
<input type="file" name="fileInput" #fileInput multiple style="display: none;" (change)="onUploadFilesChange($event)"> <input *ngIf="!readonly" type="file" name="fileInput" #fileInput multiple style="display: none;" (change)="onUploadFilesChange($event)">
</ion-buttons> </ion-buttons>
</ion-toolbar> </ion-toolbar>
<div class="content-wrapper" (contextmenu)="onSurfaceContextMenu($event)"> <div class="content-wrapper" (contextmenu)="onSurfaceContextMenu($event)">

View File

@ -1,6 +1,6 @@
.container { .container {
display: flex; display: flex;
min-height: 600px; min-height: 200px;
flex-direction: row; flex-direction: row;
.editor-container, .display { .editor-container, .display {

View File

@ -22,9 +22,13 @@
<ion-button fill="outline" color="medium" (click)="getShareLink('update')"> <ion-button fill="outline" color="medium" (click)="getShareLink('update')">
<ion-icon name="link" color="dark"></ion-icon>&nbsp;&nbsp;Edit <ion-icon name="link" color="dark"></ion-icon>&nbsp;&nbsp;Edit
</ion-button> </ion-button>
<ion-button fill="outline" color="medium" (click)="getShareLink('manage')"> <ion-button fill="outline" color="medium" (click)="getShareLink('manage')" [disabled]="publicLink">
<ion-icon name="link" color="dark"></ion-icon>&nbsp;&nbsp;Manage <ion-icon name="link" color="dark"></ion-icon>&nbsp;&nbsp;Manage
</ion-button> </ion-button>
<ion-item title="If checked, the link will never expire, and can be accessed by anyone without logging in.">
<ion-checkbox [(ngModel)]="publicLink"></ion-checkbox>
<ion-label>&nbsp;&nbsp;Public link?</ion-label>
</ion-item>
</ion-buttons> </ion-buttons>
</ion-col> </ion-col>
</ion-row> </ion-row>
@ -49,7 +53,7 @@
<ion-button fill="invisible" (click)="setShareLevel(sharingInfo.view[i], 'update')"> <ion-button fill="invisible" (click)="setShareLevel(sharingInfo.view[i], 'update')">
<ion-icon name="create" color="medium"></ion-icon> <ion-icon name="create" color="medium"></ion-icon>
</ion-button> </ion-button>
<ion-button fill="invisible" (click)="setShareLevel(sharingInfo.view[i], 'manage')"> <ion-button fill="invisible" (click)="setShareLevel(sharingInfo.view[i], 'manage')" *ngIf="!group.public">
<ion-icon name="build" color="medium"></ion-icon> <ion-icon name="build" color="medium"></ion-icon>
</ion-button> </ion-button>
<ion-button fill="invisible" (click)="unsharePage(sharingInfo.view[i])"> <ion-button fill="invisible" (click)="unsharePage(sharingInfo.view[i])">
@ -66,7 +70,7 @@
<ion-button fill="invisible" (click)="setShareLevel(sharingInfo.update[i], 'view')"> <ion-button fill="invisible" (click)="setShareLevel(sharingInfo.update[i], 'view')">
<ion-icon name="eye" color="medium"></ion-icon> <ion-icon name="eye" color="medium"></ion-icon>
</ion-button> </ion-button>
<ion-button fill="invisible" (click)="setShareLevel(sharingInfo.update[i], 'manage')"> <ion-button fill="invisible" (click)="setShareLevel(sharingInfo.update[i], 'manage')" *ngIf="!group.public">
<ion-icon name="build" color="medium"></ion-icon> <ion-icon name="build" color="medium"></ion-icon>
</ion-button> </ion-button>
<ion-button fill="invisible" (click)="unsharePage(sharingInfo.update[i])"> <ion-button fill="invisible" (click)="unsharePage(sharingInfo.update[i])">

View File

@ -2,6 +2,7 @@ import {Component, Input, OnInit} from '@angular/core';
import {ModalController} from '@ionic/angular'; import {ModalController} from '@ionic/angular';
import {ApiService} from '../../../service/api.service'; import {ApiService} from '../../../service/api.service';
import {Observable} from 'rxjs'; import {Observable} from 'rxjs';
import {environment} from '../../../../environments/environment';
@Component({ @Component({
selector: 'app-selector', selector: 'app-selector',
@ -11,11 +12,12 @@ import {Observable} from 'rxjs';
export class SelectorComponent implements OnInit { export class SelectorComponent implements OnInit {
@Input() node: any; @Input() node: any;
public sharingInfo: { public sharingInfo: {
view: Array<{username: string, id: string, level: 'view'|'update'|'manage'}>, view: Array<{username: string, public?: boolean, id: string, level: 'view'|'update'|'manage'}>,
update: Array<{username: string, id: string, level: 'view'|'update'|'manage'}>, update: Array<{username: string, public?: boolean, id: string, level: 'view'|'update'|'manage'}>,
manage: Array<{username: string, id: string, level: 'view'|'update'|'manage'}>, manage: Array<{username: string, public?: boolean, id: string, level: 'view'|'update'|'manage'}>,
} = {view: [], update: [], manage: []}; } = {view: [], update: [], manage: []};
public generatedLink = ''; public generatedLink = '';
public publicLink = false;
constructor( constructor(
protected modals: ModalController, protected modals: ModalController,
@ -54,7 +56,7 @@ export class SelectorComponent implements OnInit {
} }
setShareLevel(group, level) { setShareLevel(group, level) {
this.api.post(`/share/page/${this.node.id}/share`, { user_id: group.id, level }).subscribe(result => { this.api.post(`/share/page/${this.node.id}/share?public=${!!group.public}`, { user_id: group.id, level }).subscribe(result => {
this.loadShareInfo().subscribe(data => { this.loadShareInfo().subscribe(data => {
this.sharingInfo = data; this.sharingInfo = data;
}); });
@ -62,7 +64,7 @@ export class SelectorComponent implements OnInit {
} }
unsharePage(group) { unsharePage(group) {
this.api.post(`/share/page/${this.node.id}/revoke`, { user_id: group.id }).subscribe(result => { this.api.post(`/share/page/${this.node.id}/revoke?public=${!!group.public}`, { user_id: group.id }).subscribe(result => {
this.loadShareInfo().subscribe(data => { this.loadShareInfo().subscribe(data => {
this.sharingInfo = data; this.sharingInfo = data;
}); });
@ -70,8 +72,13 @@ export class SelectorComponent implements OnInit {
} }
getShareLink(level) { getShareLink(level) {
this.api.get(`/share/page/${this.node.id}/link/${level}`).subscribe(result => { this.api.get(`/share/page/${this.node.id}/link/${level}?public=${this.publicLink}`).subscribe(result => {
if ( this.publicLink ) {
this.generatedLink = `${window.location.origin}${environment.appBase}editor;id=${this.node.id}`;
this.ngOnInit();
} else {
this.generatedLink = result.data.link; this.generatedLink = result.data.link;
}
}); });
} }
} }

View File

@ -1,6 +1,7 @@
import {Component, OnInit} 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'; import {isDebug, debug} from '../utility';
import {Router} from '@angular/router';
@Component({ @Component({
selector: 'app-home', selector: 'app-home',
@ -10,6 +11,7 @@ import {isDebug, debug} from '../utility';
export class HomePage implements OnInit { export class HomePage implements OnInit {
constructor( constructor(
public readonly api: ApiService, public readonly api: ApiService,
public readonly router: Router,
) {} ) {}
ngOnInit() { ngOnInit() {
@ -17,10 +19,10 @@ export class HomePage implements OnInit {
if ( isDebug() ) { if ( isDebug() ) {
debug('Forcing authentication...'); debug('Forcing authentication...');
setTimeout(() => { setTimeout(() => {
this.api.forceRestart(); this.router.navigate(['/login']);
}, 2000); }, 2000);
} else { } else {
this.api.forceRestart(); this.router.navigate(['/login']);
} }
} }
} }

View File

@ -1463,6 +1463,36 @@ export class ApiService {
}); });
} }
public checkPermission(permission: string): Promise<boolean> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
return rej(new ResourceNotAvailableOfflineError());
}
this.post('/share/check', { permission }).subscribe({
next: result => {
return res(result.data.check);
},
error: rej,
});
});
}
public checkPagePermission(PageId: string, level: string = 'view'): Promise<boolean> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
return rej(new ResourceNotAvailableOfflineError());
}
this.post(`/share/check-page/${PageId}/${level}`, {}).subscribe({
next: result => {
return res(result.data.check);
},
error: rej,
});
});
}
public attemptLogin(uid: string, password: string): Promise<{ success: boolean, message?: string }> { public attemptLogin(uid: string, password: string): Promise<{ success: boolean, message?: string }> {
return new Promise(async (res, rej) => { return new Promise(async (res, rej) => {
if ( this.isOffline ) { if ( this.isOffline ) {

View File

@ -0,0 +1,43 @@
import {ActivatedRoute, ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router';
import {ApiService} from '../api.service';
import {NavigationService} from '../navigation.service';
import {Injectable} from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class EditorGuard implements CanActivate {
constructor(
protected readonly api: ApiService,
protected readonly router: Router,
protected readonly nav: NavigationService,
) { }
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
const checkCanActivate = async () => {
const PageId = route.paramMap.get('id');
const canView = await this.api.checkPagePermission(String(PageId));
if ( !canView ) {
await this.router.navigate(['/login']);
return false;
} else {
return true;
}
};
return new Promise(async res => {
if ( !this.nav.initialized$.getValue() ) {
const sub = this.nav.initialized$.subscribe(async initialized => {
if ( initialized ) {
sub.unsubscribe();
return res(await checkCanActivate());
}
});
} else {
return res(await checkCanActivate());
}
});
}
}

View File

@ -6,5 +6,6 @@ export const environment = {
versionUrl: '/i/version.html?ngsw-bypass', versionUrl: '/i/version.html?ngsw-bypass',
logoUrl: '/i/assets/icon/logo_lines.svg', logoUrl: '/i/assets/icon/logo_lines.svg',
starshipUrl: '/auth/starship_oauth/login', starshipUrl: '/auth/starship_oauth/login',
appBase: '/i/',
outputDebug: false, outputDebug: false,
}; };

View File

@ -10,6 +10,7 @@ export const environment = {
versionUrl: '/link_api/assets/version.html?ngsw-bypass', versionUrl: '/link_api/assets/version.html?ngsw-bypass',
logoUrl: '/assets/icon/logo_lines.svg', logoUrl: '/assets/icon/logo_lines.svg',
starshipUrl: '/link_api/auth/starship_oauth/login', starshipUrl: '/link_api/auth/starship_oauth/login',
appBase: '/',
outputDebug: true, outputDebug: true,
}; };