233 lines
6.5 KiB
Go
233 lines
6.5 KiB
Go
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, ErrUserNotFound
|
|
}
|
|
|
|
// 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 = "/"
|
|
}
|
|
|
|
// TODO: Apakah kita perlu param email untuk generate access token?
|
|
// 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))
|
|
}
|