implement server side auth checking,

implement some account server side rendering
This commit is contained in:
Arthur Lu 2025-03-25 19:24:25 +00:00
parent 75330e8a59
commit b8ebbf6c3d
7 changed files with 191 additions and 117 deletions

@ -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")
}
}

@ -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
}
}

@ -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
}
}

@ -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">

@ -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);

@ -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();

@ -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;