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 af958421a..8716a9fd6 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -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 b23d52d48..3357ffc45 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -253,6 +253,10 @@ src/app/app.component.ts 87 + + src/app/components/admin/trash/trash.component.ts + 118 + src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html 37 @@ -520,6 +524,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 +584,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 +592,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 +720,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 +1111,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 @@ -1468,11 +1484,11 @@ src/app/components/admin/trash/trash.component.ts - 57 + 59 src/app/components/admin/trash/trash.component.ts - 86 + 88 src/app/components/admin/users-groups/users-groups.component.html @@ -1707,7 +1723,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 +1735,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 @@ -2208,11 +2224,11 @@ Confirm delete src/app/components/admin/trash/trash.component.ts - 53 + 55 src/app/components/admin/trash/trash.component.ts - 80 + 82 src/app/components/manage/management-list/management-list.component.ts @@ -2227,18 +2243,18 @@ This operation will permanently delete this document. src/app/components/admin/trash/trash.component.ts - 54 + 56 This operation cannot be undone. src/app/components/admin/trash/trash.component.ts - 55 + 57 src/app/components/admin/trash/trash.component.ts - 84 + 86 src/app/components/admin/users-groups/users-groups.component.ts @@ -2273,14 +2289,14 @@ Document deleted src/app/components/admin/trash/trash.component.ts - 64 + 66 Error deleting document src/app/components/admin/trash/trash.component.ts - 69 + 71 src/app/components/document-detail/document-detail.component.ts @@ -2291,56 +2307,56 @@ This operation will permanently delete the selected documents. src/app/components/admin/trash/trash.component.ts - 82 + 84 This operation will permanently delete all documents in the trash. src/app/components/admin/trash/trash.component.ts - 83 + 85 Document(s) deleted src/app/components/admin/trash/trash.component.ts - 94 + 96 Error deleting document(s) src/app/components/admin/trash/trash.component.ts - 101 + 103 Document restored src/app/components/admin/trash/trash.component.ts - 113 + 116 Error restoring document src/app/components/admin/trash/trash.component.ts - 117 + 126 Document(s) restored src/app/components/admin/trash/trash.component.ts - 127 + 136 Error restoring document(s) src/app/components/admin/trash/trash.component.ts - 133 + 142 @@ -2518,7 +2534,7 @@ src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts - 159 + 173 @@ -2921,21 +2937,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 @@ -3728,7 +3744,7 @@ src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 18 + 20 @@ -4271,7 +4287,7 @@ src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 8 + 10 @@ -4282,7 +4298,7 @@ src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 28 + 30 @@ -4293,7 +4309,7 @@ src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 29 + 31 @@ -4331,18 +4347,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 @@ -5254,32 +5322,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 @@ -5310,14 +5382,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 @@ -5328,91 +5404,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 + 264 + + + + Error activating TOTP + + src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts + 266 + + + src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts + 272 + + + + TOTP deactivated successfully + + src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts + 288 + + + + Error deactivating TOTP + + src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts + 290 + + + src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts + 295 @@ -7351,18 +7512,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/admin/trash/trash.component.spec.ts b/src-ui/src/app/components/admin/trash/trash.component.spec.ts index c9e797a1f..9ac89d9a5 100644 --- a/src-ui/src/app/components/admin/trash/trash.component.spec.ts +++ b/src-ui/src/app/components/admin/trash/trash.component.spec.ts @@ -16,6 +16,7 @@ import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dial import { By } from '@angular/platform-browser' import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe' import { ToastService } from 'src/app/services/toast.service' +import { Router } from '@angular/router' const documentsInTrash = [ { @@ -38,6 +39,7 @@ describe('TrashComponent', () => { let trashService: TrashService let modalService: NgbModal let toastService: ToastService + let router: Router beforeEach(async () => { await TestBed.configureTestingModule({ @@ -61,6 +63,7 @@ describe('TrashComponent', () => { trashService = TestBed.inject(TrashService) modalService = TestBed.inject(NgbModal) toastService = TestBed.inject(ToastService) + router = TestBed.inject(Router) component = fixture.componentInstance fixture.detectChanges() }) @@ -161,6 +164,22 @@ describe('TrashComponent', () => { expect(restoreSpy).toHaveBeenCalledWith([1, 2]) }) + it('should offer link to restored document', () => { + let toasts + const navigateSpy = jest.spyOn(router, 'navigate') + toastService.getToasts().subscribe((allToasts) => { + toasts = [...allToasts] + }) + jest.spyOn(trashService, 'restoreDocuments').mockReturnValue(of('OK')) + component.restore(documentsInTrash[0]) + expect(toasts.length).toEqual(1) + toasts[0].action() + expect(navigateSpy).toHaveBeenCalledWith([ + 'documents', + documentsInTrash[0].id, + ]) + }) + it('should support toggle all items in view', () => { component.documentsInTrash = documentsInTrash expect(component.selectedDocuments.size).toEqual(0) diff --git a/src-ui/src/app/components/admin/trash/trash.component.ts b/src-ui/src/app/components/admin/trash/trash.component.ts index cf807e831..9364d4cce 100644 --- a/src-ui/src/app/components/admin/trash/trash.component.ts +++ b/src-ui/src/app/components/admin/trash/trash.component.ts @@ -7,6 +7,7 @@ import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dial import { Subject, takeUntil } from 'rxjs' import { SettingsService } from 'src/app/services/settings.service' import { SETTINGS_KEYS } from 'src/app/data/ui-settings' +import { Router } from '@angular/router' @Component({ selector: 'pngx-trash', @@ -26,7 +27,8 @@ export class TrashComponent implements OnDestroy { private trashService: TrashService, private toastService: ToastService, private modalService: NgbModal, - private settingsService: SettingsService + private settingsService: SettingsService, + private router: Router ) { this.reload() } @@ -110,7 +112,14 @@ export class TrashComponent implements OnDestroy { restore(document: Document) { this.trashService.restoreDocuments([document.id]).subscribe({ next: () => { - this.toastService.showInfo($localize`Document restored`) + this.toastService.show({ + content: $localize`Document restored`, + delay: 5000, + actionName: $localize`Open document`, + action: () => { + this.router.navigate(['documents', document.id]) + }, + }) this.reload() }, error: (err) => { 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/filterable-dropdown/filterable-dropdown.component.html b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html index 2ef970801..a3b49cf62 100644 --- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html +++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html @@ -35,7 +35,7 @@
@if (selectionModel.items) {
- @for (item of selectionModel.itemsSorted | filter: filterText:'name'; track item; let i = $index) { + @for (item of selectionModel.items | filter: filterText:'name'; track item; let i = $index) { @if (allowSelectNone || item.id) { @@ -45,13 +45,13 @@
} @if (editing) { - @if ((selectionModel.itemsSorted | filter: filterText:'name').length === 0 && createRef !== undefined) { + @if ((selectionModel.items | filter: filterText:'name').length === 0 && createRef !== undefined) { } - @if ((selectionModel.itemsSorted | filter: filterText:'name').length > 0) { + @if ((selectionModel.items | filter: filterText:'name').length > 0) {