add app review system with validation pipeline and admin htmx UI
This commit is contained in:
@@ -2,15 +2,20 @@ package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/omixlab/mosis-portal/internal/database"
|
||||
"github.com/omixlab/mosis-portal/internal/review"
|
||||
"github.com/omixlab/mosis-portal/internal/storage"
|
||||
)
|
||||
|
||||
// Handler handles web page requests
|
||||
type Handler struct {
|
||||
db *database.DB
|
||||
templates *Templates
|
||||
store *storage.Storage
|
||||
review *review.Service
|
||||
}
|
||||
|
||||
// NewHandler creates a new web handler
|
||||
@@ -22,9 +27,15 @@ func NewHandler(db *database.DB) (*Handler, error) {
|
||||
return &Handler{
|
||||
db: db,
|
||||
templates: templates,
|
||||
review: review.New(db),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetStorage sets the storage instance for the handler
|
||||
func (h *Handler) SetStorage(store *storage.Storage) {
|
||||
h.store = store
|
||||
}
|
||||
|
||||
// PageData is the base data structure for all pages
|
||||
type PageData struct {
|
||||
Title string
|
||||
@@ -192,3 +203,236 @@ func getDeveloperFromContext(r *http.Request) *database.Developer {
|
||||
developer, _ := r.Context().Value("developer").(*database.Developer)
|
||||
return developer
|
||||
}
|
||||
|
||||
// AdminReviewQueue renders the admin review queue page
|
||||
func (h *Handler) AdminReviewQueue(w http.ResponseWriter, r *http.Request) {
|
||||
developer := getDeveloperFromContext(r)
|
||||
if developer == nil {
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Add admin role check here
|
||||
pending, approved, rejected, _ := h.db.GetReviewStats(r.Context())
|
||||
|
||||
data := struct {
|
||||
PageData
|
||||
Stats struct {
|
||||
Pending int
|
||||
Approved int
|
||||
Rejected int
|
||||
}
|
||||
}{
|
||||
PageData: PageData{
|
||||
Title: "Review Queue",
|
||||
ActiveNav: "admin",
|
||||
Developer: developer,
|
||||
},
|
||||
}
|
||||
data.Stats.Pending = pending
|
||||
data.Stats.Approved = approved
|
||||
data.Stats.Rejected = rejected
|
||||
|
||||
h.render(w, "admin_review_queue", data)
|
||||
}
|
||||
|
||||
// AdminReviewDetail renders the review detail page
|
||||
func (h *Handler) AdminReviewDetail(w http.ResponseWriter, r *http.Request) {
|
||||
developer := getDeveloperFromContext(r)
|
||||
if developer == nil {
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
versionID := chi.URLParam(r, "versionID")
|
||||
vwa, err := h.db.GetVersionWithApp(r.Context(), versionID)
|
||||
if err != nil {
|
||||
http.Error(w, "Version not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Run validation if package exists
|
||||
var validationResult *review.FullValidationResult
|
||||
if h.store != nil && vwa.Version.PackageURL != "" {
|
||||
packagePath := h.store.GetPackagePath(vwa.Version.PackageURL)
|
||||
result, err := h.review.ValidatePackage(packagePath)
|
||||
if err == nil {
|
||||
validationResult = result
|
||||
}
|
||||
}
|
||||
|
||||
data := struct {
|
||||
PageData
|
||||
App *database.App
|
||||
Version *database.AppVersion
|
||||
DeveloperName string
|
||||
DeveloperEmail string
|
||||
Validation *review.FullValidationResult
|
||||
}{
|
||||
PageData: PageData{
|
||||
Title: "Review: " + vwa.App.Name,
|
||||
ActiveNav: "admin",
|
||||
Developer: developer,
|
||||
},
|
||||
App: vwa.App,
|
||||
Version: vwa.Version,
|
||||
DeveloperName: vwa.DeveloperName,
|
||||
DeveloperEmail: vwa.DeveloperEmail,
|
||||
Validation: validationResult,
|
||||
}
|
||||
|
||||
h.render(w, "admin_review_detail", data)
|
||||
}
|
||||
|
||||
// AdminReviewQueuePartial renders the review queue list partial for htmx
|
||||
func (h *Handler) AdminReviewQueuePartial(w http.ResponseWriter, r *http.Request) {
|
||||
developer := getDeveloperFromContext(r)
|
||||
if developer == nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
limit := 20
|
||||
offset := (page - 1) * limit
|
||||
|
||||
items, total, err := h.db.GetVersionsInReview(r.Context(), limit, offset)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to load queue", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
data := struct {
|
||||
Items []database.VersionWithApp
|
||||
Pagination struct {
|
||||
Page int
|
||||
Limit int
|
||||
Total int
|
||||
TotalPages int
|
||||
}
|
||||
}{
|
||||
Items: items,
|
||||
}
|
||||
data.Pagination.Page = page
|
||||
data.Pagination.Limit = limit
|
||||
data.Pagination.Total = total
|
||||
data.Pagination.TotalPages = (total + limit - 1) / limit
|
||||
|
||||
h.renderPartial(w, "review_queue_list", data)
|
||||
}
|
||||
|
||||
// AdminApprove handles approval of a version from the admin UI
|
||||
func (h *Handler) AdminApprove(w http.ResponseWriter, r *http.Request) {
|
||||
developer := getDeveloperFromContext(r)
|
||||
if developer == nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
versionID := chi.URLParam(r, "versionID")
|
||||
notes := r.FormValue("notes")
|
||||
|
||||
// Verify version exists and is in review
|
||||
version, err := h.db.GetVersion(r.Context(), versionID)
|
||||
if err != nil {
|
||||
http.Error(w, "Version not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if version.Status != "in_review" {
|
||||
http.Error(w, "Version is not in review", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Approve the version
|
||||
if err := h.review.ApproveVersion(r.Context(), versionID, notes); err != nil {
|
||||
http.Error(w, "Failed to approve version: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Redirect back to queue via htmx
|
||||
w.Header().Set("HX-Redirect", "/admin/review-queue")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// AdminReject handles rejection of a version from the admin UI
|
||||
func (h *Handler) AdminReject(w http.ResponseWriter, r *http.Request) {
|
||||
developer := getDeveloperFromContext(r)
|
||||
if developer == nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
versionID := chi.URLParam(r, "versionID")
|
||||
reason := r.FormValue("reason")
|
||||
message := r.FormValue("message")
|
||||
|
||||
if reason == "" {
|
||||
http.Error(w, "Rejection reason is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify version exists and is in review
|
||||
version, err := h.db.GetVersion(r.Context(), versionID)
|
||||
if err != nil {
|
||||
http.Error(w, "Version not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if version.Status != "in_review" {
|
||||
http.Error(w, "Version is not in review", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Reject the version
|
||||
feedback := &review.RejectionFeedback{
|
||||
Reason: reason,
|
||||
Message: message,
|
||||
CanResubmit: true,
|
||||
}
|
||||
if err := h.review.RejectVersion(r.Context(), versionID, feedback); err != nil {
|
||||
http.Error(w, "Failed to reject version: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Redirect back to queue via htmx
|
||||
w.Header().Set("HX-Redirect", "/admin/review-queue")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// AdminValidate runs validation and returns HTML partial with results
|
||||
func (h *Handler) AdminValidate(w http.ResponseWriter, r *http.Request) {
|
||||
developer := getDeveloperFromContext(r)
|
||||
if developer == nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
versionID := chi.URLParam(r, "versionID")
|
||||
|
||||
// Get version with package info
|
||||
version, err := h.db.GetVersion(r.Context(), versionID)
|
||||
if err != nil {
|
||||
http.Error(w, "Version not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if version.PackageURL == "" {
|
||||
http.Error(w, "Version has no uploaded package", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Run validation
|
||||
packagePath := h.store.GetPackagePath(version.PackageURL)
|
||||
result, err := h.review.ValidatePackage(packagePath)
|
||||
if err != nil {
|
||||
http.Error(w, "Validation error: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Render validation results partial
|
||||
h.renderPartial(w, "validation_results", result)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user