Add user post deletion

Users can delete their posts as long as they don't clear their cookies,
and as long as server-side user sessions are persistent.

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
This commit is contained in:
2022-07-13 21:28:07 -07:00
parent 96e8b7752f
commit a4f00e6242
5 changed files with 39 additions and 2 deletions

View File

@@ -25,11 +25,12 @@ class PostForm(ModelForm):
model = Post model = Post
fields = ["subject", "name", "text", "capcode", "image"] fields = ["subject", "name", "text", "capcode", "image"]
def __init__(self, *args, user, board, ip, **kwargs): def __init__(self, *args, user, board, ip, user_token, **kwargs):
super(PostForm, self).__init__(*args, **kwargs) super(PostForm, self).__init__(*args, **kwargs)
self.user = user self.user = user
self.instance.board = board self.instance.board = board
self.instance.ip = ip self.instance.ip = ip
self.instance.user_token = user_token
self.fields["capcode"].queryset = get_objects_for_user( self.fields["capcode"].queryset = get_objects_for_user(
self.user, "board.use_capcode" self.user, "board.use_capcode"
) )

View File

@@ -124,6 +124,10 @@ class Post(models.Model):
# Image width and height # Image width and height
image_width = models.IntegerField(null=True, blank=True) image_width = models.IntegerField(null=True, blank=True)
image_height = models.IntegerField(null=True, blank=True) image_height = models.IntegerField(null=True, blank=True)
# The user token that was used to make this post. This is used for
# verification when a user wants to delete their post. It is not
# particularly secret and does not need to be hashed.
user_token = models.CharField(max_length=30, null=True)
class Meta: class Meta:
permissions = [ permissions = [

View File

@@ -1,6 +1,9 @@
from typing import Optional, TYPE_CHECKING from typing import Optional, TYPE_CHECKING
from django.utils import timezone from django.utils import timezone
from django.conf import settings
import ipaddress import ipaddress
import random
import string
from board.models import Ban, RangeBan from board.models import Ban, RangeBan
@@ -45,3 +48,11 @@ def is_banned(ip: str, board: Optional["Board"]) -> bool:
return bool(active) return bool(active)
else: else:
return False return False
def generate_user_token() -> str:
"""
Generates a non-secure user token.
User tokens need not be secure so this is a simple implementation.
"""
return "".join(random.choices(string.ascii_letters, k=settings.USER_TOKEN_LENGTH))

View File

@@ -8,7 +8,7 @@ from django.http import Http404, HttpResponseRedirect
from django.http.request import QueryDict from django.http.request import QueryDict
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
from django.views.generic import detail, edit from django.views.generic import edit
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils import timezone from django.utils import timezone
@@ -147,6 +147,10 @@ class PostCreateView(CreateView):
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super(PostCreateView, self).get_form_kwargs() kwargs = super(PostCreateView, self).get_form_kwargs()
kwargs["user"] = self.request.user kwargs["user"] = self.request.user
if "user_token" not in self.request.session:
# Generate a user token
self.request.session["user_token"] = generate_user_token()
kwargs["user_token"] = self.request.session["user_token"]
return kwargs return kwargs
def get_success_url(self) -> str: def get_success_url(self) -> str:
@@ -213,6 +217,10 @@ class ReplyCreateView(CreateView):
post = get_object_or_404(Post, id=post_id) post = get_object_or_404(Post, id=post_id)
kwargs["op"] = post kwargs["op"] = post
kwargs["user"] = self.request.user kwargs["user"] = self.request.user
if "user_token" not in self.request.session:
# Generate a user token
self.request.session["user_token"] = generate_user_token()
kwargs["user_token"] = self.request.session["user_token"]
return kwargs return kwargs
def get_success_url(self) -> str: def get_success_url(self) -> str:
@@ -256,6 +264,13 @@ class PostDeleteView(PermissionRequiredMixin, edit.DeleteView):
success_url = reverse_lazy("board:post_delete_success") success_url = reverse_lazy("board:post_delete_success")
raise_exception = True raise_exception = True
def has_permission(self) -> bool:
object = self.get_object()
user_token = self.request.session.get("user_token", None)
return self.request.user.has_perm("board.delete_post") or (
user_token and object.user_token == user_token
)
def form_valid(self, form): def form_valid(self, form):
success_url = self.get_success_url() success_url = self.get_success_url()
if form["image_only"].value() != "0": if form["image_only"].value() != "0":

View File

@@ -174,3 +174,9 @@ BAN_WINDOW_CLOSE_TIMEOUT = 0
# By default, wait 0 seconds. If there is an error, the window won't close # By default, wait 0 seconds. If there is an error, the window won't close
# because it will be redirected elsewhere. # because it will be redirected elsewhere.
POST_WINDOW_CLOSE_TIMEOUT = 0 POST_WINDOW_CLOSE_TIMEOUT = 0
# This is the length of a user's token, which is held in a server-side session.
# This token is used to identify a user's individual web client, outside of IP
# address. You probably don't need to change this.
# It should be at most 30, and probably at least 10.
USER_TOKEN_LENGTH = 30