diff --git a/internal/api/models/shortlink.go b/internal/api/models/shortlink.go index adedc15..2acfb73 100644 --- a/internal/api/models/shortlink.go +++ b/internal/api/models/shortlink.go @@ -27,6 +27,7 @@ type GenerateShortLinkResponse struct { ShortToken string `json:"short_token"` FullURL string `json:"full_url"` ExpiresAt string `json:"expires_at"` + IsExisting bool `json:"is_existing"` // Indicates if this is an existing link that was reused } // ShortLinkAuthRequest represents the shortlink authentication request diff --git a/internal/api/repository/shortlink.go b/internal/api/repository/shortlink.go index fec5449..1799941 100644 --- a/internal/api/repository/shortlink.go +++ b/internal/api/repository/shortlink.go @@ -139,3 +139,28 @@ func (r *ShortLinkRepository) UpdateShortLink(shortLink *models.ShortLink) error return nil } + +// GetActiveShortLinkByPatientAndStudy retrieves an active (unexpired, not revoked) shortlink +// for the given patient ID and study UID +func (r *ShortLinkRepository) GetActiveShortLinkByPatientAndStudy(patientID string, studyUID string) (*models.ShortLink, error) { + var dbShortLink DBShortLink + + query := `SELECT * FROM shortlink + WHERE Shortlink_PatientID = ? + AND Shortlink_Study_IUID = ? + AND ShortlinkExpiredAt > NOW() + AND ShortlinkIsRevoked = FALSE + ORDER BY ShortlinkExpiredAt DESC + LIMIT 1` + + err := database.DB.Get(&dbShortLink, query, patientID, studyUID) + + if err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, fmt.Errorf("database error getting active shortlink: %w", err) + } + + return dbShortLink.ToShortLink(), nil +} diff --git a/internal/api/service/shortlink_service.go b/internal/api/service/shortlink_service.go index f16bafd..e8334ed 100644 --- a/internal/api/service/shortlink_service.go +++ b/internal/api/service/shortlink_service.go @@ -94,6 +94,31 @@ func (s *ShortLinkService) GenerateShortLink(req *models.GenerateShortLinkReques return nil, errors.New("invalid date of birth format, expected YYYY-MM-DD") } + // Check if an unexpired shortlink already exists for this patient and study + existingShortLink, err := s.shortLinkRepo.GetActiveShortLinkByPatientAndStudy(req.PatientID, req.StudyUID) + if err != nil { + s.logger.Error("Error checking for existing shortlinks", zap.Error(err)) + return nil, ErrCreationFailed + } + + // If an active shortlink exists, return it instead of creating a new one + if existingShortLink != nil { + s.logger.Info("Returning existing active shortlink", + zap.String("patientID", req.PatientID), + zap.String("studyUID", req.StudyUID), + zap.String("token", existingShortLink.Token)) + + // Generate the full URL using the configured base URL + fullURL := fmt.Sprintf("%s/short-auth?short=%s", s.baseURL, existingShortLink.Token) + + return &models.GenerateShortLinkResponse{ + ShortToken: existingShortLink.Token, + FullURL: fullURL, + ExpiresAt: existingShortLink.ExpiresAt, + IsExisting: true, + }, nil + } + // Set expiration if not provided expiresIn := s.defaultExpiryTime if req.ExpiresIn > 0 { @@ -139,6 +164,7 @@ func (s *ShortLinkService) GenerateShortLink(req *models.GenerateShortLinkReques ShortToken: token, FullURL: fullURL, ExpiresAt: shortLink.ExpiresAt, + IsExisting: false, }, nil } diff --git a/test/http/test.http b/test/http/dicom-proxy.http similarity index 93% rename from test/http/test.http rename to test/http/dicom-proxy.http index a2320ae..8eafa4b 100644 --- a/test/http/test.http +++ b/test/http/dicom-proxy.http @@ -10,23 +10,6 @@ GET {{baseUrl}}/health Accept: application/json -### Login Success -POST {{baseUrl}}/auth/login -Content-Type: application/json - -{ - "email": "patient1@example.com", - "password": "password123" -} - -### Refresh TOken -POST {{baseUrl}}/auth/refresh -Content-Type: application/json - -{ - "refresh_token": {{ refresh_token}} -} - ### 2. QIDO-RS: Search for Studies # Returns all studies (should return a list of DICOM studies if any exist) GET {{baseUrl}}/dicomWeb/studies diff --git a/test/http/ohif-flow.http b/test/http/ohif-flow.http index 155eb10..8471abd 100644 --- a/test/http/ohif-flow.http +++ b/test/http/ohif-flow.http @@ -1,4 +1,4 @@ -@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMiIsImVtYWlsIjoicGF0aWVudCIsInJvbGUiOiJwYXRpZW50IiwidXNlcl9uYW1lIjoiUGF0aWVudCBVc2VyIiwidG9rZW5fdHlwZSI6ImFjY2VzcyIsInBhdGllbnRfaWQiOiIwMDIxMTYyMiIsInBhdGllbnRfbmFtZSI6IkRJRElUIFNVWUFUTkFeUi4xMDA0OS4xOCIsImFjY2Vzc2lvbl9udW1iZXIiOiJDUi4xODA3MTMuMDM2Iiwic3R1ZHlfaXVpZCI6IjEuMi44MjYuMC4xLjM2ODAwNDMuOS43MzA3LjEuMjAxODA3MTMwMzYiLCJob21lX3VybCI6InZpZXdlcj9TdHVkeUluc3RhbmNlVUlEcz0xLjIuODI2LjAuMS4zNjgwMDQzLjkuNzMwNy4xLjIwMTgwNzEzMDM2Iiwic3R1ZHlfbGlzdCI6ImRpc2FibGVkIiwiZXhwIjoxNzQ2ODUyMDU2LCJpYXQiOjE3NDY3NjU2NTZ9.wGKCU77toQ5D27DRcNJE_XQjoJmxxzqoPbmf5N7D0cw +@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMSIsImVtYWlsIjoicGF0aWVudDFAZXhhbXBsZS5jb20iLCJyb2xlIjoicGF0aWVudCIsInVzZXJfbmFtZSI6IkRJRElUIFNVWUFUTkFeUi4xMDA0OS4xOCIsInRva2VuX3R5cGUiOiJhY2Nlc3MiLCJwYXRpZW50X2lkIjoiMDAyMTE2MjIiLCJwYXRpZW50X25hbWUiOiJESURJVCBTVVlBVE5BXlIuMTAwNDkuMTgiLCJzdHVkeV9pdWlkcyI6WyIxLjIuODI2LjAuMS4zNjgwMDQzLjkuNzMwNy4xLjIwMTgwNTMwMDY2IiwiMS4yLjgyNi4wLjEuMzY4MDA0My45LjczMDcuMS4yMDE4MDcxMzAzNiJdLCJhY2Nlc3Npb25fbnVtYmVycyI6WyJDUi4xODA1MzAuMDY2IiwiQ1IuMTgwNzEzLjAzNiJdLCJob21lX3VybCI6InZpZXdlcj9TdHVkeUluc3RhbmNlVUlEcz0xLjIuODI2LjAuMS4zNjgwMDQzLjkuNzMwNy4xLjIwMTgwNTMwMDY2Iiwic3R1ZHlfbGlzdCI6ImRpc2FibGVkIiwiZXhwIjoxNzQ3MzY0OTY1LCJpYXQiOjE3NDcyNzg1NjV9.I6L7nnT-xexPtkTIfIXetT41KGQlVPbCY8l_2fTgJdg @token_exp_doctor = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMSIsImVtYWlsIjoiYWRtaW4iLCJyb2xlIjoiZXhwZXJ0aXNlX2RvY3RvciIsInRva2VuX3R5cGUiOiJhY2Nlc3MiLCJleHAiOjE3NDY1MDQ1MTYsImlhdCI6MTc0NjQxODExNn0.vlDrns1oPFXHE5--TmWqwzvzxnfcCPcV2UW8_4GwDwE @baseUrl = http://localhost:5555 @@ -25,11 +25,11 @@ 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.20180713036 +GET {{baseUrl}}/dicomWeb/studies?limit=101&offset=0&fuzzymatching=true&includefield=00081030,00080060&StudyInstanceUID=1.2.826.0.1.3680043.9.7307.1.20180530066 Authorization: Bearer {{token}} ### Series List -GET {{baseUrl}}/dicomWeb/studies/1.2.826.0.1.3680043.9.7307.1.202503196393.01/series?includefield=00080021,00080031,0008103E,00200011 +GET {{baseUrl}}/dicomWeb/studies/1.2.826.0.1.3680043.9.7307.1.20180530066/series?includefield=00080021,00080031,0008103E,00200011 Authorization: Bearer {{token}} ### Semua Study dari Patient ID diff --git a/test/http/register-flow.http b/test/http/register-flow.http index f8025b6..93d0eb1 100644 --- a/test/http/register-flow.http +++ b/test/http/register-flow.http @@ -79,12 +79,12 @@ Content-Type: application/json { "email": "expert@example.com", - "password": "expert123", - "name": "Dr. Expert Johnson", + "password": "password123", + "name": "Dr. Expertise", "role": "expertise_doctor", "doctor": { "doctor_id": "EX456", - "doctor_name": "Dr. Expert Johnson" + "doctor_name": "Dr. Expertise" } } @@ -109,7 +109,7 @@ Content-Type: application/json { "email": "admin@example.com", - "password": "admin123", + "password": "password123", "name": "Admin User", "role": "admin" } @@ -148,4 +148,12 @@ Content-Type: application/json { "email": "doctor@example.com", "password": "password123" +} + +### Refresh TOken +POST {{baseUrl}}/auth/refresh +Content-Type: application/json + +{ + "refresh_token": {{ refresh_token}} } \ No newline at end of file diff --git a/test/http/shortlink-flow.http b/test/http/shortlink-flow.http index 76f976f..d03c7c8 100644 --- a/test/http/shortlink-flow.http +++ b/test/http/shortlink-flow.http @@ -1,18 +1,8 @@ ### Shortlink Authentication Test File @baseUrl = http://localhost:5555 -@adminToken = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMSIsImVtYWlsIjoiYWRtaW4iLCJyb2xlIjoiZXhwZXJ0aXNlX2RvY3RvciIsInVzZXJfbmFtZSI6IkFkbWluIFVzZXIiLCJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiaG9tZV91cmwiOiIvIiwic3R1ZHlfbGlzdCI6ImVuYWJsZWQiLCJleHAiOjE3NDcyMDY5NTAsImlhdCI6MTc0NzEyMDU1MH0.M3fhlKB8MX-NxGdEgnaA9-AhMXnXjUjRsWYOBXntJe4 +@adminToken = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNiIsImVtYWlsIjoiYWRtaW5AZXhhbXBsZS5jb20iLCJyb2xlIjoiYWRtaW4iLCJ1c2VyX25hbWUiOiJBZG1pbiBVc2VyIiwidG9rZW5fdHlwZSI6ImFjY2VzcyIsImhvbWVfdXJsIjoiLyIsInN0dWR5X2xpc3QiOiJlbmFibGVkIiwiZXhwIjoxNzQ3MzY0NjgxLCJpYXQiOjE3NDcyNzgyODF9.bkzxV8f26wN_r6uI5T6o58TNX4U0z-Wel1hCAl5-8ag -### 0. Login as Admin and take the token -POST http://localhost:5555/auth/login -Content-Type: application/json - -{ - "email": "patient1@example.com", - "password": "password123" -} - - -### 1. Generate Short Link (Admin/Expertise Doctor Only) +### 1. Generate Short Link Single Study POST {{baseUrl}}/generate-link Content-Type: application/json Authorization: Bearer {{adminToken}} @@ -24,27 +14,51 @@ Authorization: Bearer {{adminToken}} "expires_in": 48 } -### 2. Authenticate with Short Link and DOB -# Use the short_token from the response of request #1 +### 2. Generate Short Link Multiple Studies +POST {{baseUrl}}/generate-link +Content-Type: application/json +Authorization: Bearer {{adminToken}} + +{ + "patient_id": "00211622", + "study_uid": "1.2.826.0.1.3680043.9.7307.1.20180713036", + "dob": "1980-01-01", + "expires_in": 48 +} + +### +POST {{baseUrl}}/generate-link +Content-Type: application/json +Authorization: Bearer {{adminToken}} + +{ + "patient_id": "00211622", + "study_uid": "1.2.826.0.1.3680043.9.7307.1.20180530066", + "dob": "1980-01-01", + "expires_in": 48 +} + +### Authenticate correct POST {{baseUrl}}/auth/shortlink Content-Type: application/json { - "short_token": "oP-tLWeu", - "dob": "2001-10-07" + "short_token": "aePWCTrU", + "dob": "1980-01-01" } -### 3. Try authentication with incorrect DOB +### Authenticate with Incorrect DOB POST {{baseUrl}}/auth/shortlink Content-Type: application/json { - "short_token": "erB5xT5S", - "dob": "1985-04-16" + "short_token": "HNHh_zem", + "dob": "2001-10-00" } + ### 4. Access DICOM data with the JWT from successful shortlink auth # Replace with token from successful auth in request #2 -GET {{baseUrl}}/dicomWeb/studies/1.2.826.0.1.3680043.9.7307.1.202503196393.01 +GET {{baseUrl}}/dicomWeb/studies/1.2.826.0.1.3680043.9.7307.1.20180713036 Content-Type: application/json -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoic2hvcnRsaW5rX3NsXzgzZGNmNzQ1NGYwZiIsImVtYWlsIjoicGF0aWVudF9zbF84M2RjZjc0NTRmMGZAc2hvcnRsaW5rLmxvY2FsIiwicm9sZSI6InBhdGllbnQiLCJ1c2VyX25hbWUiOiJQYXRpZW50IiwidG9rZW5fdHlwZSI6ImFjY2VzcyIsInBhdGllbnRfaWQiOiJNUjAwMDAwMzU5IiwicGF0aWVudF9uYW1lIjoiUGF0aWVudCIsInN0dWR5X2l1aWRzIjpbIjEuMi44MjYuMC4xLjM2ODAwNDMuOS43MzA3LjEuMjAyNTAzMTk2MzkzLjAxIl0sImhvbWVfdXJsIjoidmlld2VyP1N0dWR5SW5zdGFuY2VVSURzPTEuMi44MjYuMC4xLjM2ODAwNDMuOS43MzA3LjEuMjAyNTAzMTk2MzkzLjAxIiwic3R1ZHlfbGlzdCI6ImRpc2FibGVkIiwiZXhwIjoxNzQ3MjA3MDI4LCJpYXQiOjE3NDcxMjA2Mjh9.RMGF9ParYAmOXbJqd0DP2kl0X6O0n8j_LI6FF9el4qM \ No newline at end of file +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoic2hvcnRsaW5rXzIiLCJlbWFpbCI6InBhdGllbnRfMkBzaG9ydGxpbmsubG9jYWwiLCJyb2xlIjoicGF0aWVudCIsInVzZXJfbmFtZSI6IlBhdGllbnQiLCJ0b2tlbl90eXBlIjoiYWNjZXNzIiwicGF0aWVudF9pZCI6IjAwMjExNjIyIiwicGF0aWVudF9uYW1lIjoiUGF0aWVudCIsInN0dWR5X2l1aWRzIjpbIjEuMi44MjYuMC4xLjM2ODAwNDMuOS43MzA3LjEuMjAxODA3MTMwMzYiXSwiaG9tZV91cmwiOiJ2aWV3ZXI_U3R1ZHlJbnN0YW5jZVVJRHM9MS4yLjgyNi4wLjEuMzY4MDA0My45LjczMDcuMS4yMDE4MDcxMzAzNiIsInN0dWR5X2xpc3QiOiJkaXNhYmxlZCIsImV4cCI6MTc0NzM2OTc1NSwiaWF0IjoxNzQ3MjgzMzU1fQ.rj1Xr7StW_O3rE2Pwq6L-WGBAW1tFcUq8bt5nu4u050 \ No newline at end of file