add: register and login with DB query AND some struct type correction

This commit is contained in:
mario
2025-05-15 09:46:32 +07:00
parent dd4451c2a8
commit c13f834b92
16 changed files with 465 additions and 25 deletions

View File

@@ -69,6 +69,12 @@ go-ohif-proxy/
└── 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
### Prasyarat

View 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)
}

View File

@@ -71,7 +71,7 @@ func Auth(authService *service.AuthService, logger *zap.Logger) func(http.Handle
// Add user info to request context
ctx := context.WithValue(r.Context(), UserIDKey, claims.UserID)
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
ctx = context.WithValue(ctx, ClaimsKey, claims)

View File

@@ -4,6 +4,7 @@ package models
type PatientDetails struct {
PatientID string `json:"patient_id"`
PatientName string `json:"patient_name"`
DateOfBirth string `json:"date_of_birth"` // YYYY-MM-DD format
StudyInstanceUIDs []string `json:"study_instance_uids,omitempty"`
AccessionNumbers []string `json:"accession_numbers,omitempty"`
}

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"time"
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models"
"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
}
// 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
}

View File

@@ -60,3 +60,22 @@ func (r *PatientRepository) GetPatientDetailsByUserID(userID string) (*models.Pa
AccessionNumbers: accessionNumbers,
}, 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
}

View File

