From f5d9fe5223988a0b6980cb0250691f04bab89831 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 7 May 2026 15:59:58 +0700 Subject: [PATCH] Implement JWT session and token --- config/configs.go | 8 + config/jwt.go | 6 + infra/development.yaml | 3 + internal/app/app.go | 13 +- internal/contract/self_order_contract.go | 18 +- internal/handler/self_order_handler.go | 167 +++++++++++++----- .../middleware/self_order_auth_middleware.go | 86 +++++++++ internal/router/router.go | 16 +- internal/util/jwt_util.go | 53 ++++++ 9 files changed, 309 insertions(+), 61 deletions(-) create mode 100644 internal/middleware/self_order_auth_middleware.go diff --git a/config/configs.go b/config/configs.go index 6a8dfe0..a40f1ba 100644 --- a/config/configs.go +++ b/config/configs.go @@ -79,6 +79,14 @@ func (c *Config) GetCustomerJWTExpiresTTL() int { return c.Jwt.Customer.ExpiresTTL } +func (c *Config) GetSelfOrderJWTSecret() string { + return c.Jwt.SelfOrder.Secret +} + +func (c *Config) GetSelfOrderJWTExpiresTTL() int { + return c.Jwt.SelfOrder.ExpiresTTL +} + func (c *Config) LogLevel() string { return c.Log.LogLevel } diff --git a/config/jwt.go b/config/jwt.go index 858c28e..19f268c 100644 --- a/config/jwt.go +++ b/config/jwt.go @@ -4,6 +4,12 @@ type Jwt struct { Token Token `mapstructure:"token"` RefreshToken RefreshToken `mapstructure:"refresh_token"` Customer Customer `mapstructure:"customer"` + SelfOrder SelfOrder `mapstructure:"self_order"` +} + +type SelfOrder struct { + ExpiresTTL int `mapstructure:"expires-ttl"` + Secret string `mapstructure:"secret"` } type Token struct { diff --git a/infra/development.yaml b/infra/development.yaml index 6880e35..e5cb81e 100644 --- a/infra/development.yaml +++ b/infra/development.yaml @@ -13,6 +13,9 @@ jwt: customer: expires-ttl: 7776000 secret: "z8d5TlFCT58Q$i0%S^2M&3WtE$PMgd" + self_order: + expires-ttl: 120 + secret: "S3lf0rd3r_S3ss10n_S3cr3t_K3y_2024" postgresql: host: 62.72.45.250 diff --git a/internal/app/app.go b/internal/app/app.go index 1596bb2..ff419fb 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -51,6 +51,8 @@ func (a *App) Initialize(cfg *config.Config) error { repos.tableRepo, repos.outletRepo, repos.userRepo, + cfg.GetSelfOrderJWTSecret(), + cfg.GetSelfOrderJWTExpiresTTL(), ) a.router = router.NewRouter( @@ -114,6 +116,7 @@ func (a *App) Initialize(cfg *config.Config) error { services.spinGameService, middleware.customerAuthMiddleware, selfOrderHandler, + middleware.selfOrderAuthMiddleware, ) return nil @@ -448,14 +451,16 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con } type middlewares struct { - authMiddleware *middleware.AuthMiddleware - customerAuthMiddleware *middleware.CustomerAuthMiddleware + authMiddleware *middleware.AuthMiddleware + customerAuthMiddleware *middleware.CustomerAuthMiddleware + selfOrderAuthMiddleware *middleware.SelfOrderAuthMiddleware } func (a *App) initMiddleware(services *services, cfg *config.Config) *middlewares { return &middlewares{ - authMiddleware: middleware.NewAuthMiddleware(services.authService), - customerAuthMiddleware: middleware.NewCustomerAuthMiddleware(cfg.GetCustomerJWTSecret()), + authMiddleware: middleware.NewAuthMiddleware(services.authService), + customerAuthMiddleware: middleware.NewCustomerAuthMiddleware(cfg.GetCustomerJWTSecret()), + selfOrderAuthMiddleware: middleware.NewSelfOrderAuthMiddleware(cfg.GetSelfOrderJWTSecret()), } } diff --git a/internal/contract/self_order_contract.go b/internal/contract/self_order_contract.go index 8cea50f..2de185b 100644 --- a/internal/contract/self_order_contract.go +++ b/internal/contract/self_order_contract.go @@ -4,12 +4,18 @@ import ( "github.com/google/uuid" ) -type SelfOrderMenuRequest struct { +type SelfOrderSessionRequest struct { TableID uuid.UUID `json:"table_id" validate:"required"` CustomerName string `json:"customer_name" validate:"required"` Phone *string `json:"phone,omitempty"` } +type SelfOrderSessionResponse struct { + Token string `json:"token"` + ExpiresAt int64 `json:"expires_at"` + TableID uuid.UUID `json:"table_id"` +} + type SelfOrderMenuResponse struct { OutletName string `json:"outlet_name"` TableName string `json:"table_name"` @@ -40,10 +46,8 @@ type SelfOrderMenuVariant struct { } type SelfOrderCreateOrderRequest struct { - TableID uuid.UUID `json:"table_id" validate:"required"` - CustomerName string `json:"customer_name" validate:"required"` - Phone *string `json:"phone,omitempty"` - OrderItems []SelfOrderCreateOrderItem `json:"order_items" validate:"required,min=1,dive"` + Phone *string `json:"phone,omitempty"` + OrderItems []SelfOrderCreateOrderItem `json:"order_items" validate:"required,min=1,dive"` } type SelfOrderCreateOrderItem struct { @@ -53,10 +57,6 @@ type SelfOrderCreateOrderItem struct { Notes *string `json:"notes,omitempty"` } -type SelfOrderListCategoriesRequest struct { - TableID uuid.UUID `form:"table_id" validate:"required"` -} - type SelfOrderCategoryItem struct { ID uuid.UUID `json:"id"` Name string `json:"name"` diff --git a/internal/handler/self_order_handler.go b/internal/handler/self_order_handler.go index 563f2c7..8021230 100644 --- a/internal/handler/self_order_handler.go +++ b/internal/handler/self_order_handler.go @@ -19,12 +19,14 @@ import ( ) type SelfOrderHandler struct { - orderService service.OrderService - categoryService service.CategoryService - productService service.ProductService - tableRepo repository.TableRepositoryInterface - outletRepo processor.OutletRepository - userRepo processor.UserRepository + orderService service.OrderService + categoryService service.CategoryService + productService service.ProductService + tableRepo repository.TableRepositoryInterface + outletRepo processor.OutletRepository + userRepo processor.UserRepository + selfOrderJWTSecret string + selfOrderJWTTTL int } func NewSelfOrderHandler( @@ -34,44 +36,120 @@ func NewSelfOrderHandler( tableRepo repository.TableRepositoryInterface, outletRepo processor.OutletRepository, userRepo processor.UserRepository, + selfOrderJWTSecret string, + selfOrderJWTTTL int, ) *SelfOrderHandler { return &SelfOrderHandler{ - orderService: orderService, - categoryService: categoryService, - productService: productService, - tableRepo: tableRepo, - outletRepo: outletRepo, - userRepo: userRepo, + orderService: orderService, + categoryService: categoryService, + productService: productService, + tableRepo: tableRepo, + outletRepo: outletRepo, + userRepo: userRepo, + selfOrderJWTSecret: selfOrderJWTSecret, + selfOrderJWTTTL: selfOrderJWTTTL, } } -func (h *SelfOrderHandler) GetMenu(c *gin.Context) { +func (h *SelfOrderHandler) getSelfOrderContext(c *gin.Context) (uuid.UUID, string, string, error) { + tableIDStr, _ := c.Get("self_order_table_id") + customerName, _ := c.Get("self_order_customer_name") + phoneStr, _ := c.Get("self_order_phone") + + tableIDStrTyped, ok := tableIDStr.(string) + if !ok || tableIDStrTyped == "" { + return uuid.Nil, "", "", fmt.Errorf("table_id not found in context") + } + tableID, err := uuid.Parse(tableIDStrTyped) + if err != nil { + return uuid.Nil, "", "", fmt.Errorf("invalid table_id in token") + } + + nameTyped, ok := customerName.(string) + if !ok || nameTyped == "" { + return uuid.Nil, "", "", fmt.Errorf("customer_name not found in context") + } + + phone, _ := phoneStr.(string) + + return tableID, nameTyped, phone, nil +} + +func (h *SelfOrderHandler) CreateSession(c *gin.Context) { ctx := c.Request.Context() - var req contract.SelfOrderMenuRequest + var req contract.SelfOrderSessionRequest if err := c.ShouldBindJSON(&req); err != nil { - logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::GetMenu -> request binding failed") + logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::CreateSession -> request binding failed") util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()), - }), "SelfOrderHandler::GetMenu") + }), "SelfOrderHandler::CreateSession") return } if req.TableID == uuid.Nil { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "table_id is required"), - }), "SelfOrderHandler::GetMenu") + }), "SelfOrderHandler::CreateSession") return } if req.CustomerName == "" { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "customer_name is required"), - }), "SelfOrderHandler::GetMenu") + }), "SelfOrderHandler::CreateSession") return } table, err := h.tableRepo.GetByID(ctx, req.TableID) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::CreateSession -> table not found") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ + contract.NewResponseError(constants.NotFoundErrorCode, constants.TableEntity, "table not found"), + }), "SelfOrderHandler::CreateSession") + return + } + + if !table.IsActive { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ + contract.NewResponseError(constants.ValidationErrorCode, constants.TableEntity, "table is not active"), + }), "SelfOrderHandler::CreateSession") + return + } + + phone := "" + if req.Phone != nil { + phone = *req.Phone + } + + token, expiresAt, err := util.GenerateSelfOrderSessionToken(req.TableID, req.CustomerName, phone, h.selfOrderJWTSecret, h.selfOrderJWTTTL) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::CreateSession -> failed to generate token") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ + contract.NewResponseError(constants.InternalServerErrorCode, constants.OrderServiceEntity, "failed to create session"), + }), "SelfOrderHandler::CreateSession") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(&contract.SelfOrderSessionResponse{ + Token: token, + ExpiresAt: expiresAt, + TableID: req.TableID, + }), "SelfOrderHandler::CreateSession") +} + +func (h *SelfOrderHandler) GetMenu(c *gin.Context) { + ctx := c.Request.Context() + + tableID, customerName, _, err := h.getSelfOrderContext(c) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ + contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, err.Error()), + }), "SelfOrderHandler::GetMenu") + return + } + + table, err := h.tableRepo.GetByID(ctx, tableID) if err != nil { logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::GetMenu -> table not found") util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ @@ -96,6 +174,8 @@ func (h *SelfOrderHandler) GetMenu(c *gin.Context) { return } + _ = customerName + isActive := true catResp := h.categoryService.ListCategories(ctx, &contract.ListCategoriesRequest{ OrganizationID: &table.OrganizationID, @@ -192,6 +272,14 @@ func (h *SelfOrderHandler) buildMenuResponse( func (h *SelfOrderHandler) CreateOrder(c *gin.Context) { ctx := c.Request.Context() + tableID, customerName, phone, err := h.getSelfOrderContext(c) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ + contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, err.Error()), + }), "SelfOrderHandler::CreateOrder") + return + } + var req contract.SelfOrderCreateOrderRequest if err := c.ShouldBindJSON(&req); err != nil { logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::CreateOrder -> request binding failed") @@ -208,7 +296,7 @@ func (h *SelfOrderHandler) CreateOrder(c *gin.Context) { return } - table, err := h.tableRepo.GetByID(ctx, req.TableID) + table, err := h.tableRepo.GetByID(ctx, tableID) if err != nil { logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::CreateOrder -> table not found") util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ @@ -243,22 +331,27 @@ func (h *SelfOrderHandler) CreateOrder(c *gin.Context) { }) } - metadata := make(map[string]interface{}) - metadata["self_order"] = true - metadata["customer_name"] = req.CustomerName + customerPhone := phone if req.Phone != nil { - metadata["customer_phone"] = *req.Phone + customerPhone = *req.Phone } - tableID := req.TableID + metadata := make(map[string]interface{}) + metadata["self_order"] = true + metadata["customer_name"] = customerName + if customerPhone != "" { + metadata["customer_phone"] = customerPhone + } + + tableIDPtr := tableID modelReq := &models.CreateOrderRequest{ OutletID: table.OutletID, UserID: userID, - TableID: &tableID, + TableID: &tableIDPtr, TableNumber: &table.TableName, OrderType: constants.OrderTypeDineIn, OrderItems: orderItems, - CustomerName: &req.CustomerName, + CustomerName: &customerName, Metadata: metadata, } @@ -276,12 +369,6 @@ func (h *SelfOrderHandler) CreateOrder(c *gin.Context) { } func (h *SelfOrderHandler) validateCreateOrderRequest(req *contract.SelfOrderCreateOrderRequest) error { - if req.TableID == uuid.Nil { - return fmt.Errorf("table_id is required") - } - if req.CustomerName == "" { - return fmt.Errorf("customer_name is required") - } if len(req.OrderItems) == 0 { return fmt.Errorf("at least one order item is required") } @@ -299,23 +386,15 @@ func (h *SelfOrderHandler) validateCreateOrderRequest(req *contract.SelfOrderCre func (h *SelfOrderHandler) ListCategories(c *gin.Context) { ctx := c.Request.Context() - var req contract.SelfOrderListCategoriesRequest - if err := c.ShouldBindQuery(&req); err != nil { - logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::ListCategories -> query binding failed") + tableID, _, _, err := h.getSelfOrderContext(c) + if err != nil { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ - contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()), + contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, err.Error()), }), "SelfOrderHandler::ListCategories") return } - if req.TableID == uuid.Nil { - util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ - contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "table_id is required"), - }), "SelfOrderHandler::ListCategories") - return - } - - table, err := h.tableRepo.GetByID(ctx, req.TableID) + table, err := h.tableRepo.GetByID(ctx, tableID) if err != nil { logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::ListCategories -> table not found") util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ diff --git a/internal/middleware/self_order_auth_middleware.go b/internal/middleware/self_order_auth_middleware.go new file mode 100644 index 0000000..78c332f --- /dev/null +++ b/internal/middleware/self_order_auth_middleware.go @@ -0,0 +1,86 @@ +package middleware + +import ( + "strings" + + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/util" + + "github.com/gin-gonic/gin" +) + +type SelfOrderAuthMiddleware struct { + selfOrderJWTSecret string +} + +func NewSelfOrderAuthMiddleware(selfOrderJWTSecret string) *SelfOrderAuthMiddleware { + return &SelfOrderAuthMiddleware{ + selfOrderJWTSecret: selfOrderJWTSecret, + } +} + +func (m *SelfOrderAuthMiddleware) ValidateSelfOrderToken() gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ + contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "Authorization header is required"), + }), "SelfOrderAuthMiddleware::ValidateSelfOrderToken") + c.Abort() + return + } + + if !strings.HasPrefix(authHeader, "Bearer ") { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ + contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "Invalid authorization header format"), + }), "SelfOrderAuthMiddleware::ValidateSelfOrderToken") + c.Abort() + return + } + + tokenString := strings.TrimPrefix(authHeader, "Bearer ") + if tokenString == "" { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ + contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "Token is required"), + }), "SelfOrderAuthMiddleware::ValidateSelfOrderToken") + c.Abort() + return + } + + claims, err := util.ValidateSelfOrderToken(tokenString, m.selfOrderJWTSecret) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ + contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "Invalid token: "+err.Error()), + }), "SelfOrderAuthMiddleware::ValidateSelfOrderToken") + c.Abort() + return + } + + tableID, ok := claims["table_id"].(string) + if !ok || tableID == "" { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ + contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "table_id not found in token"), + }), "SelfOrderAuthMiddleware::ValidateSelfOrderToken") + c.Abort() + return + } + + customerName, ok := claims["customer_name"].(string) + if !ok || customerName == "" { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ + contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "customer_name not found in token"), + }), "SelfOrderAuthMiddleware::ValidateSelfOrderToken") + c.Abort() + return + } + + phone, _ := claims["phone"].(string) + + c.Set("self_order_table_id", tableID) + c.Set("self_order_customer_name", customerName) + c.Set("self_order_phone", phone) + + c.Next() + } +} diff --git a/internal/router/router.go b/internal/router/router.go index 9a11c20..ca77104 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -49,9 +49,10 @@ type Router struct { selfOrderHandler *handler.SelfOrderHandler authMiddleware *middleware.AuthMiddleware customerAuthMiddleware *middleware.CustomerAuthMiddleware + selfOrderAuthMiddleware *middleware.SelfOrderAuthMiddleware } -func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authService service.AuthService, authMiddleware *middleware.AuthMiddleware, userService *service.UserServiceImpl, userValidator *validator.UserValidatorImpl, organizationService service.OrganizationService, organizationValidator validator.OrganizationValidator, outletService service.OutletService, outletValidator validator.OutletValidator, outletSettingService service.OutletSettingService, categoryService service.CategoryService, categoryValidator validator.CategoryValidator, productService service.ProductService, productValidator validator.ProductValidator, productVariantService service.ProductVariantService, productVariantValidator validator.ProductVariantValidator, inventoryService service.InventoryService, inventoryValidator validator.InventoryValidator, orderService service.OrderService, orderValidator validator.OrderValidator, fileService service.FileService, fileValidator validator.FileValidator, customerService service.CustomerService, customerValidator validator.CustomerValidator, paymentMethodService service.PaymentMethodService, paymentMethodValidator validator.PaymentMethodValidator, analyticsService *service.AnalyticsServiceImpl, reportService service.ReportService, tableService *service.TableServiceImpl, tableValidator *validator.TableValidator, unitService handler.UnitService, ingredientService handler.IngredientService, productRecipeService service.ProductRecipeService, vendorService service.VendorService, vendorValidator validator.VendorValidator, purchaseOrderService service.PurchaseOrderService, purchaseOrderValidator validator.PurchaseOrderValidator, unitConverterService service.IngredientUnitConverterService, unitConverterValidator validator.IngredientUnitConverterValidator, chartOfAccountTypeService service.ChartOfAccountTypeService, chartOfAccountTypeValidator validator.ChartOfAccountTypeValidator, chartOfAccountService service.ChartOfAccountService, chartOfAccountValidator validator.ChartOfAccountValidator, accountService service.AccountService, accountValidator validator.AccountValidator, orderIngredientTransactionService service.OrderIngredientTransactionService, orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator, gamificationService service.GamificationService, gamificationValidator validator.GamificationValidator, rewardService service.RewardService, rewardValidator validator.RewardValidator, campaignService service.CampaignService, campaignValidator validator.CampaignValidator, customerAuthService service.CustomerAuthService, customerAuthValidator validator.CustomerAuthValidator, customerPointsService service.CustomerPointsService, spinGameService service.SpinGameService, customerAuthMiddleware *middleware.CustomerAuthMiddleware, selfOrderHandler *handler.SelfOrderHandler) *Router { +func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authService service.AuthService, authMiddleware *middleware.AuthMiddleware, userService *service.UserServiceImpl, userValidator *validator.UserValidatorImpl, organizationService service.OrganizationService, organizationValidator validator.OrganizationValidator, outletService service.OutletService, outletValidator validator.OutletValidator, outletSettingService service.OutletSettingService, categoryService service.CategoryService, categoryValidator validator.CategoryValidator, productService service.ProductService, productValidator validator.ProductValidator, productVariantService service.ProductVariantService, productVariantValidator validator.ProductVariantValidator, inventoryService service.InventoryService, inventoryValidator validator.InventoryValidator, orderService service.OrderService, orderValidator validator.OrderValidator, fileService service.FileService, fileValidator validator.FileValidator, customerService service.CustomerService, customerValidator validator.CustomerValidator, paymentMethodService service.PaymentMethodService, paymentMethodValidator validator.PaymentMethodValidator, analyticsService *service.AnalyticsServiceImpl, reportService service.ReportService, tableService *service.TableServiceImpl, tableValidator *validator.TableValidator, unitService handler.UnitService, ingredientService handler.IngredientService, productRecipeService service.ProductRecipeService, vendorService service.VendorService, vendorValidator validator.VendorValidator, purchaseOrderService service.PurchaseOrderService, purchaseOrderValidator validator.PurchaseOrderValidator, unitConverterService service.IngredientUnitConverterService, unitConverterValidator validator.IngredientUnitConverterValidator, chartOfAccountTypeService service.ChartOfAccountTypeService, chartOfAccountTypeValidator validator.ChartOfAccountTypeValidator, chartOfAccountService service.ChartOfAccountService, chartOfAccountValidator validator.ChartOfAccountValidator, accountService service.AccountService, accountValidator validator.AccountValidator, orderIngredientTransactionService service.OrderIngredientTransactionService, orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator, gamificationService service.GamificationService, gamificationValidator validator.GamificationValidator, rewardService service.RewardService, rewardValidator validator.RewardValidator, campaignService service.CampaignService, campaignValidator validator.CampaignValidator, customerAuthService service.CustomerAuthService, customerAuthValidator validator.CustomerAuthValidator, customerPointsService service.CustomerPointsService, spinGameService service.SpinGameService, customerAuthMiddleware *middleware.CustomerAuthMiddleware, selfOrderHandler *handler.SelfOrderHandler, selfOrderAuthMiddleware *middleware.SelfOrderAuthMiddleware) *Router { return &Router{ config: cfg, @@ -91,6 +92,7 @@ func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authSer customerAuthMiddleware: customerAuthMiddleware, productVariantHandler: handler.NewProductVariantHandler(productVariantService, productVariantValidator), selfOrderHandler: selfOrderHandler, + selfOrderAuthMiddleware: selfOrderAuthMiddleware, } } @@ -149,9 +151,15 @@ func (r *Router) addAppRoutes(rg *gin.Engine) { selfOrder := v1.Group("/self-order") { - selfOrder.GET("/categories", r.selfOrderHandler.ListCategories) - selfOrder.POST("/menu", r.selfOrderHandler.GetMenu) - selfOrder.POST("/order", r.selfOrderHandler.CreateOrder) + selfOrder.POST("/session", r.selfOrderHandler.CreateSession) + } + + selfOrderProtected := v1.Group("/self-order") + selfOrderProtected.Use(r.selfOrderAuthMiddleware.ValidateSelfOrderToken()) + { + selfOrderProtected.GET("/menu", r.selfOrderHandler.GetMenu) + selfOrderProtected.GET("/categories", r.selfOrderHandler.ListCategories) + selfOrderProtected.POST("/order", r.selfOrderHandler.CreateOrder) } organizations := v1.Group("/organizations") diff --git a/internal/util/jwt_util.go b/internal/util/jwt_util.go index 50df276..a5016d3 100644 --- a/internal/util/jwt_util.go +++ b/internal/util/jwt_util.go @@ -7,6 +7,7 @@ import ( "apskel-pos-be/internal/entities" "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" ) // GenerateCustomerTokens generates access and refresh tokens for customer @@ -85,3 +86,55 @@ func ExtractCustomerIDFromToken(token *jwt.Token) (string, error) { return customerID, nil } + +func GenerateSelfOrderSessionToken(tableID uuid.UUID, customerName string, phone string, secret string, ttlMinutes int) (string, int64, error) { + now := time.Now() + expiresAt := now.Add(time.Duration(ttlMinutes) * time.Minute) + + claims := jwt.MapClaims{ + "table_id": tableID.String(), + "customer_name": customerName, + "phone": phone, + "session_id": uuid.New().String(), + "type": "self_order_access", + "iat": now.Unix(), + "exp": expiresAt.Unix(), + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString([]byte(secret)) + if err != nil { + return "", 0, fmt.Errorf("failed to generate self-order session token: %w", err) + } + + return tokenString, expiresAt.Unix(), nil +} + +func ValidateSelfOrderToken(tokenString string, secret string) (jwt.MapClaims, error) { + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(secret), nil + }) + + if err != nil { + return nil, fmt.Errorf("failed to parse self-order token: %w", err) + } + + if !token.Valid { + return nil, fmt.Errorf("invalid self-order token") + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return nil, fmt.Errorf("invalid self-order token claims") + } + + tokenType, ok := claims["type"].(string) + if !ok || tokenType != "self_order_access" { + return nil, fmt.Errorf("invalid self-order token type") + } + + return claims, nil +}