mirror of
https://github.com/bpatrik/pigallery2.git
synced 2025-01-08 04:03:48 +02:00
Merge branch 'xmp-sidecar-support' of https://github.com/grahamalderson/pigallery2 into xmp-sidecar-support
This commit is contained in:
commit
bce4eb4e08
@ -18,6 +18,7 @@ services:
|
||||
container_name: pigallery2
|
||||
environment:
|
||||
- NODE_ENV=production # set to 'debug' for full debug logging
|
||||
# - NODE_OPTIONS=--enable-source-maps # enable source map support on the backend for development
|
||||
volumes:
|
||||
- "./pigallery2/config:/app/data/config" # CHANGE ME
|
||||
- "db-data:/app/data/db"
|
||||
|
@ -5,6 +5,7 @@ services:
|
||||
container_name: pigallery2
|
||||
environment:
|
||||
- NODE_ENV=production # set to 'debug' for full debug logging
|
||||
# - NODE_OPTIONS=--enable-source-maps # enable source map support on the backend for development
|
||||
volumes:
|
||||
- "./pigallery2/config:/app/data/config" # CHANGE ME
|
||||
- "db-data:/app/data/db"
|
||||
|
@ -30,6 +30,7 @@ services:
|
||||
container_name: pigallery2
|
||||
environment:
|
||||
- NODE_ENV=production # set to 'debug' for full debug logging
|
||||
# - NODE_OPTIONS=--enable-source-maps # enable source map support on the backend for development
|
||||
volumes:
|
||||
- "./pigallery2/config:/app/data/config" # CHANGE ME
|
||||
- "db-data:/app/data/db"
|
||||
|
12
extension/REPOSITORY.md
Normal file
12
extension/REPOSITORY.md
Normal file
@ -0,0 +1,12 @@
|
||||
# Pigallery2 extension repository
|
||||
|
||||
The app loads this file to discover available extensions for download.
|
||||
Note: pigallery2 is not responsible for the extensions that are available here.
|
||||
In case of any issue, contact the extension developer.
|
||||
|
||||
| **Name** | **Url** | **Readme** | **Download** | |
|
||||
|:----------------:|:-------------------------------------------------------------------------------------------------------------:|:-------------------------------------------------------------------------------------------------:|:----------------------------------------------------------------------------------------------:|---|
|
||||
| Sample extension | [pigallery2-sample-extension](https://github.com/bpatrik/pigallery2-sample-extension) | [REAMDE.md](https://raw.githubusercontent.com/bpatrik/pigallery2-sample-extension/main/README.md) | [main.zip](https://github.com/bpatrik/pigallery2-sample-extension/archive/refs/heads/main.zip) | |
|
||||
| DigiKam Connector for Pigallery2 | [pigallery2-extension-digikam-connector](https://github.com/mblythe86/pigallery2-extension-digikam-connector) | [README.md](https://raw.githubusercontent.com/mblythe86/pigallery2-extension-digikam-connector/main/README.md) | [main.zip](https://github.com/mblythe86/pigallery2-extension-digikam-connector/archive/refs/heads/main.zip) | |
|
||||
| | | | | |
|
||||
| | | | | |
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pigallery2-extension-kit",
|
||||
"version": "2.0.3-edge3",
|
||||
"version": "2.0.3-edge4",
|
||||
"description": "Interfaces for developing extensions for pigallery2",
|
||||
"author": "Patrik J. Braun",
|
||||
"homepage": "https://github.com/bpatrik/pigallery2",
|
||||
|
@ -5,6 +5,7 @@ import * as path from 'path';
|
||||
import * as util from 'util';
|
||||
import * as zip from 'gulp-zip';
|
||||
import * as ts from 'gulp-typescript';
|
||||
import * as sourcemaps from 'gulp-sourcemaps';
|
||||
import * as xml2js from 'xml2js';
|
||||
import * as child_process from 'child_process';
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
@ -39,10 +40,12 @@ const getSwitch = (name: string, def: string = null): string => {
|
||||
gulp.task('build-backend', (): any =>
|
||||
gulp
|
||||
.src(['src/common/**/*.ts', 'src/backend/**/*.ts', 'benchmark/**/*.ts'], {
|
||||
base: '.',
|
||||
base: '.'
|
||||
})
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(tsBackendProject())
|
||||
.js.pipe(gulp.dest('./release'))
|
||||
.pipe(sourcemaps.write('.', {includeContent: false}))
|
||||
.pipe(gulp.dest('./release'))
|
||||
);
|
||||
|
||||
const createDynamicTranslationFile = async (
|
||||
@ -56,7 +59,7 @@ const createDynamicTranslationFile = async (
|
||||
);
|
||||
const translationXml: XLIFF.Root = await xml2js.parseStringPromise(data);
|
||||
|
||||
// clean translations, keep only .ts transaltions
|
||||
// clean translations, keep only .ts translations
|
||||
const hasTsTranslation = (cg: XLIFF.ContextGroup): boolean =>
|
||||
cg.context.findIndex(
|
||||
(c: any): boolean =>
|
||||
|
12151
package-lock.json
generated
12151
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -96,6 +96,7 @@
|
||||
"@types/express-jwt": "6.0.4",
|
||||
"@types/fluent-ffmpeg": "2.1.21",
|
||||
"@types/gulp": "4.0.10",
|
||||
"@types/gulp-sourcemaps": "^0.0.38",
|
||||
"@types/gulp-zip": "4.0.2",
|
||||
"@types/jasmine": "4.3.1",
|
||||
"@types/jsonwebtoken": "9.0.1",
|
||||
@ -120,6 +121,7 @@
|
||||
"eslint": "8.36.0",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-json-editor": "2.5.6",
|
||||
"gulp-sourcemaps": "3.0.0",
|
||||
"gulp-typescript": "5.0.1",
|
||||
"gulp-zip": "5.1.0",
|
||||
"hammerjs": "2.0.8",
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 {ErrorCodes, ErrorDTO} from '../../common/entities/Error';
|
||||
import {Config} from '../../common/config/private/Config';
|
||||
@ -9,9 +9,9 @@ import {UserRoles} from '../../common/entities/UserDTO';
|
||||
|
||||
export class SharingMWs {
|
||||
public static async getSharing(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> {
|
||||
if (Config.Sharing.enabled === false) {
|
||||
return next();
|
||||
@ -20,36 +20,69 @@ export class SharingMWs {
|
||||
|
||||
try {
|
||||
req.resultPipe =
|
||||
await ObjectManagers.getInstance().SharingManager.findOne(sharingKey);
|
||||
await ObjectManagers.getInstance().SharingManager.findOne(sharingKey);
|
||||
return next();
|
||||
} catch (err) {
|
||||
return next(
|
||||
new ErrorDTO(
|
||||
ErrorCodes.GENERAL_ERROR,
|
||||
'Error during retrieving sharing link',
|
||||
err
|
||||
)
|
||||
new ErrorDTO(
|
||||
ErrorCodes.GENERAL_ERROR,
|
||||
'Error during retrieving sharing link',
|
||||
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(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> {
|
||||
if (Config.Sharing.enabled === false) {
|
||||
return next();
|
||||
}
|
||||
if (
|
||||
typeof req.body === 'undefined' ||
|
||||
typeof req.body.createSharing === 'undefined'
|
||||
typeof req.body === 'undefined' ||
|
||||
typeof req.body.createSharing === 'undefined'
|
||||
) {
|
||||
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;
|
||||
|
||||
if (Config.Sharing.passwordRequired && !createSharing.password) {
|
||||
|
||||
return next(
|
||||
new ErrorDTO(ErrorCodes.INPUT_ERROR, 'Password is required.')
|
||||
);
|
||||
}
|
||||
|
||||
let sharingKey = SharingMWs.generateKey();
|
||||
|
||||
// create one not yet used
|
||||
@ -71,45 +104,45 @@ export class SharingMWs {
|
||||
password: createSharing.password,
|
||||
creator: req.session['user'],
|
||||
expires:
|
||||
createSharing.valid >= 0 // if === -1 its forever
|
||||
? Date.now() + createSharing.valid
|
||||
: new Date(9999, 0, 1).getTime(), // never expire
|
||||
createSharing.valid >= 0 // if === -1 its forever
|
||||
? Date.now() + createSharing.valid
|
||||
: new Date(9999, 0, 1).getTime(), // never expire
|
||||
includeSubfolders: createSharing.includeSubfolders,
|
||||
timeStamp: Date.now(),
|
||||
};
|
||||
|
||||
try {
|
||||
req.resultPipe =
|
||||
await ObjectManagers.getInstance().SharingManager.createSharing(
|
||||
sharing
|
||||
);
|
||||
await ObjectManagers.getInstance().SharingManager.createSharing(
|
||||
sharing
|
||||
);
|
||||
return next();
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
return next(
|
||||
new ErrorDTO(
|
||||
ErrorCodes.GENERAL_ERROR,
|
||||
'Error during creating sharing link',
|
||||
err
|
||||
)
|
||||
new ErrorDTO(
|
||||
ErrorCodes.GENERAL_ERROR,
|
||||
'Error during creating sharing link',
|
||||
err
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static async updateSharing(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> {
|
||||
if (Config.Sharing.enabled === false) {
|
||||
return next();
|
||||
}
|
||||
if (
|
||||
typeof req.body === 'undefined' ||
|
||||
typeof req.body.updateSharing === 'undefined'
|
||||
typeof req.body === 'undefined' ||
|
||||
typeof req.body.updateSharing === 'undefined'
|
||||
) {
|
||||
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;
|
||||
@ -119,14 +152,14 @@ export class SharingMWs {
|
||||
path: directoryName,
|
||||
sharingKey: '',
|
||||
password:
|
||||
updateSharing.password && updateSharing.password !== ''
|
||||
? updateSharing.password
|
||||
: null,
|
||||
updateSharing.password && updateSharing.password !== ''
|
||||
? updateSharing.password
|
||||
: null,
|
||||
creator: req.session['user'],
|
||||
expires:
|
||||
updateSharing.valid >= 0 // if === -1 its forever
|
||||
? Date.now() + updateSharing.valid
|
||||
: new Date(9999, 0, 1).getTime(), // never expire
|
||||
updateSharing.valid >= 0 // if === -1 its forever
|
||||
? Date.now() + updateSharing.valid
|
||||
: new Date(9999, 0, 1).getTime(), // never expire
|
||||
includeSubfolders: updateSharing.includeSubfolders,
|
||||
timeStamp: Date.now(),
|
||||
};
|
||||
@ -134,36 +167,37 @@ export class SharingMWs {
|
||||
try {
|
||||
const forceUpdate = req.session['user'].role >= UserRoles.Admin;
|
||||
req.resultPipe =
|
||||
await ObjectManagers.getInstance().SharingManager.updateSharing(
|
||||
sharing,
|
||||
forceUpdate
|
||||
);
|
||||
await ObjectManagers.getInstance().SharingManager.updateSharing(
|
||||
sharing,
|
||||
forceUpdate
|
||||
);
|
||||
console.log(req.resultPipe);
|
||||
return next();
|
||||
} catch (err) {
|
||||
return next(
|
||||
new ErrorDTO(
|
||||
ErrorCodes.GENERAL_ERROR,
|
||||
'Error during updating sharing link',
|
||||
err
|
||||
)
|
||||
new ErrorDTO(
|
||||
ErrorCodes.GENERAL_ERROR,
|
||||
'Error during updating sharing link',
|
||||
err
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static async deleteSharing(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> {
|
||||
if (Config.Sharing.enabled === false) {
|
||||
return next();
|
||||
}
|
||||
if (
|
||||
typeof req.params === 'undefined' ||
|
||||
typeof req.params['sharingKey'] === 'undefined'
|
||||
typeof req.params === 'undefined' ||
|
||||
typeof req.params['sharingKey'] === 'undefined'
|
||||
) {
|
||||
return next(
|
||||
new ErrorDTO(ErrorCodes.INPUT_ERROR, 'sharingKey is missing')
|
||||
new ErrorDTO(ErrorCodes.INPUT_ERROR, 'sharingKey is missing')
|
||||
);
|
||||
}
|
||||
const sharingKey: string = req.params['sharingKey'];
|
||||
@ -177,49 +211,49 @@ export class SharingMWs {
|
||||
}
|
||||
}
|
||||
req.resultPipe =
|
||||
await ObjectManagers.getInstance().SharingManager.deleteSharing(
|
||||
sharingKey
|
||||
);
|
||||
await ObjectManagers.getInstance().SharingManager.deleteSharing(
|
||||
sharingKey
|
||||
);
|
||||
req.resultPipe = 'ok';
|
||||
return next();
|
||||
} catch (err) {
|
||||
return next(
|
||||
new ErrorDTO(
|
||||
ErrorCodes.GENERAL_ERROR,
|
||||
'Error during deleting sharing',
|
||||
err
|
||||
)
|
||||
new ErrorDTO(
|
||||
ErrorCodes.GENERAL_ERROR,
|
||||
'Error during deleting sharing',
|
||||
err
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static async listSharing(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> {
|
||||
if (Config.Sharing.enabled === false) {
|
||||
return next();
|
||||
}
|
||||
try {
|
||||
req.resultPipe =
|
||||
await ObjectManagers.getInstance().SharingManager.listAll();
|
||||
await ObjectManagers.getInstance().SharingManager.listAll();
|
||||
return next();
|
||||
} catch (err) {
|
||||
return next(
|
||||
new ErrorDTO(
|
||||
ErrorCodes.GENERAL_ERROR,
|
||||
'Error during listing shares',
|
||||
err
|
||||
)
|
||||
new ErrorDTO(
|
||||
ErrorCodes.GENERAL_ERROR,
|
||||
'Error during listing shares',
|
||||
err
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static async listSharingForDir(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> {
|
||||
if (Config.Sharing.enabled === false) {
|
||||
return next();
|
||||
@ -229,19 +263,19 @@ export class SharingMWs {
|
||||
try {
|
||||
if (req.session['user'].role >= UserRoles.Admin) {
|
||||
req.resultPipe =
|
||||
await ObjectManagers.getInstance().SharingManager.listAllForDir(dir);
|
||||
await ObjectManagers.getInstance().SharingManager.listAllForDir(dir);
|
||||
} else {
|
||||
req.resultPipe =
|
||||
await ObjectManagers.getInstance().SharingManager.listAllForDir(dir, req.session['user']);
|
||||
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
|
||||
)
|
||||
new ErrorDTO(
|
||||
ErrorCodes.GENERAL_ERROR,
|
||||
'Error during listing shares',
|
||||
err
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -249,8 +283,8 @@ export class SharingMWs {
|
||||
private static generateKey(): string {
|
||||
function s4(): string {
|
||||
return Math.floor((1 + Math.random()) * 0x10000)
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
}
|
||||
|
||||
return s4() + s4();
|
||||
|
@ -156,8 +156,8 @@ export class AuthenticationMWs {
|
||||
if (
|
||||
!sharing ||
|
||||
sharing.expires < Date.now() ||
|
||||
(Config.Sharing.passwordProtected === true &&
|
||||
sharing.password &&
|
||||
((Config.Sharing.passwordRequired === true ||
|
||||
sharing.password) &&
|
||||
!PasswordHelper.comparePassword(password, sharing.password))
|
||||
) {
|
||||
Logger.warn(LOG_TAG, 'Failed login with sharing:' + sharing.sharingKey + ', bad password');
|
||||
@ -265,8 +265,9 @@ export class AuthenticationMWs {
|
||||
return null;
|
||||
}
|
||||
|
||||
// no 'free login' if passwords are required, or it is set
|
||||
if (
|
||||
Config.Sharing.passwordProtected === true &&
|
||||
Config.Sharing.passwordRequired === true ||
|
||||
sharing.password
|
||||
) {
|
||||
return null;
|
||||
|
@ -14,7 +14,7 @@ export const ExtensionDecorator = <I extends unknown[], O>(fn: (ee: IExtensionEv
|
||||
return (
|
||||
target: unknown,
|
||||
propertyName: string,
|
||||
descriptor: TypedPropertyDescriptor<(...args:I)=>Promise<O>>
|
||||
descriptor: TypedPropertyDescriptor<(...args: I) => Promise<O>>
|
||||
) => {
|
||||
|
||||
const targetMethod = descriptor.value;
|
||||
@ -32,7 +32,7 @@ export const ExtensionDecorator = <I extends unknown[], O>(fn: (ee: IExtensionEv
|
||||
return input as O;
|
||||
}
|
||||
const out = await targetMethod.apply(this, input);
|
||||
return await event.triggerAfter(out);
|
||||
return await event.triggerAfter(input as I, out);
|
||||
};
|
||||
|
||||
return descriptor;
|
||||
|
@ -2,7 +2,7 @@ import {IExtensionAfterEventHandler, IExtensionBeforeEventHandler, IExtensionEve
|
||||
|
||||
export class ExtensionEvent<I extends unknown[], O> implements IExtensionEvent<I, O> {
|
||||
protected beforeHandlers: IExtensionBeforeEventHandler<I, O>[] = [];
|
||||
protected afterHandlers: IExtensionAfterEventHandler<O>[] = [];
|
||||
protected afterHandlers: IExtensionAfterEventHandler<I, O>[] = [];
|
||||
|
||||
public before(handler: IExtensionBeforeEventHandler<I, O>): void {
|
||||
if (typeof handler !== 'function') {
|
||||
@ -11,14 +11,14 @@ export class ExtensionEvent<I extends unknown[], O> implements IExtensionEvent<I
|
||||
this.beforeHandlers.push(handler);
|
||||
}
|
||||
|
||||
public after(handler: IExtensionAfterEventHandler<O>): void {
|
||||
public after(handler: IExtensionAfterEventHandler<I, O>): void {
|
||||
if (typeof handler !== 'function') {
|
||||
throw new Error('ExtensionEvent::after: Handler is not a function');
|
||||
}
|
||||
this.afterHandlers.push(handler);
|
||||
}
|
||||
|
||||
public offAfter(handler: IExtensionAfterEventHandler<O>): void {
|
||||
public offAfter(handler: IExtensionAfterEventHandler<I, O>): void {
|
||||
this.afterHandlers = this.afterHandlers.filter((h) => h !== handler);
|
||||
}
|
||||
|
||||
@ -41,11 +41,11 @@ export class ExtensionEvent<I extends unknown[], O> implements IExtensionEvent<I
|
||||
return pipe;
|
||||
}
|
||||
|
||||
public async triggerAfter(output: O): Promise<O> {
|
||||
public async triggerAfter(input: I, output: O): Promise<O> {
|
||||
if (this.afterHandlers && this.afterHandlers.length > 0) {
|
||||
const s = this.afterHandlers.slice(0);
|
||||
for (let i = 0; i < s.length; ++i) {
|
||||
output = await s[i](output);
|
||||
output = await s[i]({input, output});
|
||||
}
|
||||
}
|
||||
return output;
|
||||
|
@ -19,12 +19,16 @@ import {DirectoryScanSettings} from '../fileaccess/DiskManager';
|
||||
|
||||
|
||||
export type IExtensionBeforeEventHandler<I extends unknown[], O> = (input: I, event: { stopPropagation: boolean }) => Promise<I | O>;
|
||||
export type IExtensionAfterEventHandler<O> = (output: O) => Promise<O>;
|
||||
/**
|
||||
* input: is the original input: this is output of all before handler. This value was also piped to app's function
|
||||
* output: is the output of the app's function or the previous after handler
|
||||
*/
|
||||
export type IExtensionAfterEventHandler<I extends unknown[], O> = (data: { input: I, output: O }) => Promise<O>;
|
||||
|
||||
|
||||
export interface IExtensionEvent<I extends unknown[], O> {
|
||||
before: (handler: IExtensionBeforeEventHandler<I, O>) => void;
|
||||
after: (handler: IExtensionAfterEventHandler<O>) => void;
|
||||
after: (handler: IExtensionAfterEventHandler<I, O>) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -59,7 +63,9 @@ export interface IExtensionEvents {
|
||||
* Reads exif, iptc, etc.. metadata for photos/videos
|
||||
*/
|
||||
MetadataLoader: {
|
||||
// input: file path
|
||||
loadVideoMetadata: IExtensionEvent<[string], VideoMetadata>,
|
||||
// input: file path
|
||||
loadPhotoMetadata: IExtensionEvent<[string], PhotoMetadata>
|
||||
},
|
||||
/**
|
||||
@ -122,7 +128,7 @@ export interface IExtensionDB {
|
||||
setExtensionTables(tables: Function[]): Promise<void>;
|
||||
|
||||
/**
|
||||
* Exposes all tables. You can use this if you van to have a foreign key to a built in table.
|
||||
* Exposes all tables. You can use this if you van to have a foreign key to a built-in table.
|
||||
* Use with caution. This exposes the app's internal working.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -12,6 +12,7 @@ import {ServerTimeEntry} from '../middlewares/ServerTimingMWs';
|
||||
import {ClientConfig, TAGS} from '../../common/config/public/ClientConfig';
|
||||
import {QueryParams} from '../../common/QueryParams';
|
||||
import {PhotoProcessing} from '../model/fileaccess/fileprocessing/PhotoProcessing';
|
||||
import {Utils} from '../../common/Utils';
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
@ -35,7 +36,7 @@ export class PublicRouter {
|
||||
let selectedLocale = req.locale;
|
||||
if (req.cookies && req.cookies[CookieNames.lang]) {
|
||||
if (
|
||||
Config.Server.languages.indexOf(req.cookies[CookieNames.lang]) !== -1
|
||||
Config.Server.languages.indexOf(req.cookies[CookieNames.lang]) !== -1
|
||||
) {
|
||||
selectedLocale = req.cookies[CookieNames.lang];
|
||||
}
|
||||
@ -48,14 +49,14 @@ export class PublicRouter {
|
||||
// index.html should not be cached as it contains template that can change
|
||||
const renderIndex = (req: Request, res: Response, next: NextFunction) => {
|
||||
ejs.renderFile(
|
||||
path.join(ProjectPath.FrontendFolder, req.localePath, 'index.html'),
|
||||
res.tpl,
|
||||
(err, str) => {
|
||||
if (err) {
|
||||
return next(new ErrorDTO(ErrorCodes.GENERAL_ERROR, err.message));
|
||||
}
|
||||
res.send(str);
|
||||
path.join(ProjectPath.FrontendFolder, req.localePath, 'index.html'),
|
||||
res.tpl,
|
||||
(err, str) => {
|
||||
if (err) {
|
||||
return next(new ErrorDTO(ErrorCodes.GENERAL_ERROR, err.message));
|
||||
}
|
||||
res.send(str);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@ -64,7 +65,7 @@ export class PublicRouter {
|
||||
if (Config.Server.languages.indexOf(locale) !== -1) {
|
||||
res.cookie(CookieNames.lang, locale);
|
||||
}
|
||||
res.redirect('/?ln=' + locale);
|
||||
res.redirect(Utils.concatUrls('/' + Config.Server.urlBase) + '/?ln=' + locale);
|
||||
};
|
||||
};
|
||||
|
||||
@ -94,12 +95,12 @@ export class PublicRouter {
|
||||
}) as unknown as ClientConfig;
|
||||
// Escaping html tags, like <script></script>
|
||||
confCopy.Server.customHTMLHead =
|
||||
confCopy.Server.customHTMLHead
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
confCopy.Server.customHTMLHead
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
res.tpl.Config = confCopy;
|
||||
res.tpl.customHTMLHead = Config.Server.customHTMLHead;
|
||||
const selectedTheme = Config.Gallery.Themes.availableThemes.find(th => th.name === Config.Gallery.Themes.selectedTheme)?.theme || '';
|
||||
@ -140,7 +141,7 @@ export class PublicRouter {
|
||||
'photo'
|
||||
],
|
||||
start_url:
|
||||
Config.Server.publicUrl === '' ? '.' : Config.Server.publicUrl,
|
||||
Config.Server.publicUrl === '' ? '.' : Config.Server.publicUrl,
|
||||
background_color: '#000000',
|
||||
theme_color: '#000000',
|
||||
});
|
||||
@ -185,40 +186,40 @@ export class PublicRouter {
|
||||
y: vBs[1]
|
||||
};
|
||||
return '<svg ' +
|
||||
' xmlns="http://www.w3.org/2000/svg"' +
|
||||
' viewBox="' + vBs.join(' ') + '">' +
|
||||
(theme === 'auto' ? ('<style>' +
|
||||
' path, circle {' +
|
||||
' fill: black;' +
|
||||
' }' +
|
||||
' circle.bg,rect.bg {' +
|
||||
' fill: white;' +
|
||||
' }' +
|
||||
' @media (prefers-color-scheme: dark) {' +
|
||||
' path, circle {' +
|
||||
' fill: white;' +
|
||||
' }' +
|
||||
' circle.bg,rect.bg {' +
|
||||
' fill: black;' +
|
||||
' }' +
|
||||
' }' +
|
||||
' </style>') :
|
||||
(theme != null ?
|
||||
('<style>' +
|
||||
' path, circle {' +
|
||||
' fill: ' + theme + ';' +
|
||||
' }' +
|
||||
' circle.bg {' +
|
||||
' fill: black;' +
|
||||
' }' +
|
||||
' </style>')
|
||||
: '<style>' +
|
||||
' circle.bg,rect.bg {' +
|
||||
' fill: white;' +
|
||||
' }' +
|
||||
' </style>')) +
|
||||
`<rect class="bg" x="${canvasStart.x}" y="${canvasStart.y}" width="${canvasSize}" height="${canvasSize}" rx="15" />` +
|
||||
Config.Server.svgIcon.items + '</svg>';
|
||||
' xmlns="http://www.w3.org/2000/svg"' +
|
||||
' viewBox="' + vBs.join(' ') + '">' +
|
||||
(theme === 'auto' ? ('<style>' +
|
||||
' path, circle {' +
|
||||
' fill: black;' +
|
||||
' }' +
|
||||
' circle.bg,rect.bg {' +
|
||||
' fill: white;' +
|
||||
' }' +
|
||||
' @media (prefers-color-scheme: dark) {' +
|
||||
' path, circle {' +
|
||||
' fill: white;' +
|
||||
' }' +
|
||||
' circle.bg,rect.bg {' +
|
||||
' fill: black;' +
|
||||
' }' +
|
||||
' }' +
|
||||
' </style>') :
|
||||
(theme != null ?
|
||||
('<style>' +
|
||||
' path, circle {' +
|
||||
' fill: ' + theme + ';' +
|
||||
' }' +
|
||||
' circle.bg {' +
|
||||
' fill: black;' +
|
||||
' }' +
|
||||
' </style>')
|
||||
: '<style>' +
|
||||
' circle.bg,rect.bg {' +
|
||||
' fill: white;' +
|
||||
' }' +
|
||||
' </style>')) +
|
||||
`<rect class="bg" x="${canvasStart.x}" y="${canvasStart.y}" width="${canvasSize}" height="${canvasSize}" rx="15" />` +
|
||||
Config.Server.svgIcon.items + '</svg>';
|
||||
};
|
||||
|
||||
app.get('/icon.svg', (req: Request, res: Response) => {
|
||||
@ -276,44 +277,44 @@ export class PublicRouter {
|
||||
});
|
||||
|
||||
app.get(
|
||||
[
|
||||
'/',
|
||||
'/login',
|
||||
'/gallery*',
|
||||
'/share/:' + QueryParams.gallery.sharingKey_params,
|
||||
'/shareLogin',
|
||||
'/admin',
|
||||
'/duplicates',
|
||||
'/faces',
|
||||
'/albums',
|
||||
'/search*',
|
||||
],
|
||||
AuthenticationMWs.tryAuthenticate,
|
||||
addTPl, // add template after authentication was successful
|
||||
setLocale,
|
||||
renderIndex
|
||||
[
|
||||
'/',
|
||||
'/login',
|
||||
'/gallery*',
|
||||
'/share/:' + QueryParams.gallery.sharingKey_params,
|
||||
'/shareLogin',
|
||||
'/admin',
|
||||
'/duplicates',
|
||||
'/faces',
|
||||
'/albums',
|
||||
'/search*',
|
||||
],
|
||||
AuthenticationMWs.tryAuthenticate,
|
||||
addTPl, // add template after authentication was successful
|
||||
setLocale,
|
||||
renderIndex
|
||||
);
|
||||
Config.Server.languages.forEach((l) => {
|
||||
app.get(
|
||||
[
|
||||
'/' + l + '/',
|
||||
'/' + l + '/login',
|
||||
'/' + l + '/gallery*',
|
||||
'/' + l + '/share*',
|
||||
'/' + l + '/admin',
|
||||
'/' + l + '/search*',
|
||||
],
|
||||
redirectToBase(l)
|
||||
[
|
||||
'/' + l + '/',
|
||||
'/' + l + '/login',
|
||||
'/' + l + '/gallery*',
|
||||
'/' + l + '/share*',
|
||||
'/' + l + '/admin',
|
||||
'/' + l + '/search*',
|
||||
],
|
||||
redirectToBase(l)
|
||||
);
|
||||
});
|
||||
|
||||
const renderFile = (subDir = '') => {
|
||||
return (req: Request, res: Response) => {
|
||||
const file = path.join(
|
||||
ProjectPath.FrontendFolder,
|
||||
req.localePath,
|
||||
subDir,
|
||||
req.params.file
|
||||
ProjectPath.FrontendFolder,
|
||||
req.localePath,
|
||||
subDir,
|
||||
req.params.file
|
||||
);
|
||||
if (!fs.existsSync(file)) {
|
||||
return res.sendStatus(404);
|
||||
@ -326,16 +327,16 @@ export class PublicRouter {
|
||||
};
|
||||
|
||||
app.get(
|
||||
'/assets/:file(*)',
|
||||
setLocale,
|
||||
AuthenticationMWs.normalizePathParam('file'),
|
||||
renderFile('assets')
|
||||
'/assets/:file(*)',
|
||||
setLocale,
|
||||
AuthenticationMWs.normalizePathParam('file'),
|
||||
renderFile('assets')
|
||||
);
|
||||
app.get(
|
||||
'/:file',
|
||||
setLocale,
|
||||
AuthenticationMWs.normalizePathParam('file'),
|
||||
renderFile()
|
||||
'/:file',
|
||||
setLocale,
|
||||
AuthenticationMWs.normalizePathParam('file'),
|
||||
renderFile()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ export class SharingRouter {
|
||||
public static route(app: express.Express): void {
|
||||
this.addShareLogin(app);
|
||||
this.addGetSharing(app);
|
||||
this.addGetSharingKey(app);
|
||||
this.addCreateSharing(app);
|
||||
this.addUpdateSharing(app);
|
||||
this.addListSharing(app);
|
||||
@ -20,79 +21,94 @@ export class SharingRouter {
|
||||
|
||||
private static addShareLogin(app: express.Express): void {
|
||||
app.post(
|
||||
Config.Server.apiPath + '/share/login',
|
||||
AuthenticationMWs.inverseAuthenticate,
|
||||
AuthenticationMWs.shareLogin,
|
||||
ServerTimingMWs.addServerTiming,
|
||||
RenderingMWs.renderSessionUser
|
||||
Config.Server.apiPath + '/share/login',
|
||||
AuthenticationMWs.inverseAuthenticate,
|
||||
AuthenticationMWs.shareLogin,
|
||||
ServerTimingMWs.addServerTiming,
|
||||
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 {
|
||||
app.get(
|
||||
Config.Server.apiPath + '/share/:' + QueryParams.gallery.sharingKey_params,
|
||||
AuthenticationMWs.authenticate,
|
||||
AuthenticationMWs.authorise(UserRoles.LimitedGuest),
|
||||
SharingMWs.getSharing,
|
||||
ServerTimingMWs.addServerTiming,
|
||||
RenderingMWs.renderSharing
|
||||
Config.Server.apiPath + '/share/:' + QueryParams.gallery.sharingKey_params,
|
||||
AuthenticationMWs.authenticate,
|
||||
AuthenticationMWs.authorise(UserRoles.LimitedGuest),
|
||||
SharingMWs.getSharing,
|
||||
ServerTimingMWs.addServerTiming,
|
||||
RenderingMWs.renderSharing
|
||||
);
|
||||
}
|
||||
|
||||
private static addCreateSharing(app: express.Express): void {
|
||||
app.post(
|
||||
[Config.Server.apiPath + '/share/:directory(*)', Config.Server.apiPath + '/share/', Config.Server.apiPath + '/share//'],
|
||||
AuthenticationMWs.authenticate,
|
||||
AuthenticationMWs.authorise(UserRoles.User),
|
||||
SharingMWs.createSharing,
|
||||
ServerTimingMWs.addServerTiming,
|
||||
RenderingMWs.renderSharing
|
||||
[Config.Server.apiPath + '/share/:directory(*)', Config.Server.apiPath + '/share/', Config.Server.apiPath + '/share//'],
|
||||
AuthenticationMWs.authenticate,
|
||||
AuthenticationMWs.authorise(UserRoles.User),
|
||||
SharingMWs.createSharing,
|
||||
ServerTimingMWs.addServerTiming,
|
||||
RenderingMWs.renderSharing
|
||||
);
|
||||
}
|
||||
|
||||
private static addUpdateSharing(app: express.Express): void {
|
||||
app.put(
|
||||
[Config.Server.apiPath + '/share/:directory(*)', Config.Server.apiPath + '/share/', Config.Server.apiPath + '/share//'],
|
||||
AuthenticationMWs.authenticate,
|
||||
AuthenticationMWs.authorise(UserRoles.User),
|
||||
SharingMWs.updateSharing,
|
||||
ServerTimingMWs.addServerTiming,
|
||||
RenderingMWs.renderSharing
|
||||
[Config.Server.apiPath + '/share/:directory(*)', Config.Server.apiPath + '/share/', Config.Server.apiPath + '/share//'],
|
||||
AuthenticationMWs.authenticate,
|
||||
AuthenticationMWs.authorise(UserRoles.User),
|
||||
SharingMWs.updateSharing,
|
||||
ServerTimingMWs.addServerTiming,
|
||||
RenderingMWs.renderSharing
|
||||
);
|
||||
}
|
||||
|
||||
private static addDeleteSharing(app: express.Express): void {
|
||||
app.delete(
|
||||
[Config.Server.apiPath + '/share/:' + QueryParams.gallery.sharingKey_params],
|
||||
AuthenticationMWs.authenticate,
|
||||
AuthenticationMWs.authorise(UserRoles.User),
|
||||
SharingMWs.deleteSharing,
|
||||
ServerTimingMWs.addServerTiming,
|
||||
RenderingMWs.renderResult
|
||||
[Config.Server.apiPath + '/share/:' + QueryParams.gallery.sharingKey_params],
|
||||
AuthenticationMWs.authenticate,
|
||||
AuthenticationMWs.authorise(UserRoles.User),
|
||||
SharingMWs.deleteSharing,
|
||||
ServerTimingMWs.addServerTiming,
|
||||
RenderingMWs.renderResult
|
||||
);
|
||||
}
|
||||
|
||||
private static addListSharing(app: express.Express): void {
|
||||
app.get(
|
||||
[Config.Server.apiPath + '/share/listAll'],
|
||||
AuthenticationMWs.authenticate,
|
||||
AuthenticationMWs.authorise(UserRoles.Admin),
|
||||
SharingMWs.listSharing,
|
||||
ServerTimingMWs.addServerTiming,
|
||||
RenderingMWs.renderSharingList
|
||||
[Config.Server.apiPath + '/share/listAll'],
|
||||
AuthenticationMWs.authenticate,
|
||||
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
|
||||
[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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -59,7 +59,6 @@ export class Server {
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
|
||||
this.app = express();
|
||||
LoggerRouter.route(this.app);
|
||||
this.app.set('view engine', 'ejs');
|
||||
|
@ -273,12 +273,12 @@ export class ClientSharingConfig {
|
||||
@ConfigProperty({
|
||||
tags:
|
||||
{
|
||||
name: $localize`Password protected`,
|
||||
name: $localize`Require password`,
|
||||
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})
|
||||
@ -1335,7 +1335,7 @@ export class ClientServiceConfig {
|
||||
applicationTitle: string = 'PiGallery 2';
|
||||
|
||||
@ConfigProperty({
|
||||
description: $localize`If you access the page form local network its good to know the public url for creating sharing link.`,
|
||||
description: $localize`If you access the page from local network its good to know the public url for creating sharing link.`,
|
||||
tags: {
|
||||
name: $localize`Page public url`,
|
||||
hint: typeof window !== 'undefined' ? window?.origin : '',
|
||||
|
@ -1,6 +1,10 @@
|
||||
import {UserDTO} from './UserDTO';
|
||||
|
||||
export interface SharingDTO {
|
||||
export interface SharingDTOKey {
|
||||
sharingKey: string;
|
||||
}
|
||||
|
||||
export interface SharingDTO extends SharingDTOKey {
|
||||
id: number;
|
||||
path: string;
|
||||
sharingKey: string;
|
||||
|
@ -65,16 +65,16 @@
|
||||
<li class="nav-item dropdown">
|
||||
<div class="btn-group" dropdown #dropdown="bs-dropdown" placement="bottom"
|
||||
[autoClose]="false" container="body">
|
||||
<button id="button-alignment" dropdownToggle
|
||||
<button id="button-frame-menu" dropdownToggle
|
||||
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>
|
||||
<span *ngIf="isAdmin() && notificationService.numberOfNotifications>0"
|
||||
class="navbar-badge badge text-bg-warning">{{notificationService.numberOfNotifications}}</span>
|
||||
</button>
|
||||
<ul id="dropdown-alignment" *dropdownMenu
|
||||
<ul id="dropdown-frame-menu" *dropdownMenu
|
||||
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">
|
||||
<div style="white-space: nowrap;" class="dropdown-item">
|
||||
|
@ -2,7 +2,7 @@ import {Injectable} from '@angular/core';
|
||||
import {BehaviorSubject, Observable} from 'rxjs';
|
||||
import {PhotoDTO} from '../../../../../common/entities/PhotoDTO';
|
||||
import {DirectoryContent} from '../contentLoader.service';
|
||||
import {debounceTime, map, switchMap} from 'rxjs/operators';
|
||||
import {map, switchMap} from 'rxjs/operators';
|
||||
|
||||
export enum FilterRenderType {
|
||||
enum = 1,
|
||||
@ -65,16 +65,6 @@ export class FilterService {
|
||||
mapFn: (m: PhotoDTO): number => m.metadata.rating,
|
||||
renderType: FilterRenderType.enum,
|
||||
},
|
||||
{
|
||||
name: $localize`Camera`,
|
||||
mapFn: (m: PhotoDTO): string => m.metadata.cameraData?.model,
|
||||
renderType: FilterRenderType.enum,
|
||||
},
|
||||
{
|
||||
name: $localize`Lens`,
|
||||
mapFn: (m: PhotoDTO): string => m.metadata.cameraData?.lens,
|
||||
renderType: FilterRenderType.enum,
|
||||
},
|
||||
{
|
||||
name: $localize`City`,
|
||||
mapFn: (m: PhotoDTO): string => m.metadata.positionData?.city,
|
||||
@ -90,6 +80,45 @@ export class FilterService {
|
||||
mapFn: (m: PhotoDTO): string => m.metadata.positionData?.country,
|
||||
renderType: FilterRenderType.enum,
|
||||
},
|
||||
{
|
||||
name: $localize`Camera`,
|
||||
mapFn: (m: PhotoDTO): string => m.metadata.cameraData?.model,
|
||||
renderType: FilterRenderType.enum,
|
||||
},
|
||||
{
|
||||
name: $localize`Lens`,
|
||||
mapFn: (m: PhotoDTO): string => m.metadata.cameraData?.lens,
|
||||
renderType: FilterRenderType.enum,
|
||||
},
|
||||
{
|
||||
name: $localize`ISO`,
|
||||
mapFn: (m: PhotoDTO): number => m.metadata.cameraData?.ISO,
|
||||
renderType: FilterRenderType.enum,
|
||||
},
|
||||
{
|
||||
name: $localize`Aperture`,
|
||||
mapFn: (m: PhotoDTO): string => m.metadata.cameraData?.fStop ? `f/${m.metadata.cameraData?.fStop}` : undefined,
|
||||
renderType: FilterRenderType.enum,
|
||||
},
|
||||
{
|
||||
name: $localize`Shutter speed`,
|
||||
mapFn: (m: PhotoDTO): string => {
|
||||
const f = m.metadata.cameraData?.exposure;
|
||||
if (typeof f === 'undefined') {
|
||||
return undefined;
|
||||
}
|
||||
if (f > 1) {
|
||||
return `${f} s`;
|
||||
}
|
||||
return `1/${Math.round(1 / f)} s`;
|
||||
},
|
||||
renderType: FilterRenderType.enum,
|
||||
},
|
||||
{
|
||||
name: $localize`Focal length`,
|
||||
mapFn: (m: PhotoDTO): string => m.metadata.cameraData?.focalLength ? `${m.metadata.cameraData?.focalLength} mm` : undefined,
|
||||
renderType: FilterRenderType.enum,
|
||||
},
|
||||
];
|
||||
|
||||
public readonly activeFilters = new BehaviorSubject({
|
||||
@ -279,7 +308,7 @@ export class FilterService {
|
||||
for (const f of afilters.selectedFilters) {
|
||||
|
||||
/* Update filter options */
|
||||
const valueMap: { [key: string]: any } = {};
|
||||
const valueMap: { [key: string]: { name: string | number, count: number, selected: boolean } } = {};
|
||||
f.options.forEach((o) => {
|
||||
valueMap[o.name] = o;
|
||||
o.count = 0; // reset count so unknown option can be removed at the end
|
||||
@ -307,10 +336,13 @@ export class FilterService {
|
||||
valueMap[key].count++;
|
||||
});
|
||||
}
|
||||
|
||||
f.options = Object.values(valueMap)
|
||||
.filter((o) => o.count > 0)
|
||||
.sort((a, b) => b.count - a.count);
|
||||
// sort by count and alpha. if counts are the same
|
||||
.sort((a, b) =>
|
||||
a.count == b.count && (a.name !== undefined && b.name !== undefined) ?
|
||||
Number.isFinite(a.name) && Number.isFinite(b.name) ? (a.name as number) - (b.name as number) : a.name.toString().localeCompare(b.name.toString()) :
|
||||
b.count - a.count);
|
||||
|
||||
/* Apply filters */
|
||||
f.options.forEach((opt) => {
|
||||
|
@ -52,18 +52,18 @@ export class GalleryComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
|
||||
constructor(
|
||||
public contentLoader: ContentLoaderService,
|
||||
public galleryService: ContentService,
|
||||
private authService: AuthenticationService,
|
||||
private router: Router,
|
||||
private shareService: ShareService,
|
||||
private route: ActivatedRoute,
|
||||
private navigation: NavigationService,
|
||||
private filterService: FilterService,
|
||||
private sortingService: GallerySortingService,
|
||||
private piTitleService: PiTitleService,
|
||||
private gpxFilesFilterPipe: GPXFilesFilterPipe,
|
||||
private mdFilesFilterPipe: MDFilesFilterPipe,
|
||||
public contentLoader: ContentLoaderService,
|
||||
public galleryService: ContentService,
|
||||
private authService: AuthenticationService,
|
||||
private router: Router,
|
||||
private shareService: ShareService,
|
||||
private route: ActivatedRoute,
|
||||
private navigation: NavigationService,
|
||||
private filterService: FilterService,
|
||||
private sortingService: GallerySortingService,
|
||||
private piTitleService: PiTitleService,
|
||||
private gpxFilesFilterPipe: GPXFilesFilterPipe,
|
||||
private mdFilesFilterPipe: MDFilesFilterPipe,
|
||||
) {
|
||||
this.mapEnabled = Config.Map.enabled;
|
||||
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 (
|
||||
(this.shareService.sharingSubject.value.expires - Date.now()) /
|
||||
1000 /
|
||||
86400 /
|
||||
365 >
|
||||
10
|
||||
(this.shareService.sharingSubject.value.expires - Date.now()) /
|
||||
1000 /
|
||||
86400 /
|
||||
365 >
|
||||
10
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
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.day = Math.floor(t / 86400);
|
||||
@ -118,31 +118,26 @@ export class GalleryComponent implements OnInit, OnDestroy {
|
||||
|
||||
async ngOnInit(): Promise<boolean> {
|
||||
await this.shareService.wait();
|
||||
if (
|
||||
!this.authService.isAuthenticated() &&
|
||||
(!this.shareService.isSharing() ||
|
||||
(this.shareService.isSharing() &&
|
||||
Config.Sharing.passwordProtected === true))
|
||||
) {
|
||||
if (!this.authService.isAuthenticated()) {
|
||||
return this.navigation.toLogin();
|
||||
}
|
||||
this.showSearchBar = this.authService.canSearch();
|
||||
this.showShare =
|
||||
Config.Sharing.enabled &&
|
||||
this.authService.isAuthorized(UserRoles.User);
|
||||
Config.Sharing.enabled &&
|
||||
this.authService.isAuthorized(UserRoles.User);
|
||||
this.showRandomPhotoBuilder =
|
||||
Config.RandomPhoto.enabled &&
|
||||
this.authService.isAuthorized(UserRoles.User);
|
||||
Config.RandomPhoto.enabled &&
|
||||
this.authService.isAuthorized(UserRoles.User);
|
||||
this.subscription.content = this.galleryService.sortedFilteredContent
|
||||
.subscribe((dc: GroupedDirectoryContent) => {
|
||||
this.onContentChange(dc);
|
||||
});
|
||||
.subscribe((dc: GroupedDirectoryContent) => {
|
||||
this.onContentChange(dc);
|
||||
});
|
||||
this.subscription.route = this.route.params.subscribe(this.onRoute);
|
||||
|
||||
if (this.shareService.isSharing()) {
|
||||
this.$counter = interval(1000);
|
||||
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 (
|
||||
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
|
||||
.pipe(take(1))
|
||||
.toPromise();
|
||||
.pipe(take(1))
|
||||
.toPromise();
|
||||
const qParams: { [key: string]: any } = {};
|
||||
qParams[QueryParams.gallery.sharingKey_query] =
|
||||
this.shareService.getSharingKey();
|
||||
this.shareService.getSharingKey();
|
||||
this.router
|
||||
.navigate(['/gallery', sharing.path], {queryParams: qParams})
|
||||
.catch(console.error);
|
||||
.navigate(['/gallery', sharing.path], {queryParams: qParams})
|
||||
.catch(console.error);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -191,8 +186,8 @@ export class GalleryComponent implements OnInit, OnDestroy {
|
||||
for (const mediaGroup of content.mediaGroups) {
|
||||
|
||||
if (
|
||||
mediaGroup.media
|
||||
.findIndex((m: PhotoDTO) => !!m.metadata?.positionData?.GPSData?.longitude) !== -1
|
||||
mediaGroup.media
|
||||
.findIndex((m: PhotoDTO) => !!m.metadata?.positionData?.GPSData?.longitude) !== -1
|
||||
) {
|
||||
this.isPhotoWithLocation = true;
|
||||
break;
|
||||
|
@ -433,7 +433,7 @@ export class ControlsLightboxComponent implements OnDestroy, OnInit, OnChanges {
|
||||
}
|
||||
|
||||
private hideControls = () => {
|
||||
//this.controllersDimmed = true;
|
||||
this.controllersDimmed = true;
|
||||
};
|
||||
|
||||
private updateFaceContainerDim(): void {
|
||||
|
@ -1,11 +1,11 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
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 {BehaviorSubject} from 'rxjs';
|
||||
import {distinctUntilChanged, filter} from 'rxjs/operators';
|
||||
import {QueryParams} from '../../../../common/QueryParams';
|
||||
import {UserDTO} from '../../../../common/entities/UserDTO';
|
||||
import {UserDTO, UserRoles} from '../../../../common/entities/UserDTO';
|
||||
import {Utils} from '../../../../common/Utils';
|
||||
import {Config} from '../../../../common/config/public/Config';
|
||||
|
||||
@ -21,12 +21,15 @@ export class ShareService {
|
||||
inited = false;
|
||||
public ReadyPR: Promise<void>;
|
||||
public sharingSubject: BehaviorSubject<SharingDTO> = new BehaviorSubject(
|
||||
null
|
||||
null
|
||||
);
|
||||
public sharingIsValid: BehaviorSubject<boolean> = new BehaviorSubject(
|
||||
null
|
||||
);
|
||||
public currentSharing = this.sharingSubject
|
||||
.asObservable()
|
||||
.pipe(filter((s) => s !== null))
|
||||
.pipe(distinctUntilChanged());
|
||||
.asObservable()
|
||||
.pipe(filter((s) => s !== null))
|
||||
.pipe(distinctUntilChanged());
|
||||
|
||||
private resolve: () => void;
|
||||
|
||||
@ -41,18 +44,18 @@ export class ShareService {
|
||||
this.router.events.subscribe(async (val) => {
|
||||
if (val instanceof RoutesRecognized) {
|
||||
this.param =
|
||||
val.state.root.firstChild.params[
|
||||
QueryParams.gallery.sharingKey_params
|
||||
] || null;
|
||||
val.state.root.firstChild.params[
|
||||
QueryParams.gallery.sharingKey_params
|
||||
] || null;
|
||||
this.queryParam =
|
||||
val.state.root.firstChild.queryParams[
|
||||
QueryParams.gallery.sharingKey_query
|
||||
] || null;
|
||||
val.state.root.firstChild.queryParams[
|
||||
QueryParams.gallery.sharingKey_query
|
||||
] || null;
|
||||
|
||||
const changed = this.sharingKey !== (this.param || this.queryParam);
|
||||
if (changed) {
|
||||
this.sharingKey = this.param || this.queryParam || this.sharingKey;
|
||||
await this.getSharing();
|
||||
await this.checkSharing();
|
||||
}
|
||||
if (this.resolve) {
|
||||
this.resolve();
|
||||
@ -69,12 +72,17 @@ export class ShareService {
|
||||
|
||||
|
||||
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 (
|
||||
user.usedSharingKey !== this.sharingKey ||
|
||||
this.sharingSubject.value == null
|
||||
(user?.usedSharingKey &&
|
||||
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();
|
||||
}
|
||||
if (this.resolve) {
|
||||
@ -93,24 +101,26 @@ export class ShareService {
|
||||
}
|
||||
|
||||
public createSharing(
|
||||
dir: string,
|
||||
includeSubFolders: boolean,
|
||||
valid: number
|
||||
dir: string,
|
||||
includeSubFolders: boolean,
|
||||
password: string,
|
||||
valid: number
|
||||
): Promise<SharingDTO> {
|
||||
return this.networkService.postJson('/share/' + dir, {
|
||||
createSharing: {
|
||||
includeSubfolders: includeSubFolders,
|
||||
valid,
|
||||
...(!!password && {password: password}) // only add password if present
|
||||
} as CreateSharingDTO,
|
||||
});
|
||||
}
|
||||
|
||||
public updateSharing(
|
||||
dir: string,
|
||||
sharingId: number,
|
||||
includeSubFolders: boolean,
|
||||
password: string,
|
||||
valid: number
|
||||
dir: string,
|
||||
sharingId: number,
|
||||
includeSubFolders: boolean,
|
||||
password: string,
|
||||
valid: number
|
||||
): Promise<SharingDTO> {
|
||||
return this.networkService.putJson('/share/' + dir, {
|
||||
updateSharing: {
|
||||
@ -134,7 +144,7 @@ export class ShareService {
|
||||
try {
|
||||
this.sharingSubject.next(null);
|
||||
const sharing = await this.networkService.getJson<SharingDTO>(
|
||||
'/share/' + this.getSharingKey()
|
||||
'/share/' + this.getSharingKey()
|
||||
);
|
||||
this.sharingSubject.next(sharing);
|
||||
} 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(
|
||||
dir: string
|
||||
dir: string
|
||||
): Promise<SharingDTO[]> {
|
||||
return this.networkService.getJson('/share/list/' + dir);
|
||||
}
|
||||
|
@ -82,11 +82,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" *ngIf="passwordProtection">
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
<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>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
@ -98,7 +98,7 @@
|
||||
[(ngModel)]="input.password"
|
||||
i18n-placeholder
|
||||
placeholder="Password"
|
||||
required>
|
||||
[required]="passwordRequired">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -36,7 +36,7 @@ export class GalleryShareComponent implements OnInit, OnDestroy {
|
||||
currentDir = '';
|
||||
sharing: SharingDTO = null;
|
||||
contentSubscription: Subscription = null;
|
||||
readonly passwordProtection = Config.Sharing.passwordProtected;
|
||||
readonly passwordRequired = Config.Sharing.passwordRequired;
|
||||
readonly ValidityTypes = ValidityTypes;
|
||||
|
||||
modalRef: BsModalRef;
|
||||
@ -138,11 +138,16 @@ export class GalleryShareComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
async get(): Promise<void> {
|
||||
if(Config.Sharing.passwordRequired && !this.input.password){
|
||||
this.url = $localize`Set password.`;
|
||||
return;
|
||||
}
|
||||
this.urlValid = false;
|
||||
this.url = $localize`loading..`;
|
||||
this.sharing = await this.sharingService.createSharing(
|
||||
this.currentDir,
|
||||
this.input.includeSubfolders,
|
||||
this.input.password,
|
||||
this.calcValidity()
|
||||
);
|
||||
this.url = this.sharingService.getUrl(this.sharing);
|
||||
|
@ -7,10 +7,10 @@
|
||||
</div>
|
||||
<div class="row card align-self-center">
|
||||
<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.
|
||||
</div>
|
||||
<form *ngIf="(shareService.currentSharing | async) != shareService.UnknownSharingKey"
|
||||
<form *ngIf="(shareService.sharingIsValid | async)"
|
||||
name="form" id="form" class="form-horizontal" #LoginForm="ngForm" (submit)="onLogin()">
|
||||
<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">
|
||||
<button class="btn btn-primary btn-lg"
|
||||
id="button-share-login"
|
||||
[disabled]="!LoginForm.form.valid || inProgress"
|
||||
type="submit"
|
||||
name="action" i18n>Enter
|
||||
|
@ -75,8 +75,15 @@ describe('PublicRouter', () => {
|
||||
.get('/share/' + sharingKey);
|
||||
};
|
||||
|
||||
it('should not get default user with passworded share share without password', async () => {
|
||||
Config.Sharing.passwordProtected = true;
|
||||
it('should not get default user with passworded share without required password', async () => {
|
||||
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 res = await fistLoad(server, sharing.sharingKey);
|
||||
shouldHaveInjectedUser(res, null);
|
||||
@ -84,25 +91,13 @@ describe('PublicRouter', () => {
|
||||
|
||||
|
||||
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 res = await fistLoad(server, sharing.sharingKey);
|
||||
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));
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
@ -74,15 +74,32 @@ describe('SharingRouter', () => {
|
||||
beforeEach(setUp);
|
||||
afterEach(tearDown);
|
||||
|
||||
it('should login with passworded share', async () => {
|
||||
Config.Sharing.passwordProtected = true;
|
||||
it('should login with passworded share when password required', async () => {
|
||||
Config.Sharing.passwordRequired = true;
|
||||
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 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 () => {
|
||||
Config.Sharing.passwordProtected = true;
|
||||
Config.Sharing.passwordRequired = true;
|
||||
const sharing = await RouteTestingHelper.createSharing(testUser, 'secret_pass');
|
||||
const result = await shareLogin(server, sharing.sharingKey);
|
||||
|
||||
@ -92,22 +109,22 @@ describe('SharingRouter', () => {
|
||||
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 login with no-password share', async () => {
|
||||
Config.Sharing.passwordProtected = true;
|
||||
it('should not login to share without password when password 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));
|
||||
Config.Sharing.passwordRequired = true;
|
||||
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);
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
@ -125,9 +125,9 @@ describe('UserRouter', () => {
|
||||
it('it should authenticate as user with sharing key', async () => {
|
||||
Config.Users.authenticationRequired = 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);
|
||||
@ -146,7 +146,7 @@ describe('UserRouter', () => {
|
||||
it('it should authenticate with sharing key', async () => {
|
||||
Config.Users.authenticationRequired = true;
|
||||
Config.Sharing.enabled = true;
|
||||
Config.Sharing.passwordProtected = true;
|
||||
Config.Sharing.passwordRequired = false;
|
||||
const sharing = (await RouteTestingHelper.createSharing(testUser));
|
||||
|
||||
|
||||
@ -161,7 +161,7 @@ describe('UserRouter', () => {
|
||||
it('it should not authenticate with sharing key without password', async () => {
|
||||
Config.Users.authenticationRequired = true;
|
||||
Config.Sharing.enabled = true;
|
||||
Config.Sharing.passwordProtected = true;
|
||||
Config.Sharing.passwordRequired = true;
|
||||
const sharing = (await RouteTestingHelper.createSharing(testUser, 'pass_secret'));
|
||||
|
||||
|
||||
|
@ -10,7 +10,9 @@ describe('Utils', () => {
|
||||
expect(Utils.concatUrls('abc\\', 'cde')).to.be.equal('abc/cde');
|
||||
expect(Utils.concatUrls('abc/', 'cde/')).to.be.equal('abc/cde');
|
||||
expect(Utils.concatUrls('./abc\\', 'cde/')).to.be.equal('./abc/cde');
|
||||
expect(Utils.concatUrls('/abc\\', 'cde/')).to.be.equal('/abc/cde');
|
||||
expect(Utils.concatUrls('abc/', '\\cde/')).to.be.equal('abc/cde');
|
||||
expect(Utils.concatUrls('/abc/', '\\cde/')).to.be.equal('/abc/cde');
|
||||
expect(Utils.concatUrls('abc\\', '\\cde/')).to.be.equal('abc/cde');
|
||||
expect(Utils.concatUrls('abc\\', '/cde/')).to.be.equal('abc/cde');
|
||||
expect(Utils.concatUrls('abc/', '/cde/')).to.be.equal('abc/cde');
|
||||
|
111
test/cypress/e2e/share.cy.ts
Normal file
111
test/cypress/e2e/share.cy.ts
Normal file
@ -0,0 +1,111 @@
|
||||
describe('Share', () => {
|
||||
beforeEach(() => {
|
||||
cy.viewport(1200, 600);
|
||||
cy.visit('/');
|
||||
cy.get('.card-body');
|
||||
cy.get('.col-sm-12').contains('Login');
|
||||
/* ==== Generated with Cypress Studio ==== */
|
||||
cy.get('#username').type('admin');
|
||||
cy.get('#password').clear();
|
||||
cy.get('#password').type('admin');
|
||||
cy.intercept({
|
||||
method: 'Get',
|
||||
url: '/pgapi/gallery/content/',
|
||||
}).as('getContent');
|
||||
cy.get('.col-sm-12 > .btn').click();
|
||||
});
|
||||
it('Open password protected sharing', () => {
|
||||
cy.wait('@getContent');
|
||||
|
||||
cy.get('button#shareButton').click();
|
||||
cy.get('input#share-password').type('secret', {force: true});
|
||||
cy.get('button#getShareButton').click();
|
||||
|
||||
cy.get('input#shareLink').should('contain.value', 'http');
|
||||
cy.get('input#shareLink')
|
||||
.invoke('val')
|
||||
.then((link: string) => {
|
||||
cy.get('button.btn-close').click();
|
||||
cy.get('button#button-frame-menu').click();
|
||||
cy.get('#dropdown-frame-menu ng-icon[name="ionLogOutOutline"]').click({scrollBehavior: false});
|
||||
|
||||
cy.intercept({
|
||||
method: 'Get',
|
||||
url: '/pgapi/gallery/content/*',
|
||||
}).as('getSharedContent');
|
||||
cy.visit(link);
|
||||
cy.get('input#password').type('secret');
|
||||
cy.get('button#button-share-login').click();
|
||||
|
||||
|
||||
cy.get('.mb-0 > :nth-child(1) > .nav-link').contains('Gallery');
|
||||
|
||||
cy.wait('@getSharedContent').then((interception) => {
|
||||
assert.isNotNull(interception.response.body, '1st API call has data');
|
||||
assert.isNull(interception.response.body?.error, '1st API call has data');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('Open password protected sharing with logged in user', () => {
|
||||
cy.wait('@getContent');
|
||||
|
||||
cy.get('button#shareButton').click();
|
||||
cy.get('input#share-password').type('secret', {force: true});
|
||||
cy.get('button#getShareButton').click();
|
||||
|
||||
cy.get('input#shareLink').should('contain.value', 'http');
|
||||
cy.get('input#shareLink')
|
||||
.invoke('val')
|
||||
.then((link: string) => {
|
||||
|
||||
cy.intercept({
|
||||
method: 'Get',
|
||||
url: '/pgapi/gallery/content/*',
|
||||
}).as('getSharedContent');
|
||||
cy.visit(link);
|
||||
|
||||
|
||||
cy.get('.mb-0 > :nth-child(1) > .nav-link').contains('Gallery');
|
||||
|
||||
cy.wait('@getSharedContent').then((interception) => {
|
||||
assert.isNotNull(interception.response.body, '1st API call has data');
|
||||
assert.isNull(interception.response.body?.error, '1st API call has data');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
it('Open no password sharing', () => {
|
||||
cy.wait('@getContent');
|
||||
|
||||
cy.get('button#shareButton').click();
|
||||
cy.get('button#getShareButton').click();
|
||||
|
||||
cy.get('input#shareLink').should('contain.value', 'http');
|
||||
cy.get('input#shareLink')
|
||||
.invoke('val')
|
||||
.then((link: string) => {
|
||||
cy.get('button.btn-close').click();
|
||||
cy.get('button#button-frame-menu').click();
|
||||
cy.get('#dropdown-frame-menu ng-icon[name="ionLogOutOutline"]').click({scrollBehavior: false});
|
||||
|
||||
|
||||
cy.intercept({
|
||||
method: 'Get',
|
||||
url: '/pgapi/gallery/content/*',
|
||||
}).as('getSharedContent');
|
||||
cy.visit(link);
|
||||
|
||||
|
||||
cy.get('.mb-0 > :nth-child(1) > .nav-link').contains('Gallery');
|
||||
|
||||
cy.wait('@getSharedContent').then((interception) => {
|
||||
assert.isNotNull(interception.response.body, '1st API call has data');
|
||||
assert.isNull(interception.response.body?.error, '1st API call has data');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user