add: db tx commit and rollback implementation
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models"
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models"
|
||||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/database"
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/database"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DBDoctor represents a doctor from the database
|
// DBDoctor represents a doctor from the database
|
||||||
@@ -44,12 +45,12 @@ func (r *DoctorRepository) GetDoctorDetailsByUserID(userID string) (*DBDoctor, e
|
|||||||
return &dbDoctor, nil
|
return &dbDoctor, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateDoctor creates a new doctor record
|
// CreateDoctorTx creates a new doctor record within a transaction
|
||||||
func (r *DoctorRepository) CreateDoctor(doctorDetails *models.DoctorDetails, userID string) error {
|
func (r *DoctorRepository) CreateDoctorTx(tx *sqlx.Tx, doctorDetails *models.DoctorDetails, userID string) error {
|
||||||
query := `INSERT INTO doctor (Doctor_UsersID, DoctorID, DoctorName, DoctorCreatedAt, DoctorLastUpdatedAt)
|
query := `INSERT INTO doctor (Doctor_UsersID, DoctorID, DoctorName, DoctorCreatedAt, DoctorLastUpdatedAt)
|
||||||
VALUES (?, ?, ?, NOW(), NOW())`
|
VALUES (?, ?, ?, NOW(), NOW())`
|
||||||
|
|
||||||
_, err := database.DB.Exec(query, userID, doctorDetails.DoctorID, doctorDetails.DoctorName)
|
_, err := tx.Exec(query, userID, doctorDetails.DoctorID, doctorDetails.DoctorName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("database error creating doctor: %w", err)
|
return fmt.Errorf("database error creating doctor: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models"
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models"
|
||||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/database"
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/database"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DBPatient represents a patient from the database
|
// DBPatient represents a patient from the database
|
||||||
@@ -61,9 +62,9 @@ func (r *PatientRepository) GetPatientDetailsByUserID(userID string) (*models.Pa
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreatePatient creates a new patient record
|
// CreatePatientTx creates a new patient record within a transaction
|
||||||
func (r *PatientRepository) CreatePatient(patientRecord *models.PatientDetails, userID string) error {
|
func (r *PatientRepository) CreatePatientTx(tx *sqlx.Tx, patientRecord *models.PatientDetails, userID string) error {
|
||||||
// Parse DOB to time.Time
|
// Parse DOB to time.Time. 2006-1-02 = reference format YYYY-MM-DD in Go, not a default value
|
||||||
dob, err := time.Parse("2006-01-02", patientRecord.DateOfBirth)
|
dob, err := time.Parse("2006-01-02", patientRecord.DateOfBirth)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid date of birth format: %w", err)
|
return fmt.Errorf("invalid date of birth format: %w", err)
|
||||||
@@ -72,7 +73,7 @@ func (r *PatientRepository) CreatePatient(patientRecord *models.PatientDetails,
|
|||||||
query := `INSERT INTO patient (Patient_UsersID, PatientMedrec, PatientName, PatientDoB, PatientCreatedAt, PatientUpdatedAt)
|
query := `INSERT INTO patient (Patient_UsersID, PatientMedrec, PatientName, PatientDoB, PatientCreatedAt, PatientUpdatedAt)
|
||||||
VALUES (?, ?, ?, ?, NOW(), NOW())`
|
VALUES (?, ?, ?, ?, NOW(), NOW())`
|
||||||
|
|
||||||
_, err = database.DB.Exec(query, userID, patientRecord.PatientID, patientRecord.PatientName, dob)
|
_, err = tx.Exec(query, userID, patientRecord.PatientID, patientRecord.PatientName, dob)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("database error creating patient: %w", err)
|
return fmt.Errorf("database error creating patient: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models"
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models"
|
||||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/database"
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/database"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DBShortLink represents a shortlink from the database
|
// DBShortLink represents a shortlink from the database
|
||||||
@@ -69,8 +70,8 @@ func (r *ShortLinkRepository) GetShortLinkByToken(token string) (*models.ShortLi
|
|||||||
return dbShortLink.ToShortLink(), nil
|
return dbShortLink.ToShortLink(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateShortLink stores a new shortlink in the database
|
// CreateShortLinkTx stores a new shortlink in the database within a transaction
|
||||||
func (r *ShortLinkRepository) CreateShortLink(shortLink *models.ShortLink) error {
|
func (r *ShortLinkRepository) CreateShortLinkTx(tx *sqlx.Tx, shortLink *models.ShortLink) error {
|
||||||
query := `INSERT INTO shortlink (
|
query := `INSERT INTO shortlink (
|
||||||
ShortlinkCode,
|
ShortlinkCode,
|
||||||
Shortlink_PatientID,
|
Shortlink_PatientID,
|
||||||
@@ -93,7 +94,7 @@ func (r *ShortLinkRepository) CreateShortLink(shortLink *models.ShortLink) error
|
|||||||
return fmt.Errorf("invalid expiration date: %w", err)
|
return fmt.Errorf("invalid expiration date: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = database.DB.Exec(
|
_, err = tx.Exec(
|
||||||
query,
|
query,
|
||||||
shortLink.Token,
|
shortLink.Token,
|
||||||
shortLink.PatientID,
|
shortLink.PatientID,
|
||||||
@@ -112,8 +113,8 @@ func (r *ShortLinkRepository) CreateShortLink(shortLink *models.ShortLink) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateShortLink updates an existing shortlink in the database
|
// UpdateShortLinkTx updates an existing shortlink in the database within a transaction
|
||||||
func (r *ShortLinkRepository) UpdateShortLink(shortLink *models.ShortLink) error {
|
func (r *ShortLinkRepository) UpdateShortLinkTx(tx *sqlx.Tx, shortLink *models.ShortLink) error {
|
||||||
query := `UPDATE shortlink SET
|
query := `UPDATE shortlink SET
|
||||||
ShortlinkIsRevoked = ?,
|
ShortlinkIsRevoked = ?,
|
||||||
ShortlinkRemainingTries = ?,
|
ShortlinkRemainingTries = ?,
|
||||||
@@ -125,7 +126,7 @@ func (r *ShortLinkRepository) UpdateShortLink(shortLink *models.ShortLink) error
|
|||||||
return fmt.Errorf("invalid expiration date: %w", err)
|
return fmt.Errorf("invalid expiration date: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = database.DB.Exec(
|
_, err = tx.Exec(
|
||||||
query,
|
query,
|
||||||
shortLink.IsRevoked,
|
shortLink.IsRevoked,
|
||||||
shortLink.RemainingTries,
|
shortLink.RemainingTries,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models"
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models"
|
||||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/database"
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/database"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DBStudy represents a study from the database
|
// DBStudy represents a study from the database
|
||||||
@@ -69,8 +70,8 @@ func (r *StudyRepository) GetStudyByUID(studyUID string) (*DBStudy, error) {
|
|||||||
return &study, nil
|
return &study, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateStudy creates a new study record for a patient
|
// CreateStudyTx creates a new study record for a patient within a transaction
|
||||||
func (r *StudyRepository) CreateStudy(patientID string, study models.Study) error {
|
func (r *StudyRepository) CreateStudyTx(tx *sqlx.Tx, patientID string, study models.Study) error {
|
||||||
// Parse study date if provided
|
// Parse study date if provided
|
||||||
var studyDate *time.Time
|
var studyDate *time.Time
|
||||||
if study.StudyDate != "" {
|
if study.StudyDate != "" {
|
||||||
@@ -85,7 +86,7 @@ func (r *StudyRepository) CreateStudy(patientID string, study models.Study) erro
|
|||||||
(Study_PatientID, StudyIUID, StudyAccessionNumber, StudyDate, StudyCreatedAt, StudyUpdatedAt)
|
(Study_PatientID, StudyIUID, StudyAccessionNumber, StudyDate, StudyCreatedAt, StudyUpdatedAt)
|
||||||
VALUES (?, ?, ?, ?, NOW(), NOW())`
|
VALUES (?, ?, ?, ?, NOW(), NOW())`
|
||||||
|
|
||||||
_, err := database.DB.Exec(query,
|
_, err := tx.Exec(query,
|
||||||
patientID,
|
patientID,
|
||||||
study.StudyInstanceUID,
|
study.StudyInstanceUID,
|
||||||
study.AccessionNumber,
|
study.AccessionNumber,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models"
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models"
|
||||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/database"
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/database"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -140,8 +141,8 @@ func (r *UserRepository) RevokeRefreshToken(token string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser creates a new user
|
// CreateUserTx creates a new user within a transaction
|
||||||
func (r *UserRepository) CreateUser(user *models.User) error {
|
func (r *UserRepository) CreateUserTx(tx *sqlx.Tx, user *models.User) error {
|
||||||
// Hash the password before storing
|
// Hash the password before storing
|
||||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -151,7 +152,7 @@ func (r *UserRepository) CreateUser(user *models.User) error {
|
|||||||
query := `INSERT INTO user (UserEmail, UserPassword, UserRole, UserName, UserCreatedAt, UserUpdatedAt)
|
query := `INSERT INTO user (UserEmail, UserPassword, UserRole, UserName, UserCreatedAt, UserUpdatedAt)
|
||||||
VALUES (?, ?, ?, ?, NOW(), NOW())`
|
VALUES (?, ?, ?, ?, NOW(), NOW())`
|
||||||
|
|
||||||
result, err := database.DB.Exec(query, user.Email, string(hashedPassword), user.Role, user.Name)
|
result, err := tx.Exec(query, user.Email, string(hashedPassword), user.Role, user.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("database error creating user: %w", err)
|
return fmt.Errorf("database error creating user: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,8 +88,8 @@ func SetupRouter(cfg *config.Config, logger *zap.Logger) http.Handler {
|
|||||||
r.Post("/refresh", authHandler.RefreshToken)
|
r.Post("/refresh", authHandler.RefreshToken)
|
||||||
r.Post("/logout", authHandler.Logout)
|
r.Post("/logout", authHandler.Logout)
|
||||||
|
|
||||||
// Registration endpoint
|
// Registration endpoint
|
||||||
registerService := service.NewRegisterService()
|
registerService := service.NewRegisterService(logger)
|
||||||
registerHandler := handlers.NewRegisterHandler(logger, registerService)
|
registerHandler := handlers.NewRegisterHandler(logger, registerService)
|
||||||
r.Post("/register", registerHandler.Register)
|
r.Post("/register", registerHandler.Register)
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
|
|
||||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models"
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models"
|
||||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/repository"
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/repository"
|
||||||
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/database"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -15,6 +17,7 @@ var (
|
|||||||
ErrInvalidRole = errors.New("invalid user role")
|
ErrInvalidRole = errors.New("invalid user role")
|
||||||
ErrInvalidPatient = errors.New("invalid patient data")
|
ErrInvalidPatient = errors.New("invalid patient data")
|
||||||
ErrInvalidDoctor = errors.New("invalid doctor data")
|
ErrInvalidDoctor = errors.New("invalid doctor data")
|
||||||
|
ErrTransaction = errors.New("transaction error")
|
||||||
)
|
)
|
||||||
|
|
||||||
// RegisterRequest represents a user registration request
|
// RegisterRequest represents a user registration request
|
||||||
@@ -34,15 +37,22 @@ type RegisterService struct {
|
|||||||
patientRepo *repository.PatientRepository
|
patientRepo *repository.PatientRepository
|
||||||
doctorRepo *repository.DoctorRepository
|
doctorRepo *repository.DoctorRepository
|
||||||
studyRepo *repository.StudyRepository
|
studyRepo *repository.StudyRepository
|
||||||
|
logger *zap.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRegisterService creates a new register service
|
// NewRegisterService creates a new register service
|
||||||
func NewRegisterService() *RegisterService {
|
func NewRegisterService(logger *zap.Logger) *RegisterService {
|
||||||
|
if logger == nil {
|
||||||
|
// If no logger is provided, create a no-op logger
|
||||||
|
logger = zap.NewNop()
|
||||||
|
}
|
||||||
|
|
||||||
return &RegisterService{
|
return &RegisterService{
|
||||||
userRepo: repository.NewUserRepository(),
|
userRepo: repository.NewUserRepository(),
|
||||||
patientRepo: repository.NewPatientRepository(),
|
patientRepo: repository.NewPatientRepository(),
|
||||||
doctorRepo: repository.NewDoctorRepository(),
|
doctorRepo: repository.NewDoctorRepository(),
|
||||||
studyRepo: repository.NewStudyRepository(),
|
studyRepo: repository.NewStudyRepository(),
|
||||||
|
logger: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +72,7 @@ func (s *RegisterService) Register(req *RegisterRequest) (*models.User, error) {
|
|||||||
return nil, ErrInvalidDoctor
|
return nil, ErrInvalidDoctor
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if email already exists
|
// Check if email already exists - do this outside the transaction
|
||||||
existingUser, err := s.userRepo.GetUserByEmail(req.Email)
|
existingUser, err := s.userRepo.GetUserByEmail(req.Email)
|
||||||
if err != nil && err != sql.ErrNoRows {
|
if err != nil && err != sql.ErrNoRows {
|
||||||
return nil, fmt.Errorf("error checking existing user: %w", err)
|
return nil, fmt.Errorf("error checking existing user: %w", err)
|
||||||
@@ -72,6 +82,22 @@ func (s *RegisterService) Register(req *RegisterRequest) (*models.User, error) {
|
|||||||
return nil, ErrEmailExists
|
return nil, ErrEmailExists
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start a transaction
|
||||||
|
tx, err := database.DB.Beginx()
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to begin transaction", zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("%w: failed to begin transaction", ErrTransaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the transaction is rolled back if we return an error
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
s.logger.Error("Panic in transaction", zap.Any("recover", r))
|
||||||
|
tx.Rollback()
|
||||||
|
panic(r) // re-throw the panic after cleanup
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Create user
|
// Create user
|
||||||
newUser := &models.User{
|
newUser := &models.User{
|
||||||
Email: req.Email,
|
Email: req.Email,
|
||||||
@@ -80,15 +106,19 @@ func (s *RegisterService) Register(req *RegisterRequest) (*models.User, error) {
|
|||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.userRepo.CreateUser(newUser); err != nil {
|
// Use the transaction for user creation
|
||||||
|
if err := s.userRepo.CreateUserTx(tx, newUser); err != nil {
|
||||||
|
s.logger.Error("Failed to create user", zap.Error(err), zap.String("email", req.Email))
|
||||||
|
tx.Rollback()
|
||||||
return nil, fmt.Errorf("error creating user: %w", err)
|
return nil, fmt.Errorf("error creating user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create role-specific data
|
// Create role-specific data
|
||||||
if req.Role == "patient" {
|
if req.Role == "patient" {
|
||||||
err = s.patientRepo.CreatePatient(req.Patient, newUser.ID)
|
err = s.patientRepo.CreatePatientTx(tx, req.Patient, newUser.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO: Consider rollback user creation if this fails
|
s.logger.Error("Failed to create patient", zap.Error(err), zap.String("patientID", req.Patient.PatientID))
|
||||||
|
tx.Rollback()
|
||||||
return nil, fmt.Errorf("error creating patient: %w", err)
|
return nil, fmt.Errorf("error creating patient: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,19 +129,37 @@ func (s *RegisterService) Register(req *RegisterRequest) (*models.User, error) {
|
|||||||
continue // Skip studies without UIDs
|
continue // Skip studies without UIDs
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.studyRepo.CreateStudy(req.Patient.PatientID, study)
|
err = s.studyRepo.CreateStudyTx(tx, req.Patient.PatientID, study)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to create study",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.String("patientID", req.Patient.PatientID),
|
||||||
|
zap.String("studyUID", study.StudyInstanceUID))
|
||||||
|
tx.Rollback()
|
||||||
return nil, fmt.Errorf("error creating study for patient: %w", err)
|
return nil, fmt.Errorf("error creating study for patient: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if req.Role == "ref_doctor" || req.Role == "expertise_doctor" {
|
} else if req.Role == "ref_doctor" || req.Role == "expertise_doctor" {
|
||||||
err = s.doctorRepo.CreateDoctor(req.Doctor, newUser.ID)
|
err = s.doctorRepo.CreateDoctorTx(tx, req.Doctor, newUser.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO: Consider rollback user creation if this fails
|
s.logger.Error("Failed to create doctor", zap.Error(err), zap.String("doctorID", req.Doctor.DoctorID))
|
||||||
|
tx.Rollback()
|
||||||
return nil, fmt.Errorf("error creating doctor: %w", err)
|
return nil, fmt.Errorf("error creating doctor: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Commit the transaction
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
s.logger.Error("Failed to commit transaction", zap.Error(err))
|
||||||
|
tx.Rollback() // This is actually redundant as the transaction will be rolled back on failure
|
||||||
|
return nil, fmt.Errorf("%w: failed to commit transaction", ErrTransaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("Successfully registered new user",
|
||||||
|
zap.String("email", req.Email),
|
||||||
|
zap.String("role", req.Role),
|
||||||
|
zap.String("userID", newUser.ID))
|
||||||
|
|
||||||
return newUser, nil
|
return newUser, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models"
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models"
|
||||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/repository"
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/repository"
|
||||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/auth"
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/auth"
|
||||||
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/database"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -150,13 +151,36 @@ func (s *ShortLinkService) GenerateShortLink(req *models.GenerateShortLinkReques
|
|||||||
RemainingTries: s.maxAttempts,
|
RemainingTries: s.maxAttempts,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the short link in the database
|
// Start a transaction for creating the shortlink
|
||||||
err = s.shortLinkRepo.CreateShortLink(shortLink)
|
tx, err := database.DB.Beginx()
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to start transaction", zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("database error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up deferred rollback that will be canceled if we commit
|
||||||
|
defer func() {
|
||||||
|
if tx != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Store the short link in the database using transaction
|
||||||
|
err = s.shortLinkRepo.CreateShortLinkTx(tx, shortLink)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to store shortlink in database", zap.Error(err))
|
s.logger.Error("Failed to store shortlink in database", zap.Error(err))
|
||||||
return nil, ErrCreationFailed
|
return nil, ErrCreationFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Commit the transaction
|
||||||
|
if err = tx.Commit(); err != nil {
|
||||||
|
s.logger.Error("Failed to commit transaction", zap.Error(err))
|
||||||
|
return nil, errors.New("database error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the tx to prevent the deferred rollback
|
||||||
|
tx = nil
|
||||||
|
|
||||||
// Generate the full URL using the configured base URL
|
// Generate the full URL using the configured base URL
|
||||||
fullURL := fmt.Sprintf("%s/short-auth?short=%s", s.baseURL, token)
|
fullURL := fmt.Sprintf("%s/short-auth?short=%s", s.baseURL, token)
|
||||||
|
|
||||||
@@ -203,43 +227,101 @@ func (s *ShortLinkService) ValidateShortLink(req *models.ShortLinkAuthRequest) (
|
|||||||
// Normalize and hash the provided DOB
|
// Normalize and hash the provided DOB
|
||||||
dob := normalizeDOB(req.DOB)
|
dob := normalizeDOB(req.DOB)
|
||||||
if !isValidDOBFormat(dob) {
|
if !isValidDOBFormat(dob) {
|
||||||
|
// Use a transaction for updating the tries counter
|
||||||
|
tx, err := database.DB.Beginx()
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to start transaction", zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("database error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up deferred rollback that will be canceled if we commit
|
||||||
|
defer func() {
|
||||||
|
if tx != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Decrement remaining tries on invalid format
|
// Decrement remaining tries on invalid format
|
||||||
shortLink.RemainingTries--
|
shortLink.RemainingTries--
|
||||||
|
|
||||||
// Update the shortlink in the database
|
// Update the shortlink in the database using the transaction
|
||||||
err = s.shortLinkRepo.UpdateShortLink(shortLink)
|
err = s.shortLinkRepo.UpdateShortLinkTx(tx, shortLink)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to update shortlink tries", zap.Error(err))
|
s.logger.Error("Failed to update shortlink tries", zap.Error(err))
|
||||||
|
return nil, errors.New("invalid date of birth format, expected YYYY-MM-DD")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Commit the transaction
|
||||||
|
if err = tx.Commit(); err != nil {
|
||||||
|
s.logger.Error("Failed to commit transaction", zap.Error(err))
|
||||||
|
return nil, errors.New("database error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the tx to prevent the deferred rollback
|
||||||
|
tx = nil
|
||||||
|
|
||||||
return nil, errors.New("invalid date of birth format, expected YYYY-MM-DD")
|
return nil, errors.New("invalid date of birth format, expected YYYY-MM-DD")
|
||||||
}
|
}
|
||||||
|
|
||||||
hashedDOB := hashDOB(dob)
|
hashedDOB := hashDOB(dob)
|
||||||
|
|
||||||
|
// Start a transaction for updating the tries counter
|
||||||
|
tx, err := database.DB.Beginx()
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to start transaction", zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("database error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up deferred rollback that will be canceled if we commit
|
||||||
|
defer func() {
|
||||||
|
if tx != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Verify the DOB
|
// Verify the DOB
|
||||||
if hashedDOB != shortLink.HashedDOB {
|
if hashedDOB != shortLink.HashedDOB {
|
||||||
// Decrement remaining tries on failed verification
|
// Decrement remaining tries on failed verification
|
||||||
shortLink.RemainingTries--
|
shortLink.RemainingTries--
|
||||||
|
|
||||||
// Update the shortlink in the database
|
// Update the shortlink in the database using the transaction
|
||||||
err = s.shortLinkRepo.UpdateShortLink(shortLink)
|
err = s.shortLinkRepo.UpdateShortLinkTx(tx, shortLink)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to update shortlink tries", zap.Error(err))
|
s.logger.Error("Failed to update shortlink tries", zap.Error(err))
|
||||||
|
return nil, ErrInvalidDOB
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Commit the transaction
|
||||||
|
if err = tx.Commit(); err != nil {
|
||||||
|
s.logger.Error("Failed to commit transaction", zap.Error(err))
|
||||||
|
return nil, errors.New("database error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the tx to prevent the deferred rollback
|
||||||
|
tx = nil
|
||||||
|
|
||||||
return nil, ErrInvalidDOB
|
return nil, ErrInvalidDOB
|
||||||
}
|
}
|
||||||
|
|
||||||
// DOB verified, reset tries count as successful login
|
// DOB verified, reset tries count as successful login
|
||||||
shortLink.RemainingTries = s.maxAttempts
|
shortLink.RemainingTries = s.maxAttempts
|
||||||
|
|
||||||
// Update the shortlink in the database
|
// Update the shortlink in the database using the transaction
|
||||||
err = s.shortLinkRepo.UpdateShortLink(shortLink)
|
err = s.shortLinkRepo.UpdateShortLinkTx(tx, shortLink)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to update shortlink tries after successful validation", zap.Error(err))
|
s.logger.Error("Failed to update shortlink tries after successful validation", zap.Error(err))
|
||||||
|
return nil, errors.New("database error")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Commit the transaction
|
||||||
|
if err = tx.Commit(); err != nil {
|
||||||
|
s.logger.Error("Failed to commit transaction", zap.Error(err))
|
||||||
|
return nil, errors.New("database error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the tx to prevent the deferred rollback
|
||||||
|
tx = nil
|
||||||
|
|
||||||
return shortLink, nil
|
return shortLink, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
### Local OHIF Proxy Test File
|
### Local OHIF Proxy Test File
|
||||||
@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMiIsImVtYWlsIjoicGF0aWVudCIsInJvbGUiOiJwYXRpZW50IiwidG9rZW5fdHlwZSI6ImFjY2VzcyIsImV4cCI6MTc0NjM2NDczMiwiaWF0IjoxNzQ2MzYyOTMyfQ.4IGGV77jnewQVXOCuFWmcx4X7EMMxx341j6DeNKYcFY
|
@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMSIsImVtYWlsIjoiYWRtaW5AZXhhbXBsZS5jb20iLCJyb2xlIjoiYWRtaW4iLCJ1c2VyX25hbWUiOiJBZG1pbiBVc2VyIiwidG9rZW5fdHlwZSI6ImFjY2VzcyIsImhvbWVfdXJsIjoiLyIsInN0dWR5X2xpc3QiOiJlbmFibGVkIiwiZXhwIjoxNzQ3Mzg0MDE2LCJpYXQiOjE3NDcyOTc2MTZ9.Ak1DECP1MXzQAPyU-AJM6Tsu6-sw04UtWYvY37-SaT4
|
||||||
@baseUrl = http://localhost:5555
|
@baseUrl = http://localhost:5555
|
||||||
|
|
||||||
# @baseUrl = http://devone.aplikasi.web.id:5555
|
# @baseUrl = http://devone.aplikasi.web.id:5555
|
||||||
@@ -20,11 +20,13 @@ Authorization: Bearer {{token}}
|
|||||||
# Returns studies matching patient name (replace SMITH with a name in your dataset)
|
# Returns studies matching patient name (replace SMITH with a name in your dataset)
|
||||||
GET {{baseUrl}}/dicomWeb/studies?limit=10&offset=0&fuzzymatching=true&includefield=00081030%2C00080060%2C00080090&00080090=DR.%20HERWINDO%20RIDWAN%2C%20SP.OT
|
GET {{baseUrl}}/dicomWeb/studies?limit=10&offset=0&fuzzymatching=true&includefield=00081030%2C00080060%2C00080090&00080090=DR.%20HERWINDO%20RIDWAN%2C%20SP.OT
|
||||||
Accept: application/dicom+json
|
Accept: application/dicom+json
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
|
||||||
### 4. QIDO-RS: Search for Studies with Date Range
|
### 4. QIDO-RS: Search for Studies with Date Range
|
||||||
# Returns studies within a date range
|
# Returns studies within a date range
|
||||||
GET {{baseUrl}}/dicomWeb/studies?StudyDate=20250301-20250415
|
GET {{baseUrl}}/dicomWeb/studies?StudyDate=20250301-20250415
|
||||||
Accept: application/dicom+json
|
Accept: application/dicom+json
|
||||||
|
Authorization: Bearer {{ token }}
|
||||||
|
|
||||||
### 5. QIDO-RS: Search for Series in a Study
|
### 5. QIDO-RS: Search for Series in a Study
|
||||||
# Replace STUDY_INSTANCE_UID with an actual Study UID from your data
|
# Replace STUDY_INSTANCE_UID with an actual Study UID from your data
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMSIsImVtYWlsIjoicGF0aWVudDFAZXhhbXBsZS5jb20iLCJyb2xlIjoicGF0aWVudCIsInVzZXJfbmFtZSI6IkRJRElUIFNVWUFUTkFeUi4xMDA0OS4xOCIsInRva2VuX3R5cGUiOiJhY2Nlc3MiLCJwYXRpZW50X2lkIjoiMDAyMTE2MjIiLCJwYXRpZW50X25hbWUiOiJESURJVCBTVVlBVE5BXlIuMTAwNDkuMTgiLCJzdHVkeV9pdWlkcyI6WyIxLjIuODI2LjAuMS4zNjgwMDQzLjkuNzMwNy4xLjIwMTgwNTMwMDY2IiwiMS4yLjgyNi4wLjEuMzY4MDA0My45LjczMDcuMS4yMDE4MDcxMzAzNiJdLCJhY2Nlc3Npb25fbnVtYmVycyI6WyJDUi4xODA1MzAuMDY2IiwiQ1IuMTgwNzEzLjAzNiJdLCJob21lX3VybCI6InZpZXdlcj9TdHVkeUluc3RhbmNlVUlEcz0xLjIuODI2LjAuMS4zNjgwMDQzLjkuNzMwNy4xLjIwMTgwNTMwMDY2Iiwic3R1ZHlfbGlzdCI6ImRpc2FibGVkIiwiZXhwIjoxNzQ3MzY0OTY1LCJpYXQiOjE3NDcyNzg1NjV9.I6L7nnT-xexPtkTIfIXetT41KGQlVPbCY8l_2fTgJdg
|
@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMSIsImVtYWlsIjoiYWRtaW5AZXhhbXBsZS5jb20iLCJyb2xlIjoiYWRtaW4iLCJ1c2VyX25hbWUiOiJBZG1pbiBVc2VyIiwidG9rZW5fdHlwZSI6ImFjY2VzcyIsImhvbWVfdXJsIjoiLyIsInN0dWR5X2xpc3QiOiJlbmFibGVkIiwiZXhwIjoxNzQ3Mzg0MDE2LCJpYXQiOjE3NDcyOTc2MTZ9.Ak1DECP1MXzQAPyU-AJM6Tsu6-sw04UtWYvY37-SaT4
|
||||||
@token_exp_doctor = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMSIsImVtYWlsIjoiYWRtaW4iLCJyb2xlIjoiZXhwZXJ0aXNlX2RvY3RvciIsInRva2VuX3R5cGUiOiJhY2Nlc3MiLCJleHAiOjE3NDY1MDQ1MTYsImlhdCI6MTc0NjQxODExNn0.vlDrns1oPFXHE5--TmWqwzvzxnfcCPcV2UW8_4GwDwE
|
@token_exp_doctor = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMSIsImVtYWlsIjoiYWRtaW4iLCJyb2xlIjoiZXhwZXJ0aXNlX2RvY3RvciIsInRva2VuX3R5cGUiOiJhY2Nlc3MiLCJleHAiOjE3NDY1MDQ1MTYsImlhdCI6MTc0NjQxODExNn0.vlDrns1oPFXHE5--TmWqwzvzxnfcCPcV2UW8_4GwDwE
|
||||||
@baseUrl = http://localhost:5555
|
@baseUrl = http://localhost:5555
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,17 @@
|
|||||||
@baseUrl = http://localhost:5555
|
@baseUrl = http://localhost:5555
|
||||||
|
|
||||||
|
|
||||||
|
### Register an admin user
|
||||||
|
POST {{baseUrl}}/auth/register
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "admin@example.com",
|
||||||
|
"password": "password123",
|
||||||
|
"name": "Admin User",
|
||||||
|
"role": "admin"
|
||||||
|
}
|
||||||
|
|
||||||
### Register a new patient with multiple studies
|
### Register a new patient with multiple studies
|
||||||
POST {{baseUrl}}/auth/register
|
POST {{baseUrl}}/auth/register
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
@@ -24,7 +35,24 @@ Content-Type: application/json
|
|||||||
"accession_number": "CR.180530.066",
|
"accession_number": "CR.180530.066",
|
||||||
"study_date": "2018-05-30",
|
"study_date": "2018-05-30",
|
||||||
"study_description": "Chest X-Ray"
|
"study_description": "Chest X-Ray"
|
||||||
},
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
###
|
||||||
|
POST {{baseUrl}}/auth/register
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "patient1@example.com",
|
||||||
|
"password": "password123",
|
||||||
|
"name": "DIDIT SUYATNA^R.10049.18",
|
||||||
|
"role": "patient",
|
||||||
|
"patient": {
|
||||||
|
"patient_id": "00211622",
|
||||||
|
"patient_name": "DIDIT SUYATNA^R.10049.18",
|
||||||
|
"date_of_birth": "1980-01-01"
|
||||||
|
},
|
||||||
|
"studies": [
|
||||||
{
|
{
|
||||||
"study_instance_uid": "1.2.826.0.1.3680043.9.7307.1.20180713036",
|
"study_instance_uid": "1.2.826.0.1.3680043.9.7307.1.20180713036",
|
||||||
"accession_number": "CR.180713.036",
|
"accession_number": "CR.180713.036",
|
||||||
@@ -73,21 +101,6 @@ Content-Type: application/json
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
### Register a new expertise doctor
|
|
||||||
POST {{baseUrl}}/auth/register
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"email": "expert@example.com",
|
|
||||||
"password": "password123",
|
|
||||||
"name": "Dr. Expertise",
|
|
||||||
"role": "expertise_doctor",
|
|
||||||
"doctor": {
|
|
||||||
"doctor_id": "EX456",
|
|
||||||
"doctor_name": "Dr. Expertise"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
### Register another referring doctor
|
### Register another referring doctor
|
||||||
POST {{baseUrl}}/auth/register
|
POST {{baseUrl}}/auth/register
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
@@ -103,15 +116,19 @@ Content-Type: application/json
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
### Register an admin user
|
### Register a new expertise doctor
|
||||||
POST {{baseUrl}}/auth/register
|
POST {{baseUrl}}/auth/register
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"email": "admin@example.com",
|
"email": "expert@example.com",
|
||||||
"password": "password123",
|
"password": "password123",
|
||||||
"name": "Admin User",
|
"name": "Dr. Expertise",
|
||||||
"role": "admin"
|
"role": "expertise_doctor",
|
||||||
|
"doctor": {
|
||||||
|
"doctor_id": "EX456",
|
||||||
|
"doctor_name": "Dr. Expertise"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
### Login with registered user
|
### Login with registered user
|
||||||
@@ -150,10 +167,12 @@ Content-Type: application/json
|
|||||||
"password": "password123"
|
"password": "password123"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@refresh_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNCIsImVtYWlsIjoiZG9jdG9yQGV4YW1wbGUuY29tIiwicm9sZSI6InJlZl9kb2N0b3IiLCJ1c2VyX25hbWUiOiJEUi4gSEVSV0lORE8gUklEV0FOLCBTUC5PVCIsInRva2VuX3R5cGUiOiJyZWZyZXNoIiwiaG9tZV91cmwiOiIvIiwic3R1ZHlfbGlzdCI6ImVuYWJsZWQiLCJmaWx0ZXJfdXJsIjoic3R1ZGllcz9saW1pdD0xMDFcdTAwMjZvZmZzZXQ9MFx1MDAyNmZ1enp5bWF0Y2hpbmc9ZmFsc2VcdTAwMjZpbmNsdWRlZmllbGQ9MDAwODEwMzAsMDAwODAwNjAsMDAwODAwOTBcdTAwMjYwMDA4MDA5MD1EUi4rSEVSV0lORE8rUklEV0FOJTJDK1NQLk9UIiwiZXhwIjoxNzQ3OTAxMzI5LCJpYXQiOjE3NDcyOTY1Mjl9.M7khgg8eZHk20qGsdPDmUojdBvItXOE-834R6t_AF9Q"
|
||||||
|
|
||||||
### Refresh TOken
|
### Refresh TOken
|
||||||
POST {{baseUrl}}/auth/refresh
|
POST {{baseUrl}}/auth/refresh
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"refresh_token": {{ refresh_token}}
|
"refresh_token" : {{refresh_token}}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user