diff --git a/app/app.go b/app/app.go index 8ca5cd1..1cefcf6 100644 --- a/app/app.go +++ b/app/app.go @@ -9,7 +9,6 @@ import ( "proxmoxaas-dashboard/dist/web" // go will complain here until the first build "github.com/gin-gonic/gin" - "github.com/go-viper/mapstructure/v2" "github.com/tdewolff/minify" ) @@ -69,18 +68,28 @@ func ServeStatic(router *gin.Engine, m *minify.M) { } func handle_GET_Account(c *gin.Context) { - c.HTML(http.StatusOK, "html/account.html", gin.H{ - "global": global, - "page": "account", - }) + username, token, csrf, err := GetAuth(c) + if err == nil { + account, err := GetUserAccount(username, token, csrf) + if err != nil { + HandleNonFatalError(c, err) + return + } + + c.HTML(http.StatusOK, "html/account.html", gin.H{ + "global": global, + "page": "account", + "account": account, + }) + } else { + c.Redirect(http.StatusFound, "/login.html") // if user is not authed, redirect user to login page + } } func handle_GET_Index(c *gin.Context) { - _, err := c.Cookie("auth") - token, _ := c.Cookie("PVEAuthCookie") - csrf, _ := c.Cookie("CSRFPreventionToken") + _, token, csrf, err := GetAuth(c) if err == nil { // user should be authed, try to return index with population - instances, _, err := get_API_resources(token, csrf) + instances, _, err := GetClusterResources(token, csrf) if err != nil { HandleNonFatalError(c, err) } @@ -90,19 +99,14 @@ func handle_GET_Index(c *gin.Context) { "instances": instances, }) } else { // return index without populating - c.HTML(http.StatusOK, "html/index.html", gin.H{ - "global": global, - "page": "index", - }) + c.Redirect(http.StatusFound, "/login.html") // if user is not authed, redirect user to login page } } func handle_GET_Instances_Fragment(c *gin.Context) { - _, err := c.Cookie("auth") - token, _ := c.Cookie("PVEAuthCookie") - csrf, _ := c.Cookie("CSRFPreventionToken") + _, token, csrf, err := GetAuth(c) if err == nil { // user should be authed, try to return index with population - instances, _, err := get_API_resources(token, csrf) + instances, _, err := GetClusterResources(token, csrf) if err != nil { HandleNonFatalError(c, err) } @@ -118,40 +122,30 @@ func handle_GET_Instances_Fragment(c *gin.Context) { } func handle_GET_Instance(c *gin.Context) { - c.HTML(http.StatusOK, "html/instance.html", gin.H{ - "global": global, - "page": "instance", - }) + _, _, _, err := GetAuth(c) + if err == nil { + c.HTML(http.StatusOK, "html/instance.html", gin.H{ + "global": global, + "page": "instance", + }) + } else { + c.Redirect(http.StatusFound, "/login.html") + } } func handle_GET_Login(c *gin.Context) { - ctx := RequestContext{ - Cookies: nil, - Body: map[string]any{}, - } - res, err := RequestGetAPI("/proxmox/access/domains", ctx) + realms, err := GetLoginRealms() if err != nil { HandleNonFatalError(c, err) - return - } - if res.StatusCode != 200 { // we expect /access/domains to always be avaliable - HandleNonFatalError(c, err) - return } - realms := Select{ + sel := Select{ ID: "realm", Name: "realm", } - for _, v := range ctx.Body["data"].([]any) { - v = v.(map[string]any) - realm := Realm{} - err := mapstructure.Decode(v, &realm) - if err != nil { - HandleNonFatalError(c, err) - } - realms.Options = append(realms.Options, Option{ + for _, realm := range realms { + sel.Options = append(sel.Options, Option{ Selected: realm.Default != 0, Value: realm.Realm, Display: realm.Comment, @@ -161,13 +155,18 @@ func handle_GET_Login(c *gin.Context) { c.HTML(http.StatusOK, "html/login.html", gin.H{ "global": global, "page": "login", - "realms": realms, + "realms": sel, }) } func handle_GET_Settings(c *gin.Context) { - c.HTML(http.StatusOK, "html/settings.html", gin.H{ - "global": global, - "page": "settings", - }) + _, _, _, err := GetAuth(c) + if err == nil { + c.HTML(http.StatusOK, "html/settings.html", gin.H{ + "global": global, + "page": "settings", + }) + } else { + c.Redirect(http.StatusFound, "/login.html") + } } diff --git a/app/types.go b/app/types.go index 0a179e0..77a8dcc 100644 --- a/app/types.go +++ b/app/types.go @@ -67,3 +67,13 @@ type Instance struct { ConfigureBtnIcon Icon DeleteBtnIcon Icon } + +type Account struct { + Username string + Pools map[string]bool + Nodes map[string]bool + VMID struct { + Min int + Max int + } +} diff --git a/app/utils.go b/app/utils.go index 6c2fe79..78de83e 100644 --- a/app/utils.go +++ b/app/utils.go @@ -11,6 +11,7 @@ import ( "log" "net/http" "os" + "reflect" "strings" "github.com/gin-gonic/gin" @@ -85,6 +86,20 @@ func MinifyStatic(m *minify.M, files embed.FS) map[string]StaticFile { func LoadHTMLToGin(engine *gin.Engine, html map[string]StaticFile) *template.Template { root := template.New("") + engine.FuncMap = template.FuncMap{ + "MapKeys": func(x any, sep string) string { + v := reflect.ValueOf(x) + keys := v.MapKeys() + s := "" + for i := 0; i < len(keys); i++ { + if i != 0 { + s += sep + } + s += keys[i].String() + } + return s + }, + } tmpl := template.Must(root, LoadAndAddToRoot(engine.FuncMap, root, html)) engine.SetHTMLTemplate(tmpl) return tmpl @@ -127,10 +142,10 @@ func HandleNonFatalError(c *gin.Context, err error) { c.Status(http.StatusInternalServerError) } -func RequestGetAPI(path string, context RequestContext) (*http.Response, error) { +func RequestGetAPI(path string, context RequestContext) (*http.Response, int, error) { req, err := http.NewRequest("GET", global.API+path, nil) if err != nil { - return nil, err + return nil, 0, err } for k, v := range context.Cookies { req.AddCookie(&http.Cookie{Name: k, Value: v}) @@ -139,31 +154,42 @@ func RequestGetAPI(path string, context RequestContext) (*http.Response, error) client := &http.Client{} response, err := client.Do(req) if err != nil { - return nil, err + return nil, response.StatusCode, err } else if response.StatusCode != 200 { - return response, nil + return response, response.StatusCode, nil } data, err := io.ReadAll(response.Body) if err != nil { - return nil, err + return nil, response.StatusCode, err } err = response.Body.Close() if err != nil { - return nil, err + return nil, response.StatusCode, err } err = json.Unmarshal(data, &context.Body) if err != nil { - return nil, err + return nil, response.StatusCode, err } - return response, nil - + return response, response.StatusCode, nil } -func get_API_resources(token string, csrf string) (map[uint]Instance, map[string]Node, error) { +func GetAuth(c *gin.Context) (string, string, string, error) { + _, errAuth := c.Cookie("auth") + username, errUsername := c.Cookie("username") + token, errToken := c.Cookie("PVEAuthCookie") + csrf, errCSRF := c.Cookie("CSRFPreventionToken") + if errUsername != nil || errAuth != nil || errToken != nil || errCSRF != nil { + return "", "", "", fmt.Errorf("auth: %s, token: %s, csrf: %s", errAuth, errToken, errCSRF) + } else { + return username, token, csrf, nil + } +} + +func GetClusterResources(token string, csrf string) (map[uint]Instance, map[string]Node, error) { ctx := RequestContext{ Cookies: map[string]string{ "PVEAuthCookie": token, @@ -171,53 +197,111 @@ func get_API_resources(token string, csrf string) (map[uint]Instance, map[string }, Body: map[string]any{}, } - res, err := RequestGetAPI("/proxmox/cluster/resources", ctx) + res, code, err := RequestGetAPI("/proxmox/cluster/resources", ctx) if err != nil { return nil, nil, err } + if code != 200 { // if we did not successfully retrieve resources, then return 500 because auth was 1 but was invalid somehow + return nil, nil, fmt.Errorf("request to /cluster/resources/ resulted in %+v", res) + } instances := map[uint]Instance{} nodes := map[string]Node{} - if res.StatusCode == 200 { // if we successfully retrieved the resources, then process it and return index - for _, v := range ctx.Body["data"].([]any) { - m := v.(map[string]any) - if m["type"] == "node" { - node := Node{} - err := mapstructure.Decode(v, &node) - if err != nil { - return nil, nil, err - } - nodes[node.Node] = node - } else if m["type"] == "lxc" || m["type"] == "qemu" { - instance := Instance{} - err := mapstructure.Decode(v, &instance) - if err != nil { - return nil, nil, err - } - instances[instance.VMID] = instance + // if we successfully retrieved the resources, then process it and return index + for _, v := range ctx.Body["data"].([]any) { + m := v.(map[string]any) + if m["type"] == "node" { + node := Node{} + err := mapstructure.Decode(v, &node) + if err != nil { + return nil, nil, err } + nodes[node.Node] = node + } else if m["type"] == "lxc" || m["type"] == "qemu" { + instance := Instance{} + err := mapstructure.Decode(v, &instance) + if err != nil { + return nil, nil, err + } + instances[instance.VMID] = instance } - for vmid, instance := range instances { - status := instance.Status - icons := Icons[status] - instance.StatusIcon = icons["status"] - instance.PowerBtnIcon = icons["power"] - instance.PowerBtnIcon.ID = "power-btn" - instance.ConfigureBtnIcon = icons["config"] - instance.ConfigureBtnIcon.ID = "configure-btn" - instance.ConsoleBtnIcon = icons["console"] - instance.ConsoleBtnIcon.ID = "console-btn" - instance.DeleteBtnIcon = icons["delete"] - instance.DeleteBtnIcon.ID = "delete-btn" - nodestatus := nodes[instance.Node].Status - icons = Icons[nodestatus] - instance.NodeStatus = nodestatus - instance.NodeStatusIcon = icons["status"] - instances[vmid] = instance + } + for vmid, instance := range instances { + status := instance.Status + icons := Icons[status] + instance.StatusIcon = icons["status"] + instance.PowerBtnIcon = icons["power"] + instance.PowerBtnIcon.ID = "power-btn" + instance.ConfigureBtnIcon = icons["config"] + instance.ConfigureBtnIcon.ID = "configure-btn" + instance.ConsoleBtnIcon = icons["console"] + instance.ConsoleBtnIcon.ID = "console-btn" + instance.DeleteBtnIcon = icons["delete"] + instance.DeleteBtnIcon.ID = "delete-btn" + nodestatus := nodes[instance.Node].Status + icons = Icons[nodestatus] + instance.NodeStatus = nodestatus + instance.NodeStatusIcon = icons["status"] + instances[vmid] = instance + } + return instances, nodes, nil +} + +func GetLoginRealms() ([]Realm, error) { + realms := []Realm{} + + ctx := RequestContext{ + Cookies: nil, + Body: map[string]any{}, + } + res, code, err := RequestGetAPI("/proxmox/access/domains", ctx) + if err != nil { + //HandleNonFatalError(c, err) + return realms, err + } + if code != 200 { // we expect /access/domains to always be avaliable + //HandleNonFatalError(c, err) + return realms, fmt.Errorf("request to /proxmox/access/do9mains resulted in %+v", res) + } + + for _, v := range ctx.Body["data"].([]any) { + v = v.(map[string]any) + realm := Realm{} + err := mapstructure.Decode(v, &realm) + if err != nil { + return realms, err } - return instances, nodes, nil - } else { // if we did not successfully retrieve resources, then return 500 because auth was 1 but was invalid somehow - return nil, nil, fmt.Errorf("request to /cluster/resources/ resulted in %+v", res) + realms = append(realms, realm) + } + + return realms, nil +} + +func GetUserAccount(username string, token string, csrf string) (Account, error) { + account := Account{} + + ctx := RequestContext{ + Cookies: map[string]string{ + "username": username, + "PVEAuthCookie": token, + "CSRFPreventionToken": csrf, + }, + Body: map[string]any{}, + } + res, code, err := RequestGetAPI("/user/config/cluster", ctx) + if err != nil { + return account, err + } + if code != 200 { + return account, fmt.Errorf("request to /user/config/cluster resulted in %+v", res) + } + + err = mapstructure.Decode(ctx.Body, &account) + if err != nil { + return account, err + } else { + account.Username = username + return account, nil } } diff --git a/web/html/account.html b/web/html/account.html index 53ec6c3..e91ece7 100644 --- a/web/html/account.html +++ b/web/html/account.html @@ -43,10 +43,10 @@ <h2>Account</h2> <section class="w3-card w3-padding"> <h3>Account Details</h3> - <p id="username">Username:</p> - <p id="pool">Pools:</p> - <p id="vmid">VMID Range:</p> - <p id="nodes">Nodes:</p> + <p id="username">Username: {{.account.Username}}</p> + <p id="pool">Pools: {{MapKeys .account.Pools ", "}}</p> + <p id="vmid">VMID Range: {{.account.VMID.Min}} - {{.account.VMID.Max}}</p> + <p id="nodes">Nodes: {{MapKeys .account.Nodes ", "}}</p> </section> <section class="w3-card w3-padding"> <div class="flex row nowrap"> diff --git a/web/scripts/account.js b/web/scripts/account.js index 159db59..a6eabaa 100644 --- a/web/scripts/account.js +++ b/web/scripts/account.js @@ -1,5 +1,5 @@ import { dialog } from "./dialog.js"; -import { requestAPI, goToPage, getCookie, setAppearance } from "./utils.js"; +import { requestAPI, setAppearance } from "./utils.js"; class ResourceChart extends HTMLElement { constructor () { @@ -139,23 +139,12 @@ const prefixes = { async function init () { setAppearance(); - const cookie = document.cookie; - if (cookie === "") { - goToPage("login.html"); - } let resources = requestAPI("/user/dynamic/resources"); let meta = requestAPI("/global/config/resources"); - let userCluster = requestAPI("/user/config/cluster"); resources = await resources; meta = (await meta).resources; - userCluster = await userCluster; - - document.querySelector("#username").innerText = `Username: ${getCookie("username")}`; - document.querySelector("#pool").innerText = `Pools: ${Object.keys(userCluster.pools).toString()}`; - document.querySelector("#vmid").innerText = `VMID Range: ${userCluster.vmid.min} - ${userCluster.vmid.max}`; - document.querySelector("#nodes").innerText = `Nodes: ${Object.keys(userCluster.nodes).toString()}`; populateResources("#resource-container", meta, resources); diff --git a/web/scripts/index.js b/web/scripts/index.js index 84eb37c..eda9e84 100644 --- a/web/scripts/index.js +++ b/web/scripts/index.js @@ -230,10 +230,6 @@ window.addEventListener("DOMContentLoaded", init); async function init () { setAppearance(); - const cookie = document.cookie; - if (cookie === "") { - goToPage("login.html"); - } wfaInit("modules/wfa.wasm"); initInstances(); diff --git a/web/scripts/instance.js b/web/scripts/instance.js index 65e6f65..08d11f8 100644 --- a/web/scripts/instance.js +++ b/web/scripts/instance.js @@ -42,10 +42,6 @@ const resourcesConfigPage = mergeDeep({}, resourcesConfig, resourceInputTypes); async function init () { setAppearance(); - const cookie = document.cookie; - if (cookie === "") { - goToPage("login.html"); - } const uriData = getURIData(); node = uriData.node;