Ownership and permission validation for guilds

This commit is contained in:
Corban-Lee Jones 2024-04-17 09:58:41 +01:00
parent 14a9ebd2f7
commit 4bd0ea77ab
10 changed files with 131 additions and 14 deletions

View File

@ -10,6 +10,9 @@ from apps.authentication.models import UserServerLink
log = logging.getLogger(__name__)
# This DynamicModelSerializer is from a StackOverflow user in an obscure thread.
# I wish that I could remember which thread, because god bless that man.
class DynamicModelSerializer(serializers.ModelSerializer):
"""
For use with GET requests, to specify which fields to include or exclude
@ -127,7 +130,7 @@ class UserServerLinkSerializer(DynamicModelSerializer):
class Meta:
model = UserServerLink
fields = ("id", "server_id", "user", "name", "icon", "icon_url", "permissions")
fields = ("id", "server_id", "user", "name", "icon", "icon_url")
class SavedGuildSerializer(DynamicModelSerializer):
@ -137,4 +140,4 @@ class SavedGuildSerializer(DynamicModelSerializer):
class Meta:
model = SavedGuilds
fields = ("id", "guild_id", "name", "icon")
fields = ("id", "guild_id", "name", "icon", "added_by", "permissions", "owner")

View File

@ -1,7 +1,10 @@
# -*- encoding: utf-8 -*-
import json
import logging
import requests
from django.conf import settings
from django.db.utils import IntegrityError
from django.core.exceptions import ValidationError
from django_filters import rest_framework as rest_filters
@ -189,28 +192,43 @@ class SavedGuild_ListView(generics.ListCreateAPIView):
serializer_class = SavedGuildSerializer
filter_backends = [filters.SearchFilter, rest_filters.DjangoFilterBackend, filters.OrderingFilter]
filterset_fields = ["id", "guild_id", "name", "icon"]
filterset_fields = ["id", "guild_id", "name", "icon", "added_by", "permissions", "owner"]
search_fields = ["name"]
def get_queryset(self):
return SavedGuilds.objects.all()
return SavedGuilds.objects.filter(added_by=self.request.user)
def post(self, request):
is_owner = request.data["owner"].lower() == "true"
# Check user is admin in server
if not (self.is_server_admin(request.data["permissions"]) or is_owner):
return Response(
{"detail": "You must be a server administrator"},
status=status.HTTP_403_FORBIDDEN,
exception=False
)
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
try:
self.perform_create(serializer)
except IntegrityError:
except IntegrityError as err:
return Response(
{"detail": "SavedGuild must be unique"},
status=status.HTTP_409_CONFLICT,
{"detail": str(err)},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
exception=True
)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def is_server_admin(self, permissions) -> bool:
return (int(permissions) & 1 << 3) == 1 << 3
class SavedGuild_DetailView(generics.RetrieveDestroyAPIView):
"""
@ -224,4 +242,6 @@ class SavedGuild_DetailView(generics.RetrieveDestroyAPIView):
parser_classes = [MultiPartParser, FormParser]
serializer_class = SavedGuildSerializer
queryset = SavedGuilds.objects.all()
def get_queryset(self):
return SavedGuilds.objects.filter(added_by=self.request.user)

View File

@ -101,7 +101,16 @@ class GuildsView(View):
status = response.status_code
print(response, content, status)
return JsonResponse(content, safe=False, status=status)
valid_guilds = [guild for guild in response.json() if self._has_permissions(guild)]
return JsonResponse(valid_guilds, safe=False, status=status)
def _has_permissions(self, guild):
permissions = guild["permissions"]
is_owner = guild["owner"]
return (int(permissions) & 1 << 3) == 1 << 3 or is_owner
class GuildChannelsView(View):

View File

