From 8b7fdf59b5e2fc274ecf606c76f3ef677e4f1861 Mon Sep 17 00:00:00 2001 From: syrell Date: Sat, 26 Oct 2024 15:03:14 +0200 Subject: [PATCH] feat: Added chapter 8 --- chirp.go | 49 ++++++++++++ internal/auth/auth.go | 12 +++ internal/database/chirps.sql.go | 16 ++++ internal/database/models.go | 1 + internal/database/update_users.sql.go | 49 ++++++++++++ internal/database/users.sql.go | 22 +++++- login.go | 2 + main.go | 9 +++ sql/queries/chirps.sql | 7 +- sql/queries/update_users.sql | 7 ++ sql/queries/users.sql | 7 +- sql/schema/005_users_membership.sql | 7 ++ users.go | 106 ++++++++++++++++++++++++++ 13 files changed, 289 insertions(+), 5 deletions(-) create mode 100644 internal/database/update_users.sql.go create mode 100644 sql/queries/update_users.sql create mode 100644 sql/schema/005_users_membership.sql diff --git a/chirp.go b/chirp.go index fb77cff..653e810 100644 --- a/chirp.go +++ b/chirp.go @@ -167,3 +167,52 @@ func (cfg *apiConfig) getChirp(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) w.Write(dat) } + +func (cfg *apiConfig) deleteChirp(w http.ResponseWriter, r *http.Request) { + token, err := auth.GetBearerToken(r.Header) + if err != nil { + log.Printf("Error extracting token: %s", err) + w.WriteHeader(401) + return + } + userId, err := auth.ValidateJWT(token, cfg.JWT) + if err != nil { + log.Printf("Invalid JWT: %s", err) + w.WriteHeader(401) + return + } + idFromQuery := r.PathValue("chirpID") + + id, err := uuid.Parse(idFromQuery) + if err != nil { + log.Printf("Not a valid ID: %s", err) + w.WriteHeader(500) + return + } + chirp, err := cfg.DB.GetChirp(r.Context(), id) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + w.WriteHeader(http.StatusNotFound) + return + } + log.Printf("Error getting chirp: %s", err) + w.WriteHeader(500) + return + } + if chirp.UserID != userId { + log.Printf("User %s not allowed to delete chirp owned by %s", userId, chirp.UserID) + w.WriteHeader(403) + return + } + + err = cfg.DB.DeleteChirp(r.Context(), database.DeleteChirpParams{ + ID: id, + UserID: userId, + }) + if err != nil { + log.Printf("Error deleting chirp: %v", err) + w.WriteHeader(404) + return + } + w.WriteHeader(204) +} diff --git a/internal/auth/auth.go b/internal/auth/auth.go index d8b16ea..60c42e4 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -81,3 +81,15 @@ func MakeRefreshToken() (string, error) { hexData := hex.EncodeToString(buffer) return hexData, nil } + +func GetAPIKey(headers http.Header) (string, error) { + authHeader := headers.Get("Authorization") + if authHeader == "" { + return "", errors.New("authorization header is not set") + } + if !strings.HasPrefix(authHeader, "ApiKey ") { + return "", errors.New("incorrect authorization type, must be of type ApiKey") + } + apiKey := strings.TrimPrefix(authHeader, "ApiKey ") + return strings.TrimSpace(apiKey), nil +} diff --git a/internal/database/chirps.sql.go b/internal/database/chirps.sql.go index b4348f4..1cca2c0 100644 --- a/internal/database/chirps.sql.go +++ b/internal/database/chirps.sql.go @@ -41,6 +41,22 @@ func (q *Queries) CreateChirp(ctx context.Context, arg CreateChirpParams) (Chirp return i, err } +const deleteChirp = `-- name: DeleteChirp :exec +DELETE FROM chirps +WHERE id = $1 +AND user_id = $2 +` + +type DeleteChirpParams struct { + ID uuid.UUID + UserID uuid.UUID +} + +func (q *Queries) DeleteChirp(ctx context.Context, arg DeleteChirpParams) error { + _, err := q.db.ExecContext(ctx, deleteChirp, arg.ID, arg.UserID) + return err +} + const getChirp = `-- name: GetChirp :one SELECT id, created_at, updated_at, body, user_id FROM chirps WHERE chirps.id = $1 diff --git a/internal/database/models.go b/internal/database/models.go index 7cc8f3a..b9bab9d 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/internal/database/update_users.sql.go b/internal/database/update_users.sql.go new file mode 100644 index 0000000..39aa305 --- /dev/null +++ b/internal/database/update_users.sql.go @@ -0,0 +1,49 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: update_users.sql + +package database + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +const updateUserCredentials = `-- name: UpdateUserCredentials :one +UPDATE users +SET email = $2, +hashed_password = $3, +updated_at = NOW() +WHERE users.id = $1 +RETURNING id, created_at, updated_at, email, is_chirpy_red +` + +type UpdateUserCredentialsParams struct { + ID uuid.UUID + Email string + HashedPassword string +} + +type UpdateUserCredentialsRow struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Email string + IsChirpyRed bool +} + +func (q *Queries) UpdateUserCredentials(ctx context.Context, arg UpdateUserCredentialsParams) (UpdateUserCredentialsRow, error) { + row := q.db.QueryRowContext(ctx, updateUserCredentials, arg.ID, arg.Email, arg.HashedPassword) + var i UpdateUserCredentialsRow + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Email, + &i.IsChirpyRed, + ) + return i, err +} diff --git a/internal/database/users.sql.go b/internal/database/users.sql.go index e2693f1..0a592dc 100644 --- a/internal/database/users.sql.go +++ b/internal/database/users.sql.go @@ -7,6 +7,8 @@ package database import ( "context" + + "github.com/google/uuid" ) const createUser = `-- name: CreateUser :one @@ -18,7 +20,7 @@ VALUES ( $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 { @@ -35,13 +37,14 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e &i.UpdatedAt, &i.Email, &i.HashedPassword, + &i.IsChirpyRed, ) return i, err } const deleteUser = `-- name: DeleteUser :one DELETE FROM users -RETURNING id, created_at, updated_at, email, hashed_password +RETURNING id, created_at, updated_at, email, hashed_password, is_chirpy_red ` func (q *Queries) DeleteUser(ctx context.Context) (User, error) { @@ -53,12 +56,13 @@ func (q *Queries) DeleteUser(ctx context.Context) (User, error) { &i.UpdatedAt, &i.Email, &i.HashedPassword, + &i.IsChirpyRed, ) return i, err } const getUserByEmail = `-- name: GetUserByEmail :one -SELECT id, created_at, updated_at, email, hashed_password FROM users +SELECT id, created_at, updated_at, email, hashed_password, is_chirpy_red FROM users WHERE users.email = $1 ` @@ -71,6 +75,18 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error &i.UpdatedAt, &i.Email, &i.HashedPassword, + &i.IsChirpyRed, ) return i, err } + +const upgradeUser = `-- name: UpgradeUser :exec +UPDATE users +SET is_chirpy_red = true +WHERE id = $1 +` + +func (q *Queries) UpgradeUser(ctx context.Context, id uuid.UUID) error { + _, err := q.db.ExecContext(ctx, upgradeUser, id) + return err +} diff --git a/login.go b/login.go index 4801327..52df23d 100644 --- a/login.go +++ b/login.go @@ -70,6 +70,7 @@ func (cfg *apiConfig) Login(w http.ResponseWriter, r *http.Request) { Email string `json:"email"` AccessToken string `json:"token"` RefreshToken string `json:"refresh_token"` + ChirpyRed bool `json:"is_chirpy_red"` } data, err := json.Marshal(loginResponse{ @@ -79,6 +80,7 @@ func (cfg *apiConfig) Login(w http.ResponseWriter, r *http.Request) { Email: loggedUser.Email, AccessToken: newJwt, RefreshToken: newRefreshToken, + ChirpyRed: loggedUser.IsChirpyRed, }) if err != nil { log.Printf("Error marshalling JSON: %s", err) diff --git a/main.go b/main.go index 39e9d52..d6f2e85 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ type apiConfig struct { DB *database.Queries Platform string JWT string + PolkaKey string } func main() { @@ -33,6 +34,10 @@ func main() { if jwtSecret == "" { log.Fatalf("Empty JWT_SECRET env var!") } + polkaKey := os.Getenv("POLKA_KEY") + if polkaKey == "" { + log.Fatalf("Empty POLKA_KEY env var!") + } db, err := sql.Open("postgres", dbURL) if err != nil { log.Fatalf("Unable to connect to db: %s", err) @@ -42,6 +47,7 @@ func main() { DB: database.New(db), Platform: platform, JWT: jwtSecret, + PolkaKey: polkaKey, } mux := http.NewServeMux() fsHandler := apiCfg.middlewareMetricsInc(http.StripPrefix("/app", http.FileServer(http.Dir(".")))) @@ -56,10 +62,13 @@ func main() { mux.HandleFunc("GET /api/chirps", apiCfg.getChirps) mux.HandleFunc("POST /api/chirps", apiCfg.chirpsCreate) mux.HandleFunc("POST /api/users", apiCfg.createUsers) + mux.HandleFunc("PUT /api/users", apiCfg.updateUsers) mux.HandleFunc("GET /api/chirps/{chirpID}", apiCfg.getChirp) + mux.HandleFunc("DELETE /api/chirps/{chirpID}", apiCfg.deleteChirp) mux.HandleFunc("POST /api/login", apiCfg.Login) mux.HandleFunc("POST /api/refresh", apiCfg.RefreshToken) mux.HandleFunc("POST /api/revoke", apiCfg.RevokeToken) + mux.HandleFunc("POST /api/polka/webhooks", apiCfg.subscribeUser) server := &http.Server{ Addr: ":8080", diff --git a/sql/queries/chirps.sql b/sql/queries/chirps.sql index 01e58fa..848f7e3 100644 --- a/sql/queries/chirps.sql +++ b/sql/queries/chirps.sql @@ -15,4 +15,9 @@ ORDER BY created_at ASC; -- name: GetChirp :one SELECT * FROM chirps -WHERE chirps.id = $1; \ No newline at end of file +WHERE chirps.id = $1; + +-- name: DeleteChirp :exec +DELETE FROM chirps +WHERE id = $1 +AND user_id = $2; \ No newline at end of file diff --git a/sql/queries/update_users.sql b/sql/queries/update_users.sql new file mode 100644 index 0000000..5ac4692 --- /dev/null +++ b/sql/queries/update_users.sql @@ -0,0 +1,7 @@ +-- name: UpdateUserCredentials :one +UPDATE users +SET email = $2, +hashed_password = $3, +updated_at = NOW() +WHERE users.id = $1 +RETURNING id, created_at, updated_at, email, is_chirpy_red; \ No newline at end of file diff --git a/sql/queries/users.sql b/sql/queries/users.sql index 1f63c63..6c706e7 100644 --- a/sql/queries/users.sql +++ b/sql/queries/users.sql @@ -15,4 +15,9 @@ RETURNING *; -- name: GetUserByEmail :one SELECT * FROM users -WHERE users.email = $1; \ No newline at end of file +WHERE users.email = $1; + +-- name: UpgradeUser :exec +UPDATE users +SET is_chirpy_red = true +WHERE id = $1; \ No newline at end of file diff --git a/sql/schema/005_users_membership.sql b/sql/schema/005_users_membership.sql new file mode 100644 index 0000000..85060ad --- /dev/null +++ b/sql/schema/005_users_membership.sql @@ -0,0 +1,7 @@ +-- +goose Up +ALTER TABLE users +ADD COLUMN is_chirpy_red BOOLEAN DEFAULT false NOT NULL; + +-- +goose Down +ALTER TABLE users +DROP COLUMN is_chirpy_red; \ No newline at end of file diff --git a/users.go b/users.go index 0c30cc8..e4cbad7 100644 --- a/users.go +++ b/users.go @@ -17,6 +17,7 @@ type User struct { UpdatedAt time.Time `json:"updated_at"` Email string `json:"email"` HashedPassword string `json:"-"` + ChirpyRed bool `json:"is_chirpy_red"` } func (cfg *apiConfig) createUsers(w http.ResponseWriter, r *http.Request) { @@ -54,6 +55,7 @@ func (cfg *apiConfig) createUsers(w http.ResponseWriter, r *http.Request) { CreatedAt: newDBUser.CreatedAt, UpdatedAt: newDBUser.UpdatedAt, Email: newDBUser.Email, + ChirpyRed: newDBUser.IsChirpyRed, } dat, err := json.Marshal(newUser) if err != nil { @@ -65,3 +67,107 @@ func (cfg *apiConfig) createUsers(w http.ResponseWriter, r *http.Request) { w.WriteHeader(201) w.Write(dat) } + +func (cfg *apiConfig) updateUsers(w http.ResponseWriter, r *http.Request) { + token, err := auth.GetBearerToken(r.Header) + if err != nil { + log.Printf("Error extracting token: %s", err) + w.WriteHeader(401) + return + } + userId, err := auth.ValidateJWT(token, cfg.JWT) + if err != nil { + log.Printf("Invalid JWT: %s", err) + w.WriteHeader(401) + return + } + + type parameters struct { + Email string `json:"email"` + Password string `json:"password"` + } + + decoder := json.NewDecoder(r.Body) + params := parameters{} + err = decoder.Decode(¶ms) + if err != nil { + log.Printf("Error decoding parameters: %s", err) + w.WriteHeader(500) + return + } + defer r.Body.Close() + + hashedPassword, err := auth.HashPassword(params.Password) + if err != nil { + log.Printf("Error hashing password: %s", err) + w.WriteHeader(500) + return + } + credentialsQueryParams := database.UpdateUserCredentialsParams{ + ID: userId, + Email: params.Email, + HashedPassword: hashedPassword, + } + updatedCredentials, err := cfg.DB.UpdateUserCredentials(r.Context(), credentialsQueryParams) + if err != nil { + log.Printf("Error updating user credentials: %v", err) + w.WriteHeader(500) + return + } + data, err := json.Marshal(User{ + ID: userId, + CreatedAt: updatedCredentials.CreatedAt, + UpdatedAt: updatedCredentials.UpdatedAt, + Email: updatedCredentials.Email, + ChirpyRed: updatedCredentials.IsChirpyRed, + }) + if err != nil { + log.Printf("Error marshalling JSON: %s", err) + w.WriteHeader(500) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + w.Write(data) +} + +func (cfg *apiConfig) subscribeUser(w http.ResponseWriter, r *http.Request) { + _, err := auth.GetAPIKey(r.Header) + if err != nil { + log.Printf("Error extracting apiKey: %s", err) + w.WriteHeader(401) + return + } + type parameters struct { + Event string `json:"event"` + Data map[string]string `json:"data"` + } + decoder := json.NewDecoder(r.Body) + params := parameters{} + err = decoder.Decode(¶ms) + if err != nil { + log.Printf("Error decoding parameters: %s", err) + w.WriteHeader(400) + return + } + defer r.Body.Close() + + if params.Event != "user.upgraded" { + w.WriteHeader(204) + return + } + paramsUserIdString := params.Data["user_id"] + paramsUserId, err := uuid.Parse(paramsUserIdString) + if err != nil { + log.Printf("Specified user_id is not a valid UUID: %v", err) + w.WriteHeader(400) + return + } + err = cfg.DB.UpgradeUser(r.Context(), paramsUserId) + if err != nil { + log.Printf("No user matches user_id given: %v", err) + w.WriteHeader(404) + return + } + w.WriteHeader(204) +}