Move to Poetry for deps and packaging

setup.py has been removed, and Poetry is now used to package for
install. This fixed an issue i had with uploading to PyPI, so one can
now run 'pip install jellyfin-accounts' to install.
This commit is contained in:
2020-06-16 20:07:47 +01:00
parent f4f18d41ea
commit bfbaca4094
37 changed files with 787 additions and 357 deletions

View File

@@ -0,0 +1,116 @@
[jellyfin]
; It is reccommended to create a limited admin account for this program.
username = username
password = password
; Jellyfin server address. Can be public, or local for security purposes.
server = http://jellyfin.local:8096
; Publicly accessible Jellyfin address, used on invite form.
; Leave blank to use the same address as above.
public_server = https://jellyf.in:443
client = jf-accounts
version = 0.1
device = jf-accounts
device_id = jf-accounts-0.1
[ui]
; Set 0.0.0.0 to run localhost
host = 0.0.0.0
port = 8056
; Enable this to use Jellyfin users instead of the below username and pw.
jellyfin_login = true
; Allows only admin users on Jellyfin to access admin page.
admin_only = true
; Username to use on admin page... (leave blank if using jellyfin_login)
username = your username
; ..and its corresponding password (leave blank if using jellyfin_login)
password = your password
debug = false
; Displayed at the bottom of all pages except admin
contact_message = Need help? contact me.
; Displayed at top of form page.
help_message = Enter your details to create an account.
; Displayed when an account is created.
success_message = Your account has been created. Click below to continue to Jellyfin.
[password_validation]
; Enables password validation.
enabled = true
; Min. password length
min_length = 8
; Min. number of uppercase characters
upper = 1
; Min. number of lowercase characters
lower = 0
; Min. number of numbers
number = 1
; Min. number of special characters
special = 0
[email]
; Leave this whole section if you aren't using any email-related features.
use_24h = true
; Date format follows datetime's strftime.
date_format = %d/%m/%y
; Displayed at bottom of emails
message = Need help? contact me.
; Mail methods: mailgun, smtp
method = smtp
; Address to send from
address = jellyfin@jellyf.in
; The name of the sender
from = Jellyfin
[password_resets]
; Enable to store provided email addresses, monitor jellyfin directory for pw-resets, and send pin
enabled = true
; Directory to monitor for passwordReset*.json files. Usually the jellyfin config directory
watch_directory = /path/to/jellyfin
; Path to custom email html. If blank, uses the internal template.
email_html =
; Path to alternate plaintext email. If blank, uses the internal template.
email_text =
; Subject of emails
subject = Password Reset - Jellyfin
[invite_emails]
; If enabled, allows one to send an invite directly to an email address.
enabled = true
; Path to custom email html. If blank, uses the internal template.
email_html =
; Path to alternate plaintext email. If blank, uses the internal template.
email_text =
subject = Invite - Jellyfin
; Base url for jf-accounts. This necessary because most will use a reverse proxy, so the program has no other way of knowing what URL to send.
url_base = http://accounts.jellyf.in:8056/invite
[mailgun]
api_url = https://api.mailgun.net...
api_key = your api key
[smtp]
; Choose between ssl_tls and starttls. Your provider should tell you which to use, but generally SSL/TLS is 465, STARTTLS 587
encryption = starttls
server = smtp.jellyf.in
; Uses SMTP_SSL, so make sure the port is for this, not starttls.
port = 465
password = smtp password
[files]
; When the below paths are left blank, files are stored in ~/.jf-accounts/.
; Path to store valid invites.
invites =
; Path to store emails addresses in JSON
emails =
; Path to the user policy template. Can be acquired with get-defaults (jf-accounts -g).
user_template =
; Path to the user configuration template (part of homescreen layout). Can be acquired with get-defaults (jf-accounts -g).
user_configuration =
; Path to the user display preferences template (part of homescreen layout). Can be acquired with get-defaults (jf-accounts -g).
user_displayprefs =
; Path to custom bootstrap.css
custom_css =

View File