@ -0,0 +1,22 @@
# Generated by Django 5.0.1 on 2024-04-16 10:35
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0010_alter_savedguilds_guild_id'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='savedguilds',
name='added_by',
field=models.ForeignKey(default='1', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
preserve_default=False,
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 5.0.1 on 2024-04-16 14:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0011_savedguilds_added_by'),
]
operations = [
migrations.AddField(
model_name='savedguilds',
name='permissions',
field=models.CharField(default='1', max_length=64),
preserve_default=False,
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 5.0.1 on 2024-04-16 14:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0012_savedguilds_permissions'),
]
operations = [
migrations.AddField(
model_name='savedguilds',
name='owner',
field=models.BooleanField(default=True),
preserve_default=False,
),
]

View File

@ -61,6 +61,16 @@ class SavedGuilds(models.Model):
max_length=128
)
added_by = models.ForeignKey(to="authentication.DiscordUser", on_delete=models.CASCADE)
permissions = models.CharField(
max_length=64
)
owner = models.BooleanField(
default=False
)
class Subscription(models.Model):
"""

View File

@ -20,7 +20,7 @@ function addToLoadedServers(server, selectNew=true) {
loadedServers[id] = server;
// Display the loaded server
addServerTemplate(id, server.guild_id, server.name, server.icon);
addServerTemplate(id, server.guild_id, server.name, server.icon, server.permissions, server.owner);
// Select the newly added server
if (selectNew) {
@ -70,6 +70,8 @@ async function loadServerOptions() {
value: server.id,
text: server.name,
"data-icon": server.icon,
"data-permissions": server.permissions,
"data-isowner": server.owner
}));
});
}
@ -103,13 +105,15 @@ async function loadSavedGuilds() {
}
// Create an element for the added server and show it
function addServerTemplate(serverPrimaryKey, serverGuildId, serverName, serverIconHash) {
function addServerTemplate(serverPrimaryKey, serverGuildId, serverName, serverIconHash, serverPermissions, serverIsOwner) {
template = $($("#serverItemTemplate").html());
template.find("img").attr("src", `https://cdn.discordapp.com/icons/${serverGuildId}/${serverIconHash}.webp?size=80`);
template.attr("data-guild-id", serverGuildId);
template.attr("data-name", serverName);
template.attr("data-icon", serverIconHash);
template.attr("data-permissions", serverPermissions);
template.attr("data-isowner", serverIsOwner);
template.attr("data-id", serverPrimaryKey);
// Bind the button for selecting this server
@ -138,15 +142,19 @@ $("#serverForm").on("submit", async function(event) {
serverName = selectedOption.text();
serverGuildId = selectedOption.val();
serverIconHash = selectedOption.attr("data-icon");
serverPermissions = selectedOption.attr("data-permissions");
serverIsOwner = selectedOption.attr("data-isowner");
var serverPrimaryKey = await registerNewServer(serverName, serverGuildId, serverIconHash);
var serverPrimaryKey = await registerNewServer(serverName, serverGuildId, serverIconHash, serverPermissions, serverIsOwner);
if (serverPrimaryKey !== false) {
addToLoadedServers({
id: serverPrimaryKey,
name: serverName,
guild_id: serverGuildId,
icon: serverIconHash
icon: serverIconHash,
permissions: serverPermissions,
owner: serverIsOwner
});
}
@ -155,11 +163,14 @@ $("#serverForm").on("submit", async function(event) {
// Add a new 'saved guild' based on the info provided
// returns `response.id` if successful, else false
async function registerNewServer(serverName, serverGuildId, serverIconHash) {
async function registerNewServer(serverName, serverGuildId, serverIconHash, serverPermissions, serverIsOwner) {
var formData = new FormData();
formData.append("name", serverName);
formData.append("guild_id", serverGuildId);
formData.append("icon", serverIconHash);
formData.append("added_by", currentUserId);
formData.append("permissions", serverPermissions);
formData.append("owner", serverIsOwner === "true");
try { response = await newSavedGuild(formData); }
catch (err) { return false }

View File

@ -21,6 +21,9 @@
</button>
</div>
</div>
<p class="mb-0 form-text">
You must be an administrator, or own the selected server.
</p>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">Submit</button>

View File

@ -80,6 +80,7 @@
<script>
const CSRF_MiddlewareToken = "{{ csrf_token }}";
const currentUserId = "{{ request.user.id }}";
</script>
{% include 'includes/scripts.html' %}