diff --git a/go.mod b/go.mod index 5c6bdf8..4140df8 100644 --- a/go.mod +++ b/go.mod @@ -1 +1,10 @@ -module TripAdvisor +module 2024_2_ThereWillBeName + +go 1.23.1 + +require ( + github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/lib/pq v1.10.9 +) + +require golang.org/x/crypto v0.27.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ba250a6 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +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.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= diff --git a/internal/models/user.go b/internal/models/user.go new file mode 100644 index 0000000..aa612af --- /dev/null +++ b/internal/models/user.go @@ -0,0 +1,10 @@ +package models + +import "time" + +type User struct { + ID int64 `json:"id" db:"id"` + Email string `json:"email" db:"email"` + Password string `json:"-" db:"password"` + CreatedAt time.Time `json:"created_at" db:"created_at"` +} diff --git a/internal/pkg/auth/delivery/http/handler.go b/internal/pkg/auth/delivery/http/handler.go new file mode 100644 index 0000000..df0ede9 --- /dev/null +++ b/internal/pkg/auth/delivery/http/handler.go @@ -0,0 +1,84 @@ +package http + +import ( + "context" + "encoding/json" + "net/http" + "2024_2_ThereWillBeName/internal/models" + "2024_2_ThereWillBeName/internal/pkg/auth" + "2024_2_ThereWillBeName/internal/pkg/jwt" +) + +type Handler struct { + usecase auth.AuthUsecase + jwt *jwt.JWT +} + +func NewHandler(usecase auth.AuthUsecase, jwt *jwt.JWT) *Handler { + return &Handler{ + usecase: usecase, + jwt: jwt, + } +} + +func (h *Handler) SignUp(w http.ResponseWriter, r *http.Request) { + var user models.User + if err := json.NewDecoder(r.Body).Decode(&user); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if err := h.usecase.SignUp(context.Background(), user); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusCreated) +} + +func (h *Handler) Login(w http.ResponseWriter, r *http.Request) { + var credentials struct { + Email string `json:"email"` + Password string `json:"password"` + } + if err := json.NewDecoder(r.Body).Decode(&credentials); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + token, err := h.usecase.Login(context.Background(), credentials.Email, credentials.Password) + if err != nil { + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + + http.SetCookie(w, &http.Cookie{ + Name: "token", + Value: token, + Path: "/", + HttpOnly: true, + Secure: true, + }) + + w.WriteHeader(http.StatusOK) +} + +func (h *Handler) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie("token") + if err != nil { + http.Error(w, "Cookie not found", http.StatusUnauthorized) + return + } + + _, err = h.jwt.ParseToken(cookie.Value) + if err != nil { + http.Error(w, "Invalid token", http.StatusUnauthorized) + return + } + + //логика для работы с claims + + next.ServeHTTP(w, r) + }) +} diff --git a/internal/pkg/auth/interfaces.go b/internal/pkg/auth/interfaces.go new file mode 100644 index 0000000..d4f3926 --- /dev/null +++ b/internal/pkg/auth/interfaces.go @@ -0,0 +1,20 @@ +package auth + +import ( + "context" + "2024_2_ThereWillBeName/internal/models" +) + +type AuthUsecase interface { + SignUp(ctx context.Context, user models.User) error + Login(ctx context.Context, email, password string) (string, error) // Возвращает JWT токен + Logout(ctx context.Context, token string) error +} + +type AuthRepo interface { + CreateUser(ctx context.Context, user models.User) error + GetUserByEmail(ctx context.Context, email string) (models.User, error) + UpdateUser(ctx context.Context, user models.User) error + DeleteUser(ctx context.Context, id string) error + GetUsers(ctx context.Context, count, offset int64) ([]models.User, error) +} diff --git a/internal/pkg/auth/repo/auth_repository.go b/internal/pkg/auth/repo/auth_repository.go new file mode 100644 index 0000000..7cad458 --- /dev/null +++ b/internal/pkg/auth/repo/auth_repository.go @@ -0,0 +1,70 @@ +package repo + +import ( + "context" + "database/sql" + "2024_2_ThereWillBeName/internal/models" + _ "github.com/lib/pq" + "time" +) + +type RepositoryImpl struct { + db *sql.DB +} + +func NewRepository(db *sql.DB) *RepositoryImpl { + return &RepositoryImpl{db: db} +} + +func (r *RepositoryImpl) CreateUser(ctx context.Context, user models.User) error { + user.CreatedAt = time.Now() + query := "INSERT INTO users (email, password, created_at) VALUES ($1, $2, NOW())" + _, err := r.db.ExecContext(ctx, query, user.Email, user.Password, user.CreatedAt) + return err +} + +func (r *RepositoryImpl) GetUserByEmail(ctx context.Context, email string) (models.User, error) { + var user models.User + query := "SELECT id, email, password, created_at FROM users WHERE email = $1" + row := r.db.QueryRowContext(ctx, query, email) + fmt.Println("1") + err := row.Scan(&user.ID, &user.Email, &user.Password, &user.CreatedAt) + if err != nil { + if err == sql.ErrNoRows { + return models.User{}, fmt.Errorf("user not found with email: %s", email) + } + return models.User{}, err + } + return user, nil +} + +func (r *RepositoryImpl) UpdateUser(ctx context.Context, user models.User) error { + query := "UPDATE users SET email = $1, password = $2 WHERE id = $3" + _, err := r.db.ExecContext(ctx, query, user.Email, user.Password, user.ID) + return err +} + +func (r *RepositoryImpl) DeleteUser(ctx context.Context, id string) error { + query := "DELETE FROM users WHERE id = $1" + _, err := r.db.ExecContext(ctx, query, id) + return err +} + +func (r *RepositoryImpl) GetUsers(ctx context.Context, count, offset int64) ([]models.User, error) { + query := "SELECT id, email, password, created_at FROM users LIMIT $1 OFFSET $2" + rows, err := r.db.QueryContext(ctx, query, count, offset) + if err != nil { + return nil, err + } + defer rows.Close() + + var users []models.User + for rows.Next() { + var user models.User + if err := rows.Scan(&user.ID, &user.Email, &user.Password, &user.CreatedAt); err != nil { + return nil, err + } + users = append(users, user) + } + return users, nil +} diff --git a/internal/pkg/auth/usecase/auth_usecase.go b/internal/pkg/auth/usecase/auth_usecase.go new file mode 100644 index 0000000..39dc84a --- /dev/null +++ b/internal/pkg/auth/usecase/auth_usecase.go @@ -0,0 +1,45 @@ +package usecase + +import ( + "context" + "2024_2_ThereWillBeName/internal/models" + "2024_2_ThereWillBeName/internal/pkg/auth" + "2024_2_ThereWillBeName/internal/pkg/jwt" + "golang.org/x/crypto/bcrypt" +) + +type AuthUsecaseImpl struct { + repo auth.AuthRepo + jwt *jwt.JWT +} + +func NewAuthUsecase(repo auth.AuthRepo, jwt *jwt.JWT) *AuthUsecaseImpl { + return &AuthUsecaseImpl{ + repo: repo, + jwt: jwt, + } +} + +func (a *AuthUsecaseImpl) SignUp(ctx context.Context, user models.User) error { + hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) + user.Password = string(hashedPassword) + return a.repo.CreateUser(ctx, user) +} + +func (a *AuthUsecaseImpl) Login(ctx context.Context, email, password string) (string, error) { + user, err := a.repo.GetUserByEmail(ctx, email) + if err != nil { + return "", err + } + + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { + return "", err + } + + return a.jwt.GenerateToken(uint(user.ID), user.Email) +} + +func (a *AuthUsecaseImpl) Logout(ctx context.Context, token string) error { + + return nil +} diff --git a/internal/pkg/jwt/jwt.go b/internal/pkg/jwt/jwt.go new file mode 100644 index 0000000..90181bb --- /dev/null +++ b/internal/pkg/jwt/jwt.go @@ -0,0 +1,42 @@ +package jwt + +import ( + "github.com/dgrijalva/jwt-go" + "time" + "fmt" +) + +type JWT struct { + secret []byte +} + +func NewJWT(secret string) *JWT { + return &JWT{ + secret: []byte(secret), + } +} + +func (j *JWT) GenerateToken(userID uint, email string) (string, error) { + claims := jwt.MapClaims{ + "id": userID, + "email": email, + "exp": time.Now().Add(time.Hour * 24).Unix(), + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(j.secret) +} + +func (j *JWT) ParseToken(token string) (map[string]interface{}, error) { + parsedToken, err := 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 j.secret, nil + }) + + if claims, ok := parsedToken.Claims.(jwt.MapClaims); ok && parsedToken.Valid { + return claims, nil + } + return nil, err +}