From 72581d912e4acba2428beba7668e7e11278c8ba9 Mon Sep 17 00:00:00 2001 From: syrell Date: Sat, 26 Oct 2024 21:19:11 +0200 Subject: [PATCH] feat: Added last chapter and refactored whole project --- chirp.go | 218 ----------------------- main.go => cmd/server/main.go | 46 +++-- internal/database/chirps.sql.go | 34 ++++ internal/database/refresh_token.sql.go | 27 +++ internal/database/update_token.sql.go | 23 --- internal/database/update_users.sql.go | 49 ----- internal/database/user_from_token.sql.go | 26 --- internal/database/users.sql.go | 37 ++++ internal/handler/chirp.go | 213 ++++++++++++++++++++++ internal/handler/config.go | 15 ++ login.go => internal/handler/login.go | 29 ++- internal/handler/metrics.go | 27 +++ internal/handler/readiness.go | 9 + internal/handler/reset.go | 22 +++ internal/handler/response.go | 19 ++ token.go => internal/handler/token.go | 34 ++-- users.go => internal/handler/users.go | 75 +++----- metrics.go | 26 --- reset.go | 20 --- sql/queries/chirps.sql | 4 + sql/queries/refresh_token.sql | 15 +- sql/queries/update_token.sql | 6 - sql/queries/update_users.sql | 7 - sql/queries/user_from_token.sql | 5 - sql/queries/users.sql | 10 +- 25 files changed, 501 insertions(+), 495 deletions(-) delete mode 100644 chirp.go rename main.go => cmd/server/main.go (51%) delete mode 100644 internal/database/update_token.sql.go delete mode 100644 internal/database/update_users.sql.go delete mode 100644 internal/database/user_from_token.sql.go create mode 100644 internal/handler/chirp.go create mode 100644 internal/handler/config.go rename login.go => internal/handler/login.go (77%) create mode 100644 internal/handler/metrics.go create mode 100644 internal/handler/readiness.go create mode 100644 internal/handler/reset.go create mode 100644 internal/handler/response.go rename token.go => internal/handler/token.go (55%) rename users.go => internal/handler/users.go (67%) delete mode 100644 metrics.go delete mode 100644 reset.go delete mode 100644 sql/queries/update_token.sql delete mode 100644 sql/queries/update_users.sql delete mode 100644 sql/queries/user_from_token.sql diff --git a/chirp.go b/chirp.go deleted file mode 100644 index 653e810..0000000 --- a/chirp.go +++ /dev/null @@ -1,218 +0,0 @@ -package main - -import ( - "database/sql" - "encoding/json" - "errors" - "log" - "net/http" - "strings" - "time" - - "github.com/finchrelia/chirpy-server/internal/auth" - "github.com/finchrelia/chirpy-server/internal/database" - "github.com/google/uuid" -) - -type Chirp struct { - ID uuid.UUID `json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - UserID uuid.UUID `json:"user_id"` - Body string `json:"body"` -} - -func (cfg *apiConfig) chirpsCreate(w http.ResponseWriter, r *http.Request) { - type parameters struct { - Content string `json:"body"` - } - 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 - } - 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 - } else if len(params.Content) > 140 { - type errorVals struct { - Data string `json:"error"` - } - - respBody := errorVals{ - Data: "Chirp is too long", - } - dat, err := json.Marshal(respBody) - if err != nil { - log.Printf("Error marshalling JSON: %s", err) - w.WriteHeader(500) - return - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(400) - w.Write(dat) - } else { - cleanedData := cleanText(params.Content) - chirp, err := cfg.DB.CreateChirp(r.Context(), database.CreateChirpParams{ - Body: cleanedData, - UserID: userId, - }) - if err != nil { - log.Printf("Error creating chirp: %s", err) - w.WriteHeader(500) - return - } - dat, err := json.Marshal(Chirp{ - ID: chirp.ID, - CreatedAt: chirp.CreatedAt, - UpdatedAt: chirp.UpdatedAt, - Body: chirp.Body, - UserID: chirp.UserID, - }) - if err != nil { - log.Printf("Error marshalling JSON: %s", err) - w.WriteHeader(500) - return - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(201) - w.Write(dat) - } -} - -func cleanText(s string) string { - splittedString := strings.Split(s, " ") - for idx, element := range splittedString { - switch strings.ToLower(element) { - case - "kerfuffle", - "sharbert", - "fornax": - splittedString[idx] = "****" - } - } - return strings.Join(splittedString, " ") -} - -func (cfg *apiConfig) getChirps(w http.ResponseWriter, r *http.Request) { - chirps, err := cfg.DB.GetChirps(r.Context()) - if err != nil { - log.Printf("Error getting chirps: %s", err) - w.WriteHeader(500) - return - } - newChirps := []Chirp{} - for _, chirp := range chirps { - newChirps = append(newChirps, Chirp{ - ID: chirp.ID, - CreatedAt: chirp.CreatedAt, - UpdatedAt: chirp.UpdatedAt, - Body: chirp.Body, - UserID: chirp.UserID, - }) - } - dat, err := json.Marshal(newChirps) - 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(dat) -} - -func (cfg *apiConfig) getChirp(w http.ResponseWriter, r *http.Request) { - 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 - } - dat, err := json.Marshal(Chirp{ - ID: chirp.ID, - CreatedAt: chirp.CreatedAt, - UpdatedAt: chirp.UpdatedAt, - Body: chirp.Body, - UserID: chirp.UserID, - }) - 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(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/main.go b/cmd/server/main.go similarity index 51% rename from main.go rename to cmd/server/main.go index d6f2e85..698e65b 100644 --- a/main.go +++ b/cmd/server/main.go @@ -8,18 +8,11 @@ import ( "sync/atomic" "github.com/finchrelia/chirpy-server/internal/database" + "github.com/finchrelia/chirpy-server/internal/handler" "github.com/joho/godotenv" _ "github.com/lib/pq" ) -type apiConfig struct { - fileserverHits atomic.Int32 - DB *database.Queries - Platform string - JWT string - PolkaKey string -} - func main() { godotenv.Load() dbURL := os.Getenv("DB_URL") @@ -40,35 +33,36 @@ func main() { } db, err := sql.Open("postgres", dbURL) if err != nil { - log.Fatalf("Unable to connect to db: %s", err) + log.Fatalf("Unable to connect to db: %v", err) } - apiCfg := &apiConfig{ - fileserverHits: atomic.Int32{}, + apiCfg := &handler.APIConfig{ + FileserverHits: atomic.Int32{}, DB: database.New(db), Platform: platform, JWT: jwtSecret, PolkaKey: polkaKey, } + mux := http.NewServeMux() - fsHandler := apiCfg.middlewareMetricsInc(http.StripPrefix("/app", http.FileServer(http.Dir(".")))) + fsHandler := apiCfg.MiddlewareMetricsInc(http.StripPrefix("/app", http.FileServer(http.Dir(".")))) mux.Handle("/app/", fsHandler) - mux.HandleFunc("GET /api/healthz", func(w http.ResponseWriter, req *http.Request) { - req.Header.Set("Content-Type", "text/plain; charset=utf-8") - w.WriteHeader(http.StatusOK) - w.Write([]byte("OK")) - }) - mux.Handle("GET /admin/metrics", http.HandlerFunc(apiCfg.serveMetrics)) - mux.Handle("POST /admin/reset", http.HandlerFunc(apiCfg.serveReset)) - 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("GET /api/healthz", handler.Readiness) + mux.HandleFunc("POST /api/polka/webhooks", apiCfg.SubscribeUser) + 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) + + mux.Handle("GET /admin/metrics", http.HandlerFunc(apiCfg.Metrics)) + mux.Handle("POST /admin/reset", http.HandlerFunc(apiCfg.Reset)) + + mux.HandleFunc("GET /api/chirps", apiCfg.GetChirps) + mux.HandleFunc("POST /api/chirps", apiCfg.ChirpsCreate) + mux.HandleFunc("GET /api/chirps/{chirpID}", apiCfg.GetChirp) + mux.HandleFunc("DELETE /api/chirps/{chirpID}", apiCfg.DeleteChirp) + + mux.HandleFunc("POST /api/users", apiCfg.CreateUsers) + mux.HandleFunc("PUT /api/users", apiCfg.UpdateUsers) server := &http.Server{ Addr: ":8080", diff --git a/internal/database/chirps.sql.go b/internal/database/chirps.sql.go index 1cca2c0..4f60811 100644 --- a/internal/database/chirps.sql.go +++ b/internal/database/chirps.sql.go @@ -108,3 +108,37 @@ func (q *Queries) GetChirps(ctx context.Context) ([]Chirp, error) { } return items, nil } + +const getChirpsByUserid = `-- name: GetChirpsByUserid :many +SELECT id, created_at, updated_at, body, user_id FROM chirps +WHERE chirps.user_id = $1 +` + +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/refresh_token.sql.go b/internal/database/refresh_token.sql.go index 4e38bd5..2d206d2 100644 --- a/internal/database/refresh_token.sql.go +++ b/internal/database/refresh_token.sql.go @@ -44,3 +44,30 @@ func (q *Queries) CreateRefreshToken(ctx context.Context, arg CreateRefreshToken ) return i, err } + +const getUserFromRefreshToken = `-- name: GetUserFromRefreshToken :one +SELECT user_id FROM refresh_tokens +WHERE refresh_tokens.token = $1 +AND refresh_tokens.expires_at > NOW() +AND refresh_tokens.revoked_at IS NULL +` + +func (q *Queries) GetUserFromRefreshToken(ctx context.Context, token string) (uuid.UUID, error) { + row := q.db.QueryRowContext(ctx, getUserFromRefreshToken, token) + var user_id uuid.UUID + err := row.Scan(&user_id) + return user_id, err +} + +const revokeRefreshToken = `-- name: RevokeRefreshToken :exec +UPDATE refresh_tokens +SET + revoked_at = NOW(), + updated_at = NOW() +WHERE token = $1 +` + +func (q *Queries) RevokeRefreshToken(ctx context.Context, token string) error { + _, err := q.db.ExecContext(ctx, revokeRefreshToken, token) + return err +} diff --git a/internal/database/update_token.sql.go b/internal/database/update_token.sql.go deleted file mode 100644 index 3a4ac72..0000000 --- a/internal/database/update_token.sql.go +++ /dev/null @@ -1,23 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.27.0 -// source: update_token.sql - -package database - -import ( - "context" -) - -const revokeRefreshToken = `-- name: RevokeRefreshToken :exec -UPDATE refresh_tokens -SET - revoked_at = NOW(), - updated_at = NOW() -WHERE token = $1 -` - -func (q *Queries) RevokeRefreshToken(ctx context.Context, token string) error { - _, err := q.db.ExecContext(ctx, revokeRefreshToken, token) - return err -} diff --git a/internal/database/update_users.sql.go b/internal/database/update_users.sql.go deleted file mode 100644 index 39aa305..0000000 --- a/internal/database/update_users.sql.go +++ /dev/null @@ -1,49 +0,0 @@ -// 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/user_from_token.sql.go b/internal/database/user_from_token.sql.go deleted file mode 100644 index 92206ac..0000000 --- a/internal/database/user_from_token.sql.go +++ /dev/null @@ -1,26 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.27.0 -// source: user_from_token.sql - -package database - -import ( - "context" - - "github.com/google/uuid" -) - -const getUserFromRefreshToken = `-- name: GetUserFromRefreshToken :one -SELECT user_id FROM refresh_tokens -WHERE refresh_tokens.token = $1 -AND refresh_tokens.expires_at > NOW() -AND refresh_tokens.revoked_at IS NULL -` - -func (q *Queries) GetUserFromRefreshToken(ctx context.Context, token string) (uuid.UUID, error) { - row := q.db.QueryRowContext(ctx, getUserFromRefreshToken, token) - var user_id uuid.UUID - err := row.Scan(&user_id) - return user_id, err -} diff --git a/internal/database/users.sql.go b/internal/database/users.sql.go index 0a592dc..5322458 100644 --- a/internal/database/users.sql.go +++ b/internal/database/users.sql.go @@ -7,6 +7,7 @@ package database import ( "context" + "time" "github.com/google/uuid" ) @@ -80,6 +81,42 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error return i, err } +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 +} + const upgradeUser = `-- name: UpgradeUser :exec UPDATE users SET is_chirpy_red = true diff --git a/internal/handler/chirp.go b/internal/handler/chirp.go new file mode 100644 index 0000000..37958c3 --- /dev/null +++ b/internal/handler/chirp.go @@ -0,0 +1,213 @@ +package handler + +import ( + "database/sql" + "encoding/json" + "errors" + "log" + "net/http" + "sort" + "strings" + "time" + + "github.com/finchrelia/chirpy-server/internal/auth" + "github.com/finchrelia/chirpy-server/internal/database" + "github.com/google/uuid" +) + +type Chirp struct { + ID uuid.UUID `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + UserID uuid.UUID `json:"user_id"` + Body string `json:"body"` +} + +func (cfg *APIConfig) ChirpsCreate(w http.ResponseWriter, r *http.Request) { + type parameters struct { + Content string `json:"body"` + } + token, err := auth.GetBearerToken(r.Header) + if err != nil { + log.Printf("Error extracting token: %v", err) + w.WriteHeader(http.StatusUnauthorized) + return + } + userId, err := auth.ValidateJWT(token, cfg.JWT) + if err != nil { + log.Printf("Invalid JWT: %v", err) + w.WriteHeader(http.StatusUnauthorized) + return + } + decoder := json.NewDecoder(r.Body) + params := parameters{} + err = decoder.Decode(¶ms) + if err != nil { + log.Printf("Error decoding parameters: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + cleanedChirp, err := cleanChirp(params.Content) + if err != nil { + type errorResponse struct { + Error error `json:"error"` + } + JsonResponse(w, http.StatusBadRequest, errorResponse{Error: err}) + } + chirp, err := cfg.DB.CreateChirp(r.Context(), database.CreateChirpParams{ + Body: cleanedChirp, + UserID: userId, + }) + if err != nil { + log.Printf("Error creating chirp: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + JsonResponse(w, http.StatusCreated, Chirp{ + ID: chirp.ID, + CreatedAt: chirp.CreatedAt, + UpdatedAt: chirp.UpdatedAt, + Body: chirp.Body, + UserID: chirp.UserID, + }) +} + +func cleanChirp(s string) (string, error) { + const maxChirpLength = 140 + if len(s) > maxChirpLength { + return "", errors.New("Chirp is too long") + } + + splittedString := strings.Split(s, " ") + for idx, element := range splittedString { + switch strings.ToLower(element) { + case + "kerfuffle", + "sharbert", + "fornax": + splittedString[idx] = "****" + } + } + return strings.Join(splittedString, " "), nil +} + +func (cfg *APIConfig) GetChirps(w http.ResponseWriter, r *http.Request) { + dbChirps := []database.Chirp{} + authorIdString := r.URL.Query().Get("author_id") + if authorIdString != "" { + authorId, err := uuid.Parse(authorIdString) + if err != nil { + log.Printf("Incorrect author ID: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + dbChirps, err = cfg.DB.GetChirpsByUserid(r.Context(), authorId) + if err != nil { + log.Printf("No chirp for user given: %v", err) + } + } else { + var err error + dbChirps, err = cfg.DB.GetChirps(r.Context()) + if err != nil { + log.Printf("Error getting chirps: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + } + + chirps := []Chirp{} + for _, chirp := range dbChirps { + chirps = append(chirps, Chirp{ + ID: chirp.ID, + CreatedAt: chirp.CreatedAt, + UpdatedAt: chirp.UpdatedAt, + Body: chirp.Body, + UserID: chirp.UserID, + }) + } + sortOrder := r.URL.Query().Get("sort") + sort.Slice(chirps, func(i, j int) bool { + if sortOrder == "desc" { + return chirps[i].CreatedAt.After(chirps[j].CreatedAt) + } + // Defaults to asc + return chirps[i].CreatedAt.Before(chirps[j].CreatedAt) + }) + JsonResponse(w, http.StatusOK, chirps) +} + +func (cfg *APIConfig) GetChirp(w http.ResponseWriter, r *http.Request) { + idFromQuery := r.PathValue("chirpID") + id, err := uuid.Parse(idFromQuery) + if err != nil { + log.Printf("Not a valid ID: %v", err) + w.WriteHeader(http.StatusInternalServerError) + 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: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + JsonResponse(w, http.StatusOK, Chirp{ + ID: chirp.ID, + CreatedAt: chirp.CreatedAt, + UpdatedAt: chirp.UpdatedAt, + Body: chirp.Body, + UserID: chirp.UserID, + }) +} + +func (cfg *APIConfig) DeleteChirp(w http.ResponseWriter, r *http.Request) { + token, err := auth.GetBearerToken(r.Header) + if err != nil { + log.Printf("Error extracting token: %v", err) + w.WriteHeader(http.StatusUnauthorized) + return + } + userId, err := auth.ValidateJWT(token, cfg.JWT) + if err != nil { + log.Printf("Invalid JWT: %v", err) + w.WriteHeader(http.StatusUnauthorized) + return + } + idFromQuery := r.PathValue("chirpID") + + id, err := uuid.Parse(idFromQuery) + if err != nil { + log.Printf("Not a valid ID: %v", err) + w.WriteHeader(http.StatusInternalServerError) + 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: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + if chirp.UserID != userId { + log.Printf("User %s not allowed to delete chirp owned by %s", userId, chirp.UserID) + w.WriteHeader(http.StatusForbidden) + 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(http.StatusNotFound) + return + } + w.WriteHeader(http.StatusNoContent) +} diff --git a/internal/handler/config.go b/internal/handler/config.go new file mode 100644 index 0000000..003399d --- /dev/null +++ b/internal/handler/config.go @@ -0,0 +1,15 @@ +package handler + +import ( + "sync/atomic" + + "github.com/finchrelia/chirpy-server/internal/database" +) + +type APIConfig struct { + FileserverHits atomic.Int32 + DB *database.Queries + Platform string + JWT string + PolkaKey string +} diff --git a/login.go b/internal/handler/login.go similarity index 77% rename from login.go rename to internal/handler/login.go index 52df23d..1a5a726 100644 --- a/login.go +++ b/internal/handler/login.go @@ -1,4 +1,4 @@ -package main +package handler import ( "database/sql" @@ -12,7 +12,7 @@ import ( "github.com/google/uuid" ) -func (cfg *apiConfig) Login(w http.ResponseWriter, r *http.Request) { +func (cfg *APIConfig) Login(w http.ResponseWriter, r *http.Request) { type params struct { Email string `json:"email"` Password string `json:"password"` @@ -23,7 +23,7 @@ func (cfg *apiConfig) Login(w http.ResponseWriter, r *http.Request) { err := decoder.Decode(&p) if err != nil { log.Printf("Incorrect email or password") - w.WriteHeader(401) + w.WriteHeader(http.StatusUnauthorized) return } @@ -35,20 +35,20 @@ func (cfg *apiConfig) Login(w http.ResponseWriter, r *http.Request) { err = auth.CheckPasswordHash(p.Password, loggedUser.HashedPassword) if err != nil { log.Printf("Incorrect email or password") - w.WriteHeader(401) + w.WriteHeader(http.StatusUnauthorized) return } newJwt, err := auth.MakeJWT(loggedUser.ID, cfg.JWT) if err != nil { - log.Printf("Error creating JWT: %s", newJwt) - w.WriteHeader(500) + log.Printf("Error creating JWT: %v", newJwt) + w.WriteHeader(http.StatusInternalServerError) return } newRefreshToken, err := auth.MakeRefreshToken() if err != nil { log.Printf("Error creating refresh token: %v", err) - w.WriteHeader(500) + w.WriteHeader(http.StatusInternalServerError) return } @@ -59,8 +59,8 @@ func (cfg *apiConfig) Login(w http.ResponseWriter, r *http.Request) { } _, err = cfg.DB.CreateRefreshToken(r.Context(), refreshTokenParams) if err != nil { - log.Printf("Error adding refresh token to db: %s", err) - w.WriteHeader(500) + log.Printf("Error adding refresh token to db: %v", err) + w.WriteHeader(http.StatusInternalServerError) return } type loginResponse struct { @@ -72,8 +72,7 @@ func (cfg *apiConfig) Login(w http.ResponseWriter, r *http.Request) { RefreshToken string `json:"refresh_token"` ChirpyRed bool `json:"is_chirpy_red"` } - - data, err := json.Marshal(loginResponse{ + JsonResponse(w, http.StatusOK, loginResponse{ ID: loggedUser.ID, CreatedAt: loggedUser.CreatedAt, UpdatedAt: loggedUser.UpdatedAt, @@ -82,12 +81,4 @@ func (cfg *apiConfig) Login(w http.ResponseWriter, r *http.Request) { RefreshToken: newRefreshToken, ChirpyRed: loggedUser.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) } diff --git a/internal/handler/metrics.go b/internal/handler/metrics.go new file mode 100644 index 0000000..1f6a372 --- /dev/null +++ b/internal/handler/metrics.go @@ -0,0 +1,27 @@ +package handler + +import ( + "fmt" + "net/http" +) + +func (cfg *APIConfig) MiddlewareMetricsInc(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + cfg.FileserverHits.Add(1) + next.ServeHTTP(w, r) + }) +} + +func (cfg *APIConfig) Metrics(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "text/html") + w.WriteHeader(http.StatusOK) + hits := cfg.FileserverHits.Load() + template := ` + + +

Welcome, Chirpy Admin

+

Chirpy has been visited %d times!

+ + ` + w.Write([]byte(fmt.Sprintf(template, hits))) +} diff --git a/internal/handler/readiness.go b/internal/handler/readiness.go new file mode 100644 index 0000000..da15d66 --- /dev/null +++ b/internal/handler/readiness.go @@ -0,0 +1,9 @@ +package handler + +import "net/http" + +func Readiness(w http.ResponseWriter, req *http.Request) { + req.Header.Set("Content-Type", "text/plain; charset=utf-8") + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) +} diff --git a/internal/handler/reset.go b/internal/handler/reset.go new file mode 100644 index 0000000..1bff2a5 --- /dev/null +++ b/internal/handler/reset.go @@ -0,0 +1,22 @@ +package handler + +import ( + "log" + "net/http" +) + +func (cfg *APIConfig) Reset(w http.ResponseWriter, r *http.Request) { + cfg.FileserverHits.Store(0) + if cfg.Platform != "dev" { + log.Printf("Invalid %s platform !", cfg.Platform) + w.WriteHeader(http.StatusForbidden) + return + } + + _, err := cfg.DB.DeleteUser(r.Context()) + if err != nil { + log.Printf("Error deleting users: %v", err) + w.WriteHeader(http.StatusBadRequest) + } + w.WriteHeader(http.StatusOK) +} diff --git a/internal/handler/response.go b/internal/handler/response.go new file mode 100644 index 0000000..f37eae7 --- /dev/null +++ b/internal/handler/response.go @@ -0,0 +1,19 @@ +package handler + +import ( + "encoding/json" + "log" + "net/http" +) + +func JsonResponse(w http.ResponseWriter, statusCode int, payload interface{}) { + w.Header().Set("Content-Type", "application/json") + data, err := json.Marshal(payload) + if err != nil { + log.Printf("Error marshalling JSON: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(statusCode) + w.Write(data) +} diff --git a/token.go b/internal/handler/token.go similarity index 55% rename from token.go rename to internal/handler/token.go index 5613e01..4769bc3 100644 --- a/token.go +++ b/internal/handler/token.go @@ -1,62 +1,52 @@ -package main +package handler import ( - "encoding/json" "log" "net/http" "github.com/finchrelia/chirpy-server/internal/auth" ) -func (cfg *apiConfig) RefreshToken(w http.ResponseWriter, r *http.Request) { +func (cfg *APIConfig) RefreshToken(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) + log.Printf("Error extracting token: %v", err) + w.WriteHeader(http.StatusUnauthorized) return } dbUser, err := cfg.DB.GetUserFromRefreshToken(r.Context(), token) if err != nil { log.Printf("Error getting user: %v", err) - w.WriteHeader(401) + w.WriteHeader(http.StatusUnauthorized) return } newToken, err := auth.MakeJWT(dbUser, cfg.JWT) if err != nil { log.Printf("Error creating new JWT: %v", err) - w.WriteHeader(500) + w.WriteHeader(http.StatusInternalServerError) return } type tokenResponse struct { AccessToken string `json:"token"` } - - data, err := json.Marshal(tokenResponse{ + JsonResponse(w, http.StatusOK, tokenResponse{ AccessToken: newToken, }) - 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) RevokeToken(w http.ResponseWriter, r *http.Request) { +func (cfg *APIConfig) RevokeToken(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) + log.Printf("Error extracting token: %v", err) + w.WriteHeader(http.StatusUnauthorized) return } err = cfg.DB.RevokeRefreshToken(r.Context(), token) if err != nil { log.Printf("Error revoking token in database: %v", err) - w.WriteHeader(500) + w.WriteHeader(http.StatusInternalServerError) return } - w.WriteHeader(204) + w.WriteHeader(http.StatusNoContent) } diff --git a/users.go b/internal/handler/users.go similarity index 67% rename from users.go rename to internal/handler/users.go index e4cbad7..eb557fa 100644 --- a/users.go +++ b/internal/handler/users.go @@ -1,4 +1,4 @@ -package main +package handler import ( "encoding/json" @@ -20,7 +20,7 @@ type User struct { ChirpyRed bool `json:"is_chirpy_red"` } -func (cfg *apiConfig) createUsers(w http.ResponseWriter, r *http.Request) { +func (cfg *APIConfig) CreateUsers(w http.ResponseWriter, r *http.Request) { type parameters struct { Email string `json:"email"` Password string `json:"password"` @@ -30,55 +30,46 @@ func (cfg *apiConfig) createUsers(w http.ResponseWriter, r *http.Request) { params := parameters{} err := decoder.Decode(¶ms) if err != nil { - log.Printf("Error decoding parameters: %s", err) - w.WriteHeader(500) + log.Printf("Error decoding parameters: %v", err) + w.WriteHeader(http.StatusInternalServerError) return } defer r.Body.Close() hashedPassword, err := auth.HashPassword(params.Password) if err != nil { - log.Printf("Error hashing password: %s", err) + log.Printf("Error hashing password: %v", err) } newDBUser, err := cfg.DB.CreateUser(r.Context(), database.CreateUserParams{ Email: params.Email, HashedPassword: hashedPassword, }) if err != nil { - log.Printf("Error creating user %s: %s", params.Email, err) - w.WriteHeader(500) + log.Printf("Error creating user %s: %v", params.Email, err) + w.WriteHeader(http.StatusInternalServerError) return } newId := newDBUser.ID - newUser := User{ + JsonResponse(w, http.StatusCreated, User{ ID: newId, CreatedAt: newDBUser.CreatedAt, UpdatedAt: newDBUser.UpdatedAt, Email: newDBUser.Email, ChirpyRed: newDBUser.IsChirpyRed, - } - dat, err := json.Marshal(newUser) - if err != nil { - log.Printf("Error marshalling JSON: %s", err) - w.WriteHeader(500) - return - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(201) - w.Write(dat) + }) } -func (cfg *apiConfig) updateUsers(w http.ResponseWriter, r *http.Request) { +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) + log.Printf("Error extracting token: %v", err) + w.WriteHeader(http.StatusUnauthorized) return } userId, err := auth.ValidateJWT(token, cfg.JWT) if err != nil { - log.Printf("Invalid JWT: %s", err) - w.WriteHeader(401) + log.Printf("Invalid JWT: %v", err) + w.WriteHeader(http.StatusUnauthorized) return } @@ -91,16 +82,16 @@ func (cfg *apiConfig) updateUsers(w http.ResponseWriter, r *http.Request) { params := parameters{} err = decoder.Decode(¶ms) if err != nil { - log.Printf("Error decoding parameters: %s", err) - w.WriteHeader(500) + log.Printf("Error decoding parameters: %v", err) + w.WriteHeader(http.StatusInternalServerError) return } defer r.Body.Close() hashedPassword, err := auth.HashPassword(params.Password) if err != nil { - log.Printf("Error hashing password: %s", err) - w.WriteHeader(500) + log.Printf("Error hashing password: %v", err) + w.WriteHeader(http.StatusInternalServerError) return } credentialsQueryParams := database.UpdateUserCredentialsParams{ @@ -111,31 +102,23 @@ func (cfg *apiConfig) updateUsers(w http.ResponseWriter, r *http.Request) { updatedCredentials, err := cfg.DB.UpdateUserCredentials(r.Context(), credentialsQueryParams) if err != nil { log.Printf("Error updating user credentials: %v", err) - w.WriteHeader(500) + w.WriteHeader(http.StatusInternalServerError) return } - data, err := json.Marshal(User{ + JsonResponse(w, http.StatusOK, 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) { +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) + log.Printf("Error extracting apiKey: %v", err) + w.WriteHeader(http.StatusUnauthorized) return } type parameters struct { @@ -146,28 +129,28 @@ func (cfg *apiConfig) subscribeUser(w http.ResponseWriter, r *http.Request) { params := parameters{} err = decoder.Decode(¶ms) if err != nil { - log.Printf("Error decoding parameters: %s", err) - w.WriteHeader(400) + log.Printf("Error decoding parameters: %v", err) + w.WriteHeader(http.StatusBadRequest) return } defer r.Body.Close() if params.Event != "user.upgraded" { - w.WriteHeader(204) + w.WriteHeader(http.StatusNoContent) 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) + w.WriteHeader(http.StatusBadRequest) return } err = cfg.DB.UpgradeUser(r.Context(), paramsUserId) if err != nil { log.Printf("No user matches user_id given: %v", err) - w.WriteHeader(404) + w.WriteHeader(http.StatusNotFound) return } - w.WriteHeader(204) + w.WriteHeader(http.StatusNoContent) } diff --git a/metrics.go b/metrics.go deleted file mode 100644 index 0ca32cf..0000000 --- a/metrics.go +++ /dev/null @@ -1,26 +0,0 @@ -package main - -import ( - "fmt" - "net/http" -) - -func (cfg *apiConfig) middlewareMetricsInc(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - cfg.fileserverHits.Add(1) - next.ServeHTTP(w, req) - }) -} - -func (cfg *apiConfig) serveMetrics(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Content-Type", "text/html") - hits := cfg.fileserverHits.Load() - template := ` - - -

Welcome, Chirpy Admin

-

Chirpy has been visited %d times!

- - ` - fmt.Fprintf(w, template, hits) -} diff --git a/reset.go b/reset.go deleted file mode 100644 index 11aade0..0000000 --- a/reset.go +++ /dev/null @@ -1,20 +0,0 @@ -package main - -import ( - "log" - "net/http" -) - -func (cfg *apiConfig) serveReset(w http.ResponseWriter, r *http.Request) { - cfg.fileserverHits.Store(0) - if cfg.Platform != "dev" { - log.Printf("Invalid %s platform !", cfg.Platform) - w.WriteHeader(http.StatusForbidden) - return - } - - _, err := cfg.DB.DeleteUser(r.Context()) - if err != nil { - log.Printf("Error deleting users: %s", err) - } -} diff --git a/sql/queries/chirps.sql b/sql/queries/chirps.sql index 848f7e3..2054536 100644 --- a/sql/queries/chirps.sql +++ b/sql/queries/chirps.sql @@ -13,6 +13,10 @@ RETURNING *; SELECT * FROM chirps ORDER BY created_at ASC; +-- name: GetChirpsByUserid :many +SELECT * FROM chirps +WHERE chirps.user_id = $1; + -- name: GetChirp :one SELECT * FROM chirps WHERE chirps.id = $1; diff --git a/sql/queries/refresh_token.sql b/sql/queries/refresh_token.sql index ca40189..f53c1ff 100644 --- a/sql/queries/refresh_token.sql +++ b/sql/queries/refresh_token.sql @@ -8,4 +8,17 @@ VALUES ( $3, NULL ) -RETURNING *; \ No newline at end of file +RETURNING *; + +-- name: RevokeRefreshToken :exec +UPDATE refresh_tokens +SET + revoked_at = NOW(), + updated_at = NOW() +WHERE token = $1; + +-- name: GetUserFromRefreshToken :one +SELECT user_id FROM refresh_tokens +WHERE refresh_tokens.token = $1 +AND refresh_tokens.expires_at > NOW() +AND refresh_tokens.revoked_at IS NULL; \ No newline at end of file diff --git a/sql/queries/update_token.sql b/sql/queries/update_token.sql deleted file mode 100644 index 6000e9d..0000000 --- a/sql/queries/update_token.sql +++ /dev/null @@ -1,6 +0,0 @@ --- name: RevokeRefreshToken :exec -UPDATE refresh_tokens -SET - revoked_at = NOW(), - updated_at = NOW() -WHERE token = $1; \ No newline at end of file diff --git a/sql/queries/update_users.sql b/sql/queries/update_users.sql deleted file mode 100644 index 5ac4692..0000000 --- a/sql/queries/update_users.sql +++ /dev/null @@ -1,7 +0,0 @@ --- 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/user_from_token.sql b/sql/queries/user_from_token.sql deleted file mode 100644 index 6d7f8d8..0000000 --- a/sql/queries/user_from_token.sql +++ /dev/null @@ -1,5 +0,0 @@ --- name: GetUserFromRefreshToken :one -SELECT user_id FROM refresh_tokens -WHERE refresh_tokens.token = $1 -AND refresh_tokens.expires_at > NOW() -AND refresh_tokens.revoked_at IS NULL; \ No newline at end of file diff --git a/sql/queries/users.sql b/sql/queries/users.sql index 6c706e7..cf14ef8 100644 --- a/sql/queries/users.sql +++ b/sql/queries/users.sql @@ -20,4 +20,12 @@ WHERE users.email = $1; -- name: UpgradeUser :exec UPDATE users SET is_chirpy_red = true -WHERE id = $1; \ No newline at end of file +WHERE id = $1; + +-- 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