Employee Portal Dashboard
Introduction
I created the employee dashboard as a platform for internal company efficiency apps. The dashboard was deployed to host various applications, including an inventory app, an expense reimbursement application, an employee training application, and an internal auditable ChatGPT AI tool with censoring features for security.
The employee dashboard serves as a foundation for user management, featuring individual profiles with privacy policies, profile picture management using Cropper.js, and tools for auditing and updating personal information. It handles session management, password changes, and enforced password updates.
The main page displays icons for all accessible apps, with notification bubbles indicating actions required by the user. The web application is fully mobile-responsive, allowing employees to access it from various devices. I also planned to add a time management system and a PTO system as the fifth app in the lineup.
Features
Security
Django is an excellent web framework that comes with robust security features out of the box. It provides a strong user authentication system that securely manages user sessions and passwords. Django has a robust permissions system that allows you to group users and restrict resources. It also includes Cross-Site Scripting (XSS) protections by automatically escaping special characters in templates to prevent XSS attacks. Additionally, Django has Cross-Site Request Forgery protection (CSRF) and guards against SQL injection using its ORM database system, which parameterizes queries. Django also includes clickjacking protection by setting frames to prevent clickjacking. Lastly, Django hashes and salts user passwords for secure storage.
To add to the strong security built in, I added two features, ForcePasswordChangeMiddleware
and ProtectedServeView
.
Force Password Change Middleware
Middleware in Django is a series of hooks into Django's request/response processing. It's a way to plug in extra functionality to process requests before they reach the view or process responses before they're sent back to the client. Middleware classes process requests in the order they are defined, before the view gets called, and process responses in reverse order, after the view is called.
The ForcePasswordChangeMiddleware
is a custom middleware that forces users to change their password under certain conditions. Here's how it works:
-
__init__(self, get_response)
: This method simply stores the get_response callable for later use. -
__call__(self, request)
: This method is called on each request. It first checks if the user is authenticated and if thechange_password
attribute of the user isTrue
. If both conditions are met, it checks if the current request path is not the password change page or the logout page. If it is not, it constructs a URL for the password change page, appending the current path as a query parameter. This URL is then used to redirect the user to the password change page. If any of the conditions are not met, it simply calls theget_response
callable with the request, allowing the request to proceed to the next middleware or the view.
class ForcePasswordChangeMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.user.is_authenticated and request.user.change_password:
if not request.path.startswith(
reverse("core:force_password_change")
) and not request.path.startswith(reverse("logout")):
url = f"{reverse('core:force_password_change')}?next={request.path}"
return redirect(url)
return self.get_response(request)
Protected Serve View
In Django, a view is a Python function or class that takes a web request and returns a web response. This response can be the HTML contents of a document, a redirect, a 404 error, an XML document, an image, or anything else you can think of. The view itself contains whatever arbitrary logic is necessary to return that response.
Views in Django can be written either as functions or as classes. Class-based views were introduced to perform common tasks in a more reusable way. They are organized around the idea of providing a set of common case solutions to the problem of turning a URL into a web page.
The ProtectedServeView
is a class-based view that serves files from the MEDIA_ROOT
directory, but with an added layer of security. It checks if the user has permission to access the requested file. This is done by checking if the file is in the 'profile_pics' folder, if the file is the user's profile picture, or if the file is the default profile picture. If none of these conditions are met, the view raises a 404 error, effectively denying access to the file.
The serve_file
method serves the file if it exists, using different methods depending on whether the application is running in production or development. In production, it uses the X-Sendfile
header, which allows the web server to serve the file directly, bypassing Django. In development, it uses Django's FileResponse
, which reads the file and streams it to the client.
I made the ProtectedServeView
a class, so I could easily extend it in other apps for the dashboard to create custom file security for each app's file needs. For the core app, my goal was to ensure that non-users of the application can't retrieve images of employees that work at the company. This is a great example of how class-based views can be used to create reusable, extendable functionality.
@method_decorator(login_required, name="dispatch")
class ProtectedServeView(View):
def get(self, request, path):
absolute_path = os.path.join(settings.MEDIA_ROOT, path)
# Check if the path is within the 'profile_pics' folder, the user's picture, or default
is_profile_folder = absolute_path.startswith(
os.path.join(settings.MEDIA_ROOT, "core/profile_pics", "")
)
is_owned_by_user = os.path.basename(absolute_path) == os.path.basename(
request.user.profile_picture.name
)
is_default = request.user.profile_picture.name == "core/defaultProfilePic.png"
# Check if the file is owned by the user or is the default picture
if (is_default and is_owned_by_user) or (
is_owned_by_user and is_profile_folder
):
return self.serve_file(absolute_path)
# If conditions are not met, forbid access
raise Http404("You don't have permission to access this file.")
def serve_file(self, FilePath):
if os.path.exists(FilePath):
if "prod" in settings.SETTINGS_MODULE:
return self.serve_prod(FilePath) # Uses X-Sendfile
else:
return self.serve_dev(FilePath) # Uses FileResponse
raise Http404("You don't have permission to access this file.")
def serve_prod(self, FilePath):
# Guess the content type of the file based on its extension
content_type, encoding = mimetypes.guess_type(FilePath)
content_type = (
content_type or "application/octet-stream"
) # Fallback to generic binary stream
# Production serving using X-Sendfile
response = HttpResponse(content_type=content_type)
if encoding:
response["Content-Encoding"] = encoding
response["X-Sendfile"] = FilePath
return response
def serve_dev(self, FilePath):
# Guess the content type of the file based on its extension
content_type, encoding = mimetypes.guess_type(FilePath)
content_type = (
content_type or "application/octet-stream"
) # Fallback to generic binary stream
# Development serving using Django's FileResponse
return FileResponse(open(FilePath, "rb"), content_type=content_type)
User Information Update Form
This is the Django form I built for the user information update modal displayed in the slideshow.
class UserProfileChangeForm(UserChangeForm):
class Meta(UserChangeForm.Meta):
model = CustomUser
fields = [
"phone_number",
"address1",
"address2",
"city",
"state",
"zipcode",
"country",
"birthday",
]
widgets = {
"phone_number": forms.TextInput(
attrs={
"type": "tel",
"pattern": "[0-9]{3}-[0-9]{3}-[0-9]{4}",
"title": "Enter a valid phone number",
"class": "form-control",
"placeholder": "123-456-7890",
}
),
"address1": forms.TextInput(
attrs={"class": "form-control", "placeholder": "1234 Main St"}
),
"address2": forms.TextInput(
attrs={
"class": "form-control",
"placeholder": "Apartment, studio, or floor",
}
),
"city": forms.TextInput(
attrs={"class": "form-control", "placeholder": "City"}
),
"state": forms.TextInput(
attrs={"class": "form-control", "placeholder": "State"}
),
"zipcode": forms.TextInput(
attrs={
"class": "form-control",
"pattern": "\\d{5}(-\\d{4})?",
"title": "Enter a valid zip code",
"placeholder": "Zipcode",
}
),
"country": forms.Select(attrs={"class": "form-select"}),
"birthday": forms.DateInput(
attrs={"type": "date", "class": "form-control"}
),
}
Profile Picture Cropper
Cropper.js is a JavaScript library for cropping image. With the Cropper.js, you can select an area of an image, and then crop the image to that selected area. It provides a variety of options to customize the cropping behavior such as aspect ratio, minimum/maximum crop box size, etc. It also provides methods to get the cropped area in the image and the result cropped image.
The profileSettingsPicture
function in my code handles the POST request when a user submits a new profile picture.
-
It first checks if the
croppedImage
field exists in the POST data. This field should contain the base64 encoded string of the cropped image from the client-side Cropper.js. -
If the
croppedImage
field exists, it tries to split the base64 string into the image format and the actual image data. If the splitting fails, it returns a JSON response with an error message. -
It then tries to decode the base64 image data into bytes. If the decoding fails, it returns a JSON response with an error message.
-
It opens the image with Pillow, a Python imaging library, and compresses the image. If the image format is PNG, it saves the image with PNG format; otherwise, it saves the image with JPEG format.
-
It then creates a
ContentFile
object with the compressed image data and a randomly generated file name. -
Before saving the new profile picture, it checks if the user has an existing profile picture that isn't the default one. If so, it deletes the old picture.
-
It assigns the new profile picture to the form instance and checks if the form is valid. If the form is valid, it saves the form, sends a success message, and returns a JSON response with a success status. If the form is not valid, it returns a JSON response with an error status and the form errors.
-
If the
croppedImage
field does not exist in the POST data, it sends an error message and redirects to thecore:profileSettingsPicture
view. -
If the request method is not POST, it initializes the form with the current user's instance and renders the
profile/profileSettings_profilepic.html
template with the form in the context.
@login_required
def profileSettingsPicture(request):
if request.method == "POST":
# Check if 'croppedImage' exists in request.POST
if "croppedImage" in request.POST:
form = ProfilePictureForm(
request.POST, request.FILES, instance=request.user
)
try:
# Convert base64 to an ImageField-friendly format
format, imgstr = request.POST["croppedImage"].split(";base64,")
except ValueError:
return JsonResponse(
{"status": "error", "errors": "Invalid image data."}, status=400
)
ext = format.split("/")[-1]
# Ensure ext has a value; if not, default to 'jpg'
if not ext:
ext = "jpg"
try:
# Convert the base64 string to bytes
image_bytes = base64.b64decode(imgstr)
except binascii.Error:
return JsonResponse(
{"status": "error", "errors": "Invalid base64 data."}, status=400
)
# Open the image with Pillow
image = Image.open(BytesIO(image_bytes))
# Optional: Resize or other operations can be done here
# image = ImageOps.fit(image, (desired_width, desired_height), Image.ANTIALIAS)
# Compress the image
output = BytesIO()
if ext == "png":
image.save(output, format="PNG", optimize=True)
else:
image.save(
output, format="JPEG", quality=85
) # adjust quality as needed
data = ContentFile(output.getvalue(), name=f"{uuid.uuid4()}.{ext}")
# Check if the user has an existing profile picture that isn't the default one
default_picture_path = "defaultProfilePic.png"
if (
request.user.profile_picture
and request.user.profile_picture.name != default_picture_path
):
# If so, delete the old picture
request.user.profile_picture.delete(save=False)
form.instance.profile_picture = data
if form.is_valid():
form.save()
messages.success(request, "Your profile picture has been updated!")
return JsonResponse(
{"status": "success", "message": "Profile picture updated."}
)
else:
return JsonResponse(
{"status": "error", "errors": form.errors}, status=400
)
else:
messages.error(
request, "The cropped image was not submitted. Please try again."
)
return redirect("core:profileSettingsPicture")
else:
form = ProfilePictureForm(instance=request.user)
context = {
"form": form,
}
return render(request, "profile/profileSettings_profilepic.html", context)
Testing
Testing is an integral part of my development process. Here are some of the many tests I have written for the employee portal dashboard.
These tests check to ensure that the landing page is hosted at the right URL and that the images are rendering correctly.
class LandingPageTests(StaticLiveServerTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
options = Options()
options.add_argument("--headless") # Using headless mode
cls.selenium = webdriver.Firefox(
options=options
) # Using Firefox WebDriver with options
cls.selenium.implicitly_wait(10)
@classmethod
def tearDownClass(cls):
cls.selenium.quit()
super().tearDownClass()
def test_url_exists_at_correct_location(self):
response = self.client.get("/")
self.assertEqual(response.status_code, 200)
def test_landing_view_name(self):
response = self.client.get(reverse("landing"))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "landing.html")
def test_logo_rendering(self):
# Load The Landing Page
self.selenium.get(self.live_server_url + "")
# Wait for the image to load and check if it's displayed
WebDriverWait(self.selenium, 10).until(
EC.presence_of_element_located(
(By.CSS_SELECTOR, "img[src*='core/logoshort.png']")
)
)
img = self.selenium.find_element(
By.CSS_SELECTOR, "img[src*='core/logoshort.png']"
)
self.assertTrue(img.is_displayed())
def test_login_icon_rendering(self):
# Load The Landing Page
self.selenium.get(self.live_server_url + "")
# Wait for the image to load and check if it's displayed
WebDriverWait(self.selenium, 10).until(
EC.presence_of_element_located(
(By.CSS_SELECTOR, "img[src*='core/login.png']")
)
)
img = self.selenium.find_element(By.CSS_SELECTOR, "img[src*='core/login.png']")
self.assertTrue(img.is_displayed())
def test_background_image_rendering(self):
# Load The Landing Page
self.selenium.get(self.live_server_url + "")
# Get the 'background-image' CSS property of the 'body' element
background_image_css = self.selenium.find_element(
By.TAG_NAME, "body"
).value_of_css_property("background-image")
# Check if 'core/loginbackground.jpeg' is in the 'background-image' URL
self.assertIn("core/loginbackground.jpeg", background_image_css)
The tests below are some of the test cases I used to validate the Dark mode feature.
class ToggleDarkModeViewTests(TestCase):
def setUp(self):
self.client = Client()
self.user = get_user_model().objects.create_user(
username="testuser",
password="testpassword",
dark_mode=False, # assuming this field exists in your User model
)
self.admin = get_user_model().objects.create_superuser(
username="admin",
password="adminpassword",
dark_mode=False, # assuming this field exists in your User model
)
self.toggle_dark_mode_url = reverse(
"core:toggle_dark_mode"
) # assuming this URL name exists in your project
def test_toggle_dark_mode_GET_authenticated(self):
self.client.login(username="testuser", password="testpassword")
response = self.client.get(self.toggle_dark_mode_url)
# Fetch the user again from the database
user = get_user_model().objects.get(username="testuser")
# Check that the user's dark_mode field has been toggled
self.assertEqual(user.dark_mode, True)
# The response should be a redirect
self.assertEqual(response.status_code, 302)
def test_toggle_dark_mode_GET_unauthenticated(self):
response = self.client.get(self.toggle_dark_mode_url)
# Unauthenticated users should be redirected to login page
self.assertNotEqual(response.status_code, 200)
self.assertEqual(response.status_code, 302)
def test_toggle_dark_mode_admin(self):
self.client.login(username="admin", password="adminpassword")
response = self.client.get(self.toggle_dark_mode_url)
# Fetch the admin again from the database
admin = get_user_model().objects.get(username="admin")
# Check that the admin's dark_mode field has been toggled
self.assertEqual(admin.dark_mode, True)
# The response should be a redirect
self.assertEqual(response.status_code, 302)
Sources:
- JavaScript Image Cropper - Cropper.js
- Python Web Development Framework - Django
- Build Fast Responsive Sites - Bootstrap
- Django Middleware
- Django for Beginners: Build websites with Python and Django
- Django for Professionals: Production websites with Python & Django
- Django for APIs: Build web APIs with Python and Django