diff --git a/cmd/server/main.go b/cmd/server/main.go index ced8593..24e2467 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -12,7 +12,6 @@ import ( "devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/config" "devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api" - "devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/logger" "go.uber.org/zap" ) @@ -24,7 +23,16 @@ func main() { } // Initialize logger - l := logger.New(cfg.LogLevel) + // l := logger.New(cfg.LogLevel) + // defer l.Sync() + + // * Uncomment kalau bukan debug + config := zap.NewDevelopmentConfig() + config.Level = zap.NewAtomicLevelAt(zap.DebugLevel) + l, err := config.Build() + if err != nil { + log.Fatalf("can't initialize zap logger: %v", err) + } defer l.Sync() // Setup router diff --git a/internal/api/handlers/auth.go b/internal/api/handlers/auth.go index 5470b84..7f19dfa 100644 --- a/internal/api/handlers/auth.go +++ b/internal/api/handlers/auth.go @@ -34,7 +34,7 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { return } - // Authenticate user + // Authenticate user using mock database response, err := h.authService.Login(req.Email, req.Password) if err != nil { h.logger.Warn("Login failed", zap.Error(err), zap.String("email", req.Email)) @@ -42,6 +42,12 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { return } + // Log successful login with role information + h.logger.Info("User logged in successfully", + zap.String("email", req.Email), + zap.String("userID", response.User.ID), + zap.String("role", response.User.Role)) + // Return tokens and user info w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) diff --git a/internal/api/handlers/dicom.go b/internal/api/handlers/dicom.go index c378d74..afbd0d4 100644 --- a/internal/api/handlers/dicom.go +++ b/internal/api/handlers/dicom.go @@ -4,8 +4,10 @@ import ( "fmt" "io" "net/http" + "net/url" "strings" + "devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/auth" "devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/proxy" "github.com/go-chi/chi/v5" "go.uber.org/zap" @@ -27,6 +29,13 @@ func NewDicomHandler(client *proxy.Client, logger *zap.Logger) *DicomHandler { // ForwardRequest forwards the request to Google Healthcare API func (h *DicomHandler) ForwardRequest(w http.ResponseWriter, r *http.Request) { + // Get claims from context if they exist + var claims *auth.CustomClaims + claimsValue := r.Context().Value("claims") + if claimsValue != nil { + claims = claimsValue.(*auth.CustomClaims) + } + // Get the path after /dicomWeb urlPath := chi.URLParam(r, "*") @@ -45,6 +54,46 @@ func (h *DicomHandler) ForwardRequest(w http.ResponseWriter, r *http.Request) { zap.String("url", r.URL.String()), ) + // Copy query parameters + queryParams := r.URL.Query() + + // Apply role-specific query modifications + if claims != nil { + switch claims.Role { + case "patient": + // For patients requesting study list, filter to only show their study + if strings.HasPrefix(urlPath, "/studies") && !strings.Contains(urlPath, "/") { + // Ensure only the patient's study is shown + queryParams.Set("StudyInstanceUID", claims.StudyIUID) + } + case "ref_doctor": + // For ref_doctor requesting study list, apply filter from token + if strings.HasPrefix(urlPath, "/studies") && !strings.Contains(urlPath, "/") && claims.FilterURL != "" { + // Parse the filter URL and add its query params + filterURL, err := url.Parse(claims.FilterURL) + if err == nil { + filterQuery := filterURL.Query() + for key, values := range filterQuery { + for _, value := range values { + queryParams.Add(key, value) + } + } + } + } + case "expertise_doctor": + // No restrictions for expertise_doctor + } + } + + // Add the query string back to the path + encodedQuery := queryParams.Encode() + if encodedQuery != "" { + if !strings.HasPrefix(urlPath, "/") { + urlPath = "/" + urlPath + } + urlPath = urlPath + "?" + encodedQuery + } + // Read request body if present var bodyBytes []byte if r.Body != nil && r.ContentLength > 0 { @@ -57,15 +106,6 @@ func (h *DicomHandler) ForwardRequest(w http.ResponseWriter, r *http.Request) { } } - // Copy query parameters - queryParams := r.URL.Query().Encode() - if queryParams != "" { - if !strings.HasPrefix(urlPath, "/") { - urlPath = "/" + urlPath - } - urlPath = urlPath + "?" + queryParams - } - // Get request headers headers := make(map[string]string) for k, v := range r.Header { diff --git a/internal/api/middleware/auth.go b/internal/api/middleware/auth.go index ab19f6f..c3be7d2 100644 --- a/internal/api/middleware/auth.go +++ b/internal/api/middleware/auth.go @@ -3,12 +3,13 @@ package middleware import ( "context" "encoding/json" + "fmt" "net/http" "regexp" "strings" "devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/service" - + "devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/auth" "go.uber.org/zap" ) @@ -18,6 +19,7 @@ const ( UserIDKey contextKey = "user_id" UserRoleKey contextKey = "user_role" UserEmailKey contextKey = "user_email" + ClaimsKey contextKey = "auth_claims" // Use this same key everywhere ) // WhitelistedEndpoints contains paths that can be accessed without authentication @@ -33,25 +35,10 @@ var WhitelistedEndpoints = []*regexp.Regexp{ func Auth(authService *service.AuthService, logger *zap.Logger) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Check if the request path is whitelisted - path := r.URL.Path - if r.URL.RawQuery != "" { - path = path + "?" + r.URL.RawQuery - } - - for _, pattern := range WhitelistedEndpoints { - if pattern.MatchString(path) { - // Path is whitelisted, skip authentication - logger.Debug("Skipping authentication for whitelisted path", zap.String("path", path)) - next.ServeHTTP(w, r) - return - } - } - // Get authorization header authHeader := r.Header.Get("Authorization") if authHeader == "" { - logger.Warn("Missing Authorization header") + logger.Warn("Missing Authorization header", zap.String("path", r.URL.Path)) respondWithError(w, http.StatusUnauthorized, "missing authorization header") return } @@ -59,7 +46,7 @@ func Auth(authService *service.AuthService, logger *zap.Logger) func(http.Handle // Extract token from Bearer token bearerToken := strings.Split(authHeader, " ") if len(bearerToken) != 2 || strings.ToLower(bearerToken[0]) != "bearer" { - logger.Warn("Invalid Authorization header format") + logger.Warn("Invalid Authorization header format", zap.String("header", authHeader)) respondWithError(w, http.StatusUnauthorized, "invalid authorization format") return } @@ -86,8 +73,13 @@ func Auth(authService *service.AuthService, logger *zap.Logger) func(http.Handle ctx = context.WithValue(ctx, UserRoleKey, claims.Role) ctx = context.WithValue(ctx, UserEmailKey, claims.Email) - // Log user info - logger.Info("Authenticated user", zap.String("userID", claims.UserID), zap.String("role", claims.Role), zap.String("email", claims.Email)) + // Store the claims with the defined context key + ctx = context.WithValue(ctx, ClaimsKey, claims) + + // Log successful authentication + logger.Debug("Auth middleware: Token validated", + zap.String("userID", claims.UserID), + zap.String("role", claims.Role)) // Continue with the request next.ServeHTTP(w, r.WithContext(ctx)) @@ -140,55 +132,88 @@ func RoleRequired(roles ...string) func(http.Handler) http.Handler { } } -// PatientViewRestriction middleware restricts patients to view only their studies +// PatientViewRestriction ensures patients can only access their own studies func PatientViewRestriction(logger *zap.Logger) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Check if the request path is whitelisted first - path := r.URL.Path - if r.URL.RawQuery != "" { - path = path + "?" + r.URL.RawQuery - } - - for _, pattern := range WhitelistedEndpoints { - if pattern.MatchString(path) { - // Path is whitelisted, skip restrictions - logger.Debug("Skipping patient restrictions for whitelisted path", zap.String("path", path)) - next.ServeHTTP(w, r) - return - } - } - - // Get user role from context - userRole, ok := r.Context().Value(UserRoleKey).(string) - if !ok { - respondWithError(w, http.StatusUnauthorized, "user context not found") + // Get claims from context using the defined key + claimsValue := r.Context().Value(ClaimsKey) + if claimsValue == nil { + logger.Error("Missing claims in context - PatientViewRestriction middleware", + zap.String("path", r.URL.Path), + zap.String("method", r.Method)) + http.Error(w, "Unauthorized", http.StatusUnauthorized) return } - // Only apply restrictions to patients - if userRole != "patient" { + claims, ok := claimsValue.(*auth.CustomClaims) + if !ok { + logger.Error("Invalid claims type in context", + zap.String("type", fmt.Sprintf("%T", claimsValue))) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + logger.Debug("PatientViewRestriction: Got claims from context", + zap.String("userID", claims.UserID), + zap.String("role", claims.Role)) + + // Only apply restrictions to patient role + if claims.Role != "patient" { + // For non-patient roles, continue with the request next.ServeHTTP(w, r) return } - // TODO: Logic to restrict patients to only access their assigned studies - // For now, we're just letting the request through, but in a real - // implementation, you would check the study ID against the patient's - // assigned studies. + // Parse the path to extract StudyInstanceUID if present + path := r.URL.Path + parts := strings.Split(path, "/") - // TODO: Check if the requested study is assigned to the patient - // This would likely involve parsing the URL path to extract study ID - // and checking it against a database of patient assignments + // Check if this is a study-specific request + var requestedStudyUID string + for i, part := range parts { + if part == "studies" && i+1 < len(parts) { + requestedStudyUID = parts[i+1] + break + } + } + // If there's no study UID in the path, check query parameters + if requestedStudyUID == "" { + queryStudyUID := r.URL.Query().Get("StudyInstanceUID") + if queryStudyUID != "" { + requestedStudyUID = queryStudyUID + } + } + + // If a study is being requested, verify patient has access + if requestedStudyUID != "" && requestedStudyUID != claims.StudyIUID { + logger.Warn("Patient attempted to access unauthorized study", + zap.String("userID", claims.UserID), + zap.String("role", claims.Role), + zap.String("authorizedStudy", claims.StudyIUID), + zap.String("requestedStudy", requestedStudyUID)) + + // Return 403 Forbidden with a clear message + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusForbidden) + json.NewEncoder(w).Encode(map[string]string{ + "error": "Access denied: You do not have permission to view this study", + "code": "forbidden_study_access", + "redirect_url": "/notfoundstudy", + }) + return + } + + // Patient has access or is requesting a list (which will be filtered) next.ServeHTTP(w, r) }) } } -// Helper function to respond with JSON error -func respondWithError(w http.ResponseWriter, code int, message string) { +// Helper function to respond with an error +func respondWithError(w http.ResponseWriter, statusCode int, message string) { w.Header().Set("Content-Type", "application/json") - w.WriteHeader(code) + w.WriteHeader(statusCode) json.NewEncoder(w).Encode(map[string]string{"error": message}) } diff --git a/internal/api/models/mock_data.go b/internal/api/models/mock_data.go new file mode 100644 index 0000000..b437506 --- /dev/null +++ b/internal/api/models/mock_data.go @@ -0,0 +1,104 @@ +package models + +// MockUsers represents a mock database of users +var MockUsers = []User{ + { + ID: "1", + Email: "admin", + Role: "expertise_doctor", + Name: "Admin User", + CreatedAt: "2025-01-01T00:00:00Z", + UpdatedAt: "2025-01-01T00:00:00Z", + }, + { + ID: "2", + Email: "patient", + Role: "patient", + Name: "Patient User", + CreatedAt: "2025-01-01T00:00:00Z", + UpdatedAt: "2025-01-01T00:00:00Z", + }, + { + ID: "3", + Email: "doctor", + Role: "ref_doctor", + Name: "DR. HERWINDO RIDWAN, SP.OT", + CreatedAt: "2025-01-01T00:00:00Z", + UpdatedAt: "2025-01-01T00:00:00Z", + }, + { + ID: "4", + Email: "patient2", + Role: "patient", + Name: "Patient Two", + CreatedAt: "2025-01-01T00:00:00Z", + UpdatedAt: "2025-01-01T00:00:00Z", + }, +} + +// PatientData represents additional data for patients +type PatientData struct { + PatientID string `json:"patient_id"` + UserID string `json:"user_id"` + StudyIUID string `json:"study_iuid"` + AccessionNumber string `json:"accession_number"` + PatientName string `json:"patient_name"` + ReferringPhysician string `json:"referring_physician"` +} + +// MockPatients represents a mock database of patient data +var MockPatients = []PatientData{ + { + PatientID: "00211622", + UserID: "2", + StudyIUID: "1.2.826.0.1.3680043.9.7307.1.20180713036", + AccessionNumber: "CR.180713.036", + PatientName: "DIDIT SUYATNA^R.10049.18", + ReferringPhysician: "DR. HERWINDO RIDWAN, SP.OT", + }, + { + PatientID: "MR00000359", + UserID: "4", + StudyIUID: "1.2.826.0.1.3680043.9.7307.1.202503196393.01", + AccessionNumber: "CR.250319.6393.01", + PatientName: "Bobon Santoso", + ReferringPhysician: "DR. HERWINDO RIDWAN, SP.OT", + }, +} + +// FindUserByCredentials finds a user by email and password (mock authentication) +func FindUserByCredentials(email, password string) *User { + // In a real implementation, you would hash passwords + // For the mock, we'll just match email and assume password is the same as email + if password != email { + return nil + } + + for _, user := range MockUsers { + if user.Email == email { + return &user + } + } + return nil +} + +// FindPatientDataByUserID finds patient data by user ID +func FindPatientDataByUserID(userID string) *PatientData { + for _, patient := range MockPatients { + if patient.UserID == userID { + return &patient + } + } + return nil +} + +// FindStudiesByReferringPhysician returns all study IUIDs that belong to a referring physician +func FindStudiesByReferringPhysician(physicianName string) []string { + var studies []string + for _, patient := range MockPatients { + if patient.ReferringPhysician == physicianName { + studies = append(studies, patient.StudyIUID) + } + } + return studies +} diff --git a/internal/api/routes.go b/internal/api/routes.go index 3fab51a..3484bb6 100644 --- a/internal/api/routes.go +++ b/internal/api/routes.go @@ -1,7 +1,9 @@ package api import ( + "encoding/json" "net/http" + "strings" "time" "devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/config" @@ -113,5 +115,54 @@ func SetupRouter(cfg *config.Config, logger *zap.Logger) http.Handler { }) }) + // Add this to your routes setup - in the public routes group + r.Get("/debug/token", func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{ + "error": "No Authorization header provided", + }) + return + } + + bearerToken := strings.Split(authHeader, " ") + if len(bearerToken) != 2 || strings.ToLower(bearerToken[0]) != "bearer" { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{ + "error": "Invalid Authorization format", + }) + return + } + + token := bearerToken[1] + claims, err := authService.ValidateToken(token) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{ + "error": err.Error(), + }) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "valid", + "token_type": claims.TokenType, + "user": map[string]string{ + "id": claims.UserID, + "email": claims.Email, + "role": claims.Role, + "name": claims.UserName, + }, + "patient_info": map[string]string{ + "patient_id": claims.PatientID, + "study_iuid": claims.StudyIUID, + "accession_number": claims.AccessionNumber, + }, + }) + }) + return r } diff --git a/internal/api/service/auth_service.go b/internal/api/service/auth_service.go index 1458834..e8ebdba 100644 --- a/internal/api/service/auth_service.go +++ b/internal/api/service/auth_service.go @@ -2,7 +2,8 @@ package service import ( "errors" - "time" + "fmt" + "net/url" "golang.org/x/crypto/bcrypt" @@ -31,105 +32,70 @@ func NewAuthService(jwtManager *auth.JWTManager) *AuthService { // Login authenticates a user and generates tokens func (s *AuthService) Login(email, password string) (*models.LoginResponse, error) { - // For now, use hardcoded credentials - // TODO: In a real implementation, you would query the database - if email == "admin" && password == "admin" { - // Create a dummy user - user := &models.User{ - ID: "1", - Email: "admin", - Role: "expertise_doctor", - Name: "Admin User", - CreatedAt: time.Now().Format(time.RFC3339), - UpdatedAt: time.Now().Format(time.RFC3339), - } - - // Generate tokens - accessToken, err := s.jwtManager.GenerateAccessToken(user.ID, user.Email, user.Role) - if err != nil { - return nil, err - } - - refreshToken, err := s.jwtManager.GenerateRefreshToken(user.ID, user.Email, user.Role) - if err != nil { - return nil, err - } - - // TODO: In a real implementation, you would store the refresh token in the database - // For example: - // s.storeRefreshToken(user.ID, refreshToken) - - // Determine redirect URL based on role - redirectURL := "/viewer" - if user.Role == "ref_doctor" || user.Role == "expertise_doctor" { - redirectURL = "/studylist" - } - - return &models.LoginResponse{ - AccessToken: accessToken, - RefreshToken: refreshToken, - User: user, - RedirectURL: redirectURL, - }, nil - } else if email == "patient" && password == "patient" { - // Create a patient user - user := &models.User{ - ID: "2", - Email: "patient", - Role: "patient", - Name: "Patient User", - CreatedAt: time.Now().Format(time.RFC3339), - UpdatedAt: time.Now().Format(time.RFC3339), - } - - // Generate tokens with patient-specific claims - accessToken, err := s.jwtManager.GenerateAccessToken(user.ID, user.Email, user.Role) - if err != nil { - return nil, err - } - - refreshToken, err := s.jwtManager.GenerateRefreshToken(user.ID, user.Email, user.Role) - if err != nil { - return nil, err - } - - return &models.LoginResponse{ - AccessToken: accessToken, - RefreshToken: refreshToken, - User: user, - RedirectURL: "/viewer", - }, nil - } else if email == "doctor" && password == "doctor" { - // Create a referring doctor user - user := &models.User{ - ID: "3", - Email: "doctor", - Role: "ref_doctor", - Name: "Doctor User", - CreatedAt: time.Now().Format(time.RFC3339), - UpdatedAt: time.Now().Format(time.RFC3339), - } - - // Generate tokens - accessToken, err := s.jwtManager.GenerateAccessToken(user.ID, user.Email, user.Role) - if err != nil { - return nil, err - } - - refreshToken, err := s.jwtManager.GenerateRefreshToken(user.ID, user.Email, user.Role) - if err != nil { - return nil, err - } - - return &models.LoginResponse{ - AccessToken: accessToken, - RefreshToken: refreshToken, - User: user, - RedirectURL: "/studylist", - }, nil + // Find user in mock data + user := models.FindUserByCredentials(email, password) + if user == nil { + return nil, ErrInvalidCredentials } - return nil, ErrInvalidCredentials + // Create token claims based on user role + additionalClaims := make(map[string]string) + var redirectURL string + + switch user.Role { + case "patient": + // Get patient data + patientData := models.FindPatientDataByUserID(user.ID) + if patientData == nil { + return nil, ErrUserNotFound + } + + // Set patient-specific claims + additionalClaims["patient_id"] = patientData.PatientID + additionalClaims["patient_name"] = patientData.PatientName + additionalClaims["accession_number"] = patientData.AccessionNumber + additionalClaims["study_iuid"] = patientData.StudyIUID + additionalClaims["home_url"] = fmt.Sprintf("viewer?StudyInstanceUIDs=%s", patientData.StudyIUID) + additionalClaims["study_list"] = "disabled" + + redirectURL = fmt.Sprintf("/viewer?StudyInstanceUIDs=%s", patientData.StudyIUID) + + 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": + // 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 + } + + return &models.LoginResponse{ + AccessToken: accessToken, + RefreshToken: refreshToken, + User: user, + RedirectURL: redirectURL, + }, nil } // RefreshToken generates a new access token using a refresh token @@ -145,9 +111,32 @@ func (s *AuthService) RefreshToken(refreshToken string) (string, error) { return "", errors.New("invalid token type") } - // TODO: In a real implementation, you would check if the token is in the database and not revoked - // Here we just generate a new access token - accessToken, err := s.jwtManager.GenerateAccessToken(claims.UserID, claims.Email, claims.Role) + // Build additionalClaims from the refresh token + additionalClaims := make(map[string]string) + if claims.PatientID != "" { + additionalClaims["patient_id"] = claims.PatientID + } + if claims.PatientName != "" { + additionalClaims["patient_name"] = claims.PatientName + } + if claims.AccessionNumber != "" { + additionalClaims["accession_number"] = claims.AccessionNumber + } + if claims.StudyIUID != "" { + additionalClaims["study_iuid"] = claims.StudyIUID + } + if claims.HomeURL != "" { + additionalClaims["home_url"] = claims.HomeURL + } + if claims.StudyList != "" { + additionalClaims["study_list"] = claims.StudyList + } + if claims.FilterURL != "" { + additionalClaims["filter_url"] = claims.FilterURL + } + + // 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 } @@ -179,39 +168,11 @@ func CheckPassword(password, hash string) error { // storeRefreshToken stores a refresh token in the database func (s *AuthService) storeRefreshToken(userID, token string) error { // TODO: In a real implementation, this would insert a record in the database - // For example: - /* - refreshToken := &models.RefreshToken{ - ID: uuid.New().String(), - UserID: userID, - Token: token, - ExpiresAt: time.Now().Add(7 * 24 * time.Hour).Format(time.RFC3339), - IsRevoked: false, - CreatedAt: time.Now().Format(time.RFC3339), - } - - _, err := s.db.NamedExec( - `INSERT INTO refresh_tokens (id, user_id, token, expires_at, is_revoked, created_at) - VALUES (:id, :user_id, :token, :expires_at, :is_revoked, :created_at)`, - refreshToken, - ) - return err - */ - return nil } // revokeRefreshToken marks a refresh token as revoked func (s *AuthService) revokeRefreshToken(token string) error { // TODO: In a real implementation, this would update a record in the database - // For example: - /* - _, err := s.db.Exec( - "UPDATE refresh_tokens SET is_revoked = true WHERE token = ?", - token, - ) - return err - */ - return nil } diff --git a/internal/auth/jwt.go b/internal/auth/jwt.go index a7b9050..e65c90b 100644 --- a/internal/auth/jwt.go +++ b/internal/auth/jwt.go @@ -34,16 +34,30 @@ type CustomClaims struct { UserID string `json:"user_id"` Email string `json:"email"` Role string `json:"role"` + UserName string `json:"user_name"` TokenType string `json:"token_type"` // access or refresh + + // Patient-specific fields + PatientID string `json:"patient_id,omitempty"` + PatientName string `json:"patient_name,omitempty"` + AccessionNumber string `json:"accession_number,omitempty"` + StudyIUID string `json:"study_iuid,omitempty"` + + // Navigation and permissions + HomeURL string `json:"home_url,omitempty"` + StudyList string `json:"study_list,omitempty"` // enabled or disabled + FilterURL string `json:"filter_url,omitempty"` // for ref_doctor filtering + jwt.RegisteredClaims } -// GenerateAccessToken creates a new access token -func (m *JWTManager) GenerateAccessToken(userID, email, role string) (string, error) { +// GenerateAccessToken creates a new access token with role-specific claims +func (m *JWTManager) GenerateAccessToken(userID, email, role, userName string, additionalClaims map[string]string) (string, error) { claims := CustomClaims{ UserID: userID, Email: email, Role: role, + UserName: userName, TokenType: "access", RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(m.accessExpiry)), @@ -51,16 +65,42 @@ func (m *JWTManager) GenerateAccessToken(userID, email, role string) (string, er }, } + // Add role-specific additional claims + if additionalClaims != nil { + if val, ok := additionalClaims["patient_id"]; ok { + claims.PatientID = val + } + if val, ok := additionalClaims["patient_name"]; ok { + claims.PatientName = val + } + if val, ok := additionalClaims["accession_number"]; ok { + claims.AccessionNumber = val + } + if val, ok := additionalClaims["study_iuid"]; ok { + claims.StudyIUID = val + } + if val, ok := additionalClaims["home_url"]; ok { + claims.HomeURL = val + } + if val, ok := additionalClaims["study_list"]; ok { + claims.StudyList = val + } + if val, ok := additionalClaims["filter_url"]; ok { + claims.FilterURL = val + } + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(m.secretKey)) } -// GenerateRefreshToken creates a new refresh token -func (m *JWTManager) GenerateRefreshToken(userID, email, role string) (string, error) { +// GenerateRefreshToken creates a new refresh token with the same claims as access token +func (m *JWTManager) GenerateRefreshToken(userID, email, role, userName string, additionalClaims map[string]string) (string, error) { claims := CustomClaims{ UserID: userID, Email: email, Role: role, + UserName: userName, TokenType: "refresh", RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(m.refreshExpiry)), @@ -68,6 +108,31 @@ func (m *JWTManager) GenerateRefreshToken(userID, email, role string) (string, e }, } + // Add role-specific additional claims (same as access token) + if additionalClaims != nil { + if val, ok := additionalClaims["patient_id"]; ok { + claims.PatientID = val + } + if val, ok := additionalClaims["patient_name"]; ok { + claims.PatientName = val + } + if val, ok := additionalClaims["accession_number"]; ok { + claims.AccessionNumber = val + } + if val, ok := additionalClaims["study_iuid"]; ok { + claims.StudyIUID = val + } + if val, ok := additionalClaims["home_url"]; ok { + claims.HomeURL = val + } + if val, ok := additionalClaims["study_list"]; ok { + claims.StudyList = val + } + if val, ok := additionalClaims["filter_url"]; ok { + claims.FilterURL = val + } + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(m.secretKey)) } diff --git a/test/http/ohif-flow.http b/test/http/ohif-flow.http index e84ae7c..045d47c 100644 --- a/test/http/ohif-flow.http +++ b/test/http/ohif-flow.http @@ -1,4 +1,4 @@ -@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMiIsImVtYWlsIjoicGF0aWVudCIsInJvbGUiOiJwYXRpZW50IiwidG9rZW5fdHlwZSI6ImFjY2VzcyIsImV4cCI6MTc0NjUwNDUzMCwiaWF0IjoxNzQ2NDE4MTMwfQ.AvSBHvy3y22Pa4M8MZS9u00fiBtHzcS_WbxukxsBcj4 +@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMiIsImVtYWlsIjoicGF0aWVudCIsInJvbGUiOiJwYXRpZW50IiwidXNlcl9uYW1lIjoiUGF0aWVudCBVc2VyIiwidG9rZW5fdHlwZSI6ImFjY2VzcyIsInBhdGllbnRfaWQiOiIwMDIxMTYyMiIsInBhdGllbnRfbmFtZSI6IkRJRElUIFNVWUFUTkFeUi4xMDA0OS4xOCIsImFjY2Vzc2lvbl9udW1iZXIiOiJDUi4xODA3MTMuMDM2Iiwic3R1ZHlfaXVpZCI6IjEuMi44MjYuMC4xLjM2ODAwNDMuOS43MzA3LjEuMjAxODA3MTMwMzYiLCJob21lX3VybCI6InZpZXdlcj9TdHVkeUluc3RhbmNlVUlEcz0xLjIuODI2LjAuMS4zNjgwMDQzLjkuNzMwNy4xLjIwMTgwNzEzMDM2Iiwic3R1ZHlfbGlzdCI6ImRpc2FibGVkIiwiZXhwIjoxNzQ2ODUyMDU2LCJpYXQiOjE3NDY3NjU2NTZ9.wGKCU77toQ5D27DRcNJE_XQjoJmxxzqoPbmf5N7D0cw @token_exp_doctor = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMSIsImVtYWlsIjoiYWRtaW4iLCJyb2xlIjoiZXhwZXJ0aXNlX2RvY3RvciIsInRva2VuX3R5cGUiOiJhY2Nlc3MiLCJleHAiOjE3NDY1MDQ1MTYsImlhdCI6MTc0NjQxODExNn0.vlDrns1oPFXHE5--TmWqwzvzxnfcCPcV2UW8_4GwDwE @baseUrl = http://localhost:5555 @@ -25,7 +25,7 @@ Content-Type: application/json # Destination URL / OHIF Viewer URL: http://152.42.173.210:3000/viewer?StudyInstanceUIDs=1.2.826.0.1.3680043.9.7307.1.202503196393.01 ### Study where StudyIUID -GET {{baseUrl}}/dicomWeb/studies?limit=101&offset=0&fuzzymatching=true&includefield=00081030,00080060&StudyInstanceUID=1.2.826.0.1.3680043.9.7307.1.202503196393.01 +GET {{baseUrl}}/dicomWeb/studies?limit=101&offset=0&fuzzymatching=true&includefield=00081030,00080060&StudyInstanceUID=1.2.826.0.1.3680043.9.7307.1.20180713036 Authorization: Bearer {{token}} ### Series List