refactor: extract API handlers into internal packages
Move HTTP handler logic out of main into internal api and server packages, slimming main to a thin entrypoint. Bundles bug fixes: login nil-panic, empty-password validation, chirp sort ordering, duplicate-email 409, and restricting the file server to the assets directory.
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/StevanFreeborn/chirpy/internal/api"
|
||||
"github.com/StevanFreeborn/chirpy/internal/auth"
|
||||
"github.com/StevanFreeborn/chirpy/internal/database"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
ascending = "asc"
|
||||
descending = "desc"
|
||||
)
|
||||
|
||||
type createChirpRequest struct {
|
||||
Body string `json:"body"`
|
||||
UserId string `json:"user_id"`
|
||||
}
|
||||
|
||||
type chirpResponse struct {
|
||||
Id string `json:"id"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
Body string `json:"body"`
|
||||
UserId string `json:"user_id"`
|
||||
}
|
||||
|
||||
// toChirpResponse maps a database Chirp to its API response shape.
|
||||
func toChirpResponse(c database.Chirp) chirpResponse {
|
||||
return chirpResponse{
|
||||
Id: c.ID.String(),
|
||||
CreatedAt: c.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: c.UpdatedAt.Format(time.RFC3339),
|
||||
Body: c.Body,
|
||||
UserId: c.UserID.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// HandleCreateChirp creates a chirp for the authenticated user.
|
||||
func (s *Server) HandleCreateChirp(w http.ResponseWriter, r *http.Request) {
|
||||
bearerToken, err := auth.GetBearerToken(r.Header)
|
||||
|
||||
if err != nil {
|
||||
api.WriteError(w, http.StatusUnauthorized, "You are not authorized to perform this action")
|
||||
return
|
||||
}
|
||||
|
||||
requestUserId, err := auth.ValidateJWT(bearerToken, s.jwtSecret)
|
||||
|
||||
if err != nil {
|
||||
api.WriteError(w, http.StatusUnauthorized, "You are not authorized to perform this action")
|
||||
return
|
||||
}
|
||||
|
||||
createChirpRequest, err := api.DecodeJSON[createChirpRequest](r)
|
||||
|
||||
if err != nil {
|
||||
api.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if len(createChirpRequest.Body) > 140 {
|
||||
api.WriteError(w, http.StatusBadRequest, "Chirp is too long")
|
||||
return
|
||||
}
|
||||
|
||||
createChirpParams := database.CreateChirpParams{
|
||||
Body: cleanProfanity(createChirpRequest.Body),
|
||||
UserID: requestUserId,
|
||||
}
|
||||
|
||||
createdChirp, err := s.db.CreateChirp(r.Context(), createChirpParams)
|
||||
|
||||
if err != nil {
|
||||
api.WriteError(w, http.StatusInternalServerError, "Failed to create chirp. 🤷🏻♂️")
|
||||
return
|
||||
}
|
||||
|
||||
api.WriteJSON(w, http.StatusCreated, toChirpResponse(createdChirp))
|
||||
}
|
||||
|
||||
// HandleGetChirps lists chirps, optionally filtered by author_id and sorted by
|
||||
// the sort query parameter.
|
||||
func (s *Server) HandleGetChirps(w http.ResponseWriter, r *http.Request) {
|
||||
authorId := r.URL.Query().Get("author_id")
|
||||
sortDir := r.URL.Query().Get("sort")
|
||||
|
||||
hasAuthorId := strings.TrimSpace(authorId) != ""
|
||||
hasSort := strings.TrimSpace(sortDir) != ""
|
||||
|
||||
validAuthorId, err := uuid.Parse(authorId)
|
||||
|
||||
if hasAuthorId && err != nil {
|
||||
api.WriteError(w, http.StatusBadRequest, "Author id must be a valid uuid")
|
||||
return
|
||||
}
|
||||
|
||||
if hasSort && sortDir != ascending && sortDir != descending {
|
||||
api.WriteError(w, http.StatusBadRequest, fmt.Sprintf("Sort direction must be either '%s' or '%s'", ascending, descending))
|
||||
return
|
||||
}
|
||||
|
||||
if !hasSort {
|
||||
sortDir = ascending
|
||||
}
|
||||
|
||||
var existingChirps []database.Chirp
|
||||
|
||||
if hasAuthorId {
|
||||
existingChirps, err = s.db.GetChirpsByAuthor(r.Context(), validAuthorId)
|
||||
} else {
|
||||
existingChirps, err = s.db.GetAllChirps(r.Context())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
api.WriteError(w, http.StatusInternalServerError, "Unable to retrieve chirps")
|
||||
return
|
||||
}
|
||||
|
||||
slices.SortFunc(existingChirps, func(a, b database.Chirp) int {
|
||||
if sortDir == descending {
|
||||
return b.CreatedAt.Compare(a.CreatedAt)
|
||||
}
|
||||
|
||||
return a.CreatedAt.Compare(b.CreatedAt)
|
||||
})
|
||||
|
||||
chirps := make([]chirpResponse, 0, len(existingChirps))
|
||||
|
||||
for _, c := range existingChirps {
|
||||
chirps = append(chirps, toChirpResponse(c))
|
||||
}
|
||||
|
||||
api.WriteJSON(w, http.StatusOK, chirps)
|
||||
}
|
||||
|
||||
// HandleGetChirp returns a single chirp by id.
|
||||
func (s *Server) HandleGetChirp(w http.ResponseWriter, r *http.Request) {
|
||||
chirpId := r.PathValue("id")
|
||||
validChirpId, err := uuid.Parse(chirpId)
|
||||
|
||||
if err != nil {
|
||||
api.WriteError(w, http.StatusBadRequest, "Chirp id is not valid. id must be valid UUID.")
|
||||
return
|
||||
}
|
||||
|
||||
existingChirp, err := s.db.GetChirpById(r.Context(), validChirpId)
|
||||
|
||||
if err != nil {
|
||||
api.WriteError(w, http.StatusNotFound, "No chirp with given id found.")
|
||||
return
|
||||
}
|
||||
|
||||
api.WriteJSON(w, http.StatusOK, toChirpResponse(existingChirp))
|
||||
}
|
||||
|
||||
// HandleDeleteChirp deletes a chirp owned by the authenticated user.
|
||||
func (s *Server) HandleDeleteChirp(w http.ResponseWriter, r *http.Request) {
|
||||
accessToken, err := auth.GetBearerToken(r.Header)
|
||||
|
||||
if err != nil {
|
||||
api.WriteError(w, http.StatusUnauthorized, "You are not authorized to perform this action")
|
||||
return
|
||||
}
|
||||
|
||||
requestUserId, err := auth.ValidateJWT(accessToken, s.jwtSecret)
|
||||
|
||||
if err != nil {
|
||||
api.WriteError(w, http.StatusUnauthorized, "You are not authorized to perform this action")
|
||||
return
|
||||
}
|
||||
|
||||
chirpId := r.PathValue("id")
|
||||
validChirpId, err := uuid.Parse(chirpId)
|
||||
|
||||
if err != nil {
|
||||
api.WriteError(w, http.StatusBadRequest, "Chirp id must be a valid uuid for an existing chirp")
|
||||
return
|
||||
}
|
||||
|
||||
existingChirp, err := s.db.GetChirpById(r.Context(), validChirpId)
|
||||
|
||||
if err != nil {
|
||||
api.WriteError(w, http.StatusNotFound, "Chirp id must be a valid uuid for an existing chirp")
|
||||
return
|
||||
}
|
||||
|
||||
if existingChirp.UserID != requestUserId {
|
||||
api.WriteError(w, http.StatusForbidden, "You can not delete a chirp that does not belong to you")
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.db.DeleteChirpById(r.Context(), existingChirp.ID); err != nil {
|
||||
api.WriteError(w, http.StatusInternalServerError, "Unable to delete chirp")
|
||||
return
|
||||
}
|
||||
|
||||
api.WriteJSON(w, http.StatusNoContent, nil)
|
||||
}
|
||||
Reference in New Issue
Block a user