package app import ( "context" "errors" "fmt" "net/http" "strconv" "strings" "time" ) func (s *Server) handlePortal(w http.ResponseWriter, r *http.Request) { tech, err := s.currentTechnician(r) if err != nil { http.Redirect(w, r, "/", http.StatusSeeOther) return } packages, err := s.listPackages(r) if err != nil { http.Error(w, "unable to list packages", http.StatusInternalServerError) return } authorizations, err := s.listAuthorizations(r) if err != nil { http.Error(w, "unable to list authorizations", http.StatusInternalServerError) return } auditEvents, err := s.listAuditEvents(r) if err != nil { http.Error(w, "unable to list audit events", http.StatusInternalServerError) return } s.render(w, "portal.html", pageData{ Title: "Deployment Access", Technician: tech, CSRFToken: tech.CSRFToken, Authorizations: authorizations, AuditEvents: auditEvents, Packages: packages, NewCode: r.URL.Query().Get("code"), DefaultHostLimit: s.cfg.DefaultHostLimit, DefaultDuration: strconv.Itoa(int(s.cfg.DefaultDuration.Hours())), Notice: r.URL.Query().Get("notice"), }) } func (s *Server) listAuditEvents(r *http.Request) ([]auditRecord, error) { rows, err := s.db.QueryContext( r.Context(), `SELECT event_type, actor, hostname, package_slug, source_ip, details, created_at FROM audit_events ORDER BY created_at DESC LIMIT 100`, ) if err != nil { return nil, err } defer rows.Close() var records []auditRecord for rows.Next() { var record auditRecord if err := rows.Scan( &record.EventType, &record.Actor, &record.Hostname, &record.PackageSlug, &record.SourceIP, &record.Details, &record.CreatedAt, ); err != nil { return nil, err } records = append(records, record) } return records, rows.Err() } func (s *Server) listPackages(r *http.Request) ([]packageRecord, error) { rows, err := s.db.QueryContext( r.Context(), `SELECT id, slug, display_name, package_name, package_version, file_name, sha256, enabled FROM packages ORDER BY display_name, package_version`, ) if err != nil { return nil, err } defer rows.Close() var records []packageRecord for rows.Next() { var record packageRecord if err := rows.Scan( &record.ID, &record.Slug, &record.DisplayName, &record.PackageName, &record.PackageVersion, &record.FileName, &record.SHA256, &record.Enabled, ); err != nil { return nil, err } records = append(records, record) } return records, rows.Err() } func (s *Server) listAuthorizations(r *http.Request) ([]authorizationRecord, error) { rows, err := s.db.QueryContext( r.Context(), `SELECT a.id, a.code_hint, a.created_by, a.customer_label, a.host_limit, COUNT(DISTINCT h.id), a.created_at, a.expires_at, a.revoked_at, COALESCE(GROUP_CONCAT(DISTINCT p.display_name ORDER BY p.display_name SEPARATOR ', '), '') FROM authorizations a LEFT JOIN authorization_hosts h ON h.authorization_id = a.id LEFT JOIN authorization_packages ap ON ap.authorization_id = a.id LEFT JOIN packages p ON p.id = ap.package_id WHERE a.created_at > UTC_TIMESTAMP(6) - INTERVAL 30 DAY GROUP BY a.id 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 if err := rows.Scan( &record.ID, &record.CodeHint, &record.CreatedBy, &record.CustomerLabel, &record.HostLimit, &record.HostCount, &record.CreatedAt, &record.ExpiresAt, &record.RevokedAt, &record.Packages, ); err != nil { return nil, err } records = append(records, record) } return records, rows.Err() } func (s *Server) handleCreateAuthorization(w http.ResponseWriter, r *http.Request) { tech, _ := s.currentTechnician(r) 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 } 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) return } packageIDs := r.Form["package_id"] if len(packageIDs) == 0 { http.Error(w, "select at least one package", http.StatusBadRequest) return } code, err := deploymentCode() if err != nil { http.Error(w, "unable to generate code", http.StatusInternalServerError) return } codeHash := hashValue(code) expiresAt := time.Now().UTC().Add(time.Duration(durationHours) * time.Hour) customerLabel := strings.TrimSpace(r.FormValue("customer_label")) if len(customerLabel) > 255 { http.Error(w, "customer label is too long", http.StatusBadRequest) return } tx, err := s.db.BeginTx(r.Context(), nil) if err != nil { http.Error(w, "unable to create authorization", http.StatusInternalServerError) return } defer tx.Rollback() result, err := tx.ExecContext( r.Context(), `INSERT INTO authorizations (code_hash, code_hint, created_by, customer_label, host_limit, expires_at) VALUES (?, ?, ?, ?, ?, ?)`, codeHash[:], code[len(code)-5:], tech.Login, customerLabel, hostLimit, expiresAt, ) if err != nil { http.Error(w, "unable to create authorization", http.StatusInternalServerError) return } authorizationID, _ := result.LastInsertId() for _, rawID := range packageIDs { packageID, err := strconv.ParseUint(rawID, 10, 64) if err != nil { http.Error(w, "invalid package selection", http.StatusBadRequest) return } result, err := tx.ExecContext( r.Context(), `INSERT INTO authorization_packages (authorization_id, package_id) SELECT ?, id FROM packages WHERE id = ? AND enabled = TRUE`, authorizationID, packageID, ) if err != nil { http.Error(w, "unable to authorize package", http.StatusInternalServerError) return } affected, _ := result.RowsAffected() if affected != 1 { http.Error(w, "selected package is unavailable", http.StatusBadRequest) 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_hours=%d customer=%q", hostLimit, durationHours, customerLabel)) http.Redirect(w, r, "/portal?code="+code, http.StatusSeeOther) } func (s *Server) handleRevokeAuthorization(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 } result, err := s.db.ExecContext( r.Context(), `UPDATE authorizations SET revoked_at = COALESCE(revoked_at, UTC_TIMESTAMP(6)) WHERE id = ?`, id, ) if err != nil { http.Error(w, "unable to revoke authorization", http.StatusInternalServerError) return } affected, _ := result.RowsAffected() if affected == 0 { http.Error(w, "authorization not found", http.StatusNotFound) return } _, _ = s.db.ExecContext( r.Context(), `UPDATE download_sessions SET revoked_at = UTC_TIMESTAMP(6) WHERE authorization_id = ? AND revoked_at IS NULL`, id, ) _ = s.audit(r.Context(), "authorization_revoked", tech.Login, &id, "", "", s.clientIP(r), "") http.Redirect(w, r, "/portal?notice=Authorization+revoked", http.StatusSeeOther) } func (s *Server) handleUpsertPackage(w http.ResponseWriter, r *http.Request) { tech, _ := s.currentTechnician(r) fields := []string{ "slug", "display_name", "package_name", "package_version", "file_name", "sha256", } values := make(map[string]string) for _, field := range fields { values[field] = strings.TrimSpace(r.FormValue(field)) if values[field] == "" { http.Error(w, field+" is required", http.StatusBadRequest) return } } if !validSlug(values["slug"]) || !validSHA256(values["sha256"]) { http.Error(w, "invalid slug or SHA-256", http.StatusBadRequest) return } enabled := r.FormValue("enabled") == "1" _, err := s.db.ExecContext( r.Context(), `INSERT INTO packages (slug, display_name, package_name, package_version, file_name, sha256, enabled) VALUES (?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE display_name = VALUES(display_name), package_name = VALUES(package_name), package_version = VALUES(package_version), file_name = VALUES(file_name), sha256 = VALUES(sha256), enabled = VALUES(enabled)`, values["slug"], values["display_name"], values["package_name"], values["package_version"], values["file_name"], strings.ToLower(values["sha256"]), enabled, ) if err != nil { http.Error(w, "unable to save package", http.StatusInternalServerError) return } _ = s.audit(r.Context(), "package_saved", tech.Login, nil, "", values["slug"], s.clientIP(r), fmt.Sprintf("version=%s enabled=%t", values["package_version"], enabled)) http.Redirect(w, r, "/portal?notice=Package+saved", http.StatusSeeOther) } func validSlug(value string) bool { if value == "" || len(value) > 100 { return false } for _, character := range value { if (character < 'a' || character > 'z') && (character < '0' || character > '9') && character != '-' && character != '_' { return false } } return true } func validSHA256(value string) bool { if len(value) != 64 { return false } for _, character := range strings.ToLower(value) { if (character < '0' || character > '9') && (character < 'a' || character > 'f') { return false } } return true } func (s *Server) audit( ctx context.Context, eventType string, actor string, authorizationID *uint64, hostname string, packageSlug string, sourceIP string, details string, ) error { var authID any if authorizationID != nil { authID = *authorizationID } _, err := s.db.ExecContext( ctx, `INSERT INTO audit_events (event_type, actor, authorization_id, hostname, package_slug, source_ip, details) VALUES (?, ?, ?, ?, ?, ?, ?)`, eventType, actor, authID, hostname, packageSlug, sourceIP, details, ) return err } var errAuthorizationDenied = errors.New("authorization denied")