@@ -0,0 +1,242 @@
<!-- FILE: email.mjml -->
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>
</title>
<!--[if !mso]><!-- -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;700&display=swap" rel="stylesheet" type="text/css">
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
@import url(https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;700&display=swap);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
}
</style>
<style type="text/css">
</style>
</head>
<body>
<div style="">
<!--[if mso | IE]>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="background:#f0f0f0;background-color:#f0f0f0;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#f0f0f0;background-color:#f0f0f0;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:20px;font-style:bold;line-height:1;text-align:left;color:#000000;">Jellyfin</div>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Noto Sans, Helvetica, Arial, sans-serif;font-size:15px;line-height:1;text-align:left;color:#000000;">
<p>Hi {{ username }},</p>
<p> Someone has recently requested a password reset on Jellyfin.</p>
<p>If this was you, enter the below pin into the prompt.</p>
<p>The code will expire on {{ expiry_date }}, at {{ expiry_time }}, which is in {{ expires_in }}.</p>
<p>If this wasn't you, please ignore this email.</p>
</div>
</td>
</tr>
<tr>
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
<tr>
<td align="center" bgcolor="#414141" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#414141;" valign="middle">
<p style="display:inline-block;background:#414141;color:#ffffff;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;">
{{ pin }}
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="background:#f0f0f0;background-color:#f0f0f0;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#f0f0f0;background-color:#f0f0f0;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:10px;font-style:italic;line-height:1;text-align:left;color:#000000;">{{ message }}</div>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</div>
</body>
</html>

View File

@@ -0,0 +1,34 @@
<mjml>
<mj-head>
<mj-font name="Noto Sans" href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;700&display=swap" />
</mj-head>
<mj-body>
<mj-section background-color="#f0f0f0">
<mj-column>
<mj-text font-style="bold" font-size="20px">
Jellyfin
</mj-text>
</mj-column>
</mj-section>
<mj-section>
<mj-column>
<mj-text font-size="15px" font-family="Noto Sans, Helvetica, Arial, sans-serif">
<p>Hi {{ username }},</p>
<p> Someone has recently requested a password reset on Jellyfin.</p>
<p>If this was you, enter the below pin into the prompt.</p>
<p>The code will expire on {{ expiry_date }}, at {{ expiry_time }}, which is in {{ expires_in }}.</p>
<p>If this wasn't you, please ignore this email.</p>
</mj-text>
<mj-button>{{ pin }}</mj-button>
</mj-column>
</mj-section>
<mj-section background-color="#f0f0f0">
<mj-column>
<mj-text font-style="italic" font-size="10px">
{{ message }}
</mj-text>
</mj-column>
</mj-section>
</body>
</mjml>

View File

@@ -0,0 +1,10 @@
Hi {{ username }},
Someone has recently requests a password reset on Jellyfin.
If this was you, enter the below pin into the prompt.
This code will expire on {{ expiry_date }}, at {{ expiry_time }} , which is in {{ expires_in }}.
If this wasn't you, please ignore this email.
PIN: {{ pin }}
{{ message }}

View File

@@ -0,0 +1,239 @@
<!-- FILE: invite-email.mjml -->
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>
</title>
<!--[if !mso]><!-- -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;700&display=swap" rel="stylesheet" type="text/css">
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
@import url(https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;700&display=swap);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
}
</style>
<style type="text/css">
</style>
</head>
<body>
<div style="">
<!--[if mso | IE]>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="background:#f0f0f0;background-color:#f0f0f0;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#f0f0f0;background-color:#f0f0f0;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:20px;font-style:bold;line-height:1;text-align:left;color:#000000;">Jellyfin</div>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Noto Sans, Helvetica, Arial, sans-serif;font-size:15px;line-height:1;text-align:left;color:#000000;">
<p>Hi,</p>
<h2>You've been invited to Jellyfin.</h2>
<p>To join, click the button below.</p>
<p>This invite will expire on {{ expiry_date }}, at {{ expiry_time }}, which is in {{ expires_in }}, so act quick.</p>
</div>
</td>
</tr>
<tr>
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
<tr>
<td align="center" bgcolor="#414141" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#414141;" valign="middle">
<a href="{{ invite_link }}" style="display:inline-block;background:#414141;color:#ffffff;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;" target="_blank"> Setup your account </a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="background:#f0f0f0;background-color:#f0f0f0;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#f0f0f0;background-color:#f0f0f0;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:10px;font-style:italic;line-height:1;text-align:left;color:#000000;">{{ message }}</div>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</div>
</body>
</html>

View File

