update gui

This commit is contained in:
2026-07-28 20:45:34 -05:00
parent 8dcee4121b
commit 510d5490f9
13 changed files with 606 additions and 40 deletions
+257 -25
View File
@@ -2,6 +2,7 @@ package app
import (
"context"
"database/sql"
"errors"
"fmt"
"net/http"
@@ -10,6 +11,8 @@ import (
"time"
)
const maxAuthorizationDays = 90
func (s *Server) handlePortal(w http.ResponseWriter, r *http.Request) {
if hasAuditQuery(r) {
http.Redirect(w, r, "/portal/audit?"+r.URL.RawQuery, http.StatusSeeOther)
@@ -50,12 +53,18 @@ func (s *Server) handlePortal(w http.ResponseWriter, r *http.Request) {
Actions: actions,
NewCode: r.URL.Query().Get("code"),
DefaultHostLimit: s.cfg.DefaultHostLimit,
DefaultDuration: strconv.Itoa(int(s.cfg.DefaultDuration.Hours())),
DefaultDays: durationDays(s.cfg.DefaultDuration),
MaxHostLimit: s.cfg.MaxHostLimit,
Notice: r.URL.Query().Get("notice"),
CurrentView: "codes",
})
}
func durationDays(duration time.Duration) int {
const day = 24 * time.Hour
return int((duration + day - 1) / day)
}
func hasAuditQuery(r *http.Request) bool {
query := r.URL.Query()
for _, field := range []string{
@@ -342,16 +351,19 @@ func (s *Server) listAuthorizations(r *http.Request) ([]authorizationRecord, err
), '')
FROM authorizations a
WHERE a.created_at > datetime('now', '-30 days')
OR (a.revoked_at IS NULL AND a.expires_at > CURRENT_TIMESTAMP)
ORDER BY a.created_at DESC
LIMIT 100`,
)
if err != nil {
return nil, err
}
defer rows.Close()
var records []authorizationRecord
for rows.Next() {
var record authorizationRecord
record := authorizationRecord{
PackageIDs: make(map[uint64]bool),
ActionSlugs: make(map[string]bool),
}
if err := rows.Scan(
&record.ID,
&record.CodeHint,
@@ -369,7 +381,76 @@ func (s *Server) listAuthorizations(r *http.Request) ([]authorizationRecord, err
}
records = append(records, record)
}
return records, rows.Err()
if err := rows.Err(); err != nil {
_ = rows.Close()
return nil, err
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := s.loadAuthorizationSelections(r.Context(), records); err != nil {
return nil, err
}
return records, nil
}
func (s *Server) loadAuthorizationSelections(
ctx context.Context,
records []authorizationRecord,
) error {
indices := make(map[uint64]int, len(records))
for index := range records {
indices[records[index].ID] = index
}
if len(indices) == 0 {
return nil
}
packageRows, err := s.db.QueryContext(
ctx,
`SELECT authorization_id, package_id FROM authorization_packages`,
)
if err != nil {
return err
}
for packageRows.Next() {
var authorizationID uint64
var packageID uint64
if err := packageRows.Scan(&authorizationID, &packageID); err != nil {
_ = packageRows.Close()
return err
}
if index, ok := indices[authorizationID]; ok {
records[index].PackageIDs[packageID] = true
}
}
if err := packageRows.Err(); err != nil {
_ = packageRows.Close()
return err
}
if err := packageRows.Close(); err != nil {
return err
}
actionRows, err := s.db.QueryContext(
ctx,
`SELECT authorization_id, action_slug FROM authorization_actions`,
)
if err != nil {
return err
}
defer actionRows.Close()
for actionRows.Next() {
var authorizationID uint64
var actionSlug string
if err := actionRows.Scan(&authorizationID, &actionSlug); err != nil {
return err
}
if index, ok := indices[authorizationID]; ok {
records[index].ActionSlugs[actionSlug] = true
}
}
return actionRows.Err()
}
func (s *Server) handleCreateAuthorization(w http.ResponseWriter, r *http.Request) {
@@ -379,9 +460,9 @@ func (s *Server) handleCreateAuthorization(w http.ResponseWriter, r *http.Reques
http.Error(w, "invalid host limit", http.StatusBadRequest)
return
}
durationHours, err := strconv.Atoi(r.FormValue("duration_hours"))
if err != nil || durationHours < 1 || durationHours > 24 {
http.Error(w, "duration must be between 1 and 24 hours", http.StatusBadRequest)
durationDays, err := strconv.Atoi(r.FormValue("duration_days"))
if err != nil || durationDays < 1 || durationDays > maxAuthorizationDays {
http.Error(w, "duration must be between 1 and 90 days", http.StatusBadRequest)
return
}
packageIDs := r.Form["package_id"]
@@ -396,7 +477,7 @@ func (s *Server) handleCreateAuthorization(w http.ResponseWriter, r *http.Reques
return
}
codeHash := hashValue(code)
expiresAt := time.Now().UTC().Add(time.Duration(durationHours) * time.Hour)
expiresAt := time.Now().UTC().Add(time.Duration(durationDays) * 24 * time.Hour)
customerLabel := strings.TrimSpace(r.FormValue("customer_label"))
if len(customerLabel) > 255 {
http.Error(w, "customer label is too long", http.StatusBadRequest)
@@ -421,14 +502,41 @@ func (s *Server) handleCreateAuthorization(w http.ResponseWriter, r *http.Reques
return
}
authorizationID, _ := result.LastInsertId()
if status, err := saveAuthorizationSelections(
r.Context(), tx, uint64(authorizationID), packageIDs, actionSlugs,
); err != nil {
http.Error(w, err.Error(), status)
return
}
if err := tx.Commit(); err != nil {
http.Error(w, "unable to save authorization", http.StatusInternalServerError)
return
}
id := uint64(authorizationID)
_ = s.audit(r.Context(), "authorization_created", tech.Login, &id, "", "", s.clientIP(r),
fmt.Sprintf("host_limit=%d duration_days=%d customer=%q", hostLimit, durationDays, customerLabel))
http.Redirect(w, r, "/portal?code="+code, http.StatusSeeOther)
}
func saveAuthorizationSelections(
ctx context.Context,
tx *sql.Tx,
authorizationID uint64,
packageIDs []string,
actionSlugs []string,
) (int, error) {
seenPackages := make(map[uint64]bool)
for _, rawID := range packageIDs {
packageID, err := strconv.ParseUint(rawID, 10, 64)
if err != nil {
http.Error(w, "invalid package selection", http.StatusBadRequest)
return
return http.StatusBadRequest, errors.New("invalid package selection")
}
if seenPackages[packageID] {
continue
}
seenPackages[packageID] = true
result, err := tx.ExecContext(
r.Context(),
ctx,
`INSERT INTO authorization_packages
(authorization_id, package_id, package_slug, display_name,
package_name, package_version, file_name, sha256)
@@ -439,35 +547,148 @@ func (s *Server) handleCreateAuthorization(w http.ResponseWriter, r *http.Reques
authorizationID, packageID,
)
if err != nil {
http.Error(w, "unable to authorize package", http.StatusInternalServerError)
return
return http.StatusInternalServerError, errors.New("unable to authorize package")
}
affected, _ := result.RowsAffected()
if affected != 1 {
http.Error(w, "selected package is unavailable", http.StatusBadRequest)
return
return http.StatusBadRequest, errors.New("selected package is unavailable")
}
}
seenActions := make(map[string]bool)
for _, actionSlug := range actionSlugs {
actionSlug = strings.TrimSpace(actionSlug)
if !validSlug(actionSlug) {
http.Error(w, "invalid installer action", http.StatusBadRequest)
return
return http.StatusBadRequest, errors.New("invalid installer action")
}
if seenActions[actionSlug] {
continue
}
seenActions[actionSlug] = true
result, err := tx.ExecContext(
r.Context(),
ctx,
`INSERT INTO authorization_actions (authorization_id, action_slug)
SELECT ?, slug FROM installer_actions
WHERE slug = ? AND enabled = TRUE`,
authorizationID, actionSlug,
)
if err != nil {
http.Error(w, "unable to authorize installer action", http.StatusInternalServerError)
return
return http.StatusInternalServerError, errors.New("unable to authorize installer action")
}
affected, _ := result.RowsAffected()
if affected != 1 {
http.Error(w, "selected installer action is unavailable", http.StatusBadRequest)
return http.StatusBadRequest, errors.New("selected installer action is unavailable")
}
}
return 0, nil
}
func (s *Server) handleUpdateAuthorization(w http.ResponseWriter, r *http.Request) {
tech, _ := s.currentTechnician(r)
id, err := parseUintPath(r, "id")
if err != nil {
http.Error(w, "invalid authorization", http.StatusBadRequest)
return
}
hostLimit, err := strconv.Atoi(r.FormValue("host_limit"))
if err != nil || hostLimit < 1 || hostLimit > s.cfg.MaxHostLimit {
http.Error(w, "invalid host limit", http.StatusBadRequest)
return
}
extendDays, err := strconv.Atoi(r.FormValue("extend_days"))
if err != nil || extendDays < 0 || extendDays > maxAuthorizationDays {
http.Error(w, "extension must be between 0 and 90 days", http.StatusBadRequest)
return
}
packageIDs := r.Form["package_id"]
actionSlugs := r.Form["action_slug"]
if len(packageIDs) == 0 && len(actionSlugs) == 0 {
http.Error(w, "select at least one package or installer action", http.StatusBadRequest)
return
}
tx, err := s.db.BeginTx(r.Context(), nil)
if err != nil {
http.Error(w, "unable to update authorization", http.StatusInternalServerError)
return
}
defer tx.Rollback()
var oldHostLimit int
var hostCount int
var expiresAt time.Time
var revokedAt sql.NullTime
err = tx.QueryRowContext(
r.Context(),
`SELECT a.host_limit,
(SELECT COUNT(*) FROM authorization_hosts h
WHERE h.authorization_id = a.id),
a.expires_at, a.revoked_at
FROM authorizations a
WHERE a.id = ?`,
id,
).Scan(&oldHostLimit, &hostCount, &expiresAt, &revokedAt)
if errors.Is(err, sql.ErrNoRows) {
http.Error(w, "authorization not found", http.StatusNotFound)
return
}
if err != nil {
http.Error(w, "unable to update authorization", http.StatusInternalServerError)
return
}
if revokedAt.Valid || !time.Now().Before(expiresAt) {
http.Error(w, "closed authorizations cannot be edited", http.StatusBadRequest)
return
}
if hostLimit < hostCount {
http.Error(
w,
fmt.Sprintf("host limit cannot be lower than %d enrolled hosts", hostCount),
http.StatusBadRequest,
)
return
}
newExpiresAt := expiresAt.Add(time.Duration(extendDays) * 24 * time.Hour)
if _, err := tx.ExecContext(
r.Context(),
`DELETE FROM authorization_packages WHERE authorization_id = ?`,
id,
); err != nil {
http.Error(w, "unable to update authorized packages", http.StatusInternalServerError)
return
}
if _, err := tx.ExecContext(
r.Context(),
`DELETE FROM authorization_actions WHERE authorization_id = ?`,
id,
); err != nil {
http.Error(w, "unable to update installer actions", http.StatusInternalServerError)
return
}
if status, err := saveAuthorizationSelections(
r.Context(), tx, id, packageIDs, actionSlugs,
); err != nil {
http.Error(w, err.Error(), status)
return
}
if _, err := tx.ExecContext(
r.Context(),
`UPDATE authorizations SET host_limit = ?, expires_at = ? WHERE id = ?`,
hostLimit, newExpiresAt, id,
); err != nil {
http.Error(w, "unable to update authorization", http.StatusInternalServerError)
return
}
if extendDays > 0 {
if _, err := tx.ExecContext(
r.Context(),
`UPDATE download_sessions
SET expires_at = ?
WHERE authorization_id = ? AND revoked_at IS NULL`,
newExpiresAt, id,
); err != nil {
http.Error(w, "unable to extend download sessions", http.StatusInternalServerError)
return
}
}
@@ -475,10 +696,21 @@ func (s *Server) handleCreateAuthorization(w http.ResponseWriter, r *http.Reques
http.Error(w, "unable to save authorization", http.StatusInternalServerError)
return
}
id := uint64(authorizationID)
_ = s.audit(r.Context(), "authorization_created", tech.Login, &id, "", "", s.clientIP(r),
fmt.Sprintf("host_limit=%d duration_hours=%d customer=%q", hostLimit, durationHours, customerLabel))
http.Redirect(w, r, "/portal?code="+code, http.StatusSeeOther)
_ = s.audit(
r.Context(),
"authorization_updated",
tech.Login,
&id,
"",
"",
s.clientIP(r),
fmt.Sprintf(
"host_limit=%d->%d extend_days=%d packages=%d actions=%d",
oldHostLimit, hostLimit, extendDays, len(packageIDs), len(actionSlugs),
),
)
http.Redirect(w, r, "/portal?notice=Authorization+updated", http.StatusSeeOther)
}
func (s *Server) handleRevokeAuthorization(w http.ResponseWriter, r *http.Request) {