add: implement shortlink
This commit is contained in:
@@ -33,6 +33,12 @@ type Config struct {
|
||||
EnableDatabaseAuth bool `mapstructure:"enable_database_auth"`
|
||||
} `mapstructure:"auth"`
|
||||
|
||||
Shortlink struct {
|
||||
BaseURL string `mapstructure:"base_url"` // Base URL for shortlinks (e.g., https://example.com)
|
||||
DefaultExpiryHours int `mapstructure:"default_expiry_hours"` // Default expiry time in hours
|
||||
MaxAttempts int `mapstructure:"max_attempts"` // Maximum failed attempts allowed
|
||||
} `mapstructure:"shortlink"`
|
||||
|
||||
Database struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
|
||||
@@ -19,6 +19,11 @@ auth:
|
||||
refresh_token_expiry: 168 # hours (7 days)
|
||||
enable_database_auth: false # Set to true when ready to use database
|
||||
|
||||
shortlink:
|
||||
base_url: "http://localhost:3333" # The base URL for generated OHIF Auth shortlinks
|
||||
default_expiry_hours: 24 # Default expiry time for shortlinks (1 day)
|
||||
max_attempts: 5 # Maximum number of failed login attempts
|
||||
|
||||
database:
|
||||
host: "localhost"
|
||||
port: 3306
|
||||
|
||||
129
internal/api/handlers/shortlink.go
Normal file
129
internal/api/handlers/shortlink.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/middleware"
|
||||
"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"
|
||||
)
|
||||
|
||||
// ShortLinkHandler handles shortlink operations
|
||||
type ShortLinkHandler struct {
|
||||
logger *zap.Logger
|
||||
shortLinkService *service.ShortLinkService
|
||||
}
|
||||
|
||||
// NewShortLinkHandler creates a new shortlink handler
|
||||
func NewShortLinkHandler(logger *zap.Logger, shortLinkService *service.ShortLinkService) *ShortLinkHandler {
|
||||
return &ShortLinkHandler{
|
||||
logger: logger,
|
||||
shortLinkService: shortLinkService,
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateShortLink handles shortlink generation requests
|
||||
func (h *ShortLinkHandler) GenerateShortLink(w http.ResponseWriter, r *http.Request) {
|
||||
// Only allow admin or expertise_doctor roles to generate shortlinks
|
||||
userRole, ok := r.Context().Value(middleware.UserRoleKey).(string)
|
||||
if !ok || (userRole != "admin" && userRole != "expertise_doctor") {
|
||||
h.logger.Warn("Unauthorized attempt to generate shortlink",
|
||||
zap.String("role", userRole))
|
||||
http.Error(w, "Only admin or expertise doctor can generate short links", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
var req models.GenerateShortLinkRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("Failed to parse shortlink generation request", zap.Error(err))
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Get user ID from context
|
||||
userID, ok := r.Context().Value(middleware.UserIDKey).(string)
|
||||
if !ok {
|
||||
h.logger.Error("User ID not found in context")
|
||||
http.Error(w, "User context not found", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate shortlink using configured baseURL from service
|
||||
response, err := h.shortLinkService.GenerateShortLink(&req, userID)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to generate shortlink",
|
||||
zap.Error(err),
|
||||
zap.String("patientID", req.PatientID),
|
||||
zap.String("studyUID", req.StudyUID))
|
||||
|
||||
statusCode := http.StatusInternalServerError
|
||||
message := "Failed to generate shortlink"
|
||||
|
||||
if err == service.ErrInvalidStudyUID {
|
||||
statusCode = http.StatusBadRequest
|
||||
message = "Invalid StudyInstanceUID"
|
||||
}
|
||||
|
||||
http.Error(w, message, statusCode)
|
||||
return
|
||||
}
|
||||
|
||||
// Log successful shortlink generation
|
||||
h.logger.Info("Shortlink generated successfully",
|
||||
zap.String("token", response.ShortToken),
|
||||
zap.String("patientID", req.PatientID),
|
||||
zap.String("studyUID", req.StudyUID),
|
||||
zap.String("createdBy", userID))
|
||||
|
||||
// Return response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// ShortLinkAuth handles authentication requests using shortlinks
|
||||
func (h *ShortLinkHandler) ShortLinkAuth(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse request body
|
||||
var req models.ShortLinkAuthRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("Failed to parse shortlink auth request", zap.Error(err))
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate and authenticate
|
||||
response, err := h.shortLinkService.AuthenticateWithShortLink(&req)
|
||||
if err != nil {
|
||||
h.logger.Warn("Shortlink authentication failed",
|
||||
zap.Error(err),
|
||||
zap.String("token", req.ShortToken))
|
||||
|
||||
statusCode := http.StatusUnauthorized
|
||||
message := "Authentication failed"
|
||||
|
||||
switch err {
|
||||
case service.ErrShortLinkNotFound, service.ErrShortLinkExpired:
|
||||
message = "Short link not found or expired"
|
||||
case service.ErrInvalidDOB:
|
||||
message = "Invalid date of birth"
|
||||
case service.ErrTooManyAttempts:
|
||||
message = "Too many failed attempts"
|
||||
}
|
||||
|
||||
http.Error(w, message, statusCode)
|
||||
return
|
||||
}
|
||||
|
||||
// Log successful authentication
|
||||
h.logger.Info("Shortlink authentication successful",
|
||||
zap.String("token", req.ShortToken))
|
||||
|
||||
// Return response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
@@ -59,3 +59,54 @@ type RefreshRequest struct {
|
||||
type RefreshResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
|
||||
// ShortLink represents a short URL token for patient access
|
||||
type ShortLink struct {
|
||||
ID string `db:"id" json:"id"`
|
||||
Token string `db:"token" json:"token"` // The short token used in the URL
|
||||
PatientID string `db:"patient_id" json:"patient_id"`
|
||||
StudyUID string `db:"study_uid" json:"study_uid"` // The StudyInstanceUID this token grants access to
|
||||
HashedDOB string `db:"hashed_dob" json:"-"` // Hashed Date of Birth for verification
|
||||
ExpiresAt string `db:"expires_at" json:"expires_at"`
|
||||
IsRevoked bool `db:"is_revoked" json:"is_revoked"`
|
||||
CreatedAt string `db:"created_at" json:"created_at"`
|
||||
CreatedByID string `db:"created_by_id" json:"created_by_id"` // ID of admin who created this
|
||||
RemainingTries int `db:"remaining_tries" json:"-"` // Number of failed attempts allowed
|
||||
}
|
||||
|
||||
// GenerateShortLinkRequest represents request to create a short URL
|
||||
type GenerateShortLinkRequest struct {
|
||||
PatientID string `json:"patient_id"`
|
||||
StudyUID string `json:"study_uid"`
|
||||
DOB string `json:"dob"` // Date of birth in YYYY-MM-DD format
|
||||
ExpiresIn int `json:"expires_in"` // Expiry in hours (optional, defaults to 72)
|
||||
}
|
||||
|
||||
// GenerateShortLinkResponse is the response for a generated short link
|
||||
type GenerateShortLinkResponse struct {
|
||||
ShortToken string `json:"short_token"`
|
||||
FullURL string `json:"full_url"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
}
|
||||
|
||||
// ShortLinkAuthRequest represents the shortlink authentication request
|
||||
type ShortLinkAuthRequest struct {
|
||||
ShortToken string `json:"short_token,omitempty"` // The original field
|
||||
ShortTokenAlt string `json:"shortToken,omitempty"` // Support for camelCase naming from OHIF
|
||||
DOB string `json:"dob"` // Date of birth in YYYY-MM-DD format
|
||||
}
|
||||
|
||||
func (r *ShortLinkAuthRequest) GetToken() string {
|
||||
// Use ShortTokenAlt if ShortToken is empty
|
||||
if r.ShortToken == "" {
|
||||
return r.ShortTokenAlt
|
||||
}
|
||||
return r.ShortToken
|
||||
}
|
||||
|
||||
// ShortLinkAuthResponse is the response for successful shortlink authentication
|
||||
type ShortLinkAuthResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int `json:"expires_in"` // Token expiry in seconds
|
||||
RedirectURL string `json:"redirect_url"`
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ func SetupRouter(cfg *config.Config, logger *zap.Logger) http.Handler {
|
||||
// Initialize JWT auth service
|
||||
jwtSecret := cfg.Auth.JWTSecret
|
||||
if jwtSecret == "" {
|
||||
jwtSecret = "vQ6PQqUyh7pBNOytClgN+Nw1XBq7F8Qo6VP3VwIqvHY=" // Default from your example, should be set in config
|
||||
logger.Warn("JWT secret not provided in config")
|
||||
}
|
||||
|
||||
// Convert config values to time.Duration
|
||||
@@ -66,6 +66,15 @@ func SetupRouter(cfg *config.Config, logger *zap.Logger) http.Handler {
|
||||
jwtManager := auth.NewJWTManager(jwtSecret, accessExpiry, refreshExpiry)
|
||||
authService := service.NewAuthService(jwtManager)
|
||||
|
||||
// Initialize shortlink service with config values
|
||||
shortLinkService := service.NewShortLinkService(
|
||||
jwtManager,
|
||||
logger,
|
||||
cfg.Shortlink.BaseURL,
|
||||
cfg.Shortlink.DefaultExpiryHours,
|
||||
cfg.Shortlink.MaxAttempts,
|
||||
)
|
||||
|
||||
// Public routes that don't require authentication
|
||||
r.Group(func(r chi.Router) {
|
||||
// Health check
|
||||
@@ -77,6 +86,10 @@ func SetupRouter(cfg *config.Config, logger *zap.Logger) http.Handler {
|
||||
r.Post("/login", authHandler.Login)
|
||||
r.Post("/refresh", authHandler.RefreshToken)
|
||||
r.Post("/logout", authHandler.Logout)
|
||||
|
||||
// ShortLink authentication - no auth required
|
||||
shortLinkHandler := handlers.NewShortLinkHandler(logger, shortLinkService)
|
||||
r.Post("/shortlink", shortLinkHandler.ShortLinkAuth)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -85,6 +98,10 @@ func SetupRouter(cfg *config.Config, logger *zap.Logger) http.Handler {
|
||||
// Apply authentication middleware
|
||||
r.Use(apiMiddleware.Auth(authService, logger))
|
||||
|
||||
// Shortlink generation - only for admin and expertise_doctor roles
|
||||
shortLinkHandler := handlers.NewShortLinkHandler(logger, shortLinkService)
|
||||
r.Post("/generate-link", shortLinkHandler.GenerateShortLink)
|
||||
|
||||
// DICOM Web routes
|
||||
r.Route("/dicomWeb", func(r chi.Router) {
|
||||
// Add audit logging middleware to DICOM routes
|
||||
|
||||
296
internal/api/service/shortlink_service.go
Normal file
296
internal/api/service/shortlink_service.go
Normal file
@@ -0,0 +1,296 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models"
|
||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/auth"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultShortLinkExpiry is the default expiration time for short links (72 hours)
|
||||
DefaultShortLinkExpiry = 72 * time.Hour
|
||||
|
||||
// DefaultMaxTries is the default number of login attempts allowed for a shortlink
|
||||
DefaultMaxTries = 5
|
||||
|
||||
// ShortTokenLength is the length of the generated short token
|
||||
ShortTokenLength = 8
|
||||
)
|
||||
|
||||
var (
|
||||
ErrShortLinkNotFound = errors.New("short link not found or expired")
|
||||
ErrInvalidDOB = errors.New("invalid date of birth")
|
||||
ErrShortLinkExpired = errors.New("short link has expired")
|
||||
ErrTooManyAttempts = errors.New("too many failed attempts")
|
||||
ErrCreationFailed = errors.New("failed to create short link")
|
||||
ErrInvalidStudyUID = errors.New("invalid or missing StudyInstanceUID")
|
||||
ErrAdminRoleRequired = errors.New("admin role required to generate shortlinks")
|
||||
)
|
||||
|
||||
// ShortLinkService handles operations related to short links
|
||||
type ShortLinkService struct {
|
||||
jwtManager *auth.JWTManager
|
||||
logger *zap.Logger
|
||||
// Configuration settings
|
||||
baseURL string
|
||||
defaultExpiryTime time.Duration
|
||||
maxAttempts int
|
||||
// Mock in-memory storage for now, would use a database in production
|
||||
shortLinks map[string]*models.ShortLink
|
||||
}
|
||||
|
||||
// NewShortLinkService creates a new short link service
|
||||
func NewShortLinkService(jwtManager *auth.JWTManager, logger *zap.Logger, baseURL string, defaultExpiryHours int, maxAttempts int) *ShortLinkService {
|
||||
// Set default values if not provided
|
||||
if baseURL == "" {
|
||||
baseURL = "http://localhost:3000"
|
||||
}
|
||||
|
||||
if defaultExpiryHours <= 0 {
|
||||
defaultExpiryHours = 72 // Default to 72 hours if not specified
|
||||
}
|
||||
|
||||
if maxAttempts <= 0 {
|
||||
maxAttempts = 5 // Default to 5 attempts if not specified
|
||||
}
|
||||
|
||||
return &ShortLinkService{
|
||||
jwtManager: jwtManager,
|
||||
logger: logger,
|
||||
baseURL: baseURL,
|
||||
defaultExpiryTime: time.Duration(defaultExpiryHours) * time.Hour,
|
||||
maxAttempts: maxAttempts,
|
||||
shortLinks: make(map[string]*models.ShortLink),
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateShortLink creates a new short link for patient and study access
|
||||
func (s *ShortLinkService) GenerateShortLink(req *models.GenerateShortLinkRequest, creatorID string) (*models.GenerateShortLinkResponse, error) {
|
||||
// Validate inputs
|
||||
if req.PatientID == "" {
|
||||
return nil, errors.New("patient ID is required")
|
||||
}
|
||||
if req.StudyUID == "" {
|
||||
return nil, ErrInvalidStudyUID
|
||||
}
|
||||
if req.DOB == "" {
|
||||
return nil, errors.New("date of birth is required")
|
||||
}
|
||||
|
||||
// Normalize DOB format (ensure YYYY-MM-DD)
|
||||
dob := normalizeDOB(req.DOB)
|
||||
if !isValidDOBFormat(dob) {
|
||||
return nil, errors.New("invalid date of birth format, expected YYYY-MM-DD")
|
||||
}
|
||||
|
||||
// Set expiration if not provided
|
||||
expiresIn := s.defaultExpiryTime
|
||||
if req.ExpiresIn > 0 {
|
||||
expiresIn = time.Duration(req.ExpiresIn) * time.Hour
|
||||
}
|
||||
expiresAt := time.Now().Add(expiresIn)
|
||||
|
||||
// Generate a secure random token
|
||||
token, err := generateSecureToken(ShortTokenLength)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to generate secure token", zap.Error(err))
|
||||
return nil, ErrCreationFailed
|
||||
}
|
||||
|
||||
// Hash the DOB for secure storage
|
||||
hashedDOB := hashDOB(dob)
|
||||
|
||||
// Create the short link record
|
||||
shortLink := &models.ShortLink{
|
||||
ID: generateID(),
|
||||
Token: token,
|
||||
PatientID: req.PatientID,
|
||||
StudyUID: req.StudyUID,
|
||||
HashedDOB: hashedDOB,
|
||||
ExpiresAt: expiresAt.Format(time.RFC3339),
|
||||
IsRevoked: false,
|
||||
CreatedAt: time.Now().Format(time.RFC3339),
|
||||
CreatedByID: creatorID,
|
||||
RemainingTries: s.maxAttempts,
|
||||
}
|
||||
|
||||
// Store the short link (in production, this would be in a database)
|
||||
s.shortLinks[token] = shortLink
|
||||
|
||||
// Generate the full URL using the configured base URL
|
||||
fullURL := fmt.Sprintf("%s/short-auth?short=%s", s.baseURL, token)
|
||||
|
||||
return &models.GenerateShortLinkResponse{
|
||||
ShortToken: token,
|
||||
FullURL: fullURL,
|
||||
ExpiresAt: shortLink.ExpiresAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ValidateShortLink validates a short link token and DOB
|
||||
func (s *ShortLinkService) ValidateShortLink(req *models.ShortLinkAuthRequest) (*models.ShortLink, error) {
|
||||
// Get the token using the helper method that handles both field names
|
||||
token := req.GetToken()
|
||||
|
||||
// Find the short link
|
||||
shortLink, exists := s.shortLinks[token]
|
||||
if !exists {
|
||||
return nil, ErrShortLinkNotFound
|
||||
}
|
||||
|
||||
// Check if expired
|
||||
expiresAt, err := time.Parse(time.RFC3339, shortLink.ExpiresAt)
|
||||
if err != nil || time.Now().After(expiresAt) {
|
||||
return nil, ErrShortLinkExpired
|
||||
}
|
||||
|
||||
// Check if revoked
|
||||
if shortLink.IsRevoked {
|
||||
return nil, ErrShortLinkNotFound
|
||||
}
|
||||
|
||||
// Check remaining tries
|
||||
if shortLink.RemainingTries <= 0 {
|
||||
return nil, ErrTooManyAttempts
|
||||
}
|
||||
|
||||
// Normalize and hash the provided DOB
|
||||
dob := normalizeDOB(req.DOB)
|
||||
if !isValidDOBFormat(dob) {
|
||||
// Decrement remaining tries on invalid format
|
||||
shortLink.RemainingTries--
|
||||
return nil, errors.New("invalid date of birth format, expected YYYY-MM-DD")
|
||||
}
|
||||
|
||||
hashedDOB := hashDOB(dob)
|
||||
|
||||
// Verify the DOB
|
||||
if hashedDOB != shortLink.HashedDOB {
|
||||
// Decrement remaining tries on failed verification
|
||||
shortLink.RemainingTries--
|
||||
return nil, ErrInvalidDOB
|
||||
}
|
||||
|
||||
// DOB verified, reset tries count as successful login
|
||||
shortLink.RemainingTries = s.maxAttempts
|
||||
|
||||
return shortLink, nil
|
||||
}
|
||||
|
||||
// AuthenticateWithShortLink authenticates a user using a short link and DOB
|
||||
func (s *ShortLinkService) AuthenticateWithShortLink(req *models.ShortLinkAuthRequest) (*models.ShortLinkAuthResponse, error) {
|
||||
// Validate the short link
|
||||
shortLink, err := s.ValidateShortLink(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Determine patient name (could be fetched from a database)
|
||||
patientName := "Patient" // Placeholder, in production get real name
|
||||
|
||||
// Create additional claims for the JWT
|
||||
additionalClaims := make(map[string]interface{})
|
||||
additionalClaims["patient_id"] = shortLink.PatientID
|
||||
additionalClaims["patient_name"] = patientName
|
||||
additionalClaims["study_iuids"] = []string{shortLink.StudyUID}
|
||||
additionalClaims["home_url"] = fmt.Sprintf("viewer?StudyInstanceUIDs=%s", shortLink.StudyUID)
|
||||
additionalClaims["study_list"] = "disabled"
|
||||
|
||||
// Generate JWT
|
||||
// Using a virtual "user" for the patient with a patient role
|
||||
userID := fmt.Sprintf("shortlink_%s", shortLink.ID)
|
||||
email := fmt.Sprintf("patient_%s@shortlink.local", shortLink.ID) // Virtual email for JWT
|
||||
role := "patient" // Always patient role for shortlinks
|
||||
|
||||
// Generate access token (24-hour validity for patient access)
|
||||
accessToken, err := s.jwtManager.GenerateAccessToken(userID, email, role, patientName, additionalClaims)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to generate JWT for shortlink auth", zap.Error(err))
|
||||
return nil, errors.New("authentication error")
|
||||
}
|
||||
|
||||
// Create response
|
||||
redirectURL := fmt.Sprintf("/viewer?StudyInstanceUIDs=%s", shortLink.StudyUID)
|
||||
|
||||
return &models.ShortLinkAuthResponse{
|
||||
AccessToken: accessToken,
|
||||
ExpiresIn: int(s.jwtManager.GetAccessExpiry().Seconds()),
|
||||
RedirectURL: redirectURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
// hashDOB creates a secure hash of a date of birth
|
||||
func hashDOB(dob string) string {
|
||||
hash := sha256.Sum256([]byte(dob))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// generateSecureToken generates a secure random token for short links
|
||||
func generateSecureToken(length int) (string, error) {
|
||||
b := make([]byte, length)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(b)[:length], nil
|
||||
}
|
||||
|
||||
// generateID generates a unique ID for a shortlink
|
||||
func generateID() string {
|
||||
b := make([]byte, 6)
|
||||
rand.Read(b)
|
||||
return fmt.Sprintf("sl_%s", hex.EncodeToString(b))
|
||||
}
|
||||
|
||||
// normalizeDOB normalizes date of birth to YYYY-MM-DD format
|
||||
func normalizeDOB(dob string) string {
|
||||
// Remove any non-alphanumeric characters except dash
|
||||
dob = strings.Map(func(r rune) rune {
|
||||
if (r >= '0' && r <= '9') || r == '-' {
|
||||
return r
|
||||
}
|
||||
return -1
|
||||
}, dob)
|
||||
|
||||
return dob
|
||||
}
|
||||
|
||||
// isValidDOBFormat checks if the DOB is in YYYY-MM-DD format
|
||||
func isValidDOBFormat(dob string) bool {
|
||||
// Check basic format
|
||||
if len(dob) != 10 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check dashes
|
||||
if dob[4] != '-' || dob[7] != '-' {
|
||||
return false
|
||||
}
|
||||
|
||||
// Parse year, month, day
|
||||
yearStr := dob[0:4]
|
||||
monthStr := dob[5:7]
|
||||
dayStr := dob[8:10]
|
||||
|
||||
// Check if all are numeric
|
||||
for _, ch := range yearStr + monthStr + dayStr {
|
||||
if ch < '0' || ch > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Could add more validation here (leap years, month/day ranges)
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -170,3 +170,13 @@ func (m *JWTManager) ValidateToken(tokenString string) (*CustomClaims, error) {
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// GetAccessExpiry returns the configured access token expiry duration
|
||||
func (m *JWTManager) GetAccessExpiry() time.Duration {
|
||||
return m.accessExpiry
|
||||
}
|
||||
|
||||
// GetRefreshExpiry returns the configured refresh token expiry duration
|
||||
func (m *JWTManager) GetRefreshExpiry() time.Duration {
|
||||
return m.refreshExpiry
|
||||
}
|
||||
|
||||
50
test/http/shortlink-flow.http
Normal file
50
test/http/shortlink-flow.http
Normal file
@@ -0,0 +1,50 @@
|
||||
### Shortlink Authentication Test File
|
||||
@baseUrl = http://localhost:5555
|
||||
@adminToken = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMSIsImVtYWlsIjoiYWRtaW4iLCJyb2xlIjoiZXhwZXJ0aXNlX2RvY3RvciIsInVzZXJfbmFtZSI6IkFkbWluIFVzZXIiLCJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiaG9tZV91cmwiOiIvIiwic3R1ZHlfbGlzdCI6ImVuYWJsZWQiLCJleHAiOjE3NDcyMDY5NTAsImlhdCI6MTc0NzEyMDU1MH0.M3fhlKB8MX-NxGdEgnaA9-AhMXnXjUjRsWYOBXntJe4
|
||||
|
||||
### 0. Login as Admin and take the token
|
||||
POST http://localhost:5555/auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"email": "admin",
|
||||
"password": "admin"
|
||||
}
|
||||
|
||||
|
||||
### 1. Generate Short Link (Admin/Expertise Doctor Only)
|
||||
POST {{baseUrl}}/generate-link
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{adminToken}}
|
||||
|
||||
{
|
||||
"patient_id": "MR00000359",
|
||||
"study_uid": "1.2.826.0.1.3680043.9.7307.1.202503196393.01",
|
||||
"dob": "2001-10-07",
|
||||
"expires_in": 48
|
||||
}
|
||||
|
||||
### 2. Authenticate with Short Link and DOB
|
||||
# Use the short_token from the response of request #1
|
||||
POST {{baseUrl}}/auth/shortlink
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"short_token": "oP-tLWeu",
|
||||
"dob": "2001-10-07"
|
||||
}
|
||||
|
||||
### 3. Try authentication with incorrect DOB
|
||||
POST {{baseUrl}}/auth/shortlink
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"short_token": "erB5xT5S",
|
||||
"dob": "1985-04-16"
|
||||
}
|
||||
|
||||
### 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
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoic2hvcnRsaW5rX3NsXzgzZGNmNzQ1NGYwZiIsImVtYWlsIjoicGF0aWVudF9zbF84M2RjZjc0NTRmMGZAc2hvcnRsaW5rLmxvY2FsIiwicm9sZSI6InBhdGllbnQiLCJ1c2VyX25hbWUiOiJQYXRpZW50IiwidG9rZW5fdHlwZSI6ImFjY2VzcyIsInBhdGllbnRfaWQiOiJNUjAwMDAwMzU5IiwicGF0aWVudF9uYW1lIjoiUGF0aWVudCIsInN0dWR5X2l1aWRzIjpbIjEuMi44MjYuMC4xLjM2ODAwNDMuOS43MzA3LjEuMjAyNTAzMTk2MzkzLjAxIl0sImhvbWVfdXJsIjoidmlld2VyP1N0dWR5SW5zdGFuY2VVSURzPTEuMi44MjYuMC4xLjM2ODAwNDMuOS43MzA3LjEuMjAyNTAzMTk2MzkzLjAxIiwic3R1ZHlfbGlzdCI6ImRpc2FibGVkIiwiZXhwIjoxNzQ3MjA3MDI4LCJpYXQiOjE3NDcxMjA2Mjh9.RMGF9ParYAmOXbJqd0DP2kl0X6O0n8j_LI6FF9el4qM
|
||||
Reference in New Issue
Block a user