Sharing: ACL authorization for REST API #18

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer
2020-06-25 14:54:04 +02:00
parent 1f1f92408a
commit 5d59b50912
88 changed files with 1378 additions and 957 deletions

View File

@@ -1,4 +1,4 @@
FROM photoprism/development:20200530
FROM photoprism/development:20200625
# Set up project directory
WORKDIR "/go/src/github.com/photoprism/photoprism"

View File

@@ -13,7 +13,7 @@ services:
environment:
PHOTOPRISM_SITE_URL: "http://localhost:2342/"
PHOTOPRISM_SITE_TITLE: "PhotoPrism"
PHOTOPRISM_SITE_CAPTION: "Browse your life"
PHOTOPRISM_SITE_CAPTION: "Browse Your Life in Pictures"
PHOTOPRISM_SITE_DESCRIPTION: "Open-Source Personal Photo Management."
PHOTOPRISM_SITE_AUTHOR: "Anonymous"
PHOTOPRISM_DEBUG: "false"

View File

@@ -19,7 +19,7 @@ services:
TF_CPP_MIN_LOG_LEVEL: 0 # Show TensorFlow log messages for development
PHOTOPRISM_SITE_URL: "http://localhost:2342/"
PHOTOPRISM_SITE_TITLE: "PhotoPrism"
PHOTOPRISM_SITE_CAPTION: "Browse your life"
PHOTOPRISM_SITE_CAPTION: "Browse Your Life in Pictures"
PHOTOPRISM_SITE_DESCRIPTION: "Open-Source Personal Photo Management."
PHOTOPRISM_SITE_AUTHOR: "Anonymous"
PHOTOPRISM_DEBUG: "true"

View File

@@ -86,12 +86,12 @@ RUN npm install --unsafe-perm=true --allow-root -g npm testcafe chromedriver
RUN npm config set cache ~/.cache/npm
# Install Go
ENV GOLANG_VERSION 1.14.3
ENV GOLANG_VERSION 1.14.4
RUN set -eux; \
\
url="https://golang.org/dl/go${GOLANG_VERSION}.linux-amd64.tar.gz"; \
wget -O go.tgz "$url"; \
echo "1c39eac4ae95781b066c144c58e45d6859652247f7515f0d2cba7be7d57d2226 *go.tgz" | sha256sum -c -; \
echo "aed845e4185a0b2a3c3d5e1d0a35491702c55889192bb9c30e67a3de6849c067 *go.tgz" | sha256sum -c -; \
tar -C /usr/local -xzf go.tgz; \
rm go.tgz; \
export PATH="/usr/local/go/bin:$PATH"; \

View File

@@ -1,4 +1,4 @@
FROM photoprism/development:20200530 as build
FROM photoprism/development:20200625 as build
# Set up project directory
WORKDIR "/go/src/github.com/photoprism/photoprism"

View File

@@ -69,12 +69,12 @@ RUN npm install --unsafe-perm=true --allow-root -g npm
RUN npm config set cache ~/.cache/npm
# Install Go
ENV GOLANG_VERSION 1.14.3
ENV GOLANG_VERSION 1.14.4
RUN set -eux; \
\
url="https://golang.org/dl/go${GOLANG_VERSION}.linux-arm64.tar.gz"; \
wget -O go.tgz "$url"; \
echo "a7a593e2ee079d83a1943edcd1c9ed2dae7529666fce04de8c142fb61c7cdd3e *go.tgz" | sha256sum -c -; \
echo "05dc46ada4e23a1f58e72349f7c366aae2e9c7a7f1e7653095538bc5bba5e077 *go.tgz" | sha256sum -c -; \
tar -C /usr/local -xzf go.tgz; \
rm go.tgz; \
export PATH="/usr/local/go/bin:$PATH"; \

View File

@@ -27,7 +27,7 @@ services:
PHOTOPRISM_EXPERIMENTAL: "false" # Enable experimental features
PHOTOPRISM_SITE_URL: "http://localhost:2342/" # Canonical / public site URL
PHOTOPRISM_SITE_TITLE: "PhotoPrism"
PHOTOPRISM_SITE_CAPTION: "Browse your life"
PHOTOPRISM_SITE_CAPTION: "Browse Your Life in Pictures"
PHOTOPRISM_SITE_DESCRIPTION: "Open-Source Personal Photo Management."
PHOTOPRISM_SITE_AUTHOR: "Anonymous"
PHOTOPRISM_HTTP_HOST: "0.0.0.0"

View File

@@ -26,7 +26,7 @@ services:
PHOTOPRISM_EXPERIMENTAL: "false" # Enable experimental features
PHOTOPRISM_SITE_URL: "http://localhost:2342/" # Canonical / public site URL
PHOTOPRISM_SITE_TITLE: "PhotoPrism"
PHOTOPRISM_SITE_CAPTION: "Browse your life"
PHOTOPRISM_SITE_CAPTION: "Browse Your Life in Pictures"
PHOTOPRISM_SITE_DESCRIPTION: "Open-Source Personal Photo Management."
PHOTOPRISM_SITE_AUTHOR: "Anonymous"
PHOTOPRISM_HTTP_HOST: "0.0.0.0"

View File

