diff --git a/README.md b/README.md index b27719d..b283416 100644 --- a/README.md +++ b/README.md @@ -58,13 +58,7 @@ By default, the `docker-compose.yml` will use the latest image from GHCR. Howeve docker compose -f docker/docker-compose.yml up ``` -5. To create an admin user, run the following command and follow the prompts: - -``` -docker compose -f docker/docker-compose.yml exec web python manage.py createsuperuser -``` - -6. After creating the admin user, you will be able to log into the site using the credentials you entered. +5. Access the site on the configured port. You will be asked to setup an admin user when you first visit the site. ### Using Postgresql @@ -80,7 +74,7 @@ These instructions are for setting up the project in development mode which may 1. Download or clone this repository. 2. Make a copy of the `.env EXAMPLE` file and name it `.env.dev`. In your new copy, make sure `DEBUG` is set to 1, and change any values that are set to `CHANGEME` to the appropriate values for your development environment. -3. In the .env.dev file, add values for the following variables: `DJANGO_SUPERUSER_EMAIL` and `DJANGO_SUPERUSER_PASSWORD`. These will be used to create an admin user when the containers are started. For example: +3. (Optional) In the .env.dev file, add values for the following variables: `DJANGO_SUPERUSER_EMAIL` and `DJANGO_SUPERUSER_PASSWORD`. These will be used to create an admin user when the containers are started. For example: ``` DJANGO_SUPERUSER_EMAIL=admin@mydomain.com @@ -93,6 +87,6 @@ DJANGO_SUPERUSER_PASSWORD=CHANGEME docker compose -f docker/dev/docker-compose.dev.yml up --build ``` -4. Once the containers are running, you should be able to access the site in your web browser at `127.0.0.1:8000`. By default, the admin user will automatically be created and you will be able to log in using the credentials you entered in the .env.dev file. +4. Once the containers are running, you should be able to access the site in your web browser at `127.0.0.1:8000`. If you added environment variables for the superuser, you should be able to login with those credentials. Otherwise you will be prompted to create a super user every time to start up the server. If you would like contribute to this project, please read the [contributing guidelines](CONTRIBUTING.md) for more information. diff --git a/shifter/requirements.txt b/shifter/requirements.txt index fc947e8..ca492ee 100644 --- a/shifter/requirements.txt +++ b/shifter/requirements.txt @@ -5,4 +5,5 @@ gunicorn==23.0.0 psycopg==3.2.3 psycopg-binary==3.2.3 sqlparse==0.5.1 -typing_extensions==4.12.2 \ No newline at end of file +typing_extensions==4.12.2 +tblib==3.0.0 \ No newline at end of file diff --git a/shifter/shifter/settings.py b/shifter/shifter/settings.py index 77a1b5b..44fd9df 100644 --- a/shifter/shifter/settings.py +++ b/shifter/shifter/settings.py @@ -64,6 +64,7 @@ "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", + "shifter_auth.middleware.ensure_first_time_setup_completed", "shifter_auth.middleware.ensure_password_changed", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", diff --git a/shifter/shifter_auth/middleware.py b/shifter/shifter_auth/middleware.py index 53ac12f..a7c903e 100644 --- a/shifter/shifter_auth/middleware.py +++ b/shifter/shifter_auth/middleware.py @@ -1,7 +1,13 @@ +from django.contrib.auth import get_user_model from django.shortcuts import redirect from django.urls import reverse +def is_first_time_setup_required(): + User = get_user_model() + return User.objects.count() == 0 + + def ensure_password_changed(get_response): CHANGE_PASSWORD_URL = "shifter_auth:settings" @@ -21,3 +27,18 @@ def middleware(request): return response return middleware + + +def ensure_first_time_setup_completed(get_response): + FIRST_TIME_SETUP_URL = "shifter_auth:first-time-setup" + + def middleware(request): + if is_first_time_setup_required() and request.path != reverse( + FIRST_TIME_SETUP_URL + ): + return redirect(FIRST_TIME_SETUP_URL) + + response = get_response(request) + return response + + return middleware diff --git a/shifter/shifter_auth/templates/shifter_auth/setup.html b/shifter/shifter_auth/templates/shifter_auth/setup.html new file mode 100644 index 0000000..7b58218 --- /dev/null +++ b/shifter/shifter_auth/templates/shifter_auth/setup.html @@ -0,0 +1,43 @@ +{% extends 'base.html' %} + +{% block title %}First Time Setup | Shifter{% endblock %} + +{% block content %} +
+
+

First Time Setup

+
+
+

Welcome to Shifter!

+

First you need to create an admin user. Admins are able to create accounts for other people, and can be used as a regular user.

+
+
+
+ {% csrf_token %} + {% if form.non_field_errors %}
{{ form.non_field_errors }}
{% endif %} + +
+

{{ form.email.label }}:

+ {% if form.email.errors %}
{{ form.email.errors }}
{% endif %} +
+ + +
+

{{ form.password.label }}:

+ {% if form.password.errors %}
{{ form.password.errors }}
{% endif %} +
+ + +
+

{{ form.confirm_password.label }}:

+ {% if form.confirm_password.errors %}
{{ form.confirm_password.errors }}
{% endif %} +
+ + +
+ +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/shifter/shifter_auth/tests.py b/shifter/shifter_auth/tests.py index 76c389b..2434775 100644 --- a/shifter/shifter_auth/tests.py +++ b/shifter/shifter_auth/tests.py @@ -1,7 +1,12 @@ from django.contrib.auth import get_user, get_user_model -from django.test import Client, TestCase +from django.test import Client, RequestFactory, TestCase from django.urls import reverse +from shifter_auth.middleware import ( + ensure_first_time_setup_completed, + is_first_time_setup_required, +) + TEST_USER_EMAIL = "iama@test.com" TEST_STAFF_USER_EMAIL = "iamastaff@test.com" TEST_ADDITIONAL_USER_EMAIL = "iamalsoa@test.com" @@ -221,3 +226,43 @@ def test_new_user_not_staff(self): self.assertEqual( User.objects.filter(email=TEST_ADDITIONAL_USER_EMAIL).count(), 0 ) + + +class FirstTimeSetupTest(TestCase): + def setUp(self): + self.factory = RequestFactory() + + def test_is_first_time_setup_required_true(self): + User = get_user_model() + User.objects.all().delete() + self.assertTrue(is_first_time_setup_required()) + + def test_is_first_time_setup_required_false(self): + User = get_user_model() + User.objects.create_user(TEST_USER_EMAIL, TEST_USER_PASSWORD) + self.assertFalse(is_first_time_setup_required()) + + def test_ensure_first_time_setup_completed_redirect(self): + User = get_user_model() + User.objects.all().delete() + + middleware = ensure_first_time_setup_completed(lambda request: None) + request = self.factory.get(reverse("shifter_files:index")) + response = middleware(request) + self.assertEqual(response.status_code, 302) + self.assertEqual( + response.url, reverse("shifter_auth:first-time-setup") + ) + + def test_ensure_first_time_setup_completed_no_redirect(self): + User = get_user_model() + User.objects.create_user( + TEST_ADDITIONAL_USER_EMAIL, TEST_USER_PASSWORD + ) + middleware = ensure_first_time_setup_completed(lambda request: None) + request = self.factory.get(reverse("shifter_files:index")) + request.user = get_user_model().objects.create_user( + TEST_USER_EMAIL, TEST_USER_PASSWORD + ) + response = middleware(request) + self.assertIsNone(response) diff --git a/shifter/shifter_auth/urls.py b/shifter/shifter_auth/urls.py index 0beb0e0..e439604 100644 --- a/shifter/shifter_auth/urls.py +++ b/shifter/shifter_auth/urls.py @@ -1,7 +1,12 @@ from django.contrib.auth.views import LoginView from django.urls import path -from .views import CreateNewUserView, SettingsView, logoutView +from .views import ( + CreateNewUserView, + FirstTimeSetupView, + SettingsView, + logoutView, +) app_name = "shifter_auth" urlpatterns = [ @@ -16,4 +21,5 @@ path("logout", logoutView, name="logout"), path("settings", SettingsView.as_view(), name="settings"), path("new-user", CreateNewUserView.as_view(), name="create-new-user"), + path("setup", FirstTimeSetupView.as_view(), name="first-time-setup"), ] diff --git a/shifter/shifter_auth/views.py b/shifter/shifter_auth/views.py index 748e826..ecc934a 100644 --- a/shifter/shifter_auth/views.py +++ b/shifter/shifter_auth/views.py @@ -1,5 +1,5 @@ from django.contrib import messages -from django.contrib.auth import get_user_model, logout +from django.contrib.auth import get_user_model, login, logout from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.shortcuts import redirect @@ -8,6 +8,7 @@ from django.views.generic.edit import FormView from .forms import ChangePasswordForm, NewUserForm +from .middleware import is_first_time_setup_required @require_POST @@ -56,3 +57,36 @@ def form_valid(self, form): ) return super().form_valid(form) + + +class FirstTimeSetupView(UserPassesTestMixin, FormView): + template_name = "shifter_auth/setup.html" + form_class = NewUserForm + success_url = reverse_lazy("shifter_files:index") + permission_denied_message = "First time setup has already been completed." + + def test_func(self): + return is_first_time_setup_required() + + def form_valid(self, form): + User = get_user_model() + email = form.cleaned_data["email"] + password = form.cleaned_data["password"] + user = User.objects.create_user(email, password) + user.change_password_on_login = False + user.is_staff = True + user.is_superuser = True + user.save() + messages.add_message( + self.request, + messages.INFO, + ( + "Here you can upload one or multiple files. Once the files " + "have been uploaded, you will be given a link to share for " + "others to download." + ), + ) + + login(self.request, user) + + return super().form_valid(form) diff --git a/shifter/templates/base.html b/shifter/templates/base.html index 8fe111c..701ab33 100644 --- a/shifter/templates/base.html +++ b/shifter/templates/base.html @@ -33,20 +33,20 @@ Settings
  • + {% if user.is_staff %} + {% endif %}
  • @@ -58,7 +58,7 @@ {% else %} - {% if request.resolver_match.url_name != 'login' %} + {% if request.resolver_match.url_name != 'login' and request.resolver_match.url_name != 'first-time-setup' %} Sign In