feat(webui): add user accounts list, admin middleware and admin restricted menu entries
This commit is contained in:
parent
a83296b79a
commit
5c6ff8f901
9 changed files with 143 additions and 2 deletions
|
@ -54,6 +54,42 @@ 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;`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load accounts from database: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
accounts := make([]model.Account, 0)
|
||||
for rows.Next() {
|
||||
var (
|
||||
account model.Account
|
||||
created int64
|
||||
lastLogin int64
|
||||
)
|
||||
err = rows.Scan(
|
||||
&account.Id,
|
||||
&account.Username,
|
||||
&account.Salt,
|
||||
&account.PasswordHash,
|
||||
&account.IsAdmin,
|
||||
&created,
|
||||
&lastLogin,
|
||||
&account.Settings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load account from row: %w", err)
|
||||
}
|
||||
account.Created = time.Unix(created, 0)
|
||||
account.LastLogin = time.Unix(lastLogin, 0)
|
||||
accounts = append(accounts, account)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("failed to load accounts from rows: %w", err)
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
func (db *DB) LoadAccountUsernames() (map[string]string, error) {
|
||||
rows, err := db.Query(
|
||||
`SELECT id, username FROM accounts;`)
|
||||
|
|
|
@ -122,8 +122,8 @@ func (db *DB) LoadStates() ([]model.State, error) {
|
|||
defer rows.Close()
|
||||
states := make([]model.State, 0)
|
||||
for rows.Next() {
|
||||
var state model.State
|
||||
var (
|
||||
state model.State
|
||||
created int64
|
||||
updated int64
|
||||
)
|
||||
|
|
31
pkg/webui/accounts.go
Normal file
31
pkg/webui/accounts.go
Normal file
|
@ -0,0 +1,31 @@
|
|||
package webui
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"git.adyxax.org/adyxax/tfstated/pkg/database"
|
||||
"git.adyxax.org/adyxax/tfstated/pkg/model"
|
||||
)
|
||||
|
||||
type AccountsPage struct {
|
||||
ActiveTab int
|
||||
Page *Page
|
||||
Accounts []model.Account
|
||||
}
|
||||
|
||||
var accountsTemplates = template.Must(template.ParseFS(htmlFS, "html/base.html", "html/accounts.html"))
|
||||
|
||||
func handleAccountsGET(db *database.DB) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
accounts, err := db.LoadAccounts()
|
||||
if err != nil {
|
||||
errorResponse(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
render(w, accountsTemplates, http.StatusOK, AccountsPage{
|
||||
Page: makePage(r, &Page{Title: "User Accounts", Section: "accounts"}),
|
||||
Accounts: accounts,
|
||||
})
|
||||
})
|
||||
}
|
28
pkg/webui/admin.go
Normal file
28
pkg/webui/admin.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package webui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.adyxax.org/adyxax/tfstated/pkg/database"
|
||||
"git.adyxax.org/adyxax/tfstated/pkg/model"
|
||||
)
|
||||
|
||||
func adminMiddleware(db *database.DB, requireLogin func(http.Handler) http.Handler) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return requireLogin(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
account := r.Context().Value(model.AccountContextKey{})
|
||||
if account == nil {
|
||||
// this could happen if the account was deleted in the short
|
||||
// time between retrieving the session and here
|
||||
http.Redirect(w, r, "/login", http.StatusFound)
|
||||
return
|
||||
}
|
||||
if !account.(*model.Account).IsAdmin {
|
||||
errorResponse(w, http.StatusForbidden, fmt.Errorf("Only administrators can perform this request."))
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
}))
|
||||
}
|
||||
}
|
35
pkg/webui/html/accounts.html
Normal file
35
pkg/webui/html/accounts.html
Normal file
|
@ -0,0 +1,35 @@
|
|||
{{ define "main" }}
|
||||
<main class="responsive" id="main">
|
||||
<div>
|
||||
<div class="tabs">
|
||||
<a data-ui="#explorer"{{ if eq .ActiveTab 0 }} class="active"{{ end }}>User Accounts</a>
|
||||
<a data-ui="#new"{{ if eq .ActiveTab 1 }} class="active"{{ end }}>Create New User Account</a>
|
||||
</div>
|
||||
<div id="explorer" class="page padding{{ if eq .ActiveTab 0 }} active{{ end }}">
|
||||
<table class="clickable-rows no-space">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Created</th>
|
||||
<th>Last Login</th>
|
||||
<th>Is Admin</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .Accounts }}
|
||||
<tr>
|
||||
<td><a href="/accounts/{{ .Id }}">{{ .Username }}</a></td>
|
||||
<td><a href="/accounts/{{ .Id }}">{{ .Created }}</a></td>
|
||||
<td><a href="/accounts/{{ .Id }}">{{ .LastLogin }}</a></td>
|
||||
<td><a href="/accounts/{{ .Id }}">{{ .IsAdmin }}</a></td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="new" class="page padding{{ if eq .ActiveTab 1 }} active{{ end }}">
|
||||
<p>TODO</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{{ end }}
|
|
@ -20,6 +20,12 @@
|
|||
<i>settings</i>
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
{{ if .Page.IsAdmin }}
|
||||
<a href="/accounts"{{ if eq .Page.Section "accounts" }} class="fill"{{ end}}>
|
||||
<i>person</i>
|
||||
<span>User Accounts</span>
|
||||
</a>
|
||||
{{ end }}
|
||||
<a href="/logout">
|
||||
<i>logout</i>
|
||||
<span>Logout</span>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<main class="responsive" id="main">
|
||||
<p>
|
||||
Created by
|
||||
<a href="/users/{{ .Account.Id }}">{{ .Account.Username }}</a>
|
||||
<a href="/accounts/{{ .Account.Id }}" class="link underline">{{ .Account.Username }}</a>
|
||||
at {{ .Version.Created }}
|
||||
</p>
|
||||
<div>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)))
|
||||
|
|
Loading…
Add table
Reference in a new issue