This commit is contained in:
efrilm 2025-10-07 00:07:38 +07:00
commit 54dc8662d6
11 changed files with 161 additions and 50 deletions

View File

@ -64,8 +64,10 @@ func LoadConfig() *Config {
func (c *Config) Auth() *AuthConfig { func (c *Config) Auth() *AuthConfig {
return &AuthConfig{ return &AuthConfig{
jwtTokenSecret: c.Jwt.Token.Secret, jwtTokenSecret: c.Jwt.Token.Secret,
jwtTokenExpiresTTL: c.Jwt.Token.ExpiresTTL, jwtTokenExpiresTTL: c.Jwt.Token.ExpiresTTL,
refreshTokenSecret: c.Jwt.RefreshToken.Secret,
refreshTokenExpiresTTL: c.Jwt.RefreshToken.ExpiresTTL,
} }
} }

View File

@ -3,8 +3,10 @@ package config
import "time" import "time"
type AuthConfig struct { type AuthConfig struct {
jwtTokenExpiresTTL int jwtTokenExpiresTTL int
jwtTokenSecret string jwtTokenSecret string
refreshTokenExpiresTTL int
refreshTokenSecret string
} }
type JWT struct { type JWT struct {
@ -20,3 +22,20 @@ func (c *AuthConfig) AccessTokenExpiresDate() time.Time {
duration := time.Duration(c.jwtTokenExpiresTTL) duration := time.Duration(c.jwtTokenExpiresTTL)
return time.Now().UTC().Add(time.Minute * duration) return time.Now().UTC().Add(time.Minute * duration)
} }
func (c *AuthConfig) RefreshTokenSecret() string {
return c.refreshTokenSecret
}
func (c *AuthConfig) RefreshTokenExpiresDate() time.Time {
duration := time.Duration(c.refreshTokenExpiresTTL)
return time.Now().UTC().Add(time.Minute * duration)
}
func (c *AuthConfig) AccessTokenTTL() time.Duration {
return time.Duration(c.jwtTokenExpiresTTL) * time.Minute
}
func (c *AuthConfig) RefreshTokenTTL() time.Duration {
return time.Duration(c.refreshTokenExpiresTTL) * time.Minute
}

View File

@ -1,8 +1,9 @@
package config package config
type Jwt struct { type Jwt struct {
Token Token `mapstructure:"token"` Token Token `mapstructure:"token"`
Customer Customer `mapstructure:"customer"` RefreshToken RefreshToken `mapstructure:"refresh_token"`
Customer Customer `mapstructure:"customer"`
} }
type Token struct { type Token struct {
@ -10,6 +11,11 @@ type Token struct {
Secret string `mapstructure:"secret"` Secret string `mapstructure:"secret"`
} }
type RefreshToken struct {
ExpiresTTL int `mapstructure:"expires-ttl"`
Secret string `mapstructure:"secret"`
}
type Customer struct { type Customer struct {
ExpiresTTL int `mapstructure:"expires-ttl"` ExpiresTTL int `mapstructure:"expires-ttl"`
Secret string `mapstructure:"secret"` Secret string `mapstructure:"secret"`

View File

@ -7,6 +7,9 @@ jwt:
token: token:
expires-ttl: 144000 expires-ttl: 144000
secret: "5Lm25V3Qd7aut8dr4QUxm5PZUrSFs" secret: "5Lm25V3Qd7aut8dr4QUxm5PZUrSFs"
refresh_token:
expires-ttl: 7776000 # 3 months in minutes (90 days * 24 hours * 60 minutes)
secret: "R3fr3sh_T0k3n_S3cr3t_K3y_2024_P0S"
customer: customer:
expires-ttl: 7776000 expires-ttl: 7776000
secret: "z8d5TlFCT58Q$i0%S^2M&3WtE$PMgd" secret: "z8d5TlFCT58Q$i0%S^2M&3WtE$PMgd"

View File

@ -365,8 +365,7 @@ type services struct {
func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services { func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services {
authConfig := cfg.Auth() authConfig := cfg.Auth()
jwtSecret := authConfig.AccessTokenSecret() authService := service.NewAuthService(processors.userProcessor, authConfig)
authService := service.NewAuthService(processors.userProcessor, jwtSecret)
organizationService := service.NewOrganizationService(processors.organizationProcessor) organizationService := service.NewOrganizationService(processors.organizationProcessor)
outletService := service.NewOutletService(processors.outletProcessor) outletService := service.NewOutletService(processors.outletProcessor)
outletSettingService := service.NewOutletSettingService(processors.outletSettingProcessor) outletSettingService := service.NewOutletSettingService(processors.outletSettingProcessor)

View File

@ -89,7 +89,7 @@ type ProductAnalyticsRequest struct {
OutletID *uuid.UUID `form:"outlet_id,omitempty"` OutletID *uuid.UUID `form:"outlet_id,omitempty"`
DateFrom string `form:"date_from" validate:"required"` DateFrom string `form:"date_from" validate:"required"`
DateTo string `form:"date_to" validate:"required"` DateTo string `form:"date_to" validate:"required"`
Limit int `form:"limit,default=10" validate:"min=1,max=100"` Limit int `form:"limit,default=1000" validate:"min=1,max=1000"`
} }
// ProductAnalyticsResponse represents the response for product analytics // ProductAnalyticsResponse represents the response for product analytics

View File

@ -40,9 +40,11 @@ type LoginRequest struct {
} }
type LoginResponse struct { type LoginResponse struct {
Token string `json:"token"` Token string `json:"token"`
ExpiresAt time.Time `json:"expires_at"` RefreshToken string `json:"refresh_token"`
User UserResponse `json:"user"` ExpiresAt time.Time `json:"expires_at"`
RefreshExpiresAt time.Time `json:"refresh_expires_at"`
User UserResponse `json:"user"`
} }
type UserResponse struct { type UserResponse struct {

View File

@ -172,7 +172,7 @@ func (p *AnalyticsProcessorImpl) GetProductAnalytics(ctx context.Context, req *m
// Set default limit // Set default limit
if req.Limit <= 0 { if req.Limit <= 0 {
req.Limit = 10 req.Limit = 1000
} }
// Get analytics data from repository // Get analytics data from repository

View File

@ -45,8 +45,10 @@ func (r *AnalyticsRepositoryImpl) GetPaymentMethodAnalytics(ctx context.Context,
Joins("JOIN payment_methods pm ON p.payment_method_id = pm.id"). Joins("JOIN payment_methods pm ON p.payment_method_id = pm.id").
Joins("JOIN orders o ON p.order_id = o.id"). Joins("JOIN orders o ON p.order_id = o.id").
Where("o.organization_id = ?", organizationID). Where("o.organization_id = ?", organizationID).
Where("o.is_void = ?", false).
Where("o.is_refund = ?", false).
Where("p.status = ?", entities.PaymentTransactionStatusCompleted). Where("p.status = ?", entities.PaymentTransactionStatusCompleted).
Where("p.created_at >= ? AND p.created_at <= ?", dateFrom, dateTo) Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo)
if outletID != nil { if outletID != nil {
query = query.Where("o.outlet_id = ?", *outletID) query = query.Where("o.outlet_id = ?", *outletID)
@ -81,7 +83,7 @@ func (r *AnalyticsRepositoryImpl) GetSalesAnalytics(ctx context.Context, organiz
`+dateFormat+` as date, `+dateFormat+` as date,
COALESCE(SUM(o.total_amount), 0) as sales, COALESCE(SUM(o.total_amount), 0) as sales,
COUNT(o.id) as orders, COUNT(o.id) as orders,
COALESCE(SUM(oi.quantity), 0) as items, COALESCE(SUM(CASE WHEN oi.status != 'cancelled' AND oi.is_fully_refunded = false THEN oi.quantity - COALESCE(oi.refund_quantity, 0) ELSE 0 END), 0) as items,
COALESCE(SUM(o.tax_amount), 0) as tax, COALESCE(SUM(o.tax_amount), 0) as tax,
COALESCE(SUM(o.discount_amount), 0) as discount, COALESCE(SUM(o.discount_amount), 0) as discount,
COALESCE(SUM(o.total_amount - o.tax_amount - o.discount_amount), 0) as net_sales COALESCE(SUM(o.total_amount - o.tax_amount - o.discount_amount), 0) as net_sales
@ -89,6 +91,8 @@ func (r *AnalyticsRepositoryImpl) GetSalesAnalytics(ctx context.Context, organiz
Joins("LEFT JOIN order_items oi ON o.id = oi.order_id"). Joins("LEFT JOIN order_items oi ON o.id = oi.order_id").
Where("o.organization_id = ?", organizationID). Where("o.organization_id = ?", organizationID).
Where("o.is_void = ?", false). Where("o.is_void = ?", false).
Where("o.is_refund = ?", false).
Where("o.payment_status = ?", entities.PaymentStatusCompleted).
Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo) Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo)
if outletID != nil { if outletID != nil {
@ -114,10 +118,11 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organ
c.id as category_id, c.id as category_id,
c.name as category_name, c.name as category_name,
c.order as category_order, c.order as category_order,
COALESCE(SUM(oi.quantity), 0) as quantity_sold, COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.quantity - COALESCE(oi.refund_quantity, 0) ELSE 0 END), 0) as quantity_sold,
COALESCE(SUM(oi.total_price), 0) as revenue, COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_price - COALESCE(oi.refund_amount, 0) ELSE 0 END), 0) as revenue,
CASE CASE
WHEN SUM(oi.quantity) > 0 THEN COALESCE(SUM(oi.total_price), 0) / SUM(oi.quantity) WHEN SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.quantity - COALESCE(oi.refund_quantity, 0) ELSE 0 END) > 0
THEN COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_price - COALESCE(oi.refund_amount, 0) ELSE 0 END), 0) / SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.quantity - COALESCE(oi.refund_quantity, 0) ELSE 0 END)
ELSE 0 ELSE 0
END as average_price, END as average_price,
COUNT(DISTINCT oi.order_id) as order_count COUNT(DISTINCT oi.order_id) as order_count
@ -127,6 +132,9 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organ
Joins("JOIN orders o ON oi.order_id = o.id"). Joins("JOIN orders o ON oi.order_id = o.id").
Where("o.organization_id = ?", organizationID). Where("o.organization_id = ?", organizationID).
Where("o.is_void = ?", false). Where("o.is_void = ?", false).
Where("o.is_refund = ?", false).
Where("o.payment_status = ?", entities.PaymentStatusCompleted).
Where("oi.status != ?", entities.OrderItemStatusCancelled).
Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo) Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo)
if outletID != nil { if outletID != nil {
@ -134,7 +142,7 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organ
} }
err := query. err := query.
Group("p.id, p.name, c.id, c.name, c.order"). Group("p.id, p.name, c.id, c.name").
Order("revenue DESC"). Order("revenue DESC").
Limit(limit). Limit(limit).
Scan(&results).Error Scan(&results).Error
@ -150,8 +158,8 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalyticsPerCategory(ctx context.Con
Select(` Select(`
c.id as category_id, c.id as category_id,
c.name as category_name, c.name as category_name,
COALESCE(SUM(oi.total_price), 0) as total_revenue, COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_price - COALESCE(oi.refund_amount, 0) ELSE 0 END), 0) as total_revenue,
COALESCE(SUM(oi.quantity), 0) as total_quantity, COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.quantity - COALESCE(oi.refund_quantity, 0) ELSE 0 END), 0) as total_quantity,
COUNT(DISTINCT p.id) as product_count, COUNT(DISTINCT p.id) as product_count,
COUNT(DISTINCT oi.order_id) as order_count COUNT(DISTINCT oi.order_id) as order_count
`). `).
@ -160,6 +168,9 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalyticsPerCategory(ctx context.Con
Joins("JOIN orders o ON oi.order_id = o.id"). Joins("JOIN orders o ON oi.order_id = o.id").
Where("o.organization_id = ?", organizationID). Where("o.organization_id = ?", organizationID).
Where("o.is_void = ?", false). Where("o.is_void = ?", false).
Where("o.is_refund = ?", false).
Where("o.payment_status = ?", entities.PaymentStatusCompleted).
Where("oi.status != ?", entities.OrderItemStatusCancelled).
Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo) Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo)
if outletID != nil { if outletID != nil {
@ -168,7 +179,7 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalyticsPerCategory(ctx context.Con
err := query. err := query.
Group("c.id, c.name"). Group("c.id, c.name").
Order("total_revenue DESC"). Order("c.name ASC").
Scan(&results).Error Scan(&results).Error
return results, err return results, err
@ -320,18 +331,18 @@ func (r *AnalyticsRepositoryImpl) GetProfitLossAnalytics(ctx context.Context, or
p.name as product_name, p.name as product_name,
c.id as category_id, c.id as category_id,
c.name as category_name, c.name as category_name,
SUM(oi.quantity) as quantity_sold, SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.quantity - COALESCE(oi.refund_quantity, 0) ELSE 0 END) as quantity_sold,
SUM(oi.total_price) as revenue, SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_price - COALESCE(oi.refund_amount, 0) ELSE 0 END) as revenue,
SUM(oi.total_cost) as cost, SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_cost * ((oi.quantity - COALESCE(oi.refund_quantity, 0))::float / NULLIF(oi.quantity, 0)) ELSE 0 END) as cost,
SUM(oi.total_price - oi.total_cost) as gross_profit, SUM(CASE WHEN oi.is_fully_refunded = false THEN (oi.total_price - COALESCE(oi.refund_amount, 0)) - (oi.total_cost * ((oi.quantity - COALESCE(oi.refund_quantity, 0))::float / NULLIF(oi.quantity, 0))) ELSE 0 END) as gross_profit,
CASE CASE
WHEN SUM(oi.total_price) > 0 WHEN SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_price - COALESCE(oi.refund_amount, 0) ELSE 0 END) > 0
THEN (SUM(oi.total_price - oi.total_cost) / SUM(oi.total_price)) * 100 THEN (SUM(CASE WHEN oi.is_fully_refunded = false THEN (oi.total_price - COALESCE(oi.refund_amount, 0)) - (oi.total_cost * ((oi.quantity - COALESCE(oi.refund_quantity, 0))::float / NULLIF(oi.quantity, 0))) ELSE 0 END) / SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_price - COALESCE(oi.refund_amount, 0) ELSE 0 END)) * 100
ELSE 0 ELSE 0
END as gross_profit_margin, END as gross_profit_margin,
AVG(oi.unit_price) as average_price, AVG(CASE WHEN oi.is_fully_refunded = false THEN oi.unit_price ELSE NULL END) as average_price,
AVG(oi.unit_cost) as average_cost, AVG(CASE WHEN oi.is_fully_refunded = false THEN oi.unit_cost ELSE NULL END) as average_cost,
AVG(oi.unit_price - oi.unit_cost) as profit_per_unit AVG(CASE WHEN oi.is_fully_refunded = false THEN oi.unit_price - oi.unit_cost ELSE NULL END) as profit_per_unit
`). `).
Joins("JOIN orders o ON oi.order_id = o.id"). Joins("JOIN orders o ON oi.order_id = o.id").
Joins("JOIN products p ON oi.product_id = p.id"). Joins("JOIN products p ON oi.product_id = p.id").
@ -340,10 +351,11 @@ func (r *AnalyticsRepositoryImpl) GetProfitLossAnalytics(ctx context.Context, or
Where("o.status = ?", entities.OrderStatusCompleted). Where("o.status = ?", entities.OrderStatusCompleted).
Where("o.payment_status = ?", entities.PaymentStatusCompleted). Where("o.payment_status = ?", entities.PaymentStatusCompleted).
Where("o.is_void = false AND o.is_refund = false"). Where("o.is_void = false AND o.is_refund = false").
Where("oi.status != ?", entities.OrderItemStatusCancelled).
Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo). Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo).
Group("p.id, p.name, c.id, c.name"). Group("p.id, p.name, c.id, c.name").
Order("gross_profit DESC"). Order("p.name ASC").
Limit(20) Limit(1000)
if outletID != nil { if outletID != nil {
productQuery = productQuery.Where("o.outlet_id = ?", *outletID) productQuery = productQuery.Where("o.outlet_id = ?", *outletID)

View File

@ -185,8 +185,8 @@ func (s *AnalyticsServiceImpl) validateProductAnalyticsRequest(req *models.Produ
return fmt.Errorf("date_from cannot be after date_to") return fmt.Errorf("date_from cannot be after date_to")
} }
if req.Limit < 1 || req.Limit > 100 { if req.Limit < 1 || req.Limit > 1000 {
return fmt.Errorf("limit must be between 1 and 100") return fmt.Errorf("limit must be between 1 and 1000")
} }
return nil return nil

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"time" "time"
"apskel-pos-be/config"
"apskel-pos-be/internal/contract" "apskel-pos-be/internal/contract"
"apskel-pos-be/internal/models" "apskel-pos-be/internal/models"
"apskel-pos-be/internal/transformer" "apskel-pos-be/internal/transformer"
@ -23,9 +24,11 @@ type AuthService interface {
} }
type AuthServiceImpl struct { type AuthServiceImpl struct {
userProcessor UserProcessor userProcessor UserProcessor
jwtSecret string jwtSecret string
tokenTTL time.Duration refreshSecret string
tokenTTL time.Duration
refreshTokenTTL time.Duration
} }
type Claims struct { type Claims struct {
@ -36,11 +39,13 @@ type Claims struct {
jwt.RegisteredClaims jwt.RegisteredClaims
} }
func NewAuthService(userProcessor UserProcessor, jwtSecret string) AuthService { func NewAuthService(userProcessor UserProcessor, authConfig *config.AuthConfig) AuthService {
return &AuthServiceImpl{ return &AuthServiceImpl{
userProcessor: userProcessor, userProcessor: userProcessor,
jwtSecret: jwtSecret, jwtSecret: authConfig.AccessTokenSecret(),
tokenTTL: 24 * time.Hour, refreshSecret: authConfig.RefreshTokenSecret(),
tokenTTL: authConfig.AccessTokenTTL(),
refreshTokenTTL: authConfig.RefreshTokenTTL(),
} }
} }
@ -71,10 +76,17 @@ func (s *AuthServiceImpl) Login(ctx context.Context, req *contract.LoginRequest)
return nil, fmt.Errorf("failed to generate token: %w", err) return nil, fmt.Errorf("failed to generate token: %w", err)
} }
refreshToken, refreshExpiresAt, err := s.generateRefreshToken(userResponse)
if err != nil {
return nil, fmt.Errorf("failed to generate refresh token: %w", err)
}
return &contract.LoginResponse{ return &contract.LoginResponse{
Token: token, Token: token,
ExpiresAt: expiresAt, RefreshToken: refreshToken,
User: *contractUserResponse, ExpiresAt: expiresAt,
RefreshExpiresAt: refreshExpiresAt,
User: *contractUserResponse,
}, nil }, nil
} }
@ -98,9 +110,9 @@ func (s *AuthServiceImpl) ValidateToken(tokenString string) (*contract.UserRespo
} }
func (s *AuthServiceImpl) RefreshToken(ctx context.Context, tokenString string) (*contract.LoginResponse, error) { func (s *AuthServiceImpl) RefreshToken(ctx context.Context, tokenString string) (*contract.LoginResponse, error) {
claims, err := s.parseToken(tokenString) claims, err := s.parseRefreshToken(tokenString)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid token: %w", err) return nil, fmt.Errorf("invalid refresh token: %w", err)
} }
userResponse, err := s.userProcessor.GetUserByID(ctx, claims.UserID) userResponse, err := s.userProcessor.GetUserByID(ctx, claims.UserID)
@ -119,10 +131,17 @@ func (s *AuthServiceImpl) RefreshToken(ctx context.Context, tokenString string)
return nil, fmt.Errorf("failed to generate token: %w", err) return nil, fmt.Errorf("failed to generate token: %w", err)
} }
refreshToken, refreshExpiresAt, err := s.generateRefreshToken(userResponse)
if err != nil {
return nil, fmt.Errorf("failed to generate refresh token: %w", err)
}
return &contract.LoginResponse{ return &contract.LoginResponse{
Token: newToken, Token: newToken,
ExpiresAt: expiresAt, RefreshToken: refreshToken,
User: *contractUserResponse, ExpiresAt: expiresAt,
RefreshExpiresAt: refreshExpiresAt,
User: *contractUserResponse,
}, nil }, nil
} }
@ -164,6 +183,32 @@ func (s *AuthServiceImpl) generateToken(user *models.UserResponse) (string, time
return tokenString, expiresAt, nil return tokenString, expiresAt, nil
} }
func (s *AuthServiceImpl) generateRefreshToken(user *models.UserResponse) (string, time.Time, error) {
expiresAt := time.Now().Add(s.refreshTokenTTL)
claims := &Claims{
UserID: user.ID,
Email: user.Email,
Role: string(user.Role),
OrganizationID: user.OrganizationID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expiresAt),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "apskel-pos-refresh",
Subject: user.ID.String(),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte(s.refreshSecret))
if err != nil {
return "", time.Time{}, err
}
return tokenString, expiresAt, nil
}
func (s *AuthServiceImpl) parseToken(tokenString string) (*Claims, error) { func (s *AuthServiceImpl) parseToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
@ -182,3 +227,26 @@ func (s *AuthServiceImpl) parseToken(tokenString string) (*Claims, error) {
return nil, errors.New("invalid token") return nil, errors.New("invalid token")
} }
func (s *AuthServiceImpl) parseRefreshToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, 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(s.refreshSecret), nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
// Verify this is a refresh token by checking the issuer
if claims.Issuer != "apskel-pos-refresh" {
return nil, errors.New("not a valid refresh token")
}
return claims, nil
}
return nil, errors.New("invalid refresh token")
}