From 14815e521b720bbd9b560e58c5d77b2094ea88e4 Mon Sep 17 00:00:00 2001 From: Daniil Tsivinsky Date: Sun, 15 Feb 2026 17:16:22 +0300 Subject: [PATCH] init --- auth/cookies.go | 30 ++++++++++ auth/jwt.go | 30 ++++++++++ go.mod | 18 ++++++ go.sum | 18 ++++++ main.go | 143 ++++++++++++++++++++++++++++++++++++++++++++++++ model/model.go | 10 ++++ model/user.go | 8 +++ 7 files changed, 257 insertions(+) create mode 100644 auth/cookies.go create mode 100644 auth/jwt.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 model/model.go create mode 100644 model/user.go diff --git a/auth/cookies.go b/auth/cookies.go new file mode 100644 index 0000000..fb69fcc --- /dev/null +++ b/auth/cookies.go @@ -0,0 +1,30 @@ +package auth + +import ( + "net/http" + "time" +) + +func SetUserCookie(w http.ResponseWriter, token string, expiryTime time.Time) { + http.SetCookie(w, &http.Cookie{ + Name: "token", + Value: token, + Secure: true, + HttpOnly: true, + Path: "/", + Expires: expiryTime, + SameSite: http.SameSiteStrictMode, + }) +} + +func RemoveUserCookie(w http.ResponseWriter) { + http.SetCookie(w, &http.Cookie{ + Name: "token", + Value: "", + Secure: true, + HttpOnly: true, + Path: "/", + Expires: time.Now().Add(-time.Hour), + SameSite: http.SameSiteStrictMode, + }) +} diff --git a/auth/jwt.go b/auth/jwt.go new file mode 100644 index 0000000..be64d75 --- /dev/null +++ b/auth/jwt.go @@ -0,0 +1,30 @@ +package auth + +import ( + "os" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +var secretKey = os.Getenv("JWT_SECRET_KEY") + +type UserClaims struct { + jwt.RegisteredClaims + UserID int64 +} + +func GenerateUserToken(userId int64, expiryTime time.Time) (string, error) { + now := time.Now() + claims := UserClaims{ + UserID: userId, + RegisteredClaims: jwt.RegisteredClaims{ + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(expiryTime), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + return token.SignedString([]byte(secretKey)) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ccca291 --- /dev/null +++ b/go.mod @@ -0,0 +1,18 @@ +module game-wishlist + +go 1.25.7 + +require ( + github.com/google/uuid v1.6.0 + golang.org/x/crypto v0.48.0 + gorm.io/driver/sqlite v1.6.0 + gorm.io/gorm v1.31.1 +) + +require ( + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect + golang.org/x/text v0.34.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c3b395a --- /dev/null +++ b/go.sum @@ -0,0 +1,18 @@ +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= diff --git a/main.go b/main.go new file mode 100644 index 0000000..33f4e00 --- /dev/null +++ b/main.go @@ -0,0 +1,143 @@ +package main + +import ( + "encoding/json" + "fmt" + "game-wishlist/auth" + "game-wishlist/model" + "log" + "net/http" + "os" + "time" + + "golang.org/x/crypto/bcrypt" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func sendError(w http.ResponseWriter, msg string, err error, status int) { + m := msg + if err != nil { + m = fmt.Sprintf("%s: %v", msg, err) + } + http.Error(w, m, status) +} + +func sendJSON(w http.ResponseWriter, data any, status int) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + if err := json.NewEncoder(w).Encode(data); err != nil { + http.Error(w, err.Error(), 500) + } +} + +func main() { + dbPath := os.Getenv("DB_PATH") + if dbPath == "" { + dbPath = "./sqlite.db" + } + + db, err := gorm.Open(sqlite.Open(dbPath)) + if err != nil { + log.Fatalf("failed to connect to db: %v\n", err) + } + + if err := db.AutoMigrate(model.User{}); err != nil { + log.Fatalf("failed to automigrate db: %v\n", err) + } + + mux := http.NewServeMux() + + mux.HandleFunc("POST /api/auth/register", func(w http.ResponseWriter, r *http.Request) { + var body struct { + Login string `json:"login"` + Password string `json:"password"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sendError(w, "invalid request body", err, 400) + return + } + + if body.Login == "" || body.Password == "" { + sendError(w, "invalid request, login and password required", nil, 400) + return + } + + hash, err := bcrypt.GenerateFromPassword([]byte(body.Password), 10) + if err != nil { + sendError(w, "failed to hash password", err, 500) + return + } + + user := &model.User{ + Login: body.Login, + Password: string(hash), + } + if tx := db.Create(user); tx.Error != nil { + sendError(w, "failed to create user", tx.Error, 400) + return + } + + expiryTime := time.Now().Add(time.Hour * 24 * 7) // 1 week lifetime + token, err := auth.GenerateUserToken(int64(user.ID), expiryTime) + if err != nil { + sendError(w, "failed to generate user token", err, 500) + return + } + + auth.SetUserCookie(w, token, expiryTime) + + sendJSON(w, user, 201) + }) + + mux.HandleFunc("POST /api/auth/login", func(w http.ResponseWriter, r *http.Request) { + var body struct { + Login string `json:"login"` + Password string `json:"password"` + } + + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sendError(w, "invalid request body", err, 400) + return + } + + if body.Login == "" || body.Password == "" { + sendError(w, "invalid request, login and password required", nil, 400) + return + } + + user := &model.User{} + if tx := db.First(user, "login = ?", body.Login); tx.Error != nil { + sendError(w, "login not found", tx.Error, 400) + return + } + + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(body.Password)); err != nil { + sendError(w, "invalid password", err, 400) + return + } + + expiryTime := time.Now().Add(time.Hour * 24 * 7) // 1 week lifetime + token, err := auth.GenerateUserToken(int64(user.ID), expiryTime) + if err != nil { + sendError(w, "failed to generate user token", err, 500) + return + } + + auth.SetUserCookie(w, token, expiryTime) + + sendJSON(w, user, 200) + }) + + mux.HandleFunc("POST /api/auth/logout", func(w http.ResponseWriter, r *http.Request) { + auth.RemoveUserCookie(w) + sendJSON(w, struct { + Ok bool `json:"ok"` + }{true}, 200) + }) + + log.Print("starting http server on http://localhost:5000") + if err := http.ListenAndServe(":5000", mux); err != nil { + log.Fatalf("failed to start http server: %v\n", err) + } +} diff --git a/model/model.go b/model/model.go new file mode 100644 index 0000000..9b0fc48 --- /dev/null +++ b/model/model.go @@ -0,0 +1,10 @@ +package model + +import "time" + +type Model struct { + ID uint `json:"id" gorm:"primarykey"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt *time.Time `json:"deletedAt" gorm:"index"` +} diff --git a/model/user.go b/model/user.go new file mode 100644 index 0000000..812eca9 --- /dev/null +++ b/model/user.go @@ -0,0 +1,8 @@ +package model + +type User struct { + Model + + Login string `json:"login" gorm:"unique"` + Password string `json:"-"` +}