merge
This commit is contained in:
commit
54dc8662d6
@ -66,6 +66,8 @@ 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,8 @@ 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
|
||||||
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package config
|
|||||||
|
|
||||||
type Jwt struct {
|
type Jwt struct {
|
||||||
Token Token `mapstructure:"token"`
|
Token Token `mapstructure:"token"`
|
||||||
|
RefreshToken RefreshToken `mapstructure:"refresh_token"`
|
||||||
Customer Customer `mapstructure:"customer"`
|
Customer Customer `mapstructure:"customer"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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"`
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -41,7 +41,9 @@ type LoginRequest struct {
|
|||||||
|
|
||||||
type LoginResponse struct {
|
type LoginResponse struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
ExpiresAt time.Time `json:"expires_at"`
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
|
RefreshExpiresAt time.Time `json:"refresh_expires_at"`
|
||||||
User UserResponse `json:"user"`
|
User UserResponse `json:"user"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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"
|
||||||
@ -25,7 +26,9 @@ type AuthService interface {
|
|||||||
type AuthServiceImpl struct {
|
type AuthServiceImpl struct {
|
||||||
userProcessor UserProcessor
|
userProcessor UserProcessor
|
||||||
jwtSecret string
|
jwtSecret string
|
||||||
|
refreshSecret string
|
||||||
tokenTTL time.Duration
|
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,9 +76,16 @@ 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,
|
||||||
|
RefreshToken: refreshToken,
|
||||||
ExpiresAt: expiresAt,
|
ExpiresAt: expiresAt,
|
||||||
|
RefreshExpiresAt: refreshExpiresAt,
|
||||||
User: *contractUserResponse,
|
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,9 +131,16 @@ 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,
|
||||||
|
RefreshToken: refreshToken,
|
||||||
ExpiresAt: expiresAt,
|
ExpiresAt: expiresAt,
|
||||||
|
RefreshExpiresAt: refreshExpiresAt,
|
||||||
User: *contractUserResponse,
|
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")
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user