diff --git a/api/auth.go b/api/auth.go index 8fa3551..422753c 100644 --- a/api/auth.go +++ b/api/auth.go @@ -40,21 +40,14 @@ func register(c *gin.Context) { } authController, err := controller.MakeAuthController() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": err.Error(), - }) + sendError(c, err) return } err = authController.Register(transaction.Username, transaction.Password, transaction.RepeatPassword) - - // TODO: handle server errors/register violations separetly if err != nil { - c.JSON(http.StatusOK, gin.H{ - "error": err.Error(), - }) + sendError(c, err) return } @@ -78,24 +71,13 @@ func login(c *gin.Context) { authController, err := controller.MakeAuthController() if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": err.Error(), - }) + sendError(c, err) return } - token, ok, err := authController.Login(transaction.Username, transaction.Password) + token, err := authController.Login(transaction.Username, transaction.Password) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "error": err.Error(), - }) - return - } - - if !ok { - c.JSON(http.StatusOK, gin.H{ - "error": "bad credentials", - }) + sendError(c, err) return } @@ -109,18 +91,14 @@ func login(c *gin.Context) { func logout(c *gin.Context) { authController, err := controller.MakeAuthController() if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": err.Error(), - }) + sendError(c, err) return } token, _ := c.Get(SESSION_COOKIE) // must exist after isLoggedIn err = authController.Logout(token.(string)) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": err.Error(), - }) + sendError(c, err) return } @@ -133,17 +111,13 @@ func logout(c *gin.Context) { func bump(c *gin.Context) { authController, err := controller.MakeAuthController() if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": err.Error(), - }) + sendError(c, err) return } token, _ := c.Get(SESSION_COOKIE) if err = authController.Bump(token.(string)); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": err.Error(), - }) + sendError(c, err) return } diff --git a/api/endpoints.go b/api/endpoints.go index d651d8e..5dcd406 100644 --- a/api/endpoints.go +++ b/api/endpoints.go @@ -18,6 +18,10 @@ func Listen(address string, base string) error { auth.GET("logout", isLoggedIn, logout) auth.GET("bump", isLoggedIn, bump) + user := api.Group("user") + user.Use(isLoggedIn) + user.GET("info/:username", userInfo) + server := &http.Server{ Addr: address, Handler: router, diff --git a/api/errors.go b/api/errors.go new file mode 100644 index 0000000..64fb063 --- /dev/null +++ b/api/errors.go @@ -0,0 +1,12 @@ +package api + +import ( + "git.tek.govt.hu/dowerx/chat/server/util" + "github.com/gin-gonic/gin" +) + +func sendError(c *gin.Context, err *util.ChatError) { + c.JSON(err.Status(), gin.H{ + "error": err.ErrorFromCode(), + }) +} diff --git a/api/user.go b/api/user.go new file mode 100644 index 0000000..b24b2b0 --- /dev/null +++ b/api/user.go @@ -0,0 +1,31 @@ +package api + +import ( + "net/http" + + "git.tek.govt.hu/dowerx/chat/server/controller" + "github.com/gin-gonic/gin" +) + +const USERNAME_PARAM string = "username" + +func userInfo(c *gin.Context) { + username := c.Param(USERNAME_PARAM) + + userController, err := controller.MakeUserController() + if err != nil { + sendError(c, err) + return + } + + user, err := userController.GetUser(username) + if err != nil { + sendError(c, err) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "user found", + "user": user, + }) +} diff --git a/controller/AuthController.go b/controller/AuthController.go index 4a92a89..cab5067 100644 --- a/controller/AuthController.go +++ b/controller/AuthController.go @@ -3,11 +3,11 @@ package controller import ( "crypto/rand" "encoding/base64" - "errors" "git.tek.govt.hu/dowerx/chat/server/config" "git.tek.govt.hu/dowerx/chat/server/dao" "git.tek.govt.hu/dowerx/chat/server/model" + "git.tek.govt.hu/dowerx/chat/server/util" "golang.org/x/crypto/bcrypt" ) @@ -23,7 +23,7 @@ const ( TOKEN_LENGTH int = 32 ) -func (c *AuthController) init() error { +func (c *AuthController) init() *util.ChatError { userDAO, err := dao.MakeUserDAO() c.userDAO = userDAO if err != nil { @@ -39,22 +39,23 @@ func (c *AuthController) init() error { return nil } -func (c AuthController) Register(username string, password string, repeatPassword string) error { +func (c AuthController) Register(username string, password string, repeatPassword string) *util.ChatError { if len(username) < MIN_USERNAME_LENGTH { - return errors.New("username too short") + return &util.ChatError{Message: "", Code: util.USERNAME_TOO_SHORT} } + if len(password) < MIN_PASSWORD_LENGTH { - return errors.New("password too short") + return &util.ChatError{Message: "", Code: util.PASSWORD_TOO_SHORT} } if password != repeatPassword { - return errors.New("passwords don't match") + return &util.ChatError{Message: "", Code: util.PASSWORDS_DONT_MATCH} } hash, err := bcrypt.GenerateFromPassword([]byte(password), HASH_COST) if err != nil { - return err + return util.MakeError(err, util.GENERAL_ERROR) } return c.userDAO.Create(model.User{ @@ -73,38 +74,38 @@ func generateToken(length int) (string, error) { return base64.URLEncoding.EncodeToString(b), nil } -func (c AuthController) Login(username string, password string) (string, bool, error) { +func (c AuthController) Login(username string, password string) (string, *util.ChatError) { user, err := c.userDAO.Read(model.User{Username: username}) if err != nil { - return "", false, err + return "", err } if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil { - return "", false, errors.New("wrong password") + return "", &util.ChatError{Message: "", Code: util.WRONG_PASSWORD} } - token, err := generateToken(TOKEN_LENGTH) - if err != nil { - return "", false, err + token, tokenErr := generateToken(TOKEN_LENGTH) + if tokenErr != nil { + return "", util.MakeError(err, util.GENERAL_ERROR) } err = c.sessionDAO.DeleteAllByID(user.ID) if err != nil { - return "", false, err + return "", err } err = c.sessionDAO.Set(token, user.ID) if err != nil { - return "", false, err + return "", err } - return token, true, nil + return token, nil } -func (c AuthController) Logout(token string) error { +func (c AuthController) Logout(token string) *util.ChatError { return c.sessionDAO.Delete(token) } -func (c AuthController) Bump(token string) error { +func (c AuthController) Bump(token string) *util.ChatError { return c.sessionDAO.Bump(token, config.GetConfig().API.TokenLife) } diff --git a/controller/Factory.go b/controller/Factory.go index e4aa4a5..9ea8190 100644 --- a/controller/Factory.go +++ b/controller/Factory.go @@ -1,7 +1,15 @@ package controller -func MakeAuthController() (AuthController, error) { +import "git.tek.govt.hu/dowerx/chat/server/util" + +func MakeAuthController() (AuthController, *util.ChatError) { authController := &AuthController{} err := authController.init() return *authController, err } + +func MakeUserController() (UserController, *util.ChatError) { + userController := &UserController{} + err := userController.init() + return *userController, err +} diff --git a/controller/IController.go b/controller/IController.go index 830aacf..9a95e64 100644 --- a/controller/IController.go +++ b/controller/IController.go @@ -1,5 +1,7 @@ package controller +import "git.tek.govt.hu/dowerx/chat/server/util" + type IController interface { - init() error + init() *util.ChatError } diff --git a/controller/UserController.go b/controller/UserController.go new file mode 100644 index 0000000..8f137e1 --- /dev/null +++ b/controller/UserController.go @@ -0,0 +1,25 @@ +package controller + +import ( + "git.tek.govt.hu/dowerx/chat/server/dao" + "git.tek.govt.hu/dowerx/chat/server/model" + "git.tek.govt.hu/dowerx/chat/server/util" +) + +type UserController struct { + userDAO dao.IUserDAO +} + +func (c *UserController) init() *util.ChatError { + userDAO, err := dao.MakeUserDAO() + c.userDAO = userDAO + if err != nil { + return err + } + + return nil +} + +func (c UserController) GetUser(username string) (model.User, *util.ChatError) { + return c.userDAO.Read(model.User{Username: username}) +} diff --git a/dao/Factory.go b/dao/Factory.go index dac3a15..39d793f 100644 --- a/dao/Factory.go +++ b/dao/Factory.go @@ -3,15 +3,16 @@ package dao import ( "git.tek.govt.hu/dowerx/chat/server/dao/postgres" "git.tek.govt.hu/dowerx/chat/server/dao/valkey" + "git.tek.govt.hu/dowerx/chat/server/util" ) -func MakeUserDAO() (IUserDAO, error) { +func MakeUserDAO() (IUserDAO, *util.ChatError) { dao := &postgres.UserDAOPG{} err := dao.Init() return dao, err } -func MakeSessionDAO() (ISessionDAO, error) { +func MakeSessionDAO() (ISessionDAO, *util.ChatError) { dao := &valkey.SessionDAOVK{} err := dao.Init() return dao, err diff --git a/dao/IDAO.go b/dao/IDAO.go index 5e86406..f2264cd 100644 --- a/dao/IDAO.go +++ b/dao/IDAO.go @@ -1,5 +1,7 @@ package dao +import "git.tek.govt.hu/dowerx/chat/server/util" + type IDAO interface { - Init() error + Init() *util.ChatError } diff --git a/dao/ISessionDAO.go b/dao/ISessionDAO.go index 307034a..b632863 100644 --- a/dao/ISessionDAO.go +++ b/dao/ISessionDAO.go @@ -1,9 +1,11 @@ package dao +import "git.tek.govt.hu/dowerx/chat/server/util" + type ISessionDAO interface { - Set(token string, id int) error - Get(token string) (int, error) - Delete(token string) error - DeleteAllByID(id int) error - Bump(token string, time int) error + Set(token string, id int) *util.ChatError + Get(token string) (int, *util.ChatError) + Delete(token string) *util.ChatError + DeleteAllByID(id int) *util.ChatError + Bump(token string, time int) *util.ChatError } diff --git a/dao/IUserDAO.go b/dao/IUserDAO.go index 4c37772..5879372 100644 --- a/dao/IUserDAO.go +++ b/dao/IUserDAO.go @@ -1,11 +1,14 @@ package dao -import "git.tek.govt.hu/dowerx/chat/server/model" +import ( + "git.tek.govt.hu/dowerx/chat/server/model" + "git.tek.govt.hu/dowerx/chat/server/util" +) type IUserDAO interface { - Create(user model.User) error - Read(user model.User) (model.User, error) - List() ([]model.User, error) - Update(user model.User) error - Delete(user model.User) error + Create(user model.User) *util.ChatError + Read(user model.User) (model.User, *util.ChatError) + List() ([]model.User, *util.ChatError) + Update(user model.User) *util.ChatError + Delete(user model.User) *util.ChatError } diff --git a/dao/postgres/Connection.go b/dao/postgres/Connection.go index c36d67d..e55c19c 100644 --- a/dao/postgres/Connection.go +++ b/dao/postgres/Connection.go @@ -4,18 +4,19 @@ import ( "fmt" "git.tek.govt.hu/dowerx/chat/server/config" + "git.tek.govt.hu/dowerx/chat/server/util" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" ) var db *sqlx.DB -func getDatabase() (*sqlx.DB, error) { +func getDatabase() (*sqlx.DB, *util.ChatError) { if db == nil { cfg := config.GetConfig() newDB, err := sqlx.Connect("postgres", fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", cfg.Database.Host, cfg.Database.Port, cfg.Database.User, cfg.Database.Password, cfg.Database.DBname)) if err != nil { - return nil, err + return nil, util.MakeError(err, util.DATABASE_CONNECTION_FAULT) } db = newDB } diff --git a/dao/postgres/UserDAO.go b/dao/postgres/UserDAO.go index 8fa2315..df37002 100644 --- a/dao/postgres/UserDAO.go +++ b/dao/postgres/UserDAO.go @@ -1,9 +1,8 @@ package postgres import ( - "errors" - "git.tek.govt.hu/dowerx/chat/server/model" + "git.tek.govt.hu/dowerx/chat/server/util" "github.com/jmoiron/sqlx" ) @@ -12,13 +11,13 @@ type UserDAOPG struct { } // Create a new user -func (d UserDAOPG) Create(user model.User) error { +func (d UserDAOPG) Create(user model.User) *util.ChatError { _, err := d.db.NamedExec(`call add_user(:username, :password_hash)`, &user) - return err + return util.MakeError(err, util.DATABASE_QUERY_FAULT) } // Read returns a user by ID if ID != 0, else by Username -func (d UserDAOPG) Read(user model.User) (model.User, error) { +func (d UserDAOPG) Read(user model.User) (model.User, *util.ChatError) { var rows *sqlx.Rows var err error if user.ID != 0 { @@ -28,22 +27,22 @@ func (d UserDAOPG) Read(user model.User) (model.User, error) { } if err != nil { - return user, err + return user, util.MakeError(err, util.DATABASE_QUERY_FAULT) } if !rows.Next() { - return user, errors.New("no such user") + return user, util.MakeError(err, util.USER_NOT_FOUND) } err = rows.StructScan(&user) - return user, err + return user, util.MakeError(err, util.DATABASE_QUERY_FAULT) } // List all users -func (d UserDAOPG) List() ([]model.User, error) { +func (d UserDAOPG) List() ([]model.User, *util.ChatError) { rows, err := d.db.Queryx(`select * from "user" order by "username"`) if err != nil { - return nil, err + return nil, util.MakeError(err, util.DATABASE_QUERY_FAULT) } users := make([]model.User, 0) @@ -59,17 +58,17 @@ func (d UserDAOPG) List() ([]model.User, error) { users = append(users, user) } - return users, err + return users, util.MakeError(err, util.DATABASE_QUERY_FAULT) } // Update sets all the fields of the User with the given ID -func (d UserDAOPG) Update(user model.User) error { +func (d UserDAOPG) Update(user model.User) *util.ChatError { _, err := d.db.NamedExec(`update "user" set "username" = :username, "password_hash" = :password_hash, "status" = :status, "picture" = :picture, "bio" = :bio where "id" = :id`, &user) - return err + return util.MakeError(err, util.DATABASE_QUERY_FAULT) } // Delete removes a user by ID if ID != 0, else by Username -func (d UserDAOPG) Delete(user model.User) error { +func (d UserDAOPG) Delete(user model.User) *util.ChatError { var err error if user.ID != 0 { _, err = d.db.NamedExec(`delete from "user" where "id" = :id`, &user) @@ -77,5 +76,5 @@ func (d UserDAOPG) Delete(user model.User) error { _, err = d.db.NamedExec(`delete from "user" where "username" = :username`, &user) } - return err + return util.MakeError(err, util.DATABASE_QUERY_FAULT) } diff --git a/dao/postgres/pgDAO.go b/dao/postgres/pgDAO.go index 76ac782..99c7fc2 100644 --- a/dao/postgres/pgDAO.go +++ b/dao/postgres/pgDAO.go @@ -1,6 +1,7 @@ package postgres import ( + "git.tek.govt.hu/dowerx/chat/server/util" "github.com/jmoiron/sqlx" ) @@ -8,7 +9,7 @@ type pgDAO struct { db *sqlx.DB } -func (d *pgDAO) Init() error { +func (d *pgDAO) Init() *util.ChatError { conn, err := getDatabase() d.db = conn return err diff --git a/dao/valkey/Connection.go b/dao/valkey/Connection.go index 1d32547..d721952 100644 --- a/dao/valkey/Connection.go +++ b/dao/valkey/Connection.go @@ -2,16 +2,17 @@ package valkey import ( "git.tek.govt.hu/dowerx/chat/server/config" + "git.tek.govt.hu/dowerx/chat/server/util" "github.com/valkey-io/valkey-go" ) var vk valkey.Client = nil -func getClient() (*valkey.Client, error) { +func getClient() (*valkey.Client, *util.ChatError) { if vk == nil { client, err := valkey.NewClient(config.GetConfig().Valkey) vk = client - return &vk, err + return &vk, util.MakeError(err, util.DATABASE_CONNECTION_FAULT) } return &vk, nil diff --git a/dao/valkey/SessionDAO.go b/dao/valkey/SessionDAO.go index 9582f56..bd20476 100644 --- a/dao/valkey/SessionDAO.go +++ b/dao/valkey/SessionDAO.go @@ -5,6 +5,7 @@ import ( "strconv" "git.tek.govt.hu/dowerx/chat/server/config" + "git.tek.govt.hu/dowerx/chat/server/util" ) const SESSION_PREFIX string = "session:" @@ -13,28 +14,28 @@ type SessionDAOVK struct { vkDAO } -func (d SessionDAOVK) Set(token string, id int) error { +func (d SessionDAOVK) Set(token string, id int) *util.ChatError { 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() + return util.MakeError((*d.vk).Do(context.Background(), cmd).Error(), util.DATABASE_QUERY_FAULT) } -func (d SessionDAOVK) Get(token string) (int, error) { +func (d SessionDAOVK) Get(token string) (int, *util.ChatError) { 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 + return 0, util.MakeError(err, util.DATABASE_QUERY_FAULT) } id, err := result.AsInt64() - return int(id), err + return int(id), util.MakeError(err, util.DATABASE_QUERY_FAULT) } -func (d SessionDAOVK) Delete(token string) error { +func (d SessionDAOVK) Delete(token string) *util.ChatError { cmd := (*d.vk).B().Del().Key(SESSION_PREFIX + token).Build() - return (*d.vk).Do(context.Background(), cmd).Error() + return util.MakeError((*d.vk).Do(context.Background(), cmd).Error(), util.DATABASE_QUERY_FAULT) } -func (d SessionDAOVK) DeleteAllByID(id int) error { +func (d SessionDAOVK) DeleteAllByID(id int) *util.ChatError { // iterate all session keys var cursor uint64 = 0 pattern := SESSION_PREFIX + "*" @@ -43,12 +44,12 @@ func (d SessionDAOVK) DeleteAllByID(id int) error { result := (*d.vk).Do(context.Background(), cmd) if err := result.Error(); err != nil { - return err + return util.MakeError(err, util.DATABASE_QUERY_FAULT) } entry, err := result.AsScanEntry() if err != nil { - return err + return util.MakeError(err, util.DATABASE_QUERY_FAULT) } for _, key := range entry.Elements { @@ -57,12 +58,12 @@ func (d SessionDAOVK) DeleteAllByID(id int) error { result := (*d.vk).Do(context.Background(), cmd) if err := result.Error(); err != nil { - return err + return util.MakeError(err, util.DATABASE_QUERY_FAULT) } value, err := result.AsInt64() if err != nil { - return err + return util.MakeError(err, util.DATABASE_QUERY_FAULT) } // check if the value is the same as our id @@ -71,7 +72,7 @@ func (d SessionDAOVK) DeleteAllByID(id int) error { cmd = (*d.vk).B().Del().Key(key).Build() result := (*d.vk).Do(context.Background(), cmd) if err := result.Error(); err != nil { - return err + return util.MakeError(err, util.DATABASE_QUERY_FAULT) } } } @@ -83,7 +84,7 @@ func (d SessionDAOVK) DeleteAllByID(id int) error { return nil } -func (d SessionDAOVK) Bump(token string, time int) error { +func (d SessionDAOVK) Bump(token string, time int) *util.ChatError { cmd := (*d.vk).B().Expire().Key(SESSION_PREFIX + token).Seconds(int64(time)).Build() - return (*d.vk).Do(context.Background(), cmd).Error() + return util.MakeError((*d.vk).Do(context.Background(), cmd).Error(), util.DATABASE_QUERY_FAULT) } diff --git a/dao/valkey/vkDAO.go b/dao/valkey/vkDAO.go index 0c0d870..9ae3a12 100644 --- a/dao/valkey/vkDAO.go +++ b/dao/valkey/vkDAO.go @@ -1,6 +1,7 @@ package valkey import ( + "git.tek.govt.hu/dowerx/chat/server/util" "github.com/valkey-io/valkey-go" ) @@ -8,7 +9,7 @@ type vkDAO struct { vk *valkey.Client } -func (d *vkDAO) Init() error { +func (d *vkDAO) Init() *util.ChatError { client, err := getClient() d.vk = client return err diff --git a/model/Message.go b/model/Message.go index 3323ffc..3ad888d 100644 --- a/model/Message.go +++ b/model/Message.go @@ -4,8 +4,8 @@ import "time" type Message struct { ID int - Sender User - Channel Channel + Sender string // username + Channel Channel // channel.id Time time.Time Content string } diff --git a/model/User.go b/model/User.go index 999afa6..44edd55 100644 --- a/model/User.go +++ b/model/User.go @@ -1,10 +1,10 @@ package model type User struct { - ID int `db:"id"` - Username string `db:"username"` - PasswordHash string `db:"password_hash"` - Status string `db:"status"` - Picture string `db:"picture"` - Bio string `db:"bio"` + ID int `db:"id" json:"-"` + Username string `db:"username" json:"username"` + PasswordHash string `db:"password_hash" json:"-"` + Status string `db:"status" json:"status"` + Picture string `db:"picture" json:"picture"` + Bio string `db:"bio" json:"bio"` } diff --git a/util/errors.go b/util/errors.go new file mode 100644 index 0000000..5e4915e --- /dev/null +++ b/util/errors.go @@ -0,0 +1,69 @@ +package util + +import "net/http" + +type ChatErrorCode int + +// List off all known error codes +const ( + // internalServerError + GENERAL_ERROR ChatErrorCode = iota + DATABASE_CONNECTION_FAULT + DATABASE_QUERY_FAULT + // statusOk + USER_NOT_FOUND + WRONG_PASSWORD + USERNAME_TOO_SHORT + PASSWORD_TOO_SHORT + PASSWORDS_DONT_MATCH +) + +var codeToMessage = map[ChatErrorCode]string{} + +type ChatError struct { + Message string + Code ChatErrorCode +} + +// Error returns the original errors message if not empty, else returns ErrorFromCode() +func (e ChatError) Error() string { + if e.Message != "" { + return e.Message + } else { + return e.ErrorFromCode() + } +} + +// ErrorFromCode returns a string that is safe to show in API responses +func (e *ChatError) ErrorFromCode() string { + message, ok := codeToMessage[e.Code] + if ok { + return message + } else { + return "unknown error code" + } +} + +// Status returns the http status of the error type +func (e *ChatError) Status() int { + switch e.Code { + case USER_NOT_FOUND: + case WRONG_PASSWORD: + case USERNAME_TOO_SHORT: + case PASSWORD_TOO_SHORT: + case PASSWORDS_DONT_MATCH: + return http.StatusOK + default: + return http.StatusInternalServerError + } + return http.StatusInternalServerError +} + +// MakeError makes an error with the given code id err exists +func MakeError(err error, code ChatErrorCode) *ChatError { + if err == nil { + return nil + } + + return &ChatError{Message: err.Error(), Code: code} +}