206 lines
5.3 KiB
Go
206 lines
5.3 KiB
Go
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)
|
||
|
|
}
|