From c4acb43cb8abdf05e41d51f5b01fdd6c0d35e766 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Sun, 20 Sep 2020 11:21:04 +0100 Subject: [PATCH] Initial features for move to profiles user templates will become profiles. You will be able to make multiple, and assign them to invites individually. This commit migrates the separate template files into one profile entry called "Default", and lets you select them on invites. No way to create profiles has been added yet. --- api.go | 84 +++++++++++++++++++++++++++++++---------- config.go | 2 +- config/config-base.json | 8 ++++ data/static/admin.js | 55 ++++++++++++++++++++++++++- main.go | 10 +++++ storage.go | 37 ++++++++++++++++-- 6 files changed, 170 insertions(+), 26 deletions(-) diff --git a/api.go b/api.go index cfab30b..e5c60a5 100644 --- a/api.go +++ b/api.go @@ -293,18 +293,26 @@ func (app *appContext) NewUser(gc *gin.Context) { 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.Code, status) + if invite.Profile != "" { + profile, ok := app.storage.profiles[invite.Profile] + if !ok { + profile = app.storage.profiles["Default"] } - } - 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.Code, status) + if len(profile.Policy) != 0 { + app.debug.Printf("Applying policy from profile \"%s\"", invite.Profile) + status, err = app.jf.setPolicy(id, profile.Policy) + if !(status == 200 || status == 204) { + app.err.Printf("%s: Failed to set user policy: Code %d", req.Code, status) + } + } + if len(profile.Configuration) != 0 && len(profile.Displayprefs) != 0 { + app.debug.Printf("Applying homescreen from profile \"%s\"", invite.Profile) + status, err = app.jf.setConfiguration(id, profile.Configuration) + if (status == 200 || status == 204) && err == nil { + status, err = app.jf.setDisplayPreferences(id, profile.Displayprefs) + } else { + app.err.Printf("%s: Failed to set configuration template: Code %d", req.Code, status) + } } } if app.config.Section("password_resets").Key("enabled").MustBool(false) { @@ -379,6 +387,7 @@ type generateInviteReq struct { MultipleUses bool `json:"multiple-uses"` NoLimit bool `json:"no-limit"` RemainingUses int `json:"remaining-uses"` + Profile string `json:"profile"` } func (app *appContext) GenerateInvite(gc *gin.Context) { @@ -418,11 +427,39 @@ func (app *appContext) GenerateInvite(gc *gin.Context) { app.info.Printf("%s: Sent invite email to %s", invite_code, req.Email) } } + if req.Profile != "" { + if _, ok := app.storage.profiles[req.Profile]; ok { + invite.Profile = req.Profile + } else { + invite.Profile = "Default" + } + } app.storage.invites[invite_code] = invite app.storage.storeInvites() gc.JSON(200, map[string]bool{"success": true}) } +type profileReq struct { + Invite string `json:"invite"` + Profile string `json:"profile"` +} + +func (app *appContext) SetProfile(gc *gin.Context) { + var req profileReq + gc.BindJSON(&req) + app.debug.Printf("%s: Setting profile to \"%s\"", req.Invite, req.Profile) + if _, ok := app.storage.profiles[req.Profile]; !ok { + app.err.Printf("%s: Profile \"%s\" not found", req.Invite, req.Profile) + respond(500, "Profile not found", gc) + return + } + inv := app.storage.invites[req.Invite] + inv.Profile = req.Profile + app.storage.invites[req.Invite] = inv + app.storage.storeInvites() + gc.JSON(200, map[string]bool{"success": true}) +} + func (app *appContext) GetInvites(gc *gin.Context) { app.debug.Println("Invites requested") current_time := time.Now() @@ -431,12 +468,14 @@ func (app *appContext) GetInvites(gc *gin.Context) { var invites []map[string]interface{} for code, inv := range app.storage.invites { _, _, days, hours, minutes, _ := timeDiff(inv.ValidTill, current_time) - invite := make(map[string]interface{}) - invite["code"] = code - invite["days"] = days - invite["hours"] = hours - invite["minutes"] = minutes - invite["created"] = app.formatDatetime(inv.Created) + invite := map[string]interface{}{ + "code": code, + "days": days, + "hours": hours, + "minutes": minutes, + "created": app.formatDatetime(inv.Created), + "profile": inv.Profile, + } if len(inv.UsedBy) != 0 { invite["used-by"] = inv.UsedBy } @@ -470,8 +509,15 @@ func (app *appContext) GetInvites(gc *gin.Context) { } invites = append(invites, invite) } - resp := map[string][]map[string]interface{}{ - "invites": invites, + profiles := make([]string, len(app.storage.profiles)) + i := 0 + for p := range app.storage.profiles { + profiles[i] = p + i++ + } + resp := map[string]interface{}{ + "profiles": profiles, + "invites": invites, } gc.JSON(200, resp) } diff --git a/config.go b/config.go index ae5359d..a045362 100644 --- a/config.go +++ b/config.go @@ -48,7 +48,7 @@ func (app *appContext) loadConfig() error { // } key.SetValue(key.MustString(filepath.Join(app.data_path, (key.Name() + ".json")))) } - for _, key := range []string{"user_configuration", "user_displayprefs", "ombi_template"} { + for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template"} { // if app.config.Section("files").Key(key).MustString("") == "" { // key.SetValue(filepath.Join(app.data_path, (key.Name() + ".json"))) // } diff --git a/config/config-base.json b/config/config-base.json index 35a1280..d2b568b 100644 --- a/config/config-base.json +++ b/config/config-base.json @@ -626,6 +626,14 @@ "value": "", "description": "Location of stored displayPreferences template (also used for homescreen layout) (json)" }, + "user_profiles": { + "name": "User Profiles", + "required": false, + "requires_restart": true, + "type": "text", + "value": "", + "description": "Location of stored user profiles (encompasses template and homescreen) (json)" + }, "custom_css": { "name": "Custom CSS", "required": false, diff --git a/data/static/admin.js b/data/static/admin.js index 5b79ddb..257c11b 100644 --- a/data/static/admin.js +++ b/data/static/admin.js @@ -137,7 +137,9 @@ var aboutModal = createModal('aboutModal'); var deleteModal = createModal('deleteModal'); var newUserModal = createModal('newUserModal'); -// Parsed invite: [, , <1: Empty invite (no delete/link), 0: Actual invite>, , , [], , , ] +var availableProfiles = []; +// god this is an ugly way to do it +// Parsed invite: [, , <1: Empty invite (no delete/link), 0: Actual invite>, , , [], , , , ] function parseInvite(invite, empty = false) { if (empty) { return ["None", "", 1]; @@ -170,6 +172,9 @@ function parseInvite(invite, empty = false) { if ('notify-creation' in invite) { i[8] = invite['notify-creation']; } + if ('profile' in invite) { + i[9] = invite['profile']; + } return i; } @@ -267,6 +272,29 @@ function addItem(parsedInvite) { let leftList = document.createElement('ul'); leftList.classList.add('list-group', 'list-group-flush'); + + let profileBox = document.createElement('li'); + profileBox.classList.add('input-group', 'py-1'); + let prof = ` + + `; + profileBox.innerHTML = prof; + leftList.appendChild(profileBox); + // 9 is profileName! availableProfiles if (typeof(parsedInvite[6]) != 'undefined') { let createdDate = document.createElement('li'); @@ -438,6 +466,7 @@ function generateInvites(empty = false) { req.onreadystatechange = function() { if (this.readyState == 4) { var data = this.response; + availableProfiles = data['profiles']; if (data['invites'] == null || data['invites'].length == 0) { document.getElementById('invites').textContent = ''; addItem(parseInvite([], true)); @@ -1117,7 +1146,29 @@ document.getElementById('settingsSave').onclick = function() { } } -// Diable 'Generate' button if days, hours, minutes are all zero +function setProfile(select) { + if (select.value == "") { + return; + } + let invite = select.id.replace("profile_", ""); + let req = new XMLHttpRequest(); + let send = { + "invite": invite, + "profile": select.value + }; + req.open("POST", "/setProfile", true); + req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":")); + req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + req.onreadystatechange = function() { + if (this.readyState == 4 && this.status != 200) { + generateInvites(false); + } + }; + req.send(JSON.stringify(send)); +} + + +// Disable 'Generate' button if days, hours, minutes are all zero function checkDuration() { let boxVals = [document.getElementById("days").value, document.getElementById("hours").value, document.getElementById("minutes").value]; let submit = document.getElementById("generateSubmit"); diff --git a/main.go b/main.go index 70fca6c..607ab3f 100644 --- a/main.go +++ b/main.go @@ -328,6 +328,15 @@ func start(asDaemon, firstCall bool) { app.storage.displayprefs_path = app.config.Section("files").Key("user_displayprefs").String() app.storage.loadDisplayprefs() + app.storage.profiles_path = app.config.Section("files").Key("user_profiles").String() + app.storage.loadProfiles() + + if !(len(app.storage.policy) == 0 && len(app.storage.configuration) == 0 && len(app.storage.displayprefs) == 0) { + app.info.Println("Migrating user template files to new profile format") + app.storage.migrateToProfile() + app.storage.storeProfiles() + } + if app.config.Section("ombi").Key("enabled").MustBool(false) { app.storage.ombi_path = app.config.Section("files").Key("ombi_template").String() app.storage.loadOmbiTemplate() @@ -438,6 +447,7 @@ func start(asDaemon, firstCall bool) { api.POST("/newUserAdmin", app.NewUserAdmin) api.POST("/generateInvite", app.GenerateInvite) api.GET("/getInvites", app.GetInvites) + api.POST("/setProfile", app.SetProfile) api.POST("/setNotify", app.SetNotify) api.POST("/deleteInvite", app.DeleteInvite) api.POST("/deleteUser", app.DeleteUser) diff --git a/storage.go b/storage.go index 1af4cc9..c37d983 100644 --- a/storage.go +++ b/storage.go @@ -7,14 +7,21 @@ import ( ) type Storage struct { - timePattern string - invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path string - invites Invites - emails, policy, configuration, displayprefs, ombi_template map[string]interface{} + timePattern string + invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path string + invites Invites + profiles map[string]Profile + emails, policy, configuration, displayprefs, ombi_template map[string]interface{} } // timePattern: %Y-%m-%dT%H:%M:%S.%f +type Profile struct { + Policy map[string]interface{} `json:"policy"` + Configuration map[string]interface{} `json:"configuration"` + Displayprefs map[string]interface{} `json:"displayprefs"` +} + type Invite struct { Created time.Time `json:"created"` NoLimit bool `json:"no-limit"` @@ -23,6 +30,7 @@ type Invite struct { Email string `json:"email"` UsedBy [][]string `json:"used-by"` Notify map[string]map[string]bool `json:"notify"` + Profile string `json:"profile"` } type Invites map[string]Invite @@ -75,6 +83,27 @@ func (st *Storage) storeOmbiTemplate() error { return storeJSON(st.ombi_path, st.ombi_template) } +func (st *Storage) loadProfiles() error { + return loadJSON(st.profiles_path, &st.profiles) +} + +func (st *Storage) storeProfiles() error { + return storeJSON(st.profiles_path, st.profiles) +} + +func (st *Storage) migrateToProfile() error { + st.loadPolicy() + st.loadConfiguration() + st.loadDisplayprefs() + st.loadProfiles() + st.profiles["Default"] = Profile{ + Policy: st.policy, + Configuration: st.configuration, + Displayprefs: st.displayprefs, + } + return st.storeProfiles() +} + func loadJSON(path string, obj interface{}) error { var file []byte var err error