From e72fd2ee8648e9a17721fa9f75c872758db3f89c Mon Sep 17 00:00:00 2001 From: syrell Date: Thu, 24 Oct 2024 22:40:26 +0200 Subject: [PATCH] feat: Added chapter 6 first auth attempt --- go.mod | 1 + go.sum | 2 ++ internal/auth/auth.go | 12 ++++++++ internal/database/models.go | 9 +++--- internal/database/users.sql.go | 33 ++++++++++++++++++---- login.go | 51 ++++++++++++++++++++++++++++++++++ main.go | 1 + sql/queries/users.sql | 6 +++- sql/schema/003_users_auth.sql | 7 +++++ users.go | 20 +++++++++---- 10 files changed, 125 insertions(+), 17 deletions(-) create mode 100644 internal/auth/auth.go create mode 100644 login.go create mode 100644 sql/schema/003_users_auth.sql diff --git a/go.mod b/go.mod index d0428d8..b819bfc 100644 --- a/go.mod +++ b/go.mod @@ -6,4 +6,5 @@ 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 + golang.org/x/crypto v0.28.0 // indirect ) diff --git a/go.sum b/go.sum index 63c8b5f..14aad1d 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.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= diff --git a/internal/auth/auth.go b/internal/auth/auth.go new file mode 100644 index 0000000..44e8eb2 --- /dev/null +++ b/internal/auth/auth.go @@ -0,0 +1,12 @@ +package auth + +import "golang.org/x/crypto/bcrypt" + +func HashPassword(password string) (string, error) { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 10) + return string(hashedPassword), err +} + +func CheckPasswordHash(password, hash string) error { + return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) +} diff --git a/internal/database/models.go b/internal/database/models.go index b7a9ca1..d77629c 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/internal/database/users.sql.go b/internal/database/users.sql.go index e9f3c23..e5b90eb 100644 --- a/internal/database/users.sql.go +++ b/internal/database/users.sql.go @@ -10,31 +10,33 @@ import ( ) const createUser = `-- name: CreateUser :one -INSERT INTO users (id, created_at, updated_at, email) +INSERT INTO users (id, created_at, updated_at, email, hashed_password) VALUES ( gen_random_uuid(), NOW(), NOW(), - $1 + $1, + $2 ) -RETURNING id, created_at, updated_at, email +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) +func (q *Queries) CreateUser(ctx context.Context, email string, hashed_password string) (User, error) { + row := q.db.QueryRowContext(ctx, createUser, email, hashed_password) var i User err := row.Scan( &i.ID, &i.CreatedAt, &i.UpdatedAt, &i.Email, + &i.HashedPassword, ) return i, err } const deleteUser = `-- name: DeleteUser :one DELETE FROM users -RETURNING id, created_at, updated_at, email +RETURNING id, created_at, updated_at, email, hashed_password ` func (q *Queries) DeleteUser(ctx context.Context) (User, error) { @@ -45,6 +47,25 @@ func (q *Queries) DeleteUser(ctx context.Context) (User, error) { &i.CreatedAt, &i.UpdatedAt, &i.Email, + &i.HashedPassword, + ) + return i, err +} + +const getUserByEmail = `-- name: GetUserByEmail :one +SELECT id, created_at, updated_at, email, hashed_password FROM users +WHERE users.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/login.go b/login.go new file mode 100644 index 0000000..c459d41 --- /dev/null +++ b/login.go @@ -0,0 +1,51 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" + + "github.com/finchrelia/chirpy-server/internal/auth" +) + +func (cfg *apiConfig) Login(w http.ResponseWriter, r *http.Request) { + type params struct { + Email string `json:"email"` + Password string `json:"password"` + } + + decoder := json.NewDecoder(r.Body) + p := params{} + err := decoder.Decode(&p) + if err != nil { + log.Printf("Incorrect email or password") + w.WriteHeader(401) + return + } + loggedUser, err := cfg.DB.GetUserByEmail(r.Context(), p.Email) + if err != nil { + log.Printf("Error retrieving user: %s", err) + } + + err = auth.CheckPasswordHash(p.Password, loggedUser.HashedPassword) + if err != nil { + log.Printf("Incorrect email or password") + w.WriteHeader(401) + return + } + + data, err := json.Marshal(User{ + ID: loggedUser.ID, + CreatedAt: loggedUser.CreatedAt, + UpdatedAt: loggedUser.UpdatedAt, + Email: loggedUser.Email, + }) + 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/main.go b/main.go index f204ccd..93a1225 100644 --- a/main.go +++ b/main.go @@ -51,6 +51,7 @@ func main() { mux.HandleFunc("POST /api/chirps", apiCfg.chirpsCreate) mux.HandleFunc("POST /api/users", apiCfg.createUsers) mux.HandleFunc("GET /api/chirps/{chirpID}", apiCfg.getChirp) + mux.HandleFunc("POST /api/login", apiCfg.Login) server := &http.Server{ Addr: ":8080", diff --git a/sql/queries/users.sql b/sql/queries/users.sql index b2d478c..b520299 100644 --- a/sql/queries/users.sql +++ b/sql/queries/users.sql @@ -10,4 +10,8 @@ RETURNING *; -- name: DeleteUser :one DELETE FROM users -RETURNING *; \ No newline at end of file +RETURNING *; + +-- name: GetUserByEmail :one +SELECT * FROM users +WHERE users.email = $1; \ No newline at end of file diff --git a/sql/schema/003_users_auth.sql b/sql/schema/003_users_auth.sql new file mode 100644 index 0000000..48ad4ef --- /dev/null +++ b/sql/schema/003_users_auth.sql @@ -0,0 +1,7 @@ +-- +goose Up +ALTER TABLE users +ADD COLUMN hashed_password TEXT DEFAULT 'unset' NOT NULL; + +-- +goose Down +ALTER TABLE users +DROP COLUMN hashed_password; \ No newline at end of file diff --git a/users.go b/users.go index 2d8199d..4f1e7ee 100644 --- a/users.go +++ b/users.go @@ -6,19 +6,22 @@ import ( "net/http" "time" + "github.com/finchrelia/chirpy-server/internal/auth" "github.com/google/uuid" ) type User struct { - ID uuid.UUID `json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Email string `json:"email"` + ID uuid.UUID `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Email string `json:"email"` + HashedPassword string `json:"-"` } func (cfg *apiConfig) createUsers(w http.ResponseWriter, r *http.Request) { type parameters struct { - Email string `json:"email"` + Email string `json:"email"` + Password string `json:"password"` } decoder := json.NewDecoder(r.Body) @@ -30,7 +33,12 @@ func (cfg *apiConfig) createUsers(w http.ResponseWriter, r *http.Request) { return } defer r.Body.Close() - newDBUser, err := cfg.DB.CreateUser(r.Context(), params.Email) + + hashedPassword, err := auth.HashPassword(params.Password) + if err != nil { + log.Printf("Error hashing password: %s", err) + } + newDBUser, err := cfg.DB.CreateUser(r.Context(), params.Email, hashedPassword) if err != nil { log.Printf("Error creating user %s: %s", params.Email, err) w.WriteHeader(500)