2020-04-11 14:20:25 +00:00
|
|
|
from flask import request, jsonify
|
2020-04-14 20:31:44 +00:00
|
|
|
from configparser import RawConfigParser
|
2020-04-11 14:20:25 +00:00
|
|
|
from jellyfin_accounts.jf_api import Jellyfin
|
|
|
|
import json
|
|
|
|
import datetime
|
|
|
|
import secrets
|
2020-04-12 20:25:27 +00:00
|
|
|
import time
|
2020-04-14 20:31:44 +00:00
|
|
|
from __main__ import config, config_path, app, g
|
2020-04-12 20:25:27 +00:00
|
|
|
from __main__ import web_log as log
|
2020-04-14 20:31:44 +00:00
|
|
|
from jellyfin_accounts.validate_password import PasswordValidator
|
2020-04-11 14:20:25 +00:00
|
|
|
|
|
|
|
def resp(success=True, code=500):
|
|
|
|
if success:
|
|
|
|
r = jsonify({'success': True})
|
|
|
|
r.status_code = 200
|
|
|
|
else:
|
|
|
|
r = jsonify({'success': False})
|
|
|
|
r.status_code = code
|
|
|
|
return r
|
|
|
|
|
|
|
|
|
|
|
|
def checkInvite(code, delete=False):
|
|
|
|
current_time = datetime.datetime.now()
|
|
|
|
try:
|
|
|
|
with open(config['files']['invites'], 'r') as f:
|
|
|
|
invites = json.load(f)
|
|
|
|
except (FileNotFoundError, json.decoder.JSONDecodeError):
|
|
|
|
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')
|
|
|
|
if current_time >= expiry:
|
2020-04-12 20:25:27 +00:00
|
|
|
log.debug(('Housekeeping: Deleting old invite ' +
|
|
|
|
invites['invites'][index]['code']))
|
2020-04-11 14:20:25 +00:00
|
|
|
del invites['invites'][index]
|
|
|
|
else:
|
|
|
|
if i['code'] == code:
|
|
|
|
valid = True
|
|
|
|
if delete:
|
|
|
|
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'],
|
|
|
|
config['jellyfin']['client'],
|
|
|
|
config['jellyfin']['version'],
|
|
|
|
config['jellyfin']['device'],
|
|
|
|
config['jellyfin']['device_id'])
|
|
|
|
|
2020-04-25 16:20:46 +00:00
|
|
|
from jellyfin_accounts.login import auth
|
|
|
|
|
2020-04-12 20:25:27 +00:00
|
|
|
attempts = 0
|
2020-04-22 20:54:31 +00:00
|
|
|
success = False
|
2020-04-12 20:25:27 +00:00
|
|
|
while attempts != 3:
|
|
|
|
try:
|
|
|
|
jf.authenticate(config['jellyfin']['username'],
|
|
|
|
config['jellyfin']['password'])
|
2020-04-22 20:54:31 +00:00
|
|
|
success = True
|
2020-04-12 20:25:27 +00:00
|
|
|
log.info(('Successfully authenticated with ' +
|
|
|
|
config['jellyfin']['server']))
|
|
|
|
break
|
2020-04-14 20:31:44 +00:00
|
|
|
except Jellyfin.AuthenticationError:
|
2020-04-12 20:25:27 +00:00
|
|
|
attempts += 1
|
|
|
|
log.error(('Failed to authenticate with ' +
|
2020-04-19 21:35:51 +00:00
|
|
|
config['jellyfin']['server'] +
|
2020-04-12 20:25:27 +00:00
|
|
|
'. Retrying...'))
|
|
|
|
time.sleep(5)
|
2020-04-11 14:20:25 +00:00
|
|
|
|
2020-04-22 20:54:31 +00:00
|
|
|
if not success:
|
|
|
|
log.error('Could not authenticate after 3 tries.')
|
|
|
|
|
2020-04-20 19:37:39 +00:00
|
|
|
|
|
|
|
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()
|
|
|
|
|
2020-04-14 20:31:44 +00:00
|
|
|
if config.getboolean('password_validation', 'enabled'):
|
|
|
|
validator = PasswordValidator(config['password_validation']['min_length'],
|
|
|
|
config['password_validation']['upper'],
|
2020-04-16 13:33:23 +00:00
|
|
|
config['password_validation']['lower'],
|
2020-04-14 20:31:44 +00:00
|
|
|
config['password_validation']['number'],
|
|
|
|
config['password_validation']['special'])
|
|
|
|
else:
|
2020-04-16 13:33:23 +00:00
|
|
|
validator = PasswordValidator(0, 0, 0, 0, 0)
|
2020-04-14 20:31:44 +00:00
|
|
|
|
|
|
|
|
|
|
|
@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())
|
|
|
|
|
2020-04-11 14:20:25 +00:00
|
|
|
|
|
|
|
@app.route('/newUser', methods=['GET', 'POST'])
|
|
|
|
def newUser():
|
|
|
|
data = request.get_json()
|
2020-04-12 20:25:27 +00:00
|
|
|
log.debug('Attempted newUser')
|
2020-04-14 20:31:44 +00:00
|
|
|
if checkInvite(data['code']):
|
|
|
|
validation = validator.validate(data['password'])
|
|
|
|
valid = True
|
|
|
|
for criterion in validation:
|
2020-04-17 14:24:56 +00:00
|
|
|
if validation[criterion] is False:
|
2020-04-14 20:31:44 +00:00
|
|
|
valid = False
|
|
|
|
if valid:
|
|
|
|
log.debug('User password valid')
|
2020-04-22 20:54:31 +00:00
|
|
|
try:
|
|
|
|
jf.authenticate(config['jellyfin']['username'],
|
|
|
|
config['jellyfin']['password'])
|
2020-04-14 20:31:44 +00:00
|
|
|
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})
|
2020-04-11 14:20:25 +00:00
|
|
|
except:
|
2020-04-14 20:31:44 +00:00
|
|
|
return jsonify({'error': 'Unknown error'})
|
|
|
|
checkInvite(data['code'], delete=True)
|
|
|
|
if user.status_code == 200:
|
2020-04-11 14:20:25 +00:00
|
|
|
try:
|
2020-04-14 20:31:44 +00:00
|
|
|
with open(config['files']['user_template'], 'r') as f:
|
|
|
|
default_policy = json.load(f)
|
|
|
|
jf.setPolicy(user.json()['Id'], default_policy)
|
|
|
|
except:
|
|
|
|
log.debug('setPolicy failed')
|
2020-04-20 19:37:39 +00:00
|
|
|
if config.getboolean('password_resets', 'enabled'):
|
2020-04-14 20:31:44 +00:00
|
|
|
try:
|
|
|
|
with open(config['files']['emails'], 'r') as f:
|
|
|
|
emails = json.load(f)
|
|
|
|
except (FileNotFoundError, json.decoder.JSONDecodeError):
|
|
|
|
emails = {}
|
2020-04-20 19:37:39 +00:00
|
|
|
emails[user.json()['Id']] = data['email']
|
2020-04-14 20:31:44 +00:00
|
|
|
with open(config['files']['emails'], 'w') as f:
|
|
|
|
f.write(json.dumps(emails, indent=4))
|
|
|
|
log.debug('Email address stored')
|
|
|
|
log.info('New User created.')
|
|
|
|
else:
|
|
|
|
log.error(f'New user creation failed: {user.status_code}')
|
|
|
|
return resp(False)
|
2020-04-11 14:20:25 +00:00
|
|
|
else:
|
2020-04-14 20:31:44 +00:00
|
|
|
log.debug('User password invalid')
|
|
|
|
return jsonify(validation)
|
2020-04-11 14:20:25 +00:00
|
|
|
else:
|
2020-04-12 20:25:27 +00:00
|
|
|
log.debug('Attempted newUser unauthorized')
|
2020-04-11 14:20:25 +00:00
|
|
|
return resp(False, code=401)
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/generateInvite', methods=['GET', 'POST'])
|
|
|
|
@auth.login_required
|
|
|
|
def generateInvite():
|
|
|
|
current_time = datetime.datetime.now()
|
|
|
|
data = request.get_json()
|
2020-04-19 21:35:51 +00:00
|
|
|
delta = datetime.timedelta(hours=int(data['hours']),
|
2020-04-11 14:20:25 +00:00
|
|
|
minutes=int(data['minutes']))
|
|
|
|
invite = {'code': secrets.token_urlsafe(16)}
|
2020-04-12 20:25:27 +00:00
|
|
|
log.debug(f'Creating new invite: {invite["code"]}')
|
2020-04-19 21:35:51 +00:00
|
|
|
valid_till = current_time + delta
|
|
|
|
invite['valid_till'] = valid_till.strftime('%Y-%m-%dT%H:%M:%S.%f')
|
|
|
|
if 'email' in data and config.getboolean('invite_emails', 'enabled'):
|
|
|
|
address = data['email']
|
|
|
|
invite['email'] = address
|
|
|
|
log.info(f'Sending invite to {address}')
|
|
|
|
method = config['email']['method']
|
|
|
|
if method == 'mailgun':
|
|
|
|
from jellyfin_accounts.email import Mailgun
|
|
|
|
email = Mailgun(address)
|
|
|
|
elif method == 'smtp':
|
|
|
|
from jellyfin_accounts.email import Smtp
|
|
|
|
email = Smtp(address)
|
|
|
|
email.construct_invite({'expiry': valid_till,
|
|
|
|
'code': invite['code']})
|
|
|
|
email.send()
|
2020-04-11 14:20:25 +00:00
|
|
|
try:
|
|
|
|
with open(config['files']['invites'], 'r') as f:
|
|
|
|
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))
|
2020-04-12 20:25:27 +00:00
|
|
|
log.info(f'New invite created: {invite["code"]}')
|
2020-04-11 14:20:25 +00:00
|
|
|
return resp()
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/getInvites', methods=['GET'])
|
|
|
|
@auth.login_required
|
|
|
|
def getInvites():
|
2020-04-12 20:25:27 +00:00
|
|
|
log.debug('Invites requested')
|
2020-04-11 14:20:25 +00:00
|
|
|
current_time = datetime.datetime.now()
|
|
|
|
try:
|
|
|
|
with open(config['files']['invites'], 'r') as f:
|
|
|
|
invites = json.load(f)
|
|
|
|
except (FileNotFoundError, json.decoder.JSONDecodeError):
|
|
|
|
invites = {'invites': []}
|
|
|
|
response = {'invites': []}
|
|
|
|
for index, i in enumerate(invites['invites']):
|
2020-04-17 14:24:56 +00:00
|
|
|
expiry = datetime.datetime.strptime(i['valid_till'],
|
|
|
|
'%Y-%m-%dT%H:%M:%S.%f')
|
2020-04-11 14:20:25 +00:00
|
|
|
if current_time >= expiry:
|
2020-04-12 20:25:27 +00:00
|
|
|
log.debug(('Housekeeping: Deleting old invite ' +
|
|
|
|
invites['invites'][index]['code']))
|
2020-04-11 14:20:25 +00:00
|
|
|
del invites['invites'][index]
|
|
|
|
else:
|
|
|
|
valid_for = expiry - current_time
|
2020-04-19 21:35:51 +00:00
|
|
|
invite = {'code': i['code'],
|
|
|
|
'hours': valid_for.seconds//3600,
|
|
|
|
'minutes': (valid_for.seconds//60) % 60}
|
|
|
|
if 'email' in i:
|
|
|
|
invite['email'] = i['email']
|
|
|
|
response['invites'].append(invite)
|
2020-04-11 14:20:25 +00:00
|
|
|
with open(config['files']['invites'], 'w') as f:
|
|
|
|
f.write(json.dumps(invites, indent=4, default=str))
|
2020-04-19 21:35:51 +00:00
|
|
|
return jsonify(response)
|
2020-04-11 14:20:25 +00:00
|
|
|
|
|
|
|
|
|
|
|
@app.route('/deleteInvite', methods=['POST'])
|
|
|
|
@auth.login_required
|
|
|
|
def deleteInvite():
|
|
|
|
code = request.get_json()['code']
|
|
|
|
try:
|
|
|
|
with open(config['files']['invites'], 'r') as f:
|
|
|
|
invites = json.load(f)
|
|
|
|
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))
|
2020-04-12 20:25:27 +00:00
|
|
|
log.info(f'Invite deleted: {code}')
|
2020-04-11 14:20:25 +00:00
|
|
|
return resp()
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/getToken')
|
|
|
|
@auth.login_required
|
|
|
|
def get_token():
|
|
|
|
token = g.user.generate_token()
|
|
|
|
return jsonify({'token': token.decode('ascii')})
|
|
|
|
|
|
|
|
|
2020-04-14 20:31:44 +00:00
|
|
|
@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)
|
2020-04-20 19:37:39 +00:00
|
|
|
|
|
|
|
|
|
|
|
@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': []}
|
2020-04-22 20:54:31 +00:00
|
|
|
jf.authenticate(config['jellyfin']['username'],
|
|
|
|
config['jellyfin']['password'])
|
2020-04-20 19:37:39 +00:00
|
|
|
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 = {}
|
2020-04-22 20:54:31 +00:00
|
|
|
jf.authenticate(config['jellyfin']['username'],
|
|
|
|
config['jellyfin']['password'])
|
2020-04-20 19:37:39 +00:00
|
|
|
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)
|
|
|
|
|
|
|
|
|