Current status
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
.venv
|
||||
*.pyc
|
||||
__pycache__/
|
||||
db.sqlite3
|
||||
settings.ini
|
20
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python: Django",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/manage.py",
|
||||
"args": [
|
||||
"runserver",
|
||||
"8102"
|
||||
],
|
||||
"django": true,
|
||||
"justMyCode": true
|
||||
}
|
||||
]
|
||||
}
|
0
apps/__init__.py
Normal file
6
apps/admin.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from .models import App, AppURL
|
||||
|
||||
admin.site.register(App)
|
||||
admin.site.register(AppURL)
|
6
apps/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AppsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps'
|
47
apps/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,47 @@
|
|||
# Generated by Django 4.2.5 on 2023-10-02 18:25
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="App",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=100)),
|
||||
("key1", models.UUIDField(default=uuid.uuid4, editable=False)),
|
||||
("key2", models.UUIDField(default=uuid.uuid4, editable=False)),
|
||||
(
|
||||
"registration_key",
|
||||
models.UUIDField(default=uuid.uuid4, editable=False),
|
||||
),
|
||||
("registered", models.BooleanField(default=False)),
|
||||
("last_heartbeat", models.DateTimeField(blank=True, null=True)),
|
||||
(
|
||||
"owner",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 4.2.5 on 2023-10-03 08:47
|
||||
|
||||
from django.db import migrations, models
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("apps", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="app",
|
||||
name="key1",
|
||||
field=models.UUIDField(default=uuid.uuid4),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="app",
|
||||
name="key2",
|
||||
field=models.UUIDField(default=uuid.uuid4),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="app",
|
||||
name="registration_key",
|
||||
field=models.UUIDField(default=uuid.uuid4),
|
||||
),
|
||||
]
|
29
apps/migrations/0003_appurl.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
# Generated by Django 4.2.5 on 2023-10-05 16:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("apps", "0002_alter_app_key1_alter_app_key2_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="AppURL",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=128)),
|
||||
("build", models.IntegerField()),
|
||||
("url", models.CharField(max_length=1024)),
|
||||
],
|
||||
),
|
||||
]
|
0
apps/migrations/__init__.py
Normal file
130
apps/models.py
Normal file
|
@ -0,0 +1,130 @@
|
|||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
|
||||
import pyqrcode
|
||||
|
||||
import uuid
|
||||
import json
|
||||
|
||||
from typing import Optional
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class App(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
name = models.CharField(max_length=100)
|
||||
owner = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
key1 = models.UUIDField(default=uuid.uuid4)
|
||||
key2 = models.UUIDField(default=uuid.uuid4)
|
||||
registration_key = models.UUIDField(default=uuid.uuid4)
|
||||
registered = models.BooleanField(default=False)
|
||||
last_heartbeat = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} - {self.owner.username}"
|
||||
|
||||
@classmethod
|
||||
def get_by_key(cls, key1: str, key2: Optional[str] = None) -> "App":
|
||||
"""Get an app installation by its key
|
||||
|
||||
Args:
|
||||
key1 (str): Either the first part of the key or the whole key
|
||||
key2 (str, optional): Either the second part of the key or None, if key1 contains the full key. Defaults to None.
|
||||
|
||||
Returns:
|
||||
App: The app installation
|
||||
|
||||
Raises:
|
||||
App.DoesNotExist: If the app installation does not exist
|
||||
ValueError: If key2 is None and key1 does not contain the full key
|
||||
"""
|
||||
if key2 is None:
|
||||
key1, key2 = key1.split(":")
|
||||
|
||||
return cls.objects.get(key1=key1, key2=key2)
|
||||
|
||||
def get_registration_data(self, qr=False) -> str:
|
||||
"""Get the data for app installation
|
||||
|
||||
Returns:
|
||||
str: The QR code as a string
|
||||
"""
|
||||
data = {
|
||||
"id": self.id,
|
||||
"key": self.registration_key,
|
||||
"name": self.name,
|
||||
"hostname": settings.ALLOWED_HOSTS[0],
|
||||
}
|
||||
|
||||
if qr:
|
||||
content = json.dumps(data)
|
||||
return pyqrcode.create(content).png_as_base64_str(scale=5)
|
||||
|
||||
return data
|
||||
|
||||
def heartbeat(self):
|
||||
"""Process a heartbeat from the app installation
|
||||
"""
|
||||
self.last_heartbeat = timezone.now()
|
||||
self.save()
|
||||
|
||||
def heartbeat_too_old(self) -> bool:
|
||||
"""Check whether the last heartbeat is too old
|
||||
|
||||
Returns:
|
||||
bool: Whether the last heartbeat is too old
|
||||
"""
|
||||
if self.last_heartbeat is None:
|
||||
return True
|
||||
|
||||
max_age = settings.SETTINGS["EXPAlert"].get("MaxHeartbeatAge", 1800)
|
||||
|
||||
return (timezone.now() - self.last_heartbeat).total_seconds() > 60
|
||||
|
||||
@classmethod
|
||||
def register(cls, registration_key: str) -> "App":
|
||||
"""Register an app installation
|
||||
|
||||
Args:
|
||||
registration_key (str): The registration key
|
||||
|
||||
Returns:
|
||||
App: The app installation
|
||||
"""
|
||||
app = cls.objects.get(registration_key=registration_key)
|
||||
|
||||
assert not app.registered, "App installation already registered"
|
||||
|
||||
app.registered = True
|
||||
app.save()
|
||||
|
||||
return app
|
||||
|
||||
def get_monitoring_url(self):
|
||||
"""Get the monitoring URL
|
||||
|
||||
Returns:
|
||||
str: The monitoring URL
|
||||
"""
|
||||
return f"https://{settings.ALLOWED_HOSTS[0]}{reverse('monitor')}?id={self.id}"
|
||||
|
||||
def get_json_url(self):
|
||||
"""Get the JSON URL
|
||||
|
||||
Returns:
|
||||
str: The JSON URL
|
||||
"""
|
||||
return f"https://{settings.ALLOWED_HOSTS[0]}{reverse('json')}?id={self.id}"
|
||||
|
||||
|
||||
class AppURL(models.Model):
|
||||
name = models.CharField(max_length=128)
|
||||
build = models.IntegerField()
|
||||
url = models.CharField(max_length=1024)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
3
apps/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
15
apps/urls.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
from django.urls import path
|
||||
|
||||
from .views import HeartbeatView, RegisterView, MonitorView, AppJSONView, SolveView, PanicView, UpdateView
|
||||
|
||||
APP = "apps"
|
||||
|
||||
urlpatterns = [
|
||||
path('monitor', MonitorView.as_view(), name='monitor'),
|
||||
path('register', RegisterView.as_view(), name='register'),
|
||||
path('heartbeat', HeartbeatView.as_view(), name='heartbeat'),
|
||||
path('json', AppJSONView.as_view(), name='json'),
|
||||
path('solve', SolveView.as_view(), name='solve'),
|
||||
path('panic', PanicView.as_view(), name='panic'),
|
||||
path('update', UpdateView.as_view(), name='update'),
|
||||
]
|
7
apps/views/__init__.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from .heartbeat import HeartbeatView
|
||||
from .register import RegisterView
|
||||
from .monitor import MonitorView
|
||||
from .json import AppJSONView
|
||||
from .solve import SolveView
|
||||
from .panic import PanicView
|
||||
from .update import UpdateView
|
59
apps/views/heartbeat.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
from django.views.generic import View
|
||||
from django.http.response import JsonResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
from ..models import App
|
||||
from frontend.models import Panic
|
||||
|
||||
import json
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class HeartbeatView(View):
|
||||
"""A view that processes a heartbeat request from the app installation"""
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Process a heartbeat request from the app installation
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The request
|
||||
*args: Additional arguments
|
||||
**kwargs: Additional keyword arguments
|
||||
|
||||
Returns:
|
||||
JsonResponse: The response
|
||||
"""
|
||||
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
key = data["key"]
|
||||
|
||||
app = App.get_by_key(key)
|
||||
app.heartbeat()
|
||||
|
||||
responseData = {"success": True}
|
||||
|
||||
if panic := Panic.active():
|
||||
responseData["panic"] = True
|
||||
responseData["triggered_by"] = panic.created_by_user.username
|
||||
responseData["triggered_at"] = panic.created_at
|
||||
responseData["reason"] = panic.create_reason
|
||||
|
||||
else:
|
||||
responseData["panic"] = False
|
||||
|
||||
return JsonResponse(responseData)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({"success": False, "error": "Invalid JSON"})
|
||||
|
||||
except KeyError:
|
||||
return JsonResponse(
|
||||
{"success": False, "error": "Missing app installation key"}
|
||||
)
|
||||
|
||||
except App.DoesNotExist:
|
||||
return JsonResponse(
|
||||
{"success": False, "error": "Invalid app installation key"}
|
||||
)
|
39
apps/views/json.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
from django.views.generic import View
|
||||
from django.http.response import JsonResponse
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
from ..models import App
|
||||
|
||||
|
||||
@method_decorator(login_required, name='dispatch')
|
||||
class AppJSONView(View):
|
||||
"""A view that returns the JSON required for the app installation
|
||||
"""
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Return the JSON required for the app installation
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The request
|
||||
*args: Additional arguments
|
||||
**kwargs: Additional keyword arguments
|
||||
|
||||
Returns:
|
||||
JsonResponse: The response
|
||||
"""
|
||||
|
||||
app_id = request.GET["id"]
|
||||
|
||||
try:
|
||||
app = App.objects.get(id=app_id)
|
||||
assert app.owner == request.user or request.user.is_superuser
|
||||
|
||||
headers = {
|
||||
"Content-Disposition": "attachment; filename=expalert-config.json",
|
||||
}
|
||||
|
||||
return JsonResponse(app.get_registration_data(), headers=headers)
|
||||
|
||||
except (App.DoesNotExist, AssertionError):
|
||||
return JsonResponse({"success": False, "error": "Invalid app ID"})
|
79
apps/views/monitor.py
Normal file
|
@ -0,0 +1,79 @@
|
|||
from django.views.generic import View
|
||||
from django.http.response import JsonResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
from ..models import App
|
||||
|
||||
import json
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class MonitorView(View):
|
||||
"""A view that returns information about a given app installation
|
||||
"""
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Return information about a given app installation
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The request
|
||||
*args: Additional arguments
|
||||
**kwargs: Additional keyword arguments
|
||||
|
||||
Returns:
|
||||
JsonResponse: The response
|
||||
"""
|
||||
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
app_id = data["id"]
|
||||
|
||||
app = App.objects.get(id=app_id)
|
||||
|
||||
return JsonResponse({
|
||||
"success": True,
|
||||
"name": app.name,
|
||||
"owner": app.owner.username,
|
||||
"last_heartbeat": app.last_heartbeat,
|
||||
"heartbeat_too_old": app.heartbeat_too_old(),
|
||||
})
|
||||
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({"success": False, "error": "Invalid JSON"})
|
||||
|
||||
except KeyError:
|
||||
return JsonResponse({"success": False, "error": "Missing app installation key"})
|
||||
|
||||
except App.DoesNotExist:
|
||||
return JsonResponse({"success": False, "error": "Invalid app installation key"})
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Return information about a given app installation
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The request
|
||||
*args: Additional arguments
|
||||
**kwargs: Additional keyword arguments
|
||||
|
||||
Returns:
|
||||
JsonResponse: The response
|
||||
"""
|
||||
|
||||
try:
|
||||
app_id = request.GET["id"]
|
||||
|
||||
app = App.objects.get(id=app_id)
|
||||
|
||||
return JsonResponse({
|
||||
"success": True,
|
||||
"name": app.name,
|
||||
"owner": app.owner.username,
|
||||
"last_heartbeat": app.last_heartbeat,
|
||||
"heartbeat_too_old": app.heartbeat_too_old(),
|
||||
})
|
||||
|
||||
except KeyError:
|
||||
return JsonResponse({"success": False, "error": "Missing app installation key"})
|
||||
|
||||
except App.DoesNotExist:
|
||||
return JsonResponse({"success": False, "error": "Invalid app installation key"})
|
58
apps/views/panic.py
Normal file
|
@ -0,0 +1,58 @@
|
|||
from django.views.generic import View
|
||||
from django.http.response import JsonResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils import timezone
|
||||
|
||||
from ..models import App
|
||||
from frontend.models import Panic
|
||||
|
||||
import json
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class PanicView(View):
|
||||
"""A view that processes a panic creation request from the app installation"""
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Process a heartbeat request from the app installation
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The request
|
||||
*args: Additional arguments
|
||||
**kwargs: Additional keyword arguments
|
||||
|
||||
Returns:
|
||||
JsonResponse: The response
|
||||
"""
|
||||
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
key = data["key"]
|
||||
|
||||
app = App.get_by_key(key)
|
||||
|
||||
assert not Panic.active()
|
||||
|
||||
panic = Panic.objects.create(
|
||||
created_by_app=app,
|
||||
created_by_user=app.owner,
|
||||
create_reason=data["reason"],
|
||||
)
|
||||
|
||||
responseData = {"success": True}
|
||||
|
||||
return JsonResponse(responseData)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({"success": False, "error": "Invalid JSON"})
|
||||
|
||||
except KeyError:
|
||||
return JsonResponse(
|
||||
{"success": False, "error": "Missing app installation key"}
|
||||
)
|
||||
|
||||
except App.DoesNotExist:
|
||||
return JsonResponse(
|
||||
{"success": False, "error": "Invalid app installation key"}
|
||||
)
|
45
apps/views/register.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
from django.views.generic import View
|
||||
from django.http.response import JsonResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
from ..models import App
|
||||
|
||||
import json
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class RegisterView(View):
|
||||
"""A view that processes a registration request from the app installation
|
||||
"""
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Process a registration request from the app installation
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The request
|
||||
*args: Additional arguments
|
||||
**kwargs: Additional keyword arguments
|
||||
|
||||
Returns:
|
||||
JsonResponse: The response
|
||||
"""
|
||||
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
key = data["key"]
|
||||
|
||||
app = App.register(key)
|
||||
|
||||
return JsonResponse({"success": True, "key": f"{app.key1}:{app.key2}"})
|
||||
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({"success": False, "error": "Invalid JSON"})
|
||||
|
||||
except KeyError:
|
||||
return JsonResponse({"success": False, "error": "Missing registration key"})
|
||||
|
||||
except App.DoesNotExist:
|
||||
return JsonResponse({"success": False, "error": "Invalid registration key"})
|
||||
|
||||
except AssertionError as e:
|
||||
return JsonResponse({"success": False, "error": str(e)})
|
63
apps/views/solve.py
Normal file
|
@ -0,0 +1,63 @@
|
|||
from django.views.generic import View
|
||||
from django.http.response import JsonResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils import timezone
|
||||
|
||||
from ..models import App
|
||||
from frontend.models import Panic
|
||||
|
||||
import json
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class SolveView(View):
|
||||
"""A view that processes a solving request from the app installation"""
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Process a heartbeat request from the app installation
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The request
|
||||
*args: Additional arguments
|
||||
**kwargs: Additional keyword arguments
|
||||
|
||||
Returns:
|
||||
JsonResponse: The response
|
||||
"""
|
||||
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
key = data["key"]
|
||||
|
||||
app = App.get_by_key(key)
|
||||
|
||||
assert (panic := Panic.active())
|
||||
|
||||
panic.resolved_at = timezone.now()
|
||||
panic.resolved_by_user = app.owner
|
||||
panic.resolved_by_app = app
|
||||
panic.resolve_reason = data["reason"]
|
||||
panic.save()
|
||||
|
||||
responseData = {"success": True}
|
||||
|
||||
return JsonResponse(responseData)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({"success": False, "error": "Invalid JSON"})
|
||||
|
||||
except KeyError:
|
||||
return JsonResponse(
|
||||
{"success": False, "error": "Missing app installation key"}
|
||||
)
|
||||
|
||||
except AssertionError:
|
||||
return JsonResponse(
|
||||
{"success": False, "error": "No panic is currently active"}
|
||||
)
|
||||
|
||||
except App.DoesNotExist:
|
||||
return JsonResponse(
|
||||
{"success": False, "error": "Invalid app installation key"}
|
||||
)
|
32
apps/views/update.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
from django.views.generic import View
|
||||
from django.http.response import JsonResponse
|
||||
|
||||
from ..models import AppURL
|
||||
|
||||
|
||||
class UpdateView(View):
|
||||
"""A view that returns the currently available app versions
|
||||
"""
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Return the JSON required for the app installation
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The request
|
||||
*args: Additional arguments
|
||||
**kwargs: Additional keyword arguments
|
||||
|
||||
Returns:
|
||||
JsonResponse: The response
|
||||
"""
|
||||
|
||||
|
||||
app_versions = AppURL.objects.all()
|
||||
|
||||
data = {"versions": {}}
|
||||
|
||||
for app_version in app_versions:
|
||||
data["versions"][app_version.name] = {"build": app_version.build, "url": app_version.url}
|
||||
|
||||
|
||||
return JsonResponse(data)
|
0
custom_auth/__init__.py
Normal file
5
custom_auth/admin.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from .models import User
|
||||
|
||||
admin.site.register(User)
|
6
custom_auth/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CustomAuthConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "custom_auth"
|
40
custom_auth/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,40 @@
|
|||
# Generated by Django 4.2.5 on 2023-10-02 18:25
|
||||
|
||||
from django.db import migrations, models
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="User",
|
||||
fields=[
|
||||
("password", models.CharField(max_length=128, verbose_name="password")),
|
||||
(
|
||||
"last_login",
|
||||
models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="last login"
|
||||
),
|
||||
),
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("email", models.EmailField(max_length=254, unique=True)),
|
||||
("is_active", models.BooleanField(default=True)),
|
||||
("is_superuser", models.BooleanField(default=False)),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
]
|
19
custom_auth/migrations/0002_alter_user_managers.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 4.2.5 on 2023-10-03 08:47
|
||||
|
||||
import custom_auth.models
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("custom_auth", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelManagers(
|
||||
name="user",
|
||||
managers=[
|
||||
("objects", custom_auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
]
|
0
custom_auth/migrations/__init__.py
Normal file
50
custom_auth/models.py
Normal file
|
@ -0,0 +1,50 @@
|
|||
from django.contrib.auth.models import AbstractBaseUser, UserManager as DjangoUserManager
|
||||
from django.db import models
|
||||
|
||||
import uuid
|
||||
|
||||
class UserManager(DjangoUserManager):
|
||||
def create_user(self, email, password=None, **extra_fields):
|
||||
if not email:
|
||||
raise ValueError('Users must have an email address')
|
||||
|
||||
user = self.model(email=email, **extra_fields)
|
||||
if password:
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
|
||||
return user
|
||||
|
||||
def create_superuser(self, email, password=None, **extra_fields):
|
||||
extra_fields.setdefault('is_superuser', True)
|
||||
return self.create_user(email, password, **extra_fields)
|
||||
|
||||
|
||||
class User(AbstractBaseUser):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
email = models.EmailField(unique=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
is_superuser = models.BooleanField(default=False)
|
||||
|
||||
USERNAME_FIELD = 'email'
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
def __str__(self):
|
||||
return self.email
|
||||
|
||||
def has_perm(self, perm, obj=None):
|
||||
return True
|
||||
|
||||
def has_module_perms(self, app_label):
|
||||
return self.is_superuser
|
||||
|
||||
@property
|
||||
def is_staff(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def username(self):
|
||||
return self.email
|
||||
|
||||
|
37
custom_auth/templates/custom_auth/base.html
Normal file
|
@ -0,0 +1,37 @@
|
|||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>EXPAlert | Log in</title>
|
||||
|
||||
<!-- Google Font: Source Sans Pro -->
|
||||
<link rel="stylesheet" href="https://fontproxy.kumi.systems/css?family=Source+Sans+Pro:300,400,400i,700&display=fallback">
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://fa.kumi.systems/versions/pro-6.4.2/css/all.min.css">
|
||||
<!-- icheck bootstrap -->
|
||||
<link rel="stylesheet" href="{% static "dist/adminlte/plugins/icheck-bootstrap/icheck-bootstrap.min.css" %}">
|
||||
<!-- Theme style -->
|
||||
<link rel="stylesheet" href="{% static "dist/adminlte/dist/css/adminlte.min.css" %}">
|
||||
</head>
|
||||
<body class="hold-transition login-page">
|
||||
|
||||
<div class="login-box">
|
||||
<div class="login-logo">
|
||||
<a href="/"><b>EXP</b>Alert</a>
|
||||
</div>
|
||||
<!-- /.login-logo -->
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
<!-- /.login-box -->
|
||||
|
||||
<!-- jQuery -->
|
||||
<script src="{% static "dist/adminlte/plugins/jquery/jquery.min.js" %}"></script>
|
||||
<!-- Bootstrap 4 -->
|
||||
<script src="{% static "dist/adminlte/plugins/bootstrap/js/bootstrap.bundle.min.js" %}"></script>
|
||||
<!-- AdminLTE App -->
|
||||
<script src="{% static "dist/adminlte/dist/js/adminlte.min.js" %}"></script>
|
||||
</body>
|
||||
</html>
|
44
custom_auth/templates/custom_auth/login.html
Normal file
|
@ -0,0 +1,44 @@
|
|||
{% extends "custom_auth/base.html" %}
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-body login-card-body">
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<p class="login-box-msg">Sign in to start your session</p>
|
||||
|
||||
<form action="/auth/login/" method="post">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
|
||||
<div class="input-group mb-3">
|
||||
<input name="username" type="email" class="form-control" placeholder="Email">
|
||||
<div class="input-group-append">
|
||||
<div class="input-group-text">
|
||||
<span class="fas fa-envelope"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<input name="password" type="password" class="form-control" placeholder="Password">
|
||||
<div class="input-group-append">
|
||||
<div class="input-group-text">
|
||||
<span class="fas fa-lock"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-8">
|
||||
</div>
|
||||
<!-- /.col -->
|
||||
<div class="col-4">
|
||||
<button type="submit" class="btn btn-primary btn-block">Sign In</button>
|
||||
</div>
|
||||
<!-- /.col -->
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<!-- /.login-card-body -->
|
||||
</div>
|
||||
{% endblock %}
|
3
custom_auth/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
10
custom_auth/urls.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
from django.urls import path
|
||||
|
||||
from .views import LoginView, LogoutView
|
||||
|
||||
APP = "frontend"
|
||||
|
||||
urlpatterns = [
|
||||
path('login/', LoginView.as_view(), name='login'),
|
||||
path('logout/', LogoutView.as_view(), name='logout'),
|
||||
]
|
6
custom_auth/views.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.contrib.auth.views import LoginView as DjangoLoginView, LogoutView
|
||||
from django.urls import path
|
||||
|
||||
class LoginView(DjangoLoginView):
|
||||
template_name = "custom_auth/login.html"
|
||||
redirect_authenticated_user = True
|
0
expalert/__init__.py
Normal file
16
expalert/asgi.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
ASGI config for expalert project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'expalert.settings')
|
||||
|
||||
application = get_asgi_application()
|
130
expalert/settings.py
Normal file
|
@ -0,0 +1,130 @@
|
|||
from autosecretkey import AutoSecretKey
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
SETTINGS_FILE = Path(__file__).parent.parent / 'settings.ini'
|
||||
ASK = AutoSecretKey(SETTINGS_FILE)
|
||||
|
||||
SECRET_KEY = ASK.secret_key
|
||||
SETTINGS = ASK.config
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = SETTINGS["EXPAlert"]["Hostname"].split(",")
|
||||
CSRF_TRUSTED_ORIGINS = [f"https://{hostname}" for hostname in ALLOWED_HOSTS]
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
|
||||
'crispy_forms',
|
||||
'crispy_bootstrap5',
|
||||
|
||||
'custom_auth',
|
||||
'apps',
|
||||
'sms',
|
||||
'frontend',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'expalert.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'expalert.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db.sqlite3',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_USER_MODEL = 'custom_auth.User'
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
LOGIN_REDIRECT_URL = "/"
|
||||
LOGIN_URL = "/auth/login/"
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/4.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
STATIC_ROOT = BASE_DIR / 'static'
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
# Crispy forms
|
||||
|
||||
CRISPY_TEMPLATE_PACK = 'bootstrap5'
|
9
expalert/urls.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('apps/', include('apps.urls')),
|
||||
path('auth/', include('custom_auth.urls')),
|
||||
path('', include('frontend.urls')),
|
||||
]
|
16
expalert/wsgi.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
WSGI config for expalert project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'expalert.settings')
|
||||
|
||||
application = get_wsgi_application()
|
0
frontend/__init__.py
Normal file
3
frontend/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
6
frontend/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class FrontendConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "frontend"
|
75
frontend/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,75 @@
|
|||
# Generated by Django 4.2.5 on 2023-10-03 08:47
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("apps", "0002_alter_app_key1_alter_app_key2_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Panic",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("create_reason", models.TextField()),
|
||||
("resolved_at", models.DateTimeField(blank=True, null=True)),
|
||||
("resolve_reason", models.TextField(blank=True, null=True)),
|
||||
(
|
||||
"created_by_app",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="panics_created",
|
||||
to="apps.app",
|
||||
),
|
||||
),
|
||||
(
|
||||
"created_by_user",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="panics_created",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"resolved_by_app",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="panics_resolved",
|
||||
to="apps.app",
|
||||
),
|
||||
),
|
||||
(
|
||||
"resolved_by_user",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="panics_resolved",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
0
frontend/migrations/__init__.py
Normal file
17
frontend/mixins.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
class KumiMixin:
|
||||
"""A mixin that adds the Kumi context to the view
|
||||
"""
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add the Kumi context to the view
|
||||
|
||||
Args:
|
||||
**kwargs: Additional keyword arguments
|
||||
|
||||
Returns:
|
||||
dict: The context
|
||||
"""
|
||||
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["title"] = self.title if hasattr(self, "title") else ""
|
||||
return context
|
24
frontend/models.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from apps.models import App
|
||||
|
||||
class Panic(models.Model):
|
||||
created_by_user = models.ForeignKey(get_user_model(), on_delete=models.SET_NULL, null=True, blank=True, related_name="panics_created")
|
||||
created_by_app = models.ForeignKey(App, on_delete=models.SET_NULL, null=True, blank=True, related_name="panics_created")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
create_reason = models.TextField()
|
||||
resolved_by_user = models.ForeignKey(get_user_model(), on_delete=models.SET_NULL, null=True, blank=True, related_name="panics_resolved")
|
||||
resolved_by_app = models.ForeignKey(App, on_delete=models.SET_NULL, null=True, blank=True, related_name="panics_resolved")
|
||||
resolved_at = models.DateTimeField(null=True, blank=True)
|
||||
resolve_reason = models.TextField(null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username} at {self.created_at}"
|
||||
|
||||
@classmethod
|
||||
def active(cls):
|
||||
try:
|
||||
return cls.objects.get(resolved_at__isnull=True)
|
||||
except cls.DoesNotExist:
|
||||
return None
|
12
frontend/static/css/devices.css
Normal file
|
@ -0,0 +1,12 @@
|
|||
.table-tasks {
|
||||
text-align: center;
|
||||
}
|
||||
.table-tasks i {
|
||||
cursor: pointer;
|
||||
}
|
||||
.table-task-download {
|
||||
color: green;
|
||||
}
|
||||
.table-task-monitor {
|
||||
color: blue;
|
||||
}
|
46
frontend/static/css/panic.css
Normal file
|
@ -0,0 +1,46 @@
|
|||
.panicContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.panic {
|
||||
background: red;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 25px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
display: inline-block;
|
||||
font-size: 20px;
|
||||
margin: 4px 2px;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.panic.active {
|
||||
animation: flash 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes flash {
|
||||
0% {opacity: 1;}
|
||||
50% {opacity: .1;}
|
||||
100% {opacity: 1;}
|
||||
}
|
||||
|
||||
#panicTable {
|
||||
border-top: 50px;
|
||||
border-top-style: solid;
|
||||
border-top-color: transparent;
|
||||
}
|
||||
|
||||
#panicTable tr > td:first-child {
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
font-weight: bold;
|
||||
}
|
53012
frontend/static/dist/adminlte/dist/css/adminlte.css
vendored
Normal file
1
frontend/static/dist/adminlte/dist/css/adminlte.css.map
vendored
Normal file
12
frontend/static/dist/adminlte/dist/css/adminlte.min.css
vendored
Normal file
1
frontend/static/dist/adminlte/dist/css/adminlte.min.css.map
vendored
Normal file
12194
frontend/static/dist/adminlte/dist/css/alt/adminlte.components.css
vendored
Normal file
1
frontend/static/dist/adminlte/dist/css/alt/adminlte.components.css.map
vendored
Normal file
8
frontend/static/dist/adminlte/dist/css/alt/adminlte.components.min.css
vendored
Normal file
1
frontend/static/dist/adminlte/dist/css/alt/adminlte.components.min.css.map
vendored
Normal file
29991
frontend/static/dist/adminlte/dist/css/alt/adminlte.core.css
vendored
Normal file
1
frontend/static/dist/adminlte/dist/css/alt/adminlte.core.css.map
vendored
Normal file
13
frontend/static/dist/adminlte/dist/css/alt/adminlte.core.min.css
vendored
Normal file
1
frontend/static/dist/adminlte/dist/css/alt/adminlte.core.min.css.map
vendored
Normal file
1756
frontend/static/dist/adminlte/dist/css/alt/adminlte.extra-components.css
vendored
Normal file
1
frontend/static/dist/adminlte/dist/css/alt/adminlte.extra-components.css.map
vendored
Normal file
8
frontend/static/dist/adminlte/dist/css/alt/adminlte.extra-components.min.css
vendored
Normal file
1
frontend/static/dist/adminlte/dist/css/alt/adminlte.extra-components.min.css.map
vendored
Normal file
36864
frontend/static/dist/adminlte/dist/css/alt/adminlte.light.css
vendored
Normal file
1
frontend/static/dist/adminlte/dist/css/alt/adminlte.light.css.map
vendored
Normal file
18
frontend/static/dist/adminlte/dist/css/alt/adminlte.light.min.css
vendored
Normal file
1
frontend/static/dist/adminlte/dist/css/alt/adminlte.light.min.css.map
vendored
Normal file
960
frontend/static/dist/adminlte/dist/css/alt/adminlte.pages.css
vendored
Normal file
|
@ -0,0 +1,960 @@
|
|||
/*!
|
||||
* AdminLTE v3.2.0
|
||||
* Only Pages
|
||||
* Author: Colorlib
|
||||
* Website: AdminLTE.io <https://adminlte.io>
|
||||
* License: Open source - MIT <https://opensource.org/licenses/MIT>
|
||||
*/
|
||||
.close, .mailbox-attachment-close {
|
||||
float: right;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
color: #000;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.close:hover, .mailbox-attachment-close:hover {
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.close:not(:disabled):not(.disabled):hover, .mailbox-attachment-close:not(:disabled):not(.disabled):hover, .close:not(:disabled):not(.disabled):focus, .mailbox-attachment-close:not(:disabled):not(.disabled):focus {
|
||||
opacity: .75;
|
||||
}
|
||||
|
||||
button.close, button.mailbox-attachment-close {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
a.close.disabled, a.disabled.mailbox-attachment-close {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@-webkit-keyframes flipInX {
|
||||
0% {
|
||||
-webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg);
|
||||
transform: perspective(400px) rotate3d(1, 0, 0, 90deg);
|
||||
transition-timing-function: ease-in;
|
||||
opacity: 0;
|
||||
}
|
||||
40% {
|
||||
-webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg);
|
||||
transform: perspective(400px) rotate3d(1, 0, 0, -20deg);
|
||||
transition-timing-function: ease-in;
|
||||
}
|
||||
60% {
|
||||
-webkit-transform: perspective(400px) rotate3d(1, 0, 0, 10deg);
|
||||
transform: perspective(400px) rotate3d(1, 0, 0, 10deg);
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
-webkit-transform: perspective(400px) rotate3d(1, 0, 0, -5deg);
|
||||
transform: perspective(400px) rotate3d(1, 0, 0, -5deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: perspective(400px);
|
||||
transform: perspective(400px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes flipInX {
|
||||
0% {
|
||||
-webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg);
|
||||
transform: perspective(400px) rotate3d(1, 0, 0, 90deg);
|
||||
transition-timing-function: ease-in;
|
||||
opacity: 0;
|
||||
}
|
||||
40% {
|
||||
-webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg);
|
||||
transform: perspective(400px) rotate3d(1, 0, 0, -20deg);
|
||||
transition-timing-function: ease-in;
|
||||
}
|
||||
60% {
|
||||
-webkit-transform: perspective(400px) rotate3d(1, 0, 0, 10deg);
|
||||
transform: perspective(400px) rotate3d(1, 0, 0, 10deg);
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
-webkit-transform: perspective(400px) rotate3d(1, 0, 0, -5deg);
|
||||
transform: perspective(400px) rotate3d(1, 0, 0, -5deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: perspective(400px);
|
||||
transform: perspective(400px);
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes fadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes shake {
|
||||
0% {
|
||||
-webkit-transform: translate(2px, 1px) rotate(0deg);
|
||||
transform: translate(2px, 1px) rotate(0deg);
|
||||
}
|
||||
10% {
|
||||
-webkit-transform: translate(-1px, -2px) rotate(-2deg);
|
||||
transform: translate(-1px, -2px) rotate(-2deg);
|
||||
}
|
||||
20% {
|
||||
-webkit-transform: translate(-3px, 0) rotate(3deg);
|
||||
transform: translate(-3px, 0) rotate(3deg);
|
||||
}
|
||||
30% {
|
||||
-webkit-transform: translate(0, 2px) rotate(0deg);
|
||||
transform: translate(0, 2px) rotate(0deg);
|
||||
}
|
||||
40% {
|
||||
-webkit-transform: translate(1px, -1px) rotate(1deg);
|
||||
transform: translate(1px, -1px) rotate(1deg);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: translate(-1px, 2px) rotate(-1deg);
|
||||
transform: translate(-1px, 2px) rotate(-1deg);
|
||||
}
|
||||
60% {
|
||||
-webkit-transform: translate(-3px, 1px) rotate(0deg);
|
||||
transform: translate(-3px, 1px) rotate(0deg);
|
||||
}
|
||||
70% {
|
||||
-webkit-transform: translate(2px, 1px) rotate(-2deg);
|
||||
transform: translate(2px, 1px) rotate(-2deg);
|
||||
}
|
||||
80% {
|
||||
-webkit-transform: translate(-1px, -1px) rotate(4deg);
|
||||
transform: translate(-1px, -1px) rotate(4deg);
|
||||
}
|
||||
90% {
|
||||
-webkit-transform: translate(2px, 2px) rotate(0deg);
|
||||
transform: translate(2px, 2px) rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: translate(1px, -2px) rotate(-1deg);
|
||||
transform: translate(1px, -2px) rotate(-1deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0% {
|
||||
-webkit-transform: translate(2px, 1px) rotate(0deg);
|
||||
transform: translate(2px, 1px) rotate(0deg);
|
||||
}
|
||||
10% {
|
||||
-webkit-transform: translate(-1px, -2px) rotate(-2deg);
|
||||
transform: translate(-1px, -2px) rotate(-2deg);
|
||||
}
|
||||
20% {
|
||||
-webkit-transform: translate(-3px, 0) rotate(3deg);
|
||||
transform: translate(-3px, 0) rotate(3deg);
|
||||
}
|
||||
30% {
|
||||
-webkit-transform: translate(0, 2px) rotate(0deg);
|
||||
transform: translate(0, 2px) rotate(0deg);
|
||||
}
|
||||
40% {
|
||||
-webkit-transform: translate(1px, -1px) rotate(1deg);
|
||||
transform: translate(1px, -1px) rotate(1deg);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: translate(-1px, 2px) rotate(-1deg);
|
||||
transform: translate(-1px, 2px) rotate(-1deg);
|
||||
}
|
||||
60% {
|
||||
-webkit-transform: translate(-3px, 1px) rotate(0deg);
|
||||
transform: translate(-3px, 1px) rotate(0deg);
|
||||
}
|
||||
70% {
|
||||
-webkit-transform: translate(2px, 1px) rotate(-2deg);
|
||||
transform: translate(2px, 1px) rotate(-2deg);
|
||||
}
|
||||
80% {
|
||||
-webkit-transform: translate(-1px, -1px) rotate(4deg);
|
||||
transform: translate(-1px, -1px) rotate(4deg);
|
||||
}
|
||||
90% {
|
||||
-webkit-transform: translate(2px, 2px) rotate(0deg);
|
||||
transform: translate(2px, 2px) rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: translate(1px, -2px) rotate(-1deg);
|
||||
transform: translate(1px, -2px) rotate(-1deg);
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes wobble {
|
||||
0% {
|
||||
-webkit-transform: none;
|
||||
transform: none;
|
||||
}
|
||||
15% {
|
||||
-webkit-transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg);
|
||||
transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg);
|
||||
}
|
||||
30% {
|
||||
-webkit-transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg);
|
||||
transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg);
|
||||
}
|
||||
45% {
|
||||
-webkit-transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg);
|
||||
transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg);
|
||||
}
|
||||
60% {
|
||||
-webkit-transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg);
|
||||
transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg);
|
||||
}
|
||||
75% {
|
||||
-webkit-transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg);
|
||||
transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: none;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes wobble {
|
||||
0% {
|
||||
-webkit-transform: none;
|
||||
transform: none;
|
||||
}
|
||||
15% {
|
||||
-webkit-transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg);
|
||||
transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg);
|
||||
}
|
||||
30% {
|
||||
-webkit-transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg);
|
||||
transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg);
|
||||
}
|
||||
45% {
|
||||
-webkit-transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg);
|
||||
transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg);
|
||||
}
|
||||
60% {
|
||||
-webkit-transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg);
|
||||
transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg);
|
||||
}
|
||||
75% {
|
||||
-webkit-transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg);
|
||||
transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: none;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mailbox-messages > .table {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mailbox-controls {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.mailbox-controls.with-border {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
|
||||
}
|
||||
|
||||
.mailbox-read-info {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.mailbox-read-info h3 {
|
||||
font-size: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mailbox-read-info h5 {
|
||||
margin: 0;
|
||||
padding: 5px 0 0;
|
||||
}
|
||||
|
||||
.mailbox-read-time {
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.mailbox-read-message {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.mailbox-attachments {
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.mailbox-attachments li {
|
||||
border: 1px solid #eee;
|
||||
float: left;
|
||||
margin-bottom: 10px;
|
||||
margin-right: 10px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.mailbox-attachment-name {
|
||||
color: #666;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.mailbox-attachment-icon,
|
||||
.mailbox-attachment-info,
|
||||
.mailbox-attachment-size {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mailbox-attachment-info {
|
||||
background-color: #f8f9fa;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.mailbox-attachment-size {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.mailbox-attachment-size > span {
|
||||
display: inline-block;
|
||||
padding-top: .75rem;
|
||||
}
|
||||
|
||||
.mailbox-attachment-icon {
|
||||
color: #666;
|
||||
font-size: 65px;
|
||||
max-height: 132.5px;
|
||||
padding: 20px 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mailbox-attachment-icon.has-img {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mailbox-attachment-icon.has-img > img {
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.lockscreen {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.lockscreen .lockscreen-name {
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.lockscreen-logo {
|
||||
font-size: 35px;
|
||||
font-weight: 300;
|
||||
margin-bottom: 25px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.lockscreen-logo a {
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.lockscreen-wrapper {
|
||||
margin: 0 auto;
|
||||
margin-top: 10%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.lockscreen-item {
|
||||
border-radius: 4px;
|
||||
background-color: #fff;
|
||||
margin: 10px auto 30px;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
width: 290px;
|
||||
}
|
||||
|
||||
.lockscreen-image {
|
||||
border-radius: 50%;
|
||||
background-color: #fff;
|
||||
left: -10px;
|
||||
padding: 5px;
|
||||
position: absolute;
|
||||
top: -25px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.lockscreen-image > img {
|
||||
border-radius: 50%;
|
||||
height: 70px;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.lockscreen-credentials {
|
||||
margin-left: 70px;
|
||||
}
|
||||
|
||||
.lockscreen-credentials .form-control {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.lockscreen-credentials .btn {
|
||||
background-color: #fff;
|
||||
border: 0;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.lockscreen-footer {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.dark-mode .lockscreen-item {
|
||||
background-color: #343a40;
|
||||
}
|
||||
|
||||
.dark-mode .lockscreen-logo a {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dark-mode .lockscreen-credentials .btn {
|
||||
background-color: #343a40;
|
||||
}
|
||||
|
||||
.dark-mode .lockscreen-image {
|
||||
background-color: #6c757d;
|
||||
}
|
||||
|
||||
.login-logo,
|
||||
.register-logo {
|
||||
font-size: 2.1rem;
|
||||
font-weight: 300;
|
||||
margin-bottom: .9rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-logo a,
|
||||
.register-logo a {
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.login-page,
|
||||
.register-page {
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
background-color: #e9ecef;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-box,
|
||||
.register-box {
|
||||
width: 360px;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.login-box,
|
||||
.register-box {
|
||||
margin-top: .5rem;
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
.login-box .card,
|
||||
.register-box .card {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.login-card-body,
|
||||
.register-card-body {
|
||||
background-color: #fff;
|
||||
border-top: 0;
|
||||
color: #666;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.login-card-body .input-group .form-control,
|
||||
.register-card-body .input-group .form-control {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.login-card-body .input-group .form-control:focus,
|
||||
.register-card-body .input-group .form-control:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.login-card-body .input-group .form-control:focus ~ .input-group-prepend .input-group-text,
|
||||
.login-card-body .input-group .form-control:focus ~ .input-group-append .input-group-text,
|
||||
.register-card-body .input-group .form-control:focus ~ .input-group-prepend .input-group-text,
|
||||
.register-card-body .input-group .form-control:focus ~ .input-group-append .input-group-text {
|
||||
border-color: #80bdff;
|
||||
}
|
||||
|
||||
.login-card-body .input-group .form-control.is-valid:focus,
|
||||
.register-card-body .input-group .form-control.is-valid:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.login-card-body .input-group .form-control.is-valid ~ .input-group-prepend .input-group-text,
|
||||
.login-card-body .input-group .form-control.is-valid ~ .input-group-append .input-group-text,
|
||||
.register-card-body .input-group .form-control.is-valid ~ .input-group-prepend .input-group-text,
|
||||
.register-card-body .input-group .form-control.is-valid ~ .input-group-append .input-group-text {
|
||||
border-color: #28a745;
|
||||
}
|
||||
|
||||
.login-card-body .input-group .form-control.is-invalid:focus,
|
||||
.register-card-body .input-group .form-control.is-invalid:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.login-card-body .input-group .form-control.is-invalid ~ .input-group-append .input-group-text,
|
||||
.register-card-body .input-group .form-control.is-invalid ~ .input-group-append .input-group-text {
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
.login-card-body .input-group .input-group-text,
|
||||
.register-card-body .input-group .input-group-text {
|
||||
background-color: transparent;
|
||||
border-bottom-right-radius: 0.25rem;
|
||||
border-left: 0;
|
||||
border-top-right-radius: 0.25rem;
|
||||
color: #777;
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.login-box-msg,
|
||||
.register-box-msg {
|
||||
margin: 0;
|
||||
padding: 0 20px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.social-auth-links {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.dark-mode .login-card-body,
|
||||
.dark-mode .register-card-body {
|
||||
background-color: #343a40;
|
||||
border-color: #6c757d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dark-mode .login-logo a,
|
||||
.dark-mode .register-logo a {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.error-page {
|
||||
margin: 20px auto 0;
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.error-page {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.error-page > .headline {
|
||||
float: left;
|
||||
font-size: 100px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.error-page > .headline {
|
||||
float: none;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.error-page > .error-content {
|
||||
display: block;
|
||||
margin-left: 190px;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.error-page > .error-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.error-page > .error-content > h3 {
|
||||
font-size: 25px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.error-page > .error-content > h3 {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.invoice {
|
||||
background-color: #fff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.125);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.invoice-title {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.dark-mode .invoice {
|
||||
background-color: #343a40;
|
||||
}
|
||||
|
||||
.profile-user-img {
|
||||
border: 3px solid #adb5bd;
|
||||
margin: 0 auto;
|
||||
padding: 3px;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.profile-username {
|
||||
font-size: 21px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.post {
|
||||
border-bottom: 1px solid #adb5bd;
|
||||
color: #666;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.post:last-of-type {
|
||||
border-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.post .user-block {
|
||||
margin-bottom: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.post .row {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dark-mode .post {
|
||||
color: #fff;
|
||||
border-color: #6c757d;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.product-image-thumbs {
|
||||
-ms-flex-align: stretch;
|
||||
align-items: stretch;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.product-image-thumb {
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
|
||||
border-radius: 0.25rem;
|
||||
background-color: #fff;
|
||||
border: 1px solid #dee2e6;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
margin-right: 1rem;
|
||||
max-width: 7rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.product-image-thumb img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
-ms-flex-item-align: center;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.product-image-thumb:hover {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.product-share a {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.projects td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.projects .list-inline {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.projects img.table-avatar,
|
||||
.projects .table-avatar img {
|
||||
border-radius: 50%;
|
||||
display: inline;
|
||||
width: 2.5rem;
|
||||
}
|
||||
|
||||
.projects .project-state {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
body.iframe-mode .main-sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.iframe-mode .content-wrapper {
|
||||
margin-left: 0 !important;
|
||||
margin-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
body.iframe-mode .main-header,
|
||||
body.iframe-mode .main-footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.iframe-mode-fullscreen {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body.iframe-mode-fullscreen.layout-navbar-fixed .wrapper .content-wrapper {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content-wrapper.iframe-mode .btn-iframe-close {
|
||||
color: #dc3545;
|
||||
position: absolute;
|
||||
line-height: 1;
|
||||
right: .125rem;
|
||||
top: .125rem;
|
||||
z-index: 10;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.content-wrapper.iframe-mode .btn-iframe-close:hover, .content-wrapper.iframe-mode .btn-iframe-close:focus {
|
||||
-webkit-animation-name: fadeIn;
|
||||
animation-name: fadeIn;
|
||||
-webkit-animation-duration: 0.3s;
|
||||
animation-duration: 0.3s;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.content-wrapper.iframe-mode .btn-iframe-close {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.content-wrapper.iframe-mode .navbar-nav {
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.content-wrapper.iframe-mode .navbar-nav .nav-link {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.content-wrapper.iframe-mode .navbar-nav .nav-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content-wrapper.iframe-mode .navbar-nav .nav-item:hover .btn-iframe-close, .content-wrapper.iframe-mode .navbar-nav .nav-item:focus .btn-iframe-close {
|
||||
-webkit-animation-name: fadeIn;
|
||||
animation-name: fadeIn;
|
||||
-webkit-animation-duration: 0.3s;
|
||||
animation-duration: 0.3s;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.content-wrapper.iframe-mode .navbar-nav .nav-item:hover .btn-iframe-close, .content-wrapper.iframe-mode .navbar-nav .nav-item:focus .btn-iframe-close {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.content-wrapper.iframe-mode .tab-content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content-wrapper.iframe-mode .tab-pane + .tab-empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.content-wrapper.iframe-mode .tab-empty {
|
||||
width: 100%;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.content-wrapper.iframe-mode .tab-loading {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
display: none;
|
||||
background-color: #f4f6f9;
|
||||
}
|
||||
|
||||
.content-wrapper.iframe-mode .tab-loading > div {
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content-wrapper.iframe-mode iframe {
|
||||
border: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-bottom: -8px;
|
||||
}
|
||||
|
||||
.content-wrapper.iframe-mode iframe .content-wrapper {
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
body.iframe-mode-fullscreen .content-wrapper.iframe-mode {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin-left: 0 !important;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
z-index: 1048;
|
||||
}
|
||||
|
||||
.permanent-btn-iframe-close .btn-iframe-close {
|
||||
-webkit-animation: none !important;
|
||||
animation: none !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dark-mode .content-wrapper.iframe-mode .tab-loading {
|
||||
background-color: #343a40;
|
||||
}
|
||||
|
||||
.content-wrapper.kanban {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.content-wrapper.kanban .content {
|
||||
height: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.content-wrapper.kanban .content .container,
|
||||
.content-wrapper.kanban .content .container-fluid {
|
||||
width: -webkit-max-content;
|
||||
width: -moz-max-content;
|
||||
width: max-content;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-ms-flex-align: stretch;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.content-wrapper.kanban .content-header + .content {
|
||||
height: calc(100% - ((2 * 15px) + (1.8rem * 1.2)));
|
||||
}
|
||||
|
||||
.content-wrapper.kanban .card .card-body {
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
.content-wrapper.kanban .card.card-row {
|
||||
width: 340px;
|
||||
display: inline-block;
|
||||
margin: 0 .5rem;
|
||||
}
|
||||
|
||||
.content-wrapper.kanban .card.card-row:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.content-wrapper.kanban .card.card-row .card-body {
|
||||
height: calc(100% - (12px + (1.8rem * 1.2) + .5rem));
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.content-wrapper.kanban .card.card-row .card:last-child {
|
||||
margin-bottom: 0;
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
.content-wrapper.kanban .card.card-row .card .card-header {
|
||||
padding: .5rem .75rem;
|
||||
}
|
||||
|
||||
.content-wrapper.kanban .card.card-row .card .card-body {
|
||||
padding: .75rem;
|
||||
}
|
||||
|
||||
.content-wrapper.kanban .btn-tool.btn-link {
|
||||
text-decoration: underline;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
/*# sourceMappingURL=adminlte.pages.css.map */
|
1
frontend/static/dist/adminlte/dist/css/alt/adminlte.pages.css.map
vendored
Normal file
8
frontend/static/dist/adminlte/dist/css/alt/adminlte.pages.min.css
vendored
Normal file
1
frontend/static/dist/adminlte/dist/css/alt/adminlte.pages.min.css.map
vendored
Normal file
9146
frontend/static/dist/adminlte/dist/css/alt/adminlte.plugins.css
vendored
Normal file
1
frontend/static/dist/adminlte/dist/css/alt/adminlte.plugins.css.map
vendored
Normal file
8
frontend/static/dist/adminlte/dist/css/alt/adminlte.plugins.min.css
vendored
Normal file
1
frontend/static/dist/adminlte/dist/css/alt/adminlte.plugins.min.css.map
vendored
Normal file
BIN
frontend/static/dist/adminlte/dist/img/AdminLTELogo.png
vendored
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
frontend/static/dist/adminlte/dist/img/avatar.png
vendored
Normal file
After Width: | Height: | Size: 7.9 KiB |
BIN
frontend/static/dist/adminlte/dist/img/avatar2.png
vendored
Normal file
After Width: | Height: | Size: 8.1 KiB |
BIN
frontend/static/dist/adminlte/dist/img/avatar3.png
vendored
Normal file
After Width: | Height: | Size: 9 KiB |
BIN
frontend/static/dist/adminlte/dist/img/avatar4.png
vendored
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
frontend/static/dist/adminlte/dist/img/avatar5.png
vendored
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
frontend/static/dist/adminlte/dist/img/boxed-bg.jpg
vendored
Normal file
After Width: | Height: | Size: 121 KiB |
BIN
frontend/static/dist/adminlte/dist/img/boxed-bg.png
vendored
Normal file
After Width: | Height: | Size: 43 KiB |
BIN
frontend/static/dist/adminlte/dist/img/credit/american-express.png
vendored
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
frontend/static/dist/adminlte/dist/img/credit/cirrus.png
vendored
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
frontend/static/dist/adminlte/dist/img/credit/mastercard.png
vendored
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
frontend/static/dist/adminlte/dist/img/credit/paypal.png
vendored
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
frontend/static/dist/adminlte/dist/img/credit/paypal2.png
vendored
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
frontend/static/dist/adminlte/dist/img/credit/visa.png
vendored
Normal file
After Width: | Height: | Size: 1 KiB |
BIN
frontend/static/dist/adminlte/dist/img/default-150x150.png
vendored
Normal file
After Width: | Height: | Size: 339 B |
BIN
frontend/static/dist/adminlte/dist/img/icons.png
vendored
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
frontend/static/dist/adminlte/dist/img/photo1.png
vendored
Normal file
After Width: | Height: | Size: 647 KiB |
BIN
frontend/static/dist/adminlte/dist/img/photo2.png
vendored
Normal file
After Width: | Height: | Size: 413 KiB |
BIN
frontend/static/dist/adminlte/dist/img/photo3.jpg
vendored
Normal file
After Width: | Height: | Size: 362 KiB |
BIN
frontend/static/dist/adminlte/dist/img/photo4.jpg
vendored
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/static/dist/adminlte/dist/img/prod-1.jpg
vendored
Normal file
After Width: | Height: | Size: 44 KiB |
BIN
frontend/static/dist/adminlte/dist/img/prod-2.jpg
vendored
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
frontend/static/dist/adminlte/dist/img/prod-3.jpg
vendored
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
frontend/static/dist/adminlte/dist/img/prod-4.jpg
vendored
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
frontend/static/dist/adminlte/dist/img/prod-5.jpg
vendored
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
frontend/static/dist/adminlte/dist/img/user1-128x128.jpg
vendored
Normal file
After Width: | Height: | Size: 2.7 KiB |