Добавление папки platform_admin
This commit is contained in:
0
myproject/platform_admin/__init__.py
Normal file
0
myproject/platform_admin/__init__.py
Normal file
64
myproject/platform_admin/admin.py
Normal file
64
myproject/platform_admin/admin.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
from django.db import connection
|
||||||
|
|
||||||
|
from .models import PlatformAdmin
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(PlatformAdmin)
|
||||||
|
class PlatformAdminAdmin(UserAdmin):
|
||||||
|
"""
|
||||||
|
Админка для управления администраторами платформы.
|
||||||
|
|
||||||
|
Доступна только в public schema.
|
||||||
|
"""
|
||||||
|
list_display = ('email', 'name', 'is_active', 'is_superuser', 'date_joined', 'last_login')
|
||||||
|
list_filter = ('is_active', 'is_superuser', 'date_joined')
|
||||||
|
search_fields = ('email', 'name')
|
||||||
|
ordering = ('-date_joined',)
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
(None, {'fields': ('email', 'password')}),
|
||||||
|
('Персональные данные', {'fields': ('name',)}),
|
||||||
|
('Права доступа', {
|
||||||
|
'fields': ('is_active', 'is_staff', 'is_superuser'),
|
||||||
|
'description': 'Суперадмин может входить в /admin/ на tenant доменах для поддержки.'
|
||||||
|
}),
|
||||||
|
('Важные даты', {'fields': ('last_login', 'date_joined')}),
|
||||||
|
)
|
||||||
|
|
||||||
|
add_fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'classes': ('wide',),
|
||||||
|
'fields': ('email', 'name', 'password1', 'password2', 'is_superuser'),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
readonly_fields = ('date_joined', 'last_login')
|
||||||
|
|
||||||
|
def has_module_permission(self, request):
|
||||||
|
"""Показывать только в public schema."""
|
||||||
|
if hasattr(connection, 'schema_name') and connection.schema_name != 'public':
|
||||||
|
return False
|
||||||
|
return super().has_module_permission(request)
|
||||||
|
|
||||||
|
def has_view_permission(self, request, obj=None):
|
||||||
|
if hasattr(connection, 'schema_name') and connection.schema_name != 'public':
|
||||||
|
return False
|
||||||
|
return super().has_view_permission(request, obj)
|
||||||
|
|
||||||
|
def has_add_permission(self, request):
|
||||||
|
if hasattr(connection, 'schema_name') and connection.schema_name != 'public':
|
||||||
|
return False
|
||||||
|
return super().has_add_permission(request)
|
||||||
|
|
||||||
|
def has_change_permission(self, request, obj=None):
|
||||||
|
if hasattr(connection, 'schema_name') and connection.schema_name != 'public':
|
||||||
|
return False
|
||||||
|
return super().has_change_permission(request, obj)
|
||||||
|
|
||||||
|
def has_delete_permission(self, request, obj=None):
|
||||||
|
if hasattr(connection, 'schema_name') and connection.schema_name != 'public':
|
||||||
|
return False
|
||||||
|
return super().has_delete_permission(request, obj)
|
||||||
7
myproject/platform_admin/apps.py
Normal file
7
myproject/platform_admin/apps.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PlatformAdminConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'platform_admin'
|
||||||
|
verbose_name = 'Администраторы платформы'
|
||||||
81
myproject/platform_admin/backends.py
Normal file
81
myproject/platform_admin/backends.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Authentication backend для PlatformAdmin.
|
||||||
|
|
||||||
|
Этот backend используется для аутентификации администраторов платформы.
|
||||||
|
Работает на public домене, а также на tenant доменах для суперадминов.
|
||||||
|
"""
|
||||||
|
from django.contrib.auth.backends import ModelBackend
|
||||||
|
from django.db import connection
|
||||||
|
|
||||||
|
|
||||||
|
class PlatformAdminBackend(ModelBackend):
|
||||||
|
"""
|
||||||
|
Backend аутентификации для PlatformAdmin.
|
||||||
|
|
||||||
|
Особенности:
|
||||||
|
- На public домене: аутентифицирует любого PlatformAdmin
|
||||||
|
- На tenant домене: аутентифицирует только PlatformAdmin с is_superuser=True
|
||||||
|
(для поддержки и отладки клиентов)
|
||||||
|
|
||||||
|
Обычные PlatformAdmin (без is_superuser) не могут логиниться на tenant доменах.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def authenticate(self, request, username=None, password=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Аутентификация PlatformAdmin по email и паролю.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: HTTP запрос
|
||||||
|
username: Email администратора
|
||||||
|
password: Пароль
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PlatformAdmin если аутентификация успешна, иначе None
|
||||||
|
"""
|
||||||
|
from platform_admin.models import PlatformAdmin
|
||||||
|
|
||||||
|
if username is None or password is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Всегда читаем из default (public schema)
|
||||||
|
user = PlatformAdmin.objects.using('default').get(email=username)
|
||||||
|
except PlatformAdmin.DoesNotExist:
|
||||||
|
# Run the default password hasher once to reduce the timing
|
||||||
|
# difference between an existing and a non-existing user
|
||||||
|
PlatformAdmin().set_password(password)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not user.check_password(password):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not self.user_can_authenticate(user):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# На tenant домене — только суперадмин может логиниться
|
||||||
|
schema_name = getattr(connection, 'schema_name', 'public')
|
||||||
|
if schema_name != 'public' and not user.is_superuser:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
def get_user(self, user_id):
|
||||||
|
"""
|
||||||
|
Получение PlatformAdmin по ID.
|
||||||
|
|
||||||
|
Всегда читает из public schema.
|
||||||
|
"""
|
||||||
|
from platform_admin.models import PlatformAdmin
|
||||||
|
|
||||||
|
try:
|
||||||
|
return PlatformAdmin.objects.using('default').get(pk=user_id)
|
||||||
|
except PlatformAdmin.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def user_can_authenticate(self, user):
|
||||||
|
"""
|
||||||
|
Проверка что пользователь активен.
|
||||||
|
"""
|
||||||
|
is_active = getattr(user, 'is_active', None)
|
||||||
|
return is_active or is_active is None
|
||||||
71
myproject/platform_admin/forms.py
Normal file
71
myproject/platform_admin/forms.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Формы для аутентификации администраторов платформы.
|
||||||
|
"""
|
||||||
|
from django import forms
|
||||||
|
from django.contrib.auth import authenticate
|
||||||
|
|
||||||
|
|
||||||
|
class PlatformAdminLoginForm(forms.Form):
|
||||||
|
"""
|
||||||
|
Форма входа для администраторов платформы.
|
||||||
|
"""
|
||||||
|
email = forms.EmailField(
|
||||||
|
label="Email",
|
||||||
|
widget=forms.EmailInput(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'admin@platform.com',
|
||||||
|
'autofocus': True,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
password = forms.CharField(
|
||||||
|
label="Пароль",
|
||||||
|
strip=False,
|
||||||
|
widget=forms.PasswordInput(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'Пароль',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
error_messages = {
|
||||||
|
'invalid_login': "Неверный email или пароль.",
|
||||||
|
'inactive': "Этот аккаунт деактивирован.",
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, request=None, *args, **kwargs):
|
||||||
|
self.request = request
|
||||||
|
self.user_cache = None
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
email = self.cleaned_data.get('email')
|
||||||
|
password = self.cleaned_data.get('password')
|
||||||
|
|
||||||
|
if email is not None and password:
|
||||||
|
self.user_cache = authenticate(
|
||||||
|
self.request,
|
||||||
|
username=email,
|
||||||
|
password=password
|
||||||
|
)
|
||||||
|
if self.user_cache is None:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
self.error_messages['invalid_login'],
|
||||||
|
code='invalid_login',
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.confirm_login_allowed(self.user_cache)
|
||||||
|
|
||||||
|
return self.cleaned_data
|
||||||
|
|
||||||
|
def confirm_login_allowed(self, user):
|
||||||
|
"""
|
||||||
|
Проверка что пользователь может войти.
|
||||||
|
"""
|
||||||
|
if not user.is_active:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
self.error_messages['inactive'],
|
||||||
|
code='inactive',
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_user(self):
|
||||||
|
return self.user_cache
|
||||||
36
myproject/platform_admin/migrations/0001_initial.py
Normal file
36
myproject/platform_admin/migrations/0001_initial.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Generated by Django 5.0.10 on 2026-01-08 15:56
|
||||||
|
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('auth', '0012_alter_user_first_name_max_length'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PlatformAdmin',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
|
('email', models.EmailField(max_length=254, unique=True, verbose_name='Email')),
|
||||||
|
('name', models.CharField(max_length=200, verbose_name='Имя')),
|
||||||
|
('is_staff', models.BooleanField(default=True, help_text='Определяет, может ли пользователь входить в Django Admin', verbose_name='Доступ к админке')),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='Активен')),
|
||||||
|
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Дата регистрации')),
|
||||||
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='Последний вход')),
|
||||||
|
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||||
|
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Администратор платформы',
|
||||||
|
'verbose_name_plural': 'Администраторы платформы',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
myproject/platform_admin/migrations/__init__.py
Normal file
0
myproject/platform_admin/migrations/__init__.py
Normal file
105
myproject/platform_admin/models.py
Normal file
105
myproject/platform_admin/models.py
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Модели для администраторов платформы.
|
||||||
|
|
||||||
|
PlatformAdmin - отдельная сущность для управления тенантами,
|
||||||
|
живёт только в public schema и не связана с CustomUser.
|
||||||
|
"""
|
||||||
|
from django.db import models
|
||||||
|
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
|
class PlatformAdminManager(BaseUserManager):
|
||||||
|
"""Менеджер для модели PlatformAdmin."""
|
||||||
|
|
||||||
|
def create_user(self, email, name, password=None, **extra_fields):
|
||||||
|
"""
|
||||||
|
Создаёт обычного администратора платформы.
|
||||||
|
"""
|
||||||
|
if not email:
|
||||||
|
raise ValueError('Email обязателен')
|
||||||
|
email = self.normalize_email(email)
|
||||||
|
extra_fields.setdefault('is_staff', True)
|
||||||
|
extra_fields.setdefault('is_active', True)
|
||||||
|
user = self.model(email=email, name=name, **extra_fields)
|
||||||
|
user.set_password(password)
|
||||||
|
user.save(using=self._db)
|
||||||
|
return user
|
||||||
|
|
||||||
|
def create_superuser(self, email, name, password=None, **extra_fields):
|
||||||
|
"""
|
||||||
|
Создаёт суперадминистратора платформы.
|
||||||
|
|
||||||
|
Суперадмин может:
|
||||||
|
- Управлять всеми тенантами
|
||||||
|
- Входить в /admin/ на tenant доменах для поддержки
|
||||||
|
"""
|
||||||
|
extra_fields['is_superuser'] = True
|
||||||
|
extra_fields['is_staff'] = True
|
||||||
|
return self.create_user(email, name, password, **extra_fields)
|
||||||
|
|
||||||
|
|
||||||
|
class PlatformAdmin(AbstractBaseUser, PermissionsMixin):
|
||||||
|
"""
|
||||||
|
Администратор платформы для управления тенантами.
|
||||||
|
|
||||||
|
Живёт ТОЛЬКО в public schema.
|
||||||
|
НЕ имеет доступа к бизнес-данным тенантов (только управление).
|
||||||
|
НЕ связан с UserRole и ролевой системой тенантов.
|
||||||
|
|
||||||
|
Используется для:
|
||||||
|
- Управления тенантами (Client)
|
||||||
|
- Одобрения заявок на регистрацию (TenantRegistration)
|
||||||
|
- Управления подписками (Subscription)
|
||||||
|
- Управления доменами (Domain)
|
||||||
|
|
||||||
|
Суперадмин (is_superuser=True) дополнительно может:
|
||||||
|
- Входить в /admin/ на tenant доменах для поддержки клиентов
|
||||||
|
"""
|
||||||
|
email = models.EmailField(
|
||||||
|
unique=True,
|
||||||
|
verbose_name="Email"
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=200,
|
||||||
|
verbose_name="Имя"
|
||||||
|
)
|
||||||
|
|
||||||
|
is_staff = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
verbose_name="Доступ к админке",
|
||||||
|
help_text="Определяет, может ли пользователь входить в Django Admin"
|
||||||
|
)
|
||||||
|
is_active = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
verbose_name="Активен"
|
||||||
|
)
|
||||||
|
|
||||||
|
date_joined = models.DateTimeField(
|
||||||
|
default=timezone.now,
|
||||||
|
verbose_name="Дата регистрации"
|
||||||
|
)
|
||||||
|
last_login = models.DateTimeField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Последний вход"
|
||||||
|
)
|
||||||
|
|
||||||
|
objects = PlatformAdminManager()
|
||||||
|
|
||||||
|
USERNAME_FIELD = 'email'
|
||||||
|
REQUIRED_FIELDS = ['name']
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Администратор платформы"
|
||||||
|
verbose_name_plural = "Администраторы платформы"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name} ({self.email})"
|
||||||
|
|
||||||
|
def get_full_name(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def get_short_name(self):
|
||||||
|
return self.name.split()[0] if self.name else self.email
|
||||||
132
myproject/platform_admin/templates/platform_admin/dashboard.html
Normal file
132
myproject/platform_admin/templates/platform_admin/dashboard.html
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Панель администратора платформы</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
.navbar {
|
||||||
|
background-color: #343a40;
|
||||||
|
}
|
||||||
|
.stat-card {
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.stat-card .card-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
.stat-number {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.stat-label {
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-dark mb-4">
|
||||||
|
<div class="container">
|
||||||
|
<span class="navbar-brand">Администратор платформы</span>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span class="text-light me-3">{{ request.user.name }}</span>
|
||||||
|
<a href="{% url 'platform_admin:logout' %}" class="btn btn-outline-light btn-sm">Выйти</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
{% if messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<h2 class="mb-4">Обзор платформы</h2>
|
||||||
|
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card stat-card">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="stat-number text-primary">{{ total_tenants }}</div>
|
||||||
|
<div class="stat-label">Всего тенантов</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card stat-card">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="stat-number text-success">{{ active_tenants }}</div>
|
||||||
|
<div class="stat-label">Активных тенантов</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card stat-card">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="stat-number text-warning">{{ pending_registrations }}</div>
|
||||||
|
<div class="stat-label">Ожидают одобрения</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card stat-card">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="stat-number text-info">{{ active_subscriptions }}</div>
|
||||||
|
<div class="stat-label">Активных подписок</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Быстрые действия</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<a href="/admin/" class="btn btn-primary me-2 mb-2">
|
||||||
|
Django Admin
|
||||||
|
</a>
|
||||||
|
<a href="/admin/tenants/client/" class="btn btn-outline-primary me-2 mb-2">
|
||||||
|
Управление тенантами
|
||||||
|
</a>
|
||||||
|
<a href="/admin/tenants/tenantregistration/" class="btn btn-outline-primary me-2 mb-2">
|
||||||
|
Заявки на регистрацию
|
||||||
|
</a>
|
||||||
|
<a href="/admin/tenants/subscription/" class="btn btn-outline-primary mb-2">
|
||||||
|
Подписки
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Информация</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p><strong>Вы вошли как:</strong> {{ request.user.email }}</p>
|
||||||
|
<p><strong>Права:</strong>
|
||||||
|
{% if request.user.is_superuser %}
|
||||||
|
Суперадминистратор (доступ к админке тенантов)
|
||||||
|
{% else %}
|
||||||
|
Администратор платформы
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
111
myproject/platform_admin/templates/platform_admin/login.html
Normal file
111
myproject/platform_admin/templates/platform_admin/login.html
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Вход - Администратор платформы</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.login-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.card-header {
|
||||||
|
background-color: #343a40;
|
||||||
|
color: white;
|
||||||
|
border-radius: 10px 10px 0 0 !important;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.card-header h4 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.card-header small {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #343a40;
|
||||||
|
border-color: #343a40;
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #23272b;
|
||||||
|
border-color: #23272b;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4>Администратор платформы</h4>
|
||||||
|
<small>Управление тенантами и подписками</small>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-4">
|
||||||
|
{% if messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
{% if form.non_field_errors %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
{% for error in form.non_field_errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="id_email" class="form-label">Email</label>
|
||||||
|
{{ form.email }}
|
||||||
|
{% if form.email.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{% for error in form.email.errors %}{{ error }}{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="id_password" class="form-label">Пароль</label>
|
||||||
|
{{ form.password }}
|
||||||
|
{% if form.password.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{% for error in form.password.errors %}{{ error }}{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg">Войти</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center mt-3 text-muted">
|
||||||
|
<small>Только для администраторов платформы</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3
myproject/platform_admin/tests.py
Normal file
3
myproject/platform_admin/tests.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
15
myproject/platform_admin/urls.py
Normal file
15
myproject/platform_admin/urls.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
URL маршрутизация для администраторов платформы.
|
||||||
|
"""
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = 'platform_admin'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('login/', views.platform_login_view, name='login'),
|
||||||
|
path('logout/', views.platform_logout_view, name='logout'),
|
||||||
|
path('dashboard/', views.dashboard_view, name='dashboard'),
|
||||||
|
]
|
||||||
75
myproject/platform_admin/views.py
Normal file
75
myproject/platform_admin/views.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Views для аутентификации администраторов платформы.
|
||||||
|
"""
|
||||||
|
from django.shortcuts import render, redirect
|
||||||
|
from django.contrib.auth import login, logout
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.db import connection
|
||||||
|
|
||||||
|
from .forms import PlatformAdminLoginForm
|
||||||
|
from .models import PlatformAdmin
|
||||||
|
|
||||||
|
|
||||||
|
def platform_login_view(request):
|
||||||
|
"""
|
||||||
|
Страница входа для администраторов платформы.
|
||||||
|
|
||||||
|
Доступна только на public домене.
|
||||||
|
"""
|
||||||
|
# Проверяем что мы на public домене
|
||||||
|
schema_name = getattr(connection, 'schema_name', 'public')
|
||||||
|
if schema_name != 'public':
|
||||||
|
messages.error(request, 'Вход для администраторов платформы доступен только на главном домене.')
|
||||||
|
return redirect('/')
|
||||||
|
|
||||||
|
# Если пользователь уже залогинен как PlatformAdmin
|
||||||
|
if request.user.is_authenticated and isinstance(request.user, PlatformAdmin):
|
||||||
|
return redirect('platform_admin:dashboard')
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = PlatformAdminLoginForm(request, data=request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
user = form.get_user()
|
||||||
|
login(request, user, backend='platform_admin.backends.PlatformAdminBackend')
|
||||||
|
messages.success(request, f'Добро пожаловать, {user.name}!')
|
||||||
|
|
||||||
|
next_url = request.GET.get('next')
|
||||||
|
if next_url:
|
||||||
|
return redirect(next_url)
|
||||||
|
return redirect('platform_admin:dashboard')
|
||||||
|
else:
|
||||||
|
form = PlatformAdminLoginForm(request)
|
||||||
|
|
||||||
|
return render(request, 'platform_admin/login.html', {'form': form})
|
||||||
|
|
||||||
|
|
||||||
|
def platform_logout_view(request):
|
||||||
|
"""
|
||||||
|
Выход администратора платформы.
|
||||||
|
"""
|
||||||
|
logout(request)
|
||||||
|
messages.info(request, 'Вы вышли из системы.')
|
||||||
|
return redirect('platform_admin:login')
|
||||||
|
|
||||||
|
|
||||||
|
def dashboard_view(request):
|
||||||
|
"""
|
||||||
|
Главная страница панели администратора платформы.
|
||||||
|
|
||||||
|
Показывает статистику по тенантам, заявкам и подпискам.
|
||||||
|
"""
|
||||||
|
# Проверяем что пользователь - PlatformAdmin
|
||||||
|
if not request.user.is_authenticated or not isinstance(request.user, PlatformAdmin):
|
||||||
|
return redirect('platform_admin:login')
|
||||||
|
|
||||||
|
from tenants.models import Client, TenantRegistration, Subscription
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'total_tenants': Client.objects.exclude(schema_name='public').count(),
|
||||||
|
'active_tenants': Client.objects.filter(is_active=True).exclude(schema_name='public').count(),
|
||||||
|
'pending_registrations': TenantRegistration.objects.filter(status='pending').count(),
|
||||||
|
'active_subscriptions': Subscription.objects.filter(is_active=True).count(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, 'platform_admin/dashboard.html', context)
|
||||||
Reference in New Issue
Block a user