Skip to content

Commit

Permalink
Task/auth (#5)
Browse files Browse the repository at this point in the history
* WIP: /signup work correctly

* WIP: /signup work correctly

* WIP: /login work correctly

* fixed mistakes

* some comments deleted, renamed folder and package

* used gorilla/mux

* fixed mistakes

* fixed mistakes

* fixing

* merged dev+auth

---------

Co-authored-by: timurIsaevIY <118661906+timurIsaevIY@users.noreply.github.com>
Co-authored-by: timurIsaevIY <isaevtimur2016@gmail.com>
  • Loading branch information
3 people authored Sep 29, 2024
1 parent 5cba629 commit d4c5666
Show file tree
Hide file tree
Showing 14 changed files with 390 additions and 19 deletions.
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 @@ func main() {
}

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 @@ func healthcheckHandler(w http.ResponseWriter, r *http.Request) {
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

0 comments on commit d4c5666

Please sign in to comment.