From 4c10f9dd824b38ca3561a876abce306cc926e8ae Mon Sep 17 00:00:00 2001 From: "Patrik J. Braun" Date: Fri, 11 Aug 2023 17:44:51 +0200 Subject: [PATCH 1/2] Implement basic sharing listing on UI #607 --- src/backend/middlewares/SharingMWs.ts | 59 +++++++++++++++---- .../middlewares/user/AuthenticationMWs.ts | 12 +--- src/backend/model/database/SharingManager.ts | 26 +++++++- src/backend/routes/SharingRouter.ts | 20 ++++++- src/frontend/app/ui/gallery/share.service.ts | 26 ++++++++ .../share/share.gallery.component.html | 27 +++++++++ .../gallery/share/share.gallery.component.ts | 24 +++++++- .../app/ui/settings/settings.service.ts | 3 + .../sharings-list/sharing-list.service.ts | 26 -------- .../sharings-list.component.html | 2 +- .../sharings-list/sharings-list.component.ts | 14 ++--- .../unit/model/sql/SharingManager.spec.ts | 2 +- 12 files changed, 174 insertions(+), 67 deletions(-) delete mode 100644 src/frontend/app/ui/settings/sharings-list/sharing-list.service.ts diff --git a/src/backend/middlewares/SharingMWs.ts b/src/backend/middlewares/SharingMWs.ts index 0a51fb1f..0895d3a1 100644 --- a/src/backend/middlewares/SharingMWs.ts +++ b/src/backend/middlewares/SharingMWs.ts @@ -1,11 +1,11 @@ -import { NextFunction, Request, Response } from 'express'; -import { CreateSharingDTO, SharingDTO } from '../../common/entities/SharingDTO'; -import { ObjectManagers } from '../model/ObjectManagers'; -import { ErrorCodes, ErrorDTO } from '../../common/entities/Error'; -import { Config } from '../../common/config/private/Config'; -import { QueryParams } from '../../common/QueryParams'; +import {NextFunction, Request, Response} from 'express'; +import {CreateSharingDTO, SharingDTO} from '../../common/entities/SharingDTO'; +import {ObjectManagers} from '../model/ObjectManagers'; +import {ErrorCodes, ErrorDTO} from '../../common/entities/Error'; +import {Config} from '../../common/config/private/Config'; +import {QueryParams} from '../../common/QueryParams'; import * as path from 'path'; -import { UserRoles } from '../../common/entities/UserDTO'; +import {UserRoles} from '../../common/entities/UserDTO'; export class SharingMWs { public static async getSharing( @@ -20,9 +20,7 @@ export class SharingMWs { try { req.resultPipe = - await ObjectManagers.getInstance().SharingManager.findOne({ - sharingKey, - }); + await ObjectManagers.getInstance().SharingManager.findOne(sharingKey); return next(); } catch (err) { return next( @@ -58,9 +56,7 @@ export class SharingMWs { // eslint-disable-next-line no-constant-condition while (true) { try { - await ObjectManagers.getInstance().SharingManager.findOne({ - sharingKey, - }); + await ObjectManagers.getInstance().SharingManager.findOne(sharingKey); sharingKey = this.generateKey(); } catch (err) { break; @@ -173,6 +169,13 @@ export class SharingMWs { const sharingKey: string = req.params['sharingKey']; try { + // Check if user has the right to delete sharing. + if (req.session['user'].role < UserRoles.Admin) { + const s = await ObjectManagers.getInstance().SharingManager.findOne(sharingKey); + if (s.creator.id !== req.session['user'].id) { + return next(new ErrorDTO(ErrorCodes.NOT_AUTHORISED, 'Can\'t delete sharing.')); + } + } req.resultPipe = await ObjectManagers.getInstance().SharingManager.deleteSharing( sharingKey @@ -213,6 +216,36 @@ export class SharingMWs { } } + public static async listSharingForDir( + req: Request, + res: Response, + next: NextFunction + ): Promise { + if (Config.Sharing.enabled === false) { + return next(); + } + + const dir = path.normalize(req.params['directory'] || '/'); + try { + if (req.session['user'].role >= UserRoles.Admin) { + req.resultPipe = + await ObjectManagers.getInstance().SharingManager.listAllForDir(dir); + } else { + req.resultPipe = + await ObjectManagers.getInstance().SharingManager.listAllForDir(dir, req.session['user']); + } + return next(); + } catch (err) { + return next( + new ErrorDTO( + ErrorCodes.GENERAL_ERROR, + 'Error during listing shares', + err + ) + ); + } + } + private static generateKey(): string { function s4(): string { return Math.floor((1 + Math.random()) * 0x10000) diff --git a/src/backend/middlewares/user/AuthenticationMWs.ts b/src/backend/middlewares/user/AuthenticationMWs.ts index 94d97821..68bc569c 100644 --- a/src/backend/middlewares/user/AuthenticationMWs.ts +++ b/src/backend/middlewares/user/AuthenticationMWs.ts @@ -151,11 +151,7 @@ export class AuthenticationMWs { const sharingKey: string = (req.query[QueryParams.gallery.sharingKey_query] as string) || (req.params[QueryParams.gallery.sharingKey_params] as string); - const sharing = await ObjectManagers.getInstance().SharingManager.findOne( - { - sharingKey, - } - ); + const sharing = await ObjectManagers.getInstance().SharingManager.findOne(sharingKey); if ( !sharing || @@ -264,11 +260,7 @@ export class AuthenticationMWs { const sharingKey: string = (req.query[QueryParams.gallery.sharingKey_query] as string) || (req.params[QueryParams.gallery.sharingKey_params] as string); - const sharing = await ObjectManagers.getInstance().SharingManager.findOne( - { - sharingKey, - } - ); + const sharing = await ObjectManagers.getInstance().SharingManager.findOne(sharingKey); if (!sharing || sharing.expires < Date.now()) { return null; } diff --git a/src/backend/model/database/SharingManager.ts b/src/backend/model/database/SharingManager.ts index 5347e548..6c3c7fec 100644 --- a/src/backend/model/database/SharingManager.ts +++ b/src/backend/model/database/SharingManager.ts @@ -3,7 +3,8 @@ import {SQLConnection} from './SQLConnection'; import {SharingEntity} from './enitites/SharingEntity'; import {Config} from '../../../common/config/private/Config'; import {PasswordHelper} from '../PasswordHelper'; -import {DeleteResult, FindOptionsWhere} from 'typeorm'; +import {DeleteResult, SelectQueryBuilder} from 'typeorm'; +import {UserDTO} from '../../../common/entities/UserDTO'; export class SharingManager { private static async removeExpiredLink(): Promise { @@ -34,10 +35,29 @@ export class SharingManager { .getMany(); } - async findOne(filter: FindOptionsWhere): Promise { + + async listAllForDir(dir: string, user?: UserDTO): Promise { await SharingManager.removeExpiredLink(); const connection = await SQLConnection.getConnection(); - return await connection.getRepository(SharingEntity).findOneBy(filter); + const q: SelectQueryBuilder = connection + .getRepository(SharingEntity) + .createQueryBuilder('share') + .leftJoinAndSelect('share.creator', 'creator') + .where('path = :dir', {dir}); + if (user) { + q.andWhere('share.creator = :user', {user: user.id}); + } + return await q.getMany(); + } + + async findOne(sharingKey: string): Promise { + await SharingManager.removeExpiredLink(); + const connection = await SQLConnection.getConnection(); + return await connection.getRepository(SharingEntity) + .createQueryBuilder('share') + .leftJoinAndSelect('share.creator', 'creator') + .where('share.sharingKey = :sharingKey', {sharingKey}) + .getOne(); } async createSharing(sharing: SharingDTO): Promise { diff --git a/src/backend/routes/SharingRouter.ts b/src/backend/routes/SharingRouter.ts index 75c62adb..771977ed 100644 --- a/src/backend/routes/SharingRouter.ts +++ b/src/backend/routes/SharingRouter.ts @@ -14,6 +14,7 @@ export class SharingRouter { this.addCreateSharing(app); this.addUpdateSharing(app); this.addListSharing(app); + this.addListSharingForDir(app); this.addDeleteSharing(app); } @@ -64,7 +65,7 @@ export class SharingRouter { app.delete( [Config.Server.apiPath + '/share/:' + QueryParams.gallery.sharingKey_params], AuthenticationMWs.authenticate, - AuthenticationMWs.authorise(UserRoles.Admin), + AuthenticationMWs.authorise(UserRoles.User), SharingMWs.deleteSharing, ServerTimingMWs.addServerTiming, RenderingMWs.renderResult @@ -73,12 +74,25 @@ export class SharingRouter { private static addListSharing(app: express.Express): void { app.get( - [Config.Server.apiPath + '/share/list'], + [Config.Server.apiPath + '/share/listAll'], AuthenticationMWs.authenticate, - AuthenticationMWs.authorise(UserRoles.User), + AuthenticationMWs.authorise(UserRoles.Admin), SharingMWs.listSharing, ServerTimingMWs.addServerTiming, RenderingMWs.renderSharingList ); } + + private static addListSharingForDir(app: express.Express): void { + app.get( + [Config.Server.apiPath + '/share/list/:directory(*)', + Config.Server.apiPath + '/share/list//', + Config.Server.apiPath + '/share/list'], + AuthenticationMWs.authenticate, + AuthenticationMWs.authorise(UserRoles.User), + SharingMWs.listSharingForDir, + ServerTimingMWs.addServerTiming, + RenderingMWs.renderSharingList + ); + } } diff --git a/src/frontend/app/ui/gallery/share.service.ts b/src/frontend/app/ui/gallery/share.service.ts index 3ac364dc..2d5fce94 100644 --- a/src/frontend/app/ui/gallery/share.service.ts +++ b/src/frontend/app/ui/gallery/share.service.ts @@ -6,6 +6,8 @@ import {BehaviorSubject} from 'rxjs'; import {distinctUntilChanged, filter} from 'rxjs/operators'; import {QueryParams} from '../../../../common/QueryParams'; import {UserDTO} from '../../../../common/entities/UserDTO'; +import {Utils} from '../../../../common/Utils'; +import {Config} from '../../../../common/config/public/Config'; @Injectable() @@ -61,6 +63,11 @@ export class ShareService { }); } + public getUrl(share: SharingDTO): string { + return Utils.concatUrls(Config.Server.publicUrl, '/share/', share.sharingKey); + } + + onNewUser = async (user: UserDTO) => { if (user && !!user.usedSharingKey) { if ( @@ -135,4 +142,23 @@ export class ShareService { console.error(e); } } + + public async getSharingListForDir( + dir: string + ): Promise { + return this.networkService.getJson('/share/list/' + dir); + } + + + + public getSharingList(): Promise { + if (!Config.Sharing.enabled) { + return Promise.resolve([]); + } + return this.networkService.getJson('/share/listAll'); + } + + public deleteSharing(sharing: SharingDTO): Promise { + return this.networkService.deleteJson('/share/' + sharing.sharingKey); + } } diff --git a/src/frontend/app/ui/gallery/share/share.gallery.component.html b/src/frontend/app/ui/gallery/share/share.gallery.component.html index c7b86512..ef342560 100644 --- a/src/frontend/app/ui/gallery/share/share.gallery.component.html +++ b/src/frontend/app/ui/gallery/share/share.gallery.component.html @@ -117,5 +117,32 @@ + +
+ + + + + + + + + + + + + + + + + +
KeyCreatorExpires
{{share.sharingKey}}{{share.creator.name}}{{share.expires | date}} + +
+
diff --git a/src/frontend/app/ui/gallery/share/share.gallery.component.ts b/src/frontend/app/ui/gallery/share/share.gallery.component.ts index c24111b4..705db94b 100644 --- a/src/frontend/app/ui/gallery/share/share.gallery.component.ts +++ b/src/frontend/app/ui/gallery/share/share.gallery.component.ts @@ -37,13 +37,15 @@ export class GalleryShareComponent implements OnInit, OnDestroy { modalRef: BsModalRef; invalidSettings = $localize`Invalid settings`; + activeShares: SharingDTO[] = []; + text = { Yes: 'Yes', No: 'No', }; constructor( - private sharingService: ShareService, + public sharingService: ShareService, public galleryService: ContentService, private notification: NotificationService, private modalService: BsModalService @@ -54,7 +56,8 @@ export class GalleryShareComponent implements OnInit, OnDestroy { ngOnInit(): void { this.contentSubscription = this.galleryService.content.subscribe( - (content: ContentWrapper) => { + async (content: ContentWrapper) => { + this.activeShares = []; this.enabled = !!content.directory; if (!this.enabled) { return; @@ -63,6 +66,7 @@ export class GalleryShareComponent implements OnInit, OnDestroy { content.directory.path, content.directory.name ); + await this.updateActiveSharesList(); } ); } @@ -73,6 +77,21 @@ export class GalleryShareComponent implements OnInit, OnDestroy { } } + + async deleteSharing(sharing: SharingDTO): Promise { + await this.sharingService.deleteSharing(sharing); + await this.updateActiveSharesList(); + } + + private async updateActiveSharesList() { + try { + this.activeShares = await this.sharingService.getSharingListForDir(this.currentDir); + } catch (e) { + this.activeShares = []; + console.error(e); + } + } + calcValidity(): number { switch (parseInt(this.input.valid.type.toString(), 10)) { case ValidityTypes.Minutes: @@ -112,6 +131,7 @@ export class GalleryShareComponent implements OnInit, OnDestroy { this.calcValidity() ); this.url = Utils.concatUrls(Config.Server.publicUrl, '/share/', this.sharing.sharingKey); + await this.updateActiveSharesList(); } async openModal(template: TemplateRef): Promise { diff --git a/src/frontend/app/ui/settings/settings.service.ts b/src/frontend/app/ui/settings/settings.service.ts index 965a1ad4..5df4e9c4 100644 --- a/src/frontend/app/ui/settings/settings.service.ts +++ b/src/frontend/app/ui/settings/settings.service.ts @@ -11,6 +11,9 @@ import {DefaultsJobs} from '../../../../common/entities/job/JobDTO'; import {StatisticDTO} from '../../../../common/entities/settings/StatisticDTO'; import {ScheduledJobsService} from './scheduled-jobs.service'; import {IWebConfigClassPrivate} from '../../../../../node_modules/typeconfig/src/decorators/class/IWebConfigClass'; +import {SharingDTO} from '../../../../common/entities/SharingDTO'; +import {Utils} from '../../../../common/Utils'; +import {Config} from '../../../../common/config/public/Config'; export enum ConfigStyle { diff --git a/src/frontend/app/ui/settings/sharings-list/sharing-list.service.ts b/src/frontend/app/ui/settings/sharings-list/sharing-list.service.ts deleted file mode 100644 index a7ca0938..00000000 --- a/src/frontend/app/ui/settings/sharings-list/sharing-list.service.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {Injectable} from '@angular/core'; -import {SharingDTO} from '../../../../../common/entities/SharingDTO'; -import {NetworkService} from '../../../model/network/network.service'; -import {SettingsService} from '../settings.service'; -import {Config} from '../../../../../common/config/public/Config'; - -@Injectable({ - providedIn: 'root' -}) -export class SharingListService { - - constructor(private networkService: NetworkService) { - } - - - public getSharingList(): Promise { - if (!Config.Sharing.enabled) { - return Promise.resolve([]); - } - return this.networkService.getJson('/share/list'); - } - - public deleteSharing(sharing: SharingDTO): Promise { - return this.networkService.deleteJson('/share/' + sharing.sharingKey); - } -} diff --git a/src/frontend/app/ui/settings/sharings-list/sharings-list.component.html b/src/frontend/app/ui/settings/sharings-list/sharings-list.component.html index f6ca68eb..cae545e0 100644 --- a/src/frontend/app/ui/settings/sharings-list/sharings-list.component.html +++ b/src/frontend/app/ui/settings/sharings-list/sharings-list.component.html @@ -20,7 +20,7 @@ - {{share.sharingKey}} + {{share.sharingKey}} {{share.path}} {{share.creator.name}} {{share.expires | date}} diff --git a/src/frontend/app/ui/settings/sharings-list/sharings-list.component.ts b/src/frontend/app/ui/settings/sharings-list/sharings-list.component.ts index 699b91b5..c3f3fed5 100644 --- a/src/frontend/app/ui/settings/sharings-list/sharings-list.component.ts +++ b/src/frontend/app/ui/settings/sharings-list/sharings-list.component.ts @@ -1,9 +1,7 @@ import {Component, OnInit} from '@angular/core'; import {SharingDTO} from '../../../../../common/entities/SharingDTO'; -import {SharingListService} from './sharing-list.service'; import {SettingsService} from '../settings.service'; -import {Config} from '../../../../../common/config/public/Config'; -import {Utils} from '../../../../../common/Utils'; +import {ShareService} from '../../gallery/share.service'; @Component({ selector: 'app-settigns-sharings-list', @@ -13,10 +11,10 @@ import {Utils} from '../../../../../common/Utils'; export class SharingsListComponent implements OnInit { public shares: SharingDTO[] = []; - public sharingUrl = Utils.concatUrls(Config.Server.publicUrl, '/share') + '/'; - constructor(public sharingList: SharingListService, - private settingsService: SettingsService) { + + constructor(public sharingService: ShareService, + public settingsService: SettingsService) { } @@ -29,13 +27,13 @@ export class SharingsListComponent implements OnInit { } async deleteSharing(sharing: SharingDTO): Promise { - await this.sharingList.deleteSharing(sharing); + await this.sharingService.deleteSharing(sharing); await this.getSharingList(); } private async getSharingList(): Promise { try { - this.shares = await this.sharingList.getSharingList(); + this.shares = await this.sharingService.getSharingList(); } catch (err) { this.shares = []; throw err; diff --git a/test/backend/unit/model/sql/SharingManager.spec.ts b/test/backend/unit/model/sql/SharingManager.spec.ts index b2afc800..fb43bc92 100644 --- a/test/backend/unit/model/sql/SharingManager.spec.ts +++ b/test/backend/unit/model/sql/SharingManager.spec.ts @@ -82,7 +82,7 @@ describe('SharingManager', (sqlHelper: DBTestHelper) => { }; const saved = await sm.createSharing(sharing); - const found = await sm.findOne({sharingKey: 'testKey'}); + const found = await sm.findOne('testKey'); expect(found.id).to.not.equals(null); expect(found.sharingKey).to.equals(sharing.sharingKey); From ca51e70e8e0d3f9afe8d72fdb950dfe22fcbe5e3 Mon Sep 17 00:00:00 2001 From: "Patrik J. Braun" Date: Sat, 12 Aug 2023 11:08:58 +0200 Subject: [PATCH 2/2] Improve sharing list design and prevent accidental shares #607 --- .../gallery/share/share.gallery.component.css | 9 ++ .../share/share.gallery.component.html | 84 +++++++++++-------- .../gallery/share/share.gallery.component.ts | 39 +++++++-- .../sharings-list/sharings-list.component.ts | 2 + 4 files changed, 94 insertions(+), 40 deletions(-) diff --git a/src/frontend/app/ui/gallery/share/share.gallery.component.css b/src/frontend/app/ui/gallery/share/share.gallery.component.css index 17ea8700..531d09e4 100644 --- a/src/frontend/app/ui/gallery/share/share.gallery.component.css +++ b/src/frontend/app/ui/gallery/share/share.gallery.component.css @@ -24,3 +24,12 @@ a.dropdown-item, button.dropdown-item, div.dropdown-item { a.dropdown-item span, button.dropdown-item span, div.dropdown-item span { padding-right: 0.8rem; } + +a.list-shares-button { + cursor: pointer; + color: inherit; +} + +a.list-shares-button:hover { + text-decoration: underline; +} diff --git a/src/frontend/app/ui/gallery/share/share.gallery.component.html b/src/frontend/app/ui/gallery/share/share.gallery.component.html index ef342560..049f37e3 100644 --- a/src/frontend/app/ui/gallery/share/share.gallery.component.html +++ b/src/frontend/app/ui/gallery/share/share.gallery.component.html @@ -28,16 +28,25 @@ class="form-control input-md" type="text" readonly - [disabled]="!shareForm.form.valid" + [disabled]="!shareForm.form.valid || !urlValid" [ngModel]="shareForm.form.valid ? url: invalidSettings">
- +
@@ -117,32 +126,39 @@ - -
- - - - - - - - - - - - - - - - - -
KeyCreatorExpires
{{share.sharingKey}}{{share.creator.name}}{{share.expires | date}} - -
-
+ + diff --git a/src/frontend/app/ui/gallery/share/share.gallery.component.ts b/src/frontend/app/ui/gallery/share/share.gallery.component.ts index 705db94b..89f874ed 100644 --- a/src/frontend/app/ui/gallery/share/share.gallery.component.ts +++ b/src/frontend/app/ui/gallery/share/share.gallery.component.ts @@ -9,6 +9,9 @@ import {NotificationService} from '../../../model/notification.service'; import {BsModalService} from 'ngx-bootstrap/modal'; import {BsModalRef} from 'ngx-bootstrap/modal/bs-modal-ref.service'; import {Subscription} from 'rxjs'; +import {UserRoles} from '../../../../../common/entities/UserDTO'; +import {AuthenticationService} from '../../../model/network/authentication.service'; +import {ClipboardService} from 'ngx-clipboard'; @Component({ selector: 'app-gallery-share', @@ -19,6 +22,8 @@ export class GalleryShareComponent implements OnInit, OnDestroy { enabled = true; @Input() dropDownItem = false; url = ''; + urlValid = false; + showSharingList = false; input = { includeSubfolders: true, @@ -48,12 +53,18 @@ export class GalleryShareComponent implements OnInit, OnDestroy { public sharingService: ShareService, public galleryService: ContentService, private notification: NotificationService, - private modalService: BsModalService + private modalService: BsModalService, + public authService: AuthenticationService, + private clipboardService: ClipboardService ) { this.text.Yes = $localize`Yes`; this.text.No = $localize`No`; } + public get IsAdmin() { + return this.authService.user.value.role > UserRoles.Admin; + } + ngOnInit(): void { this.contentSubscription = this.galleryService.content.subscribe( async (content: ContentWrapper) => { @@ -112,6 +123,7 @@ export class GalleryShareComponent implements OnInit, OnDestroy { if (this.sharing == null) { return; } + this.urlValid = false; this.url = $localize`loading..`; this.sharing = await this.sharingService.updateSharing( this.currentDir, @@ -120,32 +132,37 @@ export class GalleryShareComponent implements OnInit, OnDestroy { this.input.password, this.calcValidity() ); - this.url = Utils.concatUrls(Config.Server.publicUrl, '/share/', this.sharing.sharingKey); + this.urlValid = true; + this.url = this.sharingService.getUrl(this.sharing); + await this.updateActiveSharesList(); } async get(): Promise { + this.urlValid = false; this.url = $localize`loading..`; this.sharing = await this.sharingService.createSharing( this.currentDir, this.input.includeSubfolders, this.calcValidity() ); - this.url = Utils.concatUrls(Config.Server.publicUrl, '/share/', this.sharing.sharingKey); + this.url = this.sharingService.getUrl(this.sharing); + this.urlValid = true; await this.updateActiveSharesList(); } async openModal(template: TemplateRef): Promise { - await this.get(); + this.url = $localize`Click share to get a link.`; + this.urlValid = false; + this.sharing = null; this.input.password = ''; if (this.modalRef) { this.modalRef.hide(); } this.modalRef = this.modalService.show(template); - document.body.style.paddingRight = '0px'; } onCopy(): void { - this.notification.success($localize`Url has been copied to clipboard`); + this.notification.success($localize`Sharing link has been copied to clipboard`); } public hideModal(): void { @@ -153,6 +170,16 @@ export class GalleryShareComponent implements OnInit, OnDestroy { this.modalRef = null; this.sharing = null; } + + async share() { + await this.get(); + if (this.clipboardService.isSupported) { + this.clipboardService.copy(this.url); + this.onCopy(); + } + + } + } diff --git a/src/frontend/app/ui/settings/sharings-list/sharings-list.component.ts b/src/frontend/app/ui/settings/sharings-list/sharings-list.component.ts index c3f3fed5..e2b3e4d5 100644 --- a/src/frontend/app/ui/settings/sharings-list/sharings-list.component.ts +++ b/src/frontend/app/ui/settings/sharings-list/sharings-list.component.ts @@ -2,6 +2,8 @@ import {Component, OnInit} from '@angular/core'; import {SharingDTO} from '../../../../../common/entities/SharingDTO'; import {SettingsService} from '../settings.service'; import {ShareService} from '../../gallery/share.service'; +import {AuthenticationService} from '../../../model/network/authentication.service'; +import {UserRoles} from '../../../../../common/entities/UserDTO'; @Component({ selector: 'app-settigns-sharings-list',