def inviteProxy(path): if checkInvite(path): log.info(f"Invite {path} used to request form") try: email = data_store.invites[path]["email"] except KeyError: email = "" return render_template( "form.html", bs5=config.getboolean("ui", "bs5"), css_file=css_file, contactMessage=config["ui"]["contact_message"], helpMessage=config["ui"]["help_message"], successMessage=config["ui"]["success_message"], jfLink=config["jellyfin"]["public_server"], validate=config.getboolean("password_validation", "enabled"), requirements=validator().getCriteria(), email=email, username=(not config.getboolean("email", "no_username")), ) elif "admin.html" not in path and "admin.html" not in path: return app.send_static_file(path) else: log.debug("Attempted use of invalid invite") return render_template( "invalidCode.html", bs5=config.getboolean("ui", "bs5"), css_file=css_file, contactMessage=config["ui"]["contact_message"], )
def admin(): return render_template( "admin.html", bs5=config.getboolean("ui", "bs5"), css_file=css_file, contactMessage="", email_enabled=config.getboolean("invite_emails", "enabled"), )
def format_datetime(dt): result = dt.strftime(config["email"]["date_format"]) if config.getboolean("email", "use_24h"): result += f' {dt.strftime("%H:%M")}' else: result += f' {dt.strftime("%I:%M %p")}' return result
def generateInvite(): current_time = datetime.datetime.now() data = request.get_json() delta = datetime.timedelta(days=int(data["days"]), hours=int(data["hours"]), minutes=int(data["minutes"])) invite_code = secrets.token_urlsafe(16) invite = {} invite["created"] = format_datetime(current_time) if data["multiple-uses"]: if data["no-limit"]: invite["no-limit"] = True else: invite["remaining-uses"] = int(data["remaining-uses"]) else: invite["remaining-uses"] = 1 log.debug(f"Creating new invite: {invite_code}") 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}) response = email.send() if response is False or type(response) != bool: invite["email"] = f"Failed to send to {address}" if config.getboolean("notifications", "enabled"): if "notify-creation" in data: invite["notify-creation"] = data["notify-creation"] if "notify-expiry" in data: invite["notify-expiry"] = data["notify-expiry"] data_store.invites[invite_code] = invite log.info(f"New invite created: {invite_code}") return resp()
def page_not_found(e): return ( render_template( "404.html", bs5=config.getboolean("ui", "bs5"), css_file=css_file, contactMessage=config["ui"]["contact_message"], ), 404, )
def validator(): if config.getboolean("password_validation", "enabled"): return PasswordValidator( config["password_validation"]["min_length"], config["password_validation"]["upper"], config["password_validation"]["lower"], config["password_validation"]["number"], config["password_validation"]["special"], ) return PasswordValidator(0, 0, 0, 0, 0)
def verify_password(username, password): user = None verified = False log.debug("Verifying auth") if config.getboolean("ui", "jellyfin_login"): try: jf_user = jf.getUsers(username, public=False) id = jf_user["Id"] user = accounts[id] except KeyError: if config.getboolean("ui", "admin_only"): if jf_user["Policy"]["IsAdministrator"]: user = Account(username) accounts[id] = user else: log.debug(f"User {username} not admin.") return False else: user = Account(username) accounts[id] = user except Jellyfin.UserNotFoundError: user = Account().verify_token(username, accounts) if user: verified = True if user in accounts: user = accounts[user] if not user: log.debug(f"User {username} not found on Jellyfin") return False else: user = accounts["adminAccount"] verified = Account().verify_token(username, accounts) if not verified: if username == user.username and user.verify_password(password): g.user = user log.debug("HTTPAuth Allowed") return user else: log.debug("HTTPAuth Denied") return False g.user = user log.debug("HTTPAuth Allowed") return user
def static_proxy(path): if "html" not in path: if "admin.js" in path: return ( render_template( "admin.js", bsVersion=bsVersion(), css_file=css_file, notifications=config.getboolean("notifications", "enabled"), ), 200, {"Content-Type": "text/javascript"}, ) return app.send_static_file(path) return ( render_template( "404.html", bs5=config.getboolean("ui", "bs5"), css_file=css_file, contactMessage=config["ui"]["contact_message"], ), 404, )
def verify_token(token, accounts): log.debug(f"verifying token {token}") s = Serializer(app.config["SECRET_KEY"]) try: data = s.loads(token) except SignatureExpired: return None except BadSignature: return None if config.getboolean("ui", "jellyfin_login"): for account in accounts: if data["id"] == accounts[account].id: return account else: return accounts["adminAccount"]
def checkInvite(code, used=False, username=None): current_time = datetime.datetime.now() invites = dict(data_store.invites) match = False for invite in invites: if ("remaining-uses" not in invites[invite] and "no-limit" not in invites[invite]): invites[invite]["remaining-uses"] = 1 expiry = datetime.datetime.strptime(invites[invite]["valid_till"], "%Y-%m-%dT%H:%M:%S.%f") if current_time >= expiry or ("no-limit" not in invites[invite] and invites[invite]["remaining-uses"] < 1): log.debug(f"Housekeeping: Deleting expired invite {invite}") if (config.getboolean("notifications", "enabled") and "notify" in invites[invite]): for address in invites[invite]["notify"]: if "notify-expiry" in invites[invite]["notify"][address]: if invites[invite]["notify"][address]["notify-expiry"]: method = config["email"]["method"] if method == "mailgun": email = Mailgun(address) elif method == "smtp": email = Smtp(address) if email.construct_expiry({ "code": invite, "expiry": expiry }): threading.Thread(target=email.send).start() del data_store.invites[invite] elif invite == code: match = True if used: delete = False inv = dict(data_store.invites[code]) if "used-by" not in inv: inv["used-by"] = [] if "remaining-uses" in inv: if inv["remaining-uses"] == 1: delete = True del data_store.invites[code] elif "no-limit" not in invites[invite]: inv["remaining-uses"] -= 1 inv["used-by"].append( [username, format_datetime(current_time)]) if not delete: data_store.invites[code] = inv return match
def getInvites(): log.debug("Invites requested") current_time = datetime.datetime.now() invites = dict(data_store.invites) for code in invites: checkInvite(code) invites = dict(data_store.invites) response = {"invites": []} for code in invites: expiry = datetime.datetime.strptime(invites[code]["valid_till"], "%Y-%m-%dT%H:%M:%S.%f") valid_for = expiry - current_time invite = { "code": code, "days": valid_for.days, "hours": valid_for.seconds // 3600, "minutes": (valid_for.seconds // 60) % 60, } if "created" in invites[code]: invite["created"] = invites[code]["created"] if "used-by" in invites[code]: invite["used-by"] = invites[code]["used-by"] if "no-limit" in invites[code]: invite["no-limit"] = invites[code]["no-limit"] if "remaining-uses" in invites[code]: invite["remaining-uses"] = invites[code]["remaining-uses"] else: invite["remaining-uses"] = 1 if "email" in invites[code]: invite["email"] = invites[code]["email"] if "notify" in invites[code]: if config.getboolean("ui", "jellyfin_login"): address = data_store.emails[g.user.id] else: address = config["ui"]["email"] if address in invites[code]["notify"]: if "notify-expiry" in invites[code]["notify"][address]: invite["notify-expiry"] = invites[code]["notify"][address][ "notify-expiry"] if "notify-creation" in invites[code]["notify"][address]: invite["notify-creation"] = invites[code]["notify"][ address]["notify-creation"] response["invites"].append(invite) return jsonify(response)
def setNotify(): data = request.get_json() change = False for code in data: for key in data[code]: if key in ["notify-expiry", "notify-creation"]: inv = data_store.invites[code] if config.getboolean("ui", "jellyfin_login"): address = data_store.emails[g.user.id] else: address = config["ui"]["email"] if "notify" not in inv: inv["notify"] = {} if address not in inv["notify"]: inv["notify"][address] = {} inv["notify"][address][key] = data[code][key] log.debug(f"{code}: Notification settings changed") change = True if change: data_store.invites[code] = inv return resp() return resp(success=False)
def construct_created(self, invite): self.subject = "Notice: User created" log.debug( f'Constructing user creation notification for {invite["code"]}') created = format_datetime(invite["created"]) if config.getboolean("email", "no_username"): email = "n/a" else: email = invite["address"] for key in ["text", "html"]: fpath = Path(config["notifications"]["created_" + key]) with open(fpath, 'r') as f: template = Template(f.read()) c = template.render( code=invite["code"], username=invite["username"], address=email, time=created, ) self.content[key] = c log.info(f"{self.address}: {key} constructed") return True
def pretty_time(self, expiry, tzaware=False): if tzaware: current_time = datetime.datetime.utcnow().replace(tzinfo=pytz.utc) else: current_time = datetime.datetime.now() date = expiry.strftime(config["email"]["date_format"]) if config.getboolean("email", "use_24h"): log.debug(f"{self.address}: Using 24h time") time = expiry.strftime("%H:%M") else: log.debug(f"{self.address}: Using 12h time") time = expiry.strftime("%-I:%M %p") expiry_delta = (expiry - current_time).seconds expires_in = { "hours": expiry_delta // 3600, "minutes": (expiry_delta // 60) % 60, } if expires_in["hours"] == 0: expires_in = f'{str(expires_in["minutes"])}m' else: expires_in = f'{str(expires_in["hours"])}h {str(expires_in["minutes"])}m' log.debug(f"{self.address}: Expires in {expires_in}") return {"date": date, "time": time, "expires_in": expires_in}
def newUser(): data = request.get_json() log.debug("Attempted newUser") if checkInvite(data["code"]): validation = validator().validate(data["password"]) valid = True for criterion in validation: if validation[criterion] is False: valid = False if valid: log.debug("User password valid") try: user = jf.newUser(data["username"], data["password"]) except Jellyfin.UserExistsError: error = f'User already exists named {data["username"]}' log.debug(error) return jsonify({"error": error}) except: return jsonify({"error": "Unknown error"}) invites = dict(data_store.invites) checkInvite(data["code"], used=True, username=data["username"]) if (config.getboolean("notifications", "enabled") and "notify" in invites[data["code"]]): for address in invites[data["code"]]["notify"]: if "notify-creation" in invites[ data["code"]]["notify"][address]: if invites[data["code"]]["notify"][address][ "notify-creation"]: method = config["email"]["method"] if method == "mailgun": email = Mailgun(address) elif method == "smtp": email = Smtp(address) if email.construct_created({ "code": data["code"], "username": data["username"], "created": datetime.datetime.now(), }): threading.Thread(target=email.send).start() if user.status_code == 200: try: policy = data_store.user_template if policy != {}: jf.setPolicy(user.json()["Id"], policy) else: log.debug("user policy was blank") except: log.error("Failed to set new user policy") try: configuration = data_store.user_configuration displayprefs = data_store.user_displayprefs if configuration != {} and displayprefs != {}: if jf.setConfiguration(user.json()["Id"], configuration): jf.setDisplayPreferences(user.json()["Id"], displayprefs) log.debug("Set homescreen layout.") else: log.debug( "user configuration and/or displayprefs were blank" ) except: 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") else: log.error(f"New user creation failed: {user.status_code}") return resp(False) else: log.debug("User password invalid") return jsonify(validation) else: log.debug("Attempted newUser unauthorized") return resp(False, code=401)
return None except BadSignature: return None if config.getboolean("ui", "jellyfin_login"): for account in accounts: if data["id"] == accounts[account].id: return account else: return accounts["adminAccount"] auth = HTTPBasicAuth() accounts = {} if config.getboolean("ui", "jellyfin_login"): log.debug("Using jellyfin for admin authentication") else: log.debug("Using configured login details for admin authentication") accounts["adminAccount"] = Account(config["ui"]["username"], config["ui"]["password"]) @auth.verify_password def verify_password(username, password): user = None verified = False log.debug("Verifying auth") if config.getboolean("ui", "jellyfin_login"): try: jf_user = jf.getUsers(username, public=False)
def bsVersion(): if config.getboolean("ui", "bs5"): return 5 return 4
self.is_running = False self.next_call = time.time() self.start() def _run(self): self.is_running = False self.start() self.function(*self.args, **self.kwargs) def start(self): if not self.is_running: self.next_call += self.interval self._timer = Timer(self.next_call - time.time(), self._run) self._timer.start() self.is_running = True def stop(self): self._timer.cancel() self.is_running = False def checkInvites(): invites = dict(data_store.invites) # checkInvite already loops over everything, no point running it multiple times. if len(invites) != 0: checkInvite(list(invites.keys())[0]) if config.getboolean("notifications", "enabled"): inviteDaemon = Repeat(60, checkInvites)