mirror of
https://github.com/xuthus83/pigallery2.git
synced 2025-01-14 14:43:17 +08:00
implementing csrf security for posts
This commit is contained in:
parent
19d3f10d35
commit
5b4f06e789
158
package-lock.json
generated
158
package-lock.json
generated
@ -3105,6 +3105,12 @@
|
||||
"defer-to-connect": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"@types/bcrypt": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-3.0.0.tgz",
|
||||
"integrity": "sha512-nohgNyv+1ViVcubKBh0+XiNJ3dO8nYu///9aJ4cgSqv70gBL+94SNy/iC2NLzKPT2Zt/QavrOkBVbZRLZmw6NQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/bcryptjs": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz",
|
||||
@ -3199,6 +3205,16 @@
|
||||
"@types/serve-static": "*"
|
||||
}
|
||||
},
|
||||
"@types/express-jwt": {
|
||||
"version": "0.0.42",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-jwt/-/express-jwt-0.0.42.tgz",
|
||||
"integrity": "sha512-WszgUddvM1t5dPpJ3LhWNH8kfNN8GPIBrAGxgIYXVCEGx6Bx4A036aAuf/r5WH9DIEdlmp7gHOYvSM6U87B0ag==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/express": "*",
|
||||
"@types/express-unless": "*"
|
||||
}
|
||||
},
|
||||
"@types/express-serve-static-core": {
|
||||
"version": "4.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.0.tgz",
|
||||
@ -3209,6 +3225,15 @@
|
||||
"@types/range-parser": "*"
|
||||
}
|
||||
},
|
||||
"@types/express-unless": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.1.tgz",
|
||||
"integrity": "sha512-5fuvg7C69lemNgl0+v+CUxDYWVPSfXHhJPst4yTLcqi4zKJpORCxnDrnnilk3k0DTq/WrAUdvXFs01+vUqUZHw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/express": "*"
|
||||
}
|
||||
},
|
||||
"@types/fluent-ffmpeg": {
|
||||
"version": "2.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.11.tgz",
|
||||
@ -3973,6 +3998,15 @@
|
||||
"integrity": "sha512-kGCRI9oiCxFS6soGKlyzhMzDydfcPix9PpTkr7h11huxOxhWwP37Tg7DYBaQ18eQTNreZEuLkhpbGSqVNZPnnw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/jsonwebtoken": {
|
||||
"version": "8.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.3.5.tgz",
|
||||
"integrity": "sha512-VGM1gb+LwsQ5EPevvbvdnKncajBdYqNcrvixBif1BsiDQiSF1q+j4bBTvKC6Bt9n2kqNSx+yNTY2TVJ360E7EQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/keygrip": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.1.tgz",
|
||||
@ -5592,6 +5626,11 @@
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz",
|
||||
"integrity": "sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs="
|
||||
},
|
||||
"buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
"integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
|
||||
},
|
||||
"buffer-fill": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
|
||||
@ -7731,6 +7770,14 @@
|
||||
"safer-buffer": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"ecdsa-sig-formatter": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||
"requires": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"editorconfig": {
|
||||
"version": "0.15.3",
|
||||
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz",
|
||||
@ -8419,6 +8466,34 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"express-jwt": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/express-jwt/-/express-jwt-5.3.1.tgz",
|
||||
"integrity": "sha512-1C9RNq0wMp/JvsH/qZMlg3SIPvKu14YkZ4YYv7gJQ1Vq+Dv8LH9tLKenS5vMNth45gTlEUGx+ycp9IHIlaHP/g==",
|
||||
"requires": {
|
||||
"async": "^1.5.0",
|
||||
"express-unless": "^0.3.0",
|
||||
"jsonwebtoken": "^8.1.0",
|
||||
"lodash.set": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"async": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
|
||||
"integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo="
|
||||
},
|
||||
"express-unless": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/express-unless/-/express-unless-0.3.1.tgz",
|
||||
"integrity": "sha1-JVfBRudb65A+LSR/m1ugFFJpbiA="
|
||||
}
|
||||
}
|
||||
},
|
||||
"express-unless": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/express-unless/-/express-unless-0.5.0.tgz",
|
||||
"integrity": "sha1-wuzkd/QVUIkUPbuGnQfFfF62q5s="
|
||||
},
|
||||
"extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
@ -11900,6 +11975,30 @@
|
||||
"integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=",
|
||||
"dev": true
|
||||
},
|
||||
"jsonwebtoken": {
|
||||
"version": "8.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz",
|
||||
"integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==",
|
||||
"requires": {
|
||||
"jws": "^3.2.2",
|
||||
"lodash.includes": "^4.3.0",
|
||||
"lodash.isboolean": "^3.0.3",
|
||||
"lodash.isinteger": "^4.0.4",
|
||||
"lodash.isnumber": "^3.0.3",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.isstring": "^4.0.1",
|
||||
"lodash.once": "^4.0.0",
|
||||
"ms": "^2.1.1",
|
||||
"semver": "^5.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"jsprim": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
|
||||
@ -11979,6 +12078,25 @@
|
||||
"tslib": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"jwa": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
|
||||
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
|
||||
"requires": {
|
||||
"buffer-equal-constant-time": "1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"jws": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
||||
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
|
||||
"requires": {
|
||||
"jwa": "^1.4.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"karma": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/karma/-/karma-4.4.1.tgz",
|
||||
@ -12571,6 +12689,11 @@
|
||||
"lodash._root": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
"integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8="
|
||||
},
|
||||
"lodash.isarguments": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
|
||||
@ -12583,6 +12706,31 @@
|
||||
"integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.isboolean": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||
"integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY="
|
||||
},
|
||||
"lodash.isinteger": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||
"integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M="
|
||||
},
|
||||
"lodash.isnumber": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||
"integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w="
|
||||
},
|
||||
"lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
|
||||
},
|
||||
"lodash.isstring": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||
"integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE="
|
||||
},
|
||||
"lodash.keys": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz",
|
||||
@ -12594,12 +12742,22 @@
|
||||
"lodash.isarray": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"lodash.once": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||
"integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
|
||||
},
|
||||
"lodash.restparam": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz",
|
||||
"integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.set": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
|
||||
"integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM="
|
||||
},
|
||||
"lodash.template": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz",
|
||||
|
@ -35,6 +35,7 @@
|
||||
"ejs": "3.0.1",
|
||||
"exifreader": "2.12.0",
|
||||
"express": "4.17.1",
|
||||
"express-unless": "0.5.0",
|
||||
"fluent-ffmpeg": "2.1.2",
|
||||
"image-size": "0.8.3",
|
||||
"jimp": "0.9.3",
|
||||
@ -64,19 +65,22 @@
|
||||
"@angular/platform-browser-dynamic": "8.2.14",
|
||||
"@angular/router": "8.2.14",
|
||||
"@ngx-translate/i18n-polyfill": "1.0.0",
|
||||
"@types/bcrypt": "^3.0.0",
|
||||
"@types/bcryptjs": "2.4.2",
|
||||
"@types/chai": "4.2.6",
|
||||
"@types/cookie-parser": "1.4.2",
|
||||
"@types/cookie-session": "2.0.37",
|
||||
"@types/csurf": "1.9.36",
|
||||
"@types/csurf": "^1.9.36",
|
||||
"@types/ejs": "3.0.0",
|
||||
"@types/express": "4.17.2",
|
||||
"@types/express-jwt": "0.0.42",
|
||||
"@types/fluent-ffmpeg": "2.1.11",
|
||||
"@types/gm": "1.18.6",
|
||||
"@types/gulp": "4.0.6",
|
||||
"@types/gulp-zip": "4.0.1",
|
||||
"@types/image-size": "0.8.0",
|
||||
"@types/jasmine": "3.5.0",
|
||||
"@types/jsonwebtoken": "8.3.5",
|
||||
"@types/node": "12.12.14",
|
||||
"@types/rimraf": "2.0.3",
|
||||
"@types/sharp": "0.23.1",
|
||||
|
@ -24,7 +24,6 @@ export class GalleryMWs {
|
||||
public static async listDirectory(req: Request, res: Response, next: NextFunction) {
|
||||
const directoryName = req.params.directory || '/';
|
||||
const absoluteDirectoryName = path.join(ProjectPath.ImageFolder, directoryName);
|
||||
|
||||
try {
|
||||
if ((await fsp.stat(absoluteDirectoryName)).isDirectory() === false) {
|
||||
return next();
|
||||
|
@ -1,11 +1,9 @@
|
||||
import {NextFunction, Request, Response} from 'express';
|
||||
import {ErrorCodes, ErrorDTO} from '../../common/entities/Error';
|
||||
import {Utils} from '../../common/Utils';
|
||||
import {Message} from '../../common/entities/Message';
|
||||
import {SharingDTO} from '../../common/entities/SharingDTO';
|
||||
import {Config} from '../../common/config/private/Config';
|
||||
import {ConfigClass} from '../../common/config/private/ConfigClass';
|
||||
import {UserRoles} from '../../common/entities/UserDTO';
|
||||
import {UserDTO, UserRoles} from '../../common/entities/UserDTO';
|
||||
import {NotificationManager} from '../model/NotifocationManager';
|
||||
import {Logger} from '../Logger';
|
||||
|
||||
@ -25,8 +23,19 @@ export class RenderingMWs {
|
||||
return next(new ErrorDTO(ErrorCodes.GENERAL_ERROR, 'User not exists'));
|
||||
}
|
||||
|
||||
const user = Utils.clone(req.session.user);
|
||||
delete user.password;
|
||||
const user = <UserDTO>{
|
||||
id: req.session.user.id,
|
||||
name: req.session.user.name,
|
||||
csrfToken: req.session.user.csrfToken || req.csrfToken(),
|
||||
role: req.session.user.role,
|
||||
usedSharingKey: req.session.user.usedSharingKey,
|
||||
permissions: req.session.user.permissions
|
||||
};
|
||||
|
||||
if (!user.csrfToken && req.csrfToken) {
|
||||
user.csrfToken = req.csrfToken();
|
||||
}
|
||||
|
||||
RenderingMWs.renderMessage(res, user);
|
||||
}
|
||||
|
||||
@ -35,8 +44,7 @@ export class RenderingMWs {
|
||||
return next();
|
||||
}
|
||||
|
||||
const sharing = Utils.clone<SharingDTO>(req.resultPipe);
|
||||
delete sharing.password;
|
||||
const {password, creator, ...sharing} = req.resultPipe;
|
||||
RenderingMWs.renderMessage(res, sharing);
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,7 @@ export class SharingMWs {
|
||||
if (Config.Client.Sharing.enabled === false) {
|
||||
return next();
|
||||
}
|
||||
const sharingKey = req.params[QueryParams.gallery.sharingKey_long];
|
||||
const sharingKey = req.params[QueryParams.gallery.sharingKey_params];
|
||||
|
||||
try {
|
||||
req.resultPipe = await ObjectManagers.getInstance().SharingManager.findOne({sharingKey: sharingKey});
|
||||
@ -37,7 +37,6 @@ export class SharingMWs {
|
||||
let sharingKey = SharingMWs.generateKey();
|
||||
|
||||
// create one not yet used
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
await ObjectManagers.getInstance().SharingManager.findOne({sharingKey: sharingKey});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {LoginCredential} from '../../../common/entities/LoginCredential';
|
||||
import {UserEntity} from '../../model/database/sql/enitites/UserEntity';
|
||||
import {UserDTO} from '../../../common/entities/UserDTO';
|
||||
|
||||
|
||||
declare global {
|
||||
@ -18,7 +18,7 @@ declare global {
|
||||
}
|
||||
|
||||
interface Session {
|
||||
user?: UserEntity;
|
||||
user?: UserDTO;
|
||||
rememberMe?: boolean;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
///<reference path="../customtypings/ExtendedRequest.d.ts"/>
|
||||
import {NextFunction, Request, Response} from 'express';
|
||||
import {ErrorCodes, ErrorDTO} from '../../../common/entities/Error';
|
||||
import {UserDTO, UserRoles} from '../../../common/entities/UserDTO';
|
||||
@ -30,11 +29,16 @@ export class AuthenticationMWs {
|
||||
}
|
||||
|
||||
public static async authenticate(req: Request, res: Response, next: NextFunction) {
|
||||
|
||||
if (Config.Client.authenticationRequired === false) {
|
||||
req.session.user = <UserDTO>{name: UserRoles[Config.Client.unAuthenticatedUserRole], role: Config.Client.unAuthenticatedUserRole};
|
||||
return next();
|
||||
}
|
||||
|
||||
// if already authenticated, do not try to use sharing authentication
|
||||
if (typeof req.session.user !== 'undefined') {
|
||||
return next();
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await AuthenticationMWs.getSharingUser(req);
|
||||
if (!!user) {
|
||||
@ -45,12 +49,8 @@ export class AuthenticationMWs {
|
||||
return next(new ErrorDTO(ErrorCodes.CREDENTIAL_NOT_FOUND, null, err));
|
||||
}
|
||||
if (typeof req.session.user === 'undefined') {
|
||||
return next(new ErrorDTO(ErrorCodes.NOT_AUTHENTICATED));
|
||||
}
|
||||
if (req.session.rememberMe === true) {
|
||||
req.sessionOptions.expires = new Date(Date.now() + Config.Server.sessionTimeout);
|
||||
} else {
|
||||
delete (req.sessionOptions.expires);
|
||||
res.status(401);
|
||||
return next(new ErrorDTO(ErrorCodes.NOT_AUTHENTICATED, 'Not authenticated'));
|
||||
}
|
||||
return next();
|
||||
}
|
||||
@ -70,7 +70,7 @@ export class AuthenticationMWs {
|
||||
p = path.dirname(p);
|
||||
}
|
||||
|
||||
if (!UserDTO.isDirectoryPathAvailable(p, req.session.user.permissions, path.sep)) {
|
||||
if (!UserDTO.isDirectoryPathAvailable(p, req.session.user.permissions)) {
|
||||
return res.sendStatus(403);
|
||||
}
|
||||
|
||||
@ -94,21 +94,22 @@ export class AuthenticationMWs {
|
||||
return next();
|
||||
}
|
||||
// not enough parameter
|
||||
if ((!req.query[QueryParams.gallery.sharingKey_short] && !req.params[QueryParams.gallery.sharingKey_long])) {
|
||||
if ((!req.query[QueryParams.gallery.sharingKey_query] && !req.params[QueryParams.gallery.sharingKey_params])) {
|
||||
return next(new ErrorDTO(ErrorCodes.INPUT_ERROR, 'no sharing key provided'));
|
||||
}
|
||||
|
||||
try {
|
||||
const password = (req.body ? req.body.password : null) || null;
|
||||
|
||||
const sharingKey: string = req.query[QueryParams.gallery.sharingKey_query] || req.params[QueryParams.gallery.sharingKey_params];
|
||||
const sharing = await ObjectManagers.getInstance().SharingManager.findOne({
|
||||
sharingKey: req.query[QueryParams.gallery.sharingKey_short] || req.params[QueryParams.gallery.sharingKey_long]
|
||||
sharingKey: sharingKey
|
||||
});
|
||||
|
||||
if (!sharing || sharing.expires < Date.now() ||
|
||||
(Config.Client.Sharing.passwordProtected === true
|
||||
&& (sharing.password)
|
||||
&& !PasswordHelper.comparePassword(password, sharing.password))) {
|
||||
res.status(401);
|
||||
return next(new ErrorDTO(ErrorCodes.CREDENTIAL_NOT_FOUND));
|
||||
}
|
||||
|
||||
@ -117,7 +118,12 @@ export class AuthenticationMWs {
|
||||
sharingPath += '*';
|
||||
}
|
||||
|
||||
req.session.user = <UserDTO>{name: 'Guest', role: UserRoles.LimitedGuest, permissions: [sharingPath]};
|
||||
req.session.user = <UserDTO>{
|
||||
name: 'Guest',
|
||||
role: UserRoles.LimitedGuest,
|
||||
permissions: [sharingPath],
|
||||
usedSharingKey: sharing.sharingKey
|
||||
};
|
||||
return next();
|
||||
|
||||
} catch (err) {
|
||||
@ -135,12 +141,16 @@ export class AuthenticationMWs {
|
||||
|
||||
public static async login(req: Request, res: Response, next: NextFunction) {
|
||||
|
||||
if (Config.Client.authenticationRequired === false) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
|
||||
// not enough parameter
|
||||
if ((typeof req.body === 'undefined') ||
|
||||
(typeof req.body.loginCredential === 'undefined') ||
|
||||
(typeof req.body.loginCredential.username === 'undefined') ||
|
||||
(typeof req.body.loginCredential.password === 'undefined')) {
|
||||
return next(new ErrorDTO(ErrorCodes.INPUT_ERROR, 'not all parameters are included, got' + JSON.stringify(req.body)));
|
||||
return next(new ErrorDTO(ErrorCodes.INPUT_ERROR, 'not all parameters are included for loginCredential'));
|
||||
}
|
||||
try {
|
||||
// lets find the user
|
||||
@ -156,7 +166,8 @@ export class AuthenticationMWs {
|
||||
return next();
|
||||
|
||||
} catch (err) {
|
||||
return next(new ErrorDTO(ErrorCodes.CREDENTIAL_NOT_FOUND));
|
||||
console.error(err);
|
||||
return next(new ErrorDTO(ErrorCodes.CREDENTIAL_NOT_FOUND, 'credentials not found during login'));
|
||||
}
|
||||
|
||||
|
||||
@ -164,21 +175,21 @@ export class AuthenticationMWs {
|
||||
|
||||
public static logout(req: Request, res: Response, next: NextFunction) {
|
||||
delete req.session.user;
|
||||
delete req.session.rememberMe;
|
||||
return next();
|
||||
}
|
||||
|
||||
private static async getSharingUser(req: Request) {
|
||||
if (Config.Client.Sharing.enabled === true &&
|
||||
(!!req.params[QueryParams.gallery.sharingKey_short] || !!req.params[QueryParams.gallery.sharingKey_long])) {
|
||||
(!!req.query[QueryParams.gallery.sharingKey_query] || !!req.params[QueryParams.gallery.sharingKey_params])) {
|
||||
const sharingKey: string = req.query[QueryParams.gallery.sharingKey_query] || req.params[QueryParams.gallery.sharingKey_params];
|
||||
const sharing = await ObjectManagers.getInstance().SharingManager.findOne({
|
||||
sharingKey: req.query[QueryParams.gallery.sharingKey_short] || req.params[QueryParams.gallery.sharingKey_long],
|
||||
sharingKey: sharingKey
|
||||
});
|
||||
if (!sharing || sharing.expires < Date.now()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Config.Client.Sharing.passwordProtected === true && (sharing.password)) {
|
||||
if (Config.Client.Sharing.passwordProtected === true && sharing.password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@ export class UserRequestConstrainsMWs {
|
||||
if ((typeof req.params === 'undefined') || (typeof req.params.id === 'undefined')) {
|
||||
return next();
|
||||
}
|
||||
if (req.session.user.id !== req.params.id) {
|
||||
if (req.session.user.id !== parseInt(req.params.id, 10)) {
|
||||
return next(new ErrorDTO(ErrorCodes.NOT_AUTHORISED));
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ export class UserRequestConstrainsMWs {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (req.session.user.id === req.params.id) {
|
||||
if (req.session.user.id === parseInt(req.params.id, 10)) {
|
||||
return next(new ErrorDTO(ErrorCodes.NOT_AUTHORISED));
|
||||
}
|
||||
|
||||
@ -34,7 +34,7 @@ export class UserRequestConstrainsMWs {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (req.session.user.id !== req.params.id) {
|
||||
if (req.session.user.id !== parseInt(req.params.id, 10)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
|
@ -6,12 +6,16 @@ try {
|
||||
}
|
||||
|
||||
export class PasswordHelper {
|
||||
public static cryptPassword(password: string) {
|
||||
public static cryptPassword(password: string): string {
|
||||
const salt = bcrypt.genSaltSync(9);
|
||||
return bcrypt.hashSync(password, salt);
|
||||
}
|
||||
|
||||
public static comparePassword(password: string, encryptedPassword: string) {
|
||||
public static comparePassword(password: string, encryptedPassword: string): boolean {
|
||||
try {
|
||||
return bcrypt.compareSync(password, encryptedPassword);
|
||||
} catch (e) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,6 @@ export class UserManager implements IUserManager {
|
||||
delete filter.password;
|
||||
const user = (await connection.getRepository(UserEntity).findOne(filter));
|
||||
|
||||
|
||||
if (pass && !PasswordHelper.comparePassword(pass, user.password)) {
|
||||
throw new Error('No entry found');
|
||||
}
|
||||
|
@ -15,6 +15,6 @@ export class FileEntity implements FileDTO {
|
||||
name: string;
|
||||
|
||||
@Index()
|
||||
@ManyToOne(type => DirectoryEntity, directory => directory.metaFile, {onDelete: 'CASCADE'})
|
||||
@ManyToOne(type => DirectoryEntity, directory => directory.metaFile, {onDelete: 'CASCADE', nullable: false})
|
||||
directory: DirectoryEntity;
|
||||
}
|
||||
|
@ -71,7 +71,7 @@ export abstract class MediaEntity implements MediaDTO {
|
||||
name: string;
|
||||
|
||||
@Index()
|
||||
@ManyToOne(type => DirectoryEntity, directory => directory.media, {onDelete: 'CASCADE'})
|
||||
@ManyToOne(type => DirectoryEntity, directory => directory.media, {onDelete: 'CASCADE', nullable: false})
|
||||
directory: DirectoryEntity;
|
||||
|
||||
@Column(type => MediaMetadataEntity)
|
||||
|
@ -36,6 +36,6 @@ export class SharingEntity implements SharingDTO {
|
||||
@Column()
|
||||
includeSubfolders: boolean;
|
||||
|
||||
@ManyToOne(type => UserEntity)
|
||||
@ManyToOne(type => UserEntity, {onDelete: 'CASCADE', nullable: false})
|
||||
creator: UserDTO;
|
||||
}
|
||||
|
@ -18,10 +18,17 @@ export class ErrorRouter {
|
||||
|
||||
private static addGenericHandler(app: Express) {
|
||||
app.use((err: any, req: Request, res: Response, next: Function) => {
|
||||
|
||||
if (err.name === 'UnauthorizedError') {
|
||||
// jwt authentication error
|
||||
res.status(401);
|
||||
return next(new ErrorDTO(ErrorCodes.NOT_AUTHENTICATED, 'Invalid token'));
|
||||
}
|
||||
|
||||
// Flush out the stack to the console
|
||||
Logger.error('Unexpected error:');
|
||||
console.error(err);
|
||||
next(new ErrorDTO(ErrorCodes.SERVER_ERROR, 'Unknown server side error', err));
|
||||
return next(new ErrorDTO(ErrorCodes.SERVER_ERROR, 'Unknown server side error', err));
|
||||
},
|
||||
RenderingMWs.renderError
|
||||
);
|
||||
|
@ -2,12 +2,12 @@ import {Express, NextFunction, Request, Response} from 'express';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as ejs from 'ejs';
|
||||
import {Utils} from '../../common/Utils';
|
||||
import {Config} from '../../common/config/private/Config';
|
||||
import {ProjectPath} from '../ProjectPath';
|
||||
import {AuthenticationMWs} from '../middlewares/user/AuthenticationMWs';
|
||||
import {CookieNames} from '../../common/CookieNames';
|
||||
import {ErrorCodes, ErrorDTO} from '../../common/entities/Error';
|
||||
import {UserDTO} from '../../common/entities/UserDTO';
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
@ -69,9 +69,18 @@ export class PublicRouter {
|
||||
|
||||
res.tpl.user = null;
|
||||
if (req.session.user) {
|
||||
const user = Utils.clone(req.session.user);
|
||||
delete user.password;
|
||||
res.tpl.user = user;
|
||||
res.tpl.user = <UserDTO>{
|
||||
id: req.session.user.id,
|
||||
name: req.session.user.name,
|
||||
csrfToken: req.session.user.csrfToken,
|
||||
role: req.session.user.role,
|
||||
usedSharingKey: req.session.user.usedSharingKey,
|
||||
permissions: req.session.user.permissions
|
||||
};
|
||||
|
||||
if (!res.tpl.user.csrfToken && req.csrfToken) {
|
||||
res.tpl.user.csrfToken = req.csrfToken();
|
||||
}
|
||||
}
|
||||
res.tpl.clientConfig = Config.Client;
|
||||
|
||||
|
@ -23,7 +23,7 @@ export class SharingRouter {
|
||||
}
|
||||
|
||||
private static addGetSharing(app: express.Express) {
|
||||
app.get('/api/share/:' + QueryParams.gallery.sharingKey_long,
|
||||
app.get('/api/share/:' + QueryParams.gallery.sharingKey_params,
|
||||
AuthenticationMWs.authenticate,
|
||||
AuthenticationMWs.authorise(UserRoles.LimitedGuest),
|
||||
SharingMWs.getSharing,
|
||||
|
@ -36,7 +36,7 @@ export class UserRouter {
|
||||
|
||||
|
||||
private static addGetSessionUser(app: Express) {
|
||||
app.get('/api/user/login',
|
||||
app.get('/api/user/me',
|
||||
AuthenticationMWs.authenticate,
|
||||
RenderingMWs.renderSessionUser
|
||||
);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import * as _express from 'express';
|
||||
import {Request} from 'express';
|
||||
import * as _bodyParser from 'body-parser';
|
||||
import * as cookieParser from 'cookie-parser';
|
||||
import * as _http from 'http';
|
||||
@ -17,7 +18,9 @@ import {Router} from './routes/Router';
|
||||
import {ServerConfig} from '../common/config/private/IPrivateConfig';
|
||||
import {PhotoProcessing} from './model/fileprocessing/PhotoProcessing';
|
||||
import * as _csrf from 'csurf';
|
||||
import * as unless from 'express-unless';
|
||||
import {Event} from '../common/event/Event';
|
||||
import {QueryParams} from '../common/QueryParams';
|
||||
|
||||
const _session = require('cookie-session');
|
||||
|
||||
@ -75,7 +78,18 @@ export class Server {
|
||||
// for parsing application/json
|
||||
this.app.use(_bodyParser.json());
|
||||
this.app.use(cookieParser());
|
||||
// this.app.use(_csrf({cookie: true}));
|
||||
const csuf: any = _csrf();
|
||||
csuf.unless = unless;
|
||||
this.app.use(csuf.unless((req: Request) => {
|
||||
return Config.Client.authenticationRequired === false ||
|
||||
['/api/user/login', '/api/user/logout', '/api/share/login'].indexOf(req.originalUrl) !== -1 ||
|
||||
(Config.Client.Sharing.enabled === true && !!req.query[QueryParams.gallery.sharingKey_query]);
|
||||
}));
|
||||
|
||||
// enable token generation but do not check it
|
||||
this.app.post(['/api/user/login', '/api/share/login'], _csrf({ignoreMethods: ['POST']}));
|
||||
this.app.get(['/api/user/me', '/api/share/:' + QueryParams.gallery.sharingKey_params], _csrf({ignoreMethods: ['GET']}));
|
||||
|
||||
|
||||
DiskManager.init();
|
||||
PhotoProcessing.init();
|
||||
|
@ -1 +1 @@
|
||||
export const DataStructureVersion = 15;
|
||||
export const DataStructureVersion = 16;
|
||||
|
@ -13,11 +13,11 @@ export const QueryParams = {
|
||||
type: 'type'
|
||||
},
|
||||
photo: 'p',
|
||||
sharingKey_short: 'sk',
|
||||
sharingKey_long: 'sharingKey',
|
||||
sharingKey_query: 'sk',
|
||||
sharingKey_params: 'sharingKey',
|
||||
searchText: 'searchText',
|
||||
directory: 'directory',
|
||||
knownLastModified: 'knownLastModified',
|
||||
knownLastScanned: 'knownLastScanned'
|
||||
knownLastModified: 'klm',
|
||||
knownLastScanned: 'kls'
|
||||
}
|
||||
};
|
||||
|
@ -93,6 +93,12 @@ export class Utils {
|
||||
return arr;
|
||||
}
|
||||
|
||||
public static canonizePath(path: string) {
|
||||
return path
|
||||
.replace(new RegExp('\\\\', 'g'), '/')
|
||||
.replace(new RegExp('/+', 'g'), '/');
|
||||
}
|
||||
|
||||
static concatUrls(...args: Array<string>) {
|
||||
let url = '';
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
|
@ -122,7 +122,7 @@ export module ServerConfig {
|
||||
Threading: ThreadingConfig;
|
||||
Database: DataBaseConfig;
|
||||
Sharing: SharingConfig;
|
||||
sessionTimeout: number;
|
||||
sessionTimeout: number; // in ms
|
||||
Indexing: IndexingConfig;
|
||||
photoMetadataSize: number; // only this many bites will be loaded when scanning photo for metadata
|
||||
Duplicates: DuplicatesConfig;
|
||||
|
@ -27,7 +27,7 @@ export class ErrorDTO {
|
||||
public detailsStr: string;
|
||||
|
||||
constructor(public code: ErrorCodes, public message?: string, public details?: any) {
|
||||
this.detailsStr = (this.details ? this.details.toString() : '');
|
||||
this.detailsStr = (this.details ? this.details.toString() : '') || ErrorCodes[code];
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
|
@ -15,29 +15,35 @@ export interface UserDTO {
|
||||
name: string;
|
||||
password: string;
|
||||
role: UserRoles;
|
||||
csrfToken?: string;
|
||||
usedSharingKey?: string;
|
||||
permissions: string[]; // user can only see these permissions. if ends with *, its recursive
|
||||
}
|
||||
|
||||
export module UserDTO {
|
||||
|
||||
export const isDirectoryPathAvailable = (path: string, permissions: string[], separator = '/'): boolean => {
|
||||
if (permissions == null || permissions.length === 0 || permissions[0] === separator + '*') {
|
||||
export const isDirectoryPathAvailable = (path: string, permissions: string[]): boolean => {
|
||||
if (permissions == null) {
|
||||
return true;
|
||||
}
|
||||
permissions = permissions.map(p => Utils.canonizePath(p));
|
||||
path = Utils.canonizePath(path);
|
||||
if (permissions.length === 0 || permissions[0] === '/*') {
|
||||
return true;
|
||||
}
|
||||
for (let i = 0; i < permissions.length; i++) {
|
||||
let permission = permissions[i];
|
||||
if (permissions[i] === separator + '*') {
|
||||
if (permissions[i] === '/*') {
|
||||
return true;
|
||||
}
|
||||
if (permission[permission.length - 1] === '*') {
|
||||
permission = permission.slice(0, -1);
|
||||
if (path.startsWith(permission) && (!path[permission.length] || path[permission.length] === separator)) {
|
||||
if (path.startsWith(permission) && (!path[permission.length] || path[permission.length] === '/')) {
|
||||
return true;
|
||||
}
|
||||
} else if (path === permission) {
|
||||
return true;
|
||||
} else if (path === '.' && permission === separator) {
|
||||
} else if (path === '.' && permission === '/') {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -46,6 +52,7 @@ export module UserDTO {
|
||||
};
|
||||
|
||||
export const isDirectoryAvailable = (directory: DirectoryDTO, permissions: string[]): boolean => {
|
||||
return isDirectoryPathAvailable(Utils.concatUrls(directory.path, directory.name), permissions);
|
||||
return isDirectoryPathAvailable(
|
||||
Utils.concatUrls(directory.path, directory.name), permissions);
|
||||
};
|
||||
}
|
||||
|
@ -6,10 +6,12 @@ import {Title} from '@angular/platform-browser';
|
||||
import {ShareService} from './ui/gallery/share.service';
|
||||
import 'hammerjs';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {QueryParams} from '../../common/QueryParams';
|
||||
|
||||
@Component({
|
||||
selector: 'app-pi-gallery2',
|
||||
template: `<router-outlet></router-outlet>`
|
||||
template: `
|
||||
<router-outlet></router-outlet>`
|
||||
})
|
||||
export class AppComponent implements OnInit, OnDestroy {
|
||||
|
||||
@ -51,7 +53,9 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
|
||||
private toLogin() {
|
||||
if (this._shareService.isSharing()) {
|
||||
return this._router.navigate(['shareLogin'], {queryParams: {sk: this._shareService.getSharingKey()}});
|
||||
const q: any = {};
|
||||
q[QueryParams.gallery.sharingKey_query] = this._shareService.getSharingKey();
|
||||
return this._router.navigate(['shareLogin'], {queryParams: q});
|
||||
} else {
|
||||
return this._router.navigate(['login']);
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ import {SettingsService} from './ui/settings/settings.service';
|
||||
import {ShareSettingsComponent} from './ui/settings/share/share.settings.component';
|
||||
import {BasicSettingsComponent} from './ui/settings/basic/basic.settings.component';
|
||||
import {OtherSettingsComponent} from './ui/settings/other/other.settings.component';
|
||||
import {HttpClientModule} from '@angular/common/http';
|
||||
import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http';
|
||||
import {DefaultUrlSerializer, UrlSerializer, UrlTree} from '@angular/router';
|
||||
import {IndexingSettingsComponent} from './ui/settings/indexing/indexing.settings.component';
|
||||
import {LanguageComponent} from './ui/language/language.component';
|
||||
@ -90,6 +90,8 @@ import {JobsSettingsComponent} from './ui/settings/jobs/jobs.settings.component'
|
||||
import {ScheduledJobsService} from './ui/settings/scheduled-jobs.service';
|
||||
import {BackendtextService} from './model/backendtext.service';
|
||||
import {JobButtonComponent} from './ui/settings/jobs/button/job-button.settings.component';
|
||||
import {ErrorInterceptor} from './model/network/helper/error.interceptor';
|
||||
import {CSRFInterceptor} from './model/network/helper/csrf.interceptor';
|
||||
|
||||
|
||||
@Injectable()
|
||||
@ -212,6 +214,8 @@ export function translationsFactory(locale: string) {
|
||||
FileSizePipe
|
||||
],
|
||||
providers: [
|
||||
{provide: HTTP_INTERCEPTORS, useClass: CSRFInterceptor, multi: true},
|
||||
{provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true},
|
||||
{provide: UrlSerializer, useClass: CustomUrlSerializer},
|
||||
{provide: HAMMER_GESTURE_CONFIG, useClass: MyHammerConfig},
|
||||
NetworkService,
|
||||
|
@ -7,6 +7,7 @@ import {ShareLoginComponent} from './ui/sharelogin/share-login.component';
|
||||
import {QueryParams} from '../../common/QueryParams';
|
||||
import {DuplicateComponent} from './ui/duplicates/duplicates.component';
|
||||
import {FacesComponent} from './ui/faces/faces.component';
|
||||
import {AuthGuard} from './model/network/helper/auth.guard';
|
||||
|
||||
export function galleryMatcherFunction(
|
||||
segments: UrlSegment[]): UrlMatchResult | null {
|
||||
@ -32,13 +33,13 @@ export function galleryMatcherFunction(
|
||||
}
|
||||
if (path === 'share') {
|
||||
if (segments.length > 1) {
|
||||
posParams[QueryParams.gallery.sharingKey_long] = segments[1];
|
||||
posParams[QueryParams.gallery.sharingKey_params] = segments[1];
|
||||
}
|
||||
return {consumed: segments.slice(0, Math.min(segments.length, 2)), posParams};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// Todo: authguard - canActivate https://angular.io/api/router/CanActivate
|
||||
|
||||
const ROUTES: Routes = [
|
||||
{
|
||||
path: 'login',
|
||||
@ -50,19 +51,23 @@ const ROUTES: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'admin',
|
||||
component: AdminComponent
|
||||
component: AdminComponent,
|
||||
canActivate: [AuthGuard]
|
||||
},
|
||||
{
|
||||
path: 'duplicates',
|
||||
component: DuplicateComponent
|
||||
component: DuplicateComponent,
|
||||
canActivate: [AuthGuard]
|
||||
},
|
||||
{
|
||||
path: 'faces',
|
||||
component: FacesComponent
|
||||
component: FacesComponent,
|
||||
canActivate: [AuthGuard]
|
||||
},
|
||||
{
|
||||
matcher: galleryMatcherFunction,
|
||||
component: GalleryComponent
|
||||
component: GalleryComponent,
|
||||
canActivate: [AuthGuard]
|
||||
},
|
||||
{path: '', redirectTo: '/login', pathMatch: 'full'},
|
||||
{path: '**', redirectTo: '/login', pathMatch: 'full'}
|
||||
|
@ -14,16 +14,16 @@ declare module ServerInject {
|
||||
export let user: UserDTO;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class AuthenticationService {
|
||||
|
||||
public user: BehaviorSubject<UserDTO>;
|
||||
public readonly user: BehaviorSubject<UserDTO>;
|
||||
|
||||
constructor(private _userService: UserService,
|
||||
private _networkService: NetworkService,
|
||||
private shareService: ShareService) {
|
||||
this.user = new BehaviorSubject(null);
|
||||
this.shareService.setUserObs(this.user);
|
||||
this.user = new BehaviorSubject(JSON.parse(localStorage.getItem('currentUser')));
|
||||
|
||||
// picking up session..
|
||||
if (this.isAuthenticated() === false && Cookie.get(CookieNames.session) != null) {
|
||||
if (typeof ServerInject !== 'undefined' && typeof ServerInject.user !== 'undefined') {
|
||||
@ -48,6 +48,17 @@ export class AuthenticationService {
|
||||
return false;
|
||||
});
|
||||
|
||||
// TODO: refactor architecture remove shareService dependency
|
||||
window.setTimeout(() => {
|
||||
this.user.subscribe((u) => {
|
||||
this.shareService.onNewUser(u);
|
||||
if (u !== null) {
|
||||
localStorage.setItem('currentUser', JSON.stringify(u));
|
||||
} else {
|
||||
localStorage.removeItem('currentUser');
|
||||
}
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
public async login(credential: LoginCredential): Promise<UserDTO> {
|
||||
@ -82,9 +93,8 @@ export class AuthenticationService {
|
||||
try {
|
||||
this.user.next(await this._userService.getSessionUser());
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
21
src/frontend/app/model/network/helper/auth.guard.ts
Normal file
21
src/frontend/app/model/network/helper/auth.guard.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router';
|
||||
import {AuthenticationService} from '../authentication.service';
|
||||
import {NavigationService} from '../../navigation.service';
|
||||
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class AuthGuard implements CanActivate {
|
||||
constructor(private authenticationService: AuthenticationService,
|
||||
private navigationService: NavigationService) {
|
||||
}
|
||||
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||
if (this.authenticationService.isAuthenticated() === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this.navigationService.toLogin().catch(console.error);
|
||||
return false;
|
||||
}
|
||||
}
|
24
src/frontend/app/model/network/helper/csrf.interceptor.ts
Normal file
24
src/frontend/app/model/network/helper/csrf.interceptor.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
|
||||
import {Observable} from 'rxjs';
|
||||
import {AuthenticationService} from '../authentication.service';
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class CSRFInterceptor implements HttpInterceptor {
|
||||
constructor(private authenticationService: AuthenticationService) {
|
||||
}
|
||||
|
||||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
// add authorization header with jwt token if available
|
||||
const currentUser = this.authenticationService.user.value;
|
||||
if (currentUser && currentUser.csrfToken) {
|
||||
request = request.clone({
|
||||
setHeaders: {
|
||||
'CSRF-Token': `${currentUser.csrfToken}`
|
||||
}
|
||||
});
|
||||
}
|
||||
return next.handle(request);
|
||||
}
|
||||
}
|
23
src/frontend/app/model/network/helper/error.interceptor.ts
Normal file
23
src/frontend/app/model/network/helper/error.interceptor.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
|
||||
import {Observable, throwError} from 'rxjs';
|
||||
import {catchError} from 'rxjs/operators';
|
||||
import {AuthenticationService} from '../authentication.service';
|
||||
|
||||
@Injectable()
|
||||
export class ErrorInterceptor implements HttpInterceptor {
|
||||
constructor(private authenticationService: AuthenticationService) {
|
||||
}
|
||||
|
||||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
return next.handle(request).pipe(catchError(err => {
|
||||
if (err.status === 401) {
|
||||
// auto logout if 401 response returned from api
|
||||
this.authenticationService.logout();
|
||||
}
|
||||
|
||||
const error = err.error.message || err.statusText;
|
||||
return throwError(error);
|
||||
}));
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ import {VersionService} from '../version.service';
|
||||
@Injectable()
|
||||
export class NetworkService {
|
||||
|
||||
_apiBaseUrl = Utils.concatUrls(Config.Client.urlBase, '/api');
|
||||
readonly _apiBaseUrl = Utils.concatUrls(Config.Client.urlBase, '/api');
|
||||
private globalErrorHandlers: Array<(error: ErrorDTO) => boolean> = [];
|
||||
|
||||
constructor(private _http: HttpClient,
|
||||
@ -67,8 +67,8 @@ export class NetworkService {
|
||||
return this.callJson('put', url, data);
|
||||
}
|
||||
|
||||
public getJson<T>(url: string, data?: { [key: string]: any }): Promise<T> {
|
||||
return this.callJson('get', NetworkService.buildUrl(url, data));
|
||||
public getJson<T>(url: string, query?: { [key: string]: any }): Promise<T> {
|
||||
return this.callJson('get', NetworkService.buildUrl(url, query));
|
||||
}
|
||||
|
||||
public deleteJson<T>(url: string): Promise<T> {
|
||||
|
@ -4,35 +4,38 @@ import {NetworkService} from './network.service';
|
||||
import {UserDTO} from '../../../../common/entities/UserDTO';
|
||||
import {Config} from '../../../../common/config/public/Config';
|
||||
import {ShareService} from '../../ui/gallery/share.service';
|
||||
import {QueryParams} from '../../../../common/QueryParams';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
|
||||
// Todo use JWT instead of costume cookie
|
||||
constructor(private _networkService: NetworkService,
|
||||
private _shareService: ShareService) {
|
||||
}
|
||||
|
||||
public logout(): Promise<string> {
|
||||
public async logout(): Promise<string> {
|
||||
return this._networkService.postJson('/user/logout');
|
||||
}
|
||||
|
||||
public login(credential: LoginCredential): Promise<UserDTO> {
|
||||
return this._networkService.postJson<UserDTO>('/user/login', {'loginCredential': credential});
|
||||
public async login(credential: LoginCredential): Promise<UserDTO> {
|
||||
return this._networkService.postJson<UserDTO>('/user/login', {loginCredential: credential});
|
||||
}
|
||||
|
||||
public async shareLogin(password: string): Promise<UserDTO> {
|
||||
return this._networkService.postJson<UserDTO>('/share/login?sk=' + this._shareService.getSharingKey(), {'password': password});
|
||||
return this._networkService.postJson<UserDTO>('/share/login?' + QueryParams.gallery.sharingKey_query
|
||||
+ '=' + this._shareService.getSharingKey(), {'password': password});
|
||||
}
|
||||
|
||||
public async getSessionUser(): Promise<UserDTO> {
|
||||
await this._shareService.wait();
|
||||
if (Config.Client.Sharing.enabled === true) {
|
||||
if (this._shareService.isSharing()) {
|
||||
return this._networkService.getJson<UserDTO>('/user/login', {sk: this._shareService.getSharingKey()});
|
||||
const query: any = {};
|
||||
query[QueryParams.gallery.sharingKey_query] = this._shareService.getSharingKey();
|
||||
return this._networkService.getJson<UserDTO>('/user/me', query);
|
||||
}
|
||||
}
|
||||
return this._networkService.getJson<UserDTO>('/user/login');
|
||||
return this._networkService.getJson<UserDTO>('/user/me');
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ export class QueryService {
|
||||
}
|
||||
if (Config.Client.Sharing.enabled === true) {
|
||||
if (this.shareService.isSharing()) {
|
||||
query[QueryParams.gallery.sharingKey_short] = this.shareService.getSharingKey();
|
||||
query[QueryParams.gallery.sharingKey_query] = this.shareService.getSharingKey();
|
||||
}
|
||||
}
|
||||
return query;
|
||||
@ -40,7 +40,7 @@ export class QueryService {
|
||||
const params: { [key: string]: any } = {};
|
||||
if (Config.Client.Sharing.enabled === true) {
|
||||
if (this.shareService.isSharing()) {
|
||||
params[QueryParams.gallery.sharingKey_short] = this.shareService.getSharingKey();
|
||||
params[QueryParams.gallery.sharingKey_query] = this.shareService.getSharingKey();
|
||||
}
|
||||
}
|
||||
if (directory && directory.lastModified && directory.lastScanned &&
|
||||
|
@ -60,7 +60,7 @@
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem" *ngIf="authenticationRequired">
|
||||
<a class="dropdown-item" href="#" (click)="logout()">
|
||||
<a class="dropdown-item" (click)="logout()">
|
||||
<span class="oi oi-account-logout"></span>
|
||||
<ng-container i18n>Logout</ng-container>
|
||||
</a>
|
||||
|
@ -263,7 +263,9 @@ export class GalleryCacheService {
|
||||
|
||||
private reset() {
|
||||
try {
|
||||
const currentUserStr = localStorage.getItem('currentUser');
|
||||
localStorage.clear();
|
||||
localStorage.setItem('currentUser', currentUserStr);
|
||||
localStorage.setItem(GalleryCacheService.VERSION, this.versionService.version.value);
|
||||
} catch (e) {
|
||||
|
||||
|
@ -17,6 +17,7 @@ import {SortingMethods} from '../../../../common/entities/SortingMethods';
|
||||
import {PhotoDTO} from '../../../../common/entities/PhotoDTO';
|
||||
import {QueryParams} from '../../../../common/QueryParams';
|
||||
import {SeededRandomService} from '../../model/seededRandom.service';
|
||||
import {take} from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'app-gallery',
|
||||
@ -57,10 +58,10 @@ export class GalleryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
updateTimer(t: number) {
|
||||
if (this.shareService.sharing.value == null) {
|
||||
if (this.shareService.sharingSubject.value == null) {
|
||||
return;
|
||||
}
|
||||
t = Math.floor((this.shareService.sharing.value.expires - Date.now()) / 1000);
|
||||
t = Math.floor((this.shareService.sharingSubject.value.expires - Date.now()) / 1000);
|
||||
this.countDown = <any>{};
|
||||
this.countDown.day = Math.floor(t / 86400);
|
||||
t -= this.countDown.day * 86400;
|
||||
@ -125,10 +126,10 @@ export class GalleryComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
if (params[QueryParams.gallery.sharingKey_long] && params[QueryParams.gallery.sharingKey_long] !== '') {
|
||||
const sharing = await this.shareService.getSharing();
|
||||
if (params[QueryParams.gallery.sharingKey_params] && params[QueryParams.gallery.sharingKey_params] !== '') {
|
||||
const sharing = await this.shareService.currentSharing.pipe(take(1)).toPromise();
|
||||
const qParams: { [key: string]: any } = {};
|
||||
qParams[QueryParams.gallery.sharingKey_short] = this.shareService.getSharingKey();
|
||||
qParams[QueryParams.gallery.sharingKey_query] = this.shareService.getSharingKey();
|
||||
this._router.navigate(['/gallery', sharing.path], {queryParams: qParams}).catch(console.error);
|
||||
return;
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ import {DirectoryDTO} from '../../../../common/entities/DirectoryDTO';
|
||||
import {SearchTypes} from '../../../../common/entities/AutoCompleteItem';
|
||||
import {GalleryCacheService} from './cache.gallery.service';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
import {SharingDTO} from '../../../../common/entities/SharingDTO';
|
||||
import {Config} from '../../../../common/config/public/Config';
|
||||
import {ShareService} from './share.service';
|
||||
import {NavigationService} from '../../model/navigation.service';
|
||||
@ -79,7 +78,7 @@ export class GalleryService {
|
||||
const params: { [key: string]: any } = {};
|
||||
if (Config.Client.Sharing.enabled === true) {
|
||||
if (this._shareService.isSharing()) {
|
||||
params[QueryParams.gallery.sharingKey_short] = this._shareService.getSharingKey();
|
||||
params[QueryParams.gallery.sharingKey_query] = this._shareService.getSharingKey();
|
||||
}
|
||||
}
|
||||
|
||||
@ -199,9 +198,6 @@ export class GalleryService {
|
||||
|
||||
}
|
||||
|
||||
public async getSharing(sharingKey: string): Promise<SharingDTO> {
|
||||
return this.networkService.getJson<SharingDTO>('/share/' + sharingKey);
|
||||
}
|
||||
|
||||
|
||||
isSearchResult(): boolean {
|
||||
|
@ -2,25 +2,27 @@ import {Injectable} from '@angular/core';
|
||||
import {NetworkService} from '../../model/network/network.service';
|
||||
import {CreateSharingDTO, SharingDTO} from '../../../../common/entities/SharingDTO';
|
||||
import {Router, RoutesRecognized} from '@angular/router';
|
||||
import {BehaviorSubject, Observable} from 'rxjs';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
import {distinctUntilChanged, filter} from 'rxjs/operators';
|
||||
import {QueryParams} from '../../../../common/QueryParams';
|
||||
import {UserDTO} from '../../../../common/entities/UserDTO';
|
||||
|
||||
@Injectable()
|
||||
export class ShareService {
|
||||
|
||||
public sharing: BehaviorSubject<SharingDTO>;
|
||||
param: string = null;
|
||||
queryParam: string = null;
|
||||
sharingKey: string = null;
|
||||
inited = false;
|
||||
public ReadyPR: Promise<void>;
|
||||
private resolve: () => void;
|
||||
public sharingSubject: BehaviorSubject<SharingDTO> = new BehaviorSubject(null);
|
||||
public currentSharing = this.sharingSubject
|
||||
.asObservable().pipe(filter(s => s !== null)).pipe(distinctUntilChanged());
|
||||
|
||||
private resolve: () => void;
|
||||
|
||||
constructor(private networkService: NetworkService,
|
||||
private router: Router) {
|
||||
this.sharing = new BehaviorSubject(null);
|
||||
this.ReadyPR = new Promise((resolve: () => void) => {
|
||||
if (this.inited === true) {
|
||||
return resolve();
|
||||
@ -28,15 +30,15 @@ export class ShareService {
|
||||
this.resolve = resolve;
|
||||
});
|
||||
|
||||
this.router.events.subscribe(val => {
|
||||
this.router.events.subscribe(async val => {
|
||||
if (val instanceof RoutesRecognized) {
|
||||
this.param = val.state.root.firstChild.params[QueryParams.gallery.sharingKey_long] || null;
|
||||
this.queryParam = val.state.root.firstChild.queryParams[QueryParams.gallery.sharingKey_short] || null;
|
||||
this.param = val.state.root.firstChild.params[QueryParams.gallery.sharingKey_params] || null;
|
||||
this.queryParam = 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;
|
||||
this.getSharing();
|
||||
await this.getSharing();
|
||||
}
|
||||
if (this.resolve) {
|
||||
this.resolve();
|
||||
@ -50,13 +52,13 @@ export class ShareService {
|
||||
|
||||
}
|
||||
|
||||
public setUserObs(userOB: Observable<UserDTO>) {
|
||||
|
||||
userOB.subscribe((user) => {
|
||||
onNewUser = async (user: UserDTO) => {
|
||||
if (user && !!user.usedSharingKey) {
|
||||
if (user.usedSharingKey !== this.sharingKey) {
|
||||
if (user.usedSharingKey !== this.sharingKey ||
|
||||
this.sharingSubject.value == null) {
|
||||
this.sharingKey = user.usedSharingKey;
|
||||
this.getSharing();
|
||||
await this.getSharing();
|
||||
}
|
||||
if (this.resolve) {
|
||||
this.resolve();
|
||||
@ -64,8 +66,7 @@ export class ShareService {
|
||||
this.inited = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
public wait(): Promise<void> {
|
||||
@ -104,9 +105,13 @@ export class ShareService {
|
||||
|
||||
}
|
||||
|
||||
public async getSharing(): Promise<SharingDTO> {
|
||||
private async getSharing(): Promise<void> {
|
||||
try {
|
||||
this.sharingSubject.next(null);
|
||||
const sharing = await this.networkService.getJson<SharingDTO>('/share/' + this.getSharingKey());
|
||||
this.sharing.next(sharing);
|
||||
return sharing;
|
||||
this.sharingSubject.next(sharing);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form #shareForm="ngForm" class="form-horizontal">
|
||||
<div class="row">
|
||||
<div class="col-7 col-sm-9">
|
||||
<input id="shareLink"
|
||||
@ -24,8 +25,10 @@
|
||||
</div>
|
||||
<div class="col-5 col-sm-3">
|
||||
<button id="copyButton" name="copyButton"
|
||||
ngxClipboard [cbContent]="url"
|
||||
ngxClipboard
|
||||
[cbContent]="url"
|
||||
(cbOnSuccess)="onCopy()"
|
||||
[disabled]="!shareForm.form.valid"
|
||||
class="btn btn-primary btn-block" i18n>Copy
|
||||
</button>
|
||||
</div>
|
||||
@ -33,10 +36,12 @@
|
||||
<hr/>
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
<label class="control-label" i18n>Sharing:</label>
|
||||
<label class="control-label" for="sharing-dir" i18n>Sharing:</label>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<input disabled type="text"
|
||||
name="sharing-dir"
|
||||
id="sharing-dir"
|
||||
class="full-width form-control"
|
||||
[ngModel]="currentDir">
|
||||
</div>
|
||||
@ -44,18 +49,19 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
<label class="control-label" i18n>Include subfolders:</label>
|
||||
<label class="control-label" for="includeSubfolders" i18n>Include subfolders:</label>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<bSwitch
|
||||
class="switch"
|
||||
name="includeSubfolders"
|
||||
id="includeSubfolders"
|
||||
[switch-on-color]="'success'"
|
||||
[switch-inverse]="'inverse'"
|
||||
[switch-inverse]="true"
|
||||
[switch-off-text]="text.No"
|
||||
[switch-on-text]="text.Yes"
|
||||
[switch-handle-width]="'100'"
|
||||
[switch-label-width]="'20'"
|
||||
[switch-handle-width]="100"
|
||||
[switch-label-width]="20"
|
||||
(change)="update()"
|
||||
[(ngModel)]="input.includeSubfolders">
|
||||
</bSwitch>
|
||||
@ -64,33 +70,36 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
<label class="control-label">
|
||||
<ng-container i18n>Password</ng-container>
|
||||
:
|
||||
<label class="control-label" for="share-password">
|
||||
<ng-container i18n>Password</ng-container>*:
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<input id="password"
|
||||
<div class="col-8" *ngIf="passwordProtection">
|
||||
<input id="share-password"
|
||||
class="form-control"
|
||||
name="share-password"
|
||||
type="password"
|
||||
(change)="update()"
|
||||
[(ngModel)]="input.password"
|
||||
i18n-placeholder
|
||||
placeholder="Password">
|
||||
placeholder="Password"
|
||||
required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
<label class="control-label" i18n>Valid:</label>
|
||||
<label class="control-label" for="valid-from" i18n>Valid:</label>
|
||||
</div>
|
||||
<div class="col-4" style="padding-right: 1px">
|
||||
<input class="form-control" [(ngModel)]="input.valid.amount" (change)="update()"
|
||||
name="validAmount"
|
||||
name="valid-from"
|
||||
id="valid-from"
|
||||
type="number" min="0" step="1"/>
|
||||
</div>
|
||||
<div class="col-4" style="padding-left: 1px">
|
||||
<select class="form-control" [(ngModel)]="input.valid.type" (change)="update()" name="validType"
|
||||
<select class="form-control"
|
||||
[(ngModel)]="input.valid.type" (change)="update()" name="valid-to"
|
||||
required>
|
||||
<option [ngValue]="ValidityTypes.Minutes" i18n>Minutes</option>
|
||||
<option [ngValue]="ValidityTypes.Hours" i18n>Hours</option>
|
||||
@ -99,5 +108,6 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
@ -29,13 +29,13 @@ export class GalleryShareComponent implements OnInit, OnDestroy {
|
||||
amount: 30,
|
||||
type: ValidityTypes.Days
|
||||
},
|
||||
password: ''
|
||||
password: <string>null
|
||||
};
|
||||
currentDir = '';
|
||||
sharing: SharingDTO = null;
|
||||
contentSubscription: Subscription = null;
|
||||
passwordProtection = false;
|
||||
ValidityTypes: any;
|
||||
readonly passwordProtection = Config.Client.Sharing.passwordProtected;
|
||||
readonly ValidityTypes = ValidityTypes;
|
||||
|
||||
modalRef: BsModalRef;
|
||||
|
||||
@ -49,7 +49,6 @@ export class GalleryShareComponent implements OnInit, OnDestroy {
|
||||
private _notification: NotificationService,
|
||||
public i18n: I18n,
|
||||
private modalService: BsModalService) {
|
||||
this.ValidityTypes = ValidityTypes;
|
||||
|
||||
this.text.Yes = i18n('Yes');
|
||||
this.text.No = i18n('No');
|
||||
@ -64,7 +63,6 @@ export class GalleryShareComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
this.currentDir = Utils.concatUrls((<DirectoryDTO>content.directory).path, (<DirectoryDTO>content.directory).name);
|
||||
});
|
||||
this.passwordProtection = Config.Client.Sharing.passwordProtected;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
@ -155,7 +155,7 @@ export abstract class SettingsComponent<T extends { [key: string]: any }, S exte
|
||||
this.inProgress = false;
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
console.error(err);
|
||||
if (err.message) {
|
||||
this.error = (<ErrorDTO>err).message;
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ export class JobButtonComponent {
|
||||
this.notification.info(this.i18n('Stopping job') + ': ' + this.backendTextService.getJobName(this.jobName));
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
console.error(err);
|
||||
if (err.message) {
|
||||
this.error.emit((<ErrorDTO>err).message);
|
||||
}
|
||||
|
@ -91,7 +91,7 @@ export class UserMangerSettingsComponent implements OnInit, ISettingsComponent {
|
||||
this.notification.success(this.i18n('Password protection disabled'), this.i18n('Success'));
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
console.error(err);
|
||||
if (err.message) {
|
||||
this.error = (<ErrorDTO>err).message;
|
||||
}
|
||||
|
33
test/backend/integration/routers/RouteTestingHelper.ts
Normal file
33
test/backend/integration/routers/RouteTestingHelper.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import {SharingDTO} from '../../../../src/common/entities/SharingDTO';
|
||||
import {ObjectManagers} from '../../../../src/backend/model/ObjectManagers';
|
||||
import {UserDTO, UserRoles} from '../../../../src/common/entities/UserDTO';
|
||||
import {Utils} from '../../../../src/common/Utils';
|
||||
|
||||
export class RouteTestingHelper {
|
||||
|
||||
|
||||
static async createSharing(testUser: UserDTO, password: string = null): Promise<SharingDTO> {
|
||||
const sharing = <SharingDTO>{
|
||||
sharingKey: 'sharing_test_key_' + Date.now(),
|
||||
path: 'test',
|
||||
expires: Date.now() + 1000,
|
||||
timeStamp: Date.now(),
|
||||
includeSubfolders: false,
|
||||
creator: testUser
|
||||
};
|
||||
if (password) {
|
||||
sharing.password = password;
|
||||
}
|
||||
await ObjectManagers.getInstance().SharingManager.createSharing(Utils.clone(sharing)); // do not rewrite password
|
||||
return sharing;
|
||||
}
|
||||
|
||||
public static getExpectedSharingUser(sharing: SharingDTO): UserDTO {
|
||||
return <UserDTO>{
|
||||
name: 'Guest',
|
||||
role: UserRoles.LimitedGuest,
|
||||
permissions: [sharing.path],
|
||||
usedSharingKey: sharing.sharingKey
|
||||
};
|
||||
}
|
||||
}
|
121
test/backend/integration/routers/SharingRouter.ts
Normal file
121
test/backend/integration/routers/SharingRouter.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import {Config} from '../../../../src/common/config/private/Config';
|
||||
import {ServerConfig} from '../../../../src/common/config/private/IPrivateConfig';
|
||||
import {Server} from '../../../../src/backend/server';
|
||||
import {LoginCredential} from '../../../../src/common/entities/LoginCredential';
|
||||
import {UserDTO, UserRoles} from '../../../../src/common/entities/UserDTO';
|
||||
import * as path from 'path';
|
||||
import * as util from 'util';
|
||||
import * as rimraf from 'rimraf';
|
||||
import {SQLConnection} from '../../../../src/backend/model/database/sql/SQLConnection';
|
||||
import {ObjectManagers} from '../../../../src/backend/model/ObjectManagers';
|
||||
import {Utils} from '../../../../src/common/Utils';
|
||||
import {SuperAgentStatic} from 'superagent';
|
||||
import {RouteTestingHelper} from './RouteTestingHelper';
|
||||
import {QueryParams} from '../../../../src/common/QueryParams';
|
||||
import {ErrorCodes} from '../../../../src/common/entities/Error';
|
||||
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
const chai: any = require('chai');
|
||||
const chaiHttp = require('chai-http');
|
||||
const should = chai.should();
|
||||
chai.use(chaiHttp);
|
||||
|
||||
const rimrafPR = util.promisify(rimraf);
|
||||
describe('Sharing', () => {
|
||||
|
||||
const testUser: UserDTO = {
|
||||
id: 1,
|
||||
name: 'test',
|
||||
password: 'test',
|
||||
role: UserRoles.User,
|
||||
permissions: null
|
||||
};
|
||||
const {password: _pass, ...expectedUser} = testUser;
|
||||
const tempDir = path.join(__dirname, '../../tmp');
|
||||
let server: Server;
|
||||
const setUp = async () => {
|
||||
await rimrafPR(tempDir);
|
||||
Config.Client.authenticationRequired = true;
|
||||
Config.Server.Threading.enabled = false;
|
||||
Config.Client.Sharing.enabled = true;
|
||||
Config.Server.Database.type = ServerConfig.DatabaseType.sqlite;
|
||||
Config.Server.Database.dbFolder = tempDir;
|
||||
|
||||
server = new Server();
|
||||
await server.onStarted.wait();
|
||||
|
||||
await ObjectManagers.InitSQLManagers();
|
||||
await ObjectManagers.getInstance().UserManager.createUser(Utils.clone(testUser));
|
||||
await SQLConnection.close();
|
||||
};
|
||||
const tearDown = async () => {
|
||||
await SQLConnection.close();
|
||||
await rimrafPR(tempDir);
|
||||
};
|
||||
|
||||
const shouldBeValidUser = (result: any, user: any) => {
|
||||
|
||||
result.should.have.status(200);
|
||||
result.body.should.be.a('object');
|
||||
should.equal(result.body.error, null);
|
||||
result.body.result.csrfToken.should.be.a('string');
|
||||
const {csrfToken, ...u} = result.body.result;
|
||||
u.should.deep.equal(user);
|
||||
};
|
||||
|
||||
const shareLogin = async (srv: Server, sharingKey: string, password?: string): Promise<any> => {
|
||||
return (chai.request(srv.App) as SuperAgentStatic)
|
||||
.post('/api/share/login?' + QueryParams.gallery.sharingKey_query + '=' + sharingKey)
|
||||
.send({password});
|
||||
|
||||
};
|
||||
|
||||
const login = async (srv: Server): Promise<any> => {
|
||||
const result = await (chai.request(srv.App) as SuperAgentStatic)
|
||||
.post('/api/user/login')
|
||||
.send({
|
||||
loginCredential: <LoginCredential>{
|
||||
password: testUser.password,
|
||||
username: testUser.name,
|
||||
rememberMe: false
|
||||
}
|
||||
});
|
||||
|
||||
shouldBeValidUser(result, expectedUser);
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
describe('/POST share/login', () => {
|
||||
|
||||
beforeEach(setUp);
|
||||
afterEach(tearDown);
|
||||
|
||||
it('should login with passworded share', async () => {
|
||||
const sharing = await RouteTestingHelper.createSharing(testUser, 'secret_pass');
|
||||
const res = await shareLogin(server, sharing.sharingKey, sharing.password);
|
||||
shouldBeValidUser(res, RouteTestingHelper.getExpectedSharingUser(sharing));
|
||||
});
|
||||
|
||||
it('should not login with passworded share without password', async () => {
|
||||
const sharing = await RouteTestingHelper.createSharing(testUser, 'secret_pass');
|
||||
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);
|
||||
});
|
||||
|
||||
it('should login with no-password share', async () => {
|
||||
const sharing = await RouteTestingHelper.createSharing(testUser,);
|
||||
const res = await shareLogin(server, sharing.sharingKey, sharing.password);
|
||||
shouldBeValidUser(res, RouteTestingHelper.getExpectedSharingUser(sharing));
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
@ -1,12 +1,19 @@
|
||||
import {Server} from '../../../../src/backend/server';
|
||||
import {Config} from '../../../../src/common/config/private/Config';
|
||||
import {ServerConfig} from '../../../../src/common/config/private/IPrivateConfig';
|
||||
import {Server} from '../../../../src/backend/server';
|
||||
import {LoginCredential} from '../../../../src/common/entities/LoginCredential';
|
||||
import {UserDTO, UserRoles} from '../../../../src/common/entities/UserDTO';
|
||||
import * as path from 'path';
|
||||
import * as util from 'util';
|
||||
import * as rimraf from 'rimraf';
|
||||
import {ServerConfig} from '../../../../src/common/config/private/IPrivateConfig';
|
||||
import {SQLConnection} from '../../../../src/backend/model/database/sql/SQLConnection';
|
||||
import {ObjectManagers} from '../../../../src/backend/model/ObjectManagers';
|
||||
import {QueryParams} from '../../../../src/common/QueryParams';
|
||||
import {Utils} from '../../../../src/common/Utils';
|
||||
import {SuperAgentStatic} from 'superagent';
|
||||
import {RouteTestingHelper} from './RouteTestingHelper';
|
||||
import {ErrorCodes} from '../../../../src/common/entities/Error';
|
||||
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
const chai: any = require('chai');
|
||||
@ -17,33 +24,170 @@ chai.use(chaiHttp);
|
||||
const rimrafPR = util.promisify(rimraf);
|
||||
describe('UserRouter', () => {
|
||||
|
||||
const testUser: UserDTO = {
|
||||
id: 1,
|
||||
name: 'test',
|
||||
password: 'test',
|
||||
role: UserRoles.User,
|
||||
permissions: null
|
||||
};
|
||||
const {password, ...expectedUser} = testUser;
|
||||
const tempDir = path.join(__dirname, '../../tmp');
|
||||
beforeEach(async () => {
|
||||
let server: Server;
|
||||
const setUp = async () => {
|
||||
await rimrafPR(tempDir);
|
||||
Config.Server.Threading.enabled = false;
|
||||
Config.Server.Database.type = ServerConfig.DatabaseType.sqlite;
|
||||
Config.Server.Database.dbFolder = tempDir;
|
||||
});
|
||||
|
||||
|
||||
afterEach(async () => {
|
||||
server = new Server();
|
||||
await server.onStarted.wait();
|
||||
await ObjectManagers.InitSQLManagers();
|
||||
await ObjectManagers.getInstance().UserManager.createUser(Utils.clone(testUser));
|
||||
await SQLConnection.close();
|
||||
};
|
||||
const tearDown = async () => {
|
||||
await SQLConnection.close();
|
||||
await rimrafPR(tempDir);
|
||||
});
|
||||
};
|
||||
|
||||
describe('/POST login', () => {
|
||||
it('it should GET all the books', async () => {
|
||||
const srv = new Server();
|
||||
await srv.onStarted.wait();
|
||||
const result = await chai.request(srv.App)
|
||||
.post('/api/user/login')
|
||||
.send({loginCredential: <LoginCredential>{password: 'admin', username: 'admin', rememberMe: false}});
|
||||
const checkUserResult = (result: any, user: any) => {
|
||||
|
||||
result.res.should.have.status(200);
|
||||
result.should.have.status(200);
|
||||
result.body.should.be.a('object');
|
||||
should.equal(result.body.error, null);
|
||||
result.body.result.should.deep.equal(<UserDTO>{id: 1, name: 'admin', role: UserRoles.Admin, permissions: null});
|
||||
result.body.result.csrfToken.should.be.a('string');
|
||||
const {csrfToken, ...u} = result.body.result;
|
||||
u.should.deep.equal(user);
|
||||
};
|
||||
|
||||
const login = async (srv: Server): Promise<any> => {
|
||||
const result = await (chai.request(srv.App) as SuperAgentStatic)
|
||||
.post('/api/user/login')
|
||||
.send({
|
||||
loginCredential: <LoginCredential>{
|
||||
password: testUser.password,
|
||||
username: testUser.name,
|
||||
rememberMe: false
|
||||
}
|
||||
});
|
||||
|
||||
checkUserResult(result, expectedUser);
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
describe('/POST user/login', () => {
|
||||
beforeEach(setUp);
|
||||
afterEach(tearDown);
|
||||
it('it should login', async () => {
|
||||
Config.Client.authenticationRequired = true;
|
||||
await login(server);
|
||||
|
||||
});
|
||||
it('it skip login', async () => {
|
||||
Config.Client.authenticationRequired = false;
|
||||
const result = await chai.request(server.App)
|
||||
.post('/api/user/login');
|
||||
|
||||
result.res.should.have.status(404);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('/GET user/me', () => {
|
||||
beforeEach(setUp);
|
||||
afterEach(tearDown);
|
||||
it('it should GET the authenticated user', async () => {
|
||||
Config.Client.authenticationRequired = true;
|
||||
|
||||
const loginRes = await login(server);
|
||||
|
||||
const result = await chai.request(server.App)
|
||||
.get('/api/user/me')
|
||||
.set('Cookie', loginRes.res.headers['set-cookie'])
|
||||
.set('CSRF-Token', loginRes.body.result.csrfToken);
|
||||
|
||||
checkUserResult(result, expectedUser);
|
||||
});
|
||||
|
||||
it('it should not authenticate', async () => {
|
||||
Config.Client.authenticationRequired = true;
|
||||
|
||||
const result = await chai.request(server.App)
|
||||
.get('/api/user/me');
|
||||
|
||||
result.res.should.have.status(401);
|
||||
});
|
||||
|
||||
it('it should authenticate as user with sharing key', async () => {
|
||||
Config.Client.authenticationRequired = true;
|
||||
Config.Client.Sharing.enabled = true;
|
||||
|
||||
const sharingKey = (await RouteTestingHelper.createSharing(testUser)).sharingKey;
|
||||
|
||||
|
||||
const loginRes = await login(server);
|
||||
const q: any = {};
|
||||
q[QueryParams.gallery.sharingKey_query] = sharingKey;
|
||||
const result = await chai.request(server.App)
|
||||
.get('/api/user/me?' + QueryParams.gallery.sharingKey_query + '=' + sharingKey)
|
||||
.set('Cookie', loginRes.res.headers['set-cookie'])
|
||||
.set('CSRF-Token', loginRes.body.result.csrfToken);
|
||||
|
||||
// should return with logged in user, not limited sharing one
|
||||
checkUserResult(result, expectedUser);
|
||||
});
|
||||
|
||||
|
||||
it('it should authenticate with sharing key', async () => {
|
||||
Config.Client.authenticationRequired = true;
|
||||
Config.Client.Sharing.enabled = true;
|
||||
const sharing = (await RouteTestingHelper.createSharing(testUser));
|
||||
|
||||
|
||||
const q: any = {};
|
||||
q[QueryParams.gallery.sharingKey_query] = sharing.sharingKey;
|
||||
const result = await chai.request(server.App)
|
||||
.get('/api/user/me?' + QueryParams.gallery.sharingKey_query + '=' + sharing.sharingKey);
|
||||
|
||||
|
||||
checkUserResult(result, RouteTestingHelper.getExpectedSharingUser(sharing));
|
||||
});
|
||||
it('it should not authenticate with sharing key without password', async () => {
|
||||
Config.Client.authenticationRequired = true;
|
||||
Config.Client.Sharing.enabled = true;
|
||||
const sharing = (await RouteTestingHelper.createSharing(testUser, 'pass_secret'));
|
||||
|
||||
|
||||
const q: any = {};
|
||||
q[QueryParams.gallery.sharingKey_query] = sharing.sharingKey;
|
||||
const result = await chai.request(server.App)
|
||||
.get('/api/user/me?' + QueryParams.gallery.sharingKey_query + '=' + 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.NOT_AUTHENTICATED);
|
||||
});
|
||||
|
||||
it('it should authenticate as guest', async () => {
|
||||
Config.Client.authenticationRequired = false;
|
||||
|
||||
const result = await chai.request(server.App)
|
||||
.get('/api/user/me');
|
||||
|
||||
const expectedGuestUser = <UserDTO>{
|
||||
name: UserRoles[Config.Client.unAuthenticatedUserRole],
|
||||
role: Config.Client.unAuthenticatedUserRole
|
||||
};
|
||||
|
||||
|
||||
checkUserResult(result, expectedGuestUser);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -59,9 +59,7 @@ describe('Authentication middleware', () => {
|
||||
describe('authorisePath', () => {
|
||||
|
||||
const req = {
|
||||
session: {
|
||||
user: {permissions: <string[]>null}
|
||||
},
|
||||
user: {permissions: <string[]>null},
|
||||
sessionOptions: {},
|
||||
query: {},
|
||||
params: {
|
||||
@ -82,14 +80,14 @@ describe('Authentication middleware', () => {
|
||||
};
|
||||
|
||||
it('should catch unauthorized path usage', async () => {
|
||||
req.session.user.permissions = [path.normalize('/sub/subsub')];
|
||||
req.user.permissions = [path.normalize('/sub/subsub')];
|
||||
expect(await test('/sub/subsub')).to.be.eql('ok');
|
||||
expect(await test('/test')).to.be.eql(403);
|
||||
expect(await test('/')).to.be.eql(403);
|
||||
expect(await test('/sub/test')).to.be.eql(403);
|
||||
expect(await test('/sub/subsub/test')).to.be.eql(403);
|
||||
expect(await test('/sub/subsub/test/test2')).to.be.eql(403);
|
||||
req.session.user.permissions = [path.normalize('/sub/subsub'), path.normalize('/sub/subsub2')];
|
||||
req.user.permissions = [path.normalize('/sub/subsub'), path.normalize('/sub/subsub2')];
|
||||
expect(await test('/sub/subsub2')).to.be.eql('ok');
|
||||
expect(await test('/sub/subsub')).to.be.eql('ok');
|
||||
expect(await test('/test')).to.be.eql(403);
|
||||
@ -97,7 +95,7 @@ describe('Authentication middleware', () => {
|
||||
expect(await test('/sub/test')).to.be.eql(403);
|
||||
expect(await test('/sub/subsub/test')).to.be.eql(403);
|
||||
expect(await test('/sub/subsub2/test')).to.be.eql(403);
|
||||
req.session.user.permissions = [path.normalize('/sub/subsub*')];
|
||||
req.user.permissions = [path.normalize('/sub/subsub*')];
|
||||
expect(await test('/b')).to.be.eql(403);
|
||||
expect(await test('/sub')).to.be.eql(403);
|
||||
expect(await test('/sub/subsub2')).to.be.eql(403);
|
||||
@ -274,7 +272,7 @@ describe('Authentication middleware', () => {
|
||||
};
|
||||
const next = (err: ErrorDTO) => {
|
||||
expect(err).to.be.undefined;
|
||||
expect(req.session.user).to.be.eql('test user');
|
||||
expect(req.user).to.be.eql('test user');
|
||||
done();
|
||||
};
|
||||
ObjectManagers.getInstance().UserManager = <IUserManager>{
|
||||
@ -300,7 +298,7 @@ describe('Authentication middleware', () => {
|
||||
};
|
||||
const next: any = (err: ErrorDTO) => {
|
||||
expect(err).to.be.undefined;
|
||||
expect(req.session.user).to.be.undefined;
|
||||
expect(req.user).to.be.undefined;
|
||||
done();
|
||||
};
|
||||
AuthenticationMWs.logout(req, null, next);
|
||||
|
@ -3,6 +3,8 @@ import {Utils} from '../../../src/common/Utils';
|
||||
|
||||
describe('Utils', () => {
|
||||
it('should concat urls', () => {
|
||||
expect(Utils.concatUrls('\\')).to.be.equal('.');
|
||||
expect(Utils.concatUrls('\\*')).to.be.equal('/*');
|
||||
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');
|
||||
|
Loading…
x
Reference in New Issue
Block a user