1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-07-14 23:46:49 +00:00

[share link] email address verification: redesigned the 'verification form' UI & the 2 emails (#6399)

This commit is contained in:
llj 2024-07-23 13:30:09 +08:00 committed by GitHub
parent c64bb90669
commit cd5a9b824c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 143 additions and 224 deletions

View File

@ -2065,3 +2065,11 @@ a.sf-popover-item {
background: #6e7687;
height: 12px;
}
#email-audit-form .email-input {
width: 172px;
}
#email-audit-form .get-code-btn {
width: 102px;
}

View File

@ -32,6 +32,7 @@ from seahub.avatar.settings import AVATAR_DEFAULT_SIZE
from seahub.avatar.templatetags.avatar_tags import api_avatar_url
from seahub.utils import get_user_repos
from seahub.utils.mail import send_html_email_with_dj_template
from django.utils.translation import gettext as _
from seahub.settings import SECRET_KEY
logger = logging.getLogger(__name__)
@ -285,7 +286,7 @@ def is_web_request(request):
return True
else:
return False
def is_wiki_repo(repo):
return repo.repo_type == REPO_TYPE_WIKI
@ -306,9 +307,9 @@ def get_search_repos(username, org_id):
repos.append((repo.id, repo.origin_repo_id, repo.origin_path, repo.name))
return repos
def send_share_link_emails(emails, fs, shared_from):
subject = "Share links"
subject = _("A share link for you")
for email in emails:
c = {'url': "%s?email=%s" % (fs.get_full_url(), email), 'shared_from': shared_from}
send_success = send_html_email_with_dj_template(
@ -321,7 +322,7 @@ def send_share_link_emails(emails, fs, shared_from):
continue
def is_valid_internal_jwt(auth):
if not auth or auth[0].lower()!= 'token' or len(auth) != 2:
return False
@ -337,5 +338,5 @@ def is_valid_internal_jwt(auth):
is_internal = payload.get('is_internal')
if is_internal:
return True
return False

View File

@ -12,27 +12,33 @@ from seahub.share.models import FileShare, UploadLinkShare
from seahub.share.utils import SCOPE_SPECIFIC_EMAILS, SCOPE_ALL_USERS, SCOPE_SPECIFIC_USERS
from seahub.utils import render_error
from seahub.utils import normalize_cache_key, is_pro_version, redirect_to_login
from seahub.utils.auth import get_login_bg_image_path
from seahub.constants import REPO_SHARE_LINK_COUNT_LIMIT
def _share_link_auth_email_entry(request, fileshare, func, *args, **kwargs):
if request.user.username == fileshare.username:
return func(request, fileshare, *args, **kwargs)
session_key = "link_authed_email_%s" % fileshare.token
if request.session.get(session_key) is not None:
return func(request, fileshare, *args, **kwargs)
login_bg_image_path = get_login_bg_image_path()
if request.method == 'GET':
email = request.GET.get('email', '')
return render(request, 'share/share_link_email_audit.html', {'email': email, 'token': fileshare.token})
return render(request, 'share/share_link_email_audit.html', {
'email': email,
'token': fileshare.token,
'login_bg_image_path': login_bg_image_path,
})
elif request.method == 'POST':
code_post = request.POST.get('code', '')
email_post = request.POST.get('email', '')
cache_key = normalize_cache_key(email_post, 'share_link_email_auth_', token=fileshare.token)
code = cache.get(cache_key)
authed_details = json.loads(fileshare.authed_details)
if code == code_post and email_post in authed_details.get('authed_emails'):
request.session[session_key] = email_post
@ -40,12 +46,13 @@ def _share_link_auth_email_entry(request, fileshare, func, *args, **kwargs):
return func(request, fileshare, *args, **kwargs)
else:
return render(request, 'share/share_link_email_audit.html', {
'err_msg': 'Invalid token, please try again.',
'login_bg_image_path': login_bg_image_path,
'err_msg': _('Invalid verification code, please try again.'),
'email': email_post,
'code': code,
'token': fileshare.token,
'code_verify': False
})
else:
assert False, 'TODO'
@ -56,7 +63,7 @@ def share_link_audit(func):
def _decorated(request, token, *args, **kwargs):
assert token is not None # Checked by URLconf
is_for_upload = False
try:
fileshare = FileShare.objects.get(token=token)
@ -69,22 +76,22 @@ def share_link_audit(func):
is_for_upload = True
except UploadLinkShare.DoesNotExist:
fileshare = None
if not fileshare:
return render_error(request, _('Link does not exist.'))
if fileshare.is_expired():
return render_error(request, _('Link is expired.'))
if is_for_upload:
return func(request, fileshare, *args, **kwargs)
if fileshare.user_scope in [SCOPE_ALL_USERS, SCOPE_SPECIFIC_USERS]:
return func(request, fileshare, *args, **kwargs)
if fileshare.user_scope == SCOPE_SPECIFIC_EMAILS:
return _share_link_auth_email_entry(request, fileshare, func, *args, **kwargs)
return _decorated
def share_link_login_required(func):

View File

@ -2,15 +2,16 @@
{% load i18n %}
{% block email_con %}
{% autoescape off %}
<p style="color:#121214;font-size:14px;">{% trans "Hi," %}</p>
<p style="font-size:14px;color:#434144;">
{% blocktrans %}Your code is {{code}}, this code will be valid for one hour.{% endblocktrans%}
{% blocktrans %}The verification code is {{code}}, and it will be valid in one hour.{% endblocktrans%}
</p>
{% endautoescape %}
{% endblock %}
{% block thanks %}
{% endblock %}

View File

@ -1,80 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block main_panel %}
<div class="new-narrow-panel" role="main">
<h2 class="hd">{% trans "Email Verification" %}</h2>
<form action="" method="post" id="link-audit-form" class="con">{% csrf_token %}
<p class="tip">{% trans "Please provide your email address to continue." %}</p>
<label for="email">{% trans "Email" %}</label>
<input id="email" type="text" class="input email-input" name="email" value="{{email}}" />
<button type="button" id="get-code" class="get-code-btn">{% trans "Get code" %}</button><br />
<label for="code">{% trans "Verification code" %}</label><br />
<input id="code" type="text" class="input" name="code" value="{{code}}" />
{% if err_msg %}
<p class="error">{{ err_msg }}</p>
{% else %}
<p class="error hide"></p>
{% endif %}
<input type="submit" value="{% trans "Submit" %}" />
</form>
</div>
{% endblock %}
{% block extra_script %}
<script type="text/javascript">
$('#get-code').on('click', function() {
var email = $('input[name="email"]').val().trim();
if (!email) {
return false;
}
var $this = $(this);
var originalText = $this.text(); // Remember the original text content
var seconds = 60;
$this.prop('disabled', true).addClass('btn-disabled');
$this.text(originalText + '(' + seconds + 's)');
// do a set interval, using an interval of 1000 milliseconds
// and clear it after the number of seconds counts down to 0
var interval = setInterval(function() {
// decrement the seconds and update the text
seconds = seconds - 1;
$this.text(originalText + ' (' + seconds + 's)');
if (seconds === 0) { // once seconds is 0...
$this.prop('disabled', false).removeClass('btn-disabled')
.text(originalText); // reset to original text
clearInterval(interval); // clear interval
}
}, 1000);
$.ajax({
url: "{% url "ajax_get_link_audit_code" %}",
type: 'POST',
cache: false,
beforeSend: prepareCSRFToken,
data: {
token: "{{token}}",
email: email
},
success: function() {
feedback("{% trans "A verification code has been sent to the email." %}", 'success');
},
error: function(xhr) {
var error_msg = prepareAjaxErrorMsg(xhr);
$('.error', $this.closest('form')).html(error_msg).removeClass('hide');
}
});
});
$('#link-audit-form').on('submit', function() {
var email = $('[name="email"]').val().trim();
var code = $('[name="code"]').val().trim();
if (!email || !code) {
return false;
}
});
</script>
{% endblock %}

