This commit is contained in:
2024-10-13 13:31:58 +04:00
commit aec8d7ed48
54 changed files with 2827 additions and 0 deletions

View File

@ -0,0 +1,76 @@
package http
import (
"github.com/aykhans/oh-my-chat/internal/adapter/handlers/http/middlewares"
"github.com/gofiber/contrib/websocket"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/recover"
)
type Handlers struct {
userHandler *UserHandler
authHandler *AuthHandler
chatHandler *ChatHandler
}
type Middlewares struct {
authMiddleware *middlewares.AuthMiddleware
wsMiddleware *middlewares.WSMiddleware
}
func NewApp(
isDev bool,
corsAllowedOrigins string,
authMiddleware *middlewares.AuthMiddleware,
wsMiddleware *middlewares.WSMiddleware,
userHandler *UserHandler,
authHandler *AuthHandler,
chatHandler *ChatHandler,
) *fiber.App {
handlers := &Handlers{
userHandler: userHandler,
authHandler: authHandler,
chatHandler: chatHandler,
}
middlewares := &Middlewares{
authMiddleware: authMiddleware,
wsMiddleware: wsMiddleware,
}
app := fiber.New()
if !isDev {
app.Use(recover.New())
app.Use(cors.New(cors.Config{
AllowOrigins: corsAllowedOrigins,
}))
}
router := app.Group("/api")
setV1Routers(router, handlers, middlewares)
return app
}
func setV1Routers(
router fiber.Router,
handlers *Handlers,
middlewares *Middlewares,
) {
router = router.Group("/v1")
{ // User routes
user := router.Group("/user")
user.Post("/register", handlers.userHandler.Register)
}
{ // Auth routes
auth := router.Group("/auth")
auth.Post("/login", handlers.authHandler.Login)
}
{ // Chat routes
chat := router.Group("/chat")
chat.Use("/ws", middlewares.authMiddleware.IsUser, middlewares.wsMiddleware.Upgrade)
chat.Get("/ws", websocket.New(handlers.chatHandler.Connect))
}
}

View File

@ -0,0 +1,50 @@
package http
import (
"github.com/aykhans/oh-my-chat/internal/core/port"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
)
type AuthHandler struct {
authService port.AuthService
validator *validator.Validate
}
func NewAuthHandler(authService port.AuthService, validator *validator.Validate) *AuthHandler {
return &AuthHandler{authService, validator}
}
type loginRequest struct {
Username string `json:"username"`
Email string `json:"email" validate:"email"`
Password string `json:"password" validate:"required"`
}
func (authHandler *AuthHandler) Login(ctx *fiber.Ctx) error {
loginBody := new(loginRequest)
if err := ctx.BodyParser(loginBody); err != nil {
return invalidRequestBodyResponse(ctx)
}
if err := authHandler.validator.Struct(loginBody); err != nil {
return validationErrorResponse(ctx, err)
}
loginField := loginBody.Email
loginFunc := authHandler.authService.LoginByEmail
if loginField == "" {
if loginBody.Username == "" {
return fiber.NewError(fiber.StatusBadRequest, "email or username is required")
}
loginField = loginBody.Username
loginFunc = authHandler.authService.LoginByUsername
}
serviceCtx := ctx.Context()
token, err := loginFunc(serviceCtx, loginField, loginBody.Password)
if err != nil {
return fiber.NewError(fiber.StatusUnauthorized, err.Error())
}
return ctx.JSON(fiber.Map{"token": token})
}

View File

