1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2024-11-30 09:07:06 +02:00

Merge pull request #693 from bpatrik/sharing-history

Showing sharing history and preventing accidental sharing
This commit is contained in:
Patrik J. Braun 2023-08-12 11:26:45 +02:00 committed by GitHub
commit d64670751c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 241 additions and 80 deletions

View File

@ -20,9 +20,7 @@ export class SharingMWs {
try { try {
req.resultPipe = req.resultPipe =
await ObjectManagers.getInstance().SharingManager.findOne({ await ObjectManagers.getInstance().SharingManager.findOne(sharingKey);
sharingKey,
});
return next(); return next();
} catch (err) { } catch (err) {
return next( return next(
@ -58,9 +56,7 @@ export class SharingMWs {
// eslint-disable-next-line no-constant-condition // eslint-disable-next-line no-constant-condition
while (true) { while (true) {
try { try {
await ObjectManagers.getInstance().SharingManager.findOne({ await ObjectManagers.getInstance().SharingManager.findOne(sharingKey);
sharingKey,
});
sharingKey = this.generateKey(); sharingKey = this.generateKey();
} catch (err) { } catch (err) {
break; break;
@ -173,6 +169,13 @@ export class SharingMWs {
const sharingKey: string = req.params['sharingKey']; const sharingKey: string = req.params['sharingKey'];
try { 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 = req.resultPipe =
await ObjectManagers.getInstance().SharingManager.deleteSharing( await ObjectManagers.getInstance().SharingManager.deleteSharing(
sharingKey sharingKey
@ -213,6 +216,36 @@ export class SharingMWs {
} }
} }
public static async listSharingForDir(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
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 { private static generateKey(): string {
function s4(): string { function s4(): string {
return Math.floor((1 + Math.random()) * 0x10000) return Math.floor((1 + Math.random()) * 0x10000)

View File

@ -151,11 +151,7 @@ export class AuthenticationMWs {
const sharingKey: string = const sharingKey: string =
(req.query[QueryParams.gallery.sharingKey_query] as string) || (req.query[QueryParams.gallery.sharingKey_query] as string) ||
(req.params[QueryParams.gallery.sharingKey_params] as string); (req.params[QueryParams.gallery.sharingKey_params] as string);
const sharing = await ObjectManagers.getInstance().SharingManager.findOne( const sharing = await ObjectManagers.getInstance().SharingManager.findOne(sharingKey);
{
sharingKey,
}
);
if ( if (
!sharing || !sharing ||
@ -264,11 +260,7 @@ export class AuthenticationMWs {
const sharingKey: string = const sharingKey: string =
(req.query[QueryParams.gallery.sharingKey_query] as string) || (req.query[QueryParams.gallery.sharingKey_query] as string) ||
(req.params[QueryParams.gallery.sharingKey_params] as string); (req.params[QueryParams.gallery.sharingKey_params] as string);
const sharing = await ObjectManagers.getInstance().SharingManager.findOne( const sharing = await ObjectManagers.getInstance().SharingManager.findOne(sharingKey);
{
sharingKey,
}
);
if (!sharing || sharing.expires < Date.now()) { if (!sharing || sharing.expires < Date.now()) {
return null; return null;
} }

View File

@ -3,7 +3,8 @@ import {SQLConnection} from './SQLConnection';
import {SharingEntity} from './enitites/SharingEntity'; import {SharingEntity} from './enitites/SharingEntity';
import {Config} from '../../../common/config/private/Config'; import {Config} from '../../../common/config/private/Config';
import {PasswordHelper} from '../PasswordHelper'; import {PasswordHelper} from '../PasswordHelper';
import {DeleteResult, FindOptionsWhere} from 'typeorm'; import {DeleteResult, SelectQueryBuilder} from 'typeorm';
import {UserDTO} from '../../../common/entities/UserDTO';
export class SharingManager { export class SharingManager {
private static async removeExpiredLink(): Promise<DeleteResult> { private static async removeExpiredLink(): Promise<DeleteResult> {
@ -34,10 +35,29 @@ export class SharingManager {
.getMany(); .getMany();
} }
async findOne(filter: FindOptionsWhere<SharingDTO>): Promise<SharingDTO> {
async listAllForDir(dir: string, user?: UserDTO): Promise<SharingDTO[]> {
await SharingManager.removeExpiredLink(); await SharingManager.removeExpiredLink();
const connection = await SQLConnection.getConnection(); const connection = await SQLConnection.getConnection();
return await connection.getRepository(SharingEntity).findOneBy(filter); const q: SelectQueryBuilder<SharingEntity> = 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<SharingDTO> {
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<SharingDTO> { async createSharing(sharing: SharingDTO): Promise<SharingDTO> {

View File

@ -14,6 +14,7 @@ export class SharingRouter {
this.addCreateSharing(app); this.addCreateSharing(app);
this.addUpdateSharing(app); this.addUpdateSharing(app);
this.addListSharing(app); this.addListSharing(app);
this.addListSharingForDir(app);
this.addDeleteSharing(app); this.addDeleteSharing(app);
} }
@ -64,7 +65,7 @@ export class SharingRouter {
app.delete( app.delete(
[Config.Server.apiPath + '/share/:' + QueryParams.gallery.sharingKey_params], [Config.Server.apiPath + '/share/:' + QueryParams.gallery.sharingKey_params],
AuthenticationMWs.authenticate, AuthenticationMWs.authenticate,
AuthenticationMWs.authorise(UserRoles.Admin), AuthenticationMWs.authorise(UserRoles.User),
SharingMWs.deleteSharing, SharingMWs.deleteSharing,
ServerTimingMWs.addServerTiming, ServerTimingMWs.addServerTiming,
RenderingMWs.renderResult RenderingMWs.renderResult
@ -73,12 +74,25 @@ export class SharingRouter {
private static addListSharing(app: express.Express): void { private static addListSharing(app: express.Express): void {
app.get( app.get(
[Config.Server.apiPath + '/share/list'], [Config.Server.apiPath + '/share/listAll'],
AuthenticationMWs.authenticate, AuthenticationMWs.authenticate,
AuthenticationMWs.authorise(UserRoles.User), AuthenticationMWs.authorise(UserRoles.Admin),
SharingMWs.listSharing, SharingMWs.listSharing,
ServerTimingMWs.addServerTiming, ServerTimingMWs.addServerTiming,
RenderingMWs.renderSharingList 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
);
}
} }

View File

@ -6,6 +6,8 @@ import {BehaviorSubject} from 'rxjs';
import {distinctUntilChanged, filter} from 'rxjs/operators'; import {distinctUntilChanged, filter} from 'rxjs/operators';
import {QueryParams} from '../../../../common/QueryParams'; import {QueryParams} from '../../../../common/QueryParams';
import {UserDTO} from '../../../../common/entities/UserDTO'; import {UserDTO} from '../../../../common/entities/UserDTO';
import {Utils} from '../../../../common/Utils';
import {Config} from '../../../../common/config/public/Config';
@Injectable() @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) => { onNewUser = async (user: UserDTO) => {
if (user && !!user.usedSharingKey) { if (user && !!user.usedSharingKey) {
if ( if (
@ -135,4 +142,23 @@ export class ShareService {
console.error(e); console.error(e);
} }
} }
public async getSharingListForDir(
dir: string
): Promise<SharingDTO[]> {
return this.networkService.getJson('/share/list/' + dir);
}
public getSharingList(): Promise<SharingDTO[]> {
if (!Config.Sharing.enabled) {
return Promise.resolve([]);
}
return this.networkService.getJson('/share/listAll');
}
public deleteSharing(sharing: SharingDTO): Promise<void> {
return this.networkService.deleteJson('/share/' + sharing.sharingKey);
}
} }

View File

@ -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 { a.dropdown-item span, button.dropdown-item span, div.dropdown-item span {
padding-right: 0.8rem; padding-right: 0.8rem;
} }
a.list-shares-button {
cursor: pointer;
color: inherit;
}
a.list-shares-button:hover {
text-decoration: underline;
}

View File

@ -28,16 +28,25 @@
class="form-control input-md" class="form-control input-md"
type="text" type="text"
readonly readonly
[disabled]="!shareForm.form.valid" [disabled]="!shareForm.form.valid || !urlValid"
[ngModel]="shareForm.form.valid ? url: invalidSettings"> [ngModel]="shareForm.form.valid ? url: invalidSettings">
</div> </div>
<div class="col-5 col-sm-3"> <div class="col-5 col-sm-3">
<button id="copyButton" name="copyButton" <button
*ngIf="!sharing"
id="getShareButton" name="getShareButton"
(click)="share()"
[disabled]="!shareForm.form.valid"
class="btn btn-primary btn-block float-end" i18n>Share
</button>
<button
*ngIf="sharing"
id="copyButton" name="copyButton"
ngxClipboard ngxClipboard
[cbContent]="url" [cbContent]="url"
(cbOnSuccess)="onCopy()" (cbOnSuccess)="onCopy()"
[disabled]="!shareForm.form.valid" [disabled]="!shareForm.form.valid"
class="btn btn-primary btn-block" i18n>Copy class="btn btn-primary btn-block float-end" i18n>Copy
</button> </button>
</div> </div>
</div> </div>
@ -118,4 +127,38 @@
</div> </div>
</form> </form>
</div> </div>
<div class="modal-footer" *ngIf="activeShares && activeShares.length>0">
<a *ngIf="!showSharingList"
(click)="showSharingList = true"
class="list-shares-button m-0">
<span class="badge text-bg-secondary me-1">{{activeShares.length}}</span>
<ng-container i18n>active share(s) for this folder.
</ng-container>
<span class="oi oi-chevron-right ms-1"></span>
</a>
<table class="table table-hover table-sm" *ngIf="showSharingList">
<thead>
<tr>
<th i18n>Sharing</th>
<th *ngIf="IsAdmin" i18n>Creator</th>
<th i18n>Expires</th>
<th></th>
</tr>
</thead>
<tbody>
<tr *ngFor="let share of activeShares">
<td><a [href]="sharingService.getUrl(share)">{{share.sharingKey}}</a></td>
<td *ngIf="IsAdmin">{{share.creator.name}}</td>
<td>{{share.expires | date}}</td>
<td>
<button (click)="deleteSharing(share)"
[disabled]="share.sharingKey == sharing?.sharingKey"
class="btn btn-danger float-end">
<span class="oi oi-trash" aria-hidden="true" aria-label="Delete"></span>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</ng-template> </ng-template>

View File

@ -9,6 +9,9 @@ import {NotificationService} from '../../../model/notification.service';
import {BsModalService} from 'ngx-bootstrap/modal'; import {BsModalService} from 'ngx-bootstrap/modal';
import {BsModalRef} from 'ngx-bootstrap/modal/bs-modal-ref.service'; import {BsModalRef} from 'ngx-bootstrap/modal/bs-modal-ref.service';
import {Subscription} from 'rxjs'; import {Subscription} from 'rxjs';
import {UserRoles} from '../../../../../common/entities/UserDTO';
import {AuthenticationService} from '../../../model/network/authentication.service';
import {ClipboardService} from 'ngx-clipboard';
@Component({ @Component({
selector: 'app-gallery-share', selector: 'app-gallery-share',
@ -19,6 +22,8 @@ export class GalleryShareComponent implements OnInit, OnDestroy {
enabled = true; enabled = true;
@Input() dropDownItem = false; @Input() dropDownItem = false;
url = ''; url = '';
urlValid = false;
showSharingList = false;
input = { input = {
includeSubfolders: true, includeSubfolders: true,
@ -37,24 +42,33 @@ export class GalleryShareComponent implements OnInit, OnDestroy {
modalRef: BsModalRef; modalRef: BsModalRef;
invalidSettings = $localize`Invalid settings`; invalidSettings = $localize`Invalid settings`;
activeShares: SharingDTO[] = [];
text = { text = {
Yes: 'Yes', Yes: 'Yes',
No: 'No', No: 'No',
}; };
constructor( constructor(
private sharingService: ShareService, public sharingService: ShareService,
public galleryService: ContentService, public galleryService: ContentService,
private notification: NotificationService, private notification: NotificationService,
private modalService: BsModalService private modalService: BsModalService,
public authService: AuthenticationService,
private clipboardService: ClipboardService
) { ) {
this.text.Yes = $localize`Yes`; this.text.Yes = $localize`Yes`;
this.text.No = $localize`No`; this.text.No = $localize`No`;
} }
public get IsAdmin() {
return this.authService.user.value.role > UserRoles.Admin;
}
ngOnInit(): void { ngOnInit(): void {
this.contentSubscription = this.galleryService.content.subscribe( this.contentSubscription = this.galleryService.content.subscribe(
(content: ContentWrapper) => { async (content: ContentWrapper) => {
this.activeShares = [];
this.enabled = !!content.directory; this.enabled = !!content.directory;
if (!this.enabled) { if (!this.enabled) {
return; return;
@ -63,6 +77,7 @@ export class GalleryShareComponent implements OnInit, OnDestroy {
content.directory.path, content.directory.path,
content.directory.name content.directory.name
); );
await this.updateActiveSharesList();
} }
); );
} }
@ -73,6 +88,21 @@ export class GalleryShareComponent implements OnInit, OnDestroy {
} }
} }
async deleteSharing(sharing: SharingDTO): Promise<void> {
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 { calcValidity(): number {
switch (parseInt(this.input.valid.type.toString(), 10)) { switch (parseInt(this.input.valid.type.toString(), 10)) {
case ValidityTypes.Minutes: case ValidityTypes.Minutes:
@ -93,6 +123,7 @@ export class GalleryShareComponent implements OnInit, OnDestroy {
if (this.sharing == null) { if (this.sharing == null) {
return; return;
} }
this.urlValid = false;
this.url = $localize`loading..`; this.url = $localize`loading..`;
this.sharing = await this.sharingService.updateSharing( this.sharing = await this.sharingService.updateSharing(
this.currentDir, this.currentDir,
@ -101,31 +132,37 @@ export class GalleryShareComponent implements OnInit, OnDestroy {
this.input.password, this.input.password,
this.calcValidity() 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<void> { async get(): Promise<void> {
this.urlValid = false;
this.url = $localize`loading..`; this.url = $localize`loading..`;
this.sharing = await this.sharingService.createSharing( this.sharing = await this.sharingService.createSharing(
this.currentDir, this.currentDir,
this.input.includeSubfolders, this.input.includeSubfolders,
this.calcValidity() 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<unknown>): Promise<void> { async openModal(template: TemplateRef<unknown>): Promise<void> {
await this.get(); this.url = $localize`Click share to get a link.`;
this.urlValid = false;
this.sharing = null;
this.input.password = ''; this.input.password = '';
if (this.modalRef) { if (this.modalRef) {
this.modalRef.hide(); this.modalRef.hide();
} }
this.modalRef = this.modalService.show(template); this.modalRef = this.modalService.show(template);
document.body.style.paddingRight = '0px';
} }
onCopy(): void { 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 { public hideModal(): void {
@ -133,6 +170,16 @@ export class GalleryShareComponent implements OnInit, OnDestroy {
this.modalRef = null; this.modalRef = null;
this.sharing = null; this.sharing = null;
} }
async share() {
await this.get();
if (this.clipboardService.isSupported) {
this.clipboardService.copy(this.url);
this.onCopy();
}
}
} }

View File

@ -11,6 +11,9 @@ import {DefaultsJobs} from '../../../../common/entities/job/JobDTO';
import {StatisticDTO} from '../../../../common/entities/settings/StatisticDTO'; import {StatisticDTO} from '../../../../common/entities/settings/StatisticDTO';
import {ScheduledJobsService} from './scheduled-jobs.service'; import {ScheduledJobsService} from './scheduled-jobs.service';
import {IWebConfigClassPrivate} from '../../../../../node_modules/typeconfig/src/decorators/class/IWebConfigClass'; 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 { export enum ConfigStyle {

View File

@ -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<SharingDTO[]> {
if (!Config.Sharing.enabled) {
return Promise.resolve([]);
}
return this.networkService.getJson('/share/list');
}
public deleteSharing(sharing: SharingDTO): Promise<void> {
return this.networkService.deleteJson('/share/' + sharing.sharingKey);
}
}

View File

@ -20,7 +20,7 @@
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let share of shares"> <tr *ngFor="let share of shares">
<td><a [href]="sharingUrl + share.sharingKey">{{share.sharingKey}}</a></td> <td><a [href]="sharingService.getUrl(share)">{{share.sharingKey}}</a></td>
<td>{{share.path}}</td> <td>{{share.path}}</td>
<td>{{share.creator.name}}</td> <td>{{share.creator.name}}</td>
<td>{{share.expires | date}}</td> <td>{{share.expires | date}}</td>

View File

@ -1,9 +1,9 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {SharingDTO} from '../../../../../common/entities/SharingDTO'; import {SharingDTO} from '../../../../../common/entities/SharingDTO';
import {SharingListService} from './sharing-list.service';
import {SettingsService} from '../settings.service'; import {SettingsService} from '../settings.service';
import {Config} from '../../../../../common/config/public/Config'; import {ShareService} from '../../gallery/share.service';
import {Utils} from '../../../../../common/Utils'; import {AuthenticationService} from '../../../model/network/authentication.service';
import {UserRoles} from '../../../../../common/entities/UserDTO';
@Component({ @Component({
selector: 'app-settigns-sharings-list', selector: 'app-settigns-sharings-list',
@ -13,10 +13,10 @@ import {Utils} from '../../../../../common/Utils';
export class SharingsListComponent implements OnInit { export class SharingsListComponent implements OnInit {
public shares: SharingDTO[] = []; 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 +29,13 @@ export class SharingsListComponent implements OnInit {
} }
async deleteSharing(sharing: SharingDTO): Promise<void> { async deleteSharing(sharing: SharingDTO): Promise<void> {
await this.sharingList.deleteSharing(sharing); await this.sharingService.deleteSharing(sharing);
await this.getSharingList(); await this.getSharingList();
} }
private async getSharingList(): Promise<void> { private async getSharingList(): Promise<void> {
try { try {
this.shares = await this.sharingList.getSharingList(); this.shares = await this.sharingService.getSharingList();
} catch (err) { } catch (err) {
this.shares = []; this.shares = [];
throw err; throw err;

View File

@ -82,7 +82,7 @@ describe('SharingManager', (sqlHelper: DBTestHelper) => {
}; };
const saved = await sm.createSharing(sharing); 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.id).to.not.equals(null);
expect(found.sharingKey).to.equals(sharing.sharingKey); expect(found.sharingKey).to.equals(sharing.sharingKey);