Repository: mathesar-foundation/mathesar

Issue:
Add superuser creation page
## Problem
A Mathesar superuser is required before a user can start using Mathesar.  The process to create a Mathesar superuser is done using the command line which is slightly complicated and varies based on the installation types. Moreover having to fiddle around with the command line to create a superuser makes it hard to provide a one-click install experience. 
 
## Proposed solution
There should be a page for creating a superuser. This page should be accessible only if a super does not exist. 

## Proposed implementation
We will be creating a super user creation page using Django templates. The login page will redirect to this superuser creation page if the super-user does not exist.




Discussion:

Resolving patch:
diff --git a/mathesar/urls.py b/mathesar/urls.py
--- a/mathesar/urls.py
+++ b/mathesar/urls.py
@@ -5,7 +5,9 @@
 from mathesar import views
 from mathesar.api.db import viewsets as db_viewsets
 from mathesar.api.ui import viewsets as ui_viewsets
+from mathesar.users.decorators import superuser_exist, superuser_must_not_exist
 from mathesar.users.password_reset import MathesarPasswordResetConfirmView
+from mathesar.users.superuser_create import SuperuserFormView
 
 db_router = routers.DefaultRouter()
 db_router.register(r'tables', db_viewsets.TableViewSet, basename='table')
@@ -42,7 +44,8 @@
     path('api/ui/v0/', include(ui_table_router.urls)),
     path('api/ui/v0/reflect/', views.reflect_all, name='reflect_all'),
     path('auth/password_reset_confirm', MathesarPasswordResetConfirmView.as_view(), name='password_reset_confirm'),
-    path('auth/login/', LoginView.as_view(redirect_authenticated_user=True), name='login'),
+    path('auth/login/', superuser_exist(LoginView.as_view(redirect_authenticated_user=True)), name='login'),
+    path('auth/create_superuser/', superuser_must_not_exist(SuperuserFormView.as_view()), name='superuser_create'),
     path('auth/', include('django.contrib.auth.urls')),
     path('', views.home, name='home'),
     path('profile/', views.profile, name='profile'),
diff --git a/mathesar/users/decorators.py b/mathesar/users/decorators.py
new file mode 100644
--- /dev/null
+++ b/mathesar/users/decorators.py
@@ -0,0 +1,30 @@
+import functools
+
+from django.contrib.auth import get_user_model
+from django.shortcuts import redirect
+from django.urls import reverse
+
+
+def superuser_must_not_exist(view_function=None):
+    """
+    Decorator for views that redirects to the home page if at least one superuser exists
+    """
+    @functools.wraps(view_function)
+    def wrapper(request, *args, **kwargs):
+        if get_user_model().objects.filter(is_superuser=True).exists():
+            return redirect(reverse('home'))
+        return view_function(request, *args, **kwargs)
+    return wrapper
+
+
+def superuser_exist(view_function=None):
+    """
+    Decorator for views that checks if at least one superuser exists
+    and redirects to the superuser create screen if none exists
+    """
+    @functools.wraps(view_function)
+    def wrapper(request, *args, **kwargs):
+        if get_user_model().objects.filter(is_superuser=True).exists():
+            return view_function(request, *args, **kwargs)
+        return redirect(reverse('superuser_create'))
+    return wrapper
diff --git a/mathesar/users/superuser_create.py b/mathesar/users/superuser_create.py
new file mode 100644
--- /dev/null
+++ b/mathesar/users/superuser_create.py
@@ -0,0 +1,32 @@
+from django.contrib.auth import get_user_model, login
+from django.contrib.auth.forms import UserCreationForm
+from django.views.generic import CreateView
+
+User = get_user_model()
+
+
+class SuperuserForm(UserCreationForm):
+    class Meta(UserCreationForm.Meta):
+        model = User
+        fields = UserCreationForm.Meta.fields
+
+    def save(self, commit=True):
+        user = super(SuperuserForm, self).save(commit=False)
+        password = self.cleaned_data["password1"]
+        user.set_password(password)
+        user.is_superuser = True
+        if commit:
+            user.save()
+        return user
+
+
+class SuperuserFormView(CreateView):
+    template_name = "registration/superuser_create.html"
+    form_class = SuperuserForm
+    success_url = "/auth/login"
+
+    def form_valid(self, form):
+        success = super().form_valid(form)
+        # Fetch the user object and login
+        login(self.request, self.object)
+        return success


Test patch:
diff --git a/mathesar/tests/api/test_user_api.py b/mathesar/tests/api/test_user_api.py
--- a/mathesar/tests/api/test_user_api.py
+++ b/mathesar/tests/api/test_user_api.py
@@ -785,3 +785,15 @@ def test_schema_role_create_multiple_roles_on_same_object(client, user_bob):
 
         assert response.status_code == 500
         assert response_data[0]['code'] == 4201
+
+
+def test_superuser_create_redirect_if_superuser_exists(client, admin_user):
+    response = client.get('/auth/create_superuser/')
+    assert response.status_code == 302
+    assert response.url == '/'
+
+
+def test_login_redirect_if_superuser_not_exists(anonymous_client):
+    response = anonymous_client.get('/auth/login/')
+    assert response.status_code == 302
+    assert response.url == '/auth/create_superuser/'
diff --git a/mathesar/tests/conftest.py b/mathesar/tests/conftest.py
--- a/mathesar/tests/conftest.py
+++ b/mathesar/tests/conftest.py
@@ -413,6 +413,12 @@ def client(admin_user):
     return client
 
 
+@pytest.fixture
+def anonymous_client():
+    client = APIClient()
+    return client
+
+
 @pytest.fixture
 def client_bob(user_bob):
     client = APIClient()