first
@@ -0,0 +1,567 @@
|
||||
{
|
||||
"jellyfin": {
|
||||
"meta": {
|
||||
"name": "Jellyfin",
|
||||
"description": "Settings for connecting to Jellyfin"
|
||||
},
|
||||
"username": {
|
||||
"name": "Jellyfin Username",
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "username",
|
||||
"description": "It is recommended to create a limited admin account for this program."
|
||||
},
|
||||
"password": {
|
||||
"name": "Jellyfin Password",
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"type": "password",
|
||||
"value": "password"
|
||||
},
|
||||
"server": {
|
||||
"name": "Server address",
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "http://jellyfin.local:8096",
|
||||
"description": "Jellyfin server address. Can be public, or local for security purposes."
|
||||
},
|
||||
"public_server": {
|
||||
"name": "Public address",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "https://jellyf.in:443",
|
||||
"description": "Publicly accessible Jellyfin address for invite form. Leave blank to reuse the above address."
|
||||
},
|
||||
"client": {
|
||||
"name": "Client Name",
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "jf-accounts",
|
||||
"description": "This and below settings will show on the Jellyfin dashboard when the program connects. You may as well leave them alone."
|
||||
},
|
||||
"version": {
|
||||
"name": "Version Number",
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "{version}"
|
||||
},
|
||||
"device": {
|
||||
"name": "Device Name",
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "jf-accounts"
|
||||
},
|
||||
"device_id": {
|
||||
"name": "Device ID",
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "jf-accounts-{version}"
|
||||
}
|
||||
},
|
||||
"ui": {
|
||||
"meta": {
|
||||
"name": "General",
|
||||
"description": "Settings related to the UI and program functionality."
|
||||
},
|
||||
"theme": {
|
||||
"name": "Default Look",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "select",
|
||||
"options": [
|
||||
"Bootstrap (Light)",
|
||||
"Jellyfin (Dark)",
|
||||
"Custom CSS"
|
||||
],
|
||||
"value": "Jellyfin (Dark)",
|
||||
"description": "Default appearance for all users."
|
||||
},
|
||||
"host": {
|
||||
"name": "Address",
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "0.0.0.0",
|
||||
"description": "Set 0.0.0.0 to run on localhost"
|
||||
},
|
||||
"port": {
|
||||
"name": "Port",
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"type": "number",
|
||||
"value": 8056
|
||||
},
|
||||
"jellyfin_login": {
|
||||
"name": "Use Jellyfin for authentication",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": true,
|
||||
"description": "Enable this to use Jellyfin users instead of the below username and pw."
|
||||
},
|
||||
"admin_only": {
|
||||
"name": "Allow admin users only",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"depends_true": "jellyfin_login",
|
||||
"type": "bool",
|
||||
"value": true,
|
||||
"description": "Allows only admin users on Jellyfin to access the admin page."
|
||||
},
|
||||
"username": {
|
||||
"name": "Web Username",
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"depends_false": "jellyfin_login",
|
||||
"type": "text",
|
||||
"value": "your username",
|
||||
"description": "Username for admin page (Leave blank if using jellyfin_login)"
|
||||
},
|
||||
"password": {
|
||||
"name": "Web Password",
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"depends_false": "jellyfin_login",
|
||||
"type": "password",
|
||||
"value": "your password",
|
||||
"description": "Password for admin page (Leave blank if using jellyfin_login)"
|
||||
},
|
||||
"email": {
|
||||
"name": "Admin email address",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_false": "jellyfin_login",
|
||||
"type": "text",
|
||||
"value": "example@example.com",
|
||||
"description": "Address to send notifications to (Leave blank if using jellyfin_login)"
|
||||
},
|
||||
"debug": {
|
||||
"name": "Debug logging",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": false
|
||||
},
|
||||
"contact_message": {
|
||||
"name": "Contact message",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "Need help? contact me.",
|
||||
"description": "Displayed at bottom of all pages except admin"
|
||||
},
|
||||
"help_message": {
|
||||
"name": "Help message",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "Enter your details to create an account.",
|
||||
"description": "Displayed at top of invite form."
|
||||
},
|
||||
"success_message": {
|
||||
"name": "Success message",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "Your account has been created. Click below to continue to Jellyfin.",
|
||||
"description": "Displayed when a user creates an account"
|
||||
},
|
||||
"bs5": {
|
||||
"name": "Use Bootstrap 5",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Use Bootstrap 5 (currently in alpha). This also removes the need for jQuery, so the page should load faster."
|
||||
}
|
||||
},
|
||||
"password_validation": {
|
||||
"meta": {
|
||||
"name": "Password Validation",
|
||||
"description": "Password validation (minimum length, etc.)"
|
||||
},
|
||||
"enabled": {
|
||||
"name": "Enabled",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "bool",
|
||||
"value": true
|
||||
},
|
||||
"min_length": {
|
||||
"name": "Minimum Length",
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "8"
|
||||
},
|
||||
"upper": {
|
||||
"name": "Minimum uppercase characters",
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "1"
|
||||
},
|
||||
"lower": {
|
||||
"name": "Minimum lowercase characters",
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "0"
|
||||
},
|
||||
"number": {
|
||||
"name": "Minimum number count",
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "1"
|
||||
},
|
||||
"special": {
|
||||
"name": "Minimum number of special characters",
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "0"
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
"meta": {
|
||||
"name": "Email",
|
||||
"description": "General email settings. Ignore if not using email features."
|
||||
},
|
||||
"no_username": {
|
||||
"name": "Use email addresses as username",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "method",
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Use email address from invite form as username on Jellyfin."
|
||||
},
|
||||
"use_24h": {
|
||||
"name": "Use 24h time",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "method",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
},
|
||||
"date_format": {
|
||||
"name": "Date format",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "method",
|
||||
"type": "text",
|
||||
"value": "%d/%m/%y",
|
||||
"description": "Date format used in emails. Follows datetime.strftime format."
|
||||
},
|
||||
"message": {
|
||||
"name": "Help message",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "method",
|
||||
"type": "text",
|
||||
"value": "Need help? contact me.",
|
||||
"description": "Message displayed at bottom of emails."
|
||||
},
|
||||
"method": {
|
||||
"name": "Email method",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "select",
|
||||
"options": [
|
||||
"smtp",
|
||||
"mailgun"
|
||||
],
|
||||
"value": "smtp",
|
||||
"description": "Method of sending email to use."
|
||||
},
|
||||
"address": {
|
||||
"name": "Sent from (address)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "method",
|
||||
"type": "email",
|
||||
"value": "jellyfin@jellyf.in",
|
||||
"description": "Address to send emails from"
|
||||
},
|
||||
"from": {
|
||||
"name": "Sent from (name)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "method",
|
||||
"type": "text",
|
||||
"value": "Jellyfin",
|
||||
"description": "The name of the sender"
|
||||
}
|
||||
},
|
||||
"password_resets": {
|
||||
"meta": {
|
||||
"name": "Password Resets",
|
||||
"description": "Settings for the password reset handler."
|
||||
},
|
||||
"enabled": {
|
||||
"name": "Enabled",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": true,
|
||||
"description": "Enable to store provided email addresses, monitor Jellyfin directory for pw-resets, and send reset pins"
|
||||
},
|
||||
"watch_directory": {
|
||||
"name": "Jellyfin directory",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "/path/to/jellyfin",
|
||||
"description": "Path to the folder Jellyfin puts password-reset files."
|
||||
},
|
||||
"email_html": {
|
||||
"name": "Custom email (HTML)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to custom email html"
|
||||
},
|
||||
"email_text": {
|
||||
"name": "Custom email (plaintext)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to custom email in plain text"
|
||||
},
|
||||
"subject": {
|
||||
"name": "Email subject",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "Password Reset - Jellyfin",
|
||||
"description": "Subject of password reset emails."
|
||||
}
|
||||
},
|
||||
"invite_emails": {
|
||||
"meta": {
|
||||
"name": "Invite emails",
|
||||
"description": "Settings for sending invites directly to users."
|
||||
},
|
||||
"enabled": {
|
||||
"name": "Enabled",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "bool",
|
||||
"value": true
|
||||
},
|
||||
"email_html": {
|
||||
"name": "Custom email (HTML)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to custom email HTML"
|
||||
},
|
||||
"email_text": {
|
||||
"name": "Custom email (plaintext)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to custom email in plain text"
|
||||
},
|
||||
"subject": {
|
||||
"name": "Email subject",
|
||||
"required": true,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "Invite - Jellyfin",
|
||||
"description": "Subject of invite emails."
|
||||
},
|
||||
"url_base": {
|
||||
"name": "URL Base",
|
||||
"required": true,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "http://accounts.jellyf.in:8056/invite",
|
||||
"description": "Base URL for jf-accounts. This is necessary because using a reverse proxy means the program has no way of knowing the URL itself."
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"meta": {
|
||||
"name": "Notifications",
|
||||
"description": "Notification related settings."
|
||||
},
|
||||
"enabled": {
|
||||
"name": "Enabled",
|
||||
"required": "false",
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": true,
|
||||
"description": "Enabling adds optional toggles to invites to notify on expiry and user creation."
|
||||
},
|
||||
"expiry_html": {
|
||||
"name": "Expiry email (HTML)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to expiry notification email HTML."
|
||||
},
|
||||
"expiry_text": {
|
||||
"name": "Expiry email (Plaintext)",
|
||||
"required": false,
|
||||
"requires_restart": "false",
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to expiry notification email in plaintext."
|
||||
},
|
||||
"created_html": {
|
||||
"name": "User created email (HTML)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to user creation notification email HTML."
|
||||
},
|
||||
"created_text": {
|
||||
"name": "User created email (Plaintext)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to user creation notification email in plaintext."
|
||||
}
|
||||
},
|
||||
"mailgun": {
|
||||
"meta": {
|
||||
"name": "Mailgun (Email)",
|
||||
"description": "Mailgun API connection settings"
|
||||
},
|
||||
"api_url": {
|
||||
"name": "API URL",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "https://api.mailgun.net..."
|
||||
},
|
||||
"api_key": {
|
||||
"name": "API Key",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "your api key"
|
||||
}
|
||||
},
|
||||
"smtp": {
|
||||
"meta": {
|
||||
"name": "SMTP (Email)",
|
||||
"description": "SMTP Server connection settings."
|
||||
},
|
||||
"encryption": {
|
||||
"name": "Encryption Method",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "select",
|
||||
"options": [
|
||||
"ssl_tls",
|
||||
"starttls"
|
||||
],
|
||||
"value": "starttls",
|
||||
"description": "Your email provider should provide different ports for each encryption method. Generally 465 for ssl_tls, 587 for starttls."
|
||||
},
|
||||
"server": {
|
||||
"name": "Server address",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "smtp.jellyf.in",
|
||||
"description": "SMTP Server address."
|
||||
},
|
||||
"port": {
|
||||
"name": "Port",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "number",
|
||||
"value": 465
|
||||
},
|
||||
"password": {
|
||||
"name": "Password",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "password",
|
||||
"value": "smtp password"
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"meta": {
|
||||
"name": "File Storage",
|
||||
"description": "Optional settings for changing storage locations."
|
||||
},
|
||||
"invites": {
|
||||
"name": "Invite Storage",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Location of stored invites (json)."
|
||||
},
|
||||
"emails": {
|
||||
"name": "Email Addresses",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Location of stored email addresses (json)."
|
||||
},
|
||||
"user_template": {
|
||||
"name": "User Template",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Location of stored user policy template (json)."
|
||||
},
|
||||
"user_configuration": {
|
||||
"name": "userConfiguration",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Location of stored user configuration template (used for setting homescreen layout) (json)"
|
||||
},
|
||||
"user_displayprefs": {
|
||||
"name": "displayPreferences",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Location of stored displayPreferences template (also used for homescreen layout) (json)"
|
||||
},
|
||||
"custom_css": {
|
||||
"name": "Custom CSS",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Location of custom bootstrap CSS."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
[jellyfin]
|
||||
; settings for connecting to jellyfin
|
||||
; it is recommended 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 for invite form. leave blank to reuse the above address.
|
||||
public_server = https://jellyf.in:443
|
||||
; this and below settings will show on the jellyfin dashboard when the program connects. you may as well leave them alone.
|
||||
client = jf-accounts
|
||||
version = 0.3.7
|
||||
device = jf-accounts
|
||||
device_id = jf-accounts-0.3.7
|
||||
|
||||
[ui]
|
||||
; settings related to the ui and program functionality.
|
||||
; default appearance for all users.
|
||||
theme = Jellyfin (Dark)
|
||||
; set 0.0.0.0 to run on 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 the admin page.
|
||||
admin_only = true
|
||||
; username for admin page (leave blank if using jellyfin_login)
|
||||
username = your username
|
||||
; password for admin page (leave blank if using jellyfin_login)
|
||||
password = your password
|
||||
; address to send notifications to (leave blank if using jellyfin_login)
|
||||
email = example@example.com
|
||||
debug = false
|
||||
; displayed at bottom of all pages except admin
|
||||
contact_message = Need help? contact me.
|
||||
; displayed at top of invite form.
|
||||
help_message = Enter your details to create an account.
|
||||
; displayed when a user creates an account
|
||||
success_message = Your account has been created. Click below to continue to Jellyfin.
|
||||
; use bootstrap 5 (currently in alpha). this also removes the need for jquery, so the page should load faster.
|
||||
bs5 = false
|
||||
|
||||
[password_validation]
|
||||
; password validation (minimum length, etc.)
|
||||
enabled = true
|
||||
min_length = 8
|
||||
upper = 1
|
||||
lower = 0
|
||||
number = 1
|
||||
special = 0
|
||||
|
||||
[email]
|
||||
; general email settings. ignore if not using email features.
|
||||
; use email address from invite form as username on jellyfin.
|
||||
no_username = false
|
||||
use_24h = true
|
||||
; date format used in emails. follows datetime.strftime format.
|
||||
date_format = %d/%m/%y
|
||||
; message displayed at bottom of emails.
|
||||
message = Need help? contact me.
|
||||
; method of sending email to use.
|
||||
method = smtp
|
||||
; address to send emails from
|
||||
address = jellyfin@jellyf.in
|
||||
; the name of the sender
|
||||
from = Jellyfin
|
||||
|
||||
[password_resets]
|
||||
; settings for the password reset handler.
|
||||
; enable to store provided email addresses, monitor jellyfin directory for pw-resets, and send reset pins
|
||||
enabled = true
|
||||
; path to the folder jellyfin puts password-reset files.
|
||||
watch_directory = /path/to/jellyfin
|
||||
; path to custom email html
|
||||
email_html =
|
||||
; path to custom email in plain text
|
||||
email_text =
|
||||
; subject of password reset emails.
|
||||
subject = Password Reset - Jellyfin
|
||||
|
||||
[invite_emails]
|
||||
; settings for sending invites directly to users.
|
||||
enabled = true
|
||||
; path to custom email html
|
||||
email_html =
|
||||
; path to custom email in plain text
|
||||
email_text =
|
||||
; subject of invite emails.
|
||||
subject = Invite - Jellyfin
|
||||
; base url for jf-accounts. this is necessary because using a reverse proxy means the program has no way of knowing the url itself.
|
||||
url_base = http://accounts.jellyf.in:8056/invite
|
||||
|
||||
[notifications]
|
||||
; notification related settings.
|
||||
; enabling adds optional toggles to invites to notify on expiry and user creation.
|
||||
enabled = true
|
||||
; path to expiry notification email html.
|
||||
expiry_html =
|
||||
; path to expiry notification email in plaintext.
|
||||
expiry_text =
|
||||
; path to user creation notification email html.
|
||||
created_html =
|
||||
; path to user creation notification email in plaintext.
|
||||
created_text =
|
||||
|
||||
[mailgun]
|
||||
; mailgun api connection settings
|
||||
api_url = https://api.mailgun.net...
|
||||
api_key = your api key
|
||||
|
||||
[smtp]
|
||||
; smtp server connection settings.
|
||||
; your email provider should provide different ports for each encryption method. generally 465 for ssl_tls, 587 for starttls.
|
||||
encryption = starttls
|
||||
; smtp server address.
|
||||
server = smtp.jellyf.in
|
||||
port = 465
|
||||
password = smtp password
|
||||
|
||||
[files]
|
||||
; optional settings for changing storage locations.
|
||||
; location of stored invites (json).
|
||||
invites =
|
||||
; location of stored email addresses (json).
|
||||
emails =
|
||||
; location of stored user policy template (json).
|
||||
user_template =
|
||||
; location of stored user configuration template (used for setting homescreen layout) (json)
|
||||
user_configuration =
|
||||
; location of stored displaypreferences template (also used for homescreen layout) (json)
|
||||
user_displayprefs =
|
||||
; location of custom bootstrap css.
|
||||
custom_css =
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
<!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=Quicksand" rel="stylesheet" type="text/css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans" 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=Quicksand);
|
||||
@import url(https://fonts.googleapis.com/css2?family=Noto+Sans);
|
||||
</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:#242424;background-color:#242424;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#242424;background-color:#242424;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:Quicksand, Noto Sans, Helvetica, Arial, sans-serif;font-size:25px;line-height:1;text-align:left;color:rgba(255,255,255,0.87);">jellyfin-accounts</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="background:#101010;background-color:#101010;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#101010;background-color:#101010;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:16px;line-height:1;text-align:left;color:rgba(255,255,255,0.8);">
|
||||
<h3>User Created</h3>
|
||||
<p>A user was created using code {{ code }}.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="background:#242424;font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table cellpadding="0" cellspacing="0" width="100%" border="0" style="color:rgba(255,255,255,0.8);font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:22px;table-layout:auto;width:100%;border:none;">
|
||||
<tr style="text-align: left;">
|
||||
<th>Name</th>
|
||||
<th>Address</th>
|
||||
<th>Time</th>
|
||||
</tr>
|
||||
<tr style="font-style: italic; text-align: left; color: rgb(153,153,153);">
|
||||
<th>{{ username }}</th>
|
||||
<th>{{ address }}</th>
|
||||
<th>{{ time }}</th>
|
||||
</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:#242424;background-color:#242424;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#242424;background-color:#242424;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:14px;font-style:italic;line-height:1;text-align:left;color:rgb(153,153,153);">Notification emails can be toggled on the admin dashboard.</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>
|
||||
@@ -0,0 +1,7 @@
|
||||
A user was created using code {{ code }}.
|
||||
|
||||
Name: {{ username }}
|
||||
Address: {{ address }}
|
||||
Time: {{ time }}
|
||||
|
||||
Note: Notification emails can be toggled on the admin dashboard.
|
||||
@@ -0,0 +1,41 @@
|
||||
<!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" type="text/css" href="{{ css_file }}">
|
||||
{% if not bs5 %}
|
||||
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
|
||||
{% endif %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
|
||||
{% if bs5 %}
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/js/bootstrap.min.js" integrity="sha384-oesi62hOLfzrys4LxRF63OJCXdXDipiYWBnvTl9Y9/TRlw5xlKIEHpNyvvDShgf/" crossorigin="anonymous"></script>
|
||||
{% else %}
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
|
||||
{% endif %}
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<style>
|
||||
.messageBox {
|
||||
margin: 20%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="messageBox">
|
||||
<h1>Page not found.</h1>
|
||||
<p>
|
||||
{{ contactMessage }}
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,222 @@
|
||||
<!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">
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link rel="stylesheet" type="text/css" href="{{ css_file }}">
|
||||
{% if not bs5 %}
|
||||
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
|
||||
{% endif %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
|
||||
{% if bs5 %}
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/js/bootstrap.min.js" integrity="sha384-oesi62hOLfzrys4LxRF63OJCXdXDipiYWBnvTl9Y9/TRlw5xlKIEHpNyvvDShgf/" crossorigin="anonymous"></script>
|
||||
{% else %}
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
|
||||
{% endif %}
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<style>
|
||||
.pageContainer {
|
||||
margin: 5% 20% 5% 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">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">Details</div>
|
||||
<div class="card-body">
|
||||
<form action="#" method="POST" id="accountForm">
|
||||
<div class="form-group">
|
||||
<label for="inputEmail">Email</label>
|
||||
<input type="email" class="form-control" id="{% if username %}inputEmail{% else %}inputUsername{% endif %}" name="{% if username %}email{% else %}username{% endif %}" placeholder="Email" value="{{ email }}" required>
|
||||
</div>
|
||||
{% if username %}
|
||||
<div class="form-group">
|
||||
<label for="inputUsername">Username</label>
|
||||
<input type="username" class="form-control" id="inputUsername" name="username" placeholder="Username" required>
|
||||
</div>
|
||||
{% endif %}
|
||||
<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" style="margin-top: 1rem;">
|
||||
<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 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 src="serialize.js"></script>
|
||||
<script>
|
||||
{% if bs5 %}
|
||||
var bsVersion = 5;
|
||||
{% else %}
|
||||
var bsVersion = 4;
|
||||
{% endif %}
|
||||
if (bsVersion == 5) {
|
||||
var successBox = new bootstrap.Modal(document.getElementById('successBox'));
|
||||
} else if (bsVersion == 4) {
|
||||
var successBox = {
|
||||
show : function() {
|
||||
return $('#successBox').modal('show');
|
||||
},
|
||||
hide : function() {
|
||||
return $('#successBox').modal('hide');
|
||||
}
|
||||
};
|
||||
};
|
||||
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);
|
||||
};
|
||||
document.getElementById('accountForm').onsubmit = function() {
|
||||
if (document.getElementById('errorMessage')) {
|
||||
document.getElementById('errorMessage').remove();
|
||||
}
|
||||
toggleSpinner();
|
||||
var send = serializeForm('accountForm');
|
||||
send['code'] = code;
|
||||
{% if not username %}
|
||||
send['email'] = send['username'];
|
||||
{% endif %}
|
||||
send = JSON.stringify(send);
|
||||
var req = new XMLHttpRequest();
|
||||
req.open("POST", "/newUser", true);
|
||||
req.responseType = 'json';
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
toggleSpinner();
|
||||
var data = this.response;
|
||||
if ('error' in data || data['success'] == false) {
|
||||
if (typeof(data['error']) != 'undefined') {
|
||||
var errorMessage = data['error'];
|
||||
} else {
|
||||
var errorMessage = 'Unknown Error';
|
||||
}
|
||||
var text = document.createTextNode(errorMessage);
|
||||
var error = document.createElement('button');
|
||||
error.classList.add('btn', 'btn-outline-danger');
|
||||
error.setAttribute('disabled', '');
|
||||
error.appendChild(text);
|
||||
error.id = 'errorMessage';
|
||||
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.show();
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
req.send(send);
|
||||
return false;
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,32 @@
|
||||
<!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>
|
||||
<!-- Bootstrap CSS -->
|
||||
<link rel="stylesheet" type="text/css" href="{{ css_file }}">
|
||||
{% if not bs5 %}
|
||||
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
|
||||
{% endif %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
|
||||
{% if bs5 %}
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/js/bootstrap.min.js" integrity="sha384-oesi62hOLfzrys4LxRF63OJCXdXDipiYWBnvTl9Y9/TRlw5xlKIEHpNyvvDShgf/" crossorigin="anonymous"></script>
|
||||
{% else %}
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
|
||||
{% endif %}
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<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>
|
||||
@@ -0,0 +1,370 @@
|
||||
<!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 class="form-group">
|
||||
<label for="manualAuthEmail">Email (Optional)</label>
|
||||
<input type="email" class="form-control" id="manualAuthEmail" placeholder="example@example.com">
|
||||
<small class="form-text text-muted">Your email address is only required if you want to recieve activity notifications.</small>
|
||||
</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>
|
||||
<div id="emailCommonArea">
|
||||
<h5 class="card-title">Notifications</h5>
|
||||
<p class="card-text">Enabling notifications will allow you to choose (per-invite) to recieve emails when an invite expires, or when a new user is created. If you chose to use Manual auth instead of Jellyfin auth previously, make sure you provided an email address.</p>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="notificationsEnabled">
|
||||
<label for="notificationsEnabled" class="form-check-label">Enabled</label>
|
||||
|
||||
</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 restart. Once it's done, 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>
|
||||
@@ -0,0 +1,243 @@
|
||||
<!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=Quicksand" rel="stylesheet" type="text/css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans" 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=Quicksand);
|
||||
@import url(https://fonts.googleapis.com/css2?family=Noto+Sans);
|
||||
</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:#242424;background-color:#242424;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#242424;background-color:#242424;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:Quicksand, Noto Sans, Helvetica, Arial, sans-serif;font-size:25px;line-height:1;text-align:left;color:rgba(255,255,255,0.87);">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="background:#101010;background-color:#101010;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#101010;background-color:#101010;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:16px;line-height:1;text-align:left;color:rgba(255,255,255,0.8);">
|
||||
<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 }} UTC, 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="rgb(0,164,220)" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:rgb(0,164,220);" valign="middle">
|
||||
<p style="display:inline-block;background:rgb(0,164,220);color:rgba(255,255,255,0.87);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:#242424;background-color:#242424;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#242424;background-color:#242424;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:14px;font-style:italic;line-height:1;text-align:left;color:rgb(153,153,153);">{{ 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>
|
||||
@@ -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 }} UTC, which is in {{ expires_in }}.
|
||||
If this wasn't you, please ignore this email.
|
||||
|
||||
PIN: {{ pin }}
|
||||
|
||||
{{ message }}
|
||||
@@ -0,0 +1,227 @@
|
||||
<!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=Quicksand" rel="stylesheet" type="text/css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans" 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=Quicksand);
|
||||
@import url(https://fonts.googleapis.com/css2?family=Noto+Sans);
|
||||
</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:#242424;background-color:#242424;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#242424;background-color:#242424;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:Quicksand, Noto Sans, Helvetica, Arial, sans-serif;font-size:25px;line-height:1;text-align:left;color:rgba(255,255,255,0.87);">jellyfin-accounts</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="background:#101010;background-color:#101010;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#101010;background-color:#101010;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:16px;line-height:1;text-align:left;color:rgba(255,255,255,0.8);">
|
||||
<h3>Invite Expired.</h3>
|
||||
<p>Code {{ code }} expired at {{ expiry }}.</p>
|
||||
</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="background:#242424;background-color:#242424;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#242424;background-color:#242424;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:14px;font-style:italic;line-height:1;text-align:left;color:rgb(153,153,153);">Notification emails can be toggled on the admin dashboard.</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>
|
||||
@@ -0,0 +1,5 @@
|
||||
Invite expired.
|
||||
|
||||
Code {{ code }} expired at {{ expiry }}.
|
||||
|
||||
Note: Notification emails can be toggled on the admin dashboard.
|
||||
@@ -0,0 +1,240 @@
|
||||
<!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=Quicksand" rel="stylesheet" type="text/css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans" 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=Quicksand);
|
||||
@import url(https://fonts.googleapis.com/css2?family=Noto+Sans);
|
||||
</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:#242424;background-color:#242424;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#242424;background-color:#242424;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:Quicksand, Noto Sans, Helvetica, Arial, sans-serif;font-size:25px;line-height:1;text-align:left;color:rgba(255,255,255,0.87);">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="background:#101010;background-color:#101010;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#101010;background-color:#101010;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:16px;line-height:1;text-align:left;color:rgba(255,255,255,0.8);">
|
||||
<p>Hi,</p>
|
||||
<h3>You've been invited to Jellyfin.</h3>
|
||||
<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="rgb(0,164,220)" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:rgb(0,164,220);" valign="middle">
|
||||
<a href="{{ invite_link }}" style="display:inline-block;background:rgb(0,164,220);color:rgba(255,255,255,0.87);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:#242424;background-color:#242424;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#242424;background-color:#242424;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:14px;font-style:italic;line-height:1;text-align:left;color:rgb(153,153,153);">{{ 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>
|
||||
@@ -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 }}
|
||||
@@ -0,0 +1,8 @@
|
||||
[Unit]
|
||||
Description=A basic account management system for Jellyfin.
|
||||
|
||||
[Service]
|
||||
ExecStart={executable}
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,960 @@
|
||||
// Used for theme change animation
|
||||
function whichTransitionEvent() {
|
||||
let t;
|
||||
let el = document.createElement('fakeelement');
|
||||
let transitions = {
|
||||
'transition': 'transitionend',
|
||||
'OTransition': 'oTransitionEnd',
|
||||
'MozTransition': 'transitionend',
|
||||
'WebkitTransition': 'webkitTransitionEnd'
|
||||
};
|
||||
|
||||
for (t in transitions) {
|
||||
if (el.style[t] !== undefined) {
|
||||
return transitions[t];
|
||||
}
|
||||
}
|
||||
}
|
||||
var transitionEndEvent = whichTransitionEvent();
|
||||
|
||||
// Toggles between light and dark themes
|
||||
function toggleCSS() {
|
||||
let cssEl = document.querySelectorAll('link[rel="stylesheet"][type="text/css"]')[0];
|
||||
let href = "bs" + bsVersion;
|
||||
if (cssEl.href.includes(href + "-jf")) {
|
||||
href += ".css";
|
||||
} else {
|
||||
href += "-jf.css";
|
||||
}
|
||||
cssEl.href = href
|
||||
document.cookie = "css=" + href;
|
||||
}
|
||||
|
||||
// Toggles between light and dark themes, but runs animation if necessary (dependent on window width for performance)
|
||||
var buttonWidth = 0;
|
||||
function toggleCSSAnim(el) {
|
||||
let switchToColor = window.getComputedStyle(document.body, null).backgroundColor;
|
||||
let maxWidth = 1500;
|
||||
if (window.innerWidth < maxWidth) {
|
||||
// Calculate minimum radius to cover whole screen
|
||||
let radius = Math.sqrt(Math.pow(window.innerWidth, 2) + Math.pow(window.innerHeight, 2));
|
||||
let currentRadius = el.getBoundingClientRect().width / 2;
|
||||
let scale = radius / currentRadius;
|
||||
buttonWidth = window.getComputedStyle(el, null).width;
|
||||
document.body.classList.remove('smooth-transition');
|
||||
el.style.transform = `scale(${scale})`;
|
||||
el.style.color = switchToColor;
|
||||
el.addEventListener(transitionEndEvent, function() {
|
||||
if (this.style.transform.length != 0) {
|
||||
toggleCSS();
|
||||
this.style.removeProperty('transform');
|
||||
document.body.classList.add('smooth-transition');
|
||||
};
|
||||
}, false);
|
||||
} else {
|
||||
toggleCSS();
|
||||
el.style.color = switchToColor;
|
||||
}
|
||||
}
|
||||
|
||||
var buttonColor = "custom";
|
||||
// Predefined colors for 'theme' button
|
||||
if (cssFile.includes("jf")) {
|
||||
buttonColor = "rgb(255,255,255)";
|
||||
} else if (cssFile == ('bs' + bsVersion + '.css')) {
|
||||
buttonColor = "rgb(16,16,16)";
|
||||
}
|
||||
|
||||
if (buttonColor != "custom") {
|
||||
let fakeButton = document.createElement('i');
|
||||
fakeButton.classList.add('fa', 'fa-circle', 'circle');
|
||||
fakeButton.style = `color: ${buttonColor}; margin-left: 0.4rem;`;
|
||||
fakeButton.id = "fakeButton";
|
||||
let switchButton = document.createElement('button');
|
||||
switchButton.classList.add('btn', 'btn-secondary');
|
||||
switchButton.textContent = "Theme";
|
||||
switchButton.onclick = function() {
|
||||
let fakeButton = document.getElementById('fakeButton');
|
||||
toggleCSSAnim(fakeButton);
|
||||
};
|
||||
let group = document.getElementById('headerButtons');
|
||||
switchButton.appendChild(fakeButton);
|
||||
group.appendChild(switchButton);
|
||||
}
|
||||
|
||||
|
||||
var loginModal = createModal('login');
|
||||
var settingsModal = createModal('settingsMenu');
|
||||
var userDefaultsModal = createModal('userDefaults');
|
||||
var usersModal = createModal('users');
|
||||
var restartModal = createModal('restartModal');
|
||||
var refreshModal = createModal('refreshModal');
|
||||
|
||||
// Parsed invite: [<code>, <expires in _>, <1: Empty invite (no delete/link), 0: Actual invite>, <email address>, <remaining uses>, [<used-by>], <date created>, <notify on expiry>, <notify on creation>]
|
||||
function parseInvite(invite, empty = false) {
|
||||
if (empty) {
|
||||
return ["None", "", 1];
|
||||
}
|
||||
let i = [invite["code"], "", 0, invite["email"]];
|
||||
let time = ""
|
||||
for (m of ["days", "hours", "minutes"]) {
|
||||
if (invite[m] != 0) {
|
||||
time += `${invite[m]}${m[0]} `;
|
||||
}
|
||||
}
|
||||
i[1] = `Expires in ${time.slice(0, -1)}`;
|
||||
if ('remaining-uses' in invite) {
|
||||
i[4] = invite['remaining-uses'];
|
||||
}
|
||||
if (invite['no-limit']) {
|
||||
i[4] = '∞';
|
||||
}
|
||||
if ('used-by' in invite) {
|
||||
i[5] = invite['used-by'];
|
||||
} else {
|
||||
i[5] = [];
|
||||
}
|
||||
if ('created' in invite) {
|
||||
i[6] = invite['created'];
|
||||
}
|
||||
if ('notify-expiry' in invite) {
|
||||
i[7] = invite['notify-expiry'];
|
||||
}
|
||||
if ('notify-creation' in invite) {
|
||||
i[8] = invite['notify-creation'];
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
function addItem(parsedInvite) {
|
||||
let links = document.getElementById('invites');
|
||||
let itemContainer = document.createElement('div');
|
||||
itemContainer.id = parsedInvite[0];
|
||||
let listItem = document.createElement('div');
|
||||
// listItem.id = parsedInvite[0];
|
||||
listItem.classList.add('list-group-item', 'd-flex', 'justify-content-between', 'd-inline-block');
|
||||
|
||||
let code = document.createElement('div');
|
||||
code.classList.add('d-flex', 'align-items-center', 'font-monospace');
|
||||
let codeLink = document.createElement('a');
|
||||
codeLink.setAttribute('style', 'margin-right: 0.5rem;');
|
||||
codeLink.textContent = parsedInvite[0].replace(/-/g, '-');
|
||||
|
||||
code.appendChild(codeLink);
|
||||
|
||||
listItem.appendChild(code);
|
||||
|
||||
let listRight = document.createElement('div');
|
||||
let listText = document.createElement('span');
|
||||
listText.id = parsedInvite[0] + '_expiry';
|
||||
listText.setAttribute('style', 'margin-right: 1rem;');
|
||||
listText.textContent = parsedInvite[1];
|
||||
|
||||
listRight.appendChild(listText);
|
||||
|
||||
if (parsedInvite[2] == 0) {
|
||||
let inviteCode = window.location.href.split('#')[0] + 'invite/' + parsedInvite[0];
|
||||
//
|
||||
codeLink.href = inviteCode;
|
||||
let copyButton = document.createElement('i');
|
||||
copyButton.onclick = function() { toClipboard(inviteCode); };
|
||||
copyButton.classList.add('fa', 'fa-clipboard', 'icon-button');
|
||||
|
||||
code.appendChild(copyButton);
|
||||
|
||||
if (parsedInvite[3] !== undefined) {
|
||||
let sentTo = document.createElement('span');
|
||||
sentTo.classList.add('text-muted');
|
||||
sentTo.setAttribute('style', 'margin-left: 0.4rem; font-style: italic, font-size: 0.75rem;');
|
||||
if (!parsedInvite[3].includes('Failed to send to')) {
|
||||
sentTo.textContent = "Sent to ";
|
||||
}
|
||||
sentTo.textContent += parsedInvite[3];
|
||||
|
||||
code.appendChild(sentTo);
|
||||
}
|
||||
|
||||
let deleteButton = document.createElement('button');
|
||||
deleteButton.onclick = function() { deleteInvite(parsedInvite[0]); };
|
||||
deleteButton.classList.add('btn', 'btn-outline-danger');
|
||||
deleteButton.textContent = "Delete";
|
||||
|
||||
listRight.appendChild(deleteButton);
|
||||
let dropButton = document.createElement('i');
|
||||
dropButton.classList.add('fa', 'fa-angle-down', 'collapsed', 'icon-button', 'not-rotated');
|
||||
dropButton.setAttribute('data-toggle', 'collapse');
|
||||
dropButton.setAttribute('aria-expanded', 'false');
|
||||
dropButton.setAttribute('data-target', '#' + CSS.escape(parsedInvite[0]) + '_collapse');
|
||||
dropButton.onclick = function() {
|
||||
if (this.classList.contains('rotated')) {
|
||||
this.classList.remove('rotated');
|
||||
this.classList.add('not-rotated');
|
||||
} else {
|
||||
this.classList.remove('not-rotated');
|
||||
this.classList.add('rotated');
|
||||
}
|
||||
};
|
||||
dropButton.setAttribute('style', 'margin-left: 1rem;');
|
||||
listRight.appendChild(dropButton);
|
||||
}
|
||||
|
||||
listItem.appendChild(listRight);
|
||||
itemContainer.appendChild(listItem);
|
||||
if (parsedInvite[2] == 0) {
|
||||
let itemDropdown = document.createElement('div');
|
||||
itemDropdown.id = parsedInvite[0] + '_collapse';
|
||||
itemDropdown.classList.add('collapse');
|
||||
|
||||
let dropdownContent = document.createElement('div');
|
||||
dropdownContent.classList.add('container', 'row', 'align-items-start', 'card-body');
|
||||
|
||||
let dropdownLeft = document.createElement('div');
|
||||
dropdownLeft.classList.add('col');
|
||||
|
||||
let leftList = document.createElement('ul');
|
||||
leftList.classList.add('list-group', 'list-group-flush');
|
||||
|
||||
if (typeof(parsedInvite[6]) != 'undefined') {
|
||||
let createdDate = document.createElement('li');
|
||||
createdDate.classList.add('list-group-item', 'py-1');
|
||||
createdDate.textContent = `Created: ${parsedInvite[6]}`;
|
||||
leftList.appendChild(createdDate);
|
||||
}
|
||||
|
||||
let remainingUses = document.createElement('li');
|
||||
remainingUses.classList.add('list-group-item', 'py-1');
|
||||
remainingUses.id = parsedInvite[0] + '_remainingUses';
|
||||
remainingUses.textContent = `Remaining uses: ${parsedInvite[4]}`;
|
||||
leftList.appendChild(remainingUses);
|
||||
|
||||
dropdownLeft.appendChild(leftList);
|
||||
dropdownContent.appendChild(dropdownLeft);
|
||||
|
||||
if (notifications_enabled) {
|
||||
let dropdownMiddle = document.createElement('div');
|
||||
dropdownMiddle.id = parsedInvite[0] + '_notifyButtons';
|
||||
dropdownMiddle.classList.add('col');
|
||||
|
||||
let middleList = document.createElement('ul');
|
||||
middleList.classList.add('list-group', 'list-group-flush');
|
||||
middleList.textContent = 'Notify on:';
|
||||
|
||||
let notifyExpiry = document.createElement('li');
|
||||
notifyExpiry.classList.add('list-group-item', 'py-1', 'form-check');
|
||||
notifyExpiry.innerHTML = `
|
||||
<input class="form-check-input" type="checkbox" value="" id="${parsedInvite[0]}_notifyExpiry">
|
||||
<label class="form-check-label" for="${parsedInvite[0]}_notifyExpiry">Expiry</label>
|
||||
`;
|
||||
if (typeof(parsedInvite[7]) == 'boolean') {
|
||||
notifyExpiry.getElementsByTagName('input')[0].checked = parsedInvite[7];
|
||||
}
|
||||
|
||||
notifyExpiry.getElementsByTagName('input')[0].onclick = function() {
|
||||
let req = new XMLHttpRequest();
|
||||
var thisEl = this;
|
||||
let send = {};
|
||||
let code = thisEl.id.replace('_notifyExpiry', '');
|
||||
send[code] = {};
|
||||
send[code]['notify-expiry'] = thisEl.checked;
|
||||
req.open("POST", "/setNotify", true);
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status != 200) {
|
||||
thisEl.checked = !thisEl.checked;
|
||||
}
|
||||
};
|
||||
req.send(JSON.stringify(send));
|
||||
};
|
||||
middleList.appendChild(notifyExpiry);
|
||||
|
||||
let notifyCreation = document.createElement('li');
|
||||
notifyCreation.classList.add('list-group-item', 'py-1', 'form-check');
|
||||
notifyCreation.innerHTML = `
|
||||
<input class="form-check-input" type="checkbox" value="" id="${parsedInvite[0]}_notifyCreation">
|
||||
<label class="form-check-label" for="${parsedInvite[0]}_notifyCreation">User creation</label>
|
||||
`;
|
||||
if (typeof(parsedInvite[8]) == 'boolean') {
|
||||
notifyCreation.getElementsByTagName('input')[0].checked = parsedInvite[8];
|
||||
}
|
||||
notifyCreation.getElementsByTagName('input')[0].onclick = function() {
|
||||
let req = new XMLHttpRequest();
|
||||
var thisEl = this;
|
||||
let send = {};
|
||||
let code = thisEl.id.replace('_notifyCreation', '');
|
||||
send[code] = {};
|
||||
send[code]['notify-creation'] = thisEl.checked;
|
||||
req.open("POST", "/setNotify", true);
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status != 200) {
|
||||
thisEl.checked = !thisEl.checked;
|
||||
}
|
||||
};
|
||||
req.send(JSON.stringify(send));
|
||||
};
|
||||
middleList.appendChild(notifyCreation);
|
||||
|
||||
dropdownMiddle.appendChild(middleList);
|
||||
dropdownContent.appendChild(dropdownMiddle);
|
||||
}
|
||||
|
||||
|
||||
let dropdownRight = document.createElement('div');
|
||||
dropdownRight.id = parsedInvite[0] + '_usersCreated';
|
||||
dropdownRight.classList.add('col');
|
||||
if (parsedInvite[5].length != 0) {
|
||||
let userList = document.createElement('ul');
|
||||
userList.classList.add('list-group', 'list-group-flush');
|
||||
userList.innerHTML = '<li class="list-group-item py-1">Users created:</li>';
|
||||
for (let user of parsedInvite[5]) {
|
||||
let li = document.createElement('li');
|
||||
li.classList.add('list-group-item', 'py-1', 'disabled');
|
||||
let username = document.createElement('div');
|
||||
username.classList.add('d-flex', 'float-left');
|
||||
username.textContent = user[0];
|
||||
li.appendChild(username);
|
||||
let date = document.createElement('div');
|
||||
date.classList.add('d-flex', 'float-right');
|
||||
date.textContent = user[1];
|
||||
li.appendChild(date);
|
||||
userList.appendChild(li);
|
||||
}
|
||||
dropdownRight.appendChild(userList);
|
||||
}
|
||||
dropdownContent.appendChild(dropdownRight);
|
||||
|
||||
itemDropdown.appendChild(dropdownContent);
|
||||
|
||||
itemContainer.appendChild(itemDropdown);
|
||||
}
|
||||
links.appendChild(itemContainer);
|
||||
}
|
||||
|
||||
function updateInvite(parsedInvite) {
|
||||
let expiry = document.getElementById(parsedInvite[0] + '_expiry');
|
||||
expiry.textContent = parsedInvite[1];
|
||||
|
||||
let remainingUses = document.getElementById(parsedInvite[0] + '_remainingUses');
|
||||
if (remainingUses) {
|
||||
remainingUses.textContent = `Remaining uses: ${parsedInvite[4]}`;
|
||||
}
|
||||
|
||||
if (parsedInvite[5].length != 0) {
|
||||
let usersCreated = document.getElementById(parsedInvite[0] + '_usersCreated');
|
||||
let dropdownRight = document.createElement('div');
|
||||
dropdownRight.id = parsedInvite[0] + '_usersCreated';
|
||||
dropdownRight.classList.add('col');
|
||||
let userList = document.createElement('ul');
|
||||
userList.classList.add('list-group', 'list-group-flush');
|
||||
userList.innerHTML = '<li class="list-group-item py-1">Users created:</li>';
|
||||
for (let user of parsedInvite[5]) {
|
||||
let li = document.createElement('li');
|
||||
li.classList.add('list-group-item', 'py-1', 'disabled');
|
||||
let username = document.createElement('div');
|
||||
username.classList.add('d-flex', 'float-left');
|
||||
username.textContent = user[0];
|
||||
li.appendChild(username);
|
||||
let date = document.createElement('div');
|
||||
date.classList.add('d-flex', 'float-right');
|
||||
date.textContent = user[1];
|
||||
li.appendChild(date);
|
||||
userList.appendChild(li);
|
||||
}
|
||||
dropdownRight.appendChild(userList);
|
||||
usersCreated.replaceWith(dropdownRight);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// delete from list on page
|
||||
function removeInvite(code) {
|
||||
let item = document.getElementById(code);
|
||||
item.parentNode.removeChild(item);
|
||||
}
|
||||
|
||||
function generateInvites(empty = false) {
|
||||
if (empty === false) {
|
||||
let req = new XMLHttpRequest();
|
||||
req.open("GET", "/getInvites", true);
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.responseType = 'json';
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
var data = this.response;
|
||||
if (data['invites'].length == 0) {
|
||||
document.getElementById('invites').textContent = '';
|
||||
addItem(parseInvite([], true));
|
||||
} else {
|
||||
for (let invite of data['invites']) {
|
||||
let match = false;
|
||||
let items = document.getElementById('invites').children;
|
||||
for (let item of items) {
|
||||
if (item.id == invite['code']) {
|
||||
match = true;
|
||||
updateInvite(parseInvite(invite));
|
||||
}
|
||||
}
|
||||
if (match == false) {
|
||||
addItem(parseInvite(invite));
|
||||
}
|
||||
}
|
||||
let items = document.getElementById('invites').children;
|
||||
for (let item of items) {
|
||||
var exists = false;
|
||||
for (let invite of data['invites']) {
|
||||
if (item.id == invite['code']) {
|
||||
exists = true;
|
||||
}
|
||||
}
|
||||
if (exists == false) {
|
||||
removeInvite(item.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
req.send();
|
||||
} else if (empty === true) {
|
||||
document.getElementById('invites').textContent = '';
|
||||
addItem(parseInvite([], true));
|
||||
}
|
||||
}
|
||||
|
||||
// actually delete invite
|
||||
function deleteInvite(code) {
|
||||
let send = JSON.stringify({ "code": code });
|
||||
let req = new XMLHttpRequest();
|
||||
req.open("POST", "/deleteInvite", true);
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
generateInvites();
|
||||
}
|
||||
};
|
||||
req.send(send);
|
||||
}
|
||||
|
||||
// Add numbers to select element
|
||||
function addOptions(length, selectElement) {
|
||||
for (let v = 0; v <= length; v++) {
|
||||
let opt = document.createElement('option');
|
||||
opt.textContent = v;
|
||||
opt.value = v;
|
||||
selectElement.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);
|
||||
}
|
||||
}
|
||||
|
||||
function fixCheckboxes() {
|
||||
let send_to_address = [document.getElementById('send_to_address'), document.getElementById('send_to_address_enabled')]
|
||||
if (send_to_address[0] != null) {
|
||||
send_to_address[0].disabled = !send_to_address[1].checked;
|
||||
}
|
||||
let multiUseEnabled = document.getElementById('multiUseEnabled');
|
||||
let multiUseCount = document.getElementById('multiUseCount');
|
||||
let noUseLimit = document.getElementById('noUseLimit');
|
||||
multiUseCount.disabled = !multiUseEnabled.checked;
|
||||
noUseLimit.checked = false;
|
||||
noUseLimit.disabled = !multiUseEnabled.checked;
|
||||
}
|
||||
|
||||
fixCheckboxes();
|
||||
|
||||
document.getElementById('inviteForm').onsubmit = function() {
|
||||
let 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...';
|
||||
send_object = serializeForm('inviteForm');
|
||||
if (!send_object['multiple-uses'] || send_object['no-limit']) {
|
||||
delete send_object['remaining-uses'];
|
||||
}
|
||||
if (document.getElementById('send_to_address') != null) {
|
||||
if (send_object['send_to_address_enabled']) {
|
||||
send_object['email'] = send_object['send_to_address'];
|
||||
delete send_object['send_to_address'];
|
||||
delete send_object['send_to_address_enabled'];
|
||||
}
|
||||
}
|
||||
let send = JSON.stringify(send_object);
|
||||
let req = new XMLHttpRequest();
|
||||
req.open("POST", "/generateInvite", true);
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
button.textContent = 'Generate';
|
||||
button.disabled = false;
|
||||
generateInvites();
|
||||
}
|
||||
};
|
||||
req.send(send);
|
||||
return false;
|
||||
};
|
||||
|
||||
document.getElementById('loginForm').onsubmit = function() {
|
||||
window.token = "";
|
||||
let details = serializeForm('loginForm');
|
||||
let errorArea = document.getElementById('loginErrorArea');
|
||||
errorArea.textContent = '';
|
||||
let 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...';
|
||||
let req = new XMLHttpRequest();
|
||||
req.responseType = 'json';
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
if (this.status == 401) {
|
||||
button.disabled = false;
|
||||
button.textContent = 'Login';
|
||||
let wrongPassword = document.createElement('div');
|
||||
wrongPassword.classList.add('alert', 'alert-danger');
|
||||
wrongPassword.setAttribute('role', 'alert');
|
||||
wrongPassword.textContent = "Incorrect username or password.";
|
||||
errorArea.appendChild(wrongPassword);
|
||||
} else {
|
||||
const data = this.response;
|
||||
window.token = data['token'];
|
||||
generateInvites();
|
||||
const interval = setInterval(function() { generateInvites(); }, 60 * 1000);
|
||||
let day = document.getElementById('days');
|
||||
addOptions(30, day);
|
||||
day.selected = "0";
|
||||
let hour = document.getElementById('hours');
|
||||
addOptions(24, hour);
|
||||
hour.selected = "0";
|
||||
let minutes = document.getElementById('minutes');
|
||||
addOptions(59, minutes);
|
||||
minutes.selected = "30";
|
||||
loginModal.hide();
|
||||
}
|
||||
}
|
||||
};
|
||||
req.open("GET", "/getToken", true);
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(details['username'] + ":" + details['password']));
|
||||
req.send();
|
||||
return false;
|
||||
};
|
||||
|
||||
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...';
|
||||
let req = new XMLHttpRequest();
|
||||
req.responseType = 'json';
|
||||
req.open("GET", "/getUsers", true);
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
if (this.status == 200) {
|
||||
let users = req.response['users'];
|
||||
let radioList = document.getElementById('defaultUserRadios');
|
||||
radioList.textContent = '';
|
||||
if (document.getElementById('setDefaultUser')) {
|
||||
document.getElementById('setDefaultUser').remove();
|
||||
}
|
||||
let first = true;
|
||||
for (user of users) {
|
||||
let radio = document.createElement('div');
|
||||
radio.classList.add('radio');
|
||||
let checked = 'checked';
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
checked = '';
|
||||
};
|
||||
radio.innerHTML =
|
||||
`<label><input type="radio" name="defaultRadios" id="default_${user['name']}" style="margin-right: 1rem;" ${checked}>${user['name']}</label>`;
|
||||
radioList.appendChild(radio);
|
||||
}
|
||||
let button = document.getElementById('openDefaultsWizard');
|
||||
button.disabled = false;
|
||||
button.innerHTML = 'Set new account defaults';
|
||||
let 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');
|
||||
}
|
||||
settingsModal.hide();
|
||||
userDefaultsModal.show();
|
||||
}
|
||||
}
|
||||
};
|
||||
req.send();
|
||||
};
|
||||
|
||||
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...';
|
||||
let button = document.getElementById('storeDefaults');
|
||||
let radios = document.getElementsByName('defaultRadios');
|
||||
for (let radio of radios) {
|
||||
if (radio.checked) {
|
||||
let data = {
|
||||
'username': radio.id.slice(8),
|
||||
'homescreen': false};
|
||||
if (document.getElementById('storeDefaultHomescreen').checked) {
|
||||
data['homescreen'] = true;
|
||||
}
|
||||
let req = new XMLHttpRequest();
|
||||
req.open("POST", "/setDefaults", true);
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
if (this.status == 200 || this.status == 204) {
|
||||
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() { userDefaultsModal.hide(); }, 1000);
|
||||
} else {
|
||||
button.textContent = "Failed";
|
||||
button.classList.remove('btn-primary');
|
||||
button.classList.add('btn-danger');
|
||||
setTimeout(function() {
|
||||
let button = document.getElementById('storeDefaults');
|
||||
button.textContent = "Submit";
|
||||
button.classList.remove('btn-danger');
|
||||
button.classList.add('btn-primary');
|
||||
button.disabled = false;
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
};
|
||||
req.send(JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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...';
|
||||
let req = new XMLHttpRequest();
|
||||
req.open("GET", "/getUsers", true);
|
||||
req.responseType = 'json';
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
if (this.status == 200) {
|
||||
let list = document.getElementById('userList');
|
||||
list.textContent = '';
|
||||
if (document.getElementById('saveUsers')) {
|
||||
document.getElementById('saveUsers').remove();
|
||||
}
|
||||
let users = req.response['users'];
|
||||
for (let user of users) {
|
||||
let entry = document.createElement('div');
|
||||
entry.classList.add('form-group', 'list-group-item', 'py-1');
|
||||
entry.id = 'user_' + user['name'];
|
||||
let label = document.createElement('label');
|
||||
label.classList.add('d-inline-block');
|
||||
label.setAttribute('for', 'address_' + user['email']);
|
||||
label.textContent = user['name'];
|
||||
entry.appendChild(label);
|
||||
let address = document.createElement('input');
|
||||
address.setAttribute('type', 'email');
|
||||
address.readOnly = true;
|
||||
address.classList.add('form-control-plaintext', 'text-muted', 'd-inline-block', 'addressText');
|
||||
address.id = 'address_' + user['email'];
|
||||
if (typeof(user['email']) != 'undefined') {
|
||||
address.value = user['email'];
|
||||
address.setAttribute('style', 'width: auto; margin-left: 2%;');
|
||||
}
|
||||
let editButton = document.createElement('i');
|
||||
editButton.classList.add('fa', 'fa-edit', 'd-inline-block', 'icon-button');
|
||||
editButton.setAttribute('style', 'margin-left: 2%;');
|
||||
editButton.onclick = function() {
|
||||
this.classList.remove('fa', 'fa-edit');
|
||||
let addressElement = this.parentNode.getElementsByClassName('form-control-plaintext')[0];
|
||||
addressElement.classList.remove('form-control-plaintext', 'text-muted');
|
||||
addressElement.classList.add('form-control');
|
||||
addressElement.readOnly = false;
|
||||
if (addressElement.value == '') {
|
||||
addressElement.placeholder = 'Email Address';
|
||||
address.setAttribute('style', 'width: auto; margin-left: 2%;');
|
||||
}
|
||||
if (document.getElementById('saveUsers') == null) {
|
||||
let footer = document.getElementById('userFooter')
|
||||
let saveUsers = document.createElement('input');
|
||||
saveUsers.classList.add('btn', 'btn-primary');
|
||||
saveUsers.setAttribute('type', 'button');
|
||||
saveUsers.value = 'Save Changes';
|
||||
saveUsers.id = 'saveUsers';
|
||||
saveUsers.onclick = function() {
|
||||
let send = {}
|
||||
let entries = document.getElementById('userList').children;
|
||||
for (let entry of entries) {
|
||||
if (typeof(entry.getElementsByTagName('input')[0]) != 'undefined') {
|
||||
const name = entry.id.replace(/user_/g, '');
|
||||
const address = entry.getElementsByTagName('input')[0].value;
|
||||
send[name] = address;
|
||||
}
|
||||
}
|
||||
send = JSON.stringify(send);
|
||||
let req = new XMLHttpRequest();
|
||||
req.open("POST", "/modifyUsers", true);
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
if (this.status == 200 || this.status == 204) {
|
||||
usersModal.hide();
|
||||
}
|
||||
}
|
||||
};
|
||||
req.send(send);
|
||||
};
|
||||
footer.appendChild(saveUsers);
|
||||
}
|
||||
};
|
||||
entry.appendChild(editButton);
|
||||
entry.appendChild(address);
|
||||
list.appendChild(entry);
|
||||
};
|
||||
let button = document.getElementById('openUsers');
|
||||
button.disabled = false;
|
||||
button.innerHTML = 'Users <i class="fa fa-user"></i>';
|
||||
settingsModal.hide();
|
||||
usersModal.show();
|
||||
}
|
||||
}
|
||||
};
|
||||
req.send();
|
||||
};
|
||||
|
||||
generateInvites(empty = true);
|
||||
loginModal.show();
|
||||
|
||||
var config = {};
|
||||
var modifiedConfig = {};
|
||||
|
||||
document.getElementById('openSettings').onclick = function () {
|
||||
let req = new XMLHttpRequest();
|
||||
req.open("GET", "/getConfig", true);
|
||||
req.responseType = 'json';
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
let settingsList = document.getElementById('settingsList');
|
||||
settingsList.textContent = '';
|
||||
config = this.response;
|
||||
for (let section of Object.keys(config)) {
|
||||
let sectionCollapse = document.createElement('div');
|
||||
sectionCollapse.classList.add('collapse');
|
||||
sectionCollapse.id = section;
|
||||
|
||||
let sectionTitle = config[section]['meta']['name'];
|
||||
let sectionDescription = config[section]['meta']['description'];
|
||||
let entryListID = section + '_entryList';
|
||||
let sectionFooter = section + '_footer';
|
||||
|
||||
let innerCollapse = `
|
||||
<div class="card card-body">
|
||||
<small class="text-muted">${sectionDescription}</small>
|
||||
<div class="${entryListID}">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
sectionCollapse.innerHTML = innerCollapse;
|
||||
|
||||
for (var entry of Object.keys(config[section])) {
|
||||
if (entry != 'meta') {
|
||||
let entryName = config[section][entry]['name'];
|
||||
let required = false;
|
||||
if (config[section][entry]['required']) {
|
||||
entryName += ' <sup class="text-danger">*</sup>';
|
||||
required = true;
|
||||
}
|
||||
if (config[section][entry]['requires_restart']) {
|
||||
entryName += ' <sup class="text-danger">R</sup>';
|
||||
}
|
||||
if (config[section][entry].hasOwnProperty('description')) {
|
||||
let tooltip = `
|
||||
<a class="text-muted" href="#" data-toggle="tooltip" data-placement="right" title="${config[section][entry]['description']}"><i class="fa fa-question-circle-o"></i></a>
|
||||
`;
|
||||
entryName += ' ';
|
||||
entryName += tooltip;
|
||||
};
|
||||
let entryValue = config[section][entry]['value'];
|
||||
let entryType = config[section][entry]['type'];
|
||||
let entryGroup = document.createElement('div');
|
||||
if (entryType == 'bool') {
|
||||
entryGroup.classList.add('form-check');
|
||||
if (entryValue.toString() == 'true') {
|
||||
var checked = true;
|
||||
} else {
|
||||
var checked = false;
|
||||
}
|
||||
entryGroup.innerHTML = `
|
||||
<input class="form-check-input" type="checkbox" value="" id="${section}_${entry}">
|
||||
<label class="form-check-label" for="${section}_${entry}">${entryName}</label>
|
||||
`;
|
||||
entryGroup.getElementsByClassName('form-check-input')[0].required = required;
|
||||
entryGroup.getElementsByClassName('form-check-input')[0].checked = checked;
|
||||
entryGroup.getElementsByClassName('form-check-input')[0].onclick = function() {
|
||||
var state = this.checked;
|
||||
for (var sect of Object.keys(config)) {
|
||||
for (var ent of Object.keys(config[sect])) {
|
||||
if ((sect + '_' + config[sect][ent]['depends_true']) == this.id) {
|
||||
document.getElementById(sect + '_' + ent).disabled = !state;
|
||||
} else if ((sect + '_' + config[sect][ent]['depends_false']) == this.id) {
|
||||
document.getElementById(sect + '_' + ent).disabled = state;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
} else if ((entryType == 'text') || (entryType == 'email') || (entryType == 'password') || (entryType == 'number')) {
|
||||
entryGroup.classList.add('form-group');
|
||||
entryGroup.innerHTML = `
|
||||
<label for="${section}_${entry}">${entryName}</label>
|
||||
<input type="${entryType}" class="form-control" id="${section}_${entry}" aria-describedby="${entry}" value="${entryValue}">
|
||||
`;
|
||||
entryGroup.getElementsByClassName('form-control')[0].required = required;
|
||||
} else if (entryType == 'select') {
|
||||
entryGroup.classList.add('form-group');
|
||||
let entryOptions = config[section][entry]['options'];
|
||||
let innerGroup = `
|
||||
<label for="${section}_${entry}">${entryName}</label>
|
||||
<select class="form-control" id="${section}_${entry}">
|
||||
`;
|
||||
for (let entryOption of entryOptions) {
|
||||
if (entryOption == entryValue) {
|
||||
var selected = 'selected';
|
||||
} else {
|
||||
var selected = '';
|
||||
}
|
||||
innerGroup += `
|
||||
<option value="${entryOption}" ${selected}>${entryOption}</option>
|
||||
`;
|
||||
}
|
||||
innerGroup += '</select>';
|
||||
entryGroup.innerHTML = innerGroup;
|
||||
entryGroup.getElementsByClassName('form-control')[0].required = required;
|
||||
|
||||
}
|
||||
sectionCollapse.getElementsByClassName(entryListID)[0].appendChild(entryGroup);
|
||||
}
|
||||
}
|
||||
let sectionButton = document.createElement('button');
|
||||
sectionButton.setAttribute('type', 'button');
|
||||
sectionButton.classList.add('list-group-item', 'list-group-item-action');
|
||||
sectionButton.appendChild(document.createTextNode(sectionTitle));
|
||||
sectionButton.id = section + '_button';
|
||||
sectionButton.setAttribute('data-toggle', 'collapse');
|
||||
sectionButton.setAttribute('data-target', '#' + section);
|
||||
settingsList.appendChild(sectionButton);
|
||||
settingsList.appendChild(sectionCollapse);
|
||||
}
|
||||
}
|
||||
};
|
||||
req.send();
|
||||
settingsModal.show();
|
||||
}
|
||||
|
||||
triggerTooltips();
|
||||
|
||||
function sendConfig(modalId, restart = false) {
|
||||
let modal = document.getElementById(modalId);
|
||||
modifiedConfig['restart-program'] = false;
|
||||
if (restart) {
|
||||
modifiedConfig['restart-program'] = true;
|
||||
}
|
||||
let send = JSON.stringify(modifiedConfig);
|
||||
let req = new XMLHttpRequest();
|
||||
req.open("POST", "/modifyConfig", true);
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
if (this.status == 200 || this.status == 204) {
|
||||
createModal(modalId, true).hide();
|
||||
if (modalId != 'settingsMenu') {
|
||||
settingsModal.hide();
|
||||
}
|
||||
} else if (restart) {
|
||||
refreshModal.show();
|
||||
}
|
||||
}
|
||||
};
|
||||
req.send(send);
|
||||
}
|
||||
|
||||
document.getElementById('settingsSave').onclick = function() {
|
||||
modifiedConfig = {};
|
||||
var restart_setting_changed = false;
|
||||
var settings_changed = false;
|
||||
|
||||
for (let section of Object.keys(config)) {
|
||||
for (let entry of Object.keys(config[section])) {
|
||||
if (entry != 'meta') {
|
||||
let entryID = section + '_' + entry;
|
||||
let el = document.getElementById(entryID);
|
||||
if (el.type == 'checkbox') {
|
||||
var value = el.checked.toString();
|
||||
} else {
|
||||
var value = el.value.toString();
|
||||
}
|
||||
if (value != config[section][entry]['value'].toString()) {
|
||||
if (!modifiedConfig.hasOwnProperty(section)) {
|
||||
modifiedConfig[section] = {};
|
||||
}
|
||||
modifiedConfig[section][entry] = value;
|
||||
settings_changed = true;
|
||||
if (config[section][entry]['requires_restart']) {
|
||||
restart_setting_changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (restart_setting_changed) {
|
||||
document.getElementById('applyRestarts').onclick = function(){ sendConfig('restartModal'); };
|
||||
document.getElementById('applyAndRestart').onclick = function(){ sendConfig('restartModal', restart=true); };
|
||||
settingsModal.hide();
|
||||
restartModal.show();
|
||||
} else if (settings_changed) {
|
||||
sendConfig('settingsMenu');
|
||||
} else {
|
||||
settingsModal.hide();
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
@@ -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>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
@@ -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 |
@@ -0,0 +1,32 @@
|
||||
function serializeForm(id) {
|
||||
var form = document.getElementById(id);
|
||||
var formData = {};
|
||||
for (var i = 0; i < form.elements.length; i++) {
|
||||
var el = form.elements[i];
|
||||
if (el.type != 'submit') {
|
||||
var name = el.name;
|
||||
if (name == '') {
|
||||
name = el.id;
|
||||
};
|
||||
switch (el.type) {
|
||||
case 'checkbox':
|
||||
formData[name] = el.checked;
|
||||
break;
|
||||
case 'text':
|
||||
case 'password':
|
||||
case 'email':
|
||||
case 'number':
|
||||
formData[name] = el.value;
|
||||
break;
|
||||
case 'select-one':
|
||||
let val = el.value;
|
||||
if (!isNaN(val)) {
|
||||
val = parseInt(val)
|
||||
}
|
||||
formData[name] = val;
|
||||
break;
|
||||
};
|
||||
};
|
||||
};
|
||||
return formData;
|
||||
};
|
||||
@@ -0,0 +1,242 @@
|
||||
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('emailCommonArea').style.display = '';
|
||||
document.getElementById('emailSMTPArea').style.display = '';
|
||||
document.getElementById('emailMailgunArea').style.display = 'none';
|
||||
} else if (document.getElementById('emailMailgunRadio').checked) {
|
||||
document.getElementById('emailCommonArea').style.display = '';
|
||||
document.getElementById('emailSMTPArea').style.display = 'none';
|
||||
document.getElementById('emailMailgunArea').style.display = '';
|
||||
} else if (document.getElementById('emailDisabledRadio').checked) {
|
||||
document.getElementById('emailCommonArea').style.display = 'none';
|
||||
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;
|
||||
var req = new XMLHttpRequest();
|
||||
req.open("POST", "/testJF", true);
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.responseType = 'json';
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
testButton.disabled = false;
|
||||
testButton.className = '';
|
||||
if (this.response['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';
|
||||
};
|
||||
};
|
||||
};
|
||||
req.send(JSON.stringify(jfData));
|
||||
};
|
||||
|
||||
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'] = {};
|
||||
config['notifications'] = {};
|
||||
// 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;
|
||||
config['ui']['email'] = document.getElementById('manualAuthEmail').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;
|
||||
};
|
||||
config['notifications']['enabled'] = document.getElementById('notificationsEnabled').checked.toString();
|
||||
// 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;
|
||||
// Send it
|
||||
var req = new XMLHttpRequest();
|
||||
req.open("POST", "/modifyConfig", true);
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.responseType = 'json';
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
submitButton.disabled = false;
|
||||
submitButton.className = '';
|
||||
submitButton.classList.add('btn', 'btn-success');
|
||||
submitButton.textContent = 'Success';
|
||||
};
|
||||
};
|
||||
req.send(JSON.stringify(config));
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
<!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 -->
|
||||
|
||||
<script>
|
||||
// To grab theme preference
|
||||
function getCookie(cname) {
|
||||
let name = cname + "=";
|
||||
let decodedCookie = decodeURIComponent(document.cookie);
|
||||
let ca = decodedCookie.split(';');
|
||||
for (let c of ca) {
|
||||
while(c.charAt(0) == ' ') {
|
||||
c = c.substring(1);
|
||||
}
|
||||
if (c.indexOf(name) == 0) {
|
||||
return c.substring(name.length, c.length);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
{{ if .bs5 }}
|
||||
const bsVersion = 5;
|
||||
{{ else }}
|
||||
const bsVersion = 4;
|
||||
{{ end }}
|
||||
const cssFile = "{{ .cssFile }}";
|
||||
var css = document.createElement('link');
|
||||
css.setAttribute('rel', 'stylesheet');
|
||||
css.setAttribute('type', 'text/css');
|
||||
var cssCookie = getCookie("css");
|
||||
if (cssCookie.includes('bs' + bsVersion)) {
|
||||
css.setAttribute('href', cssCookie);
|
||||
} else {
|
||||
css.setAttribute('href', cssFile);
|
||||
};
|
||||
document.head.appendChild(css);
|
||||
</script>
|
||||
{{ if not .bs5 }}
|
||||
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
|
||||
{{ end }}
|
||||
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
|
||||
{{ if .bs5 }}
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/js/bootstrap.min.js" integrity="sha384-oesi62hOLfzrys4LxRF63OJCXdXDipiYWBnvTl9Y9/TRlw5xlKIEHpNyvvDShgf/" crossorigin="anonymous"></script>
|
||||
{{ else }}
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
|
||||
{{ end }}
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<style>
|
||||
.pageContainer {
|
||||
margin: 5% 20% 5% 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;
|
||||
}
|
||||
.circle {
|
||||
/*margin-left: 1rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 50%;
|
||||
z-index: 5000;*/
|
||||
-webkit-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||
-moz-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||
-o-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||
transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190); /* easeInCubic */
|
||||
}
|
||||
.smooth-transition {
|
||||
-webkit-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||
-moz-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||
-o-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||
transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190); /* easeincubic */
|
||||
}
|
||||
.rotated {
|
||||
transform: rotate(180deg);
|
||||
-webkit-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||
-moz-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||
-o-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||
transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000); /* easeInOutQuart */
|
||||
}
|
||||
|
||||
.not-rotated {
|
||||
transform: rotate(0deg);
|
||||
-webkit-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||
-moz-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||
-o-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||
transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000); /* easeInOutQuart */
|
||||
}
|
||||
</style>
|
||||
<title>Admin</title>
|
||||
</head>
|
||||
<body class="smooth-transition">
|
||||
<div class="modal fade" id="login" 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" 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">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ul class="list-group list-group-flush">
|
||||
<p>Note: <sup class="text-danger">*</sup> Indicates required field, <sup class="text-danger">R</sup> Indicates changes require a restart.</p>
|
||||
<button type="button" class="list-group-item list-group-item-action" id="openUsers">
|
||||
Users <i class="fa fa-user"></i>
|
||||
</button>
|
||||
<button type="button" class="list-group-item list-group-item-action" id="openDefaultsWizard">
|
||||
New account defaults
|
||||
</button>
|
||||
</ul>
|
||||
<div class="list-group list-group-flush" id="settingsList">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer" id="settingsFooter">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" id="settingsSave">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="users" 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">×</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" 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">×</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="modal fade" id="restartModal" role="dialog" aria-labelledby="Restart Warning" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Warning</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>A restart is needed to apply some settings. Restart now, later, or cancel?</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-secondary" id="applyRestarts" data-dismiss="modal">Apply, Restart later</button>
|
||||
<button type="button" class="btn btn-primary" id="applyAndRestart" data-dismiss="modal">Apply & Restart</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="refreshModal" role="dialog" aria-labelledby="Refresh page notice" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Settings applied.</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Refresh the page in a few seconds.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pageContainer">
|
||||
<h1>
|
||||
Accounts admin
|
||||
</h1>
|
||||
<div class="btn-group" role="group" id="headerButtons">
|
||||
<button type="button" class="btn btn-primary" id="openSettings">
|
||||
Settings <i class="fa fa-cog"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card 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 mb-3">
|
||||
<div class="card-header">Generate Invite</div>
|
||||
<div class="card-body">
|
||||
<form action="#" method="POST" id="inviteForm" class="container">
|
||||
<div class="row align-items-start">
|
||||
<div class="col">
|
||||
<div class="form-group">
|
||||
<label for="days">Days</label>
|
||||
<select class="form-control form-select" id="days" name="days">
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="hours">Hours</label>
|
||||
<select class="form-control form-select" id="hours" name="hours">
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="minutes">Minutes</label>
|
||||
<select class="form-control form-select" id="minutes" name="minutes">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="form-group">
|
||||
<label for="multiUseCount">
|
||||
Multiple uses
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-text">
|
||||
<input class="form-check-input" type="checkbox" onchange="document.getElementById('multiUseCount').disabled = !this.checked; document.getElementById('noUseLimit').disabled = !this.checked" aria-label="Checkbox to allow choice of invite usage limit" name="multiple-uses" id="multiUseEnabled">
|
||||
</div>
|
||||
<input type="number" class="form-control" name="remaining-uses" id="multiUseCount">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-check" style="margin-top: 1rem; margin-bottom: 1rem;">
|
||||
<input class="form-check-input" type="checkbox" value="" name="no-limit" id="noUseLimit" onchange="document.getElementById('multiUseCount').disabled = this.checked; if (this.checked) { document.getElementById('noLimitWarning').style = 'display: block;' } else { document.getElementById('noLimitWarning').style = 'display: none;'; }">
|
||||
<label class="form-check-label" for="noUseLimit">
|
||||
No use limit
|
||||
</label>
|
||||
<div id="noLimitWarning" class="form-text" style="display: none;">Warning: Unlimited usage invites pose a risk if published online.</div>
|
||||
</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-text">
|
||||
<input class="form-check-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>
|
||||
<input type="email" class="form-control" placeholder="example@example.com" id="send_to_address" disabled>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="form-group d-flex float-right">
|
||||
<button type="submit" id="generateSubmit" class="btn btn-primary" style="margin-top: 1rem;">
|
||||
Generate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="contactBox">
|
||||
<p>{{ .contactMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<script src="serialize.js"></script>
|
||||
<script>
|
||||
{{ if .bs5 }}
|
||||
function createModal(id, find = false) {
|
||||
if (find) {
|
||||
return bootstrap.Modal.getInstance(document.getElementById(id));
|
||||
}
|
||||
return new bootstrap.Modal(document.getElementById(id));
|
||||
}
|
||||
{{ else }}
|
||||
let send_to_addess_enabled = document.getElementById('send_to_address_enabled');
|
||||
if (typeof(send_to_address_enabled) != 'undefined') {
|
||||
send_to_address_enabled.classList.remove('form-check-input');
|
||||
}
|
||||
let multiUseEnabled = document.getElementById('multiUseEnabled');
|
||||
if (typeof(multiUseEnabled) != 'undefined') {
|
||||
multiUseEnabled.classList.remove('form-check-input');
|
||||
}
|
||||
function createModal(id, find = false) {
|
||||
return {
|
||||
show: function() {
|
||||
return $('#' + id).modal('show');
|
||||
},
|
||||
hide: function() {
|
||||
return $('#' + id).modal('hide');
|
||||
}
|
||||
};
|
||||
}
|
||||
{{ end }}
|
||||
|
||||
function triggerTooltips() {
|
||||
{{ if .bs5 }}
|
||||
document.getElementById('settingsMenu').addEventListener('shown.bs.modal', function() {
|
||||
{{ else }}
|
||||
$('#settingsMenu').on('shown.bs.modal', function() {
|
||||
{{ end }}
|
||||
// Hacky way to ensure anything dependent on checkbox state is disabled if necessary by just clicking them
|
||||
let checkboxes = document.getElementById('settingsMenu').querySelectorAll('input[type="checkbox"]');
|
||||
for (checkbox of checkboxes) {
|
||||
checkbox.click();
|
||||
checkbox.click();
|
||||
}
|
||||
let tooltips = [].slice.call(document.querySelectorAll('a[data-toggle="tooltip"]'));
|
||||
tooltips.map(function(el) {
|
||||
{{ if .bs5 }}
|
||||
return new bootstrap.Tooltip(el);
|
||||
{{ else }}
|
||||
return $(el).tooltip();
|
||||
{{ end }}
|
||||
});
|
||||
});
|
||||
}
|
||||
{{ if .notifications }}
|
||||
const notifications_enabled = true;
|
||||
{{ else }}
|
||||
const notifications_enabled = false;
|
||||
{{ end }}
|
||||
</script>
|
||||
<script src="admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||