-
- Created by
- {{ .Account.Username }}
- at {{ .Version.Created }}
-
- {{ .VersionData }}
-
-{{ end }}
diff --git a/pkg/webui/html/versions.html b/pkg/webui/html/versions.html
new file mode 100644
index 0000000..8adab56
--- /dev/null
+++ b/pkg/webui/html/versions.html
@@ -0,0 +1,23 @@
+{{ define "main" }}
+
+
+ Created by
+ {{ .Account.Username }}
+ at {{ .Version.Created }}
+
+
+
+{{ end }}
diff --git a/pkg/webui/index.go b/pkg/webui/index.go
index 1168098..28a4964 100644
--- a/pkg/webui/index.go
+++ b/pkg/webui/index.go
@@ -8,6 +8,7 @@ import (
)
type Page struct {
+ IsAdmin bool
LightMode bool
Precedent string
Section string
@@ -15,6 +16,8 @@ type Page struct {
}
func makePage(r *http.Request, page *Page) *Page {
+ account := r.Context().Value(model.AccountContextKey{}).(*model.Account)
+ page.IsAdmin = account.IsAdmin
settings := r.Context().Value(model.SettingsContextKey{}).(*model.Settings)
page.LightMode = settings.LightMode
return page
diff --git a/pkg/webui/routes.go b/pkg/webui/routes.go
index e84f33a..2037df6 100644
--- a/pkg/webui/routes.go
+++ b/pkg/webui/routes.go
@@ -12,6 +12,8 @@ func addRoutes(
) {
requireSession := sessionsMiddleware(db)
requireLogin := loginMiddleware(db, requireSession)
+ requireAdmin := adminMiddleware(db, requireLogin)
+ mux.Handle("GET /accounts", requireAdmin(handleAccountsGET(db)))
mux.Handle("GET /healthz", handleHealthz())
mux.Handle("GET /login", requireSession(handleLoginGET()))
mux.Handle("POST /login", requireSession(handleLoginPOST(db)))
@@ -19,8 +21,9 @@ func addRoutes(
mux.Handle("GET /settings", requireLogin(handleSettingsGET(db)))
mux.Handle("POST /settings", requireLogin(handleSettingsPOST(db)))
mux.Handle("GET /states", requireLogin(handleStatesGET(db)))
- mux.Handle("GET /state/{id}", requireLogin(handleStateGET(db)))
+ mux.Handle("POST /states", requireLogin(handleStatesPOST(db)))
+ mux.Handle("GET /states/{id}", requireLogin(handleStatesIdGET(db)))
mux.Handle("GET /static/", cache(http.FileServer(http.FS(staticFS))))
- mux.Handle("GET /version/{id}", requireLogin(handleVersionGET(db)))
+ mux.Handle("GET /versions/{id}", requireLogin(handleVersionsGET(db)))
mux.Handle("GET /", requireLogin(handleIndexGET()))
}
diff --git a/pkg/webui/state.go b/pkg/webui/state.go
deleted file mode 100644
index 141d2d8..0000000
--- a/pkg/webui/state.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package webui
-
-import (
- "html/template"
- "net/http"
- "strconv"
-
- "git.adyxax.org/adyxax/tfstated/pkg/database"
- "git.adyxax.org/adyxax/tfstated/pkg/model"
-)
-
-var stateTemplate = template.Must(template.ParseFS(htmlFS, "html/base.html", "html/state.html"))
-
-func handleStateGET(db *database.DB) http.Handler {
- type StatesData struct {
- Page *Page
- State *model.State
- Usernames map[int]string
- Versions []model.Version
- }
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- stateIdStr := r.PathValue("id")
- stateId, err := strconv.Atoi(stateIdStr)
- if err != nil {
- errorResponse(w, http.StatusBadRequest, err)
- return
- }
- state, err := db.LoadStateById(stateId)
- if err != nil {
- errorResponse(w, http.StatusInternalServerError, err)
- return
- }
- versions, err := db.LoadVersionsByState(state)
- if err != nil {
- errorResponse(w, http.StatusInternalServerError, err)
- return
- }
- usernames, err := db.LoadAccountUsernames()
- if err != nil {
- errorResponse(w, http.StatusInternalServerError, err)
- return
- }
- render(w, stateTemplate, http.StatusOK, StatesData{
- Page: makePage(r, &Page{
- Precedent: "/states",
- Section: "states",
- Title: state.Path,
- }),
- State: state,
- Usernames: usernames,
- Versions: versions,
- })
- })
-}
diff --git a/pkg/webui/states.go b/pkg/webui/states.go
index a0d16ca..64b7967 100644
--- a/pkg/webui/states.go
+++ b/pkg/webui/states.go
@@ -1,29 +1,135 @@
package webui
import (
+ "fmt"
"html/template"
+ "io"
"net/http"
+ "net/url"
+ "path"
"git.adyxax.org/adyxax/tfstated/pkg/database"
"git.adyxax.org/adyxax/tfstated/pkg/model"
+ "go.n16f.net/uuid"
)
+type StatesPage struct {
+ ActiveTab int
+ Page *Page
+ Path string
+ PathError bool
+ PathDuplicate bool
+ States []model.State
+}
+
var statesTemplates = template.Must(template.ParseFS(htmlFS, "html/base.html", "html/states.html"))
+var statesIdTemplate = template.Must(template.ParseFS(htmlFS, "html/base.html", "html/statesId.html"))
func handleStatesGET(db *database.DB) http.Handler {
- type StatesData struct {
- Page *Page
- States []model.State
- }
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
states, err := db.LoadStates()
if err != nil {
errorResponse(w, http.StatusInternalServerError, err)
return
}
- render(w, statesTemplates, http.StatusOK, StatesData{
+ render(w, statesTemplates, http.StatusOK, StatesPage{
Page: makePage(r, &Page{Title: "States", Section: "states"}),
States: states,
})
})
}
+
+func handleStatesPOST(db *database.DB) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // file upload limit of 20MB
+ if err := r.ParseMultipartForm(20 << 20); err != nil {
+ errorResponse(w, http.StatusBadRequest, err)
+ return
+ }
+ file, _, err := r.FormFile("file")
+ if err != nil {
+ errorResponse(w, http.StatusBadRequest, err)
+ return
+ }
+ defer file.Close()
+ statePath := r.FormValue("path")
+ parsedStatePath, err := url.Parse(statePath)
+ if err != nil || path.Clean(parsedStatePath.Path) != statePath || statePath[0] != '/' {
+ render(w, statesTemplates, http.StatusBadRequest, StatesPage{
+ ActiveTab: 1,
+ Page: makePage(r, &Page{Title: "New State", Section: "states"}),
+ Path: statePath,
+ PathError: true,
+ })
+ return
+ }
+ data, err := io.ReadAll(file)
+ if err != nil {
+ errorResponse(w, http.StatusBadRequest, fmt.Errorf("failed to read uploaded file: %w", err))
+ return
+ }
+ fileType := http.DetectContentType(data)
+ if fileType != "text/plain; charset=utf-8" {
+ errorResponse(w, http.StatusBadRequest, fmt.Errorf("invalid file type: expected \"text/plain; charset=utf-8\" but got \"%s\"", fileType))
+ return
+ }
+ account := r.Context().Value(model.AccountContextKey{}).(*model.Account)
+ version, err := db.CreateState(statePath, account.Id, data)
+ if err != nil {
+ errorResponse(w, http.StatusInternalServerError, err)
+ return
+ }
+ if version == nil {
+ render(w, statesTemplates, http.StatusBadRequest, StatesPage{
+ ActiveTab: 1,
+ Page: makePage(r, &Page{Title: "New State", Section: "states"}),
+ Path: statePath,
+ PathDuplicate: true,
+ })
+ return
+ }
+ destination := path.Join("/versions", version.Id.String())
+ http.Redirect(w, r, destination, http.StatusFound)
+ })
+}
+
+func handleStatesIdGET(db *database.DB) http.Handler {
+ type StatesData struct {
+ Page *Page
+ State *model.State
+ Usernames map[string]string
+ Versions []model.Version
+ }
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ var stateId uuid.UUID
+ if err := stateId.Parse(r.PathValue("id")); err != nil {
+ errorResponse(w, http.StatusBadRequest, err)
+ return
+ }
+ state, err := db.LoadStateById(stateId)
+ if err != nil {
+ errorResponse(w, http.StatusInternalServerError, err)
+ return
+ }
+ versions, err := db.LoadVersionsByState(state)
+ if err != nil {
+ errorResponse(w, http.StatusInternalServerError, err)
+ return
+ }
+ usernames, err := db.LoadAccountUsernames()
+ if err != nil {
+ errorResponse(w, http.StatusInternalServerError, err)
+ return
+ }
+ render(w, statesIdTemplate, http.StatusOK, StatesData{
+ Page: makePage(r, &Page{
+ Precedent: "/states",
+ Section: "states",
+ Title: state.Path,
+ }),
+ State: state,
+ Usernames: usernames,
+ Versions: versions,
+ })
+ })
+}
diff --git a/pkg/webui/version.go b/pkg/webui/versions.go
similarity index 70%
rename from pkg/webui/version.go
rename to pkg/webui/versions.go
index 2aa1422..8cb0a8b 100644
--- a/pkg/webui/version.go
+++ b/pkg/webui/versions.go
@@ -1,18 +1,18 @@
package webui
import (
- "fmt"
"html/template"
"net/http"
- "strconv"
+ "path"
"git.adyxax.org/adyxax/tfstated/pkg/database"
"git.adyxax.org/adyxax/tfstated/pkg/model"
+ "go.n16f.net/uuid"
)
-var versionTemplate = template.Must(template.ParseFS(htmlFS, "html/base.html", "html/version.html"))
+var versionsTemplate = template.Must(template.ParseFS(htmlFS, "html/base.html", "html/versions.html"))
-func handleVersionGET(db *database.DB) http.Handler {
+func handleVersionsGET(db *database.DB) http.Handler {
type VersionsData struct {
Page *Page
Account *model.Account
@@ -21,9 +21,8 @@ func handleVersionGET(db *database.DB) http.Handler {
VersionData string
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- versionIdStr := r.PathValue("id")
- versionId, err := strconv.Atoi(versionIdStr)
- if err != nil {
+ var versionId uuid.UUID
+ if err := versionId.Parse(r.PathValue("id")); err != nil {
errorResponse(w, http.StatusBadRequest, err)
return
}
@@ -32,6 +31,10 @@ func handleVersionGET(db *database.DB) http.Handler {
errorResponse(w, http.StatusInternalServerError, err)
return
}
+ if version == nil {
+ errorResponse(w, http.StatusNotFound, err)
+ return
+ }
state, err := db.LoadStateById(version.StateId)
if err != nil {
errorResponse(w, http.StatusInternalServerError, err)
@@ -43,9 +46,9 @@ func handleVersionGET(db *database.DB) http.Handler {
return
}
versionData := string(version.Data[:])
- render(w, versionTemplate, http.StatusOK, VersionsData{
+ render(w, versionsTemplate, http.StatusOK, VersionsData{
Page: makePage(r, &Page{
- Precedent: fmt.Sprintf("/state/%d", state.Id),
+ Precedent: path.Join("/states/", state.Id.String()),
Section: "states",
Title: state.Path,
}),