]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Enhancement: allow webUI first account signup (#9500)
authorshamoon <4887959+shamoon@users.noreply.github.com>
Sat, 29 Mar 2025 17:12:34 +0000 (10:12 -0700)
committerGitHub <noreply@github.com>
Sat, 29 Mar 2025 17:12:34 +0000 (17:12 +0000)
16 files changed:
docker/compose/docker-compose.mariadb-tika.yml
docker/compose/docker-compose.mariadb.yml
docker/compose/docker-compose.portainer.yml
docker/compose/docker-compose.postgres-tika.yml
docker/compose/docker-compose.postgres.yml
docker/compose/docker-compose.sqlite-tika.yml
docker/compose/docker-compose.sqlite.yml
docs/administration.md
docs/development.md
docs/setup.md
src/documents/context_processors.py
src/documents/templates/account/login.html
src/documents/templates/account/signup.html
src/locale/en_US/LC_MESSAGES/django.po
src/paperless/adapter.py
src/paperless/tests/test_adapter.py

index 845681cc8f9458aa7acd809cc6a033ca73e2f013..a406440e7b3b99c9d63d4c76ab02e7c0cdfdf8da 100644 (file)
@@ -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
index 9b8d57f4a9fe173c665b67d98ece038abfdbdc41..890807fa35a30e286d9265495e3a47bbc42c9eae 100644 (file)
@@ -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.
index 455b2004ea79a8d0ee9d75a02355a62df0875b46..3905309342fa7fc5f00e9a3003c11cc3870f219d 100644 (file)
 # - 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.
index dd81bd5b9b1f47b6b583a70f351421f51cc0a0f6..4b3f39801fb57b5fce1c753d91f03574be80b0e3 100644 (file)
@@ -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.
index 8212f8514402bb38bab30e8e092cced32d118a8e..f57cc283ae4b5baeb8d6b0ca114b3aee53badb24 100644 (file)
@@ -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.
index d2a74b6967fdac8e4d94e9034b4d802d044801ab..87dcb5c176dc29a5ad47a8e9c43d46210cdd21f2 100644 (file)
@@ -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.
index db63633fe883994e4ed663b8b444202d88e7f7c8..a4241e2f46e434326753b42dc9946a333448cb6f 100644 (file)
@@ -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.
index 54d918783c77e5c039d2b41c1d1c154627371cf7..bb70551418aabf80b5ec1cfdef2f2ef1456d3755 100644 (file)
@@ -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
+```
index 07eb27bd69ff4484bebad172809fc34286fbedc3..5353c164d23b772dc9c434a5b099cc95e220d4c8 100644 (file)
@@ -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/
index d132d90332338a45ee9285906ed86ff557060d60..9c75313af8f20fb3d34bb73ce7a013799b6d677c 100644 (file)
@@ -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 <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
index 2854167bc8deaa322b4335a1041944dbeac2d146..d083aaf369df7279c8818053d9f819ff295d80e1 100644 (file)
@@ -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,
     }
index e3e9ec40a4144e4461bc10251781d3cfcc5f112e..767c21d7cc072869d09b923db57e7d4a1bd12e54 100644 (file)
 {% endblock form_top_content %}
 
 {% block form_content %}
+               {% if FIRST_INSTALL %}
+                       <script type="text/javascript">
+                               // forward to the signup page if no users exist
+                               window.location.href = "{{ signup_url }}";
+                       </script>
+               {% endif %}
     {% if not DISABLE_REGULAR_LOGIN %}
         {% translate "Username" as i18n_username %}
         {% translate "Password" as i18n_password %}
index b9358bcce816cec80b59e75752bfedc683018343..9ab79d3df227f4cb3bd9fd0cae1663121ce80480 100644 (file)
@@ -6,12 +6,19 @@
 {% endblock head_title %}
 
 {% block form_top_content %}
-    <p>
-        {% blocktrans %}Already have an account? <a href="{{ login_url }}">Sign in</a>{% endblocktrans %}
-    </p>
+               {% if not FIRST_INSTALL %}
+                       <p>
+                                       {% blocktrans %}Already have an account? <a href="{{ login_url }}">Sign in</a>{% endblocktrans %}
+                       </p>
+               {% endif %}
 {% endblock form_top_content %}
 
 {% block form_content %}
+               {% if FIRST_INSTALL %}
+                       <p>
+                               {% blocktrans %}Note: This is the first user account for this installation and will be granted superuser privileges.{% endblocktrans %}
+                       </p>
+               {% endif %}
     {% translate "Username" as i18n_username %}
     {% translate "Email (optional)" as i18n_email %}
     {% translate "Password" as i18n_password1 %}
 {% endblock form_content %}
 
 {% block after_form_content %}
-  {% load allauth socialaccount %}
-  {% get_providers as socialaccount_providers %}
-  {% if socialaccount_providers %}
-      {% if not DISABLE_REGULAR_LOGIN %}
-        <p class="mt-3">{% translate "or sign in via" %}</p>
-      {% endif %}
-      <ul class="m-0 p-0">
-          {% for provider in socialaccount_providers %}
-              {% if provider.id == "openid" %}
-                  {% for brand in provider.get_brands %}
-                      {% provider_login_url provider openid=brand.openid_url process=process as href %}
-                  <li class="d-grid mt-3"><a class="btn btn-secondary" href="{{ href }}">{{ brand.name }}</a></li>
-                  {% endfor %}
-              {% else %}
-              {% provider_login_url provider process=process scope=scope auth_params=auth_params as href %}
-              <li class="d-grid mt-3">
-                <form class="d-grid" method="POST" action="{{ href }}">
-                  {% csrf_token %}
-                  <button type="submit" class="btn btn-secondary">{{ provider.name }}</button>
-                </form>
-              </li>
-              {% endif %}
-          {% endfor %}
-      </ul>
-  {% endif %}
+       {% if not FIRST_INSTALL %}
+               {% load allauth socialaccount %}
+               {% get_providers as socialaccount_providers %}
+               {% if socialaccount_providers %}
+                               {% if not DISABLE_REGULAR_LOGIN %}
+                                       <p class="mt-3">{% translate "or sign in via" %}</p>
+                               {% endif %}
+                               <ul class="m-0 p-0">
+                                               {% for provider in socialaccount_providers %}
+                                                               {% if provider.id == "openid" %}
+                                                                               {% for brand in provider.get_brands %}
+                                                                                               {% provider_login_url provider openid=brand.openid_url process=process as href %}
+                                                                               <li class="d-grid mt-3"><a class="btn btn-secondary" href="{{ href }}">{{ brand.name }}</a></li>
+                                                                               {% endfor %}
+                                                               {% else %}
+                                                               {% provider_login_url provider process=process scope=scope auth_params=auth_params as href %}
+                                                               <li class="d-grid mt-3">
+                                                                       <form class="d-grid" method="POST" action="{{ href }}">
+                                                                               {% csrf_token %}
+                                                                               <button type="submit" class="btn btn-secondary">{{ provider.name }}</button>
+                                                                       </form>
+                                                               </li>
+                                                               {% endif %}
+                                               {% endfor %}
+                               </ul>
+               {% endif %}
+       {% endif %}
 {% endblock after_form_content %}
index 45fe0df0c7eacffa6a7213764538e8d4f8a2d7d9..0c409b606497b3858e0f5f8881824888a8231458 100644 (file)
@@ -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? <a href=\"%(signup_url)s\">Sign up</a>"
 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? <a href=\"%(login_url)s\">Sign in</a>"
 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 ""
 
index 91c800cdc5677b9474b9c87e2574dcc797bb0da6..f8517a3aa1ba444494ad24ff4541484df638e677 100644 (file)
@@ -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:
index be4ad3d90814b72d7f9d356f92451fdcf8053807..b87c470964d081e0c1007c32020271efef2ae7fe 100644 (file)
@@ -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):