add: cors handler route and readme
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -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
209
README.md
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
104
internal/api/models/mock_data.go
Normal file
104
internal/api/models/mock_data.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
BIN
ohif-proxy
BIN
ohif-proxy
Binary file not shown.
BIN
ohif-proxy-old
BIN
ohif-proxy-old
Binary file not shown.
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user