Compare commits

..

2 Commits

21 changed files with 265 additions and 113 deletions

View File

@ -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
}

View File

@ -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,

12
api/errors.go Normal file
View File

@ -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(),
})
}

31
api/user.go Normal file
View File

@ -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,
})
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -1,5 +1,7 @@
package controller
import "git.tek.govt.hu/dowerx/chat/server/util"
type IController interface {
init() error
init() *util.ChatError
}

View File

@ -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})
}

View File

@ -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

View File

@ -1,5 +1,7 @@
package dao
import "git.tek.govt.hu/dowerx/chat/server/util"
type IDAO interface {
Init() error
Init() *util.ChatError
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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.ChatError{Message: "", Code: 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)
}

View File

@ -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

View File

@ -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

View File

@ -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)
}

View File

@ -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

View File

@ -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
}

View File

@ -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"`
}

83
util/errors.go Normal file
View File

@ -0,0 +1,83 @@
package util
import "net/http"
type ChatErrorCode int
// List off all known error codes
const (
// GENERAL_ERROR for not classified system errros
GENERAL_ERROR ChatErrorCode = iota
DATABASE_CONNECTION_FAULT
DATABASE_QUERY_FAULT
USER_NOT_FOUND
WRONG_PASSWORD
USERNAME_TOO_SHORT
PASSWORD_TOO_SHORT
PASSWORDS_DONT_MATCH
)
var codeToMessage = map[ChatErrorCode]string{
GENERAL_ERROR: "an unexpected error occurred",
DATABASE_CONNECTION_FAULT: "database connection failed",
DATABASE_QUERY_FAULT: "database query failed",
USER_NOT_FOUND: "user not found",
WRONG_PASSWORD: "incorrect password",
USERNAME_TOO_SHORT: "username is too short",
PASSWORD_TOO_SHORT: "password is too short",
PASSWORDS_DONT_MATCH: "passwords do not match",
}
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:
fallthrough
case WRONG_PASSWORD:
fallthrough
case USERNAME_TOO_SHORT:
fallthrough
case PASSWORD_TOO_SHORT:
fallthrough
case PASSWORDS_DONT_MATCH:
return http.StatusOK
default:
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}
}