@@ -0,0 +1,33 @@
<mjml>
<mj-head>
<mj-font name="Noto Sans" href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;700&display=swap" />
</mj-head>
<mj-body>
<mj-section background-color="#f0f0f0">
<mj-column>
<mj-text font-style="bold" font-size="20px">
Jellyfin
</mj-text>
</mj-column>
</mj-section>
<mj-section>
<mj-column>
<mj-text font-size="15px" font-family="Noto Sans, Helvetica, Arial, sans-serif">
<p>Hi,</p>
<h2>You've been invited to Jellyfin.</h2>
<p>To join, click the button below.</p>
<p>This invite will expire on {{ expiry_date }}, at {{ expiry_time }}, which is in {{ expires_in }}, so act quick.</p>
</mj-text>
<mj-button href="{{ invite_link }}">Setup your account</mj-button>
</mj-column>
</mj-section>
<mj-section background-color="#f0f0f0">
<mj-column>
<mj-text font-style="italic" font-size="10px">
{{ message }}
</mj-text>
</mj-column>
</mj-section>
</body>
</mjml>

View File

@@ -0,0 +1,8 @@
Hi,
You've been invited to Jellyfin.
To join, follow the below link.
This invite will expire on {{ expiry_date }}, at {{ expiry_time }}, which is in {{ expires_in }}, so act quick.
{{ invite_link }}
{{ message }}

View File

