feat: Add other tasks from chapter 6
This commit is contained in:
		
							
								
								
									
										25
									
								
								chirp.go
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								chirp.go
									
									
									
									
									
								
							@@ -9,6 +9,7 @@ import (
 | 
				
			|||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/finchrelia/chirpy-server/internal/auth"
 | 
				
			||||||
	"github.com/finchrelia/chirpy-server/internal/database"
 | 
						"github.com/finchrelia/chirpy-server/internal/database"
 | 
				
			||||||
	"github.com/google/uuid"
 | 
						"github.com/google/uuid"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@@ -23,16 +24,23 @@ type Chirp struct {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func (cfg *apiConfig) chirpsCreate(w http.ResponseWriter, r *http.Request) {
 | 
					func (cfg *apiConfig) chirpsCreate(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
	type parameters struct {
 | 
						type parameters struct {
 | 
				
			||||||
		Content string    `json:"body"`
 | 
							Content string `json:"body"`
 | 
				
			||||||
		UserID  uuid.UUID `json:"user_id"`
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	type returnVals struct {
 | 
						token, err := auth.GetBearerToken(r.Header)
 | 
				
			||||||
		Data string `json:"cleaned_body"`
 | 
						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)
 | 
						decoder := json.NewDecoder(r.Body)
 | 
				
			||||||
	params := parameters{}
 | 
						params := parameters{}
 | 
				
			||||||
	err := decoder.Decode(¶ms)
 | 
						err = decoder.Decode(¶ms)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Printf("Error decoding parameters: %s", err)
 | 
							log.Printf("Error decoding parameters: %s", err)
 | 
				
			||||||
		w.WriteHeader(500)
 | 
							w.WriteHeader(500)
 | 
				
			||||||
@@ -56,12 +64,9 @@ func (cfg *apiConfig) chirpsCreate(w http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
		w.Write(dat)
 | 
							w.Write(dat)
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		cleanedData := cleanText(params.Content)
 | 
							cleanedData := cleanText(params.Content)
 | 
				
			||||||
		// respBody := returnVals{
 | 
					 | 
				
			||||||
		// 	Data: cleanedData,
 | 
					 | 
				
			||||||
		// }
 | 
					 | 
				
			||||||
		chirp, err := cfg.DB.CreateChirp(r.Context(), database.CreateChirpParams{
 | 
							chirp, err := cfg.DB.CreateChirp(r.Context(), database.CreateChirpParams{
 | 
				
			||||||
			Body:   cleanedData,
 | 
								Body:   cleanedData,
 | 
				
			||||||
			UserID: params.UserID,
 | 
								UserID: userId,
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			log.Printf("Error creating chirp: %s", err)
 | 
								log.Printf("Error creating chirp: %s", err)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								go.mod
									
									
									
									
									
								
							@@ -3,6 +3,7 @@ module github.com/finchrelia/chirpy-server
 | 
				
			|||||||
go 1.22.5
 | 
					go 1.22.5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require (
 | 
					require (
 | 
				
			||||||
 | 
						github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
 | 
				
			||||||
	github.com/google/uuid v1.6.0 // indirect
 | 
						github.com/google/uuid v1.6.0 // indirect
 | 
				
			||||||
	github.com/joho/godotenv v1.5.1 // indirect
 | 
						github.com/joho/godotenv v1.5.1 // indirect
 | 
				
			||||||
	github.com/lib/pq v1.10.9 // indirect
 | 
						github.com/lib/pq v1.10.9 // indirect
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.sum
									
									
									
									
									
								
							@@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
 | 
				
			||||||
 | 
					github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 | 
				
			||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 | 
					github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 | 
				
			||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 | 
					github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 | 
				
			||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
 | 
					github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,17 @@
 | 
				
			|||||||
package auth
 | 
					package auth
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import "golang.org/x/crypto/bcrypt"
 | 
					import (
 | 
				
			||||||
 | 
						"crypto/rand"
 | 
				
			||||||
 | 
						"encoding/hex"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/golang-jwt/jwt/v5"
 | 
				
			||||||
 | 
						"github.com/google/uuid"
 | 
				
			||||||
 | 
						"golang.org/x/crypto/bcrypt"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func HashPassword(password string) (string, error) {
 | 
					func HashPassword(password string) (string, error) {
 | 
				
			||||||
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 10)
 | 
						hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 10)
 | 
				
			||||||
@@ -10,3 +21,63 @@ func HashPassword(password string) (string, error) {
 | 
				
			|||||||
func CheckPasswordHash(password, hash string) error {
 | 
					func CheckPasswordHash(password, hash string) error {
 | 
				
			||||||
	return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
 | 
						return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func MakeJWT(userID uuid.UUID, tokenSecret string) (string, error) {
 | 
				
			||||||
 | 
						newToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{
 | 
				
			||||||
 | 
							Issuer:    "chirpy",
 | 
				
			||||||
 | 
							IssuedAt:  jwt.NewNumericDate(time.Now()),
 | 
				
			||||||
 | 
							ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
 | 
				
			||||||
 | 
							Subject:   userID.String(),
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						token, err := newToken.SignedString([]byte(tokenSecret))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return token, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func ValidateJWT(tokenString, tokenSecret string) (uuid.UUID, error) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						token, err := jwt.ParseWithClaims(tokenString, &jwt.RegisteredClaims{}, func(t *jwt.Token) (interface{}, error) {
 | 
				
			||||||
 | 
							return []byte(tokenSecret), nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return uuid.UUID{}, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if !token.Valid {
 | 
				
			||||||
 | 
							return uuid.UUID{}, errors.New("token has expired")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						userIDString, err := token.Claims.GetSubject()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return uuid.UUID{}, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						userID, err := uuid.Parse(userIDString)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return uuid.UUID{}, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return userID, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func GetBearerToken(headers http.Header) (string, error) {
 | 
				
			||||||
 | 
						authHeader := headers.Get("Authorization")
 | 
				
			||||||
 | 
						if authHeader == "" {
 | 
				
			||||||
 | 
							return "", errors.New("authorization header is not set")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if !strings.HasPrefix(authHeader, "Bearer ") {
 | 
				
			||||||
 | 
							return "", errors.New("incorrect authorization type, must be of type Bearer")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						bearerToken := strings.TrimPrefix(authHeader, "Bearer ")
 | 
				
			||||||
 | 
						return strings.TrimSpace(bearerToken), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func MakeRefreshToken() (string, error) {
 | 
				
			||||||
 | 
						buffer := make([]byte, 32)
 | 
				
			||||||
 | 
						_, err := rand.Read(buffer)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						hexData := hex.EncodeToString(buffer)
 | 
				
			||||||
 | 
						return hexData, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,7 @@
 | 
				
			|||||||
package database
 | 
					package database
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"database/sql"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/google/uuid"
 | 
						"github.com/google/uuid"
 | 
				
			||||||
@@ -18,6 +19,15 @@ type Chirp struct {
 | 
				
			|||||||
	UserID    uuid.UUID
 | 
						UserID    uuid.UUID
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type RefreshToken struct {
 | 
				
			||||||
 | 
						Token     string
 | 
				
			||||||
 | 
						CreatedAt time.Time
 | 
				
			||||||
 | 
						UpdatedAt time.Time
 | 
				
			||||||
 | 
						UserID    uuid.UUID
 | 
				
			||||||
 | 
						ExpiresAt sql.NullTime
 | 
				
			||||||
 | 
						RevokedAt sql.NullTime
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type User struct {
 | 
					type User struct {
 | 
				
			||||||
	ID             uuid.UUID
 | 
						ID             uuid.UUID
 | 
				
			||||||
	CreatedAt      time.Time
 | 
						CreatedAt      time.Time
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										46
									
								
								internal/database/refresh_token.sql.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								internal/database/refresh_token.sql.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					// Code generated by sqlc. DO NOT EDIT.
 | 
				
			||||||
 | 
					// versions:
 | 
				
			||||||
 | 
					//   sqlc v1.27.0
 | 
				
			||||||
 | 
					// source: refresh_token.sql
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package database
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"database/sql"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/google/uuid"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const createRefreshToken = `-- name: CreateRefreshToken :one
 | 
				
			||||||
 | 
					INSERT INTO refresh_tokens (token, created_at, updated_at, user_id, expires_at, revoked_at)
 | 
				
			||||||
 | 
					VALUES (
 | 
				
			||||||
 | 
					    $1,
 | 
				
			||||||
 | 
					    NOW(),
 | 
				
			||||||
 | 
					    NOW(),
 | 
				
			||||||
 | 
					    $2,
 | 
				
			||||||
 | 
					    $3,
 | 
				
			||||||
 | 
					    NULL
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					RETURNING token, created_at, updated_at, user_id, expires_at, revoked_at
 | 
				
			||||||
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type CreateRefreshTokenParams struct {
 | 
				
			||||||
 | 
						Token     string
 | 
				
			||||||
 | 
						UserID    uuid.UUID
 | 
				
			||||||
 | 
						ExpiresAt sql.NullTime
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (q *Queries) CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) (RefreshToken, error) {
 | 
				
			||||||
 | 
						row := q.db.QueryRowContext(ctx, createRefreshToken, arg.Token, arg.UserID, arg.ExpiresAt)
 | 
				
			||||||
 | 
						var i RefreshToken
 | 
				
			||||||
 | 
						err := row.Scan(
 | 
				
			||||||
 | 
							&i.Token,
 | 
				
			||||||
 | 
							&i.CreatedAt,
 | 
				
			||||||
 | 
							&i.UpdatedAt,
 | 
				
			||||||
 | 
							&i.UserID,
 | 
				
			||||||
 | 
							&i.ExpiresAt,
 | 
				
			||||||
 | 
							&i.RevokedAt,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						return i, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										23
									
								
								internal/database/update_token.sql.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								internal/database/update_token.sql.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					// 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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										26
									
								
								internal/database/user_from_token.sql.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								internal/database/user_from_token.sql.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					// 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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -16,13 +16,18 @@ VALUES (
 | 
				
			|||||||
    NOW(),
 | 
					    NOW(),
 | 
				
			||||||
    NOW(),
 | 
					    NOW(),
 | 
				
			||||||
    $1,
 | 
					    $1,
 | 
				
			||||||
	$2
 | 
					    $2
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
RETURNING id, created_at, updated_at, email, hashed_password
 | 
					RETURNING id, created_at, updated_at, email, hashed_password
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (q *Queries) CreateUser(ctx context.Context, email string, hashed_password string) (User, error) {
 | 
					type CreateUserParams struct {
 | 
				
			||||||
	row := q.db.QueryRowContext(ctx, createUser, email, hashed_password)
 | 
						Email          string
 | 
				
			||||||
 | 
						HashedPassword string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
 | 
				
			||||||
 | 
						row := q.db.QueryRowContext(ctx, createUser, arg.Email, arg.HashedPassword)
 | 
				
			||||||
	var i User
 | 
						var i User
 | 
				
			||||||
	err := row.Scan(
 | 
						err := row.Scan(
 | 
				
			||||||
		&i.ID,
 | 
							&i.ID,
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										52
									
								
								login.go
									
									
									
									
									
								
							
							
						
						
									
										52
									
								
								login.go
									
									
									
									
									
								
							@@ -1,11 +1,15 @@
 | 
				
			|||||||
package main
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"database/sql"
 | 
				
			||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/finchrelia/chirpy-server/internal/auth"
 | 
						"github.com/finchrelia/chirpy-server/internal/auth"
 | 
				
			||||||
 | 
						"github.com/finchrelia/chirpy-server/internal/database"
 | 
				
			||||||
 | 
						"github.com/google/uuid"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (cfg *apiConfig) Login(w http.ResponseWriter, r *http.Request) {
 | 
					func (cfg *apiConfig) Login(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
@@ -22,9 +26,10 @@ func (cfg *apiConfig) Login(w http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
		w.WriteHeader(401)
 | 
							w.WriteHeader(401)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	loggedUser, err := cfg.DB.GetUserByEmail(r.Context(), p.Email)
 | 
						loggedUser, err := cfg.DB.GetUserByEmail(r.Context(), p.Email)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Printf("Error retrieving user: %s", err)
 | 
							log.Printf("Error retrieving user: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	err = auth.CheckPasswordHash(p.Password, loggedUser.HashedPassword)
 | 
						err = auth.CheckPasswordHash(p.Password, loggedUser.HashedPassword)
 | 
				
			||||||
@@ -34,11 +39,46 @@ func (cfg *apiConfig) Login(w http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	data, err := json.Marshal(User{
 | 
						newJwt, err := auth.MakeJWT(loggedUser.ID, cfg.JWT)
 | 
				
			||||||
		ID:        loggedUser.ID,
 | 
						if err != nil {
 | 
				
			||||||
		CreatedAt: loggedUser.CreatedAt,
 | 
							log.Printf("Error creating JWT: %s", newJwt)
 | 
				
			||||||
		UpdatedAt: loggedUser.UpdatedAt,
 | 
							w.WriteHeader(500)
 | 
				
			||||||
		Email:     loggedUser.Email,
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						newRefreshToken, err := auth.MakeRefreshToken()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Printf("Error creating refresh token: %v", err)
 | 
				
			||||||
 | 
							w.WriteHeader(500)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						refreshTokenParams := database.CreateRefreshTokenParams{
 | 
				
			||||||
 | 
							Token:     newRefreshToken,
 | 
				
			||||||
 | 
							UserID:    loggedUser.ID,
 | 
				
			||||||
 | 
							ExpiresAt: sql.NullTime{Time: time.Now().AddDate(0, 0, 60), Valid: true},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						_, err = cfg.DB.CreateRefreshToken(r.Context(), refreshTokenParams)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Printf("Error adding refresh token to db: %s", err)
 | 
				
			||||||
 | 
							w.WriteHeader(500)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						type loginResponse struct {
 | 
				
			||||||
 | 
							ID           uuid.UUID `json:"id"`
 | 
				
			||||||
 | 
							CreatedAt    time.Time `json:"created_at"`
 | 
				
			||||||
 | 
							UpdatedAt    time.Time `json:"updated_at"`
 | 
				
			||||||
 | 
							Email        string    `json:"email"`
 | 
				
			||||||
 | 
							AccessToken  string    `json:"token"`
 | 
				
			||||||
 | 
							RefreshToken string    `json:"refresh_token"`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						data, err := json.Marshal(loginResponse{
 | 
				
			||||||
 | 
							ID:           loggedUser.ID,
 | 
				
			||||||
 | 
							CreatedAt:    loggedUser.CreatedAt,
 | 
				
			||||||
 | 
							UpdatedAt:    loggedUser.UpdatedAt,
 | 
				
			||||||
 | 
							Email:        loggedUser.Email,
 | 
				
			||||||
 | 
							AccessToken:  newJwt,
 | 
				
			||||||
 | 
							RefreshToken: newRefreshToken,
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Printf("Error marshalling JSON: %s", err)
 | 
							log.Printf("Error marshalling JSON: %s", err)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										8
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								main.go
									
									
									
									
									
								
							@@ -16,6 +16,7 @@ type apiConfig struct {
 | 
				
			|||||||
	fileserverHits atomic.Int32
 | 
						fileserverHits atomic.Int32
 | 
				
			||||||
	DB             *database.Queries
 | 
						DB             *database.Queries
 | 
				
			||||||
	Platform       string
 | 
						Platform       string
 | 
				
			||||||
 | 
						JWT            string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func main() {
 | 
					func main() {
 | 
				
			||||||
@@ -28,6 +29,10 @@ func main() {
 | 
				
			|||||||
	if platform == "" {
 | 
						if platform == "" {
 | 
				
			||||||
		log.Fatalf("Empty PLATFORM env var!")
 | 
							log.Fatalf("Empty PLATFORM env var!")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						jwtSecret := os.Getenv("JWT_SECRET")
 | 
				
			||||||
 | 
						if jwtSecret == "" {
 | 
				
			||||||
 | 
							log.Fatalf("Empty JWT_SECRET env var!")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	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: %s", err)
 | 
				
			||||||
@@ -36,6 +41,7 @@ func main() {
 | 
				
			|||||||
		fileserverHits: atomic.Int32{},
 | 
							fileserverHits: atomic.Int32{},
 | 
				
			||||||
		DB:             database.New(db),
 | 
							DB:             database.New(db),
 | 
				
			||||||
		Platform:       platform,
 | 
							Platform:       platform,
 | 
				
			||||||
 | 
							JWT:            jwtSecret,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	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("."))))
 | 
				
			||||||
@@ -52,6 +58,8 @@ func main() {
 | 
				
			|||||||
	mux.HandleFunc("POST /api/users", apiCfg.createUsers)
 | 
						mux.HandleFunc("POST /api/users", apiCfg.createUsers)
 | 
				
			||||||
	mux.HandleFunc("GET /api/chirps/{chirpID}", apiCfg.getChirp)
 | 
						mux.HandleFunc("GET /api/chirps/{chirpID}", apiCfg.getChirp)
 | 
				
			||||||
	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/revoke", apiCfg.RevokeToken)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	server := &http.Server{
 | 
						server := &http.Server{
 | 
				
			||||||
		Addr:    ":8080",
 | 
							Addr:    ":8080",
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										11
									
								
								sql/queries/refresh_token.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								sql/queries/refresh_token.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					-- name: CreateRefreshToken :one
 | 
				
			||||||
 | 
					INSERT INTO refresh_tokens (token, created_at, updated_at, user_id, expires_at, revoked_at)
 | 
				
			||||||
 | 
					VALUES (
 | 
				
			||||||
 | 
					    $1,
 | 
				
			||||||
 | 
					    NOW(),
 | 
				
			||||||
 | 
					    NOW(),
 | 
				
			||||||
 | 
					    $2,
 | 
				
			||||||
 | 
					    $3,
 | 
				
			||||||
 | 
					    NULL
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					RETURNING *;
 | 
				
			||||||
							
								
								
									
										6
									
								
								sql/queries/update_token.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								sql/queries/update_token.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					-- name: RevokeRefreshToken :exec
 | 
				
			||||||
 | 
					UPDATE refresh_tokens
 | 
				
			||||||
 | 
					SET 
 | 
				
			||||||
 | 
					    revoked_at = NOW(),
 | 
				
			||||||
 | 
					    updated_at = NOW()
 | 
				
			||||||
 | 
					WHERE token = $1;
 | 
				
			||||||
							
								
								
									
										5
									
								
								sql/queries/user_from_token.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								sql/queries/user_from_token.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					-- 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;
 | 
				
			||||||
@@ -1,10 +1,11 @@
 | 
				
			|||||||
-- name: CreateUser :one
 | 
					-- name: CreateUser :one
 | 
				
			||||||
INSERT INTO users (id, created_at, updated_at, email)
 | 
					INSERT INTO users (id, created_at, updated_at, email, hashed_password)
 | 
				
			||||||
VALUES (
 | 
					VALUES (
 | 
				
			||||||
    gen_random_uuid(),
 | 
					    gen_random_uuid(),
 | 
				
			||||||
    NOW(),
 | 
					    NOW(),
 | 
				
			||||||
    NOW(),
 | 
					    NOW(),
 | 
				
			||||||
    $1
 | 
					    $1,
 | 
				
			||||||
 | 
					    $2
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
RETURNING *;
 | 
					RETURNING *;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										12
									
								
								sql/schema/004_refresh_token.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								sql/schema/004_refresh_token.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					-- +goose Up
 | 
				
			||||||
 | 
					CREATE TABLE refresh_tokens (
 | 
				
			||||||
 | 
					    token TEXT PRIMARY KEY,
 | 
				
			||||||
 | 
					    created_at TIMESTAMP NOT NULL,
 | 
				
			||||||
 | 
					    updated_at TIMESTAMP NOT NULL,
 | 
				
			||||||
 | 
					    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
 | 
				
			||||||
 | 
					    expires_at TIMESTAMP,
 | 
				
			||||||
 | 
					    revoked_at TIMESTAMP
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- +goose Down
 | 
				
			||||||
 | 
					DROP TABLE refresh_tokens;
 | 
				
			||||||
							
								
								
									
										62
									
								
								token.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								token.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,62 @@
 | 
				
			|||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/finchrelia/chirpy-server/internal/auth"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						dbUser, err := cfg.DB.GetUserFromRefreshToken(r.Context(), token)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Printf("Error getting user: %v", err)
 | 
				
			||||||
 | 
							w.WriteHeader(401)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						newToken, err := auth.MakeJWT(dbUser, cfg.JWT)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Printf("Error creating new JWT: %v", err)
 | 
				
			||||||
 | 
							w.WriteHeader(500)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						type tokenResponse struct {
 | 
				
			||||||
 | 
							AccessToken string `json:"token"`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						data, err := json.Marshal(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) {
 | 
				
			||||||
 | 
						token, err := auth.GetBearerToken(r.Header)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Printf("Error extracting token: %s", err)
 | 
				
			||||||
 | 
							w.WriteHeader(401)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						err = cfg.DB.RevokeRefreshToken(r.Context(), token)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Printf("Error revoking token in database: %v", err)
 | 
				
			||||||
 | 
							w.WriteHeader(500)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						w.WriteHeader(204)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										6
									
								
								users.go
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								users.go
									
									
									
									
									
								
							@@ -7,6 +7,7 @@ import (
 | 
				
			|||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/finchrelia/chirpy-server/internal/auth"
 | 
						"github.com/finchrelia/chirpy-server/internal/auth"
 | 
				
			||||||
 | 
						"github.com/finchrelia/chirpy-server/internal/database"
 | 
				
			||||||
	"github.com/google/uuid"
 | 
						"github.com/google/uuid"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -38,7 +39,10 @@ func (cfg *apiConfig) createUsers(w http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Printf("Error hashing password: %s", err)
 | 
							log.Printf("Error hashing password: %s", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	newDBUser, err := cfg.DB.CreateUser(r.Context(), params.Email, hashedPassword)
 | 
						newDBUser, err := cfg.DB.CreateUser(r.Context(), database.CreateUserParams{
 | 
				
			||||||
 | 
							Email:          params.Email,
 | 
				
			||||||
 | 
							HashedPassword: hashedPassword,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Printf("Error creating user %s: %s", params.Email, err)
 | 
							log.Printf("Error creating user %s: %s", params.Email, err)
 | 
				
			||||||
		w.WriteHeader(500)
 | 
							w.WriteHeader(500)
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user