\w+)$", notify.MessageNotify.as_asgi()),
16 | ]
17 |
--------------------------------------------------------------------------------
/notifications/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/notifications/__init__.py
--------------------------------------------------------------------------------
/notifications/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 | from notifications.models import *
5 |
6 | admin.site.register(MessageContent)
7 | admin.site.register(MessageUserRead)
8 | admin.site.register(UserMsgSubscription)
9 | admin.site.register(SystemMsgSubscription)
10 |
--------------------------------------------------------------------------------
/notifications/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 | from django.utils.translation import gettext_lazy as _
3 |
4 |
5 | class NotificationsConfig(AppConfig):
6 | default_auto_field = 'django.db.models.BigAutoField'
7 | name = 'notifications'
8 | verbose_name = _('App Notifications')
9 |
10 | def ready(self):
11 | from notifications.backends import BACKEND # noqa
12 | from . import signal_handlers # noqa
13 | from . import notifications # noqa
14 | super().ready()
15 |
--------------------------------------------------------------------------------
/notifications/backends/__init__.py:
--------------------------------------------------------------------------------
1 | import importlib
2 |
3 | from django.db import models
4 | from django.utils.translation import gettext_lazy as _
5 |
6 | client_name_mapper = {}
7 |
8 |
9 | class BACKEND(models.TextChoices):
10 | EMAIL = 'email', _('Email')
11 | SITE_MSG = 'site_msg', _('Site message')
12 |
13 | # DINGTALK = 'dingtalk', _('DingTalk')
14 | # SMS = 'sms', _('SMS')
15 |
16 | @property
17 | def client(self):
18 | return client_name_mapper[self]
19 |
20 | def get_account(self, user):
21 | return self.client.get_account(user)
22 |
23 | @property
24 | def is_enable(self):
25 | return self.client.is_enable()
26 |
27 | @classmethod
28 | def filter_enable_backends(cls, backends):
29 | enable_backends = [b for b in backends if cls(b).is_enable]
30 | return enable_backends
31 |
32 |
33 | for b in BACKEND:
34 | m = importlib.import_module(f'.{b}', __package__)
35 | client_name_mapper[b] = m.backend
36 |
--------------------------------------------------------------------------------
/notifications/backends/base.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 |
3 |
4 | class BackendBase:
5 | # User 表中的字段
6 | account_field = None
7 |
8 | # Django setting 中的字段名
9 | is_enable_field_in_settings = None
10 |
11 | def get_accounts(self, users):
12 | accounts = []
13 | unbound_users = []
14 | account_user_mapper = {}
15 |
16 | for user in users:
17 | account = getattr(user, self.account_field, None)
18 | if account:
19 | account_user_mapper[account] = user
20 | accounts.append(account)
21 | else:
22 | unbound_users.append(user)
23 | return accounts, unbound_users, account_user_mapper
24 |
25 | @classmethod
26 | def get_account(cls, user):
27 | return getattr(user, cls.account_field)
28 |
29 | @classmethod
30 | def is_enable(cls):
31 | enable = getattr(settings, cls.is_enable_field_in_settings)
32 | return bool(enable)
33 |
--------------------------------------------------------------------------------
/notifications/backends/email.py:
--------------------------------------------------------------------------------
1 | from common.tasks import send_mail_async
2 | from .base import BackendBase
3 |
4 |
5 | class Email(BackendBase):
6 | account_field = 'email'
7 | is_enable_field_in_settings = 'EMAIL_ENABLED'
8 |
9 | def send_msg(self, users, message, subject):
10 | accounts, __, __ = self.get_accounts(users)
11 | if not accounts:
12 | return
13 | send_mail_async(subject, message, accounts, html_message=message)
14 |
15 |
16 | backend = Email
17 |
--------------------------------------------------------------------------------
/notifications/backends/site_msg.py:
--------------------------------------------------------------------------------
1 | from notifications.message import SiteMessageUtil as Client
2 | from .base import BackendBase
3 |
4 |
5 | class SiteMessage(BackendBase):
6 | account_field = 'id'
7 |
8 | def send_msg(self, users, message, subject, **kwargs):
9 | accounts, __, __ = self.get_accounts(users)
10 | Client.send_msg(subject, message, user_ids=accounts, **kwargs)
11 |
12 | @classmethod
13 | def is_enable(cls):
14 | return True
15 |
16 |
17 | backend = SiteMessage
18 |
--------------------------------------------------------------------------------
/notifications/backends/sms.py:
--------------------------------------------------------------------------------
1 | from common.sdk.sms.endpoint import SMS
2 | from .base import BackendBase
3 |
4 |
5 | class SMS(BackendBase):
6 | account_field = 'phone'
7 | is_enable_field_in_settings = 'SMS_ENABLED'
8 |
9 | def __init__(self):
10 | self.client = SMS()
11 |
12 | def send_msg(self, users, sign_name: str, template_code: str, template_param: dict):
13 | accounts, __, __ = self.get_accounts(users)
14 | if not accounts:
15 | return
16 | return self.client.send_sms(accounts, sign_name, template_code, template_param)
17 |
18 |
19 | backend = SMS
20 |
--------------------------------------------------------------------------------
/notifications/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/notifications/migrations/__init__.py
--------------------------------------------------------------------------------
/notifications/models/__init__.py:
--------------------------------------------------------------------------------
1 | from .message import *
2 | from .notification import *
3 |
--------------------------------------------------------------------------------
/notifications/models/notification.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : notification
5 | # author : ly_13
6 | # date : 9/13/2024
7 |
8 | from django.db import models
9 | from django.utils.translation import gettext_lazy as _
10 |
11 | from common.core.models import DbAuditModel
12 |
13 |
14 | class UserMsgSubscription(DbAuditModel):
15 | message_type = models.CharField(max_length=128, verbose_name=_('message type'))
16 | user = models.ForeignKey('system.UserInfo', related_name='user_msg_subscription', on_delete=models.CASCADE,
17 | verbose_name=_('User'))
18 | receive_backends = models.JSONField(default=list, verbose_name=_('receive backend'))
19 |
20 | class Meta:
21 | verbose_name = _('User message subscription')
22 | unique_together = (('user', 'message_type'),)
23 |
24 | def __str__(self):
25 | return _('{} subscription').format(self.user)
26 |
27 |
28 | class SystemMsgSubscription(DbAuditModel):
29 | message_type = models.CharField(max_length=128, unique=True, verbose_name=_('message type'))
30 | users = models.ManyToManyField('system.UserInfo', related_name='system_msg_subscriptions', verbose_name=_("User"))
31 | receive_backends = models.JSONField(default=list, verbose_name=_('receive backend'))
32 |
33 | class Meta:
34 | verbose_name = _('System message subscription')
35 |
36 | def __str__(self):
37 | return f'{self.message_type} -- {self.receive_backends}'
38 |
--------------------------------------------------------------------------------
/notifications/serializers/__init__.py:
--------------------------------------------------------------------------------
1 | from .notifications import *
2 |
--------------------------------------------------------------------------------
/notifications/tests.py:
--------------------------------------------------------------------------------
1 | # Create your tests here.
2 |
--------------------------------------------------------------------------------
/notifications/urls.py:
--------------------------------------------------------------------------------
1 | from rest_framework.routers import SimpleRouter
2 |
3 | from notifications.views.message import NoticeMessageViewSet, NoticeUserReadMessageViewSet
4 | from notifications.views.notifications import SystemMsgSubscriptionViewSet, UserMsgSubscriptionViewSet
5 | from notifications.views.user_site_msg import UserSiteMessageViewSet
6 |
7 | app_name = 'notifications'
8 |
9 | router = SimpleRouter(False)
10 |
11 | # 消息通知路由
12 | router.register('notice-messages', NoticeMessageViewSet, basename='notice-messages')
13 | router.register('user-read-messages', NoticeUserReadMessageViewSet, basename='user-read-messages')
14 | router.register('site-messages', UserSiteMessageViewSet, basename='site-messages')
15 |
16 | # 消息订阅配置
17 | router.register('system-msg-subscription', SystemMsgSubscriptionViewSet, basename='system-msg-subscription')
18 | router.register('user-msg-subscription', UserMsgSubscriptionViewSet, basename='user-msg-subscription')
19 |
20 | urlpatterns = router.urls
21 |
--------------------------------------------------------------------------------
/notifications/views/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : __init__.py
5 | # author : ly_13
6 | # date : 9/13/2024
7 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | django==5.1.7
2 | djangorestframework==3.15.2
3 | django-cors-headers==4.7.0
4 | django-filter==25.1
5 | mysqlclient==2.2.7
6 | psycopg2-binary==2.9.10
7 | django-redis==5.4.0
8 | pycryptodomex==3.22.0
9 | djangorestframework-simplejwt==5.5.0
10 | celery==5.4.0
11 | django-celery-beat==2.7.0
12 | django-celery-results==2.5.1
13 | flower==2.0.1
14 | python-daemon==3.1.2
15 | gunicorn==23.0.0
16 | django-proxy==1.3.0
17 | psutil==6.1.1
18 | uvicorn==0.34.0
19 | daphne==4.1.2
20 | channels==4.2.0
21 | channels-redis==4.2.1
22 | django-ranged-response==0.2.0
23 | user-agents==2.2.0
24 | aiofiles==24.1.0
25 | websockets==15.0.1
26 | django-imagekit==5.0.0
27 | pilkit==3.0
28 | drf-spectacular==0.28.0
29 | drf-spectacular-sidecar==2025.3.1
30 | openpyxl==3.2.0b1
31 | pyzipper==0.3.6
32 | unicodecsv==0.14.1
33 | chardet==5.2.0
34 | pyexcel==0.7.2
35 | pyexcel-xlsx==0.6.1
36 | alibabacloud-dysmsapi20170525==3.1.1
37 | phonenumbers==8.13.55
38 | pycountry==24.6.1
39 | geoip2==4.8.1
40 | ipip-ipdb==1.6.1
41 | requests==2.32.3
42 | html2text==2024.2.26
43 | pyotp==2.9.0
--------------------------------------------------------------------------------
/server/__init__.py:
--------------------------------------------------------------------------------
1 | # This will make sure the app is always imported when
2 | # Django starts so that shared_task will use this app.
3 | from .celery import app as celery_app
4 |
5 | __all__ = ('celery_app',)
6 |
--------------------------------------------------------------------------------
/server/celery.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin_server
4 | # filename : celery
5 | # author : ly_13
6 | # date : 6/29/2023
7 |
8 | import os
9 |
10 | from celery import Celery
11 |
12 | # Set the default Django settings module for the 'celery' program.
13 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings')
14 |
15 | app = Celery('server')
16 |
17 | # Using a string here means the worker doesn't have to serialize
18 | # the configuration object to child processes.
19 | # - namespace='CELERY' means all celery-related configuration keys
20 | # should have a `CELERY_` prefix.
21 | app.config_from_object('django.conf:settings', namespace='CELERY')
22 |
23 | # Load task modules from all registered Django apps.
24 | app.autodiscover_tasks()
25 |
--------------------------------------------------------------------------------
/server/const.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | import os
4 |
5 | from .conf import ConfigManager
6 |
7 | __all__ = ['PROJECT_DIR', 'VERSION', 'CONFIG', 'LOG_DIR', 'TMP_DIR', 'CELERY_LOG_DIR']
8 |
9 | PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
10 | LOG_DIR = os.path.join(PROJECT_DIR, "data", "logs")
11 | TMP_DIR = os.path.join(PROJECT_DIR, "tmp")
12 | CELERY_LOG_DIR = os.path.join(LOG_DIR, "task")
13 | VERSION = '4.2.1'
14 | CONFIG = ConfigManager.load_user_config()
15 |
--------------------------------------------------------------------------------
/server/logging.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : logging
5 | # author : ly_13
6 | # date : 10/18/2024
7 | import logging
8 | import os
9 | from datetime import datetime, timedelta
10 | from logging.handlers import TimedRotatingFileHandler
11 |
12 | from server.utils import get_current_request
13 |
14 |
15 | class DailyTimedRotatingFileHandler(TimedRotatingFileHandler):
16 | def rotator(self, source, dest):
17 | """ Override the original method to rotate the log file daily."""
18 | dest = self._get_rotate_dest_filename(source)
19 | if os.path.exists(source) and not os.path.exists(dest):
20 | # 存在多个服务进程时, 保证只有一个进程成功 rotate
21 | os.rename(source, dest)
22 |
23 | @staticmethod
24 | def _get_rotate_dest_filename(source):
25 | date_yesterday = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
26 | path = [os.path.dirname(source), date_yesterday, os.path.basename(source)]
27 | filename = os.path.join(*path)
28 | os.makedirs(os.path.dirname(filename), exist_ok=True)
29 | return filename
30 |
31 |
32 | class ServerFormatter(logging.Formatter):
33 | def format(self, record):
34 | current_request = get_current_request()
35 | record.requestUser = str(current_request.user if current_request else 'SYSTEM')[:16]
36 | record.requestUuid = str(getattr(current_request, 'request_uuid', ""))
37 | return super().format(record)
38 |
39 |
40 | class ColorHandler(logging.StreamHandler):
41 | WHITE = "0"
42 | RED = "31"
43 | GREEN = "32"
44 | YELLOW = "33"
45 | BLUE = "34"
46 | PURPLE = "35"
47 |
48 | def emit(self, record):
49 | try:
50 | msg = self.format(record)
51 | level_color_map = {
52 | logging.DEBUG: self.BLUE,
53 | logging.INFO: self.GREEN,
54 | logging.WARNING: self.YELLOW,
55 | logging.ERROR: self.RED,
56 | logging.CRITICAL: self.PURPLE
57 | }
58 |
59 | csi = f"{chr(27)}[" # 控制序列引入符
60 | color = level_color_map.get(record.levelno, self.WHITE)
61 |
62 | self.stream.write(f"{csi}{color}m{msg}{csi}m\n")
63 | self.flush()
64 | except RecursionError:
65 | raise
66 | except Exception:
67 | self.handleError(record)
68 |
--------------------------------------------------------------------------------
/server/settings/__init__.py:
--------------------------------------------------------------------------------
1 | from .base import *
2 | from .custom import *
3 | from .libs import *
4 | from .logging import *
5 | from .setting import *
6 |
--------------------------------------------------------------------------------
/server/settings/custom.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : custom
5 | # author : ly_13
6 | # date : 11/14/2024
7 |
8 | from ..const import CONFIG
9 |
10 | # 访问白名单配置,无需权限配置, key为路由,value为列表,对应的是请求方式, * 表示全部请求方式, 请求方式为大写
11 | PERMISSION_WHITE_URL = {
12 | "^/api/system/login$": ['*'],
13 | "^/api/system/logout$": ['*'],
14 | "^/api/system/userinfo$": ['GET'],
15 | "^/api/system/routes$": ['*'],
16 | "^/api/system/dashboard/": ['*'],
17 | "^/api/.*choices$": ['*'],
18 | "^/api/.*search-fields$": ['*'],
19 | "^/api/common/resources/cache$": ['*'],
20 | "^/api/notifications/site-messages/unread$": ['*'],
21 | }
22 |
23 | # 前端权限路由 忽略配置
24 | ROUTE_IGNORE_URL = [
25 | "^/api/system/.*choices$", # 每个方法都有该路由,则忽略即可
26 | "^/api/.*search-fields$", # 每个方法都有该路由,则忽略即可
27 | "^/api/.*search-columns$", # 该路由使用list权限字段,无需重新配置
28 | "^/api/settings/.*search-columns$", # 该路由使用list权限字段,无需重新配置
29 | "^/api/system/dashboard/", # 忽略dashboard路由
30 | "^/api/system/captcha", # 忽略图片验证码路由
31 | ]
32 |
33 | # 访问权限配置
34 | PERMISSION_SHOW_PREFIX = [
35 | r'api/system',
36 | r'api/settings',
37 | r'api/notifications',
38 | r'api/flower',
39 | r'api-docs',
40 | ]
41 | # 数据权限配置
42 | PERMISSION_DATA_AUTH_APPS = [
43 | 'system',
44 | 'settings',
45 | 'notifications'
46 | ]
47 |
48 | API_LOG_ENABLE = CONFIG.API_LOG_ENABLE
49 | API_LOG_METHODS = CONFIG.API_LOG_METHODS # 'ALL'
50 |
51 | # 忽略日志记录, 支持model 或者 request_path, 不支持正则
52 | API_LOG_IGNORE = CONFIG.API_LOG_IGNORE
53 |
54 | # 在操作日志中详细记录的请求模块映射
55 | API_MODEL_MAP = CONFIG.API_MODEL_MAP
56 |
--------------------------------------------------------------------------------
/server/utils.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : utils
5 | # author : ly_13
6 | # date : 10/18/2024
7 |
8 | from django.conf import settings
9 | from django.db import connection
10 | from django.db.backends.utils import truncate_name
11 | from django.db.models.signals import class_prepared
12 |
13 | from common.local import thread_local
14 |
15 |
16 | def set_current_request(request):
17 | setattr(thread_local, 'current_request', request)
18 |
19 |
20 | def _find(attr):
21 | return getattr(thread_local, attr, None)
22 |
23 |
24 | def get_current_request():
25 | return _find('current_request')
26 |
27 |
28 | def add_db_prefix(sender, **kwargs):
29 | prefix = settings.DB_PREFIX
30 | meta = sender._meta
31 | if not meta.managed:
32 | return
33 | if isinstance(prefix, dict):
34 | app_label = meta.app_label.lower()
35 | if meta.label_lower in prefix:
36 | prefix = prefix[meta.label_lower]
37 | elif meta.label in prefix:
38 | prefix = prefix[meta.label]
39 | elif app_label in prefix:
40 | prefix = prefix[app_label]
41 | else:
42 | prefix = prefix.get("", None)
43 | if prefix and not meta.db_table.startswith(prefix):
44 | meta.db_table = truncate_name("%s%s" % (prefix, meta.db_table), connection.ops.max_name_length())
45 |
46 |
47 | class_prepared.connect(add_db_prefix)
48 |
--------------------------------------------------------------------------------
/server/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for server project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings')
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/settings/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/settings/__init__.py
--------------------------------------------------------------------------------
/settings/admin.py:
--------------------------------------------------------------------------------
1 | # Register your models here.
2 |
--------------------------------------------------------------------------------
/settings/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class SettingsConfig(AppConfig):
5 | default_auto_field = 'django.db.models.BigAutoField'
6 | name = 'settings'
7 |
8 | def ready(self):
9 | from . import signal_handlers # noqa
10 | super().ready()
11 |
--------------------------------------------------------------------------------
/settings/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.1.1 on 2024-09-23 06:50
2 |
3 | import uuid
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 | initial = True
10 |
11 | dependencies = [
12 | ]
13 |
14 | operations = [
15 | migrations.CreateModel(
16 | name='Setting',
17 | fields=[
18 | ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='ID')),
19 | ('created_time', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Created time')),
20 | ('updated_time', models.DateTimeField(auto_now=True, null=True, verbose_name='Updated time')),
21 | ('description', models.CharField(blank=True, max_length=256, null=True, verbose_name='Description')),
22 | ('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
23 | ('value', models.TextField(blank=True, null=True, verbose_name='Value')),
24 | ('category', models.CharField(default='default', max_length=128, verbose_name='Category')),
25 | ('encrypted', models.BooleanField(default=False, verbose_name='Encrypted')),
26 | ('is_active', models.BooleanField(default=True, verbose_name='Is active')),
27 | ],
28 | options={
29 | 'verbose_name': 'System setting',
30 | 'verbose_name_plural': 'System setting',
31 | },
32 | ),
33 | ]
34 |
--------------------------------------------------------------------------------
/settings/migrations/0002_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.1.1 on 2024-09-23 06:50
2 |
3 | import django.db.models.deletion
4 | from django.conf import settings
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 | initial = True
10 |
11 | dependencies = [
12 | ('settings', '0001_initial'),
13 | ('system', '0001_initial'),
14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15 | ]
16 |
17 | operations = [
18 | migrations.AddField(
19 | model_name='setting',
20 | name='creator',
21 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL,
22 | related_name='+', related_query_name='creator_query', to=settings.AUTH_USER_MODEL,
23 | verbose_name='Creator'),
24 | ),
25 | migrations.AddField(
26 | model_name='setting',
27 | name='dept_belong',
28 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL,
29 | related_name='+', related_query_name='dept_belong_query', to='system.deptinfo',
30 | verbose_name='Data ownership department'),
31 | ),
32 | migrations.AddField(
33 | model_name='setting',
34 | name='modifier',
35 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL,
36 | related_name='+', related_query_name='modifier_query', to=settings.AUTH_USER_MODEL,
37 | verbose_name='Modifier'),
38 | ),
39 | ]
40 |
--------------------------------------------------------------------------------
/settings/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/settings/migrations/__init__.py
--------------------------------------------------------------------------------
/settings/serializers/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : __init__.py
5 | # author : ly_13
6 | # date : 8/1/2024
7 |
--------------------------------------------------------------------------------
/settings/serializers/basic.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : basic
5 | # author : ly_13
6 | # date : 8/1/2024
7 | from django.utils.translation import gettext_lazy as _
8 | from rest_framework import serializers
9 |
10 | from system.signal import invalid_user_cache_signal
11 |
12 |
13 | class BasicSettingSerializer(serializers.Serializer):
14 | SITE_URL = serializers.URLField(
15 | required=False, label=_("Site URL"),
16 | help_text=_(
17 | 'Site URL is the externally accessible address of the current product '
18 | 'service and is usually used in links in system emails'
19 | )
20 | )
21 |
22 | FRONT_END_WEB_WATERMARK_ENABLED = serializers.BooleanField(
23 | required=False, default=True, label=_("Front-end web watermark"),
24 | help_text=_("Enable watermark for front-end web")
25 | )
26 |
27 | PERMISSION_FIELD_ENABLED = serializers.BooleanField(
28 | required=False, default=True, label=_("Field permission"),
29 | help_text=_("Field permissions are used to authorize access to data field display")
30 | )
31 |
32 | PERMISSION_DATA_ENABLED = serializers.BooleanField(
33 | required=False, default=True, label=_("Data permission"),
34 | help_text=_("Data permissions are used to authorize access to data")
35 | )
36 |
37 | EXPORT_MAX_LIMIT = serializers.IntegerField(
38 | required=False, label=_('Export max limit'),
39 | help_text=_("Limit the maximum number of rows of exported data")
40 | )
41 |
42 | @staticmethod
43 | def validate_SITE_URL(s):
44 | if not s:
45 | return 'http://127.0.0.1'
46 | return s.strip('/')
47 |
48 | def post_save(self):
49 | if set(getattr(self, '_change_fields', [])) & {'PERMISSION_FIELD_ENABLED', 'PERMISSION_DATA_ENABLED'}:
50 | invalid_user_cache_signal.send(sender=self, user_pk='*')
51 |
--------------------------------------------------------------------------------
/settings/serializers/email.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : email
5 | # author : ly_13
6 | # date : 8/1/2024
7 | from django.utils.translation import gettext_lazy as _
8 | from rest_framework import serializers
9 |
10 |
11 | class EmailSettingSerializer(serializers.Serializer):
12 | EMAIL_ENABLED = serializers.BooleanField(
13 | default=False, label=_('Email'), help_text=_('Enable Email Service (Email)')
14 | )
15 | EMAIL_HOST = serializers.CharField(max_length=1024, required=True, label=_("Host"))
16 | EMAIL_PORT = serializers.CharField(max_length=5, required=True, label=_("Port"))
17 | EMAIL_HOST_USER = serializers.CharField(
18 | max_length=128, required=True, label=_("Account"),
19 | help_text=_("The user to be used for email server authentication")
20 | )
21 | EMAIL_HOST_PASSWORD = serializers.CharField(
22 | max_length=1024, required=False, label=_("Password"), write_only=True,
23 | help_text=_(
24 | "Password to use for the email server. It is used in conjunction with `User` when authenticating to the email server")
25 | )
26 | EMAIL_FROM = serializers.CharField(
27 | max_length=128, allow_blank=True, required=False, label=_('Sender'),
28 | help_text=_('Sender email address (default to using the `User`)')
29 | )
30 |
31 | EMAIL_SUBJECT_PREFIX = serializers.CharField(
32 | max_length=128, allow_blank=True, required=False, label=_('Subject prefix'),
33 | help_text=_("The subject line prefix of the sent email")
34 | )
35 | EMAIL_USE_SSL = serializers.BooleanField(
36 | required=False, label=_('Use SSL'),
37 | help_text=_(
38 | 'Whether to use an implicit TLS (secure) connection when talking to the SMTP server. In most email documentation this type of TLS connection is referred to as SSL. It is generally used on port 465')
39 | )
40 | EMAIL_USE_TLS = serializers.BooleanField(
41 | required=False, label=_("Use TLS"),
42 | help_text=_(
43 | 'Whether to use a TLS (secure) connection when talking to the SMTP server. This is used for explicit TLS connections, generally on port 587')
44 | )
45 |
46 | EMAIL_RECIPIENT = serializers.EmailField(
47 | max_length=128, allow_blank=True, required=False, label=_('Recipient'),
48 | help_text=_("The recipient is used for testing the email server's connectivity")
49 | )
50 |
--------------------------------------------------------------------------------
/settings/serializers/setting.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : settings
5 | # author : ly_13
6 | # date : 10/25/2024
7 | from common.core.serializers import BaseModelSerializer
8 | from settings.models import Setting
9 |
10 |
11 | class SettingSerializer(BaseModelSerializer):
12 | class Meta:
13 | model = Setting
14 | fields = ['pk', 'name', 'value', 'category', 'is_active', 'encrypted', 'created_time']
15 | read_only_fields = ['pk']
16 |
--------------------------------------------------------------------------------
/settings/serializers/sms.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : sms
5 | # author : ly_13
6 | # date : 8/6/2024
7 | import phonenumbers
8 | from django.utils.translation import gettext_lazy as _
9 | from rest_framework import serializers
10 |
11 | from common.core.fields import PhoneField
12 | from common.core.validators import PhoneValidator
13 | from common.sdk.sms.endpoint import BACKENDS
14 |
15 |
16 | class SMSSettingSerializer(serializers.Serializer):
17 | SMS_ENABLED = serializers.BooleanField(
18 | default=False, label=_('SMS'), help_text=_('Enable Short Message Service (SMS)')
19 | )
20 | SMS_BACKEND = serializers.ChoiceField(choices=BACKENDS.choices, default=BACKENDS.ALIBABA, label=_('Provider'),
21 | help_text=_('Short Message Service (SMS) provider or protocol'))
22 |
23 |
24 | class BaseSMSSettingSerializer(serializers.Serializer):
25 | PREFIX_TITLE = _('SMS')
26 |
27 | SMS_TEST_PHONE = PhoneField(
28 | validators=[PhoneValidator()], required=False, allow_blank=True, allow_null=True,
29 | label=_('Phone'), help_text=_("The phone is used for testing the SMS server's connectivity")
30 | )
31 |
32 | def post_save(self):
33 | value = self._data['SMS_TEST_PHONE']
34 | if isinstance(value, dict):
35 | return
36 | try:
37 | phone = phonenumbers.parse(value, 'CN')
38 | value = {'code': '+%s' % phone.country_code, 'phone': phone.national_number}
39 | except phonenumbers.NumberParseException:
40 | value = {'code': '+86', 'phone': value}
41 | self._data['SMS_TEST_PHONE'] = value
42 |
43 |
44 | class AlibabaSMSSettingSerializer(BaseSMSSettingSerializer):
45 | ALIBABA_ACCESS_KEY_ID = serializers.CharField(max_length=256, required=True, label='Access Key ID')
46 | ALIBABA_ACCESS_KEY_SECRET = serializers.CharField(
47 | max_length=256, required=False, label='Access Key Secret', write_only=True
48 | )
49 | ALIBABA_VERIFY_SIGN_NAME = serializers.CharField(max_length=256, required=True, label=_('Signature'))
50 | ALIBABA_VERIFY_TEMPLATE_CODE = serializers.CharField(max_length=256, required=True, label=_('Template code'))
51 |
52 |
53 | class SMSBackendSerializer(serializers.Serializer):
54 | name = serializers.CharField(max_length=256, required=True, label=_('Name'))
55 | label = serializers.CharField(max_length=256, required=True, label=_('Label'))
56 |
--------------------------------------------------------------------------------
/settings/signal_handlers.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : signal_handlers.py
5 | # author : ly_13
6 | # date : 7/31/2024
7 |
8 | from django.db.models.signals import post_save
9 | from django.dispatch import receiver
10 | from django.utils.functional import LazyObject
11 |
12 | from common.signals import django_ready
13 | from common.utils import get_logger
14 | from common.utils.connection import RedisPubSub
15 | from settings.models import Setting
16 |
17 | logger = get_logger(__name__)
18 |
19 |
20 | class SettingSubPub(LazyObject):
21 | def _setup(self):
22 | self._wrapped = RedisPubSub('settings')
23 |
24 |
25 | setting_pub_sub = SettingSubPub()
26 |
27 |
28 | @receiver(post_save, sender=Setting)
29 | def refresh_settings_on_changed(sender, instance=None, **kwargs):
30 | if not instance:
31 | return
32 | setting_pub_sub.publish((instance.name, instance.cleaned_value))
33 |
34 |
35 | @receiver(django_ready)
36 | def on_django_ready_add_db_config(sender, **kwargs):
37 | Setting.refresh_all_settings()
38 |
39 |
40 | @receiver(django_ready)
41 | def subscribe_settings_change(sender, **kwargs):
42 | logger.debug("Start subscribe setting change")
43 |
44 | setting_pub_sub.subscribe(lambda name: Setting.refresh_item(name))
45 |
--------------------------------------------------------------------------------
/settings/tests.py:
--------------------------------------------------------------------------------
1 | # Create your tests here.
2 |
--------------------------------------------------------------------------------
/settings/urls.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : server
4 | # filename : urls
5 | # author : ly_13
6 | # date : 6/6/2023
7 | from rest_framework.routers import SimpleRouter
8 |
9 | from common.core.routers import NoDetailRouter
10 | from settings.views.basic import BasicSettingViewSet
11 | from settings.views.block_ip import SecurityBlockIpViewSet
12 | from settings.views.email import EmailServerSettingViewSet
13 | from settings.views.security import SecurityPasswordRuleViewSet, SecurityLoginLimitViewSet, \
14 | SecurityLoginAuthViewSet, SecurityRegisterAuthViewSet, SecurityResetPasswordAuthViewSet, \
15 | SecurityBindEmailAuthViewSet, SecurityBindPhoneAuthViewSet, SecurityVerifyCodeViewSet, SecurityCaptchaCodeViewSet
16 | from settings.views.settings import SettingViewSet
17 | from settings.views.sms import SmsSettingViewSet, SmsConfigViewSet
18 |
19 | app_name = "settings"
20 |
21 | router = SimpleRouter(False)
22 | no_detail_router = NoDetailRouter(False)
23 |
24 | # 设置相关
25 | no_detail_router.register('email', EmailServerSettingViewSet, basename='email-server')
26 |
27 | no_detail_router.register('basic', BasicSettingViewSet, basename='basic')
28 | no_detail_router.register('password', SecurityPasswordRuleViewSet, basename='security-password')
29 | no_detail_router.register('verify', SecurityVerifyCodeViewSet, basename='verify-code')
30 | no_detail_router.register('captcha', SecurityCaptchaCodeViewSet, basename='captcha-code')
31 |
32 | no_detail_router.register('login/limit', SecurityLoginLimitViewSet, basename='security-login-limit')
33 | no_detail_router.register('login/auth', SecurityLoginAuthViewSet, basename='security-login-auth')
34 |
35 | no_detail_router.register('register/auth', SecurityRegisterAuthViewSet, basename='security-register-auth')
36 | no_detail_router.register('reset/auth', SecurityResetPasswordAuthViewSet, basename='security-reset-auth')
37 | no_detail_router.register('bind/email', SecurityBindEmailAuthViewSet, basename='security-bind-email-auth')
38 | no_detail_router.register('bind/phone', SecurityBindPhoneAuthViewSet, basename='security-bind-phone-auth')
39 |
40 | no_detail_router.register('sms', SmsSettingViewSet, basename='sms-settings')
41 |
42 | router.register('ip/block', SecurityBlockIpViewSet, basename='ip-block')
43 | router.register('setting', SettingViewSet, basename='setting')
44 |
45 | no_detail_router.register('sms/config', SmsConfigViewSet, basename='sms-config')
46 |
47 | urlpatterns = no_detail_router.urls + router.urls
48 |
--------------------------------------------------------------------------------
/settings/utils/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : __init__.py
5 | # author : ly_13
6 | # date : 7/31/2024
7 |
--------------------------------------------------------------------------------
/settings/utils/password.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : password
5 | # author : ly_13
6 | # date : 8/10/2024
7 | import re
8 |
9 | from django.conf import settings
10 |
11 |
12 | def get_password_check_rules(user):
13 | check_rules = []
14 | for rule in settings.SECURITY_PASSWORD_RULES:
15 | if user.is_superuser and rule == 'SECURITY_PASSWORD_MIN_LENGTH':
16 | rule = 'SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH'
17 | value = getattr(settings, rule)
18 | if not value:
19 | continue
20 | check_rules.append({'key': rule, 'value': int(value)})
21 | return check_rules
22 |
23 |
24 | def check_password_rules(password, is_super_admin=False):
25 | pattern = r"^"
26 | if settings.SECURITY_PASSWORD_UPPER_CASE:
27 | pattern += r'(?=.*[A-Z])'
28 | if settings.SECURITY_PASSWORD_LOWER_CASE:
29 | pattern += r'(?=.*[a-z])'
30 | if settings.SECURITY_PASSWORD_NUMBER:
31 | pattern += r'(?=.*\d)'
32 | if settings.SECURITY_PASSWORD_SPECIAL_CHAR:
33 | pattern += r'(?=.*[`~!@#$%^&*()\-=_+\[\]{}|;:\'",.<>/?])'
34 | pattern += r'[a-zA-Z\d`~!@#\$%\^&\*\(\)-=_\+\[\]\{\}\|;:\'\",\.<>\/\?]'
35 | if is_super_admin:
36 | min_length = settings.SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH
37 | else:
38 | min_length = settings.SECURITY_PASSWORD_MIN_LENGTH
39 | pattern += '.{' + str(min_length - 1) + ',}$'
40 | match_obj = re.match(pattern, password)
41 | return bool(match_obj)
42 |
--------------------------------------------------------------------------------
/settings/views/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/settings/views/__init__.py
--------------------------------------------------------------------------------
/settings/views/basic.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : basic
5 | # author : ly_13
6 | # date : 7/31/2024
7 |
8 | from common.utils import get_logger
9 | from settings.serializers.basic import BasicSettingSerializer
10 | from settings.views.settings import BaseSettingViewSet
11 |
12 | logger = get_logger(__name__)
13 |
14 |
15 | class BasicSettingViewSet(BaseSettingViewSet):
16 | """基本设置"""
17 | serializer_class = BasicSettingSerializer
18 | category = "basic"
19 |
--------------------------------------------------------------------------------
/settings/views/block_ip.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : block_ip
5 | # author : ly_13
6 | # date : 8/12/2024
7 | import socket
8 | import struct
9 |
10 | from django.conf import settings
11 | from django.core.cache import cache
12 |
13 | from common.core.modelset import ListDeleteModelSet
14 | from settings.models import Setting
15 | from settings.serializers.security import SecurityBlockIPSerializer
16 | from settings.utils.security import LoginIpBlockUtil
17 |
18 |
19 | class FilterIps(list):
20 |
21 | def filter(self, pk__in=None):
22 | if pk__in is None:
23 | pk__in = []
24 | return [obj.get('ip') for obj in self.__iter__() if obj.get('pk')() in pk__in]
25 |
26 |
27 | class IpUtils(object):
28 | def __init__(self, ip):
29 | self.ip = ip
30 |
31 | def ip_to_int(self):
32 | return str(struct.unpack("!I", socket.inet_aton(self.ip))[0])
33 |
34 | def int_to_ip(self):
35 | return socket.inet_ntoa(struct.pack("!I", int(self.ip)))
36 |
37 |
38 | class SecurityBlockIpViewSet(ListDeleteModelSet):
39 | """Ip拦截名单"""
40 | serializer_class = SecurityBlockIPSerializer
41 | queryset = Setting.objects.none()
42 |
43 | def filter_queryset(self, obj):
44 | # 为啥写函数,去没有加(), 因为只有在序列化的时候,才会判断,如果是方法就执行,减少资源浪费
45 | data = [{'ip': ip, 'pk': IpUtils(ip).ip_to_int, 'created_time': LoginIpBlockUtil(ip).get_block_info} for ip in
46 | obj]
47 | return FilterIps(data)
48 |
49 | def get_queryset(self):
50 | ips = []
51 | prefix = LoginIpBlockUtil.BLOCK_KEY_TMPL.replace('{}', '')
52 | keys = cache.keys(f'{prefix}*')
53 | for key in keys:
54 | ips.append(key.replace(prefix, ''))
55 |
56 | white_list = settings.SECURITY_LOGIN_IP_WHITE_LIST
57 | ips = list(set(ips) - set(white_list))
58 | ips = [ip for ip in ips if ip != '*']
59 | return ips
60 |
61 | def get_object(self):
62 | return IpUtils(self.kwargs.get("pk")).int_to_ip()
63 |
64 | def perform_destroy(self, ip):
65 | LoginIpBlockUtil(ip).clean_block_if_need()
66 | return 1, 1
67 |
--------------------------------------------------------------------------------
/settings/views/security.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : security
5 | # author : ly_13
6 | # date : 8/1/2024
7 |
8 | from common.utils import get_logger
9 | from settings.serializers.security import SecurityPasswordRuleSerializer, SecurityLoginLimitSerializer, \
10 | SecurityLoginAuthSerializer, SecurityRegisterAuthSerializer, SecurityResetPasswordAuthSerializer, \
11 | SecurityBindEmailAuthSerializer, SecurityBindPhoneAuthSerializer, SecurityVerifyCodeSerializer, \
12 | SecurityCaptchaCodeSerializer
13 | from settings.views.settings import BaseSettingViewSet
14 |
15 | logger = get_logger(__name__)
16 |
17 |
18 | class SecurityPasswordRuleViewSet(BaseSettingViewSet):
19 | """密码规则"""
20 | serializer_class = SecurityPasswordRuleSerializer
21 | category = "security_password"
22 |
23 |
24 | class SecurityLoginLimitViewSet(BaseSettingViewSet):
25 | """登录限制"""
26 | serializer_class = SecurityLoginLimitSerializer
27 | category = "security_login_limit"
28 |
29 |
30 | class SecurityLoginAuthViewSet(BaseSettingViewSet):
31 | """登录安全"""
32 | serializer_class = SecurityLoginAuthSerializer
33 | category = "security_login_auth"
34 |
35 |
36 | class SecurityRegisterAuthViewSet(BaseSettingViewSet):
37 | """注册安全"""
38 | serializer_class = SecurityRegisterAuthSerializer
39 | category = "security_register_auth"
40 |
41 |
42 | class SecurityResetPasswordAuthViewSet(BaseSettingViewSet):
43 | """重置密码"""
44 | serializer_class = SecurityResetPasswordAuthSerializer
45 | category = "security_reset_password_auth"
46 |
47 |
48 | class SecurityBindEmailAuthViewSet(BaseSettingViewSet):
49 | """绑定邮件"""
50 | serializer_class = SecurityBindEmailAuthSerializer
51 | category = "security_bind_email_auth"
52 |
53 |
54 | class SecurityBindPhoneAuthViewSet(BaseSettingViewSet):
55 | """绑定手机"""
56 | serializer_class = SecurityBindPhoneAuthSerializer
57 | category = "security_bind_phone_auth"
58 |
59 |
60 | class SecurityVerifyCodeViewSet(BaseSettingViewSet):
61 | """验证码规则"""
62 | serializer_class = SecurityVerifyCodeSerializer
63 | category = "verify"
64 |
65 |
66 | class SecurityCaptchaCodeViewSet(BaseSettingViewSet):
67 | """图片验证码"""
68 | serializer_class = SecurityCaptchaCodeSerializer
69 | category = "captcha"
70 |
--------------------------------------------------------------------------------
/system/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/system/__init__.py
--------------------------------------------------------------------------------
/system/admin.py:
--------------------------------------------------------------------------------
1 | # Register your models here.
2 | from django.contrib import admin
3 |
4 | # Register your models here.
5 | from system.models import *
6 |
7 | admin.site.register(UserInfo)
8 | admin.site.register(DeptInfo)
9 | admin.site.register(ModelLabelField)
10 | admin.site.register(UserLoginLog)
11 | admin.site.register(OperationLog)
12 | admin.site.register(MenuMeta)
13 | admin.site.register(Menu)
14 | admin.site.register(DataPermission)
15 | admin.site.register(FieldPermission)
16 | admin.site.register(UserRole)
17 | admin.site.register(UploadFile)
18 | admin.site.register(SystemConfig)
19 | admin.site.register(UserPersonalConfig)
20 |
--------------------------------------------------------------------------------
/system/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class SystemConfig(AppConfig):
5 | default_auto_field = 'django.db.models.BigAutoField'
6 | name = 'system'
7 |
8 | def ready(self):
9 | from . import signal_handler # noqa
10 | super().ready()
11 |
--------------------------------------------------------------------------------
/system/management/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/system/management/__init__.py
--------------------------------------------------------------------------------
/system/management/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/system/management/commands/__init__.py
--------------------------------------------------------------------------------
/system/management/commands/dump_init_json.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : dump_init_json
5 | # author : ly_13
6 | # date : 12/25/2023
7 | import os.path
8 |
9 | from django.conf import settings
10 | from django.core import serializers
11 | from django.core.management.base import BaseCommand
12 |
13 | from settings.models import Setting
14 | from system.models import *
15 |
16 |
17 | def get_fields(model):
18 | if issubclass(model, FieldPermission):
19 | exclude_fields = ['updated_time', 'created_time']
20 | elif issubclass(model, ModelLabelField):
21 | exclude_fields = ['updated_time']
22 | else:
23 | exclude_fields = []
24 |
25 | return [x.name for x in model._meta.get_fields() if x.name not in exclude_fields]
26 |
27 |
28 | class Command(BaseCommand):
29 | help = 'dump init json data'
30 | model_names = [UserRole, DeptInfo, Menu, MenuMeta, SystemConfig, DataPermission, FieldPermission, ModelLabelField,
31 | Setting]
32 |
33 | def save_json(self, queryset, filename):
34 | stream = open(filename, 'w', encoding='utf8')
35 | try:
36 | serializers.serialize(
37 | 'json',
38 | queryset,
39 | indent=2,
40 | stream=stream or self.stdout,
41 | object_count=queryset.count(),
42 | fields=get_fields(queryset.model)
43 | )
44 | except Exception as e:
45 | print(f"{queryset.model._meta.model_name} {filename} dump failed {e}")
46 | finally:
47 | if stream:
48 | stream.close()
49 |
50 | def handle(self, *args, **options):
51 | file_root = os.path.join(settings.PROJECT_DIR, "loadjson")
52 | for model in self.model_names:
53 | self.save_json(model.objects.all().order_by('pk'),
54 | os.path.join(file_root, f"{model._meta.model_name}.json"))
55 |
--------------------------------------------------------------------------------
/system/management/commands/expire_config_caches.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : expire_config_caches
5 | # author : ly_13
6 | # date : 12/25/2023
7 | from django.core.management.base import BaseCommand
8 |
9 | from common.core.config import ConfigCacheBase
10 |
11 |
12 | class Command(BaseCommand):
13 | help = 'Expire config caches'
14 |
15 | def add_arguments(self, parser):
16 | parser.add_argument('key', nargs='?', type=str, default='*')
17 |
18 | def handle(self, *args, **options):
19 | ConfigCacheBase().invalid_config_cache(options.get('key', '*'))
20 | ConfigCacheBase(px='user').invalid_config_cache(options.get('key', '*'))
21 |
--------------------------------------------------------------------------------
/system/management/commands/load_init_json.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : load_init_json
5 | # author : ly_13
6 | # date : 12/25/2023
7 | import os.path
8 |
9 | from django.conf import settings
10 | from django.core.management.commands.loaddata import Command as LoadCommand
11 | from django.db import DEFAULT_DB_ALIAS
12 | from django.db.models.signals import ModelSignal
13 |
14 | from settings.models import Setting
15 | from system.models import *
16 |
17 |
18 | class Command(LoadCommand):
19 | help = 'load init json data'
20 | model_names = [MenuMeta, Menu, SystemConfig, DataPermission, UserRole, FieldPermission, ModelLabelField, DeptInfo,
21 | Setting]
22 | missing_args_message = None
23 |
24 | def add_arguments(self, parser):
25 | pass
26 |
27 | def handle(self, *args, **options):
28 | ModelSignal.send = lambda *args, **kwargs: [] # 忽略任何信号
29 |
30 | fixture_labels = []
31 | file_root = os.path.join(settings.PROJECT_DIR, "loadjson")
32 | for model in self.model_names:
33 | fixture_labels.append(os.path.join(file_root, f"{model._meta.model_name}.json"))
34 | options["ignore"] = ""
35 | options["database"] = DEFAULT_DB_ALIAS
36 | options["app_label"] = ""
37 | options["exclude"] = []
38 | options["format"] = "json"
39 | super(Command, self).handle(*fixture_labels, **options)
40 |
--------------------------------------------------------------------------------
/system/management/commands/sync_model_field.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : sync_model_field
5 | # author : ly_13
6 | # date : 10/25/2024
7 | from django.core.management.base import BaseCommand
8 |
9 | from system.utils.modelfield import sync_model_field
10 |
11 |
12 | class Command(BaseCommand):
13 | help = 'Sync Model Field'
14 |
15 | def handle(self, *args, **options):
16 | sync_model_field()
17 |
--------------------------------------------------------------------------------
/system/migrations/0002_operationlog_exec_time_operationlog_request_uuid.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.1.2 on 2024-10-20 08:36
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ('system', '0001_initial'),
9 | ]
10 |
11 | operations = [
12 | migrations.AddField(
13 | model_name='operationlog',
14 | name='exec_time',
15 | field=models.FloatField(blank=True, null=True, verbose_name='Execution time'),
16 | ),
17 | migrations.AddField(
18 | model_name='operationlog',
19 | name='request_uuid',
20 | field=models.UUIDField(blank=True, null=True, verbose_name='Request ID'),
21 | ),
22 | ]
23 |
--------------------------------------------------------------------------------
/system/migrations/0003_userloginlog_channel_name_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.1.7 on 2025-03-28 14:39
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ('system', '0002_operationlog_exec_time_operationlog_request_uuid'),
9 | ]
10 |
11 | operations = [
12 | migrations.AddField(
13 | model_name='userloginlog',
14 | name='channel_name',
15 | field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Channel name'),
16 | ),
17 | migrations.AlterField(
18 | model_name='userloginlog',
19 | name='login_type',
20 | field=models.SmallIntegerField(
21 | choices=[(0, 'Username and password'), (1, 'SMS verification code'), (2, 'Email verification code'),
22 | (4, 'Wechat scan code'), (8, 'Websocket'), (9, 'Unknown')], default=0,
23 | verbose_name='Login type'),
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/system/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/system/migrations/__init__.py
--------------------------------------------------------------------------------
/system/models/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : __init__.py
5 | # author : ly_13
6 | # date : 8/10/2024
7 |
8 |
9 | from .abstract import *
10 | from .config import *
11 | from .department import *
12 | from .field import *
13 | from .log import *
14 | from .menu import *
15 | from .permission import *
16 | from .role import *
17 | from .upload import *
18 | from .user import *
19 |
--------------------------------------------------------------------------------
/system/models/abstract.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : abstract
5 | # author : ly_13
6 | # date : 8/10/2024
7 |
8 | from django.db import models
9 | from django.utils.translation import gettext_lazy as _
10 |
11 |
12 | class ModeTypeAbstract(models.Model):
13 | class ModeChoices(models.IntegerChoices):
14 | OR = 0, _("Or mode")
15 | AND = 1, _("And mode")
16 |
17 | mode_type = models.SmallIntegerField(choices=ModeChoices, default=ModeChoices.OR,
18 | verbose_name=_("Data permission mode"),
19 | help_text=_(
20 | "Permission mode, and the mode indicates that the data needs to satisfy each rule in the rule list at the same time, or the mode satisfies any rule"))
21 |
22 | class Meta:
23 | abstract = True
24 |
--------------------------------------------------------------------------------
/system/models/config.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : config
5 | # author : ly_13
6 | # date : 8/10/2024
7 |
8 |
9 | from django.db import models
10 | from django.utils.translation import gettext_lazy as _
11 |
12 | from common.core.models import DbAuditModel, DbUuidModel
13 |
14 |
15 | class BaseConfig(DbAuditModel):
16 | value = models.JSONField(max_length=10240, verbose_name=_("Config value"))
17 | is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
18 | access = models.BooleanField(default=False, verbose_name=_("API access"),
19 | help_text=_("Allows API interfaces to access this config"))
20 |
21 | class Meta:
22 | abstract = True
23 |
24 |
25 | class SystemConfig(BaseConfig, DbUuidModel):
26 | key = models.CharField(max_length=255, unique=True, verbose_name=_("Config name"))
27 | inherit = models.BooleanField(default=False, verbose_name=_("User inherit"),
28 | help_text=_("Allows users to inherit this config"))
29 |
30 | class Meta:
31 | verbose_name = _("System config")
32 | verbose_name_plural = verbose_name
33 |
34 | def __str__(self):
35 | return "%s-%s" % (self.key, self.description)
36 |
37 |
38 | class UserPersonalConfig(BaseConfig):
39 | owner = models.ForeignKey("system.UserInfo", verbose_name=_("User"), on_delete=models.CASCADE)
40 | key = models.CharField(max_length=255, verbose_name=_("Config name"))
41 |
42 | class Meta:
43 | verbose_name = _("User config")
44 | verbose_name_plural = verbose_name
45 | unique_together = (('owner', 'key'),)
46 |
47 | def __str__(self):
48 | return "%s-%s" % (self.key, self.description)
49 |
--------------------------------------------------------------------------------
/system/models/field.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : field
5 | # author : ly_13
6 | # date : 8/10/2024
7 |
8 | from django.db import models
9 | from django.utils.translation import gettext_lazy as _
10 |
11 | from common.core.models import DbAuditModel, DbUuidModel
12 |
13 |
14 | class ModelLabelField(DbAuditModel, DbUuidModel):
15 | class KeyChoices(models.TextChoices):
16 | TEXT = 'value.text', _('Text')
17 | JSON = 'value.json', _('Json')
18 | ALL = 'value.all', _('All data')
19 | DATETIME = 'value.datetime', _('Datetime')
20 | DATETIME_RANGE = 'value.datetime.range', _('Datetime range selector')
21 | DATE = 'value.date', _('Seconds to the current time')
22 | OWNER = 'value.user.id', _('My ID')
23 | OWNER_DEPARTMENT = 'value.user.dept.id', _('My department ID')
24 | OWNER_DEPARTMENTS = 'value.user.dept.ids', _('My department ID and data below the department')
25 | DEPARTMENTS = 'value.dept.ids', _('Department ID and data below the department')
26 | TABLE_USER = 'value.table.user.ids', _('Select the user ID')
27 | TABLE_MENU = 'value.table.menu.ids', _('Select menu ID')
28 | TABLE_ROLE = 'value.table.role.ids', _('Select role ID')
29 | TABLE_DEPT = 'value.table.dept.ids', _('Select department ID')
30 |
31 | class FieldChoices(models.IntegerChoices):
32 | ROLE = 0, _("Role permission")
33 | DATA = 1, _("Data permission")
34 |
35 | field_type = models.SmallIntegerField(choices=FieldChoices, default=FieldChoices.DATA, verbose_name=_("Field type"))
36 | parent = models.ForeignKey('system.ModelLabelField', on_delete=models.CASCADE, null=True, blank=True,
37 | verbose_name=_("Parent node"))
38 | name = models.CharField(verbose_name=_("Model/Field name"), max_length=128)
39 | label = models.CharField(verbose_name=_("Model/Field label"), max_length=128)
40 |
41 | class Meta:
42 | ordering = ('-created_time',)
43 | unique_together = ('name', 'parent')
44 | verbose_name = _("Model label field")
45 | verbose_name_plural = verbose_name
46 |
47 | def __str__(self):
48 | return f"{self.label}({self.name})"
49 |
--------------------------------------------------------------------------------
/system/models/permission.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : permission
5 | # author : ly_13
6 | # date : 8/10/2024
7 |
8 |
9 | from django.db import models
10 | from django.utils.translation import gettext_lazy as _
11 |
12 | from common.core.models import DbAuditModel, DbUuidModel, DbCharModel
13 | from system.models import ModeTypeAbstract
14 |
15 |
16 | class DataPermission(DbAuditModel, ModeTypeAbstract, DbUuidModel):
17 | name = models.CharField(verbose_name=_("Name"), max_length=255, unique=True)
18 | rules = models.JSONField(verbose_name=_("Rules"), max_length=10240)
19 | is_active = models.BooleanField(verbose_name=_("Is active"), default=True)
20 | menu = models.ManyToManyField("system.Menu", verbose_name=_("Menu"), blank=True,
21 | help_text=_("If a menu exists, it only applies to the selected menu permission"))
22 |
23 | class Meta:
24 | ordering = ('-created_time',)
25 | verbose_name = _("Data permission")
26 | verbose_name_plural = verbose_name
27 |
28 | def __str__(self):
29 | return f"{self.name}"
30 |
31 |
32 | class FieldPermission(DbAuditModel, DbCharModel):
33 | role = models.ForeignKey("system.UserRole", on_delete=models.CASCADE, verbose_name=_("Role"))
34 | menu = models.ForeignKey("system.Menu", on_delete=models.CASCADE, verbose_name=_("Menu"))
35 | field = models.ManyToManyField("system.ModelLabelField", verbose_name=_("Field"), blank=True)
36 |
37 | class Meta:
38 | verbose_name = _("Field permission")
39 | verbose_name_plural = verbose_name
40 | ordering = ("-created_time",)
41 | unique_together = ("role", "menu")
42 |
43 | def save(self, *args, **kwargs):
44 | self.id = f"{self.role.pk}-{self.menu.pk}"
45 | return super().save(*args, **kwargs)
46 |
47 | def __str__(self):
48 | return f"{self.pk}-{self.role.name}-{self.created_time}"
49 |
--------------------------------------------------------------------------------
/system/models/role.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : role
5 | # author : ly_13
6 | # date : 8/10/2024
7 |
8 | from django.db import models
9 | from django.utils.translation import gettext_lazy as _
10 |
11 | from common.core.models import DbAuditModel, DbUuidModel
12 |
13 |
14 | class UserRole(DbAuditModel, DbUuidModel):
15 | name = models.CharField(max_length=128, verbose_name=_("Role name"), unique=True)
16 | code = models.CharField(max_length=128, verbose_name=_("Role code"), unique=True)
17 | is_active = models.BooleanField(verbose_name=_("Is active"), default=True)
18 | menu = models.ManyToManyField('system.Menu', verbose_name=_("Menu"), blank=True)
19 |
20 | class Meta:
21 | verbose_name = _("User role")
22 | verbose_name_plural = verbose_name
23 | ordering = ("-created_time",)
24 |
25 | def __str__(self):
26 | return f"{self.name}({self.code})"
27 |
--------------------------------------------------------------------------------
/system/models/upload.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : upload
5 | # author : ly_13
6 | # date : 8/10/2024
7 |
8 | import hashlib
9 |
10 | from django.db import models
11 | from django.utils.translation import gettext_lazy as _
12 |
13 | from common.core.models import upload_directory_path, DbAuditModel, AutoCleanFileMixin
14 |
15 |
16 | class UploadFile(AutoCleanFileMixin, DbAuditModel):
17 | filepath = models.FileField(verbose_name=_("Filepath"), null=True, blank=True, upload_to=upload_directory_path)
18 | file_url = models.URLField(verbose_name=_("Internet URL"), max_length=255, blank=True, null=True,
19 | help_text=_("Usually an address accessible to the outside Internet"))
20 | filename = models.CharField(verbose_name=_("Filename"), max_length=255)
21 | filesize = models.IntegerField(verbose_name=_("Filesize"))
22 | mime_type = models.CharField(max_length=255, verbose_name=_("Mime type"))
23 | md5sum = models.CharField(max_length=36, verbose_name=_("File md5sum"))
24 | is_tmp = models.BooleanField(verbose_name=_("Tmp file"), default=False,
25 | help_text=_("Temporary files are automatically cleared by scheduled tasks"))
26 | is_upload = models.BooleanField(verbose_name=_("Upload file"), default=False)
27 |
28 | def save(self, *args, **kwargs):
29 | self.filename = self.filename[:255]
30 | if not self.md5sum and not self.file_url:
31 | md5 = hashlib.md5()
32 | for chunk in self.filepath.chunks():
33 | md5.update(chunk)
34 | if not self.filesize:
35 | self.filesize = self.filepath.size
36 | self.md5sum = md5.hexdigest()
37 | return super().save(*args, **kwargs)
38 |
39 | class Meta:
40 | verbose_name = _("Upload file")
41 | verbose_name_plural = verbose_name
42 |
43 | def __str__(self):
44 | return f"{self.filename}"
45 |
--------------------------------------------------------------------------------
/system/models/user.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : user
5 | # author : ly_13
6 | # date : 8/10/2024
7 |
8 | from django.contrib.auth.models import AbstractUser
9 | from django.db import models
10 | from django.utils.translation import gettext_lazy as _
11 | from pilkit.processors import ResizeToFill
12 |
13 | from common.core.models import upload_directory_path, DbAuditModel, AutoCleanFileMixin
14 | from common.fields.image import ProcessedImageField
15 | from system.models import ModeTypeAbstract
16 |
17 |
18 | class UserInfo(AutoCleanFileMixin, DbAuditModel, AbstractUser, ModeTypeAbstract):
19 | class GenderChoices(models.IntegerChoices):
20 | UNKNOWN = 0, _("Unknown")
21 | MALE = 1, _("Male")
22 | FEMALE = 2, _("Female")
23 |
24 | avatar = ProcessedImageField(verbose_name=_("Avatar"), null=True, blank=True,
25 | upload_to=upload_directory_path,
26 | processors=[ResizeToFill(512, 512)], # 默认存储像素大小
27 | scales=[1, 2, 3, 4], # 缩略图可缩小倍数,
28 | format='png')
29 |
30 | nickname = models.CharField(verbose_name=_("Nickname"), max_length=150, blank=True)
31 | gender = models.IntegerField(choices=GenderChoices, default=GenderChoices.UNKNOWN, verbose_name=_("Gender"))
32 | phone = models.CharField(verbose_name=_("Phone"), max_length=16, default='', blank=True, db_index=True)
33 | email = models.EmailField(verbose_name=_("Email"), default='', blank=True, db_index=True)
34 |
35 | roles = models.ManyToManyField(to="system.UserRole", verbose_name=_("Role permission"), blank=True)
36 | rules = models.ManyToManyField(to="system.DataPermission", verbose_name=_("Data permission"), blank=True)
37 | dept = models.ForeignKey(to="system.DeptInfo", verbose_name=_("Department"), on_delete=models.PROTECT, blank=True,
38 | null=True, related_query_name="dept_query")
39 |
40 | class Meta:
41 | verbose_name = _("Userinfo")
42 | verbose_name_plural = verbose_name
43 | ordering = ("-date_joined",)
44 |
45 | def __str__(self):
46 | return f"{self.nickname}({self.username})"
47 |
--------------------------------------------------------------------------------
/system/notifications.py:
--------------------------------------------------------------------------------
1 | from django.template.loader import render_to_string
2 | from django.utils.translation import gettext_lazy as _
3 |
4 | from common.utils.request import get_request_ip, get_browser
5 | from common.utils.timezone import local_now_display
6 | from notifications.notifications import UserMessage
7 |
8 |
9 | class DifferentCityLoginMessage(UserMessage):
10 | category = 'AccountSecurity'
11 | category_label = _('Account Security')
12 | message_type_label = _('Different city login reminder')
13 |
14 | def __init__(self, user, ip, city):
15 | self.ip = ip
16 | self.city = city
17 | super().__init__(user)
18 |
19 | def get_html_msg(self) -> dict:
20 | now = local_now_display()
21 | subject = _('Different city login reminder')
22 | context = dict(
23 | subject=subject,
24 | name=self.user.nickname,
25 | username=self.user.username,
26 | ip=self.ip,
27 | time=now,
28 | city=self.city,
29 | )
30 | message = render_to_string('notify/msg_different_city.html', context)
31 | return {
32 | 'subject': subject,
33 | 'message': message
34 | }
35 |
36 | @classmethod
37 | def gen_test_msg(cls):
38 | from system.models import UserInfo
39 | user = UserInfo.objects.first()
40 | ip = '8.8.8.8'
41 | city = '洛杉矶'
42 | return cls(user, ip, city)
43 |
44 |
45 | class ResetPasswordSuccessMsg(UserMessage):
46 | category = 'AccountSecurity'
47 | category_label = _('Account Security')
48 | message_type_label = _('Reset password reminder')
49 |
50 | def __init__(self, user, request):
51 | super().__init__(user)
52 | self.ip_address = get_request_ip(request)
53 | self.browser = get_browser(request)
54 |
55 | def get_html_msg(self) -> dict:
56 | user = self.user
57 |
58 | subject = _('Reset password success')
59 | context = {
60 | 'name': user.nickname,
61 | 'username': user.username,
62 | 'ip_address': self.ip_address,
63 | 'browser': self.browser,
64 | }
65 | message = render_to_string('notify/msg_rest_password_success.html', context)
66 | return {
67 | 'subject': subject,
68 | 'message': message
69 | }
70 |
71 | @classmethod
72 | def gen_test_msg(cls):
73 | pass
74 |
--------------------------------------------------------------------------------
/system/serializers/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : __init__.py
5 | # author : ly_13
6 | # date : 8/10/2024
7 |
--------------------------------------------------------------------------------
/system/serializers/department.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : department
5 | # author : ly_13
6 | # date : 8/10/2024
7 |
8 | from django.utils.translation import gettext_lazy as _
9 | from drf_spectacular.utils import extend_schema_field
10 | from rest_framework import serializers
11 | from rest_framework.exceptions import ValidationError
12 |
13 | from common.core.serializers import BaseModelSerializer
14 | from common.utils import get_logger
15 | from system.models import DeptInfo
16 |
17 | logger = get_logger(__name__)
18 |
19 |
20 | class DeptSerializer(BaseModelSerializer):
21 | class Meta:
22 | model = DeptInfo
23 | fields = [
24 | 'pk', 'name', 'code', 'parent', 'rank', 'is_active', 'roles', 'user_count', 'rules', 'mode_type',
25 | 'auto_bind', 'description', 'created_time'
26 | ]
27 |
28 | table_fields = [
29 | 'name', 'pk', 'code', 'user_count', 'rank', 'mode_type', 'auto_bind', 'is_active', 'roles', 'rules',
30 | 'created_time'
31 | ]
32 |
33 | extra_kwargs = {
34 | 'roles': {'required': False, 'attrs': ['pk', 'name', 'code'], 'format': "{name}", 'many': True},
35 | 'rules': {'required': False, 'attrs': ['pk', 'name', 'get_mode_type_display'], 'format': "{name}",
36 | 'many': True},
37 | 'parent': {'required': False, 'attrs': ['pk', 'name', 'parent_id']},
38 | }
39 |
40 | user_count = serializers.SerializerMethodField(read_only=True, label=_("User count"))
41 |
42 | def validate(self, attrs):
43 | # 权限需要其他接口设置,下面三个参数忽略
44 | attrs.pop('rules', None)
45 | attrs.pop('roles', None)
46 | attrs.pop('mode_type', None)
47 | # 上级部门必须存在,否则会出现数据权限问题
48 | parent = attrs.get('parent', self.instance.parent if self.instance else None)
49 | if not parent:
50 | attrs['parent'] = self.request.user.dept
51 | return attrs
52 |
53 | def update(self, instance, validated_data):
54 | parent = validated_data.get('parent')
55 | if parent and str(parent.pk) in DeptInfo.recursion_dept_info(dept_id=instance.pk):
56 | raise ValidationError(_("The superior department cannot be its own subordinate department"))
57 | return super().update(instance, validated_data)
58 |
59 | @extend_schema_field(serializers.IntegerField)
60 | def get_user_count(self, obj):
61 | return obj.userinfo_set.count()
62 |
--------------------------------------------------------------------------------
/system/serializers/field.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : field
5 | # author : ly_13
6 | # date : 8/10/2024
7 |
8 | from common.core.serializers import BaseModelSerializer
9 | from common.utils import get_logger
10 | from system.models import ModelLabelField
11 |
12 | logger = get_logger(__name__)
13 |
14 |
15 | class ModelLabelFieldSerializer(BaseModelSerializer):
16 | class Meta:
17 | model = ModelLabelField
18 | fields = ['pk', 'name', 'label', 'parent', 'field_type', 'created_time', 'updated_time']
19 | read_only_fields = [x.name for x in ModelLabelField._meta.fields]
20 | extra_kwargs = {'parent': {'attrs': ['pk', 'name', 'label'], 'read_only': True, 'format': '{label}({pk})'}}
21 |
22 |
23 | class ModelLabelFieldImportSerializer(BaseModelSerializer):
24 | class Meta:
25 | model = ModelLabelField
26 | fields = ['pk', 'name', 'label', 'parent', 'field_type', 'created_time', 'updated_time']
27 |
--------------------------------------------------------------------------------
/system/serializers/menu.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : menu
5 | # author : ly_13
6 | # date : 8/10/2024
7 |
8 | from django.db import transaction
9 | from django.utils.translation import gettext_lazy as _
10 | from rest_framework import serializers
11 |
12 | from common.core.serializers import BaseModelSerializer
13 | from common.utils import get_logger
14 | from system.models import Menu, MenuMeta
15 |
16 | logger = get_logger(__name__)
17 |
18 |
19 | class MenuMetaSerializer(BaseModelSerializer):
20 | class Meta:
21 | model = MenuMeta
22 | exclude = ['creator', 'modifier', 'id']
23 | read_only_fields = ['creator', 'modifier', 'dept_belong', 'id']
24 |
25 | pk = serializers.UUIDField(source='id', read_only=True)
26 |
27 |
28 | class MenuSerializer(BaseModelSerializer):
29 | meta = MenuMetaSerializer(label=_("Menu meta"))
30 |
31 | class Meta:
32 | model = Menu
33 | fields = [
34 | 'pk', 'name', 'rank', 'path', 'component', 'meta', 'parent', 'menu_type', 'is_active', 'model', 'method'
35 | ]
36 | # read_only_fields = ['pk'] # 用于文件导入导出时,不丢失上级节点
37 | extra_kwargs = {
38 | 'parent': {'attrs': ['pk', 'name'], 'allow_null': True, 'required': False},
39 | 'model': {'attrs': ['pk', 'name', 'label'], 'allow_null': True, 'required': False},
40 | }
41 |
42 | def update(self, instance, validated_data):
43 | with transaction.atomic():
44 | serializer = MenuMetaSerializer(instance.meta, data=validated_data.pop('meta'), partial=True,
45 | context=self.context)
46 | serializer.is_valid(raise_exception=True)
47 | serializer.save()
48 | return super().update(instance, validated_data)
49 |
50 | def create(self, validated_data):
51 | with transaction.atomic():
52 | serializer = MenuMetaSerializer(data=validated_data.pop('meta'), context=self.context)
53 | serializer.is_valid(raise_exception=True)
54 | validated_data['meta'] = serializer.save()
55 | return super().create(validated_data)
56 |
--------------------------------------------------------------------------------
/system/serializers/permission.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : permission
5 | # author : ly_13
6 | # date : 8/10/2024
7 |
8 | from django.db.models import Q
9 | from django.utils.translation import gettext_lazy as _
10 | from rest_framework.exceptions import ValidationError
11 |
12 | from common.core.serializers import BaseModelSerializer
13 | from common.utils import get_logger
14 | from system.models import DataPermission, Menu
15 |
16 | logger = get_logger(__name__)
17 |
18 |
19 | def get_menu_queryset():
20 | queryset = Menu.objects
21 | pks = queryset.filter(menu_type=Menu.MenuChoices.PERMISSION).values_list('parent', flat=True)
22 | return queryset.filter(Q(menu_type=Menu.MenuChoices.PERMISSION) | Q(id__in=pks)).order_by('rank')
23 |
24 |
25 | class DataPermissionSerializer(BaseModelSerializer):
26 | class Meta:
27 | model = DataPermission
28 | fields = ['pk', 'name', "is_active", "mode_type", "menu", "description", 'rules', "created_time"]
29 | table_fields = ['pk', 'name', "mode_type", "is_active", "description", "created_time"]
30 | extra_kwargs = {
31 | 'menu': {
32 | 'attrs': ['pk', 'name', 'parent_id', 'meta__title'],
33 | 'many': True, 'required': False, 'queryset': get_menu_queryset()
34 | },
35 | }
36 |
37 | def validate(self, attrs):
38 | rules = attrs.get('rules', [] if not self.instance else self.instance.rules)
39 | if not rules:
40 | raise ValidationError(_("The rule cannot be null"))
41 | if len(rules) < 2:
42 | attrs['mode_type'] = DataPermission.ModeChoices.OR
43 | return attrs
44 |
--------------------------------------------------------------------------------
/system/serializers/route.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : route
5 | # author : ly_13
6 | # date : 8/16/2024
7 |
8 | from django.utils.translation import gettext_lazy as _
9 | from rest_framework import serializers
10 | from rest_framework.serializers import ModelSerializer
11 |
12 | from common.core.serializers import BaseModelSerializer
13 | from common.utils import get_logger
14 | from system.models import Menu, MenuMeta
15 |
16 | logger = get_logger(__name__)
17 |
18 |
19 | class RouteMetaSerializer(ModelSerializer):
20 | class Meta:
21 | model = MenuMeta
22 | fields = [
23 | 'title', 'icon', 'showParent', 'showLink', 'extraIcon', 'keepAlive', 'frameSrc', 'frameLoading',
24 | 'transition', 'hiddenTag', 'dynamicLevel', 'fixedTag'
25 | ]
26 |
27 | showParent = serializers.BooleanField(source='is_show_parent', read_only=True, label=_("Show parent menu"))
28 | showLink = serializers.BooleanField(source='is_show_menu', read_only=True, label=_("Show menu"))
29 | extraIcon = serializers.CharField(source='r_svg_name', read_only=True, label=_("Right icon"))
30 | keepAlive = serializers.BooleanField(source='is_keepalive', read_only=True, label=_("Keepalive"))
31 | frameSrc = serializers.CharField(source='frame_url', read_only=True, label=_("Iframe URL"))
32 | frameLoading = serializers.BooleanField(source='frame_loading', read_only=True, label=_("Iframe loading"))
33 |
34 | transition = serializers.SerializerMethodField()
35 |
36 | def get_transition(self, obj):
37 | return {
38 | 'enterTransition': obj.transition_enter,
39 | 'leaveTransition': obj.transition_leave,
40 | }
41 |
42 | hiddenTag = serializers.BooleanField(source='is_hidden_tag', read_only=True, label=_("Hidden tag"))
43 | fixedTag = serializers.BooleanField(source='fixed_tag', read_only=True, label=_("Fixed tag"))
44 | dynamicLevel = serializers.IntegerField(source='dynamic_level', read_only=True, label=_("Dynamic level"))
45 |
46 |
47 | class RouteSerializer(BaseModelSerializer):
48 | class Meta:
49 | model = Menu
50 | fields = ['pk', 'name', 'rank', 'path', 'component', 'meta', 'parent']
51 | extra_kwargs = {
52 | 'rank': {'read_only': True},
53 | 'parent': {'attrs': ['pk', 'name'], 'allow_null': True, 'required': False},
54 | }
55 |
56 | meta = RouteMetaSerializer(label=_("Menu meta")) # 用于前端菜单渲染
57 |
--------------------------------------------------------------------------------
/system/serializers/upload.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : upload
5 | # author : ly_13
6 | # date : 8/10/2024
7 |
8 | from django.utils.translation import gettext_lazy as _
9 | from drf_spectacular.utils import extend_schema_field
10 | from rest_framework import serializers
11 | from rest_framework.exceptions import ValidationError
12 |
13 | from common.core.serializers import BaseModelSerializer
14 | from common.fields.utils import get_file_absolute_uri
15 | from common.utils import get_logger
16 | from system.models import UploadFile
17 |
18 | logger = get_logger(__name__)
19 |
20 |
21 | class UploadFileSerializer(BaseModelSerializer):
22 | class Meta:
23 | model = UploadFile
24 | fields = ['pk', 'filename', 'filesize', 'mime_type', 'md5sum', 'file_url', 'access_url', 'is_tmp', 'is_upload']
25 | read_only_fields = ["pk", "is_upload"]
26 | table_fields = ['pk', 'filename', 'filesize', 'mime_type', 'access_url', 'is_tmp', 'is_upload', 'md5sum']
27 |
28 | access_url = serializers.SerializerMethodField(label=_("Access URL"))
29 |
30 | @extend_schema_field(serializers.CharField)
31 | def get_access_url(self, obj):
32 | return obj.file_url if obj.file_url else get_file_absolute_uri(obj.filepath, self.context.get('request', None))
33 |
34 | def create(self, validated_data):
35 | if not validated_data.get('file_url'):
36 | raise ValidationError(_("Internet url cannot be null"))
37 | return super().create(validated_data)
38 |
39 | def update(self, instance, validated_data):
40 | if not validated_data.get('file_url') and not instance.is_upload:
41 | raise ValidationError('Internet url cannot be null')
42 | return super().update(instance, validated_data)
43 |
--------------------------------------------------------------------------------
/system/serializers/userinfo.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : userinfo
5 | # author : ly_13
6 | # date : 8/10/2024
7 |
8 |
9 | from django.utils.translation import gettext_lazy as _
10 | from drf_spectacular.utils import extend_schema_field
11 | from rest_framework import serializers
12 |
13 | from common.base.utils import AESCipherV2
14 | from common.core.serializers import BaseModelSerializer
15 | from common.utils import get_logger
16 | from settings.utils.password import check_password_rules
17 | from system import models
18 | from system.models import UserInfo
19 |
20 | logger = get_logger(__name__)
21 |
22 |
23 | class UserInfoSerializer(BaseModelSerializer):
24 | class Meta:
25 | model = UserInfo
26 | write_fields = ['username', 'nickname', 'gender']
27 | fields = write_fields + ['email', 'last_login', 'pk', 'phone', 'avatar', 'roles', 'date_joined', 'dept']
28 | read_only_fields = list(set([x.name for x in models.UserInfo._meta.fields]) - set(write_fields))
29 |
30 | dept = serializers.CharField(source='dept.name', read_only=True)
31 | roles = serializers.SerializerMethodField()
32 |
33 | @extend_schema_field(serializers.ListField)
34 | def get_roles(self, obj):
35 | return list(obj.roles.values_list('name', flat=True))
36 |
37 |
38 | class ChangePasswordSerializer(serializers.Serializer):
39 | old_password = serializers.CharField(
40 | min_length=5, max_length=128, required=True, write_only=True, label=_("Old password")
41 | )
42 | sure_password = serializers.CharField(
43 | min_length=5, max_length=128, required=True, write_only=True, label=_("Confirm password")
44 | )
45 |
46 | def update(self, instance, validated_data):
47 | sure_password = AESCipherV2(instance.username).decrypt(validated_data.get('sure_password'))
48 | old_password = AESCipherV2(instance.username).decrypt(validated_data.get('old_password'))
49 | if not instance.check_password(old_password):
50 | raise serializers.ValidationError(_("Old password verification failed"))
51 | if not check_password_rules(sure_password, instance.is_superuser):
52 | raise serializers.ValidationError(_('Password does not match security rules'))
53 |
54 | instance.set_password(sure_password)
55 | instance.modifier = self.context.get('request').user
56 | instance.save(update_fields=['password', 'modifier'])
57 | return instance
58 |
--------------------------------------------------------------------------------
/system/signal.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : signal
5 | # author : ly_13
6 | # date : 10/11/2024
7 |
8 |
9 | from django.dispatch import Signal
10 |
11 | invalid_user_cache_signal = Signal()
12 |
--------------------------------------------------------------------------------
/system/tasks.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin_server
4 | # filename : tasks
5 | # author : ly_13
6 | # date : 6/29/2023
7 |
8 | from celery import shared_task
9 |
10 | from common.celery.decorator import register_as_period_task
11 | from common.utils import get_logger
12 | from system.utils.ctasks import auto_clean_operation_log, auto_clean_black_token, auto_clean_tmp_file
13 |
14 | logger = get_logger(__name__)
15 |
16 |
17 | @shared_task
18 | @register_as_period_task(crontab='2 2 * * *')
19 | def auto_clean_operation_job():
20 | auto_clean_operation_log(clean_day=30 * 6)
21 |
22 |
23 | @shared_task
24 | @register_as_period_task(crontab='22 2 * * *')
25 | def auto_clean_black_token_job():
26 | auto_clean_black_token(clean_day=7)
27 |
28 |
29 | @shared_task
30 | @register_as_period_task(crontab='32 2 * * *')
31 | def auto_clean_tmp_file_job():
32 | auto_clean_tmp_file(clean_day=7)
33 |
--------------------------------------------------------------------------------
/system/templates/msg_verify_code.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 |
4 |
5 |
6 | {{ title }} |
7 |
8 |
9 | {% trans 'Hello' %} {{ username }} |
10 |
11 |
12 | {% trans 'Verify code' %}: {{ code }} |
13 |
14 |
15 |
16 | {% trans 'The validity period of the verification code is' %}{{ ttl }}{% trans 'seconds' %} |
17 |
18 |
19 | {% trans 'If you have any questions, you can contact the administrator' %} |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/system/templates/notify/msg_different_city.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 | {% trans 'Hello' %} {{ name }},
4 |
5 |
6 | {% trans 'Your account has remote login behavior, please pay attention' %}
7 |
8 |
9 | {% trans 'Username' %}: {{ username }}
10 | {% trans 'Login Date' %}: {{ time }}
11 | {% trans 'Login city' %}: {{ city }}({{ ip }})
12 |
13 |
14 | -
15 |
16 | {% trans 'If you suspect that the login behavior is abnormal, please modify the account password in time.' %}
17 |
--------------------------------------------------------------------------------
/system/templates/notify/msg_rest_password_success.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% trans 'Dear' %} {{ name }},
3 |
4 |
5 | {% trans 'Your password has just been successfully updated' %}
6 |
7 |
8 | {% trans 'Username' %}: {{ username }}
9 | {% trans 'IP' %}: {{ ip_address }}
10 | {% trans 'Browser' %}: {{ browser }}
11 |
12 | -
13 |
14 | {% trans 'If the password update was not initiated by you, your account may have security issues' %}
15 | {% trans 'If you have any questions, you can contact the administrator' %}
16 |
17 |
--------------------------------------------------------------------------------
/system/utils/ctasks.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin_server
4 | # filename : ctasks
5 | # author : ly_13
6 | # date : 6/29/2023
7 |
8 | import datetime
9 |
10 | from celery.utils.log import get_task_logger
11 | from django.utils import timezone
12 | from rest_framework_simplejwt.token_blacklist.models import OutstandingToken
13 |
14 | from system.models import OperationLog, UploadFile
15 |
16 | logger = get_task_logger(__name__)
17 |
18 |
19 | def auto_clean_operation_log(clean_day=30 * 6):
20 | return OperationLog.remove_expired(clean_day)
21 |
22 |
23 | def auto_clean_black_token(clean_day=1):
24 | clean_time = timezone.now() - datetime.timedelta(days=clean_day)
25 | deleted, _rows_count = OutstandingToken.objects.filter(expires_at__lte=clean_time).delete()
26 | logger.info(f"clean {_rows_count} black token {deleted}")
27 |
28 |
29 | def auto_clean_tmp_file(clean_day=1):
30 | clean_time = timezone.now() - datetime.timedelta(days=clean_day)
31 | _rows_count = 0
32 | for instance in UploadFile.objects.filter(created_time__lte=clean_time):
33 | if instance.delete():
34 | _rows_count += 1
35 | logger.info(f"clean {_rows_count} upload tmp file")
36 |
--------------------------------------------------------------------------------
/system/views/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : server
4 | # filename : __init__
5 | # author : ly_13
6 | # date : 6/6/2023
7 |
--------------------------------------------------------------------------------
/system/views/admin/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : __init__.py
5 | # author : ly_13
6 | # date : 3/4/2024
7 |
--------------------------------------------------------------------------------
/system/views/admin/dept.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : server
4 | # filename : dept
5 | # author : ly_13
6 | # date : 6/16/2023
7 | from django_filters import rest_framework as filters
8 |
9 | from common.core.filter import BaseFilterSet
10 | from common.core.modelset import BaseModelSet, ImportExportDataAction
11 | from common.core.pagination import DynamicPageNumber
12 | from common.utils import get_logger
13 | from system.models import DeptInfo
14 | from system.serializers.department import DeptSerializer
15 | from system.utils.modelset import ChangeRolePermissionAction
16 |
17 | logger = get_logger(__name__)
18 |
19 |
20 | class DeptFilter(BaseFilterSet):
21 | pk = filters.UUIDFilter(field_name='id')
22 | name = filters.CharFilter(field_name='name', lookup_expr='icontains')
23 |
24 | class Meta:
25 | model = DeptInfo
26 | fields = ['pk', 'is_active', 'code', 'mode_type', 'auto_bind', 'name', 'description']
27 |
28 |
29 | class DeptViewSet(BaseModelSet, ChangeRolePermissionAction, ImportExportDataAction):
30 | """部门"""
31 | queryset = DeptInfo.objects.all()
32 | serializer_class = DeptSerializer
33 | pagination_class = DynamicPageNumber(1000)
34 | ordering_fields = ['created_time', 'rank']
35 | filterset_class = DeptFilter
36 |
--------------------------------------------------------------------------------
/system/views/admin/loginlog.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : loginlog
5 | # author : ly_13
6 | # date : 1/3/2024
7 |
8 |
9 | from django_filters import rest_framework as filters
10 | from drf_spectacular.utils import extend_schema
11 | from rest_framework.decorators import action
12 |
13 | from common.core.filter import BaseFilterSet, PkMultipleFilter
14 | from common.core.modelset import ListDeleteModelSet, OnlyExportDataAction
15 | from common.core.response import ApiResponse
16 | from common.swagger.utils import get_default_response_schema
17 | from message.utils import send_logout_msg
18 | from system.models import UserLoginLog
19 | from system.serializers.log import LoginLogSerializer
20 |
21 |
22 | class LoginLogFilter(BaseFilterSet):
23 | ipaddress = filters.CharFilter(field_name='ipaddress', lookup_expr='icontains')
24 | city = filters.CharFilter(field_name='city', lookup_expr='icontains')
25 | system = filters.CharFilter(field_name='system', lookup_expr='icontains')
26 | agent = filters.CharFilter(field_name='agent', lookup_expr='icontains')
27 | creator_id = PkMultipleFilter(input_type='api-search-user')
28 |
29 | class Meta:
30 | model = UserLoginLog
31 | fields = ['login_type', 'ipaddress', 'city', 'system', 'creator_id', 'status', 'agent', 'created_time']
32 |
33 |
34 | class LoginLogViewSet(ListDeleteModelSet, OnlyExportDataAction):
35 | """登录日志"""
36 | queryset = UserLoginLog.objects.all()
37 | serializer_class = LoginLogSerializer
38 |
39 | ordering_fields = ['created_time']
40 | filterset_class = LoginLogFilter
41 |
42 | @extend_schema(responses=get_default_response_schema(), request=None)
43 | @action(methods=["post"], detail=True)
44 | def logout(self, request, *args, **kwargs):
45 | """强退用户"""
46 | instance = self.get_object()
47 | send_logout_msg(instance.creator.pk, [instance.channel_name])
48 | return ApiResponse()
49 |
--------------------------------------------------------------------------------
/system/views/admin/online.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : websocket
5 | # author : ly_13
6 | # date : 3/27/2025
7 |
8 | from common.core.filter import BaseFilterSet, PkMultipleFilter
9 | from common.core.modelset import ListDeleteModelSet, OnlyExportDataAction
10 | from common.core.pagination import DynamicPageNumber
11 | from message.utils import get_online_info, send_logout_msg
12 | from system.models import UserLoginLog
13 | from system.serializers.log import UserOnlineSerializer
14 |
15 |
16 | class UserOnlineFilter(BaseFilterSet):
17 | creator_id = PkMultipleFilter(input_type='api-search-user')
18 |
19 | class Meta:
20 | model = UserLoginLog
21 | fields = ['creator_id', 'channel_name']
22 |
23 |
24 | class UserOnlineViewSet(ListDeleteModelSet, OnlyExportDataAction):
25 | """websocket在线日志"""
26 | queryset = UserLoginLog.objects.filter(login_type=UserLoginLog.LoginTypeChoices.WEBSOCKET).all()
27 | serializer_class = UserOnlineSerializer
28 | pagination_class = DynamicPageNumber(1000)
29 | filterset_class = UserOnlineFilter
30 | ordering_fields = ['created_time']
31 |
32 | def get_queryset(self):
33 | online_user_pks, online_user_sockets = get_online_info()
34 | return self.queryset.filter(creator_id__in=online_user_pks, channel_name__in=online_user_sockets)
35 |
36 | def perform_destroy(self, instance):
37 | send_logout_msg(instance.creator_id, [instance.channel_name])
38 | return True
39 |
--------------------------------------------------------------------------------
/system/views/admin/operationlog.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin_server
4 | # filename : operationlog
5 | # author : ly_13
6 | # date : 6/27/2023
7 |
8 | from django.utils.translation import gettext_lazy as _
9 | from django_filters import rest_framework as filters
10 |
11 | from common.core.filter import BaseFilterSet, PkMultipleFilter
12 | from common.core.modelset import ListDeleteModelSet, OnlyExportDataAction
13 | from system.models import OperationLog
14 | from system.serializers.log import OperationLogSerializer
15 |
16 |
17 | class OperationLogFilter(BaseFilterSet):
18 | ipaddress = filters.CharFilter(field_name='ipaddress', lookup_expr='icontains')
19 | system = filters.CharFilter(field_name='system', lookup_expr='icontains')
20 | path = filters.CharFilter(field_name='path', lookup_expr='icontains')
21 | error_status = filters.BooleanFilter(method='get_error_status', label=_('Error status'))
22 |
23 | def get_error_status(self, queryset, name, value):
24 | if value is True:
25 | return queryset.exclude(status_code=1000)
26 | return queryset.filter(status_code=1000)
27 |
28 | # 自定义的搜索模板,需要前端同时添加 userinfo 类型
29 | creator_id = PkMultipleFilter(input_type='api-search-user')
30 |
31 | class Meta:
32 | model = OperationLog
33 | fields = ['request_uuid', 'module', 'ipaddress', 'system', 'creator_id', 'status_code', 'path', 'created_time',
34 | 'error_status']
35 |
36 |
37 | class OperationLogViewSet(ListDeleteModelSet, OnlyExportDataAction):
38 | """操作日志"""
39 | queryset = OperationLog.objects.all()
40 | serializer_class = OperationLogSerializer
41 |
42 | ordering_fields = ['created_time', 'updated_time', 'exec_time']
43 | filterset_class = OperationLogFilter
44 |
--------------------------------------------------------------------------------
/system/views/admin/permission.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : server
4 | # filename : permission
5 | # author : ly_13
6 | # date : 6/16/2023
7 |
8 | from django_filters import rest_framework as filters
9 |
10 | from common.core.filter import BaseFilterSet
11 | from common.core.modelset import BaseModelSet, ImportExportDataAction
12 | from common.utils import get_logger
13 | from system.models import DataPermission
14 | from system.serializers.permission import DataPermissionSerializer
15 |
16 | logger = get_logger(__name__)
17 |
18 |
19 | class DataPermissionFilter(BaseFilterSet):
20 | pk = filters.UUIDFilter(field_name='id')
21 | name = filters.CharFilter(field_name='name', lookup_expr='icontains')
22 |
23 | class Meta:
24 | model = DataPermission
25 | fields = ['pk', 'name', 'mode_type', 'is_active', 'description']
26 |
27 |
28 | class DataPermissionViewSet(BaseModelSet, ImportExportDataAction):
29 | """数据权限"""
30 | queryset = DataPermission.objects.all()
31 | serializer_class = DataPermissionSerializer
32 | ordering_fields = ['created_time']
33 | filterset_class = DataPermissionFilter
34 |
--------------------------------------------------------------------------------
/system/views/admin/role.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : server
4 | # filename : role
5 | # author : ly_13
6 | # date : 6/19/2023
7 |
8 | from django_filters import rest_framework as filters
9 |
10 | from common.core.filter import BaseFilterSet
11 | from common.core.modelset import BaseModelSet, ImportExportDataAction
12 | from common.utils import get_logger
13 | from system.models import UserRole
14 | from system.serializers.role import RoleSerializer, ListRoleSerializer
15 |
16 | logger = get_logger(__name__)
17 |
18 |
19 | class RoleFilter(BaseFilterSet):
20 | name = filters.CharFilter(field_name='name', lookup_expr='icontains')
21 | code = filters.CharFilter(field_name='code', lookup_expr='icontains')
22 |
23 | class Meta:
24 | model = UserRole
25 | fields = ['name', 'code', 'is_active', 'description']
26 |
27 |
28 | class RoleViewSet(BaseModelSet, ImportExportDataAction):
29 | """角色"""
30 | queryset = UserRole.objects.all()
31 | serializer_class = RoleSerializer
32 | list_serializer_class = ListRoleSerializer
33 | ordering_fields = ['updated_time', 'name', 'created_time']
34 | filterset_class = RoleFilter
35 |
--------------------------------------------------------------------------------
/system/views/auth/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : __init__.py
5 | # author : ly_13
6 | # date : 8/10/2024
7 |
--------------------------------------------------------------------------------
/system/views/auth/logout.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : logout
5 | # author : ly_13
6 | # date : 8/8/2024
7 | import hashlib
8 | import time
9 |
10 | from django.contrib.auth import logout
11 | from drf_spectacular.plumbing import build_object_type, build_basic_type
12 | from drf_spectacular.types import OpenApiTypes
13 | from drf_spectacular.utils import extend_schema, OpenApiRequest
14 | from rest_framework.generics import GenericAPIView
15 | from rest_framework_simplejwt.tokens import RefreshToken
16 |
17 | from common.cache.storage import BlackAccessTokenCache
18 | from common.core.response import ApiResponse
19 | from common.swagger.utils import get_default_response_schema
20 |
21 |
22 | class LogoutAPIView(GenericAPIView):
23 | """用户登出"""
24 |
25 | @extend_schema(
26 | request=OpenApiRequest(build_object_type(properties={'refresh': build_basic_type(OpenApiTypes.STR)})),
27 | responses=get_default_response_schema()
28 | )
29 | def post(self, request):
30 | """用户登出"""
31 | auth = request.auth
32 | if not auth:
33 | return ApiResponse()
34 | exp = auth.payload.get('exp')
35 | user_id = auth.payload.get('user_id')
36 | timeout = exp - time.time()
37 | BlackAccessTokenCache(user_id, hashlib.md5(auth.token).hexdigest()).set_storage_cache(1, timeout)
38 | if request.data.get('refresh'):
39 | try:
40 | token = RefreshToken(request.data.get('refresh'))
41 | token.blacklist() # 登出账户,并且将账户的access 和 refresh token 加入黑名单
42 | except Exception as e:
43 | pass
44 | logout(request)
45 | return ApiResponse()
46 |
--------------------------------------------------------------------------------
/system/views/auth/rule.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : rule
5 | # author : ly_13
6 | # date : 8/10/2024
7 | from drf_spectacular.plumbing import build_object_type, build_basic_type, build_array_type
8 | from drf_spectacular.types import OpenApiTypes
9 | from drf_spectacular.utils import extend_schema
10 | from rest_framework.generics import GenericAPIView
11 |
12 | from common.core.response import ApiResponse
13 | from common.swagger.utils import get_default_response_schema
14 | from settings.utils.password import get_password_check_rules
15 |
16 |
17 | class PasswordRulesAPIView(GenericAPIView):
18 | """密码规则配置信息"""
19 | permission_classes = []
20 |
21 | @extend_schema(
22 | responses=get_default_response_schema(
23 | {
24 | 'data': build_object_type(
25 | properties={
26 | 'password_rules': build_array_type(
27 | build_object_type(
28 | properties={
29 | 'key': build_basic_type(OpenApiTypes.STR),
30 | 'value': build_basic_type(OpenApiTypes.NUMBER),
31 | }
32 | )
33 | )
34 | }
35 | )
36 | }
37 | )
38 | )
39 | def get(self, request):
40 | """获取密码规则配置"""
41 | return ApiResponse(data={"password_rules": get_password_check_rules(request.user)})
42 |
--------------------------------------------------------------------------------
/system/views/auth/token.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : token
5 | # author : ly_13
6 | # date : 8/10/2024
7 | from drf_spectacular.plumbing import build_basic_type
8 | from drf_spectacular.types import OpenApiTypes
9 | from drf_spectacular.utils import extend_schema
10 | from rest_framework.generics import GenericAPIView
11 | from rest_framework_simplejwt.views import TokenRefreshView
12 |
13 | from captcha.utils import CaptchaAuth
14 | from common.core.response import ApiResponse
15 | from common.swagger.utils import get_default_response_schema
16 | from common.utils.request import get_request_ident
17 | from common.utils.token import make_token_cache
18 | from system.utils.auth import get_token_lifetime
19 |
20 |
21 | class TempTokenAPIView(GenericAPIView):
22 | """临时Token"""
23 | permission_classes = []
24 | authentication_classes = []
25 |
26 | @extend_schema(responses=get_default_response_schema({'token': build_basic_type(OpenApiTypes.STR)}))
27 | def get(self, request):
28 | """获取{cls}"""
29 | token = make_token_cache(get_request_ident(request), time_limit=600, force_new=True).encode('utf-8')
30 | return ApiResponse(token=token)
31 |
32 |
33 | class CaptchaAPIView(GenericAPIView):
34 | """图片验证码"""
35 | permission_classes = []
36 | authentication_classes = []
37 |
38 | @extend_schema(
39 | responses=get_default_response_schema(
40 | {
41 | 'captcha_image': build_basic_type(OpenApiTypes.STR),
42 | 'captcha_key': build_basic_type(OpenApiTypes.STR),
43 | 'length': build_basic_type(OpenApiTypes.NUMBER)
44 | }
45 | )
46 | )
47 | def get(self, request):
48 | """获取{cls}"""
49 | return ApiResponse(**CaptchaAuth(request=request).generate())
50 |
51 |
52 | class RefreshTokenAPIView(TokenRefreshView):
53 | """刷新Token"""
54 |
55 | def post(self, request, *args, **kwargs):
56 | data = super().post(request, *args, **kwargs).data
57 | data.update(get_token_lifetime(request.user))
58 | return ApiResponse(data=data)
59 |
--------------------------------------------------------------------------------
/system/views/routes.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : routes
5 | # author : ly_13
6 | # date : 4/21/2024
7 | from drf_spectacular.utils import extend_schema
8 | from rest_framework.generics import GenericAPIView
9 |
10 | from common.base.magic import cache_response
11 | from common.base.utils import menu_list_to_tree, format_menu_data
12 | from common.core.modelset import CacheDetailResponseMixin
13 | from common.core.permission import get_user_menu_queryset
14 | from common.core.response import ApiResponse
15 | from system.models import Menu
16 | from system.serializers.route import RouteSerializer
17 |
18 |
19 | def get_auths(user):
20 | if user.is_superuser:
21 | menu_obj = Menu.objects.filter(is_active=True)
22 | else:
23 | menu_obj = get_user_menu_queryset(user)
24 | if not menu_obj:
25 | menu_obj = Menu.objects.none()
26 | return menu_obj.filter(menu_type=Menu.MenuChoices.PERMISSION).values_list('name', flat=True).distinct()
27 |
28 |
29 | class UserRoutesAPIView(GenericAPIView, CacheDetailResponseMixin):
30 | """获取菜单路由"""
31 |
32 | @extend_schema(exclude=True)
33 | @cache_response(timeout=3600 * 24, key_func='get_cache_key')
34 | def get(self, request):
35 | route_list = []
36 | user_obj = request.user
37 | menu_type = [Menu.MenuChoices.DIRECTORY, Menu.MenuChoices.MENU]
38 | if user_obj.is_superuser:
39 | route_list = RouteSerializer(Menu.objects.filter(is_active=True, menu_type__in=menu_type).order_by('rank'),
40 | many=True, ignore_field_permission=True).data
41 |
42 | return ApiResponse(data=format_menu_data(menu_list_to_tree(route_list)), auths=get_auths(user_obj))
43 | else:
44 | menu_queryset = get_user_menu_queryset(user_obj)
45 | if menu_queryset:
46 | route_list = RouteSerializer(
47 | menu_queryset.filter(menu_type__in=menu_type).distinct().order_by('rank'), many=True,
48 | ignore_field_permission=True).data
49 |
50 | return ApiResponse(data=format_menu_data(menu_list_to_tree(route_list)), auths=get_auths(user_obj))
51 |
--------------------------------------------------------------------------------
/system/views/search/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : __init__
5 | # author : ly_13
6 | # date : 7/22/2024
7 |
--------------------------------------------------------------------------------
/system/views/search/dept.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : dept
5 | # author : ly_13
6 | # date : 7/22/2024
7 |
8 | from django_filters import rest_framework as filters
9 |
10 | from common.core.filter import BaseFilterSet
11 | from common.core.modelset import OnlyListModelSet
12 | from common.core.pagination import DynamicPageNumber
13 | from common.utils import get_logger
14 | from system.models import DeptInfo
15 | from system.serializers.department import DeptSerializer
16 |
17 | logger = get_logger(__name__)
18 |
19 |
20 | class SearchDeptFilter(BaseFilterSet):
21 | pk = filters.UUIDFilter(field_name='id')
22 | name = filters.CharFilter(field_name='name', lookup_expr='icontains')
23 |
24 | class Meta:
25 | model = DeptInfo
26 | fields = ['name', 'is_active', 'code', 'description']
27 |
28 |
29 | class SearchDeptSerializer(DeptSerializer):
30 | class Meta:
31 | model = DeptInfo
32 | fields = ['name', 'pk', 'code', 'parent', 'is_active', 'user_count', 'auto_bind', 'description', 'created_time']
33 | table_fields = ['name', 'code', 'is_active', 'user_count', 'auto_bind', 'description', 'created_time', 'pk']
34 | read_only_fields = [x.name for x in DeptInfo._meta.fields]
35 |
36 |
37 | class SearchDeptViewSet(OnlyListModelSet):
38 | """部门搜索"""
39 | queryset = DeptInfo.objects.all()
40 | serializer_class = SearchDeptSerializer
41 | pagination_class = DynamicPageNumber(1000)
42 | ordering_fields = ['created_time', 'rank']
43 | filterset_class = SearchDeptFilter
44 |
--------------------------------------------------------------------------------
/system/views/search/menu.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : menu
5 | # author : ly_13
6 | # date : 7/22/2024
7 |
8 | from django.utils.translation import gettext_lazy as _
9 | from django_filters import rest_framework as filters
10 | from rest_framework import serializers
11 |
12 | from common.core.filter import BaseFilterSet
13 | from common.core.modelset import OnlyListModelSet
14 | from common.core.pagination import DynamicPageNumber
15 | from system.models import Menu
16 | from system.serializers.menu import MenuSerializer
17 |
18 |
19 | class SearchMenuFilter(BaseFilterSet):
20 | component = filters.CharFilter(field_name='component', lookup_expr='icontains')
21 | title = filters.CharFilter(field_name='meta__title', lookup_expr='icontains')
22 | path = filters.CharFilter(field_name='path', lookup_expr='icontains')
23 |
24 | class Meta:
25 | model = Menu
26 | fields = ['title', 'path', 'component']
27 |
28 |
29 | class SearchMenuSerializer(MenuSerializer):
30 | class Meta:
31 | model = Menu
32 | fields = ['title', 'pk', 'rank', 'path', 'component', 'parent', 'menu_type', 'is_active', 'method']
33 | table_fields = ['title', 'menu_type', 'path', 'component', 'is_active', 'method']
34 | read_only_fields = [x.name for x in Menu._meta.fields]
35 |
36 | title = serializers.CharField(source='meta.title', read_only=True, label=_("Menu title"))
37 |
38 |
39 | class SearchMenuViewSet(OnlyListModelSet):
40 | """菜单搜索"""
41 | queryset = Menu.objects.order_by('rank').all()
42 | serializer_class = SearchMenuSerializer
43 | pagination_class = DynamicPageNumber(1000)
44 | ordering_fields = ['-rank', 'updated_time', 'created_time']
45 | filterset_class = SearchMenuFilter
46 |
--------------------------------------------------------------------------------
/system/views/search/role.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : role
5 | # author : ly_13
6 | # date : 7/22/2024
7 |
8 | from django_filters import rest_framework as filters
9 |
10 | from common.core.filter import BaseFilterSet
11 | from common.core.modelset import OnlyListModelSet
12 | from common.utils import get_logger
13 | from system.models import UserRole
14 | from system.serializers.role import RoleSerializer
15 |
16 | logger = get_logger(__name__)
17 |
18 |
19 | class SearchRoleFilter(BaseFilterSet):
20 | name = filters.CharFilter(field_name='name', lookup_expr='icontains')
21 | code = filters.CharFilter(field_name='code', lookup_expr='icontains')
22 |
23 | class Meta:
24 | model = UserRole
25 | fields = ['name', 'code', 'is_active', 'description']
26 |
27 |
28 | class SearchRoleSerializer(RoleSerializer):
29 | class Meta:
30 | model = UserRole
31 | fields = ['pk', 'name', 'code', 'is_active', 'description', 'updated_time']
32 | read_only_fields = [x.name for x in UserRole._meta.fields]
33 |
34 |
35 | class SearchRoleViewSet(OnlyListModelSet):
36 | """角色搜索"""
37 | queryset = UserRole.objects.all()
38 | serializer_class = SearchRoleSerializer
39 | ordering_fields = ['updated_time', 'name', 'created_time']
40 | filterset_class = SearchRoleFilter
41 |
--------------------------------------------------------------------------------
/system/views/search/user.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : user
5 | # author : ly_13
6 | # date : 7/22/2024
7 |
8 | from django_filters import rest_framework as filters
9 |
10 | from common.core.filter import BaseFilterSet
11 | from common.core.modelset import OnlyListModelSet
12 | from system.models import UserInfo
13 | from system.serializers.user import UserSerializer
14 |
15 |
16 | class SearchUserFilter(BaseFilterSet):
17 | username = filters.CharFilter(field_name='username', lookup_expr='icontains')
18 | nickname = filters.CharFilter(field_name='nickname', lookup_expr='icontains')
19 | phone = filters.CharFilter(field_name='phone', lookup_expr='icontains')
20 |
21 | class Meta:
22 | model = UserInfo
23 | fields = ['username', 'nickname', 'phone', 'email', 'is_active', 'gender', 'dept']
24 |
25 |
26 | class SearchUserSerializer(UserSerializer):
27 | class Meta:
28 | model = UserInfo
29 | fields = ['pk', 'avatar', 'username', 'nickname', 'phone', 'email', 'gender', 'is_active', 'password', 'dept',
30 | 'description', 'last_login', 'date_joined']
31 |
32 | read_only_fields = [x.name for x in UserInfo._meta.fields]
33 |
34 | table_fields = ['pk', 'avatar', 'username', 'nickname', 'gender', 'is_active', 'dept', 'phone',
35 | 'last_login', 'date_joined']
36 |
37 |
38 | class SearchUserViewSet(OnlyListModelSet):
39 | """用户搜索"""
40 | queryset = UserInfo.objects.all()
41 | serializer_class = SearchUserSerializer
42 |
43 | ordering_fields = ['date_joined', 'last_login', 'created_time']
44 | filterset_class = SearchUserFilter
45 |
--------------------------------------------------------------------------------
/system/views/user/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : __init__.py
5 | # author : ly_13
6 | # date : 3/4/2024
7 |
--------------------------------------------------------------------------------
/system/views/user/login_log.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : login_log
5 | # author : ly_13
6 | # date : 8/11/2024
7 | from rest_framework.mixins import ListModelMixin
8 | from rest_framework.viewsets import GenericViewSet
9 |
10 | from common.core.modelset import SearchColumnsAction
11 | from common.core.response import ApiResponse
12 | from system.models import UserLoginLog
13 | from system.serializers.log import UserLoginLogSerializer
14 |
15 |
16 | class UserLoginLogViewSet(ListModelMixin, SearchColumnsAction, GenericViewSet):
17 | """用户登录日志"""
18 | queryset = UserLoginLog.objects.all()
19 | serializer_class = UserLoginLogSerializer
20 |
21 | ordering_fields = ['created_time']
22 |
23 | def get_queryset(self):
24 | return self.queryset.filter(creator=self.request.user)
25 |
26 | def list(self, request, *args, **kwargs):
27 | data = super().list(request, *args, **kwargs).data
28 | return ApiResponse(data=data)
29 |
--------------------------------------------------------------------------------
/tmp/.gitkeep:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/utils/check_celery.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 |
4 | set -e
5 |
6 | test -e /tmp/worker_ready_celery
7 | test -e /tmp/worker_heartbeat_celery && test $(($(date +%s) - $(stat -c %Y /tmp/worker_heartbeat_celery))) -lt 20
8 |
--------------------------------------------------------------------------------
/utils/clean_migrations.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 |
4 | utils_dir=$(dirname "$(readlink -f "$0")")
5 | cd "$(dirname "${utils_dir}")" || exit 1
6 |
7 | for d in *;do
8 | if [ -d "$d" ] && [ -d "$d"/migrations ];then
9 | rm -f "$d"/migrations/00*
10 | fi
11 | done
12 |
13 |
--------------------------------------------------------------------------------
/utils/init_data.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # project : xadmin-server
4 | # filename : tests
5 | # author : ly_13
6 | # date : 12/23/2023
7 | import os
8 | import sys
9 |
10 | import django
11 |
12 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
13 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings')
14 | django.setup()
15 |
16 | from system.models import *
17 | from django.core import management
18 |
19 | # 如果有用户存在,则不支持初始化操作
20 | try:
21 | if UserInfo.objects.exists():
22 | print(f'User already exists')
23 | exit(-1)
24 | except Exception as e:
25 | print(e)
26 | pass
27 |
28 | # 初始化操作
29 | try:
30 | management.call_command('makemigrations', )
31 | management.call_command('migrate', )
32 | # management.call_command('collectstatic', )
33 | management.call_command('compilemessages', )
34 | management.call_command('download_ip_db', )
35 | except Exception as e:
36 | print(f'Perform migrate failed, {e} exit')
37 |
38 | # 创建是默认管理员用户,请及时修改信息
39 | UserInfo.objects.create_superuser('xadmin', 'xadmin@dvcloud.xin', 'xAdminPwd!')
40 |
41 | management.call_command('load_init_json', )
42 |
43 | # 加载默认用户数据,一般部署新服的时候,如果有默认数据,则可以进行加载
44 | # management.call_command('loaddata', 'loadjson/userinfo.json')
45 |
--------------------------------------------------------------------------------
/utils/install_centos_docker.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | if which docker &>/dev/null ;then
4 | yum remove -y docker \
5 | docker-client \
6 | docker-client-latest \
7 | docker-common \
8 | docker-latest \
9 | docker-latest-logrotate \
10 | docker-logrotate \
11 | docker-engine
12 | fi
13 | if ! which docker &>/dev/null ;then
14 | yum install -y yum-utils \
15 | && yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo \
16 | && sed -i 's+https://download.docker.com+https://mirrors.tuna.tsinghua.edu.cn/docker-ce+' /etc/yum.repos.d/docker-ce.repo \
17 | && yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin \
18 | && systemctl restart docker
19 | fi
20 |
--------------------------------------------------------------------------------
/utils/nginx.conf:
--------------------------------------------------------------------------------
1 |
2 | user nginx;
3 | worker_processes auto;
4 |
5 | error_log /var/log/nginx/error.log notice;
6 | pid /var/run/nginx.pid;
7 |
8 | events {
9 | worker_connections 1024;
10 | }
11 |
12 | stream {
13 | log_format proxy '$remote_addr [$time_local] '
14 | '$protocol $status $bytes_sent $bytes_received '
15 | '$session_time "$upstream_addr" '
16 | '"$upstream_bytes_sent" "$upstream_bytes_received" "$upstream_connect_time"';
17 |
18 | access_log /var/log/nginx/tcp-access.log proxy;
19 |
20 | open_log_file_cache off;
21 |
22 | upstream server {
23 | hash $remote_addr consistent;
24 | server server:8896 weight=5 max_fails=3 fail_timeout=30s;
25 | }
26 | server {
27 | listen 8896;
28 | proxy_pass server;
29 | }
30 |
31 | }
32 |
33 |
--------------------------------------------------------------------------------