@@ -0,0 +1,451 @@
function parseInvite(invite, empty = false) {
if (empty === true) {
return ["None", "", "1"]
} else {
var i = ["", "", "0", invite['email']];
i[0] = invite['code'];
if (invite['hours'] == 0) {
i[1] = invite['minutes'] + 'm';
} else if (invite['minutes'] == 0) {
i[1] = invite['hours'] + 'h';
} else {
i[1] = invite['hours'] + 'h ' + invite['minutes'] + 'm';
}
i[1] = "Expires in " + i[1] + " ";
return i
}
}
function addItem(invite) {
var links = document.getElementById('invites');
var listItem = document.createElement('li');
listItem.id = invite[0]
listItem.classList.add('list-group-item', 'd-flex', 'justify-content-between', 'd-inline-block');
var listCode = document.createElement('div');
listCode.classList.add('d-flex', 'align-items-center', 'text-monospace');
var codeLink = document.createElement('a');
codeLink.setAttribute('style', 'margin-right: 2%;');
codeLink.appendChild(document.createTextNode(invite[0].replace(/-/g, '')));
listCode.appendChild(codeLink);
listItem.appendChild(listCode);
var listRight = document.createElement('div');
listText = document.createElement('span');
listText.id = invite[0] + '_expiry'
listText.appendChild(document.createTextNode(invite[1]));
listRight.appendChild(listText);
if (invite[2] == 0) {
var inviteCode = window.location.href + 'invite/' + invite[0];
codeLink.href = inviteCode;
// listCode.appendChild(document.createTextNode(" "));
var codeCopy = document.createElement('i');
codeCopy.onclick = function(){toClipboard(inviteCode)};
codeCopy.classList.add('fa', 'fa-clipboard');
listCode.appendChild(codeCopy);
if (typeof(invite[3]) != 'undefined') {
var sentTo = document.createElement('span');
sentTo.setAttribute('style', 'color: grey; margin-left: 2%; font-style: italic; font-size: 75%;');
if (invite[3].includes('Failed to send to')) {
sentTo.appendChild(document.createTextNode(invite[3]));
} else {
sentTo.appendChild(document.createTextNode('Sent to ' + invite[3]));
}
listCode.appendChild(sentTo);
};
var listDelete = document.createElement('button');
listDelete.onclick = function(){deleteInvite(invite[0])};
listDelete.classList.add('btn', 'btn-outline-danger');
listDelete.appendChild(document.createTextNode('Delete'));
listRight.appendChild(listDelete);
};
listItem.appendChild(listRight);
links.appendChild(listItem);
};
function updateInvite(invite) {
var expiry = document.getElementById(invite[0] + '_expiry');
expiry.textContent = invite[1];
}
function removeInvite(code) {
var item = document.getElementById(code);
item.parentNode.removeChild(item);
}
function generateInvites(empty = false) {
// document.getElementById('invites').textContent = '';
if (empty === false) {
$.ajax('/getInvites', {
type : 'GET',
dataType : 'json',
contentType: 'json',
xhrFields : {
withCredentials: true
},
beforeSend : function (xhr) {
xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
},
data: { get_param: 'value' },
complete: function(response) {
var data = JSON.parse(response['responseText']);
if (data['invites'].length == 0) {
document.getElementById('invites').textContent = '';
addItem(parseInvite([], true));
} else {
data['invites'].forEach(function(invite) {
var match = false;
var items = document.getElementById('invites').children;
for (var i = 0; i < items.length; i++) {
if (items[i].id == invite['code']) {
match = true;
updateInvite(parseInvite(invite));
};
};
if (match == false) {
addItem(parseInvite(invite));
};
});
var items = document.getElementById('invites').children;
for (var i = 0; i < items.length; i++) {
var exists = false;
data['invites'].forEach(function(invite) {
if (items[i].id == invite['code']) {
exists = true;
}
});
if (exists == false) {
removeInvite(items[i].id);
}
};
};
}
});
} else if (empty === true) {
document.getElementById('invites').textContent = '';
addItem(parseInvite([], true));
};
};
function deleteInvite(code) {
var send = JSON.stringify({ "code": code });
$.ajax('/deleteInvite', {
data : send,
contentType : 'application/json',
type : 'POST',
xhrFields : {
withCredentials: true
},
beforeSend : function (xhr) {
xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
},
success: function() { generateInvites(); },
});
};
function addOptions(le, sel) {
for (v = 0; v <= le; v++) {
var opt = document.createElement('option');
opt.appendChild(document.createTextNode(v))
opt.value = v
sel.appendChild(opt)
}
};
function toClipboard(str) {
const el = document.createElement('textarea');
el.value = str;
el.setAttribute('readonly', '');
el.style.position = 'absolute';
el.style.left = '-9999px';
document.body.appendChild(el);
const selected =
document.getSelection().rangeCount > 0
? document.getSelection().getRangeAt(0)
: false;
el.select();
document.execCommand('copy');
document.body.removeChild(el);
if (selected) {
document.getSelection().removeAllRanges();
document.getSelection().addRange(selected);
}
};
$("form#inviteForm").submit(function() {
var button = document.getElementById('generateSubmit');
button.disabled = true;
button.innerHTML =
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
'Loading...';
var send_object = $("form#inviteForm").serializeObject();
if (document.getElementById('send_to_address') != null) {
if (document.getElementById('send_to_address_enabled').checked) {
send_object['email'] = document.getElementById('send_to_address').value;
}
}
var send = JSON.stringify(send_object);
$.ajax('/generateInvite', {
data : send,
contentType : 'application/json',
type : 'POST',
xhrFields : {
withCredentials: true
},
beforeSend : function (xhr) {
xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
},
success: function() {
button.textContent = 'Generate';
button.disabled = false;
generateInvites();
},
});
return false;
});
$("form#loginForm").submit(function() {
window.token = "";
var details = $("form#loginForm").serializeObject();
var errorArea = document.getElementById('loginErrorArea');
errorArea.textContent = '';
var button = document.getElementById('loginSubmit');
button.disabled = true;
button.innerHTML =
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
'Loading...';
$.ajax('/getToken', {
type : 'GET',
dataType : 'json',
contentType: 'json',
xhrFields : {
withCredentials: true
},
beforeSend : function (xhr) {
xhr.setRequestHeader("Authorization", "Basic " + btoa(details['username'] + ":" + details['password']));
},
data: { get_param: 'value' },
complete: function(data) {
if (data['status'] == 401) {
button.disabled = false;
button.textContent = 'Login';
var wrongPassword = document.createElement('div');
wrongPassword.classList.add('alert', 'alert-danger');
wrongPassword.setAttribute('role', 'alert');
wrongPassword.appendChild(document.createTextNode('Incorrect username or password.'));
errorArea.appendChild(wrongPassword);
} else {
window.token = JSON.parse(data['responseText'])['token'];
generateInvites();
var interval = setInterval(function() { generateInvites(); }, 60 * 1000);
var hour = document.getElementById('hours');
addOptions(24, hour);
hour.selected = "0";
var minutes = document.getElementById('minutes');
addOptions(59, minutes);
minutes.selected = "30";
$('#login').modal('hide');
}
}
});
return false;
});
document.getElementById('openSettings').onclick = function () {
$('#settingsMenu').modal('show');
}
document.getElementById('openDefaultsWizard').onclick = function () {
this.disabled = true;
this.innerHTML =
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
'Loading...';
$.ajax('getUsers', {
type : 'GET',
dataType : 'json',
contentType : 'json',
xhrFields : {
withCredentials: true
},
beforeSend : function (xhr) {
xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
},
complete : function(data) {
if (data['status'] == 200) {
var radioList = document.getElementById('defaultUserRadios');
radioList.textContent = '';
if (document.getElementById('setDefaultUser')) {
document.getElementById('setDefaultUser').remove();
};
var users = data['responseJSON']['users'];
for (var i = 0; i < users.length; i++) {
var user = users[i]
var radio = document.createElement('div');
radio.classList.add('radio');
if (i == 0) {
var checked = 'checked';
} else {
var checked = '';
};
radio.innerHTML =
'<label><input type="radio" name="defaultRadios" id="default_' +
user['name'] + '" style="margin-right: 1rem;"' + checked + '>' +
user['name'] + '</label>';
radioList.appendChild(radio);
}
var button = document.getElementById('openDefaultsWizard');
button.disabled = false;
button.innerHTML = 'Set new account defaults';
var submitButton = document.getElementById('storeDefaults');
submitButton.disabled = false;
submitButton.textContent = 'Submit';
if (submitButton.classList.contains('btn-success')) {
submitButton.classList.remove('btn-success');
submitButton.classList.add('btn-primary');
} else if (submitButton.classList.contains('btn-danger')) {
submitButton.classList.remove('btn-danger');
submitButton.classList.add('btn-primary');
}
$('#userDefaults').modal('show');
}
}
});
};
document.getElementById('storeDefaults').onclick = function () {
this.disabled = true;
this.innerHTML =
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
'Loading...';
var button = document.getElementById('storeDefaults');
var radios = document.getElementsByName('defaultRadios');
for (var i = 0; i < radios.length; i++) {
if (radios[i].checked) {
var data = {'username':radios[i].id.slice(8), 'homescreen':false};
if (document.getElementById('storeDefaultHomescreen').checked) {
data['homescreen'] = true;
}
$.ajax('/setDefaults', {
data : JSON.stringify(data),
contentType : 'application/json',
type : 'POST',
xhrFields : {
withCredentials: true
},
beforeSend : function (xhr) {
xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
},
success: function() {
button.textContent = 'Success';
if (button.classList.contains('btn-danger')) {
button.classList.remove('btn-danger');
} else if (button.classList.contains('btn-primary')) {
button.classList.remove('btn-primary');
};
button.classList.add('btn-success');
button.disabled = false;
setTimeout(function(){$('#userDefaults').modal('hide');}, 1000);
},
error: function() {
button.textContent = 'Failed';
button.classList.remove('btn-primary');
button.classList.add('btn-danger');
setTimeout(function(){
var button = document.getElementById('storeDefaults');
button.textContent = 'Submit';
button.classList.remove('btn-danger');
button.classList.add('btn-primary');
button.disabled = false;
}, 1000);
}
});
}
}
};
document.getElementById('openUsers').onclick = function () {
this.disabled = true;
this.innerHTML =
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
'Loading...';
$.ajax('/getUsers', {
type : 'GET',
dataType : 'json',
contentType: 'json',
xhrFields : {
withCredentials: true
},
beforeSend : function (xhr) {
xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
},
data: { get_param: 'value' },
complete : function(data) {
if (data['status'] == 200) {
var list = document.getElementById('userList');
list.textContent = '';
if (document.getElementById('saveUsers')) {
document.getElementById('saveUsers').remove();
};
var users = data['responseJSON']['users'];
for (var i = 0; i < users.length; i++) {
var user = users[i]
var entry = document.createElement('p');
entry.id = 'user_' + user['name'];
entry.appendChild(document.createTextNode(user['name']));
var address = document.createElement('span');
address.setAttribute('style', 'margin-left: 2%; margin-right: 2%; color: grey;');
address.classList.add('addressText');
address.id = 'address_' + user['email'];
if (typeof(user['email']) != 'undefined') {
address.appendChild(document.createTextNode(user['email']));
};
var editButton = document.createElement('i');
editButton.classList.add('fa', 'fa-edit');
editButton.onclick = function() {
this.classList.remove('fa', 'fa-edit');
var input = document.createElement('input');
input.setAttribute('type', 'email');
input.setAttribute('style', 'margin-left: 2%; color: grey;');
var addressElement = this.parentNode.getElementsByClassName('addressText')[0];
if (addressElement.textContent != '') {
input.value = addressElement.textContent;
} else {
input.placeholder = 'Email Address';
};
this.parentNode.replaceChild(input, addressElement);
if (document.getElementById('saveUsers') == null) {
var footer = document.getElementById('userFooter')
var saveUsers = document.createElement('input');
saveUsers.classList.add('btn', 'btn-primary');
saveUsers.setAttribute('type', 'button');
saveUsers.value = 'Save Changes';
saveUsers.id = 'saveUsers';
saveUsers.onclick = function() {
var send = {}
var entries = document.getElementById('userList').children;
for (var i = 0; i < entries.length; i++) {
var entry = entries[i];
if (typeof(entry.getElementsByTagName('input')[0]) != 'undefined') {
var name = entry.id.replace(/user_/g, '')
var address = entry.getElementsByTagName('input')[0].value;
send[name] = address
};
};
send = JSON.stringify(send);
$.ajax('/modifyUsers', {
data : send,
contentType : 'application/json',
type : 'POST',
xhrFields : {
withCredentials: true
},
beforeSend : function (xhr) {
xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
},
success: function() { $('#users').modal('hide'); },
});
};
footer.appendChild(saveUsers);
};
};
entry.appendChild(address);
entry.appendChild(editButton);
list.appendChild(entry);
};
var button = document.getElementById('openUsers');
button.disabled = false;
button.innerHTML = 'Users <i class="fa fa-user"></i>';
$('#users').modal('show');
};
}
});
};
generateInvites(empty = true);
$("#login").modal('show');

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#603cba</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -0,0 +1 @@
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="682.667" height="682.667" viewBox="0 0 512.000000 512.000000"><path d="M246 23.7c-13.9 2-30 8.6-40.6 16.6-20.2 15.4-32.5 40-32.5 65.5 0 38.8 24.2 70.7 61.9 81.4 11.1 3.2 31.4 3.1 42.7-.1 47.8-13.4 73-62.2 56.5-109.5-7.8-22.5-27.9-42.1-51.5-50-10.6-3.6-26.4-5.3-36.5-3.9zM235.5 205.6c-37.7 6.8-73.7 27.7-106.2 61.8-31.6 33.1-54.2 73.1-62.4 110.8-3 13.3-3.2 35.7-.5 45.8 5.6 21.2 20.1 36.8 43.4 46.9 9.2 3.9 21.4 7.8 30.7 9.6 10 1.9 26.1 4.5 32.5 5.1 4.1.4 8.9.8 10.5 1 25.3 2.5 85.7 2.4 107-.1 1.7-.2 5.7-.7 9-1 6.8-.7 11.2-1.2 17-1.9 34.7-4.4 70.3-13.2 90.8-22.4 17.8-8.1 30.7-18.6 35.4-28.8 5.8-12.6 6.6-36.1 1.9-56.4-5-22-16.8-48.3-31.5-70.5-33.6-50.8-80.8-87-128.1-98.2-13.3-3.1-37.1-3.9-49.5-1.7z"/></svg>

