Current status

This commit is contained in:
Kumi 2023-12-04 16:09:40 +01:00
commit 27eca84ada
Signed by: kumi
GPG key ID: ECBCC9082395383F
6360 changed files with 2398641 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.venv
*.pyc
__pycache__/
db.sqlite3
settings.ini

20
.vscode/launch.json vendored Normal file
View 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
View file

6
apps/admin.py Normal file
View 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
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class AppsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps'

View 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,
),
),
],
),
]

View file

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

View 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)),
],
),
]

View file

130
apps/models.py Normal file
View 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
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

15
apps/urls.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View file

5
custom_auth/admin.py Normal file
View 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
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class CustomAuthConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "custom_auth"

View 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,
},
),
]

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

View file

50
custom_auth/models.py Normal file
View 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

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

View 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
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

10
custom_auth/urls.py Normal file
View 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
View 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
View file

16
expalert/asgi.py Normal file
View 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
View 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
View 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
View 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
View file

3
frontend/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
frontend/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class FrontendConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "frontend"

View 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,
),
),
],
),
]

View file

17
frontend/mixins.py Normal file
View 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
View 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

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

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

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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 */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 647 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Some files were not shown because too many files have changed in this diff Show more