mirror of
https://github.com/hrfee/jellyfin-accounts.git
synced 2024-12-23 01:20:12 +00:00
Added user email modification to admin page
Pressing the user settings button brings up a list of all jellyfin users, and allows you to add or change their stored email addresses. Additionally, changed emails.json to use user ID instead of username. The program automatically converts the file to the new format at start.
This commit is contained in:
parent
e8ad3f98d6
commit
200ad24f96
@ -77,7 +77,7 @@ optional arguments:
|
|||||||
* When someone initiates forget password on Jellyfin, a file named `passwordreset*.json` is created in its configuration directory. This directory is monitored and when created, the program reads the username, expiry time and PIN, puts it into a template and sends it to whatever address is specified in `emails.json`.
|
* When someone initiates forget password on Jellyfin, a file named `passwordreset*.json` is created in its configuration directory. This directory is monitored and when created, the program reads the username, expiry time and PIN, puts it into a template and sends it to whatever address is specified in `emails.json`.
|
||||||
* **The default forget password popup references the `passwordreset*.json` file created. This is confusing for users, so a quick fix is to edit the `MessageForgotPasswordFileCreated` string in Jellyfin's language folder.**
|
* **The default forget password popup references the `passwordreset*.json` file created. This is confusing for users, so a quick fix is to edit the `MessageForgotPasswordFileCreated` string in Jellyfin's language folder.**
|
||||||
* Currently, jellyfin-accounts supports generic SSL/TLS secured SMTP, and the [mailgun](https://mailgun.com) REST API.
|
* Currently, jellyfin-accounts supports generic SSL/TLS secured SMTP, and the [mailgun](https://mailgun.com) REST API.
|
||||||
* Email html is created using [mjml](https://mjml.io), and [jinja](https://github.com/pallets/jinja) templating is used. If you wish to create your own, ensure you use the same jinja expressions (`{{ pin }}`, etc.) as used in `data/email.mjml`, and also create a plain text version for legacy email clients.
|
* Email html is created using [mjml](https://mjml.io), and [jinja](https://github.com/pallets/jinja) templating is used. If you wish to create your own, ensure you use the same jinja expressions (`{{ pin }}`, etc.) as used in `data/email.mjml` or `invite-email.mjml`, and also create plain text versions for legacy email clients.
|
||||||
|
|
||||||
#### Configuration
|
#### Configuration
|
||||||
* Note: Make sure to put this behind a reverse proxy with HTTPS.
|
* Note: Make sure to put this behind a reverse proxy with HTTPS.
|
||||||
|
@ -218,6 +218,96 @@ $("form#loginForm").submit(function() {
|
|||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
document.getElementById('openUsers').onclick = function () {
|
||||||
|
$.ajax('/getUsers', {
|
||||||
|
type : 'GET',
|
||||||
|
dataType : 'json',
|
||||||
|
contentType: 'json',
|
||||||
|
xhrFields : {
|
||||||
|
withCredentials: true
|
||||||
|
},
|
||||||
|
beforeSend : function (xhr) {
|
||||||
|
xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||||
|
},
|
||||||
|
data: { get_param: 'value' },
|
||||||
|
complete : function(data) {
|
||||||
|
if (data['status'] == 200) {
|
||||||
|
var list = document.getElementById('userList');
|
||||||
|
list.textContent = '';
|
||||||
|
if (document.getElementById('saveUsers')) {
|
||||||
|
document.getElementById('saveUsers').remove()
|
||||||
|
};
|
||||||
|
var users = data['responseJSON']['users'];
|
||||||
|
for (var i = 0; i < users.length; i++) {
|
||||||
|
var user = users[i]
|
||||||
|
var entry = document.createElement('p');
|
||||||
|
entry.id = 'user_' + user['name'];
|
||||||
|
entry.appendChild(document.createTextNode(user['name']));
|
||||||
|
var address = document.createElement('span');
|
||||||
|
address.setAttribute('style', 'margin-left: 2%; margin-right: 2%; color: grey;');
|
||||||
|
address.classList.add('addressText');
|
||||||
|
address.id = 'address_' + user['email'];
|
||||||
|
if (typeof(user['email']) != 'undefined') {
|
||||||
|
address.appendChild(document.createTextNode(user['email']));
|
||||||
|
};
|
||||||
|
var editButton = document.createElement('i');
|
||||||
|
editButton.classList.add('fa', 'fa-edit');
|
||||||
|
editButton.onclick = function() {
|
||||||
|
this.classList.remove('fa', 'fa-edit');
|
||||||
|
var input = document.createElement('input');
|
||||||
|
input.setAttribute('type', 'email');
|
||||||
|
input.setAttribute('style', 'margin-left: 2%; color: grey;');
|
||||||
|
var addressElement = this.parentNode.getElementsByClassName('addressText')[0];
|
||||||
|
if (addressElement.textContent != '') {
|
||||||
|
input.value = addressElement.textContent;
|
||||||
|
} else {
|
||||||
|
input.placeholder = 'Email Address';
|
||||||
|
};
|
||||||
|
this.parentNode.replaceChild(input, addressElement);
|
||||||
|
if (document.getElementById('saveUsers') == null) {
|
||||||
|
var footer = document.getElementById('userFooter')
|
||||||
|
var saveUsers = document.createElement('input');
|
||||||
|
saveUsers.classList.add('btn', 'btn-primary');
|
||||||
|
saveUsers.setAttribute('type', 'button');
|
||||||
|
saveUsers.value = 'Save Changes';
|
||||||
|
saveUsers.id = 'saveUsers';
|
||||||
|
saveUsers.onclick = function() {
|
||||||
|
var send = {}
|
||||||
|
var entries = document.getElementById('userList').children;
|
||||||
|
for (var i = 0; i < entries.length; i++) {
|
||||||
|
var entry = entries[i];
|
||||||
|
if (typeof(entry.getElementsByTagName('input')[0]) != 'undefined') {
|
||||||
|
var name = entry.id.replace(/user_/g, '')
|
||||||
|
var address = entry.getElementsByTagName('input')[0].value;
|
||||||
|
send[name] = address
|
||||||
|
};
|
||||||
|
};
|
||||||
|
console.log(send);
|
||||||
|
send = JSON.stringify(send);
|
||||||
|
$.ajax('/modifyUsers', {
|
||||||
|
data : send,
|
||||||
|
contentType : 'application/json',
|
||||||
|
type : 'POST',
|
||||||
|
xhrFields : {
|
||||||
|
withCredentials: true
|
||||||
|
},
|
||||||
|
beforeSend : function (xhr) {
|
||||||
|
xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||||
|
},
|
||||||
|
success: function() { $('#users').modal('hide'); },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
footer.appendChild(saveUsers);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
entry.appendChild(address);
|
||||||
|
entry.appendChild(editButton);
|
||||||
|
list.appendChild(entry);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$('#users').modal('show');
|
||||||
|
};
|
||||||
generateInvites(empty = true);
|
generateInvites(empty = true);
|
||||||
$("#login").modal('show');
|
$("#login").modal('show');
|
||||||
|
|
||||||
|
@ -73,10 +73,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal fade" id="users" tabindex="-1" role="dialog" aria-labelledby="users" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="usersTitle">Users</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<ul class="list-group list-group-flush" id="userList">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer" id="userFooter">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="pageContainer">
|
<div class="pageContainer">
|
||||||
<h1>
|
<h1>
|
||||||
Accounts admin
|
Accounts admin
|
||||||
</h1>
|
</h1>
|
||||||
|
<button type="button" class="btn btn-secondary" id="openUsers">
|
||||||
|
Users <i class="fa fa-user"></i>
|
||||||
|
</button>
|
||||||
<div class="card bg-light mb-3 linkGroup">
|
<div class="card bg-light mb-3 linkGroup">
|
||||||
<div class="card-header">Current Invites</div>
|
<div class="card-header">Current Invites</div>
|
||||||
<ul class="list-group list-group-flush" id="invites">
|
<ul class="list-group list-group-flush" id="invites">
|
||||||
|
@ -35,11 +35,21 @@ class Jellyfin:
|
|||||||
"User-Agent": self.useragent,
|
"User-Agent": self.useragent,
|
||||||
"X-Emby-Authorization": self.auth
|
"X-Emby-Authorization": self.auth
|
||||||
}
|
}
|
||||||
def getUsers(self, username="all"):
|
def getUsers(self, username="all", id="all", public=True):
|
||||||
response = requests.get(self.server+"/emby/Users/Public").json()
|
if public:
|
||||||
if username == "all":
|
response = requests.get(self.server+"/emby/Users/Public").json()
|
||||||
return response
|
|
||||||
else:
|
else:
|
||||||
|
response = requests.get(self.server+"/emby/Users",
|
||||||
|
headers=self.header,
|
||||||
|
params={'Username': self.username,
|
||||||
|
'Pw': self.password})
|
||||||
|
if response.status_code == 200:
|
||||||
|
response = response.json()
|
||||||
|
else:
|
||||||
|
raise self.AuthenticationRequiredError
|
||||||
|
if username == "all" and id == "all":
|
||||||
|
return response
|
||||||
|
elif id == "all":
|
||||||
match = False
|
match = False
|
||||||
for user in response:
|
for user in response:
|
||||||
if user['Name'] == username:
|
if user['Name'] == username:
|
||||||
@ -47,6 +57,14 @@ class Jellyfin:
|
|||||||
return user
|
return user
|
||||||
if not match:
|
if not match:
|
||||||
raise self.UserNotFoundError
|
raise self.UserNotFoundError
|
||||||
|
else:
|
||||||
|
match = False
|
||||||
|
for user in response:
|
||||||
|
if user['Id'] == id:
|
||||||
|
match = True
|
||||||
|
return user
|
||||||
|
if not match:
|
||||||
|
raise self.UserNotFoundError
|
||||||
def authenticate(self, username, password):
|
def authenticate(self, username, password):
|
||||||
self.username = username
|
self.username = username
|
||||||
self.password = password
|
self.password = password
|
||||||
|
@ -3,6 +3,7 @@ import json
|
|||||||
from watchdog.observers import Observer
|
from watchdog.observers import Observer
|
||||||
from watchdog.events import FileSystemEventHandler
|
from watchdog.events import FileSystemEventHandler
|
||||||
from jellyfin_accounts.email import Mailgun, Smtp
|
from jellyfin_accounts.email import Mailgun, Smtp
|
||||||
|
from jellyfin_accounts.web_api import jf
|
||||||
from __main__ import config
|
from __main__ import config
|
||||||
from __main__ import email_log as log
|
from __main__ import email_log as log
|
||||||
|
|
||||||
@ -41,7 +42,8 @@ class Handler(FileSystemEventHandler):
|
|||||||
try:
|
try:
|
||||||
with open(config['files']['emails'], 'r') as f:
|
with open(config['files']['emails'], 'r') as f:
|
||||||
emails = json.load(f)
|
emails = json.load(f)
|
||||||
address = emails[reset['UserName']]
|
id = jf.getUsers(reset['UserName'], public=False)['Id']
|
||||||
|
address = emails[id]
|
||||||
method = config['email']['method']
|
method = config['email']['method']
|
||||||
if method == 'mailgun':
|
if method == 'mailgun':
|
||||||
email = Mailgun(address)
|
email = Mailgun(address)
|
||||||
|
@ -66,6 +66,36 @@ while attempts != 3:
|
|||||||
'. Retrying...'))
|
'. Retrying...'))
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
|
|
||||||
|
|
||||||
|
def switchToIds():
|
||||||
|
try:
|
||||||
|
with open(config['files']['emails'], 'r') as f:
|
||||||
|
emails = json.load(f)
|
||||||
|
except (FileNotFoundError, json.decoder.JSONDecodeError):
|
||||||
|
emails = {}
|
||||||
|
users = jf.getUsers(public=False)
|
||||||
|
new_emails = {}
|
||||||
|
match = False
|
||||||
|
for key in emails:
|
||||||
|
for user in users:
|
||||||
|
if user['Name'] == key:
|
||||||
|
match = True
|
||||||
|
new_emails[user['Id']] = emails[key]
|
||||||
|
elif user['Id'] == key:
|
||||||
|
new_emails[user['Id']] = emails[key]
|
||||||
|
if match:
|
||||||
|
from pathlib import Path
|
||||||
|
email_file = Path(config['files']['emails']).name
|
||||||
|
log.info((f'{email_file} modified to use userID instead of ' +
|
||||||
|
'usernames. These will be used in future.'))
|
||||||
|
emails = new_emails
|
||||||
|
with open(config['files']['emails'], 'w') as f:
|
||||||
|
f.write(json.dumps(emails, indent=4))
|
||||||
|
|
||||||
|
|
||||||
|
# Temporary, switches emails.json over from using Usernames to User IDs.
|
||||||
|
switchToIds()
|
||||||
|
|
||||||
if config.getboolean('password_validation', 'enabled'):
|
if config.getboolean('password_validation', 'enabled'):
|
||||||
validator = PasswordValidator(config['password_validation']['min_length'],
|
validator = PasswordValidator(config['password_validation']['min_length'],
|
||||||
config['password_validation']['upper'],
|
config['password_validation']['upper'],
|
||||||
@ -113,13 +143,13 @@ 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')
|
||||||
if config.getboolean('email', 'enabled'):
|
if config.getboolean('password_resets', 'enabled'):
|
||||||
try:
|
try:
|
||||||
with open(config['files']['emails'], 'r') as f:
|
with open(config['files']['emails'], 'r') as f:
|
||||||
emails = json.load(f)
|
emails = json.load(f)
|
||||||
except (FileNotFoundError, json.decoder.JSONDecodeError):
|
except (FileNotFoundError, json.decoder.JSONDecodeError):
|
||||||
emails = {}
|
emails = {}
|
||||||
emails[data['username']] = data['email']
|
emails[user.json()['Id']] = data['email']
|
||||||
with open(config['files']['emails'], 'w') as f:
|
with open(config['files']['emails'], 'w') as f:
|
||||||
f.write(json.dumps(emails, indent=4))
|
f.write(json.dumps(emails, indent=4))
|
||||||
log.debug('Email address stored')
|
log.debug('Email address stored')
|
||||||
@ -251,3 +281,45 @@ def modifyConfig():
|
|||||||
temp_config.write(config_file)
|
temp_config.write(config_file)
|
||||||
log.debug('Config written')
|
log.debug('Config written')
|
||||||
return jsonify(data)
|
return jsonify(data)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/getUsers', methods=['GET', 'POST'])
|
||||||
|
@auth.login_required
|
||||||
|
def getUsers():
|
||||||
|
log.debug('User and email list requested')
|
||||||
|
try:
|
||||||
|
with open(config['files']['emails'], 'r') as f:
|
||||||
|
emails = json.load(f)
|
||||||
|
except (FileNotFoundError, json.decoder.JSONDecodeError):
|
||||||
|
emails = {}
|
||||||
|
response = {'users': []}
|
||||||
|
users = jf.getUsers(public=False)
|
||||||
|
for user in users:
|
||||||
|
entry = {'name': user['Name']}
|
||||||
|
if user['Id'] in emails:
|
||||||
|
entry['email'] = emails[user['Id']]
|
||||||
|
response['users'].append(entry)
|
||||||
|
return jsonify(response)
|
||||||
|
|
||||||
|
@app.route('/modifyUsers', methods=['POST'])
|
||||||
|
@auth.login_required
|
||||||
|
def modifyUsers():
|
||||||
|
data = request.get_json()
|
||||||
|
log.debug('User and email list modification requested')
|
||||||
|
try:
|
||||||
|
with open(config['files']['emails'], 'r') as f:
|
||||||
|
emails = json.load(f)
|
||||||
|
except (FileNotFoundError, json.decoder.JSONDecodeError):
|
||||||
|
emails = {}
|
||||||
|
for key in data:
|
||||||
|
uid = jf.getUsers(key, public=False)['Id']
|
||||||
|
log.debug(f'Email for user "{key}" modified')
|
||||||
|
emails[uid] = data[key]
|
||||||
|
try:
|
||||||
|
with open(config['files']['emails'], 'w') as f:
|
||||||
|
f.write(json.dumps(emails, indent=4))
|
||||||
|
return resp()
|
||||||
|
except:
|
||||||
|
return resp(success=False)
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user