@ -0,0 +1,127 @@
package http
import (
"context"
"encoding/json"
"time"
"github.com/aykhans/oh-my-chat/internal/adapter/logger"
"github.com/aykhans/oh-my-chat/internal/core/domain"
"github.com/aykhans/oh-my-chat/internal/core/port"
"github.com/go-playground/validator/v10"
"github.com/gofiber/contrib/websocket"
"github.com/google/uuid"
)
var log = logger.NewStdLogger()
type ChatHandler struct {
messageService port.MessageService
validator *validator.Validate
}
func NewChatHandler(
messageService port.MessageService,
validator *validator.Validate,
) *ChatHandler {
return &ChatHandler{messageService, validator}
}
type messageReuqest struct {
Content string `json:"content" validate:"required"`
To string `json:"to" validate:"required"`
}
func (chatHandler *ChatHandler) Connect(conn *websocket.Conn) {
authPayload := getAuthPayloadInWS(conn)
streamReceiveMessageCtx, streamReceiveMessageCtxCancel := context.WithCancel(context.Background())
consumeMessageChan := make(chan *domain.StreamMessage)
wsMessageChan := make(chan *domain.Message)
go readMessageFromWS(
conn,
chatHandler.validator,
authPayload.UserID,
wsMessageChan,
)
go func() {
err := chatHandler.messageService.ReceiveMessage(
streamReceiveMessageCtx,
authPayload.UserID,
consumeMessageChan,
)
if err != nil {
return
}
}()
defer func() {
streamReceiveMessageCtxCancel()
}()
var err error
for {
select {
case streamReceivedMessage := <-consumeMessageChan:
messageBytes, _ := json.Marshal(streamReceivedMessage.Message)
if err = conn.WriteMessage(websocket.TextMessage, messageBytes); err != nil {
return
}
err := streamReceivedMessage.Commit()
if err != nil {
log.Error("Stream message commit error", "error", err.Error())
return
}
case wsMessage := <-wsMessageChan:
if wsMessage == nil {
return
}
streamSendMessageCtx := context.Background()
chatHandler.messageService.SendMessage(streamSendMessageCtx, wsMessage)
}
}
}
func readMessageFromWS(
conn *websocket.Conn,
validator *validator.Validate,
userID uuid.UUID,
messageChan chan<- *domain.Message,
) {
var (
wsReceivedMessageType int
wsReceivedMessage []byte
err error
)
for {
if wsReceivedMessageType, wsReceivedMessage, err = conn.ReadMessage(); err != nil {
messageChan <- nil
break
}
if wsReceivedMessageType == websocket.TextMessage {
messageBody := new(messageReuqest)
if err := json.Unmarshal(wsReceivedMessage, &messageBody); err != nil {
messageChan <- nil
break
}
if err := validator.Struct(messageBody); err != nil {
messageChan <- nil
break
}
timestamp := time.Now()
messageChan <- &domain.Message{
UserID: userID,
ChatID: messageBody.To,
Content: messageBody.Content,
Timestamp: timestamp,
}
}
}
}

View File

@ -0,0 +1,23 @@
package http
import (
"github.com/aykhans/oh-my-chat/internal/core/domain"
"github.com/gofiber/contrib/websocket"
// "github.com/gofiber/fiber/v2"
)
// func getAuthPayload(ctx *fiber.Ctx) *domain.AuthPayload {
// payload := ctx.Locals("authPayload")
// if payload == nil {
// return nil
// }
// return payload.(*domain.AuthPayload)
// }
func getAuthPayloadInWS(conn *websocket.Conn) *domain.AuthPayload {
payload := conn.Locals("authPayload")
if payload == nil {
return nil
}
return payload.(*domain.AuthPayload)
}

View File

@ -0,0 +1,33 @@
package middlewares
import (
"fmt"
"github.com/aykhans/oh-my-chat/internal/core/domain"
"github.com/aykhans/oh-my-chat/internal/core/port"
"github.com/gofiber/fiber/v2"
)
type AuthMiddleware struct {
tokenService port.TokenService
}
func NewAuthMiddleware(tokenService port.TokenService) *AuthMiddleware {
return &AuthMiddleware{tokenService}
}
func (authMiddleware *AuthMiddleware) IsUser(ctx *fiber.Ctx) error {
token := ctx.Get("Authorization")
if token == "" {
return fiber.NewError(fiber.StatusBadRequest, "Authorization header is required")
}
payload, err := authMiddleware.tokenService.VerifyToken(token[7:])
if err != nil {
if err == domain.ErrInternal {
fiber.NewError(fiber.StatusInternalServerError, "Internal Server Error")
}
return fiber.NewError(fiber.StatusUnauthorized, fmt.Sprintf("Unauthorized: %v", err))
}
ctx.Locals("authPayload", payload)
return ctx.Next()
}

