Enhancement: allow webUI first account signup (#9500)

This commit is contained in:
shamoon 2025-03-29 10:12:34 -07:00 committed by GitHub
parent b4b2a92225
commit 32a7f9cd5a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 163 additions and 120 deletions

View File

@ -25,8 +25,6 @@
# and '.env' into a folder. # and '.env' into a folder.
# - Run 'docker compose pull'. # - Run 'docker compose pull'.
# - Run 'docker compose up -d'. # - 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 # For more extensive installation and update instructions, refer to the

View File

@ -21,8 +21,6 @@
# and '.env' into a folder. # and '.env' into a folder.
# - Run 'docker compose pull'. # - Run 'docker compose pull'.
# - Run 'docker compose up -d'. # - 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 # For more extensive installation and update instructions, refer to the
# documentation. # documentation.

View File

@ -22,10 +22,6 @@
# - Upload 'docker-compose.env' by clicking on 'Load variables from .env file' # - Upload 'docker-compose.env' by clicking on 'Load variables from .env file'
# - Modify the environment variables as needed # - Modify the environment variables as needed
# - Click 'Deploy the stack' and wait for it to be deployed # - 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 # For more extensive installation and update instructions, refer to the
# documentation. # documentation.

View File

@ -25,8 +25,6 @@
# and '.env' into a folder. # and '.env' into a folder.
# - Run 'docker compose pull'. # - Run 'docker compose pull'.
# - Run 'docker compose up -d'. # - 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 # For more extensive installation and update instructions, refer to the
# documentation. # documentation.

View File

@ -21,8 +21,6 @@
# and '.env' into a folder. # and '.env' into a folder.
# - Run 'docker compose pull'. # - Run 'docker compose pull'.
# - Run 'docker compose up -d'. # - 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 # For more extensive installation and update instructions, refer to the
# documentation. # documentation.

View File

@ -25,8 +25,6 @@
# and '.env' into a folder. # and '.env' into a folder.
# - Run 'docker compose pull'. # - Run 'docker compose pull'.
# - Run 'docker compose up -d'. # - 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 # For more extensive installation and update instructions, refer to the
# documentation. # documentation.

View File

@ -18,8 +18,6 @@
# and '.env' into a folder. # and '.env' into a folder.
# - Run 'docker compose pull'. # - Run 'docker compose pull'.
# - Run 'docker compose up -d'. # - 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 # For more extensive installation and update instructions, refer to the
# documentation. # documentation.

View File

@ -629,3 +629,11 @@ entries created prior to this are not removed. This command allows you to prune
```shell ```shell
prune_audit_logs prune_audit_logs
``` ```
### Create superuser {#create-superuser}
If you need to create a superuser, use the following command:
```shell
createsuperuser
```

View File

@ -84,7 +84,7 @@ first-time setup.
$ uv run pre-commit install $ 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 ```bash
# src/ # src/

View 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. 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: 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
```shell-session prompted to create a superuser account.
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.
### Build the Docker image yourself {#docker_build} ### 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>` dependencies for Postgres or Mariadb. You can select those extras with `--extra <EXTRA>`
or all with `--all-extras` 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 ```bash
# This creates the database schema. # This creates the database schema.
sudo -Hu paperless python3 manage.py migrate 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 10. Optional: Test that paperless is working by executing
```bash ```bash

View File

@ -1,5 +1,7 @@
from django.conf import settings as django_settings 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 from paperless.config import GeneralConfig
@ -25,4 +27,9 @@ def settings(request):
"domain": getattr(django_settings, "PAPERLESS_URL", request.get_host()), "domain": getattr(django_settings, "PAPERLESS_URL", request.get_host()),
"APP_TITLE": app_title, "APP_TITLE": app_title,
"APP_LOGO": app_logo, "APP_LOGO": app_logo,
"FIRST_INSTALL": User.objects.exclude(
username__in=["consumer", "AnonymousUser"],
).count()
== 0
and Document.global_objects.count() == 0,
} }

View File

@ -15,6 +15,12 @@
{% endblock form_top_content %} {% endblock form_top_content %}
{% block form_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 %} {% if not DISABLE_REGULAR_LOGIN %}
{% translate "Username" as i18n_username %} {% translate "Username" as i18n_username %}
{% translate "Password" as i18n_password %} {% translate "Password" as i18n_password %}

View File

@ -6,12 +6,19 @@
{% endblock head_title %} {% endblock head_title %}
{% block form_top_content %} {% block form_top_content %}
<p> {% if not FIRST_INSTALL %}
{% blocktrans %}Already have an account? <a href="{{ login_url }}">Sign in</a>{% endblocktrans %} <p>
</p> {% blocktrans %}Already have an account? <a href="{{ login_url }}">Sign in</a>{% endblocktrans %}
</p>
{% endif %}
{% endblock form_top_content %} {% endblock form_top_content %}
{% block form_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 "Username" as i18n_username %}
{% translate "Email (optional)" as i18n_email %} {% translate "Email (optional)" as i18n_email %}
{% translate "Password" as i18n_password1 %} {% translate "Password" as i18n_password1 %}
@ -42,29 +49,31 @@
{% endblock form_content %} {% endblock form_content %}
{% block after_form_content %} {% block after_form_content %}
{% load allauth socialaccount %} {% if not FIRST_INSTALL %}
{% get_providers as socialaccount_providers %} {% load allauth socialaccount %}
{% if socialaccount_providers %} {% get_providers as socialaccount_providers %}
{% if not DISABLE_REGULAR_LOGIN %} {% if socialaccount_providers %}
<p class="mt-3">{% translate "or sign in via" %}</p> {% if not DISABLE_REGULAR_LOGIN %}
{% endif %} <p class="mt-3">{% translate "or sign in via" %}</p>
<ul class="m-0 p-0"> {% endif %}
{% for provider in socialaccount_providers %} <ul class="m-0 p-0">
{% if provider.id == "openid" %} {% for provider in socialaccount_providers %}
{% for brand in provider.get_brands %} {% if provider.id == "openid" %}
{% provider_login_url provider openid=brand.openid_url process=process as href %} {% for brand in provider.get_brands %}
<li class="d-grid mt-3"><a class="btn btn-secondary" href="{{ href }}">{{ brand.name }}</a></li> {% provider_login_url provider openid=brand.openid_url process=process as href %}
{% endfor %} <li class="d-grid mt-3"><a class="btn btn-secondary" href="{{ href }}">{{ brand.name }}</a></li>
{% else %} {% endfor %}
{% provider_login_url provider process=process scope=scope auth_params=auth_params as href %} {% else %}
<li class="d-grid mt-3"> {% provider_login_url provider process=process scope=scope auth_params=auth_params as href %}
<form class="d-grid" method="POST" action="{{ href }}"> <li class="d-grid mt-3">
{% csrf_token %} <form class="d-grid" method="POST" action="{{ href }}">
<button type="submit" class="btn btn-secondary">{{ provider.name }}</button> {% csrf_token %}
</form> <button type="submit" class="btn btn-secondary">{{ provider.name }}</button>
</li> </form>
{% endif %} </li>
{% endfor %} {% endif %}
</ul> {% endfor %}
{% endif %} </ul>
{% endif %}
{% endif %}
{% endblock after_form_content %} {% endblock after_form_content %}

View File

@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2022-02-17 04:17\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: English\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>" msgid "Don't have an account yet? <a href=\"%(signup_url)s\">Sign up</a>"
msgstr "" msgstr ""
#: documents/templates/account/login.html:19 #: documents/templates/account/login.html:25
#: documents/templates/account/signup.html:15 #: documents/templates/account/signup.html:22
#: documents/templates/socialaccount/signup.html:13 #: documents/templates/socialaccount/signup.html:13
msgid "Username" msgid "Username"
msgstr "" msgstr ""
#: documents/templates/account/login.html:20 #: documents/templates/account/login.html:26
#: documents/templates/account/signup.html:17 #: documents/templates/account/signup.html:24
msgid "Password" msgid "Password"
msgstr "" msgstr ""
#: documents/templates/account/login.html:30 #: documents/templates/account/login.html:36
#: documents/templates/mfa/authenticate.html:23 #: documents/templates/mfa/authenticate.html:23
msgid "Sign in" msgid "Sign in"
msgstr "" msgstr ""
#: documents/templates/account/login.html:34 #: documents/templates/account/login.html:40
msgid "Forgot your password?" msgid "Forgot your password?"
msgstr "" msgstr ""
#: documents/templates/account/login.html:45 #: documents/templates/account/login.html:51
#: documents/templates/account/signup.html:49 #: documents/templates/account/signup.html:57
msgid "or sign in via" msgid "or sign in via"
msgstr "" msgstr ""
@ -1335,21 +1335,27 @@ msgstr ""
msgid "Paperless-ngx sign up" msgid "Paperless-ngx sign up"
msgstr "" msgstr ""
#: documents/templates/account/signup.html:10 #: documents/templates/account/signup.html:11
#, python-format #, python-format
msgid "Already have an account? <a href=\"%(login_url)s\">Sign in</a>" msgid "Already have an account? <a href=\"%(login_url)s\">Sign in</a>"
msgstr "" 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 #: documents/templates/socialaccount/signup.html:14
msgid "Email (optional)" msgid "Email (optional)"
msgstr "" msgstr ""
#: documents/templates/account/signup.html:18 #: documents/templates/account/signup.html:25
msgid "Password (again)" msgid "Password (again)"
msgstr "" msgstr ""
#: documents/templates/account/signup.html:36 #: documents/templates/account/signup.html:43
#: documents/templates/socialaccount/signup.html:27 #: documents/templates/socialaccount/signup.html:27
msgid "Sign up" msgid "Sign up"
msgstr "" msgstr ""
@ -1578,139 +1584,139 @@ msgstr ""
msgid "paperless application settings" msgid "paperless application settings"
msgstr "" msgstr ""
#: paperless/settings.py:723 #: paperless/settings.py:722
msgid "English (US)" msgid "English (US)"
msgstr "" msgstr ""
#: paperless/settings.py:724 #: paperless/settings.py:723
msgid "Arabic" msgid "Arabic"
msgstr "" msgstr ""
#: paperless/settings.py:725 #: paperless/settings.py:724
msgid "Afrikaans" msgid "Afrikaans"
msgstr "" msgstr ""
#: paperless/settings.py:726 #: paperless/settings.py:725
msgid "Belarusian" msgid "Belarusian"
msgstr "" msgstr ""
#: paperless/settings.py:727 #: paperless/settings.py:726
msgid "Bulgarian" msgid "Bulgarian"
msgstr "" msgstr ""
#: paperless/settings.py:728 #: paperless/settings.py:727
msgid "Catalan" msgid "Catalan"
msgstr "" msgstr ""
#: paperless/settings.py:729 #: paperless/settings.py:728
msgid "Czech" msgid "Czech"
msgstr "" msgstr ""
#: paperless/settings.py:730 #: paperless/settings.py:729
msgid "Danish" msgid "Danish"
msgstr "" msgstr ""
#: paperless/settings.py:731 #: paperless/settings.py:730
msgid "German" msgid "German"
msgstr "" msgstr ""
#: paperless/settings.py:732 #: paperless/settings.py:731
msgid "Greek" msgid "Greek"
msgstr "" msgstr ""
#: paperless/settings.py:733 #: paperless/settings.py:732
msgid "English (GB)" msgid "English (GB)"
msgstr "" msgstr ""
#: paperless/settings.py:734 #: paperless/settings.py:733
msgid "Spanish" msgid "Spanish"
msgstr "" msgstr ""
#: paperless/settings.py:735 #: paperless/settings.py:734
msgid "Finnish" msgid "Finnish"
msgstr "" msgstr ""
#: paperless/settings.py:736 #: paperless/settings.py:735
msgid "French" msgid "French"
msgstr "" msgstr ""
#: paperless/settings.py:737 #: paperless/settings.py:736
msgid "Hungarian" msgid "Hungarian"
msgstr "" msgstr ""
#: paperless/settings.py:738 #: paperless/settings.py:737
msgid "Italian" msgid "Italian"
msgstr "" msgstr ""
#: paperless/settings.py:739 #: paperless/settings.py:738
msgid "Japanese" msgid "Japanese"
msgstr "" msgstr ""
#: paperless/settings.py:740 #: paperless/settings.py:739
msgid "Korean" msgid "Korean"
msgstr "" msgstr ""
#: paperless/settings.py:741 #: paperless/settings.py:740
msgid "Luxembourgish" msgid "Luxembourgish"
msgstr "" msgstr ""
#: paperless/settings.py:742 #: paperless/settings.py:741
msgid "Norwegian" msgid "Norwegian"
msgstr "" msgstr ""
#: paperless/settings.py:743 #: paperless/settings.py:742
msgid "Dutch" msgid "Dutch"
msgstr "" msgstr ""
#: paperless/settings.py:744 #: paperless/settings.py:743
msgid "Polish" msgid "Polish"
msgstr "" msgstr ""
#: paperless/settings.py:745 #: paperless/settings.py:744
msgid "Portuguese (Brazil)" msgid "Portuguese (Brazil)"
msgstr "" msgstr ""
#: paperless/settings.py:746 #: paperless/settings.py:745
msgid "Portuguese" msgid "Portuguese"
msgstr "" msgstr ""
#: paperless/settings.py:747 #: paperless/settings.py:746
msgid "Romanian" msgid "Romanian"
msgstr "" msgstr ""
#: paperless/settings.py:748 #: paperless/settings.py:747
msgid "Russian" msgid "Russian"
msgstr "" msgstr ""
#: paperless/settings.py:749 #: paperless/settings.py:748
msgid "Slovak" msgid "Slovak"
msgstr "" msgstr ""
#: paperless/settings.py:750 #: paperless/settings.py:749
msgid "Slovenian" msgid "Slovenian"
msgstr "" msgstr ""
#: paperless/settings.py:751 #: paperless/settings.py:750
msgid "Serbian" msgid "Serbian"
msgstr "" msgstr ""
#: paperless/settings.py:752 #: paperless/settings.py:751
msgid "Swedish" msgid "Swedish"
msgstr "" msgstr ""
#: paperless/settings.py:753 #: paperless/settings.py:752
msgid "Turkish" msgid "Turkish"
msgstr "" msgstr ""
#: paperless/settings.py:754 #: paperless/settings.py:753
msgid "Ukrainian" msgid "Ukrainian"
msgstr "" msgstr ""
#: paperless/settings.py:755 #: paperless/settings.py:754
msgid "Chinese Simplified" msgid "Chinese Simplified"
msgstr "" msgstr ""
#: paperless/settings.py:756 #: paperless/settings.py:755
msgid "Chinese Traditional" msgid "Chinese Traditional"
msgstr "" msgstr ""

View File

@ -10,6 +10,7 @@ from django.contrib.auth.models import User
from django.forms import ValidationError from django.forms import ValidationError
from django.urls import reverse from django.urls import reverse
from documents.models import Document
from paperless.signals import handle_social_account_updated from paperless.signals import handle_social_account_updated
logger = logging.getLogger("paperless.auth") logger = logging.getLogger("paperless.auth")
@ -21,6 +22,13 @@ class CustomAccountAdapter(DefaultAccountAdapter):
Check whether the site is open for signups, which can be Check whether the site is open for signups, which can be
disabled via the ACCOUNT_ALLOW_SIGNUPS setting. 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) allow_signups = super().is_open_for_signup(request)
# Override with setting, otherwise default to super. # Override with setting, otherwise default to super.
return getattr(settings, "ACCOUNT_ALLOW_SIGNUPS", allow_signups) 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 Save the user instance. Default groups are assigned to the user, if
specified in the settings. 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) user: User = super().save_user(request, user, form, commit)
group_names: list[str] = settings.ACCOUNT_DEFAULT_GROUPS group_names: list[str] = settings.ACCOUNT_DEFAULT_GROUPS
if len(group_names) > 0: if len(group_names) > 0:

View File

@ -17,6 +17,11 @@ class TestCustomAccountAdapter(TestCase):
def test_is_open_for_signup(self): def test_is_open_for_signup(self):
adapter = get_adapter() 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 # Test when ACCOUNT_ALLOW_SIGNUPS is True
settings.ACCOUNT_ALLOW_SIGNUPS = True settings.ACCOUNT_ALLOW_SIGNUPS = True
self.assertTrue(adapter.is_open_for_signup(None)) self.assertTrue(adapter.is_open_for_signup(None))
@ -101,6 +106,27 @@ class TestCustomAccountAdapter(TestCase):
self.assertTrue(user.groups.filter(name="group1").exists()) self.assertTrue(user.groups.filter(name="group1").exists())
self.assertFalse(user.groups.filter(name="group2").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): class TestCustomSocialAccountAdapter(TestCase):
def test_is_open_for_signup(self): def test_is_open_for_signup(self):