diff --git a/.gitattributes b/.gitattributes index 996deab..fa6bf5e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,13 +8,11 @@ # # Note that binary is a macro for -text -diff. ###################################################################### - # Auto detect ## Handle line endings automatically for files detected as ## text and leave all files detected as binary untouched. ## This will handle all files NOT defined below. * text=auto - # Source code *.bash text eol=lf *.bat text eol=crlf @@ -52,10 +50,8 @@ *.tsx text *.xml text *.xhtml text diff=html - # Docker Dockerfile text - # Documentation *.ipynb text eol=lf *.markdown text diff=markdown @@ -81,7 +77,6 @@ NEWS text readme text *README* text TODO text - # Templates *.dot text *.ejs text @@ -100,7 +95,6 @@ TODO text *.tpl text *.twig text *.vue text - # Configs *.cnf text *.conf text @@ -124,10 +118,8 @@ Makefile text makefile text # Fixes syntax highlighting on GitHub to allow comments tsconfig.json linguist-language=JSON-with-Comments - # Heroku Procfile text - # Graphics *.ai binary *.bmp binary @@ -155,7 +147,6 @@ Procfile text *.tiff binary *.wbmp binary *.webp binary - # Audio *.kar binary *.m4a binary @@ -164,7 +155,6 @@ Procfile text *.mp3 binary *.ogg binary *.ra binary - # Video *.3gpp binary *.3gp binary @@ -184,7 +174,6 @@ Procfile text *.swc binary *.swf binary *.webm binary - # Archives *.7z binary *.gz binary @@ -192,26 +181,22 @@ Procfile text *.rar binary *.tar binary *.zip binary - # Fonts *.ttf binary *.eot binary *.otf binary *.woff binary *.woff2 binary - # Executables *.exe binary *.pyc binary # Prevents massive diffs caused by vendored, minified files **/.yarn/releases/** binary **/.yarn/plugins/** binary - # RC files (like .babelrc or .eslintrc) *.*rc text - # Ignore files (like .npmignore or .gitignore) *.*ignore text - # Prevents massive diffs from built files dist/* binary +*.mmdb filter=lfs diff=lfs merge=lfs -text diff --git a/README.md b/README.md index c16e7c0..d1488ce 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ Ixion is designed to catalog public [LandSandBoat](https://github.com/LandSandBo The React client fetches server data from the API and displays it in a relevant way to users. Filtering is built into the client for now. When the API response fails or is empty, a default LSB demo server is displayed, pulling some info from GitHub. +[IP Geolocation by DB-IP](https://db-ip.com) + ## Features - **Automatic Verification**: Verifies LSB server URL by sending a request to the public API. [Requires the LSB server to enable the HTTP server.](https://github.com/LandSandBoat/server/blob/df311283c4abb779d212e2b8af6734b0d8d11ad7/settings/default/network.lua#L33) diff --git a/api/.env.example b/api/.env.example index 134ee8a..3c838ed 100644 --- a/api/.env.example +++ b/api/.env.example @@ -5,4 +5,4 @@ CSRF_TRUSTED_ORIGINS=https://api.ixion.dev CORS_ALLOWED_ORIGINS=https://ixion.dev # API configuration -SERVER_INACTIVITY_TIMEOUT=24 +SERVER_UPDATE_INTERVAL=10 diff --git a/api/api/celery.py b/api/api/celery.py index fa035b6..fb2454b 100644 --- a/api/api/celery.py +++ b/api/api/celery.py @@ -26,7 +26,9 @@ app.conf.result_backend = f"redis://redis:6379" app.conf.beat_schedule = { "verify-urls-every-hour": { "task": "api.tasks.verify_urls_task", - "schedule": timedelta(hours=1), + "schedule": timedelta( + minutes=int(os.getenv("SERVER_UPDATE_INTERVAL", default="10")) + ), # "schedule": timedelta(minutes=1), }, } diff --git a/api/api/settings.py b/api/api/settings.py index 188eebb..c6f340b 100644 --- a/api/api/settings.py +++ b/api/api/settings.py @@ -42,6 +42,7 @@ INSTALLED_APPS = [ "django.contrib.staticfiles", "corsheaders", "rest_framework", + "geoip2", "v1", ] @@ -139,3 +140,5 @@ CORS_ALLOWED_ORIGINS = os.getenv( CORS_ALLOW_ALL_ORIGINS = ( os.getenv("CORS_ALLOW_ALL_ORIGINS", default="False").lower() == "true" ) + +GEOIP_PATH = os.path.join(BASE_DIR, "geoip") diff --git a/api/requirements.txt b/api/requirements.txt index 93cffba..eb9dee6 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -6,3 +6,4 @@ celery redis python-dotenv gunicorn +geoip2 diff --git a/api/v1/migrations/0002_server_location.py b/api/v1/migrations/0002_server_location.py new file mode 100644 index 0000000..bb47794 --- /dev/null +++ b/api/v1/migrations/0002_server_location.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.4 on 2024-04-22 19:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('v1', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='server', + name='location', + field=models.CharField(editable=False, max_length=255, null=True), + ), + ] diff --git a/api/v1/migrations/0003_remove_server_inactivity_counter_server_up_and_more.py b/api/v1/migrations/0003_remove_server_inactivity_counter_server_up_and_more.py new file mode 100644 index 0000000..d3798a2 --- /dev/null +++ b/api/v1/migrations/0003_remove_server_inactivity_counter_server_up_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 5.0.4 on 2024-04-22 20:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('v1', '0002_server_location'), + ] + + operations = [ + migrations.RemoveField( + model_name='server', + name='inactivity_counter', + ), + migrations.AddField( + model_name='server', + name='up', + field=models.BooleanField(editable=False, null=True), + ), + migrations.AlterField( + model_name='server', + name='updated', + field=models.DateTimeField(editable=False, null=True), + ), + ] diff --git a/api/v1/models/server.py b/api/v1/models/server.py index b05c0fe..5374e53 100644 --- a/api/v1/models/server.py +++ b/api/v1/models/server.py @@ -1,3 +1,4 @@ +import datetime import json import os import socket @@ -7,6 +8,7 @@ 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 # Required and recommended settings used to build the initial client server card required_settings = [ @@ -41,14 +43,20 @@ class Server(models.Model): null=False, validators=[OptionalSchemeURLValidator()], ) + location = models.CharField(max_length=255, null=True, editable=False) max_level = models.IntegerField(null=True, editable=False) settings = models.JSONField(null=True, editable=False) customizations = models.JSONField(null=True, editable=False) login_limit = models.IntegerField(null=True, editable=False) active_sessions = models.IntegerField(null=True, editable=False) created = models.DateTimeField(auto_now_add=True) - updated = models.DateTimeField(auto_now=True) - inactivity_counter = models.IntegerField(null=True, editable=False) + updated = models.DateTimeField(null=True, editable=False) + up = models.BooleanField(null=True, editable=False) + + @property + def expires(self): + time_since_creation = self.updated - self.created + return self.updated + min(time_since_creation, datetime.timedelta(hours=24)) def _str_(self): return self.url @@ -69,17 +77,14 @@ class Server(models.Model): # Validate the server API if not self.parse_server_api(): - if ( - self.inactivity_counter is None - or self.inactivity_counter - >= int(os.getenv("SERVER_INACTIVITY_TIMEOUT", default="24")) - 1 - ): + if self.expires <= datetime.datetime.now(datetime.timezone.utc): return False - self.inactivity_counter += 1 + self.up = False else: if self.settings["API.DO_NOT_TRACK"]: return False - self.inactivity_counter = 0 + self.up = True + self.updated = datetime.datetime.now(datetime.timezone.utc) # Proceed with saving the object logger.info(f"Saving Server object with URL: {self.url}") @@ -142,6 +147,10 @@ class Server(models.Model): # except socket.error: # return False + g = GeoIP2() + city = g.city(f"{self.url}") + self.location = city["continent_code"] + return True except requests.RequestException: diff --git a/api/v1/serializers/server.py b/api/v1/serializers/server.py index 992db72..213f420 100644 --- a/api/v1/serializers/server.py +++ b/api/v1/serializers/server.py @@ -10,12 +10,13 @@ class ServerSerializer(serializers.ModelSerializer): "id", "name", "url", + "location", "max_level", "customizations", "login_limit", "active_sessions", "updated", - "inactivity_counter", + "up", ] def create(self, validated_data): diff --git a/client/src/components/ServerCard.tsx b/client/src/components/ServerCard.tsx index d418bb5..907745b 100644 --- a/client/src/components/ServerCard.tsx +++ b/client/src/components/ServerCard.tsx @@ -142,32 +142,57 @@ export default function ServerCard({ server }: { server: ServerData }) { }> - - {server.customizations['LOGIN.MAINT_MODE'] === 1 ? ( + + + {server.customizations['LOGIN.MAINT_MODE'] === 1 ? ( + + + + ) : ( +
+ {!server.up ? ( + + + + ) : ( + + + + )} +
+ )} +
+ - + { + event.stopPropagation(); + }} + sx={{ + textDecoration: 'none', + }} + color={(theme) => alpha(theme.palette.text.primary, 0.5)} + > + {server.location} + - ) : ( -
- {server.inactivity_counter > 0 ? ( - - - - ) : ( - - - - )} -
- )} +
diff --git a/client/src/data/ServerData.tsx b/client/src/data/ServerData.tsx index 8d64c9a..19674bd 100644 --- a/client/src/data/ServerData.tsx +++ b/client/src/data/ServerData.tsx @@ -15,19 +15,21 @@ export default interface ServerData { id: number; name: string; url: string; + location: string; max_level: number; settings?: ServerSettings; customizations: ServerSettings; login_limit: number; active_sessions: number; updated: string; - inactivity_counter: number; + up: boolean; } export const DemoServerData: ServerData = { id: 0, name: 'LandSandBoat Demo', url: 'github.com/LandSandBoat/server', + location: 'NA', max_level: 99, settings: LsbDefaults, customizations: { @@ -43,7 +45,7 @@ export const DemoServerData: ServerData = { login_limit: 0, active_sessions: 0, updated: new Date().toISOString(), - inactivity_counter: 0, + up: true, }; export const ServerSettingsInfo: Record = { diff --git a/client/src/pages/ServerDetails.tsx b/client/src/pages/ServerDetails.tsx index 0a036c1..c274b60 100644 --- a/client/src/pages/ServerDetails.tsx +++ b/client/src/pages/ServerDetails.tsx @@ -108,32 +108,61 @@ export default function ServerDetails({ ) : ( - - {server.customizations['LOGIN.MAINT_MODE'] === 1 ? ( + + + {server.customizations['LOGIN.MAINT_MODE'] === 1 ? ( + + + + ) : ( +
+ {!server.up ? ( + + + + ) : ( + + + + )} +
+ )} +
+ - + { + event.stopPropagation(); + }} + sx={{ + textDecoration: 'none', + }} + color={(theme) => alpha(theme.palette.text.primary, 0.5)} + > + {server.location} + - ) : ( -
- {server.inactivity_counter > 0 ? ( - - - - ) : ( - - - - )} -
- )} +