mirror of
https://github.com/hrfee/jellyfin-accounts.git
synced 2024-10-31 19:40:10 +00:00
added password validation; changed create account layout
Password validation added, configurable under the [password_validation] section in config.ini. Each criterion is displayed next to the form, in red or green depending on whether the password passes it. form.html now looks different because of it, whether it is enabled or disabled. An error message is now displayed if the user already exists.
This commit is contained in:
parent
0ab93990e8
commit
690de58e9f
12
README.md
12
README.md
@ -102,6 +102,18 @@ help_message = Enter your details to create an account.
|
|||||||
; Displayed when an account is created.
|
; Displayed when an account is created.
|
||||||
success_message = Your account has been created. Click below to continue to Jellyfin.
|
success_message = Your account has been created. Click below to continue to Jellyfin.
|
||||||
|
|
||||||
|
[password_validation]
|
||||||
|
; Enables password validation.
|
||||||
|
enabled = true
|
||||||
|
; Min. password length
|
||||||
|
min_length = 8
|
||||||
|
; Min. number of uppercase characters
|
||||||
|
upper = 1
|
||||||
|
; Min. number of numbers
|
||||||
|
number = 1
|
||||||
|
; Min. number of special characters
|
||||||
|
special = 0
|
||||||
|
|
||||||
[email]
|
[email]
|
||||||
; Enable to store provided email addresses, monitor jellyfin directory for pw-resets, and send pin
|
; Enable to store provided email addresses, monitor jellyfin directory for pw-resets, and send pin
|
||||||
enabled = true
|
enabled = true
|
||||||
|
@ -23,6 +23,18 @@ help_message = Enter your details to create an account.
|
|||||||
; Displayed when an account is created.
|
; Displayed when an account is created.
|
||||||
success_message = Your account has been created. Click below to continue to Jellyfin.
|
success_message = Your account has been created. Click below to continue to Jellyfin.
|
||||||
|
|
||||||
|
[password_validation]
|
||||||
|
; Enables password validation.
|
||||||
|
enabled = true
|
||||||
|
; Min. password length
|
||||||
|
min_length = 8
|
||||||
|
; Min. number of uppercase characters
|
||||||
|
upper = 1
|
||||||
|
; Min. number of numbers
|
||||||
|
number = 1
|
||||||
|
; Min. number of special characters
|
||||||
|
special = 0
|
||||||
|
|
||||||
[email]
|
[email]
|
||||||
; Enable to store provided email addresses, monitor jellyfin directory for pw-resets, and send pin
|
; Enable to store provided email addresses, monitor jellyfin directory for pw-resets, and send pin
|
||||||
enabled = true
|
enabled = true
|
||||||
|
@ -10,20 +10,16 @@
|
|||||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-serialize-object/2.5.0/jquery.serialize-object.min.js" integrity="sha256-E8KRdFk/LTaaCBoQIV/rFNc0s3ICQQiOHFT4Cioifa8=" crossorigin="anonymous"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-serialize-object/2.5.0/jquery.serialize-object.min.js" integrity="sha256-E8KRdFk/LTaaCBoQIV/rFNc0s3ICQQiOHFT4Cioifa8=" crossorigin="anonymous"></script>
|
||||||
<style>
|
<style>
|
||||||
.pageTitle {
|
.pageContainer {
|
||||||
margin: 20%;
|
margin: 20%;
|
||||||
margin-bottom: 5%;
|
|
||||||
}
|
|
||||||
.accountForm {
|
|
||||||
margin: 20%;
|
|
||||||
margin-bottom: 5%;
|
|
||||||
margin-top: 5%;
|
|
||||||
}
|
}
|
||||||
.contactBox {
|
.contactBox {
|
||||||
margin: 20%;
|
|
||||||
margin-top: 5%;
|
|
||||||
color: grey;
|
color: grey;
|
||||||
}
|
}
|
||||||
|
#container {
|
||||||
|
margin-top: 5%;
|
||||||
|
margin-bottom: 5%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<title>Create Jellyfin Account</title>
|
<title>Create Jellyfin Account</title>
|
||||||
</head>
|
</head>
|
||||||
@ -43,13 +39,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pageTitle">
|
<div class="pageContainer">
|
||||||
<h1>
|
<h1>
|
||||||
Create Account
|
Create Account
|
||||||
</h1>
|
</h1>
|
||||||
<p>{{ helpMessage }}</p>
|
<p>{{ helpMessage }}</p>
|
||||||
</div>
|
<p class="contactBox">{{ contactMessage }}</p>
|
||||||
<div class="accountForm" id="accountForm">
|
<div class="container" id="container">
|
||||||
|
<div class="row" id="cardContainer">
|
||||||
|
<div class="col-sm" id="accountForm">
|
||||||
|
<div class="card bg-light mb-3">
|
||||||
|
<div class="card-header">Details</div>
|
||||||
|
<div class="card-body">
|
||||||
<form action="#" method="POST">
|
<form action="#" method="POST">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="inputEmail">Email</label>
|
<label for="inputEmail">Email</label>
|
||||||
@ -63,24 +64,101 @@
|
|||||||
<label for="inputPassword">Password</label>
|
<label for="inputPassword">Password</label>
|
||||||
<input type="password" class="form-control" id="inputPassword" name="password" placeholder="Password" required>
|
<input type="password" class="form-control" id="inputPassword" name="password" placeholder="Password" required>
|
||||||
</div>
|
</div>
|
||||||
<input type="submit" class="btn btn-primary" value="Create Account">
|
<div class="btn-group" role="group" aria-label="Button & Error" id="errorBox">
|
||||||
|
<input type="submit" class="btn btn-outline-primary" value="Create Account">
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="contactBox">
|
</div>
|
||||||
<p>{{ contactMessage }}</p>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
var code = window.location.href.split('/').pop();
|
||||||
|
$.ajax('/getRequirements', {
|
||||||
|
data : JSON.stringify({ 'code': code }),
|
||||||
|
contentType : 'application/json',
|
||||||
|
type : 'POST',
|
||||||
|
crossDomain : true,
|
||||||
|
complete : function(response){
|
||||||
|
var data = response['responseJSON'];
|
||||||
|
if (Object.keys(data).length > 1) {
|
||||||
|
var col = document.createElement('div');
|
||||||
|
col.classList.add('col-sm');
|
||||||
|
col.id = 'requirementBox';
|
||||||
|
var card = document.createElement('div');
|
||||||
|
card.classList.add('card', 'bg-light', 'mb-3', 'requirementBox');
|
||||||
|
// card.setAttribute('style', 'max-width: 18rem;');
|
||||||
|
var header = document.createElement('div');
|
||||||
|
header.classList.add('card-header');
|
||||||
|
header.appendChild(document.createTextNode('Password Requirements'));
|
||||||
|
card.appendChild(header);
|
||||||
|
var body = document.createElement('div');
|
||||||
|
body.classList.add('card-body');
|
||||||
|
var listGroup = document.createElement('ul');
|
||||||
|
listGroup.classList.add('list-group');
|
||||||
|
listGroup.id = 'requirements';
|
||||||
|
for (var key in data) {
|
||||||
|
if (data.hasOwnProperty(key)) {
|
||||||
|
var criterion = document.createElement('li');
|
||||||
|
criterion.id = key;
|
||||||
|
criterion.classList.add('list-group-item', 'list-group-item-danger');
|
||||||
|
var text = document.createElement('div');
|
||||||
|
text.appendChild(document.createTextNode(' ' + data[key]));
|
||||||
|
criterion.appendChild(text);
|
||||||
|
listGroup.appendChild(criterion);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
body.appendChild(listGroup);
|
||||||
|
card.appendChild(body);
|
||||||
|
col.appendChild(card);
|
||||||
|
document.getElementById('cardContainer').appendChild(col);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
$("form").submit(function() {
|
$("form").submit(function() {
|
||||||
var send = $("form").serializeObject();
|
var send = $("form").serializeObject();
|
||||||
send['code'] = window.location.href.split('/').pop();
|
send['code'] = code;
|
||||||
send = JSON.stringify(send);
|
send = JSON.stringify(send);
|
||||||
$.ajax('/newUser', {
|
$.ajax('/newUser', {
|
||||||
data : send,
|
data : send,
|
||||||
contentType : 'application/json',
|
contentType : 'application/json',
|
||||||
type : 'POST',
|
type : 'POST',
|
||||||
crossDomain: true,
|
crossDomain : true,
|
||||||
success : function(){
|
complete : function(response){
|
||||||
|
var data = response['responseJSON'];
|
||||||
|
if ('error' in data) {
|
||||||
|
var text = document.createTextNode(data['error']);
|
||||||
|
// <div class="alert alert-danger" id="errorBox"></div>
|
||||||
|
var error = document.createElement('button');
|
||||||
|
error.classList.add('btn', 'btn-outline-danger');
|
||||||
|
error.setAttribute('disabled', '');
|
||||||
|
error.appendChild(text);
|
||||||
|
document.getElementById('errorBox').appendChild(error);
|
||||||
|
} else {
|
||||||
|
var valid = true
|
||||||
|
for (var key in data) {
|
||||||
|
if (data.hasOwnProperty(key)) {
|
||||||
|
var criterion = document.getElementById(key);
|
||||||
|
if (data[key] == false) {
|
||||||
|
valid = false;
|
||||||
|
if (criterion.classList.contains('list-group-item-success')) {
|
||||||
|
criterion.classList.remove('list-group-item-success');
|
||||||
|
criterion.classList.add('list-group-item-danger');
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
if (criterion.classList.contains('list-group-item-danger')) {
|
||||||
|
criterion.classList.remove('list-group-item-danger');
|
||||||
|
criterion.classList.add('list-group-item-success');
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
if (valid == true) {
|
||||||
$('#successBox').modal('show');
|
$('#successBox').modal('show');
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
|
BIN
images/admin.png
BIN
images/admin.png
Binary file not shown.
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 42 KiB |
Binary file not shown.
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 70 KiB |
45
jellyfin_accounts/validate_password.py
Normal file
45
jellyfin_accounts/validate_password.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
specials = ['[', '@', '_', '!', '#', '$', '%', '^', '&', '*', '(', ')',
|
||||||
|
'<', '>', '?', '/', '\\', '|', '}', '{', '~', ':', ']']
|
||||||
|
|
||||||
|
class PasswordValidator:
|
||||||
|
def __init__(self, min_length, upper, number, special):
|
||||||
|
self.criteria = {'characters': int(min_length),
|
||||||
|
'uppercase characters': int(upper),
|
||||||
|
'numbers': int(number),
|
||||||
|
'special characters': int(special)}
|
||||||
|
def validate(self, password):
|
||||||
|
count = {'characters': 0,
|
||||||
|
'uppercase characters': 0,
|
||||||
|
'numbers': 0,
|
||||||
|
'special characters': 0}
|
||||||
|
for c in password:
|
||||||
|
count['characters'] += 1
|
||||||
|
if c.isupper():
|
||||||
|
count['uppercase characters'] += 1
|
||||||
|
elif c.isnumeric():
|
||||||
|
count['numbers'] += 1
|
||||||
|
elif c in specials:
|
||||||
|
count['special characters'] += 1
|
||||||
|
for criterion in count:
|
||||||
|
if count[criterion] < self.criteria[criterion]:
|
||||||
|
count[criterion] = False
|
||||||
|
else:
|
||||||
|
count[criterion] = True
|
||||||
|
return count
|
||||||
|
def getCriteria(self):
|
||||||
|
lines = {}
|
||||||
|
for criterion in self.criteria:
|
||||||
|
min = self.criteria[criterion]
|
||||||
|
if min > 0:
|
||||||
|
text = f"Must have at least {min} "
|
||||||
|
if min == 1:
|
||||||
|
text += criterion[:-1]
|
||||||
|
else:
|
||||||
|
text += criterion
|
||||||
|
lines[criterion] = text
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
|||||||
from flask import request, jsonify
|
from flask import request, jsonify
|
||||||
|
from configparser import RawConfigParser
|
||||||
from jellyfin_accounts.jf_api import Jellyfin
|
from jellyfin_accounts.jf_api import Jellyfin
|
||||||
import json
|
import json
|
||||||
import datetime
|
import datetime
|
||||||
import secrets
|
import secrets
|
||||||
import time
|
import time
|
||||||
from __main__ import config, app, g
|
from __main__ import config, config_path, app, g
|
||||||
from __main__ import web_log as log
|
from __main__ import web_log as log
|
||||||
from jellyfin_accounts.login import auth
|
from jellyfin_accounts.login import auth
|
||||||
|
from jellyfin_accounts.validate_password import PasswordValidator
|
||||||
|
|
||||||
def resp(success=True, code=500):
|
def resp(success=True, code=500):
|
||||||
if success:
|
if success:
|
||||||
@ -57,20 +59,52 @@ while attempts != 3:
|
|||||||
log.info(('Successfully authenticated with ' +
|
log.info(('Successfully authenticated with ' +
|
||||||
config['jellyfin']['server']))
|
config['jellyfin']['server']))
|
||||||
break
|
break
|
||||||
except jellyfin_accounts.jf_api.AuthenticationError:
|
except Jellyfin.AuthenticationError:
|
||||||
attempts += 1
|
attempts += 1
|
||||||
log.error(('Failed to authenticate with ' +
|
log.error(('Failed to authenticate with ' +
|
||||||
config['jellyfin']['server'] +
|
config['jellyfin']['server'] +
|
||||||
'. Retrying...'))
|
'. Retrying...'))
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
|
|
||||||
|
if config.getboolean('password_validation', 'enabled'):
|
||||||
|
validator = PasswordValidator(config['password_validation']['min_length'],
|
||||||
|
config['password_validation']['upper'],
|
||||||
|
config['password_validation']['number'],
|
||||||
|
config['password_validation']['special'])
|
||||||
|
else:
|
||||||
|
validator = PasswordValidator(0, 0, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/getRequirements', methods=['GET', 'POST'])
|
||||||
|
def getRequirements():
|
||||||
|
data = request.get_json()
|
||||||
|
log.debug('Password Requirements requested')
|
||||||
|
if checkInvite(data['code']):
|
||||||
|
return jsonify(validator.getCriteria())
|
||||||
|
|
||||||
|
|
||||||
@app.route('/newUser', methods=['GET', 'POST'])
|
@app.route('/newUser', methods=['GET', 'POST'])
|
||||||
def newUser():
|
def newUser():
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
log.debug('Attempted newUser')
|
log.debug('Attempted newUser')
|
||||||
if checkInvite(data['code'], delete=True):
|
if checkInvite(data['code']):
|
||||||
|
validation = validator.validate(data['password'])
|
||||||
|
valid = True
|
||||||
|
for criterion in validation:
|
||||||
|
if validation[criterion] == False:
|
||||||
|
valid = False
|
||||||
|
if valid:
|
||||||
|
log.debug('User password valid')
|
||||||
|
try:
|
||||||
user = jf.newUser(data['username'], data['password'])
|
user = jf.newUser(data['username'], data['password'])
|
||||||
|
except Jellyfin.UserExistsError:
|
||||||
|
error = 'User already exists with name '
|
||||||
|
error += data['username']
|
||||||
|
log.debug(error)
|
||||||
|
return jsonify({'error': error})
|
||||||
|
except:
|
||||||
|
return jsonify({'error': 'Unknown error'})
|
||||||
|
checkInvite(data['code'], delete=True)
|
||||||
if user.status_code == 200:
|
if user.status_code == 200:
|
||||||
try:
|
try:
|
||||||
with open(config['files']['user_template'], 'r') as f:
|
with open(config['files']['user_template'], 'r') as f:
|
||||||
@ -78,7 +112,6 @@ def newUser():
|
|||||||
jf.setPolicy(user.json()['Id'], default_policy)
|
jf.setPolicy(user.json()['Id'], default_policy)
|
||||||
except:
|
except:
|
||||||
log.debug('setPolicy failed')
|
log.debug('setPolicy failed')
|
||||||
pass
|
|
||||||
if config.getboolean('email', 'enabled'):
|
if config.getboolean('email', 'enabled'):
|
||||||
try:
|
try:
|
||||||
with open(config['files']['emails'], 'r') as f:
|
with open(config['files']['emails'], 'r') as f:
|
||||||
@ -90,10 +123,12 @@ def newUser():
|
|||||||
f.write(json.dumps(emails, indent=4))
|
f.write(json.dumps(emails, indent=4))
|
||||||
log.debug('Email address stored')
|
log.debug('Email address stored')
|
||||||
log.info('New User created.')
|
log.info('New User created.')
|
||||||
return resp()
|
|
||||||
else:
|
else:
|
||||||
log.error(f'New user creation failed: {user.status_code}')
|
log.error(f'New user creation failed: {user.status_code}')
|
||||||
return resp(False)
|
return resp(False)
|
||||||
|
else:
|
||||||
|
log.debug('User password invalid')
|
||||||
|
return jsonify(validation)
|
||||||
else:
|
else:
|
||||||
log.debug('Attempted newUser unauthorized')
|
log.debug('Attempted newUser unauthorized')
|
||||||
return resp(False, code=401)
|
return resp(False, code=401)
|
||||||
@ -176,4 +211,27 @@ def get_token():
|
|||||||
return jsonify({'token': token.decode('ascii')})
|
return jsonify({'token': token.decode('ascii')})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/modifyConfig', methods=['POST'])
|
||||||
|
@auth.login_required
|
||||||
|
def modifyConfig():
|
||||||
|
log.info('Config modification requested')
|
||||||
|
data = request.get_json()
|
||||||
|
temp_config = RawConfigParser(comment_prefixes='/',
|
||||||
|
allow_no_value=True)
|
||||||
|
temp_config.read(config_path)
|
||||||
|
for section in data:
|
||||||
|
if section in temp_config:
|
||||||
|
for item in data[section]:
|
||||||
|
if item in temp_config[section]:
|
||||||
|
temp_config[section][item] = data[section][item]
|
||||||
|
data[section][item] = True
|
||||||
|
log.debug(f'{section}/{item} modified')
|
||||||
|
else:
|
||||||
|
data[section][item] = False
|
||||||
|
log.debug(f'{section}/{item} does not exist in config')
|
||||||
|
with open(config_path, 'w') as config_file:
|
||||||
|
temp_config.write(config_file)
|
||||||
|
log.debug('Config written')
|
||||||
|
return jsonify(data)
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user