@@ -533,79 +533,23 @@
}
},
"@babel/helper-function-name": {
"version": "7.10.1",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.1.tgz",
"integrity": "sha512-fcpumwhs3YyZ/ttd5Rz0xn0TpIwVkN7X0V38B9TWNfVF42KEkhkAAuPCQ3oXmtTRtiPJrmZ0TrfS0GKF0eMaRQ==",
"version": "7.10.3",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.3.tgz",
"integrity": "sha512-FvSj2aiOd8zbeqijjgqdMDSyxsGHaMt5Tr0XjQsGKHD3/1FP3wksjnLAWzxw7lvXiej8W1Jt47SKTZ6upQNiRw==",
"requires": {
"@babel/helper-get-function-arity": "^7.10.1",
"@babel/template": "^7.10.1",
"@babel/types": "^7.10.1"
"@babel/helper-get-function-arity": "^7.10.3",
"@babel/template": "^7.10.3",
"@babel/types": "^7.10.3"
},
"dependencies": {
"@babel/code-frame": {
"version": "7.10.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz",
"integrity": "sha512-IGhtTmpjGbYzcEDOw7DcQtbQSXcG9ftmAXtWTu9V936vDye4xjjekktFAtgZsWpzTj/X01jocB46mTywm/4SZw==",
"requires": {
"@babel/highlight": "^7.10.1"
}
},
"@babel/helper-get-function-arity": {
"version": "7.10.1",
"resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.1.tgz",
"integrity": "sha512-F5qdXkYGOQUb0hpRaPoetF9AnsXknKjWMZ+wmsIRsp5ge5sFh4c3h1eH2pRTTuy9KKAA2+TTYomGXAtEL2fQEw==",
"requires": {
"@babel/types": "^7.10.1"
}
},
"@babel/helper-validator-identifier": {
"version": "7.10.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.1.tgz",
"integrity": "sha512-5vW/JXLALhczRCWP0PnFDMCJAchlBvM7f4uk/jXritBnIa6E1KmqmtrS3yn1LAnxFBypQ3eneLuXjsnfQsgILw=="
},
"@babel/highlight": {
"version": "7.10.1",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.1.tgz",
"integrity": "sha512-8rMof+gVP8mxYZApLF/JgNDAkdKa+aJt3ZYxF8z6+j/hpeXL7iMsKCPHa2jNMHu/qqBwzQF4OHNoYi8dMA/rYg==",
"requires": {
"@babel/helper-validator-identifier": "^7.10.1",
"chalk": "^2.0.0",
"js-tokens": "^4.0.0"
}
},
"@babel/parser": {
"version": "7.10.2",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.2.tgz",
"integrity": "sha512-PApSXlNMJyB4JiGVhCOlzKIif+TKFTvu0aQAhnTvfP/z3vVSN6ZypH5bfUNwFXXjRQtUEBNFd2PtmCmG2Py3qQ=="
},
"@babel/template": {
"version": "7.10.1",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.1.tgz",
"integrity": "sha512-OQDg6SqvFSsc9A0ej6SKINWrpJiNonRIniYondK2ViKhB06i3c0s+76XUft71iqBEe9S1OKsHwPAjfHnuvnCig==",
"version": "7.10.3",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz",
"integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==",
"requires": {
"@babel/code-frame": "^7.10.1",
"@babel/parser": "^7.10.1",
"@babel/types": "^7.10.1"
}
},
"@babel/types": {
"version": "7.10.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.2.tgz",
"integrity": "sha512-AD3AwWBSz0AWF0AkCN9VPiWrvldXq+/e3cHa4J89vo4ymjz1XwrBFFVZmkJTsQIPNk+ZVomPSXUJqq8yyjZsng==",
"requires": {
"@babel/helper-validator-identifier": "^7.10.1",
"lodash": "^4.17.13",
"to-fast-properties": "^2.0.0"
}
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
"@babel/code-frame": "^7.10.3",
"@babel/parser": "^7.10.3",
"@babel/types": "^7.10.3"
}
}
}
@@ -2273,9 +2217,9 @@
"integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA=="
},
"@types/node": {
"version": "14.0.13",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.13.tgz",
"integrity": "sha512-rouEWBImiRaSJsVA+ITTFM6ZxibuAlTuNOCyxVbwreu6k6+ujs7DfnU9o+PShFhET78pMBl3eH+AGSI5eOTkPA=="
"version": "14.0.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.14.tgz",
"integrity": "sha512-syUgf67ZQpaJj01/tRTknkMNoBBLWJOBODF0Zm4NrXmiSuxjymFrxnTu1QVYRubhVkRcZLYZG8STTwJRdVm/WQ=="
},
"@types/q": {
"version": "1.5.4",
@@ -2721,8 +2665,7 @@
},
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
"resolved": ""
}
}
},
@@ -2995,24 +2938,19 @@
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
},
"autoprefixer": {
"version": "9.8.2",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.2.tgz",
"integrity": "sha512-9UwMMU8Rg7Fj0c55mbOpXrr/2WrRqoOwOlLNTyyYt+nhiyQdIBWipp5XWzt+Lge8r3DK5y+EHMc1OBf8VpZA6Q==",
"version": "9.8.4",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.4.tgz",
"integrity": "sha512-84aYfXlpUe45lvmS+HoAWKCkirI/sw4JK0/bTeeqgHYco3dcsOn0NqdejISjptsYwNji/21dnkDri9PsYKk89A==",
"requires": {
"browserslist": "^4.12.0",
"caniuse-lite": "^1.0.30001084",
"kleur": "^4.0.1",
"caniuse-lite": "^1.0.30001087",
"colorette": "^1.2.0",
"normalize-range": "^0.1.2",
"num2fraction": "^1.2.2",
"postcss": "^7.0.32",
"postcss-value-parser": "^4.1.0"
},
"dependencies": {
"caniuse-lite": {
"version": "1.0.30001085",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001085.tgz",
"integrity": "sha512-x0YRFRE0pmOD90z+9Xk7jwO58p4feVNXP+U8kWV+Uo/HADyrgESlepzIkUqPgaXkpyceZU6siM1gsK7sHgplqA=="
},
"postcss-value-parser": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz",
@@ -3689,14 +3627,14 @@
}
},
"browserslist": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.12.0.tgz",
"integrity": "sha512-UH2GkcEDSI0k/lRkuDSzFl9ZZ87skSy9w2XAn1MsZnL+4c4rqbBd3e82UWHbYDpztABrPBhZsTEeuxVfHppqDg==",
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.12.1.tgz",
"integrity": "sha512-WMjXwFtPskSW1pQUDJRxvRKRkeCr7usN0O/Za76N+F4oadaTdQHotSGcX9jT/Hs7mSKPkyMFNvqawB/1HzYDKQ==",
"requires": {
"caniuse-lite": "^1.0.30001043",
"electron-to-chromium": "^1.3.413",
"node-releases": "^1.1.53",
"pkg-up": "^2.0.0"
"caniuse-lite": "^1.0.30001088",
"electron-to-chromium": "^1.3.481",
"escalade": "^3.0.1",
"node-releases": "^1.1.58"
}
},
"buffer": {
@@ -3844,9 +3782,9 @@
}
},
"caniuse-lite": {
"version": "1.0.30001085",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001085.tgz",
"integrity": "sha512-x0YRFRE0pmOD90z+9Xk7jwO58p4feVNXP+U8kWV+Uo/HADyrgESlepzIkUqPgaXkpyceZU6siM1gsK7sHgplqA=="
"version": "1.0.30001088",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001088.tgz",
"integrity": "sha512-6eYUrlShRYveyqKG58HcyOfPgh3zb2xqs7NvT2VVtP3hEUeeWvc3lqhpeMTxYWBBeeaT9A4bKsrtjATm66BTHg=="
},
"center-align": {
"version": "0.1.3",
@@ -4239,6 +4177,11 @@
"simple-swizzle": "^0.2.2"
}
},
"colorette": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.0.tgz",
"integrity": "sha512-soRSroY+OF/8OdA3PTQXwaDJeMc7TfknKKrxeSCencL2a4+Tx5zhxmmv7hdpCjhKBjehzp8+bwe/T68K0hpIjw=="
},
"colors": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
@@ -5225,9 +5168,9 @@
"integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA=="
},
"electron-to-chromium": {
"version": "1.3.480",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.480.tgz",
"integrity": "sha512-wnuUfQCBMAdzu5Xe+F4FjaRK+6ToG6WvwG72s8k/3E6b+hoGVYGiQE7JD1NhiCMcqF3+wV+c2vAnaLGRSSWVqA=="
"version": "1.3.483",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.483.tgz",
"integrity": "sha512-+05RF8S9rk8S0G8eBCqBRBaRq7+UN3lDs2DAvnG8SBSgQO3hjy0+qt4CmRk5eiuGbTcaicgXfPmBi31a+BD3lg=="
},
"elliptic": {
"version": "6.5.3",
@@ -5476,6 +5419,11 @@
"ext": "^1.1.2"
}
},
"escalade": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.0.1.tgz",
"integrity": "sha512-DR6NO3h9niOT+MZs7bjxlj2a1k+POu5RN8CLTPX2+i78bRi9eLe7+0zXgUHMnGXWybYcL61E9hGhPKqedy8tQA=="
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -8751,11 +8699,6 @@
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="
},
"kleur": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.0.1.tgz",
"integrity": "sha512-Qs6SqCLm63rd0kNVh+wO4XsWLU6kgfwwaPYsLiClWf0Tewkzsa6MvB21bespb8cz+ANS+2t3So1ge3gintzhlw=="
},
"last-call-webpack-plugin": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz",
@@ -10178,54 +10121,6 @@
"find-up": "^3.0.0"
}
},
"pkg-up": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz",
"integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=",
"requires": {
"find-up": "^2.1.0"
},
"dependencies": {
"find-up": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
"integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
"requires": {
"locate-path": "^2.0.0"
}
},
"locate-path": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
"integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
"requires": {
"p-locate": "^2.0.0",
"path-exists": "^3.0.0"
}
},
"p-limit": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
"integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
"requires": {
"p-try": "^1.0.0"
}
},
"p-locate": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
"integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
"requires": {
"p-limit": "^1.1.0"
}
},
"p-try": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
"integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M="
}
}
},
"plur": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/plur/-/plur-3.1.1.tgz",
@@ -12038,8 +11933,7 @@
},
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
"resolved": ""
},
"postcss": {
"version": "7.0.21",
@@ -13837,8 +13731,7 @@
},
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
"resolved": ""
},
"parse-json": {
"version": "2.2.0",

View File

@@ -28,13 +28,13 @@
"@fortawesome/fontawesome-free": "^5.13.1",
"acorn": "6.4.1",
"ajv": "^6.12.2",
"autoprefixer": "^9.8.2",
"autoprefixer": "^9.8.4",
"axios": "^0.19.2",
"axios-mock-adapter": "^1.18.1",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0",
"babel-plugin-istanbul": "^6.0.0",
"browserslist": "^4.12.0",
"browserslist": "^4.12.1",
"chai": "^4.2.0",
"chalk": "^4.1.0",
"chart.js": "^2.9.3",

View File

@@ -37,7 +37,7 @@ const config = window.__CONFIG__ ? window.__CONFIG__ : testConfig;
const Api = Axios.create({
baseURL: "/api/v1",
headers: {common: {
"X-Session-Token": window.localStorage.getItem("session_token"),
"X-Session-ID": window.localStorage.getItem("session_id"),
"X-Client-Hash": config.jsHash,
"X-Client-Version": config.version,
}},

View File

@@ -48,7 +48,7 @@ export default class Session {
this.storage = storage;
}
if (this.applyToken(this.storage.getItem("session_token"))) {
if (this.applyId(this.storage.getItem("session_id"))) {
const dataJson = this.storage.getItem("data");
this.data = dataJson !== "undefined" ? JSON.parse(dataJson) : null;
}
@@ -73,7 +73,7 @@ export default class Session {
}
useSessionStorage() {
this.deleteToken();
this.deleteId();
this.storage.setItem("session_storage", "true");
this.storage = window.sessionStorage;
}
@@ -83,35 +83,35 @@ export default class Session {
this.storage = window.localStorage;
}
applyToken(token) {
if (!token) {
this.deleteToken();
applyId(id) {
if (!id) {
this.deleteId();
return false;
}
this.session_token = token;
Api.defaults.headers.common["X-Session-Token"] = token;
this.session_id = id;
Api.defaults.headers.common["X-Session-ID"] = id;
return true;
}
setToken(token) {
this.storage.setItem("session_token", token);
return this.applyToken(token);
setId(id) {
this.storage.setItem("session_id", id);
return this.applyId(id);
}
setConfig(values) {
this.config.setValues(values);
}
getToken() {
return this.session_token;
getId() {
return this.session_id;
}
deleteToken() {
this.session_token = null;
this.storage.removeItem("session_token");
delete Api.defaults.headers.common["X-Session-Token"];
deleteId() {
this.session_id = null;
this.storage.removeItem("session_id");
delete Api.defaults.headers.common["X-Session-ID"];
this.deleteData();
}
@@ -166,7 +166,7 @@ export default class Session {
return !this.user || !this.user.hasId();
}
shareToken(token) {
hasToken(token) {
if(!this.data || !this.data.tokens) {
return false;
}
@@ -183,7 +183,7 @@ export default class Session {
sendClientInfo() {
const clientInfo = {
"session": this.getToken(),
"session": this.getId(),
"js": window.__CONFIG__.jsHash,
"css": window.__CONFIG__.cssHash,
"version": window.__CONFIG__.version,
@@ -197,12 +197,23 @@ export default class Session {
}
login(username, password, token) {
this.deleteToken();
this.deleteId();
return Api.post("session", {username, password, token}).then(
(resp) => {
this.setConfig(resp.data.config);
this.setToken(resp.data.token);
this.setId(resp.data.id);
this.setData(resp.data.data);
this.sendClientInfo();
}
);
}
redeemToken(token) {
return Api.post("session", {token}).then(
(resp) => {
this.setConfig(resp.data.config);
this.setId(resp.data.id);
this.setData(resp.data.data);
this.sendClientInfo();
}
@@ -210,16 +221,16 @@ export default class Session {
}
onLogout() {
this.deleteToken();
this.deleteId();
window.location = "/";
}
logout() {
const token = this.getToken();
const id = this.getId();
this.deleteToken();
this.deleteId();
Api.delete("session/" + token).then(
Api.delete("session/" + id).then(
() => {
window.location = "/";
}

View File

@@ -48,7 +48,9 @@ export class User extends RestModel {
Confirmed: false,
Admin: false,
Guest: false,
Child: false,
Family: false,
Friend: false,
Artist: false,
Subject: false,
CanEdit: false,

View File

@@ -177,7 +177,6 @@
v-else-if="album.Type === 'album'">
<div v-if="album.PhotoCount === 1" class="caption">
<translate>Contains one photo.</translate>
<translate>Add more by selecting them from search results.</translate>
</div>
<div v-else-if="album.PhotoCount > 0" class="caption">
<translate>Contains</translate>

View File

@@ -2,15 +2,15 @@
<div class="p-page p-page-login">
<v-toolbar flat color="secondary">
<v-toolbar-title>
<translate>Authentication required</translate>
{{ $config.get("siteTitle") }}: {{ $config.get("siteCaption") }}
</v-toolbar-title>
<v-spacer></v-spacer>
</v-toolbar>
<v-container class="pt-5">
<v-container class="pt-4">
<p class="subheading">
<span><translate>Please enter your user name and password to proceed:</translate></span>
<span><translate>Please enter your name and password to proceed:</translate></span>
</p>
<v-form ref="form" autocomplete="off" class="p-form-login" @submit.prevent="login" dense>
<v-text-field
@@ -56,7 +56,7 @@
password: "",
nextUrl: this.$route.params.nextUrl ? this.$route.params.nextUrl : "/",
labels: {
username: this.$gettext("User Name"),
username: this.$gettext("Name"),
password: this.$gettext("Password"),
}
};

View File

@@ -5,7 +5,9 @@
@submit.prevent="onChange">
<v-card flat tile class="mt-0 px-1 application">
<v-card-title primary-title class="pb-0">
<h3 class="body-2 mb-0"><translate key="Library">Library</translate></h3>
<h3 class="body-2 mb-0">
<translate key="Library">Library</translate>
</h3>
</v-card-title>
<v-card-actions>
@@ -75,7 +77,9 @@
<v-card flat tile class="mt-0 px-1 application">
<v-card-title primary-title class="pb-2">
<h3 class="body-2 mb-0"><translate key="User Interface">User Interface</translate></h3>
<h3 class="body-2 mb-0">
<translate key="User Interface">User Interface</translate>
</h3>
</v-card-title>
<v-card-actions>
@@ -299,7 +303,9 @@
<v-card flat tile class="mt-0 px-1 application" v-if="settings.features.places">
<v-card-title primary-title class="pb-2">
<h3 class="body-2 mb-0"><translate key="Places">Places</translate></h3>
<h3 class="body-2 mb-0">
<translate key="Places">Places</translate>
</h3>
</v-card-title>
<v-card-actions>
@@ -339,13 +345,14 @@
<v-card-actions>
<v-layout wrap align-top>
<v-flex xs12 sm6 class="px-2 pb-2 body-1">
<a href="https://docs.photoprism.org/contact/" class="text-link" target="_blank">PhotoPrism {{$config.get("version")}}
<a href="https://docs.photoprism.org/contact/" class="text-link" target="_blank">PhotoPrism
{{$config.get("version")}}
<br>© 2018-2020 Michael Mayer</a>
</v-flex>
<v-flex xs12 sm6 class="px-2 pb-2 body-1 text-xs-left text-sm-right">
A big <a href="https://docs.photoprism.org/credits/" class="secondary-dark--text"
target="_blank">thank you</a> to everyone who made this possible!
<a href="https://docs.photoprism.org/credits/" class="secondary-dark--text"
target="_blank">Thank you</a> to everyone who made this possible!
<br>
<a href="https://raw.githubusercontent.com/photoprism/photoprism/develop/NOTICE"
class="secondary-dark--text" target="_blank">

View File

@@ -487,10 +487,10 @@
created() {
const token = this.$route.params.token;
if (this.$session.shareToken(token)) {
if (this.$session.hasToken(token)) {
this.search();
} else {
this.$session.login("", "", token).then(() => {
this.$session.redeemToken(token).then(() => {
this.search();
});
}

View File

@@ -424,10 +424,10 @@
created() {
const token = this.$route.params.token;
if (this.$session.shareToken(token)) {
if (this.$session.hasToken(token)) {
this.findAlbum().then(() => this.search());
} else {
this.$session.login("", "", token).then(() => {
this.$session.redeemToken(token).then(() => {
this.findAlbum().then(() => this.search());
});
}

View File

@@ -165,19 +165,19 @@ describe('common/session', () => {
it('should construct session', () => {
const storage = window.localStorage;
const session = new Session(storage, config);
assert.equal(session.session_token, null);
assert.equal(session.session_id, null);
});
it('should set, get and delete token', () => {
const storage = window.localStorage;
const session = new Session(storage, config);
assert.equal(session.session_token, null);
session.setToken(123421);
assert.equal(session.session_token, 123421);
const result = session.getToken();
assert.equal(session.session_id, null);
session.setId(123421);
assert.equal(session.session_id, 123421);
const result = session.getId();
assert.equal(result, 123421);
session.deleteToken();
assert.equal(session.session_token, null);
session.deleteId();
assert.equal(session.session_id, null);
});
it('should set, get and delete user', () => {
@@ -269,17 +269,17 @@ describe('common/session', () => {
it('should test login and logout', async () => {
mock
.onPost("session").reply(200, {token: "8877", data: {user: {ID: 1, Email: "test@test.com"}}})
.onPost("session").reply(200, {id: "8877", data: {user: {ID: 1, Email: "test@test.com"}}})
.onDelete("session/8877").reply(200);
const storage = window.localStorage;
const session = new Session(storage, config);
assert.equal(session.session_token, null);
assert.equal(session.session_id, null);
assert.equal(session.storage.data, undefined);
await session.login("test@test.com", "passwd");
assert.equal(session.session_token, 8877);
assert.equal(session.session_id, 8877);
assert.equal(session.storage.data, '{"user":{"ID":1,"Email":"test@test.com"}}');
await session.logout();
assert.equal(session.session_token, null);
assert.equal(session.session_id, null);
mock.reset();
});

16
go.mod
View File

@@ -11,13 +11,15 @@ require (
github.com/dsoprea/go-png-image-structure v0.0.0-20200518003737-91ceb687d379
github.com/dustin/go-humanize v1.0.0
github.com/gin-gonic/gin v1.6.3
github.com/go-playground/validator/v10 v10.3.0 // indirect
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d
github.com/golang/protobuf v1.3.5 // indirect
github.com/golang/protobuf v1.4.2 // indirect
github.com/google/open-location-code/go v0.0.0-20191230190541-a6eb95b4d2f9
github.com/gorilla/websocket v1.4.2
github.com/gosimple/slug v1.9.0
github.com/jinzhu/gorm v1.9.12
github.com/jinzhu/inflection v1.0.0
github.com/json-iterator/go v1.1.10 // indirect
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
github.com/karrick/godirwalk v1.15.6
github.com/kr/pretty v0.1.0 // indirect
@@ -33,20 +35,22 @@ require (
github.com/sevlyar/go-daemon v0.1.5
github.com/shopspring/decimal v1.2.0 // indirect
github.com/sirupsen/logrus v1.6.0
github.com/stretchr/testify v1.5.1
github.com/stretchr/testify v1.6.1
github.com/studio-b12/gowebdav v0.0.0-20200303150724-9380631c29a1
github.com/tensorflow/tensorflow v1.15.2
github.com/tidwall/gjson v1.6.0
github.com/tidwall/pretty v1.0.1 // indirect
github.com/ulule/deepcopier v0.0.0-20200430083143-45decc6639b6
github.com/urfave/cli v1.22.4
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37
golang.org/x/image v0.0.0-20200430140353-33d19683fad8 // indirect
golang.org/x/net v0.0.0-20200513185701-a91f0712d120
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9 // indirect
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
golang.org/x/image v0.0.0-20200618115811-c13761719519 // indirect
golang.org/x/net v0.0.0-20200625001655-4c5254603344
golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4 // indirect
google.golang.org/protobuf v1.25.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/ugjka/go-tz.v2 v2.0.9
gopkg.in/yaml.v2 v2.3.0
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect
)
go 1.13

90
go.sum
View File

@@ -1,9 +1,12 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/allegro/bigcache v1.2.1 h1:hg1sY1raCwic3Vnsvje6TT7/pnZba83LeFck5NrFKSc=
github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
github.com/araddon/dateparse v0.0.0-20200409225146-d820a6159ab1 h1:TEBmxO80TM04L8IuMWk77SGL1HomBmKTdzdJLLWznxI=
github.com/araddon/dateparse v0.0.0-20200409225146-d820a6159ab1/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
@@ -35,6 +38,8 @@ github.com/dsoprea/go-utility v0.0.0-20200512094054-1abbbc781176 h1:CfXezFYb2STG
github.com/dsoprea/go-utility v0.0.0-20200512094054-1abbbc781176/go.mod h1:95+K3z2L0mqsVYd6yveIv1lmtT3tcQQ3dVakPySffW8=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
@@ -53,6 +58,8 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-playground/validator/v10 v10.3.0 h1:nZU+7q+yJoFmwvNgv/LnPUkwPal62+b2xXj0AU1Es7o=
github.com/go-playground/validator/v10 v10.3.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
@@ -61,12 +68,27 @@ github.com/golang/geo v0.0.0-20190916061304-5b978397cfec h1:lJwO/92dFXWeXOZdoGXg
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d h1:C/hKUcHT483btRbeGkrRjJz+Zbcj8audldIi9tRJDCc=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/open-location-code/go v0.0.0-20191230190541-a6eb95b4d2f9 h1:6ILzS4n0F17S38XvOB1BcyzB+0BtVzU77EyuMtkMffo=
github.com/google/open-location-code/go v0.0.0-20191230190541-a6eb95b4d2f9/go.mod h1:eJfRN6aj+kR/rnua/rw9jAgYhqoMHldQkdTi+sePRKk=
@@ -83,6 +105,8 @@ github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/karrick/godirwalk v1.15.6 h1:Yf2mmR8TJy+8Fa0SuQVto5SYap6IF7lNVX4Jdl8G1qA=
@@ -123,6 +147,7 @@ github.com/paulmach/go.geojson v1.4.0 h1:5x5moCkCtDo5x8af62P9IOAYGQcYHtxz2QJ3x1D
github.com/paulmach/go.geojson v1.4.0/go.mod h1:YaKx1hKpWF+T2oj2lFJPsW/t1Q5e1jQI61eoQSTwpIs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ=
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
@@ -137,6 +162,7 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5I
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
@@ -144,8 +170,8 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/studio-b12/gowebdav v0.0.0-20200303150724-9380631c29a1 h1:TPyHV/OgChqNcnYqCoCvIFjR9TU60gFXXBKnhOBzVEI=
github.com/studio-b12/gowebdav v0.0.0-20200303150724-9380631c29a1/go.mod h1:gCcfDlA1Y7GqOaeEKw5l9dOGx1VLdc/HuQSlQAaZ30s=
github.com/tensorflow/tensorflow v1.15.2 h1:7/f/A664Tml/nRJg04+p3StcrsT53mkcvmxYHXI21Qo=
@@ -169,13 +195,20 @@ github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtX
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw=
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200618115811-c13761719519 h1:1e2ufUJNM3lCHEY5jIgac/7UTjd6cgJNdatjPdFWf34=
golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
@@ -185,21 +218,52 @@ golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5 h1:WQ8q63x+f/zpC8Ac1s9wLElVo
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120 h1:EZ3cVSzKOlJxAd8e8YAJ7no8nNypTxexh/YE/xW3ZEY=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9 h1:YTzHMGlqJu67/uEo1lBv0n3wBXhXNeUbB1XfN2vmTm0=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4 h1:5/PjkGUjvEU5Gl6BxmvKRPpqo2uNMv4rcHBMwzk/st8=
golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -213,3 +277,9 @@ gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

73
internal/acl/acl.go Normal file
View File

@@ -0,0 +1,73 @@
/*
Package acl contains PhotoPrism's access control lists for authorizing user actions.
Copyright (c) 2018 - 2020 Michael Mayer <hello@photoprism.org>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
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.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
PhotoPrism™ is a registered trademark of Michael Mayer. You may use it as required
to describe our software, run your own server, for educational purposes, but not for
offering commercial goods, products, or services without prior written permission.
In other words, please ask.
Feel free to send an e-mail to hello@photoprism.org 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.org/developer-guide/
*/
package acl
type Permission struct {
Roles Roles
Actions Actions
}
type ACL map[Resource]Roles
func (l ACL) Deny(resource Resource, role Role, action Action) bool {
return !l.Allow(resource, role, action)
}
func (l ACL) Allow(resource Resource, role Role, action Action) bool {
if p, ok := l[resource]; ok {
return p.Allow(role, action)
} else if p, ok := l[ResourceDefault]; ok {
return p.Allow(role, action)
}
return false
}
func (a Actions) Allow(action Action) bool {
if result, ok := a[action]; ok {
return result
} else if result, ok := a[ActionDefault]; ok {
return result
}
return false
}
func (r Roles) Allow(role Role, action Action) bool {
if a, ok := r[role]; ok {
return a.Allow(action)
} else if a, ok := r[RoleDefault]; ok {
return a.Allow(action)
}
return false
}

21
internal/acl/actions.go Normal file
View File

@@ -0,0 +1,21 @@
package acl
type Action string
type Actions map[Action]bool
const (
ActionDefault Action = "*"
ActionSearch Action = "search"
ActionCreate Action = "create"
ActionRead Action = "read"
ActionUpdate Action = "update"
ActionDelete Action = "delete"
ActionPrivate Action = "private"
ActionUpload Action = "upload"
ActionDownload Action = "download"
ActionShare Action = "share"
ActionLike Action = "like"
ActionComment Action = "comment"
ActionExport Action = "export"
ActionImport Action = "import"
)

View File

@@ -0,0 +1,19 @@
package acl
var Permissions = ACL{
ResourceDefault: Roles{
RoleAdmin: Actions{ActionDefault: true},
},
ResourceConfig: Roles{
RoleAdmin: Actions{ActionDefault: true},
RoleGuest: Actions{ActionRead: true},
},
ResourceAlbums: Roles{
RoleAdmin: Actions{ActionDefault: true},
RoleGuest: Actions{ActionSearch: true, ActionRead: true},
},
ResourcePhotos: Roles{
RoleAdmin: Actions{ActionDefault: true},
RoleGuest: Actions{ActionSearch: true, ActionRead: true},
},
}

View File

@@ -0,0 +1,52 @@
package acl
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestACL_Allow(t *testing.T) {
t.Run("photos/admin/update", func(t *testing.T) {
assert.True(t, Permissions.Allow(ResourcePhotos, RoleAdmin, ActionUpdate))
})
t.Run("default/admin", func(t *testing.T) {
assert.True(t, Permissions.Allow(ResourceDefault, RoleAdmin, ActionDefault))
})
t.Run("default/guest", func(t *testing.T) {
assert.False(t, Permissions.Allow(ResourceDefault, RoleGuest, ActionDefault))
})
t.Run("photos/guest/search", func(t *testing.T) {
assert.True(t, Permissions.Allow(ResourcePhotos, RoleGuest, ActionSearch))
})
t.Run("photos/guest/default", func(t *testing.T) {
assert.False(t, Permissions.Allow(ResourcePhotos, RoleGuest, ActionDefault))
})
t.Run("albums/guest/search", func(t *testing.T) {
assert.True(t, Permissions.Allow(ResourceAlbums, RoleGuest, ActionSearch))
})
t.Run("albums/guest/default", func(t *testing.T) {
assert.False(t, Permissions.Allow(ResourceAlbums, RoleGuest, ActionDefault))
})
}
func TestACL_Deny(t *testing.T) {
t.Run("default/admin", func(t *testing.T) {
assert.False(t, Permissions.Deny(ResourceDefault, RoleAdmin, ActionDefault))
})
t.Run("default/guest", func(t *testing.T) {
assert.True(t, Permissions.Deny(ResourceDefault, RoleGuest, ActionDefault))
})
t.Run("photos/guest/search", func(t *testing.T) {
assert.False(t, Permissions.Deny(ResourcePhotos, RoleGuest, ActionSearch))
})
t.Run("photos/guest/default", func(t *testing.T) {
assert.True(t, Permissions.Deny(ResourcePhotos, RoleGuest, ActionDefault))
})
t.Run("albums/guest/search", func(t *testing.T) {
assert.False(t, Permissions.Deny(ResourceAlbums, RoleGuest, ActionSearch))
})
t.Run("albums/guest/default", func(t *testing.T) {
assert.True(t, Permissions.Deny(ResourceAlbums, RoleGuest, ActionDefault))
})
}

25
internal/acl/resources.go Normal file
View File

@@ -0,0 +1,25 @@
package acl
type Resource string
const (
ResourceDefault Resource = "*"
ResourceConfig Resource = "config"
ResourceSettings Resource = "settings"
ResourceLogs Resource = "logs"
ResourceAccounts Resource = "accounts"
ResourceAlbums Resource = "albums"
ResourceCameras Resource = "cameras"
ResourceCategories Resource = "categories"
ResourceCountries Resource = "countries"
ResourceFiles Resource = "files"
ResourceFolders Resource = "folders"
ResourceLabels Resource = "labels"
ResourceLenses Resource = "lenses"
ResourceLinks Resource = "links"
ResourceLocations Resource = "locations"
ResourcePasswords Resource = "passwords"
ResourcePeople Resource = "people"
ResourcePhotos Resource = "photos"
ResourcePlaces Resource = "places"
)

13
internal/acl/roles.go Normal file
View File

@@ -0,0 +1,13 @@
package acl
type Role string
type Roles map[Role]Actions
const (
RoleDefault Role = "*"
RoleAdmin Role = "admin"
RoleChild Role = "child"
RoleFamily Role = "family"
RoleFriend Role = "friend"
RoleGuest Role = "guest"
)

View File

@@ -6,19 +6,22 @@ import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/photoprism/photoprism/internal/config"
"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/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/workers"
"github.com/photoprism/photoprism/pkg/txt"
)
// GET /api/v1/accounts
func GetAccounts(router *gin.RouterGroup, conf *config.Config) {
func GetAccounts(router *gin.RouterGroup) {
router.GET("/accounts", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourceAccounts, acl.ActionSearch)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -51,9 +54,11 @@ func GetAccounts(router *gin.RouterGroup, conf *config.Config) {
//
// Parameters:
// id: string Account ID as returned by the API
func GetAccount(router *gin.RouterGroup, conf *config.Config) {
func GetAccount(router *gin.RouterGroup) {
router.GET("/accounts/:id", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourceAccounts, acl.ActionRead)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -72,9 +77,11 @@ func GetAccount(router *gin.RouterGroup, conf *config.Config) {
//
// Parameters:
// id: string Account ID as returned by the API
func GetAccountDirs(router *gin.RouterGroup, conf *config.Config) {
func GetAccountDirs(router *gin.RouterGroup) {
router.GET("/accounts/:id/dirs", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourceAccounts, acl.ActionRead)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -104,9 +111,11 @@ func GetAccountDirs(router *gin.RouterGroup, conf *config.Config) {
//
// Parameters:
// id: string Account ID as returned by the API
func ShareWithAccount(router *gin.RouterGroup, conf *config.Config) {
func ShareWithAccount(router *gin.RouterGroup) {
router.POST("/accounts/:id/share", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourceAccounts, acl.ActionUpload)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -142,16 +151,18 @@ func ShareWithAccount(router *gin.RouterGroup, conf *config.Config) {
entity.FirstOrCreateFileShare(fileShare)
}
workers.StartShare(conf)
workers.StartShare(service.Config())
c.JSON(http.StatusOK, files)
})
}
// POST /api/v1/accounts
func CreateAccount(router *gin.RouterGroup, conf *config.Config) {
func CreateAccount(router *gin.RouterGroup) {
router.POST("/accounts", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourceAccounts, acl.ActionCreate)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -189,9 +200,11 @@ func CreateAccount(router *gin.RouterGroup, conf *config.Config) {
//
// Parameters:
// id: string Account ID as returned by the API
func UpdateAccount(router *gin.RouterGroup, conf *config.Config) {
func UpdateAccount(router *gin.RouterGroup) {
router.PUT("/accounts/:id", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourceAccounts, acl.ActionUpdate)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -245,9 +258,11 @@ func UpdateAccount(router *gin.RouterGroup, conf *config.Config) {
//
// Parameters:
// id: string Account ID as returned by the API
func DeleteAccount(router *gin.RouterGroup, conf *config.Config) {
func DeleteAccount(router *gin.RouterGroup) {
router.DELETE("/accounts/:id", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourceAccounts, acl.ActionDelete)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}

View File

@@ -10,8 +10,8 @@ import (
func TestGetAccounts(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
GetAccounts(router, conf)
app, router, _ := NewApiTest()
GetAccounts(router)
r := PerformRequest(app, "GET", "/api/v1/accounts?count=10")
val := gjson.Get(r.Body.String(), "#(AccName=\"Test Account\").AccURL")
count := gjson.Get(r.Body.String(), "#")
@@ -20,8 +20,8 @@ func TestGetAccounts(t *testing.T) {
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("invalid request", func(t *testing.T) {
app, router, conf := NewApiTest()
GetAccounts(router, conf)
app, router, _ := NewApiTest()
GetAccounts(router)
r := PerformRequest(app, "GET", "/api/v1/accounts?xxx=10")
assert.Equal(t, http.StatusBadRequest, r.Code)
})
@@ -29,16 +29,16 @@ func TestGetAccounts(t *testing.T) {
func TestGetAccount(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
GetAccount(router, conf)
app, router, _ := NewApiTest()
GetAccount(router)
r := PerformRequest(app, "GET", "/api/v1/accounts/1000000")
val := gjson.Get(r.Body.String(), "AccName")
assert.Equal(t, "Test Account", val.String())
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("account not found", func(t *testing.T) {
app, router, conf := NewApiTest()
GetAccount(router, conf)
app, router, _ := NewApiTest()
GetAccount(router)
r := PerformRequest(app, "GET", "/api/v1/accounts/999000")
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "Account not found", val.String())
@@ -48,8 +48,8 @@ func TestGetAccount(t *testing.T) {
func TestGetAccountDirs(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
GetAccountDirs(router, conf)
app, router, _ := NewApiTest()
GetAccountDirs(router)
r := PerformRequest(app, "GET", "/api/v1/accounts/1000000/dirs")
count := gjson.Get(r.Body.String(), "#")
assert.LessOrEqual(t, int64(2), count.Int())
@@ -58,8 +58,8 @@ func TestGetAccountDirs(t *testing.T) {
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("account not found", func(t *testing.T) {
app, router, conf := NewApiTest()
GetAccountDirs(router, conf)
app, router, _ := NewApiTest()
GetAccountDirs(router)
r := PerformRequest(app, "GET", "/api/v1/accounts/999000/dirs")
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "Account not found", val.String())
@@ -69,16 +69,16 @@ func TestGetAccountDirs(t *testing.T) {
func TestShareWithAccount(t *testing.T) {
t.Run("invalid request", func(t *testing.T) {
app, router, conf := NewApiTest()
ShareWithAccount(router, conf)
app, router, _ := NewApiTest()
ShareWithAccount(router)
r := PerformRequest(app, "POST", "/api/v1/accounts/1000000/share")
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "Invalid request", val.String())
assert.Equal(t, http.StatusBadRequest, r.Code)
})
t.Run("account not found", func(t *testing.T) {
app, router, conf := NewApiTest()
ShareWithAccount(router, conf)
app, router, _ := NewApiTest()
ShareWithAccount(router)
r := PerformRequest(app, "POST", "/api/v1/accounts/999000/share")
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "Account not found", val.String())
@@ -88,16 +88,16 @@ func TestShareWithAccount(t *testing.T) {
func TestCreateAccount(t *testing.T) {
t.Run("invalid request", func(t *testing.T) {
app, router, conf := NewApiTest()
CreateAccount(router, conf)
app, router, _ := NewApiTest()
CreateAccount(router)
r := PerformRequest(app, "POST", "/api/v1/accounts")
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "Invalid request", val.String())
assert.Equal(t, http.StatusBadRequest, r.Code)
})
t.Run("could not connect", func(t *testing.T) {
app, router, conf := NewApiTest()
CreateAccount(router, conf)
app, router, _ := NewApiTest()
CreateAccount(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/accounts", `{"AccName": "CreateTest1", "AccOwner": "Test", "AccUrl": "http://webdav123/", "AccType": "webdav",
"AccKey": "123", "AccUser": "testuser", "AccPass": "testpasswd", "AccError": "", "AccShare": false, "AccSync": false, "RetryLimit": 3, "SharePath": "", "ShareSize": "", "ShareExpires": 0,
"SyncPath": "", "SyncInterval": 3, "SyncUpload": false, "SyncDownload": false, "SyncFilenames": false, "SyncRaw": false}`)
@@ -106,8 +106,8 @@ func TestCreateAccount(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, r.Code)
})
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
CreateAccount(router, conf)
app, router, _ := NewApiTest()
CreateAccount(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/accounts", `{"AccName": "CreateTest", "AccOwner": "Test", "AccUrl": "http://webdav-dummy/", "AccType": "webdav",
"AccKey": "123", "AccUser": "admin", "AccPass": "photoprism", "AccError": "", "AccShare": false, "AccSync": false, "RetryLimit": 3, "SharePath": "", "ShareSize": "", "ShareExpires": 0,
"SyncPath": "", "SyncInterval": 3, "SyncUpload": false, "SyncDownload": false, "SyncFilenames": false, "SyncRaw": false}`)
@@ -118,8 +118,8 @@ func TestCreateAccount(t *testing.T) {
}
func TestUpdateAccount(t *testing.T) {
app, router, conf := NewApiTest()
CreateAccount(router, conf)
app, router, _ := NewApiTest()
CreateAccount(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/accounts", `{"AccName": "CreateTest3", "AccOwner": "TestUpdate", "AccUrl": "http://webdav-dummy/", "AccType": "webdav",
"AccKey": "123", "AccUser": "admin", "AccPass": "photoprism", "AccError": "", "AccShare": false, "AccSync": false, "RetryLimit": 3, "SharePath": "", "ShareSize": "", "ShareExpires": 0,
"SyncPath": "", "SyncInterval": 5, "SyncUpload": false, "SyncDownload": false, "SyncFilenames": false, "SyncRaw": false}`)
@@ -133,8 +133,8 @@ func TestUpdateAccount(t *testing.T) {
id := gjson.Get(r.Body.String(), "ID").String()
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
UpdateAccount(router, conf)
app, router, _ := NewApiTest()
UpdateAccount(router)
r := PerformRequestWithBody(app, "PUT", "/api/v1/accounts/"+id, `{"AccName": "CreateTestUpdated", "AccOwner": "TestUpdated123", "SyncInterval": 9}`)
val := gjson.Get(r.Body.String(), "AccOwner")
assert.Equal(t, "TestUpdated123", val.String())
@@ -146,8 +146,8 @@ func TestUpdateAccount(t *testing.T) {
})
t.Run("not found", func(t *testing.T) {
app, router, conf := NewApiTest()
UpdateAccount(router, conf)
app, router, _ := NewApiTest()
UpdateAccount(router)
r := PerformRequestWithBody(app, "PUT", "/api/v1/accounts/xxx", `{"AccName": "CreateTestUpdated", "AccOwner": "TestUpdated123", "SyncInterval": 9}`)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "Photo not found", val.String())
@@ -155,8 +155,8 @@ func TestUpdateAccount(t *testing.T) {
})
t.Run("changes could not be saved", func(t *testing.T) {
app, router, conf := NewApiTest()
UpdateAccount(router, conf)
app, router, _ := NewApiTest()
UpdateAccount(router)
r := PerformRequestWithBody(app, "PUT", "/api/v1/accounts/"+id, `{"AccName": 6, "AccOwner": "TestUpdated123", "SyncInterval": 9, "AccUrl": "https:xxx.com"}`)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "Changes could not be saved", val.String())
@@ -165,8 +165,8 @@ func TestUpdateAccount(t *testing.T) {
}
func TestDeleteAccount(t *testing.T) {
app, router, conf := NewApiTest()
CreateAccount(router, conf)
app, router, _ := NewApiTest()
CreateAccount(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/accounts", `{"AccName": "DeleteTest", "AccOwner": "TestDelete", "AccUrl": "http://webdav-dummy/", "AccType": "webdav",
"AccKey": "123", "AccUser": "admin", "AccPass": "photoprism", "AccError": "", "AccShare": false, "AccSync": false, "RetryLimit": 3, "SharePath": "", "ShareSize": "", "ShareExpires": 0,
"SyncPath": "", "SyncInterval": 5, "SyncUpload": false, "SyncDownload": false, "SyncFilenames": false, "SyncRaw": false}`)
@@ -174,13 +174,13 @@ func TestDeleteAccount(t *testing.T) {
id := gjson.Get(r.Body.String(), "ID").String()
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
DeleteAccount(router, conf)
app, router, _ := NewApiTest()
DeleteAccount(router)
r := PerformRequest(app, "DELETE", "/api/v1/accounts/"+id)
val := gjson.Get(r.Body.String(), "AccOwner")
assert.Equal(t, "TestDelete", val.String())
assert.Equal(t, http.StatusOK, r.Code)
GetAccount(router, conf)
GetAccount(router)
r2 := PerformRequest(app, "GET", "/api/v1/accounts/"+id)
val2 := gjson.Get(r2.Body.String(), "error")
assert.Equal(t, "Account not found", val2.String())
@@ -188,8 +188,8 @@ func TestDeleteAccount(t *testing.T) {
})
t.Run("not found", func(t *testing.T) {
app, router, conf := NewApiTest()
DeleteAccount(router, conf)
app, router, _ := NewApiTest()
DeleteAccount(router)
r := PerformRequest(app, "DELETE", "/api/v1/accounts/xxx")
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "Account not found", val.String())

View File

@@ -12,6 +12,7 @@ import (
"strings"
"time"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
@@ -24,14 +25,15 @@ import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/pkg/txt"
)
// GET /api/v1/albums
func GetAlbums(router *gin.RouterGroup, conf *config.Config) {
func GetAlbums(router *gin.RouterGroup) {
router.GET("/albums", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionSearch)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -45,6 +47,11 @@ func GetAlbums(router *gin.RouterGroup, conf *config.Config) {
return
}
// Guest permissions are limited to shared albums.
if s.Guest() {
f.ID = s.Shares.String()
}
result, err := query.AlbumSearch(f)
if err != nil {
@@ -61,8 +68,15 @@ func GetAlbums(router *gin.RouterGroup, conf *config.Config) {
}
// GET /api/v1/albums/:uid
func GetAlbum(router *gin.RouterGroup, conf *config.Config) {
func GetAlbum(router *gin.RouterGroup) {
router.GET("/albums/:uid", func(c *gin.Context) {
s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionRead)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
id := c.Param("uid")
m, err := query.AlbumByUID(id)
@@ -76,9 +90,11 @@ func GetAlbum(router *gin.RouterGroup, conf *config.Config) {
}
// POST /api/v1/albums
func CreateAlbum(router *gin.RouterGroup, conf *config.Config) {
func CreateAlbum(router *gin.RouterGroup) {
router.POST("/albums", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionCreate)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -103,7 +119,7 @@ func CreateAlbum(router *gin.RouterGroup, conf *config.Config) {
event.Success("album created")
UpdateClientConfig(conf)
UpdateClientConfig()
PublishAlbumEvent(EntityCreated, m.AlbumUID, c)
@@ -112,9 +128,11 @@ func CreateAlbum(router *gin.RouterGroup, conf *config.Config) {
}
// PUT /api/v1/albums/:uid
func UpdateAlbum(router *gin.RouterGroup, conf *config.Config) {
func UpdateAlbum(router *gin.RouterGroup) {
router.PUT("/albums/:uid", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionUpdate)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -147,7 +165,7 @@ func UpdateAlbum(router *gin.RouterGroup, conf *config.Config) {
return
}
UpdateClientConfig(conf)
UpdateClientConfig()
event.Success("album saved")
@@ -158,13 +176,16 @@ func UpdateAlbum(router *gin.RouterGroup, conf *config.Config) {
}
// DELETE /api/v1/albums/:uid
func DeleteAlbum(router *gin.RouterGroup, conf *config.Config) {
func DeleteAlbum(router *gin.RouterGroup) {
router.DELETE("/albums/:uid", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionDelete)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
conf := service.Config()
id := c.Param("uid")
m, err := query.AlbumByUID(id)
@@ -178,7 +199,7 @@ func DeleteAlbum(router *gin.RouterGroup, conf *config.Config) {
conf.Db().Delete(&m)
UpdateClientConfig(conf)
UpdateClientConfig()
event.Success(fmt.Sprintf("album %s deleted", txt.Quote(m.AlbumTitle)))
c.JSON(http.StatusOK, m)
@@ -189,13 +210,16 @@ func DeleteAlbum(router *gin.RouterGroup, conf *config.Config) {
//
// Parameters:
// uid: string Album UID
func LikeAlbum(router *gin.RouterGroup, conf *config.Config) {
func LikeAlbum(router *gin.RouterGroup) {
router.POST("/albums/:uid/like", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionLike)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
conf := service.Config()
id := c.Param("uid")
album, err := query.AlbumByUID(id)
@@ -207,7 +231,7 @@ func LikeAlbum(router *gin.RouterGroup, conf *config.Config) {
album.AlbumFavorite = true
conf.Db().Save(&album)
UpdateClientConfig(conf)
UpdateClientConfig()
PublishAlbumEvent(EntityUpdated, id, c)
c.JSON(http.StatusOK, http.Response{})
@@ -218,13 +242,16 @@ func LikeAlbum(router *gin.RouterGroup, conf *config.Config) {
//
// Parameters:
// uid: string Album UID
func DislikeAlbum(router *gin.RouterGroup, conf *config.Config) {
func DislikeAlbum(router *gin.RouterGroup) {
router.DELETE("/albums/:uid/like", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionLike)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
conf := service.Config()
id := c.Param("uid")
album, err := query.AlbumByUID(id)
@@ -236,7 +263,7 @@ func DislikeAlbum(router *gin.RouterGroup, conf *config.Config) {
album.AlbumFavorite = false
conf.Db().Save(&album)
UpdateClientConfig(conf)
UpdateClientConfig()
PublishAlbumEvent(EntityUpdated, id, c)
c.JSON(http.StatusOK, http.Response{})
@@ -244,9 +271,11 @@ func DislikeAlbum(router *gin.RouterGroup, conf *config.Config) {
}
// POST /api/v1/albums/:uid/clone
func CloneAlbums(router *gin.RouterGroup, conf *config.Config) {
func CloneAlbums(router *gin.RouterGroup) {
router.POST("/albums/:uid/clone", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionUpdate)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -296,9 +325,11 @@ func CloneAlbums(router *gin.RouterGroup, conf *config.Config) {
}
// POST /api/v1/albums/:uid/photos
func AddPhotosToAlbum(router *gin.RouterGroup, conf *config.Config) {
func AddPhotosToAlbum(router *gin.RouterGroup) {
router.POST("/albums/:uid/photos", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionUpdate)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -343,9 +374,11 @@ func AddPhotosToAlbum(router *gin.RouterGroup, conf *config.Config) {
}
// DELETE /api/v1/albums/:uid/photos
func RemovePhotosFromAlbum(router *gin.RouterGroup, conf *config.Config) {
func RemovePhotosFromAlbum(router *gin.RouterGroup) {
router.DELETE("/albums/:uid/photos", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionUpdate)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -387,15 +420,15 @@ func RemovePhotosFromAlbum(router *gin.RouterGroup, conf *config.Config) {
}
// GET /api/v1/albums/:uid/dl
func DownloadAlbum(router *gin.RouterGroup, conf *config.Config) {
func DownloadAlbum(router *gin.RouterGroup) {
router.GET("/albums/:uid/dl", func(c *gin.Context) {
if InvalidDownloadToken(c, conf) {
if InvalidDownloadToken(c) {
c.Data(http.StatusForbidden, "image/svg+xml", brokenIconSvg)
return
}
start := time.Now()
conf := service.Config()
a, err := query.AlbumByUID(c.Param("uid"))
if err != nil {
@@ -476,14 +509,15 @@ func DownloadAlbum(router *gin.RouterGroup, conf *config.Config) {
// Parameters:
// uid: string Album UID
// type: string Thumbnail type, see photoprism.ThumbnailTypes
func AlbumThumbnail(router *gin.RouterGroup, conf *config.Config) {
func AlbumThumbnail(router *gin.RouterGroup) {
router.GET("/albums/:uid/t/:token/:type", func(c *gin.Context) {
if InvalidToken(c, conf) {
if InvalidToken(c) {
c.Data(http.StatusForbidden, "image/svg+xml", albumIconSvg)
return
}
start := time.Now()
conf := service.Config()
typeName := c.Param("type")
uid := c.Param("uid")

View File

@@ -11,16 +11,16 @@ import (
func TestGetAlbums(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
GetAlbums(router, conf)
app, router, _ := NewApiTest()
GetAlbums(router)
r := PerformRequest(app, "GET", "/api/v1/albums?count=10")
count := gjson.Get(r.Body.String(), "#")
assert.LessOrEqual(t, int64(3), count.Int())
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("invalid request", func(t *testing.T) {
app, router, conf := NewApiTest()
GetAlbums(router, conf)
app, router, _ := NewApiTest()
GetAlbums(router)
r := PerformRequest(app, "GET", "/api/v1/albums?xxx=10")
assert.Equal(t, http.StatusBadRequest, r.Code)
})
@@ -28,16 +28,16 @@ func TestGetAlbums(t *testing.T) {
func TestGetAlbum(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
GetAlbum(router, conf)
app, router, _ := NewApiTest()
GetAlbum(router)
r := PerformRequest(app, "GET", "/api/v1/albums/at9lxuqxpogaaba8")
val := gjson.Get(r.Body.String(), "Slug")
assert.Equal(t, "holiday-2030", val.String())
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("invalid request", func(t *testing.T) {
app, router, conf := NewApiTest()
GetAlbum(router, conf)
app, router, _ := NewApiTest()
GetAlbum(router)
r := PerformRequest(app, "GET", "/api/v1/albums/999000")
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "Album not found", val.String())
@@ -47,8 +47,8 @@ func TestGetAlbum(t *testing.T) {
func TestCreateAlbum(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
CreateAlbum(router, conf)
app, router, _ := NewApiTest()
CreateAlbum(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"Title": "New created album", "Notes": "", "Favorite": true}`)
val := gjson.Get(r.Body.String(), "Slug")
assert.Equal(t, "new-created-album", val.String())
@@ -57,22 +57,22 @@ func TestCreateAlbum(t *testing.T) {
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("invalid request", func(t *testing.T) {
app, router, conf := NewApiTest()
CreateAlbum(router, conf)
app, router, _ := NewApiTest()
CreateAlbum(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"Title": 333, "Description": "Created via unit test", "Notes": "", "Favorite": true}`)
assert.Equal(t, http.StatusBadRequest, r.Code)
})
}
func TestUpdateAlbum(t *testing.T) {
app, router, conf := NewApiTest()
CreateAlbum(router, conf)
app, router, _ := NewApiTest()
CreateAlbum(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"Title": "Update", "Description": "To be updated", "Notes": "", "Favorite": true}`)
assert.Equal(t, http.StatusOK, r.Code)
uid := gjson.Get(r.Body.String(), "UID").String()
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
UpdateAlbum(router, conf)
app, router, _ := NewApiTest()
UpdateAlbum(router)
r := PerformRequestWithBody(app, "PUT", "/api/v1/albums/"+uid, `{"Title": "Updated01", "Notes": "", "Favorite": false}`)
val := gjson.Get(r.Body.String(), "Slug")
assert.Equal(t, "updated01", val.String())
@@ -82,15 +82,15 @@ func TestUpdateAlbum(t *testing.T) {
})
t.Run("invalid request", func(t *testing.T) {
app, router, conf := NewApiTest()
UpdateAlbum(router, conf)
app, router, _ := NewApiTest()
UpdateAlbum(router)
r := PerformRequestWithBody(app, "PUT", "/api/v1/albums"+uid, `{"Title": 333, "Description": "Created via unit test", "Notes": "", "Favorite": true}`)
assert.Equal(t, http.StatusNotFound, r.Code)
})
t.Run("not found", func(t *testing.T) {
app, router, conf := NewApiTest()
UpdateAlbum(router, conf)
app, router, _ := NewApiTest()
UpdateAlbum(router)
r := PerformRequestWithBody(app, "PUT", "/api/v1/albums/xxx", `{"Title": "Update03", "Description": "Created via unit test", "Notes": "", "Favorite": true}`)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "Album not found", val.String())
@@ -98,26 +98,26 @@ func TestUpdateAlbum(t *testing.T) {
})
}
func TestDeleteAlbum(t *testing.T) {
app, router, conf := NewApiTest()
CreateAlbum(router, conf)
app, router, _ := NewApiTest()
CreateAlbum(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"Title": "Delete", "Description": "To be deleted", "Notes": "", "Favorite": true}`)
assert.Equal(t, http.StatusOK, r.Code)
uid := gjson.Get(r.Body.String(), "UID").String()
t.Run("delete existing album", func(t *testing.T) {
app, router, conf := NewApiTest()
DeleteAlbum(router, conf)
app, router, _ := NewApiTest()
DeleteAlbum(router)
r := PerformRequest(app, "DELETE", "/api/v1/albums/"+uid)
assert.Equal(t, http.StatusOK, r.Code)
val := gjson.Get(r.Body.String(), "Slug")
assert.Equal(t, "delete", val.String())
GetAlbums(router, conf)
GetAlbums(router)
r2 := PerformRequest(app, "GET", "/api/v1/albums/"+uid)
assert.Equal(t, http.StatusNotFound, r2.Code)
})
t.Run("delete not existing album", func(t *testing.T) {
app, router, conf := NewApiTest()
DeleteAlbum(router, conf)
app, router, _ := NewApiTest()
DeleteAlbum(router)
r := PerformRequest(app, "DELETE", "/api/v1/albums/999000")
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "Album not found", val.String())
@@ -127,20 +127,20 @@ func TestDeleteAlbum(t *testing.T) {
func TestLikeAlbum(t *testing.T) {
t.Run("like not existing album", func(t *testing.T) {
app, router, ctx := NewApiTest()
app, router, _ := NewApiTest()
LikeAlbum(router, ctx)
LikeAlbum(router)
r := PerformRequest(app, "POST", "/api/v1/albums/xxx/like")
assert.Equal(t, http.StatusNotFound, r.Code)
})
t.Run("like existing album", func(t *testing.T) {
app, router, ctx := NewApiTest()
app, router, _ := NewApiTest()
LikeAlbum(router, ctx)
LikeAlbum(router)
r := PerformRequest(app, "POST", "/api/v1/albums/at9lxuqxpogaaba7/like")
assert.Equal(t, http.StatusOK, r.Code)
GetAlbum(router, ctx)
GetAlbum(router)
r2 := PerformRequest(app, "GET", "/api/v1/albums/at9lxuqxpogaaba7")
val := gjson.Get(r2.Body.String(), "Favorite")
assert.Equal(t, "true", val.String())
@@ -149,21 +149,21 @@ func TestLikeAlbum(t *testing.T) {
func TestDislikeAlbum(t *testing.T) {
t.Run("dislike not existing album", func(t *testing.T) {
app, router, conf := NewApiTest()
app, router, _ := NewApiTest()
DislikeAlbum(router, conf)
DislikeAlbum(router)
r := PerformRequest(app, "DELETE", "/api/v1/albums/5678/like")
assert.Equal(t, http.StatusNotFound, r.Code)
})
t.Run("dislike existing album", func(t *testing.T) {
app, router, conf := NewApiTest()
app, router, _ := NewApiTest()
DislikeAlbum(router, conf)
DislikeAlbum(router)
r := PerformRequest(app, "DELETE", "/api/v1/albums/at9lxuqxpogaaba8/like")
assert.Equal(t, http.StatusOK, r.Code)
GetAlbum(router, conf)
GetAlbum(router)
r2 := PerformRequest(app, "GET", "/api/v1/albums/at9lxuqxpogaaba8")
val := gjson.Get(r2.Body.String(), "Favorite")
assert.Equal(t, "false", val.String())
@@ -171,77 +171,77 @@ func TestDislikeAlbum(t *testing.T) {
}
func TestAddPhotosToAlbum(t *testing.T) {
app, router, conf := NewApiTest()
CreateAlbum(router, conf)
app, router, _ := NewApiTest()
CreateAlbum(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"Title": "Add photos", "Description": "", "Notes": "", "Favorite": true}`)
assert.Equal(t, http.StatusOK, r.Code)
uid := gjson.Get(r.Body.String(), "UID").String()
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
AddPhotosToAlbum(router, conf)
app, router, _ := NewApiTest()
AddPhotosToAlbum(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums/"+uid+"/photos", `{"photos": ["pt9jtdre2lvl0y12", "pt9jtdre2lvl0y11"]}`)
val := gjson.Get(r.Body.String(), "message")
assert.Equal(t, "photos added to album", val.String())
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("add one photo to album", func(t *testing.T) {
app, router, conf := NewApiTest()
AddPhotosToAlbum(router, conf)
app, router, _ := NewApiTest()
AddPhotosToAlbum(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums/"+uid+"/photos", `{"photos": ["pt9jtdre2lvl0y12"]}`)
val := gjson.Get(r.Body.String(), "message")
assert.Equal(t, "photos added to album", val.String())
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("invalid request", func(t *testing.T) {
app, router, conf := NewApiTest()
AddPhotosToAlbum(router, conf)
app, router, _ := NewApiTest()
AddPhotosToAlbum(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums/"+uid+"/photos", `{"photos": [123, "pt9jtdre2lvl0yxx"]}`)
assert.Equal(t, http.StatusBadRequest, r.Code)
})
t.Run("not found", func(t *testing.T) {
app, router, conf := NewApiTest()
AddPhotosToAlbum(router, conf)
app, router, _ := NewApiTest()
AddPhotosToAlbum(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums/xxx/photos", `{"photos": ["pt9jtdre2lvl0yxx"]}`)
assert.Equal(t, http.StatusNotFound, r.Code)
})
}
func TestRemovePhotosFromAlbum(t *testing.T) {
app, router, conf := NewApiTest()
CreateAlbum(router, conf)
app, router, _ := NewApiTest()
CreateAlbum(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"Title": "Remove photos", "Description": "", "Notes": "", "Favorite": true}`)
assert.Equal(t, http.StatusOK, r.Code)
uid := gjson.Get(r.Body.String(), "UID").String()
AddPhotosToAlbum(router, conf)
AddPhotosToAlbum(router)
r2 := PerformRequestWithBody(app, "POST", "/api/v1/albums/"+uid+"/photos", `{"photos": ["pt9jtdre2lvl0y12", "pt9jtdre2lvl0y11"]}`)
assert.Equal(t, http.StatusOK, r2.Code)
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
RemovePhotosFromAlbum(router, conf)
app, router, _ := NewApiTest()
RemovePhotosFromAlbum(router)
r := PerformRequestWithBody(app, "DELETE", "/api/v1/albums/"+uid+"/photos", `{"photos": ["pt9jtdre2lvl0y12", "pt9jtdre2lvl0y11"]}`)
val := gjson.Get(r.Body.String(), "message")
assert.Equal(t, "entries removed from album", val.String())
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("no items selected", func(t *testing.T) {
app, router, conf := NewApiTest()
RemovePhotosFromAlbum(router, conf)
app, router, _ := NewApiTest()
RemovePhotosFromAlbum(router)
r := PerformRequestWithBody(app, "DELETE", "/api/v1/albums/at9lxuqxpogaaba7/photos", `{"photos": []}`)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "No items selected", val.String())
assert.Equal(t, http.StatusBadRequest, r.Code)
})
t.Run("invalid request", func(t *testing.T) {
app, router, conf := NewApiTest()
RemovePhotosFromAlbum(router, conf)
app, router, _ := NewApiTest()
RemovePhotosFromAlbum(router)
r := PerformRequestWithBody(app, "DELETE", "/api/v1/albums/"+uid+"/photos", `{"photos": [123, "pt9jtdre2lvl0yxx"]}`)
assert.Equal(t, http.StatusBadRequest, r.Code)
})
t.Run("album not found", func(t *testing.T) {
app, router, conf := NewApiTest()
RemovePhotosFromAlbum(router, conf)
app, router, _ := NewApiTest()
RemovePhotosFromAlbum(router)
r := PerformRequestWithBody(app, "DELETE", "/api/v1/albums/xxx/photos", `{"photos": ["pt9jtdre2lvl0yxx"]}`)
assert.Equal(t, http.StatusNotFound, r.Code)
})
@@ -251,7 +251,7 @@ func TestDownloadAlbum(t *testing.T) {
t.Run("download not existing album", func(t *testing.T) {
app, router, conf := NewApiTest()
DownloadAlbum(router, conf)
DownloadAlbum(router)
r := PerformRequest(app, "GET", "/api/v1/albums/5678/dl?t="+conf.DownloadToken())
assert.Equal(t, http.StatusNotFound, r.Code)
@@ -259,7 +259,7 @@ func TestDownloadAlbum(t *testing.T) {
t.Run("download existing album", func(t *testing.T) {
app, router, conf := NewApiTest()
DownloadAlbum(router, conf)
DownloadAlbum(router)
r := PerformRequest(app, "GET", "/api/v1/albums/at9lxuqxpogaaba8/dl?t="+conf.DownloadToken())
assert.Equal(t, http.StatusOK, r.Code)
@@ -269,20 +269,20 @@ func TestDownloadAlbum(t *testing.T) {
func TestAlbumThumbnail(t *testing.T) {
t.Run("invalid type", func(t *testing.T) {
app, router, conf := NewApiTest()
AlbumThumbnail(router, conf)
AlbumThumbnail(router)
r := PerformRequest(app, "GET", "/api/v1/albums/at9lxuqxpogaaba7/t/"+conf.PreviewToken()+"/xxx")
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("album has no photo (because is not existing)", func(t *testing.T) {
app, router, conf := NewApiTest()
AlbumThumbnail(router, conf)
AlbumThumbnail(router)
r := PerformRequest(app, "GET", "/api/v1/albums/987-986435/t/"+conf.PreviewToken()+"/tile_500")
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("album: could not find original", func(t *testing.T) {
app, router, conf := NewApiTest()
AlbumThumbnail(router, conf)
AlbumThumbnail(router)
r := PerformRequest(app, "GET", "/api/v1/albums/at9lxuqxpogaaba8/t/"+conf.PreviewToken()+"/tile_500")
assert.Equal(t, http.StatusOK, r.Code)
})

View File

@@ -32,8 +32,8 @@ https://docs.photoprism.org/developer-guide/
package api
import (
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/service"
)
var log = event.Log
@@ -44,6 +44,8 @@ func logError(prefix string, err error) {
}
}
func UpdateClientConfig(conf *config.Config) {
func UpdateClientConfig() {
conf := service.Config()
event.Publish("config.updated", event.Data{"config": conf.UserConfig()})
}

View File

@@ -6,7 +6,7 @@ import (
"time"
"github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
@@ -17,9 +17,11 @@ import (
)
// POST /api/v1/batch/photos/archive
func BatchPhotosArchive(router *gin.RouterGroup, conf *config.Config) {
func BatchPhotosArchive(router *gin.RouterGroup) {
router.POST("/batch/photos/archive", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionDelete)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -58,7 +60,7 @@ func BatchPhotosArchive(router *gin.RouterGroup, conf *config.Config) {
elapsed := int(time.Since(start).Seconds())
UpdateClientConfig(conf)
UpdateClientConfig()
event.EntitiesArchived("photos", f.Photos)
@@ -67,9 +69,11 @@ func BatchPhotosArchive(router *gin.RouterGroup, conf *config.Config) {
}
// POST /api/v1/batch/photos/restore
func BatchPhotosRestore(router *gin.RouterGroup, conf *config.Config) {
func BatchPhotosRestore(router *gin.RouterGroup) {
router.POST("/batch/photos/restore", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionDelete)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -105,7 +109,7 @@ func BatchPhotosRestore(router *gin.RouterGroup, conf *config.Config) {
elapsed := int(time.Since(start).Seconds())
UpdateClientConfig(conf)
UpdateClientConfig()
event.EntitiesRestored("photos", f.Photos)
@@ -114,9 +118,11 @@ func BatchPhotosRestore(router *gin.RouterGroup, conf *config.Config) {
}
// POST /api/v1/batch/albums/delete
func BatchAlbumsDelete(router *gin.RouterGroup, conf *config.Config) {
func BatchAlbumsDelete(router *gin.RouterGroup) {
router.POST("/batch/albums/delete", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionDelete)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -139,7 +145,7 @@ func BatchAlbumsDelete(router *gin.RouterGroup, conf *config.Config) {
entity.Db().Where("album_uid IN (?)", f.Albums).Delete(&entity.Album{})
entity.Db().Where("album_uid IN (?)", f.Albums).Delete(&entity.PhotoAlbum{})
UpdateClientConfig(conf)
UpdateClientConfig()
event.EntitiesDeleted("albums", f.Albums)
@@ -148,9 +154,11 @@ func BatchAlbumsDelete(router *gin.RouterGroup, conf *config.Config) {
}
// POST /api/v1/batch/photos/private
func BatchPhotosPrivate(router *gin.RouterGroup, conf *config.Config) {
func BatchPhotosPrivate(router *gin.RouterGroup) {
router.POST("/batch/photos/private", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionPrivate)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -188,7 +196,7 @@ func BatchPhotosPrivate(router *gin.RouterGroup, conf *config.Config) {
event.EntitiesUpdated("photos", entities)
}
UpdateClientConfig(conf)
UpdateClientConfig()
elapsed := time.Since(start)
@@ -197,9 +205,11 @@ func BatchPhotosPrivate(router *gin.RouterGroup, conf *config.Config) {
}
// POST /api/v1/batch/labels/delete
func BatchLabelsDelete(router *gin.RouterGroup, conf *config.Config) {
func BatchLabelsDelete(router *gin.RouterGroup) {
router.POST("/batch/labels/delete", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourceLabels, acl.ActionDelete)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -231,7 +241,7 @@ func BatchLabelsDelete(router *gin.RouterGroup, conf *config.Config) {
logError("labels", label.Delete())
}
UpdateClientConfig(conf)
UpdateClientConfig()
event.EntitiesDeleted("labels", f.Labels)

View File

@@ -10,14 +10,14 @@ import (
func TestBatchPhotosArchive(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
GetPhoto(router, conf)
app, router, _ := NewApiTest()
GetPhoto(router)
r := PerformRequest(app, "GET", "/api/v1/photos/pt9jtdre2lvl0yh7")
assert.Equal(t, http.StatusOK, r.Code)
val := gjson.Get(r.Body.String(), "DeletedAt")
assert.Empty(t, val.String())
BatchPhotosArchive(router, conf)
BatchPhotosArchive(router)
r2 := PerformRequestWithBody(app, "POST", "/api/v1/batch/photos/archive", `{"photos": ["pt9jtdre2lvl0yh7", "pt9jtdre2lvl0ycc"]}`)
val2 := gjson.Get(r2.Body.String(), "message")
assert.Contains(t, val2.String(), "photos archived")
@@ -29,16 +29,16 @@ func TestBatchPhotosArchive(t *testing.T) {
assert.NotEmpty(t, val3.String())
})
t.Run("no items selected", func(t *testing.T) {
app, router, conf := NewApiTest()
BatchPhotosArchive(router, conf)
app, router, _ := NewApiTest()
BatchPhotosArchive(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/batch/photos/archive", `{"photos": []}`)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "No items selected", val.String())
assert.Equal(t, http.StatusBadRequest, r.Code)
})
t.Run("invalid request", func(t *testing.T) {
app, router, conf := NewApiTest()
BatchPhotosArchive(router, conf)
app, router, _ := NewApiTest()
BatchPhotosArchive(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/batch/photos/archive", `{"photos": 123}`)
assert.Equal(t, http.StatusBadRequest, r.Code)
})
@@ -46,21 +46,21 @@ func TestBatchPhotosArchive(t *testing.T) {
func TestBatchPhotosRestore(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
app, router, _ := NewApiTest()
BatchPhotosArchive(router, conf)
BatchPhotosArchive(router)
r2 := PerformRequestWithBody(app, "POST", "/api/v1/batch/photos/archive", `{"photos": ["pt9jtdre2lvl0yh8", "pt9jtdre2lvl0ycc"]}`)
val2 := gjson.Get(r2.Body.String(), "message")
assert.Contains(t, val2.String(), "photos archived")
assert.Equal(t, http.StatusOK, r2.Code)
GetPhoto(router, conf)
GetPhoto(router)
r3 := PerformRequest(app, "GET", "/api/v1/photos/pt9jtdre2lvl0yh8")
assert.Equal(t, http.StatusOK, r3.Code)
val3 := gjson.Get(r3.Body.String(), "DeletedAt")
assert.NotEmpty(t, val3.String())
BatchPhotosRestore(router, conf)
BatchPhotosRestore(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/batch/photos/restore", `{"photos": ["pt9jtdre2lvl0yh8", "pt9jtdre2lvl0ycc"]}`)
val := gjson.Get(r.Body.String(), "message")
assert.Contains(t, val.String(), "photos restored")
@@ -72,37 +72,37 @@ func TestBatchPhotosRestore(t *testing.T) {
assert.Empty(t, val4.String())
})
t.Run("no items selected", func(t *testing.T) {
app, router, conf := NewApiTest()
BatchPhotosRestore(router, conf)
app, router, _ := NewApiTest()
BatchPhotosRestore(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/batch/photos/restore", `{"photos": []}`)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "No items selected", val.String())
assert.Equal(t, http.StatusBadRequest, r.Code)
})
t.Run("invalid request", func(t *testing.T) {
app, router, conf := NewApiTest()
BatchPhotosRestore(router, conf)
app, router, _ := NewApiTest()
BatchPhotosRestore(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/batch/photos/restore", `{"photos": 123}`)
assert.Equal(t, http.StatusBadRequest, r.Code)
})
}
func TestBatchAlbumsDelete(t *testing.T) {
app, router, conf := NewApiTest()
CreateAlbum(router, conf)
app, router, _ := NewApiTest()
CreateAlbum(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"Title": "BatchDelete", "Description": "To be deleted", "Notes": "", "Favorite": true}`)
assert.Equal(t, http.StatusOK, r.Code)
uid := gjson.Get(r.Body.String(), "UID").String()
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
app, router, _ := NewApiTest()
GetAlbum(router, conf)
GetAlbum(router)
r := PerformRequest(app, "GET", "/api/v1/albums/"+uid)
val := gjson.Get(r.Body.String(), "Slug")
assert.Equal(t, "batchdelete", val.String())
BatchAlbumsDelete(router, conf)
BatchAlbumsDelete(router)
r2 := PerformRequestWithBody(app, "POST", "/api/v1/batch/albums/delete", fmt.Sprintf(`{"albums": ["%s", "pt9jtdre2lvl0ycc"]}`, uid))
val2 := gjson.Get(r2.Body.String(), "message")
assert.Contains(t, val2.String(), "albums deleted")
@@ -114,16 +114,16 @@ func TestBatchAlbumsDelete(t *testing.T) {
assert.Equal(t, http.StatusNotFound, r3.Code)
})
t.Run("no albums selected", func(t *testing.T) {
app, router, conf := NewApiTest()
BatchAlbumsDelete(router, conf)
app, router, _ := NewApiTest()
BatchAlbumsDelete(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/batch/albums/delete", `{"albums": []}`)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "No albums selected", val.String())
assert.Equal(t, http.StatusBadRequest, r.Code)
})
t.Run("invalid request", func(t *testing.T) {
app, router, conf := NewApiTest()
BatchAlbumsDelete(router, conf)
app, router, _ := NewApiTest()
BatchAlbumsDelete(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/batch/albums/delete", `{"albums": 123}`)
assert.Equal(t, http.StatusBadRequest, r.Code)
})
@@ -131,14 +131,14 @@ func TestBatchAlbumsDelete(t *testing.T) {
func TestBatchPhotosPrivate(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
GetPhoto(router, conf)
app, router, _ := NewApiTest()
GetPhoto(router)
r := PerformRequest(app, "GET", "/api/v1/photos/pt9jtdre2lvl0yh8")
assert.Equal(t, http.StatusOK, r.Code)
val := gjson.Get(r.Body.String(), "Private")
assert.Equal(t, "false", val.String())
BatchPhotosPrivate(router, conf)
BatchPhotosPrivate(router)
r2 := PerformRequestWithBody(app, "POST", "/api/v1/batch/photos/private", `{"photos": ["pt9jtdre2lvl0yh8", "pt9jtdre2lvl0ycc"]}`)
val2 := gjson.Get(r2.Body.String(), "message")
assert.Contains(t, val2.String(), "photos marked as private")
@@ -150,16 +150,16 @@ func TestBatchPhotosPrivate(t *testing.T) {
assert.Equal(t, "true", val3.String())
})
t.Run("no items selected", func(t *testing.T) {
app, router, conf := NewApiTest()
BatchPhotosPrivate(router, conf)
app, router, _ := NewApiTest()
BatchPhotosPrivate(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/batch/photos/private", `{"photos": []}`)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "No items selected", val.String())
assert.Equal(t, http.StatusBadRequest, r.Code)
})
t.Run("invalid request", func(t *testing.T) {
app, router, conf := NewApiTest()
BatchPhotosPrivate(router, conf)
app, router, _ := NewApiTest()
BatchPhotosPrivate(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/batch/photos/private", `{"photos": 123}`)
assert.Equal(t, http.StatusBadRequest, r.Code)
})
@@ -167,13 +167,13 @@ func TestBatchPhotosPrivate(t *testing.T) {
func TestBatchLabelsDelete(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
GetLabels(router, conf)
app, router, _ := NewApiTest()
GetLabels(router)
r := PerformRequest(app, "GET", "/api/v1/labels?count=15")
val := gjson.Get(r.Body.String(), `#(Name=="BatchDelete").Slug`)
assert.Equal(t, val.String(), "batchdelete")
BatchLabelsDelete(router, conf)
BatchLabelsDelete(router)
r2 := PerformRequestWithBody(app, "POST", "/api/v1/batch/labels/delete", fmt.Sprintf(`{"labels": ["lt9k3pw1wowuy3c6", "pt9jtdre2lvl0ycc"]}`))
val2 := gjson.Get(r2.Body.String(), "message")
assert.Contains(t, val2.String(), "labels deleted")
@@ -184,16 +184,16 @@ func TestBatchLabelsDelete(t *testing.T) {
assert.Equal(t, val3.String(), "")
})
t.Run("no labels selected", func(t *testing.T) {
app, router, conf := NewApiTest()
BatchLabelsDelete(router, conf)
app, router, _ := NewApiTest()
BatchLabelsDelete(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/batch/labels/delete", `{"labels": []}`)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "No labels selected", val.String())
assert.Equal(t, http.StatusBadRequest, r.Code)
})
t.Run("invalid request", func(t *testing.T) {
app, router, conf := NewApiTest()
BatchLabelsDelete(router, conf)
app, router, _ := NewApiTest()
BatchLabelsDelete(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/batch/labels/delete", `{"labels": 123}`)
assert.Equal(t, http.StatusBadRequest, r.Code)
})

View File

@@ -4,27 +4,25 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/service"
)
// GET /api/v1/config
func GetConfig(router *gin.RouterGroup, conf *config.Config) {
func GetConfig(router *gin.RouterGroup) {
router.GET("/config", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourceConfig, acl.ActionRead)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
sess := Session(SessionToken(c), conf)
conf := service.Config()
if sess == nil {
c.JSON(http.StatusNotFound, ErrSessionNotFound)
return
}
if sess.User.Guest() {
if s.User.Guest() {
c.JSON(http.StatusOK, conf.GuestConfig())
} else if sess.User.User() {
} else if s.User.Registered() {
c.JSON(http.StatusOK, conf.UserConfig())
} else {
c.JSON(http.StatusOK, conf.PublicConfig())

View File

@@ -4,7 +4,6 @@ import (
"fmt"
"net/http"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/fs"
@@ -21,9 +20,9 @@ import (
//
// Parameters:
// hash: string The file hash as returned by the search API
func GetDownload(router *gin.RouterGroup, conf *config.Config) {
func GetDownload(router *gin.RouterGroup) {
router.GET("/dl/:hash", func(c *gin.Context) {
if InvalidDownloadToken(c, conf) {
if InvalidDownloadToken(c) {
c.Data(http.StatusForbidden, "image/svg+xml", brokenIconSvg)
return
}

View File

@@ -12,7 +12,7 @@ func TestGetDownload(t *testing.T) {
t.Run("download not existing file", func(t *testing.T) {
app, router, conf := NewApiTest()
GetDownload(router, conf)
GetDownload(router)
r := PerformRequest(app, "GET", "/api/v1/dl/123xxx?t="+conf.DownloadToken())
val := gjson.Get(r.Body.String(), "error")
@@ -21,7 +21,7 @@ func TestGetDownload(t *testing.T) {
})
t.Run("could not find original", func(t *testing.T) {
app, router, conf := NewApiTest()
GetDownload(router, conf)
GetDownload(router)
r := PerformRequest(app, "GET", "/api/v1/dl/3cad9168fa6acc5c5c2965ddf6ec465ca42fd818?t="+conf.DownloadToken())
assert.Equal(t, http.StatusNotFound, r.Code)
})

View File

@@ -24,4 +24,5 @@ var (
ErrDeleteFailed = gin.H{"code": http.StatusInternalServerError, "error": "Changes could not be saved"}
ErrFormInvalid = gin.H{"code": http.StatusBadRequest, "error": "Changes could not be saved"}
ErrFeatureDisabled = gin.H{"code": http.StatusForbidden, "error": "Feature disabled"}
ErrNotFound = gin.H{"code": http.StatusNotFound, "error": "Not found"}
)

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/query"
)
@@ -12,9 +12,11 @@ import (
//
// Parameters:
// hash: string SHA-1 hash of the file
func GetFile(router *gin.RouterGroup, conf *config.Config) {
func GetFile(router *gin.RouterGroup) {
router.GET("/files/:hash", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourceFiles, acl.ActionRead)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}

View File

@@ -11,8 +11,8 @@ import (
func TestGetFile(t *testing.T) {
t.Run("search for existing file", func(t *testing.T) {
app, router, ctx := NewApiTest()
GetFile(router, ctx)
app, router, _ := NewApiTest()
GetFile(router)
r := PerformRequest(app, "GET", "/api/v1/files/2cad9168fa6acc5c5c2965ddf6ec465ca42fd818")
assert.Equal(t, http.StatusOK, r.Code)
@@ -20,8 +20,8 @@ func TestGetFile(t *testing.T) {
assert.Equal(t, "exampleFileName.jpg", val.String())
})
t.Run("search for not existing file", func(t *testing.T) {
app, router, ctx := NewApiTest()
GetFile(router, ctx)
app, router, _ := NewApiTest()
GetFile(router)
r := PerformRequest(app, "GET", "/api/v1/files/111")
assert.Equal(t, http.StatusNotFound, r.Code)
})

View File

@@ -10,7 +10,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/query"
@@ -27,9 +27,11 @@ type FoldersResponse struct {
}
// GetFolders is a reusable request handler for directory listings (GET /api/v1/folders/*).
func GetFolders(router *gin.RouterGroup, conf *config.Config, urlPath, rootName, rootPath string) {
func GetFolders(router *gin.RouterGroup, urlPath, rootName, rootPath string) {
handler := func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourceFolders, acl.ActionSearch)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -104,11 +106,13 @@ func GetFolders(router *gin.RouterGroup, conf *config.Config, urlPath, rootName,
}
// GET /api/v1/folders/originals
func GetFoldersOriginals(router *gin.RouterGroup, conf *config.Config) {
GetFolders(router, conf, "originals", entity.RootOriginals, conf.OriginalsPath())
func GetFoldersOriginals(router *gin.RouterGroup) {
conf := service.Config()
GetFolders(router, "originals", entity.RootOriginals, conf.OriginalsPath())
}
// GET /api/v1/folders/import
func GetFoldersImport(router *gin.RouterGroup, conf *config.Config) {
GetFolders(router, conf, "import", entity.RootImport, conf.ImportPath())
func GetFoldersImport(router *gin.RouterGroup) {
conf := service.Config()
GetFolders(router, "import", entity.RootImport, conf.ImportPath())
}

View File

@@ -19,7 +19,7 @@ func TestGetFoldersOriginals(t *testing.T) {
t.Fatal(err)
}
GetFoldersOriginals(router, conf)
GetFoldersOriginals(router)
r := PerformRequest(app, "GET", "/api/v1/folders/originals")
// t.Logf("RESPONSE: %s", r.Body.Bytes())
@@ -61,7 +61,7 @@ func TestGetFoldersOriginals(t *testing.T) {
if err != nil {
t.Fatal(err)
}
GetFoldersOriginals(router, conf)
GetFoldersOriginals(router)
r := PerformRequest(app, "GET", "/api/v1/folders/originals?recursive=true")
// t.Logf("RESPONSE: %s", r.Body.Bytes())
@@ -102,7 +102,7 @@ func TestGetFoldersImport(t *testing.T) {
t.Fatal(err)
}
GetFoldersImport(router, conf)
GetFoldersImport(router)
r := PerformRequest(app, "GET", "/api/v1/folders/import")
// t.Logf("RESPONSE: %s", r.Body.Bytes())
@@ -146,7 +146,7 @@ func TestGetFoldersImport(t *testing.T) {
t.Fatal(err)
}
GetFoldersImport(router, conf)
GetFoldersImport(router)
r := PerformRequest(app, "GET", "/api/v1/folders/import?recursive=true")
var resp FoldersResponse

View File

@@ -3,7 +3,7 @@ package api
import (
"net/http"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/txt"
@@ -16,9 +16,11 @@ import (
)
// GET /api/v1/geo
func GetGeo(router *gin.RouterGroup, conf *config.Config) {
func GetGeo(router *gin.RouterGroup) {
router.GET("/geo", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionSearch)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}

View File

@@ -9,9 +9,9 @@ import (
func TestGetGeo(t *testing.T) {
t.Run("get geo", func(t *testing.T) {
app, router, conf := NewApiTest()
app, router, _ := NewApiTest()
GetGeo(router, conf)
GetGeo(router)
result := PerformRequest(app, "GET", "/api/v1/geo")
assert.Equal(t, http.StatusOK, result.Code)

View File

@@ -9,7 +9,7 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/photoprism"
@@ -19,13 +19,17 @@ import (
)
// POST /api/v1/import*
func StartImport(router *gin.RouterGroup, conf *config.Config) {
func StartImport(router *gin.RouterGroup) {
router.POST("/import/*path", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionImport)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
conf := service.Config()
if conf.ReadOnly() || !conf.Settings().Features.Import {
c.AbortWithStatusJSON(http.StatusForbidden, ErrFeatureDisabled)
return
@@ -96,20 +100,24 @@ func StartImport(router *gin.RouterGroup, conf *config.Config) {
PublishAlbumEvent(EntityUpdated, uid, c)
}
UpdateClientConfig(conf)
UpdateClientConfig()
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("import completed in %d s", elapsed)})
})
}
// DELETE /api/v1/import
func CancelImport(router *gin.RouterGroup, conf *config.Config) {
func CancelImport(router *gin.RouterGroup) {
router.DELETE("/import", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionImport)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
conf := service.Config()
if conf.ReadOnly() || !conf.Settings().Features.Import {
c.AbortWithStatusJSON(http.StatusForbidden, ErrFeatureDisabled)
return

View File

@@ -10,8 +10,8 @@ import (
func TestCancelImport(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
CancelImport(router, conf)
app, router, _ := NewApiTest()
CancelImport(router)
r := PerformRequest(app, "DELETE", "/api/v1/import")
val := gjson.Get(r.Body.String(), "message")
assert.Equal(t, "import canceled", val.String())

View File

@@ -7,7 +7,7 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/photoprism"
@@ -16,13 +16,17 @@ import (
)
// POST /api/v1/index
func StartIndexing(router *gin.RouterGroup, conf *config.Config) {
func StartIndexing(router *gin.RouterGroup) {
router.POST("/index", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
conf := service.Config()
if !conf.Settings().Features.Library {
c.AbortWithStatusJSON(http.StatusForbidden, ErrFeatureDisabled)
return
@@ -80,20 +84,24 @@ func StartIndexing(router *gin.RouterGroup, conf *config.Config) {
event.Success(fmt.Sprintf("indexing completed in %d s", elapsed))
event.Publish("index.completed", event.Data{"path": path, "seconds": elapsed})
UpdateClientConfig(conf)
UpdateClientConfig()
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("indexing completed in %d s", elapsed)})
})
}
// DELETE /api/v1/index
func CancelIndexing(router *gin.RouterGroup, conf *config.Config) {
func CancelIndexing(router *gin.RouterGroup) {
router.DELETE("/index", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
conf := service.Config()
if !conf.Settings().Features.Library {
c.AbortWithStatusJSON(http.StatusForbidden, ErrFeatureDisabled)
return

View File

@@ -9,8 +9,8 @@ import (
func TestCancelIndex(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
CancelIndexing(router, conf)
app, router, _ := NewApiTest()
CancelIndexing(router)
r := PerformRequest(app, "DELETE", "/api/v1/index")
val := gjson.Get(r.Body.String(), "message")
assert.Equal(t, "indexing canceled", val.String())

View File

@@ -10,7 +10,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
@@ -23,9 +23,11 @@ import (
)
// GET /api/v1/labels
func GetLabels(router *gin.RouterGroup, conf *config.Config) {
func GetLabels(router *gin.RouterGroup) {
router.GET("/labels", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourceLabels, acl.ActionSearch)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -55,9 +57,11 @@ func GetLabels(router *gin.RouterGroup, conf *config.Config) {
}
// PUT /api/v1/labels/:uid
func UpdateLabel(router *gin.RouterGroup, conf *config.Config) {
func UpdateLabel(router *gin.RouterGroup) {
router.PUT("/labels/:uid", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourceLabels, acl.ActionUpdate)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -92,9 +96,11 @@ func UpdateLabel(router *gin.RouterGroup, conf *config.Config) {
//
// Parameters:
// uid: string Label UID
func LikeLabel(router *gin.RouterGroup, conf *config.Config) {
func LikeLabel(router *gin.RouterGroup) {
router.POST("/labels/:uid/like", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourceLabels, acl.ActionUpdate)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -128,9 +134,11 @@ func LikeLabel(router *gin.RouterGroup, conf *config.Config) {
//
// Parameters:
// uid: string Label UID
func DislikeLabel(router *gin.RouterGroup, conf *config.Config) {
func DislikeLabel(router *gin.RouterGroup) {
router.DELETE("/labels/:uid/like", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourceLabels, acl.ActionUpdate)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -165,14 +173,15 @@ func DislikeLabel(router *gin.RouterGroup, conf *config.Config) {
// Parameters:
// uid: string Label UID
// type: string Thumbnail type, see photoprism.ThumbnailTypes
func LabelThumbnail(router *gin.RouterGroup, conf *config.Config) {
func LabelThumbnail(router *gin.RouterGroup) {
router.GET("/labels/:uid/t/:token/:type", func(c *gin.Context) {
if InvalidToken(c, conf) {
if InvalidToken(c) {
c.Data(http.StatusForbidden, "image/svg+xml", labelIconSvg)
return
}
start := time.Now()
conf := service.Config()
typeName := c.Param("type")
uid := c.Param("uid")

View File

@@ -11,16 +11,16 @@ import (
func TestGetLabels(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
app, router, ctx := NewApiTest()
GetLabels(router, ctx)
app, router, _ := NewApiTest()
GetLabels(router)
r := PerformRequest(app, "GET", "/api/v1/labels?count=15")
count := gjson.Get(r.Body.String(), "#")
assert.LessOrEqual(t, int64(4), count.Int())
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("invalid request", func(t *testing.T) {
app, router, ctx := NewApiTest()
GetLabels(router, ctx)
app, router, _ := NewApiTest()
GetLabels(router)
r := PerformRequest(app, "GET", "/api/v1/labels?xxx=15")
assert.Equal(t, http.StatusBadRequest, r.Code)
})
@@ -28,8 +28,8 @@ func TestGetLabels(t *testing.T) {
func TestUpdateLabel(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
UpdateLabel(router, conf)
app, router, _ := NewApiTest()
UpdateLabel(router)
r := PerformRequestWithBody(app, "PUT", "/api/v1/labels/lt9k3pw1wowuy3c7", `{"Name": "Updated01", "Priority": 2}`)
val := gjson.Get(r.Body.String(), "Name")
assert.Equal(t, "Updated01", val.String())
@@ -39,15 +39,15 @@ func TestUpdateLabel(t *testing.T) {
})
t.Run("invalid request", func(t *testing.T) {
app, router, conf := NewApiTest()
UpdateLabel(router, conf)
app, router, _ := NewApiTest()
UpdateLabel(router)
r := PerformRequestWithBody(app, "PUT", "/api/v1/labels/lt9k3pw1wowuy3c7", `{"Name": 123, "Priority": 4, "Uncertainty": 80}`)
assert.Equal(t, http.StatusBadRequest, r.Code)
})
t.Run("not found", func(t *testing.T) {
app, router, conf := NewApiTest()
UpdateLabel(router, conf)
app, router, _ := NewApiTest()
UpdateLabel(router)
r := PerformRequestWithBody(app, "PUT", "/api/v1/labels/xxx", `{"Name": "Updated01", "Priority": 4, "Uncertainty": 80}`)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "Label not found", val.String())
@@ -57,19 +57,19 @@ func TestUpdateLabel(t *testing.T) {
func TestLikeLabel(t *testing.T) {
t.Run("like not existing label", func(t *testing.T) {
app, router, ctx := NewApiTest()
LikeLabel(router, ctx)
app, router, _ := NewApiTest()
LikeLabel(router)
r := PerformRequest(app, "POST", "/api/v1/labels/8775789/like")
assert.Equal(t, http.StatusNotFound, r.Code)
})
t.Run("like existing label", func(t *testing.T) {
app, router, ctx := NewApiTest()
GetLabels(router, ctx)
app, router, _ := NewApiTest()
GetLabels(router)
r2 := PerformRequest(app, "GET", "/api/v1/labels?count=1&q=likeLabel")
t.Log(r2.Body.String())
val := gjson.Get(r2.Body.String(), `#(Slug=="likeLabel").Favorite`)
assert.Equal(t, "false", val.String())
LikeLabel(router, ctx)
LikeLabel(router)
r := PerformRequest(app, "POST", "/api/v1/labels/lt9k3pw1wowuy3c9/like")
t.Log(r.Body.String())
assert.Equal(t, http.StatusOK, r.Code)
@@ -83,22 +83,22 @@ func TestLikeLabel(t *testing.T) {
func TestDislikeLabel(t *testing.T) {
t.Run("dislike not existing label", func(t *testing.T) {
app, router, ctx := NewApiTest()
app, router, _ := NewApiTest()
DislikeLabel(router, ctx)
DislikeLabel(router)
r := PerformRequest(app, "DELETE", "/api/v1/labels/5678/like")
assert.Equal(t, http.StatusNotFound, r.Code)
})
t.Run("dislike existing label", func(t *testing.T) {
app, router, ctx := NewApiTest()
GetLabels(router, ctx)
app, router, _ := NewApiTest()
GetLabels(router)
r2 := PerformRequest(app, "GET", "/api/v1/labels?count=3&q=landscape")
t.Logf("HTTP BODY: %s", r2.Body.String())
val := gjson.Get(r2.Body.String(), `#(Slug=="landscape").Favorite`)
assert.Equal(t, "true", val.String())
DislikeLabel(router, ctx)
DislikeLabel(router)
r := PerformRequest(app, "DELETE", "/api/v1/labels/lt9k3pw1wowuy3c2/like")
assert.Equal(t, http.StatusOK, r.Code)
@@ -112,20 +112,20 @@ func TestDislikeLabel(t *testing.T) {
func TestLabelThumbnail(t *testing.T) {
t.Run("invalid type", func(t *testing.T) {
app, router, conf := NewApiTest()
LabelThumbnail(router, conf)
LabelThumbnail(router)
r := PerformRequest(app, "GET", "/api/v1/labels/lt9k3pw1wowuy3c2/t/"+conf.PreviewToken()+"/xxx")
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("invalid label", func(t *testing.T) {
app, router, conf := NewApiTest()
LabelThumbnail(router, conf)
LabelThumbnail(router)
r := PerformRequest(app, "GET", "/api/v1/labels/xxx/t/"+conf.PreviewToken()+"/tile_500")
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("could not find original", func(t *testing.T) {
app, router, conf := NewApiTest()
LabelThumbnail(router, conf)
LabelThumbnail(router)
r := PerformRequest(app, "GET", "/api/v1/labels/lt9k3pw1wowuy3c3/t/"+conf.PreviewToken()+"/tile_500")
assert.Equal(t, http.StatusOK, r.Code)
})

View File

@@ -5,7 +5,7 @@ import (
"strings"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
@@ -14,8 +14,10 @@ import (
)
// PUT /api/v1/:entity/:uid/links/:link
func UpdateLink(c *gin.Context, conf *config.Config) {
if Unauthorized(c, conf) {
func UpdateLink(c *gin.Context) {
s := Auth(SessionID(c), acl.ResourceLinks, acl.ActionUpdate)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -53,8 +55,10 @@ func UpdateLink(c *gin.Context, conf *config.Config) {
}
// DELETE /api/v1/:entity/:uid/links/:link
func DeleteLink(c *gin.Context, conf *config.Config) {
if Unauthorized(c, conf) {
func DeleteLink(c *gin.Context) {
s := Auth(SessionID(c), acl.ResourceLinks, acl.ActionDelete)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -72,8 +76,10 @@ func DeleteLink(c *gin.Context, conf *config.Config) {
}
// CreateLink returns a new link entity initialized with request data
func CreateLink(c *gin.Context, conf *config.Config) {
if Unauthorized(c, conf) {
func CreateLink(c *gin.Context) {
s := Auth(SessionID(c), acl.ResourceLinks, acl.ActionCreate)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -109,33 +115,33 @@ func CreateLink(c *gin.Context, conf *config.Config) {
}
// POST /api/v1/albums/:uid/links
func CreateAlbumLink(router *gin.RouterGroup, conf *config.Config) {
func CreateAlbumLink(router *gin.RouterGroup) {
router.POST("/albums/:uid/links", func(c *gin.Context) {
if _, err := query.AlbumByUID(c.Param("uid")); err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
return
}
CreateLink(c, conf)
CreateLink(c)
})
}
// PUT /api/v1/albums/:uid/links/:link
func UpdateAlbumLink(router *gin.RouterGroup, conf *config.Config) {
func UpdateAlbumLink(router *gin.RouterGroup) {
router.PUT("/albums/:uid/links/:link", func(c *gin.Context) {
UpdateLink(c, conf)
UpdateLink(c)
})
}
// DELETE /api/v1/albums/:uid/links/:link
func DeleteAlbumLink(router *gin.RouterGroup, conf *config.Config) {
func DeleteAlbumLink(router *gin.RouterGroup) {
router.DELETE("/albums/:uid/links/:link", func(c *gin.Context) {
DeleteLink(c, conf)
DeleteLink(c)
})
}
// GET /api/v1/albums/:uid/links
func GetAlbumLinks(router *gin.RouterGroup, conf *config.Config) {
func GetAlbumLinks(router *gin.RouterGroup) {
router.GET("/albums/:uid/links", func(c *gin.Context) {
m, err := query.AlbumByUID(c.Param("uid"))
@@ -149,33 +155,33 @@ func GetAlbumLinks(router *gin.RouterGroup, conf *config.Config) {
}
// POST /api/v1/photos/:uid/links
func CreatePhotoLink(router *gin.RouterGroup, conf *config.Config) {
func CreatePhotoLink(router *gin.RouterGroup) {
router.POST("/photos/:uid/links", func(c *gin.Context) {
if _, err := query.PhotoByUID(c.Param("uid")); err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
return
}
CreateLink(c, conf)
CreateLink(c)
})
}
// PUT /api/v1/photos/:uid/links/:link
func UpdatePhotoLink(router *gin.RouterGroup, conf *config.Config) {
func UpdatePhotoLink(router *gin.RouterGroup) {
router.PUT("/photos/:uid/links/:link", func(c *gin.Context) {
UpdateLink(c, conf)
UpdateLink(c)
})
}
// DELETE /api/v1/photos/:uid/links/:link
func DeletePhotoLink(router *gin.RouterGroup, conf *config.Config) {
func DeletePhotoLink(router *gin.RouterGroup) {
router.DELETE("/photos/:uid/links/:link", func(c *gin.Context) {
DeleteLink(c, conf)
DeleteLink(c)
})
}
// GET /api/v1/photos/:uid/links
func GetPhotoLinks(router *gin.RouterGroup, conf *config.Config) {
func GetPhotoLinks(router *gin.RouterGroup) {
router.GET("/photos/:uid/links", func(c *gin.Context) {
m, err := query.PhotoByUID(c.Param("uid"))
@@ -189,33 +195,33 @@ func GetPhotoLinks(router *gin.RouterGroup, conf *config.Config) {
}
// POST /api/v1/labels/:uid/links
func CreateLabelLink(router *gin.RouterGroup, conf *config.Config) {
func CreateLabelLink(router *gin.RouterGroup) {
router.POST("/labels/:uid/links", func(c *gin.Context) {
if _, err := query.LabelByUID(c.Param("uid")); err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrLabelNotFound)
return
}
CreateLink(c, conf)
CreateLink(c)
})
}
// PUT /api/v1/labels/:uid/links/:link
func UpdateLabelLink(router *gin.RouterGroup, conf *config.Config) {
func UpdateLabelLink(router *gin.RouterGroup) {
router.PUT("/labels/:uid/links/:link", func(c *gin.Context) {
UpdateLink(c, conf)
UpdateLink(c)
})
}
// DELETE /api/v1/labels/:uid/links/:link
func DeleteLabelLink(router *gin.RouterGroup, conf *config.Config) {
func DeleteLabelLink(router *gin.RouterGroup) {
router.DELETE("/labels/:uid/links/:link", func(c *gin.Context) {
DeleteLink(c, conf)
DeleteLink(c)
})
}
// GET /api/v1/labels/:uid/links
func GetLabelLinks(router *gin.RouterGroup, conf *config.Config) {
func GetLabelLinks(router *gin.RouterGroup) {
router.GET("/labels/:uid/links", func(c *gin.Context) {
m, err := query.LabelByUID(c.Param("uid"))

View File

@@ -12,11 +12,11 @@ import (
func TestLinkAlbum(t *testing.T) {
t.Run("create share link", func(t *testing.T) {
app, router, ctx := NewApiTest()
app, router, _ := NewApiTest()
var link entity.Link
CreateAlbumLink(router, ctx)
CreateAlbumLink(router)
resp := PerformRequestWithBody(app, "POST", "/api/v1/albums/at9lxuqxpogaaba7/links", `{"Password": "foobar", "ShareExpires": 0, "CanEdit": true}`)
@@ -37,8 +37,8 @@ func TestLinkAlbum(t *testing.T) {
assert.True(t, link.CanEdit)
})
t.Run("album does not exist", func(t *testing.T) {
app, router, ctx := NewApiTest()
CreateAlbumLink(router, ctx)
app, router, _ := NewApiTest()
CreateAlbumLink(router)
resp := PerformRequestWithBody(app, "POST", "/api/v1/albums/xxx/links", `{"Password": "foobar", "ShareExpires": 0, "CanEdit": true}`)
if resp.Code != http.StatusNotFound {
@@ -49,9 +49,9 @@ func TestLinkAlbum(t *testing.T) {
assert.Equal(t, "Album not found", val.String())
})
t.Run("invalid request", func(t *testing.T) {
app, router, ctx := NewApiTest()
app, router, _ := NewApiTest()
CreateAlbumLink(router, ctx)
CreateAlbumLink(router)
resp := PerformRequestWithBody(app, "POST", "/api/v1/albums/at9lxuqxpogaaba7/links", `{"Password": "foobar", "ShareExpires": "abc", "CanEdit": true}`)
@@ -63,11 +63,11 @@ func TestLinkAlbum(t *testing.T) {
func TestLinkPhoto(t *testing.T) {
t.Run("create share link", func(t *testing.T) {
app, router, ctx := NewApiTest()
app, router, _ := NewApiTest()
var link entity.Link
CreatePhotoLink(router, ctx)
CreatePhotoLink(router)
resp := PerformRequestWithBody(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh7/links", `{"Password": "foobar", "ShareExpires": 0, "CanEdit": true}`)
assert.Equal(t, http.StatusOK, resp.Code)
@@ -84,9 +84,9 @@ func TestLinkPhoto(t *testing.T) {
assert.True(t, link.CanEdit)
})
t.Run("photo not found", func(t *testing.T) {
app, router, ctx := NewApiTest()
app, router, _ := NewApiTest()
CreatePhotoLink(router, ctx)
CreatePhotoLink(router)
resp := PerformRequestWithBody(app, "POST", "/api/v1/photos/xxx/link", `{"Password": "foobar", "ShareExpires": 0, "CanEdit": true}`)
@@ -95,8 +95,8 @@ func TestLinkPhoto(t *testing.T) {
}
})
t.Run("invalid request", func(t *testing.T) {
app, router, ctx := NewApiTest()
CreatePhotoLink(router, ctx)
app, router, _ := NewApiTest()
CreatePhotoLink(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh7/links", `{"xxx": 123, "ShareExpires": "abc", "CanEdit": "xxx"}`)
assert.Equal(t, http.StatusBadRequest, r.Code)
})
@@ -104,11 +104,11 @@ func TestLinkPhoto(t *testing.T) {
func TestLinkLabel(t *testing.T) {
t.Run("create share link", func(t *testing.T) {
app, router, ctx := NewApiTest()
app, router, _ := NewApiTest()
var link entity.Link
CreateLabelLink(router, ctx)
CreateLabelLink(router)
resp := PerformRequestWithBody(app, "POST", "/api/v1/labels/lt9k3pw1wowuy3c2/links", `{"Password": "foobar", "ShareExpires": 0, "CanEdit": true}`)
assert.Equal(t, http.StatusOK, resp.Code)
@@ -125,8 +125,8 @@ func TestLinkLabel(t *testing.T) {
assert.True(t, link.CanEdit)
})
t.Run("label not found", func(t *testing.T) {
app, router, ctx := NewApiTest()
CreateLabelLink(router, ctx)
app, router, _ := NewApiTest()
CreateLabelLink(router)
resp := PerformRequestWithBody(app, "POST", "/api/v1/labels/xxx/links", `{"Password": "foobar", "ShareExpires": 0, "CanEdit": true}`)
if resp.Code != http.StatusNotFound {
@@ -134,8 +134,8 @@ func TestLinkLabel(t *testing.T) {
}
})
t.Run("invalid request", func(t *testing.T) {
app, router, ctx := NewApiTest()
CreateLabelLink(router, ctx)
app, router, _ := NewApiTest()
CreateLabelLink(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/labels/lt9k3pw1wowuy3c2/links", `{"xxx": 123, "ShareExpires": "abc", "CanEdit": "xxx"}`)
assert.Equal(t, http.StatusBadRequest, r.Code)
})

View File

@@ -3,7 +3,7 @@ package api
import (
"net/http"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/txt"
@@ -11,9 +11,11 @@ import (
)
// GET /api/v1/moments/time
func GetMomentsTime(router *gin.RouterGroup, conf *config.Config) {
func GetMomentsTime(router *gin.RouterGroup) {
router.GET("/moments/time", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionExport)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}

View File

@@ -10,9 +10,9 @@ import (
func TestGetMomentsTime(t *testing.T) {
t.Run("get moments time", func(t *testing.T) {
app, router, conf := NewApiTest()
app, router, _ := NewApiTest()
GetMomentsTime(router, conf)
GetMomentsTime(router)
r := PerformRequest(app, "GET", "/api/v1/moments/time")
val := gjson.Get(r.Body.String(), `#(Year=="2790").Count`)

View File

@@ -5,18 +5,21 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config"
"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/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/txt"
)
// SavePhotoAsYaml saves photo data as YAML file.
func SavePhotoAsYaml(p entity.Photo, conf *config.Config) {
func SavePhotoAsYaml(p entity.Photo) {
conf := service.Config()
// Write YAML sidecar file (optional).
if conf.SidecarYaml() {
yamlFile := p.YamlFileName(conf.OriginalsPath(), conf.SidecarPath())
@@ -33,9 +36,11 @@ func SavePhotoAsYaml(p entity.Photo, conf *config.Config) {
//
// Parameters:
// uid: string PhotoUID as returned by the API
func GetPhoto(router *gin.RouterGroup, conf *config.Config) {
func GetPhoto(router *gin.RouterGroup) {
router.GET("/photos/:uid", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionRead)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -52,13 +57,16 @@ func GetPhoto(router *gin.RouterGroup, conf *config.Config) {
}
// PUT /api/v1/photos/:uid
func UpdatePhoto(router *gin.RouterGroup, conf *config.Config) {
func UpdatePhoto(router *gin.RouterGroup) {
router.PUT("/photos/:uid", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
conf := service.Config()
uid := c.Param("uid")
m, err := query.PhotoByUID(uid)
@@ -102,7 +110,7 @@ func UpdatePhoto(router *gin.RouterGroup, conf *config.Config) {
return
}
SavePhotoAsYaml(p, conf)
SavePhotoAsYaml(p)
c.JSON(http.StatusOK, p)
})
@@ -112,9 +120,9 @@ func UpdatePhoto(router *gin.RouterGroup, conf *config.Config) {
//
// Parameters:
// uid: string PhotoUID as returned by the API
func GetPhotoDownload(router *gin.RouterGroup, conf *config.Config) {
func GetPhotoDownload(router *gin.RouterGroup) {
router.GET("/photos/:uid/dl", func(c *gin.Context) {
if InvalidDownloadToken(c, conf) {
if InvalidDownloadToken(c) {
c.Data(http.StatusForbidden, "image/svg+xml", brokenIconSvg)
return
}
@@ -150,9 +158,11 @@ func GetPhotoDownload(router *gin.RouterGroup, conf *config.Config) {
//
// Parameters:
// uid: string PhotoUID as returned by the API
func GetPhotoYaml(router *gin.RouterGroup, conf *config.Config) {
func GetPhotoYaml(router *gin.RouterGroup) {
router.GET("/photos/:uid/yaml", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionExport)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -183,9 +193,11 @@ func GetPhotoYaml(router *gin.RouterGroup, conf *config.Config) {
//
// Parameters:
// uid: string PhotoUID as returned by the API
func ApprovePhoto(router *gin.RouterGroup, conf *config.Config) {
func ApprovePhoto(router *gin.RouterGroup) {
router.POST("/photos/:uid/approve", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -204,7 +216,7 @@ func ApprovePhoto(router *gin.RouterGroup, conf *config.Config) {
return
}
SavePhotoAsYaml(m, conf)
SavePhotoAsYaml(m)
PublishPhotoEvent(EntityUpdated, id, c)
@@ -216,9 +228,11 @@ func ApprovePhoto(router *gin.RouterGroup, conf *config.Config) {
//
// Parameters:
// uid: string PhotoUID as returned by the API
func LikePhoto(router *gin.RouterGroup, conf *config.Config) {
func LikePhoto(router *gin.RouterGroup) {
router.POST("/photos/:uid/like", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionLike)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -237,7 +251,7 @@ func LikePhoto(router *gin.RouterGroup, conf *config.Config) {
return
}
SavePhotoAsYaml(m, conf)
SavePhotoAsYaml(m)
PublishPhotoEvent(EntityUpdated, id, c)
@@ -249,9 +263,11 @@ func LikePhoto(router *gin.RouterGroup, conf *config.Config) {
//
// Parameters:
// uid: string PhotoUID as returned by the API
func DislikePhoto(router *gin.RouterGroup, conf *config.Config) {
func DislikePhoto(router *gin.RouterGroup) {
router.DELETE("/photos/:uid/like", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionLike)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -270,7 +286,7 @@ func DislikePhoto(router *gin.RouterGroup, conf *config.Config) {
return
}
SavePhotoAsYaml(m, conf)
SavePhotoAsYaml(m)
PublishPhotoEvent(EntityUpdated, id, c)
@@ -282,9 +298,11 @@ func DislikePhoto(router *gin.RouterGroup, conf *config.Config) {
//
// Parameters:
// uid: string PhotoUID as returned by the API
func SetPhotoPrimary(router *gin.RouterGroup, conf *config.Config) {
func SetPhotoPrimary(router *gin.RouterGroup) {
router.POST("/photos/:uid/primary/:file_uid", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}

View File

@@ -5,8 +5,8 @@ import (
"strconv"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/classify"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
@@ -18,9 +18,11 @@ import (
//
// Parameters:
// uid: string PhotoUID as returned by the API
func AddPhotoLabel(router *gin.RouterGroup, conf *config.Config) {
func AddPhotoLabel(router *gin.RouterGroup) {
router.POST("/photos/:uid/label", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -91,9 +93,11 @@ func AddPhotoLabel(router *gin.RouterGroup, conf *config.Config) {
// Parameters:
// uid: string PhotoUID as returned by the API
// id: int LabelId as returned by the API
func RemovePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
func RemovePhotoLabel(router *gin.RouterGroup) {
router.DELETE("/photos/:uid/label/:id", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -153,9 +157,11 @@ func RemovePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
// Parameters:
// uid: string PhotoUID as returned by the API
// id: int LabelId as returned by the API
func UpdatePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
func UpdatePhotoLabel(router *gin.RouterGroup) {
router.PUT("/photos/:uid/label/:id", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}

View File

@@ -10,31 +10,31 @@ import (
func TestAddPhotoLabel(t *testing.T) {
t.Run("add new label", func(t *testing.T) {
app, router, ctx := NewApiTest()
AddPhotoLabel(router, ctx)
app, router, _ := NewApiTest()
AddPhotoLabel(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh8/label", `{"Name": "testAddLabel", "Uncertainty": 95, "Priority": 2}`)
assert.Equal(t, http.StatusOK, r.Code)
assert.Contains(t, r.Body.String(), "TestAddLabel")
})
t.Run("add existing label", func(t *testing.T) {
app, router, ctx := NewApiTest()
AddPhotoLabel(router, ctx)
app, router, _ := NewApiTest()
AddPhotoLabel(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh8/label", `{"Name": "Flower", "Uncertainty": 10, "Priority": 2}`)
assert.Equal(t, http.StatusOK, r.Code)
val := gjson.Get(r.Body.String(), "Labels.#(LabelID==1000001).Uncertainty")
assert.Equal(t, "10", val.String())
})
t.Run("not found", func(t *testing.T) {
app, router, ctx := NewApiTest()
AddPhotoLabel(router, ctx)
app, router, _ := NewApiTest()
AddPhotoLabel(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/photos/xxx/label", `{"Name": "Flower", "Uncertainty": 10, "Priority": 2}`)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "Photo not found", val.String())
assert.Equal(t, http.StatusNotFound, r.Code)
})
t.Run("invalid request", func(t *testing.T) {
app, router, ctx := NewApiTest()
AddPhotoLabel(router, ctx)
app, router, _ := NewApiTest()
AddPhotoLabel(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh8/label", `{"Name": 123, "Uncertainty": 10, "Priority": 2}`)
assert.Equal(t, http.StatusBadRequest, r.Code)
})
@@ -43,8 +43,8 @@ func TestAddPhotoLabel(t *testing.T) {
func TestRemovePhotoLabel(t *testing.T) {
t.Run("photo with label", func(t *testing.T) {
app, router, ctx := NewApiTest()
RemovePhotoLabel(router, ctx)
app, router, _ := NewApiTest()
RemovePhotoLabel(router)
r := PerformRequest(app, "DELETE", "/api/v1/photos/pt9jtdre2lvl0yh7/label/1000001")
assert.Equal(t, http.StatusOK, r.Code)
val := gjson.Get(r.Body.String(), "Labels.#(LabelID==1000001).Uncertainty")
@@ -53,38 +53,38 @@ func TestRemovePhotoLabel(t *testing.T) {
})
t.Run("remove manually added label", func(t *testing.T) {
app, router, ctx := NewApiTest()
RemovePhotoLabel(router, ctx)
app, router, _ := NewApiTest()
RemovePhotoLabel(router)
r := PerformRequest(app, "DELETE", "/api/v1/photos/pt9jtdre2lvl0yh7/label/1000002")
assert.Equal(t, http.StatusOK, r.Code)
val := gjson.Get(r.Body.String(), "Labels")
assert.NotContains(t, val.String(), "cake")
})
t.Run("photo not found", func(t *testing.T) {
app, router, ctx := NewApiTest()
RemovePhotoLabel(router, ctx)
app, router, _ := NewApiTest()
RemovePhotoLabel(router)
r := PerformRequest(app, "DELETE", "/api/v1/photos/xxx/label/10000001")
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "Photo not found", val.String())
assert.Equal(t, http.StatusNotFound, r.Code)
})
t.Run("label not existing", func(t *testing.T) {
app, router, ctx := NewApiTest()
RemovePhotoLabel(router, ctx)
app, router, _ := NewApiTest()
RemovePhotoLabel(router)
r := PerformRequest(app, "DELETE", "/api/v1/photos/pt9jtdre2lvl0yh7/label/xxx")
assert.Equal(t, http.StatusNotFound, r.Code)
})
t.Run("try to remove wrong label", func(t *testing.T) {
app, router, ctx := NewApiTest()
RemovePhotoLabel(router, ctx)
app, router, _ := NewApiTest()
RemovePhotoLabel(router)
r := PerformRequest(app, "DELETE", "/api/v1/photos/pt9jtdre2lvl0yh7/label/1000000")
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "Record not found", val.String())
assert.Equal(t, http.StatusNotFound, r.Code)
})
t.Run("not existing photo", func(t *testing.T) {
app, router, ctx := NewApiTest()
RemovePhotoLabel(router, ctx)
app, router, _ := NewApiTest()
RemovePhotoLabel(router)
r := PerformRequest(app, "DELETE", "/api/v1/photos/xx/label/")
assert.Equal(t, http.StatusNotFound, r.Code)
})
@@ -92,36 +92,36 @@ func TestRemovePhotoLabel(t *testing.T) {
func TestUpdatePhotoLabel(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
app, router, ctx := NewApiTest()
UpdatePhotoLabel(router, ctx)
app, router, _ := NewApiTest()
UpdatePhotoLabel(router)
r := PerformRequestWithBody(app, "PUT", "/api/v1/photos/pt9jtdre2lvl0yh0/label/1000006", `{"Label": {"Name": "NewLabelName"}}`)
assert.Equal(t, http.StatusOK, r.Code)
val := gjson.Get(r.Body.String(), "Title")
assert.Contains(t, val.String(), "NewLabelName")
})
t.Run("photo not found", func(t *testing.T) {
app, router, ctx := NewApiTest()
UpdatePhotoLabel(router, ctx)
app, router, _ := NewApiTest()
UpdatePhotoLabel(router)
r := PerformRequestWithBody(app, "PUT", "/api/v1/photos/xxx/label/1000006", `{"Label": {"Name": "NewLabelName"}}`)
assert.Equal(t, http.StatusNotFound, r.Code)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "Photo not found", val.String())
})
t.Run("label not existing", func(t *testing.T) {
app, router, ctx := NewApiTest()
UpdatePhotoLabel(router, ctx)
app, router, _ := NewApiTest()
UpdatePhotoLabel(router)
r := PerformRequestWithBody(app, "PUT", "/api/v1/photos/pt9jtdre2lvl0yh0/label/9000006", `{"Label": {"Name": "NewLabelName"}}`)
assert.Equal(t, http.StatusNotFound, r.Code)
})
t.Run("label not linked to photo", func(t *testing.T) {
app, router, ctx := NewApiTest()
UpdatePhotoLabel(router, ctx)
app, router, _ := NewApiTest()
UpdatePhotoLabel(router)
r := PerformRequestWithBody(app, "PUT", "/api/v1/photos/pt9jtdre2lvl0yh0/label/1000005", `{"Label": {"Name": "NewLabelName"}}`)
assert.Equal(t, http.StatusNotFound, r.Code)
})
t.Run("bad request", func(t *testing.T) {
app, router, ctx := NewApiTest()
UpdatePhotoLabel(router, ctx)
app, router, _ := NewApiTest()
UpdatePhotoLabel(router)
r := PerformRequestWithBody(app, "PUT", "/api/v1/photos/pt9jtdre2lvl0yh0/label/1000006", `{"Label": {"Name": 123}}`)
assert.Equal(t, http.StatusBadRequest, r.Code)
})

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"strconv"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/txt"
@@ -27,9 +27,11 @@ import (
// before: date Find photos taken before (format: "2006-01-02")
// after: date Find photos taken after (format: "2006-01-02")
// favorite: bool Find favorites only
func GetPhotos(router *gin.RouterGroup, conf *config.Config) {
func GetPhotos(router *gin.RouterGroup) {
router.GET("/photos", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionSearch)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
@@ -43,6 +45,12 @@ func GetPhotos(router *gin.RouterGroup, conf *config.Config) {
return
}
// Guest permissions are limited to shared albums.
if s.Guest() && (f.Album == "" || !s.HasShare(f.Album)){
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
result, count, err := query.PhotoSearch(f)
if err != nil {

View File

@@ -10,9 +10,9 @@ import (
func TestGetPhotos(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
app, router, ctx := NewApiTest()
app, router, _ := NewApiTest()
GetPhotos(router, ctx)
GetPhotos(router)
r := PerformRequest(app, "GET", "/api/v1/photos?count=10")
count := gjson.Get(r.Body.String(), "#")
assert.LessOrEqual(t, int64(2), count.Int())
@@ -20,8 +20,8 @@ func TestGetPhotos(t *testing.T) {
})
t.Run("invalid request", func(t *testing.T) {
app, router, ctx := NewApiTest()
GetPhotos(router, ctx)
app, router, _ := NewApiTest()
GetPhotos(router)
result := PerformRequest(app, "GET", "/api/v1/photos?xxx=10")
assert.Equal(t, http.StatusBadRequest, result.Code)
})

View File

@@ -10,16 +10,17 @@ import (
func TestGetPhoto(t *testing.T) {
t.Run("search for existing photo", func(t *testing.T) {
app, router, ctx := NewApiTest()
GetPhoto(router, ctx)
app, router, _ := NewApiTest()
GetPhoto(router)
r := PerformRequest(app, "GET", "/api/v1/photos/pt9jtdre2lvl0yh7")
assert.Equal(t, http.StatusOK, r.Code)
val := gjson.Get(r.Body.String(), "Lat")
assert.Equal(t, "48.519234", val.String())
})
t.Run("search for not existing photo", func(t *testing.T) {
app, router, ctx := NewApiTest()
GetPhoto(router, ctx)
app, router, _ := NewApiTest()
GetPhoto(router)
r := PerformRequest(app, "GET", "/api/v1/photos/xxx")
assert.Equal(t, http.StatusNotFound, r.Code)
})
@@ -27,8 +28,8 @@ func TestGetPhoto(t *testing.T) {
func TestUpdatePhoto(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
UpdatePhoto(router, conf)
app, router, _ := NewApiTest()
UpdatePhoto(router)
r := PerformRequestWithBody(app, "PUT", "/api/v1/photos/pt9jtdre2lvl0y13", `{"Title": "Updated01", "Country": "de"}`)
val := gjson.Get(r.Body.String(), "Title")
assert.Equal(t, "Updated01", val.String())
@@ -38,15 +39,15 @@ func TestUpdatePhoto(t *testing.T) {
})
t.Run("invalid request", func(t *testing.T) {
app, router, conf := NewApiTest()
UpdatePhoto(router, conf)
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) {
app, router, conf := NewApiTest()
UpdatePhoto(router, conf)
app, router, _ := NewApiTest()
UpdatePhoto(router)
r := PerformRequestWithBody(app, "PUT", "/api/v1/photos/xxx", `{"Name": "Updated01", "Country": "de"}`)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "Photo not found", val.String())
@@ -57,13 +58,14 @@ func TestUpdatePhoto(t *testing.T) {
func TestGetPhotoDownload(t *testing.T) {
t.Run("could not find original", func(t *testing.T) {
app, router, conf := NewApiTest()
GetPhotoDownload(router, conf)
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) {
app, router, conf := NewApiTest()
GetPhotoDownload(router, conf)
GetPhotoDownload(router)
r := PerformRequest(app, "GET", "/api/v1/photos/xxx/dl?t="+conf.DownloadToken())
assert.Equal(t, http.StatusNotFound, r.Code)
})
@@ -71,18 +73,19 @@ func TestGetPhotoDownload(t *testing.T) {
func TestLikePhoto(t *testing.T) {
t.Run("existing photo", func(t *testing.T) {
app, router, conf := NewApiTest()
LikePhoto(router, conf)
app, router, _ := NewApiTest()
LikePhoto(router)
r := PerformRequest(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh9/like")
assert.Equal(t, http.StatusOK, r.Code)
GetPhoto(router, conf)
GetPhoto(router)
r2 := PerformRequest(app, "GET", "/api/v1/photos/pt9jtdre2lvl0yh9")
val := gjson.Get(r2.Body.String(), "Favorite")
assert.Equal(t, "true", val.String())
})
t.Run("not existing photo", func(t *testing.T) {
app, router, conf := NewApiTest()
LikePhoto(router, conf)
app, router, _ := NewApiTest()
LikePhoto(router)
r := PerformRequest(app, "POST", "/api/v1/photos/xxx/like")
assert.Equal(t, http.StatusNotFound, r.Code)
})
@@ -90,18 +93,19 @@ func TestLikePhoto(t *testing.T) {
func TestDislikePhoto(t *testing.T) {
t.Run("existing photo", func(t *testing.T) {
app, router, ctx := NewApiTest()
DislikePhoto(router, ctx)
app, router, _ := NewApiTest()
DislikePhoto(router)
r := PerformRequest(app, "DELETE", "/api/v1/photos/pt9jtdre2lvl0yh8/like")
assert.Equal(t, http.StatusOK, r.Code)
GetPhoto(router, ctx)
GetPhoto(router)
r2 := PerformRequest(app, "GET", "/api/v1/photos/pt9jtdre2lvl0yh8")
val := gjson.Get(r2.Body.String(), "Favorite")
assert.Equal(t, "false", val.String())
})
t.Run("not existing photo", func(t *testing.T) {
app, router, ctx := NewApiTest()
DislikePhoto(router, ctx)
app, router, _ := NewApiTest()
DislikePhoto(router)
r := PerformRequest(app, "DELETE", "/api/v1/photos/xxx/like")
assert.Equal(t, http.StatusNotFound, r.Code)
})
@@ -109,11 +113,11 @@ func TestDislikePhoto(t *testing.T) {
func TestSetPhotoPrimary(t *testing.T) {
t.Run("existing photo", func(t *testing.T) {
app, router, ctx := NewApiTest()
SetPhotoPrimary(router, ctx)
app, router, _ := NewApiTest()
SetPhotoPrimary(router)
r := PerformRequest(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh8/primary/ft1es39w45bnlqdw")
assert.Equal(t, http.StatusOK, r.Code)
GetFile(router, ctx)
GetFile(router)
r2 := PerformRequest(app, "GET", "/api/v1/files/ocad9168fa6acc5c5c2965ddf6ec465ca42fd818")
val := gjson.Get(r2.Body.String(), "Primary")
assert.Equal(t, "true", val.String())
@@ -121,9 +125,10 @@ func TestSetPhotoPrimary(t *testing.T) {
val2 := gjson.Get(r3.Body.String(), "Primary")
assert.Equal(t, "false", val2.String())
})
t.Run("wrong photo uid", func(t *testing.T) {
app, router, ctx := NewApiTest()
SetPhotoPrimary(router, ctx)
app, router, _ := NewApiTest()
SetPhotoPrimary(router)
r := PerformRequest(app, "POST", "/api/v1/photos/xxx/primary/ft1es39w45bnlqdw")
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "Photo not found", val.String())

View File

@@ -8,7 +8,6 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
@@ -31,14 +30,15 @@ type ByteCache struct {
// Parameters:
// hash: string The file hash as returned by the search API
// type: string Thumbnail type, see photoprism.ThumbnailTypes
func GetThumbnail(router *gin.RouterGroup, conf *config.Config) {
func GetThumbnail(router *gin.RouterGroup) {
router.GET("/t/:hash/:token/:type", func(c *gin.Context) {
if InvalidToken(c, conf) {
if InvalidToken(c) {
c.Data(http.StatusForbidden, "image/svg+xml", brokenIconSvg)
return
}
start := time.Now()
conf := service.Config()
fileHash := c.Param("hash")
typeName := c.Param("type")

View File

@@ -10,21 +10,21 @@ import (
func TestGetThumbnail(t *testing.T) {
t.Run("invalid type", func(t *testing.T) {
app, router, conf := NewApiTest()
GetThumbnail(router, conf)
GetThumbnail(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) {
app, router, conf := NewApiTest()
GetThumbnail(router, conf)
GetThumbnail(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) {
app, router, conf := NewApiTest()
GetThumbnail(router, conf)
GetThumbnail(router)
r := PerformRequest(app, "GET", "/api/v1/t/2cad9168fa6acc5c5c2965ddf6ec465ca42fd818/"+conf.PreviewToken()+"/tile_500")
assert.Equal(t, http.StatusOK, r.Code)
})

View File

@@ -11,20 +11,21 @@ import (
"github.com/disintegration/imaging"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/txt"
)
// GET /api/v1/preview
func GetPreview(router *gin.RouterGroup, conf *config.Config) {
func GetPreview(router *gin.RouterGroup) {
router.GET("/preview", func(c *gin.Context) {
// TODO: proof of concept - code needs refactoring!
t := time.Now().Format("20060102")
conf := service.Config()
thumbPath := path.Join(conf.ThumbPath(), "preview", t[0:4], t[4:6])
if err := os.MkdirAll(thumbPath, os.ModePerm); err != nil {

View File

@@ -9,8 +9,8 @@ import (
func TestGetPreview(t *testing.T) {
t.Run("not found", func(t *testing.T) {
app, router, ctx := NewApiTest()
GetPreview(router, ctx)
app, router, _ := NewApiTest()
GetPreview(router)
r := PerformRequest(app, "GET", "/api/v1/preview")
assert.Equal(t, http.StatusNotFound, r.Code)
})

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/service"
@@ -13,7 +13,7 @@ import (
)
// POST /api/v1/session
func CreateSession(router *gin.RouterGroup, conf *config.Config) {
func CreateSession(router *gin.RouterGroup) {
router.POST("/session", func(c *gin.Context) {
var f form.Login
@@ -22,7 +22,18 @@ func CreateSession(router *gin.RouterGroup, conf *config.Config) {
return
}
data := session.Data{}
var data session.Data
id := SessionID(c)
if s := Session(id); s.Valid() {
data = s
} else {
data = session.Data{}
id = ""
}
conf := service.Config()
if f.HasToken() {
links := entity.FindLinks(f.Token, "")
@@ -34,10 +45,13 @@ func CreateSession(router *gin.RouterGroup, conf *config.Config) {
data.Tokens = []string{f.Token}
for _, link := range links {
data.Shared = append(data.Shared, link.ShareUID)
data.Shares = append(data.Shares, link.ShareUID)
}
// Upgrade from anonymous to guest. Don't downgrade.
if data.User.Anonymous() {
data.User = entity.Guest
}
} else if f.HasCredentials() {
user := entity.FindPersonByUserName(f.UserName)
@@ -57,89 +71,70 @@ func CreateSession(router *gin.RouterGroup, conf *config.Config) {
return
}
token := service.Session().Create(data)
if err := service.Session().Update(id, data); err != nil {
id = service.Session().Create(data)
}
c.Header("X-Session-Token", token)
c.Header("X-Session-ID", id)
if data.User.Anonymous() {
c.JSON(http.StatusOK, gin.H{"token": token, "data": data, "config": conf.GuestConfig()})
c.JSON(http.StatusOK, gin.H{"status": "ok", "id": id, "data": data, "config": conf.GuestConfig()})
} else {
c.JSON(http.StatusOK, gin.H{"token": token, "data": data, "config": conf.UserConfig()})
c.JSON(http.StatusOK, gin.H{"status": "ok", "id": id, "data": data, "config": conf.UserConfig()})
}
})
}
// DELETE /api/v1/session/
func DeleteSession(router *gin.RouterGroup, conf *config.Config) {
router.DELETE("/session/:token", func(c *gin.Context) {
token := c.Param("token")
func DeleteSession(router *gin.RouterGroup) {
router.DELETE("/session/:id", func(c *gin.Context) {
id := c.Param("id")
service.Session().Delete(token)
service.Session().Delete(id)
c.JSON(http.StatusOK, gin.H{"status": "ok", "token": token})
c.JSON(http.StatusOK, gin.H{"status": "ok", "id": id})
})
}
// Returns true, if user doesn't have a valid session token
func Unauthorized(c *gin.Context, conf *config.Config) bool {
// Always return false if site is public.
if conf.Public() {
return false
}
// Get session token from HTTP header.
token := c.GetHeader("X-Session-Token")
// Check if session token is valid.
return !service.Session().Exists(token)
}
// Gets session token from HTTP header.
func SessionToken(c *gin.Context) string {
return c.GetHeader("X-Session-Token")
// Gets session id from HTTP header.
func SessionID(c *gin.Context) string {
return c.GetHeader("X-Session-ID")
}
// Session returns the current session data.
func Session(token string, conf *config.Config) (data *session.Data) {
if token == "" {
return nil
func Session(id string) session.Data {
// Return fake admin session if site is public.
if service.Config().Public() {
return session.Data{User: entity.Admin}
}
defer func() {
if err := recover(); err != nil {
data = nil
log.Errorf("session: %s [panic]", err)
}
}()
// Always return false if site is public.
if conf.Public() {
admin := entity.FindPersonByUserName("admin")
if admin == nil {
log.Error("session: admin user not found - bug?")
return nil
// Check if session id is valid.
return service.Session().Get(id)
}
return &session.Data{User: *admin}
// Auth returns the session if user is authorized for the current action.
func Auth(id string, resource acl.Resource, action acl.Action) session.Data {
sess := Session(id)
if acl.Permissions.Deny(resource, sess.User.Role(), action) {
return session.Data{}
}
// Check if session token is valid.
return service.Session().Get(token)
return sess
}
// InvalidToken returns true if the token is invalid.
func InvalidToken(c *gin.Context, conf *config.Config) bool {
func InvalidToken(c *gin.Context) bool {
token := c.Param("token")
if token == "" {
token = c.Query("t")
}
return conf.InvalidToken(token)
return service.Config().InvalidToken(token)
}
// InvalidDownloadToken returns true if the token is invalid.
func InvalidDownloadToken(c *gin.Context, conf *config.Config) bool {
return conf.InvalidDownloadToken(c.Query("t"))
func InvalidDownloadToken(c *gin.Context) bool {
return service.Config().InvalidDownloadToken(c.Query("t"))
}

View File

@@ -9,22 +9,22 @@ import (
func TestCreateSession(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
CreateSession(router, conf)
app, router, _ := NewApiTest()
CreateSession(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/session", `{"username": "admin", "password": "photoprism"}`)
val2 := gjson.Get(r.Body.String(), "user.Email")
assert.Equal(t, "", val2.String())
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("bad request", func(t *testing.T) {
app, router, conf := NewApiTest()
CreateSession(router, conf)
app, router, _ := NewApiTest()
CreateSession(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/session", `{"username": 123, "password": "xxx"}`)
assert.Equal(t, http.StatusBadRequest, r.Code)
})
t.Run("invalid password", func(t *testing.T) {
app, router, conf := NewApiTest()
CreateSession(router, conf)
app, router, _ := NewApiTest()
CreateSession(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/session", `{"username": "admin", "password": "xxx"}`)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "Invalid user name or password", val.String())
@@ -33,15 +33,15 @@ func TestCreateSession(t *testing.T) {
}
func TestDeleteSession(t *testing.T) {
app, router, conf := NewApiTest()
CreateSession(router, conf)
app, router, _ := NewApiTest()
CreateSession(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/session", `{"username": "admin", "password": "photoprism"}`)
token := gjson.Get(r.Body.String(), "token")
id := gjson.Get(r.Body.String(), "id")
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
DeleteSession(router, conf)
r := PerformRequest(app, "DELETE", "/api/v1/session/"+token.String())
app, router, _ := NewApiTest()
DeleteSession(router)
r := PerformRequest(app, "DELETE", "/api/v1/session/"+id.String())
assert.Equal(t, http.StatusOK, r.Code)
})
}

View File

@@ -4,48 +4,62 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/txt"
)
// GET /api/v1/settings
func GetSettings(router *gin.RouterGroup, conf *config.Config) {
func GetSettings(router *gin.RouterGroup) {
router.GET("/settings", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourceSettings, acl.ActionRead)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
s := conf.Settings()
c.JSON(http.StatusOK, s)
if settings := service.Config().Settings(); settings != nil {
c.JSON(http.StatusOK, settings)
} else {
c.AbortWithStatusJSON(http.StatusNotFound, ErrNotFound)
}
})
}
// POST /api/v1/settings
func SaveSettings(router *gin.RouterGroup, conf *config.Config) {
func SaveSettings(router *gin.RouterGroup) {
router.POST("/settings", func(c *gin.Context) {
if conf.SettingsHidden() || Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourceSettings, acl.ActionUpdate)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
s := conf.Settings()
conf := service.Config()
if err := c.BindJSON(s); err != nil {
if conf.SettingsHidden() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
settings := conf.Settings()
if err := c.BindJSON(settings); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())})
return
}
if err := s.Save(conf.SettingsFile()); err != nil {
if err := settings.Save(conf.SettingsFile()); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, err)
return
}
UpdateClientConfig(conf)
UpdateClientConfig()
log.Infof("settings saved")
c.JSON(http.StatusOK, s)
c.JSON(http.StatusOK, settings)
})
}

View File

@@ -9,8 +9,8 @@ import (
func TestGetSettings(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
GetSettings(router, conf)
app, router, _ := NewApiTest()
GetSettings(router)
r := PerformRequest(app, "GET", "/api/v1/settings")
val := gjson.Get(r.Body.String(), "theme")
assert.NotEmpty(t, val.String())
@@ -38,8 +38,8 @@ func TestSaveSettings(t *testing.T) {
assert.Equal(t, http.StatusOK, r3.Code)
}) */
t.Run("bad request", func(t *testing.T) {
app, router, conf := NewApiTest()
SaveSettings(router, conf)
app, router, _ := NewApiTest()
SaveSettings(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/settings", `{"language": 123}`)
assert.Equal(t, http.StatusBadRequest, r.Code)
})

View File

@@ -6,6 +6,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -24,7 +25,9 @@ func shareHandler(c *gin.Context, conf *config.Config) {
c.HTML(http.StatusOK, "share.tmpl", gin.H{"config": clientConfig})
}
func InitShare(router *gin.RouterGroup, conf *config.Config) {
func InitShare(router *gin.RouterGroup) {
conf := service.Config()
router.GET("/:token", func(c *gin.Context) {
shareHandler(c, conf)
})

View File

@@ -4,11 +4,10 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config"
)
// GET /api/v1/status
func GetStatus(router *gin.RouterGroup, conf *config.Config) {
func GetStatus(router *gin.RouterGroup) {
router.GET("/status", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "operational"})
})

View File

@@ -9,8 +9,8 @@ import (
func TestGetStatus(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
GetStatus(router, conf)
app, router, _ := NewApiTest()
GetStatus(router)
r := PerformRequest(app, "GET", "/api/v1/status")
val := gjson.Get(r.Body.String(), "status")
assert.Equal(t, "operational", val.String())

View File

@@ -8,7 +8,7 @@ import (
"path/filepath"
"time"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/txt"
@@ -17,14 +17,17 @@ import (
)
// POST /api/v1/upload/:path
func Upload(router *gin.RouterGroup, conf *config.Config) {
func Upload(router *gin.RouterGroup) {
router.POST("/upload/:path", func(c *gin.Context) {
conf := service.Config()
if conf.ReadOnly() || !conf.Settings().Features.Upload {
c.AbortWithStatusJSON(http.StatusForbidden, ErrReadOnly)
return
}
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpload)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}

View File

@@ -4,7 +4,6 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/video"
@@ -17,9 +16,9 @@ import (
// Parameters:
// hash: string The photo or video file hash as returned by the search API
// type: string Video type
func GetVideo(router *gin.RouterGroup, conf *config.Config) {
func GetVideo(router *gin.RouterGroup) {
router.GET("/videos/:hash/:token/:type", func(c *gin.Context) {
if InvalidToken(c, conf) {
if InvalidToken(c) {
c.Data(http.StatusForbidden, "image/svg+xml", brokenIconSvg)
return
}

View File

@@ -9,25 +9,28 @@ import (
func TestGetVideo(t *testing.T) {
t.Run("invalid hash", func(t *testing.T) {
app, router, conf := NewApiTest()
GetVideo(router, conf)
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) {
app, router, conf := NewApiTest()
GetVideo(router, conf)
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) {
app, router, conf := NewApiTest()
GetVideo(router, conf)
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) {
app, router, conf := NewApiTest()
GetVideo(router, conf)
GetVideo(router)
r := PerformRequest(app, "GET", "/api/v1/videos/acad9168fa6acc5c5c2965ddf6ec465ca42fd832/"+conf.PreviewToken()+"/mp4")
assert.Equal(t, http.StatusOK, r.Code)
})

View File

@@ -11,6 +11,7 @@ import (
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/rnd"
)
@@ -54,7 +55,7 @@ func wsReader(ws *websocket.Conn, writeMutex *sync.Mutex, connId string, conf *c
if err := json.Unmarshal(m, &info); err != nil {
log.Error(err)
} else {
if sess := Session(info.SessionToken, conf); sess != nil {
if sess := Session(info.SessionToken); sess.Valid() {
log.Debug("websocket: authenticated")
wsAuth.mutex.Lock()
@@ -68,7 +69,7 @@ func wsReader(ws *websocket.Conn, writeMutex *sync.Mutex, connId string, conf *c
if err := ws.WriteJSON(gin.H{"event": "config.updated", "data": event.Data{"config": conf.GuestConfig()}}); err != nil {
log.Error(err)
}
} else if sess.User.User() {
} else if sess.User.Registered() {
if err := ws.WriteJSON(gin.H{"event": "config.updated", "data": event.Data{"config": conf.UserConfig()}}); err != nil {
log.Error(err)
}
@@ -125,7 +126,7 @@ func wsWriter(ws *websocket.Conn, writeMutex *sync.Mutex, connId string) {
user := wsAuth.user[connId]
wsAuth.mutex.RUnlock()
if user.User() {
if user.Registered() {
writeMutex.Lock()
ws.SetWriteDeadline(time.Now().Add(30 * time.Second))
@@ -141,12 +142,14 @@ func wsWriter(ws *websocket.Conn, writeMutex *sync.Mutex, connId string) {
}
// GET /api/v1/ws
func Websocket(router *gin.RouterGroup, conf *config.Config) {
func Websocket(router *gin.RouterGroup) {
if router == nil {
log.Error("websocket: router is nil")
return
}
conf := service.Config()
if conf == nil {
log.Error("websocket: conf is nil")
return

View File

@@ -8,20 +8,15 @@ import (
func TestWebsocket(t *testing.T) {
t.Run("bad request", func(t *testing.T) {
app, router, conf := NewApiTest()
Websocket(router, conf)
app, router, _ := NewApiTest()
Websocket(router)
r := PerformRequest(app, "GET", "/api/v1/ws")
assert.Equal(t, http.StatusBadRequest, r.Code)
})
t.Run("router nil", func(t *testing.T) {
app, _, conf := NewApiTest()
Websocket(nil, conf)
r := PerformRequest(app, "GET", "/api/v1/ws")
assert.Equal(t, http.StatusNotFound, r.Code)
})
t.Run("conf nil", func(t *testing.T) {
app, router, _ := NewApiTest()
Websocket(router, nil)
app, _, _ := NewApiTest()
Websocket(nil)
r := PerformRequest(app, "GET", "/api/v1/ws")
assert.Equal(t, http.StatusNotFound, r.Code)
})

View File

@@ -10,10 +10,11 @@ import (
"path/filepath"
"time"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/rnd"
"github.com/photoprism/photoprism/pkg/txt"
@@ -22,13 +23,17 @@ import (
)
// POST /api/v1/zip
func CreateZip(router *gin.RouterGroup, conf *config.Config) {
func CreateZip(router *gin.RouterGroup) {
router.POST("/zip", func(c *gin.Context) {
if Unauthorized(c, conf) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionDownload)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
conf := service.Config()
if !conf.Settings().Features.Download {
c.AbortWithStatusJSON(http.StatusForbidden, ErrFeatureDisabled)
return
@@ -108,13 +113,14 @@ func CreateZip(router *gin.RouterGroup, conf *config.Config) {
}
// GET /api/v1/zip/:filename
func DownloadZip(router *gin.RouterGroup, conf *config.Config) {
func DownloadZip(router *gin.RouterGroup) {
router.GET("/zip/:filename", func(c *gin.Context) {
if InvalidDownloadToken(c, conf) {
if InvalidDownloadToken(c) {
c.Data(http.StatusForbidden, "image/svg+xml", brokenIconSvg)
return
}
conf := service.Config()
zipBaseName := filepath.Base(c.Param("filename"))
zipPath := path.Join(conf.TempPath(), "zip")
zipFileName := path.Join(zipPath, zipBaseName)

View File

@@ -9,32 +9,32 @@ import (
func TestCreateZip(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
CreateZip(router, conf)
app, router, _ := NewApiTest()
CreateZip(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/zip", `{"photos": ["pt9jtdre2lvl0y12", "pt9jtdre2lvl0y11"]}`)
val := gjson.Get(r.Body.String(), "message")
assert.Contains(t, val.String(), "zip created")
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("no items selected", func(t *testing.T) {
app, router, conf := NewApiTest()
CreateZip(router, conf)
app, router, _ := NewApiTest()
CreateZip(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/zip", `{"photos": []}`)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "No items selected", val.String())
assert.Equal(t, http.StatusBadRequest, r.Code)
})
t.Run("invalid request", func(t *testing.T) {
app, router, conf := NewApiTest()
CreateZip(router, conf)
app, router, _ := NewApiTest()
CreateZip(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/zip", `{"photos": [123, "pt9jtdre2lvl0yxx"]}`)
assert.Equal(t, http.StatusBadRequest, r.Code)
})
}
func TestDownloadZip(t *testing.T) {
app, router, conf := NewApiTest()
CreateZip(router, conf)
app, router, _ := NewApiTest()
CreateZip(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/zip", `{"photos": ["pt9jtdre2lvl0y12", "pt9jtdre2lvl0y11"]}`)
// filename := gjson.Get(r.Body.String(), "filename")
assert.Equal(t, http.StatusNotFound, r.Code) // TODO: Should be http.StatusOK
@@ -51,7 +51,7 @@ func TestDownloadZip(t *testing.T) {
t.Run("zip not existing", func(t *testing.T) {
app, router, conf := NewApiTest()
DownloadZip(router, conf)
DownloadZip(router)
r := PerformRequest(app, "GET", "/api/v1/zip/xxx?t="+conf.DownloadToken())
assert.Equal(t, http.StatusNotFound, r.Code)
})

View File

@@ -193,7 +193,7 @@ func (c *Config) GuestConfig() ClientConfig {
// UserConfig returns client configuration values for registered users.
func (c *Config) UserConfig() ClientConfig {
defer log.Debug(capture.Time(time.Now(), "config: admin config created"))
defer log.Debug(capture.Time(time.Now(), "config: user config created"))
result := ClientConfig{
Settings: *c.Settings(),

View File

@@ -68,7 +68,7 @@ var GlobalFlags = []cli.Flag{
cli.StringFlag{
Name: "site-caption",
Usage: "short caption / tagline",
Value: "Browse Your Life",
Value: "Browse Your Life in Pictures",
EnvVar: "PHOTOPRISM_SITE_CAPTION",
},
cli.StringFlag{

View File

@@ -5,6 +5,7 @@ import (
"time"
"github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/pkg/rnd"
)
@@ -25,9 +26,11 @@ type Person struct {
UserConfirmed bool `json:"Confirmed" yaml:"Confirmed,omitempty"`
RoleAdmin bool `json:"Admin" yaml:"Admin,omitempty"`
RoleGuest bool `json:"Guest" yaml:"Guest,omitempty"`
RoleChild bool `json:"Child" yaml:"Child,omitempty"`
RoleFamily bool `json:"Family" yaml:"Family,omitempty"`
RoleArtist bool `json:"Artist" yaml:"Artist,omitempty"`
RoleSubject bool `json:"Subject" yaml:"Subject,omitempty"`
RoleFriend bool `json:"Friend" yaml:"Friend,omitempty"`
IsArtist bool `json:"Artist" yaml:"Artist,omitempty"`
IsSubject bool `json:"Subject" yaml:"Subject,omitempty"`
CanEdit bool `json:"CanEdit" yaml:"CanEdit,omitempty"`
CanComment bool `json:"CanComment" yaml:"CanComment,omitempty"`
CanUpload bool `json:"CanUpload" yaml:"CanUpload,omitempty"`
@@ -165,13 +168,13 @@ func (m *Person) String() string {
}
// User returns true if the person has a user name.
func (m *Person) User() bool {
func (m *Person) Registered() bool {
return m.UserName != "" && rnd.IsPPID(m.PersonUID, 'u')
}
// Admin returns true if the person is an admin with user name.
func (m *Person) Admin() bool {
return m.User() && m.RoleAdmin
return m.Registered() && m.RoleAdmin
}
// Anonymous returns true if the person is unknown.
@@ -186,8 +189,8 @@ func (m *Person) Guest() bool {
// SetPassword sets a new password stored as hash.
func (m *Person) SetPassword(password string) error {
if !m.User() {
return fmt.Errorf("login: only users can have a password")
if !m.Registered() {
return fmt.Errorf("auth: only registered users can change their password")
}
pw := NewPassword(m.PersonUID, password)
@@ -197,8 +200,8 @@ func (m *Person) SetPassword(password string) error {
// InitPassword sets the initial user password stored as hash.
func (m *Person) InitPassword(password string) {
if !m.User() {
log.Warn("login: only users can have a password")
if !m.Registered() {
log.Warn("auth: only registered users can change their password")
return
}
@@ -217,8 +220,8 @@ func (m *Person) InitPassword(password string) {
// InvalidPassword returns true if the given password does not match the hash.
func (m *Person) InvalidPassword(password string) bool {
if !m.User() {
log.Warn("login: only users can have a password")
if !m.Registered() {
log.Warn("auth: only registered users can change their password")
return true
}
@@ -230,3 +233,28 @@ func (m *Person) InvalidPassword(password string) bool {
return pw.InvalidPassword(password)
}
// Role returns the user role for ACL permission checks.
func (m *Person) Role() acl.Role {
if m.RoleAdmin {
return acl.RoleAdmin
}
if m.RoleChild {
return acl.RoleChild
}
if m.RoleFamily {
return acl.RoleFamily
}
if m.RoleFriend {
return acl.RoleFriend
}
if m.RoleGuest {
return acl.RoleGuest
}
return acl.RoleDefault
}

View File

@@ -24,101 +24,101 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
// JSON-REST API Version 1
v1 := router.Group("/api/v1")
{
api.GetStatus(v1, conf)
api.GetConfig(v1, conf)
api.GetStatus(v1)
api.GetConfig(v1)
api.CreateSession(v1, conf)
api.DeleteSession(v1, conf)
api.CreateSession(v1)
api.DeleteSession(v1)
api.GetPreview(v1, conf)
api.GetThumbnail(v1, conf)
api.GetDownload(v1, conf)
api.GetVideo(v1, conf)
api.CreateZip(v1, conf)
api.DownloadZip(v1, conf)
api.GetPreview(v1)
api.GetThumbnail(v1)
api.GetDownload(v1)
api.GetVideo(v1)
api.CreateZip(v1)
api.DownloadZip(v1)
api.GetGeo(v1, conf)
api.GetPhoto(v1, conf)
api.GetPhotoYaml(v1, conf)
api.UpdatePhoto(v1, conf)
api.GetPhotos(v1, conf)
api.GetPhotoDownload(v1, conf)
api.GetPhotoLinks(v1, conf)
api.CreatePhotoLink(v1, conf)
api.UpdatePhotoLink(v1, conf)
api.DeletePhotoLink(v1, conf)
api.ApprovePhoto(v1, conf)
api.LikePhoto(v1, conf)
api.DislikePhoto(v1, conf)
api.AddPhotoLabel(v1, conf)
api.RemovePhotoLabel(v1, conf)
api.UpdatePhotoLabel(v1, conf)
api.GetMomentsTime(v1, conf)
api.GetFile(v1, conf)
api.SetPhotoPrimary(v1, conf)
api.GetGeo(v1)
api.GetPhoto(v1)
api.GetPhotoYaml(v1)
api.UpdatePhoto(v1)
api.GetPhotos(v1)
api.GetPhotoDownload(v1)
api.GetPhotoLinks(v1)
api.CreatePhotoLink(v1)
api.UpdatePhotoLink(v1)
api.DeletePhotoLink(v1)
api.ApprovePhoto(v1)
api.LikePhoto(v1)
api.DislikePhoto(v1)
api.AddPhotoLabel(v1)
api.RemovePhotoLabel(v1)
api.UpdatePhotoLabel(v1)
api.GetMomentsTime(v1)
api.GetFile(v1)
api.SetPhotoPrimary(v1)
api.GetLabels(v1, conf)
api.UpdateLabel(v1, conf)
api.GetLabelLinks(v1, conf)
api.CreateLabelLink(v1, conf)
api.UpdateLabelLink(v1, conf)
api.DeleteLabelLink(v1, conf)
api.LikeLabel(v1, conf)
api.DislikeLabel(v1, conf)
api.LabelThumbnail(v1, conf)
api.GetLabels(v1)
api.UpdateLabel(v1)
api.GetLabelLinks(v1)
api.CreateLabelLink(v1)
api.UpdateLabelLink(v1)
api.DeleteLabelLink(v1)
api.LikeLabel(v1)
api.DislikeLabel(v1)
api.LabelThumbnail(v1)
api.GetFoldersOriginals(v1, conf)
api.GetFoldersImport(v1, conf)
api.GetFoldersOriginals(v1)
api.GetFoldersImport(v1)
api.Upload(v1, conf)
api.StartImport(v1, conf)
api.CancelImport(v1, conf)
api.StartIndexing(v1, conf)
api.CancelIndexing(v1, conf)
api.Upload(v1)
api.StartImport(v1)
api.CancelImport(v1)
api.StartIndexing(v1)
api.CancelIndexing(v1)
api.BatchPhotosArchive(v1, conf)
api.BatchPhotosRestore(v1, conf)
api.BatchPhotosPrivate(v1, conf)
api.BatchAlbumsDelete(v1, conf)
api.BatchLabelsDelete(v1, conf)
api.BatchPhotosArchive(v1)
api.BatchPhotosRestore(v1)
api.BatchPhotosPrivate(v1)
api.BatchAlbumsDelete(v1)
api.BatchLabelsDelete(v1)
api.GetAlbum(v1, conf)
api.CreateAlbum(v1, conf)
api.UpdateAlbum(v1, conf)
api.DeleteAlbum(v1, conf)
api.DownloadAlbum(v1, conf)
api.GetAlbums(v1, conf)
api.GetAlbumLinks(v1, conf)
api.CreateAlbumLink(v1, conf)
api.UpdateAlbumLink(v1, conf)
api.DeleteAlbumLink(v1, conf)
api.LikeAlbum(v1, conf)
api.DislikeAlbum(v1, conf)
api.AlbumThumbnail(v1, conf)
api.CloneAlbums(v1, conf)
api.AddPhotosToAlbum(v1, conf)
api.RemovePhotosFromAlbum(v1, conf)
api.GetAlbum(v1)
api.CreateAlbum(v1)
api.UpdateAlbum(v1)
api.DeleteAlbum(v1)
api.DownloadAlbum(v1)
api.GetAlbums(v1)
api.GetAlbumLinks(v1)
api.CreateAlbumLink(v1)
api.UpdateAlbumLink(v1)
api.DeleteAlbumLink(v1)
api.LikeAlbum(v1)
api.DislikeAlbum(v1)
api.AlbumThumbnail(v1)
api.CloneAlbums(v1)
api.AddPhotosToAlbum(v1)
api.RemovePhotosFromAlbum(v1)
api.GetAccounts(v1, conf)
api.GetAccount(v1, conf)
api.GetAccountDirs(v1, conf)
api.ShareWithAccount(v1, conf)
api.CreateAccount(v1, conf)
api.DeleteAccount(v1, conf)
api.UpdateAccount(v1, conf)
api.GetAccounts(v1)
api.GetAccount(v1)
api.GetAccountDirs(v1)
api.ShareWithAccount(v1)
api.CreateAccount(v1)
api.DeleteAccount(v1)
api.UpdateAccount(v1)
api.GetSettings(v1, conf)
api.SaveSettings(v1, conf)
api.GetSettings(v1)
api.SaveSettings(v1)
api.GetSvg(v1)
api.Websocket(v1, conf)
api.Websocket(v1)
}
// Link sharing.
share := router.Group("/s")
{
api.InitShare(share, conf)
api.InitShare(share)
}
// WebDAV server for file management, sync and sharing.

View File

@@ -1,19 +1,55 @@
package session
import "github.com/photoprism/photoprism/internal/entity"
import (
"strings"
"github.com/photoprism/photoprism/internal/entity"
)
type Saved struct {
UID string `json:"uid"`
User string `json:"user"`
Tokens []string `json:"tokens"`
Expiration int64 `json:"expiration"`
}
type UIDs []string
func (list UIDs) String() string {
return strings.Join(list, ",")
}
type Data struct {
User entity.Person `json:"user"` // Session user, guest or anonymous person.
Tokens []string `json:"tokens"` // Slice of secret share tokens.
Shared []string `json:"shared"` // Slice of shared entity UIDs.
Shares UIDs `json:"shares"` // Slice of shared entity UIDs.
}
func (data Data) Saved() Saved {
return Saved{UID: data.User.PersonUID, Tokens: data.Tokens}
func (s Data) Saved() Saved {
return Saved{User: s.User.PersonUID, Tokens: s.Tokens}
}
func (s Data) Invalid() bool {
return s.User.ID == 0 || s.User.PersonUID == "" || (s.Guest() && s.NoShares())
}
func (s Data) Valid() bool {
return !s.Invalid()
}
func (s Data) Guest() bool {
return s.User.Guest()
}
func (s Data) NoShares() bool {
return len(s.Shares) == 0
}
func (s Data) HasShare(uid string) bool {
for _, share := range s.Shares {
if share == uid {
return true
}
}
return false
}

View File

@@ -28,7 +28,7 @@ func New(expiration time.Duration, cachePath string) *Session {
log.Errorf("session: %s", err)
} else {
for key, saved := range savedItems {
user := entity.FindPersonByUID(saved.UID)
user := entity.FindPersonByUID(saved.User)
if user == nil {
continue
@@ -49,7 +49,7 @@ func New(expiration time.Duration, cachePath string) *Session {
}
data := &Data{User: *user, Tokens: tokens, Shared: shared}
data := Data{User: *user, Tokens: tokens, Shares: shared}
items[key] = gc.Item{Expiration: saved.Expiration, Object: data}
}
@@ -73,7 +73,7 @@ func (s *Session) Save() error {
savedItems := make(map[string]Saved, len(items))
for key, item := range items {
saved := item.Object.(*Data).Saved()
saved := item.Object.(Data).Saved()
saved.Expiration = item.Expiration
savedItems[key] = saved
}

View File

@@ -7,23 +7,27 @@ import (
)
func (s *Session) Create(data Data) string {
token := Token()
s.cache.Set(token, &data, gc.DefaultExpiration)
id := NewID()
s.cache.Set(id, data, gc.DefaultExpiration)
log.Debugf("session: created")
if err := s.Save(); err != nil {
log.Errorf("session: %s (create)", err)
}
return token
return id
}
func (s *Session) Update(token string, data Data) error {
if _, found := s.cache.Get(token); !found {
return fmt.Errorf("session: %s not found (update)", token)
func (s *Session) Update(id string, data Data) error {
if id == "" {
return fmt.Errorf("session: empty id")
}
s.cache.Set(token, &data, gc.DefaultExpiration)
if _, found := s.cache.Get(id); !found {
return fmt.Errorf("session: %s not found (update)", id)
}
s.cache.Set(id, data, gc.DefaultExpiration)
log.Debugf("session: updated")
@@ -34,8 +38,8 @@ func (s *Session) Update(token string, data Data) error {
return nil
}
func (s *Session) Delete(token string) {
s.cache.Delete(token)
func (s *Session) Delete(id string) {
s.cache.Delete(id)
log.Debugf("session: deleted")
if err := s.Save(); err != nil {
@@ -43,16 +47,20 @@ func (s *Session) Delete(token string) {
}
}
func (s *Session) Get(token string) (data *Data) {
if hit, ok := s.cache.Get(token); ok {
return hit.(*Data)
func (s *Session) Get(id string) Data {
if id == "" {
return Data{}
}
return nil
if hit, ok := s.cache.Get(id); ok {
return hit.(Data)
}
func (s *Session) Exists(token string) bool {
_, found := s.cache.Get(token)
return Data{}
}
func (s *Session) Exists(id string) bool {
_, found := s.cache.Get(id)
return found
}

View File

@@ -15,9 +15,9 @@ func TestSession_Create(t *testing.T) {
User: entity.Admin,
}
token := s.Create(data)
t.Logf("token: %s", token)
assert.Equal(t, 48, len(token))
id := s.Create(data)
t.Logf("id: %s", id)
assert.Equal(t, 48, len(id))
}
func TestSession_Update(t *testing.T) {
@@ -27,38 +27,39 @@ func TestSession_Update(t *testing.T) {
User: entity.Admin,
}
randomToken := Token()
assert.Equal(t, 48, len(randomToken))
id := NewID()
assert.Equal(t, 48, len(id))
if result := s.Get(randomToken); result != nil {
t.Fatalf("session %s should not exist", randomToken)
if result := s.Get(id); result.Valid() {
t.Fatalf("session %s should not exist", id)
}
if err := s.Update(randomToken, data); err == nil {
t.Fatalf("update should fail for unknown token %s", randomToken)
if err := s.Update(id, data); err == nil {
t.Fatalf("update should fail for unknown session id %s", id)
}
token := s.Create(data)
assert.Equal(t, 48, len(token))
newId := s.Create(data)
assert.Equal(t, 48, len(newId))
cachedData := s.Get(token)
cachedData := s.Get(newId)
if cachedData == nil {
t.Fatalf("session %s should exist", token)
if cachedData.Invalid() {
t.Fatalf("session %s should exist", newId)
}
assert.Equal(t, *cachedData, data)
assert.Equal(t, cachedData, data)
newData := Data{
User: entity.Guest,
Shares: UIDs{"a000000000000001"},
}
if err := s.Update(token, newData); err != nil {
if err := s.Update(newId, newData); err != nil {
t.Fatalf(err.Error())
}
if cachedData := s.Get(token); cachedData == nil {
t.Fatalf("session %s should exist", token)
if cachedData := s.Get(newId); cachedData.Invalid() {
t.Fatalf("session %s should be valid", newId)
}
}
@@ -71,24 +72,25 @@ func TestSession_Get(t *testing.T) {
s := New(time.Hour, "testdata")
data := Data{
User: entity.Guest,
Shares: UIDs{"a000000000000001"},
}
token := s.Create(data)
t.Logf("token: %s", token)
assert.Equal(t, 48, len(token))
id := s.Create(data)
t.Logf("id: %s", id)
assert.Equal(t, 48, len(id))
cachedData := s.Get(token)
cachedData := s.Get(id)
if cachedData == nil {
t.Fatal("cachedData should not be nil")
if cachedData.Invalid() {
t.Fatal("cachedData should be valid")
}
assert.Equal(t, data, *cachedData)
assert.Equal(t, data, cachedData)
s.Delete(token)
s.Delete(id)
if cachedData := s.Get(token); cachedData != nil {
t.Fatal("cachedData should be nil")
if sess := s.Get(id); sess.Valid() {
t.Fatal("session should be invalid")
}
}
@@ -98,10 +100,10 @@ func TestSession_Exists(t *testing.T) {
data := Data{
User: entity.Guest,
}
token := s.Create(data)
t.Logf("token: %s", token)
assert.Equal(t, 48, len(token))
assert.True(t, s.Exists(token))
s.Delete(token)
assert.False(t, s.Exists(token))
id := s.Create(data)
t.Logf("id: %s", id)
assert.Equal(t, 48, len(id))
assert.True(t, s.Exists(id))
s.Delete(id)
assert.False(t, s.Exists(id))
}

View File

@@ -5,7 +5,7 @@ import (
"fmt"
)
func Token() string {
func NewID() string {
b := make([]byte, 24)
if _, err := rand.Read(b); err != nil {

View File

@@ -6,10 +6,10 @@ import (
"github.com/stretchr/testify/assert"
)
func TestToken(t *testing.T) {
func TestNewID(t *testing.T) {
for n := 0; n < 5; n++ {
token := Token()
t.Logf("token: %s", token)
assert.Equal(t, 48, len(token))
id := NewID()
t.Logf("id: %s", id)
assert.Equal(t, 48, len(id))
}
}