mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-29 12:30:11 +00:00
Compare commits
4 Commits
49b056f1d6
...
b6ceee508c
Author | SHA1 | Date | |
---|---|---|---|
b6ceee508c | |||
32b8ed4aa2 | |||
73886fc037 | |||
c4acb43cb8 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -5,6 +5,8 @@ scss/*.css*
|
|||||||
scss/bs4/*.css*
|
scss/bs4/*.css*
|
||||||
scss/bs5/*.css*
|
scss/bs5/*.css*
|
||||||
data/static/*.css
|
data/static/*.css
|
||||||
|
data/static/*.js
|
||||||
|
!data/static/setup.js
|
||||||
data/config-base.json
|
data/config-base.json
|
||||||
data/config-default.ini
|
data/config-default.ini
|
||||||
data/*.html
|
data/*.html
|
||||||
|
34
Makefile
34
Makefile
@ -1,40 +1,44 @@
|
|||||||
configuration:
|
configuration:
|
||||||
echo "Fixing config-base"
|
$(info Fixing config-base)
|
||||||
python3 config/fixconfig.py -i config/config-base.json -o data/config-base.json
|
python3 config/fixconfig.py -i config/config-base.json -o data/config-base.json
|
||||||
echo "Generating config-default.ini"
|
$(info Generating config-default.ini)
|
||||||
python3 config/generate_ini.py -i config/config-base.json -o data/config-default.ini --version git
|
python3 config/generate_ini.py -i config/config-base.json -o data/config-default.ini --version git
|
||||||
|
|
||||||
sass:
|
sass:
|
||||||
echo "Getting libsass"
|
$(info Getting libsass)
|
||||||
python3 -m pip install libsass
|
python3 -m pip install libsass
|
||||||
echo "Getting node dependencies"
|
$(info Getting node dependencies)
|
||||||
python3 scss/get_node_deps.py
|
python3 scss/get_node_deps.py
|
||||||
echo "Compiling sass"
|
$(info Compiling sass)
|
||||||
python3 scss/compile.py
|
python3 scss/compile.py
|
||||||
|
|
||||||
sass-headless:
|
sass-headless:
|
||||||
echo "Getting libsass"
|
$(info Getting libsass)
|
||||||
python3 -m pip install libsass
|
python3 -m pip install libsass
|
||||||
echo "Getting node dependencies"
|
$(info Getting node dependencies)
|
||||||
python3 scss/get_node_deps.py
|
python3 scss/get_node_deps.py
|
||||||
echo "Compiling sass"
|
$(info Compiling sass)
|
||||||
python3 scss/compile.py -y
|
python3 scss/compile.py -y
|
||||||
|
|
||||||
mail-headless:
|
mail-headless:
|
||||||
echo "Generating email html"
|
$(info Generating email html)
|
||||||
python3 mail/generate.py -y
|
python3 mail/generate.py -y
|
||||||
|
|
||||||
mail:
|
mail:
|
||||||
echo "Generating email html"
|
$(info Generating email html)
|
||||||
python3 mail/generate.py
|
python3 mail/generate.py
|
||||||
|
|
||||||
|
typescript:
|
||||||
|
$(info Compiling typescript)
|
||||||
|
-npx tsc -p ts/
|
||||||
|
|
||||||
version:
|
version:
|
||||||
python3 version.py auto version.go
|
python3 version.py auto version.go
|
||||||
|
|
||||||
compile:
|
compile:
|
||||||
echo "Downloading deps"
|
$(info Downloading deps)
|
||||||
go mod download
|
go mod download
|
||||||
echo "Building"
|
$(info Building)
|
||||||
mkdir -p build
|
mkdir -p build
|
||||||
CGO_ENABLED=0 go build -o build/jfa-go *.go
|
CGO_ENABLED=0 go build -o build/jfa-go *.go
|
||||||
|
|
||||||
@ -42,14 +46,14 @@ compress:
|
|||||||
upx --lzma build/jfa-go
|
upx --lzma build/jfa-go
|
||||||
|
|
||||||
copy:
|
copy:
|
||||||
echo "Copying data"
|
$(info Copying data)
|
||||||
cp -r data build/
|
cp -r data build/
|
||||||
|
|
||||||
install:
|
install:
|
||||||
cp -r build $(DESTDIR)/jfa-go
|
cp -r build $(DESTDIR)/jfa-go
|
||||||
|
|
||||||
all: configuration sass mail version compile copy
|
all: configuration sass mail version typescript compile copy
|
||||||
headless: configuration sass-headless mail-headless version compile copy
|
headless: configuration sass-headless mail-headless version typescript compile copy
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
79
api.go
79
api.go
@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -230,6 +231,7 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
app.jf.cacheExpiry = time.Now()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) NewUser(gc *gin.Context) {
|
func (app *appContext) NewUser(gc *gin.Context) {
|
||||||
@ -293,20 +295,28 @@ func (app *appContext) NewUser(gc *gin.Context) {
|
|||||||
if user["Id"] != nil {
|
if user["Id"] != nil {
|
||||||
id = user["Id"].(string)
|
id = user["Id"].(string)
|
||||||
}
|
}
|
||||||
if len(app.storage.policy) != 0 {
|
if invite.Profile != "" {
|
||||||
status, err = app.jf.setPolicy(id, app.storage.policy)
|
profile, ok := app.storage.profiles[invite.Profile]
|
||||||
|
if !ok {
|
||||||
|
profile = app.storage.profiles["Default"]
|
||||||
|
}
|
||||||
|
if len(profile.Policy) != 0 {
|
||||||
|
app.debug.Printf("Applying policy from profile \"%s\"", invite.Profile)
|
||||||
|
status, err = app.jf.setPolicy(id, profile.Policy)
|
||||||
if !(status == 200 || status == 204) {
|
if !(status == 200 || status == 204) {
|
||||||
app.err.Printf("%s: Failed to set user policy: Code %d", req.Code, status)
|
app.err.Printf("%s: Failed to set user policy: Code %d", req.Code, status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(app.storage.configuration) != 0 && len(app.storage.displayprefs) != 0 {
|
if len(profile.Configuration) != 0 && len(profile.Displayprefs) != 0 {
|
||||||
status, err = app.jf.setConfiguration(id, app.storage.configuration)
|
app.debug.Printf("Applying homescreen from profile \"%s\"", invite.Profile)
|
||||||
|
status, err = app.jf.setConfiguration(id, profile.Configuration)
|
||||||
if (status == 200 || status == 204) && err == nil {
|
if (status == 200 || status == 204) && err == nil {
|
||||||
status, err = app.jf.setDisplayPreferences(id, app.storage.displayprefs)
|
status, err = app.jf.setDisplayPreferences(id, profile.Displayprefs)
|
||||||
} else {
|
} else {
|
||||||
app.err.Printf("%s: Failed to set configuration template: Code %d", req.Code, status)
|
app.err.Printf("%s: Failed to set configuration template: Code %d", req.Code, status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if app.config.Section("password_resets").Key("enabled").MustBool(false) {
|
if app.config.Section("password_resets").Key("enabled").MustBool(false) {
|
||||||
app.storage.emails[id] = req.Email
|
app.storage.emails[id] = req.Email
|
||||||
app.storage.storeEmails()
|
app.storage.storeEmails()
|
||||||
@ -379,6 +389,7 @@ type generateInviteReq struct {
|
|||||||
MultipleUses bool `json:"multiple-uses"`
|
MultipleUses bool `json:"multiple-uses"`
|
||||||
NoLimit bool `json:"no-limit"`
|
NoLimit bool `json:"no-limit"`
|
||||||
RemainingUses int `json:"remaining-uses"`
|
RemainingUses int `json:"remaining-uses"`
|
||||||
|
Profile string `json:"profile"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) GenerateInvite(gc *gin.Context) {
|
func (app *appContext) GenerateInvite(gc *gin.Context) {
|
||||||
@ -389,7 +400,13 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
|
|||||||
current_time := time.Now()
|
current_time := time.Now()
|
||||||
valid_till := current_time.AddDate(0, 0, req.Days)
|
valid_till := current_time.AddDate(0, 0, req.Days)
|
||||||
valid_till = valid_till.Add(time.Hour*time.Duration(req.Hours) + time.Minute*time.Duration(req.Minutes))
|
valid_till = valid_till.Add(time.Hour*time.Duration(req.Hours) + time.Minute*time.Duration(req.Minutes))
|
||||||
|
// make sure code doesn't begin with number
|
||||||
invite_code := shortuuid.New()
|
invite_code := shortuuid.New()
|
||||||
|
_, err := strconv.Atoi(string(invite_code[0]))
|
||||||
|
for err == nil {
|
||||||
|
invite_code = shortuuid.New()
|
||||||
|
_, err = strconv.Atoi(string(invite_code[0]))
|
||||||
|
}
|
||||||
var invite Invite
|
var invite Invite
|
||||||
invite.Created = current_time
|
invite.Created = current_time
|
||||||
if req.MultipleUses {
|
if req.MultipleUses {
|
||||||
@ -418,11 +435,40 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
|
|||||||
app.info.Printf("%s: Sent invite email to %s", invite_code, req.Email)
|
app.info.Printf("%s: Sent invite email to %s", invite_code, req.Email)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if req.Profile != "" {
|
||||||
|
if _, ok := app.storage.profiles[req.Profile]; ok {
|
||||||
|
invite.Profile = req.Profile
|
||||||
|
} else {
|
||||||
|
invite.Profile = "Default"
|
||||||
|
}
|
||||||
|
}
|
||||||
app.storage.invites[invite_code] = invite
|
app.storage.invites[invite_code] = invite
|
||||||
app.storage.storeInvites()
|
app.storage.storeInvites()
|
||||||
gc.JSON(200, map[string]bool{"success": true})
|
gc.JSON(200, map[string]bool{"success": true})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type profileReq struct {
|
||||||
|
Invite string `json:"invite"`
|
||||||
|
Profile string `json:"profile"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *appContext) SetProfile(gc *gin.Context) {
|
||||||
|
var req profileReq
|
||||||
|
gc.BindJSON(&req)
|
||||||
|
app.debug.Printf("%s: Setting profile to \"%s\"", req.Invite, req.Profile)
|
||||||
|
// "" means "Don't apply profile"
|
||||||
|
if _, ok := app.storage.profiles[req.Profile]; !ok && req.Profile != "" {
|
||||||
|
app.err.Printf("%s: Profile \"%s\" not found", req.Invite, req.Profile)
|
||||||
|
respond(500, "Profile not found", gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
inv := app.storage.invites[req.Invite]
|
||||||
|
inv.Profile = req.Profile
|
||||||
|
app.storage.invites[req.Invite] = inv
|
||||||
|
app.storage.storeInvites()
|
||||||
|
gc.JSON(200, map[string]bool{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
func (app *appContext) GetInvites(gc *gin.Context) {
|
func (app *appContext) GetInvites(gc *gin.Context) {
|
||||||
app.debug.Println("Invites requested")
|
app.debug.Println("Invites requested")
|
||||||
current_time := time.Now()
|
current_time := time.Now()
|
||||||
@ -431,12 +477,14 @@ func (app *appContext) GetInvites(gc *gin.Context) {
|
|||||||
var invites []map[string]interface{}
|
var invites []map[string]interface{}
|
||||||
for code, inv := range app.storage.invites {
|
for code, inv := range app.storage.invites {
|
||||||
_, _, days, hours, minutes, _ := timeDiff(inv.ValidTill, current_time)
|
_, _, days, hours, minutes, _ := timeDiff(inv.ValidTill, current_time)
|
||||||
invite := make(map[string]interface{})
|
invite := map[string]interface{}{
|
||||||
invite["code"] = code
|
"code": code,
|
||||||
invite["days"] = days
|
"days": days,
|
||||||
invite["hours"] = hours
|
"hours": hours,
|
||||||
invite["minutes"] = minutes
|
"minutes": minutes,
|
||||||
invite["created"] = app.formatDatetime(inv.Created)
|
"created": app.formatDatetime(inv.Created),
|
||||||
|
"profile": inv.Profile,
|
||||||
|
}
|
||||||
if len(inv.UsedBy) != 0 {
|
if len(inv.UsedBy) != 0 {
|
||||||
invite["used-by"] = inv.UsedBy
|
invite["used-by"] = inv.UsedBy
|
||||||
}
|
}
|
||||||
@ -470,7 +518,14 @@ func (app *appContext) GetInvites(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
invites = append(invites, invite)
|
invites = append(invites, invite)
|
||||||
}
|
}
|
||||||
resp := map[string][]map[string]interface{}{
|
profiles := make([]string, len(app.storage.profiles))
|
||||||
|
i := 0
|
||||||
|
for p := range app.storage.profiles {
|
||||||
|
profiles[i] = p
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
resp := map[string]interface{}{
|
||||||
|
"profiles": profiles,
|
||||||
"invites": invites,
|
"invites": invites,
|
||||||
}
|
}
|
||||||
gc.JSON(200, resp)
|
gc.JSON(200, resp)
|
||||||
|
@ -48,7 +48,7 @@ func (app *appContext) loadConfig() error {
|
|||||||
// }
|
// }
|
||||||
key.SetValue(key.MustString(filepath.Join(app.data_path, (key.Name() + ".json"))))
|
key.SetValue(key.MustString(filepath.Join(app.data_path, (key.Name() + ".json"))))
|
||||||
}
|
}
|
||||||
for _, key := range []string{"user_configuration", "user_displayprefs", "ombi_template"} {
|
for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template"} {
|
||||||
// if app.config.Section("files").Key(key).MustString("") == "" {
|
// if app.config.Section("files").Key(key).MustString("") == "" {
|
||||||
// key.SetValue(filepath.Join(app.data_path, (key.Name() + ".json")))
|
// key.SetValue(filepath.Join(app.data_path, (key.Name() + ".json")))
|
||||||
// }
|
// }
|
||||||
|
@ -626,6 +626,14 @@
|
|||||||
"value": "",
|
"value": "",
|
||||||
"description": "Location of stored displayPreferences template (also used for homescreen layout) (json)"
|
"description": "Location of stored displayPreferences template (also used for homescreen layout) (json)"
|
||||||
},
|
},
|
||||||
|
"user_profiles": {
|
||||||
|
"name": "User Profiles",
|
||||||
|
"required": false,
|
||||||
|
"requires_restart": true,
|
||||||
|
"type": "text",
|
||||||
|
"value": "",
|
||||||
|
"description": "Location of stored user profiles (encompasses template and homescreen) (json)"
|
||||||
|
},
|
||||||
"custom_css": {
|
"custom_css": {
|
||||||
"name": "Custom CSS",
|
"name": "Custom CSS",
|
||||||
"required": false,
|
"required": false,
|
||||||
|
@ -1,356 +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";
|
|
||||||
let msg = "Notify user";
|
|
||||||
if (selected.length > 1) {
|
|
||||||
title += "s";
|
|
||||||
msg += "s";
|
|
||||||
}
|
|
||||||
title = "Delete " + selected.length + title;
|
|
||||||
msg += " of account deletion";
|
|
||||||
document.getElementById('deleteModalTitle').textContent = title;
|
|
||||||
document.getElementById('deleteModalNotify').checked = false;
|
|
||||||
document.getElementById('deleteModalNotifyLabel').textContent = msg;
|
|
||||||
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";
|
|
||||||
}
|
|
||||||
let fci = "form-check-input";
|
|
||||||
if (bsVersion != 5) {
|
|
||||||
fci = "";
|
|
||||||
}
|
|
||||||
return `
|
|
||||||
<td nowrap="nowrap" class="align-middle" scope="row"><input class="${fci}" 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;
|
|
||||||
}
|
|
||||||
populateRadios();
|
|
||||||
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();
|
|
||||||
};
|
|
1133
data/static/admin.js
1133
data/static/admin.js
File diff suppressed because it is too large
Load Diff
@ -1,32 +0,0 @@
|
|||||||
function serializeForm(id) {
|
|
||||||
var form = document.getElementById(id);
|
|
||||||
var formData = {};
|
|
||||||
for (var i = 0; i < form.elements.length; i++) {
|
|
||||||
var el = form.elements[i];
|
|
||||||
if (el.type != 'submit') {
|
|
||||||
var name = el.name;
|
|
||||||
if (name == '') {
|
|
||||||
name = el.id;
|
|
||||||
};
|
|
||||||
switch (el.type) {
|
|
||||||
case 'checkbox':
|
|
||||||
formData[name] = el.checked;
|
|
||||||
break;
|
|
||||||
case 'text':
|
|
||||||
case 'password':
|
|
||||||
case 'email':
|
|
||||||
case 'number':
|
|
||||||
formData[name] = el.value;
|
|
||||||
break;
|
|
||||||
case 'select-one':
|
|
||||||
let val = el.value;
|
|
||||||
if (!isNaN(val)) {
|
|
||||||
val = parseInt(val)
|
|
||||||
}
|
|
||||||
formData[name] = val;
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return formData;
|
|
||||||
};
|
|
@ -31,9 +31,9 @@
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
{{ if .bs5 }}
|
{{ if .bs5 }}
|
||||||
const bsVersion = 5;
|
var bsVersion = 5;
|
||||||
{{ else }}
|
{{ else }}
|
||||||
const bsVersion = 4;
|
var bsVersion = 4;
|
||||||
{{ end }}
|
{{ end }}
|
||||||
var cssFile = "{{ .cssFile }}";
|
var cssFile = "{{ .cssFile }}";
|
||||||
var css = document.createElement('link');
|
var css = document.createElement('link');
|
||||||
@ -52,8 +52,6 @@
|
|||||||
}
|
}
|
||||||
css.setAttribute('href', cssFile);
|
css.setAttribute('href', cssFile);
|
||||||
document.head.appendChild(css);
|
document.head.appendChild(css);
|
||||||
// store whether ombi is enabled, 1 or 0.
|
|
||||||
var ombiEnabled = {{ .ombiEnabled }}
|
|
||||||
</script>
|
</script>
|
||||||
{{ if not .bs5 }}
|
{{ if not .bs5 }}
|
||||||
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
|
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
|
||||||
@ -315,7 +313,7 @@
|
|||||||
<button type="button" class="btn btn-primary" id="openSettings">
|
<button type="button" class="btn btn-primary" id="openSettings">
|
||||||
Settings <i class="fa fa-cog"></i>
|
Settings <i class="fa fa-cog"></i>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-danger" id="logoutButton" style="display: none;">
|
<button type="button" class="btn btn-danger unfocused" id="logoutButton">
|
||||||
Logout <i class="fa fa-sign-out"></i>
|
Logout <i class="fa fa-sign-out"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -367,6 +365,11 @@
|
|||||||
</label>
|
</label>
|
||||||
<div id="noLimitWarning" class="form-text" style="display: none;">Warning: Unlimited usage invites pose a risk if published online.</div>
|
<div id="noLimitWarning" class="form-text" style="display: none;">Warning: Unlimited usage invites pose a risk if published online.</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom: 1rem;">
|
||||||
|
<label for="inviteProfile">Account creation profile</label>
|
||||||
|
<select class="form-control form-select" id="inviteProfile" name="profile">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
{{ if .email_enabled }}
|
{{ if .email_enabled }}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="send_to_address">Send invite to address</label>
|
<label for="send_to_address">Send invite to address</label>
|
||||||
@ -431,80 +434,24 @@
|
|||||||
</div>
|
</div>
|
||||||
<script src="serialize.js"></script>
|
<script src="serialize.js"></script>
|
||||||
<script>
|
<script>
|
||||||
{{ if .bs5 }}
|
|
||||||
function createModal(id, find = false) {
|
|
||||||
let modal;
|
|
||||||
if (find) {
|
|
||||||
modal = bootstrap.Modal.getInstance(document.getElementById(id));
|
|
||||||
} else {
|
|
||||||
modal = new bootstrap.Modal(document.getElementById(id));
|
|
||||||
}
|
|
||||||
document.getElementById(id).addEventListener('shown.bs.modal', function () {
|
|
||||||
document.body.classList.add("modal-open");
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
modal: modal,
|
|
||||||
show: function() {
|
|
||||||
let temp = this.modal.show();
|
|
||||||
return temp
|
|
||||||
},
|
|
||||||
hide: function() { return this.modal.hide(); }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
{{ else }}
|
|
||||||
let send_to_addess_enabled = document.getElementById('send_to_address_enabled');
|
|
||||||
if (typeof(send_to_address_enabled) != 'undefined') {
|
|
||||||
send_to_address_enabled.classList.remove('form-check-input');
|
|
||||||
}
|
|
||||||
let multiUseEnabled = document.getElementById('multiUseEnabled');
|
|
||||||
if (typeof(multiUseEnabled) != 'undefined') {
|
|
||||||
multiUseEnabled.classList.remove('form-check-input');
|
|
||||||
}
|
|
||||||
function createModal(id, find = false) {
|
|
||||||
$('#' + id).on('shown.bs.modal', function () {
|
|
||||||
document.body.classList.add("modal-open");
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
show: function() {
|
|
||||||
let temp = $('#' + id).modal('show');
|
|
||||||
return temp
|
|
||||||
},
|
|
||||||
hide: function() {
|
|
||||||
return $('#' + id).modal('hide');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
function triggerTooltips() {
|
|
||||||
{{ if .bs5 }}
|
|
||||||
document.getElementById('settingsMenu').addEventListener('shown.bs.modal', function() {
|
|
||||||
{{ else }}
|
|
||||||
$('#settingsMenu').on('shown.bs.modal', function() {
|
|
||||||
{{ end }}
|
|
||||||
// Hacky way to ensure anything dependent on checkbox state is disabled if necessary by just clicking them
|
|
||||||
let checkboxes = document.getElementById('settingsMenu').querySelectorAll('input[type="checkbox"]');
|
|
||||||
for (checkbox of checkboxes) {
|
|
||||||
checkbox.click();
|
|
||||||
checkbox.click();
|
|
||||||
}
|
|
||||||
let tooltips = [].slice.call(document.querySelectorAll('a[data-toggle="tooltip"]'));
|
|
||||||
tooltips.map(function(el) {
|
|
||||||
{{ if .bs5 }}
|
|
||||||
return new bootstrap.Tooltip(el);
|
|
||||||
{{ else }}
|
|
||||||
return $(el).tooltip();
|
|
||||||
{{ end }}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
{{ if .notifications }}
|
{{ if .notifications }}
|
||||||
const notifications_enabled = true;
|
var notifications_enabled = true;
|
||||||
{{ else }}
|
{{ else }}
|
||||||
const notifications_enabled = false;
|
var notifications_enabled = false;
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</script>
|
</script>
|
||||||
|
{{ if .bs5 }}
|
||||||
|
<script src="bs5.js"></script>
|
||||||
|
{{ else }}
|
||||||
|
<script src="bs4.js"></script>
|
||||||
|
{{ end }}
|
||||||
|
<script src="animation.js"></script>
|
||||||
<script src="accounts.js"></script>
|
<script src="accounts.js"></script>
|
||||||
|
<script src="invites.js"></script>
|
||||||
<script src="admin.js"></script>
|
<script src="admin.js"></script>
|
||||||
|
<script src="settings.js"></script>
|
||||||
|
{{ if .ombiEnabled }}
|
||||||
|
<script src="ombi.js"></script>
|
||||||
|
{{ end }}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
21
main.go
21
main.go
@ -328,6 +328,26 @@ func start(asDaemon, firstCall bool) {
|
|||||||
app.storage.displayprefs_path = app.config.Section("files").Key("user_displayprefs").String()
|
app.storage.displayprefs_path = app.config.Section("files").Key("user_displayprefs").String()
|
||||||
app.storage.loadDisplayprefs()
|
app.storage.loadDisplayprefs()
|
||||||
|
|
||||||
|
app.storage.profiles_path = app.config.Section("files").Key("user_profiles").String()
|
||||||
|
app.storage.loadProfiles()
|
||||||
|
|
||||||
|
if !(len(app.storage.policy) == 0 && len(app.storage.configuration) == 0 && len(app.storage.displayprefs) == 0) {
|
||||||
|
app.info.Println("Migrating user template files to new profile format")
|
||||||
|
app.storage.migrateToProfile()
|
||||||
|
for _, path := range [3]string{app.storage.policy_path, app.storage.configuration_path, app.storage.displayprefs_path} {
|
||||||
|
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||||
|
dir, fname := filepath.Split(path)
|
||||||
|
newFname := strings.Replace(fname, ".json", ".old.json", 1)
|
||||||
|
err := os.Rename(path, filepath.Join(dir, newFname))
|
||||||
|
if err != nil {
|
||||||
|
app.err.Fatalf("Failed to rename %s: %s", fname, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.info.Println("In case of a problem, your original files have been renamed to <file>.old.json")
|
||||||
|
app.storage.storeProfiles()
|
||||||
|
}
|
||||||
|
|
||||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||||
app.storage.ombi_path = app.config.Section("files").Key("ombi_template").String()
|
app.storage.ombi_path = app.config.Section("files").Key("ombi_template").String()
|
||||||
app.storage.loadOmbiTemplate()
|
app.storage.loadOmbiTemplate()
|
||||||
@ -438,6 +458,7 @@ func start(asDaemon, firstCall bool) {
|
|||||||
api.POST("/newUserAdmin", app.NewUserAdmin)
|
api.POST("/newUserAdmin", app.NewUserAdmin)
|
||||||
api.POST("/generateInvite", app.GenerateInvite)
|
api.POST("/generateInvite", app.GenerateInvite)
|
||||||
api.GET("/getInvites", app.GetInvites)
|
api.GET("/getInvites", app.GetInvites)
|
||||||
|
api.POST("/setProfile", app.SetProfile)
|
||||||
api.POST("/setNotify", app.SetNotify)
|
api.POST("/setNotify", app.SetNotify)
|
||||||
api.POST("/deleteInvite", app.DeleteInvite)
|
api.POST("/deleteInvite", app.DeleteInvite)
|
||||||
api.POST("/deleteUser", app.DeleteUser)
|
api.POST("/deleteUser", app.DeleteUser)
|
||||||
|
23
package-lock.json
generated
23
package-lock.json
generated
@ -49,6 +49,19 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
|
||||||
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
|
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
|
||||||
},
|
},
|
||||||
|
"@types/jquery": {
|
||||||
|
"version": "3.5.1",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/@types/jquery/download/@types/jquery-3.5.1.tgz",
|
||||||
|
"integrity": "sha1-zrsFes9QccQOQ58w6EDFejDUBsM=",
|
||||||
|
"requires": {
|
||||||
|
"@types/sizzle": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/sizzle": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/@types/sizzle/download/@types/sizzle-2.3.2.tgz",
|
||||||
|
"integrity": "sha1-qBG4wY4rq6t9VCszZYh64uTZ3kc="
|
||||||
|
},
|
||||||
"abbrev": {
|
"abbrev": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||||
@ -1845,6 +1858,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||||
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw="
|
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw="
|
||||||
},
|
},
|
||||||
|
"popper.js": {
|
||||||
|
"version": "1.16.1",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/popper.js/download/popper.js-1.16.1.tgz",
|
||||||
|
"integrity": "sha1-KiI8s9x7YhPXQOQDcr5A3kPmWxs="
|
||||||
|
},
|
||||||
"postcss": {
|
"postcss": {
|
||||||
"version": "7.0.32",
|
"version": "7.0.32",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.32.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.32.tgz",
|
||||||
@ -2201,6 +2219,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||||
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
|
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
|
||||||
},
|
},
|
||||||
|
"typescript": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/typescript/download/typescript-4.0.3.tgz?cache=0&sync_timestamp=1600584904815&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ftypescript%2Fdownload%2Ftypescript-4.0.3.tgz",
|
||||||
|
"integrity": "sha1-FTu9Ro7wdyXB35x36LRT+NNqu6U="
|
||||||
|
},
|
||||||
"uglify-js": {
|
"uglify-js": {
|
||||||
"version": "3.4.10",
|
"version": "3.4.10",
|
||||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz",
|
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz",
|
||||||
|
@ -17,12 +17,14 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/hrfee/jellyfin-accounts#readme",
|
"homepage": "https://github.com/hrfee/jellyfin-accounts#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/jquery": "^3.5.1",
|
||||||
"autoprefixer": "^9.8.5",
|
"autoprefixer": "^9.8.5",
|
||||||
"bootstrap": "^5.0.0-alpha1",
|
"bootstrap": "^5.0.0-alpha1",
|
||||||
"bootstrap4": "npm:bootstrap@^4.5.0",
|
"bootstrap4": "npm:bootstrap@^4.5.0",
|
||||||
"clean-css-cli": "^4.3.0",
|
"clean-css-cli": "^4.3.0",
|
||||||
"lodash": "^4.17.19",
|
"lodash": "^4.17.19",
|
||||||
"mjml": "^4.6.3",
|
"mjml": "^4.6.3",
|
||||||
"postcss-cli": "^7.1.1"
|
"postcss-cli": "^7.1.1",
|
||||||
|
"typescript": "^4.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
31
storage.go
31
storage.go
@ -8,13 +8,20 @@ import (
|
|||||||
|
|
||||||
type Storage struct {
|
type Storage struct {
|
||||||
timePattern string
|
timePattern string
|
||||||
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path string
|
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path string
|
||||||
invites Invites
|
invites Invites
|
||||||
|
profiles map[string]Profile
|
||||||
emails, policy, configuration, displayprefs, ombi_template map[string]interface{}
|
emails, policy, configuration, displayprefs, ombi_template map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// timePattern: %Y-%m-%dT%H:%M:%S.%f
|
// timePattern: %Y-%m-%dT%H:%M:%S.%f
|
||||||
|
|
||||||
|
type Profile struct {
|
||||||
|
Policy map[string]interface{} `json:"policy"`
|
||||||
|
Configuration map[string]interface{} `json:"configuration"`
|
||||||
|
Displayprefs map[string]interface{} `json:"displayprefs"`
|
||||||
|
}
|
||||||
|
|
||||||
type Invite struct {
|
type Invite struct {
|
||||||
Created time.Time `json:"created"`
|
Created time.Time `json:"created"`
|
||||||
NoLimit bool `json:"no-limit"`
|
NoLimit bool `json:"no-limit"`
|
||||||
@ -23,6 +30,7 @@ type Invite struct {
|
|||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
UsedBy [][]string `json:"used-by"`
|
UsedBy [][]string `json:"used-by"`
|
||||||
Notify map[string]map[string]bool `json:"notify"`
|
Notify map[string]map[string]bool `json:"notify"`
|
||||||
|
Profile string `json:"profile"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Invites map[string]Invite
|
type Invites map[string]Invite
|
||||||
@ -75,6 +83,27 @@ func (st *Storage) storeOmbiTemplate() error {
|
|||||||
return storeJSON(st.ombi_path, st.ombi_template)
|
return storeJSON(st.ombi_path, st.ombi_template)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (st *Storage) loadProfiles() error {
|
||||||
|
return loadJSON(st.profiles_path, &st.profiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (st *Storage) storeProfiles() error {
|
||||||
|
return storeJSON(st.profiles_path, st.profiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (st *Storage) migrateToProfile() error {
|
||||||
|
st.loadPolicy()
|
||||||
|
st.loadConfiguration()
|
||||||
|
st.loadDisplayprefs()
|
||||||
|
st.loadProfiles()
|
||||||
|
st.profiles["Default"] = Profile{
|
||||||
|
Policy: st.policy,
|
||||||
|
Configuration: st.configuration,
|
||||||
|
Displayprefs: st.displayprefs,
|
||||||
|
}
|
||||||
|
return st.storeProfiles()
|
||||||
|
}
|
||||||
|
|
||||||
func loadJSON(path string, obj interface{}) error {
|
func loadJSON(path string, obj interface{}) error {
|
||||||
var file []byte
|
var file []byte
|
||||||
var err error
|
var err error
|
||||||
|
335
ts/accounts.ts
Normal file
335
ts/accounts.ts
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
const checkCheckboxes = (): void => {
|
||||||
|
const defaultsButton = document.getElementById('accountsTabSetDefaults');
|
||||||
|
const deleteButton = document.getElementById('accountsTabDelete');
|
||||||
|
const checkboxes: NodeListOf<HTMLInputElement> = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]:checked');
|
||||||
|
let checked = checkboxes.length;
|
||||||
|
if (checked == 0) {
|
||||||
|
Unfocus(defaultsButton);
|
||||||
|
Unfocus(deleteButton);
|
||||||
|
} else {
|
||||||
|
Focus(defaultsButton);
|
||||||
|
Focus(deleteButton);
|
||||||
|
if (checked == 1) {
|
||||||
|
deleteButton.textContent = 'Delete User';
|
||||||
|
} else {
|
||||||
|
deleteButton.textContent = 'Delete Users';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateEmail = (email: string): boolean => /\S+@\S+\.\S+/.test(email);
|
||||||
|
|
||||||
|
function changeEmail(icon: HTMLElement, id: string): void {
|
||||||
|
const iconContent = icon.outerHTML;
|
||||||
|
icon.setAttribute('class', '');
|
||||||
|
const entry = icon.nextElementSibling as HTMLInputElement;
|
||||||
|
const ogEmail = entry.value;
|
||||||
|
entry.readOnly = false;
|
||||||
|
entry.classList.remove('form-control-plaintext');
|
||||||
|
entry.classList.add('form-control');
|
||||||
|
if (ogEmail == "") {
|
||||||
|
entry.placeholder = 'Address';
|
||||||
|
}
|
||||||
|
const tick = createEl(`
|
||||||
|
<i class="fa fa-check d-inline-block icon-button text-success" style="margin-left: 0.5rem; margin-right: 0.5rem;"></i>
|
||||||
|
`);
|
||||||
|
tick.onclick = (): void => {
|
||||||
|
const newEmail = entry.value;
|
||||||
|
if (!validateEmail(newEmail) || newEmail == ogEmail) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cross.remove();
|
||||||
|
const spinner = createEl(`
|
||||||
|
<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>
|
||||||
|
`);
|
||||||
|
tick.replaceWith(spinner);
|
||||||
|
let send = {};
|
||||||
|
send[id] = newEmail;
|
||||||
|
_post("/modifyEmails", send, function (): void {
|
||||||
|
if (this.readyState == 4) {
|
||||||
|
if (this.status == 200 || this.status == 204) {
|
||||||
|
entry.nextElementSibling.remove();
|
||||||
|
} else {
|
||||||
|
entry.value = ogEmail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
icon.outerHTML = iconContent;
|
||||||
|
entry.readOnly = true;
|
||||||
|
entry.classList.remove('form-control');
|
||||||
|
entry.classList.add('form-control-plaintext');
|
||||||
|
entry.placeholder = '';
|
||||||
|
};
|
||||||
|
const cross = createEl(`
|
||||||
|
<i class="fa fa-close d-inline-block icon-button text-danger"></i>
|
||||||
|
`);
|
||||||
|
cross.onclick = (): void => {
|
||||||
|
tick.remove();
|
||||||
|
cross.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);
|
||||||
|
};
|
||||||
|
|
||||||
|
var jfUsers: Array<Object>;
|
||||||
|
|
||||||
|
function populateUsers(): void {
|
||||||
|
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>
|
||||||
|
`;
|
||||||
|
Unfocus(acList.parentNode.querySelector('thead'));
|
||||||
|
const accountsList = document.createElement('tbody');
|
||||||
|
accountsList.id = 'accountsList';
|
||||||
|
const generateEmail = (id: string, name: string, email: string): string => {
|
||||||
|
let entry: HTMLDivElement = document.createElement('div');
|
||||||
|
entry.id = 'email_' + id;
|
||||||
|
let emailValue: string = email;
|
||||||
|
if (emailValue == 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 = (id: string, username: string, email: string, lastActive: string, admin: boolean): string => {
|
||||||
|
let isAdmin = "No";
|
||||||
|
if (admin) {
|
||||||
|
isAdmin = "Yes";
|
||||||
|
}
|
||||||
|
let fci = "form-check-input";
|
||||||
|
if (bsVersion != 5) {
|
||||||
|
fci = "";
|
||||||
|
}
|
||||||
|
return `
|
||||||
|
<td nowrap="nowrap" class="align-middle" scope="row"><input class="${fci}" 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>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
_get("/getUsers", null, function (): void {
|
||||||
|
if (this.readyState == 4 && this.status == 200) {
|
||||||
|
jfUsers = this.response['users'];
|
||||||
|
for (const 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);
|
||||||
|
}
|
||||||
|
Focus(acList.parentNode.querySelector('thead'));
|
||||||
|
acList.replaceWith(accountsList);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateRadios(): void {
|
||||||
|
const radioList = document.getElementById('defaultUserRadios');
|
||||||
|
radioList.textContent = '';
|
||||||
|
let first = true;
|
||||||
|
for (const i in jfUsers) {
|
||||||
|
const user = jfUsers[i];
|
||||||
|
const radio = document.createElement('div');
|
||||||
|
radio.classList.add('form-check');
|
||||||
|
let checked = '';
|
||||||
|
if (first) {
|
||||||
|
checked = 'checked';
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
radio.innerHTML = `
|
||||||
|
<input class="form-check-input" type="radio" name="defaultRadios" id="default_${user['id']}" ${checked}>
|
||||||
|
<label class="form-check-label" for="default_${user['id']}">${user['name']}</label>`;
|
||||||
|
radioList.appendChild(radio);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(<HTMLInputElement>document.getElementById('selectAll')).onclick = function (): void {
|
||||||
|
const checkboxes: NodeListOf<HTMLInputElement> = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]');
|
||||||
|
for (let i = 0; i < checkboxes.length; i++) {
|
||||||
|
checkboxes[i].checked = (<HTMLInputElement>this).checked;
|
||||||
|
}
|
||||||
|
checkCheckboxes();
|
||||||
|
};
|
||||||
|
|
||||||
|
(<HTMLInputElement>document.getElementById('deleteModalNotify')).onclick = function (): void {
|
||||||
|
const textbox: HTMLElement = document.getElementById('deleteModalReasonBox');
|
||||||
|
if ((<HTMLInputElement>this).checked) {
|
||||||
|
Focus(textbox);
|
||||||
|
} else {
|
||||||
|
Unfocus(textbox);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
(<HTMLButtonElement>document.getElementById('accountsTabDelete')).onclick = function (): void {
|
||||||
|
const deleteButton = this as HTMLButtonElement;
|
||||||
|
const checkboxes: NodeListOf<HTMLInputElement> = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]:checked');
|
||||||
|
let selected: Array<string> = new Array(checkboxes.length);
|
||||||
|
for (let i = 0; i < checkboxes.length; i++) {
|
||||||
|
selected[i] = checkboxes[i].id.replace("select_", "");
|
||||||
|
}
|
||||||
|
let title = " user";
|
||||||
|
let msg = "Notify user";
|
||||||
|
if (selected.length > 1) {
|
||||||
|
title += "s";
|
||||||
|
msg += "s";
|
||||||
|
}
|
||||||
|
title = `Delete ${selected.length} ${title}`;
|
||||||
|
msg += " of account deletion";
|
||||||
|
|
||||||
|
document.getElementById('deleteModalTitle').textContent = title;
|
||||||
|
const dmNotify = document.getElementById('deleteModalNotify') as HTMLInputElement;
|
||||||
|
dmNotify.checked = false;
|
||||||
|
document.getElementById('deleteModalNotifyLabel').textContent = msg;
|
||||||
|
const dmReason = document.getElementById('deleteModalReason') as HTMLTextAreaElement;
|
||||||
|
dmReason.value = '';
|
||||||
|
Unfocus(document.getElementById('deleteModalReasonBox'));
|
||||||
|
const dmSend = document.getElementById('deleteModalSend') as HTMLButtonElement;
|
||||||
|
dmSend.textContent = 'Delete';
|
||||||
|
dmSend.onclick = function (): void {
|
||||||
|
const button = this as HTMLButtonElement;
|
||||||
|
const send = {
|
||||||
|
'users': selected,
|
||||||
|
'notify': dmNotify.checked,
|
||||||
|
'reason': dmReason.value
|
||||||
|
};
|
||||||
|
_post("/deleteUser", send, function (): void {
|
||||||
|
if (this.readyState == 4) {
|
||||||
|
if (this.status == 500) {
|
||||||
|
if ("error" in this.reponse) {
|
||||||
|
button.textContent = 'Failed';
|
||||||
|
} else {
|
||||||
|
button.textContent = 'Partial fail (check console)';
|
||||||
|
console.log(this.response);
|
||||||
|
}
|
||||||
|
setTimeout((): void => {
|
||||||
|
Unfocus(deleteButton);
|
||||||
|
deleteModal.hide();
|
||||||
|
}, 4000);
|
||||||
|
} else {
|
||||||
|
Unfocus(deleteButton);
|
||||||
|
deleteModal.hide()
|
||||||
|
}
|
||||||
|
populateUsers();
|
||||||
|
checkCheckboxes();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
deleteModal.show();
|
||||||
|
};
|
||||||
|
|
||||||
|
(<HTMLInputElement>document.getElementById('selectAll')).checked = false;
|
||||||
|
|
||||||
|
(<HTMLButtonElement>document.getElementById('accountsTabSetDefaults')).onclick = function (): void {
|
||||||
|
const checkboxes: NodeListOf<HTMLInputElement> = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]:checked');
|
||||||
|
let userIDs: Array<string> = new Array(checkboxes.length);
|
||||||
|
for (let i = 0; i < checkboxes.length; i++){
|
||||||
|
userIDs[i] = checkboxes[i].id.replace("select_", "");
|
||||||
|
}
|
||||||
|
if (userIDs.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
populateRadios();
|
||||||
|
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`;
|
||||||
|
Focus(document.getElementById('defaultsSourceSection'));
|
||||||
|
(<HTMLSelectElement>document.getElementById('defaultsSource')).value = 'userTemplate';
|
||||||
|
Unfocus(document.getElementById('defaultUserRadios'));
|
||||||
|
document.getElementById('storeDefaults').onclick = (): void => storeDefaults(userIDs);
|
||||||
|
userDefaultsModal.show();
|
||||||
|
};
|
||||||
|
|
||||||
|
(<HTMLSelectElement>document.getElementById('defaultsSource')).addEventListener('change', function (): void {
|
||||||
|
const radios = document.getElementById('defaultUserRadios');
|
||||||
|
if (this.value == 'userTemplate') {
|
||||||
|
Unfocus(radios);
|
||||||
|
} else {
|
||||||
|
Focus(radios);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
(<HTMLButtonElement>document.getElementById('newUserCreate')).onclick = function (): void {
|
||||||
|
const button = this as HTMLButtonElement;
|
||||||
|
const ogText = button.textContent;
|
||||||
|
button.innerHTML = `
|
||||||
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Creating...
|
||||||
|
`;
|
||||||
|
const email: string = (<HTMLInputElement>document.getElementById('newUserEmail')).value;
|
||||||
|
var username: string = email;
|
||||||
|
if (document.getElementById('newUserName') != null) {
|
||||||
|
username = (<HTMLInputElement>document.getElementById('newUserName')).value;
|
||||||
|
}
|
||||||
|
const password: string = (<HTMLInputElement>document.getElementById('newUserPassword')).value;
|
||||||
|
if (!validateEmail(email) && email != "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const send = {
|
||||||
|
'username': username,
|
||||||
|
'password': password,
|
||||||
|
'email': email
|
||||||
|
};
|
||||||
|
_post("/newUserAdmin", send, function (): void {
|
||||||
|
if (this.readyState == 4) {
|
||||||
|
rmAttr(button, 'btn-primary');
|
||||||
|
if (this.status == 200) {
|
||||||
|
addAttr(button, 'btn-success');
|
||||||
|
button.textContent = 'Success';
|
||||||
|
setTimeout((): void => {
|
||||||
|
rmAttr(button, 'btn-success');
|
||||||
|
addAttr(button, 'btn-primary');
|
||||||
|
button.textContent = ogText;
|
||||||
|
newUserModal.hide();
|
||||||
|
}, 1000);
|
||||||
|
populateUsers();
|
||||||
|
} else {
|
||||||
|
addAttr(button, 'btn-danger');
|
||||||
|
if ("error" in this.response) {
|
||||||
|
button.textContent = this.response["error"];
|
||||||
|
} else {
|
||||||
|
button.textContent = 'Failed';
|
||||||
|
}
|
||||||
|
setTimeout((): void => {
|
||||||
|
rmAttr(button, 'btn-danger');
|
||||||
|
addAttr(button, 'btn-primary');
|
||||||
|
button.textContent = ogText;
|
||||||
|
}, 2000);
|
||||||
|
populateUsers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
(<HTMLButtonElement>document.getElementById('accountsTabAddUser')).onclick = function (): void {
|
||||||
|
(<HTMLInputElement>document.getElementById('newUserEmail')).value = '';
|
||||||
|
(<HTMLInputElement>document.getElementById('newUserPassword')).value = '';
|
||||||
|
if (document.getElementById('newUserName') != null) {
|
||||||
|
(<HTMLInputElement>document.getElementById('newUserName')).value = '';
|
||||||
|
}
|
||||||
|
newUserModal.show();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
271
ts/admin.ts
Normal file
271
ts/admin.ts
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
interface Window {
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set in admin.html
|
||||||
|
var cssFile: string;
|
||||||
|
|
||||||
|
const _post = (url: string, data: Object, onreadystatechange: () => void): void => {
|
||||||
|
let req = new XMLHttpRequest();
|
||||||
|
req.open("POST", url, true);
|
||||||
|
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||||
|
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||||
|
req.onreadystatechange = onreadystatechange;
|
||||||
|
req.send(JSON.stringify(data));
|
||||||
|
};
|
||||||
|
|
||||||
|
const _get = (url: string, data: Object, onreadystatechange: () => void): void => {
|
||||||
|
let req = new XMLHttpRequest();
|
||||||
|
req.open("GET", url, true);
|
||||||
|
req.responseType = 'json';
|
||||||
|
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||||
|
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||||
|
req.onreadystatechange = onreadystatechange;
|
||||||
|
req.send(JSON.stringify(data));
|
||||||
|
};
|
||||||
|
|
||||||
|
const rmAttr = (el: HTMLElement, attr: string): void => {
|
||||||
|
if (el.classList.contains(attr)) {
|
||||||
|
el.classList.remove(attr);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const addAttr = (el: HTMLElement, attr: string): void => el.classList.add(attr);
|
||||||
|
|
||||||
|
const Focus = (el: HTMLElement): void => rmAttr(el, 'unfocused');
|
||||||
|
const Unfocus = (el: HTMLElement): void => addAttr(el, 'unfocused');
|
||||||
|
|
||||||
|
interface TabSwitcher {
|
||||||
|
invitesEl: HTMLDivElement;
|
||||||
|
accountsEl: HTMLDivElement;
|
||||||
|
invitesTabButton: HTMLAnchorElement;
|
||||||
|
accountsTabButton: HTMLAnchorElement;
|
||||||
|
invites: () => void;
|
||||||
|
accounts: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabs: TabSwitcher = {
|
||||||
|
invitesEl: document.getElementById('invitesTab') as HTMLDivElement,
|
||||||
|
accountsEl: document.getElementById('accountsTab') as HTMLDivElement,
|
||||||
|
invitesTabButton: document.getElementById('invitesTabButton') as HTMLAnchorElement,
|
||||||
|
accountsTabButton: document.getElementById('accountsTabButton') as HTMLAnchorElement,
|
||||||
|
invites: (): void => {
|
||||||
|
Unfocus(tabs.accountsEl);
|
||||||
|
Focus(tabs.invitesEl);
|
||||||
|
rmAttr(tabs.accountsTabButton, "active");
|
||||||
|
addAttr(tabs.invitesTabButton, "active");
|
||||||
|
},
|
||||||
|
accounts: (): void => {
|
||||||
|
populateUsers();
|
||||||
|
(document.getElementById('selectAll') as HTMLInputElement).checked = false;
|
||||||
|
checkCheckboxes();
|
||||||
|
Unfocus(tabs.invitesEl);
|
||||||
|
Focus(tabs.accountsEl);
|
||||||
|
rmAttr(tabs.invitesTabButton, "active");
|
||||||
|
addAttr(tabs.accountsTabButton, "active");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tabs.invitesTabButton.onclick = tabs.invites;
|
||||||
|
tabs.accountsTabButton.onclick = tabs.accounts;
|
||||||
|
|
||||||
|
tabs.invites();
|
||||||
|
|
||||||
|
// Predefined colors for the theme button.
|
||||||
|
var buttonColor: string = "custom";
|
||||||
|
if (cssFile.includes("jf")) {
|
||||||
|
buttonColor = "rgb(255,255,255)";
|
||||||
|
} else if (cssFile == ("bs" + bsVersion + ".css")) {
|
||||||
|
buttonColor = "rgb(16,16,16)";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buttonColor != "custom") {
|
||||||
|
const switchButton = document.createElement('button') as HTMLButtonElement;
|
||||||
|
switchButton.classList.add('btn', 'btn-secondary');
|
||||||
|
switchButton.innerHTML = `
|
||||||
|
Theme
|
||||||
|
<i class="fa fa-circle circle" style="color: ${buttonColor}; margin-left: 0.4rem;" id="fakeButton"></i>
|
||||||
|
`;
|
||||||
|
switchButton.onclick = (): void => toggleCSS(document.getElementById('fakeButton'));
|
||||||
|
document.getElementById('headerButtons').appendChild(switchButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
var loginModal = createModal('login');
|
||||||
|
var settingsModal = createModal('settingsMenu');
|
||||||
|
var userDefaultsModal = createModal('userDefaults');
|
||||||
|
var usersModal = createModal('users');
|
||||||
|
var restartModal = createModal('restartModal');
|
||||||
|
var refreshModal = createModal('refreshModal');
|
||||||
|
var aboutModal = createModal('aboutModal');
|
||||||
|
var deleteModal = createModal('deleteModal');
|
||||||
|
var newUserModal = createModal('newUserModal');
|
||||||
|
|
||||||
|
var availableProfiles: Array<string>;
|
||||||
|
|
||||||
|
window["token"] = "";
|
||||||
|
|
||||||
|
function toClipboard(str: string): void {
|
||||||
|
const el = document.createElement('textarea') as HTMLTextAreaElement;
|
||||||
|
el.value = str;
|
||||||
|
el.readOnly = true;
|
||||||
|
el.style.position = "absolute";
|
||||||
|
el.style.left = "-9999px";
|
||||||
|
document.body.appendChild(el);
|
||||||
|
const selected = document.getSelection().rangeCount > 0 ? document.getSelection().getRangeAt(0) : false;
|
||||||
|
el.select();
|
||||||
|
document.execCommand("copy");
|
||||||
|
document.body.removeChild(el);
|
||||||
|
if (selected) {
|
||||||
|
document.getSelection().removeAllRanges();
|
||||||
|
document.getSelection().addRange(selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function login(username: string, password: string, modal: boolean, button?: HTMLButtonElement, run?: (arg0: number) => void): void {
|
||||||
|
const req = new XMLHttpRequest();
|
||||||
|
req.responseType = 'json';
|
||||||
|
req.open("GET", "/getToken", true);
|
||||||
|
req.setRequestHeader("Authorization", "Basic " + btoa(username + ":" + password));
|
||||||
|
req.onreadystatechange = function (): void {
|
||||||
|
if (this.readyState == 4) {
|
||||||
|
if (this.status != 200) {
|
||||||
|
let errorMsg = this.response["error"];
|
||||||
|
if (!errorMsg) {
|
||||||
|
errorMsg = "Unknown error";
|
||||||
|
}
|
||||||
|
if (modal) {
|
||||||
|
button.disabled = false;
|
||||||
|
button.textContent = errorMsg;
|
||||||
|
addAttr(button, "btn-danger");
|
||||||
|
rmAttr(button, "btn-primary");
|
||||||
|
setTimeout((): void => {
|
||||||
|
addAttr(button, "btn-primary");
|
||||||
|
rmAttr(button, "btn-danger");
|
||||||
|
button.textContent = "Login";
|
||||||
|
}, 4000);
|
||||||
|
} else {
|
||||||
|
loginModal.show();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const data = this.response;
|
||||||
|
window.token = data["token"];
|
||||||
|
generateInvites();
|
||||||
|
setInterval((): void => generateInvites(), 60 * 1000);
|
||||||
|
addOptions(30, document.getElementById('days') as HTMLSelectElement);
|
||||||
|
addOptions(24, document.getElementById('hours') as HTMLSelectElement);
|
||||||
|
const minutes = document.getElementById('minutes') as HTMLSelectElement;
|
||||||
|
addOptions(59, minutes);
|
||||||
|
minutes.value = "30";
|
||||||
|
checkDuration();
|
||||||
|
if (modal) {
|
||||||
|
loginModal.hide();
|
||||||
|
}
|
||||||
|
Focus(document.getElementById('logoutButton'));
|
||||||
|
}
|
||||||
|
if (run) {
|
||||||
|
run(+this.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
req.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEl(html: string): HTMLElement {
|
||||||
|
let div = document.createElement('div') as HTMLDivElement;
|
||||||
|
div.innerHTML = html;
|
||||||
|
return div.firstElementChild as HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
(document.getElementById('loginForm') as HTMLFormElement).onsubmit = function (): boolean {
|
||||||
|
window.token = "";
|
||||||
|
const details = serializeForm('loginForm');
|
||||||
|
const button = document.getElementById('loginSubmit') as HTMLButtonElement;
|
||||||
|
addAttr(button, "btn-primary");
|
||||||
|
rmAttr(button, "btn-danger");
|
||||||
|
button.disabled = true;
|
||||||
|
button.innerHTML = `
|
||||||
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>
|
||||||
|
Loading...`;
|
||||||
|
login(details["username"], details["password"], true, button);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
function storeDefaults(users: string | Array<string>): void {
|
||||||
|
// not sure if this does anything, but w/e
|
||||||
|
this.disabled = true;
|
||||||
|
this.innerHTML =
|
||||||
|
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
||||||
|
'Loading...';
|
||||||
|
const button = document.getElementById('storeDefaults') as HTMLButtonElement;
|
||||||
|
const radio = document.querySelector('input[name=defaultRadios]:checked') as HTMLInputElement
|
||||||
|
let id = radio.id.replace("default_", "");
|
||||||
|
let route = "/setDefaults";
|
||||||
|
let data = {
|
||||||
|
"from": "user",
|
||||||
|
"id": id,
|
||||||
|
"homescreen": false
|
||||||
|
};
|
||||||
|
if ((document.getElementById('defaultsSource') as HTMLSelectElement).value == 'userTemplate') {
|
||||||
|
data["from"] = "template";
|
||||||
|
}
|
||||||
|
if (users != "all") {
|
||||||
|
data["apply_to"] = users;
|
||||||
|
route = "/applySettings";
|
||||||
|
}
|
||||||
|
if ((document.getElementById('storeDefaultHomescreen') as HTMLInputElement).checked) {
|
||||||
|
data["homescreen"] = true;
|
||||||
|
}
|
||||||
|
_post(route, data, function (): void {
|
||||||
|
if (this.readyState == 4) {
|
||||||
|
if (this.status == 200 || this.status == 204) {
|
||||||
|
button.textContent = "Success";
|
||||||
|
addAttr(button, "btn-success");
|
||||||
|
rmAttr(button, "btn-danger");
|
||||||
|
rmAttr(button, "btn-primary");
|
||||||
|
button.disabled = false;
|
||||||
|
setTimeout((): void => {
|
||||||
|
button.textContent = "Submit";
|
||||||
|
addAttr(button, "btn-primary");
|
||||||
|
rmAttr(button, "btn-success");
|
||||||
|
button.disabled = false;
|
||||||
|
userDefaultsModal.hide();
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
if ("error" in this.response) {
|
||||||
|
button.textContent = this.response["error"];
|
||||||
|
} else if (("policy" in this.response) || ("homescreen" in this.response)) {
|
||||||
|
button.textContent = "Failed (check console)";
|
||||||
|
} else {
|
||||||
|
button.textContent = "Failed";
|
||||||
|
}
|
||||||
|
addAttr(button, "btn-danger");
|
||||||
|
rmAttr(button, "btn-primary");
|
||||||
|
setTimeout((): void => {
|
||||||
|
button.textContent = "Submit";
|
||||||
|
addAttr(button, "btn-primary");
|
||||||
|
rmAttr(button, "btn-danger");
|
||||||
|
button.disabled = false;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
generateInvites(true);
|
||||||
|
|
||||||
|
login("", "", false, null, (status: number): void => {
|
||||||
|
if (!(status == 200 || status == 204)) {
|
||||||
|
loginModal.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
(document.getElementById('logoutButton') as HTMLButtonElement).onclick = function (): void {
|
||||||
|
_post("/logout", null, function (): boolean {
|
||||||
|
if (this.readyState == 4 && this.status == 200) {
|
||||||
|
window.token = "";
|
||||||
|
location.reload();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
79
ts/animation.ts
Normal file
79
ts/animation.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
// Used for animation on theme change
|
||||||
|
const whichTransitionEvent = (): string => {
|
||||||
|
const el = document.createElement('fakeElement');
|
||||||
|
const transitions = {
|
||||||
|
'transition': 'transitionend',
|
||||||
|
'OTransition': 'oTransitionEnd',
|
||||||
|
'MozTransition': 'transitionend',
|
||||||
|
'WebkitTransition': 'webkitTransitionEnd'
|
||||||
|
};
|
||||||
|
for (const t in transitions) {
|
||||||
|
if (el.style[t] !== undefined) {
|
||||||
|
return transitions[t];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
var transitionEndEvent = whichTransitionEvent();
|
||||||
|
|
||||||
|
// Toggles between light and dark themes
|
||||||
|
const _toggleCSS = (): void => {
|
||||||
|
const els: NodeListOf<HTMLLinkElement> = document.querySelectorAll('link[rel="stylesheet"][type="text/css"]');
|
||||||
|
let cssEl = 0;
|
||||||
|
let remove = false;
|
||||||
|
if (els.length != 1) {
|
||||||
|
cssEl = 1;
|
||||||
|
remove = true
|
||||||
|
}
|
||||||
|
let href: string = "bs" + bsVersion;
|
||||||
|
if (!els[cssEl].href.includes(href + "-jf")) {
|
||||||
|
href += "-jf";
|
||||||
|
}
|
||||||
|
href += ".css";
|
||||||
|
let newEl = els[cssEl].cloneNode(true) as HTMLLinkElement;
|
||||||
|
newEl.href = href;
|
||||||
|
els[cssEl].parentNode.insertBefore(newEl, els[cssEl].nextSibling);
|
||||||
|
if (remove) {
|
||||||
|
els[0].remove();
|
||||||
|
}
|
||||||
|
document.cookie = "css=" + href;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggles between light and dark themes, but runs animation if window small enough.
|
||||||
|
var buttonWidth = 0;
|
||||||
|
const toggleCSS = (el: HTMLElement): void => {
|
||||||
|
const switchToColor = window.getComputedStyle(document.body, null).backgroundColor;
|
||||||
|
// Max page width for animation to take place
|
||||||
|
let maxWidth = 1500;
|
||||||
|
if (window.innerWidth < maxWidth) {
|
||||||
|
// Calculate minimum radius to cover screen
|
||||||
|
const radius = Math.sqrt(Math.pow(window.innerWidth, 2) + Math.pow(window.innerHeight, 2));
|
||||||
|
const currentRadius = el.getBoundingClientRect().width / 2;
|
||||||
|
const scale = radius / currentRadius;
|
||||||
|
buttonWidth = +window.getComputedStyle(el, null).width;
|
||||||
|
document.body.classList.remove('smooth-transition');
|
||||||
|
el.style.transform = `scale(${scale})`;
|
||||||
|
el.style.color = switchToColor;
|
||||||
|
el.addEventListener(transitionEndEvent, function (): void {
|
||||||
|
if (this.style.transform.length != 0) {
|
||||||
|
_toggleCSS();
|
||||||
|
this.style.removeProperty('transform');
|
||||||
|
document.body.classList.add('smooth-transition');
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
} else {
|
||||||
|
_toggleCSS();
|
||||||
|
el.style.color = switchToColor;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const rotateButton = (el: HTMLElement): void => {
|
||||||
|
if (el.classList.contains("rotated")) {
|
||||||
|
rmAttr(el, "rotated")
|
||||||
|
addAttr(el, "not-rotated");
|
||||||
|
} else {
|
||||||
|
rmAttr(el, "not-rotated");
|
||||||
|
addAttr(el, "rotated");
|
||||||
|
}
|
||||||
|
};
|
38
ts/bs4.ts
Normal file
38
ts/bs4.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
var bsVersion = 4;
|
||||||
|
|
||||||
|
const send_to_addess_enabled = document.getElementById('send_to_addess_enabled');
|
||||||
|
if (send_to_addess_enabled) {
|
||||||
|
send_to_addess_enabled.classList.remove("form-check-input");
|
||||||
|
}
|
||||||
|
const multiUseEnabled = document.getElementById('multiUseEnabled');
|
||||||
|
if (multiUseEnabled) {
|
||||||
|
multiUseEnabled.classList.remove("form-check-input");
|
||||||
|
}
|
||||||
|
|
||||||
|
function createModal(id: string, find?: boolean): any {
|
||||||
|
$(`#${id}`).on("shown.bs.modal", (): void => document.body.classList.add("modal-open"));
|
||||||
|
return {
|
||||||
|
show: function (): any {
|
||||||
|
const temp = ($(`#${id}`) as any).modal("show");
|
||||||
|
return temp;
|
||||||
|
},
|
||||||
|
hide: function (): any {
|
||||||
|
return ($(`#${id}`) as any).modal("hide");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerTooltips(): void {
|
||||||
|
$("#settingsMenu").on("shown.bs.modal", (): void => {
|
||||||
|
const checkboxes = [].slice.call(document.getElementById('settingsMenu').querySelectorAll('input[type="checkbox"]'));
|
||||||
|
for (const i in checkboxes) {
|
||||||
|
checkboxes[i].click();
|
||||||
|
checkboxes[i].click();
|
||||||
|
}
|
||||||
|
const tooltips = [].slice.call(document.querySelectorAll('a[data-toggle="tooltip"]'));
|
||||||
|
tooltips.map((el: HTMLAnchorElement): any => {
|
||||||
|
return ($(el) as any).tooltip();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
36
ts/bs5.ts
Normal file
36
ts/bs5.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
declare var bootstrap: any;
|
||||||
|
|
||||||
|
var bsVersion = 5;
|
||||||
|
|
||||||
|
function createModal(id: string, find?: boolean): any {
|
||||||
|
let modal: any;
|
||||||
|
if (find) {
|
||||||
|
modal = bootstrap.Modal.getInstance(document.getElementById(id));
|
||||||
|
} else {
|
||||||
|
modal = new bootstrap.Modal(document.getElementById(id));
|
||||||
|
}
|
||||||
|
document.getElementById(id).addEventListener('shown.bs.modal', (): void => document.body.classList.add("modal-open"));
|
||||||
|
return {
|
||||||
|
modal: modal,
|
||||||
|
show: function (): any {
|
||||||
|
const temp = this.modal.show();
|
||||||
|
return temp;
|
||||||
|
},
|
||||||
|
hide: function (): any { return this.modal.hide(); }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerTooltips(): void {
|
||||||
|
(document.getElementById('settingsMenu') as HTMLButtonElement).addEventListener('shown.bs.modal', (): void => {
|
||||||
|
const checkboxes = [].slice.call(document.getElementById('settingsMenu').querySelectorAll('input[type="checkbox"]'));
|
||||||
|
for (const i in checkboxes) {
|
||||||
|
checkboxes[i].click();
|
||||||
|
checkboxes[i].click();
|
||||||
|
}
|
||||||
|
const tooltips = [].slice.call(document.querySelectorAll('a[data-toggle="tooltip"]'));
|
||||||
|
tooltips.map((el: HTMLAnchorElement): any => {
|
||||||
|
return new bootstrap.Tooltip(el);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
378
ts/invites.ts
Normal file
378
ts/invites.ts
Normal file
@ -0,0 +1,378 @@
|
|||||||
|
// Actually defined by templating in admin.html, this is just to avoid errors from tsc.
|
||||||
|
var notifications_enabled: any;
|
||||||
|
|
||||||
|
interface Invite {
|
||||||
|
code?: string;
|
||||||
|
expiresIn?: string;
|
||||||
|
empty: boolean;
|
||||||
|
remainingUses?: string;
|
||||||
|
email?: string;
|
||||||
|
usedBy?: Array<Array<string>>;
|
||||||
|
created?: string;
|
||||||
|
notifyExpiry?: boolean;
|
||||||
|
notifyCreation?: boolean;
|
||||||
|
profile?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyInvite = (): Invite => { return { code: "None", empty: true } as Invite; }
|
||||||
|
|
||||||
|
function parseInvite(invite: Object): Invite {
|
||||||
|
let inv: Invite = { code: invite["code"], empty: false, };
|
||||||
|
if (invite["email"]) {
|
||||||
|
inv.email = invite["email"];
|
||||||
|
}
|
||||||
|
let time = ""
|
||||||
|
const f = ["days", "hours", "minutes"];
|
||||||
|
for (const i in f) {
|
||||||
|
if (invite[f[i]] != 0) {
|
||||||
|
time += `${invite[f[i]]}${f[i][0]} `;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inv.expiresIn = `Expires in ${time.slice(0, -1)}`;
|
||||||
|
if (invite["no-limit"]) {
|
||||||
|
inv.remainingUses = "∞";
|
||||||
|
} else if ("remaining-uses" in invite) {
|
||||||
|
inv.remainingUses = invite["remaining-uses"];
|
||||||
|
}
|
||||||
|
if ("used-by" in invite) {
|
||||||
|
inv.usedBy = invite["used-by"];
|
||||||
|
}
|
||||||
|
if ("created" in invite) {
|
||||||
|
inv.created = invite["created"];
|
||||||
|
}
|
||||||
|
if ("notify-expiry" in invite) {
|
||||||
|
inv.notifyExpiry = invite["notify-expiry"];
|
||||||
|
}
|
||||||
|
if ("notify-creation" in invite) {
|
||||||
|
inv.notifyCreation = invite["notify-creation"];
|
||||||
|
}
|
||||||
|
if ("profile" in invite) {
|
||||||
|
inv.profile = invite["profile"];
|
||||||
|
}
|
||||||
|
return inv;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNotify(el: HTMLElement): void {
|
||||||
|
let send = {};
|
||||||
|
let code: string;
|
||||||
|
let notifyType: string;
|
||||||
|
if (el.id.includes("Expiry")) {
|
||||||
|
code = el.id.replace("_notifyExpiry", "");
|
||||||
|
notifyType = "notify-expiry";
|
||||||
|
} else if (el.id.includes("Creation")) {
|
||||||
|
code = el.id.replace("_notifyCreation", "");
|
||||||
|
notifyType = "notify-creation";
|
||||||
|
}
|
||||||
|
send[code] = {};
|
||||||
|
send[code][notifyType] = (el as HTMLInputElement).checked;
|
||||||
|
_post("/setNotify", send, function (): void {
|
||||||
|
if (this.readyState == 4 && this.status != 200) {
|
||||||
|
(el as HTMLInputElement).checked = !(el as HTMLInputElement).checked;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function genUsedBy(usedBy: Array<Array<string>>): string {
|
||||||
|
let uB = "";
|
||||||
|
if (usedBy && usedBy.length != 0) {
|
||||||
|
uB = `
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
<li class="list-group-item py-1">Users created:</li>
|
||||||
|
`;
|
||||||
|
for (const i in usedBy) {
|
||||||
|
uB += `
|
||||||
|
<li class="list-group-item py-1 disabled">
|
||||||
|
<div class="d-flex float-left">${usedBy[i][0]}</div>
|
||||||
|
<div class="d-flex float-right">${usedBy[i][1]}</div>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
uB += `</ul>`
|
||||||
|
}
|
||||||
|
return uB;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addItem(invite: Invite): void {
|
||||||
|
const links = document.getElementById('invites');
|
||||||
|
const container = document.createElement('div') as HTMLDivElement;
|
||||||
|
container.id = invite.code;
|
||||||
|
const item = document.createElement('div') as HTMLDivElement;
|
||||||
|
item.classList.add('list-group-item', 'd-flex', 'justify-content-between', 'd-inline-block');
|
||||||
|
let link = "";
|
||||||
|
let innerHTML = `<a>None</a>`;
|
||||||
|
if (invite.empty) {
|
||||||
|
item.innerHTML = `
|
||||||
|
<div class="d-flex align-items-center font-monospace" style="width: 40%;">
|
||||||
|
${innerHTML}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
container.appendChild(item);
|
||||||
|
links.appendChild(container);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
link = window.location.href.split('#')[0] + "invite/" + invite.code;
|
||||||
|
innerHTML = `
|
||||||
|
<div class="d-flex align-items-center font-monospace" style="width: 40%;">
|
||||||
|
<a class="invite-link" href="${link}">${invite.code.replace(/-/g, '-')}</a>
|
||||||
|
<i class="fa fa-clipboard icon-button" onclick="toClipboard('${link}')" style="margin-right: 0.5rem; margin-left: 0.5rem;"></i>
|
||||||
|
`;
|
||||||
|
if (invite.email) {
|
||||||
|
let email = invite.email;
|
||||||
|
if (!invite.email.includes("Failed to send to")) {
|
||||||
|
email = `Sent to ${email}`;
|
||||||
|
}
|
||||||
|
innerHTML += `
|
||||||
|
<span class="text-muted" style="margin-left: 0.4rem; font-style: italic; font-size: 0.8rem;">${email}</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
innerHTML += `
|
||||||
|
</div>
|
||||||
|
<div style="text-align: right;">
|
||||||
|
<span id="${invite.code}_expiry" style="margin-right: 1rem;">${invite.expiresIn}</span>
|
||||||
|
<div style="display: inline-block;">
|
||||||
|
<button class="btn btn-outline-danger" onclick="deleteInvite('${invite.code}')">Delete</button>
|
||||||
|
<i class="fa fa-angle-down collapsed icon-button not-rotated" style="padding: 1rem; margin: -1rem -1rem -1rem 0;" data-toggle="collapse" aria-expanded="false" data-target="#${CSS.escape(invite.code)}_collapse" onclick="rotateButton(this)"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
item.innerHTML = innerHTML;
|
||||||
|
container.appendChild(item);
|
||||||
|
|
||||||
|
let profiles = `
|
||||||
|
<label class="input-group-text" for="profile_${CSS.escape(invite.code)}">Profile: </label>
|
||||||
|
<select class="form-select" id="profile_${CSS.escape(invite.code)}" onchange="setProfile(this)">
|
||||||
|
<option value="NoProfile" selected>No Profile</option>
|
||||||
|
`;
|
||||||
|
for (const i in availableProfiles) {
|
||||||
|
let selected = "";
|
||||||
|
if (availableProfiles[i] == invite.profile) {
|
||||||
|
selected = "selected";
|
||||||
|
}
|
||||||
|
profiles += `<option value="${availableProfiles[i]}" ${selected}>${availableProfiles[i]}</option>`;
|
||||||
|
}
|
||||||
|
profiles += `</select>`;
|
||||||
|
|
||||||
|
let dateCreated: string;
|
||||||
|
if (invite.created) {
|
||||||
|
dateCreated = `<li class="list-group-item py-1">Created: ${invite.created}</li>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let middle: string;
|
||||||
|
if (notifications_enabled) {
|
||||||
|
middle = `
|
||||||
|
<div class="col" id="${CSS.escape(invite.code)}_notifyButtons">
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
Notify on:
|
||||||
|
<li class="list-group-item py-1 form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" value="" id="${CSS.escape(invite.code)}_notifyExpiry" onclick="setNotify(this)" ${invite.notifyExpiry ? "checked" : ""}>
|
||||||
|
<label class="form-check-label" for="${CSS.escape(invite.code)}_notifyExpiry">Expiry</label>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item py-1 form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" value="" id="${CSS.escape(invite.code)}_notifyCreation" onclick="setNotify(this)" ${invite.notifyCreation ? "checked" : ""}>
|
||||||
|
<label class="form-check-label" for="${CSS.escape(invite.code)}_notifyCreation">User creation</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let right: string = genUsedBy(invite.usedBy)
|
||||||
|
|
||||||
|
const dropdown = document.createElement('div') as HTMLDivElement;
|
||||||
|
dropdown.id = `${CSS.escape(invite.code)}_collapse`;
|
||||||
|
dropdown.classList.add("collapse");
|
||||||
|
dropdown.innerHTML = `
|
||||||
|
<div class="container row align-items-start card-body">
|
||||||
|
<div class="col">
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
<li class="input-group py-1">
|
||||||
|
${profiles}
|
||||||
|
</li>
|
||||||
|
${dateCreated}
|
||||||
|
<li class="list-group-item py-1" id="${CSS.escape(invite.code)}_remainingUses">Remaining uses: ${invite.remainingUses}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
${middle}
|
||||||
|
<div class="col" id="${CSS.escape(invite.code)}_usersCreated">
|
||||||
|
${right}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
container.appendChild(dropdown);
|
||||||
|
links.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateInvite(invite: Invite): void {
|
||||||
|
document.getElementById(invite.code + "_expiry").textContent = invite.expiresIn;
|
||||||
|
const remainingUses: any = document.getElementById(CSS.escape(invite.code) + "_remainingUses");
|
||||||
|
if (remainingUses) {
|
||||||
|
remainingUses.textContent = `Remaining uses: ${invite.remainingUses}`;
|
||||||
|
}
|
||||||
|
document.getElementById(CSS.escape(invite.code) + "_usersCreated").innerHTML = genUsedBy(invite.usedBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete invite from DOM
|
||||||
|
const hideInvite = (code: string): void => document.getElementById(CSS.escape(code)).remove();
|
||||||
|
|
||||||
|
// delete invite from jfa-go
|
||||||
|
const deleteInvite = (code: string): void => _post("/deleteInvite", { "code": code }, function (): void {
|
||||||
|
if (this.readyState == 4) {
|
||||||
|
generateInvites();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function generateInvites(empty?: boolean): void {
|
||||||
|
if (empty) {
|
||||||
|
document.getElementById('invites').textContent = '';
|
||||||
|
addItem(emptyInvite());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_get("/getInvites", null, function (): void {
|
||||||
|
if (this.readyState == 4) {
|
||||||
|
let data = this.response;
|
||||||
|
availableProfiles = data['profiles'];
|
||||||
|
const Profiles = document.getElementById('inviteProfile') as HTMLSelectElement;
|
||||||
|
let innerHTML = "";
|
||||||
|
for (let i = 0; i < availableProfiles.length; i++) {
|
||||||
|
const profile = availableProfiles[i];
|
||||||
|
innerHTML += `
|
||||||
|
<option value="${profile}" ${(i == 0) ? "selected" : ""}>${profile}</option>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
innerHTML += `
|
||||||
|
<option value="NoProfile" ${(availableProfiles.length == 0) ? "selected" : ""}>No Profile</option>
|
||||||
|
`;
|
||||||
|
Profiles.innerHTML = innerHTML;
|
||||||
|
if (data['invites'] == null || data['invites'].length == 0) {
|
||||||
|
document.getElementById('invites').textContent = '';
|
||||||
|
addItem(emptyInvite());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let items = document.getElementById('invites').children;
|
||||||
|
for (const i in data['invites']) {
|
||||||
|
let match = false;
|
||||||
|
const inv = parseInvite(data['invites'][i]);
|
||||||
|
for (const x in items) {
|
||||||
|
if (items[x].id == inv.code) {
|
||||||
|
match = true;
|
||||||
|
updateInvite(inv);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!match) {
|
||||||
|
addItem(inv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// second pass to check for expired invites
|
||||||
|
items = document.getElementById('invites').children;
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
let exists = false;
|
||||||
|
for (const x in data['invites']) {
|
||||||
|
if (items[i].id == data['invites'][x]['code']) {
|
||||||
|
exists = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!exists) {
|
||||||
|
hideInvite(items[i].id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const addOptions = (length: number, el: HTMLSelectElement): void => {
|
||||||
|
for (let v = 0; v <= length; v++) {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.textContent = ""+v;
|
||||||
|
opt.value = ""+v;
|
||||||
|
el.appendChild(opt);
|
||||||
|
}
|
||||||
|
el.value = "0";
|
||||||
|
};
|
||||||
|
|
||||||
|
function fixCheckboxes(): void {
|
||||||
|
const send_to_address: Array<HTMLInputElement> = [document.getElementById('send_to_address') as HTMLInputElement, document.getElementById('send_to_address_enabled') as HTMLInputElement];
|
||||||
|
if (send_to_address[0] != null) {
|
||||||
|
send_to_address[0].disabled = !send_to_address[1].checked;
|
||||||
|
}
|
||||||
|
const multiUseEnabled = document.getElementById('multiUseEnabled') as HTMLInputElement;
|
||||||
|
const multiUseCount = document.getElementById('multiUseCount') as HTMLInputElement;
|
||||||
|
const noUseLimit = document.getElementById('noUseLimit') as HTMLInputElement;
|
||||||
|
multiUseCount.disabled = !multiUseEnabled.checked;
|
||||||
|
noUseLimit.checked = false;
|
||||||
|
noUseLimit.disabled = !multiUseEnabled.checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
fixCheckboxes();
|
||||||
|
|
||||||
|
(document.getElementById('inviteForm') as HTMLFormElement).onsubmit = function (): boolean {
|
||||||
|
const button = document.getElementById('generateSubmit') as HTMLButtonElement;
|
||||||
|
button.disabled = true;
|
||||||
|
button.innerHTML = `
|
||||||
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>
|
||||||
|
Loading...`;
|
||||||
|
let send = serializeForm('inviteForm');
|
||||||
|
send["remaining-uses"] = +send["remaining-uses"];
|
||||||
|
if (!send['multiple-uses'] || send['no-limit']) {
|
||||||
|
delete send['remaining-uses'];
|
||||||
|
}
|
||||||
|
if (send["profile"] == "NoProfile") {
|
||||||
|
send["profile"] = "";
|
||||||
|
}
|
||||||
|
const sendToAddress: any = document.getElementById('send_to_address');
|
||||||
|
const sendToAddressEnabled: any = document.getElementById('send_to_address_enabled');
|
||||||
|
if (sendToAddress && sendToAddressEnabled) {
|
||||||
|
send['email'] = send['send_to_address'];
|
||||||
|
delete send['send_to_address'];
|
||||||
|
delete send['send_to_address_enabled'];
|
||||||
|
}
|
||||||
|
console.log(send);
|
||||||
|
_post("/generateInvite", send, function (): void {
|
||||||
|
if (this.readyState == 4) {
|
||||||
|
button.textContent = 'Generate';
|
||||||
|
button.disabled = false;
|
||||||
|
generateInvites();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
triggerTooltips();
|
||||||
|
|
||||||
|
function setProfile(select: HTMLSelectElement): void {
|
||||||
|
if (!select.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let val = select.value;
|
||||||
|
if (select.value == "NoProfile") {
|
||||||
|
val = ""
|
||||||
|
}
|
||||||
|
const invite = select.id.replace("profile_", "");
|
||||||
|
const send = {
|
||||||
|
"invite": invite,
|
||||||
|
"profile": val
|
||||||
|
};
|
||||||
|
_post("/setProfile", send, function (): void {
|
||||||
|
if (this.readyState == 4 && this.status != 200) {
|
||||||
|
generateInvites();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkDuration(): void {
|
||||||
|
const boxVals: Array<number> = [+(document.getElementById("days") as HTMLSelectElement).value, +(document.getElementById("hours") as HTMLSelectElement).value, +(document.getElementById("minutes") as HTMLSelectElement).value];
|
||||||
|
const submit = document.getElementById('generateSubmit') as HTMLButtonElement;
|
||||||
|
if (boxVals.reduce((a: number, b: number): number => a + b) == 0) {
|
||||||
|
submit.disabled = true;
|
||||||
|
} else {
|
||||||
|
submit.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nE: Array<string> = ["days", "hours", "minutes"];
|
||||||
|
for (const i in nE) {
|
||||||
|
document.getElementById(nE[i]).addEventListener("change", checkDuration);
|
||||||
|
}
|
83
ts/ombi.ts
Normal file
83
ts/ombi.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
const ombiDefaultsModal = createModal('ombiDefaults');
|
||||||
|
(document.getElementById('openOmbiDefaults') as HTMLButtonElement).onclick = function (): void {
|
||||||
|
let button = this as HTMLButtonElement;
|
||||||
|
button.disabled = true;
|
||||||
|
const ogHTML = button.innerHTML;
|
||||||
|
button.innerHTML =
|
||||||
|
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
||||||
|
'Loading...';
|
||||||
|
_get("/getOmbiUsers", null, function (): void {
|
||||||
|
if (this.readyState == 4) {
|
||||||
|
if (this.status == 200) {
|
||||||
|
const users = this.response['users'];
|
||||||
|
const radioList = document.getElementById('ombiUserRadios');
|
||||||
|
radioList.textContent = '';
|
||||||
|
let first = true;
|
||||||
|
for (const i in users) {
|
||||||
|
const user = users[i];
|
||||||
|
const radio = document.createElement('div') as HTMLDivElement;
|
||||||
|
radio.classList.add('form-check');
|
||||||
|
let checked = '';
|
||||||
|
if (first) {
|
||||||
|
checked = 'checked';
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
radio.innerHTML = `
|
||||||
|
<input class="form-check-input" type="radio" name="ombiRadios" id="ombiDefault_${user['id']}" ${checked}>
|
||||||
|
<label class="form-check-label" for="ombiDefault_${user['id']}">${user['name']}</label>
|
||||||
|
`;
|
||||||
|
radioList.appendChild(radio);
|
||||||
|
}
|
||||||
|
button.disabled = false;
|
||||||
|
button.innerHTML = ogHTML;
|
||||||
|
const submitButton = document.getElementById('storeOmbiDefaults') as HTMLButtonElement;
|
||||||
|
submitButton.disabled = false;
|
||||||
|
submitButton.textContent = 'Submit';
|
||||||
|
addAttr(submitButton, "btn-primary");
|
||||||
|
rmAttr(submitButton, "btn-success");
|
||||||
|
rmAttr(submitButton, "btn-danger");
|
||||||
|
settingsModal.hide();
|
||||||
|
ombiDefaultsModal.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
(document.getElementById('storeOmbiDefaults') as HTMLButtonElement).onclick = function (): void {
|
||||||
|
let button = this as HTMLButtonElement;
|
||||||
|
button.disabled = true;
|
||||||
|
const ogHTML = button.innerHTML;
|
||||||
|
button.innerHTML =
|
||||||
|
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
||||||
|
'Loading...';
|
||||||
|
const radio = document.querySelector('input[name=ombiRadios]:checked') as HTMLInputElement;
|
||||||
|
const data = {
|
||||||
|
"id": radio.id.replace("ombiDefault_", "")
|
||||||
|
};
|
||||||
|
_post("/setOmbiDefaults", data, function (): void {
|
||||||
|
if (this.readyState == 4) {
|
||||||
|
if (this.status == 200 || this.status == 204) {
|
||||||
|
button.textContent = "Success";
|
||||||
|
addAttr(button, "btn-success");
|
||||||
|
rmAttr(button, "btn-danger");
|
||||||
|
rmAttr(button, "btn-primary");
|
||||||
|
button.disabled = false;
|
||||||
|
setTimeout((): void => ombiDefaultsModal.hide(), 1000);
|
||||||
|
} else {
|
||||||
|
button.textContent = "Failed";
|
||||||
|
rmAttr(button, "btn-primary");
|
||||||
|
addAttr(button, "btn-danger");
|
||||||
|
setTimeout((): void => {
|
||||||
|
button.textContent = "Submit";
|
||||||
|
addAttr(button, "btn-primary");
|
||||||
|
rmAttr(button, "btn-danger");
|
||||||
|
button.disabled = false;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
35
ts/serialize.ts
Normal file
35
ts/serialize.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
function serializeForm(id: string): Object {
|
||||||
|
const form = document.getElementById(id) as HTMLFormElement;
|
||||||
|
let formData = {};
|
||||||
|
for (let i = 0; i < form.elements.length; i++) {
|
||||||
|
const el = form.elements[i];
|
||||||
|
if ((el as HTMLInputElement).type == "submit") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let name = (el as HTMLInputElement).name;
|
||||||
|
if (!name) {
|
||||||
|
name = el.id;
|
||||||
|
}
|
||||||
|
switch ((el as HTMLInputElement).type) {
|
||||||
|
case "checkbox":
|
||||||
|
formData[name] = (el as HTMLInputElement).checked;
|
||||||
|
break;
|
||||||
|
case "text":
|
||||||
|
case "password":
|
||||||
|
case "email":
|
||||||
|
case "number":
|
||||||
|
formData[name] = (el as HTMLInputElement).value;
|
||||||
|
break;
|
||||||
|
case "select-one":
|
||||||
|
case "select":
|
||||||
|
let val: string = (el as HTMLSelectElement).value.toString();
|
||||||
|
if (!isNaN(val as any)) {
|
||||||
|
formData[name] = +val;
|
||||||
|
} else {
|
||||||
|
formData[name] = val;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return formData;
|
||||||
|
}
|
206
ts/settings.ts
Normal file
206
ts/settings.ts
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
var config: Object = {};
|
||||||
|
var modifiedConfig: Object = {};
|
||||||
|
|
||||||
|
function sendConfig(modalID: string, restart?: boolean): void {
|
||||||
|
modifiedConfig["restart-program"] = restart;
|
||||||
|
_post("/modifyConfig", modifiedConfig, function (): void {
|
||||||
|
if (this.readyState == 4) {
|
||||||
|
if (this.status == 200 || this.status == 204) {
|
||||||
|
createModal(modalID, true).hide();
|
||||||
|
if (modalID != "settingsMenu") {
|
||||||
|
settingsModal.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (restart) {
|
||||||
|
refreshModal.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
(document.getElementById('openDefaultsWizard') as HTMLButtonElement).onclick = function (): void {
|
||||||
|
const button = this as HTMLButtonElement;
|
||||||
|
button.disabled = true;
|
||||||
|
const ogHTML = button.innerHTML;
|
||||||
|
button.innerHTML = `
|
||||||
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>
|
||||||
|
Loading...`;
|
||||||
|
_get("/getUsers", null, function (): void {
|
||||||
|
if (this.readyState == 4) {
|
||||||
|
if (this.status == 200) {
|
||||||
|
jfUsers = this.response['users'];
|
||||||
|
populateRadios();
|
||||||
|
button.disabled = false;
|
||||||
|
button.innerHTML = ogHTML;
|
||||||
|
const submitButton = document.getElementById('storeDefaults') as HTMLButtonElement;
|
||||||
|
submitButton.disabled = false;
|
||||||
|
submitButton.textContent = 'Submit';
|
||||||
|
addAttr(submitButton, "btn-primary");
|
||||||
|
rmAttr(submitButton, "btn-danger");
|
||||||
|
rmAttr(submitButton, "btn-success");
|
||||||
|
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') as HTMLSelectElement).value = 'fromUser';
|
||||||
|
document.getElementById('defaultsSourceSection').classList.add('unfocused');
|
||||||
|
(document.getElementById('storeDefaults') as HTMLButtonElement).onclick = (): void => storeDefaults('all');
|
||||||
|
Focus(document.getElementById('defaultUserRadios'));
|
||||||
|
settingsModal.hide();
|
||||||
|
userDefaultsModal.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
(document.getElementById('openAbout') as HTMLButtonElement).onclick = (): void => {
|
||||||
|
settingsModal.hide();
|
||||||
|
aboutModal.show();
|
||||||
|
};
|
||||||
|
|
||||||
|
(document.getElementById('openSettings') as HTMLButtonElement).onclick = (): void => _get("/getConfig", null, function (): void {
|
||||||
|
if (this.readyState == 4 && this.status == 200) {
|
||||||
|
const settingsList = document.getElementById('settingsList');
|
||||||
|
settingsList.textContent = '';
|
||||||
|
config = this.response;
|
||||||
|
for (const i in config["order"]) {
|
||||||
|
const section: string = config["order"][i]
|
||||||
|
const sectionCollapse = document.createElement('div') as HTMLDivElement;
|
||||||
|
addAttr(sectionCollapse, "collapse");
|
||||||
|
sectionCollapse.id = section;
|
||||||
|
|
||||||
|
const title: string = config[section]["meta"]["name"];
|
||||||
|
const description: string = config[section]["meta"]["description"];
|
||||||
|
const entryListID: string = `${section}_entryList`;
|
||||||
|
// const footerID: string = `${section}_footer`;
|
||||||
|
|
||||||
|
sectionCollapse.innerHTML = `
|
||||||
|
<div class="card card-body">
|
||||||
|
<small class="text-muted">${description}</small>
|
||||||
|
<div class="${entryListID}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
for (const x in config[section]["order"]) {
|
||||||
|
const entry: string = config[section]["order"][x];
|
||||||
|
if (entry == "meta") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let entryName: string = config[section][entry]["name"];
|
||||||
|
let required = false;
|
||||||
|
if (config[section][entry]["required"]) {
|
||||||
|
entryName += ` <sup class="text-danger">*</sup>`;
|
||||||
|
required = true;
|
||||||
|
}
|
||||||
|
if (config[section][entry]["requires_restart"]) {
|
||||||
|
entryName += ` <sup class="text-danger">R</sup>`;
|
||||||
|
}
|
||||||
|
if ("description" in config[section][entry]) {
|
||||||
|
entryName +=`
|
||||||
|
<a class="text-muted" href="#" data-toggle="tooltip" data-placement="right" title="${config[section][entry]['description']}"><i class="fa fa-question-circle-o"></i></a>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
const entryValue: boolean | string = config[section][entry]["value"];
|
||||||
|
const entryType: string = config[section][entry]["type"];
|
||||||
|
const entryGroup = document.createElement('div');
|
||||||
|
if (entryType == "bool") {
|
||||||
|
entryGroup.classList.add("form-check");
|
||||||
|
entryGroup.innerHTML = `
|
||||||
|
<input class="form-check-input" type="checkbox" value="" id="${section}_${entry}" ${(entryValue as boolean) ? 'checked': ''} ${required ? 'required' : ''}>
|
||||||
|
<label class="form-check-label" for="${section}_${entry}">${entryName}</label>
|
||||||
|
`;
|
||||||
|
(entryGroup.querySelector('input[type=checkbox]') as HTMLInputElement).onclick = function (): void {
|
||||||
|
const me = this as HTMLInputElement;
|
||||||
|
for (const y in config["order"]) {
|
||||||
|
const sect: string = config["order"][y];
|
||||||
|
for (const z in config[sect]["order"]) {
|
||||||
|
const ent: string = config[sect]["order"][z];
|
||||||
|
if (`${sect}_${config[sect][ent]['depends_true']}` == me.id) {
|
||||||
|
(document.getElementById(`${sect}_${ent}`) as HTMLInputElement).disabled = !(me.checked);
|
||||||
|
} else if (`${sect}_${config[sect][ent]['depends_false']}` == me.id) {
|
||||||
|
(document.getElementById(`${sect}_${ent}`) as HTMLInputElement).disabled = me.checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else if ((entryType == 'text') || (entryType == 'email') || (entryType == 'password') || (entryType == 'number')) {
|
||||||
|
entryGroup.classList.add("form-group");
|
||||||
|
entryGroup.innerHTML = `
|
||||||
|
<label for="${section}_${entry}">${entryName}</label>
|
||||||
|
<input type="${entryType}" class="form-control" id="${section}_${entry}" aria-describedby="${entry}" value="${entryValue}" ${required ? 'required' : ''}>
|
||||||
|
`;
|
||||||
|
} else if (entryType == 'select') {
|
||||||
|
entryGroup.classList.add("form-group");
|
||||||
|
const entryOptions: Array<string> = config[section][entry]["options"];
|
||||||
|
let innerGroup = `
|
||||||
|
<label for="${section}_${entry}">${entryName}</label>
|
||||||
|
<select class="form-control" id="${section}_${entry}" ${required ? 'required' : ''}>
|
||||||
|
`;
|
||||||
|
for (const z in entryOptions) {
|
||||||
|
const entryOption = entryOptions[z];
|
||||||
|
let selected: boolean = (entryOption == entryValue);
|
||||||
|
innerGroup += `
|
||||||
|
<option value="${entryOption}" ${selected ? 'selected' : ''}>${entryOption}</option>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
innerGroup += `</select>`;
|
||||||
|
entryGroup.innerHTML = innerGroup;
|
||||||
|
}
|
||||||
|
sectionCollapse.getElementsByClassName(entryListID)[0].appendChild(entryGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsList.innerHTML += `
|
||||||
|
<button type="button" class="list-group-item list-group-item-action" id="${section}_button" data-toggle="collapse" data-target="#${section}">${title}</button>
|
||||||
|
`;
|
||||||
|
settingsList.appendChild(sectionCollapse);
|
||||||
|
}
|
||||||
|
settingsModal.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
(document.getElementById('settingsSave') as HTMLButtonElement).onclick = function (): void {
|
||||||
|
modifiedConfig = {};
|
||||||
|
let restartSettingsChanged = false;
|
||||||
|
let settingsChanged = false;
|
||||||
|
for (const i in config["order"]) {
|
||||||
|
const section = config["order"][i];
|
||||||
|
for (const x in config[section]["order"]) {
|
||||||
|
const entry = config[section]["order"][x];
|
||||||
|
if (entry == "meta") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let val: string;
|
||||||
|
const entryID = `${section}_${entry}`;
|
||||||
|
const el = document.getElementById(entryID) as HTMLInputElement;
|
||||||
|
if (el.type == "checkbox") {
|
||||||
|
val = el.checked.toString();
|
||||||
|
} else {
|
||||||
|
val = el.value.toString();
|
||||||
|
}
|
||||||
|
if (val != config[section][entry]["value"]) {
|
||||||
|
if (!(section in modifiedConfig)) {
|
||||||
|
modifiedConfig[section] = {};
|
||||||
|
}
|
||||||
|
modifiedConfig[section][entry] = val;
|
||||||
|
settingsChanged = true;
|
||||||
|
if (config[section][entry]["requires_restart"]) {
|
||||||
|
restartSettingsChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (restartSettingsChanged) {
|
||||||
|
(document.getElementById('applyRestarts') as HTMLButtonElement).onclick = (): void => sendConfig("restartModal");
|
||||||
|
const restartButton = document.getElementById('applyAndRestart') as HTMLButtonElement;
|
||||||
|
if (restartButton) {
|
||||||
|
restartButton.onclick = (): void => sendConfig("restartModal", true);
|
||||||
|
}
|
||||||
|
settingsModal.hide();
|
||||||
|
restartModal.show();
|
||||||
|
} else if (settingsChanged) {
|
||||||
|
sendConfig("settingsMenu");
|
||||||
|
} else {
|
||||||
|
settingsModal.hide();
|
||||||
|
}
|
||||||
|
};
|
8
ts/tsconfig.json
Normal file
8
ts/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../data/static",
|
||||||
|
"target": "es6",
|
||||||
|
"lib": ["dom", "es2017"],
|
||||||
|
"types": ["jquery"]
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user