Skip to content

Commit

Permalink
Merge pull request #5 from go-park-mail-ru/PRI-3
Browse files Browse the repository at this point in the history
Pri-3: autorization
  • Loading branch information
marrgancovka authored Mar 2, 2024
2 parents 750d97e + a1e93b9 commit c94dcce
Show file tree
Hide file tree
Showing 18 changed files with 481 additions and 7 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.idea
.bin
.bin
*.env
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,10 @@ lint:
golangci-lint run --config=.golangci.yaml

test:
go test -race ./...
go test -race ./...

dev-compose-up:
docker compose -f "dev-docker-compose.yaml" up -d

dev-compose-down:
docker compose -f "dev-docker-compose.yaml" down
35 changes: 33 additions & 2 deletions cmd/main/main.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package main

import (
authH "2024_1_TeaStealers/internal/pkg/auth/delivery"
authR "2024_1_TeaStealers/internal/pkg/auth/repo"
authUc "2024_1_TeaStealers/internal/pkg/auth/usecase"
"2024_1_TeaStealers/internal/pkg/middleware"
"context"
"database/sql"
"fmt"
"log"
"net/http"
Expand All @@ -11,15 +16,41 @@ import (
"time"

"github.com/gorilla/mux"
"github.com/joho/godotenv"
_ "github.com/lib/pq"
)

func main() {
_ = godotenv.Load()
//postgres.Open(dsn.FromEnv()), &gorm.Config{}

Check failure on line 25 in cmd/main/main.go

View workflow job for this annotation

GitHub Actions / Linters

commentFormatting: put a space between `//` and comment text (gocritic)
db, err := sql.Open("postgres", fmt.Sprintf("postgres://%v:%v@%v:%v/%v?sslmode=disable",
os.Getenv("DB_USER"),
os.Getenv("DB_PASS"),
os.Getenv("DB_HOST"),
os.Getenv("DB_PORT"),
os.Getenv("DB_NAME")))
if err != nil {
panic("failed to connect database" + err.Error())
}

r := mux.NewRouter().PathPrefix("/api").Subrouter()
r.HandleFunc("/ping", pingPongHandler).Methods(http.MethodGet)

authRepo := authR.NewRepository(db)
authUsecase := authUc.NewAuthUsecase(authRepo)
autHandler := authH.NewAuthHandler(authUsecase)

auth := r.PathPrefix("/auth").Subrouter()
auth.HandleFunc("/signup", autHandler.SignUp).Methods(http.MethodPost, http.MethodOptions)
auth.HandleFunc("/login", autHandler.Login).Methods(http.MethodPost, http.MethodOptions)
auth.Handle("/logout", middleware.JwtMiddleware(http.HandlerFunc(autHandler.Logout))).Methods(http.MethodGet, http.MethodOptions)

Check failure on line 46 in cmd/main/main.go

View workflow job for this annotation

GitHub Actions / Linters

line is 133 characters (lll)

srv := &http.Server{
Addr: ":8080",
Handler: r,
Addr: ":8080",
Handler: r,
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}

signalCh := make(chan os.Signal, 1)
Expand Down
18 changes: 18 additions & 0 deletions dev-docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
version: "3.9"
services:
db:
container_name: postgres
image: postgres:latest
restart: always
volumes:
- type: volume
source: postgresdb-data
target: /var/lib/postgresql/data
env_file:
- ./.env
ports:
- ${DB_PORT}:5432

volumes:
postgresdb-data:
driver: local
14 changes: 13 additions & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,16 @@ services:
context: .
dockerfile: ./build/main.Dockerfile
ports:
- '8080:8080'
- '8080:8080'
db:
container_name: postgres
image: postgres:latest
restart: always
volumes:
- type: volume
source: postgresdb-data
target: /var/lib/postgresql/data
env_file:
- ./.env
ports:
- ${DB_PORT}:5432
7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,10 @@ module 2024_1_TeaStealers
go 1.21.1

require github.com/gorilla/mux v1.8.1

require (
github.com/golang-jwt/jwt/v5 v5.2.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/satori/uuid v1.2.0 // indirect
)
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,10 @@
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
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=
github.com/satori/uuid v1.2.0 h1:6TFY4nxn5XwBx0gDfzbEMCNT6k4N/4FNIuN8RACZ0KI=
github.com/satori/uuid v1.2.0/go.mod h1:B8HLsPLik/YNn6KKWVMDJ8nzCL8RP5WyfsnmvnAEwIU=
9 changes: 9 additions & 0 deletions internal/models/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package models

// UserLoginData represents user information for login and signup
type UserLoginData struct {
// Login stands for users nickname
Login string `json:"login"`
// Password stands for users password
Password string `json:"password"`
}
13 changes: 11 additions & 2 deletions internal/models/user.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
package models

import (
"github.com/satori/uuid"
)

// User represents user information
type User struct {
Login string
PasswordHash string
// 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:"-"`
}
83 changes: 83 additions & 0 deletions internal/pkg/auth/delivery/http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package delivery

import (
"2024_1_TeaStealers/internal/models"
"2024_1_TeaStealers/internal/pkg/auth"
"2024_1_TeaStealers/internal/pkg/middleware"
"2024_1_TeaStealers/internal/pkg/utils"
"net/http"
"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{}

if err := utils.ReadRequestData(r, &data); err != nil {
utils.WriteError(w, http.StatusBadRequest, "incorrect data format")
return
}

newUser, token, exp, err := h.uc.SignUp(r.Context(), &data)
if err != nil {
utils.WriteError(w, http.StatusBadRequest, err.Error())
return
}

http.SetCookie(w, tokenCookie(middleware.CookieName, token, exp))

if err = utils.WriteResponse(w, http.StatusCreated, newUser); err != nil {
utils.WriteError(w, http.StatusInternalServerError, err.Error())
}
}

// 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 {
utils.WriteError(w, http.StatusBadRequest, err.Error())
return
}

user, token, exp, err := h.uc.Login(r.Context(), &data)
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "incorrect password or login")
}

