Security: Use individual preview tokens for each user account #98
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
ccfdf22590
commit
884dea17de
119 changed files with 1394 additions and 581 deletions
18
Makefile
18
Makefile
|
@ -426,15 +426,29 @@ docker-release-impish:
|
|||
docker pull --platform=arm64 ubuntu:impish
|
||||
scripts/docker/buildx-multi.sh photoprism linux/amd64,linux/arm64 impish /impish
|
||||
start-local:
|
||||
docker-compose -f docker-compose.local.yml up -d
|
||||
docker-compose -f docker-compose.local.yml up -d --wait
|
||||
stop-local:
|
||||
docker-compose -f docker-compose.local.yml stop
|
||||
mysql:
|
||||
docker-compose -f docker-compose.mysql.yml pull mysql
|
||||
docker-compose -f docker-compose.mysql.yml stop mysql
|
||||
docker-compose -f docker-compose.mysql.yml up -d --wait mysql
|
||||
start-mysql:
|
||||
docker-compose -f docker-compose.mysql.yml up -d mysql
|
||||
docker-compose -f docker-compose.mysql.yml up -d --wait mysql
|
||||
stop-mysql:
|
||||
docker-compose -f docker-compose.mysql.yml stop mysql
|
||||
logs-mysql:
|
||||
docker-compose -f docker-compose.mysql.yml logs -f mysql
|
||||
latest:
|
||||
docker-compose -f docker-compose.latest.yml pull photoprism-latest
|
||||
docker-compose -f docker-compose.latest.yml stop photoprism-latest
|
||||
docker-compose -f docker-compose.latest.yml up -d --wait photoprism-latest
|
||||
start-latest:
|
||||
docker-compose -f docker-compose.latest.yml up -d --wait photoprism-latest
|
||||
stop-latest:
|
||||
docker-compose -f docker-compose.latest.yml stop photoprism-latest
|
||||
logs-latest:
|
||||
docker-compose -f docker-compose.latest.yml logs -f photoprism-latest
|
||||
docker-local: docker-local-bookworm
|
||||
docker-local-all: docker-local-bookworm docker-local-bullseye docker-local-buster docker-local-jammy
|
||||
docker-local-bookworm:
|
||||
|
|
|
@ -37,9 +37,9 @@ services:
|
|||
PHOTOPRISM_HTTP_COMPRESSION: "gzip" # improves transfer speed and bandwidth utilization (none or gzip)
|
||||
PHOTOPRISM_DATABASE_DRIVER: "mysql"
|
||||
PHOTOPRISM_DATABASE_SERVER: "mariadb:4001"
|
||||
PHOTOPRISM_DATABASE_NAME: "latest"
|
||||
PHOTOPRISM_DATABASE_USER: "latest"
|
||||
PHOTOPRISM_DATABASE_PASSWORD: "latest"
|
||||
PHOTOPRISM_DATABASE_NAME: "photoprism"
|
||||
PHOTOPRISM_DATABASE_USER: "root"
|
||||
PHOTOPRISM_DATABASE_PASSWORD: "photoprism"
|
||||
PHOTOPRISM_DISABLE_CHOWN: "false" # disables updating storage permissions via chmod and chown on startup
|
||||
PHOTOPRISM_DISABLE_BACKUPS: "false" # disables backing up albums and photo metadata to YAML files
|
||||
PHOTOPRISM_DISABLE_WEBDAV: "false" # disables built-in WebDAV server
|
||||
|
|
128
frontend/package-lock.json
generated
128
frontend/package-lock.json
generated
|
@ -2350,9 +2350,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.8.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.8.4.tgz",
|
||||
"integrity": "sha512-WdlVphvfR/GJCLEMbNA8lJ0lhFNBj4SW3O+O5/cEGw9oYrv0al9zTwuQsq+myDUXgNx2jgBynoVgZ2MMJ6pbow=="
|
||||
"version": "18.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.8.5.tgz",
|
||||
"integrity": "sha512-Bq7G3AErwe5A/Zki5fdD3O6+0zDChhg671NfPjtIcbtzDNZTv4NPKMRFr7gtYPG7y+B8uTiNK4Ngd9T0FTar6Q=="
|
||||
},
|
||||
"node_modules/@types/parse-json": {
|
||||
"version": "4.0.0",
|
||||
|
@ -3503,9 +3503,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001418",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz",
|
||||
"integrity": "sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg==",
|
||||
"version": "1.0.30001419",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001419.tgz",
|
||||
"integrity": "sha512-aFO1r+g6R7TW+PNQxKzjITwLOyDhVRLjW0LcwS/HCZGUUKTGNp9+IwLC4xyDSZBygVL/mxaFR3HIV6wEKQuSzw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
@ -4645,9 +4645,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.4.279",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.279.tgz",
|
||||
"integrity": "sha512-xs7vEuSZ84+JsHSTFqqG0TE3i8EAivHomRQZhhcRvsmnjsh5C2KdhwNKf4ZRYtzq75wojpFyqb62m32Oam57wA=="
|
||||
"version": "1.4.281",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.281.tgz",
|
||||
"integrity": "sha512-yer0w5wCYdFoZytfmbNhwiGI/3cW06+RV7E23ln4490DVMxs7PvYpbsrSmAiBn/V6gode8wvJlST2YfWgvzWIg=="
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
|
@ -5429,9 +5429,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-promise": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.0.1.tgz",
|
||||
"integrity": "sha512-uM4Tgo5u3UWQiroOyDEsYcVMOo7re3zmno0IZmB5auxoaQNIceAbXEkSt8RNrKtaYehARHG06pYK6K1JhtP0Zw==",
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.0.tgz",
|
||||
"integrity": "sha512-NYCfDZF/KHt27p06nFAttgWuFyIDSUMnNaJBIY1FY9GpBFhdT2vMG64HlFguSgcJeyM5by6Yr5csSOuJm60eXQ==",
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
},
|
||||
|
@ -6356,19 +6356,19 @@
|
|||
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ=="
|
||||
},
|
||||
"node_modules/flow-parser": {
|
||||
"version": "0.188.2",
|
||||
"resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.188.2.tgz",
|
||||
"integrity": "sha512-Qnvihm7h4YDgFVQV2h0TcLE421D20/giBg93Dtobj+CHRnZ39vmsbDPM9IenUBtZuY0vWRiJp6slOv7dvmlKbg==",
|
||||
"version": "0.189.0",
|
||||
"resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.189.0.tgz",
|
||||
"integrity": "sha512-dS8FC7Ek6UyhDkjoTBSvZNLvNI6ukDzUtuugaSlANQfVwdQwiIwAVVdqnbczHr5uuNLQc/mWCR0Ag6nPUIBH9g==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/flow-remove-types": {
|
||||
"version": "2.188.2",
|
||||
"resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.188.2.tgz",
|
||||
"integrity": "sha512-X2KZLLzpBMLkt+4rCJKQquK9hIHaHbY0BFE0XTbfpPcBDOa/XVDL3RW9/Qb7X9AM2x9T6ex5vetnGIHAV+j0fA==",
|
||||
"version": "2.189.0",
|
||||
"resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.189.0.tgz",
|
||||
"integrity": "sha512-adE+pINez4sWdpTDtQtY3+0bY+G+7s8z75GTyoPUTDLmxjFmjR3BJctHy130fyaxJQ+7bH5pyg1KSDjTFOaceg==",
|
||||
"dependencies": {
|
||||
"flow-parser": "^0.188.2",
|
||||
"flow-parser": "^0.189.0",
|
||||
"pirates": "^3.0.2",
|
||||
"vlq": "^0.2.1"
|
||||
},
|
||||
|
@ -9236,9 +9236,9 @@
|
|||
"integrity": "sha512-sk96pUvpNwDV6PLrnhr68Uu1S5NohsxqLKz0GuracgrDo40BdF/r1RhHnjakUk6Q4Z0OKIybOQ7GevLKGN1iYw=="
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.17",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.17.tgz",
|
||||
"integrity": "sha512-UNxNOLQydcOFi41yHNMcKRZ39NeXlr8AxGuZJsdub8vIb12fHzcq37DTU/QtbI6WLxNg2gF9Z+8qtRwTj1UI1Q==",
|
||||
"version": "8.4.18",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz",
|
||||
"integrity": "sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
@ -12444,11 +12444,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "2.7.11",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-2.7.11.tgz",
|
||||
"integrity": "sha512-VPAW5QelT7Tx6UoSw/cwx/jDROOKAK1y/Q0o7HkmVJ1WAypE7w1+UoFa+KsGxy1aYdHPU1oODB3vR6XwSfVhDg==",
|
||||
"version": "2.7.12",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-2.7.12.tgz",
|
||||
"integrity": "sha512-yRS44vPsCj6b5IZQHdEYqIwnay8stCnL8RsaVsm5aGtOhka00aoG+3ybaBAELDsXtNlzECe8myb2ukdzn19IOg==",
|
||||
"dependencies": {
|
||||
"@vue/compiler-sfc": "2.7.11",
|
||||
"@vue/compiler-sfc": "2.7.12",
|
||||
"csstype": "^3.1.0"
|
||||
}
|
||||
},
|
||||
|
@ -12698,9 +12698,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vue-template-compiler": {
|
||||
"version": "2.7.11",
|
||||
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.11.tgz",
|
||||
"integrity": "sha512-17QnXkFiBLOH3gGCA3nWAWpmdlTjOWLyP/2eg5ptgY1OvDBuIDGOW9FZ7ZSKmF1UFyf56mLR3/E1SlCTml1LWQ==",
|
||||
"version": "2.7.12",
|
||||
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.12.tgz",
|
||||
"integrity": "sha512-6rhJAuo2vRzJMs8X/pd9yqtsJmnPEnv4E0cb9KCu0sfGhoDt8roCCa/6qbrvpc1b38zYgdmY/xrk4qfNWZIjwA==",
|
||||
"dependencies": {
|
||||
"de-indent": "^1.0.2",
|
||||
"he": "^1.2.0"
|
||||
|
@ -12712,9 +12712,9 @@
|
|||
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw=="
|
||||
},
|
||||
"node_modules/vue/node_modules/@vue/compiler-sfc": {
|
||||
"version": "2.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.11.tgz",
|
||||
"integrity": "sha512-Cf8zvrZWjROgd8yPL8Tc+O3q/Y8ZGM0Y+8blrAvj1RQsVouzUY0oHcx8BA7Hybhb90JRnzeApFrlQGZRUdYpRw==",
|
||||
"version": "2.7.12",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.12.tgz",
|
||||
"integrity": "sha512-7FOITA2+4ND7lMAfegljHBpNSG3X9mVzgQwcS3g928QZM1EADedUw2JLKcgOm1ZEJEkvyDHh6lwa08vrLmoCOA==",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.18.4",
|
||||
"postcss": "^8.4.14",
|
||||
|
@ -14877,9 +14877,9 @@
|
|||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "18.8.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.8.4.tgz",
|
||||
"integrity": "sha512-WdlVphvfR/GJCLEMbNA8lJ0lhFNBj4SW3O+O5/cEGw9oYrv0al9zTwuQsq+myDUXgNx2jgBynoVgZ2MMJ6pbow=="
|
||||
"version": "18.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.8.5.tgz",
|
||||
"integrity": "sha512-Bq7G3AErwe5A/Zki5fdD3O6+0zDChhg671NfPjtIcbtzDNZTv4NPKMRFr7gtYPG7y+B8uTiNK4Ngd9T0FTar6Q=="
|
||||
},
|
||||
"@types/parse-json": {
|
||||
"version": "4.0.0",
|
||||
|
@ -15775,9 +15775,9 @@
|
|||
}
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30001418",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz",
|
||||
"integrity": "sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg=="
|
||||
"version": "1.0.30001419",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001419.tgz",
|
||||
"integrity": "sha512-aFO1r+g6R7TW+PNQxKzjITwLOyDhVRLjW0LcwS/HCZGUUKTGNp9+IwLC4xyDSZBygVL/mxaFR3HIV6wEKQuSzw=="
|
||||
},
|
||||
"chai": {
|
||||
"version": "4.3.6",
|
||||
|
@ -16596,9 +16596,9 @@
|
|||
}
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.4.279",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.279.tgz",
|
||||
"integrity": "sha512-xs7vEuSZ84+JsHSTFqqG0TE3i8EAivHomRQZhhcRvsmnjsh5C2KdhwNKf4ZRYtzq75wojpFyqb62m32Oam57wA=="
|
||||
"version": "1.4.281",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.281.tgz",
|
||||
"integrity": "sha512-yer0w5wCYdFoZytfmbNhwiGI/3cW06+RV7E23ln4490DVMxs7PvYpbsrSmAiBn/V6gode8wvJlST2YfWgvzWIg=="
|
||||
},
|
||||
"emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
|
@ -17306,9 +17306,9 @@
|
|||
}
|
||||
},
|
||||
"eslint-plugin-promise": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.0.1.tgz",
|
||||
"integrity": "sha512-uM4Tgo5u3UWQiroOyDEsYcVMOo7re3zmno0IZmB5auxoaQNIceAbXEkSt8RNrKtaYehARHG06pYK6K1JhtP0Zw==",
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.0.tgz",
|
||||
"integrity": "sha512-NYCfDZF/KHt27p06nFAttgWuFyIDSUMnNaJBIY1FY9GpBFhdT2vMG64HlFguSgcJeyM5by6Yr5csSOuJm60eXQ==",
|
||||
"requires": {}
|
||||
},
|
||||
"eslint-plugin-vue": {
|
||||
|
@ -17855,16 +17855,16 @@
|
|||
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ=="
|
||||
},
|
||||
"flow-parser": {
|
||||
"version": "0.188.2",
|
||||
"resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.188.2.tgz",
|
||||
"integrity": "sha512-Qnvihm7h4YDgFVQV2h0TcLE421D20/giBg93Dtobj+CHRnZ39vmsbDPM9IenUBtZuY0vWRiJp6slOv7dvmlKbg=="
|
||||
"version": "0.189.0",
|
||||
"resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.189.0.tgz",
|
||||
"integrity": "sha512-dS8FC7Ek6UyhDkjoTBSvZNLvNI6ukDzUtuugaSlANQfVwdQwiIwAVVdqnbczHr5uuNLQc/mWCR0Ag6nPUIBH9g=="
|
||||
},
|
||||
"flow-remove-types": {
|
||||
"version": "2.188.2",
|
||||
"resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.188.2.tgz",
|
||||
"integrity": "sha512-X2KZLLzpBMLkt+4rCJKQquK9hIHaHbY0BFE0XTbfpPcBDOa/XVDL3RW9/Qb7X9AM2x9T6ex5vetnGIHAV+j0fA==",
|
||||
"version": "2.189.0",
|
||||
"resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.189.0.tgz",
|
||||
"integrity": "sha512-adE+pINez4sWdpTDtQtY3+0bY+G+7s8z75GTyoPUTDLmxjFmjR3BJctHy130fyaxJQ+7bH5pyg1KSDjTFOaceg==",
|
||||
"requires": {
|
||||
"flow-parser": "^0.188.2",
|
||||
"flow-parser": "^0.189.0",
|
||||
"pirates": "^3.0.2",
|
||||
"vlq": "^0.2.1"
|
||||
},
|
||||
|
@ -19934,9 +19934,9 @@
|
|||
"integrity": "sha512-sk96pUvpNwDV6PLrnhr68Uu1S5NohsxqLKz0GuracgrDo40BdF/r1RhHnjakUk6Q4Z0OKIybOQ7GevLKGN1iYw=="
|
||||
},
|
||||
"postcss": {
|
||||
"version": "8.4.17",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.17.tgz",
|
||||
"integrity": "sha512-UNxNOLQydcOFi41yHNMcKRZ39NeXlr8AxGuZJsdub8vIb12fHzcq37DTU/QtbI6WLxNg2gF9Z+8qtRwTj1UI1Q==",
|
||||
"version": "8.4.18",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz",
|
||||
"integrity": "sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==",
|
||||
"requires": {
|
||||
"nanoid": "^3.3.4",
|
||||
"picocolors": "^1.0.0",
|
||||
|
@ -22106,18 +22106,18 @@
|
|||
}
|
||||
},
|
||||
"vue": {
|
||||
"version": "2.7.11",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-2.7.11.tgz",
|
||||
"integrity": "sha512-VPAW5QelT7Tx6UoSw/cwx/jDROOKAK1y/Q0o7HkmVJ1WAypE7w1+UoFa+KsGxy1aYdHPU1oODB3vR6XwSfVhDg==",
|
||||
"version": "2.7.12",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-2.7.12.tgz",
|
||||
"integrity": "sha512-yRS44vPsCj6b5IZQHdEYqIwnay8stCnL8RsaVsm5aGtOhka00aoG+3ybaBAELDsXtNlzECe8myb2ukdzn19IOg==",
|
||||
"requires": {
|
||||
"@vue/compiler-sfc": "2.7.11",
|
||||
"@vue/compiler-sfc": "2.7.12",
|
||||
"csstype": "^3.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vue/compiler-sfc": {
|
||||
"version": "2.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.11.tgz",
|
||||
"integrity": "sha512-Cf8zvrZWjROgd8yPL8Tc+O3q/Y8ZGM0Y+8blrAvj1RQsVouzUY0oHcx8BA7Hybhb90JRnzeApFrlQGZRUdYpRw==",
|
||||
"version": "2.7.12",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.12.tgz",
|
||||
"integrity": "sha512-7FOITA2+4ND7lMAfegljHBpNSG3X9mVzgQwcS3g928QZM1EADedUw2JLKcgOm1ZEJEkvyDHh6lwa08vrLmoCOA==",
|
||||
"requires": {
|
||||
"@babel/parser": "^7.18.4",
|
||||
"postcss": "^8.4.14",
|
||||
|
@ -22307,9 +22307,9 @@
|
|||
}
|
||||
},
|
||||
"vue-template-compiler": {
|
||||
"version": "2.7.11",
|
||||
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.11.tgz",
|
||||
"integrity": "sha512-17QnXkFiBLOH3gGCA3nWAWpmdlTjOWLyP/2eg5ptgY1OvDBuIDGOW9FZ7ZSKmF1UFyf56mLR3/E1SlCTml1LWQ==",
|
||||
"version": "2.7.12",
|
||||
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.12.tgz",
|
||||
"integrity": "sha512-6rhJAuo2vRzJMs8X/pd9yqtsJmnPEnv4E0cb9KCu0sfGhoDt8roCCa/6qbrvpc1b38zYgdmY/xrk4qfNWZIjwA==",
|
||||
"requires": {
|
||||
"de-indent": "^1.0.2",
|
||||
"he": "^1.2.0"
|
||||
|
|
|
@ -78,9 +78,13 @@ Api.interceptors.response.use(
|
|||
// Update preview token.
|
||||
if (resp.headers && resp.headers["x-preview-token"]) {
|
||||
const previewToken = resp.headers["x-preview-token"];
|
||||
const downloadToken = resp.headers["x-download-token"]
|
||||
? resp.headers["x-download-token"]
|
||||
: "";
|
||||
if (config.previewToken !== previewToken) {
|
||||
config.previewToken = previewToken;
|
||||
Event.publish("config.updated", { config: { previewToken } });
|
||||
config.downloadToken = downloadToken;
|
||||
Event.publish("config.updated", { config: { previewToken, downloadToken } });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -42,6 +42,8 @@ export default class Config {
|
|||
this.disconnected = false;
|
||||
this.storage = storage;
|
||||
this.storage_key = "config";
|
||||
this.previewToken = "";
|
||||
this.downloadToken = "";
|
||||
|
||||
this.$vuetify = null;
|
||||
this.translations = translations;
|
||||
|
@ -101,6 +103,8 @@ export default class Config {
|
|||
this.test = !!values.test;
|
||||
this.demo = !!values.demo;
|
||||
|
||||
this.updateTokens();
|
||||
|
||||
Event.subscribe("config.updated", (ev, data) => this.setValues(data.config));
|
||||
Event.subscribe("count", (ev, data) => this.onCount(ev, data));
|
||||
Event.subscribe("people", (ev, data) => this.onPeople(ev, data));
|
||||
|
@ -150,6 +154,8 @@ export default class Config {
|
|||
}
|
||||
}
|
||||
|
||||
this.updateTokens();
|
||||
|
||||
if (values.settings) {
|
||||
this.setBatchSize(values.settings);
|
||||
this.setLanguage(values.settings.ui.language);
|
||||
|
@ -519,12 +525,13 @@ export default class Config {
|
|||
return Languages().some((lang) => lang.value === this.values.settings.ui.language && lang.rtl);
|
||||
}
|
||||
|
||||
downloadToken() {
|
||||
return this.values["downloadToken"];
|
||||
}
|
||||
|
||||
previewToken() {
|
||||
return this.values["previewToken"];
|
||||
updateTokens() {
|
||||
if (this.values["previewToken"]) {
|
||||
this.previewToken = this.values.previewToken;
|
||||
}
|
||||
if (this.values["downloadToken"]) {
|
||||
this.downloadToken = this.values.downloadToken;
|
||||
}
|
||||
}
|
||||
|
||||
albumCategories() {
|
||||
|
|
|
@ -203,7 +203,7 @@ export default {
|
|||
|
||||
Notify.success(this.$gettext("Downloading…"));
|
||||
|
||||
this.onDownload(`${this.$config.apiUri}/albums/${this.selection[0]}/dl?t=${this.$config.downloadToken()}`);
|
||||
this.onDownload(`${this.$config.apiUri}/albums/${this.selection[0]}/dl?t=${this.$config.downloadToken}`);
|
||||
|
||||
this.expanded = false;
|
||||
},
|
||||
|
|
|
@ -182,7 +182,7 @@ export default {
|
|||
}
|
||||
},
|
||||
download() {
|
||||
this.onDownload(`${this.$config.apiUri}/albums/${this.album.UID}/dl?t=${this.$config.downloadToken()}`);
|
||||
this.onDownload(`${this.$config.apiUri}/albums/${this.album.UID}/dl?t=${this.$config.downloadToken}`);
|
||||
},
|
||||
onDownload(path) {
|
||||
Notify.success(this.$gettext("Downloading…"));
|
||||
|
|
|
@ -101,7 +101,7 @@ export default {
|
|||
},
|
||||
download() {
|
||||
Api.post("zip", {"files": this.selection}).then(r => {
|
||||
this.onDownload(`${this.$config.apiUri}/zip/${r.data.filename}?t=${this.$config.downloadToken()}`);
|
||||
this.onDownload(`${this.$config.apiUri}/zip/${r.data.filename}?t=${this.$config.downloadToken}`);
|
||||
});
|
||||
|
||||
this.expanded = false;
|
||||
|
|
|
@ -147,7 +147,7 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
this.onDownload(`${this.$config.apiUri}/labels/${this.selection[0]}/dl?t=${this.$config.downloadToken()}`);
|
||||
this.onDownload(`${this.$config.apiUri}/labels/${this.selection[0]}/dl?t=${this.$config.downloadToken}`);
|
||||
|
||||
this.expanded = false;
|
||||
},
|
||||
|
|
|
@ -374,7 +374,7 @@ export default {
|
|||
Notify.success(this.$gettext("Downloading…"));
|
||||
|
||||
const photo = this.photos[index];
|
||||
download(`${this.$config.apiUri}/dl/${photo.Hash}?t=${this.$config.downloadToken()}`, photo.FileName);
|
||||
download(`${this.$config.apiUri}/dl/${photo.Hash}?t=${this.$config.downloadToken}`, photo.FileName);
|
||||
},
|
||||
toggleLike(ev, index) {
|
||||
const inputType = this.input.eval(ev, index);
|
||||
|
|
|
@ -356,7 +356,7 @@ export default {
|
|||
default:
|
||||
Api.post("zip", {"photos": this.selection})
|
||||
.then(r => {
|
||||
this.onDownload(`${this.$config.apiUri}/zip/${r.data.filename}?t=${this.$config.downloadToken()}`);
|
||||
this.onDownload(`${this.$config.apiUri}/zip/${r.data.filename}?t=${this.$config.downloadToken}`);
|
||||
})
|
||||
.finally(() => {
|
||||
this.busy = false;
|
||||
|
|
|
@ -264,7 +264,7 @@ export default {
|
|||
Notify.success(this.$gettext("Downloading…"));
|
||||
|
||||
const photo = this.photos[index];
|
||||
download(`${this.$config.apiUri}/dl/${photo.Hash}?t=${this.$config.downloadToken()}`, photo.FileName);
|
||||
download(`${this.$config.apiUri}/dl/${photo.Hash}?t=${this.$config.downloadToken}`, photo.FileName);
|
||||
},
|
||||
onSelect(ev, index) {
|
||||
if (ev.shiftKey) {
|
||||
|
|
|
@ -118,7 +118,7 @@ export default {
|
|||
Notify.success(this.$gettext("Downloading…"));
|
||||
|
||||
Api.post("zip", {"subjects": this.selection}).then(r => {
|
||||
this.onDownload(`${this.$config.apiUri}/zip/${r.data.filename}?t=${this.$config.downloadToken()}`);
|
||||
this.onDownload(`${this.$config.apiUri}/zip/${r.data.filename}?t=${this.$config.downloadToken}`);
|
||||
});
|
||||
|
||||
this.expanded = false;
|
||||
|
|
|
@ -136,9 +136,9 @@ export class Album extends RestModel {
|
|||
|
||||
thumbnailUrl(size) {
|
||||
if (this.Thumb) {
|
||||
return `${config.contentUri}/t/${this.Thumb}/${config.previewToken()}/${size}`;
|
||||
return `${config.contentUri}/t/${this.Thumb}/${config.previewToken}/${size}`;
|
||||
} else if (this.UID) {
|
||||
return `${config.contentUri}/albums/${this.UID}/t/${config.previewToken()}/${size}`;
|
||||
return `${config.contentUri}/albums/${this.UID}/t/${config.previewToken}/${size}`;
|
||||
} else {
|
||||
return `${config.contentUri}/svg/album`;
|
||||
}
|
||||
|
|
|
@ -91,7 +91,7 @@ export class Face extends RestModel {
|
|||
}
|
||||
|
||||
if (this.Thumb) {
|
||||
return `${config.contentUri}/t/${this.Thumb}/${config.previewToken()}/${size}`;
|
||||
return `${config.contentUri}/t/${this.Thumb}/${config.previewToken}/${size}`;
|
||||
} else {
|
||||
return `${config.contentUri}/svg/portrait`;
|
||||
}
|
||||
|
|
|
@ -124,11 +124,11 @@ export class File extends RestModel {
|
|||
return `${config.contentUri}/svg/file`;
|
||||
}
|
||||
|
||||
return `${config.contentUri}/t/${this.Hash}/${config.previewToken()}/${size}`;
|
||||
return `${config.contentUri}/t/${this.Hash}/${config.previewToken}/${size}`;
|
||||
}
|
||||
|
||||
getDownloadUrl() {
|
||||
return `${config.apiUri}/dl/${this.Hash}?t=${config.downloadToken()}`;
|
||||
return `${config.apiUri}/dl/${this.Hash}?t=${config.downloadToken}`;
|
||||
}
|
||||
|
||||
download() {
|
||||
|
|
|
@ -93,7 +93,7 @@ export class Folder extends RestModel {
|
|||
}
|
||||
|
||||
thumbnailUrl(size) {
|
||||
return `${config.contentUri}/folders/t/${this.UID}/${config.previewToken()}/${size}`;
|
||||
return `${config.contentUri}/folders/t/${this.UID}/${config.previewToken}/${size}`;
|
||||
}
|
||||
|
||||
getDateString() {
|
||||
|
|
|
@ -73,9 +73,9 @@ export class Label extends RestModel {
|
|||
|
||||
thumbnailUrl(size) {
|
||||
if (this.Thumb) {
|
||||
return `${config.contentUri}/t/${this.Thumb}/${config.previewToken()}/${size}`;
|
||||
return `${config.contentUri}/t/${this.Thumb}/${config.previewToken}/${size}`;
|
||||
} else if (this.UID) {
|
||||
return `${config.contentUri}/labels/${this.UID}/t/${config.previewToken()}/${size}`;
|
||||
return `${config.contentUri}/labels/${this.UID}/t/${config.previewToken}/${size}`;
|
||||
} else {
|
||||
return `${config.contentUri}/svg/label`;
|
||||
}
|
||||
|
|
|
@ -84,7 +84,7 @@ export class Marker extends RestModel {
|
|||
}
|
||||
|
||||
if (this.Thumb) {
|
||||
return `${config.contentUri}/t/${this.Thumb}/${config.previewToken()}/${size}`;
|
||||
return `${config.contentUri}/t/${this.Thumb}/${config.previewToken}/${size}`;
|
||||
} else {
|
||||
return `${config.contentUri}/svg/portrait`;
|
||||
}
|
||||
|
|
|
@ -501,10 +501,10 @@ export class Photo extends RestModel {
|
|||
videoFormat = FormatWebM;
|
||||
}
|
||||
|
||||
return `${config.apiUri}/videos/${file.Hash}/${config.previewToken()}/${videoFormat}`;
|
||||
return `${config.apiUri}/videos/${file.Hash}/${config.previewToken}/${videoFormat}`;
|
||||
}
|
||||
|
||||
return `${config.apiUri}/videos/${this.Hash}/${config.previewToken()}/${FormatAvc}`;
|
||||
return `${config.apiUri}/videos/${this.Hash}/${config.previewToken}/${FormatAvc}`;
|
||||
}
|
||||
|
||||
mainFile() {
|
||||
|
@ -578,7 +578,7 @@ export class Photo extends RestModel {
|
|||
this.mainFileHash(),
|
||||
this.videoFile(),
|
||||
config.contentUri,
|
||||
config.previewToken(),
|
||||
config.previewToken,
|
||||
size
|
||||
);
|
||||
}
|
||||
|
@ -598,7 +598,7 @@ export class Photo extends RestModel {
|
|||
});
|
||||
|
||||
getDownloadUrl() {
|
||||
return `${config.apiUri}/dl/${this.mainFileHash()}?t=${config.downloadToken()}`;
|
||||
return `${config.apiUri}/dl/${this.mainFileHash()}?t=${config.downloadToken}`;
|
||||
}
|
||||
|
||||
downloadAll() {
|
||||
|
@ -609,7 +609,7 @@ export class Photo extends RestModel {
|
|||
return;
|
||||
}
|
||||
|
||||
const token = config.downloadToken();
|
||||
const token = config.downloadToken;
|
||||
|
||||
if (!this.Files) {
|
||||
const hash = this.mainFileHash();
|
||||
|
|
|
@ -95,7 +95,7 @@ export class Subject extends RestModel {
|
|||
size = "tile_160";
|
||||
}
|
||||
|
||||
return `${config.contentUri}/t/${this.Thumb}/${config.previewToken()}/${size}`;
|
||||
return `${config.contentUri}/t/${this.Thumb}/${config.previewToken}/${size}`;
|
||||
}
|
||||
|
||||
getDateString() {
|
||||
|
|
|
@ -235,7 +235,7 @@ export class Thumb extends Model {
|
|||
return `${config.contentUri}/svg/photo`;
|
||||
}
|
||||
|
||||
return `${config.contentUri}/t/${file.Hash}/${config.previewToken()}/${size}`;
|
||||
return `${config.contentUri}/t/${file.Hash}/${config.previewToken}/${size}`;
|
||||
}
|
||||
|
||||
static downloadUrl(file) {
|
||||
|
@ -243,7 +243,7 @@ export class Thumb extends Model {
|
|||
return "";
|
||||
}
|
||||
|
||||
return `${config.apiUri}/dl/${file.Hash}?t=${config.downloadToken()}`;
|
||||
return `${config.apiUri}/dl/${file.Hash}?t=${config.downloadToken}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -44,10 +44,10 @@ export class User extends RestModel {
|
|||
Attr: "",
|
||||
SuperAdmin: false,
|
||||
CanLogin: false,
|
||||
CanInvite: false,
|
||||
BasePath: "",
|
||||
UploadPath: "",
|
||||
CanSync: false,
|
||||
CanInvite: false,
|
||||
WebDAV: false,
|
||||
Thumb: "",
|
||||
ThumbSrc: "",
|
||||
Settings: {
|
||||
|
|
|
@ -216,7 +216,7 @@ export default {
|
|||
Notify.success(this.$gettext("Downloading…"));
|
||||
|
||||
const model = this.results[index];
|
||||
download(`${this.$config.apiUri}/dl/${model.Hash}?t=${this.$config.downloadToken()}`, model.Name);
|
||||
download(`${this.$config.apiUri}/dl/${model.Hash}?t=${this.$config.downloadToken}`, model.Name);
|
||||
},
|
||||
selectRange(rangeEnd, models) {
|
||||
if (!models || !models[rangeEnd] || !(models[rangeEnd] instanceof RestModel)) {
|
||||
|
|
|
@ -355,7 +355,7 @@ export default {
|
|||
let id = features[i].id;
|
||||
|
||||
let marker = this.markers[id];
|
||||
let token = this.$config.previewToken();
|
||||
let token = this.$config.previewToken;
|
||||
if (!marker) {
|
||||
let el = document.createElement('div');
|
||||
el.className = 'marker';
|
||||
|
|
|
@ -9,10 +9,6 @@ type Role string
|
|||
|
||||
// String returns the type as string.
|
||||
func (r Role) String() string {
|
||||
if r == "" {
|
||||
return "unauthorized"
|
||||
}
|
||||
|
||||
return string(r)
|
||||
}
|
||||
|
||||
|
|
|
@ -2,18 +2,17 @@ package acl
|
|||
|
||||
// Roles that can be assigned to users.
|
||||
const (
|
||||
RoleAdmin Role = "admin"
|
||||
RoleVisitor Role = "visitor"
|
||||
RoleUnauthorized Role = "unauthorized"
|
||||
RoleDefault Role = "default"
|
||||
RoleUnknown Role = ""
|
||||
RoleDefault Role = "default"
|
||||
RoleAdmin Role = "admin"
|
||||
RoleVisitor Role = "visitor"
|
||||
RoleUnknown Role = ""
|
||||
)
|
||||
|
||||
// ValidRoles specifies the valid user roles.
|
||||
var ValidRoles = map[string]Role{
|
||||
string(RoleAdmin): RoleAdmin,
|
||||
string(RoleVisitor): RoleVisitor,
|
||||
string(RoleUnauthorized): RoleUnauthorized,
|
||||
string(RoleAdmin): RoleAdmin,
|
||||
string(RoleVisitor): RoleVisitor,
|
||||
string(RoleUnknown): RoleUnknown,
|
||||
}
|
||||
|
||||
// Roles grants permissions to roles.
|
||||
|
|
|
@ -56,7 +56,7 @@ func SearchAlbums(router *gin.RouterGroup) {
|
|||
AddCountHeader(c, len(result))
|
||||
AddLimitHeader(c, f.Count)
|
||||
AddOffsetHeader(c, f.Offset)
|
||||
AddTokenHeaders(c)
|
||||
AddTokenHeaders(c, s)
|
||||
|
||||
// Return as JSON.
|
||||
c.JSON(http.StatusOK, result)
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
)
|
||||
|
||||
func TestSearchAlbums(t *testing.T) {
|
||||
t.Run("successful request", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
SearchAlbums(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/albums?count=10")
|
||||
|
@ -18,7 +18,7 @@ func TestSearchAlbums(t *testing.T) {
|
|||
assert.LessOrEqual(t, int64(3), count.Int())
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
t.Run("invalid request", func(t *testing.T) {
|
||||
t.Run("BadRequest", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
SearchAlbums(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/albums?xxx=10")
|
||||
|
|
|
@ -24,12 +24,8 @@ func GetClientConfig(router *gin.RouterGroup) {
|
|||
|
||||
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())
|
||||
c.JSON(http.StatusOK, conf.ClientSession(s))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -129,17 +129,7 @@ func wsReader(ws *websocket.Conn, writeMutex *sync.Mutex, connId string, conf *c
|
|||
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)
|
||||
wsSendMessage("config.updated", event.Data{"config": conf.ClientSession(s)}, ws, writeMutex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
|
@ -42,16 +41,15 @@ func ChangePassword(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
uid := clean.UID(c.Param("uid"))
|
||||
m := entity.FindUserByUID(uid)
|
||||
|
||||
// Users may only change their own password.
|
||||
if s.User().UserUID != m.UserUID {
|
||||
if s.User().UserUID != clean.UID(c.Param("uid")) {
|
||||
AbortForbidden(c)
|
||||
return
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
u := s.User()
|
||||
|
||||
if u == nil {
|
||||
Abort(c, http.StatusNotFound, i18n.ErrUserNotFound)
|
||||
return
|
||||
}
|
||||
|
@ -64,14 +62,14 @@ func ChangePassword(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
// Verify that the old password is correct.
|
||||
if m.WrongPassword(f.OldPassword) {
|
||||
if u.WrongPassword(f.OldPassword) {
|
||||
limiter.Auth.Reserve(ClientIP(c))
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrInvalidPassword)
|
||||
return
|
||||
}
|
||||
|
||||
// Change password.
|
||||
if err := m.SetPassword(f.NewPassword); err != nil {
|
||||
if err := s.ChangePassword(f.NewPassword); err != nil {
|
||||
Error(c, http.StatusBadRequest, err, i18n.ErrInvalidPassword)
|
||||
return
|
||||
}
|
||||
|
@ -79,8 +77,9 @@ func ChangePassword(router *gin.RouterGroup) {
|
|||
// 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"))
|
||||
english.Plural(u.DeleteSessions([]string{s.ID}), "session", "sessions"))
|
||||
|
||||
AddTokenHeaders(c, s)
|
||||
c.JSON(http.StatusOK, i18n.NewResponse(http.StatusOK, i18n.MsgPasswordChanged))
|
||||
})
|
||||
}
|
|
@ -39,7 +39,7 @@ func TestChangePassword(t *testing.T) {
|
|||
}
|
||||
})
|
||||
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
|
@ -5,7 +5,6 @@ import (
|
|||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
|
@ -32,7 +31,14 @@ func CreateSession(router *gin.RouterGroup) {
|
|||
// Skip authentication if app is running in public mode.
|
||||
if conf.Public() {
|
||||
sess := service.Session().Public()
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok", "id": sess.ID, "user": sess.User(), "data": sess.Data(), "config": conf.ClientPublic()})
|
||||
data := gin.H{
|
||||
"status": "ok",
|
||||
"id": sess.ID,
|
||||
"user": sess.User(),
|
||||
"data": sess.Data(),
|
||||
"config": conf.ClientPublic(),
|
||||
}
|
||||
c.JSON(http.StatusOK, data)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -75,17 +81,19 @@ func CreateSession(router *gin.RouterGroup) {
|
|||
// Add session id to response headers.
|
||||
AddSessionHeader(c, sess.ID)
|
||||
|
||||
// Get config values for use by the JavaScript UI and other clients.
|
||||
var clientConfig config.ClientConfig
|
||||
if sess.User().IsVisitor() {
|
||||
clientConfig = conf.ClientShare()
|
||||
} else if sess.User().IsRegistered() {
|
||||
clientConfig = conf.ClientSession(sess)
|
||||
} else {
|
||||
clientConfig = conf.ClientPublic()
|
||||
// Get config values for the UI.
|
||||
clientConfig := conf.ClientSession(sess)
|
||||
|
||||
// User information, session data, and client config values.
|
||||
data := gin.H{
|
||||
"status": "ok",
|
||||
"id": sess.ID,
|
||||
"user": sess.User(),
|
||||
"data": sess.Data(),
|
||||
"config": clientConfig,
|
||||
}
|
||||
|
||||
// Send JSON response with user information, session data, and client config values.
|
||||
c.JSON(sess.HttpStatus(), gin.H{"status": "ok", "id": sess.ID, "user": sess.User(), "data": sess.Data(), "config": clientConfig})
|
||||
// Send JSON response.
|
||||
c.JSON(sess.HttpStatus(), data)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
)
|
||||
|
@ -45,21 +44,15 @@ func GetSession(router *gin.RouterGroup) {
|
|||
// 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 not set - 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.
|
||||
data := gin.H{
|
||||
"status": "ok",
|
||||
"id": sess.ID,
|
||||
"user": sess.User(),
|
||||
"data": sess.Data(),
|
||||
"config": service.Config().ClientSession(sess),
|
||||
}
|
||||
|
||||
// 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})
|
||||
c.JSON(http.StatusOK, data)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ func TestSession(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCreateSession(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
)
|
||||
|
||||
func TestGetShares(t *testing.T) {
|
||||
t.Run("invalid token or share", func(t *testing.T) {
|
||||
t.Run("InvalidToken", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
Shares(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/1jxf3jfn2k/st9lxuqxpogaaba7")
|
||||
|
@ -21,7 +21,7 @@ func TestGetShares(t *testing.T) {
|
|||
r := PerformRequest(app, "GET", "/api/v1/4jxf3jfn2k/at9lxuqxpogaaba7")
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, r.Code)
|
||||
})*/
|
||||
t.Run("invalid token", func(t *testing.T) {
|
||||
t.Run("InvalidToken", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
Shares(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/xxx")
|
||||
|
|
|
@ -3,7 +3,7 @@ package api
|
|||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
)
|
||||
|
||||
|
@ -15,10 +15,10 @@ func InvalidPreviewToken(c *gin.Context) bool {
|
|||
token = clean.UrlToken(c.Query("t"))
|
||||
}
|
||||
|
||||
return service.Config().InvalidPreviewToken(token)
|
||||
return entity.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")))
|
||||
return entity.InvalidDownloadToken(clean.UrlToken(c.Query("t")))
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
)
|
||||
|
||||
func TestGetSettings(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
GetSettings(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/settings")
|
||||
|
@ -22,7 +22,7 @@ func TestGetSettings(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSaveSettings(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
GetSettings(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/settings")
|
||||
|
|
|
@ -4,11 +4,13 @@ import (
|
|||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAlbumCover(t *testing.T) {
|
||||
t.Run("invalid type", func(t *testing.T) {
|
||||
t.Run("InvalidType", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
AlbumCover(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/albums/at9lxuqxpogaaba7/t/"+conf.PreviewToken()+"/xxx")
|
||||
|
@ -27,8 +29,10 @@ func TestAlbumCover(t *testing.T) {
|
|||
r := PerformRequest(app, "GET", "/api/v1/albums/at9lxuqxpogaaba9/t/"+conf.PreviewToken()+"/tile_500")
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
t.Run("invalid token", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
t.Run("InvalidToken", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
AlbumCover(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/albums/at9lxuqxpogaaba8/t/xxx/tile_500")
|
||||
assert.Equal(t, http.StatusForbidden, r.Code)
|
||||
|
@ -36,7 +40,7 @@ func TestAlbumCover(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLabelCover(t *testing.T) {
|
||||
t.Run("invalid type", func(t *testing.T) {
|
||||
t.Run("InvalidType", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
LabelCover(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/labels/lt9k3pw1wowuy3c2/t/"+conf.PreviewToken()+"/xxx")
|
||||
|
@ -57,8 +61,10 @@ func TestLabelCover(t *testing.T) {
|
|||
r := PerformRequest(app, "GET", "/api/v1/labels/lt9k3pw1wowuy3c2/t/"+conf.PreviewToken()+"/tile_500")
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
t.Run("invalid token", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
t.Run("InvalidToken", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
LabelCover(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/labels/lt9k3pw1wowuy3c3/t/xxx/tile_500")
|
||||
assert.Equal(t, http.StatusForbidden, r.Code)
|
||||
|
|
|
@ -4,30 +4,31 @@ import (
|
|||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
)
|
||||
|
||||
func TestGetDownload(t *testing.T) {
|
||||
t.Run("download not existing file", func(t *testing.T) {
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
|
||||
GetDownload(router)
|
||||
|
||||
r := PerformRequest(app, "GET", "/api/v1/dl/123xxx?t="+conf.DownloadToken())
|
||||
val := gjson.Get(r.Body.String(), "error")
|
||||
assert.Equal(t, "record not found", val.String())
|
||||
assert.Equal(t, http.StatusNotFound, r.Code)
|
||||
})
|
||||
t.Run("could not find original", func(t *testing.T) {
|
||||
t.Run("MissingOriginal", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
GetDownload(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/dl/3cad9168fa6acc5c5c2965ddf6ec465ca42fd818?t="+conf.DownloadToken())
|
||||
assert.Equal(t, http.StatusNotFound, r.Code)
|
||||
})
|
||||
t.Run("invalid download token", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
t.Run("InvalidDownloadToken", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
GetDownload(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/dl/3cad9168fa6acc5c5c2965ddf6ec465ca42fd818?t=xxx")
|
||||
assert.Equal(t, http.StatusForbidden, r.Code)
|
||||
|
|
|
@ -43,7 +43,7 @@ func SearchFaces(router *gin.RouterGroup) {
|
|||
AddCountHeader(c, len(result))
|
||||
AddLimitHeader(c, f.Count)
|
||||
AddOffsetHeader(c, f.Offset)
|
||||
AddTokenHeaders(c)
|
||||
AddTokenHeaders(c, s)
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
})
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
)
|
||||
|
||||
func TestSearchFaces(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
SearchFaces(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/faces?count=15")
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
)
|
||||
|
||||
func TestGetFace(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
GetFace(router)
|
||||
// Example:
|
||||
|
@ -46,7 +46,7 @@ func TestGetFace(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestUpdateFace(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
UpdateFace(router)
|
||||
r := PerformRequestWithBody(app, "PUT", "/api/v1/faces/PN6QO5INYTUSAATOFL43LL2ABAV5ACzk", `{"SubjUID": "jqu0xs11qekk9jx8"}`)
|
||||
|
|
|
@ -5,31 +5,35 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
)
|
||||
|
||||
func TestGetFolderCover(t *testing.T) {
|
||||
t.Run("no cover yet", func(t *testing.T) {
|
||||
t.Run("NoCover", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
FolderCover(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/folders/t/dqo63pn35k2d495z/"+conf.PreviewToken()+"/tile_500")
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
|
||||
t.Run("invalid thumb type", func(t *testing.T) {
|
||||
t.Run("InvalidType", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
FolderCover(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/folders/t/dqo63pn35k2d495z/"+conf.PreviewToken()+"/xxx")
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
|
||||
t.Run("invalid token", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
t.Run("InvalidToken", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
FolderCover(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/folders/t/dqo63pn35k2d495z/xxx/tile_500")
|
||||
assert.Equal(t, http.StatusForbidden, r.Code)
|
||||
})
|
||||
|
||||
t.Run("could not find original", func(t *testing.T) {
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
FolderCover(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/folders/t/dqo63pn2f87f02oi/"+conf.PreviewToken()+"/fit_7680")
|
||||
|
|
|
@ -107,7 +107,7 @@ func SearchFolders(router *gin.RouterGroup, urlPath, rootName, rootPath string)
|
|||
AddCountHeader(c, len(resp.Files)+len(resp.Folders))
|
||||
AddLimitHeader(c, f.Count)
|
||||
AddOffsetHeader(c, f.Offset)
|
||||
AddTokenHeaders(c)
|
||||
AddTokenHeaders(c, s)
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/session"
|
||||
)
|
||||
|
||||
|
@ -66,7 +66,11 @@ func AddFileCountHeaders(c *gin.Context, filesCount, foldersCount int) {
|
|||
}
|
||||
|
||||
// AddTokenHeaders adds preview token headers to the response.
|
||||
func AddTokenHeaders(c *gin.Context) {
|
||||
c.Header("X-Preview-Token", service.Config().PreviewToken())
|
||||
c.Header("X-Download-Token", service.Config().DownloadToken())
|
||||
func AddTokenHeaders(c *gin.Context, s *entity.Session) {
|
||||
if s.PreviewToken != "" {
|
||||
c.Header("X-Preview-Token", s.PreviewToken)
|
||||
}
|
||||
if s.DownloadToken != "" {
|
||||
c.Header("X-Download-Token", s.DownloadToken)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ func SearchLabels(router *gin.RouterGroup) {
|
|||
// TODO c.Header("X-Count", strconv.Itoa(count))
|
||||
AddLimitHeader(c, f.Count)
|
||||
AddOffsetHeader(c, f.Offset)
|
||||
AddTokenHeaders(c)
|
||||
AddTokenHeaders(c, s)
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
})
|
||||
|
|
|
@ -76,7 +76,7 @@ func SearchPhotos(router *gin.RouterGroup) {
|
|||
AddCountHeader(c, count)
|
||||
AddLimitHeader(c, f.Count)
|
||||
AddOffsetHeader(c, f.Offset)
|
||||
AddTokenHeaders(c)
|
||||
AddTokenHeaders(c, s)
|
||||
|
||||
// Return as JSON.
|
||||
c.JSON(http.StatusOK, result)
|
||||
|
@ -93,7 +93,7 @@ func SearchPhotos(router *gin.RouterGroup) {
|
|||
|
||||
conf := service.Config()
|
||||
|
||||
result, count, err := search.UserPhotosViewerResults(f, s, conf.ContentUri(), conf.ApiUri(), conf.PreviewToken(), conf.DownloadToken())
|
||||
result, count, err := search.UserPhotosViewerResults(f, s, conf.ContentUri(), conf.ApiUri(), s.PreviewToken, s.DownloadToken)
|
||||
|
||||
if err != nil {
|
||||
event.AuditWarn([]string{ClientIP(c), "session %s", string(acl.ResourcePhotos), "view", "%s"}, s.RefID, err)
|
||||
|
@ -105,7 +105,7 @@ func SearchPhotos(router *gin.RouterGroup) {
|
|||
AddCountHeader(c, count)
|
||||
AddLimitHeader(c, f.Count)
|
||||
AddOffsetHeader(c, f.Offset)
|
||||
AddTokenHeaders(c)
|
||||
AddTokenHeaders(c, s)
|
||||
|
||||
// Return as JSON.
|
||||
c.JSON(http.StatusOK, result)
|
||||
|
|
|
@ -67,14 +67,14 @@ func SearchGeo(router *gin.RouterGroup) {
|
|||
AddCountHeader(c, len(photos))
|
||||
AddLimitHeader(c, f.Count)
|
||||
AddOffsetHeader(c, f.Offset)
|
||||
AddTokenHeaders(c)
|
||||
AddTokenHeaders(c, s)
|
||||
|
||||
var resp []byte
|
||||
|
||||
// Render JSON response.
|
||||
switch clean.Token(c.Param("format")) {
|
||||
case "view":
|
||||
resp, err = photos.ViewerJSON(conf.ContentUri(), conf.ApiUri(), conf.PreviewToken(), conf.DownloadToken())
|
||||
resp, err = photos.ViewerJSON(conf.ContentUri(), conf.ApiUri(), s.PreviewToken, s.DownloadToken)
|
||||
default:
|
||||
resp, err = photos.GeoJSON()
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
)
|
||||
|
||||
func TestSearchPhotos(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
SearchPhotos(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/photos?count=10")
|
||||
|
|
|
@ -4,13 +4,14 @@ import (
|
|||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestGetPhoto(t *testing.T) {
|
||||
t.Run("search for existing photo", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
GetPhoto(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/photos/pt9jtdre2lvl0yh7")
|
||||
|
@ -19,7 +20,7 @@ func TestGetPhoto(t *testing.T) {
|
|||
assert.Equal(t, "200", val.String())
|
||||
})
|
||||
|
||||
t.Run("search for not existing photo", func(t *testing.T) {
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
GetPhoto(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/photos/xxx")
|
||||
|
@ -28,7 +29,7 @@ func TestGetPhoto(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestUpdatePhoto(t *testing.T) {
|
||||
t.Run("successful request", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
UpdatePhoto(router)
|
||||
r := PerformRequestWithBody(app, "PUT", "/api/v1/photos/pt9jtdre2lvl0y13", `{"Title": "Updated01", "Country": "de"}`)
|
||||
|
@ -39,14 +40,14 @@ func TestUpdatePhoto(t *testing.T) {
|
|||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
|
||||
t.Run("invalid request", func(t *testing.T) {
|
||||
t.Run("BadRequest", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
UpdatePhoto(router)
|
||||
r := PerformRequestWithBody(app, "PUT", "/api/v1/photos/pt9jtdre2lvl0y13", `{"Name": "Updated01", "Country": 123}`)
|
||||
assert.Equal(t, http.StatusBadRequest, r.Code)
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
UpdatePhoto(router)
|
||||
r := PerformRequestWithBody(app, "PUT", "/api/v1/photos/xxx", `{"Name": "Updated01", "Country": "de"}`)
|
||||
|
@ -57,22 +58,24 @@ func TestUpdatePhoto(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGetPhotoDownload(t *testing.T) {
|
||||
t.Run("could not find original", func(t *testing.T) {
|
||||
t.Run("OriginalMissing", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
GetPhotoDownload(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/photos/pt9jtdre2lvl0yh7/dl?t="+conf.DownloadToken())
|
||||
assert.Equal(t, http.StatusNotFound, r.Code)
|
||||
})
|
||||
|
||||
t.Run("not existing photo", func(t *testing.T) {
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
GetPhotoDownload(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/photos/xxx/dl?t="+conf.DownloadToken())
|
||||
assert.Equal(t, http.StatusNotFound, r.Code)
|
||||
})
|
||||
|
||||
t.Run("invalid token", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
t.Run("InvalidToken", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
GetPhotoDownload(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/photos/pt9jtdre2lvl0yh7/dl?t=xxx")
|
||||
assert.Equal(t, http.StatusForbidden, r.Code)
|
||||
|
@ -80,7 +83,7 @@ func TestGetPhotoDownload(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLikePhoto(t *testing.T) {
|
||||
t.Run("existing photo", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
LikePhoto(router)
|
||||
r := PerformRequest(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh9/like")
|
||||
|
@ -91,7 +94,7 @@ func TestLikePhoto(t *testing.T) {
|
|||
assert.Equal(t, "true", val.String())
|
||||
})
|
||||
|
||||
t.Run("not existing photo", func(t *testing.T) {
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
LikePhoto(router)
|
||||
r := PerformRequest(app, "POST", "/api/v1/photos/xxx/like")
|
||||
|
@ -100,7 +103,7 @@ func TestLikePhoto(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDislikePhoto(t *testing.T) {
|
||||
t.Run("existing photo", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
DislikePhoto(router)
|
||||
r := PerformRequest(app, "DELETE", "/api/v1/photos/pt9jtdre2lvl0yh8/like")
|
||||
|
@ -111,7 +114,7 @@ func TestDislikePhoto(t *testing.T) {
|
|||
assert.Equal(t, "false", val.String())
|
||||
})
|
||||
|
||||
t.Run("not existing photo", func(t *testing.T) {
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
DislikePhoto(router)
|
||||
r := PerformRequest(app, "DELETE", "/api/v1/photos/xxx/like")
|
||||
|
@ -120,7 +123,7 @@ func TestDislikePhoto(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPhotoPrimary(t *testing.T) {
|
||||
t.Run("existing photo", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
PhotoPrimary(router)
|
||||
r := PerformRequest(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh8/files/ft1es39w45bnlqdw/primary")
|
||||
|
@ -134,7 +137,7 @@ func TestPhotoPrimary(t *testing.T) {
|
|||
assert.Equal(t, "false", val2.String())
|
||||
})
|
||||
|
||||
t.Run("incorrect photo uid", func(t *testing.T) {
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
PhotoPrimary(router)
|
||||
r := PerformRequest(app, "POST", "/api/v1/photos/xxx/files/ft1es39w45bnlqdw/primary")
|
||||
|
@ -145,14 +148,14 @@ func TestPhotoPrimary(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGetPhotoYaml(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
GetPhotoYaml(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/photos/pt9jtdre2lvl0yh7/yaml")
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
|
||||
t.Run("not existing photo", func(t *testing.T) {
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
GetPhotoYaml(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/photos/xxx/yaml")
|
||||
|
@ -161,7 +164,7 @@ func TestGetPhotoYaml(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestApprovePhoto(t *testing.T) {
|
||||
t.Run("existing photo", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
GetPhoto(router)
|
||||
r3 := PerformRequest(app, "GET", "/api/v1/photos/pt9jtxrexxvl0y20")
|
||||
|
@ -175,7 +178,7 @@ func TestApprovePhoto(t *testing.T) {
|
|||
assert.Equal(t, "3", val.String())
|
||||
})
|
||||
|
||||
t.Run("not existing photo", func(t *testing.T) {
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
ApprovePhoto(router)
|
||||
r := PerformRequest(app, "POST", "/api/v1/photos/xxx/approve")
|
||||
|
|
|
@ -5,17 +5,23 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
)
|
||||
|
||||
func TestGetPreview(t *testing.T) {
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
SharePreview(router)
|
||||
r := PerformRequest(app, "GET", "api/v1/s/1jxf3jfn2k/st9lxuqxpogaaba7/preview")
|
||||
assert.Equal(t, http.StatusNotFound, r.Code)
|
||||
})
|
||||
t.Run("invalid token", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
t.Run("InvalidToken", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
SharePreview(router)
|
||||
r := PerformRequest(app, "GET", "api/v1/s/xxx/st9lxuqxpogaaba7/preview")
|
||||
assert.Equal(t, http.StatusNotFound, r.Code)
|
||||
|
|
|
@ -42,7 +42,7 @@ func SearchSubjects(router *gin.RouterGroup) {
|
|||
AddCountHeader(c, len(result))
|
||||
AddLimitHeader(c, f.Count)
|
||||
AddOffsetHeader(c, f.Offset)
|
||||
AddTokenHeaders(c)
|
||||
AddTokenHeaders(c, s)
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
})
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
)
|
||||
|
||||
func TestSearchSubjects(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
SearchSubjects(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/subjects?count=10")
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
)
|
||||
|
||||
func TestGetSubject(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
GetSubject(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/subjects/jqy1y111h1njaaaa")
|
||||
|
|
|
@ -5,42 +5,46 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
)
|
||||
|
||||
func TestGetThumb(t *testing.T) {
|
||||
t.Run("invalid type", func(t *testing.T) {
|
||||
t.Run("InvalidType", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
GetThumb(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/t/1/"+conf.PreviewToken()+"/xxx")
|
||||
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
t.Run("invalid hash", func(t *testing.T) {
|
||||
t.Run("WrongHash", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
GetThumb(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/t/1/"+conf.PreviewToken()+"/tile_500")
|
||||
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
t.Run("could not find original", func(t *testing.T) {
|
||||
t.Run("WrongFile", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
GetThumb(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/t/2cad9168fa6acc5c5c2965ddf6ec465ca42fd818/"+conf.PreviewToken()+"/fit_7680")
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
t.Run("invalid token", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
t.Run("WrongToken", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
GetThumb(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/t/2cad9168fa6acc5c5c2965ddf6ec465ca42fd818/xxx/tile_500")
|
||||
assert.Equal(t, http.StatusForbidden, r.Code)
|
||||
})
|
||||
t.Run("no jpg", func(t *testing.T) {
|
||||
t.Run("NoJPEG", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
GetThumb(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/t/pcad9168fa6acc5c5ba965adf6ec465ca42fd819/"+conf.PreviewToken()+"/fit_7680")
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
t.Run("file error", func(t *testing.T) {
|
||||
t.Run("FileError", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
GetThumb(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/t/acad9168fa6acc5c5c2965ddf6ec465ca42fd832/"+conf.PreviewToken()+"/fit_7680")
|
||||
|
@ -53,24 +57,5 @@ func TestGetThumb(t *testing.T) {
|
|||
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
t.Run("InvalidType", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
GetThumb(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/t/1-016014058037/"+conf.PreviewToken()+"/xxx")
|
||||
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
t.Run("InvalidHash", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
GetThumb(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/t/1-016014058037/"+conf.PreviewToken()+"/tile_500")
|
||||
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
t.Run("invalid token", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
GetThumb(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/t/2cad9168fa6acc5c5c2965ddf6ec465ca42fd818-016014058037/xxx/tile_500")
|
||||
assert.Equal(t, http.StatusForbidden, r.Code)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,9 +5,10 @@ import (
|
|||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
)
|
||||
|
||||
func TestGetVideo(t *testing.T) {
|
||||
|
@ -15,41 +16,43 @@ func TestGetVideo(t *testing.T) {
|
|||
assert.Equal(t, ContentTypeAvc, fmt.Sprintf("%s; codecs=\"%s\"", "video/mp4", clean.Codec("avc1")))
|
||||
})
|
||||
|
||||
t.Run("invalid hash", func(t *testing.T) {
|
||||
t.Run("InvalidHash", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
GetVideo(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/videos/xxx/"+conf.PreviewToken()+"/mp4")
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
|
||||
t.Run("invalid type", func(t *testing.T) {
|
||||
t.Run("InvalidType", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
GetVideo(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/videos/acad9168fa6acc5c5c2965ddf6ec465ca42fd831/"+conf.PreviewToken()+"/xxx")
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
|
||||
t.Run("file for video not found", func(t *testing.T) {
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
GetVideo(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/videos/acad9168fa6acc5c5c2965ddf6ec465ca42fd831/"+conf.PreviewToken()+"/mp4")
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
|
||||
t.Run("file with error", func(t *testing.T) {
|
||||
t.Run("FileError", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
GetVideo(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/videos/acad9168fa6acc5c5c2965ddf6ec465ca42fd832/"+conf.PreviewToken()+"/mp4")
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
|
||||
t.Run("invalid token", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
t.Run("InvalidToken", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
GetVideo(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/videos/acad9168fa6acc5c5c2965ddf6ec465ca42fd832/xxx/mp4")
|
||||
assert.Equal(t, http.StatusForbidden, r.Code)
|
||||
})
|
||||
t.Run("no video file", func(t *testing.T) {
|
||||
t.Run("NoVideo", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
GetVideo(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/videos/ocad9168fa6acc5c5c2965ddf6ec465ca42fd818/"+conf.PreviewToken()+"/mp4")
|
||||
|
|
|
@ -188,7 +188,7 @@ func backupAction(ctx *cli.Context) error {
|
|||
|
||||
elapsed := time.Since(start)
|
||||
|
||||
log.Infof("backup completed in %s", elapsed)
|
||||
log.Infof("completed in %s", elapsed)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ func convertAction(ctx *cli.Context) error {
|
|||
|
||||
elapsed := time.Since(start)
|
||||
|
||||
log.Infof("converting completed in %s", elapsed)
|
||||
log.Infof("completed in %s", elapsed)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -86,7 +86,7 @@ func copyAction(ctx *cli.Context) error {
|
|||
|
||||
elapsed := time.Since(start)
|
||||
|
||||
log.Infof("import completed in %s", elapsed)
|
||||
log.Infof("completed in %s", elapsed)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -86,7 +86,7 @@ func importAction(ctx *cli.Context) error {
|
|||
|
||||
elapsed := time.Since(start)
|
||||
|
||||
log.Infof("import completed in %s", elapsed)
|
||||
log.Infof("completed in %s", elapsed)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -169,7 +169,7 @@ func migrationsRunAction(ctx *cli.Context) error {
|
|||
|
||||
elapsed := time.Since(start)
|
||||
|
||||
log.Infof("migration completed in %s", elapsed)
|
||||
log.Infof("completed in %s", elapsed)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -184,7 +184,7 @@ func resetIndexDb(c *config.Config) {
|
|||
entity.Admin.InitAccount(c.AdminUser(), c.AdminPassword())
|
||||
}
|
||||
|
||||
log.Infof("database reset completed in %s", time.Since(start))
|
||||
log.Infof("completed in %s", time.Since(start))
|
||||
}
|
||||
|
||||
// resetCache removes all cache files and folders.
|
||||
|
|
|
@ -15,7 +15,7 @@ const (
|
|||
UserAttrUsage = "custom user account `ATTRIBUTES`"
|
||||
UserAdminUsage = "make user super admin with full access"
|
||||
UserNoLoginUsage = "disable login on the web interface"
|
||||
UserCanSyncUsage = "allow to sync files via WebDAV"
|
||||
UserWebDAVUsage = "allow to sync files via WebDAV"
|
||||
)
|
||||
|
||||
// UsersCommand registers the user management subcommands.
|
||||
|
@ -25,6 +25,7 @@ var UsersCommand = cli.Command{
|
|||
Usage: "User management subcommands",
|
||||
Subcommands: []cli.Command{
|
||||
UsersListCommand,
|
||||
UsersLegacyCommand,
|
||||
UsersAddCommand,
|
||||
UsersShowCommand,
|
||||
UsersModCommand,
|
||||
|
@ -65,7 +66,7 @@ var UserFlags = []cli.Flag{
|
|||
Usage: UserNoLoginUsage,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "can-sync, w",
|
||||
Usage: UserCanSyncUsage,
|
||||
Name: "webdav, w",
|
||||
Usage: UserWebDAVUsage,
|
||||
},
|
||||
}
|
||||
|
|
53
internal/commands/users_legacy.go
Normal file
53
internal/commands/users_legacy.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/dustin/go-humanize/english"
|
||||
"github.com/urfave/cli"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/pkg/report"
|
||||
)
|
||||
|
||||
// UsersLegacyCommand configures the command name, flags, and action.
|
||||
var UsersLegacyCommand = cli.Command{
|
||||
Name: "legacy",
|
||||
Usage: "Displays legacy user accounts",
|
||||
Flags: report.CliFlags,
|
||||
Action: usersLegacyAction,
|
||||
}
|
||||
|
||||
// usersLegacyAction displays legacy user accounts.
|
||||
func usersLegacyAction(ctx *cli.Context) error {
|
||||
return CallWithDependencies(ctx, func(conf *config.Config) error {
|
||||
cols := []string{"ID", "UID", "User Name", "Display Name", "Email", "Admin", "Created At"}
|
||||
|
||||
// Fetch users from database.
|
||||
users := entity.FindLegacyUsers()
|
||||
rows := make([][]string, len(users))
|
||||
|
||||
// Show log message.
|
||||
log.Infof("found %s", english.Plural(len(users), "legacy user", "legacy users"))
|
||||
|
||||
// Display report.
|
||||
for i, user := range users {
|
||||
rows[i] = []string{
|
||||
fmt.Sprintf("%d", user.ID),
|
||||
user.UserUID,
|
||||
user.UserName,
|
||||
user.FullName,
|
||||
user.PrimaryEmail,
|
||||
report.Bool(user.Admin(), report.Yes, report.No),
|
||||
user.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
}
|
||||
|
||||
result, err := report.RenderFormat(rows, cols, report.CliFormat(ctx))
|
||||
|
||||
fmt.Printf("\n%s\n", result)
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
|
@ -40,8 +40,8 @@ func usersListAction(ctx *cli.Context) error {
|
|||
user.Email(),
|
||||
user.AclRole().String(),
|
||||
report.Bool(user.SuperAdmin, report.Yes, report.No),
|
||||
report.Bool(user.LoginAllowed(), report.Enabled, report.Disabled),
|
||||
report.Bool(user.SyncAllowed(), report.Enabled, report.Disabled),
|
||||
report.Bool(user.CanLogIn(), report.Enabled, report.Disabled),
|
||||
report.Bool(user.CanUseWebDAV(), report.Enabled, report.Disabled),
|
||||
user.Attr(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ package commands
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
|
@ -24,28 +26,34 @@ func usersModAction(ctx *cli.Context) error {
|
|||
return CallWithDependencies(ctx, func(conf *config.Config) error {
|
||||
conf.MigrateDb(false, nil)
|
||||
|
||||
username := clean.Username(ctx.Args().First())
|
||||
id := clean.Username(ctx.Args().First())
|
||||
|
||||
// Username provided?
|
||||
if username == "" {
|
||||
// Name or UID provided?
|
||||
if id == "" {
|
||||
return cli.ShowSubcommandHelp(ctx)
|
||||
}
|
||||
|
||||
// Find user by name.
|
||||
user := entity.FindUserByName(username)
|
||||
// Find user record.
|
||||
var m *entity.User
|
||||
|
||||
if user == nil {
|
||||
return fmt.Errorf("user %s not found", clean.LogQuote(username))
|
||||
if rnd.IsUID(id, entity.UserUID) {
|
||||
m = entity.FindUserByUID(id)
|
||||
} else {
|
||||
m = entity.FindUserByName(id)
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
return fmt.Errorf("user %s not found", clean.LogQuote(id))
|
||||
}
|
||||
|
||||
// Set values.
|
||||
if err := user.SetValuesFromCli(ctx); err != nil {
|
||||
if err := m.SetValuesFromCli(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Change password?
|
||||
if val := clean.Password(ctx.String("password")); ctx.IsSet("password") && val != "" {
|
||||
err := user.SetPassword(val)
|
||||
err := m.SetPassword(val)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -55,11 +63,11 @@ func usersModAction(ctx *cli.Context) error {
|
|||
}
|
||||
|
||||
// Save values.
|
||||
if err := user.Save(); err != nil {
|
||||
if err := m.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("user %s has been updated", clean.LogQuote(user.Name()))
|
||||
log.Infof("user %s has been updated", m.String())
|
||||
|
||||
return nil
|
||||
})
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/urfave/cli"
|
||||
|
||||
|
@ -25,28 +26,39 @@ func usersRemoveAction(ctx *cli.Context) error {
|
|||
return CallWithDependencies(ctx, func(conf *config.Config) error {
|
||||
conf.MigrateDb(false, nil)
|
||||
|
||||
username := clean.Username(ctx.Args().First())
|
||||
id := clean.Username(ctx.Args().First())
|
||||
|
||||
// Username provided?
|
||||
if username == "" {
|
||||
// Name or UID provided?
|
||||
if id == "" {
|
||||
return cli.ShowSubcommandHelp(ctx)
|
||||
}
|
||||
|
||||
// Find user record.
|
||||
var m *entity.User
|
||||
|
||||
if rnd.IsUID(id, entity.UserUID) {
|
||||
m = entity.FindUserByUID(id)
|
||||
} else {
|
||||
m = entity.FindUserByName(id)
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
return fmt.Errorf("user %s not found", clean.LogQuote(id))
|
||||
}
|
||||
|
||||
actionPrompt := promptui.Prompt{
|
||||
Label: fmt.Sprintf("Remove user %s?", clean.LogQuote(username)),
|
||||
Label: fmt.Sprintf("Remove user %s?", m.String()),
|
||||
IsConfirm: true,
|
||||
}
|
||||
|
||||
if _, err := actionPrompt.Run(); err == nil {
|
||||
if m := entity.FindUserByName(username); m == nil {
|
||||
return errors.New("user not found")
|
||||
} else if err := m.Delete(); err != nil {
|
||||
if err = m.Delete(); err != nil {
|
||||
return err
|
||||
} else {
|
||||
log.Infof("user %s has been removed", clean.LogQuote(username))
|
||||
log.Infof("user %s has been removed", m.String())
|
||||
}
|
||||
} else {
|
||||
log.Infof("user %s was not removed", clean.LogQuote(username))
|
||||
log.Infof("user %s was not removed", m.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -3,6 +3,8 @@ package commands
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
|
@ -23,22 +25,28 @@ var UsersShowCommand = cli.Command{
|
|||
// usersShowAction Shows user account details.
|
||||
func usersShowAction(ctx *cli.Context) error {
|
||||
return CallWithDependencies(ctx, func(conf *config.Config) error {
|
||||
username := clean.Username(ctx.Args().First())
|
||||
id := clean.Username(ctx.Args().First())
|
||||
|
||||
// Username provided?
|
||||
if username == "" {
|
||||
// Name or UID provided?
|
||||
if id == "" {
|
||||
return cli.ShowSubcommandHelp(ctx)
|
||||
}
|
||||
|
||||
// Find user by name.
|
||||
user := entity.FindUserByName(username)
|
||||
// Find user record.
|
||||
var m *entity.User
|
||||
|
||||
if user == nil {
|
||||
return fmt.Errorf("user %s not found", clean.LogQuote(username))
|
||||
if rnd.IsUID(id, entity.UserUID) {
|
||||
m = entity.FindUserByUID(id)
|
||||
} else {
|
||||
m = entity.FindUserByName(id)
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
return fmt.Errorf("user %s not found", clean.LogQuote(id))
|
||||
}
|
||||
|
||||
// Get user information.
|
||||
rows, cols := user.Report(true)
|
||||
rows, cols := m.Report(true)
|
||||
|
||||
// Sort values by name.
|
||||
report.Sort(rows)
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
func TestClientAssets_Load(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
a := NewClientAssets(c.StaticUri())
|
||||
|
||||
err := a.Load("testdata/static/build/assets.json")
|
||||
|
|
|
@ -68,8 +68,8 @@ type ClientConfig struct {
|
|||
Thumbs ThumbSizes `json:"thumbs"`
|
||||
Status string `json:"status"`
|
||||
MapKey string `json:"mapKey"`
|
||||
DownloadToken string `json:"downloadToken"`
|
||||
PreviewToken string `json:"previewToken"`
|
||||
DownloadToken string `json:"downloadToken,omitempty"`
|
||||
PreviewToken string `json:"previewToken,omitempty"`
|
||||
Disable ClientDisable `json:"disable"`
|
||||
Count ClientCounts `json:"count"`
|
||||
Pos ClientPosition `json:"pos"`
|
||||
|
@ -208,7 +208,7 @@ func (c *Config) ClientPublic() ClientConfig {
|
|||
|
||||
cfg := ClientConfig{
|
||||
Settings: c.PublicSettings(),
|
||||
ACL: acl.Resources.Grants(acl.RoleUnauthorized),
|
||||
ACL: acl.Resources.Grants(acl.RoleUnknown),
|
||||
Disable: ClientDisable{
|
||||
Backups: true,
|
||||
WebDAV: true,
|
||||
|
@ -270,8 +270,8 @@ func (c *Config) ClientPublic() ClientConfig {
|
|||
Colors: colors.All.List(),
|
||||
ManifestUri: c.ClientManifestUri(),
|
||||
Clip: txt.ClipDefault,
|
||||
PreviewToken: "public",
|
||||
DownloadToken: "public",
|
||||
PreviewToken: entity.TokenPublic,
|
||||
DownloadToken: entity.TokenPublic,
|
||||
Ext: ClientExt(c, ClientPublic),
|
||||
}
|
||||
|
||||
|
@ -582,9 +582,23 @@ func (c *Config) ClientRole(role acl.Role) ClientConfig {
|
|||
}
|
||||
|
||||
// ClientSession provides the client config values for the specified session.
|
||||
func (c *Config) ClientSession(sess *entity.Session) ClientConfig {
|
||||
result := c.ClientUser(false).ApplyACL(acl.Resources, sess.User().AclRole())
|
||||
result.Settings = c.SessionSettings(sess)
|
||||
func (c *Config) ClientSession(sess *entity.Session) (cfg ClientConfig) {
|
||||
if sess.User().IsVisitor() {
|
||||
cfg = c.ClientShare()
|
||||
} else if sess.User().IsRegistered() {
|
||||
cfg = c.ClientUser(false).ApplyACL(acl.Resources, sess.User().AclRole())
|
||||
cfg.Settings = c.SessionSettings(sess)
|
||||
} else {
|
||||
cfg = c.ClientPublic()
|
||||
}
|
||||
|
||||
return result
|
||||
if c.Public() {
|
||||
cfg.PreviewToken = entity.TokenPublic
|
||||
cfg.DownloadToken = entity.TokenPublic
|
||||
} else if sess.PreviewToken != "" || sess.DownloadToken != "" {
|
||||
cfg.PreviewToken = sess.PreviewToken
|
||||
cfg.DownloadToken = sess.DownloadToken
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
|
|
@ -79,7 +79,6 @@ func TestConfig_ClientShareConfig(t *testing.T) {
|
|||
|
||||
func TestConfig_ClientRoleConfig(t *testing.T) {
|
||||
c := NewTestConfig("config")
|
||||
|
||||
c.SetAuthMode(AuthModePasswd)
|
||||
|
||||
assert.Equal(t, AuthModePasswd, c.AuthMode())
|
||||
|
@ -167,8 +166,8 @@ func TestConfig_ClientRoleConfig(t *testing.T) {
|
|||
|
||||
assert.Equal(t, expected, f)
|
||||
})
|
||||
t.Run("RoleUnauthorized", func(t *testing.T) {
|
||||
cfg := c.ClientRole(acl.RoleUnauthorized)
|
||||
t.Run("RoleUnknown", func(t *testing.T) {
|
||||
cfg := c.ClientRole(acl.RoleUnknown)
|
||||
f := cfg.Settings.Features
|
||||
|
||||
assert.NotEqual(t, adminFeatures, f)
|
||||
|
@ -195,38 +194,11 @@ func TestConfig_ClientRoleConfig(t *testing.T) {
|
|||
assert.False(t, f.Reactions)
|
||||
assert.False(t, f.Ratings)
|
||||
})
|
||||
t.Run("RoleUnknown", func(t *testing.T) {
|
||||
cfg := c.ClientRole(acl.RoleUnknown)
|
||||
f := cfg.Settings.Features
|
||||
|
||||
assert.NotEqual(t, adminFeatures, f)
|
||||
assert.False(t, f.Search)
|
||||
assert.False(t, f.Videos)
|
||||
assert.False(t, f.Albums)
|
||||
assert.False(t, f.Settings)
|
||||
assert.False(t, f.Edit)
|
||||
assert.False(t, f.Private)
|
||||
assert.False(t, f.Advanced)
|
||||
assert.False(t, f.Upload)
|
||||
assert.False(t, f.Download)
|
||||
assert.False(t, f.Sync)
|
||||
assert.False(t, f.Delete)
|
||||
assert.False(t, f.Import)
|
||||
assert.False(t, f.Library)
|
||||
assert.False(t, f.Logs)
|
||||
assert.True(t, f.Review)
|
||||
assert.False(t, f.Share)
|
||||
assert.False(t, f.Favorites)
|
||||
assert.False(t, f.Reactions)
|
||||
assert.False(t, f.Ratings)
|
||||
})
|
||||
}
|
||||
|
||||
func TestConfig_ClientSessionConfig(t *testing.T) {
|
||||
c := NewTestConfig("config")
|
||||
|
||||
c.Options().AuthMode = AuthModePasswd
|
||||
c.Options().Public = false
|
||||
c.SetAuthMode(AuthModePasswd)
|
||||
|
||||
assert.Equal(t, AuthModePasswd, c.AuthMode())
|
||||
|
||||
|
@ -236,6 +208,8 @@ func TestConfig_ClientSessionConfig(t *testing.T) {
|
|||
cfg := c.ClientSession(entity.SessionFixtures.Pointer("alice"))
|
||||
assert.IsType(t, ClientConfig{}, cfg)
|
||||
assert.Equal(t, false, cfg.Public)
|
||||
assert.NotEmpty(t, cfg.PreviewToken)
|
||||
assert.NotEmpty(t, cfg.DownloadToken)
|
||||
|
||||
f := cfg.Settings.Features
|
||||
assert.Equal(t, adminFeatures, f)
|
||||
|
@ -265,6 +239,8 @@ func TestConfig_ClientSessionConfig(t *testing.T) {
|
|||
|
||||
assert.IsType(t, ClientConfig{}, cfg)
|
||||
assert.Equal(t, false, cfg.Public)
|
||||
assert.NotEmpty(t, cfg.PreviewToken)
|
||||
assert.NotEmpty(t, cfg.DownloadToken)
|
||||
|
||||
f := cfg.Settings.Features
|
||||
assert.NotEqual(t, adminFeatures, f)
|
||||
|
@ -290,13 +266,15 @@ func TestConfig_ClientSessionConfig(t *testing.T) {
|
|||
assert.True(t, f.Review)
|
||||
assert.False(t, f.Share)
|
||||
})
|
||||
t.Run("RoleUnauthorized", func(t *testing.T) {
|
||||
t.Run("RoleUnknown", func(t *testing.T) {
|
||||
sess := entity.SessionFixtures.Pointer("unauthorized")
|
||||
|
||||
cfg := c.ClientSession(sess)
|
||||
|
||||
assert.IsType(t, ClientConfig{}, cfg)
|
||||
assert.Equal(t, false, cfg.Public)
|
||||
assert.NotEmpty(t, cfg.PreviewToken)
|
||||
assert.NotEmpty(t, cfg.DownloadToken)
|
||||
|
||||
f := cfg.Settings.Features
|
||||
assert.NotEqual(t, adminFeatures, f)
|
||||
|
@ -326,7 +304,8 @@ func TestConfig_ClientSessionConfig(t *testing.T) {
|
|||
|
||||
assert.IsType(t, ClientConfig{}, cfg)
|
||||
assert.Equal(t, false, cfg.Public)
|
||||
|
||||
assert.NotEmpty(t, cfg.PreviewToken)
|
||||
assert.NotEmpty(t, cfg.DownloadToken)
|
||||
f := cfg.Settings.Features
|
||||
|
||||
assert.True(t, f.Search)
|
||||
|
|
|
@ -157,6 +157,11 @@ func (c *Config) Propagate() {
|
|||
places.UserAgent = c.UserAgent()
|
||||
entity.GeoApi = c.GeoApi()
|
||||
|
||||
// Set API preview and download default tokens.
|
||||
entity.PreviewToken.Set(c.PreviewToken(), entity.TokenConfig)
|
||||
entity.DownloadToken.Set(c.DownloadToken(), entity.TokenConfig)
|
||||
entity.CheckTokens = !c.Public()
|
||||
|
||||
// Set face recognition parameters.
|
||||
face.ScoreThreshold = c.FaceScore()
|
||||
face.OverlapThreshold = c.FaceOverlap()
|
||||
|
|
|
@ -3,6 +3,8 @@ package config
|
|||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
|
@ -75,9 +77,11 @@ func (c *Config) SetAuthMode(mode string) {
|
|||
case AuthModePublic:
|
||||
c.options.AuthMode = AuthModePublic
|
||||
c.options.Public = true
|
||||
entity.CheckTokens = false
|
||||
default:
|
||||
c.options.AuthMode = AuthModePasswd
|
||||
c.options.Public = false
|
||||
entity.CheckTokens = true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -112,31 +116,28 @@ func (c *Config) CheckPassword(p string) bool {
|
|||
return ap == p
|
||||
}
|
||||
|
||||
// InvalidDownloadToken checks if the token is invalid.
|
||||
func (c *Config) InvalidDownloadToken(t string) bool {
|
||||
return c.DownloadToken() != t
|
||||
}
|
||||
|
||||
// DownloadToken returns the DOWNLOAD api token (you can optionally use a static value for permanent caching).
|
||||
func (c *Config) DownloadToken() string {
|
||||
if c.options.DownloadToken == "" {
|
||||
if c.Public() {
|
||||
return entity.TokenPublic
|
||||
} else if c.options.DownloadToken == "" {
|
||||
c.options.DownloadToken = rnd.GenerateToken(8)
|
||||
}
|
||||
|
||||
return c.options.DownloadToken
|
||||
}
|
||||
|
||||
// InvalidPreviewToken checks if the preview token is invalid.
|
||||
func (c *Config) InvalidPreviewToken(t string) bool {
|
||||
return c.PreviewToken() != t && c.DownloadToken() != t
|
||||
// InvalidDownloadToken checks if the token is invalid.
|
||||
func (c *Config) InvalidDownloadToken(t string) bool {
|
||||
return entity.InvalidDownloadToken(t)
|
||||
}
|
||||
|
||||
// PreviewToken returns the preview image api token (based on the unique storage serial by default).
|
||||
func (c *Config) PreviewToken() string {
|
||||
if c.options.PreviewToken == "" {
|
||||
if c.Public() {
|
||||
c.options.PreviewToken = "public"
|
||||
} else if c.Serial() == "" {
|
||||
if c.Public() {
|
||||
return entity.TokenPublic
|
||||
} else if c.options.PreviewToken == "" {
|
||||
if c.Serial() == "" {
|
||||
return "********"
|
||||
} else {
|
||||
c.options.PreviewToken = c.SerialChecksum()
|
||||
|
@ -145,3 +146,8 @@ func (c *Config) PreviewToken() string {
|
|||
|
||||
return c.options.PreviewToken
|
||||
}
|
||||
|
||||
// InvalidPreviewToken checks if the preview token is invalid.
|
||||
func (c *Config) InvalidPreviewToken(t string) bool {
|
||||
return entity.InvalidPreviewToken(t)
|
||||
}
|
||||
|
|
|
@ -280,6 +280,7 @@ func (c *Config) InitDb() {
|
|||
|
||||
// MigrateDb initializes the database and migrates the schema if needed.
|
||||
func (c *Config) MigrateDb(runFailed bool, ids []string) {
|
||||
entity.Admin.UserName = c.AdminUser()
|
||||
entity.InitDb(true, runFailed, ids)
|
||||
|
||||
// Init admin account?
|
||||
|
|
|
@ -56,7 +56,7 @@ type Options struct {
|
|||
WakeupInterval time.Duration `yaml:"WakeupInterval" json:"WakeupInterval" flag:"wakeup-interval"`
|
||||
AutoIndex int `yaml:"AutoIndex" json:"AutoIndex" flag:"auto-index"`
|
||||
AutoImport int `yaml:"AutoImport" json:"AutoImport" flag:"auto-import"`
|
||||
DisableWebDAV bool `yaml:"DisableWebDAV" json:"DisableWebDAV" flag:"disable-webdav"`
|
||||
DisableWebDAV bool `yaml:"WebDAV" json:"WebDAV" flag:"disable-webdav"`
|
||||
DisableBackups bool `yaml:"DisableBackups" json:"DisableBackups" flag:"disable-backups"`
|
||||
DisableSettings bool `yaml:"DisableSettings" json:"-" flag:"disable-settings"`
|
||||
DisablePlaces bool `yaml:"DisablePlaces" json:"DisablePlaces" flag:"disable-places"`
|
||||
|
|
|
@ -43,8 +43,18 @@ var PkgNameRegexp = regexp.MustCompile("[^a-zA-Z\\-_]+")
|
|||
|
||||
// NewTestOptions returns valid config options for tests.
|
||||
func NewTestOptions(pkg string) *Options {
|
||||
assetsPath := fs.Abs("../../assets")
|
||||
storagePath := fs.Abs("../../storage")
|
||||
// Find assets path.
|
||||
assetsPath := os.Getenv("PHOTOPRISM_ASSETS_PATH")
|
||||
if assetsPath == "" {
|
||||
fs.Abs("../../assets")
|
||||
}
|
||||
|
||||
// Find storage path.
|
||||
storagePath := os.Getenv("PHOTOPRISM_STORAGE_PATH")
|
||||
if storagePath == "" {
|
||||
storagePath = fs.Abs("../../storage")
|
||||
}
|
||||
|
||||
dataPath := filepath.Join(storagePath, "testdata")
|
||||
|
||||
pkg = PkgNameRegexp.ReplaceAllString(pkg, "")
|
||||
|
|
25
internal/customize/customize.go
Normal file
25
internal/customize/customize.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
Package customize provides user settings to customize the app.
|
||||
|
||||
Copyright (c) 2018 - 2022 PhotoPrism UG. All rights reserved.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under Version 3 of the GNU Affero General Public License (the "AGPL"):
|
||||
<https://docs.photoprism.app/license/agpl>
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
The AGPL is supplemented by our Trademark and Brand Guidelines,
|
||||
which describe how our Brand Assets may be used:
|
||||
<https://photoprism.app/trademark>
|
||||
|
||||
Feel free to send an email to hello@photoprism.app if you have questions,
|
||||
want to support our work, or just want to say hello.
|
||||
|
||||
Additional information can be found in our Developer Guide:
|
||||
<https://docs.photoprism.app/developer-guide/>
|
||||
*/
|
||||
package customize
|
|
@ -7,7 +7,7 @@ import (
|
|||
)
|
||||
|
||||
func TestFlushAlbumCache(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
FlushAlbumCache()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package entity
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
@ -83,6 +84,16 @@ func NewSession(maxAge, timeout int64) (m *Session) {
|
|||
return m
|
||||
}
|
||||
|
||||
// Expires sets an explicit expiration time.
|
||||
func (m *Session) Expires(t time.Time) *Session {
|
||||
if t.IsZero() {
|
||||
return m
|
||||
}
|
||||
|
||||
m.SessExpires = t.Unix()
|
||||
return m
|
||||
}
|
||||
|
||||
// DeleteExpiredSessions deletes expired sessions.
|
||||
func DeleteExpiredSessions() (deleted int) {
|
||||
expired := Sessions{}
|
||||
|
@ -139,7 +150,7 @@ func (m *Session) CacheDuration(d time.Duration) {
|
|||
return
|
||||
}
|
||||
|
||||
sessionCache.Set(m.ID, m, d)
|
||||
CacheSession(m, d)
|
||||
}
|
||||
|
||||
// Cache caches the session with the default expiration duration.
|
||||
|
@ -167,10 +178,9 @@ func (m *Session) Save() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Delete removes a session.
|
||||
// Delete permanently deletes a session.
|
||||
func (m *Session) Delete() error {
|
||||
DeleteFromSessionCache(m.ID)
|
||||
return UnscopedDb().Delete(m).Error
|
||||
return DeleteSession(m)
|
||||
}
|
||||
|
||||
// Updates multiple properties in the database.
|
||||
|
@ -237,12 +247,58 @@ func (m *Session) SetUser(u *User) *Session {
|
|||
m.UserName = u.UserName
|
||||
}
|
||||
|
||||
if u.DownloadToken != "" {
|
||||
m.DownloadToken = u.DownloadToken
|
||||
m.SetPreviewToken(u.PreviewToken)
|
||||
m.SetDownloadToken(u.DownloadToken)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// ChangePassword changes the password of the current user.
|
||||
func (m *Session) ChangePassword(newPw string) (err error) {
|
||||
u := m.User()
|
||||
|
||||
if u == nil {
|
||||
return fmt.Errorf("unknown user")
|
||||
}
|
||||
|
||||
if u.PreviewToken != "" {
|
||||
m.PreviewToken = u.PreviewToken
|
||||
// Change password.
|
||||
err = u.SetPassword(newPw)
|
||||
|
||||
m.SetPreviewToken(u.PreviewToken)
|
||||
m.SetDownloadToken(u.DownloadToken)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetPreviewToken updates the preview token if not empty.
|
||||
func (m *Session) SetPreviewToken(token string) *Session {
|
||||
if m.ID == "" {
|
||||
return m
|
||||
}
|
||||
|
||||
if token != "" {
|
||||
m.PreviewToken = token
|
||||
PreviewToken.Set(token, m.ID)
|
||||
} else if m.PreviewToken == "" {
|
||||
m.PreviewToken = GenerateToken()
|
||||
PreviewToken.Set(token, m.ID)
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// SetDownloadToken updates the download token if not empty.
|
||||
func (m *Session) SetDownloadToken(token string) *Session {
|
||||
if m.ID == "" {
|
||||
return m
|
||||
}
|
||||
|
||||
if token != "" {
|
||||
m.DownloadToken = token
|
||||
DownloadToken.Set(token, m.ID)
|
||||
} else if m.DownloadToken == "" {
|
||||
m.DownloadToken = GenerateToken()
|
||||
DownloadToken.Set(token, m.ID)
|
||||
}
|
||||
|
||||
return m
|
||||
|
|
|
@ -40,7 +40,7 @@ func FindSession(id string) (*Session, error) {
|
|||
return found, fmt.Errorf("has invalid id %s", clean.LogQuote(found.ID))
|
||||
} else if !found.Expired() {
|
||||
found.UpdateLastActive()
|
||||
sessionCache.SetDefault(found.ID, found)
|
||||
CacheSession(found, sessionCacheExpiration)
|
||||
return found, nil
|
||||
} else if err := found.Delete(); err != nil {
|
||||
event.AuditErr([]string{found.IP(), "session %s", "failed to delete after expiration", "%s"}, found.RefID, err)
|
||||
|
@ -54,6 +54,50 @@ func FlushSessionCache() {
|
|||
sessionCache.Flush()
|
||||
}
|
||||
|
||||
// CacheSession adds a session to the cache if its ID is valid.
|
||||
func CacheSession(s *Session, d time.Duration) {
|
||||
if s == nil {
|
||||
return
|
||||
} else if !rnd.IsSessionID(s.ID) {
|
||||
return
|
||||
}
|
||||
|
||||
if d == 0 {
|
||||
d = sessionCacheExpiration
|
||||
}
|
||||
|
||||
if s.PreviewToken != "" {
|
||||
PreviewToken.Set(s.PreviewToken, s.ID)
|
||||
}
|
||||
|
||||
if s.DownloadToken != "" {
|
||||
DownloadToken.Set(s.DownloadToken, s.ID)
|
||||
}
|
||||
|
||||
sessionCache.Set(s.ID, s, d)
|
||||
}
|
||||
|
||||
// DeleteSession permanently deletes a session.
|
||||
func DeleteSession(s *Session) error {
|
||||
if s == nil {
|
||||
return nil
|
||||
} else if !rnd.IsSessionID(s.ID) {
|
||||
return fmt.Errorf("invalid session id")
|
||||
}
|
||||
|
||||
DeleteFromSessionCache(s.ID)
|
||||
|
||||
if s.PreviewToken != "" {
|
||||
PreviewToken.Set(s.PreviewToken, s.ID)
|
||||
}
|
||||
|
||||
if s.DownloadToken != "" {
|
||||
DownloadToken.Set(s.DownloadToken, s.ID)
|
||||
}
|
||||
|
||||
return UnscopedDb().Delete(s).Error
|
||||
}
|
||||
|
||||
// DeleteFromSessionCache deletes a session from the cache.
|
||||
func DeleteFromSessionCache(id string) {
|
||||
if id == "" {
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
)
|
||||
|
||||
func TestFlushSessionCache(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
FlushSessionCache()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -2,6 +2,9 @@ package entity
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
|
@ -38,7 +41,7 @@ func (m *Session) LogIn(f form.Login, c *gin.Context) (err error) {
|
|||
}
|
||||
|
||||
// Login allowed?
|
||||
if !user.LoginAllowed() {
|
||||
if !user.CanLogIn() {
|
||||
message := "account disabled"
|
||||
event.AuditWarn([]string{m.IP(), "session %s", "login as %s", message}, m.RefID, clean.LogQuote(name))
|
||||
event.LoginError(m.IP(), "api", name, m.UserAgent, message)
|
||||
|
@ -94,6 +97,9 @@ func (m *Session) LogIn(f form.Login, c *gin.Context) (err error) {
|
|||
if user.IsUnknown() {
|
||||
user = &Visitor
|
||||
event.AuditDebug([]string{m.IP(), "session %s", "role upgraded to %s"}, m.RefID, user.AclRole().String())
|
||||
expires := UTC().Add(time.Hour * 24)
|
||||
m.Expires(expires)
|
||||
event.AuditDebug([]string{m.IP(), "session %s", "expires at %s"}, m.RefID, txt.TimeStamp(&expires))
|
||||
}
|
||||
|
||||
m.SetUser(user)
|
||||
|
|
27
internal/entity/auth_tokens.go
Normal file
27
internal/entity/auth_tokens.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
const TokenConfig = "__config__"
|
||||
const TokenPublic = "public"
|
||||
|
||||
var PreviewToken = NewStringMap(Strings{})
|
||||
var DownloadToken = NewStringMap(Strings{})
|
||||
var CheckTokens = true
|
||||
|
||||
// GenerateToken returns a random string token.
|
||||
func GenerateToken() string {
|
||||
return rnd.GenerateToken(8)
|
||||
}
|
||||
|
||||
// InvalidDownloadToken checks if the token is unknown.
|
||||
func InvalidDownloadToken(t string) bool {
|
||||
return CheckTokens && DownloadToken.Missing(t)
|
||||
}
|
||||
|
||||
// InvalidPreviewToken checks if the preview token is unknown.
|
||||
func InvalidPreviewToken(t string) bool {
|
||||
return CheckTokens && PreviewToken.Missing(t) && DownloadToken.Missing(t)
|
||||
}
|
|
@ -44,15 +44,15 @@ type User struct {
|
|||
DisplayName string `gorm:"size:200;" json:"DisplayName" yaml:"DisplayName,omitempty"`
|
||||
UserEmail string `gorm:"size:255;index;" json:"Email" yaml:"Email,omitempty"`
|
||||
BackupEmail string `gorm:"size:255;" json:"BackupEmail,omitempty" yaml:"BackupEmail,omitempty"`
|
||||
UserRole string `gorm:"size:64;default:'restricted';" json:"Role,omitempty" yaml:"Role,omitempty"`
|
||||
UserRole string `gorm:"size:64;default:'';" json:"Role,omitempty" yaml:"Role,omitempty"`
|
||||
UserAttr string `gorm:"size:1024;" json:"Attr,omitempty" yaml:"Attr,omitempty"`
|
||||
SuperAdmin bool `json:"SuperAdmin,omitempty" yaml:"SuperAdmin,omitempty"`
|
||||
CanLogin bool `json:"CanLogin,omitempty" yaml:"CanLogin,omitempty"`
|
||||
LoginAt *time.Time `json:"LoginAt,omitempty" yaml:"LoginAt,omitempty"`
|
||||
ExpiresAt *time.Time `sql:"index" json:"ExpiresAt,omitempty" yaml:"ExpiresAt,omitempty"`
|
||||
WebDAV bool `gorm:"column:webdav;" json:"WebDAV,omitempty" yaml:"WebDAV,omitempty"`
|
||||
BasePath string `gorm:"type:VARBINARY(1024);" json:"BasePath,omitempty" yaml:"BasePath,omitempty"`
|
||||
UploadPath string `gorm:"type:VARBINARY(1024);" json:"UploadPath,omitempty" yaml:"UploadPath,omitempty"`
|
||||
CanSync bool `json:"CanSync,omitempty" yaml:"CanSync,omitempty"`
|
||||
CanInvite bool `json:"CanInvite,omitempty" yaml:"CanInvite,omitempty"`
|
||||
InviteToken string `gorm:"type:VARBINARY(64);index;" json:"-" yaml:"-"`
|
||||
InvitedBy string `gorm:"size:64;" json:"-" yaml:"-"`
|
||||
|
@ -84,60 +84,75 @@ func NewUser() (m *User) {
|
|||
uid := rnd.GenerateUID(UserUID)
|
||||
|
||||
return &User{
|
||||
UserUID: uid,
|
||||
UserDetails: NewUserDetails(uid),
|
||||
UserSettings: NewUserSettings(uid),
|
||||
RefID: rnd.RefID(UserPrefix),
|
||||
UserUID: uid,
|
||||
UserDetails: NewUserDetails(uid),
|
||||
UserSettings: NewUserSettings(uid),
|
||||
PreviewToken: GenerateToken(),
|
||||
DownloadToken: GenerateToken(),
|
||||
RefID: rnd.RefID(UserPrefix),
|
||||
}
|
||||
}
|
||||
|
||||
// FindUser returns the matching user or nil if it was not found.
|
||||
func FindUser(find User) *User {
|
||||
m := &User{}
|
||||
|
||||
// Build query.
|
||||
stmt := UnscopedDb()
|
||||
if find.ID != 0 {
|
||||
stmt = stmt.Where("id = ?", find.ID)
|
||||
} else if rnd.IsUID(find.UserUID, UserUID) {
|
||||
stmt = stmt.Where("user_uid = ?", find.UserUID)
|
||||
} else if find.UserName != "" {
|
||||
stmt = stmt.Where("user_name = ?", find.UserName)
|
||||
} else if find.UserEmail != "" {
|
||||
stmt = stmt.Where("user_email = ?", find.UserEmail)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find matching record.
|
||||
if err := stmt.First(m).Error; err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fetch related records.
|
||||
return m.LoadRelated()
|
||||
}
|
||||
|
||||
// FirstOrCreateUser returns an existing record, inserts a new record, or returns nil in case of an error.
|
||||
func FirstOrCreateUser(m *User) *User {
|
||||
result := User{}
|
||||
|
||||
if err := Db().Where("id = ? OR (user_uid = ? AND user_uid <> '') OR (user_name = ? AND user_name <> '')", m.ID, m.UserUID, m.UserName).First(&result).Error; err == nil {
|
||||
return &result
|
||||
} else if err = m.Create(); err != nil {
|
||||
event.AuditErr([]string{"user", "failed to create", "%s"}, err)
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return m
|
||||
if found := FindUser(*m); found != nil {
|
||||
return found
|
||||
} else if err := m.Create(); err != nil {
|
||||
event.AuditErr([]string{"user", "failed to create", "%s"}, err)
|
||||
return nil
|
||||
} else {
|
||||
return m
|
||||
}
|
||||
}
|
||||
|
||||
// FindUserByName finds a user by its username or returns nil if it was not found.
|
||||
// FindUserByName returns the matching user or nil if it was not found.
|
||||
func FindUserByName(name string) *User {
|
||||
name = clean.Username(name)
|
||||
if name == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
m := &User{}
|
||||
|
||||
// Find matching record.
|
||||
if Db().First(m, "user_name = ?", name).RecordNotFound() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fetch related settings and details.
|
||||
return m.LoadRelated()
|
||||
return FindUser(User{UserName: name})
|
||||
}
|
||||
|
||||
// FindUserByUID returns an existing user or nil if not found.
|
||||
// FindUserByUID returns the matching user or nil if it was not found.
|
||||
func FindUserByUID(uid string) *User {
|
||||
if rnd.InvalidUID(uid, UserUID) {
|
||||
return nil
|
||||
}
|
||||
|
||||
m := &User{}
|
||||
|
||||
// Find matching record.
|
||||
if UnscopedDb().First(m, "user_uid = ?", uid).RecordNotFound() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fetch related settings and details.
|
||||
return m.LoadRelated()
|
||||
return FindUser(User{UserUID: uid})
|
||||
}
|
||||
|
||||
// UID returns the unique id as string.
|
||||
|
@ -161,34 +176,36 @@ func (m *User) SameUID(uid string) bool {
|
|||
}
|
||||
|
||||
// InitAccount sets the name and password of the initial admin account.
|
||||
func (m *User) InitAccount(login, password string) (updated bool) {
|
||||
if !m.IsRegistered() {
|
||||
log.Warn("only registered users can change their password")
|
||||
func (m *User) InitAccount(initName, initPasswd string) (updated bool) {
|
||||
// User must exist and the password must not be empty.
|
||||
initPasswd = strings.TrimSpace(initPasswd)
|
||||
if rnd.InvalidUID(m.UserUID, UserUID) || initPasswd == "" {
|
||||
return false
|
||||
} else if !m.CanLogIn() {
|
||||
log.Warnf("users: %s account is not allowed to log in", m.String())
|
||||
}
|
||||
|
||||
// Abort if user has a password.
|
||||
existingPasswd := FindPassword(m.UserUID)
|
||||
|
||||
if existingPasswd != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Password must not be empty.
|
||||
if password == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
existing := FindPassword(m.UserUID)
|
||||
|
||||
if existing != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
pw := NewPassword(m.UserUID, password)
|
||||
// Set initial password.
|
||||
initialPasswd := NewPassword(m.UserUID, initPasswd)
|
||||
|
||||
// Save password.
|
||||
if err := pw.Save(); err != nil {
|
||||
if err := initialPasswd.Save(); err != nil {
|
||||
event.AuditErr([]string{"user %s", "failed to change password", "%s"}, m.RefID, err)
|
||||
return false
|
||||
}
|
||||
|
||||
// Change username.
|
||||
if err := m.UpdateName(login); err != nil {
|
||||
event.AuditErr([]string{"user %s", "failed to change username to %s", "%s"}, m.RefID, clean.Log(login), err)
|
||||
// Change username if needed.
|
||||
if initName != "" && initName != m.UserName {
|
||||
if err := m.UpdateName(initName); err != nil {
|
||||
event.AuditErr([]string{"user %s", "failed to change username to %s", "%s"}, m.RefID, clean.Log(initName), err)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
|
@ -207,6 +224,8 @@ func (m *User) Create() (err error) {
|
|||
|
||||
// Save updates the record in the database or inserts a new record if it does not already exist.
|
||||
func (m *User) Save() (err error) {
|
||||
m.GenerateTokens(false)
|
||||
|
||||
err = Db().Save(m).Error
|
||||
|
||||
if err == nil {
|
||||
|
@ -269,6 +288,8 @@ func (m *User) BeforeCreate(scope *gorm.Scope) error {
|
|||
m.UserDetails.UserUID = m.UserUID
|
||||
}
|
||||
|
||||
m.GenerateTokens(false)
|
||||
|
||||
if rnd.InvalidRefID(m.RefID) {
|
||||
m.RefID = rnd.RefID(UserPrefix)
|
||||
Log("user", "set ref id", scope.SetColumn("RefID", m.RefID))
|
||||
|
@ -293,12 +314,14 @@ func (m *User) Expired() bool {
|
|||
|
||||
// Disabled checks if the user account has been deleted or has expired.
|
||||
func (m *User) Disabled() bool {
|
||||
return m.Deleted() || m.Expired()
|
||||
return m.Deleted() || m.Expired() && !m.SuperAdmin
|
||||
}
|
||||
|
||||
// LoginAllowed checks if the user is allowed to log in and use the web UI.
|
||||
func (m *User) LoginAllowed() bool {
|
||||
if role := m.AclRole(); m.Disabled() || !m.CanLogin || m.ID <= 0 || m.UserName == "" || role == acl.RoleUnauthorized {
|
||||
// CanLogIn checks if the user is allowed to log in and use the web UI.
|
||||
func (m *User) CanLogIn() bool {
|
||||
if !m.CanLogin && !m.SuperAdmin || m.ID <= 0 || m.UserName == "" {
|
||||
return false
|
||||
} else if role := m.AclRole(); m.Disabled() || role == acl.RoleUnknown {
|
||||
return false
|
||||
} else {
|
||||
return acl.Resources.Allow(acl.ResourceConfig, role, acl.AccessOwn)
|
||||
|
@ -306,18 +329,18 @@ func (m *User) LoginAllowed() bool {
|
|||
|
||||
}
|
||||
|
||||
// SyncAllowed checks whether the user is allowed to use WebDAV to synchronize files.
|
||||
func (m *User) SyncAllowed() bool {
|
||||
if role := m.AclRole(); m.Disabled() || !m.CanSync || m.ID <= 0 || m.UserName == "" || role == acl.RoleUnauthorized {
|
||||
// CanUseWebDAV checks whether the user is allowed to use WebDAV to synchronize files.
|
||||
func (m *User) CanUseWebDAV() bool {
|
||||
if role := m.AclRole(); m.Disabled() || !m.WebDAV || m.ID <= 0 || m.UserName == "" || role == acl.RoleUnknown {
|
||||
return false
|
||||
} else {
|
||||
return acl.Resources.Allow(acl.ResourcePhotos, role, acl.ActionUpload)
|
||||
}
|
||||
}
|
||||
|
||||
// UploadAllowed checks if the user is allowed to upload files.
|
||||
func (m *User) UploadAllowed() bool {
|
||||
if role := m.AclRole(); m.Disabled() || role == acl.RoleUnauthorized {
|
||||
// CanUpload checks if the user is allowed to upload files.
|
||||
func (m *User) CanUpload() bool {
|
||||
if role := m.AclRole(); m.Disabled() || role == acl.RoleUnknown {
|
||||
return false
|
||||
} else {
|
||||
return acl.Resources.Allow(acl.ResourcePhotos, role, acl.ActionUpload)
|
||||
|
@ -414,7 +437,7 @@ func (m *User) AclRole() acl.Role {
|
|||
case m.SuperAdmin:
|
||||
return acl.RoleAdmin
|
||||
case role == "":
|
||||
return acl.RoleUnauthorized
|
||||
return acl.RoleUnknown
|
||||
case m.UserName == "":
|
||||
return acl.RoleVisitor
|
||||
default:
|
||||
|
@ -531,7 +554,11 @@ func (m *User) SetPassword(password string) error {
|
|||
|
||||
pw := NewPassword(m.UserUID, password)
|
||||
|
||||
return pw.Save()
|
||||
if err := pw.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.RegenerateTokens()
|
||||
}
|
||||
|
||||
// HasPassword checks if the user has the specified password and the account is registered.
|
||||
|
@ -629,7 +656,7 @@ func (m *User) SetFormValues(frm form.User) *User {
|
|||
m.DisplayName = frm.DisplayName
|
||||
m.SuperAdmin = frm.SuperAdmin
|
||||
m.CanLogin = frm.CanLogin
|
||||
m.CanSync = frm.CanSync
|
||||
m.WebDAV = frm.WebDAV
|
||||
m.UserRole = frm.Role()
|
||||
m.UserAttr = frm.Attr()
|
||||
m.SetBasePath(frm.BasePath)
|
||||
|
@ -638,6 +665,34 @@ func (m *User) SetFormValues(frm form.User) *User {
|
|||
return m
|
||||
}
|
||||
|
||||
// GenerateTokens generates preview and download tokens as needed.
|
||||
func (m *User) GenerateTokens(force bool) *User {
|
||||
if m.ID < 0 {
|
||||
return m
|
||||
}
|
||||
|
||||
if m.PreviewToken == "" || force {
|
||||
m.PreviewToken = GenerateToken()
|
||||
}
|
||||
|
||||
if m.DownloadToken == "" || force {
|
||||
m.DownloadToken = GenerateToken()
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// RegenerateTokens replaces the existing preview and download tokens.
|
||||
func (m *User) RegenerateTokens() error {
|
||||
if m.ID < 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
m.GenerateTokens(true)
|
||||
|
||||
return m.Updates(Values{"PreviewToken": m.PreviewToken, "DownloadToken": m.DownloadToken})
|
||||
}
|
||||
|
||||
// RefreshShares updates the list of shares.
|
||||
func (m *User) RefreshShares() *User {
|
||||
m.UserShares = FindUserShares(m.UID())
|
||||
|
|
|
@ -31,17 +31,17 @@ func (m *User) SetValuesFromCli(ctx *cli.Context) error {
|
|||
m.SuperAdmin = frm.SuperAdmin
|
||||
}
|
||||
|
||||
// Disable Web UI?
|
||||
// Disable login (Web UI)?
|
||||
if ctx.IsSet("no-login") {
|
||||
m.CanLogin = frm.CanLogin
|
||||
}
|
||||
|
||||
// Can use WebDAV.
|
||||
if ctx.IsSet("can-sync") {
|
||||
m.CanSync = frm.CanSync
|
||||
// Allow the use of WebDAV?
|
||||
if ctx.IsSet("webdav") {
|
||||
m.WebDAV = frm.WebDAV
|
||||
}
|
||||
|
||||
// Custom attributes.
|
||||
// Set custom attributes?
|
||||
if ctx.IsSet("attr") {
|
||||
m.UserAttr = frm.Attr()
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package entity
|
|||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
)
|
||||
|
||||
// Role defaults.
|
||||
|
@ -10,53 +10,83 @@ const (
|
|||
AdminUserName = "admin"
|
||||
AdminDisplayName = "Admin"
|
||||
VisitorDisplayName = "Visitor"
|
||||
UnknownDisplayName = "Unknown"
|
||||
)
|
||||
|
||||
// Admin is the default admin user.
|
||||
var Admin = User{
|
||||
ID: 1,
|
||||
UserName: AdminUserName,
|
||||
UserRole: acl.RoleAdmin.String(),
|
||||
DisplayName: AdminDisplayName,
|
||||
SuperAdmin: true,
|
||||
CanLogin: true,
|
||||
CanSync: true,
|
||||
CanInvite: true,
|
||||
InviteToken: rnd.GenerateToken(8),
|
||||
ID: 1,
|
||||
UserName: AdminUserName,
|
||||
UserRole: acl.RoleAdmin.String(),
|
||||
DisplayName: AdminDisplayName,
|
||||
SuperAdmin: true,
|
||||
CanLogin: true,
|
||||
WebDAV: true,
|
||||
CanInvite: true,
|
||||
InviteToken: GenerateToken(),
|
||||
PreviewToken: GenerateToken(),
|
||||
DownloadToken: GenerateToken(),
|
||||
}
|
||||
|
||||
// UnknownUser is an anonymous, public user without own account.
|
||||
var UnknownUser = User{
|
||||
ID: -1,
|
||||
UserUID: "u000000000000001",
|
||||
UserRole: acl.RoleUnauthorized.String(),
|
||||
UserName: "",
|
||||
DisplayName: "",
|
||||
SuperAdmin: false,
|
||||
CanLogin: false,
|
||||
CanSync: false,
|
||||
CanInvite: false,
|
||||
InviteToken: "",
|
||||
ID: -1,
|
||||
UserUID: "u000000000000001",
|
||||
UserName: "",
|
||||
UserRole: acl.RoleUnknown.String(),
|
||||
CanLogin: false,
|
||||
WebDAV: false,
|
||||
CanInvite: false,
|
||||
DisplayName: UnknownDisplayName,
|
||||
InviteToken: "",
|
||||
PreviewToken: "",
|
||||
DownloadToken: "",
|
||||
}
|
||||
|
||||
// Visitor is a user without own account e.g. for link sharing.
|
||||
var Visitor = User{
|
||||
ID: -2,
|
||||
UserUID: "u000000000000002",
|
||||
UserRole: acl.RoleVisitor.String(),
|
||||
UserName: "",
|
||||
DisplayName: VisitorDisplayName,
|
||||
SuperAdmin: false,
|
||||
CanLogin: false,
|
||||
CanSync: false,
|
||||
CanInvite: false,
|
||||
InviteToken: "",
|
||||
ID: -2,
|
||||
UserUID: "u000000000000002",
|
||||
UserName: "",
|
||||
UserRole: acl.RoleVisitor.String(),
|
||||
DisplayName: VisitorDisplayName,
|
||||
CanLogin: false,
|
||||
WebDAV: false,
|
||||
CanInvite: false,
|
||||
InviteToken: "",
|
||||
PreviewToken: "",
|
||||
DownloadToken: "",
|
||||
}
|
||||
|
||||
// CreateDefaultUsers initializes the database with default user accounts.
|
||||
func CreateDefaultUsers() {
|
||||
if user := FirstOrCreateUser(&Admin); user != nil {
|
||||
Admin = *user
|
||||
if admin := FindUser(Admin); admin != nil {
|
||||
Admin = *admin
|
||||
} else {
|
||||
// Set legacy values.
|
||||
if leg := FindLegacyUser(Admin); leg != nil {
|
||||
Admin.UserUID = leg.UserUID
|
||||
Admin.UserName = leg.UserName
|
||||
Admin.UserEmail = leg.PrimaryEmail
|
||||
Admin.DisplayName = leg.FullName
|
||||
Admin.LoginAt = leg.LoginAt
|
||||
log.Infof("users: migrating %s account", Admin.UserName)
|
||||
}
|
||||
|
||||
// Set default values.
|
||||
Admin.SuperAdmin = true
|
||||
Admin.CanLogin = true
|
||||
Admin.WebDAV = true
|
||||
|
||||
// Username is required.
|
||||
if Admin.UserName == "" {
|
||||
Admin.UserName = "admin"
|
||||
}
|
||||
|
||||
// Add initial admin account.
|
||||
if err := Admin.Create(); err != nil {
|
||||
event.AuditErr([]string{"user", "failed to create", "%s"}, err)
|
||||
}
|
||||
}
|
||||
|
||||
if user := FirstOrCreateUser(&UnknownUser); user != nil {
|
||||
|
|
|
@ -2,7 +2,6 @@ package entity
|
|||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
type UserMap map[string]User
|
||||
|
@ -36,9 +35,9 @@ var UserFixtures = UserMap{
|
|||
UserRole: acl.RoleAdmin.String(),
|
||||
SuperAdmin: true,
|
||||
CanLogin: true,
|
||||
CanSync: true,
|
||||
WebDAV: true,
|
||||
CanInvite: true,
|
||||
InviteToken: rnd.GenerateToken(8),
|
||||
InviteToken: GenerateToken(),
|
||||
UserSettings: &UserSettings{
|
||||
UITheme: "",
|
||||
MapsStyle: "",
|
||||
|
@ -60,7 +59,7 @@ var UserFixtures = UserMap{
|
|||
UserRole: acl.RoleAdmin.String(),
|
||||
SuperAdmin: false,
|
||||
CanLogin: true,
|
||||
CanSync: true,
|
||||
WebDAV: true,
|
||||
CanInvite: false,
|
||||
UserSettings: &UserSettings{
|
||||
UITheme: "grayscale",
|
||||
|
@ -86,7 +85,7 @@ var UserFixtures = UserMap{
|
|||
SuperAdmin: false,
|
||||
DisplayName: "Guy Friend",
|
||||
CanLogin: true,
|
||||
CanSync: false,
|
||||
WebDAV: false,
|
||||
CanInvite: false,
|
||||
UserSettings: &UserSettings{
|
||||
UITheme: "gemstone",
|
||||
|
@ -108,7 +107,7 @@ var UserFixtures = UserMap{
|
|||
SuperAdmin: false,
|
||||
UserRole: acl.RoleVisitor.String(),
|
||||
CanLogin: false,
|
||||
CanSync: true,
|
||||
WebDAV: true,
|
||||
CanInvite: false,
|
||||
DeletedAt: TimePointer(),
|
||||
UserSettings: &UserSettings{
|
||||
|
@ -124,11 +123,11 @@ var UserFixtures = UserMap{
|
|||
UserUID: "uriku0138hqql4bz",
|
||||
UserName: "jens.mander",
|
||||
UserEmail: "jens.mander@microsoft.com",
|
||||
UserRole: acl.RoleUnauthorized.String(),
|
||||
UserRole: acl.RoleUnknown.String(),
|
||||
SuperAdmin: false,
|
||||
DisplayName: "Jens Mander",
|
||||
CanLogin: true,
|
||||
CanSync: true,
|
||||
WebDAV: true,
|
||||
CanInvite: false,
|
||||
UserSettings: &UserSettings{
|
||||
UITheme: "",
|
||||
|
@ -150,9 +149,9 @@ var UserFixtures = UserMap{
|
|||
UserRole: acl.RoleAdmin.String(),
|
||||
SuperAdmin: false,
|
||||
CanLogin: true,
|
||||
CanSync: true,
|
||||
WebDAV: true,
|
||||
CanInvite: true,
|
||||
InviteToken: rnd.GenerateToken(8),
|
||||
InviteToken: GenerateToken(),
|
||||
UserSettings: &UserSettings{
|
||||
UITheme: "custom",
|
||||
MapsStyle: "invalid",
|
||||
|
|
|
@ -39,7 +39,7 @@ func TestUserMap_Pointer(t *testing.T) {
|
|||
r := UserFixtures.Pointer("monstera")
|
||||
assert.Equal(t, "", r.UserName)
|
||||
assert.Equal(t, "", r.Email())
|
||||
assert.Equal(t, acl.RoleUnauthorized, r.AclRole())
|
||||
assert.Equal(t, acl.RoleUnknown, r.AclRole())
|
||||
assert.IsType(t, &User{}, r)
|
||||
})
|
||||
}
|
||||
|
|
38
internal/entity/auth_user_legacy.go
Normal file
38
internal/entity/auth_user_legacy.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package entity
|
||||
|
||||
import "github.com/photoprism/photoprism/internal/entity/legacy"
|
||||
|
||||
// FindLegacyUser returns the matching legacy user or nil if it was not found.
|
||||
func FindLegacyUser(find User) *legacy.User {
|
||||
m := &legacy.User{}
|
||||
|
||||
// Build query.
|
||||
stmt := Db()
|
||||
if find.ID != 0 {
|
||||
stmt = stmt.Where("id = ?", find.ID)
|
||||
} else if find.UserUID != "" {
|
||||
stmt = stmt.Where("user_uid = ?", find.UserUID)
|
||||
} else if find.UserName != "" {
|
||||
stmt = stmt.Where("user_name = ?", find.UserName)
|
||||
} else if find.UserEmail != "" {
|
||||
stmt = stmt.Where("primary_email = ?", find.UserEmail)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find matching record.
|
||||
if err := stmt.First(m).Error; err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// FindLegacyUsers finds registered legacy users.
|
||||
func FindLegacyUsers() legacy.Users {
|
||||
result := make(legacy.Users, 0, 1)
|
||||
|
||||
Db().Where("id > 0").Find(&result)
|
||||
|
||||
return result
|
||||
}
|
40
internal/entity/auth_user_legacy_test.go
Normal file
40
internal/entity/auth_user_legacy_test.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity/legacy"
|
||||
)
|
||||
|
||||
func TestFindLegacyUser(t *testing.T) {
|
||||
notFound := FindLegacyUser(Admin)
|
||||
assert.Nil(t, notFound)
|
||||
|
||||
t.Logf("Legacy Admin: %#v", notFound)
|
||||
|
||||
if err := Db().AutoMigrate(legacy.User{}).Error; err != nil {
|
||||
log.Debugf("TestFindLegacyUser: %s (waiting 1s)", err.Error())
|
||||
|
||||
time.Sleep(time.Second)
|
||||
|
||||
if err = Db().AutoMigrate(legacy.User{}).Error; err != nil {
|
||||
log.Errorf("TestFindLegacyUser: failed migrating legacy.User")
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
Db().Save(legacy.Admin)
|
||||
|
||||
found := FindLegacyUser(Admin)
|
||||
assert.NotNil(t, found)
|
||||
|
||||
t.Logf("Legacy Admin: %#v", found)
|
||||
|
||||
if err := Db().DropTable(legacy.User{}).Error; err != nil {
|
||||
log.Errorf("TestFindLegacyUser: failed dropping legacy.User")
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
|
@ -190,7 +190,7 @@ func TestUser_WrongPassword(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestUser_Save(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
p := User{}
|
||||
|
||||
err := p.Save()
|
||||
|
@ -236,6 +236,63 @@ func TestFirstOrCreateUser(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestFindUser(t *testing.T) {
|
||||
t.Run("ID", func(t *testing.T) {
|
||||
m := FindUser(User{ID: 1})
|
||||
|
||||
if m == nil {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
|
||||
assert.Equal(t, 1, m.ID)
|
||||
assert.NotEmpty(t, m.UserUID)
|
||||
assert.Equal(t, "admin", m.UserName)
|
||||
assert.NotEmpty(t, m.CreatedAt)
|
||||
assert.NotEmpty(t, m.UpdatedAt)
|
||||
})
|
||||
t.Run("UserUID", func(t *testing.T) {
|
||||
m := FindUser(User{UserUID: "u000000000000002"})
|
||||
|
||||
if m == nil {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
|
||||
assert.Equal(t, -2, m.ID)
|
||||
assert.NotEmpty(t, m.UserUID)
|
||||
assert.Equal(t, "", m.UserName)
|
||||
assert.Equal(t, "Visitor", m.DisplayName)
|
||||
assert.NotEmpty(t, m.CreatedAt)
|
||||
assert.NotEmpty(t, m.UpdatedAt)
|
||||
})
|
||||
t.Run("UserName", func(t *testing.T) {
|
||||
m := FindUser(User{UserName: "admin"})
|
||||
|
||||
if m == nil {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
|
||||
assert.Equal(t, 1, m.ID)
|
||||
assert.NotEmpty(t, m.UserUID)
|
||||
assert.Equal(t, "admin", m.UserName)
|
||||
assert.NotEmpty(t, m.CreatedAt)
|
||||
assert.NotEmpty(t, m.UpdatedAt)
|
||||
})
|
||||
t.Run("Unknown", func(t *testing.T) {
|
||||
m := FindUser(User{})
|
||||
|
||||
if m != nil {
|
||||
t.Fatal("result should be nil")
|
||||
}
|
||||
})
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
m := FindUser(User{UserUID: "xxx"})
|
||||
|
||||
if m != nil {
|
||||
t.Fatal("result should be nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFindUserByUID(t *testing.T) {
|
||||
t.Run("Visitor", func(t *testing.T) {
|
||||
m := FindUserByUID("u000000000000002")
|
||||
|
@ -251,7 +308,6 @@ func TestFindUserByUID(t *testing.T) {
|
|||
assert.NotEmpty(t, m.CreatedAt)
|
||||
assert.NotEmpty(t, m.UpdatedAt)
|
||||
})
|
||||
|
||||
t.Run("Unknown", func(t *testing.T) {
|
||||
m := FindUserByUID("")
|
||||
|
||||
|
@ -259,7 +315,6 @@ func TestFindUserByUID(t *testing.T) {
|
|||
t.Fatal("result should be nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
m := FindUserByUID("xxx")
|
||||
|
||||
|
@ -267,7 +322,6 @@ func TestFindUserByUID(t *testing.T) {
|
|||
t.Fatal("result should be nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Alice", func(t *testing.T) {
|
||||
m := FindUserByUID("uqxetse3cy5eo9z2")
|
||||
|
||||
|
@ -287,7 +341,6 @@ func TestFindUserByUID(t *testing.T) {
|
|||
assert.NotEmpty(t, m.CreatedAt)
|
||||
assert.NotEmpty(t, m.UpdatedAt)
|
||||
})
|
||||
|
||||
t.Run("Bob", func(t *testing.T) {
|
||||
m := FindUserByUID("uqxc08w3d0ej2283")
|
||||
|
||||
|
@ -323,7 +376,6 @@ func TestFindUserByUID(t *testing.T) {
|
|||
assert.NotEmpty(t, m.CreatedAt)
|
||||
assert.NotEmpty(t, m.UpdatedAt)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestUser_String(t *testing.T) {
|
||||
|
@ -379,7 +431,7 @@ func TestUser_Guest(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestUser_SetPassword(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
p := User{UserUID: "u000000000000008", UserName: "Hanna", DisplayName: ""}
|
||||
if err := p.SetPassword("insecure"); err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -397,7 +449,7 @@ func TestUser_SetPassword(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestUser_InitLogin(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
p := User{UserUID: "u000000000000009", UserName: "Hanna", DisplayName: ""}
|
||||
assert.Nil(t, FindPassword("u000000000000009"))
|
||||
p.InitAccount("admin", "insecure")
|
||||
|
@ -461,7 +513,7 @@ func TestUser_AclRole(t *testing.T) {
|
|||
})
|
||||
t.Run("Unauthorized", func(t *testing.T) {
|
||||
p := User{UserUID: "u000000000000008", UserName: "Hanna", DisplayName: ""}
|
||||
assert.Equal(t, acl.RoleUnauthorized, p.AclRole())
|
||||
assert.Equal(t, acl.RoleUnknown, p.AclRole())
|
||||
assert.False(t, p.IsAdmin())
|
||||
assert.False(t, p.IsVisitor())
|
||||
})
|
||||
|
@ -615,7 +667,7 @@ func TestAddUser(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDeleteUser(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
u := &User{
|
||||
UserName: "thomasdel",
|
||||
UserEmail: "thomasdel@example.com",
|
||||
|
@ -632,7 +684,7 @@ func TestDeleteUser(t *testing.T) {
|
|||
UserName: "thomasdel2",
|
||||
UserEmail: "thomasdel2@example.com",
|
||||
DisplayName: "Thomas Delete 2",
|
||||
UserRole: acl.RoleUnauthorized.String(),
|
||||
UserRole: acl.RoleUnknown.String(),
|
||||
}
|
||||
|
||||
err := u.Delete()
|
||||
|
@ -655,21 +707,21 @@ func TestUser_Disabled(t *testing.T) {
|
|||
assert.True(t, UserFixtures.Pointer("deleted").Disabled())
|
||||
}
|
||||
|
||||
func TestUser_LoginAllowed(t *testing.T) {
|
||||
assert.True(t, UserFixtures.Pointer("alice").LoginAllowed())
|
||||
assert.False(t, UserFixtures.Pointer("deleted").LoginAllowed())
|
||||
func TestUser_CanUseAPI(t *testing.T) {
|
||||
assert.True(t, UserFixtures.Pointer("alice").CanLogIn())
|
||||
assert.False(t, UserFixtures.Pointer("deleted").CanLogIn())
|
||||
}
|
||||
|
||||
func TestUser_SyncAllowed(t *testing.T) {
|
||||
assert.True(t, UserFixtures.Pointer("alice").SyncAllowed())
|
||||
assert.False(t, UserFixtures.Pointer("deleted").SyncAllowed())
|
||||
assert.False(t, UserFixtures.Pointer("friend").SyncAllowed())
|
||||
func TestUser_CanUseWebDAV(t *testing.T) {
|
||||
assert.True(t, UserFixtures.Pointer("alice").CanUseWebDAV())
|
||||
assert.False(t, UserFixtures.Pointer("deleted").CanUseWebDAV())
|
||||
assert.False(t, UserFixtures.Pointer("friend").CanUseWebDAV())
|
||||
}
|
||||
|
||||
func TestUser_UploadAllowed(t *testing.T) {
|
||||
assert.True(t, UserFixtures.Pointer("alice").UploadAllowed())
|
||||
assert.False(t, UserFixtures.Pointer("deleted").UploadAllowed())
|
||||
assert.True(t, UserFixtures.Pointer("friend").UploadAllowed())
|
||||
func TestUser_CanUpdate(t *testing.T) {
|
||||
assert.True(t, UserFixtures.Pointer("alice").CanUpload())
|
||||
assert.False(t, UserFixtures.Pointer("deleted").CanUpload())
|
||||
assert.True(t, UserFixtures.Pointer("friend").CanUpload())
|
||||
}
|
||||
|
||||
func TestUser_SharedUIDs(t *testing.T) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Package entity provides models for data storage and business logic based on the GORM library.
|
||||
Package entity provides entity models based on the GORM library.
|
||||
|
||||
Copyright (c) 2018 - 2022 PhotoPrism UG. All rights reserved.
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ func TestFaces_IDs(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDeleteOrphanFaces(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
if count, err := DeleteOrphanFaces(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
|
|
|
@ -660,7 +660,7 @@ func TestFile_ReplaceHash(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestFile_SetHDR(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
m := FileFixtures.Get("exampleFileName.jpg")
|
||||
|
||||
assert.Equal(t, false, m.IsHDR())
|
||||
|
|
25
internal/entity/legacy/legacy.go
Normal file
25
internal/entity/legacy/legacy.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
Package legacy provides legacy entity models to be used in migrations.
|
||||
|
||||
Copyright (c) 2018 - 2022 PhotoPrism UG. All rights reserved.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under Version 3 of the GNU Affero General Public License (the "AGPL"):
|
||||
<https://docs.photoprism.app/license/agpl>
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
The AGPL is supplemented by our Trademark and Brand Guidelines,
|
||||
which describe how our Brand Assets may be used:
|
||||
<https://photoprism.app/trademark>
|
||||
|
||||
Feel free to send an email to hello@photoprism.app if you have questions,
|
||||
want to support our work, or just want to say hello.
|
||||
|
||||
Additional information can be found in our Developer Guide:
|
||||
<https://docs.photoprism.app/developer-guide/>
|
||||
*/
|
||||
package legacy
|
155
internal/entity/legacy/user.go
Normal file
155
internal/entity/legacy/user.go
Normal file
|
@ -0,0 +1,155 @@
|
|||
package legacy
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
type Users []User
|
||||
|
||||
// User represents a person that may optionally log in as user.
|
||||
type User struct {
|
||||
ID int `gorm:"primary_key" json:"-" yaml:"-"`
|
||||
AddressID int `gorm:"default:1" json:"-" yaml:"-"`
|
||||
UserUID string `gorm:"type:VARBINARY(42);unique_index;" json:"UID" yaml:"UID"`
|
||||
MotherUID string `gorm:"type:VARBINARY(42);" json:"MotherUID" yaml:"MotherUID,omitempty"`
|
||||
FatherUID string `gorm:"type:VARBINARY(42);" json:"FatherUID" yaml:"FatherUID,omitempty"`
|
||||
GlobalUID string `gorm:"type:VARBINARY(42);index;" json:"GlobalUID" yaml:"GlobalUID,omitempty"`
|
||||
FullName string `gorm:"size:128;" json:"FullName" yaml:"FullName,omitempty"`
|
||||
NickName string `gorm:"size:64;" json:"NickName" yaml:"NickName,omitempty"`
|
||||
MaidenName string `gorm:"size:64;" json:"MaidenName" yaml:"MaidenName,omitempty"`
|
||||
ArtistName string `gorm:"size:64;" json:"ArtistName" yaml:"ArtistName,omitempty"`
|
||||
UserName string `gorm:"size:64;" json:"UserName" yaml:"UserName,omitempty"`
|
||||
UserStatus string `gorm:"size:32;" json:"UserStatus" yaml:"UserStatus,omitempty"`
|
||||
UserDisabled bool `json:"UserDisabled" yaml:"UserDisabled,omitempty"`
|
||||
UserSettings string `gorm:"type:LONGTEXT;" json:"-" yaml:"-"`
|
||||
PrimaryEmail string `gorm:"size:255;index;" json:"PrimaryEmail" yaml:"PrimaryEmail,omitempty"`
|
||||
EmailConfirmed bool `json:"EmailConfirmed" yaml:"EmailConfirmed,omitempty"`
|
||||
BackupEmail string `gorm:"size:255;" json:"BackupEmail" yaml:"BackupEmail,omitempty"`
|
||||
PersonURL string `gorm:"type:VARBINARY(255);" json:"PersonURL" yaml:"PersonURL,omitempty"`
|
||||
PersonPhone string `gorm:"size:32;" json:"PersonPhone" yaml:"PersonPhone,omitempty"`
|
||||
PersonStatus string `gorm:"size:32;" json:"PersonStatus" yaml:"PersonStatus,omitempty"`
|
||||
PersonAvatar string `gorm:"type:VARBINARY(255);" json:"PersonAvatar" yaml:"PersonAvatar,omitempty"`
|
||||
PersonLocation string `gorm:"size:128;" json:"PersonLocation" yaml:"PersonLocation,omitempty"`
|
||||
PersonBio string `gorm:"type:TEXT;" json:"PersonBio" yaml:"PersonBio,omitempty"`
|
||||
PersonAccounts string `gorm:"type:LONGTEXT;" json:"-" yaml:"-"`
|
||||
BusinessURL string `gorm:"type:VARBINARY(255);" json:"BusinessURL" yaml:"BusinessURL,omitempty"`
|
||||
BusinessPhone string `gorm:"size:32;" json:"BusinessPhone" yaml:"BusinessPhone,omitempty"`
|
||||
BusinessEmail string `gorm:"size:255;" json:"BusinessEmail" yaml:"BusinessEmail,omitempty"`
|
||||
CompanyName string `gorm:"size:128;" json:"CompanyName" yaml:"CompanyName,omitempty"`
|
||||
DepartmentName string `gorm:"size:128;" json:"DepartmentName" yaml:"DepartmentName,omitempty"`
|
||||
JobTitle string `gorm:"size:64;" json:"JobTitle" yaml:"JobTitle,omitempty"`
|
||||
BirthYear int `json:"BirthYear" yaml:"BirthYear,omitempty"`
|
||||
BirthMonth int `json:"BirthMonth" yaml:"BirthMonth,omitempty"`
|
||||
BirthDay int `json:"BirthDay" yaml:"BirthDay,omitempty"`
|
||||
TermsAccepted bool `json:"TermsAccepted" yaml:"TermsAccepted,omitempty"`
|
||||
IsArtist bool `json:"IsArtist" yaml:"IsArtist,omitempty"`
|
||||
IsSubject bool `json:"IsSubject" yaml:"IsSubject,omitempty"`
|
||||
RoleAdmin bool `json:"RoleAdmin" yaml:"RoleAdmin,omitempty"`
|
||||
RoleGuest bool `json:"RoleGuest" yaml:"RoleGuest,omitempty"`
|
||||
RoleChild bool `json:"RoleChild" yaml:"RoleChild,omitempty"`
|
||||
RoleFamily bool `json:"RoleFamily" yaml:"RoleFamily,omitempty"`
|
||||
RoleFriend bool `json:"RoleFriend" yaml:"RoleFriend,omitempty"`
|
||||
WebDAV bool `gorm:"column:webdav" json:"WebDAV" yaml:"WebDAV,omitempty"`
|
||||
StoragePath string `gorm:"column:storage_path;type:VARBINARY(500);" json:"StoragePath" yaml:"StoragePath,omitempty"`
|
||||
CanInvite bool `json:"CanInvite" yaml:"CanInvite,omitempty"`
|
||||
InviteToken string `gorm:"type:VARBINARY(32);" json:"-" yaml:"-"`
|
||||
InvitedBy string `gorm:"type:VARBINARY(32);" json:"-" yaml:"-"`
|
||||
ConfirmToken string `gorm:"type:VARBINARY(64);" json:"-" yaml:"-"`
|
||||
ResetToken string `gorm:"type:VARBINARY(64);" json:"-" yaml:"-"`
|
||||
ApiToken string `gorm:"column:api_token;type:VARBINARY(128);" json:"-" yaml:"-"`
|
||||
ApiSecret string `gorm:"column:api_secret;type:VARBINARY(128);" json:"-" yaml:"-"`
|
||||
LoginAttempts int `json:"-" yaml:"-"`
|
||||
LoginAt *time.Time `json:"-" yaml:"-"`
|
||||
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
|
||||
DeletedAt *time.Time `sql:"index" json:"DeletedAt,omitempty" yaml:"-"`
|
||||
}
|
||||
|
||||
// TableName returns the entity database table name.
|
||||
func (User) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
// Admin is the default admin user.
|
||||
var Admin = User{
|
||||
ID: 1,
|
||||
AddressID: 1,
|
||||
UserName: "admin",
|
||||
FullName: "Admin",
|
||||
RoleAdmin: true,
|
||||
UserDisabled: false,
|
||||
}
|
||||
|
||||
// UnknownUser is an anonymous, public user without own account.
|
||||
var UnknownUser = User{
|
||||
ID: -1,
|
||||
AddressID: 1,
|
||||
UserUID: "u000000000000001",
|
||||
UserName: "",
|
||||
FullName: "Anonymous",
|
||||
RoleAdmin: false,
|
||||
RoleGuest: false,
|
||||
UserDisabled: true,
|
||||
}
|
||||
|
||||
// Guest is a user without own account e.g. for link sharing.
|
||||
var Guest = User{
|
||||
ID: -2,
|
||||
AddressID: 1,
|
||||
UserUID: "u000000000000002",
|
||||
UserName: "",
|
||||
FullName: "Guest",
|
||||
RoleAdmin: false,
|
||||
RoleGuest: true,
|
||||
UserDisabled: true,
|
||||
}
|
||||
|
||||
// Deleted tests if the entity is marked as deleted.
|
||||
func (m *User) Deleted() bool {
|
||||
if m.DeletedAt == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return !m.DeletedAt.IsZero()
|
||||
}
|
||||
|
||||
// String returns an identifier that can be used in logs.
|
||||
func (m *User) String() string {
|
||||
if n := m.Username(); n != "" {
|
||||
return clean.Log(n)
|
||||
}
|
||||
|
||||
if m.FullName != "" {
|
||||
return clean.Log(m.FullName)
|
||||
}
|
||||
|
||||
return clean.Log(m.UserUID)
|
||||
}
|
||||
|
||||
// Username returns the normalized username.
|
||||
func (m *User) Username() string {
|
||||
return clean.Username(m.UserName)
|
||||
}
|
||||
|
||||
// Registered tests if the user is registered e.g. has a username.
|
||||
func (m *User) Registered() bool {
|
||||
return m.Username() != "" && rnd.IsUID(m.UserUID, 'u')
|
||||
}
|
||||
|
||||
// Admin returns true if the user is an admin with user name.
|
||||
func (m *User) Admin() bool {
|
||||
return m.Registered() && m.RoleAdmin
|
||||
}
|
||||
|
||||
// Anonymous returns true if the user is unknown.
|
||||
func (m *User) Anonymous() bool {
|
||||
return !rnd.IsUID(m.UserUID, 'u') || m.ID == UnknownUser.ID || m.UserUID == UnknownUser.UserUID
|
||||
}
|
||||
|
||||
// Guest returns true if the user is a guest.
|
||||
func (m *User) Guest() bool {
|
||||
return m.RoleGuest
|
||||
}
|
|
@ -165,7 +165,7 @@ func TestMarkers_Labels(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMarkers_AppendWithEmbedding(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
m1 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea1, "lt9k3pw1wowuy1c1", SrcImage, MarkerFace, 100, 65)
|
||||
m2 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea3, "lt9k3pw1wowuy1c3", SrcImage, MarkerFace, 100, 65)
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import (
|
|||
)
|
||||
|
||||
func TestSavePhotoForm(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
f := form.Photo{
|
||||
TakenAt: time.Date(2008, 1, 1, 2, 0, 0, 0, time.UTC),
|
||||
TakenAtLocal: time.Date(2008, 1, 1, 2, 0, 0, 0, time.UTC),
|
||||
|
@ -143,7 +143,7 @@ func TestPhoto_ClassifyLabels(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPhoto_PreloadFiles(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo01")
|
||||
assert.Empty(t, m.Files)
|
||||
m.PreloadFiles()
|
||||
|
@ -152,7 +152,7 @@ func TestPhoto_PreloadFiles(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPhoto_PreloadKeywords(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo01")
|
||||
assert.Empty(t, m.Keywords)
|
||||
m.PreloadKeywords()
|
||||
|
@ -161,7 +161,7 @@ func TestPhoto_PreloadKeywords(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPhoto_PreloadAlbums(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo01")
|
||||
assert.Empty(t, m.Albums)
|
||||
m.PreloadAlbums()
|
||||
|
@ -170,7 +170,7 @@ func TestPhoto_PreloadAlbums(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPhoto_PreloadMany(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo01")
|
||||
assert.Empty(t, m.Albums)
|
||||
assert.Empty(t, m.Files)
|
||||
|
@ -266,7 +266,7 @@ func TestPhoto_SetDescription(t *testing.T) {
|
|||
m.SetDescription("new photo description", SrcName)
|
||||
assert.Equal(t, "photo description blacklist", m.PhotoDescription)
|
||||
})
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo15")
|
||||
assert.Equal(t, "photo description blacklist", m.PhotoDescription)
|
||||
m.SetDescription("new photo description", SrcMeta)
|
||||
|
@ -294,7 +294,7 @@ func TestPhoto_Delete(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPhotos_UIDs(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
photo1 := Photo{PhotoUID: "abc123"}
|
||||
photo2 := Photo{PhotoUID: "abc456"}
|
||||
photos := Photos{photo1, photo2}
|
||||
|
@ -314,7 +314,7 @@ func TestPhoto_String(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPhoto_Create(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
photo := Photo{PhotoUID: "567", PhotoName: "Holiday", OriginalName: "holidayOriginal2"}
|
||||
err := photo.Create()
|
||||
if err != nil {
|
||||
|
@ -324,7 +324,7 @@ func TestPhoto_Create(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPhoto_Save(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
photo := Photo{PhotoUID: "567", PhotoName: "Holiday", OriginalName: "holidayOriginal2"}
|
||||
err := photo.Save()
|
||||
if err != nil {
|
||||
|
@ -370,7 +370,7 @@ func TestFindPhoto(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPhoto_RemoveKeyword(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
keyword := Keyword{Keyword: "snake"}
|
||||
keyword2 := Keyword{Keyword: "otter"}
|
||||
keywords := []Keyword{keyword, keyword2}
|
||||
|
@ -389,7 +389,7 @@ func TestPhoto_RemoveKeyword(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPhoto_SyncKeywordLabels(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
keyword := Keyword{Keyword: "snake"}
|
||||
keyword2 := Keyword{Keyword: "otter"}
|
||||
keywords := []Keyword{keyword, keyword2}
|
||||
|
@ -430,7 +430,7 @@ func TestPhoto_LocationLoaded(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPhoto_LoadLocation(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
photo := PhotoFixtures.Get("Photo03")
|
||||
if err := photo.LoadLocation(); err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -456,7 +456,7 @@ func TestPhoto_PlaceLoaded(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPhoto_LoadPlace(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
photo := PhotoFixtures.Get("Photo03")
|
||||
err := photo.LoadPlace()
|
||||
if err != nil {
|
||||
|
@ -500,7 +500,7 @@ func TestPhoto_AllFilesMissing(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPhoto_Updates(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
photo := Photo{PhotoDescription: "bcss", PhotoName: "InitialName"}
|
||||
|
||||
if err := photo.Save(); err != nil {
|
||||
|
@ -604,7 +604,7 @@ func TestPhoto_Links(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPhoto_SetPrimary(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("19800101_000002_D640C559")
|
||||
|
||||
if err := m.SetPrimary(""); err != nil {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue