mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
Sharing: ACL authorization for REST API #18
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
FROM photoprism/development:20200530
|
||||
FROM photoprism/development:20200625
|
||||
|
||||
# Set up project directory
|
||||
WORKDIR "/go/src/github.com/photoprism/photoprism"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"; \
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"; \
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
199
frontend/package-lock.json
generated
199
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
}},
|
||||
|
||||
@@ -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 = "/";
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
16
go.mod
@@ -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
90
go.sum
@@ -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
73
internal/acl/acl.go
Normal 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
21
internal/acl/actions.go
Normal 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"
|
||||
)
|
||||
19
internal/acl/permissions.go
Normal file
19
internal/acl/permissions.go
Normal 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},
|
||||
},
|
||||
}
|
||||
52
internal/acl/permissions_test.go
Normal file
52
internal/acl/permissions_test.go
Normal 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
25
internal/acl/resources.go
Normal 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
13
internal/acl/roles.go
Normal 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"
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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()})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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"}
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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"})
|
||||
})
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func Token() string {
|
||||
func NewID() string {
|
||||
b := make([]byte, 24)
|
||||
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user