feat: Added last chapter and refactored whole project

This commit is contained in:
syrell 2024-10-26 21:19:11 +02:00
parent 8b7fdf59b5
commit 72581d912e
25 changed files with 501 additions and 495 deletions

218
chirp.go
View File

@ -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(&params)
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)
}

View File

@ -8,18 +8,11 @@ import (
"sync/atomic" "sync/atomic"
"github.com/finchrelia/chirpy-server/internal/database" "github.com/finchrelia/chirpy-server/internal/database"
"github.com/finchrelia/chirpy-server/internal/handler"
"github.com/joho/godotenv" "github.com/joho/godotenv"
_ "github.com/lib/pq" _ "github.com/lib/pq"
) )
type apiConfig struct {
fileserverHits atomic.Int32
DB *database.Queries
Platform string
JWT string
PolkaKey string
}
func main() { func main() {
godotenv.Load() godotenv.Load()
dbURL := os.Getenv("DB_URL") dbURL := os.Getenv("DB_URL")
@ -40,35 +33,36 @@ func main() {
} }
db, err := sql.Open("postgres", dbURL) db, err := sql.Open("postgres", dbURL)
if err != nil { if err != nil {
log.Fatalf("Unable to connect to db: %s", err) log.Fatalf("Unable to connect to db: %v", err)
} }
apiCfg := &apiConfig{ apiCfg := &handler.APIConfig{
fileserverHits: atomic.Int32{}, FileserverHits: atomic.Int32{},
DB: database.New(db), DB: database.New(db),
Platform: platform, Platform: platform,
JWT: jwtSecret, JWT: jwtSecret,
PolkaKey: polkaKey, PolkaKey: polkaKey,
} }
mux := http.NewServeMux() 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.Handle("/app/", fsHandler)
mux.HandleFunc("GET /api/healthz", func(w http.ResponseWriter, req *http.Request) { mux.HandleFunc("GET /api/healthz", handler.Readiness)
req.Header.Set("Content-Type", "text/plain; charset=utf-8") mux.HandleFunc("POST /api/polka/webhooks", apiCfg.SubscribeUser)
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("POST /api/login", apiCfg.Login) mux.HandleFunc("POST /api/login", apiCfg.Login)
mux.HandleFunc("POST /api/refresh", apiCfg.RefreshToken) mux.HandleFunc("POST /api/refresh", apiCfg.RefreshToken)
mux.HandleFunc("POST /api/revoke", apiCfg.RevokeToken) 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{ server := &http.Server{
Addr: ":8080", Addr: ":8080",

View File

@ -108,3 +108,37 @@ func (q *Queries) GetChirps(ctx context.Context) ([]Chirp, error) {
} }
return items, nil 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
}

View File

@ -44,3 +44,30 @@ func (q *Queries) CreateRefreshToken(ctx context.Context, arg CreateRefreshToken
) )
return i, err 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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -7,6 +7,7 @@ package database
import ( import (
"context" "context"
"time"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -80,6 +81,42 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error
return i, err 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 const upgradeUser = `-- name: UpgradeUser :exec
UPDATE users UPDATE users
SET is_chirpy_red = true SET is_chirpy_red = true

213
internal/handler/chirp.go Normal file
View File

@ -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(&params)
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)
}

View File

@ -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
}

View File

@ -1,4 +1,4 @@
package main package handler
import ( import (
"database/sql" "database/sql"
@ -12,7 +12,7 @@ import (
"github.com/google/uuid" "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 { type params struct {
Email string `json:"email"` Email string `json:"email"`
Password string `json:"password"` Password string `json:"password"`
@ -23,7 +23,7 @@ func (cfg *apiConfig) Login(w http.ResponseWriter, r *http.Request) {
err := decoder.Decode(&p) err := decoder.Decode(&p)
if err != nil { if err != nil {
log.Printf("Incorrect email or password") log.Printf("Incorrect email or password")
w.WriteHeader(401) w.WriteHeader(http.StatusUnauthorized)
return return
} }
@ -35,20 +35,20 @@ func (cfg *apiConfig) Login(w http.ResponseWriter, r *http.Request) {
err = auth.CheckPasswordHash(p.Password, loggedUser.HashedPassword) err = auth.CheckPasswordHash(p.Password, loggedUser.HashedPassword)
if err != nil { if err != nil {
log.Printf("Incorrect email or password") log.Printf("Incorrect email or password")
w.WriteHeader(401) w.WriteHeader(http.StatusUnauthorized)
return return
} }
newJwt, err := auth.MakeJWT(loggedUser.ID, cfg.JWT) newJwt, err := auth.MakeJWT(loggedUser.ID, cfg.JWT)
if err != nil { if err != nil {
log.Printf("Error creating JWT: %s", newJwt) log.Printf("Error creating JWT: %v", newJwt)
w.WriteHeader(500) w.WriteHeader(http.StatusInternalServerError)
return return
} }
newRefreshToken, err := auth.MakeRefreshToken() newRefreshToken, err := auth.MakeRefreshToken()
if err != nil { if err != nil {
log.Printf("Error creating refresh token: %v", err) log.Printf("Error creating refresh token: %v", err)
w.WriteHeader(500) w.WriteHeader(http.StatusInternalServerError)
return return
} }
@ -59,8 +59,8 @@ func (cfg *apiConfig) Login(w http.ResponseWriter, r *http.Request) {
} }
_, err = cfg.DB.CreateRefreshToken(r.Context(), refreshTokenParams) _, err = cfg.DB.CreateRefreshToken(r.Context(), refreshTokenParams)
if err != nil { if err != nil {
log.Printf("Error adding refresh token to db: %s", err) log.Printf("Error adding refresh token to db: %v", err)
w.WriteHeader(500) w.WriteHeader(http.StatusInternalServerError)
return return
} }
type loginResponse struct { type loginResponse struct {
@ -72,8 +72,7 @@ func (cfg *apiConfig) Login(w http.ResponseWriter, r *http.Request) {
RefreshToken string `json:"refresh_token"` RefreshToken string `json:"refresh_token"`
ChirpyRed bool `json:"is_chirpy_red"` ChirpyRed bool `json:"is_chirpy_red"`
} }
JsonResponse(w, http.StatusOK, loginResponse{
data, err := json.Marshal(loginResponse{
ID: loggedUser.ID, ID: loggedUser.ID,
CreatedAt: loggedUser.CreatedAt, CreatedAt: loggedUser.CreatedAt,
UpdatedAt: loggedUser.UpdatedAt, UpdatedAt: loggedUser.UpdatedAt,
@ -82,12 +81,4 @@ func (cfg *apiConfig) Login(w http.ResponseWriter, r *http.Request) {
RefreshToken: newRefreshToken, RefreshToken: newRefreshToken,
ChirpyRed: loggedUser.IsChirpyRed, 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)
} }

View File

@ -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 := `
<html>
<body>
<h1>Welcome, Chirpy Admin</h1>
<p>Chirpy has been visited %d times!</p>
</body>
</html>`
w.Write([]byte(fmt.Sprintf(template, hits)))
}

View File

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

22
internal/handler/reset.go Normal file
View File

@ -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)
}

View File

@ -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)
}

View File

@ -1,62 +1,52 @@
package main package handler
import ( import (
"encoding/json"
"log" "log"
"net/http" "net/http"
"github.com/finchrelia/chirpy-server/internal/auth" "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) token, err := auth.GetBearerToken(r.Header)
if err != nil { if err != nil {
log.Printf("Error extracting token: %s", err) log.Printf("Error extracting token: %v", err)
w.WriteHeader(401) w.WriteHeader(http.StatusUnauthorized)
return return
} }
dbUser, err := cfg.DB.GetUserFromRefreshToken(r.Context(), token) dbUser, err := cfg.DB.GetUserFromRefreshToken(r.Context(), token)
if err != nil { if err != nil {
log.Printf("Error getting user: %v", err) log.Printf("Error getting user: %v", err)
w.WriteHeader(401) w.WriteHeader(http.StatusUnauthorized)
return return
} }
newToken, err := auth.MakeJWT(dbUser, cfg.JWT) newToken, err := auth.MakeJWT(dbUser, cfg.JWT)
if err != nil { if err != nil {
log.Printf("Error creating new JWT: %v", err) log.Printf("Error creating new JWT: %v", err)
w.WriteHeader(500) w.WriteHeader(http.StatusInternalServerError)
return return
} }
type tokenResponse struct { type tokenResponse struct {
AccessToken string `json:"token"` AccessToken string `json:"token"`
} }
JsonResponse(w, http.StatusOK, tokenResponse{
data, err := json.Marshal(tokenResponse{
AccessToken: newToken, 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) token, err := auth.GetBearerToken(r.Header)
if err != nil { if err != nil {
log.Printf("Error extracting token: %s", err) log.Printf("Error extracting token: %v", err)
w.WriteHeader(401) w.WriteHeader(http.StatusUnauthorized)
return return
} }
err = cfg.DB.RevokeRefreshToken(r.Context(), token) err = cfg.DB.RevokeRefreshToken(r.Context(), token)
if err != nil { if err != nil {
log.Printf("Error revoking token in database: %v", err) log.Printf("Error revoking token in database: %v", err)
w.WriteHeader(500) w.WriteHeader(http.StatusInternalServerError)
return return
} }
w.WriteHeader(204) w.WriteHeader(http.StatusNoContent)
} }

View File

@ -1,4 +1,4 @@
package main package handler
import ( import (
"encoding/json" "encoding/json"
@ -20,7 +20,7 @@ type User struct {
ChirpyRed bool `json:"is_chirpy_red"` 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 { type parameters struct {
Email string `json:"email"` Email string `json:"email"`
Password string `json:"password"` Password string `json:"password"`
@ -30,55 +30,46 @@ func (cfg *apiConfig) createUsers(w http.ResponseWriter, r *http.Request) {
params := parameters{} params := parameters{}
err := decoder.Decode(&params) err := decoder.Decode(&params)
if err != nil { if err != nil {
log.Printf("Error decoding parameters: %s", err) log.Printf("Error decoding parameters: %v", err)
w.WriteHeader(500) w.WriteHeader(http.StatusInternalServerError)
return return
} }
defer r.Body.Close() defer r.Body.Close()
hashedPassword, err := auth.HashPassword(params.Password) hashedPassword, err := auth.HashPassword(params.Password)
if err != nil { 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{ newDBUser, err := cfg.DB.CreateUser(r.Context(), database.CreateUserParams{
Email: params.Email, Email: params.Email,
HashedPassword: hashedPassword, HashedPassword: hashedPassword,
}) })
if err != nil { if err != nil {
log.Printf("Error creating user %s: %s", params.Email, err) log.Printf("Error creating user %s: %v", params.Email, err)
w.WriteHeader(500) w.WriteHeader(http.StatusInternalServerError)
return return
} }
newId := newDBUser.ID newId := newDBUser.ID
newUser := User{ JsonResponse(w, http.StatusCreated, User{
ID: newId, ID: newId,
CreatedAt: newDBUser.CreatedAt, CreatedAt: newDBUser.CreatedAt,
UpdatedAt: newDBUser.UpdatedAt, UpdatedAt: newDBUser.UpdatedAt,
Email: newDBUser.Email, Email: newDBUser.Email,
ChirpyRed: newDBUser.IsChirpyRed, 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) token, err := auth.GetBearerToken(r.Header)
if err != nil { if err != nil {
log.Printf("Error extracting token: %s", err) log.Printf("Error extracting token: %v", err)
w.WriteHeader(401) w.WriteHeader(http.StatusUnauthorized)
return return
} }
userId, err := auth.ValidateJWT(token, cfg.JWT) userId, err := auth.ValidateJWT(token, cfg.JWT)
if err != nil { if err != nil {
log.Printf("Invalid JWT: %s", err) log.Printf("Invalid JWT: %v", err)
w.WriteHeader(401) w.WriteHeader(http.StatusUnauthorized)
return return
} }
@ -91,16 +82,16 @@ func (cfg *apiConfig) updateUsers(w http.ResponseWriter, r *http.Request) {
params := parameters{} params := parameters{}
err = decoder.Decode(&params) err = decoder.Decode(&params)
if err != nil { if err != nil {
log.Printf("Error decoding parameters: %s", err) log.Printf("Error decoding parameters: %v", err)
w.WriteHeader(500) w.WriteHeader(http.StatusInternalServerError)
return return
} }
defer r.Body.Close() defer r.Body.Close()
hashedPassword, err := auth.HashPassword(params.Password) hashedPassword, err := auth.HashPassword(params.Password)
if err != nil { if err != nil {
log.Printf("Error hashing password: %s", err) log.Printf("Error hashing password: %v", err)
w.WriteHeader(500) w.WriteHeader(http.StatusInternalServerError)
return return
} }
credentialsQueryParams := database.UpdateUserCredentialsParams{ 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) updatedCredentials, err := cfg.DB.UpdateUserCredentials(r.Context(), credentialsQueryParams)
if err != nil { if err != nil {
log.Printf("Error updating user credentials: %v", err) log.Printf("Error updating user credentials: %v", err)
w.WriteHeader(500) w.WriteHeader(http.StatusInternalServerError)
return return
} }
data, err := json.Marshal(User{ JsonResponse(w, http.StatusOK, User{
ID: userId, ID: userId,
CreatedAt: updatedCredentials.CreatedAt, CreatedAt: updatedCredentials.CreatedAt,
UpdatedAt: updatedCredentials.UpdatedAt, UpdatedAt: updatedCredentials.UpdatedAt,
Email: updatedCredentials.Email, Email: updatedCredentials.Email,
ChirpyRed: updatedCredentials.IsChirpyRed, 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) _, err := auth.GetAPIKey(r.Header)
if err != nil { if err != nil {
log.Printf("Error extracting apiKey: %s", err) log.Printf("Error extracting apiKey: %v", err)
w.WriteHeader(401) w.WriteHeader(http.StatusUnauthorized)
return return
} }
type parameters struct { type parameters struct {
@ -146,28 +129,28 @@ func (cfg *apiConfig) subscribeUser(w http.ResponseWriter, r *http.Request) {
params := parameters{} params := parameters{}
err = decoder.Decode(&params) err = decoder.Decode(&params)
if err != nil { if err != nil {
log.Printf("Error decoding parameters: %s", err) log.Printf("Error decoding parameters: %v", err)
w.WriteHeader(400) w.WriteHeader(http.StatusBadRequest)
return return
} }
defer r.Body.Close() defer r.Body.Close()
if params.Event != "user.upgraded" { if params.Event != "user.upgraded" {
w.WriteHeader(204) w.WriteHeader(http.StatusNoContent)
return return
} }
paramsUserIdString := params.Data["user_id"] paramsUserIdString := params.Data["user_id"]
paramsUserId, err := uuid.Parse(paramsUserIdString) paramsUserId, err := uuid.Parse(paramsUserIdString)
if err != nil { if err != nil {
log.Printf("Specified user_id is not a valid UUID: %v", err) log.Printf("Specified user_id is not a valid UUID: %v", err)
w.WriteHeader(400) w.WriteHeader(http.StatusBadRequest)
return return
} }
err = cfg.DB.UpgradeUser(r.Context(), paramsUserId) err = cfg.DB.UpgradeUser(r.Context(), paramsUserId)
if err != nil { if err != nil {
log.Printf("No user matches user_id given: %v", err) log.Printf("No user matches user_id given: %v", err)
w.WriteHeader(404) w.WriteHeader(http.StatusNotFound)
return return
} }
w.WriteHeader(204) w.WriteHeader(http.StatusNoContent)
} }

View File

@ -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 := `
<html>
<body>
<h1>Welcome, Chirpy Admin</h1>
<p>Chirpy has been visited %d times!</p>
</body>
</html>`
fmt.Fprintf(w, template, hits)
}

View File

@ -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)
}
}

View File

@ -13,6 +13,10 @@ RETURNING *;
SELECT * FROM chirps SELECT * FROM chirps
ORDER BY created_at ASC; ORDER BY created_at ASC;
-- name: GetChirpsByUserid :many
SELECT * FROM chirps
WHERE chirps.user_id = $1;
-- name: GetChirp :one -- name: GetChirp :one
SELECT * FROM chirps SELECT * FROM chirps
WHERE chirps.id = $1; WHERE chirps.id = $1;

View File

@ -9,3 +9,16 @@ VALUES (
NULL NULL
) )
RETURNING *; 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;

View File

@ -1,6 +0,0 @@
-- name: RevokeRefreshToken :exec
UPDATE refresh_tokens
SET
revoked_at = NOW(),
updated_at = NOW()
WHERE token = $1;

View File

@ -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;

View File

@ -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;

View File

@ -21,3 +21,11 @@ WHERE users.email = $1;
UPDATE users UPDATE users
SET is_chirpy_red = true SET is_chirpy_red = true
WHERE id = $1; 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;