feat(webui): implement states list

This commit is contained in:
Julien Dessaux 2025-01-22 00:46:49 +01:00
parent 7885b19b54
commit 09885ef1e4
Signed by: adyxax
GPG key ID: F92E51B86E07177E
10 changed files with 117 additions and 12 deletions

View file

@ -111,6 +111,10 @@ func (db *DB) Exec(query string, args ...any) (sql.Result, error) {
return db.writeDB.ExecContext(db.ctx, query, args...) return db.writeDB.ExecContext(db.ctx, query, args...)
} }
func (db *DB) Query(query string, args ...any) (*sql.Rows, error) {
return db.readDB.QueryContext(db.ctx, query, args...)
}
func (db *DB) QueryRow(query string, args ...any) *sql.Row { func (db *DB) QueryRow(query string, args ...any) *sql.Row {
return db.readDB.QueryRowContext(db.ctx, query, args...) return db.readDB.QueryRowContext(db.ctx, query, args...)
} }

View file

@ -6,6 +6,8 @@ import (
"fmt" "fmt"
"slices" "slices"
"time" "time"
"git.adyxax.org/adyxax/tfstated/pkg/model"
) )
// returns true in case of successful deletion // returns true in case of successful deletion
@ -43,6 +45,34 @@ func (db *DB) GetState(path string) ([]byte, error) {
return db.dataEncryptionKey.DecryptAES256(encryptedData) return db.dataEncryptionKey.DecryptAES256(encryptedData)
} }
func (db *DB) LoadStatesByPath() ([]model.State, error) {
rows, err := db.Query(
`SELECT created, id, lock, path, updated FROM states;`)
if err != nil {
return nil, fmt.Errorf("failed to load states from database: %w", err)
}
defer rows.Close()
states := make([]model.State, 0)
for rows.Next() {
var state model.State
var (
created int64
updated int64
)
err = rows.Scan(&created, &state.Id, &state.Lock, &state.Path, &updated)
if err != nil {
return nil, fmt.Errorf("failed to load states from row: %w", err)
}
state.Created = time.Unix(created, 0)
state.Updated = time.Unix(updated, 0)
states = append(states, state)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("failed to load states from rows: %w", err)
}
return states, nil
}
// returns true in case of id mismatch // returns true in case of id mismatch
func (db *DB) SetState(path string, accountID int, data []byte, lockID string) (bool, error) { func (db *DB) SetState(path string, accountID int, data []byte, lockID string) (bool, error) {
encryptedData, err := db.dataEncryptionKey.EncryptAES256(data) encryptedData, err := db.dataEncryptionKey.EncryptAES256(data)

13
pkg/model/state.go Normal file
View file

@ -0,0 +1,13 @@
package model
import (
"time"
)
type State struct {
Created time.Time
Id int
Lock *string
Path string
Updated time.Time
}

View file

@ -9,12 +9,20 @@
<title>tfstated</title> <title>tfstated</title>
</head> </head>
<body> <body>
<header> <header class="container">
<nav>
<ul>
<li><a href="/"><strong>TFSTATED</strong></a></li>
</ul>
<ul>
<li><a href="/states">States</a></li>
</ul>
</nav>
</header> </header>
<main class="container"> <main class="container">
{{ template "main" . }} {{ template "main" . }}
</main> </main>
<footer> <footer class="container">
</footer> </footer>
</body> </body>
</html> </html>

View file

@ -1,3 +0,0 @@
{{ define "main" }}
<h1>TODO</h1>
{{ end }}

View file

@ -0,0 +1,23 @@
{{ define "main" }}
<h1>States</h1>
<table class="striped">
<thead>
<tr>
<th scope="col">Path</th>
<th scope="col">Created</th>
<th scope="col">Updated</th>
<th scope="col">Locked</th>
</tr>
</thead>
<tbody>
{{ range .States }}
<tr>
<th scope="row">{{ .Path }}</th>
<td>{{ .Created }}</td>
<td>{{ .Updated }}</td>
<td>{{ .Lock }}</td>
</tr>
{{ end }}
</tbody>
</table>
{{ end }}

View file

@ -1,16 +1,16 @@
package webui package webui
import ( import (
"html/template" "fmt"
"net/http" "net/http"
) )
var indexTemplates = template.Must(template.ParseFS(htmlFS, "html/base.html", "html/index.html"))
func handleIndexGET() http.Handler { func handleIndexGET() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-store, no-cache") if r.URL.Path == "/" {
http.Redirect(w, r, "/states", http.StatusFound)
render(w, indexTemplates, http.StatusOK, nil) } else {
errorResponse(w, http.StatusNotFound, fmt.Errorf("Page not found"))
}
}) })
} }

View file

@ -24,7 +24,7 @@ func handleLoginGET() http.Handler {
session := r.Context().Value(model.SessionContextKey{}) session := r.Context().Value(model.SessionContextKey{})
if session != nil { if session != nil {
http.Redirect(w, r, "/", http.StatusFound) http.Redirect(w, r, "/states", http.StatusFound)
return return
} }

View file

@ -16,6 +16,7 @@ func addRoutes(
mux.Handle("GET /login", requireSession(handleLoginGET())) mux.Handle("GET /login", requireSession(handleLoginGET()))
mux.Handle("POST /login", requireSession(handleLoginPOST(db))) mux.Handle("POST /login", requireSession(handleLoginPOST(db)))
mux.Handle("GET /logout", requireLogin(handleLogoutGET(db))) mux.Handle("GET /logout", requireLogin(handleLogoutGET(db)))
mux.Handle("GET /states", requireLogin(handleStatesGET(db)))
mux.Handle("GET /static/", cache(http.FileServer(http.FS(staticFS)))) mux.Handle("GET /static/", cache(http.FileServer(http.FS(staticFS))))
mux.Handle("GET /", requireLogin(handleIndexGET())) mux.Handle("GET /", requireLogin(handleIndexGET()))
} }

29
pkg/webui/states.go Normal file
View file

@ -0,0 +1,29 @@
package webui
import (
"html/template"
"net/http"
"git.adyxax.org/adyxax/tfstated/pkg/database"
"git.adyxax.org/adyxax/tfstated/pkg/model"
)
var statesTemplates = template.Must(template.ParseFS(htmlFS, "html/base.html", "html/states.html"))
func handleStatesGET(db *database.DB) http.Handler {
type StatesData struct {
States []model.State
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-store, no-cache")
states, err := db.LoadStatesByPath()
if err != nil {
errorResponse(w, http.StatusInternalServerError, err)
return
}
render(w, statesTemplates, http.StatusOK, StatesData{
States: states,
})
})
}