Compare commits

...

1 Commits

Author SHA1 Message Date
f5d9fe5223 Implement JWT session and token 2026-05-07 15:59:58 +07:00
9 changed files with 309 additions and 61 deletions

View File

@ -79,6 +79,14 @@ func (c *Config) GetCustomerJWTExpiresTTL() int {
return c.Jwt.Customer.ExpiresTTL 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 { func (c *Config) LogLevel() string {
return c.Log.LogLevel return c.Log.LogLevel
} }

View File

@ -4,6 +4,12 @@ type Jwt struct {
Token Token `mapstructure:"token"` Token Token `mapstructure:"token"`
RefreshToken RefreshToken `mapstructure:"refresh_token"` RefreshToken RefreshToken `mapstructure:"refresh_token"`
Customer Customer `mapstructure:"customer"` Customer Customer `mapstructure:"customer"`
SelfOrder SelfOrder `mapstructure:"self_order"`
}
type SelfOrder struct {
ExpiresTTL int `mapstructure:"expires-ttl"`
Secret string `mapstructure:"secret"`
} }
type Token struct { type Token struct {

View File

@ -13,6 +13,9 @@ jwt:
customer: customer:
expires-ttl: 7776000 expires-ttl: 7776000
secret: "z8d5TlFCT58Q$i0%S^2M&3WtE$PMgd" secret: "z8d5TlFCT58Q$i0%S^2M&3WtE$PMgd"
self_order:
expires-ttl: 120
secret: "S3lf0rd3r_S3ss10n_S3cr3t_K3y_2024"
postgresql: postgresql:
host: 62.72.45.250 host: 62.72.45.250

View File

@ -51,6 +51,8 @@ func (a *App) Initialize(cfg *config.Config) error {
repos.tableRepo, repos.tableRepo,
repos.outletRepo, repos.outletRepo,
repos.userRepo, repos.userRepo,
cfg.GetSelfOrderJWTSecret(),
cfg.GetSelfOrderJWTExpiresTTL(),
) )
a.router = router.NewRouter( a.router = router.NewRouter(
@ -114,6 +116,7 @@ func (a *App) Initialize(cfg *config.Config) error {
services.spinGameService, services.spinGameService,
middleware.customerAuthMiddleware, middleware.customerAuthMiddleware,
selfOrderHandler, selfOrderHandler,
middleware.selfOrderAuthMiddleware,
) )
return nil return nil
@ -450,12 +453,14 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
type middlewares struct { type middlewares struct {
authMiddleware *middleware.AuthMiddleware authMiddleware *middleware.AuthMiddleware
customerAuthMiddleware *middleware.CustomerAuthMiddleware customerAuthMiddleware *middleware.CustomerAuthMiddleware
selfOrderAuthMiddleware *middleware.SelfOrderAuthMiddleware
} }
func (a *App) initMiddleware(services *services, cfg *config.Config) *middlewares { func (a *App) initMiddleware(services *services, cfg *config.Config) *middlewares {
return &middlewares{ return &middlewares{
authMiddleware: middleware.NewAuthMiddleware(services.authService), authMiddleware: middleware.NewAuthMiddleware(services.authService),
customerAuthMiddleware: middleware.NewCustomerAuthMiddleware(cfg.GetCustomerJWTSecret()), customerAuthMiddleware: middleware.NewCustomerAuthMiddleware(cfg.GetCustomerJWTSecret()),
selfOrderAuthMiddleware: middleware.NewSelfOrderAuthMiddleware(cfg.GetSelfOrderJWTSecret()),
} }
} }

View File

@ -4,12 +4,18 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
) )
type SelfOrderMenuRequest struct { type SelfOrderSessionRequest struct {
TableID uuid.UUID `json:"table_id" validate:"required"` TableID uuid.UUID `json:"table_id" validate:"required"`
CustomerName string `json:"customer_name" validate:"required"` CustomerName string `json:"customer_name" validate:"required"`
Phone *string `json:"phone,omitempty"` 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 { type SelfOrderMenuResponse struct {
OutletName string `json:"outlet_name"` OutletName string `json:"outlet_name"`
TableName string `json:"table_name"` TableName string `json:"table_name"`
@ -40,8 +46,6 @@ type SelfOrderMenuVariant struct {
} }
type SelfOrderCreateOrderRequest struct { type SelfOrderCreateOrderRequest struct {
TableID uuid.UUID `json:"table_id" validate:"required"`
CustomerName string `json:"customer_name" validate:"required"`
Phone *string `json:"phone,omitempty"` Phone *string `json:"phone,omitempty"`
OrderItems []SelfOrderCreateOrderItem `json:"order_items" validate:"required,min=1,dive"` OrderItems []SelfOrderCreateOrderItem `json:"order_items" validate:"required,min=1,dive"`
} }
@ -53,10 +57,6 @@ type SelfOrderCreateOrderItem struct {
Notes *string `json:"notes,omitempty"` Notes *string `json:"notes,omitempty"`
} }
type SelfOrderListCategoriesRequest struct {
TableID uuid.UUID `form:"table_id" validate:"required"`
}
type SelfOrderCategoryItem struct { type SelfOrderCategoryItem struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
Name string `json:"name"` Name string `json:"name"`

View File

@ -25,6 +25,8 @@ type SelfOrderHandler struct {
tableRepo repository.TableRepositoryInterface tableRepo repository.TableRepositoryInterface
outletRepo processor.OutletRepository outletRepo processor.OutletRepository
userRepo processor.UserRepository userRepo processor.UserRepository
selfOrderJWTSecret string
selfOrderJWTTTL int
} }
func NewSelfOrderHandler( func NewSelfOrderHandler(
@ -34,6 +36,8 @@ func NewSelfOrderHandler(
tableRepo repository.TableRepositoryInterface, tableRepo repository.TableRepositoryInterface,
outletRepo processor.OutletRepository, outletRepo processor.OutletRepository,
userRepo processor.UserRepository, userRepo processor.UserRepository,
selfOrderJWTSecret string,
selfOrderJWTTTL int,
) *SelfOrderHandler { ) *SelfOrderHandler {
return &SelfOrderHandler{ return &SelfOrderHandler{
orderService: orderService, orderService: orderService,
@ -42,36 +46,110 @@ func NewSelfOrderHandler(
tableRepo: tableRepo, tableRepo: tableRepo,
outletRepo: outletRepo, outletRepo: outletRepo,
userRepo: userRepo, 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() ctx := c.Request.Context()
var req contract.SelfOrderMenuRequest var req contract.SelfOrderSessionRequest
if err := c.ShouldBindJSON(&req); err != nil { 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{ util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()), contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()),
}), "SelfOrderHandler::GetMenu") }), "SelfOrderHandler::CreateSession")
return return
} }
if req.TableID == uuid.Nil { if req.TableID == uuid.Nil {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "table_id is required"), contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "table_id is required"),
}), "SelfOrderHandler::GetMenu") }), "SelfOrderHandler::CreateSession")
return return
} }
if req.CustomerName == "" { if req.CustomerName == "" {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "customer_name is required"), contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "customer_name is required"),
}), "SelfOrderHandler::GetMenu") }), "SelfOrderHandler::CreateSession")
return return
} }
table, err := h.tableRepo.GetByID(ctx, req.TableID) 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 { if err != nil {
logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::GetMenu -> table not found") logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::GetMenu -> table not found")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
@ -96,6 +174,8 @@ func (h *SelfOrderHandler) GetMenu(c *gin.Context) {
return return
} }
_ = customerName
isActive := true isActive := true
catResp := h.categoryService.ListCategories(ctx, &contract.ListCategoriesRequest{ catResp := h.categoryService.ListCategories(ctx, &contract.ListCategoriesRequest{
OrganizationID: &table.OrganizationID, OrganizationID: &table.OrganizationID,
@ -192,6 +272,14 @@ func (h *SelfOrderHandler) buildMenuResponse(
func (h *SelfOrderHandler) CreateOrder(c *gin.Context) { func (h *SelfOrderHandler) CreateOrder(c *gin.Context) {
ctx := c.Request.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 var req contract.SelfOrderCreateOrderRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::CreateOrder -> request binding failed") logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::CreateOrder -> request binding failed")
@ -208,7 +296,7 @@ func (h *SelfOrderHandler) CreateOrder(c *gin.Context) {
return return
} }
table, err := h.tableRepo.GetByID(ctx, req.TableID) table, err := h.tableRepo.GetByID(ctx, tableID)
if err != nil { if err != nil {
logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::CreateOrder -> table not found") logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::CreateOrder -> table not found")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ 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{}) customerPhone := phone
metadata["self_order"] = true
metadata["customer_name"] = req.CustomerName
if req.Phone != nil { 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{ modelReq := &models.CreateOrderRequest{
OutletID: table.OutletID, OutletID: table.OutletID,
UserID: userID, UserID: userID,
TableID: &tableID, TableID: &tableIDPtr,
TableNumber: &table.TableName, TableNumber: &table.TableName,
OrderType: constants.OrderTypeDineIn, OrderType: constants.OrderTypeDineIn,
OrderItems: orderItems, OrderItems: orderItems,
CustomerName: &req.CustomerName, CustomerName: &customerName,
Metadata: metadata, Metadata: metadata,
} }
@ -276,12 +369,6 @@ func (h *SelfOrderHandler) CreateOrder(c *gin.Context) {
} }
func (h *SelfOrderHandler) validateCreateOrderRequest(req *contract.SelfOrderCreateOrderRequest) error { 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 { if len(req.OrderItems) == 0 {
return fmt.Errorf("at least one order item is required") 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) { func (h *SelfOrderHandler) ListCategories(c *gin.Context) {
ctx := c.Request.Context() ctx := c.Request.Context()
var req contract.SelfOrderListCategoriesRequest tableID, _, _, err := h.getSelfOrderContext(c)
if err := c.ShouldBindQuery(&req); err != nil { if err != nil {
logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::ListCategories -> query binding failed")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ 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") }), "SelfOrderHandler::ListCategories")
return return
} }
if req.TableID == uuid.Nil { table, err := h.tableRepo.GetByID(ctx, tableID)
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)
if err != nil { if err != nil {
logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::ListCategories -> table not found") logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::ListCategories -> table not found")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{

View File

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

View File

@ -49,9 +49,10 @@ type Router struct {
selfOrderHandler *handler.SelfOrderHandler selfOrderHandler *handler.SelfOrderHandler
authMiddleware *middleware.AuthMiddleware authMiddleware *middleware.AuthMiddleware
customerAuthMiddleware *middleware.CustomerAuthMiddleware 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{ return &Router{
config: cfg, config: cfg,
@ -91,6 +92,7 @@ func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authSer
customerAuthMiddleware: customerAuthMiddleware, customerAuthMiddleware: customerAuthMiddleware,
productVariantHandler: handler.NewProductVariantHandler(productVariantService, productVariantValidator), productVariantHandler: handler.NewProductVariantHandler(productVariantService, productVariantValidator),
selfOrderHandler: selfOrderHandler, selfOrderHandler: selfOrderHandler,
selfOrderAuthMiddleware: selfOrderAuthMiddleware,
} }
} }
@ -149,9 +151,15 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
selfOrder := v1.Group("/self-order") selfOrder := v1.Group("/self-order")
{ {
selfOrder.GET("/categories", r.selfOrderHandler.ListCategories) selfOrder.POST("/session", r.selfOrderHandler.CreateSession)
selfOrder.POST("/menu", r.selfOrderHandler.GetMenu) }
selfOrder.POST("/order", r.selfOrderHandler.CreateOrder)
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") organizations := v1.Group("/organizations")

View File

@ -7,6 +7,7 @@ import (
"apskel-pos-be/internal/entities" "apskel-pos-be/internal/entities"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
) )
// GenerateCustomerTokens generates access and refresh tokens for customer // GenerateCustomerTokens generates access and refresh tokens for customer
@ -85,3 +86,55 @@ func ExtractCustomerIDFromToken(token *jwt.Token) (string, error) {
return customerID, nil 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
}