diff --git a/babybuddy/management/commands/createuser.py b/babybuddy/management/commands/createuser.py
new file mode 100644
index 00000000..a14ec161
--- /dev/null
+++ b/babybuddy/management/commands/createuser.py
@@ -0,0 +1,164 @@
+# -*- coding: utf-8 -*-
+"""
+Management utility to create users
+
+Example usage:
+
+ manage.py createuser \
+ --username test \
+ --email test@test.test \
+ --is-staff
+"""
+import sys
+import getpass
+
+from django.contrib.auth import get_user_model
+from django.contrib.auth.password_validation import validate_password
+from django.core import exceptions
+from django.core.management.base import BaseCommand, CommandError
+from django.db import DEFAULT_DB_ALIAS
+from django.utils.functional import cached_property
+from django.utils.text import capfirst
+
+
+class NotRunningInTTYException(Exception):
+ pass
+
+
+class Command(BaseCommand):
+ help = "Used to create a user"
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.UserModel = get_user_model()
+ self.username_field = self.UserModel._meta.get_field(
+ self.UserModel.USERNAME_FIELD
+ )
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ f"--{self.UserModel.USERNAME_FIELD}",
+ help="Specifies the login for a user.",
+ )
+ parser.add_argument(
+ "--email",
+ dest="email",
+ default="",
+ help="Specifies the email for the user. Optional.",
+ )
+ parser.add_argument(
+ "--password",
+ dest="password",
+ help="Specifies the password for the user. Optional.",
+ )
+ parser.add_argument(
+ "--is-staff",
+ dest="is_staff",
+ action="store_true",
+ default=False,
+ help="Specifies the staff status for the user. Default is False.",
+ )
+
+ def handle(self, *args, **options):
+ username = options.get(self.UserModel.USERNAME_FIELD)
+ password = options.get("password")
+
+ user_data = {}
+ user_password = ""
+ verbose_field_name = self.username_field.verbose_name
+
+ try:
+ error_msg = self._validate_username(
+ username, verbose_field_name, DEFAULT_DB_ALIAS
+ )
+ if error_msg:
+ raise CommandError(error_msg)
+
+ user_data[self.UserModel.USERNAME_FIELD] = username
+
+ # Prompt for a password interactively (if password not set via arg)
+ while password is None:
+ password = getpass.getpass()
+ password2 = getpass.getpass("Password (again): ")
+
+ if password.strip() == "":
+ self.stderr.write("Error: Blank passwords aren't allowed.")
+ password = None
+ # Don't validate blank passwords.
+ continue
+
+ if password != password2:
+ self.stderr.write("Error: Your passwords didn't match.")
+ password = None
+ password2 = None
+ # Don't validate passwords that don't match.
+ continue
+
+ try:
+ validate_password(password2, self.UserModel(**user_data))
+ except exceptions.ValidationError as err:
+ self.stderr.write("\n".join(err.messages))
+ response = input(
+ "Bypass password validation and create user anyway? [y/N]: "
+ )
+ if response.lower() != "y":
+ password = None
+ password2 = None
+ continue
+
+ user_password = password
+
+ user = self.UserModel._default_manager.db_manager(
+ DEFAULT_DB_ALIAS
+ ).create_user(**user_data, password=user_password)
+ user.email = options.get("email")
+ user.is_staff = options.get("is_staff")
+ user.save()
+
+ if options.get("verbosity") > 0:
+ self.stdout.write(f"User {username} created successfully.")
+
+ except KeyboardInterrupt:
+ self.stderr.write("\nOperation cancelled.")
+ sys.exit(1)
+ except exceptions.ValidationError as e:
+ raise CommandError("; ".join(e.messages))
+ except NotRunningInTTYException:
+ self.stdout.write(
+ "User creation skipped due to not running in a TTY. "
+ "You can run `manage.py createuser` in your project "
+ "to create one manually."
+ )
+
+ @cached_property
+ def username_is_unique(self):
+ """
+ Check if username is unique.
+ """
+ if self.username_field.unique:
+ return True
+ return any(
+ len(unique_constraint.fields) == 1
+ and unique_constraint.fields[0] == self.username_field.name
+ for unique_constraint in self.UserModel._meta.total_unique_constraints
+ )
+
+ def _validate_username(self, username, verbose_field_name, database):
+ """
+ Validate username. If invalid, return a string error message.
+ """
+ if self.username_is_unique:
+ try:
+ self.UserModel._default_manager.db_manager(database).get_by_natural_key(
+ username
+ )
+ except self.UserModel.DoesNotExist:
+ pass
+ else:
+ return f"Error: The {verbose_field_name} is already taken."
+ if not username:
+ return f"{capfirst(verbose_field_name)} cannot be blank."
+ try:
+ self.username_field.clean(username, None)
+ except exceptions.ValidationError as e:
+ return "; ".join(e.messages)
diff --git a/babybuddy/tests/tests_commands.py b/babybuddy/tests/tests_commands.py
index ccdb70cd..43c18461 100644
--- a/babybuddy/tests/tests_commands.py
+++ b/babybuddy/tests/tests_commands.py
@@ -22,3 +22,24 @@ class CommandsTestCase(TransactionTestCase):
call_command("reset", verbosity=0, interactive=False)
self.assertIsInstance(User.objects.get(username="admin"), User)
self.assertEqual(Child.objects.count(), 1)
+
+ def test_createuser(self):
+ call_command(
+ "createuser",
+ username="test",
+ email="test@test.test",
+ password="test",
+ verbosity=0,
+ )
+ self.assertIsInstance(User.objects.get(username="test"), User)
+ self.assertFalse(User.objects.filter(username="test", is_staff=True))
+ call_command(
+ "createuser",
+ "--is-staff",
+ username="testadmin",
+ email="testadmin@testadmin.testadmin",
+ password="test",
+ verbosity=0,
+ )
+ self.assertIsInstance(User.objects.get(username="testadmin"), User)
+ self.assertTrue(User.objects.filter(username="testadmin", is_staff=True))
diff --git a/docs/user-guide/managing-users.md b/docs/user-guide/managing-users.md
index a105455b..0f2667ed 100644
--- a/docs/user-guide/managing-users.md
+++ b/docs/user-guide/managing-users.md
@@ -17,3 +17,39 @@
+
+## Creating a User from the Command Line
+
+There are 2 ways you can create a user from the command line:
+
+1. Passing user's password as an argument:
+
+```shell
+python manage.py createuser --username --password
+```
+
+2. Interactively setting user's password:
+
+```shell
+python manage.py createuser --username
+```
+
+You will then be prompted to enter and confirm a password.
+
+- If you want to make the user a staff, you can append the `--is-staff` argument:
+
+```shell
+python manage.py createuser --username --is-staff
+```
+
+- Another argument you can use with this command is `--email`
+
+```shell
+python manage.py createuser --username --email
+```
+
+- To get a list of supported commands:
+
+```shell
+python manage.py createuser --help
+```