diff --git a/api/v1/migrations/0004_remove_server_id_alter_server_url.py b/api/v1/migrations/0004_remove_server_id_alter_server_url.py
new file mode 100644
index 0000000..ad38a86
--- /dev/null
+++ b/api/v1/migrations/0004_remove_server_id_alter_server_url.py
@@ -0,0 +1,23 @@
+# Generated by Django 5.0.4 on 2024-05-02 22:25
+
+import v1.models.server
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('v1', '0003_remove_server_inactivity_counter_server_up_and_more'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='server',
+ name='id',
+ ),
+ migrations.AlterField(
+ model_name='server',
+ name='url',
+ field=models.CharField(max_length=255, primary_key=True, serialize=False, validators=[v1.models.server.OptionalSchemeURLValidator()]),
+ ),
+ ]
diff --git a/api/v1/migrations/0005_alter_server_url.py b/api/v1/migrations/0005_alter_server_url.py
new file mode 100644
index 0000000..d8a0d1a
--- /dev/null
+++ b/api/v1/migrations/0005_alter_server_url.py
@@ -0,0 +1,19 @@
+# Generated by Django 5.0.4 on 2024-05-02 23:51
+
+import v1.models.server
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('v1', '0004_remove_server_id_alter_server_url'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='server',
+ name='url',
+ field=models.CharField(error_messages={'unique': 'A server with this URL already exists.'}, max_length=255, primary_key=True, serialize=False, validators=[v1.models.server.OptionalSchemeURLValidator()]),
+ ),
+ ]
diff --git a/api/v1/models/server.py b/api/v1/models/server.py
index 0366836..54f5462 100644
--- a/api/v1/models/server.py
+++ b/api/v1/models/server.py
@@ -1,14 +1,13 @@
import datetime
import json
-import os
-import socket
import requests
from django.db import models
from django.core.validators import URLValidator
-from django.core.exceptions import ValidationError
from urllib.parse import urlparse
from v1.common.logging import logger
from django.contrib.gis.geoip2 import GeoIP2
+from django.core.exceptions import ValidationError
+
# Required and recommended settings used to build the initial client server card
required_settings = [
@@ -39,9 +38,11 @@ class OptionalSchemeURLValidator(URLValidator):
class Server(models.Model):
name = models.CharField(max_length=255, null=True, editable=False)
url = models.CharField(
+ primary_key=True,
max_length=255,
null=False,
validators=[OptionalSchemeURLValidator()],
+ error_messages={"unique": "A server with this URL already exists."},
)
location = models.CharField(max_length=255, null=True, editable=False)
max_level = models.IntegerField(null=True, editable=False)
@@ -55,8 +56,11 @@ class Server(models.Model):
@property
def expires(self):
- time_since_creation = self.updated - self.created
- return self.updated + min(time_since_creation, datetime.timedelta(hours=24))
+ try:
+ time_since_creation = self.updated - self.created
+ return self.updated + min(time_since_creation, datetime.timedelta(hours=24))
+ except:
+ return datetime.datetime(0, 1, 1, 0, 0, 0)
def _str_(self):
return self.url
@@ -70,13 +74,10 @@ class Server(models.Model):
parsed_url = urlparse(self.url)
self.url = parsed_url.netloc.lower()
- # Check if server URL is unique
- existing_server = Server.objects.filter(url=self.url).first()
- if existing_server and existing_server.pk != self.pk:
- raise ValidationError("A server with this URL already exists.")
-
# Validate the server API
if not self.parse_server_api():
+ if not self.created:
+ raise ValidationError("Server verification failed.")
if self.expires <= datetime.datetime.now(datetime.timezone.utc):
return False
self.up = False
diff --git a/api/v1/serializers/server.py b/api/v1/serializers/server.py
index 213f420..38385f0 100644
--- a/api/v1/serializers/server.py
+++ b/api/v1/serializers/server.py
@@ -7,7 +7,6 @@ class ServerSerializer(serializers.ModelSerializer):
class Meta:
model = Server
fields = [
- "id",
"name",
"url",
"location",
diff --git a/api/v1/views/server.py b/api/v1/views/server.py
index eaf303e..35ad485 100644
--- a/api/v1/views/server.py
+++ b/api/v1/views/server.py
@@ -1,7 +1,11 @@
+from django.http import Http404
from rest_framework import viewsets, mixins
+from rest_framework.response import Response
from django.db.models.fields.json import KT
from v1.serializers.server import ServerDetailsSerializer, ServerSerializer
from v1.models.server import Server
+from rest_framework import status
+from django.core.exceptions import ValidationError
class ServerViewSet(
@@ -43,8 +47,53 @@ class ServerViewSet(
class ServerDetailsViewSet(
+ mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
+ mixins.ListModelMixin,
viewsets.GenericViewSet,
):
queryset = Server.objects.all()
serializer_class = ServerDetailsSerializer
+
+ def get_object(self):
+ queryset = self.filter_queryset(self.get_queryset())
+ url_param = self.request.query_params.get("url")
+ if url_param is not None:
+ try:
+ return queryset.get(url=url_param)
+ except Server.DoesNotExist:
+ raise Http404("Server not found with the provided URL.")
+ return super().get_object()
+
+ def create_server(self, url_param):
+ try:
+ serializer = self.get_serializer(data={"url": url_param})
+ serializer.is_valid(raise_exception=True)
+ self.perform_create(serializer)
+ return serializer.instance
+ except ValidationError as e:
+ raise Http404(e.message)
+
+ def create(self, request, *args, **kwargs):
+ url_param = request.data.get("url")
+ if url_param:
+ try:
+ server = Server.objects.get(url=url_param)
+ serializer = self.get_serializer(server)
+ return Response(serializer.data, status=status.HTTP_200_OK)
+ except Server.DoesNotExist:
+ instance = self.create_server(url_param)
+ serializer = self.get_serializer(instance)
+ return Response(serializer.data, status=status.HTTP_201_CREATED)
+
+ def list(self, request, *args, **kwargs):
+ queryset = self.filter_queryset(self.get_queryset())
+ url_param = self.request.query_params.get("url")
+ if url_param is not None:
+ queryset = queryset.filter(url=url_param)
+ if not queryset.exists():
+ instance = self.create_server(url_param)
+ serializer = self.get_serializer(instance)
+ return Response(serializer.data, status=status.HTTP_201_CREATED)
+ return self.retrieve(request, *args, **kwargs)
+ raise Http404("No URL provided.")
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 8e80877..167ea1a 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -39,7 +39,7 @@ export function App() {
element={}
/>
}
/>
} />
diff --git a/client/src/apiUtil.tsx b/client/src/apiUtil.tsx
index 3ec9046..077c407 100644
--- a/client/src/apiUtil.tsx
+++ b/client/src/apiUtil.tsx
@@ -26,23 +26,34 @@ export const fetchDataFromBackend = async (
queryParams = {}
) => {
const queryString = new URLSearchParams(queryParams).toString();
- const url = `${import.meta.env.PROD ? 'https://api.ixion.dev' : 'http://localhost:8000'}/${dir}/${queryString ? `?${queryString}` : ''}`;
+ const url = `${import.meta.env.PROD ? 'https://api.ixion.dev' : 'http://localhost:8000'}/${dir}${queryString ? `?${queryString}` : ''}`;
try {
const response = await fetch(url, {
signal: AbortSignal.timeout(5000),
});
+
if (!response.ok) {
- throw new Error(
- `(${response.status.toString()} - ${response.statusText || 'unknown'})`
- );
+ let errorMessage =
+ 'An unknown error occurred while fetching data from the backend.';
+
+ // Check if the response has JSON content
+ const contentType = response.headers.get('content-type');
+ if (contentType && contentType.includes('application/json')) {
+ const errorData = await response.json();
+ errorMessage = errorData.url || errorData.detail;
+ }
+
+ throw new Error(errorMessage);
}
return await response.json();
} catch (err) {
if (err instanceof Error) {
- throw new Error(`${err.message} - ${url}`);
+ throw new Error(err.message);
} else {
- throw new Error('Something went wrong.');
+ throw new Error(
+ 'An unhandled error occurred while fetching data from the backend.'
+ );
}
}
};
@@ -59,7 +70,7 @@ export const postData = async (inputText: string) => {
body: JSON.stringify({ url: inputText }),
}
);
- let message = `Server verification failed!`;
+ let message = `An unknown error occurred while sending data to the backend.`;
let success = false;
if (!response.ok) {
// bad url or duplicate
@@ -73,13 +84,13 @@ export const postData = async (inputText: string) => {
};
}
const responseData = await response.json();
- if (responseData.id !== null) {
+ if (response.status === 201) {
message = 'Added new server!';
success = true;
}
return { message, success, data: responseData };
} catch (err) {
- let message = 'An unknown error occurred.';
+ let message = 'An unhandled error while sending data to the backend.';
if (err instanceof Error) {
message = err.message;
}
diff --git a/client/src/components/AddServer.tsx b/client/src/components/AddServer.tsx
index 42127aa..86b14f0 100644
--- a/client/src/components/AddServer.tsx
+++ b/client/src/components/AddServer.tsx
@@ -73,7 +73,7 @@ export default function AddServer({
severity: 'success',
});
if (response.data && typeof response.data === 'object') {
- navigate(`/server/${response.data.id}`);
+ navigate(`/server/${encodeURIComponent(response.data.url)}`);
setServers([response.data, ...servers]);
}
if (inputRef.current) {
@@ -119,7 +119,11 @@ export default function AddServer({
-
+
Max Level
@@ -194,15 +194,16 @@ export default function SearchServers({
onChange={handleSearchMaxLevelMin}
// onBlur={handleBlur}
inputProps={{
+ name: 'max-level-min',
step: 1,
min: 1,
max: 99,
type: 'number',
- 'aria-labelledby': 'max-level-min',
+ 'aria-labelledby': 'max-level',
}}
/>
diff --git a/client/src/components/ServerCard.tsx b/client/src/components/ServerCard.tsx
index 4610ff1..e22569f 100644
--- a/client/src/components/ServerCard.tsx
+++ b/client/src/components/ServerCard.tsx
@@ -330,7 +330,7 @@ export default function ServerCard({ server }: { server: ServerData }) {
) : (
filteredServers.map((server: ServerData) => (
-
+
))
)}
diff --git a/client/src/pages/ServerDetails.tsx b/client/src/pages/ServerDetails.tsx
index 1839b24..4397764 100644
--- a/client/src/pages/ServerDetails.tsx
+++ b/client/src/pages/ServerDetails.tsx
@@ -30,7 +30,7 @@ export default function ServerDetails({
}: {
setAlertInfo: React.Dispatch>;
}) {
- const { id } = useParams();
+ const { url } = useParams();
const [server, setServer] = useState();
const [error, setError] = useState('');
@@ -38,10 +38,10 @@ export default function ServerDetails({
let data: ServerData;
const fetchServerData = async () => {
try {
- if (id === 'demo') {
+ if (url === 'demo') {
data = await fetchDemo();
} else {
- data = await fetchDataFromBackend(`server/${id}`);
+ data = await fetchDataFromBackend(`server/?url=${url}`);
}
} catch (err) {
if (err instanceof Error) {
@@ -58,7 +58,7 @@ export default function ServerDetails({
};
fetchServerData();
- }, [id, setServer, setAlertInfo, setError]);
+ }, [url, setServer, setAlertInfo, setError]);
const [clipboardTooltip, setClipboardTooltip] = useState('Copy server URL.');
const [clipboardTooltipOpen, setClipboardTooltipOpen] = useState(false);
@@ -93,12 +93,12 @@ export default function ServerDetails({
}
};
- function formatExternalUrl(url: string): string {
+ function formatExternalUrl(serverUrl: string): string {
// prepend 'https://' to the URL if it's not already there
- if (!/^https?:\/\//i.test(url)) {
- return `https://${url}`;
+ if (!/^https?:\/\//i.test(serverUrl)) {
+ return `https://${serverUrl}`;
}
- return url;
+ return serverUrl;
}
return (