http.SetCookie(w, tokenCookie(middleware.CookieName, token, exp))

if err = utils.WriteResponse(w, http.StatusOK, user); err != nil {
utils.WriteError(w, http.StatusInternalServerError, err.Error())
}
}

// 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,
Value: "",
Path: "/",
})
}

// 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,
Value: token,
Expires: exp,
Path: "/",
HttpOnly: true,
}
}
19 changes: 19 additions & 0 deletions internal/pkg/auth/interfaces.go
Original file line number Diff line number Diff line change
@@ -1 +1,20 @@
package auth

import (
"2024_1_TeaStealers/internal/models"
"context"
"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)
}
58 changes: 58 additions & 0 deletions internal/pkg/auth/repo/postgres.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package repo

import (
"2024_1_TeaStealers/internal/models"
"context"
"database/sql"
"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, password_hash) VALUES ($1, $2, $3, $4)`

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 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.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) {

Check failure on line 47 in internal/pkg/auth/repo/postgres.go

View workflow job for this annotation

GitHub Actions / Linters

line is 108 characters (lll)
user, err := r.GetUserByLogin(ctx, login)
if err != nil {
return nil, err
}

if user.PasswordHash != passwordHash {
return nil, errors.New("wrong password")
}

return user, nil
}
64 changes: 64 additions & 0 deletions internal/pkg/auth/usecase/usecase.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package usecase

import (
"2024_1_TeaStealers/internal/models"
"2024_1_TeaStealers/internal/pkg/auth"
"2024_1_TeaStealers/internal/pkg/jwt"
"context"
"crypto/sha1"
"encoding/hex"
"github.com/satori/uuid"

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

View workflow job for this annotation

GitHub Actions / Linters

File is not `goimports`-ed (goimports)
"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) {

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

View workflow job for this annotation

GitHub Actions / Linters

line is 120 characters (lll)
newUser := &models.User{
ID: uuid.NewV4(),
Login: data.Login,
PasswordHash: generateHashString(data.Password),
}

if err := u.repo.CreateUser(ctx, newUser); err != nil {
return nil, "", time.Now(), err
}

token, exp, err := jwt.GenerateToken(newUser)
if err != nil {
return nil, "", time.Now(), err
}

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) {

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

View workflow job for this annotation

GitHub Actions / Linters

line is 119 characters (lll)
user, err := u.repo.CheckUser(ctx, data.Login, generateHashString(data.Password))
if err != nil {
return nil, "", time.Now(), err
}

token, exp, err := jwt.GenerateToken(user)
if err != nil {
return nil, "", time.Now(), err
}

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))
return hex.EncodeToString(h.Sum(nil))
}
Loading

0 comments on commit c94dcce

Please sign in to comment.