Modularized JSON storage

user_template and other files are now accessed via JSONStorage, which
has dictionary like attributes for each file which can be used like a
dictionary, without the need to manually read and write the file. This
was done so that other storage types (e.g a database) can be added in
future.
This commit is contained in:
Harvey Tindall 2020-06-14 17:58:18 +01:00
parent 39a27eb762
commit f4f18d41ea
5 changed files with 197 additions and 184 deletions

View File

@ -0,0 +1,66 @@
import json
import datetime
class JSONFile(dict):
@staticmethod
def readJSON(path):
try:
with open(path, 'r') as f:
return json.load(f)
except FileNotFoundError:
return {}
@staticmethod
def writeJSON(path, data):
with open(path, 'w') as f:
return f.write(json.dumps(data, indent=4, default=str))
def __init__(self, path, data=None):
self.path = path
if data is None:
super(JSONFile, self).__init__(self.readJSON(self.path))
else:
super(JSONFile, self).__init__(data)
self.writeJSON(self.path, data)
def __getitem__(self, key):
super(JSONFile, self).__init__(self.readJSON(self.path))
return super(JSONFile, self).__getitem__(key)
def __setitem__(self, key, value):
data = self.readJSON(self.path)
data[key] = value
self.writeJSON(self.path, data)
super(JSONFile, self).__init__(data)
def __delitem__(self, key):
data = self.readJSON(self.path)
super(JSONFile, self).__init__(data)
del data[key]
self.writeJSON(self.path, data)
super(JSONFile, self).__delitem__(key)
def __str__(self):
super(JSONFile, self).__init__(self.readJSON(self.path))
return json.dumps(super(JSONFile, self))
class JSONStorage:
def __init__(self,
emails,
invites,
user_template,
user_displayprefs,
user_configuration):
self.emails = JSONFile(path=emails)
self.invites = JSONFile(path=invites)
self.user_template = JSONFile(path=user_template)
self.user_displayprefs = JSONFile(path=user_displayprefs)
self.user_configuration = JSONFile(path=user_configuration)
def __setattr__(self, name, value):
if hasattr(self, name):
path = self.__dict__[name].path
self.__dict__[name] = JSONFile(path=path, data=value)
else:
self.__dict__[name] = value

View File

@ -4,7 +4,7 @@ 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 jellyfin_accounts.web_api import jf
from __main__ import config from __main__ import config, data_store
from __main__ import email_log as log from __main__ import email_log as log
@ -42,17 +42,18 @@ class Handler(FileSystemEventHandler):
reset = json.load(f) reset = json.load(f)
log.info(f'New password reset for {reset["UserName"]}') log.info(f'New password reset for {reset["UserName"]}')
try: try:
with open(config['files']['emails'], 'r') as f: id = jf.getUsers(reset['UserName'], public=False)['Id']
emails = json.load(f) address = data_store.emails[id]
id = jf.getUsers(reset['UserName'], public=False)['Id'] if address != '':
address = emails[id] method = config['email']['method']
method = config['email']['method'] if method == 'mailgun':
if method == 'mailgun': email = Mailgun(address)
email = Mailgun(address) elif method == 'smtp':
elif method == 'smtp': email = Smtp(address)
email = Smtp(address) if email.construct_reset(reset):
if email.construct_reset(reset): email.send()
email.send() else:
raise IndexError
except (FileNotFoundError, except (FileNotFoundError,
json.decoder.JSONDecodeError, json.decoder.JSONDecodeError,
IndexError) as e: IndexError) as e:

View File

