package handlers import ( "context" "crypto/rand" "database/sql" "encoding/hex" "encoding/json" "fmt" "log" "net/http" "os" "path/filepath" "time" "golang.org/x/crypto/bcrypt" "pdf-wizard/internal/db" "pdf-wizard/internal/htmlform" "pdf-wizard/internal/llm" "pdf-wizard/internal/models" "pdf-wizard/internal/pdfcontent" ) const sessionDuration = 24 * time.Hour type AdminHandler struct { db *sql.DB dataDir string pdfDir string } func NewAdminHandler(db *sql.DB, dataDir string) *AdminHandler { return &AdminHandler{ db: db, dataDir: dataDir, pdfDir: filepath.Join(dataDir, "pdfs"), } } func (h *AdminHandler) Login(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } var req models.LoginRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"}) return } user, err := db.GetUserByUsername(h.db, req.Username) if err != nil { log.Printf("Login error: %v", err) writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid credentials"}) return } if user == nil { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid credentials"}) return } if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid credentials"}) return } token, err := generateToken() if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to create session"}) return } session, err := db.CreateSession(h.db, user.ID, token, sessionDuration) if err != nil { log.Printf("Session creation error: %v", err) writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to create session"}) return } http.SetCookie(w, &http.Cookie{ Name: "admin_session", Value: token, Path: "/", Expires: session.ExpiresAt, HttpOnly: true, SameSite: http.SameSiteStrictMode, }) writeJSON(w, http.StatusOK, models.LoginResponse{Token: token}) } func (h *AdminHandler) Logout(w http.ResponseWriter, r *http.Request) { token := getSessionToken(r) if token != "" { db.DeleteSession(h.db, token) } http.SetCookie(w, &http.Cookie{ Name: "admin_session", Value: "", Path: "/", Expires: time.Unix(0, 0), HttpOnly: true, SameSite: http.SameSiteStrictMode, }) http.Redirect(w, r, "/admin/login", http.StatusSeeOther) } func (h *AdminHandler) AdminPage(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } user, err := h.requireAuth(r) if err != nil { http.Redirect(w, r, "/admin/login", http.StatusSeeOther) return } _ = user http.ServeFile(w, r, "frontend/dist/index.html") } func (h *AdminHandler) LoginPage(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } token := getSessionToken(r) if token != "" { session, err := db.GetSessionByToken(h.db, token) if err == nil && session != nil && session.ExpiresAt.After(time.Now()) { http.Redirect(w, r, "/admin", http.StatusSeeOther) return } } http.ServeFile(w, r, "frontend/dist/index.html") } func (h *AdminHandler) UserPage(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } user, err := h.requireAuth(r) if err != nil { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"}) return } writeJSON(w, http.StatusOK, map[string]interface{}{ "username": user.Username, }) } func (h *AdminHandler) ListOrgs(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } user, err := h.requireAuth(r) if err != nil { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"}) return } orgs, err := db.GetUserOrgs(h.db, user.ID) if err != nil { log.Printf("Error listing orgs: %v", err) writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal error"}) return } type orgItem struct { ID int `json:"id"` Name string `json:"name"` } result := make([]orgItem, len(orgs)) for i, o := range orgs { result[i] = orgItem{ID: o.ID, Name: o.Name} } writeJSON(w, http.StatusOK, result) } func (h *AdminHandler) ListPDFs(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } user, err := h.requireAuth(r) if err != nil { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"}) return } orgID := r.URL.Query().Get("org_id") if orgID == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing org_id"}) return } var id int if _, err := fmt.Sscanf(orgID, "%d", &id); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid org_id"}) return } inOrg, err := db.IsUserInOrg(h.db, user.ID, id) if err != nil || !inOrg { writeJSON(w, http.StatusForbidden, map[string]string{"error": "access denied"}) return } pdfs, err := db.ListOrgPDFs(h.db, id) if err != nil { log.Printf("Error listing PDFs: %v", err) writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal error"}) return } writeJSON(w, http.StatusOK, pdfs) } func (h *AdminHandler) GetMarkdown(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } user, err := h.requireAuth(r) if err != nil { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"}) return } pdf, err := h.requirePDFAccess(r, user) if err != nil { writeJSON(w, http.StatusForbidden, map[string]string{"error": err.Error()}) return } if pdf.CustomMarkdown != nil && *pdf.CustomMarkdown != "" { writeJSON(w, http.StatusOK, models.MarkdownResponse{Markdown: *pdf.CustomMarkdown}) return } pdfPath := filepath.Join(h.pdfDir, fmt.Sprintf("%s.pdf", pdf.MD5Hash)) if _, statErr := os.Stat(pdfPath); statErr != nil { writeJSON(w, http.StatusNotFound, map[string]string{"error": "PDF file not found"}) return } markdown, err := pdfcontent.ExtractPDFContent(pdfPath) if err != nil { log.Printf("PDF %d: extraction failed: %v", pdf.ID, err) writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "extraction failed"}) return } writeJSON(w, http.StatusOK, models.MarkdownResponse{Markdown: markdown}) } func (h *AdminHandler) SaveMarkdown(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } user, err := h.requireAuth(r) if err != nil { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"}) return } pdf, err := h.requirePDFAccess(r, user) if err != nil { writeJSON(w, http.StatusForbidden, map[string]string{"error": err.Error()}) return } var req models.MarkdownRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"}) return } if req.Markdown == "" { if err := db.ClearCustomMarkdown(h.db, pdf.ID); err != nil { log.Printf("Error clearing markdown: %v", err) writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal error"}) return } writeJSON(w, http.StatusOK, map[string]string{"status": "reset to default"}) return } if err := db.UpdateCustomMarkdown(h.db, pdf.ID, req.Markdown); err != nil { log.Printf("Error saving markdown: %v", err) writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal error"}) return } // Save version snapshot if _, err := db.CreateMarkdownVersion(h.db, pdf.ID, req.Markdown); err != nil { log.Printf("Error saving markdown version: %v", err) } writeJSON(w, http.StatusOK, map[string]string{"status": "saved"}) } func (h *AdminHandler) ListMarkdownVersions(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } user, err := h.requireAuth(r) if err != nil { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"}) return } pdf, err := h.requirePDFAccess(r, user) if err != nil { writeJSON(w, http.StatusForbidden, map[string]string{"error": err.Error()}) return } versions, err := db.ListMarkdownVersions(h.db, pdf.ID) if err != nil { log.Printf("Error listing markdown versions: %v", err) writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal error"}) return } writeJSON(w, http.StatusOK, versions) } func (h *AdminHandler) GetMarkdownVersion(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } user, err := h.requireAuth(r) if err != nil { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"}) return } versionIDStr := r.URL.Query().Get("version_id") if versionIDStr == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing version_id"}) return } var versionID int if _, err := fmt.Sscanf(versionIDStr, "%d", &versionID); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid version_id"}) return } version, err := db.GetMarkdownVersion(h.db, versionID) if err != nil { log.Printf("Error getting markdown version: %v", err) writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal error"}) return } if version == nil { writeJSON(w, http.StatusNotFound, map[string]string{"error": "version not found"}) return } // Verify user has access to the PDF pdf, err := h.requirePDFAccessByID(r, user, version.PDFID) if err != nil { writeJSON(w, http.StatusForbidden, map[string]string{"error": "access denied"}) return } _ = pdf writeJSON(w, http.StatusOK, version) } func (h *AdminHandler) ListPromptVersions(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } user, err := h.requireAuth(r) if err != nil { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"}) return } _ = user overrideIDStr := r.URL.Query().Get("override_id") if overrideIDStr == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing override_id"}) return } var overrideID int if _, err := fmt.Sscanf(overrideIDStr, "%d", &overrideID); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid override_id"}) return } versions, err := db.ListPromptVersions(h.db, overrideID) if err != nil { log.Printf("Error listing prompt versions: %v", err) writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal error"}) return } writeJSON(w, http.StatusOK, versions) } func (h *AdminHandler) GetPromptVersion(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } user, err := h.requireAuth(r) if err != nil { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"}) return } _ = user versionIDStr := r.URL.Query().Get("version_id") if versionIDStr == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing version_id"}) return } var versionID int if _, err := fmt.Sscanf(versionIDStr, "%d", &versionID); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid version_id"}) return } version, err := db.GetPromptVersion(h.db, versionID) if err != nil { log.Printf("Error getting prompt version: %v", err) writeJSON(w, http.StatusNotFound, map[string]string{"error": "version not found"}) return } writeJSON(w, http.StatusOK, version) } func (h *AdminHandler) PreviewForm(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } user, err := h.requireAuth(r) if err != nil { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"}) return } pdf, err := h.requirePDFAccess(r, user) if err != nil { writeJSON(w, http.StatusForbidden, map[string]string{"error": err.Error()}) return } var markdown string pdfPath := filepath.Join(h.pdfDir, fmt.Sprintf("%s.pdf", pdf.MD5Hash)) if pdf.CustomMarkdown != nil && *pdf.CustomMarkdown != "" { markdown = *pdf.CustomMarkdown } else { if _, statErr := os.Stat(pdfPath); statErr != nil { writeJSON(w, http.StatusNotFound, map[string]string{"error": "PDF file not found"}) return } markdown, err = pdfcontent.ExtractPDFContent(pdfPath) if err != nil { log.Printf("PDF %d: extraction failed: %v", pdf.ID, err) writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "extraction failed"}) return } } // Build default field types from raw PDF extraction defaultTypes, _ := buildDefaultFieldTypes(pdfPath) html := htmlform.Render(markdown, defaultTypes) writeJSON(w, http.StatusOK, map[string]string{"html": html}) } // Prompt override endpoints func (h *AdminHandler) GetPrompts(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } user, err := h.requireAuth(r) if err != nil { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"}) return } orgID := r.URL.Query().Get("org_id") if orgID == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing org_id"}) return } var id int if _, err := fmt.Sscanf(orgID, "%d", &id); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid org_id"}) return } inOrg, err := db.IsUserInOrg(h.db, user.ID, id) if err != nil || !inOrg { writeJSON(w, http.StatusForbidden, map[string]string{"error": "access denied"}) return } overrides, err := db.ListPromptOverridesForOrg(h.db, id) if err != nil { log.Printf("Error listing prompts: %v", err) writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal error"}) return } // Load defaults alongside overrides defaults := make(map[string]string) for _, pt := range []string{llm.PromptTypeFormatting} { if content, err := llm.LoadDefaultPrompt(pt); err == nil { defaults[pt] = content } } writeJSON(w, http.StatusOK, map[string]interface{}{ "defaults": defaults, "overrides": overrides, }) } func (h *AdminHandler) SavePrompt(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } user, err := h.requireAuth(r) if err != nil { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"}) return } var req models.PromptOverrideRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"}) return } if req.PromptType == "" || req.PromptContent == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing prompt_type or prompt_content"}) return } // Validate access and resolve org_id var saveOrgID *int if req.OrgID != nil { inOrg, err := db.IsUserInOrg(h.db, user.ID, *req.OrgID) if err != nil || !inOrg { writeJSON(w, http.StatusForbidden, map[string]string{"error": "access denied"}) return } saveOrgID = req.OrgID } else if req.PDFID != nil { pdf, err := db.GetPDF(h.db, *req.PDFID) if err != nil { writeJSON(w, http.StatusForbidden, map[string]string{"error": "access denied"}) return } if pdf.OrgID == nil { writeJSON(w, http.StatusForbidden, map[string]string{"error": "access denied"}) return } inOrg, err := db.IsUserInOrg(h.db, user.ID, *pdf.OrgID) if err != nil || !inOrg { writeJSON(w, http.StatusForbidden, map[string]string{"error": "access denied"}) return } saveOrgID = pdf.OrgID } else { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "must specify org_id or pdf_id"}) return } overrideID, err := db.SavePromptOverride(h.db, saveOrgID, req.PDFID, req.PromptType, req.PromptContent) if err != nil { log.Printf("Error saving prompt: %v", err) writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal error"}) return } // Create a version snapshot if _, err := db.CreatePromptVersion(h.db, overrideID, req.PromptContent); err != nil { log.Printf("Error creating prompt version for override %d: %v", overrideID, err) } writeJSON(w, http.StatusOK, map[string]interface{}{ "status": "saved", "override_id": overrideID, }) } func (h *AdminHandler) TogglePromptOverride(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } user, err := h.requireAuth(r) if err != nil { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"}) return } var req struct { OverrideID int `json:"override_id"` Active bool `json:"active"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"}) return } if req.OverrideID == 0 { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing override_id"}) return } // Validate ownership po, err := db.GetPromptOverrideByID(h.db, req.OverrideID) if err != nil { log.Printf("Error getting prompt override: %v", err) writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal error"}) return } if po == nil { writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"}) return } if po.OrgID != nil { inOrg, err := db.IsUserInOrg(h.db, user.ID, *po.OrgID) if err != nil || !inOrg { writeJSON(w, http.StatusForbidden, map[string]string{"error": "access denied"}) return } } if err := db.TogglePromptOverride(h.db, req.OverrideID, req.Active); err != nil { log.Printf("Error toggling prompt override: %v", err) writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal error"}) return } writeJSON(w, http.StatusOK, map[string]string{"status": "toggled"}) } func (h *AdminHandler) DeletePrompt(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } user, err := h.requireAuth(r) if err != nil { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"}) return } var req struct { ID int `json:"id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"}) return } if req.ID == 0 { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing id"}) return } po, err := db.GetPromptOverrideByID(h.db, req.ID) if err != nil { log.Printf("Error looking up prompt override: %v", err) writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal error"}) return } if po == nil { writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"}) return } if po.OrgID == nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid override"}) return } inOrg, err := db.IsUserInOrg(h.db, user.ID, *po.OrgID) if err != nil || !inOrg { writeJSON(w, http.StatusForbidden, map[string]string{"error": "access denied"}) return } if err := db.DeletePromptOverride(h.db, req.ID); err != nil { log.Printf("Error deleting prompt: %v", err) writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal error"}) return } writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"}) } // Field group endpoints func (h *AdminHandler) GetFieldGroups(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } user, err := h.requireAuth(r) if err != nil { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"}) return } pdf, err := h.requirePDFAccess(r, user) if err != nil { writeJSON(w, http.StatusForbidden, map[string]string{"error": err.Error()}) return } groups, err := db.GetFieldGroups(h.db, pdf.ID) if err != nil { log.Printf("Error getting field groups: %v", err) writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal error"}) return } fields, _ := db.GetFormFields(h.db, pdf.ID) fieldMap := make(map[int]models.FormField) for _, f := range fields { fieldMap[f.ID] = f } var result []models.AdminFieldGroup for _, g := range groups { members, _ := db.GetFieldGroupMembers(h.db, g.ID) var adminMembers []models.AdminFieldGroupMember for _, m := range members { if ff, ok := fieldMap[m.FieldID]; ok { adminMembers = append(adminMembers, models.AdminFieldGroupMember{ FieldID: m.FieldID, FieldName: ff.FieldName, Type: ff.Type, CustomLabel: ff.CustomLabel, }) } } result = append(result, models.AdminFieldGroup{ ID: g.ID, GroupType: g.GroupType, Members: adminMembers, }) } writeJSON(w, http.StatusOK, result) } // LLM reprocessing func (h *AdminHandler) ReprocessLLM(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } user, err := h.requireAuth(r) if err != nil { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"}) return } pdf, err := h.requirePDFAccess(r, user) if err != nil { writeJSON(w, http.StatusForbidden, map[string]string{"error": err.Error()}) return } pdfPath := filepath.Join(h.pdfDir, fmt.Sprintf("%s.pdf", pdf.MD5Hash)) if _, statErr := os.Stat(pdfPath); statErr != nil { writeJSON(w, http.StatusNotFound, map[string]string{"error": "PDF file not found"}) return } // Clear previous custom markdown so reprocessing starts fresh db.ClearCustomMarkdown(h.db, pdf.ID) pdf.CustomMarkdown = nil writeJSON(w, http.StatusOK, map[string]string{"status": "processing"}) // Run LLM processing in background go func() { RunLLMProcessing(h.db, pdf, pdfPath) }() } // RunLLMProcessing runs the LLM formatting pass on a PDF. // The LLM cleans up the markdown, adds labels/types to [field] shortcodes, // and fixes formatting anomalies. This is a shared function used by both // the main handler and admin handler. func RunLLMProcessing(dbConn *sql.DB, pdf *models.PDF, pdfPath string) { if !llm.HasOpenAIKey() { log.Printf("PDF %d: skipping LLM processing (no API key)", pdf.ID) db.UpdatePDFLLMStatus(dbConn, pdf.ID, "skipped") return } cl, err := llm.NewClient() if err != nil { log.Printf("PDF %d: LLM client not available: %v", pdf.ID, err) db.UpdatePDFLLMStatusWithError(dbConn, pdf.ID, "failed", err.Error()) return } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() db.UpdatePDFLLMStatus(dbConn, pdf.ID, "pending") // --- Formatting pass (includes shortcode conversion) --- log.Printf("PDF %d: running LLM formatting pass", pdf.ID) var markdown string if pdf.CustomMarkdown != nil && *pdf.CustomMarkdown != "" { markdown = *pdf.CustomMarkdown } else { markdown, err = pdfcontent.ExtractPDFContent(pdfPath) if err != nil { log.Printf("PDF %d: extraction for LLM failed: %v", pdf.ID, err) db.UpdatePDFLLMStatusWithError(dbConn, pdf.ID, "failed", err.Error()) return } } // Resolve prompt: override > default promptSource := "default" formattingPrompt, err := resolvePrompt(dbConn, pdf.ID, pdf.OrgID, llm.PromptTypeFormatting) if err != nil { log.Printf("PDF %d: loading formatting prompt override failed: %v", pdf.ID, err) } else if formattingPrompt != "" { promptSource = "override" } if formattingPrompt == "" { formattingPrompt, err = llm.LoadDefaultPrompt(llm.PromptTypeFormatting) if err != nil { log.Printf("PDF %d: loading default formatting prompt failed: %v", pdf.ID, err) db.UpdatePDFLLMStatusWithError(dbConn, pdf.ID, "failed", err.Error()) return } } log.Printf("PDF %d: using %s prompt (%d chars)", pdf.ID, promptSource, len(formattingPrompt)) cleanedMarkdown, err := cl.FormatMarkdown(ctx, formattingPrompt, markdown, "") if err != nil { log.Printf("PDF %d: LLM formatting failed: %v", pdf.ID, err) db.UpdatePDFLLMStatusWithError(dbConn, pdf.ID, "failed", err.Error()) return } cleanedMarkdown = llm.FormatResponse(cleanedMarkdown) if pdf.CustomMarkdown == nil || *pdf.CustomMarkdown == "" { if err := db.UpdateCustomMarkdown(dbConn, pdf.ID, cleanedMarkdown); err != nil { log.Printf("PDF %d: saving LLM-formatted markdown failed: %v", pdf.ID, err) } else { log.Printf("PDF %d: saved LLM-formatted markdown", pdf.ID) if _, err := db.CreateMarkdownVersion(dbConn, pdf.ID, cleanedMarkdown); err != nil { log.Printf("PDF %d: saving markdown version failed: %v", pdf.ID, err) } } } db.UpdatePDFLLMStatus(dbConn, pdf.ID, "completed") log.Printf("PDF %d: LLM processing complete", pdf.ID) } func resolvePrompt(dbConn *sql.DB, pdfID int, orgID *int, promptType string) (string, error) { if override, err := db.ResolvePrompt(dbConn, pdfID, orgID, promptType); err != nil { return "", err } else if override != "" { return override, nil } return "", nil } func (h *AdminHandler) requireAuth(r *http.Request) (*models.User, error) { token := getSessionToken(r) if token == "" { return nil, fmt.Errorf("no session") } session, err := db.GetSessionByToken(h.db, token) if err != nil { return nil, fmt.Errorf("session error") } if session == nil || session.ExpiresAt.Before(time.Now()) { if token != "" { db.DeleteSession(h.db, token) } return nil, fmt.Errorf("expired session") } user, err := db.GetUser(h.db, session.UserID) if err != nil { return nil, fmt.Errorf("user not found") } return user, nil } func (h *AdminHandler) requirePDFAccess(r *http.Request, user *models.User) (*models.PDF, error) { idStr := r.URL.Query().Get("id") if idStr == "" { return nil, fmt.Errorf("missing id") } var pdfID int if _, err := fmt.Sscanf(idStr, "%d", &pdfID); err != nil { return nil, fmt.Errorf("invalid id") } pdf, err := db.GetPDF(h.db, pdfID) if err != nil { return nil, fmt.Errorf("PDF not found") } if pdf.OrgID == nil { return nil, fmt.Errorf("PDF has no org") } inOrg, err := db.IsUserInOrg(h.db, user.ID, *pdf.OrgID) if err != nil || !inOrg { return nil, fmt.Errorf("access denied") } return pdf, nil } func (h *AdminHandler) requirePDFAccessByID(r *http.Request, user *models.User, pdfID int) (*models.PDF, error) { pdf, err := db.GetPDF(h.db, pdfID) if err != nil { return nil, fmt.Errorf("PDF not found") } if pdf.OrgID == nil { return nil, fmt.Errorf("PDF has no org") } inOrg, err := db.IsUserInOrg(h.db, user.ID, *pdf.OrgID) if err != nil || !inOrg { return nil, fmt.Errorf("access denied") } return pdf, nil } func getSessionToken(r *http.Request) string { cookie, err := r.Cookie("admin_session") if err != nil { return "" } return cookie.Value } func generateToken() (string, error) { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { return "", err } return hex.EncodeToString(b), nil }