Initialize base
This commit is contained in:
@@ -0,0 +1,389 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Customer represents a mobile app user (customer)
|
||||
type Customer struct {
|
||||
ID uuid.UUID `json:"id" bson:"_id"`
|
||||
Email string `json:"email" bson:"email"`
|
||||
Password string `json:"-" bson:"password"` // Never serialize password
|
||||
FirstName string `json:"first_name" bson:"first_name"`
|
||||
LastName string `json:"last_name" bson:"last_name"`
|
||||
Phone string `json:"phone" bson:"phone"`
|
||||
Role CustomerRole `json:"role" bson:"role"`
|
||||
CompanyID uuid.UUID `json:"company_id" bson:"company_id"`
|
||||
IsActive bool `json:"is_active" bson:"is_active"`
|
||||
IsVerified bool `json:"is_verified" bson:"is_verified"`
|
||||
DeviceTokens []string `json:"device_tokens" bson:"device_tokens"` // For push notifications
|
||||
Preferences CustomerPreferences `json:"preferences" bson:"preferences"`
|
||||
LastLoginAt *time.Time `json:"last_login_at,omitempty" bson:"last_login_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at" bson:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
|
||||
}
|
||||
|
||||
// CustomerPreferences represents customer-specific preferences
|
||||
type CustomerPreferences struct {
|
||||
Language string `json:"language" bson:"language"`
|
||||
NotificationSettings NotificationSettings `json:"notification_settings" bson:"notification_settings"`
|
||||
TenderAlerts bool `json:"tender_alerts" bson:"tender_alerts"`
|
||||
EmailNotifications bool `json:"email_notifications" bson:"email_notifications"`
|
||||
PushNotifications bool `json:"push_notifications" bson:"push_notifications"`
|
||||
PreferredCategories []string `json:"preferred_categories" bson:"preferred_categories"`
|
||||
}
|
||||
|
||||
// NotificationSettings represents notification preferences
|
||||
type NotificationSettings struct {
|
||||
NewTenders bool `json:"new_tenders" bson:"new_tenders"`
|
||||
DeadlineReminder bool `json:"deadline_reminder" bson:"deadline_reminder"`
|
||||
StatusUpdates bool `json:"status_updates" bson:"status_updates"`
|
||||
SystemAlerts bool `json:"system_alerts" bson:"system_alerts"`
|
||||
}
|
||||
|
||||
// CustomerRole defines roles for mobile app users
|
||||
type CustomerRole string
|
||||
|
||||
const (
|
||||
CustomerRoleUser CustomerRole = "customer_user" // Regular customer
|
||||
CustomerRoleManager CustomerRole = "customer_manager" // Company manager
|
||||
)
|
||||
|
||||
// User represents a panel user (admin/staff)
|
||||
type User struct {
|
||||
ID uuid.UUID `json:"id" bson:"_id"`
|
||||
Email string `json:"email" bson:"email"`
|
||||
Password string `json:"-" bson:"password"` // Never serialize password
|
||||
FirstName string `json:"first_name" bson:"first_name"`
|
||||
LastName string `json:"last_name" bson:"last_name"`
|
||||
Role UserRole `json:"role" bson:"role"`
|
||||
Department string `json:"department" bson:"department"`
|
||||
IsActive bool `json:"is_active" bson:"is_active"`
|
||||
Permissions []string `json:"permissions" bson:"permissions"`
|
||||
LastLoginAt *time.Time `json:"last_login_at,omitempty" bson:"last_login_at,omitempty"`
|
||||
CreatedBy *uuid.UUID `json:"created_by,omitempty" bson:"created_by,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at" bson:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
|
||||
}
|
||||
|
||||
// UserRole defines roles for panel users
|
||||
type UserRole string
|
||||
|
||||
const (
|
||||
UserRoleAdmin UserRole = "admin" // Full system administrator
|
||||
UserRoleModerator UserRole = "moderator" // Moderator with limited admin access
|
||||
UserRoleSupport UserRole = "support" // Support user with read-only access
|
||||
UserRoleAnalyst UserRole = "analyst" // Data analyst with reporting access
|
||||
)
|
||||
|
||||
// Customer methods
|
||||
func (c *Customer) HasRole(role CustomerRole) bool {
|
||||
return c.Role == role
|
||||
}
|
||||
|
||||
func (c *Customer) IsManager() bool {
|
||||
return c.Role == CustomerRoleManager
|
||||
}
|
||||
|
||||
func (c *Customer) CanAccessCompanySettings() bool {
|
||||
return c.IsActive && c.IsVerified && c.IsManager()
|
||||
}
|
||||
|
||||
func (c *Customer) FullName() string {
|
||||
return c.FirstName + " " + c.LastName
|
||||
}
|
||||
|
||||
// User methods
|
||||
func (u *User) HasRole(role UserRole) bool {
|
||||
return u.Role == role
|
||||
}
|
||||
|
||||
func (u *User) HasAnyRole(roles ...UserRole) bool {
|
||||
for _, role := range roles {
|
||||
if u.Role == role {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (u *User) HasPermission(permission string) bool {
|
||||
for _, perm := range u.Permissions {
|
||||
if perm == permission {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (u *User) IsAdmin() bool {
|
||||
return u.Role == UserRoleAdmin
|
||||
}
|
||||
|
||||
func (u *User) CanManageUsers() bool {
|
||||
return u.IsActive && (u.Role == UserRoleAdmin || u.HasPermission("manage_users"))
|
||||
}
|
||||
|
||||
func (u *User) CanManageTenders() bool {
|
||||
return u.IsActive && (u.Role == UserRoleAdmin || u.Role == UserRoleModerator || u.HasPermission("manage_tenders"))
|
||||
}
|
||||
|
||||
func (u *User) CanViewReports() bool {
|
||||
return u.IsActive && (u.Role == UserRoleAdmin || u.Role == UserRoleAnalyst || u.HasPermission("view_reports"))
|
||||
}
|
||||
|
||||
func (u *User) FullName() string {
|
||||
return u.FirstName + " " + u.LastName
|
||||
}
|
||||
|
||||
// Company represents a company profile
|
||||
type Company struct {
|
||||
ID uuid.UUID `json:"id" bson:"_id"`
|
||||
Name string `json:"name" bson:"name"`
|
||||
Description string `json:"description" bson:"description"`
|
||||
Industry string `json:"industry" bson:"industry"`
|
||||
Country string `json:"country" bson:"country"`
|
||||
Website string `json:"website" bson:"website"`
|
||||
Keywords []string `json:"keywords" bson:"keywords"`
|
||||
Products []string `json:"products" bson:"products"`
|
||||
Services []string `json:"services" bson:"services"`
|
||||
Capabilities []string `json:"capabilities" bson:"capabilities"`
|
||||
Documents []Document `json:"documents" bson:"documents"`
|
||||
Interests InterestProfile `json:"interests" bson:"interests"`
|
||||
CreatedAt time.Time `json:"created_at" bson:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
|
||||
}
|
||||
|
||||
// InterestProfile represents company's tender interests
|
||||
type InterestProfile struct {
|
||||
Keywords []string `json:"keywords" bson:"keywords"`
|
||||
Industries []string `json:"industries" bson:"industries"`
|
||||
CPVCodes []string `json:"cpv_codes" bson:"cpv_codes"`
|
||||
MinBudget *float64 `json:"min_budget,omitempty" bson:"min_budget,omitempty"`
|
||||
MaxBudget *float64 `json:"max_budget,omitempty" bson:"max_budget,omitempty"`
|
||||
Regions []string `json:"regions" bson:"regions"`
|
||||
Embeddings []float32 `json:"-" bson:"embeddings"` // AI embeddings for matching
|
||||
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
|
||||
}
|
||||
|
||||
// Tender represents a tender opportunity
|
||||
type Tender struct {
|
||||
ID uuid.UUID `json:"id" bson:"_id"`
|
||||
Title string `json:"title" bson:"title"`
|
||||
Description string `json:"description" bson:"description"`
|
||||
Summary string `json:"summary" bson:"summary"`
|
||||
Type TenderType `json:"type" bson:"type"`
|
||||
Status TenderStatus `json:"status" bson:"status"`
|
||||
PublicationDate time.Time `json:"publication_date" bson:"publication_date"`
|
||||
Deadline time.Time `json:"deadline" bson:"deadline"`
|
||||
Budget *float64 `json:"budget,omitempty" bson:"budget,omitempty"`
|
||||
Currency string `json:"currency" bson:"currency"`
|
||||
Region string `json:"region" bson:"region"`
|
||||
Country string `json:"country" bson:"country"`
|
||||
CPVCodes []string `json:"cpv_codes" bson:"cpv_codes"`
|
||||
Categories []string `json:"categories" bson:"categories"`
|
||||
Keywords []string `json:"keywords" bson:"keywords"`
|
||||
Tags []string `json:"tags" bson:"tags"`
|
||||
Language string `json:"language" bson:"language"`
|
||||
SourceURL string `json:"source_url" bson:"source_url"`
|
||||
SourceName string `json:"source_name" bson:"source_name"`
|
||||
Documents []TenderDocument `json:"documents" bson:"documents"`
|
||||
Requirements []Requirement `json:"requirements" bson:"requirements"`
|
||||
Embeddings []float32 `json:"-" bson:"embeddings"` // AI embeddings for matching
|
||||
ProcessedAt *time.Time `json:"processed_at,omitempty" bson:"processed_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at" bson:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
|
||||
}
|
||||
|
||||
// TenderType defines types of tenders
|
||||
type TenderType string
|
||||
|
||||
const (
|
||||
TenderTypeRFI TenderType = "RFI" // Request for Information
|
||||
TenderTypeRFP TenderType = "RFP" // Request for Proposal
|
||||
TenderTypeRFQ TenderType = "RFQ" // Request for Quote
|
||||
TenderTypeTender TenderType = "TENDER" // Public Tender
|
||||
)
|
||||
|
||||
// TenderStatus defines tender statuses
|
||||
type TenderStatus string
|
||||
|
||||
const (
|
||||
TenderStatusActive TenderStatus = "active"
|
||||
TenderStatusExpired TenderStatus = "expired"
|
||||
TenderStatusCancelled TenderStatus = "cancelled"
|
||||
TenderStatusAwarded TenderStatus = "awarded"
|
||||
)
|
||||
|
||||
// TenderDocument represents documents associated with a tender
|
||||
type TenderDocument struct {
|
||||
ID uuid.UUID `json:"id" bson:"id"`
|
||||
Name string `json:"name" bson:"name"`
|
||||
Type DocumentType `json:"type" bson:"type"`
|
||||
URL string `json:"url" bson:"url"`
|
||||
LocalPath string `json:"local_path" bson:"local_path"`
|
||||
Size int64 `json:"size" bson:"size"`
|
||||
ContentType string `json:"content_type" bson:"content_type"`
|
||||
Checksum string `json:"checksum" bson:"checksum"`
|
||||
Downloaded bool `json:"downloaded" bson:"downloaded"`
|
||||
DownloadedAt *time.Time `json:"downloaded_at,omitempty" bson:"downloaded_at,omitempty"`
|
||||
}
|
||||
|
||||
// Requirement represents tender requirements
|
||||
type Requirement struct {
|
||||
ID uuid.UUID `json:"id" bson:"id"`
|
||||
Type RequirementType `json:"type" bson:"type"`
|
||||
Description string `json:"description" bson:"description"`
|
||||
Mandatory bool `json:"mandatory" bson:"mandatory"`
|
||||
Category string `json:"category" bson:"category"`
|
||||
}
|
||||
|
||||
// RequirementType defines types of requirements
|
||||
type RequirementType string
|
||||
|
||||
const (
|
||||
RequirementTypeLegal RequirementType = "legal"
|
||||
RequirementTypeTechnical RequirementType = "technical"
|
||||
RequirementTypeFinancial RequirementType = "financial"
|
||||
RequirementTypeExperience RequirementType = "experience"
|
||||
)
|
||||
|
||||
// Document represents uploaded company documents
|
||||
type Document struct {
|
||||
ID uuid.UUID `json:"id" bson:"id"`
|
||||
Name string `json:"name" bson:"name"`
|
||||
Type DocumentType `json:"type" bson:"type"`
|
||||
Category string `json:"category" bson:"category"`
|
||||
Description string `json:"description" bson:"description"`
|
||||
FilePath string `json:"file_path" bson:"file_path"`
|
||||
Size int64 `json:"size" bson:"size"`
|
||||
ContentType string `json:"content_type" bson:"content_type"`
|
||||
Checksum string `json:"checksum" bson:"checksum"`
|
||||
ExpiryDate *time.Time `json:"expiry_date,omitempty" bson:"expiry_date,omitempty"`
|
||||
Keywords []string `json:"keywords" bson:"keywords"`
|
||||
UploadedBy uuid.UUID `json:"uploaded_by" bson:"uploaded_by"`
|
||||
CreatedAt time.Time `json:"created_at" bson:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
|
||||
}
|
||||
|
||||
// DocumentType defines types of documents
|
||||
type DocumentType string
|
||||
|
||||
const (
|
||||
DocumentTypePDF DocumentType = "pdf"
|
||||
DocumentTypeWord DocumentType = "word"
|
||||
DocumentTypeExcel DocumentType = "excel"
|
||||
DocumentTypeImage DocumentType = "image"
|
||||
DocumentTypeCertificate DocumentType = "certificate"
|
||||
DocumentTypeLicense DocumentType = "license"
|
||||
DocumentTypeContract DocumentType = "contract"
|
||||
DocumentTypeCatalog DocumentType = "catalog"
|
||||
)
|
||||
|
||||
// TenderSource represents tender source configuration
|
||||
type TenderSource struct {
|
||||
ID uuid.UUID `json:"id" bson:"_id"`
|
||||
Name string `json:"name" bson:"name"`
|
||||
BaseURL string `json:"base_url" bson:"base_url"`
|
||||
Type SourceType `json:"type" bson:"type"`
|
||||
AuthRequired bool `json:"auth_required" bson:"auth_required"`
|
||||
CredentialsID *uuid.UUID `json:"credentials_id,omitempty" bson:"credentials_id,omitempty"`
|
||||
CheckInterval time.Duration `json:"check_interval" bson:"check_interval"`
|
||||
CategoryTags []string `json:"category_tags" bson:"category_tags"`
|
||||
Region string `json:"region" bson:"region"`
|
||||
Language string `json:"language" bson:"language"`
|
||||
Status SourceStatus `json:"status" bson:"status"`
|
||||
LastCheckedAt *time.Time `json:"last_checked_at,omitempty" bson:"last_checked_at,omitempty"`
|
||||
Configuration SourceConfiguration `json:"configuration" bson:"configuration"`
|
||||
CreatedAt time.Time `json:"created_at" bson:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
|
||||
}
|
||||
|
||||
// SourceType defines types of tender sources
|
||||
type SourceType string
|
||||
|
||||
const (
|
||||
SourceTypeAPI SourceType = "api"
|
||||
SourceTypeHTML SourceType = "html"
|
||||
SourceTypeJS SourceType = "js"
|
||||
SourceTypePDF SourceType = "pdf"
|
||||
SourceTypeRSS SourceType = "rss"
|
||||
)
|
||||
|
||||
// SourceStatus defines tender source statuses
|
||||
type SourceStatus string
|
||||
|
||||
const (
|
||||
SourceStatusActive SourceStatus = "active"
|
||||
SourceStatusInactive SourceStatus = "inactive"
|
||||
SourceStatusError SourceStatus = "error"
|
||||
)
|
||||
|
||||
// SourceConfiguration holds source-specific configuration
|
||||
type SourceConfiguration struct {
|
||||
Headers map[string]string `json:"headers,omitempty" bson:"headers,omitempty"`
|
||||
QueryParams map[string]string `json:"query_params,omitempty" bson:"query_params,omitempty"`
|
||||
Selectors map[string]string `json:"selectors,omitempty" bson:"selectors,omitempty"`
|
||||
PaginationKey string `json:"pagination_key,omitempty" bson:"pagination_key,omitempty"`
|
||||
MaxPages int `json:"max_pages,omitempty" bson:"max_pages,omitempty"`
|
||||
RateLimitDelay time.Duration `json:"rate_limit_delay,omitempty" bson:"rate_limit_delay,omitempty"`
|
||||
}
|
||||
|
||||
// TenderMatch represents AI-powered tender recommendations
|
||||
type TenderMatch struct {
|
||||
ID uuid.UUID `json:"id" bson:"_id"`
|
||||
CompanyID uuid.UUID `json:"company_id" bson:"company_id"`
|
||||
TenderID uuid.UUID `json:"tender_id" bson:"tender_id"`
|
||||
Score float64 `json:"score" bson:"score"`
|
||||
Explanation string `json:"explanation" bson:"explanation"`
|
||||
Tags []string `json:"tags" bson:"tags"`
|
||||
Status MatchStatus `json:"status" bson:"status"`
|
||||
UserAction *UserAction `json:"user_action,omitempty" bson:"user_action,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at" bson:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
|
||||
}
|
||||
|
||||
// MatchStatus defines tender match statuses
|
||||
type MatchStatus string
|
||||
|
||||
const (
|
||||
MatchStatusPending MatchStatus = "pending"
|
||||
MatchStatusViewed MatchStatus = "viewed"
|
||||
MatchStatusLiked MatchStatus = "liked"
|
||||
MatchStatusDisliked MatchStatus = "disliked"
|
||||
MatchStatusApplied MatchStatus = "applied"
|
||||
)
|
||||
|
||||
// UserAction represents user interaction with tender matches
|
||||
type UserAction struct {
|
||||
Action MatchStatus `json:"action" bson:"action"`
|
||||
Timestamp time.Time `json:"timestamp" bson:"timestamp"`
|
||||
UserID uuid.UUID `json:"user_id" bson:"user_id"`
|
||||
}
|
||||
|
||||
// Notification represents system notifications
|
||||
type Notification struct {
|
||||
ID uuid.UUID `json:"id" bson:"_id"`
|
||||
UserID uuid.UUID `json:"user_id" bson:"user_id"`
|
||||
CompanyID uuid.UUID `json:"company_id" bson:"company_id"`
|
||||
Type NotificationType `json:"type" bson:"type"`
|
||||
Title string `json:"title" bson:"title"`
|
||||
Message string `json:"message" bson:"message"`
|
||||
Data map[string]any `json:"data,omitempty" bson:"data,omitempty"`
|
||||
Read bool `json:"read" bson:"read"`
|
||||
Sent bool `json:"sent" bson:"sent"`
|
||||
SentAt *time.Time `json:"sent_at,omitempty" bson:"sent_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at" bson:"created_at"`
|
||||
}
|
||||
|
||||
// NotificationType defines types of notifications
|
||||
type NotificationType string
|
||||
|
||||
const (
|
||||
NotificationTypeNewTender NotificationType = "new_tender"
|
||||
NotificationTypeDeadline NotificationType = "deadline_reminder"
|
||||
NotificationTypeStatusUpdate NotificationType = "status_update"
|
||||
NotificationTypeSystemAlert NotificationType = "system_alert"
|
||||
)
|
||||
@@ -0,0 +1,321 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Repository interfaces define data access contracts
|
||||
|
||||
// CustomerRepository defines methods for customer (mobile user) data access
|
||||
type CustomerRepository interface {
|
||||
Create(ctx context.Context, customer *Customer) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*Customer, error)
|
||||
GetByEmail(ctx context.Context, email string) (*Customer, error)
|
||||
Update(ctx context.Context, customer *Customer) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
List(ctx context.Context, limit, offset int) ([]*Customer, error)
|
||||
GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*Customer, error)
|
||||
UpdatePreferences(ctx context.Context, id uuid.UUID, preferences *CustomerPreferences) error
|
||||
AddDeviceToken(ctx context.Context, id uuid.UUID, token string) error
|
||||
RemoveDeviceToken(ctx context.Context, id uuid.UUID, token string) error
|
||||
}
|
||||
|
||||
// UserRepository defines methods for panel user data access
|
||||
type UserRepository interface {
|
||||
Create(ctx context.Context, user *User) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*User, error)
|
||||
GetByEmail(ctx context.Context, email string) (*User, error)
|
||||
Update(ctx context.Context, user *User) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
List(ctx context.Context, limit, offset int) ([]*User, error)
|
||||
GetByRole(ctx context.Context, role UserRole, limit, offset int) ([]*User, error)
|
||||
UpdatePermissions(ctx context.Context, id uuid.UUID, permissions []string) error
|
||||
}
|
||||
|
||||
// CompanyRepository defines methods for company data access
|
||||
type CompanyRepository interface {
|
||||
Create(ctx context.Context, company *Company) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*Company, error)
|
||||
Update(ctx context.Context, company *Company) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
List(ctx context.Context, limit, offset int) ([]*Company, error)
|
||||
UpdateInterests(ctx context.Context, id uuid.UUID, interests *InterestProfile) error
|
||||
}
|
||||
|
||||
// TenderRepository defines methods for tender data access
|
||||
type TenderRepository interface {
|
||||
Create(ctx context.Context, tender *Tender) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*Tender, error)
|
||||
Update(ctx context.Context, tender *Tender) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
List(ctx context.Context, filters TenderFilters, limit, offset int) ([]*Tender, error)
|
||||
ListByStatus(ctx context.Context, status TenderStatus, limit, offset int) ([]*Tender, error)
|
||||
Search(ctx context.Context, query string, filters TenderFilters, limit, offset int) ([]*Tender, error)
|
||||
GetExpiringTenders(ctx context.Context, days int) ([]*Tender, error)
|
||||
BulkCreate(ctx context.Context, tenders []*Tender) error
|
||||
}
|
||||
|
||||
// TenderSourceRepository defines methods for tender source data access
|
||||
type TenderSourceRepository interface {
|
||||
Create(ctx context.Context, source *TenderSource) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*TenderSource, error)
|
||||
Update(ctx context.Context, source *TenderSource) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
List(ctx context.Context, limit, offset int) ([]*TenderSource, error)
|
||||
GetActiveources(ctx context.Context) ([]*TenderSource, error)
|
||||
UpdateLastChecked(ctx context.Context, id uuid.UUID) error
|
||||
}
|
||||
|
||||
// TenderMatchRepository defines methods for tender match data access
|
||||
type TenderMatchRepository interface {
|
||||
Create(ctx context.Context, match *TenderMatch) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*TenderMatch, error)
|
||||
Update(ctx context.Context, match *TenderMatch) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
GetByCompany(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*TenderMatch, error)
|
||||
GetByTender(ctx context.Context, tenderID uuid.UUID, limit, offset int) ([]*TenderMatch, error)
|
||||
UpdateUserAction(ctx context.Context, id uuid.UUID, action *UserAction) error
|
||||
BulkCreate(ctx context.Context, matches []*TenderMatch) error
|
||||
}
|
||||
|
||||
// DocumentRepository defines methods for document data access
|
||||
type DocumentRepository interface {
|
||||
Create(ctx context.Context, document *Document) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*Document, error)
|
||||
Update(ctx context.Context, document *Document) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
GetByCompany(ctx context.Context, companyID uuid.UUID) ([]*Document, error)
|
||||
GetByCategory(ctx context.Context, companyID uuid.UUID, category string) ([]*Document, error)
|
||||
}
|
||||
|
||||
// NotificationRepository defines methods for notification data access
|
||||
type NotificationRepository interface {
|
||||
Create(ctx context.Context, notification *Notification) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*Notification, error)
|
||||
Update(ctx context.Context, notification *Notification) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
GetByUser(ctx context.Context, userID uuid.UUID, limit, offset int) ([]*Notification, error)
|
||||
GetUnreadByUser(ctx context.Context, userID uuid.UUID) ([]*Notification, error)
|
||||
MarkAsRead(ctx context.Context, id uuid.UUID) error
|
||||
MarkAsSent(ctx context.Context, id uuid.UUID) error
|
||||
}
|
||||
|
||||
// Service interfaces define business logic contracts
|
||||
|
||||
// CustomerAuthService defines authentication methods for mobile customers
|
||||
type CustomerAuthService interface {
|
||||
Register(ctx context.Context, req CustomerRegisterRequest) (*CustomerAuthResponse, error)
|
||||
Login(ctx context.Context, req LoginRequest) (*CustomerAuthResponse, error)
|
||||
RefreshToken(ctx context.Context, refreshToken string) (*CustomerAuthResponse, error)
|
||||
ValidateToken(ctx context.Context, token string) (*Customer, error)
|
||||
ChangePassword(ctx context.Context, customerID uuid.UUID, oldPassword, newPassword string) error
|
||||
ResetPassword(ctx context.Context, email string) error
|
||||
VerifyEmail(ctx context.Context, customerID uuid.UUID, token string) error
|
||||
ResendVerification(ctx context.Context, email string) error
|
||||
}
|
||||
|
||||
// UserAuthService defines authentication methods for panel users
|
||||
type UserAuthService interface {
|
||||
Login(ctx context.Context, req LoginRequest) (*UserAuthResponse, error)
|
||||
RefreshToken(ctx context.Context, refreshToken string) (*UserAuthResponse, error)
|
||||
ValidateToken(ctx context.Context, token string) (*User, error)
|
||||
ChangePassword(ctx context.Context, userID uuid.UUID, oldPassword, newPassword string) error
|
||||
CreatePanelUser(ctx context.Context, req CreateUserRequest, createdBy uuid.UUID) (*User, error)
|
||||
UpdateUserPermissions(ctx context.Context, userID uuid.UUID, permissions []string, updatedBy uuid.UUID) error
|
||||
}
|
||||
|
||||
// TenderService defines tender management methods
|
||||
type TenderService interface {
|
||||
GetTenders(ctx context.Context, filters TenderFilters, limit, offset int) ([]*Tender, error)
|
||||
GetTenderByID(ctx context.Context, id uuid.UUID) (*Tender, error)
|
||||
SearchTenders(ctx context.Context, query string, filters TenderFilters, limit, offset int) ([]*Tender, error)
|
||||
GetTenderMatches(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*TenderMatch, error)
|
||||
ProcessTenderAction(ctx context.Context, matchID uuid.UUID, action MatchStatus, customerID uuid.UUID) error
|
||||
}
|
||||
|
||||
// CompanyService defines company management methods
|
||||
type CompanyService interface {
|
||||
CreateCompany(ctx context.Context, req CreateCompanyRequest) (*Company, error)
|
||||
GetCompany(ctx context.Context, id uuid.UUID) (*Company, error)
|
||||
UpdateCompany(ctx context.Context, id uuid.UUID, req UpdateCompanyRequest, updatedBy uuid.UUID) (*Company, error)
|
||||
UpdateInterests(ctx context.Context, companyID uuid.UUID, interests *InterestProfile) error
|
||||
UploadDocument(ctx context.Context, companyID uuid.UUID, req UploadDocumentRequest) (*Document, error)
|
||||
GetDocuments(ctx context.Context, companyID uuid.UUID) ([]*Document, error)
|
||||
}
|
||||
|
||||
// CustomerService defines customer management methods (for panel users)
|
||||
type CustomerService interface {
|
||||
GetCustomers(ctx context.Context, limit, offset int) ([]*Customer, error)
|
||||
GetCustomerByID(ctx context.Context, id uuid.UUID) (*Customer, error)
|
||||
GetCustomersByCompany(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*Customer, error)
|
||||
UpdateCustomerStatus(ctx context.Context, customerID uuid.UUID, isActive bool, updatedBy uuid.UUID) error
|
||||
UpdateCustomerPreferences(ctx context.Context, customerID uuid.UUID, preferences *CustomerPreferences) error
|
||||
}
|
||||
|
||||
// ScrapingService defines tender scraping methods
|
||||
type ScrapingService interface {
|
||||
ScrapeTenderSource(ctx context.Context, sourceID uuid.UUID) error
|
||||
ProcessTenderData(ctx context.Context, rawData []byte, sourceID uuid.UUID) ([]*Tender, error)
|
||||
DownloadTenderDocuments(ctx context.Context, tenderID uuid.UUID) error
|
||||
ScheduleScrapingJobs(ctx context.Context) error
|
||||
}
|
||||
|
||||
// AIService defines AI and machine learning methods
|
||||
type AIService interface {
|
||||
GenerateEmbeddings(ctx context.Context, text string) ([]float32, error)
|
||||
MatchTenders(ctx context.Context, companyID uuid.UUID) ([]*TenderMatch, error)
|
||||
ExtractTenderRequirements(ctx context.Context, tenderID uuid.UUID) ([]*Requirement, error)
|
||||
TranslateDocument(ctx context.Context, text, sourceLanguage, targetLanguage string) (string, error)
|
||||
ClassifyTender(ctx context.Context, tender *Tender) ([]string, error)
|
||||
ExtractKeywords(ctx context.Context, text string) ([]string, error)
|
||||
}
|
||||
|
||||
// NotificationService defines notification methods
|
||||
type NotificationService interface {
|
||||
SendNotification(ctx context.Context, notification *Notification) error
|
||||
SendBulkNotifications(ctx context.Context, notifications []*Notification) error
|
||||
GetUserNotifications(ctx context.Context, userID uuid.UUID, limit, offset int) ([]*Notification, error)
|
||||
MarkNotificationAsRead(ctx context.Context, id uuid.UUID) error
|
||||
CreateTenderNotification(ctx context.Context, tender *Tender, companyIDs []uuid.UUID) error
|
||||
}
|
||||
|
||||
// Cache interface defines caching methods
|
||||
type CacheService interface {
|
||||
Set(ctx context.Context, key string, value interface{}, ttl int) error
|
||||
Get(ctx context.Context, key string, dest interface{}) error
|
||||
Delete(ctx context.Context, key string) error
|
||||
Exists(ctx context.Context, key string) (bool, error)
|
||||
SetHash(ctx context.Context, key, field string, value interface{}) error
|
||||
GetHash(ctx context.Context, key, field string, dest interface{}) error
|
||||
}
|
||||
|
||||
// Queue interface defines message queue methods
|
||||
type QueueService interface {
|
||||
Publish(ctx context.Context, queue string, message interface{}) error
|
||||
Subscribe(ctx context.Context, queue string, handler func([]byte) error) error
|
||||
PublishDelayed(ctx context.Context, queue string, message interface{}, delay int) error
|
||||
}
|
||||
|
||||
// Search interface defines search engine methods
|
||||
type SearchService interface {
|
||||
IndexTender(ctx context.Context, tender *Tender) error
|
||||
SearchTenders(ctx context.Context, query string, filters map[string]interface{}, limit, offset int) ([]*Tender, error)
|
||||
DeleteTender(ctx context.Context, tenderID uuid.UUID) error
|
||||
BulkIndexTenders(ctx context.Context, tenders []*Tender) error
|
||||
}
|
||||
|
||||
// DTOs and Request/Response types
|
||||
|
||||
// TenderFilters defines filters for tender queries
|
||||
type TenderFilters struct {
|
||||
Type *TenderType `json:"type,omitempty"`
|
||||
Status *TenderStatus `json:"status,omitempty"`
|
||||
CPVCodes []string `json:"cpv_codes,omitempty"`
|
||||
Categories []string `json:"categories,omitempty"`
|
||||
Region *string `json:"region,omitempty"`
|
||||
Country *string `json:"country,omitempty"`
|
||||
MinBudget *float64 `json:"min_budget,omitempty"`
|
||||
MaxBudget *float64 `json:"max_budget,omitempty"`
|
||||
Keywords []string `json:"keywords,omitempty"`
|
||||
DateFrom *string `json:"date_from,omitempty"`
|
||||
DateTo *string `json:"date_to,omitempty"`
|
||||
}
|
||||
|
||||
// Customer Authentication DTOs
|
||||
type CustomerRegisterRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"required,min=8"`
|
||||
FirstName string `json:"first_name" validate:"required"`
|
||||
LastName string `json:"last_name" validate:"required"`
|
||||
Phone string `json:"phone"`
|
||||
CompanyID string `json:"company_id,omitempty"` // Optional, will create new company if not provided
|
||||
}
|
||||
|
||||
type CustomerAuthResponse struct {
|
||||
Customer *Customer `json:"customer"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
// Panel User Authentication DTOs
|
||||
type CreateUserRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"required,min=8"`
|
||||
FirstName string `json:"first_name" validate:"required"`
|
||||
LastName string `json:"last_name" validate:"required"`
|
||||
Role UserRole `json:"role" validate:"required"`
|
||||
Department string `json:"department"`
|
||||
Permissions []string `json:"permissions"`
|
||||
}
|
||||
|
||||
type UserAuthResponse struct {
|
||||
User *User `json:"user"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
// Shared Authentication DTOs
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
}
|
||||
|
||||
// RegisterRequest defines user registration request (kept for backward compatibility)
|
||||
type RegisterRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"required,min=8"`
|
||||
FirstName string `json:"first_name" validate:"required"`
|
||||
LastName string `json:"last_name" validate:"required"`
|
||||
CompanyID string `json:"company_id,omitempty"`
|
||||
}
|
||||
|
||||
// AuthResponse defines authentication response (kept for backward compatibility)
|
||||
type AuthResponse struct {
|
||||
User *User `json:"user,omitempty"`
|
||||
Customer *Customer `json:"customer,omitempty"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
// CreateCompanyRequest defines company creation request
|
||||
type CreateCompanyRequest struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Description string `json:"description"`
|
||||
Industry string `json:"industry" validate:"required"`
|
||||
Country string `json:"country" validate:"required"`
|
||||
Website string `json:"website"`
|
||||
Keywords []string `json:"keywords"`
|
||||
Products []string `json:"products"`
|
||||
Services []string `json:"services"`
|
||||
}
|
||||
|
||||
// UpdateCompanyRequest defines company update request
|
||||
type UpdateCompanyRequest struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Industry *string `json:"industry,omitempty"`
|
||||
Country *string `json:"country,omitempty"`
|
||||
Website *string `json:"website,omitempty"`
|
||||
Keywords []string `json:"keywords,omitempty"`
|
||||
Products []string `json:"products,omitempty"`
|
||||
Services []string `json:"services,omitempty"`
|
||||
Capabilities []string `json:"capabilities,omitempty"`
|
||||
}
|
||||
|
||||
// UploadDocumentRequest defines document upload request
|
||||
type UploadDocumentRequest struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Type DocumentType `json:"type" validate:"required"`
|
||||
Category string `json:"category" validate:"required"`
|
||||
Description string `json:"description"`
|
||||
FilePath string `json:"file_path" validate:"required"`
|
||||
Size int64 `json:"size"`
|
||||
ContentType string `json:"content_type"`
|
||||
ExpiryDate *string `json:"expiry_date,omitempty"`
|
||||
Keywords []string `json:"keywords"`
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"tm/internal/domain"
|
||||
"tm/pkg/logger"
|
||||
"tm/pkg/response"
|
||||
)
|
||||
|
||||
// AuthHandler handles authentication related HTTP requests (legacy/generic)
|
||||
// This handler provides backward compatibility and can handle both customer and user authentication
|
||||
type AuthHandler struct {
|
||||
customerAuthService domain.CustomerAuthService
|
||||
userAuthService domain.UserAuthService
|
||||
validator *validator.Validate
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewAuthHandler creates a new authentication handler
|
||||
func NewAuthHandler(
|
||||
customerAuthService domain.CustomerAuthService,
|
||||
userAuthService domain.UserAuthService,
|
||||
validator *validator.Validate,
|
||||
logger logger.Logger,
|
||||
) *AuthHandler {
|
||||
return &AuthHandler{
|
||||
customerAuthService: customerAuthService,
|
||||
userAuthService: userAuthService,
|
||||
validator: validator,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Register handles user registration (defaults to customer registration)
|
||||
// @Summary Register new user
|
||||
// @Description Create a new user account (defaults to customer)
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body domain.RegisterRequest true "Registration request"
|
||||
// @Success 201 {object} response.APIResponse{data=domain.AuthResponse}
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 409 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /auth/register [post]
|
||||
func (h *AuthHandler) Register(c echo.Context) error {
|
||||
var req domain.RegisterRequest
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
h.logger.Warn("Failed to bind register request", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
h.logger.Warn("Register request validation failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": req.Email,
|
||||
})
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Convert to customer registration request
|
||||
customerReq := domain.CustomerRegisterRequest{
|
||||
Email: req.Email,
|
||||
Password: req.Password,
|
||||
FirstName: req.FirstName,
|
||||
LastName: req.LastName,
|
||||
CompanyID: req.CompanyID,
|
||||
}
|
||||
|
||||
// Call customer service (default registration is for customers)
|
||||
customerAuthResponse, err := h.customerAuthService.Register(c.Request().Context(), customerReq)
|
||||
if err != nil {
|
||||
h.logger.Error("Registration failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": req.Email,
|
||||
})
|
||||
|
||||
if err.Error() == "customer already exists" {
|
||||
return response.Conflict(c, "User already exists")
|
||||
}
|
||||
|
||||
return response.InternalServerError(c, "Registration failed")
|
||||
}
|
||||
|
||||
// Convert to generic auth response for backward compatibility
|
||||
authResponse := &domain.AuthResponse{
|
||||
Customer: customerAuthResponse.Customer,
|
||||
AccessToken: customerAuthResponse.AccessToken,
|
||||
RefreshToken: customerAuthResponse.RefreshToken,
|
||||
ExpiresIn: customerAuthResponse.ExpiresIn,
|
||||
}
|
||||
|
||||
return response.Created(c, authResponse, "User registered successfully")
|
||||
}
|
||||
|
||||
// Login handles user authentication (tries both customer and panel user)
|
||||
// @Summary User login
|
||||
// @Description Authenticate user and return tokens
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body domain.LoginRequest true "Login request"
|
||||
// @Success 200 {object} response.APIResponse{data=domain.AuthResponse}
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /auth/login [post]
|
||||
func (h *AuthHandler) Login(c echo.Context) error {
|
||||
var req domain.LoginRequest
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
h.logger.Warn("Failed to bind login request", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
h.logger.Warn("Login request validation failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": req.Email,
|
||||
})
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Try customer login first
|
||||
if customerAuthResponse, err := h.customerAuthService.Login(c.Request().Context(), req); err == nil {
|
||||
h.logger.Info("Customer logged in successfully", map[string]interface{}{
|
||||
"customer_id": customerAuthResponse.Customer.ID.String(),
|
||||
"email": customerAuthResponse.Customer.Email,
|
||||
})
|
||||
|
||||
// Convert to generic auth response
|
||||
authResponse := &domain.AuthResponse{
|
||||
Customer: customerAuthResponse.Customer,
|
||||
AccessToken: customerAuthResponse.AccessToken,
|
||||
RefreshToken: customerAuthResponse.RefreshToken,
|
||||
ExpiresIn: customerAuthResponse.ExpiresIn,
|
||||
}
|
||||
|
||||
return response.Success(c, authResponse, "Login successful")
|
||||
}
|
||||
|
||||
// Try panel user login
|
||||
if userAuthResponse, err := h.userAuthService.Login(c.Request().Context(), req); err == nil {
|
||||
h.logger.Info("Panel user logged in successfully", map[string]interface{}{
|
||||
"user_id": userAuthResponse.User.ID.String(),
|
||||
"email": userAuthResponse.User.Email,
|
||||
"role": userAuthResponse.User.Role,
|
||||
})
|
||||
|
||||
// Convert to generic auth response
|
||||
authResponse := &domain.AuthResponse{
|
||||
User: userAuthResponse.User,
|
||||
AccessToken: userAuthResponse.AccessToken,
|
||||
RefreshToken: userAuthResponse.RefreshToken,
|
||||
ExpiresIn: userAuthResponse.ExpiresIn,
|
||||
}
|
||||
|
||||
return response.Success(c, authResponse, "Login successful")
|
||||
}
|
||||
|
||||
h.logger.Warn("Login failed for both customer and user", map[string]interface{}{
|
||||
"email": req.Email,
|
||||
})
|
||||
|
||||
return response.Unauthorized(c, "Invalid email or password")
|
||||
}
|
||||
|
||||
// RefreshToken handles token refresh (tries both customer and panel user)
|
||||
// @Summary Refresh access token
|
||||
// @Description Generate new access token using refresh token
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body RefreshTokenRequest true "Refresh token request"
|
||||
// @Success 200 {object} response.APIResponse{data=domain.AuthResponse}
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /auth/refresh [post]
|
||||
func (h *AuthHandler) RefreshToken(c echo.Context) error {
|
||||
var req struct {
|
||||
RefreshToken string `json:"refresh_token" validate:"required"`
|
||||
}
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Try customer token refresh first
|
||||
if customerAuthResponse, err := h.customerAuthService.RefreshToken(c.Request().Context(), req.RefreshToken); err == nil {
|
||||
// Convert to generic auth response
|
||||
authResponse := &domain.AuthResponse{
|
||||
Customer: customerAuthResponse.Customer,
|
||||
AccessToken: customerAuthResponse.AccessToken,
|
||||
RefreshToken: customerAuthResponse.RefreshToken,
|
||||
ExpiresIn: customerAuthResponse.ExpiresIn,
|
||||
}
|
||||
|
||||
return response.Success(c, authResponse, "Token refreshed successfully")
|
||||
}
|
||||
|
||||
// Try panel user token refresh
|
||||
if userAuthResponse, err := h.userAuthService.RefreshToken(c.Request().Context(), req.RefreshToken); err == nil {
|
||||
// Convert to generic auth response
|
||||
authResponse := &domain.AuthResponse{
|
||||
User: userAuthResponse.User,
|
||||
AccessToken: userAuthResponse.AccessToken,
|
||||
RefreshToken: userAuthResponse.RefreshToken,
|
||||
ExpiresIn: userAuthResponse.ExpiresIn,
|
||||
}
|
||||
|
||||
return response.Success(c, authResponse, "Token refreshed successfully")
|
||||
}
|
||||
|
||||
h.logger.Warn("Token refresh failed for both customer and user", map[string]interface{}{})
|
||||
|
||||
return response.Unauthorized(c, "Invalid refresh token")
|
||||
}
|
||||
|
||||
// ChangePassword handles password change (determines user type from context)
|
||||
// @Summary Change user password
|
||||
// @Description Change authenticated user's password
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param request body ChangePasswordRequest true "Change password request"
|
||||
// @Success 200 {object} response.APIResponse
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /auth/change-password [post]
|
||||
func (h *AuthHandler) ChangePassword(c echo.Context) error {
|
||||
var req struct {
|
||||
OldPassword string `json:"old_password" validate:"required"`
|
||||
NewPassword string `json:"new_password" validate:"required,min=8"`
|
||||
}
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Check if it's a customer
|
||||
if customer, ok := c.Get("customer").(*domain.Customer); ok {
|
||||
err := h.customerAuthService.ChangePassword(c.Request().Context(), customer.ID, req.OldPassword, req.NewPassword)
|
||||
if err != nil {
|
||||
h.logger.Error("Customer password change failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": customer.ID.String(),
|
||||
})
|
||||
|
||||
if err.Error() == "invalid old password" {
|
||||
return response.BadRequest(c, "Invalid old password", "")
|
||||
}
|
||||
|
||||
return response.InternalServerError(c, "Password change failed")
|
||||
}
|
||||
|
||||
return response.Success(c, nil, "Password changed successfully")
|
||||
}
|
||||
|
||||
// Check if it's a panel user
|
||||
if user, ok := c.Get("user").(*domain.User); ok {
|
||||
err := h.userAuthService.ChangePassword(c.Request().Context(), user.ID, req.OldPassword, req.NewPassword)
|
||||
if err != nil {
|
||||
h.logger.Error("Panel user password change failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": user.ID.String(),
|
||||
})
|
||||
|
||||
if err.Error() == "invalid old password" {
|
||||
return response.BadRequest(c, "Invalid old password", "")
|
||||
}
|
||||
|
||||
return response.InternalServerError(c, "Password change failed")
|
||||
}
|
||||
|
||||
return response.Success(c, nil, "Password changed successfully")
|
||||
}
|
||||
|
||||
return response.Unauthorized(c, "Authentication required")
|
||||
}
|
||||
|
||||
// GetProfile returns current user profile (determines user type from context)
|
||||
// @Summary Get user profile
|
||||
// @Description Get authenticated user's profile information
|
||||
// @Tags auth
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Success 200 {object} response.APIResponse{data=domain.User}
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Router /auth/profile [get]
|
||||
func (h *AuthHandler) GetProfile(c echo.Context) error {
|
||||
// Check if it's a customer
|
||||
if customer, ok := c.Get("customer").(*domain.Customer); ok {
|
||||
return response.Success(c, customer, "Profile retrieved successfully")
|
||||
}
|
||||
|
||||
// Check if it's a panel user
|
||||
if user, ok := c.Get("user").(*domain.User); ok {
|
||||
return response.Success(c, user, "Profile retrieved successfully")
|
||||
}
|
||||
|
||||
return response.Unauthorized(c, "Authentication required")
|
||||
}
|
||||
|
||||
// Logout handles user logout (determines user type from context)
|
||||
// @Summary User logout
|
||||
// @Description Logout user (client should remove tokens)
|
||||
// @Tags auth
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Success 200 {object} response.APIResponse
|
||||
// @Router /auth/logout [post]
|
||||
func (h *AuthHandler) Logout(c echo.Context) error {
|
||||
// Check if it's a customer
|
||||
if customer, ok := c.Get("customer").(*domain.Customer); ok {
|
||||
h.logger.Info("Customer logged out", map[string]interface{}{
|
||||
"customer_id": customer.ID.String(),
|
||||
"email": customer.Email,
|
||||
})
|
||||
return response.Success(c, nil, "Logged out successfully")
|
||||
}
|
||||
|
||||
// Check if it's a panel user
|
||||
if user, ok := c.Get("user").(*domain.User); ok {
|
||||
h.logger.Info("Panel user logged out", map[string]interface{}{
|
||||
"user_id": user.ID.String(),
|
||||
"email": user.Email,
|
||||
})
|
||||
return response.Success(c, nil, "Logged out successfully")
|
||||
}
|
||||
|
||||
return response.Success(c, nil, "Logged out successfully")
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"tm/internal/domain"
|
||||
"tm/pkg/logger"
|
||||
"tm/pkg/response"
|
||||
)
|
||||
|
||||
// CustomerAuthHandler handles authentication related HTTP requests for mobile customers
|
||||
type CustomerAuthHandler struct {
|
||||
customerAuthService domain.CustomerAuthService
|
||||
validator *validator.Validate
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewCustomerAuthHandler creates a new customer authentication handler
|
||||
func NewCustomerAuthHandler(
|
||||
customerAuthService domain.CustomerAuthService,
|
||||
validator *validator.Validate,
|
||||
logger logger.Logger,
|
||||
) *CustomerAuthHandler {
|
||||
return &CustomerAuthHandler{
|
||||
customerAuthService: customerAuthService,
|
||||
validator: validator,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Register handles customer registration
|
||||
// @Summary Register new customer
|
||||
// @Description Create a new customer account for mobile app
|
||||
// @Tags customer-auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body domain.CustomerRegisterRequest true "Customer registration request"
|
||||
// @Success 201 {object} response.APIResponse{data=domain.CustomerAuthResponse}
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 409 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /api/mobile/auth/register [post]
|
||||
func (h *CustomerAuthHandler) Register(c echo.Context) error {
|
||||
var req domain.CustomerRegisterRequest
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
h.logger.Warn("Failed to bind customer register request", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
h.logger.Warn("Customer register request validation failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": req.Email,
|
||||
})
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Call service
|
||||
authResponse, err := h.customerAuthService.Register(c.Request().Context(), req)
|
||||
if err != nil {
|
||||
h.logger.Error("Customer registration failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": req.Email,
|
||||
})
|
||||
|
||||
if err.Error() == "customer already exists" {
|
||||
return response.Conflict(c, "Customer already exists")
|
||||
}
|
||||
|
||||
return response.InternalServerError(c, "Registration failed")
|
||||
}
|
||||
|
||||
h.logger.Info("Customer registered successfully", map[string]interface{}{
|
||||
"customer_id": authResponse.Customer.ID.String(),
|
||||
"email": authResponse.Customer.Email,
|
||||
})
|
||||
|
||||
return response.Created(c, authResponse, "Customer registered successfully")
|
||||
}
|
||||
|
||||
// Login handles customer authentication
|
||||
// @Summary Customer login
|
||||
// @Description Authenticate customer and return tokens
|
||||
// @Tags customer-auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body domain.LoginRequest true "Login request"
|
||||
// @Success 200 {object} response.APIResponse{data=domain.CustomerAuthResponse}
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /api/mobile/auth/login [post]
|
||||
func (h *CustomerAuthHandler) Login(c echo.Context) error {
|
||||
var req domain.LoginRequest
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
h.logger.Warn("Failed to bind customer login request", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
h.logger.Warn("Customer login request validation failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": req.Email,
|
||||
})
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Call service
|
||||
authResponse, err := h.customerAuthService.Login(c.Request().Context(), req)
|
||||
if err != nil {
|
||||
h.logger.Warn("Customer login failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": req.Email,
|
||||
})
|
||||
|
||||
if err.Error() == "invalid credentials" {
|
||||
return response.Unauthorized(c, "Invalid email or password")
|
||||
}
|
||||
|
||||
return response.InternalServerError(c, "Login failed")
|
||||
}
|
||||
|
||||
h.logger.Info("Customer logged in successfully", map[string]interface{}{
|
||||
"customer_id": authResponse.Customer.ID.String(),
|
||||
"email": authResponse.Customer.Email,
|
||||
})
|
||||
|
||||
return response.Success(c, authResponse, "Login successful")
|
||||
}
|
||||
|
||||
// RefreshToken handles token refresh for customers
|
||||
// @Summary Refresh customer access token
|
||||
// @Description Generate new access token using refresh token
|
||||
// @Tags customer-auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body RefreshTokenRequest true "Refresh token request"
|
||||
// @Success 200 {object} response.APIResponse{data=domain.CustomerAuthResponse}
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /api/mobile/auth/refresh [post]
|
||||
func (h *CustomerAuthHandler) RefreshToken(c echo.Context) error {
|
||||
var req struct {
|
||||
RefreshToken string `json:"refresh_token" validate:"required"`
|
||||
}
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Call service
|
||||
authResponse, err := h.customerAuthService.RefreshToken(c.Request().Context(), req.RefreshToken)
|
||||
if err != nil {
|
||||
h.logger.Warn("Customer token refresh failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
if err.Error() == "invalid refresh token" {
|
||||
return response.Unauthorized(c, "Invalid refresh token")
|
||||
}
|
||||
|
||||
return response.InternalServerError(c, "Token refresh failed")
|
||||
}
|
||||
|
||||
return response.Success(c, authResponse, "Token refreshed successfully")
|
||||
}
|
||||
|
||||
// ChangePassword handles password change for customers
|
||||
// @Summary Change customer password
|
||||
// @Description Change authenticated customer's password
|
||||
// @Tags customer-auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param request body ChangePasswordRequest true "Change password request"
|
||||
// @Success 200 {object} response.APIResponse
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /api/mobile/auth/change-password [post]
|
||||
func (h *CustomerAuthHandler) ChangePassword(c echo.Context) error {
|
||||
var req struct {
|
||||
OldPassword string `json:"old_password" validate:"required"`
|
||||
NewPassword string `json:"new_password" validate:"required,min=8"`
|
||||
}
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Get customer from context (set by auth middleware)
|
||||
customer, ok := c.Get("customer").(*domain.Customer)
|
||||
if !ok {
|
||||
return response.Unauthorized(c, "Authentication required")
|
||||
}
|
||||
|
||||
// Call service
|
||||
err := h.customerAuthService.ChangePassword(c.Request().Context(), customer.ID, req.OldPassword, req.NewPassword)
|
||||
if err != nil {
|
||||
h.logger.Error("Customer password change failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": customer.ID.String(),
|
||||
})
|
||||
|
||||
if err.Error() == "invalid old password" {
|
||||
return response.BadRequest(c, "Invalid old password", "")
|
||||
}
|
||||
|
||||
return response.InternalServerError(c, "Password change failed")
|
||||
}
|
||||
|
||||
h.logger.Info("Customer password changed successfully", map[string]interface{}{
|
||||
"customer_id": customer.ID.String(),
|
||||
})
|
||||
|
||||
return response.Success(c, nil, "Password changed successfully")
|
||||
}
|
||||
|
||||
// GetProfile returns current customer profile
|
||||
// @Summary Get customer profile
|
||||
// @Description Get authenticated customer's profile information
|
||||
// @Tags customer-auth
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Success 200 {object} response.APIResponse{data=domain.Customer}
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Router /api/mobile/auth/profile [get]
|
||||
func (h *CustomerAuthHandler) GetProfile(c echo.Context) error {
|
||||
// Get customer from context (set by auth middleware)
|
||||
customer, ok := c.Get("customer").(*domain.Customer)
|
||||
if !ok {
|
||||
return response.Unauthorized(c, "Authentication required")
|
||||
}
|
||||
|
||||
return response.Success(c, customer, "Profile retrieved successfully")
|
||||
}
|
||||
|
||||
// VerifyEmail handles email verification for customers
|
||||
// @Summary Verify customer email
|
||||
// @Description Verify customer's email address using token
|
||||
// @Tags customer-auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body VerifyEmailRequest true "Email verification request"
|
||||
// @Success 200 {object} response.APIResponse
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /api/mobile/auth/verify-email [post]
|
||||
func (h *CustomerAuthHandler) VerifyEmail(c echo.Context) error {
|
||||
var req struct {
|
||||
Token string `json:"token" validate:"required"`
|
||||
}
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Get customer from context (set by auth middleware)
|
||||
customer, ok := c.Get("customer").(*domain.Customer)
|
||||
if !ok {
|
||||
return response.Unauthorized(c, "Authentication required")
|
||||
}
|
||||
|
||||
// Call service
|
||||
err := h.customerAuthService.VerifyEmail(c.Request().Context(), customer.ID, req.Token)
|
||||
if err != nil {
|
||||
h.logger.Error("Customer email verification failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": customer.ID.String(),
|
||||
})
|
||||
|
||||
if err.Error() == "invalid verification token" {
|
||||
return response.BadRequest(c, "Invalid verification token", "")
|
||||
}
|
||||
|
||||
return response.InternalServerError(c, "Email verification failed")
|
||||
}
|
||||
|
||||
h.logger.Info("Customer email verified successfully", map[string]interface{}{
|
||||
"customer_id": customer.ID.String(),
|
||||
})
|
||||
|
||||
return response.Success(c, nil, "Email verified successfully")
|
||||
}
|
||||
|
||||
// ResendVerification handles resending verification email
|
||||
// @Summary Resend verification email
|
||||
// @Description Resend verification email to customer
|
||||
// @Tags customer-auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body ResendVerificationRequest true "Resend verification request"
|
||||
// @Success 200 {object} response.APIResponse
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /api/mobile/auth/resend-verification [post]
|
||||
func (h *CustomerAuthHandler) ResendVerification(c echo.Context) error {
|
||||
var req struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
}
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Call service
|
||||
err := h.customerAuthService.ResendVerification(c.Request().Context(), req.Email)
|
||||
if err != nil {
|
||||
h.logger.Error("Resend verification failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": req.Email,
|
||||
})
|
||||
|
||||
return response.InternalServerError(c, "Failed to resend verification email")
|
||||
}
|
||||
|
||||
return response.Success(c, nil, "Verification email sent successfully")
|
||||
}
|
||||
|
||||
// Logout handles customer logout
|
||||
// @Summary Customer logout
|
||||
// @Description Logout customer (client should remove tokens)
|
||||
// @Tags customer-auth
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Success 200 {object} response.APIResponse
|
||||
// @Router /api/mobile/auth/logout [post]
|
||||
func (h *CustomerAuthHandler) Logout(c echo.Context) error {
|
||||
// In a stateless JWT implementation, logout is typically handled client-side
|
||||
// by removing the tokens from storage. However, you could implement token
|
||||
// blacklisting here if needed.
|
||||
|
||||
customer, ok := c.Get("customer").(*domain.Customer)
|
||||
if ok {
|
||||
h.logger.Info("Customer logged out", map[string]interface{}{
|
||||
"customer_id": customer.ID.String(),
|
||||
"email": customer.Email,
|
||||
})
|
||||
}
|
||||
|
||||
return response.Success(c, nil, "Logged out successfully")
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"tm/internal/domain"
|
||||
"tm/pkg/logger"
|
||||
"tm/pkg/response"
|
||||
)
|
||||
|
||||
// UserAuthHandler handles authentication related HTTP requests for panel users
|
||||
type UserAuthHandler struct {
|
||||
userAuthService domain.UserAuthService
|
||||
validator *validator.Validate
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewUserAuthHandler creates a new panel user authentication handler
|
||||
func NewUserAuthHandler(
|
||||
userAuthService domain.UserAuthService,
|
||||
validator *validator.Validate,
|
||||
logger logger.Logger,
|
||||
) *UserAuthHandler {
|
||||
return &UserAuthHandler{
|
||||
userAuthService: userAuthService,
|
||||
validator: validator,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Login handles panel user authentication
|
||||
// @Summary Panel user login
|
||||
// @Description Authenticate panel user and return tokens
|
||||
// @Tags user-auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body domain.LoginRequest true "Login request"
|
||||
// @Success 200 {object} response.APIResponse{data=domain.UserAuthResponse}
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /api/admin/auth/login [post]
|
||||
func (h *UserAuthHandler) Login(c echo.Context) error {
|
||||
var req domain.LoginRequest
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
h.logger.Warn("Failed to bind panel user login request", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
h.logger.Warn("Panel user login request validation failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": req.Email,
|
||||
})
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Call service
|
||||
authResponse, err := h.userAuthService.Login(c.Request().Context(), req)
|
||||
if err != nil {
|
||||
h.logger.Warn("Panel user login failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": req.Email,
|
||||
})
|
||||
|
||||
if err.Error() == "invalid credentials" {
|
||||
return response.Unauthorized(c, "Invalid email or password")
|
||||
}
|
||||
|
||||
return response.InternalServerError(c, "Login failed")
|
||||
}
|
||||
|
||||
h.logger.Info("Panel user logged in successfully", map[string]interface{}{
|
||||
"user_id": authResponse.User.ID.String(),
|
||||
"email": authResponse.User.Email,
|
||||
"role": authResponse.User.Role,
|
||||
})
|
||||
|
||||
return response.Success(c, authResponse, "Login successful")
|
||||
}
|
||||
|
||||
// RefreshToken handles token refresh for panel users
|
||||
// @Summary Refresh panel user access token
|
||||
// @Description Generate new access token using refresh token
|
||||
// @Tags user-auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body RefreshTokenRequest true "Refresh token request"
|
||||
// @Success 200 {object} response.APIResponse{data=domain.UserAuthResponse}
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /api/admin/auth/refresh [post]
|
||||
func (h *UserAuthHandler) RefreshToken(c echo.Context) error {
|
||||
var req struct {
|
||||
RefreshToken string `json:"refresh_token" validate:"required"`
|
||||
}
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Call service
|
||||
authResponse, err := h.userAuthService.RefreshToken(c.Request().Context(), req.RefreshToken)
|
||||
if err != nil {
|
||||
h.logger.Warn("Panel user token refresh failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
if err.Error() == "invalid refresh token" {
|
||||
return response.Unauthorized(c, "Invalid refresh token")
|
||||
}
|
||||
|
||||
return response.InternalServerError(c, "Token refresh failed")
|
||||
}
|
||||
|
||||
return response.Success(c, authResponse, "Token refreshed successfully")
|
||||
}
|
||||
|
||||
// ChangePassword handles password change for panel users
|
||||
// @Summary Change panel user password
|
||||
// @Description Change authenticated panel user's password
|
||||
// @Tags user-auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param request body ChangePasswordRequest true "Change password request"
|
||||
// @Success 200 {object} response.APIResponse
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /api/admin/auth/change-password [post]
|
||||
func (h *UserAuthHandler) ChangePassword(c echo.Context) error {
|
||||
var req struct {
|
||||
OldPassword string `json:"old_password" validate:"required"`
|
||||
NewPassword string `json:"new_password" validate:"required,min=8"`
|
||||
}
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Get user from context (set by auth middleware)
|
||||
user, ok := c.Get("user").(*domain.User)
|
||||
if !ok {
|
||||
return response.Unauthorized(c, "Authentication required")
|
||||
}
|
||||
|
||||
// Call service
|
||||
err := h.userAuthService.ChangePassword(c.Request().Context(), user.ID, req.OldPassword, req.NewPassword)
|
||||
if err != nil {
|
||||
h.logger.Error("Panel user password change failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": user.ID.String(),
|
||||
})
|
||||
|
||||
if err.Error() == "invalid old password" {
|
||||
return response.BadRequest(c, "Invalid old password", "")
|
||||
}
|
||||
|
||||
return response.InternalServerError(c, "Password change failed")
|
||||
}
|
||||
|
||||
h.logger.Info("Panel user password changed successfully", map[string]interface{}{
|
||||
"user_id": user.ID.String(),
|
||||
})
|
||||
|
||||
return response.Success(c, nil, "Password changed successfully")
|
||||
}
|
||||
|
||||
// GetProfile returns current panel user profile
|
||||
// @Summary Get panel user profile
|
||||
// @Description Get authenticated panel user's profile information
|
||||
// @Tags user-auth
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Success 200 {object} response.APIResponse{data=domain.User}
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Router /api/admin/auth/profile [get]
|
||||
func (h *UserAuthHandler) GetProfile(c echo.Context) error {
|
||||
// Get user from context (set by auth middleware)
|
||||
user, ok := c.Get("user").(*domain.User)
|
||||
if !ok {
|
||||
return response.Unauthorized(c, "Authentication required")
|
||||
}
|
||||
|
||||
return response.Success(c, user, "Profile retrieved successfully")
|
||||
}
|
||||
|
||||
// CreatePanelUser handles creating new panel users (admin only)
|
||||
// @Summary Create new panel user
|
||||
// @Description Create a new panel user (admin only)
|
||||
// @Tags user-auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param request body domain.CreateUserRequest true "Create user request"
|
||||
// @Success 201 {object} response.APIResponse{data=domain.User}
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 403 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /api/admin/users [post]
|
||||
func (h *UserAuthHandler) CreatePanelUser(c echo.Context) error {
|
||||
var req domain.CreateUserRequest
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
h.logger.Warn("Failed to bind create user request", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
h.logger.Warn("Create user request validation failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": req.Email,
|
||||
})
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Get current user from context (set by auth middleware)
|
||||
currentUser, ok := c.Get("user").(*domain.User)
|
||||
if !ok {
|
||||
return response.Unauthorized(c, "Authentication required")
|
||||
}
|
||||
|
||||
// Check if current user can create users
|
||||
if !currentUser.CanManageUsers() {
|
||||
return response.Forbidden(c, "Insufficient permissions to create users")
|
||||
}
|
||||
|
||||
// Call service
|
||||
newUser, err := h.userAuthService.CreatePanelUser(c.Request().Context(), req, currentUser.ID)
|
||||
if err != nil {
|
||||
h.logger.Error("Panel user creation failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": req.Email,
|
||||
"created_by": currentUser.ID.String(),
|
||||
})
|
||||
|
||||
if err.Error() == "user already exists" {
|
||||
return response.Conflict(c, "User already exists")
|
||||
}
|
||||
|
||||
return response.InternalServerError(c, "User creation failed")
|
||||
}
|
||||
|
||||
h.logger.Info("Panel user created successfully", map[string]interface{}{
|
||||
"user_id": newUser.ID.String(),
|
||||
"email": newUser.Email,
|
||||
"role": newUser.Role,
|
||||
"created_by": currentUser.ID.String(),
|
||||
})
|
||||
|
||||
return response.Created(c, newUser, "User created successfully")
|
||||
}
|
||||
|
||||
// UpdateUserPermissions handles updating user permissions (admin only)
|
||||
// @Summary Update user permissions
|
||||
// @Description Update panel user permissions (admin only)
|
||||
// @Tags user-auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param user_id path string true "User ID"
|
||||
// @Param request body UpdatePermissionsRequest true "Update permissions request"
|
||||
// @Success 200 {object} response.APIResponse
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 403 {object} response.APIResponse
|
||||
// @Failure 404 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /api/admin/users/{user_id}/permissions [put]
|
||||
func (h *UserAuthHandler) UpdateUserPermissions(c echo.Context) error {
|
||||
var req struct {
|
||||
Permissions []string `json:"permissions" validate:"required"`
|
||||
}
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Get user ID from path
|
||||
userIDStr := c.Param("user_id")
|
||||
if userIDStr == "" {
|
||||
return response.BadRequest(c, "User ID is required", "")
|
||||
}
|
||||
|
||||
userID, err := uuid.Parse(userIDStr)
|
||||
if err != nil {
|
||||
return response.BadRequest(c, "Invalid user ID format", "")
|
||||
}
|
||||
|
||||
// Get current user from context (set by auth middleware)
|
||||
currentUser, ok := c.Get("user").(*domain.User)
|
||||
if !ok {
|
||||
return response.Unauthorized(c, "Authentication required")
|
||||
}
|
||||
|
||||
// Check if current user can manage users
|
||||
if !currentUser.CanManageUsers() {
|
||||
return response.Forbidden(c, "Insufficient permissions to update user permissions")
|
||||
}
|
||||
|
||||
// Call service
|
||||
err = h.userAuthService.UpdateUserPermissions(c.Request().Context(), userID, req.Permissions, currentUser.ID)
|
||||
if err != nil {
|
||||
h.logger.Error("User permissions update failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": userID.String(),
|
||||
"updated_by": currentUser.ID.String(),
|
||||
})
|
||||
|
||||
if err.Error() == "user not found" {
|
||||
return response.NotFound(c, "User not found")
|
||||
}
|
||||
|
||||
return response.InternalServerError(c, "Permissions update failed")
|
||||
}
|
||||
|
||||
h.logger.Info("User permissions updated successfully", map[string]interface{}{
|
||||
"user_id": userID.String(),
|
||||
"permissions": req.Permissions,
|
||||
"updated_by": currentUser.ID.String(),
|
||||
})
|
||||
|
||||
return response.Success(c, nil, "Permissions updated successfully")
|
||||
}
|
||||
|
||||
// Logout handles panel user logout
|
||||
// @Summary Panel user logout
|
||||
// @Description Logout panel user (client should remove tokens)
|
||||
// @Tags user-auth
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Success 200 {object} response.APIResponse
|
||||
// @Router /api/admin/auth/logout [post]
|
||||
func (h *UserAuthHandler) Logout(c echo.Context) error {
|
||||
// In a stateless JWT implementation, logout is typically handled client-side
|
||||
// by removing the tokens from storage. However, you could implement token
|
||||
// blacklisting here if needed.
|
||||
|
||||
user, ok := c.Get("user").(*domain.User)
|
||||
if ok {
|
||||
h.logger.Info("Panel user logged out", map[string]interface{}{
|
||||
"user_id": user.ID.String(),
|
||||
"email": user.Email,
|
||||
})
|
||||
}
|
||||
|
||||
return response.Success(c, nil, "Logged out successfully")
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package infrastructure
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Config holds all configuration for the application
|
||||
type Config struct {
|
||||
Server ServerConfig `mapstructure:"server"`
|
||||
Database DatabaseConfig `mapstructure:"database"`
|
||||
Cache CacheConfig `mapstructure:"cache"`
|
||||
Queue QueueConfig `mapstructure:"queue"`
|
||||
Search SearchConfig `mapstructure:"search"`
|
||||
Auth AuthConfig `mapstructure:"auth"`
|
||||
AI AIConfig `mapstructure:"ai"`
|
||||
Scraping ScrapingConfig `mapstructure:"scraping"`
|
||||
Logging LoggingConfig `mapstructure:"logging"`
|
||||
RateLimit RateLimitConfig `mapstructure:"rate_limiting"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
Timeout time.Duration `mapstructure:"timeout"`
|
||||
ReadTimeout time.Duration `mapstructure:"read_timeout"`
|
||||
WriteTimeout time.Duration `mapstructure:"write_timeout"`
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
MongoDB MongoConfig `mapstructure:"mongodb"`
|
||||
}
|
||||
|
||||
type MongoConfig struct {
|
||||
URI string `mapstructure:"uri"`
|
||||
Name string `mapstructure:"name"`
|
||||
Timeout time.Duration `mapstructure:"timeout"`
|
||||
MaxPoolSize int `mapstructure:"max_pool_size"`
|
||||
}
|
||||
|
||||
type CacheConfig struct {
|
||||
Redis RedisConfig `mapstructure:"redis"`
|
||||
}
|
||||
|
||||
type RedisConfig struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
Password string `mapstructure:"password"`
|
||||
DB int `mapstructure:"db"`
|
||||
PoolSize int `mapstructure:"pool_size"`
|
||||
}
|
||||
|
||||
type QueueConfig struct {
|
||||
RabbitMQ RabbitMQConfig `mapstructure:"rabbitmq"`
|
||||
}
|
||||
|
||||
type RabbitMQConfig struct {
|
||||
URL string `mapstructure:"url"`
|
||||
Exchange string `mapstructure:"exchange"`
|
||||
Queues map[string]string `mapstructure:"queues"`
|
||||
}
|
||||
|
||||
type SearchConfig struct {
|
||||
Elasticsearch ElasticsearchConfig `mapstructure:"elasticsearch"`
|
||||
}
|
||||
|
||||
type ElasticsearchConfig struct {
|
||||
URLs []string `mapstructure:"urls"`
|
||||
Username string `mapstructure:"username"`
|
||||
Password string `mapstructure:"password"`
|
||||
IndexPrefix string `mapstructure:"index_prefix"`
|
||||
}
|
||||
|
||||
type AuthConfig struct {
|
||||
JWT JWTConfig `mapstructure:"jwt"`
|
||||
}
|
||||
|
||||
type JWTConfig struct {
|
||||
Secret string `mapstructure:"secret"`
|
||||
AccessTokenDuration time.Duration `mapstructure:"access_token_duration"`
|
||||
RefreshTokenDuration time.Duration `mapstructure:"refresh_token_duration"`
|
||||
}
|
||||
|
||||
type AIConfig struct {
|
||||
OpenAI OpenAIConfig `mapstructure:"openai"`
|
||||
}
|
||||
|
||||
type OpenAIConfig struct {
|
||||
APIKey string `mapstructure:"api_key"`
|
||||
Model string `mapstructure:"model"`
|
||||
MaxTokens int `mapstructure:"max_tokens"`
|
||||
}
|
||||
|
||||
type ScrapingConfig struct {
|
||||
UserAgent string `mapstructure:"user_agent"`
|
||||
Timeout time.Duration `mapstructure:"timeout"`
|
||||
MaxRetries int `mapstructure:"max_retries"`
|
||||
DelayBetweenRequests time.Duration `mapstructure:"delay_between_requests"`
|
||||
}
|
||||
|
||||
type LoggingConfig struct {
|
||||
Level string `mapstructure:"level"`
|
||||
Format string `mapstructure:"format"`
|
||||
Output string `mapstructure:"output"`
|
||||
File LogFileConfig `mapstructure:"file"`
|
||||
}
|
||||
|
||||
type LogFileConfig struct {
|
||||
Path string `mapstructure:"path"`
|
||||
MaxSize int `mapstructure:"max_size"` // MB
|
||||
MaxBackups int `mapstructure:"max_backups"`
|
||||
MaxAge int `mapstructure:"max_age"` // days
|
||||
Compress bool `mapstructure:"compress"`
|
||||
}
|
||||
|
||||
type RateLimitConfig struct {
|
||||
RequestsPerMinute int `mapstructure:"requests_per_minute"`
|
||||
Burst int `mapstructure:"burst"`
|
||||
}
|
||||
|
||||
// LoadConfig loads configuration from file and environment variables
|
||||
func LoadConfig(path string) (*Config, error) {
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
viper.AddConfigPath(path)
|
||||
|
||||
// Enable reading from environment variables
|
||||
viper.AutomaticEnv()
|
||||
|
||||
// Read configuration file
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var config Config
|
||||
if err := viper.Unmarshal(&config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
|
||||
"tm/internal/domain"
|
||||
"tm/pkg/logger"
|
||||
)
|
||||
|
||||
// CompanyRepositoryImpl implements the CompanyRepository interface
|
||||
type CompanyRepositoryImpl struct {
|
||||
collection *mongo.Collection
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewCompanyRepository creates a new company repository
|
||||
func NewCompanyRepository(db *mongo.Database, logger logger.Logger) domain.CompanyRepository {
|
||||
collection := db.Collection("companies")
|
||||
|
||||
// Create indexes
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Name index
|
||||
nameIndex := mongo.IndexModel{
|
||||
Keys: bson.D{{"name", 1}},
|
||||
}
|
||||
|
||||
// Created at index
|
||||
createdAtIndex := mongo.IndexModel{
|
||||
Keys: bson.D{{"created_at", -1}},
|
||||
}
|
||||
|
||||
_, err := collection.Indexes().CreateMany(ctx, []mongo.IndexModel{
|
||||
nameIndex,
|
||||
createdAtIndex,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Warn("Failed to create company indexes", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return &CompanyRepositoryImpl{
|
||||
collection: collection,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Create creates a new company
|
||||
func (r *CompanyRepositoryImpl) Create(ctx context.Context, company *domain.Company) error {
|
||||
// Set created/updated timestamps
|
||||
now := time.Now()
|
||||
company.CreatedAt = now
|
||||
company.UpdatedAt = now
|
||||
|
||||
// Insert company
|
||||
_, err := r.collection.InsertOne(ctx, company)
|
||||
if err != nil {
|
||||
if mongo.IsDuplicateKeyError(err) {
|
||||
return errors.New("company already exists")
|
||||
}
|
||||
r.logger.Error("Failed to create company", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": company.ID.String(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Info("Company created successfully", map[string]interface{}{
|
||||
"company_id": company.ID.String(),
|
||||
"name": company.Name,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID retrieves a company by ID
|
||||
func (r *CompanyRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*domain.Company, error) {
|
||||
var company domain.Company
|
||||
|
||||
filter := bson.M{"_id": id}
|
||||
err := r.collection.FindOne(ctx, filter).Decode(&company)
|
||||
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, errors.New("company not found")
|
||||
}
|
||||
r.logger.Error("Failed to get company by ID", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": id.String(),
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &company, nil
|
||||
}
|
||||
|
||||
// Update updates a company
|
||||
func (r *CompanyRepositoryImpl) Update(ctx context.Context, company *domain.Company) error {
|
||||
// Set updated timestamp
|
||||
company.UpdatedAt = time.Now()
|
||||
|
||||
filter := bson.M{"_id": company.ID}
|
||||
update := bson.M{"$set": company}
|
||||
|
||||
result, err := r.collection.UpdateOne(ctx, filter, update)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to update company", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": company.ID.String(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
if result.MatchedCount == 0 {
|
||||
return errors.New("company not found")
|
||||
}
|
||||
|
||||
r.logger.Info("Company updated successfully", map[string]interface{}{
|
||||
"company_id": company.ID.String(),
|
||||
"name": company.Name,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete deletes a company
|
||||
func (r *CompanyRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
filter := bson.M{"_id": id}
|
||||
|
||||
result, err := r.collection.DeleteOne(ctx, filter)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to delete company", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": id.String(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
if result.DeletedCount == 0 {
|
||||
return errors.New("company not found")
|
||||
}
|
||||
|
||||
r.logger.Info("Company deleted successfully", map[string]interface{}{
|
||||
"company_id": id.String(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// List retrieves companies with pagination
|
||||
func (r *CompanyRepositoryImpl) List(ctx context.Context, limit, offset int) ([]*domain.Company, error) {
|
||||
var companies []*domain.Company
|
||||
|
||||
// Build options
|
||||
opts := options.Find()
|
||||
opts.SetLimit(int64(limit))
|
||||
opts.SetSkip(int64(offset))
|
||||
opts.SetSort(bson.D{{"created_at", -1}}) // Sort by created_at desc
|
||||
|
||||
cursor, err := r.collection.Find(ctx, bson.M{}, opts)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to list companies", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
if err = cursor.All(ctx, &companies); err != nil {
|
||||
r.logger.Error("Failed to decode companies", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return companies, nil
|
||||
}
|
||||
|
||||
// UpdateInterests updates company interests
|
||||
func (r *CompanyRepositoryImpl) UpdateInterests(ctx context.Context, id uuid.UUID, interests *domain.InterestProfile) error {
|
||||
filter := bson.M{"_id": id}
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
"interests": interests,
|
||||
"updated_at": time.Now(),
|
||||
},
|
||||
}
|
||||
|
||||
result, err := r.collection.UpdateOne(ctx, filter, update)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to update company interests", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": id.String(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
if result.MatchedCount == 0 {
|
||||
return errors.New("company not found")
|
||||
}
|
||||
|
||||
r.logger.Info("Company interests updated successfully", map[string]interface{}{
|
||||
"company_id": id.String(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
|
||||
"tm/internal/domain"
|
||||
"tm/pkg/logger"
|
||||
)
|
||||
|
||||
// CustomerRepositoryImpl implements the CustomerRepository interface
|
||||
type CustomerRepositoryImpl struct {
|
||||
collection *mongo.Collection
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewCustomerRepository creates a new customer repository
|
||||
func NewCustomerRepository(db *mongo.Database, logger logger.Logger) domain.CustomerRepository {
|
||||
collection := db.Collection("customers")
|
||||
|
||||
// Create indexes
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Email index (unique)
|
||||
emailIndex := mongo.IndexModel{
|
||||
Keys: bson.D{{"email", 1}},
|
||||
Options: options.Index().SetUnique(true),
|
||||
}
|
||||
|
||||
// Company ID index
|
||||
companyIndex := mongo.IndexModel{
|
||||
Keys: bson.D{{"company_id", 1}},
|
||||
}
|
||||
|
||||
// Active status index
|
||||
activeIndex := mongo.IndexModel{
|
||||
Keys: bson.D{{"is_active", 1}},
|
||||
}
|
||||
|
||||
// Created at index
|
||||
createdAtIndex := mongo.IndexModel{
|
||||
Keys: bson.D{{"created_at", -1}},
|
||||
}
|
||||
|
||||
_, err := collection.Indexes().CreateMany(ctx, []mongo.IndexModel{
|
||||
emailIndex,
|
||||
companyIndex,
|
||||
activeIndex,
|
||||
createdAtIndex,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Warn("Failed to create customer indexes", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return &CustomerRepositoryImpl{
|
||||
collection: collection,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Create creates a new customer
|
||||
func (r *CustomerRepositoryImpl) Create(ctx context.Context, customer *domain.Customer) error {
|
||||
// Set created/updated timestamps
|
||||
now := time.Now()
|
||||
customer.CreatedAt = now
|
||||
customer.UpdatedAt = now
|
||||
|
||||
// Insert customer
|
||||
_, err := r.collection.InsertOne(ctx, customer)
|
||||
if err != nil {
|
||||
if mongo.IsDuplicateKeyError(err) {
|
||||
return errors.New("customer already exists")
|
||||
}
|
||||
r.logger.Error("Failed to create customer", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": customer.Email,
|
||||
"customer_id": customer.ID.String(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Info("Customer created successfully", map[string]interface{}{
|
||||
"customer_id": customer.ID.String(),
|
||||
"email": customer.Email,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID retrieves a customer by ID
|
||||
func (r *CustomerRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*domain.Customer, error) {
|
||||
var customer domain.Customer
|
||||
|
||||
filter := bson.M{"_id": id}
|
||||
err := r.collection.FindOne(ctx, filter).Decode(&customer)
|
||||
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, errors.New("customer not found")
|
||||
}
|
||||
r.logger.Error("Failed to get customer by ID", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": id.String(),
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &customer, nil
|
||||
}
|
||||
|
||||
// GetByEmail retrieves a customer by email
|
||||
func (r *CustomerRepositoryImpl) GetByEmail(ctx context.Context, email string) (*domain.Customer, error) {
|
||||
var customer domain.Customer
|
||||
|
||||
filter := bson.M{"email": email}
|
||||
err := r.collection.FindOne(ctx, filter).Decode(&customer)
|
||||
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, errors.New("customer not found")
|
||||
}
|
||||
r.logger.Error("Failed to get customer by email", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": email,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &customer, nil
|
||||
}
|
||||
|
||||
// Update updates a customer
|
||||
func (r *CustomerRepositoryImpl) Update(ctx context.Context, customer *domain.Customer) error {
|
||||
// Set updated timestamp
|
||||
customer.UpdatedAt = time.Now()
|
||||
|
||||
filter := bson.M{"_id": customer.ID}
|
||||
update := bson.M{"$set": customer}
|
||||
|
||||
result, err := r.collection.UpdateOne(ctx, filter, update)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to update customer", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": customer.ID.String(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
if result.MatchedCount == 0 {
|
||||
return errors.New("customer not found")
|
||||
}
|
||||
|
||||
r.logger.Info("Customer updated successfully", map[string]interface{}{
|
||||
"customer_id": customer.ID.String(),
|
||||
"email": customer.Email,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete deletes a customer (soft delete by setting is_active to false)
|
||||
func (r *CustomerRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
filter := bson.M{"_id": id}
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
"is_active": false,
|
||||
"updated_at": time.Now(),
|
||||
},
|
||||
}
|
||||
|
||||
result, err := r.collection.UpdateOne(ctx, filter, update)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to delete customer", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": id.String(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
if result.MatchedCount == 0 {
|
||||
return errors.New("customer not found")
|
||||
}
|
||||
|
||||
r.logger.Info("Customer deleted successfully", map[string]interface{}{
|
||||
"customer_id": id.String(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// List retrieves customers with pagination
|
||||
func (r *CustomerRepositoryImpl) List(ctx context.Context, limit, offset int) ([]*domain.Customer, error) {
|
||||
var customers []*domain.Customer
|
||||
|
||||
// Build options
|
||||
opts := options.Find()
|
||||
opts.SetLimit(int64(limit))
|
||||
opts.SetSkip(int64(offset))
|
||||
opts.SetSort(bson.D{{"created_at", -1}}) // Sort by created_at desc
|
||||
|
||||
// Only active customers by default
|
||||
filter := bson.M{"is_active": true}
|
||||
|
||||
cursor, err := r.collection.Find(ctx, filter, opts)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to list customers", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
if err = cursor.All(ctx, &customers); err != nil {
|
||||
r.logger.Error("Failed to decode customers", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return customers, nil
|
||||
}
|
||||
|
||||
// GetByCompanyID retrieves customers by company ID with pagination
|
||||
func (r *CustomerRepositoryImpl) GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*domain.Customer, error) {
|
||||
var customers []*domain.Customer
|
||||
|
||||
// Build options
|
||||
opts := options.Find()
|
||||
opts.SetLimit(int64(limit))
|
||||
opts.SetSkip(int64(offset))
|
||||
opts.SetSort(bson.D{{"created_at", -1}}) // Sort by created_at desc
|
||||
|
||||
// Filter by company ID and active status
|
||||
filter := bson.M{
|
||||
"company_id": companyID,
|
||||
"is_active": true,
|
||||
}
|
||||
|
||||
cursor, err := r.collection.Find(ctx, filter, opts)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to get customers by company ID", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": companyID.String(),
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
if err = cursor.All(ctx, &customers); err != nil {
|
||||
r.logger.Error("Failed to decode customers by company", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": companyID.String(),
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return customers, nil
|
||||
}
|
||||
|
||||
// UpdatePreferences updates customer preferences
|
||||
func (r *CustomerRepositoryImpl) UpdatePreferences(ctx context.Context, id uuid.UUID, preferences *domain.CustomerPreferences) error {
|
||||
filter := bson.M{"_id": id}
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
"preferences": preferences,
|
||||
"updated_at": time.Now(),
|
||||
},
|
||||
}
|
||||
|
||||
result, err := r.collection.UpdateOne(ctx, filter, update)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to update customer preferences", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": id.String(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
if result.MatchedCount == 0 {
|
||||
return errors.New("customer not found")
|
||||
}
|
||||
|
||||
r.logger.Info("Customer preferences updated successfully", map[string]interface{}{
|
||||
"customer_id": id.String(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddDeviceToken adds a device token for push notifications
|
||||
func (r *CustomerRepositoryImpl) AddDeviceToken(ctx context.Context, id uuid.UUID, token string) error {
|
||||
filter := bson.M{"_id": id}
|
||||
update := bson.M{
|
||||
"$addToSet": bson.M{"device_tokens": token}, // Use $addToSet to avoid duplicates
|
||||
"$set": bson.M{"updated_at": time.Now()},
|
||||
}
|
||||
|
||||
result, err := r.collection.UpdateOne(ctx, filter, update)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to add device token", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": id.String(),
|
||||
"token": token,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
if result.MatchedCount == 0 {
|
||||
return errors.New("customer not found")
|
||||
}
|
||||
|
||||
r.logger.Info("Device token added successfully", map[string]interface{}{
|
||||
"customer_id": id.String(),
|
||||
"token": token,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveDeviceToken removes a device token
|
||||
func (r *CustomerRepositoryImpl) RemoveDeviceToken(ctx context.Context, id uuid.UUID, token string) error {
|
||||
filter := bson.M{"_id": id}
|
||||
update := bson.M{
|
||||
"$pull": bson.M{"device_tokens": token}, // Use $pull to remove the token
|
||||
"$set": bson.M{"updated_at": time.Now()},
|
||||
}
|
||||
|
||||
result, err := r.collection.UpdateOne(ctx, filter, update)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to remove device token", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": id.String(),
|
||||
"token": token,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
if result.MatchedCount == 0 {
|
||||
return errors.New("customer not found")
|
||||
}
|
||||
|
||||
r.logger.Info("Device token removed successfully", map[string]interface{}{
|
||||
"customer_id": id.String(),
|
||||
"token": token,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
|
||||
"tm/internal/domain"
|
||||
"tm/pkg/logger"
|
||||
)
|
||||
|
||||
// UserRepositoryImpl implements the UserRepository interface
|
||||
type UserRepositoryImpl struct {
|
||||
collection *mongo.Collection
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewUserRepository creates a new panel user repository
|
||||
func NewUserRepository(db *mongo.Database, logger logger.Logger) domain.UserRepository {
|
||||
collection := db.Collection("users")
|
||||
|
||||
// Create indexes
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Email index (unique)
|
||||
emailIndex := mongo.IndexModel{
|
||||
Keys: bson.D{{"email", 1}},
|
||||
Options: options.Index().SetUnique(true),
|
||||
}
|
||||
|
||||
// Role index
|
||||
roleIndex := mongo.IndexModel{
|
||||
Keys: bson.D{{"role", 1}},
|
||||
}
|
||||
|
||||
// Active status index
|
||||
activeIndex := mongo.IndexModel{
|
||||
Keys: bson.D{{"is_active", 1}},
|
||||
}
|
||||
|
||||
// Department index
|
||||
departmentIndex := mongo.IndexModel{
|
||||
Keys: bson.D{{"department", 1}},
|
||||
}
|
||||
|
||||
// Created at index
|
||||
createdAtIndex := mongo.IndexModel{
|
||||
Keys: bson.D{{"created_at", -1}},
|
||||
}
|
||||
|
||||
_, err := collection.Indexes().CreateMany(ctx, []mongo.IndexModel{
|
||||
emailIndex,
|
||||
roleIndex,
|
||||
activeIndex,
|
||||
departmentIndex,
|
||||
createdAtIndex,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Warn("Failed to create user indexes", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return &UserRepositoryImpl{
|
||||
collection: collection,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Create creates a new panel user
|
||||
func (r *UserRepositoryImpl) Create(ctx context.Context, user *domain.User) error {
|
||||
// Set created/updated timestamps
|
||||
now := time.Now()
|
||||
user.CreatedAt = now
|
||||
user.UpdatedAt = now
|
||||
|
||||
// Insert user
|
||||
_, err := r.collection.InsertOne(ctx, user)
|
||||
if err != nil {
|
||||
if mongo.IsDuplicateKeyError(err) {
|
||||
return errors.New("user already exists")
|
||||
}
|
||||
r.logger.Error("Failed to create user", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": user.Email,
|
||||
"user_id": user.ID.String(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Info("Panel user created successfully", map[string]interface{}{
|
||||
"user_id": user.ID.String(),
|
||||
"email": user.Email,
|
||||
"role": user.Role,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID retrieves a panel user by ID
|
||||
func (r *UserRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*domain.User, error) {
|
||||
var user domain.User
|
||||
|
||||
filter := bson.M{"_id": id}
|
||||
err := r.collection.FindOne(ctx, filter).Decode(&user)
|
||||
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
r.logger.Error("Failed to get user by ID", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": id.String(),
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetByEmail retrieves a panel user by email
|
||||
func (r *UserRepositoryImpl) GetByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||
var user domain.User
|
||||
|
||||
filter := bson.M{"email": email}
|
||||
err := r.collection.FindOne(ctx, filter).Decode(&user)
|
||||
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
r.logger.Error("Failed to get user by email", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": email,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// Update updates a panel user
|
||||
func (r *UserRepositoryImpl) Update(ctx context.Context, user *domain.User) error {
|
||||
// Set updated timestamp
|
||||
user.UpdatedAt = time.Now()
|
||||
|
||||
filter := bson.M{"_id": user.ID}
|
||||
update := bson.M{"$set": user}
|
||||
|
||||
result, err := r.collection.UpdateOne(ctx, filter, update)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to update user", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": user.ID.String(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
if result.MatchedCount == 0 {
|
||||
return errors.New("user not found")
|
||||
}
|
||||
|
||||
r.logger.Info("Panel user updated successfully", map[string]interface{}{
|
||||
"user_id": user.ID.String(),
|
||||
"email": user.Email,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete deletes a panel user (soft delete by setting is_active to false)
|
||||
func (r *UserRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
filter := bson.M{"_id": id}
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
"is_active": false,
|
||||
"updated_at": time.Now(),
|
||||
},
|
||||
}
|
||||
|
||||
result, err := r.collection.UpdateOne(ctx, filter, update)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to delete user", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": id.String(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
if result.MatchedCount == 0 {
|
||||
return errors.New("user not found")
|
||||
}
|
||||
|
||||
r.logger.Info("Panel user deleted successfully", map[string]interface{}{
|
||||
"user_id": id.String(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// List retrieves panel users with pagination
|
||||
func (r *UserRepositoryImpl) List(ctx context.Context, limit, offset int) ([]*domain.User, error) {
|
||||
var users []*domain.User
|
||||
|
||||
// Build options
|
||||
opts := options.Find()
|
||||
opts.SetLimit(int64(limit))
|
||||
opts.SetSkip(int64(offset))
|
||||
opts.SetSort(bson.D{{"created_at", -1}}) // Sort by created_at desc
|
||||
|
||||
// Only active users by default
|
||||
filter := bson.M{"is_active": true}
|
||||
|
||||
cursor, err := r.collection.Find(ctx, filter, opts)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to list users", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
if err = cursor.All(ctx, &users); err != nil {
|
||||
r.logger.Error("Failed to decode users", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// GetByRole retrieves panel users by role with pagination
|
||||
func (r *UserRepositoryImpl) GetByRole(ctx context.Context, role domain.UserRole, limit, offset int) ([]*domain.User, error) {
|
||||
var users []*domain.User
|
||||
|
||||
// Build options
|
||||
opts := options.Find()
|
||||
opts.SetLimit(int64(limit))
|
||||
opts.SetSkip(int64(offset))
|
||||
opts.SetSort(bson.D{{"created_at", -1}}) // Sort by created_at desc
|
||||
|
||||
// Filter by role and active status
|
||||
filter := bson.M{
|
||||
"role": role,
|
||||
"is_active": true,
|
||||
}
|
||||
|
||||
cursor, err := r.collection.Find(ctx, filter, opts)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to get users by role", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"role": role,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
if err = cursor.All(ctx, &users); err != nil {
|
||||
r.logger.Error("Failed to decode users by role", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"role": role,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// UpdatePermissions updates panel user permissions
|
||||
func (r *UserRepositoryImpl) UpdatePermissions(ctx context.Context, id uuid.UUID, permissions []string) error {
|
||||
filter := bson.M{"_id": id}
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
"permissions": permissions,
|
||||
"updated_at": time.Now(),
|
||||
},
|
||||
}
|
||||
|
||||
result, err := r.collection.UpdateOne(ctx, filter, update)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to update user permissions", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": id.String(),
|
||||
"permissions": permissions,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
if result.MatchedCount == 0 {
|
||||
return errors.New("user not found")
|
||||
}
|
||||
|
||||
r.logger.Info("Panel user permissions updated successfully", map[string]interface{}{
|
||||
"user_id": id.String(),
|
||||
"permissions": permissions,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"tm/internal/domain"
|
||||
"tm/pkg/logger"
|
||||
)
|
||||
|
||||
// CustomerServiceImpl implements the CustomerService interface
|
||||
type CustomerServiceImpl struct {
|
||||
customerRepo domain.CustomerRepository
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewCustomerService creates a new customer service
|
||||
func NewCustomerService(
|
||||
customerRepo domain.CustomerRepository,
|
||||
logger logger.Logger,
|
||||
) domain.CustomerService {
|
||||
return &CustomerServiceImpl{
|
||||
customerRepo: customerRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// GetCustomers retrieves customers with pagination (for panel users)
|
||||
func (s *CustomerServiceImpl) GetCustomers(ctx context.Context, limit, offset int) ([]*domain.Customer, error) {
|
||||
s.logger.Info("Retrieving customers", map[string]interface{}{
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
})
|
||||
|
||||
customers, err := s.customerRepo.List(ctx, limit, offset)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to retrieve customers", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
})
|
||||
return nil, errors.New("failed to retrieve customers")
|
||||
}
|
||||
|
||||
s.logger.Info("Customers retrieved successfully", map[string]interface{}{
|
||||
"count": len(customers),
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
})
|
||||
|
||||
return customers, nil
|
||||
}
|
||||
|
||||
// GetCustomerByID retrieves a customer by ID (for panel users)
|
||||
func (s *CustomerServiceImpl) GetCustomerByID(ctx context.Context, id uuid.UUID) (*domain.Customer, error) {
|
||||
s.logger.Info("Retrieving customer by ID", map[string]interface{}{
|
||||
"customer_id": id.String(),
|
||||
})
|
||||
|
||||
customer, err := s.customerRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to retrieve customer", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": id.String(),
|
||||
})
|
||||
return nil, errors.New("customer not found")
|
||||
}
|
||||
|
||||
s.logger.Info("Customer retrieved successfully", map[string]interface{}{
|
||||
"customer_id": customer.ID.String(),
|
||||
"email": customer.Email,
|
||||
})
|
||||
|
||||
return customer, nil
|
||||
}
|
||||
|
||||
// GetCustomersByCompany retrieves customers by company ID with pagination (for panel users)
|
||||
func (s *CustomerServiceImpl) GetCustomersByCompany(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*domain.Customer, error) {
|
||||
s.logger.Info("Retrieving customers by company", map[string]interface{}{
|
||||
"company_id": companyID.String(),
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
})
|
||||
|
||||
customers, err := s.customerRepo.GetByCompanyID(ctx, companyID, limit, offset)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to retrieve customers by company", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": companyID.String(),
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
})
|
||||
return nil, errors.New("failed to retrieve customers")
|
||||
}
|
||||
|
||||
s.logger.Info("Customers by company retrieved successfully", map[string]interface{}{
|
||||
"count": len(customers),
|
||||
"company_id": companyID.String(),
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
})
|
||||
|
||||
return customers, nil
|
||||
}
|
||||
|
||||
// UpdateCustomerStatus updates customer active status (for panel users)
|
||||
func (s *CustomerServiceImpl) UpdateCustomerStatus(ctx context.Context, customerID uuid.UUID, isActive bool, updatedBy uuid.UUID) error {
|
||||
s.logger.Info("Updating customer status", map[string]interface{}{
|
||||
"customer_id": customerID.String(),
|
||||
"is_active": isActive,
|
||||
"updated_by": updatedBy.String(),
|
||||
})
|
||||
|
||||
// Get customer
|
||||
customer, err := s.customerRepo.GetByID(ctx, customerID)
|
||||
if err != nil {
|
||||
s.logger.Error("Customer not found for status update", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": customerID.String(),
|
||||
})
|
||||
return errors.New("customer not found")
|
||||
}
|
||||
|
||||
// Update status
|
||||
customer.IsActive = isActive
|
||||
|
||||
// Save changes
|
||||
if err := s.customerRepo.Update(ctx, customer); err != nil {
|
||||
s.logger.Error("Failed to update customer status", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": customerID.String(),
|
||||
"is_active": isActive,
|
||||
"updated_by": updatedBy.String(),
|
||||
})
|
||||
return errors.New("failed to update customer status")
|
||||
}
|
||||
|
||||
s.logger.Info("Customer status updated successfully", map[string]interface{}{
|
||||
"customer_id": customerID.String(),
|
||||
"is_active": isActive,
|
||||
"updated_by": updatedBy.String(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateCustomerPreferences updates customer preferences (for panel users)
|
||||
func (s *CustomerServiceImpl) UpdateCustomerPreferences(ctx context.Context, customerID uuid.UUID, preferences *domain.CustomerPreferences) error {
|
||||
s.logger.Info("Updating customer preferences", map[string]interface{}{
|
||||
"customer_id": customerID.String(),
|
||||
})
|
||||
|
||||
// Verify customer exists
|
||||
_, err := s.customerRepo.GetByID(ctx, customerID)
|
||||
if err != nil {
|
||||
s.logger.Error("Customer not found for preferences update", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": customerID.String(),
|
||||
})
|
||||
return errors.New("customer not found")
|
||||
}
|
||||
|
||||
// Update preferences
|
||||
if err := s.customerRepo.UpdatePreferences(ctx, customerID, preferences); err != nil {
|
||||
s.logger.Error("Failed to update customer preferences", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": customerID.String(),
|
||||
})
|
||||
return errors.New("failed to update customer preferences")
|
||||
}
|
||||
|
||||
s.logger.Info("Customer preferences updated successfully", map[string]interface{}{
|
||||
"customer_id": customerID.String(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,497 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"tm/internal/domain"
|
||||
"tm/internal/infrastructure"
|
||||
"tm/pkg/logger"
|
||||
)
|
||||
|
||||
// CustomerAuthServiceImpl implements the CustomerAuthService interface
|
||||
type CustomerAuthServiceImpl struct {
|
||||
customerRepo domain.CustomerRepository
|
||||
companyRepo domain.CompanyRepository
|
||||
config *infrastructure.AuthConfig
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewCustomerAuthService creates a new customer authentication service
|
||||
func NewCustomerAuthService(
|
||||
customerRepo domain.CustomerRepository,
|
||||
companyRepo domain.CompanyRepository,
|
||||
config *infrastructure.AuthConfig,
|
||||
logger logger.Logger,
|
||||
) domain.CustomerAuthService {
|
||||
return &CustomerAuthServiceImpl{
|
||||
customerRepo: customerRepo,
|
||||
companyRepo: companyRepo,
|
||||
config: config,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Register creates a new customer account
|
||||
func (s *CustomerAuthServiceImpl) Register(ctx context.Context, req domain.CustomerRegisterRequest) (*domain.CustomerAuthResponse, error) {
|
||||
s.logger.Info("Registering new customer", map[string]interface{}{
|
||||
"email": req.Email,
|
||||
})
|
||||
|
||||
// Check if customer already exists
|
||||
existingCustomer, err := s.customerRepo.GetByEmail(ctx, req.Email)
|
||||
if err == nil && existingCustomer != nil {
|
||||
return nil, errors.New("customer already exists")
|
||||
}
|
||||
|
||||
// Hash password
|
||||
hashedPassword, err := s.hashPassword(req.Password)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to hash password", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, errors.New("failed to process password")
|
||||
}
|
||||
|
||||
// Handle company assignment
|
||||
var companyID uuid.UUID
|
||||
if req.CompanyID != "" {
|
||||
// Parse existing company ID
|
||||
companyID, err = uuid.Parse(req.CompanyID)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid company ID")
|
||||
}
|
||||
|
||||
// Verify company exists
|
||||
_, err = s.companyRepo.GetByID(ctx, companyID)
|
||||
if err != nil {
|
||||
return nil, errors.New("company not found")
|
||||
}
|
||||
} else {
|
||||
// Create new company for customer
|
||||
company := &domain.Company{
|
||||
ID: uuid.New(),
|
||||
Name: req.FirstName + " " + req.LastName + "'s Company",
|
||||
Description: "Personal company profile",
|
||||
Industry: "Not specified",
|
||||
Country: "Not specified",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.companyRepo.Create(ctx, company); err != nil {
|
||||
s.logger.Error("Failed to create company for customer", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": req.Email,
|
||||
})
|
||||
return nil, errors.New("failed to create company")
|
||||
}
|
||||
|
||||
companyID = company.ID
|
||||
}
|
||||
|
||||
// Create customer
|
||||
customer := &domain.Customer{
|
||||
ID: uuid.New(),
|
||||
Email: req.Email,
|
||||
Password: hashedPassword,
|
||||
FirstName: req.FirstName,
|
||||
LastName: req.LastName,
|
||||
Phone: req.Phone,
|
||||
Role: domain.CustomerRoleUser,
|
||||
CompanyID: companyID,
|
||||
IsActive: true,
|
||||
IsVerified: false, // Require email verification
|
||||
DeviceTokens: make([]string, 0),
|
||||
Preferences: domain.CustomerPreferences{
|
||||
Language: "en",
|
||||
NotificationSettings: domain.NotificationSettings{
|
||||
NewTenders: true,
|
||||
DeadlineReminder: true,
|
||||
StatusUpdates: true,
|
||||
SystemAlerts: true,
|
||||
},
|
||||
TenderAlerts: true,
|
||||
EmailNotifications: true,
|
||||
PushNotifications: true,
|
||||
PreferredCategories: make([]string, 0),
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.customerRepo.Create(ctx, customer); err != nil {
|
||||
s.logger.Error("Failed to create customer", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": req.Email,
|
||||
})
|
||||
return nil, errors.New("failed to create customer")
|
||||
}
|
||||
|
||||
// Generate tokens
|
||||
accessToken, err := s.generateAccessToken(customer)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to generate access token", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": customer.ID.String(),
|
||||
})
|
||||
return nil, errors.New("failed to generate access token")
|
||||
}
|
||||
|
||||
refreshToken, err := s.generateRefreshToken(customer)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to generate refresh token", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": customer.ID.String(),
|
||||
})
|
||||
return nil, errors.New("failed to generate refresh token")
|
||||
}
|
||||
|
||||
s.logger.Info("Customer registered successfully", map[string]interface{}{
|
||||
"customer_id": customer.ID.String(),
|
||||
"email": customer.Email,
|
||||
"company_id": customer.CompanyID.String(),
|
||||
})
|
||||
|
||||
// TODO: Send verification email
|
||||
s.sendVerificationEmail(ctx, customer)
|
||||
|
||||
return &domain.CustomerAuthResponse{
|
||||
Customer: customer,
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Login authenticates a customer and returns tokens
|
||||
func (s *CustomerAuthServiceImpl) Login(ctx context.Context, req domain.LoginRequest) (*domain.CustomerAuthResponse, error) {
|
||||
s.logger.Info("Customer login attempt", map[string]interface{}{
|
||||
"email": req.Email,
|
||||
})
|
||||
|
||||
// Get customer by email
|
||||
customer, err := s.customerRepo.GetByEmail(ctx, req.Email)
|
||||
if err != nil {
|
||||
s.logger.Warn("Customer login failed - customer not found", map[string]interface{}{
|
||||
"email": req.Email,
|
||||
})
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
// Check if customer is active
|
||||
if !customer.IsActive {
|
||||
s.logger.Warn("Customer login failed - account inactive", map[string]interface{}{
|
||||
"email": req.Email,
|
||||
"customer_id": customer.ID.String(),
|
||||
})
|
||||
return nil, errors.New("account is inactive")
|
||||
}
|
||||
|
||||
// Verify password
|
||||
if !s.verifyPassword(req.Password, customer.Password) {
|
||||
s.logger.Warn("Customer login failed - invalid password", map[string]interface{}{
|
||||
"email": req.Email,
|
||||
"customer_id": customer.ID.String(),
|
||||
})
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
// Update last login time
|
||||
customer.LastLoginAt = &time.Time{}
|
||||
*customer.LastLoginAt = time.Now()
|
||||
customer.UpdatedAt = time.Now()
|
||||
|
||||
if err := s.customerRepo.Update(ctx, customer); err != nil {
|
||||
s.logger.Warn("Failed to update customer last login", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": customer.ID.String(),
|
||||
})
|
||||
// Don't fail login for this
|
||||
}
|
||||
|
||||
// Generate tokens
|
||||
accessToken, err := s.generateAccessToken(customer)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to generate access token", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": customer.ID.String(),
|
||||
})
|
||||
return nil, errors.New("failed to generate access token")
|
||||
}
|
||||
|
||||
refreshToken, err := s.generateRefreshToken(customer)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to generate refresh token", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": customer.ID.String(),
|
||||
})
|
||||
return nil, errors.New("failed to generate refresh token")
|
||||
}
|
||||
|
||||
s.logger.Info("Customer logged in successfully", map[string]interface{}{
|
||||
"customer_id": customer.ID.String(),
|
||||
"email": customer.Email,
|
||||
"is_verified": customer.IsVerified,
|
||||
})
|
||||
|
||||
return &domain.CustomerAuthResponse{
|
||||
Customer: customer,
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RefreshToken generates new tokens using refresh token
|
||||
func (s *CustomerAuthServiceImpl) RefreshToken(ctx context.Context, refreshToken string) (*domain.CustomerAuthResponse, error) {
|
||||
// Parse and validate refresh token
|
||||
token, err := jwt.Parse(refreshToken, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, errors.New("invalid signing method")
|
||||
}
|
||||
return []byte(s.config.JWT.Secret), nil
|
||||
})
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
return nil, errors.New("invalid refresh token")
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid token claims")
|
||||
}
|
||||
|
||||
// Verify token type
|
||||
tokenType, ok := claims["type"].(string)
|
||||
if !ok || tokenType != "refresh" {
|
||||
return nil, errors.New("invalid token type")
|
||||
}
|
||||
|
||||
// Verify user type
|
||||
userType, ok := claims["user_type"].(string)
|
||||
if !ok || userType != "customer" {
|
||||
return nil, errors.New("invalid user type")
|
||||
}
|
||||
|
||||
customerIDStr, ok := claims["customer_id"].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid customer ID in token")
|
||||
}
|
||||
|
||||
customerID, err := uuid.Parse(customerIDStr)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid customer ID format")
|
||||
}
|
||||
|
||||
// Get customer from database
|
||||
customer, err := s.customerRepo.GetByID(ctx, customerID)
|
||||
if err != nil {
|
||||
return nil, errors.New("customer not found")
|
||||
}
|
||||
|
||||
if !customer.IsActive {
|
||||
return nil, errors.New("account is inactive")
|
||||
}
|
||||
|
||||
// Generate new tokens
|
||||
accessToken, err := s.generateAccessToken(customer)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to generate access token")
|
||||
}
|
||||
|
||||
newRefreshToken, err := s.generateRefreshToken(customer)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to generate refresh token")
|
||||
}
|
||||
|
||||
return &domain.CustomerAuthResponse{
|
||||
Customer: customer,
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: newRefreshToken,
|
||||
ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ValidateToken validates a JWT token and returns the customer
|
||||
func (s *CustomerAuthServiceImpl) ValidateToken(ctx context.Context, tokenString string) (*domain.Customer, error) {
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, errors.New("invalid signing method")
|
||||
}
|
||||
return []byte(s.config.JWT.Secret), nil
|
||||
})
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid token claims")
|
||||
}
|
||||
|
||||
// Verify token type
|
||||
tokenType, ok := claims["type"].(string)
|
||||
if !ok || tokenType != "access" {
|
||||
return nil, errors.New("invalid token type")
|
||||
}
|
||||
|
||||
// Verify user type
|
||||
userType, ok := claims["user_type"].(string)
|
||||
if !ok || userType != "customer" {
|
||||
return nil, errors.New("invalid user type")
|
||||
}
|
||||
|
||||
customerIDStr, ok := claims["customer_id"].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid customer ID in token")
|
||||
}
|
||||
|
||||
customerID, err := uuid.Parse(customerIDStr)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid customer ID format")
|
||||
}
|
||||
|
||||
customer, err := s.customerRepo.GetByID(ctx, customerID)
|
||||
if err != nil {
|
||||
return nil, errors.New("customer not found")
|
||||
}
|
||||
|
||||
if !customer.IsActive {
|
||||
return nil, errors.New("account is inactive")
|
||||
}
|
||||
|
||||
return customer, nil
|
||||
}
|
||||
|
||||
// ChangePassword changes customer password
|
||||
func (s *CustomerAuthServiceImpl) ChangePassword(ctx context.Context, customerID uuid.UUID, oldPassword, newPassword string) error {
|
||||
customer, err := s.customerRepo.GetByID(ctx, customerID)
|
||||
if err != nil {
|
||||
return errors.New("customer not found")
|
||||
}
|
||||
|
||||
// Verify old password
|
||||
if !s.verifyPassword(oldPassword, customer.Password) {
|
||||
return errors.New("invalid old password")
|
||||
}
|
||||
|
||||
// Hash new password
|
||||
hashedPassword, err := s.hashPassword(newPassword)
|
||||
if err != nil {
|
||||
return errors.New("failed to process new password")
|
||||
}
|
||||
|
||||
// Update password
|
||||
customer.Password = hashedPassword
|
||||
customer.UpdatedAt = time.Now()
|
||||
|
||||
if err := s.customerRepo.Update(ctx, customer); err != nil {
|
||||
s.logger.Error("Failed to update customer password", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": customerID.String(),
|
||||
})
|
||||
return errors.New("failed to update password")
|
||||
}
|
||||
|
||||
s.logger.Info("Customer password changed successfully", map[string]interface{}{
|
||||
"customer_id": customerID.String(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetPassword initiates password reset process
|
||||
func (s *CustomerAuthServiceImpl) ResetPassword(ctx context.Context, email string) error {
|
||||
// TODO: Implement password reset logic
|
||||
// This would typically involve:
|
||||
// 1. Generate reset token
|
||||
// 2. Store token with expiration
|
||||
// 3. Send reset email
|
||||
return errors.New("password reset not implemented")
|
||||
}
|
||||
|
||||
// VerifyEmail verifies customer's email address
|
||||
func (s *CustomerAuthServiceImpl) VerifyEmail(ctx context.Context, customerID uuid.UUID, token string) error {
|
||||
// TODO: Implement email verification logic
|
||||
// This would typically involve:
|
||||
// 1. Validate verification token
|
||||
// 2. Update customer verification status
|
||||
// 3. Send welcome email
|
||||
return errors.New("email verification not implemented")
|
||||
}
|
||||
|
||||
// ResendVerification resends verification email
|
||||
func (s *CustomerAuthServiceImpl) ResendVerification(ctx context.Context, email string) error {
|
||||
customer, err := s.customerRepo.GetByEmail(ctx, email)
|
||||
if err != nil {
|
||||
return errors.New("customer not found")
|
||||
}
|
||||
|
||||
if customer.IsVerified {
|
||||
return errors.New("email already verified")
|
||||
}
|
||||
|
||||
// TODO: Send verification email
|
||||
s.sendVerificationEmail(ctx, customer)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Private helper methods
|
||||
|
||||
func (s *CustomerAuthServiceImpl) hashPassword(password string) (string, error) {
|
||||
hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(hashedBytes), nil
|
||||
}
|
||||
|
||||
func (s *CustomerAuthServiceImpl) verifyPassword(password, hashedPassword string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (s *CustomerAuthServiceImpl) generateAccessToken(customer *domain.Customer) (string, error) {
|
||||
claims := jwt.MapClaims{
|
||||
"customer_id": customer.ID.String(),
|
||||
"email": customer.Email,
|
||||
"role": customer.Role,
|
||||
"company_id": customer.CompanyID.String(),
|
||||
"user_type": "customer",
|
||||
"exp": time.Now().Add(s.config.JWT.AccessTokenDuration).Unix(),
|
||||
"iat": time.Now().Unix(),
|
||||
"type": "access",
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(s.config.JWT.Secret))
|
||||
}
|
||||
|
||||
func (s *CustomerAuthServiceImpl) generateRefreshToken(customer *domain.Customer) (string, error) {
|
||||
claims := jwt.MapClaims{
|
||||
"customer_id": customer.ID.String(),
|
||||
"user_type": "customer",
|
||||
"exp": time.Now().Add(s.config.JWT.RefreshTokenDuration).Unix(),
|
||||
"iat": time.Now().Unix(),
|
||||
"type": "refresh",
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(s.config.JWT.Secret))
|
||||
}
|
||||
|
||||
func (s *CustomerAuthServiceImpl) sendVerificationEmail(ctx context.Context, customer *domain.Customer) {
|
||||
// TODO: Implement email sending logic
|
||||
s.logger.Info("Verification email would be sent", map[string]interface{}{
|
||||
"customer_id": customer.ID.String(),
|
||||
"email": customer.Email,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,517 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"tm/internal/domain"
|
||||
"tm/internal/infrastructure"
|
||||
"tm/pkg/logger"
|
||||
)
|
||||
|
||||
// UserAuthServiceImpl implements the UserAuthService interface
|
||||
type UserAuthServiceImpl struct {
|
||||
userRepo domain.UserRepository
|
||||
config *infrastructure.AuthConfig
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewUserAuthService creates a new panel user authentication service
|
||||
func NewUserAuthService(
|
||||
userRepo domain.UserRepository,
|
||||
config *infrastructure.AuthConfig,
|
||||
logger logger.Logger,
|
||||
) domain.UserAuthService {
|
||||
return &UserAuthServiceImpl{
|
||||
userRepo: userRepo,
|
||||
config: config,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Login authenticates a panel user and returns tokens
|
||||
func (s *UserAuthServiceImpl) Login(ctx context.Context, req domain.LoginRequest) (*domain.UserAuthResponse, error) {
|
||||
s.logger.Info("Panel user login attempt", map[string]interface{}{
|
||||
"email": req.Email,
|
||||
})
|
||||
|
||||
// Get user by email
|
||||
user, err := s.userRepo.GetByEmail(ctx, req.Email)
|
||||
if err != nil {
|
||||
s.logger.Warn("Panel user login failed - user not found", map[string]interface{}{
|
||||
"email": req.Email,
|
||||
})
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
// Check if user is active
|
||||
if !user.IsActive {
|
||||
s.logger.Warn("Panel user login failed - account inactive", map[string]interface{}{
|
||||
"email": req.Email,
|
||||
"user_id": user.ID.String(),
|
||||
})
|
||||
return nil, errors.New("account is inactive")
|
||||
}
|
||||
|
||||
// Verify password
|
||||
if !s.verifyPassword(req.Password, user.Password) {
|
||||
s.logger.Warn("Panel user login failed - invalid password", map[string]interface{}{
|
||||
"email": req.Email,
|
||||
"user_id": user.ID.String(),
|
||||
})
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
// Update last login time
|
||||
user.LastLoginAt = &time.Time{}
|
||||
*user.LastLoginAt = time.Now()
|
||||
|
||||
if err := s.userRepo.Update(ctx, user); err != nil {
|
||||
s.logger.Warn("Failed to update user last login", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": user.ID.String(),
|
||||
})
|
||||
// Don't fail login for this
|
||||
}
|
||||
|
||||
// Generate tokens
|
||||
accessToken, err := s.generateAccessToken(user)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to generate access token", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": user.ID.String(),
|
||||
})
|
||||
return nil, errors.New("failed to generate access token")
|
||||
}
|
||||
|
||||
refreshToken, err := s.generateRefreshToken(user)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to generate refresh token", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": user.ID.String(),
|
||||
})
|
||||
return nil, errors.New("failed to generate refresh token")
|
||||
}
|
||||
|
||||
s.logger.Info("Panel user logged in successfully", map[string]interface{}{
|
||||
"user_id": user.ID.String(),
|
||||
"email": user.Email,
|
||||
"role": user.Role,
|
||||
"department": user.Department,
|
||||
})
|
||||
|
||||
return &domain.UserAuthResponse{
|
||||
User: user,
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RefreshToken generates new tokens using refresh token
|
||||
func (s *UserAuthServiceImpl) RefreshToken(ctx context.Context, refreshToken string) (*domain.UserAuthResponse, error) {
|
||||
// Parse and validate refresh token
|
||||
token, err := jwt.Parse(refreshToken, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, errors.New("invalid signing method")
|
||||
}
|
||||
return []byte(s.config.JWT.Secret), nil
|
||||
})
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
return nil, errors.New("invalid refresh token")
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid token claims")
|
||||
}
|
||||
|
||||
// Verify token type
|
||||
tokenType, ok := claims["type"].(string)
|
||||
if !ok || tokenType != "refresh" {
|
||||
return nil, errors.New("invalid token type")
|
||||
}
|
||||
|
||||
// Verify user type
|
||||
userType, ok := claims["user_type"].(string)
|
||||
if !ok || userType != "user" {
|
||||
return nil, errors.New("invalid user type")
|
||||
}
|
||||
|
||||
userIDStr, ok := claims["user_id"].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid user ID in token")
|
||||
}
|
||||
|
||||
userID, err := uuid.Parse(userIDStr)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid user ID format")
|
||||
}
|
||||
|
||||
// Get user from database
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
if !user.IsActive {
|
||||
return nil, errors.New("account is inactive")
|
||||
}
|
||||
|
||||
// Generate new tokens
|
||||
accessToken, err := s.generateAccessToken(user)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to generate access token")
|
||||
}
|
||||
|
||||
newRefreshToken, err := s.generateRefreshToken(user)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to generate refresh token")
|
||||
}
|
||||
|
||||
return &domain.UserAuthResponse{
|
||||
User: user,
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: newRefreshToken,
|
||||
ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ValidateToken validates a JWT token and returns the user
|
||||
func (s *UserAuthServiceImpl) ValidateToken(ctx context.Context, tokenString string) (*domain.User, error) {
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, errors.New("invalid signing method")
|
||||
}
|
||||
return []byte(s.config.JWT.Secret), nil
|
||||
})
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid token claims")
|
||||
}
|
||||
|
||||
// Verify token type
|
||||
tokenType, ok := claims["type"].(string)
|
||||
if !ok || tokenType != "access" {
|
||||
return nil, errors.New("invalid token type")
|
||||
}
|
||||
|
||||
// Verify user type
|
||||
userType, ok := claims["user_type"].(string)
|
||||
if !ok || userType != "user" {
|
||||
return nil, errors.New("invalid user type")
|
||||
}
|
||||
|
||||
userIDStr, ok := claims["user_id"].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid user ID in token")
|
||||
}
|
||||
|
||||
userID, err := uuid.Parse(userIDStr)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid user ID format")
|
||||
}
|
||||
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
if !user.IsActive {
|
||||
return nil, errors.New("account is inactive")
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// ChangePassword changes panel user password
|
||||
func (s *UserAuthServiceImpl) ChangePassword(ctx context.Context, userID uuid.UUID, oldPassword, newPassword string) error {
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return errors.New("user not found")
|
||||
}
|
||||
|
||||
// Verify old password
|
||||
if !s.verifyPassword(oldPassword, user.Password) {
|
||||
return errors.New("invalid old password")
|
||||
}
|
||||
|
||||
// Hash new password
|
||||
hashedPassword, err := s.hashPassword(newPassword)
|
||||
if err != nil {
|
||||
return errors.New("failed to process new password")
|
||||
}
|
||||
|
||||
// Update password
|
||||
user.Password = hashedPassword
|
||||
|
||||
if err := s.userRepo.Update(ctx, user); err != nil {
|
||||
s.logger.Error("Failed to update user password", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": userID.String(),
|
||||
})
|
||||
return errors.New("failed to update password")
|
||||
}
|
||||
|
||||
s.logger.Info("Panel user password changed successfully", map[string]interface{}{
|
||||
"user_id": userID.String(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreatePanelUser creates a new panel user (admin only)
|
||||
func (s *UserAuthServiceImpl) CreatePanelUser(ctx context.Context, req domain.CreateUserRequest, createdBy uuid.UUID) (*domain.User, error) {
|
||||
s.logger.Info("Creating new panel user", map[string]interface{}{
|
||||
"email": req.Email,
|
||||
"role": req.Role,
|
||||
"created_by": createdBy.String(),
|
||||
})
|
||||
|
||||
// Check if user already exists
|
||||
existingUser, err := s.userRepo.GetByEmail(ctx, req.Email)
|
||||
if err == nil && existingUser != nil {
|
||||
return nil, errors.New("user already exists")
|
||||
}
|
||||
|
||||
// Validate role
|
||||
if !s.isValidRole(req.Role) {
|
||||
return nil, errors.New("invalid role")
|
||||
}
|
||||
|
||||
// Hash password
|
||||
hashedPassword, err := s.hashPassword(req.Password)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to hash password", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, errors.New("failed to process password")
|
||||
}
|
||||
|
||||
// Create user
|
||||
user := &domain.User{
|
||||
ID: uuid.New(),
|
||||
Email: req.Email,
|
||||
Password: hashedPassword,
|
||||
FirstName: req.FirstName,
|
||||
LastName: req.LastName,
|
||||
Role: req.Role,
|
||||
Department: req.Department,
|
||||
IsActive: true,
|
||||
Permissions: s.getDefaultPermissions(req.Role),
|
||||
CreatedBy: &createdBy,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Add custom permissions if provided
|
||||
if len(req.Permissions) > 0 {
|
||||
user.Permissions = append(user.Permissions, req.Permissions...)
|
||||
user.Permissions = s.removeDuplicatePermissions(user.Permissions)
|
||||
}
|
||||
|
||||
if err := s.userRepo.Create(ctx, user); err != nil {
|
||||
s.logger.Error("Failed to create panel user", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": req.Email,
|
||||
"created_by": createdBy.String(),
|
||||
})
|
||||
return nil, errors.New("failed to create user")
|
||||
}
|
||||
|
||||
s.logger.Info("Panel user created successfully", map[string]interface{}{
|
||||
"user_id": user.ID.String(),
|
||||
"email": user.Email,
|
||||
"role": user.Role,
|
||||
"created_by": createdBy.String(),
|
||||
})
|
||||
|
||||
// Remove password from response
|
||||
user.Password = ""
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// UpdateUserPermissions updates panel user permissions (admin only)
|
||||
func (s *UserAuthServiceImpl) UpdateUserPermissions(ctx context.Context, userID uuid.UUID, permissions []string, updatedBy uuid.UUID) error {
|
||||
// Check if user exists
|
||||
_, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return errors.New("user not found")
|
||||
}
|
||||
|
||||
// Validate permissions
|
||||
for _, permission := range permissions {
|
||||
if !s.isValidPermission(permission) {
|
||||
return errors.New("invalid permission: " + permission)
|
||||
}
|
||||
}
|
||||
|
||||
// Update permissions
|
||||
if err := s.userRepo.UpdatePermissions(ctx, userID, permissions); err != nil {
|
||||
s.logger.Error("Failed to update user permissions", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": userID.String(),
|
||||
"updated_by": updatedBy.String(),
|
||||
})
|
||||
return errors.New("failed to update permissions")
|
||||
}
|
||||
|
||||
s.logger.Info("Panel user permissions updated successfully", map[string]interface{}{
|
||||
"user_id": userID.String(),
|
||||
"permissions": permissions,
|
||||
"updated_by": updatedBy.String(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Private helper methods
|
||||
|
||||
func (s *UserAuthServiceImpl) hashPassword(password string) (string, error) {
|
||||
hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(hashedBytes), nil
|
||||
}
|
||||
|
||||
func (s *UserAuthServiceImpl) verifyPassword(password, hashedPassword string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (s *UserAuthServiceImpl) generateAccessToken(user *domain.User) (string, error) {
|
||||
claims := jwt.MapClaims{
|
||||
"user_id": user.ID.String(),
|
||||
"email": user.Email,
|
||||
"role": user.Role,
|
||||
"department": user.Department,
|
||||
"permissions": user.Permissions,
|
||||
"user_type": "user",
|
||||
"exp": time.Now().Add(s.config.JWT.AccessTokenDuration).Unix(),
|
||||
"iat": time.Now().Unix(),
|
||||
"type": "access",
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(s.config.JWT.Secret))
|
||||
}
|
||||
|
||||
func (s *UserAuthServiceImpl) generateRefreshToken(user *domain.User) (string, error) {
|
||||
claims := jwt.MapClaims{
|
||||
"user_id": user.ID.String(),
|
||||
"user_type": "user",
|
||||
"exp": time.Now().Add(s.config.JWT.RefreshTokenDuration).Unix(),
|
||||
"iat": time.Now().Unix(),
|
||||
"type": "refresh",
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(s.config.JWT.Secret))
|
||||
}
|
||||
|
||||
func (s *UserAuthServiceImpl) isValidRole(role domain.UserRole) bool {
|
||||
validRoles := []domain.UserRole{
|
||||
domain.UserRoleAdmin,
|
||||
domain.UserRoleModerator,
|
||||
domain.UserRoleSupport,
|
||||
domain.UserRoleAnalyst,
|
||||
}
|
||||
|
||||
for _, validRole := range validRoles {
|
||||
if role == validRole {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *UserAuthServiceImpl) getDefaultPermissions(role domain.UserRole) []string {
|
||||
switch role {
|
||||
case domain.UserRoleAdmin:
|
||||
return []string{
|
||||
"manage_users",
|
||||
"manage_tenders",
|
||||
"manage_companies",
|
||||
"manage_customers",
|
||||
"view_reports",
|
||||
"manage_system",
|
||||
"manage_sources",
|
||||
}
|
||||
case domain.UserRoleModerator:
|
||||
return []string{
|
||||
"manage_tenders",
|
||||
"manage_companies",
|
||||
"view_customers",
|
||||
"view_reports",
|
||||
"manage_sources",
|
||||
}
|
||||
case domain.UserRoleSupport:
|
||||
return []string{
|
||||
"view_customers",
|
||||
"view_tenders",
|
||||
"view_companies",
|
||||
"create_tickets",
|
||||
}
|
||||
case domain.UserRoleAnalyst:
|
||||
return []string{
|
||||
"view_reports",
|
||||
"view_tenders",
|
||||
"view_customers",
|
||||
"view_companies",
|
||||
"export_data",
|
||||
}
|
||||
default:
|
||||
return []string{}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UserAuthServiceImpl) isValidPermission(permission string) bool {
|
||||
validPermissions := []string{
|
||||
"manage_users",
|
||||
"manage_tenders",
|
||||
"manage_companies",
|
||||
"manage_customers",
|
||||
"view_customers",
|
||||
"view_tenders",
|
||||
"view_companies",
|
||||
"view_reports",
|
||||
"manage_system",
|
||||
"manage_sources",
|
||||
"create_tickets",
|
||||
"export_data",
|
||||
}
|
||||
|
||||
for _, validPermission := range validPermissions {
|
||||
if permission == validPermission {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *UserAuthServiceImpl) removeDuplicatePermissions(permissions []string) []string {
|
||||
seen := make(map[string]bool)
|
||||
result := []string{}
|
||||
|
||||
for _, permission := range permissions {
|
||||
if !seen[permission] {
|
||||
seen[permission] = true
|
||||
result = append(result, permission)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user