-
Notifications
You must be signed in to change notification settings - Fork 1
Add occupation field to EditUserForm and update profile template; imp… #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| from django.contrib.auth.backends import ModelBackend | ||
| from django.contrib.auth import get_user_model | ||
|
|
||
| User = get_user_model() | ||
|
|
||
|
|
||
| class EmailBackend(ModelBackend): | ||
| """ | ||
| Custom authentication backend that authenticates users using email instead of username. | ||
| """ | ||
|
|
||
| def authenticate(self, request, email=None, password=None, **kwargs): | ||
| if email is None or password is None: | ||
| return None | ||
|
|
||
| try: | ||
| user = User.objects.get(email=email) | ||
| except User.DoesNotExist: | ||
| # Run the default password hasher once to reduce timing attacks | ||
| User().set_password(password) | ||
| return None | ||
|
|
||
| if user.check_password(password) and self.user_can_authenticate(user): | ||
| return user | ||
|
|
||
| return None | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,9 +10,10 @@ <h1 id="edit_profile_header" class="mt-10 text-xl">Welcome back, {{ user.full_na | |
| id="edit_profile_form" | ||
| hx-post="{% url 'home' %}" | ||
| hx-trigger="submit" | ||
| hx-swap="multi:#edit_profile_form:outerHTML,h1:outerHTML" | ||
| hx-swap="multi:#edit_profile_form:outerHTML,#edit_profile_header:outerHTML" | ||
| hx-ext="multi" | ||
| > | ||
| {% csrf_token %} | ||
| <div class="mt-3 max-w"> | ||
|
Comment on lines
+13
to
17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Profile template wiring for occupation and invite modal looks consistent beep boop
If you want to be extra strict with client‑side validation, you could mirror the model’s Also applies to: 49-58, 69-127 🤖 Prompt for AI Agents |
||
| <label for="input-label" class="block text-sm font-medium mb-2 dark:text-white">Email</label> | ||
| <input | ||
|
|
@@ -45,6 +46,16 @@ <h1 id="edit_profile_header" class="mt-10 text-xl">Welcome back, {{ user.full_na | |
| required | ||
| > | ||
| </div> | ||
| <div class="mt-3 max-w"> | ||
| <label for="occupation" class="block text-sm font-medium mb-2 dark:text-white">Occupation</label> | ||
| <input | ||
| type="text" | ||
| id="occupation" | ||
| name="occupation" | ||
| class="py-3 px-4 block w-full border-gray-200 rounded-lg text-sm focus:border-primary focus:ring-primary-500 disabled:opacity-50 disabled:pointer-events-none" | ||
| value="{{ form.occupation.value|default:'' }}" | ||
| > | ||
| </div> | ||
| <div class="flex justify-center"> | ||
| <button | ||
| type="submit" | ||
|
|
@@ -55,7 +66,64 @@ <h1 id="edit_profile_header" class="mt-10 text-xl">Welcome back, {{ user.full_na | |
| </div> | ||
| </form> | ||
| </div> | ||
| <!-- Invite User Link --> | ||
| <button type="button" class="mt-6 py-2 px-4 text-gray-700 hover:text-primary" style="text-decoration: underline;" onclick="document.getElementById('invite-user-modal').classList.remove('hidden')"> | ||
| Invite User | ||
| </button> | ||
| </div> | ||
| </div> | ||
|
|
||
| <!-- Invite User Modal --> | ||
| <div id="invite-user-modal" class="hidden fixed inset-0 z-[80] overflow-y-auto" role="dialog" aria-modal="true"> | ||
| <!-- Backdrop --> | ||
| <div class="fixed inset-0" style="background-color: rgba(0, 0, 0, 0.5);" onclick="document.getElementById('invite-user-modal').classList.add('hidden')"></div> | ||
|
|
||
| <!-- Modal Content --> | ||
| <div class="fixed inset-0 flex items-center justify-center p-4"> | ||
| <div class="relative bg-white rounded-xl shadow-xl w-full max-w-md"> | ||
| <div class="flex justify-between items-center py-3 px-4 border-b"> | ||
| <h3 class="font-bold text-gray-800"> | ||
| Invite User | ||
| </h3> | ||
| <button type="button" class="size-8 inline-flex justify-center items-center rounded-full border border-transparent bg-gray-100 text-gray-800 hover:bg-gray-200" onclick="document.getElementById('invite-user-modal').classList.add('hidden')"> | ||
| <span class="sr-only">Close</span> | ||
| <svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||
| <path d="M18 6 6 18"></path> | ||
| <path d="m6 6 12 12"></path> | ||
| </svg> | ||
| </button> | ||
| </div> | ||
| <div class="p-4"> | ||
| <p class="text-gray-600 mb-4"> | ||
| Enter the email address of the user you would like to invite. | ||
| </p> | ||
| <form id="invite-user-form" hx-post="{% url 'invite_user' %}" hx-trigger="submit" hx-swap="innerHTML" hx-target="#invite-form-content"> | ||
| {% csrf_token %} | ||
| <div id="invite-form-content"> | ||
| <div class="mb-4"> | ||
| <label for="invite-email" class="block text-sm font-medium mb-2 text-gray-700">Email</label> | ||
| <input | ||
| type="email" | ||
| id="invite-email" | ||
| name="email" | ||
| class="py-3 px-4 block w-full border border-gray-300 rounded-lg text-sm focus:border-primary focus:ring-primary" | ||
| placeholder="test@test.com" | ||
| required | ||
| > | ||
| </div> | ||
| </div> | ||
| <div class="flex justify-end items-center gap-x-2 pt-3 border-t"> | ||
| <button type="button" class="py-2 px-4 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-sm hover:bg-gray-50" onclick="document.getElementById('invite-user-modal').classList.add('hidden')"> | ||
| Close | ||
| </button> | ||
| <button type="submit" class="py-2 px-4 text-sm font-medium rounded-lg border border-transparent bg-primary text-white hover:bg-primary-700"> | ||
| Invite User | ||
| </button> | ||
| </div> | ||
| </form> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <div> | ||
| </div> | ||
|
|
||
| {% endblock content %} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| from django.shortcuts import render | ||
| from django.http import HttpResponse | ||
| from django.views import View | ||
| from django.contrib.auth.mixins import LoginRequiredMixin | ||
|
|
||
|
|
@@ -16,14 +17,38 @@ def post(self, request, *args, **kwargs): | |
| form = InviteUserForm(request.POST) | ||
|
|
||
| if form.is_valid(): | ||
| # We could further improve this here to first check if an invitation for this email already exists and is not expired | ||
| UserInvitation.objects.filter(email=form.cleaned_data["email"]).delete() | ||
| email = form.cleaned_data["email"] | ||
|
|
||
| # Delete any existing invitation for this email | ||
| UserInvitation.objects.filter(email=email).delete() | ||
|
|
||
| invitation = UserInvitation(email=form.cleaned_data["email"], invited_by=request.user) | ||
| # Create new invitation | ||
| invitation = UserInvitation(email=email, invited_by=request.user) | ||
| invitation.save() | ||
|
|
||
| # Send invitation email | ||
| invitation.send_invitation_email() | ||
|
|
||
| return render(request, "accounts_app/profile.html", {"invite_user_form": form, "invited": True}) | ||
| # Return success message for HTMX swap | ||
| return HttpResponse( | ||
| '<div class="p-4 text-center">' | ||
| '<div class="text-green-600 mb-2">' | ||
| '<svg class="w-12 h-12 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">' | ||
| '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>' | ||
| '</svg>' | ||
| '</div>' | ||
| f'<p class="text-gray-800 font-medium">Invitation sent to {email}!</p>' | ||
| '<p class="text-gray-500 text-sm mt-1">The user will receive an email with instructions to join.</p>' | ||
| '</div>' | ||
| ) | ||
| else: | ||
| return render(request, "accounts_app/profile.html", {"invite_user_form": form}) | ||
| # Return form with errors | ||
| error_html = '<div class="mb-4">' | ||
| error_html += '<label for="invite-email" class="block text-sm font-medium mb-2 dark:text-white">Email</label>' | ||
| error_html += '<input type="email" id="invite-email" name="email" ' | ||
| error_html += 'class="py-3 px-4 block w-full border-red-500 rounded-lg text-sm focus:border-primary focus:ring-primary" ' | ||
| error_html += f'value="{form.data.get("email", "")}" required>' | ||
| for error in form.errors.get("email", []): | ||
| error_html += f'<p class="text-red-500 text-sm mt-1">{error}</p>' | ||
| error_html += '</div>' | ||
| return HttpResponse(error_html) | ||
|
Comment on lines
+45
to
+54
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # Explore repository structure
find . -type f -name "invite_user.py" | head -20Repository: e1plus/django-htmx-coding-challenge Length of output: 214 🏁 Script executed: # Check if the file exists and view the relevant section
cat -n accounts_app/views/invite_user.py 2>/dev/null | head -80Repository: e1plus/django-htmx-coding-challenge Length of output: 214 🏁 Script executed: # Search for form usage and how this HTML might be rendered
rg "form\.data|form\.errors" accounts_app/views/invite_user.py -B 5 -A 5Repository: e1plus/django-htmx-coding-challenge Length of output: 214 🏁 Script executed: # Check for existing security imports and mechanisms
rg "escape|mark_safe|autoescape" accounts_app/views/invite_user.pyRepository: e1plus/django-htmx-coding-challenge Length of output: 214 🏁 Script executed: # Look at the full file to understand request/response flow
wc -l accounts_app/views/invite_user.pyRepository: e1plus/django-htmx-coding-challenge Length of output: 214 🌐 Web query:
💡 Result: Short answer
Actionable checklist
Sources 🌐 Web query:
💡 Result: Short answer
Key points and best practices
Sources 🌐 Web query:
💡 Result: Short answer: yes — returning raw HTML via Django HttpResponse (or marking strings safe) can create XSS when that HTML is injected into the DOM by HTMX (hx-swap/innerHTML), because you bypass Django’s template auto-escaping and HTMX will insert and execute any scripts/behavioral attributes in the fragment. Protect by escaping or sanitizing untrusted HTML, avoiding mark_safe/ SafeString, and adding CSP/other controls. [1][2][3][4] Why (quick):
Practical mitigation checklist:
Key references:
If you want, I can:
Escape user input in The code injects raw Use -from django.shortcuts import render
-from django.http import HttpResponse
+from django.shortcuts import render
+from django.http import HttpResponse
+from django.utils.html import escape
@@
- else:
- # Return form with errors
- error_html = '<div class="mb-4">'
- error_html += '<label for="invite-email" class="block text-sm font-medium mb-2 dark:text-white">Email</label>'
- error_html += '<input type="email" id="invite-email" name="email" '
- error_html += 'class="py-3 px-4 block w-full border-red-500 rounded-lg text-sm focus:border-primary focus:ring-primary" '
- error_html += f'value="{form.data.get("email", "")}" required>'
- for error in form.errors.get("email", []):
- error_html += f'<p class="text-red-500 text-sm mt-1">{error}</p>'
- error_html += '</div>'
- return HttpResponse(error_html)
+ else:
+ # Return form with errors (escaped to prevent XSS)
+ safe_email = escape(form.data.get("email", ""))
+
+ error_html = '<div class="mb-4">'
+ error_html += '<label for="invite-email" class="block text-sm font-medium mb-2 dark:text-white">Email</label>'
+ error_html += '<input type="email" id="invite-email" name="email" '
+ error_html += 'class="py-3 px-4 block w-full border-red-500 rounded-lg text-sm focus:border-primary focus:ring-primary" '
+ error_html += f'value="{safe_email}" required>'
+ for error in form.errors.get("email", []):
+ error_html += f'<p class="text-red-500 text-sm mt-1">{escape(error)}</p>'
+ error_html += '</div>'
+ return HttpResponse(error_html)Refactor to use a Django template for this form fragment to leverage automatic escaping and improve maintainability.
🤖 Prompt for AI Agents |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: e1plus/django-htmx-coding-challenge
Length of output: 214
🏁 Script executed:
Repository: e1plus/django-htmx-coding-challenge
Length of output: 214
🏁 Script executed:
Repository: e1plus/django-htmx-coding-challenge
Length of output: 214
🏁 Script executed:
Repository: e1plus/django-htmx-coding-challenge
Length of output: 214
Fix
EmailBackend.authenticatesignature to work with Django's standard authentication callsDjango's built-in authentication system (admin, login views, and
authenticate()) calls your backend withusername=..., notemail=.... Your current signature explicitly expectsemail, sousernamegets lost in**kwargs,emailstaysNone, and authentication always fails.Change the signature to accept
usernameas the primary parameter and useUserModel.USERNAME_FIELDto support both standard Django calls and email-based authentication:This change allows Django's standard authentication flows (admin, built-in views) to work correctly while maintaining email-based authentication.
🧰 Tools
🪛 Ruff (0.14.6)
12-12: Unused method argument:
request(ARG002)
12-12: Missing type annotation for
**kwargs(ANN003)
12-12: Unused method argument:
kwargs(ARG002)
🤖 Prompt for AI Agents