package channel import ( "database/sql" "encoding/json" "net/http" "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware" "github.com/go-chi/chi/v5" "github.com/google/uuid" ) // Handler holds the database dependency for channel CRUD operations. type Handler struct { db *sql.DB } // NewHandler creates a new channel Handler. func NewHandler(db *sql.DB) *Handler { return &Handler{db: db} } // RegisterRoutes registers channel routes on the given chi.Router. // Expects to be mounted under /servers/{serverID}/channels. func (h *Handler) RegisterRoutes(r chi.Router) { r.Post("/", h.Create) r.Get("/", h.List) r.Get("/{channelID}", h.Get) r.Patch("/{channelID}", h.Update) r.Delete("/{channelID}", h.Delete) } // Channel is the JSON representation of a channel. type Channel struct { ID string `json:"id"` ServerID string `json:"server_id"` Name string `json:"name"` Type string `json:"type"` Category string `json:"category"` Position int `json:"position"` CreatedAt string `json:"created_at"` } type createChannelRequest struct { Name string `json:"name"` Type string `json:"type"` Category string `json:"category"` Position *int `json:"position,omitempty"` } type updateChannelRequest struct { Name *string `json:"name,omitempty"` Type *string `json:"type,omitempty"` Category *string `json:"category,omitempty"` Position *int `json:"position,omitempty"` } // Create handles POST / — creates a new channel in a server. func (h *Handler) Create(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.UserIDFromContext(r.Context()) if !ok { writeError(w, http.StatusUnauthorized, "unauthorized") return } serverID := chi.URLParam(r, "serverID") if _, err := uuid.Parse(serverID); err != nil { writeError(w, http.StatusBadRequest, "invalid server id") return } // Verify membership if !h.isMember(r, userID, serverID) { writeError(w, http.StatusForbidden, "not a member of this server") return } var req createChannelRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request") return } if req.Name == "" { writeError(w, http.StatusBadRequest, "name is required") return } if req.Type == "" { req.Type = "text" } if req.Type != "text" && req.Type != "voice" { writeError(w, http.StatusBadRequest, "type must be text or voice") return } if req.Category == "" { req.Category = "General" } position := 0 if req.Position != nil { position = *req.Position } var channelID string err := h.db.QueryRowContext(r.Context(), ` INSERT INTO channels (server_id, name, type, category, position) VALUES ($1, $2, $3, $4, $5) RETURNING id `, serverID, req.Name, req.Type, req.Category, position).Scan(&channelID) if err != nil { writeError(w, http.StatusInternalServerError, "failed to create channel") return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(map[string]string{"id": channelID}) } // List handles GET / — returns all channels in a server. func (h *Handler) List(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.UserIDFromContext(r.Context()) if !ok { writeError(w, http.StatusUnauthorized, "unauthorized") return } serverID := chi.URLParam(r, "serverID") if _, err := uuid.Parse(serverID); err != nil { writeError(w, http.StatusBadRequest, "invalid server id") return } if !h.isMember(r, userID, serverID) { writeError(w, http.StatusForbidden, "not a member of this server") return } rows, err := h.db.QueryContext(r.Context(), ` SELECT id, server_id, name, type, category, position, created_at FROM channels WHERE server_id = $1 ORDER BY position ASC, created_at ASC `, serverID) if err != nil { writeError(w, http.StatusInternalServerError, "failed to list channels") return } defer rows.Close() channels := []Channel{} for rows.Next() { var c Channel if err := rows.Scan(&c.ID, &c.ServerID, &c.Name, &c.Type, &c.Category, &c.Position, &c.CreatedAt); err != nil { writeError(w, http.StatusInternalServerError, "server error") return } channels = append(channels, c) } if err := rows.Err(); err != nil { writeError(w, http.StatusInternalServerError, "server error") return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(channels) } // Get handles GET /{channelID} — returns a single channel. func (h *Handler) Get(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.UserIDFromContext(r.Context()) if !ok { writeError(w, http.StatusUnauthorized, "unauthorized") return } serverID := chi.URLParam(r, "serverID") channelID := chi.URLParam(r, "channelID") if _, err := uuid.Parse(channelID); err != nil { writeError(w, http.StatusBadRequest, "invalid channel id") return } if !h.isMember(r, userID, serverID) { writeError(w, http.StatusForbidden, "not a member of this server") return } var c Channel err := h.db.QueryRowContext(r.Context(), ` SELECT id, server_id, name, type, category, position, created_at FROM channels WHERE id = $1 AND server_id = $2 `, channelID, serverID).Scan(&c.ID, &c.ServerID, &c.Name, &c.Type, &c.Category, &c.Position, &c.CreatedAt) if err != nil { writeError(w, http.StatusNotFound, "channel not found") return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(c) } // Update handles PATCH /{channelID} — updates a channel's properties. func (h *Handler) Update(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.UserIDFromContext(r.Context()) if !ok { writeError(w, http.StatusUnauthorized, "unauthorized") return } serverID := chi.URLParam(r, "serverID") channelID := chi.URLParam(r, "channelID") if _, err := uuid.Parse(channelID); err != nil { writeError(w, http.StatusBadRequest, "invalid channel id") return } if !h.isMember(r, userID, serverID) { writeError(w, http.StatusForbidden, "not a member of this server") return } var req updateChannelRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request") return } // Validate type if provided if req.Type != nil && *req.Type != "text" && *req.Type != "voice" { writeError(w, http.StatusBadRequest, "type must be text or voice") return } result, err := h.db.ExecContext(r.Context(), ` UPDATE channels SET name = COALESCE($1, name), type = COALESCE($2, type), category = COALESCE($3, category), position = COALESCE($4, position) WHERE id = $5 AND server_id = $6 `, req.Name, req.Type, req.Category, req.Position, channelID, serverID) if err != nil { writeError(w, http.StatusInternalServerError, "failed to update channel") return } rows, _ := result.RowsAffected() if rows == 0 { writeError(w, http.StatusNotFound, "channel not found") return } var c Channel h.db.QueryRowContext(r.Context(), ` SELECT id, server_id, name, type, category, position, created_at FROM channels WHERE id = $1 `, channelID).Scan(&c.ID, &c.ServerID, &c.Name, &c.Type, &c.Category, &c.Position, &c.CreatedAt) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(c) } // Delete handles DELETE /{channelID} — deletes a channel. func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.UserIDFromContext(r.Context()) if !ok { writeError(w, http.StatusUnauthorized, "unauthorized") return } serverID := chi.URLParam(r, "serverID") channelID := chi.URLParam(r, "channelID") if _, err := uuid.Parse(channelID); err != nil { writeError(w, http.StatusBadRequest, "invalid channel id") return } if !h.isMember(r, userID, serverID) { writeError(w, http.StatusForbidden, "not a member of this server") return } result, err := h.db.ExecContext(r.Context(), `DELETE FROM channels WHERE id = $1 AND server_id = $2`, channelID, serverID, ) if err != nil { writeError(w, http.StatusInternalServerError, "failed to delete channel") return } rows, _ := result.RowsAffected() if rows == 0 { writeError(w, http.StatusNotFound, "channel not found") return } w.WriteHeader(http.StatusNoContent) } // isMember checks if a user is a member of the given server. func (h *Handler) isMember(r *http.Request, userID, serverID string) bool { var exists bool h.db.QueryRowContext(r.Context(), `SELECT EXISTS(SELECT 1 FROM members WHERE user_id = $1 AND server_id = $2)`, userID, serverID, ).Scan(&exists) return exists } // writeError sends a JSON error response. func writeError(w http.ResponseWriter, status int, message string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(map[string]string{"error": message}) }