mirror of
https://github.com/hrfee/jellyfin-accounts.git
synced 2024-12-23 01:20:12 +00:00
Add setup wizard
Added a setup wizard that appears on first run, or when no config dir is found.
This commit is contained in:
parent
16a7ceb8ea
commit
922987454f
@ -82,9 +82,12 @@ optional arguments:
|
|||||||
#### Configuration
|
#### Configuration
|
||||||
* Note: Make sure to put this behind a reverse proxy with HTTPS.
|
* Note: Make sure to put this behind a reverse proxy with HTTPS.
|
||||||
|
|
||||||
|
On first run, access the setup wizard at `0.0.0.0:8056`. When finished, restart the program.
|
||||||
|
|
||||||
|
The configuration is stored at `~/.jf-accounts/config.ini`.
|
||||||
|
|
||||||
For detailed descriptions of each setting, see [setup](https://github.com/hrfee/jellyfin-accounts/wiki/Setup).
|
For detailed descriptions of each setting, see [setup](https://github.com/hrfee/jellyfin-accounts/wiki/Setup).
|
||||||
|
|
||||||
On first run, the default configuration is copied to `~/.jf-accounts/config.ini`.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
[jellyfin]
|
[jellyfin]
|
||||||
|
234
data/static/setup.js
Normal file
234
data/static/setup.js
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
function checkAuthRadio() {
|
||||||
|
if (document.getElementById('manualAuthRadio').checked) {
|
||||||
|
document.getElementById('adminOnlyArea').style.display = 'none';
|
||||||
|
document.getElementById('manualAuthArea').style.display = '';
|
||||||
|
} else {
|
||||||
|
document.getElementById('manualAuthArea').style.display = 'none';
|
||||||
|
document.getElementById('adminOnlyArea').style.display = '';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
var authRadios = ['manualAuthRadio', 'jfAuthRadio'];
|
||||||
|
for (var i = 0; i < authRadios.length; i++) {
|
||||||
|
document.getElementById(authRadios[i]).addEventListener('change', function() {
|
||||||
|
checkAuthRadio();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
function checkEmailRadio() {
|
||||||
|
document.getElementById('emailNextButton').href = '#page-5';
|
||||||
|
document.getElementById('valBackButton').href = '#page-7';
|
||||||
|
if (document.getElementById('emailSMTPRadio').checked) {
|
||||||
|
document.getElementById('emailSMTPArea').style.display = '';
|
||||||
|
document.getElementById('emailMailgunArea').style.display = 'none';
|
||||||
|
} else if (document.getElementById('emailMailgunRadio').checked) {
|
||||||
|
document.getElementById('emailSMTPArea').style.display = 'none';
|
||||||
|
document.getElementById('emailMailgunArea').style.display = '';
|
||||||
|
} else if (document.getElementById('emailDisabledRadio').checked) {
|
||||||
|
document.getElementById('emailSMTPArea').style.display = 'none';
|
||||||
|
document.getElementById('emailMailgunArea').style.display = 'none';
|
||||||
|
document.getElementById('emailNextButton').href = '#page-8';
|
||||||
|
document.getElementById('valBackButton').href = '#page-4';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
var emailRadios = ['emailDisabledRadio', 'emailSMTPRadio', 'emailMailgunRadio'];
|
||||||
|
for (var i = 0; i < emailRadios.length; i++) {
|
||||||
|
document.getElementById(emailRadios[i]).addEventListener('change', function() {
|
||||||
|
checkEmailRadio();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
function checkSSL() {
|
||||||
|
var label = document.getElementById('emailSSL_TLSLabel');
|
||||||
|
if (document.getElementById('emailSSL_TLS').checked) {
|
||||||
|
label.textContent = 'Use SSL/TLS';
|
||||||
|
} else {
|
||||||
|
label.textContent = 'Use STARTTLS';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
document.getElementById('emailSSL_TLS').addEventListener('change', function() {
|
||||||
|
checkSSL();
|
||||||
|
});
|
||||||
|
|
||||||
|
function checkPwrEnabled() {
|
||||||
|
if (document.getElementById('pwrEnabled').checked) {
|
||||||
|
document.getElementById('pwrArea').style.display = '';
|
||||||
|
} else {
|
||||||
|
document.getElementById('pwrArea').style.display = 'none';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
var pwrEnabled = document.getElementById('pwrEnabled');
|
||||||
|
pwrEnabled.addEventListener('change', function() {
|
||||||
|
checkPwrEnabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
function checkInvEnabled() {
|
||||||
|
if (document.getElementById('invEnabled').checked) {
|
||||||
|
document.getElementById('invArea').style.display = '';
|
||||||
|
} else {
|
||||||
|
document.getElementById('invArea').style.display = 'none';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
document.getElementById('invEnabled').addEventListener('change', function() {
|
||||||
|
checkInvEnabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
function checkValEnabled() {
|
||||||
|
if (document.getElementById('valEnabled').checked) {
|
||||||
|
document.getElementById('valArea').style.display = '';
|
||||||
|
} else {
|
||||||
|
document.getElementById('valArea').style.display = 'none';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
document.getElementById('valEnabled').addEventListener('change', function() {
|
||||||
|
checkValEnabled();
|
||||||
|
});
|
||||||
|
checkValEnabled();
|
||||||
|
checkInvEnabled();
|
||||||
|
checkSSL();
|
||||||
|
checkAuthRadio();
|
||||||
|
checkEmailRadio();
|
||||||
|
checkPwrEnabled();
|
||||||
|
|
||||||
|
var jfValid = false
|
||||||
|
document.getElementById('jfTestButton').onclick = function() {
|
||||||
|
var testButton = document.getElementById('jfTestButton');
|
||||||
|
var nextButton = document.getElementById('jfNextButton');
|
||||||
|
testButton.disabled = true;
|
||||||
|
testButton.innerHTML =
|
||||||
|
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
||||||
|
'Testing...';
|
||||||
|
nextButton.classList.add('disabled');
|
||||||
|
nextButton.setAttribute('aria-disabled', 'true');
|
||||||
|
var jfData = {};
|
||||||
|
jfData['jfHost'] = document.getElementById('jfHost').value;
|
||||||
|
jfData['jfUser'] = document.getElementById('jfUser').value;
|
||||||
|
jfData['jfPassword'] = document.getElementById('jfPassword').value;
|
||||||
|
$.ajax('/testJF', {
|
||||||
|
type : 'POST',
|
||||||
|
dataType : 'json',
|
||||||
|
contentType : 'application/json',
|
||||||
|
data : JSON.stringify(jfData),
|
||||||
|
complete: function(response) {
|
||||||
|
testButton.disabled = false;
|
||||||
|
testButton.className = '';
|
||||||
|
var success = response['responseJSON']['success'];
|
||||||
|
if (success == true) {
|
||||||
|
testButton.classList.add('btn', 'btn-success');
|
||||||
|
testButton.textContent = 'Success';
|
||||||
|
nextButton.classList.remove('disabled');
|
||||||
|
nextButton.setAttribute('aria-disabled', 'false');
|
||||||
|
} else {
|
||||||
|
testButton.classList.add('btn', 'btn-danger');
|
||||||
|
testButton.textContent = 'Failed';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('submitButton').onclick = function() {
|
||||||
|
var submitButton = document.getElementById('submitButton');
|
||||||
|
submitButton.disabled = true;
|
||||||
|
submitButton.innerHTML =
|
||||||
|
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
||||||
|
'Submitting...';
|
||||||
|
var config = {};
|
||||||
|
config['jellyfin'] = {};
|
||||||
|
config['ui'] = {};
|
||||||
|
config['password_validation'] = {};
|
||||||
|
config['email'] = {};
|
||||||
|
config['password_resets'] = {};
|
||||||
|
config['invite_emails'] = {};
|
||||||
|
config['mailgun'] = {};
|
||||||
|
config['smtp'] = {};
|
||||||
|
// Page 2: Auth
|
||||||
|
if (document.getElementById('jfAuthRadio').checked) {
|
||||||
|
config['ui']['jellyfin_login'] = 'true';
|
||||||
|
if (document.getElementById('jfAuthAdminOnly').checked) {
|
||||||
|
config['ui']['admin_only'] = 'true';
|
||||||
|
} else {
|
||||||
|
config['ui']['admin_only'] = 'false'
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
config['ui']['username'] = document.getElementById('manualAuthUsername').value;
|
||||||
|
config['ui']['password'] = document.getElementById('manualAuthPassword').value;
|
||||||
|
};
|
||||||
|
// Page 3: Connect to jellyfin
|
||||||
|
config['jellyfin']['server'] = document.getElementById('jfHost').value;
|
||||||
|
config['jellyfin']['username'] = document.getElementById('jfUser').value;
|
||||||
|
config['jellyfin']['password'] = document.getElementById('jfPassword').value;
|
||||||
|
// Page 4: Email (Page 5, 6, 7 are only used if this is enabled)
|
||||||
|
if (document.getElementById('emailDisabledRadio').checked) {
|
||||||
|
config['password_resets']['enabled'] = 'false';
|
||||||
|
config['invite_emails']['enabled'] = 'false';
|
||||||
|
} else {
|
||||||
|
if (document.getElementById('emailSMTPRadio').checked) {
|
||||||
|
if (document.getElementById('emailSSL_TLS').checked) {
|
||||||
|
config['smtp']['encryption'] = 'ssl_tls';
|
||||||
|
} else {
|
||||||
|
config['smtp']['encryption'] = 'starttls';
|
||||||
|
};
|
||||||
|
config['email']['method'] = 'smtp';
|
||||||
|
config['smtp']['server'] = document.getElementById('emailSMTPServer').value;
|
||||||
|
config['smtp']['port'] = document.getElementById('emailSMTPPort').value;
|
||||||
|
config['smtp']['password'] = document.getElementById('emailSMTPPassword').value;
|
||||||
|
config['email']['address'] = document.getElementById('emailSMTPAddress').value;
|
||||||
|
} else {
|
||||||
|
config['email']['method'] = 'mailgun';
|
||||||
|
config['mailgun']['api_url'] = document.getElementById('emailMailgunURL').value;
|
||||||
|
config['mailgun']['api_key'] = document.getElementById('emailMailgunKey').value;
|
||||||
|
config['email']['address'] = document.getElementById('emailMailgunAddress').value;
|
||||||
|
};
|
||||||
|
// Page 5: Email formatting
|
||||||
|
config['email']['from'] = document.getElementById('emailSender').value;
|
||||||
|
config['email']['date_format'] = document.getElementById('emailDateFormat').value;
|
||||||
|
if (document.getElementById('email24hTimeRadio').checked) {
|
||||||
|
config['email']['use_24h'] = 'true';
|
||||||
|
} else {
|
||||||
|
config['email']['use_24h'] = 'false';
|
||||||
|
};
|
||||||
|
config['email']['message'] = document.getElementById('emailMessage').value;
|
||||||
|
// Page 6: Password Resets
|
||||||
|
if (document.getElementById('pwrEnabled').checked) {
|
||||||
|
config['password_resets']['enabled'] = 'true';
|
||||||
|
config['password_resets']['watch_directory'] = document.getElementById('pwrJfPath').value;
|
||||||
|
config['password_resets']['subject'] = document.getElementById('pwrSubject').value;
|
||||||
|
} else {
|
||||||
|
config['password_resets']['enabled'] = 'false';
|
||||||
|
};
|
||||||
|
// Page 7: Invite Emails
|
||||||
|
if (document.getElementById('invEnabled').checked) {
|
||||||
|
config['invite_emails']['enabled'] = 'true';
|
||||||
|
config['invite_emails']['url_base'] = document.getElementById('invURLBase').value;
|
||||||
|
config['invite_emails']['subject'] = document.getElementById('invSubject').value;
|
||||||
|
} else {
|
||||||
|
config['invite_emails']['enabled'] = 'false';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
// Page 8: Password Validation
|
||||||
|
if (document.getElementById('valEnabled').checked) {
|
||||||
|
config['password_validation']['enabled'] = 'true';
|
||||||
|
config['password_validation']['min_length'] = document.getElementById('valLength').value;
|
||||||
|
config['password_validation']['upper'] = document.getElementById('valUpper').value;
|
||||||
|
config['password_validation']['lower'] = document.getElementById('valLower').value;
|
||||||
|
config['password_validation']['number'] = document.getElementById('valNumber').value;
|
||||||
|
config['password_validation']['special'] = document.getElementById('valSpecial').value;
|
||||||
|
} else {
|
||||||
|
config['password_validation']['enabled'] = 'false';
|
||||||
|
};
|
||||||
|
// Page 9: Messages
|
||||||
|
config['ui']['contact_message'] = document.getElementById('msgContact').value;
|
||||||
|
config['ui']['help_message'] = document.getElementById('msgHelp').value;
|
||||||
|
config['ui']['success_message'] = document.getElementById('msgSuccess').value;
|
||||||
|
console.log(config);
|
||||||
|
$.ajax('/modifyConfig', {
|
||||||
|
type : 'POST',
|
||||||
|
dataType : 'json',
|
||||||
|
contentType : 'application/json',
|
||||||
|
data : JSON.stringify(config),
|
||||||
|
complete: function(response) {
|
||||||
|
submitButton.disabled = false;
|
||||||
|
submitButton.className = '';
|
||||||
|
submitButton.classList.add('btn', 'btn-success');
|
||||||
|
submitButton.textContent = 'Success';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
356
data/templates/setup.html
Normal file
356
data/templates/setup.html
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
|
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-serialize-object/2.5.0/jquery.serialize-object.min.js" integrity="sha256-E8KRdFk/LTaaCBoQIV/rFNc0s3ICQQiOHFT4Cioifa8=" crossorigin="anonymous"></script>
|
||||||
|
<style>
|
||||||
|
.card-body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 10%;
|
||||||
|
}
|
||||||
|
.slider {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
overflow-x: hidden;
|
||||||
|
-webkit-overflow-scrolling: none;
|
||||||
|
scroll-snap-type: x mandatory;
|
||||||
|
}
|
||||||
|
.slider > div {
|
||||||
|
scroll-snap-align: start;
|
||||||
|
}
|
||||||
|
.slide {
|
||||||
|
width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 100%;
|
||||||
|
margin: 10%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<title>Setup - Jellyfin Accounts</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="pageContainer">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm"></div>
|
||||||
|
<div id="setupCarousel" class="col-md-auto slider">
|
||||||
|
<div class="slide card text-center" id="page-1">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Welcome!</h5>
|
||||||
|
<p class="card-text">
|
||||||
|
You'll need to do a few things to start using jellyfin-accounts. Click below to get started, or quit and edit the config file manually.
|
||||||
|
</p>
|
||||||
|
<a class="btn btn-primary nextButton" href="#page-2">Get Started</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<small>Note: Make sure you are accessing this page through HTTPS, or on a private network.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="slide card" id="page-2">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Login</h5>
|
||||||
|
<p class="card-text">
|
||||||
|
To access the admin page, you'll need to login. Choose how below.
|
||||||
|
<ul>
|
||||||
|
<li><b>Authorize through Jellyfin: </b>Checks credentials with jellyfin, allowing you to share login details and grant multiple users access.</li>
|
||||||
|
<li><b>Username & Password: </b>Set your own username and password manually.</li>
|
||||||
|
</ul>
|
||||||
|
<div class="form-check" id="jfAuthFormGroup">
|
||||||
|
<input class="form-check-input" type="radio" name="auth" id="jfAuthRadio" value="jfAuth" checked>
|
||||||
|
<label class="form-check-label" for="jfAuthRadio">
|
||||||
|
Authorize through Jellyfin
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div id="adminOnlyArea">
|
||||||
|
<div class="form-check" style="margin-left: 1rem;">
|
||||||
|
<input type="checkbox" class="form-check-input" id="jfAuthAdminOnly" checked>
|
||||||
|
<label for="jfAuthAdminOnly" class="form-check-label">Allow admin users only</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="auth" id="manualAuthRadio" value="manualAuth">
|
||||||
|
<label class="form-check-label" for="manualAuthRadio">
|
||||||
|
Manual username & password
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div id="manualAuthArea">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="manualAuthUsername">Username</label>
|
||||||
|
<input type="text" class="form-control" id="manualAuthUsername" placeholder="Username">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="manualAuthPassword">Password</label>
|
||||||
|
<input type="password" class="form-control" id="manualAuthPassword" placeholder="Password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
|
||||||
|
<a class="btn btn-secondary backButton" href="#page-1">Back</a>
|
||||||
|
<a class="btn btn-primary nextButton" href="#page-3">Next</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="slide card" id="page-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Jellyfin</h5>
|
||||||
|
<p class="card-text">
|
||||||
|
jellyfin-accounts needs admin access so that it can create users.
|
||||||
|
You should create a separate account for it, checking 'Allow this user to manage the server'. You can disable everything else. Once done, enter the credentials here.
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="jfHost">Host</label>
|
||||||
|
<input type="url" class="form-control" id="jfHost" placeholder="http://jellyf.in:443">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="jfUser">Username</label>
|
||||||
|
<input type="text" class="form-control" id="jfUser" placeholder="Username">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="jfPassword">Password</label>
|
||||||
|
<input type="password" class="form-control" id="jfPassword" placeholder="Password">
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-secondary" id="jfTestButton">Test</button>
|
||||||
|
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
|
||||||
|
<a class="btn btn-secondary backButton" href="#page-2">Back</a>
|
||||||
|
<a class="btn btn-primary nextButton disabled" id="jfNextButton" aria-disabled="true" href="#page-4">Next</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="slide card" id="page-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Email</h5>
|
||||||
|
<p class="card-text">jellyfin-accounts is capable of sending a PIN code when a user tries to reset their password on Jellyfin. One can also choose to send an invite code directly to an email address. This can be done through SMTP or through <a href="https://www.mailgun.com/">Mailgun's</a> API.
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="form-check" id="emailDisabled">
|
||||||
|
<input class="form-check-input" type="radio" name="email" id="emailDisabledRadio" value="emailDisabled">
|
||||||
|
<label class="form-check-label" for="emailDisabledRadio">
|
||||||
|
Disabled
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check" id="emailSMTP">
|
||||||
|
<input class="form-check-input" type="radio" name="email" id="emailSMTPRadio" value="emailSMTP" checked>
|
||||||
|
<label class="form-check-label" for="emailSMTPRadio">
|
||||||
|
SMTP
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="email" id="emailMailgunRadio" value="emailMailgun">
|
||||||
|
<label class="form-check-label" for="emailMailgunRadio">
|
||||||
|
Mailgun API
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="emailSMTPArea">
|
||||||
|
<div class="form-group form-check">
|
||||||
|
<input type="checkbox" class="custom-control-input" id="emailSSL_TLS" checked>
|
||||||
|
<label for="emailSSL_TLS" class="custom-control-label" id="emailSSL_TLSLabel">Use SSL/TLS</label>
|
||||||
|
<small class="form-text text-muted">Note: SSL/TLS usually uses port 465, whereas STARTTLS usually uses 587.</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group form-row">
|
||||||
|
<div class="col">
|
||||||
|
<input type="text" class="form-control" id="emailSMTPServer" placeholder="SMTP Server Address">
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<input type="number" class="form-control" id="emailSMTPPort" placeholder="Port">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group form-row">
|
||||||
|
<div class="col">
|
||||||
|
<input type="email" class="form-control" id="emailSMTPAddress" placeholder="jellyfin@jellyf.in">
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<input type="password" class="form-control" id="emailSMTPPassword" placeholder="Password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="emailMailgunArea">
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="url" class="form-control" id="emailMailgunURL" placeholder="API URL">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="password" class="form-control" id="emailMailgunKey" placeholder="API Key">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="email" class="form-control" id="emailMailgunAddress" placeholder="jellyfin@jellyf.in">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
|
||||||
|
<a class="btn btn-secondary backButton" href="#page-3">Back</a>
|
||||||
|
<a class="btn btn-primary nextButton" id="emailNextButton" href="#page-5">Next</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="slide card" id="page-5">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Email</h5>
|
||||||
|
<p class="card-text">Just a few more things to get your emails looking great.
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="emailSender">Sender: The name shown when a user receives an email.</label>
|
||||||
|
<input type="text" class="form-control" id="emailSender" value="Jellyfin">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="emailDateFormat">Date Format: Follows <a target="_blank" href="https://strftime.org/">strftime</a> format.</label>
|
||||||
|
<input type="text" class="form-control" id="emailDateFormat" value="%d/%m/%y">
|
||||||
|
</div>
|
||||||
|
<div class="form-group form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="time" id="email24hTimeRadio" value="email24hTime" checked>
|
||||||
|
<label class="form-check-label" for="email24hTimeRadio">24h time</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="time" id="email12hTimeRadio" value="email12hTime">
|
||||||
|
<label class="form-check-label" for="email12hTimeRadio">12h time</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="emailMessage">Message: Short message displayed at the bottom of emails.</label>
|
||||||
|
<input type="text" class="form-control" id="emailMessage" value="Need help? Contact me.">
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
|
||||||
|
<a class="btn btn-secondary backButton" href="#page-4">Back</a>
|
||||||
|
<a class="btn btn-primary nextButton" href="#page-6">Next</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="slide card" id="page-6">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Password Resets</h5>
|
||||||
|
<p class="card-text">
|
||||||
|
When a user tries to reset their password in jellyfin, it informs them that a file has been created, named "passwordreset*.json" where * is a number. jellyfin-accounts will then read this file, and send the PIN to the user's email. Try it now, and put the folder that it informs you it put the file in below. Also, if enter a custom email subject if you don't like the default one.
|
||||||
|
</p>
|
||||||
|
<div class="form-group form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="pwrEnabled" value="enabled">
|
||||||
|
<label class="form-check-label" for="pwrEnabled">Enabled</label>
|
||||||
|
</div>
|
||||||
|
<div id="pwrArea">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="pwrJfPath">Path to Jellyfin</label>
|
||||||
|
<input type="text" class="form-control" id="pwrJfPath" placeholder="Folder">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="pwrSubject">Email Subject</label>
|
||||||
|
<input type="text" class="form-control" id="pwrSubject" value="Password Reset - Jellyfin">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
|
||||||
|
<a class="btn btn-secondary backButton" href="#page-5">Back</a>
|
||||||
|
<a class="btn btn-primary nextButton" href="#page-7">Next</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="slide card" id="page-7">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Invite Emails</h5>
|
||||||
|
<p class="card-text">
|
||||||
|
Allows you to send an invite code directly to a specified email address.
|
||||||
|
Since you'll most likely being running this behind a reverse proxy, the program has no way of knowing the address it will be accessed from. This is needed for sending emails with links. Write your URL Base with the protocol and append '/invite', e.g:
|
||||||
|
<ul>
|
||||||
|
<li>On the local network, you might use <a href="#">http://localhost:8056/invite</a></li>
|
||||||
|
<li>Exposed to the internet, you might use <a href="#">https://accounts.jellyf.in/invite</a></li>
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
<div class="form-group form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="invEnabled" value="enabled" checked>
|
||||||
|
<label class="form-check-label" for="invEnabled">Enabled</label>
|
||||||
|
</div>
|
||||||
|
<div id="invArea">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="invURLBase">URL Base</label>
|
||||||
|
<input type="url" class="form-control" id="invURLBase" placeholder="https://accounts.jellyf.in/invite">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="invSubject">Subject</label>
|
||||||
|
<input type="text" class="form-control" id="invSubject" value="Invite - Jellyfin">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
|
||||||
|
<a class="btn btn-secondary backButton" href="#page-6">Back</a>
|
||||||
|
<a class="btn btn-primary nextButton" href="#page-8">Next</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="slide card" id="page-8">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Password Validation</h5>
|
||||||
|
<p class="card-text">
|
||||||
|
Enabling this will display a set of password requirements on the create account page, such as minimum length, uppercase characters, special characters, etc.
|
||||||
|
</p>
|
||||||
|
<div class="form-group form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="valEnabled" value="enabled">
|
||||||
|
<label class="form-check-label" for="valEnabled">Enabled</label>
|
||||||
|
</div>
|
||||||
|
<div id="valArea">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="valLength">Minimum Length</label>
|
||||||
|
<input type="number" class="form-control" id="valLength" value="8">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="valUpper">Minimum number of uppercase characters</label>
|
||||||
|
<input type="number" class="form-control" id="valUpper" value="1">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="valLower">Minimum number of lowercase characters</label>
|
||||||
|
<input type="number" class="form-control" id="valLower" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="valNumber">Minimum number of numbers</label>
|
||||||
|
<input type="number" class="form-control" id="valNumber" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="valSpecial">Minimum number of special characters</label>
|
||||||
|
<input type="number" class="form-control" id="valSpecial" value="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
|
||||||
|
<a class="btn btn-secondary backButton" id="valBackButton" href="#page-7">Back</a>
|
||||||
|
<a class="btn btn-primary nextButton" href="#page-9">Next</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="slide card" id="page-9">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Help Messages</h5>
|
||||||
|
<p class="card-text">
|
||||||
|
Just a few little messages that will display in various places. Leave these alone if you want.
|
||||||
|
</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="msgContact">Contact message: Displays at bottom of all pages (except admin).</label>
|
||||||
|
<input id="msgContact" type="text" class="form-control" value="Need help? Contact me.">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="msgHelp">Help message: Displays when a user is creating an account.</label>
|
||||||
|
<input id="msgHelp" type="text" class="form-control" value="Enter your details to create an account.">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="msgSuccess">Success message: Displays when a user successfully creates an account, just above a button taking the user to Jellyfin.</label>
|
||||||
|
<input id="msgSuccess" type="text" class="form-control" value="Your account has been created. Click below to continue to Jellyfin.">
|
||||||
|
</div>
|
||||||
|
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
|
||||||
|
<a class="btn btn-secondary backButton" href="#page-8">Back</a>
|
||||||
|
<a class="btn btn-primary nextButton" href="#page-10">Next</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="slide card" id="page-10">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h5 class="card-title">Finished!</h5>
|
||||||
|
<p class="card-text">
|
||||||
|
Press the button below to submit your settings. The program will quit, so run it again, then refresh this page.
|
||||||
|
</p>
|
||||||
|
<button id="submitButton" class="btn btn-primary">Submit</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="setup.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -177,3 +177,7 @@ class Smtp(Email):
|
|||||||
err = f'{self.address}: Failed to send via smtp: '
|
err = f'{self.address}: Failed to send via smtp: '
|
||||||
err += type(e).__name__
|
err += type(e).__name__
|
||||||
log.error(err)
|
log.error(err)
|
||||||
|
try:
|
||||||
|
log.error(e.smtp_error)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
76
jellyfin_accounts/setup.py
Normal file
76
jellyfin_accounts/setup.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
from flask import request, jsonify, render_template
|
||||||
|
from configparser import RawConfigParser
|
||||||
|
from jellyfin_accounts.jf_api import Jellyfin
|
||||||
|
from __main__ import config, config_path, app, first_run
|
||||||
|
from __main__ import web_log as log
|
||||||
|
import os
|
||||||
|
|
||||||
|
if first_run:
|
||||||
|
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 tempJF(server):
|
||||||
|
return Jellyfin(server,
|
||||||
|
config['jellyfin']['client'],
|
||||||
|
config['jellyfin']['version'],
|
||||||
|
config['jellyfin']['device'] + '_temp',
|
||||||
|
config['jellyfin']['device_id'] + '_temp')
|
||||||
|
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def page_not_found(e):
|
||||||
|
return render_template('404.html'), 404
|
||||||
|
|
||||||
|
@app.route('/', methods=['GET', 'POST'])
|
||||||
|
def setup():
|
||||||
|
return render_template('setup.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/<path:path>')
|
||||||
|
def static_proxy(path):
|
||||||
|
if 'html' not in path:
|
||||||
|
return app.send_static_file(path)
|
||||||
|
else:
|
||||||
|
return render_template('404.html'), 404
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/modifyConfig', methods=['POST'])
|
||||||
|
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')
|
||||||
|
os._exit(1)
|
||||||
|
return resp()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/testJF', methods=['GET', 'POST'])
|
||||||
|
def testJF():
|
||||||
|
data = request.get_json()
|
||||||
|
tempjf = tempJF(data['jfHost'])
|
||||||
|
try:
|
||||||
|
tempjf.authenticate(data['jfUser'],
|
||||||
|
data['jfPassword'])
|
||||||
|
tempjf.getUsers(public=False)
|
||||||
|
return resp()
|
||||||
|
except:
|
||||||
|
return resp(False)
|
@ -20,7 +20,7 @@ def admin():
|
|||||||
|
|
||||||
@app.route('/<path:path>')
|
@app.route('/<path:path>')
|
||||||
def static_proxy(path):
|
def static_proxy(path):
|
||||||
if 'form.html' not in path and 'admin.html' not in path:
|
if 'html' not in path:
|
||||||
return app.send_static_file(path)
|
return app.send_static_file(path)
|
||||||
return render_template('404.html',
|
return render_template('404.html',
|
||||||
contactMessage=config['ui']['contact_message']), 404
|
contactMessage=config['ui']['contact_message']), 404
|
||||||
|
@ -266,30 +266,6 @@ def get_token():
|
|||||||
return jsonify({'token': token.decode('ascii')})
|
return jsonify({'token': token.decode('ascii')})
|
||||||
|
|
||||||
|
|
||||||
@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)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/getUsers', methods=['GET', 'POST'])
|
@app.route('/getUsers', methods=['GET', 'POST'])
|
||||||
@auth.login_required
|
@auth.login_required
|
||||||
def getUsers():
|
def getUsers():
|
||||||
@ -333,4 +309,4 @@ def modifyUsers():
|
|||||||
except:
|
except:
|
||||||
return resp(success=False)
|
return resp(success=False)
|
||||||
|
|
||||||
|
import jellyfin_accounts.setup
|
||||||
|
49
jf-accounts
49
jf-accounts
@ -36,6 +36,7 @@ else:
|
|||||||
|
|
||||||
local_dir = (Path(__file__).parents[2] / 'data').resolve()
|
local_dir = (Path(__file__).parents[2] / 'data').resolve()
|
||||||
|
|
||||||
|
first_run = False
|
||||||
if not data_dir.exists():
|
if not data_dir.exists():
|
||||||
Path.mkdir(data_dir)
|
Path.mkdir(data_dir)
|
||||||
print(f'Config dir not found, so created at {str(data_dir)}')
|
print(f'Config dir not found, so created at {str(data_dir)}')
|
||||||
@ -43,8 +44,8 @@ if not data_dir.exists():
|
|||||||
config_path = data_dir / 'config.ini'
|
config_path = data_dir / 'config.ini'
|
||||||
shutil.copy(str(local_dir / 'config-default.ini'),
|
shutil.copy(str(local_dir / 'config-default.ini'),
|
||||||
str(config_path))
|
str(config_path))
|
||||||
print("Edit the configuration and restart.")
|
print("Setup through the web UI, or quit and edit the configuration manually.")
|
||||||
raise SystemExit
|
first_run = True
|
||||||
else:
|
else:
|
||||||
config_path = Path(args.config)
|
config_path = Path(args.config)
|
||||||
print(f'config.ini can be found at {str(config_path)}')
|
print(f'config.ini can be found at {str(config_path)}')
|
||||||
@ -71,9 +72,10 @@ def create_log(name):
|
|||||||
return log
|
return log
|
||||||
|
|
||||||
log = create_log('main')
|
log = create_log('main')
|
||||||
email_log = create_log('emails')
|
|
||||||
web_log = create_log('waitress')
|
web_log = create_log('waitress')
|
||||||
auth_log = create_log('auth')
|
if not first_run:
|
||||||
|
email_log = create_log('emails')
|
||||||
|
auth_log = create_log('auth')
|
||||||
|
|
||||||
if args.host is not None:
|
if args.host is not None:
|
||||||
log.debug(f'Using specified host {args.host}')
|
log.debug(f'Using specified host {args.host}')
|
||||||
@ -144,20 +146,29 @@ else:
|
|||||||
app.config['SECRET_KEY'] = secrets.token_urlsafe(16)
|
app.config['SECRET_KEY'] = secrets.token_urlsafe(16)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import jellyfin_accounts.web_api
|
|
||||||
import jellyfin_accounts.web
|
|
||||||
from waitress import serve
|
from waitress import serve
|
||||||
host = config['ui']['host']
|
if first_run:
|
||||||
port = config['ui']['port']
|
import jellyfin_accounts.setup
|
||||||
log.info(f'Starting web UI on {host}:{port}')
|
host = '0.0.0.0'
|
||||||
if config.getboolean('password_resets', 'enabled'):
|
port = 8056
|
||||||
def start_pwr():
|
log.info('Starting web UI for first run setup...')
|
||||||
import jellyfin_accounts.pw_reset
|
serve(app,
|
||||||
jellyfin_accounts.pw_reset.start()
|
host=host,
|
||||||
pwr = threading.Thread(target=start_pwr, daemon=True)
|
port=port)
|
||||||
log.info('Starting email thread')
|
else:
|
||||||
pwr.start()
|
import jellyfin_accounts.web_api
|
||||||
|
import jellyfin_accounts.web
|
||||||
|
host = config['ui']['host']
|
||||||
|
port = config['ui']['port']
|
||||||
|
log.info(f'Starting web UI on {host}:{port}')
|
||||||
|
if config.getboolean('password_resets', 'enabled'):
|
||||||
|
def start_pwr():
|
||||||
|
import jellyfin_accounts.pw_reset
|
||||||
|
jellyfin_accounts.pw_reset.start()
|
||||||
|
pwr = threading.Thread(target=start_pwr, daemon=True)
|
||||||
|
log.info('Starting email thread')
|
||||||
|
pwr.start()
|
||||||
|
|
||||||
serve(app,
|
serve(app,
|
||||||
host=host,
|
host=host,
|
||||||
port=int(port))
|
port=int(port))
|
||||||
|
6
setup.py
6
setup.py
@ -27,12 +27,14 @@ setup(
|
|||||||
'data/invite-email.html',
|
'data/invite-email.html',
|
||||||
'data/invite-email.mjml',
|
'data/invite-email.mjml',
|
||||||
'data/invite-email.txt']),
|
'data/invite-email.txt']),
|
||||||
('data/static', ['data/static/admin.js']),
|
('data/static', ['data/static/admin.js',
|
||||||
|
'data/static/setup.js']),
|
||||||
('data/templates', [
|
('data/templates', [
|
||||||
'data/templates/404.html',
|
'data/templates/404.html',
|
||||||
'data/templates/invalidCode.html',
|
'data/templates/invalidCode.html',
|
||||||
'data/templates/admin.html',
|
'data/templates/admin.html',
|
||||||
'data/templates/form.html'])],
|
'data/templates/form.html',
|
||||||
|
'data/templates/setup.html'])],
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'pyOpenSSL',
|
'pyOpenSSL',
|
||||||
|
Loading…
Reference in New Issue
Block a user