Commit dc3a9561 authored by ibuler's avatar ibuler

[Update] 基本完成登陆审核

parent 23b777b2
...@@ -5,3 +5,4 @@ from .auth import * ...@@ -5,3 +5,4 @@ from .auth import *
from .token import * from .token import *
from .mfa import * from .mfa import *
from .access_key import * from .access_key import *
from .login_confirm import *
# -*- coding: utf-8 -*-
#
from rest_framework.generics import UpdateAPIView
from django.shortcuts import get_object_or_404
from common.permissions import IsOrgAdmin
from ..models import LoginConfirmSetting
from ..serializers import LoginConfirmSettingSerializer
__all__ = ['LoginConfirmSettingUpdateApi']
class LoginConfirmSettingUpdateApi(UpdateAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = LoginConfirmSettingSerializer
def get_object(self):
from users.models import User
user_id = self.kwargs.get('user_id')
user = get_object_or_404(User, pk=user_id)
defaults = {'user': user}
s, created = LoginConfirmSetting.objects.get_or_create(
defaults, user=user,
)
return s
# Generated by Django 2.2.5 on 2019-10-31 10:23
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('authentication', '0002_auto_20190729_1423'),
]
operations = [
migrations.CreateModel(
name='LoginConfirmSetting',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
('reviewers', models.ManyToManyField(blank=True, related_name='review_login_confirm_settings', to=settings.AUTH_USER_MODEL, verbose_name='Reviewers')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='login_confirm_setting', to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'abstract': False,
},
),
]
import uuid import uuid
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _, ugettext as __
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from django.conf import settings from django.conf import settings
...@@ -40,8 +40,8 @@ class PrivateToken(Token): ...@@ -40,8 +40,8 @@ class PrivateToken(Token):
class LoginConfirmSetting(CommonModelMixin): class LoginConfirmSetting(CommonModelMixin):
user = models.OneToOneField('users.User', on_delete=models.CASCADE, verbose_name=_("User"), related_name=_("login_confirmation_setting")) user = models.OneToOneField('users.User', on_delete=models.CASCADE, verbose_name=_("User"), related_name="login_confirm_setting")
reviewers = models.ManyToManyField('users.User', verbose_name=_("Reviewers"), related_name=_("review_login_confirmation_settings")) reviewers = models.ManyToManyField('users.User', verbose_name=_("Reviewers"), related_name="review_login_confirm_settings", blank=True)
is_active = models.BooleanField(default=True, verbose_name=_("Is active")) is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
@classmethod @classmethod
...@@ -50,7 +50,7 @@ class LoginConfirmSetting(CommonModelMixin): ...@@ -50,7 +50,7 @@ class LoginConfirmSetting(CommonModelMixin):
def create_confirm_order(self, request=None): def create_confirm_order(self, request=None):
from orders.models import LoginConfirmOrder from orders.models import LoginConfirmOrder
title = _('User login confirm: {}'.format(self.user)) title = _('User login confirm: {}').format(self.user)
if request: if request:
remote_addr = get_request_ip(request) remote_addr = get_request_ip(request)
city = get_ip_city(remote_addr) city = get_ip_city(remote_addr)
......
...@@ -4,17 +4,16 @@ from django.core.cache import cache ...@@ -4,17 +4,16 @@ from django.core.cache import cache
from rest_framework import serializers from rest_framework import serializers
from users.models import User from users.models import User
from .models import AccessKey from .models import AccessKey, LoginConfirmSetting
__all__ = [ __all__ = [
'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer', 'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer',
'MFAChallengeSerializer', 'MFAChallengeSerializer', 'LoginConfirmSettingSerializer',
] ]
class AccessKeySerializer(serializers.ModelSerializer): class AccessKeySerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = AccessKey model = AccessKey
fields = ['id', 'secret', 'is_active', 'date_created'] fields = ['id', 'secret', 'is_active', 'date_created']
...@@ -87,3 +86,9 @@ class MFAChallengeSerializer(BearerTokenMixin, serializers.Serializer): ...@@ -87,3 +86,9 @@ class MFAChallengeSerializer(BearerTokenMixin, serializers.Serializer):
username = self.context["username"] username = self.context["username"]
return self.create_response(username) return self.create_response(username)
class LoginConfirmSettingSerializer(serializers.ModelSerializer):
class Meta:
model = LoginConfirmSetting
fields = ['id', 'user', 'reviewers', 'date_created', 'date_updated']
read_only_fields = ['date_created', 'date_updated']
...@@ -61,9 +61,6 @@ ...@@ -61,9 +61,6 @@
<div class="col-md-6"> <div class="col-md-6">
{% include '_copyright.html' %} {% include '_copyright.html' %}
</div> </div>
<div class="col-md-6 text-right">
<small>2014-2019</small>
</div>
</div> </div>
</div> </div>
</body> </body>
......
...@@ -19,7 +19,8 @@ urlpatterns = [ ...@@ -19,7 +19,8 @@ urlpatterns = [
api.UserConnectionTokenApi.as_view(), name='connection-token'), api.UserConnectionTokenApi.as_view(), name='connection-token'),
path('otp/auth/', api.UserOtpAuthApi.as_view(), name='user-otp-auth'), path('otp/auth/', api.UserOtpAuthApi.as_view(), name='user-otp-auth'),
path('otp/verify/', api.UserOtpVerifyApi.as_view(), name='user-otp-verify'), path('otp/verify/', api.UserOtpVerifyApi.as_view(), name='user-otp-verify'),
path('order/auth/', api.UserOrderAcceptAuthApi.as_view(), name='user-order-auth') path('order/auth/', api.UserOrderAcceptAuthApi.as_view(), name='user-order-auth'),
path('login-confirm-settings/<uuid:user_id>/', api.LoginConfirmSettingUpdateApi.as_view(), name='login-confirm-setting-update')
] ]
urlpatterns += router.urls urlpatterns += router.urls
......
...@@ -179,7 +179,7 @@ class UserLoginGuardView(RedirectView): ...@@ -179,7 +179,7 @@ class UserLoginGuardView(RedirectView):
if user.otp_enabled and user.otp_secret_key and \ if user.otp_enabled and user.otp_secret_key and \
not self.request.session.get('auth_otp'): not self.request.session.get('auth_otp'):
return reverse('authentication:login-otp') return reverse('authentication:login-otp')
confirm_setting = LoginConfirmSetting.get_user_confirm_setting(user) confirm_setting = user.get_login_confirm_setting()
if confirm_setting and not self.request.session.get('auth_confirm'): if confirm_setting and not self.request.session.get('auth_confirm'):
order = confirm_setting.create_confirm_order(self.request) order = confirm_setting.create_confirm_order(self.request)
self.request.session['auth_order_id'] = str(order.id) self.request.session['auth_order_id'] = str(order.id)
......
This diff is collapsed.
# Generated by Django 2.2.5 on 2019-10-31 10:23
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='LoginConfirmOrder',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('user_display', models.CharField(max_length=128, verbose_name='User display name')),
('title', models.CharField(max_length=256, verbose_name='Title')),
('body', models.TextField(verbose_name='Body')),
('assignee_display', models.CharField(blank=True, max_length=128, null=True, verbose_name='Assignee display name')),
('assignees_display', models.CharField(blank=True, max_length=128, verbose_name='Assignees display name')),
('type', models.CharField(choices=[('login_confirm', 'Login confirm')], max_length=16, verbose_name='Type')),
('status', models.CharField(choices=[('accepted', 'Accepted'), ('rejected', 'Rejected'), ('pending', 'Pending')], default='pending', max_length=16)),
('ip', models.GenericIPAddressField(blank=True, null=True)),
('city', models.CharField(blank=True, default='', max_length=16)),
('assignee', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='loginconfirmorder_handled', to=settings.AUTH_USER_MODEL, verbose_name='Assignee')),
('assignees', models.ManyToManyField(related_name='loginconfirmorder_assigned', to=settings.AUTH_USER_MODEL, verbose_name='Assignees')),
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='loginconfirmorder_requested', to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'ordering': ('-date_created',),
'abstract': False,
},
),
migrations.CreateModel(
name='Comment',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('order_id', models.UUIDField()),
('user_display', models.CharField(max_length=128, verbose_name='User display name')),
('body', models.TextField(verbose_name='Body')),
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='comments', to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'ordering': ('date_created',),
},
),
]
...@@ -48,6 +48,14 @@ class BaseOrder(CommonModelMixin): ...@@ -48,6 +48,14 @@ class BaseOrder(CommonModelMixin):
def comments(self): def comments(self):
return Comment.objects.filter(order_id=self.id) return Comment.objects.filter(order_id=self.id)
@property
def body_as_html(self):
return self.body.replace('\n', '<br/>')
@property
def status_display(self):
return self.get_status_display()
class Meta: class Meta:
abstract = True abstract = True
ordering = ('-date_created',) ordering = ('-date_created',)
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.utils.translation import ugettext as _
from django.dispatch import receiver from django.dispatch import receiver
from django.db.models.signals import m2m_changed from django.db.models.signals import m2m_changed, post_save
from django.conf import settings
from common.tasks import send_mail_async from common.utils import get_logger
from common.utils import get_logger, reverse
from .models import LoginConfirmOrder from .models import LoginConfirmOrder
from .utils import (
send_login_confirm_order_mail_to_assignees,
send_login_confirm_action_mail_to_user
)
logger = get_logger(__name__)
def send_mail(order, assignees):
recipient_list = [user.email for user in assignees]
user = order.user
if not recipient_list:
logger.error("Order not has assignees: {}".format(order.id))
return
subject = '{}: {}'.format(_("New order"), order.title)
detail_url = reverse('orders:login-confirm-order-detail',
kwargs={'pk': order.id}, external=True)
message = _("""
<div>
<p>Your has a new order</p>
<div>
<b>Title:</b> {order.title}
<br/>
<b>User:</b> {user}
<br/>
<b>City:</b> {order.city}
<br/>
<b>IP:</b> {order.ip}
<br/>
<a href={url}>click here to review</a>
</div>
</div>
""").format(order=order, user=user, url=detail_url)
if settings.DEBUG:
try:
print(message)
except OSError:
pass
send_mail_async.delay(subject, message, recipient_list, html_message=message) logger = get_logger(__name__)
@receiver(m2m_changed, sender=LoginConfirmOrder.assignees.through) @receiver(m2m_changed, sender=LoginConfirmOrder.assignees.through)
def on_login_confirm_order_assignee_set(sender, instance=None, action=None, def on_login_confirm_order_assignees_set(sender, instance=None, action=None,
model=None, pk_set=None, **kwargs): model=None, pk_set=None, **kwargs):
print(">>>>>>>>>>>>>>>>>>>>>>>.")
print(action)
if action == 'post_add': if action == 'post_add':
print("<<<<<<<<<<<<<<<<<<<<")
logger.debug('New order create, send mail: {}'.format(instance.id)) logger.debug('New order create, send mail: {}'.format(instance.id))
assignees = model.objects.filter(pk__in=pk_set) assignees = model.objects.filter(pk__in=pk_set)
send_mail(instance, assignees) print(assignees)
send_login_confirm_order_mail_to_assignees(instance, assignees)
@receiver(post_save, sender=LoginConfirmOrder)
def on_login_confirm_order_status_change(sender, instance=None, created=False, **kwargs):
if created or instance.status == "pending":
return
logger.debug('Order changed, send mail: {}'.format(instance.id))
send_login_confirm_action_mail_to_user(instance)
...@@ -14,7 +14,6 @@ ...@@ -14,7 +14,6 @@
<th class="text-center">{% trans 'Title' %}</th> <th class="text-center">{% trans 'Title' %}</th>
<th class="text-center">{% trans 'User' %}</th> <th class="text-center">{% trans 'User' %}</th>
<th class="text-center">{% trans 'IP' %}</th> <th class="text-center">{% trans 'IP' %}</th>
<th class="text-center">{% trans 'City' %}</th>
<th class="text-center">{% trans 'Status' %}</th> <th class="text-center">{% trans 'Status' %}</th>
<th class="text-center">{% trans 'Datetime' %}</th> <th class="text-center">{% trans 'Datetime' %}</th>
<th class="text-center">{% trans 'Action' %}</th> <th class="text-center">{% trans 'Action' %}</th>
...@@ -38,8 +37,12 @@ function initTable() { ...@@ -38,8 +37,12 @@ function initTable() {
cellData = htmlEscape(cellData); cellData = htmlEscape(cellData);
var detailBtn = '<a href="{% url "orders:login-confirm-order-detail" pk=DEFAULT_PK %}">' + cellData + '</a>'; var detailBtn = '<a href="{% url "orders:login-confirm-order-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
$(td).html(detailBtn.replace("{{ DEFAULT_PK }}", rowData.id)); $(td).html(detailBtn.replace("{{ DEFAULT_PK }}", rowData.id));
}}, }},
{targets: 5, createdCell: function (td, cellData, rowData) { {targets: 3, createdCell: function (td, cellData, rowData) {
var d = cellData + "(" + rowData.city + ")";
$(td).html(d)
}},
{targets: 4, createdCell: function (td, cellData, rowData) {
if (cellData === "accepted") { if (cellData === "accepted") {
$(td).html('<i class="fa fa-check text-navy"></i>') $(td).html('<i class="fa fa-check text-navy"></i>')
} else if (cellData === "rejected") { } else if (cellData === "rejected") {
...@@ -48,13 +51,13 @@ function initTable() { ...@@ -48,13 +51,13 @@ function initTable() {
$(td).html('<i class="fa fa-spinner text-info"></i>') $(td).html('<i class="fa fa-spinner text-info"></i>')
} }
}}, }},
{targets: 6, createdCell: function (td, cellData) { {targets: 5, createdCell: function (td, cellData) {
var d = toSafeLocalDateStr(cellData); var d = toSafeLocalDateStr(cellData);
$(td).html(d) $(td).html(d)
}}, }},
{targets: 7, createdCell: function (td, cellData, rowData) { {targets: 6, createdCell: function (td, cellData, rowData) {
var acceptBtn = '<a class="btn btn-xs btn-info" data-uid="{{ DEFAULT_PK }}" >{% trans "Accept" %}</a> '; var acceptBtn = '<a class="btn btn-xs btn-info btn-action" data-action="accept" data-uid="{{ DEFAULT_PK }}" >{% trans "Accept" %}</a> ';
var rejectBtn = '<a class="btn btn-xs btn-danger " data-uid="{{ DEFAULT_PK }}" >{% trans "Reject" %}</a>'; var rejectBtn = '<a class="btn btn-xs btn-danger btn-action" data-action="reject" data-uid="{{ DEFAULT_PK }}" >{% trans "Reject" %}</a>';
acceptBtn = acceptBtn.replace('{{ DEFAULT_PK }}', cellData); acceptBtn = acceptBtn.replace('{{ DEFAULT_PK }}', cellData);
rejectBtn = rejectBtn.replace('{{ DEFAULT_PK }}', cellData); rejectBtn = rejectBtn.replace('{{ DEFAULT_PK }}', cellData);
var acceptBtnRef = $(acceptBtn); var acceptBtnRef = $(acceptBtn);
...@@ -68,10 +71,10 @@ function initTable() { ...@@ -68,10 +71,10 @@ function initTable() {
}}], }}],
ajax_url: '{% url "api-orders:login-confirm-order-list" %}', ajax_url: '{% url "api-orders:login-confirm-order-list" %}',
columns: [ columns: [
{data: "id"}, {data: "title", className: "text-left"}, {data: "user_display"}, {data: "id"}, {data: "title", className: "text-left"},
{data: "ip"}, {data: "city"}, {data: "user_display"}, {data: "ip"},
{data: "status", orderable: false}, {data: "status", orderable: false, width: "30px"},
{data: "date_created"}, {data: "date_created", width: "120px"},
{data: "id", orderable: false, width: "100px"} {data: "id", orderable: false, width: "100px"}
], ],
op_html: $('#actions').html() op_html: $('#actions').html()
...@@ -82,7 +85,6 @@ function initTable() { ...@@ -82,7 +85,6 @@ function initTable() {
$(document).ready(function(){ $(document).ready(function(){
initTable(); initTable();
$('')
var menu = [ var menu = [
{title: "IP", value: "ip"}, {title: "IP", value: "ip"},
{title: "{% trans 'Title' %}", value: "title"}, {title: "{% trans 'Title' %}", value: "title"},
...@@ -93,12 +95,21 @@ $(document).ready(function(){ ...@@ -93,12 +95,21 @@ $(document).ready(function(){
]} ]}
]; ];
initTableFilterDropdown('#login_confirm_order_list_table_filter input', menu) initTableFilterDropdown('#login_confirm_order_list_table_filter input', menu)
}).on('click', '.expired', function () { }).on('click', '.btn-action', function () {
var msg = '{% trans "User is expired" %}'; var actionCreateUrl = "{% url 'api-orders:login-confirm-order-create-action' pk=DEFAULT_PK %}";
toastr.error(msg) var orderId = $(this).data('uid');
}).on('click', '.inactive', function () { actionCreateUrl = actionCreateUrl.replace("{{ DEFAULT_PK }}", orderId);
var msg = '{% trans 'User is inactive' %}'; var action = $(this).data('action');
toastr.error(msg) var comment = '';
var data = {
url: actionCreateUrl,
method: 'POST',
body: JSON.stringify({action: action, comment: comment}),
success: function () {
window.location.reload();
}
};
requestApi(data);
}) })
</script> </script>
{% endblock %} {% endblock %}
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.conf import settings
from django.utils.translation import ugettext as _
from common.utils import get_logger, reverse
from common.tasks import send_mail_async
logger = get_logger(__name__)
def send_login_confirm_order_mail_to_assignees(order, assignees):
recipient_list = [user.email for user in assignees]
user = order.user
if not recipient_list:
logger.error("Order not has assignees: {}".format(order.id))
return
subject = '{}: {}'.format(_("New order"), order.title)
detail_url = reverse('orders:login-confirm-order-detail',
kwargs={'pk': order.id}, external=True)
message = _("""
<div>
<p>Your has a new order</p>
<div>
<b>Title:</b> {order.title}
<br/>
<b>User:</b> {user}
<br/>
<b>Assignees:</b> {order.assignees_display}
<br/>
<b>City:</b> {order.city}
<br/>
<b>IP:</b> {order.ip}
<br/>
<a href={url}>click here to review</a>
</div>
</div>
""").format(order=order, user=user, url=detail_url)
send_mail_async.delay(subject, message, recipient_list, html_message=message)
def send_login_confirm_action_mail_to_user(order):
if not order.user:
logger.error("Order not has user: {}".format(order.id))
return
user = order.user
recipient_list = [user.email]
subject = '{}: {}'.format(_("Order has been reply"), order.title)
message = _("""
<div>
<p>Your order has been replay</p>
<div>
<b>Title:</b> {order.title}
<br/>
<b>Assignee:</b> {order.assignee_display}
<br/>
<b>Status:</b> {order.status_display}
<br/>
</div>
</div>
""").format(order=order)
send_mail_async.delay(subject, message, recipient_list, html_message=message)
...@@ -416,6 +416,9 @@ function makeLabel(data) { ...@@ -416,6 +416,9 @@ function makeLabel(data) {
function parseTableFilter(value) { function parseTableFilter(value) {
var cleanValues = []; var cleanValues = [];
if (!value) {
return {}
}
var valuesArray = value.split(':'); var valuesArray = value.split(':');
for (var i=0; i<valuesArray.length; i++) { for (var i=0; i<valuesArray.length; i++) {
var v = valuesArray[i].trim(); var v = valuesArray[i].trim();
......
...@@ -55,9 +55,6 @@ ...@@ -55,9 +55,6 @@
<div class="col-md-6"> <div class="col-md-6">
{% include '_copyright.html' %} {% include '_copyright.html' %}
</div> </div>
<div class="col-md-6 text-right">
<small>2014-2019</small>
</div>
</div> </div>
</div> </div>
</body> </body>
......
...@@ -117,6 +117,13 @@ class AuthMixin: ...@@ -117,6 +117,13 @@ class AuthMixin:
return True return True
return False return False
def get_login_confirm_setting(self):
if hasattr(self, 'login_confirm_setting'):
s = self.login_confirm_setting
if s.reviewers.all().count() and s.is_active:
return s
return False
class RoleMixin: class RoleMixin:
ROLE_ADMIN = 'Admin' ROLE_ADMIN = 'Admin'
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment