Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Task/auth #5

Merged
merged 11 commits into from
Sep 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 65 additions & 11 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -1,40 +1,71 @@
package main

import (
"TripAdvisor/internal/models"
"TripAdvisor/internal/pkg/middleware"
"TripAdvisor/internal/pkg/places/delivery"
"TripAdvisor/internal/pkg/places/repo"
"TripAdvisor/internal/pkg/places/usecase"
"2024_2_ThereWillBeName/internal/models"
httpHandler "2024_2_ThereWillBeName/internal/pkg/auth/delivery/http"
"2024_2_ThereWillBeName/internal/pkg/auth/repo"
"2024_2_ThereWillBeName/internal/pkg/auth/usecase"
"2024_2_ThereWillBeName/internal/pkg/jwt"
"2024_2_ThereWillBeName/internal/pkg/middleware"
"2024_2_ThereWillBeName/internal/pkg/places/delivery"
placerepo "2024_2_ThereWillBeName/internal/pkg/places/repo"
placeusecase "2024_2_ThereWillBeName/internal/pkg/places/usecase"
"database/sql"
"encoding/hex"
"flag"
"fmt"
"github.com/gorilla/mux"
_ "github.com/lib/pq"
"log"
"math/rand"
"net/http"
"os"
"time"
)

func main() {
newPlaceRepo := repo.NewPLaceRepository()
placeUsecase := usecase.NewPlaceUsecase(newPlaceRepo)
handler := delivery.NewPlacesHandler(placeUsecase)

var cfg models.Config
flag.IntVar(&cfg.Port, "port", 8080, "API server port")
flag.StringVar(&cfg.Env, "env", "development", "Environment")
flag.StringVar(&cfg.AllowedOrigin, "allowed-origin", "*", "Allowed origin")
flag.StringVar(&cfg.ConnStr, "connStr", "host=localhost port=5433 user=test_user password=1234567890 dbname=testdb_tripadvisor sslmode=disable", "PostgreSQL connection string")
flag.Parse()

corsMiddleware := middleware.NewCORSMiddleware([]string{cfg.AllowedOrigin})
newPlaceRepo := placerepo.NewPLaceRepository()
placeUsecase := placeusecase.NewPlaceUsecase(newPlaceRepo)
handler := delivery.NewPlacesHandler(placeUsecase)

logger := log.New(os.Stdout, "", log.Ldate|log.Ltime)

db, err := openDB(cfg.ConnStr)
if err != nil {
logger.Fatal(err)
}
defer db.Close()

jwtSecret, err := generateSecretKey(32)
if err != nil {
logger.Fatal("Error generating secret key:", err)
}

authRepo := repo.NewAuthRepository(db)
jwtHandler := jwt.NewJWT(string(jwtSecret))
authUseCase := usecase.NewAuthUsecase(authRepo, jwtHandler)
h := httpHandler.NewAuthHandler(authUseCase, jwtHandler)

corsMiddleware := middleware.NewCORSMiddleware([]string{cfg.AllowedOrigin})

r := mux.NewRouter().PathPrefix("/api/v1").Subrouter()
r.Use(corsMiddleware.CorsMiddleware)
r.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Not Found", http.StatusNotFound)
})
r.HandleFunc("/healthcheck", healthcheckHandler).Methods(http.MethodGet)

auth := r.PathPrefix("/auth").Subrouter()
auth.HandleFunc("/signup", h.SignUp).Methods(http.MethodPost)
auth.HandleFunc("/login", h.Login).Methods(http.MethodPost)
auth.HandleFunc("/logout", h.Logout).Methods(http.MethodPost)
places := r.PathPrefix("/places").Subrouter()
places.HandleFunc("", handler.GetPlaceHandler).Methods(http.MethodGet)
srv := &http.Server{
Expand All @@ -46,7 +77,7 @@
}

logger.Printf("starting %s server on %s", cfg.Env, srv.Addr)
err := srv.ListenAndServe()
err = srv.ListenAndServe()
if err != nil {
logger.Println(fmt.Errorf("Failed to start server: %v", err))
os.Exit(1)
Expand All @@ -59,3 +90,26 @@
http.Error(w, "", http.StatusBadRequest)
}
}

func openDB(connStr string) (*sql.DB, error) {
db, err := sql.Open("postgres", connStr)
if err != nil {
return nil, err
}

err = db.Ping()
if err != nil {
return nil, err
}

return db, nil
}

func generateSecretKey(length int) (string, error) {
key := make([]byte, length)
_, err := rand.Read(key)

Check failure on line 110 in cmd/main.go

View workflow job for this annotation

GitHub Actions / linters

G404: Use of weak random number generator (math/rand or math/rand/v2 instead of crypto/rand) (gosec)
if err != nil {
return "", err
}
return hex.EncodeToString(key), nil
}
13 changes: 10 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
module TripAdvisor
go 1.23.0
require github.com/gorilla/mux v1.8.1
module 2024_2_ThereWillBeName

