diff --git a/pkg/database/accounts.go b/pkg/database/accounts.go index dc8b485..bb99cc4 100644 --- a/pkg/database/accounts.go +++ b/pkg/database/accounts.go @@ -49,7 +49,7 @@ func (db *DB) CreateAccount(username string, isAdmin bool) (*model.Account, erro Id: accountId, Username: username, IsAdmin: isAdmin, - PasswordReset: passwordReset, + PasswordReset: &passwordReset, }, nil } @@ -91,7 +91,7 @@ func (db *DB) InitAdminAccount() error { func (db *DB) LoadAccounts() ([]model.Account, error) { rows, err := db.Query( - `SELECT id, username, salt, password_hash, is_admin, created, last_login, settings FROM accounts;`) + `SELECT id, username, salt, password_hash, is_admin, created, last_login, settings, password_reset FROM accounts;`) if err != nil { return nil, fmt.Errorf("failed to load accounts from database: %w", err) } @@ -111,7 +111,8 @@ func (db *DB) LoadAccounts() ([]model.Account, error) { &account.IsAdmin, &created, &lastLogin, - &account.Settings) + &account.Settings, + &account.PasswordReset) if err != nil { return nil, fmt.Errorf("failed to load account from row: %w", err) } @@ -159,7 +160,7 @@ func (db *DB) LoadAccountById(id uuid.UUID) (*model.Account, error) { lastLogin int64 ) err := db.QueryRow( - `SELECT username, salt, password_hash, is_admin, created, last_login, settings + `SELECT username, salt, password_hash, is_admin, created, last_login, settings, password_reset FROM accounts WHERE id = ?;`, id, @@ -170,7 +171,7 @@ func (db *DB) LoadAccountById(id uuid.UUID) (*model.Account, error) { &created, &lastLogin, &account.Settings, - ) + &account.PasswordReset) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, nil @@ -191,7 +192,7 @@ func (db *DB) LoadAccountByUsername(username string) (*model.Account, error) { lastLogin int64 ) err := db.QueryRow( - `SELECT id, salt, password_hash, is_admin, created, last_login, settings + `SELECT id, salt, password_hash, is_admin, created, last_login, settings, password_reset FROM accounts WHERE username = ?;`, username, @@ -202,7 +203,7 @@ func (db *DB) LoadAccountByUsername(username string) (*model.Account, error) { &created, &lastLogin, &account.Settings, - ) + &account.PasswordReset) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, nil diff --git a/pkg/database/states.go b/pkg/database/states.go index 61e5f88..090e533 100644 --- a/pkg/database/states.go +++ b/pkg/database/states.go @@ -113,6 +113,31 @@ func (db *DB) LoadStateById(stateId uuid.UUID) (*model.State, error) { return &state, nil } +func (db *DB) LoadStatePaths() (map[string]string, error) { + rows, err := db.Query( + `SELECT id, path FROM states;`) + if err != nil { + return nil, fmt.Errorf("failed to load states from database: %w", err) + } + defer rows.Close() + states := make(map[string]string) + for rows.Next() { + var ( + id string + path string + ) + err = rows.Scan(&id, &path) + if err != nil { + return nil, fmt.Errorf("failed to load state from row: %w", err) + } + states[id] = path + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("failed to load states from rows: %w", err) + } + return states, nil +} + func (db *DB) LoadStates() ([]model.State, error) { rows, err := db.Query( `SELECT created, id, lock, path, updated FROM states;`) diff --git a/pkg/database/versions.go b/pkg/database/versions.go index d636b6b..e6626a8 100644 --- a/pkg/database/versions.go +++ b/pkg/database/versions.go @@ -52,7 +52,7 @@ func (db *DB) LoadVersionsByState(state *model.State) ([]model.Version, error) { defer rows.Close() versions := make([]model.Version, 0) for rows.Next() { - var version model.Version + version := model.Version{StateId: state.Id} var created int64 err = rows.Scan(&version.AccountId, &created, &version.Data, &version.Id, &version.Lock) if err != nil { @@ -66,3 +66,30 @@ func (db *DB) LoadVersionsByState(state *model.State) ([]model.Version, error) { } return versions, nil } + +func (db *DB) LoadVersionsByAccount(account *model.Account) ([]model.Version, error) { + rows, err := db.Query( + `SELECT created, data, id, lock, state_id + FROM versions + WHERE account_id = ? + ORDER BY id DESC;`, account.Id) + if err != nil { + return nil, fmt.Errorf("failed to load versions from database: %w", err) + } + defer rows.Close() + versions := make([]model.Version, 0) + for rows.Next() { + version := model.Version{AccountId: account.Id} + var created int64 + err = rows.Scan(&created, &version.Data, &version.Id, &version.Lock, &version.StateId) + if err != nil { + return nil, fmt.Errorf("failed to load version from row: %w", err) + } + version.Created = time.Unix(created, 0) + versions = append(versions, version) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("failed to load versions from rows: %w", err) + } + return versions, nil +} diff --git a/pkg/model/account.go b/pkg/model/account.go index fe7c0e8..c1ea958 100644 --- a/pkg/model/account.go +++ b/pkg/model/account.go @@ -20,7 +20,7 @@ type Account struct { Created time.Time LastLogin time.Time Settings json.RawMessage - PasswordReset uuid.UUID + PasswordReset *uuid.UUID } func (account *Account) CheckPassword(password string) bool { diff --git a/pkg/webui/accountsId.go b/pkg/webui/accountsId.go new file mode 100644 index 0000000..73a93c1 --- /dev/null +++ b/pkg/webui/accountsId.go @@ -0,0 +1,66 @@ +package webui + +import ( + "html/template" + "net/http" + + "git.adyxax.org/adyxax/tfstated/pkg/database" + "git.adyxax.org/adyxax/tfstated/pkg/model" + "go.n16f.net/uuid" +) + +type AccountsIdPage struct { + Account *model.Account + IsAdmin string + Page *Page + Username string + StatePaths map[string]string + UsernameDuplicate bool + UsernameInvalid bool + Versions []model.Version +} + +var accountsIdTemplates = template.Must(template.ParseFS(htmlFS, "html/base.html", "html/accountsId.html")) + +func handleAccountsIdGET(db *database.DB) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var accountId uuid.UUID + if err := accountId.Parse(r.PathValue("id")); err != nil { + errorResponse(w, r, http.StatusBadRequest, err) + return + } + account, err := db.LoadAccountById(accountId) + if err != nil { + errorResponse(w, r, http.StatusInternalServerError, err) + return + } + if account == nil { + errorResponse(w, r, http.StatusNotFound, err) + return + } + statePaths, err := db.LoadStatePaths() + if err != nil { + errorResponse(w, r, http.StatusInternalServerError, err) + return + } + versions, err := db.LoadVersionsByAccount(account) + if err != nil { + errorResponse(w, r, http.StatusInternalServerError, err) + return + } + isAdmin := "" + if account.IsAdmin { + isAdmin = "1" + } + render(w, accountsIdTemplates, http.StatusOK, AccountsIdPage{ + Account: account, + IsAdmin: isAdmin, + Page: makePage(r, &Page{ + Section: "accounts", + Title: account.Username, + }), + StatePaths: statePaths, + Versions: versions, + }) + }) +} diff --git a/pkg/webui/html/accounts.html b/pkg/webui/html/accounts.html index 80d699f..b6253ec 100644 --- a/pkg/webui/html/accounts.html +++ b/pkg/webui/html/accounts.html @@ -2,7 +2,7 @@

User Accounts

-

There are {{ len .Accounts }} user accounts.

+

There are {{ len .Accounts }} user accounts.

Use this page to inspect user accounts or create a new one.

@@ -16,6 +16,12 @@ required type="text" value="{{ .Username }}"> + +
{{ if .UsernameDuplicate }} This username already exist. diff --git a/pkg/webui/html/accountsId.html b/pkg/webui/html/accountsId.html new file mode 100644 index 0000000..8ecf70d --- /dev/null +++ b/pkg/webui/html/accountsId.html @@ -0,0 +1,92 @@ +{{ define "main" }} +

User Account

+{{ if ne .Account.PasswordReset nil }} +

Password Reset

+
+Direct the user to /account/{{ .Account.Id }}/reset/{{ .Account.PasswordReset }} so that they can create their password. +
+{{ end }} +

Status

+

+ The + account + {{ .Account.Username }} + was created on + {{ .Account.Created }} + and + {{ if eq .Account.Created .Account.LastLogin }} + never logged in. + {{ else }} + last logged in on + {{ .Account.LastLogin }}. + {{ end }} +

+{{ if .Account.IsAdmin }} +

This accounts has admin privileges on TfStated.

+{{ end }} +

Operations

+ +
+
+ Edit User Account +
+ + + + +
+ {{ if .UsernameDuplicate }} + This username already exist. + {{ else if .UsernameInvalid }} + + + Invalid username. + + Username must start with a letter and be composed of only letters, numbers or underscores. + + + + {{ end }} +
+ +
+
+
+ Danger Zone + + + +
+
+ +{{ if gt (len .Versions) 0 }} +

Activity

+
+ + + + + + + + + {{ range .Versions }} + + + + + {{ end }} + +
StateCreated
{{ index $.StatePaths .StateId.String }}{{ .Created }}
+
+{{ end }} +Go back to the user accounts list +{{ end }} diff --git a/pkg/webui/html/states.html b/pkg/webui/html/states.html index 0282bc5..493b74d 100644 --- a/pkg/webui/html/states.html +++ b/pkg/webui/html/states.html @@ -2,7 +2,7 @@

States

-

TfStated is currently managing {{ len .States }} states.

+

TfStated is currently managing {{ len .States }} states.

Use this page to inspect the existing states.

You also have the option to upload a JSON state file in order to create a new state in TfStated. This is equivalent to using the state push command of OpenTofu/Terraform on a brand new state.

diff --git a/pkg/webui/html/statesId.html b/pkg/webui/html/statesId.html index f309a98..8dd159e 100644 --- a/pkg/webui/html/statesId.html +++ b/pkg/webui/html/statesId.html @@ -33,7 +33,7 @@ {{ range .Versions }} {{ .Created }} - {{ index $.Usernames .AccountId.String }} + {{ index $.Usernames .AccountId.String }} {{ end }} diff --git a/pkg/webui/routes.go b/pkg/webui/routes.go index 59f3d6e..772489b 100644 --- a/pkg/webui/routes.go +++ b/pkg/webui/routes.go @@ -14,6 +14,7 @@ func addRoutes( requireLogin := loginMiddleware(db, requireSession) requireAdmin := adminMiddleware(db, requireLogin) mux.Handle("GET /accounts", requireAdmin(handleAccountsGET(db))) + mux.Handle("GET /accounts/{id}", requireAdmin(handleAccountsIdGET(db))) mux.Handle("POST /accounts", requireAdmin(handleAccountsPOST(db))) mux.Handle("GET /healthz", handleHealthz()) mux.Handle("GET /login", requireSession(handleLoginGET()))