View File

@ -2,18 +2,21 @@
{% load i18n %}
{% block email_con %}
{% autoescape off %}
<p style="color:#121214;font-size:14px;">{% trans "Hi," %}</p>
<p style="font-size:14px;color:#434144;">
{% blocktrans %}
{{ shared_from }} has shared a library with you.
Please click <a href={{ url }}>here</a> to verify your email.
{{ shared_from }} shared a file link with you.
{% endblocktrans%}
</p>
<p style="font-size:14px;color:#434144;">
{% blocktrans %}
You can click <a href={{ url }}>here</a> to verify your email address and visit it.
{% endblocktrans%}
</p>
{% endautoescape %}
{% endblock %}
{% block thanks %}
{% endblock %}

View File

@ -1,82 +1,112 @@
{% extends "base.html" %}
{% load i18n %}
{% block sub_title %}{% trans "Email address verification" %} - {% endblock %}
{% block header_css_class %}hide{% endblock %}
{% block extra_base_style %}
<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/seafile-ui.css" />
{% endblock %}
{% block extra_style %}
<style type="text/css">
html, body, #wrapper { height:100%; }
#wrapper {
background: url('{{ MEDIA_URL }}{{login_bg_image_path}}') center top no-repeat scroll;
background-size: cover;
padding-top:1px;
}
</style>
{% endblock %}
{% block main_panel %}
<div class="new-narrow-panel" role="main">
<h2 class="hd">{% trans "Email Verification" %}</h2>
<form action="" method="post" id="link-audit-form" class="con">{% csrf_token %}
<p class="tip">{% trans "Please provide your email address to continue." %}</p>
<label for="email">{% trans "Email" %}</label>
<input id="email" type="text" class="input email-input" name="email" value="{{email}}" />
<button type="button" id="get-code" class="get-code-btn">{% trans "Get code" %}</button><br />
<label for="code">{% trans "Verification code" %}</label><br />
<input id="code" type="text" class="input" name="code" value="{{code}}" />
<div class="login-panel-outer-container vh">
<div class="login-panel" id="log-in-panel">
<h1 class="login-panel-hd">{% trans "Email address verification" %}</h1>
<p class="text-center">{% trans "Please provide your email address to continue." %}</p>
<form action="" method="post" id="email-audit-form" class="con">{% csrf_token %}
<div class="d-flex mt-2 mb-3">
<input type="text" class="input email-input mr-2" name="email" value="{{email}}" placeholder="{% trans "Enter your email address" %}" />
<button type="button" id="get-code" class="btn btn-primary btn-sm text-truncate get-code-btn flex-shrink-0" title="{% trans "Get code" %}">{% trans "Get code" %}</button>
</div>
<input id="code" type="text" class="input d-block" name="code" placeholder="{% trans "Paste the verification code here" %}" />
{% if err_msg %}
<p class="error">{{ err_msg }}</p>
{% else %}
<p class="error hide"></p>
{% endif %}
{% if err_msg %}
<p class="error">{{ err_msg }}</p>
{% else %}
<p class="error hide"></p>
{% endif %}
<input type="submit" value="{% trans "Submit" %}" />
<button type="submit" class="submit btn btn-primary btn-block h-auto">{% trans "Submit" %}</button>
</form>
</div>
</div>
{% endblock %}
{% block extra_script %}
<script type="text/javascript">
$('.login-panel-outer-container').prepend($($('#logo').html()).attr({'height': 40}).addClass('login-panel-logo'));
var $el = $('.login-panel-outer-container');
var elHeight = $el.outerHeight();
var wdHeight = $(window).height();
if (wdHeight > elHeight) {
$el.css({'margin-top': (wdHeight - elHeight)/2});
}
$el.removeClass('vh');
$('#get-code').on('click', function() {
var email = $('input[name="email"]').val().trim();
if (!email) {
return false;
}
var email = $('input[name="email"]').val().trim();
if (!email) {
return false;
}
var $this = $(this);
var originalText = $this.text(); // Remember the original text content
var seconds = 60;
var $this = $(this);
//var originalText = $this.text(); // Remember the original text content
var originalText = "{% trans "Resend" %}";
var seconds = 60;
$this.prop('disabled', true).addClass('btn-disabled');
$this.prop('disabled', true);
$this.text(originalText + '(' + seconds + 's)');
// do a set interval, using an interval of 1000 milliseconds
// and clear it after the number of seconds counts down to 0
var interval = setInterval(function() {
// decrement the seconds and update the text
seconds = seconds - 1;
$this.text(originalText + '(' + seconds + 's)');
// do a set interval, using an interval of 1000 milliseconds
// and clear it after the number of seconds counts down to 0
var interval = setInterval(function() {
// decrement the seconds and update the text
seconds = seconds - 1;
$this.text(originalText + ' (' + seconds + 's)');
if (seconds === 0) { // once seconds is 0...
$this.prop('disabled', false).removeClass('btn-disabled')
.text(originalText); // reset to original text
clearInterval(interval); // clear interval
}
}, 1000);
if (seconds === 0) { // once seconds is 0...
$this.prop('disabled', false)
.text(originalText); // reset to original text
clearInterval(interval); // clear interval
}
}, 1000);
$.ajax({
url: "{% url "ajax_get_link_email_audit_code" %}",
type: 'POST',
cache: false,
beforeSend: prepareCSRFToken,
data: {
token: "{{token}}",
email: email
},
success: function() {
feedback("{% trans "A verification code has been sent to the email." %}", 'success');
},
error: function(xhr) {
var error_msg = prepareAjaxErrorMsg(xhr);
$('.error', $this.closest('form')).html(error_msg).removeClass('hide');
$this.prop('disabled', false).removeClass('btn-disabled')
.text(originalText); // reset to original text
clearInterval(interval);
}
});
$.ajax({
url: "{% url "ajax_get_link_email_audit_code" %}",
type: 'POST',
cache: false,
beforeSend: prepareCSRFToken,
data: {
token: "{{token}}",
email: email
},
success: function() {
feedback("{% trans "A verification code has been sent to the email address." %}", 'success');
},
error: function(xhr) {
var error_msg = prepareAjaxErrorMsg(xhr);
$('.error', $this.closest('form')).html(error_msg).removeClass('hide');
$this.prop('disabled', false)
.text(originalText); // reset to original text
clearInterval(interval);
}
});
});
$('#link-audit-form').on('submit', function() {
var email = $('[name="email"]').val().trim();
var code = $('[name="code"]').val().trim();
if (!email || !code) {
return false;
}
$('#email-audit-form').on('submit', function() {
var email = $('[name="email"]').val().trim();
var code = $('[name="code"]').val().trim();
if (!email || !code) {
return false;
}
});
</script>
{% endblock %}

View File

@ -7,6 +7,5 @@ urlpatterns = [
path('link/save/', save_shared_link, name='save_shared_link'),
path('link/export-excel/', export_shared_link, name='export_shared_link'),
path('ajax/private-share-dir/', ajax_private_share_dir, name='ajax_private_share_dir'),
path('ajax/get-link-audit-code/', ajax_get_link_audit_code, name='ajax_get_link_audit_code'),
path('ajax/get-link-email-audit-code/', ajax_get_link_email_audit_code, name='ajax_get_link_email_audit_code'),
]

View File

@ -344,56 +344,6 @@ def ajax_private_share_dir(request):
data = json.dumps({"error": _("Please check the email(s) you entered")})
return HttpResponse(data, status=400, content_type=content_type)
def ajax_get_link_audit_code(request):
"""
Generate a token, and record that token with email in cache, expires in
one hour, send token to that email address.
User provide token and email at share link page, if the token and email
are valid, record that email in session.
"""
content_type = 'application/json; charset=utf-8'
token = request.POST.get('token')
email = request.POST.get('email')
if not is_valid_email(email):
return HttpResponse(json.dumps({
'error': _('Email address is not valid')
}), status=400, content_type=content_type)
dfs = FileShare.objects.get_valid_file_link_by_token(token)
ufs = UploadLinkShare.objects.get_valid_upload_link_by_token(token)
fs = dfs if dfs else ufs
if fs is None:
return HttpResponse(json.dumps({
'error': _('Share link is not found')
}), status=400, content_type=content_type)
cache_key = normalize_cache_key(email, 'share_link_audit_')
code = gen_token(max_length=6)
cache.set(cache_key, code, SHARE_LINK_AUDIT_CODE_TIMEOUT)
# send code to user via email
subject = _("Verification code for visiting share links")
c = {'code': code}
send_success = send_html_email_with_dj_template(email,
subject=subject,
dj_template='share/audit_code_email.html',
context=c)
if not send_success:
logger.error('Failed to send audit code via email to %s')
return HttpResponse(json.dumps({
"error": _("Failed to send a verification code, please try again later.")
}), status=500, content_type=content_type)
return HttpResponse(json.dumps({'success': True}), status=200,
content_type=content_type)
def ajax_get_link_email_audit_code(request):
content_type = 'application/json; charset=utf-8'
@ -422,7 +372,7 @@ def ajax_get_link_email_audit_code(request):
cache.set(cache_key, code, 60 * 60)
# send code to user via email
subject = _("Verification code for visiting share links")
subject = _("The verification code")
c = {'code': code}
send_success = send_html_email_with_dj_template(email,

View File

@ -5,11 +5,13 @@
<div style="padding:30px 55px 40px;min-height:300px;border-top:1px solid #efefef;background:transparent url('{{ url_base }}{{media_url}}img/email_bg.jpg') scroll repeat-x center top;">
{% block email_con %}{% endblock %}
<p style="font-size:14px;color:#434144;margin:30px 0;">
{% block thanks %}
<p style="font-size:14px;color:#434144;margin-top:30px;">
{% trans "Thanks for using our site!" %}
</p>
{% endblock %}
<p style="font-size:14px;color:#434144;">
<p style="font-size:14px;color:#434144;margin-top:30px;">
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
</p>
</div>

View File

@ -2,9 +2,7 @@
{% load i18n %}
{% block sub_title %}{% trans "Log In" %} - {% endblock %}
{% block header_css_class %}hide{% endblock %}
{% block extra_base_style %}
<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/seafile-ui.css" />
{% endblock %}