package channel import ( "context" "encoding/json" "net/http" "time" "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware" "github.com/go-chi/chi/v5" ) type document struct { ID string `json:"id"` ChannelID string `json:"channel_id"` CreatorID string `json:"creator_id"` Title string `json:"title"` Content string `json:"content"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } type createDocRequest struct { Title string `json:"title"` Content string `json:"content"` } // ponytail: no version history, no lock/unlock. Add when concurrent editing arises. func (h *Handler) registerDocRoutes(r chi.Router) { r.Get("/{channelID}/docs", h.ListDocs) r.Post("/{channelID}/docs", h.CreateDoc) r.Get("/docs/{docID}", h.GetDoc) r.Patch("/docs/{docID}", h.UpdateDoc) r.Delete("/docs/{docID}", h.DeleteDoc) } func (h *Handler) ListDocs(w http.ResponseWriter, r *http.Request) { channelID := chi.URLParam(r, "channelID") userID, _ := middleware.UserIDFromContext(r.Context()) allowed := h.checkAccess(r.Context(), w, channelID, userID) if !allowed { return } rows, err := h.db.QueryContext(r.Context(), ` SELECT id, channel_id, creator_id, title, created_at, updated_at FROM documents WHERE channel_id = $1 ORDER BY updated_at DESC `, channelID) if err != nil { http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError) return } defer rows.Close() // ponytail: list returns title-only summary, not full content type docSummary struct { ID string `json:"id"` ChannelID string `json:"channel_id"` CreatorID string `json:"creator_id"` Title string `json:"title"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } docs := make([]docSummary, 0) for rows.Next() { var d docSummary if err := rows.Scan(&d.ID, &d.ChannelID, &d.CreatorID, &d.Title, &d.CreatedAt, &d.UpdatedAt); err != nil { continue } docs = append(docs, d) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(docs) } func (h *Handler) CreateDoc(w http.ResponseWriter, r *http.Request) { channelID := chi.URLParam(r, "channelID") userID, _ := middleware.UserIDFromContext(r.Context()) allowed := h.checkAccess(r.Context(), w, channelID, userID) if !allowed { return } var req createDocRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Title == "" { http.Error(w, `{"error":"title required"}`, http.StatusBadRequest) return } var d document err := h.db.QueryRowContext(r.Context(), ` INSERT INTO documents (channel_id, creator_id, title, content) VALUES ($1, $2, $3, $4) RETURNING id, channel_id, creator_id, title, content, created_at, updated_at `, channelID, userID, req.Title, req.Content).Scan( &d.ID, &d.ChannelID, &d.CreatorID, &d.Title, &d.Content, &d.CreatedAt, &d.UpdatedAt, ) if err != nil { http.Error(w, `{"error":"failed to create doc"}`, http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(d) } func (h *Handler) GetDoc(w http.ResponseWriter, r *http.Request) { docID := chi.URLParam(r, "docID") // ponytail: check channel access via doc lookup var channelID, creatorID, title, content, createdAt, updatedAt string err := h.db.QueryRowContext(r.Context(), ` SELECT id, channel_id, creator_id, title, content, created_at, updated_at FROM documents WHERE id = $1 `, docID).Scan(&docID, &channelID, &creatorID, &title, &content, &createdAt, &updatedAt) if err != nil { http.Error(w, `{"error":"doc not found"}`, http.StatusNotFound) return } userID, _ := middleware.UserIDFromContext(r.Context()) allowed := h.checkAccess(r.Context(), w, channelID, userID) if !allowed { return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(document{ ID: docID, ChannelID: channelID, CreatorID: creatorID, Title: title, Content: content, CreatedAt: createdAt, UpdatedAt: updatedAt, }) } func (h *Handler) UpdateDoc(w http.ResponseWriter, r *http.Request) { docID := chi.URLParam(r, "docID") userID, _ := middleware.UserIDFromContext(r.Context()) // get channel_id first for access check var channelID, creatorID string err := h.db.QueryRowContext(r.Context(), `SELECT channel_id, creator_id FROM documents WHERE id = $1`, docID).Scan(&channelID, &creatorID) if err != nil { http.Error(w, `{"error":"doc not found"}`, http.StatusNotFound) return } allowed := h.checkAccess(r.Context(), w, channelID, userID) if !allowed { return } var req createDocRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) return } var d document err = h.db.QueryRowContext(r.Context(), ` UPDATE documents SET title = COALESCE(NULLIF($2, ''), title), content = $3, updated_at = $4 WHERE id = $1 RETURNING id, channel_id, creator_id, title, content, created_at, updated_at `, docID, req.Title, req.Content, time.Now().UTC().Format(time.RFC3339)).Scan( &d.ID, &d.ChannelID, &d.CreatorID, &d.Title, &d.Content, &d.CreatedAt, &d.UpdatedAt, ) if err != nil { http.Error(w, `{"error":"failed to update doc"}`, http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(d) } func (h *Handler) DeleteDoc(w http.ResponseWriter, r *http.Request) { docID := chi.URLParam(r, "docID") userID, _ := middleware.UserIDFromContext(r.Context()) var channelID, creatorID string err := h.db.QueryRowContext(r.Context(), `SELECT channel_id, creator_id FROM documents WHERE id = $1`, docID).Scan(&channelID, &creatorID) if err != nil { http.Error(w, `{"error":"doc not found"}`, http.StatusNotFound) return } allowed := h.checkAccess(r.Context(), w, channelID, userID) if !allowed { return } _, err = h.db.ExecContext(r.Context(), `DELETE FROM documents WHERE id = $1`, docID) if err != nil { http.Error(w, `{"error":"failed to delete doc"}`, http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) } // ponytail: shared access check, reuses isMember. No permission fine-tuning. func (h *Handler) checkAccess(ctx context.Context, w http.ResponseWriter, channelID, userID string) bool { serverID, err := h.serverIDForChannel(ctx, channelID) if err != nil { http.Error(w, `{"error":"channel not found"}`, http.StatusNotFound) return false } ok, _ := h.isMember(ctx, userID, serverID) if !ok { http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden) return false } return true }