added template, to remove redundant code
This commit is contained in:
parent
53dd7fcc0a
commit
cd4d4fcc8c
|
@ -195,7 +195,9 @@ class Setting(db.Model):
|
||||||
'pwd_min_digits' : 2,
|
'pwd_min_digits' : 2,
|
||||||
'pwd_min_special' : 1,
|
'pwd_min_special' : 1,
|
||||||
'pwd_must_not_contain' : 'username,firstname',
|
'pwd_must_not_contain' : 'username,firstname',
|
||||||
'max_history_records': 1000
|
'max_history_records': 1000,
|
||||||
|
'zxcvbn_enabled': True,
|
||||||
|
'zxcvbn_guesses_log' : 11
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, id=None, name=None, value=None):
|
def __init__(self, id=None, name=None, value=None):
|
||||||
|
|
|
@ -1356,13 +1356,17 @@ def setting_authentication():
|
||||||
'local_db_enabled') else False
|
'local_db_enabled') else False
|
||||||
signup_enabled = True if request.form.get(
|
signup_enabled = True if request.form.get(
|
||||||
'signup_enabled', ) else False
|
'signup_enabled', ) else False
|
||||||
min_len = int(request.form.get('min_len'))
|
password_package_enabled = request.form.get('zxcvbn')
|
||||||
min_lowercase = int(request.form.get('min_lowercase'))
|
if password_package_enabled is None:
|
||||||
min_uppercase = int(request.form.get('min_uppercase'))
|
min_len = int(request.form.get('min_len'))
|
||||||
min_digits = int(request.form.get('min_digits'))
|
min_lowercase = int(request.form.get('min_lowercase'))
|
||||||
min_special = int(request.form.get('min_special'))
|
min_uppercase = int(request.form.get('min_uppercase'))
|
||||||
must_not_contain = request.form.get('must_not_contain')
|
min_digits = int(request.form.get('min_digits'))
|
||||||
|
min_special = int(request.form.get('min_special'))
|
||||||
|
must_not_contain = request.form.get('must_not_contain')
|
||||||
|
else:
|
||||||
|
Setting().set('zxcvbn_enabled', True)
|
||||||
|
|
||||||
if not has_an_auth_method(local_db_enabled=local_db_enabled):
|
if not has_an_auth_method(local_db_enabled=local_db_enabled):
|
||||||
result = {
|
result = {
|
||||||
'status':
|
'status':
|
||||||
|
@ -1371,15 +1375,19 @@ def setting_authentication():
|
||||||
'Must have at least one authentication method enabled.'
|
'Must have at least one authentication method enabled.'
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
Setting().set('local_db_enabled', local_db_enabled)
|
if password_package_enabled is None:
|
||||||
Setting().set('signup_enabled', signup_enabled)
|
Setting().set('local_db_enabled', local_db_enabled)
|
||||||
Setting().set('pwd_min_len', min_len)
|
Setting().set('signup_enabled', signup_enabled)
|
||||||
Setting().set('pwd_min_lowercase', min_lowercase)
|
Setting().set('pwd_min_len', min_len)
|
||||||
Setting().set('pwd_min_uppercase', min_uppercase)
|
Setting().set('pwd_min_lowercase', min_lowercase)
|
||||||
Setting().set('pwd_min_digits', min_digits)
|
Setting().set('pwd_min_uppercase', min_uppercase)
|
||||||
Setting().set('pwd_min_special', min_special)
|
Setting().set('pwd_min_digits', min_digits)
|
||||||
Setting().set('pwd_must_not_contain', must_not_contain)
|
Setting().set('pwd_min_special', min_special)
|
||||||
|
Setting().set('pwd_must_not_contain', must_not_contain)
|
||||||
|
Setting().set('zxcvbn_enabled', False)
|
||||||
|
else:
|
||||||
|
Setting().set('zxcvbn_enabled', True)
|
||||||
|
|
||||||
result = {'status': True, 'msg': 'Saved successfully'}
|
result = {'status': True, 'msg': 'Saved successfully'}
|
||||||
elif conf_type == 'ldap':
|
elif conf_type == 'ldap':
|
||||||
ldap_enabled = True if request.form.get('ldap_enabled') else False
|
ldap_enabled = True if request.form.get('ldap_enabled') else False
|
||||||
|
|
|
@ -7,8 +7,9 @@ import ipaddress
|
||||||
from distutils.util import strtobool
|
from distutils.util import strtobool
|
||||||
from yaml import Loader, load
|
from yaml import Loader, load
|
||||||
from onelogin.saml2.utils import OneLogin_Saml2_Utils
|
from onelogin.saml2.utils import OneLogin_Saml2_Utils
|
||||||
from flask import Blueprint, render_template, make_response, url_for, current_app, g, session, request, redirect, abort
|
from flask import Blueprint, render_template, make_response, url_for, current_app, g, session, request, redirect, abort, jsonify
|
||||||
from flask_login import login_user, logout_user, login_required, current_user
|
from flask_login import login_user, logout_user, login_required, current_user
|
||||||
|
from zxcvbn import zxcvbn
|
||||||
|
|
||||||
from .base import login_manager
|
from .base import login_manager
|
||||||
from ..lib import utils
|
from ..lib import utils
|
||||||
|
@ -676,14 +677,63 @@ def password_quality_check(user, password):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@index_bp.route('/ratepass', methods=['POST'])
|
@index_bp.route('/ratepassword', methods=['POST'])
|
||||||
def rate_password():
|
def rate_password():
|
||||||
|
# print("\n\nGot pass = ", passwd)
|
||||||
|
# result = zxcvbn(pwd, user_inputs=[wordlist])
|
||||||
|
fname = request.form['fname']
|
||||||
|
lname = request.form['lname']
|
||||||
|
email = request.form['email']
|
||||||
|
username = request.form['username']
|
||||||
|
password = request.form['password']
|
||||||
|
inputs = []
|
||||||
|
for i in [fname, lname, email, username]:
|
||||||
|
if len(i) != 0:
|
||||||
|
inputs.append(i)
|
||||||
|
if len(password) == 0:
|
||||||
|
return make_response(
|
||||||
|
jsonify({
|
||||||
|
'msg' : 'no-passwd',
|
||||||
|
'feedback': '',
|
||||||
|
'valid' : 'false',
|
||||||
|
'strength': ''
|
||||||
|
}), 200)
|
||||||
|
|
||||||
|
result = zxcvbn(password, user_inputs=inputs)
|
||||||
|
defined_guesses_log = 11
|
||||||
|
# attrubutes to return as json
|
||||||
|
feedback = []
|
||||||
|
rate = result['guesses_log10']/defined_guesses_log
|
||||||
|
if rate < 0.5:
|
||||||
|
strength = "very weak"
|
||||||
|
if rate < 0.6:
|
||||||
|
strength = "weak"
|
||||||
|
elif rate < 1:
|
||||||
|
strength = "medium"
|
||||||
|
else:
|
||||||
|
strength = "strong"
|
||||||
|
|
||||||
|
if result['guesses_log10'] < defined_guesses_log:
|
||||||
|
feedback.append("Add more complexity to your password")
|
||||||
|
for s in result['sequence']:
|
||||||
|
if s['pattern'] == 'dictionary':
|
||||||
|
if s['dictionary_name'] == 'user_inputs':
|
||||||
|
feedback.append("Your password must not contain parts of your firstname, lastname, username or email")
|
||||||
|
break
|
||||||
|
for s in result['sequence']:
|
||||||
|
if s['pattern'] == 'dictionary' and s['dictionary_name'] != 'user_inputs':
|
||||||
|
feedback.append("Your password contains one or more words which exist in common wordlists.")
|
||||||
|
break
|
||||||
|
# in case complexity is high but feedback is still given, then downgrade to 'medium'
|
||||||
|
if strength == "strong" and (len(feedback) != 0 or result['score'] < 4):
|
||||||
|
strength = "medium"
|
||||||
|
return make_response(
|
||||||
|
jsonify({
|
||||||
|
'feedback': feedback,
|
||||||
|
'strength': strength,
|
||||||
|
'valid' : 'true' if strength == 'strong' and len(feedback) == 0 else 'false'
|
||||||
|
}), 200)
|
||||||
|
|
||||||
username = request.form.get('username')
|
|
||||||
fname = request.form.get('fname')
|
|
||||||
lname = request.form.get('name')
|
|
||||||
email = request.form.get('email')
|
|
||||||
|
|
||||||
|
|
||||||
@index_bp.route('/register', methods=['GET', 'POST'])
|
@index_bp.route('/register', methods=['GET', 'POST'])
|
||||||
def register():
|
def register():
|
||||||
|
|
|
@ -27,8 +27,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
window.onload = function() {
|
window.onload = function() {
|
||||||
ldapSelection();
|
ldapSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -76,27 +77,39 @@
|
||||||
<legend>Password Policy </br>(database authentication)</legend>
|
<legend>Password Policy </br>(database authentication)</legend>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="min_len">Minimum length</label>
|
<label for="min_len">Minimum length</label>
|
||||||
<input type="text" class="form-control" name="min_len" id="min_len" value="{{ SETTING.get('pwd_min_len') }}">
|
<input type="text" class="form-control char_specified" name="min_len" id="min_len" value="{{ SETTING.get('pwd_min_len') }}"
|
||||||
|
{% if SETTING.get('zxcvbn_enabled')== true %}disabled{% endif %}>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="min_lowercase">Minimum number of lowercase letters</label>
|
<label for="min_lowercase">Minimum number of lowercase letters</label>
|
||||||
<input type="text" class="form-control" name="min_lowercase" id="min_lowercase" value="{{ SETTING.get('pwd_min_lowercase') }}">
|
<input type="text" class="form-control char_specified" name="min_lowercase" id="min_lowercase" value="{{ SETTING.get('pwd_min_lowercase') }}"
|
||||||
|
{% if SETTING.get('zxcvbn_enabled')== true %}disabled{% endif %}>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="min_uppercase">Minimum number of uppercase letters</label>
|
<label for="min_uppercase">Minimum number of uppercase letters</label>
|
||||||
<input type="text" class="form-control" name="min_uppercase" id="min_uppercase" value="{{ SETTING.get('pwd_min_uppercase') }}">
|
<input type="text" class="form-control char_specified" name="min_uppercase" id="min_uppercase" value="{{ SETTING.get('pwd_min_uppercase') }}"
|
||||||
|
{% if SETTING.get('zxcvbn_enabled')== true %}disabled{% endif %}>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="min_digits">Minimum number of digits</label>
|
<label for="min_digits">Minimum number of digits</label>
|
||||||
<input type="text" class="form-control" name="min_digits" id="min_digits" value="{{ SETTING.get('pwd_min_digits') }}">
|
<input type="text" class="form-control char_specified" name="min_digits" id="min_digits" value="{{ SETTING.get('pwd_min_digits') }}"
|
||||||
|
{% if SETTING.get('zxcvbn_enabled')== true %}disabled{% endif %}>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="min_special">Minimum number of special characters</label>
|
<label for="min_special">Minimum number of special characters</label>
|
||||||
<input type="text" class="form-control" name="min_special" id="min_special" value="{{ SETTING.get('pwd_min_special') }}">
|
<input type="text" class="form-control char_specified" name="min_special" id="min_special" value="{{ SETTING.get('pwd_min_special') }}"
|
||||||
|
{% if SETTING.get('zxcvbn_enabled')== true %}disabled{% endif %}>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="must_not_contain">Must not contain</label>
|
<label for="must_not_contain">Must not contain</label>
|
||||||
<input type="text" class="form-control" name="must_not_contain" id="must_not_contain" placeholder="username,firstname,lastname,email" value="{{ SETTING.get('pwd_must_not_contain') }}">
|
<input type="text" class="form-control char_specified" name="must_not_contain" id="must_not_contain" placeholder="username,firstname,lastname,email" value="{{ SETTING.get('pwd_must_not_contain') }}"
|
||||||
|
{% if SETTING.get('zxcvbn_enabled')== true %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<label for="zxcvbn">Use zxcvbn python package</label>
|
||||||
|
<input type="checkbox" class="checkbox" name="zxcvbn" id="zxcvbn" onclick="javascript:passPolicySelection();" {% if SETTING.get('zxcvbn_enabled')== true %}checked{% endif %}>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<button type="submit" class="btn btn-flat btn-primary">Save</button>
|
<button type="submit" class="btn btn-flat btn-primary">Save</button>
|
||||||
|
@ -745,7 +758,21 @@
|
||||||
{%- endassets %}
|
{%- endassets %}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
$('input').on('ifChecked', function(event){
|
||||||
|
// alert(event.type + ' callback');
|
||||||
|
$(".form-control.char_specified").prop("disabled", true)
|
||||||
|
});
|
||||||
|
$('input').on('ifUnchecked', function(event){
|
||||||
|
// alert(event.type + ' callback');
|
||||||
|
$(".form-control.char_specified").prop("disabled", false)
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#zxcvbn').iCheck({
|
||||||
|
checkboxClass : 'icheckbox_square-blue',
|
||||||
|
increaseArea : '20%'
|
||||||
|
})
|
||||||
|
|
||||||
$(function() {
|
$(function() {
|
||||||
$('#tabs').tabs({
|
$('#tabs').tabs({
|
||||||
// add url anchor tags
|
// add url anchor tags
|
||||||
|
|
5
powerdnsadmin/templates/password_policy.html
Normal file
5
powerdnsadmin/templates/password_policy.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{% macro password_polic(zxcvbn_enabled) -%}
|
||||||
|
{{ caller() }}
|
||||||
|
|
||||||
|
|
||||||
|
{%- endmacro %}
|
|
@ -64,7 +64,9 @@
|
||||||
<div id="pass-feedback" class="form-group">
|
<div id="pass-feedback" class="form-group">
|
||||||
<input type="password" class="form-control" placeholder="Password" id="password" name="password"
|
<input type="password" class="form-control" placeholder="Password" id="password" name="password"
|
||||||
required>
|
required>
|
||||||
|
{% if SETTING.get('zxcvbn_enabled') == true %}
|
||||||
<small class="help-block" id="password-text"></small> <br>
|
<small class="help-block" id="password-text"></small> <br>
|
||||||
|
{% endif %}
|
||||||
<div id="policy-err" style='color: #df5948;'></div>
|
<div id="policy-err" style='color: #df5948;'></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group has-feedback">
|
<div class="form-group has-feedback">
|
||||||
|
@ -106,6 +108,7 @@
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
{% if SETTING.get('zxcvbn_enabled') == false %}
|
||||||
// handling password complexity requirements message
|
// handling password complexity requirements message
|
||||||
$(':input').on('keyup', function() {
|
$(':input').on('keyup', function() {
|
||||||
|
|
||||||
|
@ -170,15 +173,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% if use_package %}
|
|
||||||
var csrftoken = $('meta[name=csrf-token]').attr('content')
|
var csrftoken = $('meta[name=csrf-token]').attr('content')
|
||||||
|
|
||||||
$.ajaxSetup({
|
$.ajaxSetup({
|
||||||
|
@ -206,10 +201,17 @@
|
||||||
console.log("Resp = " , response)
|
console.log("Resp = " , response)
|
||||||
console.log('sccess')
|
console.log('sccess')
|
||||||
var x = document.getElementById('policy-err');
|
var x = document.getElementById('policy-err');
|
||||||
console.log(response['feedback'])
|
// x.innerHTML = response['feedback'];
|
||||||
x.innerHTML = response['feedback'] // response['feedback']
|
x.innerHTML = "<ul>";
|
||||||
|
for (let i = 0; i < response['feedback'].length; i++) {
|
||||||
|
x.innerHTML += "<li>" + response['feedback'][i] + "</li>";
|
||||||
|
}
|
||||||
|
x.innerHTML += "</ul>"
|
||||||
var strength;
|
var strength;
|
||||||
switch (response['strength']) {
|
switch (response['strength']) {
|
||||||
|
case '':
|
||||||
|
strength = ''; // no password was given
|
||||||
|
break;
|
||||||
case 'very weak':
|
case 'very weak':
|
||||||
strength = "<small class='progress-bar bg-danger' style='background-color: #a50021; width: 25%'>Very weak</small>";
|
strength = "<small class='progress-bar bg-danger' style='background-color: #a50021; width: 25%'>Very weak</small>";
|
||||||
break;
|
break;
|
||||||
|
@ -225,6 +227,16 @@
|
||||||
}
|
}
|
||||||
var y = document.getElementById('password-text')
|
var y = document.getElementById('password-text')
|
||||||
y.innerHTML = strength;
|
y.innerHTML = strength;
|
||||||
|
|
||||||
|
if (response['feedback'] != "") {
|
||||||
|
document.getElementById('register').disabled = true;
|
||||||
|
// $('#pass-feedback').addClass("has-error");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
document.getElementById('register').disabled = false;
|
||||||
|
// $('#pass-feedback').addClass("has-success");
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
error: function(xhr) {
|
error: function(xhr) {
|
||||||
console.log("Ajax call to rate pass, has failed")
|
console.log("Ajax call to rate pass, has failed")
|
||||||
|
@ -235,7 +247,7 @@
|
||||||
// handling password complexity requirements message
|
// handling password complexity requirements message
|
||||||
$(':input').on('keyup', function() {
|
$(':input').on('keyup', function() {
|
||||||
|
|
||||||
var seconds = 1.5;
|
var seconds = 1;
|
||||||
if (timer == null) { // if user typed sth and timer is not running, then start one
|
if (timer == null) { // if user typed sth and timer is not running, then start one
|
||||||
timer = setTimeout(send_pass, seconds*1000);
|
timer = setTimeout(send_pass, seconds*1000);
|
||||||
}
|
}
|
||||||
|
|
|
@ -174,6 +174,9 @@
|
||||||
applyChanges(postdata, $SCRIPT_ROOT + '/user/profile', false, true);
|
applyChanges(postdata, $SCRIPT_ROOT + '/user/profile', false, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{% if SETTING.get('zxcvbn_enabled') == false %}
|
||||||
// handling password complexity requirements message and password comparison
|
// handling password complexity requirements message and password comparison
|
||||||
$(':input').on('keyup', function() {
|
$(':input').on('keyup', function() {
|
||||||
var rpass = document.getElementById('rpassword').value;
|
var rpass = document.getElementById('rpassword').value;
|
||||||
|
@ -237,5 +240,82 @@
|
||||||
document.getElementById('pwd-submit').disabled = false;
|
document.getElementById('pwd-submit').disabled = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
var timer = null;
|
||||||
|
function send_pass() {
|
||||||
|
var fname = document.getElementById('firstname').value;
|
||||||
|
var lname = document.getElementById('lastname').value;
|
||||||
|
var email = document.getElementById('email').value;
|
||||||
|
var username = document.getElementById('username').value;
|
||||||
|
var password = document.getElementById('password').value;
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: "/ratepassword",
|
||||||
|
// headers: { "X-CSRFToken": getCookie("csrftoken") },
|
||||||
|
type: "post",
|
||||||
|
data : {'fname': fname, 'lname': lname, 'email' : email, 'username' : username, 'password': password},
|
||||||
|
success: function(response) {
|
||||||
|
console.log('Submission was successful.');
|
||||||
|
console.log("Resp = " , response)
|
||||||
|
console.log('sccess')
|
||||||
|
var x = document.getElementById('policy-err');
|
||||||
|
// x.innerHTML = response['feedback'];
|
||||||
|
x.innerHTML = "<ul>";
|
||||||
|
for (let i = 0; i < response['feedback'].length; i++) {
|
||||||
|
x.innerHTML += "<li>" + response['feedback'][i] + "</li>";
|
||||||
|
}
|
||||||
|
x.innerHTML += "</ul>"
|
||||||
|
var strength;
|
||||||
|
switch (response['strength']) {
|
||||||
|
case '':
|
||||||
|
strength = ''; // no password was given
|
||||||
|
break;
|
||||||
|
case 'very weak':
|
||||||
|
strength = "<small class='progress-bar bg-danger' style='background-color: #a50021; width: 25%'>Very weak</small>";
|
||||||
|
break;
|
||||||
|
case 'weak':
|
||||||
|
strength = "<small class='progress-bar bg-danger' style='background-color: #f7a73e;width: 50%'>Weak</small>";
|
||||||
|
break;
|
||||||
|
case 'medium':
|
||||||
|
strength = "<small class='progress-bar bg-warning' style='background-color: #a0cb89; width: 75%'>Medium</small>";
|
||||||
|
break;
|
||||||
|
case 'strong':
|
||||||
|
strength = "<small class='progress-bar bg-success' style='background-color: #2e8b57; width: 100%'>Strong</small>";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var y = document.getElementById('password-text')
|
||||||
|
y.innerHTML = strength;
|
||||||
|
|
||||||
|
if (response['feedback'] != "") {
|
||||||
|
document.getElementById('register').disabled = true;
|
||||||
|
// $('#pass-feedback').addClass("has-error");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
document.getElementById('register').disabled = false;
|
||||||
|
// $('#pass-feedback').addClass("has-success");
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
error: function(xhr) {
|
||||||
|
console.log("Ajax call to rate pass, has failed")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
timer = null; // turn the timer off
|
||||||
|
}
|
||||||
|
// handling password complexity requirements message
|
||||||
|
$(':input').on('keyup', function() {
|
||||||
|
|
||||||
|
var seconds = 1;
|
||||||
|
if (timer == null) { // if user typed sth and timer is not running, then start one
|
||||||
|
timer = setTimeout(send_pass, seconds*1000);
|
||||||
|
}
|
||||||
|
else { // if user typed sth and timer is still up and running,then reset timer
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = null;
|
||||||
|
timer = setTimeout(send_pass, seconds*1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
{% endif %}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -28,3 +28,4 @@ PyYAML==5.4
|
||||||
Flask-SSLify==0.1.5
|
Flask-SSLify==0.1.5
|
||||||
Flask-Mail==0.9.1
|
Flask-Mail==0.9.1
|
||||||
flask-session==0.3.2
|
flask-session==0.3.2
|
||||||
|
zxcvbn==4.4.28
|
Loading…
Reference in a new issue