diff --git a/package-lock.json b/package-lock.json index ab6e6a5a..3c2ebe71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 3a50aed0..7fa81bd5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/backend/middlewares/GalleryMWs.ts b/src/backend/middlewares/GalleryMWs.ts index 9dea2bad..09af4f73 100644 --- a/src/backend/middlewares/GalleryMWs.ts +++ b/src/backend/middlewares/GalleryMWs.ts @@ -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(); diff --git a/src/backend/middlewares/RenderingMWs.ts b/src/backend/middlewares/RenderingMWs.ts index 9df0f73c..2d886aab 100644 --- a/src/backend/middlewares/RenderingMWs.ts +++ b/src/backend/middlewares/RenderingMWs.ts @@ -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 = { + 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(req.resultPipe); - delete sharing.password; + const {password, creator, ...sharing} = req.resultPipe; RenderingMWs.renderMessage(res, sharing); } diff --git a/src/backend/middlewares/SharingMWs.ts b/src/backend/middlewares/SharingMWs.ts index 2ed0b339..088f581f 100644 --- a/src/backend/middlewares/SharingMWs.ts +++ b/src/backend/middlewares/SharingMWs.ts @@ -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}); diff --git a/src/backend/middlewares/customtypings/ExtendedRequest.d.ts b/src/backend/middlewares/customtypings/ExtendedRequest.d.ts index e04a8f1e..69da02dc 100644 --- a/src/backend/middlewares/customtypings/ExtendedRequest.d.ts +++ b/src/backend/middlewares/customtypings/ExtendedRequest.d.ts @@ -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; } } diff --git a/src/backend/middlewares/user/AuthenticationMWs.ts b/src/backend/middlewares/user/AuthenticationMWs.ts index 8dee7b49..e62d85c2 100644 --- a/src/backend/middlewares/user/AuthenticationMWs.ts +++ b/src/backend/middlewares/user/AuthenticationMWs.ts @@ -1,4 +1,3 @@ -/// 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 = {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 = {name: 'Guest', role: UserRoles.LimitedGuest, permissions: [sharingPath]}; + req.session.user = { + 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; } diff --git a/src/backend/middlewares/user/UserRequestConstrainsMWs.ts b/src/backend/middlewares/user/UserRequestConstrainsMWs.ts index ceec64b8..4d82ba86 100644 --- a/src/backend/middlewares/user/UserRequestConstrainsMWs.ts +++ b/src/backend/middlewares/user/UserRequestConstrainsMWs.ts @@ -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(); } diff --git a/src/backend/model/PasswordHelper.ts b/src/backend/model/PasswordHelper.ts index 66743d74..415649cc 100644 --- a/src/backend/model/PasswordHelper.ts +++ b/src/backend/model/PasswordHelper.ts @@ -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) { - return bcrypt.compareSync(password, encryptedPassword); + public static comparePassword(password: string, encryptedPassword: string): boolean { + try { + return bcrypt.compareSync(password, encryptedPassword); + } catch (e) { + } + return false; } } diff --git a/src/backend/model/database/sql/UserManager.ts b/src/backend/model/database/sql/UserManager.ts index c98c06a0..5a1438d3 100644 --- a/src/backend/model/database/sql/UserManager.ts +++ b/src/backend/model/database/sql/UserManager.ts @@ -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'); } diff --git a/src/backend/model/database/sql/enitites/FileEntity.ts b/src/backend/model/database/sql/enitites/FileEntity.ts index 65d59d6d..40ffb017 100644 --- a/src/backend/model/database/sql/enitites/FileEntity.ts +++ b/src/backend/model/database/sql/enitites/FileEntity.ts @@ -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; } diff --git a/src/backend/model/database/sql/enitites/MediaEntity.ts b/src/backend/model/database/sql/enitites/MediaEntity.ts index 1e85817f..24a877c1 100644 --- a/src/backend/model/database/sql/enitites/MediaEntity.ts +++ b/src/backend/model/database/sql/enitites/MediaEntity.ts @@ -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) diff --git a/src/backend/model/database/sql/enitites/SharingEntity.ts b/src/backend/model/database/sql/enitites/SharingEntity.ts index 7b69d184..481ecdab 100644 --- a/src/backend/model/database/sql/enitites/SharingEntity.ts +++ b/src/backend/model/database/sql/enitites/SharingEntity.ts @@ -36,6 +36,6 @@ export class SharingEntity implements SharingDTO { @Column() includeSubfolders: boolean; - @ManyToOne(type => UserEntity) + @ManyToOne(type => UserEntity, {onDelete: 'CASCADE', nullable: false}) creator: UserDTO; } diff --git a/src/backend/routes/ErrorRouter.ts b/src/backend/routes/ErrorRouter.ts index bb316b34..652333d8 100644 --- a/src/backend/routes/ErrorRouter.ts +++ b/src/backend/routes/ErrorRouter.ts @@ -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 ); diff --git a/src/backend/routes/PublicRouter.ts b/src/backend/routes/PublicRouter.ts index 81b7dcec..de7f0944 100644 --- a/src/backend/routes/PublicRouter.ts +++ b/src/backend/routes/PublicRouter.ts @@ -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 = { + 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; diff --git a/src/backend/routes/SharingRouter.ts b/src/backend/routes/SharingRouter.ts index 3446536b..3d55f650 100644 --- a/src/backend/routes/SharingRouter.ts +++ b/src/backend/routes/SharingRouter.ts @@ -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, diff --git a/src/backend/routes/UserRouter.ts b/src/backend/routes/UserRouter.ts index 041fafde..5a5c29d1 100644 --- a/src/backend/routes/UserRouter.ts +++ b/src/backend/routes/UserRouter.ts @@ -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 ); diff --git a/src/backend/server.ts b/src/backend/server.ts index 6484ad74..d34d6525 100644 --- a/src/backend/server.ts +++ b/src/backend/server.ts @@ -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(); diff --git a/src/common/DataStructureVersion.ts b/src/common/DataStructureVersion.ts index 9f2a44da..a6f339ae 100644 --- a/src/common/DataStructureVersion.ts +++ b/src/common/DataStructureVersion.ts @@ -1 +1 @@ -export const DataStructureVersion = 15; +export const DataStructureVersion = 16; diff --git a/src/common/QueryParams.ts b/src/common/QueryParams.ts index af0f2846..0cac6f5d 100644 --- a/src/common/QueryParams.ts +++ b/src/common/QueryParams.ts @@ -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' } }; diff --git a/src/common/Utils.ts b/src/common/Utils.ts index 31bd029e..3106bbe7 100644 --- a/src/common/Utils.ts +++ b/src/common/Utils.ts @@ -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) { let url = ''; for (let i = 0; i < args.length; i++) { diff --git a/src/common/config/private/IPrivateConfig.ts b/src/common/config/private/IPrivateConfig.ts index a1f2a341..c82ecfcd 100644 --- a/src/common/config/private/IPrivateConfig.ts +++ b/src/common/config/private/IPrivateConfig.ts @@ -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; diff --git a/src/common/entities/Error.ts b/src/common/entities/Error.ts index 2fca81f5..95535a39 100644 --- a/src/common/entities/Error.ts +++ b/src/common/entities/Error.ts @@ -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 { diff --git a/src/common/entities/UserDTO.ts b/src/common/entities/UserDTO.ts index 9443d3eb..7ce11adc 100644 --- a/src/common/entities/UserDTO.ts +++ b/src/common/entities/UserDTO.ts @@ -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); }; } diff --git a/src/frontend/app/app.component.ts b/src/frontend/app/app.component.ts index 78316482..979e11ff 100644 --- a/src/frontend/app/app.component.ts +++ b/src/frontend/app/app.component.ts @@ -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: `` + template: ` + ` }) 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']); } diff --git a/src/frontend/app/app.module.ts b/src/frontend/app/app.module.ts index 00daa115..64574a06 100644 --- a/src/frontend/app/app.module.ts +++ b/src/frontend/app/app.module.ts @@ -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, diff --git a/src/frontend/app/app.routing.ts b/src/frontend/app/app.routing.ts index 4508c9f3..a11c0b7a 100644 --- a/src/frontend/app/app.routing.ts +++ b/src/frontend/app/app.routing.ts @@ -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'} diff --git a/src/frontend/app/model/network/authentication.service.ts b/src/frontend/app/model/network/authentication.service.ts index f7c2bdfb..697143b2 100644 --- a/src/frontend/app/model/network/authentication.service.ts +++ b/src/frontend/app/model/network/authentication.service.ts @@ -14,16 +14,16 @@ declare module ServerInject { export let user: UserDTO; } -@Injectable() +@Injectable({providedIn: 'root'}) export class AuthenticationService { - public user: BehaviorSubject; + public readonly user: BehaviorSubject; 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 { @@ -82,9 +93,8 @@ export class AuthenticationService { try { this.user.next(await this._userService.getSessionUser()); } catch (error) { - console.log(error); + console.error(error); } - } diff --git a/src/frontend/app/model/network/helper/auth.guard.ts b/src/frontend/app/model/network/helper/auth.guard.ts new file mode 100644 index 00000000..05a0161a --- /dev/null +++ b/src/frontend/app/model/network/helper/auth.guard.ts @@ -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; + } +} diff --git a/src/frontend/app/model/network/helper/csrf.interceptor.ts b/src/frontend/app/model/network/helper/csrf.interceptor.ts new file mode 100644 index 00000000..d66bdb0b --- /dev/null +++ b/src/frontend/app/model/network/helper/csrf.interceptor.ts @@ -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, next: HttpHandler): Observable> { + // 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); + } +} diff --git a/src/frontend/app/model/network/helper/error.interceptor.ts b/src/frontend/app/model/network/helper/error.interceptor.ts new file mode 100644 index 00000000..93fde6b5 --- /dev/null +++ b/src/frontend/app/model/network/helper/error.interceptor.ts @@ -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, next: HttpHandler): Observable> { + 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); + })); + } +} diff --git a/src/frontend/app/model/network/network.service.ts b/src/frontend/app/model/network/network.service.ts index 649cacbb..5c70e8f0 100644 --- a/src/frontend/app/model/network/network.service.ts +++ b/src/frontend/app/model/network/network.service.ts @@ -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(url: string, data?: { [key: string]: any }): Promise { - return this.callJson('get', NetworkService.buildUrl(url, data)); + public getJson(url: string, query?: { [key: string]: any }): Promise { + return this.callJson('get', NetworkService.buildUrl(url, query)); } public deleteJson(url: string): Promise { diff --git a/src/frontend/app/model/network/user.service.ts b/src/frontend/app/model/network/user.service.ts index bb37e72d..786d2bdf 100644 --- a/src/frontend/app/model/network/user.service.ts +++ b/src/frontend/app/model/network/user.service.ts @@ -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 { + public async logout(): Promise { return this._networkService.postJson('/user/logout'); } - public login(credential: LoginCredential): Promise { - return this._networkService.postJson('/user/login', {'loginCredential': credential}); + public async login(credential: LoginCredential): Promise { + return this._networkService.postJson('/user/login', {loginCredential: credential}); } public async shareLogin(password: string): Promise { - return this._networkService.postJson('/share/login?sk=' + this._shareService.getSharingKey(), {'password': password}); + return this._networkService.postJson('/share/login?' + QueryParams.gallery.sharingKey_query + + '=' + this._shareService.getSharingKey(), {'password': password}); } public async getSessionUser(): Promise { await this._shareService.wait(); if (Config.Client.Sharing.enabled === true) { if (this._shareService.isSharing()) { - return this._networkService.getJson('/user/login', {sk: this._shareService.getSharingKey()}); + const query: any = {}; + query[QueryParams.gallery.sharingKey_query] = this._shareService.getSharingKey(); + return this._networkService.getJson('/user/me', query); } } - return this._networkService.getJson('/user/login'); + return this._networkService.getJson('/user/me'); } } diff --git a/src/frontend/app/model/query.service.ts b/src/frontend/app/model/query.service.ts index 953791cb..bee114b7 100644 --- a/src/frontend/app/model/query.service.ts +++ b/src/frontend/app/model/query.service.ts @@ -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 && diff --git a/src/frontend/app/ui/frame/frame.component.html b/src/frontend/app/ui/frame/frame.component.html index d4e204de..62513726 100644 --- a/src/frontend/app/ui/frame/frame.component.html +++ b/src/frontend/app/ui/frame/frame.component.html @@ -60,7 +60,7 @@
  • - + Logout diff --git a/src/frontend/app/ui/gallery/cache.gallery.service.ts b/src/frontend/app/ui/gallery/cache.gallery.service.ts index f84e5930..dfd240f9 100644 --- a/src/frontend/app/ui/gallery/cache.gallery.service.ts +++ b/src/frontend/app/ui/gallery/cache.gallery.service.ts @@ -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) { diff --git a/src/frontend/app/ui/gallery/gallery.component.ts b/src/frontend/app/ui/gallery/gallery.component.ts index 1d07e7aa..c9cc646c 100644 --- a/src/frontend/app/ui/gallery/gallery.component.ts +++ b/src/frontend/app/ui/gallery/gallery.component.ts @@ -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 = {}; 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; } diff --git a/src/frontend/app/ui/gallery/gallery.service.ts b/src/frontend/app/ui/gallery/gallery.service.ts index 9b411172..562010a5 100644 --- a/src/frontend/app/ui/gallery/gallery.service.ts +++ b/src/frontend/app/ui/gallery/gallery.service.ts @@ -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 { - return this.networkService.getJson('/share/' + sharingKey); - } isSearchResult(): boolean { diff --git a/src/frontend/app/ui/gallery/share.service.ts b/src/frontend/app/ui/gallery/share.service.ts index b252daa9..eab4ec69 100644 --- a/src/frontend/app/ui/gallery/share.service.ts +++ b/src/frontend/app/ui/gallery/share.service.ts @@ -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; param: string = null; queryParam: string = null; sharingKey: string = null; inited = false; public ReadyPR: Promise; - private resolve: () => void; + public sharingSubject: BehaviorSubject = 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,22 +52,21 @@ export class ShareService { } - public setUserObs(userOB: Observable) { - userOB.subscribe((user) => { - if (user && !!user.usedSharingKey) { - if (user.usedSharingKey !== this.sharingKey) { - this.sharingKey = user.usedSharingKey; - this.getSharing(); - } - if (this.resolve) { - this.resolve(); - this.resolve = null; - this.inited = true; - } + onNewUser = async (user: UserDTO) => { + if (user && !!user.usedSharingKey) { + if (user.usedSharingKey !== this.sharingKey || + this.sharingSubject.value == null) { + this.sharingKey = user.usedSharingKey; + await this.getSharing(); } - }); - } + if (this.resolve) { + this.resolve(); + this.resolve = null; + this.inited = true; + } + } + }; public wait(): Promise { @@ -104,9 +105,13 @@ export class ShareService { } - public async getSharing(): Promise { - const sharing = await this.networkService.getJson('/share/' + this.getSharingKey()); - this.sharing.next(sharing); - return sharing; + private async getSharing(): Promise { + try { + this.sharingSubject.next(null); + const sharing = await this.networkService.getJson('/share/' + this.getSharingKey()); + this.sharingSubject.next(sharing); + } catch (e) { + console.error(e); + } } } diff --git a/src/frontend/app/ui/gallery/share/share.gallery.component.html b/src/frontend/app/ui/gallery/share/share.gallery.component.html index d77b6db4..a15a8a99 100644 --- a/src/frontend/app/ui/gallery/share/share.gallery.component.html +++ b/src/frontend/app/ui/gallery/share/share.gallery.component.html @@ -13,91 +13,101 @@