go 1.23.1

require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gorilla/mux v1.8.1
github.com/lib/pq v1.10.9
golang.org/x/crypto v0.27.0
)
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
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/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
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=
10 changes: 10 additions & 0 deletions internal/models/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package models

import "time"

type User struct {
ID int64 `json:"id" db:"id"`
Login string `json:"login" db:"login"`
Password string `json:"-" db:"password"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
}
85 changes: 85 additions & 0 deletions internal/pkg/auth/delivery/http/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package http

import (
"2024_2_ThereWillBeName/internal/models"
"2024_2_ThereWillBeName/internal/pkg/auth"
"2024_2_ThereWillBeName/internal/pkg/jwt"
"context"
"encoding/json"
"net/http"
)

type Handler struct {
usecase auth.AuthUsecase
jwt *jwt.JWT
}

func NewAuthHandler(usecase auth.AuthUsecase, jwt *jwt.JWT) *Handler {
return &Handler{
usecase: usecase,
jwt: jwt,
}
}

func (h *Handler) SignUp(w http.ResponseWriter, r *http.Request) {
var credentials struct {
Login string `json:"login"`
Password string `json:"password"`
}

if err := json.NewDecoder(r.Body).Decode(&credentials); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

user := models.User{
Login: credentials.Login,
Password: credentials.Password,
}

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 {
Login string `json:"login"`
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.Login, 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: false,
})

w.WriteHeader(http.StatusOK)
}

func (h *Handler) Logout(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, &http.Cookie{
Name: "token",
Value: "",
Path: "/",
HttpOnly: true,
Secure: false,
})

w.WriteHeader(http.StatusOK)
}
19 changes: 19 additions & 0 deletions internal/pkg/auth/interfaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package auth

import (
"2024_2_ThereWillBeName/internal/models"
"context"
)

type AuthUsecase interface {
SignUp(ctx context.Context, user models.User) error
Login(ctx context.Context, login, password string) (string, error) // Возвращает JWT токен
}

type AuthRepo interface {
CreateUser(ctx context.Context, user models.User) error
GetUserByLogin(ctx context.Context, login 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)
}
69 changes: 69 additions & 0 deletions internal/pkg/auth/repo/auth_repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package repo

import (
"2024_2_ThereWillBeName/internal/models"
"context"
"database/sql"
"fmt"

_ "github.com/lib/pq"
)

type RepositoryImpl struct {
db *sql.DB
}

func NewAuthRepository(db *sql.DB) *RepositoryImpl {
return &RepositoryImpl{db: db}
}

func (r *RepositoryImpl) CreateUser(ctx context.Context, user models.User) error {
query := "INSERT INTO users (login, password, created_at) VALUES ($1, $2, NOW())"
_, err := r.db.ExecContext(ctx, query, user.Login, user.Password)
return err
}

func (r *RepositoryImpl) GetUserByLogin(ctx context.Context, login string) (models.User, error) {
var user models.User
query := "SELECT id, login, password, created_at FROM users WHERE login = $1"
row := r.db.QueryRowContext(ctx, query, login)
err := row.Scan(&user.ID, &user.Login, &user.Password, &user.CreatedAt)
if err != nil {
if err == sql.ErrNoRows {
return models.User{}, fmt.Errorf("user not found with login: %s", login)
}
return models.User{}, err
}
return user, nil
}

func (r *RepositoryImpl) UpdateUser(ctx context.Context, user models.User) error {
query := "UPDATE users SET login = $1, password = $2 WHERE id = $3"
_, err := r.db.ExecContext(ctx, query, user.Login, 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, login, 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.Login, &user.CreatedAt); err != nil {
return nil, err
}
users = append(users, user)
}
return users, nil
}
47 changes: 47 additions & 0 deletions internal/pkg/auth/usecase/auth_usecase.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package usecase

import (
"2024_2_ThereWillBeName/internal/models"
"2024_2_ThereWillBeName/internal/pkg/auth"
"2024_2_ThereWillBeName/internal/pkg/jwt"
"context"
"log"

"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, login, password string) (string, error) {
user, err := a.repo.GetUserByLogin(ctx, login)

if err != nil {
log.Printf("Error retrieving user: %v\n", err)
return "", err
}

if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
log.Printf("Password mismatch: %v\n", err)
return "", err
} else {
log.Println("Password match!")
}

return a.jwt.GenerateToken(uint(user.ID), user.Login)

Check failure on line 46 in internal/pkg/auth/usecase/auth_usecase.go

View workflow job for this annotation

GitHub Actions / linters

G115: integer overflow conversion int64 -> uint (gosec)
}
Loading
Loading