Initialize base

This commit is contained in:
hdbar
2025-07-27 16:20:21 +03:30
commit ad9db7bcce
31 changed files with 8268 additions and 0 deletions
+389
View File
@@ -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"
)
+321
View File
@@ -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"`
}
+358
View File
@@ -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")
}
+378
View File
@@ -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")
}
+380
View File
@@ -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")
}
+142
View File
@@ -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
}
+219
View File
@@ -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
}
+362
View File
@@ -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
}
+311
View File
@@ -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
}
+179
View File
@@ -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
}
+497
View File
@@ -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,
})
}
+517
View File
@@ -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
}