After

Width:  |  Height:  |  Size: 768 B

View 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';
}
});
};

View File

@@ -0,0 +1,19 @@
{
"name": "jf-accounts",
"short_name": "jf-accounts",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View File

@@ -0,0 +1,35 @@
<!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="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#603cba">
<meta name="theme-color" content="#ffffff">
<title>404</title>
<link rel="stylesheet" href="{{ css_href }}" integrity="{{ css_integrity }}" crossorigin="{{ css_crossorigin }}">
<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>
<style>
.messageBox {
margin: 20%;
}
</style>
</head>
<body>
<div class="messageBox">
<h1>Page not found.</h1>
<p>
{{ contactMessage }}
</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,207 @@
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#603cba">
<meta name="theme-color" content="#ffffff">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="{{ css_href }}" integrity="{{ css_integrity }}" crossorigin="{{ css_crossorigin }}">
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></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>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<style>
.pageContainer {
margin: 20%;
}
@media (max-width: 1100px) {
.pageContainer {
margin: 2%;
}
}
h1 {
/*margin: 20%;*/
margin-bottom: 5%;
}
.linkGroup {
/*margin: 20%;*/
margin-bottom: 5%;
margin-top: 5%;
}
.linkForm {
/*margin: 20%;*/
margin-top: 5%;
margin-bottom: 5%;
}
.contactBox {
/*margin: 20%;*/
margin-top: 5%;
color: grey;
}
.fa-clipboard {
color: grey;
}
.fa-clipboard:hover {
color: black;
}
</style>
<title>Admin</title>
</head>
<body>
<div class="modal fade" id="login" tabindex="-1" role="dialog" aria-labelledby="login" aria-hidden="true" data-backdrop="static">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="loginTitle">Login</h5>
</div>
<div class="modal-body" id="formBody">
<form action="#" method="POST" id="loginForm">
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" id="username" name="username" placeholder="Username" required>
<label for="password">Password</label>
<input type="password" class="form-control" id="password" name="password" placeholder="Password" required>
</div>
</form>
<div id="loginErrorArea"></div>
</div>
<div class="modal-footer">
<button type="submit" id="loginSubmit" class="btn btn-primary" form="loginForm">Login</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="settingsMenu" tabindex="-1" role="dialog" aria-labelledby="settings menu" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="settingsTitle">Settings</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<ul class="list-group list-group-flush">
<li class="list-group-item">
<button type="button" class="btn btn-secondary" id="openUsers">
Users <i class="fa fa-user"></i>
</button>
</li>
<li class="list-group-item">
<button type="button" class="btn btn-secondary" id="openDefaultsWizard">
New account defaults
</button>
</li>
</ul>
</div>
<div class="modal-footer" id="settingsFooter">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="users" tabindex="-1" role="dialog" aria-labelledby="users" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="usersTitle">Users</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<ul class="list-group list-group-flush" id="userList">
</ul>
</div>
<div class="modal-footer" id="userFooter">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="userDefaults" tabindex="-1" role="dialog" aria-labelledby="users" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="defaultsTitle">New user defaults</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>Create an account and configure it to your liking, then choose it from below to store the settings as a template for all new users.</p>
<div id="defaultUserRadios"></div>
<div class="checkbox">
<label><input type="checkbox" value="" style="margin-right: 1rem;" id="storeDefaultHomescreen" checked>Store homescreen layout</label>
</div>
</div>
<div class="modal-footer" id="defaultsFooter">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="storeDefaults">Submit</button>
</div>
</div>
</div>
</div>
<div class="pageContainer">
<h1>
Accounts admin
</h1>
<button type="button" class="btn btn-secondary" id="openSettings">
Settings <i class="fa fa-cog"></i>
</button>
<div class="card bg-light mb-3 linkGroup">
<div class="card-header">Current Invites</div>
<ul class="list-group list-group-flush" id="invites">
</ul>
</div>
<div class="linkForm">
<div class="card bg-light mb-3">
<div class="card-header">Generate Invite</div>
<div class="card-body">
<form action="#" method="POST" id="inviteForm">
<div class="form-group">
<label for="hours">Hours</label>
<select class="form-control" id="hours" name="hours">
</select>
</div>
<div class="form-group">
<label for="minutes">Minutes</label>
<select class="form-control" id="minutes" name="minutes">
</select>
</div>
{% if email_enabled %}
<div class="form-group">
<label for="send_to_address">Send invite to address</label>
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text">
<input type="checkbox" onchange="document.getElementById('send_to_address').disabled = !this.checked;" aria-label="Checkbox to allow input of email address" id="send_to_address_enabled">
</div>
</div>
<input type="email" class="form-control" placeholder="example@example.com" id="send_to_address" disabled>
</div>
</div>
{% endif %}
<button type="submit" id="generateSubmit" class="btn btn-primary">Generate</button>
</form>
</div>
</div>
</div>
<div class="contactBox">
<p>{{ contactMessage }}</p>
</div>
</div>
<script src="admin.js"></script>
</body>
</html>

View File

@@ -0,0 +1,184 @@
<!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="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#603cba">
<meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="{{ css_href }}" integrity="{{ css_integrity }}" crossorigin="{{ css_crossorigin }}">
<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>
.pageContainer {
margin: 20%;
}
@media (max-width: 1100px) {
.pageContainer {
margin: 2%;
}
}
.contactBox {
color: grey;
}
#container {
margin-top: 5%;
margin-bottom: 5%;
}
</style>
<title>Create Jellyfin Account</title>
</head>
<body>
<div class="modal fade" id="successBox" tabindex="-1" role="dialog" aria-labelledby="successBox" aria-hidden="true" data-backdrop="static">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="successTitle">Success!</h5>
</div>
<div class="modal-body" id="successBody">
<p>{{ successMessage }}</p>
</div>
<div class="modal-footer">
<a href="{{ jfLink }}" class="btn btn-primary">Continue</a>
</div>
</div>
</div>
</div>
<div class="pageContainer">
<h1>
Create Account
</h1>
<p>{{ helpMessage }}</p>
<p class="contactBox">{{ contactMessage }}</p>
<div class="container" id="container">
<div class="row" id="cardContainer">
<div class="col-sm" id="accountForm">
<div class="card bg-light mb-3">
<div class="card-header">Details</div>
<div class="card-body">
<form action="#" method="POST">
<div class="form-group">
<label for="inputEmail">Email</label>
<input type="email" class="form-control" id="inputEmail" name="email" placeholder="Email" value="{{ email }}" required>
</div>
<div class="form-group">
<label for="inputUsername">Username</label>
<input type="username" class="form-control" id="inputUsername" name="username" placeholder="Username" required>
</div>
<div class="form-group">
<label for="inputPassword">Password</label>
<input type="password" class="form-control" id="inputPassword" name="password" placeholder="Password" required>
</div>
<div class="btn-group" role="group" aria-label="Button & Error" id="errorBox">
<button type="submit" class="btn btn-outline-primary" id="submitButton">
<span id="createAccount">Create Account</span>
</button>
</div>
</form>
</div>
</div>
</div>
{% if validate %}
<div class="col-sm" id="requirementBox">
<div class="card bg-light mb-3 requirementBox">
<div class="card-header">Password Requirements</div>
<div class="card-body">
<ul class="list-group">
{% for key, value in requirements.items() %}
<li id="{{ key }}" class="list-group-item list-group-item-danger">
<div> {{ value }}</div>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>
<script>
var code = window.location.href.split('/').pop();
function toggleSpinner () {
var submitButton = document.getElementById('submitButton');
var oldSpan = document.getElementById('createAccount');
var newSpan = document.createElement('span');
newSpan.id = 'createAccount';
if (document.getElementById('createAccountSpinner')) {
newSpan.appendChild(document.createTextNode('Create Account'));
submitButton.disabled = false;
} else {
var spinner = document.createElement('span');
spinner.id = 'createAccountSpinner';
spinner.classList.add('spinner-border', 'spinner-border-sm');
spinner.setAttribute('role', 'status');
spinner.setAttribute('aria-hidden', 'true');
var text = document.createTextNode(' Creating...');
newSpan.appendChild(spinner);
newSpan.appendChild(text);
submitButton.disabled = true;
}
submitButton.replaceChild(newSpan, oldSpan);
};
$("form").submit(function() {
toggleSpinner();
var send = $("form").serializeObject();
send['code'] = code;
send = JSON.stringify(send);
$.ajax('/newUser', {
data : send,
contentType : 'application/json',
type : 'POST',
crossDomain : true,
complete : function(response){
toggleSpinner();
var data = response['responseJSON'];
if ('error' in data) {
var text = document.createTextNode(data['error']);
// <div class="alert alert-danger" id="errorBox"></div>
var error = document.createElement('button');
error.classList.add('btn', 'btn-outline-danger');
error.setAttribute('disabled', '');
error.appendChild(text);
document.getElementById('errorBox').appendChild(error);
} else {
var valid = true
for (var key in data) {
if (data.hasOwnProperty(key)) {
var criterion = document.getElementById(key);
if (criterion) {
if (data[key] == false) {
valid = false;
if (criterion.classList.contains('list-group-item-success')) {
criterion.classList.remove('list-group-item-success');
criterion.classList.add('list-group-item-danger');
};
} else {
if (criterion.classList.contains('list-group-item-danger')) {
criterion.classList.remove('list-group-item-danger');
criterion.classList.add('list-group-item-success');
};
};
};
};
};
if (valid == true) {
$('#successBox').modal('show');
};
}
}
});
return false;
});
</script>
</body>
</html>

View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Invalid Code</title>
<link rel="stylesheet" href="{{ css_href }}" integrity="{{ css_integrity }}" crossorigin="{{ css_crossorigin }}">
<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>
<style>
.messageBox {
margin: 20%;
}
</style>
</head>
<body>
<div class="messageBox">
<h1>Invalid Code.</h1>
<p>The above code is either incorrect, or has expired.</p>
<p>{{ contactMessage }}</p>
</div>
</body>
</html>

View 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 &amp; 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>