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