mirror of https://github.com/cocosolos/ixion.git
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:
parent
9b0a1701d4
commit
739cfdd07e
|
|
@ -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()]),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -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()]),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import socket
|
|
||||||
import requests
|
import requests
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.core.validators import URLValidator
|
from django.core.validators import URLValidator
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from v1.common.logging import logger
|
from v1.common.logging import logger
|
||||||
from django.contrib.gis.geoip2 import GeoIP2
|
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 and recommended settings used to build the initial client server card
|
||||||
required_settings = [
|
required_settings = [
|
||||||
|
|
@ -39,9 +38,11 @@ class OptionalSchemeURLValidator(URLValidator):
|
||||||
class Server(models.Model):
|
class Server(models.Model):
|
||||||
name = models.CharField(max_length=255, null=True, editable=False)
|
name = models.CharField(max_length=255, null=True, editable=False)
|
||||||
url = models.CharField(
|
url = models.CharField(
|
||||||
|
primary_key=True,
|
||||||
max_length=255,
|
max_length=255,
|
||||||
null=False,
|
null=False,
|
||||||
validators=[OptionalSchemeURLValidator()],
|
validators=[OptionalSchemeURLValidator()],
|
||||||
|
error_messages={"unique": "A server with this URL already exists."},
|
||||||
)
|
)
|
||||||
location = models.CharField(max_length=255, null=True, editable=False)
|
location = models.CharField(max_length=255, null=True, editable=False)
|
||||||
max_level = models.IntegerField(null=True, editable=False)
|
max_level = models.IntegerField(null=True, editable=False)
|
||||||
|
|
@ -55,8 +56,11 @@ class Server(models.Model):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def expires(self):
|
def expires(self):
|
||||||
time_since_creation = self.updated - self.created
|
try:
|
||||||
return self.updated + min(time_since_creation, datetime.timedelta(hours=24))
|
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):
|
def _str_(self):
|
||||||
return self.url
|
return self.url
|
||||||
|
|
@ -70,13 +74,10 @@ class Server(models.Model):
|
||||||
parsed_url = urlparse(self.url)
|
parsed_url = urlparse(self.url)
|
||||||
self.url = parsed_url.netloc.lower()
|
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
|
# Validate the server API
|
||||||
if not self.parse_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):
|
if self.expires <= datetime.datetime.now(datetime.timezone.utc):
|
||||||
return False
|
return False
|
||||||
self.up = False
|
self.up = False
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ class ServerSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Server
|
model = Server
|
||||||
fields = [
|
fields = [
|
||||||
"id",
|
|
||||||
"name",
|
"name",
|
||||||
"url",
|
"url",
|
||||||
"location",
|
"location",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
|
from django.http import Http404
|
||||||
from rest_framework import viewsets, mixins
|
from rest_framework import viewsets, mixins
|
||||||
|
from rest_framework.response import Response
|
||||||
from django.db.models.fields.json import KT
|
from django.db.models.fields.json import KT
|
||||||
from v1.serializers.server import ServerDetailsSerializer, ServerSerializer
|
from v1.serializers.server import ServerDetailsSerializer, ServerSerializer
|
||||||
from v1.models.server import Server
|
from v1.models.server import Server
|
||||||
|
from rest_framework import status
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
class ServerViewSet(
|
class ServerViewSet(
|
||||||
|
|
@ -43,8 +47,53 @@ class ServerViewSet(
|
||||||
|
|
||||||
|
|
||||||
class ServerDetailsViewSet(
|
class ServerDetailsViewSet(
|
||||||
|
mixins.CreateModelMixin,
|
||||||
mixins.RetrieveModelMixin,
|
mixins.RetrieveModelMixin,
|
||||||
|
mixins.ListModelMixin,
|
||||||
viewsets.GenericViewSet,
|
viewsets.GenericViewSet,
|
||||||
):
|
):
|
||||||
queryset = Server.objects.all()
|
queryset = Server.objects.all()
|
||||||
serializer_class = ServerDetailsSerializer
|
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.")
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ export function App() {
|
||||||
element={<Home servers={servers} searchState={searchState} />}
|
element={<Home servers={servers} searchState={searchState} />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/server/:id"
|
path="/server/:url"
|
||||||
element={<ServerDetails setAlertInfo={setAlertInfo} />}
|
element={<ServerDetails setAlertInfo={setAlertInfo} />}
|
||||||
/>
|
/>
|
||||||
<Route path="/about" element={<About />} />
|
<Route path="/about" element={<About />} />
|
||||||
|
|
|
||||||
|
|
@ -26,23 +26,34 @@ export const fetchDataFromBackend = async (
|
||||||
queryParams = {}
|
queryParams = {}
|
||||||
) => {
|
) => {
|
||||||
const queryString = new URLSearchParams(queryParams).toString();
|
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 {
|
try {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
signal: AbortSignal.timeout(5000),
|
signal: AbortSignal.timeout(5000),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
let errorMessage =
|
||||||
`(${response.status.toString()} - ${response.statusText || 'unknown'})`
|
'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();
|
return await response.json();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
throw new Error(`${err.message} - ${url}`);
|
throw new Error(err.message);
|
||||||
} else {
|
} 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 }),
|
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;
|
let success = false;
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
// bad url or duplicate
|
// bad url or duplicate
|
||||||
|
|
@ -73,13 +84,13 @@ export const postData = async (inputText: string) => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const responseData = await response.json();
|
const responseData = await response.json();
|
||||||
if (responseData.id !== null) {
|
if (response.status === 201) {
|
||||||
message = 'Added new server!';
|
message = 'Added new server!';
|
||||||
success = true;
|
success = true;
|
||||||
}
|
}
|
||||||
return { message, success, data: responseData };
|
return { message, success, data: responseData };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
let message = 'An unknown error occurred.';
|
let message = 'An unhandled error while sending data to the backend.';
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
message = err.message;
|
message = err.message;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ export default function AddServer({
|
||||||
severity: 'success',
|
severity: 'success',
|
||||||
});
|
});
|
||||||
if (response.data && typeof response.data === 'object') {
|
if (response.data && typeof response.data === 'object') {
|
||||||
navigate(`/server/${response.data.id}`);
|
navigate(`/server/${encodeURIComponent(response.data.url)}`);
|
||||||
setServers([response.data, ...servers]);
|
setServers([response.data, ...servers]);
|
||||||
}
|
}
|
||||||
if (inputRef.current) {
|
if (inputRef.current) {
|
||||||
|
|
@ -119,7 +119,11 @@ export default function AddServer({
|
||||||
<StyledInputBase
|
<StyledInputBase
|
||||||
inputRef={inputRef}
|
inputRef={inputRef}
|
||||||
placeholder="URL"
|
placeholder="URL"
|
||||||
inputProps={{ 'aria-label': 'add-server' }}
|
autoComplete="off"
|
||||||
|
inputProps={{
|
||||||
|
name: 'server-url',
|
||||||
|
'aria-label': 'add-server',
|
||||||
|
}}
|
||||||
onKeyUp={handleKeyUp}
|
onKeyUp={handleKeyUp}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
endAdornment={
|
endAdornment={
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,7 @@ export default function SearchServers({
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
>
|
>
|
||||||
<Typography id="max-level-slider" variant="caption">
|
<Typography id="max-level" variant="caption">
|
||||||
Max Level
|
Max Level
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
@ -194,15 +194,16 @@ export default function SearchServers({
|
||||||
onChange={handleSearchMaxLevelMin}
|
onChange={handleSearchMaxLevelMin}
|
||||||
// onBlur={handleBlur}
|
// onBlur={handleBlur}
|
||||||
inputProps={{
|
inputProps={{
|
||||||
|
name: 'max-level-min',
|
||||||
step: 1,
|
step: 1,
|
||||||
min: 1,
|
min: 1,
|
||||||
max: 99,
|
max: 99,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
'aria-labelledby': 'max-level-min',
|
'aria-labelledby': 'max-level',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Slider
|
<Slider
|
||||||
aria-labelledby="max-level-slider"
|
aria-labelledby="max-level"
|
||||||
value={searchState.maxLevel}
|
value={searchState.maxLevel}
|
||||||
onChange={handleSearchMaxLevel}
|
onChange={handleSearchMaxLevel}
|
||||||
valueLabelDisplay="auto"
|
valueLabelDisplay="auto"
|
||||||
|
|
@ -218,11 +219,12 @@ export default function SearchServers({
|
||||||
onChange={handleSearchMaxLevelMax}
|
onChange={handleSearchMaxLevelMax}
|
||||||
// onBlur={handleBlur}
|
// onBlur={handleBlur}
|
||||||
inputProps={{
|
inputProps={{
|
||||||
|
name: 'max-level-max',
|
||||||
step: 1,
|
step: 1,
|
||||||
min: 1,
|
min: 1,
|
||||||
max: 99,
|
max: 99,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
'aria-labelledby': 'max-level-max',
|
'aria-labelledby': 'max-level',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
||||||
|
|
@ -330,7 +330,7 @@ export default function ServerCard({ server }: { server: ServerData }) {
|
||||||
<Tooltip title="View full settings." arrow disableInteractive>
|
<Tooltip title="View full settings." arrow disableInteractive>
|
||||||
<IconButton
|
<IconButton
|
||||||
component={Link}
|
component={Link}
|
||||||
to={`/server/${server.id}`}
|
to={`/server/${encodeURIComponent(server.url)}`}
|
||||||
onClick={scrollToTop}
|
onClick={scrollToTop}
|
||||||
className="p-0"
|
className="p-0"
|
||||||
disableRipple
|
disableRipple
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ export interface ServerSettings {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default interface ServerData {
|
export default interface ServerData {
|
||||||
id: number;
|
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
location: string;
|
location: string;
|
||||||
|
|
@ -26,7 +25,6 @@ export default interface ServerData {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DemoServerData: ServerData = {
|
export const DemoServerData: ServerData = {
|
||||||
id: 0,
|
|
||||||
name: 'LandSandBoat Demo',
|
name: 'LandSandBoat Demo',
|
||||||
url: 'github.com/LandSandBoat/server',
|
url: 'github.com/LandSandBoat/server',
|
||||||
location: 'NA',
|
location: 'NA',
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@ export default function Home({
|
||||||
<ErrorCard error="" />
|
<ErrorCard error="" />
|
||||||
) : (
|
) : (
|
||||||
filteredServers.map((server: ServerData) => (
|
filteredServers.map((server: ServerData) => (
|
||||||
<ServerCard key={server.id} server={server} />
|
<ServerCard key={server.url} server={server} />
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ export default function ServerDetails({
|
||||||
}: {
|
}: {
|
||||||
setAlertInfo: React.Dispatch<React.SetStateAction<AlertResponse>>;
|
setAlertInfo: React.Dispatch<React.SetStateAction<AlertResponse>>;
|
||||||
}) {
|
}) {
|
||||||
const { id } = useParams();
|
const { url } = useParams();
|
||||||
const [server, setServer] = useState<ServerData>();
|
const [server, setServer] = useState<ServerData>();
|
||||||
const [error, setError] = useState<string>('');
|
const [error, setError] = useState<string>('');
|
||||||
|
|
||||||
|
|
@ -38,10 +38,10 @@ export default function ServerDetails({
|
||||||
let data: ServerData;
|
let data: ServerData;
|
||||||
const fetchServerData = async () => {
|
const fetchServerData = async () => {
|
||||||
try {
|
try {
|
||||||
if (id === 'demo') {
|
if (url === 'demo') {
|
||||||
data = await fetchDemo();
|
data = await fetchDemo();
|
||||||
} else {
|
} else {
|
||||||
data = await fetchDataFromBackend(`server/${id}`);
|
data = await fetchDataFromBackend(`server/?url=${url}`);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
|
|
@ -58,7 +58,7 @@ export default function ServerDetails({
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchServerData();
|
fetchServerData();
|
||||||
}, [id, setServer, setAlertInfo, setError]);
|
}, [url, setServer, setAlertInfo, setError]);
|
||||||
|
|
||||||
const [clipboardTooltip, setClipboardTooltip] = useState('Copy server URL.');
|
const [clipboardTooltip, setClipboardTooltip] = useState('Copy server URL.');
|
||||||
const [clipboardTooltipOpen, setClipboardTooltipOpen] = useState(false);
|
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
|
// prepend 'https://' to the URL if it's not already there
|
||||||
if (!/^https?:\/\//i.test(url)) {
|
if (!/^https?:\/\//i.test(serverUrl)) {
|
||||||
return `https://${url}`;
|
return `https://${serverUrl}`;
|
||||||
}
|
}
|
||||||
return url;
|
return serverUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue