diff --git a/pkg/database/states.go b/pkg/database/states.go index 090e533..0bc6d17 100644 --- a/pkg/database/states.go +++ b/pkg/database/states.go @@ -166,6 +166,28 @@ func (db *DB) LoadStates() ([]model.State, error) { return states, nil } +// Returns (true, nil) on successful save +func (db *DB) SaveState(state *model.State) (bool, error) { + _, err := db.Exec( + `UPDATE states + SET lock = ?, + path = ? + WHERE id = ?`, + state.Lock, + state.Path, + state.Id) + if err != nil { + var sqliteErr sqlite3.Error + if errors.As(err, &sqliteErr) { + if sqliteErr.Code == sqlite3.ErrNo(sqlite3.ErrConstraint) { + return false, nil + } + } + return false, fmt.Errorf("failed to update state id %s: %w", state.Id, err) + } + return true, nil +} + // returns true in case of lock mismatch func (db *DB) SetState(path string, accountId uuid.UUID, data []byte, lock string) (bool, error) { encryptedData, err := db.dataEncryptionKey.EncryptAES256(data) diff --git a/pkg/webui/routes.go b/pkg/webui/routes.go index a3a8a1a..d6e6a8d 100644 --- a/pkg/webui/routes.go +++ b/pkg/webui/routes.go @@ -27,6 +27,7 @@ func addRoutes( mux.Handle("GET /states", requireLogin(handleStatesGET(db))) mux.Handle("POST /states", requireLogin(handleStatesPOST(db))) mux.Handle("GET /states/{id}", requireLogin(handleStatesIdGET(db))) + mux.Handle("POST /states/{id}", requireLogin(handleStatesIdPOST(db))) mux.Handle("GET /static/", cache(http.FileServer(http.FS(staticFS)))) mux.Handle("GET /versions/{id}", requireLogin(handleVersionsGET(db))) mux.Handle("GET /", requireLogin(handleIndexGET())) diff --git a/pkg/webui/statesId.go b/pkg/webui/statesId.go index 4d8d413..8127db5 100644 --- a/pkg/webui/statesId.go +++ b/pkg/webui/statesId.go @@ -3,6 +3,8 @@ package webui import ( "html/template" "net/http" + "net/url" + "path" "git.adyxax.org/adyxax/tfstated/pkg/database" "git.adyxax.org/adyxax/tfstated/pkg/model" @@ -54,3 +56,79 @@ func handleStatesIdGET(db *database.DB) http.Handler { }) }) } + +func handleStatesIdPOST(db *database.DB) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + errorResponse(w, r, http.StatusBadRequest, err) + return + } + action := r.FormValue("action") + var stateId uuid.UUID + if err := stateId.Parse(r.PathValue("id")); err != nil { + errorResponse(w, r, http.StatusBadRequest, err) + return + } + state, err := db.LoadStateById(stateId) + if err != nil { + errorResponse(w, r, http.StatusInternalServerError, err) + return + } + versions, err := db.LoadVersionsByState(state) + if err != nil { + errorResponse(w, r, http.StatusInternalServerError, err) + return + } + usernames, err := db.LoadAccountUsernames() + if err != nil { + errorResponse(w, r, http.StatusInternalServerError, err) + return + } + switch action { + case "delete": + errorResponse(w, r, http.StatusNotImplemented, err) + case "edit": + statePath := r.FormValue("path") + parsedStatePath, err := url.Parse(statePath) + if err != nil || path.Clean(parsedStatePath.Path) != statePath || statePath[0] != '/' { + render(w, statesIdTemplate, http.StatusBadRequest, StatesIdPage{ + Page: makePage(r, &Page{Title: state.Path, Section: "states"}), + Path: statePath, + PathError: true, + State: state, + Usernames: usernames, + Versions: versions, + }) + return + } + state.Path = statePath + success, err := db.SaveState(state) + if err != nil { + errorResponse(w, r, http.StatusInternalServerError, err) + return + } + if !success { + render(w, statesIdTemplate, http.StatusBadRequest, StatesIdPage{ + Page: makePage(r, &Page{Title: state.Path, Section: "states"}), + Path: statePath, + PathDuplicate: true, + State: state, + Usernames: usernames, + Versions: versions, + }) + return + } + render(w, statesIdTemplate, http.StatusOK, StatesIdPage{ + Page: makePage(r, &Page{ + Section: "states", + Title: state.Path, + }), + State: state, + Usernames: usernames, + Versions: versions, + }) + default: + errorResponse(w, r, http.StatusBadRequest, err) + } + }) +}