Change server details viewset

Change primary key to url instead of arbitrary id.
Queries for server details will now attempt to add the server if it doesn't exist.
This commit is contained in:
Corey 2024-05-03 03:35:36 +00:00
parent 9b0a1701d4
commit 739cfdd07e
13 changed files with 145 additions and 39 deletions

View File

@ -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()]),
),
]

View File

@ -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()]),
),
]

View File

@ -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

View File

@ -7,7 +7,6 @@ class ServerSerializer(serializers.ModelSerializer):
class Meta:
model = Server
fields = [
"id",
"name",
"url",
"location",

View File

@ -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.")

View File

@ -39,7 +39,7 @@ export function App() {
element={<Home servers={servers} searchState={searchState} />}
/>
<Route
path="/server/:id"
path="/server/:url"
element={<ServerDetails setAlertInfo={setAlertInfo} />}
/>
<Route path="/about" element={<About />} />

View File

@ -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;
}

View File

@ -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({
<StyledInputBase
inputRef={inputRef}
placeholder="URL"
inputProps={{ 'aria-label': 'add-server' }}
autoComplete="off"
inputProps={{
name: 'server-url',
'aria-label': 'add-server',
}}
onKeyUp={handleKeyUp}
disabled={isLoading}
endAdornment={

View File

@ -183,7 +183,7 @@ export default function SearchServers({
flexDirection="column"
alignItems="center"
>
<Typography id="max-level-slider" variant="caption">
<Typography id="max-level" variant="caption">
Max Level
</Typography>
</Grid>
@ -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',
}}
/>
<Slider
aria-labelledby="max-level-slider"
aria-labelledby="max-level"
value={searchState.maxLevel}
onChange={handleSearchMaxLevel}
valueLabelDisplay="auto"
@ -218,11 +219,12 @@ export default function SearchServers({
onChange={handleSearchMaxLevelMax}
// onBlur={handleBlur}
inputProps={{
name: 'max-level-max',
step: 1,
min: 1,
max: 99,
type: 'number',
'aria-labelledby': 'max-level-max',
'aria-labelledby': 'max-level',
}}
/>
</Grid>

View File

@ -330,7 +330,7 @@ export default function ServerCard({ server }: { server: ServerData }) {
<Tooltip title="View full settings." arrow disableInteractive>
<IconButton
component={Link}
to={`/server/${server.id}`}
to={`/server/${encodeURIComponent(server.url)}`}
onClick={scrollToTop}
className="p-0"
disableRipple

View File

@ -12,7 +12,6 @@ export interface ServerSettings {
}
export default interface ServerData {
id: number;
name: string;
url: string;
location: string;
@ -26,7 +25,6 @@ export default interface ServerData {
}
export const DemoServerData: ServerData = {
id: 0,
name: 'LandSandBoat Demo',
url: 'github.com/LandSandBoat/server',
location: 'NA',

View File

@ -129,7 +129,7 @@ export default function Home({
<ErrorCard error="" />
) : (
filteredServers.map((server: ServerData) => (
<ServerCard key={server.id} server={server} />
<ServerCard key={server.url} server={server} />
))
)}
</Box>

View File

@ -30,7 +30,7 @@ export default function ServerDetails({
}: {
setAlertInfo: React.Dispatch<React.SetStateAction<AlertResponse>>;
}) {
const { id } = useParams();
const { url } = useParams();
const [server, setServer] = useState<ServerData>();
const [error, setError] = useState<string>('');
@ -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 (