mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-28 20:10:11 +00:00
Compare commits
No commits in common. "35a0be6a2c0c0a7d59d909c8fb2c813fa7609696" and "7d84fdec961635d454009c7274e1618a17e6b685" have entirely different histories.
35a0be6a2c
...
7d84fdec96
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,7 +4,6 @@ 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,7 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
@ -180,58 +179,6 @@ 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)
|
||||
@ -326,51 +273,6 @@ 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"`
|
||||
@ -556,29 +458,13 @@ func (app *appContext) DeleteInvite(gc *gin.Context) {
|
||||
respond(401, "Code doesn't exist", gc)
|
||||
}
|
||||
|
||||
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 userResp struct {
|
||||
UserList []respUser `json:"users"`
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -594,18 +480,10 @@ 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)
|
||||
@ -638,7 +516,6 @@ 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 {
|
||||
@ -648,7 +525,7 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
|
||||
return
|
||||
}
|
||||
for _, jfUser := range users {
|
||||
if address, ok := req[jfUser["Id"].(string)]; ok {
|
||||
if address, ok := req[jfUser["Name"].(string)]; ok {
|
||||
app.storage.emails[jfUser["Id"].(string)] = address
|
||||
}
|
||||
}
|
||||
@ -673,24 +550,22 @@ func (app *appContext) SetOmbiDefaults(gc *gin.Context) {
|
||||
}
|
||||
|
||||
type defaultsReq struct {
|
||||
From string `json:"from"`
|
||||
ApplyTo []string `json:"apply_to"`
|
||||
ID string `json:"id"`
|
||||
Homescreen bool `json:"homescreen"`
|
||||
Username string `json:"username"`
|
||||
Homescreen bool `json:"homescreen"`
|
||||
}
|
||||
|
||||
func (app *appContext) SetDefaults(gc *gin.Context) {
|
||||
var req defaultsReq
|
||||
gc.BindJSON(&req)
|
||||
userID := req.ID
|
||||
user, status, err := app.jf.userById(userID, false)
|
||||
app.info.Printf("Getting user defaults from \"%s\"", req.Username)
|
||||
user, status, err := app.jf.userByName(req.Username, 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
|
||||
}
|
||||
app.info.Printf("Getting user defaults from \"%s\"", user["Name"].(string))
|
||||
userID := user["Id"].(string)
|
||||
policy := user["Policy"].(map[string]interface{})
|
||||
app.storage.policy = policy
|
||||
app.storage.storePolicy()
|
||||
@ -715,81 +590,6 @@ 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,9 +69,6 @@ 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,36 +543,6 @@
|
||||
"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,7 +10,6 @@
|
||||
"mailgun",
|
||||
"smtp",
|
||||
"ombi",
|
||||
"deletion",
|
||||
"files"
|
||||
],
|
||||
"jellyfin": {
|
||||
@ -635,41 +634,6 @@
|
||||
"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",
|
||||
|
@ -1,363 +0,0 @@
|
||||
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,36 +1,3 @@
|
||||
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;
|
||||
@ -134,8 +101,6 @@ 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) {
|
||||
@ -673,7 +638,7 @@ document.getElementById('openDefaultsWizard').onclick = function() {
|
||||
checked = '';
|
||||
}
|
||||
radio.innerHTML =
|
||||
`<label><input type="radio" name="defaultRadios" id="default_${user['id']}" style="margin-right: 1rem;" ${checked}>${user['name']}</label>`;
|
||||
`<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');
|
||||
@ -690,19 +655,6 @@ 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();
|
||||
}
|
||||
}
|
||||
@ -710,81 +662,54 @@ document.getElementById('openDefaultsWizard').onclick = function() {
|
||||
req.send();
|
||||
};
|
||||
|
||||
function storeDefaults(users) {
|
||||
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');
|
||||
let id = '';
|
||||
for (let radio of radios) {
|
||||
if (radio.checked) {
|
||||
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);
|
||||
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));
|
||||
}
|
||||
};
|
||||
req.send(JSON.stringify(data));
|
||||
}
|
||||
};
|
||||
|
||||
var ombiDefaultsModal = '';
|
||||
@ -888,6 +813,104 @@ 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){
|
||||
|
6
data/static/bs4-jf.css
Normal file
6
data/static/bs4-jf.css
Normal file
File diff suppressed because one or more lines are too long
7
data/static/bs4.css
Normal file
7
data/static/bs4.css
Normal file
File diff suppressed because one or more lines are too long
6
data/static/bs5-jf.css
Normal file
6
data/static/bs5-jf.css
Normal file
File diff suppressed because one or more lines are too long
7
data/static/bs5.css
Normal file
7
data/static/bs5.css
Normal file
File diff suppressed because one or more lines are too long
@ -65,6 +65,82 @@
|
||||
<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">
|
||||
@ -105,6 +181,9 @@
|
||||
<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>
|
||||
@ -147,24 +226,16 @@
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="defaultsTitle"></h5>
|
||||
<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 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>
|
||||
<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">
|
||||
<input type="checkbox" value="" style="margin-right: 1rem;" id="storeDefaultHomescreen" checked>
|
||||
<label for="storeDefaultHomescreen" id="storeHomescreenLabel"></label>
|
||||
<label><input type="checkbox" value="" style="margin-right: 1rem;" id="storeDefaultHomescreen" checked>Store homescreen layout</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer" id="defaultsFooter">
|
||||
@ -207,8 +278,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>
|
||||
@ -244,73 +315,10 @@
|
||||
</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">
|
||||
<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>
|
||||
<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>
|
||||
@ -319,105 +327,76 @@
|
||||
Logout <i class="fa fa-sign-out"></i>
|
||||
</button>
|
||||
</div>
|
||||
<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 class="card mb-3 linkGroup">
|
||||
<div class="card-header">Current Invites</div>
|
||||
<ul class="list-group list-group-flush" id="invites">
|
||||
</ul>
|
||||
</div>
|
||||
<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 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>
|
||||
@ -500,7 +479,6 @@
|
||||
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,32 +278,6 @@ 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,17 +215,6 @@ 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
|
||||
|
@ -1,36 +0,0 @@
|
||||
<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>
|
@ -1,4 +0,0 @@
|
||||
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.configuration_path = app.config.Section("files").Key("user_configuration").String()
|
||||
app.storage.policy_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,16 +435,13 @@ 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("/modifyEmails", app.ModifyEmails)
|
||||
api.POST("/modifyUsers", 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
117
scss/base.scss
@ -1,117 +0,0 @@
|
||||
.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,8 +138,3 @@ $list-group-action-active-bg: $jf-blue-focus;
|
||||
background-color: $success;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background-color: $jf-blue-hover;
|
||||
}
|
||||
|
||||
@import "../base.scss";
|
||||
|
@ -1,19 +0,0 @@
|
||||
@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,8 +138,3 @@ $list-group-action-active-bg: $jf-blue-focus;
|
||||
background-color: $success;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background-color: $jf-blue-hover;
|
||||
}
|
||||
|
||||
@import "../base.scss";
|
||||
|
@ -1,19 +0,0 @@
|
||||
@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,45 +39,44 @@ 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"), (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
|
||||
)
|
||||
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
|
||||
)
|
||||
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())}.")
|
||||
)
|
||||
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())}.")
|
||||
|
||||
# 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