mirror of
https://github.com/jumpserver/jumpserver.git
synced 2026-07-02 07:01:30 +00:00
Merge branches 'osm' and 'osm' of github.com:jumpserver/jumpserver into osm
This commit is contained in:
@@ -48,7 +48,7 @@
|
||||
var time = '{{ interval }}'
|
||||
var error = '{{ error }}'
|
||||
var auto_redirect = '{{ auto_redirect }}'
|
||||
|
||||
|
||||
if (error) {
|
||||
message = error
|
||||
} else {
|
||||
@@ -62,7 +62,7 @@
|
||||
time--;
|
||||
setTimeout(redirect_page, 1000);
|
||||
} else {
|
||||
window.location.href = "{{ redirect_url }}";
|
||||
window.location.href = '{{ redirect_url|escapejs }}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,4 +71,3 @@
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -10,16 +10,16 @@ from django.views import View
|
||||
from rest_framework.exceptions import APIException
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
|
||||
from authentication.decorators import post_save_next_to_session_if_guard_redirect, pre_save_next_to_session
|
||||
from authentication import errors
|
||||
from authentication.const import ConfirmType
|
||||
from authentication.decorators import post_save_next_to_session_if_guard_redirect, pre_save_next_to_session
|
||||
from authentication.mixins import AuthMixin
|
||||
from authentication.notifications import OAuthBindMessage
|
||||
from authentication.permissions import UserConfirmation
|
||||
from common.sdk.im.dingtalk import URL, DingTalk
|
||||
from common.utils import get_logger
|
||||
from common.utils.common import get_request_ip
|
||||
from common.utils.django import get_object_or_none, reverse, safe_next_url
|
||||
from common.utils.django import get_object_or_none, reverse
|
||||
from common.utils.random import random_string
|
||||
from common.views.mixins import PermissionsMixin, UserConfirmRequiredExceptionMixin
|
||||
from users.models import User
|
||||
@@ -237,7 +237,7 @@ class DingTalkOAuthLoginCallbackView(AuthMixin, DingTalkOAuthMixin, View):
|
||||
appsecret=settings.DINGTALK_APPSECRET,
|
||||
agentid=settings.DINGTALK_AGENTID
|
||||
)
|
||||
userid, __ = dingtalk.get_user_id_by_code(code)
|
||||
userid, __ = dingtalk.get_user_id_by_code_for_oauth(code)
|
||||
if not userid:
|
||||
# 正常流程不会出这个错误,hack 行为
|
||||
msg = _('Failed to get user from DingTalk')
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import time
|
||||
|
||||
@@ -16,7 +17,7 @@ def sign(secret, data):
|
||||
digest = hmac.HMAC(
|
||||
key=secret.encode('utf8'),
|
||||
msg=data.encode('utf8'),
|
||||
digestmod=hmac._hashlib.sha256
|
||||
digestmod=hashlib.sha256
|
||||
).digest()
|
||||
signature = base64.standard_b64encode(digest).decode('utf8')
|
||||
# signature = urllib.parse.quote(signature, safe='')
|
||||
@@ -33,6 +34,7 @@ class URL:
|
||||
OAUTH_CONNECT = 'https://oapi.dingtalk.com/connect/oauth2/sns_authorize'
|
||||
GET_USER_ACCESSTOKEN = 'https://api.dingtalk.com/v1.0/oauth2/userAccessToken'
|
||||
GET_USER_INFO = 'https://api.dingtalk.com/v1.0/contact/users/me'
|
||||
GET_USERINFO_BYCODE = "https://oapi.dingtalk.com/sns/getuserinfo_bycode"
|
||||
GET_TOKEN = 'https://oapi.dingtalk.com/gettoken'
|
||||
SEND_MESSAGE_BY_TEMPLATE = 'https://oapi.dingtalk.com/topapi/message/corpconversation/sendbytemplate'
|
||||
SEND_MESSAGE = 'https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2'
|
||||
@@ -145,6 +147,14 @@ class DingTalk:
|
||||
headers={'x-acs-dingtalk-access-token': token}, check_errcode_is_0=False)
|
||||
return user
|
||||
|
||||
def get_user_id_by_code_for_oauth(self, code):
|
||||
# https://open.dingtalk.com/document/orgapp/use-a-dingtalk-account-to-log-on-to-a-third-party
|
||||
user = self._request.post(URL.GET_USERINFO_BYCODE, json={"tmp_auth_code": code},
|
||||
check_errcode_is_0=False, with_sign=True)
|
||||
unionid = user["user_info"]['unionid']
|
||||
userid = self.get_userid_by_unionid(unionid)
|
||||
return userid, None
|
||||
|
||||
def get_user_id_by_code(self, code):
|
||||
user_info = self.get_userinfo_bycode(code)
|
||||
unionid = user_info['unionId']
|
||||
|
||||
@@ -2,30 +2,24 @@
|
||||
#
|
||||
import datetime
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.templatetags.static import static
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from jumpserver.vendor import get_vendor_value, is_default_vendor
|
||||
|
||||
default_interface = dict((
|
||||
('logo_logout', get_vendor_value('logo_logout')),
|
||||
('logo_index', get_vendor_value('logo_index')),
|
||||
('login_image', get_vendor_value('login_image')),
|
||||
('favicon', get_vendor_value('favicon')),
|
||||
('login_title', get_vendor_value('login_title', default=_('JumpServer - An open-source PAM'))),
|
||||
('theme', get_vendor_value('theme', default='classic_green')),
|
||||
('logo_logout', static('img/logo.png')),
|
||||
('logo_index', static('img/logo_text_white.png')),
|
||||
('login_image', static('img/login_image.png')),
|
||||
('logo_white', static('img/logo_white.png')),
|
||||
('logo_text_white', static('img/logo_text_white.png')),
|
||||
('favicon', static('img/facio.ico')),
|
||||
('login_title', _('JumpServer - An open-source PAM')),
|
||||
('theme', 'classic_green'),
|
||||
('theme_info', {}),
|
||||
('footer_content', get_vendor_value('footer_content', default='')),
|
||||
('ext', get_vendor_value('interface_ext_defaults', default={})),
|
||||
('vendor', settings.VENDOR),
|
||||
('footer_content', ''),
|
||||
('version', os.environ.get("CURRENT_VERSION", ""))
|
||||
))
|
||||
|
||||
if not is_default_vendor():
|
||||
default_interface['theme_info'] = get_vendor_value('theme_info', default={})
|
||||
|
||||
current_year = datetime.datetime.now().year
|
||||
|
||||
default_context = {
|
||||
@@ -49,4 +43,4 @@ def jumpserver_processor(request):
|
||||
'FORCE_SCRIPT_NAME': settings.FORCE_SCRIPT_NAME,
|
||||
'SECURITY_VIEW_AUTH_NEED_MFA': settings.SECURITY_VIEW_AUTH_NEED_MFA,
|
||||
})
|
||||
return context
|
||||
return context
|
||||
@@ -280,9 +280,6 @@ SUGGESTION_LIMIT = CONFIG.SUGGESTION_LIMIT
|
||||
MCP_ENABLED = CONFIG.MCP_ENABLED
|
||||
|
||||
VENDOR = CONFIG.VENDOR
|
||||
VENDOR_TEMPLATES_DIR = Path(STATIC_DIR) / VENDOR.lower()
|
||||
if Path(VENDOR_TEMPLATES_DIR).is_dir():
|
||||
TEMPLATES[0]['DIRS'].insert(0, VENDOR_TEMPLATES_DIR)
|
||||
|
||||
# Trusted IP
|
||||
TRUSTED_IP_VERIFY_ENABLED = CONFIG.TRUSTED_IP_VERIFY_ENABLED
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.templatetags.static import static
|
||||
from django.utils.translation import get_language
|
||||
|
||||
DEFAULT_VENDOR = "jumpserver"
|
||||
DEFAULT_LOGIN_TEMPLATE = "authentication/login.html"
|
||||
DEFAULT_THEME = "classic_green"
|
||||
|
||||
VENDOR_THEMES_DIR = settings.VENDOR_TEMPLATES_DIR / "themes"
|
||||
|
||||
VENDOR_LOWER = settings.VENDOR.lower()
|
||||
|
||||
|
||||
def is_default_vendor() -> bool:
|
||||
return VENDOR_LOWER == DEFAULT_VENDOR
|
||||
|
||||
|
||||
def find_theme_path(theme_dirs, theme_name: str) -> Path | None:
|
||||
filename = f"{theme_name}.json"
|
||||
for d in theme_dirs:
|
||||
p = d / filename
|
||||
if p.is_file():
|
||||
return p
|
||||
|
||||
|
||||
def _default_theme_dir() -> Path:
|
||||
data_dir = Path(settings.BASE_DIR)
|
||||
return data_dir / "xpack" / "plugins" / "interface" / "themes"
|
||||
|
||||
|
||||
def _build_theme() -> str:
|
||||
return DEFAULT_THEME if is_default_vendor() else VENDOR_LOWER
|
||||
|
||||
|
||||
def _build_theme_info() -> dict:
|
||||
default_theme_path = find_theme_path([_default_theme_dir()], DEFAULT_THEME)
|
||||
|
||||
search_dirs = [_default_theme_dir()] if is_default_vendor() else [
|
||||
settings.VENDOR_TEMPLATES_DIR / 'themes',
|
||||
_default_theme_dir(),
|
||||
]
|
||||
theme_name = DEFAULT_THEME if is_default_vendor() else VENDOR_LOWER
|
||||
|
||||
theme_path = find_theme_path(search_dirs, theme_name) or default_theme_path
|
||||
if not theme_path:
|
||||
return {}
|
||||
return json.loads(theme_path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _build_vendor_info() -> dict:
|
||||
if is_default_vendor():
|
||||
return {}
|
||||
info_path = settings.VENDOR_TEMPLATES_DIR / "info.json"
|
||||
if not info_path.exists():
|
||||
return {}
|
||||
return json.loads(info_path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _build_vendor_info_value(key: str, default=None):
|
||||
info = _build_vendor_info()
|
||||
return info.get(key, default)
|
||||
|
||||
|
||||
def _build_vendor_interface_ext() -> dict:
|
||||
value = _build_vendor_info_value("interface_ext", default={})
|
||||
return value if isinstance(value, dict) else {}
|
||||
|
||||
def _build_vendor_translate_ext() -> dict:
|
||||
value = _build_vendor_info_value("translate", default={})
|
||||
return value if isinstance(value, dict) else {}
|
||||
|
||||
|
||||
def _build_vendor_interface_ext_defaults() -> dict:
|
||||
defaults = {}
|
||||
translate = get_vendor_value('translate', default={})
|
||||
for field_name, field_info in _build_vendor_interface_ext().items():
|
||||
if "default" in field_info:
|
||||
default_value = field_info["default"]
|
||||
if field_info['type'] == "image":
|
||||
defaults[field_name] = _build_asset(default_value)
|
||||
else:
|
||||
defaults[field_name] = translate.get(get_language(), {}).get(default_value, default_value)
|
||||
return defaults
|
||||
|
||||
|
||||
def _build_asset(filename: str) -> str:
|
||||
if is_default_vendor():
|
||||
return static(filename)
|
||||
|
||||
vendor_path = f"{VENDOR_LOWER}/{filename}"
|
||||
if finders.find(vendor_path):
|
||||
return static(vendor_path)
|
||||
return static(filename)
|
||||
|
||||
|
||||
def _build_optional_asset(filename: str, default=None):
|
||||
if not is_default_vendor():
|
||||
vendor_path = f"{VENDOR_LOWER}/{filename}"
|
||||
if finders.find(vendor_path):
|
||||
return static(vendor_path)
|
||||
|
||||
if finders.find(filename):
|
||||
return static(filename)
|
||||
return default
|
||||
|
||||
|
||||
VENDOR_BUILDERS = {
|
||||
"theme": _build_theme,
|
||||
"theme_info": _build_theme_info,
|
||||
"logo_logout": lambda: _build_asset("img/logo.png"),
|
||||
"logo_index": lambda: _build_asset("img/logo_text_white.png"),
|
||||
"login_image": lambda: _build_asset("img/login_image.png"),
|
||||
"favicon": lambda: _build_asset("img/facio.ico"),
|
||||
"logo_white": lambda: _build_asset("img/logo_white.png"),
|
||||
"logo_text_white": lambda: _build_asset("img/logo_text_white.png"),
|
||||
"login_title": lambda: _build_vendor_info_value("login_title"),
|
||||
"footer_content": lambda: _build_vendor_info_value("footer_content"),
|
||||
"interface_ext": _build_vendor_interface_ext,
|
||||
"translate": _build_vendor_translate_ext,
|
||||
"interface_ext_defaults": _build_vendor_interface_ext_defaults,
|
||||
}
|
||||
|
||||
|
||||
def get_vendor_value(kind: str, default=None):
|
||||
builder = VENDOR_BUILDERS.get(kind)
|
||||
if not builder:
|
||||
return default
|
||||
value = builder()
|
||||
return default if value is None else value
|
||||
@@ -1,5 +1,7 @@
|
||||
import re
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from drf_spectacular.openapi import AutoSchema
|
||||
from drf_spectacular.generators import SchemaGenerator
|
||||
|
||||
@@ -70,6 +72,20 @@ class CustomAutoSchema(AutoSchema):
|
||||
|
||||
return '_'.join(tokenized_path + [action])
|
||||
|
||||
def get_description(self):
|
||||
description = super().get_description()
|
||||
base_dir = str(settings.BASE_DIR)
|
||||
my_apps = [
|
||||
app.label for app in apps.get_app_configs()
|
||||
if app.module.__file__ and app.module.__file__.startswith(base_dir)
|
||||
]
|
||||
view_app = str(self.view.__class__.__module__.split('.')[0])
|
||||
if view_app in my_apps:
|
||||
# 内部 app 的 view 注释不展示在文档里
|
||||
return ''
|
||||
else:
|
||||
return description
|
||||
|
||||
def get_filter_parameters(self):
|
||||
if not self.should_filter():
|
||||
return []
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
from common.utils import get_logger
|
||||
from common.utils.timezone import local_now
|
||||
from users.models import User
|
||||
from .models import MessageContent as SiteMessageModel, SiteMessage
|
||||
from .models import MessageContent, SiteMessage
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
@@ -12,19 +14,22 @@ class SiteMessageUtil:
|
||||
|
||||
@classmethod
|
||||
def send_msg(cls, subject, message, user_ids=(), group_ids=(),
|
||||
sender=None, is_broadcast=False, display_mode=SiteMessageModel.DisplayMode.default):
|
||||
sender=None, is_broadcast=False, display_mode=MessageContent.DisplayMode.default):
|
||||
if not any((user_ids, group_ids, is_broadcast)):
|
||||
raise ValueError('No recipient is specified')
|
||||
|
||||
with transaction.atomic():
|
||||
site_msg = SiteMessageModel(
|
||||
site_msg = MessageContent(
|
||||
subject=subject, message=message,
|
||||
is_broadcast=is_broadcast, sender=sender,
|
||||
display_mode=display_mode
|
||||
)
|
||||
|
||||
if is_broadcast:
|
||||
user_ids = User.objects.all().values_list('id', flat=True)
|
||||
# 广播消息时只为在线用户创建,未登录的用户不创建消息,
|
||||
# 等用户登录后在 SiteMessage.create_site_msg_for_user_if_need 按需创建
|
||||
from audits.models import UserSession
|
||||
user_ids = UserSession.objects.all().values_list('user_id', flat=True).distinct()
|
||||
elif group_ids:
|
||||
site_msg.groups.add(*group_ids)
|
||||
|
||||
@@ -66,10 +71,32 @@ class SiteMessageUtil:
|
||||
|
||||
@classmethod
|
||||
def get_user_display_msgs(cls, user_id):
|
||||
# 获取用户未读的且需要展示的消息
|
||||
msgs = SiteMessage.objects.filter(user_id=user_id, has_read=False).exclude(
|
||||
content__display_mode=SiteMessageModel.DisplayMode.default
|
||||
content__display_mode=MessageContent.DisplayMode.default
|
||||
).prefetch_related('content')
|
||||
return msgs
|
||||
|
||||
@classmethod
|
||||
def create_site_msgs_for_user_if_need(cls, user_id):
|
||||
'''
|
||||
创建用户未读的且需要展示的消息
|
||||
广播消息时只为在线用户创建,未登录的用户不创建消息,等用户登录后在这里按需创建
|
||||
只创建用户没有的、最近24小时内的、需要广播的消息
|
||||
'''
|
||||
contents = MessageContent.objects.filter(
|
||||
is_broadcast=True,
|
||||
date_created__gt=local_now() - timedelta(hours=24)
|
||||
).exclude(display_mode=MessageContent.DisplayMode.default)
|
||||
content_ids = set(contents.values_list('id', flat=True))
|
||||
has_content_ids = SiteMessage.objects.filter(
|
||||
user_id=user_id, content_id__in=content_ids
|
||||
).values_list('content_id', flat=True)
|
||||
to_create_content_ids = set(content_ids) - set(has_content_ids)
|
||||
site_msgs = [
|
||||
SiteMessage(user_id=user_id, content_id=cid) for cid in to_create_content_ids
|
||||
]
|
||||
SiteMessage.objects.bulk_create(site_msgs)
|
||||
|
||||
@classmethod
|
||||
def mark_msgs_as_read(cls, user_id, msg_ids=None):
|
||||
|
||||
@@ -66,6 +66,7 @@ class SiteMsgWebsocket(JsonWebsocketConsumer):
|
||||
|
||||
# 先发一个消息再说
|
||||
with safe_db_connection():
|
||||
SiteMessageUtil.create_site_msgs_for_user_if_need(user_id)
|
||||
self.send_site_msg()
|
||||
|
||||
def handle_new_site_msg_recv(msg):
|
||||
|
||||
@@ -90,6 +90,7 @@ class PrivateSettingSerializer(PublicSettingSerializer):
|
||||
JDMC_ENABLED = serializers.BooleanField()
|
||||
FLOWER_ENABLED = serializers.BooleanField()
|
||||
REMOTE_APP_STORE_URL = serializers.CharField()
|
||||
VENDOR = serializers.CharField()
|
||||
|
||||
|
||||
class ServerInfoSerializer(serializers.Serializer):
|
||||
|
||||
@@ -9,8 +9,9 @@ from common.utils import lookup_domain
|
||||
def get_interface_setting_or_default():
|
||||
if not settings.XPACK_ENABLED:
|
||||
return default_interface
|
||||
|
||||
from xpack.plugins.interface.models import Interface
|
||||
return Interface.get_interface_setting()
|
||||
return Interface.get_interface_setting(default_interface)
|
||||
|
||||
|
||||
def get_login_title():
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
{% else %}
|
||||
message = '{{ message|safe }}'
|
||||
{% endif %}
|
||||
var redirect_url = '{{ redirect_url }}'
|
||||
var redirect_url = '{{ redirect_url|escapejs }}'
|
||||
|
||||
function redirect_page() {
|
||||
if (time >= 0) {
|
||||
@@ -69,4 +69,3 @@
|
||||
{% endif %}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -50,12 +50,15 @@ class NativeClient(TextChoices):
|
||||
Protocol.mysql: [cls.db_client, cls.db_guide],
|
||||
Protocol.mariadb: [cls.db_client, cls.db_guide],
|
||||
Protocol.redis: [cls.db_client, cls.db_guide],
|
||||
Protocol.mongodb: [cls.db_client, cls.db_guide],
|
||||
Protocol.oracle: [cls.db_client, cls.db_guide],
|
||||
Protocol.postgresql: [cls.db_client, cls.db_guide],
|
||||
Protocol.sqlserver: [cls.db_client, cls.db_guide],
|
||||
Protocol.vnc: [cls.vnc_client, cls.vnc_guide],
|
||||
}
|
||||
if settings.VENDOR.lower() == 'jumpserver':
|
||||
clients.update({
|
||||
Protocol.mongodb: [cls.db_client, cls.db_guide],
|
||||
Protocol.oracle: [cls.db_client, cls.db_guide],
|
||||
})
|
||||
return clients
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.conf import settings
|
||||
from rest_framework import serializers
|
||||
|
||||
from acls.serializers.rules import address_validator, ip_group_help_text
|
||||
@@ -30,6 +31,15 @@ class EndpointSerializer(BulkModelSerializer):
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.remove_fields_if_need()
|
||||
|
||||
def remove_fields_if_need(self):
|
||||
if settings.VENDOR.lower() != 'jumpserver':
|
||||
self.fields.pop('oracle_port')
|
||||
self.fields.pop('mongodb_port')
|
||||
|
||||
def get_extra_kwargs(self):
|
||||
extra_kwargs = super().get_extra_kwargs()
|
||||
|
||||
@@ -111,7 +111,6 @@
|
||||
|
||||
.field-value {
|
||||
color: #1F2329;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,16 +3,14 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.http import HttpResponse
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.shortcuts import redirect, reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic.base import TemplateView
|
||||
|
||||
from common.utils import get_logger, FlashMessageUtil
|
||||
from common.exceptions import JMSException
|
||||
from users.models import User
|
||||
from common.utils import get_logger, FlashMessageUtil
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from tickets.const import TicketType
|
||||
from tickets.errors import AlreadyClosed
|
||||
@@ -20,6 +18,7 @@ from tickets.models import (
|
||||
Ticket, ApplyAssetTicket,
|
||||
ApplyLoginTicket, ApplyLoginAssetTicket, ApplyCommandTicket
|
||||
)
|
||||
from users.models import User
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -49,7 +48,7 @@ class TicketDirectApproveView(TemplateView):
|
||||
|
||||
@property
|
||||
def login_url(self):
|
||||
return reverse('authentication:login') + '?admin=1'
|
||||
return reverse('authentication:login') + f'?admin=1&{self.redirect_field_name}=/'
|
||||
|
||||
def redirect_message_response(self, **kwargs):
|
||||
message_data = self.message_data
|
||||
@@ -76,10 +75,11 @@ class TicketDirectApproveView(TemplateView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
if not (settings.TICKETS_DIRECT_APPROVE or request.user.is_authenticated):
|
||||
direct_url = reverse('tickets:direct-approve', kwargs={'token': kwargs['token']})
|
||||
redirect_url = f'{reverse('authentication:login') + '?admin=1'}&{self.redirect_field_name}={direct_url}'
|
||||
message_data = {
|
||||
'title': _('Ticket approval'),
|
||||
'message': _('After successful authentication, this ticket can be approved directly'),
|
||||
'redirect_url': f'{self.login_url}&{self.redirect_field_name}={direct_url}',
|
||||
'redirect_url': redirect_url,
|
||||
'auto_redirect': True,
|
||||
}
|
||||
redirect_url = FlashMessageUtil.gen_message_url(message_data)
|
||||
|
||||
Reference in New Issue
Block a user