Files
chirpy/main.go
T

315 lines
6.9 KiB
Go

package main
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"slices"
"strings"
"sync/atomic"
"syscall"
"time"
"github.com/StevanFreeborn/chirpy/internal/database"
"github.com/google/uuid"
"github.com/joho/godotenv"
_ "github.com/lib/pq"
)
type server struct {
fileServerHits atomic.Int32
database *database.Queries
platform string
}
func writeJsonResponse(w http.ResponseWriter, response any) {
encoder := json.NewEncoder(w)
if err := encoder.Encode(response); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func (s *server) HandleFiles(prefix string) http.Handler {
fileServer := http.StripPrefix(prefix, http.FileServer(http.Dir(".")))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
s.fileServerHits.Add(1)
fileServer.ServeHTTP(w, r)
})
}
func (s *server) HandleHealthChecks(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
func (s *server) HandleReset(w http.ResponseWriter, r *http.Request) {
if s.platform != "dev" {
w.WriteHeader(http.StatusForbidden)
return
}
s.fileServerHits.Store(0)
s.database.DeleteAllUsers(r.Context())
w.Header().Add("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
func (s *server) HandleMetrics(w http.ResponseWriter, r *http.Request) {
hits := s.fileServerHits.Load()
w.Header().Add("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
template := `
<html>
<body>
<h1>Welcome, Chirpy Admin</h1>
<p>Chirpy has been visited %d times!</p>
</body>
</html>
`
fmt.Fprintf(w, template, hits)
}
type apiError struct {
Err string `json:"error"`
}
type createChirpRequest struct {
Body string `json:"body"`
UserId string `json:"user_id"`
}
type createChirpResponse struct {
Id string `json:"id"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Body string `json:"body"`
UserId string `json:"user_id"`
}
func (s *server) HandleCreateChirp(w http.ResponseWriter, r *http.Request) {
blacklist := []string{
"kerfuffle",
"sharbert",
"fornax",
}
var createChirpRequest createChirpRequest
defer r.Body.Close()
w.Header().Add("Content-Type", "application/json")
err := json.NewDecoder(r.Body).Decode(&createChirpRequest)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
writeJsonResponse(w, apiError{
Err: "Unable to deserialize JSON",
})
return
}
if len(createChirpRequest.Body) > 140 {
w.WriteHeader(http.StatusBadRequest)
writeJsonResponse(w, apiError{
Err: "Chirp is too long",
})
return
}
requestUserId, err := uuid.Parse(createChirpRequest.UserId)
invalidUserIdError := apiError{
Err: "Invalid user_id. user_id must be valid UUID",
}
if err != nil {
w.WriteHeader(http.StatusBadRequest)
writeJsonResponse(w, invalidUserIdError)
return
}
existingUser, err := s.database.GetUserById(r.Context(), requestUserId)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
writeJsonResponse(w, invalidUserIdError)
return
}
words := strings.Split(createChirpRequest.Body, " ")
sanitized := []string{}
for _, word := range words {
if slices.Contains(blacklist, strings.ToLower(word)) {
sanitized = append(sanitized, "****")
continue
}
sanitized = append(sanitized, word)
}
cleanedBody := strings.Join(sanitized, " ")
createChirpParams := database.CreateChirpParams{
Body: cleanedBody,
UserID: existingUser.ID,
}
createdChirp, err := s.database.CreateChirp(r.Context(), createChirpParams)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
writeJsonResponse(w, apiError{
Err: "Failed to create chirp. 🤷🏻‍♂️",
})
}
w.WriteHeader(http.StatusCreated)
writeJsonResponse(w, createChirpResponse{
Id: createdChirp.ID.String(),
CreatedAt: createdChirp.CreatedAt.Format(time.RFC3339),
UpdatedAt: createdChirp.UpdatedAt.Format(time.RFC3339),
Body: createdChirp.Body,
UserId: createdChirp.UserID.String(),
})
}
type createUserRequest struct {
Email string `json:"email"`
}
type createUserResponse struct {
Id string `json:"id"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Email string `json:"email"`
}
func (s *server) HandleCreateUser(w http.ResponseWriter, r *http.Request) {
var createUserRequest createUserRequest
defer r.Body.Close()
err := json.NewDecoder(r.Body).Decode(&createUserRequest)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
writeJsonResponse(w, apiError{
Err: "Unable to deserialize JSON",
})
}
trimmedEmail := strings.TrimSpace(createUserRequest.Email)
if strings.TrimSpace(trimmedEmail) == "" {
w.WriteHeader(http.StatusBadRequest)
writeJsonResponse(w, apiError{
Err: "email is required. must be valid email address.",
})
}
createdUser, err := s.database.CreateUser(r.Context(), trimmedEmail)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
writeJsonResponse(w, apiError{
Err: "Uh oh we were unable to create a new user",
})
}
w.WriteHeader(http.StatusCreated)
writeJsonResponse(w, createUserResponse{
Id: createdUser.ID.String(),
CreatedAt: createdUser.CreatedAt.Format(time.RFC3339),
UpdatedAt: createdUser.UpdatedAt.Format(time.RFC3339),
Email: createdUser.Email,
})
}
func main() {
err := godotenv.Load()
if err != nil {
log.Fatalf("Failed to load environment variables")
os.Exit(1)
}
dbURL := os.Getenv("DB_URL")
platform := os.Getenv("PLATFORM")
db, err := sql.Open("postgres", dbURL)
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
os.Exit(1)
}
server := &server{
database: database.New(db),
platform: platform,
}
mux := http.NewServeMux()
appRoot := "/app/"
mux.Handle(appRoot, server.HandleFiles(appRoot))
mux.HandleFunc("GET /admin/metrics", server.HandleMetrics)
mux.HandleFunc("POST /admin/reset", server.HandleReset)
mux.HandleFunc("GET /api/healthz", server.HandleHealthChecks)
mux.HandleFunc("POST /api/users", server.HandleCreateUser)
mux.HandleFunc("POST /api/chirps", server.HandleCreateChirp)
port := ":8080"
httpServer := http.Server{
Handler: mux,
Addr: port,
}
go func() {
log.Printf("Server started and listening on %s\n", port)
err := httpServer.ListenAndServe()
if !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("HTTP Server error: %v", err)
}
log.Println("Stopped serving new connections")
}()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownRelease()
if err := httpServer.Shutdown(shutdownCtx); err != nil {
log.Fatalf("HTTP shutdown error: %v", err)
}
log.Println("Graceful shutdown complete")
}