update gui
This commit is contained in:
+257
-25
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user