add: register and login with DB query AND some struct type correction
This commit is contained in:
@@ -69,6 +69,12 @@ go-ohif-proxy/
|
|||||||
└── README.md # Project documentation
|
└── README.md # Project documentation
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 2c. Alur Kerja Kode
|
||||||
|
Secara umum kode di repo ini kurang lebih memiliki alur:
|
||||||
|
```
|
||||||
|
Routes --> Handlers --> Service --> Repository
|
||||||
|
```
|
||||||
|
|
||||||
## 3. Instalasi dan Penggunaan
|
## 3. Instalasi dan Penggunaan
|
||||||
|
|
||||||
### Prasyarat
|
### Prasyarat
|
||||||
|
|||||||
68
internal/api/handlers/register.go
Normal file
68
internal/api/handlers/register.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/service"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterHandler handles user registration requests
|
||||||
|
type RegisterHandler struct {
|
||||||
|
logger *zap.Logger
|
||||||
|
registerService *service.RegisterService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRegisterHandler creates a new registration handler
|
||||||
|
func NewRegisterHandler(logger *zap.Logger, registerService *service.RegisterService) *RegisterHandler {
|
||||||
|
return &RegisterHandler{
|
||||||
|
logger: logger,
|
||||||
|
registerService: registerService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register handles the creation of new users, patients, and doctors
|
||||||
|
func (h *RegisterHandler) Register(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Parse registration request
|
||||||
|
var req service.RegisterRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
h.logger.Error("Failed to parse registration request", zap.Error(err))
|
||||||
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if req.Email == "" || req.Password == "" || req.Name == "" || req.Role == "" {
|
||||||
|
http.Error(w, "Missing required fields", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform registration
|
||||||
|
user, err := h.registerService.Register(&req)
|
||||||
|
if err != nil {
|
||||||
|
switch err {
|
||||||
|
case service.ErrEmailExists:
|
||||||
|
http.Error(w, "Email already exists", http.StatusConflict)
|
||||||
|
case service.ErrInvalidRole:
|
||||||
|
http.Error(w, "Invalid user role", http.StatusBadRequest)
|
||||||
|
case service.ErrInvalidPatient:
|
||||||
|
http.Error(w, "Invalid patient data", http.StatusBadRequest)
|
||||||
|
case service.ErrInvalidDoctor:
|
||||||
|
http.Error(w, "Invalid doctor data", http.StatusBadRequest)
|
||||||
|
default:
|
||||||
|
h.logger.Error("Registration failed", zap.Error(err))
|
||||||
|
http.Error(w, "Registration failed", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return created user
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
|
||||||
|
// Don't include password in response
|
||||||
|
user.Password = ""
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(user)
|
||||||
|
}
|
||||||
@@ -71,7 +71,7 @@ func Auth(authService *service.AuthService, logger *zap.Logger) func(http.Handle
|
|||||||
// Add user info to request context
|
// Add user info to request context
|
||||||
ctx := context.WithValue(r.Context(), UserIDKey, claims.UserID)
|
ctx := context.WithValue(r.Context(), UserIDKey, claims.UserID)
|
||||||
ctx = context.WithValue(ctx, UserRoleKey, claims.Role)
|
ctx = context.WithValue(ctx, UserRoleKey, claims.Role)
|
||||||
ctx = context.WithValue(ctx, UserEmailKey, claims.Email)
|
ctx = context.WithValue(ctx, UserEmailKey, claims.Email) // TODO: Apakah kita perlu param email untuk generate access token?
|
||||||
|
|
||||||
// Store the claims with the defined context key
|
// Store the claims with the defined context key
|
||||||
ctx = context.WithValue(ctx, ClaimsKey, claims)
|
ctx = context.WithValue(ctx, ClaimsKey, claims)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package models
|
|||||||
type PatientDetails struct {
|
type PatientDetails struct {
|
||||||
PatientID string `json:"patient_id"`
|
PatientID string `json:"patient_id"`
|
||||||
PatientName string `json:"patient_name"`
|
PatientName string `json:"patient_name"`
|
||||||
|
DateOfBirth string `json:"date_of_birth"` // YYYY-MM-DD format
|
||||||
StudyInstanceUIDs []string `json:"study_instance_uids,omitempty"`
|
StudyInstanceUIDs []string `json:"study_instance_uids,omitempty"`
|
||||||
AccessionNumbers []string `json:"accession_numbers,omitempty"`
|
AccessionNumbers []string `json:"accession_numbers,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -42,3 +43,16 @@ func (r *DoctorRepository) GetDoctorDetailsByUserID(userID string) (*DBDoctor, e
|
|||||||
|
|
||||||
return &dbDoctor, nil
|
return &dbDoctor, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateDoctor creates a new doctor record
|
||||||
|
func (r *DoctorRepository) CreateDoctor(doctorDetails *models.DoctorDetails, userID string) error {
|
||||||
|
query := `INSERT INTO doctor (Doctor_UsersID, DoctorID, DoctorName, DoctorCreatedAt, DoctorLastUpdatedAt)
|
||||||
|
VALUES (?, ?, ?, NOW(), NOW())`
|
||||||
|
|
||||||
|
_, err := database.DB.Exec(query, userID, doctorDetails.DoctorID, doctorDetails.DoctorName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("database error creating doctor: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -60,3 +60,22 @@ func (r *PatientRepository) GetPatientDetailsByUserID(userID string) (*models.Pa
|
|||||||
AccessionNumbers: accessionNumbers,
|
AccessionNumbers: accessionNumbers,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreatePatient creates a new patient record
|
||||||
|
func (r *PatientRepository) CreatePatient(patientRecord *models.PatientDetails, userID string) error {
|
||||||
|
// Parse DOB to time.Time
|
||||||
|
dob, err := time.Parse("2006-01-02", patientRecord.DateOfBirth)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid date of birth format: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `INSERT INTO patient (Patient_UsersID, PatientMedrec, PatientName, PatientDoB, PatientCreatedAt, PatientUpdatedAt)
|
||||||
|
VALUES (?, ?, ?, ?, NOW(), NOW())`
|
||||||
|
|
||||||
|
_, err = database.DB.Exec(query, userID, patientRecord.PatientID, patientRecord.PatientName, dob)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("database error creating patient: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ 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"`
|
||||||
@@ -40,7 +40,7 @@ func NewShortLinkRepository() *ShortLinkRepository {
|
|||||||
func (s *DBShortLink) ToShortLink() *models.ShortLink {
|
func (s *DBShortLink) ToShortLink() *models.ShortLink {
|
||||||
return &models.ShortLink{
|
return &models.ShortLink{
|
||||||
ID: fmt.Sprintf("%d", s.ShortlinkID),
|
ID: fmt.Sprintf("%d", s.ShortlinkID),
|
||||||
Token: s.ShortlinKCode,
|
Token: s.ShortlinkCode,
|
||||||
PatientID: s.Shortlink_PatientID,
|
PatientID: s.Shortlink_PatientID,
|
||||||
StudyUID: s.Shortlink_Study_IUID,
|
StudyUID: s.Shortlink_Study_IUID,
|
||||||
HashedDOB: s.ShortlinkHashDoB,
|
HashedDOB: s.ShortlinkHashDoB,
|
||||||
@@ -56,7 +56,7 @@ func (s *DBShortLink) ToShortLink() *models.ShortLink {
|
|||||||
func (r *ShortLinkRepository) GetShortLinkByToken(token string) (*models.ShortLink, error) {
|
func (r *ShortLinkRepository) GetShortLinkByToken(token string) (*models.ShortLink, error) {
|
||||||
var dbShortLink DBShortLink
|
var dbShortLink DBShortLink
|
||||||
|
|
||||||
query := `SELECT * FROM shortlink WHERE ShortlinKCode = ?`
|
query := `SELECT * FROM shortlink WHERE ShortlinkCode = ?`
|
||||||
err := database.DB.Get(&dbShortLink, query, token)
|
err := database.DB.Get(&dbShortLink, query, token)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -72,7 +72,7 @@ func (r *ShortLinkRepository) GetShortLinkByToken(token string) (*models.ShortLi
|
|||||||
// CreateShortLink stores a new shortlink in the database
|
// CreateShortLink stores a new shortlink in the database
|
||||||
func (r *ShortLinkRepository) CreateShortLink(shortLink *models.ShortLink) error {
|
func (r *ShortLinkRepository) CreateShortLink(shortLink *models.ShortLink) error {
|
||||||
query := `INSERT INTO shortlink (
|
query := `INSERT INTO shortlink (
|
||||||
ShortlinKCode,
|
ShortlinkCode,
|
||||||
Shortlink_PatientID,
|
Shortlink_PatientID,
|
||||||
Shortlink_Study_IUID,
|
Shortlink_Study_IUID,
|
||||||
ShortlinkHashDoB,
|
ShortlinkHashDoB,
|
||||||
@@ -118,7 +118,7 @@ func (r *ShortLinkRepository) UpdateShortLink(shortLink *models.ShortLink) error
|
|||||||
ShortlinkIsRevoked = ?,
|
ShortlinkIsRevoked = ?,
|
||||||
ShortlinkRemainingTries = ?,
|
ShortlinkRemainingTries = ?,
|
||||||
ShortlinkExpiredAt = ?
|
ShortlinkExpiredAt = ?
|
||||||
WHERE ShortlinKCode = ?`
|
WHERE ShortlinkCode = ?`
|
||||||
|
|
||||||
expiresAt, err := time.Parse(time.RFC3339, shortLink.ExpiresAt)
|
expiresAt, err := time.Parse(time.RFC3339, shortLink.ExpiresAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -4,18 +4,19 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DBStudy represents a study from the database
|
// DBStudy represents a study from the database
|
||||||
type DBStudy struct {
|
type DBStudy struct {
|
||||||
ID int `db:"id"`
|
ID int `db:"id"`
|
||||||
Study_PatientID string `db:"Study_PatientID"`
|
Study_PatientID string `db:"Study_PatientID"`
|
||||||
Study_IUID string `db:"Study_IUID"`
|
StudyIUID string `db:"StudyIUID"`
|
||||||
Study_AccessionNumber string `db:"Study_AccessionNumber"`
|
StudyAccessionNumber string `db:"StudyAccessionNumber"`
|
||||||
StudyDate time.Time `db:"StudyDate"`
|
StudyDate time.Time `db:"StudyDate"`
|
||||||
StudyCreatedAt time.Time `db:"StudyCreatedAt"`
|
StudyCreatedAt time.Time `db:"StudyCreatedAt"`
|
||||||
StudyUpdatedAt time.Time `db:"StudyUpdatedAt"`
|
StudyUpdatedAt time.Time `db:"StudyUpdatedAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// StudyRepository handles database operations related to studies
|
// StudyRepository handles database operations related to studies
|
||||||
@@ -45,9 +46,9 @@ func (r *StudyRepository) GetPatientStudies(patientID string) ([]string, []strin
|
|||||||
var accessionNumbers []string
|
var accessionNumbers []string
|
||||||
|
|
||||||
for _, study := range studies {
|
for _, study := range studies {
|
||||||
studyUIDs = append(studyUIDs, study.Study_IUID)
|
studyUIDs = append(studyUIDs, study.StudyIUID)
|
||||||
if study.Study_AccessionNumber != "" {
|
if study.StudyAccessionNumber != "" {
|
||||||
accessionNumbers = append(accessionNumbers, study.Study_AccessionNumber)
|
accessionNumbers = append(accessionNumbers, study.StudyAccessionNumber)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ func (r *StudyRepository) GetPatientStudies(patientID string) ([]string, []strin
|
|||||||
func (r *StudyRepository) GetStudyByUID(studyUID string) (*DBStudy, error) {
|
func (r *StudyRepository) GetStudyByUID(studyUID string) (*DBStudy, error) {
|
||||||
var study DBStudy
|
var study DBStudy
|
||||||
|
|
||||||
query := `SELECT * FROM study WHERE Study_IUID = ?`
|
query := `SELECT * FROM study WHERE StudyIUID = ?`
|
||||||
err := database.DB.Get(&study, query, studyUID)
|
err := database.DB.Get(&study, query, studyUID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -67,3 +68,32 @@ func (r *StudyRepository) GetStudyByUID(studyUID string) (*DBStudy, error) {
|
|||||||
|
|
||||||
return &study, nil
|
return &study, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateStudy creates a new study record for a patient
|
||||||
|
func (r *StudyRepository) CreateStudy(patientID string, study models.Study) error {
|
||||||
|
// Parse study date if provided
|
||||||
|
var studyDate *time.Time
|
||||||
|
if study.StudyDate != "" {
|
||||||
|
parsedTime, err := time.Parse("2006-01-02", study.StudyDate)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid study date format: %w", err)
|
||||||
|
}
|
||||||
|
studyDate = &parsedTime
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `INSERT INTO study
|
||||||
|
(Study_PatientID, StudyIUID, StudyAccessionNumber, StudyDate, StudyCreatedAt, StudyUpdatedAt)
|
||||||
|
VALUES (?, ?, ?, ?, NOW(), NOW())`
|
||||||
|
|
||||||
|
_, err := database.DB.Exec(query,
|
||||||
|
patientID,
|
||||||
|
study.StudyInstanceUID,
|
||||||
|
study.AccessionNumber,
|
||||||
|
studyDate)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("database error creating study: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DBUser represents a user from the database
|
// DBUser represents a user from the database
|
||||||
@@ -138,3 +139,30 @@ func (r *UserRepository) RevokeRefreshToken(token string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateUser creates a new user
|
||||||
|
func (r *UserRepository) CreateUser(user *models.User) error {
|
||||||
|
// Hash the password before storing
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to hash password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `INSERT INTO user (UserEmail, UserPassword, UserRole, UserName, UserCreatedAt, UserUpdatedAt)
|
||||||
|
VALUES (?, ?, ?, ?, NOW(), NOW())`
|
||||||
|
|
||||||
|
result, err := database.DB.Exec(query, user.Email, string(hashedPassword), user.Role, user.Name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("database error creating user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the last inserted ID
|
||||||
|
id, err := result.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get last insert ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the user ID
|
||||||
|
user.ID = fmt.Sprintf("%d", id)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -88,6 +88,11 @@ 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
|
||||||
|
registerService := service.NewRegisterService()
|
||||||
|
registerHandler := handlers.NewRegisterHandler(logger, registerService)
|
||||||
|
r.Post("/register", registerHandler.Register)
|
||||||
|
|
||||||
// ShortLink authentication - no auth required
|
// ShortLink authentication - no auth required
|
||||||
shortLinkHandler := handlers.NewShortLinkHandler(logger, shortLinkService)
|
shortLinkHandler := handlers.NewShortLinkHandler(logger, shortLinkService)
|
||||||
r.Post("/shortlink", shortLinkHandler.ShortLinkAuth)
|
r.Post("/shortlink", shortLinkHandler.ShortLinkAuth)
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func (s *AuthService) Login(email, password string) (*models.LoginResponse, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, ErrInvalidCredentials
|
return nil, ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify password
|
// Verify password
|
||||||
@@ -104,6 +104,7 @@ func (s *AuthService) Login(email, password string) (*models.LoginResponse, erro
|
|||||||
redirectURL = "/"
|
redirectURL = "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Apakah kita perlu param email untuk generate access token?
|
||||||
// Generate tokens
|
// Generate tokens
|
||||||
accessToken, err := s.jwtManager.GenerateAccessToken(user.ID, user.Email, user.Role, user.Name, additionalClaims)
|
accessToken, err := s.jwtManager.GenerateAccessToken(user.ID, user.Email, user.Role, user.Name, additionalClaims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
117
internal/api/service/register_service.go
Normal file
117
internal/api/service/register_service.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models"
|
||||||
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrEmailExists = errors.New("email already exists")
|
||||||
|
ErrUserNotCreated = errors.New("failed to create user")
|
||||||
|
ErrInvalidRole = errors.New("invalid user role")
|
||||||
|
ErrInvalidPatient = errors.New("invalid patient data")
|
||||||
|
ErrInvalidDoctor = errors.New("invalid doctor data")
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterRequest represents a user registration request
|
||||||
|
type RegisterRequest struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Role string `json:"role"` // "patient", "ref_doctor", "expertise_doctor", or "admin"
|
||||||
|
Patient *models.PatientDetails `json:"patient,omitempty"`
|
||||||
|
Doctor *models.DoctorDetails `json:"doctor,omitempty"`
|
||||||
|
Studies []models.Study `json:"studies,omitempty"` // Study records for patient
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterService handles user registration
|
||||||
|
type RegisterService struct {
|
||||||
|
userRepo *repository.UserRepository
|
||||||
|
patientRepo *repository.PatientRepository
|
||||||
|
doctorRepo *repository.DoctorRepository
|
||||||
|
studyRepo *repository.StudyRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRegisterService creates a new register service
|
||||||
|
func NewRegisterService() *RegisterService {
|
||||||
|
return &RegisterService{
|
||||||
|
userRepo: repository.NewUserRepository(),
|
||||||
|
patientRepo: repository.NewPatientRepository(),
|
||||||
|
doctorRepo: repository.NewDoctorRepository(),
|
||||||
|
studyRepo: repository.NewStudyRepository(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register creates a new user with their associated role-specific data
|
||||||
|
func (s *RegisterService) Register(req *RegisterRequest) (*models.User, error) {
|
||||||
|
// Validate role
|
||||||
|
if req.Role != "patient" && req.Role != "ref_doctor" && req.Role != "expertise_doctor" && req.Role != "admin" {
|
||||||
|
return nil, ErrInvalidRole
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check role-specific data
|
||||||
|
if req.Role == "patient" && (req.Patient == nil || req.Patient.PatientID == "" || req.Patient.DateOfBirth == "") {
|
||||||
|
return nil, ErrInvalidPatient
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.Role == "ref_doctor" || req.Role == "expertise_doctor") && (req.Doctor == nil || req.Doctor.DoctorID == "") {
|
||||||
|
return nil, ErrInvalidDoctor
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if email already exists
|
||||||
|
existingUser, err := s.userRepo.GetUserByEmail(req.Email)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
return nil, fmt.Errorf("error checking existing user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if existingUser != nil {
|
||||||
|
return nil, ErrEmailExists
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user
|
||||||
|
newUser := &models.User{
|
||||||
|
Email: req.Email,
|
||||||
|
Password: req.Password, // Will be hashed in repository
|
||||||
|
Role: req.Role,
|
||||||
|
Name: req.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.userRepo.CreateUser(newUser); err != nil {
|
||||||
|
return nil, fmt.Errorf("error creating user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create role-specific data
|
||||||
|
if req.Role == "patient" {
|
||||||
|
err = s.patientRepo.CreatePatient(req.Patient, newUser.ID)
|
||||||
|
if err != nil {
|
||||||
|
// TODO: Consider rollback user creation if this fails
|
||||||
|
return nil, fmt.Errorf("error creating patient: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create associated study records if provided
|
||||||
|
if len(req.Studies) > 0 {
|
||||||
|
for _, study := range req.Studies {
|
||||||
|
if study.StudyInstanceUID == "" {
|
||||||
|
continue // Skip studies without UIDs
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.studyRepo.CreateStudy(req.Patient.PatientID, study)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error creating study for patient: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if req.Role == "ref_doctor" || req.Role == "expertise_doctor" {
|
||||||
|
err = s.doctorRepo.CreateDoctor(req.Doctor, newUser.ID)
|
||||||
|
if err != nil {
|
||||||
|
// TODO: Consider rollback user creation if this fails
|
||||||
|
return nil, fmt.Errorf("error creating doctor: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newUser, nil
|
||||||
|
}
|
||||||
@@ -7,8 +7,8 @@ POST {{baseUrl}}/auth/login
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"email": "patient",
|
"email": "doctor@example.com",
|
||||||
"password": "patient"
|
"password": "password123"
|
||||||
}
|
}
|
||||||
|
|
||||||
### Login Admin / Exp_doctor (ALL ACCESS)
|
### Login Admin / Exp_doctor (ALL ACCESS)
|
||||||
|
|||||||
151
test/http/register-flow.http
Normal file
151
test/http/register-flow.http
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
### Register new user, patient, and doctor tests
|
||||||
|
|
||||||
|
# Base URL for the API
|
||||||
|
@baseUrl = http://localhost:5555
|
||||||
|
|
||||||
|
|
||||||
|
### Register a new patient with multiple studies
|
||||||
|
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.20180530066",
|
||||||
|
"accession_number": "CR.180530.066",
|
||||||
|
"study_date": "2018-05-30",
|
||||||
|
"study_description": "Chest X-Ray"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"study_instance_uid": "1.2.826.0.1.3680043.9.7307.1.20180713036",
|
||||||
|
"accession_number": "CR.180713.036",
|
||||||
|
"study_date": "2018-07-13",
|
||||||
|
"study_description": "Follow-up X-Ray"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
### Register another patient with one study
|
||||||
|
POST {{baseUrl}}/auth/register
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
### Register another referring doctor
|
||||||
|
POST {{baseUrl}}/auth/register
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "doctor@example.com",
|
||||||
|
"password": "password123",
|
||||||
|
"name": "DR. HERWINDO RIDWAN, SP.OT",
|
||||||
|
"role": "ref_doctor",
|
||||||
|
"doctor": {
|
||||||
|
"doctor_id": "DOC002",
|
||||||
|
"doctor_name": "DR. HERWINDO RIDWAN, SP.OT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
### Register a new expertise doctor
|
||||||
|
POST {{baseUrl}}/auth/register
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "expert@example.com",
|
||||||
|
"password": "expert123",
|
||||||
|
"name": "Dr. Expert Johnson",
|
||||||
|
"role": "expertise_doctor",
|
||||||
|
"doctor": {
|
||||||
|
"doctor_id": "EX456",
|
||||||
|
"doctor_name": "Dr. Expert Johnson"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
### Register another referring doctor
|
||||||
|
POST {{baseUrl}}/auth/register
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "doctor2@example.com",
|
||||||
|
"password": "password123",
|
||||||
|
"name": "Referring^Physician",
|
||||||
|
"role": "ref_doctor",
|
||||||
|
"doctor": {
|
||||||
|
"doctor_id": "DOC003",
|
||||||
|
"doctor_name": "Referring^Physician"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
### Register an admin user
|
||||||
|
POST {{baseUrl}}/auth/register
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "admin@example.com",
|
||||||
|
"password": "admin123",
|
||||||
|
"name": "Admin User",
|
||||||
|
"role": "admin"
|
||||||
|
}
|
||||||
|
|
||||||
|
### Login with registered user
|
||||||
|
POST {{baseUrl}}/auth/login
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "patient1@example.com",
|
||||||
|
"password": "password123"
|
||||||
|
}
|
||||||
|
|
||||||
|
### Login with admin user
|
||||||
|
POST {{baseUrl}}/auth/login
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "admin@example.com",
|
||||||
|
"password": "password123"
|
||||||
|
}
|
||||||
|
|
||||||
|
### Login with patient user
|
||||||
|
POST {{baseUrl}}/auth/login
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "patient2@example.com",
|
||||||
|
"password": "password123"
|
||||||
|
}
|
||||||
|
|
||||||
|
### Login with doctor user
|
||||||
|
POST {{baseUrl}}/auth/login
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "doctor@example.com",
|
||||||
|
"password": "password123"
|
||||||
|
}
|
||||||
@@ -7,8 +7,8 @@ POST http://localhost:5555/auth/login
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"email": "admin",
|
"email": "patient1@example.com",
|
||||||
"password": "admin"
|
"password": "password123"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ POST {{baseUrl}}/auth/login
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"email": "patient",
|
"email": "patient1@example.com",
|
||||||
"password": "patient"
|
"password": "password123"
|
||||||
}
|
}
|
||||||
|
|
||||||
### Refresh TOken
|
### Refresh TOken
|
||||||
@@ -24,7 +24,7 @@ POST {{baseUrl}}/auth/refresh
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"refresh"
|
"refresh_token": {{ refresh_token}}
|
||||||
}
|
}
|
||||||
|
|
||||||
### 2. QIDO-RS: Search for Studies
|
### 2. QIDO-RS: Search for Studies
|
||||||
|
|||||||
Reference in New Issue
Block a user