2020-04-11 14:20:25 +00:00
|
|
|
from flask import request, jsonify
|
|
|
|
from jellyfin_accounts.jf_api import Jellyfin
|
|
|
|
import json
|
|
|
|
import datetime
|
|
|
|
import secrets
|
2020-04-12 20:25:27 +00:00
|
|
|
import time
|
2020-06-16 19:07:47 +00:00
|
|
|
from jellyfin_accounts import config, config_path, app, g, data_store
|
|
|
|
from jellyfin_accounts 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
|
|
|
|
2020-06-21 19:29:53 +00:00
|
|
|
|
2020-04-11 14:20:25 +00:00
|
|
|
def resp(success=True, code=500):
|
|
|
|
if success:
|
2020-06-21 19:29:53 +00:00
|
|
|
r = jsonify({"success": True})
|
2020-06-14 16:58:18 +00:00
|
|
|
if code == 500:
|
|
|
|
r.status_code = 200
|
|
|
|
else:
|
|
|
|
r.status_code = code
|
2020-04-11 14:20:25 +00:00
|
|
|
else:
|
2020-06-21 19:29:53 +00:00
|
|
|
r = jsonify({"success": False})
|
2020-04-11 14:20:25 +00:00
|
|
|
r.status_code = code
|
|
|
|
return r
|
|
|
|
|
2020-06-21 19:29:53 +00:00
|
|
|
|
2020-04-11 14:20:25 +00:00
|
|
|
def checkInvite(code, delete=False):
|
|
|
|
current_time = datetime.datetime.now()
|
2020-06-14 16:58:18 +00:00
|
|
|
invites = dict(data_store.invites)
|
|
|
|
match = False
|
|
|
|
for invite in invites:
|
2020-06-21 19:29:53 +00:00
|
|
|
expiry = datetime.datetime.strptime(
|
|
|
|
invites[invite]["valid_till"], "%Y-%m-%dT%H:%M:%S.%f"
|
|
|
|
)
|
2020-04-11 14:20:25 +00:00
|
|
|
if current_time >= expiry:
|
2020-06-21 19:29:53 +00:00
|
|
|
log.debug(f"Housekeeping: Deleting old invite {invite}")
|
2020-06-14 16:58:18 +00:00
|
|
|
del data_store.invites[invite]
|
|
|
|
elif invite == code:
|
|
|
|
match = True
|
|
|
|
if delete:
|
|
|
|
del data_store.invites[code]
|
|
|
|
return match
|
2020-04-11 14:20:25 +00:00
|
|
|
|
2020-06-21 19:29:53 +00:00
|
|
|
|
|
|
|
jf = Jellyfin(
|
|
|
|
config["jellyfin"]["server"],
|
|
|
|
config["jellyfin"]["client"],
|
|
|
|
config["jellyfin"]["version"],
|
|
|
|
config["jellyfin"]["device"],
|
|
|
|
config["jellyfin"]["device_id"],
|
|
|
|
)
|
2020-04-11 14:20:25 +00:00
|
|
|
|
2020-04-25 16:20:46 +00:00
|
|
|
from jellyfin_accounts.login import auth
|
|
|
|
|
2020-06-21 19:29:53 +00:00
|
|
|
jf_address = config["jellyfin"]["server"]
|
2020-04-22 20:54:31 +00:00
|
|
|
success = False
|
2020-06-14 16:58:18 +00:00
|
|
|
for i in range(3):
|
2020-04-12 20:25:27 +00:00
|
|
|
try:
|
2020-06-21 19:29:53 +00:00
|
|
|
jf.authenticate(config["jellyfin"]["username"], config["jellyfin"]["password"])
|
2020-04-22 20:54:31 +00:00
|
|
|
success = True
|
2020-06-21 19:29:53 +00:00
|
|
|
log.info(f"Successfully authenticated with {jf_address}")
|
2020-04-12 20:25:27 +00:00
|
|
|
break
|
2020-04-14 20:31:44 +00:00
|
|
|
except Jellyfin.AuthenticationError:
|
2020-06-21 19:29:53 +00:00
|
|
|
log.error(f"Failed to authenticate with {jf_address}, Retrying...")
|
2020-04-12 20:25:27 +00:00
|
|
|
time.sleep(5)
|
2020-04-11 14:20:25 +00:00
|
|
|
|
2020-04-22 20:54:31 +00:00
|
|
|
if not success:
|
2020-06-21 19:29:53 +00:00
|
|
|
log.error("Could not authenticate after 3 tries.")
|
2020-06-14 16:58:18 +00:00
|
|
|
exit()
|
2020-04-20 19:37:39 +00:00
|
|
|
|
2020-06-27 14:38:51 +00:00
|
|
|
# Temporary fixes below.
|
|
|
|
|
2020-06-21 19:29:53 +00:00
|
|
|
|
2020-04-20 19:37:39 +00:00
|
|
|
def switchToIds():
|
|
|
|
try:
|
2020-06-21 19:29:53 +00:00
|
|
|
with open(config["files"]["emails"], "r") as f:
|
2020-04-20 19:37:39 +00:00
|
|
|
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:
|
2020-06-21 19:29:53 +00:00
|
|
|
if user["Name"] == key:
|
2020-04-20 19:37:39 +00:00
|
|
|
match = True
|
2020-06-21 19:29:53 +00:00
|
|
|
new_emails[user["Id"]] = emails[key]
|
|
|
|
elif user["Id"] == key:
|
|
|
|
new_emails[user["Id"]] = emails[key]
|
2020-04-20 19:37:39 +00:00
|
|
|
if match:
|
|
|
|
from pathlib import Path
|
2020-06-21 19:29:53 +00:00
|
|
|
|
|
|
|
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."
|
|
|
|
)
|
|
|
|
)
|
2020-04-20 19:37:39 +00:00
|
|
|
emails = new_emails
|
2020-06-21 19:29:53 +00:00
|
|
|
with open(config["files"]["emails"], "w") as f:
|
2020-04-20 19:37:39 +00:00
|
|
|
f.write(json.dumps(emails, indent=4))
|
|
|
|
|
|
|
|
|
|
|
|
# Temporary, switches emails.json over from using Usernames to User IDs.
|
|
|
|
switchToIds()
|
|
|
|
|
2020-06-27 14:38:51 +00:00
|
|
|
|
|
|
|
from packaging import version
|
|
|
|
|
|
|
|
if (
|
|
|
|
version.parse(jf.info["Version"]) >= version.parse("10.6.0")
|
|
|
|
and bool(data_store.user_template) is not False
|
|
|
|
):
|
|
|
|
log.info("Updating user_template for Jellyfin >= 10.6.0")
|
2020-06-27 14:45:12 +00:00
|
|
|
if (
|
|
|
|
data_store.user_template["AuthenticationProviderId"]
|
|
|
|
== "Emby.Server.Implementations.Library.DefaultAuthenticationProvider"
|
|
|
|
):
|
|
|
|
data_store.user_template[
|
|
|
|
"AuthenticationProviderId"
|
|
|
|
] = "Jellyfin.Server.Implementations.Users.DefaultAuthenticationProvider"
|
|
|
|
if (
|
|
|
|
data_store.user_template["PasswordResetProviderId"]
|
|
|
|
== "Emby.Server.Implementations.Library.DefaultPasswordResetProvider"
|
|
|
|
):
|
|
|
|
data_store.user_template[
|
|
|
|
"PasswordResetProviderId"
|
|
|
|
] = "Jellyfin.Server.Implementations.Users.DefaultPasswordResetProvider"
|
2020-06-27 14:38:51 +00:00
|
|
|
|
|
|
|
|
2020-06-21 19:29:53 +00:00
|
|
|
if config.getboolean("password_validation", "enabled"):
|
|
|
|
validator = PasswordValidator(
|
|
|
|
config["password_validation"]["min_length"],
|
|
|
|
config["password_validation"]["upper"],
|
|
|
|
config["password_validation"]["lower"],
|
|
|
|
config["password_validation"]["number"],
|
|
|
|
config["password_validation"]["special"],
|
|
|
|
)
|
2020-04-14 20:31:44 +00:00
|
|
|
else:
|
2020-04-16 13:33:23 +00:00
|
|
|
validator = PasswordValidator(0, 0, 0, 0, 0)
|
2020-04-14 20:31:44 +00:00
|
|
|
|
|
|
|
|
2020-06-21 19:29:53 +00:00
|
|
|
@app.route("/newUser", methods=["POST"])
|
2020-04-11 14:20:25 +00:00
|
|
|
def newUser():
|
|
|
|
data = request.get_json()
|
2020-06-21 19:29:53 +00:00
|
|
|
log.debug("Attempted newUser")
|
|
|
|
if checkInvite(data["code"]):
|
|
|
|
validation = validator.validate(data["password"])
|
2020-04-14 20:31:44 +00:00
|
|
|
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:
|
2020-06-21 19:29:53 +00:00
|
|
|
log.debug("User password valid")
|
2020-06-07 14:00:31 +00:00
|
|
|
try:
|
2020-06-21 19:29:53 +00:00
|
|
|
user = jf.newUser(data["username"], data["password"])
|
2020-04-14 20:31:44 +00:00
|
|
|
except Jellyfin.UserExistsError:
|
2020-06-14 16:58:18 +00:00
|
|
|
error = f'User already exists named {data["username"]}'
|
2020-04-14 20:31:44 +00:00
|
|
|
log.debug(error)
|
2020-06-21 19:29:53 +00:00
|
|
|
return jsonify({"error": error})
|
2020-04-11 14:20:25 +00:00
|
|
|
except:
|
2020-06-21 19:29:53 +00:00
|
|
|
return jsonify({"error": "Unknown error"})
|
|
|
|
checkInvite(data["code"], delete=True)
|
2020-04-14 20:31:44 +00:00
|
|
|
if user.status_code == 200:
|
2020-04-11 14:20:25 +00:00
|
|
|
try:
|
2020-06-14 16:58:18 +00:00
|
|
|
policy = data_store.user_template
|
|
|
|
if policy != {}:
|
2020-06-21 19:29:53 +00:00
|
|
|
jf.setPolicy(user.json()["Id"], policy)
|
2020-06-14 16:58:18 +00:00
|
|
|
else:
|
2020-06-21 19:29:53 +00:00
|
|
|
log.debug("user policy was blank")
|
2020-04-14 20:31:44 +00:00
|
|
|
except:
|
2020-06-21 19:29:53 +00:00
|
|
|
log.error("Failed to set new user policy")
|
2020-06-07 14:00:31 +00:00
|
|
|
try:
|
2020-06-14 16:58:18 +00:00
|
|
|
configuration = data_store.user_configuration
|
|
|
|
displayprefs = data_store.user_displayprefs
|
|
|
|
if configuration != {} and displayprefs != {}:
|
2020-06-21 19:29:53 +00:00
|
|
|
if jf.setConfiguration(user.json()["Id"], configuration):
|
|
|
|
jf.setDisplayPreferences(user.json()["Id"], displayprefs)
|
|
|
|
log.debug("Set homescreen layout.")
|
2020-06-14 16:58:18 +00:00
|
|
|
else:
|
2020-06-21 19:29:53 +00:00
|
|
|
log.debug(
|
|
|
|
"user configuration and/or " + "displayprefs were blank"
|
|
|
|
)
|
2020-06-07 14:00:31 +00:00
|
|
|
except:
|
2020-06-21 19:29:53 +00:00
|
|
|
log.error("Failed to set new user homescreen layout")
|
|
|
|
if config.getboolean("password_resets", "enabled"):
|
|
|
|
data_store.emails[user.json()["Id"]] = data["email"]
|
|
|
|
log.debug("Email address stored")
|
|
|
|
log.info("New user created")
|
2020-04-14 20:31:44 +00:00
|
|
|
else:
|
2020-06-21 19:29:53 +00:00
|
|
|
log.error(f"New user creation failed: {user.status_code}")
|
2020-04-14 20:31:44 +00:00
|
|
|
return resp(False)
|
2020-04-11 14:20:25 +00:00
|
|
|
else:
|
2020-06-21 19:29:53 +00:00
|
|
|
log.debug("User password invalid")
|
2020-04-14 20:31:44 +00:00
|
|
|
return jsonify(validation)
|
2020-04-11 14:20:25 +00:00
|
|
|
else:
|
2020-06-21 19:29:53 +00:00
|
|
|
log.debug("Attempted newUser unauthorized")
|
2020-04-11 14:20:25 +00:00
|
|
|
return resp(False, code=401)
|
|
|
|
|
|
|
|
|
2020-06-21 19:29:53 +00:00
|
|
|
@app.route("/generateInvite", methods=["POST"])
|
2020-04-11 14:20:25 +00:00
|
|
|
@auth.login_required
|
|
|
|
def generateInvite():
|
|
|
|
current_time = datetime.datetime.now()
|
|
|
|
data = request.get_json()
|
2020-06-21 19:29:53 +00:00
|
|
|
delta = datetime.timedelta(hours=int(data["hours"]), minutes=int(data["minutes"]))
|
2020-06-14 16:58:18 +00:00
|
|
|
invite_code = secrets.token_urlsafe(16)
|
|
|
|
invite = {}
|
2020-06-21 19:29:53 +00:00
|
|
|
log.debug(f"Creating new invite: {invite_code}")
|
2020-04-19 21:35:51 +00:00
|
|
|
valid_till = current_time + delta
|
2020-06-21 19:29:53 +00:00
|
|
|
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":
|
2020-04-19 21:35:51 +00:00
|
|
|
from jellyfin_accounts.email import Mailgun
|
2020-06-21 19:29:53 +00:00
|
|
|
|
2020-04-19 21:35:51 +00:00
|
|
|
email = Mailgun(address)
|
2020-06-21 19:29:53 +00:00
|
|
|
elif method == "smtp":
|
2020-04-19 21:35:51 +00:00
|
|
|
from jellyfin_accounts.email import Smtp
|
2020-06-21 19:29:53 +00:00
|
|
|
|
2020-04-19 21:35:51 +00:00
|
|
|
email = Smtp(address)
|
2020-06-21 19:29:53 +00:00
|
|
|
email.construct_invite({"expiry": valid_till, "code": invite_code})
|
2020-05-05 10:37:13 +00:00
|
|
|
response = email.send()
|
|
|
|
if response is False or type(response) != bool:
|
2020-06-21 19:29:53 +00:00
|
|
|
invite["email"] = f"Failed to send to {address}"
|
2020-06-14 16:58:18 +00:00
|
|
|
data_store.invites[invite_code] = invite
|
2020-06-21 19:29:53 +00:00
|
|
|
log.info(f"New invite created: {invite_code}")
|
2020-04-11 14:20:25 +00:00
|
|
|
return resp()
|
|
|
|
|
|
|
|
|
2020-06-21 19:29:53 +00:00
|
|
|
@app.route("/getInvites", methods=["GET"])
|
2020-04-11 14:20:25 +00:00
|
|
|
@auth.login_required
|
|
|
|
def getInvites():
|
2020-06-21 19:29:53 +00:00
|
|
|
log.debug("Invites requested")
|
2020-04-11 14:20:25 +00:00
|
|
|
current_time = datetime.datetime.now()
|
2020-06-14 16:58:18 +00:00
|
|
|
invites = dict(data_store.invites)
|
|
|
|
for code in invites:
|
|
|
|
checkInvite(code)
|
|
|
|
invites = dict(data_store.invites)
|
2020-06-21 19:29:53 +00:00
|
|
|
response = {"invites": []}
|
2020-06-14 16:58:18 +00:00
|
|
|
for code in invites:
|
2020-06-21 19:29:53 +00:00
|
|
|
expiry = datetime.datetime.strptime(
|
|
|
|
invites[code]["valid_till"], "%Y-%m-%dT%H:%M:%S.%f"
|
|
|
|
)
|
2020-06-14 16:58:18 +00:00
|
|
|
valid_for = expiry - current_time
|
2020-06-21 19:29:53 +00:00
|
|
|
invite = {
|
|
|
|
"code": code,
|
|
|
|
"hours": valid_for.seconds // 3600,
|
|
|
|
"minutes": (valid_for.seconds // 60) % 60,
|
|
|
|
}
|
|
|
|
if "email" in invites[code]:
|
|
|
|
invite["email"] = invites[code]["email"]
|
|
|
|
response["invites"].append(invite)
|
2020-04-19 21:35:51 +00:00
|
|
|
return jsonify(response)
|
2020-04-11 14:20:25 +00:00
|
|
|
|
2020-06-21 19:29:53 +00:00
|
|
|
|
|
|
|
@app.route("/deleteInvite", methods=["POST"])
|
2020-04-11 14:20:25 +00:00
|
|
|
@auth.login_required
|
|
|
|
def deleteInvite():
|
2020-06-21 19:29:53 +00:00
|
|
|
code = request.get_json()["code"]
|
2020-06-14 16:58:18 +00:00
|
|
|
invites = dict(data_store.invites)
|
|
|
|
if code in invites:
|
|
|
|
del data_store.invites[code]
|
2020-06-21 19:29:53 +00:00
|
|
|
log.info(f"Invite deleted: {code}")
|
2020-04-11 14:20:25 +00:00
|
|
|
return resp()
|
|
|
|
|
|
|
|
|
2020-06-21 19:29:53 +00:00
|
|
|
@app.route("/getToken")
|
2020-04-11 14:20:25 +00:00
|
|
|
@auth.login_required
|
|
|
|
def get_token():
|
|
|
|
token = g.user.generate_token()
|
2020-06-21 19:29:53 +00:00
|
|
|
return jsonify({"token": token.decode("ascii")})
|
2020-04-11 14:20:25 +00:00
|
|
|
|
|
|
|
|
2020-06-21 19:29:53 +00:00
|
|
|
@app.route("/getUsers", methods=["GET"])
|
2020-04-20 19:37:39 +00:00
|
|
|
@auth.login_required
|
|
|
|
def getUsers():
|
2020-06-21 19:29:53 +00:00
|
|
|
log.debug("User and email list requested")
|
|
|
|
response = {"users": []}
|
2020-04-20 19:37:39 +00:00
|
|
|
users = jf.getUsers(public=False)
|
2020-06-14 16:58:18 +00:00
|
|
|
emails = data_store.emails
|
2020-04-20 19:37:39 +00:00
|
|
|
for user in users:
|
2020-06-21 19:29:53 +00:00
|
|
|
entry = {"name": user["Name"]}
|
|
|
|
if user["Id"] in emails:
|
|
|
|
entry["email"] = emails[user["Id"]]
|
|
|
|
response["users"].append(entry)
|
2020-04-20 19:37:39 +00:00
|
|
|
return jsonify(response)
|
|
|
|
|
2020-06-14 16:58:18 +00:00
|
|
|
|
2020-06-21 19:29:53 +00:00
|
|
|
@app.route("/modifyUsers", methods=["POST"])
|
2020-04-20 19:37:39 +00:00
|
|
|
@auth.login_required
|
|
|
|
def modifyUsers():
|
|
|
|
data = request.get_json()
|
2020-06-21 19:29:53 +00:00
|
|
|
log.debug("Email list modification requested")
|
2020-04-20 19:37:39 +00:00
|
|
|
for key in data:
|
2020-06-21 19:29:53 +00:00
|
|
|
uid = jf.getUsers(key, public=False)["Id"]
|
2020-06-14 16:58:18 +00:00
|
|
|
data_store.emails[uid] = data[key]
|
2020-04-20 19:37:39 +00:00
|
|
|
log.debug(f'Email for user "{key}" modified')
|
2020-06-14 16:58:18 +00:00
|
|
|
return resp()
|
2020-04-20 19:37:39 +00:00
|
|
|
|
2020-06-08 12:33:04 +00:00
|
|
|
|
2020-06-21 19:29:53 +00:00
|
|
|
@app.route("/setDefaults", methods=["POST"])
|
2020-06-08 12:33:04 +00:00
|
|
|
@auth.login_required
|
|
|
|
def setDefaults():
|
|
|
|
data = request.get_json()
|
2020-06-21 19:29:53 +00:00
|
|
|
username = data["username"]
|
|
|
|
log.debug(f"Storing default settings from user {username}")
|
2020-06-08 12:33:04 +00:00
|
|
|
try:
|
2020-06-21 19:29:53 +00:00
|
|
|
user = jf.getUsers(username=username, public=False)
|
2020-06-08 12:33:04 +00:00
|
|
|
except Jellyfin.UserNotFoundError:
|
2020-06-21 19:29:53 +00:00
|
|
|
log.error(f"Storing defaults failed: Couldn't find user {username}")
|
2020-06-14 16:58:18 +00:00
|
|
|
return resp(False)
|
2020-06-21 19:29:53 +00:00
|
|
|
uid = user["Id"]
|
|
|
|
policy = user["Policy"]
|
2020-06-14 16:58:18 +00:00
|
|
|
data_store.user_template = policy
|
2020-06-21 19:29:53 +00:00
|
|
|
if data["homescreen"]:
|
|
|
|
configuration = user["Configuration"]
|
2020-06-08 12:33:04 +00:00
|
|
|
try:
|
2020-06-14 16:58:18 +00:00
|
|
|
displayprefs = jf.getDisplayPreferences(uid)
|
|
|
|
data_store.user_configuration = configuration
|
|
|
|
data_store.user_displayprefs = displayprefs
|
2020-06-08 12:33:04 +00:00
|
|
|
except:
|
2020-06-21 19:29:53 +00:00
|
|
|
log.error("Storing defaults failed: " + "couldn't store homescreen layout")
|
2020-06-14 16:58:18 +00:00
|
|
|
return resp(False)
|
2020-06-08 12:33:04 +00:00
|
|
|
return resp()
|
2020-06-14 16:58:18 +00:00
|
|
|
|
2020-06-21 19:29:53 +00:00
|
|
|
|
|
|
|
import jellyfin_accounts.setup
|