mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
8
Makefile
8
Makefile
@@ -229,6 +229,9 @@ acceptance-auth-short:
|
|||||||
acceptance-auth-firefox:
|
acceptance-auth-firefox:
|
||||||
$(info Running JS acceptance-auth tests in Firefox...)
|
$(info Running JS acceptance-auth tests in Firefox...)
|
||||||
(cd frontend && npm run testcafe -- firefox:headless --test-grep "^(Common|Core)\:*" --test-meta mode=auth --config-file ./testcaferc.json "tests/acceptance")
|
(cd frontend && npm run testcafe -- firefox:headless --test-grep "^(Common|Core)\:*" --test-meta mode=auth --config-file ./testcaferc.json "tests/acceptance")
|
||||||
|
reset-mariadb:
|
||||||
|
$(info Resetting photoprism database...)
|
||||||
|
mysql < scripts/sql/reset-photoprism.sql
|
||||||
reset-mariadb-testdb:
|
reset-mariadb-testdb:
|
||||||
$(info Resetting testdb database...)
|
$(info Resetting testdb database...)
|
||||||
mysql < scripts/sql/reset-testdb.sql
|
mysql < scripts/sql/reset-testdb.sql
|
||||||
@@ -238,10 +241,7 @@ reset-mariadb-local:
|
|||||||
reset-mariadb-acceptance:
|
reset-mariadb-acceptance:
|
||||||
$(info Resetting acceptance database...)
|
$(info Resetting acceptance database...)
|
||||||
mysql < scripts/sql/reset-acceptance.sql
|
mysql < scripts/sql/reset-acceptance.sql
|
||||||
reset-mariadb-photoprism:
|
reset-mariadb-all: reset-mariadb-testdb reset-mariadb-local reset-mariadb-acceptance reset-mariadb-photoprism
|
||||||
$(info Resetting photoprism database...)
|
|
||||||
mysql < scripts/sql/reset-photoprism.sql
|
|
||||||
reset-mariadb: reset-mariadb-testdb reset-mariadb-local reset-mariadb-acceptance reset-mariadb-photoprism
|
|
||||||
reset-testdb: reset-sqlite reset-mariadb-testdb
|
reset-testdb: reset-sqlite reset-mariadb-testdb
|
||||||
reset-acceptance: reset-mariadb-acceptance
|
reset-acceptance: reset-mariadb-acceptance
|
||||||
reset-sqlite:
|
reset-sqlite:
|
||||||
|
|||||||
489
frontend/package-lock.json
generated
489
frontend/package-lock.json
generated
@@ -122,9 +122,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/cli": {
|
"node_modules/@babel/cli": {
|
||||||
"version": "7.18.10",
|
"version": "7.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.19.3.tgz",
|
||||||
"integrity": "sha512-dLvWH+ZDFAkd2jPBSghrsFBuXrREvFwjpDycXbmUoeochqKYe4zNSLEJYErpLg8dvxvZYe79/MkN461XCwpnGw==",
|
"integrity": "sha512-643/TybmaCAe101m2tSVHi9UKpETXP9c/Ff4mD2tAwkdP6esKIfaauZFc67vGEM6r9fekbEGid+sZhbEnSe3dg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/trace-mapping": "^0.3.8",
|
"@jridgewell/trace-mapping": "^0.3.8",
|
||||||
"commander": "^4.0.1",
|
"commander": "^4.0.1",
|
||||||
@@ -161,28 +161,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/compat-data": {
|
"node_modules/@babel/compat-data": {
|
||||||
"version": "7.19.1",
|
"version": "7.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.19.3.tgz",
|
||||||
"integrity": "sha512-72a9ghR0gnESIa7jBN53U32FOVCEoztyIlKaNoU05zRhEecduGK9L9c3ww7Mp06JiR+0ls0GBPFJQwwtjn9ksg==",
|
"integrity": "sha512-prBHMK4JYYK+wDjJF1q99KK4JLL+egWS4nmNqdlMUgCExMZ+iZW0hGhyC3VEbsPjvaN0TBhW//VIFwBrk8sEiw==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/core": {
|
"node_modules/@babel/core": {
|
||||||
"version": "7.19.1",
|
"version": "7.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.3.tgz",
|
||||||
"integrity": "sha512-1H8VgqXme4UXCRv7/Wa1bq7RVymKOzC7znjyFM8KiEzwFqcKUKYNoQef4GhdklgNvoBXyW4gYhuBNCM5o1zImw==",
|
"integrity": "sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.1.0",
|
"@ampproject/remapping": "^2.1.0",
|
||||||
"@babel/code-frame": "^7.18.6",
|
"@babel/code-frame": "^7.18.6",
|
||||||
"@babel/generator": "^7.19.0",
|
"@babel/generator": "^7.19.3",
|
||||||
"@babel/helper-compilation-targets": "^7.19.1",
|
"@babel/helper-compilation-targets": "^7.19.3",
|
||||||
"@babel/helper-module-transforms": "^7.19.0",
|
"@babel/helper-module-transforms": "^7.19.0",
|
||||||
"@babel/helpers": "^7.19.0",
|
"@babel/helpers": "^7.19.0",
|
||||||
"@babel/parser": "^7.19.1",
|
"@babel/parser": "^7.19.3",
|
||||||
"@babel/template": "^7.18.10",
|
"@babel/template": "^7.18.10",
|
||||||
"@babel/traverse": "^7.19.1",
|
"@babel/traverse": "^7.19.3",
|
||||||
"@babel/types": "^7.19.0",
|
"@babel/types": "^7.19.3",
|
||||||
"convert-source-map": "^1.7.0",
|
"convert-source-map": "^1.7.0",
|
||||||
"debug": "^4.1.0",
|
"debug": "^4.1.0",
|
||||||
"gensync": "^1.0.0-beta.2",
|
"gensync": "^1.0.0-beta.2",
|
||||||
@@ -215,11 +215,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/generator": {
|
"node_modules/@babel/generator": {
|
||||||
"version": "7.19.0",
|
"version": "7.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.19.3.tgz",
|
||||||
"integrity": "sha512-S1ahxf1gZ2dpoiFgA+ohK9DIpz50bJ0CWs7Zlzb54Z4sG8qmdIrGrVqmy1sAtTVRb+9CU6U8VqT9L0Zj7hxHVg==",
|
"integrity": "sha512-fqVZnmp1ncvZU757UzDheKZpfPgatqY59XtW2/j/18H7u76akb8xqvjw82f+i2UKd/ksYsSick/BCLQUUtJ/qQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.19.0",
|
"@babel/types": "^7.19.3",
|
||||||
"@jridgewell/gen-mapping": "^0.3.2",
|
"@jridgewell/gen-mapping": "^0.3.2",
|
||||||
"jsesc": "^2.5.1"
|
"jsesc": "^2.5.1"
|
||||||
},
|
},
|
||||||
@@ -264,11 +264,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-compilation-targets": {
|
"node_modules/@babel/helper-compilation-targets": {
|
||||||
"version": "7.19.1",
|
"version": "7.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.3.tgz",
|
||||||
"integrity": "sha512-LlLkkqhCMyz2lkQPvJNdIYU7O5YjWRgC2R4omjCTpZd8u8KMQzZvX4qce+/BluN1rcQiV7BoGUpmQ0LeHerbhg==",
|
"integrity": "sha512-65ESqLGyGmLvgR0mst5AdW1FkNlj9rQsCKduzEoEPhBCDFGXvz2jW6bXFG6i0/MrV2s7hhXjjb2yAzcPuQlLwg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/compat-data": "^7.19.1",
|
"@babel/compat-data": "^7.19.3",
|
||||||
"@babel/helper-validator-option": "^7.18.6",
|
"@babel/helper-validator-option": "^7.18.6",
|
||||||
"browserslist": "^4.21.3",
|
"browserslist": "^4.21.3",
|
||||||
"semver": "^6.3.0"
|
"semver": "^6.3.0"
|
||||||
@@ -562,9 +562,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/parser": {
|
"node_modules/@babel/parser": {
|
||||||
"version": "7.19.1",
|
"version": "7.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.19.3.tgz",
|
||||||
"integrity": "sha512-h7RCSorm1DdTVGJf3P2Mhj3kdnkmF/EiysUkzS2TdgAYqyjFdMQJbVuXOBej2SBJaXan/lIVtT6KkGbyyq753A==",
|
"integrity": "sha512-pJ9xOlNWHiy9+FuFP09DEAFbAn4JskgRsVcc169w2xRBC3FRGuQEwjeIMMND9L2zc0iEhO/tGv4Zq+km+hxNpQ==",
|
||||||
"bin": {
|
"bin": {
|
||||||
"parser": "bin/babel-parser.js"
|
"parser": "bin/babel-parser.js"
|
||||||
},
|
},
|
||||||
@@ -1510,12 +1510,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/preset-env": {
|
"node_modules/@babel/preset-env": {
|
||||||
"version": "7.19.1",
|
"version": "7.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.19.3.tgz",
|
||||||
"integrity": "sha512-c8B2c6D16Lp+Nt6HcD+nHl0VbPKVnNPTpszahuxJJnurfMtKeZ80A+qUv48Y7wqvS+dTFuLuaM9oYxyNHbCLWA==",
|
"integrity": "sha512-ziye1OTc9dGFOAXSWKUqQblYHNlBOaDl8wzqf2iKXJAltYiR3hKHUKmkt+S9PppW7RQpq4fFCrwwpIDj/f5P4w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/compat-data": "^7.19.1",
|
"@babel/compat-data": "^7.19.3",
|
||||||
"@babel/helper-compilation-targets": "^7.19.1",
|
"@babel/helper-compilation-targets": "^7.19.3",
|
||||||
"@babel/helper-plugin-utils": "^7.19.0",
|
"@babel/helper-plugin-utils": "^7.19.0",
|
||||||
"@babel/helper-validator-option": "^7.18.6",
|
"@babel/helper-validator-option": "^7.18.6",
|
||||||
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6",
|
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6",
|
||||||
@@ -1583,7 +1583,7 @@
|
|||||||
"@babel/plugin-transform-unicode-escapes": "^7.18.10",
|
"@babel/plugin-transform-unicode-escapes": "^7.18.10",
|
||||||
"@babel/plugin-transform-unicode-regex": "^7.18.6",
|
"@babel/plugin-transform-unicode-regex": "^7.18.6",
|
||||||
"@babel/preset-modules": "^0.1.5",
|
"@babel/preset-modules": "^0.1.5",
|
||||||
"@babel/types": "^7.19.0",
|
"@babel/types": "^7.19.3",
|
||||||
"babel-plugin-polyfill-corejs2": "^0.3.3",
|
"babel-plugin-polyfill-corejs2": "^0.3.3",
|
||||||
"babel-plugin-polyfill-corejs3": "^0.6.0",
|
"babel-plugin-polyfill-corejs3": "^0.6.0",
|
||||||
"babel-plugin-polyfill-regenerator": "^0.4.1",
|
"babel-plugin-polyfill-regenerator": "^0.4.1",
|
||||||
@@ -1655,18 +1655,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/traverse": {
|
"node_modules/@babel/traverse": {
|
||||||
"version": "7.19.1",
|
"version": "7.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.19.3.tgz",
|
||||||
"integrity": "sha512-0j/ZfZMxKukDaag2PtOPDbwuELqIar6lLskVPPJDjXMXjfLb1Obo/1yjxIGqqAJrmfaTIY3z2wFLAQ7qSkLsuA==",
|
"integrity": "sha512-qh5yf6149zhq2sgIXmwjnsvmnNQC2iw70UFjp4olxucKrWd/dvlUsBI88VSLUsnMNF7/vnOiA+nk1+yLoCqROQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.18.6",
|
"@babel/code-frame": "^7.18.6",
|
||||||
"@babel/generator": "^7.19.0",
|
"@babel/generator": "^7.19.3",
|
||||||
"@babel/helper-environment-visitor": "^7.18.9",
|
"@babel/helper-environment-visitor": "^7.18.9",
|
||||||
"@babel/helper-function-name": "^7.19.0",
|
"@babel/helper-function-name": "^7.19.0",
|
||||||
"@babel/helper-hoist-variables": "^7.18.6",
|
"@babel/helper-hoist-variables": "^7.18.6",
|
||||||
"@babel/helper-split-export-declaration": "^7.18.6",
|
"@babel/helper-split-export-declaration": "^7.18.6",
|
||||||
"@babel/parser": "^7.19.1",
|
"@babel/parser": "^7.19.3",
|
||||||
"@babel/types": "^7.19.0",
|
"@babel/types": "^7.19.3",
|
||||||
"debug": "^4.1.0",
|
"debug": "^4.1.0",
|
||||||
"globals": "^11.1.0"
|
"globals": "^11.1.0"
|
||||||
},
|
},
|
||||||
@@ -1675,12 +1675,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/types": {
|
"node_modules/@babel/types": {
|
||||||
"version": "7.19.0",
|
"version": "7.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.3.tgz",
|
||||||
"integrity": "sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA==",
|
"integrity": "sha512-hGCaQzIY22DJlDh9CH7NOxgKkFjBk0Cw9xDO1Xmh2151ti7wiGfQ3LauXzL4HP1fmFlTX6XjpRETTpUcv7wQLw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-string-parser": "^7.18.10",
|
"@babel/helper-string-parser": "^7.18.10",
|
||||||
"@babel/helper-validator-identifier": "^7.18.6",
|
"@babel/helper-validator-identifier": "^7.19.1",
|
||||||
"to-fast-properties": "^2.0.0"
|
"to-fast-properties": "^2.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -2033,9 +2033,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@humanwhocodes/config-array": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.10.4",
|
"version": "0.10.5",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.4.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.5.tgz",
|
||||||
"integrity": "sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw==",
|
"integrity": "sha512-XVVDtp+dVvRxMoxSiSfasYaG02VEe1qH5cKgMQJWhol6HwzbcqoCMJi8dAGoYAO57jhUyhI6cWuRiTcRaDaYug==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@humanwhocodes/object-schema": "^1.2.1",
|
"@humanwhocodes/object-schema": "^1.2.1",
|
||||||
"debug": "^4.1.1",
|
"debug": "^4.1.1",
|
||||||
@@ -2350,9 +2350,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "18.7.18",
|
"version": "18.7.23",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.18.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.23.tgz",
|
||||||
"integrity": "sha512-m+6nTEOadJZuTPkKR/SYK3A2d7FZrgElol9UP1Kae90VVU4a6mxnPuLiIW1m4Cq4gZ/nWb9GrdVXJCoCazDAbg=="
|
"integrity": "sha512-DWNcCHolDq0ZKGizjx2DZjR/PqsYwAcYUJmfMWqtVU2MBMG5Mo+xFZrhGId5r/O5HOuMPyQEcM6KUBp5lBZZBg=="
|
||||||
},
|
},
|
||||||
"node_modules/@types/parse-json": {
|
"node_modules/@types/parse-json": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
@@ -2370,36 +2370,36 @@
|
|||||||
"integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q=="
|
"integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q=="
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-core": {
|
"node_modules/@vue/compiler-core": {
|
||||||
"version": "3.2.39",
|
"version": "3.2.40",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.39.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.40.tgz",
|
||||||
"integrity": "sha512-mf/36OWXqWn0wsC40nwRRGheR/qoID+lZXbIuLnr4/AngM0ov8Xvv8GHunC0rKRIkh60bTqydlqTeBo49rlbqw==",
|
"integrity": "sha512-2Dc3Stk0J/VyQ4OUr2yEC53kU28614lZS+bnrCbFSAIftBJ40g/2yQzf4mPBiFuqguMB7hyHaujdgZAQ67kZYA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.16.4",
|
"@babel/parser": "^7.16.4",
|
||||||
"@vue/shared": "3.2.39",
|
"@vue/shared": "3.2.40",
|
||||||
"estree-walker": "^2.0.2",
|
"estree-walker": "^2.0.2",
|
||||||
"source-map": "^0.6.1"
|
"source-map": "^0.6.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-dom": {
|
"node_modules/@vue/compiler-dom": {
|
||||||
"version": "3.2.39",
|
"version": "3.2.40",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.39.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.40.tgz",
|
||||||
"integrity": "sha512-HMFI25Be1C8vLEEv1hgEO1dWwG9QQ8LTTPmCkblVJY/O3OvWx6r1+zsox5mKPMGvqYEZa6l8j+xgOfUspgo7hw==",
|
"integrity": "sha512-OZCNyYVC2LQJy4H7h0o28rtk+4v+HMQygRTpmibGoG9wZyomQiS5otU7qo3Wlq5UfHDw2RFwxb9BJgKjVpjrQw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-core": "3.2.39",
|
"@vue/compiler-core": "3.2.40",
|
||||||
"@vue/shared": "3.2.39"
|
"@vue/shared": "3.2.40"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-sfc": {
|
"node_modules/@vue/compiler-sfc": {
|
||||||
"version": "3.2.39",
|
"version": "3.2.40",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.39.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.40.tgz",
|
||||||
"integrity": "sha512-fqAQgFs1/BxTUZkd0Vakn3teKUt//J3c420BgnYgEOoVdTwYpBTSXCMJ88GOBCylmUBbtquGPli9tVs7LzsWIA==",
|
"integrity": "sha512-tzqwniIN1fu1PDHC3CpqY/dPCfN/RN1thpBC+g69kJcrl7mbGiHKNwbA6kJ3XKKy8R6JLKqcpVugqN4HkeBFFg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.16.4",
|
"@babel/parser": "^7.16.4",
|
||||||
"@vue/compiler-core": "3.2.39",
|
"@vue/compiler-core": "3.2.40",
|
||||||
"@vue/compiler-dom": "3.2.39",
|
"@vue/compiler-dom": "3.2.40",
|
||||||
"@vue/compiler-ssr": "3.2.39",
|
"@vue/compiler-ssr": "3.2.40",
|
||||||
"@vue/reactivity-transform": "3.2.39",
|
"@vue/reactivity-transform": "3.2.40",
|
||||||
"@vue/shared": "3.2.39",
|
"@vue/shared": "3.2.40",
|
||||||
"estree-walker": "^2.0.2",
|
"estree-walker": "^2.0.2",
|
||||||
"magic-string": "^0.25.7",
|
"magic-string": "^0.25.7",
|
||||||
"postcss": "^8.1.10",
|
"postcss": "^8.1.10",
|
||||||
@@ -2407,12 +2407,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-ssr": {
|
"node_modules/@vue/compiler-ssr": {
|
||||||
"version": "3.2.39",
|
"version": "3.2.40",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.39.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.40.tgz",
|
||||||
"integrity": "sha512-EoGCJ6lincKOZGW+0Ky4WOKsSmqL7hp1ZYgen8M7u/mlvvEQUaO9tKKOy7K43M9U2aA3tPv0TuYYQFrEbK2eFQ==",
|
"integrity": "sha512-80cQcgasKjrPPuKcxwuCx7feq+wC6oFl5YaKSee9pV3DNq+6fmCVwEEC3vvkf/E2aI76rIJSOYHsWSEIxK74oQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.2.39",
|
"@vue/compiler-dom": "3.2.40",
|
||||||
"@vue/shared": "3.2.39"
|
"@vue/shared": "3.2.40"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/component-compiler-utils": {
|
"node_modules/@vue/component-compiler-utils": {
|
||||||
@@ -2455,26 +2455,26 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/reactivity-transform": {
|
"node_modules/@vue/reactivity-transform": {
|
||||||
"version": "3.2.39",
|
"version": "3.2.40",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.39.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.40.tgz",
|
||||||
"integrity": "sha512-HGuWu864zStiWs9wBC6JYOP1E00UjMdDWIG5W+FpUx28hV3uz9ODOKVNm/vdOy/Pvzg8+OcANxAVC85WFBbl3A==",
|
"integrity": "sha512-HQUCVwEaacq6fGEsg2NUuGKIhUveMCjOk8jGHqLXPI2w6zFoPrlQhwWEaINTv5kkZDXKEnCijAp+4gNEHG03yw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.16.4",
|
"@babel/parser": "^7.16.4",
|
||||||
"@vue/compiler-core": "3.2.39",
|
"@vue/compiler-core": "3.2.40",
|
||||||
"@vue/shared": "3.2.39",
|
"@vue/shared": "3.2.40",
|
||||||
"estree-walker": "^2.0.2",
|
"estree-walker": "^2.0.2",
|
||||||
"magic-string": "^0.25.7"
|
"magic-string": "^0.25.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/shared": {
|
"node_modules/@vue/shared": {
|
||||||
"version": "3.2.39",
|
"version": "3.2.40",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.39.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.40.tgz",
|
||||||
"integrity": "sha512-D3dl2ZB9qE6mTuWPk9RlhDeP1dgNRUKC3NJxji74A4yL8M2MwlhLKUC/49WHjrNzSPug58fWx/yFbaTzGAQSBw=="
|
"integrity": "sha512-0PLQ6RUtZM0vO3teRfzGi4ltLUO5aO+kLgwh4Um3THSR03rpQWLTuRCkuO5A41ITzwdWeKdPHtSARuPkoo5pCQ=="
|
||||||
},
|
},
|
||||||
"node_modules/@vvo/tzdb": {
|
"node_modules/@vvo/tzdb": {
|
||||||
"version": "6.64.0",
|
"version": "6.68.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vvo/tzdb/-/tzdb-6.64.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vvo/tzdb/-/tzdb-6.68.0.tgz",
|
||||||
"integrity": "sha512-EdX+RDaodSS05iq+rK+zAYzpNQX/ZKzRpBSiKk0m5ZcLdOfjvZ1kIyCJSVN7weL2tLkCFZ1q0tdljJTTyPpkQA=="
|
"integrity": "sha512-gTYX0c/zfvdeywLFZdHJzxczXjZf4oZHRnkTemziyn4p0R+qoqdrRK5PqY2DFnH64YkFcpreNS1JbbnfWiMQgQ=="
|
||||||
},
|
},
|
||||||
"node_modules/@webassemblyjs/ast": {
|
"node_modules/@webassemblyjs/ast": {
|
||||||
"version": "1.11.1",
|
"version": "1.11.1",
|
||||||
@@ -3502,9 +3502,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001409",
|
"version": "1.0.30001412",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001409.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001412.tgz",
|
||||||
"integrity": "sha512-V0mnJ5dwarmhYv8/MzhJ//aW68UpvnQBXv8lJ2QUsvn2pHcmAuNtu8hQEDz37XnA1iE+lRR9CIfGWWpgJ5QedQ==",
|
"integrity": "sha512-+TeEIee1gS5bYOiuf+PS/kp2mrXic37Hl66VY6EAfxasIk5fELTktK2oOezYed12H8w7jt3s512PpulQidPjwA==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -3941,9 +3941,9 @@
|
|||||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
|
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
|
||||||
},
|
},
|
||||||
"node_modules/core-js": {
|
"node_modules/core-js": {
|
||||||
"version": "3.25.2",
|
"version": "3.25.3",
|
||||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.25.2.tgz",
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.25.3.tgz",
|
||||||
"integrity": "sha512-YB4IAT1bjEfxTJ1XYy11hJAKskO+qmhuDBM8/guIfMz4JvdsAQAqvyb97zXX7JgSrfPLG5mRGFWJwJD39ruq2A==",
|
"integrity": "sha512-y1hvKXmPHvm5B7w4ln1S4uc9eV/O5+iFExSRUimnvIph11uaizFR8LFMdONN8hG3P2pipUfX4Y/fR8rAEtcHcQ==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -3951,9 +3951,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/core-js-compat": {
|
"node_modules/core-js-compat": {
|
||||||
"version": "3.25.2",
|
"version": "3.25.3",
|
||||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.25.2.tgz",
|
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.25.3.tgz",
|
||||||
"integrity": "sha512-TxfyECD4smdn3/CjWxczVtJqVLEEC2up7/82t7vC0AzNogr+4nQ8vyF7abxAuTXWvjTClSbvGhU0RgqA4ToQaQ==",
|
"integrity": "sha512-xVtYpJQ5grszDHEUU9O7XbjjcZ0ccX3LgQsyqSvTnjX97ZqEgn9F5srmrwwwMtbKzDllyFPL+O+2OFMl1lU4TQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browserslist": "^4.21.4"
|
"browserslist": "^4.21.4"
|
||||||
},
|
},
|
||||||
@@ -4376,9 +4376,9 @@
|
|||||||
"integrity": "sha512-APql/TZ6FdLEpf2z7/X2a2zyqK8juYtqaSVqxw9mYoQ64CXkfU15AeLh8pUszT8+fnYjgm6t0aIYpWKJbnLkuA=="
|
"integrity": "sha512-APql/TZ6FdLEpf2z7/X2a2zyqK8juYtqaSVqxw9mYoQ64CXkfU15AeLh8pUszT8+fnYjgm6t0aIYpWKJbnLkuA=="
|
||||||
},
|
},
|
||||||
"node_modules/date-format": {
|
"node_modules/date-format": {
|
||||||
"version": "4.0.13",
|
"version": "4.0.14",
|
||||||
"resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz",
|
||||||
"integrity": "sha512-bnYCwf8Emc3pTD8pXnre+wfnjGtfi5ncMDKy7+cWZXbmRAsdWkOQHrfC1yz/KiwP5thDp2kCHWYWKBX4HP1hoQ==",
|
"integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4.0"
|
"node": ">=4.0"
|
||||||
}
|
}
|
||||||
@@ -4661,9 +4661,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.4.256",
|
"version": "1.4.265",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.256.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.265.tgz",
|
||||||
"integrity": "sha512-x+JnqyluoJv8I0U9gVe+Sk2st8vF0CzMt78SXxuoWCooLLY2k5VerIBdpvG7ql6GKI4dzNnPjmqgDJ76EdaAKw=="
|
"integrity": "sha512-38KaYBNs0oCzWCpr6j7fY/W9vF0vSp4tKFIshQTgdZMhUpkxgotkQgjJP6iGMdmlsgMs3i0/Hkko4UXLTrkYVQ=="
|
||||||
},
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
@@ -4822,21 +4822,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/es-abstract": {
|
"node_modules/es-abstract": {
|
||||||
"version": "1.20.2",
|
"version": "1.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.3.tgz",
|
||||||
"integrity": "sha512-XxXQuVNrySBNlEkTYJoDNFe5+s2yIOpzq80sUHEdPdQr0S5nTLz4ZPPPswNIpKseDDUS5yghX1gfLIHQZ1iNuQ==",
|
"integrity": "sha512-AyrnaKVpMzljIdwjzrj+LxGmj8ik2LckwXacHqrJJ/jxz6dDDBcZ7I7nlHM0FvEW8MfbWJwOd+yT2XzYW49Frw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind": "^1.0.2",
|
"call-bind": "^1.0.2",
|
||||||
"es-to-primitive": "^1.2.1",
|
"es-to-primitive": "^1.2.1",
|
||||||
"function-bind": "^1.1.1",
|
"function-bind": "^1.1.1",
|
||||||
"function.prototype.name": "^1.1.5",
|
"function.prototype.name": "^1.1.5",
|
||||||
"get-intrinsic": "^1.1.2",
|
"get-intrinsic": "^1.1.3",
|
||||||
"get-symbol-description": "^1.0.0",
|
"get-symbol-description": "^1.0.0",
|
||||||
"has": "^1.0.3",
|
"has": "^1.0.3",
|
||||||
"has-property-descriptors": "^1.0.0",
|
"has-property-descriptors": "^1.0.0",
|
||||||
"has-symbols": "^1.0.3",
|
"has-symbols": "^1.0.3",
|
||||||
"internal-slot": "^1.0.3",
|
"internal-slot": "^1.0.3",
|
||||||
"is-callable": "^1.2.4",
|
"is-callable": "^1.2.6",
|
||||||
"is-negative-zero": "^2.0.2",
|
"is-negative-zero": "^2.0.2",
|
||||||
"is-regex": "^1.1.4",
|
"is-regex": "^1.1.4",
|
||||||
"is-shared-array-buffer": "^1.0.2",
|
"is-shared-array-buffer": "^1.0.2",
|
||||||
@@ -4846,6 +4846,7 @@
|
|||||||
"object-keys": "^1.1.1",
|
"object-keys": "^1.1.1",
|
||||||
"object.assign": "^4.1.4",
|
"object.assign": "^4.1.4",
|
||||||
"regexp.prototype.flags": "^1.4.3",
|
"regexp.prototype.flags": "^1.4.3",
|
||||||
|
"safe-regex-test": "^1.0.0",
|
||||||
"string.prototype.trimend": "^1.0.5",
|
"string.prototype.trimend": "^1.0.5",
|
||||||
"string.prototype.trimstart": "^1.0.5",
|
"string.prototype.trimstart": "^1.0.5",
|
||||||
"unbox-primitive": "^1.0.2"
|
"unbox-primitive": "^1.0.2"
|
||||||
@@ -5255,9 +5256,9 @@
|
|||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-n": {
|
"node_modules/eslint-plugin-n": {
|
||||||
"version": "15.2.5",
|
"version": "15.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.3.0.tgz",
|
||||||
"integrity": "sha512-8+BYsqiyZfpu6NXmdLOXVUfk8IocpCjpd8nMRRH0A9ulrcemhb2VI9RSJMEy5udx++A/YcVPD11zT8hpFq368g==",
|
"integrity": "sha512-IyzPnEWHypCWasDpxeJnim60jhlumbmq0pubL6IOcnk8u2y53s5QfT8JnXy7skjHJ44yWHRb11PLtDHuu1kg/Q==",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"builtins": "^5.0.1",
|
"builtins": "^5.0.1",
|
||||||
@@ -7328,9 +7329,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-callable": {
|
"node_modules/is-callable": {
|
||||||
"version": "1.2.6",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
|
||||||
"integrity": "sha512-krO72EO2NptOGAX2KYyqbP9vYMlNAXdB53rq6f8LXY6RY7JdSR/3BD6wLUlPHSAesmY9vstNrjvqGaCiRK/91Q==",
|
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
},
|
},
|
||||||
@@ -11049,15 +11050,28 @@
|
|||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-regex-test": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.2",
|
||||||
|
"get-intrinsic": "^1.1.3",
|
||||||
|
"is-regex": "^1.1.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/safer-buffer": {
|
"node_modules/safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||||
},
|
},
|
||||||
"node_modules/sass": {
|
"node_modules/sass": {
|
||||||
"version": "1.54.9",
|
"version": "1.55.0",
|
||||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.54.9.tgz",
|
"resolved": "https://registry.npmjs.org/sass/-/sass-1.55.0.tgz",
|
||||||
"integrity": "sha512-xb1hjASzEH+0L0WI9oFjqhRi51t/gagWnxLiwUNMltA0Ab6jIDkAacgKiGYKM9Jhy109osM7woEEai6SXeJo5Q==",
|
"integrity": "sha512-Pk+PMy7OGLs9WaxZGJMn7S96dvlyVBwwtToX895WmCpAOr5YiJYEUJfiJidMuKb613z2xNWcXCHEuOvjZbqC6A==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": ">=3.0.0 <4.0.0",
|
"chokidar": ">=3.0.0 <4.0.0",
|
||||||
"immutable": "^4.0.0",
|
"immutable": "^4.0.0",
|
||||||
@@ -11640,11 +11654,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/streamroller": {
|
"node_modules/streamroller": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.3.tgz",
|
||||||
"integrity": "sha512-wZswqzbgGGsXYIrBYhOE0yP+nQ6XRk7xDcYwuQAGTYXdyAUmvgVFE0YU1g5pvQT0m7GBaQfYcSnlHbapuK0H0A==",
|
"integrity": "sha512-CphIJyFx2SALGHeINanjFRKQ4l7x2c+rXYJ4BMq0gd+ZK0gi4VT8b+eHe2wi58x4UayBAKx4xtHpXT/ea1cz8w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"date-format": "^4.0.13",
|
"date-format": "^4.0.14",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"fs-extra": "^8.1.0"
|
"fs-extra": "^8.1.0"
|
||||||
},
|
},
|
||||||
@@ -12236,9 +12250,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/uglify-js": {
|
"node_modules/uglify-js": {
|
||||||
"version": "3.17.1",
|
"version": "3.17.2",
|
||||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.2.tgz",
|
||||||
"integrity": "sha512-+juFBsLLw7AqMaqJ0GFvlsGZwdQfI2ooKQB39PSBgMnMakcFosi9O8jCwE+2/2nMNcc0z63r9mwjoDG8zr+q0Q==",
|
"integrity": "sha512-bbxglRjsGQMchfvXZNusUcYgiB9Hx2K4AHYXQy2DITZ9Rd+JzhX7+hoocE5Winr7z2oHvPsekkBwXtigvxevXg==",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"uglifyjs": "bin/uglifyjs"
|
"uglifyjs": "bin/uglifyjs"
|
||||||
@@ -13428,9 +13442,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/cli": {
|
"@babel/cli": {
|
||||||
"version": "7.18.10",
|
"version": "7.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.19.3.tgz",
|
||||||
"integrity": "sha512-dLvWH+ZDFAkd2jPBSghrsFBuXrREvFwjpDycXbmUoeochqKYe4zNSLEJYErpLg8dvxvZYe79/MkN461XCwpnGw==",
|
"integrity": "sha512-643/TybmaCAe101m2tSVHi9UKpETXP9c/Ff4mD2tAwkdP6esKIfaauZFc67vGEM6r9fekbEGid+sZhbEnSe3dg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@jridgewell/trace-mapping": "^0.3.8",
|
"@jridgewell/trace-mapping": "^0.3.8",
|
||||||
"@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3",
|
"@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3",
|
||||||
@@ -13452,25 +13466,25 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/compat-data": {
|
"@babel/compat-data": {
|
||||||
"version": "7.19.1",
|
"version": "7.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.19.3.tgz",
|
||||||
"integrity": "sha512-72a9ghR0gnESIa7jBN53U32FOVCEoztyIlKaNoU05zRhEecduGK9L9c3ww7Mp06JiR+0ls0GBPFJQwwtjn9ksg=="
|
"integrity": "sha512-prBHMK4JYYK+wDjJF1q99KK4JLL+egWS4nmNqdlMUgCExMZ+iZW0hGhyC3VEbsPjvaN0TBhW//VIFwBrk8sEiw=="
|
||||||
},
|
},
|
||||||
"@babel/core": {
|
"@babel/core": {
|
||||||
"version": "7.19.1",
|
"version": "7.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.3.tgz",
|
||||||
"integrity": "sha512-1H8VgqXme4UXCRv7/Wa1bq7RVymKOzC7znjyFM8KiEzwFqcKUKYNoQef4GhdklgNvoBXyW4gYhuBNCM5o1zImw==",
|
"integrity": "sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@ampproject/remapping": "^2.1.0",
|
"@ampproject/remapping": "^2.1.0",
|
||||||
"@babel/code-frame": "^7.18.6",
|
"@babel/code-frame": "^7.18.6",
|
||||||
"@babel/generator": "^7.19.0",
|
"@babel/generator": "^7.19.3",
|
||||||
"@babel/helper-compilation-targets": "^7.19.1",
|
"@babel/helper-compilation-targets": "^7.19.3",
|
||||||
"@babel/helper-module-transforms": "^7.19.0",
|
"@babel/helper-module-transforms": "^7.19.0",
|
||||||
"@babel/helpers": "^7.19.0",
|
"@babel/helpers": "^7.19.0",
|
||||||
"@babel/parser": "^7.19.1",
|
"@babel/parser": "^7.19.3",
|
||||||
"@babel/template": "^7.18.10",
|
"@babel/template": "^7.18.10",
|
||||||
"@babel/traverse": "^7.19.1",
|
"@babel/traverse": "^7.19.3",
|
||||||
"@babel/types": "^7.19.0",
|
"@babel/types": "^7.19.3",
|
||||||
"convert-source-map": "^1.7.0",
|
"convert-source-map": "^1.7.0",
|
||||||
"debug": "^4.1.0",
|
"debug": "^4.1.0",
|
||||||
"gensync": "^1.0.0-beta.2",
|
"gensync": "^1.0.0-beta.2",
|
||||||
@@ -13489,11 +13503,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/generator": {
|
"@babel/generator": {
|
||||||
"version": "7.19.0",
|
"version": "7.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.19.3.tgz",
|
||||||
"integrity": "sha512-S1ahxf1gZ2dpoiFgA+ohK9DIpz50bJ0CWs7Zlzb54Z4sG8qmdIrGrVqmy1sAtTVRb+9CU6U8VqT9L0Zj7hxHVg==",
|
"integrity": "sha512-fqVZnmp1ncvZU757UzDheKZpfPgatqY59XtW2/j/18H7u76akb8xqvjw82f+i2UKd/ksYsSick/BCLQUUtJ/qQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/types": "^7.19.0",
|
"@babel/types": "^7.19.3",
|
||||||
"@jridgewell/gen-mapping": "^0.3.2",
|
"@jridgewell/gen-mapping": "^0.3.2",
|
||||||
"jsesc": "^2.5.1"
|
"jsesc": "^2.5.1"
|
||||||
},
|
},
|
||||||
@@ -13528,11 +13542,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/helper-compilation-targets": {
|
"@babel/helper-compilation-targets": {
|
||||||
"version": "7.19.1",
|
"version": "7.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.3.tgz",
|
||||||
"integrity": "sha512-LlLkkqhCMyz2lkQPvJNdIYU7O5YjWRgC2R4omjCTpZd8u8KMQzZvX4qce+/BluN1rcQiV7BoGUpmQ0LeHerbhg==",
|
"integrity": "sha512-65ESqLGyGmLvgR0mst5AdW1FkNlj9rQsCKduzEoEPhBCDFGXvz2jW6bXFG6i0/MrV2s7hhXjjb2yAzcPuQlLwg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/compat-data": "^7.19.1",
|
"@babel/compat-data": "^7.19.3",
|
||||||
"@babel/helper-validator-option": "^7.18.6",
|
"@babel/helper-validator-option": "^7.18.6",
|
||||||
"browserslist": "^4.21.3",
|
"browserslist": "^4.21.3",
|
||||||
"semver": "^6.3.0"
|
"semver": "^6.3.0"
|
||||||
@@ -13742,9 +13756,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/parser": {
|
"@babel/parser": {
|
||||||
"version": "7.19.1",
|
"version": "7.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.19.3.tgz",
|
||||||
"integrity": "sha512-h7RCSorm1DdTVGJf3P2Mhj3kdnkmF/EiysUkzS2TdgAYqyjFdMQJbVuXOBej2SBJaXan/lIVtT6KkGbyyq753A=="
|
"integrity": "sha512-pJ9xOlNWHiy9+FuFP09DEAFbAn4JskgRsVcc169w2xRBC3FRGuQEwjeIMMND9L2zc0iEhO/tGv4Zq+km+hxNpQ=="
|
||||||
},
|
},
|
||||||
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
|
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
|
||||||
"version": "7.18.6",
|
"version": "7.18.6",
|
||||||
@@ -14327,12 +14341,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/preset-env": {
|
"@babel/preset-env": {
|
||||||
"version": "7.19.1",
|
"version": "7.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.19.3.tgz",
|
||||||
"integrity": "sha512-c8B2c6D16Lp+Nt6HcD+nHl0VbPKVnNPTpszahuxJJnurfMtKeZ80A+qUv48Y7wqvS+dTFuLuaM9oYxyNHbCLWA==",
|
"integrity": "sha512-ziye1OTc9dGFOAXSWKUqQblYHNlBOaDl8wzqf2iKXJAltYiR3hKHUKmkt+S9PppW7RQpq4fFCrwwpIDj/f5P4w==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/compat-data": "^7.19.1",
|
"@babel/compat-data": "^7.19.3",
|
||||||
"@babel/helper-compilation-targets": "^7.19.1",
|
"@babel/helper-compilation-targets": "^7.19.3",
|
||||||
"@babel/helper-plugin-utils": "^7.19.0",
|
"@babel/helper-plugin-utils": "^7.19.0",
|
||||||
"@babel/helper-validator-option": "^7.18.6",
|
"@babel/helper-validator-option": "^7.18.6",
|
||||||
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6",
|
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6",
|
||||||
@@ -14400,7 +14414,7 @@
|
|||||||
"@babel/plugin-transform-unicode-escapes": "^7.18.10",
|
"@babel/plugin-transform-unicode-escapes": "^7.18.10",
|
||||||
"@babel/plugin-transform-unicode-regex": "^7.18.6",
|
"@babel/plugin-transform-unicode-regex": "^7.18.6",
|
||||||
"@babel/preset-modules": "^0.1.5",
|
"@babel/preset-modules": "^0.1.5",
|
||||||
"@babel/types": "^7.19.0",
|
"@babel/types": "^7.19.3",
|
||||||
"babel-plugin-polyfill-corejs2": "^0.3.3",
|
"babel-plugin-polyfill-corejs2": "^0.3.3",
|
||||||
"babel-plugin-polyfill-corejs3": "^0.6.0",
|
"babel-plugin-polyfill-corejs3": "^0.6.0",
|
||||||
"babel-plugin-polyfill-regenerator": "^0.4.1",
|
"babel-plugin-polyfill-regenerator": "^0.4.1",
|
||||||
@@ -14451,29 +14465,29 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/traverse": {
|
"@babel/traverse": {
|
||||||
"version": "7.19.1",
|
"version": "7.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.19.3.tgz",
|
||||||
"integrity": "sha512-0j/ZfZMxKukDaag2PtOPDbwuELqIar6lLskVPPJDjXMXjfLb1Obo/1yjxIGqqAJrmfaTIY3z2wFLAQ7qSkLsuA==",
|
"integrity": "sha512-qh5yf6149zhq2sgIXmwjnsvmnNQC2iw70UFjp4olxucKrWd/dvlUsBI88VSLUsnMNF7/vnOiA+nk1+yLoCqROQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/code-frame": "^7.18.6",
|
"@babel/code-frame": "^7.18.6",
|
||||||
"@babel/generator": "^7.19.0",
|
"@babel/generator": "^7.19.3",
|
||||||
"@babel/helper-environment-visitor": "^7.18.9",
|
"@babel/helper-environment-visitor": "^7.18.9",
|
||||||
"@babel/helper-function-name": "^7.19.0",
|
"@babel/helper-function-name": "^7.19.0",
|
||||||
"@babel/helper-hoist-variables": "^7.18.6",
|
"@babel/helper-hoist-variables": "^7.18.6",
|
||||||
"@babel/helper-split-export-declaration": "^7.18.6",
|
"@babel/helper-split-export-declaration": "^7.18.6",
|
||||||
"@babel/parser": "^7.19.1",
|
"@babel/parser": "^7.19.3",
|
||||||
"@babel/types": "^7.19.0",
|
"@babel/types": "^7.19.3",
|
||||||
"debug": "^4.1.0",
|
"debug": "^4.1.0",
|
||||||
"globals": "^11.1.0"
|
"globals": "^11.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/types": {
|
"@babel/types": {
|
||||||
"version": "7.19.0",
|
"version": "7.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.3.tgz",
|
||||||
"integrity": "sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA==",
|
"integrity": "sha512-hGCaQzIY22DJlDh9CH7NOxgKkFjBk0Cw9xDO1Xmh2151ti7wiGfQ3LauXzL4HP1fmFlTX6XjpRETTpUcv7wQLw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/helper-string-parser": "^7.18.10",
|
"@babel/helper-string-parser": "^7.18.10",
|
||||||
"@babel/helper-validator-identifier": "^7.18.6",
|
"@babel/helper-validator-identifier": "^7.19.1",
|
||||||
"to-fast-properties": "^2.0.0"
|
"to-fast-properties": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -14653,9 +14667,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@humanwhocodes/config-array": {
|
"@humanwhocodes/config-array": {
|
||||||
"version": "0.10.4",
|
"version": "0.10.5",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.4.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.5.tgz",
|
||||||
"integrity": "sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw==",
|
"integrity": "sha512-XVVDtp+dVvRxMoxSiSfasYaG02VEe1qH5cKgMQJWhol6HwzbcqoCMJi8dAGoYAO57jhUyhI6cWuRiTcRaDaYug==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@humanwhocodes/object-schema": "^1.2.1",
|
"@humanwhocodes/object-schema": "^1.2.1",
|
||||||
"debug": "^4.1.1",
|
"debug": "^4.1.1",
|
||||||
@@ -14925,9 +14939,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "18.7.18",
|
"version": "18.7.23",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.18.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.23.tgz",
|
||||||
"integrity": "sha512-m+6nTEOadJZuTPkKR/SYK3A2d7FZrgElol9UP1Kae90VVU4a6mxnPuLiIW1m4Cq4gZ/nWb9GrdVXJCoCazDAbg=="
|
"integrity": "sha512-DWNcCHolDq0ZKGizjx2DZjR/PqsYwAcYUJmfMWqtVU2MBMG5Mo+xFZrhGId5r/O5HOuMPyQEcM6KUBp5lBZZBg=="
|
||||||
},
|
},
|
||||||
"@types/parse-json": {
|
"@types/parse-json": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
@@ -14945,36 +14959,36 @@
|
|||||||
"integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q=="
|
"integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q=="
|
||||||
},
|
},
|
||||||
"@vue/compiler-core": {
|
"@vue/compiler-core": {
|
||||||
"version": "3.2.39",
|
"version": "3.2.40",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.39.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.40.tgz",
|
||||||
"integrity": "sha512-mf/36OWXqWn0wsC40nwRRGheR/qoID+lZXbIuLnr4/AngM0ov8Xvv8GHunC0rKRIkh60bTqydlqTeBo49rlbqw==",
|
"integrity": "sha512-2Dc3Stk0J/VyQ4OUr2yEC53kU28614lZS+bnrCbFSAIftBJ40g/2yQzf4mPBiFuqguMB7hyHaujdgZAQ67kZYA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/parser": "^7.16.4",
|
"@babel/parser": "^7.16.4",
|
||||||
"@vue/shared": "3.2.39",
|
"@vue/shared": "3.2.40",
|
||||||
"estree-walker": "^2.0.2",
|
"estree-walker": "^2.0.2",
|
||||||
"source-map": "^0.6.1"
|
"source-map": "^0.6.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@vue/compiler-dom": {
|
"@vue/compiler-dom": {
|
||||||
"version": "3.2.39",
|
"version": "3.2.40",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.39.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.40.tgz",
|
||||||
"integrity": "sha512-HMFI25Be1C8vLEEv1hgEO1dWwG9QQ8LTTPmCkblVJY/O3OvWx6r1+zsox5mKPMGvqYEZa6l8j+xgOfUspgo7hw==",
|
"integrity": "sha512-OZCNyYVC2LQJy4H7h0o28rtk+4v+HMQygRTpmibGoG9wZyomQiS5otU7qo3Wlq5UfHDw2RFwxb9BJgKjVpjrQw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@vue/compiler-core": "3.2.39",
|
"@vue/compiler-core": "3.2.40",
|
||||||
"@vue/shared": "3.2.39"
|
"@vue/shared": "3.2.40"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@vue/compiler-sfc": {
|
"@vue/compiler-sfc": {
|
||||||
"version": "3.2.39",
|
"version": "3.2.40",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.39.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.40.tgz",
|
||||||
"integrity": "sha512-fqAQgFs1/BxTUZkd0Vakn3teKUt//J3c420BgnYgEOoVdTwYpBTSXCMJ88GOBCylmUBbtquGPli9tVs7LzsWIA==",
|
"integrity": "sha512-tzqwniIN1fu1PDHC3CpqY/dPCfN/RN1thpBC+g69kJcrl7mbGiHKNwbA6kJ3XKKy8R6JLKqcpVugqN4HkeBFFg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/parser": "^7.16.4",
|
"@babel/parser": "^7.16.4",
|
||||||
"@vue/compiler-core": "3.2.39",
|
"@vue/compiler-core": "3.2.40",
|
||||||
"@vue/compiler-dom": "3.2.39",
|
"@vue/compiler-dom": "3.2.40",
|
||||||
"@vue/compiler-ssr": "3.2.39",
|
"@vue/compiler-ssr": "3.2.40",
|
||||||
"@vue/reactivity-transform": "3.2.39",
|
"@vue/reactivity-transform": "3.2.40",
|
||||||
"@vue/shared": "3.2.39",
|
"@vue/shared": "3.2.40",
|
||||||
"estree-walker": "^2.0.2",
|
"estree-walker": "^2.0.2",
|
||||||
"magic-string": "^0.25.7",
|
"magic-string": "^0.25.7",
|
||||||
"postcss": "^8.1.10",
|
"postcss": "^8.1.10",
|
||||||
@@ -14982,12 +14996,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@vue/compiler-ssr": {
|
"@vue/compiler-ssr": {
|
||||||
"version": "3.2.39",
|
"version": "3.2.40",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.39.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.40.tgz",
|
||||||
"integrity": "sha512-EoGCJ6lincKOZGW+0Ky4WOKsSmqL7hp1ZYgen8M7u/mlvvEQUaO9tKKOy7K43M9U2aA3tPv0TuYYQFrEbK2eFQ==",
|
"integrity": "sha512-80cQcgasKjrPPuKcxwuCx7feq+wC6oFl5YaKSee9pV3DNq+6fmCVwEEC3vvkf/E2aI76rIJSOYHsWSEIxK74oQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@vue/compiler-dom": "3.2.39",
|
"@vue/compiler-dom": "3.2.40",
|
||||||
"@vue/shared": "3.2.39"
|
"@vue/shared": "3.2.40"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@vue/component-compiler-utils": {
|
"@vue/component-compiler-utils": {
|
||||||
@@ -15023,26 +15037,26 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@vue/reactivity-transform": {
|
"@vue/reactivity-transform": {
|
||||||
"version": "3.2.39",
|
"version": "3.2.40",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.39.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.40.tgz",
|
||||||
"integrity": "sha512-HGuWu864zStiWs9wBC6JYOP1E00UjMdDWIG5W+FpUx28hV3uz9ODOKVNm/vdOy/Pvzg8+OcANxAVC85WFBbl3A==",
|
"integrity": "sha512-HQUCVwEaacq6fGEsg2NUuGKIhUveMCjOk8jGHqLXPI2w6zFoPrlQhwWEaINTv5kkZDXKEnCijAp+4gNEHG03yw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/parser": "^7.16.4",
|
"@babel/parser": "^7.16.4",
|
||||||
"@vue/compiler-core": "3.2.39",
|
"@vue/compiler-core": "3.2.40",
|
||||||
"@vue/shared": "3.2.39",
|
"@vue/shared": "3.2.40",
|
||||||
"estree-walker": "^2.0.2",
|
"estree-walker": "^2.0.2",
|
||||||
"magic-string": "^0.25.7"
|
"magic-string": "^0.25.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@vue/shared": {
|
"@vue/shared": {
|
||||||
"version": "3.2.39",
|
"version": "3.2.40",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.39.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.40.tgz",
|
||||||
"integrity": "sha512-D3dl2ZB9qE6mTuWPk9RlhDeP1dgNRUKC3NJxji74A4yL8M2MwlhLKUC/49WHjrNzSPug58fWx/yFbaTzGAQSBw=="
|
"integrity": "sha512-0PLQ6RUtZM0vO3teRfzGi4ltLUO5aO+kLgwh4Um3THSR03rpQWLTuRCkuO5A41ITzwdWeKdPHtSARuPkoo5pCQ=="
|
||||||
},
|
},
|
||||||
"@vvo/tzdb": {
|
"@vvo/tzdb": {
|
||||||
"version": "6.64.0",
|
"version": "6.68.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vvo/tzdb/-/tzdb-6.64.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vvo/tzdb/-/tzdb-6.68.0.tgz",
|
||||||
"integrity": "sha512-EdX+RDaodSS05iq+rK+zAYzpNQX/ZKzRpBSiKk0m5ZcLdOfjvZ1kIyCJSVN7weL2tLkCFZ1q0tdljJTTyPpkQA=="
|
"integrity": "sha512-gTYX0c/zfvdeywLFZdHJzxczXjZf4oZHRnkTemziyn4p0R+qoqdrRK5PqY2DFnH64YkFcpreNS1JbbnfWiMQgQ=="
|
||||||
},
|
},
|
||||||
"@webassemblyjs/ast": {
|
"@webassemblyjs/ast": {
|
||||||
"version": "1.11.1",
|
"version": "1.11.1",
|
||||||
@@ -15822,9 +15836,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"caniuse-lite": {
|
"caniuse-lite": {
|
||||||
"version": "1.0.30001409",
|
"version": "1.0.30001412",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001409.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001412.tgz",
|
||||||
"integrity": "sha512-V0mnJ5dwarmhYv8/MzhJ//aW68UpvnQBXv8lJ2QUsvn2pHcmAuNtu8hQEDz37XnA1iE+lRR9CIfGWWpgJ5QedQ=="
|
"integrity": "sha512-+TeEIee1gS5bYOiuf+PS/kp2mrXic37Hl66VY6EAfxasIk5fELTktK2oOezYed12H8w7jt3s512PpulQidPjwA=="
|
||||||
},
|
},
|
||||||
"chai": {
|
"chai": {
|
||||||
"version": "4.3.6",
|
"version": "4.3.6",
|
||||||
@@ -16157,14 +16171,14 @@
|
|||||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
|
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
|
||||||
},
|
},
|
||||||
"core-js": {
|
"core-js": {
|
||||||
"version": "3.25.2",
|
"version": "3.25.3",
|
||||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.25.2.tgz",
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.25.3.tgz",
|
||||||
"integrity": "sha512-YB4IAT1bjEfxTJ1XYy11hJAKskO+qmhuDBM8/guIfMz4JvdsAQAqvyb97zXX7JgSrfPLG5mRGFWJwJD39ruq2A=="
|
"integrity": "sha512-y1hvKXmPHvm5B7w4ln1S4uc9eV/O5+iFExSRUimnvIph11uaizFR8LFMdONN8hG3P2pipUfX4Y/fR8rAEtcHcQ=="
|
||||||
},
|
},
|
||||||
"core-js-compat": {
|
"core-js-compat": {
|
||||||
"version": "3.25.2",
|
"version": "3.25.3",
|
||||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.25.2.tgz",
|
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.25.3.tgz",
|
||||||
"integrity": "sha512-TxfyECD4smdn3/CjWxczVtJqVLEEC2up7/82t7vC0AzNogr+4nQ8vyF7abxAuTXWvjTClSbvGhU0RgqA4ToQaQ==",
|
"integrity": "sha512-xVtYpJQ5grszDHEUU9O7XbjjcZ0ccX3LgQsyqSvTnjX97ZqEgn9F5srmrwwwMtbKzDllyFPL+O+2OFMl1lU4TQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"browserslist": "^4.21.4"
|
"browserslist": "^4.21.4"
|
||||||
}
|
}
|
||||||
@@ -16451,9 +16465,9 @@
|
|||||||
"integrity": "sha512-APql/TZ6FdLEpf2z7/X2a2zyqK8juYtqaSVqxw9mYoQ64CXkfU15AeLh8pUszT8+fnYjgm6t0aIYpWKJbnLkuA=="
|
"integrity": "sha512-APql/TZ6FdLEpf2z7/X2a2zyqK8juYtqaSVqxw9mYoQ64CXkfU15AeLh8pUszT8+fnYjgm6t0aIYpWKJbnLkuA=="
|
||||||
},
|
},
|
||||||
"date-format": {
|
"date-format": {
|
||||||
"version": "4.0.13",
|
"version": "4.0.14",
|
||||||
"resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz",
|
||||||
"integrity": "sha512-bnYCwf8Emc3pTD8pXnre+wfnjGtfi5ncMDKy7+cWZXbmRAsdWkOQHrfC1yz/KiwP5thDp2kCHWYWKBX4HP1hoQ=="
|
"integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg=="
|
||||||
},
|
},
|
||||||
"de-indent": {
|
"de-indent": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
@@ -16648,9 +16662,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"electron-to-chromium": {
|
"electron-to-chromium": {
|
||||||
"version": "1.4.256",
|
"version": "1.4.265",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.256.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.265.tgz",
|
||||||
"integrity": "sha512-x+JnqyluoJv8I0U9gVe+Sk2st8vF0CzMt78SXxuoWCooLLY2k5VerIBdpvG7ql6GKI4dzNnPjmqgDJ76EdaAKw=="
|
"integrity": "sha512-38KaYBNs0oCzWCpr6j7fY/W9vF0vSp4tKFIshQTgdZMhUpkxgotkQgjJP6iGMdmlsgMs3i0/Hkko4UXLTrkYVQ=="
|
||||||
},
|
},
|
||||||
"emoji-regex": {
|
"emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
@@ -16773,21 +16787,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"es-abstract": {
|
"es-abstract": {
|
||||||
"version": "1.20.2",
|
"version": "1.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.3.tgz",
|
||||||
"integrity": "sha512-XxXQuVNrySBNlEkTYJoDNFe5+s2yIOpzq80sUHEdPdQr0S5nTLz4ZPPPswNIpKseDDUS5yghX1gfLIHQZ1iNuQ==",
|
"integrity": "sha512-AyrnaKVpMzljIdwjzrj+LxGmj8ik2LckwXacHqrJJ/jxz6dDDBcZ7I7nlHM0FvEW8MfbWJwOd+yT2XzYW49Frw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"call-bind": "^1.0.2",
|
"call-bind": "^1.0.2",
|
||||||
"es-to-primitive": "^1.2.1",
|
"es-to-primitive": "^1.2.1",
|
||||||
"function-bind": "^1.1.1",
|
"function-bind": "^1.1.1",
|
||||||
"function.prototype.name": "^1.1.5",
|
"function.prototype.name": "^1.1.5",
|
||||||
"get-intrinsic": "^1.1.2",
|
"get-intrinsic": "^1.1.3",
|
||||||
"get-symbol-description": "^1.0.0",
|
"get-symbol-description": "^1.0.0",
|
||||||
"has": "^1.0.3",
|
"has": "^1.0.3",
|
||||||
"has-property-descriptors": "^1.0.0",
|
"has-property-descriptors": "^1.0.0",
|
||||||
"has-symbols": "^1.0.3",
|
"has-symbols": "^1.0.3",
|
||||||
"internal-slot": "^1.0.3",
|
"internal-slot": "^1.0.3",
|
||||||
"is-callable": "^1.2.4",
|
"is-callable": "^1.2.6",
|
||||||
"is-negative-zero": "^2.0.2",
|
"is-negative-zero": "^2.0.2",
|
||||||
"is-regex": "^1.1.4",
|
"is-regex": "^1.1.4",
|
||||||
"is-shared-array-buffer": "^1.0.2",
|
"is-shared-array-buffer": "^1.0.2",
|
||||||
@@ -16797,6 +16811,7 @@
|
|||||||
"object-keys": "^1.1.1",
|
"object-keys": "^1.1.1",
|
||||||
"object.assign": "^4.1.4",
|
"object.assign": "^4.1.4",
|
||||||
"regexp.prototype.flags": "^1.4.3",
|
"regexp.prototype.flags": "^1.4.3",
|
||||||
|
"safe-regex-test": "^1.0.0",
|
||||||
"string.prototype.trimend": "^1.0.5",
|
"string.prototype.trimend": "^1.0.5",
|
||||||
"string.prototype.trimstart": "^1.0.5",
|
"string.prototype.trimstart": "^1.0.5",
|
||||||
"unbox-primitive": "^1.0.2"
|
"unbox-primitive": "^1.0.2"
|
||||||
@@ -17228,9 +17243,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"eslint-plugin-n": {
|
"eslint-plugin-n": {
|
||||||
"version": "15.2.5",
|
"version": "15.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.3.0.tgz",
|
||||||
"integrity": "sha512-8+BYsqiyZfpu6NXmdLOXVUfk8IocpCjpd8nMRRH0A9ulrcemhb2VI9RSJMEy5udx++A/YcVPD11zT8hpFq368g==",
|
"integrity": "sha512-IyzPnEWHypCWasDpxeJnim60jhlumbmq0pubL6IOcnk8u2y53s5QfT8JnXy7skjHJ44yWHRb11PLtDHuu1kg/Q==",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"builtins": "^5.0.1",
|
"builtins": "^5.0.1",
|
||||||
@@ -18549,9 +18564,9 @@
|
|||||||
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ=="
|
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ=="
|
||||||
},
|
},
|
||||||
"is-callable": {
|
"is-callable": {
|
||||||
"version": "1.2.6",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
|
||||||
"integrity": "sha512-krO72EO2NptOGAX2KYyqbP9vYMlNAXdB53rq6f8LXY6RY7JdSR/3BD6wLUlPHSAesmY9vstNrjvqGaCiRK/91Q=="
|
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="
|
||||||
},
|
},
|
||||||
"is-core-module": {
|
"is-core-module": {
|
||||||
"version": "2.10.0",
|
"version": "2.10.0",
|
||||||
@@ -21126,15 +21141,25 @@
|
|||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||||
},
|
},
|
||||||
|
"safe-regex-test": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==",
|
||||||
|
"requires": {
|
||||||
|
"call-bind": "^1.0.2",
|
||||||
|
"get-intrinsic": "^1.1.3",
|
||||||
|
"is-regex": "^1.1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"safer-buffer": {
|
"safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||||
},
|
},
|
||||||
"sass": {
|
"sass": {
|
||||||
"version": "1.54.9",
|
"version": "1.55.0",
|
||||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.54.9.tgz",
|
"resolved": "https://registry.npmjs.org/sass/-/sass-1.55.0.tgz",
|
||||||
"integrity": "sha512-xb1hjASzEH+0L0WI9oFjqhRi51t/gagWnxLiwUNMltA0Ab6jIDkAacgKiGYKM9Jhy109osM7woEEai6SXeJo5Q==",
|
"integrity": "sha512-Pk+PMy7OGLs9WaxZGJMn7S96dvlyVBwwtToX895WmCpAOr5YiJYEUJfiJidMuKb613z2xNWcXCHEuOvjZbqC6A==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"chokidar": ">=3.0.0 <4.0.0",
|
"chokidar": ">=3.0.0 <4.0.0",
|
||||||
"immutable": "^4.0.0",
|
"immutable": "^4.0.0",
|
||||||
@@ -21594,11 +21619,11 @@
|
|||||||
"integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="
|
"integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="
|
||||||
},
|
},
|
||||||
"streamroller": {
|
"streamroller": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.3.tgz",
|
||||||
"integrity": "sha512-wZswqzbgGGsXYIrBYhOE0yP+nQ6XRk7xDcYwuQAGTYXdyAUmvgVFE0YU1g5pvQT0m7GBaQfYcSnlHbapuK0H0A==",
|
"integrity": "sha512-CphIJyFx2SALGHeINanjFRKQ4l7x2c+rXYJ4BMq0gd+ZK0gi4VT8b+eHe2wi58x4UayBAKx4xtHpXT/ea1cz8w==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"date-format": "^4.0.13",
|
"date-format": "^4.0.14",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"fs-extra": "^8.1.0"
|
"fs-extra": "^8.1.0"
|
||||||
}
|
}
|
||||||
@@ -22004,9 +22029,9 @@
|
|||||||
"integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ=="
|
"integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ=="
|
||||||
},
|
},
|
||||||
"uglify-js": {
|
"uglify-js": {
|
||||||
"version": "3.17.1",
|
"version": "3.17.2",
|
||||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.2.tgz",
|
||||||
"integrity": "sha512-+juFBsLLw7AqMaqJ0GFvlsGZwdQfI2ooKQB39PSBgMnMakcFosi9O8jCwE+2/2nMNcc0z63r9mwjoDG8zr+q0Q==",
|
"integrity": "sha512-bbxglRjsGQMchfvXZNusUcYgiB9Hx2K4AHYXQy2DITZ9Rd+JzhX7+hoocE5Winr7z2oHvPsekkBwXtigvxevXg==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"uid-safe": {
|
"uid-safe": {
|
||||||
|
|||||||
@@ -54,39 +54,40 @@ import "common/maptiler-lang";
|
|||||||
import { T, Mount } from "common/vm";
|
import { T, Mount } from "common/vm";
|
||||||
import * as offline from "@lcdp/offline-plugin/runtime";
|
import * as offline from "@lcdp/offline-plugin/runtime";
|
||||||
|
|
||||||
// Initialize helpers
|
config.load().finally(() => {
|
||||||
const viewer = new Viewer();
|
// Initialize helpers.
|
||||||
const isPublic = config.get("public");
|
const viewer = new Viewer();
|
||||||
const isMobile =
|
const isPublic = config.get("public");
|
||||||
|
const isMobile =
|
||||||
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
|
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
|
||||||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2);
|
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2);
|
||||||
|
|
||||||
// Initialize language and detect alignment
|
// Initialize language and detect alignment.
|
||||||
Vue.config.language = config.values.settings.ui.language;
|
Vue.config.language = config.getLanguage();
|
||||||
Settings.defaultLocale = Vue.config.language.substring(0, 2);
|
Settings.defaultLocale = Vue.config.language.substring(0, 2);
|
||||||
// Detect right-to-left languages such as Arabic and Hebrew
|
// Detect right-to-left languages such as Arabic and Hebrew
|
||||||
const rtl = config.rtl();
|
const rtl = config.rtl();
|
||||||
|
|
||||||
// Get initial theme colors from config
|
// Get initial theme colors from config.
|
||||||
const theme = config.theme.colors;
|
const theme = config.theme.colors;
|
||||||
|
|
||||||
// HTTP Live Streaming (video support)
|
// HTTP Live Streaming (video support).
|
||||||
window.Hls = Hls;
|
window.Hls = Hls;
|
||||||
|
|
||||||
// Assign helpers to VueJS prototype
|
// Assign helpers to VueJS prototype.
|
||||||
Vue.prototype.$event = Event;
|
Vue.prototype.$event = Event;
|
||||||
Vue.prototype.$notify = Notify;
|
Vue.prototype.$notify = Notify;
|
||||||
Vue.prototype.$scrollbar = Scrollbar;
|
Vue.prototype.$scrollbar = Scrollbar;
|
||||||
Vue.prototype.$viewer = viewer;
|
Vue.prototype.$viewer = viewer;
|
||||||
Vue.prototype.$session = session;
|
Vue.prototype.$session = session;
|
||||||
Vue.prototype.$api = Api;
|
Vue.prototype.$api = Api;
|
||||||
Vue.prototype.$log = Log;
|
Vue.prototype.$log = Log;
|
||||||
Vue.prototype.$socket = Socket;
|
Vue.prototype.$socket = Socket;
|
||||||
Vue.prototype.$config = config;
|
Vue.prototype.$config = config;
|
||||||
Vue.prototype.$clipboard = Clipboard;
|
Vue.prototype.$clipboard = Clipboard;
|
||||||
Vue.prototype.$isMobile = isMobile;
|
Vue.prototype.$isMobile = isMobile;
|
||||||
Vue.prototype.$rtl = rtl;
|
Vue.prototype.$rtl = rtl;
|
||||||
Vue.prototype.$sponsorFeatures = () => {
|
Vue.prototype.$sponsorFeatures = () => {
|
||||||
return config.load().finally(() => {
|
return config.load().finally(() => {
|
||||||
if (config.values.sponsor) {
|
if (config.values.sponsor) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
@@ -94,29 +95,29 @@ Vue.prototype.$sponsorFeatures = () => {
|
|||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Register Vuetify
|
// Register Vuetify.
|
||||||
Vue.use(Vuetify, { rtl, icons, theme });
|
Vue.use(Vuetify, { rtl, icons, theme });
|
||||||
|
|
||||||
// Register other VueJS plugins
|
// Register other VueJS plugins.
|
||||||
Vue.use(GetTextPlugin, {
|
Vue.use(GetTextPlugin, {
|
||||||
translations: config.translations,
|
translations: config.translations,
|
||||||
silent: true, // !config.values.debug,
|
silent: true, // !config.values.debug,
|
||||||
defaultLanguage: Vue.config.language,
|
defaultLanguage: Vue.config.language,
|
||||||
autoAddKeyAttributes: true,
|
autoAddKeyAttributes: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
Vue.use(VueLuxon);
|
Vue.use(VueLuxon);
|
||||||
Vue.use(VueInfiniteScroll);
|
Vue.use(VueInfiniteScroll);
|
||||||
Vue.use(VueFullscreen);
|
Vue.use(VueFullscreen);
|
||||||
Vue.use(VueFilters);
|
Vue.use(VueFilters);
|
||||||
Vue.use(Components);
|
Vue.use(Components);
|
||||||
Vue.use(Dialogs);
|
Vue.use(Dialogs);
|
||||||
Vue.use(Router);
|
Vue.use(Router);
|
||||||
|
|
||||||
// Configure client-side routing
|
// Configure client-side routing.
|
||||||
const router = new Router({
|
const router = new Router({
|
||||||
routes: Routes,
|
routes: Routes,
|
||||||
mode: "history",
|
mode: "history",
|
||||||
base: config.baseUri + "/",
|
base: config.baseUri + "/",
|
||||||
@@ -134,13 +135,16 @@ const router = new Router({
|
|||||||
return { x: 0, y: 0 };
|
return { x: 0, y: 0 };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
if (document.querySelector(".v-dialog--active.v-dialog--fullscreen")) {
|
if (document.querySelector(".v-dialog--active.v-dialog--fullscreen")) {
|
||||||
// Disable back button in full-screen viewers and editors.
|
// Disable back button in full-screen viewers and editors.
|
||||||
next(false);
|
next(false);
|
||||||
} else if (to.matched.some((record) => record.meta.settings) && config.values.disable.settings) {
|
} else if (
|
||||||
|
to.matched.some((record) => record.meta.settings) &&
|
||||||
|
config.values.disable.settings
|
||||||
|
) {
|
||||||
next({ name: "home" });
|
next({ name: "home" });
|
||||||
} else if (to.matched.some((record) => record.meta.admin)) {
|
} else if (to.matched.some((record) => record.meta.admin)) {
|
||||||
if (isPublic || session.isAdmin()) {
|
if (isPublic || session.isAdmin()) {
|
||||||
@@ -163,9 +167,9 @@ router.beforeEach((to, from, next) => {
|
|||||||
} else {
|
} else {
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.afterEach((to) => {
|
router.afterEach((to) => {
|
||||||
const t = to.meta["title"] ? to.meta["title"] : "";
|
const t = to.meta["title"] ? to.meta["title"] : "";
|
||||||
|
|
||||||
if (t !== "" && config.values.siteTitle !== t && config.values.name !== t) {
|
if (t !== "" && config.values.siteTitle !== t && config.values.name !== t) {
|
||||||
@@ -189,18 +193,19 @@ router.afterEach((to) => {
|
|||||||
window.document.title = config.values.siteCaption;
|
window.document.title = config.values.siteCaption;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
document.body.classList.add("mobile");
|
document.body.classList.add("mobile");
|
||||||
} else {
|
} else {
|
||||||
// Pull client config every 10 minutes in case push fails (except on mobile to save battery).
|
// Pull client config every 10 minutes in case push fails (except on mobile to save battery).
|
||||||
setInterval(() => config.update(), 600000);
|
setInterval(() => config.update(), 600000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start application.
|
// Start application.
|
||||||
Mount(Vue, PhotoPrism, router);
|
Mount(Vue, PhotoPrism, router);
|
||||||
|
|
||||||
if (config.baseUri === "") {
|
if (config.baseUri === "") {
|
||||||
offline.install();
|
offline.install();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export default [
|
|||||||
meta: { title: siteTitle, auth: false, hideNav: true },
|
meta: { title: siteTitle, auth: false, hideNav: true },
|
||||||
beforeEnter: (to, from, next) => {
|
beforeEnter: (to, from, next) => {
|
||||||
if (session.isUser()) {
|
if (session.isUser()) {
|
||||||
next({ name: "home" });
|
next({ name: session.getHome() });
|
||||||
} else {
|
} else {
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
@@ -283,7 +283,11 @@ export default [
|
|||||||
config.load().finally(() => {
|
config.load().finally(() => {
|
||||||
// Open new faces tab when there are no people.
|
// Open new faces tab when there are no people.
|
||||||
if (config.values.count.people === 0) {
|
if (config.values.count.people === 0) {
|
||||||
|
if (config.allow("people", "manage")) {
|
||||||
next({ name: "people_faces" });
|
next({ name: "people_faces" });
|
||||||
|
} else {
|
||||||
|
next({ name: "albums" });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
@@ -409,6 +413,6 @@ export default [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "*",
|
path: "*",
|
||||||
redirect: "/browse",
|
redirect: "/albums",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ export default class Config {
|
|||||||
return Api.get("config")
|
return Api.get("config")
|
||||||
.then(
|
.then(
|
||||||
(response) => this.setValues(response.data),
|
(response) => this.setValues(response.data),
|
||||||
() => console.warn("failed pulling updated client config")
|
() => console.warn("config update failed")
|
||||||
)
|
)
|
||||||
.finally(() => Promise.resolve());
|
.finally(() => Promise.resolve());
|
||||||
}
|
}
|
||||||
@@ -138,7 +138,7 @@ export default class Config {
|
|||||||
if (!values) return;
|
if (!values) return;
|
||||||
|
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
console.log("config: new values", values);
|
console.log("config: updated", values);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (values.jsUri && this.values.jsUri !== values.jsUri) {
|
if (values.jsUri && this.values.jsUri !== values.jsUri) {
|
||||||
@@ -146,13 +146,14 @@ export default class Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let key in values) {
|
for (let key in values) {
|
||||||
if (values.hasOwnProperty(key)) {
|
if (values.hasOwnProperty(key) && values[key] != null) {
|
||||||
this.set(key, values[key]);
|
this.set(key, values[key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (values.settings) {
|
if (values.settings) {
|
||||||
this.setBatchSize(values.settings);
|
this.setBatchSize(values.settings);
|
||||||
|
this.setLanguage(values.settings.ui.language);
|
||||||
this.setTheme(values.settings.ui.theme);
|
this.setTheme(values.settings.ui.theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,7 +233,7 @@ export default class Config {
|
|||||||
return result[0];
|
return result[0];
|
||||||
} else {
|
} else {
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
console.warn("more than one person matching the same name", result);
|
console.warn("more than one person having the same name", result);
|
||||||
}
|
}
|
||||||
return result[0];
|
return result[0];
|
||||||
}
|
}
|
||||||
@@ -343,6 +344,82 @@ export default class Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
aclClasses(resource) {
|
||||||
|
let result = [];
|
||||||
|
const perms = ["update", "search", "manage", "share", "delete"];
|
||||||
|
|
||||||
|
perms.forEach((perm) => {
|
||||||
|
if (this.deny(resource, perm)) result.push(`disable-${perm}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
allow(resource, perm) {
|
||||||
|
if (this.values["acl"] && this.values["acl"][resource]) {
|
||||||
|
return !!this.values["acl"][resource][perm] || !!this.values["acl"][resource]["full_access"];
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
deny(resource, perm) {
|
||||||
|
return !this.allow(resource, perm);
|
||||||
|
}
|
||||||
|
|
||||||
|
settings() {
|
||||||
|
return this.values.settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSettings(settings) {
|
||||||
|
if (!settings) return;
|
||||||
|
|
||||||
|
if (this.debug) {
|
||||||
|
console.log("config: new settings", settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.values.settings = settings;
|
||||||
|
|
||||||
|
this.setBatchSize(settings);
|
||||||
|
this.setLanguage(settings.ui.language);
|
||||||
|
this.setTheme(settings.ui.theme);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLanguage(locale) {
|
||||||
|
if (!locale || this.loading()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.values.settings && this.values.settings.ui) {
|
||||||
|
this.values.settings.ui.language = locale;
|
||||||
|
this.storage.setItem(this.storage_key + ".locale", locale);
|
||||||
|
Api.defaults.headers.common["Accept-Language"] = locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLanguage() {
|
||||||
|
let locale = "en";
|
||||||
|
|
||||||
|
if (this.loading()) {
|
||||||
|
const stored = this.storage.getItem(this.storage_key + ".locale");
|
||||||
|
if (stored) {
|
||||||
|
locale = stored;
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
this.values.settings &&
|
||||||
|
this.values.settings.ui &&
|
||||||
|
this.values.settings.ui.language
|
||||||
|
) {
|
||||||
|
locale = this.values.settings.ui.language;
|
||||||
|
}
|
||||||
|
|
||||||
|
return locale;
|
||||||
|
}
|
||||||
|
|
||||||
setTheme(name) {
|
setTheme(name) {
|
||||||
let theme = onSetTheme(name, this);
|
let theme = onSetTheme(name, this);
|
||||||
|
|
||||||
@@ -383,6 +460,14 @@ export default class Config {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
restoreValues() {
|
||||||
|
const json = this.storage.getItem(this.storage_key);
|
||||||
|
if (json !== "undefined") {
|
||||||
|
this.setValues(JSON.parse(json));
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
set(key, value) {
|
set(key, value) {
|
||||||
this.values[key] = value;
|
this.values[key] = value;
|
||||||
return this;
|
return this;
|
||||||
@@ -397,11 +482,7 @@ export default class Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
feature(name) {
|
feature(name) {
|
||||||
return this.values.settings.features[name];
|
return this.values.settings.features[name] === true;
|
||||||
}
|
|
||||||
|
|
||||||
settings() {
|
|
||||||
return this.values.settings;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rtl() {
|
rtl() {
|
||||||
@@ -472,4 +553,8 @@ export default class Config {
|
|||||||
getVersion() {
|
getVersion() {
|
||||||
return this.get("version");
|
return this.get("version");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSiteDescription() {
|
||||||
|
return this.values.siteDescription ? this.values.siteDescription : this.values.siteCaption;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,34 +28,46 @@ import Event from "pubsub-js";
|
|||||||
import User from "model/user";
|
import User from "model/user";
|
||||||
import Socket from "websocket.js";
|
import Socket from "websocket.js";
|
||||||
|
|
||||||
|
const SessionHeader = "X-Session-ID";
|
||||||
|
|
||||||
export default class Session {
|
export default class Session {
|
||||||
/**
|
/**
|
||||||
* @param {Storage} storage
|
* @param {Storage} storage
|
||||||
* @param {Config} config
|
* @param {Config} config
|
||||||
*/
|
*/
|
||||||
constructor(storage, config) {
|
constructor(storage, config) {
|
||||||
|
this.storage_key = "session_storage";
|
||||||
this.auth = false;
|
this.auth = false;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
this.user = new User(false);
|
||||||
|
this.data = null;
|
||||||
|
|
||||||
if (storage.getItem("session_storage") === "true") {
|
// Set session storage.
|
||||||
|
if (storage.getItem(this.storage_key) === "true") {
|
||||||
this.storage = window.sessionStorage;
|
this.storage = window.sessionStorage;
|
||||||
} else {
|
} else {
|
||||||
this.storage = storage;
|
this.storage = storage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restore from session storage.
|
||||||
if (this.applyId(this.storage.getItem("session_id"))) {
|
if (this.applyId(this.storage.getItem("session_id"))) {
|
||||||
const dataJson = this.storage.getItem("data");
|
const dataJson = this.storage.getItem("data");
|
||||||
this.data = dataJson !== "undefined" ? JSON.parse(dataJson) : null;
|
if (dataJson !== "undefined") {
|
||||||
|
this.data = JSON.parse(dataJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.data && this.data.user) {
|
const userJson = this.storage.getItem("user");
|
||||||
this.user = new User(this.data.user);
|
if (userJson !== "undefined") {
|
||||||
|
this.user = new User(JSON.parse(userJson));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Authenticated?
|
||||||
if (this.isUser()) {
|
if (this.isUser()) {
|
||||||
this.auth = true;
|
this.auth = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Subscribe to session events.
|
||||||
Event.subscribe("session.logout", () => {
|
Event.subscribe("session.logout", () => {
|
||||||
return this.onLogout();
|
return this.onLogout();
|
||||||
});
|
});
|
||||||
@@ -64,17 +76,18 @@ export default class Session {
|
|||||||
this.sendClientInfo();
|
this.sendClientInfo();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Say hello.
|
||||||
this.sendClientInfo();
|
this.sendClientInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
useSessionStorage() {
|
useSessionStorage() {
|
||||||
this.deleteId();
|
this.deleteId();
|
||||||
this.storage.setItem("session_storage", "true");
|
this.storage.setItem(this.storage_key, "true");
|
||||||
this.storage = window.sessionStorage;
|
this.storage = window.sessionStorage;
|
||||||
}
|
}
|
||||||
|
|
||||||
useLocalStorage() {
|
useLocalStorage() {
|
||||||
this.storage.setItem("session_storage", "false");
|
this.storage.setItem(this.storage_key, "false");
|
||||||
this.storage = window.localStorage;
|
this.storage = window.localStorage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +98,8 @@ export default class Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.session_id = id;
|
this.session_id = id;
|
||||||
Api.defaults.headers.common["X-Session-ID"] = id;
|
|
||||||
|
Api.defaults.headers.common[SessionHeader] = id;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -110,8 +124,10 @@ export default class Session {
|
|||||||
deleteId() {
|
deleteId() {
|
||||||
this.session_id = null;
|
this.session_id = null;
|
||||||
this.storage.removeItem("session_id");
|
this.storage.removeItem("session_id");
|
||||||
delete Api.defaults.headers.common["X-Session-ID"];
|
|
||||||
this.deleteData();
|
delete Api.defaults.headers.common[SessionHeader];
|
||||||
|
|
||||||
|
this.deleteAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
setData(data) {
|
setData(data) {
|
||||||
@@ -120,13 +136,11 @@ export default class Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.data = data;
|
this.data = data;
|
||||||
this.user = new User(this.data.user);
|
|
||||||
this.storage.setItem("data", JSON.stringify(data));
|
this.storage.setItem("data", JSON.stringify(data));
|
||||||
this.auth = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
getUser() {
|
if (data.user) {
|
||||||
return this.user;
|
this.setUser(data.user);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getEmail() {
|
getEmail() {
|
||||||
@@ -137,39 +151,42 @@ export default class Session {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
getUsername() {
|
|
||||||
if (this.isUser()) {
|
|
||||||
if (this.user.Username) {
|
|
||||||
return this.user.Username;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
getDisplayName() {
|
getDisplayName() {
|
||||||
if (this.isUser()) {
|
if (this.isUser()) {
|
||||||
if (this.user.DisplayName) {
|
return this.user.getEntityName();
|
||||||
return this.user.DisplayName;
|
|
||||||
}
|
|
||||||
if (this.user.ArtistName) {
|
|
||||||
return this.user.ArtistName;
|
|
||||||
}
|
|
||||||
if (this.user.FullName) {
|
|
||||||
return this.user.FullName;
|
|
||||||
}
|
|
||||||
if (this.user.Username) {
|
|
||||||
return this.user.Username;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setUser(user) {
|
||||||
|
if (!user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.user = new User(user);
|
||||||
|
this.storage.setItem("user", JSON.stringify(user));
|
||||||
|
this.auth = this.isUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
getUser() {
|
||||||
|
return this.user;
|
||||||
|
}
|
||||||
|
|
||||||
isUser() {
|
isUser() {
|
||||||
return this.user && this.user.hasId();
|
return this.user && this.user.hasId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getHome() {
|
||||||
|
if (!this.isUser()) {
|
||||||
|
return "login";
|
||||||
|
} else if (this.user.Role === "guest") {
|
||||||
|
return "albums";
|
||||||
|
} else {
|
||||||
|
return "browse";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
isAdmin() {
|
isAdmin() {
|
||||||
return this.user && this.user.hasId() && (this.user.Role === "admin" || this.user.SuperAdmin);
|
return this.user && this.user.hasId() && (this.user.Role === "admin" || this.user.SuperAdmin);
|
||||||
}
|
}
|
||||||
@@ -187,12 +204,21 @@ export default class Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deleteData() {
|
deleteData() {
|
||||||
this.auth = false;
|
|
||||||
this.user = new User();
|
|
||||||
this.data = null;
|
this.data = null;
|
||||||
this.storage.removeItem("data");
|
this.storage.removeItem("data");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteUser() {
|
||||||
|
this.auth = false;
|
||||||
|
this.user = new User(false);
|
||||||
|
this.storage.removeItem("user");
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteAll() {
|
||||||
|
this.deleteData();
|
||||||
|
this.deleteUser();
|
||||||
|
}
|
||||||
|
|
||||||
sendClientInfo() {
|
sendClientInfo() {
|
||||||
const hasConfig = !!window.__CONFIG__;
|
const hasConfig = !!window.__CONFIG__;
|
||||||
const clientInfo = {
|
const clientInfo = {
|
||||||
@@ -211,14 +237,19 @@ export default class Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
login(username, password, token) {
|
login(name, password, token) {
|
||||||
this.deleteId();
|
this.deleteId();
|
||||||
|
|
||||||
return Api.post("session", { username, password, token }).then((resp) => {
|
return Api.post("session", { name, password, token }).then((resp) => {
|
||||||
|
const reload = this.config.getLanguage() !== resp.data?.config?.settings?.ui?.language;
|
||||||
|
|
||||||
this.setConfig(resp.data.config);
|
this.setConfig(resp.data.config);
|
||||||
this.setId(resp.data.id);
|
this.setId(resp.data.id);
|
||||||
|
this.setUser(resp.data.user);
|
||||||
this.setData(resp.data.data);
|
this.setData(resp.data.data);
|
||||||
this.sendClientInfo();
|
this.sendClientInfo();
|
||||||
|
|
||||||
|
return Promise.resolve(reload);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,6 +257,7 @@ export default class Session {
|
|||||||
return Api.post("session", { token }).then((resp) => {
|
return Api.post("session", { token }).then((resp) => {
|
||||||
this.setConfig(resp.data.config);
|
this.setConfig(resp.data.config);
|
||||||
this.setId(resp.data.id);
|
this.setId(resp.data.id);
|
||||||
|
this.setUser(resp.data.user);
|
||||||
this.setData(resp.data.data);
|
this.setData(resp.data.data);
|
||||||
this.sendClientInfo();
|
this.sendClientInfo();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -175,6 +175,8 @@ export default class Util {
|
|||||||
case "avc":
|
case "avc":
|
||||||
case "avc1":
|
case "avc1":
|
||||||
return "Advanced Video Coding (AVC) / H.264";
|
return "Advanced Video Coding (AVC) / H.264";
|
||||||
|
case "avif":
|
||||||
|
return "AV1 Image File Format (AVIF)";
|
||||||
case "hevc":
|
case "hevc":
|
||||||
case "hvc":
|
case "hvc":
|
||||||
case "hvc1":
|
case "hvc1":
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="features.share"
|
v-if="canShare"
|
||||||
fab dark small
|
fab dark small
|
||||||
:title="$gettext('Share')"
|
:title="$gettext('Share')"
|
||||||
color="share"
|
color="share"
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
<v-icon>share</v-icon>
|
<v-icon>share</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
|
v-if="canManage"
|
||||||
fab dark small
|
fab dark small
|
||||||
:title="$gettext('Edit')"
|
:title="$gettext('Edit')"
|
||||||
color="edit"
|
color="edit"
|
||||||
@@ -43,18 +44,17 @@
|
|||||||
<v-icon>edit</v-icon>
|
<v-icon>edit</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="features.download"
|
|
||||||
fab dark small
|
fab dark small
|
||||||
:title="$gettext('Download')"
|
:title="$gettext('Download')"
|
||||||
color="download"
|
color="download"
|
||||||
class="action-download"
|
class="action-download"
|
||||||
:disabled="selection.length !== 1"
|
:disabled="!canDownload || selection.length !== 1"
|
||||||
@click.stop="download()"
|
@click.stop="download()"
|
||||||
>
|
>
|
||||||
<v-icon>get_app</v-icon>
|
<v-icon>get_app</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="features.albums"
|
v-if="canManage"
|
||||||
fab dark small
|
fab dark small
|
||||||
:title="$gettext('Add to album')"
|
:title="$gettext('Add to album')"
|
||||||
color="album"
|
color="album"
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
<v-icon>bookmark</v-icon>
|
<v-icon>bookmark</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="deletable.includes(context)"
|
v-if="canDelete && deletable.includes(context)"
|
||||||
fab dark small
|
fab dark small
|
||||||
color="remove"
|
color="remove"
|
||||||
:title="$gettext('Delete')"
|
:title="$gettext('Delete')"
|
||||||
@@ -104,16 +104,40 @@ export default {
|
|||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
refresh: Function,
|
refresh: {
|
||||||
clearSelection: Function,
|
type: Function,
|
||||||
share: Function,
|
default: () => {
|
||||||
edit: Function,
|
},
|
||||||
context: String,
|
},
|
||||||
|
clearSelection: {
|
||||||
|
type: Function,
|
||||||
|
default: () => {
|
||||||
|
},
|
||||||
|
},
|
||||||
|
share: {
|
||||||
|
type: Function,
|
||||||
|
default: () => {
|
||||||
|
},
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
type: Function,
|
||||||
|
default: () => {
|
||||||
|
},
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
const features = this.$config.settings().features;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
canDelete: this.$config.allow("albums", "delete"),
|
||||||
|
canDownload: this.$config.allow("albums", "download") && features.download,
|
||||||
|
canShare: this.$config.allow("albums", "share") && features.share,
|
||||||
|
canManage: this.$config.allow("albums", "manage"),
|
||||||
deletable: ["album", "moment", "state"],
|
deletable: ["album", "moment", "state"],
|
||||||
features: this.$config.settings().features,
|
|
||||||
expanded: false,
|
expanded: false,
|
||||||
dialog: {
|
dialog: {
|
||||||
delete: false,
|
delete: false,
|
||||||
|
|||||||
@@ -13,16 +13,16 @@
|
|||||||
<v-icon>refresh</v-icon>
|
<v-icon>refresh</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<v-btn icon class="action-edit" :title="$gettext('Edit')" @click.stop="dialog.edit = true">
|
<v-btn v-if="canManage" icon class="action-edit" :title="$gettext('Edit')" @click.stop="dialog.edit = true">
|
||||||
<v-icon>edit</v-icon>
|
<v-icon>edit</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<v-btn v-if="$config.feature('share')" icon class="action-share" :title="$gettext('Share')"
|
<v-btn v-if="canShare" icon class="action-share" :title="$gettext('Share')"
|
||||||
@click.stop="dialog.share = true">
|
@click.stop="dialog.share = true">
|
||||||
<v-icon>share</v-icon>
|
<v-icon>share</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<v-btn v-if="$config.feature('download')" icon class="hidden-xs-only action-download" :title="$gettext('Download')"
|
<v-btn v-if="canDownload" icon class="hidden-xs-only action-download" :title="$gettext('Download')"
|
||||||
@click.stop="download()">
|
@click.stop="download()">
|
||||||
<v-icon>get_app</v-icon>
|
<v-icon>get_app</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
<v-icon>view_column</v-icon>
|
<v-icon>view_column</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<v-btn v-if="!$config.values.readonly && $config.feature('upload')" icon class="hidden-sm-and-down action-upload"
|
<v-btn v-if="canUpload" icon class="hidden-sm-and-down action-upload"
|
||||||
:title="$gettext('Upload')" @click.stop="showUpload()">
|
:title="$gettext('Upload')" @click.stop="showUpload()">
|
||||||
<v-icon>cloud_upload</v-icon>
|
<v-icon>cloud_upload</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
@@ -109,8 +109,12 @@ export default {
|
|||||||
ID: '',
|
ID: '',
|
||||||
Name: this.$gettext('All Countries')
|
Name: this.$gettext('All Countries')
|
||||||
}].concat(this.$config.get('countries'));
|
}].concat(this.$config.get('countries'));
|
||||||
|
const features = this.$config.settings().features;
|
||||||
return {
|
return {
|
||||||
|
canUpload: this.$config.allow("albums", "upload") && features.upload,
|
||||||
|
canDownload: this.$config.allow("albums", "download") && features.download,
|
||||||
|
canShare: this.$config.allow("albums", "share") && features.share,
|
||||||
|
canManage: this.$config.allow("albums", "manage"),
|
||||||
experimental: this.$config.get("experimental"),
|
experimental: this.$config.get("experimental"),
|
||||||
isFullScreen: !!document.fullscreenElement,
|
isFullScreen: !!document.fullscreenElement,
|
||||||
categories: this.$config.albumCategories(),
|
categories: this.$config.albumCategories(),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<v-toolbar dark fixed flat scroll-off-screen dense color="navigation darken-1" class="nav-small elevation-2"
|
<v-toolbar dark fixed flat scroll-off-screen dense color="navigation darken-1" class="nav-small elevation-2"
|
||||||
@click.stop.prevent>
|
@click.stop.prevent>
|
||||||
<v-avatar tile :size="28" :class="{'clickable': auth}" @click.stop.prevent="showNavigation()">
|
<v-avatar tile :size="28" :class="{'clickable': auth}" @click.stop.prevent="showNavigation()">
|
||||||
<img :src="appIcon" :alt="config.name">
|
<img :src="appIcon" :alt="config.name" :class="{'animate-hue': indexing}">
|
||||||
</v-avatar>
|
</v-avatar>
|
||||||
<v-toolbar-title class="nav-title">
|
<v-toolbar-title class="nav-title">
|
||||||
<span :class="{'clickable': auth}" @click.stop.prevent="showNavigation()">{{ page.title }}</span>
|
<span :class="{'clickable': auth}" @click.stop.prevent="showNavigation()">{{ page.title }}</span>
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
</v-list-tile-action>
|
</v-list-tile-action>
|
||||||
</v-list-tile>
|
</v-list-tile>
|
||||||
|
|
||||||
<v-list-tile v-if="isMini" to="/browse" class="nav-browse" @click.stop="">
|
<v-list-tile v-if="isMini && $config.feature('search')" to="/browse" class="nav-browse" @click.stop="">
|
||||||
<v-list-tile-action :title="$gettext('Search')">
|
<v-list-tile-action :title="$gettext('Search')">
|
||||||
<v-icon>search</v-icon>
|
<v-icon>search</v-icon>
|
||||||
</v-list-tile-action>
|
</v-list-tile-action>
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
</v-list-tile-content>
|
</v-list-tile-content>
|
||||||
</v-list-tile>
|
</v-list-tile>
|
||||||
|
|
||||||
<v-list-group v-if="!isMini" prepend-icon="search" no-action>
|
<v-list-group v-if="!isMini && $config.feature('search')" prepend-icon="search" no-action>
|
||||||
<template #activator>
|
<template #activator>
|
||||||
<v-list-tile to="/browse" class="nav-browse" @click.stop="">
|
<v-list-tile to="/browse" class="nav-browse" @click.stop="">
|
||||||
<v-list-tile-content>
|
<v-list-tile-content>
|
||||||
@@ -177,9 +177,10 @@
|
|||||||
</v-list-tile-content>
|
</v-list-tile-content>
|
||||||
</v-list-tile>
|
</v-list-tile>
|
||||||
|
|
||||||
<v-list-group v-if="!isMini && $config.feature('albums')" prepend-icon="bookmark" no-action>
|
<template v-if="!isMini && $config.feature('albums')">
|
||||||
|
<v-list-group v-if="canSearch" prepend-icon="bookmark" no-action>
|
||||||
<template #activator>
|
<template #activator>
|
||||||
<v-list-tile to="/albums" class="nav-albums" @click.stop="">
|
<v-list-tile :to="{ name: 'albums' }" class="nav-albums" @click.stop="">
|
||||||
<v-list-tile-content>
|
<v-list-tile-content>
|
||||||
<v-list-tile-title class="p-flex-menuitem">
|
<v-list-tile-title class="p-flex-menuitem">
|
||||||
<translate key="Albums">Albums</translate>
|
<translate key="Albums">Albums</translate>
|
||||||
@@ -198,6 +199,20 @@
|
|||||||
</v-list-tile-content>
|
</v-list-tile-content>
|
||||||
</v-list-tile>
|
</v-list-tile>
|
||||||
</v-list-group>
|
</v-list-group>
|
||||||
|
<v-list-tile v-else :to="{ name: 'albums' }" class="nav-albums" @click.stop="">
|
||||||
|
<v-list-tile-action :title="$gettext('Albums')">
|
||||||
|
<v-icon>bookmark</v-icon>
|
||||||
|
</v-list-tile-action>
|
||||||
|
|
||||||
|
<v-list-tile-content>
|
||||||
|
<v-list-tile-title class="p-flex-menuitem">
|
||||||
|
<translate key="Albums">Albums</translate>
|
||||||
|
<span v-if="config.count.albums > 0"
|
||||||
|
:class="`nav-count ${rtl ? '--rtl' : ''}`">{{ config.count.albums | abbreviateCount }}</span>
|
||||||
|
</v-list-tile-title>
|
||||||
|
</v-list-tile-content>
|
||||||
|
</v-list-tile>
|
||||||
|
</template>
|
||||||
|
|
||||||
<v-list-tile v-if="isMini && $config.feature('videos')" to="/videos" class="nav-video" @click.stop="">
|
<v-list-tile v-if="isMini && $config.feature('videos')" to="/videos" class="nav-video" @click.stop="">
|
||||||
<v-list-tile-action :title="$gettext('Videos')">
|
<v-list-tile-action :title="$gettext('Videos')">
|
||||||
@@ -235,7 +250,7 @@
|
|||||||
</v-list-tile>
|
</v-list-tile>
|
||||||
</v-list-group>
|
</v-list-group>
|
||||||
|
|
||||||
<v-list-tile v-show="$config.feature('people')" :to="{ name: 'people' }" class="nav-people" @click.stop="">
|
<v-list-tile v-show="$config.feature('people') && (canManagePeople || config.count.people > 0)" :to="{ name: 'people' }" class="nav-people" @click.stop="">
|
||||||
<v-list-tile-action :title="$gettext('People')">
|
<v-list-tile-action :title="$gettext('People')">
|
||||||
<v-icon>person</v-icon>
|
<v-icon>person</v-icon>
|
||||||
</v-list-tile-action>
|
</v-list-tile-action>
|
||||||
@@ -249,7 +264,7 @@
|
|||||||
</v-list-tile-content>
|
</v-list-tile-content>
|
||||||
</v-list-tile>
|
</v-list-tile>
|
||||||
|
|
||||||
<v-list-tile to="/favorites" class="nav-favorites" @click.stop="">
|
<v-list-tile v-show="$config.feature('favorites')" :to="{ name: 'favorites' }" class="nav-favorites" @click.stop="">
|
||||||
<v-list-tile-action :title="$gettext('Favorites')">
|
<v-list-tile-action :title="$gettext('Favorites')">
|
||||||
<v-icon>favorite</v-icon>
|
<v-icon>favorite</v-icon>
|
||||||
</v-list-tile-action>
|
</v-list-tile-action>
|
||||||
@@ -278,7 +293,7 @@
|
|||||||
</v-list-tile-content>
|
</v-list-tile-content>
|
||||||
</v-list-tile>
|
</v-list-tile>
|
||||||
|
|
||||||
<v-list-tile :to="{ name: 'calendar' }" class="nav-calendar" @click.stop="">
|
<v-list-tile v-show="$config.feature('moments')" :to="{ name: 'calendar' }" class="nav-calendar" @click.stop="">
|
||||||
<v-list-tile-action :title="$gettext('Calendar')">
|
<v-list-tile-action :title="$gettext('Calendar')">
|
||||||
<v-icon>date_range</v-icon>
|
<v-icon>date_range</v-icon>
|
||||||
</v-list-tile-action>
|
</v-list-tile-action>
|
||||||
@@ -455,7 +470,7 @@
|
|||||||
</v-list-tile-content>
|
</v-list-tile-content>
|
||||||
</v-list-tile>
|
</v-list-tile>
|
||||||
|
|
||||||
<v-list-tile v-show="!isPublic" :to="{ name: 'feedback' }" :exact="true" class="nav-feedback"
|
<v-list-tile v-show="!isPublic && isAdmin" :to="{ name: 'feedback' }" :exact="true" class="nav-feedback"
|
||||||
@click.stop="">
|
@click.stop="">
|
||||||
<v-list-tile-content>
|
<v-list-tile-content>
|
||||||
<v-list-tile-title :class="`menu-item ${rtl ? '--rtl' : ''}`">
|
<v-list-tile-title :class="`menu-item ${rtl ? '--rtl' : ''}`">
|
||||||
@@ -575,10 +590,16 @@
|
|||||||
<translate>Albums</translate>
|
<translate>Albums</translate>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="auth && !routeName('library') && $config.feature('library')" class="menu-action nav-library">
|
<div v-if="auth && canManagePeople && !routeName('people') && $config.feature('people')" class="menu-action nav-people">
|
||||||
<router-link :to="{ name: 'library' }">
|
<router-link to="/places">
|
||||||
<v-icon>camera_roll</v-icon>
|
<v-icon>person</v-icon>
|
||||||
<translate>Index</translate>
|
<translate>People</translate>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div v-if="auth && !routeName('places') && $config.feature('places')" class="menu-action nav-places">
|
||||||
|
<router-link to="/places">
|
||||||
|
<v-icon>place</v-icon>
|
||||||
|
<translate>Places</translate>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="auth && !routeName('files') && $config.feature('files') && $config.feature('library')"
|
<div v-if="auth && !routeName('files') && $config.feature('files') && $config.feature('library')"
|
||||||
@@ -588,23 +609,29 @@
|
|||||||
<translate>Files</translate>
|
<translate>Files</translate>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="auth && !config.disable.settings && !routeName('settings')" class="menu-action nav-sync">
|
<div v-if="auth && !routeName('library') && $config.feature('library')" class="menu-action nav-library">
|
||||||
<router-link :to="{ name: 'settings_sync' }">
|
<router-link :to="{ name: 'library' }">
|
||||||
<v-icon>sync</v-icon>
|
<v-icon>camera_roll</v-icon>
|
||||||
<translate>Connect</translate>
|
<translate>Index</translate>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="auth && !config.disable.settings && !routeName('settings')" class="menu-action nav-account">
|
<div v-if="auth && $config.feature('sync') && !routeName('settings')" class="menu-action nav-sync">
|
||||||
|
<router-link :to="{ name: 'settings_sync' }">
|
||||||
|
<v-icon>sync</v-icon>
|
||||||
|
<translate>Sync</translate>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<!-- div v-if="auth && $config.feature('account') && !routeName('settings')" class="menu-action nav-account">
|
||||||
<router-link :to="{ name: 'settings_account' }">
|
<router-link :to="{ name: 'settings_account' }">
|
||||||
<v-icon>person</v-icon>
|
<v-icon>person</v-icon>
|
||||||
<translate>Account</translate>
|
<translate>Account</translate>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div -->
|
||||||
<div class="menu-action nav-manual"><a href="https://link.photoprism.app/docs" target="_blank">
|
<div class="menu-action nav-manual"><a href="https://link.photoprism.app/docs" target="_blank">
|
||||||
<v-icon>auto_stories</v-icon>
|
<v-icon>auto_stories</v-icon>
|
||||||
<translate>User Guide</translate>
|
<translate>User Guide</translate>
|
||||||
</a></div>
|
</a></div>
|
||||||
<div v-if="!isSponsor" class="menu-action nav-membership"><a href="https://link.photoprism.app/membership"
|
<div v-if="!isSponsor && isAdmin" class="menu-action nav-membership"><a href="https://link.photoprism.app/membership"
|
||||||
target="_blank">
|
target="_blank">
|
||||||
<v-icon>workspace_premium</v-icon>
|
<v-icon>workspace_premium</v-icon>
|
||||||
<translate>Become a sponsor</translate>
|
<translate>Become a sponsor</translate>
|
||||||
@@ -657,6 +684,8 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
canSearch: this.$config.allow("photos", "search"),
|
||||||
|
canManagePeople: this.$config.allow("people", "manage"),
|
||||||
appNameSuffix: appNameSuffix,
|
appNameSuffix: appNameSuffix,
|
||||||
appName: this.$config.getName(),
|
appName: this.$config.getName(),
|
||||||
appEdition: this.$config.getEdition(),
|
appEdition: this.$config.getEdition(),
|
||||||
@@ -666,6 +695,7 @@ export default {
|
|||||||
isMini: localStorage.getItem('last_navigation_mode') !== 'false',
|
isMini: localStorage.getItem('last_navigation_mode') !== 'false',
|
||||||
isPublic: this.$config.get("public"),
|
isPublic: this.$config.get("public"),
|
||||||
isDemo: this.$config.get("demo"),
|
isDemo: this.$config.get("demo"),
|
||||||
|
isAdmin: this.$session.isAdmin(),
|
||||||
isSponsor: this.$config.isSponsor(),
|
isSponsor: this.$config.isSponsor(),
|
||||||
isTest: this.$config.test,
|
isTest: this.$config.test,
|
||||||
isReadOnly: this.$config.get("readonly"),
|
isReadOnly: this.$config.get("readonly"),
|
||||||
@@ -698,19 +728,19 @@ export default {
|
|||||||
},
|
},
|
||||||
displayName() {
|
displayName() {
|
||||||
const user = this.$session.getUser();
|
const user = this.$session.getUser();
|
||||||
if (!user) {
|
if (user) {
|
||||||
return '';
|
return user.getDisplayName();
|
||||||
} else if (user.DisplayName) {
|
|
||||||
return user.DisplayName;
|
|
||||||
} else if (user.Username) {
|
|
||||||
return user.Username;
|
|
||||||
} else {
|
|
||||||
return 'User';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return this.$gettext("Unregistered");
|
||||||
},
|
},
|
||||||
accountInfo() {
|
accountInfo() {
|
||||||
const user = this.$session.getUser();
|
const user = this.$session.getUser();
|
||||||
return user.Email ? user.Email : this.$gettext("Account");
|
if (user) {
|
||||||
|
return user.getAccountInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.$gettext("Account");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
@@ -734,6 +764,10 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
routeName(name) {
|
routeName(name) {
|
||||||
|
if (!name || !this.$route.name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return this.$route.name.startsWith(name);
|
return this.$route.name.startsWith(name);
|
||||||
},
|
},
|
||||||
reloadApp() {
|
reloadApp() {
|
||||||
|
|||||||
@@ -17,12 +17,12 @@
|
|||||||
|
|
||||||
<button class="pswp__button pswp__button--close action-close" :title="$gettext('Close')"></button>
|
<button class="pswp__button pswp__button--close action-close" :title="$gettext('Close')"></button>
|
||||||
|
|
||||||
<button v-if="config.settings.features.download" class="pswp__button action-download" style="background: none;"
|
<button v-if="canDownload" class="pswp__button action-download" style="background: none;"
|
||||||
:title="$gettext('Download')" @click.exact="onDownload">
|
:title="$gettext('Download')" @click.exact="onDownload">
|
||||||
<v-icon size="16" color="white">get_app</v-icon>
|
<v-icon size="16" color="white">get_app</v-icon>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="pswp__button action-edit hidden-shared-only" style="background: none;" :title="$gettext('Edit')"
|
<button v-if="canEdit" class="pswp__button action-edit hidden-shared-only" style="background: none;" :title="$gettext('Edit')"
|
||||||
@click.exact="onEdit">
|
@click.exact="onEdit">
|
||||||
<v-icon size="16" color="white">edit</v-icon>
|
<v-icon size="16" color="white">edit</v-icon>
|
||||||
</button>
|
</button>
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
<v-icon v-else size="16" color="white">radio_button_off</v-icon>
|
<v-icon v-else size="16" color="white">radio_button_off</v-icon>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="pswp__button action-like hidden-shared-only" style="background: none;"
|
<button v-if="canLike" class="pswp__button action-like hidden-shared-only" style="background: none;"
|
||||||
:title="$gettext('Like')" @click.exact="onLike">
|
:title="$gettext('Like')" @click.exact="onLike">
|
||||||
<v-icon v-if="item.Favorite" size="16" color="white">favorite</v-icon>
|
<v-icon v-if="item.Favorite" size="16" color="white">favorite</v-icon>
|
||||||
<v-icon v-else size="16" color="white">favorite_border</v-icon>
|
<v-icon v-else size="16" color="white">favorite_border</v-icon>
|
||||||
@@ -96,6 +96,9 @@ export default {
|
|||||||
name: "PPhotoViewer",
|
name: "PPhotoViewer",
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
canEdit: this.$config.allow("photos", "update") && this.$config.feature("edit"),
|
||||||
|
canLike: this.$config.allow("photos", "manage") && this.$config.feature("favorites"),
|
||||||
|
canDownload: this.$config.allow("photos", "download") && this.$config.feature("download"),
|
||||||
selection: this.$clipboard.selection,
|
selection: this.$clipboard.selection,
|
||||||
config: this.$config.values,
|
config: this.$config.values,
|
||||||
item: new Thumb(),
|
item: new Thumb(),
|
||||||
|
|||||||
@@ -188,36 +188,36 @@
|
|||||||
</button>
|
</button>
|
||||||
</h3>
|
</h3>
|
||||||
<div v-if="photo.Description" class="caption mb-2" :title="$gettext('Description')">
|
<div v-if="photo.Description" class="caption mb-2" :title="$gettext('Description')">
|
||||||
<button @[!isSharedView&&`click`].exact="editPhoto(index)">
|
<button @click.exact="editPhoto(index)">
|
||||||
{{ photo.Description }}
|
{{ photo.Description }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="caption">
|
<div class="caption">
|
||||||
<button class="action-date-edit" :data-uid="photo.UID"
|
<button class="action-date-edit" :data-uid="photo.UID"
|
||||||
@[!isSharedView&&`click`].exact="editPhoto(index)">
|
@click.exact="editPhoto(index)">
|
||||||
<i :title="$gettext('Taken')">date_range</i>
|
<i :title="$gettext('Taken')">date_range</i>
|
||||||
{{ photo.getDateString(true) }}
|
{{ photo.getDateString(true) }}
|
||||||
</button>
|
</button>
|
||||||
<br>
|
<br>
|
||||||
<button v-if="photo.Type === 'video'" :title="$gettext('Video')"
|
<button v-if="photo.Type === 'video'" :title="$gettext('Video')"
|
||||||
@[!isSharedView&&`click`].exact="openPhoto(index)">
|
@click.exact="openPhoto(index)">
|
||||||
<i>movie</i>
|
<i>movie</i>
|
||||||
{{ photo.getVideoInfo() }}
|
{{ photo.getVideoInfo() }}
|
||||||
</button>
|
</button>
|
||||||
<button v-else-if="photo.Type === 'animated'" :title="$gettext('Animated')+' GIF'"
|
<button v-else-if="photo.Type === 'animated'" :title="$gettext('Animated')+' GIF'"
|
||||||
@[!isSharedView&&`click`].exact="openPhoto(index)">
|
@click.exact="openPhoto(index)">
|
||||||
<i>gif_box</i>
|
<i>gif_box</i>
|
||||||
{{ photo.getVideoInfo() }}
|
{{ photo.getVideoInfo() }}
|
||||||
</button>
|
</button>
|
||||||
<button v-else :title="$gettext('Camera')" class="action-camera-edit"
|
<button v-else :title="$gettext('Camera')" class="action-camera-edit"
|
||||||
:data-uid="photo.UID" @[!isSharedView&&`click`].exact="editPhoto(index)">
|
:data-uid="photo.UID" @click.exact="editPhoto(index)">
|
||||||
<i>photo_camera</i>
|
<i>photo_camera</i>
|
||||||
{{ photo.getPhotoInfo() }}
|
{{ photo.getPhotoInfo() }}
|
||||||
</button>
|
</button>
|
||||||
<template v-if="filter.order === 'name' && $config.feature('download')">
|
<template v-if="filter.order === 'name' && $config.feature('download')">
|
||||||
<br>
|
<br>
|
||||||
<button :title="$gettext('Name')"
|
<button :title="$gettext('Name')"
|
||||||
@[!isSharedView&&`click`].exact="downloadFile(index)">
|
@click.exact="downloadFile(index)">
|
||||||
<i>insert_drive_file</i>
|
<i>insert_drive_file</i>
|
||||||
{{ photo.baseName() }}
|
{{ photo.baseName() }}
|
||||||
</button>
|
</button>
|
||||||
@@ -225,7 +225,7 @@
|
|||||||
<template v-if="featPlaces && photo.Country !== 'zz'">
|
<template v-if="featPlaces && photo.Country !== 'zz'">
|
||||||
<br>
|
<br>
|
||||||
<button :title="$gettext('Location')" class="action-location"
|
<button :title="$gettext('Location')" class="action-location"
|
||||||
:data-uid="photo.UID" @[!isSharedView&&`click`].exact="openLocation(index)">
|
:data-uid="photo.UID" @click.exact="openLocation(index)">
|
||||||
<i>location_on</i>
|
<i>location_on</i>
|
||||||
{{ photo.locationInfo() }}
|
{{ photo.locationInfo() }}
|
||||||
</button>
|
</button>
|
||||||
@@ -269,13 +269,11 @@ export default {
|
|||||||
},
|
},
|
||||||
album: {
|
album: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {
|
default: () => {},
|
||||||
},
|
|
||||||
},
|
},
|
||||||
filter: {
|
filter: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {
|
default: () => {},
|
||||||
},
|
|
||||||
},
|
},
|
||||||
context: {
|
context: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -349,7 +347,7 @@ export default {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// we observe only every 5th item, so we increase the rendered
|
// we observe only every 5th item, so we increase the rendered
|
||||||
// range here by 4 items in every directio just to be safe
|
// range here by 4 items in every direction just to be safe
|
||||||
this.firstVisibleElementIndex = smallestIndex - 4;
|
this.firstVisibleElementIndex = smallestIndex - 4;
|
||||||
this.lastVisibileElementIndex = largestIndex + 4;
|
this.lastVisibileElementIndex = largestIndex + 4;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="context !== 'archive' && context !== 'review' && features.share" fab dark
|
v-if="canShare && context !== 'archive' && context !== 'review'" fab dark
|
||||||
small
|
small
|
||||||
:title="$gettext('Share')"
|
:title="$gettext('Share')"
|
||||||
color="share"
|
color="share"
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="context === 'review'" fab dark
|
v-if="canManage && context === 'review'" fab dark
|
||||||
small
|
small
|
||||||
:title="$gettext('Approve')"
|
:title="$gettext('Approve')"
|
||||||
color="share"
|
color="share"
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
<v-icon>check</v-icon>
|
<v-icon>check</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="context !== 'archive' && features.edit" fab dark
|
v-if="canEdit" fab dark
|
||||||
small
|
small
|
||||||
:title="$gettext('Edit')"
|
:title="$gettext('Edit')"
|
||||||
color="edit"
|
color="edit"
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
<v-icon>edit</v-icon>
|
<v-icon>edit</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="context !== 'archive' && features.private" fab dark
|
v-if="canTogglePrivate" fab dark
|
||||||
small
|
small
|
||||||
:title="$gettext('Change private flag')"
|
:title="$gettext('Change private flag')"
|
||||||
color="private"
|
color="private"
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
<v-icon>lock</v-icon>
|
<v-icon>lock</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="context !== 'archive' && features.download" fab dark
|
v-if="canDownload && context !== 'archive'" fab dark
|
||||||
small
|
small
|
||||||
:title="$gettext('Download')"
|
:title="$gettext('Download')"
|
||||||
:disabled="busy"
|
:disabled="busy"
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
<v-icon>get_app</v-icon>
|
<v-icon>get_app</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="context !== 'archive' && features.albums" fab dark
|
v-if="canEditAlbum && context !== 'archive'" fab dark
|
||||||
small
|
small
|
||||||
:title="$gettext('Add to album')"
|
:title="$gettext('Add to album')"
|
||||||
color="album"
|
color="album"
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
<v-icon>bookmark</v-icon>
|
<v-icon>bookmark</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="!isAlbum && context !== 'archive' && features.archive" fab dark
|
v-if="canArchive && !isAlbum && context !== 'archive'" fab dark
|
||||||
small
|
small
|
||||||
color="remove"
|
color="remove"
|
||||||
:title="$gettext('Archive')"
|
:title="$gettext('Archive')"
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
<v-icon>archive</v-icon>
|
<v-icon>archive</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="!album && context === 'archive'" fab dark
|
v-if="canArchive && !album && context === 'archive'" fab dark
|
||||||
small
|
small
|
||||||
color="restore"
|
color="restore"
|
||||||
:title="$gettext('Restore')"
|
:title="$gettext('Restore')"
|
||||||
@@ -112,7 +112,7 @@
|
|||||||
<v-icon>unarchive</v-icon>
|
<v-icon>unarchive</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="isAlbum && features.albums" fab dark
|
v-if="canEditAlbum && isAlbum" fab dark
|
||||||
small
|
small
|
||||||
:title="$gettext('Remove from album')"
|
:title="$gettext('Remove from album')"
|
||||||
color="remove"
|
color="remove"
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
<v-icon>eject</v-icon>
|
<v-icon>eject</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="!album && context === 'archive' && features.delete" fab dark
|
v-if="canDelete && !album && context === 'archive'" fab dark
|
||||||
small
|
small
|
||||||
:title="$gettext('Delete')"
|
:title="$gettext('Delete')"
|
||||||
color="remove"
|
color="remove"
|
||||||
@@ -181,10 +181,19 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
const features = this.$config.settings().features;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
canTogglePrivate: this.$config.allow("photos", "manage") && this.context !== 'archive' && features.private,
|
||||||
|
canArchive: this.$config.allow("photos", "delete") && features.archive,
|
||||||
|
canDelete: this.$config.allow("photos", "delete") && features.delete,
|
||||||
|
canDownload: this.$config.allow("photos", "download") && features.download,
|
||||||
|
canShare: this.$config.allow("photos", "share") && features.share,
|
||||||
|
canManage: this.$config.allow("photos", "manage") && features.albums,
|
||||||
|
canEdit: this.$config.allow("photos", "update") && features.edit,
|
||||||
|
canEditAlbum: this.$config.allow("albums", "update") && features.albums,
|
||||||
busy: false,
|
busy: false,
|
||||||
config: this.$config.values,
|
config: this.$config.values,
|
||||||
features: this.$config.settings().features,
|
|
||||||
expanded: false,
|
expanded: false,
|
||||||
isAlbum: this.album && this.album.Type === 'album',
|
isAlbum: this.album && this.album.Type === 'album',
|
||||||
dialog: {
|
dialog: {
|
||||||
@@ -202,7 +211,7 @@ export default {
|
|||||||
this.expanded = false;
|
this.expanded = false;
|
||||||
},
|
},
|
||||||
batchApprove() {
|
batchApprove() {
|
||||||
if (this.busy) {
|
if (this.busy || !this.canManage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,14 +228,18 @@ export default {
|
|||||||
this.clearClipboard();
|
this.clearClipboard();
|
||||||
},
|
},
|
||||||
archivePhotos() {
|
archivePhotos() {
|
||||||
if (!this.features.delete) {
|
if (!this.canArchive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.canDelete) {
|
||||||
this.dialog.archive = true;
|
this.dialog.archive = true;
|
||||||
} else {
|
} else {
|
||||||
this.batchArchive();
|
this.batchArchive();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
batchArchive() {
|
batchArchive() {
|
||||||
if (this.busy) {
|
if (this.busy || !this.canArchive) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,9 +257,17 @@ export default {
|
|||||||
this.clearClipboard();
|
this.clearClipboard();
|
||||||
},
|
},
|
||||||
deletePhotos() {
|
deletePhotos() {
|
||||||
|
if (!this.canDelete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.dialog.delete = true;
|
this.dialog.delete = true;
|
||||||
},
|
},
|
||||||
batchDelete() {
|
batchDelete() {
|
||||||
|
if (!this.canDelete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.dialog.delete = false;
|
this.dialog.delete = false;
|
||||||
|
|
||||||
Api.post("batch/photos/delete", {"photos": this.selection}).then(() => this.onDeleted());
|
Api.post("batch/photos/delete", {"photos": this.selection}).then(() => this.onDeleted());
|
||||||
@@ -269,7 +290,7 @@ export default {
|
|||||||
this.clearClipboard();
|
this.clearClipboard();
|
||||||
},
|
},
|
||||||
addToAlbum(ppid) {
|
addToAlbum(ppid) {
|
||||||
if (!ppid) {
|
if (!ppid || !this.canManage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,7 +316,7 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.busy) {
|
if (this.busy || !this.canManage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,7 +336,7 @@ export default {
|
|||||||
this.clearClipboard();
|
this.clearClipboard();
|
||||||
},
|
},
|
||||||
download() {
|
download() {
|
||||||
if (this.busy) {
|
if (this.busy || !this.canDownload) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ nav .v-list__tile__title.title {
|
|||||||
fill: rgba(255,255,255,.85);
|
fill: rgba(255,255,255,.85);
|
||||||
color: rgba(255,255,255,.85);
|
color: rgba(255,255,255,.85);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
min-width: 200px;
|
min-width: 210px;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 50px;
|
line-height: 50px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|||||||
@@ -343,6 +343,10 @@ body.chrome #photoprism .search-results .result {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#photoprism .disable-manage .search-results .result:not(.is-favorite) .input-favorite {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
#photoprism .cards-view .input-reject,
|
#photoprism .cards-view .input-reject,
|
||||||
#photoprism .mosaic-view .input-reject {
|
#photoprism .mosaic-view .input-reject {
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
hide-details autofocus
|
hide-details autofocus
|
||||||
:rules="[titleRule]"
|
:rules="[titleRule]"
|
||||||
:label="$gettext('Name')"
|
:label="$gettext('Name')"
|
||||||
|
:disabled="disabled"
|
||||||
color="secondary-dark"
|
color="secondary-dark"
|
||||||
class="input-title"
|
class="input-title"
|
||||||
@keyup.enter.native="confirm"
|
@keyup.enter.native="confirm"
|
||||||
@@ -32,6 +33,7 @@
|
|||||||
<v-text-field v-model="model.Location"
|
<v-text-field v-model="model.Location"
|
||||||
hide-details
|
hide-details
|
||||||
:label="$gettext('Location')"
|
:label="$gettext('Location')"
|
||||||
|
:disabled="disabled"
|
||||||
color="secondary-dark"
|
color="secondary-dark"
|
||||||
class="input-location"
|
class="input-location"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
@@ -43,6 +45,7 @@
|
|||||||
browser-autocomplete="off"
|
browser-autocomplete="off"
|
||||||
:label="$gettext('Description')"
|
:label="$gettext('Description')"
|
||||||
:rows="1"
|
:rows="1"
|
||||||
|
:disabled="disabled"
|
||||||
class="input-description"
|
class="input-description"
|
||||||
color="secondary-dark">
|
color="secondary-dark">
|
||||||
</v-textarea>
|
</v-textarea>
|
||||||
@@ -51,6 +54,7 @@
|
|||||||
<v-combobox v-model="model.Category" hide-details
|
<v-combobox v-model="model.Category" hide-details
|
||||||
:search-input.sync="model.Category"
|
:search-input.sync="model.Category"
|
||||||
:items="categories"
|
:items="categories"
|
||||||
|
:disabled="disabled"
|
||||||
:label="$gettext('Category')"
|
:label="$gettext('Category')"
|
||||||
:allow-overflow="false"
|
:allow-overflow="false"
|
||||||
return-masked-value
|
return-masked-value
|
||||||
@@ -65,6 +69,7 @@
|
|||||||
:label="$gettext('Sort Order')"
|
:label="$gettext('Sort Order')"
|
||||||
hide-details
|
hide-details
|
||||||
:items="sorting"
|
:items="sorting"
|
||||||
|
:disabled="disabled"
|
||||||
item-value="value"
|
item-value="value"
|
||||||
item-text="text"
|
item-text="text"
|
||||||
color="secondary-dark">
|
color="secondary-dark">
|
||||||
@@ -83,6 +88,7 @@
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn depressed dark color="primary-button"
|
<v-btn depressed dark color="primary-button"
|
||||||
class="action-confirm"
|
class="action-confirm"
|
||||||
|
:disabled="disabled"
|
||||||
@click.stop="confirm">
|
@click.stop="confirm">
|
||||||
<translate>Save</translate>
|
<translate>Save</translate>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
@@ -107,6 +113,7 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
disabled: !this.$config.allow("albums", "manage"),
|
||||||
model: new Album(),
|
model: new Album(),
|
||||||
growDesc: false,
|
growDesc: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -138,6 +145,11 @@ export default {
|
|||||||
this.$emit('close');
|
this.$emit('close');
|
||||||
},
|
},
|
||||||
confirm() {
|
confirm() {
|
||||||
|
if (this.disabled) {
|
||||||
|
this.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.model.update().then((m) => {
|
this.model.update().then((m) => {
|
||||||
this.categories = this.$config.albumCategories();
|
this.categories = this.$config.albumCategories();
|
||||||
this.$emit('close');
|
this.$emit('close');
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -25,81 +25,121 @@ Additional information can be found in our Developer Guide:
|
|||||||
|
|
||||||
import RestModel from "model/rest";
|
import RestModel from "model/rest";
|
||||||
import Form from "common/form";
|
import Form from "common/form";
|
||||||
|
import Util from "common/util";
|
||||||
import Api from "common/api";
|
import Api from "common/api";
|
||||||
import { $gettext } from "common/vm";
|
import { T, $gettext } from "common/vm";
|
||||||
|
|
||||||
export class User extends RestModel {
|
export class User extends RestModel {
|
||||||
getDefaults() {
|
getDefaults() {
|
||||||
return {
|
return {
|
||||||
UID: "",
|
UID: "",
|
||||||
Slug: "",
|
UUID: "",
|
||||||
Username: "",
|
AuthProvider: "",
|
||||||
|
AuthID: "",
|
||||||
|
Name: "",
|
||||||
|
DisplayName: "",
|
||||||
Email: "",
|
Email: "",
|
||||||
|
BackupEmail: "",
|
||||||
Role: "",
|
Role: "",
|
||||||
|
Attr: "",
|
||||||
SuperAdmin: false,
|
SuperAdmin: false,
|
||||||
CanLogin: false,
|
WebLogin: false,
|
||||||
|
WebDAV: false,
|
||||||
CanInvite: false,
|
CanInvite: false,
|
||||||
AuthUID: "",
|
Thumb: "",
|
||||||
AuthSrc: "",
|
ThumbSrc: "",
|
||||||
WebUID: "",
|
Settings: {
|
||||||
WebDAV: "",
|
UITheme: "",
|
||||||
AvatarURL: "",
|
UILanguage: "",
|
||||||
AvatarSrc: "",
|
UITimeZone: "",
|
||||||
Country: "",
|
MapsStyle: "",
|
||||||
TimeZone: "",
|
MapsAnimate: 0,
|
||||||
|
IndexPath: "",
|
||||||
|
IndexRescan: 0,
|
||||||
|
ImportPath: "",
|
||||||
|
ImportMove: 0,
|
||||||
|
UploadPath: "",
|
||||||
|
CreatedAt: "",
|
||||||
|
UpdatedAt: "",
|
||||||
|
},
|
||||||
|
Details: {
|
||||||
|
SubjUID: "",
|
||||||
|
SubjSrc: "",
|
||||||
PlaceID: "",
|
PlaceID: "",
|
||||||
PlaceSrc: "",
|
PlaceSrc: "",
|
||||||
CellID: "",
|
CellID: "",
|
||||||
SubjUID: "",
|
IdURL: "",
|
||||||
Bio: "",
|
AvatarURL: "",
|
||||||
Status: "",
|
SiteURL: "",
|
||||||
URL: "",
|
FeedURL: "",
|
||||||
Phone: "",
|
UserGender: "",
|
||||||
DisplayName: "",
|
NamePrefix: "",
|
||||||
FullName: "",
|
GivenName: "",
|
||||||
Alias: "",
|
MiddleName: "",
|
||||||
ArtistName: "",
|
FamilyName: "",
|
||||||
Artist: false,
|
NameSuffix: "",
|
||||||
Favorite: false,
|
NickName: "",
|
||||||
Hidden: false,
|
UserPhone: "",
|
||||||
Private: false,
|
UserAddress: "",
|
||||||
Excluded: false,
|
UserCountry: "",
|
||||||
CompanyName: "",
|
UserBio: "",
|
||||||
DepartmentName: "",
|
|
||||||
JobTitle: "",
|
JobTitle: "",
|
||||||
BusinessURL: "",
|
Department: "",
|
||||||
BusinessPhone: "",
|
Company: "",
|
||||||
BusinessEmail: "",
|
CompanyURL: "",
|
||||||
BackupEmail: "",
|
|
||||||
BirthYear: -1,
|
BirthYear: -1,
|
||||||
BirthMonth: -1,
|
BirthMonth: -1,
|
||||||
BirthDay: -1,
|
BirthDay: -1,
|
||||||
FileRoot: "",
|
|
||||||
FilePath: "",
|
|
||||||
InviteToken: "",
|
|
||||||
InvitedBy: "",
|
|
||||||
DownloadToken: "",
|
|
||||||
PreviewToken: "",
|
|
||||||
ConfirmedAt: "",
|
|
||||||
TermsAccepted: "",
|
|
||||||
CreatedAt: "",
|
CreatedAt: "",
|
||||||
UpdatedAt: "",
|
UpdatedAt: "",
|
||||||
DeletedAt: "",
|
},
|
||||||
|
VerifiedAt: "",
|
||||||
|
ConsentAt: "",
|
||||||
|
BornAt: "",
|
||||||
|
CreatedAt: "",
|
||||||
|
UpdatedAt: "",
|
||||||
|
ExpiresAt: "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getEntityName() {
|
getDisplayName() {
|
||||||
if (this.DisplayName) {
|
if (this.DisplayName) {
|
||||||
return this.DisplayName;
|
return this.DisplayName;
|
||||||
|
} else if (this.Details && this.Details.NickName) {
|
||||||
|
return this.Details.NickName;
|
||||||
|
} else if (this.Details && this.Details.GivenName) {
|
||||||
|
return this.Details.GivenName;
|
||||||
|
} else if (this.Name) {
|
||||||
|
return Util.capitalize(this.Name);
|
||||||
|
} else if (this.Details && this.Details.JobTitle) {
|
||||||
|
return this.Details.JobTitle;
|
||||||
|
} else if (this.Email) {
|
||||||
|
return this.Email;
|
||||||
|
} else if (this.Role) {
|
||||||
|
return T(Util.capitalize(this.Role));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.FullName) {
|
return this.$gettext("Unregistered");
|
||||||
return this.FullName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAccountInfo() {
|
||||||
|
if (this.Email) {
|
||||||
|
return this.Email;
|
||||||
|
} else if (this.Details && this.Details.JobTitle) {
|
||||||
|
return this.Details.JobTitle;
|
||||||
|
} else if (this.Role) {
|
||||||
|
return T(Util.capitalize(this.Role));
|
||||||
|
} else if (this.Name) {
|
||||||
return this.Name;
|
return this.Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return this.$gettext("Account");
|
||||||
|
}
|
||||||
|
|
||||||
|
getEntityName() {
|
||||||
|
return this.getDisplayName();
|
||||||
|
}
|
||||||
|
|
||||||
getRegisterForm() {
|
getRegisterForm() {
|
||||||
return Api.options(this.getEntityResource() + "/register").then((response) =>
|
return Api.options(this.getEntityResource() + "/register").then((response) =>
|
||||||
Promise.resolve(new Form(response.data))
|
Promise.resolve(new Form(response.data))
|
||||||
|
|||||||
@@ -405,3 +405,16 @@ export const ThumbFilters = () => [
|
|||||||
{ value: "cubic", text: $gettext("Cubic: Moderate Quality, Good Performance") },
|
{ value: "cubic", text: $gettext("Cubic: Moderate Quality, Good Performance") },
|
||||||
{ value: "linear", text: $gettext("Linear: Very Smooth, Best Performance") },
|
{ value: "linear", text: $gettext("Linear: Very Smooth, Best Performance") },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const UserRoles = () => [
|
||||||
|
{ value: "admin", text: $gettext("Admin") },
|
||||||
|
{ value: "user", text: $gettext("User") },
|
||||||
|
{ value: "family", text: $gettext("Family") },
|
||||||
|
{ value: "friend", text: $gettext("Friend") },
|
||||||
|
{ value: "viewer", text: $gettext("Viewer") },
|
||||||
|
{ value: "contributor", text: $gettext("Contributor") },
|
||||||
|
{ value: "guest", text: $gettext("Guest") },
|
||||||
|
{ value: "visitor", text: $gettext("Visitor") },
|
||||||
|
{ value: "unauthorized", text: $gettext("Unauthorized") },
|
||||||
|
{ value: "", text: $gettext("Unknown") },
|
||||||
|
];
|
||||||
|
|||||||
@@ -22,7 +22,8 @@
|
|||||||
:filter="filter"
|
:filter="filter"
|
||||||
:album="model"
|
:album="model"
|
||||||
:edit-photo="editPhoto"
|
:edit-photo="editPhoto"
|
||||||
:open-photo="openPhoto"></p-photo-mosaic>
|
:open-photo="openPhoto"
|
||||||
|
:is-shared-view="isShared"></p-photo-mosaic>
|
||||||
<p-photo-list v-else-if="settings.view === 'list'"
|
<p-photo-list v-else-if="settings.view === 'list'"
|
||||||
context="album"
|
context="album"
|
||||||
:photos="results"
|
:photos="results"
|
||||||
@@ -31,7 +32,8 @@
|
|||||||
:album="model"
|
:album="model"
|
||||||
:open-photo="openPhoto"
|
:open-photo="openPhoto"
|
||||||
:edit-photo="editPhoto"
|
:edit-photo="editPhoto"
|
||||||
:open-location="openLocation"></p-photo-list>
|
:open-location="openLocation"
|
||||||
|
:is-shared-view="isShared"></p-photo-list>
|
||||||
<p-photo-cards v-else
|
<p-photo-cards v-else
|
||||||
context="album"
|
context="album"
|
||||||
:photos="results"
|
:photos="results"
|
||||||
@@ -40,7 +42,8 @@
|
|||||||
:album="model"
|
:album="model"
|
||||||
:open-photo="openPhoto"
|
:open-photo="openPhoto"
|
||||||
:edit-photo="editPhoto"
|
:edit-photo="editPhoto"
|
||||||
:open-location="openLocation"></p-photo-cards>
|
:open-location="openLocation"
|
||||||
|
:is-shared-view="isShared"></p-photo-cards>
|
||||||
</v-container>
|
</v-container>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -74,6 +77,9 @@ export default {
|
|||||||
const batchSize = Photo.batchSize();
|
const batchSize = Photo.batchSize();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
isShared: this.$config.deny("photos", "manage"),
|
||||||
|
canEdit: this.$config.allow("photos", "update") && this.$config.feature("edit"),
|
||||||
|
hasPlaces: this.$config.allow("places", "view") && this.$config.feature("places"),
|
||||||
subscriptions: [],
|
subscriptions: [],
|
||||||
listen: false,
|
listen: false,
|
||||||
dirty: false,
|
dirty: false,
|
||||||
@@ -159,6 +165,10 @@ export default {
|
|||||||
return 'cards';
|
return 'cards';
|
||||||
},
|
},
|
||||||
openLocation(index) {
|
openLocation(index) {
|
||||||
|
if (!this.hasPlaces) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const photo = this.results[index];
|
const photo = this.results[index];
|
||||||
|
|
||||||
if (photo.CellID && photo.CellID !== "zz") {
|
if (photo.CellID && photo.CellID !== "zz") {
|
||||||
@@ -170,6 +180,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
editPhoto(index) {
|
editPhoto(index) {
|
||||||
|
if (!this.canEdit) {
|
||||||
|
return this.openPhoto(index);
|
||||||
|
}
|
||||||
|
|
||||||
let selection = this.results.map((p) => {
|
let selection = this.results.map((p) => {
|
||||||
return p.getId();
|
return p.getId();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-infinite-scroll="loadMore" class="p-page p-page-albums" style="user-select: none"
|
<div v-infinite-scroll="loadMore" :class="$config.aclClasses('albums')" class="p-page p-page-albums" style="user-select: none"
|
||||||
:infinite-scroll-disabled="scrollDisabled" :infinite-scroll-distance="scrollDistance"
|
:infinite-scroll-disabled="scrollDisabled" :infinite-scroll-distance="scrollDistance"
|
||||||
:infinite-scroll-listen-for-event="'scrollRefresh'">
|
:infinite-scroll-listen-for-event="'scrollRefresh'">
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
@mousedown.stop.prevent="input.mouseDown($event, index)"
|
@mousedown.stop.prevent="input.mouseDown($event, index)"
|
||||||
@click.stop.prevent="onClick($event, index)"
|
@click.stop.prevent="onClick($event, index)"
|
||||||
>
|
>
|
||||||
<v-btn v-if="featureShare && album.LinkCount > 0" :ripple="false"
|
<v-btn v-if="canShare && album.LinkCount > 0" :ripple="false"
|
||||||
icon flat absolute
|
icon flat absolute
|
||||||
class="action-share"
|
class="action-share"
|
||||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||||
@@ -191,7 +191,7 @@
|
|||||||
</v-card>
|
</v-card>
|
||||||
</v-flex>
|
</v-flex>
|
||||||
</v-layout>
|
</v-layout>
|
||||||
<div v-if="staticFilter.type === 'album' && config.count.albums === 0" class="text-xs-center my-2">
|
<div v-if="canManage && staticFilter.type === 'album' && config.count.albums === 0" class="text-xs-center my-2">
|
||||||
<v-btn class="action-add" color="secondary" round @click.prevent="create">
|
<v-btn class="action-add" color="secondary" round @click.prevent="create">
|
||||||
<translate>Add Album</translate>
|
<translate>Add Album</translate>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
@@ -234,6 +234,7 @@ export default {
|
|||||||
const category = query["category"] ? query["category"] : "";
|
const category = query["category"] ? query["category"] : "";
|
||||||
const filter = {q, category};
|
const filter = {q, category};
|
||||||
const settings = {};
|
const settings = {};
|
||||||
|
const features = this.$config.settings().features;
|
||||||
|
|
||||||
let categories = [{"value": "", "text": this.$gettext("All Categories")}];
|
let categories = [{"value": "", "text": this.$gettext("All Categories")}];
|
||||||
|
|
||||||
@@ -244,8 +245,12 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
canUpload: features.upload,
|
||||||
|
canShare: this.$config.allow("albums", "share") && features.share,
|
||||||
|
canManage: this.$config.allow("albums", "manage"),
|
||||||
|
canEdit: this.$config.allow("albums", "update"),
|
||||||
config: this.$config.values,
|
config: this.$config.values,
|
||||||
featureShare: this.$config.feature('share'),
|
featureShare: features.share,
|
||||||
categories: categories,
|
categories: categories,
|
||||||
subscriptions: [],
|
subscriptions: [],
|
||||||
listen: false,
|
listen: false,
|
||||||
@@ -271,7 +276,7 @@ export default {
|
|||||||
upload: false,
|
upload: false,
|
||||||
edit: false,
|
edit: false,
|
||||||
},
|
},
|
||||||
model: new Album(),
|
model: new Album(false),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -328,7 +333,7 @@ export default {
|
|||||||
window.localStorage.setItem("albums_offset", offset);
|
window.localStorage.setItem("albums_offset", offset);
|
||||||
},
|
},
|
||||||
share(album) {
|
share(album) {
|
||||||
if (!album) {
|
if (!album || !this.canShare) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,19 +343,34 @@ export default {
|
|||||||
edit(album) {
|
edit(album) {
|
||||||
if (!album) {
|
if (!album) {
|
||||||
return;
|
return;
|
||||||
|
} else if (!this.canManage) {
|
||||||
|
this.$router.push(album.route(this.view));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.model = album;
|
this.model = album;
|
||||||
this.dialog.edit = true;
|
this.dialog.edit = true;
|
||||||
},
|
},
|
||||||
webdavUpload() {
|
webdavUpload() {
|
||||||
|
if (!this.canShare) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.dialog.share = false;
|
this.dialog.share = false;
|
||||||
this.dialog.upload = true;
|
this.dialog.upload = true;
|
||||||
},
|
},
|
||||||
showUpload() {
|
showUpload() {
|
||||||
|
if (!this.canUpload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Event.publish("dialog.upload");
|
Event.publish("dialog.upload");
|
||||||
},
|
},
|
||||||
toggleLike(ev, index) {
|
toggleLike(ev, index) {
|
||||||
|
if (!this.canManage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const inputType = this.input.eval(ev, index);
|
const inputType = this.input.eval(ev, index);
|
||||||
|
|
||||||
if (inputType !== ClickShort) {
|
if (inputType !== ClickShort) {
|
||||||
@@ -391,6 +411,10 @@ export default {
|
|||||||
return (rangeEnd - rangeStart) + 1;
|
return (rangeEnd - rangeStart) + 1;
|
||||||
},
|
},
|
||||||
onShare(ev, index) {
|
onShare(ev, index) {
|
||||||
|
if (!this.canShare) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const inputType = this.input.eval(ev, index);
|
const inputType = this.input.eval(ev, index);
|
||||||
|
|
||||||
if (inputType !== ClickShort) {
|
if (inputType !== ClickShort) {
|
||||||
|
|||||||
@@ -10,11 +10,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="username"
|
v-model="name"
|
||||||
required hide-details solo flat light autofocus
|
required hide-details solo flat light autofocus
|
||||||
type="text"
|
type="text"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
name="username"
|
name="name"
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
autocapitalize="none"
|
autocapitalize="none"
|
||||||
:label="$gettext('Name')"
|
:label="$gettext('Name')"
|
||||||
@@ -99,9 +99,6 @@
|
|||||||
export default {
|
export default {
|
||||||
name: "PPageLogin",
|
name: "PPageLogin",
|
||||||
data() {
|
data() {
|
||||||
const c = this.$config.values;
|
|
||||||
const sponsor = this.$config.isSponsor();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
colors: {
|
colors: {
|
||||||
accent: "#05dde1",
|
accent: "#05dde1",
|
||||||
@@ -110,19 +107,19 @@ export default {
|
|||||||
},
|
},
|
||||||
loading: false,
|
loading: false,
|
||||||
showPassword: false,
|
showPassword: false,
|
||||||
username: "",
|
name: "",
|
||||||
password: "",
|
password: "",
|
||||||
sponsor: sponsor,
|
sponsor: this.$config.isSponsor(),
|
||||||
config: this.$config.values,
|
config: this.$config.values,
|
||||||
siteDescription: c.siteDescription ? c.siteDescription : c.siteCaption,
|
siteDescription: this.$config.getSiteDescription(),
|
||||||
nextUrl: this.$route.params.nextUrl ? this.$route.params.nextUrl : "/",
|
nextUrl: this.$route.params.nextUrl ? this.$route.params.nextUrl : "/",
|
||||||
wallpaperUri: c.wallpaperUri,
|
wallpaperUri: this.$config.values.wallpaperUri,
|
||||||
rtl: this.$rtl,
|
rtl: this.$rtl,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
loginDisabled() {
|
loginDisabled() {
|
||||||
return this.loading || this.username.trim() === "" || this.password.trim() === "";
|
return this.loading || this.name.trim() === "" || this.password.trim() === "";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
@@ -139,19 +136,27 @@ export default {
|
|||||||
|
|
||||||
return "";
|
return "";
|
||||||
},
|
},
|
||||||
|
load() {
|
||||||
|
this.$notify.blockUI();
|
||||||
|
|
||||||
|
let route = this.$router.resolve({
|
||||||
|
name: this.$session.getHome(),
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => { window.location = route.href; }, 100);
|
||||||
|
},
|
||||||
login() {
|
login() {
|
||||||
const username = this.username.trim();
|
const name = this.name.trim();
|
||||||
const password = this.password.trim();
|
const password = this.password.trim();
|
||||||
|
|
||||||
if (username === "" || password === "") {
|
if (name === "" || password === "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.$session.login(username, password).then(
|
this.$session.login(name, password).then(
|
||||||
() => {
|
() => {
|
||||||
this.loading = false;
|
this.load();
|
||||||
this.$router.push(this.nextUrl);
|
|
||||||
}
|
}
|
||||||
).catch(() => this.loading = false);
|
).catch(() => this.loading = false);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-infinite-scroll="loadMore" class="p-page p-page-labels" style="user-select: none"
|
<div v-infinite-scroll="loadMore" :class="$config.aclClasses('labels')" class="p-page p-page-labels" style="user-select: none"
|
||||||
:infinite-scroll-disabled="scrollDisabled" :infinite-scroll-distance="scrollDistance"
|
:infinite-scroll-disabled="scrollDisabled" :infinite-scroll-distance="scrollDistance"
|
||||||
:infinite-scroll-listen-for-event="'scrollRefresh'">
|
:infinite-scroll-listen-for-event="'scrollRefresh'">
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-page p-page-library">
|
<div :class="$config.aclClasses('library')" class="p-page p-page-library">
|
||||||
<v-tabs
|
<v-tabs
|
||||||
v-model="active"
|
v-model="active"
|
||||||
flat
|
flat
|
||||||
@@ -44,7 +44,10 @@ function initTabs(flag, tabs) {
|
|||||||
export default {
|
export default {
|
||||||
name: 'PPageLibrary',
|
name: 'PPageLibrary',
|
||||||
props: {
|
props: {
|
||||||
tab: String,
|
tab: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
const config = this.$config.values;
|
const config = this.$config.values;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<v-layout row wrap fill-height class="pa-0 ma-3">
|
<v-layout row wrap fill-height class="pa-0 ma-3">
|
||||||
<v-flex grow xs12 class="pa-2 terminal elevation-0 p-logs" style="overflow: auto;">
|
<v-flex grow xs12 class="pa-2 terminal elevation-0 p-logs" style="overflow: auto;">
|
||||||
<p v-if="logs.length === 0" class="p-log-empty">
|
<p v-if="logs.length === 0" class="p-log-empty">
|
||||||
<translate>Nothing to see here yet. Be patient.</translate>
|
<translate>Nothing to see here yet.</translate>
|
||||||
</p>
|
</p>
|
||||||
<p v-for="(log, index) in logs" :key="index.id" class="p-log-message text-selectable" :class="'p-log-' + log.level">
|
<p v-for="(log, index) in logs" :key="index.id" class="p-log-message text-selectable" :class="'p-log-' + log.level">
|
||||||
{{ formatTime(log.time) }} {{ level(log) }} <span>{{ log.message }}</span>
|
{{ formatTime(log.time) }} {{ level(log) }} <span>{{ log.message }}</span>
|
||||||
@@ -17,12 +17,14 @@
|
|||||||
import {DateTime} from "luxon";
|
import {DateTime} from "luxon";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'p-tab-logs',
|
name: 'PTabLogs',
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
logs: this.$log.logs,
|
logs: this.$log.logs,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
level(log) {
|
level(log) {
|
||||||
return log.level.substr(0, 4).toUpperCase();
|
return log.level.substr(0, 4).toUpperCase();
|
||||||
@@ -35,7 +37,5 @@ export default {
|
|||||||
return DateTime.fromISO(s).toFormat("yyyy-LL-dd HH:mm:ss");
|
return DateTime.fromISO(s).toFormat("yyyy-LL-dd HH:mm:ss");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created() {
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-page p-page-people">
|
<div class="p-page p-page-people" :class="$config.aclClasses('people')">
|
||||||
<v-tabs
|
<v-tabs
|
||||||
v-model="active"
|
v-model="active"
|
||||||
flat
|
flat
|
||||||
@@ -33,8 +33,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Subjects from "pages/people/subjects.vue";
|
import Recognized from "pages/people/recognized.vue";
|
||||||
import Faces from "pages/people/faces.vue";
|
import NewFaces from "pages/people/new.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PPagePeople',
|
name: 'PPagePeople',
|
||||||
@@ -47,24 +47,27 @@ export default {
|
|||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
'name': 'people',
|
'name': 'people',
|
||||||
'component': Subjects,
|
'component': Recognized,
|
||||||
'filter': {files: 1, type: "person"},
|
'filter': {files: 1, type: "person"},
|
||||||
'label': this.$gettext('Recognized'),
|
'label': this.$gettext('Recognized'),
|
||||||
'class': '',
|
'class': '',
|
||||||
'path': '/people',
|
'path': '/people',
|
||||||
'icon': 'people_alt',
|
'icon': 'people_alt',
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
|
|
||||||
|
if (this.$config.allow("people", "manage")) {
|
||||||
|
tabs.push({
|
||||||
'name': 'people_faces',
|
'name': 'people_faces',
|
||||||
'component': Faces,
|
'component': NewFaces,
|
||||||
'filter': {markers: true, unknown: true},
|
'filter': {markers: true, unknown: true},
|
||||||
'label': this.$gettext('New'),
|
'label': this.$gettext('New'),
|
||||||
'class': '',
|
'class': '',
|
||||||
'path': '/people/new',
|
'path': '/people/new',
|
||||||
'icon': 'person_add',
|
'icon': 'person_add',
|
||||||
'count': 0,
|
'count': 0,
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tabs: tabs,
|
tabs: tabs,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-page p-page-faces" style="user-select: none">
|
<div class="p-page p-page-faces" style="user-select: none">
|
||||||
|
|
||||||
<v-form ref="form" class="p-faces-search" lazy-validation dense @submit.prevent="updateQuery">
|
<v-form ref="form" class="p-faces-search" lazy-validation dense @submit.prevent="updateQuery">
|
||||||
<v-toolbar dense class="page-toolbar" flat color="secondary-light pa-0">
|
<v-toolbar dense class="page-toolbar" flat color="secondary-light pa-0">
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
<v-form ref="form" class="p-people-search" lazy-validation dense @submit.prevent="updateQuery()">
|
<v-form ref="form" class="p-people-search" lazy-validation dense @submit.prevent="updateQuery()">
|
||||||
<v-toolbar dense flat class="page-toolbar" color="secondary-light pa-0">
|
<v-toolbar dense flat class="page-toolbar" color="secondary-light pa-0">
|
||||||
<v-text-field :value="filter.q"
|
<v-text-field v-if="canSearch" :value="filter.q"
|
||||||
solo hide-details clearable overflow single-line validate-on-blur
|
solo hide-details clearable overflow single-line validate-on-blur
|
||||||
class="input-search background-inherit elevation-0"
|
class="input-search background-inherit elevation-0"
|
||||||
:label="$gettext('Search')"
|
:label="$gettext('Search')"
|
||||||
@@ -25,12 +25,14 @@
|
|||||||
<v-icon>refresh</v-icon>
|
<v-icon>refresh</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
|
<template v-if="canManage">
|
||||||
<v-btn v-if="!filter.hidden" icon class="action-show-hidden" :title="$gettext('Show hidden')" @click.stop="onShowHidden()">
|
<v-btn v-if="!filter.hidden" icon class="action-show-hidden" :title="$gettext('Show hidden')" @click.stop="onShowHidden()">
|
||||||
<v-icon>visibility</v-icon>
|
<v-icon>visibility</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn v-else icon class="action-exclude-hidden" :title="$gettext('Exclude hidden')" @click.stop="onExcludeHidden()">
|
<v-btn v-else icon class="action-exclude-hidden" :title="$gettext('Exclude hidden')" @click.stop="onExcludeHidden()">
|
||||||
<v-icon>visibility_off</v-icon>
|
<v-icon>visibility_off</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
</template>
|
||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
</v-form>
|
</v-form>
|
||||||
|
|
||||||
@@ -85,7 +87,7 @@
|
|||||||
@mousedown.stop.prevent="input.mouseDown($event, index)"
|
@mousedown.stop.prevent="input.mouseDown($event, index)"
|
||||||
@click.stop.prevent="onClick($event, index)"
|
@click.stop.prevent="onClick($event, index)"
|
||||||
>
|
>
|
||||||
<v-btn :ripple="false" :depressed="false" class="input-hidden"
|
<v-btn v-if="canManage" :ripple="false" :depressed="false" class="input-hidden"
|
||||||
icon flat small absolute
|
icon flat small absolute
|
||||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||||
@touchend.stop.prevent="onToggleHidden($event, index)"
|
@touchend.stop.prevent="onToggleHidden($event, index)"
|
||||||
@@ -119,6 +121,7 @@
|
|||||||
|
|
||||||
<v-card-title primary-title class="pa-3 card-details" style="user-select: none;" @click.stop.prevent="">
|
<v-card-title primary-title class="pa-3 card-details" style="user-select: none;" @click.stop.prevent="">
|
||||||
<v-edit-dialog
|
<v-edit-dialog
|
||||||
|
v-if="canManage"
|
||||||
:return-value.sync="model.Name"
|
:return-value.sync="model.Name"
|
||||||
lazy
|
lazy
|
||||||
class="inline-edit"
|
class="inline-edit"
|
||||||
@@ -142,6 +145,9 @@
|
|||||||
></v-text-field>
|
></v-text-field>
|
||||||
</template>
|
</template>
|
||||||
</v-edit-dialog>
|
</v-edit-dialog>
|
||||||
|
<span v-else class="body-2 ma-0">
|
||||||
|
{{ model.Name }}
|
||||||
|
</span>
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
|
|
||||||
<v-card-text primary-title class="pb-2 pt-0 card-details" style="user-select: none;"
|
<v-card-text primary-title class="pb-2 pt-0 card-details" style="user-select: none;"
|
||||||
@@ -195,6 +201,9 @@ export default {
|
|||||||
const order = this.sortOrder();
|
const order = this.sortOrder();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
canView: this.$config.allow("people", "view"),
|
||||||
|
canSearch: this.$config.allow("people", "search"),
|
||||||
|
canManage: this.$config.allow("people", "manage"),
|
||||||
view: 'all',
|
view: 'all',
|
||||||
config: this.$config.values,
|
config: this.$config.values,
|
||||||
subscriptions: [],
|
subscriptions: [],
|
||||||
@@ -263,7 +272,7 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onSave(m) {
|
onSave(m) {
|
||||||
if (!m.Name || m.Name.trim() === "") {
|
if (!this.canManage || !m.Name || m.Name.trim() === "") {
|
||||||
// Refuse to save empty name.
|
// Refuse to save empty name.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -288,6 +297,10 @@ export default {
|
|||||||
this.merge.subj2 = null;
|
this.merge.subj2 = null;
|
||||||
},
|
},
|
||||||
onMerge() {
|
onMerge() {
|
||||||
|
if(!this.canManage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.busy = true;
|
this.busy = true;
|
||||||
this.merge.show = false;
|
this.merge.show = false;
|
||||||
this.$notify.blockUI();
|
this.$notify.blockUI();
|
||||||
@@ -316,6 +329,10 @@ export default {
|
|||||||
window.localStorage.setItem("subjects_offset", offset);
|
window.localStorage.setItem("subjects_offset", offset);
|
||||||
},
|
},
|
||||||
toggleLike(ev, index) {
|
toggleLike(ev, index) {
|
||||||
|
if(!this.canManage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const inputType = this.input.eval(ev, index);
|
const inputType = this.input.eval(ev, index);
|
||||||
|
|
||||||
if (inputType !== ClickShort) {
|
if (inputType !== ClickShort) {
|
||||||
@@ -397,12 +414,24 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onShowHidden() {
|
onShowHidden() {
|
||||||
|
if(!this.canManage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.showHidden("yes");
|
this.showHidden("yes");
|
||||||
},
|
},
|
||||||
onExcludeHidden() {
|
onExcludeHidden() {
|
||||||
|
if(!this.canManage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.showHidden("");
|
this.showHidden("");
|
||||||
},
|
},
|
||||||
showHidden(value) {
|
showHidden(value) {
|
||||||
|
if(!this.canManage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.$sponsorFeatures().then(() => {
|
this.$sponsorFeatures().then(() => {
|
||||||
this.filter.hidden = value;
|
this.filter.hidden = value;
|
||||||
this.updateQuery();
|
this.updateQuery();
|
||||||
@@ -411,6 +440,10 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
onToggleHidden(ev, index) {
|
onToggleHidden(ev, index) {
|
||||||
|
if(!this.canManage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const inputType = this.input.eval(ev, index);
|
const inputType = this.input.eval(ev, index);
|
||||||
|
|
||||||
if (inputType !== ClickShort) {
|
if (inputType !== ClickShort) {
|
||||||
@@ -424,7 +457,7 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
toggleHidden(model) {
|
toggleHidden(model) {
|
||||||
if (!model) {
|
if (!model || !this.canManage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.busy = true;
|
this.busy = true;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-infinite-scroll="loadMore" class="p-page p-page-photos" style="user-select: none"
|
<div v-infinite-scroll="loadMore" :class="$config.aclClasses('photos')" class="p-page p-page-photos" style="user-select: none"
|
||||||
:infinite-scroll-disabled="scrollDisabled" :infinite-scroll-distance="scrollDistance"
|
:infinite-scroll-disabled="scrollDisabled" :infinite-scroll-distance="scrollDistance"
|
||||||
:infinite-scroll-listen-for-event="'scrollRefresh'">
|
:infinite-scroll-listen-for-event="'scrollRefresh'">
|
||||||
|
|
||||||
@@ -20,7 +20,8 @@
|
|||||||
:select-mode="selectMode"
|
:select-mode="selectMode"
|
||||||
:filter="filter"
|
:filter="filter"
|
||||||
:edit-photo="editPhoto"
|
:edit-photo="editPhoto"
|
||||||
:open-photo="openPhoto"></p-photo-mosaic>
|
:open-photo="openPhoto"
|
||||||
|
:is-shared-view="isShared"></p-photo-mosaic>
|
||||||
<p-photo-list v-else-if="settings.view === 'list'"
|
<p-photo-list v-else-if="settings.view === 'list'"
|
||||||
:context="context"
|
:context="context"
|
||||||
:photos="results"
|
:photos="results"
|
||||||
@@ -28,7 +29,8 @@
|
|||||||
:filter="filter"
|
:filter="filter"
|
||||||
:open-photo="openPhoto"
|
:open-photo="openPhoto"
|
||||||
:edit-photo="editPhoto"
|
:edit-photo="editPhoto"
|
||||||
:open-location="openLocation"></p-photo-list>
|
:open-location="openLocation"
|
||||||
|
:is-shared-view="isShared"></p-photo-list>
|
||||||
<p-photo-cards v-else
|
<p-photo-cards v-else
|
||||||
:context="context"
|
:context="context"
|
||||||
:photos="results"
|
:photos="results"
|
||||||
@@ -36,7 +38,8 @@
|
|||||||
:filter="filter"
|
:filter="filter"
|
||||||
:open-photo="openPhoto"
|
:open-photo="openPhoto"
|
||||||
:edit-photo="editPhoto"
|
:edit-photo="editPhoto"
|
||||||
:open-location="openLocation"></p-photo-cards>
|
:open-location="openLocation"
|
||||||
|
:is-shared-view="isShared"></p-photo-cards>
|
||||||
</v-container>
|
</v-container>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -82,13 +85,14 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const settings = this.$config.settings();
|
const settings = this.$config.settings();
|
||||||
|
const features = settings.features;
|
||||||
|
|
||||||
if (settings) {
|
if (settings) {
|
||||||
if (settings.features.private) {
|
if (features.private) {
|
||||||
filter.public = "true";
|
filter.public = "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings.features.review && (!this.staticFilter || !("quality" in this.staticFilter))) {
|
if (features.review && (!this.staticFilter || !("quality" in this.staticFilter))) {
|
||||||
filter.quality = "3";
|
filter.quality = "3";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,6 +100,9 @@ export default {
|
|||||||
const batchSize = Photo.batchSize();
|
const batchSize = Photo.batchSize();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
isShared: this.$config.deny("photos", "manage"),
|
||||||
|
canEdit: this.$config.allow("photos", "update") && features.edit,
|
||||||
|
hasPlaces: this.$config.allow("places", "view") && features.places,
|
||||||
subscriptions: [],
|
subscriptions: [],
|
||||||
listen: false,
|
listen: false,
|
||||||
dirty: false,
|
dirty: false,
|
||||||
@@ -226,6 +233,10 @@ export default {
|
|||||||
return "newest";
|
return "newest";
|
||||||
},
|
},
|
||||||
openLocation(index) {
|
openLocation(index) {
|
||||||
|
if (!this.hasPlaces) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const photo = this.results[index];
|
const photo = this.results[index];
|
||||||
|
|
||||||
if (photo.CellID && photo.CellID !== "zz") {
|
if (photo.CellID && photo.CellID !== "zz") {
|
||||||
@@ -237,6 +248,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
editPhoto(index) {
|
editPhoto(index) {
|
||||||
|
if (!this.canEdit) {
|
||||||
|
return this.openPhoto(index);
|
||||||
|
}
|
||||||
|
|
||||||
let selection = this.results.map((p) => {
|
let selection = this.results.map((p) => {
|
||||||
return p.getId();
|
return p.getId();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container fluid fill-height class="pa-0 p-page p-page-places">
|
<v-container fluid fill-height :class="$config.aclClasses('places')" class="pa-0 p-page p-page-places">
|
||||||
<div id="map" style="width: 100%; height: 100%;">
|
<div id="map" style="width: 100%; height: 100%;">
|
||||||
<div class="map-control">
|
<div class="map-control">
|
||||||
<div class="maplibregl-ctrl maplibregl-ctrl-group">
|
<div class="maplibregl-ctrl maplibregl-ctrl-group">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-page p-page-settings">
|
<div :class="$config.aclClasses('settings')" class="p-page p-page-settings">
|
||||||
<v-tabs
|
<v-tabs
|
||||||
v-model="active"
|
v-model="active"
|
||||||
flat
|
flat
|
||||||
@@ -13,7 +13,8 @@
|
|||||||
@click="changePath(item.path)">
|
@click="changePath(item.path)">
|
||||||
<v-icon v-if="$vuetify.breakpoint.smAndDown" :title="item.label">{{ item.icon }}</v-icon>
|
<v-icon v-if="$vuetify.breakpoint.smAndDown" :title="item.label">{{ item.icon }}</v-icon>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<v-icon :size="18" :left="!rtl" :right="rtl">{{ item.icon }}</v-icon> {{ item.label }}
|
<v-icon :size="18" :left="!rtl" :right="rtl">{{ item.icon }}</v-icon>
|
||||||
|
{{ item.label }}
|
||||||
</template>
|
</template>
|
||||||
</v-tab>
|
</v-tab>
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ import Library from "pages/settings/library.vue";
|
|||||||
import Advanced from "pages/settings/advanced.vue";
|
import Advanced from "pages/settings/advanced.vue";
|
||||||
import Sync from "pages/settings/sync.vue";
|
import Sync from "pages/settings/sync.vue";
|
||||||
import Account from "pages/settings/account.vue";
|
import Account from "pages/settings/account.vue";
|
||||||
|
import {config} from "app/session";
|
||||||
|
|
||||||
function initTabs(flag, tabs) {
|
function initTabs(flag, tabs) {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
@@ -47,11 +49,15 @@ function initTabs(flag, tabs) {
|
|||||||
export default {
|
export default {
|
||||||
name: 'PPageSettings',
|
name: 'PPageSettings',
|
||||||
props: {
|
props: {
|
||||||
tab: String,
|
tab: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
const isDemo = this.$config.get("demo");
|
const isDemo = this.$config.get("demo");
|
||||||
const isPublic = this.$config.get("public");
|
const isPublic = this.$config.get("public");
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
'name': 'settings-general',
|
'name': 'settings-general',
|
||||||
@@ -63,6 +69,7 @@ export default {
|
|||||||
'public': true,
|
'public': true,
|
||||||
'admin': true,
|
'admin': true,
|
||||||
'demo': true,
|
'demo': true,
|
||||||
|
'show': config.feature('settings'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'settings-library',
|
'name': 'settings-library',
|
||||||
@@ -74,6 +81,7 @@ export default {
|
|||||||
'public': true,
|
'public': true,
|
||||||
'admin': true,
|
'admin': true,
|
||||||
'demo': true,
|
'demo': true,
|
||||||
|
'show': config.feature('advanced'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'settings-advanced',
|
'name': 'settings-advanced',
|
||||||
@@ -85,6 +93,7 @@ export default {
|
|||||||
'public': false,
|
'public': false,
|
||||||
'admin': true,
|
'admin': true,
|
||||||
'demo': true,
|
'demo': true,
|
||||||
|
'show': config.feature('advanced'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'settings-sync',
|
'name': 'settings-sync',
|
||||||
@@ -96,6 +105,7 @@ export default {
|
|||||||
'public': false,
|
'public': false,
|
||||||
'admin': true,
|
'admin': true,
|
||||||
'demo': true,
|
'demo': true,
|
||||||
|
'show': config.feature('sync'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'settings-account',
|
'name': 'settings-account',
|
||||||
@@ -107,13 +117,16 @@ export default {
|
|||||||
'public': false,
|
'public': false,
|
||||||
'admin': true,
|
'admin': true,
|
||||||
'demo': true,
|
'demo': true,
|
||||||
|
'show': config.feature('account'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if(isDemo) {
|
if (isDemo) {
|
||||||
initTabs("demo", tabs);
|
initTabs("demo", tabs);
|
||||||
} else if(isPublic) {
|
} else if (isPublic) {
|
||||||
initTabs("public", tabs);
|
initTabs("public", tabs);
|
||||||
|
} else {
|
||||||
|
initTabs("show", tabs);
|
||||||
}
|
}
|
||||||
|
|
||||||
let active = 0;
|
let active = 0;
|
||||||
|
|||||||
@@ -43,13 +43,13 @@
|
|||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
<v-card flat tile class="mt-0 px-1 application">
|
<v-card v-if="isDemo || isAdmin" flat tile class="mt-0 px-1 application">
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-layout wrap align-top>
|
<v-layout wrap align-top>
|
||||||
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
|
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
|
||||||
<v-checkbox
|
<v-checkbox
|
||||||
v-model="settings.features.upload"
|
v-model="settings.features.upload"
|
||||||
:disabled="busy || config.readonly || demo"
|
:disabled="busy || config.readonly || isDemo"
|
||||||
class="ma-0 pa-0 input-upload"
|
class="ma-0 pa-0 input-upload"
|
||||||
color="secondary-dark"
|
color="secondary-dark"
|
||||||
:label="$gettext('Upload')"
|
:label="$gettext('Upload')"
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
|
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
|
||||||
<v-checkbox
|
<v-checkbox
|
||||||
v-model="settings.features.download"
|
v-model="settings.features.download"
|
||||||
:disabled="busy || demo"
|
:disabled="busy || isDemo"
|
||||||
class="ma-0 pa-0 input-download"
|
class="ma-0 pa-0 input-download"
|
||||||
color="secondary-dark"
|
color="secondary-dark"
|
||||||
:label="$gettext('Download')"
|
:label="$gettext('Download')"
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
|
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
|
||||||
<v-checkbox
|
<v-checkbox
|
||||||
v-model="settings.features.edit"
|
v-model="settings.features.edit"
|
||||||
:disabled="busy || demo"
|
:disabled="busy || isDemo"
|
||||||
class="ma-0 pa-0 input-edit"
|
class="ma-0 pa-0 input-edit"
|
||||||
color="secondary-dark"
|
color="secondary-dark"
|
||||||
:label="$gettext('Edit')"
|
:label="$gettext('Edit')"
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
|
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
|
||||||
<v-checkbox
|
<v-checkbox
|
||||||
v-model="settings.features.import"
|
v-model="settings.features.import"
|
||||||
:disabled="busy || config.readonly || demo"
|
:disabled="busy || config.readonly || isDemo"
|
||||||
class="ma-0 pa-0 input-import"
|
class="ma-0 pa-0 input-import"
|
||||||
color="secondary-dark"
|
color="secondary-dark"
|
||||||
:label="$gettext('Import')"
|
:label="$gettext('Import')"
|
||||||
@@ -229,7 +229,7 @@
|
|||||||
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
|
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
|
||||||
<v-checkbox
|
<v-checkbox
|
||||||
v-model="settings.features.library"
|
v-model="settings.features.library"
|
||||||
:disabled="busy || demo"
|
:disabled="busy || isDemo"
|
||||||
class="ma-0 pa-0 input-library"
|
class="ma-0 pa-0 input-library"
|
||||||
color="secondary-dark"
|
color="secondary-dark"
|
||||||
:label="$gettext('Library')"
|
:label="$gettext('Library')"
|
||||||
@@ -259,7 +259,7 @@
|
|||||||
<v-flex v-if="!config.disable.places" xs12 sm6 lg3 class="px-2 pb-2 pt-2">
|
<v-flex v-if="!config.disable.places" xs12 sm6 lg3 class="px-2 pb-2 pt-2">
|
||||||
<v-checkbox
|
<v-checkbox
|
||||||
v-model="settings.features.places"
|
v-model="settings.features.places"
|
||||||
:disabled="busy || demo"
|
:disabled="busy || isDemo"
|
||||||
class="ma-0 pa-0 input-places"
|
class="ma-0 pa-0 input-places"
|
||||||
color="secondary-dark"
|
color="secondary-dark"
|
||||||
:label="$gettext('Places')"
|
:label="$gettext('Places')"
|
||||||
@@ -331,10 +331,10 @@ import Event from "pubsub-js";
|
|||||||
export default {
|
export default {
|
||||||
name: 'PSettingsGeneral',
|
name: 'PSettingsGeneral',
|
||||||
data() {
|
data() {
|
||||||
const isDemo = this.$config.get("demo");
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
demo: isDemo,
|
isDemo: this.$config.get("demo"),
|
||||||
|
isAdmin: this.$session.isAdmin(),
|
||||||
|
isPublic: this.$config.get("public"),
|
||||||
config: this.$config.values,
|
config: this.$config.values,
|
||||||
settings: new Settings(this.$config.settings()),
|
settings: new Settings(this.$config.settings()),
|
||||||
options: options,
|
options: options,
|
||||||
@@ -421,6 +421,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.settings.save().then(() => {
|
this.settings.save().then(() => {
|
||||||
|
this.$config.setSettings(this.settings);
|
||||||
if (reload) {
|
if (reload) {
|
||||||
this.$notify.info(this.$gettext("Reloading…"));
|
this.$notify.info(this.$gettext("Reloading…"));
|
||||||
this.$notify.blockUI();
|
this.$notify.blockUI();
|
||||||
|
|||||||
@@ -53,60 +53,61 @@ import Hls from "hls.js";
|
|||||||
import { $gettext, Mount } from "common/vm";
|
import { $gettext, Mount } from "common/vm";
|
||||||
import * as options from "./options/options";
|
import * as options from "./options/options";
|
||||||
|
|
||||||
// Initialize helpers
|
config.load().finally(() => {
|
||||||
const viewer = new Viewer();
|
// Initialize helpers.
|
||||||
const isPublic = config.get("public");
|
const viewer = new Viewer();
|
||||||
const isMobile =
|
const isPublic = config.get("public");
|
||||||
|
const isMobile =
|
||||||
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
|
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
|
||||||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2);
|
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2);
|
||||||
|
|
||||||
// Initialize language and detect alignment
|
// Initialize language and detect alignment.
|
||||||
Vue.config.language = config.values.settings.ui.language;
|
Vue.config.language = config.getLanguage();
|
||||||
Settings.defaultLocale = Vue.config.language.substring(0, 2);
|
Settings.defaultLocale = Vue.config.language.substring(0, 2);
|
||||||
const languages = options.Languages();
|
const languages = options.Languages();
|
||||||
const rtl = languages.some((lang) => lang.value === Vue.config.language && lang.rtl);
|
const rtl = languages.some((lang) => lang.value === Vue.config.language && lang.rtl);
|
||||||
|
|
||||||
// Get initial theme colors from config
|
// Get initial theme colors from config.
|
||||||
const theme = config.theme.colors;
|
const theme = config.theme.colors;
|
||||||
|
|
||||||
// HTTP Live Streaming (video support)
|
// HTTP Live Streaming (video support)
|
||||||
window.Hls = Hls;
|
window.Hls = Hls;
|
||||||
|
|
||||||
// Assign helpers to VueJS prototype
|
// Assign helpers to VueJS prototype.
|
||||||
Vue.prototype.$event = Event;
|
Vue.prototype.$event = Event;
|
||||||
Vue.prototype.$notify = Notify;
|
Vue.prototype.$notify = Notify;
|
||||||
Vue.prototype.$scrollbar = Scrollbar;
|
Vue.prototype.$scrollbar = Scrollbar;
|
||||||
Vue.prototype.$viewer = viewer;
|
Vue.prototype.$viewer = viewer;
|
||||||
Vue.prototype.$session = session;
|
Vue.prototype.$session = session;
|
||||||
Vue.prototype.$api = Api;
|
Vue.prototype.$api = Api;
|
||||||
Vue.prototype.$log = Log;
|
Vue.prototype.$log = Log;
|
||||||
Vue.prototype.$socket = Socket;
|
Vue.prototype.$socket = Socket;
|
||||||
Vue.prototype.$config = config;
|
Vue.prototype.$config = config;
|
||||||
Vue.prototype.$clipboard = Clipboard;
|
Vue.prototype.$clipboard = Clipboard;
|
||||||
Vue.prototype.$isMobile = isMobile;
|
Vue.prototype.$isMobile = isMobile;
|
||||||
Vue.prototype.$rtl = rtl;
|
Vue.prototype.$rtl = rtl;
|
||||||
|
|
||||||
// Register Vuetify
|
// Register Vuetify.
|
||||||
Vue.use(Vuetify, { rtl, icons, theme });
|
Vue.use(Vuetify, { rtl, icons, theme });
|
||||||
|
|
||||||
// Register other VueJS plugins
|
// Register other VueJS plugins.
|
||||||
Vue.use(GetTextPlugin, {
|
Vue.use(GetTextPlugin, {
|
||||||
translations: config.translations,
|
translations: config.translations,
|
||||||
silent: true, // !config.values.debug,
|
silent: true, // !config.values.debug,
|
||||||
defaultLanguage: Vue.config.language,
|
defaultLanguage: Vue.config.language,
|
||||||
autoAddKeyAttributes: true,
|
autoAddKeyAttributes: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
Vue.use(VueLuxon);
|
Vue.use(VueLuxon);
|
||||||
Vue.use(VueInfiniteScroll);
|
Vue.use(VueInfiniteScroll);
|
||||||
Vue.use(VueFullscreen);
|
Vue.use(VueFullscreen);
|
||||||
Vue.use(VueFilters);
|
Vue.use(VueFilters);
|
||||||
Vue.use(Components);
|
Vue.use(Components);
|
||||||
Vue.use(Dialogs);
|
Vue.use(Dialogs);
|
||||||
Vue.use(Router);
|
Vue.use(Router);
|
||||||
|
|
||||||
// Configure client-side routing
|
// Configure client-side routing.
|
||||||
const router = new Router({
|
const router = new Router({
|
||||||
routes: Routes,
|
routes: Routes,
|
||||||
mode: "history",
|
mode: "history",
|
||||||
base: config.baseUri + "/",
|
base: config.baseUri + "/",
|
||||||
@@ -124,13 +125,16 @@ const router = new Router({
|
|||||||
return { x: 0, y: 0 };
|
return { x: 0, y: 0 };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
if (document.querySelector(".v-dialog--active.v-dialog--fullscreen")) {
|
if (document.querySelector(".v-dialog--active.v-dialog--fullscreen")) {
|
||||||
// Disable back button in full-screen viewers and editors.
|
// Disable back button in full-screen viewers and editors.
|
||||||
next(false);
|
next(false);
|
||||||
} else if (to.matched.some((record) => record.meta.settings) && config.values.disable.settings) {
|
} else if (
|
||||||
|
to.matched.some((record) => record.meta.settings) &&
|
||||||
|
config.values.disable.settings
|
||||||
|
) {
|
||||||
next({ name: "home" });
|
next({ name: "home" });
|
||||||
} else if (to.matched.some((record) => record.meta.admin)) {
|
} else if (to.matched.some((record) => record.meta.admin)) {
|
||||||
if (isPublic || session.isAdmin()) {
|
if (isPublic || session.isAdmin()) {
|
||||||
@@ -153,9 +157,9 @@ router.beforeEach((to, from, next) => {
|
|||||||
} else {
|
} else {
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.afterEach((to) => {
|
router.afterEach((to) => {
|
||||||
if (to.meta.title && config.values.siteTitle !== to.meta.title) {
|
if (to.meta.title && config.values.siteTitle !== to.meta.title) {
|
||||||
config.page.title = $gettext(to.meta.title);
|
config.page.title = $gettext(to.meta.title);
|
||||||
window.document.title = config.page.title;
|
window.document.title = config.page.title;
|
||||||
@@ -163,14 +167,15 @@ router.afterEach((to) => {
|
|||||||
config.page.title = config.values.siteTitle;
|
config.page.title = config.values.siteTitle;
|
||||||
window.document.title = config.values.siteTitle;
|
window.document.title = config.values.siteTitle;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
document.body.classList.add("mobile");
|
document.body.classList.add("mobile");
|
||||||
} else {
|
} else {
|
||||||
// Pull client config every 10 minutes in case push fails (except on mobile to save battery).
|
// Pull client config every 10 minutes in case push fails (except on mobile to save battery).
|
||||||
setInterval(() => config.update(), 600000);
|
setInterval(() => config.update(), 600000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start application.
|
// Start application.
|
||||||
Mount(Vue, PhotoPrism, router);
|
Mount(Vue, PhotoPrism, router);
|
||||||
|
});
|
||||||
|
|||||||
@@ -34,24 +34,46 @@ describe("common/session", () => {
|
|||||||
const storage = new StorageShim();
|
const storage = new StorageShim();
|
||||||
const session = new Session(storage, config);
|
const session = new Session(storage, config);
|
||||||
assert.isFalse(session.user.hasId());
|
assert.isFalse(session.user.hasId());
|
||||||
const values = {
|
|
||||||
user: {
|
const user = {
|
||||||
ID: 5,
|
ID: 5,
|
||||||
NickName: "Foo",
|
NickName: "Foo",
|
||||||
FullName: "Max Last",
|
GivenName: "Max",
|
||||||
PrimaryEmail: "test@test.com",
|
DisplayName: "Max Example",
|
||||||
RoleAdmin: true,
|
Email: "test@test.com",
|
||||||
},
|
SuperAdmin: true,
|
||||||
|
Role: "admin",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
user,
|
||||||
|
};
|
||||||
|
|
||||||
session.setData();
|
session.setData();
|
||||||
assert.equal(session.user.FullName, "");
|
assert.equal(session.user.DisplayName, "");
|
||||||
session.setData(values);
|
session.setData(data);
|
||||||
assert.equal(session.user.FullName, "Max Last");
|
assert.equal(session.user.DisplayName, "Max Example");
|
||||||
assert.equal(session.user.RoleAdmin, true);
|
assert.equal(session.user.SuperAdmin, true);
|
||||||
|
assert.equal(session.user.Role, "admin");
|
||||||
|
session.deleteAll();
|
||||||
|
assert.equal(session.user.DisplayName, "");
|
||||||
|
assert.equal(session.user.SuperAdmin, false);
|
||||||
|
assert.equal(session.user.Role, "");
|
||||||
|
session.setUser(user);
|
||||||
|
assert.equal(session.user.DisplayName, "Max Example");
|
||||||
|
assert.equal(session.user.SuperAdmin, true);
|
||||||
|
assert.equal(session.user.Role, "admin");
|
||||||
|
|
||||||
const result = session.getUser();
|
const result = session.getUser();
|
||||||
|
|
||||||
|
assert.equal(result.DisplayName, "Max Example");
|
||||||
|
assert.equal(result.SuperAdmin, true);
|
||||||
|
assert.equal(result.Role, "admin");
|
||||||
|
assert.equal(result.Email, "test@test.com");
|
||||||
assert.equal(result.ID, 5);
|
assert.equal(result.ID, 5);
|
||||||
assert.equal(result.PrimaryEmail, "test@test.com");
|
|
||||||
session.deleteData();
|
session.deleteData();
|
||||||
|
assert.isTrue(session.user.hasId());
|
||||||
|
session.deleteUser();
|
||||||
assert.isFalse(session.user.hasId());
|
assert.isFalse(session.user.hasId());
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,8 +83,8 @@ describe("common/session", () => {
|
|||||||
const values = {
|
const values = {
|
||||||
user: {
|
user: {
|
||||||
ID: 5,
|
ID: 5,
|
||||||
DisplayName: "Foo",
|
Name: "foo",
|
||||||
FullName: "Max Last",
|
DisplayName: "Max Last",
|
||||||
Email: "test@test.com",
|
Email: "test@test.com",
|
||||||
Role: "admin",
|
Role: "admin",
|
||||||
},
|
},
|
||||||
@@ -72,8 +94,8 @@ describe("common/session", () => {
|
|||||||
assert.equal(result, "test@test.com");
|
assert.equal(result, "test@test.com");
|
||||||
const values2 = {
|
const values2 = {
|
||||||
user: {
|
user: {
|
||||||
DisplayName: "Foo",
|
Name: "foo",
|
||||||
FullName: "Max Last",
|
DisplayName: "Max Last",
|
||||||
Email: "test@test.com",
|
Email: "test@test.com",
|
||||||
Role: "admin",
|
Role: "admin",
|
||||||
},
|
},
|
||||||
@@ -84,32 +106,33 @@ describe("common/session", () => {
|
|||||||
session.deleteData();
|
session.deleteData();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should get user nick name", () => {
|
it("should get user display name", () => {
|
||||||
const storage = new StorageShim();
|
const storage = new StorageShim();
|
||||||
const session = new Session(storage, config);
|
const session = new Session(storage, config);
|
||||||
const values = {
|
const values = {
|
||||||
user: {
|
user: {
|
||||||
ID: 5,
|
ID: 5,
|
||||||
DisplayName: "Foo",
|
Name: "foo",
|
||||||
FullName: "Max Last",
|
DisplayName: "Max Last",
|
||||||
Email: "test@test.com",
|
Email: "test@test.com",
|
||||||
Role: "admin",
|
Role: "admin",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
session.setData(values);
|
session.setData(values);
|
||||||
const result = session.getDisplayName();
|
const result = session.getDisplayName();
|
||||||
assert.equal(result, "Foo");
|
assert.equal(result, "Max Last");
|
||||||
const values2 = {
|
const values2 = {
|
||||||
user: {
|
user: {
|
||||||
DisplayName: "Bar",
|
ID: 5,
|
||||||
FullName: "Max Last",
|
Name: "bar",
|
||||||
|
DisplayName: "",
|
||||||
Email: "test@test.com",
|
Email: "test@test.com",
|
||||||
Role: "admin",
|
Role: "admin",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
session.setData(values2);
|
session.setData(values2);
|
||||||
const result2 = session.getDisplayName();
|
const result2 = session.getDisplayName();
|
||||||
assert.equal(result2, "");
|
assert.equal(result2, "Bar");
|
||||||
session.deleteData();
|
session.deleteData();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -119,19 +142,19 @@ describe("common/session", () => {
|
|||||||
const values = {
|
const values = {
|
||||||
user: {
|
user: {
|
||||||
ID: 5,
|
ID: 5,
|
||||||
DisplayName: "Foo",
|
Name: "foo",
|
||||||
FullName: "Max Last",
|
DisplayName: "Max Last",
|
||||||
Email: "test@test.com",
|
Email: "test@test.com",
|
||||||
Role: "admin",
|
Role: "admin",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
session.setData(values);
|
session.setData(values);
|
||||||
const result = session.getDisplayName();
|
const result = session.getDisplayName();
|
||||||
assert.equal(result, "Foo");
|
assert.equal(result, "Max Last");
|
||||||
const values2 = {
|
const values2 = {
|
||||||
user: {
|
user: {
|
||||||
DisplayName: "Bar",
|
Name: "bar",
|
||||||
FullName: "Max New",
|
DisplayName: "Max New",
|
||||||
Email: "test@test.com",
|
Email: "test@test.com",
|
||||||
Role: "admin",
|
Role: "admin",
|
||||||
},
|
},
|
||||||
@@ -148,8 +171,8 @@ describe("common/session", () => {
|
|||||||
const values = {
|
const values = {
|
||||||
user: {
|
user: {
|
||||||
ID: 5,
|
ID: 5,
|
||||||
DisplayName: "Foo",
|
Name: "foo",
|
||||||
FullName: "Max Last",
|
DisplayName: "Max Last",
|
||||||
Email: "test@test.com",
|
Email: "test@test.com",
|
||||||
Role: "admin",
|
Role: "admin",
|
||||||
},
|
},
|
||||||
@@ -166,8 +189,8 @@ describe("common/session", () => {
|
|||||||
const values = {
|
const values = {
|
||||||
user: {
|
user: {
|
||||||
ID: 5,
|
ID: 5,
|
||||||
DisplayName: "Foo",
|
Name: "foo",
|
||||||
FullName: "Max Last",
|
DisplayName: "Max Last",
|
||||||
Email: "test@test.com",
|
Email: "test@test.com",
|
||||||
Role: "admin",
|
Role: "admin",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -36,7 +36,11 @@ const putEntityResponse = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const deleteEntityResponse = null;
|
const deleteEntityResponse = null;
|
||||||
Mock.onPost("api/v1/users/55/profile").reply(200, { FullName: "Max New" }, mockHeaders);
|
Mock.onPost("api/v1/users/urii20d30w2wqzjf/profile").reply(
|
||||||
|
200,
|
||||||
|
{ DisplayName: "Max New" },
|
||||||
|
mockHeaders
|
||||||
|
);
|
||||||
Mock.onGet("api/v1/foo").reply(200, getCollectionResponse, mockHeaders);
|
Mock.onGet("api/v1/foo").reply(200, getCollectionResponse, mockHeaders);
|
||||||
Mock.onGet("api/v1/foo/123").reply(200, getEntityResponse, mockHeaders);
|
Mock.onGet("api/v1/foo/123").reply(200, getEntityResponse, mockHeaders);
|
||||||
Mock.onPost("api/v1/foo").reply(201, postEntityResponse, mockHeaders);
|
Mock.onPost("api/v1/foo").reply(201, postEntityResponse, mockHeaders);
|
||||||
@@ -260,9 +264,9 @@ Mock.onPut("api/v1/albums/abc").reply(
|
|||||||
mockHeaders
|
mockHeaders
|
||||||
);
|
);
|
||||||
|
|
||||||
//Mock.onPost("api/v1/users/55/profile").reply(200, { FullName: "Max New" }, mockHeaders);
|
//Mock.onPost("api/v1/users/55/profile").reply(200, { DisplayName: "Max New" }, mockHeaders);
|
||||||
//Mock.onPost("users/55/profile").reply(200, { FullName: "Max New" }, mockHeaders);
|
//Mock.onPost("users/55/profile").reply(200, { DisplayName: "Max New" }, mockHeaders);
|
||||||
//Mock.onPost("api/v1/users/55/profile").reply(200, { FullName: "Max New" }, mockHeaders);
|
//Mock.onPost("api/v1/users/55/profile").reply(200, { DisplayName: "Max New" }, mockHeaders);
|
||||||
|
|
||||||
Mock.onAny("api/v1/users/52/register").reply(200, { foo: "register" }, mockHeaders);
|
Mock.onAny("api/v1/users/52/register").reply(200, { foo: "register" }, mockHeaders);
|
||||||
|
|
||||||
@@ -336,7 +340,6 @@ Mock.onGet("api/v1/config/options").reply(200, { success: "ok" }, mockHeaders);
|
|||||||
Mock.onPost("api/v1/config/options").reply(200, { success: "ok" }, mockHeaders);
|
Mock.onPost("api/v1/config/options").reply(200, { success: "ok" }, mockHeaders);
|
||||||
Mock.onPost("api/v1/albums").reply(200, { success: "ok" }, mockHeaders);
|
Mock.onPost("api/v1/albums").reply(200, { success: "ok" }, mockHeaders);
|
||||||
|
|
||||||
|
|
||||||
//Mock.onPost().reply(200);
|
//Mock.onPost().reply(200);
|
||||||
//Mock.onDelete().reply(200);
|
//Mock.onDelete().reply(200);
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -6,14 +6,28 @@ let assert = chai.assert;
|
|||||||
|
|
||||||
describe("model/user", () => {
|
describe("model/user", () => {
|
||||||
it("should get entity name", () => {
|
it("should get entity name", () => {
|
||||||
const values = { ID: 5, Username: "max", FullName: "Max Last", Email: "test@test.com", Role: "admin" };
|
const values = {
|
||||||
|
ID: 5,
|
||||||
|
Name: "max",
|
||||||
|
DisplayName: "Max Last",
|
||||||
|
Email: "test@test.com",
|
||||||
|
Role: "admin",
|
||||||
|
};
|
||||||
|
|
||||||
const user = new User(values);
|
const user = new User(values);
|
||||||
const result = user.getEntityName();
|
const result = user.getEntityName();
|
||||||
assert.equal(result, "Max Last");
|
assert.equal(result, "Max Last");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should get id", () => {
|
it("should get id", () => {
|
||||||
const values = { ID: 5, Username: "max", FullName: "Max Last", Email: "test@test.com", Role: "admin" };
|
const values = {
|
||||||
|
ID: 5,
|
||||||
|
Name: "max",
|
||||||
|
DisplayName: "Max Last",
|
||||||
|
Email: "test@test.com",
|
||||||
|
Role: "admin",
|
||||||
|
};
|
||||||
|
|
||||||
const user = new User(values);
|
const user = new User(values);
|
||||||
const result = user.getId();
|
const result = user.getId();
|
||||||
assert.equal(result, 5);
|
assert.equal(result, 5);
|
||||||
@@ -30,31 +44,45 @@ describe("model/user", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should get register form", async () => {
|
it("should get register form", async () => {
|
||||||
const values = { ID: 52, Username: "max", FullName: "Max Last" };
|
const values = { ID: 52, Name: "max", DisplayName: "Max Last" };
|
||||||
const user = new User(values);
|
const user = new User(values);
|
||||||
const result = await user.getRegisterForm();
|
const result = await user.getRegisterForm();
|
||||||
assert.equal(result.definition.foo, "register");
|
assert.equal(result.definition.foo, "register");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should get profile form", async () => {
|
it("should get profile form", async () => {
|
||||||
const values = { ID: 53, Username: "max", FullName: "Max Last" };
|
const values = { ID: 53, Name: "max", DisplayName: "Max Last" };
|
||||||
const user = new User(values);
|
const user = new User(values);
|
||||||
const result = await user.getProfileForm();
|
const result = await user.getProfileForm();
|
||||||
assert.equal(result.definition.foo, "profile");
|
assert.equal(result.definition.foo, "profile");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should get change password", async () => {
|
it("should get change password", async () => {
|
||||||
const values = { ID: 54, Username: "max", FullName: "Max Last", Email: "test@test.com", Role: "admin" };
|
const values = {
|
||||||
|
ID: 54,
|
||||||
|
Name: "max",
|
||||||
|
DisplayName: "Max Last",
|
||||||
|
Email: "test@test.com",
|
||||||
|
Role: "admin",
|
||||||
|
};
|
||||||
|
|
||||||
const user = new User(values);
|
const user = new User(values);
|
||||||
const result = await user.changePassword("old", "new");
|
const result = await user.changePassword("old", "new");
|
||||||
assert.equal(result.new_password, "new");
|
assert.equal(result.new_password, "new");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should save profile", async () => {
|
it("should save profile", async () => {
|
||||||
const values = { ID: 55, Username: "max", FullName: "Max Last", Email: "test@test.com", Role: "admin" };
|
const values = {
|
||||||
|
UID: "urii20d30w2wqzjf",
|
||||||
|
Name: "max",
|
||||||
|
DisplayName: "Max Last",
|
||||||
|
Email: "test@test.com",
|
||||||
|
Role: "admin",
|
||||||
|
};
|
||||||
|
|
||||||
const user = new User(values);
|
const user = new User(values);
|
||||||
assert.equal(user.FullName, "Max Last");
|
assert.equal(user.DisplayName, "Max Last");
|
||||||
await user.saveProfile();
|
await user.saveProfile();
|
||||||
assert.equal(user.FullName, "Max New");
|
assert.equal(user.DisplayName, "Max New");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
6
go.mod
6
go.mod
@@ -20,7 +20,7 @@ require (
|
|||||||
github.com/go-errors/errors v1.4.2 // indirect
|
github.com/go-errors/errors v1.4.2 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.11.0 // indirect
|
github.com/go-playground/validator/v10 v10.11.0 // indirect
|
||||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551
|
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551
|
||||||
github.com/google/open-location-code/go v0.0.0-20220627184029-8a4173398f7e
|
github.com/google/open-location-code/go v0.0.0-20220922185916-75f4f40254f8
|
||||||
github.com/gorilla/websocket v1.5.0
|
github.com/gorilla/websocket v1.5.0
|
||||||
github.com/gosimple/slug v1.12.0
|
github.com/gosimple/slug v1.12.0
|
||||||
github.com/h2non/filetype v1.1.3
|
github.com/h2non/filetype v1.1.3
|
||||||
@@ -50,8 +50,8 @@ require (
|
|||||||
github.com/ulule/deepcopier v0.0.0-20200430083143-45decc6639b6
|
github.com/ulule/deepcopier v0.0.0-20200430083143-45decc6639b6
|
||||||
github.com/urfave/cli v1.22.10
|
github.com/urfave/cli v1.22.10
|
||||||
go4.org v0.0.0-20201209231011-d4a079459e60 // indirect
|
go4.org v0.0.0-20201209231011-d4a079459e60 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90
|
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be
|
||||||
golang.org/x/net v0.0.0-20220909164309-bea034e7d591
|
golang.org/x/net v0.0.0-20220927171203-f486391704dc
|
||||||
gonum.org/v1/gonum v0.12.0
|
gonum.org/v1/gonum v0.12.0
|
||||||
gopkg.in/photoprism/go-tz.v2 v2.1.1
|
gopkg.in/photoprism/go-tz.v2 v2.1.1
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
|
|||||||
12
go.sum
12
go.sum
@@ -164,8 +164,8 @@ github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
|||||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
github.com/google/open-location-code/go v0.0.0-20220627184029-8a4173398f7e h1:6yfwVPy5ecxRjRsS2LcQERwIFJg6QRkMjs7K2mKYMvM=
|
github.com/google/open-location-code/go v0.0.0-20220922185916-75f4f40254f8 h1:k2VIEPX7uDmceLb5cOKws0cHrvIMak1TT+Le4WcFreU=
|
||||||
github.com/google/open-location-code/go v0.0.0-20220627184029-8a4173398f7e/go.mod h1:eJfRN6aj+kR/rnua/rw9jAgYhqoMHldQkdTi+sePRKk=
|
github.com/google/open-location-code/go v0.0.0-20220922185916-75f4f40254f8/go.mod h1:eJfRN6aj+kR/rnua/rw9jAgYhqoMHldQkdTi+sePRKk=
|
||||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
@@ -336,8 +336,8 @@ golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPh
|
|||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
|
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A=
|
||||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
@@ -412,8 +412,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
|
|||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220909164309-bea034e7d591 h1:D0B/7al0LLrVC8aWF4+oxpv/m8bc7ViFfVS8/gXGdqI=
|
golang.org/x/net v0.0.0-20220927171203-f486391704dc h1:FxpXZdoBqT8RjqTy6i1E8nXHhW21wK7ptQ/EPIGxzPQ=
|
||||||
golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
golang.org/x/net v0.0.0-20220927171203-f486391704dc/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Package acl provides access control lists for authorization checking of user actions.
|
Package acl provides access control lists for authorization checks.
|
||||||
|
|
||||||
Copyright (c) 2018 - 2022 PhotoPrism UG. All rights reserved.
|
Copyright (c) 2018 - 2022 PhotoPrism UG. All rights reserved.
|
||||||
|
|
||||||
@@ -24,43 +24,56 @@ Additional information can be found in our Developer Guide:
|
|||||||
*/
|
*/
|
||||||
package acl
|
package acl
|
||||||
|
|
||||||
type Permission struct {
|
// ACL represents an access control list based on Resource, Roles, and Permissions.
|
||||||
Roles Roles
|
|
||||||
Actions Actions
|
|
||||||
}
|
|
||||||
|
|
||||||
type ACL map[Resource]Roles
|
type ACL map[Resource]Roles
|
||||||
|
|
||||||
func (l ACL) Deny(resource Resource, role Role, action Action) bool {
|
// Deny checks whether the role must be denied access to the specified resource.
|
||||||
return !l.Allow(resource, role, action)
|
func (acl ACL) Deny(resource Resource, role Role, perm Permission) bool {
|
||||||
|
return !acl.Allow(resource, role, perm)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l ACL) Allow(resource Resource, role Role, action Action) bool {
|
// DenyAll checks whether the role is granted none of the permissions for the specified resource.
|
||||||
if p, ok := l[resource]; ok {
|
func (acl ACL) DenyAll(resource Resource, role Role, perms Permissions) bool {
|
||||||
return p.Allow(role, action)
|
return !acl.AllowAny(resource, role, perms)
|
||||||
} else if p, ok := l[ResourceDefault]; ok {
|
}
|
||||||
return p.Allow(role, action)
|
|
||||||
|
// Allow checks whether the role is granted permission for the specified resource.
|
||||||
|
func (acl ACL) Allow(resource Resource, role Role, perm Permission) bool {
|
||||||
|
if p, ok := acl[resource]; ok {
|
||||||
|
return p.Allow(role, perm)
|
||||||
|
} else if p, ok = acl[ResourceDefault]; ok {
|
||||||
|
return p.Allow(role, perm)
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Actions) Allow(action Action) bool {
|
// AllowAny checks whether the role is granted any of the permissions for the specified resource.
|
||||||
if result, ok := a[action]; ok {
|
func (acl ACL) AllowAny(resource Resource, role Role, perms Permissions) bool {
|
||||||
return result
|
if len(perms) == 0 {
|
||||||
} else if result, ok := a[ActionDefault]; ok {
|
return false
|
||||||
return result
|
}
|
||||||
|
|
||||||
|
for i := range perms {
|
||||||
|
if acl.Allow(resource, role, perms[i]) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r Roles) Allow(role Role, action Action) bool {
|
// AllowAll checks whether the role is granted all of the permissions for the specified resource.
|
||||||
if a, ok := r[role]; ok {
|
func (acl ACL) AllowAll(resource Resource, role Role, perms Permissions) bool {
|
||||||
return a.Allow(action)
|
if len(perms) == 0 {
|
||||||
} else if a, ok := r[RoleDefault]; ok {
|
return false
|
||||||
return a.Allow(action)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i := range perms {
|
||||||
|
if acl.Deny(resource, role, perms[i]) {
|
||||||
return false
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
12
internal/acl/acl_events.go
Normal file
12
internal/acl/acl_events.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package acl
|
||||||
|
|
||||||
|
// Events specifies granted permissions by event channel and Role.
|
||||||
|
var Events = ACL{
|
||||||
|
ResourceDefault: Roles{
|
||||||
|
RoleAdmin: GrantFullAccess,
|
||||||
|
},
|
||||||
|
ChannelSession: Roles{
|
||||||
|
RoleAdmin: GrantFullAccess,
|
||||||
|
RoleVisitor: GrantSubscribeOwn,
|
||||||
|
},
|
||||||
|
}
|
||||||
67
internal/acl/acl_resources.go
Normal file
67
internal/acl/acl_resources.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package acl
|
||||||
|
|
||||||
|
// Resources specifies granted permissions by Resource and Role.
|
||||||
|
var Resources = ACL{
|
||||||
|
ResourceDefault: Roles{
|
||||||
|
RoleAdmin: GrantFullAccess,
|
||||||
|
},
|
||||||
|
ResourceConfig: Roles{
|
||||||
|
RoleAdmin: GrantFullAccess,
|
||||||
|
},
|
||||||
|
ResourceSettings: Roles{
|
||||||
|
RoleAdmin: GrantFullAccess,
|
||||||
|
},
|
||||||
|
ResourceCalendar: Roles{
|
||||||
|
RoleAdmin: GrantFullAccess,
|
||||||
|
},
|
||||||
|
ResourceMoments: Roles{
|
||||||
|
RoleAdmin: GrantFullAccess,
|
||||||
|
},
|
||||||
|
ResourceFiles: Roles{
|
||||||
|
RoleAdmin: GrantFullAccess,
|
||||||
|
},
|
||||||
|
ResourcePeople: Roles{
|
||||||
|
RoleAdmin: GrantFullAccess,
|
||||||
|
},
|
||||||
|
ResourceFavorites: Roles{
|
||||||
|
RoleAdmin: GrantFullAccess,
|
||||||
|
},
|
||||||
|
ResourceFeedback: Roles{
|
||||||
|
RoleAdmin: GrantFullAccess,
|
||||||
|
},
|
||||||
|
ResourceShares: Roles{
|
||||||
|
RoleAdmin: GrantFullAccess,
|
||||||
|
},
|
||||||
|
ResourcePassword: Roles{
|
||||||
|
RoleAdmin: GrantFullAccess,
|
||||||
|
},
|
||||||
|
ResourceAccounts: Roles{
|
||||||
|
RoleAdmin: GrantFullAccess,
|
||||||
|
},
|
||||||
|
ResourceLogs: Roles{
|
||||||
|
RoleAdmin: GrantFullAccess,
|
||||||
|
},
|
||||||
|
ResourceLabels: Roles{
|
||||||
|
RoleAdmin: GrantFullAccess,
|
||||||
|
},
|
||||||
|
ResourcePhotos: Roles{
|
||||||
|
RoleAdmin: GrantFullAccess,
|
||||||
|
RoleVisitor: Grant{AccessShared: true, ActionView: true, ActionDownload: true},
|
||||||
|
},
|
||||||
|
ResourceAlbums: Roles{
|
||||||
|
RoleAdmin: GrantFullAccess,
|
||||||
|
RoleVisitor: Grant{AccessShared: true, ActionView: true, ActionDownload: true},
|
||||||
|
},
|
||||||
|
ResourceVideos: Roles{
|
||||||
|
RoleAdmin: GrantFullAccess,
|
||||||
|
RoleVisitor: Grant{AccessShared: true, ActionView: true, ActionDownload: true},
|
||||||
|
},
|
||||||
|
ResourcePlaces: Roles{
|
||||||
|
RoleAdmin: GrantFullAccess,
|
||||||
|
RoleVisitor: Grant{AccessShared: true, ActionView: true, ActionDownload: true},
|
||||||
|
},
|
||||||
|
ResourceFolders: Roles{
|
||||||
|
RoleAdmin: GrantFullAccess,
|
||||||
|
RoleVisitor: Grant{AccessShared: true, ActionView: true, ActionDownload: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
107
internal/acl/acl_test.go
Normal file
107
internal/acl/acl_test.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package acl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestACL_Allow(t *testing.T) {
|
||||||
|
t.Run("ResourcePhotosRoleAdminActionModify", func(t *testing.T) {
|
||||||
|
assert.True(t, Resources.Allow(ResourcePhotos, RoleAdmin, ActionUpdate))
|
||||||
|
})
|
||||||
|
t.Run("ResourceDefaultRoleAdminActionDefault", func(t *testing.T) {
|
||||||
|
assert.True(t, Resources.Allow(ResourceDefault, RoleAdmin, FullAccess))
|
||||||
|
})
|
||||||
|
t.Run("ResourceDefaultRoleVisitorActionDefault", func(t *testing.T) {
|
||||||
|
assert.False(t, Resources.Allow(ResourceDefault, RoleVisitor, FullAccess))
|
||||||
|
})
|
||||||
|
t.Run("ResourcePhotosRoleVisitorActionDefault", func(t *testing.T) {
|
||||||
|
assert.False(t, Resources.Allow(ResourcePhotos, RoleVisitor, FullAccess))
|
||||||
|
})
|
||||||
|
t.Run("ResourceAlbumsRoleVisitorAccessShared", func(t *testing.T) {
|
||||||
|
assert.True(t, Resources.Allow(ResourceAlbums, RoleVisitor, AccessShared))
|
||||||
|
})
|
||||||
|
t.Run("ResourceAlbumsRoleVisitorActionDefault", func(t *testing.T) {
|
||||||
|
assert.False(t, Resources.Allow(ResourceAlbums, RoleVisitor, FullAccess))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestACL_AllowAny(t *testing.T) {
|
||||||
|
t.Run("Empty", func(t *testing.T) {
|
||||||
|
assert.False(t, Resources.AllowAny(ResourceAlbums, RoleVisitor, Permissions{}))
|
||||||
|
})
|
||||||
|
t.Run("VisitorAccess", func(t *testing.T) {
|
||||||
|
assert.True(t, Resources.AllowAny(ResourceAlbums, RoleVisitor, Permissions{AccessAll, AccessShared}))
|
||||||
|
assert.True(t, Resources.AllowAny(ResourceAlbums, RoleVisitor, Permissions{AccessShared}))
|
||||||
|
assert.False(t, Resources.AllowAny(ResourceAlbums, RoleVisitor, Permissions{AccessAll}))
|
||||||
|
})
|
||||||
|
t.Run("ResourcePhotosRoleAdminActionModify", func(t *testing.T) {
|
||||||
|
assert.True(t, Resources.AllowAny(ResourcePhotos, RoleAdmin, Permissions{ActionUpdate}))
|
||||||
|
})
|
||||||
|
t.Run("ResourceDefaultRoleAdminActionDefault", func(t *testing.T) {
|
||||||
|
assert.True(t, Resources.AllowAny(ResourceDefault, RoleAdmin, Permissions{FullAccess}))
|
||||||
|
})
|
||||||
|
t.Run("ResourceDefaultRoleVisitorActionDefault", func(t *testing.T) {
|
||||||
|
assert.False(t, Resources.AllowAny(ResourceDefault, RoleVisitor, Permissions{FullAccess}))
|
||||||
|
})
|
||||||
|
t.Run("ResourcePhotosRoleVisitorActionDefault", func(t *testing.T) {
|
||||||
|
assert.False(t, Resources.AllowAny(ResourcePhotos, RoleVisitor, Permissions{FullAccess}))
|
||||||
|
})
|
||||||
|
t.Run("ResourceAlbumsRoleVisitorAccessShared", func(t *testing.T) {
|
||||||
|
assert.True(t, Resources.AllowAny(ResourceAlbums, RoleVisitor, Permissions{AccessShared}))
|
||||||
|
})
|
||||||
|
t.Run("ResourceAlbumsRoleVisitorActionDefault", func(t *testing.T) {
|
||||||
|
assert.False(t, Resources.AllowAny(ResourceAlbums, RoleVisitor, Permissions{FullAccess}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestACL_AllowAll(t *testing.T) {
|
||||||
|
t.Run("Empty", func(t *testing.T) {
|
||||||
|
assert.False(t, Resources.AllowAll(ResourceAlbums, RoleVisitor, Permissions{}))
|
||||||
|
})
|
||||||
|
t.Run("VisitorAccess", func(t *testing.T) {
|
||||||
|
assert.False(t, Resources.AllowAll(ResourceAlbums, RoleVisitor, Permissions{AccessAll, AccessShared}))
|
||||||
|
assert.True(t, Resources.AllowAll(ResourceAlbums, RoleVisitor, Permissions{AccessShared}))
|
||||||
|
assert.False(t, Resources.AllowAll(ResourceAlbums, RoleVisitor, Permissions{AccessAll}))
|
||||||
|
})
|
||||||
|
t.Run("ResourcePhotosRoleAdminActionModify", func(t *testing.T) {
|
||||||
|
assert.True(t, Resources.AllowAll(ResourcePhotos, RoleAdmin, Permissions{ActionUpdate}))
|
||||||
|
})
|
||||||
|
t.Run("ResourceDefaultRoleAdminActionDefault", func(t *testing.T) {
|
||||||
|
assert.True(t, Resources.AllowAll(ResourceDefault, RoleAdmin, Permissions{FullAccess}))
|
||||||
|
})
|
||||||
|
t.Run("ResourceDefaultRoleVisitorActionDefault", func(t *testing.T) {
|
||||||
|
assert.False(t, Resources.AllowAll(ResourceDefault, RoleVisitor, Permissions{FullAccess}))
|
||||||
|
})
|
||||||
|
t.Run("ResourcePhotosRoleVisitorActionDefault", func(t *testing.T) {
|
||||||
|
assert.False(t, Resources.AllowAll(ResourcePhotos, RoleVisitor, Permissions{FullAccess}))
|
||||||
|
})
|
||||||
|
t.Run("ResourceAlbumsRoleVisitorAccessShared", func(t *testing.T) {
|
||||||
|
assert.True(t, Resources.AllowAll(ResourceAlbums, RoleVisitor, Permissions{AccessShared}))
|
||||||
|
})
|
||||||
|
t.Run("ResourceAlbumsRoleVisitorActionDefault", func(t *testing.T) {
|
||||||
|
assert.False(t, Resources.AllowAll(ResourceAlbums, RoleVisitor, Permissions{FullAccess}))
|
||||||
|
})
|
||||||
|
t.Run("Empty", func(t *testing.T) {
|
||||||
|
assert.False(t, Resources.AllowAll(ResourceAlbums, RoleVisitor, Permissions{}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestACL_Deny(t *testing.T) {
|
||||||
|
t.Run("ResourceDefaultRoleAdminActionDefault", func(t *testing.T) {
|
||||||
|
assert.False(t, Resources.Deny(ResourceDefault, RoleAdmin, FullAccess))
|
||||||
|
})
|
||||||
|
t.Run("ResourceDefaultRoleVisitorActionDefault", func(t *testing.T) {
|
||||||
|
assert.True(t, Resources.Deny(ResourceDefault, RoleVisitor, FullAccess))
|
||||||
|
})
|
||||||
|
t.Run("ResourceAlbumsRoleVisitorActionAccessShared", func(t *testing.T) {
|
||||||
|
assert.False(t, Resources.Deny(ResourceAlbums, RoleVisitor, AccessShared))
|
||||||
|
})
|
||||||
|
t.Run("ResourcePhotosRoleVisitorActionDefault", func(t *testing.T) {
|
||||||
|
assert.True(t, Resources.Deny(ResourcePhotos, RoleVisitor, FullAccess))
|
||||||
|
})
|
||||||
|
t.Run("ResourceAlbumsRoleVisitorActionDefault", func(t *testing.T) {
|
||||||
|
assert.True(t, Resources.Deny(ResourceAlbums, RoleVisitor, FullAccess))
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package acl
|
|
||||||
|
|
||||||
type Action string
|
|
||||||
type Actions map[Action]bool
|
|
||||||
|
|
||||||
const (
|
|
||||||
ActionDefault Action = "*"
|
|
||||||
ActionSearch Action = "search"
|
|
||||||
ActionCreate Action = "create"
|
|
||||||
ActionRead Action = "read"
|
|
||||||
ActionUpdate Action = "update"
|
|
||||||
ActionUpdateSelf Action = "update-self"
|
|
||||||
ActionDelete Action = "delete"
|
|
||||||
ActionPrivate Action = "private"
|
|
||||||
ActionUpload Action = "upload"
|
|
||||||
ActionDownload Action = "download"
|
|
||||||
ActionShare Action = "share"
|
|
||||||
ActionLike Action = "like"
|
|
||||||
ActionComment Action = "comment"
|
|
||||||
ActionExport Action = "export"
|
|
||||||
ActionImport Action = "import"
|
|
||||||
)
|
|
||||||
22
internal/acl/channels.go
Normal file
22
internal/acl/channels.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package acl
|
||||||
|
|
||||||
|
const (
|
||||||
|
ChannelSession Resource = "session"
|
||||||
|
ChannelAudit Resource = "audit"
|
||||||
|
ChannelLog Resource = "log"
|
||||||
|
ChannelNotify Resource = "notify"
|
||||||
|
ChannelIndex Resource = "index"
|
||||||
|
ChannelUpload Resource = "upload"
|
||||||
|
ChannelImport Resource = "import"
|
||||||
|
ChannelConfig Resource = "config"
|
||||||
|
ChannelCount Resource = "count"
|
||||||
|
ChannelPhotos Resource = "photos"
|
||||||
|
ChannelCameras Resource = "cameras"
|
||||||
|
ChannelLenses Resource = "lenses"
|
||||||
|
ChannelCountries Resource = "countries"
|
||||||
|
ChannelAlbums Resource = "albums"
|
||||||
|
ChannelLabels Resource = "labels"
|
||||||
|
ChannelSubjects Resource = "subjects"
|
||||||
|
ChannelPeople Resource = "people"
|
||||||
|
ChannelSync Resource = "sync"
|
||||||
|
)
|
||||||
22
internal/acl/grant.go
Normal file
22
internal/acl/grant.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package acl
|
||||||
|
|
||||||
|
// Predefined grants to simplify configuration.
|
||||||
|
var (
|
||||||
|
GrantFullAccess = Grant{FullAccess: true, AccessAll: true, ActionCreate: true, ActionUpdate: true, ActionDelete: true, ActionDownload: true, ActionShare: true, ActionRate: true, ActionReact: true, ActionManage: true, ActionSubscribe: true}
|
||||||
|
GrantSubscribeAll = Grant{AccessAll: true, ActionSubscribe: true}
|
||||||
|
GrantSubscribeOwn = Grant{AccessOwn: true, ActionSubscribe: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Grant represents permissions granted or denied.
|
||||||
|
type Grant map[Permission]bool
|
||||||
|
|
||||||
|
// Allow checks whether the permission is granted.
|
||||||
|
func (grant Grant) Allow(perm Permission) bool {
|
||||||
|
if result, ok := grant[perm]; ok {
|
||||||
|
return result
|
||||||
|
} else if result, ok = grant[FullAccess]; ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
15
internal/acl/grants.go
Normal file
15
internal/acl/grants.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package acl
|
||||||
|
|
||||||
|
// Grants represents Permission Grant by Resource.
|
||||||
|
type Grants map[Resource]Grant
|
||||||
|
|
||||||
|
// Grants returns the permissions granted to the specified Role by Resource.
|
||||||
|
func (acl ACL) Grants(role Role) Grants {
|
||||||
|
result := make(map[Resource]Grant, len(acl))
|
||||||
|
|
||||||
|
for resource := range acl {
|
||||||
|
result[resource] = acl[resource][role]
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
20
internal/acl/grants_test.go
Normal file
20
internal/acl/grants_test.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package acl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestACL_Grants(t *testing.T) {
|
||||||
|
t.Run("RoleAdmin", func(t *testing.T) {
|
||||||
|
result := Resources.Grants(RoleAdmin)
|
||||||
|
assert.True(t, result[ResourcePhotos][ActionManage])
|
||||||
|
assert.True(t, result[ResourceConfig][ActionManage])
|
||||||
|
})
|
||||||
|
t.Run("RoleVisitor", func(t *testing.T) {
|
||||||
|
result := Resources.Grants(RoleVisitor)
|
||||||
|
assert.False(t, result[ResourcePhotos][ActionUpdate])
|
||||||
|
assert.False(t, result[ResourceConfig][ActionManage])
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
package acl
|
|
||||||
|
|
||||||
var Permissions = ACL{
|
|
||||||
ResourceDefault: Roles{
|
|
||||||
RoleAdmin: Actions{ActionDefault: true},
|
|
||||||
},
|
|
||||||
ResourceConfig: Roles{
|
|
||||||
RoleAdmin: Actions{ActionDefault: true},
|
|
||||||
RoleEditor: Actions{ActionRead: true},
|
|
||||||
RoleViewer: Actions{ActionRead: true},
|
|
||||||
RoleGuest: Actions{ActionRead: true},
|
|
||||||
},
|
|
||||||
ResourceConfigOptions: Roles{
|
|
||||||
RoleAdmin: Actions{ActionDefault: true},
|
|
||||||
RoleEditor: Actions{ActionRead: true},
|
|
||||||
},
|
|
||||||
ResourceSettings: Roles{
|
|
||||||
RoleAdmin: Actions{ActionDefault: true},
|
|
||||||
RoleEditor: Actions{ActionDefault: true},
|
|
||||||
},
|
|
||||||
ResourceLogs: Roles{
|
|
||||||
RoleAdmin: Actions{ActionDefault: true},
|
|
||||||
RoleEditor: Actions{ActionRead: true},
|
|
||||||
RoleViewer: Actions{ActionRead: true},
|
|
||||||
},
|
|
||||||
ResourceAccounts: Roles{
|
|
||||||
RoleAdmin: Actions{ActionDefault: true},
|
|
||||||
RoleEditor: Actions{ActionRead: true},
|
|
||||||
},
|
|
||||||
ResourceSubjects: Roles{
|
|
||||||
RoleAdmin: Actions{ActionDefault: true},
|
|
||||||
RoleEditor: Actions{ActionRead: true},
|
|
||||||
RoleViewer: Actions{ActionRead: true},
|
|
||||||
},
|
|
||||||
ResourceAlbums: Roles{
|
|
||||||
RoleAdmin: Actions{ActionDefault: true},
|
|
||||||
RoleEditor: Actions{ActionDefault: true},
|
|
||||||
RoleViewer: Actions{ActionSearch: true, ActionRead: true, ActionComment: true},
|
|
||||||
RoleGuest: Actions{ActionSearch: true, ActionRead: true},
|
|
||||||
},
|
|
||||||
ResourceCameras: Roles{
|
|
||||||
RoleAdmin: Actions{ActionDefault: true},
|
|
||||||
RoleEditor: Actions{ActionDefault: true},
|
|
||||||
RoleViewer: Actions{ActionSearch: true, ActionRead: true},
|
|
||||||
RoleGuest: Actions{ActionSearch: true, ActionRead: true},
|
|
||||||
},
|
|
||||||
ResourceCategories: Roles{
|
|
||||||
RoleAdmin: Actions{ActionDefault: true},
|
|
||||||
RoleEditor: Actions{ActionDefault: true},
|
|
||||||
RoleViewer: Actions{ActionSearch: true, ActionRead: true},
|
|
||||||
RoleGuest: Actions{ActionSearch: true, ActionRead: true},
|
|
||||||
},
|
|
||||||
ResourceCountries: Roles{
|
|
||||||
RoleAdmin: Actions{ActionDefault: true},
|
|
||||||
RoleEditor: Actions{ActionDefault: true},
|
|
||||||
RoleViewer: Actions{ActionSearch: true, ActionRead: true},
|
|
||||||
RoleGuest: Actions{ActionSearch: true, ActionRead: true},
|
|
||||||
},
|
|
||||||
ResourceFiles: Roles{
|
|
||||||
RoleAdmin: Actions{ActionDefault: true},
|
|
||||||
RoleEditor: Actions{ActionDefault: true},
|
|
||||||
RoleViewer: Actions{ActionSearch: true, ActionRead: true},
|
|
||||||
RoleGuest: Actions{ActionSearch: true, ActionRead: true},
|
|
||||||
},
|
|
||||||
ResourceFolders: Roles{
|
|
||||||
RoleAdmin: Actions{ActionDefault: true},
|
|
||||||
RoleEditor: Actions{ActionDefault: true},
|
|
||||||
RoleViewer: Actions{ActionSearch: true, ActionRead: true},
|
|
||||||
RoleGuest: Actions{ActionSearch: true, ActionRead: true},
|
|
||||||
},
|
|
||||||
ResourceLabels: Roles{
|
|
||||||
RoleAdmin: Actions{ActionDefault: true},
|
|
||||||
RoleEditor: Actions{ActionDefault: true},
|
|
||||||
RoleViewer: Actions{ActionSearch: true, ActionRead: true},
|
|
||||||
RoleGuest: Actions{ActionSearch: true, ActionRead: true},
|
|
||||||
},
|
|
||||||
ResourceLenses: Roles{
|
|
||||||
RoleAdmin: Actions{ActionDefault: true},
|
|
||||||
RoleEditor: Actions{ActionDefault: true},
|
|
||||||
RoleViewer: Actions{ActionSearch: true, ActionRead: true},
|
|
||||||
RoleGuest: Actions{ActionSearch: true, ActionRead: true},
|
|
||||||
},
|
|
||||||
ResourceLinks: Roles{
|
|
||||||
RoleAdmin: Actions{ActionDefault: true},
|
|
||||||
RoleEditor: Actions{ActionDefault: true},
|
|
||||||
},
|
|
||||||
ResourceGeo: Roles{
|
|
||||||
RoleAdmin: Actions{ActionDefault: true},
|
|
||||||
RoleEditor: Actions{ActionDefault: true},
|
|
||||||
RoleViewer: Actions{ActionSearch: true, ActionRead: true, ActionComment: true},
|
|
||||||
RoleGuest: Actions{ActionSearch: true, ActionRead: true},
|
|
||||||
},
|
|
||||||
ResourcePhotos: Roles{
|
|
||||||
RoleAdmin: Actions{ActionDefault: true},
|
|
||||||
RoleEditor: Actions{ActionDefault: true},
|
|
||||||
RoleViewer: Actions{ActionSearch: true, ActionRead: true, ActionDownload: true, ActionComment: true},
|
|
||||||
RoleGuest: Actions{ActionSearch: true, ActionRead: true, ActionDownload: true},
|
|
||||||
},
|
|
||||||
ResourcePrivate: Roles{
|
|
||||||
RoleAdmin: Actions{ActionDefault: true},
|
|
||||||
RoleEditor: Actions{ActionDefault: true},
|
|
||||||
},
|
|
||||||
ResourcePlaces: Roles{
|
|
||||||
RoleAdmin: Actions{ActionDefault: true},
|
|
||||||
RoleEditor: Actions{ActionDefault: true},
|
|
||||||
RoleViewer: Actions{ActionSearch: true, ActionRead: true, ActionDownload: true},
|
|
||||||
},
|
|
||||||
ResourceUsers: Roles{
|
|
||||||
RoleAdmin: Actions{ActionDefault: true},
|
|
||||||
RoleDefault: Actions{ActionUpdateSelf: true},
|
|
||||||
},
|
|
||||||
ResourcePasswords: Roles{
|
|
||||||
RoleAdmin: Actions{ActionDefault: true},
|
|
||||||
RoleEditor: Actions{ActionUpdateSelf: true},
|
|
||||||
RoleViewer: Actions{ActionUpdateSelf: true},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package acl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestACL_Allow(t *testing.T) {
|
|
||||||
t.Run("photos/admin/update", func(t *testing.T) {
|
|
||||||
assert.True(t, Permissions.Allow(ResourcePhotos, RoleAdmin, ActionUpdate))
|
|
||||||
})
|
|
||||||
t.Run("default/admin", func(t *testing.T) {
|
|
||||||
assert.True(t, Permissions.Allow(ResourceDefault, RoleAdmin, ActionDefault))
|
|
||||||
})
|
|
||||||
t.Run("default/guest", func(t *testing.T) {
|
|
||||||
assert.False(t, Permissions.Allow(ResourceDefault, RoleGuest, ActionDefault))
|
|
||||||
})
|
|
||||||
t.Run("photos/guest/search", func(t *testing.T) {
|
|
||||||
assert.True(t, Permissions.Allow(ResourcePhotos, RoleGuest, ActionSearch))
|
|
||||||
})
|
|
||||||
t.Run("photos/guest/default", func(t *testing.T) {
|
|
||||||
assert.False(t, Permissions.Allow(ResourcePhotos, RoleGuest, ActionDefault))
|
|
||||||
})
|
|
||||||
t.Run("albums/guest/search", func(t *testing.T) {
|
|
||||||
assert.True(t, Permissions.Allow(ResourceAlbums, RoleGuest, ActionSearch))
|
|
||||||
})
|
|
||||||
t.Run("albums/guest/default", func(t *testing.T) {
|
|
||||||
assert.False(t, Permissions.Allow(ResourceAlbums, RoleGuest, ActionDefault))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestACL_Deny(t *testing.T) {
|
|
||||||
t.Run("default/admin", func(t *testing.T) {
|
|
||||||
assert.False(t, Permissions.Deny(ResourceDefault, RoleAdmin, ActionDefault))
|
|
||||||
})
|
|
||||||
t.Run("default/guest", func(t *testing.T) {
|
|
||||||
assert.True(t, Permissions.Deny(ResourceDefault, RoleGuest, ActionDefault))
|
|
||||||
})
|
|
||||||
t.Run("photos/guest/search", func(t *testing.T) {
|
|
||||||
assert.False(t, Permissions.Deny(ResourcePhotos, RoleGuest, ActionSearch))
|
|
||||||
})
|
|
||||||
t.Run("photos/guest/default", func(t *testing.T) {
|
|
||||||
assert.True(t, Permissions.Deny(ResourcePhotos, RoleGuest, ActionDefault))
|
|
||||||
})
|
|
||||||
t.Run("albums/guest/search", func(t *testing.T) {
|
|
||||||
assert.False(t, Permissions.Deny(ResourceAlbums, RoleGuest, ActionSearch))
|
|
||||||
})
|
|
||||||
t.Run("albums/guest/default", func(t *testing.T) {
|
|
||||||
assert.True(t, Permissions.Deny(ResourceAlbums, RoleGuest, ActionDefault))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
62
internal/acl/perms.go
Normal file
62
internal/acl/perms.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package acl
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// Permissions that can be granted to roles.
|
||||||
|
const (
|
||||||
|
FullAccess Permission = "full_access"
|
||||||
|
AccessShared Permission = "access_shared"
|
||||||
|
AccessLibrary Permission = "access_library"
|
||||||
|
AccessPrivate Permission = "access_private"
|
||||||
|
AccessOwn Permission = "access_own"
|
||||||
|
AccessAll Permission = "access_all"
|
||||||
|
ActionSearch Permission = "search"
|
||||||
|
ActionView Permission = "view"
|
||||||
|
ActionUpload Permission = "upload"
|
||||||
|
ActionCreate Permission = "create"
|
||||||
|
ActionUpdate Permission = "update"
|
||||||
|
ActionDownload Permission = "download"
|
||||||
|
ActionShare Permission = "share"
|
||||||
|
ActionDelete Permission = "delete"
|
||||||
|
ActionRate Permission = "rate"
|
||||||
|
ActionReact Permission = "react"
|
||||||
|
ActionManage Permission = "manage"
|
||||||
|
ActionSubscribe Permission = "subscribe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Permission represents a single ability.
|
||||||
|
type Permission string
|
||||||
|
|
||||||
|
// String returns the type as string.
|
||||||
|
func (p Permission) String() string {
|
||||||
|
return strings.ReplaceAll(string(p), "_", " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogId returns an identifier string for use in log messages.
|
||||||
|
func (p Permission) LogId() string {
|
||||||
|
return p.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal checks if the type matches.
|
||||||
|
func (p Permission) Equal(s string) bool {
|
||||||
|
return strings.EqualFold(s, p.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotEqual checks if the type is different.
|
||||||
|
func (p Permission) NotEqual(s string) bool {
|
||||||
|
return !p.Equal(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permissions is a list of permissions.
|
||||||
|
type Permissions []Permission
|
||||||
|
|
||||||
|
// String returns the permissions as a comma-separated string.
|
||||||
|
func (perm Permissions) String() string {
|
||||||
|
s := make([]string, len(perm))
|
||||||
|
|
||||||
|
for i := range perm {
|
||||||
|
s[i] = perm[i].String()
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(s, ", ")
|
||||||
|
}
|
||||||
22
internal/acl/perms_test.go
Normal file
22
internal/acl/perms_test.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package acl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPermissions_String(t *testing.T) {
|
||||||
|
t.Run("Empty", func(t *testing.T) {
|
||||||
|
perms := Permissions{}
|
||||||
|
assert.Equal(t, "", perms.String())
|
||||||
|
})
|
||||||
|
t.Run("FullAccess", func(t *testing.T) {
|
||||||
|
perms := Permissions{FullAccess}
|
||||||
|
assert.Equal(t, "full access", perms.String())
|
||||||
|
})
|
||||||
|
t.Run("ManageUploadAll", func(t *testing.T) {
|
||||||
|
perms := Permissions{ActionManage, ActionUpload, AccessAll}
|
||||||
|
assert.Equal(t, "manage, upload, access all", perms.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
54
internal/acl/resource.go
Normal file
54
internal/acl/resource.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package acl
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// Resources that Roles can be granted Permission.
|
||||||
|
const (
|
||||||
|
ResourceDefault Resource = "default"
|
||||||
|
ResourcePhotos Resource = "photos"
|
||||||
|
ResourceFavorites Resource = "favorites"
|
||||||
|
ResourceAlbums Resource = "albums"
|
||||||
|
ResourcePeople Resource = "people"
|
||||||
|
ResourceMoments Resource = "moments"
|
||||||
|
ResourceCalendar Resource = "calendar"
|
||||||
|
ResourcePlaces Resource = "places"
|
||||||
|
ResourceLabels Resource = "labels"
|
||||||
|
ResourceLogs Resource = "logs"
|
||||||
|
ResourceConfig Resource = "config"
|
||||||
|
ResourceSettings Resource = "settings"
|
||||||
|
ResourcePassword Resource = "password"
|
||||||
|
ResourceUsers Resource = "users"
|
||||||
|
ResourceAccounts Resource = "accounts"
|
||||||
|
ResourceFiles Resource = "files"
|
||||||
|
ResourceFolders Resource = "folders"
|
||||||
|
ResourceShares Resource = "shares"
|
||||||
|
ResourceVideos Resource = "videos"
|
||||||
|
ResourceFeedback Resource = "feedback"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Resource represents a resource for which roles can be granted Permission.
|
||||||
|
type Resource string
|
||||||
|
|
||||||
|
// String returns the type as string.
|
||||||
|
func (r Resource) String() string {
|
||||||
|
if r == "" {
|
||||||
|
return "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogId returns an identifier string for use in log messages.
|
||||||
|
func (r Resource) LogId() string {
|
||||||
|
return r.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal checks if the type matches.
|
||||||
|
func (r Resource) Equal(s string) bool {
|
||||||
|
return strings.EqualFold(s, r.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotEqual checks if the type is different.
|
||||||
|
func (r Resource) NotEqual(s string) bool {
|
||||||
|
return !r.Equal(s)
|
||||||
|
}
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package acl
|
|
||||||
|
|
||||||
type Resource string
|
|
||||||
|
|
||||||
const (
|
|
||||||
ResourceDefault Resource = "*"
|
|
||||||
ResourceConfig Resource = "config"
|
|
||||||
ResourceConfigOptions Resource = "config_options"
|
|
||||||
ResourceSettings Resource = "settings"
|
|
||||||
ResourceLogs Resource = "logs"
|
|
||||||
ResourceAccounts Resource = "accounts"
|
|
||||||
ResourceSubjects Resource = "subjects"
|
|
||||||
ResourceAlbums Resource = "albums"
|
|
||||||
ResourceCameras Resource = "cameras"
|
|
||||||
ResourceCategories Resource = "categories"
|
|
||||||
ResourceCountries Resource = "countries"
|
|
||||||
ResourceFiles Resource = "files"
|
|
||||||
ResourceFolders Resource = "folders"
|
|
||||||
ResourceLabels Resource = "labels"
|
|
||||||
ResourceLenses Resource = "lenses"
|
|
||||||
ResourceLinks Resource = "links"
|
|
||||||
ResourceGeo Resource = "geo"
|
|
||||||
ResourcePasswords Resource = "passwords"
|
|
||||||
ResourceUsers Resource = "users"
|
|
||||||
ResourcePhotos Resource = "photos"
|
|
||||||
ResourcePrivate Resource = "private"
|
|
||||||
ResourcePlaces Resource = "places"
|
|
||||||
ResourceFeedback Resource = "feedback"
|
|
||||||
)
|
|
||||||
42
internal/acl/role.go
Normal file
42
internal/acl/role.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package acl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Role represents a user role.
|
||||||
|
type Role string
|
||||||
|
|
||||||
|
// String returns the type as string.
|
||||||
|
func (r Role) String() string {
|
||||||
|
if r == "" {
|
||||||
|
return "unauthorized"
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogId returns an identifier string for use in log messages.
|
||||||
|
func (r Role) LogId() string {
|
||||||
|
return "role " + r.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal checks if the type matches.
|
||||||
|
func (r Role) Equal(s string) bool {
|
||||||
|
return strings.EqualFold(s, r.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotEqual checks if the type is different.
|
||||||
|
func (r Role) NotEqual(s string) bool {
|
||||||
|
return !r.Equal(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid checks if the role is valid.
|
||||||
|
func (r Role) Valid(s string) bool {
|
||||||
|
return ValidRoles[s] != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalid checks if the role is invalid.
|
||||||
|
func (r Role) Invalid(s string) bool {
|
||||||
|
return !r.Valid(s)
|
||||||
|
}
|
||||||
@@ -1,33 +1,31 @@
|
|||||||
package acl
|
package acl
|
||||||
|
|
||||||
import (
|
// Roles that can be assigned to users.
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/pkg/clean"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Role string
|
|
||||||
type Roles map[Role]Actions
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
RoleAdmin Role = "admin"
|
RoleAdmin Role = "admin"
|
||||||
RoleEditor Role = "editor"
|
RoleVisitor Role = "visitor"
|
||||||
RoleViewer Role = "viewer"
|
RoleUnauthorized Role = "unauthorized"
|
||||||
RoleGuest Role = "guest"
|
RoleDefault Role = "default"
|
||||||
RoleDefault Role = "*"
|
RoleUnknown Role = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
// String returns the type as string.
|
// ValidRoles specifies the valid user roles.
|
||||||
func (t Role) String() string {
|
var ValidRoles = map[string]Role{
|
||||||
return clean.Role(string(t))
|
string(RoleAdmin): RoleAdmin,
|
||||||
|
string(RoleVisitor): RoleVisitor,
|
||||||
|
string(RoleUnauthorized): RoleUnauthorized,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Equal checks if the type matches.
|
// Roles grants permissions to roles.
|
||||||
func (t Role) Equal(s string) bool {
|
type Roles map[Role]Grant
|
||||||
return strings.EqualFold(s, t.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// NotEqual checks if the type is different.
|
// Allow checks whether the permission is granted based on the role.
|
||||||
func (t Role) NotEqual(s string) bool {
|
func (roles Roles) Allow(role Role, grant Permission) bool {
|
||||||
return !t.Equal(s)
|
if a, ok := roles[role]; ok {
|
||||||
|
return a.Allow(grant)
|
||||||
|
} else if a, ok = roles[RoleDefault]; ok {
|
||||||
|
return a.Allow(grant)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
79
internal/api/abort.go
Normal file
79
internal/api/abort.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/i18n"
|
||||||
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Abort(c *gin.Context, code int, id i18n.Message, params ...interface{}) {
|
||||||
|
resp := i18n.NewResponse(code, id, params...)
|
||||||
|
|
||||||
|
log.Debugf("api-v1: abort %s with code %d (%s)", clean.Log(c.FullPath()), code, strings.ToLower(resp.String()))
|
||||||
|
|
||||||
|
c.AbortWithStatusJSON(code, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Error(c *gin.Context, code int, err error, id i18n.Message, params ...interface{}) {
|
||||||
|
resp := i18n.NewResponse(code, id, params...)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
resp.Details = err.Error()
|
||||||
|
log.Errorf("api-v1: error %s with code %d in %s (%s)", clean.Log(err.Error()), code, clean.Log(c.FullPath()), strings.ToLower(resp.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
c.AbortWithStatusJSON(code, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AbortUnauthorized aborts with status code 401.
|
||||||
|
func AbortUnauthorized(c *gin.Context) {
|
||||||
|
Abort(c, http.StatusUnauthorized, i18n.ErrUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AbortForbidden aborts with status code 403.
|
||||||
|
func AbortForbidden(c *gin.Context) {
|
||||||
|
Abort(c, http.StatusForbidden, i18n.ErrForbidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AbortNotFound aborts with status code 404.
|
||||||
|
func AbortNotFound(c *gin.Context) {
|
||||||
|
Abort(c, http.StatusNotFound, i18n.ErrNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AbortEntityNotFound aborts with status code 404.
|
||||||
|
func AbortEntityNotFound(c *gin.Context) {
|
||||||
|
Abort(c, http.StatusNotFound, i18n.ErrEntityNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AbortAlbumNotFound aborts with status code 404.
|
||||||
|
func AbortAlbumNotFound(c *gin.Context) {
|
||||||
|
Abort(c, http.StatusNotFound, i18n.ErrAlbumNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AbortSaveFailed(c *gin.Context) {
|
||||||
|
Abort(c, http.StatusInternalServerError, i18n.ErrSaveFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AbortDeleteFailed(c *gin.Context) {
|
||||||
|
Abort(c, http.StatusInternalServerError, i18n.ErrDeleteFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AbortUnexpected(c *gin.Context) {
|
||||||
|
Abort(c, http.StatusInternalServerError, i18n.ErrUnexpected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AbortBadRequest(c *gin.Context) {
|
||||||
|
Abort(c, http.StatusBadRequest, i18n.ErrBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AbortFeatureDisabled(c *gin.Context) {
|
||||||
|
Abort(c, http.StatusForbidden, i18n.ErrFeatureDisabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AbortBusy(c *gin.Context) {
|
||||||
|
Abort(c, http.StatusTooManyRequests, i18n.ErrBusy)
|
||||||
|
}
|
||||||
@@ -35,17 +35,16 @@ const (
|
|||||||
// id: string Account ID as returned by the API
|
// id: string Account ID as returned by the API
|
||||||
func GetAccount(router *gin.RouterGroup) {
|
func GetAccount(router *gin.RouterGroup) {
|
||||||
router.GET("/accounts/:id", func(c *gin.Context) {
|
router.GET("/accounts/:id", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourceAccounts, acl.ActionRead)
|
s := Auth(c, acl.ResourceAccounts, acl.ActionView)
|
||||||
|
|
||||||
if s.Invalid() {
|
if s.Abort(c) {
|
||||||
AbortUnauthorized(c)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
conf := service.Config()
|
conf := service.Config()
|
||||||
|
|
||||||
if conf.Demo() || conf.DisableSettings() {
|
if conf.Demo() || conf.DisableSettings() {
|
||||||
AbortUnauthorized(c)
|
AbortForbidden(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,17 +67,16 @@ func GetAccount(router *gin.RouterGroup) {
|
|||||||
// id: string Account ID as returned by the API
|
// id: string Account ID as returned by the API
|
||||||
func GetAccountFolders(router *gin.RouterGroup) {
|
func GetAccountFolders(router *gin.RouterGroup) {
|
||||||
router.GET("/accounts/:id/folders", func(c *gin.Context) {
|
router.GET("/accounts/:id/folders", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourceAccounts, acl.ActionRead)
|
s := Auth(c, acl.ResourceAccounts, acl.ActionView)
|
||||||
|
|
||||||
if s.Invalid() {
|
if s.Abort(c) {
|
||||||
AbortUnauthorized(c)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
conf := service.Config()
|
conf := service.Config()
|
||||||
|
|
||||||
if conf.Demo() || conf.DisableSettings() {
|
if conf.Demo() || conf.DisableSettings() {
|
||||||
AbortUnauthorized(c)
|
AbortForbidden(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,10 +125,9 @@ func GetAccountFolders(router *gin.RouterGroup) {
|
|||||||
// id: string Account ID as returned by the API
|
// id: string Account ID as returned by the API
|
||||||
func ShareWithAccount(router *gin.RouterGroup) {
|
func ShareWithAccount(router *gin.RouterGroup) {
|
||||||
router.POST("/accounts/:id/share", func(c *gin.Context) {
|
router.POST("/accounts/:id/share", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourceAccounts, acl.ActionUpload)
|
s := Auth(c, acl.ResourceAccounts, acl.ActionUpload)
|
||||||
|
|
||||||
if s.Invalid() {
|
if s.Abort(c) {
|
||||||
AbortUnauthorized(c)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,17 +184,16 @@ func ShareWithAccount(router *gin.RouterGroup) {
|
|||||||
// POST /api/v1/accounts
|
// POST /api/v1/accounts
|
||||||
func CreateAccount(router *gin.RouterGroup) {
|
func CreateAccount(router *gin.RouterGroup) {
|
||||||
router.POST("/accounts", func(c *gin.Context) {
|
router.POST("/accounts", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourceAccounts, acl.ActionCreate)
|
s := Auth(c, acl.ResourceAccounts, acl.ActionCreate)
|
||||||
|
|
||||||
if s.Invalid() {
|
if s.Abort(c) {
|
||||||
AbortUnauthorized(c)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
conf := service.Config()
|
conf := service.Config()
|
||||||
|
|
||||||
if conf.Demo() || conf.DisableSettings() {
|
if conf.Demo() || conf.DisableSettings() {
|
||||||
AbortUnauthorized(c)
|
AbortForbidden(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,17 +233,16 @@ func CreateAccount(router *gin.RouterGroup) {
|
|||||||
// id: string Account ID as returned by the API
|
// id: string Account ID as returned by the API
|
||||||
func UpdateAccount(router *gin.RouterGroup) {
|
func UpdateAccount(router *gin.RouterGroup) {
|
||||||
router.PUT("/accounts/:id", func(c *gin.Context) {
|
router.PUT("/accounts/:id", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourceAccounts, acl.ActionUpdate)
|
s := Auth(c, acl.ResourceAccounts, acl.ActionUpdate)
|
||||||
|
|
||||||
if s.Invalid() {
|
if s.Abort(c) {
|
||||||
AbortUnauthorized(c)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
conf := service.Config()
|
conf := service.Config()
|
||||||
|
|
||||||
if conf.Demo() || conf.DisableSettings() {
|
if conf.Demo() || conf.DisableSettings() {
|
||||||
AbortUnauthorized(c)
|
AbortForbidden(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,17 +304,16 @@ func UpdateAccount(router *gin.RouterGroup) {
|
|||||||
// id: string Account ID as returned by the API
|
// id: string Account ID as returned by the API
|
||||||
func DeleteAccount(router *gin.RouterGroup) {
|
func DeleteAccount(router *gin.RouterGroup) {
|
||||||
router.DELETE("/accounts/:id", func(c *gin.Context) {
|
router.DELETE("/accounts/:id", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourceAccounts, acl.ActionDelete)
|
s := Auth(c, acl.ResourceAccounts, acl.ActionDelete)
|
||||||
|
|
||||||
if s.Invalid() {
|
if s.Abort(c) {
|
||||||
AbortUnauthorized(c)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
conf := service.Config()
|
conf := service.Config()
|
||||||
|
|
||||||
if conf.Demo() || conf.DisableSettings() {
|
if conf.Demo() || conf.DisableSettings() {
|
||||||
AbortUnauthorized(c)
|
AbortForbidden(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,10 +18,9 @@ import (
|
|||||||
// GET /api/v1/accounts
|
// GET /api/v1/accounts
|
||||||
func SearchAccounts(router *gin.RouterGroup) {
|
func SearchAccounts(router *gin.RouterGroup) {
|
||||||
router.GET("/accounts", func(c *gin.Context) {
|
router.GET("/accounts", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourceAccounts, acl.ActionSearch)
|
s := Auth(c, acl.ResourceAccounts, acl.ActionSearch)
|
||||||
|
|
||||||
if s.Invalid() {
|
if s.Abort(c) {
|
||||||
AbortUnauthorized(c)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,14 +44,13 @@ func SaveAlbumAsYaml(a entity.Album) {
|
|||||||
// GET /api/v1/albums/:uid
|
// GET /api/v1/albums/:uid
|
||||||
func GetAlbum(router *gin.RouterGroup) {
|
func GetAlbum(router *gin.RouterGroup) {
|
||||||
router.GET("/albums/:uid", func(c *gin.Context) {
|
router.GET("/albums/:uid", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionRead)
|
s := Auth(c, acl.ResourceAlbums, acl.ActionView)
|
||||||
|
|
||||||
if s.Invalid() {
|
if s.Abort(c) {
|
||||||
AbortUnauthorized(c)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
id := clean.IdString(c.Param("uid"))
|
id := clean.UID(c.Param("uid"))
|
||||||
a, err := query.AlbumByUID(id)
|
a, err := query.AlbumByUID(id)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -68,10 +67,9 @@ func GetAlbum(router *gin.RouterGroup) {
|
|||||||
// POST /api/v1/albums
|
// POST /api/v1/albums
|
||||||
func CreateAlbum(router *gin.RouterGroup) {
|
func CreateAlbum(router *gin.RouterGroup) {
|
||||||
router.POST("/albums", func(c *gin.Context) {
|
router.POST("/albums", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionCreate)
|
s := Auth(c, acl.ResourceAlbums, acl.ActionCreate)
|
||||||
|
|
||||||
if s.Invalid() {
|
if s.Abort(c) {
|
||||||
AbortUnauthorized(c)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,12 +87,9 @@ func CreateAlbum(router *gin.RouterGroup) {
|
|||||||
a.AlbumFavorite = f.AlbumFavorite
|
a.AlbumFavorite = f.AlbumFavorite
|
||||||
|
|
||||||
// Existing album?
|
// Existing album?
|
||||||
if err := a.Find(); err != nil {
|
if found := a.Find(); found == nil {
|
||||||
// Not found, create new album.
|
// Not found, create new album.
|
||||||
err = a.Create()
|
if err := a.Create(); err != nil {
|
||||||
|
|
||||||
// Should never happen.
|
|
||||||
if err != nil {
|
|
||||||
// Report unexpected error.
|
// Report unexpected error.
|
||||||
log.Errorf("album: %s (create)", err)
|
log.Errorf("album: %s (create)", err)
|
||||||
AbortUnexpected(c)
|
AbortUnexpected(c)
|
||||||
@@ -104,11 +99,12 @@ func CreateAlbum(router *gin.RouterGroup) {
|
|||||||
event.SuccessMsg(i18n.MsgAlbumCreated)
|
event.SuccessMsg(i18n.MsgAlbumCreated)
|
||||||
} else {
|
} else {
|
||||||
// Exists, restore if necessary.
|
// Exists, restore if necessary.
|
||||||
|
a = found
|
||||||
if !a.Deleted() {
|
if !a.Deleted() {
|
||||||
event.InfoMsg(i18n.ErrAlreadyExists, a.Title())
|
event.InfoMsg(i18n.ErrAlreadyExists, a.Title())
|
||||||
c.JSON(http.StatusOK, a)
|
c.JSON(http.StatusOK, a)
|
||||||
return
|
return
|
||||||
} else if err = a.Restore(); err == nil {
|
} else if err := a.Restore(); err == nil {
|
||||||
event.SuccessMsg(i18n.MsgRestored, a.Title())
|
event.SuccessMsg(i18n.MsgRestored, a.Title())
|
||||||
} else {
|
} else {
|
||||||
// Report unexpected error.
|
// Report unexpected error.
|
||||||
@@ -133,14 +129,13 @@ func CreateAlbum(router *gin.RouterGroup) {
|
|||||||
// PUT /api/v1/albums/:uid
|
// PUT /api/v1/albums/:uid
|
||||||
func UpdateAlbum(router *gin.RouterGroup) {
|
func UpdateAlbum(router *gin.RouterGroup) {
|
||||||
router.PUT("/albums/:uid", func(c *gin.Context) {
|
router.PUT("/albums/:uid", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionUpdate)
|
s := Auth(c, acl.ResourceAlbums, acl.ActionUpdate)
|
||||||
|
|
||||||
if s.Invalid() {
|
if s.Abort(c) {
|
||||||
AbortUnauthorized(c)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
uid := clean.IdString(c.Param("uid"))
|
uid := clean.UID(c.Param("uid"))
|
||||||
a, err := query.AlbumByUID(uid)
|
a, err := query.AlbumByUID(uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -188,14 +183,13 @@ func UpdateAlbum(router *gin.RouterGroup) {
|
|||||||
// DELETE /api/v1/albums/:uid
|
// DELETE /api/v1/albums/:uid
|
||||||
func DeleteAlbum(router *gin.RouterGroup) {
|
func DeleteAlbum(router *gin.RouterGroup) {
|
||||||
router.DELETE("/albums/:uid", func(c *gin.Context) {
|
router.DELETE("/albums/:uid", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionDelete)
|
s := Auth(c, acl.ResourceAlbums, acl.ActionDelete)
|
||||||
|
|
||||||
if s.Invalid() {
|
if s.Abort(c) {
|
||||||
AbortUnauthorized(c)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
id := clean.IdString(c.Param("uid"))
|
id := clean.UID(c.Param("uid"))
|
||||||
|
|
||||||
a, err := query.AlbumByUID(id)
|
a, err := query.AlbumByUID(id)
|
||||||
|
|
||||||
@@ -243,14 +237,13 @@ func DeleteAlbum(router *gin.RouterGroup) {
|
|||||||
// uid: string Album UID
|
// uid: string Album UID
|
||||||
func LikeAlbum(router *gin.RouterGroup) {
|
func LikeAlbum(router *gin.RouterGroup) {
|
||||||
router.POST("/albums/:uid/like", func(c *gin.Context) {
|
router.POST("/albums/:uid/like", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionLike)
|
s := Auth(c, acl.ResourceAlbums, acl.ActionUpdate)
|
||||||
|
|
||||||
if s.Invalid() {
|
if s.Abort(c) {
|
||||||
AbortUnauthorized(c)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
id := clean.IdString(c.Param("uid"))
|
id := clean.UID(c.Param("uid"))
|
||||||
a, err := query.AlbumByUID(id)
|
a, err := query.AlbumByUID(id)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -282,14 +275,13 @@ func LikeAlbum(router *gin.RouterGroup) {
|
|||||||
// uid: string Album UID
|
// uid: string Album UID
|
||||||
func DislikeAlbum(router *gin.RouterGroup) {
|
func DislikeAlbum(router *gin.RouterGroup) {
|
||||||
router.DELETE("/albums/:uid/like", func(c *gin.Context) {
|
router.DELETE("/albums/:uid/like", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionLike)
|
s := Auth(c, acl.ResourceAlbums, acl.ActionUpdate)
|
||||||
|
|
||||||
if s.Invalid() {
|
if s.Abort(c) {
|
||||||
AbortUnauthorized(c)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
id := clean.IdString(c.Param("uid"))
|
id := clean.UID(c.Param("uid"))
|
||||||
a, err := query.AlbumByUID(id)
|
a, err := query.AlbumByUID(id)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -317,14 +309,13 @@ func DislikeAlbum(router *gin.RouterGroup) {
|
|||||||
// POST /api/v1/albums/:uid/clone
|
// POST /api/v1/albums/:uid/clone
|
||||||
func CloneAlbums(router *gin.RouterGroup) {
|
func CloneAlbums(router *gin.RouterGroup) {
|
||||||
router.POST("/albums/:uid/clone", func(c *gin.Context) {
|
router.POST("/albums/:uid/clone", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionUpdate)
|
s := Auth(c, acl.ResourceAlbums, acl.ActionCreate)
|
||||||
|
|
||||||
if s.Invalid() {
|
if s.Abort(c) {
|
||||||
AbortUnauthorized(c)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
a, err := query.AlbumByUID(clean.IdString(c.Param("uid")))
|
a, err := query.AlbumByUID(clean.UID(c.Param("uid")))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
AbortAlbumNotFound(c)
|
AbortAlbumNotFound(c)
|
||||||
@@ -375,10 +366,9 @@ func CloneAlbums(router *gin.RouterGroup) {
|
|||||||
// POST /api/v1/albums/:uid/photos
|
// POST /api/v1/albums/:uid/photos
|
||||||
func AddPhotosToAlbum(router *gin.RouterGroup) {
|
func AddPhotosToAlbum(router *gin.RouterGroup) {
|
||||||
router.POST("/albums/:uid/photos", func(c *gin.Context) {
|
router.POST("/albums/:uid/photos", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionUpdate)
|
s := Auth(c, acl.ResourceAlbums, acl.ActionUpdate)
|
||||||
|
|
||||||
if s.Invalid() {
|
if s.Abort(c) {
|
||||||
AbortUnauthorized(c)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,7 +379,7 @@ func AddPhotosToAlbum(router *gin.RouterGroup) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
uid := clean.IdString(c.Param("uid"))
|
uid := clean.UID(c.Param("uid"))
|
||||||
a, err := query.AlbumByUID(uid)
|
a, err := query.AlbumByUID(uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -431,10 +421,9 @@ func AddPhotosToAlbum(router *gin.RouterGroup) {
|
|||||||
// DELETE /api/v1/albums/:uid/photos
|
// DELETE /api/v1/albums/:uid/photos
|
||||||
func RemovePhotosFromAlbum(router *gin.RouterGroup) {
|
func RemovePhotosFromAlbum(router *gin.RouterGroup) {
|
||||||
router.DELETE("/albums/:uid/photos", func(c *gin.Context) {
|
router.DELETE("/albums/:uid/photos", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionUpdate)
|
s := Auth(c, acl.ResourceAlbums, acl.ActionUpdate)
|
||||||
|
|
||||||
if s.Invalid() {
|
if s.Abort(c) {
|
||||||
AbortUnauthorized(c)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,7 +439,7 @@ func RemovePhotosFromAlbum(router *gin.RouterGroup) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
a, err := query.AlbumByUID(clean.IdString(c.Param("uid")))
|
a, err := query.AlbumByUID(clean.UID(c.Param("uid")))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
AbortAlbumNotFound(c)
|
AbortAlbumNotFound(c)
|
||||||
@@ -463,7 +452,7 @@ func RemovePhotosFromAlbum(router *gin.RouterGroup) {
|
|||||||
if len(removed) == 1 {
|
if len(removed) == 1 {
|
||||||
event.SuccessMsg(i18n.MsgEntryRemovedFrom, clean.Log(a.Title()))
|
event.SuccessMsg(i18n.MsgEntryRemovedFrom, clean.Log(a.Title()))
|
||||||
} else {
|
} else {
|
||||||
event.SuccessMsg(i18n.MsgEntriesRemovedFrom, len(removed), clean.Log(clean.Log(a.Title())))
|
event.SuccessMsg(i18n.MsgEntriesRemovedFrom, len(removed), clean.Log(a.Title()))
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoveFromAlbumCoverCache(a.AlbumUID)
|
RemoveFromAlbumCoverCache(a.AlbumUID)
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin/binding"
|
"github.com/gin-gonic/gin/binding"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/acl"
|
"github.com/photoprism/photoprism/internal/acl"
|
||||||
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
"github.com/photoprism/photoprism/internal/form"
|
"github.com/photoprism/photoprism/internal/form"
|
||||||
"github.com/photoprism/photoprism/internal/search"
|
"github.com/photoprism/photoprism/internal/search"
|
||||||
"github.com/photoprism/photoprism/internal/service"
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
@@ -18,10 +19,10 @@ import (
|
|||||||
// GET /api/v1/albums
|
// GET /api/v1/albums
|
||||||
func SearchAlbums(router *gin.RouterGroup) {
|
func SearchAlbums(router *gin.RouterGroup) {
|
||||||
router.GET("/albums", func(c *gin.Context) {
|
router.GET("/albums", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionSearch)
|
s := AuthAny(c, acl.ResourceAlbums, acl.Permissions{acl.ActionSearch, acl.ActionView, acl.AccessShared})
|
||||||
|
|
||||||
if s.Invalid() {
|
// Abort if permission was not granted.
|
||||||
AbortUnauthorized(c)
|
if s.Abort(c) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,24 +30,32 @@ func SearchAlbums(router *gin.RouterGroup) {
|
|||||||
|
|
||||||
err := c.MustBindWith(&f, binding.Form)
|
err := c.MustBindWith(&f, binding.Form)
|
||||||
|
|
||||||
|
// Abort if request params are invalid.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
event.AuditWarn([]string{ClientIP(c), "session %s", "albums", "search", "form invalid", "%s"}, s.RefID, err)
|
||||||
AbortBadRequest(c)
|
AbortBadRequest(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
conf := service.Config()
|
conf := service.Config()
|
||||||
|
|
||||||
// Guest permissions are limited to shared albums.
|
// Sharing link visitors permissions are limited to shared albums.
|
||||||
if s.Guest() {
|
if s.IsVisitor() {
|
||||||
f.UID = s.Shares.Join(txt.Or)
|
f.UID = s.SharedUIDs().Join(txt.Or)
|
||||||
f.Public = true
|
f.Public = true
|
||||||
|
event.AuditDebug([]string{ClientIP(c), "session %s", "albums", "search", "shared", "%s"}, s.RefID, f.UID)
|
||||||
|
} else if conf.Settings().Features.Private {
|
||||||
|
f.Public = true
|
||||||
|
event.AuditDebug([]string{ClientIP(c), "session %s", "albums", "search", "all public"}, s.RefID)
|
||||||
} else {
|
} else {
|
||||||
f.Public = conf.Settings().Features.Private
|
f.Public = false
|
||||||
|
event.AuditDebug([]string{ClientIP(c), "session %s", "albums", "search", "all public and private"}, s.RefID)
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := search.Albums(f)
|
result, err := search.Albums(f)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
event.AuditWarn([]string{ClientIP(c), "session %s", "albums", "search", "%s"}, s.RefID, err)
|
||||||
c.AbortWithStatusJSON(400, gin.H{"error": txt.UpperFirst(err.Error())})
|
c.AbortWithStatusJSON(400, gin.H{"error": txt.UpperFirst(err.Error())})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Package api provides REST API request handlers.
|
Package api provides REST API authentication and request handlers.
|
||||||
|
|
||||||
Copyright (c) 2018 - 2022 PhotoPrism UG. All rights reserved.
|
Copyright (c) 2018 - 2022 PhotoPrism UG. All rights reserved.
|
||||||
|
|
||||||
@@ -23,94 +23,3 @@ Additional information can be found in our Developer Guide:
|
|||||||
<https://docs.photoprism.app/developer-guide/>
|
<https://docs.photoprism.app/developer-guide/>
|
||||||
*/
|
*/
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/event"
|
|
||||||
"github.com/photoprism/photoprism/internal/i18n"
|
|
||||||
"github.com/photoprism/photoprism/internal/service"
|
|
||||||
"github.com/photoprism/photoprism/pkg/clean"
|
|
||||||
)
|
|
||||||
|
|
||||||
var log = event.Log
|
|
||||||
|
|
||||||
func logError(prefix string, err error) {
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("%s: %s", prefix, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func logWarn(prefix string, err error) {
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("%s: %s", prefix, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func UpdateClientConfig() {
|
|
||||||
conf := service.Config()
|
|
||||||
|
|
||||||
event.Publish("config.updated", event.Data{"config": conf.UserConfig()})
|
|
||||||
}
|
|
||||||
|
|
||||||
func Abort(c *gin.Context, code int, id i18n.Message, params ...interface{}) {
|
|
||||||
resp := i18n.NewResponse(code, id, params...)
|
|
||||||
|
|
||||||
log.Debugf("api-v1: abort %s with code %d (%s)", clean.Log(c.FullPath()), code, strings.ToLower(resp.String()))
|
|
||||||
|
|
||||||
c.AbortWithStatusJSON(code, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Error(c *gin.Context, code int, err error, id i18n.Message, params ...interface{}) {
|
|
||||||
resp := i18n.NewResponse(code, id, params...)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
resp.Details = err.Error()
|
|
||||||
log.Errorf("api-v1: error %s with code %d in %s (%s)", clean.Log(err.Error()), code, clean.Log(c.FullPath()), strings.ToLower(resp.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
c.AbortWithStatusJSON(code, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func AbortUnauthorized(c *gin.Context) {
|
|
||||||
Abort(c, http.StatusUnauthorized, i18n.ErrUnauthorized)
|
|
||||||
}
|
|
||||||
|
|
||||||
func AbortEntityNotFound(c *gin.Context) {
|
|
||||||
Abort(c, http.StatusNotFound, i18n.ErrEntityNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
func AbortAlbumNotFound(c *gin.Context) {
|
|
||||||
Abort(c, http.StatusNotFound, i18n.ErrAlbumNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
func AbortSaveFailed(c *gin.Context) {
|
|
||||||
Abort(c, http.StatusInternalServerError, i18n.ErrSaveFailed)
|
|
||||||
}
|
|
||||||
|
|
||||||
func AbortDeleteFailed(c *gin.Context) {
|
|
||||||
Abort(c, http.StatusInternalServerError, i18n.ErrDeleteFailed)
|
|
||||||
}
|
|
||||||
|
|
||||||
func AbortUnexpected(c *gin.Context) {
|
|
||||||
Abort(c, http.StatusInternalServerError, i18n.ErrUnexpected)
|
|
||||||
}
|
|
||||||
|
|
||||||
func AbortBadRequest(c *gin.Context) {
|
|
||||||
Abort(c, http.StatusBadRequest, i18n.ErrBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
func AbortAlreadyExists(c *gin.Context, s string) {
|
|
||||||
Abort(c, http.StatusConflict, i18n.ErrAlreadyExists, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func AbortFeatureDisabled(c *gin.Context) {
|
|
||||||
Abort(c, http.StatusForbidden, i18n.ErrFeatureDisabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
func AbortBusy(c *gin.Context) {
|
|
||||||
Abort(c, http.StatusTooManyRequests, i18n.ErrBusy)
|
|
||||||
}
|
|
||||||
|
|||||||
29
internal/api/api_client.go
Normal file
29
internal/api/api_client.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ClientIP returns the client IP address from the request context or a placeholder if it is unknown.
|
||||||
|
func ClientIP(c *gin.Context) (ip string) {
|
||||||
|
if c == nil {
|
||||||
|
// Should never happen.
|
||||||
|
return "0.0.0.0"
|
||||||
|
} else if ip = c.ClientIP(); ip == "" {
|
||||||
|
// Unit tests generally do not set a client IP. According to RFC 5737, the 192.0.2.0/24 subnet
|
||||||
|
// is intended for use in documentation and examples.
|
||||||
|
return "192.0.2.42"
|
||||||
|
}
|
||||||
|
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserAgent returns the user agent from the request context or an empty string if it is unknown.
|
||||||
|
func UserAgent(c *gin.Context) string {
|
||||||
|
if c == nil {
|
||||||
|
// Should never happen.
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Request.UserAgent()
|
||||||
|
}
|
||||||
35
internal/api/api_client_config.go
Normal file
35
internal/api/api_client_config.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UpdateClientConfig publishes updated client configuration values over the websocket connections.
|
||||||
|
func UpdateClientConfig() {
|
||||||
|
event.Publish("config.updated", event.Data{"config": service.Config().ClientUser(false)})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClientConfig returns the client configuration values as JSON.
|
||||||
|
//
|
||||||
|
// GET /api/v1/config
|
||||||
|
func GetClientConfig(router *gin.RouterGroup) {
|
||||||
|
router.GET("/config", func(c *gin.Context) {
|
||||||
|
s := Session(SessionID(c))
|
||||||
|
conf := service.Config()
|
||||||
|
|
||||||
|
if s == nil {
|
||||||
|
c.JSON(http.StatusOK, conf.ClientPublic())
|
||||||
|
} else if s.User().IsVisitor() {
|
||||||
|
c.JSON(http.StatusOK, conf.ClientShare())
|
||||||
|
} else if s.User().IsRegistered() {
|
||||||
|
c.JSON(http.StatusOK, conf.ClientSession(s))
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, conf.ClientPublic())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
21
internal/api/api_log.go
Normal file
21
internal/api/api_log.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
var log = event.Log
|
||||||
|
|
||||||
|
// logError logs an error if err is not nil.
|
||||||
|
func logError(prefix string, err error) {
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("%s: %s", prefix, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// logWarn logs a warning if err is not nil.
|
||||||
|
func logWarn(prefix string, err error) {
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("%s: %s", prefix, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
@@ -12,81 +11,14 @@ import (
|
|||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/form"
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
"github.com/photoprism/photoprism/internal/service"
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewApiTest returns new API test helper.
|
|
||||||
func NewApiTest() (app *gin.Engine, router *gin.RouterGroup, conf *config.Config) {
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
app = gin.New()
|
|
||||||
router = app.Group("/api/v1")
|
|
||||||
return app, router, service.Config()
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthenticateAdmin Register session routes and returns valid SessionId.
|
|
||||||
// Call this func after registering other routes and before performing other requests.
|
|
||||||
func AuthenticateAdmin(app *gin.Engine, router *gin.RouterGroup) (sessId string) {
|
|
||||||
return AuthenticateUser(app, router, "admin", "photoprism")
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthenticateUser Register session routes and returns valid SessionId.
|
|
||||||
// Call this func after registering other routes and before performing other requests.
|
|
||||||
func AuthenticateUser(app *gin.Engine, router *gin.RouterGroup, username string, password string) (sessId string) {
|
|
||||||
CreateSession(router)
|
|
||||||
f := form.Login{
|
|
||||||
Username: username,
|
|
||||||
Password: password,
|
|
||||||
}
|
|
||||||
loginStr, err := json.Marshal(f)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
r0 := PerformRequestWithBody(app, http.MethodPost, "/api/v1/session", string(loginStr))
|
|
||||||
sessId = r0.Header().Get("X-Session-ID")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Executes an API request with an empty request body.
|
|
||||||
// See https://medium.com/@craigchilds94/testing-gin-json-responses-1f258ce3b0b1
|
|
||||||
func PerformRequest(r http.Handler, method, path string) *httptest.ResponseRecorder {
|
|
||||||
req, _ := http.NewRequest(method, path, nil)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
r.ServeHTTP(w, req)
|
|
||||||
return w
|
|
||||||
}
|
|
||||||
|
|
||||||
// Performs authenticated API request with empty request body.
|
|
||||||
func AuthenticatedRequest(r http.Handler, method, path, sess string) *httptest.ResponseRecorder {
|
|
||||||
req, _ := http.NewRequest(method, path, nil)
|
|
||||||
req.Header.Add("X-Session-ID", sess)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
r.ServeHTTP(w, req)
|
|
||||||
return w
|
|
||||||
}
|
|
||||||
|
|
||||||
// Executes an API request with the request body as a string.
|
|
||||||
func PerformRequestWithBody(r http.Handler, method, path, body string) *httptest.ResponseRecorder {
|
|
||||||
reader := strings.NewReader(body)
|
|
||||||
req, _ := http.NewRequest(method, path, reader)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
r.ServeHTTP(w, req)
|
|
||||||
return w
|
|
||||||
}
|
|
||||||
|
|
||||||
// Performs an authenticated API request containing the request body as a string.
|
|
||||||
func AuthenticatedRequestWithBody(r http.Handler, method, path, body string, sessionId string) *httptest.ResponseRecorder {
|
|
||||||
reader := strings.NewReader(body)
|
|
||||||
req, _ := http.NewRequest(method, path, reader)
|
|
||||||
req.Header.Add("X-Session-ID", sessionId)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
r.ServeHTTP(w, req)
|
|
||||||
return w
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
log = logrus.StandardLogger()
|
log = logrus.StandardLogger()
|
||||||
log.SetLevel(logrus.TraceLevel)
|
log.SetLevel(logrus.TraceLevel)
|
||||||
|
event.AuditLog = log
|
||||||
|
|
||||||
c := config.NewTestConfig("api")
|
c := config.NewTestConfig("api")
|
||||||
service.SetConfig(c)
|
service.SetConfig(c)
|
||||||
@@ -97,3 +29,34 @@ func TestMain(m *testing.M) {
|
|||||||
|
|
||||||
os.Exit(code)
|
os.Exit(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewApiTest returns new API test helper.
|
||||||
|
func NewApiTest() (app *gin.Engine, router *gin.RouterGroup, conf *config.Config) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
app = gin.New()
|
||||||
|
router = app.Group("/api/v1")
|
||||||
|
|
||||||
|
return app, router, service.Config()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Executes an API request with an empty request body.
|
||||||
|
// See https://medium.com/@craigchilds94/testing-gin-json-responses-1f258ce3b0b1
|
||||||
|
func PerformRequest(r http.Handler, method, path string) *httptest.ResponseRecorder {
|
||||||
|
req, _ := http.NewRequest(method, path, nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// Executes an API request with the request body as a string.
|
||||||
|
func PerformRequestWithBody(r http.Handler, method, path, body string) *httptest.ResponseRecorder {
|
||||||
|
reader := strings.NewReader(body)
|
||||||
|
req, _ := http.NewRequest(method, path, reader)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|||||||
247
internal/api/api_websocket.go
Normal file
247
internal/api/api_websocket.go
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/acl"
|
||||||
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
|
"github.com/photoprism/photoprism/pkg/rnd"
|
||||||
|
)
|
||||||
|
|
||||||
|
// wsTimeout specifies the timeout duration for WebSocket connections.
|
||||||
|
var wsTimeout = 90 * time.Second
|
||||||
|
|
||||||
|
// wsSubPerm specifies the permissions required to subscribe to a channel.
|
||||||
|
var wsSubscribePerms = acl.Permissions{acl.ActionSubscribe}
|
||||||
|
|
||||||
|
// wsAuth maps connection IDs to specific users and session IDs.
|
||||||
|
var wsAuth = struct {
|
||||||
|
sid map[string]string
|
||||||
|
rid map[string]string
|
||||||
|
user map[string]entity.User
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}{
|
||||||
|
sid: make(map[string]string),
|
||||||
|
rid: make(map[string]string),
|
||||||
|
user: make(map[string]entity.User),
|
||||||
|
}
|
||||||
|
|
||||||
|
// wsConnection upgrades the HTTP server connection to the WebSocket protocol.
|
||||||
|
var wsConnection = websocket.Upgrader{
|
||||||
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// clientInfo represents information provided by the WebSocket client.
|
||||||
|
type clientInfo struct {
|
||||||
|
SessionID string `json:"session"`
|
||||||
|
CssUri string `json:"css"`
|
||||||
|
JsUri string `json:"js"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebSocket registers the /ws endpoint for establishing websocket connections.
|
||||||
|
func WebSocket(router *gin.RouterGroup) {
|
||||||
|
if router == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
conf := service.Config()
|
||||||
|
|
||||||
|
if conf == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
router.GET("/ws", func(c *gin.Context) {
|
||||||
|
w := c.Writer
|
||||||
|
r := c.Request
|
||||||
|
|
||||||
|
ws, err := wsConnection.Upgrade(w, r, nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var writeMutex sync.Mutex
|
||||||
|
|
||||||
|
defer ws.Close()
|
||||||
|
|
||||||
|
connId := rnd.UUID()
|
||||||
|
|
||||||
|
// Init connection.
|
||||||
|
wsAuth.mutex.Lock()
|
||||||
|
|
||||||
|
if conf.Public() {
|
||||||
|
wsAuth.user[connId] = entity.Admin
|
||||||
|
} else {
|
||||||
|
wsAuth.user[connId] = entity.UnknownUser
|
||||||
|
}
|
||||||
|
|
||||||
|
wsAuth.mutex.Unlock()
|
||||||
|
|
||||||
|
// Init writer.
|
||||||
|
go wsWriter(ws, &writeMutex, connId)
|
||||||
|
|
||||||
|
// Init reader.
|
||||||
|
wsReader(ws, &writeMutex, connId, conf)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// wsReader initializes a WebSocket reader for receiving messages.
|
||||||
|
func wsReader(ws *websocket.Conn, writeMutex *sync.Mutex, connId string, conf *config.Config) {
|
||||||
|
defer ws.Close()
|
||||||
|
|
||||||
|
ws.SetReadLimit(4096)
|
||||||
|
|
||||||
|
if err := ws.SetReadDeadline(time.Now().Add(wsTimeout)); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.SetPongHandler(func(string) error { _ = ws.SetReadDeadline(time.Now().Add(wsTimeout)); return nil })
|
||||||
|
|
||||||
|
for {
|
||||||
|
_, m, err := ws.ReadMessage()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
var info clientInfo
|
||||||
|
|
||||||
|
if err := json.Unmarshal(m, &info); err != nil {
|
||||||
|
// Do nothing.
|
||||||
|
} else {
|
||||||
|
if s := Session(info.SessionID); s != nil {
|
||||||
|
wsAuth.mutex.Lock()
|
||||||
|
wsAuth.sid[connId] = s.ID
|
||||||
|
wsAuth.rid[connId] = s.RefID
|
||||||
|
wsAuth.user[connId] = *s.User()
|
||||||
|
wsAuth.mutex.Unlock()
|
||||||
|
|
||||||
|
var clientConfig config.ClientConfig
|
||||||
|
|
||||||
|
if s.User().IsVisitor() {
|
||||||
|
clientConfig = conf.ClientShare()
|
||||||
|
} else if s.User().IsRegistered() {
|
||||||
|
clientConfig = conf.ClientSession(s)
|
||||||
|
} else {
|
||||||
|
clientConfig = conf.ClientPublic()
|
||||||
|
}
|
||||||
|
|
||||||
|
wsSendMessage("config.updated", event.Data{"config": clientConfig}, ws, writeMutex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wsWriter initializes a WebSocket writer for sending messages.
|
||||||
|
func wsWriter(ws *websocket.Conn, writeMutex *sync.Mutex, connId string) {
|
||||||
|
pingTicker := time.NewTicker(15 * time.Second)
|
||||||
|
|
||||||
|
// Subscribe to events.
|
||||||
|
e := event.Subscribe(
|
||||||
|
"session.*",
|
||||||
|
"log.fatal",
|
||||||
|
"log.error",
|
||||||
|
"log.warning",
|
||||||
|
"log.warn",
|
||||||
|
"log.info",
|
||||||
|
"notify.*",
|
||||||
|
"index.*",
|
||||||
|
"upload.*",
|
||||||
|
"import.*",
|
||||||
|
"config.*",
|
||||||
|
"count.*",
|
||||||
|
"photos.*",
|
||||||
|
"cameras.*",
|
||||||
|
"lenses.*",
|
||||||
|
"countries.*",
|
||||||
|
"albums.*",
|
||||||
|
"labels.*",
|
||||||
|
"subjects.*",
|
||||||
|
"people.*",
|
||||||
|
"sync.*",
|
||||||
|
)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
pingTicker.Stop()
|
||||||
|
event.Unsubscribe(e)
|
||||||
|
_ = ws.Close()
|
||||||
|
|
||||||
|
wsAuth.mutex.Lock()
|
||||||
|
delete(wsAuth.sid, connId)
|
||||||
|
delete(wsAuth.rid, connId)
|
||||||
|
delete(wsAuth.user, connId)
|
||||||
|
wsAuth.mutex.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-pingTicker.C:
|
||||||
|
writeMutex.Lock()
|
||||||
|
|
||||||
|
if err := ws.SetWriteDeadline(time.Now().Add(30 * time.Second)); err != nil {
|
||||||
|
writeMutex.Unlock()
|
||||||
|
return
|
||||||
|
} else if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
|
||||||
|
writeMutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeMutex.Unlock()
|
||||||
|
case msg := <-e.Receiver:
|
||||||
|
wsAuth.mutex.RLock()
|
||||||
|
|
||||||
|
sid := wsAuth.sid[connId] // Session ID.
|
||||||
|
// rid := wsAuth.rid[connId] // Session RefID.
|
||||||
|
user := entity.UnknownUser // User.
|
||||||
|
|
||||||
|
if hit, ok := wsAuth.user[connId]; ok {
|
||||||
|
user = hit
|
||||||
|
}
|
||||||
|
|
||||||
|
wsAuth.mutex.RUnlock()
|
||||||
|
|
||||||
|
// Split topic into channel and event name.
|
||||||
|
ch, ev := event.Topic(msg.Topic())
|
||||||
|
|
||||||
|
// Message intended for a specific session only?
|
||||||
|
if acl.ChannelSession.Equal(ch) {
|
||||||
|
if s, topic := event.Topic(ev); s == sid && topic != "" {
|
||||||
|
// Send to client with the matching session ID.
|
||||||
|
wsSendMessage(topic, msg.Fields, ws, writeMutex)
|
||||||
|
}
|
||||||
|
} else if chRes := acl.Resource(ch); acl.Events.AllowAll(chRes, user.AclRole(), wsSubscribePerms) {
|
||||||
|
// Send the message to authorized recipient.
|
||||||
|
// event.AuditDebug([]string{"websocket", "session %s", "%s %s as %s", "granted"}, rid, wsSubscribePerms.String(), chRes.String(), user.AclRole().String())
|
||||||
|
wsSendMessage(msg.Topic(), msg.Fields, ws, writeMutex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wsSendMessage sends a message to the WebSocket client.
|
||||||
|
func wsSendMessage(topic string, data interface{}, ws *websocket.Conn, writeMutex *sync.Mutex) {
|
||||||
|
if topic == "" || ws == nil || writeMutex == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeMutex.Lock()
|
||||||
|
defer writeMutex.Unlock()
|
||||||
|
|
||||||
|
if err := ws.SetWriteDeadline(time.Now().Add(30 * time.Second)); err != nil {
|
||||||
|
return
|
||||||
|
} else if err := ws.WriteJSON(gin.H{"event": topic, "data": data}); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,14 +10,14 @@ import (
|
|||||||
func TestWebsocket(t *testing.T) {
|
func TestWebsocket(t *testing.T) {
|
||||||
t.Run("bad request", func(t *testing.T) {
|
t.Run("bad request", func(t *testing.T) {
|
||||||
app, router, _ := NewApiTest()
|
app, router, _ := NewApiTest()
|
||||||
Websocket(router)
|
WebSocket(router)
|
||||||
r := PerformRequest(app, "GET", "/api/v1/ws")
|
r := PerformRequest(app, "GET", "/api/v1/ws")
|
||||||
assert.Equal(t, http.StatusBadRequest, r.Code)
|
assert.Equal(t, http.StatusBadRequest, r.Code)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("router nil", func(t *testing.T) {
|
t.Run("router nil", func(t *testing.T) {
|
||||||
app, _, _ := NewApiTest()
|
app, _, _ := NewApiTest()
|
||||||
Websocket(nil)
|
WebSocket(nil)
|
||||||
r := PerformRequest(app, "GET", "/api/v1/ws")
|
r := PerformRequest(app, "GET", "/api/v1/ws")
|
||||||
assert.Equal(t, http.StatusNotFound, r.Code)
|
assert.Equal(t, http.StatusNotFound, r.Code)
|
||||||
})
|
})
|
||||||
34
internal/api/auth.go
Normal file
34
internal/api/auth.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/photoprism/photoprism/internal/acl"
|
||||||
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Auth checks if the user has permission to access the specified resource and returns the session if so.
|
||||||
|
func Auth(c *gin.Context, resource acl.Resource, grant acl.Permission) *entity.Session {
|
||||||
|
return AuthAny(c, resource, acl.Permissions{grant})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthAny checks if at least one permission allows access and returns the session in this case.
|
||||||
|
func AuthAny(c *gin.Context, resource acl.Resource, grants acl.Permissions) *entity.Session {
|
||||||
|
// Get session ID, if any.
|
||||||
|
sessId := SessionID(c)
|
||||||
|
|
||||||
|
// Find and return the client session after all checks have passed.
|
||||||
|
if s := Session(sessId); s == nil {
|
||||||
|
event.AuditWarn([]string{ClientIP(c), "unauthenticated", "%s %s as unknown user", "denied"}, grants.String(), string(resource))
|
||||||
|
return entity.SessionStatusUnauthorized()
|
||||||
|
} else if s.User() == nil {
|
||||||
|
event.AuditWarn([]string{ClientIP(c), "session %s", "%s %s as unknown user", "denied"}, s.RefID, grants.String(), string(resource))
|
||||||
|
return entity.SessionStatusUnauthorized()
|
||||||
|
} else if acl.Resources.DenyAll(resource, s.User().AclRole(), grants) {
|
||||||
|
event.AuditErr([]string{ClientIP(c), "session %s", "%s %s as %s", "denied"}, s.RefID, grants.String(), string(resource), s.User().AclRole().String())
|
||||||
|
return entity.SessionStatusForbidden()
|
||||||
|
} else {
|
||||||
|
event.AuditInfo([]string{ClientIP(c), "session %s", "%s %s as %s", "granted"}, s.RefID, grants.String(), string(resource), s.User().AclRole().String())
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,38 +3,44 @@ package api
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/pkg/clean"
|
"github.com/dustin/go-humanize/english"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/acl"
|
"github.com/photoprism/photoprism/internal/acl"
|
||||||
"github.com/photoprism/photoprism/internal/entity"
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
"github.com/photoprism/photoprism/internal/form"
|
"github.com/photoprism/photoprism/internal/form"
|
||||||
"github.com/photoprism/photoprism/internal/i18n"
|
"github.com/photoprism/photoprism/internal/i18n"
|
||||||
"github.com/photoprism/photoprism/internal/service"
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ChangePassword changes the password of the currently authenticated user.
|
||||||
|
//
|
||||||
// PUT /api/v1/users/:uid/password
|
// PUT /api/v1/users/:uid/password
|
||||||
func ChangePassword(router *gin.RouterGroup) {
|
func ChangePassword(router *gin.RouterGroup) {
|
||||||
router.PUT("/users/:uid/password", func(c *gin.Context) {
|
router.PUT("/users/:uid/password", func(c *gin.Context) {
|
||||||
conf := service.Config()
|
conf := service.Config()
|
||||||
|
|
||||||
|
// You cannot change any passwords without authentication and settings enabled.
|
||||||
if conf.Public() || conf.DisableSettings() {
|
if conf.Public() || conf.DisableSettings() {
|
||||||
Abort(c, http.StatusForbidden, i18n.ErrPublic)
|
Abort(c, http.StatusForbidden, i18n.ErrPublic)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s := Auth(SessionID(c), acl.ResourceUsers, acl.ActionUpdateSelf)
|
// Get session.
|
||||||
|
s := Auth(c, acl.ResourcePassword, acl.ActionUpdate)
|
||||||
|
|
||||||
if s.Invalid() {
|
if s.Abort(c) {
|
||||||
AbortUnauthorized(c)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
uid := clean.IdString(c.Param("uid"))
|
uid := clean.UID(c.Param("uid"))
|
||||||
m := entity.FindUserByUID(uid)
|
m := entity.FindUserByUID(uid)
|
||||||
|
|
||||||
if s.User.UserUID != m.UserUID {
|
// Users may only change their own password.
|
||||||
AbortUnauthorized(c)
|
if s.User().UserUID != m.UserUID {
|
||||||
|
AbortForbidden(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,16 +56,23 @@ func ChangePassword(router *gin.RouterGroup) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify that the old password is correct.
|
||||||
if m.InvalidPassword(f.OldPassword) {
|
if m.InvalidPassword(f.OldPassword) {
|
||||||
Abort(c, http.StatusBadRequest, i18n.ErrInvalidPassword)
|
Abort(c, http.StatusBadRequest, i18n.ErrInvalidPassword)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Change password.
|
||||||
if err := m.SetPassword(f.NewPassword); err != nil {
|
if err := m.SetPassword(f.NewPassword); err != nil {
|
||||||
Error(c, http.StatusBadRequest, err, i18n.ErrInvalidPassword)
|
Error(c, http.StatusBadRequest, err, i18n.ErrInvalidPassword)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Invalidate all other user sessions to protect the account:
|
||||||
|
// https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
|
||||||
|
event.AuditInfo([]string{ClientIP(c), "session %s", "password changed", "invalidated %s"}, s.RefID,
|
||||||
|
english.Plural(m.DeleteSessions([]string{s.ID}), "session", "sessions"))
|
||||||
|
|
||||||
c.JSON(http.StatusOK, i18n.NewResponse(http.StatusOK, i18n.MsgPasswordChanged))
|
c.JSON(http.StatusOK, i18n.NewResponse(http.StatusOK, i18n.MsgPasswordChanged))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -11,16 +11,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestChangePassword(t *testing.T) {
|
func TestChangePassword(t *testing.T) {
|
||||||
t.Run("not existing user", func(t *testing.T) {
|
t.Run("NonExistentUser", func(t *testing.T) {
|
||||||
app, router, _ := NewApiTest()
|
app, router, _ := NewApiTest()
|
||||||
ChangePassword(router)
|
ChangePassword(router)
|
||||||
r := PerformRequestWithBody(app, "PUT", "/api/v1/users/xxx/password", `{}`)
|
r := PerformRequestWithBody(app, "PUT", "/api/v1/users/xxx/password", `{}`)
|
||||||
assert.Equal(t, http.StatusForbidden, r.Code)
|
assert.Equal(t, http.StatusForbidden, r.Code)
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
func TestChangeUserPasswords(t *testing.T) {
|
t.Run("AliceProvidesWrongPassword", func(t *testing.T) {
|
||||||
t.Run("alice: change password invalid", func(t *testing.T) {
|
|
||||||
app, router, conf := NewApiTest()
|
app, router, conf := NewApiTest()
|
||||||
conf.SetPublic(false)
|
conf.SetPublic(false)
|
||||||
defer conf.SetPublic(true)
|
defer conf.SetPublic(true)
|
||||||
@@ -39,32 +37,52 @@ func TestChangeUserPasswords(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusBadRequest, r.Code)
|
assert.Equal(t, http.StatusBadRequest, r.Code)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("alice: change password valid", func(t *testing.T) {
|
|
||||||
|
t.Run("Success", func(t *testing.T) {
|
||||||
|
app, router, conf := NewApiTest()
|
||||||
|
conf.SetPublic(false)
|
||||||
|
defer conf.SetPublic(true)
|
||||||
|
ChangePassword(router)
|
||||||
|
|
||||||
|
oldPassword := "PleaseChange$42"
|
||||||
|
newPassword := "SoftwareDevelopmentIsAYoungProfession1234567890!@#$%^&*()_+[]{}|:<>?/.,"
|
||||||
|
|
||||||
|
sessId := AuthenticateUser(app, router, "fowler", oldPassword)
|
||||||
|
|
||||||
|
frm := form.ChangePassword{
|
||||||
|
OldPassword: oldPassword,
|
||||||
|
NewPassword: newPassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
if jsonFrm, err := json.Marshal(frm); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
} else {
|
||||||
|
r := AuthenticatedRequestWithBody(app, "PUT", "/api/v1/users/urinotv3d6jedvlm/password",
|
||||||
|
string(jsonFrm), sessId)
|
||||||
|
assert.Equal(t, http.StatusOK, r.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
frm = form.ChangePassword{
|
||||||
|
OldPassword: newPassword,
|
||||||
|
NewPassword: oldPassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
if jsonFrm, err := json.Marshal(frm); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
} else {
|
||||||
|
r := AuthenticatedRequestWithBody(app, "PUT", "/api/v1/users/urinotv3d6jedvlm/password",
|
||||||
|
string(jsonFrm), sessId)
|
||||||
|
assert.Equal(t, http.StatusOK, r.Code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("AliceChangesOtherUsersPassword", func(t *testing.T) {
|
||||||
app, router, conf := NewApiTest()
|
app, router, conf := NewApiTest()
|
||||||
conf.SetPublic(false)
|
conf.SetPublic(false)
|
||||||
defer conf.SetPublic(true)
|
defer conf.SetPublic(true)
|
||||||
ChangePassword(router)
|
ChangePassword(router)
|
||||||
sessId := AuthenticateUser(app, router, "alice", "Alice123!")
|
sessId := AuthenticateUser(app, router, "alice", "Alice123!")
|
||||||
|
|
||||||
f := form.ChangePassword{
|
|
||||||
OldPassword: "Alice123!",
|
|
||||||
NewPassword: "aliceinwonderland",
|
|
||||||
}
|
|
||||||
if pwStr, err := json.Marshal(f); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
} else {
|
|
||||||
r := AuthenticatedRequestWithBody(app, "PUT", "/api/v1/users/uqxetse3cy5eo9z2/password",
|
|
||||||
string(pwStr), sessId)
|
|
||||||
assert.Equal(t, http.StatusOK, r.Code)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
t.Run("alice as admin: change bob's password", func(t *testing.T) {
|
|
||||||
app, router, conf := NewApiTest()
|
|
||||||
conf.SetPublic(false)
|
|
||||||
defer conf.SetPublic(true)
|
|
||||||
ChangePassword(router)
|
|
||||||
sessId := AuthenticateUser(app, router, "alice", "aliceinwonderland")
|
|
||||||
|
|
||||||
f := form.ChangePassword{
|
f := form.ChangePassword{
|
||||||
OldPassword: "Bobbob123!",
|
OldPassword: "Bobbob123!",
|
||||||
NewPassword: "helloworld",
|
NewPassword: "helloworld",
|
||||||
@@ -74,10 +92,11 @@ func TestChangeUserPasswords(t *testing.T) {
|
|||||||
} else {
|
} else {
|
||||||
r := AuthenticatedRequestWithBody(app, "PUT", "/api/v1/users/uqxc08w3d0ej2283/password",
|
r := AuthenticatedRequestWithBody(app, "PUT", "/api/v1/users/uqxc08w3d0ej2283/password",
|
||||||
string(pwStr), sessId)
|
string(pwStr), sessId)
|
||||||
assert.Equal(t, http.StatusUnauthorized, r.Code)
|
assert.Equal(t, http.StatusForbidden, r.Code)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("bob: change wrong password", func(t *testing.T) {
|
|
||||||
|
t.Run("BobProvidesWrongPassword", func(t *testing.T) {
|
||||||
app, router, conf := NewApiTest()
|
app, router, conf := NewApiTest()
|
||||||
conf.SetPublic(false)
|
conf.SetPublic(false)
|
||||||
defer conf.SetPublic(true)
|
defer conf.SetPublic(true)
|
||||||
@@ -97,7 +116,7 @@ func TestChangeUserPasswords(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("friend: change password to same", func(t *testing.T) {
|
t.Run("SameNewPassword", func(t *testing.T) {
|
||||||
app, router, conf := NewApiTest()
|
app, router, conf := NewApiTest()
|
||||||
conf.SetPublic(false)
|
conf.SetPublic(false)
|
||||||
defer conf.SetPublic(true)
|
defer conf.SetPublic(true)
|
||||||
@@ -117,7 +136,7 @@ func TestChangeUserPasswords(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("bob: change alice's password", func(t *testing.T) {
|
t.Run("BobChangesOtherUsersPassword", func(t *testing.T) {
|
||||||
app, router, conf := NewApiTest()
|
app, router, conf := NewApiTest()
|
||||||
conf.SetPublic(false)
|
conf.SetPublic(false)
|
||||||
defer conf.SetPublic(true)
|
defer conf.SetPublic(true)
|
||||||
@@ -133,7 +152,7 @@ func TestChangeUserPasswords(t *testing.T) {
|
|||||||
} else {
|
} else {
|
||||||
r := AuthenticatedRequestWithBody(app, "PUT", "/api/v1/users/uqxetse3cy5eo9z2/password",
|
r := AuthenticatedRequestWithBody(app, "PUT", "/api/v1/users/uqxetse3cy5eo9z2/password",
|
||||||
string(pwStr), sessId)
|
string(pwStr), sessId)
|
||||||
assert.Equal(t, http.StatusUnauthorized, r.Code)
|
assert.Equal(t, http.StatusForbidden, r.Code)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
40
internal/api/auth_session.go
Normal file
40
internal/api/auth_session.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
|
"github.com/photoprism/photoprism/internal/session"
|
||||||
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SessionID returns the session ID from the request context.
|
||||||
|
func SessionID(c *gin.Context) (sessId string) {
|
||||||
|
if c == nil {
|
||||||
|
// Should never happen.
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the authentication token from the HTTP headers.
|
||||||
|
return clean.ID(c.GetHeader(session.Header))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session finds the client session for the given ID or returns nil otherwise.
|
||||||
|
func Session(id string) *entity.Session {
|
||||||
|
// Return default session when public mode is enabled.
|
||||||
|
if service.Config().Public() {
|
||||||
|
return service.Session().Public()
|
||||||
|
} else if id == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find session or otherwise return nil.
|
||||||
|
s, err := service.Session().Get(id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &s
|
||||||
|
}
|
||||||
136
internal/api/auth_session_create.go
Normal file
136
internal/api/auth_session_create.go
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/acl"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
|
"github.com/photoprism/photoprism/internal/form"
|
||||||
|
"github.com/photoprism/photoprism/internal/i18n"
|
||||||
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateSession creates a new client session and returns it as JSON if authentication was successful.
|
||||||
|
//
|
||||||
|
// POST /api/v1/session
|
||||||
|
func CreateSession(router *gin.RouterGroup) {
|
||||||
|
router.POST("/session", func(c *gin.Context) {
|
||||||
|
var err error
|
||||||
|
var f form.Login
|
||||||
|
|
||||||
|
if err = c.BindJSON(&f); err != nil {
|
||||||
|
event.AuditWarn([]string{ClientIP(c), "invalid create session request"})
|
||||||
|
AbortBadRequest(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var user *entity.User
|
||||||
|
var sess *entity.Session
|
||||||
|
var data *entity.SessionData
|
||||||
|
|
||||||
|
id := SessionID(c)
|
||||||
|
|
||||||
|
// Search existing session.
|
||||||
|
if s := Session(id); s != nil {
|
||||||
|
sess = s
|
||||||
|
data = s.Data()
|
||||||
|
user = s.User()
|
||||||
|
} else {
|
||||||
|
data = entity.NewSessionData()
|
||||||
|
user = &entity.User{}
|
||||||
|
id = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
conf := service.Config()
|
||||||
|
|
||||||
|
// Share token provided?
|
||||||
|
if f.HasToken() {
|
||||||
|
if shares := data.RedeemToken(f.AuthToken); shares == 0 {
|
||||||
|
event.AuditWarn([]string{ClientIP(c), "share token %s", "invalid"}, clean.LogQuote(f.AuthToken))
|
||||||
|
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": i18n.Msg(i18n.ErrInvalidLink)})
|
||||||
|
event.LoginError(ClientIP(c), "", UserAgent(c), "invalid share token")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.AuditInfo([]string{ClientIP(c), "share token %s", "redeemed", "%#v"}, clean.LogQuote(f.AuthToken), data)
|
||||||
|
|
||||||
|
// Upgrade from Unknown to Visitor. Don't downgrade.
|
||||||
|
if user.IsUnknown() {
|
||||||
|
user = &entity.Visitor
|
||||||
|
event.AuditDebug([]string{ClientIP(c), "share token %s", "upgrading session to user role %s"}, clean.LogQuote(f.AuthToken), acl.RoleVisitor.String())
|
||||||
|
}
|
||||||
|
} else if f.HasCredentials() {
|
||||||
|
// If not, authenticate with username and password.
|
||||||
|
userName := f.Name()
|
||||||
|
user = entity.FindUserByName(userName)
|
||||||
|
|
||||||
|
// User found?
|
||||||
|
if user == nil {
|
||||||
|
message := "account not found"
|
||||||
|
event.AuditWarn([]string{ClientIP(c), "login as %s", message}, clean.LogQuote(userName))
|
||||||
|
c.AbortWithStatusJSON(400, gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
|
||||||
|
event.LoginError(ClientIP(c), f.Name(), UserAgent(c), message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login allowed?
|
||||||
|
if !user.LoginAllowed() {
|
||||||
|
message := "account disabled"
|
||||||
|
event.AuditWarn([]string{ClientIP(c), "login as %s", message}, clean.LogQuote(userName))
|
||||||
|
c.AbortWithStatusJSON(400, gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
|
||||||
|
event.LoginError(ClientIP(c), f.Name(), UserAgent(c), message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password valid?
|
||||||
|
if user.InvalidPassword(f.Password) {
|
||||||
|
message := "incorrect password"
|
||||||
|
event.AuditErr([]string{ClientIP(c), "login as %s", message}, clean.LogQuote(userName))
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
|
||||||
|
event.LoginError(ClientIP(c), f.Name(), UserAgent(c), message)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
event.AuditInfo([]string{ClientIP(c), "login as %s", "succeeded"}, clean.LogQuote(userName))
|
||||||
|
event.LoginSuccess(ClientIP(c), f.Name(), UserAgent(c))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
|
||||||
|
event.LoginError(ClientIP(c), f.Name(), UserAgent(c), "invalid request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save session.
|
||||||
|
if sess, err = service.Session().Save(id, user, c, data); err != nil {
|
||||||
|
event.AuditWarn([]string{ClientIP(c), "%s"}, err)
|
||||||
|
} else if sess == nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": i18n.Msg(i18n.ErrUnexpected)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log event.
|
||||||
|
event.AuditInfo([]string{ClientIP(c), "session %s", "created"}, sess.RefID)
|
||||||
|
|
||||||
|
// Add session id to response headers.
|
||||||
|
AddSessionHeader(c, sess.ID)
|
||||||
|
|
||||||
|
var clientConfig config.ClientConfig
|
||||||
|
|
||||||
|
if sess.User().IsVisitor() {
|
||||||
|
clientConfig = conf.ClientShare()
|
||||||
|
} else if sess.User().IsRegistered() {
|
||||||
|
clientConfig = conf.ClientSession(sess)
|
||||||
|
} else {
|
||||||
|
clientConfig = conf.ClientPublic()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send JSON response with user information, session data, and client config values.
|
||||||
|
c.JSON(http.StatusOK, gin.H{"status": "ok", "id": sess.ID, "user": sess.User(), "data": sess.Data(), "config": clientConfig})
|
||||||
|
})
|
||||||
|
}
|
||||||
36
internal/api/auth_session_delete.go
Normal file
36
internal/api/auth_session_delete.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DeleteSession deletes an existing client session (logout).
|
||||||
|
//
|
||||||
|
// DELETE /api/v1/session/:id
|
||||||
|
func DeleteSession(router *gin.RouterGroup) {
|
||||||
|
router.DELETE("/session/:id", func(c *gin.Context) {
|
||||||
|
id := clean.ID(c.Param("id"))
|
||||||
|
|
||||||
|
if id == "" {
|
||||||
|
AbortBadRequest(c)
|
||||||
|
return
|
||||||
|
} else if service.Config().Public() {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"status": "authentication disabled", "id": id})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := service.Session().Delete(id); err != nil {
|
||||||
|
event.AuditErr([]string{ClientIP(c), "session %s"}, err)
|
||||||
|
} else {
|
||||||
|
event.AuditDebug([]string{ClientIP(c), "session deleted"})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"status": "ok", "id": id})
|
||||||
|
})
|
||||||
|
}
|
||||||
67
internal/api/auth_session_get.go
Normal file
67
internal/api/auth_session_get.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetSession returns the session data as JSON if authentication was successful.
|
||||||
|
//
|
||||||
|
// GET /api/v1/session/:id
|
||||||
|
func GetSession(router *gin.RouterGroup) {
|
||||||
|
router.GET("/session/:id", func(c *gin.Context) {
|
||||||
|
id := clean.ID(c.Param("id"))
|
||||||
|
|
||||||
|
if id == "" {
|
||||||
|
AbortBadRequest(c)
|
||||||
|
return
|
||||||
|
} else if id != SessionID(c) {
|
||||||
|
AbortForbidden(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sess := Session(id)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case sess == nil:
|
||||||
|
AbortUnauthorized(c)
|
||||||
|
return
|
||||||
|
case sess.Expired(), sess.ID == "":
|
||||||
|
AbortUnauthorized(c)
|
||||||
|
return
|
||||||
|
case sess.Invalid():
|
||||||
|
AbortForbidden(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update user information.
|
||||||
|
sess.RefreshUser()
|
||||||
|
|
||||||
|
// Add session id to response headers.
|
||||||
|
AddSessionHeader(c, sess.ID)
|
||||||
|
|
||||||
|
var clientConfig config.ClientConfig
|
||||||
|
|
||||||
|
if conf := service.Config(); conf == nil {
|
||||||
|
log.Errorf("session: config is nil - possible bug")
|
||||||
|
AbortUnexpected(c)
|
||||||
|
return
|
||||||
|
} else if sess.User().IsVisitor() {
|
||||||
|
clientConfig = conf.ClientShare()
|
||||||
|
} else if sess.User().IsRegistered() {
|
||||||
|
clientConfig = conf.ClientSession(sess)
|
||||||
|
} else {
|
||||||
|
clientConfig = conf.ClientPublic()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send JSON response with user information, session data, and client config values.
|
||||||
|
c.JSON(http.StatusOK, gin.H{"status": "ok", "id": sess.ID, "user": sess.User(), "data": sess.Data(), "config": clientConfig})
|
||||||
|
})
|
||||||
|
}
|
||||||
157
internal/api/auth_session_test.go
Normal file
157
internal/api/auth_session_test.go
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/i18n"
|
||||||
|
"github.com/photoprism/photoprism/internal/session"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSessionID(t *testing.T) {
|
||||||
|
t.Run("NoContext", func(t *testing.T) {
|
||||||
|
result := SessionID(nil)
|
||||||
|
assert.Equal(t, "", result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSession(t *testing.T) {
|
||||||
|
t.Run("Public", func(t *testing.T) {
|
||||||
|
assert.Equal(t, session.Public, Session(""))
|
||||||
|
assert.Equal(t, session.Public, Session("638bffc9b86a8fda0d908ebee84a43930cb8d1e3507f4aa0"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateSession(t *testing.T) {
|
||||||
|
t.Run("Success", func(t *testing.T) {
|
||||||
|
app, router, _ := NewApiTest()
|
||||||
|
CreateSession(router)
|
||||||
|
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/session", `{"name": "admin", "password": "photoprism"}`)
|
||||||
|
log.Debugf("BODY: %s", r.Body.String())
|
||||||
|
val2 := gjson.Get(r.Body.String(), "user.Name")
|
||||||
|
assert.Equal(t, "admin", val2.String())
|
||||||
|
assert.Equal(t, http.StatusOK, r.Code)
|
||||||
|
})
|
||||||
|
t.Run("BadRequest", func(t *testing.T) {
|
||||||
|
app, router, _ := NewApiTest()
|
||||||
|
CreateSession(router)
|
||||||
|
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/session", `{"name": 123, "password": "xxx"}`)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, r.Code)
|
||||||
|
})
|
||||||
|
t.Run("InvalidShareToken", func(t *testing.T) {
|
||||||
|
app, router, _ := NewApiTest()
|
||||||
|
CreateSession(router)
|
||||||
|
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/session", `{"name": "admin", "password": "photoprism", "token": "xxx"}`)
|
||||||
|
assert.Equal(t, http.StatusNotFound, r.Code)
|
||||||
|
})
|
||||||
|
t.Run("ValidShareToken", func(t *testing.T) {
|
||||||
|
app, router, _ := NewApiTest()
|
||||||
|
CreateSession(router)
|
||||||
|
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/session", `{"name": "admin", "password": "photoprism", "token": "1jxf3jfn2k"}`)
|
||||||
|
assert.Equal(t, http.StatusOK, r.Code)
|
||||||
|
})
|
||||||
|
t.Run("AdminInvalidPassword", func(t *testing.T) {
|
||||||
|
app, router, _ := NewApiTest()
|
||||||
|
CreateSession(router)
|
||||||
|
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/session", `{"name": "admin", "password": "xxx"}`)
|
||||||
|
val := gjson.Get(r.Body.String(), "error")
|
||||||
|
assert.Equal(t, i18n.Msg(i18n.ErrInvalidCredentials), val.String())
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, r.Code)
|
||||||
|
})
|
||||||
|
t.Run("AliceSuccess", func(t *testing.T) {
|
||||||
|
app, router, _ := NewApiTest()
|
||||||
|
CreateSession(router)
|
||||||
|
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/session", `{"name": "alice", "password": "Alice123!"}`)
|
||||||
|
userEmail := gjson.Get(r.Body.String(), "user.Email")
|
||||||
|
userName := gjson.Get(r.Body.String(), "user.Name")
|
||||||
|
assert.Equal(t, "alice@example.com", userEmail.String())
|
||||||
|
assert.Equal(t, "alice", userName.String())
|
||||||
|
assert.Equal(t, http.StatusOK, r.Code)
|
||||||
|
})
|
||||||
|
t.Run("BobSuccess", func(t *testing.T) {
|
||||||
|
app, router, _ := NewApiTest()
|
||||||
|
CreateSession(router)
|
||||||
|
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/session", `{"name": "bob", "password": "Bobbob123!"}`)
|
||||||
|
userEmail := gjson.Get(r.Body.String(), "user.Email")
|
||||||
|
userName := gjson.Get(r.Body.String(), "user.Name")
|
||||||
|
assert.Equal(t, "bob@example.com", userEmail.String())
|
||||||
|
assert.Equal(t, "bob", userName.String())
|
||||||
|
assert.Equal(t, http.StatusOK, r.Code)
|
||||||
|
})
|
||||||
|
t.Run("BobInvalidPassword", func(t *testing.T) {
|
||||||
|
app, router, _ := NewApiTest()
|
||||||
|
CreateSession(router)
|
||||||
|
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/session", `{"name": "bob", "password": "helloworld"}`)
|
||||||
|
val := gjson.Get(r.Body.String(), "error")
|
||||||
|
assert.Equal(t, i18n.Msg(i18n.ErrInvalidCredentials), val.String())
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, r.Code)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetSession(t *testing.T) {
|
||||||
|
t.Run("AdminWithoutAuthentication", func(t *testing.T) {
|
||||||
|
app, router, _ := NewApiTest()
|
||||||
|
GetSession(router)
|
||||||
|
|
||||||
|
sessId := AuthenticateAdmin(app, router)
|
||||||
|
r := PerformRequest(app, http.MethodGet, "/api/v1/session/"+sessId)
|
||||||
|
assert.Equal(t, http.StatusForbidden, r.Code)
|
||||||
|
})
|
||||||
|
t.Run("AdminAuthenticatedRequest", func(t *testing.T) {
|
||||||
|
app, router, _ := NewApiTest()
|
||||||
|
GetSession(router)
|
||||||
|
|
||||||
|
sessId := AuthenticateAdmin(app, router)
|
||||||
|
r := AuthenticatedRequest(app, http.MethodGet, "/api/v1/session/"+sessId, sessId)
|
||||||
|
assert.Equal(t, http.StatusOK, r.Code)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteSession(t *testing.T) {
|
||||||
|
t.Run("AdminWithoutAuthentication", func(t *testing.T) {
|
||||||
|
app, router, _ := NewApiTest()
|
||||||
|
DeleteSession(router)
|
||||||
|
|
||||||
|
sessId := AuthenticateAdmin(app, router)
|
||||||
|
|
||||||
|
r := PerformRequest(app, http.MethodDelete, "/api/v1/session/"+sessId)
|
||||||
|
assert.Equal(t, http.StatusOK, r.Code)
|
||||||
|
})
|
||||||
|
t.Run("AdminAuthenticatedRequest", func(t *testing.T) {
|
||||||
|
app, router, _ := NewApiTest()
|
||||||
|
DeleteSession(router)
|
||||||
|
|
||||||
|
sessId := AuthenticateAdmin(app, router)
|
||||||
|
|
||||||
|
r := AuthenticatedRequest(app, http.MethodDelete, "/api/v1/session/"+sessId, sessId)
|
||||||
|
assert.Equal(t, http.StatusOK, r.Code)
|
||||||
|
})
|
||||||
|
t.Run("UserWithoutAuthentication", func(t *testing.T) {
|
||||||
|
app, router, _ := NewApiTest()
|
||||||
|
DeleteSession(router)
|
||||||
|
|
||||||
|
sessId := AuthenticateUser(app, router, "alice", "Alice123!")
|
||||||
|
|
||||||
|
r := PerformRequest(app, http.MethodDelete, "/api/v1/session/"+sessId)
|
||||||
|
assert.Equal(t, http.StatusOK, r.Code)
|
||||||
|
})
|
||||||
|
t.Run("UserAuthenticatedRequest", func(t *testing.T) {
|
||||||
|
app, router, _ := NewApiTest()
|
||||||
|
DeleteSession(router)
|
||||||
|
|
||||||
|
sessId := AuthenticateUser(app, router, "alice", "Alice123!")
|
||||||
|
|
||||||
|
r := AuthenticatedRequest(app, http.MethodDelete, "/api/v1/session/"+sessId, sessId)
|
||||||
|
assert.Equal(t, http.StatusOK, r.Code)
|
||||||
|
})
|
||||||
|
t.Run("InvalidSession", func(t *testing.T) {
|
||||||
|
sessId := "638bffc9b86a8fda0d908ebee84a43930cb8d1e3507f4aa0"
|
||||||
|
app, router, _ := NewApiTest()
|
||||||
|
DeleteSession(router)
|
||||||
|
r := PerformRequest(app, http.MethodDelete, "/api/v1/session/"+sessId)
|
||||||
|
assert.Equal(t, http.StatusOK, r.Code)
|
||||||
|
})
|
||||||
|
}
|
||||||
63
internal/api/auth_test.go
Normal file
63
internal/api/auth_test.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/form"
|
||||||
|
"github.com/photoprism/photoprism/internal/session"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthenticateAdmin Register session routes and returns valid SessionId.
|
||||||
|
// Call this func after registering other routes and before performing other requests.
|
||||||
|
func AuthenticateAdmin(app *gin.Engine, router *gin.RouterGroup) (sessId string) {
|
||||||
|
return AuthenticateUser(app, router, "admin", "photoprism")
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthenticateUser Register session routes and returns valid SessionId.
|
||||||
|
// Call this func after registering other routes and before performing other requests.
|
||||||
|
func AuthenticateUser(app *gin.Engine, router *gin.RouterGroup, name string, password string) (sessId string) {
|
||||||
|
CreateSession(router)
|
||||||
|
|
||||||
|
f := form.Login{
|
||||||
|
UserName: name,
|
||||||
|
Password: password,
|
||||||
|
}
|
||||||
|
|
||||||
|
loginStr, err := json.Marshal(f)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/session", string(loginStr))
|
||||||
|
sessId = r.Header().Get(session.Header)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performs authenticated API request with empty request body.
|
||||||
|
func AuthenticatedRequest(r http.Handler, method, path, sess string) *httptest.ResponseRecorder {
|
||||||
|
req, _ := http.NewRequest(method, path, nil)
|
||||||
|
req.Header.Add(session.Header, sess)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performs an authenticated API request containing the request body as a string.
|
||||||
|
func AuthenticatedRequestWithBody(r http.Handler, method, path, body string, sess string) *httptest.ResponseRecorder {
|
||||||
|
reader := strings.NewReader(body)
|
||||||
|
req, _ := http.NewRequest(method, path, reader)
|
||||||
|
req.Header.Add(session.Header, sess)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
return w
|
||||||
|
}
|
||||||
24
internal/api/auth_tokens.go
Normal file
24
internal/api/auth_tokens.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InvalidPreviewToken checks if the token found in the request is valid for image thumbnails and video streams.
|
||||||
|
func InvalidPreviewToken(c *gin.Context) bool {
|
||||||
|
token := clean.UrlToken(c.Param("token"))
|
||||||
|
|
||||||
|
if token == "" {
|
||||||
|
token = clean.UrlToken(c.Query("t"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return service.Config().InvalidPreviewToken(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvalidDownloadToken checks if the token found in the request is valid for file downloads.
|
||||||
|
func InvalidDownloadToken(c *gin.Context) bool {
|
||||||
|
return service.Config().InvalidDownloadToken(clean.UrlToken(c.Query("t")))
|
||||||
|
}
|
||||||
@@ -25,10 +25,9 @@ import (
|
|||||||
// POST /api/v1/batch/photos/archive
|
// POST /api/v1/batch/photos/archive
|
||||||
func BatchPhotosArchive(router *gin.RouterGroup) {
|
func BatchPhotosArchive(router *gin.RouterGroup) {
|
||||||
router.POST("/batch/photos/archive", func(c *gin.Context) {
|
router.POST("/batch/photos/archive", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionDelete)
|
s := Auth(c, acl.ResourcePhotos, acl.ActionDelete)
|
||||||
|
|
||||||
if s.Invalid() {
|
if s.Abort(c) {
|
||||||
AbortUnauthorized(c)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,10 +88,9 @@ func BatchPhotosArchive(router *gin.RouterGroup) {
|
|||||||
// POST /api/v1/batch/photos/restore
|
// POST /api/v1/batch/photos/restore
|
||||||
func BatchPhotosRestore(router *gin.RouterGroup) {
|
func BatchPhotosRestore(router *gin.RouterGroup) {
|
||||||
router.POST("/batch/photos/restore", func(c *gin.Context) {
|
router.POST("/batch/photos/restore", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionDelete)
|
s := Auth(c, acl.ResourcePhotos, acl.ActionDelete)
|
||||||
|
|
||||||
if s.Invalid() {
|
if s.Abort(c) {
|
||||||
AbortUnauthorized(c)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,10 +150,9 @@ func BatchPhotosRestore(router *gin.RouterGroup) {
|
|||||||
// POST /api/v1/batch/photos/approve
|
// POST /api/v1/batch/photos/approve
|
||||||
func BatchPhotosApprove(router *gin.RouterGroup) {
|
func BatchPhotosApprove(router *gin.RouterGroup) {
|
||||||
router.POST("batch/photos/approve", func(c *gin.Context) {
|
router.POST("batch/photos/approve", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate)
|
s := Auth(c, acl.ResourcePhotos, acl.ActionUpdate)
|
||||||
|
|
||||||
if s.Invalid() {
|
if s.Abort(c) {
|
||||||
AbortUnauthorized(c)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,10 +202,9 @@ func BatchPhotosApprove(router *gin.RouterGroup) {
|
|||||||
// POST /api/v1/batch/albums/delete
|
// POST /api/v1/batch/albums/delete
|
||||||
func BatchAlbumsDelete(router *gin.RouterGroup) {
|
func BatchAlbumsDelete(router *gin.RouterGroup) {
|
||||||
router.POST("/batch/albums/delete", func(c *gin.Context) {
|
router.POST("/batch/albums/delete", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionDelete)
|
s := Auth(c, acl.ResourceAlbums, acl.ActionDelete)
|
||||||
|
|
||||||
if s.Invalid() {
|
if s.Abort(c) {
|
||||||
AbortUnauthorized(c)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,10 +243,9 @@ func BatchAlbumsDelete(router *gin.RouterGroup) {
|
|||||||
// POST /api/v1/batch/photos/private
|
// POST /api/v1/batch/photos/private
|
||||||
func BatchPhotosPrivate(router *gin.RouterGroup) {
|
func BatchPhotosPrivate(router *gin.RouterGroup) {
|
||||||
router.POST("/batch/photos/private", func(c *gin.Context) {
|
router.POST("/batch/photos/private", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionPrivate)
|
s := Auth(c, acl.ResourcePhotos, acl.AccessPrivate)
|
||||||
|
|
||||||
if s.Invalid() {
|
if s.Abort(c) {
|
||||||
AbortUnauthorized(c)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,10 +295,9 @@ func BatchPhotosPrivate(router *gin.RouterGroup) {
|
|||||||
// POST /api/v1/batch/labels/delete
|
// POST /api/v1/batch/labels/delete
|
||||||
func BatchLabelsDelete(router *gin.RouterGroup) {
|
func BatchLabelsDelete(router *gin.RouterGroup) {
|
||||||
router.POST("/batch/labels/delete", func(c *gin.Context) {
|
router.POST("/batch/labels/delete", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourceLabels, acl.ActionDelete)
|
s := Auth(c, acl.ResourceLabels, acl.ActionDelete)
|
||||||
|
|
||||||
if s.Invalid() {
|
if s.Abort(c) {
|
||||||
AbortUnauthorized(c)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,10 +340,9 @@ func BatchLabelsDelete(router *gin.RouterGroup) {
|
|||||||
// POST /api/v1/batch/photos/delete
|
// POST /api/v1/batch/photos/delete
|
||||||
func BatchPhotosDelete(router *gin.RouterGroup) {
|
func BatchPhotosDelete(router *gin.RouterGroup) {
|
||||||
router.POST("/batch/photos/delete", func(c *gin.Context) {
|
router.POST("/batch/photos/delete", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionDelete)
|
s := Auth(c, acl.ResourcePhotos, acl.ActionDelete)
|
||||||
|
|
||||||
if s.Invalid() {
|
if s.Abort(c) {
|
||||||
AbortUnauthorized(c)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,40 +16,17 @@ import (
|
|||||||
"github.com/photoprism/photoprism/pkg/fs"
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetConfig returns client config values.
|
|
||||||
//
|
|
||||||
// GET /api/v1/config
|
|
||||||
func GetConfig(router *gin.RouterGroup) {
|
|
||||||
router.GET("/config", func(c *gin.Context) {
|
|
||||||
s := Auth(SessionID(c), acl.ResourceConfig, acl.ActionRead)
|
|
||||||
|
|
||||||
if s.Invalid() {
|
|
||||||
AbortUnauthorized(c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
conf := service.Config()
|
|
||||||
|
|
||||||
if s.User.IsGuest() {
|
|
||||||
c.JSON(http.StatusOK, conf.GuestConfig())
|
|
||||||
} else if s.User.IsRegistered() {
|
|
||||||
c.JSON(http.StatusOK, conf.UserConfig())
|
|
||||||
} else {
|
|
||||||
c.JSON(http.StatusOK, conf.PublicConfig())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetConfigOptions returns backend config options.
|
// GetConfigOptions returns backend config options.
|
||||||
//
|
//
|
||||||
// GET /api/v1/config/options
|
// GET /api/v1/config/options
|
||||||
func GetConfigOptions(router *gin.RouterGroup) {
|
func GetConfigOptions(router *gin.RouterGroup) {
|
||||||
router.GET("/config/options", func(c *gin.Context) {
|
router.GET("/config/options", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourceConfigOptions, acl.ActionRead)
|
s := Auth(c, acl.ResourceConfig, acl.AccessAll)
|
||||||
conf := service.Config()
|
conf := service.Config()
|
||||||
|
|
||||||
|
// Abort if permission was not granted.
|
||||||
if s.Invalid() || conf.Public() || conf.DisableSettings() {
|
if s.Invalid() || conf.Public() || conf.DisableSettings() {
|
||||||
AbortUnauthorized(c)
|
AbortForbidden(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,11 +39,11 @@ func GetConfigOptions(router *gin.RouterGroup) {
|
|||||||
// POST /api/v1/config/options
|
// POST /api/v1/config/options
|
||||||
func SaveConfigOptions(router *gin.RouterGroup) {
|
func SaveConfigOptions(router *gin.RouterGroup) {
|
||||||
router.POST("/config/options", func(c *gin.Context) {
|
router.POST("/config/options", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourceConfigOptions, acl.ActionUpdate)
|
s := Auth(c, acl.ResourceConfig, acl.ActionManage)
|
||||||
conf := service.Config()
|
conf := service.Config()
|
||||||
|
|
||||||
if s.Invalid() || conf.Public() || conf.DisableSettings() {
|
if s.Invalid() || conf.Public() || conf.DisableSettings() {
|
||||||
AbortUnauthorized(c)
|
AbortForbidden(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
func TestGetConfig(t *testing.T) {
|
func TestGetConfig(t *testing.T) {
|
||||||
t.Run("successful request", func(t *testing.T) {
|
t.Run("successful request", func(t *testing.T) {
|
||||||
app, router, _ := NewApiTest()
|
app, router, _ := NewApiTest()
|
||||||
GetConfig(router)
|
GetClientConfig(router)
|
||||||
r := PerformRequest(app, "GET", "/api/v1/config")
|
r := PerformRequest(app, "GET", "/api/v1/config")
|
||||||
val := gjson.Get(r.Body.String(), "flags")
|
val := gjson.Get(r.Body.String(), "flags")
|
||||||
assert.Equal(t, "public debug test sponsor experimental settings", val.String())
|
assert.Equal(t, "public debug test sponsor experimental settings", val.String())
|
||||||
@@ -25,7 +25,7 @@ func TestGetConfigOptions(t *testing.T) {
|
|||||||
app, router, _ := NewApiTest()
|
app, router, _ := NewApiTest()
|
||||||
GetConfigOptions(router)
|
GetConfigOptions(router)
|
||||||
r := PerformRequest(app, "GET", "/api/v1/config/options")
|
r := PerformRequest(app, "GET", "/api/v1/config/options")
|
||||||
assert.Equal(t, http.StatusUnauthorized, r.Code)
|
assert.Equal(t, http.StatusForbidden, r.Code)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +34,6 @@ func TestSaveConfigOptions(t *testing.T) {
|
|||||||
app, router, _ := NewApiTest()
|
app, router, _ := NewApiTest()
|
||||||
SaveConfigOptions(router)
|
SaveConfigOptions(router)
|
||||||
r := PerformRequest(app, "POST", "/api/v1/config/options")
|
r := PerformRequest(app, "POST", "/api/v1/config/options")
|
||||||
assert.Equal(t, http.StatusUnauthorized, r.Code)
|
assert.Equal(t, http.StatusForbidden, r.Code)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
102
internal/api/config_settings.go
Normal file
102
internal/api/config_settings.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/customize"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/acl"
|
||||||
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
|
"github.com/photoprism/photoprism/internal/i18n"
|
||||||
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetSettings returns the user app settings as JSON.
|
||||||
|
//
|
||||||
|
// GET /api/v1/settings
|
||||||
|
func GetSettings(router *gin.RouterGroup) {
|
||||||
|
router.GET("/settings", func(c *gin.Context) {
|
||||||
|
s := AuthAny(c, acl.ResourceSettings, acl.Permissions{acl.AccessAll, acl.AccessOwn})
|
||||||
|
|
||||||
|
// Abort if permission was not granted.
|
||||||
|
if s.Abort(c) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
settings := service.Config().SessionSettings(s)
|
||||||
|
|
||||||
|
if settings == nil {
|
||||||
|
Abort(c, http.StatusNotFound, i18n.ErrNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, settings)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveSettings saved the user app settings.
|
||||||
|
//
|
||||||
|
// POST /api/v1/settings
|
||||||
|
func SaveSettings(router *gin.RouterGroup) {
|
||||||
|
router.POST("/settings", func(c *gin.Context) {
|
||||||
|
s := AuthAny(c, acl.ResourceSettings, acl.Permissions{acl.ActionUpdate, acl.ActionManage})
|
||||||
|
|
||||||
|
// Abort if permission was not granted.
|
||||||
|
if s.Abort(c) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
conf := service.Config()
|
||||||
|
|
||||||
|
if conf.DisableSettings() {
|
||||||
|
AbortForbidden(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var settings *customize.Settings
|
||||||
|
|
||||||
|
if acl.Resources.Allow(acl.ResourceSettings, s.User().AclRole(), acl.ActionManage) {
|
||||||
|
settings = conf.Settings()
|
||||||
|
|
||||||
|
if err := c.BindJSON(settings); err != nil {
|
||||||
|
AbortBadRequest(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := settings.Save(conf.SettingsYaml()); err != nil {
|
||||||
|
log.Debugf("config: %s (save app settings)", err)
|
||||||
|
c.AbortWithStatusJSON(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateClientConfig()
|
||||||
|
} else {
|
||||||
|
user := s.User()
|
||||||
|
|
||||||
|
if user == nil {
|
||||||
|
AbortUnexpected(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
settings = &customize.Settings{}
|
||||||
|
|
||||||
|
if err := c.BindJSON(settings); err != nil {
|
||||||
|
AbortBadRequest(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply to user preferences and keep current values if unspecified.
|
||||||
|
if err := user.Settings().Apply(settings).Save(); err != nil {
|
||||||
|
log.Debugf("config: %s (save user settings)", err)
|
||||||
|
AbortSaveFailed(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event.InfoMsg(i18n.MsgSettingsSaved)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, service.Config().SessionSettings(s))
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestGetSettings(t *testing.T) {
|
func TestGetSettings(t *testing.T) {
|
||||||
t.Run("successful request", func(t *testing.T) {
|
t.Run("Success", func(t *testing.T) {
|
||||||
app, router, _ := NewApiTest()
|
app, router, _ := NewApiTest()
|
||||||
GetSettings(router)
|
GetSettings(router)
|
||||||
r := PerformRequest(app, "GET", "/api/v1/settings")
|
r := PerformRequest(app, "GET", "/api/v1/settings")
|
||||||
@@ -22,14 +22,13 @@ func TestGetSettings(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSaveSettings(t *testing.T) {
|
func TestSaveSettings(t *testing.T) {
|
||||||
t.Run("successful request", func(t *testing.T) {
|
t.Run("Success", func(t *testing.T) {
|
||||||
app, router, _ := NewApiTest()
|
app, router, _ := NewApiTest()
|
||||||
GetSettings(router)
|
GetSettings(router)
|
||||||
r := PerformRequest(app, "GET", "/api/v1/settings")
|
r := PerformRequest(app, "GET", "/api/v1/settings")
|
||||||
val := gjson.Get(r.Body.String(), "ui.language")
|
val := gjson.Get(r.Body.String(), "ui.language")
|
||||||
assert.Equal(t, "en", val.String())
|
assert.Equal(t, "en", val.String())
|
||||||
assert.Equal(t, http.StatusOK, r.Code)
|
assert.Equal(t, http.StatusOK, r.Code)
|
||||||
|
|
||||||
SaveSettings(router)
|
SaveSettings(router)
|
||||||
r2 := PerformRequestWithBody(app, "POST", "/api/v1/settings", `{"ui":{"language": "de"}}`)
|
r2 := PerformRequestWithBody(app, "POST", "/api/v1/settings", `{"ui":{"language": "de"}}`)
|
||||||
assert.Equal(t, http.StatusOK, r2.Code)
|
assert.Equal(t, http.StatusOK, r2.Code)
|
||||||
@@ -39,7 +38,7 @@ func TestSaveSettings(t *testing.T) {
|
|||||||
r3 := PerformRequestWithBody(app, "POST", "/api/v1/settings", `{"ui":{"language": "en"}}`)
|
r3 := PerformRequestWithBody(app, "POST", "/api/v1/settings", `{"ui":{"language": "en"}}`)
|
||||||
assert.Equal(t, http.StatusOK, r3.Code)
|
assert.Equal(t, http.StatusOK, r3.Code)
|
||||||
})
|
})
|
||||||
t.Run("bad request", func(t *testing.T) {
|
t.Run("BadRequest", func(t *testing.T) {
|
||||||
app, router, _ := NewApiTest()
|
app, router, _ := NewApiTest()
|
||||||
SaveSettings(router)
|
SaveSettings(router)
|
||||||
r := PerformRequestWithBody(app, "POST", "/api/v1/settings", `{"ui":{"language":123}}`)
|
r := PerformRequestWithBody(app, "POST", "/api/v1/settings", `{"ui":{"language":123}}`)
|
||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
// PUT /api/v1/connect/:name
|
// PUT /api/v1/connect/:name
|
||||||
func Connect(router *gin.RouterGroup) {
|
func Connect(router *gin.RouterGroup) {
|
||||||
router.PUT("/connect/:name", func(c *gin.Context) {
|
router.PUT("/connect/:name", func(c *gin.Context) {
|
||||||
name := clean.IdString(c.Param("name"))
|
name := clean.ID(c.Param("name"))
|
||||||
|
|
||||||
if name == "" {
|
if name == "" {
|
||||||
log.Errorf("connect: empty service name")
|
log.Errorf("connect: empty service name")
|
||||||
@@ -46,11 +46,11 @@ func Connect(router *gin.RouterGroup) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s := Auth(SessionID(c), acl.ResourceConfigOptions, acl.ActionUpdate)
|
s := Auth(c, acl.ResourceConfig, acl.ActionUpdate)
|
||||||
|
|
||||||
if s.Invalid() {
|
if s.Invalid() {
|
||||||
log.Errorf("connect: %s not authorized", clean.Log(s.User.Username))
|
log.Errorf("connect: %s not authorized", clean.Log(s.User().UserName))
|
||||||
AbortUnauthorized(c)
|
AbortForbidden(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ func AlbumCover(router *gin.RouterGroup) {
|
|||||||
start := time.Now()
|
start := time.Now()
|
||||||
conf := service.Config()
|
conf := service.Config()
|
||||||
thumbName := thumb.Name(clean.Token(c.Param("size")))
|
thumbName := thumb.Name(clean.Token(c.Param("size")))
|
||||||
uid := clean.IdString(c.Param("uid"))
|
uid := clean.UID(c.Param("uid"))
|
||||||
|
|
||||||
size, ok := thumb.Sizes[thumbName]
|
size, ok := thumb.Sizes[thumbName]
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ func LabelCover(router *gin.RouterGroup) {
|
|||||||
start := time.Now()
|
start := time.Now()
|
||||||
conf := service.Config()
|
conf := service.Config()
|
||||||
thumbName := thumb.Name(clean.Token(c.Param("size")))
|
thumbName := thumb.Name(clean.Token(c.Param("size")))
|
||||||
uid := clean.IdString(c.Param("uid"))
|
uid := clean.UID(c.Param("uid"))
|
||||||
|
|
||||||
size, ok := thumb.Sizes[thumbName]
|
size, ok := thumb.Sizes[thumbName]
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import (
|
|||||||
func DownloadAlbum(router *gin.RouterGroup) {
|
func DownloadAlbum(router *gin.RouterGroup) {
|
||||||
router.GET("/albums/:uid/dl", func(c *gin.Context) {
|
router.GET("/albums/:uid/dl", func(c *gin.Context) {
|
||||||
if InvalidDownloadToken(c) {
|
if InvalidDownloadToken(c) {
|
||||||
AbortUnauthorized(c)
|
AbortForbidden(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ func DownloadAlbum(router *gin.RouterGroup) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
a, err := query.AlbumByUID(clean.IdString(c.Param("uid")))
|
a, err := query.AlbumByUID(clean.UID(c.Param("uid")))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
AbortAlbumNotFound(c)
|
AbortAlbumNotFound(c)
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ package api
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/customize"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/entity"
|
|
||||||
"github.com/photoprism/photoprism/internal/photoprism"
|
"github.com/photoprism/photoprism/internal/photoprism"
|
||||||
"github.com/photoprism/photoprism/internal/query"
|
"github.com/photoprism/photoprism/internal/query"
|
||||||
"github.com/photoprism/photoprism/internal/service"
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
@@ -19,14 +20,14 @@ import (
|
|||||||
// TODO: GET /api/v1/dl/album/:uid
|
// TODO: GET /api/v1/dl/album/:uid
|
||||||
|
|
||||||
// DownloadName returns the download file name type.
|
// DownloadName returns the download file name type.
|
||||||
func DownloadName(c *gin.Context) entity.DownloadName {
|
func DownloadName(c *gin.Context) customize.DownloadName {
|
||||||
switch c.Query("name") {
|
switch c.Query("name") {
|
||||||
case "file":
|
case "file":
|
||||||
return entity.DownloadNameFile
|
return customize.DownloadNameFile
|
||||||
case "share":
|
case "share":
|
||||||
return entity.DownloadNameShare
|
return customize.DownloadNameShare
|
||||||
case "original":
|
case "original":
|
||||||
return entity.DownloadNameOriginal
|
return customize.DownloadNameOriginal
|
||||||
default:
|
default:
|
||||||
return service.Config().Settings().Download.Name
|
return service.Config().Settings().Download.Name
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"github.com/photoprism/photoprism/internal/i18n"
|
"github.com/photoprism/photoprism/internal/i18n"
|
||||||
"github.com/photoprism/photoprism/internal/query"
|
"github.com/photoprism/photoprism/internal/query"
|
||||||
"github.com/photoprism/photoprism/internal/service"
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/pkg/txt"
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,10 +18,9 @@ import (
|
|||||||
func GetErrors(router *gin.RouterGroup) {
|
func GetErrors(router *gin.RouterGroup) {
|
||||||
router.GET("/errors", func(c *gin.Context) {
|
router.GET("/errors", func(c *gin.Context) {
|
||||||
// Check authentication and authorization.
|
// Check authentication and authorization.
|
||||||
s := Auth(SessionID(c), acl.ResourceLogs, acl.ActionSearch)
|
s := Auth(c, acl.ResourceLogs, acl.ActionSearch)
|
||||||
|
|
||||||
if s.Invalid() {
|
if s.Abort(c) {
|
||||||
AbortUnauthorized(c)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,10 +55,9 @@ func DeleteErrors(router *gin.RouterGroup) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check authentication and authorization.
|
// Check authentication and authorization.
|
||||||
s := Auth(SessionID(c), acl.ResourceLogs, acl.ActionDelete)
|
s := Auth(c, acl.ResourceLogs, acl.ActionDelete)
|
||||||
|
|
||||||
if s.Invalid() {
|
if s.Abort(c) {
|
||||||
AbortUnauthorized(c)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const (
|
|||||||
EntityUpdated EntityEvent = "updated"
|
EntityUpdated EntityEvent = "updated"
|
||||||
EntityCreated EntityEvent = "created"
|
EntityCreated EntityEvent = "created"
|
||||||
EntityDeleted EntityEvent = "deleted"
|
EntityDeleted EntityEvent = "deleted"
|
||||||
|
EntityReacted EntityEvent = "reacted"
|
||||||
)
|
)
|
||||||
|
|
||||||
func PublishPhotoEvent(e EntityEvent, uid string, c *gin.Context) {
|
func PublishPhotoEvent(e EntityEvent, uid string, c *gin.Context) {
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ package api
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/pkg/clean"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/acl"
|
"github.com/photoprism/photoprism/internal/acl"
|
||||||
@@ -13,6 +11,7 @@ import (
|
|||||||
"github.com/photoprism/photoprism/internal/form"
|
"github.com/photoprism/photoprism/internal/form"
|
||||||
"github.com/photoprism/photoprism/internal/i18n"
|
"github.com/photoprism/photoprism/internal/i18n"
|
||||||
"github.com/photoprism/photoprism/internal/search"
|
"github.com/photoprism/photoprism/internal/search"
|
||||||
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
"github.com/photoprism/photoprism/pkg/txt"
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,10 +20,10 @@ import (
|
|||||||
// GET /api/v1/faces/:id
|
// GET /api/v1/faces/:id
|
||||||
func GetFace(router *gin.RouterGroup) {
|
func GetFace(router *gin.RouterGroup) {
|
||||||
router.GET("/faces/:id", func(c *gin.Context) {
|
router.GET("/faces/:id", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourceSubjects, acl.ActionRead)
|
s := Auth(c, acl.ResourcePeople, acl.ActionView)
|
||||||
|
|
||||||
if s.Invalid() {
|
// Abort if permission was not granted.
|
||||||
AbortUnauthorized(c)
|
if s.Abort(c) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,10 +43,10 @@ func GetFace(router *gin.RouterGroup) {
|
|||||||
// PUT /api/v1/faces/:id
|
// PUT /api/v1/faces/:id
|
||||||
func UpdateFace(router *gin.RouterGroup) {
|
func UpdateFace(router *gin.RouterGroup) {
|
||||||
router.PUT("/faces/:id", func(c *gin.Context) {
|
router.PUT("/faces/:id", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourceSubjects, acl.ActionUpdate)
|
s := Auth(c, acl.ResourcePeople, acl.ActionUpdate)
|
||||||
|
|
||||||
if s.Invalid() {
|
// Abort if permission was not granted.
|
||||||
AbortUnauthorized(c)
|
if s.Abort(c) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,10 +17,10 @@ import (
|
|||||||
// GET /api/v1/faces
|
// GET /api/v1/faces
|
||||||
func SearchFaces(router *gin.RouterGroup) {
|
func SearchFaces(router *gin.RouterGroup) {
|
||||||
router.GET("/faces", func(c *gin.Context) {
|
router.GET("/faces", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourceSubjects, acl.ActionSearch)
|
s := Auth(c, acl.ResourcePeople, acl.ActionSearch)
|
||||||
|
|
||||||
if s.Invalid() {
|
// Abort if permission was not granted.
|
||||||
AbortUnauthorized(c)
|
if s.Abort(c) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,10 +23,10 @@ func SendFeedback(router *gin.RouterGroup) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s := Auth(SessionID(c), acl.ResourceFeedback, acl.ActionCreate)
|
s := Auth(c, acl.ResourceFeedback, acl.ActionCreate)
|
||||||
|
|
||||||
if s.Invalid() {
|
// Abort if permission was not granted.
|
||||||
AbortUnauthorized(c)
|
if s.Abort(c) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,10 +24,9 @@ import (
|
|||||||
// file_uid: string File UID as returned by the API
|
// file_uid: string File UID as returned by the API
|
||||||
func DeleteFile(router *gin.RouterGroup) {
|
func DeleteFile(router *gin.RouterGroup) {
|
||||||
router.DELETE("/photos/:uid/files/:file_uid", func(c *gin.Context) {
|
router.DELETE("/photos/:uid/files/:file_uid", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourceFiles, acl.ActionDelete)
|
s := Auth(c, acl.ResourceFiles, acl.ActionDelete)
|
||||||
|
|
||||||
if s.Invalid() {
|
if s.Abort(c) {
|
||||||
AbortUnauthorized(c)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,8 +37,8 @@ func DeleteFile(router *gin.RouterGroup) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
photoUID := clean.IdString(c.Param("uid"))
|
photoUID := clean.UID(c.Param("uid"))
|
||||||
fileUID := clean.IdString(c.Param("file_uid"))
|
fileUID := clean.UID(c.Param("file_uid"))
|
||||||
|
|
||||||
file, err := query.FileByUID(fileUID)
|
file, err := query.FileByUID(fileUID)
|
||||||
|
|
||||||
|
|||||||
@@ -3,24 +3,24 @@ package api
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/pkg/clean"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/acl"
|
"github.com/photoprism/photoprism/internal/acl"
|
||||||
"github.com/photoprism/photoprism/internal/query"
|
"github.com/photoprism/photoprism/internal/query"
|
||||||
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetFile returns file details as JSON.
|
// GetFile returns file details as JSON.
|
||||||
//
|
//
|
||||||
// Route: GET /api/v1/files/:hash
|
// GET /api/v1/files/:hash
|
||||||
// Params:
|
// Params:
|
||||||
// - hash (string) SHA-1 hash of the file
|
// - hash (string) SHA-1 hash of the file
|
||||||
func GetFile(router *gin.RouterGroup) {
|
func GetFile(router *gin.RouterGroup) {
|
||||||
router.GET("/files/:hash", func(c *gin.Context) {
|
router.GET("/files/:hash", func(c *gin.Context) {
|
||||||
s := Auth(SessionID(c), acl.ResourceFiles, acl.ActionRead)
|
s := Auth(c, acl.ResourceFiles, acl.ActionView)
|
||||||
|
|
||||||
if s.Invalid() {
|
// Abort if permission was not granted.
|
||||||
AbortUnauthorized(c)
|
if s.Abort(c) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user