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 := `

Welcome, Chirpy Admin

Chirpy has been visited %d times!

` 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") }