Impersonation system implementation
This commit is contained in:
@@ -41,6 +41,9 @@
|
||||
<a href="{% url 'library:browse' %}">
|
||||
<i class="fas fa-compass"></i><span class="nav-text"> Browse</span>
|
||||
</a>
|
||||
<a href="{% url 'profiles:user_list' %}">
|
||||
<i class="fas fa-users"></i><span class="nav-text"> Users</span>
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
<main class="main-content">
|
||||
@@ -49,6 +52,17 @@
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
<div class="user-bar">
|
||||
{% if impersonating %}
|
||||
<a class="impersonate-chip" href="{% url 'profiles:user_profile' impersonator.username %}"
|
||||
title="Impersonator: {{ impersonator.username }}">
|
||||
{% if impersonator_avatar_url %}
|
||||
<img src="{{ impersonator_avatar_url }}" alt="">
|
||||
{% else %}
|
||||
<div class="default-avatar mini">{{ impersonator.username|first|upper }}</div>
|
||||
{% endif %}
|
||||
<span>Impersonator: {{ impersonator.username }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="user-avatar" onclick="toggleUserMenu()">
|
||||
{% if user.userprofile.avatar %}
|
||||
<img src="{{ user.userprofile.avatar_url }}" alt="Avatar">
|
||||
@@ -60,7 +74,12 @@
|
||||
<a href="{% url 'profiles:user_profile' request.user.username %}" class="user-edit">
|
||||
<i class="fas fa-user-circle"></i> {{ user.username }}
|
||||
</a>
|
||||
<div class="user-info"></div>
|
||||
<div class="user-info">
|
||||
{% if impersonating %}
|
||||
<i class="fas fa-mask"></i> acting from
|
||||
<a href="{% url 'profiles:user_profile' impersonator.username %}"><strong>{{ impersonator.username }}</strong></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="{% url 'profiles:account' %}">
|
||||
<i class="fas fa-user-cog"></i> Account Settings
|
||||
</a>
|
||||
@@ -70,7 +89,11 @@
|
||||
<form method="post" action="{% url 'profiles:logout' %}" style="display: inline;">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="logout-button">
|
||||
<i class="fas fa-sign-out-alt"></i> Logout
|
||||
{% if impersonating %}
|
||||
<i class="fas fa-undo-alt"></i> Stop impersonating
|
||||
{% else %}
|
||||
<i class="fas fa-sign-out-alt"></i> Logout
|
||||
{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,14 @@
|
||||
<div class="profile-info">
|
||||
<h1>{{ profile_user.username }}</h1>
|
||||
<p class="profile-joined"><i class="fas fa-calendar"></i> Joined {{ profile.joined|date:"F j, Y" }}</p>
|
||||
{% if can_impersonate %}
|
||||
<form method="post" action="{% url 'profiles:impersonate_start' profile_user.pk %}" class="impersonate-form">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-secondary" id="impersonate-btn">
|
||||
<i class="fas fa-mask"></i> Impersonate
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if is_owner %}
|
||||
<p><a href="{% url 'profiles:account' %}" class="btn btn-secondary"><i class="fas fa-cog"></i> Account Settings</a></p>
|
||||
{% endif %}
|
||||
@@ -78,6 +86,28 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{% if can_impersonate %}
|
||||
<script>
|
||||
(function () {
|
||||
const btn = document.getElementById('impersonate-btn');
|
||||
if (!btn) return;
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const reason = prompt('Reason for impersonation (optional):', '');
|
||||
if (reason === null) return;
|
||||
const form = btn.closest('form');
|
||||
if (reason.trim()) {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = 'reason';
|
||||
input.value = reason.trim();
|
||||
form.appendChild(input);
|
||||
}
|
||||
form.submit();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
{% if is_owner %}
|
||||
<script>
|
||||
(function () {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Users - Packs Site{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1><i class="fas fa-users"></i> Users</h1>
|
||||
|
||||
<form method="get" class="browse-search-form">
|
||||
<div class="browse-search-row">
|
||||
<input type="text" name="q" value="{{ q }}" placeholder="Search users…" class="browse-search-input">
|
||||
<button type="submit" class="btn btn-primary"><i class="fas fa-search"></i></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="user-grid">
|
||||
{% for u in users %}
|
||||
<a href="{% url 'profiles:user_profile' u.username %}" class="user-card">
|
||||
<div class="user-card-avatar">
|
||||
{% if u.userprofile.avatar %}
|
||||
<img src="{{ u.userprofile.avatar_url }}" alt="{{ u.username }}">
|
||||
{% else %}
|
||||
<div class="default-avatar">{{ u.username|first|upper }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="user-card-body">
|
||||
<h3>{{ u.username }}</h3>
|
||||
<p class="user-card-meta"><i class="fas fa-calendar"></i> Joined {{ u.userprofile.joined|date:"M j, Y" }}</p>
|
||||
<p class="user-card-meta"><i class="fas fa-box-open"></i> {{ u.projects.count }} pack{{ u.projects.count|pluralize }}</p>
|
||||
</div>
|
||||
</a>
|
||||
{% empty %}
|
||||
<p class="empty-hint">No users found.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if users.has_other_pages %}
|
||||
<div class="pagination">
|
||||
{% if users.has_previous %}
|
||||
<a href="?{% if q %}q={{ q|urlencode }}&{% endif %}page={{ users.previous_page_number }}">«</a>
|
||||
{% endif %}
|
||||
<span>Page {{ users.number }} of {{ users.paginator.num_pages }}</span>
|
||||
{% if users.has_next %}
|
||||
<a href="?{% if q %}q={{ q|urlencode }}&{% endif %}page={{ users.next_page_number }}">»</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user