@@ -13,7 +13,7 @@ import (
// DBShortLink represents a shortlink from the database
type DBShortLink struct {
ShortlinkID int `db:"ShortlinkID"`
ShortlinKCode string `db:"ShortlinKCode"`
ShortlinkCode string `db:"ShortlinkCode"`
Shortlink_PatientID string `db:"Shortlink_PatientID"`
Shortlink_Study_IUID string `db:"Shortlink_Study_IUID"`
ShortlinkHashDoB string `db:"ShortlinkHashDoB"`
@@ -40,7 +40,7 @@ func NewShortLinkRepository() *ShortLinkRepository {
func (s *DBShortLink) ToShortLink() *models.ShortLink {
return &models.ShortLink{
ID: fmt.Sprintf("%d", s.ShortlinkID),
Token: s.ShortlinKCode,
Token: s.ShortlinkCode,
PatientID: s.Shortlink_PatientID,
StudyUID: s.Shortlink_Study_IUID,
HashedDOB: s.ShortlinkHashDoB,
@@ -56,7 +56,7 @@ func (s *DBShortLink) ToShortLink() *models.ShortLink {
func (r *ShortLinkRepository) GetShortLinkByToken(token string) (*models.ShortLink, error) {
var dbShortLink DBShortLink
query := `SELECT * FROM shortlink WHERE ShortlinKCode = ?`
query := `SELECT * FROM shortlink WHERE ShortlinkCode = ?`
err := database.DB.Get(&dbShortLink, query, token)
if err != nil {
@@ -72,7 +72,7 @@ func (r *ShortLinkRepository) GetShortLinkByToken(token string) (*models.ShortLi
// CreateShortLink stores a new shortlink in the database
func (r *ShortLinkRepository) CreateShortLink(shortLink *models.ShortLink) error {
query := `INSERT INTO shortlink (
ShortlinKCode,
ShortlinkCode,
Shortlink_PatientID,
Shortlink_Study_IUID,
ShortlinkHashDoB,
@@ -118,7 +118,7 @@ func (r *ShortLinkRepository) UpdateShortLink(shortLink *models.ShortLink) error
ShortlinkIsRevoked = ?,
ShortlinkRemainingTries = ?,
ShortlinkExpiredAt = ?
WHERE ShortlinKCode = ?`
WHERE ShortlinkCode = ?`
expiresAt, err := time.Parse(time.RFC3339, shortLink.ExpiresAt)
if err != nil {

View File

@@ -4,18 +4,19 @@ import (
"fmt"
"time"
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models"
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/database"
)
// DBStudy represents a study from the database
type DBStudy struct {
ID int `db:"id"`
Study_PatientID string `db:"Study_PatientID"`
Study_IUID string `db:"Study_IUID"`
Study_AccessionNumber string `db:"Study_AccessionNumber"`
StudyDate time.Time `db:"StudyDate"`
StudyCreatedAt time.Time `db:"StudyCreatedAt"`
StudyUpdatedAt time.Time `db:"StudyUpdatedAt"`
ID int `db:"id"`
Study_PatientID string `db:"Study_PatientID"`
StudyIUID string `db:"StudyIUID"`
StudyAccessionNumber string `db:"StudyAccessionNumber"`
StudyDate time.Time `db:"StudyDate"`
StudyCreatedAt time.Time `db:"StudyCreatedAt"`
StudyUpdatedAt time.Time `db:"StudyUpdatedAt"`
}
// StudyRepository handles database operations related to studies
@@ -45,9 +46,9 @@ func (r *StudyRepository) GetPatientStudies(patientID string) ([]string, []strin
var accessionNumbers []string
for _, study := range studies {
studyUIDs = append(studyUIDs, study.Study_IUID)
if study.Study_AccessionNumber != "" {
accessionNumbers = append(accessionNumbers, study.Study_AccessionNumber)
studyUIDs = append(studyUIDs, study.StudyIUID)
if study.StudyAccessionNumber != "" {
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) {
var study DBStudy
query := `SELECT * FROM study WHERE Study_IUID = ?`
query := `SELECT * FROM study WHERE StudyIUID = ?`
err := database.DB.Get(&study, query, studyUID)
if err != nil {
@@ -67,3 +68,32 @@ func (r *StudyRepository) GetStudyByUID(studyUID string) (*DBStudy, error) {
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
}

View File

@@ -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/database"
"golang.org/x/crypto/bcrypt"
)
// DBUser represents a user from the database
@@ -138,3 +139,30 @@ func (r *UserRepository) RevokeRefreshToken(token string) error {
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
}

View File

@@ -88,6 +88,11 @@ func SetupRouter(cfg *config.Config, logger *zap.Logger) http.Handler {
r.Post("/refresh", authHandler.RefreshToken)
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
shortLinkHandler := handlers.NewShortLinkHandler(logger, shortLinkService)
r.Post("/shortlink", shortLinkHandler.ShortLinkAuth)

View File

@@ -43,7 +43,7 @@ func (s *AuthService) Login(email, password string) (*models.LoginResponse, erro
}
if user == nil {
return nil, ErrInvalidCredentials
return nil, ErrUserNotFound
}
// Verify password
@@ -104,6 +104,7 @@ func (s *AuthService) Login(email, password string) (*models.LoginResponse, erro
redirectURL = "/"
}
// TODO: Apakah kita perlu param email untuk generate access token?
// Generate tokens
accessToken, err := s.jwtManager.GenerateAccessToken(user.ID, user.Email, user.Role, user.Name, additionalClaims)
if err != nil {

View 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
}

View File

@@ -7,8 +7,8 @@ POST {{baseUrl}}/auth/login
Content-Type: application/json
{
"email": "patient",
"password": "patient"
"email": "doctor@example.com",
"password": "password123"
}
### Login Admin / Exp_doctor (ALL ACCESS)

View 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"
}

View File

@@ -7,8 +7,8 @@ POST http://localhost:5555/auth/login
Content-Type: application/json
{
"email": "admin",
"password": "admin"
"email": "patient1@example.com",
"password": "password123"
}

View File

@@ -15,8 +15,8 @@ POST {{baseUrl}}/auth/login
Content-Type: application/json
{
"email": "patient",
"password": "patient"
"email": "patient1@example.com",
"password": "password123"
}
### Refresh TOken
@@ -24,7 +24,7 @@ POST {{baseUrl}}/auth/refresh
Content-Type: application/json
{
"refresh"
"refresh_token": {{ refresh_token}}
}
### 2. QIDO-RS: Search for Studies