Apenas um usuário por view, como você programaria isso?
python > django
Como você programaria um sistema em que o usuário não possa acessar a mesma view com os mesmos parâmetros já aberta por outro? A parte fácil eu já implementei.
De que forma poderia ser seguro e à prova de erros catastróficos?
Além disso, como você faria para que a atividade do usuário retirasse essa trava? E como garantir que a trava seja liberada quando o usuário sair da página?
Quem conhece o SAP sabe como funciona, e eu gostaria de implementar algo parecido no meu site, mas estou com dificuldades.
Já fiz uma implementação, mas não ficou boa. Quero ajuda para melhorar. Eu uso Django, mas podemos conversar também sobre jQuery, Alpine, HTMX, e qual seria a melhor opção e a melhor maneira de implementar isso.
O sistema precisa ser à prova do tempo. Ou seja, independente de atualizações ou alterações no código, o resultado deve ser sempre o mesmo: bloquear a view e desbloquear quando necessário.
Antes de mostrar o código, o que estou fazendo atualmente é criar um decorador chamado protected_view, que verifica se o usuário está logado com alguns parâmetros e se já há alguém bloqueando aquela view. Caso não haja ninguém, ele insere um novo registro no banco de dados com o path da view e o usuário que a bloqueou. Depois disso, um código JavaScript verifica, a cada 5 segundos, se o usuário ainda está naquela view; caso contrário, o valor é deletado. No entanto, estou achando essa implementação bem ruim.
Meu código
models
from django.db import models
class LockedView(models.Model):
view_informations = models.JSONField()
locked_by = models.ForeignKey(
"accounts.Account", on_delete=models.SET_NULL, null=True
)
is_open = models.BooleanField(default=False)
def __str__(self):
return "This view are open by %s" % self.locked_by.email
view
#Vou mudar esse generic.views de pasta
from apps.accounts.utils.generic.views import UpdateView
from django.contrib.auth.mixins import LoginRequiredMixin
class ProfileUpdateTestView(LoginRequiredMixin, UpdateView):
model = Profile
fields = [
"name",
"avatar",
"phone",
"house_address_street",
"house_address_number",
"buss_stop_address_street",
"buss_stop_address_number",
"city",
"turn",
"role",
]
template_name = "profile.html"
success_url = "/profile"
login_url = "/login"
app_name = "accounts" # algo que criei para utilizar m2m , fk no frontend
protected = True
def get_object(self, queryset=None):
return self.model.objects.get(pk=self.kwargs.get("pk"))
def form_valid(self, form):
messages.success(self.request, "Profile updated")
return super().form_valid(form)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["instance"] = self.get_object()
context["account"] = self.get_object().user
return context
mixin
from apps.core.decorators import protected_view
class ProtectionViewMixin(View):
@protected_view
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
decorator
from django.contrib import messages
from django.db.transaction import atomic
from django.http import HttpRequest
from django.shortcuts import redirect
from apps.core.models import LockedView
@atomic
def protected_view(func):
def wrapper(cls, request: HttpRequest, *args, **kwargs):
path = request.get_full_path()
if not hasattr(cls, "protected") or not cls.protected:
return func(cls, request, *args, **kwargs)
if not request.user.is_authenticated:
messages.error(request, "You need to be logged in to access this view")
return redirect("/login")
is_open = LockedView.objects.filter(view_informations__paths__icontains=path)
if is_open.exists():
locked_view = is_open.first()
if locked_view.is_open and request.user != locked_view.locked_by:
messages.error(request, f"This view is open by {locked_view.locked_by}")
return redirect("/")
elif locked_view.is_open and request.user == locked_view.locked_by:
return func(cls, request, *args, **kwargs)
else:
LockedView.objects.create(
view_informations=dict(paths=[path]),
locked_by=request.user,
is_open=True,
)
return func(cls, request, *args, **kwargs)
return wrapper
js
function getCSRFToken() {
const CSRFToken = $("body").attr('hx-headers');
const parsedToken = CSRFToken ? JSON.parse(CSRFToken) : null;
return parsedToken ? parsedToken['X-CSRFToken'] : null;
}
async function sendRequest(url, data) {
const CSRFToken = getCSRFToken();
if (!CSRFToken) {
return;
}
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': CSRFToken
},
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(`Error on request: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error('Error on request', error);
}
}
async function verifyPath() {
const pathAtual = window.location.pathname;
const user = $("body").attr('data-user');
const response = await sendRequest('/core/check-locked/', {
path: pathAtual,
user: user
});
if (response && !response.locked) {
await sendRequest('/core/unlock-view/', { user: user });
}
}
setInterval(verifyPath, 5000);
is_locked and unlock_view functions
import json
from django.http import JsonResponse
from apps.core.models import LockedView
def is_locked(request, *args, **kwargs):
if request.content_type == "application/json":
data = json.loads(request.body.decode("utf-8"))
user = data.get("user")
path = data.get("path")
is_locked_by = LockedView.objects.filter(
locked_by__email__iexact=user, view_informations__paths__contains=path
).exists()
if is_locked_by:
return JsonResponse({"locked": True})
else:
return JsonResponse({"locked": False})
else:
return JsonResponse({"error": "Invalid content type"}, status=400)
def unlock_view(request, *args, **kwargs):
if request.content_type == "application/json":
data = json.loads(request.body.decode("utf-8"))
user = data.get("user")
locked_by = LockedView.objects.filter(locked_by__email__iexact=user)
if locked_by.exists():
locked_by.delete()
return JsonResponse({}, status=200)
else:
return JsonResponse({"error": "Invalid content type"}, status=400)