1
0
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:
Patrik J. Braun 2020-01-07 22:17:54 +01:00
parent 19d3f10d35
commit 5b4f06e789
49 changed files with 865 additions and 236 deletions

158
package-lock.json generated
View File

@ -3105,6 +3105,12 @@
"defer-to-connect": "^1.0.1" "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": { "@types/bcryptjs": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz", "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz",
@ -3199,6 +3205,16 @@
"@types/serve-static": "*" "@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": { "@types/express-serve-static-core": {
"version": "4.17.0", "version": "4.17.0",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.0.tgz", "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/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": { "@types/fluent-ffmpeg": {
"version": "2.1.11", "version": "2.1.11",
"resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.11.tgz", "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.11.tgz",
@ -3973,6 +3998,15 @@
"integrity": "sha512-kGCRI9oiCxFS6soGKlyzhMzDydfcPix9PpTkr7h11huxOxhWwP37Tg7DYBaQ18eQTNreZEuLkhpbGSqVNZPnnw==", "integrity": "sha512-kGCRI9oiCxFS6soGKlyzhMzDydfcPix9PpTkr7h11huxOxhWwP37Tg7DYBaQ18eQTNreZEuLkhpbGSqVNZPnnw==",
"dev": true "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": { "@types/keygrip": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.1.tgz", "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", "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz",
"integrity": "sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs=" "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": { "buffer-fill": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
@ -7731,6 +7770,14 @@
"safer-buffer": "^2.1.0" "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": { "editorconfig": {
"version": "0.15.3", "version": "0.15.3",
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", "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": { "extend": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@ -11900,6 +11975,30 @@
"integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=",
"dev": true "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": { "jsprim": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
@ -11979,6 +12078,25 @@
"tslib": "^1.9.0" "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": { "karma": {
"version": "4.4.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/karma/-/karma-4.4.1.tgz", "resolved": "https://registry.npmjs.org/karma/-/karma-4.4.1.tgz",
@ -12571,6 +12689,11 @@
"lodash._root": "^3.0.0" "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": { "lodash.isarguments": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
@ -12583,6 +12706,31 @@
"integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=",
"dev": true "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": { "lodash.keys": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz",
@ -12594,12 +12742,22 @@
"lodash.isarray": "^3.0.0" "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": { "lodash.restparam": {
"version": "3.6.1", "version": "3.6.1",
"resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz",
"integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=", "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=",
"dev": true "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": { "lodash.template": {
"version": "3.6.2", "version": "3.6.2",
"resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz", "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz",

View File

@ -35,6 +35,7 @@
"ejs": "3.0.1", "ejs": "3.0.1",
"exifreader": "2.12.0", "exifreader": "2.12.0",
"express": "4.17.1", "express": "4.17.1",
"express-unless": "0.5.0",
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.2",
"image-size": "0.8.3", "image-size": "0.8.3",
"jimp": "0.9.3", "jimp": "0.9.3",
@ -64,19 +65,22 @@
"@angular/platform-browser-dynamic": "8.2.14", "@angular/platform-browser-dynamic": "8.2.14",
"@angular/router": "8.2.14", "@angular/router": "8.2.14",
"@ngx-translate/i18n-polyfill": "1.0.0", "@ngx-translate/i18n-polyfill": "1.0.0",
"@types/bcrypt": "^3.0.0",
"@types/bcryptjs": "2.4.2", "@types/bcryptjs": "2.4.2",
"@types/chai": "4.2.6", "@types/chai": "4.2.6",
"@types/cookie-parser": "1.4.2", "@types/cookie-parser": "1.4.2",
"@types/cookie-session": "2.0.37", "@types/cookie-session": "2.0.37",
"@types/csurf": "1.9.36", "@types/csurf": "^1.9.36",
"@types/ejs": "3.0.0", "@types/ejs": "3.0.0",
"@types/express": "4.17.2", "@types/express": "4.17.2",
"@types/express-jwt": "0.0.42",
"@types/fluent-ffmpeg": "2.1.11", "@types/fluent-ffmpeg": "2.1.11",
"@types/gm": "1.18.6", "@types/gm": "1.18.6",
"@types/gulp": "4.0.6", "@types/gulp": "4.0.6",
"@types/gulp-zip": "4.0.1", "@types/gulp-zip": "4.0.1",
"@types/image-size": "0.8.0", "@types/image-size": "0.8.0",
"@types/jasmine": "3.5.0", "@types/jasmine": "3.5.0",
"@types/jsonwebtoken": "8.3.5",
"@types/node": "12.12.14", "@types/node": "12.12.14",
"@types/rimraf": "2.0.3", "@types/rimraf": "2.0.3",
"@types/sharp": "0.23.1", "@types/sharp": "0.23.1",

View File

@ -24,7 +24,6 @@ export class GalleryMWs {
public static async listDirectory(req: Request, res: Response, next: NextFunction) { public static async listDirectory(req: Request, res: Response, next: NextFunction) {
const directoryName = req.params.directory || '/'; const directoryName = req.params.directory || '/';
const absoluteDirectoryName = path.join(ProjectPath.ImageFolder, directoryName); const absoluteDirectoryName = path.join(ProjectPath.ImageFolder, directoryName);
try { try {
if ((await fsp.stat(absoluteDirectoryName)).isDirectory() === false) { if ((await fsp.stat(absoluteDirectoryName)).isDirectory() === false) {
return next(); return next();

View File

@ -1,11 +1,9 @@
import {NextFunction, Request, Response} from 'express'; import {NextFunction, Request, Response} from 'express';
import {ErrorCodes, ErrorDTO} from '../../common/entities/Error'; import {ErrorCodes, ErrorDTO} from '../../common/entities/Error';
import {Utils} from '../../common/Utils';
import {Message} from '../../common/entities/Message'; import {Message} from '../../common/entities/Message';
import {SharingDTO} from '../../common/entities/SharingDTO';
import {Config} from '../../common/config/private/Config'; import {Config} from '../../common/config/private/Config';
import {ConfigClass} from '../../common/config/private/ConfigClass'; 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 {NotificationManager} from '../model/NotifocationManager';
import {Logger} from '../Logger'; import {Logger} from '../Logger';
@ -25,8 +23,19 @@ export class RenderingMWs {
return next(new ErrorDTO(ErrorCodes.GENERAL_ERROR, 'User not exists')); return next(new ErrorDTO(ErrorCodes.GENERAL_ERROR, 'User not exists'));
} }
const user = Utils.clone(req.session.user); const user = <UserDTO>{
delete user.password; 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); RenderingMWs.renderMessage(res, user);
} }
@ -35,8 +44,7 @@ export class RenderingMWs {
return next(); return next();
} }
const sharing = Utils.clone<SharingDTO>(req.resultPipe); const {password, creator, ...sharing} = req.resultPipe;
delete sharing.password;
RenderingMWs.renderMessage(res, sharing); RenderingMWs.renderMessage(res, sharing);
} }

View File

@ -14,7 +14,7 @@ export class SharingMWs {
if (Config.Client.Sharing.enabled === false) { if (Config.Client.Sharing.enabled === false) {
return next(); return next();
} }
const sharingKey = req.params[QueryParams.gallery.sharingKey_long]; const sharingKey = req.params[QueryParams.gallery.sharingKey_params];
try { try {
req.resultPipe = await ObjectManagers.getInstance().SharingManager.findOne({sharingKey: sharingKey}); req.resultPipe = await ObjectManagers.getInstance().SharingManager.findOne({sharingKey: sharingKey});
@ -37,7 +37,6 @@ export class SharingMWs {
let sharingKey = SharingMWs.generateKey(); let sharingKey = SharingMWs.generateKey();
// create one not yet used // create one not yet used
while (true) { while (true) {
try { try {
await ObjectManagers.getInstance().SharingManager.findOne({sharingKey: sharingKey}); await ObjectManagers.getInstance().SharingManager.findOne({sharingKey: sharingKey});

View File

@ -1,5 +1,5 @@
import {LoginCredential} from '../../../common/entities/LoginCredential'; import {LoginCredential} from '../../../common/entities/LoginCredential';
import {UserEntity} from '../../model/database/sql/enitites/UserEntity'; import {UserDTO} from '../../../common/entities/UserDTO';
declare global { declare global {
@ -18,7 +18,7 @@ declare global {
} }
interface Session { interface Session {
user?: UserEntity; user?: UserDTO;
rememberMe?: boolean; rememberMe?: boolean;
} }
} }

View File

@ -1,4 +1,3 @@
///<reference path="../customtypings/ExtendedRequest.d.ts"/>
import {NextFunction, Request, Response} from 'express'; import {NextFunction, Request, Response} from 'express';
import {ErrorCodes, ErrorDTO} from '../../../common/entities/Error'; import {ErrorCodes, ErrorDTO} from '../../../common/entities/Error';
import {UserDTO, UserRoles} from '../../../common/entities/UserDTO'; import {UserDTO, UserRoles} from '../../../common/entities/UserDTO';
@ -30,11 +29,16 @@ export class AuthenticationMWs {
} }
public static async authenticate(req: Request, res: Response, next: NextFunction) { public static async authenticate(req: Request, res: Response, next: NextFunction) {
if (Config.Client.authenticationRequired === false) { if (Config.Client.authenticationRequired === false) {
req.session.user = <UserDTO>{name: UserRoles[Config.Client.unAuthenticatedUserRole], role: Config.Client.unAuthenticatedUserRole}; req.session.user = <UserDTO>{name: UserRoles[Config.Client.unAuthenticatedUserRole], role: Config.Client.unAuthenticatedUserRole};
return next(); return next();
} }
// if already authenticated, do not try to use sharing authentication
if (typeof req.session.user !== 'undefined') {
return next();
}
try { try {
const user = await AuthenticationMWs.getSharingUser(req); const user = await AuthenticationMWs.getSharingUser(req);
if (!!user) { if (!!user) {
@ -45,12 +49,8 @@ export class AuthenticationMWs {
return next(new ErrorDTO(ErrorCodes.CREDENTIAL_NOT_FOUND, null, err)); return next(new ErrorDTO(ErrorCodes.CREDENTIAL_NOT_FOUND, null, err));
} }
if (typeof req.session.user === 'undefined') { if (typeof req.session.user === 'undefined') {
return next(new ErrorDTO(ErrorCodes.NOT_AUTHENTICATED)); res.status(401);
} return next(new ErrorDTO(ErrorCodes.NOT_AUTHENTICATED, 'Not authenticated'));
if (req.session.rememberMe === true) {
req.sessionOptions.expires = new Date(Date.now() + Config.Server.sessionTimeout);
} else {
delete (req.sessionOptions.expires);
} }
return next(); return next();
} }
@ -70,7 +70,7 @@ export class AuthenticationMWs {
p = path.dirname(p); 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); return res.sendStatus(403);
} }
@ -94,21 +94,22 @@ export class AuthenticationMWs {
return next(); return next();
} }
// not enough parameter // 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')); return next(new ErrorDTO(ErrorCodes.INPUT_ERROR, 'no sharing key provided'));
} }
try { try {
const password = (req.body ? req.body.password : null) || null; 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({ 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() || if (!sharing || sharing.expires < Date.now() ||
(Config.Client.Sharing.passwordProtected === true (Config.Client.Sharing.passwordProtected === true
&& (sharing.password) && (sharing.password)
&& !PasswordHelper.comparePassword(password, sharing.password))) { && !PasswordHelper.comparePassword(password, sharing.password))) {
res.status(401);
return next(new ErrorDTO(ErrorCodes.CREDENTIAL_NOT_FOUND)); return next(new ErrorDTO(ErrorCodes.CREDENTIAL_NOT_FOUND));
} }
@ -117,7 +118,12 @@ export class AuthenticationMWs {
sharingPath += '*'; 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(); return next();
} catch (err) { } catch (err) {
@ -135,12 +141,16 @@ export class AuthenticationMWs {
public static async login(req: Request, res: Response, next: NextFunction) { public static async login(req: Request, res: Response, next: NextFunction) {
if (Config.Client.authenticationRequired === false) {
return res.sendStatus(404);
}
// not enough parameter // not enough parameter
if ((typeof req.body === 'undefined') || if ((typeof req.body === 'undefined') ||
(typeof req.body.loginCredential === 'undefined') || (typeof req.body.loginCredential === 'undefined') ||
(typeof req.body.loginCredential.username === 'undefined') || (typeof req.body.loginCredential.username === 'undefined') ||
(typeof req.body.loginCredential.password === '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 { try {
// lets find the user // lets find the user
@ -156,7 +166,8 @@ export class AuthenticationMWs {
return next(); return next();
} catch (err) { } 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) { public static logout(req: Request, res: Response, next: NextFunction) {
delete req.session.user; delete req.session.user;
delete req.session.rememberMe;
return next(); return next();
} }
private static async getSharingUser(req: Request) { private static async getSharingUser(req: Request) {
if (Config.Client.Sharing.enabled === true && 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({ 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()) { if (!sharing || sharing.expires < Date.now()) {
return null; return null;
} }
if (Config.Client.Sharing.passwordProtected === true && (sharing.password)) { if (Config.Client.Sharing.passwordProtected === true && sharing.password) {
return null; return null;
} }

View File

@ -9,7 +9,7 @@ export class UserRequestConstrainsMWs {
if ((typeof req.params === 'undefined') || (typeof req.params.id === 'undefined')) { if ((typeof req.params === 'undefined') || (typeof req.params.id === 'undefined')) {
return next(); 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)); return next(new ErrorDTO(ErrorCodes.NOT_AUTHORISED));
} }
@ -22,7 +22,7 @@ export class UserRequestConstrainsMWs {
return next(); 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)); return next(new ErrorDTO(ErrorCodes.NOT_AUTHORISED));
} }
@ -34,7 +34,7 @@ export class UserRequestConstrainsMWs {
return next(); return next();
} }
if (req.session.user.id !== req.params.id) { if (req.session.user.id !== parseInt(req.params.id, 10)) {
return next(); return next();
} }

View File

@ -6,12 +6,16 @@ try {
} }
export class PasswordHelper { export class PasswordHelper {
public static cryptPassword(password: string) { public static cryptPassword(password: string): string {
const salt = bcrypt.genSaltSync(9); const salt = bcrypt.genSaltSync(9);
return bcrypt.hashSync(password, salt); return bcrypt.hashSync(password, salt);
} }
public static comparePassword(password: string, encryptedPassword: string) { public static comparePassword(password: string, encryptedPassword: string): boolean {
return bcrypt.compareSync(password, encryptedPassword); try {
return bcrypt.compareSync(password, encryptedPassword);
} catch (e) {
}
return false;
} }
} }

View File

@ -17,7 +17,6 @@ export class UserManager implements IUserManager {
delete filter.password; delete filter.password;
const user = (await connection.getRepository(UserEntity).findOne(filter)); const user = (await connection.getRepository(UserEntity).findOne(filter));
if (pass && !PasswordHelper.comparePassword(pass, user.password)) { if (pass && !PasswordHelper.comparePassword(pass, user.password)) {
throw new Error('No entry found'); throw new Error('No entry found');
} }

View File

@ -15,6 +15,6 @@ export class FileEntity implements FileDTO {
name: string; name: string;
@Index() @Index()
@ManyToOne(type => DirectoryEntity, directory => directory.metaFile, {onDelete: 'CASCADE'}) @ManyToOne(type => DirectoryEntity, directory => directory.metaFile, {onDelete: 'CASCADE', nullable: false})
directory: DirectoryEntity; directory: DirectoryEntity;
} }

View File

@ -71,7 +71,7 @@ export abstract class MediaEntity implements MediaDTO {
name: string; name: string;
@Index() @Index()
@ManyToOne(type => DirectoryEntity, directory => directory.media, {onDelete: 'CASCADE'}) @ManyToOne(type => DirectoryEntity, directory => directory.media, {onDelete: 'CASCADE', nullable: false})
directory: DirectoryEntity; directory: DirectoryEntity;
@Column(type => MediaMetadataEntity) @Column(type => MediaMetadataEntity)

View File

@ -36,6 +36,6 @@ export class SharingEntity implements SharingDTO {
@Column() @Column()
includeSubfolders: boolean; includeSubfolders: boolean;
@ManyToOne(type => UserEntity) @ManyToOne(type => UserEntity, {onDelete: 'CASCADE', nullable: false})
creator: UserDTO; creator: UserDTO;
} }

View File

@ -18,10 +18,17 @@ export class ErrorRouter {
private static addGenericHandler(app: Express) { private static addGenericHandler(app: Express) {
app.use((err: any, req: Request, res: Response, next: Function) => { 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 // Flush out the stack to the console
Logger.error('Unexpected error:'); Logger.error('Unexpected error:');
console.error(err); 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 RenderingMWs.renderError
); );

View File

@ -2,12 +2,12 @@ import {Express, NextFunction, Request, Response} from 'express';
import * as path from 'path'; import * as path from 'path';
import * as fs from 'fs'; import * as fs from 'fs';
import * as ejs from 'ejs'; import * as ejs from 'ejs';
import {Utils} from '../../common/Utils';
import {Config} from '../../common/config/private/Config'; import {Config} from '../../common/config/private/Config';
import {ProjectPath} from '../ProjectPath'; import {ProjectPath} from '../ProjectPath';
import {AuthenticationMWs} from '../middlewares/user/AuthenticationMWs'; import {AuthenticationMWs} from '../middlewares/user/AuthenticationMWs';
import {CookieNames} from '../../common/CookieNames'; import {CookieNames} from '../../common/CookieNames';
import {ErrorCodes, ErrorDTO} from '../../common/entities/Error'; import {ErrorCodes, ErrorDTO} from '../../common/entities/Error';
import {UserDTO} from '../../common/entities/UserDTO';
declare global { declare global {
namespace Express { namespace Express {
@ -69,9 +69,18 @@ export class PublicRouter {
res.tpl.user = null; res.tpl.user = null;
if (req.session.user) { if (req.session.user) {
const user = Utils.clone(req.session.user); res.tpl.user = <UserDTO>{
delete user.password; id: req.session.user.id,
res.tpl.user = user; 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; res.tpl.clientConfig = Config.Client;

View File

@ -23,7 +23,7 @@ export class SharingRouter {
} }
private static addGetSharing(app: express.Express) { 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.authenticate,
AuthenticationMWs.authorise(UserRoles.LimitedGuest), AuthenticationMWs.authorise(UserRoles.LimitedGuest),
SharingMWs.getSharing, SharingMWs.getSharing,

View File

@ -36,7 +36,7 @@ export class UserRouter {
private static addGetSessionUser(app: Express) { private static addGetSessionUser(app: Express) {
app.get('/api/user/login', app.get('/api/user/me',
AuthenticationMWs.authenticate, AuthenticationMWs.authenticate,
RenderingMWs.renderSessionUser RenderingMWs.renderSessionUser
); );

View File

@ -1,4 +1,5 @@
import * as _express from 'express'; import * as _express from 'express';
import {Request} from 'express';
import * as _bodyParser from 'body-parser'; import * as _bodyParser from 'body-parser';
import * as cookieParser from 'cookie-parser'; import * as cookieParser from 'cookie-parser';
import * as _http from 'http'; import * as _http from 'http';
@ -17,7 +18,9 @@ import {Router} from './routes/Router';
import {ServerConfig} from '../common/config/private/IPrivateConfig'; import {ServerConfig} from '../common/config/private/IPrivateConfig';
import {PhotoProcessing} from './model/fileprocessing/PhotoProcessing'; import {PhotoProcessing} from './model/fileprocessing/PhotoProcessing';
import * as _csrf from 'csurf'; import * as _csrf from 'csurf';
import * as unless from 'express-unless';
import {Event} from '../common/event/Event'; import {Event} from '../common/event/Event';
import {QueryParams} from '../common/QueryParams';
const _session = require('cookie-session'); const _session = require('cookie-session');
@ -75,7 +78,18 @@ export class Server {
// for parsing application/json // for parsing application/json
this.app.use(_bodyParser.json()); this.app.use(_bodyParser.json());
this.app.use(cookieParser()); 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(); DiskManager.init();
PhotoProcessing.init(); PhotoProcessing.init();

View File

@ -1 +1 @@
export const DataStructureVersion = 15; export const DataStructureVersion = 16;

View File

@ -13,11 +13,11 @@ export const QueryParams = {
type: 'type' type: 'type'
}, },
photo: 'p', photo: 'p',
sharingKey_short: 'sk', sharingKey_query: 'sk',
sharingKey_long: 'sharingKey', sharingKey_params: 'sharingKey',
searchText: 'searchText', searchText: 'searchText',
directory: 'directory', directory: 'directory',
knownLastModified: 'knownLastModified', knownLastModified: 'klm',
knownLastScanned: 'knownLastScanned' knownLastScanned: 'kls'
} }
}; };

View File

@ -93,6 +93,12 @@ export class Utils {
return arr; return arr;
} }
public static canonizePath(path: string) {
return path
.replace(new RegExp('\\\\', 'g'), '/')
.replace(new RegExp('/+', 'g'), '/');
}
static concatUrls(...args: Array<string>) { static concatUrls(...args: Array<string>) {
let url = ''; let url = '';
for (let i = 0; i < args.length; i++) { for (let i = 0; i < args.length; i++) {

View File

@ -122,7 +122,7 @@ export module ServerConfig {
Threading: ThreadingConfig; Threading: ThreadingConfig;
Database: DataBaseConfig; Database: DataBaseConfig;
Sharing: SharingConfig; Sharing: SharingConfig;
sessionTimeout: number; sessionTimeout: number; // in ms
Indexing: IndexingConfig; Indexing: IndexingConfig;
photoMetadataSize: number; // only this many bites will be loaded when scanning photo for metadata photoMetadataSize: number; // only this many bites will be loaded when scanning photo for metadata
Duplicates: DuplicatesConfig; Duplicates: DuplicatesConfig;

View File

@ -27,7 +27,7 @@ export class ErrorDTO {
public detailsStr: string; public detailsStr: string;
constructor(public code: ErrorCodes, public message?: string, public details?: any) { 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 { toString(): string {

View File

@ -15,29 +15,35 @@ export interface UserDTO {
name: string; name: string;
password: string; password: string;
role: UserRoles; role: UserRoles;
csrfToken?: string;
usedSharingKey?: string; usedSharingKey?: string;
permissions: string[]; // user can only see these permissions. if ends with *, its recursive permissions: string[]; // user can only see these permissions. if ends with *, its recursive
} }
export module UserDTO { export module UserDTO {
export const isDirectoryPathAvailable = (path: string, permissions: string[], separator = '/'): boolean => { export const isDirectoryPathAvailable = (path: string, permissions: string[]): boolean => {
if (permissions == null || permissions.length === 0 || permissions[0] === separator + '*') { if (permissions == null) {
return true;
}
permissions = permissions.map(p => Utils.canonizePath(p));
path = Utils.canonizePath(path);
if (permissions.length === 0 || permissions[0] === '/*') {
return true; return true;
} }
for (let i = 0; i < permissions.length; i++) { for (let i = 0; i < permissions.length; i++) {
let permission = permissions[i]; let permission = permissions[i];
if (permissions[i] === separator + '*') { if (permissions[i] === '/*') {
return true; return true;
} }
if (permission[permission.length - 1] === '*') { if (permission[permission.length - 1] === '*') {
permission = permission.slice(0, -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; return true;
} }
} else if (path === permission) { } else if (path === permission) {
return true; return true;
} else if (path === '.' && permission === separator) { } else if (path === '.' && permission === '/') {
return true; return true;
} }
@ -46,6 +52,7 @@ export module UserDTO {
}; };
export const isDirectoryAvailable = (directory: DirectoryDTO, permissions: string[]): boolean => { 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);
}; };
} }

View File

@ -6,10 +6,12 @@ import {Title} from '@angular/platform-browser';
import {ShareService} from './ui/gallery/share.service'; import {ShareService} from './ui/gallery/share.service';
import 'hammerjs'; import 'hammerjs';
import {Subscription} from 'rxjs'; import {Subscription} from 'rxjs';
import {QueryParams} from '../../common/QueryParams';
@Component({ @Component({
selector: 'app-pi-gallery2', selector: 'app-pi-gallery2',
template: `<router-outlet></router-outlet>` template: `
<router-outlet></router-outlet>`
}) })
export class AppComponent implements OnInit, OnDestroy { export class AppComponent implements OnInit, OnDestroy {
@ -51,7 +53,9 @@ export class AppComponent implements OnInit, OnDestroy {
private toLogin() { private toLogin() {
if (this._shareService.isSharing()) { 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 { } else {
return this._router.navigate(['login']); return this._router.navigate(['login']);
} }

View File

@ -53,7 +53,7 @@ import {SettingsService} from './ui/settings/settings.service';
import {ShareSettingsComponent} from './ui/settings/share/share.settings.component'; import {ShareSettingsComponent} from './ui/settings/share/share.settings.component';
import {BasicSettingsComponent} from './ui/settings/basic/basic.settings.component'; import {BasicSettingsComponent} from './ui/settings/basic/basic.settings.component';
import {OtherSettingsComponent} from './ui/settings/other/other.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 {DefaultUrlSerializer, UrlSerializer, UrlTree} from '@angular/router';
import {IndexingSettingsComponent} from './ui/settings/indexing/indexing.settings.component'; import {IndexingSettingsComponent} from './ui/settings/indexing/indexing.settings.component';
import {LanguageComponent} from './ui/language/language.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 {ScheduledJobsService} from './ui/settings/scheduled-jobs.service';
import {BackendtextService} from './model/backendtext.service'; import {BackendtextService} from './model/backendtext.service';
import {JobButtonComponent} from './ui/settings/jobs/button/job-button.settings.component'; 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() @Injectable()
@ -212,6 +214,8 @@ export function translationsFactory(locale: string) {
FileSizePipe FileSizePipe
], ],
providers: [ providers: [
{provide: HTTP_INTERCEPTORS, useClass: CSRFInterceptor, multi: true},
{provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true},
{provide: UrlSerializer, useClass: CustomUrlSerializer}, {provide: UrlSerializer, useClass: CustomUrlSerializer},
{provide: HAMMER_GESTURE_CONFIG, useClass: MyHammerConfig}, {provide: HAMMER_GESTURE_CONFIG, useClass: MyHammerConfig},
NetworkService, NetworkService,

View File

@ -7,6 +7,7 @@ import {ShareLoginComponent} from './ui/sharelogin/share-login.component';
import {QueryParams} from '../../common/QueryParams'; import {QueryParams} from '../../common/QueryParams';
import {DuplicateComponent} from './ui/duplicates/duplicates.component'; import {DuplicateComponent} from './ui/duplicates/duplicates.component';
import {FacesComponent} from './ui/faces/faces.component'; import {FacesComponent} from './ui/faces/faces.component';
import {AuthGuard} from './model/network/helper/auth.guard';
export function galleryMatcherFunction( export function galleryMatcherFunction(
segments: UrlSegment[]): UrlMatchResult | null { segments: UrlSegment[]): UrlMatchResult | null {
@ -32,13 +33,13 @@ export function galleryMatcherFunction(
} }
if (path === 'share') { if (path === 'share') {
if (segments.length > 1) { 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 {consumed: segments.slice(0, Math.min(segments.length, 2)), posParams};
} }
return null; return null;
} }
// Todo: authguard - canActivate https://angular.io/api/router/CanActivate
const ROUTES: Routes = [ const ROUTES: Routes = [
{ {
path: 'login', path: 'login',
@ -50,19 +51,23 @@ const ROUTES: Routes = [
}, },
{ {
path: 'admin', path: 'admin',
component: AdminComponent component: AdminComponent,
canActivate: [AuthGuard]
}, },
{ {
path: 'duplicates', path: 'duplicates',
component: DuplicateComponent component: DuplicateComponent,
canActivate: [AuthGuard]
}, },
{ {
path: 'faces', path: 'faces',
component: FacesComponent component: FacesComponent,
canActivate: [AuthGuard]
}, },
{ {
matcher: galleryMatcherFunction, matcher: galleryMatcherFunction,
component: GalleryComponent component: GalleryComponent,
canActivate: [AuthGuard]
}, },
{path: '', redirectTo: '/login', pathMatch: 'full'}, {path: '', redirectTo: '/login', pathMatch: 'full'},
{path: '**', redirectTo: '/login', pathMatch: 'full'} {path: '**', redirectTo: '/login', pathMatch: 'full'}

View File

@ -14,16 +14,16 @@ declare module ServerInject {
export let user: UserDTO; export let user: UserDTO;
} }
@Injectable() @Injectable({providedIn: 'root'})
export class AuthenticationService { export class AuthenticationService {
public user: BehaviorSubject<UserDTO>; public readonly user: BehaviorSubject<UserDTO>;
constructor(private _userService: UserService, constructor(private _userService: UserService,
private _networkService: NetworkService, private _networkService: NetworkService,
private shareService: ShareService) { private shareService: ShareService) {
this.user = new BehaviorSubject(null); this.user = new BehaviorSubject(JSON.parse(localStorage.getItem('currentUser')));
this.shareService.setUserObs(this.user);
// picking up session.. // picking up session..
if (this.isAuthenticated() === false && Cookie.get(CookieNames.session) != null) { if (this.isAuthenticated() === false && Cookie.get(CookieNames.session) != null) {
if (typeof ServerInject !== 'undefined' && typeof ServerInject.user !== 'undefined') { if (typeof ServerInject !== 'undefined' && typeof ServerInject.user !== 'undefined') {
@ -48,6 +48,17 @@ export class AuthenticationService {
return false; 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> { public async login(credential: LoginCredential): Promise<UserDTO> {
@ -82,9 +93,8 @@ export class AuthenticationService {
try { try {
this.user.next(await this._userService.getSessionUser()); this.user.next(await this._userService.getSessionUser());
} catch (error) { } catch (error) {
console.log(error); console.error(error);
} }
} }

View 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;
}
}

View 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);
}
}

View 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);
}));
}
}

View File

@ -11,7 +11,7 @@ import {VersionService} from '../version.service';
@Injectable() @Injectable()
export class NetworkService { export class NetworkService {
_apiBaseUrl = Utils.concatUrls(Config.Client.urlBase, '/api'); readonly _apiBaseUrl = Utils.concatUrls(Config.Client.urlBase, '/api');
private globalErrorHandlers: Array<(error: ErrorDTO) => boolean> = []; private globalErrorHandlers: Array<(error: ErrorDTO) => boolean> = [];
constructor(private _http: HttpClient, constructor(private _http: HttpClient,
@ -67,8 +67,8 @@ export class NetworkService {
return this.callJson('put', url, data); return this.callJson('put', url, data);
} }
public getJson<T>(url: string, data?: { [key: string]: any }): Promise<T> { public getJson<T>(url: string, query?: { [key: string]: any }): Promise<T> {
return this.callJson('get', NetworkService.buildUrl(url, data)); return this.callJson('get', NetworkService.buildUrl(url, query));
} }
public deleteJson<T>(url: string): Promise<T> { public deleteJson<T>(url: string): Promise<T> {

View File

@ -4,35 +4,38 @@ import {NetworkService} from './network.service';
import {UserDTO} from '../../../../common/entities/UserDTO'; import {UserDTO} from '../../../../common/entities/UserDTO';
import {Config} from '../../../../common/config/public/Config'; import {Config} from '../../../../common/config/public/Config';
import {ShareService} from '../../ui/gallery/share.service'; import {ShareService} from '../../ui/gallery/share.service';
import {QueryParams} from '../../../../common/QueryParams';
@Injectable() @Injectable()
export class UserService { export class UserService {
// Todo use JWT instead of costume cookie
constructor(private _networkService: NetworkService, constructor(private _networkService: NetworkService,
private _shareService: ShareService) { private _shareService: ShareService) {
} }
public logout(): Promise<string> { public async logout(): Promise<string> {
return this._networkService.postJson('/user/logout'); return this._networkService.postJson('/user/logout');
} }
public login(credential: LoginCredential): Promise<UserDTO> { public async login(credential: LoginCredential): Promise<UserDTO> {
return this._networkService.postJson<UserDTO>('/user/login', {'loginCredential': credential}); return this._networkService.postJson<UserDTO>('/user/login', {loginCredential: credential});
} }
public async shareLogin(password: string): Promise<UserDTO> { 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> { public async getSessionUser(): Promise<UserDTO> {
await this._shareService.wait(); await this._shareService.wait();
if (Config.Client.Sharing.enabled === true) { if (Config.Client.Sharing.enabled === true) {
if (this._shareService.isSharing()) { 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');
} }
} }

View File

@ -30,7 +30,7 @@ export class QueryService {
} }
if (Config.Client.Sharing.enabled === true) { if (Config.Client.Sharing.enabled === true) {
if (this.shareService.isSharing()) { if (this.shareService.isSharing()) {
query[QueryParams.gallery.sharingKey_short] = this.shareService.getSharingKey(); query[QueryParams.gallery.sharingKey_query] = this.shareService.getSharingKey();
} }
} }
return query; return query;
@ -40,7 +40,7 @@ export class QueryService {
const params: { [key: string]: any } = {}; const params: { [key: string]: any } = {};
if (Config.Client.Sharing.enabled === true) { if (Config.Client.Sharing.enabled === true) {
if (this.shareService.isSharing()) { 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 && if (directory && directory.lastModified && directory.lastScanned &&

View File

@ -60,7 +60,7 @@
</a> </a>
</li> </li>
<li role="menuitem" *ngIf="authenticationRequired"> <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> <span class="oi oi-account-logout"></span>
<ng-container i18n>Logout</ng-container> <ng-container i18n>Logout</ng-container>
</a> </a>

View File

@ -263,7 +263,9 @@ export class GalleryCacheService {
private reset() { private reset() {
try { try {
const currentUserStr = localStorage.getItem('currentUser');
localStorage.clear(); localStorage.clear();
localStorage.setItem('currentUser', currentUserStr);
localStorage.setItem(GalleryCacheService.VERSION, this.versionService.version.value); localStorage.setItem(GalleryCacheService.VERSION, this.versionService.version.value);
} catch (e) { } catch (e) {

View File

@ -17,6 +17,7 @@ import {SortingMethods} from '../../../../common/entities/SortingMethods';
import {PhotoDTO} from '../../../../common/entities/PhotoDTO'; import {PhotoDTO} from '../../../../common/entities/PhotoDTO';
import {QueryParams} from '../../../../common/QueryParams'; import {QueryParams} from '../../../../common/QueryParams';
import {SeededRandomService} from '../../model/seededRandom.service'; import {SeededRandomService} from '../../model/seededRandom.service';
import {take} from 'rxjs/operators';
@Component({ @Component({
selector: 'app-gallery', selector: 'app-gallery',
@ -57,10 +58,10 @@ export class GalleryComponent implements OnInit, OnDestroy {
} }
updateTimer(t: number) { updateTimer(t: number) {
if (this.shareService.sharing.value == null) { if (this.shareService.sharingSubject.value == null) {
return; 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 = <any>{};
this.countDown.day = Math.floor(t / 86400); this.countDown.day = Math.floor(t / 86400);
t -= this.countDown.day * 86400; t -= this.countDown.day * 86400;
@ -125,10 +126,10 @@ export class GalleryComponent implements OnInit, OnDestroy {
return; return;
} }
if (params[QueryParams.gallery.sharingKey_long] && params[QueryParams.gallery.sharingKey_long] !== '') { if (params[QueryParams.gallery.sharingKey_params] && params[QueryParams.gallery.sharingKey_params] !== '') {
const sharing = await this.shareService.getSharing(); const sharing = await this.shareService.currentSharing.pipe(take(1)).toPromise();
const qParams: { [key: string]: any } = {}; 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); this._router.navigate(['/gallery', sharing.path], {queryParams: qParams}).catch(console.error);
return; return;
} }

View File

@ -5,7 +5,6 @@ import {DirectoryDTO} from '../../../../common/entities/DirectoryDTO';
import {SearchTypes} from '../../../../common/entities/AutoCompleteItem'; import {SearchTypes} from '../../../../common/entities/AutoCompleteItem';
import {GalleryCacheService} from './cache.gallery.service'; import {GalleryCacheService} from './cache.gallery.service';
import {BehaviorSubject} from 'rxjs'; import {BehaviorSubject} from 'rxjs';
import {SharingDTO} from '../../../../common/entities/SharingDTO';
import {Config} from '../../../../common/config/public/Config'; import {Config} from '../../../../common/config/public/Config';
import {ShareService} from './share.service'; import {ShareService} from './share.service';
import {NavigationService} from '../../model/navigation.service'; import {NavigationService} from '../../model/navigation.service';
@ -79,7 +78,7 @@ export class GalleryService {
const params: { [key: string]: any } = {}; const params: { [key: string]: any } = {};
if (Config.Client.Sharing.enabled === true) { if (Config.Client.Sharing.enabled === true) {
if (this._shareService.isSharing()) { 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 { isSearchResult(): boolean {

View File

@ -2,25 +2,27 @@ import {Injectable} from '@angular/core';
import {NetworkService} from '../../model/network/network.service'; import {NetworkService} from '../../model/network/network.service';
import {CreateSharingDTO, SharingDTO} from '../../../../common/entities/SharingDTO'; import {CreateSharingDTO, SharingDTO} from '../../../../common/entities/SharingDTO';
import {Router, RoutesRecognized} from '@angular/router'; 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 {QueryParams} from '../../../../common/QueryParams';
import {UserDTO} from '../../../../common/entities/UserDTO'; import {UserDTO} from '../../../../common/entities/UserDTO';
@Injectable() @Injectable()
export class ShareService { export class ShareService {
public sharing: BehaviorSubject<SharingDTO>;
param: string = null; param: string = null;
queryParam: string = null; queryParam: string = null;
sharingKey: string = null; sharingKey: string = null;
inited = false; inited = false;
public ReadyPR: Promise<void>; 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, constructor(private networkService: NetworkService,
private router: Router) { private router: Router) {
this.sharing = new BehaviorSubject(null);
this.ReadyPR = new Promise((resolve: () => void) => { this.ReadyPR = new Promise((resolve: () => void) => {
if (this.inited === true) { if (this.inited === true) {
return resolve(); return resolve();
@ -28,15 +30,15 @@ export class ShareService {
this.resolve = resolve; this.resolve = resolve;
}); });
this.router.events.subscribe(val => { this.router.events.subscribe(async val => {
if (val instanceof RoutesRecognized) { if (val instanceof RoutesRecognized) {
this.param = val.state.root.firstChild.params[QueryParams.gallery.sharingKey_long] || null; this.param = val.state.root.firstChild.params[QueryParams.gallery.sharingKey_params] || null;
this.queryParam = val.state.root.firstChild.queryParams[QueryParams.gallery.sharingKey_short] || null; this.queryParam = val.state.root.firstChild.queryParams[QueryParams.gallery.sharingKey_query] || null;
const changed = this.sharingKey !== (this.param || this.queryParam); const changed = this.sharingKey !== (this.param || this.queryParam);
if (changed) { if (changed) {
this.sharingKey = this.param || this.queryParam || this.sharingKey; this.sharingKey = this.param || this.queryParam || this.sharingKey;
this.getSharing(); await this.getSharing();
} }
if (this.resolve) { if (this.resolve) {
this.resolve(); this.resolve();
@ -50,22 +52,21 @@ export class ShareService {
} }
public setUserObs(userOB: Observable<UserDTO>) {
userOB.subscribe((user) => { onNewUser = async (user: UserDTO) => {
if (user && !!user.usedSharingKey) { if (user && !!user.usedSharingKey) {
if (user.usedSharingKey !== this.sharingKey) { if (user.usedSharingKey !== this.sharingKey ||
this.sharingKey = user.usedSharingKey; this.sharingSubject.value == null) {
this.getSharing(); this.sharingKey = user.usedSharingKey;
} await this.getSharing();
if (this.resolve) {
this.resolve();
this.resolve = null;
this.inited = true;
}
} }
}); if (this.resolve) {
} this.resolve();
this.resolve = null;
this.inited = true;
}
}
};
public wait(): Promise<void> { public wait(): Promise<void> {
@ -104,9 +105,13 @@ export class ShareService {
} }
public async getSharing(): Promise<SharingDTO> { private async getSharing(): Promise<void> {
const sharing = await this.networkService.getJson<SharingDTO>('/share/' + this.getSharingKey()); try {
this.sharing.next(sharing); this.sharingSubject.next(null);
return sharing; const sharing = await this.networkService.getJson<SharingDTO>('/share/' + this.getSharingKey());
this.sharingSubject.next(sharing);
} catch (e) {
console.error(e);
}
} }
} }

View File

@ -13,91 +13,101 @@
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="row"> <form #shareForm="ngForm" class="form-horizontal">
<div class="col-7 col-sm-9"> <div class="row">
<input id="shareLink" <div class="col-7 col-sm-9">
name="shareLink" <input id="shareLink"
placeholder="link" name="shareLink"
class="form-control input-md" placeholder="link"
type="text" class="form-control input-md"
[ngModel]="url"> type="text"
[ngModel]="url">
</div>
<div class="col-5 col-sm-3">
<button id="copyButton" name="copyButton"
ngxClipboard
[cbContent]="url"
(cbOnSuccess)="onCopy()"
[disabled]="!shareForm.form.valid"
class="btn btn-primary btn-block" i18n>Copy
</button>
</div>
</div> </div>
<div class="col-5 col-sm-3"> <hr/>
<button id="copyButton" name="copyButton" <div class="row">
ngxClipboard [cbContent]="url" <div class="col-4">
(cbOnSuccess)="onCopy()" <label class="control-label" for="sharing-dir" i18n>Sharing:</label>
class="btn btn-primary btn-block" i18n>Copy </div>
</button> <div class="col-8">
<input disabled type="text"
name="sharing-dir"
id="sharing-dir"
class="full-width form-control"
[ngModel]="currentDir">
</div>
</div> </div>
</div>
<hr/>
<div class="row">
<div class="col-4">
<label class="control-label" i18n>Sharing:</label>
</div>
<div class="col-8">
<input disabled type="text"
class="full-width form-control"
[ngModel]="currentDir">
</div>
</div>
<div class="row"> <div class="row">
<div class="col-4"> <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]="true"
[switch-off-text]="text.No"
[switch-on-text]="text.Yes"
[switch-handle-width]="100"
[switch-label-width]="20"
(change)="update()"
[(ngModel)]="input.includeSubfolders">
</bSwitch>
</div>
</div> </div>
<div class="col-8">
<bSwitch
class="switch"
name="includeSubfolders"
[switch-on-color]="'success'"
[switch-inverse]="'inverse'"
[switch-off-text]="text.No"
[switch-on-text]="text.Yes"
[switch-handle-width]="'100'"
[switch-label-width]="'20'"
(change)="update()"
[(ngModel)]="input.includeSubfolders">
</bSwitch>
</div>
</div>
<div class="row"> <div class="row">
<div class="col-4"> <div class="col-4">
<label class="control-label"> <label class="control-label" for="share-password">
<ng-container i18n>Password</ng-container> <ng-container i18n>Password</ng-container>*:
: </label>
</label> </div>
<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"
required>
</div>
</div> </div>
<div class="col-8">
<input id="password"
class="form-control"
type="password"
(change)="update()"
[(ngModel)]="input.password"
i18n-placeholder
placeholder="Password">
</div>
</div>
<div class="row"> <div class="row">
<div class="col-4"> <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="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="valid-to"
required>
<option [ngValue]="ValidityTypes.Minutes" i18n>Minutes</option>
<option [ngValue]="ValidityTypes.Hours" i18n>Hours</option>
<option [ngValue]="ValidityTypes.Days" i18n>Days</option>
<option [ngValue]="ValidityTypes.Months" i18n>Months</option>
</select>
</div>
</div> </div>
<div class="col-4" style="padding-right: 1px"> </form>
<input class="form-control" [(ngModel)]="input.valid.amount" (change)="update()"
name="validAmount"
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"
required>
<option [ngValue]="ValidityTypes.Minutes" i18n>Minutes</option>
<option [ngValue]="ValidityTypes.Hours" i18n>Hours</option>
<option [ngValue]="ValidityTypes.Days" i18n>Days</option>
<option [ngValue]="ValidityTypes.Months" i18n>Months</option>
</select>
</div>
</div>
</div> </div>
</ng-template> </ng-template>

View File

@ -29,13 +29,13 @@ export class GalleryShareComponent implements OnInit, OnDestroy {
amount: 30, amount: 30,
type: ValidityTypes.Days type: ValidityTypes.Days
}, },
password: '' password: <string>null
}; };
currentDir = ''; currentDir = '';
sharing: SharingDTO = null; sharing: SharingDTO = null;
contentSubscription: Subscription = null; contentSubscription: Subscription = null;
passwordProtection = false; readonly passwordProtection = Config.Client.Sharing.passwordProtected;
ValidityTypes: any; readonly ValidityTypes = ValidityTypes;
modalRef: BsModalRef; modalRef: BsModalRef;
@ -49,7 +49,6 @@ export class GalleryShareComponent implements OnInit, OnDestroy {
private _notification: NotificationService, private _notification: NotificationService,
public i18n: I18n, public i18n: I18n,
private modalService: BsModalService) { private modalService: BsModalService) {
this.ValidityTypes = ValidityTypes;
this.text.Yes = i18n('Yes'); this.text.Yes = i18n('Yes');
this.text.No = i18n('No'); 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.currentDir = Utils.concatUrls((<DirectoryDTO>content.directory).path, (<DirectoryDTO>content.directory).name);
}); });
this.passwordProtection = Config.Client.Sharing.passwordProtected;
} }
ngOnDestroy() { ngOnDestroy() {

View File

@ -155,7 +155,7 @@ export abstract class SettingsComponent<T extends { [key: string]: any }, S exte
this.inProgress = false; this.inProgress = false;
return true; return true;
} catch (err) { } catch (err) {
console.log(err); console.error(err);
if (err.message) { if (err.message) {
this.error = (<ErrorDTO>err).message; this.error = (<ErrorDTO>err).message;
} }

View File

@ -61,7 +61,7 @@ export class JobButtonComponent {
this.notification.info(this.i18n('Stopping job') + ': ' + this.backendTextService.getJobName(this.jobName)); this.notification.info(this.i18n('Stopping job') + ': ' + this.backendTextService.getJobName(this.jobName));
return true; return true;
} catch (err) { } catch (err) {
console.log(err); console.error(err);
if (err.message) { if (err.message) {
this.error.emit((<ErrorDTO>err).message); this.error.emit((<ErrorDTO>err).message);
} }

View File

@ -91,7 +91,7 @@ export class UserMangerSettingsComponent implements OnInit, ISettingsComponent {
this.notification.success(this.i18n('Password protection disabled'), this.i18n('Success')); this.notification.success(this.i18n('Password protection disabled'), this.i18n('Success'));
} }
} catch (err) { } catch (err) {
console.log(err); console.error(err);
if (err.message) { if (err.message) {
this.error = (<ErrorDTO>err).message; this.error = (<ErrorDTO>err).message;
} }

View 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
};
}
}

View 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));
});
});
});

View File

@ -1,12 +1,19 @@
import {Server} from '../../../../src/backend/server';
import {Config} from '../../../../src/common/config/private/Config'; 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 {LoginCredential} from '../../../../src/common/entities/LoginCredential';
import {UserDTO, UserRoles} from '../../../../src/common/entities/UserDTO'; import {UserDTO, UserRoles} from '../../../../src/common/entities/UserDTO';
import * as path from 'path'; import * as path from 'path';
import * as util from 'util'; import * as util from 'util';
import * as rimraf from 'rimraf'; import * as rimraf from 'rimraf';
import {ServerConfig} from '../../../../src/common/config/private/IPrivateConfig';
import {SQLConnection} from '../../../../src/backend/model/database/sql/SQLConnection'; 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'; process.env.NODE_ENV = 'test';
const chai: any = require('chai'); const chai: any = require('chai');
@ -17,33 +24,170 @@ chai.use(chaiHttp);
const rimrafPR = util.promisify(rimraf); const rimrafPR = util.promisify(rimraf);
describe('UserRouter', () => { 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'); const tempDir = path.join(__dirname, '../../tmp');
beforeEach(async () => { let server: Server;
const setUp = async () => {
await rimrafPR(tempDir); await rimrafPR(tempDir);
Config.Server.Threading.enabled = false; Config.Server.Threading.enabled = false;
Config.Server.Database.type = ServerConfig.DatabaseType.sqlite; Config.Server.Database.type = ServerConfig.DatabaseType.sqlite;
Config.Server.Database.dbFolder = tempDir; 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 SQLConnection.close();
await rimrafPR(tempDir); await rimrafPR(tempDir);
};
const checkUserResult = (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 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('/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}});
result.res.should.have.status(200); 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.should.be.a('object');
should.equal(result.body.error, null); result.body.error.should.be.a('object');
result.body.result.should.deep.equal(<UserDTO>{id: 1, name: 'admin', role: UserRoles.Admin, permissions: null}); 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);
}); });
}); });
}); });

View File

@ -59,9 +59,7 @@ describe('Authentication middleware', () => {
describe('authorisePath', () => { describe('authorisePath', () => {
const req = { const req = {
session: { user: {permissions: <string[]>null},
user: {permissions: <string[]>null}
},
sessionOptions: {}, sessionOptions: {},
query: {}, query: {},
params: { params: {
@ -82,14 +80,14 @@ describe('Authentication middleware', () => {
}; };
it('should catch unauthorized path usage', async () => { 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('/sub/subsub')).to.be.eql('ok');
expect(await test('/test')).to.be.eql(403); expect(await test('/test')).to.be.eql(403);
expect(await 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/test')).to.be.eql(403);
expect(await test('/sub/subsub/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); 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/subsub2')).to.be.eql('ok');
expect(await test('/sub/subsub')).to.be.eql('ok'); expect(await test('/sub/subsub')).to.be.eql('ok');
expect(await test('/test')).to.be.eql(403); 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/test')).to.be.eql(403);
expect(await test('/sub/subsub/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); 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('/b')).to.be.eql(403);
expect(await test('/sub')).to.be.eql(403); expect(await test('/sub')).to.be.eql(403);
expect(await test('/sub/subsub2')).to.be.eql(403); expect(await test('/sub/subsub2')).to.be.eql(403);
@ -274,7 +272,7 @@ describe('Authentication middleware', () => {
}; };
const next = (err: ErrorDTO) => { const next = (err: ErrorDTO) => {
expect(err).to.be.undefined; expect(err).to.be.undefined;
expect(req.session.user).to.be.eql('test user'); expect(req.user).to.be.eql('test user');
done(); done();
}; };
ObjectManagers.getInstance().UserManager = <IUserManager>{ ObjectManagers.getInstance().UserManager = <IUserManager>{
@ -300,7 +298,7 @@ describe('Authentication middleware', () => {
}; };
const next: any = (err: ErrorDTO) => { const next: any = (err: ErrorDTO) => {
expect(err).to.be.undefined; expect(err).to.be.undefined;
expect(req.session.user).to.be.undefined; expect(req.user).to.be.undefined;
done(); done();
}; };
AuthenticationMWs.logout(req, null, next); AuthenticationMWs.logout(req, null, next);

View File

@ -3,6 +3,8 @@ import {Utils} from '../../../src/common/Utils';
describe('Utils', () => { describe('Utils', () => {
it('should concat urls', () => { 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'); 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');