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.
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)
Para evitar acesso simultâneo à mesma view com os mesmos parâmetros, uma solução eficaz é combinar uma tabela de "checkout" no banco de dados com um mecanismo de "heartbeat" via WebSockets. A tabela de chefkoit registra qual usuário está visualizando qual view com quais parâmetros, impedindo novos acessos. O heartbeat, enviado periodicamente pelo cliente, mantém o checkout ativo enquanto o usuário estiver na página; caso o heartbeat cesse (por fechamento da aba ou perda de conexão), o checkout é removido, liberando o acesso para outros usuários.
Como um bom SAPeiro sei bem do que você está falando. Não sei se você conhece por baixo dos panos esse processo no SAP, mas ele tem uma função de ENQUEUE e DEQUEUE que basicamente faz o lock e unlock de uma chave que você passa, podendo ser um número de pedido, funcionário e etc. e a tela sempre tenta fazer um ENQUEUE mas se alguém fez antes ele devolve que está bloqueado. Com websocket isso fincionará bem e uma tabela auxiliar para armazenar sase lock.
cria um sistema de Permissão x perfil X usuário com JWT.
a cada view protegida você vai ter um método que cria uma permissão dinâmica para esse view que vai ser criada ao usuário acessar essa rota, após a permissão criada você atribui ela a um perfil e esse perfil ao usuário. se algum outro usuário tentar entrar na mesma rota ele vai ser bloqueado pois ele não tem a permissão necessária. utilize JWT para transformar a permissão em um token e atribuir esse token a sessão do usuário uma vez o usuário saindo da sessão a rota vai estar livre para outro poder utilizar.
fiz algo parecido para um sistema que lidava com dados sensíveis e as vezes eles precisavam ser editados diversas vezes então para garantir que as pessoas pudessem estar lidando com a informações atualizadas adicionei um sistema de fila para que cada um dos requests só fosse direcionado a rota quando realmente tivesse sido liberada pelo usuário que a estava utilizando. enfim boa sorte aí mano qq coisa se puder ajudar mais entra em contato
Eu entendi o que quer fazer e acho a resposta do clacerda
válida.
No entanto, tenho algumas dúvidas: a view necessariamente precisa ser bloqueada para outro usuário?
O que quero dizer é: pense em um site de reservas de assento de cinema, por exemplo. O usuário tem uma janela de tempo até efetuar o pagamento. Caso o tempo expire, ele deve reiniciar o processo e, caso o assento esteja indisponível no momento do pagamento, uma mensagem de erro é exibida e ele também deve reiniciar o processo (agora, com o estado do assento escolhido anteriormente atualizado, ou seja, indisponível).
Esse cenário seria possível em sua aplicação?
Sua solução é bem inteligente, o unico problema do JS estar em looping de verificação é realmente q o JS do lado do cliente é uma paulada no processamento, mas se fizer como o clacerda
sinalizou, e inserir um websocket, problema resolvido
Gostaria de entender melhor o caso de uso, q talvez tenha algum outro tipo de solução possivel para esse problema
O que você quer implementar, implica em uma abordagem de controle de sessão. Há algumas maneiras de se fazer isso, e boas ideias já foram apresentadas aqui. Pensando em algo simples para validar a ideia, Eu implementaria usando um banco de chave-valor para salvar a sessão, sendo o endereço da view a chave. Você insere essa verificação em um middleware e toda request vai passar por essa verificação. Quando uma nova sessão for iniciada, você salva aquela view no banco chave-valor e pronto. Aí você pode criar mecanismos de invalidação para garantir desbloqueio de sessão em caso de inatividade, e por aí vai. Bom trabalho pra ti :)
Um forte abraço!
Sua implementação vai por esse caminho. A ideia de um banco de chave-valor vai na direção de ser mais rápido em leitura. Eu sinceramente, não ficaria preocupado em tentar outra abordagem, sem primeiro testar se o que você já fez, vai funcionar, ou não nos cenários de uso da sua aplicação.