package server import ( "database/sql" "encoding/json" "errors" "net/http" "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware" "github.com/go-chi/chi/v5" ) type Handler struct { db *sql.DB } func NewHandler(db *sql.DB) *Handler { return &Handler{db: db} } func (h *Handler) RegisterRoutes(r chi.Router) { r.Post("/", h.Create) r.Get("/", h.List) r.Get("/{serverID}", h.Get) r.Patch("/{serverID}", h.Update) r.Delete("/{serverID}", h.Delete) r.Delete("/{serverID}/members/me", h.LeaveServer) } type createServerRequest struct { Name string `json:"name"` Icon string `json:"icon"` } type serverResponse struct { ID string `json:"id"` Name string `json:"name"` Icon *string `json:"icon"` OwnerID string `json:"owner_id"` } // @Summary Create a server // @Description Create a new server // @Tags servers // @Accept json // @Produce json // @Security SessionAuth // @Param body body createServerRequest true "Server data" // @Success 201 {object} serverResponse // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string // @Router /servers [post] func (h *Handler) Create(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.UserIDFromContext(r.Context()) if !ok { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) return } var req createServerRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) return } if req.Name == "" { http.Error(w, `{"error":"name is required"}`, http.StatusBadRequest) return } tx, err := h.db.BeginTx(r.Context(), nil) if err != nil { http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError) return } defer tx.Rollback() var srv serverResponse var icon sql.NullString if req.Icon != "" { icon = sql.NullString{String: req.Icon, Valid: true} } err = tx.QueryRowContext(r.Context(), ` INSERT INTO servers (name, icon, owner_id) VALUES ($1, $2, $3) RETURNING id, name, icon, owner_id `, req.Name, icon, userID).Scan(&srv.ID, &srv.Name, &srv.Icon, &srv.OwnerID) if err != nil { http.Error(w, `{"error":"failed to create server"}`, http.StatusInternalServerError) return } _, err = tx.ExecContext(r.Context(), ` INSERT INTO members (user_id, server_id) VALUES ($1, $2) `, userID, srv.ID) if err != nil { http.Error(w, `{"error":"failed to add owner as member"}`, http.StatusInternalServerError) return } if err := tx.Commit(); err != nil { http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError) return } // Seed the default @everyone role (non-fatal if it fails) _ = SeedDefaultRole(r.Context(), h.db, srv.ID) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(srv) } // @Summary List servers // @Description List all servers the current user is a member of // @Tags servers // @Produce json // @Security SessionAuth // @Success 200 {array} serverResponse // @Failure 401 {object} map[string]string // @Router /servers [get] func (h *Handler) List(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.UserIDFromContext(r.Context()) if !ok { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) return } rows, err := h.db.QueryContext(r.Context(), ` SELECT s.id, s.name, s.icon, s.owner_id FROM servers s INNER JOIN members m ON m.server_id = s.id WHERE m.user_id = $1 ORDER BY s.name `, userID) if err != nil { http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError) return } defer rows.Close() servers := make([]serverResponse, 0) for rows.Next() { var srv serverResponse if err := rows.Scan(&srv.ID, &srv.Name, &srv.Icon, &srv.OwnerID); err != nil { http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError) return } servers = append(servers, srv) } if err := rows.Err(); err != nil { http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(servers) } // @Summary Get a server // @Description Get a server by ID // @Tags servers // @Produce json // @Security SessionAuth // @Param serverID path string true "Server ID" // @Success 200 {object} serverResponse // @Failure 401 {object} map[string]string // @Failure 404 {object} map[string]string // @Router /servers/{serverID} [get] func (h *Handler) Get(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.UserIDFromContext(r.Context()) if !ok { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) return } serverID := chi.URLParam(r, "serverID") var srv serverResponse err := h.db.QueryRowContext(r.Context(), ` SELECT s.id, s.name, s.icon, s.owner_id FROM servers s INNER JOIN members m ON m.server_id = s.id WHERE s.id = $1 AND m.user_id = $2 `, serverID, userID).Scan(&srv.ID, &srv.Name, &srv.Icon, &srv.OwnerID) if err != nil { if errors.Is(err, sql.ErrNoRows) { http.Error(w, `{"error":"server not found"}`, http.StatusNotFound) return } http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(srv) } type updateServerRequest struct { Name *string `json:"name"` Icon *string `json:"icon"` } // @Summary Update a server // @Description Update a server's name or icon (owner only) // @Tags servers // @Accept json // @Produce json // @Security SessionAuth // @Param serverID path string true "Server ID" // @Param body body updateServerRequest true "Server update data" // @Success 200 {object} serverResponse // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string // @Failure 403 {object} map[string]string // @Failure 404 {object} map[string]string // @Router /servers/{serverID} [patch] func (h *Handler) Update(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.UserIDFromContext(r.Context()) if !ok { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) return } serverID := chi.URLParam(r, "serverID") var req updateServerRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) return } if req.Name == nil && req.Icon == nil { http.Error(w, `{"error":"nothing to update"}`, http.StatusBadRequest) return } // Verify ownership var ownerID string err := h.db.QueryRowContext(r.Context(), ` SELECT owner_id FROM servers WHERE id = $1 `, serverID).Scan(&ownerID) if err != nil { if errors.Is(err, sql.ErrNoRows) { http.Error(w, `{"error":"server not found"}`, http.StatusNotFound) return } http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError) return } if ownerID != userID { http.Error(w, `{"error":"server not found"}`, http.StatusNotFound) return } var srv serverResponse err = h.db.QueryRowContext(r.Context(), ` UPDATE servers SET name = COALESCE($1, name), icon = COALESCE($2, icon) WHERE id = $3 RETURNING id, name, icon, owner_id `, req.Name, req.Icon, serverID).Scan(&srv.ID, &srv.Name, &srv.Icon, &srv.OwnerID) if err != nil { http.Error(w, `{"error":"failed to update server"}`, http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(srv) } // @Summary Delete a server // @Description Delete a server (owner only) // @Tags servers // @Security SessionAuth // @Param serverID path string true "Server ID" // @Success 204 // @Failure 401 {object} map[string]string // @Failure 403 {object} map[string]string // @Failure 404 {object} map[string]string // @Router /servers/{serverID} [delete] func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.UserIDFromContext(r.Context()) if !ok { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) return } serverID := chi.URLParam(r, "serverID") // Verify ownership var ownerID string err := h.db.QueryRowContext(r.Context(), ` SELECT owner_id FROM servers WHERE id = $1 `, serverID).Scan(&ownerID) if err != nil { if errors.Is(err, sql.ErrNoRows) { http.Error(w, `{"error":"server not found"}`, http.StatusNotFound) return } http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError) return } if ownerID != userID { http.Error(w, `{"error":"server not found"}`, http.StatusNotFound) return } _, err = h.db.ExecContext(r.Context(), ` DELETE FROM servers WHERE id = $1 `, serverID) if err != nil { http.Error(w, `{"error":"failed to delete server"}`, http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) } func (h *Handler) LeaveServer(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.UserIDFromContext(r.Context()) if !ok { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) return } serverID := chi.URLParam(r, "serverID") // Cannot leave a server you own var ownerID string err := h.db.QueryRowContext(r.Context(), `SELECT owner_id FROM servers WHERE id = $1`, serverID).Scan(&ownerID) if err != nil { http.Error(w, `{"error":"server not found"}`, http.StatusNotFound) return } if ownerID == userID { http.Error(w, `{"error":"server owner cannot leave; transfer ownership or delete the server"}`, http.StatusForbidden) return } result, err := h.db.ExecContext(r.Context(), `DELETE FROM members WHERE user_id = $1 AND server_id = $2`, userID, serverID) if err != nil { http.Error(w, `{"error":"failed to leave server"}`, http.StatusInternalServerError) return } rows, _ := result.RowsAffected() if rows == 0 { http.Error(w, `{"error":"not a member"}`, http.StatusNotFound) return } w.WriteHeader(http.StatusNoContent) }