From a1e93b9bfb34167d6f94fe7cd75794158df54d6d Mon Sep 17 00:00:00 2001 From: Margarita Date: Fri, 1 Mar 2024 21:33:49 +0300 Subject: [PATCH] feat: add migrations and godoc comments --- Makefile | 7 ++- internal/models/auth.go | 12 ++---- internal/models/user.go | 11 +++-- internal/pkg/auth/delivery/http.go | 7 +++ internal/pkg/auth/interfaces.go | 41 +----------------- internal/pkg/auth/repo/postgres.go | 13 ++++-- internal/pkg/auth/usecase/usecase.go | 12 ++++-- internal/pkg/jwt/jwt.go | 54 +++++++++++++++++++++++ internal/pkg/middleware/auth.go | 62 +++------------------------ internal/pkg/utils/utils.go | 3 ++ migrations/000001_table_user.down.sql | 1 + migrations/000001_table_user.up.sql | 5 +++ 12 files changed, 111 insertions(+), 117 deletions(-) create mode 100644 internal/pkg/jwt/jwt.go create mode 100644 migrations/000001_table_user.down.sql create mode 100644 migrations/000001_table_user.up.sql diff --git a/Makefile b/Makefile index 1c9330b..294e514 100644 --- a/Makefile +++ b/Makefile @@ -11,5 +11,8 @@ lint: test: go test -race ./... -dev-compose: - docker compose -f "dev-docker-compose.yaml" up -d \ No newline at end of file +dev-compose-up: + docker compose -f "dev-docker-compose.yaml" up -d + +dev-compose-down: + docker compose -f "dev-docker-compose.yaml" down diff --git a/internal/models/auth.go b/internal/models/auth.go index e0b4d98..07f4398 100644 --- a/internal/models/auth.go +++ b/internal/models/auth.go @@ -1,13 +1,9 @@ package models -import "github.com/satori/uuid" - -type JwtPayload struct { - ID uuid.UUID - Login string -} - +// UserLoginData represents user information for login and signup type UserLoginData struct { - Login string `json:"login"` + // Login stands for users nickname + Login string `json:"login"` + // Password stands for users password Password string `json:"password"` } diff --git a/internal/models/user.go b/internal/models/user.go index 0600172..c46a368 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -4,9 +4,12 @@ import ( "github.com/satori/uuid" ) +// User represents user information type User struct { - ID uuid.UUID `json:"id"` - Login string `json:"login"` - Phone string `json:"phone"` - PasswordHash string `json:"-"` + // ID uniquely identifies the user. + ID uuid.UUID `json:"id"` + // Login is the username of the user. + Login string `json:"login"` + // PasswordHash is the hashed password of the user. + PasswordHash string `json:"-"` } diff --git a/internal/pkg/auth/delivery/http.go b/internal/pkg/auth/delivery/http.go index 4c83d9d..6a7e2b6 100644 --- a/internal/pkg/auth/delivery/http.go +++ b/internal/pkg/auth/delivery/http.go @@ -9,14 +9,18 @@ import ( "time" ) +// AuthHandler handles HTTP requests for user authentication. type AuthHandler struct { + // uc represents the usecase interface for authentication. uc auth.AuthUsecase } +// NewAuthHandler creates a new instance of AuthHandler. func NewAuthHandler(uc auth.AuthUsecase) *AuthHandler { return &AuthHandler{uc: uc} } +// SignUp handles the request for registering a new user. func (h *AuthHandler) SignUp(w http.ResponseWriter, r *http.Request) { data := models.UserLoginData{} @@ -38,6 +42,7 @@ func (h *AuthHandler) SignUp(w http.ResponseWriter, r *http.Request) { } } +// Login handles the request for user login. func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { data := models.UserLoginData{} if err := utils.ReadRequestData(r, &data); err != nil { @@ -57,6 +62,7 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { } } +// Logout handles the request for user logout. func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) { http.SetCookie(w, &http.Cookie{ Name: middleware.CookieName, @@ -65,6 +71,7 @@ func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) { }) } +// tokenCookie creates a new cookie for storing the authentication token. func tokenCookie(name, token string, exp time.Time) *http.Cookie { return &http.Cookie{ Name: name, diff --git a/internal/pkg/auth/interfaces.go b/internal/pkg/auth/interfaces.go index 4f317a9..495345d 100644 --- a/internal/pkg/auth/interfaces.go +++ b/internal/pkg/auth/interfaces.go @@ -6,52 +6,15 @@ import ( "time" ) +// AuthUsecase represents the usecase interface for authentication. type AuthUsecase interface { SignUp(context.Context, *models.UserLoginData) (*models.User, string, time.Time, error) Login(context.Context, *models.UserLoginData) (*models.User, string, time.Time, error) } +// AuthRepo represents the repository interface for authentication. type AuthRepo interface { CreateUser(ctx context.Context, newUser *models.User) error CheckUser(ctx context.Context, login string, passwordHash string) (*models.User, error) GetUserByLogin(cts context.Context, login string) (*models.User, error) } - -// func (r *User) Register(email, password string) (*User, error) { - -// var err error -// passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), 12) - -// if err != nil { -// return &User{}, err -// } - -// user := &User{ -// ID: generateID(), -// CreatedAt: time.Now(), -// Email: email, -// PasswordHash: passwordHash, -// } - -// user.JWTSession, _ = generateJWT(user.ID) -// _, err = r.Create(r) - -// return user, err -// } - -// func (r *User) GetByEmail (e,ail string) (*User, error) { - -// } - -// func (r *User) Create(user *User) error { -// _, err := -// } - -// func generateID() int64 { -// return 10 -// } - -// func generateJWT(id int64) ([]byte, error) { -// var err error -// return []byte("123"), err -// } diff --git a/internal/pkg/auth/repo/postgres.go b/internal/pkg/auth/repo/postgres.go index 141b877..670baee 100644 --- a/internal/pkg/auth/repo/postgres.go +++ b/internal/pkg/auth/repo/postgres.go @@ -7,38 +7,43 @@ import ( "errors" ) +// AuthRepo represents a repository for authentication. type AuthRepo struct { db *sql.DB } +// NewRepository creates a new instance of AuthRepo. func NewRepository(db *sql.DB) *AuthRepo { return &AuthRepo{db: db} } +// CreateUser creates a new user in the database. func (r *AuthRepo) CreateUser(ctx context.Context, user *models.User) error { - insert := `INSERT INTO users (id, login, phone, password_hash) VALUES ($1, $2, $3, $4)` + insert := `INSERT INTO users (id, login, password_hash) VALUES ($1, $2, $3, $4)` - if _, err := r.db.ExecContext(ctx, insert, user.ID, user.Login, user.Phone, user.PasswordHash); err != nil { + if _, err := r.db.ExecContext(ctx, insert, user.ID, user.Login, user.PasswordHash); err != nil { return err } return nil } +// GetUserByLogin retrieves a user from the database by their login. func (r *AuthRepo) GetUserByLogin(ctx context.Context, login string) (*models.User, error) { - query := `SELECT * FROM users WHERE login = $1` + query := `SELECT id, login FROM users WHERE login = $1` res := r.db.QueryRowContext(ctx, query, login) user := &models.User{ Login: login, } - if err := res.Scan(&user.ID, &user.Login, &user.Phone, &user.PasswordHash); err != nil { + if err := res.Scan(&user.ID, &user.Login, &user.PasswordHash); err != nil { return nil, err } return user, nil } +// CheckUser checks if the user with the given login and password hash exists in the database. func (r *AuthRepo) CheckUser(ctx context.Context, login string, passwordHash string) (*models.User, error) { user, err := r.GetUserByLogin(ctx, login) if err != nil { diff --git a/internal/pkg/auth/usecase/usecase.go b/internal/pkg/auth/usecase/usecase.go index 54e737d..69ccd41 100644 --- a/internal/pkg/auth/usecase/usecase.go +++ b/internal/pkg/auth/usecase/usecase.go @@ -3,7 +3,7 @@ package usecase import ( "2024_1_TeaStealers/internal/models" "2024_1_TeaStealers/internal/pkg/auth" - "2024_1_TeaStealers/internal/pkg/middleware" + "2024_1_TeaStealers/internal/pkg/jwt" "context" "crypto/sha1" "encoding/hex" @@ -11,19 +11,21 @@ import ( "time" ) +// AuthUsecase represents the usecase for authentication. type AuthUsecase struct { repo auth.AuthRepo } +// NewAuthUsecase creates a new instance of AuthUsecase. func NewAuthUsecase(repo auth.AuthRepo) *AuthUsecase { return &AuthUsecase{repo: repo} } +// SignUp handles the user registration process. func (u *AuthUsecase) SignUp(ctx context.Context, data *models.UserLoginData) (*models.User, string, time.Time, error) { newUser := &models.User{ ID: uuid.NewV4(), Login: data.Login, - Phone: "", PasswordHash: generateHashString(data.Password), } @@ -31,7 +33,7 @@ func (u *AuthUsecase) SignUp(ctx context.Context, data *models.UserLoginData) (* return nil, "", time.Now(), err } - token, exp, err := middleware.GenerateToken(newUser) + token, exp, err := jwt.GenerateToken(newUser) if err != nil { return nil, "", time.Now(), err } @@ -39,13 +41,14 @@ func (u *AuthUsecase) SignUp(ctx context.Context, data *models.UserLoginData) (* return newUser, token, exp, nil } +// Login handles the user login process. func (u *AuthUsecase) Login(ctx context.Context, data *models.UserLoginData) (*models.User, string, time.Time, error) { user, err := u.repo.CheckUser(ctx, data.Login, generateHashString(data.Password)) if err != nil { return nil, "", time.Now(), err } - token, exp, err := middleware.GenerateToken(user) + token, exp, err := jwt.GenerateToken(user) if err != nil { return nil, "", time.Now(), err } @@ -53,6 +56,7 @@ func (u *AuthUsecase) Login(ctx context.Context, data *models.UserLoginData) (*m return user, token, exp, nil } +// generateHashString returns a hash string for the given input string. func generateHashString(s string) string { h := sha1.New() h.Write([]byte(s)) diff --git a/internal/pkg/jwt/jwt.go b/internal/pkg/jwt/jwt.go new file mode 100644 index 0000000..200de9d --- /dev/null +++ b/internal/pkg/jwt/jwt.go @@ -0,0 +1,54 @@ +package jwt + +import ( + "2024_1_TeaStealers/internal/models" + "errors" + "fmt" + "github.com/golang-jwt/jwt/v5" + "github.com/satori/uuid" + "os" + "time" +) + +// GenerateToken returns a new JWT token for the given user. +func GenerateToken(user *models.User) (string, time.Time, error) { + exp := time.Now().Add(time.Hour * 24) + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "id": user.ID, + "login": user.Login, + "exp": exp.Unix(), + }) + tokenStr, err := token.SignedString([]byte(os.Getenv("JWT_SECRET"))) + if err != nil { + return "", time.Now(), err + } + return tokenStr, exp, nil +} + +// ParseToken parses the provided JWT token string and returns the parsed token. +func ParseToken(token string) (*jwt.Token, error) { + return jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(os.Getenv("JWT_SECRET")), nil + }) +} + +// ParseId parses the user ID from the JWT token claims. +func ParseId(claims *jwt.Token) (uuid.UUID, error) { + payloadMap, ok := claims.Claims.(jwt.MapClaims) + if !ok { + return uuid.Nil, errors.New("invalid claims") + } + idStr, ok := payloadMap["id"].(string) + if !ok { + return uuid.Nil, errors.New("incorrect id") + } + id, err := uuid.FromString(idStr) + if err != nil { + return uuid.Nil, errors.New("incorrect id") + } + + return id, nil +} diff --git a/internal/pkg/middleware/auth.go b/internal/pkg/middleware/auth.go index 77993ea..5c2fa5c 100644 --- a/internal/pkg/middleware/auth.go +++ b/internal/pkg/middleware/auth.go @@ -1,42 +1,16 @@ package middleware import ( - "2024_1_TeaStealers/internal/models" + "2024_1_TeaStealers/internal/pkg/jwt" "context" - "errors" - "fmt" - "github.com/golang-jwt/jwt/v5" - "github.com/satori/uuid" "net/http" - "os" "time" ) +// CookieName represents the name of the JWT cookie. const CookieName = "jwt-tean" -func GenerateToken(user *models.User) (string, time.Time, error) { - exp := time.Now().Add(time.Hour * 24) - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "id": user.ID, - "login": user.Login, - "exp": exp.Unix(), - }) - tokenStr, err := token.SignedString([]byte(os.Getenv("JWT_SECRET"))) - if err != nil { - return "", time.Now(), err - } - return tokenStr, exp, nil -} - -func ParseToken(token string) (*jwt.Token, error) { - return jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) - } - return []byte(os.Getenv("JWT_SECRET")), nil - }) -} - +// JwtMiddleware is a middleware function that handles JWT authentication. func JwtMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie(CookieName) @@ -46,7 +20,7 @@ func JwtMiddleware(next http.Handler) http.Handler { } token := cookie.Value - claims, err := ParseToken(token) + claims, err := jwt.ParseToken(token) if err != nil { w.WriteHeader(http.StatusUnauthorized) return @@ -62,38 +36,14 @@ func JwtMiddleware(next http.Handler) http.Handler { return } - payload, err := ParsePayload(claims) + id, err := jwt.ParseId(claims) if err != nil { w.WriteHeader(http.StatusUnauthorized) return } - r = r.WithContext(context.WithValue(r.Context(), "payload", payload)) + r = r.WithContext(context.WithValue(r.Context(), CookieName, id)) next.ServeHTTP(w, r) }) } - -func ParsePayload(claims *jwt.Token) (*models.JwtPayload, error) { - payloadMap, ok := claims.Claims.(jwt.MapClaims) - if !ok { - return nil, errors.New("invalid format (claims)") - } - idStr, ok := payloadMap["id"].(string) - if !ok { - return nil, errors.New("incorrect id") - } - login, ok := payloadMap["login"].(string) - if !ok { - return nil, errors.New("incorrect login") - } - id, err := uuid.FromString(idStr) - if err != nil { - return nil, errors.New("incorrect id") - } - - return &models.JwtPayload{ - ID: id, - Login: login, - }, nil -} diff --git a/internal/pkg/utils/utils.go b/internal/pkg/utils/utils.go index d07e29f..6acdb38 100644 --- a/internal/pkg/utils/utils.go +++ b/internal/pkg/utils/utils.go @@ -7,11 +7,13 @@ import ( "net/http" ) +// WriteError writes an error response with the specified status code and message. func WriteError(w http.ResponseWriter, statusCode int, message string) { w.WriteHeader(statusCode) fmt.Fprintln(w, message) } +// WriteResponse writes a JSON response with the specified status code and data. func WriteResponse(w http.ResponseWriter, statusCode int, response interface{}) error { resp, err := json.Marshal(response) if err != nil { @@ -23,6 +25,7 @@ func WriteResponse(w http.ResponseWriter, statusCode int, response interface{}) return nil } +// ReadRequestData reads and parses the request body into the provided structure. func ReadRequestData(r *http.Request, request interface{}) error { data, err := io.ReadAll(r.Body) if err != nil { diff --git a/migrations/000001_table_user.down.sql b/migrations/000001_table_user.down.sql new file mode 100644 index 0000000..365a210 --- /dev/null +++ b/migrations/000001_table_user.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS users; \ No newline at end of file diff --git a/migrations/000001_table_user.up.sql b/migrations/000001_table_user.up.sql new file mode 100644 index 0000000..f093519 --- /dev/null +++ b/migrations/000001_table_user.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS users ( + id UUID PRIMARY KEY, + login VARCHAR(50) NOT NULL, + password_hash VARCHAR(255) NOT NULL +); \ No newline at end of file