From eb24084e249b642ed29b5ba182f7449ae9341afd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?BENEDEK=20L=C3=A1szl=C3=B3?= Date: Sun, 1 Jun 2025 17:17:37 +0200 Subject: [PATCH] Implement session management with login and logout functionality --- api/auth.go | 80 ++++++++++++++++++++++++++++++++++-- controller/UserController.go | 55 ++++++++++++++++++++++++- dao/Factory.go | 11 ++++- dao/ISessionDAO.go | 2 + dao/valkey/Connection.go | 18 ++++++++ dao/valkey/SessionDAO.go | 40 ++++++++++++++++++ dao/valkey/vkDAO.go | 15 +++++++ 7 files changed, 214 insertions(+), 7 deletions(-) create mode 100644 dao/valkey/Connection.go create mode 100644 dao/valkey/SessionDAO.go create mode 100644 dao/valkey/vkDAO.go diff --git a/api/auth.go b/api/auth.go index e4a0bd8..2f35f37 100644 --- a/api/auth.go +++ b/api/auth.go @@ -3,19 +3,32 @@ package api import ( "net/http" + "git.tek.govt.hu/dowerx/chat/server/config" "git.tek.govt.hu/dowerx/chat/server/controller" "github.com/gin-gonic/gin" ) -func isLoggedIn(c *gin.Context) { +const SESSION_COOKIE string = "session" +func isLoggedIn(c *gin.Context) { + token, err := c.Cookie(SESSION_COOKIE) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "missing token", + }) + c.Abort() + return + } + + c.Set(SESSION_COOKIE, token) + c.Next() } func register(c *gin.Context) { type registerTransaction struct { - Username string `form:"username"` - Password string `form:"password"` - RepeatPassword string `form:"repeatPassword"` + Username string `form:"username" json:"username"` + Password string `form:"password" json:"password"` + RepeatPassword string `form:"repeatPassword" json:"repeatPassword"` } transaction := registerTransaction{} @@ -49,9 +62,68 @@ func register(c *gin.Context) { } func login(c *gin.Context) { + type loginTransaction struct { + Username string `form:"username" json:"username"` + Password string `form:"password" json:"password"` + } + transaction := loginTransaction{} + if err := c.Bind(&transaction); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": err.Error(), + }) + return + } + userController, err := controller.MakeUserController() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": err.Error(), + }) + return + } + + token, ok, err := userController.Login(transaction.Username, transaction.Password) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": err.Error(), + }) + return + } + + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{ + "error": "bad credentials", + }) + return + } + + c.SetCookie(SESSION_COOKIE, token, config.GetConfig().API.TokenLife, "", "", false, false) + c.JSON(http.StatusOK, gin.H{ + "message": "sucessful login", + "session": token, + }) } func logout(c *gin.Context) { + userController, err := controller.MakeUserController() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": err.Error(), + }) + return + } + token, _ := c.Get(SESSION_COOKIE) // must exist after isLoggedIn + err = userController.Logout(token.(string)) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": err.Error(), + }) + return + } + + c.SetCookie(SESSION_COOKIE, "", 0, "", "", false, false) + c.JSON(http.StatusOK, gin.H{ + "message": "sucessful logout", + }) } diff --git a/controller/UserController.go b/controller/UserController.go index 313fc5e..2f8adfc 100644 --- a/controller/UserController.go +++ b/controller/UserController.go @@ -1,6 +1,8 @@ package controller import ( + "crypto/rand" + "encoding/base64" "errors" "git.tek.govt.hu/dowerx/chat/server/dao" @@ -9,19 +11,31 @@ import ( ) type UserController struct { - userDAO dao.IUserDAO + userDAO dao.IUserDAO + sessionDAO dao.ISessionDAO } const ( MIN_USERNAME_LENGTH int = 3 MIN_PASSWORD_LENGTH int = 6 HASH_COST int = bcrypt.DefaultCost + TOKEN_LENGTH int = 32 ) func (c *UserController) init() error { userDAO, err := dao.MakeUserDAO() c.userDAO = userDAO - return err + if err != nil { + return err + } + + sessionDao, err := dao.MakeSessionDAO() + c.sessionDAO = sessionDao + if err != nil { + return err + } + + return nil } func (c UserController) Register(username string, password string, repeatPassword string) error { @@ -47,3 +61,40 @@ func (c UserController) Register(username string, password string, repeatPasswor PasswordHash: string(hash), }) } + +func generateToken(length int) (string, error) { + b := make([]byte, length) + _, err := rand.Read(b) + if err != nil { + return "", err + } + + return base64.URLEncoding.EncodeToString(b), nil +} + +func (c UserController) Login(username string, password string) (string, bool, error) { + user, err := c.userDAO.Read(model.User{Username: username}) + if err != nil { + return "", false, err + } + + if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil { + return "", false, errors.New("wrong password") + } + + token, err := generateToken(TOKEN_LENGTH) + if err != nil { + return "", false, err + } + + err = c.sessionDAO.Set(token, user.ID) + if err != nil { + return "", false, err + } + + return token, true, nil +} + +func (c UserController) Logout(token string) error { + return c.sessionDAO.Delete(token) +} diff --git a/dao/Factory.go b/dao/Factory.go index a05ed76..dac3a15 100644 --- a/dao/Factory.go +++ b/dao/Factory.go @@ -1,9 +1,18 @@ package dao -import "git.tek.govt.hu/dowerx/chat/server/dao/postgres" +import ( + "git.tek.govt.hu/dowerx/chat/server/dao/postgres" + "git.tek.govt.hu/dowerx/chat/server/dao/valkey" +) func MakeUserDAO() (IUserDAO, error) { dao := &postgres.UserDAOPG{} err := dao.Init() return dao, err } + +func MakeSessionDAO() (ISessionDAO, error) { + dao := &valkey.SessionDAOVK{} + err := dao.Init() + return dao, err +} diff --git a/dao/ISessionDAO.go b/dao/ISessionDAO.go index 93e5d62..7e48a54 100644 --- a/dao/ISessionDAO.go +++ b/dao/ISessionDAO.go @@ -3,4 +3,6 @@ package dao type ISessionDAO interface { Set(token string, id int) error Get(token string) (int, error) + Delete(token string) error + Bump(token string, time int) error } diff --git a/dao/valkey/Connection.go b/dao/valkey/Connection.go new file mode 100644 index 0000000..1d32547 --- /dev/null +++ b/dao/valkey/Connection.go @@ -0,0 +1,18 @@ +package valkey + +import ( + "git.tek.govt.hu/dowerx/chat/server/config" + "github.com/valkey-io/valkey-go" +) + +var vk valkey.Client = nil + +func getClient() (*valkey.Client, error) { + if vk == nil { + client, err := valkey.NewClient(config.GetConfig().Valkey) + vk = client + return &vk, err + } + + return &vk, nil +} diff --git a/dao/valkey/SessionDAO.go b/dao/valkey/SessionDAO.go new file mode 100644 index 0000000..158335e --- /dev/null +++ b/dao/valkey/SessionDAO.go @@ -0,0 +1,40 @@ +package valkey + +import ( + "context" + "strconv" + + "git.tek.govt.hu/dowerx/chat/server/config" +) + +const SESSION_PREFIX string = "session:" + +type SessionDAOVK struct { + vkDAO +} + +func (d SessionDAOVK) Set(token string, id int) error { + cmd := (*d.vk).B().Set().Key(SESSION_PREFIX + token).Value(strconv.Itoa(id)).ExSeconds(int64(config.GetConfig().API.TokenLife)).Build() + return (*d.vk).Do(context.Background(), cmd).Error() +} + +func (d SessionDAOVK) Get(token string) (int, error) { + cmd := (*d.vk).B().Get().Key(SESSION_PREFIX + token).Build() + result := (*d.vk).Do(context.Background(), cmd) + if err := result.Error(); err != nil { + return 0, err + } + + id, err := result.AsInt64() + return int(id), err +} + +func (d SessionDAOVK) Delete(token string) error { + cmd := (*d.vk).B().Del().Key(SESSION_PREFIX + token).Build() + return (*d.vk).Do(context.Background(), cmd).Error() +} + +func (d SessionDAOVK) Bump(token string, time int) error { + cmd := (*d.vk).B().Expire().Key(SESSION_PREFIX + token).Seconds(int64(time)).Build() + return (*d.vk).Do(context.Background(), cmd).Error() +} diff --git a/dao/valkey/vkDAO.go b/dao/valkey/vkDAO.go new file mode 100644 index 0000000..0c0d870 --- /dev/null +++ b/dao/valkey/vkDAO.go @@ -0,0 +1,15 @@ +package valkey + +import ( + "github.com/valkey-io/valkey-go" +) + +type vkDAO struct { + vk *valkey.Client +} + +func (d *vkDAO) Init() error { + client, err := getClient() + d.vk = client + return err +}