diff --git a/Pipfile b/Pipfile index 794af014d..335d9bff3 100644 --- a/Pipfile +++ b/Pipfile @@ -8,7 +8,7 @@ dateparser = "~=1.2" # WARNING: django does not use semver. # Only patch versions are guaranteed to not introduce breaking changes. django = "~=5.1.3" -django-allauth = {extras = ["socialaccount"], version = "*"} +django-allauth = {extras = ["mfa", "socialaccount"], version = "*"} django-auditlog = "*" django-celery-results = "*" django-compression-middleware = "*" @@ -55,7 +55,7 @@ tika-client = "*" tqdm = "*" # See https://github.com/paperless-ngx/paperless-ngx/issues/5494 uvicorn = {extras = ["standard"], version = "==0.25.0"} -watchdog = "~=5.0" +watchdog = "~=6.0" whitenoise = "~=6.8" whoosh = "~=2.7" zxing-cpp = {version = "*", platform_machine = "== 'x86_64'"} diff --git a/Pipfile.lock b/Pipfile.lock index 9377e3575..26a2c3377 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "dccf58aea1ba4c0aa4aa93c1cc13881229889db25bc6e5b2384413a7e7e85182" + "sha256": "e4cb2328c49829f56793ef25780dcc73ea8e4838e6e9bc25d1b6feb74eb3befe" }, "pipfile-spec": 6, "requires": {}, @@ -522,6 +522,7 @@ }, "django-allauth": { "extras": [ + "mfa", "socialaccount" ], "hashes": [ @@ -641,6 +642,13 @@ "markers": "python_version >= '3.7'", "version": "==1.2.2" }, + "fido2": { + "hashes": [ + "sha256:26100f226d12ced621ca6198528ce17edf67b78df4287aee1285fee3cd5aa9fc", + "sha256:6be34c0b9fe85e4911fd2d103cce7ae8ce2f064384a7a2a3bd970b3ef7702931" + ], + "version": "==1.1.3" + }, "filelock": { "hashes": [ "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", @@ -1776,6 +1784,13 @@ "index": "pypi", "version": "==0.1.9" }, + "qrcode": { + "hashes": [ + "sha256:025ce2b150f7fe4296d116ee9bad455a6643ab4f6e7dce541613a4758cbce347", + "sha256:9fc05f03305ad27a709eb742cf3097fa19e6f6f93bb9e2f039c0979190f6f1b1" + ], + "version": "==8.0" + }, "rapidfuzz": { "hashes": [ "sha256:00d02cbd75d283c287471b5b3738b3e05c9096150f93f2d2dfa10b3d700f2db9", @@ -2336,40 +2351,40 @@ }, "watchdog": { "hashes": [ - "sha256:0f9332243355643d567697c3e3fa07330a1d1abf981611654a1f2bf2175612b7", - "sha256:1021223c08ba8d2d38d71ec1704496471ffd7be42cfb26b87cd5059323a389a1", - "sha256:108f42a7f0345042a854d4d0ad0834b741d421330d5f575b81cb27b883500176", - "sha256:1e9679245e3ea6498494b3028b90c7b25dbb2abe65c7d07423ecfc2d6218ff7c", - "sha256:223160bb359281bb8e31c8f1068bf71a6b16a8ad3d9524ca6f523ac666bb6a1e", - "sha256:26dd201857d702bdf9d78c273cafcab5871dd29343748524695cecffa44a8d97", - "sha256:294b7a598974b8e2c6123d19ef15de9abcd282b0fbbdbc4d23dfa812959a9e05", - "sha256:349c9488e1d85d0a58e8cb14222d2c51cbc801ce11ac3936ab4c3af986536926", - "sha256:49f4d36cb315c25ea0d946e018c01bb028048023b9e103d3d3943f58e109dd45", - "sha256:53a3f10b62c2d569e260f96e8d966463dec1a50fa4f1b22aec69e3f91025060e", - "sha256:53adf73dcdc0ef04f7735066b4a57a4cd3e49ef135daae41d77395f0b5b692cb", - "sha256:560135542c91eaa74247a2e8430cf83c4342b29e8ad4f520ae14f0c8a19cfb5b", - "sha256:720ef9d3a4f9ca575a780af283c8fd3a0674b307651c1976714745090da5a9e8", - "sha256:752fb40efc7cc8d88ebc332b8f4bcbe2b5cc7e881bccfeb8e25054c00c994ee3", - "sha256:78864cc8f23dbee55be34cc1494632a7ba30263951b5b2e8fc8286b95845f82c", - "sha256:85527b882f3facda0579bce9d743ff7f10c3e1e0db0a0d0e28170a7d0e5ce2ea", - "sha256:90a67d7857adb1d985aca232cc9905dd5bc4803ed85cfcdcfcf707e52049eda7", - "sha256:91b522adc25614cdeaf91f7897800b82c13b4b8ac68a42ca959f992f6990c490", - "sha256:9413384f26b5d050b6978e6fcd0c1e7f0539be7a4f1a885061473c5deaa57221", - "sha256:94d11b07c64f63f49876e0ab8042ae034674c8653bfcdaa8c4b32e71cfff87e8", - "sha256:950f531ec6e03696a2414b6308f5c6ff9dab7821a768c9d5788b1314e9a46ca7", - "sha256:a2e8f3f955d68471fa37b0e3add18500790d129cc7efe89971b8a4cc6fdeb0b2", - "sha256:ae6deb336cba5d71476caa029ceb6e88047fc1dc74b62b7c4012639c0b563906", - "sha256:b8ca4d854adcf480bdfd80f46fdd6fb49f91dd020ae11c89b3a79e19454ec627", - "sha256:c66f80ee5b602a9c7ab66e3c9f36026590a0902db3aea414d59a2f55188c1f49", - "sha256:d52db5beb5e476e6853da2e2d24dbbbed6797b449c8bf7ea118a4ee0d2c9040e", - "sha256:dd021efa85970bd4824acacbb922066159d0f9e546389a4743d56919b6758b91", - "sha256:e25adddab85f674acac303cf1f5835951345a56c5f7f582987d266679979c75b", - "sha256:f00b4cf737f568be9665563347a910f8bdc76f88c2970121c86243c8cfdf90e9", - "sha256:f01f4a3565a387080dc49bdd1fefe4ecc77f894991b88ef927edbfa45eb10818" + "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", + "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", + "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", + "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", + "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", + "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", + "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", + "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", + "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", + "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa", + "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", + "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", + "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a", + "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", + "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", + "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", + "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", + "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", + "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", + "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", + "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", + "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", + "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", + "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", + "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", + "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", + "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e", + "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8", + "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c", + "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==5.0.3" + "version": "==6.0.0" }, "watchfiles": { "hashes": [ diff --git a/docker/compose/docker-compose.portainer.yml b/docker/compose/docker-compose.portainer.yml index 621908ff8..588a93c84 100644 --- a/docker/compose/docker-compose.portainer.yml +++ b/docker/compose/docker-compose.portainer.yml @@ -19,6 +19,8 @@ # # - Open portainer Stacks list and click 'Add stack' # - Paste the contents of this file and assign a name, e.g. 'paperless' +# - Upload 'docker-compose.env' by clicking on 'Load variables from .env file' +# - Modify the environment variables as needed # - Click 'Deploy the stack' and wait for it to be deployed # - Open the list of containers, select paperless_webserver_1 # - Click 'Console' and then 'Connect' to open the command line inside the container @@ -61,28 +63,8 @@ services: environment: PAPERLESS_REDIS: redis://broker:6379 PAPERLESS_DBHOST: db -# The UID and GID of the user used to run paperless in the container. Set this -# to your UID and GID on the host so that you have write access to the -# consumption directory. - USERMAP_UID: 1000 - USERMAP_GID: 100 -# Additional languages to install for text recognition, separated by a -# whitespace. Note that this is -# different from PAPERLESS_OCR_LANGUAGE (default=eng), which defines the -# language used for OCR. -# The container installs English, German, Italian, Spanish and French by -# default. -# See https://packages.debian.org/search?keywords=tesseract-ocr-&searchon=names&suite=buster -# for available languages. - #PAPERLESS_OCR_LANGUAGES: tur ces -# Adjust this key if you plan to make paperless available publicly. It should -# be a very long sequence of random characters. You don't need to remember it. - #PAPERLESS_SECRET_KEY: change-me -# Use this variable to set a timezone for the Paperless Docker containers. If not specified, defaults to UTC. - #PAPERLESS_TIME_ZONE: America/Los_Angeles -# The default language to use for OCR. Set this to the language most of your -# documents are written in. - #PAPERLESS_OCR_LANGUAGE: eng + env_file: + - stack.env volumes: data: diff --git a/docs/administration.md b/docs/administration.md index 7624de41b..8204352d8 100644 --- a/docs/administration.md +++ b/docs/administration.md @@ -241,6 +241,7 @@ document_exporter target [-c] [-d] [-f] [-na] [-nt] [-p] [-sm] [-z] optional arguments: -c, --compare-checksums +-cj, --compare-json -d, --delete -f, --use-filename-format -na, --no-archive @@ -269,7 +270,8 @@ only export changed and added files. Paperless determines whether a file has changed by inspecting the file attributes "date/time modified" and "size". If that does not work out for you, specify `-c` or `--compare-checksums` and paperless will attempt to compare file -checksums instead. This is slower. +checksums instead. This is slower. The manifest and metadata json files +are always updated, unless `cj` or `--compare-json` is specified. Paperless will not remove any existing files in the export directory. If you want paperless to also remove files that do not belong to the diff --git a/docs/api.md b/docs/api.md index ccbde9b22..c5f20edd1 100644 --- a/docs/api.md +++ b/docs/api.md @@ -556,3 +556,11 @@ Initial API version. - Consumption templates were refactored to workflows and API endpoints changed as such. + +#### Version 5 + +- Added bulk deletion methods for documents and objects. + +#### Version 6 + +- Moved acknowledge tasks endpoint to be under `/api/tasks/acknowledge/`. diff --git a/docs/usage.md b/docs/usage.md index f853bb7f5..8f22ec3eb 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -299,6 +299,12 @@ In order to enable the password reset feature you will need to setup an SMTP bac [`PAPERLESS_EMAIL_HOST`](configuration.md#PAPERLESS_EMAIL_HOST). If your installation does not have [`PAPERLESS_URL`](configuration.md#PAPERLESS_URL) set, the reset link included in emails will use the server host. +### Two-factor authentication + +Users can enable two-factor authentication (2FA) for their accounts from the 'My Profile' dialog. Opening the dropdown reveals a QR code that can be scanned by a 2FA app (e.g. Google Authenticator) to generate a code. The code must then be entered in the dialog to enable 2FA. If the code is accepted and 2FA is enabled, the user will be shown a set of 10 recovery codes that can be used to login in the event that the 2FA device is lost or unavailable. These codes should be stored securely and cannot be retrieved again. Once enabled, users will be required to enter a code from their 2FA app when logging in. + +Should a user lose access to their 2FA device and all recovery codes, a superuser can disable 2FA for the user from the 'Users & Groups' management screen. + ## Workflows !!! note diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index b9aa4e03e..3eee47eb8 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -520,6 +520,10 @@ src/app/components/admin/config/config.component.html 34 + + src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html + 124 + Discard @@ -576,7 +580,7 @@ src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.html - 43 + 57 src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html @@ -584,7 +588,7 @@ src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 99 + 184 src/app/components/document-detail/document-detail.component.html @@ -712,6 +716,14 @@ src/app/components/common/permissions-dialog/permissions-dialog.component.html 23 + + src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html + 111 + + + src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html + 127 + src/app/components/common/system-status-dialog/system-status-dialog.component.html 10 @@ -1095,7 +1107,7 @@ src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.html - 37 + 51 src/app/components/common/input/permissions/permissions-form/permissions-form.component.html @@ -1707,7 +1719,7 @@ src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.html - 42 + 56 src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html @@ -1719,7 +1731,7 @@ src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 98 + 183 src/app/components/common/select-dialog/select-dialog.component.html @@ -2514,7 +2526,7 @@ src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts - 159 + 173 @@ -2917,21 +2929,21 @@ Sidebar views updated src/app/components/app-frame/app-frame.component.ts - 208 + 209 Error updating sidebar views src/app/components/app-frame/app-frame.component.ts - 211 + 212 An error occurred while saving update checking settings. src/app/components/app-frame/app-frame.component.ts - 232 + 233 @@ -3720,7 +3732,7 @@ src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 18 + 20 @@ -4263,7 +4275,7 @@ src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 8 + 10 @@ -4274,7 +4286,7 @@ src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 28 + 30 @@ -4285,7 +4297,7 @@ src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 29 + 31 @@ -4323,18 +4335,70 @@ 30 + + Two-factor Authentication + + src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.html + 37 + + + src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html + 104 + + + src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html + 138 + + + + Disable Two-factor Authentication + + src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.html + 39 + + + src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.html + 41 + + + src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html + 169 + + + src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html + 171 + + Create new user account src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts - 44 + 49 Edit user account src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts - 48 + 53 + + + + Totp deactivated + + src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts + 109 + + + + Totp deactivation failed + + src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts + 112 + + + src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts + 117 @@ -5151,32 +5215,36 @@ Confirm Email src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 13 + 15 Confirm Password src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 23 + 25 API Auth Token src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 31 + 33 Copy src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 35 + 37 src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 42 + 44 + + + src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html + 156 src/app/components/common/share-links-dropdown/share-links-dropdown.component.html @@ -5207,14 +5275,18 @@ Regenerate auth token src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 45 + 47 Copied! src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 53 + 55 + + + src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html + 163 src/app/components/common/share-links-dropdown/share-links-dropdown.component.html @@ -5225,91 +5297,176 @@ Warning: changing the token cannot be undone src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 55 + 57 Connected social accounts src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 59 + 63 Set a password before disconnecting social account. src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 63 + 67 Disconnect src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 69 + 73 Disconnect social account src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 71 + 75 Warning: disconnecting social accounts cannot be undone src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 81 + 85 Connect new social account src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 86 + 90 + + + + Scan the QR code with your authenticator app and then enter the code below + + src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html + 115 + + + + Authenticator secret + + src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html + 118 + + + + You can store this secret and use it to reinstall your authenticator app at a later time. + + src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html + 119 + + + + Code + + src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html + 122 + + + + Recovery codes will not be shown again, make sure to save them. + + src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html + 141 + + + + Copy codes + + src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html + 159 Emails must match src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts - 108 + 121 Passwords must match src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts - 136 + 149 Profile updated successfully src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts - 156 + 170 Error saving profile src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts - 168 + 182 Error generating auth token src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts - 185 + 199 Error disconnecting social account src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts - 210 + 224 + + + + Error fetching TOTP settings + + src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts + 243 + + + + TOTP activated successfully + + src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts + 263 + + + + Error activating TOTP + + src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts + 265 + + + src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts + 271 + + + + TOTP deactivated successfully + + src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts + 287 + + + + Error deactivating TOTP + + src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts + 289 + + + src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts + 294 @@ -7248,18 +7405,32 @@ 264 + + Previous page + + src/app/components/document-list/document-list.component.ts + 280 + + + + Next page + + src/app/components/document-list/document-list.component.ts + 292 + + View "" saved successfully. src/app/components/document-list/document-list.component.ts - 300 + 324 View "" created successfully. src/app/components/document-list/document-list.component.ts - 343 + 367 diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts index 08b3348e7..a0e23193d 100644 --- a/src-ui/src/app/app.module.ts +++ b/src-ui/src/app/app.module.ts @@ -145,7 +145,6 @@ import { asterisk, braces, bodyText, - boxArrowInRight, boxArrowUp, boxArrowUpRight, boxes, @@ -186,6 +185,7 @@ import { fileEarmarkFill, fileEarmarkLock, fileEarmarkMinus, + fileEarmarkRichtext, files, fileText, filter, @@ -253,7 +253,6 @@ const icons = { asterisk, braces, bodyText, - boxArrowInRight, boxArrowUp, boxArrowUpRight, boxes, @@ -294,6 +293,7 @@ const icons = { fileEarmarkFill, fileEarmarkLock, fileEarmarkMinus, + fileEarmarkRichtext, files, fileText, filter, diff --git a/src-ui/src/app/components/app-frame/app-frame.component.spec.ts b/src-ui/src/app/components/app-frame/app-frame.component.spec.ts index 1354a187e..f440946da 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.spec.ts +++ b/src-ui/src/app/components/app-frame/app-frame.component.spec.ts @@ -343,6 +343,7 @@ describe('AppFrameComponent', () => { component.editProfile() expect(modalSpy).toHaveBeenCalledWith(ProfileEditDialogComponent, { backdrop: 'static', + size: 'xl', }) }) diff --git a/src-ui/src/app/components/app-frame/app-frame.component.ts b/src-ui/src/app/components/app-frame/app-frame.component.ts index df6ac65a2..83d927562 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.ts +++ b/src-ui/src/app/components/app-frame/app-frame.component.ts @@ -136,6 +136,7 @@ export class AppFrameComponent editProfile() { this.modalService.open(ProfileEditDialogComponent, { backdrop: 'static', + size: 'xl', }) this.closeMenu() } diff --git a/src-ui/src/app/components/app-frame/global-search/global-search.component.html b/src-ui/src/app/components/app-frame/global-search/global-search.component.html index 2f5104547..3399b4fad 100644 --- a/src-ui/src/app/components/app-frame/global-search/global-search.component.html +++ b/src-ui/src/app/components/app-frame/global-search/global-search.component.html @@ -49,7 +49,7 @@ [disabled]="disablePrimaryButton(type, item)" (mouseenter)="onButtonHover($event)"> @if (type === DataType.Document) { - +  Open } @else if (type === DataType.SavedView) { @@ -72,7 +72,7 @@  Download } @else { - +  Open } diff --git a/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.html b/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.html index ca834a3ad..a2b3db67d 100644 --- a/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.html +++ b/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.html @@ -32,6 +32,20 @@ + + @if (object?.is_mfa_enabled && currentUserIsSuperUser) { + + + + }
diff --git a/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.spec.ts b/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.spec.ts index 96a0044fe..5adaf3388 100644 --- a/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.spec.ts +++ b/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.spec.ts @@ -7,7 +7,7 @@ import { } from '@angular/forms' import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgSelectModule } from '@ng-select/ng-select' -import { of } from 'rxjs' +import { of, throwError } from 'rxjs' import { IfOwnerDirective } from 'src/app/directives/if-owner.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { GroupService } from 'src/app/services/rest/group.service' @@ -21,10 +21,15 @@ import { EditDialogMode } from '../edit-dialog.component' import { UserEditDialogComponent } from './user-edit-dialog.component' import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' +import { ToastService } from 'src/app/services/toast.service' +import { UserService } from 'src/app/services/rest/user.service' +import { PermissionsService } from 'src/app/services/permissions.service' describe('UserEditDialogComponent', () => { let component: UserEditDialogComponent let settingsService: SettingsService + let permissionsService: PermissionsService + let toastService: ToastService let fixture: ComponentFixture beforeEach(async () => { @@ -71,6 +76,8 @@ describe('UserEditDialogComponent', () => { fixture = TestBed.createComponent(UserEditDialogComponent) settingsService = TestBed.inject(SettingsService) settingsService.currentUser = { id: 99, username: 'user99' } + permissionsService = TestBed.inject(PermissionsService) + toastService = TestBed.inject(ToastService) component = fixture.componentInstance fixture.detectChanges() @@ -121,4 +128,38 @@ describe('UserEditDialogComponent', () => { component.save() expect(component.passwordIsSet).toBeTruthy() }) + + it('should support deactivation of TOTP', () => { + component.object = { id: 99, username: 'user99' } + const deactivateSpy = jest.spyOn( + component['service'] as UserService, + 'deactivateTotp' + ) + const toastErrorSpy = jest.spyOn(toastService, 'showError') + const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + deactivateSpy.mockReturnValueOnce(throwError(() => new Error('error'))) + component.deactivateTotp() + expect(deactivateSpy).toHaveBeenCalled() + expect(toastErrorSpy).toHaveBeenCalled() + + deactivateSpy.mockReturnValueOnce(of(false)) + component.deactivateTotp() + expect(deactivateSpy).toHaveBeenCalled() + expect(toastErrorSpy).toHaveBeenCalled() + + deactivateSpy.mockReturnValueOnce(of(true)) + component.deactivateTotp() + expect(deactivateSpy).toHaveBeenCalled() + expect(toastInfoSpy).toHaveBeenCalled() + }) + + it('should check superuser status of current user', () => { + expect(component.currentUserIsSuperUser).toBeFalsy() + permissionsService.initialize([], { + id: 99, + username: 'user99', + is_superuser: true, + }) + expect(component.currentUserIsSuperUser).toBeTruthy() + }) }) diff --git a/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts b/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts index baadfa541..acd327d3a 100644 --- a/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts +++ b/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts @@ -5,9 +5,11 @@ import { first } from 'rxjs' import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' import { Group } from 'src/app/data/group' import { User } from 'src/app/data/user' +import { PermissionsService } from 'src/app/services/permissions.service' import { GroupService } from 'src/app/services/rest/group.service' import { UserService } from 'src/app/services/rest/user.service' import { SettingsService } from 'src/app/services/settings.service' +import { ToastService } from 'src/app/services/toast.service' @Component({ selector: 'pngx-user-edit-dialog', @@ -20,12 +22,15 @@ export class UserEditDialogComponent { groups: Group[] passwordIsSet: boolean = false + public totpLoading: boolean = false constructor( service: UserService, activeModal: NgbActiveModal, groupsService: GroupService, - settingsService: SettingsService + settingsService: SettingsService, + private toastService: ToastService, + private permissionsService: PermissionsService ) { super(service, activeModal, service, settingsService) @@ -87,4 +92,30 @@ export class UserEditDialogComponent .length > 0 super.save() } + + get currentUserIsSuperUser(): boolean { + return this.permissionsService.isSuperUser() + } + + deactivateTotp() { + this.totpLoading = true + ;(this.service as UserService) + .deactivateTotp(this.object) + .pipe(first()) + .subscribe({ + next: (result) => { + this.totpLoading = false + if (result) { + this.toastService.showInfo($localize`Totp deactivated`) + this.object.is_mfa_enabled = false + } else { + this.toastService.showError($localize`Totp deactivation failed`) + } + }, + error: (e) => { + this.totpLoading = false + this.toastService.showError($localize`Totp deactivation failed`, e) + }, + }) + } } diff --git a/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html b/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html index 713d68864..f9d57baf3 100644 --- a/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html +++ b/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html @@ -5,94 +5,179 @@