package ecodash import ( "encoding/json" "errors" "fmt" "html" "html/template" "math" "os" "time" "git.massivebox.net/ecodash/ecodash/src/tools" "github.com/gofiber/fiber/v2" ) type Link struct { Label string `json:"label"` Destination string `json:"destination"` NewTab bool `json:"new_tab"` Primary bool `json:"primary"` } type Warning struct { Header string Body string BodyHTML template.HTML IsSuccess bool } func (config *Config) getTemplateDefaults() fiber.Map { return fiber.Map{ "DashboardName": config.Dashboard.Name, "HeaderLinks": config.Dashboard.HeaderLinks, "FooterLinks": config.Dashboard.FooterLinks, } } func (config *Config) TemplateDefaultsMap() fiber.Map { return fiber.Map{ "Default": config.getTemplateDefaults(), } } func (config *Config) AdminEndpoint(c *fiber.Ctx) error { if c.Method() == "POST" { if config.IsAuthorized(c) { // here the user is submitting the form to change configuration err := config.saveAdminForm(c) if err != nil { // #nosec the input is admin-defined, and the admin is assumed to be trusted. return config.RenderAdminPanel(c, &MessageCard{ Title: "An error occurred!", Content: template.HTML(html.EscapeString(err.Error())), Style: "error", }) } return config.RenderAdminPanel(c, &MessageCard{ Title: "Settings applied", Content: "Your settings have been tested and applied successfully.
" + "You can continue using EcoDash on the Home.", Style: "success", }) } // here the user is trying to authenticate if c.FormValue("username") == config.Administrator.Username && tools.Hash(c.FormValue("password")) == config.Administrator.PasswordHash { c.Cookie(&fiber.Cookie{Name: "admin_username", Value: c.FormValue("username")}) c.Cookie(&fiber.Cookie{Name: "admin_password_hash", Value: tools.Hash(c.FormValue("password"))}) return config.RenderAdminPanel(c, nil) } return c.Render("login", fiber.Map{"Defaults": config.getTemplateDefaults(), "Failed": true}, "base") } if config.IsAuthorized(c) { return config.RenderAdminPanel(c, nil) } return c.Render("login", config.TemplateDefaultsMap(), "base") } func (config *Config) RenderAdminPanel(c *fiber.Ctx, message *MessageCard) error { dirs, err := os.ReadDir("./templates") if err != nil { return err } return c.Render("admin", fiber.Map{ "Defaults": config.getTemplateDefaults(), "Themes": dirs, "Config": config, "Message": message, }, "base") } var ( errNoChanges = errors.New("no changes from previous config") errMissingField = errors.New("required field is missing") ) func (config *Config) saveAdminForm(c *fiber.Ctx) error { requiredFields := []string{"base_url", "api_key", "polled_smart_energy_summation", "fossil_percentage", "username", "theme", "name", "installation_date"} for _, requiredField := range requiredFields { if c.FormValue(requiredField) == "" { return fmt.Errorf("%w: %s", errMissingField, requiredField) } } parsedTime, err := time.Parse("2006-01-02", c.FormValue("installation_date")) if err != nil { return err } form := Config{ HomeAssistant: HomeAssistant{ /*BaseURL to be filled later*/ APIKey: c.FormValue("api_key"), InstallationDate: dayStart(parsedTime)}, Sensors: Sensors{PolledSmartEnergySummation: c.FormValue("polled_smart_energy_summation"), FossilPercentage: c.FormValue("fossil_percentage")}, Administrator: Administrator{Username: c.FormValue("username") /*PasswordHash to be filled later*/}, Dashboard: Dashboard{Theme: c.FormValue("theme"), Name: c.FormValue("name"), HeaderLinks: config.Dashboard.HeaderLinks, FooterLinks: config.Dashboard.FooterLinks /*MessageCard to be filled later*/}, } if c.FormValue("keep_old_password") == "" { form.Administrator.PasswordHash = tools.Hash(c.FormValue("password")) } else { form.Administrator.PasswordHash = config.Administrator.PasswordHash } if c.FormValue("motd_title") != "" || c.FormValue("motd_content") != "" { // #nosec the input is admin-defined, and the admin is assumed to be trusted. form.Dashboard.MOTD = &MessageCard{ Title: c.FormValue("motd_title"), Content: template.HTML(c.FormValue("motd_content")), Style: c.FormValue("motd_style"), } } fmtURL, err := formatURL(c.FormValue("base_url")) if err != nil { return err } form.HomeAssistant.BaseURL = fmtURL if form.Equals(config) { return errNoChanges } form.db = config.db err = form.refreshCacheFromInstall() if err != nil { return err } js, err := json.Marshal(form) if err != nil { return err } *config = form return os.WriteFile("config.json", js, 0o600) } func averageExcludingCurrentDay(data []float32) float32 { if len(data) <= 1 { return 0 } data = data[:len(data)-1] var sum float32 for _, num := range data { sum += num } avg := sum / float32(len(data)) return float32(math.Floor(float64(avg)*100)) / 100 } func (config *Config) RenderIndex(c *fiber.Ctx) error { if config.HomeAssistant.InstallationDate.IsZero() { return c.Render("config-error", fiber.Map{ "Defaults": config.getTemplateDefaults(), "Error": "The installation date is not set! This is normal if you've just updated from v0.1 to v0.2.", }, "base") } data, err := config.readHistory() if err != nil { return err } labels := make([]string, 0, len(data)) greenEnergyConsumptionAbsolute := make([]float32, 0, len(data)) greenEnergyPercents := make([]float32, 0, len(data)) energyConsumptions := make([]float32, 0, len(data)) for _, datum := range data { labels = append(labels, time.Unix(datum.Date, 0).Format("02/01")) greenEnergyPercents = append(greenEnergyPercents, datum.GreenEnergyPercentage) greenEnergyConsumptionAbsolute = append(greenEnergyConsumptionAbsolute, datum.GreenEnergyPercentage/100*datum.PolledSmartEnergySummation) energyConsumptions = append(energyConsumptions, datum.PolledSmartEnergySummation) } perDayUsage := averageExcludingCurrentDay(energyConsumptions) return c.Render("index", fiber.Map{ "Defaults": config.getTemplateDefaults(), "Labels": labels, "GreenEnergyPercents": greenEnergyConsumptionAbsolute, "EnergyConsumptions": energyConsumptions, "GreenEnergyPercent": averageExcludingCurrentDay(greenEnergyPercents), "PerDayUsage": perDayUsage, "MOTD": config.Dashboard.MOTD, }, "base") }