add: /uploaded_dicom for pydicom-google-upload to get shortlink
This commit is contained in:
@@ -31,6 +31,7 @@ type Config struct {
|
|||||||
AccessTokenExpiry int `mapstructure:"access_token_expiry"` // in minutes
|
AccessTokenExpiry int `mapstructure:"access_token_expiry"` // in minutes
|
||||||
RefreshTokenExpiry int `mapstructure:"refresh_token_expiry"` // in hours
|
RefreshTokenExpiry int `mapstructure:"refresh_token_expiry"` // in hours
|
||||||
EnableDatabaseAuth bool `mapstructure:"enable_database_auth"`
|
EnableDatabaseAuth bool `mapstructure:"enable_database_auth"`
|
||||||
|
PydicomApiKey string `mapstructure:"pydicom_api_key"` // API Key for PYDICOM uploader service
|
||||||
} `mapstructure:"auth"`
|
} `mapstructure:"auth"`
|
||||||
|
|
||||||
Shortlink struct {
|
Shortlink struct {
|
||||||
|
|||||||
@@ -18,10 +18,11 @@ auth:
|
|||||||
access_token_expiry: 1440 # minutes (24 hours)
|
access_token_expiry: 1440 # minutes (24 hours)
|
||||||
refresh_token_expiry: 168 # hours (7 days)
|
refresh_token_expiry: 168 # hours (7 days)
|
||||||
enable_database_auth: true # Changed to true to use database
|
enable_database_auth: true # Changed to true to use database
|
||||||
|
pydicom_api_key: "2f0ff447b2c3aeef2004e83a750ded97e29ba8c0ccc70053d5e26f5d715e42ff"
|
||||||
|
|
||||||
shortlink:
|
shortlink:
|
||||||
base_url: "http://localhost:3333" # The base URL for generated OHIF Auth shortlinks
|
base_url: "http://localhost:3333" # The base URL for generated OHIF Auth shortlinks
|
||||||
default_expiry_hours: 24 # Default expiry time for shortlinks (1 day)
|
default_expiry_hours: 30 * 24 # Default expiry time for shortlinks (30 days)
|
||||||
max_attempts: 5 # Maximum number of failed login attempts
|
max_attempts: 5 # Maximum number of failed login attempts
|
||||||
|
|
||||||
database:
|
database:
|
||||||
|
|||||||
158
internal/api/handlers/pydicom.go
Normal file
158
internal/api/handlers/pydicom.go
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models"
|
||||||
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/service"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PydicomHandler handles operations related to PYDICOM uploads
|
||||||
|
type PydicomHandler struct {
|
||||||
|
logger *zap.Logger
|
||||||
|
shortLinkService *service.ShortLinkService
|
||||||
|
registerService *service.RegisterService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPydicomHandler creates a new PydicomHandler
|
||||||
|
func NewPydicomHandler(logger *zap.Logger, shortLinkService *service.ShortLinkService, registerService *service.RegisterService) *PydicomHandler {
|
||||||
|
return &PydicomHandler{
|
||||||
|
logger: logger,
|
||||||
|
shortLinkService: shortLinkService,
|
||||||
|
registerService: registerService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleUploadedDicom processes a request from the PYDICOM uploader service to register a patient and generate a shortlink
|
||||||
|
func (h *PydicomHandler) HandleUploadedDicom(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Parse the request body into a temporary struct that matches the expected JSON
|
||||||
|
var reqData struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
Patient struct {
|
||||||
|
PatientID string `json:"patient_id"`
|
||||||
|
PatientName string `json:"patient_name"`
|
||||||
|
DateOfBirth string `json:"date_of_birth"`
|
||||||
|
} `json:"patient"`
|
||||||
|
Studies []struct {
|
||||||
|
StudyInstanceUID string `json:"study_instance_uid"`
|
||||||
|
AccessionNumber string `json:"accession_number"`
|
||||||
|
StudyDate string `json:"study_date"`
|
||||||
|
StudyDescription string `json:"study_description"`
|
||||||
|
} `json:"studies"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&reqData); err != nil {
|
||||||
|
h.logger.Error("Failed to parse uploaded DICOM request", zap.Error(err))
|
||||||
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the request
|
||||||
|
if reqData.Email == "" || reqData.Password == "" || reqData.Name == "" {
|
||||||
|
h.logger.Error("Missing required user fields in uploaded DICOM request")
|
||||||
|
http.Error(w, "Missing required user fields", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if reqData.Patient.PatientID == "" || reqData.Patient.DateOfBirth == "" {
|
||||||
|
h.logger.Error("Missing required patient fields in uploaded DICOM request")
|
||||||
|
http.Error(w, "Missing required patient fields", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(reqData.Studies) == 0 || reqData.Studies[0].StudyInstanceUID == "" {
|
||||||
|
h.logger.Error("Missing required study fields in uploaded DICOM request")
|
||||||
|
http.Error(w, "Missing required study fields", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to our internal models
|
||||||
|
patientDetails := &models.PatientDetails{
|
||||||
|
PatientID: reqData.Patient.PatientID,
|
||||||
|
PatientName: reqData.Patient.PatientName,
|
||||||
|
DateOfBirth: reqData.Patient.DateOfBirth,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert studies to our internal model
|
||||||
|
studies := make([]models.Study, len(reqData.Studies))
|
||||||
|
for i, s := range reqData.Studies {
|
||||||
|
studies[i] = models.Study{
|
||||||
|
StudyInstanceUID: s.StudyInstanceUID,
|
||||||
|
AccessionNumber: s.AccessionNumber,
|
||||||
|
StudyDate: s.StudyDate,
|
||||||
|
StudyDescription: s.StudyDescription,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create registration request
|
||||||
|
regRequest := &service.RegisterRequest{
|
||||||
|
Email: reqData.Email,
|
||||||
|
Password: reqData.Password,
|
||||||
|
Name: reqData.Name,
|
||||||
|
Role: "patient", // Force the role to be "patient" regardless of what was sent
|
||||||
|
Patient: patientDetails,
|
||||||
|
Studies: studies,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the patient (or confirm it exists) using the RegisterService
|
||||||
|
user, err := h.registerService.Register(regRequest)
|
||||||
|
if err != nil {
|
||||||
|
// If the error is about duplicate email, we'll continue with generating a shortlink
|
||||||
|
// Otherwise, return the error
|
||||||
|
if err != service.ErrEmailExists {
|
||||||
|
h.logger.Error("Failed to register patient", zap.Error(err))
|
||||||
|
http.Error(w, fmt.Sprintf("Failed to register patient: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.logger.Info("Patient already exists, continuing with shortlink generation",
|
||||||
|
zap.String("email", reqData.Email),
|
||||||
|
zap.String("patientID", reqData.Patient.PatientID))
|
||||||
|
} else {
|
||||||
|
h.logger.Info("Patient registered successfully",
|
||||||
|
zap.String("userID", user.ID),
|
||||||
|
zap.String("email", reqData.Email),
|
||||||
|
zap.String("patientID", reqData.Patient.PatientID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each study, generate a shortlink
|
||||||
|
// For simplicity, we'll just use the first study in the array
|
||||||
|
study := reqData.Studies[0]
|
||||||
|
|
||||||
|
// Create a shortlink request
|
||||||
|
shortLinkReq := &models.GenerateShortLinkRequest{
|
||||||
|
PatientID: reqData.Patient.PatientID,
|
||||||
|
StudyUID: study.StudyInstanceUID,
|
||||||
|
DOB: reqData.Patient.DateOfBirth,
|
||||||
|
ExpiresIn: 0, // Set to 0 to use the default expiry from config
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a shortlink
|
||||||
|
// We set empty string as creatorID because you mentioned ShortlinkCreate_UserID is now nullable
|
||||||
|
shortLinkResp, err := h.shortLinkService.GenerateShortLink(shortLinkReq, "")
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("Failed to generate shortlink",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.String("patientID", reqData.Patient.PatientID),
|
||||||
|
zap.String("studyUID", study.StudyInstanceUID))
|
||||||
|
http.Error(w, fmt.Sprintf("Failed to generate shortlink: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log successful shortlink generation
|
||||||
|
h.logger.Info("Shortlink generated for uploaded DICOM",
|
||||||
|
zap.String("patientID", reqData.Patient.PatientID),
|
||||||
|
zap.String("studyUID", study.StudyInstanceUID),
|
||||||
|
zap.String("shortToken", shortLinkResp.ShortToken))
|
||||||
|
|
||||||
|
// Return the shortlink response
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
json.NewEncoder(w).Encode(shortLinkResp)
|
||||||
|
}
|
||||||
32
internal/api/middleware/pydicom.go
Normal file
32
internal/api/middleware/pydicom.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PydicomAPIKey validates requests to pydicom endpoints by checking the API key
|
||||||
|
func PydicomAPIKey(apiKey string, 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 API key header is present
|
||||||
|
providedKey := r.Header.Get("X-PYDICOM-API-KEY")
|
||||||
|
if providedKey == "" {
|
||||||
|
logger.Warn("API key missing from PYDICOM request")
|
||||||
|
http.Error(w, "API key required", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the API key
|
||||||
|
if providedKey != apiKey {
|
||||||
|
logger.Warn("Invalid API key for PYDICOM request")
|
||||||
|
http.Error(w, "Invalid API key", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// API key is valid, proceed to the next handler
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,16 +13,16 @@ import (
|
|||||||
|
|
||||||
// DBShortLink represents a shortlink from the database
|
// DBShortLink represents a shortlink from the database
|
||||||
type DBShortLink struct {
|
type DBShortLink struct {
|
||||||
ShortlinkID int `db:"ShortlinkID"`
|
ShortlinkID int `db:"ShortlinkID"`
|
||||||
ShortlinkCode string `db:"ShortlinkCode"`
|
ShortlinkCode string `db:"ShortlinkCode"`
|
||||||
Shortlink_PatientID string `db:"Shortlink_PatientID"`
|
Shortlink_PatientID string `db:"Shortlink_PatientID"`
|
||||||
Shortlink_Study_IUID string `db:"Shortlink_Study_IUID"`
|
Shortlink_Study_IUID string `db:"Shortlink_Study_IUID"`
|
||||||
ShortlinkHashDoB string `db:"ShortlinkHashDoB"`
|
ShortlinkHashDoB string `db:"ShortlinkHashDoB"`
|
||||||
ShortlinkExpiredAt time.Time `db:"ShortlinkExpiredAt"`
|
ShortlinkExpiredAt time.Time `db:"ShortlinkExpiredAt"`
|
||||||
ShortlinkIsRevoked bool `db:"ShortlinkIsRevoked"`
|
ShortlinkIsRevoked bool `db:"ShortlinkIsRevoked"`
|
||||||
ShortlinkRemainingTries int `db:"ShortlinkRemainingTries"`
|
ShortlinkRemainingTries int `db:"ShortlinkRemainingTries"`
|
||||||
ShortlinkCreatedAt time.Time `db:"ShortlinkCreatedAt"`
|
ShortlinkCreatedAt time.Time `db:"ShortlinkCreatedAt"`
|
||||||
ShortlinkCreate_UserID int `db:"ShortlinkCreate_UserID"`
|
ShortlinkCreate_UserID sql.NullInt64 `db:"ShortlinkCreate_UserID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShortLinkRepository handles database operations related to shortlinks
|
// ShortLinkRepository handles database operations related to shortlinks
|
||||||
@@ -39,6 +39,13 @@ func NewShortLinkRepository() *ShortLinkRepository {
|
|||||||
|
|
||||||
// ToShortLink converts a DBShortLink to a ShortLink model
|
// ToShortLink converts a DBShortLink to a ShortLink model
|
||||||
func (s *DBShortLink) ToShortLink() *models.ShortLink {
|
func (s *DBShortLink) ToShortLink() *models.ShortLink {
|
||||||
|
var createdByID string
|
||||||
|
if s.ShortlinkCreate_UserID.Valid {
|
||||||
|
createdByID = fmt.Sprintf("%d", s.ShortlinkCreate_UserID.Int64)
|
||||||
|
} else {
|
||||||
|
createdByID = ""
|
||||||
|
}
|
||||||
|
|
||||||
return &models.ShortLink{
|
return &models.ShortLink{
|
||||||
ID: fmt.Sprintf("%d", s.ShortlinkID),
|
ID: fmt.Sprintf("%d", s.ShortlinkID),
|
||||||
Token: s.ShortlinkCode,
|
Token: s.ShortlinkCode,
|
||||||
@@ -49,7 +56,7 @@ func (s *DBShortLink) ToShortLink() *models.ShortLink {
|
|||||||
IsRevoked: s.ShortlinkIsRevoked,
|
IsRevoked: s.ShortlinkIsRevoked,
|
||||||
RemainingTries: s.ShortlinkRemainingTries,
|
RemainingTries: s.ShortlinkRemainingTries,
|
||||||
CreatedAt: s.ShortlinkCreatedAt.Format(time.RFC3339),
|
CreatedAt: s.ShortlinkCreatedAt.Format(time.RFC3339),
|
||||||
CreatedByID: fmt.Sprintf("%d", s.ShortlinkCreate_UserID),
|
CreatedByID: createdByID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,9 +91,19 @@ func (r *ShortLinkRepository) CreateShortLinkTx(tx *sqlx.Tx, shortLink *models.S
|
|||||||
ShortlinkCreate_UserID)
|
ShortlinkCreate_UserID)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), ?)`
|
VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), ?)`
|
||||||
|
|
||||||
createdByID, err := strconv.Atoi(shortLink.CreatedByID)
|
var createdByID sql.NullInt64
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid created by ID: %w", err)
|
// Handle empty CreatedByID
|
||||||
|
if shortLink.CreatedByID != "" {
|
||||||
|
id, err := strconv.Atoi(shortLink.CreatedByID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid created by ID: %w", err)
|
||||||
|
}
|
||||||
|
createdByID.Int64 = int64(id)
|
||||||
|
createdByID.Valid = true
|
||||||
|
} else {
|
||||||
|
// If CreatedByID is empty, insert NULL
|
||||||
|
createdByID.Valid = false
|
||||||
}
|
}
|
||||||
|
|
||||||
expiresAt, err := time.Parse(time.RFC3339, shortLink.ExpiresAt)
|
expiresAt, err := time.Parse(time.RFC3339, shortLink.ExpiresAt)
|
||||||
|
|||||||
@@ -141,5 +141,26 @@ func SetupRouter(cfg *config.Config, logger *zap.Logger) http.Handler {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// PYDICOM Uploader Service endpoint
|
||||||
|
// This endpoint is protected by API key middleware
|
||||||
|
r.Group(func(r chi.Router) {
|
||||||
|
// Apply PYDICOM API key middleware
|
||||||
|
r.Use(apiMiddleware.PydicomAPIKey(cfg.Auth.PydicomApiKey, logger))
|
||||||
|
|
||||||
|
// Create handler for PYDICOM uploads
|
||||||
|
registerService := service.NewRegisterService(logger)
|
||||||
|
shortLinkService := service.NewShortLinkService(
|
||||||
|
jwtManager,
|
||||||
|
logger,
|
||||||
|
cfg.Shortlink.BaseURL,
|
||||||
|
cfg.Shortlink.DefaultExpiryHours,
|
||||||
|
cfg.Shortlink.MaxAttempts,
|
||||||
|
)
|
||||||
|
pydicomHandler := handlers.NewPydicomHandler(logger, shortLinkService, registerService)
|
||||||
|
|
||||||
|
// Add route for uploaded DICOM
|
||||||
|
r.Post("/uploaded_dicom", pydicomHandler.HandleUploadedDicom)
|
||||||
|
})
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|||||||
38
test/http/pydicom-upload.http
Normal file
38
test/http/pydicom-upload.http
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# pydicom-upload.http
|
||||||
|
# This file can be used with REST Client extension in VS Code to test the PYDICOM upload endpoint
|
||||||
|
|
||||||
|
@baseUrl = http://localhost:5555
|
||||||
|
@pydicomApiKey=2f0ff447b2c3aeef2004e83a750ded97e29ba8c0ccc70053d5e26f5d715e42ff
|
||||||
|
|
||||||
|
### Test the PYDICOM upload endpoint
|
||||||
|
POST {{baseUrl}}/uploaded_dicom
|
||||||
|
X-PYDICOM-API-KEY: {{pydicomApiKey}}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "patient2@example.com",
|
||||||
|
"password": "password123",
|
||||||
|
"name": "Bobon Santoso",
|
||||||
|
"role": "patient",
|
||||||
|
"patient": {
|
||||||
|
"patient_id": "MR00000359",
|
||||||
|
"patient_name": "Bobon Santoso",
|
||||||
|
"date_of_birth": "1985-01-01"
|
||||||
|
},
|
||||||
|
"studies": [
|
||||||
|
{
|
||||||
|
"study_instance_uid": "1.2.826.0.1.3680043.9.7307.1.202503196393.01",
|
||||||
|
"accession_number": "CR.250319.6393.01",
|
||||||
|
"study_date": "2025-03-19",
|
||||||
|
"study_description": "MRI Scan"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
### Expected Response:
|
||||||
|
# {
|
||||||
|
# "short_token": "LDYZX",
|
||||||
|
# "full_url": "http://localhost:3333/short-auth?short=LDYZX",
|
||||||
|
# "expires_at": "2025-05-18T02:04:46Z",
|
||||||
|
# "is_existing": true
|
||||||
|
# }
|
||||||
Reference in New Issue
Block a user