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) {
|
func (db *DB) LoadAccountUsernames() (map[string]string, error) {
|
||||||
rows, err := db.Query(
|
rows, err := db.Query(
|
||||||
`SELECT id, username FROM accounts;`)
|
`SELECT id, username FROM accounts;`)
|
||||||
|
|
|
@ -122,8 +122,8 @@ func (db *DB) LoadStates() ([]model.State, error) {
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
states := make([]model.State, 0)
|
states := make([]model.State, 0)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var state model.State
|
|
||||||
var (
|
var (
|
||||||
|
state model.State
|
||||||
created int64
|
created int64
|
||||||
updated 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>
|
<i>settings</i>
|
||||||
<span>Settings</span>
|
<span>Settings</span>
|
||||||
</a>
|
</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">
|
<a href="/logout">
|
||||||
<i>logout</i>
|
<i>logout</i>
|
||||||
<span>Logout</span>
|
<span>Logout</span>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<main class="responsive" id="main">
|
<main class="responsive" id="main">
|
||||||
<p>
|
<p>
|
||||||
Created by
|
Created by
|
||||||
<a href="/users/{{ .Account.Id }}">{{ .Account.Username }}</a>
|
<a href="/accounts/{{ .Account.Id }}" class="link underline">{{ .Account.Username }}</a>
|
||||||
at {{ .Version.Created }}
|
at {{ .Version.Created }}
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Page struct {
|
type Page struct {
|
||||||
|
IsAdmin bool
|
||||||
LightMode bool
|
LightMode bool
|
||||||
Precedent string
|
Precedent string
|
||||||
Section string
|
Section string
|
||||||
|
@ -15,6 +16,8 @@ type Page struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func makePage(r *http.Request, page *Page) *Page {
|
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)
|
settings := r.Context().Value(model.SettingsContextKey{}).(*model.Settings)
|
||||||
page.LightMode = settings.LightMode
|
page.LightMode = settings.LightMode
|
||||||
return page
|
return page
|
||||||
|
|
|
@ -12,6 +12,8 @@ func addRoutes(
|
||||||
) {
|
) {
|
||||||
requireSession := sessionsMiddleware(db)
|
requireSession := sessionsMiddleware(db)
|
||||||
requireLogin := loginMiddleware(db, requireSession)
|
requireLogin := loginMiddleware(db, requireSession)
|
||||||
|
requireAdmin := adminMiddleware(db, requireLogin)
|
||||||
|
mux.Handle("GET /accounts", requireAdmin(handleAccountsGET(db)))
|
||||||
mux.Handle("GET /healthz", handleHealthz())
|
mux.Handle("GET /healthz", handleHealthz())
|
||||||
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)))
|
||||||
|
|
Loading…
Add table
Reference in a new issue