mirror of
https://github.com/hrfee/jellyfin-accounts.git
synced 2025-12-08 11:39:33 +00:00
first commit
This commit is contained in:
0
jellyfin_accounts/__init__.py
Normal file
0
jellyfin_accounts/__init__.py
Normal file
82
jellyfin_accounts/jf_api.py
Normal file
82
jellyfin_accounts/jf_api.py
Normal file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
import requests
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
class Jellyfin:
|
||||
class UserExistsError(Error):
|
||||
pass
|
||||
class UserNotFoundError(Error):
|
||||
pass
|
||||
class AuthenticationError(Error):
|
||||
pass
|
||||
class AuthenticationRequiredError(Error):
|
||||
pass
|
||||
def __init__(self, server, client, version, device, deviceId):
|
||||
self.server = server
|
||||
self.client = client
|
||||
self.version = version
|
||||
self.device = device
|
||||
self.deviceId = deviceId
|
||||
self.useragent = f"{self.client}/{self.version}"
|
||||
self.auth = "MediaBrowser "
|
||||
self.auth += f"Client={self.client}, "
|
||||
self.auth += f"Device={self.device}, "
|
||||
self.auth += f"DeviceId={self.deviceId}, "
|
||||
self.auth += f"Version={self.version}"
|
||||
self.header = {
|
||||
"Accept": "application/json",
|
||||
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
"X-Application": f"{self.client}/{self.version}",
|
||||
"Accept-Charset": "UTF-8,*",
|
||||
"Accept-encoding": "gzip",
|
||||
"User-Agent": self.useragent,
|
||||
"X-Emby-Authorization": self.auth
|
||||
}
|
||||
def getUsers(self, username="all"):
|
||||
response = requests.get(self.server+"/emby/Users/Public").json()
|
||||
if username == "all":
|
||||
return response
|
||||
else:
|
||||
match = False
|
||||
for user in response:
|
||||
if user['Name'] == username:
|
||||
match = True
|
||||
return user
|
||||
if not match:
|
||||
raise self.UserNotFoundError
|
||||
def authenticate(self, username, password):
|
||||
self.username = username
|
||||
self.password = password
|
||||
response = requests.post(self.server+"/emby/Users/AuthenticateByName",
|
||||
headers=self.header,
|
||||
params={'Username': self.username,
|
||||
'Pw': self.password})
|
||||
if response.status_code == 200:
|
||||
json = response.json()
|
||||
self.userId = json['User']['Id']
|
||||
self.accessToken = json['AccessToken']
|
||||
self.auth += f", Token={self.accessToken}"
|
||||
self.header['X-Emby-Authorization'] = self.auth
|
||||
return True
|
||||
else:
|
||||
raise self.AuthenticationError
|
||||
def setPolicy(self, userId, policy):
|
||||
return requests.post(self.server+"/Users/"+userId+"/Policy",
|
||||
headers=self.header,
|
||||
params=policy)
|
||||
def newUser(self, username, password):
|
||||
for user in self.getUsers():
|
||||
if user['Name'] == username:
|
||||
raise self.UserExistsError
|
||||
response = requests.post(self.server+"/emby/Users/New",
|
||||
headers=self.header,
|
||||
params={'Name': username,
|
||||
'Password': password})
|
||||
if response.status_code == 401:
|
||||
raise self.AuthenticationRequiredError
|
||||
return response
|
||||
|
||||
# template user's policies should be copied to each new account.
|
||||
55
jellyfin_accounts/login.py
Normal file
55
jellyfin_accounts/login.py
Normal file
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
# from flask import g
|
||||
|
||||
from flask_httpauth import HTTPBasicAuth
|
||||
from itsdangerous import (TimedJSONWebSignatureSerializer
|
||||
as Serializer, BadSignature, SignatureExpired)
|
||||
from passlib.apps import custom_app_context as pwd_context
|
||||
import uuid
|
||||
from __main__ import config, app, g
|
||||
|
||||
|
||||
class Account():
|
||||
def __init__(self, username, password):
|
||||
self.username = username
|
||||
self.password_hash = pwd_context.hash(password)
|
||||
self.id = str(uuid.uuid4())
|
||||
def verify_password(self, password):
|
||||
return pwd_context.verify(password, self.password_hash)
|
||||
def generate_token(self, expiration=1200):
|
||||
s = Serializer(app.config['SECRET_KEY'], expires_in=expiration)
|
||||
return s.dumps({ 'id': self.id })
|
||||
|
||||
@staticmethod
|
||||
def verify_token(token, account):
|
||||
s = Serializer(app.config['SECRET_KEY'])
|
||||
try:
|
||||
data = s.loads(token)
|
||||
except SignatureExpired:
|
||||
return None
|
||||
except BadSignature:
|
||||
return None
|
||||
if data['id'] == account.id:
|
||||
return account
|
||||
|
||||
auth = HTTPBasicAuth()
|
||||
|
||||
|
||||
adminAccount = Account(config['ui']['username'], config['ui']['password'])
|
||||
|
||||
|
||||
@auth.verify_password
|
||||
def verify_password(username, password):
|
||||
user = adminAccount.verify_token(username, adminAccount)
|
||||
if not user:
|
||||
if username == adminAccount.username and adminAccount.verify_password(password):
|
||||
g.user = adminAccount
|
||||
print(g)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
g.user = adminAccount
|
||||
return True
|
||||
|
||||
|
||||
|
||||
41
jellyfin_accounts/web.py
Normal file
41
jellyfin_accounts/web.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from pathlib import Path
|
||||
from flask import Flask, send_from_directory, render_template
|
||||
from __main__ import config, app, g
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(e):
|
||||
return render_template('404.html',
|
||||
contactMessage=config['ui']['contact_message']), 404
|
||||
|
||||
|
||||
@app.route('/', methods=['GET', 'POST'])
|
||||
def admin():
|
||||
# return app.send_static_file('admin.html')
|
||||
return render_template('admin.html',
|
||||
contactMessage='')
|
||||
|
||||
|
||||
@app.route('/<path:path>')
|
||||
def static_proxy(path):
|
||||
if 'form.html' not in path and 'admin.html' not in path:
|
||||
return app.send_static_file(path)
|
||||
return render_template('404.html',
|
||||
contactMessage=config['ui']['contact_message']), 404
|
||||
|
||||
from jellyfin_accounts.web_api import checkInvite
|
||||
|
||||
|
||||
@app.route('/invite/<path:path>')
|
||||
def inviteProxy(path):
|
||||
if checkInvite(path):
|
||||
return render_template('form.html',
|
||||
contactMessage=config['ui']['contact_message'],
|
||||
helpMessage=config['ui']['help_message'],
|
||||
successMessage=config['ui']['success_message'],
|
||||
jfLink=config['jellyfin']['server'])
|
||||
elif 'admin.html' not in path and 'admin.html' not in path:
|
||||
return app.send_static_file(path)
|
||||
else:
|
||||
return render_template('invalidCode.html',
|
||||
contactMessage=config['ui']['contact_message'])
|
||||
150
jellyfin_accounts/web_api.py
Normal file
150
jellyfin_accounts/web_api.py
Normal file
@@ -0,0 +1,150 @@
|
||||
from flask import request, jsonify
|
||||
from jellyfin_accounts.jf_api import Jellyfin
|
||||
import json
|
||||
import datetime
|
||||
import secrets
|
||||
from __main__ import config, app, g
|
||||
from jellyfin_accounts.login import auth
|
||||
|
||||
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:
|
||||
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'])
|
||||
|
||||
jf.authenticate(config['jellyfin']['username'],
|
||||
config['jellyfin']['password'])
|
||||
|
||||
|
||||
@app.route('/newUser', methods=['GET', 'POST'])
|
||||
def newUser():
|
||||
data = request.get_json()
|
||||
if checkInvite(data['code'], delete=True):
|
||||
user = jf.newUser(data['username'], data['password'])
|
||||
if user.status_code == 200:
|
||||
try:
|
||||
with open(config['files']['user_template'], 'r') as f:
|
||||
default_policy = json.load(f)
|
||||
jf.setPolicy(user.json()['Id'], default_policy)
|
||||
except:
|
||||
pass
|
||||
if config['ui']['emails_enabled'] == 'true':
|
||||
try:
|
||||
with open(config['files']['emails'], 'r') as f:
|
||||
emails = json.load(f)
|
||||
except (FileNotFoundError, json.decoder.JSONDecodeError):
|
||||
emails = {}
|
||||
emails[data['username']] = data['email']
|
||||
with open(config['files']['emails'], 'w') as f:
|
||||
f.write(json.dumps(emails, indent=4))
|
||||
return resp()
|
||||
else:
|
||||
return resp(False)
|
||||
else:
|
||||
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()
|
||||
delta = datetime.timedelta(hours=int(data['hours']),
|
||||
minutes=int(data['minutes']))
|
||||
invite = {'code': secrets.token_urlsafe(16)}
|
||||
invite['valid_till'] = (current_time +
|
||||
delta).strftime('%Y-%m-%dT%H:%M:%S.%f')
|
||||
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))
|
||||
return resp()
|
||||
|
||||
|
||||
@app.route('/getInvites', methods=['GET'])
|
||||
@auth.login_required
|
||||
def getInvites():
|
||||
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']):
|
||||
expiry = datetime.datetime.strptime(i['valid_till'], '%Y-%m-%dT%H:%M:%S.%f')
|
||||
if current_time >= expiry:
|
||||
del invites['invites'][index]
|
||||
else:
|
||||
valid_for = expiry - current_time
|
||||
response['invites'].append({
|
||||
'code': i['code'],
|
||||
'hours': valid_for.seconds//3600,
|
||||
'minutes': (valid_for.seconds//60) % 60})
|
||||
with open(config['files']['invites'], 'w') as f:
|
||||
f.write(json.dumps(invites, indent=4, default=str))
|
||||
return jsonify(response)
|
||||
|
||||
|
||||
@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))
|
||||
return resp()
|
||||
|
||||
|
||||
@app.route('/getToken')
|
||||
@auth.login_required
|
||||
def get_token():
|
||||
token = g.user.generate_token()
|
||||
return jsonify({'token': token.decode('ascii')})
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user