add: cors handler route and readme

This commit is contained in:
mario
2025-05-09 10:13:16 +07:00
parent 6c9ab574ce
commit 13bb380f51
13 changed files with 674 additions and 203 deletions

3
.gitignore vendored
View File

@@ -1,6 +1,7 @@
# Binary files
bin/
dicom-proxy
build/*
# Credentials
credentials/*.json
@@ -18,4 +19,4 @@ vendor/
# OS specific files
.DS_Store
Thumbs.db
Thumbs.db

209
README.md
View File

@@ -183,4 +183,211 @@ make test
---
Untuk informasi lebih lanjut tentang OHIF: [https://ohif.org/](https://ohif.org/)
Untuk informasi lebih lanjut tentang Google Cloud Healthcare API: [https://cloud.google.com/healthcare](https://cloud.google.com/healthcare)
Untuk informasi lebih lanjut tentang Google Cloud Healthcare API: [https://cloud.google.com/healthcare](https://cloud.google.com/healthcare)
# GO-OHIF-Proxy Architecture
## Project Structure
```
go-ohif-proxy/
├── cmd/
│ └── server/ # Application entry point
│ └── main.go # Main function and application start
├── config/ # Configuration
│ ├── config.go # Config structure definitions
│ └── config.yaml # YAML configuration file
├── credentials/ # Authentication credentials
│ └── service-account.json # Google service account credentials
├── internal/ # Internal packages (not importable)
│ ├── api/ # API implementation
│ │ ├── handlers/ # HTTP request handlers
│ │ │ ├── auth.go # Authentication handlers
│ │ │ ├── dicom.go # DICOM request handlers
│ │ │ └── healthcheck.go # Health check endpoint
│ │ │
│ │ ├── middleware/ # HTTP middleware
│ │ │ ├── auth.go # Authentication middleware
│ │ │ └── logging.go # Request logging middleware
│ │ │
│ │ ├── models/ # Data models
│ │ │ └── user.go # User and authentication models
│ │ │
│ │ ├── repository/ # Data access layer
│ │ │ ├── interfaces.go # Repository interfaces
│ │ │ └── mysql_repository.go # MySQL implementation
│ │ │
│ │ ├── service/ # Business logic
│ │ │ ├── auth_service.go # Authentication service
│ │ │ └── db_repository.go # Legacy repository code (to be removed)
│ │ │
│ │ └── routes.go # API route definitions
│ │
│ ├── auth/ # Authentication utilities
│ │ ├── google.go # Google authentication client
│ │ └── jwt.go # JWT generation and validation
│ │
│ ├── logger/ # Logging utilities
│ │ └── logger.go # Logger configuration
│ │
│ └── proxy/ # DICOM web proxy functionality
│ └── client.go # Google Healthcare API client
├── pkg/ # Public packages (importable)
├── test/ # Test files
│ └── http/ # HTTP test requests
│ ├── ohif-flow.http # OHIF workflow tests
│ └── test.http # General API tests
├── docker-compose.yaml # Docker Compose configuration
├── Dockerfile # Docker build instructions
├── go.mod # Go module definition
├── go.sum # Go module checksums
├── Makefile # Build automation
└── README.md # Project documentation
```
## Architecture Diagram
```mermaid
graph TD
subgraph Client
OHIF[OHIF Viewer]
end
subgraph API_Gateway
Router[Chi Router]
Auth_MW[Auth Middleware]
Logging_MW[Logging Middleware]
CORS_MW[CORS Middleware]
PatientView_MW[Patient View Restriction]
end
subgraph Services
AuthService[Auth Service]
DicomService[DICOM Service]
end
subgraph Repositories
UserRepo[User Repository]
TokenRepo[Token Repository]
PatientRepo[Patient Repository]
DoctorRepo[Doctor Repository]
end
subgraph External
GoogleHealthcare[Google Healthcare API]
MySQL[MySQL Database]
end
subgraph Utilities
JWTManager[JWT Manager]
GoogleAuth[Google Auth Client]
Logger[Logger]
end
OHIF -->|HTTP Requests| Router
Router --> Auth_MW
Auth_MW --> Logging_MW
Logging_MW --> CORS_MW
CORS_MW --> PatientView_MW
PatientView_MW --> DicomService
Router -->|Auth Routes| AuthService
AuthService -->|Uses| JWTManager
AuthService -->|Uses| UserRepo
AuthService -->|Uses| TokenRepo
DicomService -->|Uses| GoogleHealthcare
DicomService -->|Uses| PatientRepo
UserRepo -->|Implements| MySQL
TokenRepo -->|Implements| MySQL
PatientRepo -->|Implements| MySQL
DoctorRepo -->|Implements| MySQL
DicomService -->|Uses| GoogleAuth
GoogleAuth -->|Authenticates| GoogleHealthcare
```
## Authentication Flow
```mermaid
sequenceDiagram
participant Client
participant API as API Gateway
participant AuthService
participant Repo as Repository
participant JWT as JWT Manager
participant DB as Database
Client->>API: POST /auth/login
API->>AuthService: Login(email, password)
alt Database Auth Enabled
AuthService->>Repo: GetUserByEmail(email)
Repo->>DB: SELECT * FROM users
DB->>Repo: User Data
Repo->>AuthService: User Object
AuthService->>JWT: Generate Tokens
JWT->>AuthService: Access + Refresh Tokens
AuthService->>Repo: StoreRefreshToken()
Repo->>DB: INSERT INTO refresh_tokens
else Hardcoded Auth
AuthService->>JWT: Generate Tokens
JWT->>AuthService: Access + Refresh Tokens
end
AuthService->>API: Tokens + User Info
API->>Client: Auth Response
Note over Client,API: Later - Protected Request
Client->>API: GET /dicomWeb/* with Bearer Token
API->>AuthService: ValidateToken()
AuthService->>JWT: ParseToken()
JWT->>AuthService: Claim Data
AuthService->>API: User Context
API->>Client: Protected Resource
```
## DICOM Request Flow
```mermaid
sequenceDiagram
participant Client
participant API as API Gateway
participant Auth as Auth Middleware
participant PatientMW as Patient Restriction MW
participant DicomHandler
participant GCP as Google Healthcare API
Client->>API: GET /dicomWeb/studies/{studyUID}
alt Whitelisted Path
API->>DicomHandler: Forward Request (Skip Auth)
else Protected Path
API->>Auth: Check Authentication
Auth->>API: User Context
API->>PatientMW: Check Access Rights
alt Patient Role
PatientMW->>Repo: IsStudyAssignedToPatient()
Repo->>PatientMW: Access Result
alt Study Assigned
PatientMW->>DicomHandler: Forward Request
else Study Not Assigned
PatientMW->>Client: 403 Forbidden
end
else Doctor Role
PatientMW->>DicomHandler: Forward Request
end
end
DicomHandler->>GCP: Forward Request to DICOM Store
GCP->>DicomHandler: DICOM Data
DicomHandler->>Client: DICOM Response
```

View File

@@ -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

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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,87 @@ 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",
})
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})
}

View File

@@ -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
}

View File

@@ -1,7 +1,9 @@
package api
import (
"encoding/json"
"net/http"
"strings"
"time"
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/config"
@@ -31,12 +33,16 @@ func SetupRouter(cfg *config.Config, logger *zap.Logger) http.Handler {
r.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{"*"}, // In production, restrict this to your frontend domains
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
ExposedHeaders: []string{"Link"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token", "X-Requested-With"},
ExposedHeaders: []string{"Link", "Content-Length", "Content-Disposition", "Content-Type"},
AllowCredentials: true,
MaxAge: 300, // Maximum value not ignored by any of major browsers
}))
r.Options("/*", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
// Initialize Google auth client for proxy
googleAuth, err := auth.NewGoogleClient(cfg.Google.CredentialsPath)
if err != nil {
@@ -109,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
}

View File

@@ -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
}

View File

@@ -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))
}

Binary file not shown.

Binary file not shown.

View File

@@ -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