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/auth" "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 jwtSecret []byte } func writeJsonResponse(w http.ResponseWriter, statusCode int, response any) { w.WriteHeader(statusCode) w.Header().Add("Content-Type", "application/json") encoder := json.NewEncoder(w) if err := encoder.Encode(response); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func decodeJsonRequest[T any](r *http.Request) (*T, error) { defer r.Body.Close() var data *T decoder := json.NewDecoder(r.Body) err := decoder.Decode(&data) if err != nil { return nil, apiError{ Err: "Failed to deserialize request", } } return data, err } 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 := `
Chirpy has been visited %d times!
` fmt.Fprintf(w, template, hits) } type apiError struct { Err string `json:"error"` } func (e apiError) Error() string { return e.Err } 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"` } func (s *server) HandleCreateChirp(w http.ResponseWriter, r *http.Request) { blacklist := []string{ "kerfuffle", "sharbert", "fornax", } bearerToken, err := auth.GetBearerToken(r.Header) unauthorizedError := apiError{ Err: "You are not authorized to perform this action", } if err != nil { writeJsonResponse(w, http.StatusUnauthorized, unauthorizedError) return } requestUserId, err := auth.ValidateJWT(bearerToken, s.jwtSecret) if err != nil { writeJsonResponse(w, http.StatusUnauthorized, unauthorizedError) return } createChirpRequest, err := decodeJsonRequest[createChirpRequest](r) if err != nil { writeJsonResponse(w, http.StatusBadRequest, err) return } if len(createChirpRequest.Body) > 140 { writeJsonResponse(w, http.StatusBadRequest, apiError{ Err: "Chirp is too long", }) 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: requestUserId, } createdChirp, err := s.database.CreateChirp(r.Context(), createChirpParams) if err != nil { writeJsonResponse(w, http.StatusInternalServerError, apiError{ Err: "Failed to create chirp. 🤷🏻♂️", }) return } writeJsonResponse(w, http.StatusCreated, chirpResponse{ 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"` Password string `json:"password"` } 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) { createUserRequest, err := decodeJsonRequest[createUserRequest](r) if err != nil { writeJsonResponse(w, http.StatusBadRequest, err) return } trimmedEmail := strings.TrimSpace(createUserRequest.Email) if strings.TrimSpace(trimmedEmail) == "" { writeJsonResponse(w, http.StatusBadRequest, apiError{ Err: "email is required. must be valid email address.", }) return } trimmedPassword := strings.TrimSpace(createUserRequest.Password) if strings.TrimSpace(trimmedEmail) == "" { writeJsonResponse(w, http.StatusBadRequest, apiError{ Err: "password is required. must be non-empty string.", }) return } hashed_password, err := auth.HashPassword(trimmedPassword) createUserError := apiError{ Err: "Uh oh we were unable to create a new user", } if err != nil { writeJsonResponse(w, http.StatusInternalServerError, createUserError) return } createUserParams := database.CreateUserParams{ Email: trimmedEmail, HashedPassword: hashed_password, } createdUser, err := s.database.CreateUser(r.Context(), createUserParams) if err != nil { writeJsonResponse(w, http.StatusInternalServerError, createUserError) return } writeJsonResponse(w, http.StatusCreated, createUserResponse{ Id: createdUser.ID.String(), CreatedAt: createdUser.CreatedAt.Format(time.RFC3339), UpdatedAt: createdUser.UpdatedAt.Format(time.RFC3339), Email: createdUser.Email, }) } func (s *server) HandleGetAllChirps(w http.ResponseWriter, r *http.Request) { existingChirps, err := s.database.GetAllChirps(r.Context()) if err != nil { writeJsonResponse(w, http.StatusInternalServerError, apiError{ Err: "Unable to retrieve chirps", }) return } chirps := []chirpResponse{} for _, c := range existingChirps { chirps = append(chirps, chirpResponse{ Id: c.ID.String(), CreatedAt: c.CreatedAt.Format(time.RFC3339), UpdatedAt: c.UpdatedAt.Format(time.RFC3339), Body: c.Body, UserId: c.UserID.String(), }) } writeJsonResponse(w, http.StatusOK, chirps) } func (s *server) HandleGetChirp(w http.ResponseWriter, r *http.Request) { chirpId := r.PathValue("id") validChirpId, err := uuid.Parse(chirpId) if err != nil { writeJsonResponse(w, http.StatusBadRequest, apiError{ Err: "Chirp id is not valid. id must be valid UUID.", }) return } existingChirp, err := s.database.GetChirpById(r.Context(), validChirpId) if err != nil { writeJsonResponse(w, http.StatusNotFound, apiError{ Err: "No chirp with given id found.", }) return } writeJsonResponse(w, http.StatusOK, chirpResponse{ Id: existingChirp.ID.String(), CreatedAt: existingChirp.CreatedAt.Format(time.RFC3339), UpdatedAt: existingChirp.UpdatedAt.Format(time.RFC3339), Body: existingChirp.Body, UserId: existingChirp.UserID.String(), }) } type loginRequest struct { Email string `json:"email"` Password string `json:"password"` } func (r *loginRequest) Validate() error { if strings.TrimSpace(r.Email) == "" || strings.TrimSpace(r.Password) == "" { return apiError{ Err: "Email and password must be non-empty string", } } return nil } type loginResponse struct { Id string `json:"id"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` Email string `json:"email"` Token string `json:"token"` RefreshToken string `json:"refresh_token"` } func (s *server) HandleLogin(w http.ResponseWriter, r *http.Request) { loginRequest, err := decodeJsonRequest[loginRequest](r) err = loginRequest.Validate() if err != nil { writeJsonResponse(w, http.StatusBadRequest, err) return } existingUser, err := s.database.GetUserByEmail(r.Context(), loginRequest.Email) invalidLoginError := apiError{ Err: "Invalid login request", } if err != nil { writeJsonResponse(w, http.StatusUnauthorized, invalidLoginError) return } isCorrectPassword, err := auth.CheckPasswordHash(loginRequest.Password, existingUser.HashedPassword) if err != nil || isCorrectPassword == false { writeJsonResponse(w, http.StatusUnauthorized, invalidLoginError) return } SECONDS_PER_HOUR := 3600 expiresInDuration := time.Duration(SECONDS_PER_HOUR) * time.Second accessToken, err := auth.MakeJWT(existingUser.ID, s.jwtSecret, expiresInDuration) loginFailedError := apiError{ Err: "Login failed", } if err != nil { writeJsonResponse(w, http.StatusInternalServerError, loginFailedError) return } createRefreshTokenParams := database.CreateRefreshTokenParams{ Token: auth.MakeRefreshToken(), ExpiresAt: time.Now().Add(60 * 24 * time.Hour), RevokedAt: sql.NullTime{ Valid: false, }, UserID: existingUser.ID, } createdRefreshToken, err := s.database.CreateRefreshToken(r.Context(), createRefreshTokenParams) if err != nil { writeJsonResponse(w, http.StatusInternalServerError, loginFailedError) return } writeJsonResponse(w, http.StatusOK, loginResponse{ Id: existingUser.ID.String(), CreatedAt: existingUser.CreatedAt.Format(time.RFC3339), UpdatedAt: existingUser.UpdatedAt.Format(time.RFC3339), Email: existingUser.Email, Token: accessToken, RefreshToken: createdRefreshToken.Token, }) } type refreshTokenResponse struct { Token string `json:"token"` } func (s *server) HandleRefresh(w http.ResponseWriter, r *http.Request) { refreshToken, err := auth.GetBearerToken(r.Header) unauthorizedError := apiError{ Err: "Unable to refresh token", } if err != nil { writeJsonResponse(w, http.StatusUnauthorized, unauthorizedError) return } existingRefreshToken, err := s.database.GetRefreshTokenByToken(r.Context(), refreshToken) if err != nil { writeJsonResponse(w, http.StatusUnauthorized, unauthorizedError) return } if existingRefreshToken.ExpiresAt.Before(time.Now()) || existingRefreshToken.RevokedAt.Valid { writeJsonResponse(w, http.StatusUnauthorized, unauthorizedError) return } SECONDS_PER_HOUR := 3600 expiresInDuration := time.Duration(SECONDS_PER_HOUR) * time.Second accessToken, err := auth.MakeJWT(existingRefreshToken.UserID, s.jwtSecret, expiresInDuration) if err != nil { writeJsonResponse(w, http.StatusInternalServerError, apiError{ Err: "Failed to refresh token", }) return } // TODO: We should rotate the refresh token writeJsonResponse(w, http.StatusOK, refreshTokenResponse{ Token: accessToken, }) } func (s *server) HandleRevoke(w http.ResponseWriter, r *http.Request) { refreshToken, err := auth.GetBearerToken(r.Header) if err != nil { writeJsonResponse(w, http.StatusBadRequest, apiError{ Err: "No refresh token present in request", }) return } existingRefreshToken, err := s.database.GetRefreshTokenByToken(r.Context(), refreshToken) if err != nil { writeJsonResponse(w, http.StatusNotFound, apiError{ Err: "Unable to revoke token", }) return } err = s.database.RevokeRefreshToken(r.Context(), existingRefreshToken.Token) if err != nil { writeJsonResponse(w, http.StatusInternalServerError, apiError{ Err: "Unable to revoke token", }) return } writeJsonResponse(w, http.StatusNoContent, nil) } type updateUserRequest struct { Email string `json:"email"` Password string `json:"password"` } type updateUserResponse struct { Id string `json:"id"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` Email string `json:"email"` } func (r *updateUserRequest) Validate() error { if strings.TrimSpace(r.Email) == "" || strings.TrimSpace(r.Password) == "" { return apiError{ Err: "Email and password must be non-empty string", } } return nil } func (s *server) HandleUpdateUser(w http.ResponseWriter, r *http.Request) { accessToken, err := auth.GetBearerToken(r.Header) if err != nil { writeJsonResponse(w, http.StatusUnauthorized, apiError{ Err: "You are not authorized to perform this action", }) return } requestUserId, err := auth.ValidateJWT(accessToken, s.jwtSecret) if err != nil { writeJsonResponse(w, http.StatusUnauthorized, apiError{ Err: "You are not authorized to perform this action", }) return } updateUserRequest, err := decodeJsonRequest[updateUserRequest](r) if err != nil { writeJsonResponse(w, http.StatusBadRequest, err) return } err = updateUserRequest.Validate() if err != nil { writeJsonResponse(w, http.StatusBadRequest, err) return } updatedHashedPassword, err := auth.HashPassword(updateUserRequest.Password) updateError := apiError{ Err: "Unable to perform update", } if err != nil { writeJsonResponse(w, http.StatusInternalServerError, updateError) return } updateUserParams := database.UpdateUserParams{ ID: requestUserId, Email: updateUserRequest.Email, HashedPassword: updatedHashedPassword, } updatedUser, err := s.database.UpdateUser(r.Context(), updateUserParams) if err != nil { writeJsonResponse(w, http.StatusInternalServerError, updateError) return } writeJsonResponse(w, http.StatusOK, updateUserResponse{ Id: updatedUser.ID.String(), CreatedAt: updatedUser.CreatedAt.Format(time.RFC3339), UpdatedAt: updatedUser.UpdatedAt.Format(time.RFC3339), Email: updatedUser.Email, }) } func (s *server) HandleDeleteChirp(w http.ResponseWriter, r *http.Request) { accessToken, err := auth.GetBearerToken(r.Header) unauthorizedError := apiError{ Err: "You are not authorized to perform this action", } if err != nil { writeJsonResponse(w, http.StatusUnauthorized, unauthorizedError) return } requestUserId, err := auth.ValidateJWT(accessToken, s.jwtSecret) if err != nil { writeJsonResponse(w, http.StatusUnauthorized, unauthorizedError) return } chirpId := r.PathValue("id") validChirpId, err := uuid.Parse(chirpId) invalidChirpIdError := apiError{ Err: "Chirp id must be a valid uuid for an existing chirp", } if err != nil { writeJsonResponse(w, http.StatusBadRequest, invalidChirpIdError) return } existingChirp, err := s.database.GetChirpById(r.Context(), validChirpId) if err != nil { writeJsonResponse(w, http.StatusNotFound, invalidChirpIdError) return } if existingChirp.UserID != requestUserId { writeJsonResponse(w, http.StatusForbidden, apiError{ Err: "You can not delete a chirp that does not belong to you", }) return } err = s.database.DeleteChirpById(r.Context(), existingChirp.ID) if err != nil { writeJsonResponse(w, http.StatusInternalServerError, apiError{ Err: "Yo bro we couldn't delete that shit", }) return } writeJsonResponse(w, http.StatusNoContent, nil) } 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") jwtSecret := os.Getenv("JWT_SECRET") if strings.TrimSpace(jwtSecret) == "" { log.Fatalf("Failed to retrieve JWT_SECRET") os.Exit(1) } 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, jwtSecret: []byte(jwtSecret), } 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/login", server.HandleLogin) mux.HandleFunc("POST /api/refresh", server.HandleRefresh) mux.HandleFunc("POST /api/revoke", server.HandleRevoke) mux.HandleFunc("POST /api/users", server.HandleCreateUser) mux.HandleFunc("PUT /api/users", server.HandleUpdateUser) mux.HandleFunc("GET /api/chirps", server.HandleGetAllChirps) mux.HandleFunc("GET /api/chirps/{id}", server.HandleGetChirp) mux.HandleFunc("POST /api/chirps", server.HandleCreateChirp) mux.HandleFunc("DELETE /api/chirps/{id}", server.HandleDeleteChirp) 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") }