diff --git a/go.mod b/go.mod index 6961458..487420f 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,8 @@ module github.com/rdarius/go-http-server go 1.23.6 require ( - github.com/google/uuid v1.6.0 // indirect - github.com/joho/godotenv v1.5.1 // indirect - github.com/lib/pq v1.10.9 // indirect + github.com/google/uuid v1.6.0 + github.com/joho/godotenv v1.5.1 + github.com/lib/pq v1.10.9 + golang.org/x/crypto v0.36.0 ) diff --git a/go.sum b/go.sum index 63c8b5f..b4e2b34 100644 --- a/go.sum +++ b/go.sum @@ -4,3 +4,5 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= diff --git a/internal/auth/auth.go b/internal/auth/auth.go new file mode 100644 index 0000000..eb1cc08 --- /dev/null +++ b/internal/auth/auth.go @@ -0,0 +1,14 @@ +package auth + +import ( + "golang.org/x/crypto/bcrypt" +) + +func HashPassword(password string) (string, error) { + pass, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(pass), err +} + +func CheckPasswordHash(password, hash string) error { + return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) +} diff --git a/internal/database/CreateUser.sql.go b/internal/database/CreateUser.sql.go index d989368..c943a18 100644 --- a/internal/database/CreateUser.sql.go +++ b/internal/database/CreateUser.sql.go @@ -10,19 +10,25 @@ import ( ) const createUser = `-- name: CreateUser :one -INSERT INTO users (id, created_at, updated_at, email) -VALUES (gen_random_uuid(), NOW(), NOW(), $1) -RETURNING id, created_at, updated_at, email +INSERT INTO users (id, created_at, updated_at, email, hashed_password) +VALUES (gen_random_uuid(), NOW(), NOW(), $1, $2) +RETURNING id, created_at, updated_at, email, hashed_password ` -func (q *Queries) CreateUser(ctx context.Context, email string) (User, error) { - row := q.db.QueryRowContext(ctx, createUser, email) +type CreateUserParams struct { + 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 err := row.Scan( &i.ID, &i.CreatedAt, &i.UpdatedAt, &i.Email, + &i.HashedPassword, ) return i, err } diff --git a/internal/database/GetUserByEmail.sql.go b/internal/database/GetUserByEmail.sql.go new file mode 100644 index 0000000..2307798 --- /dev/null +++ b/internal/database/GetUserByEmail.sql.go @@ -0,0 +1,27 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 +// source: GetUserByEmail.sql + +package database + +import ( + "context" +) + +const getUserByEmail = `-- name: GetUserByEmail :one +SELECT id, created_at, updated_at, email, hashed_password FROM users WHERE email = $1 +` + +func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) { + row := q.db.QueryRowContext(ctx, getUserByEmail, email) + var i User + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Email, + &i.HashedPassword, + ) + return i, err +} diff --git a/internal/database/models.go b/internal/database/models.go index 768de98..e10c8df 100644 --- a/internal/database/models.go +++ b/internal/database/models.go @@ -19,8 +19,9 @@ type Chirp struct { } type User struct { - ID uuid.UUID - CreatedAt time.Time - UpdatedAt time.Time - Email string + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Email string + HashedPassword string } diff --git a/main.go b/main.go index a4f1c41..c0018ad 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "github.com/google/uuid" "github.com/joho/godotenv" _ "github.com/lib/pq" + "github.com/rdarius/go-http-server/internal/auth" "github.com/rdarius/go-http-server/internal/dataMaps" "github.com/rdarius/go-http-server/internal/database" "github.com/rdarius/go-http-server/internal/httpResponse" @@ -177,11 +178,11 @@ func postChirpsHandler(cfg *apiConfig) http.HandlerFunc { httpResponse.JSONHandler(w, http.StatusCreated, string(jsonData)) } } - -func postUsersHandler(cfg *apiConfig) http.HandlerFunc { +func loginHandler(cfg *apiConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { type parameters struct { - Email string `json:"email"` + Email string `json:"email"` + Password string `json:"password"` } params := parameters{} @@ -191,7 +192,57 @@ func postUsersHandler(cfg *apiConfig) http.HandlerFunc { return } - usr, err := cfg.db.CreateUser(context.Background(), params.Email) + user, err := cfg.db.GetUserByEmail(context.Background(), params.Email) + if err != nil { + httpResponse.JSONHandler(w, http.StatusNotFound, `{"error": "UserNotFound"}`) + return + } + + err = auth.CheckPasswordHash(params.Password, user.HashedPassword) + if err != nil { + httpResponse.JSONHandler(w, http.StatusUnauthorized, `{"error": "Incorrect email or password"}`) + return + } + + userJSON := dataMaps.User{ + ID: user.ID, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + Email: user.Email, + } + + jsonData, err := json.Marshal(userJSON) + if err != nil { + httpResponse.JSONHandler(w, http.StatusInternalServerError, fmt.Sprintf(`{"error": "%s"}`, err.Error())) + } + httpResponse.JSONHandler(w, http.StatusOK, string(jsonData)) + } +} + +func postUsersHandler(cfg *apiConfig) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + type parameters struct { + Email string `json:"email"` + Password string `json:"password"` + } + + params := parameters{} + err := json.NewDecoder(r.Body).Decode(¶ms) + if err != nil { + httpResponse.JSONHandler(w, http.StatusBadRequest, `{"error": "Failed to parse request body"}`) + return + } + + hash, err := auth.HashPassword(params.Password) + if err != nil { + httpResponse.JSONHandler(w, http.StatusInternalServerError, fmt.Sprintf(`{"error": "%s"}`, err.Error())) + } + + usr, err := cfg.db.CreateUser(context.Background(), database.CreateUserParams{ + Email: params.Email, + HashedPassword: hash, + }) + if err != nil { httpResponse.JSONHandler(w, http.StatusInternalServerError, fmt.Sprintf(`{"error": "Failed to create user", "message": "%s"}`, err.Error())) return @@ -252,6 +303,7 @@ func main() { mux.HandleFunc("GET /api/metrics", metricsHandler(apiCfg)) mux.HandleFunc("POST /api/reset", resetMetricsHandler(apiCfg)) mux.HandleFunc("POST /api/users", postUsersHandler(apiCfg)) + mux.HandleFunc("POST /api/login", loginHandler(apiCfg)) mux.HandleFunc("POST /api/chirps", postChirpsHandler(apiCfg)) mux.HandleFunc("GET /api/chirps", getAllChirpsHandler(apiCfg)) mux.HandleFunc("GET /api/chirps/{chirpID}", getChirpByIDHandler(apiCfg)) diff --git a/sql/queries/CreateUser.sql b/sql/queries/CreateUser.sql index 6378eb9..e19f0cb 100644 --- a/sql/queries/CreateUser.sql +++ b/sql/queries/CreateUser.sql @@ -1,4 +1,4 @@ -- name: CreateUser :one -INSERT INTO users (id, created_at, updated_at, email) -VALUES (gen_random_uuid(), NOW(), NOW(), $1) +INSERT INTO users (id, created_at, updated_at, email, hashed_password) +VALUES (gen_random_uuid(), NOW(), NOW(), $1, $2) RETURNING *; \ No newline at end of file diff --git a/sql/queries/GetUserByEmail.sql b/sql/queries/GetUserByEmail.sql new file mode 100644 index 0000000..2c4c14b --- /dev/null +++ b/sql/queries/GetUserByEmail.sql @@ -0,0 +1,2 @@ +-- name: GetUserByEmail :one +SELECT * FROM users WHERE email = $1; \ No newline at end of file diff --git a/sql/schema/003_app_hashed_password_to_users_table.sql b/sql/schema/003_app_hashed_password_to_users_table.sql new file mode 100644 index 0000000..f2fbb11 --- /dev/null +++ b/sql/schema/003_app_hashed_password_to_users_table.sql @@ -0,0 +1,5 @@ +-- +goose Up +ALTER TABLE users ADD COLUMN hashed_password TEXT NOT NULL DEFAULT 'unset'; + +-- +goose Down +ALTER TABLE users DROP COLUMN hashed_password; \ No newline at end of file