init
This commit is contained in:
76
internal/adapter/handlers/http/app.go
Normal file
76
internal/adapter/handlers/http/app.go
Normal 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))
|
||||
}
|
||||
}
|
50
internal/adapter/handlers/http/auth.go
Normal file
50
internal/adapter/handlers/http/auth.go
Normal 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})
|
||||
}
|
127
internal/adapter/handlers/http/chat.go
Normal file
127
internal/adapter/handlers/http/chat.go
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
23
internal/adapter/handlers/http/helper.go
Normal file
23
internal/adapter/handlers/http/helper.go
Normal 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)
|
||||
}
|
33
internal/adapter/handlers/http/middlewares/auth.go
Normal file
33
internal/adapter/handlers/http/middlewares/auth.go
Normal 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()
|
||||
}
|
20
internal/adapter/handlers/http/middlewares/ws.go
Normal file
20
internal/adapter/handlers/http/middlewares/ws.go
Normal 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
|
||||
}
|
29
internal/adapter/handlers/http/responses.go
Normal file
29
internal/adapter/handlers/http/responses.go
Normal 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)},
|
||||
)
|
||||
}
|
57
internal/adapter/handlers/http/user.go
Normal file
57
internal/adapter/handlers/http/user.go
Normal 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,
|
||||
},
|
||||
)
|
||||
}
|
60
internal/adapter/handlers/http/validator.go
Normal file
60
internal/adapter/handlers/http/validator.go
Normal 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)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user