mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-11-09 20:00:12 +00:00
Compare commits
8 Commits
7d84fdec96
...
35a0be6a2c
Author | SHA1 | Date | |
---|---|---|---|
35a0be6a2c | |||
6b81358cd2 | |||
d4b94bc9d9 | |||
9213f2a078 | |||
2b84e45b65 | |||
7ac750879f | |||
cd61989495 | |||
a8b4842895 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,6 +4,7 @@ mail/*.html
|
||||
scss/*.css*
|
||||
scss/bs4/*.css*
|
||||
scss/bs5/*.css*
|
||||
data/static/*.css
|
||||
data/config-base.json
|
||||
data/config-default.ini
|
||||
data/*.html
|
||||
|
216
api.go
216
api.go
@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
@ -179,6 +180,58 @@ type newUserReq struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
func (app *appContext) NewUserAdmin(gc *gin.Context) {
|
||||
var req newUserReq
|
||||
gc.BindJSON(&req)
|
||||
existingUser, _, _ := app.jf.userByName(req.Username, false)
|
||||
if existingUser != nil {
|
||||
msg := fmt.Sprintf("User already exists named %s", req.Username)
|
||||
app.info.Printf("%s New user failed: %s", req.Username, msg)
|
||||
respond(401, msg, gc)
|
||||
return
|
||||
}
|
||||
user, status, err := app.jf.newUser(req.Username, req.Password)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
app.err.Printf("%s New user failed: Jellyfin responded with %d", req.Username, status)
|
||||
respond(401, "Unknown error", gc)
|
||||
return
|
||||
}
|
||||
var id string
|
||||
if user["Id"] != nil {
|
||||
id = user["Id"].(string)
|
||||
}
|
||||
if len(app.storage.policy) != 0 {
|
||||
status, err = app.jf.setPolicy(id, app.storage.policy)
|
||||
if !(status == 200 || status == 204) {
|
||||
app.err.Printf("%s: Failed to set user policy: Code %d", req.Username, status)
|
||||
}
|
||||
}
|
||||
if len(app.storage.configuration) != 0 && len(app.storage.displayprefs) != 0 {
|
||||
status, err = app.jf.setConfiguration(id, app.storage.configuration)
|
||||
if (status == 200 || status == 204) && err == nil {
|
||||
status, err = app.jf.setDisplayPreferences(id, app.storage.displayprefs)
|
||||
} else {
|
||||
app.err.Printf("%s: Failed to set configuration template: Code %d", req.Username, status)
|
||||
}
|
||||
}
|
||||
if app.config.Section("password_resets").Key("enabled").MustBool(false) {
|
||||
app.storage.emails[id] = req.Email
|
||||
app.storage.storeEmails()
|
||||
}
|
||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||
app.storage.loadOmbiTemplate()
|
||||
if len(app.storage.ombi_template) != 0 {
|
||||
errors, code, err := app.ombi.newUser(req.Username, req.Password, req.Email, app.storage.ombi_template)
|
||||
if err != nil || code != 200 {
|
||||
app.info.Printf("Failed to create Ombi user (%d): %s", code, err)
|
||||
app.debug.Printf("Errors reported by Ombi: %s", strings.Join(errors, ", "))
|
||||
} else {
|
||||
app.info.Println("Created Ombi user")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (app *appContext) NewUser(gc *gin.Context) {
|
||||
var req newUserReq
|
||||
gc.BindJSON(&req)
|
||||
@ -273,6 +326,51 @@ func (app *appContext) NewUser(gc *gin.Context) {
|
||||
gc.JSON(200, validation)
|
||||
}
|
||||
|
||||
type deleteUserReq struct {
|
||||
Users []string `json:"users"`
|
||||
Notify bool `json:"notify"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
func (app *appContext) DeleteUser(gc *gin.Context) {
|
||||
var req deleteUserReq
|
||||
gc.BindJSON(&req)
|
||||
errors := map[string]string{}
|
||||
for _, userID := range req.Users {
|
||||
status, err := app.jf.deleteUser(userID)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
errors[userID] = fmt.Sprintf("%d: %s", status, err)
|
||||
}
|
||||
if req.Notify {
|
||||
addr, ok := app.storage.emails[userID]
|
||||
if addr != nil && ok {
|
||||
go func(userID, reason, address string) {
|
||||
msg, err := app.email.constructDeleted(reason, app)
|
||||
if err != nil {
|
||||
app.err.Printf("%s: Failed to construct account deletion email", userID)
|
||||
app.debug.Printf("%s: Error: %s", userID, err)
|
||||
} else if err := app.email.send(address, msg); err != nil {
|
||||
app.err.Printf("%s: Failed to send to %s", userID, address)
|
||||
app.debug.Printf("%s: Error: %s", userID, err)
|
||||
} else {
|
||||
app.info.Printf("%s: Sent invite email to %s", userID, address)
|
||||
}
|
||||
}(userID, req.Reason, addr.(string))
|
||||
}
|
||||
}
|
||||
}
|
||||
app.jf.cacheExpiry = time.Now()
|
||||
if len(errors) == len(req.Users) {
|
||||
respond(500, "Failed", gc)
|
||||
app.err.Printf("Account deletion failed: %s", errors[req.Users[0]])
|
||||
return
|
||||
} else if len(errors) != 0 {
|
||||
gc.JSON(500, errors)
|
||||
return
|
||||
}
|
||||
gc.JSON(200, map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
type generateInviteReq struct {
|
||||
Days int `json:"days"`
|
||||
Hours int `json:"hours"`
|
||||
@ -458,13 +556,29 @@ func (app *appContext) DeleteInvite(gc *gin.Context) {
|
||||
respond(401, "Code doesn't exist", gc)
|
||||
}
|
||||
|
||||
type userResp struct {
|
||||
UserList []respUser `json:"users"`
|
||||
type dateToParse struct {
|
||||
Parsed time.Time `json:"parseme"`
|
||||
}
|
||||
|
||||
// json magically parses datetimes so why not
|
||||
func parseDt(date string) time.Time {
|
||||
timeJSON := []byte("{ \"parseme\": \"" + date + "\" }")
|
||||
var parsed dateToParse
|
||||
json.Unmarshal(timeJSON, &parsed)
|
||||
return parsed.Parsed
|
||||
}
|
||||
|
||||
type respUser struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email,omitempty"`
|
||||
// this magically parses a string to time.time
|
||||
LastActive string `json:"last_active"`
|
||||
Admin bool `json:"admin"`
|
||||
}
|
||||
|
||||
type userResp struct {
|
||||
UserList []respUser `json:"users"`
|
||||
}
|
||||
|
||||
func (app *appContext) GetUsers(gc *gin.Context) {
|
||||
@ -480,10 +594,18 @@ func (app *appContext) GetUsers(gc *gin.Context) {
|
||||
}
|
||||
for _, jfUser := range users {
|
||||
var user respUser
|
||||
user.LastActive = "n/a"
|
||||
if jfUser["LastActivityDate"] != nil {
|
||||
date := parseDt(jfUser["LastActivityDate"].(string))
|
||||
user.LastActive = app.formatDatetime(date)
|
||||
}
|
||||
user.ID = jfUser["Id"].(string)
|
||||
user.Name = jfUser["Name"].(string)
|
||||
user.Admin = jfUser["Policy"].(map[string]interface{})["IsAdministrator"].(bool)
|
||||
if email, ok := app.storage.emails[jfUser["Id"].(string)]; ok {
|
||||
user.Email = email.(string)
|
||||
}
|
||||
|
||||
resp.UserList = append(resp.UserList, user)
|
||||
}
|
||||
gc.JSON(200, resp)
|
||||
@ -516,6 +638,7 @@ func (app *appContext) OmbiUsers(gc *gin.Context) {
|
||||
func (app *appContext) ModifyEmails(gc *gin.Context) {
|
||||
var req map[string]string
|
||||
gc.BindJSON(&req)
|
||||
fmt.Println(req)
|
||||
app.debug.Println("Email modification requested")
|
||||
users, status, err := app.jf.getUsers(false)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
@ -525,7 +648,7 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
|
||||
return
|
||||
}
|
||||
for _, jfUser := range users {
|
||||
if address, ok := req[jfUser["Name"].(string)]; ok {
|
||||
if address, ok := req[jfUser["Id"].(string)]; ok {
|
||||
app.storage.emails[jfUser["Id"].(string)] = address
|
||||
}
|
||||
}
|
||||
@ -550,22 +673,24 @@ func (app *appContext) SetOmbiDefaults(gc *gin.Context) {
|
||||
}
|
||||
|
||||
type defaultsReq struct {
|
||||
Username string `json:"username"`
|
||||
Homescreen bool `json:"homescreen"`
|
||||
From string `json:"from"`
|
||||
ApplyTo []string `json:"apply_to"`
|
||||
ID string `json:"id"`
|
||||
Homescreen bool `json:"homescreen"`
|
||||
}
|
||||
|
||||
func (app *appContext) SetDefaults(gc *gin.Context) {
|
||||
var req defaultsReq
|
||||
gc.BindJSON(&req)
|
||||
app.info.Printf("Getting user defaults from \"%s\"", req.Username)
|
||||
user, status, err := app.jf.userByName(req.Username, false)
|
||||
userID := req.ID
|
||||
user, status, err := app.jf.userById(userID, false)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
app.err.Printf("Failed to get user from Jellyfin: Code %d", status)
|
||||
app.debug.Printf("Error: %s", err)
|
||||
respond(500, "Couldn't get user", gc)
|
||||
return
|
||||
}
|
||||
userID := user["Id"].(string)
|
||||
app.info.Printf("Getting user defaults from \"%s\"", user["Name"].(string))
|
||||
policy := user["Policy"].(map[string]interface{})
|
||||
app.storage.policy = policy
|
||||
app.storage.storePolicy()
|
||||
@ -590,6 +715,81 @@ func (app *appContext) SetDefaults(gc *gin.Context) {
|
||||
gc.JSON(200, map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
func (app *appContext) ApplySettings(gc *gin.Context) {
|
||||
var req defaultsReq
|
||||
gc.BindJSON(&req)
|
||||
applyingFrom := "template"
|
||||
var policy, configuration, displayprefs map[string]interface{}
|
||||
if req.From == "template" {
|
||||
if len(app.storage.policy) == 0 {
|
||||
respond(500, "No policy template available", gc)
|
||||
return
|
||||
}
|
||||
app.storage.loadPolicy()
|
||||
policy = app.storage.policy
|
||||
if req.Homescreen {
|
||||
if len(app.storage.configuration) == 0 || len(app.storage.displayprefs) == 0 {
|
||||
respond(500, "No homescreen template available", gc)
|
||||
return
|
||||
}
|
||||
configuration = app.storage.configuration
|
||||
displayprefs = app.storage.displayprefs
|
||||
}
|
||||
} else if req.From == "user" {
|
||||
applyingFrom = "user"
|
||||
user, status, err := app.jf.userById(req.ID, false)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
app.err.Printf("Failed to get user from Jellyfin: Code %d", status)
|
||||
app.debug.Printf("Error: %s", err)
|
||||
respond(500, "Couldn't get user", gc)
|
||||
return
|
||||
}
|
||||
applyingFrom = "\"" + user["Name"].(string) + "\""
|
||||
policy = user["Policy"].(map[string]interface{})
|
||||
if req.Homescreen {
|
||||
displayprefs, status, err = app.jf.getDisplayPreferences(req.ID)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
app.err.Printf("Failed to get DisplayPrefs: Code %d", status)
|
||||
app.debug.Printf("Error: %s", err)
|
||||
respond(500, "Couldn't get displayprefs", gc)
|
||||
return
|
||||
}
|
||||
configuration = user["Configuration"].(map[string]interface{})
|
||||
}
|
||||
}
|
||||
app.info.Printf("Applying settings to %d user(s) from %s", len(req.ApplyTo), applyingFrom)
|
||||
errors := map[string]map[string]string{
|
||||
"policy": map[string]string{},
|
||||
"homescreen": map[string]string{},
|
||||
}
|
||||
for _, id := range req.ApplyTo {
|
||||
status, err := app.jf.setPolicy(id, policy)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
errors["policy"][id] = fmt.Sprintf("%d: %s", status, err)
|
||||
}
|
||||
if req.Homescreen {
|
||||
status, err = app.jf.setConfiguration(id, configuration)
|
||||
errorString := ""
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
errorString += fmt.Sprintf("Configuration %d: %s ", status, err)
|
||||
} else {
|
||||
status, err = app.jf.setDisplayPreferences(id, displayprefs)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
errorString += fmt.Sprintf("Displayprefs %d: %s ", status, err)
|
||||
}
|
||||
}
|
||||
if errorString != "" {
|
||||
errors["homescreen"][id] = errorString
|
||||
}
|
||||
}
|
||||
}
|
||||
code := 200
|
||||
if len(errors["policy"]) == len(req.ApplyTo) || len(errors["homescreen"]) == len(req.ApplyTo) {
|
||||
code = 500
|
||||
}
|
||||
gc.JSON(code, errors)
|
||||
}
|
||||
|
||||
func (app *appContext) GetConfig(gc *gin.Context) {
|
||||
app.info.Println("Config requested")
|
||||
resp := map[string]interface{}{}
|
||||
|
@ -69,6 +69,9 @@ func (app *appContext) loadConfig() error {
|
||||
app.config.Section("notifications").Key("created_html").SetValue(app.config.Section("notifications").Key("created_html").MustString(filepath.Join(app.local_path, "created.html")))
|
||||
app.config.Section("notifications").Key("created_text").SetValue(app.config.Section("notifications").Key("created_text").MustString(filepath.Join(app.local_path, "created.txt")))
|
||||
|
||||
app.config.Section("deletion").Key("email_html").SetValue(app.config.Section("deletion").Key("email_html").MustString(filepath.Join(app.local_path, "deleted.html")))
|
||||
app.config.Section("deletion").Key("email_text").SetValue(app.config.Section("deletion").Key("email_text").MustString(filepath.Join(app.local_path, "deleted.txt")))
|
||||
|
||||
app.email = NewEmailer(app)
|
||||
|
||||
return nil
|
||||
|
@ -543,6 +543,36 @@
|
||||
"description": "API Key. Get this from the first tab in Ombi settings."
|
||||
}
|
||||
},
|
||||
"deletion": {
|
||||
"meta": {
|
||||
"name": "Account Deletion",
|
||||
"description": "Subject/email files for account deletion emails."
|
||||
},
|
||||
"subject": {
|
||||
"name": "Email subject",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "Your account was deleted - Jellyfin",
|
||||
"description": "Subject of account deletion emails."
|
||||
},
|
||||
"email_html": {
|
||||
"name": "Custom email (HTML)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to custom email html"
|
||||
},
|
||||
"email_text": {
|
||||
"name": "Custom email (plaintext)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to custom email in plain text"
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"meta": {
|
||||
"name": "File Storage",
|
||||
|
@ -10,6 +10,7 @@
|
||||
"mailgun",
|
||||
"smtp",
|
||||
"ombi",
|
||||
"deletion",
|
||||
"files"
|
||||
],
|
||||
"jellyfin": {
|
||||
@ -634,6 +635,41 @@
|
||||
"description": "API Key. Get this from the first tab in Ombi settings."
|
||||
}
|
||||
},
|
||||
"deletion": {
|
||||
"order": [
|
||||
"subject",
|
||||
"email_html",
|
||||
"email_text"
|
||||
],
|
||||
"meta": {
|
||||
"name": "Account Deletion",
|
||||
"description": "Subject/email files for account deletion emails."
|
||||
},
|
||||
"subject": {
|
||||
"name": "Email subject",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "Your account was deleted - Jellyfin",
|
||||
"description": "Subject of account deletion emails."
|
||||
},
|
||||
"email_html": {
|
||||
"name": "Custom email (HTML)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to custom email html"
|
||||
},
|
||||
"email_text": {
|
||||
"name": "Custom email (plaintext)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to custom email in plain text"
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"order": [
|
||||
"invites",
|
||||
|
363
data/static/accounts.js
Normal file
363
data/static/accounts.js
Normal file
@ -0,0 +1,363 @@
|
||||
document.getElementById('selectAll').onclick = function() {
|
||||
const checkboxes = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]');
|
||||
for (check of checkboxes) {
|
||||
check.checked = this.checked;
|
||||
}
|
||||
checkCheckboxes();
|
||||
};
|
||||
|
||||
function checkCheckboxes() {
|
||||
const defaultsButton = document.getElementById('accountsTabSetDefaults');
|
||||
const deleteButton = document.getElementById('accountsTabDelete');
|
||||
const checkboxes = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]');
|
||||
let checked = 0;
|
||||
for (check of checkboxes) {
|
||||
if (check.checked) {
|
||||
checked++;
|
||||
}
|
||||
}
|
||||
if (checked == 0) {
|
||||
defaultsButton.classList.add('unfocused');
|
||||
deleteButton.classList.add('unfocused');
|
||||
} else {
|
||||
if (defaultsButton.classList.contains('unfocused')) {
|
||||
defaultsButton.classList.remove('unfocused');
|
||||
}
|
||||
if (deleteButton.classList.contains('unfocused')) {
|
||||
deleteButton.classList.remove('unfocused');
|
||||
}
|
||||
if (checked == 1) {
|
||||
deleteButton.textContent = 'Delete User';
|
||||
} else {
|
||||
deleteButton.textContent = 'Delete Users';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('deleteModalNotify').onclick = function() {
|
||||
const textbox = document.getElementById('deleteModalReasonBox');
|
||||
if (this.checked && textbox.classList.contains('unfocused')) {
|
||||
textbox.classList.remove('unfocused');
|
||||
} else if (!this.checked) {
|
||||
textbox.classList.add('unfocused');
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('accountsTabDelete').onclick = function() {
|
||||
const deleteButton = this;
|
||||
let selected = [];
|
||||
const checkboxes = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]');
|
||||
for (check of checkboxes) {
|
||||
if (check.checked) {
|
||||
selected.push(check.id.replace('select_', ''));
|
||||
}
|
||||
}
|
||||
let title = " user";
|
||||
if (selected.length > 1) {
|
||||
title += "s";
|
||||
}
|
||||
title = "Delete " + selected.length + title;
|
||||
document.getElementById('deleteModalTitle').textContent = title;
|
||||
document.getElementById('deleteModalNotify').checked = false;
|
||||
document.getElementById('deleteModalReason').value = '';
|
||||
document.getElementById('deleteModalReasonBox').classList.add('unfocused');
|
||||
document.getElementById('deleteModalSend').textContent = 'Delete';
|
||||
|
||||
document.getElementById('deleteModalSend').onclick = function() {
|
||||
const button = this;
|
||||
const send = {
|
||||
'users': selected,
|
||||
'notify': document.getElementById('deleteModalNotify').checked,
|
||||
'reason': document.getElementById('deleteModalReason').value
|
||||
};
|
||||
let req = new XMLHttpRequest();
|
||||
req.open("POST", "/deleteUser", true);
|
||||
req.responseType = 'json';
|
||||
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 == 500) {
|
||||
if ("error" in req.response) {
|
||||
button.textContent = 'Failed';
|
||||
} else {
|
||||
button.textContent = 'Partial fail (check console)';
|
||||
console.log(req.response);
|
||||
}
|
||||
setTimeout(function() {
|
||||
deleteModal.hide();
|
||||
deleteButton.classList.add('unfocused');
|
||||
}, 4000);
|
||||
} else {
|
||||
deleteButton.classList.add('unfocused');
|
||||
deleteModal.hide();
|
||||
}
|
||||
populateUsers();
|
||||
checkCheckboxes();
|
||||
}
|
||||
};
|
||||
req.send(JSON.stringify(send));
|
||||
};
|
||||
deleteModal.show();
|
||||
}
|
||||
|
||||
var jfUsers = [];
|
||||
|
||||
function validEmail(email) {
|
||||
const re = /\S+@\S+\.\S+/;
|
||||
return re.test(email);
|
||||
}
|
||||
|
||||
function changeEmail(icon, id) {
|
||||
const iconContent = icon.outerHTML;
|
||||
icon.setAttribute("class", "");
|
||||
const entry = icon.nextElementSibling;
|
||||
const ogEmail = entry.value;
|
||||
entry.readOnly = false;
|
||||
entry.classList.remove('form-control-plaintext');
|
||||
entry.classList.add('form-control');
|
||||
if (entry.value == "") {
|
||||
entry.placeholder = 'Address';
|
||||
}
|
||||
const tick = document.createElement('i');
|
||||
tick.classList.add("fa", "fa-check", "d-inline-block", "icon-button", "text-success");
|
||||
tick.setAttribute('style', 'margin-left: 0.5rem; margin-right: 0.5rem;');
|
||||
tick.onclick = function() {
|
||||
const newEmail = entry.value;
|
||||
if (!validEmail(newEmail) || newEmail == ogEmail) {
|
||||
return
|
||||
}
|
||||
cross.remove();
|
||||
this.outerHTML = `
|
||||
<div class="spinner-border spinner-border-sm" role="status" style="width: 1rem; height: 1rem; margin-left: 0.5rem;">
|
||||
<span class="sr-only">Saving...</span>
|
||||
</div>`;
|
||||
//this.remove();
|
||||
let send = {};
|
||||
send[id] = newEmail;
|
||||
let req = new XMLHttpRequest();
|
||||
req.open("POST", "/modifyEmails", 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) {
|
||||
entry.nextElementSibling.remove();
|
||||
} else {
|
||||
entry.value = ogEmail;
|
||||
}
|
||||
}
|
||||
};
|
||||
req.send(JSON.stringify(send));
|
||||
icon.outerHTML = iconContent;
|
||||
entry.readOnly = true;
|
||||
entry.classList.remove('form-control');
|
||||
entry.classList.add('form-control-plaintext');
|
||||
entry.placeholder = '';
|
||||
};
|
||||
const cross = document.createElement('i');
|
||||
cross.classList.add("fa", "fa-close", "d-inline-block", "icon-button", "text-danger");
|
||||
cross.onclick = function() {
|
||||
tick.remove();
|
||||
this.remove();
|
||||
icon.outerHTML = iconContent;
|
||||
entry.readOnly = true;
|
||||
entry.classList.remove('form-control');
|
||||
entry.classList.add('form-control-plaintext');
|
||||
entry.placeholder = '';
|
||||
entry.value = ogEmail;
|
||||
};
|
||||
icon.parentNode.appendChild(tick);
|
||||
icon.parentNode.appendChild(cross);
|
||||
}
|
||||
|
||||
function populateUsers() {
|
||||
const acList = document.getElementById('accountsList');
|
||||
acList.innerHTML = `
|
||||
<div class="d-flex align-items-center">
|
||||
<strong>Getting Users...</strong>
|
||||
<div class="spinner-border ml-auto" role="status" aria-hidden="true"></div>
|
||||
</div>`;
|
||||
acList.parentNode.querySelector('thead').classList.add('unfocused');
|
||||
const accountsList = document.createElement('tbody');
|
||||
accountsList.id = 'accountsList';
|
||||
const generateEmail = function(id, name, email) {
|
||||
let entry = document.createElement('div');
|
||||
// entry.classList.add('py-1');
|
||||
entry.id = 'email_' + id;
|
||||
let emailValue = email;
|
||||
if (email === undefined) {
|
||||
emailValue = "";
|
||||
}
|
||||
entry.innerHTML = `
|
||||
<i class="fa fa-edit d-inline-block icon-button" style="margin-right: 2%;" onclick="changeEmail(this, '${id}')"></i>
|
||||
<input type="email" class="form-control-plaintext form-control-sm text-muted d-inline-block addressText" id="address_${id}" style="width: auto;" value="${emailValue}" readonly>
|
||||
`;
|
||||
return entry.outerHTML
|
||||
};
|
||||
const template = function(id, username, email, lastActive, admin) {
|
||||
let isAdmin = "No";
|
||||
if (admin) {
|
||||
isAdmin = "Yes";
|
||||
}
|
||||
return `
|
||||
<td nowrap="nowrap" class="align-middle" scope="row"><input class="form-check-input" type="checkbox" value="" id="select_${id}" onclick="checkCheckboxes();"></td>
|
||||
<td nowrap="nowrap" class="align-middle">${username}</td>
|
||||
<td nowrap="nowrap" class="align-middle">${generateEmail(id, name, email)}</td>
|
||||
<td nowrap="nowrap" class="align-middle">${lastActive}</td>
|
||||
<td nowrap="nowrap" class="align-middle">${isAdmin}</td>
|
||||
`;
|
||||
};
|
||||
|
||||
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) {
|
||||
jfUsers = req.response['users'];
|
||||
for (user of jfUsers) {
|
||||
let tr = document.createElement('tr');
|
||||
tr.innerHTML = template(user['id'], user['name'], user['email'], user['last_active'], user['admin']);
|
||||
accountsList.appendChild(tr);
|
||||
}
|
||||
const header = acList.parentNode.querySelector('thead');
|
||||
if (header.classList.contains('unfocused')) {
|
||||
header.classList.remove('unfocused');
|
||||
}
|
||||
acList.replaceWith(accountsList);
|
||||
}
|
||||
}
|
||||
};
|
||||
req.send();
|
||||
}
|
||||
|
||||
document.getElementById('selectAll').checked = false;
|
||||
|
||||
document.getElementById('accountsTabSetDefaults').onclick = function() {
|
||||
const checkboxes = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]');
|
||||
let userIDs = [];
|
||||
for (check of checkboxes) {
|
||||
if (check.checked) {
|
||||
userIDs.push(check.id.replace('select_', ''));
|
||||
}
|
||||
}
|
||||
if (userIDs.length == 0) {
|
||||
return;
|
||||
}
|
||||
let radioList = document.getElementById('defaultUserRadios');
|
||||
radioList.textContent = '';
|
||||
let first = true;
|
||||
for (user of jfUsers) {
|
||||
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['id']}" style="margin-right: 1rem;" ${checked}>${user['name']}</label>`;
|
||||
radioList.appendChild(radio);
|
||||
}
|
||||
let userstring = 'user';
|
||||
if (userIDs.length > 1) {
|
||||
userstring += 's';
|
||||
}
|
||||
document.getElementById('defaultsTitle').textContent = `Apply settings to ${userIDs.length} ${userstring}`;
|
||||
document.getElementById('userDefaultsDescription').textContent = `
|
||||
Create an account and configure it to your liking, then choose it from below to apply to your selected users.`;
|
||||
document.getElementById('storeHomescreenLabel').textContent = `Apply homescreen layout`;
|
||||
if (document.getElementById('defaultsSourceSection').classList.contains('unfocused')) {
|
||||
document.getElementById('defaultsSourceSection').classList.remove('unfocused');
|
||||
}
|
||||
document.getElementById('defaultsSource').value = 'userTemplate';
|
||||
document.getElementById('defaultUserRadios').classList.add('unfocused');
|
||||
document.getElementById('storeDefaults').onclick = function() {
|
||||
storeDefaults(userIDs);
|
||||
};
|
||||
userDefaultsModal.show();
|
||||
};
|
||||
|
||||
document.getElementById('defaultsSource').addEventListener('change', function() {
|
||||
const radios = document.getElementById('defaultUserRadios');
|
||||
if (this.value == 'userTemplate') {
|
||||
radios.classList.add('unfocused');
|
||||
} else if (radios.classList.contains('unfocused')) {
|
||||
radios.classList.remove('unfocused');
|
||||
}
|
||||
})
|
||||
|
||||
document.getElementById('newUserCreate').onclick = function() {
|
||||
const ogText = this.textContent;
|
||||
this.innerHTML = `
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Creating...`;
|
||||
const email = document.getElementById('newUserEmail').value;
|
||||
var username = email;
|
||||
if (document.getElementById('newUserName') != null) {
|
||||
username = document.getElementById('newUserName').value;
|
||||
}
|
||||
const password = document.getElementById('newUserPassword').value;
|
||||
if (!validEmail(email) && email != "") {
|
||||
return;
|
||||
}
|
||||
const send = {
|
||||
'username': username,
|
||||
'password': password,
|
||||
'email': email
|
||||
}
|
||||
let req = new XMLHttpRequest()
|
||||
req.open("POST", "/newUserAdmin", true);
|
||||
req.responseType = 'json';
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
const button = this;
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
button.textContent = ogText;
|
||||
if (this.status == 200) {
|
||||
if (button.classList.contains('btn-primary')) {
|
||||
button.classList.remove('btn-primary');
|
||||
}
|
||||
button.classList.add('btn-success');
|
||||
button.textContent = 'Success';
|
||||
setTimeout(function() {
|
||||
if (button.classList.contains('btn-success')) {
|
||||
button.classList.remove('btn-success');
|
||||
}
|
||||
button.classList.add('btn-primary');
|
||||
button.textContent = ogText;
|
||||
newUserModal.hide();
|
||||
}, 1000);
|
||||
} else {
|
||||
if (button.classList.contains('btn-primary')) {
|
||||
button.classList.remove('btn-primary');
|
||||
}
|
||||
button.classList.add('btn-danger');
|
||||
if ("error" in req.response) {
|
||||
button.textContent = req.response["error"];
|
||||
} else {
|
||||
button.textContent = 'Failed';
|
||||
}
|
||||
setTimeout(function() {
|
||||
if (button.classList.contains('btn-danger')) {
|
||||
button.classList.remove('btn-danger');
|
||||
}
|
||||
button.classList.add('btn-primary');
|
||||
button.textContent = ogText;
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
};
|
||||
req.send(JSON.stringify(send));
|
||||
}
|
||||
|
||||
document.getElementById('accountsTabAddUser').onclick = function() {
|
||||
document.getElementById('newUserEmail').value = '';
|
||||
document.getElementById('newUserPassword').value = '';
|
||||
if (document.getElementById('newUserName') != null) {
|
||||
document.getElementById('newUserName').value = '';
|
||||
}
|
||||
newUserModal.show();
|
||||
};
|
@ -1,3 +1,36 @@
|
||||
const tabs = {
|
||||
invitesEl: document.getElementById('invitesTab'),
|
||||
accountsEl: document.getElementById('accountsTab'),
|
||||
invitesTabButton: document.getElementById('invitesTabButton'),
|
||||
accountsTabButton: document.getElementById('accountsTabButton'),
|
||||
invites: function() {
|
||||
if (tabs.invitesEl.classList.contains('unfocused')) {
|
||||
tabs.accountsEl.classList.add('unfocused');
|
||||
tabs.invitesEl.classList.remove('unfocused');
|
||||
}
|
||||
if (tabs.accountsTabButton.classList.contains("active")) {
|
||||
tabs.accountsTabButton.classList.remove("active");
|
||||
}
|
||||
tabs.invitesTabButton.classList.add("active");
|
||||
},
|
||||
accounts: function() {
|
||||
populateUsers();
|
||||
if (tabs.accountsEl.classList.contains('unfocused')) {
|
||||
tabs.invitesEl.classList.add('unfocused');
|
||||
tabs.accountsEl.classList.remove('unfocused');
|
||||
}
|
||||
if (tabs.invitesTabButton.classList.contains("active")) {
|
||||
tabs.invitesTabButton.classList.remove("active");
|
||||
tabs.accountsTabButton.classList.add("active");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
tabs.invitesTabButton.onclick = tabs.invites;
|
||||
tabs.accountsTabButton.onclick = tabs.accounts;
|
||||
|
||||
tabs.invites();
|
||||
|
||||
// Used for theme change animation
|
||||
function whichTransitionEvent() {
|
||||
let t;
|
||||
@ -101,6 +134,8 @@ var usersModal = createModal('users');
|
||||
var restartModal = createModal('restartModal');
|
||||
var refreshModal = createModal('refreshModal');
|
||||
var aboutModal = createModal('aboutModal');
|
||||
var deleteModal = createModal('deleteModal');
|
||||
var newUserModal = createModal('newUserModal');
|
||||
|
||||
// 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) {
|
||||
@ -638,7 +673,7 @@ document.getElementById('openDefaultsWizard').onclick = function() {
|
||||
checked = '';
|
||||
}
|
||||
radio.innerHTML =
|
||||
`<label><input type="radio" name="defaultRadios" id="default_${user['name']}" style="margin-right: 1rem;" ${checked}>${user['name']}</label>`;
|
||||
`<label><input type="radio" name="defaultRadios" id="default_${user['id']}" style="margin-right: 1rem;" ${checked}>${user['name']}</label>`;
|
||||
radioList.appendChild(radio);
|
||||
}
|
||||
let button = document.getElementById('openDefaultsWizard');
|
||||
@ -655,6 +690,19 @@ document.getElementById('openDefaultsWizard').onclick = function() {
|
||||
submitButton.classList.add('btn-primary');
|
||||
}
|
||||
settingsModal.hide();
|
||||
document.getElementById('defaultsTitle').textContent = `New user defaults`;
|
||||
document.getElementById('userDefaultsDescription').textContent = `
|
||||
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.`;
|
||||
document.getElementById('storeHomescreenLabel').textContent = `Store homescreen layout`;
|
||||
document.getElementById('defaultsSource').value = 'fromUser';
|
||||
document.getElementById('defaultsSourceSection').classList.add('unfocused');
|
||||
document.getElementById('storeDefaults').onclick = function() {
|
||||
storeDefaults('all');
|
||||
};
|
||||
const list = document.getElementById('defaultUserRadios');
|
||||
if (list.classList.contains('unfocused')) {
|
||||
list.classList.remove('unfocused');
|
||||
}
|
||||
userDefaultsModal.show();
|
||||
}
|
||||
}
|
||||
@ -662,54 +710,81 @@ document.getElementById('openDefaultsWizard').onclick = function() {
|
||||
req.send();
|
||||
};
|
||||
|
||||
document.getElementById('storeDefaults').onclick = function () {
|
||||
function storeDefaults(users) {
|
||||
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');
|
||||
let id = '';
|
||||
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));
|
||||
id = radio.id.replace('default_', '');
|
||||
break;
|
||||
}
|
||||
}
|
||||
let route = '/setDefaults';
|
||||
let data = {
|
||||
'from': 'user',
|
||||
'id': id,
|
||||
'homescreen': false
|
||||
};
|
||||
if (document.getElementById('defaultsSource').value == 'userTemplate') {
|
||||
data['from'] = 'template';
|
||||
}
|
||||
if (users != 'all') {
|
||||
data['apply_to'] = users;
|
||||
route = '/applySettings';
|
||||
}
|
||||
if (document.getElementById('storeDefaultHomescreen').checked) {
|
||||
data['homescreen'] = true;
|
||||
}
|
||||
let req = new XMLHttpRequest();
|
||||
req.open("POST", route, true);
|
||||
req.responseType = 'json';
|
||||
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() {
|
||||
let button = document.getElementById('storeDefaults');
|
||||
button.textContent = "Submit";
|
||||
button.classList.remove('btn-success');
|
||||
button.classList.add('btn-primary');
|
||||
button.disabled = false;
|
||||
userDefaultsModal.hide();
|
||||
}, 1000);
|
||||
} else {
|
||||
if ("error" in req.response) {
|
||||
button.textContent = req.response["error"];
|
||||
} else if (("policy" in req.response) || ("homescreen" in req.response)) {
|
||||
button.textContent = "Failed (Check JS Console)";
|
||||
} 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));
|
||||
};
|
||||
|
||||
var ombiDefaultsModal = '';
|
||||
@ -813,104 +888,6 @@ if (ombiEnabled) {
|
||||
};
|
||||
}
|
||||
|
||||
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['name'];
|
||||
address.setAttribute('style', 'width: auto; margin-left: 2%;');
|
||||
if (typeof(user['email']) != 'undefined') {
|
||||
address.value = user['email'];
|
||||
}
|
||||
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);
|
||||
|
||||
tryLogin("", "", false, callback = function(code){
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -65,82 +65,6 @@
|
||||
<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 */
|
||||
}
|
||||
|
||||
.invite-link {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.settingIcon {
|
||||
margin-left: 0.2rem;
|
||||
}
|
||||
|
||||
body.modal-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
<title>Admin</title>
|
||||
</head>
|
||||
<body class="smooth-transition">
|
||||
@ -181,9 +105,6 @@
|
||||
<button type="button" class="list-group-item list-group-item-action" id="openAbout">
|
||||
About <i class="fa fa-info-circle settingIcon"></i>
|
||||
</button>
|
||||
<button type="button" class="list-group-item list-group-item-action" id="openUsers">
|
||||
Email Addresses <i class="fa fa-envelope settingIcon"></i>
|
||||
</button>
|
||||
<button type="button" class="list-group-item list-group-item-action" id="openDefaultsWizard">
|
||||
New User Defaults <i class="fa fa-user settingIcon"></i>
|
||||
</button>
|
||||
@ -226,16 +147,24 @@
|
||||
<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>
|
||||
<h5 class="modal-title" id="defaultsTitle"></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>
|
||||
<p id="userDefaultsDescription"></p>
|
||||
<div class="mb-3" id="defaultsSourceSection">
|
||||
<label for="defaultsSource">Use settings from:</label>
|
||||
<select class="form-select" id="defaultsSource" aria-label="User settings source">
|
||||
<option value="userTemplate" selected>Use existing user template</option>
|
||||
<option value="fromUser">Source from existing user</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="defaultUserRadios"></div>
|
||||
<div class="checkbox">
|
||||
<label><input type="checkbox" value="" style="margin-right: 1rem;" id="storeDefaultHomescreen" checked>Store homescreen layout</label>
|
||||
<input type="checkbox" value="" style="margin-right: 1rem;" id="storeDefaultHomescreen" checked>
|
||||
<label for="storeDefaultHomescreen" id="storeHomescreenLabel"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer" id="defaultsFooter">
|
||||
@ -278,8 +207,8 @@
|
||||
</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>
|
||||
<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>
|
||||
@ -315,10 +244,73 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="deleteModal" role="dialog" aria-labelledby="Account deletion" 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="deleteModalTitle"></h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="" id="deleteModalNotify">
|
||||
<label class="form-check-label" for="deleteModalNotify">Notify users of account deletion</label>
|
||||
</div>
|
||||
<div class="mb-3 unfocused" id="deleteModalReasonBox">
|
||||
<label for="deleteModalReason" class="form-label">Reason for deletion</label>
|
||||
<textarea class="form-control" id="deleteModalReason" rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" id="deleteModalSend">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="newUserModal" role="dialog" aria-labelledby="Create new user" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Create a user</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="newUserEmail" class="form-label">Email address</label>
|
||||
<input type="email" class="form-control" id="newUserEmail" aria-describedby="Email address">
|
||||
</div>
|
||||
{{ if .username }}
|
||||
<div class="mb-3">
|
||||
<label for="newUserName" class="form-label">Username</label>
|
||||
<input type="text" class="form-control" id="newUserName" aria-describedby="Username">
|
||||
</div>
|
||||
{{ end }}
|
||||
<div class="mb-3">
|
||||
<label for="newUserPassword" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" id="newUserPassword" aria-describedby="Password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="newUserCreate">Create</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pageContainer">
|
||||
<h1>
|
||||
Accounts admin
|
||||
</h1>
|
||||
<ul class="nav nav-pills" style="margin-bottom: 2rem;">
|
||||
<li class="nav-item">
|
||||
<h2><a id="invitesTabButton" class="nl nav-link active">Invites</a></h2>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<h2><a id="accountsTabButton" class="nl nav-link">Accounts</a></h2>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="btn-group" role="group" id="headerButtons">
|
||||
<button type="button" class="btn btn-primary" id="openSettings">
|
||||
Settings <i class="fa fa-cog"></i>
|
||||
@ -327,76 +319,105 @@
|
||||
Logout <i class="fa fa-sign-out"></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 id="invitesTab">
|
||||
<div class="card mb-3 tabGroup">
|
||||
<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-sm">
|
||||
<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>
|
||||
<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-sm">
|
||||
<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 id="accountsTab" class="unfocused">
|
||||
<div class="card mb-3 tabGroup">
|
||||
<div class="card-header d-flex" style="align-items: center;">
|
||||
<div>Accounts</div>
|
||||
<div class="ml-auto">
|
||||
<button type="button" class="btn btn-secondary" id="accountsTabAddUser">Add User</button>
|
||||
<button type="button" class="btn btn-primary unfocused" id="accountsTabSetDefaults">Modify Settings</button>
|
||||
<button type="button" class="btn btn-danger unfocused" id="accountsTabDelete"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body table-responsive">
|
||||
<table class="table table-hover table-striped table-borderless">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"><input class="form-check-input" type="checkbox" value="" id="selectAll"></th>
|
||||
<th scope="col">Username</th>
|
||||
<th scope="col">Email Address</th>
|
||||
<th scope="col">Last Active</th>
|
||||
<th scope="col">Admin?</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="accountsList">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -479,6 +500,7 @@
|
||||
const notifications_enabled = false;
|
||||
{{ end }}
|
||||
</script>
|
||||
<script src="accounts.js"></script>
|
||||
<script src="admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
26
email.go
26
email.go
@ -278,6 +278,32 @@ func (emailer *Emailer) constructReset(pwr Pwr, app *appContext) (*Email, error)
|
||||
return email, nil
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructDeleted(reason string, app *appContext) (*Email, error) {
|
||||
email := &Email{
|
||||
subject: app.config.Section("deletion").Key("subject").MustString("Your account was deleted - Jellyfin"),
|
||||
}
|
||||
for _, key := range []string{"html", "text"} {
|
||||
fpath := app.config.Section("deletion").Key("email_" + key).String()
|
||||
tpl, err := template.ParseFiles(fpath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var tplData bytes.Buffer
|
||||
err = tpl.Execute(&tplData, map[string]string{
|
||||
"reason": reason,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if key == "html" {
|
||||
email.html = tplData.String()
|
||||
} else {
|
||||
email.text = tplData.String()
|
||||
}
|
||||
}
|
||||
return email, nil
|
||||
}
|
||||
|
||||
// calls the send method in the underlying emailClient.
|
||||
func (emailer *Emailer) send(address string, email *Email) error {
|
||||
return emailer.sender.send(address, emailer.fromName, emailer.fromAddr, email)
|
||||
|
11
jfapi.go
11
jfapi.go
@ -215,6 +215,17 @@ func (jf *Jellyfin) _post(url string, data map[string]interface{}, response bool
|
||||
return "", resp.StatusCode, nil
|
||||
}
|
||||
|
||||
func (jf *Jellyfin) deleteUser(id string) (int, error) {
|
||||
url := fmt.Sprintf("%s/Users/%s", jf.server, id)
|
||||
req, _ := http.NewRequest("DELETE", url, nil)
|
||||
for name, value := range jf.header {
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
resp, err := jf.httpClient.Do(req)
|
||||
defer timeoutHandler("Jellyfin", jf.server, jf.noFail)
|
||||
return resp.StatusCode, err
|
||||
}
|
||||
|
||||
func (jf *Jellyfin) getUsers(public bool) ([]map[string]interface{}, int, error) {
|
||||
var result []map[string]interface{}
|
||||
var data string
|
||||
|
36
mail/deleted.mjml
Normal file
36
mail/deleted.mjml
Normal file
@ -0,0 +1,36 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-attributes>
|
||||
<mj-class name="bg" background-color="#101010" />
|
||||
<mj-class name="bg2" background-color="#242424" />
|
||||
<mj-class name="text" color="rgba(255,255,255,0.8)" />
|
||||
<mj-class name="bold" color="rgba(255,255,255,0.87)" />
|
||||
<mj-class name="secondary" color="rgb(153,153,153)" />
|
||||
<mj-class name="blue" background-color="rgb(0,164,220)" />
|
||||
</mj-attributes>
|
||||
<mj-font name="Quicksand" href="https://fonts.googleapis.com/css2?family=Quicksand" />
|
||||
<mj-font name="Noto Sans" href="https://fonts.googleapis.com/css2?family=Noto+Sans" />
|
||||
</mj-head>
|
||||
<mj-body>
|
||||
<mj-section mj-class="bg2">
|
||||
<mj-column>
|
||||
<mj-text mj-class="bold" font-size="25px" font-family="Quicksand, Noto Sans, Helvetica, Arial, sans-serif"> Jellyfin </mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section mj-class="bg">
|
||||
<mj-column>
|
||||
<mj-text mj-class="text" font-size="16px" font-family="Noto Sans, Helvetica, Arial, sans-serif">
|
||||
<h3>Your account was deleted.</h3>
|
||||
<p>Reason: <i>{{ .reason }}</i></p>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section mj-class="bg2">
|
||||
<mj-column>
|
||||
<mj-text mj-class="secondary" font-style="italic" font-size="14px">
|
||||
{{ .message }}
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</body>
|
||||
</mjml>
|
4
mail/deleted.txt
Normal file
4
mail/deleted.txt
Normal file
@ -0,0 +1,4 @@
|
||||
Your Jellyfin account was deleted.
|
||||
Reason: {{ .reason }}
|
||||
|
||||
{{ .message }}
|
7
main.go
7
main.go
@ -322,7 +322,7 @@ func start(asDaemon, firstCall bool) {
|
||||
app.storage.policy_path = app.config.Section("files").Key("user_template").String()
|
||||
app.storage.loadPolicy()
|
||||
// app.storage.configuration_path = filepath.Join(app.data_path, "user_configuration.json")
|
||||
app.storage.policy_path = app.config.Section("files").Key("user_configuration").String()
|
||||
app.storage.configuration_path = app.config.Section("files").Key("user_configuration").String()
|
||||
app.storage.loadConfiguration()
|
||||
// app.storage.displayprefs_path = filepath.Join(app.data_path, "user_displayprefs.json")
|
||||
app.storage.displayprefs_path = app.config.Section("files").Key("user_displayprefs").String()
|
||||
@ -435,13 +435,16 @@ func start(asDaemon, firstCall bool) {
|
||||
router.GET("/invite/:invCode", app.InviteProxy)
|
||||
api := router.Group("/", app.webAuth())
|
||||
router.POST("/logout", app.Logout)
|
||||
api.POST("/newUserAdmin", app.NewUserAdmin)
|
||||
api.POST("/generateInvite", app.GenerateInvite)
|
||||
api.GET("/getInvites", app.GetInvites)
|
||||
api.POST("/setNotify", app.SetNotify)
|
||||
api.POST("/deleteInvite", app.DeleteInvite)
|
||||
api.POST("/deleteUser", app.DeleteUser)
|
||||
api.GET("/getUsers", app.GetUsers)
|
||||
api.POST("/modifyUsers", app.ModifyEmails)
|
||||
api.POST("/modifyEmails", app.ModifyEmails)
|
||||
api.POST("/setDefaults", app.SetDefaults)
|
||||
api.POST("/applySettings", app.ApplySettings)
|
||||
api.GET("/getConfig", app.GetConfig)
|
||||
api.POST("/modifyConfig", app.ModifyConfig)
|
||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||
|
117
scss/base.scss
Normal file
117
scss/base.scss
Normal file
@ -0,0 +1,117 @@
|
||||
.pageContainer {
|
||||
margin: 5% 20% 5% 20%;
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.pageContainer {
|
||||
margin: 2%;
|
||||
}
|
||||
}
|
||||
h1 {
|
||||
/*margin: 20%;*/
|
||||
margin-bottom: 5%;
|
||||
}
|
||||
.tabGroup {
|
||||
/*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 */
|
||||
}
|
||||
|
||||
.invite-link {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.settingIcon {
|
||||
margin-left: 0.2rem;
|
||||
}
|
||||
|
||||
body.modal-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@mixin white-text {
|
||||
&, &:visited, &:hover, &:active {
|
||||
font-style: inherit;
|
||||
color: inherit;
|
||||
font-size: inherit;
|
||||
text-decoration: none;
|
||||
font-variant: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
font-family: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
%white-text {
|
||||
@include white-text;
|
||||
}
|
||||
|
||||
%link-unstyled {
|
||||
@include white-text;
|
||||
background-color: transparent;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.text-button {
|
||||
@extend %link-unstyled;
|
||||
}
|
||||
|
||||
.text-button:hover {
|
||||
@extend %link-unstyled;
|
||||
}
|
||||
|
||||
.nl {
|
||||
@extend %link-unstyled;
|
||||
}
|
||||
|
||||
.nl:hover {
|
||||
@extend %white-text;
|
||||
}
|
||||
|
||||
.unfocused {
|
||||
display: none;
|
||||
}
|
@ -138,3 +138,8 @@ $list-group-action-active-bg: $jf-blue-focus;
|
||||
background-color: $success;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background-color: $jf-blue-hover;
|
||||
}
|
||||
|
||||
@import "../base.scss";
|
||||
|
19
scss/bs4/bs4.scss
Normal file
19
scss/bs4/bs4.scss
Normal file
@ -0,0 +1,19 @@
|
||||
@import "../../node_modules/bootstrap4/scss/bootstrap";
|
||||
|
||||
.icon-button {
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
.icon-button:hover {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.icon-button:active {
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background-color: $list-group-hover-bg;
|
||||
}
|
||||
|
||||
@import "../base.scss";
|
@ -138,3 +138,8 @@ $list-group-action-active-bg: $jf-blue-focus;
|
||||
background-color: $success;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background-color: $jf-blue-hover;
|
||||
}
|
||||
|
||||
@import "../base.scss";
|
||||
|
19
scss/bs5/bs5.scss
Normal file
19
scss/bs5/bs5.scss
Normal file
@ -0,0 +1,19 @@
|
||||
@import "../../node_modules/bootstrap/scss/bootstrap";
|
||||
|
||||
.icon-button {
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
.icon-button:hover {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.icon-button:active {
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background-color: $list-group-hover-bg;
|
||||
}
|
||||
|
||||
@import "../base.scss";
|
@ -39,44 +39,45 @@ if not args.yes:
|
||||
node_bin = input("input bin directory: ")
|
||||
|
||||
for bsv in [d for d in local_path.iterdir() if "bs" in d.name]:
|
||||
scss = bsv / f"{bsv.name}-jf.scss"
|
||||
css = bsv / f"{bsv.name}-jf.css"
|
||||
min_css = bsv.parents[1] / "data" / "static" / f"{bsv.name}-jf.css"
|
||||
with open(css, "w") as f:
|
||||
f.write(
|
||||
sass.compile(
|
||||
filename=str(scss.resolve()), output_style="expanded", precision=6
|
||||
scss = [(bsv / f"{bsv.name}-jf.scss"), (bsv / f"{bsv.name}.scss")]
|
||||
css = [(bsv / f"{bsv.name}-jf.css"), (bsv / f"{bsv.name}.css")]
|
||||
min_css = [(bsv.parents[1] / "data" / "static" / f"{bsv.name}-jf.css"), (bsv.parents[1] / "data" / "static" / f"{bsv.name}.css")]
|
||||
for i in range(2):
|
||||
with open(css[i], "w") as f:
|
||||
f.write(
|
||||
sass.compile(
|
||||
filename=str(scss[i].resolve()), output_style="expanded", precision=6
|
||||
)
|
||||
)
|
||||
)
|
||||
if css.exists():
|
||||
print(f"{bsv.name}: Compiled.")
|
||||
# postcss only excepts forwards slashes? weird.
|
||||
cssPath = str(css.resolve())
|
||||
if os.name == "nt":
|
||||
cssPath = cssPath.replace("\\", "/")
|
||||
runcmd(
|
||||
f'{str((node_bin / "postcss").resolve())} {cssPath} --replace --use autoprefixer'
|
||||
)
|
||||
print(f"{bsv.name}: Prefixed.")
|
||||
runcmd(
|
||||
f'{str((node_bin / "cleancss").resolve())} --level 1 --format breakWith=lf --output {str(min_css.resolve())} {str(css.resolve())}'
|
||||
)
|
||||
if min_css.exists():
|
||||
print(f"{bsv.name}: Minified and copied to {str(min_css.resolve())}.")
|
||||
if css[i].exists():
|
||||
print(f"{scss[i].name}: Compiled.")
|
||||
# postcss only excepts forwards slashes? weird.
|
||||
cssPath = str(css[i].resolve())
|
||||
if os.name == "nt":
|
||||
cssPath = cssPath.replace("\\", "/")
|
||||
runcmd(
|
||||
f'{str((node_bin / "postcss").resolve())} {cssPath} --replace --use autoprefixer'
|
||||
)
|
||||
print(f"{scss[i].name}: Prefixed.")
|
||||
runcmd(
|
||||
f'{str((node_bin / "cleancss").resolve())} --level 1 --format breakWith=lf --output {str(min_css[i].resolve())} {str(css[i].resolve())}'
|
||||
)
|
||||
if min_css[i].exists():
|
||||
print(f"{scss[i].name}: Minified and copied to {str(min_css[i].resolve())}.")
|
||||
|
||||
for v in [("bootstrap", "bs5"), ("bootstrap4", "bs4")]:
|
||||
new_path = str((local_path.parent / "data" / "static" / (v[1] + ".css")).resolve())
|
||||
shutil.copy(
|
||||
str(
|
||||
(
|
||||
local_path.parent
|
||||
/ "node_modules"
|
||||
/ v[0]
|
||||
/ "dist"
|
||||
/ "css"
|
||||
/ "bootstrap.min.css"
|
||||
).resolve()
|
||||
),
|
||||
new_path,
|
||||
)
|
||||
print(f"Copied {v[1]} to {new_path}")
|
||||
# for v in [("bootstrap", "bs5"), ("bootstrap4", "bs4")]:
|
||||
# new_path = str((local_path.parent / "data" / "static" / (v[1] + ".css")).resolve())
|
||||
# shutil.copy(
|
||||
# str(
|
||||
# (
|
||||
# local_path.parent
|
||||
# / "node_modules"
|
||||
# / v[0]
|
||||
# / "dist"
|
||||
# / "css"
|
||||
# / "bootstrap.min.css"
|
||||
# ).resolve()
|
||||
# ),
|
||||
# new_path,
|
||||
# )
|
||||
# print(f"Copied {v[1]} to {new_path}")
|
||||
|
Loading…
Reference in New Issue
Block a user