582 lines
16 KiB
Go
582 lines
16 KiB
Go
package services
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"mohportal/db"
|
|
"mohportal/models"
|
|
|
|
"github.com/google/uuid"
|
|
"golang.org/x/crypto/bcrypt"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// TenantService handles tenant-related operations
|
|
type TenantService struct{}
|
|
|
|
// CreateTenant creates a new tenant
|
|
func (ts *TenantService) CreateTenant(name, slug, description, logoURL string) (*models.Tenant, error) {
|
|
tenant := &models.Tenant{
|
|
ID: uuid.New(),
|
|
Name: name,
|
|
Slug: slug,
|
|
Description: description,
|
|
LogoURL: logoURL,
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
IsActive: true,
|
|
}
|
|
|
|
if err := db.DB.Create(tenant).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return tenant, nil
|
|
}
|
|
|
|
// GetTenant retrieves a tenant by ID
|
|
func (ts *TenantService) GetTenant(id uuid.UUID) (*models.Tenant, error) {
|
|
var tenant models.Tenant
|
|
if err := db.DB.First(&tenant, "id = ?", id).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, errors.New("tenant not found")
|
|
}
|
|
return nil, err
|
|
}
|
|
return &tenant, nil
|
|
}
|
|
|
|
// GetTenantBySlug retrieves a tenant by slug
|
|
func (ts *TenantService) GetTenantBySlug(slug string) (*models.Tenant, error) {
|
|
var tenant models.Tenant
|
|
if err := db.DB.First(&tenant, "slug = ?", slug).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, errors.New("tenant not found")
|
|
}
|
|
return nil, err
|
|
}
|
|
return &tenant, nil
|
|
}
|
|
|
|
// GetTenants retrieves all tenants (with optional filtering)
|
|
func (ts *TenantService) GetTenants(limit, offset int) ([]*models.Tenant, error) {
|
|
var tenants []*models.Tenant
|
|
query := db.DB.Limit(limit).Offset(offset)
|
|
|
|
if err := query.Find(&tenants).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return tenants, nil
|
|
}
|
|
|
|
// UpdateTenant updates an existing tenant
|
|
func (ts *TenantService) UpdateTenant(id uuid.UUID, name, slug, description, logoURL string) (*models.Tenant, error) {
|
|
var tenant models.Tenant
|
|
if err := db.DB.First(&tenant, "id = ?", id).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tenant.Name = name
|
|
tenant.Slug = slug
|
|
tenant.Description = description
|
|
tenant.LogoURL = logoURL
|
|
tenant.UpdatedAt = time.Now()
|
|
|
|
if err := db.DB.Save(&tenant).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &tenant, nil
|
|
}
|
|
|
|
// DeleteTenant deletes a tenant
|
|
func (ts *TenantService) DeleteTenant(id uuid.UUID) error {
|
|
return db.DB.Delete(&models.Tenant{}, "id = ?", id).Error
|
|
}
|
|
|
|
// UserService handles user-related operations
|
|
type UserService struct{}
|
|
|
|
// CreateUser creates a new user
|
|
func (us *UserService) CreateUser(tenantID uuid.UUID, email, username, firstName, lastName, phone, role, password string) (*models.User, error) {
|
|
// Check if user already exists
|
|
var existingUser models.User
|
|
if err := db.DB.Where("email = ? OR username = ?", email, username).First(&existingUser).Error; err == nil {
|
|
return nil, errors.New("user with email or username already exists")
|
|
}
|
|
|
|
// Hash the password
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
user := &models.User{
|
|
ID: uuid.New(),
|
|
TenantID: tenantID,
|
|
Email: email,
|
|
Username: username,
|
|
FirstName: firstName,
|
|
LastName: lastName,
|
|
Phone: phone,
|
|
Role: role,
|
|
PasswordHash: string(hashedPassword),
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
IsActive: true,
|
|
}
|
|
|
|
if err := db.DB.Create(user).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// GetUser retrieves a user by ID
|
|
func (us *UserService) GetUser(id uuid.UUID) (*models.User, error) {
|
|
var user models.User
|
|
if err := db.DB.Preload("Tenant").First(&user, "id = ?", id).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, errors.New("user not found")
|
|
}
|
|
return nil, err
|
|
}
|
|
return &user, nil
|
|
}
|
|
|
|
// GetUserByEmail retrieves a user by email
|
|
func (us *UserService) GetUserByEmail(email string) (*models.User, error) {
|
|
var user models.User
|
|
if err := db.DB.Preload("Tenant").First(&user, "email = ?", email).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, errors.New("user not found")
|
|
}
|
|
return nil, err
|
|
}
|
|
return &user, nil
|
|
}
|
|
|
|
// AuthenticateUser authenticates a user by email and password
|
|
func (us *UserService) AuthenticateUser(email, password string) (*models.User, error) {
|
|
var user models.User
|
|
if err := db.DB.First(&user, "email = ?", email).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
// Return error without specifying whether email exists to prevent timing attacks
|
|
return nil, errors.New("invalid credentials")
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
if !user.IsActive {
|
|
return nil, errors.New("user account is deactivated")
|
|
}
|
|
|
|
// Compare the password
|
|
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
|
|
return nil, errors.New("invalid credentials")
|
|
}
|
|
|
|
// Update last login
|
|
loginTime := time.Now()
|
|
user.LastLogin = &loginTime
|
|
db.DB.Save(&user)
|
|
|
|
return &user, nil
|
|
}
|
|
|
|
// UpdateUser updates an existing user
|
|
func (us *UserService) UpdateUser(id uuid.UUID, email, username, firstName, lastName, phone, role string) (*models.User, error) {
|
|
var user models.User
|
|
if err := db.DB.First(&user, "id = ?", id).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Check if email or username is already taken by another user
|
|
var existingUser models.User
|
|
if err := db.DB.Where("email = ? AND id != ?", email, id).First(&existingUser).Error; err == nil {
|
|
return nil, errors.New("email already in use by another user")
|
|
}
|
|
|
|
if username != "" {
|
|
if err := db.DB.Where("username = ? AND id != ?", username, id).First(&existingUser).Error; err == nil {
|
|
return nil, errors.New("username already in use by another user")
|
|
}
|
|
}
|
|
|
|
user.Email = email
|
|
if username != "" {
|
|
user.Username = username
|
|
}
|
|
user.FirstName = firstName
|
|
user.LastName = lastName
|
|
user.Phone = phone
|
|
user.Role = role
|
|
user.UpdatedAt = time.Now()
|
|
|
|
if err := db.DB.Save(&user).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &user, nil
|
|
}
|
|
|
|
// PositionService handles job position-related operations
|
|
type PositionService struct{}
|
|
|
|
// CreatePosition creates a new job position
|
|
func (ps *PositionService) CreatePosition(tenantID, userID uuid.UUID, title, description, requirements, location, employmentType, experienceLevel string, salaryMin, salaryMax *float64) (*models.JobPosition, error) {
|
|
position := &models.JobPosition{
|
|
ID: uuid.New(),
|
|
TenantID: tenantID,
|
|
UserID: userID,
|
|
Title: title,
|
|
Description: description,
|
|
Requirements: requirements,
|
|
Location: location,
|
|
EmploymentType: employmentType,
|
|
SalaryMin: salaryMin,
|
|
SalaryMax: salaryMax,
|
|
ExperienceLevel: experienceLevel,
|
|
PostedAt: time.Now(),
|
|
Status: "open",
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
IsActive: true,
|
|
}
|
|
|
|
if err := db.DB.Create(position).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return position, nil
|
|
}
|
|
|
|
// GetPosition retrieves a position by ID
|
|
func (ps *PositionService) GetPosition(id uuid.UUID) (*models.JobPosition, error) {
|
|
var position models.JobPosition
|
|
if err := db.DB.Preload("User").First(&position, "id = ?", id).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, errors.New("position not found")
|
|
}
|
|
return nil, err
|
|
}
|
|
return &position, nil
|
|
}
|
|
|
|
// GetPositions retrieves positions with optional filtering
|
|
func (ps *PositionService) GetPositions(tenantID *uuid.UUID, limit, offset int, status, employmentType, experienceLevel, location string) ([]*models.JobPosition, error) {
|
|
var positions []*models.JobPosition
|
|
query := db.DB.Preload("User").Limit(limit).Offset(offset)
|
|
|
|
if tenantID != nil {
|
|
query = query.Where("tenant_id = ?", tenantID)
|
|
}
|
|
|
|
if status != "" {
|
|
query = query.Where("status = ?", status)
|
|
}
|
|
|
|
if employmentType != "" {
|
|
query = query.Where("employment_type = ?", employmentType)
|
|
}
|
|
|
|
if experienceLevel != "" {
|
|
query = query.Where("experience_level = ?", experienceLevel)
|
|
}
|
|
|
|
if location != "" {
|
|
query = query.Where("location LIKE ?", "%"+location+"%")
|
|
}
|
|
|
|
query = query.Where("is_active = ? AND status = ?", true, "open")
|
|
|
|
if err := query.Find(&positions).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return positions, nil
|
|
}
|
|
|
|
// UpdatePosition updates an existing position
|
|
func (ps *PositionService) UpdatePosition(id uuid.UUID, title, description, requirements, location, employmentType, experienceLevel string, salaryMin, salaryMax *float64) (*models.JobPosition, error) {
|
|
var position models.JobPosition
|
|
if err := db.DB.First(&position, "id = ?", id).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
position.Title = title
|
|
position.Description = description
|
|
position.Requirements = requirements
|
|
position.Location = location
|
|
position.EmploymentType = employmentType
|
|
position.ExperienceLevel = experienceLevel
|
|
position.SalaryMin = salaryMin
|
|
position.SalaryMax = salaryMax
|
|
position.UpdatedAt = time.Now()
|
|
|
|
if err := db.DB.Save(&position).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &position, nil
|
|
}
|
|
|
|
// ClosePosition closes a position (marks as filled or cancelled)
|
|
func (ps *PositionService) ClosePosition(id uuid.UUID, status string) (*models.JobPosition, error) {
|
|
var position models.JobPosition
|
|
if err := db.DB.First(&position, "id = ?", id).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if status != "closed" && status != "filled" {
|
|
return nil, errors.New("invalid status for closing position")
|
|
}
|
|
|
|
position.Status = status
|
|
closedTime := time.Now()
|
|
position.ClosedAt = &closedTime
|
|
position.UpdatedAt = time.Now()
|
|
|
|
if err := db.DB.Save(&position).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &position, nil
|
|
}
|
|
|
|
// DeletePosition deletes a position
|
|
func (ps *PositionService) DeletePosition(id uuid.UUID) error {
|
|
return db.DB.Delete(&models.JobPosition{}, "id = ?", id).Error
|
|
}
|
|
|
|
// ResumeService handles resume-related operations
|
|
type ResumeService struct{}
|
|
|
|
// UploadResume uploads a resume file and creates a record
|
|
func (rs *ResumeService) UploadResume(userID uuid.UUID, fileHeader *multipart.FileHeader, title string) (*models.Resume, error) {
|
|
// Validate file type
|
|
allowedTypes := map[string]bool{
|
|
"application/pdf": true,
|
|
"application/msword": true,
|
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": true,
|
|
"text/plain": true,
|
|
}
|
|
|
|
file, err := fileHeader.Open()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer file.Close()
|
|
|
|
// Detect content type
|
|
buffer := make([]byte, 512)
|
|
_, err = file.Read(buffer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fileType := http.DetectContentType(buffer)
|
|
if !allowedTypes[fileType] {
|
|
return nil, errors.New("file type not allowed")
|
|
}
|
|
|
|
// Ensure the static/uploads directory exists
|
|
uploadDir := "./static/uploads/resumes"
|
|
if err := os.MkdirAll(uploadDir, 0755); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Generate unique filename
|
|
filename := fmt.Sprintf("%s_%s", userID.String(), fileHeader.Filename)
|
|
uploadPath := filepath.Join(uploadDir, filename)
|
|
|
|
// Copy file to upload directory
|
|
src, err := fileHeader.Open()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer src.Close()
|
|
|
|
dst, err := os.Create(uploadPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer dst.Close()
|
|
|
|
if _, err := io.Copy(dst, src); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create resume record
|
|
resume := &models.Resume{
|
|
ID: uuid.New(),
|
|
UserID: userID,
|
|
Title: title,
|
|
FilePath: uploadPath,
|
|
FileType: fileType,
|
|
FileSize: fileHeader.Size,
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
IsActive: true,
|
|
}
|
|
|
|
if err := db.DB.Create(resume).Error; err != nil {
|
|
// Clean up the uploaded file if DB operation fails
|
|
os.Remove(uploadPath)
|
|
return nil, err
|
|
}
|
|
|
|
return resume, nil
|
|
}
|
|
|
|
// GetResume retrieves a resume by ID
|
|
func (rs *ResumeService) GetResume(id uuid.UUID) (*models.Resume, error) {
|
|
var resume models.Resume
|
|
if err := db.DB.First(&resume, "id = ?", id).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, errors.New("resume not found")
|
|
}
|
|
return nil, err
|
|
}
|
|
return &resume, nil
|
|
}
|
|
|
|
// GetResumeByUser retrieves all resumes for a user
|
|
func (rs *ResumeService) GetResumeByUser(userID uuid.UUID) ([]*models.Resume, error) {
|
|
var resumes []*models.Resume
|
|
if err := db.DB.Where("user_id = ?", userID).Find(&resumes).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return resumes, nil
|
|
}
|
|
|
|
// ApplicationService handles job application-related operations
|
|
type ApplicationService struct{}
|
|
|
|
// CreateApplication creates a new job application
|
|
func (as *ApplicationService) CreateApplication(positionID, userID uuid.UUID, resumeID *uuid.UUID, coverLetter string) (*models.Application, error) {
|
|
// Check if user already applied for this position
|
|
var existingApplication models.Application
|
|
if err := db.DB.Where("position_id = ? AND user_id = ?", positionID, userID).First(&existingApplication).Error; err == nil {
|
|
return nil, errors.New("user has already applied for this position")
|
|
}
|
|
|
|
// Verify position exists and is open
|
|
var position models.JobPosition
|
|
if err := db.DB.First(&position, "id = ? AND status = ? AND is_active = ?", positionID, "open", true).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, errors.New("position not found or not open")
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
application := &models.Application{
|
|
ID: uuid.New(),
|
|
PositionID: positionID,
|
|
UserID: userID,
|
|
ResumeID: resumeID,
|
|
CoverLetter: coverLetter,
|
|
Status: "pending",
|
|
AppliedAt: time.Now(),
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
|
|
if err := db.DB.Create(application).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return application, nil
|
|
}
|
|
|
|
// GetApplication retrieves an application by ID
|
|
func (as *ApplicationService) GetApplication(id uuid.UUID) (*models.Application, error) {
|
|
var application models.Application
|
|
if err := db.DB.Preload("User").Preload("Position").Preload("Resume").First(&application, "id = ?", id).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, errors.New("application not found")
|
|
}
|
|
return nil, err
|
|
}
|
|
return &application, nil
|
|
}
|
|
|
|
// GetApplications retrieves applications with optional filtering
|
|
func (as *ApplicationService) GetApplications(userID, positionID *uuid.UUID, status string, limit, offset int) ([]*models.Application, error) {
|
|
var applications []*models.Application
|
|
query := db.DB.Preload("User").Preload("Position").Preload("Resume").Limit(limit).Offset(offset)
|
|
|
|
if userID != nil {
|
|
query = query.Where("user_id = ?", userID)
|
|
}
|
|
|
|
if positionID != nil {
|
|
query = query.Where("position_id = ?", positionID)
|
|
}
|
|
|
|
if status != "" {
|
|
query = query.Where("status = ?", status)
|
|
}
|
|
|
|
if err := query.Find(&applications).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return applications, nil
|
|
}
|
|
|
|
// UpdateApplication updates an application status
|
|
func (as *ApplicationService) UpdateApplication(id uuid.UUID, status string, reviewerID uuid.UUID, notes string) (*models.Application, error) {
|
|
var application models.Application
|
|
if err := db.DB.First(&application, "id = ?", id).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Validate status
|
|
if !models.ValidStatus(status) {
|
|
return nil, errors.New("invalid status")
|
|
}
|
|
|
|
application.Status = status
|
|
application.ReviewerUserID = &reviewerID
|
|
application.Notes = notes
|
|
application.UpdatedAt = time.Now()
|
|
reviewedTime := time.Now()
|
|
application.ReviewedAt = &reviewedTime
|
|
|
|
if err := db.DB.Save(&application).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &application, nil
|
|
}
|
|
|
|
// DeleteApplication deletes an application
|
|
func (as *ApplicationService) DeleteApplication(id uuid.UUID) error {
|
|
return db.DB.Delete(&models.Application{}, "id = ?", id).Error
|
|
}
|
|
|
|
// GetApplicationsForPosition retrieves all applications for a specific position
|
|
func (as *ApplicationService) GetApplicationsForPosition(positionID uuid.UUID, status string, limit, offset int) ([]*models.Application, error) {
|
|
var applications []*models.Application
|
|
query := db.DB.Preload("User").Preload("Resume").Limit(limit).Offset(offset).Where("position_id = ?", positionID)
|
|
|
|
if status != "" {
|
|
query = query.Where("status = ?", status)
|
|
}
|
|
|
|
if err := query.Find(&applications).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return applications, nil
|
|
} |