From 66be0a2b40664b43f88dd78451f2447239cd16a1 Mon Sep 17 00:00:00 2001 From: Darius Rapalis Date: Thu, 13 Mar 2025 22:47:03 +0200 Subject: [PATCH] webhooks and chirps by user id --- internal/auth/auth.go | 9 ++ internal/dataMaps/maps.go | 10 +- internal/database/CreateUser.sql.go | 3 +- internal/database/GetChirpsByUserID.sql.go | 45 +++++++ internal/database/GetUserByEmail.sql.go | 3 +- internal/database/GetUserByID.sql.go | 3 +- .../UpdateUserEmailAndPassword.sql.go | 3 +- .../database/UpgradeUserToChirpyRed.sql.go | 22 ++++ internal/database/models.go | 1 + main.go | 110 +++++++++++++++--- sql/queries/GetChirpsByUserID.sql | 2 + sql/queries/UpgradeUserToChirpyRed.sql | 3 + ...dd_is_chirpy_red_column_to_users_table.sql | 5 + 13 files changed, 195 insertions(+), 24 deletions(-) create mode 100644 internal/database/GetChirpsByUserID.sql.go create mode 100644 internal/database/UpgradeUserToChirpyRed.sql.go create mode 100644 sql/queries/GetChirpsByUserID.sql create mode 100644 sql/queries/UpgradeUserToChirpyRed.sql create mode 100644 sql/schema/005_add_is_chirpy_red_column_to_users_table.sql diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 069ca66..a8eb37e 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -96,3 +96,12 @@ func MakeRefreshToken() (string, error) { token := hex.EncodeToString(key) return token, nil } + +func GetAPIKey(headers http.Header) (string, error) { + auth := headers.Get("Authorization") + auth = strings.TrimPrefix(auth, "ApiKey ") + if auth == "" { + return "", errors.New("missing Authorization header") + } + return auth, nil +} diff --git a/internal/dataMaps/maps.go b/internal/dataMaps/maps.go index 9e7f6b3..e55ed08 100644 --- a/internal/dataMaps/maps.go +++ b/internal/dataMaps/maps.go @@ -14,10 +14,11 @@ type Chirp struct { } type User struct { - ID uuid.UUID `json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Email string `json:"email"` + ID uuid.UUID `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Email string `json:"email"` + IsChirpyRed bool `json:"is_chirpy_red"` } type UserWithToken struct { @@ -27,4 +28,5 @@ type UserWithToken struct { Email string `json:"email"` Token string `json:"token"` RefreshToken string `json:"refresh_token"` + IsChirpyRed bool `json:"is_chirpy_red"` } diff --git a/internal/database/CreateUser.sql.go b/internal/database/CreateUser.sql.go index c943a18..b25bb7d 100644 --- a/internal/database/CreateUser.sql.go +++ b/internal/database/CreateUser.sql.go @@ -12,7 +12,7 @@ import ( const createUser = `-- name: CreateUser :one INSERT INTO users (id, created_at, updated_at, email, hashed_password) VALUES (gen_random_uuid(), NOW(), NOW(), $1, $2) -RETURNING id, created_at, updated_at, email, hashed_password +RETURNING id, created_at, updated_at, email, hashed_password, is_chirpy_red ` type CreateUserParams struct { @@ -29,6 +29,7 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e &i.UpdatedAt, &i.Email, &i.HashedPassword, + &i.IsChirpyRed, ) return i, err } diff --git a/internal/database/GetChirpsByUserID.sql.go b/internal/database/GetChirpsByUserID.sql.go new file mode 100644 index 0000000..6391661 --- /dev/null +++ b/internal/database/GetChirpsByUserID.sql.go @@ -0,0 +1,45 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 +// source: GetChirpsByUserID.sql + +package database + +import ( + "context" + + "github.com/google/uuid" +) + +const getChirpsByUserID = `-- name: GetChirpsByUserID :many +SELECT id, created_at, updated_at, body, user_id FROM chirps WHERE user_id = $1 ORDER BY created_at +` + +func (q *Queries) GetChirpsByUserID(ctx context.Context, userID uuid.UUID) ([]Chirp, error) { + rows, err := q.db.QueryContext(ctx, getChirpsByUserID, userID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Chirp + for rows.Next() { + var i Chirp + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Body, + &i.UserID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/database/GetUserByEmail.sql.go b/internal/database/GetUserByEmail.sql.go index 2307798..c9526a0 100644 --- a/internal/database/GetUserByEmail.sql.go +++ b/internal/database/GetUserByEmail.sql.go @@ -10,7 +10,7 @@ import ( ) const getUserByEmail = `-- name: GetUserByEmail :one -SELECT id, created_at, updated_at, email, hashed_password FROM users WHERE email = $1 +SELECT id, created_at, updated_at, email, hashed_password, is_chirpy_red FROM users WHERE email = $1 ` func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) { @@ -22,6 +22,7 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error &i.UpdatedAt, &i.Email, &i.HashedPassword, + &i.IsChirpyRed, ) return i, err } diff --git a/internal/database/GetUserByID.sql.go b/internal/database/GetUserByID.sql.go index fc1239f..919ce24 100644 --- a/internal/database/GetUserByID.sql.go +++ b/internal/database/GetUserByID.sql.go @@ -12,7 +12,7 @@ import ( ) const getUserByID = `-- name: GetUserByID :one -SELECT id, created_at, updated_at, email, hashed_password FROM users WHERE id = $1 +SELECT id, created_at, updated_at, email, hashed_password, is_chirpy_red FROM users WHERE id = $1 ` func (q *Queries) GetUserByID(ctx context.Context, id uuid.UUID) (User, error) { @@ -24,6 +24,7 @@ func (q *Queries) GetUserByID(ctx context.Context, id uuid.UUID) (User, error) { &i.UpdatedAt, &i.Email, &i.HashedPassword, + &i.IsChirpyRed, ) return i, err } diff --git a/internal/database/UpdateUserEmailAndPassword.sql.go b/internal/database/UpdateUserEmailAndPassword.sql.go index f2eba6e..223eedd 100644 --- a/internal/database/UpdateUserEmailAndPassword.sql.go +++ b/internal/database/UpdateUserEmailAndPassword.sql.go @@ -13,7 +13,7 @@ import ( const updateUserEmailAndPassword = `-- name: UpdateUserEmailAndPassword :one UPDATE users SET email = $1, hashed_password = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $3 -RETURNING id, created_at, updated_at, email, hashed_password +RETURNING id, created_at, updated_at, email, hashed_password, is_chirpy_red ` type UpdateUserEmailAndPasswordParams struct { @@ -31,6 +31,7 @@ func (q *Queries) UpdateUserEmailAndPassword(ctx context.Context, arg UpdateUser &i.UpdatedAt, &i.Email, &i.HashedPassword, + &i.IsChirpyRed, ) return i, err } diff --git a/internal/database/UpgradeUserToChirpyRed.sql.go b/internal/database/UpgradeUserToChirpyRed.sql.go new file mode 100644 index 0000000..af5fb4d --- /dev/null +++ b/internal/database/UpgradeUserToChirpyRed.sql.go @@ -0,0 +1,22 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 +// source: UpgradeUserToChirpyRed.sql + +package database + +import ( + "context" + + "github.com/google/uuid" +) + +const upgradeUserToChirpyRed = `-- name: UpgradeUserToChirpyRed :exec +UPDATE users SET is_chirpy_red = true WHERE id = $1 +RETURNING id, created_at, updated_at, email, hashed_password, is_chirpy_red +` + +func (q *Queries) UpgradeUserToChirpyRed(ctx context.Context, id uuid.UUID) error { + _, err := q.db.ExecContext(ctx, upgradeUserToChirpyRed, id) + return err +} diff --git a/internal/database/models.go b/internal/database/models.go index 787dda5..21b6d50 100644 --- a/internal/database/models.go +++ b/internal/database/models.go @@ -34,4 +34,5 @@ type User struct { UpdatedAt time.Time Email string HashedPassword string + IsChirpyRed bool } diff --git a/main.go b/main.go index abf8a32..508131c 100644 --- a/main.go +++ b/main.go @@ -25,6 +25,7 @@ type apiConfig struct { db database.Queries platform string secret string + polkaKey string } func (cfg *apiConfig) middlewareMetricsInc(next http.Handler) http.Handler { @@ -82,10 +83,29 @@ func fileServerHandler() http.Handler { func getAllChirpsHandler(cfg *apiConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - chirps, err := cfg.db.GetAllChirps(context.Background()) - if err != nil { - httpResponse.SomethingWentWrong(w) - return + q := r.URL.Query().Get("author_id") + + var chirps []database.Chirp + var err error + var id uuid.UUID + + if len(q) == 0 { + chirps, err = cfg.db.GetAllChirps(context.Background()) + if err != nil { + httpResponse.SomethingWentWrong(w) + return + } + } else { + id, err = uuid.Parse(q) + if err != nil { + httpResponse.SomethingWentWrong(w) + return + } + chirps, err = cfg.db.GetChirpsByUserID(context.Background(), id) + if err != nil { + httpResponse.SomethingWentWrong(w) + return + } } data := make([]dataMaps.Chirp, 0) @@ -182,6 +202,59 @@ func deleteChirpByIDHandler(cfg *apiConfig) http.HandlerFunc { httpResponse.PlainTextHandler(w, http.StatusNoContent, "") } } +func polkaWebhookHandler(cfg *apiConfig) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + key, err := auth.GetAPIKey(r.Header) + if err != nil { + httpResponse.JSONHandler(w, http.StatusUnauthorized, `{"error": "Unauthorized"}`) + return + } + + if key != cfg.polkaKey { + httpResponse.JSONHandler(w, http.StatusUnauthorized, `{"error": "Unauthorized"}`) + return + } + + type polkaWebhook struct { + Event string `json:"event"` + Data struct { + UserId string `json:"user_id"` + } `json:"data"` + } + + data := polkaWebhook{} + err = json.NewDecoder(r.Body).Decode(&data) + if err != nil { + httpResponse.SomethingWentWrong(w) + return + } + + if data.Event != "user.upgraded" { + httpResponse.JSONHandler(w, http.StatusNoContent, "") + return + } + + userID, err := uuid.Parse(data.Data.UserId) + if err != nil { + httpResponse.JSONHandler(w, http.StatusBadRequest, `{"error": "InvalidUserID"}`) + return + } + + _, err = cfg.db.GetUserByID(context.Background(), userID) + if err != nil { + httpResponse.JSONHandler(w, http.StatusNotFound, `{"error": "UserNotFound"}`) + return + } + + err = cfg.db.UpgradeUserToChirpyRed(context.Background(), userID) + if err != nil { + httpResponse.SomethingWentWrong(w) + return + } + httpResponse.PlainTextHandler(w, http.StatusNoContent, "") + } +} func updateUserDatahandler(cfg *apiConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -228,10 +301,11 @@ func updateUserDatahandler(cfg *apiConfig) http.HandlerFunc { } userJSON := dataMaps.User{ - ID: user.ID, - CreatedAt: user.CreatedAt, - UpdatedAt: user.UpdatedAt, - Email: user.Email, + ID: user.ID, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + Email: user.Email, + IsChirpyRed: user.IsChirpyRed, } jsonData, err := json.Marshal(userJSON) @@ -389,10 +463,11 @@ func loginHandler(cfg *apiConfig) http.HandlerFunc { } userJSON := dataMaps.UserWithToken{ - ID: user.ID, - CreatedAt: user.CreatedAt, - UpdatedAt: user.UpdatedAt, - Email: user.Email, + ID: user.ID, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + Email: user.Email, + IsChirpyRed: user.IsChirpyRed, } expiresIn := time.Duration(3600) * time.Second @@ -459,10 +534,11 @@ func postUsersHandler(cfg *apiConfig) http.HandlerFunc { } userJSON := dataMaps.User{ - ID: usr.ID, - CreatedAt: usr.CreatedAt, - UpdatedAt: usr.UpdatedAt, - Email: usr.Email, + ID: usr.ID, + CreatedAt: usr.CreatedAt, + UpdatedAt: usr.UpdatedAt, + Email: usr.Email, + IsChirpyRed: usr.IsChirpyRed, } jsonData, err := json.Marshal(userJSON) @@ -500,6 +576,7 @@ func main() { apiCfg.platform = os.Getenv("PLATFORM") apiCfg.secret = os.Getenv("SECRET") + apiCfg.polkaKey = os.Getenv("POLKA_KEY") db, err := sql.Open("postgres", dbURL) if err != nil { @@ -522,6 +599,7 @@ func main() { mux.HandleFunc("GET /api/chirps", getAllChirpsHandler(apiCfg)) mux.HandleFunc("GET /api/chirps/{chirpID}", getChirpByIDHandler(apiCfg)) mux.HandleFunc("DELETE /api/chirps/{chirpID}", deleteChirpByIDHandler(apiCfg)) + mux.HandleFunc("POST /api/polka/webhooks", polkaWebhookHandler(apiCfg)) mux.HandleFunc("PUT /api/users", updateUserDatahandler(apiCfg)) mux.HandleFunc("GET /admin/metrics", adminMetricsHandler(apiCfg)) diff --git a/sql/queries/GetChirpsByUserID.sql b/sql/queries/GetChirpsByUserID.sql new file mode 100644 index 0000000..709e957 --- /dev/null +++ b/sql/queries/GetChirpsByUserID.sql @@ -0,0 +1,2 @@ +-- name: GetChirpsByUserID :many +SELECT * FROM chirps WHERE user_id = $1 ORDER BY created_at; \ No newline at end of file diff --git a/sql/queries/UpgradeUserToChirpyRed.sql b/sql/queries/UpgradeUserToChirpyRed.sql new file mode 100644 index 0000000..d4dae62 --- /dev/null +++ b/sql/queries/UpgradeUserToChirpyRed.sql @@ -0,0 +1,3 @@ +-- name: UpgradeUserToChirpyRed :exec +UPDATE users SET is_chirpy_red = true WHERE id = $1 +RETURNING *; \ No newline at end of file diff --git a/sql/schema/005_add_is_chirpy_red_column_to_users_table.sql b/sql/schema/005_add_is_chirpy_red_column_to_users_table.sql new file mode 100644 index 0000000..357fbd1 --- /dev/null +++ b/sql/schema/005_add_is_chirpy_red_column_to_users_table.sql @@ -0,0 +1,5 @@ +-- +goose Up +ALTER TABLE users ADD COLUMN is_chirpy_red BOOLEAN NOT NULL DEFAULT false; + +-- +goose Down +ALTER TABLE users DROP COLUMN is_chirpy_red; \ No newline at end of file