From 32a7f9cd5a7f5797c9d570fecaa23f0b3f85365d Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sat, 29 Mar 2025 10:12:34 -0700 Subject: [PATCH] Enhancement: allow webUI first account signup (#9500) --- .../compose/docker-compose.mariadb-tika.yml | 2 - docker/compose/docker-compose.mariadb.yml | 2 - docker/compose/docker-compose.portainer.yml | 4 - .../compose/docker-compose.postgres-tika.yml | 2 - docker/compose/docker-compose.postgres.yml | 2 - docker/compose/docker-compose.sqlite-tika.yml | 2 - docker/compose/docker-compose.sqlite.yml | 2 - docs/administration.md | 8 ++ docs/development.md | 2 +- docs/setup.md | 34 ++---- src/documents/context_processors.py | 7 ++ src/documents/templates/account/login.html | 6 ++ src/documents/templates/account/signup.html | 65 +++++++----- src/locale/en_US/LC_MESSAGES/django.po | 100 ++++++++++-------- src/paperless/adapter.py | 19 ++++ src/paperless/tests/test_adapter.py | 26 +++++ 16 files changed, 163 insertions(+), 120 deletions(-) diff --git a/docker/compose/docker-compose.mariadb-tika.yml b/docker/compose/docker-compose.mariadb-tika.yml index 845681cc8..a406440e7 100644 --- a/docker/compose/docker-compose.mariadb-tika.yml +++ b/docker/compose/docker-compose.mariadb-tika.yml @@ -25,8 +25,6 @@ # and '.env' into a folder. # - Run 'docker compose pull'. # - Run 'docker compose up -d'. -# - Wait until the webserver has completed startup -# - Run 'docker compose exec webserver createsuperuser' to create a user. # # For more extensive installation and update instructions, refer to the diff --git a/docker/compose/docker-compose.mariadb.yml b/docker/compose/docker-compose.mariadb.yml index 9b8d57f4a..890807fa3 100644 --- a/docker/compose/docker-compose.mariadb.yml +++ b/docker/compose/docker-compose.mariadb.yml @@ -21,8 +21,6 @@ # and '.env' into a folder. # - Run 'docker compose pull'. # - Run 'docker compose up -d'. -# - Wait until the webserver has completed startup -# - Run 'docker compose exec webserver createsuperuser' to create a user. # # For more extensive installation and update instructions, refer to the # documentation. diff --git a/docker/compose/docker-compose.portainer.yml b/docker/compose/docker-compose.portainer.yml index 455b2004e..390530934 100644 --- a/docker/compose/docker-compose.portainer.yml +++ b/docker/compose/docker-compose.portainer.yml @@ -22,10 +22,6 @@ # - 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 -# - Run 'createsuperuser' to create a user -# - Exit the console # # For more extensive installation and update instructions, refer to the # documentation. diff --git a/docker/compose/docker-compose.postgres-tika.yml b/docker/compose/docker-compose.postgres-tika.yml index dd81bd5b9..4b3f39801 100644 --- a/docker/compose/docker-compose.postgres-tika.yml +++ b/docker/compose/docker-compose.postgres-tika.yml @@ -25,8 +25,6 @@ # and '.env' into a folder. # - Run 'docker compose pull'. # - Run 'docker compose up -d'. -# - Wait until the webserver has completed startup -# - Run 'docker compose exec webserver createsuperuser' to create a user. # # For more extensive installation and update instructions, refer to the # documentation. diff --git a/docker/compose/docker-compose.postgres.yml b/docker/compose/docker-compose.postgres.yml index 8212f8514..f57cc283a 100644 --- a/docker/compose/docker-compose.postgres.yml +++ b/docker/compose/docker-compose.postgres.yml @@ -21,8 +21,6 @@ # and '.env' into a folder. # - Run 'docker compose pull'. # - Run 'docker compose up -d'. -# - Wait until the webserver has completed startup -# - Run 'docker compose exec webserver createsuperuser' to create a user. # # For more extensive installation and update instructions, refer to the # documentation. diff --git a/docker/compose/docker-compose.sqlite-tika.yml b/docker/compose/docker-compose.sqlite-tika.yml index d2a74b696..87dcb5c17 100644 --- a/docker/compose/docker-compose.sqlite-tika.yml +++ b/docker/compose/docker-compose.sqlite-tika.yml @@ -25,8 +25,6 @@ # and '.env' into a folder. # - Run 'docker compose pull'. # - Run 'docker compose up -d'. -# - Wait until the webserver has completed startup -# - Run 'docker compose exec webserver createsuperuser' to create a user. # # For more extensive installation and update instructions, refer to the # documentation. diff --git a/docker/compose/docker-compose.sqlite.yml b/docker/compose/docker-compose.sqlite.yml index db63633fe..a4241e2f4 100644 --- a/docker/compose/docker-compose.sqlite.yml +++ b/docker/compose/docker-compose.sqlite.yml @@ -18,8 +18,6 @@ # and '.env' into a folder. # - Run 'docker compose pull'. # - Run 'docker compose up -d'. -# - Wait until the webserver has completed startup -# - Run 'docker compose exec webserver createsuperuser' to create a user. # # For more extensive installation and update instructions, refer to the # documentation. diff --git a/docs/administration.md b/docs/administration.md index 54d918783..bb7055141 100644 --- a/docs/administration.md +++ b/docs/administration.md @@ -629,3 +629,11 @@ entries created prior to this are not removed. This command allows you to prune ```shell prune_audit_logs ``` + +### Create superuser {#create-superuser} + +If you need to create a superuser, use the following command: + +```shell +createsuperuser +``` diff --git a/docs/development.md b/docs/development.md index 07eb27bd6..5353c164d 100644 --- a/docs/development.md +++ b/docs/development.md @@ -84,7 +84,7 @@ first-time setup. $ uv run pre-commit install ``` -6. Apply migrations and create a superuser for your development instance: +6. Apply migrations and create a superuser (also can be done via the web UI) for your development instance: ```bash # src/ diff --git a/docs/setup.md b/docs/setup.md index d132d9033..9c75313af 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -133,30 +133,9 @@ account. The script essentially automatically performs the steps described in [D 6. Run `docker compose up -d`. This will create and start the necessary containers. -7. Wait for the containers to complete their startup. You can monitor the logs using Docker, such as: - - ```shell-session - docker logs --follow webserver - ``` - -8. To be able to login, you will need a "superuser". To create it, - execute the following command: - - ```shell-session - docker compose exec webserver createsuperuser - ``` - - or using docker exec from within the container: - - ```shell-session - createsuperuser - ``` - - This will guide you through the superuser setup. - -9. Congratulations! Your Paperless-ngx instance should now be accessible at `http://127.0.0.1:8000` - (or similar, depending on your configuration). Use the superuser credentials you have - created in the previous step to login. +7. Congratulations! Your Paperless-ngx instance should now be accessible at `http://127.0.0.1:8000` + (or similar, depending on your configuration). When you first access the web interface, you will be + prompted to create a superuser account. ### Build the Docker image yourself {#docker_build} @@ -392,16 +371,15 @@ are released, dependency support is confirmed, etc. dependencies for Postgres or Mariadb. You can select those extras with `--extra ` or all with `--all-extras` -9. Go to `/opt/paperless/src`, and execute the following commands: +9. Go to `/opt/paperless/src`, and execute the following command: ```bash # This creates the database schema. sudo -Hu paperless python3 manage.py migrate - - # This creates your first paperless user - sudo -Hu paperless python3 manage.py createsuperuser ``` + When you first access the web interface you will be prompted to create a superuser account. + 10. Optional: Test that paperless is working by executing ```bash diff --git a/src/documents/context_processors.py b/src/documents/context_processors.py index 2854167bc..d083aaf36 100644 --- a/src/documents/context_processors.py +++ b/src/documents/context_processors.py @@ -1,5 +1,7 @@ from django.conf import settings as django_settings +from django.contrib.auth.models import User +from documents.models import Document from paperless.config import GeneralConfig @@ -25,4 +27,9 @@ def settings(request): "domain": getattr(django_settings, "PAPERLESS_URL", request.get_host()), "APP_TITLE": app_title, "APP_LOGO": app_logo, + "FIRST_INSTALL": User.objects.exclude( + username__in=["consumer", "AnonymousUser"], + ).count() + == 0 + and Document.global_objects.count() == 0, } diff --git a/src/documents/templates/account/login.html b/src/documents/templates/account/login.html index e3e9ec40a..767c21d7c 100644 --- a/src/documents/templates/account/login.html +++ b/src/documents/templates/account/login.html @@ -15,6 +15,12 @@ {% endblock form_top_content %} {% block form_content %} + {% if FIRST_INSTALL %} + + {% endif %} {% if not DISABLE_REGULAR_LOGIN %} {% translate "Username" as i18n_username %} {% translate "Password" as i18n_password %} diff --git a/src/documents/templates/account/signup.html b/src/documents/templates/account/signup.html index b9358bcce..9ab79d3df 100644 --- a/src/documents/templates/account/signup.html +++ b/src/documents/templates/account/signup.html @@ -6,12 +6,19 @@ {% endblock head_title %} {% block form_top_content %} -

- {% blocktrans %}Already have an account? Sign in{% endblocktrans %} -

+ {% if not FIRST_INSTALL %} +

+ {% blocktrans %}Already have an account? Sign in{% endblocktrans %} +

+ {% endif %} {% endblock form_top_content %} {% block form_content %} + {% if FIRST_INSTALL %} +

+ {% blocktrans %}Note: This is the first user account for this installation and will be granted superuser privileges.{% endblocktrans %} +

+ {% endif %} {% translate "Username" as i18n_username %} {% translate "Email (optional)" as i18n_email %} {% translate "Password" as i18n_password1 %} @@ -42,29 +49,31 @@ {% endblock form_content %} {% block after_form_content %} - {% load allauth socialaccount %} - {% get_providers as socialaccount_providers %} - {% if socialaccount_providers %} - {% if not DISABLE_REGULAR_LOGIN %} -

{% translate "or sign in via" %}

- {% endif %} - - {% endif %} + {% if not FIRST_INSTALL %} + {% load allauth socialaccount %} + {% get_providers as socialaccount_providers %} + {% if socialaccount_providers %} + {% if not DISABLE_REGULAR_LOGIN %} +

{% translate "or sign in via" %}

+ {% endif %} + + {% endif %} + {% endif %} {% endblock after_form_content %} diff --git a/src/locale/en_US/LC_MESSAGES/django.po b/src/locale/en_US/LC_MESSAGES/django.po index 45fe0df0c..0c409b606 100644 --- a/src/locale/en_US/LC_MESSAGES/django.po +++ b/src/locale/en_US/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: paperless-ngx\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-03-11 13:33-0700\n" +"POT-Creation-Date: 2025-03-26 21:04-0700\n" "PO-Revision-Date: 2022-02-17 04:17\n" "Last-Translator: \n" "Language-Team: English\n" @@ -1235,28 +1235,28 @@ msgstr "" msgid "Don't have an account yet? Sign up" msgstr "" -#: documents/templates/account/login.html:19 -#: documents/templates/account/signup.html:15 +#: documents/templates/account/login.html:25 +#: documents/templates/account/signup.html:22 #: documents/templates/socialaccount/signup.html:13 msgid "Username" msgstr "" -#: documents/templates/account/login.html:20 -#: documents/templates/account/signup.html:17 +#: documents/templates/account/login.html:26 +#: documents/templates/account/signup.html:24 msgid "Password" msgstr "" -#: documents/templates/account/login.html:30 +#: documents/templates/account/login.html:36 #: documents/templates/mfa/authenticate.html:23 msgid "Sign in" msgstr "" -#: documents/templates/account/login.html:34 +#: documents/templates/account/login.html:40 msgid "Forgot your password?" msgstr "" -#: documents/templates/account/login.html:45 -#: documents/templates/account/signup.html:49 +#: documents/templates/account/login.html:51 +#: documents/templates/account/signup.html:57 msgid "or sign in via" msgstr "" @@ -1335,21 +1335,27 @@ msgstr "" msgid "Paperless-ngx sign up" msgstr "" -#: documents/templates/account/signup.html:10 +#: documents/templates/account/signup.html:11 #, python-format msgid "Already have an account? Sign in" msgstr "" -#: documents/templates/account/signup.html:16 +#: documents/templates/account/signup.html:19 +msgid "" +"Note: This is the first user account for this installation and will be " +"granted superuser privileges." +msgstr "" + +#: documents/templates/account/signup.html:23 #: documents/templates/socialaccount/signup.html:14 msgid "Email (optional)" msgstr "" -#: documents/templates/account/signup.html:18 +#: documents/templates/account/signup.html:25 msgid "Password (again)" msgstr "" -#: documents/templates/account/signup.html:36 +#: documents/templates/account/signup.html:43 #: documents/templates/socialaccount/signup.html:27 msgid "Sign up" msgstr "" @@ -1578,139 +1584,139 @@ msgstr "" msgid "paperless application settings" msgstr "" -#: paperless/settings.py:723 +#: paperless/settings.py:722 msgid "English (US)" msgstr "" -#: paperless/settings.py:724 +#: paperless/settings.py:723 msgid "Arabic" msgstr "" -#: paperless/settings.py:725 +#: paperless/settings.py:724 msgid "Afrikaans" msgstr "" -#: paperless/settings.py:726 +#: paperless/settings.py:725 msgid "Belarusian" msgstr "" -#: paperless/settings.py:727 +#: paperless/settings.py:726 msgid "Bulgarian" msgstr "" -#: paperless/settings.py:728 +#: paperless/settings.py:727 msgid "Catalan" msgstr "" -#: paperless/settings.py:729 +#: paperless/settings.py:728 msgid "Czech" msgstr "" -#: paperless/settings.py:730 +#: paperless/settings.py:729 msgid "Danish" msgstr "" -#: paperless/settings.py:731 +#: paperless/settings.py:730 msgid "German" msgstr "" -#: paperless/settings.py:732 +#: paperless/settings.py:731 msgid "Greek" msgstr "" -#: paperless/settings.py:733 +#: paperless/settings.py:732 msgid "English (GB)" msgstr "" -#: paperless/settings.py:734 +#: paperless/settings.py:733 msgid "Spanish" msgstr "" -#: paperless/settings.py:735 +#: paperless/settings.py:734 msgid "Finnish" msgstr "" -#: paperless/settings.py:736 +#: paperless/settings.py:735 msgid "French" msgstr "" -#: paperless/settings.py:737 +#: paperless/settings.py:736 msgid "Hungarian" msgstr "" -#: paperless/settings.py:738 +#: paperless/settings.py:737 msgid "Italian" msgstr "" -#: paperless/settings.py:739 +#: paperless/settings.py:738 msgid "Japanese" msgstr "" -#: paperless/settings.py:740 +#: paperless/settings.py:739 msgid "Korean" msgstr "" -#: paperless/settings.py:741 +#: paperless/settings.py:740 msgid "Luxembourgish" msgstr "" -#: paperless/settings.py:742 +#: paperless/settings.py:741 msgid "Norwegian" msgstr "" -#: paperless/settings.py:743 +#: paperless/settings.py:742 msgid "Dutch" msgstr "" -#: paperless/settings.py:744 +#: paperless/settings.py:743 msgid "Polish" msgstr "" -#: paperless/settings.py:745 +#: paperless/settings.py:744 msgid "Portuguese (Brazil)" msgstr "" -#: paperless/settings.py:746 +#: paperless/settings.py:745 msgid "Portuguese" msgstr "" -#: paperless/settings.py:747 +#: paperless/settings.py:746 msgid "Romanian" msgstr "" -#: paperless/settings.py:748 +#: paperless/settings.py:747 msgid "Russian" msgstr "" -#: paperless/settings.py:749 +#: paperless/settings.py:748 msgid "Slovak" msgstr "" -#: paperless/settings.py:750 +#: paperless/settings.py:749 msgid "Slovenian" msgstr "" -#: paperless/settings.py:751 +#: paperless/settings.py:750 msgid "Serbian" msgstr "" -#: paperless/settings.py:752 +#: paperless/settings.py:751 msgid "Swedish" msgstr "" -#: paperless/settings.py:753 +#: paperless/settings.py:752 msgid "Turkish" msgstr "" -#: paperless/settings.py:754 +#: paperless/settings.py:753 msgid "Ukrainian" msgstr "" -#: paperless/settings.py:755 +#: paperless/settings.py:754 msgid "Chinese Simplified" msgstr "" -#: paperless/settings.py:756 +#: paperless/settings.py:755 msgid "Chinese Traditional" msgstr "" diff --git a/src/paperless/adapter.py b/src/paperless/adapter.py index 91c800cdc..f8517a3aa 100644 --- a/src/paperless/adapter.py +++ b/src/paperless/adapter.py @@ -10,6 +10,7 @@ from django.contrib.auth.models import User from django.forms import ValidationError from django.urls import reverse +from documents.models import Document from paperless.signals import handle_social_account_updated logger = logging.getLogger("paperless.auth") @@ -21,6 +22,13 @@ class CustomAccountAdapter(DefaultAccountAdapter): Check whether the site is open for signups, which can be disabled via the ACCOUNT_ALLOW_SIGNUPS setting. """ + if ( + User.objects.exclude(username__in=["consumer", "AnonymousUser"]).count() + == 0 + and Document.global_objects.count() == 0 + ): + # I.e. a fresh install, allow signups + return True allow_signups = super().is_open_for_signup(request) # Override with setting, otherwise default to super. return getattr(settings, "ACCOUNT_ALLOW_SIGNUPS", allow_signups) @@ -73,6 +81,17 @@ class CustomAccountAdapter(DefaultAccountAdapter): Save the user instance. Default groups are assigned to the user, if specified in the settings. """ + + if ( + User.objects.exclude(username__in=["consumer", "AnonymousUser"]).count() + == 0 + and Document.global_objects.count() == 0 + ): + # I.e. a fresh install, make the user a superuser + logger.debug(f"Creating initial superuser `{user}`") + user.is_superuser = True + user.is_staff = True + user: User = super().save_user(request, user, form, commit) group_names: list[str] = settings.ACCOUNT_DEFAULT_GROUPS if len(group_names) > 0: diff --git a/src/paperless/tests/test_adapter.py b/src/paperless/tests/test_adapter.py index be4ad3d90..b87c47096 100644 --- a/src/paperless/tests/test_adapter.py +++ b/src/paperless/tests/test_adapter.py @@ -17,6 +17,11 @@ class TestCustomAccountAdapter(TestCase): def test_is_open_for_signup(self): adapter = get_adapter() + # With no accounts, signups should be allowed + self.assertTrue(adapter.is_open_for_signup(None)) + + User.objects.create_user("testuser") + # Test when ACCOUNT_ALLOW_SIGNUPS is True settings.ACCOUNT_ALLOW_SIGNUPS = True self.assertTrue(adapter.is_open_for_signup(None)) @@ -101,6 +106,27 @@ class TestCustomAccountAdapter(TestCase): self.assertTrue(user.groups.filter(name="group1").exists()) self.assertFalse(user.groups.filter(name="group2").exists()) + def test_fresh_install_save_creates_superuser(self): + adapter = get_adapter() + form = mock.Mock( + cleaned_data={ + "username": "testuser", + "email": "user@paperless-ngx.com", + }, + ) + user = adapter.save_user(HttpRequest(), User(), form, commit=True) + self.assertTrue(user.is_superuser) + + # Next time, it should not create a superuser + form = mock.Mock( + cleaned_data={ + "username": "testuser2", + "email": "user2@paperless-ngx.com", + }, + ) + user2 = adapter.save_user(HttpRequest(), User(), form, commit=True) + self.assertFalse(user2.is_superuser) + class TestCustomSocialAccountAdapter(TestCase): def test_is_open_for_signup(self):