1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2025-01-08 04:03:48 +02:00

Fix sharing password. fixes #744

This commit is contained in:
Patrik J. Braun 2023-12-01 19:33:39 +01:00
parent 16bf756582
commit ba9b5292e1
14 changed files with 336 additions and 245 deletions

View File

@ -1,5 +1,5 @@
import {NextFunction, Request, Response} from 'express'; import {NextFunction, Request, Response} from 'express';
import {CreateSharingDTO, SharingDTO} from '../../common/entities/SharingDTO'; import {CreateSharingDTO, SharingDTO, SharingDTOKey} from '../../common/entities/SharingDTO';
import {ObjectManagers} from '../model/ObjectManagers'; import {ObjectManagers} from '../model/ObjectManagers';
import {ErrorCodes, ErrorDTO} from '../../common/entities/Error'; import {ErrorCodes, ErrorDTO} from '../../common/entities/Error';
import {Config} from '../../common/config/private/Config'; import {Config} from '../../common/config/private/Config';
@ -9,9 +9,9 @@ import {UserRoles} from '../../common/entities/UserDTO';
export class SharingMWs { export class SharingMWs {
public static async getSharing( public static async getSharing(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
): Promise<void> { ): Promise<void> {
if (Config.Sharing.enabled === false) { if (Config.Sharing.enabled === false) {
return next(); return next();
@ -20,36 +20,69 @@ export class SharingMWs {
try { try {
req.resultPipe = req.resultPipe =
await ObjectManagers.getInstance().SharingManager.findOne(sharingKey); await ObjectManagers.getInstance().SharingManager.findOne(sharingKey);
return next(); return next();
} catch (err) { } catch (err) {
return next( return next(
new ErrorDTO( new ErrorDTO(
ErrorCodes.GENERAL_ERROR, ErrorCodes.GENERAL_ERROR,
'Error during retrieving sharing link', 'Error during retrieving sharing link',
err err
) )
);
}
}
public static async getSharingKey(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
if (Config.Sharing.enabled === false) {
return next();
}
const sharingKey = req.params[QueryParams.gallery.sharingKey_params];
try {
req.resultPipe =
{sharingKey: (await ObjectManagers.getInstance().SharingManager.findOne(sharingKey)).sharingKey} as SharingDTOKey;
return next();
} catch (err) {
return next(
new ErrorDTO(
ErrorCodes.GENERAL_ERROR,
'Error during retrieving sharing key',
err
)
); );
} }
} }
public static async createSharing( public static async createSharing(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
): Promise<void> { ): Promise<void> {
if (Config.Sharing.enabled === false) { if (Config.Sharing.enabled === false) {
return next(); return next();
} }
if ( if (
typeof req.body === 'undefined' || typeof req.body === 'undefined' ||
typeof req.body.createSharing === 'undefined' typeof req.body.createSharing === 'undefined'
) { ) {
return next( return next(
new ErrorDTO(ErrorCodes.INPUT_ERROR, 'createSharing filed is missing') new ErrorDTO(ErrorCodes.INPUT_ERROR, 'createSharing filed is missing')
); );
} }
const createSharing: CreateSharingDTO = req.body.createSharing; const createSharing: CreateSharingDTO = req.body.createSharing;
if (Config.Sharing.passwordRequired && !createSharing.password) {
return next(
new ErrorDTO(ErrorCodes.INPUT_ERROR, 'Password is required.')
);
}
let sharingKey = SharingMWs.generateKey(); let sharingKey = SharingMWs.generateKey();
// create one not yet used // create one not yet used
@ -71,45 +104,45 @@ export class SharingMWs {
password: createSharing.password, password: createSharing.password,
creator: req.session['user'], creator: req.session['user'],
expires: expires:
createSharing.valid >= 0 // if === -1 its forever createSharing.valid >= 0 // if === -1 its forever
? Date.now() + createSharing.valid ? Date.now() + createSharing.valid
: new Date(9999, 0, 1).getTime(), // never expire : new Date(9999, 0, 1).getTime(), // never expire
includeSubfolders: createSharing.includeSubfolders, includeSubfolders: createSharing.includeSubfolders,
timeStamp: Date.now(), timeStamp: Date.now(),
}; };
try { try {
req.resultPipe = req.resultPipe =
await ObjectManagers.getInstance().SharingManager.createSharing( await ObjectManagers.getInstance().SharingManager.createSharing(
sharing sharing
); );
return next(); return next();
} catch (err) { } catch (err) {
console.warn(err); console.warn(err);
return next( return next(
new ErrorDTO( new ErrorDTO(
ErrorCodes.GENERAL_ERROR, ErrorCodes.GENERAL_ERROR,
'Error during creating sharing link', 'Error during creating sharing link',
err err
) )
); );
} }
} }
public static async updateSharing( public static async updateSharing(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
): Promise<void> { ): Promise<void> {
if (Config.Sharing.enabled === false) { if (Config.Sharing.enabled === false) {
return next(); return next();
} }
if ( if (
typeof req.body === 'undefined' || typeof req.body === 'undefined' ||
typeof req.body.updateSharing === 'undefined' typeof req.body.updateSharing === 'undefined'
) { ) {
return next( return next(
new ErrorDTO(ErrorCodes.INPUT_ERROR, 'updateSharing filed is missing') new ErrorDTO(ErrorCodes.INPUT_ERROR, 'updateSharing filed is missing')
); );
} }
const updateSharing: CreateSharingDTO = req.body.updateSharing; const updateSharing: CreateSharingDTO = req.body.updateSharing;
@ -119,14 +152,14 @@ export class SharingMWs {
path: directoryName, path: directoryName,
sharingKey: '', sharingKey: '',
password: password:
updateSharing.password && updateSharing.password !== '' updateSharing.password && updateSharing.password !== ''
? updateSharing.password ? updateSharing.password
: null, : null,
creator: req.session['user'], creator: req.session['user'],
expires: expires:
updateSharing.valid >= 0 // if === -1 its forever updateSharing.valid >= 0 // if === -1 its forever
? Date.now() + updateSharing.valid ? Date.now() + updateSharing.valid
: new Date(9999, 0, 1).getTime(), // never expire : new Date(9999, 0, 1).getTime(), // never expire
includeSubfolders: updateSharing.includeSubfolders, includeSubfolders: updateSharing.includeSubfolders,
timeStamp: Date.now(), timeStamp: Date.now(),
}; };
@ -134,36 +167,37 @@ export class SharingMWs {
try { try {
const forceUpdate = req.session['user'].role >= UserRoles.Admin; const forceUpdate = req.session['user'].role >= UserRoles.Admin;
req.resultPipe = req.resultPipe =
await ObjectManagers.getInstance().SharingManager.updateSharing( await ObjectManagers.getInstance().SharingManager.updateSharing(
sharing, sharing,
forceUpdate forceUpdate
); );
console.log(req.resultPipe);
return next(); return next();
} catch (err) { } catch (err) {
return next( return next(
new ErrorDTO( new ErrorDTO(
ErrorCodes.GENERAL_ERROR, ErrorCodes.GENERAL_ERROR,
'Error during updating sharing link', 'Error during updating sharing link',
err err
) )
); );
} }
} }
public static async deleteSharing( public static async deleteSharing(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
): Promise<void> { ): Promise<void> {
if (Config.Sharing.enabled === false) { if (Config.Sharing.enabled === false) {
return next(); return next();
} }
if ( if (
typeof req.params === 'undefined' || typeof req.params === 'undefined' ||
typeof req.params['sharingKey'] === 'undefined' typeof req.params['sharingKey'] === 'undefined'
) { ) {
return next( return next(
new ErrorDTO(ErrorCodes.INPUT_ERROR, 'sharingKey is missing') new ErrorDTO(ErrorCodes.INPUT_ERROR, 'sharingKey is missing')
); );
} }
const sharingKey: string = req.params['sharingKey']; const sharingKey: string = req.params['sharingKey'];
@ -177,49 +211,49 @@ export class SharingMWs {
} }
} }
req.resultPipe = req.resultPipe =
await ObjectManagers.getInstance().SharingManager.deleteSharing( await ObjectManagers.getInstance().SharingManager.deleteSharing(
sharingKey sharingKey
); );
req.resultPipe = 'ok'; req.resultPipe = 'ok';
return next(); return next();
} catch (err) { } catch (err) {
return next( return next(
new ErrorDTO( new ErrorDTO(
ErrorCodes.GENERAL_ERROR, ErrorCodes.GENERAL_ERROR,
'Error during deleting sharing', 'Error during deleting sharing',
err err
) )
); );
} }
} }
public static async listSharing( public static async listSharing(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
): Promise<void> { ): Promise<void> {
if (Config.Sharing.enabled === false) { if (Config.Sharing.enabled === false) {
return next(); return next();
} }
try { try {
req.resultPipe = req.resultPipe =
await ObjectManagers.getInstance().SharingManager.listAll(); await ObjectManagers.getInstance().SharingManager.listAll();
return next(); return next();
} catch (err) { } catch (err) {
return next( return next(
new ErrorDTO( new ErrorDTO(
ErrorCodes.GENERAL_ERROR, ErrorCodes.GENERAL_ERROR,
'Error during listing shares', 'Error during listing shares',
err err
) )
); );
} }
} }
public static async listSharingForDir( public static async listSharingForDir(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
): Promise<void> { ): Promise<void> {
if (Config.Sharing.enabled === false) { if (Config.Sharing.enabled === false) {
return next(); return next();
@ -229,19 +263,19 @@ export class SharingMWs {
try { try {
if (req.session['user'].role >= UserRoles.Admin) { if (req.session['user'].role >= UserRoles.Admin) {
req.resultPipe = req.resultPipe =
await ObjectManagers.getInstance().SharingManager.listAllForDir(dir); await ObjectManagers.getInstance().SharingManager.listAllForDir(dir);
} else { } else {
req.resultPipe = req.resultPipe =
await ObjectManagers.getInstance().SharingManager.listAllForDir(dir, req.session['user']); await ObjectManagers.getInstance().SharingManager.listAllForDir(dir, req.session['user']);
} }
return next(); return next();
} catch (err) { } catch (err) {
return next( return next(
new ErrorDTO( new ErrorDTO(
ErrorCodes.GENERAL_ERROR, ErrorCodes.GENERAL_ERROR,
'Error during listing shares', 'Error during listing shares',
err err
) )
); );
} }
} }
@ -249,8 +283,8 @@ export class SharingMWs {
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)
.toString(16) .toString(16)
.substring(1); .substring(1);
} }
return s4() + s4(); return s4() + s4();

View File

@ -156,8 +156,8 @@ export class AuthenticationMWs {
if ( if (
!sharing || !sharing ||
sharing.expires < Date.now() || sharing.expires < Date.now() ||
(Config.Sharing.passwordProtected === true && ((Config.Sharing.passwordRequired === true ||
sharing.password && sharing.password) &&
!PasswordHelper.comparePassword(password, sharing.password)) !PasswordHelper.comparePassword(password, sharing.password))
) { ) {
Logger.warn(LOG_TAG, 'Failed login with sharing:' + sharing.sharingKey + ', bad password'); Logger.warn(LOG_TAG, 'Failed login with sharing:' + sharing.sharingKey + ', bad password');
@ -265,8 +265,9 @@ export class AuthenticationMWs {
return null; return null;
} }
// no 'free login' if passwords are required, or it is set
if ( if (
Config.Sharing.passwordProtected === true && Config.Sharing.passwordRequired === true ||
sharing.password sharing.password
) { ) {
return null; return null;

View File

@ -11,6 +11,7 @@ export class SharingRouter {
public static route(app: express.Express): void { public static route(app: express.Express): void {
this.addShareLogin(app); this.addShareLogin(app);
this.addGetSharing(app); this.addGetSharing(app);
this.addGetSharingKey(app);
this.addCreateSharing(app); this.addCreateSharing(app);
this.addUpdateSharing(app); this.addUpdateSharing(app);
this.addListSharing(app); this.addListSharing(app);
@ -20,79 +21,94 @@ export class SharingRouter {
private static addShareLogin(app: express.Express): void { private static addShareLogin(app: express.Express): void {
app.post( app.post(
Config.Server.apiPath + '/share/login', Config.Server.apiPath + '/share/login',
AuthenticationMWs.inverseAuthenticate, AuthenticationMWs.inverseAuthenticate,
AuthenticationMWs.shareLogin, AuthenticationMWs.shareLogin,
ServerTimingMWs.addServerTiming, ServerTimingMWs.addServerTiming,
RenderingMWs.renderSessionUser RenderingMWs.renderSessionUser
);
}
/**
* Used to check the key validity
* @param app
* @private
*/
private static addGetSharingKey(app: express.Express): void {
app.get(
Config.Server.apiPath + '/share/:' + QueryParams.gallery.sharingKey_params + '/key',
// its a public path
SharingMWs.getSharingKey,
ServerTimingMWs.addServerTiming,
RenderingMWs.renderSharing
); );
} }
private static addGetSharing(app: express.Express): void { private static addGetSharing(app: express.Express): void {
app.get( app.get(
Config.Server.apiPath + '/share/:' + QueryParams.gallery.sharingKey_params, Config.Server.apiPath + '/share/:' + QueryParams.gallery.sharingKey_params,
AuthenticationMWs.authenticate, AuthenticationMWs.authenticate,
AuthenticationMWs.authorise(UserRoles.LimitedGuest), AuthenticationMWs.authorise(UserRoles.LimitedGuest),
SharingMWs.getSharing, SharingMWs.getSharing,
ServerTimingMWs.addServerTiming, ServerTimingMWs.addServerTiming,
RenderingMWs.renderSharing RenderingMWs.renderSharing
); );
} }
private static addCreateSharing(app: express.Express): void { private static addCreateSharing(app: express.Express): void {
app.post( app.post(
[Config.Server.apiPath + '/share/:directory(*)', Config.Server.apiPath + '/share/', Config.Server.apiPath + '/share//'], [Config.Server.apiPath + '/share/:directory(*)', Config.Server.apiPath + '/share/', Config.Server.apiPath + '/share//'],
AuthenticationMWs.authenticate, AuthenticationMWs.authenticate,
AuthenticationMWs.authorise(UserRoles.User), AuthenticationMWs.authorise(UserRoles.User),
SharingMWs.createSharing, SharingMWs.createSharing,
ServerTimingMWs.addServerTiming, ServerTimingMWs.addServerTiming,
RenderingMWs.renderSharing RenderingMWs.renderSharing
); );
} }
private static addUpdateSharing(app: express.Express): void { private static addUpdateSharing(app: express.Express): void {
app.put( app.put(
[Config.Server.apiPath + '/share/:directory(*)', Config.Server.apiPath + '/share/', Config.Server.apiPath + '/share//'], [Config.Server.apiPath + '/share/:directory(*)', Config.Server.apiPath + '/share/', Config.Server.apiPath + '/share//'],
AuthenticationMWs.authenticate, AuthenticationMWs.authenticate,
AuthenticationMWs.authorise(UserRoles.User), AuthenticationMWs.authorise(UserRoles.User),
SharingMWs.updateSharing, SharingMWs.updateSharing,
ServerTimingMWs.addServerTiming, ServerTimingMWs.addServerTiming,
RenderingMWs.renderSharing RenderingMWs.renderSharing
); );
} }
private static addDeleteSharing(app: express.Express): void { private static addDeleteSharing(app: express.Express): void {
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.User), AuthenticationMWs.authorise(UserRoles.User),
SharingMWs.deleteSharing, SharingMWs.deleteSharing,
ServerTimingMWs.addServerTiming, ServerTimingMWs.addServerTiming,
RenderingMWs.renderResult RenderingMWs.renderResult
); );
} }
private static addListSharing(app: express.Express): void { private static addListSharing(app: express.Express): void {
app.get( app.get(
[Config.Server.apiPath + '/share/listAll'], [Config.Server.apiPath + '/share/listAll'],
AuthenticationMWs.authenticate, AuthenticationMWs.authenticate,
AuthenticationMWs.authorise(UserRoles.Admin), AuthenticationMWs.authorise(UserRoles.Admin),
SharingMWs.listSharing, SharingMWs.listSharing,
ServerTimingMWs.addServerTiming, ServerTimingMWs.addServerTiming,
RenderingMWs.renderSharingList RenderingMWs.renderSharingList
); );
} }
private static addListSharingForDir(app: express.Express): void { private static addListSharingForDir(app: express.Express): void {
app.get( app.get(
[Config.Server.apiPath + '/share/list/:directory(*)', [Config.Server.apiPath + '/share/list/:directory(*)',
Config.Server.apiPath + '/share/list//', Config.Server.apiPath + '/share/list//',
Config.Server.apiPath + '/share/list'], Config.Server.apiPath + '/share/list'],
AuthenticationMWs.authenticate, AuthenticationMWs.authenticate,
AuthenticationMWs.authorise(UserRoles.User), AuthenticationMWs.authorise(UserRoles.User),
SharingMWs.listSharingForDir, SharingMWs.listSharingForDir,
ServerTimingMWs.addServerTiming, ServerTimingMWs.addServerTiming,
RenderingMWs.renderSharingList RenderingMWs.renderSharingList
); );
} }
} }

View File

@ -273,12 +273,12 @@ export class ClientSharingConfig {
@ConfigProperty({ @ConfigProperty({
tags: tags:
{ {
name: $localize`Password protected`, name: $localize`Require password`,
priority: ConfigPriority.advanced priority: ConfigPriority.advanced
}, },
description: $localize`Enables password protected sharing links.`, description: $localize`Requires password protected sharing links.`,
}) })
passwordProtected: boolean = true; passwordRequired: boolean = false;
} }
@SubConfigClass({tags: {client: true}, softReadonly: true}) @SubConfigClass({tags: {client: true}, softReadonly: true})

View File

@ -1,6 +1,10 @@
import {UserDTO} from './UserDTO'; import {UserDTO} from './UserDTO';
export interface SharingDTO { export interface SharingDTOKey {
sharingKey: string;
}
export interface SharingDTO extends SharingDTOKey {
id: number; id: number;
path: string; path: string;
sharingKey: string; sharingKey: string;

View File

@ -65,16 +65,16 @@
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<div class="btn-group" dropdown #dropdown="bs-dropdown" placement="bottom" <div class="btn-group" dropdown #dropdown="bs-dropdown" placement="bottom"
[autoClose]="false" container="body"> [autoClose]="false" container="body">
<button id="button-alignment" dropdownToggle <button id="button-frame-menu" dropdownToggle
type="button" class="btn btn-tertiary dropdown-toggle" type="button" class="btn btn-tertiary dropdown-toggle"
aria-controls="dropdown-alignment"> aria-controls="dropdown-frame-menu">
<ng-icon class="align-text-top" size="1.2em" name="ionMenuOutline"></ng-icon> <ng-icon class="align-text-top" size="1.2em" name="ionMenuOutline"></ng-icon>
<span *ngIf="isAdmin() && notificationService.numberOfNotifications>0" <span *ngIf="isAdmin() && notificationService.numberOfNotifications>0"
class="navbar-badge badge text-bg-warning">{{notificationService.numberOfNotifications}}</span> class="navbar-badge badge text-bg-warning">{{notificationService.numberOfNotifications}}</span>
</button> </button>
<ul id="dropdown-alignment" *dropdownMenu <ul id="dropdown-frame-menu" *dropdownMenu
class="dropdown-menu dropdown-menu-right" class="dropdown-menu dropdown-menu-right"
role="menu" aria-labelledby="button-alignment"> role="menu" aria-labelledby="button-frame-menu">
<li role="menuitem" class="d-xl-none"> <li role="menuitem" class="d-xl-none">
<div style="white-space: nowrap;" class="dropdown-item"> <div style="white-space: nowrap;" class="dropdown-item">

View File

@ -52,18 +52,18 @@ export class GalleryComponent implements OnInit, OnDestroy {
}; };
constructor( constructor(
public contentLoader: ContentLoaderService, public contentLoader: ContentLoaderService,
public galleryService: ContentService, public galleryService: ContentService,
private authService: AuthenticationService, private authService: AuthenticationService,
private router: Router, private router: Router,
private shareService: ShareService, private shareService: ShareService,
private route: ActivatedRoute, private route: ActivatedRoute,
private navigation: NavigationService, private navigation: NavigationService,
private filterService: FilterService, private filterService: FilterService,
private sortingService: GallerySortingService, private sortingService: GallerySortingService,
private piTitleService: PiTitleService, private piTitleService: PiTitleService,
private gpxFilesFilterPipe: GPXFilesFilterPipe, private gpxFilesFilterPipe: GPXFilesFilterPipe,
private mdFilesFilterPipe: MDFilesFilterPipe, private mdFilesFilterPipe: MDFilesFilterPipe,
) { ) {
this.mapEnabled = Config.Map.enabled; this.mapEnabled = Config.Map.enabled;
PageHelper.showScrollY(); PageHelper.showScrollY();
@ -79,17 +79,17 @@ export class GalleryComponent implements OnInit, OnDestroy {
} }
// if the timer is longer than 10 years, just do not show it // if the timer is longer than 10 years, just do not show it
if ( if (
(this.shareService.sharingSubject.value.expires - Date.now()) / (this.shareService.sharingSubject.value.expires - Date.now()) /
1000 / 1000 /
86400 / 86400 /
365 > 365 >
10 10
) { ) {
return; return;
} }
t = Math.floor( t = Math.floor(
(this.shareService.sharingSubject.value.expires - Date.now()) / 1000 (this.shareService.sharingSubject.value.expires - Date.now()) / 1000
); );
this.countDown = {} as any; this.countDown = {} as any;
this.countDown.day = Math.floor(t / 86400); this.countDown.day = Math.floor(t / 86400);
@ -118,31 +118,26 @@ export class GalleryComponent implements OnInit, OnDestroy {
async ngOnInit(): Promise<boolean> { async ngOnInit(): Promise<boolean> {
await this.shareService.wait(); await this.shareService.wait();
if ( if (!this.authService.isAuthenticated()) {
!this.authService.isAuthenticated() &&
(!this.shareService.isSharing() ||
(this.shareService.isSharing() &&
Config.Sharing.passwordProtected === true))
) {
return this.navigation.toLogin(); return this.navigation.toLogin();
} }
this.showSearchBar = this.authService.canSearch(); this.showSearchBar = this.authService.canSearch();
this.showShare = this.showShare =
Config.Sharing.enabled && Config.Sharing.enabled &&
this.authService.isAuthorized(UserRoles.User); this.authService.isAuthorized(UserRoles.User);
this.showRandomPhotoBuilder = this.showRandomPhotoBuilder =
Config.RandomPhoto.enabled && Config.RandomPhoto.enabled &&
this.authService.isAuthorized(UserRoles.User); this.authService.isAuthorized(UserRoles.User);
this.subscription.content = this.galleryService.sortedFilteredContent this.subscription.content = this.galleryService.sortedFilteredContent
.subscribe((dc: GroupedDirectoryContent) => { .subscribe((dc: GroupedDirectoryContent) => {
this.onContentChange(dc); this.onContentChange(dc);
}); });
this.subscription.route = this.route.params.subscribe(this.onRoute); this.subscription.route = this.route.params.subscribe(this.onRoute);
if (this.shareService.isSharing()) { if (this.shareService.isSharing()) {
this.$counter = interval(1000); this.$counter = interval(1000);
this.subscription.timer = this.$counter.subscribe((x): void => this.subscription.timer = this.$counter.subscribe((x): void =>
this.updateTimer(x) this.updateTimer(x)
); );
} }
} }
@ -156,18 +151,18 @@ export class GalleryComponent implements OnInit, OnDestroy {
} }
if ( if (
params[QueryParams.gallery.sharingKey_params] && params[QueryParams.gallery.sharingKey_params] &&
params[QueryParams.gallery.sharingKey_params] !== '' params[QueryParams.gallery.sharingKey_params] !== ''
) { ) {
const sharing = await this.shareService.currentSharing const sharing = await this.shareService.currentSharing
.pipe(take(1)) .pipe(take(1))
.toPromise(); .toPromise();
const qParams: { [key: string]: any } = {}; const qParams: { [key: string]: any } = {};
qParams[QueryParams.gallery.sharingKey_query] = qParams[QueryParams.gallery.sharingKey_query] =
this.shareService.getSharingKey(); this.shareService.getSharingKey();
this.router this.router
.navigate(['/gallery', sharing.path], {queryParams: qParams}) .navigate(['/gallery', sharing.path], {queryParams: qParams})
.catch(console.error); .catch(console.error);
return; return;
} }
@ -191,8 +186,8 @@ export class GalleryComponent implements OnInit, OnDestroy {
for (const mediaGroup of content.mediaGroups) { for (const mediaGroup of content.mediaGroups) {
if ( if (
mediaGroup.media mediaGroup.media
.findIndex((m: PhotoDTO) => !!m.metadata?.positionData?.GPSData?.longitude) !== -1 .findIndex((m: PhotoDTO) => !!m.metadata?.positionData?.GPSData?.longitude) !== -1
) { ) {
this.isPhotoWithLocation = true; this.isPhotoWithLocation = true;
break; break;

View File

@ -1,11 +1,11 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {NetworkService} from '../../model/network/network.service'; import {NetworkService} from '../../model/network/network.service';
import {CreateSharingDTO, SharingDTO,} from '../../../../common/entities/SharingDTO'; import {CreateSharingDTO, SharingDTO, SharingDTOKey,} from '../../../../common/entities/SharingDTO';
import {Router, RoutesRecognized} from '@angular/router'; import {Router, RoutesRecognized} from '@angular/router';
import {BehaviorSubject} from 'rxjs'; 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, UserRoles} from '../../../../common/entities/UserDTO';
import {Utils} from '../../../../common/Utils'; import {Utils} from '../../../../common/Utils';
import {Config} from '../../../../common/config/public/Config'; import {Config} from '../../../../common/config/public/Config';
@ -21,12 +21,15 @@ export class ShareService {
inited = false; inited = false;
public ReadyPR: Promise<void>; public ReadyPR: Promise<void>;
public sharingSubject: BehaviorSubject<SharingDTO> = new BehaviorSubject( public sharingSubject: BehaviorSubject<SharingDTO> = new BehaviorSubject(
null null
);
public sharingIsValid: BehaviorSubject<boolean> = new BehaviorSubject(
null
); );
public currentSharing = this.sharingSubject public currentSharing = this.sharingSubject
.asObservable() .asObservable()
.pipe(filter((s) => s !== null)) .pipe(filter((s) => s !== null))
.pipe(distinctUntilChanged()); .pipe(distinctUntilChanged());
private resolve: () => void; private resolve: () => void;
@ -41,18 +44,18 @@ export class ShareService {
this.router.events.subscribe(async (val) => { this.router.events.subscribe(async (val) => {
if (val instanceof RoutesRecognized) { if (val instanceof RoutesRecognized) {
this.param = this.param =
val.state.root.firstChild.params[ val.state.root.firstChild.params[
QueryParams.gallery.sharingKey_params QueryParams.gallery.sharingKey_params
] || null; ] || null;
this.queryParam = this.queryParam =
val.state.root.firstChild.queryParams[ val.state.root.firstChild.queryParams[
QueryParams.gallery.sharingKey_query QueryParams.gallery.sharingKey_query
] || null; ] || null;
const changed = this.sharingKey !== (this.param || this.queryParam); const changed = this.sharingKey !== (this.param || this.queryParam);
if (changed) { if (changed) {
this.sharingKey = this.param || this.queryParam || this.sharingKey; this.sharingKey = this.param || this.queryParam || this.sharingKey;
await this.getSharing(); await this.checkSharing();
} }
if (this.resolve) { if (this.resolve) {
this.resolve(); this.resolve();
@ -69,12 +72,17 @@ export class ShareService {
onNewUser = async (user: UserDTO) => { onNewUser = async (user: UserDTO) => {
if (user && !!user.usedSharingKey) { // if this is a sharing user or a logged-in user, get sharing key
if (user?.usedSharingKey || user?.role > UserRoles.LimitedGuest) {
if ( if (
user.usedSharingKey !== this.sharingKey || (user?.usedSharingKey &&
this.sharingSubject.value == null user?.usedSharingKey !== this.sharingKey) ||
this.sharingSubject.value == null
) { ) {
this.sharingKey = user.usedSharingKey; this.sharingKey = user.usedSharingKey || this.getSharingKey();
if(!this.sharingKey){ //no key to fetch
return
}
await this.getSharing(); await this.getSharing();
} }
if (this.resolve) { if (this.resolve) {
@ -93,24 +101,26 @@ export class ShareService {
} }
public createSharing( public createSharing(
dir: string, dir: string,
includeSubFolders: boolean, includeSubFolders: boolean,
valid: number password: string,
valid: number
): Promise<SharingDTO> { ): Promise<SharingDTO> {
return this.networkService.postJson('/share/' + dir, { return this.networkService.postJson('/share/' + dir, {
createSharing: { createSharing: {
includeSubfolders: includeSubFolders, includeSubfolders: includeSubFolders,
valid, valid,
...(!!password && {password: password}) // only add password if present
} as CreateSharingDTO, } as CreateSharingDTO,
}); });
} }
public updateSharing( public updateSharing(
dir: string, dir: string,
sharingId: number, sharingId: number,
includeSubFolders: boolean, includeSubFolders: boolean,
password: string, password: string,
valid: number valid: number
): Promise<SharingDTO> { ): Promise<SharingDTO> {
return this.networkService.putJson('/share/' + dir, { return this.networkService.putJson('/share/' + dir, {
updateSharing: { updateSharing: {
@ -134,7 +144,7 @@ export class ShareService {
try { try {
this.sharingSubject.next(null); this.sharingSubject.next(null);
const sharing = await this.networkService.getJson<SharingDTO>( const sharing = await this.networkService.getJson<SharingDTO>(
'/share/' + this.getSharingKey() '/share/' + this.getSharingKey()
); );
this.sharingSubject.next(sharing); this.sharingSubject.next(sharing);
} catch (e) { } catch (e) {
@ -143,8 +153,21 @@ export class ShareService {
} }
} }
private async checkSharing(): Promise<void> {
try {
this.sharingIsValid.next(null);
const sharing = await this.networkService.getJson<SharingDTOKey>(
'/share/' + this.getSharingKey() + '/key'
);
this.sharingIsValid.next(sharing.sharingKey === this.getSharingKey());
} catch (e) {
this.sharingIsValid.next(false);
console.error(e);
}
}
public async getSharingListForDir( public async getSharingListForDir(
dir: string dir: string
): Promise<SharingDTO[]> { ): Promise<SharingDTO[]> {
return this.networkService.getJson('/share/list/' + dir); return this.networkService.getJson('/share/list/' + dir);
} }

View File

@ -82,11 +82,11 @@
</div> </div>
</div> </div>
<div class="row" *ngIf="passwordProtection"> <div class="row">
<div class="col-4"> <div class="col-4">
<label class="control-label" for="share-password"> <label class="control-label" for="share-password">
<ng-container i18n>Password</ng-container><!-- <ng-container i18n>Password</ng-container>
-->*: <ng-container *ngIf="passwordRequired">*</ng-container>
</label> </label>
</div> </div>
<div class="col-8"> <div class="col-8">
@ -98,7 +98,7 @@
[(ngModel)]="input.password" [(ngModel)]="input.password"
i18n-placeholder i18n-placeholder
placeholder="Password" placeholder="Password"
required> [required]="passwordRequired">
</div> </div>
</div> </div>

View File

@ -36,7 +36,7 @@ export class GalleryShareComponent implements OnInit, OnDestroy {
currentDir = ''; currentDir = '';
sharing: SharingDTO = null; sharing: SharingDTO = null;
contentSubscription: Subscription = null; contentSubscription: Subscription = null;
readonly passwordProtection = Config.Sharing.passwordProtected; readonly passwordRequired = Config.Sharing.passwordRequired;
readonly ValidityTypes = ValidityTypes; readonly ValidityTypes = ValidityTypes;
modalRef: BsModalRef; modalRef: BsModalRef;
@ -138,11 +138,16 @@ export class GalleryShareComponent implements OnInit, OnDestroy {
} }
async get(): Promise<void> { async get(): Promise<void> {
if(Config.Sharing.passwordRequired && !this.input.password){
this.url = $localize`Set password.`;
return;
}
this.urlValid = false; 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.input.password,
this.calcValidity() this.calcValidity()
); );
this.url = this.sharingService.getUrl(this.sharing); this.url = this.sharingService.getUrl(this.sharing);

View File

@ -7,10 +7,10 @@
</div> </div>
<div class="row card align-self-center"> <div class="row card align-self-center">
<div class="card-body"> <div class="card-body">
<div *ngIf="(shareService.currentSharing | async) == shareService.UnknownSharingKey" <div *ngIf="!(shareService.sharingIsValid | async)"
class="h3 text-center text-danger" i18n>Unknown sharing key. class="h3 text-center text-danger" i18n>Unknown sharing key.
</div> </div>
<form *ngIf="(shareService.currentSharing | async) != shareService.UnknownSharingKey" <form *ngIf="(shareService.sharingIsValid | async)"
name="form" id="form" class="form-horizontal" #LoginForm="ngForm" (submit)="onLogin()"> name="form" id="form" class="form-horizontal" #LoginForm="ngForm" (submit)="onLogin()">
<div class="error-message" [hidden]="loginError==false" i18n>Wrong password</div> <div class="error-message" [hidden]="loginError==false" i18n>Wrong password</div>
@ -32,6 +32,7 @@
<div class="col-sm-12 controls d-grid gap-2"> <div class="col-sm-12 controls d-grid gap-2">
<button class="btn btn-primary btn-lg" <button class="btn btn-primary btn-lg"
id="button-share-login"
[disabled]="!LoginForm.form.valid || inProgress" [disabled]="!LoginForm.form.valid || inProgress"
type="submit" type="submit"
name="action" i18n>Enter name="action" i18n>Enter

View File

@ -75,8 +75,15 @@ describe('PublicRouter', () => {
.get('/share/' + sharingKey); .get('/share/' + sharingKey);
}; };
it('should not get default user with passworded share share without password', async () => { it('should not get default user with passworded share without required password', async () => {
Config.Sharing.passwordProtected = true; Config.Sharing.passwordRequired = false;
const sharing = await RouteTestingHelper.createSharing(testUser, 'secret_pass');
const res = await fistLoad(server, sharing.sharingKey);
shouldHaveInjectedUser(res, null);
});
it('should not get default user with passworded share share with required password', async () => {
Config.Sharing.passwordRequired = true;
const sharing = await RouteTestingHelper.createSharing(testUser, 'secret_pass'); const sharing = await RouteTestingHelper.createSharing(testUser, 'secret_pass');
const res = await fistLoad(server, sharing.sharingKey); const res = await fistLoad(server, sharing.sharingKey);
shouldHaveInjectedUser(res, null); shouldHaveInjectedUser(res, null);
@ -84,25 +91,13 @@ describe('PublicRouter', () => {
it('should get default user with no-password share', async () => { it('should get default user with no-password share', async () => {
Config.Sharing.passwordProtected = true; Config.Sharing.passwordRequired = false;
const sharing = await RouteTestingHelper.createSharing(testUser); const sharing = await RouteTestingHelper.createSharing(testUser);
const res = await fistLoad(server, sharing.sharingKey); const res = await fistLoad(server, sharing.sharingKey);
shouldHaveInjectedUser(res, RouteTestingHelper.getExpectedSharingUser(sharing)); shouldHaveInjectedUser(res, RouteTestingHelper.getExpectedSharingUser(sharing));
}); });
it('should get default user for no-password share when password protection disabled', async () => {
Config.Sharing.passwordProtected = false;
const sharing = await RouteTestingHelper.createSharing(testUser);
const res = await fistLoad(server, sharing.sharingKey);
shouldHaveInjectedUser(res, RouteTestingHelper.getExpectedSharingUser(sharing));
});
it('should get default user for passworded share when password protection disabled', async () => {
Config.Sharing.passwordProtected = false;
const sharing = await RouteTestingHelper.createSharing(testUser, 'secret_pass');
const res = await fistLoad(server, sharing.sharingKey);
shouldHaveInjectedUser(res, RouteTestingHelper.getExpectedSharingUser(sharing));
});
}); });

View File

@ -74,15 +74,32 @@ describe('SharingRouter', () => {
beforeEach(setUp); beforeEach(setUp);
afterEach(tearDown); afterEach(tearDown);
it('should login with passworded share', async () => { it('should login with passworded share when password required', async () => {
Config.Sharing.passwordProtected = true; Config.Sharing.passwordRequired = true;
const sharing = await RouteTestingHelper.createSharing(testUser, 'secret_pass'); const sharing = await RouteTestingHelper.createSharing(testUser, 'secret_pass');
const res = await shareLogin(server, sharing.sharingKey, sharing.password); const res = await shareLogin(server, sharing.sharingKey, sharing.password);
shouldBeValidUser(res, RouteTestingHelper.getExpectedSharingUser(sharing)); shouldBeValidUser(res, RouteTestingHelper.getExpectedSharingUser(sharing));
}); });
it('should login with passworded share when password not required', async () => {
Config.Sharing.passwordRequired = false;
const sharing = await RouteTestingHelper.createSharing(testUser, 'secret_pass');
const res = await shareLogin(server, sharing.sharingKey, sharing.password);
shouldBeValidUser(res, RouteTestingHelper.getExpectedSharingUser(sharing));
});
it('should login without passworded share when password not required', async () => {
Config.Sharing.passwordRequired = false;
const sharing = await RouteTestingHelper.createSharing(testUser );
const res = await shareLogin(server, sharing.sharingKey, sharing.password);
shouldBeValidUser(res, RouteTestingHelper.getExpectedSharingUser(sharing));
});
it('should not login with passworded share without password', async () => { it('should not login with passworded share without password', async () => {
Config.Sharing.passwordProtected = true; Config.Sharing.passwordRequired = true;
const sharing = await RouteTestingHelper.createSharing(testUser, 'secret_pass'); const sharing = await RouteTestingHelper.createSharing(testUser, 'secret_pass');
const result = await shareLogin(server, sharing.sharingKey); const result = await shareLogin(server, sharing.sharingKey);
@ -92,22 +109,22 @@ describe('SharingRouter', () => {
should.equal(result.body.error.code, ErrorCodes.CREDENTIAL_NOT_FOUND); should.equal(result.body.error.code, ErrorCodes.CREDENTIAL_NOT_FOUND);
}); });
it('should not login with passworded share but password protection disabled', async () => {
Config.Sharing.passwordProtected = false;
const sharing = await RouteTestingHelper.createSharing(testUser, 'secret_pass');
const res = await shareLogin(server, sharing.sharingKey);
shouldBeValidUser(res, RouteTestingHelper.getExpectedSharingUser(sharing)); it('should not login to share without password when password required', async () => {
}); Config.Sharing.passwordRequired = false;
it('should login with no-password share', async () => {
Config.Sharing.passwordProtected = true;
const sharing = await RouteTestingHelper.createSharing(testUser); const sharing = await RouteTestingHelper.createSharing(testUser);
const res = await shareLogin(server, sharing.sharingKey, sharing.password); Config.Sharing.passwordRequired = true;
shouldBeValidUser(res, RouteTestingHelper.getExpectedSharingUser(sharing)); const result = await shareLogin(server, sharing.sharingKey);
result.should.have.status(401);
result.body.should.be.a('object');
result.body.error.should.be.a('object');
should.equal(result.body.error.code, ErrorCodes.CREDENTIAL_NOT_FOUND);
}); });
}); });

View File

@ -125,9 +125,9 @@ describe('UserRouter', () => {
it('it should authenticate as user with sharing key', async () => { it('it should authenticate as user with sharing key', async () => {
Config.Users.authenticationRequired = true; Config.Users.authenticationRequired = true;
Config.Sharing.enabled = true; Config.Sharing.enabled = true;
Config.Sharing.passwordProtected = true; Config.Sharing.passwordRequired = true;
const sharingKey = (await RouteTestingHelper.createSharing(testUser)).sharingKey; const sharingKey = (await RouteTestingHelper.createSharing(testUser, 'pass')).sharingKey;
const loginRes = await login(server); const loginRes = await login(server);
@ -146,7 +146,7 @@ describe('UserRouter', () => {
it('it should authenticate with sharing key', async () => { it('it should authenticate with sharing key', async () => {
Config.Users.authenticationRequired = true; Config.Users.authenticationRequired = true;
Config.Sharing.enabled = true; Config.Sharing.enabled = true;
Config.Sharing.passwordProtected = true; Config.Sharing.passwordRequired = false;
const sharing = (await RouteTestingHelper.createSharing(testUser)); const sharing = (await RouteTestingHelper.createSharing(testUser));
@ -161,7 +161,7 @@ describe('UserRouter', () => {
it('it should not authenticate with sharing key without password', async () => { it('it should not authenticate with sharing key without password', async () => {
Config.Users.authenticationRequired = true; Config.Users.authenticationRequired = true;
Config.Sharing.enabled = true; Config.Sharing.enabled = true;
Config.Sharing.passwordProtected = true; Config.Sharing.passwordRequired = true;
const sharing = (await RouteTestingHelper.createSharing(testUser, 'pass_secret')); const sharing = (await RouteTestingHelper.createSharing(testUser, 'pass_secret'));