@ -1,7 +1,7 @@
import json import json
from pathlib import Path from pathlib import Path
from flask import Flask, send_from_directory, render_template from flask import Flask, send_from_directory, render_template
from __main__ import config, app, g, css from __main__ import config, app, g, css, data_store
from __main__ import web_log as log from __main__ import web_log as log
from jellyfin_accounts.web_api import checkInvite, validator from jellyfin_accounts.web_api import checkInvite, validator
@ -43,16 +43,9 @@ def inviteProxy(path):
if checkInvite(path): if checkInvite(path):
log.info(f'Invite {path} used to request form') log.info(f'Invite {path} used to request form')
try: try:
with open(config['files']['invites'], 'r') as f: email = data_store.invites[path]['email']
invites = json.load(f) except KeyError:
except (FileNotFoundError, json.decoder.JSONDecodeError): email = ''
invites = {'invites': []}
for invite in invites['invites']:
if invite['code'] == path:
try:
email = invite['email']
except KeyError:
email = ""
return render_template('form.html', return render_template('form.html',
css_href=css['href'], css_href=css['href'],
css_integrity=css['integrity'], css_integrity=css['integrity'],

View File

@ -1,48 +1,40 @@
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, config_path, app, g from __main__ import config, config_path, app, g, data_store
from __main__ import web_log as log from __main__ import web_log as log
from jellyfin_accounts.validate_password import PasswordValidator from jellyfin_accounts.validate_password import PasswordValidator
def resp(success=True, code=500): def resp(success=True, code=500):
if success: if success:
r = jsonify({'success': True}) r = jsonify({'success': True})
r.status_code = 200 if code == 500:
r.status_code = 200
else:
r.status_code = code
else: else:
r = jsonify({'success': False}) r = jsonify({'success': False})
r.status_code = code r.status_code = code
return r return r
def checkInvite(code, delete=False): def checkInvite(code, delete=False):
current_time = datetime.datetime.now() current_time = datetime.datetime.now()
try: invites = dict(data_store.invites)
with open(config['files']['invites'], 'r') as f: match = False
invites = json.load(f) for invite in invites:
except (FileNotFoundError, json.decoder.JSONDecodeError): expiry = datetime.datetime.strptime(invites[invite]['valid_till'],
invites = {'invites': []}
valid = False
for index, i in enumerate(invites['invites']):
expiry = datetime.datetime.strptime(i['valid_till'],
'%Y-%m-%dT%H:%M:%S.%f') '%Y-%m-%dT%H:%M:%S.%f')
if current_time >= expiry: if current_time >= expiry:
log.debug(('Housekeeping: Deleting old invite ' + log.debug(f'Housekeeping: Deleting old invite {invite}')
invites['invites'][index]['code'])) del data_store.invites[invite]
del invites['invites'][index] elif invite == code:
else: match = True
if i['code'] == code: if delete:
valid = True del data_store.invites[code]
if delete: return match
del invites['invites'][index]
with open(config['files']['invites'], 'w') as f:
f.write(json.dumps(invites, indent=4, default=str))
return valid
jf = Jellyfin(config['jellyfin']['server'], jf = Jellyfin(config['jellyfin']['server'],
config['jellyfin']['client'], config['jellyfin']['client'],
@ -52,26 +44,22 @@ jf = Jellyfin(config['jellyfin']['server'],
from jellyfin_accounts.login import auth from jellyfin_accounts.login import auth
attempts = 0 jf_address = config['jellyfin']['server']
success = False success = False
while attempts != 3: for i in range(3):
try: try:
jf.authenticate(config['jellyfin']['username'], jf.authenticate(config['jellyfin']['username'],
config['jellyfin']['password']) config['jellyfin']['password'])
success = True success = True
log.info(('Successfully authenticated with ' + log.info(f'Successfully authenticated with {jf_address}')
config['jellyfin']['server']))
break break
except Jellyfin.AuthenticationError: except Jellyfin.AuthenticationError:
attempts += 1 log.error(f'Failed to authenticate with {jf_address}, Retrying...')
log.error(('Failed to authenticate with ' +
config['jellyfin']['server'] +
'. Retrying...'))
time.sleep(5) time.sleep(5)
if not success: if not success:
log.error('Could not authenticate after 3 tries.') log.error('Could not authenticate after 3 tries.')
exit()
def switchToIds(): def switchToIds():
try: try:
@ -112,7 +100,7 @@ else:
validator = PasswordValidator(0, 0, 0, 0, 0) validator = PasswordValidator(0, 0, 0, 0, 0)
@app.route('/newUser', methods=['GET', 'POST']) @app.route('/newUser', methods=['POST'])
def newUser(): def newUser():
data = request.get_json() data = request.get_json()
log.debug('Attempted newUser') log.debug('Attempted newUser')
@ -125,12 +113,10 @@ def newUser():
if valid: if valid:
log.debug('User password valid') log.debug('User password valid')
try: try:
jf.authenticate(config['jellyfin']['username'], user = jf.newUser(data['username'],
config['jellyfin']['password']) data['password'])
user = jf.newUser(data['username'], data['password'])
except Jellyfin.UserExistsError: except Jellyfin.UserExistsError:
error = 'User already exists with name ' error = f'User already exists named {data["username"]}'
error += data['username']
log.debug(error) log.debug(error)
return jsonify({'error': error}) return jsonify({'error': error})
except: except:
@ -138,36 +124,31 @@ def newUser():
checkInvite(data['code'], delete=True) 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: policy = data_store.user_template
default_policy = json.load(f) if policy != {}:
jf.setPolicy(user.json()['Id'], default_policy) jf.setPolicy(user.json()['Id'], policy)
else:
log.debug('user policy was blank')
except: except:
log.error('Failed to set new user policy. ' + log.error('Failed to set new user policy')
'Ignore if you didn\'t create a template')
try: try:
with open(config['files']['user_configuration'], 'r') as f: configuration = data_store.user_configuration
default_configuration = json.load(f) displayprefs = data_store.user_displayprefs
with open(config['files']['user_displayprefs'], 'r') as f: if configuration != {} and displayprefs != {}:
default_displayprefs = json.load(f) if jf.setConfiguration(user.json()['Id'],
if jf.setConfiguration(user.json()['Id'], configuration):
default_configuration): jf.setDisplayPreferences(user.json()['Id'],
jf.setDisplayPreferences(user.json()['Id'], displayprefs)
default_displayprefs) log.debug('Set homescreen layout.')
log.debug('Set homescreen layout.') else:
log.debug('user configuration and/or ' +
'displayprefs were blank')
except: except:
log.error('Failed to set new user homescreen kayout.' + log.error('Failed to set new user homescreen layout')
'Ignore if you didn\'t create a template')
if config.getboolean('password_resets', 'enabled'): if config.getboolean('password_resets', 'enabled'):
try: data_store.emails[user.json()['Id']] = data['email']
with open(config['files']['emails'], 'r') as f:
emails = json.load(f)
except (FileNotFoundError, json.decoder.JSONDecodeError):
emails = {}
emails[user.json()['Id']] = data['email']
with open(config['files']['emails'], 'w') as f:
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')
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)
@ -179,15 +160,16 @@ def newUser():
return resp(False, code=401) return resp(False, code=401)
@app.route('/generateInvite', methods=['GET', 'POST']) @app.route('/generateInvite', methods=['POST'])
@auth.login_required @auth.login_required
def generateInvite(): def generateInvite():
current_time = datetime.datetime.now() current_time = datetime.datetime.now()
data = request.get_json() data = request.get_json()
delta = datetime.timedelta(hours=int(data['hours']), delta = datetime.timedelta(hours=int(data['hours']),
minutes=int(data['minutes'])) minutes=int(data['minutes']))
invite = {'code': secrets.token_urlsafe(16)} invite_code = secrets.token_urlsafe(16)
log.debug(f'Creating new invite: {invite["code"]}') invite = {}
log.debug(f'Creating new invite: {invite_code}')
valid_till = current_time + delta valid_till = current_time + delta
invite['valid_till'] = valid_till.strftime('%Y-%m-%dT%H:%M:%S.%f') invite['valid_till'] = valid_till.strftime('%Y-%m-%dT%H:%M:%S.%f')
if 'email' in data and config.getboolean('invite_emails', 'enabled'): if 'email' in data and config.getboolean('invite_emails', 'enabled'):
@ -202,19 +184,12 @@ def generateInvite():
from jellyfin_accounts.email import Smtp from jellyfin_accounts.email import Smtp
email = Smtp(address) email = Smtp(address)
email.construct_invite({'expiry': valid_till, email.construct_invite({'expiry': valid_till,
'code': invite['code']}) 'code': invite_code})
response = email.send() response = email.send()
if response is False or type(response) != bool: if response is False or type(response) != bool:
invite['email'] = f'Failed to send to {address}' invite['email'] = f'Failed to send to {address}'
try: data_store.invites[invite_code] = invite
with open(config['files']['invites'], 'r') as f: log.info(f'New invite created: {invite_code}')
invites = json.load(f)
except (FileNotFoundError, json.decoder.JSONDecodeError):
invites = {'invites': []}
invites['invites'].append(invite)
with open(config['files']['invites'], 'w') as f:
f.write(json.dumps(invites, indent=4, default=str))
log.info(f'New invite created: {invite["code"]}')
return resp() return resp()
@ -223,46 +198,30 @@ def generateInvite():
def getInvites(): def getInvites():
log.debug('Invites requested') log.debug('Invites requested')
current_time = datetime.datetime.now() current_time = datetime.datetime.now()
try: invites = dict(data_store.invites)
with open(config['files']['invites'], 'r') as f: for code in invites:
invites = json.load(f) checkInvite(code)
except (FileNotFoundError, json.decoder.JSONDecodeError): invites = dict(data_store.invites)
invites = {'invites': []}
response = {'invites': []} response = {'invites': []}
for index, i in enumerate(invites['invites']): for code in invites:
expiry = datetime.datetime.strptime(i['valid_till'], expiry = datetime.datetime.strptime(invites[code]['valid_till'],
'%Y-%m-%dT%H:%M:%S.%f') '%Y-%m-%dT%H:%M:%S.%f')
if current_time >= expiry: valid_for = expiry - current_time
log.debug(('Housekeeping: Deleting old invite ' + invite = {'code': code,
invites['invites'][index]['code'])) 'hours': valid_for.seconds//3600,
del invites['invites'][index] 'minutes': (valid_for.seconds//60) % 60}
else: if 'email' in invites[code]:
valid_for = expiry - current_time invite['email'] = invites[code]['email']
invite = {'code': i['code'], response['invites'].append(invite)
'hours': valid_for.seconds//3600,
'minutes': (valid_for.seconds//60) % 60}
if 'email' in i:
invite['email'] = i['email']
response['invites'].append(invite)
with open(config['files']['invites'], 'w') as f:
f.write(json.dumps(invites, indent=4, default=str))
return jsonify(response) return jsonify(response)
@app.route('/deleteInvite', methods=['POST']) @app.route('/deleteInvite', methods=['POST'])
@auth.login_required @auth.login_required
def deleteInvite(): def deleteInvite():
code = request.get_json()['code'] code = request.get_json()['code']
try: invites = dict(data_store.invites)
with open(config['files']['invites'], 'r') as f: if code in invites:
invites = json.load(f) del data_store.invites[code]
except (FileNotFoundError, json.decoder.JSONDecodeError):
invites = {'invites': []}
for index, i in enumerate(invites['invites']):
if i['code'] == code:
del invites['invites'][index]
with open(config['files']['invites'], 'w') as f:
f.write(json.dumps(invites, indent=4, default=str))
log.info(f'Invite deleted: {code}') log.info(f'Invite deleted: {code}')
return resp() return resp()
@ -274,19 +233,13 @@ def get_token():
return jsonify({'token': token.decode('ascii')}) return jsonify({'token': token.decode('ascii')})
@app.route('/getUsers', methods=['GET', 'POST']) @app.route('/getUsers', methods=['GET'])
@auth.login_required @auth.login_required
def getUsers(): def getUsers():
log.debug('User and email list requested') 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': []} response = {'users': []}
jf.authenticate(config['jellyfin']['username'],
config['jellyfin']['password'])
users = jf.getUsers(public=False) users = jf.getUsers(public=False)
emails = data_store.emails
for user in users: for user in users:
entry = {'name': user['Name']} entry = {'name': user['Name']}
if user['Id'] in emails: if user['Id'] in emails:
@ -294,29 +247,17 @@ def getUsers():
response['users'].append(entry) response['users'].append(entry)
return jsonify(response) return jsonify(response)
@app.route('/modifyUsers', methods=['POST']) @app.route('/modifyUsers', methods=['POST'])
@auth.login_required @auth.login_required
def modifyUsers(): def modifyUsers():
data = request.get_json() data = request.get_json()
log.debug('User and email list modification requested') log.debug('Email list modification requested')
try:
with open(config['files']['emails'], 'r') as f:
emails = json.load(f)
except (FileNotFoundError, json.decoder.JSONDecodeError):
emails = {}
jf.authenticate(config['jellyfin']['username'],
config['jellyfin']['password'])
for key in data: for key in data:
uid = jf.getUsers(key, public=False)['Id'] uid = jf.getUsers(key, public=False)['Id']
data_store.emails[uid] = data[key]
log.debug(f'Email for user "{key}" modified') log.debug(f'Email for user "{key}" modified')
emails[uid] = data[key] return resp()
try:
with open(config['files']['emails'], 'w') as f:
f.write(json.dumps(emails, indent=4))
return resp()
except:
log.error('Could not store email')
return resp(success=False)
@app.route('/setDefaults', methods=['POST']) @app.route('/setDefaults', methods=['POST'])
@ -324,33 +265,27 @@ def modifyUsers():
def setDefaults(): def setDefaults():
data = request.get_json() data = request.get_json()
username = data['username'] username = data['username']
log.debug(f'storing default settings from user {username}') log.debug(f'Storing default settings from user {username}')
jf.authenticate(config['jellyfin']['username'],
config['jellyfin']['password'])
try: try:
user = jf.getUsers(username=username, user = jf.getUsers(username=username,
public=False) public=False)
except Jellyfin.UserNotFoundError: except Jellyfin.UserNotFoundError:
log.error(f'couldn\'t find user {username}') log.error(f'Storing defaults failed: Couldn\'t find user {username}')
return resp(success=False) return resp(False)
uid = user['Id'] uid = user['Id']
policy = user['Policy'] policy = user['Policy']
try: data_store.user_template = policy
with open(config['files']['user_template'], 'w') as f:
f.write(json.dumps(policy, indent=4))
except:
log.error('Could not store user template')
return resp(success=False)
if data['homescreen']: if data['homescreen']:
configuration = user['Configuration'] configuration = user['Configuration']
try: try:
display_prefs = jf.getDisplayPreferences(uid) displayprefs = jf.getDisplayPreferences(uid)
with open(config['files']['user_configuration'], 'w') as f: data_store.user_configuration = configuration
f.write(json.dumps(configuration, indent=4)) data_store.user_displayprefs = displayprefs
with open(config['files']['user_displayprefs'], 'w') as f:
f.write(json.dumps(display_prefs, indent=4))
except: except:
log.error('Could not store homescreen layout') log.error('Storing defaults failed: ' +
'couldn\'t store homescreen layout')
return resp(False)
return resp() return resp()
import jellyfin_accounts.setup import jellyfin_accounts.setup

View File

@ -7,8 +7,10 @@ import logging
import threading import threading
import signal import signal
import sys import sys
import json
from pathlib import Path from pathlib import Path
from flask import Flask, g from flask import Flask, g
from jellyfin_accounts.data_store import JSONStorage
parser = argparse.ArgumentParser(description="jellyfin-accounts") parser = argparse.ArgumentParser(description="jellyfin-accounts")
@ -97,6 +99,25 @@ for key in ['user_configuration', 'user_displayprefs']:
log.debug(f'Using default {key}') log.debug(f'Using default {key}')
config['files'][key] = str(data_dir / (key + '.json')) config['files'][key] = str(data_dir / (key + '.json'))
with open(config['files']['invites'], 'r') as f:
temp_invites = json.load(f)
if 'invites' in temp_invites:
new_invites = {}
log.info('Converting invites.json to new format, temporary.')
for el in temp_invites['invites']:
i = {'valid_till': el['valid_till']}
if 'email' in el:
i['email'] = el['email']
new_invites[el['code']] = i
with open(config['files']['invites'], 'w') as f:
f.write(json.dumps(new_invites, indent=4, default=str))
data_store = JSONStorage(config['files']['emails'],
config['files']['invites'],
config['files']['user_template'],
config['files']['user_displayprefs'],
config['files']['user_configuration'])
def default_css(): def default_css():
css = {} css = {}
@ -148,10 +169,10 @@ if args.get_defaults:
import json import json
from jellyfin_accounts.jf_api import Jellyfin from jellyfin_accounts.jf_api import Jellyfin
jf = Jellyfin(config['jellyfin']['server'], jf = Jellyfin(config['jellyfin']['server'],
config['jellyfin']['client'], config['jellyfin']['client'],
config['jellyfin']['version'], config['jellyfin']['version'],
config['jellyfin']['device'], config['jellyfin']['device'],
config['jellyfin']['device_id']) config['jellyfin']['device_id'])
print("NOTE: This can now be done through the web ui.") print("NOTE: This can now be done through the web ui.")
print(""" print("""
This tool lets you grab various settings from a user, This tool lets you grab various settings from a user,
@ -187,8 +208,7 @@ if args.get_defaults:
success = True success = True
except (ValueError, IndexError): except (ValueError, IndexError):
pass pass
with open(config['files']['user_template'], 'w') as f: data_store.user_template = policy
f.write(json.dumps(policy, indent=4))
print(f'Policy written to "{config["files"]["user_template"]}".') print(f'Policy written to "{config["files"]["user_template"]}".')
print('In future, this policy will be copied to all new users.') print('In future, this policy will be copied to all new users.')
print('Step 2: Homescreen Layout') print('Step 2: Homescreen Layout')
@ -203,11 +223,9 @@ if args.get_defaults:
user_id = users[user_index]['Id'] user_id = users[user_index]['Id']
configuration = users[user_index]['Configuration'] configuration = users[user_index]['Configuration']
display_prefs = jf.getDisplayPreferences(user_id) display_prefs = jf.getDisplayPreferences(user_id)
with open(config['files']['user_configuration'], 'w') as f: data_store.user_configuration = configuration
f.write(json.dumps(configuration, indent=4))
print(f'Configuration written to "{config["files"]["user_configuration"]}".') print(f'Configuration written to "{config["files"]["user_configuration"]}".')
with open(config['files']['user_displayprefs'], 'w') as f: data_store.user_displayprefs = display_prefs
f.write(json.dumps(display_prefs, indent=4))
print(f'Display Prefs written to "{config["files"]["user_displayprefs"]}".') print(f'Display Prefs written to "{config["files"]["user_displayprefs"]}".')
success = True success = True
elif choice.lower() == 'n': elif choice.lower() == 'n':