View File

@ -0,0 +1,20 @@
package middlewares
import (
"github.com/gofiber/contrib/websocket"
"github.com/gofiber/fiber/v2"
)
type WSMiddleware struct{}
func NewWSMiddleware() *WSMiddleware {
return &WSMiddleware{}
}
func (wsMiddleware *WSMiddleware) Upgrade(ctx *fiber.Ctx) error {
if websocket.IsWebSocketUpgrade(ctx) {
ctx.Locals("allowed", true)
return ctx.Next()
}
return fiber.ErrUpgradeRequired
}

View File

@ -0,0 +1,29 @@
package http
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
)
func notFoundResponse(ctx *fiber.Ctx, err ...error) error {
errMsg := "Not found"
if len(err) > 0 {
errMsg = err[0].Error()
}
return ctx.Status(fiber.StatusNotFound).JSON(
fiber.Map{"error": errMsg},
)
}
func invalidRequestBodyResponse(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusBadRequest).JSON(
fiber.Map{"error": "Invalid request body"},
)
}
func validationErrorResponse(ctx *fiber.Ctx, err error) error {
errs := err.(validator.ValidationErrors)
return ctx.Status(fiber.StatusBadRequest).JSON(
fiber.Map{"errors": validationErrorFormater(errs)},
)
}

View File

@ -0,0 +1,57 @@
package http
import (
"github.com/aykhans/oh-my-chat/internal/core/domain"
"github.com/aykhans/oh-my-chat/internal/core/port"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
)
type UserHandler struct {
userService port.UserService
validator *validator.Validate
}
func NewUserHandler(svc port.UserService, validator *validator.Validate) *UserHandler {
return &UserHandler{svc, validator}
}
type registerRequest struct {
Username string `json:"username" validate:"required,max=50"`
Email string `json:"email" validate:"email"`
Password string `json:"password" validate:"min=8,max=72"`
}
func (userHandler *UserHandler) Register(ctx *fiber.Ctx) error {
registerBody := new(registerRequest)
if err := ctx.BodyParser(registerBody); err != nil {
return invalidRequestBodyResponse(ctx)
}
if err := userHandler.validator.Struct(registerBody); err != nil {
return validationErrorResponse(ctx, err)
}
user := domain.User{
Username: registerBody.Username,
Email: registerBody.Email,
Password: registerBody.Password,
}
serviceCtx := ctx.Context()
_, err := userHandler.userService.Register(serviceCtx, &user)
if err != nil {
if err == domain.ErrUsernameExists || err == domain.ErrEmailExists {
return notFoundResponse(ctx, err)
}
return fiber.ErrInternalServerError
}
return ctx.Status(fiber.StatusCreated).JSON(
fiber.Map{
"ID": user.ID,
"Username": user.Username,
"Email": user.Email,
},
)
}

View File

@ -0,0 +1,60 @@
package http
import (
"fmt"
"reflect"
"github.com/go-playground/validator/v10"
)
type fieldError struct {
Field string
Message string
}
func NewValidator() *validator.Validate {
validate := validator.New()
validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
if fld.Tag.Get("validation_name") != "" {
return fld.Tag.Get("validation_name")
} else {
return fld.Tag.Get("json")
}
})
return validate
}
func validationErrorFormater(errs validator.ValidationErrors) []fieldError {
fieldErrs := make([]fieldError, 0)
if errs != nil {
for _, err := range errs {
fieldErrs = append(
fieldErrs,
fieldError{
Field: err.Field(),
Message: msgForTag(err.Tag(), err.Field(), err.Param()),
},
)
}
return fieldErrs
}
return nil
}
func msgForTag(tag, field, param string) string {
switch tag {
case "required":
return fmt.Sprintf("%s is required", field)
case "email":
return "email is invalid"
case "min":
return fmt.Sprintf("The length of %s must be at least %s", field, param)
case "max":
return fmt.Sprintf("The length of %s must be at most %s", field, param)
default:
return fmt.Sprintf("%s is invalid", field)
}
}