package service import ( "errors" "fmt" "net/url" "time" "golang.org/x/crypto/bcrypt" "devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models" "devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/repository" "devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/auth" ) var ( ErrInvalidCredentials = errors.New("invalid credentials") ErrUserNotFound = errors.New("user not found") ) // AuthService handles authentication operations type AuthService struct { jwtManager *auth.JWTManager userRepo *repository.UserRepository patientRepo *repository.PatientRepository } // NewAuthService creates a new authentication service func NewAuthService(jwtManager *auth.JWTManager) *AuthService { return &AuthService{ jwtManager: jwtManager, userRepo: repository.NewUserRepository(), patientRepo: repository.NewPatientRepository(), } } // Login authenticates a user and generates tokens func (s *AuthService) Login(email, password string) (*models.LoginResponse, error) { // Find user in database user, err := s.userRepo.GetUserByEmail(email) if err != nil { return nil, fmt.Errorf("error finding user: %w", err) } if user == nil { return nil, ErrInvalidCredentials } // Verify password if err := CheckPassword(password, user.Password); err != nil { return nil, ErrInvalidCredentials } // Create token claims based on user role additionalClaims := make(map[string]interface{}) var redirectURL string switch user.Role { case "patient": // Get patient data patientData, err := s.patientRepo.GetPatientDetailsByUserID(user.ID) if err != nil { return nil, fmt.Errorf("error getting patient details: %w", err) } if patientData == nil { return nil, ErrUserNotFound } // Set patient-specific claims additionalClaims["patient_id"] = patientData.PatientID additionalClaims["patient_name"] = patientData.PatientName additionalClaims["study_iuids"] = patientData.StudyInstanceUIDs additionalClaims["accession_numbers"] = patientData.AccessionNumbers // For redirectURL and home_url, use first study for simplicity if len(patientData.StudyInstanceUIDs) > 0 { additionalClaims["home_url"] = fmt.Sprintf("viewer?StudyInstanceUIDs=%s", patientData.StudyInstanceUIDs[0]) redirectURL = fmt.Sprintf("/viewer?StudyInstanceUIDs=%s", patientData.StudyInstanceUIDs[0]) } else { // Fallback for empty studies array additionalClaims["home_url"] = "/" redirectURL = "/" } additionalClaims["study_list"] = "disabled" case "ref_doctor": // Set referring doctor claims encodedName := url.QueryEscape(user.Name) filterURL := fmt.Sprintf("studies?limit=101&offset=0&fuzzymatching=false&includefield=00081030,00080060,00080090&00080090=%s", encodedName) additionalClaims["home_url"] = "/" additionalClaims["study_list"] = "enabled" additionalClaims["filter_url"] = filterURL redirectURL = "/" case "expertise_doctor", "admin": // Expertise doctors have full access additionalClaims["home_url"] = "/" additionalClaims["study_list"] = "enabled" redirectURL = "/" } // Generate tokens accessToken, err := s.jwtManager.GenerateAccessToken(user.ID, user.Email, user.Role, user.Name, additionalClaims) if err != nil { return nil, err } refreshToken, err := s.jwtManager.GenerateRefreshToken(user.ID, user.Email, user.Role, user.Name, additionalClaims) if err != nil { return nil, err } // Store refresh token in database expiresAt := time.Now().Add(s.jwtManager.GetRefreshExpiry()) if err := s.userRepo.StoreRefreshToken(user.ID, refreshToken, expiresAt); err != nil { return nil, fmt.Errorf("failed to store refresh token: %w", err) } return &models.LoginResponse{ AccessToken: accessToken, RefreshToken: refreshToken, User: user, RedirectURL: redirectURL, }, nil } // RefreshToken generates a new access token using a refresh token func (s *AuthService) RefreshToken(refreshToken string) (string, error) { // Check if token exists and is not revoked dbToken, err := s.userRepo.GetRefreshToken(refreshToken) if err != nil { return "", fmt.Errorf("failed to get refresh token: %w", err) } if dbToken == nil || dbToken.IsRevoked { return "", errors.New("invalid or revoked refresh token") } // Parse expiry time expiresAt, err := time.Parse(time.RFC3339, dbToken.ExpiresAt) if err != nil { return "", errors.New("invalid token expiry format") } // Check if token is expired if time.Now().After(expiresAt) { return "", auth.ErrExpiredToken } // Validate the refresh token claims, err := s.jwtManager.ValidateToken(refreshToken) if err != nil { return "", err } // Check if token is a refresh token if claims.TokenType != "refresh" { return "", errors.New("invalid token type") } // Build additionalClaims from the refresh token additionalClaims := make(map[string]interface{}) // Add string claims if claims.PatientID != "" { additionalClaims["patient_id"] = claims.PatientID } if claims.PatientName != "" { additionalClaims["patient_name"] = claims.PatientName } if claims.HomeURL != "" { additionalClaims["home_url"] = claims.HomeURL } if claims.StudyList != "" { additionalClaims["study_list"] = claims.StudyList } if claims.FilterURL != "" { additionalClaims["filter_url"] = claims.FilterURL } // Add array claims if len(claims.StudyIUIDs) > 0 { additionalClaims["study_iuids"] = claims.StudyIUIDs } if len(claims.AccessionNumbers) > 0 { additionalClaims["accession_numbers"] = claims.AccessionNumbers } // Generate a new access token with the same claims accessToken, err := s.jwtManager.GenerateAccessToken( claims.UserID, claims.Email, claims.Role, claims.UserName, additionalClaims, ) if err != nil { return "", err } return accessToken, nil } // ValidateToken validates a token and returns the claims func (s *AuthService) ValidateToken(token string) (*auth.CustomClaims, error) { return s.jwtManager.ValidateToken(token) } // Logout revokes a refresh token func (s *AuthService) Logout(refreshToken string) error { return s.userRepo.RevokeRefreshToken(refreshToken) } // HashPassword hashes a password using bcrypt func HashPassword(password string) (string, error) { hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return "", err } return string(hashedPassword), nil } // CheckPassword compares a password with a hash func CheckPassword(password, hash string) error { return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) }