mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-29 12:30:11 +00:00
Compare commits
8 Commits
7d84fdec96
...
35a0be6a2c
Author | SHA1 | Date | |
---|---|---|---|
35a0be6a2c | |||
6b81358cd2 | |||
d4b94bc9d9 | |||
9213f2a078 | |||
2b84e45b65 | |||
7ac750879f | |||
cd61989495 | |||
a8b4842895 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,6 +4,7 @@ mail/*.html
|
|||||||
scss/*.css*
|
scss/*.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,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -179,6 +180,58 @@ 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)
|
||||||
@ -273,6 +326,51 @@ 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"`
|
||||||
@ -458,13 +556,29 @@ func (app *appContext) DeleteInvite(gc *gin.Context) {
|
|||||||
respond(401, "Code doesn't exist", gc)
|
respond(401, "Code doesn't exist", gc)
|
||||||
}
|
}
|
||||||
|
|
||||||
type userResp struct {
|
type dateToParse struct {
|
||||||
UserList []respUser `json:"users"`
|
Parsed time.Time `json:"parseme"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// json magically parses datetimes so why not
|
||||||
|
func parseDt(date string) time.Time {
|
||||||
|
timeJSON := []byte("{ \"parseme\": \"" + date + "\" }")
|
||||||
|
var parsed dateToParse
|
||||||
|
json.Unmarshal(timeJSON, &parsed)
|
||||||
|
return parsed.Parsed
|
||||||
}
|
}
|
||||||
|
|
||||||
type respUser struct {
|
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) {
|
||||||
@ -480,10 +594,18 @@ 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)
|
||||||
@ -516,6 +638,7 @@ 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 {
|
||||||
@ -525,7 +648,7 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, jfUser := range users {
|
for _, jfUser := range users {
|
||||||
if address, ok := req[jfUser["Name"].(string)]; ok {
|
if address, ok := req[jfUser["Id"].(string)]; ok {
|
||||||
app.storage.emails[jfUser["Id"].(string)] = address
|
app.storage.emails[jfUser["Id"].(string)] = address
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -550,22 +673,24 @@ func (app *appContext) SetOmbiDefaults(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type defaultsReq struct {
|
type defaultsReq struct {
|
||||||
Username string `json:"username"`
|
From string `json:"from"`
|
||||||
|
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)
|
||||||
app.info.Printf("Getting user defaults from \"%s\"", req.Username)
|
userID := req.ID
|
||||||
user, status, err := app.jf.userByName(req.Username, false)
|
user, status, err := app.jf.userById(userID, 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
|
||||||
}
|
}
|
||||||
userID := user["Id"].(string)
|
app.info.Printf("Getting user defaults from \"%s\"", user["Name"].(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()
|
||||||
@ -590,6 +715,81 @@ 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,6 +69,9 @@ func (app *appContext) loadConfig() error {
|
|||||||
app.config.Section("notifications").Key("created_html").SetValue(app.config.Section("notifications").Key("created_html").MustString(filepath.Join(app.local_path, "created.html")))
|
app.config.Section("notifications").Key("created_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,6 +543,36 @@
|
|||||||
"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,6 +10,7 @@
|
|||||||
"mailgun",
|
"mailgun",
|
||||||
"smtp",
|
"smtp",
|
||||||
"ombi",
|
"ombi",
|
||||||
|
"deletion",
|
||||||
"files"
|
"files"
|
||||||
],
|
],
|
||||||
"jellyfin": {
|
"jellyfin": {
|
||||||
@ -634,6 +635,41 @@
|
|||||||
"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",
|
||||||
|
363
data/static/accounts.js
Normal file
363
data/static/accounts.js
Normal file
@ -0,0 +1,363 @@
|
|||||||
|
document.getElementById('selectAll').onclick = function() {
|
||||||
|
const checkboxes = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]');
|
||||||
|
for (check of checkboxes) {
|
||||||
|
check.checked = this.checked;
|
||||||
|
}
|
||||||
|
checkCheckboxes();
|
||||||
|
};
|
||||||
|
|
||||||
|
function checkCheckboxes() {
|
||||||
|
const defaultsButton = document.getElementById('accountsTabSetDefaults');
|
||||||
|
const deleteButton = document.getElementById('accountsTabDelete');
|
||||||
|
const checkboxes = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]');
|
||||||
|
let checked = 0;
|
||||||
|
for (check of checkboxes) {
|
||||||
|
if (check.checked) {
|
||||||
|
checked++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (checked == 0) {
|
||||||
|
defaultsButton.classList.add('unfocused');
|
||||||
|
deleteButton.classList.add('unfocused');
|
||||||
|
} else {
|
||||||
|
if (defaultsButton.classList.contains('unfocused')) {
|
||||||
|
defaultsButton.classList.remove('unfocused');
|
||||||
|
}
|
||||||
|
if (deleteButton.classList.contains('unfocused')) {
|
||||||
|
deleteButton.classList.remove('unfocused');
|
||||||
|
}
|
||||||
|
if (checked == 1) {
|
||||||
|
deleteButton.textContent = 'Delete User';
|
||||||
|
} else {
|
||||||
|
deleteButton.textContent = 'Delete Users';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('deleteModalNotify').onclick = function() {
|
||||||
|
const textbox = document.getElementById('deleteModalReasonBox');
|
||||||
|
if (this.checked && textbox.classList.contains('unfocused')) {
|
||||||
|
textbox.classList.remove('unfocused');
|
||||||
|
} else if (!this.checked) {
|
||||||
|
textbox.classList.add('unfocused');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('accountsTabDelete').onclick = function() {
|
||||||
|
const deleteButton = this;
|
||||||
|
let selected = [];
|
||||||
|
const checkboxes = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]');
|
||||||
|
for (check of checkboxes) {
|
||||||
|
if (check.checked) {
|
||||||
|
selected.push(check.id.replace('select_', ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let title = " user";
|
||||||
|
if (selected.length > 1) {
|
||||||
|
title += "s";
|
||||||
|
}
|
||||||
|
title = "Delete " + selected.length + title;
|
||||||
|
document.getElementById('deleteModalTitle').textContent = title;
|
||||||
|
document.getElementById('deleteModalNotify').checked = false;
|
||||||
|
document.getElementById('deleteModalReason').value = '';
|
||||||
|
document.getElementById('deleteModalReasonBox').classList.add('unfocused');
|
||||||
|
document.getElementById('deleteModalSend').textContent = 'Delete';
|
||||||
|
|
||||||
|
document.getElementById('deleteModalSend').onclick = function() {
|
||||||
|
const button = this;
|
||||||
|
const send = {
|
||||||
|
'users': selected,
|
||||||
|
'notify': document.getElementById('deleteModalNotify').checked,
|
||||||
|
'reason': document.getElementById('deleteModalReason').value
|
||||||
|
};
|
||||||
|
let req = new XMLHttpRequest();
|
||||||
|
req.open("POST", "/deleteUser", true);
|
||||||
|
req.responseType = 'json';
|
||||||
|
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||||
|
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||||
|
req.onreadystatechange = function() {
|
||||||
|
if (this.readyState == 4) {
|
||||||
|
if (this.status == 500) {
|
||||||
|
if ("error" in req.response) {
|
||||||
|
button.textContent = 'Failed';
|
||||||
|
} else {
|
||||||
|
button.textContent = 'Partial fail (check console)';
|
||||||
|
console.log(req.response);
|
||||||
|
}
|
||||||
|
setTimeout(function() {
|
||||||
|
deleteModal.hide();
|
||||||
|
deleteButton.classList.add('unfocused');
|
||||||
|
}, 4000);
|
||||||
|
} else {
|
||||||
|
deleteButton.classList.add('unfocused');
|
||||||
|
deleteModal.hide();
|
||||||
|
}
|
||||||
|
populateUsers();
|
||||||
|
checkCheckboxes();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
req.send(JSON.stringify(send));
|
||||||
|
};
|
||||||
|
deleteModal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
var jfUsers = [];
|
||||||
|
|
||||||
|
function validEmail(email) {
|
||||||
|
const re = /\S+@\S+\.\S+/;
|
||||||
|
return re.test(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeEmail(icon, id) {
|
||||||
|
const iconContent = icon.outerHTML;
|
||||||
|
icon.setAttribute("class", "");
|
||||||
|
const entry = icon.nextElementSibling;
|
||||||
|
const ogEmail = entry.value;
|
||||||
|
entry.readOnly = false;
|
||||||
|
entry.classList.remove('form-control-plaintext');
|
||||||
|
entry.classList.add('form-control');
|
||||||
|
if (entry.value == "") {
|
||||||
|
entry.placeholder = 'Address';
|
||||||
|
}
|
||||||
|
const tick = document.createElement('i');
|
||||||
|
tick.classList.add("fa", "fa-check", "d-inline-block", "icon-button", "text-success");
|
||||||
|
tick.setAttribute('style', 'margin-left: 0.5rem; margin-right: 0.5rem;');
|
||||||
|
tick.onclick = function() {
|
||||||
|
const newEmail = entry.value;
|
||||||
|
if (!validEmail(newEmail) || newEmail == ogEmail) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cross.remove();
|
||||||
|
this.outerHTML = `
|
||||||
|
<div class="spinner-border spinner-border-sm" role="status" style="width: 1rem; height: 1rem; margin-left: 0.5rem;">
|
||||||
|
<span class="sr-only">Saving...</span>
|
||||||
|
</div>`;
|
||||||
|
//this.remove();
|
||||||
|
let send = {};
|
||||||
|
send[id] = newEmail;
|
||||||
|
let req = new XMLHttpRequest();
|
||||||
|
req.open("POST", "/modifyEmails", true);
|
||||||
|
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||||
|
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||||
|
req.onreadystatechange = function() {
|
||||||
|
if (this.readyState == 4) {
|
||||||
|
if (this.status == 200 || this.status == 204) {
|
||||||
|
entry.nextElementSibling.remove();
|
||||||
|
} else {
|
||||||
|
entry.value = ogEmail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
req.send(JSON.stringify(send));
|
||||||
|
icon.outerHTML = iconContent;
|
||||||
|
entry.readOnly = true;
|
||||||
|
entry.classList.remove('form-control');
|
||||||
|
entry.classList.add('form-control-plaintext');
|
||||||
|
entry.placeholder = '';
|
||||||
|
};
|
||||||
|
const cross = document.createElement('i');
|
||||||
|
cross.classList.add("fa", "fa-close", "d-inline-block", "icon-button", "text-danger");
|
||||||
|
cross.onclick = function() {
|
||||||
|
tick.remove();
|
||||||
|
this.remove();
|
||||||
|
icon.outerHTML = iconContent;
|
||||||
|
entry.readOnly = true;
|
||||||
|
entry.classList.remove('form-control');
|
||||||
|
entry.classList.add('form-control-plaintext');
|
||||||
|
entry.placeholder = '';
|
||||||
|
entry.value = ogEmail;
|
||||||
|
};
|
||||||
|
icon.parentNode.appendChild(tick);
|
||||||
|
icon.parentNode.appendChild(cross);
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateUsers() {
|
||||||
|
const acList = document.getElementById('accountsList');
|
||||||
|
acList.innerHTML = `
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<strong>Getting Users...</strong>
|
||||||
|
<div class="spinner-border ml-auto" role="status" aria-hidden="true"></div>
|
||||||
|
</div>`;
|
||||||
|
acList.parentNode.querySelector('thead').classList.add('unfocused');
|
||||||
|
const accountsList = document.createElement('tbody');
|
||||||
|
accountsList.id = 'accountsList';
|
||||||
|
const generateEmail = function(id, name, email) {
|
||||||
|
let entry = document.createElement('div');
|
||||||
|
// entry.classList.add('py-1');
|
||||||
|
entry.id = 'email_' + id;
|
||||||
|
let emailValue = email;
|
||||||
|
if (email === undefined) {
|
||||||
|
emailValue = "";
|
||||||
|
}
|
||||||
|
entry.innerHTML = `
|
||||||
|
<i class="fa fa-edit d-inline-block icon-button" style="margin-right: 2%;" onclick="changeEmail(this, '${id}')"></i>
|
||||||
|
<input type="email" class="form-control-plaintext form-control-sm text-muted d-inline-block addressText" id="address_${id}" style="width: auto;" value="${emailValue}" readonly>
|
||||||
|
`;
|
||||||
|
return entry.outerHTML
|
||||||
|
};
|
||||||
|
const template = function(id, username, email, lastActive, admin) {
|
||||||
|
let isAdmin = "No";
|
||||||
|
if (admin) {
|
||||||
|
isAdmin = "Yes";
|
||||||
|
}
|
||||||
|
return `
|
||||||
|
<td nowrap="nowrap" class="align-middle" scope="row"><input class="form-check-input" type="checkbox" value="" id="select_${id}" onclick="checkCheckboxes();"></td>
|
||||||
|
<td nowrap="nowrap" class="align-middle">${username}</td>
|
||||||
|
<td nowrap="nowrap" class="align-middle">${generateEmail(id, name, email)}</td>
|
||||||
|
<td nowrap="nowrap" class="align-middle">${lastActive}</td>
|
||||||
|
<td nowrap="nowrap" class="align-middle">${isAdmin}</td>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
let req = new XMLHttpRequest();
|
||||||
|
req.responseType = 'json';
|
||||||
|
req.open("GET", "/getUsers", true);
|
||||||
|
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||||
|
req.onreadystatechange = function() {
|
||||||
|
if (this.readyState == 4) {
|
||||||
|
if (this.status == 200) {
|
||||||
|
jfUsers = req.response['users'];
|
||||||
|
for (user of jfUsers) {
|
||||||
|
let tr = document.createElement('tr');
|
||||||
|
tr.innerHTML = template(user['id'], user['name'], user['email'], user['last_active'], user['admin']);
|
||||||
|
accountsList.appendChild(tr);
|
||||||
|
}
|
||||||
|
const header = acList.parentNode.querySelector('thead');
|
||||||
|
if (header.classList.contains('unfocused')) {
|
||||||
|
header.classList.remove('unfocused');
|
||||||
|
}
|
||||||
|
acList.replaceWith(accountsList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
req.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('selectAll').checked = false;
|
||||||
|
|
||||||
|
document.getElementById('accountsTabSetDefaults').onclick = function() {
|
||||||
|
const checkboxes = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]');
|
||||||
|
let userIDs = [];
|
||||||
|
for (check of checkboxes) {
|
||||||
|
if (check.checked) {
|
||||||
|
userIDs.push(check.id.replace('select_', ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (userIDs.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let radioList = document.getElementById('defaultUserRadios');
|
||||||
|
radioList.textContent = '';
|
||||||
|
let first = true;
|
||||||
|
for (user of jfUsers) {
|
||||||
|
let radio = document.createElement('div');
|
||||||
|
radio.classList.add('radio');
|
||||||
|
let checked = 'checked';
|
||||||
|
if (first) {
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
checked = '';
|
||||||
|
}
|
||||||
|
radio.innerHTML = `
|
||||||
|
<label><input type="radio" name="defaultRadios" id="default_${user['id']}" style="margin-right: 1rem;" ${checked}>${user['name']}</label>`;
|
||||||
|
radioList.appendChild(radio);
|
||||||
|
}
|
||||||
|
let userstring = 'user';
|
||||||
|
if (userIDs.length > 1) {
|
||||||
|
userstring += 's';
|
||||||
|
}
|
||||||
|
document.getElementById('defaultsTitle').textContent = `Apply settings to ${userIDs.length} ${userstring}`;
|
||||||
|
document.getElementById('userDefaultsDescription').textContent = `
|
||||||
|
Create an account and configure it to your liking, then choose it from below to apply to your selected users.`;
|
||||||
|
document.getElementById('storeHomescreenLabel').textContent = `Apply homescreen layout`;
|
||||||
|
if (document.getElementById('defaultsSourceSection').classList.contains('unfocused')) {
|
||||||
|
document.getElementById('defaultsSourceSection').classList.remove('unfocused');
|
||||||
|
}
|
||||||
|
document.getElementById('defaultsSource').value = 'userTemplate';
|
||||||
|
document.getElementById('defaultUserRadios').classList.add('unfocused');
|
||||||
|
document.getElementById('storeDefaults').onclick = function() {
|
||||||
|
storeDefaults(userIDs);
|
||||||
|
};
|
||||||
|
userDefaultsModal.show();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('defaultsSource').addEventListener('change', function() {
|
||||||
|
const radios = document.getElementById('defaultUserRadios');
|
||||||
|
if (this.value == 'userTemplate') {
|
||||||
|
radios.classList.add('unfocused');
|
||||||
|
} else if (radios.classList.contains('unfocused')) {
|
||||||
|
radios.classList.remove('unfocused');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
document.getElementById('newUserCreate').onclick = function() {
|
||||||
|
const ogText = this.textContent;
|
||||||
|
this.innerHTML = `
|
||||||
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Creating...`;
|
||||||
|
const email = document.getElementById('newUserEmail').value;
|
||||||
|
var username = email;
|
||||||
|
if (document.getElementById('newUserName') != null) {
|
||||||
|
username = document.getElementById('newUserName').value;
|
||||||
|
}
|
||||||
|
const password = document.getElementById('newUserPassword').value;
|
||||||
|
if (!validEmail(email) && email != "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const send = {
|
||||||
|
'username': username,
|
||||||
|
'password': password,
|
||||||
|
'email': email
|
||||||
|
}
|
||||||
|
let req = new XMLHttpRequest()
|
||||||
|
req.open("POST", "/newUserAdmin", true);
|
||||||
|
req.responseType = 'json';
|
||||||
|
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||||
|
const button = this;
|
||||||
|
req.onreadystatechange = function() {
|
||||||
|
if (this.readyState == 4) {
|
||||||
|
button.textContent = ogText;
|
||||||
|
if (this.status == 200) {
|
||||||
|
if (button.classList.contains('btn-primary')) {
|
||||||
|
button.classList.remove('btn-primary');
|
||||||
|
}
|
||||||
|
button.classList.add('btn-success');
|
||||||
|
button.textContent = 'Success';
|
||||||
|
setTimeout(function() {
|
||||||
|
if (button.classList.contains('btn-success')) {
|
||||||
|
button.classList.remove('btn-success');
|
||||||
|
}
|
||||||
|
button.classList.add('btn-primary');
|
||||||
|
button.textContent = ogText;
|
||||||
|
newUserModal.hide();
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
if (button.classList.contains('btn-primary')) {
|
||||||
|
button.classList.remove('btn-primary');
|
||||||
|
}
|
||||||
|
button.classList.add('btn-danger');
|
||||||
|
if ("error" in req.response) {
|
||||||
|
button.textContent = req.response["error"];
|
||||||
|
} else {
|
||||||
|
button.textContent = 'Failed';
|
||||||
|
}
|
||||||
|
setTimeout(function() {
|
||||||
|
if (button.classList.contains('btn-danger')) {
|
||||||
|
button.classList.remove('btn-danger');
|
||||||
|
}
|
||||||
|
button.classList.add('btn-primary');
|
||||||
|
button.textContent = ogText;
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
req.send(JSON.stringify(send));
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('accountsTabAddUser').onclick = function() {
|
||||||
|
document.getElementById('newUserEmail').value = '';
|
||||||
|
document.getElementById('newUserPassword').value = '';
|
||||||
|
if (document.getElementById('newUserName') != null) {
|
||||||
|
document.getElementById('newUserName').value = '';
|
||||||
|
}
|
||||||
|
newUserModal.show();
|
||||||
|
};
|
@ -1,3 +1,36 @@
|
|||||||
|
const tabs = {
|
||||||
|
invitesEl: document.getElementById('invitesTab'),
|
||||||
|
accountsEl: document.getElementById('accountsTab'),
|
||||||
|
invitesTabButton: document.getElementById('invitesTabButton'),
|
||||||
|
accountsTabButton: document.getElementById('accountsTabButton'),
|
||||||
|
invites: function() {
|
||||||
|
if (tabs.invitesEl.classList.contains('unfocused')) {
|
||||||
|
tabs.accountsEl.classList.add('unfocused');
|
||||||
|
tabs.invitesEl.classList.remove('unfocused');
|
||||||
|
}
|
||||||
|
if (tabs.accountsTabButton.classList.contains("active")) {
|
||||||
|
tabs.accountsTabButton.classList.remove("active");
|
||||||
|
}
|
||||||
|
tabs.invitesTabButton.classList.add("active");
|
||||||
|
},
|
||||||
|
accounts: function() {
|
||||||
|
populateUsers();
|
||||||
|
if (tabs.accountsEl.classList.contains('unfocused')) {
|
||||||
|
tabs.invitesEl.classList.add('unfocused');
|
||||||
|
tabs.accountsEl.classList.remove('unfocused');
|
||||||
|
}
|
||||||
|
if (tabs.invitesTabButton.classList.contains("active")) {
|
||||||
|
tabs.invitesTabButton.classList.remove("active");
|
||||||
|
tabs.accountsTabButton.classList.add("active");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tabs.invitesTabButton.onclick = tabs.invites;
|
||||||
|
tabs.accountsTabButton.onclick = tabs.accounts;
|
||||||
|
|
||||||
|
tabs.invites();
|
||||||
|
|
||||||
// Used for theme change animation
|
// Used for theme change animation
|
||||||
function whichTransitionEvent() {
|
function whichTransitionEvent() {
|
||||||
let t;
|
let t;
|
||||||
@ -101,6 +134,8 @@ 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) {
|
||||||
@ -638,7 +673,7 @@ document.getElementById('openDefaultsWizard').onclick = function() {
|
|||||||
checked = '';
|
checked = '';
|
||||||
}
|
}
|
||||||
radio.innerHTML =
|
radio.innerHTML =
|
||||||
`<label><input type="radio" name="defaultRadios" id="default_${user['name']}" style="margin-right: 1rem;" ${checked}>${user['name']}</label>`;
|
`<label><input type="radio" name="defaultRadios" id="default_${user['id']}" style="margin-right: 1rem;" ${checked}>${user['name']}</label>`;
|
||||||
radioList.appendChild(radio);
|
radioList.appendChild(radio);
|
||||||
}
|
}
|
||||||
let button = document.getElementById('openDefaultsWizard');
|
let button = document.getElementById('openDefaultsWizard');
|
||||||
@ -655,6 +690,19 @@ 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -662,23 +710,39 @@ document.getElementById('openDefaultsWizard').onclick = function() {
|
|||||||
req.send();
|
req.send();
|
||||||
};
|
};
|
||||||
|
|
||||||
document.getElementById('storeDefaults').onclick = function () {
|
function storeDefaults(users) {
|
||||||
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 = {
|
||||||
'username': radio.id.slice(8),
|
'from': 'user',
|
||||||
'homescreen': false};
|
'id': id,
|
||||||
|
'homescreen': false
|
||||||
|
};
|
||||||
|
if (document.getElementById('defaultsSource').value == 'userTemplate') {
|
||||||
|
data['from'] = 'template';
|
||||||
|
}
|
||||||
|
if (users != 'all') {
|
||||||
|
data['apply_to'] = users;
|
||||||
|
route = '/applySettings';
|
||||||
|
}
|
||||||
if (document.getElementById('storeDefaultHomescreen').checked) {
|
if (document.getElementById('storeDefaultHomescreen').checked) {
|
||||||
data['homescreen'] = true;
|
data['homescreen'] = true;
|
||||||
}
|
}
|
||||||
let req = new XMLHttpRequest();
|
let req = new XMLHttpRequest();
|
||||||
req.open("POST", "/setDefaults", true);
|
req.open("POST", route, 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() {
|
||||||
@ -692,9 +756,22 @@ document.getElementById('storeDefaults').onclick = function () {
|
|||||||
}
|
}
|
||||||
button.classList.add('btn-success');
|
button.classList.add('btn-success');
|
||||||
button.disabled = false;
|
button.disabled = false;
|
||||||
setTimeout(function() { userDefaultsModal.hide(); }, 1000);
|
setTimeout(function() {
|
||||||
|
let button = document.getElementById('storeDefaults');
|
||||||
|
button.textContent = "Submit";
|
||||||
|
button.classList.remove('btn-success');
|
||||||
|
button.classList.add('btn-primary');
|
||||||
|
button.disabled = false;
|
||||||
|
userDefaultsModal.hide();
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
if ("error" in req.response) {
|
||||||
|
button.textContent = req.response["error"];
|
||||||
|
} else if (("policy" in req.response) || ("homescreen" in req.response)) {
|
||||||
|
button.textContent = "Failed (Check JS Console)";
|
||||||
} else {
|
} 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() {
|
||||||
@ -708,8 +785,6 @@ document.getElementById('storeDefaults').onclick = function () {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
req.send(JSON.stringify(data));
|
req.send(JSON.stringify(data));
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var ombiDefaultsModal = '';
|
var ombiDefaultsModal = '';
|
||||||
@ -813,104 +888,6 @@ if (ombiEnabled) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('openUsers').onclick = function () {
|
|
||||||
this.disabled = true;
|
|
||||||
this.innerHTML =
|
|
||||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
|
||||||
'Loading...';
|
|
||||||
let req = new XMLHttpRequest();
|
|
||||||
req.open("GET", "/getUsers", true);
|
|
||||||
req.responseType = 'json';
|
|
||||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
|
||||||
req.onreadystatechange = function() {
|
|
||||||
if (this.readyState == 4) {
|
|
||||||
if (this.status == 200) {
|
|
||||||
let list = document.getElementById('userList');
|
|
||||||
list.textContent = '';
|
|
||||||
if (document.getElementById('saveUsers')) {
|
|
||||||
document.getElementById('saveUsers').remove();
|
|
||||||
}
|
|
||||||
let users = req.response['users'];
|
|
||||||
for (let user of users) {
|
|
||||||
let entry = document.createElement('div');
|
|
||||||
entry.classList.add('form-group', 'list-group-item', 'py-1');
|
|
||||||
entry.id = 'user_' + user['name'];
|
|
||||||
let label = document.createElement('label');
|
|
||||||
label.classList.add('d-inline-block');
|
|
||||||
label.setAttribute('for', 'address_' + user['email']);
|
|
||||||
label.textContent = user['name'];
|
|
||||||
entry.appendChild(label);
|
|
||||||
let address = document.createElement('input');
|
|
||||||
address.setAttribute('type', 'email');
|
|
||||||
address.readOnly = true;
|
|
||||||
address.classList.add('form-control-plaintext', 'text-muted', 'd-inline-block', 'addressText');
|
|
||||||
address.id = 'address_' + user['name'];
|
|
||||||
address.setAttribute('style', 'width: auto; margin-left: 2%;');
|
|
||||||
if (typeof(user['email']) != 'undefined') {
|
|
||||||
address.value = user['email'];
|
|
||||||
}
|
|
||||||
let editButton = document.createElement('i');
|
|
||||||
editButton.classList.add('fa', 'fa-edit', 'd-inline-block', 'icon-button');
|
|
||||||
editButton.setAttribute('style', 'margin-left: 2%;');
|
|
||||||
editButton.onclick = function() {
|
|
||||||
this.classList.remove('fa', 'fa-edit');
|
|
||||||
let addressElement = this.parentNode.getElementsByClassName('form-control-plaintext')[0];
|
|
||||||
addressElement.classList.remove('form-control-plaintext', 'text-muted');
|
|
||||||
addressElement.classList.add('form-control');
|
|
||||||
addressElement.readOnly = false;
|
|
||||||
if (addressElement.value == '') {
|
|
||||||
addressElement.placeholder = 'Email Address';
|
|
||||||
address.setAttribute('style', 'width: auto; margin-left: 2%;');
|
|
||||||
}
|
|
||||||
if (document.getElementById('saveUsers') == null) {
|
|
||||||
let footer = document.getElementById('userFooter')
|
|
||||||
let saveUsers = document.createElement('input');
|
|
||||||
saveUsers.classList.add('btn', 'btn-primary');
|
|
||||||
saveUsers.setAttribute('type', 'button');
|
|
||||||
saveUsers.value = 'Save Changes';
|
|
||||||
saveUsers.id = 'saveUsers';
|
|
||||||
saveUsers.onclick = function() {
|
|
||||||
let send = {}
|
|
||||||
let entries = document.getElementById('userList').children;
|
|
||||||
for (let entry of entries) {
|
|
||||||
if (typeof(entry.getElementsByTagName('input')[0]) != 'undefined') {
|
|
||||||
const name = entry.id.replace(/user_/g, '');
|
|
||||||
const address = entry.getElementsByTagName('input')[0].value;
|
|
||||||
send[name] = address;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
send = JSON.stringify(send);
|
|
||||||
let req = new XMLHttpRequest();
|
|
||||||
req.open("POST", "/modifyUsers", true);
|
|
||||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
|
||||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
|
||||||
req.onreadystatechange = function() {
|
|
||||||
if (this.readyState == 4) {
|
|
||||||
if (this.status == 200 || this.status == 204) {
|
|
||||||
usersModal.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
req.send(send);
|
|
||||||
};
|
|
||||||
footer.appendChild(saveUsers);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
entry.appendChild(editButton);
|
|
||||||
entry.appendChild(address);
|
|
||||||
list.appendChild(entry);
|
|
||||||
};
|
|
||||||
let button = document.getElementById('openUsers');
|
|
||||||
button.disabled = false;
|
|
||||||
button.innerHTML = 'Users <i class="fa fa-user"></i>';
|
|
||||||
settingsModal.hide();
|
|
||||||
usersModal.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
req.send();
|
|
||||||
};
|
|
||||||
|
|
||||||
generateInvites(empty = true);
|
generateInvites(empty = true);
|
||||||
|
|
||||||
tryLogin("", "", false, callback = function(code){
|
tryLogin("", "", false, callback = function(code){
|
||||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -65,82 +65,6 @@
|
|||||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
|
<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">
|
||||||
@ -181,9 +105,6 @@
|
|||||||
<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>
|
||||||
@ -226,16 +147,24 @@
|
|||||||
<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">New user defaults</h5>
|
<h5 class="modal-title" id="defaultsTitle"></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>Create an account and configure it to your liking, then choose it from below to store the settings as a template for all new users.</p>
|
<p id="userDefaultsDescription"></p>
|
||||||
|
<div class="mb-3" id="defaultsSourceSection">
|
||||||
|
<label for="defaultsSource">Use settings from:</label>
|
||||||
|
<select class="form-select" id="defaultsSource" aria-label="User settings source">
|
||||||
|
<option value="userTemplate" selected>Use existing user template</option>
|
||||||
|
<option value="fromUser">Source from existing user</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div id="defaultUserRadios"></div>
|
<div id="defaultUserRadios"></div>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label><input type="checkbox" value="" style="margin-right: 1rem;" id="storeDefaultHomescreen" checked>Store homescreen layout</label>
|
<input type="checkbox" value="" style="margin-right: 1rem;" id="storeDefaultHomescreen" checked>
|
||||||
|
<label for="storeDefaultHomescreen" id="storeHomescreenLabel"></label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer" id="defaultsFooter">
|
<div class="modal-footer" id="defaultsFooter">
|
||||||
@ -278,8 +207,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>
|
||||||
@ -315,10 +244,73 @@
|
|||||||
</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">
|
||||||
<h1>
|
<ul class="nav nav-pills" style="margin-bottom: 2rem;">
|
||||||
Accounts admin
|
<li class="nav-item">
|
||||||
</h1>
|
<h2><a id="invitesTabButton" class="nl nav-link active">Invites</a></h2>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<h2><a id="accountsTabButton" class="nl nav-link">Accounts</a></h2>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
<div class="btn-group" role="group" id="headerButtons">
|
<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>
|
||||||
@ -327,7 +319,8 @@
|
|||||||
Logout <i class="fa fa-sign-out"></i>
|
Logout <i class="fa fa-sign-out"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card mb-3 linkGroup">
|
<div id="invitesTab">
|
||||||
|
<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>
|
||||||
@ -400,6 +393,34 @@
|
|||||||
</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>
|
||||||
@ -479,6 +500,7 @@
|
|||||||
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,6 +278,32 @@ 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,6 +215,17 @@ 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
|
||||||
|
36
mail/deleted.mjml
Normal file
36
mail/deleted.mjml
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<mjml>
|
||||||
|
<mj-head>
|
||||||
|
<mj-attributes>
|
||||||
|
<mj-class name="bg" background-color="#101010" />
|
||||||
|
<mj-class name="bg2" background-color="#242424" />
|
||||||
|
<mj-class name="text" color="rgba(255,255,255,0.8)" />
|
||||||
|
<mj-class name="bold" color="rgba(255,255,255,0.87)" />
|
||||||
|
<mj-class name="secondary" color="rgb(153,153,153)" />
|
||||||
|
<mj-class name="blue" background-color="rgb(0,164,220)" />
|
||||||
|
</mj-attributes>
|
||||||
|
<mj-font name="Quicksand" href="https://fonts.googleapis.com/css2?family=Quicksand" />
|
||||||
|
<mj-font name="Noto Sans" href="https://fonts.googleapis.com/css2?family=Noto+Sans" />
|
||||||
|
</mj-head>
|
||||||
|
<mj-body>
|
||||||
|
<mj-section mj-class="bg2">
|
||||||
|
<mj-column>
|
||||||
|
<mj-text mj-class="bold" font-size="25px" font-family="Quicksand, Noto Sans, Helvetica, Arial, sans-serif"> Jellyfin </mj-text>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
<mj-section mj-class="bg">
|
||||||
|
<mj-column>
|
||||||
|
<mj-text mj-class="text" font-size="16px" font-family="Noto Sans, Helvetica, Arial, sans-serif">
|
||||||
|
<h3>Your account was deleted.</h3>
|
||||||
|
<p>Reason: <i>{{ .reason }}</i></p>
|
||||||
|
</mj-text>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
<mj-section mj-class="bg2">
|
||||||
|
<mj-column>
|
||||||
|
<mj-text mj-class="secondary" font-style="italic" font-size="14px">
|
||||||
|
{{ .message }}
|
||||||
|
</mj-text>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
</body>
|
||||||
|
</mjml>
|
4
mail/deleted.txt
Normal file
4
mail/deleted.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
Your Jellyfin account was deleted.
|
||||||
|
Reason: {{ .reason }}
|
||||||
|
|
||||||
|
{{ .message }}
|
7
main.go
7
main.go
@ -322,7 +322,7 @@ func start(asDaemon, firstCall bool) {
|
|||||||
app.storage.policy_path = app.config.Section("files").Key("user_template").String()
|
app.storage.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.policy_path = app.config.Section("files").Key("user_configuration").String()
|
app.storage.configuration_path = app.config.Section("files").Key("user_configuration").String()
|
||||||
app.storage.loadConfiguration()
|
app.storage.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,13 +435,16 @@ 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("/modifyUsers", app.ModifyEmails)
|
api.POST("/modifyEmails", 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
Normal file
117
scss/base.scss
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
.pageContainer {
|
||||||
|
margin: 5% 20% 5% 20%;
|
||||||
|
}
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.pageContainer {
|
||||||
|
margin: 2%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
/*margin: 20%;*/
|
||||||
|
margin-bottom: 5%;
|
||||||
|
}
|
||||||
|
.tabGroup {
|
||||||
|
/*margin: 20%;*/
|
||||||
|
margin-bottom: 5%;
|
||||||
|
margin-top: 5%;
|
||||||
|
}
|
||||||
|
.linkForm {
|
||||||
|
/*margin: 20%;*/
|
||||||
|
margin-top: 5%;
|
||||||
|
margin-bottom: 5%;
|
||||||
|
}
|
||||||
|
.contactBox {
|
||||||
|
/*margin: 20%;*/
|
||||||
|
margin-top: 5%;
|
||||||
|
color: grey;
|
||||||
|
}
|
||||||
|
.circle {
|
||||||
|
/*margin-left: 1rem;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
z-index: 5000;*/
|
||||||
|
-webkit-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||||
|
-moz-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||||
|
-o-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||||
|
transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190); /* easeInCubic */
|
||||||
|
}
|
||||||
|
.smooth-transition {
|
||||||
|
-webkit-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||||
|
-moz-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||||
|
-o-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||||
|
transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190); /* easeincubic */
|
||||||
|
}
|
||||||
|
.rotated {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
-webkit-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||||
|
-moz-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||||
|
-o-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||||
|
transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000); /* easeInOutQuart */
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-rotated {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
-webkit-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||||
|
-moz-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||||
|
-o-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||||
|
transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000); /* easeInOutQuart */
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-link {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settingIcon {
|
||||||
|
margin-left: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.modal-open {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin white-text {
|
||||||
|
&, &:visited, &:hover, &:active {
|
||||||
|
font-style: inherit;
|
||||||
|
color: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
font-variant: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
%white-text {
|
||||||
|
@include white-text;
|
||||||
|
}
|
||||||
|
|
||||||
|
%link-unstyled {
|
||||||
|
@include white-text;
|
||||||
|
background-color: transparent;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-button {
|
||||||
|
@extend %link-unstyled;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-button:hover {
|
||||||
|
@extend %link-unstyled;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nl {
|
||||||
|
@extend %link-unstyled;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nl:hover {
|
||||||
|
@extend %white-text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unfocused {
|
||||||
|
display: none;
|
||||||
|
}
|
@ -138,3 +138,8 @@ $list-group-action-active-bg: $jf-blue-focus;
|
|||||||
background-color: $success;
|
background-color: $success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
background-color: $jf-blue-hover;
|
||||||
|
}
|
||||||
|
|
||||||
|
@import "../base.scss";
|
||||||
|
19
scss/bs4/bs4.scss
Normal file
19
scss/bs4/bs4.scss
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
@import "../../node_modules/bootstrap4/scss/bootstrap";
|
||||||
|
|
||||||
|
.icon-button {
|
||||||
|
color: $text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button:hover {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button:active {
|
||||||
|
color: $text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
background-color: $list-group-hover-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
@import "../base.scss";
|
@ -138,3 +138,8 @@ $list-group-action-active-bg: $jf-blue-focus;
|
|||||||
background-color: $success;
|
background-color: $success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
background-color: $jf-blue-hover;
|
||||||
|
}
|
||||||
|
|
||||||
|
@import "../base.scss";
|
||||||
|
19
scss/bs5/bs5.scss
Normal file
19
scss/bs5/bs5.scss
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
@import "../../node_modules/bootstrap/scss/bootstrap";
|
||||||
|
|
||||||
|
.icon-button {
|
||||||
|
color: $text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button:hover {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button:active {
|
||||||
|
color: $text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
background-color: $list-group-hover-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
@import "../base.scss";
|
@ -39,44 +39,45 @@ if not args.yes:
|
|||||||
node_bin = input("input bin directory: ")
|
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"
|
scss = [(bsv / f"{bsv.name}-jf.scss"), (bsv / f"{bsv.name}.scss")]
|
||||||
css = bsv / f"{bsv.name}-jf.css"
|
css = [(bsv / f"{bsv.name}-jf.css"), (bsv / f"{bsv.name}.css")]
|
||||||
min_css = bsv.parents[1] / "data" / "static" / 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")]
|
||||||
with open(css, "w") as f:
|
for i in range(2):
|
||||||
|
with open(css[i], "w") as f:
|
||||||
f.write(
|
f.write(
|
||||||
sass.compile(
|
sass.compile(
|
||||||
filename=str(scss.resolve()), output_style="expanded", precision=6
|
filename=str(scss[i].resolve()), output_style="expanded", precision=6
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if css.exists():
|
if css[i].exists():
|
||||||
print(f"{bsv.name}: Compiled.")
|
print(f"{scss[i].name}: Compiled.")
|
||||||
# postcss only excepts forwards slashes? weird.
|
# postcss only excepts forwards slashes? weird.
|
||||||
cssPath = str(css.resolve())
|
cssPath = str(css[i].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"{bsv.name}: Prefixed.")
|
print(f"{scss[i].name}: Prefixed.")
|
||||||
runcmd(
|
runcmd(
|
||||||
f'{str((node_bin / "cleancss").resolve())} --level 1 --format breakWith=lf --output {str(min_css.resolve())} {str(css.resolve())}'
|
f'{str((node_bin / "cleancss").resolve())} --level 1 --format breakWith=lf --output {str(min_css[i].resolve())} {str(css[i].resolve())}'
|
||||||
)
|
)
|
||||||
if min_css.exists():
|
if min_css[i].exists():
|
||||||
print(f"{bsv.name}: Minified and copied to {str(min_css.resolve())}.")
|
print(f"{scss[i].name}: Minified and copied to {str(min_css[i].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,6 +20,7 @@ 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